Программисту-профессионалу
PC Magazine/RE logo
©СК Пресс 1996
PC Magazine, April 23, 1996, p. 201

Создание клиентов и серверов OLE для дистанционной работы

Ричард Хейл Шоу


(Использование VC++ и VB для создания клиента и сервера Automation)

В предыдущей статье (PC Magazine, Febrary 6, 1996) рассматривались особенности автоматизированных программ клиентов и серверов и их отличия от традиционных контейнеров и серверов OLE объектов. Теперь мы покажем, как создать простой внутренний сервер OLE, использу средства Visual C++, и клиента для него на Visual Basic.

Конечно, для разработки можно поменять роли указанных средств. Уже в версии Visual Basic 3.0 появилась возможность создания клиентов дл атоматизированного управления, а в версии 4.0 предложены средства конструирования аналогичного сервера. (Для VB4 такая необходимость действительно назрела: не столько ради создания самого сервера, сколько для того, чтобы иметь контейнер для размещени средств управления OLE.) С другой стороны, возможность создания и автоматизированных клиентов, и серверов предоставлялась в системе VC++, уже начиная с версии 1.5. Обычно к серверу предъявляются следующие требования: он должен иметь приличное быстродействие и сравнительно небольшую непроизводительную программную "надстройку", в то время как соответствующий клиент должен быть прост в подключении и программировании. В результате, поскольку у каждой системы свои особенности - возможность создания внутренних автоматизированных серверов средствами VC++ (т. е. сохраняемых в виде DLL модулей и, следовательно, загружаемых в адресное пространство вызывающего процесса), а также простота создания автоматизированных клиентов при помощи VB, - представляется естественным использовать оба этих инструмента, каждый в наиболее подходящей для него роли.

Для наглядности предложим простой пример создани средствами VC++ 4.0 внутреннего сервера, выполняющего задачу получения случайных чисел. После его загрузки в память сервер запускает стандартный генератор случайных чисел из библиотеки Cи/Cи++, задав ему в качестве начального значения ("затравки") текущее системное время. Теперь клиент может вызывать имеющиеся у сервера методы, чтобы получить очередное случайное значение или заполнить ими свой буфер (клиент задает их требуемое количество). Кроме того, клиент имеет возможность передать серверу дескриптор какого-то окна, таким образом сервер может поместить случайное число непосредственно в заголовок окна клиента. Клиент, кроме того, может использовать сервер для периодического (каждую секунду) вывода получаемых сервером случайных чисел в некотором другом окне клиента (запуск и окончание этой работы производятся клиентом). Наконец, клиент может передать собственное случайное число серверу, где оно будет использовано в качестве "затравки" для генератора случайных чисел.

Итак, на языке VB4 я составил программу-клиент. При ее работе на экране отображается форма, используема при выполнении перечисленных операций на сервере. Клиент может поместить в окно списка очередное случайное число, полученное от сервера (каждый новый элемент добавляется в конец имеющегося списка); или обратиться к серверу, чтобы либо записать некоторое случайное число в одно из полей редактирования, либо для ежесекундного повторения данной операции в другое поле. Обе эти программы (клиент и сервер) можно загрузить из информационной службы PC Magazine Online (RANDGE.ZIP).

Для начала рассмотрим более простую задачу - как создать автоматизированного клиента средствами VB4; а затем - как создать сервер в VC++ 4.0.

Клиент

Впервые средства создания автоматизированных клиентов появились в Visual Basic, версия 3.0. Делаетс это сравнительно просто, а детали установления связи с сервером искусно скрыты. Процесс создани автоматизированного клиента на языке VB укладывается в основном в следующие три операции:

В нашем примере клиента, подготовленном средствами языка VB, первые две операции выполняются на начальном этапе его работы, а свойства и методы сервера используются позже. Например, если необходима возможность подключения к серверу из любой части программы клиента, описание переменной, выделяемой под объект этого сервера, размещается в разделе (General) (Declarations) программы клиента:

Dim RandGen As Object

С помощью этого предложения дается определение RandGen как переменной с типом Object; она представляет автоматизированный объект, принадлежащий серверу.

Для инициализации объекта используется команда:

Set RandGen = CreateObject ("Randgen.Generator")

Функция CreateObject языка VB предназначена дл создания экземпляра объекта для автоматизированного сервера. В качестве параметра ей передается псевдоним (programmatic ID) этого объекта, или, другими словами, имя, через которое его можно будет найти в системном реестре (System Registry). Как уже упоминалось в предыдущей статье этой серии (OLE's Component Object Model, November 21, 1995), в этом псевдониме OLE объекта будет присутствовать параметр GUID (globally unique ID - уникальный идентификатор глобального уровня), сервера данного объекта. OLE механизм использует значение GUID для поиска в реестре элемента, соответствующего этому серверу, обнаружив его, считывает там маршрут доступа к нему и осуществляет обращение к нему. Таким образом, вызов функции CreateObject приводит к тому, что OLE механизм сначала просматривает реестр с целью поиска элемента, содержащего заданный псевдоним, затем, использу параметр GUID, находит маршрут доступа к серверу и лишь затем обращается к нему, чтобы создать экземпляр требуемого объекта. По завершении этой процедуры функция CreateObject передает в вызывающую программу указатель на интерфейс IDispatch созданного объекта: именно IDispatch является основным OLE интерфейсом механизма автоматизации (Custom Interfaces, Customizable Objects, February 6, 1996). Теперь, когда указатель на интерфейс IDispatch содержится в переменной RandGen, наша программа на языке VB сможет обращаться к любым его параметрам и методам.

Однако учтите: если где то произошла ошибка - сервер не найден, не зарегистрирован в реестре, был перемещен на новое место, что-то неверно сработало при инициализации, не удается создать нужный объект и т. д., - в таких ситуациях функция CreateObject сгенерирует сообщение об ошибке, выводимое средствами VB в окне сообщений. Сразу станет ясно, что попытка сервера создать соответствующий объект провалилась.

Поскольку я планировал, что этот объект будет доступен на протяжении всего времени, пока форма программы клиента находится на экране, то добавил приведенную выше строку в Form_Load клиента - процедуру VB, аналогичную процедуре обработки сообщений WM_INITDIALOG в Windows (или компонентной функции OnInitDialog в VC++/MFC). Созданный один раз объект сервера будет действителен до тех пор, пока Visual Basic не выгрузит его. VB прекратит использование объекта сервера автоматически, как только изменитс содержимое выделенной под этот объект переменной. Поскольку в данном случае назначенная переменная была объявлена глобально на уровне всей программы, объект сервера будет доступен вплоть до окончания работы этой программы VB. Однако с помощью приведенного ниже предложения можно принудительно сбросить данный объект сервера:

Set RandGen = Nothing

В данном случае следует использовать именно ключевое слово Nothing.

Однако учтите: совсем не обязательно, что при этом сервер будет выгружен из памяти. Данное предложение просто сбрасывает вызывавшийся ею объект этого сервера и уменьшает на единицу счетчик обращений к объектам указанного типа в генераторе класса данного объекта на сервере. Причина такой реакции состоит в следующем.

Как вы уже знаете, для механизма OLE необходимо, чтобы каждый сервер создавал для любого из обслуживаемых им типов OLE объектов его генератор класса (class factory). Эти генераторы создают (или воспроизводят) экземпляры объектов определенного типа для этого сервера, причем ведется учет количества созданных объектов. Обслуживая одновременно несколько своих объектов, отдельный экземпляр сервера в состоянии работать сразу с несколькими клиентами (по одному клиенту на каждый объект сервера). Таким образом, каждым клиентом занимается отдельный экземпляр сервера или один сервер может одновременно обслуживать несколько клиентов.

Если каждый клиент обслуживается только своим отдельным экземпляром сервера, то при сбрасывании его объекта будет происходить выгрузка сервера из памяти. Однако что, если некоторый сервер одновременно обслуживает сразу несколько клиентов? В этом случае, когда какой-то клиент выгружает свой объект из памяти, генератор класса указанного объекта уменьшает на единицу счетчик количества обращений. Таким образом, при создании нового объекта генератор класса увеличивает его счетчик на единицу, а при уничтожении - уменьшает на единицу. В результате, в любой момент времени содержимое счетчика отражает фактическое количество обслуживаемых данным сервером объектов такого типа. Выгрузка сервера из памяти будет происходить лишь тогда, когда содержимое всех счетчиков обращений у генераторов классов этого сервера достигнут нуля (или, другими словами, сервер в текущий момент не обслуживает ни одного объекта).

Использование параметров и методов

Теперь, когда вы создали и инициализировали свой экземпляр объекта сервера, вы можете получить доступ к его параметрам и методам. В качестве примера я поместил на создаваемой в программе Visual Basic форме кнопку Command1 и окно списка List1. При нажатии на Command1 выполняется следующая процедура:

Private Sub Command1_Click() temp = "0123456789" RSet temp = Str(RandGen.GetNumber()) List1.AddItem temp If (List1.NewIndex > 11) Then List1.TopIndex = List1.NewIndex -11 Else List1.TopIndex = 0 End If End Sub

Здесь происходит следующее: задается значение локальной переменной temp, вызывается принадлежащий серверу метод GetNumber для получения следующего случайного числа и затем значение заданной переменной добавляется в список окна просмотра List1. Дл обращения к методу GetNumber данного сервера используется следующая конструкция:

RandGen.GetNumber()

где RandGen - имя переменной, выделенной ранее дл объекта сервера, а GetNumber - название конкретного метода. Для обращения к методам и параметрам автоматизированных серверов используются такие же синтаксические конструкции, как для доступа к параметрам и методам обычных объектов в языке, подобном Visual Basic. С помощью функции Str языка VB полученное значение, имеющее тип integer, преобразуется в строку и запоминается в локальной переменной temp. Оператор RSet обеспечивает выравнивание по правому краю строки, содержащейся в этой переменной. Далее содержимое этой переменной просто добавляется как новый элемент в окно просмотра списка:

List1.AddItem temp

Остальная часть процедуры устанавливает позицию окна списка на экране так, чтобы его конечный элемент всегда был виден.

Теперь усложним задачу. Внесем в форму еще две кнопки и поле редактирования; это позволит показать использование двух других методов из арсенала автоматизированного сервера: StartGenerating и StopGenerating. Метод StartGenerating в качестве аргумента получает дескриптор либо окна, либо пол редактирования и запускает на сервере таймер. Сервер запоминает переданный ему дескриптор окна, создает процесс работы таймера и по мере поступления от него сообщений записывает последнее из сгенерированных им случайных чисел в строку заголовка указанного окна (или в строку ввода окна редактирования). Чтобы запустить данный процесс, следует нажать кнопку Command2, и начнется выполнение следующей процедуры:

Private Sub Command2_Click() RandGen.StartGenerating (Text2.hWnd) End Sub

Обратите внимание, что в данном фрагменте при вызове метода сервера StartGenerating ему в качестве аргумента передается дескриптор окна редактирования Text2. (Дл получения новых значений нет особой необходимости передавать дескриптор окна; вместо этого можно воспользоваться имеющимся у клиента интерфейсом автоматизированного объекта и получать через него сведения от сервера. Именно такой метод применяется при работе элементов управления OLE). Сервер будет периодически генерировать случайные числа и записывать их в поле ввода, используя переданный дескриптор окна редактирования. Это будет продолжаться до тех пор, пока программа клиент не вызовет метод StopGenerating:

Private Sub Command4_Click() RandGen.StopGenerating End Sub

По умолчанию, мой вариант сервера генерирует случайные числа ежесекундно (или на языке таймера каждые 1000 мс). Этот сервер предоставляет интервал работы таймера как параметр, и клиент Automation может самостоятельно назначить нужное ему значение. Чтобы воспользоваться такой возможностью, я добавил еще одно окно редактирования Text3; при первой загрузке его значение задается равным параметру TimerInterval данного сервера:

Text3.Text = Str(RandGen.TimerInterval)

Для его замены следует просто набрать в окне ввода новое значение: процедура обработки Change окна Text3, которая вызывается при любой правке в этом поле, изменит нужный параметр сервера соответствующим образом. Проверив условие принадлежности нового значения интервалу от 1 до 32000, процедура Text3_Change установит новый интервал для таймера этого сервера равным значению, введенному пользователем в поле редактирования:

RandGen.TimerInterval = Abs(Val(Text3.Text))

При выполнении этой программы вы увидите, что после изменения данного параметра придется перезапустить таймер, нажав сначала кнопку Stop, а затем Start. Когда вы вводите новое значение интервала, перезапуска таймера сервером не происходит.

Создание сервера Automation средствами VC++

Для разработки автоматизированного клиента средствами Visual Basic потребовалось всего несколько строк программы; решение той же задачи средствами Visual C++ с использованием библиотеки Microsoft Foundation Classes (MFC) существенно сложнее. К счастью, благодаря таким инструментам, как мастера AppWizard и ClassWizard, значительная часть работы сводится к простому выбору мышью нужных элементов управления, а для окончательной настройки сервера нужно будет ввести всего несколько предложений.

Как уже отмечалось, это будет внутренний сервер Automation. То есть он будет работать в том же адресном пространстве, что и процесс клиента, и, следовательно, выполняться как DLL модуль. Для создания внутреннего сервера средствами Visual C++ 4.0 следует просто подготовить фрагмент программы для оболочки DLL. Затем для организации с ним взаимодействия добавить описание интерфейса объекта Automation, указав все его параметры и методы.

Сначала запускаем Visual C++ 4.0 и выбираем в меню File команду New. Появляется диалоговое окно New Project Workspace (Описание среды нового проекта). Выделяем здесь пункт MFC AppWizard (dll) и указываем имя проекта и каталог, где он будет храниться. Хотя VC4 допускает использование длинных имен для названи проекта и файлов исходных текстов, я повторил использованное прежде имя RandGen; таким образом, название файла сервера будет RANDGEN.DLL.

Нажмите кнопку Create (Создать), и произойдет вызов мастера AppWizard. В случае проекта DLL модул требуется только один шаг: задайте тип создаваемого DLL модуля и укажите, необходимо ли подключение механизма автоматизации OLE. В данном случае я выбрал вариант "Regular DLL using shared MFC DLL" (стандартный DLL модуль с разделяемым DLL модулем - MFC). Стандартный DLL модуль это модуль, с которым может работать люба прикладная программа Windows, а не только созданная на базе MFC. Возможен другой вариант - Regular DLL with MFC statically linked (стандартный DLL модуль с постоянной привязкой к MFC), но постоянно привязанные к создаваемому DLL-модулю фрагменты библиотеки MFC значительно увеличат его размер. Выбор первого из названных вариантов (shared MFC DLL) означает, что комплект поставки создаваемого сервера придетс дополнять модулем MFC40.DLL; однако в этом случае размер файла, содержащего сервер, будет сравнительно небольшим (окончательный размер составил 13 Кбайт). Остается еще один вариант, который неприемлем для DLL модулей автоматизированного сервера: расширение DLL (при использовании разделяемой MFC DLL) предназначено для создания DLL модулей с классами, производными от MFC, которые могут применяться в прикладных программах на базе MFC, и только в них.

Кроме того, я выбрал еще одну возможность - автоматизацию OLE, что приводит к добавлению в файл определенного программного кода, необходимого дл формирования автоматизированного сервера. Выбрав все эти параметры, я нажал кнопку Finish (Готово) дл создания файлов проекта и текста программы. Теперь мы имеем обобщенный вариант внутреннего сервера; осталось лишь описать и разработать конкретный объект и его методы и параметры.

Испытаем программу

Пока мы образовали только оболочку нашего автоматизированного сервера, а в программе для DLL модуля еще нет самого объекта. Мастер AppWizard создал лишь единственный класс MFC - CRandGenApp, определив его как производный от класса CWinApp библиотеки MFC. Согласно принятым правилам, в любом EXE или DLL модуле, создаваемом на базе MFC, должен быть предусмотрен класс, производный от CWinApp. В нем должен содержатьс измененный вами вариант CWinApp::InitInstance - виртуальной функции, активизируемой автоматически при первом вызове механизмом MFC данного EXE или DLL модуля. (В случае EXE модулей MFC функци CWinApp::InitInstance вызывается из процедуры WinMain; а если это DLL модули - из DllMain.)

Созданный мастером AppWizard вариант InitInstance содержит всего несколько строк:

BOOL CRandGenApp::InitInstance() { COleObjectFactory::RegisterAll(); return TRUE; }

COleObjectFactory - это особый класс MFC, содержащий генератор класса OLE (OLE class factory); любой из генераторов класса MFC сервера является экземпляром этого исходного класса. При фактическом создании каждого генератора класса COleObjectFactory записывает его адрес в свой внутренний связанный список, так что всегда может получить доступ к любому или сразу ко всем генераторам сервера.

Любой генератор класса запускается функцией COleObjectFactory::RegisterAll, которая и регистрирует его как способного к созданию определенного типа объектов. Будучи статической функцией (то есть доступной глобально и не требующей для своего вызова обращения через некоторый экземпляр класса), RegisterAll просматривает связанный список всех генераторов классов, чтобы зарегистрировать каждый из них. Как только они начнут работать, к ним можно обращаться для создания обслуживаемых ими экземпляров OLE объектов. А поскольку InitInstance вызывается из функции DLLMain внутреннего сервера (при первой загрузке этого DLL модуля), все его генераторы классов с самого начала готовы производить экземпляры предоставляемых этим сервером объектов.

Так как, работая с мастером AppWizard, мы избрали вариант с автоматизацией, появились еще и следующие три функции: DllGetClassObject, DllCanUnloadNow и DllRegisterServer. Все они относятся к типу STDAPI, т. е. подчиняются стандартному соглашению о вызове функций Windows API, не входят в состав ни одного из классов Cи++ и могут вызываться из любой программы Windows (как любая другая функция Windows API).

Первые две функции обеспечивают взаимодействие внутреннего сервера с библиотекой функций OLE Component Object Model. Например, обратившись к функции CoCreateInstance из OLE API, OLE клиент (или контейнер) может создать экземпляр предоставляемого сервером объекта. В свою очередь, эта функция обратится к CoGetClassObject, чтобы получить указатель на интерфейс IUnknown генератора класса требуемого объекта, и затем, используя имеющийся в этом интерфейсе указатель, вызовет IClassFactory::CreateInstance. Эта функци запустит генератор класса, чтобы создать экземпляр запрашиваемого объекта.

DllGetClassObject - это стандартная функция API, которую должен содержать любой внутренний сервер, чтобы обеспечить работу функции CoGetClassObject: если сервер организован в виде DLL модуля, то CoGetClassObject будет вызывать DllGetClassObject. Мастер AppWizard формирует следующий фрагмент программы для этой функции:

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); return AfxDllGetClassObject(rclsid, riid, ppv); } с реализацией вызова внутренней функции MFC - AfxDllGetClassObject. Данная функция просматривает имеющийся на сервере связанный список генераторов объектов (предоставляемый COleObjectFactory) в поиске экземпляра, который позволит получить нужный тип объекта, и передает в вызывающую программу указатель на его интерфейс IUnknown.

Аналогично обстоят дела с функцией DllCanUnloadNow. Время от времени OLE клиент может вызывать функцию OLE API - CoFreeUnusedLibraries, чтобы высвободить ресурсы и выгрузить из памяти неиспользуемые больше DLL модули. Внутри CoFreeUnusedLibraries организуется вызов функции DLLCanUnloadNow, осуществляемый для каждого из имеющихся внутренних серверов; именно поэтому мастер AppWizard добавил экземпляр и этой функции в нашем примере:

STDAPI DllCanUnloadNow(void) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); return AfxDllCanUnloadNow(); }

DllCanUnloadNow обращается к внутренней функции MFC - AfxDllCanUnloadNow. Первым делом из нее вызываетс другая внутренняя функция - AfxOleCanExitApp. Сначала она быстро проверяет, больше ли нуля общее количество объектов, имеющихся у этой прикладной программы или DLL модуля MFC. Если активных объектов нет, в принципе работа данного модуля может быть завершена. В этом случае функция AfxDllCanUnloadNow просматривает содержащийся в COleObjectFactory связанный список, чтобы отыскать хотя бы один задействованный генератор класса (учитывая также генераторы в DLL модулях, загруженных из текущего модуля). Если таковые отсутствуют, функция сообщает, что данный DLL модуль можно выгрузить из памяти. Таким образом, использование функции DllCanUnloadNow служит гарантией того, что внутренний сервер MFC будет выгружен клиентом из памяти, как только он перестанет использовать его объекты или генераторы классов.

Если же внутренний сервер не содержит функции DllCanUnloadNow и не экспортирует ее извне, тогда его DLL модуль будет оставаться в памяти до тех пор, пока клиент не вызовет функцию CoUninitialize; именно ее использует OLE клиент (или контейнер), чтобы отказатьс от OLE библиотеки после закрытия рабочих окон программы и завершения основного цикла обработки сообщений. В случае когда клиент использует несколько серверов, те из них, которые не содержат функцию DllCanUnloadNow, будут оставаться в памяти еще достаточно долго с момента последнего к ним обращения, возможно завладев при этом значительной долей памяти и системных ресурсов. Функция DllCanUnloadNow используется дл того, чтобы выгрузить из памяти внутренний сервер сразу после окончания работы с ним, высвободив тем самым занятые им память и ресурсы.

Теперь подробно рассмотрим третью введенную мастером AppWizard функцию - DllRegisterServer.

Реализация класса сервера

Разработав программу внутреннего сервера обобщенного типа, для реализации предоставляемых сервером объектов можно приступать к дополнению его классами MFC. В нашем примере потребуется лишь единственный класс, предназначенный для обработки сообщений в основном окне (для реакции на сообщения таймера). Я дал определение новому классу как производному от CWnd библиотеки MFC, воспользовавшись мастером Visual C++ ClassWizard. Выбор пал именно на класс CWnd, так как мне необходимо было окно, где будут обрабатываться сообщения типа WM_TIMER. Чтобы создать класс для организации взаимодействия с механизмом, можно использовать любой класс, производный от CCmdTarget библиотеки MFC (CWnd и многие другие).

Для того чтобы новый класс работал с механизмом автоматизации, следует выбрать один из следующих двух вариантов: Automation, предоставляющий таблицу свойств и методов; или Createable by type ID (Создаваемый по идентификатору типа объекта), который, помимо этого, присваивает некоторое имя и параметр GUID дл идентификации объекта в системном реестре. Вариант Createable следует использовать, когда необходимо предусмотреть возможность создания объекта автоматизации внешними средствами, например с помощью функции CreateObject из арсенала Visual Basic. (Вариант Automation пригодится лишь в случае, когда классы, обладающие средствами работы с механизмом автоматизации, будут создаваться только через другой аналогичный объект - например как составная часть иерархии объектов.)

Выбрав вариант Createable, я ввел псевдоним (programmatic ID) для своего объекта - т. е. наименование, по которому его можно будет найти в системном реестре; в данном случае я использовал Randgen.Generator. Этот параметр всегда строится по следующему принципу:

componentname.objectname[.version_number] ИмяМодуля.ИмяОбъекта[.номер_версии]

Постарайтесь сделать его максимально информативным. Учитывая, что DLL модуль с нашим внутренним сервером будет называться RANDGEN.DLL, используя это имя сервера и добавив название объекта, я получил окончательный вариант псевдонима. Номер версии я решил опустить. Завершив работу в данном диалоговом окне, я закрыл его, и мастер ClassWizard создал новый класс.

Здесь начинается самое интересное. Слегка поработав с мышью и кнопками, а также набрав несколько строк вручную, я быстро оснастил свой сервер нужными параметрами и методами. Например, прежде чем приступить к генерированию случайных чисел, придется с помощью функции srand осуществить инициализацию их генератора, принадлежащего библиотеке языка Cи. Я внес обращение к функции srand в конструктор класса CGenerator. В результате при создании любого нового объекта класса CGenerator будет производиться инициализация генератора случайных чисел на основании текущего значени системного времени:

srand((unsigned)time(NULL)); Чтобы обеспечить возможность клиенту, содержащему обращение к конструктору CGenerator, устанавливать собственное начальное значение случайного числа, добавил метод Initialize, использующий аналогичное предложение. Для этого я запустил ClassWizard, переключился на его панель Automation, нажал кнопку Add Method.. (Добавить Метод..), в результате появилось диалоговое окно Add Method. Здесь можно указать название метода (поле External name), имя компонентной функции Cи++, которая будет выполняться при выборе данного метода (поле Internal name), тип n return-значения этого метода и его аргументы, если таковые имеются. В случае с методом Initialize я задал для return-значения тип void и добавил один аргумент с типом long. По окончании этой процедуры мастер ClassWizard внес в определение класса описание этой функции: afx_msg void Initialize (long initializer); и добавил пустое описание для самой функции: void CGenerator::Initialize (long initializer) { } Теперь, чтобы данная функция была готова окончательно, пришлось сделать лишь следующее: добавить обращение к srand, но уже с новым для этой функции аргументом: srand((unsigned)time(NULL)); Кроме этого, ClassWizard внес в таблицу параметров и методов, являющуюся частью определения данного класса, новый элемент: BEGIN_DISPATCH_MAP(CGenerator, CWnd) //{{AFX_DISPATCH_MAP(CGenerator) DISP_FUNCTION(CGenerator, "Initialize", Initialize, VT_EMPTY, VTS_14) ... //}}AFX_DISPATCH_MAP END_ DISPATCH_MAP() С помощью такой карты планировки (dispatch map) дается определение компонентам класса, отвечающим за реализацию конкретных методов и параметров объекта. В результате один раз созданный класс Cи++ становитс самостоятельным объектом нашего сервера.

Завершим разработку

Оставшаяся часть разработки сервера осуществляетс по уже рассмотренной нами схеме: используйте ClassWizard для ввода нового свойства или метода, а затем доведите до конца его реализацию.

Рассмотрим для примера, как добавить метод получени очередного случайного числа. С помощью ClassWizard создал метод GetNumber и затем завершил его разработку:

short CGenerator::GetNumber() { return (short)rand(); } Как и прежде, всю рутинную работу (внесение описани функции в определение класса, обновление карты планировки и подготовку пустого "тела" функции) выполнил мастер ClassWizard. Единственное, что оставалось сделать мне, - вставить в функцию одну строку, как видно выше. Теперь созданный нами средствами Visual Basic клиент при выполнении следующей строки программы в действительности обращается к имеющейся на сервере функции CGenerator::GetNumber: RSet temp = Str(RandGen.GetNumber()) Однако создание процедуры таймера - и реализации таких методов объекта сервера, как StartGenerating и StopGenerating, не касаясь пока параметра TimerInterval, - оказалось существенно сложнее. Во первых, пришлось составить фрагмент программы дл формирования окна, ассоциированного с CGenerator класса, производного от CWnd. CWnd - это базовый класс объектов оконного типа в MFC, поэтому для создани процедуры таймера и приема сообщений VM_TIMER приходится исходить из него или его производных. Пришлось создать и само окно. Для этой цели я внес несколько строк в конструктор класса, обратившись к функции CWnd::CreateEx, унаследованной классом CGenerator из родоначального CWnd. Функция CreateEx создает соответствующее окно, вызвав CreateWindowEx из Windows API, и сохраняет полученный дескриптор окна в CWnd::m_hWnd - элементе данных класса CWnd. Учитыва особенности процесса формирования окна средствами MFC, мне пришлось составить и свой вариант функции CWnd::PreCreateWindow, чтобы создать для MFC иллюзию создания окна потомка, хотя фактически этого не происходит. А вот теперь наступило время настоящей работы: реализация двух методов и одного параметра.

Для того чтобы добавить параметра TimerInterval, выбрал панель Automation мастера ClassWizard и нажал кнопку Add Property... (Добавить Свойство...), чтобы перейти в диалоговое окно с таким же именем. Здесь представлены два варианта реализации нового параметра: через пару принадлежащих функций Get-Set этого класса или через комбинацию какого-либо элемента данных (дл хранения значения этого параметра) и функции реакции на изменение этого параметра. В первом варианте обеспечивается большая степень контроля при считывании или задании клиентом нового значения для параметра; однако в этом случае в классе не выделяется память под новый параметр. Во втором варианте выделение памяти дл параметра предусмотрено, однако вас лишь пассивно предупредят об изменении его значения: функция реакции вызывается механизмом MFC лишь после того, как параметр изменился. Я выбрал второй вариант, и в ответ мастер ClassWizard добавил в определение класса CGenerator следующие описания:

short m_timerInterval; afx_msg void OnTimerIntervalChanged(); а в карту планировки следующую строку: DISP_PROPERTY_NOTIFY(CGenerator, "TimerInterval", m_timerInterval, OnTimerIntervalChanged, VT_12) и еще "пустую" функцию реакции на изменение параметра: void CGenerator::OnTimerIntervalChanged() { } Я добавил в конструктор класса и строку, задающую начальное значение для интервала таймера: m_timerInterval = 1000; Однако фактическое использование этого элемента данных начнется лишь при работе метода StartGenerating. Следует напомнить, что когда автоматизированный клиент обращается к методу сервера StartGenerating, то передает ему в качестве аргумента дескриптор окна (имеющий тип HWND) или дескриптор поля редактирования: RandGen.StartGenerating (Text2.hWnd) Поэтому с помощью мастера ClassWizard я создал метод, где в качестве аргумента указан единственный параметр с типом short, а затем добавил в него необходимые строки: void CGenerator::StartGenerating(short editBoxhWnd) { m_TimerhWnd = (HWND)editBoxhWnd; OnTimer(1); SetTimer(1, m_timerInterval, NULL); } Данная функция сохраняет значение, передаваемое ей в качестве аргумента, в элементе данных m_TimerhWnd, а затем обращается к моему варианту функции CWnd::OnTimer: void CGenerator::OnTimer(UINT nIDEvent) { CWnd* pEditWnd = CWnd::FromHandle(m_TimerhWnd); ASSERT(pEditWnd != NULL); CString temp; temp.Format("%d", rand()); pEditWnd->SetWindowText(temp); CWnd::OnTimer(nIDEvent); } В этом варианте функции OnTimer сначала записываетс некоторое случайное число в локальном строковом объекте (переменной temp, имеющей тип CString). Затем эта строка передается в качестве аргумента при вызове функции CWnd::SetWindowText; при этом используетс указатель на тип CWnd, начальное значение которого устанавливается с помощью ранее сохраненного дескриптора окна. В итоге наша функция OnTimer запишет новое случайное число в том поле редактирования, дескриптор окна которого был передан функции StartGenerating. Вслед за выполнением OnTimer StartGenerating вызывает CWnd::SetTimer. Эта функци создает таймер, который начнет посылать в окно CGenerator сообщения WM_TIMER с частотой, указанной в переменной m_timerInterval: SetTimer(1, m_timerInterval, NULL); Теперь каждый раз, когда обработчик окна CGenerator получает сообщение WM_TIMER, он вызывает нашу функцию OnTimer. Таким образом, CGenerator будет записывать новые случайные значения в указанное окно редактирования вновь и вновь. Этот механизм будет работать до тех пор, пока окно CGenerator не будет уничтожено (что произойдет при выгрузке сервера из памяти) либо пока клиент не обратится к функции сервера StopGenerating, реализованной в CGenerator::StopGenerating: void CGenerator::StopGenerating() { KillTimer(1); } Теперь функция KillTimer удалит подключенный таймер и поступление сообщений WM_TIMER прекратится.

Регистрация сервера

После компиляции программы автоматизированного сервера следует произвести его регистрацию в системе, чтобы он мог быть использован клиентами. Именно здесь понадобится третья из предложенных мастером AppWizard функций - DllRegisterServer. Осуществить регистрацию всех автоматизированных объектов, присутствующих на сервере, может любая программа Windows, обратившаяся к этой функции. В комплект VC++ входит программа REGSVR32, которая загрузит DLL модуль сервера (в данном случае RANDGEN.DLL) и вызовет функцию DllRegisterServer:

STDAPI DllRegisterServer(void) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); COleObjectFactory::UpdateRegistryAll(); return S_OK; } DllRegisterServer обращается к функции COleObjectFactory::UpdateRegistryAll, котора просматривает связанный список имеющихся генераторов класса и сообщает, что каждый из них должен обновить системный реестр для своих объектов.

Достаточно странно, но мастер AppWizard не предоставляет отдельной функции для удаления информации о серверах из реестра. Единственное, что придетс сделать, чтобы восполнить пробел, - это ввести дополнительную функцию DllUnregisterServer, практически идентичную DllRegisterServer:

STDAPI DllUnregisterServer(void) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); COleObjectFactory::UpdateRegistryAll(FALSE); return S_OK; } Отличается она лишь аргументом, передаваемым функции UpdateRegistryAll, благодаря которому каждому генератору класса будет выставлено требование удалить из реестра информацию относительно своих объектов Automation. Не забудьте внести функцию DllUnregisterServer в DEF файл вашего проекта. Теперь при вызове модуля REGSVR32 с параметром /u произойдет запуск данной функции и информация о сервере в реестре будет уничтожена.

Перспективы

Механизм автоматизированнных объектов OLE - это лишь верхушка айсберга, открывающая возможность использования свойств и методов одного объекта другим. Другое поле деятельности - механизм управления OLE - связано с созданием внутренних серверов, способных на практике совместно использовать интерфейс клиента и непосредственно взаимодействовать с ним, предоставл свои внутренние параметры и методы, а также механизм асинхронной передачи информации обратно клиенту. Прочитав эту серию статей, вы смогли убедиться, что благодаря механизму OLE открывается перспектива безграничного наращивания возможностей прикладных программ. Новые версии Windows позволят вам активно использовать механизм OLE и воспользоваться многими новыми возможностями операционной системы, будь то серверы OLE Automation или механизм OLE Controls.