Визуальное программирование и MFC

         

Панель управления


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

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

Панель управления является отдельным ресурсом, таким же как меню или шаблон диалоговой панели. Каждая кнопка панели управления имеет свой идентификатор ID, обычно такой же как и соответствующий пункт меню, который эта кнопка дублирует.

В панели управления можно задавать разделители (SEPARATOR), при помощи которых кнопки разделяются на группы по смыслу. Каждая кнопка и каждый разделитель в панели управления имеют свой собственный индекс. Этот индекс соответствует порядковому номеру, под которым кнопка или разделитель отображаются на экране.



Диалоговая панель управления


Если в панели управления приложения необходимо разместить много разнообразных элементов управления, то значительно удобнее создать эту панель на основе другого класса - класса CDialogBar. Этот класс, так же как и класс CToolBar, наследован от базового класса CControlBar.

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

Приложение может иметь несколько панелей управления, созданных на основе классов CToolBar и CDialogBar.



Панель состояния


В нижней части окна большинства приложений Windows находится строка состояния программы, которая называется панелью состояния. В ней обычно выводится краткая контекстная подсказка для пользователя, содержание которой зависит от того, с каким элементом окна работает пользователь. В панели состояния также может отображаться описание текущего режима приложения, текущее время и т.д.

Для управления панелями состояния в состав библиотеки MFC включен класс CStatusBar. Как и классы CToolBar и CDialogBar, предназначенные для работы с панелями управления, класс CStatusBar также наследуется от базового класса CControlBar.

По умолчанию все приложения с оконным интерфейсом, построенные с помощью средства MFC AppWizard, имеют панель состояния (но можно при создании шаблона приложения и отказаться от этой возможности). В ней отображается текущее состояние приложения или краткая подсказка для выбранных строк меню, а также текущее положение клавиш <Caps Lock>, <Scroll Lock> и <Num>.

К сожалению, нет специальных ресурсов, предназначенных для разработки панелей состояния. Панель состояния создается “вручную”, каждый элемент панели должен быть описан в специальным массиве (создание панели состояния на этапе разработки шаблона приложения осуществляет MFC AppWizard).



Ресурсы клавиш-акселераторов


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

Для создания и изменения таблиц акселераторов следует использовать редактор ресурсов Microsoft Visual C++. Он позволяет определить соответствие комбинаций клавиш и идентификаторов командных сообщений.

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

Например, таблица акселераторов с идентификатором IDR_MAINFRAME загружается многооконным приложением во время создания главного окна методом LoadFrame, вызываемом в методе InitInstance главного класса приложения. Метод LoadFrame также используется для одновременной загрузки и ряда других ресурсов (меню, таблицы текстовых строк, пиктограммы), имеющих одинаковый идентификатор ресурса:

CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME)) return FALSE; pMainFrame->ShowWindow(m_nCmdShow); pMainFrame->UpdateWindow(); m_pMainWnd = pMainFrame;

Каждый тип документа может иметь собственную таблицу акселераторов. Эта таблица будет загружена автоматически вместе с меню (и некоторыми другими ресурсами), когда пользователь откроет окно просмотра документа данного типа.

Чтобы определить таблицу акселераторов для документов данного типа, надо просто включить ее в файл ресурсов приложения, присвоив ей идентификатор данного типа документов (например, IDR_MULTITYPE). Тогда, если в список шаблонов документов приложения при добавлен шаблон документа типа IDR_MULTITYPE, то при создании окна просмотра для документа этого типа будет загружатся соответствующая таблица акселераторов. Шаблон документа добавляется в список шаблонов следующим образом:

AddDocTemplate(new CMultiDocTemplate(IDR_MULTITYPE, RUNTIME_CLASS(CMultiDoc), RUNTIME_CLASS(CChildFrame), RUNTIME_CLASS(CMultiView)));

Если документ создается без использования средств MFC AppWizard и модели “Document-View”, то можно загрузит таблицу акселераторов с помощью метода LoadAccelTable, входящего в состав класса CFrameWnd. В качестве параметра этому методу следует указать имя ресурса таблицы акселераторов. Если таблица акселераторов вместо строкового имени имеет числовой идентификатор, то необходимо воспользоваться макрокомандой MAKEINTRESOURCE.



Обновление пользовательского интерфейса


В MFC реализован специальный механизм для обновления таких объектов интерфейса пользователя, как меню, панели управления и панели состояния. Этот механизм предусматривает передачу приложению команд обновления пользовательского интерфейса (update command user interface). Для обработки этих команд предназначена макрокоманда ON_UPDATE_COMMAND_UI, размещаемая в таблице сообщений класса.

Для каждой строки меню, для каждой кнопки панели управления и для каждого индикатора панели состояния передается отдельное сообщение. Время поступления команд пользовательского интерфейса зависит от самих обновляемых объектов.

Меню

В случае меню команды обновления передаются в момент, когда пользователь открывает меню. Для каждой строки меню посылается отдельное сообщение. В результате для всех строк меню, для которых в таблице сообщений приложения присутствуют макрокоманды ON_UPDATE_COMMAND_UI, вызываются соответствующие обработчики. Они могут изменить состояние меню - заблокировать отдельные строки меню, выделить их специальными символами O

и ·

.

Панели управления и состояния

Для кнопок панели управления и индикаторов панели состояния команды обновления передаются в период “бездействия” приложения, когда очередь сообщений приложения пуста.

Если очередь сообщений приложения пуста, вызывается метод OnIdle главного класса приложения. В приложении можно переопределить этот метод и выполнять какую-либо фоновую работу.

Метод OnIdle определен в классе CWinApp и по умолчанию выполняет обновление пользовательского интерфейса - передает команды обновления для тех кнопок панели управления и индикаторов панелей состояния, которые имеют в таблице сообщений приложения макрокоманду ON_UPDATE_COMMAND_UI. Эта макрокоманда вызывает методы-обработчики, которые могут изментть состояние кнопок и индикаторов панелей управления и панелей состояния.

Как правило, ряд строк меню и кнопок панелей управления имеют одинаковые идентификаторы. В этом случае для обработки команд обновления строк меню и кнопок панели управления вызываются одни и те же методы.

Органы диалоговых панелей управления

Не только меню и панели управления обновляются с использованием механизма обновления. Точно также можно обновить и состояние кнопок и других органов управления диалоговых панелей.



Потоковая многозадачность


Потоковая многозадачность - это одно из важных улучшений Windows, которое служит причиной существенного повышения производительности системы. В современных версиях Windows поддерживается два типа многозадачности.

Первый тип основан на процессах. Такая многозадачность поддерживалась уже с первых версий Windows. Процесс - это программа, или задача, которая выполняется. В многозадачных системах такого типа две и более программы могут выполняться одновременно.

Второй тип многозадачности основан на потоках. Такая многозадачность поддерживается оболочкой Win32 и используется в Windows 95 и Windows NT. Поток - это часть выполняющегося процесса. В Windows 95/NT каждый процесс имеет по крайней мере один поток, но потоков процесса может быть и два, и больше.

В потоковой многозадачности несколько частей одной и той же программы могут выполняться одновременно. Это дает возможность писать чрезвычайно эффективные программы путем разделения их на отдельные исполняемые блоки и управления ходом выполнения всей программы в целом. Для многозадачности такого типа в MFC предусмотрены специальные средства поддержки.

С введением потоковой многозадачности возникла необходимость в специальном механизме, называемом синхронизацией. Синхронизация позволяет контролировать выполнение потоков (и процессов) строго определенным образом. В Win32 для синхронизации выделена целая подсистема. Библиотека классов MFC полностью поддерживает средства многозадачности.



Библиотеки динамической компоновки


С самого рождения (или чуть позже) операционная система Windows использовала библиотеки динамической компоновки DLL (Dynamic Link Library), в которых содержались реализации наиболее часто применяемых функций. Наследники Windows — NT и Windows 95, а также OS/2 — тоже зависят от библиотек DLL в плане обеспечения значительной части их функциональных возможностей.

Рассмотрим ряд аспектов создания и использования библиотек DLL:

как статически подключать библиотеки DLL;

как динамически загружать библиотеки DLL;

как создавать библиотеки DLL;

как создавать расширения МFC библиотек DLL.



Управление памятью


Прежде чем изучать управление памятью в Windows, надо сначала разобраться в том, что такое процесс (process). Программа — это ЕХЕ-файл, который можно запустить из Windows разными способами. После запуска программа становится процессом. У процесса есть своя память, дескрипторы файлов и другие системные ресурсы. Если дважды подряд запустить одну и ту же программу, то получается два отдельных процесса.



Введение в технологии OLE и ActiveX


Хорошую программу написать трудно. Что уж говорить о больших и сложных программах, а таких сегодня большинство. По мере того, как компьютеры все глубже внедряются в жизнь, эффективность и надежность программного обеспечения делаются все важнее. Хорошие программы становятся основой цивилизации.

Можно сказать, что история программирования — это история попыток написать совершенный код. Разработка как прикладного, так и системного программного обеспечения страдала от бесконечных проволочек, а сами программы отличались умопомрачительной сложностью и непредсказуемым количеством “жучков”. И все же без программ не обойтись, но как написать хорошую программу? Для этого нужно обладать способностью соединить общие принципы программного проекта с желанием (и даже горячим стремлением) вникнуть в миллиарды мелочей. Это требует не только колоссальных интеллектуальных усилий, но и соответствующего инструментария, который, увы, все еще далек от совершенства.

ActiveX и OLE фирмы Microsoft — еще один шаг к более совершенным, т.е. более надежным и эффективным, программам. Но не только: “более совершенные” программы должны делать то, что раньше было невозможно, т.е. решать новые проблемы. В основе ActiveX и OLE лежит очень простая идея, но, как оказалось, она позволяет существенно повысить эффективность программирования.



Создание элемента управления ActiveX


OLE-элементы управления создаются в проектах специального типа. Каркас приложения подготавливается при помощи инструментального средства MFC ActiveX ControlWizard (OLE ControlWizard), а затем в него добавляется код, определяющий специфику элемента. На втором этапе широко применяется средство ClassWizard.

Перечислим основные моменты создания OLE-элемента управления:

создание остова проекта;

создания графического образа инструментальной кнопки, задающей элемент управления;

добавление новых свойств;

добавление новых методов;

добавление событий;

корректировка базовой страницы свойств и добавление новых страниц;

связывание данных.



Модель COM/DCOM


Основой как ActiveX, так и OLE является модель многокомпонентных объектов (COM).



COM-cерверы и их клиенты


Рассмотрим различные серверы ActiveX, включая внедряемые серверы и серверы автоматизации.



Главный класс модуля OCX-объекта


Объявление класса

Файл Name.h является основным файлом заголовков для элемента управления Name Control, в нем объявляется класс CNameApp. Этот класс является потомком класса COleControlModule, а не класса CWinApp. Это справедливо обычно для всех элементов управления, построенных на основе MFC.

Класс же COleControlModule библиотеки MFC в свою очередь просто является производным от класса CWinApp, в котором для удобства переопределены методы InitInstance и ExitInstance.

Методы класса

Файл Name.cpp - это основной исходный файл для элемента управления Name Control. Функциями этого файла являются: регистрация элемента управления; обеспечение инициализации элемента управления; удаление регистрации элемента управления, когда он больше не нужен.

В этом файле происходит создание глобального объекта класса CNameApp, порожденного от класса ColeControlModule. В проекте может быть только один объект приложения, т.е. один объект класса, порожденного от COleControlModule.

MFC ActiveX ControlWizard сгенерировал GUID (уникальный идентификатор) для библиотеки типов элемента управления с именем _tlid и принял, что старший номер версии библиотеки равен 1, а младший номер – 0. Эта информация будет записана в реестр во время выполнения макроса INPLEMENT_OLETYPELIB, находящего в NameCtl.cpp.

Файл Name.cpp содержит реализацию методов класса CNameApp. Метод InitInstance отвечает за инициализацию DLL-файла: Он вызывается системой при первой загрузке элемента управления в память. Также этот метод вызывается при создании каждого экземпляра Name Control. В этом методе можно выполнить собственные методы инициализации, однако необходимо всегда в первую очередь вызвать метод InitInstance базового класса COleControlModule.

Метод ExitInstance вызывается непосредственно перед тем, как элемент управления выгружается из памяти, и очищает память, освобождает дескрипторы, удаляет GDI-объекты и т.д:

Регистрация элемента управления

В файле Name.cpp кроме методов класса CNameApp определены две функции: DllRegisterServer (заносит данные в системный реестр) и DllUnregisterServer (удаляет данные из системного реестра):


Элементы управления OLE являются саморегистрирующимися. Это означает, что они способны записывать информацию о себе в системный реестр Windows. Им не нужны ни файлы регистрации (имеющие расширение reg), ни какой-либо другой внешний механизм.

Функция DllRegisterServer реализована во всех OLE-элементах управления. Она представляет собой экспортируемую функцию, которая служит точкой входа при регистрации элемента. Это глобальная функция, вызов которой осуществляют внешние функции или приложения.

При вызове функция DllRegisterServer выполняет следующие действия. Сначала она регистрирует библиотеку типов элемента управления посредством вызова функции AfxOleRegisterTypeLib. (Этот этап включает создание и обновление параметров элемента управления в ключе HKEY_CLASSES_ROOT\TypeLib. Исходный файл библиотеки типов автоматически генерируется MFC ActiveX ControlWizard. В нем описываются свойства, методы, события элемента управления и специфическая для данного класса информация). Затем функция DllRegisterServer регистрирует все фабрики классов данного приложения в системном реестре посредством метода COleObjectFactoryEx::UpdateRegistryAll. ( Фабрика классов представляет собой COM-объект, реализующий интерфейс IClassFactory и отвечающий за производство COM-серверов заданного типа (CLSID). Библиотеки OLE не могут создавать серверы без участия фабрики классов. Понятие фабрики классов рассматривается при исследовании файла NameCtl.cpp.)

При компиляции элемента управления с использованием Microsoft Developer Studio последнее, что делает программа, - это вызывает функцию DllRegisterServer, что приводит к автоматической регистрации элемента управления на данной машине. Если необходимо инсталлировать элемент управления на другой машине, нужно каким-то образом вызвать его функцию DllRegisterServer. Обычно это делает регистрационная программа, которую необходимо предоставить. Зарегистрировать OCX-объект можно и при помощи утилиты regsvr32.exe, которая обычно находится в каталоге C:\WINDOWS\SYSTEM.



Функция DllUnregisterServer является дополнением к функции DllRegisterServer. Элементы управления OLE самостоятельно удаляют свою регистрацию. Это значит, что библиотека типов, фабрика классов и информация об элементе может быть автоматически удалена из системного реестра. Функция DllUnregisterServer не вызывается автоматически из Developer Studio. Ее должны вызывать исключительно внешние функции или приложения, которые хотят полностью удалить элемент из системы. Например, эту функцию могла бы вызвать программа деинсталляции.



Макрос AFX_MANAGE_STATE(p)



Макрос AFX_MANAGE_STATE встречается в функциях DllRegisterServer и DllUnregisterServer. Этот макрос необходим некоторым методам управления OLE для выполнения переключения состояния модуля.

Для начала уточним понятие “состояние модуля”. Во время выполнения MFC-программы библиотека хранит информацию о состоянии приложения. Эта информация включает дескрипторы окон, таблицы связей дескрипторов и MFC-объектов и др. Если выполняется только базовый модуль (DLL-файлы не подгружаются), то может существовать лишь один набор данных о состоянии.

Однако при подключении DLL-файлов, использующих библиотеку MFC (как в случае элементов управления OLE), могут возникать проблемы. Каждый элемент управления сохраняет свою информацию о состоянии. Когда поток выполнения “входит” в элемент управления, последний должен сообщить MFC о том, что наступила его очередь выполняться и следует изменить указатель на набор данных о состоянии так, чтобы он указывал на набор данных элемента управления. Элемент управления делает это с помощью макроса AFX_MANAGE_STATE. Такая переадресация должна происходить во всех точках входа в DLL, то есть во всех функциях, экспортируемых библиотекой, поскольку именно там она получает управление.

Поскольку все элементы управления OLE экспортируют функции DllRegisterServer и DllUnregisterServer, тоне удивительно, что первым оператором в теле обеих функций является макрос AFX_MANAGE_STATE. Если не выполнить изменение состояния, результат работы программы может быть непредсказуемым.Наличие такого макроса в функциях, которые не экспортируются, необязательно.


Главный класс приложения


Главный класс приложения CDlgApp определен в файле dlg.h и реализован в файле dlg.cpp.

Для класса CDlgApp описан конструктор, не имеющий параметров. Этот конструктор используется в момент запуска приложения для создания объекта класса CDlgApp.

Кроме конструктора, в классе CDlgApp переопределяется метод InitInstance базового класса CWinApp. Каждый раз при запуске очередной копии приложения вызывается этот метод. Это единственный метод главного класса, который обязательно должен быть переопределен. Средство MFC AppWizard реализует переопределяемый метод InitInstance для различного типа пользовательского интерфейса (однооконный, многооконный, основанный на диалоге) по-разному.

В данном случае в этом методе создается диалоговая панель, которая и будет выполнять роль пользовательского интерфейса приложения. Для этого сначала создается объект dlg класса CDlgDlg, который управляет диалоговой панелью. Затем адрес этого объекта присваивается элементу данных m_pMainWnd главного класса приложения.

После этого вызывается метод DoModal для объекта dlg класса CDlgDlg. Он создает диалоговую панель и отображает ее на экране. Значение, выдаваемое методом DoModal, можно использовать для того, чтобы проанализировать результат работы пользователя с диалоговой панелью.

Главный объект приложения

В файле dlg.cpp объявляется глобальный объект главного класса приложения. Именно с создания этого объекта и начинается работа приложения.

Объект класса CWinApp (или класса, наследуемого от него) входит во все приложения, созданные с использованием MFC AppWizard, вне зависимости от пользовательского интерфейса этого приложения.

Таблица сообщений класса

Таблица сообщений класса CDlgApp, созданная средством AppWizard, состоит из макрокоманд BEGIN_MESSAGE_MAP и END_MESSAGE_MAP. Между ними располагаются макрокоманды, определяющие сообщения, обрабатываемые данным классом. Изначально в таблице определено только одно командное сообщение, имеющее идентификатор ID_HELP. Для его обработки вызывается метод OnHelp базового класса.

Необработанные сообщения передаются на обработку базовому классу CWinApp, так как он указан во втором параметре макрокоманды BEGIN_MESSAGE_MAP.


Главный класс приложения CSingleApp наследуется от базового класса CWinApp, а его определение и реализация находятся в файлах single.h и single.cpp.

Таблица сообщений класса. В последней строке определения класса CSingleDoc расположена макрокоманда DECLARE_MESSAGE_MAP(). Эта макрокоманда определена в файле afxwin.h, она добавляет к классу несколько элементов.

Так как в классе CSingleApp определена эта макрокоманда, то он может обрабатывать сообщения и имеет таблицу сообщений. Эта таблица расположена в файле single.cpp.

Кроме команды для обработки командного сообщения ID_APP_ABOUT, расположенного в блоке AFX_MSG_MAP, таблица содержит еще 3 макрокоманды, предназначенные для обработки командных сообщений с идентификаторами ID_FILE_NEW, ID_FILE_OPEN, ID_FILE_PRINT_SETUP. Эти команды поступают, когда пользователь выбирает из меню приложения строки с соответствующими идентификаторами. Для обработки этих командных сообщений вызываются методы класса CWinApp.

Главный объект приложения. В приложении создается всего один объект класса CSingleApp. Этот объект определяется как глобальный, поэтому его конструктор получает управление сразу же после запуска приложения:

Конструктор класса. Конструктор класса CSingleApp никаких действий не выполняет и состоит из пустого блока:

Можно в конструкторе класса разместить код для инициализации приложения, однако лучше для этого использовать метод InitInstance.

Метод InitInstance. Этот метод является виртуальным методом класса CWinApp. Когда наследуется главный класс приложения от базового класса CWinApp, этот метод необходимо переопределить.

MFC AppWizard переопределяет метод InitInstance автоматически для приложений с любым пользовательским интерфейсом. Однако реализация этого метода может различаться для приложений с различным типом интерфейса.

После вызова метода Enable3dControls, который позволяет использовать в приложении трехмерные элементы управления, вызывается метод LoadStdProfileSettings. Этот метод загружает файл конфигурации приложения, имеющий расширение .ini.


Главный класс приложения CMultiApp управляет работой всего приложения. Методы этого класса выполняют инициализацию приложения, обработку цикла сообщений и вызываются при завершении приложения.

В приложении определен только один объект главного класса приложения -theApp. Этот объект должен быть один вне зависимости от того, какой интерфейс имеет приложение - однооконный, многооконный или основанный на диалоговой панели.

Конструктор класса. Конструктор класса, созданный MFC AppWizard, не выполняет никаких действий. В нем можно разместить код для инициализации объекта CMultiApp:

Метод InitInstance. Основную часть работы по инициализации приложения выполняет метод InitInstance главного класса приложения. Этот метод отличается от метода InitInstance, который используется (переопределяется) для однооконных приложений.

После вызова метода Enable3dControls, который позволяет использовать в приложении трехмерные элементы управления, вызывается метод LoadStdProfileSettings. Этот метод загружает файл конфигурации приложения, имеющий расширение .ini.

Затем начинается создание шаблона документа. Сначала создается указатель на объект соответствующего класса. Для однооконных приложений это класс CSingleDocTemplate, а для многооконных - CMultiDocTemplate. Новый объект класса создается при помощи оператора new.

Конструктору класса CMultiDocTemplate передается 4 параметра. Первый параметр - nIDResource - определяет идентификатор ресурсов, используемых совместно с типом документов, управляемых шаблоном. К таким ресурсам относятся меню, пиктограмма, строковый ресурс, таблица акселераторов. Для многооконного приложения в этом параметре указан идентификатор IDR_MULTITYPE.

Остальные три параметра - pDocClass, pFrameClass, pViewClass - содержат указатели на объекты класса CRuntimeClass, полученные с помощью макрокоманд RUNTIME_CLASS из классов документа CMultiDoc, дочернего окна MDI CChildFrame и окна просмотра CMultiView. Таким образом, шаблон документа объединяет всю информацию, относящуюся к данному типу документов.



Группы сообщений


Сообщения, которые могут обрабатываться приложением, построенным с использованием библиотеки классов MFC, делятся на 3 группы.

Оконные сообщения

Эта группа включает сообщения, предназначенные для обработки функцией окна. Практически все сообщения, идентификаторы которых начинаются префиксом WM_, за исключением WM_COMMAND,. относятся к этой группе.

Оконные сообщения предназначаются для обработки объектами, представляющими окна. Это могут быть практически любые объекты класса CWnd или классов, наследованных от него (CFrameWnd, CMDIFrameWnd, CMDIChildWnd, CView, CDialog). Характерной чертой этих классов является то, что они включают идентификатор окна.

Большинство этих сообщений имеют параметры, детально характеризующие сообщение.

Сообщения от органов управления

Эта группа включает в себя сообщения WM_COMMAND от дочерних окон (включая окна стандартных классов), передаваемых их родительскому окну. Сообщения от органов управления обрабатываются точно таким же образом, что и оконные сообщения.

Исключение составляет сообщение WM_COMMAND с кодом извещения BN_CLICKED. Это сообщение передается кнопкой, когда пользователь на нее нажимает. Обработка сообщений с кодом извещения BN_CLICKED от органов управления происходит аналогично обработке командных сообщений.

Командные сообщения

Это сообщения WM_COMMAND от меню, кнопок панели управления и клавиш акселераторов. В отличие от оконных сообщений и сообщений от органов управления командные сообщения могут быть обработаны более широким спектром объектов. Эти сообщения обрабатывают не только объекты, представляющие окна, но также объекты классов, представляющих приложение, документы или шаблон документов.

Характерной особенностью командных сообщений является идентификатор. Идентификатор командного сообщения определяет объект, который вырабатывает (посылает) данное сообщение.



Имена, используемые в MFC


Библиотека MFC содержит большое количество классов, структур, констант и т.д. Для того, чтобы текст MFC-приложений был более легким для понимания, принято применять ряд соглашений для используемых имен и комментариев.

Названия всех классов и шаблонов классов библиотеки MFC начинаются с заглавной буквы C. При наследовании классов от классов MFC можно давать им любые имена. Рекомендуется начинать их названия с заглавной буквы C. Это сделает исходный текст приложения более ясным для понимания.

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

Библиотека MFC включает в себя, помимо классов, набор служебных функций. Названия этих функций начинаются с символов Afx, например AfxGetApp. Символы AFX являются сокращением от словосочетания Application FrameworkX, означающих основу приложения, его внутреннее устройство.

Символы AFX встречаются не только в названии функций MFC. Многие константы, макрокоманды и другие символы начинаются с этих символов. В общем случае AFX является признаком, по которому можно определить принадлежность того или иного объекта (функция, переменная, ключевое слово или символ) к библиотеке MFC.

Когда приложение разрабатывается средствами MFC AppWizard и ClassWizard, они размещают в исходном тексте приложения комментарии следующего вида:

//{{AFX_ ... //}}AFX_

Такие комментарии образуют блок кода программы, который управляется только средствами MFC AppWizard и ClassWizard. Пользователь не должен вручную вносить изменения в этом блоке. Для этого необходимо употреблять средства ClassWizard.

В следующей таблице представлено краткое описание некоторых блоков //{{AFX_:

Блок

Описание

//{{AFX_DATA

//}}AFX_DATA

Включает объявление элементов данных класса. Используется в описании классов диалоговых панелей.

//{{AFX_DATA_INIT

//}}AFX_DATA_INIT

Включает инициализацию элементов данных класса. Используется в файле реализации классов диалоговых панелей.

//{{AFX_DATA_MAP

//}}AFX_DATA_MAP

Включает макрокоманды DDX, предназначенные для связывания элементов данных класса и органов управления диалоговых панелей. Используется в файле реализации классов диалоговых панелей.

//{{AFX_MSG

//}}AFX_MSG

Включает описание методов, которые предназначены для обработки сообщений. Этот блок используется при описании класса.

//{{AFX_MSG_MAP

//}}AFX_MSG_MAP

Включает макрокоманды таблицы сообщений класса. Используются совместно с AFX_MSG.

//{{AFX_VIRTUAL

//}}AFX_VIRTUAL

Включает описание переопределенных виртуальных методов класса. Блок AFX_VIRTUAL используется при описании класса.

MFC AppWizard и ClassWizard помогают разрабатывать приложения. Они создают все классы и методы, необходимые для его работы. Программисту остается дописать к ним свой код. В тех местах, где можно вставить свой код, MFC AppWizard и ClassWizard, как правило помещают комментарии:

//TODO:



Инициализация нового документа


Для инициализации нового документа в методе OnNewDocument класса документа произвести

вызов метод CDocument::OnNewDocument базового класса для полной подготовки к работе с новым документом;

инициализацию необходимых данных нового документа;

выделение необходимой памяти для работы над документом.

Для очистки содержимого документа при инициализации нового документа в классе документа необходимо переопределить метод DeleteContents базового класса CDocument (этот метод вызывается методом CDocument::OnNewDocument - обработчиком открытия нового документа). Для этого в определении класса документа необходимо объявить виртуальный метод DeleteContents, а в реализации класса его определить. Работа метода DeleteContents должна заключаться в следующем:

необходимо освободить всю память, выделяемую в процессе работы над предыдущим документом;

вызвать метод CDocument::DeleteContents базового класса для полной очистки документа



Инициализация объектов СОМ


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

В СОМ клиент обычно приказывает вновь созданному объекту инициализировать самого себя. Чтобы это стало возможно, данные объекта должны быть перманентно сохранены, т.е. объект должен быть способен сохранить свои данные на время своей неактивности. Одним из очевидных мест хранения перманентных данных объектов является файл на диске.

Первый интерфейс, запрашиваемый клиентом при создании объекта, обычно является одним из тех, что содержат функцию инициализации объекта. Для этой цели предназначены стандартные интерфейсы IPersistFile, IPersistStorage и IPersistStream. Каждый из них содержит методы, позволяющие клиенту приказать объекту загрузить перманентные данные последнего (т.е. выполнить самоинициализацию). Эти интерфейсы отнюдь не являются единственными, с чьей помощью можно выполнить инициализацию, однако все они применяются очень широко.



Интерфейсы


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

Идентификация интерфейса

У каждого интерфейса СОМ два имени. Одно из них предназначено для людей — строка символов. Другое имя сложнее — оно предназначено для использования в основном программным обеспечением. Легко воспринимаемое человеком символьное имя не является гарантированно уникальным — допускается (хотя это и не распространенная практика), чтобы это имя было одинаковым у двух интерфейсов. Имя же, используемое программами, уникально — это позволяет точно идентифицировать интерфейс.

По соглашению читабельные имена большинства СОМ-интерфейсов начинаются с буквы I (от interface). Различные технологии, основанные на СОМ, определяют интерфейсы с разными именами, но все они обычно начинаются с буквы I и пытаются хотя бы немного описать назначение интерфейса. Например, интерфейс корректировщика орфографии, описанный выше, мог бы называться ISpellChecker, а интерфейс словаря синонимов — IThesaurus.

Простые дружественные имена, вроде приведенных выше, удобны при упоминании интерфейса в разговоре или при выборе имен переменных и типов для указателей интерфейсов. Однако они не годятся, когда клиент должен точно указать, какой именно интерфейс объекта ему нужен. Например, если две группы независимо разрабатывают два разных интерфейса, может случиться, что для обоих интерфейсов будет выбрано имя ISpellChecker. Клиент, знающий только об одном из этих интерфейсов и запрашивающий у некоторого объекта указатель ISpellChecker, может получить неверный указатель, если объектом реализован в действительности другой интерфейс.
А если объекту нужно реализовать оба интерфейса, его клиенты окажутся в затруднительном положении, так как не будут в точности знать, указатель какого интерфейса они получат, запрашивая ISpellChecker.

Выход прост: создатель любого интерфейса должен присвоить ему уникальное имя — глобально уникальный идентификатор (globally unique identifier — GUID). GUID интерфейса называется идентификатором интерфейса (interface identifier — IID). GUID — это 16-байтовая величина, обычно генерируемая программой-утилитой. Каждый может запустить такую утилиту на любом компьютере и гарантированно (со всех практических точек зрения) получит GUID, который будет отличаться от всех остальных.

Проблема сводится к тому, чтобы гарантировать уникальность каждого GUID во времени и пространстве. Уникальность во времени достичигается за счет того, что каждый GUID содержит метку времени, указывающую, когда он был создан, что гарантирует отличие друг от друга всех GUID, сгенерированных на данной машине. Для обеспечения уникальности в пространстве у каждого компьютера, который может быть использован для генерации GUID, должен быть уникальный идентификатор. В качестве такого идентификатора программа генерации GUID использует уникальное значение, уже имеющееся на большинстве компьютеров: адрес платы сетевого интерфейса. Если в компьютере не установлен сетевой адаптер, то из различных случайных характеристик данной системы генерируется фиктивный идентификатор машины. Но и тогда маловероятно, что идентификаторы двух машин окажутся одинаковыми.

Хотя человеку и трудно работать с GUID, последние отлично подходят для назначения гарантированно уникальных имен интерфейсов — тех, что используются программами. Люди обычно выбирают для указания интерфейсов простые читабельные имена, а не GUID. Не стоит забывайть, однако, что у каждого интерфейса фактически два имени — читабельное и IID (который, конечно, по сути GUID) и что скомпилированное и работающее программное обеспечение практически всегда пользуется последним.





Спецификация интерфейса



Объект и клиент должны иметь заранее согласованный способ описания интерфейса, т.е. способ определения методов, из которых состоит интерфейс, а также параметров этих методов. СОМ не предписывает, как это должно быть сделано. СОМ-объект может описывать свои интерфейсы с помощью чистого C++, или некоторого псевдо-С++, или каким-то еще способом, который может быть согласован между создателем объекта и создателями его клиентов. Важно другое: СОМ-объект обязан точно следовать стандарту двоичного интерфейса СОМ (о нем будет рассказано далее).

И все же для определения интерфейсов удобно иметь стандартный инструмент. В СОМ такой инструмент есть — язык описания интерфейсов (Interface Definition Language — IDL). IDL СОМ является в значительной степени расширением IDL Microsoft RPC, а тот в свою очередь заимствован из IDL OSF DCE (Open Software Foundation Distributed Computing Environ- j ment). С помощью IDL можно составить полную и точную спецификацию интерфейсов объекта СОМ. Например, ниже приведена спецификация на IDL для гипотетического интерфейса корректировщика орфографии ISpellChecker:

[ object, uuid(E7CDODOO-1827-11CF-9946-444553540000) ] interface ISpellChecker : Unknown { import "unknwn.idi" HRESULT LookUpWord([in ] OLECHAR word[31], [out] boolean * found); HRESULT AddToDictionary([in] OLECHAR word[31]): HRESULT RemoveFromOictionary([in] OLECHAR word[31]); }

Как можно заметить, IDL очень похож на C++. Спецификация интерфейса начинается со слова object, указывающего, что будут использоваться расширения, добавленные СОМ к оригинальному IDL DCE. Далее следует IID интерфейса — некоторый GUID. В DCE, откуда была заимствована эта идея, GUID называется универсально уникальным идентификатором (universal unique identifier — UUID). Так как в основе IDL СОМ лежит IDL DCE, то в описании интерфейса используется термин UUID. Иначе говоря: UUID — всего лишь другое имя для GUID.

Далее идет имя интерфейса — ISpellChecker, за ним — двоеточие и имя другого интерфейса — IUnknown.


Такая запись указывает, что ISpellChecker наследует все методы, определенные для IUnknown, т.е. клиент, у которого есть указатель на ISpellChecker, может также вызывать и методы IUnknown. IUnknown, как будет рассказано далее, является критически важным интерфейсом для СОМ, и все остальные интерфейсы наследуют от него. (Как было объяснено в предыдущей лекции, СОМ поддерживает только наследование интерфейса, но не наследование реализации. Хотя объект СОМ волен при определении своего интерфейса наследовать от любого из существующих интерфейсов, этот новый объект унаследует только само определение, но не реализацию существующего интерфейса. На практике наследование интерфейсов используется в СОМ нечасто. Вместо этого объект обычно поддерживает каждый необходимый ему интерфейс по отдельности (кроме lUnknown, от которого наследуют все интерфейсы). В отличие от C++ СОМ поддерживает лишь одиночное наследование, позволяя интерфейсу наследовать только от одного предка. Множественное наследование, т.е. наследование от нескольких интерфейсов одновременно, не поддерживается. Программисты на C++ могут свободно применять множественное наследование C++ для реализации объектов СОМ, однако оно не может быть использовано для спецификации интерфейсов на IDL.)

Далее в спецификации интерфейса идет оператор import. Так как данный интерфейс наследует от IUnknown, то некоторой программе, читающей определение интерфейса, может потребоваться найти описание IDL для IUnknown. Оператор import указывает такой программе, какой файл содержит нужное описание.

Вслед за оператором import в описании интерфейса идут три метода: LookUpWord, AddToDictionary и RemoveFromDictio-nary, — а также их параметры. Все три возвращают HRESULT — стандартное возвращаемое значение, указывающее, был ли вызов обработан успешно. Параметры в IDL могут быть сколь угодно сложными, использовать такие типы, как структуры и массивы, являющиеся производными своих аналогов в C++. Каждый параметр помечен [in] или [out]. Значения параметров [in] передаются при вызове метода от клиента объекту, тогда как значения [out] передаются в обратном направлении. (Параметры, значения которых передаются в обоих направлениях, могут быть помечены как [in, out].) Подобные метки могут помочь читателю лучше понять интерфейс, однако их основное назначение в том, чтобы программа, обрабатывающая спецификацию интерфейса, точно определила, какие данные и в каком направлении копировать.



Подобная простая спецификация — все, что необходимо для заключения контракта между объектом СОМ и его клиентом. Хотя интерфейс и не обязан задаваться именно таким способом, но следование ему может значительно облегчить жизнь разработчика. Кроме того, неплохо иметь одну стандартную схему для спецификации интерфейсов объектов СОМ.

Обратите внимание на одно важное обстоятельство, связанное со спецификациями интерфейса: после того как интерфейс опубликован и начал где-то работать, после того как его задействовали в выпущенной версии какого-либо программного обеспечения, изменять его, по правилам СОМ, нельзя. Если создатель интерфейса хочет добавить новый метод, изменить список параметров метода или внести какие-либо другие изменения, это также недопустимо. Интерфейсы должны быть фиксированными.



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



Реализация интерфейса



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

Клиентский указатель интерфейса фактически является указателем на указатель внутри объекта. Последний в свою очередь указывает на таблицу, содержащую другие указатели.


Эта виртуальная таблица (viable) содержит указатели на все методы интерфейса.



Следует заметить: методы ISpellChecker изображены на рисунке как элементы 4, 5 и 6 виртуальной таблицы. Что представляют собой первые три метода? Это методы, определенные интерфейсом IUnknown. Поскольку ISpellChecker наследует от IUnknown, у клиента должна быть возможность вызова методов lUnknown через указатель на ISpellChecker. Чтобы это стало возможным, виртуальная таблица ISpellChecker должна содержать указатели на эти три метода. Фактически, так как каждый интерфейс наследует от IUnknown, виртуальная таблица любого СОМ-ин-терфейса начинается с указателей на три метода IUnknown. Подобная двоичная структура имеет место для всех интерфейсов, поддерживаемых любым объектом.



Формат интерфейса СОМ моделирует структуру данных, генерируемую компилятором C++ для класса этого языка (класс задает тип объектов, а двоичная структура генерируется для объектов класса.). Это сходство означает, что СОМ-объекты очень легко создавать на C++. Хотя СОМ-объекты можно писать на любом языке, поддерживающем описанные стандартные двоичные структуры, в СОМ, честно говоря, имеется некоторый уклон в сторону реализации на C++ (что не должно удивлять, если учесть популярность C++).



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


Использование CoCreatelnstance


Независимо от того, где исполняется объект, клиент обычно создает его и затем получает указатели на необходимые интерфейс. Для большинства описанных ранее объектов — реализованных сервером "в процессе" или локальным сервером — это можно сделать, вызвав CoCreateInstance, а затем с помощью QueryInterface запросить указатели на нужные интерфейсы. Клиент может создать объект на удаленной машине, вызвав ту же самую функцию, т.е. клиенту даже не требуется знать, что объект выполняется на другом компьютере. Чтобы создать удаленный объект, клиент вызывает CoCreateInstance, как обычно, передавая CLSID вместе с IID, указывающим первый интерфейс, указатель на который ему нужен.

Однако для удаленного объекта необходимо задать дополнительный элемент — машину, на которой он должен быть создан. Уже упоминалось, что для объекта, создаваемого на той же машине, системный реестр отображает CLSID в имя DLL или исполняемого файла, который должен быть загружен для данного класса. А при создании объекта на удаленной машине системный реестр может отображать CLS1D в имя машины, на которой этот объект должен создаваться. Для создания удаленного объекта устанавливается связь с удаленной машиной, в ее реестре отыскивается данный CLSID, и на этой удаленной машине запускается соответствующий сервер. Если удаленный объект реализован в DLL, то запускается суррогатный процесс, просто загружающий DLL (данная возможность не поддерживается в первой версии DCOM, но ее планируется реализовать как можно быстрее.). Иначе запускается процесс объекта, как и в случае локального сервера.

Рассмотрим несколько упрощенную картину создания удаленного объекта с помощью CoCreateInstance. Клиент вызывает библиотеку СОМ для создания объекта с CLSID X, запрашивая указатель на интерфейс А этого объекта. Запись в реестре для CLSID X на клиентской машине содержит имя другого компьютера. DCOM предоставляет несколько вариантов идентификации удаленных машин в зависимости от сетевых протоколов, применяемых для доступа к удаленной системе.
DCOM поддерживает доменные имена, используемые TCP/IP (типа elvis.acme.corn), а также адреса IP (Internet Protocol), имена NetBIOS и имена, применяемые NetWare IPX/SPX. Независимо от способа идентификации устанавливается связь с удаленной машиной, и там создается объект с учетом информации о CLSID Х реестра удаленной машины. Удаленная машина запустит сервер, а затем попросит его фабрику класса создать объект и вернуть указатель на интерфейс А. Этот указатель далее возвращается клиенту как обычно. Для клиента все выглядит аналогично процессу создания нового объекта локально.

Как уже упоминалось, CoCreateInstance вызывает CoGetClass Object, чтобы получить фабрику данного класса, а затем вызывает метод этой фабрики IClassFactory::CreateInstance для создания объекта на локальной машине (см. раздел "Создание нескольких объектов одного класса: фабрики классов"). Подобный процесс применяется и при создании объекта на удаленной машине, хотя бы с точки зрения программиста. На самом же деле этот процесс был оптимизирован для повышения производительности, и все эти действия выполняются за один цикл взаимодействия "запрос-ответ" с удаленной машиной.




Использование CoCreatelnstanceEx


Применение CoCreateInstance для создания удаленного объекта не всегда наилучший вариант. Сколь велико ни было бы быстродействие сети, доступ к объекту на удаленной машине всегда будет медленнее, чем доступ к объекту на локальной машине. Даже для высокоскоростной сети лучше максимально сократить объем пересылаемых по ней данных. Таким образом, обеспечение производительности, удовлетворяющей пользователей (и администраторов сетей), требует минимизации количества запросов, необходимых для подготовки к использованию удаленного объекта. Важно при этом избежать излишних вызовов QueryInterface для удаленного объекта.

С этой целью DCOM предоставляет функцию CoCreateInstanсеЕх, альтернативную CoCreateInstance. Как и CoCreateInstance, CoCreateInstanceEx позволяет клиенту задать CLSID класса объекта, который он хочет запустить. Но если СоСrеаteInstance допускает указание только одного IID, задающего первый нужный интерфейс, то CoCreateInstanceEx дает клиенту возможность задать список IID. После запуска объекта CoCreatelnstanceEx запрашивает у него указатели на все интерфейсы из этого списка и возвращает их клиенту одновременно. Вместо того, чтобы заставлять клиент многократно вызывать QueryInterface для получения указателей на интерфейсы объекта, одновременный возврат всех этих указателей может значительно ускорить процесс. И хотя CoCreatelnstanceEx создана для работы с удаленными объектами, нет причин, по которым клиенты не могли бы использовать ее для эффективного создания экземпляров объектов, реализованных локальными серверами и серверами "в процессе".

CoCreateInstanceEx также имеет параметр, позволяющий клиенту указать машину, на которой должен быть создан объект. Вместо того, чтобы полагаться в определении удаленной системы на локальный реестр, клиент может динамически выбирать удаленную машину во время создания объекта. Имя машины задается, как и в предыдущем случае, —доменным именем, адресом IP или в другом формате, поддерживаемом сетевыми протоколами. Поскольку CoCreateInstanceEx, как и CoCreateInstance, использует CoGetClassObject для получения указателя на интерфейс соответствующей фабрики класса, постольку имя машины необходимо передать при вызове CoGetClassObject. Для этого используется зарезервированный ранее параметр, так что необходимость в новой функции CoGetClassObjectEx отпадает.



Использование DLL


Практически невозможно создать приложение Windows, в котором не использовались бы библиотеки DLL. В DLL содержатся все функции Win32 API и несчетное количество других функций операционных систем Win32.

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

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

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



Использование готовых компонентов и элементов управления


Developer Studio позволяет в полной мере взаимодействовать с миром OLE-объектов и реализовывать все виды работ с OLE-объектами. Рассмотрим подробнее использование элементов управления ActiveX. Их поведение и использование аналогичны элементам управления Windows. Можно использовать как готовый, созданный кем-либо OLE-элемент управления, так и создавать собственные для последующего его использования в других приложениях.



Использование критических секций


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

int g_count; // глобальная переменная …… UINT MyThread(LPVOID pParam) { g_count=0; while(g_count++<100) { // здесь выполняются какие-то действия } return 0; }

Однако, здесь есть одна проблема, которую можно обнаружить, посмотрев сгенерированный ассемблерный код. Значение g_count загружается в регистр, увеличивается там и переписывается обратно в g_count. Предположим, g_count равно 40 и Windows прерывает выполнение рабочего потока сразу после того, как он загружает это значение в регистр. Пусть теперь управление получает основной поток и присваивает переменной g_count значение 100 (ожидая, что рабочий поток вслед за этим прекрарит свою работу). При возобновлении рабочий поток увеличивает значение регистра и записывает обратно в g_count число 41, стирая при этом предыдущее значение 100. Итог – цикл рабочего потока не завершился. Допустим, процедура потока модифицируется следующим образом:

…. g_count=0; while(g_count<100) { // здесь выполняются какие-то действия g_count++; } ….

Теперь основной поток сможет завершить рабочий, так как операция ++ увеличивает g_count непосредственно в памяти, используя значение, которое мог сохранить основной поток. Выполнение машинной команды не может быть прервано другим потоком. Но при включении оптимизации компиляции возникает новая проблема. Компилятор использовал бы для g_count регистр и этот регистр оставался бы загруженным на протяжении всей работы цикла.
Изменение основным потоком значения g_count в памяти никак бы не сказалось на цикле вычислений основного потока. Одним из способов решения такой проблемы является объявление переменной g_count как volatile, что гарантирует, что счетчик не будет храниться в регистре, а будет заново загружаться туда всякий раз при обращении к нему.

Однако есть и другой способ корректного использования разделяемых переменных. Если необходимо, чтобы несколько потоков использовали глобальные переменные или другие разделяемые ресурсы, то для этого подходит такое средство синхронизации как критические секции. События хорошо подходят для “сигнализации”, а критические секции (разделы или секции кода, требующие монопольного доступа к разделяемым данным) удобны для управления доступом к данным.

Допустим, программа отслеживает показания времени как часы, минуты и секунды, а каждое из этих значений хранится в отдельной целочисленной переменной. Теперь представим, что значения времени совместно используются двумя потоками. Поток А изменяет значение времени и прерывается потоком Б после обновления часов, но до обновления минут и секунд. Результат: поток Б получает недостоверные показания времени.

Если для данного формата времени создается класс C++, то можно легко управлять доступом к данным, сделав элементы данных закрытыми и предусмотрев открытые функции-члены. Именно таков класс CHMS, рассматриваемый ниже. Следует отметить: в этом классе есть элемент данных типа CRITICAL_SECTION. (Здесь не применяется поддержка из MFC.) Конструктор вызывает Win32-функцию InitializeCriticalSection, а деструктор — DeleteCriticalSection. Таким образом, с каждым объектом CHMS связан объект критическая секция.

#include "stdafx.h" class CHMS { private: int m_nHr, m_nMn, m_nSc; CRITICAL_SECTION in_cs; public: CHMS() : m_nHr(0), m_nMn(0), m_nSc(0) { ::InitializeCriticalSection(&m_cs); } ~CHMS() { ::DeleteCriticalSection(&m_cs); } void SetTime(int nSecs) { ::EnterCriticalSection(&m_cs): m_nSc = nSecs % 60; m_nMn = (nSecs / 60) % 60; m_nHr = nSecs / 3600; ::LeaveCriticalSection(&m_cs); } void GetTotalSecs() { int nTotalSecs; ::EnterCriticalSection(&m_cs); nTotalSecs = m_nHr * 3600 + m_nMn * 60 + m_nSc; ::LeaveCriticalSection(&m_cs); return nTotalSecs; } void IncrementSecs() { ::EnterCriticalSection(&m_cs); SetTime(GetTotalSecs() + 1): ::LeaveCriticalSection(&m_cs); } };

Обратим внимание, что функции-члены вызывают функции EnterCriticalSection и LeaveCriticalSection. Если поток А исполняется в середине SetTime, поток Б будет блокирован вызовом EnterCriticalSection в GetTotalSecs до тех пор, пока поток А не вызовет LeaveCriticalSection. Функция IncrementSecs вызывает SetTime, что означает наличие вложенных критических секций. Это допустимо, так как Windows отслеживает уровни вложения.

Класс CHMS отлично работает, если он используется для конструирования глобальных объектов. Если же потоки программы совместно используют указатели на объекты в куче, то появляется другой спектр проблем. Каждый поток должен определять, не удален ли объект другим потоком, а значит, нужна синхронизация доступа к указателям.


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


Чтобы создать удаленный объект, клиент может воспользоваться и моникером — объектом, который знает, как создать и инициализировать один экземпляр другого объекта. Моникеры могут выполнять создание экземпляров и инициализацию объектов как на локальных, так и на удаленных машинах. Для клиента моникера невидимо, исполняется созданный моникером объект локально или удаленно. Место, где выполняется объект, может быть невидимо и самому моникеру.

Когда клиент вызывает для моникера IMoniker::BindToObject, последний обычно вызывает CoCreateInstance с CLSID, полученным из своих перманентных данных. Затем моникер инициализирует вновь созданный объект, используя информацию своих перманентных данных — например, имя файла. Если для CLSID, передаваемого моникером CoCreateInstance, в реестре указана удаленная машина, то объект будет создан на этой машине. При этом сам моникер не узнает, что он создал удаленный объект.

Но моникер может быть в курсе того, что создает объект на удаленном компьютере. При вызове клиентом метода моникера IMoniker::BindToObject есть вероятность, что перманентное хранилище объекта, указываемого моникером, находится на удаленной машине и что в реестре клиентской машины для класса объекта указано ActivateAtStorage. В этом случае моникер создает объект на той машине, где находится перманентное хранилище объекта, подобно CoGetInstanceFromFile и СоGetInstanceFromIStorage.

Файловый моникер содержит имя файла, которое обычно задает и местонахождение перманентных данных объекта (файл) и класс объекта (определяется по расширению имени файла или, возможно, по содержимому файла). Если данный файл хранится на удаленном файл-сервере, а не на локальной машине и если в реестре локальной машины присутствует ActivateAtStorage, моникер создаст объект на файл-сервере, а не на локальном компьютере.

Моникер URL содержит URL, определяющий местонахождение перманентного хранилища объекта. Если в локальном реестре для класса объекта, идентифицируемого этим моникером URL, задано ActivateAtStorage, то вызов метода моникера IMoniker::BindToObject создаст объект на компьютере, указанном URL, а не на машине, где исполняются моникер и/или его клиент. Затем, моникер приказывает объекту загрузить его перманентные данные по информации, заданной URL. И, подобно CoGetInstanceFromFile и CoGetInstanceFromIStorage, файловый моникер или моникер URL автоматически пытаются создать объект на той же машине, где находится его перманентное хранилище, если в локальном реестре моникером не найдено информации о классе объекта.



Использование нескольких потоков


В программе может быть столько потоков, сколько необходимо. В следующем примере создается сразу два потока выполнения. Первый поток выполняет действия, аналогичные потоку предыдущего примера, второй поток каждые 2 секунды 50 раз выдает звуковой сигнал:

UINT MyThread1(LPVOID pParam); UINT MyThread2(LPVOID pParam); void CExampleView::OnStart() { AfxBeginThread(MyThread1,this); AfxBeginThread(MyThread2,NULL); // параметр не передается } UINT MyThread1(LPVOID pParam) { CExampleView *ptrView=(CExampleView *)pParam;

for(int i=0; i<100; i++) { CDC *dc=ptrView->GetDC(); CRect r; ptrView->GetClientRect(&r); dc->TextOut(rand()%r.Width(),rand()%r.Height(),"*",1); } return 0; } UINT MyThread2(LPVOID pParam) { for(int i=0; i<50; i++) { Sleep(2000); MessageBeep(0); } return 0; }

При работе с несколькими потоками для каждого из них должна быть определена своя потоковая функция и каждый из них должен начинаться отдельно. Все потоки процесса затем функционируют одновременно.



Использование потоков


Потоковая многозадачность дает возможность программисту контролировать выполнение отдельных частей программы. Важно понимать, что все процессы имеют по крайней мере один поток выполнения. Он называется главным, первичным потоком. Но в пределах одного и того же процесса можно создавать несколько потоков. В общем случае, когда новый поток создается, он сразу же начинается выполняться. Таким образом, каждый процесс начинается с одного потока, к которому впоследствии могут добавляться дополнительные потоки. Когда они создаются, родительский процесс начинает выполняться не последовательно, а параллельно.



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


В состав компилятора Microsoft Developer Studio встроены средства, позволяющие программисту облегчить разработку приложений. В первую очередь к ним относятся MFC AppWisard, ClassWizard и редактор ресурсов.

Благодаря MFC AppWizard среда разработчика позволяет быстро создавать шаблоны новых приложений. При этом программисту не приходится писать ни одной строчки кода. Достаточно ответить на ряд вопросов, касающихся того, какое приложение требуется создать, и исходные тексты шаблона приложения вместе с файлами ресурсов готовы. Эти тексты можно оттранслировать и получить готовый загрузочный модуль приложения.

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

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

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



Использование управляющих элементов в приложении


Чтобы использовать в проекте уже созданные OLE-элементы управления, необходимо понимать, как создать проект, допускающий встраивание OCX-объектов, и как добавить в проект эти элементы.

Первая задача решается остаточно просто. Чтобы проект мог использовать OLE-элементы управления, на шаге работы AppWizard, где речь идет о поддержки OLE, следует включить флажок “OLE controls”. Это же свойство можно добавить и в уже созданный, работающий проект. Для этого в начало работы метода InitInstance() главного класса приложения следует вписать вызов AfxEnableControlContainer(). В файл stdafx.h включить строку #include <afxdisp.h>.

О том как добавлять OCX-объекты в проект приложения, было рассказано в пункте “Вставка компонентов в проект приложения”.

Предположим, что создан проект, допускающий использование OCX-объектов, а в галерее компонентов выбраны подходящие OLE-элементы управления, добавленные в проект. Возникает ряд вопросов: как использовать эти объекты и ведут ли они себя так же, как и обычные элементы управления Windows?

Так как сходств здесь больше, чем отличий, то в большинстве случаев можно полагать, что поведение и технология работы с этими элементами управления такие же, как и для обычных элементов управления.

При проектировании диалогового окна в нем, кроме обычных элементов управления, размещаются и OLE-элементы управления. Для них точно также открывается страница свойств, и на этапе проектирования можно задать нужные значения тех или иных свойств элемента. Средство ClassWizard позволяет связать элемент управления с объектом соответствующего класса (который добавляется в проект при встраивании OCX-элемента), а также создать обработчики сообщений для OLE-элемента управления точно так же как и для обычных элементов управления. Итак, работать с OCX-объектами можно точно так же как и с обычными элементами управления Windows.

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

Что касается событий, то в данном контексте речь идет о новом понятии - одной из характеристик OLE-элемента управления. При возникновении некоторой ситуации, чаще всего связанной с действиями пользователя, OCX-объект может “зажечь” событие, вызвав связанную с ним специальную функцию с именем FireNameEvent(), где NameEvent - имя события. Когда объект “зажигает” событие, посылается соответствующее сообщение OLE-контейнеру (содержащему OCX-объект), который может это сообщение обработать. Таким образом, схема взаимодействия получается более сложной: пользователь взаимодействует с OCX-объектом; тот, “зажигая” событие, посылает сообщение контейнеру, который и обрабатывает событие.



IUnknown — фундаментальный интерфейс


Каждый объект СОМ должен поддерживать интерфейс IUnknown — в противном случае он не будет объектом СОМ. IUnknown содержит только три метода: QueryInterface, AddRef и Release. Так как все интерфейсы наследуют от IUnknown, его методы могут быть вызваны через любой из указателей на интерфейсы объекта. Тем не менее IUnknown является отдельным самостоятельным интерфейсом с собственным IID, так что клиент может запросить указатель непосредственно на IUnknown. На диаграммах lUnknown обычно изображается над объектом.

Назначение IUnknown::QueryInterface

Обычно свой первый указатель на интерфейс объекта клиент получает при создании объекта (см. ниже "Создание объектов СОМ"). Имея первый указатель, клиент может получить указатели на другие интерфейсы объекта, методы которых ему необходимо вызывать. Для этого клиент просто запрашивает у объекта эти указатели с помощью IUnknown::QueryInterface.

Чтобы воспользоваться QueryInterface, клиент вызывает его с помощью любого из имеющихся у него в данный момент указателей на интерфейсы объекта. Клиент передает IID нужного ему интерфейса как параметр метода. Например, пусть у клиента уже имеется указатель на интерфейс А, и требуется пс лучить указатель на интерфейс В. Клиент запрашивает данный указатель вызовом QueryInterface через указатель А, задавая в качестве параметра IID интерфейса В (шаг 1). Если объект поддерживает В, то он возвращает указатель на этот интерфейс (шаг 2), и клиент может теперь может вызывать методы В (шаг 3). Если же объект не поддерживает В, он возвращает NULL.

Вообще говоря, самый важный элемент СОМ — QueryInterface. Именно эта простая схема решает очень важную и сложную проблему — контроль версий. Вообразите себе мир, в котором создание программ из СОМ-объектов — обычное дело. Объекты, составляющие такие приложения, создаются множеством организаций, каждая из которых модернизирует свои объекты независимо от остальных. Как все это будет работать, если новые возможности добавляются в разные объекты в разное время? Как установить новую версию объекта с расширенными возможностями, не повредив программам, использующим только старые возможности? И как после модернизации клиента под новые возможности обеспечить автоматическое начало их использования этим клиентом? Ответ на все эти вопросы дает QueryInterface.


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

Теперь допустим, что фирма, продающая этот объект — инструментарий для обработки текста, — решила добавить поддержку словаря синонимов, доступ к которой можно получить че-рез интерфейс IThesaurus. Таким образом, следующая версия объекта поддерживает как ISpellChecker, так и IThesaurus. После того, как установить на машине эту новую версию, все будет работать так же, как и раньше. Текстовый процессор, как обычно, запрашивает указатель на ISpellChecker и успешно пользуется его методами (вспомним, СОМ запрещает изменение интерфейсов.) То, что объект теперь поддерживает еще и IThesaurus, совершенно неизвестно "ограниченному" текстовому процессору, так как он не поддерживает работы со словарем синонимов. Следовательно, старый текстовый процессор никогда не запросит у объекта указатель на этот интерфейс.



Предположим теперь, что на машине установлена новая версия текстового процессора, поддерживающая работу со словарем синонимов. Когда в следующий раз пользователь вызовет старый текстовый процессор, он, как обычно, запустит объект — инструментарий для обработки текста и запросит указатель на интерфейс ISpellChecker. Однако новая версия текстового процессора обладает информацией, достаточной для того, чтобы запросить указатель на IThesaurus. Так как версия объекта, которая поддерживает данный интерфейс, была установлена ранее, нужный указатель будет возвращен, и текстовый процессор сможет воспользоваться новой функцией. Итак, в итоге установлена новая версия инструментария для обработки текста, не нарушающая при этом работы существующих его клиентов, а также обеспечено автоматическое использование этими клиентами функций новой версии, после того как сами клиенты были обновлены!





Ну а как быть тем, кто установил новую версию текстового процессора, но еще не приобрел новую версию инструментария для обработки текста? Все также замечательно работает за исключением того, что текстовый процессор не предоставляет таким пользователям возможностей словаря синонимов. Текстовый процессор запускает объект-инструментарий и через QueryInterface успешно получает указатель на ISpellChecker. Однако, запрашивая указатель на IThesaurus, он получает в ответ NULL. Если текстовый процессор написан с учетом подобной возможности, он отключает пункт меню Thesaurus. Поскольку объект, реализующий IThesaurus, отсутствует, постольку у пользователя не будет доступа к функциям словаря синонимов. Как только пользователь потратится на модернизированный объект — инструментарий для обработки текста, этот пункт меню будет активизирован без каких-либо изменений в текстовом процессоре.

Рассмотрим еще один пример. Что, если создатель объекта — инструментария для обработки текста — пожелает изменить или расширить функциональные возможности объекта по корректировке орфографии? Это влечет за собой изменение или добавление новых методов, которые будут видимы клиенту объекта. Однако СОМ не разрешает изменять интерфейсы, поэтому существующий интерфейс ISpellChecker трогать нельзя. Вместо этого создатель объекта должен определить новый интерфейс, скажем, ISpellChecker2, и включить в него необходимые новые или измененные методы. Объект по-прежнему поддерживает ISpellChecker, но теперь он также будет поддерживать и ISpellChecker2. Добавление в объект поддержки ISpellChecker2 ничем не отличается от добавления поддержки любого нового интерфейса. Как и все СОМ-интерфей-сы, новый имеет уникальный IID, который клиент, знающий о новом интерфейсе, может использовать для запроса указателя через QueryInterface. Как и в предыдущем случае с IThesaurus, клиенты, ничего не знающие о происшедшей модернизации, никогда не запросят указатель на ISpellChecker2, и не ощутят никакого воздействия со стороны изменений — они будут продолжать использовать ISpellChecker, как прежде.





Querylnterface и требование неизменности интерфейсов СОМ позволяют программным компонентам, разрабатываемым независимыми организациями, обновляться по отдельности и тем не менее продолжать нормальную совместную работу. Это соображение трудно считать несущественным, и именно поэтому создатели СОМ иногда называют QueryInterface важнейшим элементом модели.



Подсчет ссылок



Чтобы воспользоваться объектом СОМ, клиент должен явно инициировать начало работы экземпляра этого объекта (как описано ниже в разделе "Создание объектов СОМ"). Здесь возникает естественный вопрос: "Когда завершается работа объекта?" Кажется, очевидное решение — возложить на клиента, запустивший объект на выполнение, еще и обязанность сообщить объекту, когда тот должен остановиться. Однако данное решение не работает, так как данный клиент может со временем оказаться не единственным, кто этот объект использует. Весьма распространена практика, когда клиент запускает выполнение объекта, получает указатели на его интерфейсы и затем передает один из них другому клиенту. Последний может использовать указатель для исполнения методов в том же самом объекте, а также в свою очередь передать указатель другим клиентам. Если бы первый клиент мог "убивать" экземпляр объекта по своему желанию, то положение остальных клиентов было бы незавидным — исчезновение объекта в тот момент, когда его сервисы используются, в лучшем случае огорчительно.

В то время как один объект может использоваться несколькими клиентами одновременно, никто из них не в состоянии узнать, когда все остальные завершатся. Так что разрешить клиенту убивать объект напрямую — небезопасно. Только сам объект может знать, когда он может безопасно завершить свою работу, и только в том случае, если все клиенты сообщают объекту, что они завершили работу с ним. Такой контроль объекты осуществляют с помощью механизма подсчета ссылок (reference counting), поддерживаемого двумя методами интерфейса IDnknown.



Каждый исполняющийся объект поддерживает счетчик ссылок.


Всякий раз, выдав вовне указатель на один из своих интерфейсов, объект увеличивает счетчик ссылок на 1. (Вообще объект может поддерживать отдельные счетчики ссылок для каждого интерфейса.) Если один клиент передает указатель интерфейса другому клиенту, т.е. увеличивает число пользователей объекта без ведома последнего, то клиент, получающий указатель, должен вызвать с помощью этого указателя AddRef. (Для простоты обычно в данном случае говорят "вызвать AddRef для указателя".) В результате объект увеличивает свой счетчик ссылок. Независимо от того, как он получил указатель на интерфейс, клиент всегда обязан вызвать для этого указатель Release, закончив с ним работу. Исполнение этого метода объектом состоит в уменьшении числа ссылок на 1. Обычно объект уничтожает сам себя, когда счетчик ссылок становится равным 0.

Подсчет ссылок может вызывать проблемы. Если не все клиенты следуют правилам, то экземпляр объекта может либо существовать неопределенно долго, либо, что еще хуже, быть преждевременно удаленным. И все-таки подсчет ссылок выглядит единственным работающим способом управления временем жизни объектов в многоликой и динамичной среде, которую позволяет создать СОМ.




Изменение характеристик индикаторов


После того, как при помощи метода SetIndicators индикаторы панели состояния созданы, можно изменить некоторые их характеристики, воспользовавшись методом SetPaneInfo:

void SetPaneInfo( int nIndex, UINT nID, UINT nStyle, int cxWidth );

Параметр nIndex определяет порядковый номер индикатора в панели управления (его индекс). Характеристики этого индикатора будут меняться. Метод SetPaneInfo позволяет изменить расположение индикаторов на панели или даже заменить существующий индикатор на новый. Для этого можно указать новый идентификатор через параметр nID.

Внешний вид индикатора, заданного параметрами nIndex и nID, определяется параметрами nStyle и cxWidth. В качестве nStyle можно указать один или несколько атрибутов, объединенных логической операцией ИЛИ.

SBPS_NOBORDERS - убрать трехмерную рамку вокруг индикатора.

SBPS_POPOUT - обычная рамка вокруг индикатора создает впечатление, что индикатор расположен в углублении. Если указать атрибут SBPS_POPOUT, рамка изменятся таким рбразом, что индикатор будет располагаться выше общего уровня панели состояния.

SBPS_DISABLED - если указать этот атрибут, то в индикаторе не будет отображаться текст из соответствующего строкового ресурса.

SBPS_STRETCH - один из индикаторов панели состояния может менять свой размер в зависимости от размера окна. Атрибут SBPS_STRETCH предназначен для выбора этого индикатора.

SBPS_NORMAL - стандартный индикатор.

Если первый элемент массива идентификаторов, переданного методу SetIndicators, содержит константу ID_SEPARATOR, то для первого индикатора панели состояния по умолчанию устанавливаются атрибуты SBPS_NOBORDERS и SBPS_STRETCH.

Параметр cxWidth определяет ширину индикатора. Когда создается панель состояния и устанавливаются индикаторы при помощи вызова метода SetIndicators, размер индикаторов определяется автоматически исходя из ширины текста индикатора.

Узнать текущие характеристики индикатора можно при помощи метода GetPaneInfo. Он позволяет определить идентификатор, стиль и ширину идндикатора с индексом nIndex.

Если требуется определить или установить только стиль индикатора панели состояния, то вместо методов GetPaneInfo и SetPaneInfo лучше использовать два других метода класса CStatusBar: GetPaneStyle и SetPaneStyle.



Экспортирование функций из динамических расширений


Рассмотрим теперь, как осуществляется экспортирование в приложение функций и классов из динамического расширения. Хотя добавить в DEF-файл все расширенные имена можно и вручную, лучше использовать модификаторы для объявлений экспортируемых классов и функций, такие как AFX_EXT_CLASS и AFX_EXT_API,например:

class AFX_EXT_CLASS CMyClass : public CObject ( // Your class declaration } void AFX_EXT_API MyFunc() ;



Экспортирование функций из DLL


Чтобы приложение могло обращаться к функциям динамической библиотеки, каждая из них должна занимать строку в таблице экспортируемых функций DLL. Есть два способа занести функцию в эту таблицу на этапе компиляции.

Метод __declspec (dllexport)

Можно экспортировать функцию из DLL, поставив в начале ее описания модификатор __declspec (dllexport) . Кроме того, в состав MFC входит несколько макросов, определяющих __declspec (dllexport), в том числе AFX_CLASS_EXPORT, AFX_DATA_EXPORT и AFX_API_EXPORT.

Метод __declspec применяется не так часто, как второй метод, работающий с файлами определения модуля (.def), и позволяет лучше управлять процессом экспортирования.

Файлы определения модуля

Синтаксис файлов с расширением .def в Visual C++ достаточно прямолинеен, главным образом потому, что сложные параметры, использовавшиеся в ранних версиях Windows, в Win32 более не применяются. Как станет ясно из следующего простого примера, .def-файл содержит имя и описание библиотеки, а также список экспортируемых функций:

MyDLL.def LIBRARY “MyDLL” DESCRIPTION ‘MyDLL – пример DLL-библиотеки’

EXPORTS MyFunction @1

В строке экспорта функции можно указать ее порядковый номер, поставив перед ним символ @. Этот номер будет затем использоваться при обращении к GetProcAddress (). На самом деле компилятор присваивает порядковые номера всем экспортируемым объектам. Однако способ, которым он это делает, отчасти непредсказуем, если не присвоить эти номера явно.

В строке экспорта можно использовать параметр NONAME. Он запрещает компилятору включать имя функции в таблицу экспортирования DLL:

MyFunction @1 NONAME

Иногда это позволяет сэкономить много места в файле DLL. Приложения, использующие библитеку импортирования для неявного подключения DLL, не “заметят” разницы, поскоьку при неявном подключении порядковые номера используются автоматически. Приложениям, загружающим библиотеки DLL динамически, потребуется передавать в GetProcAddress порядковый номер, а не имя функции.

При использовании вышеприведенного def-файл описания экспортируемых функций DLL-библиотеки может быть,например, не таким:

#define EXPORT extern “C” __declspec (dllexport) EXPORT int CALLBACK MyFunction(char *str); a таким: extern “C” int CALLBACK MyFunction(char *str);



Экспортирование классов


Создание .def-файла для экспортирования даже простых классов из динамической библиотеки может оказаться довольно сложным делом. Понадобится явно экспортировать каждую функцию, которая может быть использована внешним приложением.

Если взглянуть на реализованный в классе файл распределения памяти, в нем можно заметить некоторые весьма необычные функции. Оказывается, здесь есть неявные конструкторы и деструкторы, функции, объявленные в макросах MFC, в частности _DECLARE_MESSAGE_MAP, а также функции, которые написанные программистом.

Хотя можно экспортировать каждую из этих функций в отдельности, есть более простой способ. Если в объявлении класса воспользоваться макромодификатором AFX_CLASS_EXPORT, компилятор сам позаботится об экспортировании необходимых функций, позволяющих приложению использовать класс, содержащийся в DLL.



Эмуляция


Выше обсуждался метод IUnknown::QueryInterface и его возможности по автоматическому контролю версий. Добавление нового интерфейса к существующему объекту не вызывает проблем у его старых клиентов, так как они никогда не запрашивают указатель на этот интерфейс. Новые же клиенты запрашивают этот указатель, используют сервисы нового интерфейса.

Однако здесь присутствует потенциальная проблема. Предположим, существующий класс заменили другим, поддерживающим все интерфейсы старого, а также и дополнительные. Иначе говоря, новый класс является полиморфным первому: клиент может использовать его так же, как и старый класс. Допустим, однако, что у нового класса другой CLSID. Существующие клиенты написаны так, что создают объекты старого класса, используя старый CLSID. Если последний будет полностью устранен, прежние клиенты не будут работать. Необходимо нечто, позволяющее таким клиентам, не меняясь, использовать новый класс.

Это нечто называется эмуляцией. Идея проста: пусть клиент вызывает CoCreateInstance со старым CLSID, но на самом деле будет создаваться экземпляр нового объекта. Для поддержки этого СОМ предоставляет функцию CoTreatAsClass с двумя параметрами: старым и новым CLSID. После вызова соответствующей функции результатом попыток создания объектов с использованием старого CLSID будет создание объектов с новым CLSID. (Реализация этого вызова обычно осуществляется путем записи отношения эмуляции между двумя CLSID в системный реестр.) Объекты нового класса поддерживают и все интерфейсы старого, поэтому существующие клиенты продолжают работать как прежде.

Данный механизм может применяться и для создания абстрактных компонентов вроде объекта — корректора орфографии, описанного выше. Например, текстовые процессоры могли бы использовать только один CLSID для идентификации объекта — корректора орфографии, поддерживающего стандартизированный интерфейс ISpellChecker. Но так как CLSID в СОМ задает некоторую реализацию интерфейса, то объекты — корректоры орфографии разных производителей будут иметь разные CLSID, хотя оба поддерживают один и тот же интерфейс. В таком случае можно определить стандартный CLSID, который будет просто обозначать "корректор орфографии". Текстовый процессор будет всегда использовать для создания объекта — корректора орфографии именно этот CLSID. Чтобы на данной системе запускался конкретный корректор, с помощью функции CoTreatAsClass задается отображение CLSID абстрактного корректора орфографии в CLSID выбранного объекта-корректора.



Как работает СОМ


В СОМ любая часть программного обеспечения реализует свои сервисы как один или несколько объектов СОМ (не следует путать объекты СОМ с объектами в языках программирования типа C++; несмотря на то, что у них есть общие черты, это разные вещи; далее будет описано соотношение объектов СОМ и объектов других видов.). Каждый такой объект поддерживает один или несколько интерфейсов, состоящих из методов. Метод — это функция или процедура, которая выполняет некоторое действие и может быть вызвана программным обеспечением, использующим данный объект (клиентом объекта). Методы, составляющие каждый из интерфейсов, обычно определенным образом взаимосвязаны. Клиенты могут получить доступ к сервисам объекта СОМ только через вызовы методов интерфейсов объекта — у них нет непосредственного доступа к данным объекта.

Представим себе, например, корректор орфографии, реализованный в виде объекта СОМ. Такой объект может поддерживать интерфейс, включающий методы типа “НайтиСлово” (LookUpWord), “ДобавитьКСловарю” (AddToDictionary) и “УдалитьИзСловаря” (RemoveFromDictionary). Если позднее разработчик объекта СОМ захочет добавить к этому объекту поддержку словаря синонимов, то объекту потребуется еще один интерфейс, возможно, с единственным методом, вроде “НайтиСиноним” (ReturnSynonym). Методы каждого из интерфейсов сообща предоставляют связанные друг с другом сервисы: либо корректировку правописания, либо доступ к словарю синонимов.

Большинство объектов СОМ поддерживают более одного интерфейса. Сам объект всегда реализуется внутри некоторого сервеpa. Сервер может быть либо динамически подключаемой библиотекой (DLL), подгружаемой во время работы приложения, либо отдельным самостоятельным процессом.

Чтобы вызывать методы интерфейса объекта СОМ, клиент должен получить указатель на этот интерфейс. Обычно СОМ-объект предоставляет свои сервисы посредством нескольких интерфейсов, и клиенту требуется отдельный указатель для каждого интерфейса, методы которого он намерен вызывать. Например, клиенту нашего простого объекта СОМ понадобился бы один указатель интерфейса для вызова методов интерфейса корректора орфографии и другой — для вызова методов интерфейса словаря синонимов.




Любой СОМ-объект — это экземпляр определенного класса. Объекты одного класса могут, например, реализовывать сервисы корректировки орфографии и словаря синонимов, тогда как объекты другого класса — представлять банковские счета. Обычно знать класс объекта необходимо для запуска экземпляра этого объекта, выполняемого с помощью библиотеки СОМ. Эта библиотека присутствует на любой системе, поддерживающей СОМ, и имеет доступ к справочнику всех доступных на данной машине классов СОМ-объектов. Клиент может, например, вызвать функцию библиотеки СОМ, передав ей класс нужного ему СОМ-объекта и задав один из поддерживаемых объектом интерфейсов, указатель которого нужен клиенту в первую очередь. (Эти сервисы реализованы библиотекой СОМ в виде обычных вызовов функций, а не через методы интерфейса СОМ.) Затем библиотека СОМ запускает сервер, реализующий объекты данного класса. Кроме того, библиотека возвращает клиенту указатель требуемого интерфейса вновь созданного экземпляра объекта. Далее клиент может запросить указатели на другие необходимые ему интерфейсы непосредственно у самого объекта.

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





Благодаря СОМ, клиентам нет нужды учитывать данные отличия — доступ ко всему осуществляется единообразно. Для доступа к сервисам, предоставляемым любыми типами программного обеспечения, используется одна общая модель.


Как устроена виртуальная память


Естественно, на самом деле никаких сотен гигабайт оперативной памяти в компьютере нет. Нет и сотен гигабайт дискового пространства. Здесь Windows идет на определенные ухищрения. Во-первых, 4-гигабайтное адресное пространство процесса фрагментировано. Программы и элементы данных разбросаны по адресному пространству блоками по 4 Кб, выровненных по границам, кратным 4 Кб. Каждый такой блок называется страницей (page) и может содержать либо код, либо данные. Когда страница действительно используется, она занимает физическую память, но программист никогда не встретится с физическими адресами. Микропроцессорный чип фирмы Intel эффективно преобразует 32-битный виртуальный адрес в номер физической страницы и смещение внутри нее, пользуясь двумя уровнями таблиц 4-килобайтных страниц. Отметим: отдельные страницы могут быть помечены либо как "только для чтения", либо как "для чтения и записи". Кроме того, у каждого процесса свой набор таблиц страниц. Регистр чипа CR3 содержит указатель на страницу каталога — переключаясь с одного процесса на другой, Windows просто обновляет этот регистр.

Страница памяти может быть отмечена в таблице страниц как "присутствующая",что говорит о том, что данная 4-килобайтная страница находится ли сейчас в памяти. При попытке обращения к странице, отсутствующей в памяти, генерируется прерывание, и Windows приступает к анализу ситуации, просматривая свои внутренние таблицы. Если обращение к памяти было неверным, выдается сообщение об ошибке страницы (page fault), и программа завершается. В ином случае Windows считает в оперативную память нужную страницу из дискового файла и обновит таблицу страниц, записав в нее физический адрес и установив бит присутствия. Таковы азы виртуальной памяти в Win32.

Момент чтения и записи страницы (чтобы достичь максимальной производительности) определяет диспетчер виртуальной памяти Windows. Если какой-то процесс не использовал страницу в течение определенного периода и эта память нужна другому процессу, данная страница выгружается из памяти, а вместо нее загружается страница нового процесса.


Все процессы совместно используют один большой общесистемный файл подкачки (swap file), в который помещаются (при необходимости) все виды данных "для чтения и записи" и некоторые виды данных "только для чтения". (Windows NT поддерживает одновременную работу с несколькими файлами подкачки.) Windows определяет размер файла подкачки в зависимости от размера ОЗУ и свободного дискового пространства, но существуют способы тонкой настройки размера и физического расположения этого файла.

Однако файл подкачки — не единственный файл, используемый диспетчером виртуальной памяти. Нет особого смысла в том, чтобы записывать в этот файл страницы кода. Вместо этого Windows проецирует ЕХЕ- и DLL-модули непосредственно на их дисковые файлы. Поскольку страницы кода помечены как "только для чтения", то необходимости в их записи обратно на диск не возникает. Если два процесса используют один и тот же ЕХЕ-файл, то данный файл отображается на адресные пространства обоих процессов. Файлы, проецируемые в память, о которых мы поговорим позже, также отображаются напрямую. Они доступны "для чтения и записи" и разделяются несколькими процессами.


Каркас приложений


С Visual C++ тесно связано еще одно понятие - каркас приложений, которое близко и созвучно понятию каркаса приложения, но в отличие от него относится не к одному конкретному приложению, а к библиотеке, с помощью которой строятся многие приложения. Каркас приложений - это библиотека классов, из которых программист берет не только набор классов, играющих роль дополнительных типов данных, но и классы, служащие строительными блоками приложения на самом верхнем уровне. С этой точки зрения, каркас приложения является частью каркаса приложений, относящейся к данному приложению. Примеры каркасов приложений - библиотеки классов MFC и OWL.



Каркас приложения


Наследование - одна из фундаментальных идей объектно-ориентированного программирования. Именно этот механизм наследования позволяет программисту дополнять и переопределять поведение базового класса, не вторгаясь в библиотеку MFC, которая остается неизменной. Все изменения делаются в собственном производном классе. Именно в этом и заключается работа программиста.

Объекты, их которых состоит приложение, являются объектами классов, производных от классов библиотеки MFC. Разработка приложения состоит в том, что программист берет из библиотеки MFC классы CWinApp, CFrameWnd, CDocument, CView и т.д. и строит производные классы. Приложение создается как совокупность объектов этих производных классов. Каждый объект несет в себе как наследуемые черты, определяемые базовыми классами, так и новые черты, добавленные программистом. Наследуемые черты определяют общую схему поведения, свойственную таким приложениям. Новые же черты позволяют реализовать специфические особенности поведения приложения, необходимые для решения стоящей перед ним задачи.

При определении производного класса программист может:

переопределить некоторые методы базового класса, причем те методы, что не были переопределены, будут наследоваться в том виде, в каком они существуют в базовом классе;

добавить новые методы;

добавить новые переменные.

Приложение, построенное на основе библиотеки MFC, - "айсберг", большая часть которого невидима, но является основой всего приложения. Часть приложения, лежащую в библиотеке MFC, - framework - называется каркасом приложения. Рассмотрим работу приложения как процесс взаимодействия между каркасом и частью приложения, разработанной программистом. Совершенно естественно, что в методах, определенных программистом, могут встречаться вызовы методов базового класса, что вполне можно рассматривать как вызов функции из библиотеки. Важнее, однако, что и метод производного класса, определенный программистом, может быть вызван из метода родительского класса. Другими словами, каркас и производный класс в этом смысле равноправны - их методы могут вызывать друг друга.
Такое равноправие достигается благодаря виртуальным методам и полиморфизму, имеющимся в арсенале объектно-ориентированного программирования.

Если метод базового класса объявлен виртуальным и разработчик переопределил его в производном классе, это значит, что при вызове данного метода в некоторой полиморфной функции базового класса в момент исполнения будет вызван метод производного класса и, следовательно, каркас вызывает метод, определенный программистом. Точнее говоря, обращение к этому методу должно производиться через ссылку на производный объект либо через объект, являющийся формальным параметром и получающий при вызове в качестве своего значения объект производного класса. Когда вызывается виртуальный метод М1, переопределенный разработчиком, то согласно терминологии Visual C++, каркас посылает сообщение М1 объекту производного класса, а метод М1 этого объекта обрабатывает это сообщение. Если сообщение М1 послано объекту производного класса, а обработчик этого сообщения не задан программистом, объект наследует метод М1 ближайшего родительского класса, в котором определен этот метод. Если же обработчик такого сообщения создан программистом, он автоматически отменяет действия, предусмотренные родительским классом в отсутствие этого обработчика.


Класс CFile


Класс CFile предназначен для обеспечения работы с файлами. Он позволяет упростить использование файлов, представляя файл как объект, который можно создать, читать, записывать и т.д.

Чтобы получить доступ к файлу, сначала надо создать объект класса CFile. Конструктор класса позволяет сразу после создания такого объекта открыть файл. Но можно открыть файл и позднее, воспользовавшись методом Open.

Открытие и создание файлов

После создания объекта класса CFile можно открыть файл, вызвав метод Open. Методу надо указать путь к открываемому файлу и режим его использования. Прототип метода Open имеет следующий вид:

virtual BOOL Open(LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError=NULL);

В качестве параметра lpszFileName надо указать имя открываемого файла. Можно указать только имя файла или полное имя файла, включающее полный путь к нему.

Второй параметр nOpenFlags определяет действие, выполняемое методом Open с файлом, а также атрибуты файла. Ниже представлены некоторые возможеые значения параметра nOpenFlags:

CFile::modeCreate - Создается новый файл. Если указанный файл существует, то его содержимое стирается и длина файла устанавливается равной нулю.

CFile::modeNoTruncate - Этот файл предназначен для использования совместно с файлом CFile::modeCreate. Если создается уже существующий файл, то его содержимое не будет удалено.

CFile::modeRead - Файл открывается только для чтения.

CFile::modeReadWrite - Файл открывается для записи и для чтения.

CFile::modeWrite - Файл открывается только для записи.

CFile::typeText - Используется классами, порожденными от класса CFile, например CStdioFile, для работы с файлами в текстовом режиме. Текстовый режим обеспечивает преобразование комбинации символа возврата каретки и символа перевода строки.

CFile::Binary - Используется классами, порожденными от класса CFile, например CStdioFile, для работы с файлами в двоичном режиме.

Необязательный параметр pError, который является указателем на объект класса CFileException, используется только в том случае, если выполнение операции с файлом вызовет ошибку.
При этом в объект, указываемый pError, будет записана дополнительная информация.

Метод Open возвращает не нулевое значение, если файл открыт и нуль в случае ошибки. Ошибка при открытии файла может случиться, например, если методу Open указан для чтения несуществующий файл.



Идентификатор открытого файла



В состав класса CFile входит элемент данных m_hFile типа UINT. В нем хранится идентификатор открытого файла. Если объект класса CFile уже создан, но файл еще не открыт, то в переменной m_hFile записана константа hFileNull.

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



Закрытие файлов



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



Чтение и запись файлов



Для доступа к файлам предназначено несколько методов класса CFile: Read, ReadHuge, Write, WriteHuge, Flush. Методы Read и ReadHuge предназначены для чтения данных из предварительно открытого файла. В 32-разрядных операционных системах оба метода могут одновременно считать из файла больше 65535 байт. Спецификация ReadHuge считается устаревшей и оставлена только для совместимости с 16-разрядными операционными системами.

Данные, прочитанные из файла, записываются в буфер lpBuf. Параметр nCount определяет количество байт, которое надо считать из файла. Фактически из файла может быть считано меньше байт, чем запрошено параметром nCount. Это происходит, если во время чтения достигнут конец файла. Методы возвращают количество байт, прочитанных из файла.

Для записи в файл предназначены методы Write и WriteHuge. В 32-разрядных операционных системах оба метода могут одновременно записывать в файл больше 65535 байт.


Методы записывает в открытый файл nCount байт из буфера lpBuf. В случае возникновения ошибки записи, например переполнения диска, методы вызывает обработку исключения.



Метод Flush



Когда используется метод Write или WriteHuge для записи данных на диск, они некоторое время могут находиться во временном буфере. Чтобы убедиться, что необходимые изменения внесены в файл на диске, нужно воспользоваться методом Flush.



Операции с файлами



В состав класса входят методы, позволяющие выполнять над файлами различные операции, например копирование, переименование, удаление, изменение атрибутов.

Для изменения имени файла класс CFile включает статический метод Rename, выполняющий функции этой команды. Метод нельзя использовать для переименования каталогов. В случае возникновения ошибки метод вызывает исключение.

Для удаления файлов в классе CFile включен статический метод Remove, позволяющий удалить указанный файл. Этот метод не позволяет удалять каталоги. Если удалить файл невозможно, то метод вызывает исключение.

Чтобы определить дату и время создания файла, его длину и атрибуты, предназначен статический метод GetStatus. Существует две разновидности метода - первый определен как виртуальный, а второй - как статический метод.

Виртуальная версия метода GetStatus определяет состояние открытого файла, связанного с данным объектом класса CFile. Этот метод вызывается только тогда, когда объект класса CFile создан и файл открыт.

Статическая версия метода GetStatus позволяет определить характеристики файла, не связанного с объектом класса CFile. Чтобы воспользоваться этим методом, необязательно предварительно открывать файл.



Блокировка



В состав класса включены методы LockRange и UnlockRange, позволяющие заблокировать один или несколько фрагментов данных файла для доступа из других процессов. Если приложение пытается повторно блокировать данные, уже заблокированные раньше этим или другим приложением, вызывается исключение. Блокировка представляет собой один из механизмов, позволяющих нескольким приложениям или процессам одновременно работать с одним файлом, не мешая друг другу.



Установить блокировку можно с помощью метода LockRange. Чтобы снять установленные блокировки, надо воспользоваться методом UnlockRange. Если в одном файле установлены несколько блокировок, то каждая из них должна сниматься отдельным вызовом метода UnlockRange.



Позиционирование



Чтобы переместить указатель текущей позиции файла в новое положение, можно воспользоваться одним из следующих методов класса CFile - Seek, SeekToBegin, SeekToEnd. В состав класса CFile также входят методы, позволяющие установить и изменить длину файла, - GetLength, SetLength.

При открытии файла указатель текущей позиции файла находится в самом начале файла. Когда порция данных прочитана или записана, то указатель текущей позиции перемещается в сторону конца файла и указывает на данные, которые будут читаться или записываться очередной операцией чтения или записи в файл.

Чтобы переместить указатель текущей позиции файла в любое место, можно воспользоваться универсальным методом Seek. Он позволяет переместить указатель на определенное число байт относительно начала, конца или текущей позиции указателя.

Чтобы переместить указатель в начало или конец файла, наиболее удобно использовать специальные методы. Метод SeekToBegin перемещает указатель в начало файла, а метод SeekToEnd - в его конец.

Но для определения длины открытого файла совсем необязательно перемещать его указатель. Можно воспользоваться методом GetLength. Этот метод также возвращает длину открытого файла в байтах. Метод SetLength позволяет изменить длину открытого файла. Если при помощи этого метода размер файла увеличивается, то значение последних байт не определено.

Текущую позицию указателя файла можно определить с помощью метода GetPosition. Возвращаемое методом GetPosition 32-разрядное значение определяет смещение указателя от начала файла.



Характеристики открытого файла



Чтобы определить расположение открытого файла на диске, надо вызвать метод GetFilePath. Этот метод возвращает объект класса CString, в котором содержится полный путь файла, включая имя диска, каталоги, имя файла и его расширение.

Если требуется определить только имя и расширение открытого файла, можно воспользоваться методом GetFileName. Он возвращает объект класса CString, в котором находится имя файла. В случае, когда нужно узнать только имя открытого файла без расширения, пользуются методом GetFileTitle.

Следующий метод класса CFile позволяет установить путь файла. Это метод не создает, не копирует и не изменяет имени файла, он только заполняет соответствующий элемент данных в объекте класса CFile.


Класс CMemFile


В библиотеку MFC входит класс CMemFile, наследуемый от базового класса CFile. Класс CMemFile представляет файл, размещенный, в оперативной памяти. С объектами класса CMemFile так же, как и с объектами класса CFile. Отличие заключается в том, что файл, связанный с объектом CMemFile, расположен не на диске, а в оперативной памяти компьютера. За счет этого операции с таким файлом происходят значительно быстрее, чем с обычными файлами.

Работая с объектами класса CMemFile, можно использовать практически все методы класса CFile, которые были описаны выше. Можно записывать данные в такой файл или считывать их. Кроме этих методов в состав класса CMemFile включены дополнительные методы.

Для создания объектов класса CMemFile предназначено два различных конструктора. Первый конструктор CMemFile имеет всего один необязательный параметр nGrowBytes:

CMemFile(UINT nGrowBytes=1024);

Этот конструктор создает в оперативной памяти пустой файл. После создания файл автоматически открывается (не нужно вызывать метод Open).

Когда начинается запись в такой файл, автоматически выделяется блок памяти. Для получения памяти методы класса CMemFile вызывают стандартные функции malloc, realloc и free. Если выделенного блока памяти недостаточно, его размер увеличивается. Увеличение блока памяти файла происходит по частям по nGrowBytes байт. После удаления объекта класса CMemFile используемая память автоматически возвращается системе.

Второй конструктор класса CMemFile имеет более сложный прототип. Это конструктор используется в тех случаях, когда программист сам выделяет память для файла:

CMemFile(BYTE* lpBuffer, UINT nBufferSize, UINT nGrowBytes=0);

Параметр lpBuffer указывает на буфер, который будет использоваться для файла. Размер буфера определяется параметром nBufferSize.

Необязательный параметр nGrowBytes используется более комплексно, чем в первом конструкторе класса. Если nGrowBytes содержит нуль, то созданный файл будет содержать данные из буфера lpBuffer. Длина такого файла будет равна nBufferSize.


Если nGrowBytes больше нуля, то содержимое буфера lpBuffer игнорируется. Кроме того, если в такой файл записывается больше данных, чем помещается в отведенном буфере, то его размер автоматически увеличивается. Увеличение блока памяти файла происходит по частям по nGrowBytes байт.

Класс CMemFile позволяет получить указатель на область памяти, используемую файлом. Через этот указатель можно непосредственно работать с содержимым файла, не ограничивая себя методами класса CFile. Для получения указателя на буфер файла можно воспользоваться методом Detach. Перед этим полезно определить длину файла (и соответственно размер буфера памяти), вызвав метод GetLength.

Метод Detach закрывает данный файл и возвращает указатель на используемый им блок памяти. Если опять потребуется открыть файл и связать с ним оперативный блок памяти, нужно вызвать метод Attach.

Нужно отметить, что для управления буфером файла класс CMemFile вызывает стандартные функции malloc, realloc и free. Поэтому, чтобы не нарушать механизм управления памятью, буфер lpBuffer должен быть создан функциями malloc или calloc.


Класс CStdioFile


Тем, кто привык пользоваться функциями потокового ввода/вывода из стандартной библиотеки C и C++, следует обратить внимание на класс CStdioFile, наследованный от базового класса CFile. Этот класс позволяет выполнять буферизированный ввод/вывод в текстовом и двоичном режиме. Для объектов класса CStdioFile можно вызывать практически все методы класса CFile.

В класс CStdioFile входит элемент данных m_pStream, который содержит указатель на открытый файл. Если объект класса CStdioFile создан, но файл еще не открыт, либо закрыт, то m_pStream содержит константу NULL.

Класс CStdioFile имеет три различных конструктора. Первый конструктор класса CStdioFile не имеет параметров. Этот конструктор только создает объект класса, но не открывает никаких файлов. Чтобы открыть файл, надо вызвать метод Open базового класса CFile.

Второй конструктор класса CStdioFile можно вызвать, если файл уже открыт и нужно создать новый объект класса CStdioFile и связать с ним открытый файл. Этот конструктор можно использовать, если файл был открыт стандартной функцией fopen. Параметр метода должен содержать указатель на файл, полученный вызовом стандартной функции fopen.

Третий конструктор можно использовать, если надо создать объект класса CStdioFile, открыть новый файл и связать его с только что созданным объектом.

Для чтения и записи в текстовый файл класс CStdioFile включает два новых метода: ReadString и WriteString. Первый метод позволяет прочитать из файла строку символов, а второй метод - записать.



Класс диалоговой панели


Класс главной диалоговой панели объявляется в файле dlgDlg.h и реализуется в файле dlgDlg.cpp.

Объявление класса

Класс CDlgDlg наследуется от базового класса CDialog, определенного в библиотеке классов MFC. Конструктор класса имеет один необязательный параметр pParent, используемый для передачи индекса главного окна приложения. Роль главного окна выполняет сама диалоговая панель, поэтому параметр pParent не используется.

Непосредственно после объявления конструктора класса следует объявление элементов данных класса, которые добавлены средствами MFC AppWizard. Не рекомендуется вручную изменять код приложения, расположенный между комментариями AFX_DATA, между которыми заключены эти объявления.

Далее AppWisard добавляет объявления переопределенных виртуальных методов базового класса. Сначала объявлен только метод DoDataExchange, переопределенный для данной диалоговой панели. Этот метод применяется для связывания органов управления диалоговой панели с элементами управляющего ею класса.

Практически со всеми приложениями связана пиктограмма, которая будет отображаться при минимизации приложения. Обычно эта пиктограмма определяется на этапе регистрации класса главного окна приложения. Приложение dlg не имеет настоящего главного окна - вместо него используется диалоговая панель. Поэтому отображение пиктограммы не происходит автоматически, и, следовательно, необходимо управлять ее отображением (идентификатор пиктограммы m_hIcon определен в классе CDlgDlg).

Диалоговая панель CDlgDlg будет обрабатывать ряд сообщений. Объявления обработчиков сообщений, созданных при помощи AppWizard, располагаются между комментариями AFX_MSG. После создания проекта в нем объявлены обработчики 4-х сообщений: OnInitDialog, OnSysCommand, OnPaint и OnQueryDragIcon.

Конструктор класса

Конструктор класса CDlgDlg вызывает конструктор базового класса CDialog. При этом ему передаются идентификатор диалоговой панели и идентификатор главного окна приложения.

В теле конструктора расположен блок AFX_DATA_INIT.
В него ClassWizard будет добавлять код инициализации элементов данных класса CDlgDlg.

Конструктор также инициализирует m_hIcon, записывая в него идентификатор пиктограммы IDR_MAINFRAME:

m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);

Метод AfxGetAp возвращает указатель на объект главного класса приложения (в данном случае - указатель на объект theApp).



Метод DoDataExchange



Диалоговая панель имеет только кнопки и не имеет связанных с ними переменных. Однако метод DoDataExchange переопределен. Фактически он не выполняет никакой работы. Единственное, что он делает, - это вызывает метод DoDataExchange базового класса CDialog.

Если к диалоговой панели добавить новые органы управления и связать их средствами ClassWizard с элементами данных класса CDlgDlg, то в блоке AFX_DATA_MAP будут размещены вызовы функций DDX и DDV, необходимые для выполнения обмена данными.



Таблица сообщений



Таблица сообщений класса CDlgDlg содержит три макрокоманды, которые выполняют обработку сообщений WM_SYSCOMMAND, WM_PAINT и WM_QUERYDRAGICON. Они расположены в блоке AFX_MSG_MAP, поэтому для управления ими используется ClassWizard.



Метод OnInitDialog



При отображении диалоговой панели при помощи функций DoModal, Create или CreateIndirect, функция диалоговой панели передается сообщение WM_INITDIALOG. Непосредственного доступа к функции диалога нет. Ее реализация содержится в классе CDialog.

В ответ на сообщение WM_INITDIALOG вызывается метод OnInitDialog, объявленный как виртуальный метод класса CDialog. Этот метод вызывается непосредственно перед выводом панели на экран.

MFC AppWizard в реализацию этого метода добавил несколько действий: добавление строки к системному меню для вызова краткой справки о приложении, а также при помощи метода SetIcon выбрал пиктограмму для приложения.

Метод OnInitDialog возвращает значение TRUE. Это означает, что фокус ввода будет установлен на первый орган управления диалоговой панели. Первый орган диалоговой панели можно выбрать в редакторе диалоговой панели.



Если во время инициализации диалоговой панели метод OnInitDialog устанавливает фокус ввода другому органу управления, метод должен вернуть значение FALSE.



Метод OnSysCommand



Если при создании шаблонного приложения указать возможность отображения краткой справочной информации, то при этом средство AppWizard подготовит фрагмент кода, добавляющий соответствующую строку в системное меню, а также метод-обработчик OnSysCommand сообщений от системного меню.

Этот метод имеет два параметра - идентификатор строки системного меню, которую выбрал пользователь, и параметр, содержащий координаты мыши, если выбор сделан мышью.

Для обработки выбора стандартных пунктов меню переопределенный метод вызывается метод-обработчик базового класса (для обработки сообщений по умолчанию), а для добавленных пунктов выполняется специфическая обработка.



Метод OnPaint



Диалоговая панель в отличие от обычного окна не содержит встроенных средств отображения пиктограммы приложения. Приложение должно само заботиться об отображении пиктограммы - для этого переопределяется метод OnPaint. В этом методе либо вызывается метод OnPaint базового класса, либо приложение само отображает пиктограмму, если оно в данный момент минимизировано.



Метод OnQueryDragIcon



Пользователь может “перетащить” пиктограмму минимизированного приложения. Если пользователь производит это действие с пиктограммой окна, для класса которого не определена пиктограмма, то вызывается метод OnQueryDragIcon. Этот метод должен вернуть идентификатор курсора мыши, который будет отображаться в момент “перетаскивания” пиктограммы окна.

В переопределенном методе OnQueryDragIcon средство AppWizard просто возвращает идентификатор пиктограммы приложения.


Класс дочернего окна MDI


Многооконное приложение строится с использованием большего числа классов, чем однооконное приложение. Помимо классов главного окна и классов окон просмотра документов в нем определен еще один класс, непосредствено связанный с отображением дочерних окон MDI. Этот класс называется CChildFrame, и он наследуется от базового класса CMDIChildWnd, определенного в библиотеке MFC.

Объекты класса CChildFrame представляют дочерние окна MDI главного окна приложения. Внутри этих окон отображаются окна просмотра документов.

Чаще всего приложения не вносят изменений в класс дочернего MDI-окна.

Конструктор и деструктор класса. MFC AppWizard определяет для класса CChildFrame конструктор и деструктор. По умолчанию они не выполняют никаких действий. Их можно изменить для выполнения инициализации объектов класса дочернего окна.

Таблица сообщений класса. По умолчанию таблица сообщений не содержит обработчиков сообщений. При необходимости можно добавить обработку каких-либо сообщений с использованием ClassWizard.

Метод PreCreateWindow. Метод PreCreateWindow определен в классе CWnd. Он вызывается непосредственно перед созданием окна, связанного с объектом класса. В качестве параметра этому методу передается структура типа CREATESTRUCT, определяющая характеристики создаваемого окна. Приложение может изменить данные, записанные в этой структуре, чтобы переопределить стили создаваемого окна и повлиять на внешний вид создаваемого окна.

MFC AppWizard переопределяет этот метод, но не вносит в него в изменений и просто вызывает метод PreCreateWindow базового класса CMDIChildWnd.

Методы AssertValid и Dump. Для отладочной версии приложения класс CChildFrame содержит переопределения виртуальных методов AssertValid и Dump. Эти методы определены в базовом классе CObject и используются при отладке приложения.



Класс документа


Этот класс является классом документа приложения. В качестве базового класса для него используется класс CDocument библиотеки MFC.

Если просмотреть исходные тексты приложения, то нигде нельзя обнаружить кода, который бы явно создавал объекты этого класса. Объект класса CSingleDoc создается динамически шаблоном документа во время работы приложения. Шаблон документа также динамически создает еще два объекта - класса окна и класса окна просмотра.

Для того, чтобы объекты любого класса, наследованного от базового класса CObject, в том числе и CSingleDoc, можно было создавать динамически, необходимо выполнить следующее:

макрокоманду DECLARE_DYNCREATE надо поместить в описание класса. В качестве параметра этой макрокоманды необходимо указать имя данного класса;

определить конструктор класса, который не имеет параметров;

разместить макрокоманду IMPLEMENT_DYNCREATE в файле реализации класса. Макрокоманда IMPLEMENT_DYNCREATE имеет два параметра. В первом указывается имя класса, а во втором - имя его базового класса.

MFC AppWizard автоматически выполняет все эти требования для класса документа приложения CSingleDoc, класса окна приложения CMainFrame и класса окна просмотра CSingleView.

Таблица сообщений класса. Макрокоманда IMPLEMENT_DYNCREATE размещается в файле реализации класса singleDoc.cpp. Обычно MFC AppWizard размещает эту макрокоманду непосредственно перед таблицей сообщений класса (если, конечно, данный класс обрабатывает сообщения).

Непосредственно после создания проекта таблица сообщений класса CSingleDoc не содержит обработчиков сообщений. Когда приложение будет разрабатываться дальше, программист может добавлять обработчики различных сообщений к классу CSingleDoc и другим классам приложения. Для добавления новых обработчиков сообщений, а также внесения других изменений в классы, следует использовать ClassWizard.

Конструктор и деструктор класса. Конструктор, подготовленный MFC AppWizard, содержит пустой блок. Можно поместить в него код инициализации для объектов класса.




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



Вместе с конструктором класса CSingleDoc, создается деструктор ~CSingleDoc. Деструктор не содержит кода и представляет собой такую же заготовку, как и конструктор.



Методы OnNewDocument и Serialize. В классе CSingleDoc переопределены два виртуальных метода: OnNewDocument и Serialize. Виртуальный метод OnNewDocument определен в классе CDocument, от которого непосредственно наследуется класс CSingleDoc. А вот виртуальный метод Serialize определен в классе CObject. Цепочка наследования в этом случае длиннее.

Метод OnNewDocument вызывается, когда нужно создать новый документ для приложения. Если метод OnNewDocument переопределяется (в данном случае это делает MFC AppWizard), то сначала необходимо вызвать метод OnNewDocument базового класса и только затем можно выполнять инициализацию документа.

Если создание нового документа прошло успешно, метод OnNewDocument должен вернуть значение TRUE, в противном случае - FALSE. Когда вызывается метод OnNewDocument базового класса, следует выполнять проверку возвращаемого им значения. Если это значение равно FALSE, значит создание документа на уровне класса CDocument не прошло и следует прекратить дальнейшие действия.

Большой интерес представляет метод Serialize. Он вызывается в тех случаях, когда нужно загрузить документ из файла на диске или, наоборот, записать его в файл. Метод Serialize вызывается, когда пользователь выбирает из меню File строки Open или Save.

В качестве параметра методу Serialize передается указатель на объект класса СArchive, связанный с файлом, в который надо записать или из которого надо прочитать документ.

Метод Serialize вызывается и для загрузки и для сохранения документа. Чтобы узнать, что надо делать с документом , используется метод IsStoring класса CArchive .Если он возвращает ненулевое значение, значит, необходимо сохранить состояние документа в файле. В противном случае нужно считать документ из файла.



Методы AssertValid и Dump. Класс CSingleDoc содержит переопределения еще двух виртуальных методов: AssertValid и Dump, входящих в базовый класс CObject.

Следует обратить внимание на то, что описание этих методов и их определение находится в блоке #ifdef _DEBUG. Поэтому эти методы используются только для отладочной версии приложения. При окончательном построении приложения эти методы не переопределяются.


Класс главного окна приложения


Класс CMainFrame представляет главное окно приложения. Внутри этого окна отображаются окно просмотра документа, а также панели управления и состояния. Определение и реализация класса CMainFrame расположены в файлах MainFrm.h и MainFrm.cpp.

Класс CMainFrame наследуется от базового класса CFrameWnd, определенного в библиотеке MFC. Как правило, перед программистом не возникает необходимости как-либо дорабатывать класс главного окна приложения, созданный MFC AppWizard.

Шаблон документов приложения создает объекты класса CMainFrame динамически. Для этого в определении класса указана макрокоманда DECLARE_DYNCREATE, объявлен конструктор, не имеющий параметров, а в файле реализации добавлена макрокоманда IMPLEMENT_DYNCREATE.

Таблица сообщений класса. Класс CMainFrame может получать и обрабатывать сообщения, поэтому в определении класса указана макрокоманда DECLARE_MESSAGE_MAP, а в файле реализации класса расположена таблица сообщений.

Сначала в таблице сообщений расположена единственная макрокоманда ON_WM_CREATE. Эта макрокоманда устанавливает для обработки сообщения WM_CREATE метод OnCreate. Сообщение WM_CREATE передается функции окна сразу после его создания, но до того, как окно появится на экране.

Конструктор и деструктор класса. MFC AppWizard определяет конструктор и деструктор класса CMainFrame. Можно расположить в конструкторе и деструкторе код для инициализации и деинициализации объектов класса.

Метод OnCreate. Это метод определяется в классе CWnd следующим образом. Параметр lpCreateStruct этой функции является указателем на объект CREATESTRUCT, содержащей характеристики создаваемого окна.

При нормальной работе метод OnCreate должен вернуть значение 0, чтобы продолжить создание окна. Если метод OnCreate возвращает -1, окно будет удалено (уничтожено).

MFC AppWizard переопределяет метод OnCreate в классе CMainFrame. Поэтому для обработки сообщения WM_CREATE будет вызван именно переопределенный метод OnCreate класса CMainFrame.

Основное назначение метода OnCreate заключается в том, что он сначала вызывает метод OnCreate базового класса CFrameWnd, а затем создает и отображает внутри главного окна панель управления toolbar и панель состояния status bar.
Кстати, если из метода OnCreate удалить последние три вызова методов, то этим можно запретить перемещение панели управления.

Метод OnCreate базового класса CFrameWnd выполняет обработку сообщения WM_CREATE по умолчанию и возвращает нулевое значение, если обработка прошла без ошибок.

В случае возникновения ошибок при обработке сообщения в базовом классе возвращается -1. При этом метод CMainFrame::OnCreate прекращает дальнейшую обработку и возвращает -1, вызывая удаление окна.



Панель управления и панель состояния. Самая интересная задача, которую решает метод OnCreate, заключается в отображении панелей управления и состояния. Для панели управления и панели состояния в библиотеке классов MFC предусмотрены два класса CStatusBar и CToolBar.

Рассмотрим самые главные методы, необходимые для работы с панелями управления и состояния.



Панель управления.
Класс CToolBar представляет панель управления toolbar. Обычно панель управления содержит несколько кнопок и разделителей между ними. Для панели управления определен специальный ресурс типа TOOLBAR (специальная диалоговая панель.

Чтобы создать панель управления toolbar, следует выполнить следующие действия:

Создать ресурс (специальную диалоговую панель), описывающий панель toolbar.

Создать объект класса CToolBar, представляющий панель управления.

Вызвать метод Create класса CToolBar, чтобы создать панель управления и связать ее с объектом, представляющим панель управления.

Вызвать метод LoadToolBar класса CToolBar. Он загрузит ресурс диалоговой панели и завершит построение панели управления.

MFC AppWizard создает для приложения панель управления, которую программист может видоизменить по желанию.

Объект для m_wndToolBar класса CToolBar для управления панелью определяется в классе CMainFrame. Этот объект объявлен как protected, поэтому обращаться к нему можно только из класса главного окна приложения CMainFrame.

protected: // control bar embedded members CToolBar m_wndToolBar;

Создание самой панели управления и отображение ее на экране происходит во время обработки метода OnCreate класса CMainFrame.



if (!m_wndToolBar.Create(this) !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0("Failed to create toolbar\n"); return -1; // fail to create }

В качестве родительского окна панели управления методу Create указано ключевое слово this. Таким образом, родительским окном является главное окно приложения, представленное объектом класса CMainFrame.

Если хотя бы один из методов, создающих и изображающих панель управления, завершится с ошибкой, то метод OnCreate класса CMainFrame возвращает -1 и дальнейшая обработка прекращается с одновременным удалением окна.



Панель состояния.
Для управления панелью состояния используется класс CStatusBar. Чтобы создать для окна панель состояния следует выполнить следующие действия:

Создать объект класса CStatusBar. Он будет представлять панель состояния и управлять ей.

Вызвать метод Create класса CStatusBar, чтобы создать панель и связать ее с объектом, представляющим панель состояния.

Вызвать метод SetIndicators класса CStatusBar, чтобы связать каждый индикатор панели состояния с идентификатором текстовой строки.

MFC AppWizard автоматически добавляет к созданному им приложению программный код, служащий для отображения панели состояния.

Объект m_wndStatusBar класса CStatusBar определяется как элемент класса CMainFrame. Это вполне естественно, так как панель состояния принадлежит именно окну класса CMainFrame.

protected: // control bar embedded members CStatusBar m_wndStatusBar;

Создание панели состояния и отображение ее на экране выполняется во время обработки метода OnCreate класса CMainFrame сразу после создания панели управления.

Методы Create и SetIndicators, создающие панель, вызываются в одной строке.

if (!m_wndStatusBar.Create(this) !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE("Failed to create status bar\n"); return -1; // fail to create }

В качестве родительского окна панели состояния методу Create указано ключевое слово this. Таким образом, родительским окном панели состояния, так же как и панели управления, является главное окно приложения, представленное объектом класса CMainFrame.



Информация, которая должна отображаться в панели состояния, определяется идентификаторами, записанными в массиве. Каждый идентификатор представляет текстовую строку из таблицы ресурсов приложения. В приложении single массив indicators, описывающий панель состояния, определен в файле MainFrm.cpp. В этот массив и входят иденификаторы текстовых строк панели состояния.

Сразу после создания панели состояния вызывается метод SetIndicators. В первом параметре ему передается массив indicators, а во втором - количество элементов в этом массиве.

Метод SetIndicators отображает в панели состояния строки, идентификаторы которых представлены в массиве indicators. При этом первый элемент массива определяет крайнюю левую строку в панели состояния. По умолчанию, строки в панели состояния выравниваются по правой границе. Можно получить доступ к любому элементу панели состояния по его индексу или идентификатору.



Метод PreCreateWindow. Виртуальный метод PreCreateWindow определен в классе CWnd. Он вызывается непосредственно перед созданием окна, связанного с объектом класса. В качестве параметра этому методу передается структура типа CREATESTRUCT, определяющая характеристики создаваемого окна. Приложение может изменить данные, записанные в этой структуре, чтобы повлиять на внешний вид создаваемого окна.



Методы AssertValid и Dump. Когда выполняется построение отладочной версии приложения, в состав класса включается переопределения виртуальных методов AssertValid и Dump. Эти методы определены в базовом классе CObject и используются при отладке приложения.


Класс окна просмотра документа


Следующим классом приложения single является класс окна просмотра документа CSingleView. Этот класс наследуется от базового класса CView библиотеки MFC. Определение и реализация этого класса находятся в файлах singleView.h и singleView.cpp.

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

Для этого определяется конструктор класса, не имеющий параметров, а в определении класса указывается макрокоманда DECLARE_DYNCREATE, а в файле реализации - макрокоманда IMPLEMENT_DYNCREATE.

Таблица сообщений класса. Непосредственно перед таблицей сообщений класса CSingleView (singleView.cpp) находится макрокоманда INPLEMENT_DYNCREATE. В таблице указываются обработчики сообщений от пунктов меню для печати ID_FILE_PRINT, ID_FILE_PRINT_DIRECT и ID_FILE_PRINT_PREVIEW.

Конструктор и деструктор класса. Конструктор класса CSingleView, созданный MFC AppWizard, не содержит команд. Можно доработать его, чтобы конструктор выполнял инициализацию, связанную с окном просмотра.

Вместе с конструктором класса CSingleView MFC AppWizard определяет деструктор ~CSingleView. Сразу после создания проекта деструктор не выполняет никаких действий. В дальнейшем можно использовать его совместно с конструктором CSingleView.

Метод GetDocument. Этот метод возвращает указатель на документ, связанный с данным окном просмотра. Если окно просмотра не связано ни с каким документом, метод возвращает значение NULL.

Метод PreCreateWindow. Виртуальный метод PreCreateWindow определен в классе CWnd. Он вызывается непосредственно перед созданием окна, связанного с объектом класса. В качестве параметра этому методу передается структура типа CREATESTRUCT, определяющая характеристики создаваемого окна. Приложение может изменить данные, записанные в этой структуре, чтобы повлиять на внешний вид создаваемого окна.

Метод OnDraw. Это метод вызывается, когда надо отобразить документ в окне.
В качестве параметра pDC методу OnDraw передается указатель на контекст устройства, используя который надо отобразить документ.

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

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

В том случае, если внешний вид документа при выводе на его печать должен отличаться от вывода в окно, можно вызвать метод IsPrinting контекста устройства, чтобы определить, для чего вызывается OnDraw. Метод IsPrinting возвращает не нулевое значение, если объект контекста, для которого он вызывается, является контекстом принтера. Если контекст представляет другое устройство отображения, например окно, то метод IsPrinting возвращает нуль.

В строке метода OnDraw MFC AppWizard вставляет строку, которая служит для получения указателя pDoc на документ, связанный с данным окном просмотра. Предполагается, что используя указатель pDoc, можно получить данные из документа и отобразить их на экране.



Методы OnPrepare, OnBeginPrinting и OnEndPrinting. Виртуальные методы OnPrepare, OnBeginPrinting и OnEndPrinting, определенные в классе CView, вызываются, если пользователь желает распечатать документ, отображенный в данном окне просмотра.

Метод OnPreparePrinting вызывается перед печатью документа. OnBeginPrinting используется для получения шрифтов и других ресурсов GDI. Метод OnEndPrinting используется для освобождения ресурсов, полученных методом OnBeginPainting.



Методы AssertValid и Dump. На время отладки приложения в состав класса CSingleView включаются переопределения виртуальных методов AssertValid и Dump. Эти методы определены в базовом классе CObject и используются при отладке приложения. При окончательном построении приложения эти методы не переопределяются.


Класс OLE-элемента управления


Объявление класса

Класс OLE-элемента управления объявлен в файле NameCtl.h и реализуется в файле NameCtl.cpp. Базовый класс элемента управления наследуется от класса COleControl - стандартного базового класса библиотеки MFC, который используется элементами управления OLE. Этот класс, будучи потомком класса CWnd, наследует все функции объекта окна, а также располагает дополнительными, специфическими для OLE функциями, такими как генерация событий и поддержка методов и свойств OLE-элемента управления.

В классе COleControl реализованы все интерфейсы, необходимые для работы элемента управления. Он скрывает детали реализации элемента и имеет понятный, объектно-ориентированный интерфейс, который используется элементами управления при выполнении собственных функций.

Конструктор и деструктор класса

В конструкторе класса CNameCtrl происходит вызов функции InitializeIIDs. Эта функция должна вызываться только в конструкторе элемента управления. Она сообщает базовому классу идентификаторы интерфейсов (IID) элемента управления. В данном случае класс COleControl получает идентификатор базового интерфейса автоматизации и идентификатор интерфейса диспетчеризации.

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

Методы класса

Класс CNameCtrl переопределяет некоторые методы базового класса: OnDraw, DoPropExchange, OnResetState. Метод OnDraw вызывается операционной системой, когда элемент управления должен быть перерисован.

Вызов метода DoPropExchange происходит, когда элемент управления записывается или считывается из постоянного хранилища (с диска). Эта функция отвечает за сохранение (запись на диск) и восстановление (чтение с диска) информации о свойствах элемента и его версии.

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



Поддержка элементов управления



Объявление класса CNameCtrl включает в себя несколько макросов, обеспечивающих поддержку элементов управления OLE.

Макрос DECLARE_OLECREATE_EX(CNameCtrl) объявляет фабрику классов и метод GetClassID для класса элемента управления. Метод GetClassID (объявляемый в этом макросе) вызывается системой, когда ей необходимо получить CLSID элемента. CLSID используется библиотеками OLE для регистрации, создания элемента управления и т.д.

Фабрика классов представляет собой OLE-объект, который используется для производства экземпляров других объектов. Фабрики классов элемента управления располагаются внутри OCX-файла, вместе с самим элементом. Библиотека MFC сама обеспечивает реализацию фабрики классов (разработчику не нужно о ней беспокоиться). В основном фабрика классов реализована в базовом классе COleObjectFactoryEx . Фактически конструктор фабрики классов для элемента управления просто передает свои параметры базовому классу, в котором скрыты детали реализации.

Рассмотрим подробнее назначение класса COleObjectFactoryEx. Класс COleObjectFactoryEx является синонимом класса COleObjectFactory. Он реализует фабрику классов OLE и содержит методы для:

управления регистрации объектов;

обновления системного реестра, а также регистрации элемента управления во время выполнения программы (это необходимо для информирования библиотек OLE о том, что объект находится в режиме выполнения и готов к приему сообщений);

принудительного лицензирования путем задания ограничений на использование элемента зарегистрированными разработчиками (на этапе проектирования) и зарегистрированными приложениями (на этапе выполнения);

регистрации фабрики классов элемента управления в системном реестре.

Инициализация фабрики классов элемента управления и формирование GUID (CLSID) производятся в файле реализации. Эти действия выполняет макрос IMPLEMENT_OLECREATE_EX. В этом макросе реализуется фабрика классов элемента управления и метод GetClassID.


Фабрика классов создает экземпляры элемента управления. Метод GetClassID возвращает CLSID элемента всем, кто его запрашивает, например библиотекам OLE.

В объявлении класса присутствует макрос DECLARE_OLETYPELIB(CNameCtrl), который объявляет метод GetTypeLib для класса элемента управления.. Этот макрос должен присутствовать в файле заголовков элемента управления.

Метод GetTypeLib элемента управления реализуется макросом IMPLEMENT_OLETYPELIB в файле реализации класса. Библиотека типов представляет собой файл, содержащий информацию о типе элемента управления, его интерфейсах и элементах автоматизации (к ним относятся свойства, методы и события). Сгенерированный метод GetTypeLib вызывается системой, когда ей нужна информация о библиотеке типов элемента. Эти данные сравниваются с данными из системного реестра, и если обнаруживается совпадение, то информация передается системе.

Элемент управления OLE использует страницы свойств, чтобы дать пользователю возможность просматривать и изменять свойства элемента управления. Страницы свойств реализуются в виде диалоговых окон с закладками (tabs). В макросе DECLARE_PROPPAGEIDS(CNameCtrl) объявляются идентификаторы и методы, используемые этим механизмом.

Макрос DECLARE_OLECTLTYPE(CNameCtrl) объявляет методы, которые обрабатывают информацию о типе элемента и данные флагов MiscStatus.

Информация о типе элемента управления записана в переменной типа const с именем _dwNameOleMisc А макрос IMPLEMENT_OLECTLTYPE в реализации класса определяет два метода элемента управления GetUserTypeNameID и GetMiscStatus. Метод GetUserTypeNameID возвращает идентификатор ресурса строки (константа IDS_Name), содержащей имя типа элемента управления. Метод GetMiscStatus вызывается внешними объектами, например контейнером, для получения информации об элементе управления. Значения, записанные в переменной _dwNameOleMisc, генерируются при создании проекта и являются выбором установок проекта.



Идентификаторы интерфейсов (IID) автоматизации



Строки файла реализации класса, объявляющие IID_Dname и IID_DNameEvents определяют идентификаторы интерфейсов элемента управления.


Первый из них идентифицирует реализацию IDispatch - базовый интерфейс диспетчеризации класса. Этот интерфейс используется клиентами автоматизации для редактирования и просмотра элемента управления и выполнения его методов. Второй идентификатор определяет интерфейс диспетчеризации событий класса. Этот интерфейс используется элементом управления для генерации событий. Оба идентификатора хранятся в системном реестре в ключе HKEY_CLASSES_ROOT\Interface.



Таблица сообщений класса



Класс CNameCtrl c помощью макроса DECLARE_MESSAGE_MAP объявляет привычную уже таблицу сообщений (схему сообщений) класса, при помощи чего сообщает системе, что класс будет обрабатывать сообщения. Сама таблица сообщений объявляется в файле реализации класса.

За реализацию технологии OLE-автоматизации, то есть за предоставление внешнему миру методов и свойств элемента, отвечает OLE-интерфейс IDispatch. Библиотека MFC обеспечивает поддержку этой технологии с помощью конструкции под названием “схема диспетчеризации” (dispatch map). Эта схема представляет собой эквивалент знакомой уже схемы сообщений (message map). Подобно тому, как схемы сообщений скрывают детали взаимодействия с каналом сообщений Windows, схемы диспетчеризации скрывают детали OLE-автоматизации.

В этой таблице сообщений в файле реализации изначально присутствует только одна строка - макрос ON_OLEVERB(). Этот макрос связывает указанный глагол idsVerbName (сообщение, которое элемент управления будет обрабатывать) с конкретным методом memberFxn элемента управления.



Схема диспетчеризации класса



В объявлении класса присутствует макрос DECLARE_DISPATCH_MAP, который объявляет схему диспетчеризации. Посредством нее элемент управления связывает свои свойства с соответствующими методами записи, чтения и оповещения (последние вызываются при изменении значения свойства). Дополнительно устанавливается связь между идентификаторами диспетчеризации (DISPID) и фактическими методами.

Сначала схема диспетчеризации в файле реализации имеет одну запись, сделанную при помощи макроса DISP_FUNCTION_ID, который связывает между собой следующие элементы: имя класса; внешнее имя метода; идентификатор (DISPID) метода; метод C++, соответствующий вызову метода автоматизации; список параметров метода (тип VARIANT); тип значения, возвращаемого методом (тип VARIANT).


Эта информация необходима системе для того, чтобы направлять вызовы методов автоматизации по правильному пути. Когда контроллер автоматизации вызывает метод при помощи его внешнего имени, такой вызов преобразуется в вызов внутренней функции элемента управления. В основе этого механизма лежит DISPID. Каждому объявленному свойству или методу должен соответствовать свой макрос в схеме диспетчеризации класса.



Схема диспетчеризации событий класса



В объявлении класса макрос DECLARE_EVENT_MAP, который объявляет схему событий (event map). Подобно тому, как доступ к свойствам и методам элемента управления осуществляется с помощью схемы диспетчеризации, доступ к событиям элемента осуществляется посредством схемы событий. В ней имена и идентификаторы событий связываются с функциями, ответственными за генерацию событий.

На этапе проектирования у элемента нет событий по умолчанию, поэтому этот схема событий в файле реализации пуста.



Идентификаторы DISPID



Различные идентификаторы свойств, методов и событий (DISPID) располагаются между макросами AFX_DISP_ID. Эти идентификаторы используются в ODL-файле элемента управления. По мере добавления в элемент свойств, методов и событий этот раздел будет заполняться средством ClassWizard.


Класс сервера


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

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

BOOL CSvrDllApp::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, COM-клиент (или контейнер) может создать экземпляр предоставляемого сервером объекта. В свою очередь, эта функция обратится к CoGetClassObject, чтобы получить указатель на интерфейс IUnknown фабрики класса требуемого объекта, и затем, используя имеющийся в этом интерфейсе указатель, вызовет IClassFactory::CreateInstance. Эта функция запустит фабрику класса, чтобы создать экземпляр запрашиваемого объекта.



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

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); return AfxDllGetClassObject(rclsid, riid, ppv); }

Функции 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.


Класс страницы свойств OLE-элемента управления


Страницы свойств - это OLE-объекты, которые дают пользователю возможность просматривать и изменять свойства элемента управления OLE. Страницы свойств содержат один или несколько стандартных диалоговых элементов управления, связанных с различными свойствами. Страницы свойств отображаются в виде модальных или немодальных диалоговых окон с закладками, причем каждой странице соответствует своя закладка. Чтобы отобразить на экране страницы свойств элемента управления, необходимо вызвать его “глагол свойств” - OLEVERB_PROPETIES.

По умолчанию средство MFC ActiveX ControlWizard создает одну страницу свойств, за работу которой в рассматриваемом случае OLE-элемента управления отвечает класс CNamePropPpg.

Объявление класса

Класс страницы свойств CNamePropPage объявляется в файле NamePpg.h. Этот класс сгенерирован при создании проекта. Он реализует одну пустую страницу свойств. Этот класс является прямым потомком класса COlePropertyPage, базового класса всех страниц свойств.

Класс CNamePropPage является OLE-объектом. Поэтому ему необходима фабрика классов, генерацию которой обеспечивает макрос DECLARE_OLECREATE_EX. Фабрика классов и генерация CLSID для класса страницы свойств реализована в файле NamePpg.cpp при помощи макроса IMPLEMENT_OLECREATE_EX.

Стандартные элементы управления страницы свойств

Страницы свойств - это просто дочерние диалоговые окна, обычно содержащие ряд стандартных элементов управления. Как и в случае обычных диалоговых окон, можно связать с каждым элементом управления диалога соответствующую ему переменную - элемент класса диалога. Такие элементы объявляются средством ClassWizard в классе страницы свойств в следующем блоке AFX_DATA.

Обмен данными между диалоговым окном страницы свойств и его элементами управления посредством связанных с элементами управления переменных класса осуществляется при помощи метода DoDataExchange(), который реализован в файле NamePpg.cpp. Этот метод использует функции обмена и проверки данных (DDX/DDV-макросы).

Редактирование записей системного реестра для класса страницы свойств

Добавление и удаление записей из системного реестра для класса CNamePropPage производится в методе UpdateRegistry фабрики классов. Этот метод фабрики классов регистрирует либо снимает регистрацию класса CNamePropPage. Это делает страницу свойств доступной для всех OLE-серверов и контейнеров.



Классы


Всякий СОМ-объект является экземпляром некоторого класса, и каждому классу может быть присвоен GUID — идентификатор класса (CLSID). Клиент может передавать этот CLSID библиотеке СОМ для создания экземпляра класса, как описано ниже в разделе "Создание объектов СОМ". Однако наличие CLSID не обязательно для всех классов — объекты некоторых классов не создаются с помощью библиотеки СОМ, поэтому такие классы не обязаны иметь CLSID. Абсолютно допустимо существование в любой данный момент времени одного, двух или многих активных объектов некоторого класса.

Связь между классом объекта и интерфейсами, которые этот объект поддерживает, не очень прочна. Естественно было бы предположить, что объект данного класса поддерживает определенный набор интерфейсов и что добавление к объекту нового интерфейса изменяет его класс. Однако это не обязательно — добавление новых интерфейсов к объекту без изменения его класса не запрещается СОМ. Вместо этого основным назначением CLSID является идентификация некоторого фрагмента кода для библиотеки СОМ, чтобы можно было загружать и активизировать объекты данного класса. В СОМ класс объектов идентифицирует некую реализацию группы интерфейсов, а не просто саму эту группу. Предположим, что объект — инструментарий для работы с текстом — решили реализовать два разных производителя и оба созданных ими объекта поддерживают как ISpellChecker, так и IThesaurus. Хотя эти объекты и поддерживают одинаковые набор интерфейсов, они относятся к разным классам с разными CLSID, так как их реализации различны.



Классы, не имеющие базового класса


Кроме классов, наследованных от базового класса CObject, библиотека MFC включает ряд самостоятельных классов. У них нет общего базового класса, и имеют различное назначение.

Несколько классов, которые не наследуются от базового класса CObject, уже упоминались. К ним относятся классы CCmdUI, CFileStatus, CDataExchange, CFieldExchange и CDaoFieldExchange.

Простые классы. Библиотека MFC содержит классы, соответствующие объектам типа простых геометрических фигур, текстовых строк и объектам, определяющим дату и время:

CPoint - объекты класса описывают точку.

CRect - объекты класса описывают прямоугольник.

CSize - объекты класса определяют размер прямоугольника.

CString - объекты класса представляют собой текстовые строки переменной длины.

CTime - объекты класса служат для хранения даты и времени. Большое количество методов класса позволяет выполнять над объектами класса различные преобразования.

CTimeSpan - объекты класса определяют период времени.

Архивный класс (класс CArchive). Класс CArchive используется для сохранения и восстановления состояния объектов в файлах на диске. Перед использованием объекта класса CArchive он должен быть привязан к файлу - объекта класса CFile.

Информация о классе объекта (структура CRuntimeClass). Во многих случаях бывает необходимо уже во время работы приложения получать информацию о классе и его базовом классе. Для этого любой класс, наследованный от базового класса CObject, связан со структурой CRuntimeClass. Она позволяет определить имя класса объекта, размер объекта в байтах, указатель на конструктор класса, не имеющий аргументов, и деструктор класса. Можно также узнать подобную информацию о базовом классе и некоторые дополнительные сведения.

Отладка приложения (классы CDumpObject, CMemoryState). В отладочной версии приложения можно использовать класс CDumpContext. Он позволяет выдавать состояние различных объектов в текстовом виде.

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

Печать документа (класс CPrintInfo). Класс CPrintInfo предназначен для управления печатью документов на принтере. Когда пользователь отправляет документ на печать или выполняет предварительный просмотр документа перед печатью, создается объект класса CPrintInfo. Он содержит различную информацию о том, какие страницы документа печатаются , и т.д.



Классы приложения


Тексты приложения single (см. Приложение 2), созданные MFC AppWizard подробно рассматриваться не будут, вместо этого будет дано описание отдельных классов, определенных в приложении, и их связь друг с другом.

MFC AppWizard создает для приложения, обладающего однооконным интерфейсом, четыре основных класса. Эти классы представляют основу любого однооконного приложения, созданного MFC AppWizard.

Класс приложения

Базовый класс

Описание

CsingleApp

CwinApp

Главный класс приложения

CmainFrame

CframeWnd

Класс главного окна приложения

CsingleDoc

Cdocument

Класс документа приложения

CsingleView

CView

Класс окна просмотра документа

Кроме этих основных классов создается класс CAboutDlg, наследованный от базового класса CDialog, который отвечает за диалоговую панель About.

Если при создании приложения определить возможность работы с базами данных или использование технологии OLE, то список классов приложения может стать шире.


MFC AppWizard создает для приложения с многооконным приложением несколько основных классов:

Класс приложения

Базовый класс

Описание

CMultiApp

CWinApp

Главный класс приложения

CMainFrame

CMDIFrameWnd

Класс главного окна приложения

CChildFrame

CMDIChildWnd

Класс дочернего окна MDI

CMultiDoc

CDocument

Класс документа приложения

CMultiVeiw

CView

Класс окна просмотра документа

Кроме этих основных классов создается также класс CAboutDlg, наследованный от базового CDialog. Он отвечает за диалоговую панель About. Если во время определения характеристик приложения включить возможность работы с базами данных, работу с сетевыми протоколами или использование технологии OLE, список классов приложения может стать значительно шире.



Контекст отображения (класс CDC)


Для отображения информации в окне или на любом другом устройстве приложение должно получать так называемы контекст отображения. Основные свойства контекста отображения определены в классе CDC. От него наследуется 4 различных класса, представляющие контекст отображения различных устройств.

Дадим краткое описание классов, наследованных от CDC:

CClientDC - контекст отображения, связанный с внутренней областью окна (client area). Для получения контекста конструктор класса вызывает функцию программного интерфейса GetDC, а деструктор - функцию ReleaseDC.

CMetaFileDC - класс предназначен для работы с метафайлами.

CPaintDC - конструктор класса CPaintDC для получения контекста отображения вызывает метод CWnd::BeginPaint, деструктор - метод CWnd::EndPaint. Объекты данного класса можно использовать только при обработке сообщения WM_PAINT. Это сообщение обычно обрабатывает метод OnPaint.

CWindowDC - контекст отображения, связанный со всем окном. Для получения контекста конструктор класса вызывает функцию программного интерфейса GetWindowDC, а деструктор - функцию ReleaseDC.



Корректировка страниц свойств


Когда элемент управления имеет свойства, контроллеры автоматизации могут получать к ним доступ и изменять их. Но кроме этого необходимо и пользователю дать возможность делать то же самое. Элементы управления OLE, в соответствии с принятым соглашением, используют для этого страницы свойств.

Страницы свойств (property page) – это OLE-объекты, которые дают пользователю возможность просматривать и изменять свойства элемента управления. Страницы свойств содержат один или несколько диалоговых элементов управления, связанных с различными свойствами. Страницы свойств отображаются в виде модальных или немодальных диалоговых окон с закладками, причем каждой странице соответствует своя закладка.

Чтобы отобразить на экране страницы свойств элемента управления контроллеры автоматизации, например ActiveX Control Test Container, вызывают “глагол свойств” OLEIVERB_PROPERTIES элемента управления. В них обычно есть возможность сделать это через пункт меню “Properties”.

Элемент управления OLE может иметь не одну страницу свойств. Каждая страница обычно соответствует отдельной группе логически связанных свойств.

При автоматической генерации элемента управления ControlWizard автоматически генерирует одну пустую страницу свойств. Станицы свойств реализуются при помощи класса C++, порожденного от класса COlePropertyPage. Класс COlePropertyPage порожден от класса CDialog и содержит набор функций для взаимодействия с элементом управления.

Страницы свойств сами по себе являются элементами управления OLE. Это значит, что у них есть собственные CLSID, они регистрируются в системном реестре и могут быть созданы независимо от любого другого элемента управления. Это дает программисту возможность разработать т реализовать группу страниц свойств, совместно используемых различными объектами в разных программных проектах.

Страницы свойств бывают двух типов: страницы базовых свойств (stock property pages) и пользовательские страницы свойств (custom property pages).

Имеется три страницы базовых свойств, которые доступны элементам управления.
Эти страницы управляют базовыми свойствами, связанными с цветами (страница Colors), с изображениями (страница Pictures), с шрифтами (страница Fonts). Как и любые другие OLE-объекты, страницы базовых свойств имеют собственные CLSID, используемые библиотеками OLE для их идентификации: CLSID_CColorPropPage, CLSID_CPicturePropPage, CLSID_CFontPropPage.

Преимущество страниц базовых свойств заключается в их “интеллектуальности”. Они реализованы так, чтобы самостоятельно находить в библиотеке типов элемента управления информацию о свойствах интересующего их типа. Например, страница Colors ищет свойства с типом OLE_COLOR и автоматически предоставляет доступ к этим свойствам.

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

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

При проектировании следует помнить, что страницы свойств: не имеют заголовков; по умолчанию невидимы; не имеют границ; являются дочерними окнами. Следует убедиться, что соответствующие опции в редакторе ресурсов были установлены правильно.



Добавление страниц базовых свойств



Рассмотрим, как добавить в создаваемый элемент управления страницу базовых свойств Colors. Чтобы написать код, который добавляет базовую страницу свойств (Colors) к элементу управления, необходимо выполнить следующие инструкции: найти и модифицировать таблицу следующим образом:

BEGIN_PROPPAGEIDS(CNameCtrl, 2) PROPPAGEID(CNamePropPage::guid) PROPPAGEID(CLSID_CColorPropPage) END_PROPPAGEIDS(CNameCtrl)



Добавление и корректировка пользовательских страниц



Для создания новой пользовательской страницы необходимо создать диалоговую панель (отвечающую ранее перечисленным требованиям) и подключить к ней новый класс, реализующий ее работу.


Для создания класса страницы свойств используется ClassWizard. При его помощи необходимо создать новый класс, наследуемый от базового класса COlePropertyPage (не СPropertyPage), и включить его в проект элемента управления. Классы, порожденные от COlePropertyPage, наследуют от него CLSID и фабрику классов, сгенерированные MFC. Затем следует добавить идентификатор новой страницы свойств в таблицу страниц элемента управления, не забыв при этом изменить общее число страниц.

Для корректировки пользовательской страницы свойств нужно работать с соответствующей диалоговой панелью, отвечающей именно за эту страницу. Например, ControlWizard сгенерировал для элемента управления Name Control одну пользовательскую страницу свойств “General”, за которую отвечает диалоговая панель с идентификатором IDD_PROPPAGE_NAME. Можно добавить в нее стандартные элементы управления для манипуляций с теми или иными свойствами OLE- элемента управления. Для реализации этих действий следует присоединить к стандартным полям диалоговой панели страницы свойств переменные (подобно тому, как это делается для обычных диалогов), отметив при этом внешние имена свойств OCX-объекта, за которые отвечают эти переменные.


Куча библиотеки С периода выполнения, _heapmin и С++-операторы new и delete


Однако, куча Windows (и функция HeapAlloc) – это не та куча, с которой приложение будете работать чаще всего. Существует еще одна куча — ею управляет библиотека С периода выполнения (С RunTime library, CRT). Доступ к CRT-куче реализуется функциями malloc и free, напрямую вызываемыми операторами C++ new и delete. Эта куча оптимизирована для выделения блоков малого размера.

Конечно, функция malloc вызывает VirtualAlloc, но делает это очень хитро. При первом вызове она резервирует регион размером 1 Мб (в будущих версиях Visual C++ эти значения могут измениться) и передает блок памяти, размер которого кратен 64 Кб (если malloc вызван чтобы выделить память размером 64 Кб или менее, выделяется один 64-килобайтный блок. При последующих вызовах память выделяется по возможности из этого блока; в ином случае диспетчер кучи вызывает VirtualAlloc, чтобы передать дополнительную память. После того, как весь регион размером 1 Мб израсходован, malloc резервирует еще один регион размер 2 Мб, потом другой, но уже размером 4 Мб и т.д., передавая память по мере необходимости.

При вызове функции free диспетчер кучи помещает дескрипторы блоков памяти в односвязный циклический список свободных блоков памяти (free list), находящийся вне CRT кучи. Функция malloc использует этот список для последующего выделения памяти (если это возможно). Так как данный список весьма компактен, поиск свободных страниц ocyществляется быстро, без просмотра большого числа страниц виртуальной памяти.

Если процесс выделил много памяти из CRT-кучи, а потом освободил большую часть этой памяти, все страницы остаются переданными. Хотя оперативная память при этом может быть и не выделена, процесс занимает страницы в файле подкачки, которые остаются недоступными другим процессам. При вызове другой CRT-функции, _heapmin, диспетчер кучи возвращает все свободные страницы и, более того, освобождает любые, полностью возвращенные регионы.



Куча Windows и семейство функций GlobalAlloc


Куча (heap) — это пул памяти какого-либо процесса. Когда программе требуется блок памяти, онавызывает функцию, выделяющую память из кучи, а чтобы освободить ранее выделенную память, — функцию, парную первой. Выравнивание по границам, кратным 4 Кб, в этом случае не производится; диспетчер кучи использует пространство на выделенных ранее страницах или обращается к VirtualAlloc, чтобы получить дополнительные страницы. Одна из двух куч, с которыми программа работает, — куча Windows. Для выделения из нее памяти служит функция HeapAlloc, а для освобождения — функция HeapFree. HeapAlloc особенно удобна для выделения "крупных" блоков памяти.

Может быть, приложение и не будет явно вызывать НеарАlloc, но за него это сделает функция GlobalAlloc, унаследованная от Winl6. В идеальном мире 32-разрядных программ функция GlobalAlloc не понадобилась бы, но мы имеем дело с реальным миром. Все еще остается колоссальный объем кода, перенесенного из Winl6, в том числе OLE, в котором вместо 32-разрядных адресов применяются параметры типа "описатель памяти" (HGLOBAL).

Работа функции GlobalAlloc зависит от передаваемых ей атрибутов. Если ей указывается GMEM_FIXED, она просто вызывает НеарАlloс и возвращает адрес, приводя тип данных к 32-битному значению HGLOBAL. Если же ей передается GMEM_MOVEABLE, возвращаемое значение HGLOBAL является указателем на элемент "таблицы описателей" в данном процессе. В этом элементе содержится указатель на память, выделенную функцией HeapAlloc.

Зачем нужна "перемещаемая" (moveable) память, если она вводит еще один слой "непрямого управления"? Здесь мы встречаемся с наследием Winl6, в котором операционная система когда-то действительно перемещала блоки памяти. В Win32 перемещаемые блоки памяти существуют лишь для поддержки GlobalReAlloc, которая выделяет новый блок памяти, копирует в него содержимое старого блока, освобождает последний и помещает адрес нового блока в существующий элемент таблицы описателей.


К сожалению, многие библиотечные функции используют в качестве параметров и возвращаемых значений HGLOBAL, а не адреса памяти. Если такая функция возвращает HGLOBAL, приложение должно считать, что данная память выделена с атрибутом GMEM_MOVEABLE, и, следовательно, чтобы получить адрес памяти, надо вызвать функцию GlobalLock. (В случае фиксированной памяти GlobalLock просто возвращает переданный ей описатель как адрес.) Если от приложения требуется передать параметр HGLOBAL, то следует получить это значение с помощью GlobalAlloc(GMEM_MOVEABLE,...) — на тот случай, если вызываемая функция обращается к GlobalReAlloc и ожидает, что значение описателя не изменится.

Буфер обмена Windows использует фиксированную память, тем самым можно передавать адреса, возвращенные HeapAlloc, и приводить возвращаемые значения HGLOBAL к void-указателям. Можно также передавать фиксированный адрес OLE-функции, которая принимает параметр типа HGLOBAL, однако значения HGLOBAL, возвращаемые OLE-функциями, являются перемещаемыми, поэтому приложение должны вызывать GlobalLock.