Книга
Книга
ЧАСТЬ I
Основные сведения о Windows, Vusual C++- и каркасе приложения1
ГЛАВА 1 MicrosoftWindows и Visual C++.2
Модель программирования в Windews2
Обработка сообщений 2
Интерфейс графического устройства3
Программирование, основанное на ресурсах.3
Управление памятью.;.;3
Динамически подключаемые библиотеки.,4
Интерфейс прикладных программ Win32.4
Компоненты Visual C++4
Microsoft Visual C++ 6.0 и процесс построения программ5
Редакторы ресурсов и просмотр ресурсов проекта.7
Компилятор C/C++.7
Редактор исход ного текста.".7
■ Компилятор ресурсов8
Компоновщик:8
Отладчик8
AppWizard 9
ClassWizard9
Средство просмотра исходного кеда9
Интерактивная справочная система10
Диагностические утилиты10
. Контроль исходного кода11
Gallery.11
Microsoft Foundation Class Library 6.011
Microsoft Active Template Library11
ГЛАВА 2 Каркас приложений Microsoft Foundation Class Library.... .12
Назначение каркаса приложений12
.Путь, который Вам предстоит15
Понятие каркаса приложений.16
Каркас приложений и библиотека классов16
Пример на базе каркаса приложений16
Сопоставление сообщений в библиотеке MFC.19
Документы и их представление19
ЧАСТЬ II Класс «вид» библиотеки MFC..21
ГЛАВА 3 Знакомство с AppWizard — «Hello, world!».22
Что такое «ВИД*.22
Интерфейсы SDI и MDI.23
Пример ЕХОЗА23
Класс CExOSaView27
Рисование внутри окна представления: Windows GDI27
Функция-член OnDraw.27
Контекст устройства в Windows.27
Добавление кода рисования в программу ЕХОЗА27
Первое знакомство с редакторами ресурсов28
Содержимое ех03а,гс29
Работа с редактором диалоговых окон29
Конфигурации Win32 Debug и Wn32 Release.30
Активизация диагностических макросов31
Предкомпилированные заголовочные файлы31
Два способа запуска программы33
ГЛ АВ А 4 Основы обработки событий, режимы преобразования координат и окна с прокруткой34
Прием пользовательского ввода: функции таблицы сообщений34
Таблица сообщений35
Хранение состояния объекта «вид»: переменные-члены класса35
Инициализация переменных-членов класса «вид».36
Теория недействительного прямоугольника36
Клиентская область окна36
Арифметические операции с CRect, CPoint и CSize36
Попадает ли точка внутрь прямоугольника?37
Оператор LPCRECT.37
Попадает ли точка внутрь эллипса?,. 37
Пример ЕХ04А38
EX04AVIEW.H .. 38
EX04AVIEW.CPP.39
Использование ClassWizard с ЕХ04А41
Совместное использование AppWizard и ClassWizard42
Режимы преобразования координат.44
Режим преобразования ММ_ТЕХТ.44
Режимы преобразования координат с постоянныммасштабом45,
Режимы преобразования координат с переменным масштабом46
Преобразование координат.47
Пример ЕХ04В.48
Окно представления с прокруткой49'
Окно — это больше, чем видно на экране♦.... 50
Линейки прокрутки'.50
Различные способы прокрутки50
Функция OnlnitialUpdate50
Прием ввода с клавиатуры51
Пример ЕХ04С.51
Другие сообщения Windows.54
Сообщение WMLCREATE54
'Сообщение WM CLOSE .. 54
Сообщение WM_QUERYENDSESSIOH54
Сообщение WM_DESTROY54
Сообщение WM_NCDESTROY~55
ГЛАВА 5 Интерфейс графического устройства, цвет и шрифт56
Классы «контекст устройства». 56
Классы контекста дисплея CCIientDC и CWindowDC.57
Создание и уничтожение CDC-объекгов57
Состояние контекста устройства58
Класс CPaintDC.58
Объекты GDI58
Создание и уничтожение GDI-объектов59
Управление GDI-объектами.••••:59
Стандартные GDI-объекты 60
Время жизни контекста устройства 60
Преобразование цветов в Windows.61
Стандартные видеоплаты VGA»-...;.:•.•••.61
,Видеоплаты, поддерживающие 256 цветов62
-Видеоплаты с 16-разрядцым цветом. 63
Видеоплаты с 24-разрядным цветом63
Шрифты 63
Шрифты как GDI-объекты:.....63
Выбор шрифта63
Шрифты и вывод на печать64
■Отображение шрифтов на дисплее64
Логические и физинеские д юймы на дисплее.64
Вычисление высоты символа.,....,,..,..„.65
Пример ЕХ05А,66
Элементы программы ЕХ05А68
.Установка режима преобразования координат в функции OnPrepareDG68
■Закрытая функция-член ShowFont 68
Вызов CFont::CreateFont,’69
• ^Пример ЕХ05В,69'
,Элементы программы ЕХ05В..—. . . . ■. ; : -.■■.71
Функция-член OnDraw.71
Вспомогательная функция TraceMetrics72
Пример ЕХ05С.72
Элементы программы ЕХ05С..75
■Переменные-члены msizeBlipse и m_pointTopLeft.75
Переменная-член m_sizeOffset.75
Переменная-член m__bCaptured.75
- -Функции SetCapture и FteleaseCapture.75
Функции Win32 SetCursor и LoadCursor..75
Функция-член CScrollView::OnPrepareDC.75
'Преобразование координат в OnMouseMove.76
.Функция OnDraw.76
Режим SetScaleToFitSIze класса CScrollVlew.76
Режим преобразования координат «логический твил» в окне с прокруткой.76
ГЛАВА 6 Модальные диалоговые окна и стандартные элементы управления Windows77
Модальные и немодальные диалоговые окна.77
Ресурсы и элементы управления.78
Программирование модального диалогового окна.78
Пример ЕХ06А.79
Построение диалогового ресурса791
ClassWizard и класс «диалоговое окно».86
Подключение диалогового окна к классу «вид».89
Разбор приложения ЕХ06А90
Усовершенствование программы ЕХ06А91
Перехват контроля при выходе по ОпОК 91
Обработка OnCancel.92
Подключение элементов управления «полосы прокрутки»93
Доступ к элементам управления: CWnd-указатели и идентификаторы94
Выбор фона диалогового окна и цвета элементов управления94
Вывод графики в диалоговом окне95
Добавление элементов управления в период выполнения96
Использование других свойств элементов управления96
Стандартные элементы управления Windews97
Элемент управления «индикатор продвижения».98
Элемент управления «ползунок».98
Элемент управления «наборный счетчик».98
Элемент управления «графический список»99
' Элемент управления «древовидный список».99
Сообщение WM_NOTIFY99
Пример ЕХ06В100
Другие стандартные элементы управления Windows.108
ГЛАВА 7 Немодальные и стандартные диалоговые окна Windows.109
Немодальные диалоговые окна109
. Создание немодальных диалоговых окон109
Пользовательские сообщения110
Принадлежность диалогового окна110
Пример ЕХ07А110
Класс CFormView: альтернатива немодальному диалоговому окну115
Стандартные диалоговые окна Windows.115
Непосредственное использование класса CRIeDialog.116
Производные классы стандартных д иалоговых окон116
Вложение диалоговых окон.116
Пример ЕХ07В117
Прочие возможности адаптации CFileDialog121
ГЛАВА 8 Использование элементов управления ActiveX122
ActiveX и обычные элементы управления Windows.122
Основные характеристики обычных элементов управления123
Общие черты ActiveX и обычных элементов управления123
Различия ActiveX и обычных элементов управления123
Установка элементов управления ActiveX124
Элемент управления Calendar.125
Программирование контейнера ActiveX-элементов126
Доступ к свойствам127
Классы-оболочки C++, генерируемые ClassWizard для ActiveX-элеменга127
Поддержка ActiveX-элементов в AppWizard., 129
ClassWizard и диалоговое окно контейнера129
.Перементые-члены класса диалогового окна или использование
класса-оболочки?.129
Привязка событий от элементов управления ActiveX130
Блокировка ActiveX-элементов в памяти130
Пример ЕХ08А130
ACTIVEXDIALOG.H133
ACTIVEXDIALOG.CPP.134
ActiveX-элементы в файлах HTML137
Создание ActiveX-элементов в период выполнения138
Пример ЕХ08В138
Пример EX08G142
Свойства-картинки142
Связываемые свойства: уведомления об изменении142
Другие элементы управления ActiveX143
Г Л А В А 9 Стандартные элементы управления Internet Explorer 4 ... 144
Основы стандартных элементов управления Internet Explorer 4.144
Элемент выбора даты и времени144
Календарь на месяц146
Элемент ввода IP-адреса 146
Расширенный комбинированный список .. •146
Пример ЕХ09А147
ГЛАВА 1 О Управление памятью в Win32160
Процессы и адресное пространство.160
Адресное пространство процесса Windows 95.161
Адресное пространство Windows NT.1611
. Устройство виртуальной памяти163
Функция VlrtualAlloc: переданная и зарезервированная память”.165
Куча Windows и семейство функций GlobalAlloc165
Куча библиотеки С периода выполнения, „heapmin и С++-операторы new и delete166
Файлы, проецируемые в память167
Доступ к ресурсам168
Советы по работе с кучей168
Оптимизация хранения констант.169
Г Л А В А 11 Растровые изображения171
Растровые изображения GDI и DB.171
Цветные и монохромные растровые изображения172
Использование GDI-растров172
Загрузка GDI-растра из ресурса172
Влияние режима преобразования координат173
Растяжение изображений173
Пример ЕХ11А 173
Растровые изображения и улучшенный вывод на экран176
Пример ЕХ11В.176
Анимация в Windows.179
DIB-изображения и класс CDib.160
Несколько слов о программировании палитры180
DIB, пикселы и цветовые таблицы181
Структура DIB в ВМР-файле.182
Функции для работы с DIB183
Класс CDih.183
CDIB.H183
Производительность при выводе DIB-изображения на дисплей..;188
Пример ЕХ11С188
Еще несколько слов о DIB191
Функция Loadlmaga:191
Функция DrawDIBDraw.192
Растровые изображения на командных кнопках192
Пример EX11D193
И еще пара слов о растровых изображениях на кнопках195
ГЛАВА 12 Обработка сообщений Windows и программирование многопоточных приложений196
Обработка сообщений Windows.196
Обработка сообщений в однопоточной программе196
Передача управления197
Таймеры197
Пример ЕХ12А198
Обработка в периоды простоя200
Программирование многопоточных приложений201
Функция рабочего потока и запуск потока201
Общение основного потока с рабочим.;202
Общение рабочего потока с основным;203:
Пример ЕХ12В203.
Синхронизация потоков с использованием событий...........204
Пример ЕХ12С.205
Блокировка потоков207
Критические секции207
HMSH_.208
Мьютексы и семафоры",209
Потоки пользовательского интерфейса209
ЧАСТЬ II I Архитектура «документ-вид211
ГЛАВА 13 Меню, быстрые клавиши, поле Классы основного окна- рамки и документа212
Классы основного окна-рамки и документа212
Меню Windows213
Быстрые клавиши214
Обработка команд214
Обработка командных сообщений в производных классах215
Обновление командного пользовательского интерфейса215
Команды, генерируемые диалоговыми окнами.216
Меню, встроенные в каркас приложений.216
Включение и отключение команд в меню.217
Возможности редактирования текста в MFC217
Класс CEditView.217
Класс CRich EditView.217
Класс CRichEditCtrl.217
Пример ЕХ13А218
Окна свойств222
Создание окна свойств222
Обмен д анными в окне свойств223
И снова пример ЕХ1 ЗА223
PROPERTY.H.225
PROPERTY.CPP228
Обработка кнопки Apply.234
Класс CMenu235
Создание контекстных меню235
Расширенная обработка команд236
ГЛАВА 14 Панели инструментов и строки состояния237
Панели элементов управления и каркас приложений237
Панель инструментов238
Растровое изображение панели инструментов238
Состояния кнопок238
Панель инструментов и командные сообщения238
Обновление пользовательского интерфейса для панелей инструментов239
Всплывающие подсказки240
Поиск основного окна-рамки<240
Пример ЕХ14А240
Строка состояния244
Определение секций в строке состояния244
Строка сообщений244
Индикатор состояния245
Управление строкой состояния.’.245
Пример ЕХ14В246
Панель инструментов Internet bcplorer Rebar250'
Внутренняя структура rebar-панели250
Пример ЕХ14С'.251
ГЛАВА 15 Повторно используемый базовый класс окна-рамки255
Причины трудностей при создании повторно используемых базовых классов255
Класс CPersistentFrame 256
Класс CFrameiWnd и функция-член Activate Frame.256
Функция-член PreCreateWindow256
Реестр Windows257
Использование класса CString259
Положение развернутого окна260
Состояние панелей элементов управления и реестр261
Статические переменные-члены261
Оконный прямоугольник по умолчанию261
Пример ЕХ15А261
PERSIST. Н262
PERSIST.CPP.•262
Устойчивые рамки в М Dl-приложениях267
ГЛАВА 16 Документ и его представление268
Функции взаимодействия «документ-вид».268
Функция CView::GetDocument268
Функция CDocument::UpdaleAIIViewa269
Функция CView::OnUpdate269
Функция CView::OnlnitialUpdata270
Функция CDocument::OnNewDocument270
Простейшее приложение на базе архитектуры «документ-вид».270
Класс С FormView271
Класс CObject271
Диагностика272
Макрос TRACE272
Объект afxDump272
Классы CDumpConteXt й CObject273
Автоматическая диагностика неуничтоженных объектов273
Пример ЕХ16А275
STUDENT.H276
S7UDENT.CPP277
Усложненное взаимодействие «документ-вид»281
Функция CDocument-DetoteConterts282
Класс набора CObList282
Применение класса CObList для создания списков типа RFO282
Перебор элементов CObList: переменная типа POSITION.283
Класс-шаблон набора CTypedPtrList284
Диагностика и классы наборов285
' Пример ЕХ16В285
Требования к ресурсам286
Панель инструментов287
Меню Student287
Меню Edit287
Диалоговый шаблон IDD_STUDENT.287
Требования к коду.287
СЕх16ЬАрр.288
CMainFrame . . . ,♦288
CStudent288
CStudentDoc288
STUDOC.H288
STUDOC.CPP289
ClassWizard и CStudentDoc291
Переменные-члены291
Конструктор. 291
GetList291
DeleteContents.291
Dimp.291
CStudentView.292
STUVIEW.H292
STUVIEW.CPP293
ClassWizard и CStudentView.297
Переменные-члены298
OnlnitialUpdate298;
OnUpdate299
Защищенные виртуальные функции299
Тестирование программы ЁХ16В299
Пара упражнений для читателя299
ГЛ А В А 1 7 Чтение и запись документов в SDI-приложениях300
Понятие сериализации300
. Дисковые файлы и архивы301
Созд ание сериализуемого класса301
Создание функции Serialize302
Загрузка из архива: внедренные объекты и указатели303
Сериализация наборов 304
■Функция Serialize и каркас приложений.'.304
SDl-приложение.305
Объект-приложение Windows.305
Класс шаблона документа306
Ресурс шаблона документа.307
■Множественное представление документа в SDI-программах308
Создание пустого документа:функцияCWinApp::OnpileNew308
Функция On New Document класса«документ».308
Связывание File Open с кодомсериализации: функция OnFileOpen.309
Функция DeleteContents класса «документ».309
Связывание File Save и File Save As с кодом сериализации309
Флажок «документ изменен».310
Пример ЕХ17А310
CStudent311
СЕх1 7аАрр311
ЕХ17А.Н312
ЕХ17А.СРР.313
CMainFrame315
MAINFRM.H315
MAINFRM.CPP.316
CStudentDoc318
Serialize 318
DeleteContents.319
OnOpenDocument319
OnUpdateFileSave.319
CStudentView.319
-Тестирование приложения EX17A320
Запуск программ из Windows Explorer и операция drag-and-drop.’320
Регистрация программы320
Двойной щелчокдокумента321
Активизация механизма drag-and-drop321
Параметры командной строки321
Эксперименты с запуском программы из Windows Explorer и операцией -drag-and-drop322
ГЛАВА 18 Чтение и запись документов в MDI-приложениях323
MDI-приложениа323
Типичное М Dl-приложение в стиле MFC.323
■Объект «MDI-приложение». 325
.Класс шаблона MDl-документа325
,.Окно-рамка и дочернее окно в MDI-программе'.326
Ресурсы основного окна-рамкии шаблона документа327
Создание пустого документа функция O/tinApp::0nRleNew327
Создание дополнительного окна представления для существующего документа328
Загрузка и сохранение документов328
Множественные шаблоны документов328
Запуск MDI-программ из Windows Explorer и операциейdrag-and-drop329
Пример ЕХ18А329
СЕх18аАрр329
ЕХ18А.Н330
ЕХ18А.СРР331
CMainFrame333
MAINFRM.H334
MAINFRM.CPP335
CChildFrame336
CHILDFRM.H337
CHILDFRM.CPP.338
Тестирование приложения ЕХ18А339
ГЛАВА 1 9 Печать и предварительныйпросмотр340
Печать в Windows.340
Стандартные диалоговые окна печати.'.340
Интерактивный выбор страниц для печати342
Экранные и отпечатанные страницы342
Предварительный просмотр перед печатью342
Программирование вывода на печать343
Контекст принтера и функция CView::OnDraw.343
Функция CView::OnPrint343
Подготовка контекста устройства: функция CView::OnPrepareDG343
Начало и конец печати344
Пример ЕХ19А344
Огредепеше области печати349
Еще раз о классах-шаблонах наборов: класс САггау350
Пример ЕХ19В351
ГЛАВА 20 Разделяемые окна и множественное представление данных356
Разделяемое окно.:356
Варианты создания множественных представлений357
Динамически и статически разделяемые окна357
Пример EX2QA .357
Ресурсы для разделения окна358
CMainFrame.358
Тестирование приложения ЕХ20А359
■ Пример ЕХ20В359
CHexView.359
CMainFrame360
Тестирование приложения ЕХ20В.361
Пример ЕХ20С 361
Требования к ресурсам361
CMainFrame.361
Тестирование приложения ЕХ20С362
Пример EX20D.363
Требования к ресурсам363
CEx20dApp363
CMainFrame364
Тестирование приложения EX20D.365
ГЛ АВ А 21 Контекстно-зависимая справка366
Программа Win Help366
Текст с форматированием.366
Пример ЕХ21 А: подготовка простого справочного файла367
Совершенствование оглавления .,370
Каркас приложений и WinHelp371
Вызов WinHelp371
Применение строк для поиска372
Вызов WinHelp из меню программы372
Синонимы контекстной справки372
Определение контекста справки.373
Вызов справки клавишей F1373
Вызов справки сочетанием клавиш Shift+F1373
- Справка в информационном окне: функция AfxMessageBox374
Стандартные справочные разделы374
Пример создания справки без программирования 375
■ Создание справочной системы376
Обработка команд запроса справки377
. Обработка клавиши F1,.;377
Обработка сочетания клавиш Shift+Fl.378
Пример ЕХ21В.378
Требования к заголовочным файлам378
CStringView.379
CHexView379
Требования к ресурсам380
Требования к справочному файлу.380
Тестирование приложения ЕХ21В.380
ГЛАВА 22 Динамически подключаемые библиотеки .381
Основы DLL381
Согласование импортируемых элементов с экспортируемыми382
Явное и неявное связывание383
Связывание по символьным именам и порядковым номерам383
Точка входа в DLL функция DIIMain384
Описатели экземпляров и загрузка ресурсов384
Порядокпоиска DLL клиентской программой385
Отладка DLL .'385
DLL-расширения и обычные DLL385
. - Общие DLL MFC-библиотеки.386
DLL-расширения: экспорт классов386
Последовательность поиска ресурсов в DLL-расширении387
Пример ЕХ22А387
Пример ЕХ22В389
■■Обычные DLL класс, производный от CWinApp.390
Использование макроса AFX_MANAGE_STATE390
Последовательность поиска ресурсов в обычной DLL390
Пример ЕХ22С390
Коррекция ЕХ22В для проверки ex22c.dll.391
DLL с пользовательскими элементами управления.393
.Понятие пользовательского элемента управления393
Оконный класс пользовательского элемента управления393
Библиотека MFC и функция WndProc394
Уведомляющие сообщения пользовательскихэлеменговуправления.394
Пользовательские сообщения, посылаемые элементу управления394
Пример EX22D395
ЕХ22.СРР395
RYGWND.H396
. RYGWND.CPP.397
Коррекция ЕХ22В для проверки ex22d.dll400
ГЛАВА 23 MFC-программы без классов «документ» и «вид»402
Пример ЕХ23А402
Функция Iniflnstance класса приложения404
Класс диалогового окна и значок программы405
Пример ЕХ23В405
Пример ЕХ23С.407
ЧАСТЬ IV СОМ, Автоматизация и OLE409
ГЛАВА 24 Модель многокомпонентных объектов410
■ Основы технологии ActiveX410
Модель многокомпонентных объектов.411
Проблема, которую решает СОМ.411
Сущность СОМ.411
СОМ-интерфеЙс 412
Интерфейс IUnknown и функция-член Querylnterfaca416
Учет ссылок: функции AddRef и Release417
Фабрики класса418
Класс CCmdTarget420
Пример ЕХ24А420
INTERFACED420
SPACESHIP.H422
SPACESHIP.CPP423
CLIENT.CPR.425
Реальная СОМ с применением MFC426:
COM-функция CoGetCiassObject426
СОМ и Реестр Windows.427
Регистрация объекта в период выполнения428
Вызов СОМ-клиентом внутреннего компонента428
Вызов СОМ-клиентом внешнего компонента;)430
MFC-макросы для интерфейсов432
MFC-класс COIeObjectFactory.433
Поддержка внутренних COM-компонентов со стороны AppWizard и ClassWizard433
COM-клиенты на базе MFC.434
Пример ЕХ24В435
INTERFACE.H435
SPACESHIP.H435
SPACESHIP.CPP.436
Пример ЕХ24С.438
Вложение и агрегирование по сравнению с наследованием439
ГЛАВА 2 5 Автоматизация441
Взаимодействие C++ с Visual Basic for Applications441
Клиенты и компоненты Автоматизации442
Microsoft Excel — лучший Visual Basic, чем сам Visual Basic.442
Свойства, метопы и наборы'444
Задачи, решаемые Автоматизацией445
. Интерфейс IDispatch445
Варианты программирования Автоматизации446
MFC-реализация IDispatch447
Компонент Автоматизации на базе MFC.447
Клиент Автоматизации на базе MFC.448
Использование директивы компиляции #import клиентом Автоматизации450
Тип данных VARIANT451
Класс COIeVariant453
Преобразования типов параметров и возвращаемых значений для Invoke.454
Примеры Автоматизации455
Пример ЕХ25А456
Пример ЕХ25В463
EX25BAUTO.H.466
EX25BAUTO.CPP.467
Пример ЕХ25С.470
Пример EX25D.475
Библиотеки типов и ODL-файлы476
Класс контроллера для ex25a.exe.478
BANKDRIVER.H478
BANKDRIVER.CPP.478
Класс контроллера для ex25b.dll480
AUTODRIVER. К.480
Класс контроллера для ex25c.exe481
CLOCKDRIVER. Н481
Управление приложением Microsoft Бесе!483
Пример ЕХ25Е487
Раннее связывание в VBA490
Регистрация библиотеки типов.490
Регистрция компонентом своей библиотеки типов491
ODL-файл491
Использование библиотеки типов в Бесе!.492
Назначеше раннего связывания493
Ускорение подсоединения контроллера к компоненту.494
ГЛАВА 26 Унифицированная передача данных495
Интерфейс IDataObject495
Преимущества IDataObjectno сравнению со стандартной поддержкой буфера обмена . 495
Структуры FORMATETCh STGMEDIUM496
FORMATETC.".. , 496
STGMEDIUM496
Функции-члены интерфейса IDataObject497
Поддержка унифицированной передачи данных в MFC.498
Класс COIeDataSource.498
Класс COIeDataObject499
Передача объекта данных через буфер обмена499
MFC-класс CRectTracker. 501
■ Преобразование координат прямоугольника CRectTracker.502
Пример ЕХ26А502
Класс CMainFrame502
Класс СБ(26аОос503
Класс CEx26aView.503
EX26AVIEW.H:503
EX26AVIEW.CPP505
Поддержка drag-and-drop в MFC510
Что происходит в источнике510
Что происходит в приемнике.510
Последовательность действий приdrag-and-drop..511
Пример ЕХ26В.511
Класс CEx26bDoc512
Класс CEx26bView512
Приложения Windows и drag-and-drop: Dobjview514
ГЛАВА 2 7 Структурированное хранилище.515
Составные файлы.515
Хранилища и интерфейс IStorage516
Получение указателя на IStorage.517
Освобождение памяти в STATSTG517
Перечисление элементов хранилища518
Совместное использование хранилища несколькимипроцессами.518
Потоки и интерфейс IStream.518
Программирование IStream.519
Интерфейс ILockBytes519
Пример ЕХ27А520
Меню520
Класс CEx27aView.520
Рабочие потоки кода520
TH READ. Н'520
WRITETHREAD.CPP520
READTHREAD.CPP.522
Структурированные хранилища и устойчивые СОМ-объекты523
Интерфейс IPersistStorage; 524
Интерфейс IPersistStream525
/ Программирование IPersistStream525
Пример ЕХ27В525
ТЕХТ.Н•525
ТЕХТ.СРР527
Пример EX27G530
WRITETHREAD.CPP530
READTHREAD.CPP.531
Фрагментация составных файлов533
Другие преимущества составных файлов533
ГЛАВА 28 Внедряемые компоненты и контейнеры OLE535
Внедрение и Visual Editing535
Полные и мини-серверы: связывание536
Обратная сторона Visual Editing536
Метафайлы Windows и внедренные объекты537
, Архитектура OLE-компонента на базе MFC.538
Пример ЕХ28А538
Внедряемый MDI-компонент?542
Стратегия масштабирования для компонента, активизируемого по месту.542
Взаимодействие контейнер-компонент.543
Применение интерфейса компонента ICIeObject544
Загрузка и сохранение внутренних данных компонента: составные документы544
Передача данных через буфер обмена544
Получение метафайла от компонента545
Роль внутрипроцессного обработчика545
Состояния компонента .. 546
Интерфейсы контейнера546
Уведомления547
Метафайл для буфера обмена547
Сводка интерфейсов.548
Интерфейс lOleObject548
Интерфейс IViewObject2.549
Интерфейс lOleClientSite549
Интерфейс lAdviseSink549
Вспомогательные функции OLE549
Контейнерное OLE-приложение с поддержкой внедрения.550
Поддержка MFC для OLE-контейнеров550
Некоторые ограничения контейнера551
Возможности контейнера551
Пример ЕХ28В551
Класс CEx28bView.t551
EX28BVIEW.H552
EX28BV1 EW.CPP.554
GetSize561
SetNames.561
SetViewAdvise561
IMakeMetatilePict562
SaveObject562
DoPasteObjectDescriptor.562
DoPasteObject562
Класс CEx28bDoc,562
EX28BDOC.H562
EX28BDOC.CPP.564
Пример EX28C569
Класс CEx28cView,569
Класс CEx28cDoc569
EX28CDOC.H569
ГЛАВА 29 Основы ATL574
Снова COM574
Базовый интерфейс IUnknown575
Написание кода576
Классы СОМ на основе множественного наследования577
Инфраструктура СОМ578
Новый каркас579
ActiveX, OLE и СОМ579
ActiveX, MFC и СОМ579
Путеводитель по AIL580
Программирование клиента с помощью ATL580
Шаблоны C++.581
Smart-указатели582
Наполнение указателей «интеллектом».583
Применение smart-указателей.584
Smart-указатели и СОМ584
Smart-указатели в ATL585
CComPtr.585
Использование CComPtr.586
CComQIPtr.588
Использование CComQIPtr.588
Проблемы применения smart-указателей589
Программирование сервера с помощью АН590
ATL и классы СОМ,590
Параметры ATL COM AppWizard591
Создание СОМ-класса:... 592
Подразделения и потоки592
Точки соединения и ISupportErrorinfo594
Маршалер свободных потоков594
Реализация класса Spaceship с использованием ATL595
Базовая архитектура ATL596
Управление размером vtbl596
ATL-версия IUnknown;CComObjectFtootEx-.597
ATL и Query Interface599
Запуск космического корабля601
Добавление методов к интерфейсу.602
Дуальные интерфейсы604
ATL и IDispatch605
Интерфейсы IMotlon и IVisual606
Несколько дуальных интерфейсов607
Г Л А В А 3 О ATL и элементы управления ActiveX608
Элементы управления ActiveX608
Использование ATL для создания элементов управления609
■Создание элемента управления609
Архитектура элемента управления на основе ATL612
CComComtrol.613
CComControlBase,614
CWindowImpI и CWindowImpIBase614
Окна в ATL614
Таблицы сообщений в ATL.616
Проектирование элемента управления617
Отрисовка617
Отклик на оконные сообщения620
Добавление свойств и страниц свойств622
Страницы свойств.624
Устойчивость свойств632
Двустороннее взаимодействие (события).633
Использование элемента управления638
ЧАСТЬ V Работа с базами данных639
Г Л А В А 3 1 Microsoft ODBC. 640
Преимущества СУБД640
SQL641
Стандарт ODBC641
Архитектура ODBC.642
Использование ODBC SDK642
MFC-классы ODBC: CRecordset и CDatabase.644
Подсчет строк в наборе записей.645
Обработка исключений ODBC646
База данных для учета студентов646
Пример ЕХ31А646
Элементы программы ЕХ31А651
Интеграция класса набора записей в приложение651
Функция-член OnlnitialUpdate класса CEx31aView.652
Функция-член OnDraw класса СЕх31 aView652
Строки для фильтрации и сортировки653
Объединение двух таблиц базы данных653
MFC-класс CRecordView.654
Пример ЕХ31В654
Несколько наборов записей.659
Пример ЕХ31С660
Параметризованные запросы661
Многопоточность в ODBC.661
Групповая выборка строк661
Использование наборов записей без привязки661
Г Л А В А 3 2 Microsoft DAO.663
DAO, СОМ и ядро баз данных Microsoft Jet663
DAO и VBA663
DAO и MFC.664
Варианты работы с базами данных665
Использование DAO в режиме ODBC: снимки и динамические множества665
Табличные наборы записей DAO.666
DAO-классы QueryDef и TableDef. 666
■ Многопоточность в DAO.666
Отображение строк базы данных в окне с прокруткой.666
Варианты прокрутки666
Класс строкового представления667
ROWVIEW.H667
Разделение труда между базовым и производным классами668
Чисто виртуальные функции-члены CRowView668
Другие функции CRowView,669
Программирование динамического набора записей.670
Пример ЕХ32А670
СЕх32аАрр671
CMainFrame и CChildFrame671
CEx32aDoc671l
Переменные-члены671i
OnOpenDocument672
OnCloseDocument672
OnFileDaoOpenOdbc и DaoOpenOdbc.672
OnFileDaoOpenlsam и DaoOpenlsam673
OnFileDaoOpenMdb и DaoOpenMdb674
OnFileDaoDisconnect675
OpenRecordset675
OnRequery676
PutQueryn GetQuery.,676
Serialize.676
CEx32aView676
Переменные-члены676
OnUpdate676
' GetRowWidthHeight, GetActiveRow, ChangeSelectionNextRow и ChangeSelectionToRow!677
GetRowCount677
OnDrawRow и DrawDataRow.677
OnlnitialUpdate и OnTimer.678
GTableSelect679
Переменные-члены679
Конструктор679
OnlnitDialog679
OnDblcIkList!680
ClsamSelect680
Переменные-члены680
OnlnitDialog680
Файл ресурсов ЕХ32А681
Работа с программой ЕХ32А681
Г Л АВ А 33 Шаблоны OLE DB682
Понятие OLE DB.682
Основы архитектуры OLE DB683
Основы архитектуры шаблонов OLE DB.684
Архитектура шаблонов OLE DB-потребителя,684
Базовая поддержка источников д анных6В5
Поддержка доступа к данным и наборов строк685
Поддержка таблиц и команд686
Архитектура шаблонов OLE DB-поставщика,686
Совместная работа всех компонентов поставщика689
Создание OLE DB-потребителя690
Использование OLE DB-потребителя692
Создание OLE DB-поставщика693
Цель создания OLE DB-поставщика693
Написание OLE DB-поставщика693
Модификация кода поставщиса.698
Улучшение поставщика699
ЧАСТЬ VI.
Программирование для Интернета701
ГЛ АВ А 34 TCP/IP, Winsock и Winlnet702
«СОМ иль не СОМ?»,,702
Основы Интернета702
Сетевые протоколы и их уровни702
Протокол IP.703
Протокол UDR704
Формат IP-адреса и сетевой порядок байтов705
Протокол TCP705
Система доменных имен706
Серверы и имена доменов707
Клиенты и имена доменов708
Основы HTTP708
Основы FTP.710
'Интернет и интрасеть.710
Создание интрасети710
NTFS или FAT.710
Сетевое аппаратное обеспечение..711
Настройка Windows для работы в сети711
Имена хостов для интрасети: файл HOSTS.711
Тестирование интрасети: программа Ping712
Интрасеть на одном компьютере: адрес локального хоста712
Winsock..712
Синхронное и асинхронное программирование.712
Классы Winsock в MFC.712
■ ■•Классы блокирующих сокетов.713
- ■Вспомогательный класс CSockAddr..713
Класс CBlocklngSocketException.714
Класс CBIockingSocket.714
BLOCKSOCK.H.714
•Класс CHttpBlockingSocket.718
Упрощенный сервер HTTP..719
Инициализация Winsock..719
Запуск сервера.719
Серверный поток.719
Очистка720
Упрощенный клиент HTTP..721
Создание Web-сервера при помощиCHttpBlockingSocket.722
■Ограничения сервера ЕХ34А.722
Архитектура сервера ЕХ34А.722
*Использование функции Win32 TransmitFile.723
Сборка и тестирование ЕХ34А.723
Использование Telnet.724
Создание Web-клиента с помощьюCHttpBlockingSocket.724
Winsock-клиент ЕХ34А.725
■Поддержка прокси-серверов в ЕХ34А.725
Тестирование Winsock-клиентаЕХ34А.725
Winlnet.725
Преимущества Winlnet по сравнению с Winsock.725
Winlnet-классы в MFC..726
ClnternetSession.726
CHttpConnection.726
.CFtpConnection, CGopherConnection.727
ClnternetFile727
CHttpFile.727
CFtpFileFind, CGopherFileFind..727
ClnternetException727
'Функции обратного вызова.727
.Упрощенный Win I net-клиент_.728
.Создание Web-клиента при помощи Winlnet-классов MFC.729
Winlnet-клиент №1.729
.Тестирование Winlnet-клиента №1.729
Winlnet-клиент №2.730
Тестирование Winlnet-клиента №2..730
Асинхронные файловые моникеры.730
.Моникеры.731
'Класс CAsyncMonikerFile в MFC.731
Использование класса CAsyncMonikerFile в программе..731
Асинхронные файловые моникеры или программирование с помощью Winlnet732
ГЛАВА 35 Программирование для Microsoft Internet *Information Server.733
Альтернатива IIS.,.733
Microsoft IIS. .733
"Установка иуправление.733
Запуск Internet Service Manager..734
Зашита IIS..734
Каталоги IIS..735
Регистрация соединенийIIS736
' Тестирование IIS736
ISAPI-расширения сервера736
CGI и ISAPl736
Простой запрос GET к ISAPl-расширению.736
HTML-формы: GET или POST?..737
Создание DLL ISAPI-расширения сервера.738
MFC-классы для ISAPI-расширения сервера.739
CHttpServer.739
CHttpServerContext.739
CHtmlStream.‘.. 740
Пример ЕХ35А740
Первый шап получение заказа740
Второй шап обработка подтверждения.742
Сборка и тестирование ex35a.dll.743
Отладка ЕХ35А DLL.743
Доступ к базам данных из ISAPI-расширения.744
Использование подсказок745
Работа подсказок.745
Обработка подсказок ISAPl-расширением сервера.745
Проблемы при использовании подсказок.745
Аутентификация.746
Базовая аутентификация.746
Аутентификация Windows NT Challenge/Response..746
Уровень защищенных сокетов..747
ISAPl-фильтры.747
Создание DLL ISAPl-фильтра.747
MFC-классы для ISAPl-фильтра.748
CHttpFilter.z.748
CHttpFilterContext' .748
Пример ISAPl-фильтра: ex35b.dll, ехЗбаехе..748
Выбор уведомления.748
Перед ача д анных о транзакции в программу просмотра.749
Программа отображения.750
Сборка и тестирование ISAPl-фильтра ЕХ35В.,750
Г Л А В А 3 6 Интернет и серверы ActiveX-документов.752
Основы ActiveX-документов..752
Сервер ActiveX-документов в сравнении с внедренным OLE-сервером.752
Запуск сервера ActiveX-документов в браузере Internet Explorer..754
Сервер ActiveX-документов в сравнении с ActiveX-элементом.754
'Интерфейсы OLE в серверах и контейнерах ActiveX-документов.754
Поддержка серверов ActiveX-документов библиотекой MFC.755
1 COIeServerDoc.755
CDocObjectServerltem.755
CDocObjectServer..756
COIeDocIPFrameWndl.756
Пример EX36A756
Этап 1 EX36A: простой сервер.756
Отладка сервера ActiveX-документов..756
Этап 2 ЕХ36А: добавление вызовов Winlnet...... 757
Пример ЕХ36В.758
Программная генерация POST-запросоа.762
Класс «вид» в примере ЕХ36В..763
Сборка и тестирование ЕХ36В.'..765
Серверы ActiveX-документов и VB Script..765
Дальнейшее знакомство с серверами ActiveX-доку ментов..765
ГЛАВА 37 Введение в Dynamic HTML766
Модель DHTML-объектов .766
Visual C++ и DHTML.770
Примеры ЕХ37А и ЕХ37В 770
Пример ЕХ37С.775
Допогнителзная информация.776
ГЛАВА 38 Visual C++для WindowsСЕ777
Устройства Windows СЕ777
Windows СЕ и Windows 98/NT..779
Visual C++ for Windows СЕ.780
MFC для Windows СЕ.782
Пример ЕХ38А.783
Пример ЕХ38В.788
ATL и Windows СЕ.790
Дополнительная информация.791
ПРИЛОЖЕНИЕ А’
Функции таблицы сообщений в MFC792
ПРИЛОЖЕНИЕ Б
Идентификация классов во время выполнения и динамическое создание объектов798
Определение имени класса объекта в период выполнения.798
Структура CRuntimeClass и макрос RUNTIME-CLASS в MFC,.799
• Динамическое создание объектов799
Пример программы800
Предметный указатель803
Благодарности
Впервые я встретил Дэвида Круглински (David Kruglinski) на конференции разработчиков программного обеспечения. Я только что оставил свою работу и основал новую фирму. Дэвид ставил автографы на одном из предыдущих изданий этой книги. Основным направлением деятельности нашей фирмы должны были стать библиотеки классов Microsoft Foundation Classes (MFC)/Microsoft Visual C++. Я надеялся, что Дэвид выслушает наши идеи и, может быть, выскажет свое мнение о наших продуктах и степени их полезности сообществу разработчиков на Visual C++ — сообществу, которое он помог создать и понимал как никто другой.
К моему удивлению, Дэввду очень понравились наши идеи, и он попросил меня прислать ему оценочные копии наших продуктов. После этого между нами завязалась длительная переписка по электронной почте на тему наших продуктов и путей их улучшения. Дэвид тратил свое время, делился опытом и давал ценные советы, никогда не требуя за это какой-либо компенсации — он действительно хотел помочь нам создать продукты для разработчиков на Visual C++, которые сделали бы MFCA’isual C++ еще привлекательнее.
Впервые я узнал о гибели Дэвида из сообщения в группе новостей Интернета, и сначала подумал, что это чья-то глупая шутка. Я позвонил агенту Дэвида, и к моему ужасу это оказалось правдой. С уходам Дэвида сообщество разработчиков на Visual C++ потеряло одну из своих ярчайших звезд. Спросите любого разработчика на Visual C++ о том, чтобы было его учебником по Visual C++, и он, несомненно, ответит: «Книга Круглипски!». Сообщество разработчиков на Visual C++ очень обязано Дэвиду за его вклад и за то, что он много лет учил нас работе с Visual C++. Без всякого сомнения, он заслуживает особой благодарности заданную книгу, и мы выражаем глубокое соболезнование его семье и близким. ■
Это великая честь продолжать традиции Круглински этим новым, пятым изданием Inside Visual C++ (теперь Programming Microsoft Visual C++). Мы приложили максимум усилий к тому, чтобы книга получилась такой, какой ее представлял себе Дэвид, стараясь следовать его стилю.
Благодарю своих жену Крис и сына Шона за то, что они стойко вынесли все эти поздние ночи и уик-энды, когда я был занят работой над книгой. Я также очень благодарен своему соавтору Джорджу Шеферду, чья неординарная личность всегда стимулировала мои полуночные труды над книгой. Спасибо нашему агенту Клэр Хорн (Claire Ноше), благодаря которой мы смогли принять участие в этом проекте.
Visual C++ и, таким образом, данная книга не существовали бы, если бы не сотрудники группы разработки Visual C++. Особые благодарности за помощь на протяжении многих лет заслужили Майк Блашчак (Mike Blaszczak), Уолтер Салливан (Walter Sullivan), Дин Мак Крори (Dean McCrory), РикЛаплант (RickLaplante), МариХью (Marie Huwc), Кристиан Гросс (Christian Gross) и Джим Спригфидц (Jim Springfield).
Наконец, но далеко не в последнюю очередь, благодарность заслужили сотрудники Microsoft Press, работавшие над этой книгой, — особенно Кэтлин Откипс (Kathleen Atkins), Джим Фаче (Jim Fuchs), Бека Маккей (Веска McKay), Джон Пирс (John Pierce), Джин Росс (Jean Ross), Эрик Строо (Eric Stroo) и все, кто напряженно работал, чтобы эта книга как можно скорее попала к Вам в руки и была наивысшего качества.
Скотт Уингоу
Написание новой книги, и даже нового издания старей, требует большого труда. Мне хотелось бы поблагодарить следующих людей, которые помогли сделать эту книгу. Во-первых, благодарю свою жену Сэнди, не разлучавшуюся со мной, пока я работал над страницами книги. Она всесторонне поддерживала меня во время моих программистских «опытов». Спасибо моему сыну Тедди, терпеливо переносившему, когда я уходил по делам. Хочу поблагодарить свою мать Бетси, которая пробудила во мне желание писать, и моего брата-близнеца Патрика, сторонника Java, который аргументировано спорил со мной по вопросам программных и аппаратных платформ.
Благодарю Клэр Хорн (Claire Home) и агентство Moore Literary Agency, которые способствовали началу работ в Microsoft Press над данной книгой.
Благодарю Скотта Уингоу, который взялся за работу еще над одной книгой вместе со мной. Спасибо также сотрудникам Stingray — с ними отлично работается. Спасибо всем сотрудникам Develop Mentor, обеспечившим отличную среду для обучения. Также спасибо Дону Боксу (Don Box), объясняющему СОМ на понятном языке.
Для создания книги одних авторов недостаточно. Я хочу поблагодарить всех в Microsoft Press, кто помог выпустить эту книгу, а особенно Эрика Строо за доброжелательность и терпение, Кэтлин Откинс и Беку Маккей, которые буквально «просеяли* наш текст и сделали его более читабельным, Джина Росса, который помог «отловить* самые мельчайшие технические неточности, и Джона Пирса, который все это организовывал.
Наконец, благодарю Дэвида Круглински, начавшего этот проект. Хотя мне и не посчастливилось встречаться с Дэвидом, его книги очень помогли мне при изучении MFC. Надеюсь, что Скотт и я отдали должное его заслугам.
Джордж Шеферд
Введение
Выход версии 6.0 Visual C++ показал, что основные усилия Microsoft по-прежнему сосредоточены в области технологий Интернета и СОМ, которые являются ключевыми компонентами Windows Distributed interNet Application Architecture (DNA). Помимо поддержки этихплат- форм, Visual C++ 6.0 также содержит впечатляющий набор средств повышения производительности труда, в том числе Edit And Continue, IntelliSense, AutoComplete и подсказки по коду (code tips). Все эти возможности поднимают Visual C++ на новый уровень. При написании данной книги нашей основной задачей было помочь Вам не отстать от технологий, появившихся в Visual C++ 6.0. ‘
MFC, ATL и WFC- MFC мертва? 1
Еще посж выхода Microsoft ATL (Active Template library) в составе Visual C++ у разработчиков программ для Windows появилось подозрение, что Microsoft Foundation Class Library (MFC) «вышла из моды» в Microsoft, и дальнейшие усилия будут сосредоточены на развитии новых библиотек, таких как ATL. Недавно Microsoft выпустила еще одну библиотеку классов WFC (Windows Foundation Classes) для разработчиков, пишущих для Windows на Java, что, к сожалению, способствовало распространению слухов на тему «MFC мертва».
Разговоры о смерти MFC, несомненно, преждевременны. В Visual C++ 6.0 значительно расширены как MFC, так и ATL. Это доказывает, что обе библиотеки развиваются и получают внимание в одинаковой степени. Частично проблема состоит в том, что иногда направления разработки обеих библиотек не объявляются вполне четко, и разработчики, использующие Visual C++, недостаточно ясно их представляют. МРСрэзрабатыиается как мощная библиотека классов для создания сложных Windows-приложений с богатым графическим интерфейсам. ATL предназначена для создания очень небольших COM-объектов и элементов управления ActiveX. Эти два различных направления привели к созданию двух разных библиотек в помощь разработчикам.
Другой распространенной ошибкой является мнение о том, что MFC и АТЕисключаютдруг друга. Это совершенно не так! На самом деле, очень легко создать СОМ -объекты, использующие MFC, на основе ATL Единственная проблема состоитв том, что поскольку многие разработчики выбирают ATL за то, что она позволяет получать код малого размера, постольку использование MFC, которая богата возможностями и является «тяжелой», выглядит противоречащим причине использования ATL Хотя это может быть важно для некоторых разработчиков, по вовсе не делает ATL и MFC взаимоисключающими.
Хотя АП и не заменяет MFC, она является важной частью Visual C++, поэтому в данном издании книги добавлено две главы, посвященные этой библиотеке.
C++ и Java
Б течение нескольких последних лет значительно вырос интерес к языку программирования Java. Чем C++ лучше Java? Во-первых, скомпилированная программа всегда будет работать быстрее интерпретируемой. Представьте себе мощную программу электронных таблиц с поддержкой формул для ячеек и макросами, Теперь представьте себе виртуальную машину
Java, интерпретирующую код, который в свою очередь, интерпретирует формулы и макросы. Не очень быстро, не такли? При использовании технологии компиляции just-in-time компилировать программу необходимо при каждом ее запуске. Но будет ли полученный код исполняться столь же быстро, как код на выходе оптимизирующего компилятора C++?
Скорость работы — это только один из факторов выбора языка; другим является возможность доступа к операционной системе (ОС). По соображениям безопасности аплеты Java не могут выполнять такие операции, как запись надиски обращение к последовательным портам. Для достижения независимости от платформы прикладные программы Java ограничены «наименьшим общим знаменателем» возможностей ОС. Программа на C++ для Microsoft Windows имеет боже широкие возможности, так как может вызвать любую функцию Win32 в любой момент.
Java останется действующим языком программирования, но мы полагаем, что это просто еще один язык, а не тотальная революция. Если Вам нужен аплет для Интернета или действительно не зависящее от платформы приложение — используйте Java. Если же нужны эффективность и гибкость — ваш выбор C++,
Кому адресованаэта книга
Название продукта «Visual C++» вводит некоторых программистов в заблуждение. Они полагают, что купили полностью визуальную систему программирования, аналогичную Microsoft Visual Basic, и в течение нескольких первых дней иллюзия не исчезает. Однако скоро наступает понимание, что создание программы на C++ требует больших усилий. Мастеры Visual C++ экономят время и повышают производительность труда, но программисты должны понимать код, создаваемый тетерами и, в конечном счете, структуру библиотеки MFC и устройство ОС Windows,
Visu al C++ с ее мощными средствами разработки приложений предназначена для 1грофес- сиональных программистов, и эта книга — тоже для них. Мы считаем, что Вы достаточно хорошо знаете C++ и можете написать оператор ^ не обращаясь к справочнику. Мы также предполагаем, что у Вас есть некоторый опыт работы на C++ — по крайней мере, Вы прошли некоторый курс обучения или прочитали какую-нибудь книгу о нем, хотя может быть и не писали очень больших программ, Изучение C++ можно сравнить с изучением французского. Даже если Вы учили французский в школе, Вы не сможете свободно говорить на нем, пока не поедете во Францию и не пообщаетесь с носителями языка. Чтение этой книги похоже на такую поездку во Францию!
Однако мы не считаем, что Вы знакомы с программированием для Windows. Мы уверены, что опытные программисты на C++ могут изучать Windows посредством MFC. Знание C++ важнее знания API Win32. Тем не менее, мы полагаем, что читатель умеет работать с Windows и Windows-приложениями.
Что, если у Вас уже есть опыт работы с API Win32 или с библиотекой MFC? В этой книге найдется кое-что интересное и для Вас, Во-первых, Вы получите определенную помощь в переходе к Win32 программированию. Во-вторых, узнаете о новых возможностях, таких, как 1)АО (Data Access Objects), поддержка контейнера элементов управления ActiveX, а также новых элементах управления Windows 95. Если Вы еще не знакомы с моделью COM (Component Object Model), эта книга содержит некоторые важные теоретические сведения, с которых начнется ваше знакомство с элементами управления ActiveX. Вы также познакомитесь с библиотекой классов ATL, стандартными элементами управления Microsoft Internet Explorer 4-0 и программированием для баз данных с использованием OLE DB. Наконец. Вы познакомитесь с программированием на C++ для Интернета (включая новую технологию Dynamic HTML). Мыдаже включилиинформацию о том. какзаставитьваши программы на Visual C++ работать в ОС Windows СЕ.
Что не вошло в книгу
В одной книге невозможно рассмотреть все стороны программирования для Windows. Мы исключили темы, требующие специализированных аппаратных или программных средств, такие как MAPI, TAPI и работу с коммуникационными портами. Мы рассмотрим использование элементов управления ActiveX в приложениях, но оставим тему написания элементов управления ActiveX Адаму Деннигу (Adam Dennig) и его книге ActiveX Controls Inside Out (MicrosoftPress, 1997). Мы познакомим Вас с основами 32-разрядного управления памятью, теории DLL и приемами многопоточного программирования, но для серьезно изучения этих тем Вам потребуется книга Джеффри Рихтера (Jeffrey Richter) Advanced. Windows (Microsoft Press, 1995) (Д- Рихтер. Windows для профессионалов, M.: Русская Редакция, 1997). Другая полезная книга — MFC Internals Ц^кордка. Шеферда и Скотта Уингоу (Addison-Wesley, 1996).
Как пользоваться книгой
В начале работы с Visual C++ книгу можно использовать как учеб ник и читать ее последовательно. Затем книгу можно использовать в качестве справочника, отыскивая нужные темы по оглавлению или по предметному указателю. Из-за тесной взаимосвязи, существующей между большинством компонентов MFC, невозможно рассматривать каждый аспект в отдельной главе, поэтому издание, безусловно, не может рассматриваться как энциклопедия. Читая данную книгу, полезно иметь под рукой интерактивную справочную систему Visual C++ для просмотра информации о классах и функциях-членах.
Если Вы имеете опыт работы с версией Visual C++ для Wind 6, просмотрите первую часть, чтобы получить представление о новых возможностях Затем'пропустите первые три главы второй части, но прочитайте главы 6-12, где рассмотрены специфичные для Win32 вопросы.
Структура книги
Как видно из оглавления, книга состоит из шести частей и раздела приложений,
Часть!: Основные сведения о Windows, Visual C++ и каркасе приложения
В этой части мы пытались поддерживать баланс между теорией и практическими примерами. После беглого обзора Win32 и компонентов Visual C++ Вы кратко ознакомитесь с каркасом приложения MFC и архитектурой «документ-вид*. При помощи классов библиотеки МТС Вы создадите простую программу «Hello, world!*, насчитывающую всего 30 строк текста.
Часть 2: Класс «вид» библиотеки MFC
В документации по MFC все элементы каркаса приложения рассмотрены последовательно в предположении, что Вы знакомы с оригинальным API Windows. Вторая часть книги посвящена специально одному важному компоненту каркаса приложения — виду, или представлению (view), которое на самом деле является окном. Здесь Вы узнаете то, что уже и так известно опытному программисту для Windows, но в контексте C++ и классов библиотеки MFC С помощью инструментов Visual C++ мы смажем избавиться от большей части утомительного кодирования, с которым приходилось мириться программистам на «заре» Windows.
Во второй части затронуто много тем, включая графическое программирование с использованием растровых изображений, обмен данными в диалоговом окне, использование элементов управления ActiveX, 32-разрядное управление памятью и многопоточное программирование. Упражнения помогут Вам в написании достаточно сложных программ для Windows, но эти программы не будут использовать расширенные средства каркаса приложения.
Часть 3: Архитектура «документ-вид»
В этой части рассматривается истинное ядро программирования с использованием MFC - архитектура «документ-вид». Вы узнаете, что такое документ (нечто более абстрактное, чем документ в текстовом процессоре) и как подключить его к представлению, с которым Вы познакомились во второй части. Написав класс документа, Вы будете поражены, насколько библиотека MFC упрощает программирование файлового ввода/вывода и вывода на печать, Вы также познакомитесь с обработкой командных сообщений, панелями управления и состояния, многопанельными рамочными окнами и контекстно-зависимой подсказкой. В данной части также рассмотрен интерфейс MDI (Multiple Document Interface) — современный стандарт интерфейса Windows-приложений.
Здесь же обсуждается, как с помощью с помощью MFC написать динамически подключаемые библиотеки (Dynamic Link Libraries, DLL). Вы поймете различие между обычной DLL и DLL расширения. Если Вы уже использовали DLL в Win 16, то обратите внимание на некоторые изменения при переходе к Win32,
Часть 4: СОМ, Автоматизация и OLE
СОМ сама по себе заслуживает нескольких книг. В четвертой части Вы познакомитесь с основами теории СОМ с точки зрения MFC. Затем речь пойдет об Автоматизации, которая является связующим звеном между C++ и VBA (Visual Basic for Applications). Вы познакомитесь с унифицированным обменом данными и структурированным хранилищем, а также изучите основы составных документов и внедренных объектов..
Часть 5: Работа с базами данных
Часто Windows-программам требуется доступ к информации в больших базах данных (ЕД). Visual C++ поддерживает два стандарта работы с ЕД: ODBC (Open Database Connectivity) и DAO (Data Access Objects), каждому из которых посвящено по целой главе в пятой части. Вы познакомитесь с разносторонней поддержкой обоих стандартов в MFC и средствами мастеров, узнаете о сходстве и различиях между ODBC и DAO. Также будет рассмотрена новая технология доступа кданным OLE DB, которая поддерживается ATL-компонентами OLE DB-потребителя и поставщика.
Часть 6: Программирование для Интернета
Эта часть начинается с технического введения в Интернет, в котором рассматриваются протокол TCP/IP и интерфейсы программирования Windows Sockets и Winlnet. Вы узнаете, как писать на C++ серверные и клиентские программы для Интернета и интрасетей, 1SAPI DLL /для расширения Microsoft Internet Information Server, а также как программировать с использованием Dynamic HTML Кроме того, в этой части рассматривается Windows СЕ.
Приложения
Приложение А «Функции таблицы сообщений в MFC» содержит список макросов таблицы сообщений и прототипов соответствующих им функций-обработчиков. Обычно код таблицы автоматически генерируется ClassWizard, но иногда его приходится вводить вручную.
В приложении Б «Идентификация классов в период выполнения и динамическое создание объектов» приведено описание системы информации о классах периода выполнения и динамического создания объектов MFC. Эта система существует независимо от RTT1 (runtime type information) — средства, входящего в стандарт ANSI C++.
Win32 или Win16?
Windows 3.1 все еще встречается на некоторых старых компьютерах . Однако нет особого смысла тратить средства и время на создание новых программ для устаревших технологий Это, 5-е издание книги посвящено 32-разрядному программированию для MicrosoftWindows 95, MicrosoftWindows 98 и MicrosoftWindows NT с использованием API Win32. Если Вам действительно нужно писать 16-разрядные программы, отыщите второе издание книги.
Windows 98 или Windows NT 4.0?
Visual C++ версии 6.0 требует Windows 95, Windows 98 или Windows NT 4.0 или старше. Все эти ОС имеют одинаковый пользовательский интерфейс. Мы рекомендуем в качестве платформы разработчика использовать Windows NT, так как она более стабильна — зачастую можно месяцами не перезагружать компьютер. Если Вы используете только MFC, то ваши программы будут исполняться под Windows 95, Windows 98 и Windows NT, но программа может содержать вызовы Win32, использующие специфичные для Windows 98 или Windows NT возможности. •
Использование возможностей Windows: вставки «Длятех, кто программирует в Win32»
В этой книге нет возможности рассказать обо всех деталях, приемах и скрытых возможностях, которые можно найти в новых специализированных книгах по Win32. Большинство таких книг написано с точки зрения программиста на С: чтобы использовать их, Вы должны понимать пизкоуровые API Win32 и соотношение между ним и библиотекой MFC. Кроме того, потребуются знания о механизме обработки сообщений Windows и о роли классов окон i(Windows classes).
Вставки «Для тех, кто программирует в Win32», разбросанные по тексту книги, помогут Вам установить связь с низкоуровневыми программными интерфейсами Windows. Если низкоуровневое программирование Вам незнакомо, то пропустите эти вставки при первом прочтении книги, но прочтите их, если будет читать книгу во второй раз. Хотя Вам, возможно, и не придется писать низкоуровневую программу для Windows, использующую, например, функцию WinMain,понимать то, как ваша программа взаимодействует с ОС Windows, необходимо в любом случае.
Прилагаемый компакт-диск
На прилагаемом компакт-диске содержатся исходные тексты всех примеров программ. Имеются также и исполняемые файлы этих программ, так что Вам не надо обязательно компилировать заинтересовавшие Вас примеры. Для установки на компьютер файлов с компактдиска запустите программу Setup. Затем следуйте появляющимся на экране инструкциям.
ПРИМЕЧАНИЕ Программа Setup скопирует на ваш жесткий диск примерно 30 МБ файлов. При желании Вы можете вручную установить только файлы для отдельных проектов. Для этого просто скопируйте с компакт-диска нужные каталоги с подкаталогами в c:\vcpp32. Все проекты «самодостаточны», то есть ни одному из них не требуются файлы других проектов. (Если Вы копируете файлы с помощью Проводника Windows или Диспетчера файлов, то нужно снять с них атрибут «только для чтения».)
ПРИМЕЧАНИЕ Многие файлы на компакт-диске имеют длинные имена. Если Вы работаете в Windows 95 и привод компакт-дисков использует драйвер реального режима, то имена этих файлов будут отображаться в сокращенном виде, и Вы, возможно, не увидите некоторые файлы и каталоги. Тем не менее, программа Setup и в этом случае будет работать правильно, копируя файлы из специального каталога \SETUP на компакт-диске и переименовывая их с использованием надлежащих длинных имен. Затем
Вы сможете нормально просматривать данные файлы на своем жестком диске. Вы также можете просматривать файлы в каталоге \SETUP на компакт-диске с использованием псевдонимов в формате 8.3.
Для тех, кто программируетЪ;ШЙ2: Цп1<»^^У;'' 4^1^^ t I * .4.’ v < ,4 X? • -4
*; До недавнего йременц программы Для Windows иепользрвали^рлько'цабор ЫЙа^ФЙЙ^.
‘ ANSI, содержащий 256 однобайтовых символов.' Разработчигатт^трамм'для М$№ЙЬЙСГ^^ ; стран переходят к использований набора'символов IJrilCQde; К<^рЬгЙ содержит €5 ^Й^ £
: символов (щтйроюй». Каждый Пб 2 бйй^; Трётьим’вариантсм является Дб^^йржд^-и ■наборы символов ^боиЫётЬ^й characterse£, ®&^S^в которых имеются Как одно->"такй.\| двухбайтовые символы, но' область применения DBCS сужается1;' ' / ■ Г\ ’ , “ ; <; £«| t- -4? Н{Щлдй^ия,испол1Фую1Цие,и^^ Тк^^фЯййвйЮТф! как библиотекой MFG; ТЭД и «пбиблиотекой периода "йепоопгецйя. ;Bc^'<>9gps$i8J№^^Шпе^ЮШ^.
действия, описанные в интерактивной справочной системе» бес символьные перемен- > > •ные и строковые константы в щроф»та" сгаарт .биа$ол^^цжомйцддтйр /1
, будет генерировать вызовы версий функций:^.п§ 2, использующихэтот: набор .сй^ое Х^ ' ЯОВ. При этом необходимо, чтобы при объявлении указателей и массивов. символов’ис-; + ? ^пользовались некоторые макросы,например" ЖШ^■ ; чу v ; У ' v '^'j
': Однако Вы не смажете исполнять Црнлр^^^йя h$FC, иододьзующие ^old^^'^^iif ^ dons 95 и ^q^Iqws 98, так как Эти ОС внутренне его не Л|^^>фаваюТ-. Хотя в обеих с|
" ОС реализованы версии фунгщ^ Win32 д^;ш^аких,<$ИЭД$^й,:^^
: вращают ошибку. С другой сгорай! $Йп&тб!№иб1ж»1ьдуфЗУ1Йро4^$.
ив ней реализованы две вёрсии всех ф$$ф^£имеющЕИ символытые ^рамфтрбц$ При' V вызове версии длярднобайговых символов■ ^ndows NT дрттсадич^ски'.^шодтщ^тне^. $ , обхбдимыепреюбраэозап^» ймзишерёе.^?^.-1 ? :| г<^'i ; t'^£ ''л^,; $i^f>f|
- ^ Ни один из приодэд^ДО^П*^ кнйги не наст]юён на'использование^^
’' code, 1Всё они используют еднобайтовыё тйМ§ Такие как i^j^ Ьдндбайтовыё*строкр-/‘ ?
. вые константы и в вйх не <щ^^й<У,1Ю^£^Вфйй1рй«еры исполняются в^Ьу^^'f г • NTi то ОС буцет'выполнять йеобхсДимые преобразования; ’ес^ш же они и@1од™®этмж^в,..|'; ’ Endows 95 или Endows ^8, то там интерфейсы ^p^rpaitoiH^OB^dBi *шстоЦ
. ■ ' ■ ЕстьодЕта. область, тде Вам првдется иметь дело q Uiiicpdey^y STO СОЙ. Ike функций ^ I . ■ СОМ$ не связанные с MFC фа ^йвЮ1гё|ф^Й1ф’^^^йй Й^^Г<ю'ед^^
I . вольными параметрами трёбубот йшфрк^б ^ ^di^^Wltl® йапиодниидар?»^/’ ' : ^граммы, не испотазующей 1^Й^^^ Вы вййалдйге ^ep^X^O^W^PTNWc ®ЙЙЙН^^ класса MFC С^йчи^и различныхмакросов S№ds j 5 - ' '4'* PV |>7 ^^ * У1 ? р z I г ^^t^W
■' ЧЁбйй Вы хбтите пйсаль:тМа£^1фЁожясЬни^::^^посвящ^ф^ OiBdbae главу '^1
• .из книги PHXTepaAtivanced 1й&<МбЖ Вам также следует изучить материал по" tJjiiiedde в« ^ интерактивной справочной системе Visual Сн-У ? - - .У ” ^?« ';7 -. ";_ ‘;у ‘ ■ У -- л- :
Об обычной программе на С, использующей API Windows, все скажут ее исходные тексты. При использовании каркаса приложения библиотеки MFC не все так просто. Значительная члеть кода C++ генерируется АрpWizard, а ресурсы порождаются соответствующимиредак- торами ресурсов. Примеры в начальных главах книги содержат пошаговые инструкции по использованию указанных инструментов для генерациииредактирования исходных файлов. Для создания нескольких первых примеров Вам будетрекомендовано использовать эти инструкции — вводить код вручную практически не придется. Для примеров из глав середины книги используйте тексты с компакт-диска, но просмотрите последовательность шагов создания программы, чтобы понять роль редакторов ресурсов и мастеров. В последних главах исходныетексты примеров приведены не полностью. При изучении этих примеров Вам потребуется обратиться к компакт-диску.
Технические замечания и примеры программ
В этой книге есть ссылки на технические замечания и примеры программ из MSDN Library для Visual C++ 6.0. Технические замечания, идентифицируемые по номерам, доступны через вкладку Contents под заголовком:
MSDN Library Visual Studio 6.0
Visual C++ Documentation Reference
Microsoft Foundation Class Library and Templates
Microsoft Foundation Class Library•
MFC Technical Notes
На компакт-диске MSDN содержится не сколько примеров программ MFC, названия которых также встречаются в этой книге. Эти примеры программ расположены под заголовком:
MSDN Library Visual Studio 6.0
Visual C++Documentation•■
Samples
MFC Samples
Поддержка1
При создании книги был приложен максимум усилий для обеспечения отсутствия ошибок в ней и на прилагаемом компакт-диске. Исправления к данной книге Microsoft Press предоставляет по адресу: http://mspress-microsoft.com/mspress/5upport/
Если у Вас есть комментарии, вопросы или идеи по поводу данной книги или прилагаемого к ней компакт-диска, то их можно отправить в Microsoft Press по обычной или электронной почте:
Micros oft Press
Attn: Programming Microsoft Visual C++ Editor
One Microsoft Way
Redmond. WA 98052-6399
MSPINPUT@MICROSOFT.COM
Пожалуйста, учтите, что поддержка программных продуктов через указанные адреса не осуществляется. Информацию, связанную с поддержкой Microsoft Visual C++, можно получить по телефону технической поддержки (425) 635-7007 в рабочие дни с 6 утра до 6 вечера по времени Тихоокеанского побережья США. Информация о Visual C++ предоставляется Microsoft также по адресу http-.//www.microsoft.com/visualc/. а о MSDN по адресу httpzZ/www.micro- soft.com/MSDN/.
ЧАСТЬ I
Основные сведения о Windows, Vusual C++ и каркасе приложения
ГЛАВА 1
MicrosoftWindows и Visual C++
Уже более чем достаточно написано о широком распространении MicrosoftWindows и преимуществах графического интерфейса пользователя (Graphical User Interface, GUI). В этой главе мы обсудим общую модель программирования в Windows (и в Win32, в частности) и увидим, как взаимодействуюткомпоненты Visual C++, помогающие программисту создавать, приложения. Попутно Вы узнаете кое-что новое и о самой Windows.
Модель программирования в Windows
Независимо от того, какими инструментами Вы пользуетесь, программирование в Windows отличается от старомодного стиля, ориентированного на обработку последовательностей команд или запросов. В первую очередь давайте разберемся в основах самой Windows. За точку отсчета возьмем хорошо известную модель программирования в MS-DOS. Даже если Вы сейчас и не программируете под MS-DOS, эта модель Вам, скорее всего, знакома.
Обработка сообщений
Когда Вы пишете в MS-DOS-программу на С, то от Вас требуется безусловное наличие в программе функции main. Операционная система вызывает main при запуске программы, и с этого момента Вы, по сути, можете делать все, что угодно. Если программе необходимо узнать, какие клавиши нажаты на клавиатуре, или как-то иначе воспользоваться сервисом операционной системы, она вызывает соответствующие функции, например getcbur, или обращается к более специализированной библиотеке функций, оперирующих с символами.
В Windows же операционная система при запуске программы вызывает се функцию Win Main. В любом приложении обязательно должна присутствовать эта функция, па которую возлагается ряд специфических задач. Важнейшая из них — создание основного окна программы, с которым должен быть связан код, способный обрабатывать сообщения, передана- емые этому окну операционной системой. Существенное различие между MS-DOS- и Windows-программой состоит в способе получения введенных пользователем данных: первая вызывает операционную систему; вторая использует поступающие от нее сообщения.
ПРИМЕЧАНИЕ Многие среды программирования для Windows, включая Microsoft Visual C++ 6.0 с библиотекой классов Microsoft Foundation Class (MFC) Library 6.0, упрощают программирование, скрывая функцию WinMain и структурируя процесс обработки сообщений. При использовании библиотеки MFC Вам не понадобится писать функцию WinMain, но очень важно понимать, как взаимодействуют операционная система и ваша программа. - ' '
■ Большинство сообщений в Windows строго определено и относится ко всем программам. Например, сообщение WM_CREATE передается при создании окна, WM LBUTTQNDOWN --
при нажатии левой кнопки мыши, \VMCHAR — при вводе символа, a WM_CLOSB — при закрытии окна пользователем. У всех сообщений имеется два 32-разрядных параметра, передающих такую информацию, как координаты курсора, код нажатой клавиши и т. д. Сообщения группы WM_COMMAND отправляются соответствующему окну в ответ на выбор пользователем команд в меню, щелчки кнопок диалоговых окон и т. п. Параметры командных сообщений зависят от структуры меню конкретного окна. Кроме Предопределенных сообщений, программист может задавать свои, так называемые «пользовательские сообщения», которые можно послать любому окну. Они делают C++ слегка похожим на Smalltalk.
Не ломайте пока голову над тем, как программный код обрабатывает сообщения. За это отвечает каркас приложения (application framework). Однако будьте готовы к тому, что обработка Windows-сообщений накладывает на структуру программ весьма жесткие ограничения. Не пытайтесь выстраивать программы для Windows по аналогии с MS-DOS. Изучите примеры, приведенные в книге, и приготовьтесь начать все сначала.
Интерфейс графического устройства
Многие MS-DOS-программы записывали данные прямо в видеопамять и порт принтера. Недостаток этого метода состоит в том, что разработчику приходилось создавать отдельные драйверы для каждой из множества моделей видеоплат и принтеров. В Windows включен особый «слой абстракции» — интерфейс графического устройства (Graphics Device Interface. GDI). Драйверы для видеоплат и принтеров предоставляет сама Windows, благодаря чему ва - шей программе не надо знать, видеоплата и принтер какого типа подключены к системе. Вместо того, чтобы обращаться к оборудованию, программа вызывает GDI-функции, ссылающиеся на структуру данных — контекст, устройства (device context). Windows сопоставляет структуру контекста устройства с физическим устройством и выдает соответствующие команды ввода/вывода. GDI обеспечивает почти такую же скорость работы, как и прямой доступ к видеопамяти, и позволяет нескольким Windows-программам одновременно работать с дисплеем.
Программирование, основанное на ресурсах
Программируяв MS-DOS, Вы определяли данные при помощи инициализирующих констант либо путем считывания из отдельных файлов. При работе же в Windows Вы храните данные в файле ресурсов, которые представляются в нескольких стандартных форматах. Компоновщик (linker) при генерации исполняемой программы комбинирует двоичные файлы ресурсов с кодом, полученным на выходе компилятора C++. Файлы ресурсов могут включать растровые изображения, или битовые карты (bitmaps), значки (icons), определенияменю (menu definitions), описания структуры диалоговых окон и строки. Они могут содержать даже описания особых форматов ресурсов, определенных программистом.
Программу Вы набираете и модифицируете при помощи текстового редактора, но ресурсы обычно редактируют инструментальными средствами, обеспечивающими режим WYSIWYG (What You See Is What You Get — «что видишь, то и получаешь»). Так, для разметки струк- турыдиалогового окна нужно выбрать из набора значков, называемого палитрой элементов управления (control palette), необходимые элементы (кнопки, списки и пр.) и с помощью МЫШИ надлежащим образом разместить их и подогнать по размеру. Графический редактор Visual C++ позволяет эффективно редактировать все стандартные форматы ресурсов.
Управление памятью
С появлением каждой повой версии Windows управление памятью становится проще и проще. Если Вы уже наслушались леденящих душу историй об описателях заблокированной памяти (handles), трудностях трансформации 16-разрядных вызовов в 32-разрядные (и наоборот) и других ужасах, не паникуйте — все это в прошлом. Сегод ня Вы просто выделяете
память, а об остальном заботится Windows. О современных способах управления памятью в Win32 (в частности, о виртуальной памяти и проецируемых в память файлах) мы поговорим в главе 10.
Динамически подключаемые библиотеки
В среда MS-DOS все объектные модули программы связываются статически на стадии компоновки. Windows разрешает динамическое связывание, т. е. загрузку и подключение к программе в период выполнения специальных библиотек. Одной и той же динамически подключаемой библиотекой (Dynamic-Link Library, DLL) могут пользоваться сразу несколько программ, что экономит память и дисковое пространство. Кроме того, динамическое подключение позволяет увеличить модульность программ, поскольку DLL-модули компилируются и тестируются раздельно. *
Изначально DLL-библиотеки разрабатывались в расчете на язык С, а C++ привнес в этот процесс кое-какие сложности. Впрочем, разработчики MFC сумели скомпоновать все классы каркаса приложений в несколько законченных DLL Это означает, что упомянутые классы можно статически или динамически подключать к вашему приложению. Кроме того, Вы можете создать собственные DLL-модули расширения (extension DLLs), построенные на основе DLL-модулей библиотеки MFC. Обо всем этом пойдет речь в главе 22,
Интерфейс прикладных программ Win32
На заре Windows программисты писали приложения на С в расчете на интерфейс прикладных программ Winl6. Сегодня, чтобы написать 32-разрядную программу, придется — прямо или косвенно — пользоваться новым Win32 API. У большинства Winl6-ф),нкцийecть Win32- эквиваленты, но многие их параметры изменились: так, часто вместо 16-разрядных параметров применяются 32-разрядные. Однако в Win32 появилось и много новых функций, в частности, для управления дисковым вводом/выводом (раньше для этого использовали вызовы .MS-DOS) Программисты, использовавшие MFC в 16-разрядныхверсияхУ15па1С++, оказались в значительной степени изолированы от различий в API, так как они разрабатывали приложения в соответствии со стандартом MFC, поддерживающим KaKWinl6, таки Win32.
Компоненты Visual C++
Microsoft Visual C++ объединяет в себе две законченные системы разработки Windows-приложений. Можно создавать Windows-программына языке С, используятолько Win32 API. Программирование в Win32 на С описано в книге Ч. Петцольда Programming Windows (Microsoft Press, 1996). Существенно облегчают работу по низкоуровневому Win32-nporpaMMHpoBaHHio многие инструментальные средства Visual C++, включая редакторы ресурсов.
Visual C++ включаетв себя библиотеку шаблонов ActiveX (ActiveXTemplate Library, ATL), с помощью которой можно разрабатывать элементы управления ActiveX для Интернета. Программирование в ATL не сводится к программированию ни в Win32, ни в MFC, а представляет собой настолько сложную вещь, что заслуживает отдельной книги.
Однако эта книга не о Win32 или ATL (хотя главы 29 и 30 представляют собой введение в ATL), а о программировании на C++ с использованием каркаса приложений — библиотеки MFC, составляющей часть Visual C++. Вы будете применять классы C++, документированные в MicrosoftFoundation Class Library Reference, а также инструменты Visual C++, специфичные для библиотеки MFC, например, AppWizardH ClassWizard.
ПРИМЕЧАНИЕ Работа с MFC-библиотекой не лишает программиста возможности применять функции Win32. На самом деле, в программах на основе этой библиотеки почти всегда приходится напрямую вызывать те или иные Win32^yHKqnH.
Прежде чем Вы возьметесь за свое первое приложение, краткий обзор компонентов Visual 'C++ помажет понять, с чем Вам придется иметь дело. На рисунке 1-1 проиллюстрирован процесс создания программ в Visual C++. .
Рис. 1-1. Создание программы в Visual C++Рис. 1-1. Создание программы в Visual C++
Рис. 1-1. Создание программы в Visual C++
Рис. 1-1. Создание программы в Visual C++
Microsoft Visual C++ 6.0 и процесс построения программ ; . -
Visual Studio 6.0 — это комплект (suite) средств разработки, включающий Visual C++ 6.0. Интегрированную среду разработки (Integrated Development Environment, IDE) Visual C++ используют и другие средства разработки, например, Microsoft J++. Эта среда начала свой долгий путь с Visual Workbench («рабочее место для визуального программирования»), основанного на QuickC for Windows. Теперь в ней появились так называемые стыкуемые окна (docking windows), конфигурируемыепанели инструментов (configurable toolbars) и настраиваемый редактор (customizable editor), способный исполнять макросы. Встроенная справочная система, интегрированная с программой просмотра библиотеки Microsoft Developer Network, MSDN, теперь работает по принципу программы просмотра Web. На рисунке 1-2 продемонстрирован Visual C++ 6.0 в действии.
Если Вы работали с прежними версиями Visual C++ или интегрированными средами разработки других поставщиков, то понимаете, как функционирует Visual C++ 6.0. Но если интегрированные среды Вам в новинку, то сначала надо разобраться в том, что такое проект (project). Проект — это набор взаимосвязанных исходных файлов, компиляция и компоновка которых позволяет создать исполняемую Windows-программу или DLL Исходные файлы проекта обычно хранятся в отдельном подкаталоге. Кроме того, зачастую проект зависит от многих файлов, расположенных вне подкаталога проекта, таких как включаемые (include) и библиотечные файлы.
Окно проектовИсходный текст
Окно вывод а сообщений компилятора и компоновщика
Рио. 1-2. Экран Visual C++ 6.0
Опытные программисты знакомы также со сборочными файлами проекта, или таке-фай- лами (make files). Сборочный файл проекта хранит параметры компилятора и компоновщика, а также отражает все взаимосвязи между исходными файлами. (Файлу с исходным кодом необходимы те или иные включаемые файлы, исполняемому файлу — определенные объектные модули, библиотеки и т. д.). Программауправления сборкой проекта (make program) считывает make-файл, а затем вызывает компилятор, ассемблер, компоновщик и компилятор ресурсов, чтобы получить на выходе требуемый файл — исполняемый или библиотечный. Эта программа, руководствуясь указанной ей информацией, заставляет, например, компилятор генерировать OBJ-файл из определенного СРР-файла.
В проекте Visual C++ 6.0 make-файл (с расширением МАК) отсутствует, хотя можно потребовать, чтобы система его создала. Его функции выполняет текстовый файл проекта (project file) с расширением DSP. А специальный текстовый файлрабочего пространства (workspace file) с расширением DSW содержит список всех проектов данной группы. Рабочее пространство может содержать несколько проектов (но во всех примерах этой книги оно состоит из одного проекта). Чтобы начать работать с существующим проектом, достаточно открыть в Visual C++ соответствующий DSW-файл, после чего можно редактировать и собирать проект.
Visual C++ также создает промежуточные файлы нескольких типов.
Расширение файла
Описание
APS
BSC CLW
DEP
DSP
DSW
Поддержка обзора ресурсов (ResourceView)
Информация средства просмотра
Поддержка ClassWizard
Файл зависимостей
Файл проекта*
Файл рабочего пространства*
* Не удаляйте эти файлы и не открывайте их в текстовом редакторе.
Расширение файла
Описание
МАК
Внешний сборочный файл
NCB
Поддержка обзора классов (ClassView)
ОРТ
Содержит конфигурацию рабочего пространства
PLG
Файл журнала сборки
Редакторы ресурсов и просмотр ресурсов проекта •
Выбрав вкладку ResourceView (обзор ресурсов) в окне проектов Visual C++, можно приступать к редактированию ресурсов данного проекта. В основном окне располагается редактор ресурсов (resource editor), предназначенный для конкретного типа ресурсов. Это может быть и WYSIWYG-редактор меню, и мощный графический редактор диалоговых окон, и набор дополнительных инструментов для операций со значками, растровыми изображениями и строками. Редактор диалоговых окон, кроме стандартных элементов управления Windows и новых элементов управления, появившихся в Windows 95 (и значительно усовершенствованных в Visual C++ 6lO) позволяет вставлять и элементы управления ActiveX. В главе 3 приведены иллюстрации с примерами окон ResourceView и одного из редакторов ресурсов — редактора диалоговых окон.
В каждом проекте обычно есть один RC-файл, в котором описываются ресурсы меню, диалоговых окон, строк и акселераторов. Этот файл также обычно содержит операторы ♦include, «собирающие» ресурсы из других подкаталогов. В эти ресурсы включаются как специфичные для конкретного проекта, например, BMP-файлы или файлы значков (ICO), так и общие для всех программ на Visual C++, например, строки сообщений об ошибках. Модифицировать RC- файлы без помощи графического редактора не рекомендуется. Графический редактор способен обрабатывать EXE- и DLL-файлы, благодарячему Вы, например, сможете «заимствовать» растровые изображения и значки у чужих Windows-программ.
Компилятор C/C++
Компилятор Visual C++ способен обрабатывать исходный код и на С, и на C++. Он определяет язык по расширению имени файла с исходным кодом. Расширение С указывает на код на С, а СРР или СХХ — на C++. Компилятор удовлетворяет стандартам ANSI, включая самые последние рекомендации рабочей группы по библиотекам C++, и обладает рядом расширений, введенных Microsoft. Шаблоны, обработка исключений и идентификация типов во время выполнения (runtime type identification, RTTI) — все это полностью поддерживается в Visual C++ версии 6.0. Введена также (хотя и не интегрирована в библиотеку MFC) стандартная библиотека шаблонов (Standard Template Library, STL).
Редактор исходного текста
Visual C++ 6.0 содержит мощный редактор исходного текста, поддерживающий множество возможностей — выделение цветам синтаксических конструкций, автоотступы, раскладки клавиатурных команд, свойственные различным популярным редакторам (например, VI и EMACS), а также высококачественную печать В Visual C++ 6.0 появилась впечатляющая новая функция AutoComplete, Если Вы работали с продуктами из Microsoft Office или с Microsoft Visual Basic, то наверняка знаете, что это такое. Когда Вы набираете несколько начальных символов оператора программы, редактор предлагает выбрать «все слово» из списка возможных вариантов. Это очень полезно, если Вы работаете с объектами C++ и забыли точное имя члена класса, неважно, функции или переменной: все они в списке перед Вами. Благодаря AutoComplete не нужно забивать память форматами тысяч функций Win32 API или постоянно обращаться к интерактивной справочной системе.
Компилятор ресурсов
Компилятор ресурсов в Visual C++ считывает ASCII- файл описания ресурсов (RC), созданный в графическом редакторе, и создает для компоновщика двоичный RES-файл.1■
Компоновщик
Компоновщик считывает OBJ- и RES-файл ы, сгенерированные компиляторе м C/C++ и компилятором ресурсов, и обращается к LIB-файлам за MFC-кодом, кодом библиотек периода выполнения и Windows. После этого он формирует ЕХЕ-файл проекта, Возможность компоновки с приращением (incremental link) существенно ускоряет процесс, когда в исходные файлы вносятся лишь незначительные изменения. Заголовочные файлы MFC содержат операторы *£гв£го^спе1щальные директивы компилятора), указывающие требуемые библиотечные файлы, так что Вы не должны явным образам сообщать компоновщику, к каким библиотекам обращаться.
Панель инструментов отладчикаПанель инструментов отладчикаСтек вызововСтек вызововПеременные из текущего и пред шествующего операторовПеременные из текущего и пред шествующего операторовдля просмотрадля просмотраРис. 1-3. Окно отладчика Visual C++Рис. 1-3. Окно отладчика Visual C++
Панель инструментов отладчика
Панель инструментов отладчика
Стек вызовов
Стек вызовов
Переменные из текущего и пред шествующего операторов
Переменные из текущего и пред шествующего операторов
для просмотра
для просмотра
Рис. 1-3. Окно отладчика Visual C++
Рис. 1-3. Окно отладчика Visual C++
Отладчик
Если вяттти программы начинают работать сразу, отладчик Вам не нужен. Ну а нам, простым смертным, приходится им от случая к случаю пользоваться. Отладчик в Visual C++ постоянно совершенствуется, но все еще не умеет сам исправлять ошибки. При работе с точками прерывания (breakpoints) ан тесно взаимодействует с Visual C++ и обеспечивает сохранение их на диске. Соответствующие кнопки на панели инструментов позволяют расставлять и убирать точки прерывания и управлять пошаговым исполнением программ. На рисунхе 1-3 продемонстрирован отладчик Visual C++ в д ействии. Обратите внимание: в окнах Variables и Watch можно «развернуть* указатели объектов, что приведет к отображению всех перемен- ных-члениь up. -t *п'^могокяасса (derived class) и базовых классов. Если же поместить курсор мыши на скалярную переменную, отладчик покажет ее значение в маленьком окошке. При
отладке программу надо сначала собрать с такими параметрами компилятора и компоновщика, которые приводят к генерации специальной отладочной информации.
AppWizard
Арр Wizard — генератор кода, создающий рабочую заготовку Windows-приложения с теми компонентами, именами классов и исходными файлами, которые Вы задали в его диалоговых окнах. Прорабатывая примеры, приведенные в этой книге, Вы будете интенсивно пользоваться мастером AppWizard. Только не путайте его со старыми генераторами кода, формировавшими весь код приложения. AppWizard, как художник-минималист, создает минимум; главное же сосредоточено внутри базовых классов каркаса приложений. Назначение AppWizard — помочь Вам побыстрее приступить к работе над новым приложением.
'Опытные программисты теперь могут сами создавать нужные им мастеры Арр Wizards — корпорация Microsoft раскрыла секреты своей основанной на макросах системы генерации проектов. И если ваша команда занята разработкой множества проектов, скажем, в области телекоммуникаций, Вы вполне мажете построить свой мастер для автоматизации работ.
ClassWizard
Class Wizard — программа, реализованная как DLL и доступная через меню View среды разработки. Она избавляет нас от нудной работы, связанной с кодированием классов Visual C++. Вам нужен новый класс, новая виртуальная функция или новый обработчик сообщений? ClassWizard создаст прототипы, тела функций и, при необходимости, — код, связывающий Windows-сообщение с конкретной функцией. Этот мастер способен обновлять код класса, который Вы пишете, и тем самым избавляет Вас от многих проблем, свойственных обычным генераторам кода. На рисунке 1-2 Вы могли заметить, что некоторые функции ClassWizard доступны через инструментальную панель WizardBar в верхней части окна исходного кода.
Средство просмотра исходного кода
Если Вы пишете программу «с нуля», то, по-ввдимому, хорошо представляете себе ее структуру: все файлы с исходным кодом, классы и функции-члены. Но чтобы разобраться в чужой программе, Вам наверняка потребуется помощь. В Visual C++ предусмотрено средство просмотра исходного кода (Source Browser), или, для краткости, просто средство просмотра. Оно позволяет анализировать (и редактировать) программу через призму конкретного класса или функции, а не файла или файлов. В чем-то оно напоминает инструментальные средства-«ин- спекторы», доступные вместе с другими объектно-ориентированными библиотеками, например, Smalltalk. Средство просмотра работает в следующих режимах.
Definitions and References (определения и ссылки). Выбрав любую функцию, переменную, тип, макрос или класс, Вы увидите, тде они определены и где именно используются.
CallGraph/Caller Graph (схема вызываемых или вызывающих функций). Выбрав какую-то функцию, Вы увидите графическое представление функций, вызываемых ею или вызывающих ее.
Derived Class Graph/Base Class Graph (схема производных или базовых классов). Графическая схема иерархии классов. Выбрав какой-либо класс, Вы увидите его производные или базовые классы. При помощи мыши можно управлять степенью детализации («развертки») иерархии.
I* File Outline (конспектфайла.). Для выбранного файла классы, члены классов и функции появляются вместе с указанием на все места программы, тде они определены И используются.'
Типичный пример окна средства просмотра приведен в главе 3-
ПРИМЕЧАНИЕ Если Вы перегруппируете строки в любом из файлов с исходным кодом, Visual C++ обновит используемую средством просмотра базу данных в момент повторной сборки проекта. На это, естественно, уйдет дополнительное время.
Кроме упомянутого выше средства, в Visual C++ предусмотрен новый режим просмотра — ClassView, независимый от базы данных, используемой при просмотре. В этом режиме показывается дерево всех классов в проекте вместе с функциями-членами и переменными-членами. Щелкнув какой-нибудь компонент, Вы туг же увидите соответствующий исходный код. Однако режим ClassView — в отличие от средства просмотра — не позволяет отображать информацию о иерархии.
Интерактивная справочная система
В Visual C++ справочная система основывается на HTML и выделена в отдельное приложение, названное MSDN library Viewer. Каждая тема помещена в отдельном HTML-документе, а документы скомпонованы в проиндексированные файлы. MSDN Library Viewer использует кед из Microsoft Internet Explorer 4.0 и поэтому работает как хорошо известные программы просмотра Web (Web-browsers). Справочная система обеспечивает доступ к справочным файлам, находящимся как на компакт-диске Visual C++ (выбор по умолчанию), так и на жестком диске, а также к HTML-файлам в Интернете. Получить справку можно четырьмя способами.
By book (по книге). Выбор комавды Contents из меню Help среды разработки приводит к переключению MSDN Library Viewer в режим просмотра содержания. Здесь документация по Visual Studio, Visual C++, Win32 ЗВКпредставлена иерархически по книгам и главам.
By topic (по теме). Выбор комавды Search из меню Help среды разработки приводит к запуску MSDN Library Viewer. Там можно выбрать вкладку Index, задать ключевое слово и просмотреть относящиеся к нему темы и статьи.
By word (по слову). Выбор комавды Search из меню Help среды разработки приводит к запуску MSDN Library Viewer с активизированной вкладкой Search. Там можно набрать комбинацию слов и просмотреть статьи, содержащие заданные слова.
Fl help (контекстная справка). Лучший друг программиста, Поставьте курсор на имя функции, макроса или класса, нажмите клавишу F1, и справочная система примется за работу. Если имя встречается в нескольких местах (например, в справочных файлах МЕС и Win32), на экране появится список, из которого Вы сможете выбрать нужный тематический раздел.■
При этом, каким бы способом справка ни была получена, любой ее фрагмент всегда можно скопировать в буфер обмена и перенести, например, в программу.
Диагностические утилиты
В Visual C++ включен ряд полезных диагностических инструментальных средств. SPY++ показывает дерево процессов, потоков и окон, существующих в системе. Он позволяет также просматривать сообщения и исследовать окна выполняемых приложений. Утилита PV1EW (PVIEW95 для Windows 95) предназначена для закрытия сбойных процессов, не видимых в списке задач Windows 95. (В Windows NT альтернативой PVIEWможет служить Windows NT Task Manager, чтобы запустить его, нужно щелкнуть правой кнопкой мыши панель задач.) В Visual C++ имеется и исчерпывающий набор ActiveX-утилит, программа для тестирования элементов управления ActiveX (в Visual C++ 6.0 включен и ее полный исходный текст), средства построения справочных файлов (help workshop) с компилятором, программа-библиотекарь, средства просмотра и редактирования двоичных файлов, средство построения профиля исходного кода (source code profiler) и другие утилиты.
Контроль исходного кода
Microsoft недавно приобрела права на известный продукт SourceSafe, предназначенный для контроля за исходным кодом. Он включен в корпоративные редакции Visual C++ и Visual Studio, а в Visual C++ полностью интегрирован, что позволяет координировать крупномасштабные программные проекты. Главная копия исходного кода проекта хранится централизованно на одном из серверов сети, а программисты получают возможность «списывать» отдельные модули для об, юв,тения. Выбранные программистом модули обычно хранятся на его локальных жестких дисках. Когда программист вернет модифицированные им модули в об- ще хранилище, остальные участники рабочей группы могут синхронизировать содержимое жестких дисков своих компьютеров с главной копией1.
Gallery
Gallery (или, официально, Components and Controls Gallery — галерея компонентов и элементов управления) позволяет использовать одни и те же программные компоненты сразу в нескольких проектах. Gallery оперирует с тремя типами модулей
Элементы управления ActiveX. При вставке элемента управления ActiveX (OCX, ранее элемента управления OLE) в реестре Windows создается новая запись. Все зарегистрированные OCX появляются в окне Gallery, благодаря чему их можно выбирать в любом проекте.
|* Модули исходного кода на C++. Создав новый класс, можно добавить его коде Component Gallery, после чего его можно копировать в другие проекты. Впрочем, в Gallery можно добавлять не только код, но и ресурсы.
Компоненты Visual C++. Gallery может содержать инструменты для добавления в проект каких-либо функций. Такого рода инструмент мог бы автоматизировать внесение в существующий проект новых классов, функций, переменных-членов и ресурсов. Некоторые такие компоненты (например, обработка времени прост оя, поддержка палитры, экран заставки) поставляются непосредственно Microsoft в составе Visual C++, другие — сторонними фирмами.
ПОДСКАЗКА Если Вы реимли воспользоваться одним из стандартных компонентов из комплекта Visual C++, внесите его сначала в пробный проект и убедитесь, что это то, что Вам нужно. Иначе удалить сгенерированный текст из рабочего проекта может оказаться очень непросто..
Все элементы Component Gallery можно импортировать из OGX-файлов и экспортировать в них. Эти файлы предназначены для хранения и распространения компонентов Visual C++.
М ic rosoft Fou ndation Class Library6.0
Microsoft Foundation Class Library версии 6.0 (сокращенно библиотека MFC) как раз и представ- ляет собой предмет этой книги. Она определяет каркас приложений, с которым Вы еще познакомитесь поближе. В главе 2 мы увидим настоящий код и рассмотрим рад важных концепций.
Microsoft Active Template Library1
ATL представляет собой средство построения элементов управления ActiveX, отдельное от MFC. Вообще говоря, писать элементы управления ActiveX можно как на MFC, так и на ATL, но А'П.-элсмепты намного меньше по объему кода и быстрее загружаются по Интернету. В главах 29 и ЗОдается краткий обзор ATL и методики написания элементов управленияАсЦуеХ при помощи ATL>
। Следует добавить, что система обеспечения групповой работы следит, чтобы выбранный программистом модуль не мог модифицировать никто другой, а также позволяет отслеживать и хранить версии файлов и выполняет множество других очень полезных функций. — Прим, отерев.
ГЛАВА 2
Каркас приложений Microsoft Foundation Class Library
V»-
О этой главе мы познакомимся с каркасом приложений Microsoft Foundation Class Library версии 6.0 (библиотеки MFC) и увидим, в чем состоят его преимущества. Через несколько страниц Вам встретится хотя и урезанная, но вполне работоспособная Windows-программа на базе MFC, которая поможет разобраться в сути программирования на основе каркасапри- ложения. Теория здесь сведена к минимуму, но в последних двух разделах содержится очень важная информация, необходимая для осмысления примеров из следующих глав.
Назначение каркаса приложений
Собираясь разрабатывать приложения для Windows, надо сначала выбрать среду программирования. И если Вы уже отвергли варианты, не связанные с языком С (скажем, Microsoft Visual Basic или Borland Delphi), у Вас все же остается как несколько вариантов:
программировать на С, применяя Win32 API;
написать на C++ свою библиотеку классов, использующую Win32;
задействовать каркас приложений на базе MFC;
воспользоваться другим каркасом приложений, например,Object Windows library (OWL) фирмы Borland (Inprise).
Если Вы начинаете с нуля, любой из этих вариантов потребует учиться очень и очень многому. Если Вы уже умеете программировать в Win 16 или Win32, все равно придется осваивать библиотеку MFC. Почему? Потому что почти сразу после появления MFC она стала доминирующей библиотекой классов для Windows. Даже если Вы знакомы с MFC, не помешает еще раз вспомнить основные особенности этого, программного продукта.
Библиотека MFC — API-интерфейс Windows на C++. Если Вы согласны с тем, что язык C++ сегодня является стандартом для разработки серьезных приложений, то должны признать, что для Windows естественен программный интерфейс на C++. Кто напишет интерфейс лучше, чем сама корпорация Microsoft, создавшая Windows? И этот интерфейс — библиотека MFC. «Каркасные» приложения имеют стандартную структуру. Любой программист, приступающий к крупному проекту, старается выработать для себя какую-то структуру кода. Но проблема заключается в том, что эта структура у каждого программиста своя, и новому участнику команды трудно приспособиться к ней. Каркас приложений на базе MFC предлагает свою структуру, отработанную на многих проектах и опробованную в разных программных средах, Создав Windows-программу на базе библиотеки MFC, можно без всяких опасений скрыть-
ей от всех тде-нибудь на Карибских островах — тем, кто остался дома, будет легко поддерживать и совершенствовать вашу программу.
Не подумайте только, что структура библиотеки MFC «сковывает» программы. При работе с MFC можно в любой момент вызвать любую ^п32-функцию, т. е. в полной мере использовать возможности и преимущества Windows.
Достоинства «каркасных» приложений — компактность и скорость работы. Когда-то, во времена 16-разрядного программирования, можно было создать исполняемый файл Windows- программы размерам меньше 20 Кбайт. Сегодня Windows-программы гораздо больше, и одна из причин этого в том, что 32-разрядный код объемнее. Даже при использовании большой модели памяти Win 16-программы оперировали с 16-разрядными адресами стековых и многих глобальных переменных. A Win32-nporpaMMbi всегда используют 32-разрядные адреса и часто работают с 32-разрядными целыми значениями — они эффективнее 16-разрядных. Много памяти расходует и новый C++-код обработки исключений.
Вспомните: разве в тех 20-килобай1'овыхпрограммах были стыкуемые панели инструментов (docking toolbars), разделяемые окна, режим предварительного просмотра перед печатью, поддержка контейнеров с элементами управления — словом, все те возможности, которых пользователи ждут от современных программ? MFC-программы потому и объемнее, что делают больше и выглядят лучше. К счастью, теперь можно создавать программы, динамически связываемые с MFC-кодом (и С-кодом периода выполнения), так что их размер опять уменьшается — со 192 Кбайт до примерно тех же 20 Кбайт. Разумеется, такой программе понадобится множество DLL-модулей, но в наши дни от этого никуда не денешься.
Что касается скорости работы программ, то Вы имеете дело с машинным кодом, который сгенерирован оптимизирующим компилятором. Программы исполняются быстро, но при их запуске можно заметитьзадержку—вэтотмоменгзагружаются вспомогательные DLL-модули. Средства Visual C++ уменьшают необходимость в написании руганного кода. Редакторы ресурсов, Арр Wizard и ClassWizard значительно сокращают время, необходимое на написание кода, специфичного для конкретного приложения. Например, редактор ресурсов создает заголовочный файл с уже определенными значениями для *6te/i«e-KOHCTaHTAppWizard генерирует заготовку приложения, a ClassWizard — прототипы и тела функций для обработчиков сообщений.
У библиотеки MFC огромное количество разнообразных возможностей. Классы библиотеки MFC версии 1.0, поставлявшиеся с Microsoft C/C++ версии 7.0, поддерживали следующие ВОЗМОЖНОСТИ:
интерфейс C++ к Windows API;
Iе классы общего назначения (неспецифичные для Windows), в том числе:
8 набор классов для списков, массивов и словарей (тар);
т удобный и эффективный строковый класс;■
к классы для работы со временем, временными интервалами и датами;
и классы для независимого от операционной системы доступа к файлам;
к поддержка систематизации сохранения объектов на диске и их считывания с диска;
иерархия классов от общего корневого объекта;
поддержка приложений с многодокументным интерфейсом (MultipleDocumenc Interface, MDI);
поддержка для OLE версии 1.О.
Вторая версия (включенная в Visual C++ 1.0) подхватила «эстафетную палочку» у первой версии: введена поддержка многих функций пользовательского интерфейса, свойственных
современным Windows-приложениям, а также каркасная архитектура приложений. Вот какие новые возможности появились в MFC 2.0:
полная поддержка команд Open, Save, Save As в меню File и списка последних открывавшихся файлов;■■
предварительный просмотр перед печатью и поддержка принтера;
поддержка окон с прокруткой и разбиением;
поддержка панелей инструментов и строкеостояния;
доступ к элементам управления Visual Basic;
поддержка контекстно-зависимой справки;
поддержка автоматической обработки данных, вводимых в диалоговом окне;
улучшенный интерфейс с OLE 1.0;'
поддержка DLL.
Классы версии 2.5 (в Visual C++ версии 1.5) ввели в библиотеку MFC:
поддержку механизма ODBC, что позволило приложениям оперировать информацией, хранящейся в таких базах данных, как Microsoft Access, FoxPro и Microsoft SQL Server;
интерфейс c OLE2.01, включая поддержку редактирования «по месту», связывания, OLE- автоматизации, а также перемещения объектов мышью (drag and drop).
Visual C++ 2.0 был первой 32-разрадной версией продукта и включал поддержку Windows
NT версии 3.5. В него входила MFC версии 3.0 со следующими новыми возможностями:
поддержка диалоговых окон с вкладками (входила и в версию 1.51 Visual C++, поставляемую на том же компакт-диске);*
стыкуемые панели элементов управления, реализованные в MFC;
поддержка окон с тонкими рамками (thin-frame windows);
отдельный CD К (Control Development Kit) для построения 16- и 32-разрядныхэлементов управления на базе OLE, хотя поддержка OLE-контейнеров элементов управления еще не предусматривалась.
Последующая версия — Visual C++ 2.1 с MFC 3.1 — добавила:
поддержку новых стандартных элементов управления из бета-версии Windows 95;
1 • драйвер ODBC Level 2, совместимый с управляющим ядром баз данных Access Jet;
классы Winso ck для передачи данных по TCP/IP.
Далее Microsoft решила пропустить третью версию Visual C++ и приступила прямо к четвертой — чтобы синхронизировать номера версий Visual C++ и MFC. В библиотеке MFC 4.0 появились следующие дополнительные возможности:
|* новью классы объектов доступа к данным на базе OLE (DAO) для работы с Jet;
стыкуемые панели элементов управления Microsoft Windows 95 вместо панелей элемен- товуправления MFC;
I • полная поддержка стандартных элементов управления из финальной версии Windows 95 с новыми классами «древовидный список* (tree view) и «поле ввода с форматированием» (rich-edit view);
новые классы для синхронизации потоков;
поддержка OLE-контейнеров элементов управления.
Следующим важным этапом стала версия Visual C++ 4.2 с MFC 4.2, в которую были включены следующие возможности:
KnaccHWlnlnet;
классы серверов ActiveX-документов;
классы синхронных и асинхронных ActiveX-моникеров;
усовершенствованные MFC-классы элементов управления ActiveX, с такими возможностями, как активизация без образования окна, оптимизированный код отрисовки и т. д;
улучшенная подцержка MFC ODBC, включая массированную загрузку наборов данных и передачу данных без привязки.
В Visual C++ 5.0 с MFC4.21, в которой было исправлено несколько ошибок версии 4-2, было введено еще несколько заслуживающих внимания возможностей;
обновленная среда разработки, Developer Studio 97; ее особенности —основанная на HTML справочная система и интеграция с другими языками, включая Java;
Active Template Library (ATL) для эффективной разработки элементов управления ActiveX для Интернета;
подцержка в языке C++ при помощи директивы ^import программированияСОМ-клиен- тов на основе библиотек типов, как описано в главе 25.
Последняя версия, Visual C++ 6.0 содержит MFC 6.0 (обратите внимание, что номера версий вновь синхронизированы). Многие из возможностей MFC 6.0 позволяют разработчику поддерживать концепцию Microsoft Active Platform:■
классы MFC, инкапсулирующие новые стандартные элементы управления Windows, которые являются составной частью в Internet Explorer 4.0;
поддержка Dynamic HTML, позволяющая MFC-программисту разрабатывать приложения, способные динамически изменять и создавать HTML-страницы;
технология Active Document Containment, позволяющая приложениям, основанным на MFC, включать активные документы (Active Documents);
подцержка шаблонов поставщиков и потребителей OLE DB и привязки данных ADO, что ойень удобно для разработчика программ доступа к ЕД, использующего MFC или ATL
Путь, который Вам предстоит
Все сказанное выше звучит многообещающе, не правда ли? Но Вы уже, вероятно, подумали про себя: «Просто за так ничего не получишь». Верно. Чтобы эффективно пользоваться каркасом приложений, надо досконально изучить его, а это требует времени. И если Вам придется осваивать и C++, и Windows, и библиотеку MFC (без OLE), то сделать что-то полезное Вы сможете не раньше, чем через полгода. Что интересно — на изучение только Win32 API уходит примерно столько же времени.-
Как же так, спросите Вы, если библиотека MFC предлагает столько всяких возможностей? Ну, прежде всего Вы сможете избежать многого из того, чему вынуждены учиться Win32-npo- граммисты на С. Исходя из собственного опыта, можем сказать, что объектно-ориентированный каркас приложений упрощает обучение программированию для Windows — если, конечно, Вы разбираетесь в объектно-ориентированном программировании.
Безусловно, библиотека MFC не сделает программирование для Windows достоянием масс. Здесь надо сказать, что Windows-программисты вообще «стоят» выше по сравнению с другими программистами, и вряд ли эта ситуация изменится в ближайшее время. Широкий спектр возможностей MFC, вместе с мощью каркаса приложений, служит гарантией сохранения высокого спроса на программистов, умеющих работать с MFC.
Понятие каркаса приложений
Одно из определений термина каркас приложений (application framework) таково: «интегрированный набор объектно-ориентированных программных компонентов, обеспечивающих все, что нужно для программы общего назначения». Не очень-то полезное определение, не так ли? Если Вы действительно хотите знать, что такое каркас приложений, дочитайте книгу до конца. Первым Вам поможет пример, с которым Вы познакомитесь чуть позже.
Каркас приложений и библиотека классов.
Одна из причин популярности языка C++ заключается в том, что его можно «расширять» библиотеками классов. Часть таких библиотек поставляется с компиляторами C++, часть продается независимыми фирмами, а какие-то библиотеки создаются программистами, так сказать. для внутреннего потребления. Библиотека классов — это набор взаимосвязанных классов C++, которые можно использовать в приложении. Математическая библиотека классов может выполнять наиболее распространенные математические операции, а библиотека коммуникационных классов — поддерживать обмен данными по последовательному каналу. Иногда Вы конструируете объекты из предлагаемых классов, иногда создаете из них собственные классы — все зависит от конструкции конкретной библиотеки.
Каркас приложений — это надмножество библиотеки классов. Обычная библиотека представляет собой изолированный набор классов, предназначенных для использования в любой программе, а каркас приложений определяет структуру самой программы. Концепцию каркаса приложений изобрела не корпорация Microsoft — она сначала появилась в академических кругах, а первым коммерческим воплощением был МасАрр для Apple Macintosh, После появления MFC 2.0 реализацией подобных продуктов занялись и другие фирмы, в том числе Borland (Inprise).
Пример на базе каркаса приложений
Хватит общих рассуждений. Пора взглянуть на какой-нибудь код — не псевдокод, а настоящий, который действительно можно скомпилировать и выполнить с библиотекой MFC. Угадайте, какой? Ну конечно, это стараядобрая программа «Hello, world!», правда, с некоторыми дополнениями. (Если Вам доводилось работать с первой версией библиотеки MFC, то этот код Вам знаком, за исключением базового класса окна-рамки.) Кстати, это почти минимум кода, необходимого для Windows-приложения с применением библиотеки MFC. Сравните его с аналогичным чисто ^пЗЗ-приложением, приведенным в книге Ч. Петцольда Programming Windows95 (Microsoft Press, 1996 г.). He пытайтесь сразу же разобраться в каждой строке. Не набирайте и не тестируйте его, потому что пример ЕХ23В на компакт-диске — это его практически полная копия. Потерпите до следующей главы — там-то Вы и начнете работать с «ре альным» каркасом приложений.
ПРИМЕЧАНИЕ Имена классов библиотеки MFC принято начинать с буквы С.
Ниже приведен исходный код для заголовочного файла и файла реализации (implementation file) нашего приложения MYAPP. Два класса — СМуАрри. CMyFrame— порождены от базовых классов библиотеки MFC. Сначала взгляните на заголовочный файл MYAPP.H для приложения MYAPP:
// Класс "приложение" class СМуАрр: public CWinApp { public:
virtual BOOL InitlnslanceO;
};
И Класс "окно-рамка"
class CMyFrame: public CFrameWid <
public:
CMyFrameO;
orotected:
// 'afxjnsg1 означает, что следующие две функции являются частью
// системы распределения сообщений библиотеки MFC
afx_msg void OnLButtonDown(UINT nFlags, Cpoint point);
afxjnsg void OnPaint();
DECLARE MESSAGE MAP()
А вот файл реализации MYAPP.CPP для приложения MYAPP:
^Include <afxwin.h> // Заголовочный файл библиотеки MFC объявляет базовые классы einclude "myapp.h"
СМуАрр theApp; // Один-единственный объект СМуАрр
800L СМуАрр::lnitlnstance()
m_pMainWnd - new CMyFrameO;
m_pMainWnd->SnowWindow(m_nCmdShow);
m_pMainWnd->'JpdateWi ndow();
return TRUE;
BEGIN_MESSAGE MAP(CMyFrame, CFrameWnd)
ON WH_l8UTTONDOWN()
ON WH PAINTO
ENO JOSAGE^MAP()
CMyFrame: '.CMyFrameO
{
Create(HULL. "MYAPP Application");
void CMyFrame: :OnLButtonDown(UINT nFlags, Cpoint point)
ТНАСЕ("Входим в CMyFrarre: ;0nl.8uttonDowi - %lx, Kd, %d\n", (long) nFlags, point.x, paint.y);
void CMyFrame::0nPaint()
{
CPaintDC dc(this);
dc.TextOut(O, 0, "Hello, world!");
}
Теперь обсудим некоторые элементы программы.
Функция Мл/Ия/п. Вспомните, что Windows требует наличия в любой программе функции WinMain. Здесь Вы ее не видите потому, что она скрыта внутри каркаса приложения.
Класс СМуАрр. Объект класса СМуАрр представляет программу. В программе определяется единственный глобальный объект класса СМуАрр — tbeApp. Базовый класс (ЛГ/^/^опреде- ляет основные характеристики объекта tbeApp.
Запуск приложения. При запуске приложения Windows вызывает встроенную в каркас приложений функцию WinMain, а та шщт глобально сконструированный объект класса, производного от CWinApp. Не забудьте, что в программах на C++ глобальные объекты конструируются перед исполнением основной части программы.
.Функция-член CMyApp::initlnstance. Когда УРшЛ/йшнаходит объект «приложение», опа вызывает виртуальную функцию-член Initlnstance, которая делает вызовы, необходимые для создания и отображения на экране основного окна-рамки программы (application's main frame window). Вы должны переопределить Initlnstance в своем производном классе «приложение» так как базовый класс CWinApp не имеет ни малейшего представления о том, какого типа основное окно-рамку Вы хотите создать. ■
Функция-член CWInApp::Run. Функция Run скрыта в базовом классе, но она распределяет сообщения программы между ее окнами, обеспечивая тем самым работу этой программы. WinMain вызывает Run после вызова Initlnstance. '
Класс CMyFrame. Объект класса CMyFrame представляет основное окно программы. Когда конструктор вызывает функцию-член Create базового класса CFrameWnd, Windows создаст действительную оконную структуру, а каркас приложения связывает ее с С++-объектом. Для вывода окна па экран вызываются функции SbowWindown UpdateWindou^Taxxe функции- члены базового класса).
Функция CMyFrame::OnLButtonDown. Она включена для демонстрации возможностей обработки сообщенийв библиотеке MFC. Мы объявляем о сопоставлении события «нажатие левой кнопки мыши» с функцией-членом класса CMyFrame. Подробнее о сопоставлении сообщений в библиотеке MFC мы поговорим в главе 4, а пока просто примем к снедению, что эта функция вызывается при нажатии левой кнопки мыши. Функция использует макрос TRACK из библиотеки MFC, чтобы вывести сообщение в отладочное окно.
Функция CMyFrame::OnPaint. Каркас приложений вызывает эту функцию-член класса СМуЕтате всякий раз, котданадо перерисовать окно — в начале работы программы, при изменении размеров окна и при обновлении всего окна или его части. Оператор CPaintDC относится к GDI и поясняется в следующих главах. Ну, а функция ТелтОмгвыводитна экран «Hello, world!» («Здравствуй, мир!»).
Завершение приложения. Пользователь завершает приложение, закрывая основное окно программы. Тем самым он инициирует последовательность событий, заканчивающихся уничтожением объекта класса CMyFrame, выходом из Run, выходом из WinMain и уничтожением объекта класса СМуАрр.
Еще раз взгляните на пример и попробуйте теперь представить всю картину целиком Очевидно, что большая часть функциональных возможностей программы сосредоточена в базовых классах библиотеки MFC: CWinApp и Фпяте^Тг^.Составляя MYAPP, мы следовали нескольким простым правилам структуризации и поместили ключевые функции в производные классы. Как видите, C++ позволяет «заимствовать» большие объемы кода, не копируя его. Считайте это партнерскими отношениями между нами и каркасом приложений. Последний создает структуру программы, а мы — код, наполняющий эту структуру.
Теперь Вы, наверное, начинаете понимать, почему каркас приложения нечто большее, чем библиотека классов. Он определяет не только структуру программы — его роль значительнее. чем у базовых классов C++. Вы уже видели в действии скрытую функцию WinMain, а прочие компоненты каркаса поддерживают обработку сообщений, динамически подключаемые библиотеки и многое другое.
Сопоставление сообщений в библиотеке MFC
Вспомним функцию-член OntoutkmDoum из предыдущего примера. Можно подумать, что OnLBultonDotvn — идеальный кандидат на роль виртуальной функции. При таком подходе базовый класс окна определил бы виртуальные функции для сообщений о событиях, связанных с мышью, и других стандартных сообщений, а производные классы окна могли бы при необходимости переопределять эти функции, Некоторые библиотеки классов для Windows именно так и устроены. '
Но каркас приложений библиотеки MFC не использует виртуальные функции для сообщений Windows. Вместо этого с помощью макросов он ^соотносит» (mapping) определенные сообщения с функциями-членами производных классов. Чем же объясняется отказ от виртуальных функций? Представьте себе, что виртуальные функции для сообщений в MFC все- таки используются. Класс СЗ^н^должен был бы объявлять виртуальные функции более чем для 100 сообщений. Для каждого производного класса, задействованного в программе, C++ требует наличия таблицы диспетчеризации виртуальных функций (viable) (виртуальная таблица). Для каждой виртуальной функции в этой таблице потребуется 4-байтовая запись — независимо оттого, переопределена ли функция в производном классе на самом деле. Таким образом, для каждого типа окна или элемента управления приложению для поддержки виртуальных обработчиков сообщений нужна таблица размером более 400 байтов.
А как насчет обработчиков сообщений о командах меню и щелчках кнопок мыши? Их не определишь как виртуальные функции в базовом классе окна, поскольку в каждом приложении может быть свой набор команд меню и кнопок. Система таблиц сообщений, принятая в библиотеке MFC, позволяет обойтись без длинных виртуальных таблиц и сглаживает различия между обычными Windows-сообщениями и командными сообщениями, специфичными для программ. Поэтому отдельные классы, отличные от оконных (классы «документ» и «приложение»), могут обрабатывать и командные сообщения. Для сопоставления (или увязки) сообщений Windows с функциями-членами C++ в MFC используются макросы. Никакие расширения языка C++ не нужны.
MFC-обработчик сообщения требует наличия прототипа функции, тела функции и соответствующей записи в таблице сообщений. Вставлять обработчики сообщений в классы помогает Class Wizard. Вы выбираете идентификатор Windows-сообщения из списка, a Class Wizard генерирует код с необходимыми параметрами функций и возвращаемыми значениями.
Документы и их представление
В предыдущем примере мы использовали объект «приложение» и объект «окно-рамка» (frame window object). Ло большинство ваших приложений на базе библиотеки MFC будут гораздо сложнее. Как правило, в них, помимо только что упомянутых классов, будут содержаться еще два класса: «документ* (document) и «вид», или «представление» (view). Такая архитектура «документ-вид» — стержень каркаса приложений; она напоминает классы Model/View/Cont- roller, принятые вереде Smalltalk.
Проше говоря, архитектура «документ-вид» отделяет данные от их представления. Очевидное преимущество этого подхода — возможность представить одни и те же данные по- разному. Предположим, надиске хранится документ с биржевыми сводками за месяц, адан- ные представлены в виде таблицы и графика. Мы изменяем значения в окне табличного представления, и содержимое окна графического представления тоже изменяется, потому что оба окна отображают одну и ту же информацию (но представленную в разных видах).
В библиотеке MFC документам и их представлениям сопоставляются экземпляры (instances) классов C++. Например, на рисунке 2-1 показаны три объекта класса GSlochDoc. соответствующие трем фирмам: AT&T, IBM и GM. Все три документа связаны с табличным представ-
лением, а один из них — еще и с графическим. Как видите, здесь четыре объекта «вид»: три объекта класса CStockLi§tViewH. один — класса CStocfeCbartView.
Окно I таблицыОкно I таблицы.«тк.«тк
Окно I таблицы
Окно I таблицы
.«тк
.«тк
'Рис, 2-1. Взаимосвязь документов и их представления
Код базового класса «документ» взаимодействует с командами Open и Save меню File; действительное чтение и запись данных объекта «документ» реализуют производные классы «документ». (Каркас приложений берет на себя большую часть работы по выводу на экран диалоговых окон File Open и File Save, а также по открытию, закрытию, чтению и записи файлов) Базовому классу «вид* сопоставлено окно, содержавдеся внутри окна-рамки; производный класс «вид» взаимодействует со своим, сопоставленным ему классом «документ» и отвечает за вывод информации на экран и принтер. Производный класс «вид» и его базовые классы обрабатывают сообщения Windows. Библиотека MFC «дирижирует» всеми взаимосвязями между документами, их представлениями, окнами и объектом «приложение» в основном за счет виртуальных функций.
Не подумайте, что объект «документ» должен быть связан с дисковым файлом, целиком считываемым в память. Если, например, «документ» — на самом деле базаданных, Вы можете переопределить выбранные функции-члены класса «документ», и тогда команда Open меню File будет выдавать вместо списка файлов список баз данных.
ЧАСТЬ II
Класс «вид» библиотеки MFC
ГЛАВА 3
Знакомство с AppWizard — «Hello, world!»
и главе 2 мы в общих чертах познакомились с архитектурой «документ-вид» библиотеки MFC б.О. В данной главе Вы увидите, как создать приложение на базе функций этой библиотеки, не вдаваясь во все сложности иерархии классов и взаимосвязей объектов. Вы поработаете только с одним компонентом программы — классом «вид» (view class), тесно связанным с объектом «окно». Такие компоненты, как класс «приложение» (application class), «окно- рамка» (frame window) и «документ», можно пока игнорировать. Ваша программа, конечно, не сможет сохранять свои данные на диске и не буцет поддерживать множественное представление информации — о том, как это сделать, и о многом другом Вы прочтете в части III.
Поскольку в Windows-приложениях важную роль играют ресурсы, Вы будете использовать ResourceView для визуального просмотра ресурсов своей новой программы. Кроме того, мы дадим несколько советов по настройке среды Windows, чтобы скорость сборки программы была максимальной, а также по оптимизации вывода отладочной информации.
ТЕХНИЧЕСКИЕ ТРЕБОВАНИЯ Чтобы скомпилировать и запустить примеры программ, приведенные в этой и последующих главах, надо установить на компьютере Microsoft Windows 95 или Microsoft Windows NT версии 4.0 (или выше), а также все компоненты Microsoft Visual C++ 6.0. Убедитесь, что каталоги с исполняемым файлом Developer Studio, библиотеками и включаемыми файлами указаны правильно. (Поменять эти каталоги можно при помощи команды Options из меню Tools.) Если у Вас возникнут трудности, обратитесь к документации Visual C++ и файлам README.
Что такое «вид»
С точки зрения пользователя, вид (view)— это обычное окно, которое можно изменять в размерах, перемещать и закрывать, как любые окна в приложениях Windows. С точки зрения программиста, вид — это объект С++-класса, производного от класса CViewбиблиотеки MFC. Как и для любого объекта C++, поведение объекта «вид* определяется функциями-членами (и переменными-членами) его класса, включая и специфичные для приложения функции из производного класса, и стандартные функции, унаследованные от базовых классов.
Используя Visual C++, Вы мажете создавать достаточно интересные Windows-приложения, просто добавляя код в производный класс «вид», сгенерированный AppWizard. Во время работы вашей программы каркас приложения MFC создает объект производного класса «вид» и отображает на дисплее окно, тесно связанное с этим объектом C++. Как обычно при программировании на C++, код класса разделен между двумя исходными модулями — заголовочным файлом (Н) и файлом реализации (СРР). '
Интерфейсы SDI и MDI
Библиотека MFC поддерживает приложения двух разных типов: однодокументный интерфейс (Single Document Interface, SDI) и многодокументный интерфейс (Multiple Document Interface, MDI). Сточки зрения пользователя, SDI — это приложение, состоящее из единственного окна. Если такое приложение работает с «документами» в дисковых файлах, то в каждый момент времени можно загрузить только один документ. Примером приложения SDI служит Windows Notepad. Б MDI-приложении имеется несколько дочерних окон (child windows), каждое из которых соответствует отдельному документу. Прекрасный пример такого приложения — Microsoft Word.
При запуске AppWizard для создания нового проекта параметр MDI выбран по умолчанию. В начальных примерах этой книги мы будем создавать SDI-приложения, так как в них меньше классов и требуется учитывать меньше особенностей. Убедитесь, что для этих примеров Вы выбрали тип приложения Single Document (в первом диалоговом окне AppWizard). Начиная с главы 18, Вы будете создавать MDI-приложения. Архитектура каркаса приложения MFC позволяет легко преобразовывать большинство SDI-примеров в MDI-приложения.
Пример ЕХОЗА-
AppWizard генерирует код работающего приложения MFC, Это приложение просто выводит на дисплей пустое окно с меню. Впоследствии Вы добавите к нему код, выполняющий отри- совку внутри окна. Далее приведены этапы создания приложения.
Сгенерируйте приложение SDI с помощью AppWizard. Из меню File в Visual С++ выберите New, затем щелкните в появившемся на экране диалоговом окне вкладку Projects, как показано ниже.
Убедитесь, что в списке выделен элемент MFC AppWizard (ехе), и затем введите в поле Location путь С:\осрр32\. В поле Project Name введите вхОЗа и щелкните ОК Теперь Вам предстоит пройти через последовательность шагов AppWizard, первый из которых представлен на первом рисунке на стр. 24.
Убедитесь, что Вы выбрали Single Document В следующих четырех диалоговых окнах оставьте все без изменений. Последнее диалоговое окно будет выглядеть как на втором рисунке на стр. 24.
Обратите внимание, что при генерации имен классов и исходных файлов за основу было принято название проекта ЕХОЗА. Сейчас Вы можете, при желании, изменить названия. Щелкните кнопку Finish. Непосредственно перед генерацией кода AppWizard отображает
диалоговое окно New Project Information (см. третий рисунок), где представлена информация о новом проекте.'■
После того, как Вы щелкнули кнопку OK, AppWizard создает подкаталог приложения (ехОЗа в каталоге \vcpp32), а в нем ряд файлов. По окончании работы AppWizard посмотрите на содержимое каталога приложения. Интересны (на данный момент) следующие файлы.
■файл
Описание
exO3a.dsp
Файл проекта, при помощи которого Visual C++ будет выполнять сборку приложения
ex03a.dsw ехОЗагс ex03aMew.cpp
Файл рабочего пространства, содержащего единственный проект ex03a.dsp
Текстовый файл описаний ресурсов
Файл реализации класса «вид», в котором находятся функции-члены класса CExOSaView
ex03aVlew.h
ex03a.opt
Заголовочный файл класса «вид», содержащий объявление класса CEx03aView Двоичный файл, в котором Visual C++ хранит информацию о том, какие файлы открыты для данного проекта и как расположены- окна. (Файл не создается до тех пор, пока Вы не сохраните проект)
ReadMe.txt
Resou ice.h
Текстовый файл, в котором описано назначение сгенерированных файлов Заголовочный файл,содержащийопределенияконстант ^define
Откройте файлы ex03aView.cpp и ex03aView.h и взгляните на исходный код. Вместе эти файлы определяют класс CExOSaView— центральный в приложении. Объект класса СЕхОЗа- Viewсоответствует рабочему окну программы, где и происходят все «события».
Выполните компиляцию и компоновку сгенерированного кода. Помимо генерации кода, AppWizard создает для вашего приложения файлы проекта и рабочего простран- 'ства. Файл проекта ex03a.dsp описывает все зависимости файлов, а также параметры компилятор а и компоновщика. Таккакновый проект становитсятекущим проектом Visual C++, Вы можете собрать приложение, выбрав Build Ex03a.exe из меню Build или щелкнув показанную здесь кнопку Build на панели управления.
Если сборка прошла успешно, то в подкаталоге Debug каталога \vcpp32\ex03a будет создан исполняемый файл ех03а.ехе. Файлы OBJ и другие промежуточные файлы также помещаются в каталог Debug. Сравните структуру каталогов на диске со структурой страницы File View в окне Workspace.
File View содержит логическое представление проекта. Заголовочные файлы располагаются в разделе Header Files, хотя физически они хранятся в том же подкаталоге, что и файлы СРР, Файлы ресурсов хранятся в подкаталоге \res.
Протестируйте полученное приложение. Выберите из меню Build пункт Execute Ех03а.ехе. Поэкспериментируйте с программой. Она мало на что способна, не так ли? (Но вряд ли можно ожидать большего, не написав ни строки кода.) На самом деле, как Вы, вероятно, догадываетесь, у программы много возможностей, просто они еще не активизированы. Закончив эксперименты, закройте окно программы.
Просмотрите исходные тексты программы. Выберите из меню Tools пункт Source: Browser. Если параметры проекта не требуют создания базы данных средства просмотра, Visual C++ предложит изменить их соответствующим образом и перекомпилировать про грамму. (Чтобы изменить параметры самостоятельно, выберите Settings из меню Project. На вкладке C/C++ установите флажок Generate Browse Info, а на вкладке Browse Info установите флажок Build Browse Info File.).
Когда на экране появится окно Browse, выберите Base Classes And Members («Базовые классы и члены») и введите CExOiaView. Раскрыв ветви иерархии, Вы должны получить результат, аналогичный представленному ниже.
d ®*MttCRwweQ»ieCQt*l U*03eVww;: t £ CE><}3Mew:Cw^e0b№d|wid)I Ж CExC3M»w::D WillettI CEuCB^eiGelDoGtimgnifyQidli SBG^RirtirneCMvad)f ^CEMOdA^OrDie^ciawCDC*)f Я8СЕе&ЗМей« PreCregteMWcr^tfruutidgCREA Picledtd:I , СЕЛЗ<М™.:СЕхОШи^гокН I 1Ш CM3^ewI <аМШ7яп. GelBweMesMy^arivetil мем®^ Йуюиад^и!^^ ._Refefsnoet'C\«pp32SaW«03tMew «ppi'S!d ®*MttCRwweQ»ieCQt*l U*03eVww;: t £ CE><}3Mew:Cw^e0b№d|wid)I Ж CExC3M»w::D WillettI CEuCB^eiGelDoGtimgnifyQidli SBG^RirtirneCMvad)f ^CEMOdA^OrDie^ciawCDC*)f Я8СЕе&ЗМей« PreCregteMWcr^tfruutidgCREA Picledtd:I , СЕЛЗ<М™.:СЕхОШи^гокН I 1Ш CM3^ewI <аМШ7яп. GelBweMesMy^arivetil мем®^ Йуюиад^и!^^ ._Refefsnoet'C\«pp32SaW«03tMew «ppi'S!CE*03aV*w B'S tVww
d ®*MttCRwweQ»ieCQt*l U*03eVww;: t £ CE><}3Mew:Cw^e0b№d|wid)
I Ж CExC3M»w::D Willett
I CEuCB^eiGelDoGtimgnifyQidl
i SBG^RirtirneCMvad)
f ^CEMOdA^OrDie^ciawCDC*)
f Я8СЕе&ЗМей« PreCregteMWcr^tfruutidgCREA Picledtd:
I , СЕЛЗ<М™.:СЕхОШи^гокН I 1Ш CM3^ew
I <аМШ7яп. GelBweMesMy^arivetil мем®^ Йуюиад^и!^^ ._
Refefsnoet'
C\«pp32SaW«03tMew «ppi'S!
d ®*MttCRwweQ»ieCQt*l U*03eVww;: t £ CE><}3Mew:Cw^e0b№d|wid)
I Ж CExC3M»w::D Willett
I CEuCB^eiGelDoGtimgnifyQidl
i SBG^RirtirneCMvad)
f ^CEMOdA^OrDie^ciawCDC*)
f Я8СЕе&ЗМей« PreCregteMWcr^tfruutidgCREA Picledtd:
I , СЕЛЗ<М™.:СЕхОШи^гокН I 1Ш CM3^ew
I <аМШ7яп. GelBweMesMy^arivetil мем®^ Йуюиад^и!^^ ._
Refefsnoet'
C\«pp32SaW«03tMew «ppi'S!
&@|CWm
B-gj ССгМГядЯ
•• Qm*Mi
А*
rii^ W«Lsptt<jt •:rii^ W«Lsptt<jt •:IIIIСравните эти результаты с содержимым страницы ClassView окна Workspace.
rii^ W«Lsptt<jt •:
rii^ W«Lsptt<jt •:
II
II
В У «ОЗе clmuВ У «ОЗе clmuвв
В У «ОЗе clmu
В У «ОЗе clmu
в
в
BAJCMxmIDIb ф-^ СЕхИаАрр ф*9СЕ1<аз»Рос •6538851
! j ф A«erlVaMJ :CE*O3»Tieev0
i I- » -tE*MaVwO
Г • OC.'unvfCDunvCorMBKtlda) j - ft GefCocurTierffl
I i-^dn3evrf>iHrlXCOC-iiOC.CPliHlrfo,pliA>1 : I- ^OnD^CDC-pOC)
9» OnEn#*»ng(COC - pOC. CPi«M>' tMnlol ; ''--^OnPrepwePiinlff^glCPiinttnto’pInfe) |$ PBOeaiaWWpwPnFArESTnUCUul
Efl- ”lj CMwfiwa В СЭ Global!
ClassView не показывает иерархию классов, но зато не требует дополнительных накладных расходов, связанных со средством просмотра. Если Вам достаточно ClassView, не создавайте базу данных средства просмотра.
Класс CEx03aView
Класс ^ЕлРЗггУяя/егснерированAppWizardи специфичендля приложения ЕХОЗА. (AppWizard строит имена классов на основании имени проекта, заданного Вами в его первом диалоговом окне.) б’&ОЗггИ/ениаходится внизу длинной цепочки наследования классов библиотеки MFC, как Вы могли вццеть ранее в окне Browse. В классе собраны функции-члены и переменные-члены со всей цепочки. Информацию об этих классах можно получить из Microsoft foundation Class Reference (в интерактивном режиме или в отпечатанной версии), но обязательно просматривайте описания всех базовых классов, так как описания наследуемых функций-членов обычно не повторяются для производных классов.
Наиболее важные базовые классы CExOdaView — CVPWh CView, CWnd придает CEx03aVieiv свойства окна, a CView обеспечивает связь с остальными частями каркаса приложения, в частности с документом и рамочным окном, как Вы увидите в третьей части данной книги.
Рисование внутри окна представления: Windows GDI
Теперь Вы готовы к тому, чтобы написать код, который будет рисовать в окне представления. Вы внесете несколько изменений непосредственно в исходный текст ЕХОЗА.
Функция-член OnDraw
Конкретнее, Вам понадобится наполнить реализацию OnDrawB ex03aView.cpp. OnDraw — это виртуальная функция-член класса CView, которую каркас приложения вызывает всякий раз, когда необходимо перерисовать окно представления. Перерисовки требуется, когда пользователь изменил размеры окна или открыл ранее невидимые его части, либо если приложение изменило данные окна. В первых двух случаях OnDraw вызывается каркасом приложения автоматически; однако если данные окна изменены функцией внутри вашей программы, эта функция должна уведомить Windows об изменениях, вызвав унаследованную классом *«ид» функцию-член Invalidate (или InvalidateRect}. Код Invalidate впоследствии вызовет OnDraw.
Хотя внутри окна можно рисовать всегда, рекомендуется позволять ему накапливать изменения и обрабатывать их все вместе при вызове функции OnDraw. В этом случае ваша программа смажет реагировать как на события, сгенерированные ею самой, так и на события, сгенерированные Windows, например, изменения размеров окна.
Контекст устройства в Windows
Как Вы помните нз главы 1, Windows не разрешает прямой доступ к аппаратуре дисплея, а взаимодействует с ней посредством абстракции под названием *контекст устройства* (device context), который связан с окном. В библиотеке MFC контекст устройства представлен в виде объекта класса C++ с именем С DC, который передается ОпОгагскзк параметр (посредством указателя). Имея указатель на С DC, Вы можете воспользоваться множеством функций этого класса для рисования.
Добавление кода рисования в программу ЕХОЗА
Теперь напишем код, рисующий внутри окна представления текст и круг. Убедитесь, что в Visual C++ открыт проект ЕХОЗА. Чтобы найти функцию, можно воспользоваться ClassView в окне Workspace (дважды щелкните OnDraw) либо открыть исходный файл ex03aView.cpp из FileView и отыскать функцию в тексте.
Отредактируйте функцию OnDrawB exO3aView.cpp. Найдите в файле ex03aView.cpp функцию OnDraw, сгенерированную AppWizard:
vcid CExOSaView: :0n2raw(CX* pDC)
СЕхОЗаОос* pDoc - GetDocumentO;
ASSE^_VALID(oDoc):
// Доделать: добавьте код отображения своих данных.
1
Кед, выделенный полужирным шрифтом, который Вы должны ввести, заменяет старый код
void CFxOSaView: :OnDraw(CDC* pDC)
pDC->TextOut(0, 0, "Hello, world!"); //Вьвод шрифтом по умолчанию в левом верхнем углу pDC->SelcctStockObject(GRAY_BRUSH)://Выбрать ки:ть для заполнения круга
pDC->Elllpse(CRect(O, 20, 100, 120)); //Нарисовать серый круг диаметром 100 единиц
}■'
Вызов GetDocumentможно спокойно удалить, поскольку пока мы не работаем с документами. Функции TextOut,SelectStockObjectsL Ellipse — члены класса контекста устройства CDC каркаса приложения. Функция Ellipse рисует круг, если длина ограничивающего прямоугольника равна его ширине.
Дляработы с прямоугольниками Windows библиотека МРСпредоставляетудобный класс CRect. ВременныйобьекгСЯес/— аргумент, ограничивающий прямоугольникдля функции рисования эллипса. Класс СДес1буцет использоваться в примерах данной книги часто.
Перекомпилируйте и протестируйте ЕХОЗА. Выберите из меню Project пункт Build и, если нет ошибок компиляции, снова запустите программу. Теперь у Вас есть программа, работа которой сразу же вццна!
/ Для тех, кто программирует р Win32 1 ’ ЦШ.М1ЙД||Й^1М^
■ > ' Стандартная функция ^к^Й§»й оконные: процедуры стфытыгвнутри каркаса* прило- 1 ’ ■ ■ жения. Вы увцциге эти функции, когда мы будемрассма^и^тьКлассы библйотекйMFCi ■ г 1 - для окна-рамки и прщ#жвнда. В датшый -мрме™же.'
/делось сообщение WJii^PMNTi Вы ожццаете, чт® й
должно выполняться рир^в^ус в окне, а кош^сгут^Йсота н|о^од™:6”п6лучать йз/ чэдзукгуры/Н^^ф}$|фй^ЖА^^’ ^ц,^^'^•1^'■
".: Оказывается, каркас приложения сделал за Вас всю.ч^ё^трвую^боту идгерёдол кон- = /
, • текст устройства (в форме указателя на объект) виртуатйойфущецдадя^^*
/ отмечено в главе 2, йстинцд виртуальные 'функции.в.ЖМфй&^Ёла^^р,
редки. Большинство сообщений Windows каркас ЦДОЙЙ^ направляет .на обрабог-^ tЛ ку функциям таблщщ <ю9б^ений. ПрбграМАрт^]^абртаймё/В MFC 1^
вали для своих прппэррий^: классов-офн фуйкп^4та($п^^^РеМь? фД-1 '
\ нако, начиная с версии 2.5, функция О^Рв/Шнахо^гся ^таблице сообщений класса1 . . СИ^’и выполняет полиморфный вызов функций £^|ЙЙЙ\И^ Потому что OnDraw •' . должна поддерживать не только дисплей, но и принтер/ OhDrdw вызывается и Oti^iitoi. ।; - /ч ,ч , .'. -.. I - Уе р * . ^■.•Ji '*‘-**’’3v>*X-1*^J»i :^-й. w ^ Т4 <-. *1 «; й. • • i •. г 1 *.
и Оргпи^д что позволяет использовать один й тот же йздр^овагфй и для принтера, и ■ гдлядисплея. " '- ' " ;-; ",л^ г ? л, - * / </^
Первое знакомство с редакторами ресурсов
Теперь, котдаунас есть готовая программа, самое время кратко познакомиться с редакторами ресурсов. Хотя файл ресурсов приложения ехОЗа.гс — текстовый файл, изменять его с
помощью текстового редактора — не слишком удачная идея. Для этого существуют редакторы ресурсов.
Содержимое ехОЗа.гс
Файл ресурсов во многом определяет внешний вид приложения ЕХОЗА. Он содержит (или указывает на) перечисленные ниже ресурсы Windows.
Ресурс
Описание
♦Быстрая клавиша»
Задает клавиши, нажатие ид которые эквивалентно выбору элементов меню и кнопок панели управления
Диалоговое окно
Определяет формат и содержимое диалоговых окон. В ЕХОЗА имеется толь-
ко диалоговое окно About («Опрограмме»)
Значок
Значки (версии 1бг16 точек и 32г32 точки), аналогичные значкам приложений, которые отображаются Проводником Windows и в диалоговом окне About приложения. В качестве значка приложения ЕХОЗА использует эмблему MFC
Меню
Меню верхнего уровня приложения и связанные с ним раскрывающиеся меню
Таблицастрок
Строки, не являющиеся частью исходного С++-кода
Панель инструментов
Ряд кнопок непосредственно под меню
Версия
Описание программы, номер ее версии, язык и т. д.
Кроме перечисленных ресурсов, ехОЗа.гс содержит операторы:
^include "afxres.1T' ^include "а^хгез. rc"
подключающие некоторые ресурсы библиотеки MFC, общие для всех приложений. Сюда входят строки, графические кнопки и элементы, необходимые для печати и OLE.
ПРИМЕЧАНИЕ Если Вы используете MFC в виде совместно используемой DLL, то общие ресурсы хранятся внутри DLL MFC,.
Файл ехОЗа.гс также содержит оператор
«include "-esource.h”
Этот оператор вводит в приложение три константы *г7<?/?и<?:.Ю7?^1^А77^Щ цирует меню, значок, список строк и таблицу «быстрых клавиш»), ЮН_ЕХ03АТУРДиденти- фицирует значок документа по умолчанию, который в этой программе ие используется) и 1DD ЛВОЬТ5ОХ(идентификатор диалогового окна About). Этот же файл rcsource.h неявно включается исходными файлами приложения. Если с помощью редактора ресурсов Вы добавите другие константы (символы), то их определения, в конечном счете, появятся в reso- urcc.h. Будьте внимательны, редактируя этот файл в текстовом редакторе, так как внесенные В ами изменения могут быть удалены, когда Вы в следующий раз воспользуетесь редактором ресурсов.
Работа с редактором диалоговых окон
Откройте RC-файл проекта, Щелкните кнопку ResourceView в окне Workspace. Раскрой- ете все элементы — результат представлен на первом рисунке на стр. 30.
Просмотрите ресурсы приложения. Теперь потратьте немного времени, чтобы изучить отдельные ресурсы. Когда Вы выбираете ресурс, дважды щелкнув его, открывается другое окно с набором инструментов, соответствующих данному ресурсу. Если открыть ресурс диалогового окна, на экране появится палитра элементов управления. Если она не появилась, щелкните правой кнопкой поле любой панели инструментов и затем в меню установите отметку около Controls.
д-фяЙОЗаемаугвм ф^'АссУеею* I t^ IDR.MAINFRAMEд-фяЙОЗаемаугвм ф^'АссУеею* I t^ IDR.MAINFRAMEГ@ IDR^EXOTTpE Q 1DRJHANRUME ^•^ hfenu | L^iDR^WUNFaAME 0 Ф StirgTatie | *• ^StrngTabfeToobar ^I0R_Ma№^4MEГ@ IDR^EXOTTpE Q 1DRJHANRUME ^•^ hfenu | L^iDR^WUNFaAME 0 Ф StirgTatie | *• ^StrngTabfeToobar ^I0R_Ma№^4ME'•-OVSjVERSlONjNFO'•-OVSjVERSlONjNFO
д-фяЙОЗаемаугвм ф^'АссУеею* I t^ IDR.MAINFRAME
д-фяЙОЗаемаугвм ф^'АссУеею* I t^ IDR.MAINFRAME
Г@ IDR^EXOTTpE Q 1DRJHANRUME ^•^ hfenu | L^iDR^WUNFaAME 0 Ф StirgTatie | *• ^StrngTabfe
Toobar ^I0R_Ma№^4ME
Г@ IDR^EXOTTpE Q 1DRJHANRUME ^•^ hfenu | L^iDR^WUNFaAME 0 Ф StirgTatie | *• ^StrngTabfe
Toobar ^I0R_Ma№^4ME
'•-OVSjVERSlONjNFO
'•-OVSjVERSlONjNFO
Измените диалоговое окно IDDABOUTBOX. Внесите небольшие изменения в представленное ниже диалоговое окно About.
Вы можете изменить размер окна, передвигая мышью его правый и левый края, переместить кнопку ОК, изменить текст и т. д Просто щелкните элемент, чтобы выделить его, и затем с помощью щелчка правой кнопки измените его свойства.
Заново соберите проект с измененным файлом ресурсов. В Visual C++ выберите Build Ex03a.exe из меню Build. Обратите внимание, что перекомпилировать код C++ не потребуется. Visual C++ сохранит отредактированный файл ресурсов, затем компилятор ресурсов (гс.ехе) обработает ехОЗа.гс и создаст скомпилированный вариант ex03a.res, который будет передан компоновщику. Он отработает быстро, поскольку в данном случае компонует проект с приращением.
Протестируйте новую версию приложения. Снова запустите программу ВХОЗА, выберите из меню Help пункт About и убедитесь, что диалоговое окно изменилось соответствующим образом.
Конфигурации Win32 Debug и Win32 Release
Раскрыв список на панели инструментов Build, Вы обнаружите здесь два элемента: Win32 Debug и Win32 Release, (По умолчанию эта панель Build не отображается, но ее можно сделать видимей, воспользовавшись командой Customize меню Tools.) Эти элементы — названия конфигураций (targets) сборки, представляющих разные наборы параметров сборки проекта. При генерации нового проекта Арр Wizard создает две конфигурации, сводка параметров которых представлена в таблице.
Ларам етр
Окончательная сборка (Release)
Отладочная сборка (Debug)
Отладка по исходным текстам'
Отключена
Включена для компилятора и компоновщика
Диагностические макросы MFC
Отключены (определено NDEBUG)
Включены (определено _DEBUG)
Библиотеки
Рабочие библиотеки MFC
Отладочные библиотеки MFC
Оптимизация
Оптимизация по скорости (в Learning Edition отсутствует)
Без оптимизации (ускоренная компиляция)
Разработка приложений: ведется в режиме отладочной Оборки (Debug mode), а перед поставкой программа собирается заново в режиме окончательной сборки (Release mode). Исполняемые файлы, собранные в режиме Release, будут иметь меньший размер и работать быстрее. Текущая конфигурация выбирается из списка на панели инструментов Build, как показано на рисунке 1-2 в главе 1. По умолчанию результаты и промежуточные файлы сборки проекта, созданные в отладочном режиме, хранятся в подкаталоге Debug, а файлы для окончательной сборки — в подкаталоге Release. Вы можете задать другие каталоги на вкладке General диалогового окна Project Settings.
При необходимости исползуя команду Configurations из меню Build, можно создавать свои собственные конфигурации.
Активизация диагностических макросов
Макросы TRACE библиотеки MFC очень полезныдля наблюдения за работой программы. Для их работы необходимо, чтобы трассировка была активизирована, что и установлено по умолчанию. Если макросы TRACES вашей программе не генерируют никаких результатов, сначала убедитесь, что Вы запускаете отладочную версию программы из-под отладчика, и затем запустите утилиту TRACER. Если установить отметку в поле Enable Tracing, то TRACER вставляет строку
TraceEnadled - 1
в раздел [Diagnostics] файла Afx.inL (Нет, эта настройка хранится не в реестре.) С помощью TRACERмoжнoтaкжe активизировать другие диагностические средства MFC, включая выдачу информации, связанной с сообщениями, OLE, базами данных и Интернетом.
Предкомпилированные заголовочные файлы
При создании проекта AppWizard генерирует параметры и файлы, необходимые для предварительной компиляции (предкомпиляции) заголовочных файлов. Для эффективного управления проектами Вам необходимо познакомиться с работай предкомпилированныхзагсмю- вочныхфайлов (precompiledheaders).
ПРИМЕЧАНИЕ В Visual C++ имеется два «режима» предкомпиляции заголовочных файлов: автоматически и вручную. При автоматической (automatic) предкомпиляции, которая активизируется параметром командной строки компилятора/Ух, результаты работы компилятора сохраняются в файле «базы данных». Предкомпиляция вручную (manual) активизируется параметрами компилятора /Ус и /Уи, и созданные втаком режиме заголовочные файлы используется в проектах, сгенерированных AppWizard.
Предкомпилированные заголовочные файлы представляют собой «моментальные снимки», которые делает компилятор на определенной строке исходного текста. В программах MFC такой снимок обычно делается непосредственно после оператора:
«include "stdafx.h"
Файл StdAfx.h содержит операторы ^includeдля заголовочных файлов библиотеки MFC Содержимое файла зависит оттого, какой режим Вы выбрали при работе с AppWizard, однако в нем всегда имеются следующие операторы:.
«include <afxwin.h>•.
«include <afxext.h>*
Если Бы используете составные документы, то StdAfx.h также содержит строку
«include <afxoLe.h>,
а если Вы работаете с Автоматизацией или элементами управления ActiveX, то в нем содержится.
«include <afxdisp.h>
Если же Вы используете Internet Explorer 4 Common Controls, то StdAfx.h содержит «include <afxdtctl.h>
Вам могут потребоваться и другие заголовочные файлы. Например, заголовочный файл для классов наборов на основе шаблонов подключается оператором
«include <afxtempl.h>
В файле StdAfx.cpp содержится единственный оператор: ffinclude "StdAfx.h"
Этот-то файл и используется для генерации файла предкомпилированных заголовочных файлов. Заголовочные файлы библиотеки MFC, включаемые в файле StdAfx.h, никогда не изменяются, но их компиляция требует значительного времени. Параметр компилятора /Ус, используемый только для StdAfx.cpp, вызывает создание предкомпилированного заголовочного файла (РСН). Параметр /Уи, используемый для всех остальных исходных файлов, вызывает использование существующего РСН-файла. Параметр /Fp задает имя РСН-файла. В отсутствие этого параметра в подкаталоге результатов текущей конфигурации будет создан файл с именем проекта и расширением РСН. Весь процесс показан на рисунке 3-1.
/Fp «путь* задает путь для этого файла
Рис. 3-1. Процесс генерации прежомпигмроваыгьк заголовков
AppWizard автоматически генерирует параметры /Yc и /Уи, но при необходимости Вы можете внести свои изменения. Можно задавать параметры компилятора для отдельных исходных файлов. Если в диалоговом окне Project Settings выделить только файл StdAfx.h, то на
вкладке C/C++ Вы увидите параметр /Ус, который замешает параметр /Yu, заданный для всего проекта.
Учтите, что РСН-файлы достаточно велики — их типичный размер 5 Мбайт, Если использовать их неосмотрительно, то жесткий диск может переполниться. Вы можете периодически очищать каталоги Debug своих проектов или же размещать все РСН-файлы в одном каталога, используя параметр компилятора /Fp. ■
Два способа запуска программы
Visual C++ позволяет запускать программу непосредственно (с помощью сочетания клавиш Ctrl+F5) или в отладчике (с помощью клавиши F5). Непосредственный запуск выполняется значительно быстрее, так как от Visual C++ ire требуется сначала загружать отладчик. Если Вам ire нужны трассировочные сообщения и точки останова, запускайте программу с помощью Ctrl+F5 или кнопки с восклицательным знаком на панели инструментов Build.
ГЛАВА 4
Основы обработки событий, режимы преобразования координат и окна с прокруткой
В главе 3 Вы узнали, как каркас приложений MFC-библиотеки вызывает виртуальную функцию OnDrawKnacca. «вид». Заглянув в интерактивную справочную систему по библиотеке MFC, Вы узнаете, что класс CView и его базовый класс CWhd содержат несколько сотен функций- членов. Те функции, имена которых начинаются с Он — например, OnKeyDownK OnLButton- Up,— вызываются каркасом приложения в ответ на различные «события» Windows, такие, как нажатия клавиш и щелчки мышью.
Большинство этих функций не виртуальные и поэтому требуют дополнительных усилий при программировании. Б данной главе мы покажем, как с помощью ClassWizard создать структуру таблицы сообщений (message map), необходимую для подключения кода ваших функций к каркасу приложения, и разберем практический пример использования функций таблицы сообщений.
В первых двух примерах используется обычный класс CView. В примере ЕХ04А будет рассмотрено взаимодействие между событиями, генерируемыми конечным пользователем, и функцией OnDraw. Пример ЕХ04В познакомит Вас с эффектами различных режимов преобразования координат (mapping modes) в Windows.
В большинстве практических задач Вам понадобятся окна с прокруткой (scrolling view). Поэтому в примере EXQ4C вместо CView используется класс CscroHView, который позволяет каркасу приложения библиотеки MFC внести в окно линейки прокрутки и связать их с изображением.
Прием пользовательского ввода: функции таблицы сообщений
Приложение ЕХОЗАиз главы 3 не реагирует на действия конечного пользователя (за исключением стандартных команд Windows для изменения размеров и закрытия окна). Окно содержит меню и панель инструментов, но они не «подключены» к коду класса «вид». Меню и панели инструментов не будут рассматриваться до третьей части книги, так как они связаны с классом «рамка», но в Windows существует много других источников входной информации, которые не дадут Вам расслабиться. Однако прежде чем Вы смажете обработать какое-либо событие Windows, хотя бы и щелчок мышью, Вам необходимо научиться пользоваться системой таблиц сообщений MFC.
Таблица сообщений'
Когда пользователь нажимает левую кнопку мыши в окне представления, Windows посылает этому окну сообщение — а именно WM_LBUTTONDOWN. Если в ответ на это сообщение ваша программа должна выполнить некоторое действие, то в классе «вид» должна быть функция, которая выглядит следующим образам:
void CMyView::OnLButtonDown<UINT nFlags, CPoint point) {
// код обработки сообщения
}
В заголовочном файле класса должен также быть соответствующий прототип:
afxjnsg void OnL0uttonDpwn(UINT nFlags, CPoint point);.
Элемент а^_мм®реобразуется препроцессорам в пустую строку и напоминает о том, что перед нами функция таблицы сообщений. Далее в коде программы должен присутствовать макрос таблицы сообщений, подключающий функцию OnLButtonDown к каркасу приложения: SEGIN_MESSAGE_MAPCCMyView. CView)'
ON_WM_10UTTONDOWN() //Специальная запись для OnLButtonDown'
//Другие записи таблицы сообщений
EHD_MESSAGE_MAP(>
Как же узнать, какая именно функция соответствует определенному сообщению Windows? В приложении А (а также в интерактивной справочной системе по библиотеке MFQ имеется таблица, где перечислены все стандартные сообщения Windows и прототипы соответствующих им функций-членов. Вы мажете кодировать функции обработки сообщений вручную — на самом деле для некоторых сообщений это даже необходимо. К счастью, Visual C++ предоставляет инструмент Classwizard, автоматизирующий кодирование большинства функций таблицы сообщений,
Хранение состояния объекта «вид»: переменные-члены класса
Если программа принимает пользовательский ввод, имеет смысл реализовать некоторую обратную связь с пользователем. Функция OnDraw класса «вид» рисует некоторое изображение на основании текущего состояния объекта «вид», и это состояние может изменяться в результате действий пользователя. В реальном приложении MFC состояние приложения обычно хранится в объекте-документе, но нам еще предстоит дойти до этого* Пока же будем использовать переменные-члены класса «вид» m_rectHUipsew. mjnColor. Первая — это объект класса CRect, который содержит текущий ограничивающий прямоугольник для эллипса, а вторая - целое число, задающее текущий цвет эллипса.
ПРИМЕЧАНИЕ По соглашению имена нестатических переменных-членов_класса в библиотеке MFC начинаются с т_.
Наша функция таблицы сообщений будет переключать цвет эллипса (состояние объекта "вид») между серым и белым. (Пользователь будет делать это с помощью левой кнопки мыши.) Начальные значения m_rectElhps№ ^яСЪ/огзадаются конструкторам класса, а функция-член OnLButtonDown переключает цвет.
СОВЕТ Почему для хранения состояния объекта «вид» не использовать глобальную переменную? Такой подход может вызвать проблемы, если в приложении несколько таких объектов. Кроме того, инкапсуляция данных внутри объекта — одна из основ объектно-ориентированного программирования.
Инициализация переменных-членов класса «вид»
Лучшее место для инициализации переменной-члена класса — это конструктор:
CHyView: :CHyView() : m_rectEllipse(O, 0, 200, 200) {...}
Аналогично можно инициализировать »г_пСо/ог. Так как эта переменная имеет встроенный тип (целое), то компилятор сгенерирует такой же код, как если использовать в теле конструктора оператор присваивания.
Теория недействительного прямоугольника
Функция QnLButtonDown может сколько угодно переключать значение m_nColor, но если это все, что она делает, то функция OnDraw не будет вызвана (если только, например, пользователь не изменит размеры окна). Функция OnLButtonDoum должна вызывать функцию Invalida- teRect (функция-член, наследуемая классом: «вид» от CWnd). !тч^а1еКвсПшициирует посылку сообщения Windows WM_PA1NT, которое в классе CView отображается на вызов виртуальной функции OnDraw. При необходимости функции OnDraw доступен параметр «недействительный прямоугольник», который был передан в InvalidateRect. ■
В Windows оптимизировать рисование можно двумя способами. Во-первых, следует иметь в виду, что Windows обновляет только те пикселы, которые находятся внутри недействительного прямоугольника. Таким образам, чем меньше размеры этого прямоугольника (задаваемые, например, в обработчике OnLButtonDown), тем быстрее он будет перерисован. Во-вторых, исполнение команд на рисование за пределами недействительного прямоугольника - напрасная трата времени. Для получения недействительного прямоугольника функция OnDraw может вызвать функцию-член GetCIipBoxKnacca CDC и таким образом избежать попыток рисования объектов за пределами этого прямоугольника. Помните, что OnDraw вызывается не только в ответ на вызов Вами InvalidateRect, но и когда пользователь изменяет размеры окна или открывает невидимые ранее его части. Итак, функция OnDraw отвечает за все рисование в окне и обязана адаптироваться к любому недействительному прямоугольнику.
Клиентская область окна' " ■ ' 1
Клиентская область (client area) — прямоугольная часть окна, в которую не входят рамка, заголовок, меню и стыкуемые панели инструментов. Размеры клиентской области указывает функция GetClientRectKnacca CWnd. Обычно рисовать за пределами клиентской области не разрешается, да и большинство сообщений мыши поступают окну только тогда, когда ее указатель находится в клиентской области.
Арифметические операции с CRect, CPoint и CSize
Классы CRect, CPoint и CSite — производные от структур Windows RECT, POINT и SIZEn поэто- ' му наследуют следующие целочисленные переменные-члены :
CRectleft, top,right, bottom
CPointx,y
CSizecx,cy
В руководстве Microsoft Foundation Qass ReferenceBbi увидите, что для этих классов определено множество переопределенных операций. Вы можете, в частности, делать следующее: • прибавлять объект CSize к обьектуСро/пг,
• вычитать объект CSize из объекта С Point;■
m вычитать один объект CPoint из другого, в результате чего получается объект GSze; '
m прибавлять к объекту CRect объект CPoint или CSise;
m вычитать из объекта CRect объект CPoint или CSize.
Класс CRect содержит связанные с классами CPoint и СЗце функции-члены. Например, функция-член 7ор£е^возвращает объект CPoint, функция Sfce — объект CSize. С этой точки зрения объект CSize представляет собой «разницу между двумя объектами CPoint*, а объект CRect можно «сместить* на CPoint
Попадает ли точка внутрь прямоугольника?
В классе CRectесть функцияPtinRect, проверяющая, попадает ли точка внутрь прямоугольника. Второй параметр OnlButtonDown (point)— это объект класса CPoint, задающий расположение указателя мыши в клиентской области окна. Чтобы узнать, находится ли эта точка внутри прямоугольника mroctEUipse,yyxao сделать следующее:
if (m_rectEllipse.PtInRect(point)) {
/Дочка внутри прямоугольника }
Однако, как Вы скоро поймете, этой простой проверки достаточно только в том случае, если Вы работаете в координатах устройства (что пока верно).
Оператор LPCRECT
В справочнике Microsoft Foundation Class Library Reference отмечено, что GWndolnvalidateRect принимает параметр LPCRECT(указатель на структуру RECT), а не CRect. Однако CRect допускается как параметр, поскольку в классе CRect определен переопределенный оператор LPCRECTO. возвращающий адрес объекта CRect, что эквивалентно адресу объекта RECT. Таким образом, при необходимости компилятор автоматически преобразует аргументы CRect в LPCRECT, и функции можно вызывать так, как если бы они имели в качестве параметров ссылки на CRect Следующий фрагмент кода функции-члена класса «вид* '
CRect rectClient;, ,. ,
GetClientRect(rectClient);'
получает координаты клиентского прямоугольника и сохраняет их в rectClient, .. .
Попадает ли точка внутрь эллипса?
Код примера ЕХ04А определяет, была ли кнопка мыши нажата внутри прямоугольника. Более точно было бы проверить, попал ли курсор мыши внутрь эллипса. Для этого необходимо создать объект класса CRgn, соответствующий эллипсу, и затем вместо PtinRect воспользоваться функцией PtlnRegfon. Фрагмент программы приведен ниже:
GRgn rgn;■
rgn. С геатеЕШ pt icRgnlnd i rect (m_ rectEllipse);~
if (rgn.PtlnRegion(point)) {.
//Точка внутри эллипса■•■■
}
Заметьте: CreateEllipticRgnlndirect— еще одна функция, принимающая параметр LPCRECT. Она создает специальную внутреннюю структуру региона Windows, представляющую эллип-
ТИЧеский регион внутри окна. Затем эта структура связывается с объектом C++ CRgn в программе. (Структуру этого же типа можно использовать и для представления многоугольника.)
Пример ЕХ04А
В примере ЕХ04А эллипс, который здесь оказался кругам, изменяет свой цвет, если пользователь щелкает левой кнопкой мыши, а курсор мыши находится в пределах прямоугольника, описанного вокруг эллипса, Для хранения состояния используются переменные-члены класса «вид*, а для перерисовки изображения применяется функция InuahdateRect.
В примере ЕХОЗА рисование в окне зависело только от одной функции OnDraw. В примере ЕХ04А нам понадобятся три функции, включая конструктор, и две переменные-члены. Полный текст файлов заголовка и реализации для класса СЕхО^в^Уеищриведен на рисунке 4-1. (Этапы создания программы описаны после листинга.) Все изменения по сравнению с кедом, сгенерированным AppWizard, а также ClassWizard (для OnLButlonDown), выделены полужирным.
EX04AV1EW.H
И ex04aView. h : интерфейс класса CEx04aView.
//
//////////////////////////////////////^^^
«if !defined(AFX EX04AVIEW Н В188ВЕ41 6377.1100 8FD4 00C04FC2A0C2 „INCLUDED )
«define AFX_EXO4AVIEW_H„B188BE4l-6377 1100 8FD4 00C04FC2A0C2
..INCLUDED.
«if .MFC.VER > 1000
«pragma once
tfendif // .MFC.VEfl > 1000
class CEx04aView : public CView i
protected: //Создание только сериализацией
CEx04aView();
DECLARE_DYNCREATE(CEx04aView)
// Атрибуты
public:
CEx04aDoc« 6etDocument();
// Операции"'
public;
//Переопределения
// Переопределенные функции, сгенерированные ClassWizard
//{ { AFX_VIRTUAL (CEx04aVlew) .
public:
virtual void OnDraw(CDC* pOC); //Переопределена для отрисовки данного окна
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected:
virtual BOOL OnPreparePrinting(CPrintInfo* pinfo);
virtual void QnBeginPrinting(CDC* pDC, CPrintInfo‘plnfo);
virtual void OnEndPrinting(CDC* pDC, CPrintlnfo* pinfo);
//HAFX_VIRTUAL
Рис. 4-1. Файлы заголовка и реализации примера CExO4aView
// Реализация
public:■
virtual ~CEx04aView();
flifdef „DEBUG’
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;
flendif'
protected:
// Сгенерированные функции таблицы сообщений protected:
П\{AFX_MSG(CExO4aV.ieyi)
afx msg void OnLButtonDown(UINT nFlags, CPoint point);
//}}AFX MSG.
DECLARE_MESSAGE_MAP()
private:'
int m_nColor;
CRect m rectEllipee;
):
tfifndef „DEBUG //Отладочная версия находится в ex04aView.cpp-
inline CEx04aDoc* CEx04aView::GetDocument()
{ return (CEx04aDoc*)m_pDocument; } ftendif
///////////////////////////////////////////////////////////////////////
//<(AFX_INSERT_LOCAT1ON})
H Microsoft Visual C++ внесет дополнительные объявления // непосредственно перед предыдущей строкой.
«endif Ц I defined(AFX_EX04AVIEW_H„B18№E41„6377_11D0_8FD4_00C04FC2A0C2_INCLUDED_)
EX04AVIEW.CPP
// ex04aView.cpp ; реализация класса CEx04aView //
«include "stdafx.h"
«include "ex04a,h"
ffinclude 'ex04aDoc.h"
flinclude‘ex04aView.h"
ftifdef_DEBUG
fldefine new DEBUG NEW
flundefTHlS FILE ~
static char THIS_FILE[] = _FILE_;
tfendif•
llllllllllllHIIIinUIIHIIIIinillHIIIIIIIillllllltlHIIIIIIIIIHin Ц CEx04aView.
IMPLEMENT_DYNCREATE(CEx04aView, CView)
BEGIN_HESSAGE MAP(CEx04aVlew, CView)
//{(AFX MSG MAP(CEx04aView)
ON WM LBUnONDOWNC).
см, след, стр.
Рис. 4-1. (продолжение)•
//}}AFX_MSG_MAP'
// Стандартные команды печати, ON COMMAND(ID_FILE_PRINT, CView: :OnFilePrint) ON COMMAND(ID FILE_PRINT DIRECT, CView::OnFilePrint) ON COHMANO(ID FILE PRINT PREVIEW, CView::OnFilePrintPreview)
END MESSAGE MAP() 11П1Н1Ш!ШШ1Ш11ШШ1Ш1Ш1Т1ШГ111Ш11ШШ1Ш111Ш11 И конструкторы/деструкторы CEx04aView
CEx04aView::CExD4aView() : m_rectEllipse(O, 0, 200, 200) {
pi nColor = GRAY_BRUSH;
У
CEx04aView;:~CEx04aView() { }
BOOL CEx04aView; :PreCreateWlndow(CREATESTRUCT& cs)
// Доделать: модифицируйте здесь класс или стили окна, // модифицировав структуру CREATESTRUCT cs
ret urn CView: :PreCreateWindow(cs);
Ш1ШШШНП1ШтЛШ1ШШИШШНШШШШШШШШ1 И CEx04aView: отрисовка
void CExG4aView: :0n0raw(CDC« pDC) {
pDC->SelectstockObj ect (m_nColor ); pDC->Ellipse(m_rect Ellipse);
}
/////////////////////////////////////7///////////////////////////////// // CEx04aView: печать
BOOL CEx04aView: .OnPreparePrinting(CPrintInfo* plnfo) {
//Подготовка по умолчанию returnDoPreparePrinting(pInfo):
}
void CEx04aView: :0nBeginPrlntjng(CDC« /«pDCV, CPrintlnfo* /«plnfo*/) { 1.
// Доделать: добавьте сща дополнительную инициализацию перед печатью
void CEx04aVlew: :OnEndPrlntihg(CDC« /*рОС«/, CPrintlnfo» /*pInfo«/> //Доделать: внесите операции по очистке после печати
////////////////////////W^^^
// CEx04aVlew: диагностика■
#ifdef J)EBUG
void CEx04aView::AssertValid() const
{
CView::AssertValid();
}
void CEx04aView::Dump(CDumpContext& de) const CView::Dump(dc);
CEx04aDoc* CEx04aView.:GetDocument() // неотладочная версия встроена в объявление класса
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CEx04aDoc)));
return <CEx04aDoc«)m_pDocument;
tfendif//.DEBUG
шшишшш1шшншшшш11шшшшшш1шш!шп
П CEx04aView: обработчики сообщений
void CEx04aView::Onl8uttonDown(UINT nFlags, CPoint point)
{'
if (m_rectEllipse.PtInReot(point)) {
if (m nColor== GRAY.BRUSH) { m_nColor = WHITE BRUSH;
else {
m_nColor = GRAY BRUSH;
1
InvalideteRect(.m_rectEllipse);
Использование ClassWizard с EX04A
Взгляните на следующий фрагмент ex04aView.h:
//{{AFX_MSG(CExO4aVlew)
afx msg void OnLButtonDown(UINT nFlags, CPoint point);
//}}AFX_MSG■
и на фрагмент cxt)4aView.cpp:
//{{AFX MSG MAP(CEx04aView)
ON WM LBUTTONOOWK’O
//}}AFx_hsg_map■
Здесь AppWizard сгенерировал странные комментарии для ClassWizard. ClassWizard добавляет прототипы обработчиков сообщений между «скобками» AFX_MSG, а записи таблицы сообщений — между «скобками» А/^А^^ИдР-Кроме того, ClassWizard генерирует шаблон функции-члена OnLButtonDoww в ex04aView.cpp, содержащий соответствующие объявления типов параметров и возвращаемого значения.
Обратите внимание на отличие связки AppWizard-ClassWizard от обычного генератора исходных текстов. Обычный генератор запускается один раз, после чего программист редактирует полученный код. AppWizard запускается для генерации приложения только один раз, но ClassWizard можно использовать сколь угодно раз, и Вы можете редактировать код в любой момент. Исходный текст можно безопасно изменять, если при этом не изменяется содержимое внутри «скобок» AFX_MSG hAFX_MSG_MAP.'
Совместное использование AppWizard и ClassWizard
Ниже описана последовательность создания приложения ЕХ04А с помощью Арр Wizard и ClassWizard.
Создайте ЕХ04А, используя AppWizard. Сгенерируйте с помощью Арр Wizard проект SDI с именем ЕХ04Ав подкаталоге \vcpp32\exO4a. Параметры и имена классов по умолчанию показаны ниже.
Добавьте к CExO4AView переменные-члены mrectEUipsen m_«Color. Активизировав вкладку Class View в ora Workspace, щелкните правой кнопкой класс CExO4«View, выберите Add Member Variable и вставьте две следующие переменные-члены: private:
CRect m_rectEllipse;
int m_nColor;
Если хотите, мажете просто вручную ввести приведенный выше код в объявление класса в cxO4aVjew.h.
Добавьте с помощью ClassWizard обработчик сообщения в класс CEx04aView.
Выберите пункт ClassWizard из меню View в Visual C++ или нажмите правую кнопку мыши в окне исходного файла и выберите ClassWizard из контекстного меню. Когда на экране появится диалоговое окно ClassWizard (см. рис. на стр. 42), убедитесь, что выбран класс CEx04aView,как показано на приведенном рисунке. Теперь щелкните ExO4<*ViewB начале списка Object IDs и дважды щелкните в списке Messages сообщение WM_LBUTTONDOWN. В списке Member Functions должно появиться имя функции OnLButtonDown, а название сообщения WM_LBUTTONDOWN в списке Messages должно выводиться полужирным.
ПРИМЕЧАНИЕ Вместо ClassWizard можно воспользоваться панелью инструментов Visual С++ WizardBar (показана на рисунке 1-2 в главе 1).■-
Отредактируйте код функции OnLButtonDown в файле ex04aView.cpp. Щелкните кнопку Edit Code. ClassWizard откроет окно для редактирования ex04aView.cpp и установит курсор на вновь сгенерированную функцию OnLButtonDown. Код функции следует заменить на кед, набранный ниже полужирным (введите его вручную):'
void CEx04aView: :OnLButtonDown(UINT nFlags, CPoint point)■
if (m rectEllipse.PtlnRect(point)) {
if (m nColor == GRAY_BRUSH) {:
m_nColcr = №ITE_BRUSH;
else {
m_nColor = GRAY„BRUSH;
invalidateRect(m rectEllipse);
>
Отредактируйте конструктор и функцию OttiDraw в файле cx04aView.cpp. Вместо сгенерированного кода Вам необходимо вручную ввести код, набранный полужирным:
CEx04aView: :CEx04aView() : m rectElllpse(O, 0, 200, 200)
1 m nColor = GRAY BRUSH;
}
void CEx04aView: :OrDraw(CDC* pDC)'''
{' .■■
pDC->SelectStockObject(m_nColor);.
pDC->Ellipse(e rectEllipse);. .
}•
Соберите и запустите программу. Выберите из меню Build пункт Build Ex04a.exe или щелкните изображенную ниже кнопку на панели инструментов Build.
Затем выберите из меню Build пункт Execute Ех04а.ехе. Полученная программа в ответ на щелчок мыши изменяет цвет круга в окне представления. (Не нажимайте кнопку мыши два раза подряд слишком быстро — Windows интерпретирует эго как один двойной щелчок, а не как два одиночных.)
Режимы преобразования координат
До настоящего момента мы задавали координаты для рисования в пикселах экрана или в так называемых координатах устройства (device coordinates). ЕХ04А использует в качестве координат пикселы, так как в контексте устройства установленрежам преобразования координат (mapping mode) по умолчанию - Л&И_7ЕАТ.Оператор pDC->Rectangle(CRect(O, 0, 200, 200));
рисует квадрат 200г200 пикселов, левый верхний угол которого совпадает с левым верхним углом клиентской области окна. (Ось у направлена вниз.) На дисплее с высоким разрешением 1024г7б8пикселов такой квадрат будет выглядеть меньше, чем на стандартном мониторе VGA с разрешением 640Г480, а при распечатке на лазерном принтере с разрешением 600 dpi он будет и вовсе крошечным. [Убедитесь в этом сами, используя для программы ЕХ04А функцию Print Preview (предварительный просмотр перед печатью)].
Как же поступить, если размер квадрата должен быть 4г 4 см независимо от устройства отображения? Windows предоставляет целый ряд других режимов преобразования координат (или систем координат), которые можно выбрать дня контекста устройства. Координаты в текущем режиме преобразования называются логическими координатами (logical coordinates). Если, например, выбран режим преобразования MM_HIMETRICyo логической единицей будет не пиксел, а 0,01 мм. В режиме MM_HlMETRI6bcby направлена в противоположную сторону по сравнению с режимом Ш1_ТЕХТ: при перемещении по окну вниз значения у уменьшаются. Таким образам, в логических координатах квадрат4г4 см можно нарисовать так: pDC->flectangle(№ect(O, 0, 4000, -4000));
Выглядит просто, правда? В действительности это не так, потому что работать только в логических координатах нельзя. Программа постоянно переключается между координатами устройства и логическими координатами, и Вы должны знать, когда необходимо выполнить соответствующее преобразование. Далее мы рассмотрим несколько правил, которые могут облегчить вашу «программистскую» долю. Прежде всего, познакомимся с тем, какие же режимы преобразования координат предоставляет Windows. .
Режим преобразования ММ_ТЕХТ'
На первый взгляд, ММТЕХТ — это вовсе и не режим преобразования координат, а другое название системы координат устройства. Почти верно. В режиме Л£М_72Х7координаты со-
ответствуют пикселам, значения по осях возрастают при движении вправо, значения по оси у возрастают при движении вниз, но можно переместить начало координат с помощью функций SetViewportOrgn SetWindowOrgiaiacca CDC. Ниже приведен пример программы, устанавливающей начало логических координат на точку (100, 100) и рисующей затем квадрат с размерами 200г200 пикселов со смещением (100, 100). (Результат приведен на рис, 4-2.) Логическая точка (100, 100) отображается на точку (0, 0) в координатах устройства. Подобное преобразование используется окном с прокруткой.
void CMyView::OnDraw(CDC* pDC)'
pDC->SetMapNode(MM_TEXT);..,■
pDC->SetWlndo*'Org(CPoint(lOO, 100));..
pDC->Rectangle(CRect(100, 100, 300, 300));
Режимы преобразования координат с постоянным масштабом
Важная группа режимов преобразования координат Windows — режимы с постоянным масштабом. Как Вы уже видели выше, в режиме ММ_Н1МЕТК1<$начени.ях возрастают при перемещении вправо, а значения у убывают при перемещении вниз. Все режимы с постоянным масштабом следуют этому соглашению, и изменить его Вы не можете. Единственное различие между режимами с постоянным масштабом — фактический масштабный множитель, определяемый, как показано в следующей таблице. .
Режим преобразования координат
Логическая единица
MM_LOENGLISH MMJIIENGLISH MM_LOMETRIC MM_H1METRIC MMJTWIPS
0,01 дюйма
0,001 дюйма
0,1 мм
0,01 мм•
1/1440 дюйма
Последний режим преобразования — MMJTWIPS— чаще всего используется при работе с принтерами. Один twip равен 1/20 пункта. "(Пункт (point) — единица измерения размера шрифтов. В Windows пункт равен в точности 1/72 дюйма.) Если в режиме ЛШ_7ТИР5нуякн шрифт размером 12 пунктов, установите высоту символа равной 12г2О, т. е. 240 twips.
Режимы преобразования координат с переменным масштабом
Windows предоставляет два режима преобразования координат, позволяющих изменять не только начало координат, но и масштабный множитель, — MAOSOTROPKh. MM_ANISOTROP1C. При использовании этих режимов рисунок может изменять размеры, когда пользователь изменяет размеры окна. Кроме того, можно переворачивать изображение, изменяя знак масштабного множителя, а также задавать произвольные масштабные множители.
Б режиме MMJSOTROPIC всегда поддерживается коэффициент пропорциональности (aspect ratio), равный 1:1. Другими словами, при изменении масштабного множителя круг все равно остается кругом. В режиме Л<ГЛ/_АХ75ОП?ОР/Смасп1табныс множители по осям хну изменяются независимо друг от друга, Круги могут становиться эллипсами.
Ниже приведена функция OnDraw, рисующая эллипс, который точно соответствует размерам окна:
void CMyView: :Or.Oraw(CDC* pDC) {
CRect rectClient;
GctClientRect( rectClient);
pDC->SetMapMode(MM_ANISOTROPIC);
pDC->SetWi ndowExt( 1000, 1000);-
pOC->SetViewportExt(rectClient.right, -rectClient. bottom);
pDC->SetVieportOrg(rectClient.right / 2, rectClient.bottom / 2);
pDC->Elllpse(CRect(-500, -500, 500, 500));
}
Как она работает? Функции SetWindowEx№. SetViewportExteoBMecnanMH усилиями задают масштабные множители в зависимости от текущего размера клиентской области окна, который возвращает функция GetClientRect. В результате размер окна равен в точности 1 ОООт 1 000 логических единиц. Функция ЗегРгеггрог/Ог^устанавливает начало координат в центр окна, 'Следовательно, эллипс с радиусом 500 логических единиц и с центром в центре окна целиком заполняет окно (см. рис. 4-3).
Рис. 4-3. Центрированный эллипс в режиме преобразования координат MM_ANISOTROPIC
Далее приведены формулы преобразования логических координат в координаты устройства: масштаб л- = размер области вывода по х/размер окна по х
масштаб у = размер области вывода по у / размер окна по у
х устройства = х логическая * масштаб х + смещение начала координат пох у устройства = у логическая ” масштабу + смещение начала координат по у
Предположим, что окно занимает в ширину 448 пикселов (rectClient.rigbt). Правый край клиентской области эллипса находится в 500 логических единицах от начала координат. Масштаб по оси х равен 448/1000, а смещение начала координат по х равно 448/2 единиц устройства. Если воспользоваться приведенными выше формулами, то координата правого края клиентской области эллипса окажется равной 448 единицам устройства, т. е. координате правого края окна. Масштаб по х выражен как отношение размер области вывода/размер окна, так как координаты в Windows — это целые числа, а не величины с плавающей запятой. Сами по себе размеры области вывода или окна не имеют смысла.
Если в предыдущем примере заменить MM_A\’ISOTROPICka MMJSOTROPIC, то «эллипс» всегда будет крутом (см. рис. 4-4). Его радиус будет равен длине меньшего измерения окна.
Рио. 4-4. Центрированный эллипс, нарисованный в режиме преобразования координат MMJSOTROPIC
Преобразование координат
После того как Вы задали режим преобразования координат (и начало координат), большинству функций-членов CDC можно в качестве параметров передавать логические координаты. Однако если Вы, например, получили координаты курсора мнит из сообщения Windows (параметр point для OnLRutkmDowny то это будут координаты устройства. Многие другие функции MFC, в частности функции-члены класса CRect, правильно работают только с координатами устройства.
ПРИМЕЧАНИЕ Функции арифметики CPect используют соответствующие арифметические функции Win32 для ПЕСТ, которые подразумевают, что right больше, чем feft, и bottom больше, чем top. Например, для прямоугольника (О, О, 1000, -1000) в координатах MMJ-HMETRICзначение bottom меньше, чем fop, и этот прямоугольник не может обрабатываться такими функциями, как CRect: PtlnRect, если программа предварительно не вызовет CRect::NormalizeRect, которая изменит значения переменных-членов CRect на (0, -1000, 1000, 0).
Более того, вероятно, Вам понадобится третий набор координат, который мы назовем физическими координатами (physical coordinates). Зачем нужны еще одни координаты? Допустим, Вы используете режим MM_LOENGUSHysp логическая единица равна 0,01 дюйма, но
один дюйм на экране представляет собой фуг (12 дюймов) в реальном мире. Предположим далее, что пользователь работает с дюймами и десятичными дробями. Значение 26,75 дюймов преобразуется в 223 логические единицы, которые затем требуется преобразовать в координаты устройства. Во избежание ошибок округления физические координаты следует хранить либо как числа с плавающей запятой, либо как длинные целые с масштабным множителем.
Преобразования физических координат в логические остаются на вашей совести, однако GDI Windows позаботится о том, чтобы преобразовать логические координаты в координаты устройства. Преобразование между двумя системами выполняют функции LPtoDPuDPtoLP класса CDC, при этом предполагается, что режим преобразования координат и связанные с этим параметры контекста уже заданы. Ваша задача — решить, когда какую из систем координат использовать. Вог несколько правил:
считайте, что все параметры функций-членов CDC — это логические координаты;
считайте, что все параметры функций-членов GWnd — это координаты устройства;
проверяя, попадает ли указатель мьппи в определенную область, используйте координаты устройства. Задавайте области в координатах устройства. Такие функции, как CRect::PrJnRect. лучше всего работают, если используются координаты устройства;
значения, которые сохраняются на достаточно длительное время, должны использовать логические или физические координаты. Если Вы сохранили значение точки в координатах устройства и пользователь прокрутил изображение в окне, то сохраненное значение станет неверным.
Предположим, необходимо узнать, находится ли указатель мыши в момент нажатия левой кнопки в некотором прямоугольнике. Соответствующий код показан ниже.
// m_rect - это переменная-член типа CRect производного класса "вцц", // содержащая логические координаты MM_LOENGLT.SH
void CMyView::OnLButtonDown(UINT nFlags, CPoint point) <
CRect rect = m_rect;// rect - временная копия m_rect
CClientOC dc(this);// Получение контекста устройства для SetMapyode и LPtoDP
// - подробности в следующей главе
de. SetMapMode(KM_LOE№LISH):
dc.LPtoDP(rect);// Теперь rect содержит координаты устройства
if (rect.PtInRect(point)) {
TRACE("Курсор мыши находится внутри прямоугольника.");
}
Обратите внимание на использование макроса ТРАСЕ (описан в главе 3)-
ПРИМЕЧАНИЕ Как Вы скоро увидите, режим преобразования координат лучше устанавливать не в функции Onpraw, а в виртуальной функции OnPrepareDC класса CView.
Пример ЕХ04В
ЕХ04В представляет собой пример ЕХ04А, преобразованный так, чтобы можно было использовать координаты MMJH1METRICB проекте ЕХ04В на прилагаемом к книге компакт-диске используются новые имена классов и файлов, однако в приведенных далее инструкциях описано, как преобразовать код проекта ЕХ04А. Как и ЕХ04А, ЕХ04В выполняет проверку на попадание указателя мыши в заданную область и изменяет цвет эллипса только при щелчке внутри описанного прямоугольника.
С помощью ClassWizard переопределите виртуальную функцию OnPrepareDC. ClassWizard позволяет переопределять виртуальные функции некоторых базовых классов
' MFC, включая CView. Он генерирует прототип функции в заголовочном файле класса и шаблон тела функции в файле СРР. Выберите в списке Object IDs имя класса CExO4«Vieuna.
* затем дважды щелкните функцию OnPrepareDC в списке Messages. Отредактируйте функцию так, как показано ниже;
void CEc04aVjew: :OnPrepareDC(CDC* pDC, CPrintlnfo* plnfo)
pDC->SetMapNode(MM„HIMETRIC);
CView;;OnPrepareDC(pDC, plnfo):
}
Каркас приложения вызывает виртуальную функцию OnPrepareDC пред самым вызовом OnDraw.
Отредактируйте конструктор класса «вид». Необходимо изменить значения координат прямоугольника эллипса. Теперь размер прямоугольника равен 4г4 см, а не 200г200 пикселов. Обратите внимание: значениеу должно быть отрицательным, в противном случае эллипс будет выведен на «виртуальный экран», расположенный непосредственно над монитором! Измените значения, как показано ниже:
CEx04aView::CEx04aView() : m rectEllipse(O, 0, 4000, -4000)
I
m_nCoLor = GRAY.BRUSH;
1
Отредактируйте функцию OnLBwttonDoum. Теперь для проверки попадания в прямоугольник она должна преобразовать координаты эллипса в координаты устройства. Измените функцию, как показано ниже:
void CEx04aView: :OnLButtonDown(UINT nFlags, CPoint point)
{
CCIientDCdc(this);
OnPrepareDC(&dc);.
CRect rectDevice = m_rectEllipse;
de.LPtoDP(rectDevice);
if (rectDevice.PtInRect(point)) {
if (m nColor == GRAY BRUSH) {
m. nColor = WHITE BRUSH;
else {
m. nColor = GRAY.BRUSH; invalidateRect(rectDevice):
}
Соберите и запустите программу EXO4B. Результаты должны быть аналогичны результатам вывода ЕХ04А, за исключением того, что размер эллипса будет другим. В режиме предварительного просмотра печати Вы увидите, что эллипс значительно больше, чем в ЕХ04А.
Окно представления с прокруткой'
Как следует из отсутствиялакеек прокрутки (scroll bars) в примерах ЕХ04А и БХО4В, MFC- класс CView ~ базовый класс для CEx04bView— непосредственно не поддерживает прокрутку. Ее поддерживает другой класс, CScrollView. CScrolWww — наследник CView. Мы создадим
новую программу ЕХ04С, которая вместо CView будет использовать CScrollView. Код преобразования координат, написанный для ЕХ04В, подготовил все необходимое для реализации прокрутки. . , . .' i . . /
Класс CScrollView поддерживает прокрутку через линейки прокрутки, но не с клавиатуры. Добавить поддержку прокрутки с клавиатуры легко, и мы это сделаем. '
Окно—это больше, чем видно на экране
Если с помощью мыши уменьшить размеры обычного окна, то содержимое окна останется неподвижным относительно его левого верхнего уровня, а элементы, расположенные снизу и справа, исчезнут. После увеличения размеров окна эти элементы появятся вновь. Отсюда можно логично заключить, что окно больше, чем его область вывода (viewport), которую Вы вадите на экране. Однако область вывода не обязана быть жестко привязана клевому верхнему краю окна. Благодаря функциям CWhdScroliWindotcH. SetWindowOr^Jiacc CScrollView позволяет перемещать область вывода в любое место окна, включая области, расположенные выше и левее.
Линейки прокрутки
Microsoft Windows позволяет легко отобразить линейки прокрутки по краям окна, однако Windows сама по себе не пытается каким-либо образом подключить эти линейки прокрутки к окну. Эту задачу выполняет класс CScrollView. Функции-члены CScrollView обрабатывают сообщения WM_HSCROLL и WM_VSCROLL, которые линейки прокрутки посылают объекту «вид». Эти функции перемещают область вывода внутри окна и выполняют необходимые вспомогательные действия.
Различные способы прокрутки
Класс CScrollView поддерживает один определенный способ прокрутки, в котором используется одно большое окно и маленькая область вывода. Для каждого элемента определено положение внутри большого окна. Например, если необходимо отобразить на экране 10 000 адресных строк, то вместо окна длиной в 10 000 строк, вероятно, лучше иметь небольшое окно, поддерживающее алгоритм прокрутки, который выбирает для отображения столько строк, сколько можно отобразить в данный момент. В данном случае Вам необходимо создать свой класс «вид» с прокруткой, производный от CView.
ПРИМЕЧАНИЕ Microsoft Windows NT использует для представления логических координат 32-разряд- ные числа, так что пространство логических координат практически не ограничено. Но Windows 95 по- прежнему использует некоторые 16-разрядные компоненты и потому представляет логические координаты с помощью 16-разрядных чисел, что ограничивает диапазон их значений — от -32.768 до 32.767. Линейки прокрутки в обоих ОС посылают сообщения с 16-разрядными значениями. Учитывая все это, Вы, вероятно, захотите писать свои программы для наименьшего общего знаменателя, каковым является Windows 95.
Функция OninitiaiUpdate
Дополнительную информацию об этой функции Вы получите при изучении архитектуры «документ-вид», которое мы начнем с главы 16. Здесь виртуальная функция OninitiaiUpdate важна потому, что к ней первой обращается каркас приложения после того, как полностью создано окно представления, но перед вызовом On Draw, так что именно в OninitiaiUpdate следует задать логический размер и режима преобразования координат для вывода с прокруткой. Эти параметры устанавливаются с помощью функции CScroHView.:Set$croUSizes.
Г^эием ввода с клавиатуры
Ввод с клавиатуры — двухэтапный процесс. Windows посылает окну сообщения WMKEY- DOWN и WM_KEYUP с кодами виртуальных клавиш (virtual key codes), но прежде, чем эти сообщения доходят до окна, они транслируются. Если введ ен символ ANSI (в результате чего генерируется сообщение WMKEYDOWN), то функция трансляции проверяет состояние регистра клавиатуры и посылает сообщение WM_CHAR с соответствующим кедом символа — либо верхнего, либо нижнего регистра. Клавиши перемещения курсора и функциональные клавиши не имеют соответствующих кодов символов, поэтому для них трансляция не требуется. Окно получаеттолько сообщения WMKEYDOWN и WMKEYUP,
Чтобы создать в своем классе «вид» обработчики этих сообщений, Вы можете воспользоваться ClassWizard. Если Вы собираетесь принимать алфавитно-цифровые клавиши, обрабатывайте WM_CHAR; если Вам нужно обрабатывать нажатия и других клавиш — обрабатывайте WM_KEYDOWN. Библиотека МЕС аккуратно предоставляет в качестве параметра функции- обработчика код символа или кед виртуальной клавиши.
Пример ЕХ04С
Назначение ЕХ04С — сделать размер логического окна равным 20 см в ширину и 30 см в высоту. Программа рисует тот же самый эллипс, что и в примере ЕХ04В. Можно отредактировать исходные файлы ЕХ04В и заменить базовый класс CViewna CScroHView,H.o проще начать снова с AppWizard. AppWizard сгенерирует для Вас функцию OnInitiaHJpdate.№nKe приведены этапы создания программы.
С помощью AppWizard создайте ЕХ04С. Используя AppWizard, создайте проект ЕХ04С в подкаталоге \vcpp32\ex04c. На шаге б AppWizard установите класс CScroliVieu^ качестве базового для класса CExO4View,как показано ниже.
Добавьте в ех04с¥1е«.Ьпервменяые-членыт_гесГ£Нфяеи mnColor. Вставьте следующий код через контекстное меню для класса CEx04cVieu® окне Workspace или введите текст вручную в объявлении класса CEx04cVtew-.
private:
CRect »_rectEllipse;
int m_nColor;
Это те же самые переменные-члены, которые мы добавляли в проектах ЕХ04А и ЕХ04В.
Измените сгенерированную AppWizard функцию д^я<ИяЛф4ате.Отредактируй- те OnlnitialUpdate в файле ex04cView,cpp, как показано ниже:
void CEx04cView: :OnInitlalllpdate()
{•
CScrollView::OnInitialUpdate();
CSize sizeTotal(20000, 30000); H 20 m 30 cm
CSize sizePage(sizeTotal.ex / 2, sizeTotal.cy / 2);
CSize sizeLine(sizeTotal.cx / 50, sizeTotal.cy / 50);
SetScrcllSizea(MM HIMETRIC, sizeTotal, sizePage, sizeLlne);
С помощью Classwizard добавьте обработчик сообщения WM_KEYdOWN. ClassWizard сгенерирует функцию-член QnKeyDoum, а также соответствующий прототип и запись в таблице сообщений. Отредактируйте код следующим образом:
void CEx04cView: :OnKeyObwn(UINT nChar, UINT nRepCnt, UINT nFlags)
{'.
switch (nChar) {
case VICHOHE:
OnVScrolKSBTOP, 0, NULL);
OnHScroll(SB_LEFT, 0, NULL);
break;
case VK_ENO:
OnVScroll(SB_BOTTOM, 0, NULL);
OnHScroll(SB_RIGHT, 0, NULL);
break;
case VK_UP:
OnVScroll(SB_LINEUP, 0, NULL);
break;
case VK DOWN:
OnVScroll(SB_LINEOOWN, 0, NULL);'
break;
case VK_ PRIOR;
OnVScroll(SB_PAfiEUP, 0, NULL);
break;
case VK NEXT:
OnVScroll(SB_PAGEDOWN, 0, NULL);
break;
case VK.LEFT:
OnHScroll(SB_LINELEFT, 0, NULL);
break;
case VK.RIGHT:
OnHScroll(SB_LINEfiI9HT, 0, NULL);
break;
default:
break;
Отредактируйте конструктор и функцию OnDraw. Измените в файле ex04cView.cpp конструктор и функцию OnDraw, сгенерированные AppWizard, следующим образом: CEx04cView::CEx04cView() : m_rectEllipse(O, 0, 4000, -4000)
m_nCclor = GRAY BRUSH;
void CEx04cView: :OnDraw(CDC» pDC)
pDC->SelectStockObject(m_nColoг);
pDC->Ellipse(m rectElllpse);
}'
Эти функции идентичны аналогичным функциям из двух предыдущих примеров.
Создайте обработчик сообщения WM LBUTTONDOWN. Измените сгенерированный Class Wizard код следующим образом:
void CEx04cView::OnLButtonDown(UINT nFlans, CPoint point)
{'
■ CClientDC de (this);
OnPrepareDC(&dc);
CRect rectDevice = ra_rectEllipse;
de.LPtoDP(rectDevlce);
if (rectDevice.PtlnRect(point)) {
if (m nColor== 6RAY.BRUSH) { m nColor = WHITE BRUSH;
}
m_nColor = GRAY„BRUSH: }
InvalidateRect( rectDevice);
>
Функция ид ентична обработчику OnLButtonDown из проекта ЕХ04В. Как и в предыдущем случае, она вызывает OnPrepareDC, но здесь есть отличие. В классе CEx04cViewHor переопределенной функции OnPrepareDC, поэтому здесь вызывается CScmllVtew::OnPrepareDC. В соответствии с первым параметром SetScrollSizessTa функция устанавливает режим преобразования координат, а также задает начало координат окна в соответствии с текущей позицией прокрутки. Преобразование координат необходимо для коррекции смещения начала координат, даже если Вы используете режим ММ_ТЕХТ.
Соберите и запустите программу ЕХ04С. Убедитесь, что щелчок мышью работает, даже когда в результате прокрутки круг частично выходит за пределы окна. Проверьте также прокрутку с помощью клавиатуры. Результаты должны выглядеть так, как показано на рисунке выше.-
Другие сообщения Windows
Библиотека MFC непосредственно поддерживает сотни функций обработки сообщений Windows. Кроме того, Вы можете определять свои собственные сообщения. В следующих главах Вы найдете множество примеров обработки сообщений, включая сообщения от меню, дочерних окон-элементов управления и так далее. Пока же стоит обратить особое внимание на пять сообщений Windows: WM_CREATE, WM_CLOSE, WM_QUERYENDSESSION, WM_DESTROY и WM_NCDESTROY. ~
Сообщение WM_CREATE
Это первое сообщение, которое Windows посылает окну. Это происходит, когда каркас приложения вызывает функцию Create окна, так что к этому моменту создание окна еще не завершено и окно невидимо. Следовательно, ваш обработчик OnCreatene может вызывать функции Windows, которые требуют уже готового окна2. Эти функции можно вызывать в переопределенной функции OnlnitialUpdate. однако следует иметь в виду, что в приложениях SDI последняя за время существования окна представления может вызываться неоднократно.
Сообщение WM_CLOSE
Windows посылает сообщение WM_CLOSE, когда пользователь закрывает окно с помощью системного меню или котда закрывается родительское окно. Реализовав обработчик OnCIose в своем производном классе, Вы можете управлять процессом закрытия окна. Например, если нужно спросить у пользователя, следует ли сохранить изменения в файле, делайте это в OnCIose, Только убедившись в том, что окно можно закрывать безопасно, вызывайте функцию OnCIose базового класса, которая продолжит процесс закрытия. Объект «вид» и соответствующее ему окно в данный момент по-прежнему активны.
СОВЕТ При использовании всех средств каркаса приложения Вы, вероятно, не станете применять OnCIose. Вместо этого можно переопределить виртуальную функцию CDocument::SaveModih‘edKaK часть высокоструктурированной процедуры завершения программы, реализованной каркасом приложения.
Сообщение WM_QUERYENDSESSION
Сообщение WM_QUERYENDSESSION посылается всем исполняющимся приложениям, когда пользователь завершает работу с Windows. Сообщение обрабатывается функцией таблицы сообщений OnQuetyEndSessfon/Еяли Вы пишете обработчик для WM_CLOSE, напишите обработчик и для WM^QUERYENDSESSION.
Сообщение WM_DESTROY
Это сообщение посылается Windows после сообщения WM_CLOSE, и обрабатывается оно функцией таблицы сообщений OnDestroy. Получив это сообщение, следует считать, что окно представления уже невидимо, но все еще активно, как и его дочерние окна. Обработчик этого сообщения используется для выполнения очистки, которая требует существования окна Windows. Обязательно вызовите функцию OnDestroy базового класса. Функция OnDestroy в классе «вид» не может отменить процесс уничтожения окна. Для этого служит OnCIose.
Это неверно. Первым сообщением будет либо WM_NCCREATE, либо WM_GETMINMAXINFO. -• Прим, перев.'
Сообщение WM_NCDESTROY .
Это последнее сообщение, посылаемое Windows при уничтожении окна. Все дочерние окна уже уничтожены. В OnNcDestroyВы можете выполнить финальную обработку, которая нс требует наличия активного окна. Обязательно вызовите OnNcDestroy базового класса.
СОВЕТ Не пытайтесь уничтожать в OnNcDestroy динамически созданный объект-окно. Это действие выполняет специальная виртуальная функция PostNcDestroy класса CWnd, которая вызывается из OnNcDestroy базового класса. Рекомендации, когда следует удалять объект-окно, приведены в MFC Technical Note #17.
ГЛАВА 5
Интерфейс графического устройства, цвет и шрифт
Ранее мы уже встречались с некоторыми элементами интерфейса графического устройства (GDI). Всякий раз, когда программа выводит что-нибудь на дисплей или на принтер, она обязана воспользоваться GDI-функциями. Эти функции позволяют рисовать точки, линии, прямоугольники, многоугольники, эллипсы, растровые изображения и вводить текст. Круги и квадраты Вы смажете рисовать почти сразу, однако программирование вывода текста - более сложная задача. Из этой главы Вы узнаете, как эффективно использовать GDI в среде Microsoft Visual C++, работать со шрифтами ни дисплее и на принтере. Однако подробное обсуждение того, как каркас приложения управляет печатью, состоится в главе 19-
Классы «контекст устройства»
В главах 3 и 4 функции-члену класса «вид» OnDraw передавался указатель на объект «контекст устройства». OnDraw выбирала кисть и затем рисовала эллипс. Контекст устройства (device ■context) Microsoft Windows—ключевой элементов!, служащий для представления физического устройства. С каждым объектом «контекст устройства* C++ связан контекст устройства Windows, идентифицируемый 32-разрадным описателем типа HDG Библиотека MFC версии 6.0 предоставляет несколько классов контекста устройства. Базовый класс CDC содержит все необходимые для рисования функции-члены, включая несколько виртуальных. Все производные классы, за исключением CMetaFileDC, отличаются только конструкторами и деструкторами. Если Вы (или каркас приложения) создали объект производного класса контекста устройства, то указатель на CDC жажно затем передать функции, например, OnDraw. Для дисплея обычно используют производные классы CClientDCn CWindowDC.^a других устройств, таких, как принтеры или буферы в памяти, используют объекты базового класса CDC. «Виртуальность» класса CDC — важная особенность каркаса приложения. В главе 19 Вы увидите, насколько легко написать код, работающий и с дисплеем, и с принтером. Оператор в OnDraw, например,
pDC->TextOut(O. 0, "Hello");
посылает текст на дисплей, принтер или в окно предварительного просмотра печати, в зависимости от класса объекта, на который ссылается параметр pDC функции GView::OnDraw. Каркас приложения связывает описатели контекста устройства с объектами, представляющими контексты устройств дисплея и принтера. Чтобы связать описатель контекста с объекта-
ми, представляющими контексты других устройств (таких, как буфер памяти, с которым познакомитесь в главе 11), программист должен после создания объекта вызвать специальную функцию класса.
Классы контекста дисплея CCUentDCw CWindowDC
Как Вы помните, в клиентскую область окна не входят рамка, заголовок и меню Если Вы создадите объект CCIientDC, то получите контекст устройства, представляющий только эту область, — рисовать за ее пределами невозможно. Точка (0,0) обычно связана с верхним левым углом клиентской области. Как Вы увидите далее, объект CView соответствует дочернему окну, содержащемуся внутри отдельного окна-рамки, зачастую вместе с панелью инструментов, панелью состояния и линейками прокрутки. Все эти окна не входят в клиентскую область окна представления. Если, например, вверху окна имеется пристыкованная панель инструментов, то (0,0) соответствует точке непосредственно под левым краем панели. В случае, когда Вы создаете объект CWindowDC, точка (0,0) соответствует левому верхнему краю неклиенгс- кой области окна. С помощью этого полнооконного контекста устройства Вы можете рисовать по рамке окна, в области заголовка окна и т. д. Не забывайте, что у окна представления нет неклиенгской области, поэтому CWindowDCSarae подходит для окон-рамок, а не для окон представлений. .
Создание и уничтожение CDC-объектов
Важно своевременно уничтожать созданные Вами объекты CDC, когда работа с ними закончена. Microsoft Windows ограничивает число доступных контекстов устройства, и если не освободить контекст устройства Windows, небольшой участок памяти будет потерян до самого завершения программы. Чаще всего объект «контекст устройства* создается в обработчике сообщения, например, в QnlButtonDown. Проще всего гарантировать уничтожение объекта «контекст устройства» (и освобождение соответствующего контекста устройства Windows), создавая объект на стеке следующим образом:
void CMyView: :OnLButtonDown(UINT nFlags, CPoint point).
CRect rect;
CCIientDC dc(this); // создание de на стеке dc.GetClipBox(rect); // получение ограничивающего прямоугольника } // de автоматически освобождается
Обратите внимание: конструктор объекта CCIientDC принимает в качестве параметра указатель на окно. Деструктор CCIientDC вызывается при возврате управления из функции. Вы можете также получить указатель на контекст устройства с помощью функции CWndnGetDC, как показано ниже. Не забывайте вызывать ReieaseDCwva освобождения контекста устройства: void CMyView. :OnLButtonOown(UINT nFlags, CPoint point)
< CRect rect;
CDC* pDC = GetDCO; // указатель на внутренний объект CDC dc.GetClipBox(rect);Ц получение ограничивавшего прямоугольника
ReleaseDC(pDC);// Не забывайте
ВНИМАНИЕ Нельзя удалять CDC-объект, указатель на который передается функции OnDraw. Удалением этого объекта управляет каркас приложения.
Состояние контекста устройства '■■■
Вы уже знаете, что контекст устройства необходим для рисования. Когда Вы используете CDC- объект для рисования, например, эллипса, то полученное на экране (или на принтере) изображение зависит от текущего *состояния» контекста устройства. Состояние контекста вклю • чает в себя:■
связанные с контекстом объекты для рисования, такие, как перья, кисти и шрифты;
режим преобразования координат, определяющий масштаб элементов при их рисовании, (Мы уже экспериментировали с режимами преобразования координат в главе 4.);
различные детали, например, параметры выравнивания текста и режим заполнениямно- гоугольников. '
Бы уже видели, что если выбрать, например, серую кисть перед рисованием эллипса, то внутренняя область эллипса закрашивается серым. Вновь созданный контекст устройства обладает некоторыми характеристиками по умолчанию, такими, как черное перо для границ фигур, Все остальные характеристики состояния назначаются при помощи функций-членов класса CDC. GDI-объекты выбирают в контекст устройства вызовем переопределенных функций SelectObject. В любой момент времени в контекст устройства можно выбрать только одно перо, одну кисть и один шрифт.
Класс CPalntDC'
Класс CPalntDC нужен, только если Вы переопределили функцию OnPaintдля своего окна представления. Реализация OnPaint по умолчанию вызывает OnDraw с надлежащим образом настроенным контекстам устройства, но иногда Вам может потребоваться код рисования, специфичный для дисплея. Особенность CPalntDC состоит в том, что его конструктор и деструктор выполняют действия, специфичные для дисплея1. Однако, получив указатель на CPaintDC, Вы можете использовать его совершенно так же, как и любой другой контекст устройства. Вог пример функции OnPaint, создающей объект CPalntDC:
void CMyView::OnPaint()■
{.
CPaintDC de(this);
OnPrepareOC(&dc); // объясняется позднее.
dc.TexT0ut(0, 0, "для дисплея, но не для принтера");■
0r.0raw(&dc);// действия общие для дисплея и принтера
1:'
ч?ч?едееде^$М^Е1Сф№8Е№^$М^Е1Сф№8Е№т.екя^дйаШйт.екя^дйаШйц|Дотт^автоматическиЧЙ® контекстустройствац|Дотт^автоматическиЧЙ® контекстустройства,ii<7$,ii<7$
ч?
ч?
еде
еде
^$М^Е1Сф№8Е№
^$М^Е1Сф№8Е№
т.екя^дйаШй
т.екя^дйаШй
ц|Дотт^автоматически
ЧЙ® контекстустройства
ц|Дотт^автоматически
ЧЙ® контекстустройства
,ii
<7$
,ii
<7$
Объекты GDI
Каждый тип объектов GDI Windows представлен отдельным классом MFC CGdiObject— абстрактный базовый класс для классов объектов GDL Объект GDI представляется C++-объектом некоторого класса, производного or CGdiObject. Вот эти классы:
CBitmap — Растровое изображение (bitmap) — массив битов, в котором каждой точке дисплея соответствует один или несколько битов. Растровые изображения можно использовать для хранения картинок, а также для создания кистей.'
Точнее, действия, специфичные для обработки события WM_PAINT. — Прим.перев.
CBntsb— Кисть (brush) — точечный шаблон, используемый для закраски областей.
CFont — Шрифт (font) — полный набор символов некоторой гарнитуры и некоторого размера. Обычно шрифты хранятся на диске как ресурсы, причем некоторые шрифты специфичны для определенных устройств.
CPalette — Палитра (palette) — интерфейс преобразования цветов, позволяющий приложениям в полной мере использовать цветовые возможности устройства вывода, не мешая другим приложениям.
СРеп ~Пера (реп) — инструмент для рисования линий и границ фигур. Вы можете задать цвет и толщину пера, а также указать, будут ли линии сплошными, пунктирными или штриховыми.-
CRgn — Регион (region) — область, которая представляет собой многоугольник, эллипс или их комбинацию. Регионы можно использовать для закраски, обрезания выводимого изображения и проверки на попадание курсора мыши на определенные участки.
Создание и уничтожение GDI-объектов
Вы еще ни разу не создавали объекты класса CGdiObject — вместо этого Вы конструировали объекты производных классов. Конструкторы для некоторых классов, например, СРеп или CSrusb, позволяют указывать достаточно информации, чтобы создать объект за одну операцию. Создание других объектов, например, CFont или CRgn, требует второй операции. Объекты этих классов создаются конструктором по умолчанию, после чего вызывается сответствую- щая функция, например, СгеЖеРоШили CreatePolygonRgn.
Класс CGdiObject содержит виртуальный деструктор. Деструкторы производных классов удаляют GDI-объекты, связанные с соответствующими С++-объектами, Если Вы создали объект класса, производного от CGdiObject,то обязаны удалить его до завершения программы. Для удаления GDI-объекта его следует сначала отделить от контекста устройства. Пример мы рассмотрим в следующем разделе.
ПРИМЕЧАНИЕ Завершение программы без удаления GDI-объекта sWinl 6 несло серьезную опасность. Память GDI не освобождалась до момента перезапуска Windows. В Win32 память GDI принадлежит процессу и освобождается при завершении программы. Тем не менее, GDI-объект, который Вы забыли освободить, может награсно занимать значительней объем памяти.
Управление GDI-объектами
Итак, Вы знаете, что GDI-объекты нужно удалять, предварительно отсоединив от контекста устройства. Как же эго сделать? Функции семейства СОСл^е/ес/О^ес/Ьыбирают GDI - объект в контекст устройства и возвращают указатель на объект, который был выбран в контекст до этого (и теперь отсоединен). Но возникает проблема: нельзя отсоединить старый объект, не выбрав в контекст новый. Простое решение — «сохранить» первоначальный GDI-объект при выборе своего объекта в контекст и восстановить его по завершении работы. После этого Вы можете удалить свой GDI-объект. Пример приведен ниже:..
void CMyView: :OnDraw(CDC* pDC)'■.
{■ .
СРеп newPen(PS_OASHDOT, 2, (COlORREF) 0);11 черное перо шириной в 2 точки
СРеп. pOldPen = pDC->SelectObject(&newPen);
pDC->MoveTo(10, 10);
pDC->LineTo(110, 10);....
pDC->SelectObject(p01dPen);// newPen отсоединяется
, П newPen автоматически уничтожается при выходе
При уничтожении контекста устройства все его GDI-объекты отсоединяются. Таким образом, если известно, что контекст устройства буд ет уничтожен до того, как будут уничтожены выбранные в него объекты, отсоединять эти объекты не нужно. Например, если Вы объявили перо как переменную-член класса *вид» (и инициализировали его при инициализации объекта 4вид»), то не надо отсоединять перо внутри OnDraur. контекст устройства, временем жизни которого управляет обработчик OnPaint в базовом классе, будет уничтожен раньше.
Стандартные GDI-объекты
Windows предоставляет Вам ряд стандартных GDI-объектов (stock GDI objects). Так как эти объекты — часть Windows, заботиться об их удалении не нужно (Windows игнорирует запросы на удаление стандартных объектов). Функция библиотеки MFC CDC:;SelectStocteOifecl^- бирает стандартный объект в контекст устройства и возвращает указатель на выбранный ранее объект, который отсоединяется от контекста. Стандартные объекты удобны, когда Вам нужно отсоединить свой собственный нестандартный GDI-объект перед его удалением. Стандартный объект можно применять вместо «старого» объекта, который использовался в предыдущем примере1:
I void CMyView: :OnDraw(CDC« pDC) {
CPen newPen(PS_DASHDOT, 2, (COLORREF) 0); Ц черное перо шириной в 2 точки
CPen* pOldPen - pDC->SelectObject(inewPen);
pDC->MoveTo(lO, Ю);
pDC->LineTo(11G, 10);
pDC-XSelectStockObject(BLACK_PEN);// newPen отсоединяется
} // newPen автоматически уничтожается при выходе
Стандартные перья, кисти, шрифты и палитры перечислены в описании функции CDC::Se- lectStockObjectb руководстве Microsoft Foundation Class Reference.
Время жизни контекста устройства
В случае контекста устройства «дисплей» в начале каждой функции-обработчика сообщения Вы получаете «свежий» контекст, Набор выбранных объектов (а также режим преобразования координат и другие параметры контекста) теряется после завершения работы функции. Таким образом, контекст устройства необходимо всякий раз настраивать заново. Виртуальная функция-член ОпРгерагеОСкласса СУг^Уудобна для установки режима преобразования координат, но своими GDI-объектами Вы управляете сами.
Для других контекстов устройств, таких, как принтеры или буферы памяти, назначенные Вами параметры могут сохраняться дольию, С этими долгоживущими контекстами устройств возникают сложности из-за временной природы указателей на С++-объекты GDI, которые возвращаются функцией SelectObject. (Временный «объект» будет удален каркасом приложения во время обработки цикла простая приложения, некоторое время спустя после возврата управления обработчиком. Подробнее см. MFC Technical Note *3-) Вы не можете просто сохранить указатель в переменной-члене класса — вместо этого его необходимо преобразовать в описатель Windows (единственный постоянный идентификатор GDI-объекта) с помощью функции-члена GetSafeHandleUpiiMep приведен ниже:
// m_pRrintFont указывает на объект CFont, созданный в конструкторе CMyView
И m_MldForit - это переменная-член CMyView типа HF0NT, инициализированная нулем
void CMyView: :SwtichToCourier(CDC* pDC)
He восстановить предыдущий объект— плохая практика программирования. —Прим, персе.
m_pPrintFont->CreateFont(30. 10, О, О, 400, FALSE, FALSE,
О, ANSI CHARSET, OUT DEFAULT PRECIS, CLIP DEFAULT PRECIS, DEFAULT QUALITY, DEFAULT_PITCH I FF_MQDERN, "Courier New' );// True Type
CFont* pOldFolt = pOC->SelectObject(m_pPrintFont);
// m_hOldFont - это переменная-член типа HFONT, в которой хранится описатель r_h01dFont = (HFCNT) p01dFont->GetSafeHandle();
}
void CMyVlew: :S*4tchToOrlginalFont(CDC* pDC)
// FromHandle - статическая функция-член, возвращающая
Ц указатель на объект
if (m_h01dFont) {
pDC->SelectOlrect(CFont: :FromHandle(m hOldFont));
}
)
// m_pPrintFont удаляется в деструкторе CMyView
ПРИМЕЧАНИЕ Будьте осторожны при удалении объекта, указатель на который возвращает SelectObject. Если Вы создали этот объект сами, то имеете право удалить его. Если же указатель временный, что бывает с объектами, первоначально выбранными в контекст устройства, то С++-объект удалять нельзя.
Преобразование цветов в Windows
Windows GDI предоставляет аппаратно-независимый интерфейс работы с цветом. Программа задает «абсолютный» код цвета, и GDI отображает этот код на подходящий цвет или сочетание цветов дисплея компьютера. Большинство программистов, разрабатывающих приложения для Windows, стараются оптимизировать цвета своих программ для нескольких распространенных категорий вццеоплат.
Стандартные видеоплаты VGA
Стандартная видеоплата VGA (Video Graphics Array — графическая видеоматрица) использует 18-разрядные регистры цвета и таким образом поддерживает палитру из 262 144 цветов. Однако из-за ограниченного размера видеопамяти стандартная плата VGA использует 4-раз- рядные коды цвета и, следовательно, может отображать одновременно только 1 6 цветов. Поскольку Windows нужны фиксированные цвета для заголовков окон, рамок, линеек прокрутки и т. д., ваша программа может использовать только 16 «стандартных» чистых цветов. Простого способа использовать другие цвета, которые может отображать видеоплата, у Вас нет.
Каждый цвет в Windows представляется сочетанием значений «красный», «зеленый» и «синий». 16 стандартных «чистых» (не составных) прете® VGA представлены в таблице на стр. 62.
Работающие с цветом GDI-функции принимают в качестве параметров 32-разрядные значения COLORREF, содержащие 8-разрядные цветовые коды для красного, зеленого и синего цветов. Макрос Windows RGB преобразует 8-разрядные значения красного, зеленого и синего в параметр COLORREF. Приведенный ниже оператор при выполнении на компьютере со стандартной платой VGA создает кисть с составным цветом (dithered color), состоящим из смеси чистых цветов:
CBrush brush(RG9(128, 128, 192));
КрасныйЗеленыйСинийЦвет
0
0
0
черный
0
0
255
синий,
0
255
0
зеленый
0
255
255
бирюзовый-
255
Л
0
красный
255
0
255
малиновый
255
255
0
желтый
255
.255
255
белый
О
0
128
темно-синий
0
128
0
темио-зелеиый1
0
128
128
темно-бирюзовый
128
0
0
темио-красиый
128
■0
128
темно- малиновый
128
128
0
темио-жвлтый
128
128
И 28
темио-серый
192
192
' 192
светло-серый
Следующий оператор (в функции OnDrawiaiacca «вид») устанавливаеткрасныйцветфот текста:
pDC->SetBkColor(BGB(255,О,0));
Функции CDC SetBkCoiorK SetTextCoiome будут отображать составные цвета, в отличие от функций, ориентированных на рисование кистью. Если шаблон составного цвета слишком сложен, будет выбран наиболее близкий чистый цвет.
Видеоплаты, поддерживающие 256 цветов
Большинство видеоплат в состоянии поддерживать 8-разрядные цвета во всех разрешениях, т. е. могут отображать одновременно 256 цветов. Этот режим в настоящее время считается «наименьшим общим знаменателем»припрограммированиицветов.
Если Windows работает с видеоплатой, поддерживающей 256 цветов, то ваши программы ограничены 20 стандартными чистыми цветами, только если Вы не используете систему цветовой палитры Windows, поддерживаемую классом фя/е«ебиблиогеки MFC и Windows API. В последнем случае можно выбрать любые 256 из 16,7 миллиона цветов. Программирование цветовой палитры Windows рассмотрено в главе 11. А сейчас мы будем считать, что используется стандартное преобразование цветов.
При использовании 256-цветового драйвера дисплея SVGA в дополнение к 16 перечисленным выше цветам VGAb вашем распоряжении есть еще 4, что в сумме дает 20, Дополнительные четыре цвета приведены в следующей таблице.
Красный
Зеленый
Синий
Цвет
192
220
192
светло-зеленый (цвет долларовой купюры)
(66
202
240
небесно-голубой
255
251
240■
кремовый
160
160
164
серый средней интенсивности
Макрос RGB с SVGA работает практически так же, как и для стандартной VGA Если Вы задаете для кисти один из 20 стандартных цветов, то получаете чистый цвет; в противном ■случае — составной. Использование вместо RGB макроса В4Д£77Е/?ОВвсе1да дает не составной цвет, а ближайший чистый, определенный текущей палитрой.
Видеоплаты с 16-раэрядным цветом
Большинство современных видеоплат поддерживает разрешение 1024x768 пикселов. При этом разрешении 1 Мбайт видеопамяти позволяет представлять цвет 8 битами. Если на видеоплате установлено 2 Мбайт видеопамяти, то она может поддерживать 16-разрядный код цвета (по 5 битов на красный, зеленый и синий компоненты). Это означает, что одновременно можно отобразить 32 768 цветов. На первый взгляд неплохо, но это всего лишь 32 градации чистых красного, зеленого и синего цветов. Зачастую, если правильно выбрать палитру, картинка будет выглядеть лучше в режиме 8-разрядного представления цвета. Например, на изображении леса может быть до 236 оттенков зеленого. В режиме 16-разрядного представления цвета палитры не поддерживаются.
Видеоплаты с 24-разрядным цветом
Более совершенные модели видеоплат класса «high end», встречающиеся все чаще, поддерживают 24-разрядное представление цвета, что позволяет отражать 16,7 миллиона чистых цветов, причем, имея 24-разрядную видеоплату, Вы можете использовать все цвета непосредственно. Макрос RGB позволяет точнее задать цвет, который Вам нужен. Но для поддержки 24-разрядного цвета при разрешении 1024x768 пикселов потребуется 2,5 Мбайт видеопамяти.
Шрифты
Старые приложения для текстового режима могли выводить текст только унылым системным шрифтом. Windows предоставляет множество шрифтов переменного размера, не зависящих от устройств вывода. Эффективное использование шрифтов Windows может значительно улучшить внешний вид приложения без особых усилий со стороны программиста. Шрифты TrueType, впервые появившиеся в Windows 3.1, еще более эффективны, и с ними проще работать, чем с зависящими от устройства шрифтами, которые применялись ранее. Далее в этой главе будет представлено несколько примеров программ, использующих различные шрифты,.
Шрифты как GDI-объекты
Поддержка шрифтов встроена в GDI Windows. Эго означает, что шрифты — такие же объекты GDI, как другие. Выводимый текст можно масштабировать и обрезать, и шрифты можно выбирать в контекст устройства аналогично перу или кисти. Все правила GDI относительно отсоединения от контекста и удаления применимы и к шрифтам.
Выбор шрифта
Ранее выбрать шрифт в Windows было все равно что, подойдя к фруктовому ларьку, попросить красновато-желтый фрукт, с косточкой внутри, весом около 100 грамм. В результате можно получить персик, сливу или даже нектарин, весом явно не ровно 100 граммов. Уже получив фрукт, Вы могли и взвесить его, и определить его вид Теперь, с появлением шрифтов TrueType, можно указать «вид фрукта», но его вес по-прежнему нельзя задать точно.
Сегодня Вы можете указать один из двух типов шрифтов — не зависящие от устройства шрифты TrueType и зависящие от устройства шрифты, такие, как шрифт Windows для дисплея System и шрифт LinePrinter для принтеров LaserJet, — или же задать семейство шрифтов и размер, предоставив Windows самой выбрать шрифт. В последнем случае будет выбран шрифт TrueType. В библиотеке MFC есть диалоговое окно выбора шрифта, связанное с выбранным в настоящий момент принтером, поэтому «угадывать* шрифт для принтера практически не надо. Вы позволяете пользователю точно выбрать для принтера шрифт и его размер, и добиваетесь отображения на дисплее, максимально приближенного к этому выбору.
4 Зак. №787
Шрифты и вывод на печать
Для приложений, интенсивно работающих с текстам, размеры шрифта Вы, скорее всего, будете задавать в пунктах (1 пункт = 1/72 дюйма). Почему? . Большинство, если не все, внутренних шрифтов принтеров описано в терминах пунктов. Например, шрифт LaserJet LinePrinter поставляется с единственным размером 8,5 пунктов. Вы мажете использовать шрифты TrueType произвольного размера в пунктах. При использовании пунктов для преобразования координат Вам потребуется соответствующий режим — MM_TWIPS.B этом режиме размер 8,5 пунктов соответствует 170 (8,5x20) твипов — такой размер символа и следует задать.
Отображение шрифтов на дисплее
Если Вас не беспокоит тачное соответствие изображений на дисплее и на принтере, то Вы можете выбрать любой из масштабируемых шрифтов TrueType или же системные шрифты фиксированного размера (стандартные GDI-объекты). При работе со шрифтами TrueType режим преобразования координат не имеет особого значения — просто выберите нужную высоту шрифта и используйте ее. Заботиться о пунктах не нужно.
Подбор точного соответствия шрифтам принтера так, чтобы изображение на экране точно соответствовало изображению на бумаге, связан с некоторыми сложностями, однако шрифты TrueType облегчают эту задачу. Но даже если при печати Вы пользуетесь шрифтами TrueType, то добиться абсолютного соответствия изображений на дисплее и на бумаге Вам все равно не удастся. Почему? Символы, в конечном счете, изображаются с помощью пикселов (или точек), и длина строки символов равна сумме ширин отдельных символов в пикселах, возможно, с поправкой на кернинг. Ширина символа в пикселах зависит от шрифта, режима преобразования координат и разрешающей способности устройства вывода. Точное соответствие было бы возможно только в том случае, если и для принтера, и для дисплея использовался бы режим MMJTEXl^i пиксел или точка = 1 логическая единица). Если для вычисления мест переноса строки использовать функцию CDC::GelTexiExtent,места переноса на экране могут иногда отличаться от мест переноса на принтере.
ПРИМЕЧАНИЕ В режиме предварительного просмотра (Print Preview), который подробно будет рассмотрен в главе 19, переносы на новую строку происходят в тех же местах, что и на бумаге, однако качество отображения в окне предварительного просмотра при этом страдает.
Если Вы захотите отобразить текст на экране шрифтам, близким к внутреннему шрифту принтера, технология TrueType облегчит и эту задачу. Windows подставляет наиболее близкий шрифт TrueType. Для шрифта LinePrinter Windows очень точно подставляет свой шрифт CourierNew.
Логические и физические дюймы на дисплее ■ ■
Функция-член CDC <7ег£>ег^сеС’«/?5возвращает параметры дисплея, важные для программирования графики. Шесть приведенных ниже параметров предоставляют информацию о размере экрана (указаны значения для типичной видеоплаты, сконфигурированной в Windows NT 4.0 для разрешения 640x480 пикселов). ■ . ■
Номер параметра
ОписаниеЗначение
HORZSIZE VEET31ZE HORZRES
VERTRES 1DGPIXELSX LOGPIXELSY
физическая ширина в мм320
физическая высота в мм240
ширина ц пикселах640
высота в растровых линиях480
горизонтальное разрешение в точках на логический дюйм96
вертикальное разрешение и точках на логический дюйм96
Числа HORZSIZEh VERTSIZE представляют физические размеры дисплея. (Эти значения могут отличаться от действительных, так как Windows не знает, монитор какого размера подключен к видеоадаптеру.) Размер д исплея можно также вычислить, умножив соответственно HORZRESили VERTRESna LOGPIXELSX или LOGPIXELSY. Полученные таким образом размеры называются логическими размерами (logical size) дисплея. Используя представленные выше значения и зная, что в дюйме 25,4 мм, можно легко вычислить размеры экрана в разрешении 640x480 пикселов для Windows NT. Физический размер дисплея равен 12,60x9,45 дюйма, а логический — 6,67x5 дюймов. Таким образом, физический и логический размеры экрана могут различаться.
В Windows NT 4 0 HORZSIZE и VERTSIZE не зависят от разрешения дисплея, a LOGPIXELSX и ЬООР1ХЕЬ5¥т>сетда равны 96. Поэтому с изменением разрешения изменяется логический размер, но не физический. В Windows 95 физический и логический размеры равны, поэтому с изменением разрешения изменяются оба размера, (При разрешении 640x480 в Windows 95 HORZSIZE равно 169, a VERSIZE — 127.) .
При использовании режима преобразования координат с фиксированным масштабным множителем, будь тоММ_Н1МЕГЕКмли. iMM_TWIPSApaSasep дисплея использует для пересчета физический размер дисплея.
Таким образом, в Windows NT на мониторе меньшего размера текст также получается меньше, но это не то, чего бы мы хотели. Напротив, необходимо, чтобы размеры шрифта соответствовали логическому, а не физическому размеру дисплея.
Можно создать специальный режим преобразования координат — режим логических тейпов (logical twips), в котором одна логическая единица равна 1/1440 логического дюйма. Этот режим преобразования не зависит от ОС и разрешения монитора и используется такими программами, как Microsoft Word. Ниже приведен код, устанавливающий режим преобразования в логические твипы:
pDC->SetHapMade(MK_AMISOTROPlC); • pDC->SetWindowExt(144O, 1440);
pDC->SetVicwportExt(pOC->GetDeviceCaps(LOGPIXELSX),
- pDC->GetDeviceCaps(LOGP!XELSy)):
ПРИМЕЧАНИЕ Как размер шрифта, так и разрешающую способность дисплея можно настроить с помощью Панели управления (Control Panel) Windows. Если изменить размер шрифта дисплея со стандартных 100% на 200%, то HORZSIZE становится равным 160, VERTSIZE - 120, а число точек на дюйм — 192. В этом случае логический размер делится на два, и размер текста, выводимого в режиме отображения «логические твипы», увеличивается вдвое.
Вычисление высоты символа -
СОС-функция GetTextMetricsвозвращает 5 параметров высоты шрифта, из которых важны только три, показанные на рисунке 5-1- Параметр tmHeigbt задает полную высоту шрифта, включая выносные элементы для букв (g, j, р, ди у) и диакритические значки поверх заглавных букв. Параметр tmExtemalLeading задает расстояние между верхом диакритического значка и низом выносного элемента с предыдущей строки. Сумма tmHeigbt и tmExtemalLeading задает общую высоту символа. Значение tmExtemalLeading может равняться 0.
Вы мажете предположить, что tmHeigbt задает высоту шрифта в пунктах. Однако это не так. Здесь вступает в игру параметр rmZnter^ZiorfZttg., возвращаемый функцией GetTextMetrics,. Высота в пунктах соответствует разности между tmHeigbt и tmlnternalLeading. В режиме преобразования координат МИ_Л₽ХР5Выбранный в контекст шрифт в 12 пунктов может иметь значение tmHeigbt, равное 295 логическим единицам, a tmlnternalLeading — 55 логическим единицам. Тогда истинная высота шрифта, равная 240, соответствует высоте в пунктах 12.
Рис. 5-1. Параметры шрифта
Пример ЕХ05А
В этом примере для окна представления устанавливается режим преобразования координат «логические твипы». Программа выводит текстовую строку TrueType шрифтом Ariai высотой 10 пунктов, Далее приведем этапы создания приложения.
Сгенерируйте проект ЕХ05А с помощью AppWizard. Выберите New из меню Fib, затем на вкладке Project выберите MFC AppWizard (ехе). Выберите параметр Single Document и отключите параметр Printing and Print Preview. Остальные параметры оставьте без изменения. Параметры и названия классов по умолчанию показаны на рисунке.
Используя ClassWizard, переопределите функцию ОиРге/мгеОСв классе СЕхО5 «View, Измените код в файле cxOSaVlew.cpp следующим образом:
void CExOSaView: :OnPrepareDC(CDC* pDC, CPrirrtlnfo »plnfo) {
pDC->SetMapMode(MH_ANISOTROPIC);
pDC->SetWi ndowExt( 1440, 1440);
pOC->SetViewportExt(pDC->GetDeviceCaps(LOGPIXELSX),
- pDC-> GetDeviceCaps(LOGPIXELSY));
Добавьте закрытую вспомогательную функцию SbowFontK классу «вид*. Создайте показанный ниже прототип в ex05aView.h:
private:
void Sho'*'Font(CDC» pOC, intb nPos, int nPoints);
Затем добавьте саму функцию в файл ex05aView.cpp:
void CExOoaView::ShowFont(CDC* pOC, int& nPos, int nPoints)
(
TEXTMETRIC tm;
CFont fontText;
CString strText;
CSize sizeText;
fontText.CreateFont(-nPoints * 20, 0, 0, 0, 400, FALSE, FALSE, 0,
ANSI CHARSET. OUT DEFAULT PRECIS, CLIP DEFAULT PRECIS, DEFAULT-QUALITY, DEFAULT-PITCH | FF.SWISS, "Arial");
CFont* pOldFont = (CFont*) pOC->SelectObject(&fontText);
pDC->GetTextMetrics($tm);
fRACE( "points = %d. tmheight = %d, tmlntemalLeading = %d,"
" tmExternalLeading - %d\n", nPoints, tm.tmHeight, tm.tmlnternalLeading, tm.tmExternalLeading);-
strText. Format( "This is %d-polnt Arial", nPoints);
sizeText = pOC->GetTextExtent(strText);
TRACE(“string width = %d, string height = %d\n", sizeText.ex, sizeText. cy);
pDC->TextOut(O, nPos, strText):
p0C->Select0bject(D01dFont);
nPos ~= tm.tmHeight + tm.tmExternalLeading: }
Отредактируйте функцию OnDraw в ex05aView.cpp. AppWizard всегда создает для класса «вид* шаблон функции OnDraw. Найдите эту функцию и замените ее текст на следующий:
void CEx05aView::OnDraw(CDC* pDC) {
mt ntosition = 0;•
for (int i = 6; i <= 24; i += 2) { ShowFont(pDC, nPosition, i);
}
TRACE(“LOGPIXELSX = W, LOGPIXELSY = %d\n", pDC->GetDeviceCaps(LOGPIXELSX)1 pDC->GetDeviceCaps(LOGPIXELSY));
TRACE("HORZSI2E = «d, VERTSIZE = *d\n",-
pDC->GetOeviceCaps(HORZSIZE), pDC->GetDeviceCaps(VERTSIZE));
TRACEC'HORZRES = %d, VERTRES = W-
pDC->GetDeviceCaps(HORZRES), pDC->GetDeviceCaps(VERTRES));
Соберите и запустите программу ЕХ05А. Чтобы было возможным наблюдать за выводом, генерируемым операторами TRACE, программу нужно запускать «из-под отладчика*. Выберите пункт Go из меню Build в Visual C++ или щелкните на панели инструментов показанную ниже кнопку.
Результаты работы программы (если используется стандартная видеоплата VGA) будут примерно такими:
-t IlnWvJ - ctOT.t
№Ыз9фоНД|Ы
This is 10-point Aria!
This is 12-pointArial
This is 14-point Arial
This is 16-point Arial
This is 18-point Arial
This is 20-point Arial
This is 22-point ArialI
This is 24-point Arial|
Обратите внимание: размеры выведенных строк не совсем точно соответствуют размерам в пунктах. Несоответствие возникает при преобразовании логических координат в пикселы, выполняемом подсистемой шрифтов (font engine). Отладочная печать, фрагмент которой Вы видите ниже, показывает параметры шрифтов. (Точные цифры зависят от конкретного драйвера дисплея и видеодрайвера.)
points = 6, tmHeight = 150, tmlnternalbeading = 30, tmExternalbeading = 4
string width = 990, string height = 150
points = 8, tmHeight = 210, tmlnternalbeading = 45, tmExternalbeading = 5
string width = 1380, string height = 210
points = 10, tmHeight = 240, tmlnternalbeading = 45, tmExternalbeading = 6
string width = 1770, string height = 240
points = 12, tmHeight = 270, tmlnternalbeading = 30, tmExternalbeading = 8
string width = 2130. string height - 270
Элементы программы EX05A
Далее обсуждаются важнейшие элементы программы ЕХО5А
Установка режимапреобразованиякоординатв функции OnPrepareDC
Функция OnPrepareDC вызывается каркасом приложения перед вызовом функции OnDraw, и потому в ней логичнее всего готовить контекст устройства для OnDraw. Если установка режима преобразования координат требуется другим обработчикам сообщений, эти обработчики могут вызывать OnPrepareDC.
Закрытая функция-член ShowFont
$ЬошРоп£содержит код, исполняемый в цикле 10 раз. В программе на С эта функция была бы глобальной, но в C++ лучше сделать ее закрытым членам класса. Иногда такие функции называются вспомогательными (helper function).
Функция создает шрифт, выбирает его в контекст устройства, выводит строку в окно и затем удаляет шрифт. В отладочном варианте программы она также выводит информацию о параметрах шрифта, включая истинную длину строки.
Вызов CFont;;CreateFont . .
У этой функции очень много параметров, но наиболее важны первые два — высота и ширина шрифта. Ширина, равная 0, означает, что коэффициент пропорциональности (aspect ratio) шрифта будет равен значению, установленному разработчиком шрифта в качестве значения по умолчанию. Если указать ненулевое значение, то, как Вы увидите в следующем примере, коэффициент пропорциональности изменится. .
СОВЕТ Если необходимо получить шрифт точно заданного размера в пунктах,1*задайте отрицательное значение высоты (первый параметр) в вызове функции CreateFont. Нагример, если для вывода на принтер используется режим преобразования координат MMJTWIPS, значение высоты -240 гарантирует получение высоты шрифта в точности 12 пунктов и tmHeight- tmlntornalleading = 240. Использова- 1ние значения +240 даст меньший размер шрифта с tmHeight = 240. ■ :
Последний параметр CreateFont задает имя шрифта — в данном случае Arial (шрифт TrueType). Если бы этот параметр был равен NULL, то указание FF_SW1SS (чтоозначает пропорциональный шрифт без засечек) заставило бы Windows выбрать наиболее подходящий шрифт, которым, в зависимости от конкретного размера, мог бы оказаться System или Arial. Если имя шрифта задано, то этот параметр имеет преимущество перед другими. Например, если вместе с Arial указать FF_ROjMAN(4io означает пропорциональный шрифт с засечками), система все равно выберет Arial. '
Пример ЕХ05В
Эта программа похожа на ЕХ05А, но использует несколько шрифтов. В ней применяется режим преобразования координат MM_ANISOTROPIti: масштабом, изменяющимся в зависимости от текущих размеров окна. Размеры символов изменяются вместе с размером окна. Программа демонстрирует некоторые шрифты TrueType в сравнении со шрифтами старого типа. Этапы создания программы. '
Сгенерируйте проект ЕХ05В с помощью AppWizard. Параметры и имена классов по умолчанию показаны на рисунке.
Используя ClassWizard, переопределите функцию OnPrepareDCs классе СЕяОЗЬ-
View.Измените код в файле ex05bView.cpp следующим образом:■■
void CExOSbView: :OnPrepareDC(CDC* pDC, CPrintlnfo* plnfo)
CRect clientRect;.
GetClientRect(clientRect);
pDC->SetM^Mode «.ANISOTROPIC); И +y = вниз
pDC->SetWlndowExt(400, 450);
pDC->SetViewportExt(clientRect.right, clientRect.bottom);
pDC->SetViewpo rtO rg (0, 0);
Добавьте к классу «вид» закрытую вспомогательную функцию TraceMetrics.JXo бавьте в ex05bView.h следующий прототип:
private:
void Trac^fetrics (CDC* pDC);
Затем добавьте саму функцию в ex05bView.q)p:
void CExOSbView: :TraceMetrlcs(CDC* pDC).
TEXTMETRIC tm;
char szFaceName[100j;
pDC->GetTextMetrics(&tm);
pDC->GetTextFace(99, szFaceName);
TRACE("font = %s, tmHeight = %d, tmlnternalLeading = Kd," " tmExternalLeading = Nd\n", szFaceName, tm. tmHeight, tm.tmlnternalLeading, tm.tmExternalLeading);
Отредактируйте функцию OnDraw в exO5bVlew.cpp. AppWizard всегда создает для класса «вид» шаблон функции OnDraw. Навдите эту функцию и замените ее текст на следующий: "
void CExOSbView::0nDraw(CD0» pDC) {
CFont fontTestl, fontTestS, fontTest3, font test 4;
fontTestl.CreatsFont(50, 0, 0 , 0 , 400. FALSE, FALSE, 0,■
ANSI_CHARSET, OUT.DEFAULT PRECIS,
CLIP DEFAULT PRECIS, DEFAULT QUALITY, DEFAULT-PITCH I FF_SWISS, "Arial");
CFont* pOldFont = pDC->SelectObject(AfontTest1);
TraceMetrics(pDC);
pDC->Text0ut(0. 0, "Это Arial, ширина по умолчанию");
fontTest2.CreatePont(50, 0, 0, 0, 400, FALSE, FALSE, 0,
ANSI.CHARSET, OUT.DEFAULT PRECIS, CLIP.DEFAULT.PRECIS, DEFAULT QUALITY, DEFAULT-PITCH | FF.MODERH, "Courier"); // не TrueType' '
pDC->SelectQbject(AfontTeetS);■
TraoeMetrios(pDC);. ,,
pDC->Text0ut(0, 100, "Это Courier, ширина по умолчанию"); -• ■
fontTeet3.CreateFont(50, 10, 0, 0, 400, FALSE, FALSE, 0,'
ANSI_CHARSET, OUT.DEFAULT PRECIS, CLIP.DEFAULT.PRECIS, DEFAULT QUALITY, .
"' DEFAULT-PITCH | FF.ROMAN, NULL);
pDC->Select0b j ect (ifontTestS);
TraceMetrics(pDC);
lpDC->TextOut(O, 200, "Эго типовой Roman, переменная ширина'");
fontTest4.CreateFont(50, 0, о, о, 400, FALSE, FALSE, О, , ANSI CHARSET, OUT DEFAULT PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT QUALITY, DEFAULT_PITCH | FF.MODERN, "LinePrinter");
pDC->Select0bject(AfontTest4):
TraceNetrics(pDC);
pDC->TextOut(O, 300, "Это LinePrinter, ширина по умолчанию"}; pDC->SeleatObjact(pOldFont);
Соберите и запустите программу ЕХ05В. Чтобы видеть вывод, генерируемый операторами TRACE, запустите программу «из-под отладчика». Окно программы показано на рисунке.
This is Courier, default width
This is generic Roman, variable width
This is LinePrinter, default width
Уменьшите размеры окна и проследите, как меняется размер шрифтов. Сравните это окно с показанным выше.
' This is Arial, default width<
i Fhis le Courier, default width
: This is generic Romen, variable -width.
j chis is LinePrinter, default width
Продолжайте уменьшать окно и обратите внимание: размер шрифта Courier по достижении некоторого минимума перестает меняться; последите также за изменением ширины шрифта Roman.;■
Элементы программы ЕХ05В*.:
Далее обсуждаются важнейшие элементы примера ЕХ05В.,
Функция-член OnDraw
Эта функция выводит в окне строки с использованием четырех разных шрифтов:
JontTestl — шрифт Arial типа TrueType со стандартной шириной;
JontTest2 — шрифт Courier старого типа со стандартной шириной. Обратите внимание на неровные края букв при большом размере шрифта; '
JonfTestS — типовой шрифт Roman, вместо которого Windows будет использовать TrueType- шрифт Times New Roman с программным выбором ширины. Ширина связана с горизонтальным размером окна — шрифт будет растягиваться так, чтобы строка занимала всю Ширину окна;■ '
JonFFest4 — в программе задан шрифтLincPrin ter, но поскольку он не является шрифтом Windows для дисплея, подсистема шрифтов, руководствуясь параметром FFMODERN^m- берет шрифт Courier New типа TrueType.
Вспомогательная функция TraceMetrics
Вызвав C'DC::GetTextMetncsy. CDGtGeiTextFace,эта функция получает параметры текущего шрифта, которые затем распечатывает в окне Debug. ,.
Пример ЕХ05С'•
Мы уже встречались с классом CScrollViewB главе 4 (пример ЕХ04С). Программа ЕХО5С использует окно с прокруткой и режимом преобразования координат M.M_LO£NGUSHh. позволяет пользователю перемещать эллипс мышью, применяя «захват» мыши. Прокрутка с помощью клавиатуры не реализована, но Вы можете добавить ее, перенеся функцию OnKeyDoiun изЕХ04С. ‘-
Вместо стандартной кисти мы будем использовать для эллипса трафаретную кисть (pattern brush) — настоящий GDI-объект. С использованием трафаретной кисти связана одна сложность: после прокрутки окна необходимо переустанавливать начальную точку — в противном случае полосы трафарета не совпадут и результат будет выглядеть довольно уродливо.
Как и ЕХ04С, данная программа использует класс «вид», производный от CScrolWiew. Этапы создания пр ил ожения таковы.
С помощью AppWizard создайте проект ЕХ05С. Не забудьте выбрать в качестве базового класса CScrolWiew. Параметры и имена классов по умолчанию представлены на рисунке.
Отредактируйте заголовок класса CExOScViewb файле ex05cView.h. Добавьте следующее строки в объявление класса CExOScVieu:-.
private:
const CSize a_3izeEllipse; // логические
CPoint m_pointTopLeft; // логические, верхний левый угол прямоугольника эллипса
CSize m_sizeOffset; // физические, смещение от верхнего левого // угла прямоугольника до точки захвата мыши
BOOL m_bCaptured;
С помощью Classwizard добавьте к классу СЕасО^сИемтри обработчика событий.
Добавьте следующие обработчики.
Сообщениефункция-член
WMJLBUTTONDOWNOnLButtonDown
WM_LBUTTONUPOnLBtrttonUp
WM MOUSEMOVEOnMouseMove _
Отредактируйте функции-обработчики сообщений в классе СЕхвусИеытШабло- ны этих функций были сгенерированы ClassWizard на предыдущем этапе. Найдите эти функции в ex05cView.cpp и поместите в них следующий код.
void CExOScView: :OnLButtonDown(UINT nFlags, CPoint point)
CRect rectEllipse (m_pointTopLeft, m_sizeEllipse); И все еще логические координаты
CRgn circle;
CClientDC dc(this);
OnPrepareDC(ldc);
de.LPtoDP(rectEllipse); // теперь в координатах устройства
circle. CreateElllptlcRgnlndirect(rectEllipse);
if (circle.PtlnRegion(point)) {
// "захватывая" мышь, мы уверены в последующем появлении сообщения LButtonUp SetCaptureO;
m_bCapt tired = TRUE;
CPoint pointTopleft((n_pointTopleft);
de.LPtoDP(4pointTopLeft);
m_sizeOffset = point - pointTopLeft; // координаты устройства
Л на время "захвата' мыши изменяем ее курсор
: :SetCursor(:: LoadCursor(NULL, IDC_CROSS)); )
}
void CExOScView: :OnLButtonUp(UINT nFlags, CPoint point) {
if (m_bCaptured) {
::ReleaseCapture();
a_b€aptured = FALSE;
}
}
void CExOScView: :OnMouseMove(UINT nFlags, CPoint point)
if (ш bCaptured) {
OClientDC de (this) ;
OnPrepareDC(&dc);
CRect rect01d(n_pointTopLeft, ■_sizeEllipse);
dc.LPtoDP(rectOld);
InvalidateRect(rectOld, TRUE);
m_pointTopLeft = point - m_slzeOffset;.
dc,DPtcLP(Am_pcintTopLeft):.
см. след. стр.
CRect rectNew(m_pointTopleft, m_3izeEllipse); de.LPtoDP(rectNew);
InvalidateRect(rectNew, TRUE);
.1
Отредактируйте конструктор CExOScView, функции OnDraw и OnlnitialUpdate.
Шаблоны этих функций сгенерированы AppWizard. Найдите их в ex05cView.cpp и поместите в них следующий код
CExOScView::CEx05cView() : m_sizeEllip6e(100, -100),•;
m_pointTopLeft(0, 0), m sizeOffset(0, 0) <
«„bCaptured = FALSE;
void CExOScView:;OnDraw(CDC* pDC)
{ CBrush brushHatch(HS_DIAGCROSS, RGB(255. 0, 0));
CPoint point (0, 0); И логическая точка (0, 0)
pDC->LPtoDP(&point); И переход к координатам устройства
pDC->SetBrushOrg(point); И для выравнивая трафарета кисти по начальной точке окна
pDC->SelectObject(AbrushHatoh);
pDC->Elllp3e(CRect(m_polntTopLeft, m_sizeEllipse));
pDC->SelectStockObject(BLACK_BRUSH);Ц отсоединяем brushHatch
pDC->Rectangle(CRect(100, -100, 200, -200)); И объявляем прямоугольник недействительным
void CExOScView::OnInitialUpdate()
CScrollView::0nInitiaiUpdate();
CSize sizef!btal(800, 1050); // 8 на 10,5 дюймов
CSize sizePage(sizeTotal.cx / 2, sizeTotal.cy / 2);
CSize sizeLine(sizeTotal.cx / SO, sizeTotal.cy / 50);
SetScrollSizes(MM_LOENGLISH, sizeTotal, sizePage, sizeLine);;
Соберите и запустите программу ЕХ05С. Программа поддерживает перетаскивание эллипса мышью и прокрутку изображения в окне. Окно программы должно выглядеть примерно как на рисунке. При перемещении эллипса следите за черным прямоугольником. Вы должны заметить эффекты, возникающие, когда изображение прямоугольника становится «недействительным*.
Элементы программы ЕХ05С " ' '''
Ниже обсуждаются важнейшие элементы примера ЕХ05С.
Переменные-члены mjsizeEUIpseM т_pointTopLeft
Вместо того, чтобы хранить описывающий прямоугольникдля эллипса в одном объекте CRect, программа раздельно хранит размер (msizeEllipsepi положение левого верхнего угла этого прямоугольника («г_рош/7су?7е/?)Чтобы переместить эллипс, программе достаточно заново вычислить т_pointlbpLeft$ipH. этом погрешность округления не влияет на размер эллипса.
Переменная-член mjsizeOffset
Когда функция OnMouseMove перемещает эллипс, относительное положение указателя мыши внутри эллипса должно оставаться неизменным с того момента, когда пользователь нажал левую кнопку. В переменной ^яхеО^еСранится первоначальное смещение мыши относительно левого верхнего угла прямоугольника эллипса.
Переменная-член m_bCaptured.
Эта логическая переменная устанавливается в TRUE на время выполнения операции перемещения эллипса мышью.'
Функции SetCapture и Releasecapture
Функция SetCaptureкласса (^«^«захватывает» (capture) мышь, после чего сообщение о перемещении мыши будет посылаться данному окну —даже если указатель мыши выйдет заего пределы. Нежелательный побочный эффект состоит в том, что эллипс может выйти за пределы окна и «потеряться». Желательный и необходимый эффект — то, что все последующие сообщения мыши, включая WM_LBUTTONDOWN (которые иначе были бы потеряны), теперь посылаются нашему окну. Функция Win32 ReleaseCapture снимает «захват» мыши.
Функции Win32 SetCursor и LoadCursor
Некоторые функции Win32 не имеют эквивалентов в библиотеке MFC. По соглашению мы используем оператор области видимости C++ (р) при вызове функций Win32 напрямугу-.В этом случае вероятность конфликта имен с функцией-членом класса (Мегоисключена, но Вы всегда мажете вместо функции-члена класса вызвать одноименную функцию Win32. При этом оператор - гарантирует, что будет вызвана именно глобальная функция Win32. ■
Если первый параметр равен NULL, LoadCursor создает ресурс-курсор (cursor resource) из заданного стандартного курсора мыши Windows. Функция SetCursor активизирует заданный ресурс курсора. Этот курсор остается активным, пока мышь захвачена.
Функция-член CScrollView::OnPrepareDC.
Виртуальная функция OnPrepareDC в классе CView ничего ив делает. В классе CScroUVieweRa переопределена и устанавливает режим преобразования и точку начала координат окна «вид» на основании параметров, которые Вы передали SetScrollSizesB функции ©«Create. Каркас приложения автоматически вызывает OnPrepareDC перед вызовом ©nDr-@tp, так что Вы не должны заботиться об этом. Из любого другого обработчика сообщения, использующего контекст устройства окна представления, например, из O«LButtonDow«Tt OnMouseMove, функцию OnPrepareDC Вам придется вызывать самостоятельно.. .,
Преобразование координат в OnMouseMove
Данная функция содержит несколько операторов преобразования. Алгоритм следующий.
Создать предыдущий описывающий прямоугольник эллипса и преобразовать его из логических координат в координаты устройства.
Сделать предыдущий прямоугольник нед ействительным.
Обновить координаты левого верхнего угла прямоугольника.
Создать новый прямоугольник и преобразовать его в координаты устройства.
Сделать недействительным новый прямоугольник.
Функция вызывает InvalidateRectppaxAH. Windows «накапливает* недействительные прямоугольники и вычисляет новый нед ействительный прямоугольник, представляющий собой пересечение объединения двух первых с клиентским прямоугольником.
Функция OnDraw:.
Вызов 5е?Вг«5йО^обеспечивает правильную перерисовку трафарета, используемого для внутренности эллипса, когда пользователь прокручивает изображение в окне. Кисть выравнивается относительно точки, находящейся в левом верхнем углу логического окна и преобразованной в координаты устройства. Это важное исключение из правила, по которому параметрами функций-членов CDC служат логические координаты.
Режим SetScaleToFitSIze класса CScrolMew
Класс CScroilView поддерживает масштабирование по размеру окна. В этом режиме вся область прокрутки отображается в окне целиком. Используется режим преобразования координат MM_AN7SOTROPI<£ одним ограничением: положительные значения координаты у всегда возрастают при движении вниз, как в режиме ММ_ТЕХТ.
Чтобы воспользоваться масштабированием по размеру окна, замените вызов SetScroHSizesHa SetScaleToFitSize(sizeTotal);
Вы можете вызывать эту функцию по команде меню Shrink То Fit и реализовать таким образом переключение между режимом прокрутки и режимам масштабирования по размеру окна.
Режим преобразования координат «логический твип» в окне с прокруткой
MFC-класс CScrollView позволяет прибегать только к стандартным режимам преобразования координат. В примере ЕХ19Ав главе 19 представлен новый класс CIogScro/Meiy, который поддерживает режим «логические твипы*.
ГЛАВА 6
' Модальные диалоговые окна и стандартные
■ элeмeнтыyпpaвлeнияWindows
Практически вое Windows-программы взаимодействуют с пользователем при помощи диалотовых окон. Диалоговое окно может просто содержать какое-то сообщение и кнопку ОК, а может быть и очень сложной формой для ввода данных. Иногда этот мощный элемент называют «диалоговой панелью*, что по меньшей мере несправедливо. Диалоговое окно — настоящее окно, которое можно перемещать и закрывать; оно принимает сообщения и даже отрабатывает команды отрисовки данных в своей клиентской области.
Существует два типа диалоговых окон: модальные (modal) и немодальные (modeless). В этой главе мы рассмотрим более распространенный модальный тип. Б первом из двух примеров данной главы Вы будете работать с давно знакомыми элементами управления, унаследованными от Win 16, например, полем ввода и списком. Во втором — с новыми элементами управления, впервые появившимися в Windows 95. О немодальных диалоговых окнах и стандартных диалоговых окнах Windows, предназначенных для открытия файлов, выбора шрифтов и прочего, мы поговорим в главе 7. В главе 8 мы рассмотрим элементы управления ActiveX, а в главе 9 — новью классы элементов управления Internet Explorer, появившиеся в MFC 6.0.
Модальные и немодальные диалоговые окна
Базовый класс CDialog поддерживает как модальные, так и немодальные диалоговые окна. Пока открыто модальное диалоговое окно (например, Open File — «Открытие файла»), пользователь не может работать ни с каким другим окном программы (точнее, окном того же потока пользовательского интерфейса). Если же открыто немодальное диалоговое окно, работать с другим окном программы можно. Пример — диалоговое окно Find and Replace редактора Microsoft Word, которое совершенно не мешает редактировать текст.
Выбор конкретного типа диалогового окна зависит от характера создаваемого приложения. Программировать модальные диалоговые окна намного проще, и это может повлиять на ваше решение.
ПРИМЕЧАНИЕ 16-разрядные версии Windows поддерживают особый вид медального диалогового окна — системное модальное (system modal), который не дает пользователю переключаться в другое приложение. Win32 тоже поддерживает системные модальные диалоговые окна, правда, весьма своеобразно: переключаться в другую программу можно, но системное модальное диалоговое окно всегда останется сверху других окон. Так что в Wln32-пpилoжeнияx такие диалоговые окна, по-видимому, не стоит применять.
Ресурсы иэлементыуправления
Итак, диалоговое окно — это настоящее окно. Но чем же оно все-таки отличается от окон класса CView,c которыми Вы успели поработать? Хотя бы тем, что диалоговое окно почти всегда связано с каким-нибудь ресурсом Windows, идентифицирующим элементы и определяющим структуру окна. Поскольку диалоговый ресурс можно создавать и модифицировать в редакторе диалоговых окон (одном из редакторов ресурсов), диалоговые окна формируются быстро, эффективно и наглядно.
Диалоговое окно содержит набор элементов управления (controls); поля ввода (edit controls; их еще называют текстовыми окнами — text boxes), кнопки (buttons), списки (list boxes), комбинированные списки (combo boxes), статический текст (static text) идилтетикя (labels), списки с древовидным отображением (tree views), индикаторы продвижения (progress indicators), ползунки (sliders) и т. д. Windows управляет этими элементами, используя специальную, значительно упрощающую труд программиста логику группирования и обхода. На элементы управления можно ссылаться либо по указателю на <7W«c/(поскольку элементы сами являются окнами), либо по индексу (с сопоставленной константой *^тв)цазначенному в ресурсе. Реагируя на действия пользователя, скажем, ввод текста или щелчок кнопки, элементы управления передают сообщения родительскому диалоговому окну. .
MFC-библиотека и ClassWizard, тесно взаимодействуя, облегчают программирование диалоговых окон Windows. ClassWizard генерирует класс, производный от CDialog, а затем позволяет соотнести переменные-члены класса «диалоговое окно» с элементами управления. Вы можете указывать такие параметры, как максимальная длина текста или границы диапазона для вводимых чисел. После этого ClassWizard генерирует вызовы MFC-функций, отвечающих за обмен данными и проверку их достоверности. Эти функции перемещают информацию между экранными элементами и переменными-членами соответствующего класса.
Программирование модального диалогового окна
Модальные диалоговые окна используют чаще всего. Пользователь совершает какое-то действие (например, выбирает команду в меню), на экране появляется диалоговое окно пользователь вводит данные, а затем закрывает его. Чтобы дополнить существующий проект модальным диалоговым окном, сделайте следующее.
Используя редактор диалоговых окон, создайте диалоговый ресурс с элементами управления. Редактор обновит файл описания ресурсов (RC-файл) вашего проекта, включив в него новый ресурс, и файл resourceh, дополнив его соответствующими константами ^define.
С помощью ClassWizard создайте класс «диалоговое окно», производный от CDialog, и закрепите его за ресурсом, созданным в п. 1. ClassWizard добавит в проект требуемый код и заголовочный файл.
ПРИМЕЧАНИЕ При генерации производного класса диалогового окна ClassWizard формирует конструктор, который запускает конструктор CDialog, принимающий в качестве параметра идентификатор ресурса. Заголовочный файл диалогового окна содержит константу класса IDD, которой присвоен идентификатор диалогового ресурса. А реализация конструктора в СРР-файле выглядит так:
CMyDialog::CMyOialog(CWnd* pParent /«=NULL*/)
: CDialgg(CMyDialog::IDD, pParent) i // здесь должен быть инициализирующий код
Применение enum IDD избавляет СРР-файл от идентификаторов ресурсов, определяемых в файле resource.!! данного проекта.•
Используя Class Wizard, добавьте в класс диалогового окна переменные-члены и функции, предназначенные для обмена и проверки данных.
С помощью Class Wizard добавьте обработчики сообщений для кнопок и других (генерирующих события) элементов управления диалогового окна.
■5. Напишите код для инициализации (в OnlnitDialog)элементов управления и для обработчиков сообщений. Убедитесь, что при закрытии диалогового окна (если только пользователь не нажмет кнопку «Отмена») вызывается виртуальная функция-член ОпОКкласса CDialog (она вызывается по умолчанию).
6. В вашем классе «вид» напишите код, активизирующий диалоговое окно. Он сводится к вызову конструктора вайего класса диалогового окна и последующему вызову функции-члена DoModalкласса диалогового окна. РоМод^возвращает управление только после закрытия диалогового окна.
А теперь рассмотрим все эти операции на примере.
Пример ЕХ06А'
Мы не станем возиться с каким-нибудь простеньким примером, а встроим в наше диалоговое окно почти все возможные элементы управления. Работа эта несложная, потому что нам поможет редактор диалоговых окон Visual C++. Законченный вццдиалогового окна представлен на рисунке 6-1.
Рис. 6-1. Законченное диалоговое окне в действии
Как видите, это диалоговое окно предназначено для учета кадров. Это пример довольно скучного делового приложения — неплохо бы сделать с ним что-нибудь такое, что было невозможно во времена перфокарт. Программу слегка оживляют полосы прокрутки «loyalty» («лояльность») и «reliability» («надежность») — классический пример прямого ввода и наглядного отображения данных. Еще интереснее были бы здесь элементы управления ActiveX, но использовать их Вы научитесь только в главе 8.
Построение диалогового ресурса
Для создания диалогового ресурса сделайте следующее.
Запустите AppWizafd, чтобы сгенерировать проект ЕХОбА, Выберите из меню File команду New, азатем щелкните вкладку Projects и выберите MFC Арр Wizard (ехе). Примите параметры по умолчанию, но установите Single Document Interface и откажитесь от Printing And Print Preview. Параметры и имена классов по умолчанию показаны на рисунке.
Как обычно, Арр Wizard будет считать новый проект текущим.
Создайте новый диалоговый ресурс с идентификатором IDDDIALOG1.Выберите из меню Insert среды разработки команду Resource и в появившемся диалоговом окне Insert Resource щелкните строку Dialog, азатем — кнопку New. Visual C++ создаст новый диалоговый ресурс, как показано на рисунке.
Редактор диалоговых окон присвоит новому диалоговому окну идентификатор ресурса IDD_D1ALOG1. Заметьте: он вставляет в новое диалоговое окно кнопки «ОК» и «СапсеЬ>.
У. Отмасштабируйте диалоговое окно и присвойте ему заголовок. Увеличьте размеры окна редактирования, чтобы было удобно работать. Щелкнув новое диалоговое окно правой кнопкой мыши и выбрав из всплывающего меню команду Properties, Вы увидите диалоговое окно Dialog Properties. Введите заголовок создаваемого диалогового окна — так, как показано на следующем рисунке. Состояние кнопки в левом верхнем углу определяет, будет ли диалоговое окно Dialog Properties располагаться поверх остальных окон. Когда эта кнопка нажата, окно всегда находится поверх других. Щелкнув кнопку Toggle Grid на панели инструментов Dialog, включите линии сетки для создаваемого окна — это поможет выровнять элементы управления.
Выберите стиль диалогового окна. Щелкните вкладку Styles в верхней части окна Dialog Properties и установите параметры стиля, как показано на рисунке.
Укажите дополнительные стили диалогового окна. Щелкните вкладку More Styles и задайте параметры, как показано ниже.
Разместите элементы управления диалогового окна. Это делается с помощью палитры элементов управления. (Если палитра не видна, щелкните правой кнопкой мыши любую панель инструментов и выберите из списка пункт Controls.) Перетащите мышью нужные элементы с палитры в создаваемое диалоговое окно, а затем сдвиньте и отмасштаби- руйте их так, чтобы они разместились, как показано на рисунке 6-1. Что представляет ■собой палитра элементов управления показано на рисунке на стр. 82.
ПРИМЕЧАНИЕ Редактор диалоговых окон сообщает о положении и размере каждого элемента управления в своей строке состояния. При этом координаты выражаются не в аппаратных, а в специальных «диалоговых единицах* (DLUs). Горизонтальная DLU — этр средняя ширина шрифта, используемого в диалоговом окне, деленная на 4; вертикальная DLU — средняя высота этого шрифта, деленная на 8. Ну, а шрифт — это обычно MS Sans Serif размером 8 пунктов.
Select (выбрать)Static text (статический текст)Group Вах (группирующая рамка)Check Вах (флажок)Combo Вах(комбинированный список)Horizontal Scroll Bar (горизонтальная полоса прокрутки) Spin (наборный счетчик)Slider (ползунок)List Control (графический список)Tab Control (набор вкладок) ,, Rich Edit(поле ввода с форматированием)Month Calendar (календарь)*Custom Control(пользовательский элемент управления)Select (выбрать)Static text (статический текст)Group Вах (группирующая рамка)Check Вах (флажок)Combo Вах(комбинированный список)Horizontal Scroll Bar (горизонтальная полоса прокрутки) Spin (наборный счетчик)Slider (ползунок)List Control (графический список)Tab Control (набор вкладок) ,, Rich Edit(поле ввода с форматированием)Month Calendar (календарь)*Custom Control(пользовательский элемент управления)IPicture (рисунок)
Select (выбрать)
Static text (статический текст)
Group Вах (группирующая рамка)
Check Вах (флажок)
Combo Вах
(комбинированный список)
Horizontal Scroll Bar (горизонтальная полоса прокрутки) Spin (наборный счетчик)
Slider (ползунок)
List Control (графический список)
Tab Control (набор вкладок) ,, Rich Edit
(поле ввода с форматированием)
Month Calendar (календарь)*
Custom Control
(пользовательский элемент управления)
Select (выбрать)
Static text (статический текст)
Group Вах (группирующая рамка)
Check Вах (флажок)
Combo Вах
(комбинированный список)
Horizontal Scroll Bar (горизонтальная полоса прокрутки) Spin (наборный счетчик)
Slider (ползунок)
List Control (графический список)
Tab Control (набор вкладок) ,, Rich Edit
(поле ввода с форматированием)
Month Calendar (календарь)*
Custom Control
(пользовательский элемент управления)
Edit Box (поле ввода)
Button (кнопка)
» Rado Button (переключатель)
List Box (список)
„ Vertical Scrool Bar
{вертикальная полоса прокрутки)
" Progress (индикагор прсдвижения)
Hot Key (горячая клавиша)
Tree Control
.(древовидная структура отображения)
Animate (анимация)
Date Time Picker
(элемент выборадаты/времени)
• IP Address (элемент ввода IP-адреса) * Extended Combo Box
(расширенный комбинированный список)
* Звездочкой отмечены стандартные элементы управления
Internet Explorer 4, появившиеся в MFC 6.0. О них речь пойдет в главе9.
Теперь коротко рассмотрим элементы управления нашего диалогового окна.
Статический текст для поля Name (имя). Этот элемент просто выводит на экранука- занные символы и собственно к диалогу с пользователем отношения не имеет. Расположив ограничивающий прямоугольник, сведите текст (размеры прямоугольника можно при необходимости изменить). Это единственный элемент управления «статический текст», который здесь упоминается, но па рисунке 6-1 есть и другие подобные элементы; создавайте их по аналогии. Все они имеют один и тот же идентификатор, но это совершенно не важно, поскольку доступ к ним программе не нужен1.
Поле ввода Name. Поле ввода — основное средство ввода текста в диалоговых окнах 'Смените его идентификатор с IDC_EDIT1 па IDG_NAME. Остальные свойства оставьте такими, какими они предлагаются по умолчанию. Заметьте: по умолчанию устанавливается Auto HScrpll, т. е. текст по мере заполнения текстового поля прокручивается справа налево
Поле ввода SS Nbr (social security number—номер карточки социальногострахо- вания). Этот элемент управления идентичен предыдущему. Замените его идентификатор на IDC_SSN. Впоследствии при помощи ClassWizard Вы превратите это поле в числовое.
Поле ввода Bio (biography — биография). Это многострочное поле ввода. Присвойте ему идентификатор 1ПС_ВЮу. установите его свойства, как показано ниже на рисунке.
Группирующая рамка Category (категория). Этот элемент управления нужен только для того, чтобы сгруппировать два переключателя. Введите его название: Category. Идентификатор, присвоенный по умолчанию, нас устроит.
Таким единым идентификатором может быть только IDC STATIC, равный -1. — Прим, перво.
Переключатели Hourly (почасовая оплата) и Salary (оклад). Разместите эти переключатели в группирующей рамке Category. Идентификатор кнопки Hourly задайте как IDC_CAT,a остальные свойства укажите в соответствии с рисунками.
Убедитесь, что у обеих кнопок на вкладке Styles установлено свойство Auto (rto умолчанию) и что только у кнопки Hourly установлено свойство Group. При правильном задании этих свойств Windows гарантирует, что пользователь сможет одновременно выбрать только одну из двух кнопок. Группирующая рамка Category никак не сказывается на их функционировании.
Группирующая рамка Insurance (страховка). В этом элементе управления размещаются три флажка. Введите название рамки — Insurance.
ПРИМЕЧАНИЕ Позже, установив порядок обхода элементов в диалоговом окне. Вы добьетесь того, чтобы группирующая рамка Insurance следовала за последней кнопкой рамки Category. Только не забудьте пометить у элемента управления Insurance свойство Group, чтобы «завершить» предыдущую группу. Если этого не сделать, особой беды не будет, но, запустив программу под отладчиком, Вы получите предупреждающее сообщение.
Флажки Life (жизни). Disability (по инвалидности) и Medical (медицинская). Разместите эти элементы управления в группирующей рамке Insurance. Примите свойства, предлагаемые по умолчанию, но замените идентификаторы на IDC_UFE, IDC_DIS и IDC MED. В отличие от переключателей, флажки ведут себя независимо — пользователь смажет устанавливать любую их комбинацию.
Комбинированный список Skill (профессиональные навыки). Это первый из трех типов комбинированных списков. Измените идентификатор на IDC_SKIIJ.>a затем щелкните вкладку Styles и установите параметр Туре в Simple. Теперь щелкните вкладку Ва1а и введите в поле Enter Listbox Items названия трех профессий (заканчивая каждую строку нажатием сочетания клавиш Ctrl+Enter).
^Выравнивание элементов управления, ■ ^| ^ ;^ ефЙЩ В1®Ш11ЖЖ^
• ^Чтобы выровнять два или более элемента управления,-щелкните сначала первый, а за-
шш2 ^тем, удерживая кяадишф^^Ё^ тфочце. ДЦЙ£ выберите в пммзт^^‘
ш
ш
/Layout редактора диалоговых* окон одну йз кбманд йьг'1- * ' '
Этот тип комбинированного списка называется дроетми (Simple). Б поле ввода, располагающемся вверху, пользователь может вводить любые данные и при помощи мыши или клавиш-стрелок Up/Down выделять элемент в окне списка.
Комбинированный список Educ (education — образование). Замените идентификатор на IDCEDUC.a. прочие параметры оставьте такими, какими они предлагаются по умолчанию. Введите в поле Enter Listbox Items вкладки Data три уровня образования (см. рис. 6-1). В этом комбинированном списке пользователь сможет набрать в поле ввода все. что ему заблагорассудится или, щелкнув стрелку, раскрыть прикрепленное окно списка и выбрать любой элемент (мышью или клавишами-стрелками Up/Down).
ПРИМЕЧАНИЕ Чтобы задать размер раскрывающейся части комбинированного списка, щелкните 'стрелку в правой части поля и сместите ее мышью вниз.
'Список Dept (department — отдел). Замените идентификатор на IDC_DEPT, а прочие параметры оставьте такими, какими они предлагаются по умолчанию. В этом списке пользователь смажет выбрать лишь один элемент мышью, клавишами-стрелками Up/Down или, введя первый символ нужной строки. Обратите внимание: ввести строку, предлагаемую пользователю по умолчанию, в графическом редакторе нельзя; как это сделать, Вы узнаете чуть позже.
Комбинированный список Lang (language — язык). Замените идентификатор ни IDG JANG, выберите вкладку Styles и присвойте параметру Туре значение Drop List. Введите в поле Enter Listbox Items вкладки Data названия трех языков: English, French и Spanish. В этом комбинированном списке пользователь смажет выбирать элементы только из раскрывающегося списка. Для этого он должен будет, щелкнув стрелку, указать нужную строку или ввести ее первую букву и при необходимости уточнить выбор при помощи стрелок.
1* Полосы прокрутки Loyalty (лояльность) и Reliability (надежность). Не путайте элемент управления «полоса прокрутки» с полосами прокрутки, встроенными в окно. Элемент «полоса прокрутки» ведет себя так же, как и остальные элементы управления, — в частности, в период проектирования его можно масштабировать, Разместите горизонтальные полосы прокрутки, отмасштабируйте их (см рис. 6-1) и присвойте им идентификаторы IDCJ.OYAL и 1OC_RELY.'
Кнопки OK, Cancel и Special. Введите названия кнопок OK, Cancel и Special, затем присвойте кнопке Special идентификатор lDC_SPEClAL.4yrb позже Вы узнаете об особом предназначении идентификаторов по умолчанию IDOK и IDCANCEL,
Произвольный значок (для примера показывается значок MFC). В диалоговом окне при помощи элемента управления Picture можно отобразить любой значок или растровое изображение, если они определены в описании ресурсов. Воспользуемся MFC-значком программы, обозначенным как IDR_MAiNFRAMEYcTaitoEH.Te параметр Туре как Icon, а параметр Image — в /Р^МЧ/^У^ТШ/^Идентификатор оставьте ЮС_ЕТАТ1С.
Проверьте порядок перехода между элементами управления. Выберите из меню ' Layout в редакторе диалоговых окон команду Tab Order. Укажите мышью порядок переключения между элементами управления при нажатии клавиши Tab. Для этого щелкните мышью каждый из элементов управления в таком порядке, как показано на рисунке, а затем нажмите Enter.
СОВЕТ Если Вы перепутали порядок переключения, укажите на последний правильно помеченный элемент и щелкните левой кнопкой мыши, удерживая клавишу Ctrl.
ПРИМЕЧАНИЕ В текст заголовка элемента управления «статический текст* (например, Name и Skill) можно вставить знак «&». В период выполнения символ, за которым стоит этот знак, будет подчеркнут (см. рис. 6-1). Это позволяет переходить к нужным элементам управления нажатием Alt+«подчеркнутый символ». Так, Alt+N сместит фокус на поле ввода Name, a Alt+K — на комбинированный список Skill. Ясно, что такие символы должны быть уникальны в пределах диалогового окна. Буква К используется для элемента управления Skill потому, что буква S уже задействована в поле ввода SS Nbr.
Сохраните файл ресурсов на диске. Осторожности ради сохраните файл ехОбатс; выберите из меню File команду Save или щелкните одноименную кнопку на панели инструментов. Не закрывайте пока редактор диалоговых окон и оставьте на экране только что созданное диалоговое окно.
ClassWizard и класс «диалоговое окно»
Теперь Вы создали диалоговый ресурс, но без соответствующего класса диалогового окна воспользоваться им нельзя. (Взаимосвязьдиалогового окна и стоящих за ним классов обсуждается в разделе «Разбор приложения ЕХОбА».) Действия ClassWizard и редактора диалогов при создании класса диалогового окна.
Выберите ClassWizard из меню View в Visual C++ (или нажмите Ctrl+W). Убедитесь, что в редакторе диалоговых окон по-прежнему выделено только что созданное диалоговое окно IDD_DIALOGhi что ЕХОбА — текущий проект Visual C++.
Добавьте класс CEw06<*D<e/og.ClassWizard обнаружит, что Вы создали диалоговый ресурс и что ему еще не сопоставлен С++-класс, после чего вежливо осведомится, не хотите ли Вы создать класс.
Щелкните кнопку ОК, соглашаясь с предложенным по умолчанию выбором Create A New Class, и заполните верхнее поле в диалоговом окне Create New Class, как показано ниже.
Добавьте переменные класса CEx06aDi«log.nocne того как ClassWizard сформирует класс CExOGaDialogpa. экране появитсядиалоговое окно MFC ClassWizard. Щелкните вкладку Member Variables, и Вы увидите одноименную страничку (см первый рисунок на стр. 87). Вам надо сопоставить переменные-члены каждому элементу управления диалогового окна. Для этого щелкните идентификатор элемента управления, а затем — кнопку Add Variable. В результате появитсядиалоговое окно Add Member Variable (см второй рисунок на стр. 87). Введите имя переменной-члена и выберите ее тип в соответствии со следующей таблицей. Убедитесь, что Вы набрали имя переменной-члена именно так, как показано в таблице, — здесь важен регистр каждой буквы. Далее щелкните кнопку ОК, чтобы вернуться в
диалоговое окно MFC ClassWizard, и повторите только что описанную процедуру применительно к остальным элементам управления.
Идентификатор элементауправления
Переменная-членТип
idcbio idc_cat 1DC_DEPT
IDC_DIS IDC_EDUC IDCJANG IDC_LIFE 1DC_LOYAL '
ЮС_МЕП IDC.NAME 1DC_RELY IDC_SK1LL IDCSSN
mstrBioCString
m_nCatint
m strDeptCString
' m_ blnsDisBOOL
m_strEducCString
m strLangCString
m hlnsl.ifeBOOL
m «Loyalint
mblnsMedBOOL
mstrNameCString
m nRelyint
m_strSkillCString
m_nSsnint
Когда Вы выбираете элементы управления в диалоговом окне MFC Class Wizard, в нижней его части появляются соответствующие поля ввода. Указав переменную CString, можно ус-
тановить ее максимальную длину, авыбрав числовую переменную — задать ее верхний и нижний пределы. Введите для IDCSSNминимальное значение 0 и максимальное — 999999999- Большинство взаимосвязей между типами элементов управления и типами переменных совершенно прозрачны. Однако уже не столь очевидно, как кнопки-перекдючатели соотносятся с переменными. Класс СОмЬ^сопоставляетцелочисленную переменную каждойздун- пе переключателей; при этом первой кнопке соответствует значение 0, второй — 1 и т. д
Добавьте функцию-обработчик сообщений для кнопки Special. КлассуEx06aDialog не требуется много функций-обработчиков сообщений, поскольку большую часть работы по управлению диалоговым окном выполняет его базовый класс CDiatog совместно с Windows. Если Вы, например, присвоили идентификатор /ДОКкнопке ОК (установка Class Wizard по умолчанию), при щелчке этой кнопки вызывается виртуальная CDiatog- функция ОпОК. Однако для других кнопок нужны обработчики сообщений.
Поэтому щелкните вкладку Message Мар. В диалоговом окне ClassWizard в списке Object IDs должна присутствовать запись для 7£С_$РГСШ.Щелкните эту запись, азатем дважды - сообщение BN CLICKED в списке Messages. ClassWizard присваивает функции-члену имя OnSpeciata. открывает д иалоговое окно Add Member Function.
Здесь можно было бы ввести свое имя функции, но сейчас примите то, что предлагается по умолчанию, и щелкните кнопку ОК. Далее в диалоговом окне MFC ClassWizard щелкните кнопку Edit Code — откроется файл ex06aDialog.cpp и появится код функции OnSpecktL Замените существующий код оператором TRACE,выделенным в листинге полужирным: void CExOSaDialog::OrSpecial() {
TRACE("CEx06aDialog::OnSpecial\n");
Используя ClassWizard, добавьте функцию-обработчик OnlnitDielog.KaK Вы скоро увидите, ClassWizard генерирует код, инициализирующий элементы управления диалогового окна. Однако этотООХ-код (Dialog Data Exchange) не будет инициализировать варианты, предлагаемые в списках, поэтому нужно переопределить функцию CDiaiog::On- InitDialog.Xcrrs OnlnitDialog — это виртуальная функция-член, ClassWizard сформирует для нее прототип и заготовку, если Вы решите обработать сообщение WM_INITDIALOG в производном классе «диалоговое окно*. Для этого выберите в списке Object IDs класс СЕхОба- Diaiog, а в списке Messages дважды щелкните сообщение WMINITDIALOG. Далее щелкните кнопку Edit Code в диалоговом окне MFC ClassWizard, чтобы отредактировать функцию OnlnitDialog. Замените существующий код текстом, который выделен полужирным:
BOOL CExOGaDialog::OnlritDlalog()
{'
// CDialog::OnlnitDialog можно вызвать в этой функции только один раз -
// будьте внимательны
CListBox* pLB = (CListBox*) GetDlgItem{ IDC_DEPT);,
pLB->InsertString(-1, "Канцелярия");
pLB->InsertString(-1, "Бухгалтерия");
pLB->InsertString(-1, "Отдел кадров");'
pLB->InsertString(-1, "Безопасность");_
// вызываем после инициализации
return CDialog::OnInitDialog();
}
Для комбинированных списков вместо инициализации в ресурсе при желании можно воспользоваться такой же процедурой.
Подключение диалогового окна к классу «вид»
Теперь у Вас есть ресурс и код для диалогового окна, но окно не подключено к классу «вид», В большинстве приложений диалоговое окно открывается при выборе какой-либо команды из меню, но пока Вы меню «не проходили». Поэтому для открытия диалогового окна прибегнем к знакомому сообщению WM_LBUTTONDOWN, которое генерируется при щелчке левой кнопки мыши. Ваши действия.
Выберите в Class Wizard класс СЕхОблПеи^.Уб едитесь, что ЕХ06А — текущий проект Visual C++.1
С помощью ClassWizard. добавьте функцию-член OnLButtonDowri. Вы уже проделывали это в предыдущих главах. Просто выделите имя класса CEx06aView,щелкните идентификатор объекта CExO6«View,a затем дважды — сообщение WM_LBUTTONDOWN.
Добавьте код функции OtilBwttonDown в файле ex06a¥lew.cpp. Добавьте в заготовку тела функции код, выделенный полужирным. Большая часть кода состоит из операторов TRACE, выводящих значения переменных диалогового окна после того, как пользователь закрыл диалоговое окно. Но главное здесь — вызовы конструктора класса СЕхОба- Di«log и функции DoModaL
void CEx06aView::OnLBuvtonDown(UINT nFlags, CPoint point) {
CEx06aDialog dig;
dlg.m_strName = "Шекспир, Уип";
dlg.m„nSsn = 307806636;
dlg.m_nCat =1; // 0 = почасовая, 1 = оклад
dlg.m_strBio = "He слишком увлеченный своей работой технический писатель";
dlg.m_bInsLife = TRUE;■
dlg.m_bInsDiB = FALSE;
dlg.m_bInsMed = TRUE;
dlg.m_strDept = "Канцелярия";
dlg.m_3trSkill = "Писатель";
dlg.m_nLang=0;
dlg.a_strEduc="Колледж";
dig.m_nLoyal=dlg.m_nRely= 50;
int ret=dlg.DoModal();
TRACE("Возврат иэ DoModal = %d\n", ret);
TRACE("name = %s, ssn = W, cat = %d\n",
dlg.m„3trNaae, dlg.e_nSsn, dlg.m_nCat);
TRACE("dept = Us, skill = Us, lang = %d, educ = Hfe\n", dlg.m_strDept, dlg.m_strSkilL dlg.m_nLang, dlg.m_strEduc);
TRACE('life = %d, dis = %d, med - <d, bio = Ks\n",
dlg.e_bInsLife, dlg.a.blnsDis, dlg.m_bInsMed, dlg.B_str0io);
TRACE(“loyalty = lid, reliability = *d\n",
dig. nunloyal, dig.m_nRely) ;
Добавьте код в виртуальную функцию OnDraw в файле ex06aView.cpp. Чтобы предложить пользователю нажать левую кнопку мыши, закодируйте С^хОбд^егу-функцию OnDmw (ее заготовка сгенерирована мастером AppWizard). Замените существующий код текстом, выделенным полужирным:
void CExOBaView: :OnOraw(CDC* pDC)
<
pDC->TextOut(0, 0, "Щелкните здесь левой кнопкой мыши."); ■
}.
Добавьте в файл ex06aView.cpp оператор включения класса «диалоговое ОКНО». Показанная выше функция OnLButtonDoivn зависит от объявления класса CExOSaDiaiog.BH должны включить следующий оператор include-,
#include "exOSaDialog. h"
в начало файла с исходным кодом для класса СЕхОбаУгеш^йхбЬг^М^^^ после оператора: #include "ex06aView,h'’
Соберите и протестируйте приложение. Если все сделано правильно, Вы сможете собрать и запустить приложение ЕХ06А из Visual C++. Попробуйте, вводя данные в каждый элемент управления, щелкать кнопку ОК и наблюдать за выводом операторов TTMCQi окне Debug. Полосы прокрутки пока ничего особенного не делают — о них речь пойдет позже. Обратите внимание также на то, что происходит при нажатии клавиши Enter в момент ввода текстовых данных в каком-либо элементе управления — диалоговое окно немедленно закрывается. .
Разбор приложения ЕХ06А
После вызова ЛоМо^я/управление возвратится вашей программе, только когда пользователь закроет диалоговое окно. Если Вам это ясно, значит, Вы поняли, что такое модальное диало говое окно. Перейдя к работе с немодальными диалоговыми окнами. Вы еще оцените, насколько просто было программировать модальные диалоговые окна, потому что при вызове DoModal очень многое остается за кадром. Но вернемся к нашей теме и рассмотрим краткую сводку «кто кого вызывает»:
CDialog:: DoModal
CEx06aDialog::OnInitDialog'
...дополнительная инициализация...
CDialog:: OnlnitDialog
CWnd::UpdateData(FALSE)
CEx06aDialog::DoDataExchange
пользователь вводит данные...
пользователь щелкает кнопку ОК
CEx06aDialog;:OnOK
..дополнительная проверка...
CDialog::OnOK
CWnd::UpdateData(TRUE)
CEx06aDialag::DoDataExchange
CDialog;:EndDialog(IDOK)
OnlnitDialog-а DoDataExcbange— виртуальные функции, переопределенные в классе СЕхОба Dialog. Windows вызывает OnlnitDialog при инициализации диалогового окна, что приводит к вызову DbDataExcbange — виртуальной CVPW-фупкции, переопределенной мастером ClassWizard. Взгляните на листинг этой функции:
void CExOGaDialog::DoDataExchange(CDataExchange* pDX)
CDialog::DoData£xchanga(pDX):’
//{{AFX DATA MAP(CEx06aDlalog)
DDX Text(pDX, IDC BIO, m strBio);
DDX_Radio(pDX, IDC_CAT, m.nCat);
DDX LBStringCpOX, IDC_OBPT, m_strDept);
DDX_Check(pDX, IDC_DIS, in_oIhsDis);
BDX_CBString(pDX, IDG LANG, m strLang); DDX_Check(.pDX, IDC_LIFE. m.blcsLife); DDX_Scroll(pDX, IOC LOYAL, m_nLoyal); DOX Check(pDX, IDC NED. m MnsMcd); DDX Text(pDX, IDC NAME, m strName);
CDX Scroll(pDX, IDC-RELY, m_nRely);
DDX CBString(pDX, IDC.SXILL. m_strSkilI); IDDX*Text(pDX, IDC_SSN, ni_nSsn);
DDV_MinMaxInt(pDX, m_nSsn, 0, 999999999); DDX CBString(pDX, IDC EDUC. m strEduc);
//hafx_data_map
DoDataExchdngen ^ункщки. DDX_ (обмен) nDDV_ (проверка достоверности) — «двусторонние». Если ^^«геОлйявызывается с параметром FALSE, то она переносит данные из переменных-членов в элементы управления диалогового окна. Если же она вызывается с параметром TRUE, то передает данные из элементов управления в переменные-члены. DDX_Textne- реопределяется для подстройки под множество типов данных.
Функция EndDiaiogHrpaeT главную роль в процедуре завершения диалогового окна. DoMo- dai возвращает параметр, передаваемый в EndDiatog ZOOK принимает данные из диалогового окна, a IDCANCEL отменяет диалоговое окно.
СОВЕТ Вы можете написать собственные DDX-функции и подключить их к Visual C++. Это бывает весьма удобно, если во всей своей программе Вы пользуетесь уникальным типом данных. Подробнее см. MFC Technical Note #26 в интерактивной справочной системе.
Усовершенствование программы ЕХ06А
Программа ЕХОбА, обладая массой функциональных возможностей, не требовала от программиста особых усилий. Теперь создадим новую версию с расширенной функциональностью. Избавим ЕХОбА от скверной привычки завершать свою работу при нажатии клавиши Enter и введем поддержку элементов управления «полосы прокрутки*.
Перехват контроля при выходе по ОпОК
В исходной программе ЕХОбА виртуальная функция CDiaiog::OnOKобрабатывала щелчок кнопки ОК, что запускало процесс обмена данными и процедуру завершения диалогового окна. Как оказалось, клавиша Enter давала тот же результат — может быть, это как раз то, чего Вы хотели, а может быть, и нет. Если пользователь нажмет клавишу Enter, скажем, в поле ввода Name, его немедленно «выбросит» из диалогового окна. Вряд ли это ему понравится.
Что же происходит? В момент нажатия этой клавиши Windows ищет кнопку, на которой установлен фокус ввода (input focus) — на экране он выглядит как пунктирная рамка. Если фокус не установлен ни па одну кнопку, Windows ищет указанную программой или в ресурсе кнопку по умолчанию (default pushbutton) — у такой кнопки более толстые границы. Если такой кнопки нет, вызывается виртуальная функция ОпОК — даже если кнопка ОК отсутствует.
Клавишу Enter можно отключить, просто написав ничего не делающую функцию СЕхОба- Dialog;;O»OKH добавив код завершения в новую функцию, реагирующую на щелчок кнопки ОК. Последовательность действий такова.
С помощью Classwizard сопоставьте кнопку ШОК виртуальной функции ОпОК
В ClassWizard выберите 1DOKв списке Object IDs для GEx06aDialog, а затем дважды щелкните BN_CUCKED. В результате будут сгенерированы прототип и заготовка для ОпОК.
Используя редактор диалоговых окон, измените идентификатор кнопки ОК. Выберите кнопку ОК, измените ее идентификатор с JDOK на IDC_OK и отмените у нее свойство Default Button. Оставьте только функцию ОпОК
С помощью ClassWlzard. создайте функцию-член OnClickedOk. Эта функция-член класса СЕх0баО1а1о§играет ключевую роль в обработке сообщения BN_CLICKED от только что переименованной кнопки IDC ОК.'
Отредактируйте тело функции OnChckedOkB ex06aDialog.cpp. Она вызывает функцию ОиС^базового класса, как это делала исходная функция CEx06aDialog::OnOK.BoTee код void CEx06aDlalog:: OnClictedOkO
* TRACE("CEx06aDialog::OnClickedOk\n");
CDialog::OnQK();
}
Отредактируйте исходную функцию О«ОКв ex06aDialog.cpp. Эта функция — «пустой» обработчик кнопки с прежним идентификатором ЮОК. Модифицируйте код следующим образом:
void CEx06aDlalog::ОпОК()
{ // заглушка для функции ОпОК - НЕ вызывайте CDialog: :ОпОК() TRACE("CEx06a0ialog::OnOK\n");
}.
Соберите и протестируйте приложение. Попробуйте теперь нажать клавишу Enter, Ничего произойти не должно, но в окне Debug появится вывод оператора TRACE. Однако щелчок кнопки ОК должен, каки раньше, завершать диалоговое окно.
^■Для'тех, кто программирует в Win32
,^ Элементы управления передают своим род^льстош Ma.|!^l|j^
у 7 соободагия (пойЙЬаНр^jS^^QM^M^b; 'Например Ъри одцнощгрм поляке t it
L' кнопки мышью младшие Ш бйТб'в wPcram содержат идентификатор кнопки, старшие!- ^ ^ 16 бигбв 'sParam — код уведомления BN„CU6KEp,’ а Ы?гЙИ&?^ описатель кнош^' 11оЙ£И ^ Г^шнство "ррргда^, испрлыующих Wn^oc, обрабйтъ^т дЙвложвнным оператором йй%Ь?ЙЕОсглажйвйЬт?
: мая уведомляющие сообщения от элементов y<ip8tig$li№^irixfc№&?|i*|,
солений.
ji «ClassWi&ird генерирует samfc» для уведомлений в таблице сообщений гщимерно так S’[ -‘осад авжвдждейй^^
^* JM^W^O№^- ^^^е8и№рЬ1(П1с^.-; < £Щ ( ^ Ч ^• v; 17 . ); \; ’ <;4;
* . События, связанные с ЗЙющкамй, обладают особым статусом, поск^ль^-^онй генери- j =
1 f; руют'мо^йдяй^ ййЙ^й^б^Ь^ЙйаМ messages) ^ если ^ j&tEteT^ класса диалогового .1Г ■4*екна нет обработчиков уВ^^^йий, аналогичных иокааантшм выше. Как поясняется ^ ■^в главе. 13, каркас%1Й^НЙОффЖ^«направляет* такие кбманжЙе сообцрни^различным а ? объектам в вашем; у^гр^^^г^^^дрмлбнйя от элементов )^'^алию; ^г
р'' тавить с более уйй^1^%^<й| (^^СОММА^Й-зайис^'в’ таЪлищ’ <^^щений:<; \ ii^ ?
v* 0И^!ЙЙ^К1Нё<:4Й|^ • " У ■ ^ ; Ь^ .:: ^61 ^|0^« ^ ^ ; V^ rN Н Й VI !i. » ^ этом случае ф^нйтЫ^ЗЙйШ^Й не отличит Ьдин^н^Й|еЛч6к на кнога® """oT’^oft^'; , /ного, но это не пробдюма, потому что дф>Йш4б Hcft^kHjaiono^^Wirftlpws-iij^r^ “ 4 обычно НС испрльзуюфсязй £:У^J *1 , ч -И £.;-#^ i^y fr ),t г v.-1
Обработка OnCancel
Также, как нажатие клавиши Enter приводило к вызову ОпОК, нажатие клавиши Esc инициирует вызов OnCancel, в результате чего диалоговое окно завершается с кодом возврата 1DCAN-
CEL от функции DoModai. Программа ЕХОбА не предусматривает особой обработки IDCANCEL, поэтому нажатие клавиши Esc (или щелчок кнопки Close) закрывает диалоговое окно. Вы можете обойти этот процесс, применив функцию-заглушку ОпСстсе! по аналогии с тем, что уже делалось для кнопки ОК
Подключение элементов управления «полосы прокрутки»
Графический редактор позволяет размещать в диалоговом окне элементы управления «полосы прокрутки», a ClassWizard — создавать для них целочисленные переменные-члены. Чтобы полосы прокрутки Loyalty и Reliability заработали, надо дополнить программу соответствующим кодом..
Эти элементы управления позволяют считывать и записывать текущую позицию движка и границы заданного диапазона. Если Вы установили диапазон, скажем, от О до 100, то соответствующая переменная-член со значением 50 поместит движок в центр полосы прокрутки. (Функция CScrollBar-SetScrollPosroxB задает позицию движка на полосе прокрутки.) Когда пользователь перемещает движок или щелкает стрелки, полоса прокрутки отправляет в диалоговое окно сообщения WM_HSCROLL и WM_VSCROLL Обработчики сообщений в диалоговом окне расшифровывают эти сообщения и соответственно изменяют позицию движка на полосе прокрутки.
Особенность элементов управления «полоса прокрутки» в том, что все горизонтальные полосы посылают одно сообщение — WM HSCROLL, а все вертикальные — WM VSCROLL. А раз в нашем «навороченном» диалоговом окне две горизонтальные полосы прокрутки, значит, один-единственный обработчик сообщения WM_HSCROLL должен как-то различать, какая из них прислала сообщение.
Добавим в программу ЕХОбА логику управления полосами прокрутки.
Добавьте операторы епит, чтобы задать предельные значения диапазона прокрутки. Включите в файл ex06aDialog.h, в самое начало объявления класса, следующие строки:.
enum { nMin = 0 }:
епит { пМах = 100 };
Отредактируйте функцию O«InitDialog14ro6bi инициализировать границы диапазона прокрутки. Эта функция должна устанавливать минимальное и максимальное значения диапазона прокрутки так, чтобы переменные-члены CExO6aD:atogoTpaxan.n величины, выраженные в процентах. Значение, равное 100, означает «установить движок в крайнюю правую позицию», а нулевое значение — «установить движок в крайнюю левую позицию».
Добавьте в файле ex06aDialog.cpp в функцию-член Оп/игбОдя/о^класса СЕхОб'яОгя/о^такой код:
CScrollBar* pSB = (CScrollBar*) GetDlgItem(IDC_LOYAL);
pSB->SetScrollRange(nMln, пМах):
pSB = (CScrollBar*) GetDIgItem(IDC_RELY);
pSB->SetScrollRange(nMin, nMax);
Используя ClassWizard, добавьте в CEx06aDialogp5pa6oT4HK сообщений от полос прокрутки. Выберите сообщение WM HSCROLL, а затем добавьте функцию-член Оп- HScroIL Введите код, выделенный полужирным:
void CEx06aDialog: :OnHScroll(UINTnSBCode, UINT nPcs, CScrollBar* pScrollBar)
int nTempI, nTemp2;
nTempI = pScrol7lBar->GetScrollPos():
cm. след. cmp.
switch(nSBCode) {
case SBJTHUMBPOSITION:
pScrollBar->SetScrollPos(nPos);
break;
case SB_UNELEFT: // кнопка co стрелкой влево nTemp2 = (nMax- nMin) / 10;
if ((nTempI - nTemp2) > nMin) {
nTempI -= nTemp2;
else {
nTempI = nMin',
pScroll8ar->SetScrollPos(nTempI);
break;
case SB_LINERIGHT: // кнопка co стрелкой вправо nTemp2 = (nMax - nMin) / 10;
. if ((nTempI+ nTemp2) < nMax) {
nTempI += nTemp2;
else {
। nTempI = nMax;
pScrollBar->SetScrollPos(nTempI);
break;
ПРИМЕЧАНИЕ Функции, связанные с полосами прокрутки, пользуются 16-разрядными целыми числами и для позиции движка, и для границ диапазона прокрутки.
Соберите и протестируйте приложение. Вновь соберите и запустите программу ЕХ06А. Ну как, полосы прокрутки на этот раз работают? Движки должны перемещаться, когда Вы щелкаете стрелки на полосах прокрутки, а также подчиняться перетаскиванию мышью. (Обратите внимание: пока в программу не включена логика, позволяющая реагировать на щелчок пользователем самой полосы прокрутки.)
Доступ к элементам управления: CWnd-указатели и идентификаторы
Размечая диалоговый ресурс в графическом редакторе, Вы определяете элементы управления при помощи идентификаторов, таких, как IDG_SSN. Однако в программном коде бывает необходим доступ к стоящему за элементом управления оконному объекту. Поэтому в MFC-библиотеке предусмотрена функция CWnd::GelDlgItem,преобразующая идентификатор в CWnd- указатель. Вы уже видели такое преобразование в функциях-членах OnlnitDialoga GnClickedGk класса CExOGaDialog. Каркас приложений «создавал» этот <7№й#-укаэатель, потому что там не было вызова конструктора для объектов управления. Этот указатель временный, и сохранять его нельзя.
СОВЕТ Чтобы преобразовать CWnd-указатель в идентификатор элемента управления, воспользуйтесь функцией-членом GetDIgCtrllD класса CWnd.
Выбор фона диалогового окна и цвета элементов управления
Чтобы изменить фон отдельных диалоговых окон или конкретных элементов управления в каком-либо диалоговом окне, придется потрудиться. Каждый элемент управления, прежде чем появиться на экране, посылает родительскому диалоговому окну сообщение WM_CTr.COT.OR. Такое же сообщение отправляется и самому диалоговому окну. Если написать в производном
классе диалогового окна обработчик этого сообщения, можно установить цвет текста и его фон, а также выбрать кисть для заполнения нетекстовой области элемента управления или диалогового окна.
Вот пример функции OnCtlColor, задающей желтый фон для всех полей ввода и красный фон для диалогового окна. Переменные mJsYellowBrusby. m_bRedBrush ~ это переменные- члены типа HBRUSH, которые инициализируются в OnlnitDialog. Параметр пС1гСо1огопреда- ляеттип элемента управления, a pWnd— конкретный элемент управления. Чтобы установить цвет отдельного поля ввода, нужно преобразоватьpWnd в идентификатор дочернего окна и проверить его.
HBRUSH CMyDialug::OnCtlColor(CDC‘ pDC, CWncb pWnd, MINT nCtlColor) {
if (nCtlColor == CTLCOLOH_EDIT) {
pDC->SetBkColor(RGB(255, 255, 0)); // желтый return m hYcllowBrush;
if (nCtlColor == CTLCOLOR.DLG) {
pDC->SetBkColar(RGB(255, 0, 0));// красный
return mJiRedBrJsh;
!
return CDialog; :OnCtlColor(pDC, pWnd, nCtlColor);
}
ПРИМЕЧАНИЕ Диалоговое окно не помещает WM^CTLCOLOR в очередь сообщений; вместо этого оно — чтобы немедленно отправить сообщение — вызывает VVin32-фyнкцию SendMessage. Благодаря этому обработчик сообщения может вернуть параметр, в данном случае описатель кисти. Это не MFC- объект CBrush, а Win32 HBRUSH. Создать кисть можно, вызвав Win32-фyнкции CreateSolidBrush, CreateHatchBrush и т. д.
Вывод графики в диалоговом окне
Графику можно выводить прямо в клиентскую область диалогового окна, но накладки на диалоговые элементы не произойдет, только если рисовать внутри окна какого-либо элемента управления. Если Вы хотите вывести лишь текст, создайте с помощью графического редактора пустой статический элемент управления (пустую метку) с уникальным идентификатором, а затем, чтобы поместить текст в элемент управления, вызовите функцию CWnd;:Set- DlgftemText^ функции-члене диалогового окна, например, в OnlnitDialog.
Вывод собственно графики сложнее. Вы должны с помощью ClassWizard добавить к диалоговому окну функцию-член OnPaint, которая преобразует идентификатор статического
5 Зак. i^ 78"
элемента управления в CWnrf-указатель, и получить его контекст устройства. Весь «фокус > заключается в том, чтобы рисовать в поле элемента управления и в то же время не дать Windows впоследствии перезаписать вашу работу. Это достигается за счет последовательных вызовов invalidate/UpdateWindouiB3msH.wte на программный код функции OnPaint, которая рисует в статическом элементе управления маленький черный квадрат:
void CMyDialog:;OnPaint()
CWnd* pWnd = GetDlgItem(IDC_STATICl); // IDC_STATIC1 определен в графическом редакторе
CDC* pControlDC = pWnd->GetDC();
pWnd->Invalidate();
phnd->Updet eW:.ndow();'
pCo11го1DC->S e1setStockObj ect(ВLACK_BRUSH);'
pControlDC->Rectangle(0, 0, 10, 10);// маркер в виде черного квадратика
pWnd->RcleaseDC(pControlDC);
}■
Как и в случае с любым окном, функция OnPaintдиалогового окна вызывается, только если часть окна объявлена недействительной. OnPaint можно вызвать принудительно из другой функции-члена объекта диалогового окна, применив оператор:
1nvalidate();
Добавление элементов управления в период выполнения
Мы уже видели, как на этапе проектирования с помощью редактора диалоговых окноп создаются элементы управления диалогового окна. Если Вам надо добавить элемент управления в период выполнения, придерживайтесь следующей схемы.
Добавьте в свой класс «диалоговое окно» переменную-член для внедряемого элемента управления. MFC-классы таких элементов управления включают CButton, CEdit, CListBoxn CComboBox. Внедряемые C++-объекты конструируются и уничтожаются вместе с объектом «диалоговое окно».
Выберите из меню View в Visual C++ команду Resource Symbols. Добавьте константу-идентификатор нового элемента управления.
С помощью ClassWizard создайте обработчик сообщения WM INITDIALOG, переопределив тем самым CDialog::OnInitDiatog.9Ta функция должна вызывать функцию-член Create внедряемого элемента управления. В результате такого вызова в диалоговом окне появится новый элемент. Windows уничтожит окно этого элемента управления при уничтожении диалогового окна.
Добавьте вручную в производный класс диалогового окна обработчики уведомляющих сообщений, необходимые новому элементу управления.
В главе 13 попробуем в период выполнения добавить в диалоговое окно поле ввода с форматированием (rich edit control).
Использование других свойств элементов управления
Вы видели, как путем добавления кода в функцию-член OnlnitDialogKitacca «диалоговое окно» настраивается класс элементов управления CScrollBar. Другие элементы управления можно программировать аналогично. Посмотрите в справочнике Microsoft Foundation Class Library Reference список классов, относящихся к элементам управления, в частности, обратите внимание на классы CListBox и CComboBox. Каждый из них обладает рядом свойств, которые ClassWizard напрямую не поддерживает. Обычные и некоторые комбинированные списки, например, поддерживают выделение сразу нескольких строк. Если Вы хотите воспользоваться этими свойствами, не пытайтесь добавлять соответствующие переменные-члены с помо-
шью ClassWizard, а определите их сами и дополните функции OnlnilDialogH. OnCUdaetlOh своим кодом, отвечающим за обмен нужной информацией.
; ? Для тех,' кто. программ ир^ъ в Win32? .£
$$гё той родательйкйе' ся^'^вд^ТСЯ Ы_
*|гаййе фу^щц^я; как <$й$^^^вйдати ее jto^pB ВйавВМЪ^
ЧЙИЙ#е’ передают сообщений, поскольку онй" (функции) ’применяются ко
Г ОКОН. '■■
"'S • * t •
Стандартные элементы управления Windows
Элементы управления, использованные в ЕХ06А, легко программируются и дают базовое представление об элементах управления вообще. Теперь Вы готовы к изучению более интересных элементов. Рассмотрим некоторые, весьма важные стандартные элементы управления, впервые появившиеся в Windows 95, а теперь доступные и в Windows NT. В их числе индикатор продвижения (progress indicator), ползунок (trackbar), наборный счетчик (spin button control), графический список (list control) и древовидный список (tree control).
Код этих элементов управления содержится в файле Windows COMCTL32.DLL Он включает оконные процедуры, а также код, регистрирующий оконный класс для каждого элемента. Регистрирующий код вызывается в момент загрузки DLL-модуля. Инициализируя диалоговое окно, ваша программа использует символьное имя класса в диалоговом ресурсе для подключения к оконной процедуре в DLL Таким образом, она завладевает окном элемента управления, код которого остается в DLL За исключением элементов управления ActiveX, большин- iCibo элементов управления работают именно так.
Эти только что упомянутые элементы управления используются в примере ЕХ06В. Формируемое в нем диалоговое окно показано на рисунке 6-2. Сверяйтесь с ним, когда будете читать описание задействованных в программе элементов управления.
Рио. 6-2. Стандартные элементы у правления WindowsРио. 6-2. Стандартные элементы у правления Windows
Рио. 6-2. Стандартные элементы у правления Windows
Рио. 6-2. Стандартные элементы у правления Windows
Вы должны понимать, что ClassWizard не поддерживает генерацию переменных-членов для стандартных элементов управления. Для инициализации и чтения данных, относящихся к этим элементам управления, Вам надо добавить код в свои функции OnlnitDictiogu ОпОК. Однако ClassWizard помажет Вам создать обработчики уведомляющих сообщений от стан
дартных элементов управления.
Элемент управления «индикатор продвижения» 1
Индикатор продвижения — самый простой в программировании стандартный элемент управления; его представляет MFC-класс GProgressCtrl Обычно его используют только для вывода информации. Этот элемент вместе с ползунком может успешно заменить полосы прокрутки, которые мы рассмотрели в предыдущем примере. Для инициализации индикатора продвижения вызовите в OnlnitDialog функции-члены SetRangen SetPos; после этого Вы сможете при необходимости-вызывать SetPos из обработчиков сообщений. Индикатор продвижения на рисунке 6-2 имеет диапазон значений от 0 до 100 (это диапазон по умолчанию).
Элемент управления «ползунок»
Ползунок (класс CSliderCtft)позволяет вводить «аналоговые» значения. (Ползунки были бы. пожалуй, очень уместны в полях Loyality и Reliability примера ЕхОбА.) Если Вы зададите для этого элемента управления большой диапазон (например, от О до 100 или более), ползунок будет перемещаться очень плавно, а если малый (скажем, от 0 до 5) — ползунок будет двигаться скачками. Программным путем можно создать шкалу, деления которой соответствуют шагу (дискретности) приращения. Именно в таком режиме работает ползунок, позволяющий в окне свойств экрана выбирать текущее разрешение. Диапазон по умолчанию у ползунков отсутствует.
Ползунок программировать легче, чем полосу прокрутки, так как не надо разбирать сообщения WM_HSCROLL или WM_VSCROLL в классе «диалоговое окно». После установки диапазона ползунок можно перемещать мышью, или щелкая де ления шкалы. Тем не менее, если Вы хотите отображать значения, связанные с положением ползунка, в другом элементе управления, работать с сообщениями о прокрутке все же придется. Текущее положение ползунка возвращает функция-член GetPos.
В диалоговом окне на рисунке 6-2 верхний ползунок плавно перемещается в диапазоне от О до 100, а нижний — дискретно в диапазоне отО до 4, и эти индексы связаны со значениями двойной точности (4.0, 5.6, 8.0, 11.0 и 16.0).
Элементуправления «наборный счетчик» .
Наборный счетчик (класс CSpiiiBtUtonCtrty- это вариант полосы прокрутки, который часто используется совместно с полем ввода. Поле ввода, расположенное слева от наборного счетчика (при переключении между элементами управления клавишей Tab Вы сначала попадаете в это поле ввода, а потом в сам счетчик), иногда называют его «приятелем» («buddy»). Суть работы данного элемента в том, что пользователь, поместив курсор на счетчик и удерживая левую кнопку мыши, можетувеличивать или уменьшать («набирать») число в поле ввода. Скорость «набора* тем выше, чем долблю удерживается кнопка мыши.
■ Если приложение оперирует в поле ввода с целыми числами, можно полностью обойтись без программирования на C++. Просто закрепите за полем ввода (с помощью ClassWizard) целочисленную переменную-член и не забудьте установить диапазон значений счетчика в функции OnlnitDialog. (Скорее всего, Вам не понравится диапазон наборного счетчика по умолчанию, который простирается от минимального значения 100 до максимального 0.) Не забудьте пометить на вкладке Style наборного счетчика свойства Auto Buddy и Set Buddy Integer. Для изменения диапазона и схемы ускорения «набора» значений можете вызывать из OnlnitDialog функции-члены SetRange и SetAccel.
Чтобы поле ввода отображало нецелые значения (например, показания времени или числа с плавающей точкой), надо создать обработчик сообщения WMVSCROLL (или WMJTSC- ROLL) от наборного счетчика. Ваш обработчик должен преобразовывать целые значения наборного счетчика в числа, отображаемые в поле ввода.
Элемент управления «графический список» .
К этому элементу управления (класс CListCtrl) прибегают, если нужен список, способный выводить не только текст, но и графику. На рисунке 6-2 он отображает список с маленькими значками. Элементы располагаются столбцами, поэтому данный элемент управления включает горизонтальную полосу прокрутки. Когда пользователь выбирает какой-нибудь элемент, посылается уведомляющее сообщение, которое Вы должны обработать в своем классе «диалоговое окно». Обработчик сообщения определяет, какой именно элемент выбрал пользователь. Элементы идентифицируются целочисленными индексами, начиная с О.
И графический и древовидный списки получают выводимые ими картинки от стандартного элемента управления — списка изображений (image list) (класс <7msgeZ./s()Bama программа формирует список изображений из значков или растровых изображений, а затем передаст элементу управления «графический список» указатель на этот список. Формировать список лучше всего в функции QnlnitDialog, там же можно определить и нужные текстовые строки. Вставить элемент в список можно при помощи функции-члена Insertltem.
Программирование графического списка — процесс несложный, если ограничиться в нем только строками и значками. Если же Вы хотите обеспечить поддержку drag-and-drop или включить в список более сложную, определенную самим пользователем графику, придется потрудиться.
Элемент управления «древовидный список»
Вы уже знакомы с этим элементом управления, если работали с программой Windows Explorer (Проводник) или окном проекта в Visual C++. MFC-класс CTreeCtrl позволяет легко добавить в вашу программу такие же возможности. На рисунке 6-2 приведен древовидный список, который показывает состав современной американской семьи. Пользователь может раскрывать и свертывать элементы, щелкая кнопки + и - или дважды щелкая сами элементы. Значок, расположенный рядом с каждым элементом, программируется так, чтобы вид его изменялся, когда элемент выбран одинарным щелчком.
Графический и древовидный списки имеют некоторые общие черты: они могут работать с одним и тем же списком изображений и совместно использовать одни и те же уведомляющие сообщения. Однако методы идентификации элементов у них разные. В древовидном списке вместо целочисленных индексов применяют описатель Ш'КЕЕГГЕМ, Чтобы вставить новый, элемент, Вы обращаетесь к функции-члену Insertitem, но перед этим Вы должны сформировать структуру TV INSERTSTRUCTs-cnopaa (помимо всего прочего) идентифицирует строку, индекс в списке изображений и описатель родительского элемента (NULL для элементов верхнего уровня). .
Каки графические, древовидные списки предоставляют безграничные возможности настройки. Можно, например, разрешить пользователю редактировать, вставлять и удалять элементы. '
Сообщение WM_NOTIFY
Раньше элементы управления Windows посылали свои уведомления в сообщениях WM_COM- MAND. Однако стандартных 32-разрядных параметров wParam и IParam недостаточно для передачи всей информации от стандартных элементов управления их объектам-родителям. Корпорация Microsoft решила проблему «пропускной способности», введя новое сообщение WM_NOTIFY. При отправке такого сообщения wParam содержит идентификатор элемента управления, a IParam служит указателем на структуруNMHDR,модифицируемую данным элементом управления. Эта структура определяется на языке С следующим образом:
typedef struct tagHMHDS {
HWND hwndFro’; // описатель элемента управления, отправляющего сообщение
см. след. стр.
UINT idFrom; // идентификатор этого элемента
UINT code; // специфичный для этого элемента код уведомления } NMHDR;
Однако многие элементы управления посылают сообщения WM_NOTIFYс указателями на структуры более крупные, чем ММНОР.Эти. структуры содержат не только показанные выше три поля данных, но и дополнительные поля, специфичные для конкретного элемента управления. Например, во многих уведомлениях от древовидных списков передается указатель на структуру АИ/_Т^ЕИЕ^родержащую структуру TVjTEMi другие данные. Поэтому, сопоставив обработчику сообщение WM_NOTIFY, ClassWizard генерирует указатель на соответствующую структуру.
Пример ЕХ06В
Мы не станем изобретать реальную программу, в которой используются все элементы управления — просто включим их в модальное диалоговое окно. Надеемся, Вы увидите, что в нем происходит. Процесс сборки программы описан ниже. Начиная с п, 4, инструкции больше ориентированы на конкретные элементы управления, чем на компоненты Visual C++, которыми Вы будете пользоваться.
Запустите AppWizard, чтобы сгенерировать проект ЕХОбВ. Выберите из меню File в
Visual C++ комавду New, а затем — строку Microsoft AppWizard (еж) на странице Projects. Примите параметры по умолчанию, но выбери те Single Document Interface и откажитесь от Printing And Print Preview. На рисунке показ аны параметры и имена классов по умолчанию.
2. Создайте новый диалоговый ресурс с идентификатором ZDD_Z>I4ZOG/Разместите элементы управления, как показано на рисунке 6-2.
Элементы можно перетаскивать мышью с палитры элементов управления. В таблице перечислены типы элементов управления и их идентификаторы:
Порядок обхода
Типэлемента
Идентификатор дочернего окна
1
статический текст
ЮС _ STATIC
2
индикатор продвижения
ЮС-PROGRESS 1
3
статический текст
ЮС STATIC
4
ползунок
IBC- TRACKBARI
5
статический текст
IBC STATIC TRACK1
6
' статический текст
IDC STATIC
Порядок обхода
Тип элемента
Идентификатор дочернего окна
7
ползунок
IDC_TRACKBAR2
8
статический текст
1DC_$TA *ПС_ TRACK2'
9
статический текст
1DC_STATIC
10
поле ввода
iDC_BUDDY_S14NT'
И
наборный счетчик
IDCJPINl'
12
статический текст
1DC_STAT1C
13
статический текст
1DC_STA77C
14
графический список
IDCJJSTVIEWI'
15
статический текст
1DC_STAT1C_USTVIEW1
16
статический текст
JDCJTAT1C■ :■
17
древовидный список
IDC_TREEVIEW1
18
статический текст
IDCSTATIC 'Ji'REEVlEWl
19
командная кнопка
IDOK,
20
командная кнопка
iDCANCEL
О других свойствах пока не беспокойтесь — Вы установите их на следующих этапах. (Некоторые элементы управления могут выглядеть несколько иначе, чем на рисунке 6-2, пока Вы не установите их свойства.)
С помощью Classwizard создайте новый класс, CEx06bDialog,производный от CDialog. ClassWizard автоматически предложит создать этот класс, поскольку знает, что ресурс IDDJDIALOGI не сопоставлен С++-классу. Создайте обработчики сообщений WMIN- ITDIALOG, WM_HSCROLL и WM^VSCROLL'
Запрограммируйте индикатор продвижения. Так как Class Wizard не генерирует пе-- ремештую член для этого элемента управления, сделайте это вручную. Добавьте в заголовок класса CExOGbDialogpmKpbimyio (public) целочисленную переменную-член m_rtProgress и присвойте ей в конструкторе значение 0. Кроме того, добавьте в функцию-член Onlnit- Dialog следующий код:
. CProgressCtrl* pProg = (CProgressCtrl*) GetDlgItem(IDC_PR0GRESS1); .
pProg->Setfiange(O, 100);
pProg->SetPos(m_nProgress);
Запрограммируйте «непрерывный» ползунок. Добавьте в заголовок класса СЕхОбЪ- Diatog открытую целочисленную переменную-член m_nTrackbarl и присвойте ей в конструкторе значение 0. Затем дополните функцию-член OnfnitDialogследующим кодом;
CString strTextl;
CSliderCtrl* pSlidel = (CSliderctrl*) Get0lgItem(I[X)„TRACK8AR1); pSlide1->SetRange(0, 100);
' pSlide1->SetPos(m_nTrackbar1);
strTextl. Format("lid", pSlidel->GetPos());
SstDlgItemText(IDC_STATIC_TRACK1. strTextl);
Данный код устанавливает для ползунка диапазон, а также начальное положение по значению переменной-члена; в соседнем поле статического текста задается число, соответствующее положению ползунка.
Чтобы можно было обновлять статический текст в соответствии с текущим положением ползунка, создайте обработчик сообщения WM_HSCROLL, которое ползунок передает объекту «диалоговое окно». Вог как выглядит кед такого обработчика:
void CExOGMialog: :OnH$c'oll(UINT nSBCode, HINT nPos, CScrollBar* pScrollBar) {
CSliderctrl* pSlide = (CSliderCtrl*) pScrollBar;
CString strText;
strText.Format("%d", pSlide->GetPos());
SetOlgIteiaText(IOC_STATIC_TRACK1, strText);
Наконец, Вам надо будет обновлять переменную-член т_п1Уас1^>аг1, когда пользователь щелкнет кнопку ОК. Так и тянет включить этот код в обработчик кнопки ОпОК. Но тогда, если в каком-нибудь из других элементов диалогового окна выявится ошибка при проверке достоверности введенных данных, Вам не избежать проблем. Обработчик установит т_пТгаскЬаг1лдаже если пользователь «отменит» диалоговое окно. Поэтому код следует вставить в функцию DoDatoExcbange. И если Вы самостоятельно проверяете достоверность введенных данных, то, обнаружив ошибку, вызовите CDataExcbange::Fail— она уведомит пользователя о допущенной им ошибке.
if (pDX->m_bSaveAndValidate) {
TRACE("обновление переменных-членов для “ползунков"\п");
CSliderctrl* pSlidel =.
(CSliderctrl*) GetDlgItem(IDC_TRACKBAR1);
m nTrackbarl = pSlidel->GetPos();
}
Запрограммируйте «дискретный» ползунок. Добавьте в заголовок класса CEx06bDialog открытую целочисленную переменную-член т_пТтаскЬаг2^ обнулите ее в конструкторе. Эта переменная-член служит индексом для dValue — закрытой статической переменной- члена, которая представляет собой массив чисел (4-0, 5.6,8.0, 11,0 и 16.0). Определите dValue в файле ex06bDialog.h как закрытый статический массив чисел двойной точности, а в файл ex06bDiaiog.cpp добавьте следующую строку:-
double CEx06b0ialog::dValue[5] = {4.0, 5.6, 8.0, 11.0, 18.0);
Потом дополните функцию-член OnlnitDialog кодом, определяющим диапазон ползунка, разметку шкалы и начальное положение:
CString strText2;'
'CSliderctrl* pSlide2 = (CSliderCtrl*) GetDlgItem(IDC_TRACKBAR2);
pSlide2->SetRange(0. 4);
pSlide2->SetTic(l);■
pSlide2->SetTic(2);
pSlide2->SetTlc(3);
pSllde2->SetPos(m_nTraokbar2);.
strText2.Format("%3.1Г, dValue[pSlide2->GetPos()]);
SetDlgItemText(I0C_STATlC_TRACK2, struaxt2);
Если бы у Вас был только один ползунок, обработчик сообщения WM_HSCROLL с шага 5 вполне мог бы сработать Но в программе два ползунка посылают одно и то же сообщение WM_HSCROLL, и обработчик должен разобраться в них. Код следует изменить так: void CEx06b0ialog: :OnHScroll(UINTnSBCode, UINT nPos, CScrollBar* pScrollBar) {
CSliderCtrl* pSlide = (CSliderCtrl*) pScrollBar;
CString strText;
,// два ползунка посылают сообщения
// HSCfiOLL (нужна разная обработка)
awitch(pScrollBar->GetDlgCtrlID()) {
case IDC_TRACKBAR1:
strText. Format ("%d”, pSlide->GetPos());
SetDlgItemText(IDC_STATIC_TRACK1, strText);•
break;
case IDC_TRACKBAR2:
CSliderCtrl* pSlide = (CSliderCtrl*) pScrollBar;
strText. Formate"X3.1f", dValue[pSlide->GetPos()]);
SetDlgItemText(IDC_STATIC_TRACK2, strlisxt);
break;
}
Б данном случае нужна шкала с делениями, поэтому пометьте в свойствах этого элемента управления (в редакторе диалоговых окон) параметры Tick Marks и Auto Ticks. Последний параметр заставит ползунок пометить делениями каждое приращение. Для этого ползунка справедливы те же рассуждения по поводу проверки данных, что и для предыдущего. Поэтому вставьте в функцию-член DoDataExcbange класса «диалоговое окно» такой код CSliderCtrl- pSlide2 = (CSliderCtrl*) GetDlgItem(IDC_TRACKBAR2);
m_nTrackbar2 = pSlide2->GetPos();
С помощью редактора диалоговых окон установите у обоих ползунков свойство Point в Bottom/Right, а свойство Align Text элементов управления IDCjTRACKBARln IDCJERACK- BAR2 - в Right."
Запрограммируйте наборный счетчик. Наборный счетчик связан со своим «приятелем» — полем ввода, всегда предшествующим счетчику при перемещении между элементами управления клавишей Tab. Добавьте через ClassWizard переменную-член двойной ■ точности яг_^/мидля поля ввода IDC_BUDDY_SPINI"№s. используем тип double "вм&сго int, потому что последний не требует никакого программирования и задача была бы чересчур простой. Мы хотим установить диапазон поля ввода от 0.0 до 10.0, но сам счетчик оперирует только с целыми числами. Добавьте следующий код в OnlnitDialog, чтобы установить диапазон его значений от 0 до 100 и присвоить ему начальное значение r&dSpin *10.0: CSpinButtonCtrl* pSpin = (CSpinButtonCtrl*) GetDlgItee(IDC_SPIN1);
pSpin->8etRange(0, MO);
pSpin->SetPos((int) (m_dSpin* 10.0));
Чтобы текущее значение счетчика отражалось в связанном с ним поле ввода, нужно предусмотреть обработчик сообщения WMJVSCROLL, которое счетчик отправляет объекту «диалоговое окно». Вот этот иод.
void CExOBbDialog: :OnVScroll(UINT nSBCode, UINT nPos,
CScrollBar* pScrollBar)
if (nSBCode == SB_ENDSCBOll) {
return; // Отбрасываем ненужные сообщения
. см. след. стр.
И Process scroll messages from IDC_SPIN1 only
if (pScrollBar->GetDlgCtrlID() = IDC_SPIN1) { CStrlng strValue;,
. strValue.Format("43.If", (double) nPos / 10.0);
((CSpinButtonCtrl*) pScrollBar)->GetBuddy()->SetWindowText(3trValue);
}
Вводить какой-то код в ОпОКне требуется, поскольку содержимое поля ввода обрабаты- ваеткод, отвечающий за обмен данными в диалоговом окне. В редакторе диалоговых окон установите у наборного счетчика свойство Auto-Buddy, а у связанного с ним поля ввода — свойство Read-only.
у 5 Несколько слов о значках-:; ; ’ X'\; |J Уjb:.:' ' Т
; ^ ^роягно^ Вы знаете, что растровое йзрбражфщгеу йлй р^ — это массив бигов, пред-
‘ ^ ставляющих пикселы на экране (Подробнее о растровых изображениях будет рассказано в главе 11.) Б Windows значок — это целый *цучс^ растров, Прежде всего, надо
$ донимать,iЧто ^аз|^цф>К размеры у значка Достигают^ затечёт’ применения разных - |раст^в^^Ьёр^^цй,^у{пхге:дставленйя мелкого жаЧга^одЬдьзуётся^йз^рЙжегпте ^|-;16х1^^^ЙЙ^!В, а^я крупного — 32x32. Ддш каждого размер^ф^естцуюетдна епдель-
§гых растра: один (4: бита/пиксел) для цветного й^^И^фйй й один монохромный (1
У ?би^/ййкс^ф,:какйа.ж»; Если, бит маски равен 0, соответствующий пиксел из ображвния $'.прс^^Щ^^ нейртор^^Если же биг маски равен X tq при черном (0) цвете
Х^иЗо^а^йи^дашпй’^а прибелом(0к?) ■£■ цвет фона в позиции этого
? /пикседш инв£]эт^|^^|^^ что Windows ,9^и .Endows. NT интерпретируют ин- чвершрованные Ц^^з: несколько иначе, чём Wmdoyra^l^ Й^^пикселы
'; на фбне рабочего 8чояйоказываются прбэра'фаы'ми; на'^^f окна Windows Explorer -~ i черными, а на фоне Графических^спийсов и да^вовцдй^псСписков ^ белыми. Только „ не ай^шу1ыйтёУ Д^е^^• '.4. >•■ ^ > i ■ |1 Г-J'П ■ И !' " '"
. ? ^ ’МалвЙкйв. ЗБачки :-т зйо нЬелвство, появившееся S Windows 95.1 Они используклся
Ha'fiaiismB задач'^|^|^ йгЖШЙсйв Explorer и в уже ^ц^^цу^^^Еемей^х управления, если’Вы эго предусмотрите. Если у значка пет растроврг'р Йз^б^аЯ^йгядля разме-
" г ра 1бх1б пикселов, rWindows^сформирует умрнбий^^й вариант ^маленький значок) из
X растра;р^|^М8^ 3^^но тогда он>уже да додучитм.таким ^(курд-ЕНЫН как •
" если бы Вы.ттарй<кгвг^ ё|^самос^ятельн6. ^.-]^ ,.^ щк ^г^й t=?. a j ^' -:■ . । * Zs? • . i . . Графический редакЕорЗД^врЛИёт ерздявать и @№ЙйВем@% значки. Вот как выгля-
' ДИТ' ДИТ«тигра 9W0^6--sN.^ < »У^ • ?3 ^-'H^f <г$ ^ННтН-^^Н ’
' ДИТ
' ДИТ
ЙО i&’&ii?■).<#* ^&*ад ’4 ы Ыi ■; ч£i•
И Щелкните скед чтобы сделать пикселы прозрачными,' с * £f ул> jp j I
И Щёлкните ордаё "чтобы едеЛаА пикселы Й1й8|^ирое8е1нб1|^
^ ^^> ’ ‘ к1 к» >■ I ч »£§ Г ^^H^iS^i" | ^Л--?
_ я_ я.* li;.* li;
_ я
_ я
.* li;
.* li;
Верхний квадратик в верхнем левом углу окна палитры показывает основной цвет , Хдля кистей, заливки Фщур й pppsgroi а квадратик, распблйжвшгый чуть ниже, указЫВа7 ^ёт на цвет гранщгфи^1>^ОёнЬвной цвет вбгбирается зделчкдмлёвой кнопки, -а цвет. ; * границ —..щелчком правой кнопки. Теперь £^|пите внимание на центр верхней части . ■■ ? окна палитры. ЗДе$$$$<герхдаго значка монитора позв0ляст^^ра'ц> прозрачные пик- д селы (изображаются темно-голубым цветом), а щелчок нижнего значка монитора — | инвертированное пикселы (изображаются красным Цйетйм);®^ q jj’' г - 8 -
Подготовьте список изображений. Этот объект необходим графическому списку и древовидному списку, а ему самому нужны значки (icons).
Итак, с помощью графического редактора включите значки в RC-файл проекта. На прилагаемом к книге компакт-диске эти значки представляют собой разноцветные окружности .с черными контурами. Если у Вас есть значки поинтереснее, воспользуйтесь ими. В этом примере идентификаторы ресурсов значков следующие.
Идентификатор ресурса
Файл значка
1DI_BLACK
Iconl
IDI_BLUE
IconS
FDijCYAN
Icons
IDIjGREEN
Icon?
1DIJPURPLE
Icon6
1D1_RED
Icon2
IDI-WHITE
IconO
1D1_ YELLOW
Icon4
Теперь добавьте в заголовок класса C"£x06bDf«/og переменную-член mJmageList класса
CImageLism включите в OnlnitDialog следующий код
HICON hlcon[8];'
int n;
m_ieageList.Create(16, 16, 0, 8, 8); //или 32, 32 для крупных значков
hfcon[0] = AfxGetApp()->LoadIcon(IDI^WHITE);
hlcon[1] = AfxGetApp()->LoadIcon(IDI_BLACK);
hlcon[2] = AfxGetApp()->LoadIcon(IDI_RED);
hlcon[3] = AfxGetApp()->LoadIcon(IDI BLUE);
hlcon[4] = AfxGetApp()->LoadIcon(IDI_YELLOW);
hlcon[5] = AfxGetApp()->LoadIcon(IDI_CYAN);
hlcon[6] = AfxGetApp()->LoadIcon(IDI PURPLE);
hlcon[7] = AfxGetApp()->LoadIcon(IDI_GREEN);
for (n = 0; n < 8; n++) {
m_imageList.Add(hIcon[n]);
}
Запрограммируйте графический список. В редакторе диалоговых окон установите атрибуты стиля графического списка, как показано па рисунке.
Кроме того, убедитесь, что на вкладке More Styles установлен параметр Border Затем введите в OnlnitDialog код:
static char* color[] = { "белый", "черный", "красный", "синий", "желтый", "бирюзовый”, "фиолетовый", "зеленый"};
CLlstCtrl» pList = (CListCtrl*) GetDlgIte«(IDC_LISTVIEW1);
pList->SetImageList(Am_imageLlst, LVSlL_SMALL);
for (n = 0; n < 8; n++) {
plist->ln3ertltem(n, color[n], n);
}
pList->Set8kColor(RGB(0, 255, 255)): //УЖАС!
pList->SetTextBkColor(RGB(0, 255, 255));
Как показывает последняя строка, со стандартными элементами управления сообщение WM_CTLCOLOR не используется — для установки цвета фона Вы просто вызываете специальную функцию. Однако, как Вы увидите, запустив программу, прозрачные пикселы значков выглядят весьма убого.
Если Вы воспользуетесь ClassWizardдля создания обработчика уведомляющего сообщения LVN ITEMCHANGED от графического списка, то смажете отслеживать выбор элементов пользователем. Следующий обработчик показывает текст выбранных элементов списка в статическом элементе управления (метив):
void CWln950ialog: :OnItemchangedListview'(WIIDR* pNMHDR, LRESUI.T* pfiesu'H)
NM.LISTVIEW* pNMLisLView = (NM_LlSTVIEW«)pNMHDR;
iCListctrl* pList = (CListctrl*) GetDlgItem(IDC_LISTVIEW1);
int nSelected - pNMListView->iItem;
if (nSelected >= 0) {*
CString stritem = pList->GetItemText(nSelected, 0);
SetDlgItemText(IDC STATIC_LISTVIEW1, strlten);
}"
-pResult = 0;
В структуре А)И_£/.$7И£ТРестперсмснная-член Шет, содержащая индекс выбранного элемента списка.
Запрограммируйте древовидный список. В редакторе диалоговых окон установитеатрибуты стиля элемента управления «древовидный список», как показано на рисунке.
Затем вставьте в OnlnitDiaiogTaKHe строки:
СТreeCtrl* рТгее = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1); pTree->SetImageList(Am_imageList, TVSIL_NORMAL);
И общие значения древовидной структуры
TV_INSERTSTRUCT tvinsert;
tvinsert.hParent = NULL;
tvinsert. hlnsertAfter = TVI LAST;
tvinsert, item.mask = TVIFJMAGE ! TVIF_SELECTEDIMAGE I TVIF_TEXT;
tvinsert, item, hltem = NULL;
tvinsert, item, state = 0;
tvinsert, item. stateMask = o;
tvinsert.item.cchTextNax = 6;•
tvinsert, item.iSelectedlmage = 1;
tvinsert, item, cChildren = 0;
tvinsert.item.IParam = 0;
И верхний уровень'
tvinsert.item.pszText = "Homer";
tvinsert, item, jilmage = 2;.
HTREEITEM hDad = pTree->InsertItem(4tvinsert); tvinsert.item.pszText = "Marge";.
HTREEITEM hMom = pTree->InsertItem(&tvinsart);'
// второй уровень■
tvinsert. hPa rent = hDad; tvinsert, item, pszText = "Bart";.
tvinsert.item.ilmage = 3;'
pTree->lnsertltem(&tvinsert); tvinsert .item.pszText = "Lisa"; pTree->InsertItem(&tvinsert);■ ■
.// второй уровень.
tvinsert.h Parent = hMom;..
tvinsert, item. pszText = "Bart"; tvinsert, item, ilmage = 4;. ■
pTree->InsertItem(&tvinsert); tvinsert.item.pszText = "Lisa"; pTree->InsertItem(&tvinsert);.
tvinsert.item.pszText = "Gilbert”; HTREEITEM hOther - pTree->lnsertltem(&tvinsert);1.
И третий уровень■‘
tvinsert. hPa rent = hOther; tvinsert.item.pszText = "Dogbert"; tvinsert.item.ilmage = 7; pTree->lnsertltem(&tvinsert); tvinsert.item.pszText = "Ratbert"; pTree->InsertItem(&tvinsert); Как видите, этот код устанавливает в TV_INSERTSTRUCT текст и индексы изображений, а затем вызывает hisertltemmta создания узлов в древовидной структуре. И, наконец, с помощью ClassWiZard создайте обработчик уведомления TVN_SBLCHANGED от элемента управления «древовидный список». Вот как выглядит код обработчика, выводящего выбранный текст в статическом элементе управления (метке):
void CWin95Dialcg: :0nSelchangedTreeview1(NMHDR» pNMHDR, LRESULT* pResult) {
NM_"TEB'№ pNMTreeView = (NM TREEVIEW‘)pNMHDR;
CTreeQtrl* pTree = (CTreeCtrl»У GetDlgItem(IDC_TREEVIEW1);
(HTREEITEM hSelected = pNMTreeView->itemNew.hItem;.
if (hSelected I = NULL) { char text[31]; TV ITEM item;'
item.mask = TVIF.HAHDLE I TVIF_TEXT; item.hltem = hSelected; item.pszText = text;.
item.cchTextMax = 30;"’
VERIFY(pTree->GetItem(&item));■'
SetDlgItemText(IDC_STATIC_TREEVIEW1, text);■1
*pResult - 0;• •■ .
В структуре ЛЗ^гаЫМЕФ'естшеременная-член ib/tem, содержащая описатель выбранного узла. Функция GelUem отыскивает данные, относящиеся к этому узлу, и получает текст, пользуясь указателем, хранящимся в структуре 7У_/Т£ЛШеременная mask сообщает Windows, что описатель bltem достоверен и что нужно вернуть текст."• •
.Отредактируйте виртуальную функцию OnDraw в файле ex06bVlew.cpp. Выделенный псмпокириым код заменяет существующий:
void CExOEbVlew::OnDraw(CDC* pDC).
{
pDC->TextOut(O, 0, " Щелкните здесь левой кнопкой мыши.”);
}
Используя ClassWizard,добавьте функцию-член OnlButtonDown. Отредактируйте код, сгенерированный ClassWizard, как показано ниже:
void CExOShView::OnLButtonOown(UINT nFlags, CPoint point)
CEx06bDialog dig;
dlg.nunTrackbarl = 20;
dlg.ii_nTr ackbar 2 = 2; // индекс для 8.0
dlg.m_hProgress = 70; // только запись'
dlg.m_dSpin = 3.2;
dlg.DoHodalO;
Добавьте соответствующий оператор, чтобы включить файл ex06bDialog.h к файл ехОбЬ- View.cpp.'' .
Скомпилируйте и запустите программу. Поэкспериментируйте с элементами управления, чтобы увидеть, как они работают. Индикатор продвижения мы с Вами не запрограммировали — о нем поговорим в главе 12.
Другие стандартные элементы управления Windows
Вы познакомились с большинством стандартных элементов управления, предусмотренных в палитре экранных объектов в редакторе диалоговых окон. Пропущен элемент управления < ч^^лмцмя^аштаиоп control), поскольку мультимедиа-приложения в этой книге не рассматриваются, а также элемент управления «быстрая клавиша» (hot key control) — в нем нет ничего особенного. По-настоящему интересен элемент управления «вкладка» (tab control), но он очень редко применяется внутри диалогового окна, а о создании диалогового окна с вкладками, иногда называемого окном свойств (property sheet), будет рассказано в главе 13. В этой же главе мы рассмотрим приложение, построенное на базе класса CRicbEditView, в который входит поле ввода с форматированием.
ГЛАВА 7
Немодальные и стандартные ■ диалоговые окна Windows
В главе 6 Вы познакомились с обычным модальным диалоговым окном н с большинством элементов управления Microsoft Windows. А теперь мы перейдем к изучению немодальных и стандартных диалоговых окон Windows 95 и Windows NT версии 4.0 и более поздних. Немодальные диалоговые окна, как Вы помните, позволяют работать с любой частью приложения, даже когда диалоговое окно активно. Программным интерфейсом C++ с группой вспомогательных диалоговых окон Windows (втом числе File Open, PrinterSetup, Color Selection и т.д), которые поддерживает динамически подключаемая библиотека COMDLG32.DLL, служатклас- сы стандартных диалоговых окон.
В первом примере этой главы Вы построите простое немодальное диалоговое окно, управляемое из класса «вид», а во втором создадите класс, производный от класса CFileDialogm COMDLG32, который позволит удалять файлы. '
Немодальные диалоговые окна
В MFC-библиотеке 6.0 модальные и немодальные диалоговые окна совместно используют один и тот же базовый класс С Dialog и диалоговый ресурс, который можно создать с помощью редактора диалоговых окон. Если Вы захотите применять немодальное диалоговое окно вместе с объектом «вид», Вам придется освоить несколько особых приемов программирования. Создание немодальных диалоговых окон
Как Вы уже знаете, при создании модальных диалоговых окон сначала надо воспользоваться конструктором CDialog с параметр ом-идентификатор ом прикрепленного ресурса, чтобы сконструировать объект «диалоговое окно», а потом вывести модальное диалоговое окно на экран, вызвав функцию-член DoModal. И как только происходит возврат из DoModal, окно прекращает свое существование. Таким образом, зная, что к тому моменту, когда С++-объект «диалоговое окно* выходит за пределы области видимости, диалоговое окно Windows уже уничтожено, объект «модальное диалоговое окно» можно конструировать на стеке.
Процесс создания немодальных диалоговых окон сложнее. Вы начинаете с вызова конструктора CDialogno умолчанию, создавая, тем самым, объект «диалоговое окно», а вот нужное диалоговое окно создается вызовом функции-члена CDialogaCreate, а не функции DoModal. Create получает идентификатор ресурса как параметр и немедленно возвращает управление; при этом диалоговое окно остается на экране. Так что теперь именно Вы должны заботиться о том, когда конструировать объект «диалоговое окно», когда создавать само диалоговое окно, когда его уничтожать и когда обрабатывать данные, введенные пользователем.
Итак, между созданием модальных и немодальных диалоговых окон есть следующие различия.
Модальное диалоговое окноНемодальное диалоговое окно
ИспользуемыйКонструктор с параметром-Конструктор по умолчанию
конструкторидентификатором ресурса(без параметра)
Функция, используемаяDoModalCreate е параметром-
для создания окна идентификатором ресурса
Пользовательские сообщения
Допустим, Вы холите, чтобы немодальное диалоговое окно уничтожалось, когда пользователь щелкает в этом окне кнопку ОК. Туг сразу же возникает проблема. Как объект «вид» узнает, что пользователь щелкнул кнопку ОК? Диалоговое окно могло бы напрямую обратиться к какой-либо функции-члену класса «вид», но это бы связало данное диалоговое окно с конкретным классом «вид». Более удачное решение — диалоговое окно при вызове обработчика кнопки ОК отправляет объекту «вид* пользовательское сообщение (user-defined message). Получив эго сообщение, объект «вид» сможет уничтожить диалоговое окно (но не сам объект). Итак, вырисовывается сценарий создания нового диалогового окна.
Есть два варианта отправки Windows-сообщений: через функции CWnd::SendMessagewrat PostMessage. Первая немедленно вызывает функцию-обработчик сообщения, а вторая отправляет сообщение в очередь сообщений Windows. Поскольку PostMessage вносит лишь небольшую задержку, можно считать, что функция-обработчик кнопки ОК уже завершилась, когда объект «вид» получает сообщение.
Принадлежность диалогового окна
Теперь предположим, что Вы приняли стиль диалогового окна по умолчанию, т. е. диалоговое окно не ограничено клиентской областью окна представления. Поскольку речь идет о Windows, «владелец» диалогового окна — основное окно-рамка приложения (см. главу 13), а не объект «вид». Но надо знать, какой именно объект «вид» сопоставлен с диалоговым окном, чтобы отправить этому объекту сообщение. Поэтому ваш класс «диалоговое окно» должен отслеживать свой объект «вид» через переменную-член, которую устанавливает конструктор, Параметрpparent конструктора CDialogb данном случае не играет никакой роли.
Пример ЕХ07А
Мы могли бы преобразовать диалоговое окно-<-монстр» из главы 6 в немодальное диалоговое окно, но начать все заново, с простого диалогового окна, пожалуй, будет легче. В программе-примере ЕХ07 А используется диалоговое окно с одним полем ввода и кнопками ОК и Cancel. Как и в главе б, диалоговое окно открывается щелчком левой кнопки мыши (когда курсор находится в окне представления), но теперь мы будем удалять его в ответ на другое событие — щелчок правой кнопки мыши (курсор тоже должен быть в окне представления). Мы будем иметь дело только с одним диалоговым окном, поэтому придется позаботиться, чтобы повторное нажатие левой кнопки мыши не приводило к появлению дубликата диалогового окна.
Чтобы кратко объяснить смысл предстоящих операций, скажем, что класс «вид» приложения ЕХ07А сопоставляется с единственным объектом «диалоговое окно», который конструируется в куче (динамически распределяемой памяти) при конструировании объекта «вид». Диалоговое окно создается и уничтожается в ответ на действия пользователя, но объект «диалоговое окно» уничтожается только по завершении самой программы.
Процесс создания программы ЕХ07А.
Запустите AppWizard, чтобы подготовить проект \VCPP32\EX07A\EX07A. Примите параметры по умолчанию, но выберите Single Document Interface и откажитесь от Printing And Print Preview. Параметры и имена классов по умолчанию показаны ниже на рисунке.
Используя редактор диалоговых окон, создайте новый диалоговый ресурс. Вы- 'берите из меню Insert в Visual C++ команду Resource, а затем — Dialog. Редактор диалоговых окон присвоит новому диалоговому окну идентификатор 1DD DIALOG /.Измените заголовок диалогового окна па Modeless Dialog. Оставьте предлагаемые по умолчанию кнопки ОК и Cancel с идентификаторами ЛЮКи ЮСАКСЕЬвставъте элемент управления «статический текст» и поле ввода с идентификатором по умолчанию IDC_EDIT1. Замените заголовок в элементе управления «статический текст» на Edit 1. В результате, Вы должны получить вот такое диалоговое окно Modeless DiMog.
ПРИМЕЧАНИЕ Обязательно установите у диалогового окна свойство Visible.
Используя Class Wizard, создайте класс CExO7«DiatogB$i6epirie из меню View в Visual C++ команду Class Wizard. Заполните диалоговое окно New Class, как показано на рисунке на стр. 112, и щелкните кнопку ОК.
Добавьте в программу перечисленные ниже функции-обработчики сообщений. Для этого щелкните идентификатор нужного объекта, затем — сообщение и, наконец, кнопку Add Function. На экране появится диалоговое окно Add Member Function. Исправьте при необходимости имя функции и щелкните кнопку ОК,
Идентификатор объекта
Сообщение
Имя функции-члена
TOCANCEL
BN_CL1CKED
OnCancel
IDOK
BN_CLICKED
OnOK■ ■.
Добавьте переменную в класс CEx07eDialog.Haxopacb в ClassWizard, щелкните вкладку Member Variables, а потом — кнопку Add Variable, чтобы добавить переменную mjstrEditl типа CStringK элементу управления IDCEDIT1.
Отредактируйте ex07aDialog.h, включив в него указатель на объект «вид* и прототипы функций. Введите в объявление класса СЕхО/йОга^о^следующий код, выделенный полужирным:
private:
CView* m_pView;
и добавьте прототипы функций:'
public:
CEx07aDialog(CView* pView);
BOOL Create();
ПРИМЕЧАНИЕ Применяя класс CViewвместо CEx07aVlew,можно использовать класс «диалоговое окно» с любым классом «вид».
Отредактируйте ex07aDialog.h, чтобы определить идентификатор сообщения WM GOODBYE. Добавьте такую строку кода:
«define ММ_60003УЕ WM.USER + 5
Windows-константа WM_USER — первый из числа доступных идентификаторов для пользовательских сообщений. Некоторыми из них оперирует каркас приложений, поэтому мы пропустим первые пять.
ПРИМЕЧАНИЕ Редактор диалоговых окон поддерживает в файле resource.il вашего проекта список символьных определений, но не воспринимает константы, базирующиеся на других константах. Не добавляйте вручную WM_GOODBYE в resource.h, потому что Visual C++ может удалить ее.
Добавьте конструктор немодального диалогового oкнaвфaйлex07aDlalog.cpp. Вы могли бы модифицировать существующий конструктор СЕхО7«Dialog,во, включив отдельный конструктор, Вы д обьетесь того, что класс «диалоговое окно» станет пригоден как для модальных, так и для немодальных диалоговых окон. Итак, введите следующие строки: CEx07aDialog: :CEx07aDialog(CView* pView) // "немодальный" конструктор
m pView » pView;
Вы должны также вставить в «модальный» конструктор, сгенерированный AppWizard, такую строку:
m_pView = NULL;•
Компилятор C++достаточно «сообразителен», чтобы отличить немодальный конструктор CEx07dDi«log(CView*) от модального CEx07aDialog(CWnd"). Обнаружив аргумент класса CVieu> или производного от него класса CView, компилятор генерирует вызов немодального конструктора, а увидев аргумент класса Сй^и^или производного от него класса, — вызов модального конструктора.
Введите в ex07aDialog.cpp функцию Create. Эта функция производного класса диалогового окна вызывает аналогичную функцию базового класса, передавая идентификатор диалогового ресурса как параметр. Вставьте следующие строки:
BOOLCExOTaDialog::Create() {
return CDialog::Create(CEx07aDialog::IDD);
}
ПРИМЕЧАНИЕ Функция Create не является виртуальной. При желании ей можно было бы подобрать другое имя.
Отредактируйте функции ОмОКи ОпСапсе1в ex07aDialog.cpp. Эти виртуальные функции, сгенерированные мастером ClassWizard, вызываются в ответ на щелчки кнопок в диалоговом окне. Введите код, выделенный полужирным:
void CEx07al)ialog: :0nCancel()// это не настоящий обработчик сообщения
{ '
if (m_pView != NULL) {
// немодальное диалоговое окно - не вызывайте ОпСапсе! из базового класса m_pView->PostMossage(WM GOODBYE, IDCANCEL);
>
else
CDialog::OnCancel(); И случай модального диалогового окна
.1.
void CEx07aDialog;:QnOK()// это не настоящий обработчик сообщения
{
if (m_pView ! = NULL) {
// немодальное диалоговое окно - не вызывайте ОпОК из базового класса
UpdateData(THUE);
m_pViBW->PostMessage(WM GOODBYE, IDOK);
}
else
CDialog::OnOK();// случай модального диалогового окна
Если диалоговое окно используется как немодальное, оно отсылает объекту «вид» пользовательское сообщение WM_GOODBYE. О его обработке мы поговорим ниже.
ПРИМЕЧАНИЕ Применяя немодальное диалоговое окно, не вызывайте функций CDialog: :ОпОКили CD!alog::OnCancel. Это значит, что Вы должны переопределить эти виртуальные функции в своем производном классе; иначе нажатие клавиш Esc или Enter либо щелчок кнопок мыши приведут к вызову функций базового класса, которые обращаются к Windows-функции EndDialog. Последняя подходит только для модальных диалоговых окон. В немодальном диалоговом окне Вы должны вместо нее вызывать DestroyWindow, а если надо передать данные от элементов управления диалогового окна переменным-членам класса, то вызывайте UpdateData.
Отредактируйте заголовочный файл ex07aView.h. Для хранения указателя на объект «диалоговое окно» нужна соответствующая переменная-член:
private:
CEx07aDialog* m_pDlg;
Если Бы добавите в начало файла ex07aView.h упреждающее объявление:
class CEx07aDialog;'
то Вам не придется включать ex07aDialog.h в модули, содержащие ex07aView;h.
Модифицируйте конструктор и деструктор CExO7«VieWB ex07aView.cpp. В классе СЕх07аУ1еи;есгь переменная-член т_pDlg, которая указывает на объект СЕхО7 «Dialog. Конструктор объекта «вид» формирует объект «диалоговое окно» в куче, а деструктор объекта «вид» уничтожает его, Введите код, выделенный полужирным:
CEx07aView::CEx07aVicw()
m_pDlg = new CEx07aDialog(this);
1
CEx07aV;ew::'CEx07aView()
{
delete m pDlg; // уничтожает окно, если оно еще не уничтожено }
Добавьте код к виртуальной функции OnDrawe ex07aVlew.cpp. СЕх^аУгегь’-функ- цию OnDraw, заготовка которой генерируется AppWizard, нужно запрограммировать так, чтобы она предлагала пользователю щзлкнутъ кнопкой мыши:
void CEx07aVicw: :OnDrawfCDC* pDC) { pDC->Text0ut(O, 0, "Щелкните правой кнопкой мыши”);
1
13-Используя Classwizard, создайте в СЕхО7яУ/еыобработчики сообщений от мыши. Подготовьте обработчики сообщений WM_LBUTTONDOWN и WM_RBUTTONDOWN, а заТем отредактируйте код в файле cxO7aVicw.cpp:
void CEx07aView::DnLButtonDown(UINT nFlags. CPoint point) { /./ создает диалоговое окно, если оно еще не создано if (m„pDlg->GetSafeHwnd() == 0) {'
m pDlg->Greate(); // показывает диалоговое окно на экране
} void CEx07aView:OnRButtonDown(UINT nFlags, CPoint point) { m_pBlg->Oe9troyWindaw(); // ничего страшного, если окно узе уничтожено
}
Функция DeslroyWindawae уничтожает C++-объект — эго верно для окон почти всех типов, кроме основных окон-рамок. Именно такого ее поведения мы и добиваемся, так как в деструкторе объекта «вид» мы уничтожаем только объект «диалоговое окно».
И.Дополните файл ex07a¥iew.cpp оператором *include для заголовочного файла диалогового окна. Вставьте этот оператор после оператора, включающего заголовочный файл класса «вид»: ^include "exOTaView. h" ftinclude "ex07aDialog.tr
J 5. Напишите код для обработки сообщения WM_GOODBYE. Поскольку ClassWizard не поддерживает пользовательские сообщения, Вы должны сами написать этот код. Здесь-то Вы и смажете оценить, какую работу проделывает ClassWizard в случае других сообщили.
If Добавьте в файл ex07aView.cpp следующую строку после оператора BEGINJWESSAGEJtiAP, но за скобками AFX_MSGJMAP;
ON_MESSAGE (WM_GOODBYE, OnGoodbye)
m Кроме того, вставьте в этот файл саму функцию-обработчик сообщения:
LRESULT CEx07aVlew: :OnGoodbye(UINT wParam, LONG IParam)
{
// сообщение, получаемое в ответ на щелчки кнопок ОК и Cancel
//в немодальном диалоговом окне
TRACE("CEx07aView::OnGoodbye Хх, %lx\n”, wParam, IParam);
ТРАСЕ( "Лоле editl содержит = %s\n",
(const char*) m„pDlg->m_strEdit1);
m_pDlg->DestroyWlndow();
return 01;
}
г В файл ex07aView,h поместите прототип функции:
afx_msg LRESULT OnGoodbye(UINT wParam, LONG IParam);
В Win3 2 параметры wParam и IParam — обычный путь передачи данных, содержащихся в сообщении. Например, при посылке сообщения о нажатой кнопке мыши в IParam упаковываются х- иу-координаты курсора мыши. В MFC-библиотеке данные, включаемые в сообщение, передаются через параметры с более внятными именами. Те же координаты курсора мыши передаются как объект CPoint. В то же время, в пользовательских сообщениях применение wParam и IParam. обязательно, так что в любой момент Вы можете задействовать эти две переменные. В этом примере в параметр wParam мы записывали идентификатор кнопки.
Соберите и протестируйте приложение. Соберите и запустите программу ЕХ07А. Попробуйте н ажать левую кнопку мыши, а затем правую. (В последнем случае курсор мыши должен быть вне диалогового окна.) Вновь вызовите диалоговое окно на экран, введите какие-нибудь данные и щелкните в диалоговом окне кнопку ОК. Правильно ли выводит оператор TRACE (объекта «вид») содержимое поля ввода?
ПРИМЕЧАНИЕ Если Вы воспользуетесь классами «вид» и «диалоговое окно* из программы ЕХ07А в каком-нибудь MDI-приложении, то сможете создавать в каждом его дочернем окне по одному немодальному диалоговому окну. Закрытие дочернего окна MDl-приложения приводит к тому, что его немодальное диалоговое окно уничтожается, так как деструктор объекта «вид» вызывает деструктор объекта «диалоговое окно*, а тот уничтожает диалоговое окно.
Класс CFormView:альтернатива немодальному диалоговому окну
Если Вам нужно приложение, которое базируется на единственном немодальном диалоговом окне, класс CFormView позволит сэкономить массу сил. Но Вам придется потерпеть до главы 16. поскольку этот класс наиболее эффективен при использовании вместе с классом CDocument, а мы пока еще не зашли так далеко в нашем исследовании каркаса приложений.
Стандартные диалоговые окна Windows
Windows предоставляет набор стандартных диалоговых окон; их поддерживают и MFC-классы. Вы, вероятно, знакомы с этими окнами (всеми или некоторыми), поскольку они уже используются во многих Windows-приложения, включая Visual C++. Все классы стандартных
диалоговых окон являются производными от одного базового класса CCdmmonDialog. Список классов, поддерживающих диалоговые окна из COMDLG32.DLL, приведен в таблице.
Класс
Назначение
CColorDtalog
Выбор или создание цвета
CFileDialog
Открытие или сохранение файла
CFindReptoceDialog
Замена в документе одного текста другим
CPageSeiupDialog
Ввод параметров страницы документа
CFonlDiaiog
Выбор шрифта из списка доступных шрифтов
CPrintDialog
Настройка принтера и печать документа
У всех стандартных диалоговых окон есть общая особенность: они собирают информацию от пользователя, но ничего с ней не делают. Скажем, диалоговое окно File Open помогает пользователю открыть файл, но на деда всего лишь сообщает программе путь и имя выбранного файла — об остальном должна позаботиться сама программа. Примерно так же действует и диалоговое окно, предназначенное для выбора шрифта: в него вводятся параметры шрифта, но оно не создает шрифта.
Непосредственное использование класса CFileDialog
Открыть файл с помощью этого класса очень просто. Вог пример кода, открывающего файл, выбранный пользователем в диалоговом окне:
CFileDialog dig(TRUE, "bmp", “•.bmp");
if (dlg.DoModalO == IDDK> {
CFile file;
VERlFY(file.Open(dlg.GetPathNatoe(), CFile::modeRead));
}
Первый параметр (TRUE) конструктора указывает, что данный объект — это диалоговое окно «File Open», а не «File Save*, «bmp* — это расширение файлов по умолчанию, и *.Ътр появится в окне ввода имени файла. Функция CFileDialogOetPatbName возвращает объект CString, который содержит полное имя выбранного файла (с указанием пути).
Производные классы стандартных диалоговых окон
Чаще всего классами стандартных диалоговых окон Вы будете пользоваться напрямую. Если же Вы решите создать собственные, производные классы, то смажете расширить функциональность стандартных диалоговых окон, не дублируя их кед. Однако каждое диалоговое окно COMDLG32 имеет свои особенности. И хотя в следующем примере мы будем иметь дело с диалоговым окном, предназначенным для работы с файлами, ан, тем не менее, даст Вам некоторое представление об адаптации других стандартных диалоговых окон
ПРИМЕЧАНИЕ В предыдущих изданиях книги профамма-пример ЕХ07В динамически создавала элементы управления в стандартном «файловой»» диалоговом окне. Этот прием в Win32 не срабатывает, но тот же эффект достигается при помощи реализованного здесь метода вложения диалоговых окон.
Вложение диалоговых окон'
Win32 позволяет «вкладывать» одно диалоговое окно в другое и, тем самым, выводить на экран несколько диалоговых окон как единое целое. Сначала Вы должны создать шаблон диалогового ресурса с «дырой» в нем — обычно это элемент управления «группирующая рамка» — и присвоить дочернему окну специфический идентификатор stc32 (=0x045/). Далее программа должна установить ряд параметров, которые подскажут COMDLG32, что нужно пользоваться именно этим шаблоном. Кроме того, программа должнапоставигълобгуыосу (hook) в цикле выборки сообщений в COMDLG32, чтобы перехватывать определенные сообщения. Про-
делав эти операции, Вы получите диалоговое окно, все еще дочернее по отношению к диалоговому окну COMDLG32 несмотря даже на то, что ваш шаблон — это -«обертка» шаблона COMDLG32.
Все это выглядит сложно, и на самом деле оказывается сложным, если Вы не пользуетесь MFC. Работая с MFC, Вы создаете шаблон диалогового ресурса и, как уже было сказано, формируете класс, производный от одного из базовых классов стандартных диалоговых окон, включаете в О??/н/Ф/«/о# связующий код, специфичный для конкретного класса, а затем просто запускаете ClassWizard/pia создания обработчиков сообщений, поступающих от новых элементов управления вашего шаблона. Вот и все.
ПРИМЕЧАНИЕ В Windows NT 3.51 использовалась старая версия динамически подключаемой библиотеки стандартных диалоговых окон, которая не поддерживала новую возможность Windows — пространство имен (namespace), Поэтому техника вложенных диалоговых окон из примера ЕХ07 с этой версией работать не будет.
Пример ЕХ07В
В этом примере Вы создадите производный класс C"£xr?7foOz?</og, который добавит в стандартное диалоговое окно для работы с файлами кнопку «Delete all matching files» («Удалить все соответствующие файлы»). Кроме того, он изменит заголовок диалогового окна и заменит кнопку Open на Delete (для удаления отдельных файлов). Пример демонстрирует, как пользоваться вложенными диалоговыми окнами, чтобы добавлять в стандартные диалоговые окна новые элементы управления. Обновленное диалоговое окно активизируется также, как и в предыдущем примере, — щелчком левой кнопки мыши, когда курсор находится в границах окна представления. Поскольку Вы уже должны уметь работать с Visual C++, мы не будем так подробно, как раньше, описывать подготовительные операции. Вот как выглядит диалоговое окно, создаваемое этой программой (см рис. 7-1).
Рис, 7-1, Диалоговое окно Delete File в действии1
Итак, строим приложение ЕХ07В.
1 3anycTHTeAppWizard, чтобы сформировать проект \VCPP32\EX07B\EX07B. Примите параметры по умолчанию, но выберите Single Document Interface и откажитесь от Printing And Print Preview. Параметры и имена классов по умолчанию показаны на верхнем левом рисунке на стр. 118.
Используя редактор диалоговых окон, создайте диалоговый ресурс. Установите размер диалогового окна равным Зо дюймов и присвойте ему идентификатор IDDFILE- SPECIAL. Свойству Style придайте значение Child, свойству Border — None и установите флажки у свойств Clip Siblings и Visible. Создайте шаблон с кнопкой с идентификатором IDCJDEIETEh группирующей рамкой с идентификатором stc32=0x045^o™acHO приведенному ниже рисунку (нижний справа).
Проверьте результат своих трудов, выбрав пункт Resource Symbols из меню Visual C++. Список символов должен выглядеть аналогично приведенному на рисунке.
Используя ClassWiZard, создайте класс CSpecialFileDialog'3аполните окно New Class, как показано на рисунке на стр. 119, и щелкните кнопку Change.
Замените предложенные имена файлов на SpecFileDlg.h и SpecFileDlg.cpp. К сожалению, Class Wizard не позволит указать CFtieDialogB качестве базового класса — вместо этого придется выбрать CDialog и сделать замену вручную.
Отредактируйте файл SpecFileDlg.h. Замените строку:
class CSpecialFileDialog : public CDialog
на
class CSpecialFileDialog : public CFileDialog
Кроме того, добавьте две открытых переменных-члени класса:
CString m_strFi leNaire;
BOOL m_bDeleteAll;
И, наконец, отредактируйте объявление конструктора:
CSpecialFileDialog(BOOL bOpenFileDialog,
LPCTSTR IpszDefExt = NULL,
LPCTSTR IpszFileName = NULL,
DWORD dwFlags = OFN-HIDEREADONLY I OFN_OVERWRITEPROMPT,
LPCTSTR IpszFilter - NULL,
CWnd* pParentWnd = NULL);
Замените CDialog на CFileDialog в файле SpecFHeDlg.cpp. Для этого выберите из меню Edit в Visual C++ команду Replace и везде замените это имя.
Отредактируйте конструктор CSpecialFileDialog в файде SpecFHeDlg.cpp. Конструктор производного класса должен вызывать конструктор базового класса и инициализировать переменную-член mJbDeleteAll. Кроме того, он должен устанавливать некоторые поля в переменной-члене m_ojn(базового класса CFileDialog), которая является экземпляром Win 32-структуры OPENFILENAME.Поля Flags и IpTemplateNamecTyyKrypH управляют подсоединением класса к шаблону 1М)_РП.ESPECIAL,а поле IpstrTitle изменяет заголовок основного диалогового окна. Отредактируйте конструктор следующим образов
CSpecialFileDialog: :CSpecialFileDialog(BOOL bOpenFileDialog,
LPCTSTR IpszDefExt, LPCTSTR IpszFileName, DWORD dwFlags,
LPCTSTR IpszFilter, CWnd* pParentWnd) : CFileDialog(
bOpenFileDialog, IpszDefExt, IpszFileName, dwFlags,,
IpszFilter, pParentWnd)
//{{AFX_DATA_INIT(especialFileDialog)
// Примечание: ClassWizard добавит сюда код, инициализирующий переменные-члены
//}}AFX DATA INIT
m ofn.Flags |= OFN ENABLETEMPLATE;
m_ofn.lpTemplateName = MAKEI№TRESOURCE(IDD_FILESPECIAL);
m ofn.IpstrTitle = "Удаление файла";
mJbDeleteAll = FALSE;
Создайте обработчик сообщения WM_INITDIALOG в классе CSpecialDialog. Функции-члену OnlnitDialogHysKuo заменить кнопку Open в стандартном диалоговом окне на кнопку Delete. Идентификатор кнопки — 1DOK.
bool bRet = CFileDialog::OnInitDialog(); if (Met == TRUE) {
GetParent()->GetDlgItem(IDOK)->SetWindowText("y№nMTb");
return bRet;-
Создайте обработчик новой кнопки IDC_DEtETE(De\ete АП Matching Files) в классе CSpecialDialog. Функция-член OnDetete устанавливает флажок m_bDeleteAll, а потом заставляет основное диалоговое окно вернуть управление, как будто была нажата кнопка Cancel. Клиентскаяпрограмма (в данном случае объект «вид») получаетIDCANCEL, возвращаемый из DoModal, и проверяет флажок, чтобы узнать, надо ли удалить все файлы: void CSpeclalFileDialog::OnDelete().
. m_bDeleteAll = TRUE;
// 0x480 - идентификатор дочернего окна поля ввода File Name
// (по данным, полученным от SPYXX)
GetParent()->SetDlgItem(Ox480)->GetWlndowText(m_strFileName);
GetParent()->SendMessage(WM COMMAND, IDCANCEL);
Добавьте код в виртуальную функцию OnDraw в файле ex07bView.cpp. Чтобы С£х07ЬУ7бМУ-функция OnDraw, заготовка которой генерируется мастером AppWizard, предлагала пользователю нажать кнопку мыши, ее следует закодировать так:
void CExO7bView::OnDraw(GDC* pDC) {
pDC->TextOut(O, 0, "Щелкните левой кнопкой мыши"); }
Добавьте в класс ССхОТЬГ^емобработчик сообщения On£But#o«Down. Создайте с помощью Class Wizard обработчик cooбщeнияWMJLBЩTONDOWN, после чего отредактируйте полученный код следующим образом:
void CEx07bView: :OnLButtonDowri(UINT nFlags, CPolnt point) '
CSpecialFileDialog dlgFile(TflUE, NULL, "*.obj”);
CString str Message;
int nModal = dlgFile.DoModal();
if ((nModal — IDCANCEL) && (dlgFile.m_bDeleteAll)) {
strMeseage.Format("Bu действительно хотите удалить все %s файлы?", dlgFile.m_strFilename);
if (AfxMessageBox(strMessage, MB YESNO) ™ IDYES) {.
HANDLE h;"
WIN32_FIND_DATAfData;-
while ((h = : :FindFirstFile(dlgFile.m strFilename, AfData))
1= (HANDLE) OxFFFFFFFF) { // MFC-эквивалента нет
if (::DeleteFile(fData.cFlleNanie) == FALSE) {
st rMessage. Format ("He могу удалить файл %s\n", fData.cFileName):
AfxHeasageBox(3trMessage);
break;
J
Основные характеристики обычных элементов управления
В главе б мы использовали обычные элементы управления Windows, такие как поля ввода и списки, а также познакомились со стандартными элементами управления Windows, которые работают в основном аналогично. Все эти элементы управления представляют собой дочерние окна и применяются главным образом в диалоговых окнах — для их представления в MFC служат классы типа CEdi№ CTreeClrl. За создание дочернего окна элемента управления всегда отвечает клиентская программа.
Обычные элементы управления посылают диалоговому окну уведомляющие сообщения (стандартные Windows-сообщения), например, BN CL1CKED. Если Вы хотите что-то сделать с элементом управления, Вы вызываете функцию-член соответствующего С++-класса, которая передает этому элементу Windows-сообщение. Все элементы управления по своей природе — окна. MFC-классы для элементов управления являются производными от класса CWnd, ткниму, чтобы получить текст из поля ввода, Вы обращаетесь к GWnd-GetWindotoText.Ho работа и этой функции строится на передаче сообщения элементу управления.
Элементы управления Windows — неотъемлемая часть этой операционной системы, хотя стандартные элементы управления Windows и находятся в отдельном DLL-модуле. Кроме того, есть еще одна разновидность обычных элементов управления Windows — так называемые пользовательские злеменпгы управления (custom controls). (Их создает сам программист, и они не являются частью операционной системы, но работают как обычные элементы управления, т. е. передают родительскому окну уведомления WM_COMMANDh обрабатывают посылаемые ему сообщения.) Один из пользовательских элементов управления описан в главе 22.
Общие черты ActiveX и обычных элементов управления
ActiveX-элемент можно рассматривать как дочернее окно, подобно обычному элементу' управления. Чтобы включить такой элемент управления в диалоговое окно, Вы вставляете его туда с помощью редактора диалоговых окон, и его идентификатор появляется в шаблоне ресурса. При динамическом создании ActiveX-элемента («палету») Вы вызываете функцию Create класса, представляющего данный элемент управления. Обычно это делается в обработчике сообщения WM_CREATE родительского окна. Если необходимо выполнить какие-либо операции сэлементами управления ActiveX, вызывается функция-член C++ (также, как и для элемента управления Windows). Окно, содержащее ActiveX-элементы, называется контейнерам (contrainer). '
Различия ActiveX и обычных элементов управления
Наиболее очевидная отличительная черта элементов управления ActiveX — наличие у них свойств и методов. Все С++-функции, которые Вы вызываете для работы с этими элементами управления, так или иначе используют методы и свойства. Свойства имеют символические имена, которым соответствуют целочисленные индексы. РазработчикАс11УеХ-элемепта присваивает каждому свойству определенное имя, скажем BackColor или GridLineWidtb.H тип, например, строка, целое или вещественное двойной точности. Для растровых изображений и значков существует даже такой тип свойства, кэк. картинка (picture). Клиентская программа можетустанавливать отдельные свойства элемента управления, задавая их целоч исленные индексы и значения. В некоторых случаях Class Wizard позволяет определить переменные- члены в классе клиентского окна, которые сопоставляются со свойствами ActiveX-элементов, имеющихся в клиентском классе. Сгенерированный DDX-код осуществляет обмен информацией между свойствами ActiveX-элемента и переменными-членами клиентского класса.
Методы элементов управления ActiveX подобны функциям. У метода есть символьное имя, набор параметров и возвращаемое значение. Для вызова метода вызывается функция-член
класса C++, представляющего данный ActiveX-элемент. Разработчик элемента управления может определить любые нужные методы, например, PrevlousYear.LowerGontrolRodsTs. т. д.
В отличие от обычных элементов управления ActiveX-элемент не посылает своему kohtoi [неру уведомляющих сообщений WM_ — вместо этого он «генерирует события». Событие «носит» символьное имя и может обладать произвольным набором параметров — в сущности, это функция контейнера, которую вызывает элемент управления ActiveX. Как и уведомляющие сообщения от обычных элементов управления, события не возвращают ActiveX-элементу никаких данных. В качестве примеров событий можно назвать Click, KeyDouwa NewMontb. События увязываются с: вашим клиентским классом также, каки уведомляющие сообщения отэлементов управления. .
В мире MFC элементы управления ActiveX ведут себя как обычные дочерние окна, однако между окном контейнера и окном элемента управления есть весьма «толстая» прослойка кода. Кроме того, у ActiveX-элемента окна может и не быть. При вызове Create окно элемента управления непосредственно не создается — вместо этого загружается его код, и ему выдается команда для «активизации на месте». После этого ActiveX-элемент формирует свое окно, доступ к которому MFC обеспечивает через указатель CWnd. Однако использование клиентской программой £№п<яГэлемента управления ActiveX — это не самый лучший вариант.
ActiveX-элементы хранятся в отдельных DLL-модулях; обычно такая DLL имеет расширение OCX. Прикладная программа загружает DLL по мере необходимости, используя изощренные приемы COM-технологии, основанные на операциях с реестром Windows. Так что пока возьмите себе на заметку лишь одно: ActiveX-элемент, указанный Вами на этапе разработки программы, будет загружен в период ее выполнения. Естественно, при поставках приложения, требующего определенных элементов управления ActiveX, Вы должны включить в комплект поставки файлы OCX и соответствующую программу установки.
Установка элементов управления ActiveX
Допустим, Вы раздобыли какой-то «модный» ActiveX-элемент и хотите использовать его в своем проекте. В первую очередь нужно скопировать его DLL на жесткий диск. Ее можно разместить в любом месте, но Вам будет легче отслеживать элементы управления ActiveX, если все они помещены в одном месте, например, в системном каталоге (обычно \WINDOWS\SYs- ТЕМ в Microsoft Windows 95 и \WINNT\SYSTRM32 в Microsoft Windows NT). Скопируйте в тот же каталог файлы справки (HLP) и лицензирования (LIC).
Следующий шаг — регистрация элемента управления в реестре Windows. В реальности ActiveX-элемент регистрирует себя сам, когда клиент вызывает специальную экспортируемую функцию. Обычно в качестве такого клиента выступает утилита Regsvr32, которая получает имя элемента управления из командной строки. Эта утилита годится для сценариев установки. На прилагаемом к книге компакт-диске в проекте REGCOMP находится одноименная программа, которая позволяетнайти надиске заданный элемент управления. Некоторые ActiveX- элементы требуют лицензирования, что может включать дополнительные записи в Реестре, (О работе с реестром Windows см. главы 15, 17, 24 и 25.) Лицензируемые элементы управления ActiveX обычно поставляются с программой установки, которая и заботится об этих операциях,
После регистрации ActiveX-элемента Вы должны установить его в каждый проект, в котором он используется. Это не означает, что нужно копировать OCX-файл — Class Wizard создаст копию С++-класса, соответствующего данному элементу управления, и последний появится в палитре элементов управления редактора диалоговых окон данного проекта.
Для установки элемента управления ActiveX перейдите в подменю Add То Project из меню Project и затем выберите Components And Controls. Появится диалоговое окно, показанное на рисунке на стр. 125.
Раскройте папку Registered ActiveX Controls. Вы увидите список всех элементов управления ActiveX, установленных в вашей системе в данный момент. Пример списка показан на
рисунке ниже.
Элемент управления Calendar '
Файл MSCal.ocx — популярный ActiveX-элемент Microsoft Calendar, который, скорее всего, уже установлен и зарегистрирован на вашем компьютере. Если это не так, не огорчайтесь — он есть на компакт-диске, прилагаемом к книге.
На рисунке 8-1 показан элемент управления Calendar, расположенный внутри модального диалогового окна. Вместе с этим элементом управления поставляется файл справки, в котором перечислены его свойства, методы и события.
Свойства
Методы
События
BackColor
AboutBox
AfterUpdate
Day
NextDay
BeforeUpdate
DayFont
NextMonth
Click
Свойства
Методы
События■
DayFontCdor
NextWeek
DblClick
DayLength
NextYear
KeyDown
FirstDay
PrevjousDay
KeyPress
GridCellEffect
PreviousMonth
Key Up
GridFont
PreviousWeek
NewMonth
GridFonrColor
Prcviouslfcar
NcwYcar
GridLinesColor
Refresh
Month
Today
Month Length ShowDa reSelecto rs ShowDays ShowHorizontalGridlines ShowTitle ShowVerticalGridlines TiCleFont,
Title Font Color1
Value ValuelsNull Year
Рис. 8-1. Использование элемента управления Calendar
Далее в этой главе и примере ЕХ08Амы будем использовать свойства BackColor, Day, Month, Year и Value. BackColor — это длинное целое без знака (unsigned long), но используется как тип OLE_COLOR, а это почти то же самое, что и COLORREF. Day, Month и Year имеют тип короткое целое (short integer). Value имеет специальный тип МУгИАТкоторый описан в главе 25. Дата хранится в нем в виде 64-разрядного значения.
У каждого из вышеперечисленных свойств, методов и событий есть соответствующий целочисленный идентификатор. Информация об именах, типах, последовательности аргументов и идентификаторах хранится внутри элемента управления, откуда ClassWizard извлекает ее во время проектирования контейнера.
Программирование контейнера ActiveX-элементов
MFC и ClassWizard поддерживают использование ActiveX-элементов как в диалоговых окнах, так и в виде «дочерних ОКОН». Чтобы использовать элемент управления ActiveX, надо знать, каким образом он обеспечивает д оступ к своим свойствам и как ваш DDX-Код взаимодействует со значениями этих свойств.
Доступ к свойствам
Набор свойств, доступных на этапе разработки, определяет создатель ActiveX-элемента. Эти свойства отображаются мд страницах свойств, которые элемент управления выводит в редакторе диалоговых окон, если щелкнуть его правой кнопкой мыши и выбрать Properties. Основная страница свойств для элемента управления Calendar показана на рисунке ниже.
Если щелкнуть ярлычок АИ, то будет отображен список всех свойств, доступных в период разработки, в том числе, возможно, и тех, что не показаны на вклада® Control. Для Calendar вкладка АН выглядит как на рисунке.
В период выполнения доступны все свойстваАсНусХ-элемента, в том числе и те, что были доступны на этапе разработки. Однако некоторые свойства могут быть определены как «только для чтения*.
Классы-оболочки C++, генерируемые ClassWizard для ActiveX-элемента
При добавлении в проект ActiveX-элемента ClassWizard — в соответствии с набором свойств и методов данного элемента управления — генерирует класс-оболочку C++, производный от CWncL Сгенерированный класс содержит функции-члены для всех свойств и методов, а также конструкторы, которые можно использовать для динамического создания экземпляров данного элемента управления. (ClassWizard генерирует также классы-оболочки объектов, используемых элементом управления.) Бог несколько типичных функций-членов из файла Calendar.cpp, который ClassWizard генерирует для элемента управления Calendar:
unsigned long CCalendar::GetBackColor()
(
unsigned long result;
InvokeHelper(OISPID_BACKCOLQR, OISPATCHJWERTYGET, VTJ4. (void*)&result, HOLL);
return result;
}
void CCalendar: :SetBackColor(unsigned tong newValue)!
static BYTE parms[] * VTS 14;
InvokeHelper(DISPID_BACKCOLOR, DISPATCH_PROPERTYPUT, VT.EMPTY, NULL, parms, newValue);
short CCalendar::GetDay(){
short result;
JnvokeHelper(0x11, DISPATCH_PROPEfiTYGET, VT.I2, (void*)&result, NULL); return result;'
void CCalendar::SetDay(short nNeWValue)
static BYTE parms[) = VTS_I2;.
InvokeHelper(0x11, DISPATCH.PROPERTYPUT, VT EMPTY, NULL, parms, nNewValue);
COleFont CCalendar: :GetDayFont() { LPDISPATCH pDispatch;■
InvokeHelper(0x1. DISPATCH.PROPERTYGET, VTJDISPATCH, (void‘)&pDlspatch, NULL) ;
return C01eFont(pDispatch);
н•
void CCalendar: .SetDayFont(LPDISPATCH newValue)
{'■
static BYTE parms[] = VTS DISPATCH;,
InvokeHelper(0x1, DISPATCH.PROPERTYPUT, VT EMPTY, NULL, parms, neWalue);
VARIANT CCalendar; :GetValue()
VARIANT result;-
InvokeHelper(Oxc, DISPATCH.PROPERTYGET, VTVARIANT, (void*)Sresult, NULL); return result;
void CCalendar: :SetValuefconst VARIANT& newValue)■
{
static BYTE parms[] = VIS VARIANT;.
InvokeHelper(Oxc, DISPATCH PROPERTYPUT, VT EMPTY, NULL, parms, SnewValue);
void CCalendar: :NextDay()
{
lnvokeHelper(Qx16, DISPATCH METHOD, VT.EMPTY, NULL, NULL);
void CCalendar: :NextMonth()
InvokeHelper(0x17, DISPATCH METHOD, VT EMPTY, NULL, NULL);
Особенно беспокоиться насчет кеда этих функций не следует, однако обратите внимание на то, что первый параметр в вызовах GetProperty^etPropertya Invoke соответствует диспетчерскому идентификатору в списке свойств элемента управления Calendar. Как видите, для каждого свойства существуют пары функций Get (получить) и Set (установить). Для вызова метода Вы просто вызываете нужную функцию. Например, для вызова метода NextDay из функции класса диалогового окна можно написать так
m_calendar.NextDay();.
Здесь m_calendar — это объект класса CCalendar, служащего оболочкой для элемента управления Calendar.
Поддержка ActiveX-элементов в AppWIzard
Если в Арр Wizard установлен флажок ActiveX Controls (а по умолчанию это так), в функцию- член 1пШп$Гапсекласса приложения будет вставлена следующая строка:
AfxEnableControlContainerO;.
Кроме того, в файле StdAfx.hBainero проекта появится строка:
«include <afxdisp.h>
Если Вы решили использовать ActiveX-элементы в уже существующем проекте, то можете просто вставить эти две строки.
ClassWizard и диалоговое окно контейнера
Если Вы уже использовали редактор диалоговых окон для создания шаблона диалогового окна, то Вы знаете, что д ля этого окна с помощью ClassWizard можно сгенерировать С++-класс. Если шаблон содержит элементы управления ActiveX, то ClassWizard можно применить для добавления переменных-членов и обработчиков событий.
Переменные-члены класса диалогового окна или использование класса-оболочки?
Какого типа переменные-члены можно добавить к диалоговому окну для элемента управления ActiveX? Если Вам нужно установить свойство элемента управления до вызова DoModal для этого диалогового окна, то можно добавить переменную, соответствующую этому свойству. Если же Вы хотите менять свойства внутри функций-члеиов диалогового окна, то придется использовать другой подход: добавьте переменную-объект класса-оболочки элемента управления ActiveX.
Сейчас самое время вспомнить логику работы DDX MFC. Вернемся к д иалоговому окну ЕХ06А из главы 6. Функция CDialog::OnInitDi«logTaa3HBas?r CWnd::UpdateData(FALSEXjm. считывания значений переменных-членов, а функция CDialognOnOKsHSHBaer UpdateDatcfTRUE) для записи в эти переменные. Допустим, Вы добавили по одной переменнойдля каждого свойства элемента управления ActiveX и хотите получить значение свойства Value в обработчике сообщения от кнопки. Если вызвать UpdateData(FALSE)xo будут считаны значения всех свойств всех элементов управления диалогового окна — ясно, что эго излишняя трата времени Эффективнее не использовать переменную для каждого свойства, а вместо этого вызвать функцию Get класса-оболочки Для этого надо, используя ClassWizard, создать переменную-объект класса-оболочки.
Предположим, у нас есть класс-оболочка CCalendarjyia элемента управления Calendar, а в классе диалогового окна — переменная-член m_calcndar. Тогда значение свойства Value можно получить так:
COleVarient var = m_calendar.GetValue():-
ПРИМЕЧАНИЕ Тип VARIANT» класс CQteVarkmt описаны в главе 25..
Теперь рассмотрим другой случай: Вы хотите перед отображением элемента управления установить 5-й день месяца. Для этого добавьте в класс диалогового окна переменную-член mjsCalDay, соответствующую свойству Day типа короткое целое. Затем добавьте в функцию DoDataExcbange слеедующий вызов:■••■
DDX_OCShort(pDX, ID CALENDAR1, 0x11, m_sCal0ay);'
Третий параметр — диспетчерский идентификатор свойства Day, который можно найти в функциях GetDay и SetDay, генерируемых ClassWizard для элемента управления. Диалоговое окно создается и отображается так:
CMyDialog dig;
dig. m_sCalDay = 5;
dlg.OoMcdal();
Перед отображением элемента управления на экране DDX-код позаботится об установке значения свойства в соответствии со значением переменной-члена. Дополнительного программирования это не потребует. Разумеется, при щелчке кнопки ОК DDX-код установит значение переменной-члена в соответствии со значением свойства.
ПРИМЕЧАНИЕ Даже если ClassWizard правильно определит свойства элемента управления, он необязательно создаст переменные-члены для всех свойств. В частности, не существует DDX-функцийдля свойств типа VARIANT, подобных свойству Value элемента управления Calendar. Для таких свойств придется испогъзовать класс-оболочку.
Привязка событий от элементов управления ActiveX
ClassWizard позволяет увязывать события от ActiveX-элементов с функциями-членами также, как это делается для Windows-сообщений и командных сообщений от элементов управления. Если в классе диалогового окна есть элементы управления ActiveX, то ClassWizard создает и поддерживает таблицу получателей событий (event sink map), задающую соответствие междч событиями и их функциями-обработчиками. Она работает подобно таблице сообщений Пример такого кода Вы увидите на рисунке 8-2.
ПРИМЕЧАНИЕ У элементов управления ActiveX есть неприятная особенность — генерировать события до того, как ваша программа будет способна их получить. Если в обработчике события использу ются окна или указатели на С-н-объекты, то прежде, чем обращаться к ним, необходимо проверить, существуют ли эти объекты.
Блокировка ActiveX-элементов в памяти
Обычно ActiveX-элемент проецируется на адресное пространство вашего процесса только на то время, пока активно его родительское диалоговое окно. Это означает, что всякий раз, когда пользователь открывает это диалоговое окно, элемент приходится загружать заново. Благодаря дисковому кэшу повторная загрузка проходит быстрее первоначальной. Тем не менее, чтобы повысить производительность, можно заблокировать ActiveX-элемент в памяти. Для этого в переопределенную функцию OnJuttDiatognocne вызова функции базового класса добавьте следующую строку:
AfxOlelockControl(m_grid.GetClsid());
ActiveX-элемент останется спроецированным на адресное пространство процесса до тех пор, пока не завершится программа или не будет вызвана функция AfxOleUnlochConiroL
Пример ЕХ08А
Пришло время создать приложение, использующее элемент управления Calendar в диалоговом окне. Этапы создания программы ЕХ08А.
Убедитесь, что элемент управления Calendar зарегистрирован. Если Вы не обнаружили элемент управления на странице Registered ActiveX Controls в Visual C++ Gallery, то скопируйте файлы MSCalocx, MSCaLhlp и MSCal.cn t в системный каталог и зарегистрируйте элемент управления, запустив программу REGCOMB
Используя Арр Wizard, создайте проект \VCPP32\EX08A\EX08A. Примите параметры, предложенные по умолчанию, но выберите Single Document Interface и откажитесь от Printing And Print Preview. Убедитесь, что в диалоговом окне AppWizard Step 3 установлен флажок ActiveX Controls, как показано ниже на рисунке..
Установите элемент управления Calendar в проект ЕХ08А. В Visual C++ перейдите из меню Project в подменю Add То Project и выберите команду Components And Controls. Откройте папку Registered ActiveX Controls и выберите Calendar Control 8.0. Classwizard сгенерирует в каталоге ЕХ08А два класса, как показано на рисунке ниже.
Отредактируйте класс элемента управления Calendar для обработки справочных сообщений. Добавьте в Calendar.cpp следующий код таблицы сообщений:
BEGIN NESSAGE_MAP(CCalendar. CWnd)'4
ON WM HELPINFD()
END MESSAGE MAP<>•
В тот же файл добавьте функцию OnHelpinJo^cM- на стр. 132):
BOOL CCalendar::OnHelpInfo(HELPINFO* pHelpInfo)
{Л
// Измените следующую строку в соответствии с Вашей системой
: :№tnHelp(GetSafeHwnd(), "c:\\winnt\\8ystem92\\mscal.hlp", HELP_FINDER, 0);
return FALSE;
}'
В Calendar.h добавьте прототип функции и объявление таблицы сообщений:
protected:
afx msg BOOL OnHelpInfo( HELPINFO* pHelpInfo);'
PECLARE_MESSAGE_HAP()
Функция ОпНе1р1п/<№зывается, когда пользователь нажимает клавишу F1, при условии что фокус ввода установлен на элементе управления Calendar. Мы вынуждены добавлять код вручную, поскольку ClassWizard не изменяет сгенерированные классы ActiveX-элементов.
ПРИМЕЧАНИЕ Макроопределение ON_WMJHELP!NFO используется для привязки сообщения WM_HELP, нового для MicrosoftWindows 95 и MicrosoftWindows NT 4.0. Вы можете использовать ON_WM_HELPINFO в любом классе диалогового окна или окна представления, чтобы затем вызывать в его обработчике любую справочную систему. В главе 21 описана контекстная справочная MFC-система, отчасти предшественница сообщения WM_HELP.
Используя редактор д иалоговых окон, создайте новый ресурс д иалогового окна. Перейдите в Visual C++ из меню Insert в подменю Resource и выберите команду Dialog. Новому диалоговому окну будет присвоен идентификатор IDD_DIALOG1, измените его на IDD АС71УЕХО1АЬОСИзмените заголовок на ActiveX Dialog я установите для диалогового окна свойство Context Help (на странице More Styles). Оставьте созданные по умолчанию кнопки ОК и Cancel с идентификаторами ШОК и IDCANCEL и добавьте другие элементы управления в соответствии с рисунком 8-1. Сделайте кнопку Select Date кнопкой по умолчанию. Перетащите Calendar с палитры элементов управления, затем задайте нужный порядок обхода по нажатию Tab. Присвойте элементам управления идентификаторы в соответствии со следующей таблицей.
Элемент управления
Идентификатор
Calendar
, Кнопка Select Date
Поле ввода
Поле ввода
Поле ввода
Кнопка Next Week
IDCjCALENDARl
IDC_S£LECTDAT£
1DC_DAY
IDCJMONTH■
IDCJEAR
ЮС NEX'l'WEEK
Используя ClassWizard, создайте класс CActiveXDialogEaiH Вы запустите ClassWizard прямо из окна редактора диалоговых окон, мастер уже будет знать, что Вы хотите создать класс, производный от CDialag, на базе шаблона lDD_ACTlVEXDIAI.Otin.ipHKans параметры, предлагаемые по умолчанию, и назовите класс CActiveXDialog.
Щелкните ярлычок Message Maps и добавьте перечисленные в таблице функции-обработчики сообщений. Чтобы вставить обработчик сообщения, щелкните идентификатор объекта, нужное сообщение и кнопку Add Function. Если появится диалоговое окно Add Member Function, то введите имя функции и щелкните ОК.
Идентификатор объекта
Сообщение
Функция-член
CaciiveXDialog
WMJNITDIALOG
OnlnitDialog (виртуальная)
I DC-CALENDAR 1
NewMonth (событие)
OnNewMontbCalehdarl
Идентификатор объекта
Сообщение'
Функция-член
IDCJELECTDATE
BN_CLICKED
OnSeiectDate
idc_nex‘i'^eek
bn_clicked
OnNextWeek
1DOK
BN_CLICKED
OnOK (виртуальная)
Используя ClassWizard,добавьте к классу CActiveXDialog переменные-члены. Щел- , книге ярлычок Member Variables и добавьте переменные-члены, показанные ниже на рисунке.
ПРИМЕЧАНИЕ Вы могли бы подумать, что вкладка ActiveX Events предназначена для увязки событий ActiveX-элементов с функциями контейнера. Но это не так — разработчики используют ее для определения событий создаваемого элемента управления ActiveX.
Отредактируйте класс CActiveXDialog. Добавьте переменные-члены m_vatValue"a m_Back- Color и затем отредактируйте код функций-обработчиков OnlnitDialog OnNewMontibCalen- darl, OnSeiectDate, OnNextWeek и OnOK. На рисунке 8-2 показан весь код для класса диалогового окна — новый код в нем выделен полужирным. ■■
ACTIVEXDIALOG.H
//{(AFX_INCLUDES()
«include "calendar.h"-
//11AFX INCLUDES
«if !defined(AFX ACTIVEXDIALOG H 19177890 6F24_11OO_8FD9 00C04FC2A0C2„INCLUDED_) tfdefine AFX. ACTIVEXDIALOG_H__1917789D_6F24_11DO_8FD9_ODC04FC2AOC2„INCLUDED_
#lf _MSC_VER > 1000■
ftpragmaonce
ffendif // _MSC_VER > 1000
// ActiveXOialog.h : заголовочный файл■'
И_"
1111НПШ1ШП1ШШ1ШШННШ111Н1Ш1Н1ГП1ШППШ1Ш1
// Диалоговое окно CActiveXDialog.
class CActiveXDialog : public CDialog
Рис, 8-2, Код для класса CfcctiveXDialogРис, 8-2, Код для класса CfcctiveXDialog**см» след» стр»см» след» стр»
Рис, 8-2, Код для класса CfcctiveXDialog
Рис, 8-2, Код для класса CfcctiveXDialog
*
*
см» след» стр»
см» след» стр»
Рис. 8-2. (продолжение).
{ Ц Создание public:''
CActiveXDialog(CWnd* pParent = NULL); // стандартный конструктор
// Переменные-члены
//{(AFX DATA(CActiveXDialog) enum { IDO = IDD_ACTIVEXDIALOG );
CCalenda r m_calenda r;
short msOay;
short m_sMonth;■ -■• ■ ’
short m sYear; //HAFX.DATA COLeVariant m.varValue;■
unsigned long m_BackColor;
// Переопределяемые функции
// Переопределенные виртуальные функции, сгенерированные ClassWizard //((AFX_VIRTUAL(CActiveXDialog)•
protected;■
virtual void OoDataExchange(CDataExchange* pDX); // поддержка DDX/DDV //}}AFX_VIRTUAL,
// Реализация protected:
// Сгенерированные функции таблицы сообщений•'
//{(AFX_MSG(CActiveXDialog) virtual BOOL OnlnitDialogt);■'' • ■
afxjnsg void OnNewMonthCalendar1();■' ■
afx_msg void OnSelectDateO;
afxjnsg void OnNextWeek();
virtual void OnOK();•'■■
DECLARE JEVENTSINK NAP()•
//}}AFXMSG DECLARE MESSAGE HARO-
//{{AFX_INSERT_LOCATION|}’'
11 Microsoft Visual С+т- будет вставлять дополнительные объявлений-
// непосредственно перед предыдущей строкой.
fiend if // ! defined(AFX_ACTIVEXDIALOG_H_1917789D_6F24_11DO_8FD9_OOC04FC2AOC2__INCLUDED_)
ACTIVEXDIALOG.CPP' - ;
//ActiveXDialog.cpp : файл реализации И
«include "stdafx.h"■■••
«include "ex08a.h"
«include "ActiveXDialog.h" -. ; ■•.
«ifdef „DEBUG
«define new DEBUG NEW ■. -
«undef THIS FILE “
static char THIS_FILE[] = _FILEj2.
«endif
.////////////////////////////////////////////////////////////////////// // Диалоговое окно CActiveXOialog
CActiveXOialog::CActlveXDialog(CWnd* pParent /*=NULL«/) : CDialog(CActiveXDialog::IDD, pParent)
<{
//(<AFX_DATA,INIT(CActiveXDialog)
m_sDay = 0;
»_sMonth = 0;.
m_sYear = 0;
//}}AFX_DATA_INIT
m_BackColor = 0x8000000?;
void CActiveXOialog::DoDataExchange(CDataExchange* pDX)
CDialog::DoDataExchange(pDX);
//((AFX DATA_MAP(CActiveXDlalog)
DOX_Control(pDX, IDC CALENDAR1, m calendar),-
DDX Text(pDX, IDC DAY, m sDay) ;
DDX Text(pDX, IDC MONTH, m_sMonth);
DDX_Text(pDX, IDC_YEAR, m sYear);
//}}AFX DATA MAP
DDX OCColor(pDX, IDC_CALENDAR1sDISPID_BACKC0L0R, ffl.BackColor);
BEGIN MESSAGE MAP (CActiveXDialog, CDialog)
//{{AFX_MSG MAP(CActiveXDialog)
ON_BN CLICKED(IDC SELECTDATE, OnSelectDatO)
ON BN_CLICKED(IDC NEXTWEEK, OnNextWeek)
//}}AFX_MSG_MAP'
END_MESSAGE_MAP()
//////////////////////////////////////////////////////////////////////
// Обработчики сообщений CActiveXDialog
BEGIN EVENTSINK MAP(CActiveXOialog, CDialog)..
//{{AFX EVENTSINK MAP(CActiveXDialog)
ON EVENT(CActiveXDialog, IDC CALENDAR1, 3 /* NewMonth */. OnNewMonthCalendarl, VTS_NONE)
//})AFX EVENTSINK_MAP‘
ENO_EVENTSINK_MAP()..
BOOL CActiveXDialog::OnInitDialog()
CDialog::OnInitDialog();
m„calendar.SetValue(m_varValue); // для VARIANT нет DDX.
return TRUE; // возвратите TRUE, если фокус ввода не установлен на элемент управления'
// ИСКЛЮЧЕНИЕ: Страницы свойств OCX должны возвращать FALSE
}
void CActiveXDialog::0nNewMonthCalendar1()-
AfxMeesageBox("СОБЫТИЕ: CActiveXOialog; :0nltewNonthCal«idarr):
void CActiveXDialog::OnSelectDate() {
CDataExchange dx(this, TRUE);
от, след, стр.
Рис. 8-2 (продолжение)
Ж Text(idx, IDC_DAY. m_sDay);
DDX Text(&dx. IDC_H0NTH, m sMonth);
DDX_Text(tdx, IDCJyEAR, m_sYear); m_calendar.SetDay(^_aDay);
H_oalendar.SetNonth(m_aNonth); m_oalendar.SetYear(m_sYear);
void CActiveXDialog: :OnNextWeek()
^ mcalendar.NextWeek();
void CActiveXDialog: :0n0K()
{
' CDialog::DnOK();
m varValue = m_calendar.GetValue(); // для VARIANT нет DDX }
Функция QnSeleclDate вызывается при щелчке кнопки Select Date. Функция получает зна чения дня, месяца и года из полей ввода и передает их свойствам элемента управления. ClassWizardne может добавить DDX-код для свойства BackColor, поэтому Вам придется сделать это вручную. Кроме того, дня типа VARIANT нет функций DDX, поэтому необходимо добавить в функции OnlnitDialogn ОпОКкод, присваивающий и получающий дату в свойстве Value.
Свяжите диалоговое окно с окном представления. Используя CLassWizard, создайте обработчик сообщения WM_LBUTTONDOWN и отредактируйте его следующим образом: void CEx08aView; :OnLButtonDown(UINT nFlags, CPoint point) {
CActiveXDialog dig;
.dig.m_BackColor - RGB(255, 251, 240); // светло-желтый
COleDateTlme today = COleDateTlme: :GetCurrentTlme();
dig.m_varValue = C01eDateTlme(today.GetYear(), today.GetMonthO,
today. GetDeyO, 0, 0, 0);
if (dlg.DoNodal()== IDOK) {
COleDateTlme date(dlg,e_varValue); AfxReBsage9ox(dete.Format("X8 *d, %Y"));
)'
Код устанавливает светло-желтый цвет фона и текущийдень в качестве используемой да* к показывает диалоговое окно и сообщает, какую дату вернул элемент управления Calendar. Вам нужно включить ActivcXDialog.h в cxOSaVicw.cpp.
Измените виртуальную функцию OnDraw в файле ex08aView.cpp. Чтобы предложить пользователю нажать левую кнопку мыши, добавьте в функцию OnDraw следующую строку:
pDC->TextOut(O, 0, "Щелкните здесь левой кнопкой мши");
Соберите и протестируйте приложение ЕХ08А. Откройте диалоговое окно, введите дату в три поля ввода и щелкните кнопку Select Date. Щелкните кнопку Next Week. Попытайтесь перенести выбранную дату прямо в новый месяц — Вы увидите окно сообщения, вызываемое событием NewMonth. Окончательную дату Вы увидите в другом окне сообщения, щелкнув кнопку OIL Нажмите клавишу F1 для вызова справки об элементе управления Calendar.
ОДляе^ кт.г<зсфйЬм1Й|^^Д Д”ДД
^Просмотрев <£айл ех0^,гс в текстовом ред^КТр!^:, Вы Наверняка^ BW^^a t|^^ ^емеш-а^^ву^^я t^alen^ar из шаблона диалогового р^й!^a f ^
гг#®s#®e^6**,wf ■=.;,« wWll^te
г
г
№$Вч^м;-з^еср>'^дт0? ВДтам,.^»' CTorjtBawOH не передастся ввпрср^цсгветтво^ w ifiB^r^S
^^^вд^с^^^^6нй^дцд|'^эддаяия'^й«й|^^ сжвж Эта функв6гё‘$|^^
Й-£ActiveX-элементы, и даалоговое дтото создается без них; Затем рЙ4язгагружа^Ж^*^^Л ^Блемёнтьт (по их числовым $i|e^^321Щфр? назЕЙ*а^мтФи.^^^ц^пдсигЯ
^рйзиЕует'^И)^ в результате чего они создают.@э6и окна в^нужн^'ме<й^Шф^фвсЙ^'|№^ж ®^^^сВойствД-которые?Йда^^' в графическом редактореДкраюпхм^в^
Кк8ЙУ6б Пр6тэ.вДваищюй фйт^рЗсак.т^йЙЙйЕЙ'нйй ресурсШЖ^ГЙ '''V-^P^^i^^t
£ ^посылаемые этому' окну какАбычными зоевцергамя упра^ф^^ так^^мЙ^ дамь.^ М^^^я^й^®^ ^f^ .^^^ ?®e,i,!Si^^
Й'окна.К8|ав|$^^^ хо^Асйу^-йемеи^входят ^ ^акзи^фиу ^
Ц|ш(1л^$ф^^^ £sc ?;,'<■ ?Г't « ' ; Й , ^li^^i-eM^^'-
|^1 ^Йэ&дая футпщию-член объектаActiveX, Выиомсете решить^^ обращаетесь кфут ^кции^До^йрйего окна. Ис?6йф| элемента управления ActiVdX уз» давно удалено из щаб :^$яу^'ЙййУго«гоДокщ^|^ NlFCs^^работы с настоят
ЙЧ^ЙЬ^ В "тврт№н1эт01М^А^^имеется п^^нш^!^^
Ейся Окном. Вй вызвав» функции пофеэдащка, a ActiveX it MFC обес ж^м Bj&rth^^$®ii&^ Д 1 M$|;f 5ДД Д "ч.#: t£M|||l|y^ К^нб .кбтйеййера ^ йо, объект класса, nppHS^rtjRpro i^j Ф&юйшя г-^1^к>бЙЙ! класса, 1ф<^^1®№ 6т<$Ш Давления ActiveX Эго значит, что в класс" С^пд'.встрфеца 1
Sjk Таким образом, псрдаржка^'|у||_Х^^со стороны MJ^C^^
«обйчной Wintibws. Прям^п^^-йси A<;tivf^^мркйЕ^№®^"
й! ^? будущих *дерс^ ,Л№^^’ ''»
#^СЬ№ элементов управления А
ActiveX-элементы в файлах HTML
Вы видели ActiveX-элемент Calendars модальном диалоговом окне MFC Его можно использовать и на Web-странице. Следующий HTML-код будет работать, если на машине установлен и зарегистрирован элемент управления Calendar:'
<OBJECT
CLAS$ID="clsid:8E27C92B-1264-101C-BA2F-040224009C02"•
WIDTH=3OO HEIGHT=200 BORDERS HSPACE=5 ID=calendar>.
<PARAM NAME="Day" VAUJE=7>
<PARAM NAHE="Manth" VALUE=11>•
<PARAM NAHE="Year” VALUE=1998>.■
</OBJECT>
Атрибут CLASSID, значение которого совпадает co значением в ресурсе диалогового окна из примера ЕХ08А, идентифицирует в реестре элемент управления Calendar, благодаря чему средство просмотра может загрузить ActiveX-элемент.
Создание ActiveX-элементов в период выполнения
Вы видели, как, используя редактор диалоговых окоп, можно добавлять ActiveX-элементы в период разработки программы. Если же необходимо создать элемент управления в период выполнения, не применяя шаблоны ресурсов, сделайте следукшуе.
Добавьте компонент в проект. ClassWizard создаст файлы для класса-оболочки.
Добавьте в класс диалогового окна или другой оконный С++-класс переменную-член типа класс-оболочка. Внедренный таким образом С++-объект будет создаваться и разрушаться вместе с оконным объектом. .
В Visual C++ из меню View выберите Resource Symbols. Добавьте константу-идентификатор для нового элемента управления.
Если родительское окно — диалоговое, то, используя ClassWizard, создайте обработчик сообщения WM_1NITDIALOG, переопределяя тем самым функцию СОгд/о^еОи/пйО/я/о^Для других окон создайте обработчик сообщения WM CREATE. В любом случае новая функция должна вызывать функцию-член Create добавленного элемента управления ActiveX, Этот вызов неявно приведет к отображению нового элемента управления в диалоговом окне. Элемент управления будет уничтожен при уничтожении родительского окна.
В классе родительского окна вручную добавьте нужные обработчики событий для нового элемента управления, Не забудьте добавить макросы таблицы получателей событий.
СОВЕТ ClassWizard не поможет в работе с таблицей получателей событий, если ActiveX-элемент со вдается в проекте динамически. Попробуйте вставить элемент управления в диалог в другом, временном проекте. После создания всех получателей событий просто скопируйте код в класс родительскоп; окна в основном проекте.
Пример ЕХ08В
Microsoft Internet Explorer 4.x стал ведущим средством просмотра Web, Основная часть его функциональности содержится в одном большом ActiveX-элементе Shdocvw.dll. Запускам Internet Explorer, Вы вызываете маленькую программу-оболочку, которая загружает этот элемент управления WebBrowser в свое основное окно.
ПРИМЕЧАНИЕ Полную документацию к свойствам, методам и событиям элемента управления WebBrowser Вы найдете в Internet SDK, который можно загрузить с httpAKwww.microsofLcom. Документация, естественно, дана в формате HTML.
Благодаря такой модульной архитектуре Вы можете очень легко написать свою программу-браузер. ЕХ08В создает двухоконный браузер (см. рисунок на стр. 138), в котором выводится страница системы поиска, а рядом — искомая страница.
Данное окно представления содержит два элемента управления WebBrowser, которые занимают всю клиентскую область. Когда пользователь щелкает ссылку в поисковом (правом) элементе управления, программа обрабатывает эту команду и перенаправляет ее в целевой (левый) элемент управления.
Этапы создания программы ЕХ08В.
Убедитесь, что элементуправления WebBrowsersaperHCTpnpoBaH. У Вас, несомненно, установлен Microsoft Internet Explorer 4.x, поскольку он необходим для Visual C++ 6l0, поэтому элемент управления WebBrowser должен быть зарегистрирован. Если нужно, загрузите Internet Explorer с http: Wwww.mic г osoft. со m.
С помощью AppWizard создайте проект \VCPP32\EX08B\EX08B. Примите параметры, предложенные по умолчанию, но выберите Single Document Interface и откажитесь от Printing And Print Preview, Убедитесь, что флажок ActiveX Controls установлен, как в примере ЕХ08А.
Установите элементуправления WebBrowser в проект ЕХ08В. Перейдите в Visual C++ из меню Project в подменю Add То Project и выберите команду Components And Controls. Откройте папку Registered ActiveX Controls и выберите Microsoft Web Browser. ClassWizard сгенерирует класс-оболочку CWebBrowser и добавит файлы в ваш проект.
Добавьте в класс CExOSbViewnee переменные-члена типа CWebBrowser, Щелкните в окне Workspace вкладку ClassView и затем щелкните правой кнопкой мыши класс CEx08bView; Выберите команду Add Member Variable и заполните поля ввода в диалоговом окне, как показано ниже на рисунке.
Повторите то же самое для mjargel. ClassWizard добавит оператор *тс1и<3едля файла webbrowser.h.,'
Добавьте константы-цдентификаторыдочернихокондлядвухэлементовуправ- ления. В Visual C++ из меню View выберите команду Resource Symbols и добавьте символы ID_BROWSER_SEARCH и Ю„BROWSER„TARGET.
Добавьте статическую переменную-член тапа массив символов для хранения URL системы поиска AltaVista. В объявление класса в ex08bVicw,hдoбaвьтe следующую статическую переменную: private:■'
static const char s_engineAltavista[];'
Затем в файл cxOSbVicw.cpp вне кода всех функций добавьте следующее определение:
const char CEx08bView:'.s_engineAltavista[] =
“http://altavl8ta.digital.com/";
Используя Classwizard, создайте обработчики сообщений WM_CHEATE и WM_SIZE для класса «вид*. Затем измените код обработчиков в ex08bView.cpp следующим образом: int CExOSbView::OnCreate(LPCREATESTRUCT IpCreateStruct)
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
DWORD dwStyle = WS_VISIBLE I WS.CHILD;
if (m search. Create(NULL, dwStyle, CRect(O, 0, 100, 100), this, ID_BROWSER_SEARCH) == 0) {
AfxMessageBox("He могу создать поисковый элемент управленияI\n");
return -1;
m_search.Navigate(8_englneAltavista. NULL, NULL, NULL, NULL);
if (m target.Create(NULL, dwStyle, CRect(0, 0, 100, 100),
this, ID_BROWSER_TARGEF) =* 0) {
AfxMessageBox("He ногу создать целевой элемент управленияI\n");
return -1;■
m_target.GoHome(); И как задано в параметрах Internet Explorer 4
return 0;
void CExOBbView:;0nSize(UINT nType, int ex, int cy)'
(
CView::OnSize(nType, ex, cy);
CRect rectClient;
GetClientRect(rectClient); .
■ CRect rectBrowse(rectClient);
rectBrowse.right = rectClient.right / 2;
CRect rectSearoh(rectClient);
rectSearch.left = rectClient.right / 2;
m_target.SetlVidth(rect8rowse.right - rectBrowse. left); " m_target.SetHelght(rectBrowse.bottom - rectBrowse.top);
m_target.UpdateWindow();'‘'
m_search.SetLeft(rectSearch.left);
m_search.SetWidth(rectSearch.right - rectSearch. left);
m_search.SetHeight(rectSearch.bottom - rectSearch.top);
m_search.Updatp)iindow();■.
}. ■ ■.
Функция OnCrecrte создает внутри окна представления два окна браузера. Правое отображает основную страницу системы поиска AltaVista, а левое — домашнюю страницу, заданную в параметрах Интернета в панели управления. Функция OnSize, вызываемая при всяком изменении размера окна представления, гарантирует, что окна браузера полностью заполняют окно представления. Функции-члены SetWidtb и SerHeigbtKnacca CWebBrowser позволяют установить свойства Width и Height. '' ”'
Добавьте в файлы exOSbView макросы получателей событий. Class Wizard не может выполнить привязку событий для динамически создаваемых ActiveX-элементов, поэтому Вы должны сделать это вручную. Добавьте следующие строки внутри объявления класса в файле ex08bVlew.h:
protected:
afx_msg void OnBeforeNavlgateExplorer1(LPCTSTR URL, long Flags,
LPCTSTR TargetFrameName, VARIANT PAR* PostData, LPCTSTR Headers, BOOL FAR* Cancel)?
afxjnsg void 0nTitleChangeExplorer2(LPCTSTR Text);
DECLARE_EVENTSINK_MAP()
Затем добавьте следующий код в файл ex08bView.cpp:
BEGIN_EVENTSINK_MAP(CEx08bVlew, CView)
0N_EVENT(CEx08bView, ID_BROWSER_SEARCH, 100, OnBeforeNavigateExplorerl,
VTS BSTR VTS 14 VTS.BSTR VTS PVARIANT VTS BSTR VTS.PBOOL)
ON_EVENT(CEx08bView, IDJROWSERJARGET. 113, OnTitleChangeExplorarS, VTS BSTR)
ENO_EVENTSINK„MAP()
Добавьте две функции-обработчики событий. Добавьте в файл exOSbView.cpp следующие функции-члены: ,'
void CExOBbView::0nBeforeNavigateExplorer1(LPCTSTR URL,
long Flags, LPCTSTR TargetFrameName, VARIANT PAR* PostData, LPCTSTR Headers, BOCL PAR* Cancel)
TRACE("CExOBbView::OnBeforeNavigateExplorerl - URL = te\n", URL)?
' if (!strnicmp(URL,e_engineAltavistalstrlen(8_engineAltaviBta))) {.
return;
) m_target.Navigate(URL. NULL, NULL, PostData, NULL); ♦Cancel = TRUE?
void CExOBbView: :0nTitleChangeExplorer2(LPCTSTR Text)
// Остсржно! Событие может быть получено раньше, // чей мы будем готовы.
CWnd* pWnd = AfxGetApp()->m pMainWnd;
if (pWnd != NULL) {.
if (::IsWindaw(pWnd->m_hWnd)) { pWnd->SetWindowText(Text);
>
Обработчик OnBeforeNavigateExplorerl вызывается, когда пользователь щелкает ссылку на поисковой странице. Функция сравнивает URL ссылки (в строковом параметре USL)c URL системы поиска. Если они совпадают, то навигация продолжается в окне поиска—в противном случае навигация прекращается и вызывается метод Navigate для целевого окна. Обработчик OnTitleChangeExplor«2 обновляет заголовок окна ЁХОбВв соответствии с заголовком целевой страницы.
Соберите и протестируйте приложение ЕХ08В. Выполните какой-нибудь поиск на странице AltaVista, затем просмотрите информацию, появившуюся на целевой странице.
Пример ЕХ08С
Пример ЕХ08В умышленно сделан простым, чтобы четко проиллюстрировать использование элемента управления WebBrowser. Однако искушение улучшить программу, чтобы ее можно было использовать в качестве основного средства просмотра Web, очень велико. В результате появилась программа ЕХ08С, которая использует особенности MFC, описанные в следующих главах книги. Среди этих особенностей:
разделяемое окно (splitter window) с вертикальным перемещаемым разделителем окоп средства просмотра;
использование реестра для запоминания поисковой и целевой страниц;
печать поисковой и целевой страниц;
поддержка нескольких систем поиска;
кнопки панели инструментов для навигации, печати и выбора системы поиска;
। • вывод в строке текущего состояния и выбранного URL..
Свойства-картинки■
Некоторые элементы управления ActiveX поддерживают свойства -картинки (picture properties), значениями которых могут быть растровые изображения, метафайлы и значки. Если у ActiveX-элемента есть хотя бы одно такое свойство, то ClassWizard при установке данного элемента управления в проекте генерирует класс CPicture, Вы не обязаны использовать этот класс, однако вынуждены применять MFC-класс CPictureHolder.)S^a доступа к объявлению it коду класса CPictureHolder добавьте в StdAfx.h строку:
«include <afxctl.h>
Предположим, у Вас есть ActiveX-элемент со свойством-картинкой по имени Pic toe. Вог как можно инициализировать эго свойство растровым изображением из ресурса:
CPictureHolder plot;
pict.CreateFromBitmap(IDBJWBITHAP): // из ресурса m.control.SetPicture(pict.GetPictureDispatch());
ПРИМЕЧАНИЕ Подключив файл AfxCtl.h, Вы не сможете статически скомпоновать свою программу с MFC-библиотекой. Чтобы создать автономную программу, поддерживающую свойства-картинки, Вам придется позаимствовать код класса CPictureHolder. который содержится в файле \Program Files\Microscfl Visual Studio\VC98\mfc\src\ctlpict.cpp.
Связываемые свойства: уведомления об изменении
Если ActiveX-элемент имеет свойство, обозначенное как связываемое (bindable), то элемент управления буд ет посылать уведомление OnCibangedсвоему контейнеру, когда значение свойства внутри элемента управления изменяется. Кроме того, ActiveX-элемент может послать уведомление OnRequestEditsra свойства, чье значение должно измениться. Если контейнер вернет FALSE из обработчика OnRequestEdit, то элемент управления не должен изменять значение свойства,
MFC полностью поддерживает для контейнеров ActiveX-элементов уведомления об изменении свойств, однако в версии Visual C++ 6.0 соответствующая под держка со стороны ClassWizard отсутствует. Эго значит, что Вам придется вручную добавлять записи в таблицу получателей событий в классе контейнера.
Допустим, у Вас есть элемент управления ActiveX со связываемым свойством Note, диспетчерский идентификатор которого равен 4. Тогда Вы добавляете в таблицу получателей событий макрос О.Ч-РЙОР^ОТ/ГОледуюнщм образам:
BEGIN EVENTSINK MAP(CAboutDlg, CDialog)
//{{AFX_EVENTSlNK_MAP(CAboutDlg)
H ClassWizard разместит здесь
// другие макросы уведомления о событиях
//}|AFX_EVENTSINK_MAP
ON PROPNOTIFY(CAboutDlg, IDC_MYCTRL1, 4, OnNoteRequestEdit, OnNoteChanged)
IEND_EVENTSINK_HAP()
Затем Вы должны написать код функций OnNoteRequestEditH OnN'oteCbanged, возвращаемые значения и параметры которых должны в точности соответствовать приведенным ниже: BOOL CMyDlg::OnNateRequestEdit(BOOL* pb)
TRACE( "CMyDlg;: OnNoteRequestEditW);
•pb = TRUE; // TRUE означает разрешение на изменение значения
return TRUE;
BOOL CMyDlg::OnNoteChanged() {
TRACE("CMyDlg::OnNoteChanged\n");
return TRUE;
Соответствующие прототипы надо добавить в заголовочный файл класса:
afx_msg BOOL 0nNoteReqestEdlt(800L* pb);
afx_msg BOOL OnNoteChanged();
Другие элементы управления ActiveX
Вы, вероятно, обратили внимание, что ваш диск заполнен ActiveX-элементами, особенно если Вы загружаете их с Web-узлов. Большинство из этих элементов управления сложно использовать, если у Вас под рукой нет документации, зато у Вас есть широкое поле для экспериментов. Попробуйте элемент управления Marquee.ocx, который поставляется с Visual C++ 6.0. Он прекрасно работает как в программах MFC, так и в файлах HTML Необходимо только присвоить свойству szURL имя другого HTML-файла, содержащего текст для вывода в бегущем окне.
Есть много ActiveX-элементов, разработанных в расчете на программистов на Visual Basic. Например, элемент управления Syslnfo.ocx, поставляемый с Visual C++, позволяет получить данные о системе в виде значений свойств. Программистам на C++ это не очень нужно, так как они в любой момент могут вызвать соответствующие функции Win32. В отличие от объектов MFC, элементы управления ActiveX — это двоичные объекты, не допускающие расширения. Например, Вы не можете добавить к ActiveX-элементу свойство или событие. Вы также не можете использовать известные объектно-ориентированные технологии C++, например^ полиморфизм. Другой недостаток элементов управления ActiveX состоит в том. что они несовместимы со многими передовыми технологиями MFC, такими как архитектура «документ- вид», которую мы рассмотрим позже.
5
ГЛАВА 9
Стандартные элементы управления " Internet Explorer 4
Когда Microsoft выпустила Internet Explorer 4 (IE4), разработчики включили в него новую улучшенную версию COMCTL32.DLL, содержащую стандартные элементы управления Microsoft Windows. Поскольку это обновление не было частью операционной системы, то Microsc t назвала его стандартными элементами управления Internet Explorer 4 (IE4 Common Controls). Обновление включало улучшенные версии всех существовавших элементов управления и ряд новых. Microsoft Visual C++ 6.0 и MFC 6.0 вносят большой вклад в поддержку этих новых элементов управления. В этой главе мы рассмотрим данные элементы и приведем примеры использования каждого из них. Если Вы не работали с обычными или стандартными элементами управления Windows, то обязательно прочитайте главу б прежде, чем начать работу со стандартными элементами управления IE4.
ПРИМЕЧАНИЕ Хотя Microsoft Windows 95 и Microsoft Windows NT 4.0 не содержат новую версию COMCTL32.DLL, но в следующие версии Windows она войдет. На всякий случай, при установке включайте COMCTL32.DLL в комплект для этих операционных систем. Последние новости по данной теме Вы найдете по адресу httD://www.microsoft.com/msdn.
Основы стандартных элементов управления Internet Explorer 4
В примере ЕХ09А используются все новые стандартные элементы управления IE4. На рисунке 9-1 показано диалоговое окно этого примера. Смотрите на рисунок, когда будете читать описание элементов управления.
Элемент выбора даты и времени
Место для ввода даты и времени — типовое поле в диалоговых окнах. До того, как с выходом элементов управления IE4 появился стандартный элемент выбора даты и времени (date and time picker), разработчикам приходилось либо использовать элементы управления сторонних поставщиков, либо создавать подкласс MFC-класса «поле ввода», в котором выполнялась значительная по объему проверка правильности введенных данных. К счастью, новый элемент управления обеспечивает пользователя разнообразными возможностями при выборе даты и времени, а разработчику предоставляет обширную гамму стилей и параметров, Например, даты можно отображать в коротком (14.8,68) или длинном (14 августа 1968) форматах. Время можно вводить в хорошо знакомом пользователю формате чаеы-минуты-сскунды до/после полудня.
Рис. 9-1. Диалоговое окно с новыми стандартными элементами управления
Данный элемент управления также позволяет Вам решить, где пользователь будет указывать дату: в поле ввода, в календаре или в наборном счетчике. Среди других характеристик отметим возможность одиночного или множественного (для указания диапазона дат) выбора и возможность включить/отключить обведение текущей даты красным кружочком. Есть даже режим, позволяющий установить флажок «нет даты». На рисунке 9-1 первые четыре элемента управления с левой стороны иллюстрируют различные конфигурации элемента выбора даты и времени.
В MFC 6.0 новый класс СОиСеТйпеСЛгЕпредаставляег MFC-интерфейс к этому элементу управления. Класс обеспечивает несколько уведомлений, расширяющих программируемость элемента управления. Кроме того, в нем есть функции-члены для работы как со структурой СПте, так и с COleDaleTime,
Для установки даты и времени в CDateTimeCtrl Вы используете функцию SetTime^ а для их получения — GetTime, Используя функцию SetFomat^ можно задать собственные форматы отображения.
,Т1ёт№пЗПГ£д#е5яВДйё’77^^
■ : Большинство программистов, дадно знакомых с MFC, привыкли использовать класс Й ;’ , J лС^гте, Однако,обсколь^допустиьшйдлянегодцапазондатледситькаду I января 1970 Ё$:
-и 18‘января 2038 г,; многим раз^^^шкам потребовалась альтернатива — класс CQteDste-; г ? 2 ТЗЕг^который поддерживает Автоматизацию И'обрабатываетдаты между 1 января 100 гд’| * (И 3£"дейабря 9999 е. Для каждого из классов можно привести аргументы «за» и «йрр< < В , IH^*1 Н#рй?иер, СТгте обрабатывает все. особенности перехода на летнее время, в то •. Цвр^^,(к^Сб/е2^еЛте 44 . ?.>;4 ' \‘ . ; ; ■
у ’^^$1^гйС разработчики вйб^рают CQ&Dater/me из-за его более шйрокаго диапазона.; | 4^йн6ёе приложение, в котором применяется СТйпе, в ближайшие 40 лет придется пере-,* Ц рабсйат^, поскольку значение года в нем неможвт превышать 2038. Чтобы убедиться,.. ■§ |;1|чт6^этб ютрЗтшчение существует, выберите 8 примере ЕХ09А дату за допустимыми для ^4 У'^Й^ пределами. С^довательнб/ выбор класса зависит от ваших конкретных нужд и ;, g;^джйдаем'ого'времени жизшг вашего приловкеНИЯ.;, ': _Л__^_--' '- - ' - ■■■—г ■.-.4 ^^
Календарь на месяц
Большое окно слева внизу на рисунке 9-1 — эго календарь на месяц (Month Calendar), Подобно элемент)' выбора даты и времени, этот элемент управления позволяет выбрать дату. Однако его можно использовать и для реализации небольшого личного ежедневника (Personal Information Manager, PIM). Вы мажете одновременно вывести столько месяцев, на сколько у Бас хватит места — от одного месяца до целого года. В примере ЕХ09А календарь показывас т только два месяца.
Данный элемент управления поддерживает одиночный и множественный выборы, а также позволяет выбрать различные параметры отображения, например, нумерацию месяцев или обведение кружочкам текущего дня. Уведомления позволяют разработчику указать, какие даты выделить полужирным. Выбор этих дат целиком и полностью предоставлен разработчику. Например, Вы мажете обозначить таким образом праздники, назначенные встречи или неудачные дни. В MFC 6.0 этот элемент управления реализован в классе CMontbCalGtrL
Для инициализации класса CMonthCalCtrl вызовите функцию-член SetToday. Функции этого класса, в том числе и SetToday, могут работать как с Clime, так и с COleDateTime.
Элемент ввода IP-адреса
Если Вы разрабатываете приложение, использующее в том или ином виде Интернет или ТСР/ IP, то Вам может потребоваться запросить у пользователя IP-адрес. В состав стандартных элементов управления IE4 входит элемент ввода IP-адреса (IP Address Control), показанный на рисунке 9-1 справа вверху. Кроме получения от пользователя IP-адреса, этот элемент управления выполняет его автоматическую проверку. Поддержку элемента ввода IP-адреса со стороны MFC обеспечивает класс CTPAddressCtrl.
Данный элемент управления состоит из четырех «полей», показанных на рисунке 9-2. Поля нумеруются слева направо.
“ Поле 1
Рис. 9-2. Поля элемента ввода IP-адреса
Для инициализации этого элемента управления вызовите функцию-член SeiAddresss функции OnlnitDiaiogbameTO диалогового окна. SetAddress получает в качестве параметра DWORD, в котором каждый байт соответствует одному полю. В обработчиках сообщений Вы можете вызвать GetAddressy^» получения как DWORD, так и отдельных байтов, представляющих различные поля IP-адреса."
Расширенный комбинированный список
«Старомодный» комбинированный список был разработан в первые годы существования Windows. Его «возраст» и негибкий дизайн создавали множество проблем. При выпуске элементов управления IE-1 Microsoft решила включить в их число гораздо более гибкую версию комбинированного списка, названную «расширенный комбинированный список» (extended combo box).
Данный элемент управления облегчает разработчику доступ к полю редактирования комбинированного списка и обеспечивает больший контроль над ним. Кроме того, Вы можете
присоединить список, изображений (image list) к элементам списка. Вывод графических изображений в новом элементе управления достаточно прост, особенно в сравнении с ранее применявшимися комбинированными списками с программной отрисовкой (owner-drawn). Каждому элементу списка можно сопоставить три типа изображения: выделенное (selected image), невыделенное (unselected image) и перекрытое (overlay image). Эти изображения дозволяют получить различные виды вывода, что Вы и увидите в примере ЕХ09А. Два расширенных комбинированных списка показаны внизу на рисунке 9-1. MFC-класс ССотЬоВохЕх полностью поддерживает расширенный комбинированный список.
Подобно элементу’ управления «графический список», введенному в главе 6, ССотЬоВохЕх можно присоединить к ClmageLisl, который после этого будет автоматически выводить графические изображения около текста в расширенном комбинированном списке. Если Вы уже знакомы с ССотЬоВох, то ССотЬоВохЕх может вызвать некоторое замешательство: вместо строк в нем хранятся элементы типа СОМВОВОХЕХГГЕМ— структуры, состоящей из следующих полей:
VINT mask — набор битовых флажков, определяющих, какие операции выполняются при помощи этой структуры. Например, если в результате операции устанавливается или должно быть получено поле изображения, установите флажок CBETF_IMAGE
intlltem — номер элемента. Подобно обычному комбинированному списку, расширенный элемент управления использует нумерацию, начинающуюся с нуля.
• LPSTR pszText— текст элемента.
i* int ccbTextMax— длина буфера, на который указывает pszText1.
intilmage — порядковый помер (начиная с нуля) в присоединенном спискеиаображсний.
int iSelectedlmage— порядковый номер (начиная с нуля) в присоединенном списке изображений, который будет использоваться для представления «выбранного» состояния.
intiOverla^ — порядковый номер (начиная с нуля) в присоединенном списке изображений, который будет использоваться для перекрытия текущего изображения.
int Undent— количество Ю-пикссльпых отступов.
LPARAMlParam — 32-разрядный произвольный параметр, связанный с элементом.
В примере ЕХ09А Вы непосредственно увидите, как использовать эту структуру.
Пример ЕХ09А
Чтобы проиллюстрировать применение новых стандартных элементов управления IE4, мы построим диалоговое окно, в котором будут создаваться и программироваться элементы управления каждого типа. Далее приведены этапы создания диалогового окна.
Запустите AppWizard для создания проекта ЕХ09А. В Visual C++ из меню File выберите New. затем на странице Projects выберите Microsoft AppWizard (ехе). Примите настройки но умолчанию, за исключением одной: выберите Single Document Interface (SDI). На рисунке на стр. 148 показаны параметры проекта и имена классов.
Создайте новый ресурс диалогового окна с идентификатором IDD_OIALOG1. Разместите на нем элементы управления, как показано на рисунке 9-1.
Вы можете перетащить элементы управления с палитры элементов, показанной в главе 6. Помните, что стандартные элементы управления IE4 находятся внизу палитры. Ниже в таблице перечислены типы элементов и их идент ификаторы.
Только при получении текста элемента. — Ирим^парив.
Последовательность' при нажатии Tab
Тип элемента управления
Идентификатор дочернего окна
1
Группирующая рамка
IDC_STATIC
Статический текст
IDCJTATIC
3
Элемент выбора даты/време ни
IDC_DATETIMEPICKER1
4
Статический текст
IDCJTATICI
Статический текст
IDCJTATIC
б
Элемент выбора даты/време ии
IDC_DATETIMEPfCKER2
7
Статический текст
IDCJTATIC2
8
Статический текст
IDCJTATIC
9
Элемент выбора даты/времеии
IDC_DATETIMEPICKER3
10
Статический текст
IDC STATIC3
II
Статический текст
IDCJTATIC
112
Элемент выбора даты/времеии
IDC_DATETIMEPICKER4
13
Статический текст
IDCJTATIC4
1 1
Статический текст
IDCJTATIC
15‘
Календарь на месяц
1DC_MONTHCAI£NDAR
16
Статический текст
IDCJTATIC5
Группирующаярамка
IDCJTATIC
18
Статический текст
IDCJTATIC '
19
Элемент ввода 1Р-адреса
1DCJPADDRESS1
20
Статический текст
IDCJTATIC6
21
Группирующая рамка
IDCJTATIC
22
Статический текст
IDCJTATIC
23
Расширенный комбинированный список
IDCCOMBOBQXEX1
24
Статический текст
IDCJTATIC7
215
Статический текст
IDCJTATIC
26
Расширенный комбинированный список
1DC_COMBOBOXEX2
2!7
Статический текст
IDCJTATIC8
28
Кнопка
1DOK
29
Кнопка
IDCANCEL
На следующем рисунке показаны все элементы управления и последовательность их обхода при нажатии Tab.
Однако пока не установлены некоторые свойства, наше диалоговое окно не будет выглядеть в точности, как на рисунке 9-1.
Используйте ClassWizardдля создания нового класса CDialogl, производного от CDialog. ClassWizard автоматически запроситу Вас создание нового класса, так как знает, что для ресурса IDD_DIALQG1 нет соответствующего класса C++. Затем создайте обработчик сообщения WNMNITDIALOG.
Установите свойства элементов управления в диалоговом окне. Чтобы продемонстрировать все разнообразие элементов управления, нам нужно установить некоторые свойства каждого из стандартных элементов управления IE4 в данном примере. Далее кратко рассмотрены все устанавливаемые свойства.
т Элемент выбора даты и времени в коротком формате. Чтобы задать для первого, элемента выбора даты и времени короткий формат, установите свойства для IDC_DA- TETIMRPICKERl^aK показано на рисунке ниже.
Элемент выбора даты и времени в длинном формате. Второй элемент выбора даты и времени (.ЮСОАТЕПМЕРККЕРЗ^астройте на использование длинного формата, как показано на первом рисунке на стр. 150.
Элемент выбора даты и времени в коротком формате с дополнительными возможностями. Третий элемент выбора даты и времени (IDC_DATET1MEPICKER3) настройте на использование короткого формата и стилей, показанных на рисунке ниже.
Элемент выбора времени. Четвертый элемент выбора даты и времени (ЮС_ШГ£- ТШ£ИСКЕ54)1астройте на выбор времени, а не даты. Для этого выберите Time из комбинированного списка Format на вкладке Styles, как показано на рисунке ниже.
Календарь на месяц. Для настройки этого элемента управления нужно задать некоторые стили. Сначала на вкладке Styles установите Day States, как показано на рисунке.
Кроме того, по умолчанию календарь в диалоговом окне вообще выглядит не так, как элемент управления — у него отсутствует обрамление. Чтобы этот элемент управления был похож на другие, установите флажки Client Edge и Static Edge на вкладке Extended Styles, как показано на первом рисунке на стр. 151-
Элемент ввода IP-адреса. Для этого элемента управления (IDCJPADDRESSI^ie нужно менять никаких свойств.
Первый расширенный комбинированный список. Добавьте несколько элементов, как показано на рисунке ниже, и убедитесь, что размер списка достаточен для показа некоторых из них.
Второй расширенный комбинированный список. Введите три элемента: Twcety, Mack и Jaws. Далее в примере, используя эти элементы, мы продемонстрируем разные способы графического вывода в расширенном комбинированном списке.
5. Добавьте переменные в класс CDialogl.Запустите ClassWizardn щелкните ярлычок Member Variables для вывода одноименной страницы, Для каждого элемента управления добавьте соответствующую переменную из таблицы.
Идентификатор
Переменная
Тип
IBC_ DATETIMEPICKERl
mMonlbCall
CDateTimeCM
ЮС _ DATETIMEPICKER2
m_MbnthCal2
CDateTimeCtrl
1ВС_ DATET1MEPICKER3
m_MontbCal3
CDateTimeCtrl
IDC_DATETIMEPICKER4
m_MontbCal4
CD«te TimeCtrl
IBC _1PADDRESS 1
mjftrlPCirl
ClPAddressClrt
ЮС _ MONTHCALENDAR I
m_MonrbCal5
CMnntbCdlCtf!
IBC _ STATIC I
m_strDatel
CSIring
ЮС-STATIC!
m_strDatc2
CStrmg
см. след. стр.
Идентификатор
Переменная
Тип'’ .•'■
ЛРС_ STATIC3
m_strDate3
CString■ '
me _STATIG4
.m_strDate4
CString.
ЮС _ STAHC5
m_strDate5
CString'
ЮС_ STATIC6
m_strlPVa'ue
CString.
ЮС _ STATIC?
mjtirCom boExl
CString. .. ■ ■
1DC_STATIC8
m_strComboEx2
CString ■‘
Запрограммируйте короткоформатный элемент выбора даты/времени. В данном, примере нас не интересует дата, которая будет выставлена первоначально в этом элементе управления, поэтому мы никак не обрабатываем его в OnlnilDialog. Однако если необходимо задать дату, то придется в OnlnitDialog вызвать метод SefJ'ime. Кроме того, когда во время выполнения пользователь выбираетновую дату, то соответствующий статический элемент управления автоматически должен обновляться. Для этого при помощи ClassWizard добавьте обработчик сообщения DTN_DATETIMECHANGE. Запустите ClassWizard (нажав CTRL-W), выберите из списка Object IDs идентификатор JDCJMTEThXWPICKRRl^ из списка Messages — сообщение DTN_DATET1MECHANGE. Примите имя по умолчанию и щелкните ОК. Повторите эти шаги д ля каждого из остальных трех идентификаторов IDCJOATE- 1ШВР1СКиК,СЛаб^\7;а1& должен выглядеть примерно так.
Затем добавьте в обработчикдля элемента управления Datetimepicker 1 следующий код:
void CDialogl: ^nDatetiriechangeDaterimepicker^NMHDR» pNMHDR,
L.RES11LT* pResult)' “
< CTime ct; m_MonthCal1.GetTime(ct);.
,m_strDate1. Format(_T ("9tO2d/W2d/%2d"), ct. GetNon th(), ct. GetDay( >, ct. GetYear()) ;
UpdateData(FALSE);
‘pResult = 0;
В этом коде для записи значения времени в переменную ct типа. СТ/тпе используется переменная-член w_MontbCall, соответствующая первому элементу выбора даты/времени, Затем вызывается функция СЗМп&.РогтдГдяя установки текста соответствующего статического элемента управления. Наконец, вызов UpdateDat«(FALS^aiiycKaeT DDX MFC и вызывает автоматическое обновление статического элемента управления.
Запрограммируйте длинноформатный элемент выбора даты/времени. Теперь создадим аналогичный обработчик для второго элемента выбора даты/времени, Void CDlalogl: :0nDatetlmechangeDatetimeplcker2(NMHDR* pNMHDR,
LfiESULT* pResult)
dime ct;
mJlonthCal2.GetTime(ct);
m_strDate2.Farmat(_T("%02d/%02d/X2d"), ct.GetMonth(),ct.GetDay(),ct.GetYear()); UpdateData(FALSE);
«pResult = 0;
Запрограммируйте третий элемент выбора даты/времени. Для третьего элемента выбора даты/времени нужен аналогичный обработчик, но, поскольку мы установили в его свойствах флажок Show None, пользователь может задать NULL-дачу, щелкнув встроенный флажок. Вместо слепого вызова беШтемы должны проверить возвращаемое значение. Если оно не нулевое, то пользователь выбрал NULL-дату, в противном случае — допустимую дату. Как и в двух предыдущих обработчиках, значение объекта СТГхмепреобразуется в строку и автоматически выводится в соответствующем статическом элементе управления:
void CDlalogl: :0nDatetimechangeDatetimepicker3(NWDR« pNMHDR,
LRESULT* pResult)
//ПРИМЕЧАНИЕ: мотет иметь значение NULL!
CTime ct;
int nRetVal = m_HonthCal3.GetTlme(ct):
if (nRetVal) //Если это не ноль, то null; а если это так, делайте то, что следует.
else {else {1 m_strDateS = "ДАТА НЕ ЗАДАНА!!
else {
else {
m stгDate3.Format( T(‘4W2d/%02d/%2d"),ct.GetHonth(), ct.GetDay(), ct.GetYear());
UpdateData(FALSE);
«pResult = 0;
Запрограммируйте элемент ввода времени. Для элемента ввода времени нужен аналогичный обработчик, но фермат вывода вместо «месяцы/дни/годы» будет *часы/мину- ты/секунды»:
void CDlalogl::0n0atetimechangeDatetimepicker4(NMH0R* pNMHDR,
LRESULT* pResult)
CTime ct;
ra_MonthCal4.GetTime(ct);.
f f l_strDate4 .Format (_T ("<<02d:»02d:«2d" ) , ct .GetHour() , ct .GetMinute () , ct .GetSe UpdateData(FALSE);
«pResult = 0;
Запрограммируйте календарь на месяц. Обработчик для календаря похож на обработчик для элемента выбора даты/времени, но между ними есть и некоторые отличия. Во- первых, чтобы определить, что пользователь выбрал новую дату, нужно обрабатывать сообщение MCN_SELCHANGE. Выбор этого сообщения в ClassWlzard показан на первом рисунке на стр. 154.
Кроме обработчика другого сообщения, в данном элементе управлениядля получения даты и времени служитб'е/СыгУе/.ане GetTime. Ниже показан обработчик MCN^SELCHANGE Дли элемента управления «календарь на месяц»,- void COialogl::OnSelchangeHonthcalendarl(44HDP- pNMHDA,
LRESULT* pResult)
{
CTime ct;
m_MonthCal5.GetCurSel(ct);
m_strDateS. Format( T("%02d/%02d/%2d"), ct. GetMonth(),ct. GetDay(), ct. GetYearO);
UpdateData(FALSE);
«pResj’.t = 0;
Запрограммируйте элемент мода IP-адреса. Сначала нужно убедиться, что элемент управления инициализируется. Для этого в данном примере мы передаем ему значение О типа DWORD. Если не выполнить инициализацию, каждое поле элемента управления будет пустым. Добавьте в CZ?ra/og/.-.O«7wzW«^вызов:
m_ptrIPCtrl.SetAddress(OL);
Теперь нам нужен обработчик сообщения для обновления соответствующего статического элемента управления при всяком изменении IP-адреса. Для этого, используя Class Wizard, добавим обработчик сообщения IPN_FIELDCHANGED,KaKnoKa3 анона втором рисунке па стр. 152.
А затем реализуем обработчик следующим образом:
void CDiaiogt: :OiF;eldchangedIpaddress1(NMHDR* pNMHDR, LHESUiГ* pResult)
{
DWORD dwIPAddress; m_ptrIPCtrl. GetAddress(dwIPAddress);
m sTrIPValue.Format("Wd.16d.itd.Wd Wx.Xx.Xx.Wx",
HIBYTE(HIWORO(dwIPAddress)), L0BYTE(HlWORD(dwIPAddress)), HIBYTE(LOWORD(dwIPAddress)), LOBYTE(LOWORD(dwIPAddress)), HIBYTE(HIWORD(dwIPAddress)), LOBYTE(HIWORD(dwIPAddress)), HIBYTE(LO№RD(dwIPAddress)). LOBYTE(LOWORD(dwIPAddress)));
UpdateData(FALSE);
‘pResiCt = 0;
Вызов CiPAddressGti^;GetAddres$ioi&ei^ IP-адрес в локальную переменную dwIPAddress типа DWORD. Далее следует довольно сложный вызов СЗР^^.Роппаг^азделяо'пщй. DWORD на отдельные поля. Здесь используется макрос LOWORDдля получения младшего слова1 и макросы ШВУГЕДОШТ^дя выделения отдельных полей.
Добавьте обработчикдляпервогорасширенногокомбнннрованного списка. Для расширенного комбинированного списка не нужна никакая специальная инициализация, но зато нужен обработчик сообщения CBN_SELCHANGE, приведенный ниже. Можете ли Вы определить, в чем здесь отличия от «обычного» комбинированного списка?
•void CDiaiogt: :OnSelchangeComboboxex1() {
СОИВОВОХЕХПЕИ ebi;
CString str ("просто строка");
CComboBoxEx * pCombo = (CCcmboBoxEx *)GetDlgItem(IDC_COMBOBOXEX1);
int nSel = pConbo->GetCurSel();
cbi.iltem = nSel;
cbi.pszText = (LPTSTR)(LPCTSTR)str;
cbi. mask = CBEIF_TEXT;
cbi.cchTextMax = str.Getlength(); pCombo->GetItem(Scbi);
SetDlgItemText(IDC_STATIC7, str); return;
}
Первое, что Вы, вероятно, заметили — это что вместо простых целых, применяемых для элементов в обычном списке, для расширенного комбинированного списка используется структура СОЛ/ВОЙОАДШУЙИЛолучив описание элемента, обработчик извлекает строку и вызывает SetDigitemTexwia соответствующего статического элемента управления.
' И макрос HIWORDдля получения старшего слова. — Прим.перев.
^Добавьте изображения к элементам второго расширенного комбинированного списка. Первый расширенный комбинированный список не требует специального программирования, Он просто демонстрирует, как реализовать простой расширенный комбинированный список, похожий на прежний, обычный комбинированный список. Но для второго расширенного комбинированного списка потребуется довольно много программировать. Сначала создайте шесть растровых изображений и восемь значков, добавив их в ресурсы проекта, как показано на рисунке ниже.
a
S-^ енОЭа гемаисм
В- SlAeceieatoi Bimap __
-|-fflO3JIHaiRDStLECTiO
1L В idb.bmdog
1hSios.bmXjSSClectes
1ID6 6mf!$h
i-ffilOBJiWlSHSMCTED
1 EbDDialoQ
g-^i«n j k0 idi.blao; i CI.BLUE
-Didi cyan
I S Id GREEN •0 IDLPiJRPlE -0IOI_RED ~«.WE IDLYELLOW IDR.EXO9ATYFE IDRJMINFRAME e-61M«riu Q-SBStiing fable a-® Toctoe i$-^3av«iien
Конечно, Вы можете взять эти изображения с прилагаемого к книге компакт-диска вместо того, чтобы создавать их вручную. Мажете также использовать любые свои растровые изображения и значки.
Существует два способа добавить изображения в расширенный комбинированный список. Первый способ — подсоединить изображения к имеющимся элементам списка. (Помните, что в редакторе диалоговых окон мы добавили в список элементы Tweety, Mack и Jaws.) Второй — указать соответствующие изображения при добавлении новых элементов.
Прежде, чем начать добавлять изображения к расширенному комбинированному списку, давайте создадим в классе CDialogl открытую переменную-член г«_/?и«^е1й/типа Clmage- List. Убедитесь, что Вы добавили переменную в заголовочный файл (Dialogl.li) класса, Теперь мы можем добавить к списку изображений несколько растров и затем «присоединить» изображения к трем элементам, уже добавленным в расширенный комбинированный список. Добавьте в метод OnlnitDialogкласса CDialogl следующий код:
//Инициализация IDC_COMBOBOXEX2
CComboBoxEx* pCombo = (ССовЬоВохЕх») 6etDlgItem(IDC_C0M80BQXEX2); //Сначала добавим изображения к элементам.
//У нас шесть растровых изображений, соответствующих нашим строкам:
//CImageList * plmageLlst = new CImageList<); m_imageLlst.Create(32,16,ILC_MASK, 12,4);
CBitmap bitmap;"
bitmap.LoadBitmap(IDB_BMBIRD);
m.imagelist.Add(Sbitmap. (COLORREF)OxFFFFFF) ; bitmap.DeleteObject();
bitmap,LoadBitmapCIDB BMBIROSELECTEO);
m_imageList.Add(Abltmap, (COLORREF)OxFFFFFF);
bitmap. DeleteObject();
bitmap.LoadBitmap(IDB_BMDOG);
m_imageList.Add(Abitmap, (COLORREF)OxFFFFFF);
bitmap. OeleteObject();
bitmap.LoadBitmap(IDB BMDOGSELECTED);
m_imageLlst.Add(Abitmap, (COLORREF)OxFFFFFF);
bitmap. DeleteObject();
bitmap.LoadBitmap(IDB_BMFISH);
m„imageList.Add(Abitmap, (COLORREF)OxFFFFFF);
bitmap.DeleteObject();
bitmap.LoadBitmap(IDB_BMFISHSELECTED);
m_imageList.Add(Abitmap, (COLORREF)OxFFFFFF);
bitmap. DeleteObjectO;
//Задать список изображений
pCombo->SetImageList(Am_imageList);
//Подсоединить изображения к элементам списка.
COMBOBOXEXITEM cbi;
cbi.mask = CBEIF_IMAGE|CBEIF_SELECTEDIMAGE|CBEIF_INDEMT;
CString strTemp;
Int nBitmapCount = 0;
for (int nCount = 0;nCount < 3; nCount++)
{
cbi.litem = nCount;
cbi.pszText = (LPTSTR)(LPCTSTR)strTemp;
cbi.cchTextMax = 256;
pCombo->GetItem(Acbi);
cbi.llmage = nBitmapCount++;
cbi.ISelectedImage = n8itmapCount++;
cbi. Undent = (nCount & OxO3>;
pCombo->SetItem(Acbi);
}'
Сначала, используя GetDlgltem, получаем указатель на элемент управления. Затем вызываем Create для выделения памяти под изображения и инициализации списка изображений. Следующая серия вызовов загружает каждое растровое изображение, добавляет его в список изображений и удаляет ресурс, вьщеленный при загрузке.
Для привязки mjhnageLisfK. расширенному комбинированному списку вызывается ССотЬо- ВохЕх:;5е«т<г§еЫ$Г.Далее инициализируется структура COMBOBOXEXITEM, и в цикле/ог от 0 до 2 для каждого элемента устанавливаются изображения в выбранном и невыбран- ном состоянии. Переменная nBitmapCount задает идентификатор растрового изображения, помещаемый в структуру COMBOBOXEXITEM. В теле цикла сначала вызывается ССотЬо- ВохЕх::6егВет,чтобы получить информацию о каждом элементе расширенного комбинированного списка. Затем для элемента задаются растровые изображения, и вызывается CCornboB&xEx-Setitem для передачи измененной структуры COMBOBOXEXITEM обратно в список.
14-Добавьте элементы в расширенный комбинированный список. Другой способ размещения изображений в расширенном комбинированном списке — добавление их динамически, как показано в коде, добавленном в OnlnitDialog-.
HlCOH hlcon[8j; int n;
//Теперь давайте добавим несколько цветных значков hlcon[0] = AfxGetApp()->LoadIcon(IDI_WHITE);
Исол[1] = AfxGetApp()->LoadIcon(IDI_BLACK); hlcon[2] = AfxGetApp()'>LoadIcon(IDI_RED): hlcon[3] - AfxGetApp()->LoadIcon(IDI_BLUE); hlcon[4] « AfxGetApp()->LoadIcon(IDI_YELLOW); hlcon[5] = AfxGetApp()->LoadIcon(IDI_CYAN); hlcon[6] = AfxGetApp()->LoadIcon(IDI PURPLE); hlcon[7] - AfxGetApp()->LoadIcon(IDI_GREEN); for (n. = 0:, n < 8; n++) (
ralmageList.Add(hlcon[n]) ;
static char* color[] = {"белый", "черный", "красный", "синий", "желтый", "бирюзовый", "фиолетовый", "зеленый"};
cbi.mask = CBEIF_IMAGE|CBEIF_TEXT|CBEIF_OVERLAY|CBEIF_SELECTTDIMAGE;"
for (n = 0; n < 8; n++) { cbi.iltem = n;
cbi.pszText = color[n]; .
cbi.linage = n+6;//6 - это смещение в списке изображений
cbi.lSelectedlinage = n+6;// на шесть уже добавленных элементов...
cbl.iOverlay = ш-6;
int nltem = pCombo~>InsertItem(Acbi);
ASSERT(nItem == n);
}
Добавление значков в вышеприведенном коде напоминает пример ЕХОбА в главе 6. В дик- пеfor заполняется структура СОМВОВОХЕХТГЕМу. затем вызывается CComboBoxExahisert- Uem для добавления каждого элемента.
Добавьте обработчик для второго расширенного комбинированного списка. Это, в сущности, тот же самый обработчик, что и для первого:
void catalog!::OnSelchangeComboboxex2()
С0МВ0В0ХЕХ1ТЕМ ebi;
CString str ("просто строка");
CComboBoxEx * pCombo = (CComboBoxEx ♦)GetDlgItem(IDC_COMBOBOXEX2);
int nSel = pConbo->GetCurSel();
cbi.iltem = nSel;
cbi.pszText = (LPT8TR)(LPCTSTR)str;
cbi.mask = CBEIF_TEXT;
cbi.cchTextMax = str.Getlength();
pCombo->GetItem(4cbi);
SetDlgItemText(IDC_STATIC8, str);
return;
Свяжите диалоговое окно с классом «вид». Добавьте следующий код в виртуальную функцию OnDraw а файле ex09aView.cpp, Кед, выделенный полужирным, заменяет имеющийся код.
void CEx09aView::OnDraw(CDC* pDC)
pDC->TextOut(O, 0, "Нажмите здесь левую кнопку мыви.");
Используя ClassWizard,добавьте функцию-член OnLButtonDewn к классу СЕх09а-
View, Отредактируйте код, сгенерированный ClassWizard, следующим образом:
void CEx09aView::OnLButtonDown(UIHT nFlags, CPoint point)
{'
CDialogt dig;
dlg.DoModalO;
}
Добавьте оператор *include «Dialogl.h* в файл ex09aView.cpp.
Соберите и запустите программу. Теперь Вы мажете экспериментировать с различными стандартными элементами управления IE4, чтобы увидеть их в работе и понять, насколько они годятся для ваших приложений.
7 Дак. №787
Управление памятью в Win32 '
Оабудьтевсе, что Вы знали об управлении памятью в Winl6, Некоторые из функций управления памятью в Winl6, такие, как GlobalAlloc, есть и в Win32, но только потому, что это позволяет разработчикам быстро переносить исходный кед. По сути же эти функции устроены совершенно по-другому, да и появилось множество новых функций.
Мы начнем эту главу с хорошей порции теории управления памятью в Win32, включая фундаментальные функции управления кучей (динамическираспределяемой памятью) (heap). Затем Вы увидите, как операторы C++ new и delete связываются с «нижележащими» функциями управления кучей, Наконец, Вы узнаете, как пользоваться функциями для спроецированных в память файлов (memory-mapped files), а также получите несколько практических советов по управлению динамически распределяемой памятью. Эта глава никоим образом не претендует на всеобъемлющее изложение методов управления памятью в Win32. Данная тема подробно освещена в книге Джеффри Рихтера (Jeffrey Richter) Advanced Windows (Microsoft Press, 1995) (Джеффри Рихтер, ♦WindowsdAa профессионалов», M: «Русская Редакция», 1997). (Убедитесь, что у Вас в руках последнее издание — может появиться новая версия, учитывающая особенности Microsoft Windows 98 и Windows NT 5.0.)
ПРИМЕЧАНИЕ Когда шла работа над этим изданием нашей книги, и Windows 98, и Windows NT 5.0 находились на стадии бета-тестирования и еще не были выпущены. Исследовав эти бета-версии, мы выяснили, что управление памятью ме претерпело серьезных изменений.
Процессы и адресное пространство
Прежде чем изучать управление памятью в Windows, надо сначала разобраться в том, что такое процесс (process). Если Вы уже знаете, что такое программа, полдела сделано. Программа — эго EXE-файл, который можно запустить из Windows разными способами. После запуска программа называется процессом. У процесса есть свои собственные память, описатели файлов и другие системные ресурсы. Дважды подряд запустив одну и ту же программу, Вы получите два отдельных процесса. Microsoft Windows NT ТaskManager (вызываемый щелчком правой кнопкой мыши панели задач) и программа PVIEW95 в Microsoft Windows 95 предоставляют детальный список процессов, выполняемых в данный момент, а также позволяют «убить» (kill), т. е. принудительно завершить, те процессы, которые перестали отвечать на запросы. Программа SPYXX показывает взаимосвязи между процессами, задачами и окнами.
ПРИМЕЧАНИЕ На панели задач Windows представлены основные окна, а не процессы. У одного процесса (например, Windows Explorer) может быть несколько основных окон, каждое из которых обслуЖивается отдельным потоком, а у некоторых процессов — ни одного (о потоках см. главу 12).
Важно знать, что процессу выделяется свое «частное* 4-гигабайтное виртуальное адресное пространство, о котором мы подробнее расскажем чуть позже, а пока вообразите, что у вашего компьютера оперативная память размером в сотни гигабайт и каждый процесс получает свои 4 Гбайт. Программа может обращаться к любому байту этого адресного пространства, используя один-единственный 32-разрядный линейный адрес. При этом в адресном пространстве каждого процесса содержится множество самых разных элементов, в том числе: • образ EXE-файла программы;
все несистемные DLL, загруженные вашей программой (включая DLL-модули MFC);
। • глобальные данные программы (как доступные для чтения и записи, так и предназначенные только для чтения);'
стек программы;,
। • динамически выделяемая память, в том числе куча Windows и куча библиотеки С периода выполнения (С runtime library, CRT);
файлы, спроецированные в память;
блокипамяти, совместно используемые несколькими процессами;
локальная память отдельных выполняемых потоков;'
особые системные блоки памяти, в том числе таблицы виртуальной памяти;
ядро, исполнительная система и DLL-компоненты Windows.
Адресное пространство процесса Windows 95
В Windows 95 только нижние 2 Гбайт адресного пространства (от 0 до Ox7FFFFFFF) по-настоящему закрыты, причем доступ к нижним 4 Мбайт этих 2 Гбайт запрещен Стек, кучи и глобальная память, доступная для чтения и записи, проецируются на нижние 2 Гбайт, как, впрочем, и EXE- с DLL-файлами.
Верхние 2 Гбайт совместно используются всеми процессами. Ядро Windows 95, драйверы виртуальных устройств (VxD), код файловой системы, а также такие важные таблицы, как таблицы страниц, располагаются в верхнем гигабайте адресного пространства (от ОхСООООООО до OxFFFFFFFF). Динамически подключаемые библиотеки и спроецированные впамять файлы расположены в диапазоне от 0x80000000 до OxBFFFFFFF. На рисунке 10-1 показана схема распределения памяти двух процессов, исполняющих одну и ту же программу.
Насколько все это безопасно? Посторонний процесс практически не в состоянии перезаписать стек, глобальные данные или память кучи другого процесса, потому что вся эта память, расположенная в нижних 2 Гбайт виртуального адресного пространства, принадлежит одному процессу, и только ему. Весь код EXE- и DLL-файлов помечен какдоступный только для чтения, поэтому нет никакой проблемы в том, что он спроецирован в несколько процессов.
Однако верхний гигабайт адресного пространства весьма уязвим, поскольку в него спроецированы важные данные Windows, доступные для чтения и записи. При ошибке программа может уничтожить важные системные таблицы, расположенные в этой области. А какой- нибудь процесс может повредить содержимое спроецированных в память (0x80000000— OxBFFFFFFF) файлов, потому что эта область совместно используется всеми процессами.
Адресное пространство Windows NT■■■
Процесс в Windows NT имеет доступ только к нижним 2 Гбайт своего адресного пространства, причем самые нижние и самые верхние 64 Кбайт этого диапазона недоступны. Исполняемый файл, динамиче ски подключаемые библиотеки приложения и Windows, а также спроецированные в память файлы располагаются в диапазоне от 0x00010000до Ox7FFEFFFF, Ядро, исполнительная система и драйверы устройств Windows NT располагаются в верхних 2 Гбайт,
„где они полностью защищены от какого-либо искажения со стороны ошибочной программы. Файлы, спроецированные в память, также реализованы надежнее: никакой процесс не может получить доступ к спроецированному в память файлу другого процесса, если только он не знает имя файла и не создал проекцию явно.
Г^юцессАГ^юцессАГ^юцессВГ^юцессВ0XSF4000000XSF400000Совместно используемое пространство памятиСовместно используемое пространство памяти0x100000000x10000000Глобальные данные;доступные для чтения:Глобальные данные;доступные для чтения:изапИсиизапИси0x00400000Рис. 10-1,0x00400000Рис. 10-1,■ и записи■ и записиТипичное распределение виртуальной памяти двух процессов в Windows 95, связанных с одним и тем же ЕХЕ-файлом'-Типичное распределение виртуальной памяти двух процессов в Windows 95, связанных с одним и тем же ЕХЕ-файлом'-
Г^юцессА
Г^юцессА
Г^юцессВ
Г^юцессВ
0XSF400000
0XSF400000
Совместно используемое пространство памяти
Совместно используемое пространство памяти
0x10000000
0x10000000
Глобальные данные;
доступные для чтения:
Глобальные данные;
доступные для чтения:
изапИси
изапИси
0x00400000
Рис. 10-1,
0x00400000
Рис. 10-1,
■ и записи
■ и записи
Типичное распределение виртуальной памяти двух процессов в Windows 95, связанных с одним и тем же ЕХЕ-файлом'-
Типичное распределение виртуальной памяти двух процессов в Windows 95, связанных с одним и тем же ЕХЕ-файлом'-
Устройство виртуальной памяти
Конечно, на самом деле никаких сотен гигабайт оперативной памяти в вашем компьютере нет, Нети сотен гигабайт дискового пространства. Здесь Windows показывает «ловкость рук* настоящего иллюзиониста.
Во-первых, 4-гигабайтное адресное пространство процесса фрагментировано. Программы и элементы данных разбросаны по адресному пространству блоками по 4 Кбайт, выровненными по границам, кратным 4 Кбайт. Каждый такой блок называется страницей (page) и может содержать либо код, либо данные. Когда страница действительно используется, она занимает физическую память, но Вы никогда не узнаете ее физический адрес. Микропроцессор фирмы Intel эффективно преобразует 32-разрядный виртуальный адрес в номер физической страницы и смещение внутри нее, пользуясь двухуровневыми таблицами 4-килобай- тпых страниц, как показано на рисунке 10-2. Заметьте: отдельные страницы могут быть помечены либо как «только для чтения», либо как «для чтения и записи». Кроме того, у каждого процесса свой набор таблиц страниц. Регистр CR3 процессора содержит указатель на страницу каталога, поэтому при переключении с одного процесса на другой Windows просто обновляет этот регистр.
Wi п dowsaarpyxaeT CR3 для текущего процесса
Рис. 10-2. Управление виртуальной памятью в Win32 (на платформе Intel)
Ну вот, теперь объем пространства, занимаемого нашим процессом, сократился с 4 Гбайт до, скажем, 5 Мбайт — прогресс налицо. Но если мы запустим несколько программ (и не забудем собственно Windows!), то нам опять не хватит оперативной памяти. Еще раз обратившись к рисунку 10-2, Вы заметите, что запись таблицы страниц содержит бит присутствия (present), который указывает, находится ли сейчас в памяти данная 4-килобайтная страница. При попытке обращения к странице, отсутствующей в памяти, генерируется прерывание, и Windows анализирует ситуацию, просматривая свои внутренние таблицы. Если обращение к памяти было неверным, мы получим устрашающее сообщение об ошибке страницы (page fault), и программа завершится. В противном случае Windows считает в оперативную память нужную страницу из дискового файла и обновит таблицу страниц, записав в нее физический адрес и установив бит присутствия. Таковы основы виртуальной памяти в Win32.
Диспетчер виртуальной: памяти Windows, стремясь достичь максимальной производительности, определяет моменты чтения и записи страниц. Если какой-то процесс не использовал страницу в течение определенного периода, а другому процессу нужна память, страница выгружается из памяти и вместо нее загружается страница нового процесса. Обычно ваша программа ire знает об этом. Но Вы должны четко понимать: чем интенсивнее дисковый ввод/ вывод, тем ниже производительность, поэтому, чем больше объем оперативной памяти, тем лучше.
Мы употребили слово «диск*, но еще ничего не сказали о файлах. Все процессы совместно используют один большой общесистемный файл подкачки (swap file), в который помещаются (при необходимости) все виды данных для чтения и записи и некоторые виды данных только для чтения. (Windows NT поддерживает одновременную работу с несколькими файлами подкачки.) Windows определяет размер файла подкачки в зависимости от размера ОЗУ и свободного дискового пространства, но существуют способы тонкой настройки размера и физического расположения этого файла,
Однако файл подкачки — не единственный файл, используемый диспетчером виртуальной памяти. Нет особого смысла записывать в этот файл страницы кода. Вместо этого Windows хранит содержимое EXE- и DLL-модулей непосредственно в их дисковых файлах. Поскольку страницы кода помечены как «только для чтения*, то необходимости в их записи обратно на диск не возникает.
Если два процесса используют один и тог же ЕХЕ-файл, то он отображается на адресные пространства обоих процессов. Код и константы никогда не изменяются во время выполнения программы, поэтому можно проецировать файл на одну и ту же область физической памяти. Однако два процесса не могут совместно использовать глобальные данные. С ними Windows 95 и Windows NT поступают по-разному. Windows 95 проецирует в каждый процесс отдельную копию глобальных данных, а вот в Windows NT оба процесса пользуются одной и той же копией глобальных данных до тех пор, пока один из процессов не попытается что- либо записать на страницу. В этот момент страница копируется, после чего у каждого процесса появляется собственная копия, находящаяся по одинаковому виртуальному адресу.
Я'Я'-Vtfin32: сегментные, регистры.м BWin32,:'n^^-Vtfin32: сегментные, регистры.м BWin32,:'n^^вяяве^Рвяяве^РОд-да, эти 1^-разрядныё реликты еще^'«> \-<»ь**в6пй1ё^**еФ#-»*Л1‘ * . -U ->w *»w. ч» -=я<1< е*"*^ *л»Л|—*»ун Tvs ^iZ. - ^/Од-да, эти 1^-разрядныё реликты еще^'«> \-<»ь**в6пй1ё^**еФ#-»*Л1‘ * . -U ->w *»w. ч» -=я<1< е*"*^ *л»Л|—*»ун Tvs ^iZ. - ^/
Я'
Я'
-Vtfin32: сегментные, регистры.
м BWin32,:'n^^
-Vtfin32: сегментные, регистры.
м BWin32,:'n^^
вяяве^Р
вяяве^Р
Од-да, эти 1^-разрядныё реликты еще^'
«> \-<»ь**в6пй1ё^**еФ#-»*Л1‘ * . -U ->w *»w. ч» -=я<1< е*"*^ *л»Л|—*»ун Tvs ^iZ. - ^/
Од-да, эти 1^-разрядныё реликты еще^'
«> \-<»ь**в6пй1ё^**еФ#-»*Л1‘ * . -U ->w *»w. ч» -=я<1< е*"*^ *л»Л|—*»ун Tvs ^iZ. - ^/
^"^ •* "-£.“*.Sr4*й>:^*а В ■’»••••' _' * 7 ♦“ ♦ в;5.--1* с. eer^iiwii^ u t Ч^»^*^• ♦ "3 ^ I
«^микропроцессор доде.г- для трансляции- адресок церод$тем, кат^перед^тьшх туШоу? ^
$$ШХ МР^У?^Ц^йользуЙгс^Г^разрадЕ^^:
8р||||*^||?*^ЙйЙВЙтЙОГО регистра 'назьпваетСя
Дюператавной Шмоз хранится так называемая m^«^J^|||j!|^^^
||жм«1вы ^ppi^'Tщреса уедуа^^ишияш и^^уцфу Жйргё дЦ.; j
*, ссВДе»ЙГЙЙ1:адЙЙ№$&ЗГй стеки. ) ЗЗ/ЬййЬяЙЙ^ режиме размер.эпй"сегменгов<можёг:, i они .мю^’бяпу помечены кф*толькодл чтения* или«для чтения и
^^затптси*. При каждом: $я^1|$$^ к памяти микропроцессор использует селектор для lllroHC^.Ilii^дескрипторов и транслирует .^рес^: г^:; / ,л ? V ? / -
|^ i_18Win3^y каждотр ДЕ|^^^^«дан для кодаГи"один- Дл^^ЕЩйхнгстека. •/ =
1 ^У§з^ЫЬик<ВЙ1б1а0йав8Й - адрес 0 и размер-4 Гбайт. т, с. эти сегменты иепй®ывята№ В результ: ^|тате трансляция адресов ^ановится ненужйой, хотя пароч^-фюков,: чтобы исключить 1 л
сегмента,данный нижние 0 Кбайт, Windows.Bce xg тфшЁЙЙа^ '^К ^^M^ i I
11яйВЙ$1Й^!1Й^оши&фЗапщ^"ад^Й^^!
«ЙВ1^8$я8|^88&1Й^0аЗЙО^1&яЬ&ка^f * '<'
ПРИМЕЧАНИЕ Динамически подключаемая библиотека проецируется непосредственно на свой файл DLL, только если DLL-модуль удается загрузить по заданному базовому адресу. Если DLL статически скомпанована для загрузки по адресу, скажем, 0x10000000, а этот диапазон адресов уже занят другой DLL Windows необходимо «перенастроить» адреса внутри DLL Windows NT копирует модуль при его загрузке в файл подкачки, чтобы переместить его код на другой адрес, a Windows 95 выполняет настройку адресов «на лету» при копировании страниц в память. Поэтому важно, чтобы ваши DLL использовали неперекрывающиесядиапазоны адресов. Если Вы применяете DLL-модули М ЕС, установите базовый адрес своих DLL вне диапазона Ox5F400000-Ox5FFFFFFF. Подробнее о том, как писать DLL, мы поговорим в главе 22. .
Файлы, проецируемые в память, о которых мы поговорим позже, также отображаются напрямую. Они могут быть доступны для чтения и записи и могут совместно использоваться несколькими процессами.
Функция VirtualAlloc: переданная и зарезервированная память
Если вашей программе нужна динамически распределяемая память, то рано или поздно придется вызвать функцию VirtualAlloc. Скорее всего, вызывать ее будете не Вы, а функции Windows или библиотеки С периода выполнения, выделяющие память из кучи. Но, зная, как работает VirtualAlloc, Вы лучше поймете функции, которые к ней обращаются.
Сначала разберемся с понятиями зарезервированная. (reserved) и переданная (committed) память. При резервировании памяти выделяется непрерывный диапазон виртуальных адресов. Если Вы, допустим, знаете, что программа будет оперировать с одним 5-мегабайтным блоком памяти [блоки памяти называют также регионами (regions)], но весь он Вам сейчас не нужен — вызовите VirtualAlloc и в параметре, определяющем тип выделения памяти, укажите MEM_RESERVEa в параметре, задающем размер выделяемой памяти — 5 Мбайт. Windows округляет начальный и конечный адреса региона до значений, кратных 64 Кбайт, и уже не даст вашему процессу повторно зарезервировать память из этого региона. И хотя можно указать начальный адрес региона, лучше оставить это занятие самой Windows, Ну вот, собственно, и все. Больше ничего не происходит — не выделяются ни оперативная памяти, ни пространство в файле подкачки.
Когда Вам всерьез понадобится оперативная память, Вы снова вызовете У1Пиа1А11ос,указзв на этот раз МЕМ_СОММГГ,чтобы передать память из этого региона. Теперь начальный и конечный адреса блока округляются до значений, кратных 4 Кбайт, и в файле подкачки выделяются соответствующие страницы, а также создается нужная таблица страниц. Блок помечается либо «только для чтения», либо «для чтения и записи». Однако оперативная память по- прежнему не выделяется — это произойдет, лишь когда Вы попытаетесь получить доступ к этому блоку памяти. Если передаваемая память не была ранее зарезервирована, ошибки все равно не возникает. А если память уже была передана, то тем более не возникнет никаких проблем. Главное, что перед использованием память должна быть передана.
Чтобы вернуть (decommit) переданную память (по сути, вернуть соответствующим страницам статус зарезервированных), применяется функция VirtualFree-Она может также и освободить зарезервированный регион памяти, но для этого ей надо передать базовый адрес, возвращенный VirtualAlloc в момент резервирования памяти.
Куча Windows и семейство функций GtobalAHoc -
Куча (heap) — это пул памяти какого-либо процесса. Когда программе требуется блок памяти, Вы вызываете функцию, выделяющую память из кучи, а чтобы освободить память — функцию, выполняющую обратное действие. Выравнивание по границам, кратным 4 Кбайт, в этом случае не производится — диспетчер кучи использует пространство на выделенных ранее страницах или обращается к VirtualAlloc за дополнительными страницами. Сначала мы по-
знакомимся с кучами Windows, а затем — с кучами, управляемыми библиотекой С периода выполнения, в частности функциями malloc и new,
Windows предоставляет каждому процессу кучу по умолчанию, а процесс может создать любое число дополнительных куч Windows. Для выделения памяти из куч Windows служит функция HeapAUoc, а для освобождения — функция HeapFree.
Скорее всего, Вы не будете вызывать HeapAUoc ~ за Вас эго сделает унаследованная от Winl6 функция GlobalAlloc. В идеальном мире 32-разрядных программ Вам не понадобилась бы GlobalAttoc, но мы имеем дело с реальным миром. У нас все еще остается колоссальный объем кода, перенесенного из Win 16, в котором вместо 32-р азрядных адресов применяются параметры типа «описатель памяти» (HGLOBAL).
Работа функции GlobalAllocзависит от передаваемых ей атрибутов. Если указать GMEM_- FIXED,она просто вызывает HeapAllocn возвращает адрес, приводя его к 32-разрядному значению HGLOBAL. Если же Вы передаете GMEM_MOVEABLB,возвращаемое значение HGLOBAL является указателем на элемент таблицы описателей данного процесса. В этом элементе содержится указатель на память, выделенную функцией HeapAUoc.
Зачем нужна «перемещаемая» (moveable) память, если она вводит в управление памятью еще один промежуточный слой? Здесь мы встречаемся с наследием Win 16, где операционная система действительно перемещала блоки памяти. В Win32 перемещаемые блоки памяти существуют лишь для поддержки GlobalReAlloc, которая выделяет новый блок памяти, копируе'г в него содержимое старого блока, освобождает последний и помещает адрес нового блока в существующий элемент таблицы описателей. Если бы никто не вызывал GlobalReAlloc, мы могли бы обойтись HeapAUoc вместо GlobalAlloc'.
К сожалению, многие библиотечные функции используют в качестве параметров и возвращаемых значений HGLOBAL, а не адреса памяти. Если такая функция возвращает HGLOBAL. Вы должны считать, что память выделена с атрибутом GMEM_MOVEABLEyi, следовательно, чтобы получить адрес памяти, надо вызвать функцию GlobalLoch, (В случае фиксированной памяти GlobalLock просто возвращает переданный ей описатель как адрес.) Если Вы должны передать параметр HGLOBAL, то для большей надежности следует получить это значение с помощью Globa!AUoc(GMEM_MOVEABLE,...)- на случай, если вызываемая функция обращается к GlobalReAlloc и ожидает, что значение описателя не изменится.
Куча библиотеки С периода выполнения, heapmin и С++~операторы new и delete
Вы можете пользоваться функцией HeapAUoc, но, скорее всего, Вы будете работать с функциями malloc иргее библиотеки С периода выполнения. Если же Вы программируете на C++, то будете работать с операторами new и delete, напрямую вызывающими malloc иртее. Если с помощью new выделяется блок, превышающий определенный предел (по умолчанию 480 байт), то CRT сразу передает вызов функции HeapAUoc — для выделения памяти из кучи Windows, созданной для CRT. Блоками, меньшими указанного предела, управляет сама CRT с помощью кучи малых блоков (small-block heap), вызывая при необходимости VirtualAilocH. VirtualFree. Алгоритм работы следующий.
Память резервируется регионами по 4 Мбайт.
Память передается блоками по 64 Кбайт (16 страниц).
Неверно. Во-первых, для копирования данных в буфер обмена (clipboard) необходимо выделить блок памяти вызовам GlobalAlloc с атрибутами GMEM_MOVEABLE и GMEM_DDESHARE. Во-вторых, выделение памяти с атрибутом GMEM_MOVEABLE позволяет системе перемещать блоки памяти внутри кучи, тем самым не допуская ее фрагментации. — Прим. перев."
Память возвращается по 64 Кбайт за един раз. Если освобождается 128 Кбайт, то возвращаются последние 64 Кбайт.
4-мегабайтный регион освобождается, когда возвращены (decommitted) все его страницы.
Как видите, куча малых блоков сама занимается очисткой. А вот куча Windows автоматически ire возвращает страницы и не отказывается (unreserve) от них. Чтобы очистить большие блоки, Вы должны вызвать функцию CRT heapmin, которая вызывает функцию Windows HeapCompact. (К сожалению, версия HeapCompact для Windows 95 не делает ничего — еще один довод в пользу Windows NT.) Как только страницы возвращены, другие программы могут воспользоваться освободившимся пространствам файла подкачки.
ПРИМЕЧАНИЕ В предыдущих версиях CRT указатели списка свободных блоков памяти хранились в страницах самой кучи. Такой подход требовал от malloc для поиска свободного пространства «перелистывать» (считывать из файла подкачки) множество страниц, что приводило к падению производительности. Современная система, хранящая список свободных блоков в отдельной области памяти, работает быстрее и уменьшает необходимость в дополнительном программном обеспечении для управления кучей.
Если Вы хотите узнать или изменить предельное значение размера блока, то вызывайте функции CRT _set_sbb_tbresboldH. _get_sbb_tbresbold.
Специальная отладочная версия malloc - • jnalloc_dbg—з аписывает в выделяемые блоки памяти отладочную информацию. _malloc_dbg вызывается оператором new, когда Вы собираете MFC-проект с определенным символом JDEBUG. В этом случае программа может обнаружить блоки памяти, которые Вы забыли освободить или нечаянно затерли.
Файлы, проецируемые в память
На случай, если Вам не хватает возможностей управления памятью, рассмотрим еще один вариант. Допустим, программа должна считывать DIB-файл (Device-Independent Bitmap - растровое изображение в аппаратно-независимом формате). Простейшее решение — выделить буфер нужного размера, открыть файл и вызвать функцию чтения, чтобы целиком скопировать дисковый файл в буфер. Но куда элегантнее спроецировать файл в память, т. е. просто отобразить диапазон адресов непосредственно на файл. Когда процесс обращается к какой-то странице памяти, Windows вьщеляет область оперативной памяти и считывает данные с диска. Код выглядит примерно так:
HANDLE hFile = : :CreateFile(strPathname, GENERIC READ,
FILE SHARE READ, NULL, OPEN.EXISTING, FILE_ATTRIBUTE NORMAL, NULL);
ASSERT(hFile!=NULL);
HANDLE hMap = ; :CreateFileMapping(hFile, NULL, PAGE READONLY, o, 0, NULL);
ASSERT(hMap!=NULL);
LPVOID IpvFlle = : :MapViewOfFlle(hMap, FILE_MAP_READ, 0, 0, 0);//спроецировать весь файл
DWORD dwFlleSize = : :GetFileSize(hFile. NULL); // полезная информация
// работа с файлом'
::UnmapViewOfFile(lpvFile);
::CloseHandle(hMap);
;;CloseHandle(nFilc);
Здесь используется виртуальная память, содержимое которой хранится в DIB-файле. Windows определяет размер файла и передает соответствующую область виртуальной памяти. В данном случае ее начальный адрес — IpvFile. Переменная hMap содержит описатель объекта •проекция файла», который при необходимости могут совместно использовать несколько процессов.,
DIB-файл в выше приведенном примере достаточно мал, поэтому его можно было полностью считать в буфер. Но представьте себе файл большего размера, с которым обычно работают при помощи команд поиска (seek). Файлы, проецируемые в память, будут работать и с
таким файлом благодаря системе виртуальной памяти, лежащей в их основе. Память выделяется, и страницы читаются только, когда к ним происходит обращение, и ни в коем случае не раньше..
ПРИМЕЧАНИЕ По умолчанию файл проецируется целиком, хотя можно проецировать лишь его часть.
Если два процесса совместно используют объект «проекция файла» (скажем, ЬМар, как в предыдущем примере), то файл, по сути, становится разделяемой памятью; но виртуальные адреса, возвращаемые MapViewOJFile$$oryr быть различными. Именно таков предпочтительный способ разделения памяти в Win32 (Вызов функции GlobalAIloc с флагом GMEM^SHARE не создает разделяемую память, как было в Win 16.) Если Вам нужно только разделение памяти, а постоянный дисковый файл не требуется, можете не вызывать CreateFile, а передать функции CreateFileMappingB параметре bFile значение OxFFFFFFFF. В этом случае содержимое разделяемой памяти будет храниться в страницах файла подкачки. За подробностями лучше обратиться к книге Рихтера. Примеры ЕХ35В и ЕХ35С в главе 35 иллюстрирую технику проецирования файлов в память.
ПРИМЕЧАНИЕ Если Вам нужен произвольный доступ к небольшому числу страниц спроецированного в память файла, основанного на файле подкачки, воспользуйтесь приемом, описанным Джеффри Рихтером в книге Advanced Windows в разделе «Sparsely Committed Memory-Mapped Files» («Частичная передача памяти проецируемым файлам»). В этом случае CreateRleMapping вызывается со специальным флажком, а затем с помощью VlrtualAlloc передаются только заданные диапазоны адресов.
ПРИМЕЧАНИЕ Советуем обратить внимание на сообщение Windows WM_COPYDATA. Оно позволяет передавать данные между процессами при помощи разделяемой памяти, не обращаясь к API проецируемых файлов. Это сообщение является синхронным, т. е. процесс-отправитель должен ожидать, пока процесс-получатель не скопирует и обработает данные.
К сожалению, MFC не поддерживает проецирование файлов или разделение памяти напрямую. Класс CSbaredFile поддерживает только передачу данных через буфер обмена (clipboard) при помощи описателей HGLOBAL, так что этот класс не настолько универсален, как может показаться из названия.
Доступ к ресурсам
Ресурсы содержатся в EXE- или DLL-файлах и, таким образом, занимают виртуальное адресное пространство, содержимое которого не меняется в течение жизни процесса, поэтому ресурсы легко читать напрямую. Если Вам, допустим, нужен доступ к растровому изображению, адрес DIB можно получить с помощью примерно такого кода:
LPVOID IpvFesource = (LPVOID) : :LoadResource(NUI.L,
; :FindResource(NULL, HAKEINTRESOUaCE(IpB_REDBlopKS), RT.BITHAP));
Функция LoadResource возвращает значение HGLOBAL, но его можно безопасно приводить к указателю.
Советы по работе с кучей
Чем интенсивнее используется куча, тем больше она фрагментируется, и тем медленнее работает программа. Если предполагается, что время непрерывного функционирования программы будет исчисляться часами или днями, следует проявить осторожность. Лучше выделить всю необходимую память при запуске программы и освободить при закрытии, но это не всегда возможно. Тут может помешать класс CString, который постоянно выделяет и освобождает крошечные порции памяти. Однако разработчики MFC недавно внесли в данный класс ряд улучшений.
Не забывайте вызывать _beapmin каждый раз, когда программа выделяет блоки размера, превышающего предел возможного для кучи малых блоков. Внимательно отслеживайте, от-
куда получена динамически распределяемая память: у Вас возникнут жуткие проблемы, если, например, Вы вызовете HeapFree,передав ей указатель па малый блок, полученный от new.
Учтите, что размер стека может быть практически любым. Так как ограничения в 64 Кбайт теперь нет, в стек можно помещать объекты большого размера, что уменьшает необходимость в распределении памяти из кучи.
В отличие от Win 16, ваша программа, работая на полной скорости, не сгенерирует неожиданное исключение (exception), если Windows не хватит файла подкачки. Она просто станет постепенно замедляться и, в конце концов, остановится, что отнюдь не обрадует пользователя. Здесь от Вас мало что зависит — можно лишь пытаться определить, какая программа пожирает память и почему. Поскольку в модулях GDI и USER Windows 95 по-прежнему есть 16-разрядные компоненты, сохраняется вероятность переполнения 64-килобайтных куч, в которых хранятся объекты GDI и оконные структуры. Однако такая вероятность крайне мала, и если происходит именно так, то ошибка, скорее всего, в вашей программе.
Оптимизация хранения констант
Вспомните, что код вашей программы хранится не в файле подкачки, а непосредственно в EXE-и DLL-файлах. Если запущено сразу несколько экземпляров программы, на виртуальные адресные пространства каждого процесса будут спроецированы те же самые EXE и DLL Но что при этом происходит с константами? Предпочтительнее, чтобы они были частью программы, а не копировались в другой блок виртуальной памяти, хранимый в файле подкачки, Чтобы константы гарантированно хранились вместе с программой, придется немного поработать. Во-первых, обратите внимание на строковые константы, которыми часто изобилуют программы. Может, Вы решили, что они будут данными «только для чтения*? Тоща попробуйте угадать еще раз. Действительно, поскольку Вы имеете право написать что-нибудь вроде:
char* pch = "test";
-pch = 1 x';
«test* не может быть константой. Чтобы строка стала константой, ее надо соответственно объявить и инициализировать, например, так'
const char g_pch[] = "test";
Теперь^_рсй хранится вместе с кодом. Но где именно? Чтобы ответить на этот вопрос, надо знать о секциях данных (data sections), генерируемых компоновщиком Visual C++. Если потребовать от компоновщика, чтобы он генерировал файл компоновки (map file), ТО в нем Вы увидите длинный список секций (блоков памяти) вашей программы. Отдельные секции могут быть предназначены для хранения кода или данных, а также могут быть помечены «только для чтения*или «для чтения и записи». Основные секции и их характери стики перечислены в таблице.
Имя
Тип
Доступ
Содержимое
.text
код
только чтение
код программы.
.rdata
данные
только чтение
инициализированные константы
..data
данные
чтение и запись
инициал изированныеданные (не константы)
.bss
данные
чтение и запись
неинициализированные данные (не константы)
Секция .rdata — часть ЕХЕ-файла; именно сюда компоновщикпоместит переменнуюg_pcb. Чем больше данных Вы поместите в эту секцию, тем лучше. Для этого используется модификатор const. В секцию .rdata можно включать встроенные типы и даже структуры, но не объекты C++, у которых есть конструктор. Если написать следующий оператор:
const CRect g_rect(O, 0, 100, 100);
компоновщик внесет этот объект в секцию .bss, а в файле подкачки будет выделено место для хранения отдельной копии объекта для каждого процесса. Если хорошенько подумать, то так и должно быть, потому что компилятор вызывает функцию-конструктор после загрузки программы.
А теперь допустим, что Вы собираетесь сделать самое худшее: Вы объявляете глобальную переменную (или статическую переменную-член класса) типа CString следующего вида: const CString g_str("3To худшее, что можно сделать");
Вы получите объект (String, размер которого весьма мал, в секции .bss и массив символов в секции .data. Ни тот, ни другой не могут храниться в ЕХЕ-файле в период выполнения. Хуже того, при запуске программы класс CString должен вьщелить память из кучи для хранения копии массива символов. Так что в данном случае вместо объекта CString лучше использовать сошТ-массив символов.
ГЛАВА 11
Растровые изображения .
Без графических изображений Windows-программы выглядели бы уныло. Иногда приложение вообще бесполезно без графики, но и любая программа с рисунками станет симпатичнее. Растровые изображения (битовые карты, bitmaps) Windows — это массивы битов, представляющих растровые точки (пикселы) дисплея. На первый взгляд, все очень просто, но, прежде чем применять растровые изображения для создания Windows-приложений профессионального уровня, надо многому научиться.
Эту главу мы начнем с рассказа о «старом» способе работы с растровыми изображениями — о создании аппаратно-зависимых растровых изображений GDI, работающих с контекстом устройства в памяти. Знание этих приемов необходимо, потому что многие программисты по-прежнему пользуются ими, да и Вам надо быть готовыми применить их при случае.
Затем мы перейдем к современному способу работы с растровыми изображениями — созданию аппаратно-независимыхрастроныхизоброжений (Device-Independent Bitmaps, DIBs). DIB упрощает работу с цветами и принтером, а в некоторых случаях позволяет добиться и более высокой производительности. Новая Win32-функция CreateDIBSection предоставляет все преимущества DIB в сочетании с возможностями растрового изображения GDL
Наконец, Вы узнаете, как размещать растровые изображения на командных кнопках с помощью MFC-класса CBitmapHutton. Эта часть никак не связана с DIB, но прием этот очень полезен, и им очень трудно овладеть без примера.
Растровые изображения GDI и DIB
В Windows существует два типа растровых изображений: GDI-растр и DIB. Класс CBitmap библиотеки MFC 6.0 представляет объект «растровое изображение GDI». С этим объектом связана аппаратно-зависимая структура данных Windows, внутренняя для модуля GDL Ваша программа может получить копию данных объекта, но структура битов в ней зависит от конкретной аппаратуры дисплея. Растровые изображения GDI можно свободно передавать между разными программами на одном компьютере, но из-за аппаратной зависимости копировать их на другой компьютер бессмысленно.
ПРИМЕЧАНИЕ В Win32 Вы мажете поместить описатель растрового изображения GDI в глобальный буфер обмена для передачи другому процессу, но Windows при этом автоматически преобразует аппаратно-зависимые данные в DIB, который и копирует в разделяемую память. Это еще один аргумент в пользу DIB.
DIB-изображевия обладают в программировании рядом преимуществ по сравнению с GDI- растрами, Поскольку DIB содержит информацию о цвете, упрощается работа с цветовой палитрой, а также управление градациями серого цвета при печати. Любой компьютер под уп-
равнением Windows способен обрабатывать DIB, которые обычно хранятся в вида дисковых файлов в формате BMP или как ресурсы в EXE- и DLL-файлах. В частности, фоновая картинка на вашем рабочем столе считывается при запуске Windows из BMP-файла. Основным форматом хранения данных для программы Windows Paint служит BMP, a Visual C++ использует BMP-файлы для хранения изображений, например, кнопок панелей инструментов. Есть и другие форматы обмена графическими данными, например TIFF, GIF или JPEG, но Win32 API напрямую поддерживает только формат DIB.
Цветные и монохромные растровые изображения
Возможно, сейчас самое время перечитать раздал «Преобразование цветов в Windows» главы 5. Как Вы увидите в этой главе, Windows работает с цветными растровыми изображениями несколько иначе, чем с цветами кисти.
Многие растровые изображения — 16-цветныс. У обычней VGA-платы четыре цветовых «плоскости», и для представления пиксела используется по одному биту с каждой из них. При формировании растрового изображения задаются четырехразрядные значения цвета. Таким образом, для обычной VGA-платы цвета растрового изображения ограничены 16 стандартными цветами. Составные цвета (dithered colors) в растровых изображениях не применяются.
В монохромном растровом изображении лишь едва плоскость. Каждый пиксел представляется одним битом, который может быть либо включен (1), либо выключен (0). Цвет выключенного состояния задается функцией CDC::SetTextColor, а включенного — функцией SetBk- Cotor. Оба цвета можно определить через Windows-макрос RGB.
Использование GDI-растров
GDI-растр — в общем, всего лишь еще одна разновидность GDI-объектов вроде перьев или шрифтов. Вы должны создать растровое изображение, а затем подключить его к контексту устройства. Закончив работу с данным объектам, его надо отключить от контекста устройства и удалить — технология Вам известна.
Однако есть одно отличие: «растровое изображение» дисплея или принтера — эго, по сути, поверхность экрана или отпечатанной страницы. Поэтому растровое изображение нельзя подключить к контексту устройства «дисплей» или «принтер». Для него надо создать особый контекст устройства в памяти (memory device context), используя функцию CDC::Create- CompatibleDC, а затем — для копирования бигов из контекста в памяти в «настоящий» контекст устройства — использовать функцию-член StretchBU или BitBlt класса CDC. Функции пересылки бигов обычно вызываются из функции OnDraw класса «вид». И, конечно же, не забывайте очищать контекст устройства в памяти по окончании работы.
Загрузка GDI-растра из ресурса
Проще всего использовать растровое изображение, загрузив его из ресурса. На вкладке Resource View окна Workspace можно увидеть список ресурсов проекта, в том числе список растровых изображений. Если выбрать одно из них и просмотреть его свойства, можно увидеть имя файла. Вог пример записи из RC-файла (при просмотре в текстовом редакторе):
IDB_REDBLOCKS BITMAP DISCARDABLE "res\\Red Blocks, bmp"
IDB_REDBLOCKS— идентификатор ресурса; файл называется Red Blocks.bmp и содержится в подкаталоге проекта RES. (Это одно из растровых изображений «обоев» в Windows 95, которые обычно хранятся в каталоге \WINDOWS.) Компилятор ресурсов считывает DIB с дискаи сохраняет его в RES-файле проекта. КомпоновщиккопируетОТВ в EXE- файл программы, Растровое изображение Red Blocks должно храниться в аппаратно-независимом формате, так какданный EXE-файл должен работать с любой видеоплатой, поддерживаемой Windows.
Функция CDC::LoadBitmapiq®o6pasyer DIB, хранящийся в ресурсе, в GDI-растр. Вог простейшая функция OnDraw, способная воспроизводить растровое изображение Red Blocks:
CMyView::OnDraw(CDC* pDC)
{ .
CBitmap bitmap; // последовательность операторов очень важна CDC dcDisplayMemory;
bitmap.LoadBitmap(IDB_REDBLOCKS); '
dcDisplayMemory.CreateCompatibleDC(pDC);
dcDisplayMemory.SelectObject(&bitmap);..
pDC->BltBlt(1OO. 100 , 54 , 96, ^dcDisplayMemory, 0, 0, SRCCOPY);
.// деструктор CDC уничтожает dcDisplayMemory; растровое изображение отключается от контекста // деструктор CBitmap уничтожает растровое изображение
BitBlt копирует пикселы Red Blocks из контекста устройства в памяти в контекст дисплея (или принтера). Ширина растрового изображения 54 бита, высота — 96 битов, оно занимает на VGA-дисплее прямоугольник размером 54 на 96 логических единиц, смещенный на 100 логических единиц вниз и вправо относительно верхнего левого угла клиентской области окна.
ПРИМЕЧАНИЕ Приведенный выше код для дисплея работает отлично. Как Вы увидите в главе 19, каркас приложений вызывает OnDraww для печати; при этом pDC указывает на контекст принтера. К сожалению, здесь растровое изображение сконфигурировано специально для дисплея, и поэтому его нельзя подключить к контексту устройства в памяти, совместимому с принтером. Если Вы хотите распечатать растровое изображение, следует ознакомиться с классом CDib, описываемым далее в данной главе.
Влияние режима преобразования координат
Если в примере с Red Blocks режим преобразования координат — ММ_ТЕХТ,тогда каждый пиксел растрового изображения отображается как один пиксел дисплея, и изображение выглядит отлично. Если же режим преобразования координат — MM_LOENGUSHpa3M.ep растрового изображения на экране VGA будет равен 0.54x0l96 дюйма или 52x92 пиксела в Windows 95, и тогда GDI, чтобы уместить изображение на экране, его немного сожмет. Как следствие, в режиме MMJZ)ENGLIS№ho будет выглядеть уже не так хорошо. Вызов CDC:;SetSlretcbBUModec параметром СО^ОАОАСО^О^приведет сжатые растровые изображения в более пристойный вид.
Растяжение изображений
Как быть, если Вы, допустим, хотите, чтобы Red Blocks занимал прямоугольник размером строго 54x96 пикселов — даже в режиме, отличном от ММ_ТЕХТ?В этом случае используйте функцию StretcbBlt. Если заменить вызов BitBlt тремя следующими операторами, Red Blocks будет корректно отображаться в любом режиме преобразования координат:
CSize size(54, 96);■ :
pDC->D?toLP(&size); pDC->StretchBlt(O, 0, size.ex, -size.су, &dcDisplayMemory, 0, 0, 54, 96. SRCCOPY); '
Если GDI действительно приходится растягивать или сжимать изображения, обновление картинки на дисплее замедляется при использовании как BitBlt, таки StretcbBlt. Если же, как в предыдущем случае, GDI обнаруживает, что преобразования не нужны, вывод на экран выполняется быстро.
Пример ЕХ11А'.
Программа EXI 1А выводит растровое изображение из ресурса в окно представления с прокруткой, используя при этом режим преобразования координат MM_LOENGUSHB программе применяется описанная выше функция StretcbBlt — за исключением того, что контекст устройства в памяти и растровое изображение создаются в функции-члене класса «вид» OMlnitl<tlUpdaten существуют на протяжении всей жизни программы. Кроме того, программа
считывает размер растрового изображения, вызывая функцию-член CGdiObfecl^GetObject-y. е. не пользуется, как в предыдущих примерах, жестко заданными значениями. Далее приведены этапы создания программы EXI 1А.
Используя Арр Wizard, создайте проект \VCPPJ2\EX11A\EX11A. Примите параметры, предлагаемые по умолчанию, но выберите Single Document Interface и базовый класс CScroltViewmia класса CExl 1аУ1еш,как описано в главе 4. Параметры и имена классов по умолчанию показаны на рисунке.
Импортируйте растровое изображение Gold Weave. Выберите из меню Insert в Visual C++ команду Resource. Импортируйте растровое изображение GoldWeave.bmp из каталога \WINDOWS. (Если в вашей версии Windows этого растрового изображения нет, загрузите его с компакт-диска, прилагаемого к книге.) Visual C++ скопирует этот файл с растровым изображением в подкаталог \ies вашего проекта. Назначьте полученному ресурсу идентификатор IDB_GOLDWEAV& сохраните внесенные изменения.
Добавьте закрытые переменные-члены в класс С£х17 aWew. Отредактируйте файл exlOaVlew.h или воспользуйтесь ClassView. Растровое изображение и контекст устройства в памяти существуют на протяжении всей жизни объекта «вид». Объекты типа CSiz;e хранят исходные (в растровом изображении) и конечные (на дисплее) размеры:
CDC* m_pdcMemory;
CBltmap* m_pBitmap;
CSize m_sizeSource, m_sizeDest;
Отредактируйте приведенные ниже функции-члены класса CExl /вИиу-Модифи- цируйтс файл exlIaVlew.cpp. Конструктор и деструктор выполняют инициализацию и очистку внедренных объектов. Конструктор лучше сделать предельно простым, так как любая ошибка в нем грозит крупными неприятностями. Функция Оп1»^На1ирДа#еинициа- лизирует контекст устройства в памяти и растровое изображение, а также вычисляет размеры выводимого изображения, при которых каждый бит отображается как один пиксел. Функция OnDraw вызывает StretchBlt дважды — один раз с использованием специальна вычисленных размеров и один раз с отображением каждого бита на прямоугольник размером 0.01x0.01 дюйма. Введите код, выделенный полужирным:
CExIOaView::CEx11aView()
{
m_pdcMeiK> ry = new CDQ
m„pBitmap = new CBltmap;
CEx10aVlew::*CEx10aView()
{
// очищаем контекст в памяти и растровое изображение
delete ■„pdcMemory; // отключаем растровое изображение
delete m_pBitmap;
void CExIOaView: :0nDraw(C0C» pDC)
pDC->SetStretchBltMode(COLORONCOLOR);
pDC->StretchBlt(20, -20, m_slzeOest.cx, -m_sizeDest.cy, m.pdcMeeory, 0, 0, m^sizeSource.cx, nisizeSource.cy, SRCCOEY);
pDC->StretchBlt(350, -20, m_aizeSource.cx, -m_sizeSource.cy, i»_pdcMemory, 0, 0, m_sizeSou rce. ex, e_alzeSou roe. cy, SRCCOFY);
void CExIOaView::OnIniTialUpdaTe()
•!
CScrollView::OnInitialUpdate();
CSize totalSize(8OO, 1050); // 8 x 10,5 дюймов
CSize lineSize = CSize(totalSize.cx / 100, totalsize.cy / 100); SetScrollSlzes(MMj-OENGLISH, totalsize, totalsize, linesize);
BITMAP bm; // структура данных Windows (см. документацию Win32) if (m_pdcNemory->GetSafeHdc() ==NULL) {
CClientDC dc(this);
OnPrepareDC(idc); // эго необходимо m_pBitmap->LoadBitmap(IOB.GOLDWEAVE); m_pdcHemory->CreateCoinpatibleDC(&dc); n>_pdcMenK>ry->SelectObject(m_pBitmap); m_pBitmap->GetObject(slzeof(bm), &bm); m_sizeSource.cx = bm.bmWidth; m_sizeSource,cy = bm.bmHeight; m_slzeDest = m.aizeSource;
de, DPtoLP(6i»_sizeDest);
Соберите и протестируйте приложение ЕХ11А. На рисунке на стр. 175 показано, как должно выглядеть изображение на вашем экране.
Проверьте работу функций вывода на печать и предварительного просмотра. Растровое изображение при печати масштабируется, так как каркас приложений устанавливает для контекста устройства «принтер» режим преобразования координатMMJLOENG- LISH" так же, как и для контекста устройства «дисплей». В режиме предварительного просмотра результат просто великолепен, но на принтере получается или пустое место, или микроскопическое изображение. Но ничего, скоро мы это исправим.
Растровые изображения и улучшенный вывод на экран
Вы видели пример программы, которая выводит на экран растровое изображение, созданное вне этой программы. Теперь Вы увидите программу, которая сама формирует растр, чтобы обеспечить плавное перемещение изображения на экране. Принцип ее работы прост Вы рисуете в контексте устройства в памяти, с которым связана растровое изображение, и отправляете изображение на экран.
Пример ЕХ11В
В примере ЕХ05С (глава 5) пользователь перемещал окружность мышью. Изображение при этом сильно мельтешило, потому что программа стирала и перерисовывала окружность после каждого сообщения о перемещении мыши. Чтобы избежать этого, в EXI 1В применяется GDI-растр. Код обработки сообщений от мыши позаимствован из ЕХ05С почти без изменений; большая часть нового кода содержится в функциях OnPainta OnlnitialUpdate.
В этой программе функция Оп1пШа1ираа1есоз^£^ контекст устройства в памяти и растровое изображение, совместимое с дисплеем. OnPaint подготавливает контекст устройства в памяти к рисованию, передает функции OnDrawero описатель и копирует полученное изображение из контекста в памяти на экран. Далее приведены этапы создания программы EXI 1В
Используя AppWizard, создайте проект \VCPP32\EX11B\EX11B. Примите параметры предлагаемые по умолчанию, но выберите Single Document Interface и базовый класс CScroUVtewyyH. класса CExllbView. Параметры и имена классов по умолчанию:
Используя Classwizard, добавьте к СЕхИЫЧеиобработчики сообщений. Добавьте обработчики следующих сообщений:
т WM_LBUTTQNDOWN;
т WM_LBUTTONUP;'
т WM_M OUSE MOVE;
т WM_PA1NT.
Отредактируйте заголовочный файл exllbVlew.h. Введите в класс СЕхПЬУ; ^закрытые переменные-члены:
private:
const CSize m_sizeEllipse;
CPoint m_pointTapLeft;
Iboolm_bCaptured;
CSizem_sizeOffset;
CDC*m_pdcMemory;
CBitmap*m_pBitmap;
Введите код конструктора и деструктора класса CExllbView в файл exl lbView.cpp. Нам нужен объект «контекстустройства в памяти» и объект «GDI-растр». Они создаются в конструкторе и уничтожаются в деструкторе класса «вид*. Добавьте кед, выделенный полужирным:
CExIObView: :CEx10bView() : m_sizeElllpse(100, -100),.
m_pointTopleft(10, -10), sizeOffset(O.O)
* m_bCaptured = FALSE;
m_pdcMemory = new CDC;
m_p0itmap= new CBitmap;
}
CExIObVicw:;'CEx10bView()■
{
delete m_pBitmap; Ц уже отключен от контекста
delete m_pdcMemory;
Введите в exllbView.cpp код функции OnlnitialUpdate. С-н-объекты — контекстустройства в памяти и растровое изображение — уже созданы. Функция OnlniliaiUpdateco- здает соответствующие им объекты Windows. И контекст устройства, и растровое изображение совместимы с контекстом дисплея de, но Вы должны явно задать режим преобразования координат для контекста в памяти, чтобы он соответствовал режиму контекста дисплея. Растровое изображение можно было бы формировать в функции OnPaint. но программа будет работать быстрее, если создать его лишь один раз, в функции OnlnitialUpdate. Добавьте кед, выделенный полужирным:'
void CExIObView:;OnInitialUpdate()
CScrollVlew. :OnInitialUpdate();
CSize sizeTotal(800, 1050); // 8 x 10,5 дюймов
CSize sizePage(sizBTotal.cx / 2, sizeTotal. cy / 2);
CSize sizeLinefsizeTotal.cx / 50, sizeTotal.cy / 50);
SetScrollSizes(MM_LOENGLISH, sizeTotal. sizePage, aizeLine);
// создаем контекст устройства в памяти и растровое изображение
if (m-pdcHamory->GetSafeHdc() == NULL) {
CClientDC dc(thl5>;
0nPrepareDC(4dc);
iCRect rectMax(0, 0, sizeTotal.ex, -sizelbtal.cy);
de .LPtoDP(rectMax);
m_pdcMemory->CreateCompatibleDC(&dc):
И размер растрового изображений равен размеру окна на дисплее m_pBitrap->CreateCompatiblaBitmap(&dc, rectMax.right, rectMax.bottom); m_pdcMemory->SetHapMode(MM_LOENGLISH);
>
Введите в exllbView.cpp код функции OnPaint, Обычно не надо переопределять OnPaint в новом классе. Версия OnPaint в классе CVietv содержит код:
CPaintOC dc(this);
ОпР сера r eDC(&dc);
0n0raw(&dc):
В этом примере OnPaint переопределяется, чтобы уменьшить мерцание на экране за счет использования контекста дисплея в памяти. При выводе на экран функции OnDraw передается контекст дисплея в памяти, а при печати — контекст принтера. Таким образом, OnDraiv выполняет одинаковые действия для дисплея и принтера. При выводе на принтер использовать растровое изображение не нужно, так как принтер — устройство медленное. Переопределенная функция OnPaint, чтобы подготовить контекст устройства в памяти к рисованию, должна выполнить последовательно три операций:■
я подключить растровое изображение к контексту устройства в памяти;
т переместить недействительный прямоугольник, вычисленный в OnMouseMove, из КО1 - текста дисплея в контекст устройства в памяти. Функции SetClipRecty нас нет, но функция CDC::IntersectClipRectoeT тот же эффект, когда ее вызывают после COC::SelectClip Rgn (с параметром NULL), Если Вы не установите минимальный размер отсекающего прямоугольника, программа будет работать медленнее;
т инициализировать растровое изображение цветом фона текущего окна. Функция С1>С'..РаШ//закра1пиваст заданный прямоугольник по шаблону, вданном случае по шаблону кисти для фона текущего окна. Разумеется, кисть сначала надо создать и подключить к контексту устройства в памяти.
После того как контекст устройства в памяти подготовлен, OnPaint вызывает OnDraw, передавая ей контекст в памяти как параметр, Затем функция ФС;.ВгТб^копирует обновленный прямоугольник из контекста устройства в памяти в контекст устройства «дисплей». А теперь введите код, выделенный полужирным:
void CEx11bView.:OnPaint()
(
CPaintDC dc(this); // контекст устройства для рисования
OnPrepareDC(8dc);
CRect rectUpdate;
de.GetClipBox(SrectUpdate);
CBitmap* pOldBitmap = m_pdcMemory->SelectObject(m_pBitmap);
m_pdcMemory->SelectClipBgn(NULL);
m_pdcMemory->IntersectClipRect(&rectUpdate);
CBrush backgroundsrush((COLORREF) ::6etSysColor(C0L0R_WIND0W));
CBrush* pOldBrush = m_pdcMemory->SelectObject«ibackg rounds rush);
m_pdcMemory->PatBlt(rectUpdate.left, rectUpdate.top,
- rectUpdate.Width(), rectUpdate.Height(), PATCOPY);
OnDraw(m_pdcMemory);
de.BitBlt(rectUpdate.left, rectUpdate.top, rectUpdate.WidthO, rectUpdate.Height(), m_pdcMemory, rectUpdate.left, rectUpdate.top, SRCCOPY);
nij)dcMemary->SelectObject(pOldBitmap);
m_pdcMemory->8elect0bject(p01dBrush);
Напишите функцию OnDrew» exlIbView.cpp. Скопируйте код из exOScView.cpp. В ЕХ1 1Б функция OnDraw получает от OnPaint указатель на контекст устройства в памяти. При печати ей передается указатель на контекст принтера.
Скопируйте код обработки сообщений от мыши из exOScView.cpp. Скопируйте все перечисленные функции из exOScView.cpp в exl IbView.cpp. Не забудьте заменить у следующих функций имя класса с CEx05cVfeu®a CExllbVieiv-.
т OnLButtonDoivn;
И OnLBuHonUp-.
т OnMouseMove.
Модифицируйте две строки в функции ОиМоивеМоне» файле exlIbView.cpp. Следующие две строки:
IwaiiaateRect(rect01d,TRUE);
IrvalidateRecT(rectNew. TRUE);
замените на:
InvalidateRect(rectOld, FALSE);
InvalidateReot(rectNew, FALSE);
Если второй параметр CWnd;:Inv«lidateRectравен TRUE (по умолчанию), Windows перед перерисовкой недействительного прямоугольника стирает его фон. Для ЕХ05С — то, что надо, по именно это и вызывает мерцание изображения на экране. Поскольку весь недействительный прямоугольник копируется из растрового изображения, стирать фон больше не нужно, и мы присвоим второму параметру значение FALSE.
Соберите и запустите приложение. Результат работы программы ЕХ11В:
Стало ли теперь перемещение окружности более плавным? Проблема в том, что размер растрового изображения всего 8x10,5 дюйма и если окно слишком велико, окружность выходит за края. Одно из решений — сделать размер растрового изображения равным размеру самого большого экрана.
Анимация в Windows
ЕХ11В — не более чем грубая попытка реализовать анимацию в Windows. А что если вместо окружности надо перемещать изображение рыбки — морского ангела (angelfish)? В Win32 нет (пока) функции Angeljisb, так что придется хранить рыбку в отдельном растровом изображении и при вызове .SO'etoM/Z использовать маски кодов растровых операций, чтобы скомби-
пировать изображение с фоном. Вероятно, фон тоже пришлось бы хранить в отдельном растровом изображении. В общем, все это слишком сложно для данной книги. Поэтому лучше купите книгу Найджела Томпсона (Nigel Thompson) Animation Techniques in Win32 (Microsoft Press, 1995). Прочитав эту книгу, Вы заработаете кучу денег, создавая видеоигры под Windows!
DIB-изображения и класс CDib
В MFC есть класс CBihnapSAft GDI-растров, но класса для DIB нет. Не волнуйтесь: здесь Вы его получите. Это полностью переработанный класс CDib из предыдущих изданий этой книги. В нем используются все преимущества таких средств Win32, как проецирование файлов в память, улучшенное управление памятью и DIB-секции. Включена также поддержка палитр Однако прежде, чем изучать класс CDib, познакомимся с DIB поближе.
Несколько слов о программировании палитры
Программировать палитру в Windows очень сложно, но дело с ней придется иметь, пока есть пользователи, которые работают с дисплеями в режиме 8 бит/пиксел, что приходится делать, когда объем памяти на видеоплате не превышает 1 Мб.
Допустим, мы воспроизводим в окне одно DIB-изображение Первое, что надо сделать, — создать логическую палитру (logical palette) — GDI-объект, содержащий цвета из DIB. Затем нужно реализовать ее, т. е. воплотить» (realize) в аппаратную системную палитру (system palette), которая представляет собой таблицу из 256 цветов, одновременно отображаемых видеоплатой. Если программа выполняется на переднем плане (foreground), Windows пытается скопировать все заданные цвета в системную палитру, но не трогает 20 стандартных цветов, всегда присутствующих там. В конце концов, DIB обычно выгляд ит именно так, как былозадумано.
Ну а что если на переднем плане выполняется другая программа, которая отображает DIB с лесным пейзажем в 236 оттенков зеленого? Наша программа по-прежнему реализует свою палитру, но на этот раз несколько иначе. Теперь системная палитра не меняется, а просто палитры — системная и наша логическая — сопоставляются по-новому. Например, если наша программа показывает неоново-розовый цвет, Windows сопоставит его со стандартным красным цветом. И если Вы забудете в программе реализовать свою собственную палитру, то при активизации другой программы вое неоново-розовое позеленеет.
Пример с лесным пейзажем, конечно, крайность, так как в нем предполагается, что другая программа отбирает у нас 236 цветов. Если же сна реализует логическую палитру всего лишь из 200 цветов, Windows позволит нашей программе загрузить 36 своих цветов, в том числе, будем надеяться, и неоново-розовый.
Итак, когда же программе следует реализовать свою собственную палитру? Сообщение Windows WM_PALE1TECHANGED посылается основному окну вашей программы всякий раз, когда какая-то программа (и ваша в том числе) реализует свою палитру. Другое сообщение, WM_QUERYNEWPALETTE, посылается, когда одно из окон вашей программы получает фокус ввода. Программа должна реализовать свою палитру в ответ на оба этих сообщения (если только не она их и породила). Однако эти сообщения не передаются окну объекта «вид». Вы должны создать их обработчики в основном окне приложения и предусмотреть уведомление ■объекта «вид». Взаимосвязи между окном-рамкой и объектам «вид» обсуждаются в главе 13, а в главе 26 приведено полнофункциональное MDI-приложение, работающее с палитрами (ЕХ26А).
Для реализации палитры вызывают функцию Win32 RealizePalette, но сначала надо вызвать SelectPalette, чтобы подключить логическую палитру DIB-изображения к контексту устройства. У SelectPalette есть параметр — флажок, который в обработчиках сообщений WM_PA- LETTECHANGED и WM_QUERYNEWPALETTE обычно равен FALSE, Это гарантирует реализацию
палитры как палитры переднего плана, если ваше приложение действительно выполняется на переднем плане. Указав флажок TRUE, можно заставить Windows реализовать палитру так, будто приложение исполняется в фановом режиме.
Вы также должны вызывать Ее«НхеР«1еаеддя каждого DIB-изображсния, воспроизводимого при работе функции OnDraw. Сначала, конечно, нужно вызвать SelectPalette, на этот раз с флажком TRUE. Если программа показывает несколько DIB-изображений, каждое со своей палитрой, задача существенно усложняется. Но, в принципе, нужно выбрать палитру для одного из DIB-изображений и реализовать ее (подключив с параметром FALSER обработчиках сообщений для палитры. Эго изображение, скорее всего, будет выглядеть лучше остальных. Есть также несколько способов смешения палитр, но куда проще пойти и купить дополнительную видеопамять.
DIB, пикселы и цветовые таблицы
DIB состоит из двумерного массива элементов, называемых пикселами (pixels). Во многих случаях каждый пиксел DIB соответствует пикселу на экране, но его можно также спроецировать и на какую-то логическую область экрана — в зависимости от режима преобразования координат и параметров растяжения у функции вывода изображения. Вы уже видели, какие эффекты возникают из-за несовпадения пикселов растрового изображения и экрана.
Пиксел состоит из 1, 4, 8, 16, 24 или 32 последовательных битов (зависит от цветового разрешения данного изображения). В DIB с разрешениями 16, 24 и 32 бит/пиксел каждый пиксел представляет какой-то RGB-цвет. В DIB с разрешением 16 бит/пиксел на значения красного, зеленого и голубого цветов обычно отводится по 5 бигов, а в более распространенных DIB с разрешением 24 бит/пиксел — по 8 битов. DIB с разрешением 16 и 24 бит/пиксел оптимизированы под видеоплаты, способные воспроизводить одновременно 65 536 или 16,7 миллиона цветов соответственно.
DIB-изображения с разрешением 1 бит/пиксел — монохромные, но необязательно черно-белые. Допускаются любые 2 цвета из встроенной в DIB цветовой таблицы. В цветовой таблице монохромного растрового изображения есть два 32-разрядных элемента, каждый из которых содержит по 8 бигов на красный, зеленый и голубой, и еще 8 бигов для флажков. Для пикселов со значением 0 используется первый элемент, а для пикселов со значением 1 — второй. Если ваша видеоплата воспроизводит 65 536 или 16,7 миллиона цветов, Windows сможет напрямую отобразить эти два цвета. (В видеорежиме с 65 536 цветами Windows урезает 8-битные значения до 5-битных.) Если видеоплата работает в режиме 256 цветов с использованием палитры, программа смажет скорректировать системную палитру, загрузив в нее нужные два цвета.
Распространены также DIB с цветовым разрешением 8 бит/пиксел. Как и в монохромных DIB, у них тоже есть цветовая таблица, но с 256 (или менее) 32-разрядными элементами. Каждый пиксел в этой таблице — индекс. При наличии видеоплаты с поддержкой палитр программа может создать из этих 256 элементов логическую палитру. Если системную палитру контролирует другая программа, выполняемая на перед нем плане, Windows старается наилучшим образом сопоставить цвета вашей логической палитры с цветами системной.
А что, если Вы попытаетесь воспроизвести DIB с цветовым разрешением 24 бит/пиксел на 256-цвегной видеоплате с поддержкой палитр? В идеале, автор DIB должен был бы включить в нее цветовую таблицу с важнейшими цветами. Тогда ваша программа смогла бы построить по этой таблице логическую палитру, и изображение выглядело бы нормально. Если цветовой таблицы в DIB бет, воспользуйтесь палитрой, возвращаемой функцией Win32 Creale- Hal/tonePalette. Это лучше, чем 20 стандартных цветов, которые доступны при отсутствии какой бы то ни было палитры. Еще один вариант — проанализировать DIB, чтобы определить важнейшие цвета, но для этого можно приобрести готовую утилиту.
Структура DIB в ВМР-файле
Вы уже знаете, что DIB — это стандартный формат Windows для растровых изображений и что они хранятся в BMP-файлах. Заглянем же внутрь BMP-файла и посмотрим, что там находится (см. рис. 11-1).
Рис. 11-1. Формат ВМР-фаЙла
Структура В1ТМАРЫ1£НЕАВЕКоде>ржш: смещение бигов изображения, на основе которого можно вычислить общий размер структуры BITMAPINFOHEADERs. следующей за ней цветовой таблицы. В структуре BITMAPFILEHEADER есть поле, хранящее размер файла, но полагаться на него не стоит, так как Вам не известны единицы измерения — байты, слова или двойные слова1.
Структура BITMAPINFOHEADER определяет размеры растрового изображения, число битов на пиксел, информацию об упаковке изображений с 4 и 8 бит/пиксел, а также количество элементов в цветовой таблице. Если DIB упакован, этот заголовок содержит размер массива пикселов — в противном случае этот размер можно вычислить по размеру растрового изображения и цветовому разрешению. Сразу за заголовком располагается цветовая таблица (если она вообще есть в данном DIB). Далее размещается собственно DIB-изображение, состоящее из пикселов, упорядоченных по столбцам в пределах строк, начиная с нижней. Каждая строка выравнивается по границе, кратной 4 байтам.
Единственное место, где Вы встретите структуру BITMAPFILEHEADER, — BMP-файл. Если DIB получен, например, из буфера обмена, то заголовка файла в этой структуре нет. Цветовая таблица располагается сразу за структурой BITMAPINFOHEADER, но изображение не всегда располагается сразу же за цветовой таблицей. Поэтому, если Вы, допустим, используете функцию CreateDlBSection, выделяйте место для заголовка растрового изображения и цветовой таблицы, а уж где размещать изображение, пусть решит Windows.
Неверно. В соответствии с документацией Win32 размер файла указывается в байтах. — Прим, персе.
ПРИМЕЧАНИЕ Эта глава и приведенный в ней код относятся к DIB для Windows. В литературе хорошо задокументирована версия формата DIB для OS/2. Так что если Вам понадобится обработка DIB в OS/2, класс CDib придется модифицировать.
Функции для работы с DIB
В Windows предусмотрено несколько важных функций для работы с DIB. Ни для одной из них нет аналога в MFC, поэтому за более подробней информацией Вам придется обратиться к документации по Win32. Мы приведем лишь краткое описание данных функций.
SetDIBitsTbDevice.Непосредственно отображает DIB на экране или принтере. Масштабирование не осуществляется; один пиксел растрового изображения соответствует одному пикселу на экране или на принтере. Отсутствие масштабирования ограничивает эффективность этой функции. Механизм ее работы отличен от BitBlt (последняя пользуется логическими координатами).
StretcbDIBits. Непосредственно отображает DIB на экране или принтере; работает аналогично StretcbBlt.
т GetDIBits, Создает DIB из GDI-растра, используя выделенную программистом память. Вы можете в какой-то степени управлять форматом DIB, поскольку эта функция допускает задание числа «цветовых» битов на пиксел и упаковку. При использовании упаковки GetDIBits следует вызывать дважды: первый раз для вычисления нужного объема памяти и второй раз для генерации данных DIB.
CreateDIBitmap. Создает GDI-растр из DIB. Как и все подобные DIB-функции, требует передачи контекста устройства в качестве параметра. Можно указывать контекст дисплея; использовать контекст устройства в памяти необязательно.
CreeteDIBSection* Создает особую разновидность DIB, известную под названиемД/В-сек- ция (DIB section), и возвращает описатель GDI-растра. Эта функция сочетает в себе лучшие средства работы с DIB и растровыми изображениями GDI. Вы получаете прямой доступ к памяти DIB, и, располагая описателем растрового изображения и контекстом устройства в памяти, мажете вызывать GDI-функции для рисования в DIB.
Класс CD/b
Не пугайтесь сложностей работы с DIB — класс CDib упростит ее. Чтобы познакомиться с ним, лучше всего просмотреть его открытые функции- и переменные-члены. На рисунке 11-2 показан заголовочный файл класса CDib. Код реализации Вы найдете на компакт-диске, прилагаемом к этой книге.
CDIB.H
#ifndef INSIDE VISUAL СРР CDIB
«define _INSIDE_VISUAL_CPP_CDIB
class CDib : public CObject
(
enum Alloc (noAlloc, crtAlloc, heapAlloc}; // ОТНОСИТСЯ к BITMAPINFOHEADER DECLARE_SERIAL(CDib)
public:.
LPVOID mJpvColorTable:
HBITMAP Tfi_h9itmap;
LPBYTE m_lplmage;// начальный адрес битов DIB
LPBITMAPINFOHEADER m_lp0MIH; // буфф, содержащий BIIMAPINFOHEADER
Рис. 11-2. Объявление класса CDibсм. след. стр.
Рис. 11-2. (продолжение)
private:
HGLOBAL m_hGlobal;// описатель может быть выделен этим классом или вне его
// в последнем случае его нужно освободить
Allocm_nBmihAUoc;
Allocr_nImageAlloc;
DWORD r> dwSizelmage; // размер массива битов, а не структуры 8ITMAPINF0HEADER '■ // или BITMAPFILEHEADER
int m_nCol6rTableEntries'.
HANDLE m hFile;
HANDLE K^hMap;
LPVOID rc_lpvFile:1
HPALETTE m_hPalette;
public:
CDib();
CDib(CSize size, int nBitCount); // создает BITMAPINFOHEADER
~CDib();
int GetSizelmageO {return m_dwSizeImage;)
int GetSizeHeader()
{return sizeof(BITMAPINFOHEADER) + sizoof(RGBQUAO) * m_nColorTableEntries;}
CSize GetDimensionsO;
BOOL AttachMnpFile(const char* strPathname, BOOL bShare = FALSE);
BOOL CopyToMapFile(const char* strPathname);
BOOL AttacnMeirory( LPVOID IpvMem, BOOL bMustDelete = FASE, HGLOBAL hGlobal = NULL);
BOOL Draw(CDO pDC, CPoint origin, CSize size); //пока CreateDibSection не реализована
HBITMAP CreateSectionfCOC* pDC - NULL);'
UINT UsePalette(CDC* pDC, BOOL bBacksround = FALSE);
BOOL MakePalettcO;
BOOL SetSystemPalette(CDC* pDC);
BOOL Compress(GDC* pDC, BOOL bCompress = TRUE); // если FALSE - то распаковать
HBITMAP CreateBitmap(CDC* pDC);
BOOL Rcad(CFile* pFile);
BOOL ReedSection(CFile- pFile, CDC* pDC = NULL);
BOOL Write(CFile« pFile);
void Serialize(CArchive& ar);
void tmptyO;
private:
void DetachMapFileO;-
void ComputePaletteSize(int nBitCount);
void ComputeMetricsO;
};
ttendif // _INSIDE_VISUAL_CPP_CDIB
Далее кратко описаны функции-члены CDib, начиная с конструкторов и деструктора:
Конструктор по умолчанию. Используйте его, если предполагается загружать DIB из файла или подсоединять экземпляр класса к уже существующему в памяти DIB. Конструктор по умолчанию создает пустой объект.
Конструктор DIB-секцин. Используйте его, если нужна DIB-секция, создаваемая при использовании CreateDIBSection. Его параметры определяют размер и количество цветов в DIB. Конструктор выделяет память для хранения заголовка, но не для хранения изображения.
Параметр
Описание
size
Объект CSize, задающий ширину и высоту DIB
nBitCount
Число бит/пиксел, должно быть 1, 4, 8, 16, 24 или 32
Деструктор. Деструктор класса С1ХЬ освобождает всю выделенную для DIB память.
AttacbMapFUe. Открывает проецируемый в память файл в режиме чтения и подсоединяет его к объекту CDib. Функция немедленно возвращает управление, так как не считывает файл в память до тех пор, пока он не потребуется. Но при доступе к DIB могут возникать задержки, связанные с подкачкой файла в память. Функция освобождает выделенную ранее память и закрывает присоединенный ранее проецируемый файл (если такой был).
Параметр
Описание
strpatbname bSbsre
Возвращаемое значение
Полное имя проецируемого файла
Флажок, значение которого равно TRUE, если файл надо открыть в режиме совместного использования. По умолчанию — FALSE TRUE — при благополучном завершении
AttacbMemory. Связывает существующий объект CDib с DIB в памяти. Эта память может быть ресурсом программы или находиться в буфере обмена или в объекте данных OLE. Ее можно выделить оператором new или функцией GlobcilAUoc.
Параметр
Описание
IpnMem bMustDelete
bGlobal
Возвращаемое значение
Адрес присоединяемой памяти
Флажок, значение которого равно ТЕОЕ^сли за освобождение памяти отвечает объект CDib. По умолчанию — FALSE
Если память получена путем вызова функции Win32 Globa 1А Пос, объект CDibдолжен запомнить описатель области памяти, чтобы впоследствии освободить ее (при bMustDelete, равном TRUE)
TRUE — при благополучном завершении
Congress. Повторно генерирует DIB в упакованном или распакованном виде. Преобразует существующее DIB-изображение в GDI-растр, после чего создает новое упакованное или распакованное DIB-изображение. Упаковка поддерживается только для DIB с цветовым разрешением 4 или 8 бит/пиксел. DIB-секцйю упаковать нельзя.
Параметр
Описание
pDC
bCompress
Возвращаемое значение
Указатель на контекст дисплея
TRUE (по умолчанию) для упаковки DIB, FALSE для распаковки
TRUE — при благополучном завершении
СоруТоМарЕйеСоздает новый файл, проецируемый в память, и копирует в него данные из существующего DIB. При этом освобождается вся выделенная ранее память и закрывается прежний проецируемый файл (если таковой был). Данные не записываются на диск до тех пор, пока новый файл не будет закрыт, что происходит при повторном использовании или уничтожении объекта CDib.
Параметр
Описание
strPatbname
Полное имя проецируемого файла
Возвращаемое значение
TRUE — при благополучном завершении
CreateBitmap. Создает из существующего DIB-изображения GDI-растр и вызывается посредством функции Compress, Не путайте эту функцию с CreateSection, которая создает DIB и запоминает описатель,।
Параметр
0 писалие
рВС
Возвращаемое значение
Указатель на контекст дисплея или принтера
Описатель растрового изображения GDI (NULL при неудаче). Этот описатель не записывается в открытую переменную-член
CreateSection. Создает DIB-секцию, вызывая функцию Win32 CreateDlBSection. Память выделенная под изображение, не инициализируется.
Параметр
Описание
рос
Возвращаемое значение
Указатель на контекст дисплея или принтера
Описатель растрового изображения GDI (NULL при неудаче). Этот описатель также записывается в открытую ПсременНую-члеН
Draw. Выводит объект CDib на дисплей (или принтер), вызывая функцию Win325№^7?- DIBits. При необходимости растровое изображение растягивается для подгонки под задан - цый прямоугольник.
Параметр
Описание
pDCУказательна контекст дисплея или принтера — получатель’ОГВ-изоб-
ражения
originОбъект CPoint. содержащий логические координаты, в которые выво
дится DIB
sizeОбъектCSi^e, в логических единицах задающий размеры прямоуголь
ника на устройстве вывода
Возвращаемое значение TRUE — при благополучном завершении
'• Empty. Очищает DIB, освобождая при необходимости выделенную память и закрывая спроецированный в память файл.■
| Gettiimensions, Возвращает ширину ивысоту DIB в пикселах.
(ПараметрОписание
Возвращаемое значениеОбъект С Sir#
GelSizeHeader. Возвращает общее количество байтов в информационном заголовке и цветовой таблице.
ПараметрОписание
Возвращаемое значение32-разрядное целое
GetSizelmage. Возвращает размер изображения DIB в байтах (исключая информационный заголовок и цветовую таблицу).
Параметр Описание
Возвращаемое значение32-разряднос целое
MakePaleite. Считывает цветовую таблицу и, если эта таблица есть, создает палитру Windows. Описатель Я/ИЛ£7 ТС запоминается в переменной-члене.
ПараметрОписание,
Возвращаемое значение TRUE — при благополучном завершении
ПараметрОписание
pFileУказательна объект СЕйе. Соответствующий файл надиске содержит DIB
Возвращаемое значение TRUE— при благополучном завершении
ReadSection. Считывает заголовок из BMP-файла, вызывает CreateDlBSectionxnH ьыдетае- ния памяти, а затем считывает в эту память изображение из файла. Эта функция полезна, если нужно считать DJB-изображение с диска и отредактировать его вызовами GDI. Отредактированное изображение можно вновь записать на диск при помощи функций Write или CopyTbMapFile.
Параметр
Описание
pFile
Указатель па объект С File. Соответствующий файл на диске содержит DJB
pDC
Указатель на контекст устройства дисплея или принтера
Возвращаемое значение
TRUE — при благополучном завершении
Serialize. Сериализации посвящена глава 17. Функция CDib:;Seriahze, переопределяющая MFC-функцию CObject::Serialize, вызывает функции-члены Read и Write. Описание параметров см. в Microsoft Foundation Class Library Reference.
SetSystemPaletteJdTy функцию можно вызвать для DIB-изображения с цветовым разрешением 16, 24 или 32 бит/пиксел, у которого нет цветовой таблицы, чтобы создать для объекта CDib логическую палитру, соответствующую палитре, возвращаемой функцией Сгеа1еНа1/ЮпеРа1е11еРлли ваша программа работает с 256-цветным дисплеем, поддерживающим палитры, и Вы не вызываете SetSyslemPalelle.TO при выводе DIB используются только 20 стандартных цветов Windows (поскольку палитра не задана).
Параметр
Описание
pDC
Указатель на контекст дисплея
Возвращаемое значение
TRUE ~ при благополучном завершении
UsePalette. Подключает логическую палитру объекта CDib к контексту устройства и реа-
лизует ее. Функция-член Draw вызывает UsePalette перед рисованием DIB.
Параметр
Описание
рВС
Указатель на контекст устройства дисплея (для реализации палитры)
bBackground
Если этот флажок ранен FALSE (по умолчанию) и приложение выполняется на переднем плане, Windows реализует эту палитру как палитру переднего плана (копирует в системную палитру максимально возможное число цветов). Если же флажок равен TRUE.Windows реализу- етпалитру как фановую (стремясь наилучшим образом отобразить логическую палитру на системную)
Возвращаемое значение
Число элементов логической палитры, отображенных на системную палитру. В случае ошибки возвращается значение GDI_ERROR
Write. Записывает DIB
из объекта CDib в файл. Файл должен быть открыт или создан.
Параметр
Описание
pFile
Указатель на объект CFile. DIB будет записан в соответствующий дисковый файл
Возвращаемое значение
TRUE — при благополучном завершении
Для удобства четыре открытых переменных-члена обеспечивают доступ к памяти DIB и к описателю DIB-секции. Эти переменные дают ключ к структуре объекта CDib. Он представляет собой набор указателей на динамически распределяемую память. Владельцем этой па-
мяти может быть, в том числе, и DIB. Дополнительные закрытые переменные-члены определяют, должен ли класс CDib освобождать память,
Производительность при выводе DIB-изображения на дисплей
Оптимизированная обработка DIB — в настоящий момент одна из главных особенностей Windows. У современных видеоплат есть специальные буферы, совместимые со стандартным форматом DIB-изображений. Если ваша программа выполняется на компьютере, снабженном такой платой, она может воспользоваться преимуществами нового DlR-процессора (DIB engine) Windows, который ускоряет процесс прямого рисования изображений из DIB. Если же программа выполняется в режиме VGA, то она будет работать правильно, но не так быстро
При работе с Windows в режиме 256 цветов изображения 8 бит/пиксел будут выводиться на дисплей очень быстро как при помощи SlretchBlt^ так и StretcbDIBits. Но при выводе растровых изображений 16 или 24 бит/пиксел эти функции работают слишком медленно. Добиться ускорения вывода можно путем создания отдельного GDI-растра с 8 бит/пиксел с последующим вызовом StretcbBlt. Конечно же, при этом следует реализовать корректную палитру перед созданием изображения и выводом его на экран.
Ниже приведен фрагмент кода, который можно вставить непосредственно после загрузки объекта CDib из BMP-файла:
// m_hBitmap - переменная-член типа HBITMAP
// m_dcHem - объект контекста устройстза в памяти класса СЕС
m_pDib->UscPalette(&dc);
m_hBitmap = m_pDib->CreateBitmap(&dc); // может работать медленно
! :SelectObject(.m„dcMem.GetSafeHdc(), jn^hBitmap);■
А вот код, который можно использовать вместо CDib::DrawB функции OnDraw класса «вид»:
m_pDib->UsePalette(&dc):// можно поместить в обработчик сообщения палитры
CSize sizeDib = m_pDib->GetDimensions();
pOC->StretchBlt(O, 0, sizeDib.ex, sizeDib.су, &r_dcMem,
0, 0, sizeToDraw. ex, sizeTpDraw.cy. SRCCOPY);
He забудьте вызватъОе1еГеОЬ}ес1 для mjbBitmap, когда закончите работу с ним.
Пример ЕХ11С'
Теперь посмотрим, как CDib-класс работает в приложении. Программа ЕХ11С воспроизводит два DIB-изображения — одно из ресурса, а другое из выбранного BMP-файла. Программа управляет системной палитрой и корректно выводит DIB на принтер. Сравните код EXI 1С с кодом для растрового изображения GDI из EXI 1А. Заметьте, что здесь Вам не придется оперировать с контекстом устройства в памяти и помнить правила подключения объектов GDI. Теперь рассмотрим процесс создания программы ЕХ11С.
С помощью AppWizard создайте проект \VCPP32\EX11C\EX11C. Примите параметры, предлагаемые по умолчанию, но выберите Single Document и базовый класс CScroWiew для класса CExllcView. Параметры и имена классов по умолчанию приведены на рисунке настр. 189.
Импортируйте растровое изображение Red Blocks. Выберите из меню Insert в Visual C++ команду Resource. Импортируйте растровое изображение Red Blocks.bmp из каталога WINDOWS. (Если в вашей версии Windows этого изображения нет, загрузите его с компакт-диска, прилолагаемого к книге.) Visual C++ скопирует этот файл растрового изображения в подкаталог RES вашего проекта. Назначьте изображению идентификатор iDBJRED- BLOCKS и сохраните внесенные изменения.
Добавьте в проект класс CDib. Если проект создавался с нуля, скопируйте файлы cdib.h и cdib.cpp из каталога \vcpp\exl 1с прилагаемого к книге компакт-диска. Но простого копирования недостаточно, — нужно добавить эти файлы в проект. Для этого выберите команду AddTo Ргсдес1из меню Project среды разработки, а затем Files. Из списка выберите файлы cdib.h и cdib.cpp и щелкните кнопку ОК. Если Бы теперь обратитесь к вкладке ClassView окна Workspace, то увидите там класс CDib со всеми его переменными- и функциями-членами.
Добавьте следующие закрытые переменные-члены в класс CExl IcView-B окне ClassView щелкните правой кнопкой мыши класс СЕх! 1сУгещ,Из появившегося всплывающего меню выберите Add Member Variable и добавьте переменную m_dibResource, как показано на рисунке.,
Таким же образом добавьте и m_dibFile. В итоге, в конце заголовочного файла должны появиться две переменных-члена:
CDib m_dibsile;
CDib m_dibResourcc;
ClassView также добавляет следующий оператор в начале файла сх 11 cVlew.hr
«include "cdib.h" Ц добавлено ClassView
■ Отредактируйте функцию OnlnitialUpdata файле exllcView.cpp. Эта функция устанавливает режим преобразования координат в MM_HIMETRI<h загружает объект m_dib- Resource из ресурса IDB_REDRLOCKS. Обратите внимание: здесь, в отличие от ЕХ11А, мы не вызываем LoadBitmapujta загрузки GDI-растра. Объект к ресурсу в EXF-файлс подсое- динявт функция CDib.-AttacbMemory. Введите код, выделенный полужирным:
void CExIOcView::OnInitialUpdate().
CScrollView::OnInitialUpdate();'
csize totalSize( 30000, 40000) ; // 30 X 40 CM
CSize linesize = CSize(totalSize,cx / 100, totalSize.cy / 100);
SetScrollSizas(MK_HlKETRIC, totalSize, totalSize, linesize),-
lpvoid IpvResource = (lpvoid) ::LoadResource(NULL,
::FindResourcB(NULL, MAKEINTRESOURCE(IDB_REDBLOCKS), RT_BITMAP));
m^dibResource.AttachMeaory(lpvResource); // ::LockResource не требуется
cciientDG dc(this);
TRACEC'bits per pixel = %d\n", dc.GetDeviceGaps(BITSPIXEL));.
)
Отредактируйте функцию-член OnDraw^ файле exllcView.cpp. В коде этой функции вызывается CDib.rDrawjuui каждого из двух DIB. №е,Рй/еггщалжны вызывать на самом деле обработчики сообщений WM_QUERYNEWPALETTE и WM_PALETTECHANGED. С этими сообщениями работать довольно трудно, так как их не посылают непосредственно объекту *№Д», поэтому мы прибегнем к небольшому упрощению. Добавьте код, выделенный полужирным:
void CExIOcView: :OiiDraw(CDC* pDC) • i
BeginWaitCuraor();
m dibResource.UsePalette(pDC); // это должно быть не здесь,
m_dibFile.VsePalette(pDC);// а в обработчиках сообщений палитры
pDC->TextOut(O, 0, "Щелкните здесь левой кнопкой мыши для загрузки файла ");
csize sizeResourceDib = m_dibRasource.GetDimensions();
sizeResourceDib. ex *= 20;
sizeResourceDib.су *= -20;
m_dibResource.Oraw(pOC, CPoint(0, -500), sizeResourceDib); .
csize sizeFileDib = m_dibFile.GetDiinensions();
sizeFileDib.ex *= 20;
sizeFileDib. су *= -20,-
M_dibFile.Draw(pDC, CPoint(1200, -500), sizeFileDib);
EndWaitCursor();
}
Создайте в классе CExl lcVieuo6yba6oT4mt сообщения WM LBUTTONDOWN. Отредактируйте файл ex I lcView.cpp. OnLButtonDoitn содержит код для считывания DIB двумя способами. Если Вы оставите определение MEMORY_MAPPED_FILES, активизируется код, использующийАЯасйЛ/й/^^едля считывания проецируемого в память файла. Если же Вы закомментируете первую строку, активизируется вызов Read. Вызов SetSystemPalette присутствует здесь специально для DIB, у которых нет цветовой таблицы. Введите код, выделенный полужирным:
«define MEMORY_MAPPED„FILES
void CExIOcView::0nL8uttQnDown(UINT nFlags, CPoint point)
CFileDialog dIg(TRUE, "bmp", ”*.bmp‘);
if {dig.DoModal() ■= 1D0K) {
return;
}
ttifdefMEMORY_NAPPED_FILES
" if(m_dibFila.AttachMapFile(dlg.GetPathNane(),
TRUE) =■ TRUE) { // сошестное использование invalidatef);
}
ttelse
CFile file;
file, Open (dig. Get PathNameO, CFile:: mode Read);
if (i_dibFile.Read(&file) « TRUE) {
Invalidate!);
#endif// MEMORY_MAPPED_FILES
CCIientDC dc(this);
m_dibFile. SetSystemPalette(Mc);
A'
Соберите и запустите приложение. В каталоге проекта ЕХ11С на компакт-диске есть несколько интересных растровых изображений. Файл chicago.bmp — это DIB с 8 бит/пик- сел и цветовой таблицей с 256 элементами; forestbmp и clouds.bmp тоже имеют цветовое разрешение 8 бит/пиксел, но их цветовые таблицы меньше; balloons.bmp — это DIB с 24 бит/пиксел без цветовой таблицы. Попробуйте и другие BMP-файлы, если они у Вас есть. Обратите внимание: Red Blocks — эго 16-цветное DIB-изображенис, в котором используются стандартные цвета, всегда присутствующие в системной палитре.
Еще несколько слов о DIB
Каждая новая версия Windows предлагает новые возможности программирования DIB. Как Windows 95, так и Windows NT 4.0 поддерживают функции Loadimage к DrawDibDraw, которые представляют собой альтернативу уже описанным нами DIB-функциям. Поэкспериментируйте с этими функциями и посмотрите, как они работают в ваших приложениях.
Функция Loadimage
Функция Loadimage может считывать растровое изображение непосредственно из дискового файла, возвращая описатель DIB-секции. Она может обрабатывать даже растровые изображения в формате OS/2. Предположим, нам нужно добавить функцию-член к классу CDib, которая функционировала бы как ReadSection. Вот какой код можно добавить в cdib.cpp:
BOOL CDib::ImageLoad(const char* IpszPathName, CDC* pDC).
{
Empty!);
m hBitmap = (HBITMAP) : :LoadImage(NULL. IpszPathName, IMAGE BITMAP, 0,0,
LR LOADFROMFILE | LR CREATEDIBSECTION j LR DEFAULTSIZE);
DISSECTION ds;
VERIFY(: :Get0bject(m_h8itmap, sizeof(cte), &ds) == sizeof(ds));
// Выделить память для BHMAPINFOHEADER и наибольшей возможной таблицы цветов
m_lpBMIH = (LPBITMAPINFOHEADER) new char[sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)];" memcpy(m_lpBMIH, &ds.dsBmlh, sizeof(BITMAPINFOHEADER));
TRACECCDib: :LoadImage, biClrUsed = %d, biClrlmportant = %d\n",
m_lpBMIH->biClrUsed, m_lpBMIH->biClrImportant);
ComputeMetricsO; // устанавливает m_lpvColorTable
m_nBmihAlloc = crtAlloc;
m_lplmage = (LPBYTE) ds.dsBm.bmBits;
m_nImageAlloc - noAlloc;
// Получить таблицу цветов DIB секции и создать из нее палитру
CDC memdc;.
memdc.CreateCompatibleDC(pDC);
: :SelectObject(memdc.GetSafeHdc(). m_hBitmap);
MINT nColors = : :GetDIBGolorTable(memdc.GetSafeHdc(), 0 , 256, (RGBQUAO*) m_lpvColorTable);
if (nColors != 0) {
ComputePaletteSize(m_lpBMIH->bi8itCount);
MakePalettef);
// контекст устройства в памяти уничтожается, растровое изображение отключается
return TRUE;
Обратите внимание: эта функция извлекает и копирует структуру BITMAPINFOHEADEBi устанавливает значения указателей-членов класса CDib. Чтобы извлечь палитру из DIB-секции, нужно проделать определенные действия, но функция Win32 GetDIBColorTable послужит хорошим подспорьем в этом. Интересно, что GetDIBGolarTableHe может сообщить, сколько элементов палитры использует данное DIB-изображение. Если, например, DIB использует только 60 элементов, GetDIBColorTable строит цветовую таблицу из 256 элементов со 196 элементами, установленными в 0.
Функция DrawDIBDraw
Windows включает компонент Video for Windows (VFW), который поддерживается Visual C++. VFW-функция DrawDIBDraw — альтернатива функции SlretcbDlBits. Одно из преимуществ DrawDIBDraw — возможность использовать составные (dithered) цвета, другое преимущество — ускоренное рисование изображений с числом битов на пиксел, не совпадающим с текущим видеорежимом. Основной же недостаток — необходимость подключать код VFW к прикладному процессу во время выполнения.
Ниже приведена функция DrawDib класса CDib, которая вызывает DrawDIBDraw: '
BOOL CDib::DrawDib(CDC* pDC, CPoint origin, CSize size)
if (ffl lpBHIH== NULL) return FALSE;
if (m_hPalette j= NULL) {.
::SelectPalette(pDC->GetSafeHdc(), m hPalette, TRUE);
}"
HDRAWDIB hdd = ::DrawDibOpen();
CRect rect(origin, size); -
pDC->LPtoDP(rect); // Преобразуем координаты прямоугольника
Ц DIB-изображения в режим ММ_ТЕХТ
rect -= pDC->GetViewportOrg();
int nMapModeOld = pDC->SetHapMade(MM_TEXT);
::DrawDibDraw(hdd, pDC->GetSafeHdc(), rect.left, rect.top, rect.Width(), rect.HeightO, m_lpBMIH, m_lplmage, 0, 0, m_lpBMIH->biWidth, m_lpBMIH->biHeight, 0);
pDC->SetNapMode(nMapModeOld);
VERIFY(: -.DrawDibClose(hdd));
return TRUE;
)
Обратите внимание: DrawDIBDraw требует координат MMJIEXDl режима преобразования координат ММ_ТЕХТ.Тжим. образом, логические координаты должны быть преобразованы не в координаты устройства, а в пикселы — с начальной точкой в левом верхнем углу.
Чтобы использовать DrawDIBDraw, в программу необходимо включить оператор ^include <vjwl»,a также подключить библиотеку vfw32. lib в список входных файлов загрузчика. DrawDIBDraw полагает, что выводимое ею изображение располагается в памяти с доступом для чтения и записи, что следует помнить, выделяя память под ВМР-файл.
Растре выеизображениянакомандных кнопках
Библиотека MFC облегчает показ на командных кнопках растровых изображений вместо текста. При программировании с нуля следовало бы установить для кнопки стиль Owner Draw и написать обработчик сообщения в классе диалогового окна, который рисовал бы растровое изображение в окне элемента управления «кнопка*. Апри использовании MFC-класса CBitmap- Button задача значительно упрощается несмотря на то, что надо будет придерживаться строго определенной процедуры. Не очень переживайте насчет того, как все это работает, а радуйтесь, что не придется писать много кода.
ПРИМЕЧАНИЕ Существует еще один способ поместить растровые изображения на кнопках. См. в главе 36 описание функции CButlon::SetBitmap, которая ассоциирует растровое изображение с кнопкой.
Коротко говоря, Вы создаете, как обычно, диалоговый ресурс, закрепляя за кнопками, которые будут отображать растровые изображения, уникальные текстовые заголовки. Затем добавляете к проекту несколько ресурсов растровых изображений, но идентифицируете их по именам, а не числовым идентификаторам. Наконец, дополняете класс диалогового окна несколькими переменными-членами типа CBitmapButton и вызываете для каждого из них функцию-членАмго£ой(/, которая сопоставляет имя растрового изображения и текст кнопки. Если текст на кнопке — «COPY», Вы добавляете два растра: «COPYU* (кнопка «отжата») и ♦COPYD» (кнопка «нажата»). Да, кстати, не забудьте установить стиль кнопок Owner Draw. Все ■станет понятнее, когда Вы сами напишете программу.
ПРИМЕЧАНИЕ Взглянув на код MFC для класса CBitmapButton, можно заметить, что это растровое изображение — обычный GDI-растр, отображаемый с помощью BitBIt. Поэтому палитра здесь не поддерживается. Но обычно это не проблема, так как растровые изображения для кнопок чаще всего 16цветные; в них используются стандартные цвета VGA.
Пример EX11D■
Процесс создания программы EXI 1D.
Используя AppWizard, создайте проект \VCPP32\EX11D\EX11D. Примите параметры, предлагаемые по умолчанию, но выберите Single Document, Context-Sensitive Help и откажитесь от Printing And Print Preview. Параметры и имена классов по умолчанию показаны на рисунке.*
Параметр Context-sensitive Help выбран только для того, чтобы AppWizard скопировал в подкаталог HLP вашего проекта кое-какие файлы растровых изображений. Обычно эти изображения включаются в файл справочной системы проекта, но об этом мы поговорим в главе 21.
Модифицируйте диалоговый ресурс тОАВОИВОХЧтобы не создавать новое диалоговое окно из-за одних лишь кнопок, воспользуемся диалоговым окном About, которое AppWizard автоматически генерирует для каждого проекта. Добавьте три командные кнопки с указанными (см. первый рисунок на стр. 194) названиями, оставив идентификаторы кнопок по умолчанию: toCBLTTONl JDC_BUTTON2tv IDCJBШT0NЗ.Yaзм&pы. кнопок значения не имеют — в период выполнения программы MFC подгоняет их под размеры растровых изображений.
Установите свойство Owner Draw у всех трех кнопок.
Импортируйте три растровых изображения из подкаталога \Ыр проекта. Выберите из меню Insert в Visual C++ команду Resource и щелкните кнопку Import Начните с EditCopy.bmp, как показано ниже.
Назначьте ей имя «СОРУС»:
ОбязательнОзакяючите имя в кавычки, чтобы идентифицировать ресурс по имени, а не по номеру. Теперь у нас есть растровое изображение для кнопки в «ОТЖДТОМ» состоянии. Закройте окно растрового изображения и скопируйте из окна ResourceView растровое изображение через буфер обмена или операцией drag-and-drop («перетащить и отпустить»). Переименуйте копию в «COPYD* (кнопка в «нажатом» состоянии) и отредактируйте ее. Выберите из меню Image команду Invert Colors. Существуют и другие способы создания изображений отжатых кнопок, но инверсия — самый быстрый. .
Повторите описанные выше операции для растровых изображений EditCut и EditPast. В результате Вы должны получить в своем проекте следующие ресурсы.
Имя ресурса
И сходный файл
. Инвертированные цвета
«СОРУС».
EditCopy.bmp
■ нет .
«COPYD»
EditCopy.bmp
да
«сити»
EditCut.bmp
нет
«сито»
EditCut.bmp
да
Имя ресурса
Исходный файл
Инвертированные цвета
«PASTED»
EditPast. bmp
нет
«PASTED»
EdltPast.bmp
Да
Отредактируйте код класса С4ЬомШ^.И объявление и реализация этого класса содержатся в файле ex 10d.cpp. Для начала добавьте в объявление класса три закрытых перемеи- ных-члена:
CBitmapButton m_edi tCopy;
CBitmapBullon m_editCut; 1
CBitmapButton m_editPaste;'
Затем, используя Class Wizard, создайте в классе диалогового окна обработчик сообщения WM_INITDIALOG. (Убедитесь, что класс CAboutDlgвыбран.) Код обработчика сообщения (на самом деле это виртуальная функция) выглядит так:
BOOLCAboutDlg: .OnlnitpialogO
CDialog::OnInitDialog();
VERIFY(m editCopy.AutoLoad(IDC_BUTTON1, this));
VERIFY(m_editCut.AutoLoad(IDC_BUTT0N2, this));
VERIFY(m_editPaste.AutoLoad(IDC_BUnON3, this));
return TRUE; // возвращать TRUE, если на элемент управления не установлен фокус
// ИСКЛЮЧЕНИЕ; страницы свойств OCX должны возвращать FALSE
Функция AutoLoad устанавливает связь между кнопкой и двумя соответствующими ресурсами. VERIFY — это диагностический MFC-макрос, который выводит информационное окно, если имена растровых изображений заданы неверно1.
Соберите и запустите приложение. После запуска программы выберите из меню Help команду About и понаблюдайте за поведением кнопок. На рисунке кнопка CUT изображена в «нажатом» состоянии.
Обратите внимание: кнопки с растровыми изображениями, каки обычные кнопки, отправляют уведомляющие сообщения BN_CLICKED. Естественно, Class Wizard может создать в вашем классе диалогового окна обработчики этих сообщений.
1И еще пара слов о растровых изображениях на кнопках
Мы использовали растровые изображения для отображения «нажатых и «отжатых» кнопок. Класс CBitmapButton поддерживает, кроме того, растровые изображения для кнопок, имеющих фокус ввода, и для кнопок в отключенном (disabled) состоянии. Для кнопки Сору имя «сфокусированного» растрового изображения было бы «СОРУР», а имя «отключенного» — «СОРУХ». Если Вы хотите протестировать работу отключенной кнопки, создайте растровое изображение «СОРУХ», например перечеркнутое красной линией, и вставьте в программу следующую строку:
m_editCopy.EnableMindow(FALSE);
ГЛАВА 12
OбpaбoткacooбщeнийWindows
и программирование многопоточных приложений■
Вытесняющая многозадачность и многопоточность Win32 произвели настоящую революцию в программировании для Windows. Если Вам попадались статьи или книги по этим вопросам, Вы, наверное, уже оценили всю сложность применения многопоточности. Вообще-то, Вы еще долго сможете разрабатывать полезные однопоточные приложения для Windows. Но, изучив основы многопоточности, Вы сможете создавать более эффективные и мощные программы и лучше разберетесь в модели программирования Win32.
Обработка сообщений Windows
Чтобы разобраться в потоках, сначала надо понять, как 32-разрядная Windows обрабатывает сообщения. Лучше всего начать с однопоточной программы, которая продемонстрирует значимость процесса трансляции и распределения сообщений. Затем мы усовершенствуем ее, добавив поток, управляемый с помощью глобальной переменной и простого сообщения. Потом поэкспериментируем с событиями и критическими секциями. А чтобы вникнуть в более сложные элементы, обеспечивающие многопоточность (такие, как мьютексы и семафоры), Вам придется обратиться к другой книге, — Advanced Windows by Jeffrey Richter, Microsoft Press, 1995 (Джеффри Рихтер, Windou-йли профессионалов, M.: «Русская Редакция», 1997)
Обработка сообщений в однопоточной программе
До сих пор все ваши программы были однопоточными (single-threaded), т, е. у кода был лишь един поток исполнения. Используя ClassWizard, Вы создавали функции-обработчики различных сообщений Windows, а также писали код функции OnDraw, вызываемой в ответ на сообщение WM_PAINT. Казалось бы, при появлении сообщения каким-то чудом вызывается ваш обработчик, но в Windows все происходит совсем не так. Глубоко в недрах MFC-кода, компонуемого с вашей программой, спрятаны примерно такие инструкции:
MSG message;
while (::GetMe$sage(&message, null, 0, 0)) {
::TranslateMessage(&message);
::DispatchKessage(&m9ssage);
Windows определяет, какие сообщения принадлежат вашей программе, а функция GetMes- sage возвращает управление, как только появляется сообщение для обработки. Если сообще-
ний нет, ваша программа приостанавливается, и выполняются другие приложения. Когда сообщение в конце концов поступает, ваша программа «пробуждается». Функция TranslateMessage транслирует сообщения WM_KEYDOWN в сообщения WM_CHAR, содержащие ASCII- символы, а функция DispatcbMessage передает управление (через оконный класс) коду выборки сообщений МЕС, который вызывает вашу функцию на основе таблицы обработчиков сообщений. Когда ваш обработчик завершается, он возвращает управление MFC-коду, что в итоге вызывает возврат из DispatcbMessage.
Передача управления
Что произойдет, если одна из ваших функций-обработчиков окажется «жадной* и израсходует 10 секунд процессорного времени? Во времена 16-разрядной системы компьютер просто завис бы на это время. Разве что можно было бы перемещать курсор мыши, да исполнялись бы какие-то задачи, управляемые прерываниями. В Win32 многозадачность организована куда лучше. Благодаря вытесняющей многозадачности другие приложения не зависнут — Windows, когда сочтет это нужным, просто приостановит выполнение вашей «жадной» функции. Однако даже в Win32 ваша программа на эти 10 секунд была бы заблокирована. Она не смогла бы обрабатывать какие-либо сообщения, так как DispatcbMessage не возвращает управление до тех пор, пока его не возвратит злополучный обработчик
Однако есть способ обойти эту проблему. Он срабатывает как в Win 16, так и в Win3 2. Надо просто заставить «жадную» функцию вести себя дружелюбнее, т. е. периодически отдавать управление — а для этого вставить в ее основной цикл такие операторы:
MSG message; *
if (::PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) {
: ^ranslateMessageC&meesage);
::DispatchMessage(&message);
Функция PeehMessage работает так же, как и GetMessage— за исключением того, что возвращает управление немедленно, даже если для вашей программы нет никаких сообщений. В этом случае «жадная» функция продолжает поглощать процессорное время. Однако, как только появляется сообщение, вызывается обработчик, и по завершении его работы выполнение функции возобновляется. .
Таймеры ■
Таймер Windows — это полезный элемент, иногда устраняющий необходимость в многопоточном программировании. Например, если Вам нужно считывать содержимое коммуникационного буфера, установите таймер так, чтобы выбирать накопившиеся символы каждые 100 миллисекунд. Таймер можно использовать и для управления анимацией, поскольку он не зависит от тактовой частоты процессора, '
Работать с таймерами легко. Вы просто вызываете функцию CWnd-SetTimerc параметром — интервалом времени, после чего с помощью ClassWizard определяете обработчик сообщения WM^TIMER. После запуска таймера с заданным интервалом в миллисекундах сообщения WM_TIMER постоянно посылаются вашему окну до тех пор, пока не будет вызвана CWnd::KmTimer$ina уничтожено окно. При необходимости можно задействовать несколько таймеров, каждый из которых идентифицируется целым числом. Поскольку Windows не является операционной системой реального времени, интервал значительно менее 100 миллисекунд приводит к потере точности.
Подобно другим сообщениям Windows, сообщения таймера могут заблокировать другие функции-обработчики в вашей программе. К счастью, сообщения таймера не аккумулируются Windows не ставит сообщения таймера в очередь, если в ней уже есть одно сообщение от данного таймера.
Пример ЕХ12А
Сейчас мы напишем программу с циклом, в котором выполняется большой объем вычислений. Протрамма должна обрабатывать сообщения после того, как пользователь приступит к вычислениям, — иначе он не сможет прервать их. Кроме того, нам хотелось бы отображать процент выполненной работы, применив элемент управления «индикатор продвижения» (см. рис. 12-1). Программа ЕХ12А обеспечивает обработку сообщений, передавая управление в цикле вычислений. Обработчик таймера обновляет индикатор в соответствии с продвижением процесса вычислений. Если бы процесс вычислений не передавал управление, нам не удалось бы обработать сообщения WM_TIM1HL . .
Рис. 12-1, Диалоговое окно Compute ■
Процесс создания приложения ЕХ12А.
Используя Арр Wizard, создайте проект\VCPP32\EX12А\ЕХ12А. Примите параметры, предлагаемые по умолчанию, но выберите Single Document и откажитесь от Printing And Print Preview. Параметры и имена классов по умолчанию приведены ниже на рисунке:
Используя редактор диалоговых окон, создайте диалоговый ресурс IDDjCOM- PUTE. Подготовьте ресурс, как показано на рисунке.
Оставьте у кнопки Cancel идентификатор по умолчанию, а кнопке Start присвойте идентификатор П)С_5ТАНТ.Для индикатора продвижения оставьте идентификатор по умолчанию IDCJPROGRESS1.
Используя ClassWizard, создайте класс CComputeDlg. ClassWizard подключит новый класс к только что созданному ресурсу IDDjCOMPUTE.
Создав класс, добавьте к нему обработчик сообщения WM_TIMER, а также обработчики сообщения BNCLICKED для ZDC_S7>l/?7h 1DCANCEL. Оставьте у них имена, предлагаемые по умолчанию: OnStartH OnCanceL
Отредактируйте файл ComputeDlg.h. Добавьте следующие закрытые переменные-члены: int ш_пТ1лег:
int ■_nCount;
enum { nMaxCount = 10000 };
Переменная-член m_nCountкласса CComputeDlg С^дст увеличиваться в процессе выполнения вычислений. Деление ее на константу nMaxCount даст единицу измерения продвижения.
Добавьте код инициализации в конструктор CComputeDlg в файле ComputeDlg.cpp. Напишите в конструкторе следующую строку (чтобы кнопка Cancel действовала, если вычисления еще не начаты):
m_nCount = 0;
Добавьте эту строку обязательно вне комментариев ///^ХПЛТИЛУ/Тргенерированных ClassWizard.
Запрограммируйте функцию OnStart в файле ComputeDlg.cpp. Этот код исполняется, когда пользователь щелкает кнопку Start Добавьте следующий код, выделенный полужирным:
void CComputeDlg: : onstart ()
{
MSG message;
hi nTimer = SetTimer(1, 100, NULL); // 1/10 секунды
ASSEHT(m nTimer 1= 0);
GetDlgItem(I0C_STAFr)->EnableWindow(FAL8E);
volatile int nTemp;
for (m_nCount= 0; m_ntount < nMaxCount; m_nCount++) {
for (nTemp = 0; nTemp < 1000; nTemp++)
О И просто занимаем процессор
if (:: PeekMessageChnessage, NULL, 0, 0, PM_REMOVE)> {
: :TranslateMesaage(6niessage);
::DispatchM6Ssage(imessage);
}
CDlalog: :OnOK();
}
Главный цикл /or контролируется счетчиком mjrCount. На каждой итерации цикла PeekMessage позволяет обрабатывать другие сообщения, в том числе WM_T1MER. Вызов Enable- Win<fow(FALSE) отключает кнопку Start на время вычислений. Без этой меры предосторожности возможен повторный вызов функции OnStart.
Запрограммируйте функцию OnTimere ComputeDlg.cpp. При срабатывании таймера показание индикатора устанавливается в соответствии со значением m_nCount. Добавьте следующий код, выделенный полужирным:
void CComputsDlg: :OnTimer(UINT nIDEvent)
CProgressCtrl* pBar = (CProgresaCtrl*) GetDlgItem(IDC_PR0GRESS1): p0ar-‘>SetPoa(m_nCount * 100 / nMaxCount);
}
Модифицируйте функцию OnCancel» ComputeDlgxpp. При щелчке пользователем кнопки Cancel в процессе вычислений мы не уничтожаем диалоговое окно, а присваиваем т_иО>мяГмаксимальное значение, в результате чего функция On5torf закрывает диалоговое окно. Если вычисления не начаты, диалоговое окно можно закрыть напрямую. Добавьте код, выделенный полужирным:
void CCorpiteDlg: :OnCancel()
{
TRACE( "Входим в CComputeDlg: :OnCancel\n");
if (m_nCount —'0) {// до нажатия кнопки Start
CDialog: :0nCancel();
else {// идут вычисления
n)_nCount = nMaxCount; // принудительное завершение OnStart }
)
Отредактируйте класс CExl2aVieUB exl2a¥iew.cpp. Во-первых, измените виртуальную функцию OnDraw, как показано ниже, чтобы она выводила сообщение:
void CEx11aView: :OnDraw(CDC*- pDC) {
pDC->TextOut(O, 0, "Щелкните здесь левой кнопкой мыши.”);
}
Во-вторых, используя ClassWizard, создайте функцию OnLButtonDown для обработки сообщения WM_LBUTTONDOWN и добавьте в нее кед, выделенный полужирным:
void CEx11aVlew: :OnLButtonDown(UINT nFlags. CPoint point)
{
CComputeDlg dig;
dlg.DoModalO;
}.■
Этот код открывает модальное диалоговое окно всякий раз, когда пользователь нажимает левую кнопку мыши, а курсор находится в окне представления.
Пока у Вас еще открыт файл cxl1aView.cpp, введите следующий оператор include-.
flinclude "ComputeDlg. h"-
Соберите и запустите приложение. Поместив курсор в окно представления, нажмите левую кнопкумыши, чтобы активизироватьдиалоговое окно. Щелкните кнопку Start, затем — Cancel. Индикатор продвижения должен показывать состояние процесса вычислений.
Обработка в периоды простоя
До появления многопоточности разработчики программ для Windows использовали периоды простоя (idle) для выполнения фоновых задач, например, разбивки документа на страницы. Теперь обработка во время простоя потеряла былое значение, но ей по-прежнему находится применение. Каркас приложений вызывает виртуальную функцию-член Onldle класса CWinApp,Ti Вы можете переопределить ее для выполнения фановых вычислений, Onldle вызывается из цикла обработки сообщений MFC-библиотеки, который на самом деле сложнее, чем приведенная выше простая последовательность GeШessage/ГranslateMessage/Dispaich- Message. Обычно функция Onldle вызывается после того, как очередь сообщений у приложе-
ния опустеет, причем только один раз. Если Вы переопределяете ее, вызывается ваш код, но при отсутствии непрерывного потока сообщений это не происходит постоянно. Onldle в базовом классе обновляет кнопки панели инструментов и индикаторы состояния, а также «чистит» указатели на временные объекты. Имеет смысл переопределять эту функцию, если Вы хотите обновлять состояние элементов пользовательского интерфейса. Ну, а то, что она не вызывается в отсутствие сообщений, значения не имеет: ведь пользовательский интерфейс и не должен в этом случае изменяться.
ПРИМЕЧАНИЕ Переопределяя функцию CWinApp::Onidle, не забудьте вызвать Onldle базового класса. Иначе не произойдет ни обновления кнопок на панели инструментов, ни удаления временных объектов.
Onldle вообще не вызывается, если пользователь работает в модальном диалоговом окне или выбирает что-то в меню. При необходимости фоновой обработки модальных диалоговых окон или меню придется написать обработчик сообщения WM_ENTERIDLE, но его надо добавить в класс окна-рамки, а не в класс «вид». Причина в том, что владельцем диалоговых окон всегда является основное окно-рамка приложения, а не окно представления. Взаимосвязи между окиом-рамкой и окном представления рассмотрены в главе 15.
Программирование многопоточных приложений
Как Вы помните из главы 10, процесс (process) — это выполняемая программа, обладающая собственной памятью, описателями файлов и другими системными ресурсами. Процесс может допускать несколько параллельных путей исполнения кода, называемых тк/токами (threads). Но не ищите отдельный код для разных потоков, потому что одну и ту же функцию могут вызывать несколько потоков. По большей части, все пространство кода и данных процесса доступно всем его потокам. Два потока могут, например, обращаться к одним и тем же глобальным переменным. Потоками управляет операционная система, и у каждого потока есть свой стек
Потоки в Windows бывают двух видов: рабочиепотоки (worker threads) и потоки пользовательского интерфейса (user-interface threads). MFC-библиотека поддерживает оба вида. У потока пользовательского интерфейса есть окна, а, значит, и свой цикл выборки сообщений, а у рабочего — нет. Рабочие потоки легче программировать, и они, как правило, полезнее. Примеры в этой главе иллюстрируют рабочие потоки. Но в конце главы описывается приложение с применением потока пользовательского интерфейса.
Не забывайте, что даже в однопоточном приложении есть поток, который называется основным потоком (main thread). В иерархии MFC-классов СЛГ/кДррявляется производным от CWinTbread. В главе 2 было сказано, что Initinstance и т_pMainWnd— это элементы класса CWlnApp. Вообще-то, это неправда. Эти элементы объявлены в CWinTbread.HO, конечно же, наследуются классом CWlnApp. Здесь важнее помнить, что приложение — это и есть поток
Функция рабочего потока и запуск потока
Если Вы еще не догадались сами, то знайте: для выполнения длительных вычислений рабочий поток эффективнее обработчика сообщений, содержащего вызов PeekMessage. Однако прежде чем думать о запуске рабочего потока, надо написать для него глобальную функцию. Она должна возвращать значение типа UINT ипринимать в качестве параметра одно 32-раз- рядное значение, объявленное как LPVOID, При запуске потока через этот параметр можно передать ему все, что угодно. Поток выполняет свои вычисления и завершается, когда глобальная функция возвращает управление. Он завершается и при закрытии процесса, но лучше, чтобы рабочий поток завершался раньше — это поможет избежать утечек памяти.
Чтобы запустить поток (с функцией, скажем, ComputeTbreadProc).программа делает вызов: CWinThread* pThread = AfxBeginThread(ComputeThreadProc. GetSafeHwnd(), THREAD_PRIORITY_NORHAL):
Код функции потока выглядит примерно так:
U1NT ComputeThreadProc(LPVOID pParam)
// процесс обработки return 0;
Функция AfxBeynThread возвращает указатель на только что созданный объект «поток». Этот указатель можно использовать для приостановки и возобновления исполнения потока ^CWm-Tbread-SusperufTbread^ ResumeTbread), но у объекта «поток» нет функции-члена для уничтожения потока. Второй параметр AfxBeginThread — 32-разрядное значение, передаваемое глобальной функции, а третий параметр представляет собой код приоритета потока. После запуска рабочего потока оба потока исполняются независимо друг от друга. Windows распределяет время между ними (и потоками других процессов) в соответствии с их приоритетами. Если основной поток ожидает сообщение, операционная система может продолжать исполнение потока вычислений.
Общение основного потока с рабочим
Основной поток (ваша программа) может передавать информацию вспомогательному рабочему потоку разными способами. Однако отправка Windows-сообщения — один из способов, который работать не будет, так как у рабочего потока нет цикла выборки сообщений. Простейшее средство коммуникации — глобальная переменная, поскольку все глобальные переменные доступны всем потокам процесса. Допустим, рабочий поток в процессе вычислений увеличивает и проверяет значение глобальной целочисленной переменной, завершаясь, когда значение переменной достигает 100. Основной поток может принудительно завершить рабочий поток, присвоив глобальной переменной значение 100 или более.
Приведенный ниже код, на первый взгляд, должен работать, и, если Вы его протестируете, может быть, так и случится:
UINT ComputeThreadProc(LPVOIO pParam)
g_nCount = 0;
while (g_nCount++ < 100) {
// здесь выполняются какие-то вычисления
}
return 0;,
>■
Однако здесь есть одна проблема, которую можно обнаружить, лишь посмотрев на сгенерированный ассемблерный код. Значение^пСГоииГзагружается в регистр, увеличивается там и переписывается обратно »g_nCount. Предположим, g_nCount равно 40 и Windows прерывает рабочий поток сразу же после того, как он загружает эго значение в регистр. Теперь управление получает основной поток и присваивает ^_пСояяГ значение 100. При возобновлении рабочий поток увеличивает значение регистра и записывает обратно в £_я<7о«п?чис- ло 41, стирая предыдущее значение 100. И вот — цикл потока не завершается)
Если же мы включим оптимизацию кода при компиляции, то получим дополнительную проблему. Для переменной ^nCounf компилятор использует регистр, причем значение переменной остается загруженным в него на протяжении всего цикла. Если основной поток изменит значение £_яС'оия£в памяти, эго никак не повлияет на цикл вычислений в рабочем потоке. (Однако, чтобы компилятор не хранил счетчик в регистре, можно объявить^яСоипГ как volatile)
Но допустим, Вы переписали процедуру потока так:
UINT ConputeThreadProc(LPVOID pParam) {
g_nCourt = 0;''
while (g_nCount < IDO) {,
// здесь выполняются какие-то вычисления ::lnterlockedlncrement((Long«)&g_nCount);
return 0;
}
Функция /к^г/ос^йУпсгемгея/предотвращает обращение к переменной со стороны другого потока во время ее изменения. Теперь основной поток смажет завершить рабочий.
Итак, Вы познакомились с некоторыми ловушками, подстерегающими программйста при использовании глобальных переменных. Иногда применение глобальных переменных оправданно, что иллюстрирует следующий пример, но есть и альтернативные методы, более гибкие; и мы еще обсудим их в этой главе.
Общение рабочего потока с основным
Рабочий потокдолжен проверять значения глобальной переменной в цикле, но что получится, если то же самое сделает основной поток? Помните «жадную» функцию? Вы, безусловно, не хотите, чтобы основной поток входил в цикл — ведь это лишь напрасная трата процессорного времени, да и ваша программа перестанет обрабатывать сообщения. Для коммуникации рабочего потока с основным предпочтительнее передавать сообщения Windows, так как у основного потока уже есть цикл выборки сообщений. Однако это подразумевает, что у основного потока есть окно (видимое или невидимое), а у рабочего потока — его описатель.
А как рабочий поток получает описатель? Для этого предназначен 32-разрядный параметр уже рассмотренной функции потока. Вы просто передаете описатель в вызовеAjicBeginTbread. Но почему бы вместо него не передать указатель на С++-объскт «окно»? Это может оказаться опасным, так как нельзя полагаться на то, что этот объект будет существовать постоянно, и, кроме того, разные потоки не могут совместно использовать объекты MFC-классов. (Данное правило не распространяется на объекты классов, производных непосредственно от CObject, и на простые объекты таких классов, как CRect или CString.)'
Следует ли посылать сообщение синхронно (send message) или асинхронна (post message)? Асинхронная передача предпочтительнее, так как синхронная может вызвать повторное вхождение в MFC-код для выборки сообщений основного потока, а это чревато проблемами. А какое сообщение следует посылать? Да любое пользовательское.
ПримерЕХ12В
Внешне программа ЕХ12В выглядит абсолютно также, каки ЕХ12А. Однако, обратившись к се коду, можно заметить некоторые различия. Вычисления проводятся в рабочем, а не в основном потоке. Значение счетчика хранится в глобальной переменной я «Count, которой присваивается максимальное значение в обработчике кнопки Cancel диалогового окна. Завершаясь, поток посылает сообщение диалоговому окну, что вызывает завершение функции DoModal.■
Классы документа, представления, рамки и приложения остались теми же, за исключением имен; не изменился и диалоговый ресурс. Класс модального диалогового окна по-прежнему называется CComputeDlg,KO его реализация существенно изменилась. Конструктор, обработчик таймера и функции обмена данными почти те же. Приведенный ниже код показывает определение глобальной переменной и глобальную функцию потока из файла \exl2t>\Com- puteDlg.cpp на компакт-диске. Заметьте: возврат из функции (и завершение потока) проис-
ходит, когда §_иСомиГ превышает определенное максимальное значение. Однако прежде чем завершиться, функция передает диалоговому окну пользовательское сообщение:
int g_nCount = 0;
UINT ComputeThreadProc(LPVOID pParam)
volatile Int nTemp;11 volatile, иначе компилятор перестарается с оптимизацией
for (g_nCount= 0; g_nCount < CComputeDlg::nMaxCount;
::InterlockedIncrement((long*)&g_nCount++)) {
■ for (nTemp = 0; nTemp < 1000; nTemp++).
■ О И просто занимаем процессор
// WH THREADFINISHED - это пользовательское сообщение '
::PostMessage((HWND) pParam, WMJXREADFINISHED, 0, 0);
g_nCount = 0;,
return 0; H завершаем поток
}
Показанный ниже обработчик OnStart связан с кнопкой Start диалогового окна. Его задача — запустить таймер и рабочий поток. Третий параметр A/xBeginThreadiosRona^i изменять приоритет рабочего потока — например, вычисления несколько замедляются, если установить приоритет THREAD _PRIORITY_LOWEST.
void CComputeDlg::OnStart()
* mjnTimer = SetTimer(1, 100, NULL); H 1/10 секунды
ASSERT(m nTimer 1= 0);
GetDlgItem(IDC_START)->EnableWindow(FALSE);
AfxBeginThread(ComputeThreadProc, GetSafeHwndO, THREAD PRIORITY NORMAL);
Обработчик OnCancel связан с кнопкой Cancel диалогового окна. Он присваивает переменной g_nCount максимальное значение, что приводит к завершению рабочего потока: void CComputeDlg::0nCancel()
if (g_nCount == 0) { H до нажатия кнопки Start CDialog::OnCancel();
else {// идут вычисления' •
g_nCount = nMaxCount; // принудительное завершение потока } }
Обработчик OnTbreadFinisbed^BssoBaeTca с посылаемым диалоговому окну пользовательским сообщением WM_THREADFINISHED. Он заставляет функцию DoModalзавершить свою работу:
LONG CComputeDlg::OnThreadFinished(UINT wParam, LONG IParam)
CDialog::OnOK();
return 0; }
Синхронизация потоков с использованием событий
Применение глобальной переменной — грубое, но эффективное средство коммуникации между потоками. Теперь попробуем что-нибудь более совершенное и попытаемся размыш-
лятъ в терминах синхронизации потоков, а не простей передачи информации. Наши потоки должны тщательно синхронизировать свое взаимодействие.
Событие (event) — один из типов объектов ядра (процессы и потоки тоже относятся к объектам ядра), предоставляемых Windows для синхронизации потоков. В пределах конкретного процесса событие определяется уникальным 32-разрядным описателем и для совместного использования несколькими процессами может идентифицироваться по имени (или описатель события может дублироваться в другом процессе). Объект «событие» находится либо в свободном состоянии («истина», signaled state), либо в занятом («ложь», unsignaled state). Существует два типа событий: с ручным сбросом (manual reset) и автоматическим сбросом (autoreset). Мы рассмотрим события с автосбросом, потому что они идеально подходят для синхронизации двух процессов.■ ■-• •
Давайте вернемся к примеру с рабочим потоком. Мы хотим, чтобы основной поток (поток пользовательского интерфейса) сигнализировал рабочему потоку, когда начинать и прекращать работу. Поэтому нам понадобятся события «запустить» («start») и «уничтожить» («kill»). MFC предоставляет для этого удобный класс CEuent, производный от CSyncObject. Конструктор по умолчанию создает объект Win32 «событие» с автосбросом в занятом состоянии. Если определить события как глобальные объекты, любой поток смажет легко обратиться к ним. Когда основному потоку надо запустить или уничтожить рабочий поток, он переводит соответствующее событие в свободное состояние вызовом CF.vent::SetEvent.
Теперь рабочий поток должен наблюдать за состоянием двух событий и должным образом реагировать, когда одно из них переходит в свободное состояние. Для этого в MFC есть класс CSingteLoch, но проще использовать Win32-фyикцию WaitForSingleObjectOHa приостанавливает поток до тех пор, пока заданный объектив освободится. В приостановленном состоянии поток не занимает процессорного времени — и это хорошо. Первый параметр функции WaitForSingleObject— описатель события. В качестве значения этого параметра можно использовать непосредственно объект CEvent, потому что этот объект наследует от класса CSyncObject оператор HANDIF,который возвращает описатель события, хранящийся в открытой переменной-члене. Второй параметр определяет период задержки. Если его значение INFINITE, освобождения объекта «событие» функция будет ждет вечно, а когда оно равно О, функция возвращает управление немедленно с результатом WUT^OBJECTjO^cth событие было в свободном состоянии.
Пример ЕХ12С
Эта программа использует для синхронизации рабочего и основного потоков два события. Большая часть кода EX 12С взята из примера EXI 2В, но класс ССоот/м/ГеР^еализован иначе. Файл StdAfx.h содержит следующую строку, необходимую для класса CEvent tfindude <afxmt.h>
В программе есть два глобальных объекта-события, как показано ниже. Обратите внимание: конструкторы создают события Windows до начала исполнения главной процедуры: CEvent g_eventStart; // создать события с автосбросом CEvent g_eventKill;
Сначала лучше всего рассмотреть глобальную функцию рабочего потока. Как и в ЕХ12В, она увеличивает значение счетчика g_nCount. Рабочий поток запускается функцией Onlnit- Dialog, а не обработчиком сообщений от кнопки Start. При первом вызове функция WaitForSingleObject "ждеп события «запустить», которое переводится в свободное состояние обработчиком кнопки Start. Параметр INFINITE означает, что поток буд ет жд ать столько, сколько надо- Второй вызов WaitForSingleObject отличается от первого — здесь задержка нулевая. Этот вызов расположен в основном цикле вычислений и просто проверяет, не освобождено ли событие «уничтожить» обработчиком кнопки Cancel. Если эго так, поток завершается.
HINT ComputeThreadPrDC(LPVOID ррагая).
volatile int nTemp;
: :WaitForSlngleObject(g_eventStart, INRNfTE);
ТЯАСЕ("Начало вычислений\п“);
for (g_nCount = 0; gjiCount < CComputeDlg: :nMaxCount; g„nCount4+) {
for (nTemp = 0; nTemp < 1000; nTemp-H)
0 // имитируем вычисления
if (::WaitForSingleObj6Ct(g„6ventKill, 0} ~- WAIT_0BJECT_0) break;
// сообщить окну-аладельцу об окончании работы.
: :PostHe8aage((HWND) pParam, WMJTHREADFINISHED, 0, 0);
g_nCount = 0;
return 0; // завершить поток
I
А вот функция OnlnitDialog, вызываемая при инициализации диалогового окна. Заметьте: она запускает рабочий поток, который ничего не делает до тех пор, пока не освободится событие «запустить»:
BOOL CComputeDlg::OnInitDialog()
1 CDialog::OnInltDialog();
AfxBeginThread(ComputeThreadProc, GetSafeHwndO);
return TRUE; // возвращать TRUE, если не установлен фокус на элементе управления // ИСКЛЮЧЕНИЕ; вкладки свойств OCX должны возвращать FALSE
Далее обработчик кнопки Start устанавливает событие «запустить* в свободное состояние и, тем самым, запускает цикл вычислений рабочего потока:
void CComputeDlg;;OnStart()
m_nTimer = SetTimer(1, 100, NULL); // 1/10 секунды ■'.
ASSERK* nTimer 1=0);
GetDlgItem(IDC_START)->EnableWindow(FALSE);.
g_eventStart.SetEvent();
}
Обработчик кнопки Cancel устанавливает событие «уничтожить» в свободное состояние, что вызывает завершение цикла вычислений рабочего потока:
void CComputeDlg::0nCancel()
if (g_nCount = 0) { //до нажатия кнопки Start
// прежде чем уничтожить поток, его надо загустить g_eventSta rt, SetEventf );
I
g_eventKill.SetEvent();'
Обратите внимание на весьма неуклюжее использование события «запустить», когда пользователь отменяет окно, не запустив процесс вычислений. Аккуратнее было бы определить новое событие «отменить» и заменить в функции ComputeTbreadProcnepBbift вызов WaitFor- SingleObject на вызов WaiiForMultipleObjectsFram бы ^ИРогМыШркОЬ^еаззбнаруживала событие «отменить», это могло бы приводить к немедленному завершению потока.
Блокировка потоков
Пример блокировки потока — первый вызов WaitForSingleObjecb функции ComputeTbread- Ргос. Поток просто прекращает выполнение до тех пор, пока событие не перейдет в свободное состояние. Есть и много других способов заблокировать поток. Можно, например, вызвать ^ш32-функцию Sleep, чтобы «усыпить* поток на 500 миллисекунд. Блокировку потока вызывают и функции, которые обращаются к устройствам вроде коммуникационных портов или дисков. Во времена Win 16 эти функции захватывали процессор до тех пор, пока их работа не заканчивалась, а в Win32 они позволяют выполняться другим процессам и потокам.
Следует избегать блокирующих вызовов в основном потоке пользовательского интерфейса. Если заблокировать основной поток, он не сможет обрабатывать сообщения, а из-за этого программа окажется весьма медленной. Если у Вас есть задача, требующая интенсивных операций дискового ввода/вывода, поместите соответствующий код в рабочий поток и синхронизируйте его со своим основным потокам. .
Будьте осторожны в рабочем потоке с вызовами, которые могут заблокировать его на неограниченное время. Проверьте по документации, можно ли для данной конкретной операции ввода/вывода установить интервал задержки. Если же вызов блокирует поток навечно, то поток все равно завершится при завершении основного потока процесса, но тогда не исключены утечки памяти. Можно также вызвать из основного потока функцию Win 32 Termina- teThread, но проблема утечек памяти остается и в этом случае.
Критические секции
Помните проблемы с доступам к глобальной переменной g_nCount? Если Вы хотите, чтобы несколько потоков совместно использовали глобальные данные и нуждаетесь в большей гибкости, чем та, которую предоставляют простые операторы типа fnteriochedlncrement.TO подойдет такое средство синхронизации, как критические секции (critical sections). События хороши для «сигнализации*, а критические секции (секции кода, требующие монопольного доступа к разделяемым данным) удобны для управления доступам к данным.
MFC предоставляет класс CCriticalSeclion — «обертку» описателя критической секции Windows1. Его конструктор вызывает функцию InitializeCriticalSection, функции-члены Lock и Untock вызывают функции EnterCriticaiSectionn LeaveCriticaiSection соответственно, а деструктор вызываетDeleteCriticalSection. Вог как можно использовать этот класс для защиты глобальных данных:
CCritical Section g_cs;
int g_nCount;
void func()
g_cs.Lock();
g_nCount++;,
g_cs.Unlock():
Допустим, ваша программа отслеживает показания времени как часы, минуты и секунды, а каждое из этих значений хранится в отдельной целочисленной переменной. Теперь представим, что значения времени совместно используются двумя потоками. Поток ^изменяет значение времени и прерывается потокам Б после обновления часов, но до обновления минут и секунд. Результат: поток Б получает недостоверные показания времени.
Если Вы пишете для данного формата времени класс C++, то сможете легко управлять доступом кданным, сделав элементы данных закрытыми и предусмотрев открытые функции-
В Windows нет такого описателя. — Прим.перев.
члены. Именно таков класс СНМ5(см. рис. 12-2). Заметьте: в этом классе есть переменная-член типа CCriticalSection. Таким образом, с каждым объектом CHMSсвязан объект «критическая секция».
Обратите внимание, что другие функции-члены вызывают функции-члены Lock и Unlock. Если поток Л исполняется в середине SetTime, поток 5 будет блокирован вызовом EnterCritical- Section в GeiTolalSecspp тех пор, пока поток Л не вызовет LeaveCnticalSection. Функция IncrementSecs вызывает Sell ime, что означает наличие вложенных критических секций. Это допустимо, так как Windows отслеживает уровни их вложенности.
Класс CHMS отлично работает, если Вы используете его для конструирования глобальных объектов. Если же потоки вашей программы совместно используют указатели на объекты в куче, то Вы столкнетесь с рядом других проблем. Каждый поток должен определять, не удален ли объект другим потоком, а, значит, нужна синхронизация доступа к указателям.
HMS.H
#include "StdAfx.h"
class CHMS
private: int m_nHr, m_nMn, m_nSc; CCriticalSection m_cs;
public;
CHMSO : m_nHr(O), m..nMn(0), m_nSc(O) {}
'CHMSO {} void SetTime(int nSecs) {•
m_cs. I ock(); m_nSc = nSecs % 60; m_nMn = (nSecs /60) % 60; m_nHr = nSecs / 3600; m_cs.Unlock(); }
int GetTotalSecsO { int nTotalSecs; m_cs.Lock(); nTotalSecs = m_nHr * 3600 + m_nMn * 60 + m_nSc; m_cs.UnLock();
return nTotalSecs:
}■
void IncrementSecs() { m_cs.Lock();
SetTi me (GetTotalSecsO + 1); m cs.UnluckO;
};
Рис. 12-2. Листинг класса CHMS
Мы не приводим пример программы, использующей класс CHMS, но на компакт-диске в подкаталоге \vcpp32\exl 2с есть файл hms.h. Создавая многопоточную программу, Вы сможе-
те применять этот класс для разделения глобальных объектов между потоками. При этом Вам не понадобятся другие функции, связанные с синхронизацией потоков.
Мьютексы и семафоры
Как мы уже говорили, эти синхронизирующие объекты хорошо описаны в книге Джеффри Рихтера Advanced Windows. Мьютекс (mutex — сокращение от mutual exclusion — взаимное исключение) или семафор может потребоваться, если Вы управляете доступом к данным, совместно используемым разными процессами, так как критическая секция доступна лишь в пределах одного процесса. Мьютексы и семафоры разделяются между процессами путем идентификации их по именам*.
Потоки пользовательского интерфейса *
MFC-бнблиотска обеспечивает хорошую поддержку потакав пользовательского интерфейса. Вы создаете класс, производный от CWinThread, и вызываете переопределенную версию А^сВе^пТЪгеа^дя запуска потока. У этого производного класса есть своя функция Initlnstonce и, что самое важное, свой цикл выборки сообщений, — это позволяет конструировать связанные с ним окна и обработчики сообщений.
Зачем нужен поток пользовательского интерфейса? Если Вы хотите работать с несколькими окнами верхнего уровня, их можно создать и управлять ими из основного потока. Но, предположим, Вы разрешаете пользователю запускать несколько экземпляров вашего приложения, однако хотите, чтобы все они разделяли общую память. Можно сделать так, чтобы в одном процессе исполнялось несколько потоков пользовательского интерфейса, а пользователи думали, что выполняются отд ельные процессы. Именно так работает Windows Explorer, Запустите утилиту SPY++ и убедитесь сами.
При запуске второго и последующих потоков без уловок не обойтись, так как пользователь фактически каждый раз запускает новый процесс. При запуске второго процесса тог сигнализирует первому запустить второй поток и завершается. Второй процесс обнаруживает первый либо с помощью Win324f)VHKUHH.F7n<^zndbtp,nH6o путем объявления общей секции данных. Общие секции данных детально рассматриваются в книге Джеффри Рихтера.
А также путем дублирования описателей, аналогично событиям. — Прим-перев.
ЧАСТЬ III
Архитектура «документ-вид»
ГЛАВА 13
Меню, быстрые клавиши, поле ввода с форматированием и окна свойств
15 предыдущих примерах этой книги большинство действий в программах выполнялось после щелчка кнопки мыши. Даже когда удобнее было выбрать из меню, мы все равно щелкали кнопки мыши, потому что сообщения от нее обрабатываются очень просто и непосредственно в окне представления MFC 6.0. Если Вы хотите, чтобы выполнение действий в вашей программе начиналось после выбора команд из меню, придется вначале освоить другие элементы каркаса приложений.
Эта глава посвящена меню и архитектурераспределения команд (command routing architecture). Попутно Вы познакомитесь с понятиями «рамка» (frame) и «документ», уясните себе взаимосвязь между этими элементами каркаса приложений и уже известным Вам элементом «вид». Вы научитесь пользоваться редактором меню для визуального создания меню и средствами ClassWrzardAHH связи функций-членов классов «документ* и «вид» с командами меню. Кроме того, Вы освоите специальные функции-члены, позволяющие обновлять командный пользовательский интерфейс (в том числе изменять состояние команд в меню) и научитесь программировать «быстрые клавиши», ускоряющие доступ к командам меню,
Поскольку Вы, вероятно, уже устали от окружностей и диалоговых окон, здесь Вы познакомитесь и с другими ст роительными блоками MFC — элементом управления «пале ввода с форматированием» (rich edit control), который предоставляет богатые возможности редактирования текстов, и окнами свойств (property sheets), идеально подходящими для задания и редактирования значений свойств.
Классы основногоокна-рамки и документа
До сих пор мы использовали окно представления так, славно эго единственное окно приложения. Но в SDI-приложении это окно располагается внутри другого окна — основного окна- рамки приложения. Именно ему принадлежат заголовок и меню. Клиентскую область основного окна занимают всевозможные дочерние окна, в том числе окно представления, окно панели инструментов и окно строки состояния (см рис. 13-1). Каркас приложений управляет взаимодействием между окном-рамкой и окном представления, доставляя сообщения от первого второму.
Посмотрите еще раз на файлы какого-нибудь проекта, которые мы сгенерировали, используя AppWizard. Файлы MainFrm.lt и MainFrm.cpp содержат код класса основного окна-рамки приложения, производного от класса CFrameWnd. В других файлах (например, таких, как cxl3aDoc.h и cxl3aDoc.cpp) хранится код класса документа, производного от CDocument. Начиная с этой главы, мы будем работать с MFC-классом «документ*. Начнем с того, что каж-
дый объект «вид» связан только с одним объектом «документ» и функция-член tietDocument возвращает указатель на этот документ. В главе 15 Вы займетесь изучением окон-рамок, а в главе 16 продолжите углубленное изучение взаимодействия «документ-вид».
Рис. 13-1. Дочерние окна в основном окне-рамке SDI-приложения
Меню Windows
Меню Microsoft Windows — знакомый всем компонент приложения, состоящий из горизонтального списка элементов верхнего уровня; с ним связаны меню, открывающиеся, когда пользователь выбирает какой-либо из его элементов. Обычно для окна-рамки определяется ресурс меню по умолчанию, загружаемый при создании этого окна. Можно определить и ресурс меню, не связанный с каким-либо окном-рамкой. В этом случае Ваша программа должна вызывать функции, необходимые для загрузки и активизации меню.
Ресурс меню полностью определяет начальное состояние меню: какие-то команды могут быть отключены, помечены галочкой или сгруппированы с помощью разделителей. Раскрывающиеся меню могут быть многоуровневыми. Если команда меню первого уровня связана с подменю, то рядом с этой командой появляется стрелка, указывающая вправо, как у команды Debug из меню Build на рисунке 13-2.
Рис. 13-2. Многоуровневые раскрывающиеся меню (в Microsoft Visual C++)
В Visual C++ входит простой в применении редактор меню. Это средство позволяет создавать и редактировать меню в режиме WYSIWYG, Для каждой команды меню открывается диалоговое окно, в котором Вы задаете все характеристики этой команды. Полученное определение ресурса сохраняется в RC-файле проекта. Каждой команде меню присваивается свой идентификатор например, Ю_РД5_ОРЕЛ^оторый определяется в файле resource.h.
Библиотека MFC расширяет функциональные возможности стандартных меню Windows. У каждого элемента меню может быть строка подсказки, появляющаяся в строке СОСТОЯНИЯ окна-рамки при выборе этого элемента. Данные подсказки — это строковые ресурсы Windows, связанные с командами меню одинаковыми идентификаторами. Для редактора меню и вашей программы эти подсказки выглядят как часть определения команды в меню.
Быстрые клавиши
Вероятно, Вы заметили, что в большинстве команд меню есть подчеркнутый символ. В среде разработки Visual C++ (и во многих других программах) нажатие сочетания клавиш Alt+F, а затем S активирует команду Save из меню File. Такая система быстрых клавиш. — это стандартны й для Windows способ ускорения доступа к командам меню с клавиатуры. Если Вы по- , смотрите на файл описания ресурсов приложения (или заглянете в диалоговое окно свойств для команды в редакторе меню), то заметите, что символам, подчеркнутым в командах меню, предшествует знак &.'
В Windows предусмотрен еще один способ связывания последовательностей клавиш < командами меню. Ресурс клавиш-акселераторов — это таблица, описывающая комбинации клавиш и соответствующие им идентификаторы команд. Через запись в такой таблице команду Сору из меню Edit (с идентификатором ID_EDIT_COPY)s.oxho связать, скажем, с комбинацией клавиш Ctrl+C. Быстрая клавиша необязательно должна соответствовать какой- либо команде в меню. Например, даже если бы в меню Edit не было пункта Сору, комбинация клавиш Ctrl+C все равно активировала бы команду IDEDIT_COPY.
ПРИМЕЧАНИЕ Если быстрая клавиша связана с командой меню или кнопкой панели инструментов, то при отключении команды или кнопки отключается и эта клавиша.
Обработка команд
.Как Вы уже видели в главе 2, у каркаса приложений есть весьма изощренная система распределения командных сообщений, поступающих в результате выбора элементов меню, нажатия быстрых клавиш, а также щелчков кнопок на панелях инструментов или в диалоговых окнах. Командные сообщения можно также отправить, вызвав функцию CWncLSendMessage или PostMessege. Сообщения идентифицируют "^е^ие-константы, часто назначаемые в редакторе ресурсов. У каркаса приложений есть собственный набор идентификаторов внутренних командных сообщений, например, ID_FILE_PR1NT wmID_FILE_OPENA файл resource.h вашего проекта содержит идентификаторы, уникальные для вашего приложения.
Большинство командных сообщений генерируется в окне-рамке приложения, и именно здесь, не будь каркаса приложений, Вам пришлось бы размещать обработчики комацц. Благодаря же системе распределения команд эти сообщения можно обрабатывать практически где угодно. Обнаружив командное сообщение от окна-рамки, каркас приложений начинает поиск соответствующих обработчиков в следующей последовательности.
В SDI-приложенииВ MDi-приложении
Класс «вид»Класс «вид»
Класс «документ»Класс «документ»
Класс «основное окно-рамка SDI»Класс «дочернссокно-рамкаMDI»
Класс «приложение*Класс «основное окно-рамкаMDI»
Класс «приложение»
В большинстве приложений обработчик конкретной команды присутствует в одном классе, но предположим, что в программе с одним окном представления один и тот же обработчик
имеется и в классе «вид», и в классе «документ». Тогда, поскольку окно представления в иерархии распределения команд занимает более высокую ступень, вызывается обработчик из класса «вид».
Что надо сделать, чтобы задать функцию-обработчик команды? Требования по определению такого обработчика аналогичны уже известным Вам правилам определения обработчика оконных сообщений. Для этого нужна сама функция, соответствующий элемент в таблице сообщений и прототип функции. Предположим, у нас есть в меню команда Zoom (с идентификатором 1DM_ZOO№)которую надо обрабатывать в классе «вид». В этом случае добавьте сначала в файл реализации класса «вид» такой код:
BEGIN MESSAGE MAP(CMyView. CView)
ON COMMAND(IDM ZOOM, OnZoom)
END_MESSAGE_MAP()
void CMyView: :OnZoom() {
Ц код обработки командного сообщения >
а потом введите в файл заголовка класса CMyView прототип (перед макросом DECLA- RE_MESSAGE_MAP);
afx_msg void OnZoomO;
Конечно, Class Wizard автоматизирует процесс вставки обработчиков командных сообщений точно так же, как и вставку обработчиков оконных сообщений. Но как именно это работает, Вы узнаете, когда мы перейдем к примеру программы ЕХ13А.
Обработка командных сообщений в производных классах‘
Система распределения команд — лишь одна сторона обработки командных сообщений. Не менее важна здесь иерархия классов. Заглянув в исходный код MFC-классов, Вы увидите в таблице сообп^ний множество элементов ОА^С’ОМи^УР.При создании производного класса от одного из этих базовых классов, например, от CView, производный класс наследует все функции таблицы сообщений CView, включая функции-обработчики командных сообщений. Чтобы переопределить какую-то функцию таблицы сообщений базового класса, в производный' класс надо добавить как функцию, так и соответствующий элемент таблицы сообщений.
Обновление командного пользовательского интерфейса
Весьма часто приходится менять внешний вид элементов меню, чтобы отразить внутреннее состояние программы. Например, если в меню Edit присутствует команда Clear All (Очистить все), то Вы, может быть, захотите отключить ее, если очищать нечего. Без сомнения, в меню Windows-программ Вы видели отключенные и помеченные галочками команды.
При программировании в Win32 синхронизировать состояние элементов меню в соответствии с состоянием приложения не так-то просто. Каждый участок кода, изменяющий внутреннее состояние программы, должен содержать операторы, обновляющие меню. В MFC-библиотеке реализован другой подход, основанный на вызове специальной функции-обработчика, которая и обновляет командный пользовательский интерфейс при каждом открытии меню Аргумент этой функции — объект CCmdUl, содержащий указатель на соответствующий элемент меню. Используя этот указатель, функция-обработчик может изменить внешний вид команды меню. Подобные обработчики применимы к элементам раскрывающихся меню, но не к меню верхнего уровня, постоянно присутствующим на экране. Такой обработчик нельзя использовать, например, для отключения меню File.
Требования к определению обработчиков, обновляющих командный пользовательский интерфейс, сходны с требованиями к определению обработчиков команд. Вам нужна собственно функция, специальный элемент таблицы сообщений и, конечно же, прототип функ-
ции. Идентификатор (в нашем случае /Р^^ОО^примвняется тот же, что и для команды. Вот что надо добавить к файлу кода для класса «ВИДА;
BEGIN.MESSAGE MAP(CMyView, CView)'
ON.UPDATE COMMMAND UI(IDH_ZOOM, OnUpdateZoom)
END_MESSAGE MAP()
void CMyVlew: :OrUpdateZooiri(CCnidUl* pCmdUZ) { pCmdUI->SetCheck(.n_bZoomed): // m_b2oemed - переменная-член класса j
Прототип следует включить в заголовочный файл класса (перед макросом DECLARE_MES- SAGE_MAPy.
afx_msg void OnUpdateZoom(CCndUI‘ pCmdUI);
Понятно, что ClassWizard автоматизирует и процесс вставки обработчиков, обновляющих командный пользовательский интерфейс.
Команды, генерируемые диалоговыми окнами
Допустим, Вы хотите, чтобы какая-то из кнопок вашего диалогового окна посылала командное сообщение. Идентификаторы команд должны попадать в диапазон от 0x8000 до OxDFFF, который редактор ресурсов использует для элементов меню. Если присвоить кнопке диалогового окна идентификатор из этого диапазона, она будет генерировать команду, распределяемую каркасом приложений. Сначала каркас перешлет эту команду основному окну-рамке, так как именно оно — владелец всех диалоговых окон. Затем доставка команды пойдет как обычно; если в классе «вид» есть обработчик командного сообщения от кнопки, он и будет ее обрабатывать. Чтобы значение идентификатора не вышло за указанный выше диапазон, пользуйтесь редактором символов (в редакторе ресурсов) — это позволит определить идентификатор прежде, чем он будет назначен кнопке.
Меню, встроенные в каркас приложений
Создавать меню для каждого окна-рамки «с нуля» нет необходимости — в MFC-библиотеке уже определено несколько полезных меню (см. рис. 13-3) вместе со всеми функциями-обра- б отчиками команд.
Рис. 13-3. Стандартные меню в окне-рамке Spl-приложения
Какие именно меню и обработчики командных сообщений Вы получите, зависит от параметров, установленных в AppWizard. Например, если отключить Printing And Print Preview, то элементов меню Print и Print Preview не будет. Так как поддержка печати необязательна, соответствующие элементы в таблице сообщений в классе CView не определены, а генерируются для конкретного производного класса. Вот почему элементы, подобные приведенным ниже, определены в классе CMyVlew, а не в CView:
ON_COMMAND(ID FILE PRINT, CView: :OnFilePrinl)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
Включение и отключение команд в меню ■
Каркас приложений способен сам отключить команду меню, не найдя соответствующий обработчик па текущем маршруте распределения команды, и это избавляет от необходимости писать обработчики ON_UPDATE_COMMANDJL7&ry особенность можно запретить, присвоив переменной m_bAutoMenuEnableкласса СУгляте^гг/значение VALSE.
Допустим, у Вас есть два представления одного документа, но только в первом классе «вид» присутствует обработчик сообщения для команды IDM_ZOOM.'B этом случае команда Zoom в меню окна-рамки будет активна, только когда активно первое окно представления. Или же рассмотрим стандартно генерируемые элементы меню Edit: Cut, Сору и Paste. Они отключаются, если в производном классе «вид* или «документ» не определены обработчики этих командных сообщений.
Возможности редактирования текста в MFC
В Windows есть два средства редактирования текста: обычное поле ввода и поле ввода с форматированием. Их можно использовать как элементы управления в диалоговых окнах, но можно сделать и так, чтобы они выглядели как окна представления. Эту гибкость обеспечивают MFC-классы CEditView и CRicbEditView.
Класс CEditView
В основе этого класса лежит элемент управления Endows «поле ввода», следовательно, классу присущи и все ограничения этого элемента. Объем текста не может превышать 64 Кбайт, нельзя использовать и несколько шрифтов сразу. AppWizard позволяет задать CEditView в качестве базового для вашего класса «вид». При работе с объектом CEditView Вам доступны все функции-члены как CView, так и CEdit. Множественное наследование здесь не применяется, есть просто несколько «фокусов», включая создание оконных подклассов. Класс CEditView реализует функции, с которыми увязываются команды работы с буфером обмена «вырезать», «копировать» и «вставить», так что эти команды разрешены в меню Edit
Класс CRichEditVIew '
Этот класс окна представления базируется на элементе управления «ПОЛС ввода с форматированием» и потому поддерживает большие объемы текста и его форматирование. Класс CRiebEditView предназначен для совместного использования с классами CRicbEditDocm CRicb- EditCntrltem, что позволяет реализовать полноценное приложение контейнера ActiveX.
Класс CRichEditCtrl
Этот класс — обол очка для элемента управления «поле ввода с форматированием», и его можно использовать для создания довольно простого текстового редактора. Именно это мы и сделаем в примере ЕХ13А. Мы возьмем обычный класс «вид», производный от CView. и разместим по всей клиентской области окна большое поле ввода с форматированием, которое будет само менять свои размеры при масштабировании окна. Класс CRichEditCtrl включает десятки полезных функций-членов, в том числе из базового класса CWnd. В этой главе мы используем следующие функции.
Функция
Описание
Create
Создает окно элемента управления «поле ввода с форматированием» (вызывается из обработчика WM_CREATE родительского окна)’
SetWindowPos
Задает размер и положение окна ввода (устанавливает размер окна так, чтобы оно соответствовало клиентской области окна представления)
GetWindowlbxt
Получает из элемента управления неформатированный текст (текст в формате RTF возвращают другие функции)
см, след. стр.
Функция
Описание'
SetWindowText tietModijy
Записывает в элемент управления неформатированный текст
Получает признак, значение которого равно TRUE, если текст изменен [текст ’ изменяется, когда пользователь что-то набирает в элементе управления или,
когда программа вызывает SetModtjyfiltUE)^
SeiModify GetSel
Устанавливает признак модификации TRUE иди FALSE
ПЬлучает признак, значение которого указывает, выделил ли пользователь, какой-либо текст
SetDefaidtCba rFonnat
SelSeiectionCbdrFonnat
Устанавливает характеристики форматирования по умолчанию
Устанавливает характеристики форматирования выделенного текста
ПРИМЕЧАНИЕ Если Вы добавили поле ввода с форматированием в диалоговое окно, то в функции Initlnstance класса приложения необходимо вызвать функцию AfxInitRichEdit.
Пример ЕХ13А
Этот пример иллюстрирует доставку команд от меню и быстрых клавиш документу и окну представления. Класс «вид» приложения — производный от <77?еи?и содержит элемент управления «поле ввода с форматированием». Команды нового раскрывающегося меню Transfer, направляемые окну представления, пересылаютданные между объектами «вид» и «документ», а команда меню Clear Document удаляет содержимое документа. В меню Transfer команда Store Data In Document отключена, если в окне представления ничего нет. Команда Clear Document, расположенная в меню Edit, становится неактивной, когда документ пуст. На рисунке 13-4 представлен первый вариант программы ЕХ1 ЗА в действии.
covers the client area of the view. Note that automatic wordwrap is in effect.
. -- -т—-
Рис. 13-4. Программа EX1 ЗА в действии
Если полностью задействовать архитектуру «документ-вид», то поле ввода с форматированием должно хранить свой текст в документе, но сделать это довольно трудно. Вместо этого определим в документе переменную-член m_strText класса CString, содержимое которой пользователь может перемещать в элемент управления и из него. Начальное значение m_sir- Text— «Hello»; выбор команды Clear Document из меню Edit делает эту строку пустой. Поработав с примером, Вы начнете лучив попимачь, в чем различие между документом и его представлением.
Первая часть примера ЕХ1 ЗА позволяют поупражняться в работе с редактором меню и редактором быстрых клавиш совместно с ClassWizard. Собственно программирования на С++ здесь будет совсем немного. Итак, выполните следующие действия.
Используя AppWizard, создайте проект \vcpp32\exl3a\exl3a. Примите параметры, предлагаемые по умолчанию, но выберите Single Document и откажитесь от Printing And Print Preview.'
Отредактируйтеосновноеменюприложения, используяредакторресурсов. Щелкните вкладку Resource Mew в окне Workspace. Отредактируйте ресурс меню IDR_MA1N- РНАМЕ.чтобы добавить разделитель и команду Clear Document в меню Edit, как показано ниже:
СОВЕТ Редактор меню интуитивно ясен, но в первый раз Вам, наверное, будет непонятно, как вставить команду в середину меню. Так вот, внизу каждого меню есть пустая строка. Переместив ее мыиью на нужное место, определите как новую команду в меню. Закэжив, Вы увидите внизу меню новую пустую строку.
Теперь добавьте меню Transfer и определите его элементы, как показано ниже на рисунке;
Назначьте новым элементам меню следующие идентификаторы.
Меню
Команда
Идентификатор
Edit
Clear &Document
ro_EDlT_Cl£AR_ALL
Transfer
&Get Data From Document\tF2
ID_TRANSFER_GETDATA
Transfer
&Store Dara In Document\tF3
1D_ TRANSFER- STOREDATA
Первый элемент с идентификатором 1О_£О/Т_<Х£4Л_Л22определен в MFC-библиотеке. (Заметьте: \t — это символ табуляции, но введите именно \t, не нажимайте клавишу Tab.) Добавляя команды в меню, вводите соответствующие строки подсказки в диалоговом окне Menu Item Properties. Эти подсказки будут появляться в строке состояния программы при выделении подсветкой команды меню.
Используя редактор ресурсов, укажите клавиши, ускоряющие доступ. Откройте таблицу быстрых клавиш IDR_MA1NFRAM&_, пользуясь клавишей Ins, вставьте следующие
элементы.
Идентификатор быстрой клавиши
Клавиша
ID_TRANSFER_GETDA ТА
VK_F2
ID_ TRANSFER STOREDATA
VK_F3
Убедитесь, что модификаторы Ctrl, Alt и Shift отключены. А теперь посмотрим, как выглядит окно редактора быстрых клавиш и диалоговое окно свойств:
i ехГЛис ■ ШР.
_ M A1 h Ж A M t tA с c eic ? а ил |;;
ID EDIT_COFY ID RLE HEW ID_FILE_OPEN ID FILE_SAVE ID_EDIT_PASTE ID_EDIT_UNDQ ID_EDIT_DJT ID_TRANSFER_GETDATA
Ctl + C Cbl+N Cbl + O an*s a-i«v At+VK_BA£K Shift *VK DELETE VK. F2
VIRTKEY;
VIRTKEYi
VIRTKEY1
VIRTKEY VIRTKEY VIRTKEY
VIRTKEY
VIRTKEY'
| ID TRANSFER "
(№№1
VK г J
vIFTIEi
ID NEXT_PANE
ID PREV
ID.EDIT
ID EDIT pS^I
ID_EDIT de
ID EDIT ul«
A 11 z*
,1^ф>1«#Н**Ж<*Н'^^М»*>Н*4**И«****^
lei ।1
|VK_F3
H^ L ЯШ
4. Используя ClassWizard, создайте в классе «вид» обработчики командных сообщений и сообщений обновления командного пользовательского интерфейса. Выберите класс CExl3aVietVH добавьте следующие функции-члены.
Идентификатор объекта
Сообщение
Имя функции-члена
ID_ TRAMSFERjSETD^A
COMMAND
On TYamferGetdata
iDJRANSFERJTOREDATA
COMMAND
On'D-ansferStdredata
ID_ TRANSFER STOREDATA
UPDATB_COMMAND_UI
OnUpdateTransferStoredata
Используя ClassWizard, создайте в классе документа обработчики командных сообщений и сообщений обновления командного пользовательского интерфейса. Выберите класс СЕх13аОоси добавьте следующие функции-члены.
Идентификатор объекта
Сообщение
'Имя функции-члена
ZD EDIT. CLEAR ALL
COMMAND
OnEditCleerDocument
ID_EDIT_CLEAR_ALL
UPDATB_COMMAND_UI
OnUpdateEditCiearDocument
Добавьте переменную типа CStrtngB класс СЕх!JtrDoc. О тред актируйте файл ех13а- Doc.h вручную или при помощи ClassVicw:
public:
CString «„strText;'
Отредактируйте функции-члены класса «документ* в файле ex!3aDoc.cpp. Функция О»№гл£)о<-^шеггг сгенерирована ClassWizard, Как Вы увидите в главе 16, каркас приложений вызывает ее, когда документ создается впервые и когда пользователь выбирает из меню File команду New. Наша версия этой функции присваиваеттекст строковой переменной. Введите код, выделенный полужирным:
BOOL CEx13aDoc::OnNewDocument() (
if (!CDccumcnt::OnNewOocument())
return FALSE;
m_strText = "Hello (from CEx13aDoc: :OnNewDocument)";
return TRUE;
}
Обработчик сообщения Edit Clear Document очищает m_strText, а обработчик обновления пользовательского интерфейса делает блеклой соответствующую команду меню, если строка уже пуста. Помните, что каркас приложений вызывает OnUpdateEditClearDocument^sr- да открывается меню Edit. Добавьте код, выделенный полужирным:
void CEX13aDoc::OnEditClearDocument() {
m_strText.Empty();
void CExl3aDoc::OnUpdateEditC16arDocurent(CCmdUl* pCmdUI) {
pCmdUI->Enable(!m_strText.IsEmpty());
Добавьте переменную типа CRichEditCtrl в класс СЕлг/ЗлУ/^иг-Отредактируйте файл exl3aView,h вручную или используя ClassWizard:
public:
CRichEditCtrl m_rich;
Используя ClassWizard, создайте в классе СЕдг23аИе«юбработчики сообщений WMCREATE и WMSIZE, Функция OnCreate создает элемент управления «поле ввода с форматированием». Здесь размер элемента управления равен 0, так как у окна представления еще нет размера. Ниже приведен код обоих обработчиков:
int CEx13aView::OnCreate(LPCREATESTRUCT IpCreateStruct) {
CRect rect(0. 0, 0, 0);
if (CView:;OnCreate(lpCreateS*ruct) == -1)
return -1;
m_rlch.Create(ES_AUTOVSCROLL ! ES MULTILIKE ! ES WANTRETURN !
WS_CHILD ! WS_VISIBLE ! WS_VSCROLL, root, this, 1);
return 0;
}
Windows посылает сообщение WM_SIZE окну представления, как только будет определен его начальный размер, а потом — всегда, когда пользователь изменяет размер рамки. Наш обработчик корректирует размер поля ввода с форматированием так, чтобы оно заполняло всю клиентскую область окна представления. Добавьте код, выделенный полужирным: void CExl3aView: :OnSize(UINTnType, int ex, int су) {
CRect rect;
CView::OnSize(nType. ex, cy);
GetCllentRect(rect);
m_rich. SetklindowPos(4wndTop, 0, 0, rect. right - rect,left, rect.bottom - rect.top, SWP SHOWWINDOW);
}
Отредактируйте обработчики команд меню в файле exlSaView.cpp. Заготовки этих функций были сгенерированы ClassWizard при создании обработчиков команд на шаге 4. Функция Оп7Иям^г(7еЙ1дгжопирует текст из переменной-члена класса «документ» и помещает его в поле ввода с форматированием. Затем функция очищает признак модификации элемента управления. Обработчик, обновляющий командный пользовательский интерфейс, здесь отсутствует. Введите код, выделенный полужирным:
void CExl3aView::OnTrarsferGetData() {
CEx13aDoc* pDoc = GetDocumentO;
m_rich.SetWindowText(pDoc->m_strText);■
m_rich.SetModify(FALSE);
}
Функция OnTransferStoreDataiLoinipyer текст из поля ввода с форматированием в строку документа и очищает признак модификации элемента управления. Соответствующий обработчик, обновляющий командный пользовательский интерфейс, отключает команду меню, если содержимое элемента управления не менялось с момента последнего обмена содержимым с документам. Дополните заготовку кодом, выделенным полужирным:
void CEx12aView::OnTransferStoreOata()
CEx13aDoc* pDoc = GetDocumentO;
m_rich.GetWindowText(pDoc->m_strText); •
m rich.SetModify(FALSE);
}'
void CEx12aView::OnUpdateTransferStoreCata(CCmdUI* pCmdUI)
<
pCmdUI->Enable(m rich.GetNodify());
И.Соберите и протестируйте приложение ЕХ13А. Сразу после запуска приложения команда Clear Document в меню Edit должна быть активна. Выберите из меню Transfer команду Get Data From Document, и в окне появится некий текст. Отредактируйте его и выберите команду Store Data In Document Теперь эта команда должна стать неактивной. Выберите команду Clear Document и снова вызовите команду Get Data From Document.
Окна свойств
Вы уже видели их в Visual C++ и других современных программах для Windows. Они позволяют разместить в маленьком диалоговом окне много информации, да еще разбитой на категории. Пользователь выбирает страницы (вкладки), щелкая их ярлычки. В Windows предусмотрен элемент управления «набор вкладок» (tab control), который можно вставить в диалоговое окно. Но Вы, скорее всего, буд ете размещать диалоговые окна в наборе вкладок В MFC- библиотеке есть соответствующая поддержка — окно свойств (property sheet), Кстати, отдельные его диалоговые окна — вкладки — называются страницами свойств (property pages).
Создание окна свойств.
Процесс создания окна свойств с использованием инструментальных средств Visual C++,
Создайте в редакторе ресурсов набор шаблонов диалоговых окон примерно одинакового размера. Их заголовки — строки, отображаемые на ярлычках.
Используя Classwizard, сгенерируйте класс для каждого шаблона. В качестве базового класса выберите СРгорегГуРазеДобавъте переменные-члены для элементов управления.
Используя ClassWizard, создайте один класс, производный от CPropertySbeel.
Добавьте в класс окна свойств по одной переменной для каждого класса страниц свойств.
В конструкторе класса окна свойств вызовите функцию-членЛ^Ра^едля каждой страницы, указывая адрес внедряемого объекта страницы свойств.
Создайте в своем приложении объект класса, производного от CPropertySbeet, а затем вызовите DoModal. В вызове конструктора укажите заголовок окна (впоследствии его можно будет изменить обращением к функции CPropertySbeet: rSetTitle').
Запрограммируйте обработку кнопки Apply.
Обмен данными в окне свойств
Каркас приложений размещает в окне свойств три кнопки (см. рис. 13-5). Учтите: каркас вызывает DDX-код для страницы свойств всякий раз, когда пользователь переключается на нее или с нее на другую страницу. Кроме того, как и следовало ожидать, каркас приложений вызывает DDX-код для страницы, когда пользователь щелкает кнопку ОК, тем самым обновляя элементы данных для этой страницы. Отсвэда можно сделать вывод, что все элементы данных для всех страниц свойств обновляются после щелчка кнопки ОК — и все это не требует от Бас программирования на C++.
ПРИМЕЧАНИЕ В обычном модальном диалоговом окне при щелчке кнопки Cancel все изменения теряются, и переменные-члены класса диалогового окна остаются неизменными. Однако в окне свойств переменные-члены обновляются, даже если пользователь модифицирует содержимое одной страницы, переходит к другой, а потом отменяет изменения в окне свойств щелчком кнопки Cancel.
Что делает кнопка Apply? А ничего, если Вы не напишете для нее какой-нибудь код. Она даже не будет активна. Чтобы активизировать ее для конкретной страницы, проверьте, внесены ли туда изменения, и если да, установите признак модификации этой страницы, вызвав SetModiped(TRUE).
Если Вы активизировали кнопку Apply, для нее можно написать функцию-обработчик в вашем классе страницы свойств, переопределив виртуальную функцию CPropertyPage.x-OnApply. Не пытайтесь разобраться, как происходит обработка сообщений страницами свойств, по аналогии с обычным модальным диалогом — это совершенно разные виды обработки. При щелчке любой кнопки каркас приложений получает сообщение WM_NOTIFY и, если «нажата» кнопка ОК или Apply, вызывает DDX-код для страницы. Затем для всех страниц он вызывает виртуальную функцию OnApptyn сбрасывает признак модификации, отключая, тем самым, кнопку Apply. Не забудьте, что к DDX-кодууже обращались для обновления переменных-членов на всех страницах свойств, и поэтому переопределять ОпАрр^неда лингв в одном классе страницы свойств.
Что включать в функцию OnApply, остается на ваше усмотрение; один из вариантов — отправить пользовательское сообщение объекту, создавшему это окно свойств. Обработчик сообщения считывает значения переменных-членов для страниц свойств и выполняет соответствующие операции. Окно свойств тем временем остается на экране.
И снова пример ЕХ13А
Теперь мы добавим в ЕХ1 ЗА окно свойств, в котором пользователь сможет изменять характеристики шрифта в поле ввода с форматированием. Конечно, можно было бы взять стандартный диалог CFtmtDialog, но тогда как же Вы научитесь создавать окна свойств? На рисунке 13-5 показано, что получится, если Вы продолжите работу над программой ЕХ13А
Рис. 13-5. Окно свойств в программе ЕХ13А
9 Зак. №787
Если Вы еще не собрали приложение ЕХ1 ЗА, соберите его в соответствии с изложенными ранее инструкциями. Ну, а если Вы уже поработали с программой ЕХ13Л, то выполните сле- дующиедействия.’'
Используя редактор ресурсов, модифицируйте основное меню приложения. Щелкните вкладку ResourceVicwB окне Workspace. Измените меню IDRjMAINFRAMF^oSxbhb в нему меню Format, которое выглядит так:
Присвойте следующие идентификаторы элементам нового меню. .
ЭлементИдентификатор команды
<SDefaultID FORMAT „DEFAULT
^Selectionlb FORMAT „SELECTION
Задайте для этих пунктов меню соответствующие строки подсказки.
. С помощью ClassWizard создайте в классе *вид» обработчики командных сообщений и сообщений обновления командного пользовательского интерфейса. Выберите класс CExI3aVieu>n добавьте следующие функции-члены.
Идентификатор объекта
Сообщение
Имя функции-члена
id_format_default
COMMAND
OnFormatDefault
1D_FORMA Т SELECTION
COMMAND
OnFormatSeiection
ID _ FORMAT. SELECTION
UPDATE_COMMAND_UI
OnUpdateFormatSelection
Используя графический редактор, создайте четыре шаблона страниц свойств.
Эти шаблоны и соответствующие идентификаторы показаны ниже:
IDD_PAGEtIDD_PAGEtIDD_PAGE2IDD_PAGE2
IDD_PAGEt
IDD_PAGEt
IDD_PAGE2
IDD_PAGE2
Назначьте элементам управления в диалоговых окнах (страницах свойств) идентификаторы из приведенной ниже таблицы, У элемента «наборный сметчик» (spin button) установите свойства Set Auto Buddy и Set Buddy Integer, у переключателей (radio buttons) IDCJFONTu IDCjCOLOR— свойство Group. Минимальное значение элемента управления lDC_FONTSlZi§craHOBHTe равным 8, максимальное — 24.
Затем, используя ClassWizard, создайте классы CPagel, CPage2, CPage3 и CPage4, все производные от CPropertyPagePaTMecrme эти классы в файлах Property.h и Property.cpp. Добавьте в них указанные переменные-члены.
Диалоговое окно
Элемент управления
Идентификатор
Тип
Переменная-член '
IDDJAGEI
Первая кнопка-переключатель
1DCJONT
int
m_nPont
1DD_PAGE2
Флажок Bold
ЮС_В01.0
BOOL
isJiBold
W.PAGE2
Флажок Italic
ЮС „ITALIC
BOOL
m_bll«iic
IDD_PAGE2
Флажок Underline
IDC „UNDERLINE
BOOL
m_ bUnderline
1DD_PAGE3
Первая кнопка-переключатель
IDCjCOLOR
int
mjtColor
IDD_PAGE4
Поле ввода
IDCJFONTSKE
int
m_nFontSize
tDD_PAGE4
Наборный счетчик
IDCJPIN1
Наконец, используя ClassWizard, создайте для CPage4 функцию-обработчик OnlmtDialog.
Используя ClassWizard, сгенерируйте класс, производный от CPropertySbeeT.Ha- зовите его CFontSbeet. Код класса сгенерируйте в файлах Property.h и Property.cpp, в которых уже находится код классов страниц свойств. Содержимое этих файлов (сгенерированных ClassWizard) приведено на рисунке 13-6, а кед, который Вы должны добавить, выделен полужирным.
PROPERTY.H
ttif !defined(AFX РВОРЕЯТУ Н CD702F99 7495 11DO_BFDC 00C04FC2A0C2_ INCLUDED ) «define AFX_PR0PERTY_H_CD702F99_7495_11 D0_8FDC_00C04FC2A0C2_,INCLUDED_
ttif _HSC_VER > 1000
ttpragma once
flendif // _MSC_VER > 1000
// Property.h ! заголовочный файл
H’
«define WN_USERAPPLY WM_USEfl + 5
extern CVlew* g_pView;
/////////////////////////////////////////////////////////////////////////////
II Диалоговое окно CPagel
class CPagel : public CPropertyPage
{
DECLARE_DYNCREATE(CPage1>
// Создание
public:
CPageK);
"CPageK);
// Данные диалогового окна
//((AFX_DATA(CPage1)
enum { IDD = IDD_PAGE1 };
Рис. 13-6. Листинги заголовочного файла и файла реализациисм. след. стр.
для классов страницсвойств и окна свойств"
Рис. 13-6. (продолжение)'
intm nFont;.
//}}AFX_DATA-
Н Переопределенные функции
И Переопределенные функции, сгенерированные Classwizard
//{{AFX_VIRTUAL(CPage1>
protected:
virtual void DoDataExchange(CDataExchange* pDX); // поддержка DDX/DDV //}}AFX„VIRTUAL
virtual BOOL OnApplyO;,■
virtual BOOL OnCommand(WPARAM wParam, LPARAM IParam);
// Реализация
protected:
// Сгенерированные функции таблицы сообщений
//{{AFX„MSG(CPage1)ч'
// ПРИМЕЧАНИЕ: здесь Classwizard будет добавлять функции-члены
//HAFXMSG
DECLARE„MESSAGE_MAP(>
///////////////////////////////////////////////////////////////////////////// Ц Диалоговое окно СРаде2
class CPage2 : public CPropertyPage
{
DECLARE_DYNCREATE(CPage2)
// Создание
public:
CPage2();
-CPage2();
// Данные диалогового окна
//{(AFX_DATA(CPage2)■
ewm { IDO = IDD_PAGE2 h
BOOL m_bBold;
BOOL m_bltalic:
BOOL m bllnderline;
//H-afxjmta-
// Переопределенные функции
// Переопределенные функции, сгенерированные Classwizard
//{{AFX_VIRTUAL(CPage2)
protected:
virtual void DoDataExchange(COataExchaige-« pDX);// поддержка DDX/DDV
//}}AFX_VIRTUAL
virtual BOOL OnCommand(WPARAM wParam, LPARAM IParam); .
// Реализация
protected:
H Сгенерированные функции таблицы сообщений
//{{AFX_MSG(CPage2).
// ПРИМЕЧАНИЕ; здесь ClassWizard будет добавлять функции-члены //}}AFXMSG.
DEGLARE_MESSAGE_MAP<).
}:- ■
///////////////////////////////////////////////////////////////////////////// // Диалоговое окно СРадеЗ
class СРадеЗ : public CPropertyPage {
DECLARE_0YNCREATE(CPage3)
// Создание
public:
СРадеЗ О;
'СРадеЗО;
// Данные диалогового окна
//{{AFX ОАТА(СРадеЗ)
enum { TDD = IDD.PAGE3 };
tnt,m_nColor;
//}}AFX_DATA
,// Переопределенные функции
// Переопределенные функции, сгенерированные classwizard //{{AFX_VIRTUAL(СРадеЗ) protected:
virtual void DoDataExchange(CDataExchange* pDX); // поддержка DDX/DDV //}}AFX_VIRTUAL
virtual BOOL OnCcmniand(WPARAM wParam, IPARAM IParam);
// Реализация
protected:
// Сгенерированные функции таблицы сообщений
//{{AFX_MSG(СРадеЗ)
П ПРИМЕЧАНИЕ: здесь Classwizard будет добавлять функции-члены //}}AFX_MSG
DECLARE_MESSAGE_MAP()
/////////////////////////////////^^
// Диалоговое окно СРаде4
class СРаде4 : public CPropertyPaoe
{'
DECLARE. DYNCREATE(CPage4)
// Создание
public:
CPage4();
~CPage4():
// Данные диалогового окна
//{{AFX_DATA(CPage4)
enum { IDD - ID0_PAGE4 };
intm nrontSize:
//}}AFX_DATA
// Переопределенные функции
// Переопределенные функции, сгенерированные CLassWizard
//{(AFX_VIRTUAL(CPage4)
protected:
virtual void DoDataExchange(CDataExchange* pDX): // поддержка DDX/ODV //}}AFX_VIRTUAl
virtual BOOL OnCommand(WPARAM wParam, IPARAM IParam);
см. след. стр.
Рис. 13*0. (продолжение)
/I Реализация protected:.
// Сгенерированные функции таблицы сообщений
//{(AFX_MSG(CPage4)
virtual BOOL OnInltDialog();
//}}AFX MSG DECLARE_MESSAGE W()-
}:
/////////////////////////////////////////////////////////////////////////////
// CFontSheet
class CFontSheet : public CPropertySheet
J DECLARE_DYNAMIC(CFontSheet)
public:
CPagel m_page1:
CPage2 m_page2;
СРадеЗ m_page3;
CPage4 m_page4;
// Создание public!.
CFontSheet(UINT nIDCaption, CWnd* pParentWnd = NULL, UINT ISelectPage = 0);
CFontSheet(LPCTSTR pszCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0);
// Атрибуты■
public:'
// Операции public!
// Переопределенные функции''
// Педеогределенные функции, сгенерированные Classwizard //{(AFX VIRTUAL(CFontSheet)'
//}}AFX_VIRTUAL•''
И Реализация
public:
virtual ~CFontSheet0;
И Сгенерированные функции таблицу сообщений protected:
//{{AFX_MSG(CFontSheet)
// ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять и удалять функции-члены
//}}AFX_MSG
DECLARE MESSAGE МАР()
1:
//////////////////////////////^^
//<{AFX_INSEHT_LOCATIONH
// Microsoft Visual C++ вставит дополнительные объявления
// непосредственно педед предццущей стрсасой
ffendif // !defined(AFX_PROPERTY_H_-CD7O2F99_7495_11DO_8FDC_ODCO4FC2A0C2_INCLUDED_)
PROPERTY.CPP
// Property.cpp : файл реализации
«include "StdAfx.h"
«include "exlSa.h"
«include "Property.h"
ttifdef_DEBUG
«define new DEBUG_NEW'
flundef THIS.FILE'
static char THIS_FILE[] = _FILE_;.
#endif'
CView* g_pView;
///////////////////////////////////////////////////////////////////////////У/
И Страница свойств CPagel
IMPLEMENT, DYNCflEATE( CPagel, CPropertyPage).
CPagel::CPagel() : CPropertyPage(CPage1::IDD)
{ //{(AFX_DATA_INIT(CPage1)
m nFont = -1;
//BAFX DATA INIT
CPagel::"CPagel(}
{
BOOL CPagel:: OnApply( >
I
TRACE("CPagel::0nApply\n");
g_pView->SendHessage(WM_USERAPPLY);
return TRUE;
BOOL CPagel::OnCommand(WPARAM wParam, LPARAM IParam) {
SetModified(TRUE); return CPropertyPage::OnComnand(wParam, IParam);
void CPagel::Do9ataExchange(CDatiExch8nge» pDX)
TRACE("Входим в CPagel::DoDataExchange - Xd\n", pDX->m_bSaveAndValldate);
CPropertyPage::DoDataExchange(pDX):.. .
//HAFX_DATA_MAP(CPage1)•
DOX Radio(pDX, IDC FONT, m nFont);-
//)}AFX DATA MAP•
BEGIN_MESSAGE_MAP(CPagel, CPropertyPage)'" "
//{{AFX_MSG_MAP(CPagel)л .,
// ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять макросы таблицы сообщений .■
//}}AFX MSG MAP,'
ENO_MESSAGE_MAP()
11Н1НН1ПННПП!ННи1ПП1Н111ПП1ПШП1ШНШ/ГППШП?НПП '’
И Обработчики сообщений CPagel, ; ■ f :х <
см. след. стр.
Рис. 13-6. (продолжение)''
//////////////////////////////////W^^ // Страница свойств СРаде2. ■".'
IMPLEMEtfT_DYNCREATE( CPage2. С PropertyPage)
СРаде2::СРаде2() : CPropertyPage(CPage2::IDD)
//{{AFX OATA_INIT(CPage2)
m_bBold = FALSE;
m_bltalic = FALSE;.
m bUnderline = FALSE;
//}}AFX_DATA_INIT
CPage2 : '."CPage2() >
BOOL CPage2::OnCommand(WPAHAM wParam, LPARAM IParam) { SetModified(TRUE);
return CPropertyPage::OnCommand(wParam, IParam);
void CPage2: :DoOataExchange(COataExchange* pEK)
TRACE("Входим в CPage2: :DoDataExchange - W\n", pDX->a_bSaveAndValidate);
CPrope rtyPage::DoDataExchange(pOX);
//{{AFX DATA MAP(CPage2)
DDX Check(pOX, IDC.BOlD, m_bBold);
ODX Check(pDX, IDC ITALIC, m Mtalic);
DOX Check(pDX, IDC_UNDERLINE, ■ bUnderline);
//}}AFX DATA MAP
BEGIN MESSAGE MAP(CPage2, CPropertyPage)
//{{AFX„MSG_HAP(CPage2)
// ПРИМЕЧАНИЕ: здесь ClaaeWizard будет добавлять макросы таблицы сообщений //HAFX MSG_MAP-"-•.
END_MESSAGE_MAP()'
///////////////////////////////W^^
// Обработчики сообщений Фаде2
IHlIllllHIIHnUIIIIIIIIHUnilllHIIIUlinillllllllllllllHUlinillHI // Страница свойств СРадеЗ
IMPLEMENT_DYNCREATE(CPage3, CPropertyPage)
СРадеЗ:;СРадеЗ() : CPropertyPage(CPage3; ЛОО)
‘ //((AFX_DATVNIT(CPage3)
m nColor = -1;
//)>AFX DATA INIT
СРадеЗ::~CPage3()
{
}
BOOL СРадеЗ: :OnComeiand(WPARAM wParam, LPARAM IParam)
{ SetNodified(TRUE);
return CPropertyPage: :OnComteand(wParani, IParam);
)
void СРадеЗ:: DoDataExchange(CDataExchange* pDX)
{■•
TRACE ('Входим в СРадеЗ::DoDataExchange - M\n“, pDX->m_bSaveAndValidate);
CPropertyPage::DoDataExchange(pDX);.
//{{AFX_DATA_MAP(CPage3).
DDX RadioCpDX, IDC COLOR, m nColor);- ■
//)}AFX_DATA_MAP•
BEGIN MESSAGEJIAP (СРадеЗ, CPropertyPage)
//<{AFX_MSG_MAP(CPage3)
// ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять макросы таблицы сообщений //MAFX_MSG_MAP
END_MESSAGE_MAP()
lllttHlltlinillinilillHIIIHinilllllHUfillHinUIIIIHHHIIIIHIIIII
И Обработчики сообщений СРадеЗ
ШШШ1П1ШШШНШШ1ШШШШШШШШПШПНШ1ШШШ
И Страница свойств СРаде4
IMPLEMENT_DYNCREATE(CPage4, CPropertyPage)
СРаде4: :СРаде4() : CPropertyPage(CPage4: ЛОО)
//({AFX_DATA_INIT(CPage4)
m nFontSlze = 0;
//} IAFX DATA INU
CPage4::"CPage4()
BOOL CPage4::OnCoamand(WPARAM wParam, lparam IParam) { SetModified(TRUE);
return CPropertyPage: :0nCOMand(wParam, IParam);
void CPage4: :DoDataExchange(CDataExchange* pDX)
* ТНАСЕСВходим в CPage4::DoDataExchange - ld\n", pDX->m_b8aveAndValidate);
CPropertyPage::DoDataExchange(pDX);
//{{AFX DATA MAP(CPage4)
DDX_Text(pDX, IDC_FONTSIZE. m_nFontSize);
ODV MinHaxInt(pDX, m nFontSlze, 8, 24);
//}Fafx_oatamap■
см. след. стр.см. след. стр.BEGIN NESSAGE_MAP(CPage4, CPropertyPage) //{(AFX_MSG_MAP(CPage4)
см. след. стр.
см. след. стр.
Рис. 13-6. (продолжение)■
//}}AFX_MSG_MAP .. ,■
END.MESSAGE_MAP()'
.1ШШ1Ш1Ш1НН11Н11Н1НШ1П1Ш11НШ11111Ш1Ш1НШП
// Обработчики сообщений СРаде4■
BOOL CPage4::0nInit0ialog() <,,.
CPropertyPage::OnInitDialog();' '
((CSpinButtonCtrl*) GetDlgIte«(IDC_SPIN1))->SetRange(8, 24);
return TRUE; // Вернуть TRUE, если фокус не установлен на элемент управления // ИСКЛЮЧЕНИЕ: страницы свойств OCX должны возвращать FALSE
П1Ш1Ш1ШШШН1ШШПШ1НШШШШи1111Ш^1НШШШ1Ш Ц CFontSheet..
IMPLEMENT „DYNAMIC(CFontSneet, CPropertySheet)
CFontSheet::CFontSheet(UINT nIDCaption, CWnd« pParentWnd, MINT iselectPage) :CPropertySheet(nIDCaption, pParentWnd, iselectPage)
{ }
CFontSheet::CFontSheet(LPCTSTR pszCaption, CWnd* pParentWnd. UINT iselectPage) :CPropertySheet(ps2Caption, pParentWnd, iSelectBage)
AddPage(&m_page1):
AddPagej&m_page2);
AddPage(6m_paga3);
AddPage(Am_page4);
CFontSheet::"CFontSheet() }
BEGIN MESSAGE MAP(CFontSheet, CPropertySheet) //{{AFX_MSG_MAP(CFontSheet).
// ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять и удалять макросы таблицы сообщений //TTARCMSG МАР‘.
END_MESSAGE_MAP()
1пЪ1Ш1НШ11Ш1ПШ1ШШ1Ш111ШШ1ПШи11Ш11Ш1ШШШ111
Ц Обработчики сообщений CFontSheet
Добавьте две переменные и прототипы двух функций в класс CExl^aView. При использовании ClassView директива *includejtiia Property.h будет вставлена автоматически, private:' _'
CFontSheet m_sh;:
BOOL m_bDefault; // TRUE - формат по умолчанию, FALSE - для выделения
Теперь добавьте прототип закрытой функции Format:
void Format(CHAflF0flMAT4 cf);, .
а перед макросом DECLARE_MESSAGE_MAP— прототип защищенной функции OnIJserAppty-. afx.neg IflESULT OnUserApply(WPARAM wParae, LPARAM IParam);
Отредактируйте код в файле. exl3aView,cpp. Создайте обработчик пользовательского сообщения WM_USERAPPLY, приведенный ниже:
ON_MESSAGE(WM_USEHAPPLY, OnUserApply)
Добавьте в функцию OnCreaie прямо перед операторам return 0; такие строки:
CHARFORMAT cf;
Format(cf);
m_rich.SetDefaultCharFormat(cf);
Модифицируйте конструктор объекта *ВИД*, чтобы установить значения по умолчанию для переменных-членов окна свойств:
CEx12aView::CEx13aView() : m_sh("")•'
г.
f m_sh.ie_page1.m_nFont = 0;•■-
m_8h.m_page2,m_b8old = FALSE;
m_8h.m_page2.m_bltallc = FALSE;
m_ah.^_page2.m_bUnderllne = FALSE;
m_ah.m_page3.m_nColor ■ 0;
m_ah.ie_page4.m_nFontSize = 12;
gZpVlew = this;
m_bDef ault = TRUE;
Отредактируйте обработчики команд форматирования:
void CEx13aView: :OnFormatDefault()
M_sh.SatTitle("Формат по умолчании");
m_bDefault = TRUE;
w_sh.DoModal();
void CEx13aView::OnFormatSel0Ctlon() {-
w_sh.SetTitla(“Формат выделенного текста");
m_bDefault = FALSE;
m sh.DoModalQ; }
void CEx13aView::0nUpdateForfflatSelection(CCindUI« pCmdUI) {
long nStart, nEnd;•
m_rich.GetSal(nStart, nEnd); pCmdUI->Enable(nStart != nEnd);'
}
Добавьте обработчик пользовательского сообщения WM_USERAPPLY:
LRESULT CEx13aView::OnUserApply(WPARAM wParae, LPARAM IParam) <v '" - " '
TRACE("CEx12aView::0nUserApply~wPaTae = #x\n", wParea);■
CHARFORMAT Cf;
Format(cf);.
if (n.bDefault) {.r
m_rich.SetDefaultCharFormat(cf);‘'
else { m_rlch.SetSelectionCharFonnat(cf);
return 0;,.
Вставьте, как показано ниже, вспомогательную функцию Pdhfnat, чтобы инициализировать структуру СНАЙЮЙМАТв соответствии со значениямипеременных-членов для окна свойств;
void CEx13aView:;Farmat(CHARFORMAT& cf)
cf.cbSize = aizeof(CHARFORMAT);
cf.dwMask = CFM_BOLD I CFN COLOR ! CFH_FACE ! CFHITALIC I CFM_SIZE I CFM_UNDERLINE;
cf.dwEffacts = (m_sh.m_page2.m_b8old ? CFE_8OLD : 0} !
(m3sh.mZpage2.m_Mtalic ? CFE_ITALIC : 0) !
(m2sh.e_page2.m_bUnderline ? CFE_UNOERLINE : 0);
cf.yHeight = m_sh.ni_page4.m_nFontSize * 20;
switch(m_sh.m_page3.m_nColor) {
case -1: case 0: cf.crTextColor = RG0(O, 0, 0); break;: case 1: cf.crTextColor = RGB(255. 0, 0); break; case 2: cf.crTextColor = RGB(O, 255, 0); break;
switch(ie_sh.m_page1.m_nFont) {-
case -1: case 0: strcpy(cf.szFaceName, "Times New Roman"); break; case 1; atropy(cf. szFaceName, "Arial"); break;.
case 2: strcpy(cf.szFaceName, "Courier New");■
break; } cf.bCharSet = 0; cf.bPitchAndFamily - 0;
Соберите и протестируйте усовершенствованное приложение EX 13А. Введите какой-нибудь текст и вызовите из меню Format команду Default Выбирая страницы в окне свойств и щелкая кнопку Apply, понаблюдайте за сообщениями, выводимыми при помощи TRACE» окно Debug. Попробуйте выделить какой-нибудь текст и отформатировать выделенный фрагмент.
Обработка кнопки Apply
Васможетудивитьто, какой способ обработки кнопки Apply используют классы окна свойств. Переопределенная виртуальная функция OnCommand»o всех классах страниц свойств активизирует кнопку Apply всякий раз, когда какой-либо элемент управления посылает сообщение данной странице. Это отлично срабатывает для страниц 1 -3 в примере EX 1 ЗА, но не для страницы 4, где OrcCowraes/ief вызывается в процессе начального обмена данными между элементом управления «наборный счетчик» и его полем ввода.
Переопределенная виртуальная функция OnApply» классе CPagel посылает объекту «вид» пользовательское сообщение. Поиск этого объекта осуществляется довольно грубым способом: через глобальную переменную, установленную классом «вид». Лучше было бы передавать указатель на объект «вид» конструктору окна свойств, а затем конструктору страницы свойств.
Класс «вид» вызывает функцию DoModaloKHa свойств как для форматирования по умолчанию, так и для форматирования выделенного фрагмента. Чтобы указать режим, он уста-
навливает признак m_bDefaultBpсверять возвращаемое значение DaModatHe нужно, так как пользовательское сообщение отправляется при щелчке как кнопки ОК, так и кнопки Apply. Если пользователь выбирает кнопку Cancel, сообщение не посылается.
Класс СМепи
До этого момента каркас приложений и редактор меню отделяли Вас от класса СМепи. Объект этого класса может представлять любое меню Windows, в том числе меню верхнего уровня и связанные с ним раскрывающиеся меню. Чаще всего, когда вызывается функция Create для окна-рамки и объект СМепи явным образом не создастся, ресурс меню подсоединяется прямо к этому окну. Функция-член GetMenu класса СТ^и^возвращает временный указатель на СМепи'. Получив этотуказатель, Вы получаете свободный доступ к меню и можете изменять его.
Допустим, Бы хотите переключать меню после запуска приложения. Исходное меню всегда имеет идентификатор IDR_MAINFRAMEILcnH Вам нужно второе меню, Вы создаете его в редакторе меню и определяете требуемый идентификатор. Затем в своей программе Вы создаете объект СМепи, загружаете меню из ресурса с помощью СМепи: iLoadMenwa вызываете CWnd;:SetMenu,чтобы присоединить новое меню к окну-рамке. После этого вызываете метод Detach, чтобы отсоединить от объекта описатель HMENU-,тотда созданное Вами меню не будет уничтожено, когда объект СМепи выйдет из области видимости.
Можно определить меню в ресурсе и потом модифицировать его в период выполнения программы. При необходимости в период выполнения можно создать и все меню целиком, не пользуясь ресурсами. В любом случае для этого пригодятся такие функции-члены класса СМепи, как ModiJyMenulnsertMenu^ DeleteMenu. Каждая из них работает с отдельным элементом меню, определяемому по идентификатору или по индексу его позиции в меню.
Объект меню в действительности состоит из вложенной структуры подменю. Функция- член GetSubMenu возвращает указатель на объект СМепи, представляющий раскрывающееся меню в главном объекте СМепи. Функция СМепи?:СеГМепи5Ь1П^озвращает текст в элементе меню, указанному либо по индексу, либо по идентификатору команды. В последнем случае выполняется поиск по всей системе меню, включая вложенные.
Создание контекстных меню
Контекстные меню (floating pop-up menu) — одна из последних новаций в пользовательском интерфейсе. Щелчок правой кнопкой мыши открывает меню с командами, которые относятся к текущему контексту. Такие меню легко создать с помощью графического редактора и MFC-функции СМепи-ТгаскРорирМстиДля. этого выполните следующее. . ■ - ■ .
Откройте редактор меню и добавьте в файл ресурсов своего проекта новое пустое меню. 2. Введите какое-нибудь имя для крайнего слева элемента верхнего уровня и добавьте коман
ды контекстного меню в получившееся раскрывающееся меню. .t
Вставьте обработчик сообщения WM_CONTEXTMENU в класс *внд» или в класс другого окна, получающего сообщения от кнопок мыши, и запрограммируйте его следующим образом:
void CNyView::OnContextMenj(CWnd* pWnd, CPoint point).
CMenu menu;'
menu. LoadHenu(IDRJfYFLOAnNG>OU);''
menu.GetSubMenu(oj->TrackPopupMenu(TPH_LEFTALIGN I■
TMP_RIGHTBUTTON, point.x, point.y, this):•■-■
Команды контекстного меню можно сопоставить с обработчиками при помощи ClassWizard тем же способом, что и команды в меню окна-рамки.
Точнее, указатель на временный объект СМепи. — Прим., перее.
Расширенная обработка команд
Кроме макроса таблицы сообщений ON_COMMANDp библиотеке MFC есть его расширенный вариант ON_COMMAND_EXOh обеспечивает две возможности, которые не поддерживаются для обычного командного сообщения: параметр обработчика, задающий идентификатор команды, и возможность отказаться от обработки команды в период выполнения, отослав ее следующему объекту на маршруте доставки команд. Если расширенный обработчик команды возвращает TRUE, команда считается обработанной, а если FALSE — каркас приложений ищет следующий обработчик.
Параметр — идентификатор команды полезен, если Вы хотите обрабатывать одной функций несколько связанных друг с другом командных сообщений. Наверняка Вы найдете способы применения этой возможности.
ClassWizard в работе с расширенными обработчиками команд не поможет, так что придется кодировать их самостоятельно (вне скобок/№Х_М55_М4Р)Д опустим, IDM_ZOOM_1 и IDM_ZOOM_2— идентификаторы взаимосвязанных команд, определенные в файле iesource.h. Тогда для обработки обоих сообщений одной функцией OnZoom надо сделать следующее: BEGIN MESSAGE MAP(CMyView, CView)
ON COMMAND EX(IDM ZOOM 1, OnZoom)
ON COMMAND EX(IDM_ZOOM 2, OnZoom)
END_MESSAGE_MAP()
BOOL CMyView::OnZoom(DINT nID)
{
if (nID == IDM_ZOOM_1) (
// код, специфичный для первой команда
)
else {
// код, специфичный для второй команды
}
// код, общий для обеих команд
return TRUE; // обработка команды завершена }
А вот прототип функции:
afx_msg BOOL OnZoom(UINT nID);
Есть и другие макросы таблицы сообщений MFC, полезные при обработке групп команд. Такая обработка может понадобиться в приложениях, использующих динамические меню. Эти макросы включают:
ON_COMMAND RANGE.
ON COMMAND^EX RANGE
ON_UPDATE_COMMAND_ UI_RANGE
Если бы значения IDMZOOMln IDM_ZOOM_2 были последовательными, то таблицу сообщений СМрУгеа/можно было бы переписать так:
SEGIN_MESSAGE MAP(CMyView, CView)
ON COMMAND EX RANGE(IOM ZOOM 1, IDM ZOOM 2, OnZoom)
END_MESSAGE_NAP()
Теперь OnZoom будет вызываться при выборе обоих элементов меню; обработчик может определить выбранную команду по значению целочисленного параметра.
ГЛАВАГЛАВА1 41 4
ГЛАВА
ГЛАВА
1 4
1 4
Панели инструментов и строки состояния
Ьо всех приведенных до этого примерах программ на Visual C++ мы использовали панели инструментов и строки состояния. Арр Wizard генерирует код, инициализирующий эти элементы каркаса приложений, если только явно не отказаться от предлагаемых по умолчанию параметров Dockable Toolbar и Initial Status Ваг, Созданная по умолчанию панель инструментов содержит графические эквиваленты многих стандартных элементов меню, формируемых каркасом приложений; в строке состояния по умолчанию отображаются подсказки по командам меню и индикаторы состояния клавиатуры: CAPS, NUM и SCRL
В этой главе Вы узнаете, как настроить для своей программы панель инструментов и строку состояния, научитесь добавлять на панель инструментов собственные графические кнопки и управлять их отображением. И еще Вы научитесь отключать вывод подсказок и индикаторов клавиатуры в строке состояния, что позволит вашей программе задействовать эту строку в своих целях.
Панели элементовуправления и каркас приложений
Панель инструментов (toolbar) — это объект класса CToolBar, а строка состояния (status bar) — объект класса CStatusBar. Оба класса производные от CControiBar, а тот, в свою очередь — от CWnd. Класс CControlBar поддерживает окна панелей элементов управления, размещаемых в окне-рамке. Окна этих панелей автоматически изменяют свои размеры и положение при масштабировании и перемещении окна-рамки. Каркас приложений создаетиуда- ляет эти объекты и нх окна. AppWizard генерирует код панелей элементов управления для производного класса окна-рамки, хранящегося в файлах MainFim.cpp и MainFrm.h. В типичном SDI-приложении объект СТЬойВйг находится вверху клиентской области CMainFrame, а объект CStatusBar — внизу. Между ними находится окно представления.
Начиная с версии 4-0 MFC-библиотеки, панель инструментов формируется на основе стандартного элемента управления «панель инструментов» (toolbar common control), впервые появившегося в Windows 95, и поэтому поддерживает стыковку (docking) с окном-рамкой. Хотя программный интерфейс остался практически таким же, что и в предыдущих версиях MFC, работать с изображениями кнопок стало легче, поскольку редактор ресурсов поддерживает соответствующий специальный тип ресурсов. Применявшиеся ранее глобальные массивы buttons больше не требуются.
Если Арр Wizard сгенерировал код панелей элементов управления для данного приложения, пользователь может включать и отключать показ этих панелей командами из меню View. При отключении панель исчезает, и пересчитывается размер окна представления. Несмотря на некоторые обшив (описанные выше) свойства, объекты «панель инструментов» и «строка состояния» независимы друг от друга и имеют разные характеристики.
В Visual C++ 6.0 введена новая панель инструментов под названием rebar. Она основана на элементах управления, входящих в состав Microsoft Internet Explorer 4.0, и предоставляет сколъзягций (sliding) СТИЛЬ оформления, который можно увидеть в Internet Explorer. Мы познакомимся с данной панелью чуть позже.
Панель инструментов
Панель инструментов — строка горизонтально (или вертикально) расположенных графических кнопок, иногда объединенных в группы. Разбивка на группы осуществляется программным путем, в соответствии с назначением кнопок. Изображения кнопок хранятся в общем растровом изображении, вход ящем в ресурсы приложения. Если щелкнуть кнопку, она пошлет командное сообщение, подобно меню и быстрым клавишам. Для обновления состояния кнопок служат обработчики обновления командного пользовательского интерфейса, вызываемые каркасом приложений для изменения внешнего вида кнопок.
Растровое изображение панели инструментов
Может показаться, что у каждой кнопки на панели инструментов свое растровое изображение, но на самом деле растровое изображение для всей панели одно. В нем каждой кнопке выделяется ячейка высотой 15 и шириной 16 пикселов. Каркас приложений обеспечивает прорисовку границ кнопок и изменяет их вместе с цветам фона кнопки, чтобы отобразить ее текущее состояние. На рисунке 14-1 показано растровое изображение и соответствующая ему панель инструментов.
Рио. 14-1. Растровое изображением соответствующая панель инструментов
Растровое изображение панели инструментов хранится в файле Toolbar.bmp в подкаталоге RES приложения. В RC-файле оно идентифицируется как IDR_MA1NFRAME9to растровое изображение напрямую не модифицируют, вместо этого пользуются специальными средствами графического редактора.
Состояниякнопок
Любая кнопка может находиться в следующих состояниях.
Состояние
Описание
0
TBSTATE_CHECKED tbstate_enabled
Нормальное, не «нажата»
Помечена («нажата»)
Доступна для использования: если это состояние не установлено, кнопка недоступна и изображение на ней блеклое
TBSTATEJCDDEN
TESTATE„INDETERMINATE
TBSTATE_PRESSED
TESTATE „WRAP
Невидима ■
Изображается блеклой
Выбрана («нажата») мышью
Кнопка является последней в строке
Кнопка может быть либо командной («на жата»/«отжата») .либо флажком ^установлен/сброшен). На стандартной ПйНСЛИ инструментов, формируемой каркасам приложений, все кнопки — командные.
Панель инструментов и команд ные сообщения
Когда пользователь щелкает кнопку на панели инструментов, генерируется командное сооб- шение. Оно доставляется подобно командным сообщениям от меню, рассмотренным в главе 1).
Чаще всего кнопка на панели инструментов дублирует команду меню. Например, кнопка с изображением дискеты на стандартной панели инструментов (формируемой каркасом приложений) эквивалентна команде Save из меню File. Обе генерируют команду IDFILESAVEA. объекту, получившему командное сообщение, безразлично, сгенерировано оно щелчком кнопки на панели инструментов или выбором команды из меню.
Однако у кнопки на панели инструментов может и не быть эквивалента в меню. В этом случае рекомендуется определить для кнопки быструю клавишу, чтобы пользователь мог активизировать эту команду с клавиатуры или через Windows-макрос. Независимо от того, соответствуют кнопки панели инструментов командам меню или нет, используя ClassWizard, можно определить обработчики командных сообщений и сообщений обновления командного интерфейса.■
С панелью инструментов связан ресурс растрового изображения и сопутствующий ресурс TOOLBARS RC-файле, который определяет команды меню, связанные с кнопками. И растровое изображение, и ресурс TOOLBAR имеют одинаковый идентификатор — обычно это IDR_MAINFRAMEBar содержимое ресурса TOOLBAR, сгенерирированное AppWizard:
IDRJMAINFRAME TOOLBAR DISCARDABLE 16, 15 BEGIN
BUTTON ID FILE NEW
BUTTON ID FILE OPEN
BUfTONlD.FlLE SAVE.
SEPARATOR
BUTTON ID EDIT CUT
BUTTONID_EDIT_COPY
BUTTON ID EDIT PASTE
SEPARATOR
BUTTON ID FILE PRINT
BUTTONID_APP_A8OUT
END,
Константы SEPARATOR позволяют отделять группы кнопокдруг от друга. Если число кнопок в растровом изображении панели инструментов превосходит количество элементов в ресурсе (исключая разделители), лишние кнопки не показываются.
Редактируя панель инструментов в редакторе ресурсов, Вы изменяете и ресурс растрового изображения, и ресурс TOOLBAR. Чтобы отредактировать свойства кнопки, включая идентификатор, выберите ее изображение, а затем дважды щелкните левую панель.
Обновление пользовательского интерфейса для панелей инструментов
Как Вы помните из главы 13, обработчики сообщений обновления пользовательского интерфейса применяются, чтобы отключать команды меню или помечать их галочками. Эти же обработчики пригодны и для кнопок на панели инструментов. Если подобный обработчик вызывает функцию-член CCmdUI;;Enab!ec параметром Е4£5£,соответствующая кнопка отключается (становится блеклой) и больше не реагирует на щелчки мышью.
Функция CCmdUI;;SetCbecIactaB$n: рядом с командой меню галочку, а на панели инструментов реализует кнопку со свойствами флажка. После вызова SetCheck с параметром 1 кнопка ♦залипает» в нажатом состоянии, а с параметрам 0 — возвращается в исходное, «отжатое* состояние.
ПРИМЕЧАНИЕ Если параметр функции SetCheck равен 2, кнопка устанавливается в неопределенное состояние. В нем она выглядит как отключенная, но по-прежнему активна, и ее цвет несколько ярче.
Обработчики сообщений обновления пользовательского интерфейса для команд меню вызываются только при отрисовке элементов соответствующего меню. Но панель инструментов постоянно присутствует на экране — спрашивается, когда же вызывать обработчики обновления интерфейса? Чтобы состояние кнопок можно было постоянно обновлять, обработ-
ЧИКИ вызываются в моменты простоя приложения. Если для элемента меню и кнопки на панели инструментов используется один и тот же обработчик, он вызывается и в момент простоя, и при открытии меню,.
Всплывающие подсказки
Всплывающие подсказки (ToolTips) встречаются в разных Windows-приложениях, в том чис ■ ле и в Visual C++, Если поместить курсор мыши на кнопку панели инструментов и оставить его там, то через определенный промежуток времени радом с кнопкой появится маленькое окошко с текстом. В главе 13 мы уже говорили, что с элементами меню можно ассоциировать строки подсказки, которым отвечают элементы строкового ресурса с соответствующими идентификаторами, Чтобы создать всплывающую подсказку, просто добавьте ее в копен подсказки для меню, начав текст с символа новой строки \п. Редактор ресурсов позволяет модифицировать строку подсказки при редактировании изображений кнопок на панели инструментов. Для этого просто дважды щелкните левую панель.
Поиск основного окна-рамки
Объекты панели инструментов и строки состояния, с которыми Вам предстоит работать, присоединяются к основному окну-рамке приложения, а не к окну представления, Как найти основное окно-рамку? В SDI-приложении можно воспользоваться функцией CWnd::GetParent- Ртате. К сожалению, она не будет работать в MDI-приложепии, так как в этом случае родительской рамкой будет дочернее окно-рамка, а не основное,
Чтобы класс «вид» работал как в SDI-, так и в MDI-ириложении, нужно искать основное окно-рамку через объект «приложение». Указатель на этот объект возвращает глобальная 'функцияAjxGetApp^epes такой указатель можно обратиться и к элементу данных m_pMain- SWmiacca CWinAppx. В MDI-приложении код, устанавливающий m_pMainWnd, генерирует AppWizard, но в SDI-приложении эта переменная-член определяется каркасом приложений при формировании объекта «вид». Инициализировав mJiMainWnd,M.oxHO использовать его в классе «вид» для доступа к панели инструментов в окне-рамке, например, таю
CMainFrane* pF^ame = (CMainFrame*) AfxGeтApp()->m_pMainWnd•.
CToolBar* pToclBa' = &pFrane->m_wndToolBar;
ПРИМЕЧАНИЕ Тип m_pMainWnd следует преобразовать из CFrameWnd* в CMainFrame*, так как m_wndToolBar — переменная-член именно этого производного класса Вам также придется сделать m_wndToo!Bar открытой переменной или объявить свой класс дружественным CMainFrame.
ПРИМЕЧАНИЕ В SDI-приложении значение m_pMainWndeme не установлено при вызове обработчика сообщения OnCreate класса «вид*. Для доступа к основному окну-рамке в OnCreate нужна функция GetParentFrame.
Пример ЕХ14А
В этом примере мы заменим стандартные кнопки Cut, Сору и Paste тремя специальными - они будут управлять рисованием в окне представления. Кроме того, мы создадим меню Draw с тремя элементами, соответствующими этим кнопкам.
Элемент менюНазначение
CircleРисует крут в окне представления-
SquareРисует квадрат в окне представления
PatternВключает и отключает наклонную штриховку новых кругов и квадратов "
Проще воспользоваться МРС-функциейЛ/хСейУдапТГяйС— Прим.перев.
Меню и панель инструментов предлагают пользователю выбор: рисовать круги или квадраты. После отрисовки круга отключаются команда Circle и соответствующая кнопка на панели инструментов, а после отрисовки квадрата — команда и кнопка Square. Если включена штриховка, команда Pattern в меню Draw помечается галочкой, а одноименная кнопка на
панели инструментов переводится в «нажатое» состояние.
На рисунке 14-2 показана программа в действии. Пользователь только что нарисовал заш
трихованный квадрат. Обратите внимание на состояние трех кнопок, связанных с рисованием.
Рис. 14-2. Программа ЕХ14А в действииРис. 14-2. Программа ЕХ14А в действии
Рис. 14-2. Программа ЕХ14А в действии
Рис. 14-2. Программа ЕХ14А в действии
Пример ЕХ14А знакомит Вас с возможностями редактора ресурсов при работе с панелями инструментов. Кодировать на C++ придется совсем чуть-чуть. Итак, приступим.
Используя AppWizard, создайте проект vcpp32\ex!4a\ex!4a. Примите параметры по умолчанию, но выберите Single Document Interface и откажитесь от Printing And Print Preview. Параметры и имена классов по умолчанию показаны на рисунке ниже:
С помощью редактора ресурсов, измените основное меню приложения. Выберите ех14а.гс и дважды щелкните IDR_MAlNFRAMEa разделе Menu. Отредактируйте ресурс меню IDR_MA1NFRAME^k>6t3. тгозг^чигь такое же меню, как на рисунке на стр. 242 (это означает, что надо модифицировать меню Edit):
Назначьте новым элементам меню следующие идентификаторы команд.
Меню
Элемент меню
Идентификатор команды
Draw
Circle
ID_DBAW_CIRCLE
Draw
Square
ID_DRAW_SQUARE
Draw
Pattern
ID_DRAW_PA TTERN
В диалоговом окне Menu Item Properties введите нужные строки подсказки и текст всплывающих подсказок (после символа \п). Для IDDRAWjCIRCL&TpaKa может быть такой: «Нарисовать круг\пКруг».
Используя редактор ресурсов, измените панель инструментов. Отредактируйте ресурс растрового изображения IDRJWALWRAMBstoSh он выглядел как на рисунке ниже.
W С. \vcpp32\ex14а\ех14з гс IР Г" ГТ j
Сотрите изображения для Cut, Сору и Paste (четвертую, пятую и шестую слева) и замените их новыми изображениями. Редактор панелей инструментов интуитивно понятен: кнопки просто перемещаются мышью. Клавиша Del стирает пикселы на изображении кнопок. Чтобы удалить кнопку, переместите ее за пределы панели инструментов. Попробуйте поработать с инструментами для рисования прямоугольников и эллипсов, поэкспериментируйте с линиями разной толщины. Закончив, сохраните на всякий случай файл ресурсов. Присвойте новым кнопкам идентификаторы//>_D/MW_C/ACZi!,.ffi\_DRAWjSQUARii IDJDRAWjPATTERN
Используя Classwizard, создайте в классе «вид» обработчики сообщений. Добавьте обработчики следующих сообщений и оставьте имена функций по умолчанию.
ИдентификаторобьектаTini сообщения
Имя функции
№_DRAW_CIRCLECOMMAND
ID DRAW CIRCLEUPDATE_COMMAND_UI
ID DRAW PA TTERNCOMMAND
IDDRAWРАП ERNTJPDATE_COMMAND_LI
ID _DRAW SQUARECOMMAND
ID DRAW SQUAREUPDATE COMMANDUt
OnDrauCircie OnUpdateDrawCircte OnD ratvPattem OnUpdateDrawPattern OnDrawSquare
On UpdateDrawSquure
Добавьте в класс CExl4aViewTpH персменные-члена. Отредактируйте файл сх14а- Vicw.h вручную или воспользуйтесь ClassView.
private:
CRect m_rect;
BOOL m_bCircle;
BOOL m_bPattern;
Отредактируйте файл ex!4aView.cpp. Конструктор CExl4aViewnpocio инициализирует переменные-члены класса. Добавьте код, выделенный полужирным:
CExl4aView: :CExl4aView() : m_rect(0, 0, 100, 100)
m_bCircle = TRUE;
m_bPattern = FALSE;
Функция OnDraw в зависимости от значения флажка m_bCircle рисует эллипс или квадрат. По значению mJiPattem выбирается либо белая кисть, либо кисть с наклонной штриховкой.
void CEx14aView::0nDraw(0DC« pDC)
CBrush brush(HS_BDIAGONAL, OL); // кисть с наклонной штриховкой
if (m_bPattern) {
pDC->SelectObject(&brush);
else {
pDC->SelectStockObject(WHITE-BRUSH);
rf (m_bClrcle) {
pDC->Ellipse(m_rect);
else {
pDC->Rectangle(m_rect);
}
pDC->SelectStockObject(WHITE BRUSH); // отключить кисть, если она подключена
1
OnDrawCircle обрабатывает командное сообщение IDJDRAW_CIRCLE,a OnDrawSguare - сообщение ID_DRA\V_SQUAREO6e функции перемещают прямоугольную область рисования вниз и вправо, потом объявляют ее недействительной, и она перерисовывается функцией OnDraw. В результате окружности и квадраты поочередно выводятся в окно по диагонали. Вывод на дисплей не буферизуется, так что нарисованные фигуры не перерисовываются, когда окно перекрывается другим или минимизируется.
void CEx14aView: lOntHawCircleO
* OCircle = TRUE:
m_rect += CPoint(25, 25);
InvalidateRect(m_rect);
void CEx14aView; :Or.DrawSquare()
. { m_bCircle = FALSE;
m_rect += CPoint(25, 25);
InvalidateRect(m rect);
}
Следующие два обработчика, обновляющие пользовательский интерфейс, поочередно включают и отключают в меню команды Circle и Square и соответствующие кнопки. Б любой момент может быть доступна только одна из двух возможностей.
void CEx14aView; :OnUpdateDrawCircle(CCmdUI« pCmdlll)
pCmdUI->Enable(!m_bCircle);
void CEx14aView: :OnUpdateDrawSquare(CCmdUI* pCmdUI)
{'
pCmdUI->Enable(m_bCircle);
Функция OnDrawPatrem меняет состояние флажка mJbPattern на противоположное:
void CExUaView: :OnDrawPattern()
m_bPattern '= 1;.
Обработчик OnUpdateDrawPattern обновляет элемент меню и кнопку Pattern в соответствии с состоянием флажка mJbPattern-.кнопка либо «залипает», либо возвращается в исходное состояние, а радом с командой меню появляется или исчезает галочка.
void CExUaView: :OnUpdateDrawPattern(CCmcUI* pCmdUI)
pCmdUI->SetCheck(m_bPattern);
}
Соберите и протестируйте приложение ЕХ14А. Обратите внимание на поведение кнопок на панели инструментов. Поэкспериментируйте с элементами меню и обратите внимание, что они меняют свое состояние в соответствии с состоянием приложения. Понаблюдайте также за появлением всплывающей подсказки, когда курсор мыши «замирает» над одной из кнопок на панели инструментов.
Строка состояния
Окно строки состояния не принимает ввод от пользователя и не генерирует командных сообщений. Его задача — просто показывать под управлением программы текст в соответствующих секциях. Страка состояния поддерживает два типа текстовых секций: строку сообщений и индикаторы. Чтобы использовать строку состояния для вывода информации, специфичной для конкретного приложения, сначала отключите стандартную строку состояния, которая отображает подсказки по элементам меню и сообщает статус клавиатуры.
Определение секций в строке состояния
Секции в строке состояния определяются статическим массивом Indicators, который Арр- Wizard создает в файле MalnFrm.cpp. Константа 2О_5ЕЙ4/МТО2йадает секцию для строки сообщений, а другие константы служат идентификаторами строковых ресурсов и определяют секции индикаторов. На рисунке 14-3 изображена взаимосвязь массива indicators и стандартной строки состояния.
Функция-член CStatusBar::SetIndicators^H3HBaeM.aa в производном классе окна-рамки приложения, приводит строку состояния в соответствие с содержимым массива indicators.
Строка сообщений
В этой секции показывается строка, задаваемая (динамически) программой. Чтобы определить выводимый текст, сначала получите доступ к объекту строки состояния, после чего вызовите функцию-член CStatusBar::SetPaneTextjiepe®XB ей индекс секции. Индексы начинаются с О; нулевая секция — крайняя слева, секция 1 размещается правее и т. д.
Приведенный ниже фрагмент кода входит в функцию-член класса «вид». Обратите внимание: здесь приходится сначала подниматься на уровень приложения, а потом возвращаться к основному окну-рамке:
Рис. 14-3. Строка состояния и массив indicators
CMainFrame* pFrame = (CMainFrame*) AfxGetApp( )->m_pMainWnd;
CStatusBar* pStatus = &pFramc->m_wndStarusBar;
pStatus->SetPaneText(O, "строка сообщения для первой секции");
Обычно длина секции сообщений составляет ровно четверть ширины экрана. Однако у первой секции (индекс 0) длина переменная; она не менее четверти ширины экрана и может быть увеличена, если в строке состояния есть место.
Индикатор состояния
Секция индикатора состояния связана с единственной строкой ресурса, которая показывается или скрывается в соответствии с логикой функции-обработчика. Индикатор обозначается идентификатором строкового ресурса, и тот же идентификатор применяется для распределения сообщений обновления интерфейса. Индикатор Caps Lock обрабатывается в классе окна-рамки при помощи приведенных ниже элемента таблицы сообщений и функции- обработчика (функция Enable включает индикатор, если активен режим Caps Lock):
pN.UPDATE.COMMAMILUKIDJNDICATOR-CAPS, OrUpdateKeyCapsLock)
void CMainFrame::OnUpdateKeyC£psLock(CCmdUI' pCmdUI)•
pCmdUI->Enable(: :GetKeyStat6(VK_CAPITAL) & 1);
}■
Функции, обновляющие пользовательский интерфейс, вызываются в цикле простоя приложения, поэтому строка состояния обновляется всякий раз, когда приложение получает сообщения. Длина секции индикатора равна длине соответствующей строки из ресурса.
Управление строкой состояния
Стандартной строке состояния присваивается идентификатор дочернего окнаЛЬ‘Х^1О№_5ТА- TL7S_BAR.Именно его каркас приложений ищет для вывода подсказки по элементам меню. Обработчики сообщений обновления пользовательского интерфейса используют три идентификатора строковых ресурсов для индикаторов состояния клавиатуры в базовом классе окна-рамки: lE>_INDlCATOR_GAP$^D-JNDICATOR_NUMta. ID_INDICATOR_SCRL. Чтобы самому управлять строкой состояния, нужно применить другой идентификатор дочернего окна и другие константы для индикаторов.
ПРИМЕЧАНИЕ Изменять идентификатор дочернего окна строки состояния имеет смысл, только если Вы хотите предотвратить вывод каркасом приложений подсказок в секцию 0. Если подсказки Вас устраивают, можете не читать приведенные далее инструкции.
Идентификатор для окна строки состояния назначается вызовом CSlatusBdf: :Сгеа1ев функции-члене OnCreate производного класса окна-рамки. Эта функция содержится в файле
MainFrm.cpp, генерируемом AppWizard. Идентификатор окна — третий параметр функции Create и по умолчанию равен AFX_IDW_STATUS_BAR.
Чтобы назначить свой идентификатор, замените вызов
m_wndStatusBar.Create(this);
на'
n_wr.dStatus0ar.Create(this, WS.CHILD ! WS.VISIBLE : CHRS_BOTTOM, ID_MY_STATUS_BAR);
Конечно, нужно определить и константу 1D_MY_STATUS_BA& файле resource.h, применяя редактор символов.
Но мы забыли кое-что. Стандартное меню View, формируемое каркасом приложений, позволяет включать и отключать показ строки состояния. Эта логика реализуется кодом, использующим идентификатор omiAFXJDW_STATUS_BAB$nx$pH& тоже придется изменить. В своем производном классе окна-рамки напишите элементы таблицы сообщений и обработчики для команды ID_V1EW_STATUS_BAR сообщений, связанных с обновлением пользовательского интерфейса. ID_V1EWSTATUS_BAR— это идентификатор элемента меню Status Ваг. Обработчики в производном классе переопределяют стандартные обработчики из базового класса CFrameWnd. Подробности смотрите в примере ЕХ14В.
Пример ЕХ14В
Здесь стандартная строка состояния заменяется новой со следующими текстовыми секциями
Индекс секции
Идентификатор строки
Тип
Описание
0
IDJEPARATOR (0)
Строка сообщений
лг-координата курсора
1
1DJEPARATOR (0)
Строка сообщений
^-координата курсора
2
!DJNT)ICATOR_LErr
Индикатор состояния
Состояние левой кнопки мыши
3‘
1D_IND1CATOR_R1GHT
Индикатор состояния
Состояние правой кнопки мыши
Новая строка состояния показана на рисунке 14-4. Обратите внимание; при увеличении размеров окна-рамки крайняя левая секция растягивается за пределы своей обычной длины в 1/4 ширины экрана.
: Watch thestatusbirwhileyou move and clickthe mouse.I
i
Рис. 14-4. Строка состояния в программе ЕХ14В
Для создания программы ЕХ14В выполните следующие действия.
Используя Арр Wizard, создайте проект \vcpp32\exl4b\exl4b. Примите параметры по умолчанию, но выберите Single Document и откажитесь от Printing And Print Preview. Параметры и имена классов по умолчанию таковы:
Отредактируйте в редакторе ресурсов ресурс строковой таблицы. У приложения есть единственный ресурс строковой таблицы с искусственным делением на сегменты, оставшимся от эпохи 16-разрядных программ. Чтобы запустить редактор строковых ресурсов, дважды шрлкните папку String Table на странице ResourceView. Затем дважды п^л- кните пустой элемент в конце списка и в появившемся диалоговом окне введите идентификатор и текст строки, как показано ниже на рисунке:
||v*hb»ck to theptwout wndowpaneWevetn Р<*v*hb»ck to theptwout wndowpaneWevetn Р<*5763157631^137 590 99139 £0140S._ И1^137 590 99139 £0140S._ И1IDJN«CATOR_CAPS ID INOCATDRJIUM ID INDICATOR_SCRL ID INDICATOR OVR ID.INMCATOR.RECIDJN«CATOR_CAPS ID INOCATDRJIUM ID INDICATOR_SCRL ID INDICATOR OVR ID.INMCATOR.RECID_VIEW STATUS BAR "SRJB^S^AFXJDS.5CM0VEAFX IDS.SCMINIMIZE AFX IDS SCMAXIMEE AFX ©S SCNEXTVflNDCW AFX 1DS.SCPREVWIMD0W AFX IDS "SCCLOSE-BKro^JStftESTOffitAFXJDS.SCTASKUSTIDJND1CAT0RJ.EFTID_VIEW STATUS BAR "SRJB^S^AFXJDS.5CM0VEAFX IDS.SCMINIMIZE AFX IDS SCMAXIMEE AFX ©S SCNEXTVflNDCW AFX 1DS.SCPREVWIMD0W AFX IDS "SCCLOSE-BKro^JStftESTOffitAFXJDS.SCTASKUSTIDJND1CAT0RJ.EFT53893 ТОГ 61105 61 06 61187 «я 8)53893 ТОГ 61105 61 06 61187 «я 8)61190611906121 : 612046121 : 61204CAPNUMSCRLOVRREC ~"Showa1Rde thelocbaiXnToggtoi T «BeShwy cr hide it* stab# bar\nTo^*Statu$Bw "Change the window weChange thewhdow poetioti Reduce (he windowto an con Enbtge the window lofclsBe Switch tothe nedckcumarft wirctof# Switch tot* pt*v*ou* cbcunert yndow Doaethe activeCAPNUMSCRLOVRREC ~"Showa1Rde thelocbaiXnToggtoi T «BeShwy cr hide it* stab# bar\nTo^*Statu$Bw "Change the window weChange thewhdow poetioti Reduce (he windowto an con Enbtge the window lofclsBe Switch tothe nedckcumarft wirctof# Switch tot* pt*v*ou* cbcunert yndow Doaethe active^^66■"W2ff "ЙЖГ<ЙЖ9■"W2ff "ЙЖГ<ЙЖ9Activate Task Ui LEFTActivate Task Ui LEFTRIGHTRIGHT
|
|
v*hb»ck to theptwout wndowpaneWevetn Р<*
v*hb»ck to theptwout wndowpaneWevetn Р<*
57631
57631
^137 590 99139 £0140
S._ И1
^137 590 99139 £0140
S._ И1
IDJN«CATOR_CAPS ID INOCATDRJIUM ID INDICATOR_SCRL ID INDICATOR OVR ID.INMCATOR.REC
IDJN«CATOR_CAPS ID INOCATDRJIUM ID INDICATOR_SCRL ID INDICATOR OVR ID.INMCATOR.REC
ID_VIEW STATUS BAR "SRJB^S^
AFXJDS.5CM0VE
AFX IDS.SCMINIMIZE AFX IDS SCMAXIMEE AFX ©S SCNEXTVflNDCW AFX 1DS.SCPREVWIMD0W AFX IDS "SCCLOSE
-BKro^JStftESTOffit
AFXJDS.SCTASKUST
IDJND1CAT0RJ.EFT
ID_VIEW STATUS BAR "SRJB^S^
AFXJDS.5CM0VE
AFX IDS.SCMINIMIZE AFX IDS SCMAXIMEE AFX ©S SCNEXTVflNDCW AFX 1DS.SCPREVWIMD0W AFX IDS "SCCLOSE
-BKro^JStftESTOffit
AFXJDS.SCTASKUST
IDJND1CAT0RJ.EFT
53893 ТОГ 61105 61 06 61187 «я 8)
53893 ТОГ 61105 61 06 61187 «я 8)
61190
61190
6121 : 61204
6121 : 61204
CAP
NUM
SCRL
OVR
REC
~"Showa1Rde thelocbaiXnToggtoi T «Be
Shwy cr hide it* stab# bar\nTo^*Statu$Bw "Change the window we
Change thewhdow poetioti Reduce (he windowto an con Enbtge the window lofclsBe Switch tothe nedckcumarft wirctof# Switch tot* pt*v*ou* cbcunert yndow Doaethe active
CAP
NUM
SCRL
OVR
REC
~"Showa1Rde thelocbaiXnToggtoi T «Be
Shwy cr hide it* stab# bar\nTo^*Statu$Bw "Change the window we
Change thewhdow poetioti Reduce (he windowto an con Enbtge the window lofclsBe Switch tothe nedckcumarft wirctof# Switch tot* pt*v*ou* cbcunert yndow Doaethe active
^
^
6
6
■"W2ff "ЙЖГ<ЙЖ9
■"W2ff "ЙЖГ<ЙЖ9
Activate Task Ui LEFT
Activate Task Ui LEFT
RIGHT
RIGHT
Добавьте следующие две строки:
Идентификатор строкиТекст
1DJNDICA TORJJiFTLEFT
ID INDICATOR_SIGHTRIGHT
Отредактируйте, применяя Visual C++, символы в ресурсах приложения. Выберите из меню View команду Resource Symbols. Задайте для строки состояния новый идентификатор ID_MY_STATOS_BA№t оставьте для него значение, предлагаемое по умолчанию.
Используя ClassWizard, добавьте в класс CMainFrame обработчики команд, содержащихся в меню View. Добавьте следующие обработчики командных сообщений.
Идентификатор объектаСообщениеИмя функции-члена
ID_VTEW_STATUS_BARCOMMANDOnViewStatusBar
ID VlEVi STATl' BAR UPDATE, COMMAND,4OnUpdateViewStatnsBar
Вставьте в MainFrm.h прототипы функций. Вам придется добавить эти прототипы обработчиков сообщений CMainFrame вручную, так как ClassWizard не распознает связанные с ними идентификаторы командных сообщений.
afx_msg void OnUpdateLeft(CCmdUI* pCmdUI);
afx_msg void OnUpdateRight(CCmdUI* pCmdUI);
Вставьте эти операторы внутри скобок AFX_MSG, чтобы впоследствии их можно было модифицировать с помощью ClassWizard. Кроме того, объявите я?_г«ийЬо'ЛЗлготкрыгой, а не защищенной переменной.
Отредактируйте файл MainFrm.cpp. Замените старое содержимое массива indicators новым (выделено полужирным):
static UINT indicators[] =
ID_SEPARATOR, // первая секция строки сообщений
ID_SEPARATOR, Ц вторая секция строки сообщений-
ID_INDICATOR LEFT, ID_INDICATOR_RIGHT, };'
Далее отредактируйте функцию-член OnCreate. Замените оператор
if (!m_wndStatLsBar. Create(this) il
'!-n_wndStatisBar.SetI.idicators (indicatcrs, sizeof (indicatcrs)/sizeof(UINT)) 1
{'
TRACEO("Failed to create status bar\n);-■-■
return -1;// ошибка при создании
}
на:
if (!m_wndStati.sBar.Create(this, ws_child ; wsVisible : cbrs.bottom, id_my_status_bar) :: im_wndStatLsBar. Setlndicators(indicators, sizeo^indicatcrsj/sizeof (UINT)))
' TRAOEO("Ошибка при создании строки состояния\п");1
return -1; U ошибка при создании
В модифицированном вызове Create вместо AFX_IDW_STATUSBA$(o6b6¥x строки состояния, формируемый каркасом приложений) используется наш идентификатор строки состояния ID_№_STATUS_BAR..
Теперь добавьте следующие элементы таблицы сообщений для класса CMainFrame. ClassWizard это сделать не в состоянии, так как не распознает идентификаторы из строковой таблицы в качестве идентификаторов объектов:
ON_UPDATE_COMMAND UI(ID INDICATOR LEFT, OnUpdateLeft) ON_UPDATE_COMMAND_UI(ID_INDICATOR_RIGHT, OnUpdateRight)
Затем вставьте функции-члены класса CMainFrome, отвечающие за обновление индикаторов: void CMalnFrame::OnUpdateLeft(CCmdUI* pCmdUI)
* pCmdUI->Enable(::GetKeyState(VK LBUTTQN) < 0);
}
void CMalnFrame::OnUpdateRight(CCmdUI* pCmdUI)
< pCmdUI->Enable(: :GetKeyState(VK_fiBUTTON) < 0);
}'
Заметьте: левой и правой кнопкам мыши, как и клавишам на клавиатуре, соответствуют коды виртуальных клавиш, поэтому при определении состояния кнопок можно обойтись без сообщений от мыши.
И, наконец, отредактируйте следующие функции для меню View, которые ClassWizard сгенерировал в файле MainFrm.cpp:
void CMalnFrame::OnViewStatusBar()
{
m_wndStatuaBar,ShowWlndow((m_wndStatusBar.GetStyle() & WS_VISIBLE> == 0); flecalcLayout();
void CMalnFrame: :OnUpdateViewStatusBar(CCmdUI* pCmdUI)
* pCmdUI->SetCheck((m_wndStatU8Bar.GetStyle() & WS_VISIBLE) != 0);
I
Эти функции обеспечивают надлежащую связь команды Status Ваг меню View с новой строкой состояния.1
Отредактируйте функцию OnDrowe Exl4bView.cpp. Эта функция выводит сообщение в окне представления. Добавьте код, выделенный полужирным;
void CEx14bVlew: :OnDraw(CDC* pDC)
{ pDC->TextOut(O, 0,
"Следите за строкой состояния, перемещая мышь и щелкая ее кнопками");
}
Добавьте в класс С£х/4ЬИемх>бработчик для WM_MOUSEMOVE. Используя ClassWizard, сопоставьте этому сообщению функцию OnMouseMove и отредактируйте ее, как показано ниже. Функция получает указатель на объект строки состояния и вызывает Set- РапеТ&Лрдя обновления первой и второй секции строки сообщений.
void CEx14bView::OnMouseMove(UINT nFlags, CPoint point)
(
CString str;.
CMainFrame* pFrame = (CMalnFrame*) AfxGetApp()->m_pMainWnd;
CStatueBar* pStatus = &pFrame->mTwndStatusBar;
if (pStatua) {
3tr.Format(*x = %d", point,x);
pStatus->SetPaneText(O, str);•
str.FormatC'y = Hd", point.y);
pStatU8->SetPaneText(1. str);
}
}'
И, наконец, в начале файла exl4bView.cpp вставьте директиву
tfinclude "MainFrm.h"
Соберите и протестируйте приложение ЕХ14В. Подвигайте мышь и убедитесь, что де ; левые секции в строке состояния точно отражают позицию курсора. Попробуйте нажат» на левую и правую кнопки мыши. Удается ли Вам включать и отключать показ строки состояния командами из меню View?
ПРИМЕЧАНИЕ Если Вы хотите, чтобы у левой (нулевой) секции сообщений было обрамление, как и у остальных секций, а также чтобы строка состояния изменяла свой размер в соответствии с содержимым, включите в функцию CMainFrame::OnCreate (после вызова функции Create для строки состояния) такие две строки:
m_wndStatusBar.SetPaneInfo(0, 0, 0, 50);
n-^wndStatusBar.SetPanelnfod, 0, SEPS-STRE^CH, 50);
Они изменяют ширину первых двух секций (по сравнению с шириной по умолчанию в четверть экрана) и делают вторую секцию «растягивающейся».
Панель инструментов Internet Explorer Rebar
Как Вы уже знаете из главы 9, в Visual C++ 6.0 есть масса возможностей, которые открывают стандартные элементы управления, входящие в состав Internet Explorer 4.0 (Internet Explorer Common Controls). Среди них — новая панель инструментов, называемая rebar. Скорее всего, Вы уже знакомы с ней, если хоть раз работали с Internet Explorer 4.0. Ее отличает от стандартной панели инструментов MFC наличие зацепок (grippers), дающих пользователю возможность плавно изменять ее положение по горизонтали и по вертикали; положение стандартной панели меняется в результате подстыковки при перетаскивании. Rebar-панели позволяют разработчику реализовать гораздо больше типов элементов управления (например, выпадающие меню), чем это позволяет сделать класс ClbolBar.
Внутренняя структура rebar-панели
На рисунке 14-5 Вы видите термины, имеющие отношение к rebar. Каждая внутренняя панель инструментов в rebar-панели называется полосой (band). Краешек с чертой, за которую пользователь перетаскивает полосу, называется зацепкой (gripper). Каждая полоса может иметь летку (label).
Зацепка Дочернее окноПолоса 1 Текст метки Полоса 2
(комбинированный список)
Рис. 14-5. Термины, связанные с rebar-панелью
В MFC есть два класса, облегчающие работу с rebar-панелями.
CReBar — высокоуровневый абстрактный класс, поддерживающий добавление объект<)в классов CTooiBarn CDialogBarB rebar-панели в качестве полос. Кроме того, CReBar управляет (посредством уведомлений) взаимодействием между лежащим в его основе элементом уцравления и каркасом МРС-приложсния.
I* CReBarCtrl — низкоуровневый класс-оболочка вокруг элемента управления IE ReBar. В этом классе есть многочисленные элементы для создания и манипулирования rebar-панелями, но он не так элегантен, как CReBar.
Большинство MFC-приложений работают с CReBar, чтобы получить доступ к низкоуровневым возможностям, вызывают функцию-член GelReBarCtrl, которая возвращает указатель на CReBarCtrl.
Пример ЕХ14С
Давайте познакомимся с возможностями rebar-панели на конкретном примере. Это будет SDI- приложснис с rcbar-панслью с двумя полосами: уже знакомой нам панелью инструментов и диалоговой панелью. На рисунке 14-6 показано это приложение в процессе работы.
Рис. 14-6. Пример rebar-панели ЕХ14G
Для создания приложения ЕХ14С. выполните следующие действия.
Используя AppWizard, создайте проект \vcpp32\exl4c\exl4c. Выберите Single Document. На этапе 4 выберите вариант Internet Explorer Re Bars в ответ на вопрос «How Do You Want Your Toolbars То Look?» («Как должны выглядеть ваши панели инструментов?*). Правильный вариант установки параметров приведен на рисунке 14-7. Примите остальные параметры по умолчанию. ■
Рис. 14-7. Корректная установка параметров на этапе 4 AppWizard
Скомпилируйте и запустите приложение. Когда Вы запустите приложение, то увидите, что AppWizard автоматически создал rebar-панель с двумя полосами. Одна из них содержит стандартную панель инструментов, а вторая — текст «TODO: layout dialog bar» («Доделать: разместить диалоговую панель»), Начальное состояние панели приведено на рисунке 14-8.
Рис. 14-8. Окно приложения ЕХ14Сс rebar-панелью по умолчанию
Теперь, открыв файл MainFrm.h, можно увидать приведенный ниже код, в котором объявляется элемент данных m_ivndReBar класса CReBar.
protected: // внедренные переменные панелей элементов управления
CStatusBar m_wndStatusBar:.
CToolBar m_wndToolBar;
CReBar ni_wndReBar;
CDlalogBa- m_wndDlgBar;
А в файле MainFrame.cpp можно увидеть код, который вставляет панель инструментов и диалоговую панель в объект CReBar.
if (|H_wndRe3ar.Create(this) ||
!m_wndReBar.AddBar(&m_wndToolBar) | j Jm^wndReBar. Add8ar(&m_wnd[>lgBar))
TRACEO("Неудачное создание геЬаг-панели\п"); return-1; //неудачное создание
Рис. 14-9. Отредактированная диалоговая панель 1DR_MAINFRAME
Расположите диалоговую панель. Откройте редактор ресурсов Visual C++. В папке Dialog отыщите ресурс с идентификатором /Р^Л^^ЛШЕОткрыв его, Вы увидите диалоговую панель с текстом «TODO: layout dialog bar» («Доделать: разместить диалоговую панель»). Последуем этому дружескому совету и разместим здесь несколько элементов уп-
равления. Прежде всего, удалите статический элемент с текстом «TODO*. Затем поместите в диалоговую панель комбинированный список и введите в него несколько элементов данных: one, two, buckle, ту, s^o«/3utcm поместите в диалоговую панель кнопку и измените ее текст на Increment. А теперь добавьте к панели индикатор продвижения с установленными по умолчанию значениями свойств. Наконец, добавьте еще одну кнопку с надписью Decrement. После окончания работы диалоговая панель должна выглядеть как на рисунке 14-9
Свяжите диалоговую панель с классом CMainFrame. Прежде чем мы сможем запрограммировать обработчики для кнопок Increment и Decrement, нужно связать диалоговую панель с классом, используя ClassWizard. Находясь в редакторе ресурсов, вызовите ClassWizard двойным щелчком кнопки Increment. Появится следующее диалоговое окно.
Выберите Select An Existing Class. Мы выбрали именно этот вариант, так как хотим, чтобы наш диалоговый ресурс был связан с панелью инструментов, а не с отдельным диалоговым классом. Щелкните ОК, и перед Вами появится список вариантов.
Выберите из списка CMainFrame и щелкните Select.
ClassWizard сделает Вам последнее предупреждение: «Выбранный Вами класс CMainFrame не являвтсядиалоговым классом. Вы действительно хотите, чтобы ClassWizard связал с ним диалоговый ресурс?»
Щелкните Yes и выйдите из ClassWizard. Диалоговая панель lDR_MAINERAMEycneniHO связана с классом CMainFrame.
Запрограммируйте поведение диалоговой панели. Для этого вновь загрузите диалоговый ресурс IDR MAINFRAME в редактор ресурсов и дважды щелкните кнопку Increment. ClassWizard автоматически создаст обработчик OnButtonl. Примите это имя и вве дате в код функции OnButtonl текст, выделенный полужирным:
void CMainFrame::OnButton1()
{
CProgressCtrl * pProgress =
(CProgreseCt rl*)m_wndDlgBar.GetDlgItee(IDC_PROGRESS1);
pProgress->StepIt();
}
Вначале обработчик OnButtonl получает указатель на индикатор продвижения, а затем вызывает метод Steplt, чтобы увеличить его значение.
Теперь нам нужно добавить аналогичный код в обработчик второй кнопки. Дважды щелкните кнопку Decrement, и ClassWizard автоматически создаст обработчик OnButton2. Примите это имя и введите в код функции OnButtonZ текст, выделенный полужирным:
void CMainFrames :0nButton2()
CProgressCtrl * pProgress =
(CProgressCtrl*)m_wndDlgBar.GetDlgItem(IDC_PROGRESS1); int nCurrentPoa = pProgress->GetPos();
pProgres3->SetPos(nCurrentPos-10);
Скомпилируйте и протестируйте приложение. Теперь можно скомпилировать и запустить ЕХ14С, чтобы увидеть отредактированную rebar-панель в действии. Кнопка Increment увеличивает индикатор продвижения, а кнопка Decrement — уменьшает.
В этой главе Вы научились работать с элементами управления MFC «панель инструментов» и «строка состояния» и с новым элементом rebar. В следующей главе Вы узнаете, как расширить MFC, чтобы реализовать окно-рамку, которое запоминает свое расположение.
ГЛАВА 15
Повторно используемый базовый класс окна-рамки
Язык C++ позволяет использовать «конструктор» из программных блоков, которые можно запросто брать «с полки» и вставлять в новые приложения. Прекрасный пример этого — классы библиотеки MFC6.0. В этой главе мы рассмотрим, как создать свой повторно используемый базовый класс на основе библиотеки MFC.
Работая над таким классом, Вы узнаете кое-что новое о Windows и библиотеке MFC. В частности, Вы увидите, как каркас приложений обеспечивает доступ к реестру Windows, разберетесь в механизме работы класса CPrameWndH расширите свое представление о статических переменных класса и о классе CString.
Причины трудностей при создании повторно используемых базовых классов
В обычном приложении Вы пишете программные компоненты, предназначенные для решения конкретной задачи. Однако, создавая повторно используемый базовый класс, нужно учитывать будущие потребности — как свои, так и других программистов. Класс должен быть не только универсальным и законченным, но и эффективным и простым в использовании.
Разрабатывая пример для этой главы, авторы воочию убедились, насколько трудно создавать повторно используемое программное обеспечение. Хотелось написать класс окна-рамки, который «запоминал» бы размеры и позицию своего окна. Вскоре выяснилось, что существующие программы для Windows запоминают еще и состояние своего окна: свернуто оно в значок или развернуто на весь экран. Вдобавок обнаружился какой-то странный тип окон — и свернутых в значок, и развернутых одновременно. Еще надо было позаботиться о панели инструментов и строке состояния, и, кроме того, добиться, чтобы класс работал в DLL Короче говоря, написать класс окна-рамки, который делал бы все, что захочется программисту, оказалось на удивление сложно.
При разработке программного обеспечения на промышленной основе повторно используемые базовые классы зачастую не вписываются в нормальный цикл создания программ. Класс, подготовленный для одного проекта, берут и подгоняют под другой проект. При этом всегда испытываешь искушение «вырезать и вставить» существующие классы, не задаваясь вопросом: «А нельзя ли что-либо выделить в базовый класс?». Но если Вы решили заниматься программированием всерьез, создание своей библиотеки настоящих повторно используемых программных компонентов будет очень полезно.
Класс CPersistentFrame
В этой главе Вы поработаете с классом CPersistentFrame, производным от CFrameWnd. Класс CPersistentFrame поддерживает «устойчивое» (persistent) окно-рамку SDI-приложения, запоминающее следующие характеристики:
размер окна;
его положение;
развернутое состояние;
свернутое состояние;
расположение и состояние видимости панели инструментов и строки состояния.
Завершаясь, приложение, в котором используется класс CPersistentFrame, сохраняет указанную информацию в реестре Windows. При следующем запуске приложение считывает эту информацию из реестра и восстанавливает то состояние окна-рамки, которое имело месте на момент последнего завершения программы.
Этот класс устойчивого окна-рамки пригоден для любого SDI-приложения, в том числе для примеров из этой книги. Все, что нужно сделать, — заменить CFrameWnd» производном классе окна-рамки приложения на CPersistentFrame.
Класс CFrameWnd и функция-член ActivateFrame
Почему в качестве базового класса для устойчивого окна выбран бУгатс^'м^РПочему бы вместо этого не создать класс «устойчивый вид» (persistent view class)? В SDI-приложении. написанном с использованием MFC, окно-рамка всегда является родительским по отношению к окну класса «вид». Сначала создается окно-рамка, и только потом формируются его дочерние окна: панель инструментов, строка состояния и окно представления (объект «вид») Каркас приложений гарантирует соответствующее изменение размеров дочерних окон при масштабировании окна-рамки, поэтому нет никакого смысла изменять размер окна представления после создания окна-рамки.
Функция-член CFrameWnd::ActivateFrame— ключ к управлению размером окна-рамки Каркас приложений вызывает эту виртуальную функцию, объявленную в классе CFrameWnd при создании основного окна-рамки SDI-приложения (и по командам New или Open из меню File). Задача каркаса — вызвать функцию CWndaSbowWindowc. параметром nCmdSbow. Sbow- Window делает видимым окно-рамку вместе с меню, окном представления, панелью инструментов и строкой состояния. Параметр nCmdSbowопределяет, развернуто окно, свернуто, или верно и то и другое..
Если переопределить функциюActivateFrame в производном классе, то значение nCmdSbou перед передачей в функцию CFimmeWndaActivateFrameMoxHo изменить. Кроме того, можно вызвать функцию CWndi.SetWindowPtacementyLoropaa устанавливает размер и позицию окна- рамки и позволяет указать, должны ли присутствовать в нем панель инструментов и строка состояния. Поскольку все изменения выполняются до того, как окно-рамка станет видимым, неприятного мелькания на экране не возникнет.
Нужно позаботиться и о том, чтобы не инициализировать повторно положение и размер окна-рамки постю каждой команды File New и File Open. Для этого служит псрсменная-член — признак первого вызова, проверка которого гарантирует, что функция CPersistentFrame::Acti- vateFrame вызывается только при запуске приложения..
Функция-член PreCreateWindow
PreCreateWindow, объявленная в CWnd, — еще одна виртуальная функция, которую можно переопределить, чтобы изменить характеристики окна до его появления на экране. Каркас
приложений вызывает ее перед вызовом ActivateFrame. Мастер AppWizard всегда генерирует в ваших классах «вид» и «окно-рамка» переопределенную функцию PreOeateWindow.
В качестве параметра этой функции передается структура CREATESTRUCT, в которой есть две переменных-члена: style и dwExStyle. Вы можете изменить их перед передачей структуры в функцию PreOeateWindow из базового класса. Флажок style определяет, есть ли у окна границы, полосы прокрутки, кнопка сворачивания окна и т. д. Флажок dwExStyle управляет другими характеристиками, например, состоянием «always on top* («поверх остальных окон»). Подробнее об этом см. в разделах Window Styles и Extended Window Styles интерактивной документации.
Если Вы хотите изменить кисть фона, курсор или значок окна, используйте элемент ipsz- Class структуры CREATESTRUCT. Изменять курсор или фон для окна-рамки бессмысленно, та* как его клиентская область закрыта окном представления. Если Вы самостоятельно регистрируете оконный класс, нет необходимости вызывать PreOeateWindow из базового класса, Если Вы, к примеру, захотите создать окно представления с ужасающим красным фоном, переопределите PreOeateWindow для класса «вид» следующим образом: BOOL CMyView::PreCreateWindow(CREATESTRUCT& cs)'
г h if <! CV.lew: :PreCreateWindow(cs)) { return FALSE; i'.
cs.lpszClass =
AfxRegisterWndClass(CS_DBLCLKS I CS.HREDRAW I CS_VREDRAW, AfxGetApp()->LoadCursor(IDC MYCURSOR), ::CreateSolidBrush(RGB(255, 0, 0))); if (cs.lpszClass J= NULL) { return TRUE; else { return FALSE; )
Если переопределить PreOeateWindow в классе устойчивого окна-рамки, окна всех производных классов будут обладать свойствами, заложенными в базовый класс. Конечно, в производных классах функцию PreOeateWindow можно снова переопределить, но тогда надо продумать взаимодействие этих двух функций.
Реестр Windows
Если Вы работали с приложениями для 16-разрядной Windows, то, вероятно, знакомы с INI- файлами. Их можно применять и в ^1п32-приложсниях, но Microsoft рекомендует вместо этого использовать реестр Windows. Реестр — это набор системных файлов, контролируемых Windows, куда операционная система и отдельные программы могут помещать информацию для долговременного хранения. Реестр организован как иерархическая база данных, доступ к строковым и числовым данным которой осуществляется по составным ключам.
Например, текстовой процессор TEXTPROC «хочет» сохранить в реестре тип и размер последнего использованного шрифта. Допустим, имя программы образует корневой раздел (это, конечно, упрощение) и приложение поддерживает два уровня иерархии в этом разделе. Получится что-то в таком духе;
TEXTPROC.,
Text formatting
Font = Times Roman1
Points =10■
$f;S Симво^гы,. цсаз^^ду^р вевропенеких языках, можно закодировать восемью разряда-; |лйй5—’д^е'сщ&знаками. Большинство азиатских языков тре-
Для"своих ^ii^^e^'^S разрядов. Многие программы используют стандарт я*а- ?р^$ |§!Q£i|^i^|^^character set, ЬДС^ в котором некоторые
^ ^Символы кодируют^16 разрядами, в зависимости от значения первых 8 ■
; тразрядок УВСЗГ<^йч^ 39М®ЖUnicode, в котором все символы кодиру-
i з^отсяЧб разрядайй. Дйя ЬтйвЯВьйых языков не выделяются отдельные наборы символов: . Мнапрймвр, если "символ используется в японском и китайском языках, то этот символ ■ '^лишь однократно входит в символов Unicode, /у;
: . Bfchh’n^^MFC и на код, сгенерированный AppWizard, то
- можно y^^Ktb типы Ъ$Й£ц$$^^ и LPfJTSTIi, а также строковые константы вида V- •■jn^?^®)- НереидUnicode. Если собирать проект без определения сим- , . вола препроцессора .^^^ЙЗВкомпилягор сгенерирует код ддя обычных 8-разрядных. символов ANSIJ^Cl _, Ъ указателей на массивы б^раз^ядё^хсимволов '(XPSTRjtPCS^i Если же опредешпй £^?|Й^ЙЙ, компилятор сгенерируем код для 16-разрядаых симво-' . лов Unicode (Ж7Й4Й)) соответствующих указателей (LPWSTJ?, LPCWSTK) и" строковых:
V: .константi:Ь t|■ .= ; -. . v . ; ; \,-
Символ препроцессора’ (^^^ф^опредвлжт тдкж^' такие функции вызываются - приложением, так как многие^ функции Win32 существуют в двух версиях. Когда про- 1 грамма едзьдаае^».генерирует код для вызова,
г либо <ЯИ№в1й№(Й|Й^ANSl^i либо ^eidSWindenvEatVI^c параметрами
;; Unicode):" В Microsoft Windows NT, вцутри которой используется Unicode. CreateWin- (АййЗкТЙгёреДаст Ь<ж параметры системе напрямую, тогда как CreateWindowExAn?peG6- разует строковые (ANSI) и символьные параметры в Unicode , А вот в Microsoft Windows 95, J внутри котор ой используется ANSI, СгйаГ^^гмвЬгу^х^редставляет собой заглушку, ■’ которая возвращает ф$$8ф а ^^<?\Ии^оя?&е1передает параметры системе напрямую.
. Если Вы хотите' создать Уп^рдр-пртожение, его следует создавать для Windows NT ' и везде йспользовать йакр$>|ы.’ Можно создавать Unicpd^-pEpMO^eH^ и для Windows 95, 1 но в этом случае придется проделать лишнюю рабфу, яййо вызывая #^4в|ф|ЬИй функ’. indt"Win'32l Как показаненв Главах 24.-30, вызовы СОМ (за исключением DAQ) всегда ' используют ^Я1^рф^^^^е^ символы. Хотя и существуют функции Win32 для преобразования между ANSI иТГшсрсЬ , но. йспошзуя ктасс dSD^^^is: этого можно прибег- ' J цнуль к.цр^^д^^с^^^р^гора и фрпсцвд-члена^^Й^Йф?:-' ;?<: А .<' ~ ; . ? ■ J -. > Простоты щди^ё ивш^ры программ йз этой КНИГИ исцользу^т только ANSI. Код, , ^. котрры^ деюр^р^^^5^^родг ^>рWizard, содержит макросы pnicode, но код, напи- ■ ' ганньйдвтераз^щ'испо^^уетстооковые константы 8-разрядных ейзяволов и типы ейяг, • i
Для доступа к реестру MFC-библиотека предоставляет 4 функции-члена класса CWhiApp. сохранившиеся еще со времен INI-файлав. Начиная с Visual C++ 5.0, AppWizard генерирует вызов CWinApp::SelRegistryKe^ функции Inithislanceпрограммы, как показано ниже:
SetRegistryKey(_T("Local AppWizard-Generateo Ann"zcations"));•
Если Вы удалите этот вызов, то приложение будет использовать не реестр Windows, a INI- файты в каталоге Windows. Строковый параметр функции SetRe^istn’Kc^определяет верхний уровень иерархии, а перечисленные ниже «реестровые» функции определяют дна нижних уровня — с именем подраздела и с именем параметра.
I* GetPrqplelm
I* WritePmfllelnt
i • GetProfUeString
т WriteProfiieString
Эти функции рассматривают данные в реестре либо как объекты CString, либо как целые числа без знака, Если Вам нужно сохранять в реестре значения с плавающей занятой, придется использовать строковые функции и заняться преобразованиями. Всем функциям передаются имена подраздела и параметра реестра. В предыдущем примере у подраздела было имя «Text formatting», а у параметров — имена «Font» и «Points*.
Чтобы воспользоваться функциями доступа к реестру, нужен указатель на объект «приложение». Чтобы его получить, нужно вызвать глобальную функцию AfxGetApp Если вернуться к предыдуп^му примеру, то установить параметры «Font» и «Points» может следующий код: AfxGetApp()->WriteProtileST.ring("Text formattirg", ''Font", "Times Roman”);
AfxGetApp()->WriteProfileInt("Text formatting", "Points", 10),
В примере EX15A Вы реально поработаете с реестром, научитесь просматривать и редактировать его с помощью программы Regedit.
ПРИМЕЧАНИЕ Каркас приложений сохраняет список последних открытых файлов в подразделе реестра Recent File List.
Использование класса CString
Этот MFC-класс значительно расширяет язык C++. Как указано в разделе Microsoft Foundation Classes and Templates интерактивной документации, у класса CString много полезных операторов и функций-членов, но, по-видимому, главное его достоинство — способность динамически выделять память. Благодаря этому не надо заботиться о размере строки. Вот несколько типичных примеров использования объектов CString
CString strFirstName("Elvis");
CString strLsstName("Presley‘ );.
CString strTruth = strFirstName + " " + strLastName; // конкатенация strTruth += " is alive";
ASSERT(strTruth == "Elvis Presley is alive");
ASSERT(strTruth.Left(5) == strFirstName) ;
ASSERT(strTruth[2] == *v‘); //операция индексирования
В совершенном мире программы на C++ всегда использовали бы объекты CStnngn никогда — обычные массивы символов с нулем в конце. Увы, многие функции стандартной библиотеки по-прежнему полагаются на эти массивы, поэтому программы должны уметь работать с обоими представлениями строк. К счастью, в классе CString есть оператор const chat* О, преобразующий объект CString в указатель на символы. Многие функции библиотеки MFC требуют параметров типа const char*. Возьмем, к примеру, глобальную функциюЛ7^е«$Д£еВах. Вот один из ее прототипов:
int AFXAPl AfxMessage9ox(LPCTSTR IpszText, UINT niype = Mfi OK.
'UINT nIDHelp = 0);"
(Обратите внимание: LPCTSTR — это не указатель на объект CString, а заменитель для const chaf, совместимый с Unicode.)
AfxMessageBoxMaxHO вызвать так:
char szMessageText[] = "Unknown error";
AfxNessageBox(szMessageText);
или так:,
CString strMessageTextC'Unknown error");
AfxMessageBox(strHess ageText):
Теперь предположим, что надо сгенерировать строку в соответствии с определенным форматом. Эта задача решается при использовании функции CString:: Format: int nError = 23;
CString strMessageText;
strMessageText.Formate"Error number %d’, nError);
AfxMessage8ox(strMessageText);
ПРИМЕЧАНИЕ Допустим, Вам понадобился прямой доступ к символам в объекте CString. Если Вы напишете примерно такой код:
CString st rTest( "test"); strncpy(strTest, "Г, 1);
то компилятор сообщит об ошибке, так как первый параметр strnepy объявлен как char*, а не const char*. Функция С^плд:.Ое?Ви1Гегфиксирует заданный размер буфера и возвращает char*. Чтобы потом вновь сделать строку динамической, вызовите функцию-член ReleaseBuffer. Вот как правильно заменить строчную букву (прописной:
CString strTestC'test");
strncpy(strTest.GetBuffer(5), "Т", 1);.
strText.ReleaseBuffer();
ASSERT(strTest == "Test");
Оператор const сЬяг* преобразует объект CString в указатель на константу; но как быть с обратным преобразованием? У класса CString есть конструктор, преобразующий указатель на символьную константу в объект CString, и, кроме того, набор переопределенных операторов для таких указателей. Вот почему допустимы такие выражения, как truth += " is alive";.
Специальный конструктор работает с функциями, принимающими ссылку на CString, например с GDC::TexlOut. В следующем примере на стеке вызывающей программы создается временный объект CString, а его адрес передается в TextOut-.
pDC->TextC'Ut(D, 0, "Hello, world!");'
Если Вы хотите сами подсчитать число символов, эффективнее использовать переопределенную версию CDC::TextOut-.
pDC->TextOut(O, 0, "Hello, world!", 13);
Если Вы пишете функцию, принимающую строковый параметр, учтите несколько правил. ♦ Еслифункция не меняет содержимое строки и Вы собираетесь работать со стандартными библиотечными функциями, такими как strepy, используйте параметр const char*.
Если функция не меняет содержимое строки, но Вы хотите вызывать в ней функции-члены CString, используйте параметр const CString®-.
Если функция меняет содержимое строки, используйте параметр CString®-.
Положение развернутого окна
Как пользователь Windows, Вы знаете, что окно можно развернуть либо через системное меню, либо щелкнув кнопку в его правом верхнем углу. Аналогичным образом можно восстановить окно (вернуть его к исходному размеру из развернутого состояния). Отсюда ясно, что развернутое окно помнит свои исходные размеры и положение.
Экранные координаты окна возвращает функция GetWindowRectкласса CWhd. Если окно, развернуто, она сообщает размер экрана, а не координаты окна в неразвернутом состоянии. Чтобы класс устойчивого окна-рамки работал с развернутым окном, он должен знать его координаты в обычном состоянии. Эти координаты — вместе с флажками, которые указыва-
ют, развернуто ли окно в данный момент, свернуто или и то и другое вместе, — нозвращает функция CWnd::GetWindowPlacement.
Парная ей функция SetWindowPlacemenmossottaeT задать положение и размеры окна как в развернутом, так и в свернутом состоянии. Чтобы вычислить положение левого верхнего угла развернутого окна, надо учесть размер границ окна, который можно выяснить через №т32-функцик> GetSystemMetrics.^os, функции CPersistentFra-me::ActivateFramey№ используется функция SetWindowPlacement, приведен на рисунке 15-1.
Состояние панелей элементовуправления и реестр ■
Для сохранения в реестре и последующего восстановления состояния панелей элементов управления в MFC-библиотеке предусмотрены две функции-члена класса CFrameWndJPad- BarState и SaveBarState. Обе они применимы и к строке состояния, и к стыкуемым панелям инструментов, но не обрабатывают положение плавающих панелей инструментов.
Статические переменные-члены
Класс CPersistenlFrameKv^m имена своих разделов реестра в переменных-членах типа массивы static const char, Какие еще могли бы быть варианты? Строковые ресурсы не годятся, так как строки надо определять в самом классе. (Однако использование строковых ресурсов имеет смысл, если CPersisteniFrame встроен в DLL) Глобальные переменные обычно не рекомендуются, поскольку нарушают инкапсуляцию. Применение статических объектов CString бессмысленно, потому что в этом случае при запуске программы строки пришлое ь бы копировать в динамическую память.
Еще один очевидный вариант — обычные переменные-члены. Но статические лучше, так как, будучи кон стантами, они выделяются в секцию данных «только для чте ния» и могут быть доступны сразу нескольким запущенным копиям одной программы. В том случае, когда класс CPersisteniFrame — часть DLL-модуля, массивы символов можно спроецировать на все процессы, использующие DLL По сути, статические переменные-члены — это глобальные переменные, область видимости которых ограничена соответствующим классом, что исключает конфликтимен.'
Оконный прямоугольник по умолчанию ■
Вы уже определяли прямоугольники в аппаратных или логических координатах. Объект CRect, создаваемый оператором
CRect rectiCta.-JSEpEFAULT. CW_USEDEFAULT, 0, 0);
имеет особый смысл. Создавая новое окно с таким прямоугольником, Windows смещает его по диагонали вправо и вниз относительно последнего открытого окна. При этом правый и нижний края окна всегда находятся в пределах экрана.■ .
Такой особый прямоугольник содержится в статической псрсмснной-члене rgctDeJault класса CWnd, которая конструируется именно с использованием констант GW_USEDEFAljI.T Класс CPersistentFrame объявляет как статическую переменную-член собственный оконный прямоугольник по умолчанию, rectDefault, с фиксированным .размером и положением, скрывая, тем самым, переменную из базового класса.
Пример ЕХ15А• .
Программа ЕХ15А иллюстрирует применение класса устойчивого окна-рамки CPersistentFrame. На рисунке 15-1 показано содержимое файлов persist.it и persisLcpp из проекта ВХ15А на прилагаемом ккниге компакт-диске. В этом примере мы вставим новый класс окна-рамки в SDI-приложсние, сгенерированное AppWizard. ЕХ15А — это программа-«беЗделушка», но класс устойчивого окна-рамки можно легко перенести в другое SDI-приложение, которое делает что-то полезное.••
PERSIST.Н
// Persist,h
.tfifndef INSIDE VISUAL CPP PERSISTENT FRAME
«define _INSIDE_VISUAL_CPP_PERSISTENT_FRAME
class CPersistentFrame : public CFrameWnd,
{ // запоминает свое положение на рабочем столе
DECLARE_DYNAMIC(CPersistentFrame)
private:
static const CRect s_rectDefault;
static const char s_profileHeading[];■
static const char s_profileRect[J; '•
static const char s_profilelcon[]:
static const char s_profileMax[];
static const char s_profileTool[];
static const char S_profileStatus[]:
BOOL m_bFirstTime;■
protected: // создание только сериализацией "
CPersistentFrameO;
"CPersistentFrameO;
//{{AFX„VIRTUAL(CPersistentFrame),
public:
virtual void Activate? ramednt nCmdShow = -i); protected:
//}}AFX_VIRTUAL,
//{{AFX„MSG(CPersistentFrame)
afx_msg void OnDestroy();
//}}AFX_MSG•■
DECLARE MESSAGE HAPO I;
ffendif // _INSIDE_VISUAL_CPP_PERSISTENT_FRAME
PERSIST.CPP.
// Persist.cpp Класс устойчивого окна-рамки для SDl-приложений
«include "StdAfx.h"
«include "persist.h"
ffifdef„DEBUG1.
flundefTHIS_FILEi
static char BASED_CODE THIS_FILE[] = _FILE__;. ..
ffendif'.
ШШШШШ1Шт1Ш11Ш1Ш1ПШШ1Ш11Ш11Ш11Ш .'
// CPersistentFrame.'
const CRect CPersistentFrame: :s_,rectDefault(10, 10, 500, 400); // статический const char CPersistentFrame: :8_profileHeading[] = "Window size";.
const char CPersistentFrame::s_profileRect[] = "Rect";,
const char CPersistentFrame::s_profilelcon[] = "icon";
const char CPersistentFrame: :s_profileMax[] = "max";
const char CPersistentFrame::S„profileTool[] = "tool";
const char CPersistentFrame: :8_profileStatus[] = "status";.
IMPLENENT_DYNAMIC(CPersistentFrame,CFrameWnd)
Рис. 15-1. Листинг кода для класса CPersistentFrame
BEGIN_MESSAGE_MAP(CPersistentFrame, CFrameWnd)
//({AFX MSG_MAP(CPersistentFrame)
ON_WM DESTROYO
//}}AFX MSG map
END_MESSAGE_MAP(>
Ш111ПШНППШ1ПН1ШШШ1ПН11Ш111Ш1П1НШ1П
CPersistentFrame::CPersistentFrame()
m_bFirstTime = TRUE;
/////////////////////////////////////////////////////////////// CPersistentFrame::~CPersistentFrame()•
{
J
///////////////////////////////////////////////////////////////
void CPersistentFrame: :0n0estroy()
I
CString strText;■.
bool Mconic, bMaximized;
WINDOWPLACEMENT wndpl;
wndpl. length = sizeof (WINDOWPLACEMENT);
// получаем текущее положение окна и состояние "свернуто/развернуто"
BOOL bRet = GetWindowPlacement(&wndpl);
if (wndpl.showCmd == SW_SHOWNORMAL) {
ЫСОП1С = FALSE; bMaximized = FALSE;
}'
else if (wndpl.showCmd == SW.SHOWMAXIMIZED) { biconic = FALSE;
bMaximized = TRUE;
}
else if (wndpl.showCmd == SW_SHOWMINIMIZED) { biconic = TRUE;■
if (wndpl.flags) {
bMaximized = TRUE;
}■'
else {'
bMaximized = FALSE;
}
strText. Format("%04d %04d %04d %04d", wndpl.rcNormalPosition.left, wndpl. rcNormalPosition. top, wndpl.rcNormalPosition. right, wndpl.rcNormalPosition. bottom);
AfxGetApp()->WriteProfileString(s_profileHeading, sjorofileRect, strText); AfxGetApp()->WriteProfileInt(s_profileHeadlng, s_profilelcon, biconic);
AfxGetApp()->WriteProfileInt(s_profileHeading, s_profileMax, bMaximized);
Save8arState(AfxGetApp()->m_pszProfileName);.'
CFrameWnd;:0nDestroy();
/////////////////////////////////////////////////////////////// void CPersistentFrame: :ActivateFrame(int nCmdShow)
cm. ched. cmp.
Рио. 15-1* (продолжение)
CString striext;
BOOL blconic, bMaximized;
VINT flags;
WINDOWPLACEMENT wndpl;
CRect rect;
if (mJiFifstiime) {
im_t:Fi г stTiine = FALSE;
striext = AfxGetApp()->GetProfileString(s_profileHeading. s_profileRect);
if (* striext. IsEmptyO) {
rect.left = atol((const char-) striext);
rect.top = atoi((const cha.'"*) striext + 5);
rect. right = a Loi ((const char-) striext + 10);
rect.bottom = atoi((const char*) striext + 15);
else {
rcct = s_rectEefault;
^Iconic = AfxGetApp()->GetProfileInt(s_profileHeading, s_profilelcon, 0);
bMaximized = AfxGetApp()->GetProfileIrt(s_profileHeading, s_profileMax, 0); TRACF("CPersistentFrame: :ActivateFrame-Mconic = Kd,");
TRACE(" bMaximized = %d\n", blconic, bMaximized);
if (blconic) {
nCmdShow SW_SHW«INNOACirVE;
if (bMaximized) { flags = WPF RESTORETOHAXIMIZED;
}
flags = WPF SETMINPOSITION;
} ?
else {
if (bMaximized) { nCmdShow = SW SHOWMAXIMIZED;
flags = WPF_RESiORETCMAXIMIZID;-
}''
else {
nCmdShow = SW NORMAL;'
flags - WPF SETMINPOSITION;
}
wndpl.length = sizeof(WINDOWPLACEMENl);
wndpl.snowCmd = nCmdShow;
wndpl.flags = flags;'
wndpl.ptMinPosition » CPoint(0, 0);
wndpl.ptMaxPositicn =
CPointX-: ■.GetSystemMetrics(SM^CXBORDER),
-: :GetSystemMetrics(SM_Cv80RDER));
wndpl.rcNormalPosition = rect;
LoadBarState(AfxGetApp()->m_pszProfileName);-
H задаем положение и состояние окна BOOL bRet = SecWindDwPlacement(&wndpl);
}
CFrameWnd;:ActlvatcFrame(n.CmdShow);
Процесс создания приложения ЕХ15А.
Используя AppWizard, создайте проект \vcpp32\exl5a\exl5a. Примите параметры по умолчанию, но выберите Single Document и откажитесь от Printing And Print Preview. Параметры и имена классов по умолчанию показаны на рисунке.
Измените MainFrm.h. Вы должны изменить базовый класс для CMainFrame.Для этого просто замените строку class CMainR-ame : public CFramcknd на class CMainFrame : public CPersistentFrame He забудьте добавить строку «include "persist.h"
Измените MainFrm.cpp. Проведите глобальный поиск и замену всех вхождений CFrame- Wnd на CPersistentFrame.
Отредактируйте ех15а.срр. Замените строку SetRegistryKey(_T("Local AppWizarc-Generated Applications")); на SetRegistryKeyC'Programming Visual C++");
Добавьте файл persist.cpp в проект. Вы можете набрать текст файлов perslst.h и persist, срр по листингам с рис, 15-1 или скопировать их с компакт-диска. Но наличия этих файлов в каталоге \vcpp32\exl5a недостаточно. Вы должны добавить имя файла реализации в DSP-файл проекта. Выберите из меню Project в Visual C++ команду Add То Project, а из подменю — Files. Из списка файлов выберите persisth и pcrsist.cpp.
Пересоздайте файл ClassWizard, чтобы включить в проект новый класс CPersis- tentFrame. При помощи Windows Explorer удалите файл ClassWizard ex 15a.clw. Вернувшись в Visual C++, выберите пункт ClassWizard из меню View. Последуйте указаниям среды разработки Visual C++, если она потребует закрыть все файлы. На вопрос, нужно ли пересоздать CLW-файл, ответьте «Yes». Появится диалоговое окно Select Source Files. Убедитесь, что все заголовочные и исходные файлы перечислены в списке файлов проекта (Files In Project), как показано на рисунке на стр. 266.
Щелкните ОК, чтобы начать создание CI.W-файла. Обратите внимание: класс CPersistentFrame уже интегрирован в ClassWizard. Теперь Вы сможете создавать обработчики сообщений и переопределять виртуальные функции в классе CPersistentFrame.
Соберите и протестируйте приложение ЕХ15А. Измените размер и положение окна- рамки и закройте приложение. Перезапустите программу и проверьте, располагается ли окно на том же месте и сохранились ли его размеры. Поэкспериментируйте с разворачиванием и сворачиванием окна программы, затем отключите панель инструментов и строку состояния. Запомнило ли устойчивое окно-рамка новые установки?
Сохраните класс CPersistentFrame как компонент Gallery для использования в будущем. В окне ClassView щелкните правой кнопкой мыши класс CPersistentFrame и выберите из контекстного меню AddTo Galicry.Tenepb вызовите Components And Controls ■Gallery, выбрав пункт Add Tо Project из меню Project, а затем выберите Components And Controls. Обратите внимание, что среда Visual C++ создалав каталоге \ех 15а файл Persistent Frame.ogx. Измените имя этого каталога па Persistent Frame. Теперь можно добавить класс CPersistentFrame в любой проект, просто добавив файл Persistent Frame.ogx. Мы добавим CPersistentFrame в приложение ЕХ22А именно этим способам.
B-Л MyCowpuUt &(^ HKEY.CUSSES.ROOTHKEY.dJARENT.USER^AppEvenlt4 l Ш Control Panel! | Q InetAocationsMRUB-Л MyCowpuUt &(^ HKEY.CUSSES.ROOTHKEY.dJARENT.USER^AppEvenlt4 l Ш Control Panel! | Q InetAocationsMRU8 $8 Mi t8 $8 Mi tR*»R*»(DM0 icon(DM0 icon(value not set] 0x0X00000(0) ОхОСОООООЗЮ) *0013 0056 0513 0*37*(value not set] 0x0X00000(0) ОхОСОООООЗЮ) *0013 0056 0513 0*37*; Ш ^3 keyboard layout Network: Й-Sl RwroleAco»** : В Sdtwwe'jffl-OI BwcSaipt Ptogam Settmt♦Ф*^1 ProgiaetmhgVttualC**j I ем15в||-©ex15»eai0; Ш ^3 keyboard layout Network: Й-Sl RwroleAco»** : В Sdtwwe'jffl-OI BwcSaipt Ptogam Settmt♦Ф*^1 ProgiaetmhgVttualC**j I ем15в||-©ex15»eai0■St■StШ®1*е1Ш®1*е1|’-e*1Me2I|-^ex15*Scrnm*yi•-фSettingx|’-e*1Me2I|-^ex15*Scrnm*yi•-фSettingx
B-Л MyCowpuUt &(^ HKEY.CUSSES.ROOT
HKEY.dJARENT.USER^AppEvenlt
4 l Ш Control Panel
! | Q InetAocationsMRU
B-Л MyCowpuUt &(^ HKEY.CUSSES.ROOT
HKEY.dJARENT.USER^AppEvenlt
4 l Ш Control Panel
! | Q InetAocationsMRU
8 $8 Mi t
8 $8 Mi t
R*»
R*»
(DM0 icon
(DM0 icon
(value not set] 0x0X00000(0) ОхОСОООООЗЮ) *0013 0056 0513 0*37*
(value not set] 0x0X00000(0) ОхОСОООООЗЮ) *0013 0056 0513 0*37*
; Ш ^3 keyboard layout Network
: Й-Sl RwroleAco»** : В Sdtwwe'
jffl-OI BwcSaipt Ptogam Settmt
♦Ф*^1 ProgiaetmhgVttualC**
j I ем15в
||-©ex15»eai0
; Ш ^3 keyboard layout Network
: Й-Sl RwroleAco»** : В Sdtwwe'
jffl-OI BwcSaipt Ptogam Settmt
♦Ф*^1 ProgiaetmhgVttualC**
j I ем15в
||-©ex15»eai0
■St
■St
Ш®1*е1
Ш®1*е1
|’-e*1Me2
I|-^ex15*Scrnm*y
i•-фSettingx
|’-e*1Me2
I|-^ex15*Scrnm*y
i•-фSettingx
Просмотрите реестр Windows. Запустите программу regedit.exe. Перейдите в раздел HKEY_CURRENT_L7SER\Software\Programming Visual С++\ех15а. Вы должны увидеть там параме тры, аналогичные показанным на нижнем рисунке на стр. 266..,
Обра тите внимание на взаимосвязь раздела реестра и параметра «ProgrammingVisual С-+» в функции SetRegxstiyKej’.EcnH в качестве параметра SetRegisnyKe^ap,aTb пустую отроку, имя программы (в данном случае «ех 15а») будет расположено непосредственно в разделе Software.
Устойчивые рамки в MDI-приложениях
KMDI-приложениям мы перейдем только в главе 18, но если Вы пользуетесь этой книгой как справочником, то, может быть, Вам понадобится технология работы с устойчивым окном- рамкой в MDI-приложениях.
Класс CPersislentRrdme— в том виде, в каком он представлен здесь, — в MDI-приложении работать не будет, поскольку функция SbouWindow основного окна-рамки MDT-приложения вызывается не виртуальной функцией ActisateFrame, а непосредственно функцией-членом Initinstance класса приложения, Чтобы управлять характеристиками основного окна-рамки MDI-ириложения, добавьте в Initinstance необходимый кед. Однако функция ActivateFrame вызывается для объектов CzXlDlCbildWndA это значит, что MDI-приложение способно запоминать размеры и положение своих дочерних окон. Эту информацию можно сохранить в INI-файле, но при этом нужно учесть наличие нескольких окон, а для этого придется моди- фициронать класс CPersistentFrcmu<
Документ и его представление
О данной главе мы наконец рассмотрим процесс взаимодействия между документом и его представлением. Первое впечатление об этом процессе Вы получили в главе 13, когда речь шла о доставке сообщений объектам «вид» и «документ*. В этой главе Вы увидите, как объект «документ» хранит данные, обрабатываемые программой, а объект «вид» представляет их пользователю. Вы также узнаете, как объекты «документ» и «вид» обмениваются данными при выполнении программы.
В обоих примерах этой главы для классов «вид» в качестве базового используется класс CFormView.B первом, предельно простом примере документ содержит единственный объект класса CStudent, представляющий одну запись о студенте. Окно представления показывает имя и общий балл студента и позволяет их изменять. Работая с классом CStudent, Вы научитесь писать классы, отражающие реальный мир. Кроме того, Вы познакомитесь с функциями диагностического вывода библиотеки MFC 6.0.
Второй пример расширяет первый, вводя классы наборов указателей, в частности, CQbList и <^^е</Ргг!йШользуясь ими, документ хранит набор записей о студентах, а окно представления обеспечивает просмотр, вставку и удаление отдельных записей.
Функции взаимодействия «документ-вид»
Вы уже знаете, что объект «документ» содержит данные, а объект «вид» представляет их на экране и позволяет редактировать. В SDI-приложении есть класс «документ», производный от CDocument, и один или несколько классов «вид», каждый из которых, в конечном счете, происходит от CView. Документ, окно представления и остальные элементы каркаса приложений взаимодействуют весьма тесно. Чтобы в этом разобраться, надо познакомиться с пятью важными функциями-членами классов «документ» и «вид». Две из них — невиртуальные функции базового класса, вызываемые производными классами, остальные — виртуальные функции, часто переопределяемые в производных классах, Рассмотрим их по порядку.
Функция CView;:GetDocument
С объектом «вид» связан один и только один объект-документ. Функция GetDocument позволяет выяснить, какому документу сопоставлено данное окно представления. Допустим, объект «вид» получил сообщение о том, что пользователь ввел новик данные в поле ввода. Он должен уведомить об этом документ, чтобы тот обновил свои внутренние данные. GetDocument возвращает указатель на документ; через этот указатель можно обращаться к функциям-членам или открытым переменным-членам класса «документ».
ПРИМЕЧАНИЕ Функция CDocument::GetNaxtView позволяет перейти от документа к представлению, но так как у документа может быть несколько представлений, ее надо вызывать в цикле для каждого из них. Впрочем, Вы будете использовать функцию GetNextView редко — каркас приложений предоставляет более эффективный способ перебора представлений документа.
Генерируя класс, производный OTCVie«>,AppWizard создает специальную версию функции GetDocument для безопасного приведения типов; она возвращает указатель не на CDocument, а на объект производного класса. Эта включаемая в код (inline) функция выглядит примерно так: СМуОос* GetDocumentO..
{
return (CMyDoc*) m oDocument;
Встретив в коде вашего класса «вид» вызов GotDocument, компилятор использует версию из производного класса, а не из CDocument, поэтому Вам не надо приводить возвращаемый указатель к производному классу документа. Поскольку функция CView::GetDocumenrae виртуальная ,операгор:
pView->GetDocuirentO; // pView объявлен как CVlew*
вызывает функцию из базового класса, а значит, возвращает указатель на объект CDocument.
Функция CDocument: :UpdateAIIViews
Если содержание документа по какой-либо причине изменилось, надо уведомить об этом все объекты «вид», чтобы те обновили представление данных. При вызове UpdateAllViewss функции-члена производного класса документа ее первый параметр pSenderpaseaNULL Если же она вызывается посредством функции-члена производного класса «вид», параметрpSender указывает на текущий объект «вид»:
GetDocumcntt )->UocateAllViews(t‘iis);
Значение параметра, отличное от NULL, позволяет каркасу приложений не уведомлять текущий объект «вид* — предполагается, что он уже обновился сам.
У этой функции есть необязательные параметры, через которые можно передавать объекту «вид» специфичную для приложения информацию о том, какие части представления следует обновить. Как именно уведомляется объект «вид» при вызове UpdateAllViews^R ответа на этот вопрос сначала рассмотрим другую функцию — OnUpdate.
Функция CView:: OnUpdate.
Это виртуальная функция,вызываемая каркасомприложенийвответнавызоввашейпрограм- мой функции CDocumenk.LJpdateAl[V4eu,v^Вы можете вызывать ее прямо в своем клас
се, производном от CView. Обычно функция OnUpdate из производного класса обращается к документу и, получив его данные, либо обновляет переменные-члены класса «вид» или соответствующие элементы управления, либо объявляет часть представления недействительной, что заставляет функцию OnDraw перерисовать часть окна по данным документа. OnUpdate может выглядеть примерно так:
void CMyView::0TJpcate(CView* pSender, LPARAM IHint, CCbject* pHint) {
CMyDocument* pMyDoc = GetDocumentO;
CString lastName = pMyDoc->GetLastName();
m p№meStatic->SetWindowText(lastName); // m_pNameStatic - переменная-член CMyView >
Вспомогательная информация передается непосредственно через вызов UpdateAUViews. Исходная реализация OnUpdate объявляет недействительным все окно представления. В переопределенной ее версии Вы можете объявить недействительным прямоугольник меньше-
го размера — в соответствии с информацией, полученной от UpdateAUViews (через последние два параметра).
Если Фоситепг-функция UpdateAUViews вызывается с параметромpSender, указывающим на конкретный объект «вид», то OnUpdateвызывается для всех представлений документа. кроме заданного в параметре.
Функция CView::Onlnitla!Update
Эта виртуальная функция класса (View вызывается при запуске приложения, а также при выборе команд New или Open меню File, Версия OnlnitialUpdateB базовом классе CView просто вызывает OnUpdate. Если Бы переопределили ее в своем производном классе «вид», убедитесь, что она вызывает OnlnitialUpdate базового класса или OnUpdate производного класса.
Функция OnlnitialUpdate производного класса пригодна для инициализации объекта «вид» При запуске программы каркас приложений вызывает OnlnitialUpdate сразу после вызова OnCreate (если Вы задействовали OnCreate в своем производном классе «вид»). OnCreate вызывается только один раз, но OnlnitialUpdate можно вызывать сколько угодно раз.
Функция CDocument::OnNewDocument
Эту виртуальную функцию каркас приложений вызывает после создания объекта «документ > или выбора в меню File SDI-приложения команды New. Именно здесь удобнее всего инициализировать переменные-члены вашего класса «документ*. Для производного класса докумен - та AppWizard генерирует переопределенную функцию OnNewDocument. Обязательно оставьте в ней вызов функции базового класса.
Простейшее приложение на базе архитектуры «документ-вид»
Допустим, Вам не нужно несколько представлений документа, но необходима поддержка каркаса приложений для работы с файлами. Тогда забудьте о функциях UpdateAUViews и OnUpdate и придерживайтесь следующей простой схемы.
В заголовочном файле производного класса документа, сгенерированном AppWizard, объявите переменные-члены, в которых хранятся основные данные программы. Объявите их открытыми или сделайте производный класс «вид» дружественным классу документа.
В производном классе «вид» переопределите виртуальную функцию OnlnitialUpdate, Каркас приложений вызывает ее после инициализации или считывания с диска данных документа. (Работа с дисковыми файлами обсуждается в главе 17.) OnlnitialUpdate должна обновить представление, чтобы оно отражало текущее содержание документа.
Сделайте так, чтобы обработчики оконных и командных сообщений и функция OnDrawB производном классе «вид» имели прямой доступ к переменным-членам класса «документ», используя для этого функцию GetDocument.
В нашей упрощенной среде «документ-вид» события будут разворачиваться в такой последовательности:‘.
ЗапускпрограммыСоздается объект CMyDocument
Создается объект CMyView
Создается окно представления
Вызывается CMyV$ew::OnCreete(ecnii она сопоставлена этому классу) Вызывается CMyDocument: :OnNewDocument
, Вызывается CView::OnInitialUpdate
Инициализируется объект «вид»
Окно представления объявляется недействительным Вызывается CMyView: :OnDraw
Пользователь редактирует данные <7Л1уУ/еш-функции обновляют переменные-члены CMyDocument Завершение работы программы Уничтожается объект CMyView
Уничтожается объект СМуОоситепт.
Класс CFormView
CFormView — весьма полезный класс, обладающий многими свойствами немодального диалогового окна. Класс, производный от CFormView, как а производный от CDiaiog, связан с диалоговым ресурсом, определяющим параметры окна и список элементов управления. Класс CFormView поддерживает те же DDX- и DDV-функции обмена и проверки данных, что и в использовавших CDiaiog программах из главы 6.'
ПРЕДУПРЕЖДЕНИЕ Если диалоговое окно для CFormViewсгенерировано AppWizard, его свойства задаются корректно, но, создавая его самостоятельно в редакторе диалоговых окон, обязательно задайте в диалоговом окне Dialog Properties следующие свойства:
4 Style = Child (дочернее окно)
■ Border = None (без обрамления)
в Visible = флажок сброшен (изначально невидимо).
Объект CFormView получает уведомляющие сообщения непосредственно от своих элементов управления, а также принимает командные сообщения от каркаса приложений. Очевидное отличие CFormView от CDiaiog — способность первого обрабатывать команды каркаса приложений, что упрощает управление окном представления из основного меню окна-рамки или через панель инструментов.
Класс CFormView — производный от (View (точнее, от CScrolIView),a не от CDiaiog. Так что не надейтесь на присутствие функций-членов CDiaiog. В CFormView нет виртуальных функций OnlnitDialog, ОпОКи OnCancel. Функции-члены класса CFormView не вызывают UpdaieDaia и DDX-функции. Бы сами должны заботиться о вызовах UpdateData (обычно в ответ на уведомляющие сообщения от элементов управления или на командные сообщения).
Хотя CFormView происходит не от CDiaiog, он построен на основе диалогового окна Windows. Поэтому Вы мажете использовать многие функции-члены класса CDiaiog, например, Goto Dlg£tri и NextDlgCtrl-. надо всего лишь привести тип указателя на CFormView к указателю на CDiaiog Показанный ниже оператор, извлеченный из функции-члена некоего класса, производного от CFormView,устанавливает фокус на заданный элемент управления. GetDlgjtem - это <7й^«^-функция, поэтому класс, производный от CFormView, ее наследует.
((CDiaiog*) this)->GotoDlgCtrl(GetOLgItem(IOC_NAME)); ’
AppWizard позволяет' использовать CFormViews качестве базового для вашего класса «вид». Если Вы так и поступите, AppWizard сгенерирует пустое диалоговое окно с корректным набором стилей. Далее, используя ClassWizard, создайте обработчики уведомляющих сообщений от элементов управления, обработчики командных сообщений и сообщений обновления пользовательского интерфейса. (Подробнее об этом идет речь в разделе о примере ЕХ16А) Кроме того, можете определить переменные-члены и критерии проверки.
Класс CObject
Если посмотреть на иерархию MFC-классов, видно, что на ее вершине находится класс CObject, Остальные классы, кроме CStringu таких простых, как CFect и CPoint, — производные от корневого класса CObject. Класс, производный от CObject, наследует ряд важных характеристик. Многие преимущества этого станут очевидны при чтении следующих глав.
‘ Весьма опасное приведение типа, которое работает только потому, что GotoDlgCtri использует из класса CDiaiog лишь переменную-член m_bWnd.tdra переменная унаследована из CWnd,поэтому она есть и в классе CFormView. — Прим, перев.
В этой главе мы рассмотрим, как наследование от C'Otyec/позволявт задействовать объекты в организации вывода диагностической информации, а также включать их в классы наборов (collection classes).
Диагностика
В MFC-библиотеке есть рад полезных средств выдачи диагностической информации. Эти средства активизируются, если выбрать конфигурацию сборки Debug. Весь диагностический вывод направляется в окно Output отладчика. При выборе конфигурации Win32 Release вывод диагностической информации отключается, и диагностический код уже не компонуется с вашей программой.
СОВЕТ Для очистки окна диагностического вывода поместите в него курсор, щелкните правой кнопкой и выберите из контекстного меню команду Clear.
Макрос TRACE .
Вы уже встречали данный макрос в предыдущих примерах. Операторы TRACE активизируются, если определена константа JDEBUG— для этого должна быть выбрана конфигурация Debug, а переменной а/х*1УасеЕпаЬ1есЬ.ри<жоено значение TRUE. Оператор TRACE работает аналогично фупкциирг^й/языкаС, но полностью отключается в финальной (release) версии программы. Вот типичный пример:
int nCount = 9;
CString strDesc("total");
TRACE("Count = %d, Description = %s\n", nCount, (const char*) strDesc);
ПРИМЕЧАНИЕ Макрос TRACE принимает переменное число параметров и поэтому прост в использовании. Если же Вы посмотрите на исходный MFC-код, то увидите вместо TRACE макросы TRACED. TRACE1, TRACE2 и TRACES. Они принимают соответственно 0, 1,2 и 3 параметра и достались в наследство от 16-разрядной среды, где приходилось экономить пространство в сегменте данных.
Объект afxDump
Это альтернатива TRACE, более подходящая языку C++. MFC-объект afxDump принимает переменные из программы с использованием синтаксиса, аналогичного С++-синтаксису объекта потокового вывода cout. Применение сложных форматных строк не требуется — вместо этого формат вывода управляется переопределяемыми операторами. Вывод afxDump направляется туда же, куда и вывод TRACE, но объект afxDump определен только в отладочной версии MFC-библиотеки. Вот типичный потокоориентированный диагностический оператор, который дает тот же результат, что и приведенный выше макрос TRACE:
int nCount = 9;-
CString strDesc("total");
fiifdef „DEBUG
afxDump « "Count = “ « nCount « " , Description = " « strDesc « "\n";
ftendif
Хотя и afxDump и cout используют один и тот же оператор вставки («), код их реализации различен. Объект cout является частью библиотеки iostream Visual C++, a afxDump — частью MFC-библиотеки. Не думайте, что какие-то возможности форматирования, обеспечиваемые cout, доступны при работе с afxDump.■• '"
Классы, не производные от CObject, такие, как CString, CTime и CRect, имеют свои переопределенные операторы вставки для объектов CDumpContext. Класс CDumpContext, экземпляром которого является afxDump, включает переопределенные операторы вставки для базовых типов C++ (int, double, char" и т. д). Кроме того, в нем есть переопределенные операторы для ссылок и указателей на CObject — они-то и представляют для нас интерес.
Классы CDumpContext и CObject
Если оператор вставки класса CDumpContext принимает указатели и ссылки на CObject, он должен также принимать указатели и ссылки на производные от того классы. Рассмотрим тривиальный класс CAction, производный от CObject:
class CAction : public CObject
public:.
int m nTime;
}; "
Что же происходит при выполнении следующего оператора?'
flifdef .DEBUG
afxDump « action; // action - объект класса CAction.
fiend if
Происходит вот что. Вызывается виртуальная функция CObject::Dump. Если Вы не переопределили ее для CAction, то многого от нее не получите — разве что адрес объекта. Однако, переопределив Dump, можно вывести внутреннее состояние интересующего нас объекта. Посмотрим на функцию CActionoDump-..
flifdef .DEBUG
void CAction; :Oump(CDumpContext& de) const
■i
CObject::Dump(dc); // всегда вызывайте функцию базового класса
de « "\ntime = " « m nTime « "\n";
}
ffendif
Функция Dump базового класса (CObject) выводит строку наподобие следующей:
a CObject at $4498
Если в определении класса CAction использован макрос DECLARE-DYNAMICя в реализации — IMPLEMENT DYNAMICEp при выводе диагностической информации Вы получите имя данного класса:
a CAction at $4498
даже если оператор отладочного вывода выгладит так:
flifdef .DEBUG
afxDump « (CObjectS) action;
ftendif
Вместе эти два макроса вставляют в ваш класс, производный от CObject, специальный код библиотеки MFC периода выполнения. При наличии такого кода программа в период выполнения может определить имя класса объекта (например, для отладочного вывода) и получить информацию об иерархии классов.
ПРИМЕЧАНИЕ Пары макросов (DECLARE_SERIAL, IMPLEMENT-SERIAL) и (DECLARE_DYNCREATE, IMPLEMENT_DYNCREATE)o6ecn&WBaKH ту же поддержку периода выполнения, что и пара макросов (.DECLARE-DYNAMIC, IMPLEMENT-DYNAMIC).
Автоматическая диагностика неуничтоженныхобъекгов
Если при сборке программы выбрана конфигурация Debug, после завершения программы каркас приложений сообщает обо всех неуничтоженных объектах. Эта диагностика очень полезна; тем не менее, аккуратно удаляйте все объекты — даже те, что обычно сами исчезают при завершении программы. Такая очистка — хороший стиль программирования.
ПРИМЕЧАНИЕ Код, добавляющий отладочную информацию в выделенные блоки памяти, теперь находится не в MFC-библиотеке, а в отладочной версии С-библиотеки периода выполнения (CRT). Если Вы решите загружать МЕС динамически, то вместе с необходимыми DLL-модулями MFC-библиотеки будет загружаться и MSVCRTD.DLL. Если Вы добавите в начало СРР-файла строку:
ttdefine new DE3UG_NEVJ
CRT-библиотека покажет имя файла и номер строки, в которой был распределен данный блок памяти Эту строку AppWizard вставляет в начало всех сгенерированных им СРР-файлов.
Г' Создание оконных подклассов для расширенного управления вводом данных: ,-
У Как быть, если нужно поле ввода (в диалоговом окне или; окне представления в виде ;. формы), которое допускает ввод только чисел? Бы просто устанавливаете стиль Number г' $ в окне свойств элемента управления. Однако если Вам нужно исключить цифровые. , ; . символы или изменить регистр букв, придется немного цоцрограммировать. ..
,Библиотека MFC позволяет легко изменить поведение любого стагухартного элемента
управления, включая поле вводи. Для этого существует несколько способов,, Можно со- ’здать свои классы, производные от CEcKi. CListBoxyT. д (со своими фующидап^ббра- . . ботчиками сообщений), а затем динамически формироватьуправляющие объекты. Или, как это сделал бы программиств Win32, зарегистрировать специальный оконныйкласс и интегрировать его в файл ресурсов проекта, используя текстовый редактор. Однако ;
-. ни один из указанных способов,йе позволяет размещать такие элементы управления в 1 . диалоговом ресурсе с помощью" редя ктора диалоговых окоп.-. ‘ , Ф;.:. ф ' .:; s ""
,- Самый простой способ изменить поведение элемента управления — воепользовать- ;
' ся инструментом MFC-бйблйотеки, предназначенным для создания оконных иодкЛас-
.. сов (window subclassing). 'При использовании редактора диалоговых окон в диалоговом ■ ресурсе размещается обычный элемент управления, а потом на (УЦ .пишется новый класс, содержащий с^брабфгчшаи сообщений для тех событий, которые Вы хотите об
- рабатывать самостожельнр. Процесс создания подкласса поля ввода таков..: '
Используя редактор диалоговых окон, расположите поле ввода в диалоговом ресур- . се, Пусть его идентификатор дочернего окна будет 1DC_EDIT1. ... ’
. : 2 Создайте новый класр (например, С^окА'мттс1М?)дроизводт anCEdit. Напиши
? . те обработчик сообщения ’WMCCHAR, скажем, такой; " /.' . J' /.1
«old СМ>л|1итег1сЕб1т::Фп(&йгДО^ UINT cftepCnt, ШИТ айвда). .' ■A '
) "I ••• . Ч .,Ч.--;'; . L, -. ...-Ч:-...
lf(t isdiglt(nQhar.)>..{, / ,. ■ : .. . . • . i, . :.
т г GEdit::OhGhiar(hG^." (?М|бР^> nFlaga);;=
■ 3 В заголовке производного класса диалогового окна или формы объявите перемен-
t: Л Ч .t: Л Ч .:; нукичлед класта CNS^utiSeticEdit-.
t: Л Ч .
t: Л Ч .
; a i priyatM У . к-М < J1/' 'Д Д^- >’ ■
' .. ' "Qt^Nijm^
' 4< Если Вы работаете с^дп1^сум.диалоговаго окна, добавьте-в переопределенную фун- v; •■■ кцию OujnitDialog строку: ‘ -: '• « - а ., ^ .. ■ . / . . ■ ^. ..
‘ ’«LoonNumeiacEdiT^SuhclassS^■™'* 9 ?■ Ч''--• . ? .
■5.' Если же Вы работаете с классам формы, добавьте в переопределенную функцию Оп-
. гАя^&^^етакой код -и-: ■;..Ji®, .^^if?. ..т
: :-mJionNumeHcEdft?&ib&iiib^^
‘ Функция-член SubdassOZgitj^riKJiacca СТ^^гарднтйруёт^ что все ©^ббй^Шя, прежде •чем дойти до встроенной оконйой"процедуры элемента ^рЬвцей^г; ftpg^dyy через ■ организуемую каркасом Приложений систему распределения сообщений. Такой приемУ называется динамической созданием подклассов (dyriamicrshbtiatslh^у.описан в Technical Note *14 электронной до^бгентащш. ?^1 ?: ? ‘7 •к* Приведенный код только проверяет символы и отвергает недопустимые. Ве&й Же' надо изменить значение символа, обработчик должен вызватьпозволит обойти стандартное поведение МГСгк&гда значения параметров хранятся;®; .1?Иеременных объекта «поток кодак Вот пример обрабётчика, который пврев<^&1.в&?. *;' : б^в верхний регистр: ' : :'!. > • У ’ .; ’. 7 * " " ' ' " ' '' 1 ’ * ’"'"'лф!б i^ppet^tiiQi^ n6$a,h Ч1Ьт ЙёрЖ^Й|^9Д .^ j‘ Функция-член SubdassOZgitj^riKJiacca СТ^^гарднтйруёт^ что все ©^ббй^Шя, прежде •чем дойти до встроенной оконйой"процедуры элемента ^рЬвцей^г; ftpg^dyy через ■ организуемую каркасом Приложений систему распределения сообщений. Такой приемУ называется динамической созданием подклассов (dyriamicrshbtiatslh^у.описан в Technical Note *14 электронной до^бгентащш. ?^1 ?: ? ‘7 •к* Приведенный код только проверяет символы и отвергает недопустимые. Ве&й Же' надо изменить значение символа, обработчик должен вызватьпозволит обойти стандартное поведение МГСгк&гда значения параметров хранятся;®; .1?Иеременных объекта «поток кодак Вот пример обрабётчика, который пврев<^&1.в&?. *;' : б^в верхний регистр: ' : :'!. > • У ’ .; ’. 7 * " " ' ' " ' '' 1 ’ * ’"'"'лф!б i^ppet^tiiQi^ n6$a,h Ч1Ьт ЙёрЖ^Й|^9Д .^ jг ?А Лг ?А Л:‘if Clslower(nChar)) {. /i-/^ \-:., nChar= toupper(nChar)i^ . . . ;^-к - ■ ■., . ■ -,. ' и ;' -^^пёеиРго^ . (М^ЮУ-вШ,; ;; ■ L;“ - УЛ ki- • : (LPARAM) (riRepCnt Г (rtFlags «Ш:‘if Clslower(nChar)) {. /i-/^ \-:., nChar= toupper(nChar)i^ . . . ;^-к - ■ ■., . ■ -,. ' и ;' -^^пёеиРго^ . (М^ЮУ-вШ,; ;; ■ L;“ - УЛ ki- • : (LPARAM) (riRepCnt Г (rtFlags «Ш2_2_U-U-Л|;Л|;
‘ Функция-член SubdassOZgitj^riKJiacca СТ^^гарднтйруёт^ что все ©^ббй^Шя, прежде •чем дойти до встроенной оконйой"процедуры элемента ^рЬвцей^г; ftpg^dyy через ■ организуемую каркасом Приложений систему распределения сообщений. Такой прием
У называется динамической созданием подклассов (dyriamicrshbtiatslh^у.
описан в Technical Note *14 электронной до^бгентащш. ?^1 ?: ? ‘7 •к
* Приведенный код только проверяет символы и отвергает недопустимые. Ве&й Же
' надо изменить значение символа, обработчик должен вызвать
позволит обойти стандартное поведение МГСгк&гда значения параметров хранятся;®; .1?
Иеременных объекта «поток кодак Вот пример обрабётчика, который пврев<^&1.в&?. *;
' : б^в верхний регистр: ' : :'!. > • У ’ .; ’. 7 * " " ' ' " ' '' 1 ’ * ’"'"'
лф!б i^ppet^tiiQi^ n6$a,h Ч1Ьт ЙёрЖ^Й|^9Д .^ j
‘ Функция-член SubdassOZgitj^riKJiacca СТ^^гарднтйруёт^ что все ©^ббй^Шя, прежде •чем дойти до встроенной оконйой"процедуры элемента ^рЬвцей^г; ftpg^dyy через ■ организуемую каркасом Приложений систему распределения сообщений. Такой прием
У называется динамической созданием подклассов (dyriamicrshbtiatslh^у.
описан в Technical Note *14 электронной до^бгентащш. ?^1 ?: ? ‘7 •к
* Приведенный код только проверяет символы и отвергает недопустимые. Ве&й Же
' надо изменить значение символа, обработчик должен вызвать
позволит обойти стандартное поведение МГСгк&гда значения параметров хранятся;®; .1?
Иеременных объекта «поток кодак Вот пример обрабётчика, который пврев<^&1.в&?. *;
' : б^в верхний регистр: ' : :'!. > • У ’ .; ’. 7 * " " ' ' " ' '' 1 ’ * ’"'"'
лф!б i^ppet^tiiQi^ n6$a,h Ч1Ьт ЙёрЖ^Й|^9Д .^ j
г ?А Л
г ?А Л
:‘if Clslower(nChar)) {. /i-/^ \-:.
, nChar= toupper(nChar)i^ . . . ;^
-к - ■ ■., . ■ -,. ' и ;
' -^^пёеиРго^ . (М^ЮУ-вШ,; ;; ■ L;
“ - УЛ ki- • : (LPARAM) (riRepCnt Г (rtFlags «Ш
:‘if Clslower(nChar)) {. /i-/^ \-:.
, nChar= toupper(nChar)i^ . . . ;^
-к - ■ ■., . ■ -,. ' и ;
' -^^пёеиРго^ . (М^ЮУ-вШ,; ;; ■ L;
“ - УЛ ki- • : (LPARAM) (riRepCnt Г (rtFlags «Ш
2_
2_
U-
U-
Л|;
Л|;
Оконные подклассы можно также использовать для обработки ^/^мжг (ref-
lected) сообщений, о которых говорилось в главе 6. Если оконный MFC-класс не смо-, жет сопоставить сообщению от одного из дочерних элементов управления обработчик, 1 ■ -каркас приложений возвратит сообщение обратно этому элементу. Подробнее об этом yt
говорится в Technical Note *62 электронной документации.;:\‘ :' ‘' ' .
. Если Вам требуется поле ввода, например, с желтым цветом фёна, создайте класс ^ СУё^ой^г^производный от CEdit, и, используя ClaKsWizard; сопоставьте в CYeHowEtHico- - & Общению =WM^CTLCOLQR обработчик. (ClassWizard помещает перед именем возвра- г, щенного сообщения знак равенства.) Обработчик практически не отличается от обра-7 ботчика обычного сообщения 'WM CTLCOLOR из главы 6. (Переменная mJjYellotcBntsb :: инициализируется в конструкторе класса элемента управления.) ;Л • - - ,
^HSRIiSfl CYelldwE^-
k 'к к.кл' vs - У - VV к
ppC->SetBkColor(RGB.k255, 285, 0"У); // желтей, ■' ' .;7 ^
' Return m hYellowBrysh; ; ■„У. ’V
Пример ЕХ16А
Первый из двух примеров этой главы иллюстрирует простейший случай взаимодействия «документ-вид». У класса документа CExl6aDqc, производного от CDocumeni, есть единственный внедренный объект CStudctU. Класс CStudeut cooтиегствуст одной записи о студенте, состоящей из имени (.CStrin^M целочисленного балла. Класс «вид* CExl6aView,производный от ('FonnView,визуально представляет запись о студенте и содержит поля ввода имени и балла. Кнопка по умолчанию Enter обновляет данные документа в соответствии с содержимым полей ввода. Окно программы ЕХ16А показано на рисунке 16-1.
Код класса CStudeut приведен в листинге на рисунке 16-2. Большинство возможностей данного класса предназ! шчено для программы EX 16 А, но некоторые из них пригодятся как в ЕХ16В, так и в программах-пример ах 1лавы 17. А пока обратите внимание на переменные-
Члены, конструктор по умолчанию и на объявление функции Dump. Макросы DECLARE'_DYNA MIC и IMPL^MENl_DYNAMIG?apaHrap^Tarr, что имя класса будет доступно при выводе диагностической информации.
Рис. 16-1. Программа ЕХ16А в действии
STUDENT. Н
// student.h
#lfndef INSIDE VISUAL CPP STUDENT
«define JNSIDE.VISUAL. CPP.STUDENT
class CStudent : public CObject
{
DECLARE_DYNAMIC(CStudent)
public:
CString m_strName;
int m_nGrade;
CStudent()
{
r._nGrade = 0; !
CStudent(const char-* szName, long nGrade) : m_strName(szName) <
m_nGrade = nGrade;
}
CStudent(const CStudent& s) : m strName(s.m strName)
// конструктор копии
m_nGrade = s.m nGrade;
}
const CStudentA operator =(const CStbdent& s)
m_strName = s.m_strName;
fli_nGrade = s.m_nGrade;
return «this;
}
1Рис. 16-2. Листинг кода класса CStudent
BOOL operator == (const C5tudent& s) const 1 {
■ if ((m.strName == s.m.strName) && (m_nGrade== s.m.nGrade)) { return TRUE;
else {.
return FALSE;
}
BOOL operator !=(const CStudent& s) const
// задействуем только что определенный оператор! return ! («this == s);
}
flifdef.DEBUG
void Dump(CDumpContext& de) const; tfendif
#endif И .IHSIDE.VISUAL.CPP.STUDENT
STUDENT.CPP
ttinclude "StdAfx.h"
tfinclude' student, h'
IMPLEMENT DYNAHIC(CStudent, CObject)
#ifdef .DEBUG
void CStudent: :Dump(CDumpContext& do) const
CObject::0ump(dc):
de « "\nm_strName = ” « m.strNanio « "\nm_nGrade - " « m_nGrade; iendif // .DEBUG
Процесс создания программы EX16A.
Используя AppWizard, создайте проект \vcpp32\exl6a\exl6a. На этапе 6 AppWizard замените базовый класс на CFormVieu?,KaK показано ниже на рисунке.
Ml С /MjpWKdfd ■ Hl«p Ч of 6Ml С /MjpWKdfd ■ Hl«p Ч of 6|Cbfl6*pp CM«nFf«w ICbcISaDoc|Cbfl6*pp CM«nFf«w ICbcISaDoc1М«т1М«т
Ml С /MjpWKdfd ■ Hl«p Ч of 6
Ml С /MjpWKdfd ■ Hl«p Ч of 6
|Cbfl6*pp CM«nFf«w ICbcISaDoc
|Cbfl6*pp CM«nFf«w ICbcISaDoc
1М«т
1М«т
[CHtaAT** ICUtMew • CRichE<ilVhw CScroMew |C7rwV*w СУ**
Параметры и имена классов по умолчанию:
Используя редактор меню, замените команды в меню Edit. Удалите прежнее содержимое меню Edit и вставьте команду Clear АН, как показано ниже на рисунке.
Используйте константу по умолчанию 1В_ЕОГГ_СГ£АЕ_А1Х,назна.ченнхдо каркасом приложений. Подсказка об этом элементе меню появится автоматически.
В редакторе диалоговых окон измените диалоговое окно IDDEX1 6A_FORMOt- крыв диалоговый ресурс IDD_EX16A_FORM,сгенерированный AppWizard, добавьте элементы управления, как показано ниже на рисунке.
Убедитесь, что свойства на вкладке Style установлены именно так, как показано в диалоговом окне Dialog Properties на предыдущей иллюстрации (Style = Child; Border = None), a флажок Visible сброшен.
Присвойте элементам управления следующие идентификаторы.
Элемент управления
Идентификатор‘
Поле ввода Name
IDCJMME
Поле ввода Grade
IDCjGRADE
Кнопка Enter
ЮС_ЕЕТЕК
Используя ClassWizard, добавьте обработчики сообщений для CExl6aView.Bbi6e- риге класс CExi6aVietun добавьте указанные ниже обработчики сообщений. Оставьте ' имена функций по умолчанию.
Идентификатор объекта
Сообщение
Имя функции-члена
IDC_ENTER
BN_CUCKED
OnEnter
ID EDIT CLEAR ALL
COMMAND
OnEditClearAll
ID_EDIT_CLEAR_ALL
UPDATE_COMMAND_UI
OnUpdateEditClearAl£
Используя ClassWizard, добавьте переменные для CExl6a¥iew.B диалоговом окне MFC ClassWizard щелкните ярлычок Member Variables и добавьте следующие переменные.
Элемент управленияИмя переменнойКатегорияТип переменной
!DC_GRADEmjiGradeValueint
IDCJNAMEmstrNameValue CSlring.
Задайте для м мбгдг^минимальное значение 0, максимальное — 100. Заметьте, что ClassWizard генерирует код, необходимый для проверки данных, вводимых пользователем.
Добавьте прототип вспомогательной функции UpdateControlsFromDocB окне ClassView щелкните правой кнопкой мыши CExlбяУтегии выберите команду Add Member Function (добавить функцию-член). Заполните диалоговую форму, чтобы добавить следующую функцию:
private:
void UpdateControlsFromDacO;
Отредактируйте файл Exl6aView.cpp. Арр Wizard генерирует заготовку функции Onlni- tialUpdate, a ClassWiard — функции UpdateControlsFromDocnacneflHm представляет собой вспомогательную (helper) функцию-член, которая переносит данные из документа в переменные-члены CExl6aView,a затем в элементы управления диалогового окна. Отредактируйте код, как показано ниже:
void CEx16aView:;OnInitialUpdate()
{ // вызывается при запуске
UpdateControlsFromDoc():
void CEx16aView::UpdateControlsFromDoc()
( // вызывается из OnlnitialUpdate и OnEditClearAll
CEx16aDoc* pDoc = GetDocumentQ;
m_nGrade = pDoc->m_student.m_nGrade;
m etrName = pDoc->e student.e_strName;
UpdateData(FALSE); 7/ вызов DDX■
OnEnter заменяет функцию OnOK, которую можно было бы ожидать в классе диалогового окна. Она передает данные из полей ввода в переменные-члены объекта «вид», а потом в документ. Добавьте код, выделенный полужирным:
void CEx16aView; :OnEnter(),.
CEx16aDoc« pDoc = GetDocument();
UpdateData(TRUE);-' '
pDoc->m_student.e_nGrade = m_nGrade;
pDoc->m student.■ strName = m strName;
}"*:
В сложной программе, допускающей несколько представлений данных, команда Clear All передавалась бы непосредственно документу. В нашем же простом примере она доставляется объекту «вид». Обработчик сообщения обновления пользовательского интерфейса отключает элемент меню, если объект CStudent в документе уже пуст. Введите код, выделенный ПОЛуЖИрНЫМ:
void CEx16aView; :OnEditClearAll()
GetDocuBent()->m_etudent = CStudent(); // "пустой" объект.
UpdateControlsFromDocO;"
void CEx16aView: :OnUpdateEditClearAll(CCrdUI* pCmdUI) {
pCmdUI->Enable(GetDocument()->m student != CStudentO); // пустой?
}
Добавьте в проект ЕХ16А файлы класса CStudent. Из меню Project выберите Add То Project (добавить в проект), из подменю — Files (файлы), а затем выберите заголовочный и исходный файлы StudentJi и StudenLcpp. Visual C++ добавит имена этих файлов в DSP- файл проекта, так что при сборке проекта они будут автоматически скомпилированы.
Добавьте переменную типа CStudentu класс СЕх1баВос.Это можно сделать, используя Class View, тогда директива ^include будет добавлена автоматически.
public:
CStudent m_student;
Конструктор класса CStudent вызывается при создании объекта «документ», а деструктор вызывается автоматически при уничтожении документа.
Отредактируйте файл Exl6aDoc.cpp. Для инициализации объекта CStudent воспользуйтесь конструктором CExl6aDoc;"
CEx16aDoc: :CEx16aDoc() : m_student("3Ha4eHMe по умолчанию", 0) <
ТЙАСЕ("Объвкт "документ" создан\п");.
Мы не определим, правильно ли работает программа ЕХ16А, пока не выведем содержимое документа по завершении программы. Для этого воспользуемся деструктором, который вызовет функцию Dump документа, а та — функцию CStuderH:: Dump.
CEx16aDoc::'CExl6aDoc()•-.
tfifdef „DEBUG.
Dump(afxDump);
#endif
)
void CEx16aDoc; :Dump(CDjmpContext& de) const
{
CDocument::Dump(dc);
de « "\n" « m_student « "\n";
Соберите и протестируйте приложение ЕХ16А. Введите какое-нибудь имя и балл, а затем щелкните Enter. Закройте приложение. Появились ли в окне Debug сообщения, аналогичные п оказ энным ниже ?
a CEx15aDoc at $4472
m_strTiTle = Untitled
m_strPathName =.
m_bHadified = 0'
m_pOacTempiate = $438E
a CStudent at $4498
m_strName = Салливан, Уолтер
m_nGrade = 0
ПРИМЕЧАНИЕ Чтобы увидеть эти сообщения, скомпилируйте программу для конфигурации Win32 Debug и запустите ее под управлением отладчика.
Усложненное взаимодействие «документ-вид»
Если Вы прорабатываете логику программы, поддерживающей множественное представление данных, взаимодействие «документ-ВИД» должно быть сложнее, чем в примере ЕХ16А. Основная проблема состоит в том, что пользователь редактирует данные в одном окне представления, а остальные окна представления надо обновлять, чтобы отразить внесенные изменения. Здесь понадобятся функции UpdateAUViewss. OnUpdate, поскольку документ будет Выступать в качестве координирующего центра при обновлении окон представления. При разработке придерживайтесь следующей схемы.
В созданном AppWizard заголовочном файле производного класса документа объявите переменные-члены документа. Если хотите, сд елайте их закрытыми, а затем определите функции-члены для доступа к НИМ, или объявите класс «вид» дружественным классу «документ».
В производном классе «вид», используя ClassWizard, переопределите виртуальную функцию-член OnUpdate. Каркас приложений вызывает ее при любом изменении данных в документе. OnUpdate должна так изменить объект «вид», чтобы он отражал текущее содержание документа.
Проанализируйте используемые Вами командные сообщения. Для каждого определите, относится оно к документу или к его представлению. (Пример команды, относящейся к документу, — Clear АН в меню Edit.) Теперь сопоставьте команды соответствующим классам,
Предусмотрите обновление данных в документе соответствующими функциями-обработчиками командных сообщений в классе «вид». Убедитесь, что все они перед завершением обращаются к CDocument::UpdateAWiewsSyra. доступа к документу пользуйтесь версией функции-члена CView::GetDocument, обеспечивающей безопасное приведение типов.
Запрограммируйте обновление данных документа соответствующими функциями-обработчиками командных сообщений в классе документа. Убедитесь, что все они перед завершением тоже вызывают CDocument::UpdateAllViews.
Последовательность событий для сложного взаимодействия «документ-вид» показана ниже.
Запускается приложениеСоздается объект CMyDocument
Создается объект CMyView
Создаются другие объекты «вид»
Создаются окна представлений
Вызывается CMyView::OnCreate (если сопоставлена)
Вызывается CDoc«ment:,-OnNeu>Document
Вызывается CView::On!nitialUpdate
Вызывает CMyView:.-OnUpdate
Инициализирует объект «вид»
Пользователь выбирает команду,СЛ/у^иг-функции обновляют переменные-члены
относящуюся к представлению данныхCMyDycumeni
Вызывают функцию GDocumenluUpdateAUViems
Вызываются функции ОнПр&мвдяя других объектов «вид» Пользователь выбирает команду,Функции С Му Document обновляют переменные-члены
относящуюся к документуВызывают функцию CDocument-L’pclateAHViews
Визы вается CMyVietu;: Oi i Update
Вызываются функции Опирамедля других объемов «вид»
Пользователь завершает приложение Уничтожаются объекты «вид»
Уничтожается объект CMyDocumeni
Функция CDocument::DeleteContents
В какой-то момент Вам понадобится функция, чтобы удалить содержимое документа. Вы могли бы написать свою закрытую функцию-член, но каркас приложений уже определил для этой цели виртуальную функцию DeleteContentsв классе CDocument. Каркас приложений вызывает вашу переопределенную функцию DeleteContents при закрытии документа и, как Вы увидите в следующей главе, в ряде других случаев.
Класс набора CObUst"
Познакомившись с классами наборов, Вы удивитесь, как до сих пор без них обходились. Один из представителей этого семейства классов — CObList. Освоив его, Вы легко разберетесь в других классах списков (list classes), а также классахмассивов (array classes) и классах слова рей (map classes). '
Может быть, Вам показалось, что наборы — это что-то новое? Однако язык программирования С всегда поддерживал одну из их разновидностей — массивы. Размер массивов в языке: С фиксирован, они не поддерживают вставку новых элементов. Многие программисты на <" писали библиотеки функций для других наборов, включая связанные списки (linked lists), динамические массивы (dynamic array) и индексируемые словари (indexed dictionaries). В языке1 C++ есть очевидная и более удачная (по сравнению с библиотекой С-функций) альтернатива по реализации наборов — это классы. Например, объект-список корректно инкапсулируй внутренние структуры данных списка.
Класс CObList поддерживает упорядоченные списки указателей на объекты классов, производных от CObject. Другой MFC-класс наборов, CPtrList, вместо указателей на CObject храпит указатели void. Почему бы ни использовать его вместо CObList? Класс CObList имеет ряд преимуществ при выводе диагностической информации, в чем Вы убедитесь в этой главе, и при сериализации (serialization), с которой Вы познакомитесь в главе 17. Одно из важных свойств CObList — способность хранить смешанные указатели (mixed pointers). Другими словами, набор CObList может одновременно содержать указатели как на объекты CStudent. так и на объекты CTeacber, если оба эти класса производны от CObject.
Применение класса CObList для создания списков типа FIFO
Одно из простейших примеров применений объекта CObList — добавление новых элементов в конец и выборка элементов из начала списка. Элемент, первым внесенный в список, будет первым и извлечен из него (принцип FIFO — First In, First Out, т. e. «первым вошел, первым вышел»). Допустим, элементы списка — объекты созданного Вами класса CAction, производного от CObject. Приведенная ниже программа, работающая в режиме командной строки, заносит в список пять элементов, а затем извлекает их в том же порядке.
ttinclude <afx,l>
tfincliide Ofxcoll. h>
class CAction : public СОbjeer
private:.
irt m.nTime;
public:
CAction(int nTimc) { m_nTire = ti~c; } // крнс_руктор запоминает
// цело ^crciiHbo по<азани?. Epe^ci-.и
void PrintTimeO { lRACC("time = $d\n", m_nlime); }
int uain()
{
CAction- pAction;
CObList actionList; // список CAction создается на отеке int i;
// вставляем объекты CAction в порядке от 0 до 4
for(i = 0; 1 < 5; i++) {
pAction = new CAction(i);
ocrionList AddTail(pActlor); // приведение типа для pAction не требуется
}
// извлекаем и удаляем пЬъеки^ CAction в порядке от 6 до 4
while (lactlonlist: IsEirptyO) {
pAction =// для возвращаемого знамений
(CAction*) uclknlist. RenoveHcadr);// необходимо приведение типа
pAction >DrintTime();
delete pAction:
}
return 0:
}'
Вот что происходит в программе, Сначала создается объект actionList класса CObList. Затем при помощи функции-члена CObList::AddTail'B конец списка добавляются указатели на вновь создаваемые объектырЛейои. Преобразование типа для pAction не требуется, так как параметром AddTaii служит указатель на CObject, а pAction ~ указатель на производный от него класс. Далее указатели на объекты CAction извлекаются из начала списка и объекты удаляются. Б данном случае для возвращаемого значения РетоуеНесм^несбхпдамо приведение типа, поскольку эта функция возвращает указатель на класс CObject, расположенный в иерархии классов выше класса CAction.
Когда Вы извлекаете указатель на объект из набора, сам объект автоматически не уничтожается. Удалять объекты CAction нужно оператором delete.
Перебор элементов CObList: переменная типа POSITION
Предположим, Вам надо «пройти» все элементы списка. Класс CObList предусматривает функцию-член GetNext, которая возвращает указатель на «следующий» элемент списка, но принцип се использования несколько необычен. GfetiVexf принимает целочисленный параметр типа POSITION*. Он содержит внутреннее представление положения в списке извлекаемого элемента. Так как параметр POSITION передается по ссылке (&), функция может изменить его значение.
GetNext выполняет следующие операции.
Возвращает указатель па «текущий» элемент списка, который определяется передаваемым ей значением POSITION.
Изменяет значение пураметра POSITION так, чтобы ом соответствовал положению следующего элемента списка.
Вот как выглядит цикл с GetNext, если Вы используете список из предыдущего примера.
CAction* pAction;
(POSITION pos = actionList.GetHeadPosition();’
while (pcs 1= NULL) {
pAction = (CAction*) actionList.GetNext(pos);
pAction->PrintT1me();
}
Теперь допустим, что у Вас есть интерактивное Windows-приложение, в котором для перемещения вперед и назад по элементам списка применяются кнопки панели инструментов. Для выбора очередного элемента GetNext не годится, потому что она всегда «увеличивает» значение переменной POSITION, а предугадать, следующий или предыдущий элемент потребуется пользователю, просто невозможно. Взгляните на пример обработчика командного сообщения в классе «вид», который выбирает следующий элемент списка. Здесь mactionList— объект типа CObList, внедренный в класс CMyView,a т„position — переменная типа POSITION, хранящая положение текущего элемента списка.
void CMyVicwf: :OnCommandNext()
POSITION pos;
CAction* pAction;
if ((postposition) != NULL) {
m_actianList.GetNext(pos);
if (pos != NULL) { // pos равно NULL в конце списка
pAction = (CAction*) actionList.GetAt(pos);
pAction->PrintTi.te():
m position = pos;.
}
else {
AfxMessageBox("Достигнут конец списка"):
}
)
Чтобы увеличить переменную положения, вызывают GetNext, а чтобы получить элемент, используют функцию CObUsl;;GetAt, Переменная т„position обновляется, только если конец списка еще не достигнут.
Класс-шаблон набора CTypedPtrUst
Класс CObList отлично работает, когда нужен набор, содержащий смешанные указатели. Однако если требуется набор с безопасным приведением типов, содержащий указатели только на один тип объектов, используйте классы-шаблоны наборов указателей из MFC-библиотеки. Пример такого класса — CTypedPtrUst. Шаблоны (templates) — относительно новый элемент C++, впервые появившийся в Microsoft Visual C++ 2.0. CTypedPtrList — это класс-шаблон, который можно использовать для создания списка указателей на объекты любого заданного класса. Не вдаваясь в подробности, скажем только, что шаблон применяют для создания нового класса списка, производного либо от CPtrList, либо от CObList
Чтобы объявить объект для указателей на CAction, напишите:
CTypedPtrList< CObList, CAction* > m_actionList;
Первый параметр — это базовый класс для набора, второй — тип параметров и возвращаемых значений. В качестве базовых допускаются только классы CPtrList и CObList, так как других классов наборов указателей в библиотеке MFC нет. Если Вы храните объекты классов, производных от CObject, используйте как базовый класс CObList-, в ином случае — CPtrList.
При таком использовании шаблона, как показано выше, компилятор гарантирует, что все функции-члены списка возвращают указатель CAction. Таким образом, можно писать:
pAction = r>_actionLisT.GctAt(pos); Ц приведение типа не требуется
Чтобы слегка упростить запись, используя typedef, сгенерируем некое подобие самостоятельного класса:
typedef CTypedPtrLisK CObList, CAction* > CActionlist;
и тогда m_actionList можно объявить так:
CActionlist m_a.ctiorList;
Д иагностика и классы наборов
Функция Dump для CObList и других классов наборов обладает одним весьма полезным свойством. Вызвав Dump для какого-либо объекта-набора, Вы получите информацию о каждом объекте из набора. Если в классах объектов, составляющих набор, применены макросы DE- CLAREJDYNAMIC и IMPLEMENT_DYNAMI(j^ информации появится имя класса каждого объекта.
По умолчанию функции Dump для наборов выводят только имена классов и адреса объектов в наборе. Чтобы функции Dump для наборов вызывали функцию Dump для каждого элемента набора, где-то в начале программы надо сделать вызов:
#ifdef „DEBUG
afxOunp. SetDeptKl);
ttendif.
тогда оператор
ftifdef „DEBUG __
afxDump « actionList;
ttendif
будет выводить информацию примерно так:
a CObList at $4А88
with 4 elements
a CAction at $4005 time = 0
a CAction at $5632
time = 1
a CAction at $568E time =2
a CAction at $56EA time = 3
Если в наборе содержатся смешанные указатели, для класса объекта вызывается виртуальная функция Dump и выводится имя соответствующего класса.
Пример ЕХ16В
Этот пример расширяет программу ЕХ16А следующим образом.
Вместо одного объекта CStudent документ содержит список таких объектов. (Теперь Вы понимаете, почему мы использовали класс CStudentBMecro того, чтобы сделать m_strName н щ_и(7г<мЬ?переменными-членами класса «документ».)
Кнопки на панели инструментов позволяют перемешаться по списку.
Структура программы допускает дополнительные представления данных. Команда Clear All . теперь передается объекту «документ», и поэтому в игру вступают функции UpdateAHViews для документа и OnUpdatep,n$L его представления.
Код, специфичный для представления информации о студентах, изолирован с тем, чтобы класс Cfixi 6bV№WM.0QKH0 было впоследствии трансформировать в базовый, содержащий только универсальный код. Производные классы могут переопределять отдельные функции для работы со списками объектов, специфичными для приложения.
Окно программы ЕХ16В (см. рис. 16-3) несколько отличается от окна программы ЕХ16А (см. рис. 16-1). Кнопки активны, только когда это допустимо. Например, если текущая позиция соответствует концу списка, кнопка Next (со стрелкой, направленной вниз) отключена.
Рис. 16-3. Программа ЕХ16В в действииРис. 16-3. Программа ЕХ16В в действии
Рис. 16-3. Программа ЕХ16В в действии
Рис. 16-3. Программа ЕХ16В в действии
Кнопки на панели инструментов описаны в таблице.
Кнопка Назначение
Выбрать первую запись
Выбрать последнюю запись
Выбрать предыдущую запись
Выбрать следующую запись
Удалитьтекущуюзапись
Вставить новую запись
Кнопка Clear в окне представления очищает поля ввода Name и Grade. Команда Clear АН в меню Edit удаляет из списка все записи и очищает поля ввода в окне представления.
На этот раз мы не станем давать пошаговые инструкции. Поскольку объем кода увеличился, просто приведем листинг отдельных частей и требования к ресурсам. Полужирным вьще- лены дополнительные фрагменты кода и те места, где Вы должны внести изменения в код, сгенерированный AppWizard и ClassWizard. Операторы TRACE позволят наблюдать за ходом выполнения программы в окне отладки.
Требования к ресурсам
Б данном разделе описано заданное в файле ех16Ь.гс использование ресурсов.
Ланель инструментов
Чтобы создать панель инструментов (см. рис. 16-3) необходимо удалить кнопки Edit Cut, Copy и Paste (четвертую, пятую и шестую слева) и заменить их шестью новыми. Для дублирования кнопок применяется команда Flip Vertical из меню Image, а в файле ех16Ь.гс опред еляется связь между идентификаторами команд и кнопками панели инструментов.
Меню Student
Присутствие в меню пунктов, соответствующих новым кнопкам панели инструментов, вообще говоря, не обязательно. (ClassWizard позволяет создавать обработчики команд панели инструментов так же просто, как и команд меню.) Но большинство Windows-приложений позволяет вызывать все команды посредством меню, поэтому лучше не обманывать ожиданий пользователей.
Меню Edit
В меню Edit операции с буферам обмена заменены пунктам Clear АН (см. п. 2 примера ЕХ16А).
Диалоговый шаблон IDD STUDENT
Диалоговый шаблон IDD_STLтDENTдpивeдeнный здесь, напоминает шаблон из примера ЕХ16А, показанный на рисунке 16-1, за исключением того, что кнопка Еп1егзаменена на кнопку Clear.
Идентификаторы элементов управления.
Элемент управления
Идентификатор
Поле ввода Name
ЮС„МАМЕ
Поле ввода Grade
IDCjGRADE
Кнопка Clear
IDC „CLEAR
Стили элементов управления — такие же, как и у их аналогов из примера ЕХ16А.
Требования к коду
Файлы и классы в проекте ЕХ16В.
Заголовочный файл
■Файл исходного кода
Классы
Описание
exl6b.ii
exl6b.cpp
CExl6bApp
CAboutDlg
Класс приложения (создай AppWizard) Диалоговое окно About
MainFrmh
MatnFnn.cpp
CMainFrame
Основное окно-рамка SDI-приложения
SmDoc.h
StuDoccpp
CStudentDoc
Документ приложения
StuView.h
StuView.cpp
CStudantView
Класс «вид» — форма, представляющая информацию о студенте (производный от CFormVieW)
Studenth
StdAfx.h /
Studenc.cpp
StdAfx.cpp
CStudent
Запись о студенте (как и в ЕХ16А)
Включает стандартные, предварительно откомпилированные заголовочные файлы
11 Зак №78"
СЕх16ЬАрр
Файлы ех16Ь.срр и exl6b.h — стандартные, сгенерированные Арр Wizard.
CMainFrame
Код этого класса в файле MainFrm.cpp — также стандартный результат работы Арр Wizard,
CStudent
Код тот же, что и в ЕХ16А, только в конец Student.h добавлена строка:
typedef CTypedPtrList< COoList, CStudent* > CStudentList;
ПРИМЕЧАНИЕ Классы шаблонов наборов МЕС требуют наличия в StdAfx.h оператора:
«include <afxtempl.h>
CStudentDoc
Класс CStudentDoc был исходно сгенерирован Арр Wizard, а на рисунке 16-4 показан код, используемый в протрамме-примере ЕХ16В.
STUDOC.H
,// StuDoc.h : интерфейс класса CStudentDoc
//////////////////////////////////////////////////////////////////////
«if !defined(AFX STUDOC.H .40011047 7E1C_11DO_8FF.O_OOCO4FC2AOC2_INCLUDED )
«define AFX_STUD0C_H_4D011047_7E1C_11D0_8FE0_00c64FC2A0C2„INCLUDED_
#if .MSC.VER > 1000
«pragma once
' ttendif"■
H „MSC.VER > 1000
«include "student.h"
class CStudentDoc : public CDocument
protected; // создаются только сериализацией
CStudentDoc();
DECLARE_DYNCREATE(CStudentDoc).
// Атрибуты
public:
CStudentList* GetListp {
return &m_8tudentList;
}
И Операции public:
H Переопределенные функции
Ц переопределенные виртуальные функции, сгенерированные ClassWizard //{{AFX.VIRTUAL(CStudentDoc) public:
virtual BOOL 0nNewDocunient();
virtual void Serialize(CArchivei ar);
virtual void DeleteConrentsQ;
//}}AFX_VIRTUAL
Рис. 16-4. Листинг кода класса CStudentDoc'
Ц Реализация
public:
virtual "CStudentDocO;
ttifdef .DEBUG. .
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;'
tfendif-
protected:
// сгенерированные функции таблицы сообщений protected:-
//{{AFX_MSG(CStudentDoc)
afx.msg void OnEditClearAHO;
afx msg void OnUpdateEditClearAll(CCmdUI» pCmdUI);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()- '
private: CStudentList m studentList;
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ/ZZZZZZZZZZZZZZZZZZZZZZZZZ/ZZZZZZZZ/ZZZ
//{{AFX_INSERT_LOCATION}}
/ / M icrosoft Visual C++ добавит дополнительные обьявлени я
Ц непосредственно перед предыдущей строкой
flendif Ц I defined(AFX_STUD0C_H_4D0U047_7E1C_11D0_8FE0_06C04FC2A0C2_INCLUDED_)
STUDOC.CPP
.// StuDoc.cpp ; реализация класса CStudentDoc //
«include "StdAfx.h" «include "ex16b.h"
.«include "StuDoc.h"
#ifdef .DEBUG'
«define new DEBUG NEW «undef THIS.FILE static char THIS_FILE[] = FILE„; flendif1
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz Ц CStudentDoc
IMPLEMENT_DYNCREATE(CStudentDoc, CDocument)
BEGIN_MESSAGE_MAP(CStudentDoc, CDocument)’
Z/{<AFX_MSG MAP(CStudentDoc)
ON COMMAND(ID EDIT.CLEAR ALL. OnEditClearAII)
ON UPDATE_COMMAND_UI(ID EDIT CLEAR.ALL, OnUpdateEditClearAII)
Z/}}AFX MSG MAP'
END_MESSAGE_MAP()
inilinilHinillHIHilinHlIIIHiiniHniHHIlUIIIIIHIinillinilUil ll Создание/уничтожение CStudentDoc
Рис. 16-4. (продолжение)
CStudentDoc::CStudentDoc()
ТЙАСЕ(’Входим в конструктор CStudentDoc\n”);
fiifdef „DEBUG..
afxDump.SetDepth(l); // обеспечить вывод элементов списка
#endif Ц „DEBUG !.
CStudentDoc::"CStudentDoc() (•'
1
BOOL CStudentDoc::OnNewDocument()
ТВАСЕ("Входим в CStudentDoc::OnHewDocument\n"); if {!CDocument::OnNewDocument()) return FALSE;. .
// ДОДЕЛАТЬ; добавить код повторной инициализации // (документы SDI будут повторно использовать этот обьект)
return TRUE;■
ИПШ11111Ш11ШН1ПН1Ш1111Ш1Ш11ПШ11ШПШ1Ш1ПНШ1Ш
// Сериализация CStudentDoc
void CStudentDoc::Seriallze(CArchive6 gr)■
if (ar.IsStoringO)
// ДОДЕЛАТЬ; добавить сюда код сохранения.
else■■■' ■
(..
// ДОДЕЛАТЬ; добавить сюда код загрузки
} }.
ПШ1ПШ11ШПШНШ'П(1ПШШ11Ш1НШ1НННШП^
1! диагностика CStudentDoc
#ifdef .DEBUG ''’'
void CStudentDoc::AssertValid() const''
CDocument::AssertValid();, ■ . •.
void CStudentDoc::Oump(CpumpContext& do) const..
" CDocument::Dump(dc); ■••
de « "Xn" « m_stud6intList << "\n'*;■■
#endif //„DEBUG
///////////////////////////////////////^ // Команды CStudentDoc .■.
void CStudentDoc::DeleteContents().
{'
tfifdef „DEBUS
Dump(afxDump);
ttendif’
while (m_atudentL18t.GetHeadPosltion()) {
delete m studentList. RemoveHead();
)5
)
void CStudentDoc: :OnEditClearAll() {
DeleteContentsO;
UpdateAllViews(NULL):
void CStudentDoc: :0n(JpdateEditClearAll(CCmdUI» pCmdUI) {
pCmdUI->Enable(!m_studentList.IsEmpty());
}.-
ClassWizard и CStudentDoc
Команда Clear All (меню Edit) обрабатывается классом документа. С помощью ClassWizardflo- бавлены обработчики сообщений.
Идентификатор объекта СообщениеИмя функции-члена
ID_ED1T_CLEAR_ALL oFFcOMMANDOnEditCiearAM
$D ED1T CLEAR ALL ON UPDATECOMMAN'DL'lOnUpctateEdtiClearAii
Переменные-члены.
Класс документа содержит внедренный объект CStudentList — переменную-член mstudentList, хранящую указатели на объекты CStudent. Объект-список конструируется при создании объекта CStudentDoc и уничтожается при закрытии программы. CStudentList определен с помощью typede/KaKCTypedPtrListsjm указателей на CStudent,
Конструктор.
Конструктор документа устанавливает такую глубину диагностического вывода, чтобы при выводе списка выдавалась информация и по отдельным элементам.
GetList
Подставляемая в строку функция GetList помогает изолировать представление от документа. Класс документа зависит от типа объектов в списке, в данном случае — от объектов класса CStudent. Однако базовый класс «вид», чтобы получить указатель на список, не зная имени его объекта, может вызвать соответствующую функцию-член.
DeleteContents
DeleteContents — переопределенная виртуальная функция, вызываемая другими функциями класса «документ» и каркасам приложений. Ее задача — извлекать из списка указатели на объекты CStudent и удалять эти объекты. Важно помнить, что объекты-документы SDI повторно используются после закрытия. DeleteContents попутно выводит информацию о списке студентов.
Dump
AppWizard генерирует заготовку этой функции между строками *ifde_DEBUGa *endt/.Поскольку глубина диагностического вывода для «^сРитрустановдена в конструкторе документа равной 1, выводятся присутствующие в списке объекты CStudent.
CStudentView
Код класса CStudentViewпоказан на рисунке 16-5. Этот код пригодится Вам .И в следующих двух главах.
STUVIEW.H
// StuView. h ; интерфейс класса CStudentView
11Н11Н1П11Ш11П1Н11ШШ1НШШ!111Ш111ПШ1ШНШШШ1иНП
«if (defined(AFX STUVIEW H ^0011049 jeic J1DO 8FEOS0C04FC2A0C2„included )
«define AFX_STUVIEW_H_4D011049_7E1C_11DO_8FEQ_OOC04FC2AOC2_INCLUDED_
«if .MSC.VER > 1000
ff pragma once
ttendif // .MSC.VER > 1000
class CStudentView : public CFormView
{,
protected:
POSITION imposition; // текущее положение в списке
CStudentList* n_pList; // копируется из документа
protected; // создается только сериализацией
CStudentViewO;
DECLARE_DYNCREATE(CStudentVlew)
public:■
//{{AFX DATA(CStudentView)
enum { IDD = IOD_STUDENT };
intm_nGrade;
CString m strName;
//}}AFX_DATA
/У Атрибуты
public:.
CStudentDoc* GetDocumentO;.
.// Операции
public:■.
// Переопределенные функции
// переопределенные виртуальные функции, сгенерированные ClassWlzard
//{{AFX.VIRTUAL(CStudentView)
public:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected;
virtual void OoDataExchangetCOataExchange* pOX); // поддержка DDX/DDV
virtual void OnInltialUpdate(); // вызывается сразу после создания
virtual void OnUpdate(CView* pSender, LPARAM IHint, CObject* pHint);
//}}AFX_VIRTUAL■
// Реализация
public:
virtual 'CStudentViewO;
fiifdef .DEBUG
virtual void AssertValidO const;
virtual void Dump(CDumpContext& de) const;
■ffendif
Рис. 16-6. Листинг кода класса CSfucfentWew
protected:
virtual void ClearEntryO;
virtual void InsertEntry(POSITIQM position);
virtual void GetEntry(POSITION position);
protected:
// Сгенерированные функции таблицы сообщений".
protected:■
//{(AFX_MSG(CStudentView) afx_msg void OnClearO;• -■
afx_msg void OnStudentHomeO;■
afx_msg void OnStudentEnd(); afxjnsg void OnStudentPrev();'
afx_msg void OnStudentNext();
afxjnsg void OnStudentIns();
afxjnsg void QnStudentDel ();
afx_msg void OnUpdateStudentHome(CCmdUI* pCmdUI);
afxjnsg void OnUpdateStudentEnd(CCmdUI* pCmdUI);
afxjnsg void OnUpdateStudentDel(CCmdUI* pCmdUI);'
//}}AFX MSG"
DECLARE MESSAGE MAP()
);
tfifndef „DEBUG // отладочная версия в StuView. cpp
inline CStudentDpc* CStudentView: :GetDocument(>
{ return <CStudent0oc*)m_pD6cument; }
ttendif
llllllllllllilllllllliniliiltlHIUinillllllllHIHHIillllllilllHI
IK {AF>_INSERT_LOCATION}}
// Microsoft Visual C++ добавит дополнительные объявления
// непосредственно перец предыдущей строкой
ttendif // ! def ined(AFX„STUVIEW_H„„4D0l104VE1C„11DO„8FE0,00C04FC2A0C2_lNCLUDED„)
STUVIEW.CPP
// StuView. cpp : реализация класса CStudentView■
И
«include "StdAfx.h"
«include "exlBb.h"■■
«include "StuDoc.h"
«include “StuView. h"., ■-
ttifdef „DEBUG• .
fldefine new DE8UG_NEW
«undef THIS_FILE■•.
static char THIS„FILE[] =FILE; flendif
/////////////////////////////////////////////////////////////////////////////
// CStudentView
IMPLEMENT_DYNCREATE(CStudentView, CFormView).
BEGIN MESSAGE MAP(CStudentView, CFormView)...
iK {AFX_MSG_MAP(CStudentView)
Рис. 16-5. (продолжение)
ON.BN CLICKED(IDC CLEAR, OnClear)
ON COMMAND(ID STUDENT HOME, OnStudentHome)
ON COMMAND(ID STUDENT END, OnStudentEnd).
ON COMMANDED STUDENT PREV, OnStudentPrev)
ON COMMANDED STUDENT NEXT. OnStudentNext)
ON COMMANDED STUDENT INS, OnStudentlns)
ON COMMANDED STUDENT DEL, OnStudentDel)
ON UPDATE COMMAND UI(ID STUDENT HOME, OriUpdateStudentHome)
ON UPDATE COMMAND UI(lD STUDENT.ENO, OnUpdateStudentEnd)
ON UPDATE COMMAND UI(ID STUDENT.PREV. OnUpdateStudentHome)
ON UPDATE COMMAND UI(ID STUDENT.NEXT, OnUpdateStudentEnd)
ON UPDATE COMMAND UI(ID STUDENT.DEL, OnUpdateStudentDel)
//}}AFX MSG MAP
END^MESSAGE.NAPO
/////////////////////////////////////////////////////////////////////////////
// Создание/уничтожение CStudentVlew
CStudentVlew;:CStudentView( )
: CFormView(CStudentVlew::IDD)
TRACE("Входим в конструктор StudentView\n");
//{ <AFX_DATA_INIT(CStudentView)
m.nGrade = 0;
m strName = T("");.
//))AFX_DATA_INIT'
m_positlon = NULL;
CStudentVlew::'CStudentView()
i '
void CStudentVlew::DoDataExchange(CDataExchange* pDX)
CFormView::DoDataExchange(pDX);
//{{AFX DATA MAP(CStudentView)
DDX Text(pDX, IDC GRADE, m.nGrade);
DDV.MlnMaxInt(pDX, m.nGrade, 0, 100);.
DDX_Text(pDX, IDC.NAME, m.strName);'
DDV HaxChars(pDX, m strName, 20); //»AFX_DATAMAP 1.
BOOL CStudentVlew::PreCreateWlndow(CREATESTRUCTA cs) {
И ДОДЕЛАТЬ: изменить класс и стили окна // изменением CREATESTRUCT cs
ireturnCFormView: :PreCreateMlndow(cs);
void CStudentVlew: :OnInitlalUpdate(>
ТЙАСЕ("Входим в CStudentVlew;:OnInitlalUpdate\n"); m.pLlst - GetDocument()->GetLlBt();
CFormView::OnInitialUpdate();
/////////////////////////////////////////////////////////////////////////////
// Диагностика CStudentView
flifdef .DEBUG
void CStudentView: :AssertValid() const
{
CFormView::AssertValid();
void CStudentView: :Dump(CDumpContext&de) const {
CFormView: :Dump(dc);
CStudentDoc* CStudentView: :GetDocument() // не-отладочная версия подставляется в код
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLA$S(CStudentDoc)>);
return (CStudentDoc«)m_pDocument;
jendif //.DEBUG
///////////////////////////////^
Ц Обработчики сообщений CStudentView
void CStudentView::0nClear()
* ТЯАСЕ("Входим в CStudentView::0nClear\n");
ClearEntryO;
void CStudentView: :OnUpdate(CView* pSender, LPARAM IHint, CObject* pHint)
И вызывается функциями OnlnitialUpdate и UpdateAllViews
ТВАСЕГВходим в CStudentView: ;OnUpdate\n");
m_pList = GetDocument()->GetLi8t();
imposition = m_pList->GetHeadPositlon();
GetEntry(ffl_position),' // начальные данные для окна представления
void CStudentView: :OnStudentHome() <
ТНАСЕ("Входим в CStudentView: :OnStudentHome\n");
// надо проверять, не пуст ли список
if (lm_pLlst->IsEmpty()) {
imposition = ni_pList->GetHeadPasition();
Gat Ent ry (imposition);
void CStudentView: :OnStudentEnd()
^ TRACE("Входим в CStudentView::OnStudentEnd\n");
if (lm_pList->IsEmpty()> {
■.position = m_pList->GetTailPosition():
GetEntry(m position);
см. сдед, стр.см. сдед, стр.void CStudentView: :OnStudentPrev() { POSITION pos;
см. сдед, стр.
см. сдед, стр.
Рис. 16-5. (продолжение)
TRACE("Входим в CStudentView::OnStudentPrev\n");
if ((pos = Deposition) 1=NULL) { m_pllst->GetPrev(pos);
if (Pos) { GetEntry(pos); m position = pos;
void CStudentView: :OnStudontNext()
{
POSITION pos;
TRACE("Входим в CStudentView::0n3tudentHext\n");
if ((pos = imposition) l=NULL) {
m_pList->GetNext(pos);
if (pos) {
GetEntry(pos):
n position = pos;
void CStudentView: :OnStudentIns()■
{
ТЯАСЕ("Входим в CStudentView: :OnStudentIns\n");
Inse rtEnt ry (exposition);
GetOocuDient()->SetModifiedFlag();
GetDocument()->UpdateAllViews(this);
void CStudentView: :OnStudentDel()
< И удаляет текущий элемент и позиционирует список на следующий элемент или на начало POSITION pos;
ТЯАСЕ("Входим в CStudentView::OnStudentDel\n‘);
if ((роз = Deposition) I^NULL) {..:«•.
m_pL18t->GetNext(pos);.
if (pos == NULL) {''
pos = m pList->GetHeadPosition();
TRACE("GetHeadPos = *ld\n", pos);.
if (pos — m_position) {
pos = NULL;
:l 1
GetEntry(pos);
CStudent* ps = m_pList->GetAt(m_position);
m_pLi st->RemoveAt(m_posit ion);
delete ps;-
Deposition = pos;,
' GetDocument()->SetModifiedFlag();
GetDocument()->UpdateAllViews(this);
void CStudentView::OnUpdateStudentHome(CCffldUI* pCmdUl) {
// вызывается в момент простоя и при вызове меню Student POSITION pos;
// активизирует кнопку, если список не пуст и текущее положение не идентично началу списка
ipos = i_pList->GetHeadPosit-ion();'
pC»dUI->Enable((i position != NULL) &Д (pos | = m position»;
}
void CStudentView: :OnUpdateStudentEnd(CCmdUI* pCmdUI)
{
И вызывается в момент простоя и при вызове меню Student
POSITION pos;
И активизирует кнопку, если список не пуст и текущее положение не идентично концу списка pos = B_pL13t->GetTailPosition();
pCidUI->Enable((m_position ! = NULL) 41 (pos imposition));
void CStudentView: :OnUpdateStudentOel(CCmdUI* pCmdUI)
И вызывается в момент простоя и при вызове мени Student pCidUI->Enable((m_poaition != NULL»;
void CStudentView: :GetEntry(POSITION position)
< if (position) {
CStudent* pStudent = m_pL13t->GetAt(po31tion); m_strName = pStudent->ie_atrName;
m_nGrade = pStudent->m_nGrade;
else {‘
Clear Ent ry();
pdateOata( FALSE);
void CStudentView: :InsertEntry(POSITION position)
if (UpdateData(TflUE)) {
// UpdateData возвращает FALSE, обнаружив ошибку пользователя CStudent* pStudent = new CStudent;
pStudent->m_strName = ni_strNaine;
pStudent->m„nGrade=ra_nGrade;
m_position = m_pLiat->InsertAfter(m_position, pStudent);
void CStudentView: :ClearEntry()
»_strNai»e = "*;
m_nGrade = 0;
UpdateData(FALSE);
((CDialog*) this)->GotoDlgCtrl(GetDlgItee(IOC_NAME)) ;
}
ClaseWIzard и CStudentlfiew
С помощью ClassWizard в классе CStudentView создан обработчик уведомляющего сообщения от командной кнопки Clear.
Идентификатор объектаСообщениеИмя функции-члена
ЮССкЕАЯ BNCLICKEDOnCtear
Так как класс CStudentView— производный от CFormView,ClassWizard поддерживает определение переменных-членов диалогового окна. Показанные здесь переменные добавлены с помощью кнопкиАбб Variables. ..
Идентификатор элемента управления
Имя переменной
Категория
Тип переменной
mC_GRADE •
ЮСЫАМЕ
t_nGrade
т strName
Value
Value
int
CString
Присвойте УИ-ПОгж/еминимальное значение 0, максимальное — 100. Установите предельную длину m_strName в 20 символов.
ClassWizard сопоставляет обработчики командам кнопок. Ниже приведена таблица команд и их обработчиков.
Идентифик атор элемента управления
Сообщение
функция-обработчик
to_STUDENT_HOME
П)_ STUDENT_END
1D_STUDENT_PREV
ID_STUDENT_NEXT ID_STUOENTJNS.
ID_STUDENT_DEL
COMMAND COMMAND
COMMAND 1 COMMAND COMMAND COMMAND
OnStudentHome OnSUtdentEnd OnStodentPrev OnSittdenti-fext OnSitidendns OnStudentDel
В каждый обработчик встроен контроль ошибок.
Приведенные ниже обработчики сообщений обновления пользовательского интерфейса вызываются или в момент простоя — для обновления состояния кнопок панели инструментов, или при вызове меню Student — для обновления пунктов меню.
Идентификатор элемента управленияСообщение
Функция-обработчик
tD_S7TOENT_HOMEUPDATE_COMMAND_UI
iD_STODENT_ENDUPDATE_COMMAND_UI
m_STUDENT_PREVUPDATE"cOMMANDL'I
ID_STUDENT_NEXTUPDATE_COMMAND_UI
JD_STUDENT_DELUPDATE_COMMAND_UI
Например, вот эта кнопка,
OnUpdateStudentHome
On UpdateStudentEnd
On Up dateStudentHome
On UpdateStudentEnd
OnUpdateCommandDei
выбирающая первую запись о студенте, отключена, если список пуст, а также, если переменная т„pbsitionyxe указывает на первую запись. Кнопка Previous (предыдущий) отключается в тех же случаях, поэтому она использует тог же обработчик команды обновления пользовательского интерфейса. По этой же причине пользуются одним обработчиком кнопки End |(конец) и Next (следующий). Так как вызов функций обновления командного интерфейса иногда происходит с задержкой, обработчики командных сообщений должны содержать проверку ошибок. '
Переменные-члены *' .
Переменная т„position — что-то вроде курсора для набора объектов в документе, Он ссылается на отображаемый в данный момент объект CStudent. Переменная tn_pList обеспечивает быстрый доступ к списку студентов в документе.''
OnlnitiaiUpdete.
Виртуальная функция QnlnitialUpdate вызывается при запуске приложения. Она инициализирует tn_pListpfis. последующего доступа к списку в документе.
OnUpdate
Виртуальная функция OnUpdate вызывается как функцией OnlnitialUpdate,так и функцией CDocument::UpdateAUViews.OHa устанавливает текущее положение на началосписка и отображает его первый элемент. В данном случае функция Ь^йвГеД/Меыжызывается только по команде Clear АП (меню Edit). В приложении с несколькими представлениями данных в ответ на обновление документа из другого окна представления может понадобиться иная стратегия установки переменной m.position в классе CStudentView.' 'х
Защищенные виртуальные функции:
Перечисленные ниже функции — защищенные виртуальные функции, специально предназначенные для работы с объектами CStudent:."
GetEnlry..
InsertEntry'
ClearEntry- ' - ■
Чтобы выделить в базовый класс универсальные средства обработки списков, эти функции следует переместить в производный класс.'
Тестирование программы ЕХ16В
Заполните поля ввода и, чтобы вставить запись в список, нажмите кнопку:
Повторите эту операцию еще несколько раз, стирая предыдущие данные из полей ввода щелчком кнопки Clear. Закрыв приложение, Вы должны увидеть в окне вывода отладочной информации примерно такую запись:
a CStudentDoc at $48ЕВ
m_strTitle = Untitled
m_strPathName =
m_bModified = 0
m_pDocTemplate = $46AE
a CObList at $4A08
with 4 elements
a CStudent at $4DC2, .. ,. - . •
m_strName = Fisher, Lon'
m_nGrade =67
a CStudent at $50FA,
m_strName = Meyers, Brian,.
m_nGrade = 80'' ■ ■ ■
a CStudent at $5152'• ’
m_strName = Seghers, John.'■.•■•■
ffl_nGrade = 92■ .■.■. .■,
a CStudent at $51AA•..■
m_str№me = Anderson, Bob'
m_nGrade =87‘'■•
Пара упражнений для читателя.. u6.,
Возможно, Вы заметили, что на панели инструментов нет кнопки, позволяющей модифицировать запись о студенте. Попробуйте сами добавить такую кнопку и обработчики сообщений
Вспомните, что класс CStudentViewпочти готов к тому, чтобы стать универсальным базовым. Попытайтесь выделить виртуальные функции с кедом, специфичным для CStudent, в производный класс. А потом создайте другой производный класс, который булкт использовать новый класс элементов списка, отличный от CStudent.■’
Чтение и запись документов
в SDI-приложениях
Вероятно, Вы обратили внимание, что в каждой программе, сгенерированной AppWizard, есть знакомое меню File с командами New (Создать), Open (Открыть), Save (Сохранить) и Save As (Сохранить как). В этой главе Вы узнаете, как заставить приложение реагировать на них, т. е. считывать и записывать документы.
Пока Вы продолжите работать с SDI-программами, поскольку этот тип приложений Вам уже известен. Ну, а с MDI-программами, более гибкими в обработке документов и файлов, Вы познакомитесь в главе 18. В этой и следующей главах мы углубимся в теорию каркаса приложений, в том числе рассмотрим множество вспомогательных классов, о которых До сих пор не было сказано ни слова. Путь предстоит трудный, но поверьте: это действительно нужно, иначе Бы не сумеете эффективно использовать потенциал каркаса приложений.
Пример ЕХ17А — это SDI-приложение, основанное на программе ЕХ16В из предыдущей главы. В нем создается документ со списком студентов и класс «вид», производный от СРогт- View. Теперь список студентов можно будет сохранять надиске и считывать с него, применяя так называемую c^p«tiuz«3az<»K>(serialization). А в главе 18 Вы узнаете, как использовать те же классы «документ» и «вид» для создания MDI-приложения.
Понятие сериализации
Для Вас термин «сериализация», может быть, новый, но он уже получил определенное распространение в мире объектно-ориентированного программирования. Идея состоит в том, что существуютустойчивые (persistent) объекты, которые можно сохранять надиске при завершении программы и восстанавливать при следующем запуске. Этот процесс сохранения и восстановления объектов и называется сериализацией (serialization). Поддерживающие ее классы библиотеки MFC содержат функцию-член Serialize. Когда каркас приложений вызывает ее, например, для объекта класса CStudent, данные о студенте либо сохраняются надиске, либо считываются с него.
В библиотеке MFC сериализация не заменяет систему управления базами данных. Все объекты, связанные с конкретным документом, последовательно считываются или записываются в один дисковый файл. Доступ к отдельным объектам в файле по произвольным адресам невозможен. Если вашей программе нужны средства управления базами данных, подумайте об использовании открытого интерфейса подключения к базам данных (Open DataBase Connectivity, ODBC) или объектов доступа к данным (Data Access Objects, DAO). Применению ODBC и DAO в рамках MFC посвящены главы 31 и 32.
ПРИМЕЧАНИЕ На сегодняшний день есть новый способ хранения данных, занимающий промежуточное место между последовательными файлами и базами данных, — структурированное хранилище (structured storage), описанное в главе 27. Каркас MFC-приложений уже поддерживает его для программ- контейнеров, оперирующих внедренными объектами.
Дисковые файлы и архивы
Как узнать, что должна делать функция Serialize; считывать данные или записывать их? Как она связывается с дисковым файлом? В MFC-библиотеке дисковые файлы представляются объектами класса CFile. Объект CFile инкапсулирует описатель двоичного файла, возвращаемый ^'1п32-функцией CreateFile. Это не указатель на структуру FILE буферизованного файла, возвращаемый функцией Jopen стандартной С-библиотеки, а именно описатель двоичного файла. Каркас приложений использует его при вызовах Win32-фyнкций ReadFile, WriteFilen SetFilePointer.
Если программа не выполняет прямые операции ввода/вывода на диск, а полагается на сериализацию, явного применения объектов CFile можно избежать. «Между* функцией Serialize и объектом CFile располагается объект-архив класса GArcbive (см. рис. 17-1). Он буферизует данные для объекта CFile и поддерживает внутренний флажок, указывающий, записывается ли архив надиск или считывается с него. С каждым файлом единовременно может быть ■связан только один активный архив. За создание объектов CFile и CArcbive, открытие дискового файла для объекта CFile и связывание объекта-архива с файлом отвечает каркас приложений. Все, что Вам остается сделать в своей функции Serialize — загрузить данные из объекта-архива или сохранить их в нем. Каркас приложений вызывает функцию Serialize класса «документ» при обработке команд Open и Save из меню File.
Каркас грилфкений вызывает функцию Serialize при обработке команд File Open и Rie SaveКаркас грилфкений вызывает функцию Serialize при обработке команд File Open и Rie Save
Каркас грилфкений вызывает функцию Serialize при обработке команд File Open и Rie Save
Каркас грилфкений вызывает функцию Serialize при обработке команд File Open и Rie Save
Рис. 17-1. Процесс сериализации
Создание сериализуемого класса
Для того, чтобы сделать класс сериализуемым, он должен быть производным (прямо или косвенно) от CObject. Кроме того, в объявлении класса должен присутствовать макрос DECIARE-SERIAL, а в файле реализации — макрос ШР1ЕМЕУГ_5ЕЕ1АЬ^Описание этих макросов см. в Microsoft Foundation Class Керегепсе^Мы. внесем эти макросы в класс CStudent, который будем использовать в следующих примерах.
Создание функции Serialize■
В главе 16 мы создали класс CStudent, производный от CObject, с такими переменными-членами:■ '
public:
CString m.strName;
int mnGrade;
Теперь напишем для класса CStudent функцию Serialize. Так как это виртуальная функция класса CObject, нужно, чтобы типы ее параметров и возвращаемого значения совпадали с объявленными в нем. Функция Serialize для класса CStudent выглядит так:
void CStudent::Serialize(CArchive& ar)
I.
ТНАСЕ("Входим в CStudent::Serialize\n”)
if (ar.IsStoringO) {
ar « M_strNane « m_nGrade;
I
else {
ar » m„strName » m_nGrade;
}
)
Обычно функции сериализации вызывают Serialize соответствующего базового класса. Если бы CStudent был производным, скажем, от СРегзоп, первая строка в этой функции выглядела бы так
СРегэоп: :SeriaHze(ar);
Функции Serialize для классов CObject и CDocument не делают ничего полезного, так что вызывать их нет смысла.-
Заметьте: аг — параметр, ссылающийся на объект ♦архив» приложения. Функция-член САг- cbivealsStoringim^oipMH^en, для чего архив используется в настоящий момент — для записи или считывания. У класса CArcbive имеются переопределенные операторы вставки («) и извлечения (») для многих встроенных типов C++, как показано в таблице.
Тип данныхОписание
BYTE
8 разрядов, беззнаковый
WORD
16 разрядов, беззнаковый
LONG
32 разряда, знаковый
DWORD
32 разряда, беззнаковый
float32
разряда
double
64 разряда, стандарт IEEE
int
32 разряда, знаковый
short
\6 разрядов, знаковый
cbar
В разрядов, знаковый
unsigned
32 разряда, беззнаковый
Операторы вставки принимают параметры по значению, операторы извлечения — по ссылке. Иногда приходится прибегать к преобразованию типов, чтобы избежать ошибок компиляции. Допустим, у нас есть переменная-член ^яТ^реперечислимого типа. Вот какой код необходимо написать:
ar « (int) ш_пТуре:
аг » (inti) m_nfype;
У таких MFC-классов, как CString и CRect, не являющихся производными от CObject, есть свои переопределенные операторы вставки и извлечения для CArcbive.
Загрузка из архива: внедренные объекты и указатели
Допустим, в объект CStudent внедрены другие объекты, не являющиеся экземплярами стандартного класса вроде CString,CSize или CRect. Добавим к классу CStudentновую переменную- член:
public:
CTranscript m_transcript;
Будем считать, что CTranscript — некий нестандартный класс (производный от GObjecf)^ которого есть собственная функция Serialize. Для CObjectHe предусмотрены переопределенные операторы «и », поэтому функция CStudent::Serialize приобретает вцц:
void CStudent::Serialize(CArchiveS ar)
•I
if (ar.IsStoringO) {
ar « m strName « m nGrade;
else {
ar » m_strName » m_nGrade;
m transcript.Serialize(ar);
} ".
Прежде чем вызывать функцию CStudenfc.SerializeRna загрузки из архива записи о студенте, надо создать объект CStudent. Внедренный объект mjtranscriptKnacca CTranscript создается вместе с объектом CStudent перед вызовом функции CTbanscriptnSerialize. Последняя может загрузить из архива соответствующие данные во внедренный объект m_transcript3anDM.- ните одно правило: для внедренных объектов классов, производных от CObject, функция Serialize вызывается явным образом.
Теперь допустим, что CStudent вместо внедренного объекта содержигуказатель на Cfranscripfr. public:
CTranscript* m_pTranscript;
Можно было бы воспользоваться функцией Serialize, как показано ниже, но при этом пришлось бы самостоятельно создавать объект CTranscript:
void CStudent::Serialize(CArchive& ar)
{
if (ar.IsStoringQ) {
ar « m_strName « m_nGrade;
else {
m_pTranscript = new CTranscript;
ar » m strName » m nGrade;
)~ '
m pTranscript->Serialize(ar);
Поскольку операторы вставки и извлечения в CArchive для указателей на CObject на самом деле переопределены, можно написать функцию Serialize и так
void CStudent::Serialize(CArchive& ar)
if (ar.lsStoringO) {
ar « m strName « m nGrade « m pTranscript;
}"
else {
ar » m strName » m nGrade » m pTranscript;
)
Для создания объекта <77Ь»«с^>й1ри загрузке данных из архива пред назначены макросы DECIARE_SERIALh 1MPLEMENT_SER1AIh3 класса CTranscript. Когда объект CTranscript записывается в архив, эти макросы гарантируют занесение туда вместе сданными и имени класса. При загрузке архива считывается имя класса, и д инамически создается объект нужного класса под управлением кода, сгенерированного макросами. После того как объект CTranscript сформирован, можно вызвать переопределенную для класса CTranscript функцию Serialize,. чтобы считать данные из дискового файла.
И последнее. Указатель на CTranscript сохраняется в переменной-члене mjyiYanscriptTlTO- бы избежать «утечки памяти», убедитесь, что в т_р1Усимспр1ещв не занесен указатель на объект CTranscript. Если объект CStudentтолько что создан (а не загружен из архива), указатель на CTranscript будет нулевым.
Операторы вставки и извлечения не работают с внедренными объектами классов, производных от CObject.
аг » m_strName » m_nGrade » &m_transcript; // никогда так не делайте
Сериализация наборов
Так как все классы наборов производные от CObject и в их объявлениях присутствует вызов макроса DECLARE_SERlAL,ium сериализации наборов достаточно просто вызвать функцию Serialize соответствующего класса набора. Например, если вызвать Serialize для набора CObList объектов CStudent, то последует вызов функции Serialize для кажд ого объекта CStudent. Но при этом не забывайте об особенностях процесса загрузки наборов из архива.
। • Если набор содержит указатели на объекты разных классов, каждый из которых происходит от CObject, то имена этих классов сохраняются в архиве, чтобы потом можно было корректно восстановить объекты конструкторам соответствующего класса.
• Если объект-контейнер (например, документ) содержит внедренный набор, загружаемые данные добавляются к текущему содержимому набора, поэтому не исключено, что перед загрузкой из архива Вам придется очистить такой набор. Обычно это делается в виртуальной функции DeleteContents, вызываемой каркасом приложений.
। • Когда из архива загружается набор указателей на CObject, над каждым объектам в наборе выполняются следующие операции:
определяется класс объекта;
объекту выделяется память из кучи;
во вновь выделенную память загружаются данные объекта;
указатель на новый объект сохраняется в наборе.
Сериализация внедренного набора объектов CStudent описана в программе ЕХ17А.
Функция Serialize и каркас приложений
Итак, теперь Вы знаете, как писать функции Serialize и что вызовы этих функций могут быть вложенными. Но знаете ли Вы, когда вызывается первая функция Serialize для запуска процесса сериализации? В каркасе приложений вое связано с документом (с объектом класса, производного от CDocumenfy При выборе команды Save или Open из меню File каркас приложений создает объект CArcbive (и соответствующий объект CFile}, после чего вызывает функцию Serialize из вашего класса документа, передавая ссылку на объект CArcbive. Затем функция Serialize производного класса документа выполняет сериализацию всех его (постоя 1ных) переменных-членов.
ПРИМЕЧАНИЕ Присмотревшись к какому-нибудь классу документа, сгенерированному AppWizard, Вы заметите, что вместо макросов DECLARE^SERIAL и 1MPLEMENT_SER1AL в нем применяются макросы DECLAREDYNCREATE и /MPLEMENT_DYNCREATE.MaKpocbi SERIAL не нужны, т. к. объекты-документы никогда не используются вместе с СДгсЛгие-оператором извлечения и не включаются в наборы; каркас приложений вызывает функцию Serialize документа напрямую. Макросы DECLARE_SERIAL и IMPLE- MENT_SERIALnpejma3Ha4et« для остальных ^сериализуемых» классов.
SDI-приложение
Вы уже видали целый ряд SDI-приложений с одним классам «документ» и одним классом «вид». В этой главе мы по-прежнему будем иметь дело с единственным классом «вид», но постараемся исследовать взаимосвязи между объектом-приложением, основным окном-рамкой, документом, его представлением, объектом — шаблоном документа и связанными с ними ресурсами строк и меню.
Объект-приложение Wi ndows
Для каждого из ранее рассмотренных приложений AppWizard автоматически генерировал класс, производный от CWinApp. Кроме того, ан создавал оператор:
СМуАрр theApp;
То, что Вы вид ите, — это механизм запуска приложения, построенного на базе MFC. Класс СМуАрр происходит от CWinApp.a theApp — глобальный экземпляр данного класса, называемый объектом-приложением Windows.
Теперь рассмотрим последовательность операций, выполняемых при запуске Windows- приложения, написанного на базе MFC.
Windows загружает программу в память.
Создается глобальный объект CWinApp. (Вое глобально объявленные объекты формируются в момент загрузки программы.)
Windows вызывает глобальную функцию WinMain^oropaa является частью MFC-библиотеки. (WinMain эквивалентна функции main приложений текстового режима; обе они - главные точки входа в программу.)
WinMain отыскивает единственный экземпляр класса, производного от CWinApp.
■5. WinMain вызывает для theApp функцию-член Initlnstcmce, переопределенную в вашем производном классе приложения.
Ваша переопределенная функция Initlnstance инициирует загрузку документа и создание основного окна-рамки и окна представления.
WinMain вызывает для theApp функцию-член Run, которая организует распределение оконных и командных сообщений.......
Вы можете переопределить и другую важную функцию-член CWinApp — Exitinstance, вызываемую при завершении приложения, после закрытия всех его окон.
ПРИМЕЧАНИЕ Windows допускает одновременное выполнение нескольких экземпляров программы. Функция Initlnstance вызывается всякий раз, когда запускается новый экземпляр программы. В Win32 каждый такой экземпляр — отдельный процесс. И то, что на виртуальные адресные пространства каждого процесса проецируется один и таг же код, несущественно. Если Вы хотите найти другие выполняемые экземпляры своей программы, то либо воспользуйтесь Win32-фyнкциeй FindWindow, либо — для связи между процессами — определите общую секцию данных или файл, проецируемый в память.
Класс шаблона документа
Взгляните на функцию Initlnstance^ сгенерированную AppWizatd для вашего производного класса приложения:’ ‘
CSingleOocTemplate* pDocTemplate;.
pDocTemplate = new CSingleDocTemplatef''
IDR MAINFRAME. '!.
RUMTlME_CLASS(CStudentOoc), RUNTIME CLASS(CMainFrame), // основное окно-рамка SDI-программы RUNTIME_CLASS(CStudentView));
AddDocTemplate(pDocTemplate);' ■
Если Вы не собираетесь использовать разделяемые окна или множественные представления данных, то с объектом «шаблон документа* Вы встретитесь фактически только в этом месте программы. В данном случае это объект класса CSingleDocTemplate, производного от CDocTemplate. Класс CSingleDocTenplate применим только для SDI-приложений, у которых не может быть более одного объекта «документ1». Что касается AddJDocTemplate, то это функция- член класса CWinApp.
Вызов AddDocTemplate (совместно с вызовом конструктора шаблона документа) устанавливает взаимосвязь между классами приложения, документа, окна представления и основного окна-рамки. Объект-приложение, конечно, существует и до создания шаблона, но объекте® «документ», «рамка* и «представление* в этот момент еще нет. Каркас приложений будет создавать их динамически, когда в том возникнет необходимость.
Такое динамическое создание объектов — пример искусного использования языка C++. Благодаря тому что в определении и реализации класса присутствуют макросы DEGLAREJDYN- СКЕАТЕ и IMPLEMENTJAYCREATE, библиотека MFC способна создавать объекты данного класса динамически. Без этого в программу пришлось бы жестко зашить намного больше взаимосвязей между классами приложения. Например, в вашем производном классе приложения понадобился бы код для создания документа, его представления и рамки как объектов конкретных производных классов. Эго нарушило бы объектно-ориентированную природу программы.
Рис. 17-2. Взаимосвязи классов
В системе, основанной на шаблонах, необходимо всего лишь использовать макрос RUN- 7ZME_CL4S5O6paTHTe внимание: чтобы макрос работал правильно, необходимо указать в нем объявление класса.
На рисунке 17-2 показаны взаимосвязи между различными классами, а на рисунке 17-3 — между объектами. У SD1-приложения может быть только один шаблон (и ассоциированные с ним группы классов), а во время его работы — только один объект документа и один объект основного окна-рамки.
Рис. 17-3. Взаимосвязи объектовРис. 17-3. Взаимосвязи объектов
Рис. 17-3. Взаимосвязи объектов
Рис. 17-3. Взаимосвязи объектов
ПРИМЕЧАНИЕ Динамическое создание объектов существовало в MFC-библиотеке ещедотого, как в языке C++ стала возможной идентификация типов в период выполнения (runtime type identification, RTTi). Однако средства М РСзначительно превосходятвозможности RTTI, и MFC- библи отека продолжает для динамического создания объектов использовать именно их. Описание средств динамического создания объектов в MFC см. в приложении Б.
Ресурс шаблона документа
IDR_MAINFRAME"ех17а\п""\л""Ех17а\л""Ех17а Files (*.17а)\п'”.17а\п""Ех17а.Document\n""Ех17а Document”IDR_MAINFRAME"ех17а\п""\л""Ех17а\л""Ех17а Files (*.17а)\п'”.17а\п""Ех17а.Document\n""Ех17а Document”Первый параметр функции AddDocTemplate ~ идентификатор строкового ресурса IDR_MAIN- FRAME. Вот что Арр Wizard генерирует в RC-фаЙлс проекта ЕХ17А:
IDR_MAINFRAME
"ех17а\п"
"\л"
"Ех17а\л"
"Ех17а Files (*.17а)\п'
”.17а\п"
"Ех17а.Document\n"
"Ех17а Document”
IDR_MAINFRAME
"ех17а\п"
"\л"
"Ех17а\л"
"Ех17а Files (*.17а)\п'
”.17а\п"
"Ех17а.Document\n"
"Ех17а Document”
И заголовок окна приложения
// основа для имени документа по умолчанию
// (если не задано, то "Untitled")
// имя типа документа
И описание типа документа и фильтр
// расширение документов этого типа -
И идентификатор типа файла в реестре '
// описание типа файла в реестре
ПРИМЕЧАНИЕ Конкатенация (сцепление) строк компилятором ресурсов не поддерживается. Взгляните на содержимое файла ех17а.гс; отдельные «подстроки» собраны в одну длинную строку.
/Р/?_.Ш/МР./^1®пределяет одну строку, разбитую на подстроки символами \п. Эти подстроки показываются на экране в тог или иной момент исполнения программы. Строка 17Л— расширение файлов документов по умолчанию, заданное для AppWizard.
Кроме строк, идентификатор Ю/г^ИЛЛУЯаШйшределяет значок приложения, ресурсы панелей инструментов и меню. Эти ресурсы генерируются AppWizard, а Вы работаете с ними через редактор ресурсов.
Итак, Вы увидели, как шаблондокумента связывает воедино все элементы приложения. Нс пока не создано ни одного окна, на экране ничего не появляется.'
Множественное представление документа в SDI-программах
Поддержка нескольких представлений документа в SDI-приложеНИИ несколько сложнее. Тут можно просто определить элемент меню, позволяющий выбрать конкретное представление данных, или же создать несколько представлений в разделяемом окне. Оба способа рассмотрены в главе 20.
Создание пустого документа: функция CWinApp::OnRteNew
Функция Initlnstance вашего класса приложения, вызвав функцию-член AddDocTemplate, затем вызывает (неявно, через CWinA^izProces^bellCominancnip^vp^ важную функцию-член класса CWinApp — OnFileNew. Последняя, обращаясь к CWinApp;:OpenDocumentFile, распутывает «паутину» взаимосвязанных имен классов и выполняет следующие действия.
Создает объект-документ, не пытаясь читать данные с диска.
Создает объект «основное окно-рамка» (класса CMainFramepi основное окно, не отображая его на экране. У основного окна-рамки есть меню IDR_MAINFRAMBj.aHenb инструментов и строка состояния.
Формирует объект «вид* и соответствующее окно, не отображая его на экране.
Устанавливает связи между объектами «документ», «основное окно» и «представление». Не путайте связи между объектами со связями между классами, установленными вызовом AddDocTemplate...
Вызывает для объекта «документ» виртуальную функцию-член CDocument:;OnNewDocument, которая обращается к виртуальной функции DeleteContents.
Вызывает для объекта «вид» виртуальную функцию-член CView::OnlnitialUpdate.
Вызывает для объекта-рамки виртуальную функцию-член CFmmeWnd:;ActivateFramelчтобы вывести на экран основное окно-рамку вместе с меню, окном представления, панелью инструментов и строкой состояния.,
ПРИМЕЧАНИЕ Некоторые из перечисленных функций вызываются не из OpenDocumentFito, а неявно самим каркасом приложений.
В SDI-приложении объекты «вид», «документ» и «основное окно-рамка» создаются только один раз и существуют на протяжении всей жизни программы. Функция CWinApp;:OnFileNew вызывается функцией Initlnstance. Кроме того, она вызывается в ответ на выбор в меню File команды New. В данном случае OnFileNew должна вести себя несколько иначе. Она не формирует объекты «вид», «документ» и «рамка», так как они уже созданы. Вместо этого она использует существующие объекты повторно и выполняет вышеперечисленные операции 5, 6 и 7. Заметьте: OnFileNew всегда вызывает (неявно) DeleteContents, чтобы очистить документ,
Функция OnNewDocument класса «документ».
В главе 16 Вы уже встречались с функцией-членом класса «вид» OnlnitialUpdaten функцией- членом класса «документ» OnNewDocument, Если бы SDl-приложение не использовало объект- документ повторно, функция OnNewDocument была бы не нужна, так как всю инициализацию документа можно было бы провести в конструкторе его класса. Но реально Вы должны переопределить функцию OnNewDocument, чтобы инициализировать объект-документ всякий раз,
когда пользователь выбирает в меню File команду New или Open. Арр Wizard поможет Вам в этом, создав заготовку функции в сгенерированном производном классе документа.
ПРИМЕЧАНИЕ Неплохо бы свести к минимуму объем операций, выполняемых в конструкторах. Чем их меньше, тем ниже вероятность сбоя в конструкторе — а ошибки там могут иметь тяжкие последствия. Функции, подобные CDocument::OnNewDocumentw CView::OnlnitialUpdate, — идеальное место для начальной очистки. Если возникнут какие-то проблемы, Вы сможете вывести сообщения в информационном окне, а при вызове OnNewDocument — и возвратить FALSE. Обе функции можно вызывать для данного объекта неоднократно. Если какие-то действия надо выполнить однократно, объявите специальную переменную-член (флажок) — она послужит признаком «первого вызова*.
Связывание File Open с кодом сериализации: функция OnFileOpen
Арр Wizard, генерируя приложение, сопоставляет команде Open из меню File функцию-член С№1пАрр::ОпЕ11еОреп;каторая выполняет следующие действия.
Предлагает пользователю выбрать файл.
Вызывает виртуальную функцию-член CDocument::OnOpenDocumentjuiayxe существующего объекта-документа. Та открывает файл, вызывает CDocument-DeleteContents, создает объект САтсЫие, подготовленный для загрузки, и затем вызывает функцию Serialize документа, которая загружает данные из архива.
Вызывает функцию GnlnitialUpdateKJtacca. «вид*.
Альтернатива команде Open меню File — список последних открывавшихся файлов (Most Recently Used file list). Каркас приложений запоминает последние четыре файла и показывает их имена в меню File. В промежутке между запусками программы эти имена хранятся в реестре Windows.
ПРИМЕЧАНИЕ Можно изменить тело последних открывавшихся файлов, которое должно запоминаться, вызвав с соответствующим параметром функцию LoadStdProfileSetting в функции Initlnstance класса приложения.
Функция DeleteContents класса «документ»
При загрузке данных из дискового файла в существующий объект-документ SDI надо как-то стереть текущее содержимое объекта. Лучший способ сделать это — переопределить виртуальную функцию CDocttment::DeleteContents-B вашем производном классе документа. Как Вы уже видели в главе 16, такая переопределенная функция делает все, что нужно для очистки переменных-членов класса документа. При выборе в меню File команд New и Open функции OnFileNewti OnFileOpen класса CDocument обращаются к DeleteContents, а значит, она вызывается сразу после создания объекта-документа (и вновь вызывается при закрытии документа).
Чтобы ваши классы документов работали в SDI-приложениях, очищайте содержимое документа в функции DeleteContents, а не в деструкторе. Последний используйте только для очистки элементов, существующих на протяжении всей жизни объекта.
Связьвание Rie Save и Rie Save As с кодом сериализации
Арр Wizard, генерируя приложение, сопоставляет команде Save меню File функцию-член OnFi- leSave класса CDocument OnFileSave вызывает функцию OnSaveDocument класса CDocument,, которая, в свою очередь, обращается к функции Serialize документа, передавая ей объект-архив, подготовленный для сохранения. Команда Save As из меню File обрабатывается аналогично — ей сопоставляется функция OnFiteSaveAs класса CDocument, которая вызывает OnSaveDocument, Все операции с файлами, необходимые для сохранения документа на диске, осуществляет здесь каркас приложений.
ПРИМЕЧАНИЕ Очевидно, что командам file New и File Open сопоставляются функции-члены класса приложения, a File Save и File Save As связываются с функциями-членами класса документа. File New связана с OnFileNew. Версия Initinstance для SDI-приложения тоже вызывает OnFileNew (неявным образом). Объект-документ не существует в момент вызова initinstance каркасом приложений, поэтому OnFileNew не может быть функцией-членом CDocument. Но при сохранении документа объект-документ, разумеется, существует,
Флажок «документ изменен»
Многие приложения Windows, ориентированные на документ, отслеживают изменения в нем. При его закрытии или выходе из программы появляется окно с запросом, следует ли сохранить текущий документ. Каркас MFC-приложений поддерживает такое поведение при помощи переменной-члена m_bModyie<fcna£ca CDocument. Эта логическая переменная равна TRUE, если документ изменен (становится «грязным» — «dirty»), и FALSE, если он не изменился.
Доступ к защищенному флажку mJyModified осуществляется через функции-члены SetModi- jtedFlag и IsModifle<^a.ccs CDocument. Когда документ создается, открывается или сохраняется на диске, флажок объекта-документа устанавливается в FALSE. При изменении его данных Вы должны устанавливать этот флажок в TRUE с помощью ^ейУ/о^^ейР/^Виртуальная функция CDocumenL-.SaveModipedtsoTopyio каркас приложений вызывает, когда пользователь закрывает документ, выводит информационное окно, если флажок mJyModified установлен в TRUE, Если Вам нужно выполнить другие действия, переопределите эту функцию.
В примере ЕХ17А Вы увидите, как простейшая функция, обновляющая командный пользовательский интерфейс, использует IsModifiedдля управления состоянием кнопки на панели инструментов и команды в меню, которые обеспечивают доступ к сохранению документа. При изменении документа кнопка на панели инструментов с изображением дискеты активизируется, а когда пользователь сохраняет файл, она блекнет.
ПРИМЕЧАНИЕ SDI-программы, написанные на базе MFC, ведут себя несколько иначе, нежели другие SDI-приложения для Windows вроде Paint. Вот как выглядит типичная последовательность событий
Пользователь создает документ и сохраняет его надиске как, скажем, testdat.
Пользователь изменяет документ.
Пользователь выбирает команду Open из меню File и указывает файл testdat
При выборе команды File Open программа Paint спрашивает, нужно ли сохранить изменения в документе, сделанные на этапе 2. Если пользователь отвечает «нет», программа вновь считывает документ с диска. Приложение на MFC считает изменения постоянными, не перезагружая при этом файл.
Пример ЕХ17А
Программа ЕХ17А очень похожа на ЕХ16В, Диалоговое окно для ввода информации о студенте и растровые изображения — те же, класс «вид» тоже не изменился. Но в новой программе мы добавим сериализацию вместе с функцией обновления командного интерфейса для File Save. Заголовочные файлы и файлы реализации для классов «вид» и «документ» из этого примера будут использованы и в примере ЕХ18А в следующей главе.'
Далее приведен весь новый код, отличающийся от кода ЕХ16В, причем полужирным выделены все дополнения и изменения по сравнению с кодом, сгенерированным AppWizard и ClassWizard. Список файлов и классов в ЕХ17А сведан в таблицу.
3 а голо в оч ны й файл
Файл с исходным кодом
Класс
Описание
exl7a.h
exl?a.cpp
CExl 7eApp
Класс приложения (создан AppWizard)
CAboutDlg
Диалоговое окно About
MainFrm.h
MainFrm.cpp
CMainFrame
'Основное окно-рамка SDI
StuDoch
StuDoc.cpp
CStuderaDoc
Документ с данными о студенте
StuView.h
StuView.cpp
CSludentView
Представление информации о студенте (из ВХ16В)
Student, h
Student.cpp
CStudent
Запись о студенте
StdAfx.h
SrdAfx-cpp
Предкомпилированные заголовочные файлы (с добавлением afxtempl.h)
CStudent
Файл Student.h из ЕХ17А практически тот же, что и в проекте ЕХ16А (см рис. 16-2). Заголовочный файл вместо:
DECLARE„D¥MAMlC(CStudent)
содержит макрос:
О ЕС LARE_S ERIAL(CStudent)
а файл реализации вместо;
IMPLEMENT_DYMAMIC(CStuaent, CObject)
содержит макрос:
IMPLEMEHT_SERIAL(CStud6nt, CObject, 0)
Кроме того, добавлена виртуальная функция Serialize.
СЕх17аАрр
Файлы класса приложения (см. рис. 17-4) содержат только код, сгенерированный AppWizard. Это приложение сгенерировано с расширением файла документа по умолчанию и с поддержкой запуска из MicrosoftWindows Explorer, а также с поддержкой drag-and-drop, Подробнее об этих возможностях мы поговорим, позже в этой главе.
Чтобы сгенерировать дополнительный код, нужно при первом запуске AppWi-zardHa шаге 4 щелкнуть кнопку Advanced и в появившемся диалоговом окне Advanced Options ввести расширение файлов в поле ввода в левом верхнем углу, как показано на рисунке.
Это гарантирует, что строка ресурса шаблона документа содержит правильное расширение, а в функцию-член inillnstance класса приложения внесен код поддержки запуска из Explorer. При желании можно изменить и другие подстроки ресурсов. ■
ПРИМЕЧАНИЕ Сгенерированные вызовы EnableddControls и EnableSdControlsStatic в CEx17aApp::lnit- Instance не являются необходимыми при работе с Windows 95/98 или Windows NT 4.0. Эти функции поддерживают старью DLL, поставляемые с Windows NT 3.51.
ЕХ17А.Н
// ex!7a.h : основной заголовочный файл приложения ЕХ17А
//#Lf !defined(AFX ЕХ17А Н 1AO36EA3_821A 1i(to_8FE2_OOC04FC2AOC2_INClUDED_)
#def jne AFX_EX17A_H„1AO36EA3_821A J1 DO_8FE2 J)0C04FC2A0C2 „INCLUDED.
#if .msc.ver > iooo
flpragma once#endif // _MSC VER > 1000
#lfndef __AFXWIN_H__
#error include "stdafx.h' before including this file for PCH tfendif
ttinclude "resource, h" // основные идентификаторы
/////////////////////////////////////////////////////////////////////////////
// CExl7aApp:
// Реализацию этого класса см. в ех17а.срр //
class СЕх17аАрр : public CWinApp
{
public:
СЕх17аАрр();
// Переопределенные функции
// переопределенные функции, сгенерированные Classwizard
//{<AFX_VlRTUAL(CEx17aApp)
public:
virtual BOOL InitlnstanceO;
//}}AFX_VIRTUAL
// Реализация■
//{(AFX_MSG(CEx17aApp).
afxjnsg void OnAppAbout();
7/ ПРИМЕЧАНИЕ: здесь Classwizard будет добавлять и удалять функции-члены.
// НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода!
//DAFX MSG
DECLARE MESSAGE_MAP()
77ПШППШП1ПП111НШП1ПП1НШНПНПП7ПН11ПНППП '
ПК {A FX_I NSERT_ LOCATION}}
// Microsoft Visual C++ добавит дополнительные объявления
// непосредственно перед предыдущей строкой■
tiendif // I defined(AFX_EX17A„H_1A036EA3_821A_11D0_8FE2_00C04FC2A0C2_INCLUDED_)
Рис. 17-4. Листинг класса СЕх17аАрр
ЕХ17А.СРР
// ех17а.срр : Реализация класса приложения. //
«include "StdAfx.h" .
«include "ex17a,h"
ttinclude "MainFrm.h”
«include "StuDoc.h"
«include "StuView.h"
flifdef .DEBUG
«define new DEBUG NEW
«undef THIS FILE "
static char THIS_FILE[] = __FILE__;
flendif
iiiiiHiiiiiiiiiiMiiiiiiiniiiiimiiniiiiiiiiriiiiHiiniiiiniuiiiHHii
H CExllaApp
BEGIN MESSAGE MAP(CEx17aApp, CWinApp)
//HAFX_MSG_MAP(CEx17aApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
// ПРИМЕЧАНИЕ! здесь ClassWizaro будет добавлять и удалять макросы таблицы сообщений.
//НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода!-
//}}AFX_MSG_MAP
// стандартные команды для работы с файлами
ON COMMAND(ID FILE NEW, CWinApp: :OnFileNew)
ON COMMAND/ID FILE.OPEN, CWinApp: :DnFileOpen)
END.MESSAGE.MAPO
шшшшшшшшнн1шш1пшшпнпшш1нм^^^^
Н Создание CExl7aApp
CEx17aApp::CEx17aApp()
// ДОДЕЛАТЬ: добавьте сюда код создания.
// Основную часть инициализации предусмотрите в Initlnstance
//т?///////////////////////^^
// Единственный объект СЕх17аАрр
СЕх17аАрр theApp;
/////////////////////////////////////////////////////////////////////////////
// Инициализация СЕх17аАрр
IBOOL СЕх17аАрр: :lnitlnstance() {
AfxEnableControlContalnerO;
// Стандартная инициализация.f
// Если Вы не используете эти ВОЗМОЖНОСТИ и хотите уменьшить размер
// исполняемого файла, удалите ненужные процедуры инициализации.
ttifdef_AFXDLL
Enable3dControls();// вызываем при использовании MFC в разделяемой DLL
«else
Рис. 17-4. (продолжение)
Enable3dControlsStatic();// вызываем при статической компоновке с MFC'
ffendif
.// Изменяем раздел реестра, где хранятся настройки.
// 8 реальных программах эта строка быть осмысленной,
// например, содержать название компании или организации.
SetflegistryKey(_T("Local AppWizard-Geherated Applications"));
LcadStdProfileSettings (); // загрузка стандартной информации из файла INI // (включая список последних открывавшихся файлов)
// Регистрируем шаблон документа приложения. Шаблоны документов
// предназначены для связи документов, окон-рамок и окон представления.
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDA MAINFRAME,
RUNflME_CLASS(CStudentDoc>,"
RUNTIMEZCLASS(CMainFrame), // основное окно-рамка SDI
RUNTIME_CLASS(C$tudentView)):
AddDocTemplate(pDocTemplate):
// разрешаем открытие nc DDE Execute
EnableShellOpen();
AegisterShellFileTypes(TRUE);
// поиск в командной строке стандартных команд оболочки,
// DDE и имени открываемого файла
CCommandLlnelnfo cmdlnfo;
ParseCommandLine(cmdlnfc);
// выполняем действия, заданные в командной строке
if (!ProcessShellCommand(cmdlnfo)) • return FALSE;'
// Единственное окно инициализировано,'
// показываем его.
m_pMainWnd->ShowWindow(Snl_SHOW);/
m_pMainWnd->UpdateWlndow():
// Разрешаем открытие файла операцией drag-and-drop
ni,pMainWnd->DragAcceptFi Les();■
return TRUE;"
},..
/////////////////////////////////////////////////////////////////////////////
// CAboutDlg (диалоговое окно About)-
class CAboutDlg : public CDialog { public:
CAboutDlg<);
// Данные диалогового окна,■•.
//{{AFX_DATA(CAboutDlg)
enum ( IDD = IDD_A8OUTBOX };
//}}AFX_DATA
// переопределенные виртуальные функции, сгенерированные Classwizard
//{{AFX_VIRTUAL(CAboutDlg)
protected:
virtual void DoDataExchange(COataExchange* pDX); // поддержка DDX/OOV //}}AFX_VIRTUAl
// реализация
protected:
//{{AFX_MSG(CAboutDlg)
// нет обработчиков сообщений
//}}AFX MSG
DECLARE_MESSAGE_MAP()
CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD) {
//{{AFX DATA iNlT(CAbdutDlg)
//}JAFX DATA.JNIT
void CAboutDlg::DoDataExchange(CDataExchange* pDX) {
CDialog::DoDataExchange(pDX):'
//{{AFX DATA MAP(CAboutDlg)
//}}AFX_DATA_MAP
BEGIN MESSAGE MAP(CAboutDlg, CDialog)
//{(AFX_MSG_MAP(CAboutDlg)
// нет обработчиков сообщений
//}>AFX MSG MAP
EMDJESSAGE_MAP(>
// команда для отображения диалогового окна void СЕх17аАрр::OnAopAbout()
{
CAboutDlg aboutDlg;
aboutDlg.DoModa1():
CMainFrame
Код класса основного окна-рамки (см. рис. 17-5) практически не изменен по сравнению с кодом, сгенерированным AppWizard. Добавлены константы для кнопок на панели инструментов, а переопределенная функция ActivateFrome и обработчик сообщения WM DROPFILES присутствуют исключительно для трассировки.
MAINFRM.H
// MainFrm.h : интерфейс класса CMainFrame
//
ШШ11111Ш11Ш1Ш1НН111111ШШ11Ш1!ПШШШ1ШШШ!11
#if !defined(AFX MAINFRM H_1A036EA7 621A 11D0_8FE2 00C04FC2A0C2_INCLUDED )
«define AFX.MAINFRM H__1A036EA7 821A 11D0_8FE2_00C04FC2A0C2_INCLUDED_•
#if _MSC_VER >= 1000
«pragma once
tfendif // _MSC_VER >= 1000
Рис. 17-5. (продолжение)
class CMainFrare : public CFrameWnd {
protected: // создание только сериализацией
CMainFrame():
DEClAflE_DYNCREATE(CMainFranie)
// атрибуты public:
,// операции public:
// Переопределенные функции
// переопределенные функции, сгенерированные Classwizard
//{{AFX VIRTUAL(CMainFrame)
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
virtual void ActivateFrame(int nCmdShow = -1);
//i}AFX „VIRTUAL
// Реализация .
public:•
virtual 'CMainFrame();
llifdef „DEBUG
virtual void AssertValidQ const;
virtual void Dump(CDumpContext& de) const; ftendif
protected: // внедренные переменные-члены панелей элементов управления
CStatusBar m_wndStatusBar;
CToolBar m_wndToolBar;
.// сгенерированные функции таблицы сообщений protected:
//{{AFX_HSG(CMainF tame)
afx„msg int OnCreate(LPCREATESTRUCT IpCreateStruct);
afx msg void OnDropFileaCHDROP hDropInfo);
//}}AFX_MSG-
DECLARE MESSAGE MAP()
};■
llllfllinillHIIIIIIIIIIIIIflfllliniillliniilfllffllllHHIllllilf
//
//{{AFX_INSERT_LOCATION)}
// Microsoft Visual C++ добавит дополнительные объявления непосредственно перед предыдущей строкой
tfendif // ! def ined(AFX_MAINFRM_H_1A036EA7_B21A_11D0_8FE2_00C04FC2A0C2__INCLUDED_)
MAINFPM.CPP.
// MalnFrm.cpp : реализация класса CMainFrame
//'
«include "stdafx.h" flinclude "ex17a. h"
«include "MainFrm.h"
tfifdef „DEBUG
«define new DEBUG NEW .
#undef THIS FILE
static char THIS_FILE[] = _FILE_;
ttendif ‘
/////////////////////////////////^
// CMainFrame
IMPLEMENT_OYNCREATE(CMainFrame, CFrameWnd)
BEGIN MESSAGE MAPfCMainFrame. CFrameWnd)
//({AFX_MSG_MAP(CMainFrame)
ON WM_CREATE()
ON WM DRDPFILES()
//})AFX MSG MAP
END_MESSAGE_MAP(>
static UINT indicators!] =
ID_SEPARATOR. // индикаторы в строке состояния
ID INDICATOR CAPS.
ID INDICATOR NUM,
ID INDICATOR SCRL.
};
/////////////////////////////////////////////У///////////////////////////////
// Созданиё/уничтожение CMainFrame
CMainFrame; :CMainrrame()
{
// ДОДЕЛАТЬ: инициализацию переменных-членов
CMainFrame::"CMainFrame()
int CMainFrame: :OnCreate(LPCREATESTRUCT IpCreateStruct)
1'
if (CFrameWnd::OnCreate(lpCreateStruct) -= -1)
return -1;
if (!m wndToolBar.Create(thls) || ImwndToolBar. LoadToolBar(IDR MAINFRAME))
{'
TRACED(" Ошибка при создании панели инструментов\п"); ’ return -1; // ошибка при создании окна
}
if ( !m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators, sizeof (indicators)/sizeof (UINT)))
TRACED ( "Ошибка при создании строки состояния\п "};
return -1; // ошибка при создании окна
/I ДОДЕЛАТЬ: удалить, если Вам не нужны всплывающие подсказки // или изменяющая размеры панель инструментов
m wndTODlBar.SetBarStyle(m wndToolBar.GetBarStyle() |
' CBRS.TOOLTIPS CBRsZfLYBY | CBRS_SIZE_DYNAMIC);
return 0;
Рис. 17-5. (продолжение)
Л ДОДЕЛАТЬ: удалите эти три строки, если не нужна стыкуемая панель инструментов ro_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS ALIGN ANY);-
DockControlBar(&m_wndToolBar);
return 0;
}
BOOL CMainFrame; :PreCreateWlndow(CREATESTRUCT& cs)
{
// ДОДЕЛАТЬ: измените класс или стили окна путем изменения
// CREATESTRUCT cs
return CFrameWnd: :PreCreateWindow(cs);
}
/////////////////////////////////////////////////////////////////////////////
// диагностика CMainFrame■
#ifdef _DEBUG
void CMainFrame: :AssertValid() const
{
CFrameWnd; :AssertValid();
}'
void CMainFrame: :Dump(CDumpContext& de) const
i
CFrameWnd::Dump(dc):
}■
tfendif //.DEBUG
ППШ11Ш1П11111111111111111Н1Н11Ш1111ШН1111111111П11Ш11Н1Ш1
И обработчики сообщений CMainFrame
void CMainFrame: :ActivateFrame(int nCmdShow)
TRACE("Входим в CMainFrame::ActivateFrame\n");
CFrameWnd:: ActivateFrame(nCmdShow);
}
void CMainFrame: :OnDropFiles(HDROP hDropInfo)
TRACE("Входим в CMainFrame::OnDropFiles\n");
CFrameWnd::OnDropFiles(hDropInfo);
}
CStudentDoc. .
Этот класс идентичен классу CStudentDoc из предыдущей главы (см. рис. 16-4), за исключением четырех функций: Serialize, DeteteContents, OnOpenDocumentK OnUpdateFileSave.
Serialize
К коду функции, сгенерированному AppWizard, добавлена всего одна строка сериализации списка студентов:
/////////////////////////////////////////W^
И сериализаций CStudentDoc
void CStudentDoc;:SerlaIize(CArchive& ar).
{
TRACE(“Входим в CStudentDoc::Serialize\n"); if (ar. IsStoringO)
// ДОДЕЛАТЬ: добавить сюда код сохранения 1 else {-
// ДОДЕЛАТЬ: добавить сюда код загрузки
■„studentlist.Serialize(ar);
}
DeleteCbntents
Оператор Dump заменен простой директивой TRACE. Модифицированный код приведен ниже:
void CStudentDoc: :DeleteContents()
TRACE ("Входим в CStudentDoc: :DeleteContents\n‘);
while (m_studentLlst.GetHeadPosition()) { delete m_studentList.flemoveHead();■
1
OnOpenDocument
Эта виртуальная функцияпереопределена только длятого, чтобы вывести трассировочное сообщение:
BOOL CStudentDoc::OnOpenDocument(LPCTSTR LpszPathName)
TRACE("Входим в CStudentDoc: ;OnOpenDocument\n’);
if (ICOocument: :OnOpenDocument(lpszPathName)) return FALSE;
// ДОДЕЛАТЬ: добавьте сюда специализированный код создания документа
return TRUE;■
}
OnUpdateFiteSave
Эго функция таблицы сообщений, которая отключает кнопку File Save на панели инструментов, если флажок «документ изменен» не установлен. Класс «вид» управляет состоянием этого флажка через функцию SetModifiedFlag.
void CStudentDoc: :OnUpdateFileSave(CCmdUI* pCmdUI) "
{„
// отключить кнопку с изображением дискеты, если файл не изменен pCadUI->Enable(IeKodified());
}
CStudentView
Код класса CStudentView позаимствован из предыдущей главы (см рис. 16-5).
12 Зах. №787
Тестирование приложения ЕХ17А
Собрав программу, запустите ее под управлением отладчика и протестируйте, введя какие- нибудь данные и сохранив их на диске под именем Test.l7a. (Набирать .//^необязательно.)
Выйдите из программы, перезапустите ее и откройте сохраненный Вами файл. Есть ли там введенная ранее информация? Загляните в окно отладчика и проверьте последовательность вызовов функций — она должна быть такой:
Входим в конструктор CStudentDoc
Входим в конструктор CStiidentView
Входим в CStudentDoc: ;OnNewDocument
Входим в CStudentDoc::DeleteContents
Входим в CStudentView::OnLiitialUpdate
Входим в CStudentView::OnUpdate„
Входим в CMainFrame;:ActivateFrame
Входим в CStudentDoc::OnOpenDocument
Входим в CStudentDoc;:DeleteContents
Входим в CStudentDoc;;Serialize
Входим в CStudent: Serialize
Входим в CStudent: Serialize■
Входим в CStudent: :Serialize
Входим в CStudentView: :OnInitialUpdate
Входим в CStudentView::OnUpdate
Входим в CMainFrame:;ActivateFrame
Запуск программ из Windows Explorer и операция drag-and-drop
В прошлом пользователи «персоналок» запускали какую-то программу, а потом выбирали файл (иногда называемый «документом»), содержащий данные в фермате, понятном этой программе. Так работали многие программы для MS-DOS, Старый Диспетчер программ Windows упростил запуск программы .— двойным щелчком ее значка. В то же время пользователям Apple Macintosh работать было еще проще: они дважды щелкали значок документа, а операционная система сама определяла, какую программу запустить.
Б нынешнем Windows Explorer (и диспетчере файлов в Windows NT) программу можно запускать двойным щелчком не только ее значка, но и значка одного из ее документов. Но как Explorer определяет, какую программу надо запустить? Чтобы связать документ и программу, он использует реестр Windows. Основой сопоставления служит трехбуквенное расширение имени файла, которое Вы вводили в Арр Wizard. Но запуск — это далеко не все. Установив связь определенного вида документов с конкретной программой, пользователи могут запускать эту программу, дважды щелкнув значок ее документа или перетащив его мышью из Explorer на значок программы. Кроме того, значок документа можно переместить на принтер, и ваша программа распечатает этот документ.
Регистрация программы
В главе 15 мы выяснили, как сохранять данные в реестре Windows, добавив в функцию Initlns- tance вызов SetRegistryKey. Независимо от того, добавили Вы этот вызов или нет, ваша программа может при запуске записывать информацию об ассоциациях файлов в другую часть реестра. Для активизации этой возможности укажите расширение имен файлов при создании приложения с помощью AppWizard (Для этого щелкните кнопку Advanced на шаге 4.) После этого AppWizard внесет расширение в строку шаблона документа, а также вставит следующий вызов в функцию Inttlnstance-.
Regist егShel1FileTypes(True);
Теперь программа добавит в реестр два элемента. В разделе верхнего уровня HKEY_CLAS- SESROOT она создаст подраздел и строку данных, например в ЕХ17А:
17А = Ех17а.Document
Первый элемент — это выбранный для Вас мастером AppWizard ид ентификатор типа файла, a Exl 7а.Document — ключ для поиска самой программы. Параметры подраздела Exl7a.Docu- ment, также расположенного в разделе HKEY_CLASSES_ROOT, расположены на рисунке.
Заметьте: в реестре содержится полное имя программы ЕХ17А с указанием пути, Теперь Explorer, используя реестр, может перейти от расширения к идентификатору типа файла и к самой программе. После того как расширение зарегистрировано, Explorer находит значок документа и отображает его рядом с именем файла:
te$t.17a
Двойной щелчок документа.
Когда пользователь дважды щелкает значок документа, Explorer запускает соответствующую SDI-ирограмму, передавая ей в командной строке имя выбранного файла. AppWizard генерирует в функции Initinstance вызов EnableSbeliOpen, что обеспечивает поддержку запуска посредством DDE-сообщения. Эту технологию применял File Manager Windows NT 3 51. Explorer умеет запускать SDI-приложение и без этого вызова.
Активизация механизма drag-and-drop
Чтобы уже запущенная программа могла открывать файлы, которые пользователь перетаскивает в нее из Windows Explorer, вызовите функцию DragAcceptFilesKnacca. CWndp^a основного окна-рамки приложения. Открытая переменная-член тj>MainWndcfa£X33L-Txg^ указывает на объект CFrameWnd(mni CMDIFrameWnd^KoTRa пользователь «бросает» значок файла внутри окна-рамки, это окно получает сообщение WM_DROP FILES, что приводит к вызову обработчика СРгате^пД.-ОпОтрРИекАктивизирует открытие файлов операцией drag- and-drop следующая строка из Initinstance, генерируемая Арр Wizard: m_pMainWnd->DragAcceptFiles(),'
Параметры команд ной строки
Если выбрать в меню Start пункт Run или дважды щелкнуть программу в Explorer, параметров В командной строке не будет. Функция Initinstance обрабатывает командную строку при помощи вызовов ParseCommandtineH ProcessShellCymmand. Если в командной строке содержится нечто, напоминающее имя файла, программа немедленно загружает этот файл. Поэтому можно создать ярлык Windows, который умеет запускать программу с заданным файлом документа.
Эксперименты с запуском программы из Windows Explorer ■ и операцией drag-and-drop
Собр ав EX 17 А, попро буйте запустить ее из Explorer. Однако сначала запустите пр огр амму как обычно, чтобы поместить в реестр начальные записи. Сохраните надиске, по крайней мере, один 17А-файл и закройте программу ЕХ17А. Запустите Explorer и откройте каталог \vcpp32\exl7a, Дважды щелкните один из 17А-файлов в правой панели окна. Ваша программа должна запуститься и автоматически загрузить выбранный файл. Теперь, когда запущены и ЕХ17А и Explorer, попробуйте перетащить другой файл из Explorer в окно ЕХ17А Программа откроет новый файл так, будто выбрана команда Open из меню File.
Возможно, Вы захотите просмотреть записи в реестре Windows, относящиеся к EXI7A. Тогда запустите программу Regedit (Regedt32 в Windows NT) и раскройте раздел HKEYCLAS- .SES_ROOT. Просмотрите содержимое подразделов «,17А» и «Ех17а,Document». Кроме того, раскройте раздел HKEY_CURRENT_USER (или HKEY_USERS) и изучите подраздел «Software», Там в подразделе сх17а Выдолжны обнаружить Recent File List (список последних открывавшихся файлов). Программа ЕХ17А вызывает SetRegishyKeyco строкой «Local AppWizard-Gene- rated Applications», поэтому подраздел с именем этой программы находится в разделе ех17а.
ГЛАВА
1 8
Чтение и запись документов в MDI-приложениях
О этой главе Вы познакомитесь с MDI-приложениями, создаваемыми на базе MFC 6.0, и узнаете, как они загружают и сохраняют файлы своих документов. По-видимому, именно MDI- приложения лучше программировать при помощи MFC. В частности, AppWizard по умолчанию предлагает создать MDl-приложение, да и большинство примеров программ, поставляемых с Microsoft Visual C++, — это MDI-приложения.
Вы изучите сходства и различия SDI- и MDI-приложений и научитесь преобразовывать SDI- в MDI-программы. Но приступать к изучению MDI-программ можно только, когда Вы досконально разберетесь в материалах предыдущей главы.
MDl-приложение
Прежде чем обратиться к коду MFC-библиотеки для MDI-приложений, стоит присмотреться к работе программ этого типа. Давайте посмотрим на Visual C++. Это MDl-приложение, документами которого служат файлы с исходным текстом программ. Но это не самое типичное MDl-приложение, так как его документы группируются в проекты. Лучше исследовать Microsoft Word или — что предпочтительнее — MDl-приложение, написанное на базе MFC и сгенерированное AppWizard.
Типичное MDl-приложение в стиле MFC
Программа-пример ЕХ18А—это MDI-версия ЕХ17А. Запустите программу ЕХ17Аи проверьте, как, ведет себя SDI-приложение после выбора файла, а потом сравните поведение соответствующей MDI-программы (см. рис. 18-1).
Открыты два разных файла документов, каждый в своем дочернем MDI-окне, но активно только одно дочернее окно — нижнее, расположенное поверх другого. У приложения одно меню и одна панель инструментов; все команды направляются активному дочернему окну. Заголовок основного окна содержит имя файла документа, открытого в активном дочернем окне.
Дочернее окно можно свернуть в значок внутри основного окна. Меню Window (см. рис. 18-1) обеспечивает управление дочерними окнами при помощи следующих команд.
Команды меню
Действие
New Window
Cascade
Tile
Arrange Icons (имя документа)
Открывает дополнительное дочернее окно для текущего документа
Располагает существующие дочерние окна каскадом
Располагает существующие дочерние окна без перекрытия(
Упорядочивает значки окон в пределах окна-рамки
Активизирует соответствующее дочернее окно и помещает его поверх других окон
Рис. 18-1, MDI-приложениесдвумя открытыми файлами и раскрытым меню Window
На рисунке 18-2 показано как будет выглядеть окно профаммы, если закрыть оба дочерних окна (и открыть меню File).
Рис. 18-2. Окно программы ЕХ18Абез дочерних окон
Меню File теперь изменилось, большинство кнопок на панели инструментов отключено, а в заголовке окна нет имени файла. Единственное, что можно сделать, — создать новый документ или загрузить существующий.
Рис. 18-3. Окно программы ЕХ18Ас первоначальным д очерним окном
На рисунке 18-3 та же программа показана сразу после запуска и создания нового документа. Единственное дочернее окно полностью развернуто. Ему присвоено имя документа по умолчанию Ех18а1. Это имя формируется по значению параметра Doc Type Name (т. е. Ех18а), указанному Вами в длазужаьам окне Advanced Options после щелчка кнопки Advanced на шаге 4 мастера Арр Wizard. Первому новому файлу будет присвоено имя Ех18а1, второму — Ex 18а2 и т. д. Но при сохранении документа пользователь обычно задает какое-то другое имя.
MDl-приложенияна базе MFC, каки многие коммерческие МГЛ-ирограммы, при запуске автоматически создают новый пустой документ. (Visual C++ Developer Studio в этом является исключением.) Чтобы ваша программа при запуске открывал а пустое окно-рамку, измените аргумент в вызове ProcessSbellCommandB файле реализации класса приложения, как показано в примере EX 18 А."
Для тех, кто программирует в Win32. ? ; “ 1а ?: ---.у-^-уу
., Windows напрямую поддерживает MDI-приложения, начиная с версии 3-Q- На базе этой, : поддержки MFC-библиотека создает MDI-среду, сходную со Spi-средой. В MDI-прило-.
женил для Win32 основное окно-рамка содержит меню и одно клиентское окно (client window). Последнее управляет дочерними окнами, сопоставленными документам. У ' клиентского MDI-окна есть свой стандартный оконный класс (не путайте с С-Н-^класг;
сом) с процедурой, обрабатывающей специальные сообщения типа WM^MDKASCADE 2
• и WM_MD1TILE. Соответствующая процедура дочернего MDI-окна аналогична имею- . ■
- щейся у основного SDI-окна. ■’. .. _ ,, VV? 'V. . : . ;,., . tl .. .
. Класс ^fDZFrtwwWfridlii^ MFC инкапсулируег ^нк^Ьййльные Boskowbr
■ сти как основного окна-рамки, так и клиентского jMDl-окЙПЙ этом классе предусмот- .: ~ рены обработчики всех МОЬсообщений Windows,"" благодаря чему он может управлять ■' ; своими дочерними окнами, представленными объектами класса: G^^DlCbilftWd^.. y^V •%
Объект «MDI-приложение»
Вероятно, Вас интересует, как работает MDI-программа и какой код заставляет ее вести себя иначе, чем SDI-прнложение. Однако процесс запуска приложений обоих типов во многом одинаков. Объскг-приложениепроизводного от СЛ^йМр/класса включает переопределенную функцию-член Inittnstance. Она немного отличается от функции Initlnstance SDI-приложения и начинается с вызова AddDocTemplate.
Класс шаблона MDI-документа✓.
Вызов для создания шаблона MDI в функции Iniflnstance выглядит так
CM u11IDocTer pl are - pDocTemp1 atв;
pDocTemplKte = new CMultil)ocTenplate(■
I DR EX18ATYPE,.
RUHHME.CLASSCCStudertDoc),
PUNTIME CLASS(CChildFramc), // спсциализиэоваиное дочернее окно-рамка MDI RIMIMEZCLASStCStudertView)) ;
AddDocTempla t e(pDocTemplate);
В отличие от класса CSingleDocTemptate, с которым Бы встретились в главе 17, CMultiDoc- Тстр!а1епозволяет программе использовать разные типы документов и допускает одиовре менное существование более чем одного объекта-документа. Б этом суть MDI-программ.
Единственный вызов AddDocTemplate, показанный выше, позволяет MDI-программе поддерживать несколько дочерних окон, каждое из которых связано с отдельным объектом «документ» и отдельным объектам «вид». Допускается наличие нескольких дочерних окон (и соответствующих объектов «вид»), связанных с одним и тем же объектом «документ». В этой главе мы используем только один класс «вид* и един класс «документ». Применение нескольких классов «вид» и «документ» описано в главе 20.
ПРИМЕЧАНИЕ В процессе работы программы объект — шаблон документа поддерживает список активных объектов-документов, созданных с его помощью. «Проходить* по этому списку позволяют функции-члены класса CMukiDocTemplate — GetFirstDocPosition и GetNextDoc. Для перехода от документа к его шаблону используйте CDocUment::GetDocTemplate.
Окно-рамка и дочернее окно в MDI-программе
В примерах SDI-программ присутствовал лишь один класс окна-рамки и один объект этого класса. Для SDI-приложёний Арр Wizard генерирует класс с именем CMainFrame, производный от класса CFrameWnd. В MDI-цриложении присутствуют два класса окна-рамки и множество объектов-рамок (см. следующую таблицу). Взаимосвязь окна-рамки и окна представления в MDI-приложении Вы увидите на рисунке 18-4-
Базовый класс
Класс, генерируемый AppWizard
Количество объектов
Меню и панели элементов управления
Содержит окно представления
Объект создается
CMDIFrameWnd
CMainFrame
Только один
Да
Нет
В функции Initbistance класса приложения
CMDICbkdWnd
CChildFrame
По одному на дочернее окно
Нет
Да
Каркасом приложений при открытии нового дочернего окна
Рис, 18-4. Взаимосвязьокна-рамкииокнапредставлениявМП1-программе
В SDI-приложении объект CMainFrame обрамляет приложение и содержит объект «вид». В MDI-приложении эти функции разделены. Теперь в Initlnstance создается объект CMainFrame, а окно представления содержится внутри объекта C.’C'^ZWA'rame.AppWizard генерирует такой код: CMainFrame* pMainFrame = new CMainFrame;
if (IpHainFrame->LoadFr3me(IDP,.MAINFRAME))
return FALSE;
m_pMainWnd = pMainFrame;
(Код вызывает ProcessShellCommamd, чтобы создать дочернее окно-рамку.) pMainFrame->ShowWinddw(ro_nCmdShow);
pMainFrame->UpdatcWindow():
Каркас приложений может создавать объекты CChildFrame динамически, так как указатель периода выполнения иа класс СС/лУ^лзгт? передается конструктору CMultiDocTemplate.
ПРИМЕЧАНИЕ Функция initlnstance MDl-приложения присваивает переменной-члену m_pMainWndкласса CWinApp указатель на основное окно-рамку. Это значит, что если Вам понадобится указатель на основное окно-рамку, Вы сможете получить доступ к m_pMainWnd через глобальную функцию AtxGetApp'.
Ресурсы основного окна-рамки и шаблона документа
В MDI-приложении (например, в ЕХ18А далее в этой главе) есть два отдельных ресурса строк и меню, идентифицируемых константами lDR_MAINFRAMEa IDR_EX18ATYPEXle^nb\ii ресурс используется, если основное окно-рамка пусто, а второй — если в этом окне есть дочерние окна. Вот как выглядят оба строковых ресурса, разбитые на подстроки:
IOA_MAINFRAME
"ех18а"// заголовок окна приложения
IOR_EX18ATVPE•
- "\п"// (не используется)
'Ех16а\п"// основа для имени документа по умолчанию
"Ех18а\п“// имя типа документа
"Ех18а Files (*.18а)\п"// описание и фильтр для типа документа .
".18A\n"И расширение для документов этого типа
"Ex18a.Dacument\n"// идентификатор типа файла в реестре
"Ех18а Document"// описание типа файла в реестре
ПРИМЕЧАНИЕ Компилятор ресурсов не поддерживает конкатенацию (сцепление) строк. Взгляните на содержимое файла ЕХ18A.RC: на самом деле подстроки объединены в одну длинную строку.
Заголовок окна приложения берется из строки lDR_MAINFRAMEHpH. наличии открытого документа к заголовку добавляется имя файла этого документа. Последние две подстроки из ТО^^Х/ЙАТУРаюддерживают запуск с внедрением или операцией drag-and-drop.
Создание пустого документа: функция CWinApp::OnFlleNew
Как и в SDI-приложении, функция Initlnstance в МОГ-программе вызывает OnFileNew (через ProcessSbellCommand). Однако на этот раз основное окно-рамка уже создано. Теперь же OnFile- New, вызывая функцию OpenDocumenlFileKnacca CWinApp,выполняет следующие действия.
Создает объект-документ, но не пытается загружать с диска его данные.
Создает объект (класса ССЫ№г«те)доч.ернего окна-рамки MDI и формирует это окно, не показывая его на экране. Меню IDR_MAINFRAM& основном окне-рамке заменяется на мeню^J□/?JEX/M7УjPETЩeнтификлтop ZDWjEX/SX?^ресурс значка, исполь
зуемый при сворачивании дочернего окна в основном окне.
Создаст объект «вид» и формирует окно представления, не показывая его на экране.
Устанавливает связь между объектами «документ», «дочерняя рамка» и «вид». Не путайте эти связи между объектами со связями между классами, которые устанавливаются вызовом AddDocTemplate.
Вызывает для объекта «документ» виртуальную функцию-член OnNewDocument.
Вызываетдля объекта «вид» виртуальную функцию-член OninittolUpdate.
Вызывает для объекта «дочерняя рамка» виртуальную функцию-член Activate. Frame, чтобы вывести на экран это окно-рамку и окно представления.
Функция О«)'Яе№м;вызывается также командой New из меню File. В MDI-приложении функция OnFileNewpa6(yraeT точно так же, как и при вызове из Initlnstance.
ПРИМЕЧАНИЕ Некоторые из перечисленных выше функций вызываются не из OpenDocumentRte, а неявно — каркасом приложений.
Можно также использовать глобальную функцию AfxGctMainWnd. — Прим, перёе.
Создание дополнительного окна представления
для существующего документа
При выборе из меню Window команды New Window каркас приложений открывает новое дочернее окно, связанное с текущим документом. Соответствующая функция OnWindowNeiv из класса СЛТО/Агллнс^^/выполняет следующие действия.
Создаст объект дочернего окна-рамки (класса CChildFrameyn формирует само дочернее окно, не выводя его па экран.
Создает объект «вид» и формирует окно представления, не выводя его на экран.
Устанавливает связь между новым объектом «вид» и существующими объектами «документ» и «основное окно-рамка».
Вызывает для объекта «вид» виртуальную функцию-член OninitialUpdatc.
Вызывает для объекта дочернего окна-рамки виртуальную функцию-член ActivateFrame, чтобы вывести на экран это окно-рамку и окно представления.
Загрузка и сохранение документов
Загрузка и сохранение документов в MDI-приложснии происходят с двумя важными отличиями от SD1: всякий раз, когда документ загружается с диска, создается новый объект-документ, а при закрытии дочернего окна объект-документ уничтожается1. Об очистке содержимого документа перед загрузкой не беспокойтесь, по функцию CDocumcnt::DcleteContems'Bce же переопределите, чтобы класс документа можно было переносить в SDl-среду.
Множественные шаблоны документов
MDI-приложение способно поддерживать множественные шаблоны документов при помощи нескольких BuaoBOBAddOocTempiate. Каждый шаблон может задавать другую комбинацию классов «документ», «вид» и «дочерняя MDI-рамка». При вызове команды New из меню File каркас приложений выводит на экран список, позволяющий выбрать шаблон по имени, заданному в строковом ресурсе (подстрока типа документа). В SDl-приложении множественные вызовыAddDocTempkiteне поддерживаются, так как объекты «документ», «вид» и «рамка» создаются только один раз.
ПРИМЕЧАНИЕ При выполнении программы объект «приложение» ведет список объектов — активных шаблонов документов. «Проходить» по этому списку позволяют функции-члены GetFirstDocTemplate- Position и GetNextDocTemplate класса CWmApp. Вместе с функциями GetFirstDocPosifion и GetNextDoc класса CDocTemplate, перечисляющими документы шаблона, они обеспечивают доступ ко всем объектам-документам в приложении.
Если список имен шаблонов Вас не устраивает, отредактируйте меню File, добавив отдельные команды New для каждого типа документа. Закодируйте обработ чики командных сообщений, как показано ниже, используя подстроку типа документа из каждого шаблона:
v?nu СНуАрп: '.OnFlleNewStudsnt(> (
(ipenNewOocumeatCStudnl");
}
void СМуАор: :OnFileNcwTeacher<) {
OpenNewDocument(“Teachr");
}
' Объект «документ» уничтожается, только если закрыто окно с его последним представлением. Вели же. есть окна с другими представлениями документа, он продолжает существовать. — Прим.перев.
Затем добавьте вспомогательную функцию OpenNeivDocumenU
BOOL СМуАрр::OpenNewDocument(const CString& strTarget)
CString strDocName;
CDocTemplate* pSelectedTemplate;
IPOSITION pos = GetFirstDocTemplatePosition();
while (pos ! = NULL) {
pSelectedTemplate = (CDocTemplate») GetNextDocTenplate(pos);
ASSERT(pSelectedTempIate f=NULL);
ASSERT(nSelectedTemplate->IsKlndOf(RUNTlHE„CLASS(CDocTemplate))): pSelectedTemplate->GetDocString(strDoctiame, CDocTemplate:: doc Name); if (strDocName == strTarget) { // из строкового ресурса шаблона pSelecTedTemplate->OpenDocumentFile(NULL);
return TRUE;
return FALSE;
Запуск MDI-программ из Windows Explorer и операцией drag-and-drop
Если дважды щелкнуть значок документа MDI-приложения в Windows Explorer, приложение запускается, если только оно уже не запущено — в противном случае в запущенном приложении открывается новое дочернее окно для выбранного документа. Для активизации такого поведения необходимо вызватьEnableShellOpenв функции Initlnstanceкласса приложения. Операция drag-and-drop работает примерно также, как и в SDl-программах. Если перетащить файл из Explorer в основное окно-рамку MDI, программа откроет новое дочернее окно-рамку (с соответствующим документом и окном представления) — точно так же, как и по команде Open меню File. Чтобы задать расширение имен файлов, используйте кнопку Advanced на шаге 4 AppWizard.
Пример ЕХ18А
Этот пример — MDI-версия программы ЕХ17А из предыдущей главы. В нем точно такой же кодклассов «документ» и «вид» и те же ресурсы (кроме имени программы). Однако код классов приложения и основного окна-рамки изменился. Ниже приведен лишь новый код, включая сгенерированный мастером AppWizard. Список файлов и классов в проекте ЕХ18А приведен в таблице.
Заголовочный файл
Файл исходного код а
Класс
Описание
ex!8a,h
ex18a.cpp
CExl8aApp
Класс приложения (создан AppWizard)
CAboutDig
Диалоговое окно About
MainFrm-h
MainFrm.cpp
CM«inFr«me
Основное окно-рамка MDI-программы
ChildFrm.h
ChildFrm.cpp
CChildFrame
Дочернее окно-рамка MDI-программы
StuDoch
StuDoc.cpp
CSludenlDoc
Документ с данными о студенте (из ЕХ17А)
StuView.h
StuVlew.cpp
CStudenfView
Представление информации о студенте (из ЕХ16В)
Studenth
Student.cpp
CStudent
Запись о студенте (из ЕХ17А)
StdAfx.h
StdAfxcpp
ПредкомпилИрованные заголовочные файлы
(с добавлением afxtempl.h)
СЕх18аАрр
В листинге исходного кода <Жс/8«<4ррфункция-член ОрепОоситпетРНепереопредедена исключительно для вставки операторов TRACE. Кроме того, добавлено несколько строк перед
вызовом ProcessSbellCommandB Initlnstance. В них проверяется аргумент для ProcessSbell- Command, и при необходимости он изменяется, чтобы предотвратить автоматическое создание окна пустого документа при запуске программы. Исходный текст программы показан на рисунке 18-5..
ЕХ18А.Н
// ex18a.h : основной заголовочный файл приложения ЕХ18А
//#if !defined(AFX ЕХ18А H„7B5FE267 85Е9 1100 8FE3 00C04FC2A0C2_INCLUDED ).
«define AFX_EX18A_H_7B5FE267_85E9_11D0_8FE3_0CC04FC2A0C2__INCLUDED_
«if _MSC„VER > 1000
fl pragma once
fiendif // „MSC_VER > 1000
ffifndef_AFXWIN_li__,
«error include ‘stdafx.h" before including this file for PCH'
«endif
«include " resource.h" // основные идентификаторы
UllliUlllHHUlimHIIIIIHMIlINlUIIIIHUIIIIIIIIIIlUIHUIHIIIIII
H CEx18aApp:
И Реализацию класса см. в ех18а.срр
п■
class CExlSaApp ; public CWinApp
public:
CExlSaApp ();
// Переопределенные функции
// переопределенные функции, сгенерированные ClassWizard
//{{AFX_VIRTUAL(CEx18aApp>
public:
virtual BOOL Initlnstance();
//}}AFX_VIRTUAL
// Реализация
//{{AFX_MSG(CEx18aApp>
afx„msg void OnAppAbout();
// ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять и удалять функции-члены.
//НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода!
//}}AFX MSG
DECLARE MESSAGE MAP()
/////////////////////////////////////////
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ добавит дополнительные объявления
// непосредственно перед предыдущей строка!■
flendif // I defined(AFX_EX18A_H_7B5FE267_85F9_11OO 8FE3_0GC04FC2A0C2_INCLUDED,:
ЕХ18А.СРР■
П ех18а.срр : определяет поведение класса приложения //■
♦include "StdAfx.h" flinclude "exl8a.h"
♦include "MainFnn.h"
♦include 'ChildFn.h"
♦include "StuDoc.h”
♦include "StuView.h”
ttifdef .DEBUG-
♦define new DE9UG.NEW
#undef THIS FILE "
static char THIS.ЩЕП = __FILE_;
♦endif
/////////////////////////////////////////////////////////////////////////////
// CEx18aApp
BEGIN_MESSA3E_MAP(CExl8aApp, CWinApp)
//{{AFX HSG MAP(CEx18aApp)
0«.COMMAND(jD.APP.ABOUT, 0nAppAbouI)
// ПРИМЕЧАНИЕ: здесь ClassVJizard будет добавлять и удалять макросы таблицы сообщений. // НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода!
//}>AFX_M8G_KAP
// стандартные команды для работы с файлами
ON COMMANDdD.FILE NEW. CWinApp::CnFileNew)
ON COMMANDdD dLE OPEN. CWinApp::OrFileOpen)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
И Создание CEx18aApp)
CEx18aApp::CExlSaApp()1
{
// ДОДЕЛАТЬ! добавьте сюда код конструктора..
// Основную часть инициализации предусмотрите в Inltlnstance
///////////////////////////////////////////////////////////////////////////// // единственный обьект СЕх18аАрр
СЕх18аАрр theApp;
/////////////////////////////////////W^^
// инициализация СЕх18аАрр
BOOL CExiSaApp::IniTlnstance() {
// стандартная инициализация.
// Если Вы не используете эти возможности и хотите уменьшить размер // исполняемого файла, удалите ненужные процедуры инициализации.
ftifdef.AFXDLL
Enable3dControls(); // вызываем при использовании MFC в разделяемой DLL '
Рис. 18-5. (продолжение)
#else
Enable3dControlsStatic(): // вызываем при статической компоновке с MFC »endif
// изменяем раздел реестра, где хранятся настройки.
//В реальных программах эта строка должна быть более осмысленной, // например, содержать название компании или организации.
SetRegistryKey(_T("Local AppWizard-Generated Applications’’));
LoadStdProfileSettings(): // загрузка стандартной информации из INI-файла
// (включая список последних открывавшихся файлов)
// Зарегистрировать шаблон документа приложения. Шаблоны документов // предназначив для связи документов, окон-рамок и окон представления.
CMultiDocTemplatei pDocTenplate;
pOocTemplate = new CMultiDocTemplate(
IDR_EX18AIYPE,’
RUNTIME_CLASS(CStudentDoc),
RUNTIME_CLASS(CChiLdFrame), // специализированное дочернее окно-рамка MOI RUNTIME_CLASS(CStudentView)):
AddDocTemplate(pOocTemplate);
И создаем основное окно-рамку MOI
CHeinFrame* pMainFrame = new CtfaifiFrame;
if (! pMainFrame->LoadFratne(IDR_MAINFRAME))
return FALSE;'
•'m_pMainWnd = pMainFrame;
// активизируем открытие no drag-and-drop m_pMainWnd->OragAcceptFiles();
// активизируем открытие no DDE Execute
EnableShellOpenO;
RegisterShellFileTypes(TRUE);
// поиск в командной строке стандартных команд оболочки,
// DDE и имени открываемого файла
CCommandLinelnfo cmdlnfo;
iPa rseCommandLine (and Info);
// предотвращаем создание окна пустого документа при запуске
if <cmdlnfo. m_nShellCommand == CCommandLinelnfo: :FileNew) { cmdlnfo, m_nShellCommand = CCommandLinelnfo:: FlleNothing;
// выполнить действия, заданные в командной строке
if (IProce3sShellCommand(cmdInfo))
return FALSE;
// инициализация основного окна завершена. Выводим его на экран.
pMainFrame->ShowWlndow(m_nCmdShow):
ipMainFrame->UpdateWindow();
return TRUE;
ШШПШ1ШПШШН1ШПШ111ШШШШШ1П111ШШ1ШННПН1
//CAboutDlg (диалоговое окно About)
class CAboutDlg : public CDialog
public:
CAbourDlgO:
// данные диалогового окна
//{{AFX OATA(CAboutDlg)
enum { IDD = IDD A8OUTBOX };
//}}AFX_DATA'
// переопределенные виртуальные функции, сгенерированные ClassWizard
//{{AFX_VIRTUAL(CAbout01g>
protected:
virtual void |}DOataExchahge(CDataExchange« pDX); // Поддержка DDX/DDV //}}AFX.VIRTUAL''
// реализация, protectee:'
//{{AFX.MSG(CAboutDlg)
// нет обработчиков сообщений
//}}AFX MSG
DECLARE MESSAGE MAP()
CAboutDlg::CAboutDlg/> : CDialog(CAboutDlg::IDD) { //{{AFX DATA_INIT(CAboutDlg)
//} )AFXDATA_INIT
void CAboutDlg;:DoDateExchange(CDataExchange* pDX) {
CDlalog: -.DoDataExchange(pDX);
//{{AFX DATA MAP(CAboutDlg)
//}}AFX_DATA_MAP
BEGIN MESSAGE MAP(CAbout01g, CDlalog)
//{{AFX_MSG_MAP(CAboutDlg)
// Нет обработчиков сообщений
//}}AFX MSG_MAP
EHD.MESSAGE.MAPO
,// команда для отображения диалогового окна void СЕх18аАрр: :OnAppAb'out()
{
CAboutDlg aboutOlg:
aboutOlg. DoModalO;
Ш111НГП1ШНШ1Ш1Г1111Ш111Ш1111ПШ1ШШ111 ШИШИН
// обработчики сообщений CExlSaApp
CDocument* CExtSaApp: :OpenDocunientFlle(LPCTSTR IpszFileName)
TRACE(“CEx18aApp::OpenDocumentFile\n”);
return CIVinApp: :OpenDocumentFile(IpszFileName);
CMainFrame.
Этот класс основного окна-рамки (см. рис. 18-6) во многом сходен с SDI-версисй, за исключением того, что его базовым классом является CMDIFrameWnd^, не CFmmeWnd.
MA1NFRM.H
И HainFrm.h : интерфейс класса CMainFrame // ///////шин//////////// ///ntшпшпшшншитнишшн
Ulf !deflned(AFX_MAINFRM H_7B5FE26B_85E9_11DO_8FE3_00C04FC2A0C2_INCLUDED ) «define AFX_MAINFRM_H_7B5FE26B_85E9_11DO_8FE3_OOC04FC2AOC2_INCLUDED.
Ulf .MSC.VEA > 1000
«pragma once
«endif//.MSC.VER > 1000
class CMainFrame : public CNDlFrameWnd
{.■
DECLARE_DYNANIC(CMainFrame),
public: ' CMalnFrame();
// Атрибуты public: // Операции public:
// Переопределенные функции
// переопределенные функции, сгенерированные ClassWizard
//< (AFX_VlRTUAL(CMainFrame)
virtual BOOL PreCreateWlndow(CREATESTRUCT&cs);
//})AFX_VIRTUAL
// Реализация public:.
virtual *CMainFrame();
tfifidef .DEBUG
virtual void AssertValic() const;
virtual void Dump(CDumpContext8 de) const;
#endif
protected: // внедренные переменные-члены для панелей элементов управления
CStatusBar m.wndStatusBar;
CToolBar m.wndToolBar;
// сгенерированные функции таблицы сообщений protected:
//{(AFX„HSG(CMainFranie)
afx.msg int OnCreate(LPCREATESTRUCT IpCreateStruct);
// ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять и удалять функции-члены.
// НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода!
//}}AFXMSG
DECLARE. MESSAGE МАР()
};'
ШШШШШШННШ1ШШШНШШННННШНННШШНН
//({AFX.INSERT.LOCATION}}
П Microsoft Visual C++ добавит дополнительные объявления // непосредственно перед предыдущей строкой
#endif//! def ined(AFX_MAINFRM_H_7B5FE26B.85E9_11D0_8FE3_00C04FC2A0C2_IHCLUDED_)
MAINFRM.CPP
// MainFrm.срр: реализация класса CMainFrame //
«include "stdafx.h"
«include "cxlBa.h"
«include “MainFrm.h".
ttifdef „DEBUG
«define new DEBUG NEW
#undef THIS FILE ' static char THIS_FILE[] - _FILE_;
#endif-
////////////////////////////////////^^ // CMainFrame
IMPLEMENT OYNAMIC(CMainFrame, CMDIFrameWnd)
BEGIN MESSAGE MAP(CMainFrame, CMDIFrameWnd) //{(АЕХ_И80„МАР(СМа1лЕгаюе)
// ПРИМЕЧАНИЕ; здесь ClassWizard будет добавлять и удалять макросы таблицу сообщений. // НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода!
ON WM CREATEO /Д}AFX MSG MAP
END_MESSAGE„MAP()
static UINT indicators[] =
{
ID_SEPARATOR, // индикаторы в строке состояния ID~INDICATOR_CAPS, ID INDICATOR NUM, ID_INDICATOR SCRL,
///////////////////////////////^^ // создание/ уничтожение CMainFrame
CMainFrame::CMainFrame()
// ДОДЕЛАТЬ; предусмотреть здесь инициализацию переменных-членов }
CMainFrame::'CMainFrame() { }.
int CMainFrame; :0nCreate(LPCREATESTRUCTlpCreat6Struct)
if (CMDIFrameWnd: :OnCreate(lpCreateStruct) — -1) return -1;
if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILO | WS VISIBLE i CBRS TOP | CBRS GRIPPER | CBRS TOOLTIPS | CBRS FLYBY | CBRS_SIZE_DYNAMIC) || ! m wndToolBar. LoadToolBar(IDR_MAINFRAME))
TRACEO( "Ошибка при создании панели инструментов\п");
return -1;// ошибка_при создании окна
}
Рис. 18-6. (продолжение)
if (!m_wndStatus8ar.Create(this) ||
!m_wndStatusBar,Setlndicators(indicators, sizecf(indicators)/sizeof(UINT)))
TRACE0("Ошибка при создании строки состряния\п");
ireturn -1;// ошибка при создании окна
}
// ДОДЕЛАТЬ: Удалите следующие три строки, если не хотите, // чтобы панель инструментов была стыкуемой
m wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDockingt CBRS_ALIGN_ANY);
DockCc nt ro1 Ba r(&m_wndToolBar);
return 0;
IBOOL CMainFrame: :PreCreateWindow(CREATE8TRUC78. cs) {
if ( ICMDIFrameWnd::PreCreateWlndow(cs) ) return FALSE;
// ДОДЕЛАТЬ: измените класс и стили окна путем изменения CREATESTRUCT cs
return TRUE;
}
.ШШШ111ШШНШ1Ш11ШН1!1ШШ1Г!1Н1Ш!1Ш11Н111!Н11111Н11
И ддиагностика CMainFrame
flifdef_DEBUG
void CMa i n F rame:: Asse rtVai i d () const {
CMDIFrameWnd::AssertValid();
void CMainFrame::Dump(CDumpContext& de) const {
CMDIFrameWnd::Dump(dc);
ttendif //„DEBUG
iiiHiiHiijHiHiiniiHiriiiiiiiininniinimnniniiiiiiniiin // оОбработчики сообщений CMainFrame
CChfldFrame
Этот класс дочернего окна-рамки (см. рис. 18-7) позволяет легко управлять характеристиками этого окна, изменяя код функции PreCreateWindow. Кроме того, Вы можете создавать обработчики сообщений и переопределять другие виртуальные функции.
CHILDFRM.H
// ChildFrm.h : интерфейс класса CChildFrame
шшншшшшш1ш1иш1шшшшш1шшш11шнип
ttif !defined(AFX CHIL'DFRMH_7B5FE26D_85E9 11D0’8FE3 00C04FC2AOC2. INCLUDED.)
«define AFX_CHILDFRM_H_7B5FE26D_85E9_11D0_8FE3_0GC04FC2A0C2_INCLUDED_ '
«if _MSC_VER > 1000
«pragma once
«endif // .MSC.VER > 1000
class CChildFrame : public CMDIChildWid
{
DECLARE.DYNCREATE(CChildFrame)
public:
CChildFrameO;
// Атрибуты
public:.
// Операции public:
// Переопределенные функции
// переопределенные функции, сгенерированные Classwizard
//{{AFX.VIRTUAL(CChildFrame)-
virtual BOOL PreCreateWindow(CREATESTRUCT&. cs);
virtual void ActivateFrame(lnt nCmdShow - -1);
//}}AFX_VIRTUAL
// Реализация
public:
virtual *CChildFrame();
tfifdef.OEBUG',
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;
«endif
// Сгенерированные функции таблицы сообщений
protected:
//{{AFX.MSG(CChildFrame)
// ПРИМЕЧАНИЕ: здесь Classwizard будет добавлять и удалять функции-члены.
// НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода!
//}}AFXMSG
DECLARE.MESSAGE МАР()
};
////////////////////////////////^
//({AFX.INSERT.LOCATION}}
// Microsoft Visual C++ добавит дополнительные объявления
И непосредственно перед предыдущей строкой
«endif // ! defined(AFX„CHILDFRM_H_7B5FE26D.85E9.11D0_8FE3_00C04FC2A0C2_INCLUDED_)
см. след. стр.см. след. стр.Рис. 18-7. Листинг класса CChildFram
см. след. стр.
см. след. стр.
Рис. 18-7. (продолжение)
CHILDFRM.CPP
// ChlldFrm.срр ; реализация класса CChildFrame
//■
tfinclude "StdAfx.h".
«include "ex18a.h“
«include "ChildFrm.h"
flifdef.DEBUG
«define new DEBUG_NEW
ffundef THIS_FILE "
static char THIS_FILE[] = FILE;
tfendif
///////////////////////////////////////////////////////////////////////У/////
// CChildFrame
lMPLEMENT_DYNCREATE(CChildFrame, CMDIChildWnd)
BEGIN-MESSAGE MAP(CChildFrame, CMDIChildWnd);
//<<AFX_MSG_MAP(CChildFrame)
// ПРИМЕЧАНИЕ! здесь С_assWizard будет добавлять и удалять макросы таблицы сообщений, // НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода!
//}}AFX_MSG_MAP
END_MESSAGE_MAP().
/////////////////////////////////////////////////////////////////////////////
// Создание/уничтожение CChildFrame
CChildFrame::CChildFrame()
{
// ДОДЕЛАТЬ! предусмотреть здесь инициализацию переменных-членов }
CChildFrame::~CChildFrame()
BOOL CChildFrame::PreCreateWindow(CREATESTRUCT& cs) •I
// ДОДЕЛАТЬ: измените класс и стили окна путем изменения
// CREATESTRUCT cs
if { !CMDIChildWnd::PreCreateWindow(cs) ) return FALSE;
return TRUE;
///////////////////////////////////////////////////////////////////////////// И Диагностика CChildFrame
ttifdef _DE8UG,
void CChildFrame::AssertVelid() const
{•
CMDIChildWnd::AssertValid():
void CChildFrame::Durp(CDumpContext& de) const. '
(.
CMOlChlldWnd::Damp(dc);
У
ttendif //„DEBUG
iiniHUiHiiiiiniinniiniiiiiiiiiiniiiuirniiiriiiniiiniinii
// обработчики сообщений CChildFrame
void CChildFrame: :ActlvateFrame(int nCmdShow)
* ТИАСЕ("Входим в CChildFrame::ActivateFrame\n");■
CMDIChiIdWnd::Act1vateFrame(nCmdShow);
I
Тестирование приложения EX18A
Соберите программу, запустите ее из Visual C++ и создайте несколько документов. Попробуйте сохранить документы на диске, закрыть их и снова открыть. Проверьте также работу команды New Window из меню Window. Заметьте; теперь с одним и тем же документом связаны два окна представления и два дочерних окна-рамки. Завершите программу и запустите Windows Explorer. Рядом с именами созданных Вами файлов должен отображаться значок документа. Дважды щелкните мышью такой значок и посмотрите, запустится ли программа ЕХ18А. Теперь, запустив Explorer и ЕХ18А, перетащите мышью документ из Explorer в ЕХ18А. Открыла ли программа этот файл?
ГЛАВА 19
Печатьипредварительный просмотр
Если Бы полагаетесь только на Win32 API, то программирование печати станет для Вас одной из сложнейших задач. Не верите — пролистайте 60-страничную главу «Using the Printer" в книге Чарльза Петцольда Programming Windows 95 (Microsoft Press, 1996). В других книгах о Windows эта тема вообще игнорируется. К счастью, каркас приложений библиотеки MFC. 6.0 значительно упрощает программирование печати. В нем предусмотрена даже функция предварительного просмотра документов перед распечаткой, которая ведет себя так же, как и аналогичные функции в коммерческих Windows-программах, таких как Microsoft Word или Microsoft Excel.
В этой главе Вы узнаете, как использовать MFC-функции печати и предварительного просмотра. Попутно Вы получите представление о том, что происходит в Windows в процессе печати и чем этот процесс отличается от печати в MS-DOS. Сначала Вы познакомитесь с программированием печати в режиме WYSIWYG, при котором распечатка, получаемая на принтере, соответствует изображению на экране. Этот вариант требует аккуратно обращаться с режимами преобразования координат в Windows. Потом мы объясним, как напечатать многостраничный отчет, выглядящий на бумаге совершенно не так, как на экране. В этом примере Вы с помощью шаблона массива структурируете свой документ так, чтобы программа смогла печатать любой заданный диапазон страниц.
Печать в Windows
В прежние времена программистам приходилось заботиться о настройке своих приложений под множество принтеров. Теперь об этом заботится Windows, в которой есть драйверы чуть ли не для всех принтеров. Кроме того, она обеспечивает единый пользовательский интерфейс для задач, связанных с печатью. '
Стандартные диалоговые окна печати
Когда в Windows-приложении выбирают команду Print из меню File, на экране появляется стандартное диалоговое окно Print (см. рис. 19-1). А если из меню Hie выбирают команду Print Setup (настройка печати), открывается стандартное диалоговое окно Print Setup (см рис. 19-2). В процессе печати программа выводит стандартное диалоговое окно с информацией о состоянии принтера (рис. 19-3)'.
1 Данное окно появляется на экране на время передачи данных подсистеме печати, а ие иа весь период (обычно более длительный) собственно печати файла иа принтере. — Прим, перев.
Рис .19-1. Стандартное диалоговое окно Print
Рис. 19-2. Стандартное диалоговое окно Print Setup
Рис. 19-3. Стандартное окно, информирующее о состоянии принтера
Интерактивный выбор страницдля печати
Если Вы раньше занимались обработкой данных, то, наверное, привыкли к пакетному режиму печати. Программа считывает запись, форматирует ее и печатает выбранную информацию как строку в отчете. После распечатки, например, 50 строк, программа заставляет принтер вытолкнуть лист бумаги и начать печатать новую страницу. В таких случаях обычно предполагают, что отчет печатается целиком, и не предусматривают возможности выбора страниц в интерактивном режиме.
Но при печати в Windows разбиение па страницы играет большую роль (см. рис. 19-1). Программа должна реагировать на выбор страниц пользователем, вычисляя, какую именно информацию следует отпечатать, поэтому Вы должны соответственно структурировать данные в своем приложении.
Помните список студентов из главы 17? Что если в нем окажется 1000 студентов, и пользователю понадобится пятая страница отчета? Допустим, каждая запись о студенте укладывается в одну строку, а на одной странице умещается 50 строк. Тогда на пятой странице должны быть строки с 201 по 250. Если Вы пользуетесь MFC-классом списка, Вам придется, прежде чем приступить к печати, «пролистать» первые 200 элементов списка. Очевидно, что в данном случае список — далеко не идеальная структура. А что если вместо него использовать массив? Класс СОЬАгтыу (или один из классов-шаблонов массива) позволит обратиться прямо к 201-й записи о студенте. ■
Отнюдь не в каждом приложении переменные-члены жестко связаны с определенным числом печатных строк. Представьте, что в записи о студенте есть поле «биография» длиной в несколько печатных строк. Так как Вам неизвестно, сколько строк занимает биография конкретного студента, придется просматривать весь файл, чтобы определить границы страниц, Но если ваша программа запоминает эти границы по мере их вычисления, она станет работать "намного эффективнее. ■
Экранные и отпечатанные страницы
Во многих случаях предпочтительно, чтобы отпечатанная страница соответствовала изображению на экране. Из главы 5 Вы уже знаете: гарантировать, что объекты будут напечатаны именно так, как они выгладят на экране, нельзя. Однако, используя шрифты TrueType, можно добиться весьма близкого соответствия. Если Вы работаете с полноразмерными листами бумаги, значит, окно должно быть больше, чем сам экран. Поэтому для просмотра печатаемых изображений идеально подходит класс CScroltView.
Впрочем, иногда об экранных страницах можно не заботиться. Скажем, ваш класс «вид» хранитданные в каком-нибудь окне списка или их вообще не надо выводить на экран. В таких случаях в программу можно заложить логику «автономной» печати, при которой данные просто извлекаются из документа и отсылаются на принтер. Разумеется, программа должна корректно реагировать на то, что пользователь хочет напечатать не весь документ, а лишь какой-то диапазон страниц. Запросив у драйвера принтера размер бумаги и ориентацию листов (книжная или альбомная), Вы смажете корректировать разбиение документа на страницы.
Предварительныйпросмотрпередпечагью
Предваритепънъ1йп!юамотпрперед печатью (print preview), поддерживаемый MFC-библиотекой, позволяет увидеть на экране границы страниц и концы строк точно в тех местах, где они получатся при распечатке документа па выбранном принтере. Шрифт, особенно мелкий, может выглядеть несколько странно, но это не проблема.
Предварительный просмотр перед печатью поддерживается библиотекой MFC, а не Windows. Реализация поддержки потребовала от разработчиков библиотеки колоссальных усилий. Эта функция анализирует каждый символ, определяя его позицию на основе контекста
принтера. Подобрав более или менее подходящий шрифт, она выводит символ в окно предварительного просмотра,
Программирование вывода на печать
Каркас приложений берет на себя большую часть работы по поддержке печати и предварительного просмотра. Чтобы эффективно использовать принтер, надо знать последовательность вызовов функций и понимать, какие из них и когда переопределять.
Контекст принтера и функция CView::OnDraw ’
При печати на принтере ваша программа использует объект «контекст устройства* класса CDC. Не беспокойтесь о том, откуда возьмется этот объект — его создает каркас приложений, передавая затем как параметр в функцию OnDraw вашего класса «вид». Если программа просто копирует содержимое экрана на принтер, OnDraw может выполнять двойную работу. При выводе на экран OnPaint вызывает OnDraw, передавая ей контекст дисплея, а при печати OnPrint — другая виртуальная функция класса CView— вызывает OnDraw, передавая ей контекст принтера. Один вызов функции OnPrint позволяет отпечатать одну страницу целиком.
В режиме предварительного просмотра параметр функции OnDraw служит указателем на объект «контекст устройства* класса CPreviewDC. Функции OnPrint и OnDraw работают одинаково — независимо от того, печатаете Вы или просматриваете документ перед печатью.
Функция CView::OnPrint
Вы уже убедились, что функция OnPrint (базового класса) вызывает OnDraw и что та способна использовать контекст как дисплея, так и принтера. Перед вызовом OnPrint установите требуемый режим преобразования координат. Чтобы печатать элементы, которые не обязательно показывать на экране (например, титульную страницу, верхние и нижние колонтитулы), функцию OnPrint можно переопределить. Она получает следующие параметры:
I* указатель на контекстустройства;
I» указатель на структуру С Printinfo, в которой хранятся размеры страницы, номер текущей страницы и максимальный номер страницы.
Переопределяя функцию OnPrint, можно, к примеру, вообще не вызывать OnDraw, чтобы реализовать логику печати, полностью независимую от логики вывода на дисплей. Для каждой печатаемой страницы каркас приложений вызывает OnPrint по одному разу — с указанием ее номера в структуре CPrintlnfo.
Подготовка контекста устройства: функция CView::OnPrepareDC
Если Вам нужен режим преобразования координат на экране, отличный от ММ_ТЕХ1\а так юно обычно и бывает), лучше установить его в функции OnPrepareDC-класса «вид». Вы сами переопределяете эту функцию, если ваш класс «вид» является производным непосредственно от CView, но, если он производен от CScroHView, функция уже переопределена. OnPaint вызывает OnPrepareDC непосредственно перед вызовом OnDraw. Если же Вы печатаете на принтере, обращение к OnPrepareDCпроисходит перед тем, как каркас приложений вызывает OnPrint Таким образом, программа устанавливает режим преобразования координат и перед отрисовкой экранного изображения, и перед печатью страницы.
Второй параметр функции OnPrepareDC — указатель на структуру CPrintlnfo. Он действителен, только если OnPrepareDC вызывается до печати. Проверяет его достоверность функция-член CDCJsPrinting. Она особенно удобна, если с помощью OnPrepareDC Вы устанавливаете на экране и на принтере разные режимы преобразования координат.
Если Вы заранее не знаете, сколько страниц придется печатать, переопределите функцию OnPrepareDC так, чтобы она отыскивала конец документа и сбрасывала флажок т_ЬСоп-
tinuePrinthig-b структуре CPrinilnfo.Если этот флажок равен FALSE, функция ОпРгЬйнв вызывается, а управление передается в конец цикли печати..
Начало и конец печати
Когда начинается процесс печати, каркас приложений вызывает две функции класса СУ?в№ OnPreparePrintings. OnBeginPrinting. Первая вызывается перед открытием диалогового окна Print. (При выборе параметра Printing And Print Preview мастер AppWizard генерирует функции OnPreparePrinting, OnBeginPrinting и OnEndPrinting?) Если Вам известно минимальное и максимальное число страниц, вызовите из ОпРгерагеРппИп^ункщт. CPrinilnfo: :SetMinPage и CPrinilnfo: :SetMaxPage4itcna, которые Вы передаете этим функциям, появятся в диалоговом окне Print, чтобы пользователь мог изменить их.
Вторая функция, OnBeginPrinting, вызывается после того, как пользователь закрывает диалоговое окно Print. Переопределите ее, чтобы создать GDI-объекты, например, необходимые для печати шрифты. Программа будет работать быстрее, если создать шрифт заранее, а не повторять эту операцию для каждой страницы.
Функция CVieu^;:OnEndPrintingвызывaeтcя в конце печати — после того, как отпечатана последняя страница. Переопределите ее, чтобы избавиться от GDI-объектов, созданных в OnBeginPrinting.
В таблице приведены важнейшие для цикла печати функции класса CView, которые можно переопределять.
ФункцияЧто дает переопределение
OnPreparePrintingУстановка минимальногои максимального числа страниц
OnBeginPrintingСоздание GDI-объектов
OnPrepareDCijym. каждой страницы) Установка режима преобразования координат и определение момента конца печати
OnPrint (для каждой страницы) Выполнение выгода иа печать и вызов OnDratv
OnEndPrintingУдаление GDI-объектов
Пример ЕХ19А
Эта программа выводит па экран и печатает одну страницу текста, хранящегося в документе. Отпечатанная страница соответствует изображению на экране. И для принтера, и для дисплея используется режим преобразования координат MM_TWIPS. В исходном виде программа выводит текст в фиксированную область печати (fixed printable area rectangle), но потом мы модернизируем ее так, чтобы она подстраивалась под область печати, поддерживаемую драйвером принтера. Процесс создания программы ЕХ19А следующий.
Используя AppWizard, сгенерируйте проект \vcpp32\exl9a\exl9a. Примите параметры по умолчанию, но переименуйте классы «вид» и «документ», а также их файлы, как показано на рисунке на стр. 345.
Обратите внимание, что создается MDI-приложение.
Добавьте переменную-член типа CStringArr«j® класс CPoemDoc. Отредактируйте заголовочный файл PoemDoc.li или воспользуйтесь ClassView.
public:.
CStringArray m_stringArray;
Данные документа хранятся в массиве строк. MFC-класс CStringArray содержит массив объектов класса CString,доступных по индексу (нумерация, начиная с пуля). Максимальную размерность массива при объявлении указывать не надо, потому что он динамический.
Добавьте переменную-член типа CRect® класс <75<г/я§У/в1».Отредактируйте заголовочный файл StringView.hHnH воспользуйтесь ClassView:
private:
CRect m_rectPrlnt;
Отредактируйте три функции-члена класса СРоетВосв файле PoemDoc.cpp. Заготовки функций OnNewDocument'a Serialize сгенерированы AppWizard, но нам придется воспользоваться ClassWizard, чтобы переопределить еще и функцию DeleteContents. Инициализацию документа~»поэмы* мы предусмотрим в переопределенной функции OnNew- Document. DeleteContents вызывается из CDocument::OnNewDocument, так что благодаря вызову сначала функции базового класса поэма не пропадет. (Между прочим, это фрагмент двадцатой поэмы из книги Лоренса Ферлингетти (Lawrence Ferlinghetti) A Coney bland of the Mind.) Если Вам не нравится текст — возьмите другое произведение или описание любимой №т32-функции. Добавьте код, выделенный полужирным:
BOOL CPoemDoc: :OnNewDocument()
if (! CDocument:: OnNewDocument ())
return FALSE;
m_strlngArray.SetSize(1O);
m_strin gArray [0] = "The pennycandystore beyond the B”;
m_stringArray[1] = "is where I first”;
m_stringArray[2] = "fell in love";
rn_stringArray[3] = *with unreality";
m_stringArray[4] = "Jellybeans glowed in the semi-gloom";
m_stringArray[5] = "of that September afternoon";
m_stringArray[6] = "A cat upon the counter moved among";
m_stringArray[7] = "the licorice sticks";
■_strlngArray[8] = "and tootsie rolls";
m_stringArray[9] = " and Oh Boy Gum";
returnTRUE;
ПРИМЕЧАНИЕ Класс CStringArray поддерживает динамические массивы, но здесь мы используем объект m_stringArrayvaK, как будто эго статический массив с 10 элементами.
При закрытии документа каркас приложений вызывает виртуальную функцию DeleteContents класса документа; при этом строки в массиве удаляются. CStringArray содержи]’ собственно объекты, а CObArray — указатели на них. Важность этого различия станет ясна, котда придет пора удалять элементы массива. Функция RemoveAll уничтожает строковые объекты так:
void CPoemDoc:.DeleteContents(}
// вызывается перед вызовом OnNewDocument и при закрытии документа m_stringArray.HemoveAlH);
}
Сериализация в этом примере не существенна, но приведенная ниже функция иллюстрирует, насколько проста эта операция над строками. Каркас приложений вызывает DeleteContents перед загрузкой из архива, так что Вам нет нужды беспокоиться об очистке массива. Введите код, выделенный полужирным;.
void CPoemDoc:;Serlalize(CArchive& ar).
m_stringArray.Seriallze(ar);
}
Отредактируйте функцию OnlnitialVpdatea StringView.cpp. Эту функцию следует переопределить для всех классов, производных от CScrollView. Она устанавливает логический размер окна и режим преобразования координат, Добавьте код, выделенный полужирным:■
void CStringView::OnInitialUpdate()
{
CScrollView::QnInitialUpdate();..
CSize sizeTotal(n_rectPrint.Width(), - rectPrint.HeightO);
CSize sizBpage(sizeTotal.cx / 2, sizeTotal.cy / 2);// прокрутка страницы
CSize sizeLine(sizeTotal.cx / 100, sizeTotal.cy / 100); // прокрутка строки SetScrollSizeaCHM.miPS, sizeTotal, sizePage, sizeLine);
}
Отредактируйте функцию OnDrawB StringView.cpp. Функция OnDrawкласса CStringView выводит изображение и на экран, и на принтер. Она не только показывает строки поэмы шрифтом Times New Roman (размером 10 пунктов), по и очерчивает область печати и формирует какое-то подобие линеек по верхнему и левому полям. Функция предполагает применение режима MM_TWIPSpp^ котором 1 дюйм равен 1440 единицам. Добавьте код, выделенный полужирным:
void CStringView::OnDraw(CDC* pDC)
inti, j, nHeight;
CString str;.
CFont font;'
TEXTMETRIC tm;
CPoemDoc* pDoc = GetDocumentO;
// рисуем ранку - чуть меньше, чтобы избежать отсечений
pDC->Rectangle(m_rectPrint + CRect(0, о, -20, 20));
И рисуем вертикальную и горизонтальную линейки
j = m_rectPrint.Width() / 1440;
for (Г = 0; i <= j: 1++) {
}}str. Format('H02d", i);•
}
}
pDC->TextOut(i * 1440, 0, str);'
3 = -(m_rectPrint.Height() / 1440);.
for (i = О; i <= j; i++) {
str.FormatC4t02d", i);
pDC->TextOut(0, -i * 1440, str);
}'
// напечатать текст на полдюйма ниже и правее;
// использовать шрифт Times New Roman, 10 пунктов
font.CreateFont(-200, 0, 0, 0, 400, FALSE, FALSE. 0. ANSI CHARSET,-
OUT DEFAULT PRECIS, CUP_DEFAULT_PRECIS,
' OEFAULT_OUALITY, DEFAULT PITCH | FF.ROMAN,
"Times New Roman");
CFont* pOldFont = (CFant*) pDC->SelectObject(&font);
pDC->GetTextMetrios(&tm);
nHeight = tm.tuHelght + tm.tmExternalLeading;
TRACE("fcnt height = %d, internal leading = 8d\n", nHeight, tm.tmlnternalLeading); .
j = pDoc->m_etringArray.GetSizeO;
for (i = O; i < j; i++) {
pDC->Text0ut(720, -i * nHeight - 720, pDoc->e_stringArray[i]);
pDC->SelectObject(p01dFont);
TRACE("LOGPIXELSX = «d, LOGPIXELSY = %d\n", pDC->GetOevioeCaps(LOGPIXELSX), pDC->GetDeviceCaps(LOGPIXELSY));
TRACE("HORZSIZE = %d, VERTSIZE = *d\n”, pDC->GetOeviceCaps(HORZSIZE), pDC->GetDeviceCaps(VERTSIZE));
Отредактируйте функцию OnPreparePrlntingB StringView.cpp. Она устанавливает максимальное число печатаемых страниц. В нашем примере печатается только одна страница. В переопределенной функции On Prepare Printing надо обязательно вызвать функцию ПоРгерагеРгтИп§базового класса. Введите код, выделенный полужирным:
BOOL CStringView::OnPreparePrinting(CPrintInfo» plnfo)
pInfo->SetMaxPage(1);
return DoPreparePrinting(plnfo);
Отредактируйте конструктор в StrlngVlew.cpp. Начальная область печати должна составлять 8x15 дюймов и быть выражена в твипах (1 дюйм = 1440 твипов). Добавьте код, выделенный полужирным:
CStringView: :CStrlngView() : ■_rectPrint(0, 0, 11620, -15120)
<
}
Соберите и протестируйте приложение. Если запустить приложение ЕХ19А под Windows NT с низким экранным разрешением, дочернее МПЬокно должно выглядеть как на первом рисунке на стр. 348. (Текст будет выглядеть крупнее при более высоком разрешении или под Windows 95 и Windows 98).
Текст кажется мелким, правда? Пойдем дальше: выберем из меню File пункт Print Preview и увеличим изображение, дважды щелкнув «лупу». Результат будет следующим. ■
Помните «логические твипы» из главы 5? Сейчас мы попробуем воспользоваться ими, чтобы увеличить изображение на дисплее, не меняя его размер при печати. Это требует дополнительных усилий, поскольку класс CScrolIViewHe рассчитан на нестандартные режимы преобразования координат. Придется заменить базовый класс представления — вместо CScroUView взять CLogScrolIView, который один из авторов создал, изменив исходный MFC-код в ViewScrLcpp. Файлы LogScrollView.hH LogScrollView.cpp находятся в каталоге \vcpp32\exl9a на прилагаемом к книге компакт-диске.
Вставьте класс ClogScrollVteuB проект. Скопируйте файлы LogScrollView.h и LogScrol- IView.cpp с компакт-диска (если не сделали этого раньше). Из меню Project выберите Add То Project, а затем из подменю — пункт File. В списке выберите только что скопированные файлы и щелкните ОК, чтобы внести их в проект.
Отредактируйте заголовочный файл StringView.h. Добавьте в начало файла строку: «include ‘LogScrollView.h'’
Затем замените строку:V
class CStringView : public CScrollView на
class CStringView : public CLogScrollView
^.Отредактируйте файл StringView.cpp. Повсеместно измените все вхождения CScrollView на CLogScrollView. Затем отредактируйте функцию OnlnitialUpdate. Мы приведем отредактированный код, что намного короче:
void CStringView;:OnInitialUpdate(> t
CLogScrollView::OnInitialUpdate();
CSize slzeTotal(m_rectPrint.Width(), -m_rectPrint.Height());
SetLogScrollSizes(sizeTotal);
Снова соберите и протестируйте приложение. Теперь на экране должно быть при
мерно следующее.
Определение области печати
Программа ЕХ19А осуществляет вывод в фиксированную область, соответствующую настройке лазерного принтера на печать в книжной ориентации на листах размером 8.5х 11 дюймов (формат Letter). Но как быть, если Вы загрузили бумагу формата А4 или выбрали альбомную ориентацию? Программа должна уметь подстраиваться под новые параметры страницы..
• Определить область печати принтера сравнительно несложно. Помните указатель на структуру Фг^иЙпД>,передаваемый в OnPrint? В этой структуре есть поле m_rectDraw, в котором хранятся логические координаты области печати. Ее и использует функция OnDraw. Правда, определить эту область, пока не начнется печать, нельзя, поэтому надо установить для OnDraw какую-то область по умолчанию, чтобы использовать ее до начала печати.
Если Вы хотите, чтобы программа ЕХ19А определяла область печати принтера, переопределите OnPrint с помощью ClassWizard, а затем напишите ее код:
void CStringView::OnPrint(CDC* pDC, CPrintlnfo* plnfo)
{
m_rectPrint = pInfo->n_rectDraw;
SetLogScrollSizee(CSiza(iLreotPrint.Width()l -m_rectPrint.Height()));
CLogScrollView::OnPrint(pDC, plnfo);
Еще раз о классах-шаблонах наборов: класс CArray
В программе ЕХ16В (см. главу 16) Вы видели CTypedPtrList — класс-шаблон набора из MFC- библиотеки, в котором мы хранили список указателей на объекты CStudent. Другой такой класс. CArray, пригодится для примера ЕХ19В. Он отличается от CTypedPtrList по двум позициям. Во-первых, как и CStringArrayB ЕХ19А, это массив с элементами, доступными по индексу. Во-вторых, он хранит не указатели на объекты, а сами объекты. В программе ЕХ19В элементы массива — это объекты класса CRect. Класс элемента не должен быть производным от CObject, а для CRect это как раз так.
Как и в ЕХ16В, оператор typedepnpomaem работу с шаблонам. В данном случае мы напишем: typedef CArray<CRect, CRect4> CRectArray;
и определим, тем самым, класс массива, содержащий объекты CRect, ссылки на которые будут принимать функции этого .класса. (Передавать 32-разрядные указатели эффективнее, чем копировать 128-раз рядные объекты.) Чтобы использовать шаблон массива, надо объявить экземпляр класса CRectArray,» затем вызвать функции-члены класса CArray,например SetSize. Кроме того, для доступа к элементам этого массива можно применить оператор [] класса CArray.
Классы-шаблоны CArray, CList^r СМарттросты в обращении, если прост класс элементов. Класс CRect удовлетворяет этому условию, потому что не содержит указателей. Для сериализации всех элементов в наборе каждый класс-шаблон применяет глобальную функцию Seria- lizeElements. Функция SerializeElements, предлагаемая по умолчанию, осуществляет побитовое копирование объекта в архив и из архива.
Если ваш класс элементов содержит указатели или что-то еще, усложняющее его, Вам придется написать свою функцию SerializeElements. Например, для массива прямоугольников эта функция выглядела бы так (на самом деле это не нужно):
void AFXAPI SerializeElements(CArchive& ar, CRect* pNewRects, int nCount)
for (Int i = 0; i < nCount: i++, pNewRects++) {
if (ar.IsStoringO) {
ar « ^pNewRects;
else {
ar » •pNewRects:
}
}
Обнаружив эту функцию, компилятор использует ее вместо SerializeElements, определенной в шаблоне, но только, если прототип функции SerializeElements указан до объявления класса шаблона.
ПРИМЕЧАНИЕ Классы-шаблоны зависят и от двух других глобальных функций: ConstructEiemente и DestruotElements. Начиная с Visual C++ 4.0, эти функции вызывают для каждого объекта конструктор и деструктор класса элементов, так что не надо заменять их.
Пример ЕХ19В
В этом примере документ содержит массив из 50 объектов CRect, описывающих окружности, которые размещаются в прямоугольной области 6x6 дюймов случайным образом и имеют произвольные диаметры (не менее 0,5 дюйма) — получается что-то, напоминающее мыльные пузыри. Б то же время программа выводит на принтер не сами окружности, а координаты соответствующих им объектов CRect в числовой ферме — по 12 на каждой странице с колонтитулами. Далее приведены этапы создания программы ЕХ19В.
HcnonbsyflAppWizard, создайте npoeicr\vcpp32\exl9b\exl9b. Примите параметры по умолчанию, но выберите Single Document. Параметры и имена классов по умолчанию:
Отредактируйте заголовочный файл StdAfx.h:Добавьте объявление классов-шаблонов наборов MFC. Для этого вставьте строку:
tfinclude <afxtempl.h>
Отредактируйте заголовочный файл ex!9bDoc.h. В примере ЕХ19Адаиные документа состояли из строк, хранившихся в наборе CStozzg/lmzy. По скольку мы используем шаблон набора для прямоугольников, ограничивающих эллипсы, нам нужен оператор lypedef (вне объявления класса):
tvpedef CArray<CRect, CRectl> CRectArray;
Теперь объявите в ex!9bDoc.h открытые переменные-члены:
public;
enum { nLlnesPerPage = 12 };
enum { nHaxElllpses = 50 };
CRecrArray n_elllpaeArray;
Операторы перечисления — «объектно-ориентированные» заменители операторов ^define.
Отредактируйте файл реализации ex!9bDoc.cpp. Переопределенная функция OnNew- Document инициализирует массив эллипсов произвольными значениями, а функция Serialise считывает и записывает весь массив. Заготовку этих функций генерирует AppWizatd. Функция DeleteContentsHe нужна, потому что оператор /7 класса <24гт-оу записывает новый объект CRect вместо существующего. Добавьте код, выделенный полужирным:
13 Зак №7Я7
bool CEx19bDoc::OnNewDocument() {
■ if (J CDocument:: OnNewDocument ()) return FALSE;. ;
int nl, n2, n3; // создать 50 произвольных окружностей srand((unsigned) time(NULL)); ' ei_ellipseArray. SetSize(nNaxEllipses):
for (int i = 0; i < nMaxEllipses; i++) { ,nl = rand() * 600 / RANDJMAX;.
n2 = rand() * 600 / RAND_MAX;
n3 = rand() * 50 / RAND_MAX;
fli elllpseArray[l] = CRect(n1, -n2, nl + n3, -(n2 + n3)); }
return TRUE; }'
void Cex19bDoc: :Serialize(CArchlve& ar)'
m_ellipseArray.Serlalize(ar); ■ }
Модифицируйте заголовочный файл ex!9bVlew,h. Используя ClassView, добавьте переменную-член и два прототипа функций, приведенные ниже. ClassView одновременно сгенерирует в exl9bVjew.cpp шаблоны этих функций, public: int m.nPage;.
private: void PrintPageHeader(CDC* pDC); .'
void PrintPageFooter(CDC* pDC);
Переменная-член m_nPage хранит номер текущей страницы документа. Закрытые функции предназначены для подпрограмм, формирующих верхние и нижние колонтитулы.
Отредактируйте функцию OnDraw в ex!9bView.cpp. Переопределенная функция OnDraw просто рисует пузырьки в окне представления. Добавьте код, выделенный полужирным: void CEx19bView;: OnOraw(CDC* pDC).
int i, j;
CEx19bDoc* pDoc = GetDocument();
j ^ pDoc->m_ellipaeArray.GetUpperBound();
for (i = O; i < j; i++) {
pDC->Elllp8e(pDoc->m_ellip8eArray[i]); )'
I
Вставьте функцию OnPrepareDCB exl9bVlew.cpp. Класс «вид» не предусматривает прокрутки в окне представления, поэтому режим преобразования координат надо установить именно в этой функции. Используя ClassView, переопределите функцию ОпРге- рагвОС, а затем введите код, выделенный полужирным: void CEx19bView::OnPrepareDC(CDC* pDC, CPrintInfo* plnfo)
pDC->SetMapMode(MM_LOENGLISH);
Вставьте функцию OnPrintB ex!9bView.cpp. Исходная функция CVietv::0nPrintobi3ii- вает OnDvaw. В этой программе мы собираемся печатать информацию, отличную оттай, что изображается на экране, поэтому в OnPrint надо обойтись без вызова OnDraw. Функция On Print сначала устанавливает режим MMJTWIPS дотам создает шрифт фиксированной ширины. После распечатки числовых значений 12 элементов т_еШр5еА7таушрифг отключается. Можно было бы создать шрифт лишь один раз в OnBeginPrinting, но в данном случае это не дало бы заметного выигрыша. Переопределите с помощью CiassWizard функцию OnPrint, а затем добавьте в нзе код, выделенный полужирным:
void CEx19bView::OnPrint(CDC* pDC, CPrlntlnfo* plnfo)
int CString CPoint CFontint CString CPoint CFontpoint(720, -1440); font;point(720, -1440); font;nStart, nEnd, nHeight; str;
int CString CPoint CFont
int CString CPoint CFont
point(720, -1440); font;
point(720, -1440); font;
TEXTMETRIC tm;
pDC->SetMapMode(MM_TWIPS);
CExlSbDoc* pDoc = GetDocument();
m_nPage = pInfo->m_nCurPage; // для функции PrintPageFooter
nStart = (m_nPage - 1) * CExlSbDoc: :nLinesPerPage;
nEnd = nStart + CExl9bDoc: :nLinesPerPage;
// фиксированный шрифт размером 14 пт
font.CreateFont(-280, 0, 0, 0, 400, FALSE, FALSE, О, ANSI CHARSET, OUT DEFAULT PRECIS, CLIP_DEFAULT_PHECIS, DEFAULT-QUALITY, DEFAULT-PITCH ) FF MODERN, "Courier New”); // Courier New - это TrueType-шрифт
CFont* pOldFont = (CFont*) (pDC->Select0bject(8font));
PrintPageHeader(pDC);
pDC->GetTextMetrlcs(&tm);
nHeight = tm.tmHeight + tm.tmExternalLeading;
for (i = nStart; i < nEnd; i++) {
if (i > pDoc->m_ellipseArray.GetUpperBound()>
• break;
str. Format( %6d %6d %6d Md Md”, i + 1, pDoc->m_ellipseArray[i].left,•
pDoc->m_ellipseArray[i].top,■
pDoc->m_ellipseArray[i],right, р0ос->т„е111рзеАггау[1].bottom);
point.у -= nHeight;
pDC->TextOut(polnt.x, point.у, str);
l£ rintPageFooter( pDC);
pDC->Select0bject(p01dFont);
Отредактируйте функцию OnPreparePHntin^ exl9bVlew.cpp. Эта функция, шаблон которой генерирует AppWizard, вычисляет количество страниц в документе и — посредством функции SetMaxPage— сообщает результат каркасу приложений. Добавьте код, выделенный ПОЛУЖИРНЫМ:
void CExlSbView::OnPreparePrintlng(CPrintInfo» plnfo)
CExlSbDoc* pDoc = GetDocument();
pInfo->SetMaxPage(pDoc->m_ellipseArray.GetUpperBound() / CExlSbDoc::nLinesPerPaqe + 1); return DoPreparePrinting(plnfo);
Вставьте в exl9bView.cpp функции, формирующие колонтитулы. Эти закрытые функции, вызываемые из OnPriM, печатают верхние и нижние колонтитулы. Нижний колонтитул включает номер страницы, сохраненный функцией OnPrint^ переменной-члене m_nPage класса «вид». Функция CDC:.GetTextExten№mic3raeT ширину строки с номером страницы, чтобы ее можно было выровнять по правому полю. Добавьте код, выделенный полужирным:
void CEx19bView::PrintPageHeader(CDC* pDC)
{
CString str;
CPoint point(0, 0);
pDC->TextDut(point.x, point.у, "Отчет о пузырьках" );
point += CSize(720, -720);
str.Format("«6.6s «6.6s «6.6s «6.6s «6.6s",
"Номер", "Слева", "Сверху", "Справа", "Снизу");
pDC->TextOut(point.x. point y, str );
void CEx!9bView: :PrintPaaeFooter(CDC‘ pDC) {
CString str;
'CPoint point (0, -14400); // смещает на 10 дюймов вниз
CEx19bDoc* pDoc = GetDocument();
str.Format("Документ «з", (LPCSTR) pDoc->GetTitle());
pDC->TextDut(point,x, point.y, str);
str. FormatCСтраница «d", m_nPage);
CSize size = pDC->GetTextExtent(str);
point += 11520 - size.ox;
pDC->TextOut(point.x, point.y, str ); // выравнивание по правому полю }
Соберите и протестируйте приложение. Запустив программу, Вы увидите что-то похожее на рисунок.
При каждом выборе команды New из меню File изображение на экране должно меняться, а в режиме Print Preview первая страница должна выглядеть как на рисунке. В диалоговом окне Print можно задать диапазон печатаемых страниц.
ГЛАВА 2 0
Разделяемыеокна
и множественное представление данных
Во всех программах, с которыми Вы имели дело до сих пор, за исключением ЕХ18А, было только одно связанное с документом окно представления. Если Вы пользовались каким-нибудь текстовым процессором для Windows, то знаете, насколько удобно, когда один и тот же документ открыт сразу в двух окнах. Оба окна могут показывать документ в обычном виде, а могут, скажем, и так: одно окно находится в режиме «разметкастраницы» (page layout), а другое — в режиме «структура* (outline view).
Каркас приложений позволяет реализовать множественное представление документа несколькими способами, в том числеразделением окна на секции (splitter window) и созданием нескольких дочерних MDI-окон (multiple MDI child windows). В этой главе мы рассмотрим оба варианта, и Вы увидите, что сформировать множество объектов одного класса «вид* (обычное представление документа) не так уж трудно. Немного сложнее использовать в одном и том же приложении два или более класса «вид» (например, вид «структура» и вид «разметка страницы»).
В этой главе мы сделаем упор на создании нескольких представлений. В примерах предполагается, что данные документа инициализируются в функции OnNewDocument. Советуем обратиться к главе 16, чтобы освежить знания о взаимодействии «документ-вид».
Разделяемое окно
:?ю окно — особая разновидность окна-рамки; каждая его секция (рапе) содержит свое представление документа. Разбить окно на секции может сама программа при его создании или пользователь, выбрав соответствующую команду меню либо переместив маркер разбиения на полосе прокрутки. После того как окно разбито на секции, маркер разбиения позволяет корректировать размеры секций (для этого его перемещают мышью). Разделяемые окна можно применять как в SDI-, таки в MDI-приложениях. Примеры таких окон Вы уведите на рисунках, иллюстрирующих программы ЕХ20Аи ЕХ20В.
Разделяемое окно представляет собой объект класса CSplitterWndH. полностью занимает клиентскую область окна-рамки (класса CFrameWnc&uva CMDICbildWntF)^ в его секциях располагаются окна представления. Разделяемое окно не участвует в распределении команд. Активное окно представления (в секции разделяемого окна) логически связано непосредственно с окном-рамкой.
Варианты создания множестве иных представлений
Комбинируй различные способы реализации множественного представления с моделями приложений. мы получим множество вариантов. Вог некоторые из них.■
SDI-приложение с разделяемым окном, один класс «вид». Этот вариант реализован в программе-примере ЕХ20А. Каждая секция независимо от других позволяет прокрутить содержимое документа до любого места. Программист задает максимальное число горизонтальных и вертикальных секций, а пользователь при работе с приложением сам разделяет окно.
SDI-приложение с разделяемым окном, несколько классов «вид». Этот вариант показан в программе-примере ЕХ20В. Программист задает число секций и последовательность (формирования объектов «вид», а пользователь при работе с приложением может изменять размеры секций.'
|* SDI-приложение без разделяемых окон, несколько классов «вид*. Этот вариант воплощен в примере ЕХ20С. Пользователь переключает классы «вид», выбирая соответствующие команды меню.
!• МЫ-приложение без разделяемых окон, один класс «вид». Это стандартное MDI- приложение, которое Вы видели в главе 18. Команда New Window позволяла создавать новое дочернее окно для уже открытого документа.
'• MDI-приложение без разделяемых окон, несколько классов «вид». Это разновидность стандартного МОЕприложспия, позволяющая работать с множественным представлением документа. Как показано в примере EX20D, все, что для этого нужно, — заменить команду N ew Window командами, соответствующими каждому из доступных в программе классов «вид».
MDI-приложение с разделяемыми дочерними окнами. Этот вариант подробно рассмотрен в электронной документации на примере программы SCRIBBLE.
Динамически и статически разделяемые окна
Динамически разделяемое окно (dynamic splitter window) можно разделять в любой момент, выбирая соответствующую команду или перемещая маркер разбиения на полосе прокрутки. Секции в динамически разделяемом окне обычно связаны с одним классом «вид». Верхняя левая секция показывает выбранное представление данных сразу после создания разделяемого окна. Полосы прокрутки — общие для всех секций. Например, в окне, разбитом по горизонтали на две секции, нижняя полоса прокрутки управляет обоими окнами представления. При запуске приложения с динамически разделяемым окном формируется единственный объект «вид». Когда пользователь разделяет окно-рамку, создаются другие объекты «вид», а когда «воссоединяет» его, эти объекты уничтожаются.
Секции статически разделяемого окна определяются при первом создании окна; можно изменять их размеры, но не число. Статически разделяемые окна подходят для нескольких классов «вид»; при этом конфигурация окон устанавливается при их создании. В статически разделяемом окне у каждой секции свои полосы прокрутки. В приложении со статически разделяемым окном все объекты «вид» конструируются при создании окна-рамки и удаляются при его уничтожении.'
Пример ЕХ20А
В этом примере окно можно динамически разделить на четыре секции, что приводит к формированию четырех объектов «вид», — и всеми управляет один класс «вид». Код классбв «документа «вид» мы позаимствуем из примера ЕХ19А Добавить динамически разделяемое окно к новому приложению можно, используя AppWizard. На шаге 4 щелкните кнопку Advanced,
выберите вкладку Window Styles, а в ней пометьте флажок Use Split Window (использовать разделяемое окно), как показано на рисунке ниже. При этом AppWizard добавит код к вашему классу CMainFrame. Разумеется, к существующему приложению такой код придется добавить вручную.
Ресурсы для разделения окна
Генерируя приложение с разделяемым окном-рамкой, AppWizardдополняет меню View проекта командой Split. Идентификатор команды ZD_W%VDOW_SP£77coothochtch с функцией- обработчиком класса Фх№из библиотеки MFC
CMainFrame •.
Классу основного окна-рамки приложения нужна переменная-член, связанная с разделяемым окном, и прототип переопределенной функции OnCreateClient. Следующий код AppWizard добавит к файлу MainFrm.h:
protected:
GSplitterWnd m„wndSplitter;
publics
virtual BOOL OnCreateClient(LPGREATESTRUCT Ipcs, CCreateContext* pContext);
При создании объекта рамки каркас приложений вызывает виртуальную функцию-член CFrameWnd-OnCreateClient.Базовый класс формирует единственное окно представления, определенное в шаблоне документа. Функция OnCreateClient, переопределенная AppWizard в MainFrm,cpp и показанная ниже, создает вместо этого разделяемое окно, а оно, в свою очередь, формирует первое окно представления:
BOOL CMainFrame::OnCreateCliRnt(LPCREATESTRUCT /*lpcs*/, CCreateContext* pContext)
I*
return m_wndSplitter.Create( this,
2, 2,// ДОДЕЛАТЬ: установить нужное число строк и столбцов
CSize (10, 10), // ДОДЕЛАТЬ: установить минимальный размер pContext );
Функция-член CSplitterWnd::CreatecosaaeT динамически разделяемое окно. Класс «вид» известен объекту CSplitterWnd, поскольку имя этого класса внедрено в структуру COeate- Context, передаваемую в Create как параметр.
Второй и третий параметры функции Create указывают, что окно можно разбить максимум на две строки и на два столбца (т е. на 4 секции). Заменив (2, 2) на (2, I), Вы получите
две горизонтальные секции, а параметры (7,2) дадут две вертикальные секции. Параметр CSize определяет минимальный размер секции.
Тестирование приложения ЕХ20А
После запуска программы можно разделить окно командой Split меню View или маркерами разбиения на полосах прокрутки. На рисунке 20-1 показано окно представления, разделенное на 4 секции.
1 8$№МЙЙИ11 8$№МЙЙИ13030о:о:0101о?о?озоз8181i 1 02i 1 02И 8И 8ииThe pennycftndyrtwbayoftd the ELи Where I firstfell injevawith unrealityJettybeaM glowed in the semi-gloeHL cflhtt September afternoonA cat upon the counter moved among the liconcb eticksThe pennycftndyrtwbayoftd the ELи Where I firstfell injevawith unrealityJettybeaM glowed in the semi-gloeHL cflhtt September afternoonA cat upon the counter moved among the liconcb eticks0101Theperaiyc andyrteffbeyondthe Вt» where I fkAfen in lovewith unrealityJellybeans glowed nx the aetra-gloofn. ofthet September afteraoan A CM upon the oounUtRtovtd among Ihrlicorice sticksTheperaiyc andyrteffbeyondthe Вt» where I fkAfen in lovewith unrealityJellybeans glowed nx the aetra-gloofn. ofthet September afteraoan A CM upon the oounUtRtovtd among Ihrlicorice sticksЖЖIIЯЯiffiffаГаГ03 0003 000101**02**02о;о;•The pennyetndyrtore beyond the Z1:if where I first;fell in loveIlfwith unrealityJeSytreens glowed in the seatt-giocm;cftiwl September afternoon!A cat upon the sountermovedamong'thelicorice sticks•The pennyetndyrtore beyond the Z1:if where I first;fell in loveIlfwith unrealityJeSytreens glowed in the seatt-giocm;cftiwl September afternoon!A cat upon the sountermovedamong'thelicorice sticksииThe решу candy Abrebejondthe В &wheie 1 first fellin love with unreality Jellybean* glowed hi the r«mi-glaom of that eeptember afternoon Aeatuponthe «ounUrmcvedytong the licOMi ehckiThe решу candy Abrebejondthe В &wheie 1 first fellin love with unreality Jellybean* glowed hi the r«mi-glaom of that eeptember afternoon Aeatuponthe «ounUrmcvedytong the licOMi ehckiЯЯЖ2*Ж2*
1 8$№МЙЙИ1
1 8$№МЙЙИ1
30
30
о:
о:
01
01
о?
о?
оз
оз
81
81
i 1 02
i 1 02
И 8
И 8
и
и
The pennycftndyrtwbayoftd the ELи Where I firstfell injevawith unreality
JettybeaM glowed in the semi-gloeHL cflhtt September afternoon
A cat upon the counter moved among the liconcb eticks
The pennycftndyrtwbayoftd the ELи Where I firstfell injevawith unreality
JettybeaM glowed in the semi-gloeHL cflhtt September afternoon
A cat upon the counter moved among the liconcb eticks
01
01
Theperaiyc andyrteffbeyondthe Вt» where I fkAfen in lovewith unreality
Jellybeans glowed nx the aetra-gloofn. ofthet September afteraoan A CM upon the oounUtRtovtd among Ihrlicorice sticks
Theperaiyc andyrteffbeyondthe Вt» where I fkAfen in lovewith unreality
Jellybeans glowed nx the aetra-gloofn. ofthet September afteraoan A CM upon the oounUtRtovtd among Ihrlicorice sticks
Ж
Ж
I
I
Я
Я
iff
iff
аГ
аГ
03 00
03 00
01
01
**02
**02
о;
о;
•The pennyetndyrtore beyond the Z1
:if where I first
;fell in love
Ilfwith unreality
JeSytreens glowed in the seatt-giocm
;cftiwl September afternoon
!A cat upon the sountermovedamong
'thelicorice sticks
•The pennyetndyrtore beyond the Z1
:if where I first
;fell in love
Ilfwith unreality
JeSytreens glowed in the seatt-giocm
;cftiwl September afternoon
!A cat upon the sountermovedamong
'thelicorice sticks
и
и
The решу candy Abrebejondthe В &wheie 1 first fellin love with unreality Jellybean* glowed hi the r«mi-glaom of that eeptember afternoon Aeatuponthe «ounUrmcvedytong the licOMi ehcki
The решу candy Abrebejondthe В &wheie 1 first fellin love with unreality Jellybean* glowed hi the r«mi-glaom of that eeptember afternoon Aeatuponthe «ounUrmcvedytong the licOMi ehcki
Я
Я
Ж2*
Ж2*
РИс. 20-1. Единственное окно представления, разделенное на 4 секции
Пример ЕХ20В
ЕХ20В — расширенный вариант программы ЕХ20А; в ней определен второй класс «вид», благодаря чему статически разделяемое окно может показывать два представления одного документа, (СРР- и Н-файлы взяты от первоначального класса «вид».) На этот раз разделяемое окно работает чуть-чуть иначе. Теперь при запуске формируются две секции, и пользователь может варьировать их размер маркером разбиения на правой полосе прокрутки или командой Split из меню Window.
Простейший способ создать приложение со статически разделяемым окном — сгенерировать приложение с динамически разделяемым окном при помощи AppWizard, а потом отредактировать функцию GMainFrame::OnCreateCH<?nt.
CHexView
Класс СНехИеи’написан для любителей поэзии в шестнадцатеричном виде. Он отличается от CSlrin^iewтолько функцией-членом От?' raw: void CHexView: :DnDraw(CDC* pOC) { „-
// шестнадцатеричным дамп строк документа
int1, j,k,1,n,nHeight;
CStringoutputline.str;
CFontfont;
TEXTMETRICtm;
см. след. стр.
CPoemOoc* pDoc = GetDocument():■
font.CreateFont(-160. 30, 0, 0, 400, FALSE, FALSE, 0, ANSI_CHARSET, OUT DEFAULT-PRECIS, CLIP-DEFAULT_PRECIS, DEFAULT-QUALITY, DEFAULT-PITCH ! FF_SWISS, "Arial" );
CFont* pOldFont = pDC->SeiecrObjed(&funt);'
pDC->GetTextMetrics(&tm);.
nHeight = tn.tmHelght + tn.tmExternalLeadiig;
j = pDoc->m_stringArray.GetSize();
for (i = 0; i < j; i++ ) {
outputLine.Format("<02x ", i);■
= pDoc->m_stringArray[i].GetLength();
for (k = 0; k < 1; k++ ) {
л = p0oc->№_stringArray[i][k3 & OxOOff;
str.Format(«02x ", n);
outputLine += str;
pDC->TextOu1(720, -i * nHeight - 720, outputLine);
pDC->SelectObject(pOldFont);
Эта функция показывает шестнадцатеричный дамп всех строк в наборе mstrinyArra)документа. Обратите внимание на оператор индексации, используемый для доступа к отдельным символам в объекте CString.
CMainFrame
Как и в ЕХ20А, классу основного окна-рамки в ЕХ20В нужна переменная-член, связанная с разделяемым окном, и прототип переопределенной функции OnCreateClient. Чтобы сгенери- ронать код, используя Арр Wizard, установите флажок Use Split Window. Модифицировать файл MainFrirth не нужно.
Для файла реализации (MainFrm.cpp) необходимы заголовочные файлы классов «документ* и «вид», какпоказано ниже:
flinclude "PoeniDoc.n."
«include "StringView. h"
«include "'HexView, h"
AppWizard генерирует в функции ОкОг^пс<7^жод динамически разделяемого окна, так что, если хотите сформировать статически разделяемое окно, эту функцию придется подправить. Вместо CSplitterWnd::CreateBH. должны вызвать функцию CSpHtterWn^:CreateStatic;KO' торая предназначен а для работы с несколькими классами «вид». Последующие вызовы CSpllt- ierWnd;:Cre«teVieumos>3no4W^ два таких класса. Последние два параметра функции Gvate- Static заставляют формировать разделяемое окно с двумя секциями; при этом в начальном состоянии разделительная линия отстоит от верхней части окна на }ОО единиц (в аппаратных координатах). Верхняя секция — это обычное (текстовое) представление документа, а нижняя — его шестнадцатеричный дамп. Положение разделительной линии изменять можно, а конфигурацию представления — нет.
BOOL CMainFrame::OnCreateCliant(LPCREATESTRUCT lp:s, CCreateContext^ pContext)
VERIFY(m_wndSplitter.CreateStatic(this, 2, 1));
VEMFY(m-WndSplitter.CreatBViBW(O, 0, RUNTIME„CLASS(CStrIngView), CSize(100, 100), pContext));
VERIFY(m_wndSplitter.CreateView(1, 0, RUNTIME_CLASS(CHexView), CSize(100, 100), pContext)):
return TRUE;■
Тестирование приложения ЕХ20В
После запуска окно программы ВХ20В будет выглядеть, как показано ниже на рисунке. Обратите внимание: в каждой секции свои горизонтальные полосы прокрутки.
1ЬйШ J t а Ч#»1ЬйШ J t а Ч#»0101020203039191Thep<myi:an4yst«»beyood.ttU' El Iswhere 1 first;REinlove wilh unready /еВубеап» glewydiPthesen^^oein ofthat «ptohbes eftfmCon A getxiponthe eouatercneyed eaongThep<myi:an4yst«»beyood.ttU' El Iswhere 1 first;REinlove wilh unready /еВубеап» glewydiPthesen^^oein ofthat «ptohbes eftfmCon A getxiponthe eouatercneyed eaong00 5468 6620 76 65 Be6*79 63 61 Ве6«7Э 73 74 61 7565 20 62 657961 »М2С 74 68 65 20 45 6с00 5468 6620 76 65 Be6*79 63 61 Ве6«7Э 73 74 61 7565 20 62 657961 »М2С 74 68 65 20 45 6с01 В2 03 04 Ю5 56 07 •0801 В2 03 04 Ю5 56 07 •089973 2077 68 95 72 65 20*9 206669 72 73 Г20 2020 20Х 20202020 202020 26 20 20 20 202В66 556s бе20686* 20 вь 61 76 6520 20 20 20 20 20 20 20 20 20 2D 20 20 2 0 20 20 10 20 20 20 2 0 20 77 69 R 68 20 75 6б 72 66 61 бе 69 74 79.^ 4а 65 бсбс 79 62 6 5 61 бе 73 20 67 66 6177 65 64 20 69 8»20 74 68 65 20 73 65 6d 69 2d 67 fc61 61 6d 676 6 20 74 6 8 61 74 20 73 65 70 74 65 66 62 66 72 20 51 65 74 65 72 6*61 61 5e41 2069 El 74207S706t6e 20 74 5865 20 63 M7S6e74657220W6f 7665 642061 6d816e67 20 20 20 2 0 2 0 2 0 20 20 2 0 20 20 2 0 20 20 20 20 20 2Q20 20 20 74 68 65 20 6c69 63« 7 2 69 6 3 65 20 73 74 20 20 2 0 20 2 0 20 20 20 20 20 202020 30 2020X2061 Be 64 20 74 616174 73 999520 72 616*6*739973 2077 68 95 72 65 20*9 206669 72 73 Г20 2020 20Х 20202020 202020 26 20 20 20 202В66 556s бе20686* 20 вь 61 76 6520 20 20 20 20 20 20 20 20 20 2D 20 20 2 0 20 20 10 20 20 20 2 0 20 77 69 R 68 20 75 6б 72 66 61 бе 69 74 79.^ 4а 65 бсбс 79 62 6 5 61 бе 73 20 67 66 6177 65 64 20 69 8»20 74 68 65 20 73 65 6d 69 2d 67 fc61 61 6d 676 6 20 74 6 8 61 74 20 73 65 70 74 65 66 62 66 72 20 51 65 74 65 72 6*61 61 5e41 2069 El 74207S706t6e 20 74 5865 20 63 M7S6e74657220W6f 7665 642061 6d816e67 20 20 20 2 0 2 0 2 0 20 20 2 0 20 20 2 0 20 20 20 20 20 2Q20 20 20 74 68 65 20 6c69 63« 7 2 69 6 3 65 20 73 74 20 20 2 0 20 2 0 20 20 20 20 20 202020 30 2020X2061 Be 64 20 74 616174 73 999520 72 616*6*73fe > .> ^fe > .> ^
1ЬйШ J t а Ч#»
1ЬйШ J t а Ч#»
01
01
02
02
03
03
91
91
Thep<myi:an4yst«»beyood.ttU' El Iswhere 1 first
;REinlove wilh unready /еВубеап» glewydiPthesen^^oein ofthat «ptohbes eftfmCon A getxiponthe eouatercneyed eaong
Thep<myi:an4yst«»beyood.ttU' El Iswhere 1 first
;REinlove wilh unready /еВубеап» glewydiPthesen^^oein ofthat «ptohbes eftfmCon A getxiponthe eouatercneyed eaong
00 5468 6620 76 65 Be6*79 63 61 Ве6«7Э 73 74 61 7565 20 62 657961 »М2С 74 68 65 20 45 6с
00 5468 6620 76 65 Be6*79 63 61 Ве6«7Э 73 74 61 7565 20 62 657961 »М2С 74 68 65 20 45 6с
01 В2 03 04 Ю5 56 07 •08
01 В2 03 04 Ю5 56 07 •08
9973 2077 68 95 72 65 20*9 206669 72 73 Г
20 2020 20Х 20202020 202020 26 20 20 20 202В66 556s бе20686* 20 вь 61 76 65
20 20 20 20 20 20 20 20 20 20 2D 20 20 2 0 20 20 10 20 20 20 2 0 20 77 69 R 68 20 75 6б 72 66 61 бе 69 74 79.^ 4а 65 бсбс 79 62 6 5 61 бе 73 20 67 66 6177 65 64 20 69 8»20 74 68 65 20 73 65 6d 69 2d 67 fc61 61 6d 676 6 20 74 6 8 61 74 20 73 65 70 74 65 66 62 66 72 20 51 65 74 65 72 6*61 61 5e
41 2069 El 74207S706t6e 20 74 5865 20 63 M7S6e74657220W6f 7665 642061 6d816e67 20 20 20 2 0 2 0 2 0 20 20 2 0 20 20 2 0 20 20 20 20 20 2Q20 20 20 74 68 65 20 6c69 63« 7 2 69 6 3 65 20 73 74 20 20 2 0 20 2 0 20 20 20 20 20 202020 30 2020X2061 Be 64 20 74 616174 73 999520 72 616*6*73
9973 2077 68 95 72 65 20*9 206669 72 73 Г
20 2020 20Х 20202020 202020 26 20 20 20 202В66 556s бе20686* 20 вь 61 76 65
20 20 20 20 20 20 20 20 20 20 2D 20 20 2 0 20 20 10 20 20 20 2 0 20 77 69 R 68 20 75 6б 72 66 61 бе 69 74 79.^ 4а 65 бсбс 79 62 6 5 61 бе 73 20 67 66 6177 65 64 20 69 8»20 74 68 65 20 73 65 6d 69 2d 67 fc61 61 6d 676 6 20 74 6 8 61 74 20 73 65 70 74 65 66 62 66 72 20 51 65 74 65 72 6*61 61 5e
41 2069 El 74207S706t6e 20 74 5865 20 63 M7S6e74657220W6f 7665 642061 6d816e67 20 20 20 2 0 2 0 2 0 20 20 2 0 20 20 2 0 20 20 20 20 20 2Q20 20 20 74 68 65 20 6c69 63« 7 2 69 6 3 65 20 73 74 20 20 2 0 20 2 0 20 20 20 20 20 202020 30 2020X2061 Be 64 20 74 616174 73 999520 72 616*6*73
fe > .> ^
fe > .> ^
Пример EX20C
Иногда надо программно переключаться между классами «вид», а связываться с разделяемым окном не хочется. Так вот, пример ЕХ20С — это SDI-приложение, которое переключается между CStringVietva CHexVietUB ответ на команды из меню View. Все, что от Вас потребуется, — добавить две новые команды в меню и некоторое количество кода в класс CMainFrame. Кроме того, сделайте ранее защищенные конструкторы CStringViewn CHexView открытыми.
Требования к ресурсам
В ресурс меню IDR_MAINFRAM& меню View добавлены два элемента.
Элемент меню Идентификатор команды СМа№ате-функция
St&ring ViewIDJ/1EWJSTRLNGVIEWOnViewStringvietv
& Hex ViewID _VIEW_ HEXVIEWOnVtewHexvietp
Функции-обработчики команд ( и обработчики обновления командного пользовательского интерфейса) добавляются в класс CMainFrame с помощью ClassWizard.
CMainFrame
Класс CMainFrame дополняется закрытой вспомогательной функцией SwitcblbView, вызываемой из двух обработчиков команд меню. Параметр перечислимого типа сообщает функции, на какой из объектов «вид» ей переключиться. Следующий код надо добавить в заголовочный файл MainFrm. h:■
private:
enum eView 1STRING = 1, HEX = 2);
void SwitchToView(eView nView);
Функция SivitcbToViewib MainFrm.cpp), чтобы найти и активизировать запрошенный объект «вид», делает ряд низкоуровневых вызовов MFC-функций. Не забивайте себе голову тем, как
все это работает — при необходимости Вы всегда сможете адаптировать эту функцию к своей программе. Добавьте следующий код:
void CMainFrame: :SwitchToView(eView nView)
1
CView* pOldActiveView - GetActiveVlewO;
CView» pNewActiveView = (CView»)GetDlgItem(nView);
if (pNewActiveView == NULL) {
switch(nView) {
case STRING:
pNewActiveView = (CView*)new CStringView;
break;
case HEX:
pNewActiveView = (CView»)new CHexView;
break;
}
CCreateContext context;
context,m_pCuгrentDoc = p01dActiveView->Get0ocument();
pNewActiveVlew->Create(NULL, NULL, WS_BORDER,
CFrameWnd:: rectDefault, this, nView, Acontext);
pNewActiveView->OnInitialUpdate();
^etActiveView(pNewActiveView);
pNewActiveView->ShowWindow(SW_SHOW):
p01dActiveView->ShowWindow(SW_HIDE);
pOldActiveView->SetDlgCtrlID(pOldActiveView->GetRuntiffieClass() =
RUNTIME_CLASS(CStringView) ? STRING : HEX);
pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
RecalcLayout();
А теперь рассмотрим обработчики команд меню и обновления командного пользовательского интерфейса, первоначально сгенерированные ClassWizard (вместе с элементами таблицы сообп^ний и прототипами). Обработчики, обновляющие пользовательский интерфейс, проверяют класс активного объекта «вид».
void CMainFrame: :OnVicwStringView()
{'
SwltchToView(STRING);
}
void CMainFrame: ■.OnUpdateViewStringView(CCmdUI« pCmd'JI)
I
pCmdUI->Enable(!GetActiveView()->IsKindOf(RUNTIME_CLASS(CStringView)));
void CMainFrame::QnViewHexView() ' SwitcbToView(HEX);
void CMainFrame: :OnUpdateViewHexView(CCmdlH» pG'ndlU) pCmdUI->Enable(!GetActiveView()->IsKindOf(RUNTIME_CLASS(CHexView))); •
Тестирование приложения EX20C
Приложение EX20C изначально показываетдокументв окне представления класса CStringView. Вы можете переключаться между CStringView и CHexView, выбирая соответствующую команду из меню View. Оба представления документа приведены рядом на рисунке 20-2.
JeOybedns glowtdlnthe serm-gloom гJeOybedns glowtdlnthe serm-gloom гof th»*, «eptember eftemow^iAcatuponthecountermovodeniong 'of th»*, «eptember eftemow^iAcatuponthecountermovodeniong 'the teehee, stick* and toolsie icllsthe teehee, stick* and toolsie iclls01 6S Z? 20 77 69 55 72 65 20 49 20 66 69’Ж 02 20 20 20 20 20 20 20 20 20 20 20 20 30 fl 03 20 2Q 20 20 2C 20 20 20 20 20 20 50 20 S* ;04 4» 65 6c Sc 79 82 65 61 Se 7Э20 S’Bets- 05 Bl to 20 74 « 61 74 20 73 65 ’0 74 MC.& 06 41 3D63 61 7*20 7570 61 Be20 74 68£*| 07 ■20’20 20 20 20 2020 20 20 20 20 20 20: ^ 08 20 20 20 20 20 2pX 20 20 20 20 20 20 '! | 09 20 20 20 20 20 20 20 20 20 20 20 61 Be? '-01 6S Z? 20 77 69 55 72 65 20 49 20 66 69’Ж 02 20 20 20 20 20 20 20 20 20 20 20 20 30 fl 03 20 2Q 20 20 2C 20 20 20 20 20 20 50 20 S* ;04 4» 65 6c Sc 79 82 65 61 Se 7Э20 S’Bets- 05 Bl to 20 74 « 61 74 20 73 65 ’0 74 MC.& 06 41 3D63 61 7*20 7570 61 Be20 74 68£*| 07 ■20’20 20 20 20 2020 20 20 20 20 20 20: ^ 08 20 20 20 20 20 2pX 20 20 20 20 20 20 '! | 09 20 20 20 20 20 20 20 20 20 20 20 61 Be? '-' O?and OhStp Gum' O?and OhStp GumРис. 20-2. Представление документа объектами CStringVieww CHexViewРис. 20-2. Представление документа объектами CStringVieww CHexView
JeOybedns glowtdlnthe serm-gloom г
JeOybedns glowtdlnthe serm-gloom г
of th»*, «eptember eftemow^i
Acatuponthecountermovodeniong '
of th»*, «eptember eftemow^i
Acatuponthecountermovodeniong '
the teehee, stick* and toolsie iclls
the teehee, stick* and toolsie iclls
01 6S Z? 20 77 69 55 72 65 20 49 20 66 69’Ж 02 20 20 20 20 20 20 20 20 20 20 20 20 30 fl 03 20 2Q 20 20 2C 20 20 20 20 20 20 50 20 S* ;
04 4» 65 6c Sc 79 82 65 61 Se 7Э20 S’Bets- 05 Bl to 20 74 « 61 74 20 73 65 ’0 74 MC.& 06 41 3D63 61 7*20 7570 61 Be20 74 68£*| 07 ■20’20 20 20 20 2020 20 20 20 20 20 20: ^ 08 20 20 20 20 20 2pX 20 20 20 20 20 20 '! | 09 20 20 20 20 20 20 20 20 20 20 20 61 Be? '-
01 6S Z? 20 77 69 55 72 65 20 49 20 66 69’Ж 02 20 20 20 20 20 20 20 20 20 20 20 20 30 fl 03 20 2Q 20 20 2C 20 20 20 20 20 20 50 20 S* ;
04 4» 65 6c Sc 79 82 65 61 Se 7Э20 S’Bets- 05 Bl to 20 74 « 61 74 20 73 65 ’0 74 MC.& 06 41 3D63 61 7*20 7570 61 Be20 74 68£*| 07 ■20’20 20 20 20 2020 20 20 20 20 20 20: ^ 08 20 20 20 20 20 2pX 20 20 20 20 20 20 '! | 09 20 20 20 20 20 20 20 20 20 20 20 61 Be? '-
' O?and OhStp Gum
' O?and OhStp Gum
Рис. 20-2. Представление документа объектами CStringVieww CHexView
Рис. 20-2. Представление документа объектами CStringVieww CHexView
Пример EX20D
В заключительном примере для создания М1Я-приложсния с несколькими классами «вид» без разделяемого окна используются предыдущие классы «документ» и «вид». Его логика отличается от логики других программ с несколькими классами «вид». Основные действия здесь разворачиваются в классе приложения, а не в классе основного окна-рамки. Изучая EX20D, Вы глубже вникнете в объекты CDocTemplate.
Эта программа сгенерирована с параметром Context-Sensitive Help. Обработку контекстно-зависимой справки мы рассмотрим в главе 21. Если Вы начинаете с нуля, создайте, используя AppWizard, обычное МПЬириложепие с одним из классов «вид». Затемдобавьте к проекту второй класс «вид* и модифицируйте файлы классов приложения и основного окна-рамки, как описано ниже.
Требования к ресурсам
В ресурс меню idr_p.X20DTYP№l меню Window добавлены два элемента.
Элемент менюИдентификатор команды Функция СМаюРсате
New StString WindowID WINDOW NEWSTRING CMDIPrameWnd::OnWin<lowNew
(заменяет команду New Window)
New &Hcx WindowID WINDOW NEW_ HEX OnWindowNewHex.
Для создания обработчика команды OnWindotvNeivHexb классе GMainFrame используйте ClassWizard.
CEx20dApp.
В заголовочный файл класса приложения (ex20dJi) добавьте следующую переменную-член:
public:
CMultiDocTemplate* m_pTemplateHex;
Файл реализации ex20d.cpp должен содержать следующие операторы ^include-.
ftinclude ■PoeriDoc.h"
«include "StringView.h", ,.
«include "HexView.h"‘'
В функцию-член Initinstance класса CEx20dApp,сразу после вызова функции AddDocTemp- tote, вставьте такой код:
rapTemplateHex = new CMultiOocTemplate(
IDR EX20DTYPE,
RUNTIME_CLASS(CPoemDoc),
RUNTIME CLASS(CChildFrame),
RUNTlME„CLASS(CStringView));'
Ънзоз AddDocTemplate, сгенерированный AppWlzard, устанавливает для приложения первичную комбинацию «документ/рамка/вид», действующую при запуске программы. Показанный выше объект шаблона — это вторичный шаблон, который активизируется в ответ на выбор в меню команды New Hex Window.।
Остается создать функцию-член Exitinstance, которая очищает вторичный шаблон:
int CEx20dApp: :ExitInstance()-■
{
delete m_pTenplateHex;
return CWinApp: :ExitInstance(); // сохраняет параметры профиля
CMainFrame
Файл реализации класса основного окна-рамки (MainFrm.cpp) включает заголовочные файлы класса CHexView и класса «документ»:
#lnclude "PoemDoc.h" .
«include "HexView.h"’
В базовом классе окна-рамки CMDlFrameWncfccrb функция OnWindowNew, обычно связываемая со стандартной командой New Window меню Window. В EX20D с этой функцией связан пункт меню New S tring Window. Пункт New Hex Window связан с обработчиком (его текст приведен низе), который и создает новые дочерние окна с шестнадцатеричным представлением. Эта функция создана на основе OnWindoivNetva адаптирована для работы со вторичным шаблоном, определенным в Initlnstance.
void CMainFrame: :OnWindowNewHex()
CMDIChildWnd* pActiveChild = MDIGetActive();
CDooument* pDocument;
if (pActiveChild == NULL li (pDocument = pActiveChild->GetActiveDocument()) == NULL)
TRACE ("Предупреждение: нет активного окна для команды WindowNew\n");
AfxMe8sageBox(AFX„IDP_COMMAND_FAILURE);
return; // сбой команды
И иначе - новое окно-рамка
CDocTemplate* pTemplate = ((CEx2QdApp*) AfxGetApp())->m_pTemplateHex:
ASSERT_VALID(pTemplate);
CFrameWnd* pFrame = pTemplate->CreateNewFrame(pDocument, pActiveChild);
if (pFrame == NULL)
{
TRACEO(“Предупреждение: не удалось создать новую рамку\п");
AfxMes6ageBox(AFX_,IDP_C0MMAND_FAILURE);
return; // сбой команды
pTemplate->InitialUpdateFrame(pFrame, pDocument);
ПРИМЕЧАНИЕ Продемонстрированное выше «клонирование» функций — очень полезная технология программирования на базе MFC. Ее суть проста. Сначала найдите функцию базового класса, которая делает максимум того, что Вам нужно, а потом скопируйте ее из подкаталога WC98\mfc\src в свой
производный класс и отредактируйте. Правда, есть риск, что в будущих версиях MFC-библиотеки найденная Вами функция окажется реализованной по-другому.
Тестирование приложения EX20D
После запуска приложения EX20D на экране появится дочернее окно с текстовым представлением. Выберите в меню Window команду New Hex Window; у Бас должно получиться что- то подобное рисунку.
Контекстно-зависимая справка
1 ехнология интерактивных справок в настоящий момент вступила в переходную фазу своего развития. На смену «тексту с форматированием(пск text format, RTF), судя по всему, приходит язык разметки гипертекста (Hypertext Markup Language, HTML). Свидетельство этому — новая интерактивнаядокументацияпо Visual C++ «HTMLHelp», которую можно просматривать при помощи MSDN Viewer и которая основана на HTML Сейчас Microsoft разрабатывает средства для компиляции и индексирования HTML-файлов, которые с Visual C++ 60 не поставляются. Тем временем программы, основанные наиспользовании каркаса MFC-приложений 6.0, настроены на работу с программой WinHelp, входящей в состав Microsoft Windows. Это значит, что нам нужно писать RTF-файлы, а программа будет работать со скомпилированными HLP-файлами.
В этой главе мы покажем, как создавать и обрабатывать простой автономный файл справки, содержащий оглавление и позволяющий переходить от одной темы к другой. Далее Вы увидите, каким образом программа на основе MFC-библиотеки aKTHBH3HpyeTWinHelp, передавая ему идентификаторы контекста справки, производные от идентификаторов окна и команд. И, наконец, Вы узнаете, как изменить в MFC схему обработки сообщений справочной системы, чтобы подогнать ее под нужды своих программ.
Программа WinHelp
Если Вы работали с коммерческими Windows-приложениями, то, наверное, обращали внимание на их справочные системы, формирующие сложнейшие экранные страницы с графикой, гиперссылками и всплывающими окнами. В некоторых фирмах-разработчиках программ, включая Microsoft, создание справочных систем стало отдельной сферой деятельности. Эта глава не сделает Вас экспертом по справочным системам, но позволит с чего-то начать и подготовить простой справочный файл безо всяких «наворотов».
Текст с форматированием
В Windows SDK показано, как создать текстовые ASCII-файлы справки в стандарте текста с форматированием (Rich Text Format, RTF). Мы тоже воспользуемся этим RTF-форматом, но будем работать в режиме WYSIWYG, избегая прямого обращения к громоздким Esc-последовательностям. Будем применять в справочном файле те же шрифты (тех же размеров и начертания), которые пользователь нашей программы увидит в окнах справочной системы. Естественно, нам понадобится текстовый редактор, работающий с форматом RTF. Лично мы предпочитаем Microsoft Word, но формат RTF понимают и многие другие текстовые редакторы.
ПРИМЕЧАНИЕ На рынке существует множество коммерческих инструментальных средств для создания справочных систем под Windows, в том числе RoboHELP фирмы Blue Sky Software и FOREHELP от Forefront Corporation. RoboHELP — это набор шаблонов и макрокоманд для Microsoft Word, a FOREHELP —
автономный пакет, моделирующий WinHelp и, тем самым, обеспечивающий наглядную обратную связь при разработке справочной системы.
Пример ЕХ21 А: подготовка простого справочного файла
Предлагаем написать простой справочный файл, включающий оглавление и три тематических раздела. Этот файл предназначен для чтения напрямую из WinHelp. На C++ ничего программировать не надо. Последовательность действий по созданию справочного файла такова.
Создайте подкаталог \vcpp32\ex21a.
Напишите текст основного файла справки. Воспользуйтесь Microsoft Word (или другим RTF-совместимым текстовым редактором) и наберите приведенный ниже текст. Проверьте, правильно ли Вы применили режимы двойного подчеркивания и скрытого текста и правильно ли поставлен конец страницы.
—• Режим зачеркнутого текста—• Режим зачеркнутого текста-“ Режим одинарного подчеркивания Режим двойного подчеркивания-“ Режим одинарного подчеркивания Режим двойного подчеркиванияРежим скрытого текстаРежим скрытого текста1КЬнец страницы, вставленный с помощью комбинацииклавиш Ctrl-Enter1КЬнец страницы, вставленный с помощью комбинацииклавиш Ctrl-Enter
—• Режим зачеркнутого текста
—• Режим зачеркнутого текста
-“ Режим одинарного подчеркивания Режим двойного подчеркивания
-“ Режим одинарного подчеркивания Режим двойного подчеркивания
Режим скрытого текста
Режим скрытого текста
1КЬнец страницы, вставленный с помощью комбинации
клавиш Ctrl-Enter
1КЬнец страницы, вставленный с помощью комбинации
клавиш Ctrl-Enter
ПРИМЕЧАНИЕ Чтобы увидеть скрытый текст, надо активизировать режим просмотра скрытого текста в своем текстовом редакторе. Например, в Microsoft Word выберите из меню Tools (Сервис) команду Oplions (Параметры), щелкните ярлычок View (Вид) и установите флажок АН в секции Nonprinting Characters (Неотображаемые символы).
Вставьте сноски для оглавления. Оглавление (Table Of Contents) — первый экран нашей справочной системы. Откройте в текстовом редакторе секцию сносок и вставьте перед заголовком тематического раздела следующие сноски (footnotes) с метками, показанными в таблице.
МеткаТекст сноскиОписание
*HID_CONTENTSИдентификатор контекста справки
5SIMPLE Help ContentsЗаголовок тематического раздела
После этого документ должен выглядеть как на первом рисунке на стр. 368
Вставьте сноски для экрана Topic 1. Topic 1 — второй экран нашей справочной системы. Вставьте следующие сноски с метками, показанными в таблице.
Метка Текст сноскиОписание
*
HID_TOPICI
Идентификатор контекста справки
$
SIMPLE Help Topic 1
Заголовок тематического раздела
к
SIMPLE Topics
Ключевой текст
^Simple-Help-Table-of Contents^Help topicalГп»тг]ШП TOPigjf Хя^^ТОЖИ 1^*еа!у1^тоя£Лjrffier |A| Footnotes'HID CCNTENTSf* SIMPLE Heh> Continue^Simple-Help-Table-of Contents^Help topicalГп»тг]ШП TOPigjf Хя^^ТОЖИ 1^*еа!у1^тоя£Лjrffier |A| Footnotes'HID CCNTENTSf* SIMPLE Heh> ContinueNormalri 1 roes Naw Romen S18
^Simple-Help-Table-of Contents^
Help topical
Гп»тг]ШП TOPigjf Хя^^ТОЖИ 1^*еа!у1^тоя£Л
jrffier |A| Footnotes
'HID CCNTENTSf
* SIMPLE Heh> Continue
^Simple-Help-Table-of Contents^
Help topical
Гп»тг]ШП TOPigjf Хя^^ТОЖИ 1^*еа!у1^тоя£Л
jrffier |A| Footnotes
'HID CCNTENTSf
* SIMPLE Heh> Continue
mwrWfeHMMH
Дважды продублируйте экран Topic 1. Скопируйте раздел Topic 1, включая маркер конца страницы, в буфер обмена и вставьте в документ две его копии. Вместе с текстом копируются и сноски. В первой копии замените все цифры 1 на 2, во второй — на 3. Не забудьте изменить и сноски. В Word бывает трудно сразу определить, к чему относится каждая сноска, так что будьте внимательны. Документ, включая сноски, после этой операции выглядит как на рисунке ниже.
'•Simple-Help-Table-oFContents^ 1
Helptoptcol
1ЙШ61ШВ_ТОИ.С.11
^Help-Topic-3U^Help-Topic-3UТЫе istiwM. for help toy* nuwbw 11ТЫе istiwM. for help toy* nuwbw 11FootnotesFootnotes«нш.еонтЕэтзц’SIMPLE-Help CcnUrusf*HIO_?O?ICIf’SIMPLE Help Tof.c IfKsiMPLETopiexf,hid_tof:c21*S1MPLE Help TopicifKgiMPLZTopicsi;«KlD-TOPICSf$S№LE ННрТорасЗЦ^IMPLETopicef«нш.еонтЕэтзц’SIMPLE-Help CcnUrusf*HIO_?O?ICIf’SIMPLE Help Tof.c IfKsiMPLETopiexf,hid_tof:c21*S1MPLE Help TopicifKgiMPLZTopicsi;«KlD-TOPICSf$S№LE ННрТорасЗЦ^IMPLETopicef*KHelp-Topic-11f*KHelp-Topic-11fThisis-ihe text-foi help tcpisnyr^w 4 ,Ц —-н.Лpay fre*—Thisis-ihe text-foi help tcpisnyr^w 4 ,Ц —-н.Лpay fre*—*»Help-Topic-2fuThia и tfieiert/ashelp topic numbet 2 Д ,-№дэ Break—*»Help-Topic-2fuThia и tfieiert/ashelp topic numbet 2 Д ,-№дэ Break—""Ж""ЖГ|Г|Т&рк^НГР.ТОЯС^ K^Hl.DJT£^
^Help-Topic-3U
^Help-Topic-3U
ТЫе istiwM. for help toy* nuwbw 11
ТЫе istiwM. for help toy* nuwbw 11
Footnotes
Footnotes
«нш.еонтЕэтзц
’SIMPLE-Help CcnUrusf
*HIO_?O?ICIf
’SIMPLE Help Tof.c If
KsiMPLETopiexf
,hid_tof:c21
*S1MPLE Help Topicif
KgiMPLZTopicsi;
«KlD-TOPICSf
$S№LE ННрТорасЗЦ
^IMPLETopicef
«нш.еонтЕэтзц
’SIMPLE-Help CcnUrusf
*HIO_?O?ICIf
’SIMPLE Help Tof.c If
KsiMPLETopiexf
,hid_tof:c21
*S1MPLE Help Topicif
KgiMPLZTopicsi;
«KlD-TOPICSf
$S№LE ННрТорасЗЦ
^IMPLETopicef
*KHelp-Topic-11f
*KHelp-Topic-11f
Thisis-ihe text-foi help tcpisnyr^w 4 ,Ц —-н.Лpay fre*—
Thisis-ihe text-foi help tcpisnyr^w 4 ,Ц —-н.Лpay fre*—
*»Help-Topic-2f
u
Thia и tfieiert/ashelp topic numbet 2 Д ,-№дэ Break—
*»Help-Topic-2f
u
Thia и tfieiert/ashelp topic numbet 2 Д ,-№дэ Break—
""Ж
""Ж
Г|
Г|
Сохраните документ в файле \vcpp32\ex21a\simple.rtf. В качестве типа файла выберите Rich Text Format.
Подготовьте файл проекта справочной системы. Используя Visual C++ или другой текстовый редактор, создайте файл \vcpp32\ex21a\simple.hpj с таким текстом;
|[OPTIONS]
'CONTENTS=HI D_CONTENTS
TITLE=SIMPLE Application Help
.COMPHESS=true
WARNING=2
[FILES]
simple.rtf
Файл определяет идентификатор контекста справки для оглавления и имя RTF-файла, содержащего текст справки. Сохраните подготовленный файл в текстовом формате (ASCII). .8. Соберите справочный файл. Запустите в Windows утилиту Microsoft Help Workshop
(HCRTF), которая обычно располагается в каталоге Program Files\MicrosoftVisual Studio\Com- mon\Tools. Откройте файл \vcpp32\ex2 la\51mple.hpj и щелкните кнопку Save And Compile. Это запустит Windows Help Compiler с файлом проекта simple.hpj. Компилятор сформирует справочный файл simple.hlp в том же каталоге.
ПРИМЕЧАНИЕ Если для создания RTF-файла применяется Word 97, следует убедиться, что используется версия утилиты HCRTF 4.02 или выше. Более ранние версии не могут обработать флажки ■форматирования RTF, генерируемые Word 97.
Запустите WinHelp с новым справочным файлом. В Windows Explore г дважды щелкните файл \vcpp32\ex2 la\simple.hlp. Экран с оглавлением должен выглядеть как на рисунке ниже.
SIMPIF Application Help■■ I uI^hTj ’
|!_j_LL^> ^a u ^/ j
Simple Help Table of Contents1 '
Helptopics
Topic!
JjiRigv?:
Topic 3!
Теперь переместите курсор мыши на строку Topic 1 и обратите внимание на то, как меняется форма курсора: вместо стрелки он превращается в «указующий перст», Нажмите левую кнопку мыши, и появится экран Topic 1.
Help Topic 1
This is the text forhe Ip t opic nurabet t.
!
|ининуйщн8ьм5щ^ймв»«й»ыаа»в«йммааивйвм
Текст HID TOPIClb оглавлении связан с соответствующим идентификатором контекста (сноска *) на тематической странице. Эту связь называют также переходом (jump).
Переход на Topic 2 вызывает на экран всплывающее окно. Щелкнув строку Topic 2, Вы увидите следующее.
Щелкните кнопку Contents в Win Help. Щелчок этой кнопки должен открытьэкран с оглавлением (см. п. 9). Программе WinHelp известен идентификатор этого экрана, поскольку Вы определили его в HPJ-файле.
П. Щелкните кнопку Index в WinHelp. WinHelp откроет диалоговое окно Index со списком ключевых слове данном справочном файле. В нашем файле (simple.hlp) во всех тематических разделах (кроме оглавления) только одно ключевое словосочетание — SIMPLE Topics (сноски К). Дважды щелкнув его, Вы увидите все связанные с ним тематические разделы (сноски S).■
То, что мы получили в результате, называетсядвухуровневой системой поиска справочной информации. Пользователь может ввести первые несколько букв ключевого слова и выбрать тему из списка. Чем тщательнее Вы подберете ключевые слова и названия тематических разделов, тем эффективнее будет работать ваша справочная система.
Совершенствование оглавления
Оглавление, которое мы только что подготовили, сегодня считается старомодным. Последняя вс|ч : я WinHelp для Win3.2 позволяет создавать современное оглавление с древовидной струк-
турой. Все, что для этого нужно — текстовый файл с расширением CNT. Добавьте в каталог \vcpp32\ex21a новый файл simple.ent с таким текстом:
:Base simple.hip
Help topics
Topic 1=HID_TOPIC1
2 Topic 2=HID_T0PIC2
2Topic3=HID_TOPIC3
Идентификаторы контекста соответствуют содержимому справочного файла. Скомпилировав simple.cnt, при следующем запуске WinHelp с файлом simplc.hlp Вы увидите новое оглавление.
CNT-файлы можно редактировать, используя HCRTR cNT-файл не зависит от НР|- и RTF- файлов, Но, обновив свои RTF-файлы, не забудьте внести соответствующие изменения и в CNT-файл."
Каркас приложений и WinHelp
Вы видели, что WinHelp может работать как автономная программа. В то же время с ней может взаимодействовать каркас приложений, обеспечивая, тем самым, вывод контекстно-зависимой справки в ваших программах. Вот как вкратце это происходит.
При запуске AppWizard Вы выбираете параметр Context-Sensitive Help.
AppWizard формирует в меню Help вашего приложения элемент Help Topics и создает один или несколько RTF-файлов, HPJ-файл и командный файл, запускающий компилятор справочной системы Help Compiler.
Затем AppWizard определяет Fl как «быструю клавишу* и связывает ее и элемент Help Topics меню Help с функциями-членами объекта основного окна-рамки.
При выполнении ваша программа, когда пользователь нажимает клавишу F1 или выбира ет команду меню Help Topics, вызывает WinHelp, передавая ей идентификатор контекста, определяющий, какую именно справочную тему показать на экране.
Теперь разберемся, как вызвать WinHelp из вашей программы, и как последняя генерирует идентификаторы контекста для WinHelp.
Вызов WinHelp
Функция-член С№:пАрр::№тНе1р«тивизир^ WinHelp из вашего приложения Заглянув в интерактивную документацию, Вы увидите длинный перечень операции, контролируемых
необязательным (вторым) параметром функции Жи/7е/р.Проигнорируйте его и считайте, что у WinHelp есть только один параметр — dwData типа unsigned long ini. Этот параметр задает справочные темы. Представьте, что у Вас есть справочный файл SIMPLE и что в вашей программе есть оператор:.
AfxGetApp()->WlnHelp(HlDjOPIC1);
После исполнения этого оператора (в ответ на нажатие клавиши F1 или другое событие) появляется экран справки Topic 1 — словно пользователь выбрал этот раздел в оглавлении.
♦Постойте-ка, — заметите Вы, — а откуда WinHelp знает, какой справочный файл взять?» Дело в том, что имя справочного файла соответствует имени программы. Если ее исполняемый файл называется simple.exe, справочный файл называется simple.hlp.
ПРИМЕЧАНИЕ Можно заставить WinHelp использовать другой справочный файл, записав нужное значение в переменную-член т_pszHelpFilePath класса СИйпАрр.
«А как WinHelp связывает константу HIDJTOPICI с идентификатором контекста справочного файла?» — спросите Вы. Файл проекта справки должен содержать раздел МАР, в котором идентификаторы контекста увязаны с конкретными числами. Если в файле resource.h приложения HIDJTOPICIопределен как 101, то раздел МАР файла simple.hpj выглядит таю {МАР] IKID_TOPIC1 101
Имена констант (заданных в программе операторами ^define^ws обязательно должны совпадать с идентификаторами контекста — главное, чтобы совпадали их численные значения. Тем не менее, соответствие имен считается хорошим тоном в программировании.
Применение строк для поиска
В программе, так или иначе оперирующей с текстом, может понадобиться поиск справки по ключевому слову, а не по числовому идентификатору контекста. В этом случае используйте необязательный параметр HELPJKEY или //^/Т^/МйТХШСйУфункции WinHelp;
CString string("найти эту строку"):’
AfxGetАрр()->WirHelp((DWORD) (LPCSTR) string, HELP_KEY):
Двойное приведение типа для string необходимо потому, что первый параметр WinHelp — многоцелевой; его смысл зависит от значения второго параметра.
Вызов WinHelp из меню программы
AppWizard генерирует в меню Help элемент Help Topics, соотнося его с функцией CWncLOn- HelpFinder^ основном окне-рамке, которая обращается к WinHelp таким образом:
AfxGe*App()->WinHelp(OL, HELP_FINDER):
При этом вызове WinHelp вывод ит экран оглавления, позволяя пользователю перемещаться по файлу справки при помощи переходов и поиска текста.
Если Вы хотите получить «старомодное» оглавление, напишите:
AfxGetApp()->WinHelo(OL, HELP_INDEX);
А если Вам нужна справка по работе со справкой, сделайте вызов:
AfxGetApp()->WinHelp(OL. HELP_HELPONHELP);
Синонимы контекстной справки
Раздел ALIAS в HPJ-файле позволяет отождествлять один идентификатор контекста с другим. Допустим, в вашем HPJ-файле есть такие операторы:
| [ALIAS]
HID_TOPIC1 = HJD_GETTING_STARTED
[MAP]'.
HID_TOPIC1 101
Тогда в RTF-файлах идентификаторы НЮ_ТОР$С1и HlD_GETT[NG_STARTES>Hjm. бы взаимозаменяемы и связаны с контекстом справки 101.
Определение контекста справки
Теперь Вы уже знаете, как создать в MFC-программе простую систему контекстно-зависимой справки. Вы определяете F1 как «быструю клавишу* (в MFC-библиотеке это стандартная клавиша для вызова контекстной справки) и пишете обработчик команды, связывающий контекст справки с параметром функции WinHelp. Можно было бы придумать свой способ сопоставить состояние программы контекстному идентификатору, но отчего же не воспользоваться преимуществами системы, уже встроенной в каркас приложений?
Каркас приложений определяет контекст справки на основе идентификатора активного элемента программы. К последним относятся команды меню, окна-рамки, диалоговые и информационные скна, а также элементы управления. Элемент меню можно идентифицировать, скажем, как ID_EDIT_C!EAR_ALL^ основное окно-рамка обычно имеет идентификатор 1DRMAIN- FRAME. Можно было бы ожидать, что все эти идентификаторы прямо связаны с контекстами справки. А что если идентификаторы команды и окна-рамки имеют одно и то же численное значение? Очевидно, необходим какой-то способ, чтобы избежать таких накладок.
В каркасе приложений эта проблема решается путем определения нового набора *dejine- констант справки, производных от ццентификаторов программных элементов. Эти константы представляют собой сумму идентификаторов программных элементов и определенных базовых значений.
'Программный элемент
Префикс
идентификатора элемента
Префикс
идентификатора
контекста справки
Базовое значение
(шестнадцатеричное)
Элемент меню или кнопка панели инструментов
ID_, IDM_
HID_>H1DM_
10000
Окно-рамка или диалоговое окно
IDR_,IDD_
HIDR_,HIDD_
20000
Окно сообщений об ошибках
IDP_
HIDP_
30000
Неклиентская область
H_
40000
Элемент управления
1DW_
HIDW_
50000
Сообщения об ошибках диспетчеризации
60000
HIDjEDIT С1МЯ_АЩ0х1Е121) соответствует 1D_ED1T_CLEAR_ALL (ОхЕ121), a H1DR_MA1N- FRAME (0x20080) - 1DR_MAINFRaME(0x80).
Вызов справки клавишей F1■
В MFC-программу встраиваются еще два способа доступа к контекстно-зависимой справке; они доступны при выборе в AppWizard параметра Context-Sensitive Help. Первый способ — стандартный вызов справки при нажатии клавиши F1. Пользователь нажимает эту клавишу, а программа, определив контекст справки, вызывает WinHelp. Этот способ позволяет задать вывод справки об элементе меню, выбранном с клавиатуры, или о текущем окне (рамке, представлении, диалоговом или информационном окне).
Вызовсправки сочетанием клавиш Shift+F1
Второй способ вывода контекстно-зависимой справки предоставляет больше возможностей, чем первый. В нем программа может идентифицировать следующие контексты справки:
элементмсню, на который указывает курсор мыши;
кнопку на панели инструментов;
окно-рамку;
окно представления;
I* конкретный графический элемент в окне представления;
строку состояния;
различные неклиентские элементы такие, как команды системного меню.
ПРИМЕЧАНИЕ Справка Shifl+F1 не работает с модальными диалоговыми и информационными окнами.
Пользователь активизирует такую справку, нажимая сочетание клавиш Shift+Fl или шелч- кая кнопки Context Help на панели инструментов:
■
В любом случае курсор мыши меняет свою форму на:
t$?
При следующем щелчке мышью появляется справка с контекстом, определяемым по позиции курсора.
Справка в информационном окне: функция AfxMessageBox
Глобальная функция Л/№е$$#£еВажыводит сообщения об ошибках, формируемые каркасом приложений. Она аналогична функции-члену CWnd::MessageBox,Ho в качестве дополнительного параметра получает идентификатор контекста справки. Каркас приложений соотносит его с идентификатором контекста WinHelp и вызывает WinHelp, когда пользователь нажимает клавишу F1. Если Вы хотите использовать упомянутый параметр Л/xMessageBox. соответствующие идентификаторы должны начинаться с 1DP_, а контексты справки в вашем RTF- файле — cHIDP_.
Существует два варианта AfxMessageBox. В первом строка подсказки определяется параметром — указателем на символьный массив. Во втором (с ним программы работают эффективнее) параметр-идентификатор подсказки определяет строковый ресурс. У обоих вариантов A/xMessageBox есть параметр «стиль», заставляющий показывать в информационном окне восклицательный или вопросительный знак либо еще какой-нибудь графический символ.
Стандартные справочные разделы
Когда выбран параметр Context-Sensitive Help, AppWizard формирует справочные разделы, связанные со стандартными программными элементами MFC. К ним относятся:
стандартные команды меню и кнопки на панели инструментов (File, Edit и т. д.);
неклиентские элементы окна (кнопка развертывания окна, строка заголовка и т. д.);
строка состояния;
окна сообщений об ошибках.
Справочные разделы по этим элементам содержатся в файлах AfxCore.rtf и AfxPrint.rtf, размещаемых вместе с соответствующими файлами растровых изображений в подкаталоге HLP проекта, Ваша работа — адаптировать эти файлы под свою программу.
ПРИМЕЧАНИЕ AppWizard генерирует AfxPrint.rtf, только если Вы включите параметр Printing And Print Preview.
Пример созданиясправкибезпрограммирования
Прорабатывая программу-пример EX20D в главе 20, Вы должны были выбрать в AppWizard параметр Context-Sensitive Help. Давайте вернемся к тому примеру и исследуем «справочные» возможности, встроенные в каркас приложений. Вы увидите, насколько легко связать справочные разделы с идентификаторами команд меню и ресурсов окна-рамки. Вы будете редактировать RTF-, а не СРР-файлы. Для модификации справочной системы EX20D необходимо, чтобы Вы выполнили следующие действия.
Проверьте правильность построения справочного файла. Если Вы уже собрали проект EX20D, то велика вероятность, что справочный файл был создан в ходе процесса сборки. Проверьте это, запустив приложение и нажав клавишу F1. Вы должны увидеть стандартный экран Application Help с заголовком «Modifyingthe Document», какпоказано ниже на рисунке.
Modifying Ilie Document'"
<« Writeappfcea.on-spedfichelp herethdt providesenoverview of howthe use r should mcditya document using your application..
Ifyour application supports multiple document types and you went to have в distinct helptopiclor each. Ihan use the help context I. d generated by running the MAKEHELP BAT file produced byApptrVzarci Alternatively, run MAKEHM «6 follows:
mekehm IOR_HiDR_.0x260(i resource h
; К the IDR_ symbol tor one ofyour documenttypes is. for example, lDR_CHARTTYPE,*enthe help coniexl I d. generated by MAKEHMwill be
. HIDK.CHARTTYPE
j Note, AppWizerd definesthe HIDR_DOC1TYPEhelp context id. used bythis ■ help tcpicf orthe first document type supported byyoureppllcation.
AppWrard produces on alias inthe +PJ file Icrycur application mapping HIDR.DOCTTrPEto the HIDRjsroducedbyMAKEHMfortial document i type »
Если Вы не увидели такого экрана, то это означает, что командный файл MAKEHELP не сработал должным образом. Прежде всего, проверьте последние две строки в файле ex20d.hpj в подкаталоге \Ыр. Все ли пути указаны правильно в вашей установке Visual С+ +? Затем выберите команду Options в меню Took и щелкните вкладку Directories. Убедитесь, что подкаталог \VC98\bin каталога Visual C++ входит в число каталогов поиска для исполняемых файлов (Executable Files).
Для генерации справочного файла, выберите файл ex20d.hpj на вкладке FileView окна Workspace и в меню Build выберите команду Compile. Тем самым Вы запустите командный файл MAKEHELP, который находится в каталоге проекта. (Запустить его можно и непосредственно из окна MS-DOS.) Вы должны увидеть несколько сообщений «file(s) copied», но ни одного сообщения об ошибке. Теперь заново запустите программу EX20D и вновь нажмите F1.
ПРИМЕЧАНИЕ Программа make в Visual C++ не всегда отслеживает все зависимости в справочной системе. Поэтомудля перестройки HLP-файла после внесения изменений зачастую приходится запускать MAKEHELP вручную.
Проверьте стандартный справочный файл. Проведите следующие эксперименты.
т Закройте диалоговое окно справки и нажмите сочетание клавиш Alt+F, а затем Р1, Должен появиться справочный раздел о команде New из меню File. Удерживая нажатой
кнопку мыши, курсор которой указывает на пункт меню File New, и нажав клавишу F1, Вы увидите туже самую справку.
' т Закройте окно справки, нажмите кнопку Context Help (показанную ранее в разделе «Вызов справки сочетанием клавиш Shift+Fl») на панели инструментов и выберите из меню File команду Save. Появился ли соответствующий раздел справки?
т Снова нажмите кнопку Context Help на панели инструментов и щелкните строку заголовка окна-рамки. Вы должны получить справку по заголовкам окон в Windows.
т Закройте все дочерние окна и нажмите F1. Вы должны увидеть основную индексную страницу, которая представляет собой «старомодное* оглавление.
Измените заголовок приложения. В файле AfxCoie.rtf (в каталоге \vcpp32\cx20d\hlp) есть несколько строк «УоигАрр»Зам.ениге их повсюду на EX20D.
Отредактируйте справочный раздел Modifying The Document. Этот текст хранится в файле AfxCore.rtf (каталог \vcpp32\ex20d\hlp), Найдите раздел Modifying TbeDocument и замените его текст своим, подходящим для вашего приложения. Идентификатор контекста справки у этого раздела — HtoRDOC 1TYPE.Сгенерированный файл ex20d.hpj предоставляет синоним HIDR_EX20DTYPE.
Добавьте раздел для команды New String Windows меню Window. Эта команда добавлена в EX20D нами, и поэтому для нее нет справочного текста. Введите нужный текст в AfxC ore. rtf, как показано ниже на рисунке.
*hBw Hex, command (Window menu)*hBw Hex, command (Window menu)Use this command to open a new String wlndowwith the same contents as the active window You can open multiple document windows to display different pins or vows of a document at the same time When you open a new window, it becomes the aclivewindowend is displayed on lop of all other open windows.Use this command to open a new String wlndowwith the same contents as the active window You can open multiple document windows to display different pins or vows of a document at the same time When you open a new window, it becomes the aclivewindowend is displayed on lop of all other open windows.^NewSiring command (Windowmenu)^NewSiring command (Windowmenu)[<d Footnotes.[<d Footnotes.Use this command to open a new Hexwindowwiih the same conienis as; the active window. You;Use this command to open a new Hexwindowwiih the same conienis as; the active window. You;* HIDJVINDOW^NE^ST RI NG* mD-~WlNDOWJIBW^HEX* HIDJVINDOW^NE^ST RI NG* mD-~WlNDOWJIBW^HEX
*hBw Hex, command (Window menu)
*hBw Hex, command (Window menu)
Use this command to open a new String wlndowwith the same contents as the active window You can open multiple document windows to display different pins or vows of a document at the same time When you open a new window, it becomes the aclivewindowend is displayed on lop of all other open windows.
Use this command to open a new String wlndowwith the same contents as the active window You can open multiple document windows to display different pins or vows of a document at the same time When you open a new window, it becomes the aclivewindowend is displayed on lop of all other open windows.
^NewSiring command (Windowmenu)
^NewSiring command (Windowmenu)
[<d Footnotes.
[<d Footnotes.
Use this command to open a new Hexwindowwiih the same conienis as; the active window. You;
Use this command to open a new Hexwindowwiih the same conienis as; the active window. You;
* HIDJVINDOW^NE^ST RI NG
* mD-~WlNDOWJIBW^HEX
* HIDJVINDOW^NE^ST RI NG
* mD-~WlNDOWJIBW^HEX
Обратите внимание на сноску *, связывающую раздел с идентификатором контекста HID_W1N- DOWJ^EWjiTRlNGрпредел.енным. в hlp\ex20d.hm. Элемент меню New String Window, конечно же, имеет идентификатор команды 1D_WINDOW_NEW_STRING.
Соберите справочный файл повторно и снова протестируйте приложение. Вновь запустите командный файл MAKEHELP, а потом программу EX20D. Проверьте две новые .связи в справке.'
Создание справочной системы
Процесс формирования HLP-файла программы весьма сложен. Отчасти это вызвано тем, что Help Compiler не воспринимает операторы типа:.
HID_MAINFRAME = IP MAINFRAME + 0x20000
Из-за этого приходится считывать файл resource,h специальной программой предварительной обработки (makehm.exe) и создавать файл соответствия, определяющий значения идентификаторов контекста справки. Приведенная ниже схема иллюстрирует весь процесс формирования справочной системы.
Арр Wizard генерирует файл проекта справочной системы (НЮ) и файл содержания справки (CNT). В файле проекта раздел [FILES] определяет RTF-файлы, а раздел [МАР] — операторы ^indudewia включения файлов соответствия (НМ), как стандартного, так и специфичного для данной программы. Help Workshop (Hcrtf) обрабатывает файл проекта, создавая справочный файл, понятный WinHelp.
Обработка команд запроса справки
Итак, Вы увидели, из каких компонентов состоит справочный файл, и как действуют клавиша F1 и сочетание клавиш Shift+Fl. Бы знаете теперь, как идентификаторы программных элементов связаны с идентификаторами контекста справки. Однако внутреннюю обработку команд запроса справки каркасом приложений мы пока не обсуждали. Нужно ли вообще это рассматривать? Но представьте себе: Вы хотите дать справку о каком-то окне представления (а не об окне-рамке) и собираетесь связать справочные разделы с теми или иными графическими элементами в данном окне. И это, и многое другое можно реализовать, только переопределив функции, отвечающие за обработку команд в меню Help.
Такая обработка зависит от способа, которым была запрошена справка: нажатием F1 или Shift+Fl. Рассмотрим их обработку по отдельности.
Обработка клавиши F1
Клавишу F1 обычно обрабатывает команда, указанная в соответствующей записи таблицы быстрых клавиш, которую Арр Wizard вставляет в RC-фаЙл. Клавиша F1 сопоставляется команде !D_HELP,a та — функции-члену OnHelp класса CFrameWnd.
ПРИМЕЧАНИЕ В активном модальном диалоговом окне или в процессе выбора из меню клавиша F1 обрабатывается Windows-ловушкой, которая вызывает ту же функцию OnHelp. В противном случае быстрая клавиша F1 была бы заблокирована.
Функция CFrameWncLOnHelpTtocwiaer MFC-сообщение WM_COMMANDHELP самому внутреннему, окну — это обычно окно представления. Если в классе «вид» нет обработчика этого сообщения или обработчик возвращает FALSE, каркас приложений направляет сообщение вверх следующему по иерархии окну (это обычно дочернее окно-рамка MDI или основное
окно-рамка). Если в ваших производных классах окон-рамок не .определены обработчики WMCOMMANDHELP, сообщение обрабатывается в MFC-классе CFrameWnd. Он отображает справку для символа, который определил AppWizardдля вашего приложения или типа документа. •
Если Вы определили обработчик WM COMMANDHELP в производных классах, ваш обработчик должен вызывать CWinApp:;WinHelpc корректным идентификатором контекста в качестве параметра.
В SDI-приложении это сообщение обрабатывается функцией базового класса основного окна-рамки CFrameWnd:OnCommandHelp, которая вызывает WinHelp с идентификатором контекста справки H1DR MAINFRAME, Основное окно-рамка — окно верхнего уровня, которое служит отправной точкой в обработке сообщения WM_COMMAND1IELP. Если Вам надо показывать справку для окна представления или другого дочернего окна, включите обработчик сообщения WM_COMMANDHELP в производный класс рамки. Далее ваша функция ОпСот- mandHelpi\o;nKt\A переслать WM_COMMANDHELP по цепочке вниз, активному окну представления. Получив сообщение, оно вызовет Win Help с соответствующим идентификатором контекста справки
Для любого приложения AppWizard добавляет к проекту символ 1DR_MA1NFRAM^ НМ- файл определяет идентификатор контекста справки HIDR^MAINFRAME^oropHS. сопоставляется с main Judex в HPJ-файле. Стандартный файл AfxCoie.rtf ассоциирует с этим идентификатором контекста основной индекс. Например, для MDI-приложенияпод названием SAMPLE AppWizard внесет в проект символ lDR_SAMPLlSTYPEg. в НМ-файде определит идентификатор контекста HIDR_SAMPLEl'YPE^<ytoyH&. сопоставляется в HPJ-файле символу HIDRJXX3- TYPE. Стандартный файл AfxCore.rtf ассоциирует с этим идентификатором контекста страницу «Modifying the Document».
Обработка сочетания клавиш Shift+F1
При нажат ии сочетания клавиш Shift+Fl или щелчке кнопки Context Help на панели инструментов функция CFrameWndxOnContexrHelpionvuaeT сообщение, связанное с соответствующей командой меню. Когда пользователь вновь нажимает кнопку мыши, поместив курсор па какой-нибудь элемент экранна, низшему окну (из числа тех, где обнаружен щелчок мыши) посылается сообщение WM_HELPHITTEST. С этого момента распространение сообщения происходит точно так же, как pacnpocrpanei me сообщения WM_COMMANDHELP, описанное в предыдущем разделе. •
Параметр iParam функции cmHelpHitTesicoR&pym? координаты мыши в единицах устройства относительно верхнего левого угла клиентской области окна. В старшем слове хранится координата у, а в младшем — х. Используя эти координаты, можно указывать идентификатор контекста справки, специфичного для элемента окна представления. Обработчик OnHelp- HitTestдолжен возвращать правильный идентификатор контекста; каркас приложения вызовет WinHelp.
Пример ЕХ21В
Эта программа базируется на примере EX20D из главы 20 и представляет собой MDI-приложение с двумя окнами представления; в ее справочную систему добавлены разделы, специфичные для этих окон. В каждом из двух классов «вид* имеется обработчик сообщений OnCohimandHelp-^ обработки нажатий FI и OnHelpHitTest — для обработки комбинации Shift+Fl.
Требования к заголовочным файлам
Компилятор распознает специфичные для справки идентификаторы, только если присутствует следующий оператор include:
tfinclude <afxprlv.h> "
В ЕХ21В такой оператор находится в файле StdAfx.h.
CStringView■
Классу, отвечающему за строковое представление документа, в StringView.h нужны прото типы функций таблицы сообщений как для справки по F1, так и для справки по Shift+Fb
afXjnsg LRESULT OnCommandHelp(WPARAM wParam, LPARAM IParam);
afx_msg LRESULT OnHelpHitTest(WPARAM wParam, LPARAM IParam);
а в String View, cpp — следующие элементы таблицы сообщений:
ON MESSAGE(WM_COMMANDHELP, OnCommandHelp)
ON_MESSAGE(WM_HELPHITTEST, OnHelpHitTest)
Функция-член OnCommandHelpa StringView.cpp обрабатывает запросы справки no Fl. Она реагирует на сообщение, присланное из основного окна-рамки MDI, и показывает справочный раздел для окна строкового представления:
LRESULT CStringView: :0flCommandHelp(WPARAM wParam, LPARAM IParam)
■I
if (IParam == 0) {// контекст еще не определен
IParam = HID_BASE_RESOURCE + IDR STRINGVIEW;
}
AfxGetApp()->WinHelp(lParam);
return TRUE;
}
Функция-член OnHelpHitTest обрабатывает справку no Shift+Fl;
LRESULT CStringView::OnHelpHitTest(WPARAM wParam, LPARAM IParam)
return HID_BASE_RESOURCE + IDR_STRINGVIEW;
В более сложной программе могло бы потребоваться, чтобы ОяНе^НИИ^сг^иав-тавала контекст справки по позиции курсора.
CHexView
Класс CHexView обрабатывает запросы справки также, как и класс CStringView. В Hex View, h обязательно должен быть следующий код:
afxjnsg LRESULT OnCommandHelp(WPARAM wParam, LPARAM IParam);
afignsg LRESULT OnHelpHitTest(WPARAM wParam, LPARAM IParam);
Элементы таблицы сообщений в HexView.cpp выглядят так:
ON_MESSAGE(WM COMMANDHELP, OnCommandHelp)
ON_MESSAGE(WM_HELPHITTEST, OnHelpHitTest)
И, наконец, код реализации в HexView.cpp:
LRESULT CHexView: :OnCommandHelp(WPARAM wParam, LPARAM IParam) {
if (IParam == 0) {// контекст еще не определен
IParam = HID_BASE_RESOURCE + IDR.HEXVIEW;
AfxGetApp()->WinHelp(IParam);
return TRUE;
}
LRESULT CHexView::OnHelpHitTest(WPARAM wParam, LPARAM IParam)
return HID BASE_RESOURCE + IDR HEXVIEW;
}
Требования к ресурсам
В файл ресурсов добавлены два символа. В таблице перечислены их значения и соответствующие им идентификаторы контекста справки.
Символ
Значение
Идентификатор контекста справки
Значение
IDR_ STRINGVIEW
101
HIDR.STRjNGWEW
0x20065
IDRJ1EXVIEW
102
HlDR_HEXVtEW
0x20066
Требования к справочному файлу
В файл AfxCore. rtf добавлены два справочных раздела — с идентификаторами HIDR STRING- VIEW^ HIDR. HEXVIEW.~
Сгенерированный в подкаталоге HLP проекта файл ex21b.bin должен выглядеть таю
// Сгенерированный файл соответствия; используется в EX21B.HRJ
// команды (ID_* и IDH_*)
HID_WINDOkl 4EW10x18003
HID_WIND0W_NEW20x18004
// подсказки (IDP_*)
// ресурсы (IDR_*)
HIDR STRINGVIEW0x20065
HIDR IIEXVIEW0x20066
HIDR MAINFRAMEQZ20080
HIDR EX21BTYPECb^0081'
// диалоговые окна (IDD_*)
HIDD.AB0UT80X0x20064"
// элементы управления окне-рамки (IDW_«)
Тестирование приложения ЕХ21В
Откройте дочерние окна со строковым и шестнадцатеричным представлением документа. Проверьте, как работает справочная система при нажатии клавиши F1 и сочетания клавиш Shift+Fl, Если справочный файл скомпилирован неправильно, воспользуйтесь инструкцией по модификации справочной системы EX20D на этапе 1 из раздела «Пример создания справки без программирования» данной главы.
ГЛАВА 2 2
Динамически подключаемые библиотеки
Если Вы хотите писать по-настоящему модульное ПО, то Вас наверняка заинтересуют динамически подключаемые библиотеки (Dynamic-link Library, DLL). Может показаться, что программы и так модульпы, раз модульпы С++-классы. Но разница заключается в том, что классы обеспечивают модульность при разработке программы, a DLL — в период ее выполнения. Вместо гигантских ЕХЕ-файлов, которые пришлось бы перестраивать и тестировать при любом, даже самом незначительном, изменении кода, гораздо эффективнее создавать компактные DLL-модули и тестировать их по отдельности. Если, например, выделить в DLL какой-нибудь С++-класс, то после компиляции и компоновки объем модуля вряд ли превысит 12 Кб. В период выполнения клиентские программы смогут очень быстро загружать и подключать ваши DLL В частности, в самой Windows поддержка основных функций строится на применении именно DLL
Сегодня писать DLL стало гораздо легче. В Win32 модель программирования резко упростилась, да и со стороны AppWizard и MFC поддержка DLL существенно расширена и улучшена. В этой главе Бы научитесь писать на C++ DLL-модули, а также клиентские программы, способные их использовать. Вы увидите, как Win32 проецирует DLL на адресные пространства процессов, и узнаете о различиях между обычными DLL MFC-библиотеки (regular DLLs) и DLL-расширениями (extension DLLs). И, конечно же, мы рассмотрим простые примеры DLL всех типов и даже более сложный пример DLL, в которой реализован пользовательский элемент управления (custom control).
Основы DLL
Прежде чем обсуждать поддержку DLL каркасом приложений, надо разобраться в том, как Win32 интегрирует эти модули в процессы. Может быть, стоит даже вернуться к главе 10 и еще раз прочесть о процессах и о виртуальной памяти. Помните, что процесс — это выполняемый экземпляр программы, а программа запускается из ЕХЕ-файла на диске.
В общем, DLL-модуль — это файл надиске (обычно с расширением DLL), который состоит из глобальных данных, откомпилированных функций и ресурсов и который становится частью вашего процесса. Он компилируется так, чтобы его можно было загружать по определенному базовому адресу; при отсутствии конфликтов с другими DLL модуль будет спроецирован на этот же виртуальный адрес в процессе. В DLL содержатся так называемые экспортируемые (exported) функции, которые импортирует (import) клиентская программа (программа, загружающаяОЕЕ).
ПРИМЕЧАНИЕ DLL-модули в Win32 позволяют экспортировать и глобальные переменные.
В Win32 каждый процесс получает свою копию глобальных переменных DLL для чтения и записи. Если Вы хотите, чтобы несколько процессов совместно использовали какой-то участок памяти, надо либо прибегнуть к файлу, проецируемому в память, либо объявить общий раздел данных (shared data section), как описано в книге Джеффри Рихтера (Jeffrey Richter) Advanced Windows (Microsoft Press, 1995) [№7и<й>гг»для профессионалов (M: Русская Редакция, 1997)]. Всякий раз, когда DLL запрашивает память из кучи, эта память выделяется из кучи, принадлежащей клиентскому процессу.’
Согласованиеимпортируемыхэлементовсэкспортируемыми
DLL содержит таблицу экспортируемых функций. Эти функции идентифицируются извне по их символьному имени и (что необязательно) по целому числу — порядковому номеру (ordinal number). В таблице функций хранятся также адреса функций в пределах DLL Впервые загружая DLL, клиентская программа не знает адресов нужных ей функций, зато знает их символьные имена или порядковые номера. Процесс дйнамической компоновки формирует таблицу, связывающую вызовы из клиентской программы с адресами функций в DLL. После модификации DLL собирать заново клиентскую программу не надо, если Вы не изменили имена функций или последовательности их параметров.
ПРИМЕЧАНИЕ В упрощенном мире Вам хватило бы одного ЕХЕ-файла, импортирующего функции из одной или нескольких DLL. Но в реальности многие DLL вызывают функции из других DLL. Таким образом, у каких-то DLL могут быть и экспортируемые, и импортируемые элементы. Но это не проблема, потому что процесс динамической компоновки справляется и с такими ситуациями.
В коде DLL экспортируемые функции надо объявлять явным образом, например:
_declspec (dllexport) int MyFunction(int n);
(Альтернатива — перечислить экспортируемые функции в файле определения модуля (DEF) — гораздо хлопотнее). В клиентской программе необходимо указывать, что данная функция импортируется:
declspec(dllimport) int MyFunction(int n);.
Однако С-н-компилятор сгенерирует для MyFunction так называемое расширенное имя (decorated name), которое нельзя использовать в других языках. Это длинное имя, составляемое компилятором из имен класса и функции, а также типов параметров. Чтобы у функции было простое имя, например MyFunction, объявления функций следует писать таю
extern "С" _declspec(dllexport) int MyFunction(int n); extern "C"declspoc(dllimnort) int MyFunction (int n);
ПРИМЕЧАНИЕ По умолчанию компилятор формирует передачу параметров по правиламcdecl, а это значит, что параметры из стека выталкивает вызывающая программа. Программы на некоторых языках требуют придерживаться правилstdcall (как в Паскале), когда стек очищает вызванная функция. Тогда в объявлении функции, экспортируемой из DLL, понадобится модификатор stdcall.
Одних только объявлений импортируемых элементов недостаточно, чтобы клиент установил связь с DLL В проекте клиентской программы надо определить библиотеку импорта (LIB) для компоновщика, а сама программа должна содержать как минимум один явный вызов какой-то функции, импортируемой из DLL Оператор вызова должен находится на одном из путей выполнения программы.
Явное и неявное связывание
В предыдущем разделе в основном описывалось неявное связывание (implicit linking), которое Вы, как программист на C++, скорее всего, будете применять для своих DLL. Формируя DLL, компоновщик создает дополнительный LIB-файл, содержащий символьные имена всех элементов, экспортируемых из DLL и (необязательно) их порядковые номера, но никакого кода в нем нет. LIB- файл — это суррогат DLL, добавляемый к проекту клиентской программы. Когда собирается клиентская программа, импортируемые символьные имена согласуются с экспортируемыми из LIB, и эти имена (или порядковые номера) сохраняются в БХЕ-файле. LIB-файл содержит, и имя DLL-файла (без указания пути), которое тоже хранится в EXE-файле. При загрузке клиента Windows находит и загружает нужные DLL-модули, азатем динамически связывает их по символьным именам или порядковым номерам.
Явное связывание (explicit linking) больше подходит для интерпретирующих языков вроде Visual Basic, но при необходимости его можно использовать и в C++. При явном связывании файл импорта не используется — вместо этого Вы вызываете ^Чп32-функцию LoadLibrary, указывая полное имя DLL как параметр. LoadLibrary возвращает параметр типа LUNSTANCE, который можно использовать в вызове (SeiProcd^Wres^,преобразующей символьное имя (или порядковый номер) в адрес. Допустим, что DLL экспортирует функцию так:
extern "С"declspec(dllexport) double SquareRoot (double d);
Вог пример явного связывания клиента с экспортируемой функцией:
typedef double (SQRTPROO(dcuble);
HINSTANCEhlnstance;
SORTPROC» pFinction:
VERIFY(hInstance = ::LoadLibrary("c:\\winnt\\system\\mydll.dir')):
VERIFY(pFinction = (SORTPROC»)::GetProcAddress((HMODULE) hlnstance, "SquareRoot")): double d = (*pFunction)(81.0): // вызов функции из DLL
Если при явном связывании можно определить, когда загружать или выгружать DLL, то при неявном связывании все DLL загружаются в .моментзагрузки клиента. Явное связывание позволяет указывать и то, какие DLL следует загрузить. Например, пусть у Вас есть одна DLL со строковыми ресурсами на английском языке, а другая — со строковыми ресурсами на русском. Когда пользователь выберет язык для работы, ваше приложение сможет загрузить соответствующую DLL
Связывание по символьным именам и порядковым номерам
Связывание по порядковым номерам в Wlnl6 было эффективнее и предлагалось по умолчанию. В Win32 связывание по символьным именам усовершенствовано, и теперь Microsoft рекомендует использовать именно его. Однако в DLL-версии библиотеки MFC применяется связывание по порядковым номерам. Дело в том, что типичная программа на базе MFC подключается к сотням функций этой библиотеки, и при связывании по порядковым номерам EXE-файл получается компактнее, поскольку отпадает необходимость хранить длинные символьные имена импортируемых элементов. Если Вы создаете DLL со связыванием по порядковым номерам, определите в DEF-файлв проекта номера, которые не слишком часто исполь- зуютсяв Win32-cpefle.EcnH Вы экспортируете С++-функции, то должны указывать в DEF-файле их расширенные имена (или объявить функции как extern *С#). Вог краткий фрагмент одного из DEF-файлов MFC-библиотеки:
?ReadLlst@CRecentFileList@@UAEXXZ 9 5458 NONAME ?ReadNameDictFromStream©CPropertySection®©QAEHPAUIStream§@©Z f 5459 NONAME ?ReadO&ject^Archive^№PAVCObjecWP8UCRuntimeClass@@@Z 9 5460 NONAME ?ReadString@CArchive@@QAEHAAVCString@@@Z © 5461 NONAME
?ReadString@CArctiive@©QAEPADPAOl@Z © 5462 NONAME
см. след. стр.
14 Зак. №7 87
?ReadString@CInternetFlle@iWAEHAAVC$tring@®©Z © 5463 NONAME ?ReadString@CInternetFile@@(JAEPADPADl@Z © 5464 NONAME
Числа после символов @ — это и есть порядковые номера. Ну что, Вы по-прежнему хотите использовать этот метод?
Точка входа в DLL: функция DHMain
По умолчанию компоновщик предполагает, что основной точкой входа в вашу DLL является функция _DHMainCRTStartup.Windows, загружая DLL, вызывает эту функцию, а та сначала вызывает конструкторы глобальных объектов, потом — глобальную функцию DUMain, которую Вы, естественно, должны были написать. DIIMain вызывается не только при подключении DLL к процессу, но и при отключении от него, а также в ряде других случаев. Взгляните на заготовку функции DUMain:
HINSTANCE g„hlnstance;
.extern "С" int APIENTRY'
DlIMain(RINSTANCE hlnstance, DWORD dwReason, LFVOID IpReserved)
1 if (dwReascn == DLL_PROCESS_ATTACH)>
{
TRACED("EX22A.DLL Initializing!\n"): g_hlnstance - hlnstance;
// здесь реализуйте инициализацию
}
else if (dwReason == DLl._PROCESS_DETACH)
■!
TRACE0CEX21A. DLL Terminating! W): // здесь реализуйте очистку } return 1; // OK
Если Вы не пишете функцию DIIMain для своей DLL, вместо нее подставляется заглушка из стандартной библиотеки периода выполнения.
Функция DIIMain вызывается также при запуске и завершении отдельных потоков, что определяется параметром dwReason. Впрочем, эта тема исчерпывающе изложена в книге Джеффри Рихтера.
Описатели экземпляров и загрузка ресурсов
Каждая DLL в процессе ид ентифицируется уникальным 32-разрядным значением HINSTANCE. Сам процесс тоже имеет описатель HINSTANCERce эти описатели экземпляров действительны только в рамках конкретного процесса и представляют собой начальный виртуальный адрес DLL- или ЕХЕ-модуля. В Win32 значения описателей HINSTANCE и НМООШЕсовпадают, и эти описатели равнозначны. Описатель экземпляра процесса почти всегда равен 0x400000, а у DLL с базовым адресом по умолчанию — 0x10000000. Если ваша программа использует несколько DLL, у них будут разные значения HINSTANCE — либо потому, что в DLL указаны разные базовые адреса (определяется на этапе разработки), либо потому, что загрузчик скопировал и переместил код DLL
Описатели экземпляров особенно важны при загрузке ресурсов. В частности, параметр HINSTANCE необходим ^Чп32-функции FindResource, В EXE- и DLL-модулях могут быть свои ресурсы. Если нужен ресурс из DLL, указывают описатель экземпляра DLL, а если из EXE - описатель экземпляра EXE
Как же получить описатель экземпляра? Вы уже видели, что описатель экземпляра DLL передается как параметр в функцию DUMain, Если Вы хотите выяснить описатель ЕХЕ-модуля, вызовите Win32"фyнкцию GetModuleHandlec параметром NULL. Если же Вам нужен опи-
сатель DLL, вызовите GetModuleHandlec именем DLL в качестве параметра. Позднее мы рассмотрим ещз один метод загрузки ресурсов, реализованный в MFC-библиотеке, основанный на просмотре модулей в определенной последовательности.
Порядок поиска DLL клиентской программой
Если Вы компонуете программу явным образом, с использованием LoadLibrary, то можете указывать полное имя DLL (с определением пути). Если же полный путь не указан, или при неявной компоновке Windows будет искать вашу DLL в следующем порядке.
В каталоге, содержащем данный ЕХЕ-файл.'.
В текущем каталоге процесса.
В системном каталоге Windows.
В каталоге Windows.
В каталогах, прописанных в переменной окружения PATH.
Здесь есть одна ловушка, в которую можно запросто угодить. Вы создаете DLL как отдельный проект, затем копируете DLL-файл в системный каталог Windows и, наконец, загружаете DLL в клиентскую программу. До сих пор все было прекрасно. Но вот Вы обновляете DLL, забываете скопировать ее в системный каталог, и при следующем запуске клиентской программы загружается старая версия DLL Что тут сказать — будьте внимательны!
Отладка DLL
Visual C++ упрощает отладку DLL Просто запустите отладчик в проекте DLL При первом запуске отладчик запросит путь к клиентскому ЕХЕ-файлу. После этого при каждом «запуске» DLL из отладчика тот будет загружать ЕХЕ-файл, но в ЕХЕ-файл заложена своя последовательность поиска DLL-модулей. Это значит, что надо или настроить переменную окружения PATH, или скопировать DLL в каталог, который входит в последовательность поиска.
DLL-расширения и обычные DLL
До сих пор мы рассматривали DLL-модули Win32, в которых есть функция DUMain и какие- то экспортируемые функции. Теперь двинемся в мир, построенный на каркасе MFC-приложений, расширяющем базовую Win3 2-поддержку DLL Арр Wizard позволяет создавать два вида DLL с поддержкой MFC-библиотеки: DLL-расширения (extension DLL) и обычные DLL (regular DLL). Но прежде чем выбирать, надо разобраться в сути предмета.
ПРИМЕЧАНИЕ Конечно, Visual C++ позволяет создавать Win32 DLL без библиотеки MFC — так же, как и программы. Но эта книга посвящена MFC, так что этот вариант мы пропустим,
DLL-расширение поддерживает интерфейс C++. Другими словами, такая DLL может экспортировать целые классы, а клиент может создавать объекты этих классов или разрабатывать производные классы. DLL-расширение динамически связывается с кодом DLL-версии библиотеки MFC, поэтому необходимо, чтобы клиентская программа компоновалась с MFC тоже динамически (АррWizard предлагает это по умолчанию) и чтобы версии динамических библиотек MFC (mfc42.dll, mfcd42.dll и т. д) у клиентской программы и DLL-расширения были одинаковы. DLL-расширения весьма компактны: можно создать простое DLL-расширение размером всего 12 Кб, которое загружается очень быстро.
Если же Вам нужна DLL, которую можно загружать в любую Win32-cpeflynporpaMMHpoBa- ния, включая Visual Basic б.О, значит, Вам нужна обычная DLL Однако здесь есть одно большое «но»: обычная DLL может экспортировать только С-функции и не способна экспортировать С++-классы, функции-члены или переопределенные функции, поскольку в каждом С++- компиляторе используется свой метод расширения имен функций. Правда, внутри самой DLL этого типа можно использовать С++-классы (и классы MFC в том числе).
Создавая обычную DLL, Вы можете выбирать, каким образам связывать ее с MFC-библиотекой; статически или динамически. Если Вы остановитесь на статическом связывании, в DLL будет включена копия нужного кода MFC-библиотеки, и таким образом Вы получите самодостаточный DLL-модуль Средний размер статически связанной DLL без отладочной информации около 144 Кб. Если же Вы предпочтете динамическое связывание, размер уменьшится приблизительно до 17 Кб, но при этом надо будет позаботиться о том, чтобы на компьютере пользователя присутствовали необходимыединамические модули MFC. Это не проблема, если клиентская программа уже динамически связана с той же версией MFC.
Когда Вы сообщаете AppWizard, какой тип DLL или EXE Вам нужен, *<&/т<?-константьдля компилятора устанавливаются так, как показано в таблице.
Динамическое связывание с общей MFC-библиотекой
Статическое связывание* с MFC-библиотекой
Обычная DLL
_AFXDU-,_USRDU.
nSRDLL■
DLL-расширение
_AFXEXT.._AFXDLL
Не поддерживается
Клиентская ЕХЕ-программа
_AFXDLL
Константы не определены
Заглянув в исходный код и заголовочные -MFC-файлы, Вы увидите множество операторов *i/dejдлятих констант. Это значит, что библиотечный код компилируется по-разному, в зависимости от типа создаваемого Вами проекта.
Общие DLL MFC-библиотеки
Если Вы собираете проект для конфигурации Windows Debug, выбрав вариант с общими DLL- модулями MFC-библиотеки, то ваша программа динамически связывается с одной или несколькими (ANSI) библиотеки MFC:
mfc42d.dllmfco42d.dllmfcd42d.dllmfcn42d.dllmfc42d.dllmfco42d.dllmfcd42d.dllmfcn42d.dllважнейшие MFC-классы (ядро);
mfc42d.dll
mfco42d.dll
mfcd42d.dll
mfcn42d.dll
mfc42d.dll
mfco42d.dll
mfcd42d.dll
mfcn42d.dll
ActiveX-(OLE-) классы;
классы для работы с базами данных (ODBC и DAO);
KnaccHWinSock, Winliiet.
При создании конечной версии (конфигурация Release) программа динамически связывается только с mfc42.dll Компоновка с этими DLL MFC осуществляется неявно, через библиотеки импорта. Может быть, Выдумаете, что программа неявно связывается и с DLL, поддерживающими ActiveX и ODBC, но тогда все эти DLL подключались бы к клиентской программе при ее загрузке, независимо от того, используются ли на самом деле в ней средства ODBC или ActiveX. Но это не так. МРСзагружаетОЕЕ-модули поддержки ActiveXn ODBC явно (через LoadLibrar)^ — при первом обращении к одной из их функций. Таким образом, ваша клиентская программа загружает только нужные ей DLL
DLL-расширёния: экспорт классов
Если DLL-расширение должно содержать только экспортируемые С++-классы, то создать и использовать его будет легко. Операции, необходимые для сборки ЕХ22А, покажут Вам, как сообщить AppWizard, что Вам необходимо DLL-расширение. Тенерируемая им заготовка содержит одну лишь функцию DllMain. Вы должны добавить в проект свои классы C++ и дополнить объявления классов макросом АРХ_£ХТ_СЬ455^ак показано ниже:
class AFX_EXT_CLASS CStudent : public CObject'
Это изменение надо внести в Н-файл, являющийся частью DLL проекта, и в аналогичный файл, используемый клиентской программой. Другими словами, Н-файлы для клиента и DLL
Visual C++ Learning Edition не поддерживает статическое связывание.
должны совпадать. Макрос генерирует код в зависимости от ситуации: либо класс экспортируется из DLL, либо импортируется клиентом.
Последовательность поиска ресурсов в DLL-расширении
Если Вы создаете динамически связываемое клиентское приложение на базе MFC, многие стандартные ресурсы MFC-библиотеки (строки сообщений об ошибках, шаблоны диалоговых окон для предварительного просмотра перед печатью и др.) хранятся в DLL-модулях MFC (mfc42.dll, mfco40.dll и т. д.); по и у вашего приложения есть свои ресурсы. Когда Вы вызываете MFC-функцию ироде CStrhi&:La«dStFmgtt;w. CBitmap;:LaadBitmap^.^y~^c приложений ищет сначала ресурсы EXE-файла, а потом ресурсы DLL MFC.
Если же программа включает в себя DLL-расширение, то последовательность поиска такова: сначала ЕХЕ-файл, затем DLL-расширение и, наконец, DLL MFC. При наличии, скажем, уникального идентификатора строкового ресурса MFC-библиотека найдет именно его, а если в ЕХЕ-файле и файле DLL-расширения какие-то идентификаторы строковых ресурсов дублируются, MFC-библиотека загрузит строку из ЕХЕ-файла. Если же ресурс загружает DLL-расширение, то последовательность поиска такова: сначала DLL-расширение, затем DLL MFC и, наконец, ЕХЕ-файл.
При необходимости последовательность поиска можно изменить. Допустим, Вы хотите в ЕХЕ-коде сначала искать ресурсы DLL-расширения. Это можно сделать так:
HINSTANCE hlnstResourcaCHent = A^xGetResnurcellandleO;
// используем описатель экземпляра DLL
AtxSetResburceH.andle(: iGetModjleHandleC'.iydllnanie. all"):
CString stress;
StrRes.LoadString(IDS_MYSTRINS);
// восстанавливает описатель экземпляра клиентской программы
AfxSetResourcoHandlc(hlr.siResojrceCllent):
Использовать s^ckAJxCielhLsiimceHtuidlsaiKcro ;;Се1Мо<?и1еН«гиИ«жльзя, потому что в DLL- расширепии АДч7е#даГяисе/?«пг//аюзвра1цаст описатель экземпляра ЕХЕ-, а не DLL-модуля.
Пример ЕХ22А
В этом примере мы превратимв DLL-расширение класс CPersistemFrama\3 из г л ивы 15. Сначала Вы создадит е файл ex22a.dll, а потом используете его в клиентской программе ЕХ22В. Вот что нужно сделать для создания ЕХ22А.
Используя AppWizard, создайте проект \vcpp32\ex22a\ex22a. Выберите из меню File в Visual C++ команду New, а затем, как обычно, щелкните вкладку Projects. В появившемся окне вместо строки MFC AppWizard (еж) выберите MFC AppWizard (dll):
В этом случае AppWizard откроет только одно диалоговое окно. Выберите вариант создания MFC Extension DLL, какпоказанониже.
Просмотрите файл ех22а.срр. AppWizard генерирует следующий код, включающий функцию DllMain:
И ех22а.срр : Определяет процедуры инициализации для данной DLL.
//.
«include "stdafx. h"
ftinclude <afxdllx.h>
flifdef „DEBUG
«define new DEBUG NEW
«undef THIS_FILE
static char THIS_FILE[] = FILE;
tfendif
static AFX EXTENSION_MODULE Ex22aDLL = { NULL, NULL };
extern "C" int APIENTRY
DllMain(HINSTANCE hlnstance, DWORD dwReason, LPVOID IpReserved) {
// Удалите, если используете IpReserved UNREFERENCED PARAMETERIpReserved);
if (dwReason == DLL_PROCESS_AnACH)
< TRACEO( "EX22A.DLL Initialization! \n");
// однократная инициализация DLL-расширения
if (!AfxInitExtensionModule(Ex22aDLL. hlnstance)) return 0;
// включаем эту DLL в цепочку ресурсов (сгеяердроваяные комментарии удалемн) new CDynLLnklibrary(Ex22aDLL);
else if (dwReason — DLL PROCESS_DETACH)
{"
TRACE0CEX22A.DLL Terminating!\n");
AfxTermExtenslonModule(Ex22aOLL);
)
return 1; //OK
Добавьте в проект класс СРег$<$1еШРгате.Выберите из меню Project пункт Add То Project, а из подменю — Components And Controls. Отыщите файл Persistent Frame.ogx,
созданный ранее в главе 15 (или спишите его с прилагаемого к книге компакт-диска). Чтобы вставить класс в текущий проект, щелкните кнопку Insert
ПРИМЕЧАНИЕ Если Вы не хотите связываться с OGX-компонентом, просто скопируйте файлы Persist.h и Persistcpp в каталог проекта и выберите из меню Project пункт Add То Project, чтобы включить их в проект.
Отредактируйте файл persist, h. Найдите строку:
class CPersistentFrame : public CFramek'nd
и поправьте ее:
class AFX_EXT_CLASS CPersistentFrame : public CFrarneWnd
Соберите проект и скопируйте файл DLL Скопируйте ex22a.dll из каталога \vcpp32\ ex22a\debug в системный каталог (\Windows\Systein или \Winnt\Systcm32).
Пример ЕХ22В
Эту программу мы начнем создавать как клиент для ех22а.<Ш. Она импортирует из DLL класс CPersistentFrame и использует его как базовый класс окна-рамки SDL Впоследствии Выдоба- вите в нее код, который позволит загружать и проверять другие примеры DLL из этой главы. Ваши ействия по созданию ЕХ22В.
Используя Арр Wizard, создайте проект \vcpp32\ex22b\ex22b. Это обычная ЕХЕ-про- граммана базе MFC, Согласитесь с параметрами, предлагаемыми по умолчанию, но выберите Single Document и еще раз убедитесь, что на пятом шаге мастера активизирован переключатель As A Shared DLL
Скопируйте файл persist.h из каталога \vcpp32\ex22a. Обратите внимание, что копировать надо заголовочный, а не СРР-файл.
Замените базовый класс CMahtFratneHa CPersistentFrame. Проведите глобальную замену CFranieWrutea CPersistentFrame как в MainFrm.h, так и в MainFrm.cpp, а также вставьте следующую строку в MainFrriLh:
«include "persist.n"
Добавьте библиотеку импорта ех22а в список входных библиотек компоновщика. Выберите из меню Project пункт Settings. В списке Settings For выберите All Configurations (пар аметрыдля всех конфигураций) и заполните поле ввода Object/Library Modules, как показано на рисунке ниже.
При отсутствии копии файла ех22а.ИЬ в каталоге текущего проекта придется ввести полное имя файла (с указанием пути).
Соберите и протестируйте программу ЕХ22В. Если при запуске программы под отладчиком Windows не найдет cx22a.dll, то немедленно откроется окно с соответствующим сообщением. Если все пройдет благополучно, Вы получите приложение с устойчивым окном-рамкой, которое работает абсолютно также, как и ЕХ15А. Все отличие заключается в том, что код CPersistentFramcBumeaea в DLL-расширение.
Обычные DLL: класс, производный от CWInApp
Когда Арр Wizard генерирует обычную DLL, функция DUMain находится внутри каркаса приложений, а Вы имеете дело с классам, производным от CWinApp.H. с глобальным объектом этого класса — так же, как и в случае EXE-программы. Вы можете перехватить управление, переопределив функции CWinApp::initlnstcmccH. CWinApp:;ExitInstanceRo чаще поступают по- другому: просто пишут С-функции и экспортируют их, указывая модификатор declspec^dll- ехроп)или определяя соответствующие элементы в DEF-файле проекта.
Использование макроса AFX_MANAGE_STATE
Когда в процессе работы программы загружается mfc42.dll, она сохраняет данные в нескольких глобальных переменных. Если MFC-функции вызываются из MFC-программы или DLL- расширения, mfc42.dll знает, как именно установить эти глобальные переменные для вызывающего процесса. Если же к mfc42.dll обратиться из обычной DLL, глобальные переменные не синхронизируются должным образам, и результат будет непредсказуемым. Чтобы решить проблему, следует вставить в начало каждой экспортируемой функции данной DLL строку: AFX_MANAGE_STATE(AfxGetStatlcModuleState());
Если же MFC-код связывается статически, макрос не оказывает никакого эффекта.
Последовательность поиска ресурсов в обычной DLL
Когда EXE связывается с обычной DLL, функции загрузки ресурсов в ЕХЕ-модуле загружают ресурсы самого ЕХЕ-модуля, а функции загрузки ресурсов в ОЕЕзагружаютресурсы этой DLL
Если же требуется, чтобы код ЕХЕ-модуля загружал ресурсы из DLL, можно воспользоваться функцией^эс$е1КекоигсеНан<И9щя временной смены описателя ресурса. Сам код практически не отличается от привед енного в разделе, посвященном поиску ресурсов в DLL-расширениях. Если Вы пишете приложение, требующее локализации, то можете поместить зависящие от языка строки, диалоговые окна, меню и т. п. в обычные DLL (например, в English.dll, German.dll и French.dll). Клиентская программа будет явно загружать нужную DLL и использовать упомянутый выше код для загрузки ресурсов, которые, кстати, должны иметь одинаковые идентификаторы во всех DLL
Пример ЕХ22С.
В этом примере мы создадим обычную DLL, которая экспортирует единственную функцию (вычисляющую квадратный корень). Сначала мы соберем ex22c.dll, а потом изменим клиентскую программу ЕХ22В, чтобы протестировать новую DLL. Ваши действия по созданию ЕХ22С.
Используя Арр Wizard, создайте проект\vcpp32\ex22c\ex22c. Действуйте также, как и в ЕХ22А, но в AppWizard вместо MFC Extension DLL выберите Regular DLL Using Shared MFC DLL
Просмотрите файл ex22c.cpp. AppWizardгенерирует следующий код, включающий производный класс приложения.
// ех22с.срр : Определения процедур инициализации для DLL.
«■include «StdAfx.ii»
«include «ex22c.ii»
ttifdef „DEBUG
«define new DEBUG_NEW
#undef THIS_FlLE.
static char THIS_FILE[] =FILE; .ttendif■
(пропускаем сгенерированные строки комментариев)■
IIIIIIHilllllllllllllllllHlIIIMIIIIIIIIilllllllliHinilliiHIHIIII■ '
// СЕх22сАрр BEGIN MESSAGE MAP(C£x22cApp, CWinAop) //{{AFX_MSG_MAP(CEx22cApp) // ПРИМЕЧАНИЕ: здесь Classwizard добавляет и удаляет макросы-Таблицы сообщений. // НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода! //}}AFX MSG МАР END MESSAGE МАР().
Ш11Ш11ШПШ11ШШГ1ШШ11ШШШ111Ш1ПШШШ^ И Конструктор СЕх22сАрр СЕх22сАрр: :СЕх22сАрр() { // ДОДЕЛАТЬ: добавить сюда код конструктора. // Основную часть инициализации предусмотрите в InifTnstance. } //////////////////////////////////////////////////////////////////////// // единственный объект класса СЕх22сАрр
СЕх22сАрр theApp;
Добавьте код для экспортируемой функции Bx22cSquareRoot. Этот код можно ввести как в файл ех22с.срр, так и в новый файл:
extern "С"declspec(dllexport) double Ex22cSquareRoot(double d)
* TRACE( "Входим в Ex22cSquareRoot\n");
if (d >0,0) { return sqrt(d);
}'
АТхМеззадевох("Нелья извлечь квадратный корень из 0 или отрицательного числа"}’; return 0.0;
} Как видите, никаких проблем с выводом окон сообщений и других модальных диалоговых окон в DLL нет. Не забудьте включить math.h в файл с кодом функции . Ex22cSquareRoot.
Соберите проект и скопируйте файл DLL. Скопируйте ex22c.dll из каталога \vcpp32\ ex22c\Debug в системный каталог.
Коррекция ЕХ22В для проверки ex22c.dll
Когда Вы первый раз собирали программу ЕХ22В, она динамически связывалась с DLL-расширением ЕХ22А. Теперь мы так изменим проект, чтобы неявно скомпоновать ВХ22В с обычной DLL (ЕХ22С) и вызывать из нее функцию квадратного корня.
Вот что нужно сделать для модификации ЕХ22В:'
Добавьте новый диалоговый ресурс и класс в \vcpp32\ex22b\ex22b. С помощью
редактора диалоговых окон создайте шаблон IDD_F.X2 2 С .как показано ниже на рисунке.
Затем, используя ClassWizard, сгенерируйте класс 6Tej,V22^DM/cg,производный от CDiaiog. Элементы управления, переменные-члены и функция таблицы сообщений описаны в следующей таблице.
Идентификатор элемента управления
Тип
Переменная-член
Функция таблицы сообщений
1DCJNPUT
поле ввода
m_dlnput (double)
IDC_OUTPUT
поле ввода
m dOuiput (double)
IBC „COMPUTE
кнопка
OnCompute
Напишите код функции OnCompute,чтобы вызывать экспортируемую функцию ОН- Отредактируйте заготовку, сгенерированную ClassWizardB Test22cDialog.cpp:
void CTest22cDlalog::OnCOmpute()
{ UpdateData(TRUE);
m dOutput = Ex22cSquareRoot(m_dInput);
UpdateData(FALSE);
}
Объявите Ех22с5циагеПоо1как. импортируемую. Для этого добавьте в Test22cDialog.h: extern "С"daclspec(dllimport) double Ex22cSquareRoot(double d);
Интегрируйте класс CTest22cDialogb приложение EX22B. Добавьте меню верхнего уровня Test и команду Ех22с DLL с идентификатором iDJTESTJEX22 СРЫИсполъзуя ClassWizard, соотнесите эту команду с функцией-Членом в классе CEx22tiVtewa напишите код обработчика в Ex22bView.cpp:
void CEx22bView::0nTestEx22cdll()
CTest22cDialog dig;
dig. DoModaK);
И, конечно же, дополните файл Ex22bView.cpp следующей строкой:
«include ''Test22cDialog.il"
Добавьте библиотеку импорта ех22с в список входных библиотек компоновщика. Выберите из меню Proje ct в Visual C++ команду Se ttings и на вкладке link в поле Obje ct/ Library Modules введите \vcpp32\ex22c\dfbug\ex22cjib.^GBbSL элемент от существующего отделите пробелом.) Теперь программа будет неявно компоноваться с EX22A.DLL и EX22C.DLL, Как видите, клиенту, в общем-то, безразлично, какая это DLL: обычная или DLL- расширение. Вы лишь указываете компоновщику имя библиотеки LIB,
Соберите и протестируйте измененное приложение ЕХ22В. Выберите из меню Test команду Ех22с DLL Наберите в поле ввода Input какое-нибудь число и щелкните кнопку Compute Sqrt. Результат должен появиться в элементе управления Output.
DLL с пользовательскими элементами управления
Программисты используют DLL для хранения йсигьзовательскмхэлеэйентовуправления (custom controls) чуть ли не с первых дней существования Windows, потому что такие элементы управления полностью автономны. Раньше их писали на С и конфигурировали как автономные DLL Сегодня можно использовать возможности MFC-библиотеки и упростить программирование за счет применения мастеров. Для пользовательских элементов управления лучше всего подходит обычная DLL, поскольку им не нужен интерфейс C++ и их предполагается использовать в любой системе программирования, воспринимающей такого рода элементы (например, в Borland C++). Кроме того, Вы, наверное, предпочтете вариант динамической компоновки с MFC — в этом случае DLL компактнее и загружается быстрее.
Понятие пользовательского элемента управления
Вы уже познакомились в главе 6 с обычными элементами управления и стандартными элементами управления Windows 95, а в главе 8 — и с ActiveX-элементами . Пользовательский элемент управления аналогичен обычному (вроде поля ввода) в том плане, что тоже посылает родительскому окну уведомляющие сообщения WM_COMMAND и получает сообщения, определяемые пользователем. Редактор диалоговых окон позволяет включать такие элементы управления в шаблоны диалоговых окон. Для этого и предназначена кнопка с изображением головы в окне палитры элементов управления:
Вам предоставляется большая свобода при разработке собственного элемента управления. Вы можете нарисовать в его окне, управляемом клиентским приложением, все, что вздумается, и определить любое уведомление и связанные с ним сообщения. Для соотнесения обычных Windows-сообщений со своим элементом управления (например, WMJLBUTTONDOWN) можно воспользоваться ClassWizard, но обработчики уведомляющих сообщений в классе родительского окна и обработчики пользовательских сообщений придется создавать вручную.
Оконный класс пользовательского элемента управления
Пользовательские элементы управления определяются в шаблоне диалогового ресурса по символьным именам их оконных классов- Не путайте оконный класс Win32 с С++-классом — общее у них только название. Оконный класс определяет структура, содержащая:
имя класса;
указатель на функцию W^dProc, которая принимает сообщения, посланные окнам данного класса;
вспомогательные атрибуты (например, кисть, используемая для фона).
Win32^raKUHRK6g«zenC'/6?ss копирует структуру в память процесса так, что любая функция в данном процессе может создать окно на основе этого класса, После инициализации диалогового окна Windows создает дочерние окна пользовательских элементов управления в соответствии с именами оконных классов, хранящихся в шаблоне.
Теперь допустим, что функция WhdProc элемента управления расположена в DLL При инициализации (вызовом DUMairf) DLL может вызвать для элемента управления функцию Regis terClass. Поскольку DLL являются частью процесса, клиентская программа получает возможность создавать дочерние окна класса пользовательского элемента управления. Клиент знает имя оконного класса элемента управления и он использует это имя при создании до-
чернот окна. Весь код элемента управления, включая WndProc, находится в DLL Вам нужно только, чтобы клиент загружал DLL до создания дочернего окна.
Библиотека MFC и функция WndProc•
Итак, Windows вызывает WndProc элемента управления для каждого сообщения, отправленного в это окно. Но не пишите старомодные операторы sivltcb/case — соотнесите эти сообщения с функциями-членами С++-классон так. как Вы уже делали это ранее. Для этого добавьте в DLL c-н-класс, соответствующий оконному классу элемента управления; затем, используя ClassWizard, создайте обработчики сообщений. Написать С++-класс для элемента управления несложно — просто создайте с помощью ClassWizard новый класс, производный otCWw/. Куда сложнее связать С—н-класс с функцией WndProc и системой распределения сообщений в каркасе приложений. В примере EX22D Вы увидите реальную функцию WndProc, аздесь мы приведем псевдокод типичной функции элемента управления:'
LONG MyCcntrolWndProc(HWND hWnd, UINT message. DINT wPnram; LONG IParam) { if (эго верное сообщение для данного окна’! (’
CWnd' oWnd = new CMyCortrolWindowCla^O;.
свяжите pWnd c hWnd } return AfxCallWndProc(pWnd. hWnd, messaae, wPar-ir IParam);
МРС-функцияЛухСягАРпс/Ргоспередает сообщения каркасу приложений, а тот пересылает их функциям-членам CMyContrulWindowClass.
Уведомляющие сообщения пользовательских элементов управления
Элемент управления общается с родительским окном, посылая ему особые уведомляющие сообщения WM COMMaND с параметрами, показанными в таблице.
Параметр
Назначение
(HIWORD) шРагат (LOWORD) teParam IParam
Код уведомления
Идентификатор дочернего окна
Описатель дочернего окна
Значение кода уведомления зависит от конкретного элемента управления. Родительское окно должно интерпретировать этот код с учетам того, что ему известно о данном элементе управления. Например, код 77 мог бы означать, что пользователь ввел символ при установленном на элементе управления фокусе инода. Элемент управления может отправить уведом ляющее сообщение так:
GetParant ()->GsrdMessage(WM,COMMAND, GetDLyC.rlID() 1 ID_4OTIFYCODE « 15, (LONG) GetSafeHwndO);
На клиентской стороне нужно соотнести это сообщение с функцией-обработчиком при помощи MFC-макроса O/V CONTROL
CN_CONTRO_(ID_NOTIFYCODE, IDC_MYCOHTHOL, OnCLckedMyCoati ol)'
и объявитьфункцию-обработчик:
void OrCllckedMyControl();
Пользовательские сообщения, посылаемые элементу управления
Вы уже сталкивались с пользовательскими сообщениями в главе 7. С их помощью клиентская программа взаимодействует с элементом управления. Поскольку стандартное сообщение
возвращает 32-разрядное значение (если сообщение послано синхронно), в ответ клиент может получать информацию от элемента управления.
Пример EX22D
Это обычная DLL на базе MFC, которая реализует элемент управления «светофор» и переключает его состояние: «выключен», красный, желтый, зеленый. При щелчке светофора левой кнопкой мыши об этом уведомляется его родительское окно, и, кроме того, он реагирует на двапользовательскихсообщения: RYGSETSTATEhRYGGETSTATE. Состояние определяется целым числом, кодирующим цвет. Автор этого элемента управления — Ричард Уилтон (Richard Wilton), который включил его исходную версию на языке С в свою книгу Windows3 Deuelopt-r’s Workshop (Microsoft Press, 1991).
Изначально проект EX22D был сгенерирован AppWizard (компоновка с общими DLL библиотеки MFC) — так же, как и ЕХ22С (см исходный текст на рисунке 22-1; добавленный код в функции Mtlnstance выделен серым фоном). Экспортируемая функция-заглушка Ex22dEnPy агужт исключительно для того, чтобы DLL можно было связывать неявным образом. Клиентская программадолжна вызывать эту функцию, В качестве альтернативы клиентская программа могла бы связывать DLL и явным образом, вызывая blnitlnstance^vn^l-^wiayaioLoadLibraiy.
ЕХ22.СРР
Ц ех22.срр; подпрограммы инициализации DLL.
И
«include "StdAfx.h" ttinclude "ex22c.h" «include "RygWnd.h"
ttifdef .DEBUG
«define new DEBUG NEW.
flundef THIS.FILE
static char THIS_FILE[] = _FILE_;
ttendif
extern "O'-declspec(dllexport) void Ex21dEntry() О // функция-заглушка
(строки сгенерированных комментариев пропущены)
iiiiiiiiiniiiiiiiiMiiiiiiiiHiiiiiiiiiiuiiiiiuniiHiiimiiiiiiriii
// CEx22dApp
BEGIN MESSAGE MAP(CEx22dApp, CWinApp).
//7{AFX_MSG_MA?(CEx22dApp)
// ПРИМЕЧАНИЕ; здесь ClassWizerd добавляет и удаляет макросы таблицы сообщений.
// НЕ ИЗМЕНЯЙТЕ содержимое этих блоков сгенерированного кода!
//DAFX.MSG МАР
END.MESSAGE. МАР()
////////////////////////////////////////////////////////////////////////
// конструктор СБ<220Арр
CEx21dApp::CEx21dApp()
Ц ДОДЕЛАТЬ: добавьте сюда код конструктора.
// Основную часть инициализации предусмотрите в Initlnstance
см. след. стр.см. след. стр.Рис. 22-1. Исхсцц-ый текст программы EX22D
см. след. стр.
см. след. стр.
Рис. 22-1. (продолжение)
HHiiiniiiiHiiiiiiiHinHiniiiiiffiiiinnHHiriiiifiiiiiiiiimH
Ц Единственный объект класса cEx22dApp
CEx22dApp theApp;
BOOL CEx22dApp::lnitlnstance()
CRygWnd::RegisterWndCla8s(AfxGetInstanceHand].e()};
return CWinApp::lnitlnstance();
}
На рисунке 22-2 приведен листинг класса CRygWnd,в том числе глобальной функции RygWndProc.QI^ia создания этого класса щелкните в ClassWizardкнопку Add Class) Кед изменения цвета светофора не очень интересен, поэтому мы сосредоточимся на функциях, общих для большинства пользовательских элементов управления. Статическая функция-член RegisterWndClussupoBCinyi регистрацию окопного класса RYG и должна вызываться сразу после загрузки DLL ОбработчикОя1^и#ог?Оад?гвызываетсяпри щелчке левой кнопкой мьппи в окне элемента управления. Он посылает уведомление о щелчке родительскому окну. Переопределенная функция PostNcDestroyo'KHb важна — она удаляет объект CRygWnd,когда клиентская программа уничтожает окно элемента управления. Функции OnGetState и OnSetState вызываются в ответ на пользовательские сообщения, синхронно отправленные клиентом. И последнее: не забудьте скопировать DLL в системный каталог.
RYGWND.H
ttif!defined!AFX RYGWND.H _1 AA889D5_9 7S8_11D0 BED2 0DC04FC2A0C2_INCLUDED )
«define AFX.RYGWNO.H 1AA88905.9788 11D0.8ED2 00C04FC2A0C2_INCLUDED
«if .MSC.VER > 1000
tfpragmaonce
«endif // .MSC.VER > 1000
// RygWnd.h ; заголовочный файл
//•'
//////////////////////////////////^ // CRygWnd
«define RYG.SETSTATE WM.USER + о
«define RYG.GETSTATE WM.USER + 1
LRESULT CALLBACK AFX.EXPORT
RygWndPrccCHWND- aWnd, UINT message, WEAR AM wPa ram, LPARAM IParaiii);
class CRygWnd : public CWnd
private:
int m.nState; // Оаыключен, 1=красный, 2=желтый, 3=зелен|>й
static CRect s.rect;
static CPoint s_point;
static CRect s.^ColorfS];
static CBrush s_bColor[4);
// Создание
public:
CRygWnd!):
public:
static BOOL RegisterWndClass(HlNSTANCE hlnstance);
Рис. 22-2. Листинг класса CRygWhd
Ц Атрибуты
public:
// Операции public:
//Переопределенные функции
// переопределенные виртуальные функции, сгенерированное ClassWizard //{{AFX VIRTUAL(CRygWnd) protected:
virtual void PostNcOestroyO;
//)}AFX_VIRTUAL
// Реализация
public: 1
virtual 'CRy.nVJnd();
// Сгенерированные функции таблицы сообщений
private:
void SstMappingCCDC» pOC);
void UpdateCclor(CDC- pOC, int n);
protected:
//HAFX,MSG№ycWnd)
afxjnsg void OnPaintQ;
afxjnsg void OnLButtdnDoW'i(UINT nFlags, CPoint point);
afxjnsg LONG QnSetState(LUNT wParam, LONG IParar);
afx msg LONG OnGctState{UINT wParam, LONG IParar);
//}}AFXMSG
DECLARE MESSAGE_MAP()
1Ш111ШШ1Ш111Ш1Ш111Ш1ШШШ1НН1ШШ1Н!11НШ11Ш1
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++добавит дополнительные объявления
И непосредственно перед предыдущей строкой.
fiendif // ! defined(AFX_RYGWND_H_1AA889D5_9788_11D0_BED2_00C04FC2A0C2__INCLLDED_)
RYGWND.CPP
// RygWnd.cpp : файл реализации
//
«include "stdafx.tr
«induce “ex22d.h
«include "RygWnd.h"
tii'dof „DEBUG
fldefine new DEBUG NEW
ttundefTHlS.riLE
static char ""HIS_FILE[] =FILE;
fiendif
LRESULT CALLBACK AFX.EXPORT
RygWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAH IParam) {
CWnd* pWnd;
pWnd = Cl*rd: :FrofflHandlePermanent(hWnd);
if (pWnd== NOLL) {
<Ж след. стр.<Ж след. стр.// предполагаем, что клиент создал окно CRygWnd pWnd = new CRygWndO;
<Ж след. стр.
<Ж след. стр.
Рис. 22-2. (продолжение)
pWnd->Attach(hWnd);
ASSERKpWnd-Xn^hWnd == hWnd);
ASSERT(pWnd == CWnd::FromHandlePermanent(hWnd));
IRESULT IResult = AfxCallWndProc(pWnd, hWnd, message, wParam, IParam); return IResult;
lllllllllilHill! Illi НИ 1НН111111ПН11НН111П1ПН ПИП IHIIIIII
Ц CRygWnd
// статические переменные-члены
CRect CRygWnd: :s_rect(-500, 1000, 500, -1000); // ограничивающий прямоугольник
CPoint CRygWnd: :s point(300, 300); // скругленные углы
CRect CRygWnd: :s rColor[] = {CRect(-250, 800, 250, 300), CRect(-250, 250, 250 -250), CRect(-250, -300, 250, -800)};
CBrush CRygWnd::s_bColor[] = {RGB(192, 192, 192), RGB(0xFF, 0x00, 0x00), RGB(0xFF, OxFF, 0x00), RGB(OxOO, OxFF, 0x00));
BOOL CRygWnd: :RegisterWndClas8(HINSTANCE hlnstance) // статическая функция-член
С
WHDCLASS wc;
wc. IpszClassName = "RYG"; // соответствует имени класса в клиенте
wc.hlnstance = hlnstance;
wc. IpfnWndProc = RygWndProc:.
wc.hCursor = : :LoadCursor(NULL, IDC„ARROW);
wc.hlcon = 0;
wc.lpszMenuName = NULL;
wc.hbrBackground = (HBRUSH) : :GetStockObjset(LTGRAY BRUSH);
wc. style = CS.GLOBALCLASS;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
return ( ; :RegisterClass(&wc) != O);
////////////////////////////////////////////////////////////////////////
CRygWnd::CRygWnd() {
B_nState = 0;
TRACE( "Конструктор CRygWnd\n"); )
CRygWnd::~CRygWnd() {
TRACE("Деструктор CRygWnd\n");
BEGIN MESSAGE MAP(CRygWnd. CWnd)
//{{AFX MSG_MAP(CRygWnd)
ON WM PAINT()
0N_WM"l8UTT0ND0WN()
ON_MESSAGE(RYG SETSTATE, OnSetState)
ON_MESSAGE(RYG_GETSTATE, OnGetState)
//)}AFX MSG MAP■
END MESSAGE^MAP()
void CRygWnd: :SetMapping(CDC* pDC)
CRect clientRect;
GetClientRect(clientRect);
pDC->SetMapMode(MN_ISOTROPIC);
pDC->SetWlndowExt(1O00. 2000);
pDC->SetViewportExt(clientfiect.right. -clientRect.bottom);
pDC->SetViewportOrg(clientRect. right / 2, clientRect. bottom / 2);
void CRygWnd: :UpdateColor(CDC* pOC, int n) I
if (m_nState == n + 1) {
। pDC->SelectObject(&s_bColor[n + 1]);
else {
pDC->SelectObject(&s bColor[0]);
pDC->Ellipse(s_rColor[n]);
//////////////////////////////////////////////////////////////////////// // обработчики сообщений CRygWnd'
void CRygWnd::OnPaint()
{
int i;
CPaintDC dc(this); // контекст устройства для рисования
SetMapping(&dc);'
de.SelectStockObjeot(DKGRAY_BRUSH);
dc.RoundRect(s_rect, s_polnt):
for (i = O; i < 3; i++) {
UpdateColor(&dc, i);
void CRygWnd: :OnLButtonDown(UINT nFlags, CPoint point) {
// код уведомления в HIWORD wParam, в данном случае равен О
GetParent()->SendMesBage(WM COMMAND, GetDlgCtrllDO, (LONG) GetSafeHwndO); // 0
void CRygWrd::PostNcDestroy()
TRACE("CRygWnd: :PostNcDestroy\n");
delete this; Ц CWnd: :PostNcDestroy ничего не делает
ILONG CRygWnd: :OnSetState(UINTwParam, LONG IParam)
TRACE("CRygWnd::SetState, wParam = %d\n", wParam);
m_nState = (int) wParam;
Invalidate(FALSE);
return DL:
LONG CRygWnd: :OnGetState(UIHT wParam, LONG IParam) {
TRACE( "CRygWnd: :GetState\n");
return m_nState;
I
Коррекция ЕХ22В для проверки ex22d.dll
Программа ЕХ22В уже компонуется с DLL-модулями ЕХ22А и ЕХ22С. Теперь мы переработаем проект так, чтобы программу можно было неявно связывать с пользовательским элементом управления EX22D. Вот что надо сделать для модификации ЕХ22В.
Добавьте новый диалоговый ресурс и класс в проект \vcpp32\ex22b\ex22b. С помощью редактора диалоговых окон создайте шаблон IDD_EX22Dc пользовательским элементом управления и идентификатором дочернего окна 1DCJRYG.
Укажите для оконного класса пользовательского элемента управления имя RYG.
Затем с помощью ClassWizard сгенерируйте класс Clbst22dDtalog,производный от CDialog.
Отредактируйте файл Test22dDialog.h. Добавьте закрытую переменную -член:
enun {OFF, RED, YELLOW, GREEN} m_r.State;
Кроме того, объявите импортируемую функцию и идентификаторы пользовательских сообщений:
extern "С” declspec(dllimport) void Ex21dEr~ry(); //функция-заглушка
«define RYG.SETSTATE WMJJSER + О
«define RYG.GETSTATE WMJJSER + 1
Отредактируйте конструктор в Test22dDialog.cpp, чтобы инициализировать переменную-член — флажок состояния. Добавьте код, выделенный полужирным:'
CTest22dDialog: :CTest22c01alog(CWnd« pPa'ent /*=NULL*/>
: CDialog(CTest21dDialog::1DD, pParent)
{
//{{AFX_DATA_INIT(CTest21dDialog)
// ПРИМЕЧАНИЕ: здесь ClassWizard добавит инициализирующий кед
//}}AFX_DATA_INIT
m_nState = OFF;
Ex22dEntry(); // Гарантируем загрузку DLL }
Создайте обработчик уведомляющего сообщения о щелчке элемента управления. Здесь ClassWizard ничем не помажет, придется вручную добавить элемент таблицы сообщений и функцию-обработчик в файл Ibst22dDialog.cpp:
ON_CONTROL(O, IDC_RYG, OnClickedRyg) // код уведомления - 0■
void CTest21dDialag: :OnClickedRyg()
{
Switch (ir_nState) {
case OFF:
m_nState = RED: break;
case RED:'
m_nState = YELLOW;
break;
case YELLOW:
m_nState = GREEN;
break;
case GREEN:
m_nState = OFF;
break;
GetDlgItem(IOC_RYG)->SendMessage(RYG_SETSTATE, r_rSrate); return;
•I
Когда диалоговое окно получает уведомление о щелчке, оно отсылает сообщение RYGSET- STATE обратно элеменгууправления, чтобы тог изменил свой цвет. Не забудьте добавить в файл Text22dDialog.h следующий прототип:
afx_msg void CnClickedRygO;
Интегрируйте класс CTest22dDialo@ приложение ЕХ22В. Вы должны добавить в меню Test вторую команду — Ex22d DLL с идентификаторомЮ_ТЕ$Т_ЕХ22ОО1£й.сп.олъ- зуя ClassWizard, соотнесите эту команду с функцией-членом в классе CEx22bVieiv$i напишите код обработчика в Ex22bView.cpp:
void CEx22bView: :0nTc3tEx22ddll()'
{
CTest22dDialcg cig:
■ dlg.DoModaK);
}
И, конечно, включите в файл Ex22bView,h строку:
«Include "Test22dDialcg.h"
Добавьте библиотеку импорта ex22d в список входных библиотек компоновщика. Выберите из меню Project в Visual C++ команду Settings и на вкладке Links поле Object/ Library Modules введите строку \rcpp32\ex22d\Debug\ex22dJib.Tenepb программа должна неявно связываться со всеми тремя DLL,
Соберите и протестируйте измененную программу ЕХ22В. Выберите из меню Test команду Ex22d DLL Попробуйте пощелкать светофор левой кнопкой мыши — он должен изменять свой цвет.
ГЛАВА 23
IMFC-программыбезклассов «документ» и «вид»
Архитектура «документ-вид» полезна для многих приложений, но иногда достаточно и более простой структуры программы. В этой главе приведены три приложения, основанные: на диалоговом окне и на базе SDI- и MDL интерфей с он. Ни в одном из них не применяются классы «документ», *нид» или «шаблон документа», зато задействованы система распределения команд и ряддр угих средств библиотеки MFC. В Visu al С++ 6.0 можно создавать все три типа приложений, используя AppWizard.
В каждом примере мы посмотрим, как AppWizard генерирует код, который не зависит от архитектуры «документ-вид», и покажем, как можно добавлять в приложение свой код
Пример ЕХ23А
Для многих приложений пользовательский интерфейс можно построить на базе диалогового окна. Это окно появляется сразу же после запуска приложения. Пользователь может свернуть его и, поскольку диалоговое окно не является системным модальным, свободно переключаться на другие программы.
В этом примере диалоговое окно представляет собой простой калькулятор (см. рис. 23-1). ClassWizard определит переменные члены класса и сгенерирует вызовы DDX-функций, отвечающих за обмен данными в диалоговых окнах, — короче говоря, возьмет на себя все, кроме кодирования функции, нужной для вычислений. Диалоговое окно и значок программы определены в файле описания ресурсов сх23я.гс.
Рис. 23-1. Диалоговое окно ЕХ23А Calculator
AppWizard способен генерировать приложения, основанные на диалоговом окне. Этим мы и воспользуемся в примере ЕХ23А. Ваши действия по созданию ЕХ23 А.
]. С помощью Арр Wizard создайте проект \vcpp32\ex23a\ex23a. Выберите в первом диалоговом окне AppWizardпараметр Dialog Based.
В следующем диалоговом окне введите в качестве заголовка ЕХ23А Calculator.
Т. Отредактируйте ресурс IDD_EX23A_DIALOG Л ри этом руководствуйтесь рисунком 23-1. Чтобы назначить идентификаторы элементам управления, используйте редактор диалоговых окоп.
Элемент управления
Идентификатор
Поле ввода левого операнда
Поле ввода правого операнда
Поле ввода для показа результата
Первый переключатель (с группированием)
Кнопка Compute
IDC_LEFT
IDCRiGHT
IDCRESL4.T IDCOPERATION IDC_COMPUTE
Откройте окно свойств для диалоговых окон и щелкните ярлычок Styles. Установите свойства System Menu и Minimize Box.
Используя ClassWizard, добавьте переменные-члены и обработчик команды. Арр - Wizard уже сгенерировал класс CEx22aDlg. Дополните его переменными-членами.
ИдентификаторПеременные-*члены
Тил
IDC_LEFTindLefl
IDCJllGHTmdRigbt
IBC _RESDLTm_dRest<lt
IDC_OPERATJONTm«Operation
double'
double
double ini
Добавьте обработчик сообщений OnComputeдля кнопки 1DCCOMPUTE.
Напишите функцию-член OnCompute» файле ex22aDlg.cpp. Введите код, выделенный полужирным:'.
void CEx23aDlg: OrComautcO
UpdateData(TRUE):.
switch (m_nOperatlon) {.
■case 0: И сложить
m_dResult = m.dLeft + m_dflight; break;
см. след. стр.
// вычесть
m_dResult = m_dLeft - m_dRight; break;
// умножить'
m_dResult = m_dLeft * m_dRight; break;
// разделить if (m_dRight != 0.0) { m dResult = B_dLeft / m dRight; 1 :i else {
AfxMessage8ox("Деление на ноль"); m dResult = 0.0;
>
break;
default:
TRACE("дефолт; m nOperation = %d\n“, m_nOperation);
UpdateData(FALSE);,
}
Соберите и протестируйте приложение ЕХ23А. Обратите внимание: значок программы появляется па панели задач Windows 95- Убедитесь, что диалоговое окно можно свернуть.
Функция Initlnstance класса приложения
Критический элемент приложения ЕХ23А — функция СЕх23аАрр;;1пгиш1апсвц.тенерирован- ная AppWizard. Обычная функция Initlnstance создает основное окно-рамку и возвращает TRUE, после чего запускается цикл обработки сообщений. Но ее версия в ЕХ23А конструирует объект модального диалогового окна, вызывает DoModalи возвращает FALSE. Это означает, что приложение завершается, как только пользователь закрывает диалоговое окно. Функция DoModal позволяет Windows-процедуре диалогового окна получать и распределять сообщения как обычно. Обратите внимание: AppWizard не генерирует вызов CWinApp::Set- RegistryKey.’
Взгляните на сгенерированный код Initlnstance из ех23а срр:
BOOL СЕх23аАрр::lnitlrstance()
1 AlxEnaOleControlContainarO;
И Стандартная инициализация. Если Вы не используете какие-то возможности и хотите // уменьшить размер исполняемой программы, удалите ненужные процедуры инициализации. :#lfdcf _AFXDLL
Enable3cControls(); // вызывается при использовании MFC в общей DLL
ffelse
Enable3dControlsStatic();// вызывается при статической компоновке MFC
ttendif
CEx23aDlg dig;
m_pMainWnd = &dlg;•
int nResponse = cig. DoModaK);
if (nResponse == TOOK) {
H ДОДЕЛАТЬ: поместите седа код обработки закрытия диалогового окна кнопкой 0К } else if (nResponse - IDCANCEL) ' {.
И ДОДЕЛАТЬ: поместите седа код обработки закрытия диалогового окна кнопкой Cancel }
// так как диалоговогое окно завершено, возвращаем FALSE, чтобы выйти из // приложения, а не начать обработку сообщений
return FALSE;
Классдиалоговогоокнаизначокпрограммы
В сгенерированном классе СЕ^ЗдР/^содержатся два элемента таблицы сообщений:
ON WM PAINTO
ON_WM_OUERYDRAGICON()
Эти функции-обработчики отвечают за отображение значка программы при сворачивании ее окна. Они нужны только в Windows NT 3-51, в которой значки свернутых программ находятся на рабочем столе, и не требуются ни в Windows 98, ни в Windows NT 4.0, поскольку в них значки свернутых программ размещаются на панели задач.
Однако кое-какой код для значков все равно нужен. Он находится в сгенерированном AppWizard обработчике сообщения WM_INITDIALOG. Обратите внимание на два вызова Seticon. AppWizard формирует код для добавленя в системное меню команды About (если Вы установили флажек About box). Переменная m_blcon — переменная-член класса диалогового окна, инициализируемый в конструкторе.
BOOL CEx23aDlg::OnInltDialog(} <
CDialog: :OnI-iitDialog();
// добавляем команду "About..." в системное меню
// значение IDM ABOUTBOX должно укладываться в диапазон, вьщеленный системным командам ASSERTION ABOUTBOX & OxFFFO) == WM ABOUTBOX);
ASSERT(IDM_ABOUTBOX < OxFOOO);•
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (! strAboutMenu. IsEmptyO)
<
CString strAboutMenu;
strAboutMenu.LoadString (IDS.ABOUTBOX);
if {! strAboutMenu. IsEmpty()) {
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF STRING, IDM ABOUTBOX. strAboutMenu);
1
// Установить значок для этого диалогового окна. Каркас приложений делает это сам,
// когда основное окно приложения не является диалоговым окном.
Setlcon(m_hlcon, TRUE); // установить крупный значок
Setlcon(m_hlcon, FALSE); // установить мелкий значок
// ДОДЕЛАТЬ: проведите здесь дополнительную инициализацию
return TRUE; // возвращаем TRUE, если фокус не установлен на какой-то элемент управления }
Пример ЕХ23В
Эта SDI-программа из серии «Hello, world!» построена на основе кода из главы 2. У нее только одно окно — объект класса, производного or CFrameWnctonepaMKH. отрисовки осуществляются в окне-рамке, там же обрабатываются и все сообщения.
Используя AppWizard, создайте проект \vcpp32\ex23b. На первом шаге AppWizard выберите Single Document и откажитесь отопции Document/View Architecture Support, как показано на рисунке ниже.
Добавьте код отрисовки диалогового окна. В функцию CCbildView::OnPaintB файле Child View.cpp добавьте код, выделенный полужирным:
void CChildView::OnPaint() {
CPaintDC dc(thi$): // контекст устройства для отрисовки
dc.TextOut(O. О, "Hello, world!");
// Не вызывайте CWnd::OnPalnt() в ответ на сообщения отрисовки }
Скомпилируйте и запустите приложение. Мы получили полноценное SDI-приложение, которое никоим образом не зависит от архитектуры «документ-вид».
AppWizard автоматически удаляет из приложения все следы этой зависимости и задействует следующие элементы.
I* Основное меню. Windows-приложение может обойтись без меню и даже без описания ресурсов. Но в примере ЕХ23В есть и то и другое. Каркас приложений переправляет команды меню обработчикам сообщений в классе окна-рамки.
|* Значок. Полезен, если программу будут запускать из Microsoft Windows Explorer или сворачивать ее основное окно-рамку. Значок, как и меню, хранится в ресурсе.
|* Обработчик командного сообщения о закрытии окна. Многие программы должны проделывать ряд особых операций в тот момент, когда закрывается их основное окно. Если бы Вы использовали документы, то могли бы переопределить функцию CDocument::Save- Modijied.Ho в данном случае, чтобы контролировать процесс закрытия, надо написать обработчики сообщений о закрытии окна, посылаемых программе в результате действий пользователя или самой Windows при завершении ее работы.
Панель инструментов и строка состояния. AppWizard автоматически генерируетпа- нель инструментов и строку состояния и устанавливает распределение сообщений, хотя классы «документ» и «вид» здесь отсутствуют.
У SDI-приложений без поддержки архитектуры «документ-вид» имеется несколько интересных характерных черт.
Класс CCbildView— вопреки своему названию этот класс, на самом деле, наследуетCWnd и объявляется в ChildView.h, а реализован в ChildView.cpp. Этот класс реализует только виртуальную функцию-член OnPaint, которая содержит весь код для рисования в окне- рамке всего, что Вы хотите там нарисовать. (Это иллюстрирует этап 2 в примере ЕХ23В.)
Класс CMainFrame — этот класс содержит переменную-член mjvndView, которая создав ется и инициализируется в функции CMainFrame.-OnCreate.
Функция CMainFrame: ;OttSetFocus— данная функция передает фокус окну CCbtidVieuK void CMainFrame: :OnSelFocns(CWnd* pOldWno)
{
// передаем фокус дочернему окну m wndView.SetFocus();
I
Функция CMainFrame: :OnCmdMsg- эта функция дает шанс дочернему окну первым обработать любые командные сообщения:
BOOL CMainFrame::OnCmdMsg(UINT nIO, int nCodo. void* pExtra, AFX_CMDHANDLERINFO- pHandlerlnfo)
// даем дочернему окну возможность обработать команду первым
if (m_wndView.OnCmdMsgtnlO, nCode, pExtra. pHandlerlnfo)) ireturn TRUE;
// иначе производим обработку по умолчанию
return CFrameWnd::OnCmdMsg(nIO. nCode, pExtra, pHandlerlnfo);
Пример EX23C
Приступим к созданию MDI-программы, не использующей архитектуру «документувид».
С помощью AppWizard создайте проект \vcpp32\ex23c. На первом шаге AppWizard выберите Multiple Document и откажитесь от Documcnt/VicwArchitecture Support, как показано на рисунке ниже.
Добавьте код отрисовки дочернего окна. В функцию CCbildView-OnPaintB файле ChildView.cpp добавьте код, выделенный полужирным:
void CChildView::0пРа1пт(>
CPaintDC dc(this); // контекст устройства для отрисовки
dc.TextOut(O, 0, "Hello, world!");
// Не вызывайте CWnd::OnPaint() в ответ на сообщения отрисовки
}
Скомпилируйте и запустите приложение. Мы получили полноценное MDI-приложение, которое никоим образам не зависит от архитектуры «документ-вид».
Как и в ЕХ23В, в этом примере автоматически создается класс CGbildView. Основное различие между ЕХ23В и ЕХ23С состоит в том, что класс CChildVietv' «яда&ют в функции CCbild- Frame-OnCreate, а не в классе CMainFrame.
Итак, в этой главе Вы научились создавать приложения трех типов, которые не зависят от архитектуры «документ-вид». Изучение того, как создаются эти приложения, — прекрасный способ понять работу MFC. Мы советуем Вам сравнить приложения с поддержкой и без поддержки этой архитектуры, чтобы получить исчерпывающее представление о том, как классы «документ» и «вид» работают с остальной частью MFC.
1 Точнее объект этого класса. — Прим, персе.
ЧАСТЬ IV
СОМ, Автоматизация и OLE
ГЛАВА 24
Модель многокомпонентных объектов
Модель многокомпонентных объектов (Component Object Model, СОМ) лежите основе большей части новой технологии Microsoft — ActiveX, а после пяти лет существования она стала неотъемлемой частью Microsoft Windows, и поэтому рассказ о ней — обязательная часть этой книги. Недалек тот день, когда программирование для Windows станет немыслимо без СОМ. так что изучайте ее прямо сейчас. С чего же начать? Может быть, с MFC-классов для элементов управления ActiveX, Автоматизации и OLE? Но эти классы, как бы полезны они ни были, скрывают истинную архитектуру СОМ. Значит, начинать надо с фундаментальной теории, У она включает в себя собственно СОМ и так называемые интерфейсы (interfaces).
Перед Вами первая из семи глав, составляющих четвертую часть этой книги. В ней Вы получите теоретические сведения, необходимые для усвоения материала следующих шести глав. Вы узнаете об интерфейсах и о том, как библиотека MFC реализует их через свои макросы и таблицы интерфейсов (interface maps).
Основы технологи и ActiveX
Терминология меняется столь же стремительно, как и технология, и даже не все группы внутри Microsoft соглашаются, как пользоваться терминами ActiveX и OLE. Считайте, что ActiveX - это нечто, возникшее при столкновении «старого» OLE и Интернета. ActiveX включает в себя не только те возможности Windows, основанные на СОМ, которые мы рассмотрим именно и этой части книги, но и семейство Microsoft Internet Information Server и программный интерфейс Winlnet (рассмотрен в шестой части).
Да, OLE по-прежнему существует, но теперь вновь означает Object Linking and Embedding (связывание и внедрение объектов), как и в дни OLE 1. Сейчас это просто еще одно подмножество технологии ActiveX, включающее всякую всячину, например, операцию drag-and-drop (перетащить и отпустить). К сожалению (пли к счастью, если у Вас есть ранее написанный код) исходный MFC-код и Windows API не следуют за последними изменениями терминологии. Поэтому в названиях функций и классов можно увидеть множество упоминаний «OLE> и «О1е», хотя некоторые из этих функций выходят за рамки связывания и внедрения. В этой части книги в коде, сгенерированном AppWizard, Вы мажете заметить упоминания о «сервере». Теперь Microsoft сохраняют этот термин только за серверами баз данных и серверами Интернета. Для серверов OLE введен новый термин — «компонент».
Компьютерные секции книжных магазинов полны книг по OLE, СОМ и ActiveX. Мы не обещаем достигнуть в этой книге того же уровня детализации, но Вы наверняка получите хорошее представление о теории СОМ. Мы уделим большее, чем в других книгах [за исключением мА?Л«егпа& (Addison-Wesley, 1996) Джорджа Шеферда и Скотта Уингоу (George Shepherd и Scot Wingo)], внимание связи СОМ с классами библиотеки MFC. Это послужитхоро-
шей подготовкой к штудированию серьезных книг по ActiveX/СОМ, включая Inside OLE (Microsoft Press, 1995) Крейга Брокшмидта (Kraig Brockschmidt) и Essential OOM (Addison-Wesley, 1998) Дона Бокса (Don Box). Хорошая книга средней трудности — Inside COM (Microsoft Press, 1997) Дейла Роджерсона (Dale Rogerson).
И еще одно замечание: не думайте, что путь будет легким. Крейг Брокшмидт говорил, что перед тем, как он начал понимать эти концепции, у него был «шесть месяцев туман в голове». Минимальная предпосылка — исчерпывающее знание языка C++. Не бойтесь копаться в чужом коде и писать свой. Убедитесь, что умеете делать простые вещи, перед тем как приступите к более сложным — многопоточности в СОМ, специализированному (custom) маршалингу и распределенной COM (distributed COM, DCOM).
Модель многокомпонентныхобъектов
СОМ — это «индустриальный стандарт» программной архитектуры, поддерживаемый Microsoft, Digital Equipment Corporation и многими другими компаниями. Это без сомнения только стандарт. Есть другие стандарты, такие как Corba от Open Software Foundation (OSF), но, по нашему мнению, СОМ является ведущим стандартом.
Проблема, которую решает СОМ
Проблема заключается в том, что в Windows нет стандартного способа взаимодействия между программными модулями. «Но, — скажете Вы, — акакже DLL с их экспортируемыми функциями, DDE, буфер обмена и сами API Windows, не говоря уже об устаревших стандартах вроде VBX и OLE 1? Разве этого мало?» Увы, да. Вы не построите объекто-ориентированную операционную систему будущего из этих разрозненных и предназначенных каждый для своей цели стандартов. А на базе модели СОМ это возможно.
Сущность СОМ
У старых стандартов много недостатков. У Windows API слишком велика «площадь поверхности» — свыше 350 функций; VBX-расширения не работают в мире 32-разрядныхпрограмм; в DDE чрезмерно запуганная система приложений (applications), тем (topics) и элементов (items); обращение к DLL всецело зависит от конкретного приложения. Модель СОМ предоставляет унифицированный, открытый, объектно-ориентированный протокол связи между программами, который уже сейчас поддерживает:.
стандартный, не зависящий от языка программирования способ загрузки ивы зова Win32- модулей D LL клиентскими Wi п 3 2-i i рограм м а м и;
универсальный способ управления однойЕХЕ-программы другой, выполняемой на том же компьютере (замена DDE);.
замену элементовуправления VBX элементамиуправления ActiveX;
новый мощный способ взаимодействия прикладных программ с операционной системой;
расширения для поддержки новых протоколов вроде интерфейса баз данных OLE DB;
распределенную COM (DCOM), которая позволяет одной программе взаимодействовать с другой программой, выполняющейся на другом компьютере, даже если процессоры этих компьютеров принадлежат к разным семействам.
Итак, что же такое СОМ? Задать вопрос намного проще, чем ответить на него. Главная линия, проводимая в DevelopMentor (система обучения разработчиков), — это «СОМ есть любовь*. Иными словами, СОМ — это мощная интеграционная технология, которая позволяет собрать разрозненные части программного обеспечения вместе в период выполнения. СОМ позволяет разработчику писать интегрируемое программное обеспечение, не вдаваясь в тонкости многопоточности и не привязываясь к конкретному языку программирования.
СОМ — это протекал, который соединяет один программный модуль с другим, а затем покидает сцену. После того как связь установлена, модули могут взаимодействовать через механизм, называемый интерфейсом (interface). Интерфейсы не требуют статического или динамического связывания точек входа или «зашитых» в программу адресов, за исключени- емнесколькихуниверсальныхСОМ-функций,активизирующихпроцессустановлениясвязи. Интерфейс (точнее, СОМ-интерфейс) — это термин, с которым Вы встретитесь еще не раз.
СОМ-интерфейс
Перед тем как погрузиться в изучение интерфейсов, давайте вспомним суть наследования и полиморфизма в обычном C++. Для этого воспользуемся моделью межпланетных перелетов, которая подходит как для NASA, так и для Nintendo. Вообразите космический корабль, летящий в солнечной системе под влиянием гравитации Солнца. При обычном программировании на C++ Вы могли бы объявить класс CSpaceship и написать конструктор, который задает начальные координаты и ускорение космического корабля. Затем Вы написали бы невиртуальную функцию-член Ну, которая вычисляла бы на основе законов Кеплера новые координаты корабля через определенный промежуток времени, скажем, каждые 0,1 секунды. Можно было бы написать еще и функцию Display, чтобы изображать в окне космический корабль. Самая интересная особенность класса CSpaceship — это то, что интерфейс класса C++ (т. е. протокол взаимодействия класса с клиентом) и его реализация связаны между собой очень тесно. Одна из основных целей СОМ как раз и состоит в там, чтобы отделить интерфейс класса от его реализации.
Если использовать в этом примере СОМ, то код, моделирующий космический корабль, будет размещен в отдельном EXE- или DLL-файле (компоненте), который является СОМ-мо- дулем. Программа-клиентне сможет вызывать Ну или конструктор СЗрос^й^иапрямую, т. к. доступ к объекту, описывающему космический корабль, можно получить только через стандартную глобальную функцию, предоставляемую СОМ, а затем клиент и объект общаются посредством интерфейсов. Прежде чем взяться за настоящую СОМ, попробуем создать ее «модель», в которой и компонент и клиент статически скомпонованы в единый EXE-файл, Вместо упомянутой стандартной глобальной функции мы придумаем функцию GetClassObject.
ПРИМЕЧАНИЕ Если Вы хотите найти аналогии с работой MFC, можете рассмотреть CRuntimeClass, который служит объектом класса для классов, производных от CObject. Объект класса — это метакласс (как по смыслу, так и по форме).
В нашей модели клиенты будут пользоваться этой глобальной абстрактной функцией (GetClassObjecfys^a объектов конкретного класса. В реальной СОМ клиенты вначале получают объект класса, а затем запрашивают у него создание реального объекта — во многом тем же способом, каким MFC осуществляет динамическое создание. У GeClassObject есть три следующих параметра:
BOOL GetClassOaject(int nCIsid, int nlid, void** povObj);
Первый параметр — nCIsid — это 32-разрядное целое число, которое однозначно идентифицирует класс CSpacesbip. Второй параметр, — nlid— уникальный идентификатор нужного нам интерфейса. Третий параметр — указатель на интерфейс объекта. Вспомните, что мы собираемся теперь иметь дело с интерфейсами, а это не то же самое, что классы. Как выясняется, класс может иметь несколько интерфейсов, поэтому два последних параметра и обеспечивают выбор интерфейса. При благополучном завершении функция возвращает TRUE.
Теперь вернемся к разработке CSpacesbip. Мы еще не говорили о его интерфейсах. СОМ- интерфейс — это базовый класс C++ (точнее, struct), который объявляет группу чисто виртуальных функций. Эти функции полностью управляюттой или иной стороной поведения производного класса. Для CSpaceship мы напишем интерфейс IMotion, который будет управлять позицией объекта — космического корабля. Простоты ради объявим только две функции: Ну
и GetPosition, и пусть позиция определяется единственным целым числом. Fly перемещает космический корабль, a GetPosition возвращает ссылку на его текущую позицию. Сделаем объявления:
structIMotion {
virtual void Fly() = 0;
virtual inti GetPosition(> = 0;'
};
class CSpaceship : puolic motion < protected:
int m_nPosition;
public:
CSpaceshipO { m_nPosition = 0; }
void Fly();
inti GetPositionf) { return m_nPosition; } };
Код функций, относящихся к космическому кораблю, в том числе GetClassObject,находится в компонентной части программы. Клиентская часть вызывает GetClassObject,чтобы создать объект — космический корабль и получить указатель на IMotion. При компиляции объявление IMotion доступно обеим частям программы. Вот как клиент вызывает GetClassObject: IMotion* pMct;
GetClassObject(CLSID_CSpaceshio, ПОЧМоИоп, (void **) ipMot):
Допустим пока, что COM с помощью уникальных идентификаторов CLSlD_CSpacesbiptn иО_1Мо1юшож.ет создать космический корабль, а не какой-то другой объект. Если вызов был успешным,/>Мэ/указывает на объект CSpaceship,каким-то образом сконструированный функцией GetClassObject. Класс CSpaceship реализует функции Fly и GetPosition, иозаоалу основная программа может вызывать их для конкретного объекта — космического корабля: int nPos = 50; pMct->GetPosition() = nPos;
pMot->Fly(>;
nPos = pMot->GetPosition();
TRACE("новая позиция = %d\n", nPos);
Итак, корабль стартовал и находится в полете, мыже получили полный контроль над ним через указательpMot. Заметьте:pMot формально не является указателем на объект CSpaceship, но в данном случае указатели на CSpaceship и н; IMotion одинаковы, так как CSpaceship производен от IMotion. Здесь хорошо видно, как работают виртуальные функции — классический полиморфизм C++.
Давайте немного усложним задачу, добавив второй интерфейс, IVisual, отвечающий за визуальное представление космического корабля. Для пего достаточно одной функции: Display. Законченный базовый класс выглядит так:
struct IVisual
' virtual void DisplayO = 0;
Вы заметили, что СОМ требует объединения функций в группы? (Наверное, Вы не ожидали этого.) Но зачем? В этой модели космического пространства Вам, вероятно, понадобятся и другие типы объектов — не только космические корабли. Представьте, что интерфейсы IMotion и IVisual используются другими классами. Возможно, класс CSun реализует интерфейс IVisual, но не имеет реализации для Motion, а класс CSpaceStation предоставляет, кроме этих двух, еще и дополнительные интерфейсы. Если Вы «опубликуете* свои интерфейсы Motion и
IVisual, то не исключено, что их возьмут на вооружение другие компании, занимающиеся моделированием космического пространства.
Рассматривайте интерфейс как своего рода соглашение, контракт, заключаемый между двумя программными модулями. Идея состоит в том, что объявления интерфейса никогда не меняются. Если понадобится обновить код, моделирующий космический корабль, интерфейсы IMotion и IVisual останутся неизменны — Вы просто добавите новый интерфейс, скажем, ICrew. При этом существующие клиентские программы продолжат работать со старыми интерфейсами, а новые — смогут использовать и интерфейс ICrew. Такие клиенты способны ■определить в период выполнения, какие интерфейсы поддерживает данная версия программного обеспечения, моделирующего космический корабль.
Функцию GetClassObjectMOXHO рассматривать как более мощную альтернативу конструктору C++. Посредством обычного конструктора Вы получаете один объект с единственным набором функций-членов, а за счет GeiClassObject можно получить объект с набором выбранных интерфейсов. Чуть позже Вы увидите, что начинают с одного интерфейса, а потом через него узнают о других интерфейсах того же объекта.
Так как же запрограммировать два интерфейса для CSpaceship? Можно было бы использовать множественное наследование, но это не будет работать, если два интерфейса включают функции с совпадающими именами. Вместо этого в библиотеке MFC применяются вложенные классы (nested classes). Не все программисты на C++ хорошо знакомы с этим приемом, поэтому дадим кое-какие пояснения. Вот первый фрагмент кода для класса CSpaceship,ъ котором используются вложенные интерфейсы:
class CSpaceship
protected:
int m_nPosltion;
int m_nAcceleration;
int m_nColor;
public:
CSpaceship()
{ m_nPosition = m_nAcceleration = m_nColor = 0; }
class XMotion : public IMotion
public:
XMotionO { }
virtual void Fly();
virtual intS GetPosition();
} m_xMotion;
class XVisual : public IVisual
<
public:
XVisualO { }
virtual void DisplayO;
} m_xVisual;
friend class XVisual;
friend classXMotion;
ЛРИМЕЧАМИE Возможно, имеет смысл сделать m_nAcceleration переменной-членом класса XMotion, am_nCoior — класса XVisual. Мы же объявили их в классе CSpaceship, поскольку такая стратегия лучше совместима с MFC-макросами, в чем Вы позже и убедитесь.
Заметьте: реализации IMotion и IVisual содержатся внутри родительского класса CSpaceship. В СОМ такой родительский класс известен как класс с объектной идентичностью (object identity). Заметьте также, что tn xMotion и т xVisualsEnsnsyica внедренными переменными-
членами этого класса. Можно было бы реализовать CSpaceshipисключительно за счет внедрения. Но вложение классов дает два преимущества: во-первых, функции-члены вложенного класса могут получить доступ к переменным-членам родительского класса без дополнительных указателей на CSpaceship, а, во-вторых, вложенные классы упакованы в родительском и невидимы за его пределами. Взгляните на код функции-члена GetPosidon;
int& CSpaceship::XMotion::GetPosition() {
METHOD_PB0LOGUE(CSpaceship, Motion) Ц создает pThis
return pThis->m_nPosition;
}"
Обратите внимание; оператор области видимости, употреблен дважды, что необходимо для функций-членов вложенных классов. METHOD _PROLOGUE— это MFC-макрос, который с помощью стандартного оператора языка С о^вйтёнерируетрЗТЬй — указатель /№для родительского класса. Компилятору всегда известно смешение от начала данных родительского класса до начала данных вложенного класса. Таким образом, функция GetPostiion может получить доступ к переменой-члену m_nPositionкласса CSpaceship.
Теперь предположим, что у нас есть два указателя рМо$ирУ&для какого-то объекта CSpace- sbip. (Отложим пока вопрос о том, как получить эти указатели.) Функции-члены интерфейсов можно вызывать следующим образам:
pHot->Fly();
pVis->Display():
Что же здесь происходит? В C++ каждый класс (по крайней мере, класс, у кот орого есть виртуальные функции и который не является абстрактным базовым классом) имеет таблицу виртуальных функций (viable). В нашем примере это означает, что такие таблицы есть у классов CSpacesbip.XVisua! и CSpaceship: .XMotion. Для каждого объекта существует указатель на его данные, первый элемент которых — указатель на таблицу виртуальных функций класса. Структура указателей такова:
объект CSpaceship
ПРИМЕЧАНИЕ Теоретически COM-программы можно писать и на языке С. Взглянув на заголовочные файлы Windows, Вы увидите код, аналогичный приведенному ниже:
flifdef _cplusplus
И объявления для C++ «else
/* объявления для С*/
#endif
В C++- интерфейсы объявляются как struct, часто с наследованием, а в С — как typedef struct без наследования. В первом случае таблицы виртуальных функций ваших производных классов генерируют-
ся компилятором автоматически, тогда как при работе на С таблицы надо заполнять вручную, что весьма утомительно. Однако важно понимать, что ни в одном из языков в объявлениях интерфейсов нет ни переменных-членов, ни конструкторов, ни деструкторов. Поэтому не полагайтесь на то, что интерфейс обладает виртуальным деструктором. (Впрочем, это не проблема — ведь деструктор для интерфейса никогда не вызывается.)
'Интерфейс IUnknown и функция-член Queryinterface •
Давайте вернемся к тому, как получить указатель на интерфейс. Для этого в СОМ определен специальный интерфейс IUnknown. Фактически все интерфейсы производны от IUnknown, который содержит чисто виртуальную функцию-член £г^ф/я/е!^?сврозвращающую указатель на интерфейс по переданному ей идентификатору интерфейса. Эго предполагает, что у клиента есть указатель на какой-то интерфейс; либо на IUnknown, либо на производный от него. Вот новая иерархия интерфейсов, на вершине которой находится IUnknown:
struct IUnknown {
virtual BOOL Ouerylnterlace(int nlid, void" ppvObj) = 0;
struct IMotlon : public IUnknown i
virtual void Fly() =0;
virtual int& GetPosit.ior() = 0;
struct IVisual : public IUnknown
virtual void Display() - 0;
};
Чтобы выполнить требования компилятора, мы должны теперь добавить реализацию Queryinterface как в CSpacesbipoXMotibnttaK. и в CSpacesbip::XVisual.KaK же теперь выглядят таблицы виртуальных функций? Для каждого производного класса компилятор создал таблицу, в начале которой расположены указатели на функции базового класса:
viable класса CSpaceship::XMotion^указатель" на функцию P^jfh^^li" ' < указатель на фун|Цй|Ь |^^4 'указатель йа фунсцио бд^6$й£ flГ!. U I'lvTf#^^viable класса CSpaceship::XMotion^указатель" на функцию P^jfh^^li" ' < указатель на фун|Цй|Ь |^^4 'указатель йа фунсцио бд^6$й£ flГ!. U I'lvTf#^^viable класса CSpaoeship::XVisual-указатель на функциюjif!; указатель на*функцйюО^®^^viable класса CSpaoeship::XVisual-указатель на функциюjif!; указатель на*функцйюО^®^^
viable класса CSpaceship::XMotion
^указатель" на функцию P^jfh^^li" ' < указатель на фун|Цй|Ь |^^
4 'указатель йа фунсцио бд^6$й£ fl
Г!. U I'lvTf#^^
viable класса CSpaceship::XMotion
^указатель" на функцию P^jfh^^li" ' < указатель на фун|Цй|Ь |^^
4 'указатель йа фунсцио бд^6$й£ fl
Г!. U I'lvTf#^^
viable класса CSpaoeship::XVisual
-указатель на функциюj
if!; указатель на*функцйюО^®^^
viable класса CSpaoeship::XVisual
-указатель на функциюj
if!; указатель на*функцйюО^®^^
Теперь функция GetCiossObjectMoiKeT получить указатель на интерфейс данного объекта CSpacesbip, получая адрес соответствующего внедренного объекта. Взглянем на функцию Queryinterface в классе XMotion;
BOOL CSpaceship::XMotion::QueryInterface(int nlid. voic** ppvObj)
' METHOO.PROLOGUEtCSpaceship, Motion)'
switch (nlid) {
case IIDJWnknown:
- case II0_IMotion:
-ppvObj = &pTdis->m_xMoticn;
break;
case IID_IVisual:
*ppvObj = &pThis->m_xVisual;
break;
default:
«•ppvObj = NULL;
return FALSE;
}
return TRUE;'■
}■■
Поскольку IMotion производен от IUnknown, указатель на Motion подойдет даже в том случае, коща вызывающая программа запросит указатель на IUnknown.
ПРИМЕЧАНИЕ Стандарт СОМ требует: если в качестве параметра в Queryinterface передан HDJUnk- nown, функция должна возвращать один и тот же указатель на IUnknown независимо от того, для какого указателя на интерфейс она вызвана. Поэтому, сравнив два указателя на IUnknown, можно определить, ссылаются ли они на один и тот же объект. IUnknown иногда называют «void** для СОМ, потому что он представляет идентичность объектов.
Теперь перейдем к тексту функции GetCtossObject, которая на основе адреса mjxMotion получает первый указатель на интерфейс только что созданного объекта CSpaceship:
BOOL GetClassObject(int& nClsid, int& nlid. void** ppvObj)
ASS ERT( nClsid == CLSIO_CSpaceship);
CSpaceship* pObj = new CSpaceship();
IUnknown* pUnk = &pObj->m_xMotion;
return pUnk->QueryInterface(nIid, ppvObj);
Теперь клиентская программа может использовать Query Interface,чтобы получить указатель на IVisuak
IMotion* pMot;.
IVisual* pVis;
GetClassObject(CLSID_CSpaceshlp, IID.IMotion, (void**) SpMot);
pMot->Fly():
pMot->QueryInterface(IID_IVisual, (void**) SpVis);
pVis->Display();
Заметьте: хотя клиент и использует объект класса CSpaceship, он никогда не получает указатель на сам CSpaceship. Таким образом, у клиента нет прямого доступа к переменным-членам CSpaceship, даже если бы они были открытыми. Обратите также внимание, что мы еще не пытались удалять объект CSpaceship — все это еще впереди.
В СОМ принято особое графическое представление интерфейсов и COM-классов. Интерфейсы изображаются маленькими кружками или гнездами (jack), линии от которых ведут к соответствующему классу. Интерфейс IUnknown, поддерживаемый всеми COM-классами, изображается сверху, а остальные интерфейсы — слева от класса. Класс CSpaceship можно представить так:
IUnknown <^
IVisualO—- ' . ,. , .
CSpasechlp ,
IMotionО.....;' <■
Учет ссылок: функции AddRef и Release
В COM-интерфейсах нет виртуальных деструкторов, поэтому глупо писать:
delete pMot; // pMot - это указатель на IMotion;
// поэтому никогда так не делайте
В СОМ существует строгий протокол удаления объектов, ключевая роль в котором отводится двум /О nAwouw-функциям: AddRef и Release. Каждый COM-класс имеет переменную-
член — в библиотеке MFC она называется m_dwRef— в которой хранится текущее число «пользователей» данного объекта. Всякий раз, когда компонент возвращает указатель на новый интерфейс (например, при вызове Query Interface) pft -вызывает' AddRef а та увеличивает значение mdtvRefwa. единицу. Программа-клиент, закончив работу с указателем на интерфейс, вызывает Release, и та уменьшает значение ОТ^Итй^наединицу. Когда значение m_dwRej уменьшается до нуля, объект самоуничтожается1. Вот пример функции Release из класса CSpacesbip.-.XMotion:
DWORDCSpacesnlp;;XMotion::Release() {
METHODi.PROLOGUE(CSpaceshlp, Motion) // создает pThis if (pThis->m^dwRef == 0) return 0;
if (--pThis->.Ti_dwRef == 0 ) {
delete pThis; // объект "<осчический корабль” return 0:
}
return pThis->m_cwfief:
}"
В COM-программах на базе MFC конструктор объекта устанавливает mdwRef в 1, а это значит, что вызывать AddRef сразу после создания объекта не нужно. Тем не менее, клиент должен вызвать AddRefпри создании копии указателя на интерфейс.
Фабрики класса
Терминология объектно-ориентированного программирования иногда весьма туманна. Например, программисты па Smalltalk говорят об объектах там, где программисты на C++ говорят о классах. В литературе по СОМ по отношению к объекту и ассоциированному с ним коду часто используется термин «компонентный объект» (component object). Наряду с ним используется термин «класс объекта (class object) или «фабрика класса» (class factory). Большей точности ради его следовало бы называть «фабрикойобъектов» (object factory)2. Объект класса в СОМ представляет собой глобальную статическую область данного конкретного СОМ- класса. В MFC его аналогом можно считать CRuntimeCldss. Объект класса часто называют фабрикой класса, так как он часто поддерживает особый COM-интерфейс под названием IClassFactoty1. Как и другие интерфейсы, этот тоже производен от IUnknown. Главной функцией-членом в 1С1а5£расГогуявляется Createlnstance, которая в нашей модели может быть объявлена так:
virtual BOOL Createlnstance(int& nlid, void»* PpvCbj) = 0;
Для чего нужна фабрика классов? Как Вы уже видели, мы не в состоянии напрямую вызывать конструктор класса, но должны предоставить компоненту самому выбрать способ создания объектов. Для этого компонент предоставляет фабрику класса, инкапсулируя тем самым этап создания объекта. Поиск и запуск компонентных программных модулей для создания фабрики класса — операция «дорогая», а создание объектов посредством Createlnstance — «дешевая». Поэтому лучше использовать одну фабрику класса для создания множества объектов.
' Стандарт СОМ не запрещает поддерживать счетчик ссылок на каждый интерфейс в отдельности, а не на весь объект в целом. — Ефим, перев.
Чтобы не путаться, можно считать, что у каждого СОМ-клаеса имеется «фабрика» для создания объектов данного класса. — Ефим. перев.'
Что же это означает? А то, что мы прибегли к натяжке, позволив функции GetClassObject самой создавать объекты CSpaceship. Следовало бы сначала создать объект — фабрику класса, а уж потом вызвать Createlnstance, чтобы фабрика класса (фабрика объектов) сконструировала собственно объект — космический корабль.
Давайте сделаем все по правилам. Во-первых, объявим новый класс CSpaceshipFactory. Для простоты пусть он будет производным от ICIassFactory,чтобы не связываться с вложенными классами. Кроме того, напишем код для учета ссылок:
struct ICIassFactory : public IUnknown
virtual BOOL Createlnstance(int4 nlid, void*» ppvObj) = 0;
class CSpaceshipFactory : public ICIassFactory
{
private:
DWORD m_dwRef;.
public;
CSpaceshipFactoryO { m_dwRef = 1; }
// Функции IUnknown
virtual BOOL Querylnterface(int& nlid, void»» ppvObj);
virtual DWORD AddRefQ;
virtual DWORD Release();
// функции ICIassFactory
virtual BOOL Createlnstance(int& nlid, veld** ppvObj);
};
Затем напишем функцию-член Createlnstance-.
BOOL CSpaceshipFactory::CreateInstance(int4 nlid, void»* ppvObj)
CSpaceship- pObj = new CSpaceshipO;
IUnknown* pUnk = 4p0bj->m_xMotion;
return pUnk->QueryInterface(nIid, ppvObj);
И, наконец, создадим функцию GetClassObject, которая конструирует обьека — фабрику класса и возвращает указатель на интерфейс ICIassFactory:
BOOL GetClass0bject(int4 nCIsid, int& nlid, void** ppvObj)
ASSERT(nClsid == CLSID_ CSpaceship);
ASSERT((nlid == IID_IUnknown) 11 (nlid == IID_IClassFactory));
CSpaceshipFactory pObj = new CSpaceshipFactoryO;
*ppvObj = pObj; // IUnknown* = ICIassFactory* = CSpaceshipFactory*
Классы CSpaceship и CSpaceshipFactory работают совместно и используют один и тот же идентификатор. Теперь клиентский код (без проверки ошибок) выглядит так:
IMotion* рМог;
IVisual* pVis;
ICIassFactory- pFac;
GetClassObject(CLSID_CSpaceship, IID_IClassFactory, (void**) SpFac);
pFac->CreateInstancc(IID_IMotion, 4pMct);
pMot->QueryInterface(IID_IVisual, (void**) 4pVis);
pMot->Fly();
pVis->Display();
Обратите внимание: класс С5расе$ЫрГас1о1усодаржит функции AddRefn Release. Это необходимо потому, что они — чисто виртуальные функции базового класса IUnknown, Мы задействуем их на следующем этапе разработки программы.
Класс CCmdTarget .
Написанный нами код все егде далек от настоящего MFC СОМ, но, прежде чем перейти к реальному программированию, мы сделаем егде один небольшой шаг в развитии нашей модели СОМ. Как Вы, наверное, догадываетесь, кое-какой код и данные можно «вынести за скобки* наших COM-классов, моделирующих космический корабль, и включить в новый базовый класс. Так и делается в библиотеке MFC, где таким базовым классом является CCmdTarget- стандартный базовый класс для классов документов и окон. В свою очередь, CCmdTargetsB- ляется производным от CObject. Вместо настоящего класса мы воспользуемся CSimulated- CmdTarget, в который вынесем самый минимум — логику учета ссылок и переменную-член m dwRef. Функции ExternalAddRefo. ExtemalRelease класса CSimulaledCmdTargetnpema3Ha4e- ны для вызова из производных COM-классов. В связи с использованием CCmdTarget мы поставим CSpacesbipFactorjB один ряд с CSpaceship и реализуем интерфейс IClassFactoryKaK вложенный класс.
Можно «вынести за скобки» и некоторые универсальные функции класса CSpaceship. Функция Querylnter/асеможет быть «делегирована» вложенными классами вспомогательной функции внешнего класса ExternalQueryintetjacepHSHBasomeft ExternalAddRef. Каждая функция Querylnterface вызывает AddRef, но Crealelnstance после ExlernalQuerylnterface вызывает функцию ExternalRelease. Теперь, когда функция Createtnstance возвратит нам первый указатель на интерфейс, счетчик ссылок на объект — космический корабль будет равен 1. Следующий вызов Querylnterface увеличит счетчик до 2, и тогда для удаления объекта клиенту придется вызвать Release дважды.
И егде одно замечание. Мы сделаем фабрику классов глобальным объектом — тогда нам не придется вызывать ее конструктор. Когда клиент вызывает Release, никаких проблем не возникает, поскольку счетчик ссылок фабрики классов — в тот момент, когда клиент получает указатель на нее — равен 2. (Конструктор С5расе$Ь1рЕас1ог$устанавливает счетчик в 1, а ExtemalQuerylnterface, вызванная из GetClassObject, увеличивает его до 2.)
Пример ЕХ24А
Листинги на следующих четырех рисунках содержат код программы ЕХ24А — «модели СОМ». Это текстовое (консольное) ^1п32-приложение, не использующее MFC, которое создает с помощью фабрики классов объект CSpaceship, вызывает функции его интерфейсов и освобождает объект. Файл Interface.il (см рис, 24-1) содержит объявления базового класса CSimula- tedCmdTarget и интерфейсов, используемых как клиентом, так и компонентом. В заголовочном файле Spaceship.il (см. рис. 24-2) хранятся объявления классов космического корабля, используемые компонентной программой. Файл Spaceship.cpp (см. рис. 24-3) — это и есть собственно компонент, реализующий GetClassObject. Client.cpp (см рис 24-4) — клиент, вызывающий GetClassObject. Искусственным в этой программе является то, что код как клиента, так и компонента скомпонован в одной и той же программе ех24а.ехе. Поэтому наша модель СОМ не требует устанавливать связь в период выполнения. (Как устанавливать такую связь, Вы узнаете позже в этой главе.)
INTERFACE. Н .
Ч определения, делающие наш код похожим на код MFC .«define BOOLint
.«define DWORDunsigned int
«define true1
«define FALSE0
edefine TRACEprintf
rtdefine ASSERTassert
// -- определения и макросы
«define CLSID_CSoaceship 10
«define IID_lUnknown 0■,
«define IiD_IClassFactory1
«define IID.IMotion 2"
«define IlD.IVisual 3'
// этот макрос - только для 16-разрядной Windows ■••
«define METHOD_PROLOGUE(theClass. localClass) theClass* pihis = \
((theClass*)((char*)(this) - offsetof(theClass, m_x«#localCless))); \
iBOOL GetClassObjectfint nCIsid, int nlid, void** ppvObj); ''
H -- объявления интерфейсов '-
struct IUnknown
lUnknownO { TRACE( "Входим в конструктор IUnknown 1ip\n", this); } ■
virtual BOOL Ouerylnterface(int nlid, void** ppvObj) = 0;
virtual DWORD Released = 0;'1 '
virtual DWORD AddRef() = 0;■:'
struct IClassFactory : public IUnknown.
ICla.ssFactory()■
{ TRACE("Входим в конструктор IClassFactory%p\n", this); } virtual BOOL Createlnstancednt nlid, void** ppvObj) = 0:
struct IMotion ; public IUnknown {
IMotionO { TRACE("Входим в конструктор IMotion %p\n", this); } virtual void Fly( ) = 0;
virtual int& GetPositionO = 0;
struct IVisual ; public IUnknown
{'
IVisualQ { TRACEf Входим в конструктор IVisual %p\n", this); }.•
virtual void DisplayO = 0;..
class CSirnulatedCmdTarget: // моделируем CCmdTarget
public:•■' ■
P DWORD m.dwRef;•... .
protected:.-
CSimulatedCmdTargetO {
TRACE("Входим в конструктор CSirnulatedCmdTarget %p\n", this)1;
m_dwRef = 1; // неявный первый вызов AddRef
virtual -CSimulatedCmdTargetO'
{ TRACE("Входим в деструктор CSirnulatedCmdTarget %p\n", this); }
DWORD ExternalReleaseO {,
ТРАСЕ("9ходим в CSirnulatedCmdTarget: :ExternalRelease—RefCount = %ld\n',m_dwRef); ..
if (m_awRef == 0)■
return 0;
if (—m_dwRef == OL) {
. ТНАСЕ("удаляем\п");■ -
delete this;
, .. ..... см, след. стр.
Рис. 24-1. (продолжение) return 0;
return m_dwHef;
DWORD ExternalAddRefO ( return ++m_dwRef; }
SPACESHIP.H
class CSpaceship;
//объявления классов
class CSpaceshipFactory : public CSimulatedCmdTarget {
public:'
CSpaceshipFactory()
{ TRACE("Входим в конструктор CSpaceshipFactory %p\n", this); } ~CSpaceshipFactory()
{ TRACE("Входим в деструктор CSpaceshipFactory %p\n", this); }
BOOL ExternalQueryInterface(int IRid, void»* ppvObj);
class XClassFactory : public IClassFactory
public:
XClassFactoryO
{ TRACE("Входим в конструктор XClassFactory %p\n", this); } virtual BOOL Querylnterface(int IRid, void*» ppvObj);
virtual DWORD Release ();
virtual DWORD AddRefO;
virtual BOOL Createlnstance(int IRid, void** ppvObj);
} m_xClassFactory;
friend class XClassFactory;
class CSpaceship : public CSimulatedCmdTarget
private:
int m_nPosition; // доступ к этим данным возможен из всех интерфейсов int m_nAcceleration;
int m_nColor;
public:
CSpaceship() {
TRACE("Входим в конструктор CSpaceship %p\n", this);
m_nPosition = 100;1
m_nAcceleration = 101;
m_nColor = 102;
~CSpaceship()
{ TRACE("Входим в деструктор CSpaceship%p\n", this); }•
BOOL ExternalQueryInterface(int IRid, void»* ppvObj);
class XMotion : public Motion { public:
XMotionO
{ TRACE("Входим в конструктор XMotion %p\n", this); } virtual BOOL Querylnterface(int IRid, void*» ppvObj); virtual DWORD Release();
virtual DWORD AddRefO;
virtual void Fly();
virtual int& GetPosition();
} m_xMotion;
class XVisual : public IVisual
public:
XVisual() { TRACE("Входим в конструктор XVisual\n", this); } virtual BOOL Ouerylnterface(int IRid, void** ppvObj);
virtual DWORD ReleaseO;
virtual DWORD AddRef();
virtual void DisplayO:
} m_xVisual;
friend class XVisual; //это должно быть в конце!
friend class XMotion;
firend class CspaceshipFactory::XClassFactory;
SPACESHIP.CPP
ttinclude<stdio.h>
«include <stddef.h> // для offsetof в method_prologue
«include <assert.h>.
.«include "Interface. h”
#include "Spaceship, h”
CspaceshipFactory g_factory;
//функции-члены
BOOL CspaceshipFactory::ExternalOueryInterface(int nlid, void** ppvObj) {
TRACE("Входим в CspaceshipFactory: :ExternalOueryInterface—nlid = %d\n",nlid); switch (nlid) ( case IID_IUnknown: case IID_IClassFactoiy:
*ppvObj = &m_xClassFactory;
break;
default:
*ppvObj = NULL;
return FALSE;
ExternalAddRefO;
return TRUE;
BOOL CspaceshipFactory::XClassFactory::QueryInterface(int nlid, void** ppvObj) { ТЙАСЕ("Входим в CspaceshipFactory::XClassFactory::\
Queryinterface—nlid = %d\n", nlid);
METH0D_PR0L06UE(CSpaceshipFactory, ClassFactnry) // создает pThis return pThis->
ExternalQueryInterface(nIid, ppvObj); // делегируем в CSpaceshipClassFactory
iBOOL CspaceshipFactory: :XClassFactory: :CreateInstance(int nlid, void" ppvObj) { TRACE("Входим в CspaceshipFactory::XClassFactory::CreateInstance\n"); - HETHOO_PROLOGUE(CSpaceshipFactary, ClassFactory) // создает pThis CSpaceship* pObj = new CSpaceshipO;
Рис. 24-3. (продолжение)
if(pObj->ExternalQueryInterface(nIid, ppvObj) { pObj->ExternalRelease(); // сбалансируем счетчик ссылок return TRUE;
I
return FALSE;
DWORD CSpaceshipFactory::XClassFactory::Release(} {
TRACE("Входим в CSpaceshipFactory: :XClassFactory: :Release\n");
HETHOO_PROLOGUE(CSpaceshipFactory, ClassFactory) // создает pthis return pThis->ExternalRelease(); // делегируем CSimulatedCmdTarget
DWORD CSpaceshipFactory::XClassFactory::AddRef() {
TRACE(”Входим в CSpaceshipFactory::XClassFactory::AddRef\n");
METHOO,PROLOGUE(C$paceshipFactory, ClassFactory) // создает pThis return pThis->ExternalAddflef(); // делегируем CSimulatedCmdTarget
IBOOL CSpaoeship:;ExternalQucryInterface(int nlid, void** ppvObj) {
TRACEC“Входим в CSpaceship::ExternalQueryInterface--nIid = %d\n".nlid); switch (nlid) { case HD_IUnknown: case IID_IMotion:
-ppvObj = &m_xMotion; // как IMotidn, так и IVisual производны
break; // от IUnknown, поэтому подовдет любой иэ этих указателей case IID_IVisual:
«ppvObj = Sm_x Visual;
break;.
default;
«ppvObj = NULL;'
return FALSE; } ExternalAddRef();
return TRUE;
BOOL CSpaoeship::XMotion::QueryInterface(int nlid,void** ppvObj) {
ТВАСЕ(“Входим в CSpaoeship::XMotion::QueryInterface--nIid = %d\n", nlid);
METHOD,PROLOtiUE(CSpaceship, Motion) // создает pThis
return pThis->ExternalQueryInterface(nIid, ppvObj); // делегируем CSpaceship X
DWORD CSpaoeship::XMotion::Release() {
TRACE("Входим в CSpaceship:iXMotion::Release\n");
MErHOD_PROLOGUE(CSpaceship, Motion)// создает pThis
return pThis->ExternalReiease();// делегируем CSimulatedCmdTarget
DWORD CSpaceship::XMotion::AddRef() {
TRACE("Входим в CSpaceship::XMotlon::AddRef\n“);
METHOD„PROLOGUE(CSpaceship, Motion) // создает pThis
return pThis->ExternalAddRef(); // делегируем CSimulatedCmdTarget
void CSpaceship;:XMotion::Fly() {
TRACEC"Входим в CSpaceship::XMotion::Fly\n");
METHOD_PROLOGUE(CSpaceshlp. Motion) // создает pThis TRACE("this = %p, pThis = %p\n", this, pThis);
TRACE("ir_rPosition - %d\n", pThis->m_nPosition):
TRACE("n_rAcceleration = %d\n", pThis->m_nAcceleration);
int& CSpaceship::XMotion::GetPosition() {
TRACE("Входим в CSpaceship::XMotion:;GetPosition\n");
METHOD_PROLOGUE(CSpaceship, Motion) // создает plhis
TRACEC'this = ftp, pThis = %p\n", this, pThis);
TRACE("m_nPosition = %d\n", pThis->m_nPcsition);
TRACE("m_nAcceleration = %d\n", pThis->m_nAcceleration); return pThis->m_nPositior:
BOOL CSpaceship: ;XVisual;:Querylnterfacednt nlid,void»» ppvObj) {
TRACE("Входим в CSpaceship::XVisual::QueryInterface--nIid = %d\n", nlid);
METHOD_PROLOGUE(CSpaceship, Visual) // создает plhis
return pThis->ExternalQuerylnterface(nlid, ppvObj); // делегируем CSpaceship
DWORD CSpaceship::XVisual::Release() {
TRACE(“Входим в CSpaceship;:XVisual::Release\n");
METHOD_PROLOGUE(CSpaceship, Visual) // создает pThis
return pThis->ExternalRelease ();// делегируем CSimulatedCmdTarget
DWORD CSpaceship;;XVlsual::AddRef() {
ТНАСЕС'Входим в CSpaceship::XVisual::AddRef\n');
METHOD_PROLOGUE(CSpaceship, Visual)// создает pThis
return pThis->ExternalAddRef():// делегируем CSimulatedCmdTarget
void CSpaceship::XVisual::Display() {
TRACE("Входим в CSpaceship::XVisual;:Display\n");
METHOD_PROLOGUE(CSpaceship, Visual) // создает pThis
TRACEC'this = %p, pThis = %p\n''- this, pThis);
TRACE("m_nPosition = %d\n", pThis->m_nPosition);
TRACE("m_nColcr = %c\n", pThis->m_nColor);
IIмоделируем COM-компонент—
// в настоящей ССМ на этом месте была бы функция DllGetCIassObject, которая
// вызывалась бы при каждом вызове клиентом CoGetClassObject
BOOL GetClassObject(int nClsid, int nlid, void*» ppvObj)
ASSERT(nClsid == CLSID_CSpaceship) ;
ASSERT((nlid == 110_IUnknown) II (nlid == IID_IClassFactory));
return g_factory.ExternalQueryInterface(nIid, ppvObj);
// счетчик ссылок равен 2, что предотвращает случайное удаление
CLIENT.CPP
«include <stdio.h>
«include <stddef.h> // для offsetof в METHOD_PROLOGUE
«include <assert,h>
«include "Interface.h"
Рис. 24-4. (продолжение)
// -- главная программа
int main() // моделирует клиентскую программу OLE
< TRACER"Входим в главный модуль клиента\п"):
IUnknown* pUnk; // если объявить как void*, потеряем проверку типов
Motion * pMot;
IVisual* pVis;
IClassFactory* pClf;ч
GetClassObject(CLSID_CSpacestiip, inLlCiassFactory, (void**) &pClf);
pClf->CreateInstance(IID_IUnknown, (void»») SpUnk);
pUnk->OueryInterface(IID_IMotion, (void**) &pMot); // должны работать pMot->QueryInterface(IID_IVisual, (void**) SpVis); // все 3 указателя
TRACF("main: pUnk = )ip, pMot = %p, pVis = %p\n”, pUnk, pMot, pVis);
// тестируем все виртуальные функции интерфейсов
pMot->Fly();
int nPos = pMot->GctPosition();
TRACE("nPos = %d\n", nPos);
pVis->Display();
pClf->Release();
pUnk->Rolease();
pMot->Release();
pVis->Release();
return 0;
Реальная COM с применением MFC
Хватит моделей. Теперь мы готовы переделать пример с космическим кораблем под истинную СОМ. Но сначала надо познакомиться с функцией CoGetClassObject, затем узнать, как СОМ использует реестр Windows при загрузке компонента, в чем состоит различие между внутренним компонентом (in-process) (DLL) и внешним компонентам (out-of-process) (EXEили суррогатной DLL) и, наконец, освоить MFC-макросы, которые поддерживают вложенные классы.
В результате мы получим DLL-компонент на базе MFC, содержащий весь код CSpaceship^ том числе интерфейсы Motion и IVisual. В качестве клиента выступит обычное MFC-приложение. Оно будет загружать компонент и работать с ним при выборе пользователем соответствующей команды в меню.
COM-функция CoGetClassObject
В нашей модели мы использовали фиктивную функцию GetClassObject. В настоящей СОМ применяется глобальная функция CoGetClassObject, [Со расшифровывается как component object (компонентный объект).] Сравните следующий прототип с уже рассмотренной функцией GetClassObject-.
STDAPI CoGetClassObject(REFCl.SID rclsid, DWORD dwClsContext, COSERVERINFO* pServerlnfo. REFIID riid, LPVOID* ppvObj);
Указатель па интерфейс возвращается через параметр ppvObj, я pServerlnfo — это указатель на компьютер, на котором находится объект класса (NULL, если компьютер локальный). Параметры типа REFCLSIDh. ВЕРИВ являются ссылками на 128-разрядпые глобалъноуникапь-
we идентификаторы (globally unique identifiers, GUID) COM-классов и интерфейсов. STDAPI означает, что функция возвращает 32-разрядное значение типа HRESULT.
Стандартные GUID (например, GU1D, которые идентифицируют интерфейсы, уже созданные Microsoft) определены в Windows-библиотеках, динамически связываемых с вашей про граммой. Произвольные GUID, например, для объектов — космических кораблей, должны быть определены в программе аналогично тому, как показано ниже:
// (692D03A4-0689-11CE-B337-88EA36DE9E4E)
static const IID IID_IMoti.cn = {Ox692d03a4.OxcG89,Ox11ce,
{ОхЬЗ, 0x37, 0x88, Qxea, 0x36, Oxde, Qx9e, 0x4e}}:
Если параметр dwGlsContext равен CTSCTX_INPROC_SERVE№p COM ищет DLL. а если он равен ^LSCfXJNPROC'-HANDLERv- EXE Эти две константы могут быть заданы и одновременно: для выбора либо DLL, либо EXE, исходя из соображений производительности. Например., внутренние серверы — самые быстрые, поскольку располагаются непосредственно в адресном пространстве программы. Взаимодействие с ЕХЕ-серверами происходит гораздо медленнее, поскольку межпроцессные вызовы включают в себя как копирование данных, так и многочисленные переключения контекстов потоков. Возвращаемым результатом является значение типаHRESULT, равное О (Л"О£Й?О1?)при благополучном завершении функции.
ПРИМЕЧАНИЕ COM-функция CoCreateinstance объединяет в себе функциональность CoGetCfassObjecf и ICiassFactoryiiCreatetnstance.
СОМ и Реестр Windows
В примере ЕХ24А компонент и клиент статически связаны друг с другом, что нереально. В действительности компонентом является или DLL, или отдельи ы й EXE. Когда клист it вызыв а- ет CoGetCtassObject, СОМ ищет соответствующий компонент, который расположен где-то на диске. Как же СОМ устанавливает связь между клиентом и компонентом? Она ищет уникаль ный 128-разрядный идентификатор класса в Реестре Windows. Следовательно, этот класс должен быть зарегистрирован па вашем компьютере.
Запустив редактор Реестра Regedit (Regedt32 в MicrosoftWindows NT), Вы увидите разделы идентификаторов классов (см. рис 24-5), одни из которых представляют классы, связанные с DLL (InprocServcr32), а другие — с EXE (LocalServer32). Функция CoGetClassObjeciHaxo- дит в Реестре нужный идентификатор класса и загружает требуемый DLL- или EXE-модуль.
Что делать, если Вы не хотите отслеживать эти кошмарные идентификаторы к своей программе-клиенте? Нет проблем. СОМ поддерживает в своей регистрационной базе данных записи и другого типа — вполне читабельные идентификаторы программ, транслируемые в соответствующие идентификаторы классов (см. рис, 24-6). Поиске базе данных и трансляцию выполняет COM-функция CLSlDFromPro^D.
‘T.MNOCWSSSYSftM'.‘T.MNOCWSSSYSftM'.ГОГ® (Е<1№«1С0Ы«С83»0»М01*31Й)ГОГ® (Е<1№«1С0Ы«С83»0»М01*31Й)(ЕАЯАЕЛЛ9>11С&А9ОЗ«МЛБ1аЕАЗ ;.i 8мгам8ытв<о.я®«1с?1лl-Q1прос5ем*3?■1.емо|а (ГВ8ГО62*а1Б*4а18.*Й>-«Х12В^^ Ц(ЕАЯАЕЛЛ9>11С&А9ОЗ«МЛБ1аЕАЗ ;.i 8мгам8ытв<о.я®«1с?1лl-Q1прос5ем*3?■1.емо|а (ГВ8ГО62*а1Б*4а18.*Й>-«Х12В^^ Ц
‘T.MNOCWSSSYSftM'.
‘T.MNOCWSSSYSftM'.
ГОГ® (Е<1№«1С0Ы«С83»0»М01*31Й)
ГОГ® (Е<1№«1С0Ы«С83»0»М01*31Й)
(ЕАЯАЕЛЛ9>11С&А9ОЗ«МЛБ1аЕАЗ ;.i 8мгам8ытв<о.я®«1с?1л
l-Q1прос5ем*3?■
1.емо|
а (ГВ8ГО62*а1Б*4а18.*Й>-«Х12В^^ Ц
(ЕАЯАЕЛЛ9>11С&А9ОЗ«МЛБ1аЕАЗ ;.i 8мгам8ытв<о.я®«1с?1л
l-Q1прос5ем*3?■
1.емо|
а (ГВ8ГО62*а1Б*4а18.*Й>-«Х12В^^ Ц
;6S|ffC«F7«W74-1OU«ED«)D2B2ECn3l 111*-@l InmcSavtr#|, 3(£ (^^К<ОаНО)А$ГУ<Ю006£2ЕГ«3>!harocHwdbr3Z«UcaE«v«r37.У;6S|ffC«F7«W74-1OU«ED«)D2B2ECn3l 111*-@l InmcSavtr#|, 3(£ (^^К<ОаНО)А$ГУ<Ю006£2ЕГ«3>!harocHwdbr3Z«UcaE«v«r37.Уt-Canetftt-CanetftРис. 24-5. Разделы четырех идентификаторов классов в реестреРис. 24-5. Разделы четырех идентификаторов классов в реестре
;6S|ffC«F7«W74-1OU«ED«)D2B2ECn3l 1
11*-@l InmcSavtr#|
, 3(£ (^^К<ОаНО)А$ГУ<Ю006£2ЕГ«3>
!harocHwdbr3Z«
UcaE«v«r37.У
;6S|ffC«F7«W74-1OU«ED«)D2B2ECn3l 1
11*-@l InmcSavtr#|
, 3(£ (^^К<ОаНО)А$ГУ<Ю006£2ЕГ«3>
!harocHwdbr3Z«
UcaE«v«r37.У
t-Canetft
t-Canetft
Рис. 24-5. Разделы четырех идентификаторов классов в реестре
Рис. 24-5. Разделы четырех идентификаторов классов в реестре
Рис. 24-6. Читабельные идентификаторы программ в реестре
ПРИМЕЧАНИЕ Первый параметр функции CLSIDFromProgID — строка с идентификатором программы, но это необычная строка. Здесь Вы впервые в СОМ столкнетесь с двухбайтовыми символами. Все строковые параметры COM-функций (кроме ОАО) являются указателями типа OLECHAR* на строки Unicode-символов. Теперь ваша жизнь усложнится из-за постоянного преобразования «двухбайтовых» строк в обычные, и наоборот. Чтобы получить константу — строку двухбайтовых символов, ставьте перед строковым литералом символ L:
CLSIDFromProgID(L"Spaceship", Aclsid);
О возможностями MFC по преобразованию Unicode-сгрок Вы начнете знакомиться с главы 25.
Каким же образом регистрационная информация попадает в Реестр? Можно запрограммировать приложение компонента так, чтобы оно вызывало Windows-функции, напрямую модифицирующие Реестр. В MFC для этих функций предусмотрена удобная оболочка в виде функции COleObjectFactory-iUpdateRegistryAl^enogaa. находит в программе все глобальные объекты — фабрики класса и регистрирует их имена и идентификаторы классов.
Регистрация объекгавпериод выполнения
Вы только что видели, как Реестр Windows используется для регистрации COM-классов, хранящихся надиске. Объекты — фабрики класса тоже надо регистрировать, и очень жаль, что термин «регистрировать» употребляется в обоих контекстах. Объекты в модулях внешних Компонентов регистрируются в период выполнения вызовом COM-функции CoRegisterClassObject, и эта информация сохраняется системными DLL-модулями Windows в оперативной памяти. Если фабрика зарегистрирована в режиме, разрешающем одному экземпляру сервера создавать более одного COM-объекта, то при вызове клиентом CoGctCtossObjectCOM использует существующий процесс, а не создает новый.
Вызов COM-клиентом внутреннего компонента
Мы начнем с DLL, а не с ЕХЕ-компонента, так как в этом случае взаимодействие программ проще. Приведем псевдокод, поскольку мы будем работать с MFC-классами, скрывающими многие детали.
Клиент
CLSID clsid;
IClassFactory» pClf;
IUnknown* pdnk;
CoInitialize(NULL); // инициализация COM С18ЮРготРгод10("имя_компонента". Aclsid);
COM
Клиент
CoGetClassCbject(clsLd, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (vold**)&pCIf).;
COM•
no идентификатору класса ищет компонент в памяти if (DI L компонента еще не загружена) {
Считывает имя файла DLL из Реестра Загружает DLL компонента в память процесса
}
DLL компоненте
^(компонент только ЧТО загружен)-
Создаются глобальные объекты-фабрики классов Вызывается I nil-Instance для DLL (только в MFC)
}
СОМ
Вызывает из DLL глобальную экспортируемую функцию DLlGetCLassCbject
со значением CLSID, переданным ранее в CoGetClassObject
DLL компонента■
DHGetClassObject возвращает IClassFactory»
COM
Возвращает клиенту IClassFactory*
Клиент
pClf->CreateInstance(NULL, IID_Ibnknown, (void'*) SpUnk);
DLL компонента
Вызывается функция фабрики классов Createlnstance
.(напрямую, через таблицу виртуальных функций компонента)
Создается объект класса. "имя_компонен-а"
Возвращается указатель на запрошенный интерфейс .
Клиент■
pClf->Release();
pUnk->Release();1
Pit, компонента
Через таблицу виртуальных функций вызывается Release для объекта класса "имя_компонента" if(счётчик_ссылок == 0) {
Объект самоуничтожается
Клиент
CoFreeUnusedLibraries();
СОМ
Вызывает из DLL глобальную экспортируемую функцию DllCanUnloadNow
DLL компонента
Вызвана DllCanUnloadNow
if (Все созданные DLL объекты уничтожены) { return TRUE;
}
Клиент
CoUninitialize(); // перед самым завершением СОМ освобождает DLL, // если DllCanUnloadNow возвратила TRUE
СО*
Освобождает ресурсы
Клиент
Завершает работу
Windows выгружает DLL, если та еще загружена и не
используется другими программами
Обратите внимание на рад моментов. Во-первых, в ответ на вызов клиентом CoGeiClass- Objectns DLL вызывается экспортируемая функция DllGetClassObject. Во-вторых, возвращаемый адрес интерфейса фабрики классов наделе — физический адрес1 таблицы виртуальных .функций фабрики классов в DLL И в-третьих, клиент вызывает CreatelnstanceHjm. любук» другую функцию интерфейса напрямую, через таблицу виртуальных функций компонента.
Связь, которую устанавливает СОМ между ЕХЕ-клиептом и DLL-сервером, весьма эффективна — так же, как и вызов виртуальной функции C++ внутри процесса, плюс к этому осуществляется проверка соответствия типов при компиляции. Единственная «накладка» — дополнительная операция, связанная с поискам идентификатора класса в Реестре при первой загрузке DLL
Вызов COM-клиентом внешнего компонента
Связь с отдельным ЕХЕ-компонснтомчерез СОМ сложнее, чем связь с DLL-компонентом. ЕХЕ- компонент находится в другом процессе или даже на другом компьютере. Но не волнуйтесь. Пишите программы так, будто всегда существует прямая связь. О деталях позаботится СОМ посредством своей удаленной архитектуры, которая включает в себя вызов удаленных процедур (Remote Procedure Calls, RPC).
В RPC клиент обращается с вызовами к особой DLL, называемой заместителем (proxy). Заместитель передает поток данных так называемой заглушке (stub), которая представляет собой DLLs процессе компонента. Когда клиент вызываетфункцию компонента, заместитель уведомляет об этом заглушку, посылая программе компонента сообщение, которое обрабатывается в ней скрытым окном. Механизм преобразования параметров в поток данных и обратно называетсямаршалингом (marshalling).
При использовании стандартных интерфейсов (т. е. тех, которые разработаны самой Microsoft) — таких, как ICiassFactoryn IPeT$ist<c этим интерфейсом мы познакомимся при рассмотрении устойчивости в СОМ), — код заместителя и заглушки, реалтизующий маршалинг, находится в Windows-библиотеке OLE32.DLL Если же Вы разрабатываете собственные интерфейсы — такие, как (Motion и iVisual, — то писать коды заместителя и заглушки придется самостоятельно. Теперь для построения этого кода достаточно определить интерфейсы на специальном языке определения интерфейсов (Interface Definition Language, IDL) и скомпилировать код, создаваемый компилятором MIDL (Microsoft Interface Definition Language).
Теперь мы приведем псевдокод, описывающий взаимодействие ЕХЕ-клиснта с EXE-компонентом. Сравните его с тем, что было показано для DLL Обратите внимание, что вызовы со стороны клиента совершенно одинаковы.
Точнее, виртуальный адрес в адресном пространстве клиентского процесса. — Прим, перев.
Клиент
CLSID cisid; '
ICIassFactory* pClf:
IUnknown* pUnk;
CoInitialize(NULL); //инициализация COM
CLSIDFromProgID("имя_компонента", &c 1s id);
COM
По параметру "имя_компонента" ищет в Реестре идентификатор класса
Клиент
CoGetClassObject(clsid, IID_IClassFactory, (void**) &pClf);
COM■
Ищет компонент в памяти по идентификатору класса
if(EXE компонента еще не загружен или нужен его новый экземпляр) { СОМ считывает ИМЯ файла EXE из Реестра Загружает EXE компонента в память
>
EXE компонента
if (только что загружен) .{
Создаются глобальные объекты-фабрики классов
Вызывается Initlnstance (только в MFC)
CoInitialize(NULL);
for (для каждого объекта-фабрики класса) { CoRegisterClassObject(..,);
Возвращает ICIassFactory* в СОМ
)
Возвращает клиенту указатель на запрошенный интерфейс
(указатель для клиента не совпадает с указателем на интерфейс компонента)
Клиент
pClf->CreateInstance(IID_lUnknown, (void**) 8pUnk);
EXE компонента
Вызывается Createlnstance фабрики класса
(косвенным вызовом с помощью маршалинга)
Создается объект класса "имя_компонента"
Возвращается указатель на запрошенный интерфейс (косвенно)
Клиент
pClf->Release();
pUnk->Release(); •
EXE компонента
Косвенно вызывается Release для объекта класса "имя_компонента"'
1Т(счетчик_ссылок == 0) {
Объект самоуничтожается
)
if(Bce объекты освобождены) {
Корректное завершение работы компонента
Клиент
CoUnlnitialize(); // непосредственно перед завершением
COM
Вызывает Release для всех обьектоз, не освобсжденных клиентом
EXE компонента
Завершается
СОМ
Освобождает ресурсы
Клиент
Завершает работу
Как видите, СОМ играет важную р оль во взаимодействии между клиентом и компонентом. СОМ поддерживает в памяти список фабрик классов, находящихся в активных ЕХЕ-компо- нептах, но не следит за отдельными COM-объектами типа CSpaceship, Такие объекты сами заботятся о своем уничтожении через механизмЛ<И1?^<Яе&ше. СОМ также участвует в завершении клиента. Если клиент использует EXE-сервер, то СОМ прослушивает коммуникационный канал связи клиента с сервером, отслеживая счетчик ссылок на каждый объект. При завершении клиента СОМ отключается от объектов компонента, что в определенных условиях вызывает их освобождение. Но не полагайтесь на это. Перед завершением обязательно убедитесь, что ваша клиентская программа освобождает все указатели на интерфейсы.
MFC-макросы для интерфейсов
В примере ЕХ24А Вы видели, как используются вложенные классы при реализации интерфейсов. Библиотека MFC содержит набор макросов для автоматизации этого процесса. В объявлении класса CSpaceship,производного от настоящего MFC-класса CCmdTarget, присутствуют следующие макросы:.
BEGIN INTEAFACE PART(Notion. IMotion)
STDMETHDD_(void, Fly) (};
STDMETHOD (int&, GetPos.tion) ();
END_INTERFACE_PART(Motion)
BEGIN_INTERFACE_PART(Visual, IVisual)
STDMETHOD (void, Display) (),
END_INTERFACE_PART(Visual)
DECLARE_INTERFACE_MAP()
Макросы 1NTERFACL_PARTпорождают вложенные классы, добавляя к первому параметру префиксX, чтобы сформировать имя класса, и т_х, чтобы образовать имя внедряемого объекта. Эти макросы генерируют прототипы заданных функций интерфейса, а также прототипы Query lnterface,AddRefn Release.
Макрос DECLARE _INTERFACE_MAP порождает объявления для таблицы, содержащей идентификаторы всех интерфейсов класса. CCmdTarget: ;ExtemalQuery!nteifac6Lcnojib3yeT эту таблицу, чтобы получить указатели на интерфейсы, .
В файле реализации CSpaceship нужны макросы;
BEGIN_INTERFACE_MAP(CSpaceshio, CCmdTarget)•
INTERFACE_PART(CSpaceship, IID_IMotipn, Motion)
INTERFACE_PART(CSpaccshlp, ПО IVisual, Visual)
END ,INTERFACE_MAP()
которые создают таблицу интерфейсов, используемую функцией CCmdTarget;External- Query 'Interface.
Типичная функция-член интерфейса выглядит так:
STDMETHODIMP_(vold> CSpaceshlp::XMotion::Fly() {
METHOD_PRO_OGUE(CSpaceshlp, Motion)
pThis->m_nPositlon += 10;
return;
}
He забудьте реализовать все функции каждого интерфейса, в т. ч. Queryhiterjace, AddReJ и Release. Последние три функции могут делегировать вызовы функциям из CGmdTarget.
ПРИМЕЧАНИЕ Макросы STDMETHOD_n STDMETHOD1MP_ объявляют и реализуют функции, передающие параметры по правиламstdcall, как того требует СОМ. Эти макросы позволяют задавать в своем первом параметре тип возвращаемого значения. В двух других макросах — STDMETHQD и STDMETHODIMP — подразумевается, что возвращается значение типа HRESULT.'
MFC-класс COteObjectFactory
В примере с моделью СОМ класс CSpacesbipFactor^wi жестко запрограммирован Eta генерацию объектов CSpacesbip.'B MFC применяется метод динамического создания объектов. Благодаря атому единственный класс, удачно названный COIeObjectFactory, способен создавать объекты любого класса, указанного в период выполнения программы. Все, что от Вас требуется, — использовать в объявлении класса макросы, подобные этим:
DECLARE DYNCREATE(CSpaceship) DECLARE J)LECREATE(CSpacesnip)
В файле реализации класса нужно написать макросы такого вида:
IMPLEMENT_DYNCREATE(CSpaceship, CCmdTargeT};
Ц {692D03A3-C689-11CE-B337-88EA360E9E4E}•
IMPLEMENT_OLECREATE(CSpaceshlp, "$paceship",0x692d03a3,OxG689,0x11ce.
Gxb3, 0x37, 0x88, Oxea, 0x36, Oxde, 0x9e, Cx4e)
Макросы DYNCREAАктивизируют стандартный механизм динамического создания объектов (см. приложение Б). Макросы OLECREATE объявляют и определяют глобальный объект класса COkObjectFactory с указанным уникальным CLS1D. В DLL-компоненте экспортируемая 'функцияDUGetClassObjcct,используя глобальные переменные, определенные макросами OLECREATE, отыскивает нужный объект — фабрику классов и возвращает указатель на него. В ЕХЕ- компоненте инициализирующий код вызывает статическую функцию COkObjectFactary-Re- gisterAH, которая ищет все объекты-фабрики и регистрирует каждый из них вызовом CoRegis- terCktos0bjecLФyнкцияRegisterAltвы.зывaeтcяк при инициализации DLL, но в этом случае она просто устанавливает флажок в объекте-фабрике (или объектах-фабриках).
Мы только слегка коснулись проблемы поддержки СОМ в MFC. Если Вам нужны подробности, обратитесь к книге Шеферда и Уингоу MFC Internals (Addison-Wesley, 1996).
Поддержка внутренних СОМ-компоненгов со стороны AppWizard и ClassWizard
AppWizard не предназначен специально для создания DLL-компопептов СОМ, но его можно «обмануть», попросив сгенерировать обычную DLL с поддержкой Автоматизации. В главном файле проекта для нас представляют интерес следующие функции:
BOOL CEx24bApp::lnitlnstance() {
COleObjectFactory::RegisterAll();
return TRUE;.
iSTDAPI CllGetClassCbject(REFCLSID rclsid, REFIID riid, LPVOID» ppv)' '
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return AfxDHGetClassObject(rclsid. riid, ppv):-
}.
STDAPI DHCanUnloadNow(vold)
AFX_PANAGE_STATE(AfxGetStaticModuleState()):
ireturn Z’.fxOHCanlhloadNwC);
}
STDAPI DllRegisterServer(void)
{
AFX„MANAGE_STA*E(AfxGetStaticModuleState()):
COleObjectFactory::UpdatcRegistryAll();
return S_OK;
}
Эти три глобальные функции экспортируются при помощи DEF-файла проекта. Вызывая MFC-функции, они делают все, что нужно DLL-модулю СОМ. Для обновления информации г, системном реестре функцию DllRegisterServerможно вызывать из программы-утилиты.
После создания заготовки проекта следующий таг — используя ClassWizard, добавить в проект один или несколько классов, объекты которых будут создаваться через СОМ. Для этого просто заполните поля в диалоговом окне New Class, как показано ниже,
Вновь сгенерированный класс получит рад элементов для поддержки Автоматизации, например, таблицы диспетчеризации (dispatch maps), которые можно спокойно удалить. Кро ме того, из StdAfx.h можно убрать и две следующих строки:
tfinclude <afxodLgs.h>
Hinclude <afxdisp.h>
COM-клиенты на базе MFC '
Написать клиентскую COM-программу на базе MFC несложно. Бы просто создаете обычное приложение с помощью AppWizard и добавляете в StdAfich строку:
Uinclude <afxole.b>
а в функцию-член Initlnstance класса приложения — оператор:
AfxOleIni-();
Теперь можете написать код, вызын нощий CofietClassObject..'
Пример ЕХ24В
Пример ЕХ24В — обычная MFC DLL, содержащая настоящую СОМ-нерсию класса CSpacesbip, который Вы видели в ЕХ24А. Файлы ех24Ь.срр и ex24b.h сгенерированы AppWizard так, как мы только что пояснили. На рисунке 24-7 показан файл Interface.il, в котором объявлены интерфейсы IMotionи IVisual, а на рисунках 24-8 и 24-9 приведен код класса c'Spacysbip.Сравните его с кодом из примера ЕХ24А. Заметили, как применение MFC-макросов уменьшило объем кода? Обратите внимание на то, что учет ссылок и логику Querylnter/аареалмзует MFC- класс CCmdTarget.
INTERFACE.Н
struct IMotion : public IUnknown'
{ STDMETHOD (void, Fly) () = 0;
STDMETHOD_(int&, GetPosition) () = 0;
struct IVisual : public IUnknown {
STDMETHOD_(void, Display) {) = 0;
Рис. 24-7. Файл Interface.h
SPACESHIP.H
void ITrace{REFiID lid, const char* str);
//////////////////////////////////////////////////////////////////.7//,7////////
// CSpaceship command target
class CSpaceship : public CCmdTarget {
DECLARE_DYNCREATE(CS3aceship).
private:
int m_nPosition; // доступ к этим переменным возможен из всех интерфейсов
int m_nAcceleration;
int m_nColor;
protected:
CSpaceship!): // защищенный конструктор для динамического создания обвесив
// Атрибуты public:
// Операции public:
// Переопределенные функции
// переопределенное виртуальные функции, сгенерированные ClassWizard //({AFX_VIRTiJAL(CSpaceship) public:
virtual void OnFinalRelease!);
//)}AFX_VIRTUAL
// Реализация
protected:
virtual ~CSpacesh?p();
Рис. 24-8. (продолжение)
// сгенерированные функций таблицы сообщений
//{{AFX_MSG(CSpaceship)
/7 ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять и удалять функции-члены //}}AFX_MSG .
DECLARE_MESSAGE_MAP():
DECLARE OLECREATE(CSpaceship)
HFGIM INTERFACE_PART(Motion, IMotion)
STDMETHOD (void, Fly) ();
STDMETHOD_(int&, GetPosition) {);
EN3_INTERFACE_PART(Motion)
BEGIN JNTERFACE_PAflT(Vlsual, IVisual) .
STDHETHOO_;void, Display) (};
ENBJRTERFACF _PART (Visual)
CECLARE_INTERFACE_MAP()
////////////////////////////////////////////////////////////////////////////////
SPACESHIP.CPP
^include "StCAfx.h" ttinclude "ex24b.h" ffinciuce "InTnrface.h" tincluco "Spaceship, h"
flifdef DEBUG'
ftundef THIS_FILE ■
static char TH1S_FILE[] = _FILE_: flen di f
//////////////////////////////////////////////////////////////////////////////// // CSpacesnip
.// {662D03A4-C669-11CE-B337-8SEA36DE9E4E) static const I ID IID_IMotion =
{ Ox692dC3a4,Oxc589,Oxlice,{ 0xb3, 0x37, 0x88, Oxea, 0x36, Oxde, 0x9e, 0x4e } };
// {6S2D03A5-C689-11CE-B337-88EA360E9E4E)
static const IIO HD_IVisual =
{ 0x692d03a5,Oxc689,0x11ce. { Oxb3, 0x37, 0x88, Oxea, 0x36, Oxde, 0x9e, 0x4e } };
TMPI.EMFNT_DYNCREATE'CSpaceship, CCmdTarget)
CSpaceship::CSpaceship()
I
TRACE( ”Конструктор CSpeceship\n");
m_nPosltion = 100;
m_nAcceleration - 101;
m_nColor =102;
// чтобы приложение не завершалось, пока активен СОМ-обьёкт, конструктор вызывает AfxOleLockApp AfxOleLockAppO:
CSpaceshi и: :~СЙрасе«Ыр()
{
TRACE("ДеструкторС8расе9Ь1р\п”);
Рис. 24-9. (продолжение)
// чтобы завершить приложение после уничтожения всех созданных СОИ-объектов, // деструктор вызывает AfxOleUnlockApp
Afx01eUnlockApp();
void CSpaceship::OnFinalRelease()
{•
// QnFinalRelease вызывается, когда освобождается последняя ссылка на СОМ-объект.
// Эта реализация уничтожает объект. Добавьте необходимую для вашего объекта очистку
// перед удалением его из памяти.
delete this;
BEGIN_MESSAGE_MAP(CSpaceshlp, CCndTarget)
//{{AFX_MSG_MAP(CSpaceship>
// ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять и удалять макросы
//}}AFX MSG_MAP
END_MESSAGE„MAP()
BEGIN_INTERFACE_MAP(CSpaceship, CCmdTarget)
INTERFACE-PART(CSpaceship, II0_IMotion, Motion)
INTERFACE-PART(CSpaceship. IID IVisual, visual)
END_INTERFACE_MAP()
// {692D03A3-C689-11CF-B337-88EA36DC9E4E}
IMPLEMENT_OLECREATE(CSpaceship, "Spaceshla",0x692d03a3,0xc689, 0x11ce,0xb3, 0x37 , 0x88, Oxea, 0x36, Oxde, 0x9e, Qx4e)
STDMETHODIMP_(ULONG) CSpaceship::XMotion::AddRef()'
I
TRACE("CSpaceship::XHotipn::AddRef\n");
METHOD_PROLOGUE(CSpaceship, Motion)
return pThis->ExternalAddRef();
stdmethodimpjulong) CSpaceship: :XMotion: :Release()
* TRACE("CSpaceship:iXMotion::Release\n");
МЕТНОО_РЯСLOGOE(CSpaceshi p, Motion) return pThis->ExternalRelease();
STDMETHODIMP CSpaceship::XMotlon::QueryInterface(
REFIID iid, LPVOTD* ppvObj)
I'
ITrace(iid, "CSpaceship::XMotion::QueryInterface");
METHOD_PROLOGUE(CSpaceship, Motion)
return pThis->ExternalQueryInterface(iid, ppvObj);
STDMETHODIMP (void) CSpaceship::XMotion::Fly() {.''
TRACE("CSpaceship::XMotion::Fly\n"); METHOD. PRC LOGO E (CS paceshi p, M otion) TRACF("m_nPositlon = %d\nT pThis->m_nPosition); TRACE("m_nAcceleration = %d\n”, pThis->m_nAcceleration); return;
Рис. 24-9. (продолжение)
STDMETHOOIMP (int&) CSpaceship::XMotion::GetPositlon()
TRACE("CSpaceship::XMotion::GetPosition\n");
METHOD_PROLOGUE(CSpaceship, Motion)’
TRACE("m_nPosition = M\n“, pThis->m_n Position);-
TRACE("m_nAcceleration = %d\n", pThis->m_nAcceleration):
return' pThis->m_nPosition;
////////////////////////////////////////////////////////////////////////////////
STDMETHOPIMPJULONG) CSpaceship: tXVisual::AddRef()
TRACE("CSpaceship::XVisual::AddRef\n");
METHOD_PROLOGUE(CSpaceshlp, Visual)
return pThis~>ExternalAddRef ():
STDMETHODIMP_(ULONG) CSpaceship: :XVisual::Release()
TRACE("CSpaceship::XVisual::Release\n");
METHOD_PROLOGUE(CSpaceship, Visual) return pThis->ExternalRelease();
}
STDMETHOOIMP CSpaceship: :XVisual: ;QueryInterface(REFIID iid, LPVOID* ppvObj)
ITrace(iid, "CSpaceship: :XVlsual: :Queryinterface");
METHOD_PROLOGUE(CSpaceship, Visual)
return pThis->ExternalQueryInterface(ild, ppvObj);
/
STDMETHOOIMP (void) CSpaceship: :XVisual: :Display() {
TRACE("CSpaceship::XVisual::Display\n");
METHOD_PROLOGUE(CSpaceship, Visual)
TRACE("m_nPosition = %d\n", pThis->m_nPosiTion);
TRACE("m_nColor = %d\n", pThis->m_nColor);
/////////////////////////////////////^ void ITrpce(REFIID iid, const char* str).
1 OLECHAR* IpszIID;
::StringFromIID(iid, &lpszIID);
CString strTemp = (LPCWSTR) IpszIID;
TRACE("%s - %s\n", (const char*) strTemp, (const char*) str);
AfxFreeTaskMemCIpszIID);
////////////////////////////////////////^^ // Обработчики сообщений класса CSpaceship
Пример EX24C '
Пример ЕХ24С — MFC-программа, содержащая настоящую COM-версию клиентской части примера ЕХ24А. Это сгенерированное Арр Wizard SDI-приложение с добавлением операторов •include^s. включения заголовочных файлов MFC СОМ, атакже с добавлением вызова A/xOte- /яйдля инициализации DLL. Команда Spaceship из меню Test обрабатывается функцией клас-
са «вид» (см рис. 24-10). В проект входит и копия файла компонента Interface.h (см. рис. 24-7). Оператор */ис/м</едля включения этого файла помещен в начало ex24cView.cpp.
void CEx24cView::OnTestSpaceship()
{
CLSID clsid;
LPCLASSFACTORY pClf;
LPUNKNOWN pUnk;
IMotion* pMot;
IVisual* pVis;
HRESULThr;
if ((hr = : :CLSIDFromProgID(L"Spaceship", 4clsid)) != NOERROR) { TRACEC’He наеден Program ID — ошибка = %x\n", hr);
return;
}
if «hr = ::CoGetClassObject(clsid, CLSCTX INPROC SERVER, NULL, IID_IClassFactory, (void**) 4pC10) != NOERROR) { TRACE( He наеден CLSID - ошибка = %x\n ", hr);
return;
pClf->CreateInstance(NULL, IID_Wnknown, (void**) &pUnk);
pUnk->QueryInterface(IIO_IHotion, (void**) 4pMot); // должны работать
pMot->QueryInterface(IID_IVisual, (void**) 4pVis); // все 3 указателя
TRACEC’main: punk = %p, pMot - %p, pVis = %p\n", pUnk, pMot, pVis);
// тестируем все виртуальные функции интерфейсов
pMot->Fly();
int nPos = pMnt->GetPosition();
TRACE("nPos = %d\n", nPos);
pVis->Display();
pClf->Release();
pUnk->Release();
lpMot->Release():.
pVis->Release();
AfxMessageBox("TecT завершен успешно. Вывод см. в окне Debug.") }
Рис. 24-10. Клиентский обработчик команды, загружающий и тестирующий компонент CSpaceship
Чтобы протестировать клиент и компонент, сначала запустите компонент для обновления информации в Реестре. Эго можно сделать с помощью нескольких утилит, но попробуйте использовать программу REGCOMP из проекта \vcpp32\RegComp с прилагаемого компактдиска. Вам будет предложено выбрать DLL- или OCX-файл, а затем программа вызовет экспортируемую функцию DUReg^sterServeras указанного файла.
И клиент, и компонент выводят информацию о своей работе, используя макрос TRACE, поэтому Вам потребуется отладчик. Из него можно запускать как клиент, так и компонент. В последнем случае надо будет ввести полное имя исполняемого файла клиента. Но копировать DLL-модуль не надо, так как Windows найдет его по информации в Реестре.
Вложение и агрегирование по сравнению с наследованием
Обычно при программировании на C++ наследование применяют, чтобы вывести универсальные функции в базовый класс для повторного использования. Пример — класс CPersistent- Frame (глава 15).
В СОМ вместо наследования используют вложение (containment) и агрегирование (aggregation). Начнем с вложения. Допустим, мы расширили наш пример моделирования космичес-
кого пространства, введя туда моделирование движения планет. Программируя на обычном C++, мы, скорее всего, написали бы базовый класс COrbiter, который бы инкапсулировал законы небесной механики. В СОМ же применяются внешние (outer) классы CSpacesbip и CPlanet и внутренний (inner) класс COrbiter. Интерфейс IVisual был бы реализован непосредственно во внешних классах, но интерфейсы IMotion они делегировали бы внутреннему классу COrbiter. Результат выглядел бы примерно так:
IUnknownIUnknownIVisual О C^nase^p 0й?'IVisual О C^nase^p 0й?'IMotionCIMotionCГ1 ^ COrbiter &. IMotion ..'Г1 ^ COrbiter &. IMotion ..'
IUnknown
IUnknown
IVisual О C^nase^p 0й?'
IVisual О C^nase^p 0й?'
IMotionC
IMotionC
Г1 ^ COrbiter &. IMotion ..'
Г1 ^ COrbiter &. IMotion ..'
Заметьте; объект COrbiter т знает о том, что он содержится внутри объекта CSpacesbip или CPlanet, тогда как внешнему объекту, конечно же, известно, что в него встроен COrbiter. Внешний класс содержит реализации всех функций своих интерфейсов, но функции из IMotion, включая Query-lnterfaceppocro вызывают одноименные функции внутреннего класса.
Агрегирование сложнее вложения. В этом случае клиент получает прямой доступ к интерфейсам внутреннего объекта. Вот вариант модели движения планет в космосе, пост роенный на агрегировании:
IUnknown (outher) QIUnknown (inner) QIUnknown (outher) QIUnknown (inner) QIUnknown (outher)Unknown (inner) QIUnknown (outher)Unknown (inner) Q
IUnknown (outher) Q
IUnknown (inner) Q
IUnknown (outher) Q
IUnknown (inner) Q
IUnknown (outher)
Unknown (inner) Q
IUnknown (outher)
Unknown (inner) Q
W К -5й«»>:!Ж8»ШК«?-ИШЖЖ10йи:
Как и при вложении, COrbiter встроен (вложен) в CSpacesbipm CPlanet. Допустим, клиент получил указатель на IVisual sta CSpacesbip .после чего вызывает Queryinterface.что бы получить указатель на IMotion. Если для этого использовать указатель на внешний IUnknown, Вы Ничего не получите, так как класс CSpacesbip не поддерживаетIMotion. Поэтому класс CSpacesbip получает указатель на IMotion от COrbiter с помощью указателя на внутренний IUnknown (встроенный объект COrbiter").
Теперь предположим, что у клиента есть указатель на IMotion, через который он вызывает Query lnter/асеятобы получить указатель на IVisual. Внутренний объект в этом случае должен иметь возможность передать вызов внешнему объекту, но как? Давайте еще раз более внимательно рассмотрим вызов функции Createlnstance в листинге на рисунке 24-10. Первый параметр в данном случае равен NULL. Если Вы создаете агрегированный (внутренний) объект,, этот параметр применяется для передачи указателя на IUnknown внешнего объекта, который к тому времени уже должен существовать. Такой указатель называется внешним управлнюгцилг (controlling unknown). Класс COrbiter сохраняет его в одной из своих переменных-членов и использует при вызове Queiyinterfac^x интерфейсов, не поддерживаемых COrbiter.
Агрегирование поддерживается библиотекой MFC. У класса CCmdTargetecTb открытая переменная-член т_pOuterUnknown,B которой хранится указатель на IUnknown внешнего объекта (если данный объект входит в состав агрегата). Функции-члены класса CCmdTarget -- ExternalQueryinterfaceExternalAddRejxX ExternalRelease — делегируют вызовы внешнему IUnknown (если он существует). Функции-члены другой группы — InternalQuerylnterfacelntemal- AMRefnlnternalRetease— ничего не делегируют. Описание MFC-макросов, поддерживающих агрегирование, см. в MFC Technical Note *38.
ГЛАВА 2 5
Автоматизация
Из главы 24 Вы уже знаете, что такое интерфейс. Вы также познакомились с двумя стандартными COM-интерфейсами IUnknown и /67а»7йс?огу.Теперь Вы готовы изучать прикладную часть СОМ или, по крайней мере, одну из ее составляющих —Автоматизацию (Automation), ранее известную как OLE-автоматизация. В этой главе мы рассмотрим COM-интерфейс IDispatch, позволяющий программам на C++ взаимодействовать с программами на Visual Basic for Applications (VBA), а также с программами, написанными на другахязыках сценариев (scripting languages). Кроме того, IDispatcbHTpaeT ключевую роль при размещении СОМ-объектов на Web-странице. Взяв реализацию IDispatchwt библиотеки MFC, мы напишем на C++ программы компонента и клиента Автоматизации, причем рассмотрим как внешние, так и внутренние компоненты.
Но прежде чем программировать COM-автоматизацию на C++, Вы должны знать, как пи- шетподобные программы весь остальной мир. В этой главе Вы получите представление о VBA, реализованном в Microsoft Excel. Мы будем запускать из Excel свои компоненты, а также будем вызывать Excel из клиентской программы на C++.
Взаи модействие C++с Vis ual Basic for Ар pl ications
Отнюдь не все, кто программирует в Windows, собираются программировать на C++ и тем более углубляться в «дебри» СОМ. Если Вы следили за событиями в нашей профессиональной сфере в последние несколько лет, то наверно заметили тенденцию, что программисты на C++ создают модули, допускающие повторное использование, а программисты на языках боже высокого уровня (Visual Basic, VBA, языки сценариев Web и т. п.) интегрируют эти модули в приложения. Вы можете стать участником такого «разделения труда», узнав, как сделать программное обеспечение доступным для языков сценариев (Script-friendly). Одним из средств достижения этого в настоящее время, поддерживаемых библиотекой MFC. является Автоматизация. Другое средство интеграции C++ и VB А—элементы управления ActiveX, представляющие собой надмножество Автоматизации, поскольку в них тоже применяется интерфейс IDispatch. Однако ActiveX-элементы в раде случаев просто излишни. Многие приложения, например, Microsoft Excel 97, поддерживают как компоненты Автоматизации, так и ActiveX-элементы. Более того, все, что Вы узнаете об Автоматизации, можно будет использовать при написании ActiveX-элементов и работе с ними.
На успех Автоматизации работают два фактора. Во-первых, VBA (или VB Script) — сейчас стандартное средство программирования в большинстве приложений Microsoft, включая Word, Excel и Access, не говоря о самом Visual Basic. Все эти приложения поддерживают Автоматизацию, а значит, могут взаимодействовать с другими совместимыми с этим механизмом
компонентами, в том числе с написанными на C++ и VBA. Например, можно написать на C++ программу, которая будет использовать возможности Word в обработке текста, или компонент обращения матрицы, который будет вызываться из VBA-макроса в таблице Excel.
Во-вторых, десятки фирм предусматривают в своих приложениях интерфейсы Автоматизации — главным образом, для программистов на VBA. Приложив минимум дополнительных усилий, Вы сумеете воспользоваться этими интерфейсами из C++: например, написать MFC- программу, управляющую программой рисования Visio фирмы Shapeware.
Автоматизация не предназначена исключительно для программистов на C++ и VBA. Ряд фирм уже объявил о создании совместимых с Автоматизацией Бейсик-под о бных языков, которые любой разработчик может лицензировать для включения в свое приложение, допус - кающее ту или иную степень программного управления. Появилась даже версия Smalltalk, поддерживающая Автоматизаци ю.
Клиенты и компоненты Автоматизации
При взаимодействии на основе Автоматизации четко проявляется иерархия типа «ведущий- ведомый» (master-slave). Ведущий — это клиент (контроллер^Автоматизации [Automation client (controller)!, а ведомый — компонент (сервер) Автоматизации^Automation component (server)]1. Клиент инициирует взаимодействие, создавая объект компонента (для этого может понадобиться запуск программы компонента) или подключаясь к существующему объекту и уже выполняемой программе. После эт ого клиент вызывает функции интерфейсов компонен - та и, закончив свои операции, освобождает интерфейсы.
Вот несколько сценариев такого взаимодействия.
C++-клиент Автоматизации использует в качестве компонента приложение Microsoft или сторонней фирмы. Эго приводит к исполнению VBA-кода в приложении компонента.
С4+-компонеит Автоматизации вызывается из приложения Microsoft (или приложения на Visual Basic), которое выступает в качестве клиента. Тем самым, VBA-код создает объекты C++ и оперируете ними.
С++-компопепт Автоматизации используется C++» клиентом.
Программа па Visual Basic обращается кприложению, которое поддерживает Автоматизацию, например, к Excel. В этом случае Visual Basic — клиент, a Excel — компонент.
Microsoft Excel—лучший Visual Basic, чем сам Visual Basic
Во времена, когда были написаны первые три издания этой книги, Visual Basic мог быть клиентом Автоматизации, но не годился для создания компонента. Но, начиная с версии 5.0, Visual Basic позволяет писать и компоненты. Поначалу мы использовали Microsoft Excel вместо VB только потому, что Excel был первым приложением Microsoft, способным поддерживать синтаксис VBA и выступать в роли как контроллера, так и сервера. Но теперь мы используем Excel, может быть, потому, что хотим убедить программистов на C++, которые морщатся при одном упоминании о Visual Basic, приобрести Excel, — хотя бы для того, чтобы вести учет своих гонораров.
Настоятельно рекомендуем приобрести копию Excel 97 (или более позднюю). Это настоящее 32-разрядное приложение из комплекта Microsoft Office, С его помощью Вы сможете писать в отдельном модуле VBA-код, осуществляющий доступ к ячейкам электронной таблицы в объектно-ориентированном стиле. В него можно легко добавить и такие визуальные элементы, как кнопки. Словом, забудьте о старых электронных таблицах, заставлявших Вас втискивать макросы в ячейки.
Можно считать, что «клиент», «контроллер» и «контейнер» в СОМ — это одно н тоже. — Прим, перев.
Эта глава — вовсе не учебник по Excel, но мы включили в нее пример с рабочей книгой (workbook) Excel. (Рабочая книга — файл, содержащий одну или несколько таблиц и отделенный от них VBA-код.) Эта рабочая книга демонстрирует VBA-макрос, исполняемый щелчком кнопки. Вы можете загрузить файл Dcmo.xls в Excel из каталога \vcpp32\ex25a или набрать его текст самостоятельно. На рисунке 25-1 показана электронная таблица с кнопкой и тестовыми данными. Выделите ячейки с А4 по А9 и щелкните кнопку Process Col. Программа на VBA «пройдет» по столбцу и заштрихует ячейки с числами, больше 10.
На рисунке 25-2 показан собственно кед макроса, который расположен отдельно от электронной таблицы. В Excel 97 выберите из меню Tools команду Macro, а затем выберите Visual Basic Editor (быстрая клавиша Alt-Fl 1). Как видите, с этого момента мы работаем и стандартном окружении VBA 5.0.
Рис. 25-1. Электронная таблица Excel, использующая VBA
Рис. 25-2. VBA-коддляэлектроннойтаблицы Excel
Если Вы хотите подготовить этот пример самостоятельно, выполните следующие действия.
Запустите Excel с новой рабочей книгой, нажмите Alt-Fl 1, а затем дважды щелкните Sheet! в левом верхнем окне.
Наберите код макроса, показанный на рисунке 25-2.
Вернитесь в окно Excel, выбрав из меню File команду Close And Return То Microsoft Excel. Выберите команду Toolbar из меню View. Для вывода на экран панели инструментов Forms отметьте флажок Forms. (Список доступных панелей инструментов можно получить, щелкнув правой кнопкой мыши любую из находящихся на экране инструментальных панелей.)
Щелкните элемент управления Button и создайте кнопку, перетащив ее мышью в левый верхний угол таблицы. Назначьте этой кнопке макрос SbeetProcessColumn.
От регулируйт е размер кнопки и сделайте на ней надпись Process Col, как на рисунке 25-1.
Введите любые числа в столбец, начиная с ячейки А4- Затем выделите ячейки, содержащие эти числа, и щелкните кнопку, чтобы протестировать программу.
Про анализируем один из операторов Excel VBA из нашего макроса:
Selection.Of'set (1, 0).Range("А1").Select
Первый элемент (Selection) — это свойство не указанного явно объекта-приложения Excel. В данном случае подразумевается, что значением этого свойства является объектЛяп^е, представляющий прямоугольный блок ячеек. Второй элемент (Offset) является свойством объекта Range, который возвращает другой объект Range, заданный двумя параметрами. Здесь возвра ♦ щаемый объект Range — блок из одной ячейки, начинающийся на строку ниже исходного Range. Третий элемент (Range) — это метод объекта Range, возвращающий еще один блок ячеек, — па этот раз левую верхнюю ячейку во втором блоке. И, наконец, метод Select заставляет Excel выделить указанную ячейку и присвоить ее свойству Selection приложения.
По мере прохождения цикла предыдущий оператор смещает выделенную ячейку в таблице на строку за одно повторение. К такому стилю программирования еще надо привыкнуть, но игнорировать его нельзя. Главное — всеми средствами электронной таблицы и графического ядра Excel теперь можно управлять из встроенной среды программирования.
Свойства, методы и наборы
Различие между свойством и методом в какой-то мере искусственно. По сути, свойство — это некое значение, которое можно установить и считать, как свойство Selection приложения Excel. Другой пример — свойство Width, применимое ко многим объектам Excel. Отдельные свойства Excel можно только считывать, но большинство — и считывать, и устанавливать,
У свойств вроде бы нет параметров, но у некоторых есть индекс, во многом аналогичный параметру. Он необязательно должен быть целым числом и может содержать более одного элемента (скажем, строку и столбец). Вы не найдете индексируемых свойств в объектной модели Excel, по Excel VBA способен работать с индексируемыми свойствами в компонентах Автоматизации.
Методы более гибки, чем свойства. У них может быть несколько параметров (или вообще никаких параметров), они могут присваивать или считывать данные объекта и выполнять какие-то действия, допустим, открывать окно. Примером метода, реализующего определенное действие, служит метод Select приложения Excel
Excel поддерживает объекты-наборы (collection objects), например, объект Sheets, возвращаемый свойством Worksheets объекта Application, который содержит все электронные таблицы активной рабочей книги, Чтобы получить конкретный объект Worksheets набора, можно воспользоваться свойством Item (с целочисленным индексом) или просто применить индекс непосредственно к набору.
Задачи, решаемые Автоматизацией
Вы уже знаете, что СОМ-интерфейс — идеальный способ взаимодействия двух Windows-программ, но Вы знаете и то, что разрабатывать собственные СОМ-интерфейсы в большинстве случаев, как минимум, непрактично. Автоматизация предоставляет интерфейс общего применения IDispatch,удовлетворяющий программистов как на C++, так и на VB А. Наверное, Вы уже догадываетесь, бросив первый взгляд на Excel VBA, что этот интерфейс работает с объектами, методами и свойствами.
Можно создавать СОМ-интерфейсы с функциями, имеющими параметры и возвращаемые значения любого заданного Вами типа. Пример тому — IMotionn IVisuaina главы 24. Но если Вы собираетесь допустить к своему компоненту программистов на VBA, то не спешите и не проявляйте небрежность. Проблему взаимодействия можно решить за счет одного интерфейса с единственной функцией-членом, достаточно «сообразительной», чтобы поддерживать методы и свойства так, как нужно VBA. Разумеется, в IDispatch есть такая функция — Invoke. Вызов ЮйраГсЬ-Лпуокеиспользуется для COM-объектов, с которыми могут оперировать программы и на C++, и на VBA.
Теперь, надеемся, Вы начинаете понимать з адачи, решаемые Автоматизацией. Она пускает поток межмодульных взаимодействий по руслу IDispatch::lnvokeA каким образом клиент впервые подключается к компоненту? Поскольку/Уйра/сй — всего лишь еще один СОМ-интерфейс, то логика регистрации, поддерживаемая СОМ, применима и в этом случае. Компонентами Автоматизации могут быть как DLL-, так и ЕХЕ-модули; доступ к ним возможен даже по сети посредствомряспрейелвнной COM (Distributed COM, DCOM).
Интерфейс IDispatch
IDispatch — «сердце» Автоматизации. Этот интерфейс, как и остальные стандартные СОМ- интерфейсы, полностью поддерживается COM-маршалингом (т. е. реализацию его маршалинга уже написали разработчики из Microsoft) и библиотекой MFC. В компоненте должен быть COM-класс с интерфейсом IDispatch (и подходящая фабрика классов). Клиент получает указатель на IDispatch так, как это принято в СОМ.
Вспомните, что главной функцией-членом IDispatch является Invoke. Заглянув в интерактивную документацию Visual C++, Вы обнаружите, что у IDispatcb-InvokesoHCTHHy кошмарный набор параметров. Но не думайте о них сейчас. Библиотека MFC действует с обеих сторон вызова Invoke, применяя схему вызова функций компонента на основе параметров таблицы диспетчеризации, которую Вы определяете через макросы.
Invoke — не единственная функция IDispatch. Другая функция, которую может использовать контроллер, — GetlDsOJNames.C точки зрения программиста на VBA свойства и методы имеют символьные имена, но программисты на C++ предпочитают более эффективные целочисленные индексы3. Invoke задает свойства и методы целыми числами, поэтому функция GetlDsOJNames очень полезна в начале программы, позволяя преобразовать все имена в индексы, если последние не известны при компиляции. Как Вы уже видели, /ОйрщсЬподдержи- вает символьные имена для методов. Но этот интерфейс поддерживает символьные имена и для параметров методов. Эти имена функция GetlDsOJNames возвращает вместе с именем метода. К сожалению реализация IDispatch в MFC не поддерживает параметры с символьными именами.
Варианты программирования Автоматизации
Допустим, Вы собираетесь написать, компонент Автоматизации на C++. Прежде чем это сделать, ответьте на рад вопросов: каким должен быть компонент — внутренним или внешним? какой нужен пользовательский интерфейс? и нужен ли вообще? разрешать ли запускать ЕХЕ-компонент как автономное приложение? если компонент будет в ЕХЕ-моду- ле, то в каком именно: с MDI- или с SDl-интсрфсйсом? допустимо ли прямое завершение программы компонента?
Если компонент разместить в DLL, связь на основе СОМ окажется эффективнее, чем в случае ЕХЕ-компонента, поскольку не нужен никакой маршалинг. Однако в DLL возможности пользовательского интерфейса крайне ограниченны. Практически единственное, что доступно — это модальное диалоговое окно. Если же Вам нужен компонент, у которого есть свое собственное дочернее окно, то нужно использовать ActiveX-элемент, а если Вы хотите использовать окно-рамку, то придется создать внешний компонент. Как и обычная 3 2-разрядная DLL. DLL Автоматизации проецируется на адресное пространство клиентского процесса. Если два клиента используют одну и ту же DLL, Windows загружает и динамически связывает ее дважды. Оба клиента при этом и не «подозревают», что каждый из них работает с одним и тем же компонентом..
В случае ЕХЕ-компонента надо быть очень внимательным и различать понятия «программа компонента» и «объект компонента». Когда клиент вызывает IClassFaaorytCreatelnstance для создания объекта, он создается фабрикой класса в компоненте, но при этом СОМ может как запускать, так и не запускать программу компонента.
Вог некоторые из возможных сценариев.
Создаваемый СОМ класс компонента запрограммирован так, что для создания нового объекта требует запуска нового процесса. В этом случае СОМ будет запускать новую копию в ответ на второй и все последующие вызовы Createlnstance, каждый из которых возвращает указатель на IDispatch.
Особый вариант предыдущего сценария, специфичный для MFC-приложений. Класс компонента — это MFC-класс документа в SDI-приложении. Всякий раз, коща клиент вызывает’ Createlnstance, запускается новая копия приложения компонента— с новыми объектами «документ», «вид* и основным окном-рамкой SDL
Класс компонента допускает создание нескольких объектов в одном процессе. Всякий раз. когда контроллер вызывает Createlnstance, создается новый объект компонента. Но при этом запущена только одна копия процесса компонента.
Особый вариант предыдущего сценария, специфичный для приложений MFC. Класс компонента — MFC-класс документа в MDl-приложении. Выполняется одна копия компонен- тского приложения с одним основным окном-рамкой MDL Всякий раз, когда клиент вызывает Createlnstance, создаются новые объекты «документ» и «вид» вместе с новым дочерним окном- рамкой MDL
И еще один, более интересный случай: ЕХЕ-компонент уже выполняется, когда клиент решает обратиться к уже существующему объекту. Это как раз относится к Excel. Когда кли- ' енту потребуется доступ к единственному объекту-приложению Excel, оно может быть в свернутом виде на рабочем столе. Тогда контроллер вызывает COM-функцию GetActiveObject,которая возвращает указатель на интерфейс уже существующего объекта. Если вызов неудачен, контроллер может создать новый объект через CoCreatelnstance.
При уничтожении объекта компонента действуют обычные правила СОМ. У объектов Автоматизации есть счетчики ссылок, и эти объекты самоуничтожаются, когда контроллер вызывает Release, а значение счетчика достигает нуля. В случае MDI-компонента, если объект
автоматизации — MFC-документ, удаление последнего вызывает закрытие соответствующего дочернего MDI-окна. В случае SDI-компонента удаление документа приводит к завершению процесса компонента. Клиент сам отвечает за то, чтобы перед своим завершением вызывать Release для каждого интерфейса IDispatch. Если клиент, работавший с ЕХЕ-компонентом, завершился, не освободив интерфейсы, вмешивается СОМ и закрываеткомпонентский процесс. Но лучше не полагайтесь на вмешательство СОМ, а убедитесь, что ваш контроллер выполняет корректную очистку!
В общем случае клиентское приложение зачастую получает несколько указателей на интерфейсы одного объекта. Вспомним пример с космическим кораблем из главы 24, где у класса в модели COM-компонента были указатели на IMottonn IVisual. Но при использовании Автоматизации приложение обычно имеет по одному указателю ^IDispatcb^na объект. Как всегда при программировании на основе СОМ надо позаботиться об освобождении всех указателей на интерфейсы. В Excel, например, многие свойства возвращают указатель па IDispatch для новых или существующих объектов. Если Вы забудете освободить указатель на интерфейс внутреннего COM-объекта, отладочная версия библиотеки MFC сообщит об утечке памяти при завершении клиентской программы.
IMFC-реализация IDispatch
В программе компонента интерфейс IDispatch может быть реализован несколькими способами. В большинстве случаев эти реализации возлагают основную работу на DLL поддержки СОМ в Windows, вызывая COM-функцию CreateStdDispatch или передавая вызов Invoke интерфейсу /7у/м?/п/о^соторый использует библиотеку типов (type library) компонента. Она представляет собой таблицу, адрес которой хранится в реестре и которая позволяет клиенту запрашивать у компонента символьные имена объектов, методов и свойств. У клиента может быть средство просмотра библиотеки типов, позволяющее исследовать возможности компонента.
Библиотека MFC поддерживает библиотеки типов, но не применяет их в своей реализации IDispatch, основанной на использовании таблицы диспетчеризации (dispatch map). MFC- программы не вызывают CreateStdDispatch и не используют библиотеку типов для IDispatch;:Get- IDsOfNames?. значит, MFC не годится для создания многоязычных компонентов Автоматизации, которые могли бы одновременно поддерживать, скажем, английские и немецкие имена свойств и методов. (CreateStdDispatcbtoxe не поддерживает многоязычные компоненты.)
Позднее Вы узнаете, как клиент может применять библиотеку типов, и как AppWizard и ClassWizard создают такие библиотеки и работают с ними. Если у вашего компонента есть библиотека типов, клиент может просматривать ее независимо от вида реализации IDispatch. Компонент Автоматизации на базе MFC
Посмотрим, что происходит в компоненте Автоматизации (в данном случае это упрощенная версия программы-будильника ЕХ25С, которую нам еще предстоит обсудить). Реализация IDispatch в библиотеке MFC включена в базовый класс CCmdTarget.TaK что макросы INTER- ЕАСЕ_МА1Иамне понадобятся. Класс компонента Автоматизации, например CClock, объявляется как производный от ССтсПатЕрГд СРР-файл этого класса содержит макросы DISPATCH_MAP\ BEGIN DISPATCH MAP(CClock, CCmdTarget)
DISP_PROPERTY(CClock. "Time", «Mime, VT_DATE).
DISP PROPERTY PARAM(CClock, "Figure", GetFigure, SetFigure, VT_VARIANT, VTS_I2)
DISP_FUNCTION7cClock. "RefreshWin", Refresh, VT EMPTY, VTS NOME)
DISP FUNCTION(CClock, •SnowWin", ShowWin, VT BOOL, VTS 12)
END.DISPATCILMAFO
Похоже на таблицу сообщений MFC, не так ли? Заголовочный файл класса CClock содержит связанный с макросом код:
16 3.1К. № 787
public:■
DATE m_time;
afx_msg VARIANT GetFigure(short n);
afx_msg void SetFigure(short n. const VA1IAM& vaNew);
afx„msg void Refresh();
afxjnsg BOOL ShdwWin(short n);
DECLARE_DISPATCH„MAP()
Что все это значит? Ато, что у класса CClock имеются следующие свойства и методы.
Имя
Тип
Описание
Time
Свойство
Прямо связано с переменной-членом m_time этого класса
Figure
Свойство
Индексируемое свойство, доступное через функции-члены GetFigurea SetFigure; первый параметр — индекс, второй (для SetFigure') — строка. (В строках хранятся цифры «XII», «III», ^Vl» и «IX», отображаемые на циферблате масон.)
RefreshWin
Метод
Связан с функцией-членом Refresh; нет ни параметров, ни возврашае- могозначения'
ShowWin
Метод
Связан с функцией-членом ShowWin; параметр — короткое целое, возвращается булею значение
Какова связь между таблицейдиспетчеризации MFC, IDispatch и функцией -членом Invoke? Макросы таблицы диспетчеризации генерируют статические структуры данных, считываемые MFC-реализацией Invoke. Контроллер получает указатель на IDispatch для CClock (связь осуществляется через базовый класс CCmdTargefp. вызывает Invoke, передавая ей в качестве параметров массив указателей, MFC-реализация Invoke, упрятанная ще-то в CCmdTarget,использует таблицу диспетчеризации CClock для расшифровки переданных ей указателей и либо вызывает одну из функций-членов, либо осуществляет прямой доступ ■K.mjime.
Когда мы.дойдем до примеров, Вы увидите, что ClassWizard способен сгенерировать класс компонента Автоматизации и помочь построить таблицы диспетчеризации.
КлиентАвтоматизации набазе MFC
Теперь перейдем к клиентской стороне Автоматизации. Каким образом клиент Автоматизации, написанный на базе MFC, вызывает 7«?л;^е?Для этого MFC-библиотека предоставляет класс COleDispdtchDriver.y этого класса есть переменная-член mJpDispatcb.b который хранится указатель на IDispatch соответствующего компонента. Чтобы оградить Вас от сложной схемы передачи параметров в функцию Invoke, предусмотрено несколько функций-членов класса COleDispatcbDriver,"b том числе InvokeHelper,GetProperyu SetProperly. Каждая из них вызывает Invoke через указатель на IDispatch, связанный с компонентом и хранящийся в объекте COleDispatcbDriver. ,.
Предположим, в программе-клиенте есть класс CClockDnver, производный от COleDispatchDriver и управляющий объектом CClock в компоненте Автоматизации. Функции, считывающие и записывающие свойство Time, выглядят так:
DATE CCloekDriver::GetTime()■
{'
. DATE result;
GetProperty(1, VT_DATE, (void-O&resuit); return result;
}• '
void CCloekDriver::SetTimc(DATE propVal)
(
SetProperty(1, VT_DATC, propVal);
■
Аналогичные функции для индексируемого свойства Figure:
VARIANT CClockDriver: :GetFigure(short i)
{ VARIANT result;'
static BYTE parms[] = VTS_I2;
InvckeHelper(2, DISPATCH_PROPERTYGET. VT_VARIANT, (void*)&result, paries, i);
return result;
J '
void CClockDriver::SetFigure(short i, const VARIANTS propVal)
static BYTE parms[] = VTS_I2 VTS_VARIANT;
lnvokeHelper(2, DISPATCH_PROPERTYPUT, VT_EHPTY, NULL, parms, i. SpropVal);
}'
И, наконец, функции для доступа к методам компонента:
void CClockDriver::RefreshWin()
lnvokeHelper(3, DISPATCH METHOD, VT EMPTY, NULL, NULL);
}
BOOL CClockDriver: :ShovMin(short i)
BOOL result;
static BYTE parms[] = VTS 12;
InvokeHelper(4, DISPATCH_METHOD, VT_BOOL, (void*)Sresult, parms, i);
return result;
Параметры функции задают свойство или метод, его возвращаемое значение и параметры. Чуть позже Вы подробнее познакомитесь с параметрами функции диспетчеризации, а пока обратите особое внимание на первый параметр функций InvokeHelperjGetProperyR SetProperty. Это уникальный целочисленный индекс, так называемый диспетчерский идентификатор (dispatch ID, DISPID) свойства или метода. В приведенном примере эти идентификаторы можно задать при компиляции, так как компонент заранее известен. Если же Вы используете сервер Автоматизации с таблицей диспетчеризации, то идентификаторы (индексы) определяются по порядку элементов в таблице, начиная с 1. Неизвестные диспетчерские идентификаторы компонента можно определить по символьным именам свойств или методов, воспользовавшись функцией-членом IDispatch::GetIDsOjNames.
Схема на стр. 450 иллюстрирует взаимодействие клиента (контроллера) и компонента. Сплошными линиями показаны реальные связи посредством базовых классов MFC и функции Invoke, а пунктирными — логические связи между элементами классов клиента и компонента.
У большинства компонентов Автоматизации есть файл двоичной библиотеки типов с расширением TLB. ClassWizard может использовать эту библиотеку типов для автоматической генерации класса, производного от COleDispatchDriver. Такой сгенерированный класс контроллера содержит функции-члены для всех методов и свойств класса с жестко заданными диспетчерскими идентификаторами. Иногда код, сгенерированный ClassWizard, требует модификации вручную, но лучше так, чем самому писать все функции.
После генерации класса контроллера объект этого класса встраивается в класс «вид» (или какой-либо иной класс) клиентского приложения примерно так:
CClockDriver m_clock;’
■юнтроллер Автоматизациикомпонент Автоматизации
Затем делается запрос к СОМ для создания объекта компонента:
i_clock.CreateDispatct("Ех25с.Document");
Теперь мы готовы к вызовам функций компонента:
m_clock.SotTime(C01eDateTime. :Ge~CurrentTime());'
in_clock. FefreshWih();
Когда объект indock покидает область видимости, его деструктор освобождает указатель на IDispatch.
Использование директивы компиляции #import клиентом Автоматизации
Теперь появился совершенно новый способнаписанияклиентскихпрограмм Автоматизации. Вместо того чтобы воспользоваться ClassWizard для генерации класса, производного от COleDispatcbDricer, можно заставить компилятор создать файлы заголовков и реализации непосредственно из библиотеки типов. В нашем примере клиентская программа содержит следующую инструкцию:
«import "..\ex25c\dcbig\ex25c.tlb" rename_namespace("ClockDriv") using namespace ClockDriv;
Компилятор в этом случае создаст (и затем обработает) в подкаталогах проекта Debug или Release два файла: ex25c.tlh и ех25с.Ш. TLH-файл содержит объявление управляющего класса /£х25сплюс объявление так называемого интеллектуалъногоухазателя (smart pointer): _COH_SMARTPTR_TYPEOEF(IEx25c, _uuidof( IDispatch));
Макрос _СОМ_5Л4АЙТРТК_ТУРЕВЕ$енерирует тип IEx25cPtr, который инкапсулирует указатель на IDispatch компонента. TLI-файл содержит встраиваемую (inline) реализацию функций-членов, несколько примеров которых приведены ниже:
-fline HRESULT IEx25c: ;RefreshWin ( ) {
return „com.dlspatci method(this, Cx4. DISPATCH METHOD. VT EMPTY, NULL, NULL);
inline DATE IEx25c. GetTime f ) {
DATE „result;
com dispatch_propget(this, 0x1, ¥T_3ATE, (void*)&_resu11); return „result;
inline void IEx25c; ;PutTime ( DATE val ) { com dispatch propput(this, 0x1, VT DATE, val);
}- - -- - .
Обратите внимание на сходство этих функций с функциями COleDispatcbDriver, которые мы уже рассматривали. Функции comjlispatchjnetbod, сот dispatch_propgett. com dispatch prop- put находятся в библиотеке периода выполнения.
В клиентской программе переменная-член объекта smart-указателя встраивается в класс ♦вид» (или иной класс) приблизительно так:
IEx25cPtr m_clock;
затем создается объект компонента при помощи оператора:
m_clock.CreateInstance(__uuidof(Document));
Теперь для вызова функций-членов, определенных в TLI-файле, можно воспользоваться переопределенным оператором -> класса 1Ех25сРт
m_clock->PutTime(C01eDateTlme::GetCurrenrTime());
m,clock->RefreshWln();
Когда объект smart-указателя m_clock покидает область видимости, его деструктор вызывает СОМ-функцию Release.
Директива * import — будущее COM-программирования. Вы увидите, каке каждой новой версией Visual C++ возможности СОМ будут все больше проникать в компилятор наряду с собственно архитектурой «документ-вид».
Тип данных VARIANT
Несомненно, Вы обратили внимание на тип VARIANT коде клиента и компонента из предыдущего примера. VARIANT — это универсальный тип данных, используемый IDispatch:Invoke для пересылки параметров и возвращаемых значений. Он очень хорошо подходят для обмена данными с VBA. Взгляните на упрощенную версию опред еления типа VARIANT в заголовочных файлах Windows:
struct tagVARIANT
{
VARTYPE vt; //
беззнаковое i
короткое целое - код типа
WORD wfleservedl, wReserved2,
wReservedS;
union {
short
iVal;
// VT_I2 короткое целое
long
IVal;
// VT_I4 длинное целое
float
fltVal;
// VT_R4 4-байтовое с плавающей запятой
■double
dblVal;
// VT_R8 8-байтовое с плавающей запятой
DATE
date;
// VT_DATE хранится как число
// типа double в фермате "дата, время"
CY
vtCY
// VT_CY 64-разрядное целое
BSTR
bstrVal;
// VT BSTR
IUnknown*
punkVal;
// VT_UNKNOWN
IDispatch *
pdispVal;
// VT_DISPATCH
short*
plVal;
// VT_BYREF!VT_I2
long*
plVal;
// VT BYREFIVT 14
float*
pfltVal;
// VT BYREFIVT R4
double*
pdblVal;
// VT BYREFIVT.R8
DATE*
pdate;
// VT BYREFIVT DATE
CY*
pvtCY;
// VT BYREF I VT_CY
BSTR*
pbstrVal;
// VT BYREFIVT BSTR
} };
typedef struct tagVARIANT VARIANT;
Как видите, VARIANT— это С-структура, содержащая код типа vt, несколько байтов-заполнителей и большое объединение (union) уже известных Вам типов. Если значение vt равно, например VTI2, то значение ИЙАУТследует считывать из поля tVal, содержащее двухбайтовое целое. Если же vt равно VT_R8, используется поле dblVal, где хранится 8-байтовое число двойной точности.
Объект VARIANT может содержать как собственно данные, так и указатель на них. Если в поле vt установлен 6htVT_ BYREF, для доступа к данным следует использовать указатели/иШ/, plValw т. д. Заметьте, что VARIANT может содержать указатель на IUnknown или IDispatch. Это значит, что в вызове Автоматизации можно передать целый COM-объект, но если Вы хотите, чтобы этот объект обрабатывался в VBA, класс объекта должен поддерживать интерфейс IDispatcb.
Строки — особый случай. Тип BSTR — еще один способ представления строк символов. Переменная этого типа является указателем на массив символов, заканчивающийся нулевым символом1. Количество символов в массиве хранится непосредственно перед его началом. Таким образом, переменная 5<Ш?может содержать неотображаемые символы, включая нулевые. Если имеется объект VARIANT с vt = VT_BSTR. содержимое памяти выглядит так;
Объект VARIANTОбъект VARIANTРаспределенная памятьРаспределенная память
Объект VARIANT
Объект VARIANT
Распределенная память
Распределенная память
Так как строка заканчивается нулевым символом, то с Й5Л-И?/можно работать как с обычным указателем на cbar, но при этом Вы должны быть очень внимательны к освобождению памяти. Этот указатель нельзя просто удалить, поскольку выделенная область памяти начинается со счетчика символов. Для размещения и удаления объектов BSTR в OLE предусмотрены функции SysAtiocStringH. SysFreeString.
ПРИМЕЧАНИЕ SysAIiocString — еще одна COM-функция, параметром которой служит строка двухбайтовых символов. Это означает, что все BSTR-строки содержат Unicode-символы, даже если Вы не определили макрос „UNICODE. Будьте внимательны2.
Для работы с VARIANT в Windows есть несколько полезных функций, в том числе приведенные в следующей таблице. Если VARIANT содержит BSTR, эти функции гарантируют правильное выделение и освобождение памяти. Функции Vanantlnit и VariantCiear устанавливают vtB VT_EMPTY. Все функции для VARIANT— глобальные и в качестве параметра принимают значение типа VARIANT*. .
Функция
Описание
Variantlnit
VariantClear
VaviantCopy
Инициализирует VARIANT
Очищает VARIANT
Очищает память, ассоциированную с VARIANT-получателем, н копирует в него
VARIANT-ИСТОЧНИК'
1 То есть с кодом, равным нулю. — Прим, перее.
1 Так как все BSTR-строки содержат UNICODE-символы, то с такими строками нельзя работать как с указателями на char, вопреки утверждению автора. Из этого же следует, что в конце строки находится два нулевых байта, а не один, как показано на рисунке. — Прим, перее.
ФункцияОписание'
VariantCopyhid Очищает память, ассоциированную с УАКАМЧуолучатслсм, и производит необходимые преобразования, чтобы скопировать VARIANT без флаги VT_BYREF
VarkmtCbarigeType Изменяет тип VARIANT-значения
Класс COIeVariant
Есть смысл написать класс-оболочку C++ структуры М4ЯЛ4ЛгГКопструктор'Класса вызывал бы Variantlnit,a, деструктор — Шпап(С!еаг,У такого класса мог бы быть конструктор для каждого стандартного типа, а также конструктор копии и оператор присваивания, которые использовали бы VariantCopy/При передаче управления за пределы видимости блока, в котором объявлен объект этого класса, происходил бы вызов деструктора, и память освобождалась бы автоматически.
Именно такой класс и написан командной разработчиков MFC, преимущественно для подсистемы объекты доступа к данным (Data Access Objects, DAO), описываемой в главе 32. Тем не менее, он отлично работает и с клиентами и компонентами Автоматизации. Вот упрощенное объявление класса:
class COIeVariant : public tagVARIANT
.// конструкторы
public:
COleVariant();
C01eVariant( const VARIANTS varSrc);
COleVariantfconst COleVariantS varSrc);
COleVari ant(LPCTSTR IpszSrc);
COleVariant(CStringi strSrc);
COleVarlant(BYTE nSrc);
COIeVariant(short nSrc, VARTYPE vtSrc = VT_I2 /;
COleVariantdong nSrc, VARTYPE VtSrc = VT_I4);
COIeVariant(float fltSrc);
C01eVariant(double dblSrc);■■
C01eVariant(const C01eDateTime& timcSrc);
// деструктор■.
-COleVariantQ; // освобождает BSTR
// операции' '
public::
void Clear(); // освобождает BSTR.
VARIANT Detach(); // подробности позже,■
void ChangeType(VARTYPE vartype, LPVARIANT pScr = NULL): '
Кроме того, предусмотрены операции сравнения, присваивания, приведения типов и дружественные операции вставки/извлечения для классов CArcbwen CDumpContext. Полное описание этого MFC-класса Вы найдете в интерактивной документации.■■
А теперь посмотрим, как класс COIeVariant может помочь в написании функции компонента GetFigpre, ссылку на которую Бы видели в приведенной выше таблице диспетчеризации. Предположим, компонент хранит строки для четырех цифр в переменной-члене: private:..."
CString m_strFigure[4J;-
Вот что нам надо сделать при прямом использовании структуры VARIANT: ■■
VARIANT CClock::GetFigure(short n) {
VARIANT vaResult;"
::VarlantInit(&vaResult);"
vaResult.vt = VT_BSTR:
// CStrlng::SysAllocString создает 8STR
vaResult. bstrVal = m_strFigure[n].AllccSysStrlng():
return vaResult; // копируем vaResult, не копируя BSTR // BSTR надо будет освободить позднее
} .-
А вот то же самое, но с использованием COleVariant:
VARIANT CClock: :GetFigure(short n)
return C01eVariait(m strFigure[n]).Detac.'i();
}"
Вызов функции COleVariant^Detach здесь совершенно необходим. Функция GetFigure создает временный объект, содержащий указатель па BSTR. Этот объект побитно копируется в возвращаемое значение. Если Вы не вызовете Detach, деструктор COleVariant освободит память, занимаемую BSTR, и вызывающий процесс получит VARIANT, указатель в котором ссылается просто на «мусор».
ТЯ№Ь\Тпараметры функций компонента, вызываемых через IDispatch, объявлены как const УАКимта-Ькуг^т функции тип указателя на К4А£4ЛГГвсегда можно преобразовать в указатель на COleVariant. Вот пример функции SetFigure:
void CClockServ: :SetFlgure(short n, const VARIANTS vaNew) {
COleVariantvaTemp;
vaTemp.ChangeType(VT_BSTR, (COleVariant*) 4vaNew);
m_strFigure[n] = valemp.bstrVal:
ПРИМЕЧАНИЕ Помните, что все ВЗГД-строки содержат двухбайтовые символы. В классе CString есть конструктор и оператор присваивания для типа LPCWSTR (указатель на двухбайтовые символы). Таким образом, строка m_strFigure будет содержать однобайтовые символы, хотя bstrVal и указывает на массив двухбайтовых символов.
У ТИ/ЙФУГ-параметров функций клиента, вызываемых через IDispatch, тот же тип: const VARIANTS,Вы можете передавать в них как VARIANT, так и объект COleVariant. Взгляните на такой вызов функции CClock: :SetFigure, текст которой был только что приведен:
pClcckDriver->SctFiguro(0. COleVariant("XII"));
ПРИМЕЧАНИЕ В Visual C++ 5.0 появилось два новых класса для BSTR и VARIANT, независимых от библиотеки MFC: _bstr_t и .variantJ. Первый инкапсулирует тип BSTR, второй — VARIANT. Оба класса корректно распределяют и освобождают ресурсы памяти. Более подробную информацию об этих классах Вы найдете в интерактивной документации.
Преобразованиятипов параметров и возвращаемыхзначений для/nvoke
Все параметры и возвращаемые значения 7/)иу>ащй.-./иуо&0Обрабатываются в ней как данные типа VARIANT. Помните об атом! Реализация Invoke в библиотеке MFC способна самостоятельно выполнять преобразования между VARIANT н любым переданным Вами типом (если такое преобразование возможно), что обеспечивает определенную гибкость в объявлении типов параметров и возвращаемых значений. Допустим, контроллерная функция GetFigure принимает и возвращает тип BSTR. Если компонент возвращает int или long, то все в порядке: OLE и
библиотека MFC преобразуют число в строку. А что если компонент принимает параметр long, тогда как контроллер передает inti И вновь никаких проблем.
ПРИМЕЧАНИЕ MFC-клиент Автоматизации задает ожидаемый тип возвращаемого значения как параметр VT_ функций GetProperty, SetProperty и /nvo/геНе/рег класса COIeDispatchDriver. MFC-компонент Автоматизации указывает ожидаемые типы параметров как параметры VTS_ в макросах DISP_PRO- PERTY и DISPJVNCTION.
В отличие от C++, в VBA нет строгого контроля типов. Переменные в VBA внутренне часто представляются как VARIANT. Возьмем, например, значение в ячейке электронной таблицы Excel. Пользователь может ввести в ячейку текстовую строку, целое значение, число с плавающей запятой или дату. VBA рассматривает данные в ячейке как VARIANT и. возвращает клиентам Автоматизации объект именно этого типа. Если возвращаемое значение функции клиента объявлено как VARIANT, контроллер может проверить значение vt и соответственно обработать данные.
В VBA применяется формат даты/времени, отличный от формата MFC-класса CTime. Переменные типа DATEхр анят и дату, и время как единое значение типа double. Дробная часть представляет время (0,25 соответствует 6 часам утра), а целая — дату (число дней, прошедших с 30 декабря 1899 года)1. Библиотека MFC предоставляет класс COIeDateTime, облегчающий работу с датами. Вы можете сформировать дату следующим образом:
COIeDateTime date(1998, 10, 1, 18, 0, 0);// 1 октября 1998, 6 часов вечера
В классе COleVanant определен оператор присваивания для COIeDateTime, а у COIeDateTime есть функции-члены для выделения компонентов времени и даты. Вот как можно вывести показания времени:
TRACEC'time = %d:<id:%d\n",
date.GetHourO, date.GetMinute(), date.GetSecond());
Если у Вас есть переменная VARIANT,содержащая значение типа DATE, то для преобразования даты в строку можно использовать COleVariant::CbangeType:
COIeVariant vaTimeDate = date;
COIeVariant vaTemp;
vaTemp.ChangeType(VT_BSTR, &vaTimeDate);
CString str = vaTemp. bstrVal;
TRACE("date = %a\n", str);
И последнее замечание о параметрах Invoke-, у функции, вызываемой через IDispatch,могут быть необязательные параметры (optional parameters). Если компонент объявляет тип последних параметров (крайних справа) как VARIANTto клиент не обязан передавать их. При таком вызове (без определения значения необязательного параметра) поле W объекта VARIANT на стороне компонента будет содержать VT_ERROR.1
Примеры Автоматизации
В оставшейся части главы будет приведено пять программ-примеров. Первые три — компоненты Автоматизации: ЕХЕ-компонснтбез пользовательского интерфейса, DLL-компонент и ЕХЕ-компоненткак SDI-приложение, допускающее одновременный запуск нескольких копий. К каждой из этих программ прилажена рабочая книга Microsoft Excel. Четвертый пример - это MFC-клиент Автоматизации, управляющий тремя вышеупомянутыми компонентами, а также приложением Excel при помощи класса COIeDispatchDriver. Последний пример — клиентская программа, в которой вместо класса COleDispatcbDrivemcaonbsyercH директива import.
Отрицательные значения соответствуют датам др 30 декабря 1899 года. — Прим, перса.
Пример ЕХ25А
Программа Autoclick — хороший пример MDI-приложения с объектом-документом в качестве компонента Автоматизации. (Вы можете найти этот пример в разделе Visual C++ Docu- mentation/Samples/MFC Samples/Tutorial Samples интерактивной документации.) Пример ЕХ25А отличается от Autoclick тем, что не имеет пользовательского интерфейса. В нем один класс, поддерживающий Автоматизацию, и к первой версии программы один процесс поддерживает создание нескольких объектов автоматизации. Во второй ее версии при каждом создании клиентом автоматизации нового объекта запускается новый процесс.
Пример ЕХ25А иллюстрирует типичное применение Автоматизации. Компонент, написанный на C++, реализует обработку финансовых транзакций. Программисты на VBA могут писать приложения с мощным пользовательским интерфейсом, которые будут полагаться на правила аудита, заложенные в логику компонента. В коммерческом варианте такого компонента мы, скорее всего, использовали бы базуданных, но ЕХ25Апроще. Банковские счета реализованы здесь за счет двух методов Deposit (положить на счет) и Withdrawal (снять со счета), а также свойства Balance (баланс), доступного только для чтения. Ясно, что метод Withdrawal не должен создавать отрицательный баланс. Для управления компонентом можно использовать Excel, как показано на рисунке 25-3- •
Рис. 25-3. Рабочая книга Excel управляет компонентом ЕХ25А
.Этапы создания программы ЕХ25 А.
Используя AppWizard, создайте проект ЕХ25А в каталоге \vcpp32\ex25a. Выберите на шаге 1 параметр Dialog Based. Откажитесь от всех параметров на шаге 2, а далее согласитесь со значениями по умолчанию. В результате Вы получите простейшее приложение, какое только умеет генерировать AppWizard.
Уберите из проекта класс диалогового окна. В Windows Explorer или в командной строке удалите файлы ex25aDlg.cpp и ex25aDlg.h. Уберите эти файлы и из проекта, удалив их в окне Workspace (вкладка FileView). Отредактируйте файл ех25а.срр: удалите оператор *includeyt5\a класса диалогового окна, а также весь связанный с диалоговым окном код в Initlnstance. В Resource-View ликвидируйте диалоговый ресурс IDD_EX25A_DWOG.
Добавьте код поддержки Автоматизации. В StdAtx.h добавьте строку:
^include <afxdlsp.h>
В файле ех25а.срр отредактируйте следующим образом функцию Initlnstance:
BOOL СЕх25аАрр::lnitlnstance()
{ .
Afx01eInit();
lf(RunEmbedded() | | RunAutomatedO) {
П компонент запущен СОМ
COleTemplateServer::RegisterAll(): return TRUE;
// Компонент запущен непосредственно пользователем COleObjectFactory::UpdateRegistryAll();
AfxMessageBox("Банковский сервер зарегистрирован"); return FALSE;
ч
■I
Используя ClassWizard, добавьте классСВапк.
Обязательно установите флажок Create able By Type ID.
Используя ClassWizard, добавьте два метода и свойство. Щелкните вкладку Automation и добавьте метод Withdrawal.
Параметр dAmount задает сумму, снимаемую со счета, а в возвращаемом значении сообщается фактически снятая сумма. Если Вы попробуете снять $100 со счета, на котором только $60, функция вернет значение 60.
Добавьте аналогичный метод Deposit, возвращающий значение типа void, а потом — свойство Balance.
Мы могли бы напрямую обращаться к переменной-члену компонента, но тогда нам не удалось бы обеспечить доступ к нему только для чтения. Поэтому мы выбираем переключатель Get/Set Methods и превращаем функцию SelBakmceB ничего не делающую заглушку.
Добавьте к классу CBank открытую переменную-член т_<Ша1апсе типа double. Поскольку мы выбрали для свойства Balance параметр Get/Set Methods, ClassWizardne сгенерирует переменную-член. В конструкторе CBank, расположенном в файле bank.cpp, присвойте этой переменной значение 0.0.
Отредактируйте сгенерированные функции для методов и свойств. Добавьте код, выделе нныйполужирным:
double CBank; : Withdrawal (double dAmount)
* if (dAmount < 0.0) {
return 0.0;
if (dAmount <= m_dBalance) {
m_dBalance -= dAmount;
return dAmount;
II
double dTemp = m_dBalance;
m_dBalance = 0.0;
return dTemp;
void CBank: :Deposlt(double dAmount)
< if (dAmount < 0.0) {
return;
m_dBalance += dAmount;
double CBank::Get8alance()
return m_dBalance;
TAACECK сожалению, это невозможно! \n");TAACECK сожалению, это невозможно! \n");void CBank: :SetBalance(double rewValue) {
TAACECK сожалению, это невозможно! \n");
TAACECK сожалению, это невозможно! \n");
Соберите программу ЕХ25А и запустите ее один раз, чтобы зарегистрировать компонент..
Подготовьте пять макросов Excel в новом файле cx25a.xls. Вот ихтекст:
Dim Bank As Object
Sub LoadBank()
Set Bank = Create0bject(”Ex25a. Bank")
End Sub.
Sub UnloadBankQ••
Set Bank = Nothing
End Sub
Sub DoDeposit()
Range("D4”).Select"
Bank. Deposit (ActiveCell.Value)
End Sub
Sub DoWithdrawalO
Range("E4").Select
Dim Amt
Amt = Bank.Withdrawal(AcvtiveCell. Value)
Range("E5").Select
ActiveCell.Value = Amt
End Sub
Sub DoInquiryO
Dim Amt•
Amt = Bank. Balance()"
Range("G4").Select ActiveCell.Value = Amt
End Sub
Подготовьте рабочую тетрадь Excel, как показано на рисунке 25-3- Закрепите макросы за кнопками, пользуясь правой кнопкой мыши.
Протестируйте банковский компонент ЕХ25А. Щелкните кнопку Load Bank Program, введите сумму депозита в ячейку D4 и щелкните кнопку Deposit. Щелкните кнопку Balance Inquiry и просмотрите баланс в ячейке G4. Введите в ячейку Е5 сумму, снимаемую со счета, и щелкните кнопку Withdrawal. Теперь опять щелкните кнопку Balance Inquiry, чтобы просмотреть текущий баланс.
ПРИМЕЧАНИЕ Иногда придется щелкать кнопки два раза подряд. Первый щелчок устанавливает фокус на таблицу, и только второй запускает макрос. Курсор в виде песочных часов подскажет, что макрос исполняется.
Что происходит в этой программе? Рассмотрим функцию CF.x25«App"InitInstance'E,cmi ее запустить прямо из Windows, она обновляет реестр, сообщает об этом в информационном окне и завершается. Функция COIeObjectFactory::LpdateReg{sh3AiipoB^^^ поиск глобальных объектов — фабрик классов; макрос IMPLEME^._OLECREATEwi класса CSank как раз и опре-
целяет такой объект. (Строка, содержащая IMPLEMENTjDLECREATEyeHepHpoBaHa ClassWizard, гак как мы выбрали для С8ян& параметр Createable By Type ID.) В Реестр добавляются уникальный идентификатор класса и программный идентификатор ЕХ25A.BAN К.
Когда Excel вызываетCreateObject,СОМ загружает программу ЕХ25А, содержащую глобальную фабрику для объектов СВйи&;затем СОМ вызывает функцию <7ге«ге/пйапсеобъекта-фаб- рики для создания объекта СВапк и возвращает указатель на IDispatch. Если пропустить несущественные подробности (и показанные ранее функции для методов и свойств), то сгенерированный ClassWizard класс СВапк выглядиттак:
class СВапк : public CCmdTarget <
OECLARE_DYNCREATE(СВапк)
public:
double m_dBalance;
CBank(); // защищенный конструктор ДЛЯ динамического создания
// Атрибуты public:
// Операции public:
// Переопределенные функции
// переопределенные виртуальные функции, сгенерированные ClassWizard
,//{{AFX_VlRTUAL(CBank)
public:
virtual void OnFinalReleaseO;
//) )AFX_VIRTUAL
// Реализация protected:
virtual ~CBank();
// сгенерированные функции таблицы сообщений
//{(AFX_MSG(CBank)
П ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять и удалять функции-члены.
//})AFX_MSG
DECLARE MESSAGE_MAP()
DECLARE_0LECREATE(C8ank)
// сгенерированные функции таблицы диспетчеризации OLE
//{{AFX_DISPATCH(СВапк)
afx_msg double GetBalance();
afxjnsg void SetBalance(couble newValue);
afx_msg double Withdrawal(double dAmount);
afxjnsg void Deposit(double dAmount);
,//}}AFX DISPATCH
DECLARE DISPATCH MAP()
DECLARE_INTERFACE_MAP()
А вот код, автоматически сгенерированный ClassWizard в файле bank.cpp:
IMPLEMEHT_D¥NCREATE(СВапк, CCmdTarget)
СВапк::СВапк() {
EnableAutomation();
// чтобы приложение не завершалось, пока активен объект Анюматизации, // конструктор вызывает AfxOleLockApp
■А,
AfxOleLockApoO;..
CBank::-CBankO■
{ // чтобы завершить приложение после того, как все объекты, созданные Автоматизацией, // уничтожены, деструктор вызывает AfxOleUnlockApp,
Afx01eUnlockApp():
void CBank::OnFinalRe]ease()
{
// OnFinalRelease вызывается, когда освобождается последняя ссылка на объект Автоматизации, // Эта реализация уничтожает объект. Добавьте операции очистки, необходимые перед удалением П из памяти вашего объекта.
CCmdTarget:: OnFinalRelease<);
BEGIN MESSAGE-MAP(CBank, CCmdTarget)
//{{AFX_MSG_MAP(CBank)
// ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять и удалять макросы таблицы сообщений
//}}AFX MSG MAP
END-MESSAGE МАР()
BEGIN DISPATCH MAP(CBank, CCmdTarget)
//"((AFX_DISPATCH_MAP(CBank)
DISP_PROPERTY_EX(CBank, "Balance", GetBalance, SetBalance, VT_R8)
DISP~FUNCTIQN(CBank, 'Withdrawal', Withdrawal, VT_R8, VTS_R8)
DISP FUNCTION(CBank, "Deposit", Deposit, VT_EMPTY, VTS_R8)
//}}AFX_DISPATCH_MAP
END_DISPATCH_HAP()
// Примечание: мы добавляем IID_IBank для проверки типов при связывании из VBA.
// Этот IID должен совпадать с GUID, который задан для dispinterface в ODL-файле.
// {A9515AB6-5B85-11D0-848F-00400526305B} static const IID IID_IBank =
{ 0ха9515яЬ6. Ox5b85, 0x11d0, { 0x84, Ox8f, 0x0, 0x40, 0x5, 0x26, 0x30, 0x5b } };
BEGIN INTERFACE MAP(CBank, CCmdTarget)
INTERFACE-PART(CBank, IID_IBank, Dispatch)
END_INTERFACE_MAP()
// {632B1E4C-F287-11CE-B5E3-00AAO05B1574}
IMPLEMENT_OLECREATE2(CBank, "EX25A.BANK", 0x632Me4c, Qxf287, Oxllce,
Oxb5, Qxe3, 0x0, Oxaa, 0x0, 0x5b, 0x15, 0x74}-
Эта первая версия программы ЕХ25А работает в режиме единственного процесса, как и программа Auto click. Если второй клиент Автоматизации запрашивает второй объект CBank, СОМ вновь вызывает функцию Сгеа£в/п$Гапсефабрики класса, и существующий процесс конструирует в куче новый объект CBank. Это можно проверить, создав копию рабочей книги ex25a.xls под другим именем и загрузив и копию, и оригинал. Щелкните кнопку Load Bank Program в обеих книгах и проследите за сообщениями в окне Debug. Процедура Initlnstance должна быть вызвана только один раз.■
Маленькое изменение в программе ЕХ25А меняет ее поведение. Чтобы при запросе каждого нового объекта запускался новый процесс ЕХ25А, выполните следующие действия.
Добавьте следующий макрос в bank.h:
«define IMPLEMENT 0LECREATE2(class_nane, external.name. \
w1, w2, M."b2, ЬЗ, Ь4, Ь5, Ь6, Ь7, Ь8) \
" AFXDATADEF COIeObjectFactory classjme:-.factory(с1азз_паше;:guid, \ RUNTlNE_CLASS(class_name), TRUE, _T(external_name)); \ const AFX DATADEF GUID class name::guid = \
{ 1, w1, w2, { Ы, Ь2, ЬЗ, Ь4, Ь5, Ь6, Ь7, Ь8 } };
Он ничем не отличается от стандартного макроса MFC IMPLEMENT^OLECREATEga исключением того, что параметр FALSE, стоявший после параметра RU^ПlME_CLASSизмeнeн. на TRUE.
В файле bank.cpp измените вызов макроса IMPLEMENTjOLECREATBa. вызов IMP- LEMENTOLECREATE2."
Соберите программу и протестируйте ее, используя Excel. Запустите две копии Excel и загрузите в каждую из них банковскую программу. Используя Task Manager в Windows NT или PWIEW95, убедитесь, что активны два процесса ЕХ25А.
ПРИМЕЧАНИЕ Программа ЕХ25А на прилагаемом компакт-диске использует макрос IMPLEMENT, OLECREATE2.
|1бтладка 'пр^равм^ЕХвЧййМ^Ш^ * • з ■ %:f j ^^it1^ т у;Ё7 ^ ■ ,, £:-.}:;.
■Ц При ^й1|у^к^ ЁХЕ^ксмпрнеща клиент Автоматизации указывает в командной строке ■ А|у|$ар|^й^^2^Вы должны^д елать то я® самое. Выбе
Й -рите Й Wiial ‘^Й^.И© M^S^Prcgect команду iSStogs Н.нд вюйдкё Pdsig в поде Program
IX Arguments’^‘едиге^Е^б&э'tt '’ ’ "“ ": ’ 4”
'О'ОZEebeddhgZEebeddhgVsVs^3№jj^3№jjй?1Н'1 f -^'sй?1Н'1 f -^'s4?4?айай№е^Й инщрУйентов(Ё^^1£ программа запустится и- ^б^тждат^, -кедда;^ ^^О^ЙРУ^клиёнг. Теперь мо&но Запустить из Witidbws>H кли- ^нтск^ ^ОгрЙт&ли^ не задушена) и созДа-тйте помощью компонент- ^^^да-обьекг/В резул^яте *!ррфрамма, запушенная ^-пж отладгзт:1^,данжца скоуструи^ /
'О
'О
ZEebeddhg
ZEebeddhg
Vs
Vs
^3№jj
^3№jj
й?1Н'1 f -^'s
й?1Н'1 f -^'s
4?
4?
ай
ай
|^^Не^&У^йНз.а^фр^й^аЕ^^^<^аммужомне найд ет. Это
|"^ЙР|6#у6;чуо Вы^дсрркн^ед^ее без параметра ^Bhbedding," Мнапв кли-
Йен™ sfetarcire^реестре, цр^теж^ ^сяй: ваТц .клиент уже работал в
у| момент регистрации 1^ШК>й^Т^>‘?го^ может быть,’ придется перезапустить. ‘'
■ ^ Передача параметров по осыпке ^ ^ 44"|-: ^{ *t^ ^'- ?4|^‘л5 Й^ 4: ? #< ^ V"1 ?bi '-■ '• f i t ‘^p' сих п<^ Вы ввд^й переда^’параметрс®?ф^Айд Значений.'Н^р'а^з^гЪ^Чтов’УВАL \2 весьма странные правила " вызова жтрД^й один'параметр^у-меуода т^г М03®0 с™®®^^ ?". скобки, более одного „— нельзй(но если'^ы использует® Л*^^Ч|А^|ррзнзйениё 5>унк7;-" ' ::ции,""скобки нео^^^ы ;-в любом сл^ае). Вот нескшькр;пример.ов: передачи ^.'УЙА ' ? < ;:стрркбвРг6 Тгарамётратго^начен^ 141 (111?» У Л ^^ if у4? f ^^ f^ff-| | •:
ЭД^|МвйЫТ oNf ЭШ1^И^ ^ 1 ^f HvI^H? (-Й 3 *; rtl e?r41^4 f V^ H^if Й^^Zv|
^||^$$<$4 ^ ;-^* ''текст" * * ■ рь^ЙМЙho^ls)
Й
: , ’ Тем не менее, иногда УВАпередает параметр как адрес : - дующем примере строка'передается по ц-*--^-—1 ^■’ 1 '
"г < 11
h№lkM
". Dim/s^^M.^ f ? ~;
s.;= ^xt"v 4 • . " >"
■^&^#^ий 'ИМ ;s
. i Правила, н^мсда^ёйые VBA no умрлчаншр/ &|>жно тй" ери^,^8|^й^|1^^
.параметром ключевые слова" ^Уег/илй ^Й^Крмпонешу заранее^ не известно,, будет ЛЙ ? парамер пере,!^ пд-&1|^^=• "* ', s^-'• ^'' -'-* •1 ■*
*
Si k<{ •! <
у =y ** • » ^-^-f' Зп1£
: л ! f т» -’-* •■* "■» н”1т ". Г ’I 1 '«К 41 V.-П Л V » У I .
ь поле г#. Вот прим ер, реализации метода, допускающего
. ТакиТто значению:^^v >5i$£« » ..il^.. ' '■ ■ г ’
void «Й^Н# ^#рй(Й^Й1$^Ш ^oost
String M^ fe ^ X<f 4^^ ( T«il -if '.(«(^аРапйШ .&" fl^wH^.^J^ffife^' ; ■ 'if ^feffii2M
V .Г
^■^ • ^5(|ileeMi8ek*G8t^
V, v« ;ЁслцЬМзетяй^айт^метр как J». е J -' *'4 • Г ^^•^ч•*lr:■t А» *А Iff «Г*» j*i «1V
К й?^а WriW^№^^ ^7 внешний компонент,- котррый^йзменяет гзначение_ с: 4^1®4ёёт доступа: кнамятиклиеэтскот^^^^%(^
|
ётв^Б.преобразования^са^оь й
tv
•1вш#1|о»авеа^ i "затем, едрляфЬ еще qg
;Ф^Шйй. даэтд^
^ссылок через 1ДЕфб^1(^^ ^ то» }г^1ПёфД%ча^^^
ей? Поскольку компонент *не | о^ша скопировать строку щя1 -
k^JS ^* Л: ^ * *ЙЛ ^
Пример ЕХ25В
ЕХ2 5 А можно легко переделать из ЕХЕ-варианта в DLL. Класс СВдпктфи этом не изменится, а макрос Excel будет очень похож. Однако интереснее написать новое приложение, на этот раз с минимальным пользовательским интерфейсом. Применим в нем модальное диалоговое окно, поскольку на практике это максимум того, что можно более-менее легко добавить в DLL Автоматизации.
ПрограммаЕХ25Вдовольнопроста. КласскомпонентаАвтоматизации, определяемыйпо регистрируем ому имени Ex25b. Auto, обладает следующими свойствамииметодами (см. табл, на стр. 464).
LongData TextData DisplayDialogLongData TextData DisplayDialogРис. 25-4. Диалоговое окно ЕХ25В DLL в действииРис. 25-4. Диалоговое окно ЕХ25В DLL в действии|^ Отладка DLL-компонента^^ ^п-7' . V ■ - '.. ' '' , 7 z ;{. : 1; т У i '|;Дгйс отладки DLL надо ссюбтщггьугладчику, какой ВХЕ-^айЛон доджендаг^узить. Вы .берите из меню Project' BVisual dM- команду Settings и lift вкладке РЙЧ^в Ноде Executable ‘ File For Debug Session введите* полное имя контроллера -(включая путьиEXE), какпоказанонижвна рисунке.v ' . 1 ■ ■■'"« ^ | ;| •• ’■ V|^ Отладка DLL-компонента^^ ^п-7' . V ■ - '.. ' '' , 7 z ;{. : 1; т У i '|;Дгйс отладки DLL надо ссюбтщггьугладчику, какой ВХЕ-^айЛон доджендаг^узить. Вы .берите из меню Project' BVisual dM- команду Settings и lift вкладке РЙЧ^в Ноде Executable ‘ File For Debug Session введите* полное имя контроллера -(включая путьиEXE), какпоказанонижвна рисунке.v ' . 1 ■ ■■'"« ^ | ;| •• ’■ VрасширениерасширениеWnKDekeWnKDeke1-1-,{^* -^^ Л *,{^* -^^ Л *^ jp >4 ?; V §i * ’^ jp >4 ?; V §i * ’нн44LL№№i • Щелчок кнопка Gp .на панели инструментов Debug запустит контроллер (что при" i .ведет и к загрузке вашей DLL), и тог будет ждать, пока Вы не активизируете компонент, i " ; Как только Вы активизируете его, DLL в отладчике должна создать компонентный " т j’oGl-ekL В конструктор этого объекта можно включить сОф&тд^ ЗПЙ^КНЬ не забудьте: 1, "прежде'чем клиент смажет загрузить ВТЕ-компонент, тот должен быть зареЕИСхрировам.Есть й^другой вариант, Если Вам доступен исходный’код клиентской программы, , можно запустить в отладчике ее; Ког^а клиент загрузит DLL-компонент, будет виден ■i • Щелчок кнопка Gp .на панели инструментов Debug запустит контроллер (что при" i .ведет и к загрузке вашей DLL), и тог будет ждать, пока Вы не активизируете компонент, i " ; Как только Вы активизируете его, DLL в отладчике должна создать компонентный " т j’oGl-ekL В конструктор этого объекта можно включить сОф&тд^ ЗПЙ^КНЬ не забудьте: 1, "прежде'чем клиент смажет загрузить ВТЕ-компонент, тот должен быть зареЕИСхрировам.Есть й^другой вариант, Если Вам доступен исходный’код клиентской программы, , можно запустить в отладчике ее; Ког^а клиент загрузит DLL-компонент, будет виден ■z вывод операторов Т№2& - ; ■z вывод операторов Т№2& - ; ■
LongData TextData DisplayDialog
LongData TextData DisplayDialog
Рис. 25-4. Диалоговое окно ЕХ25В DLL в действии
Рис. 25-4. Диалоговое окно ЕХ25В DLL в действии
|^ Отладка DLL-компонента^^ ^п-7' . V ■ - '.. ' '' , 7 z ;{. : 1; т У i '
|;Дгйс отладки DLL надо ссюбтщггьугладчику, какой ВХЕ-^айЛон доджендаг^узить. Вы .
берите из меню Project' BVisual dM- команду Settings и lift вкладке РЙЧ^в Ноде Executable ‘ File For Debug Session введите* полное имя контроллера -(включая путьи
EXE), какпоказанонижвна рисунке.v ' . 1 ■ ■■'"« ^ | ;| •• ’■ V
|^ Отладка DLL-компонента^^ ^п-7' . V ■ - '.. ' '' , 7 z ;{. : 1; т У i '
|;Дгйс отладки DLL надо ссюбтщггьугладчику, какой ВХЕ-^айЛон доджендаг^узить. Вы .
берите из меню Project' BVisual dM- команду Settings и lift вкладке РЙЧ^в Ноде Executable ‘ File For Debug Session введите* полное имя контроллера -(включая путьи
EXE), какпоказанонижвна рисунке.v ' . 1 ■ ■■'"« ^ | ;| •• ’■ V
расширение
расширение
WnKDeke
WnKDeke
1-
1-
,{^* -^^ Л *
,{^* -^^ Л *
^ jp >4 ?; V §i * ’
^ jp >4 ?; V §i * ’
н
н
4
4
L
L
№
№
i • Щелчок кнопка Gp .на панели инструментов Debug запустит контроллер (что при" i .ведет и к загрузке вашей DLL), и тог будет ждать, пока Вы не активизируете компонент, i " ; Как только Вы активизируете его, DLL в отладчике должна создать компонентный " т j’oGl-ekL В конструктор этого объекта можно включить сОф&тд^ ЗПЙ^КНЬ не забудьте: 1, "прежде'чем клиент смажет загрузить ВТЕ-компонент, тот должен быть зареЕИСхрировам.
Есть й^другой вариант, Если Вам доступен исходный’код клиентской программы, , можно запустить в отладчике ее; Ког^а клиент загрузит DLL-компонент, будет виден ■
i • Щелчок кнопка Gp .на панели инструментов Debug запустит контроллер (что при" i .ведет и к загрузке вашей DLL), и тог будет ждать, пока Вы не активизируете компонент, i " ; Как только Вы активизируете его, DLL в отладчике должна создать компонентный " т j’oGl-ekL В конструктор этого объекта можно включить сОф&тд^ ЗПЙ^КНЬ не забудьте: 1, "прежде'чем клиент смажет загрузить ВТЕ-компонент, тот должен быть зареЕИСхрировам.
Есть й^другой вариант, Если Вам доступен исходный’код клиентской программы, , можно запустить в отладчике ее; Ког^а клиент загрузит DLL-компонент, будет виден ■
z вывод операторов Т№2& - ; ■
z вывод операторов Т№2& - ; ■
НазваниеОписание
Свойство типа длинное целое
Свойство типа VARIANT
Метод без параметров, возвращает BOOL
/Н^/суДпя/о^тображает диалоговое окно для ввода данных (см. рис. 25-4). Макрос Excel передает ПЕЕзначения двух ячеек, а потом обновляет эти же ячейки новыми значениями.
Этот пример изначально сгенерирован как MFC Арр Wizard DLL с параметрами Regular DLL Using Shared MFC DLL и Automation. Вот операции, необходимые для подготовки и тестирования DLL-сервера ЕХ25В, используя код с прилагаемого к книге компакт-диска.
Откройте в Visual C++ рабочее пространство \vcpp32\ex25b\ex25b.dsw. Соберите проект.
Зарегистрируйте DLL, используя утилиту RegComp. Мажете использовать для этой цели программу RegComp из каталога \vcpp32\regComp\Release на прилагаемом к книге компакт-диске, которая открывает диалоговое окно, упрощающее выбор DLL-файла.
Запустите Excel и загрузите в него файл \vcpp32\ex25b\ex25b.xls. Введите целое число в ячейку СЗ и какой-нибудь текст в ячейку D3.
Щелкните кнопку Load DLL, затем — кнопку Gather Data. Введите данные, щелкните кнопку ОК и посмотрите на новые значения, появившиеся в электронной таблице.
Щелкните кнопку Unload DLL Если Вы запустили DLL (и Excel) из отладчика, просмотрите окно Debug и убедитесь, что в DLL была вызвана функция Exitlnsrance.
Теперь обратимся к коду ЕХ25В. Как и в MFC EXE, в обычной DLL на базе MFC есть класс приложения (производный от <Жп4др),а также глобальный объект-приложение Переопределенная функция Inittnstanceb ех25Ь.срр выглядит так:
800LCEx25bApp: :lnitlnstance() {
TRACE( "Входим в CEx25bApp: :Ini*In$tancE\n");
// Регистрируем фабрики OLE-сервера как функционирующие.
// Это позволит библиотекам OLE создавать объекты по запросу
//из других приложений
COleQbjectFactory::RegisterAll();
returnTRUE;
}
В программе есть и функция Etitlnstance, присутствующая исключительно для диагностики, а также код для трех стандартных экспортируемых функций COM DLL
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
AFXJANAGE_STATE(AfxGetStaticModuleState());
return AfxDHGetClassObject(rclsid, riid, ppv);
STOAPI DllCanUnloadNow(void) {
AFX„MANAGE_STATE(AfxGotStaticModuleState());
return Af xDHGar>UnloadNow();
см. след. стр.см. след. стр.)
см. след. стр.
см. след. стр.
STOAPI DllRegisterServerCvoid)
AFX_MANAGE_STATE(AfxGetStatlcModuleState()):
COleObjectFactory::UpdateRegistryAll();
VERIFY(AfxReglsterTypeLtb(AfxGetInstancellandle(), theTypeLibGUlD, "ex25b. tlb"));
return S_0K;
}
Код класса Фгот/^О^находится в файле PromptDl.cpp. Но это — стандартный класс, производный от CDialog. Файл PromptDLh содержит заголовок этого класса. Нам более интересен класс компонента Автоматизации CEx25bAuta,изначально сгенерированный Class Wizard (с параметром Createable By Type ID). Этот класс доступен СОМ под программным идентификатором Ex25bAuto. На рисунке 25-5 показан заголовочный файл ex25bAuto.h.
EX25BAUTO.H
■class CEx25bAuto : public CCmdTarget.
{
DECLARE_DYNCREATE(CEx25bAutp)
CEx25bAuto( ); H защищенный конструктор, используемый для динамического создания объектов
П Атрибуты'
public:_
П Операции public;
К Переопределенные функции
И переопределенные виртуальные функции, сгенерированные Classwizard //{(AFX_VIRTUAL(CEx25bAuto) public:
virtual void OnFinalReleaseO;
//11AFX.VIRTUAL
H Реализация protected:
virtual ~CEx25bAuto();
U сгенерированные функции таблицы сообщений
//{{AFX_MSG(CEx25bAuto)
// ПРИМЕЧАНИЕ: здесь ClassWizard будет добавлять и удалять функции-члены
//}}AFX_MSG
DECLARE MESSAGE МАР().
DECLARE„0LECREATE(CEx25bAuto)
// сгенерированные функции таблицы диспетчеризации OLE
//<{AFX„DISPATCH(CEx25bAuto)
long m_lbata;'
afx_irsg void OnLongDataChangedO:-
VARIANT m_vaTextData; ''
afx_msg void (JnTextOataChanged();
afxjnsg BOOL DisplayDialogO;
//}}AFX_DISPATCH
DECLARE DISPATCH MAP()
. DECIARE_INTFHFACE_MAP()
Рис. 25-5. Фрагмент заголовочного файла Ex25bAuto.h ’.
На рисунке 25-6 показан файл реализации ex25bAuto.cpp.
EX25BAUTO.CPP
«Include "StdAfx.h" ttinclude "ex25b.h"■
«include ''Ex25bAuto.h'‘
ttinclude "PromptDl. h”
ffifdef DEBUG
fiundef THIS_FILE
static char THIS_FILE[] = _FILE_; fiend if
////////////////////////////////////////////////////////////////////// // CEx25bAuto
IMPLEMENT_DYNCREATE(CEx25bAuto, CCmdTarget)
CEx25bAuto::CEx25bAuto()
EnableAutomation();
// чтобы приложение не завершалось, пока активен объект Автоматизации.
// конструктор вызывает AfxOleLockApp
: :VarianLlriit(&n_vaTextData); // необходимая инициализация
rr_lData = 0;
AfxOleLockAppO:
CEx25bAuto::~CEx25bAuto()
//чтобы завершить приложение после того, как все объекты, созданные Автоматизацией,
// удалены, деструктор вызывает AfxOleUnlockAop
Afx01eUnlockApp();
void CEx25bAuto;:OnFinalRelease(>
{'
// OnFinalRelease вызывается, когда освобождается последняя ссылка на объект Автоматизации.
// Эта реализация уничтожает объект. Добавьте операции очистки, необходимые перед удалением
// из памяти Вашего объекта.
CCmdTarget::OrFlnalRelease()
}
BEGlN_MESSAGE_MAP(CEx25bAuto, CCmdTarget)
П\ {AFX_MSG_MAP(CEx25bAuto)
// ПРИМЕЧАНИЕ: здесь QlassWizard будет добавлять и удалять макросы таблицы сообщений.
//}}AFX MSG MAP
END_MESSAGE_MAP()
BEGIN DISPATCH MAP(CEx25bAuto, CCmdTarget)
//{(AFX_DISPATCH_MAP(CEx25bAuto)
DISP_PR0PERTY_N0TIFY(CEx25bAuto, "LongData", m_lData, OnlongDataChanged, VT_14)
Рис. 25-6. Файл реализации Ex25bAuto.cppсм. след. стр.
Рис. 25-6. {продолжение)
0ISP_PR0PERTY_N0TIFY(CEx25bAuto, "ТехтСага", m.vaTextData, OnTextDataChanged, VT.VARIANT) MSP FUNCTION!CEx26bAutn, "OlsplayOialog", MsplayOialog, VT BOOL, VTS_NOHE) //}}AFX DISPATCH MAP'
END_DISPATCH_MAP()
j j Примечание: мы добавляем IID_IEx25bAuto для проверки типов при связывании из VBA.
// Этот IID должен совпадать с QUID, который задан для dispinterface в ODL-фаЙле.
// {A9515AD7-5B86-11D0-848F-00400526305B)
static const IIO IID lEx25bAuto =
{ Oxa9515ad7, 0x5b85, Ox11dO, { 0x84, 0x8f, 0x0, 0x40, 0x5, 0x26, 0x30, 0x5b } };
BEGIN INTERFACE MAP(CEx25bAuto, CCmdTarget)
INTERFACE_PART(CEx2SbAuto. IID IEx25bAuto, Dispatch)
END_INTERFACE_MAP()
// {A9515A08-5B85-11D0-848F-00400526305B}
IMPLEMENT_0LECREATE(CEx25bAuto, "ex25b.Auto", 0xa9515ad8, Ox5b85, OxlldO, 0x84, 0x8f, 0x0, 0x40, 0x5, 0x26, 0x30, 0x5b)
//////////////////////////////////////////////////////////////////////
// Обработчики сообщений для CEx25bAuto
void CEx25bAuto: :OnlongDataChanged() {
TRACE!"CEx25bAuto::OnLongDataChanged\n");
void CEx25bAuto: :OnTextDataChanged!)
{'
TRACE ("CEx25bAutQ; •,OnTextOataChanged\n");
BOOL CEx25bAuto: ;DisplayDialog() {
ТЯАСЕ("Входим b CEx25bALto: : DisplayDialog Kp\n", this);
BOOL bRet = TRUE;
AfxLockTempMaps(); // CM. MFC Technical Note #3
CWnd* pTopWnd = CWnd: :FromHandle(: :GetTopWindow(NULL));
try {
CPromptDlg dig /*(pTopWnd)*/;
if (m_vaTextData.vt == VT_BSTR){
dlg.m strData = m vaTextData.bstrVal; // преобразует двухбайтовые символы в однобайтовые
dlg.m_lData = m IData;
if (dlg.DoModalf) == IDOK.) {
m_vaTextData = C01eVariant(dlg.m_strData). Detach();
m_lData = dig.m_10ata;
bRet = TRUE;
}
else { bRet = FALSE;
}
I
catch(CExeption* pe) {
TRACE( "Исключительная ситуация: сбой в работе диалогового окна\п");
bRet = FALSE;'
pe->Delete();
AfxUnlocKTempMapsO; return bRet:
Свойства LongData и TextData представлены переменными-членами mJDatais. m_i;aText- Data, инициализируемыми в конструкторе. Когда Вы добавили в мастере ClassWizard свойство LongData, была определена уведомляющая функция OnLongDataCbanged. Она вызывается всякий раз, когда контроллер изменяет значение свойства. Такие функции можно задать только для тех свойств, что представлены переменными-членами. Не путайте эти уведомления с теми, которые AcrtveX-элемепты посылают своему контейнеру при изменении связанного свойства.
Функция-член DisplayDialog,реализующая одноименный метод, проста за исключением того, что для очистки указателей на временные объекты нужны функции АрхЬоскТегпрМардд. AjxUnlockTem.pMapip6HSHo такая операция осуществляется в холостом цикле ЕХЕ-программы).
А как насчет VBA-кода в Excel? Вот три макроса и глобальные объявления:
Dim Dllcomp As Object
Declare Sub CoFreeUnusedLibraries Lib "OLE32" ()
Sub LoadDllCompQ
Set Dllcomp = Create0bject(“Ex25b.Auto")
Range("C3").Select
01 Icomp.LongData = Selection.Value
Range("D3).Select
Dllcomp. TextData = Selection.Value
End Sub
Sub Ref reshOlIComp( } 1 кнопка Gather Data
Range("C3").Select
Dllcomp. LongData - Selection. Value
Range("D3). Select■
Dllcomp. TextOata = Selection. Value
Dllcomp.DisplayDialog
Range("C3").Select
Selection.Value = Dllcomp.LongData
Range("D3").Select
Selection.Value = Dllcomp.TextData
End Sub
Sub UnloadDllCompO
Set Dllcomp = Nothing
Call CoFrceUnusedLibraries
End Sub
Первая строка в LoadDUComp создает объект компонента в соответствии с зарегистрированным именем Hx25bAuto. Макрос /?^геяйО//СЪщробращается к свойствам этого объекта TextData и LongData. При первом запуске LoadDUComp загружается DLL и создается Ex25bAuto. При втором происходит нечто весьма любопытное: создается второй объект и удаляется первый. Запустив LoadDllComp из другой копии рабочей книги, Вы получите два отдельных объекта Ех25ЬАн?о.Конечно, в памяти находится только одна проекция ex25b.dll, если Вы не запустили несколько экземпляров Excel.
Давайте более внимательно рассмотрим макрос ЬЗДо^ЕМ/сГотрИсполнение оператора «Set Dllcomp = Nothings» заставляет Excel отсоединить DLL, но из адресного пространства Excel она не выгружается, а значит, функция компонента Exitlnslarice не вызывается. Функция СоГгее- UnusedLibraries вызывает для каждой компонентной DLL экспортируемую функцию DUCan- UnloudNouH., если функция возвращает TRUE, освобождает DLL. Программы на базе MFC вы-
зывают CoFreeUnusedLibrariesa холостом цикле (с минутной задержкой), но Excel этого не делает. Вот почему ип1оасЮ11Сотрдолжна вызывать CoFreeUnusedLibraries после отсоедине- ниякомпонента.
ПРИМЕЧАНИЕ Функция CoFreeUnusedLibrariesв Windows NT 3.51 ничего не делает, если не установлен Service Pack 2 (SP2).
Пример ЕХ25С
Данный пример компонента Автоматизации иллюстрирует применение класса документа в качестве компонентного класса в SDI-приложеиии, допускающем одновременный запуск нескольких своих копий. Здесь также демонстрируется индексируемое свойство и метод, конструирующий новый СОМ-объект.
В первом компоненте Автоматизации — Ех25А — не было пользовательского интерфейса. Глобальная фабрика классов создавала объект CBank, который делал всю работу. А что если нужен EXE-компонент с окном? Если Вы уже разбираетесь в архитектуре «документ-вид», поддерживаемой MFC, то, несомненно, захотите использовать все ее преимущества.
Допустим, Вы создали обычное MFC-приложение и добавили к нему класс (например. С Bank), который можно создать посредством СОМ. Как связать объект СВапк с документом и его представлением? Из функции-члена класса СВапк Вы могли бы перейти через объект- приложение и основное окно к текущему документу или объекту «вид», но в MDI-приложении это не так-то просто — особенно при наличии нескольких объектов и документов. Есть способ получше: предусмотреть в классе документа возможность его создания по СОМ — AppWizard поддерживает это и в MDI-, и в SDI-приложепиях.
МОГ-приложегшеАиюсПскдемонстрирует, как СОМ инициирует создание нового документа, окна представления и дочернего окна-рамки всякий раз, когда контроллер автоматизации создает новый объект компонента. Ну а поскольку ЕХ25С — SDI-приложение, Windows при создании клиентом нового объекта запускает новый экземпляр программы. Сразу после запуска программы СОМ при помощи каркаса приложения MFC создает не только документ, поддерживающий Автоматизацию, но и окно представления и основное окно-рамку.
Давайте поэкспериментируем с приложением ЕХ25С, изначально сгенерированным Арр - Wizard с установленным параметром Automation. Оно представляют собой программу-будильник с оконным пользовательским интерфейсом, предназначенную для управления клиентом Автоматизации, таким как Excel. У ЕХ25С имеются следующие свойства и методы.
Название Описание''
TimeСвойство типа DATE, содержащее COM-дату (m_vaTime)
FigureИндексируемое свойство типа VARIANT для четырех цифр на циферблате (m_strFigure[J)
RefreshWin Метод, объявляющий окно представления недействительным и помещающий основное окно-рамку поверх других окоп (Refresh)
SbouWin Метод, отображающий основное окно приложения (SbowWin)
CreateAlarm Метод, создающий объект CAlann и возвращающий его /ОйрвГоб-указатель (CreateAIarm)
Для создания и запуска программы ЕХ25С с прилагаемого к книге компакт-диска выполните следующие действия.
Откройте в Visual C++ проект \vcpp32\ex25c\ex25c.dsw. Соберите проект, чтобы получить файл ех25с.ехе в подкаталоге Debug проекта.
Запустите программу один раз для ее регистрации. Программа может работать как автономное приложение или как компонент Автоматизации. При запуске из Windows или из Developer Studio она обновляет реестр и показывает циферблат часов с римскими цифрами XII, III, VI и IX в соответствующих позициях. Закройте приложение.
Загрузите файл Excel \vcpp32\ex25c\ex25c.xls. Таблица выглядит как на рисунке.
Щелкните кнопку Load Clock, затемдважды — кнопку Set Alarm (после щелчка кнопки Load Clock может быть длительная задержка). На экране должны появиться часы с буквой А - индикатором того, что будильник установлен.
Если Вы запустите компонент под управлением отладчика, то сможете увидеть в окне Debug, когда была вызвана Initlnstance и когда был создан объект-документ.
Если Вас удивляет отсутствие меню в окне будильника, то причина этого — наличие в GMainFrome-PreCreateWindow оператора
Cs.hMenu = NULL;-
Завершите программу Clock, а затем щелкните кнопку Unload Clock. Если компонент запущен из отладчика, в окне Debug появится сообщение о вызове функции ExitInstance.
Наибольшую часть работы по созданию документа как компонента Автоматизации проделал AppWizard. В производном классе приложения СЕх25сАррон сгенерировал дан компонента переменную-член: public;'
COleTemplateServer m_server;
MFC-класс СО1еТетр1а1е£еп’е1является производным от COleOlyeclFacloiyOH создает СОМ- объект «документ* при вызове клиентом IClassFactory::Createlnstance.Идентификатор класса хранится в глобальной переменной clsid, определенной в файле ех25с.срр. Программный идентификатор (Ex25c.Document) находится в строковом ресурсе IDR_MAINFRAME. '
В функции Initlnstance (файл ех25с.срр) AppWizard сгенерировал код, подключающий компонентный объект (документ) к шаблону документа:
CSingleDocTemplate* pDocTemplate;
pDocTemplate - new CSingleDocTemplate(
IOR_MAINFRAME,
RUNTIME.CLASS(CEx25cDoc),
RUNTIME_CLASS(CMainFrame), // основное oki-o-рамка SDI
RUNTTME_CLASS(CEx25cView)):
AddOocTempfate(pDocTemplate);
m_sorver.ConncctTemplate(clsid, pDocTemplate, TRUE);
Теперь все готово для того, чтобы СОМ и каркас приложений смогли создать документ вместе с окном представления и окном-рамкой. Однако формирование объектов не влечет автоматического вывода основного окна на экран. Эго уже ваша работа — пишите соответствующий метод
Следующий вызов UpdateRegistr^B функции Initlnstance обновляет Реестр Windows на основе строкового ресурса IDR_MA1NFRaMF.-.
m_server. UpdateRegistry(DAT_DISPATCH_OBJECT):
Взгляните на таблицу диспетчеризации в файле ex25cDoc.cpp, которая показывает методы и свойства класса CEx.25cDoc. Обратите внимание: Figure — индексируемое свойство, которое ClassWizard может сгенерировать самостоятельно, если Вы зададите ему подходящий параметр. Код необходимый для функций GetFigure и SetFigure^M рассмотрим позднее.
BEGIN DISPATCH MAP(CEx25cDoc, CDocument)
/А {AFX_DISPATCH_MAP(CEx25cDoc)
DISP PROPERTY NOTIFY!CEx25cDoc. "Time", m time. OnTimeCnanged, VT_DATE)
CISP_FUNCTI0N(CEx25cDoc. "ShowWin", ShowWin, VT_EMPTY. VTS_NONE)
DISP_FUNCTION(CEx25cDoc, "CreateAlarm", CreateAlarm, VT DISPATCH, VTS DATE)
DISP_FUMCTION(CEx25cDoc, "RefreshW1r,“, Refresh, VT_EMPTY, VTS_NONE)
DISP PROPERTY PARAM(CEx25cDoc, "Figure", Get^lguie, SelFigure, VT_VARIANT, VTSJ2)
//ИАЕХ DISPATCH КАР
END_DISPATCH_HAP()-
Методы RefreshWinn SbowWin не представляют особого интереса, в отличие от метода CreateAlarm, который заслуживает более внимательного рассмотрения. Вот как выглядит соответствующая ему функция-член CreateAlarm:
LPDISPATCH CEx25cDoc:: Great eAla iam( DATE time)
* TRACE ("Входим в CEx25cDcc: :CrcatcAlaram, время = %f\n", time);
// OLE удалит любой предыдущий объект CAlarm .
m_pAlarm = new CAlarm(time);
return m_pAlarm->GetIDispatcti(FALSE); // здесь нет AddRef
)
Мы предпочли, чтобы компонент создавал объект CAlarm, когда контроллер вызывает метод CreateAlarm. CAlarm — это класс компонента Автоматизации, который мы сгенерировали при помощи ClassWizard. В нем не предусмотрено создание его объектов через СОМ, чем и обусловлено отсутствие для этого класса макроса IMPLEMENTjOLECREATB. фабрики классов. Функция CreateAlarm создает объект CAlarm и возвращает указатель на IDispatch. (Параметр FALSE, передаваемый в CCmcnargeL:GetlDispatcb,o3Ha4aev, что счетчик ссылок не увеличивается; счетчик ссылок на объект CAlarm уже установлен в 1 при создании объекта.)
Класс CAlarm объявлен в alarnkh так:
-class CAlarm : public CCmdTarget.
{
// DECLARE„DYNCREATE(CAlarm)
public:
CAlarm(DATE time);
// Атрибуты
public:
// Операции public:
// Переопределенные функции
// переопределенные виртуальные функции, сгенерированные Classwizard
//{(AFX_VIRTUAL(CAlarm)
public:
virtual void OnFinalReleaseO;
//}}AFX_VIRTUAL
// Реализация
protected:.
virtual ~CAlarm();•
// сгенерированные функции таблицы сообщений
//{(AFX_MSG(CAlarm}
// ПРИМЕЧАНИЕ: здесь ClasdWizard будет добавлять и удалять функции-члены
//}}AFX_MSG
DECLARE_HESSAGE_HAP()
// Сгенерированные функции таблицы диспетчеризации OLE public:
//{{AFX_DISPATCH(CAlarm)
DATE Ui time;
//}}AFX DISPATCH
DECLARE_DISPATCH_MAP()
DECLARE INTERFACE MAP()
1:
Обратите внимание на отсутствие макроса DECLAREJDYNCREATE.
В файле alarm, срр содержится таблица диспетчеризации:
BEGIN DISPATCH МАР(CAlarm, CCmdTarget) '
//{{AFX DISPATCH MAP(CAlarm)
DISP PROPERTY(CAlarm, "Time", m_time. VT_DATE);
//}}AFX DISPATCH MAP
END_DISPATCH_MAP
Для чего нам класс CAlarm? Вместо него мы могли бы добавить в класс CEx25cDocсвойство AlarmTime, но тогда для включения и отключения подачи сигнала будильником понадобилось бы другое свойство или метод. Чего мы действительно добились, введя класс CAlarm, так это поддержки набора сигналов.
Для реализации набора сигналов мы могли бы написать еще один класс — GAlarmsc методами АяЦ Remove и Item. Назначение первых двух понятно, a Item возвращает указатель IDispatch для элемента набора, задаваемого индексом, числам или чем-то еще. Мы также могли бы реализовать свойство Count, доступное только для чтения и возвращающее число элементов, В классе документа, в котором содержится набор, был бы метод Alarms с необязательным параметром типа VARIANT Если этот параметр отсутствует, метод возвращает указатель
на IDispatcb№» набора, а если в параметре задан индекс, — указатель на IDispatcb^x выбранного сигнала.
ПРИМЕЧАНИЕ Пожелай мы, чтобы набор поддерживал синтаксис VBA «For Each», нам пришлось бы проделать еще кое-что. К классу CAIarms надо было бы добавить интерфейс lEnumVARIANTw реализовать функцию-член Next этого интерфейса для прохода по элементам набора. Затем ввести в класс CAIarms метод _NewEnum, возвращающий указатель на интерфейс lEnumVARIANT. А чтобы набор был универсальным, нужно было бы разрешить создание отдельных объектов-перечислителей (enumerator objects) (с интерфейсом lEnumVARIANT] и реализовать другие функции lEnumVariant:Skip, Reset и Стопе.
Свойство Figures интересно тем, что допускает индексацию. Оно представляет четыре числа из римских цифр на циферблате часок XII, III, VI и IX. Свойство реализовано как массив CSiring, так что применение римских цифр вполне допустимо. Объявление в ex25cDoc.h выглядиттаю
public:
CString m_strFigure[4];
А функции GerFiguren SetFi&ire в ex25cDoc.cpp реализованы так:
VARIANT CEx25cDoc: :GetFigure(short n)
TRACE("Входим в CEx25cDcc::GetFigure - n = %d r_strFigure[n] = %s\n", n, m_strFigure[n]); return COleVariant(m_strFigure[n]);
void CEx25cDoc: :SetFigure(short n, const VARIANT FAR& newValue)
I,
ТЯАСЕ("Входим в CEx25cDoc::SetFigure - n = %d, vt = %d\n", n, newValue.vt);
COleVariant vaTemp;
vaTemp.ChangeType(VT_BSTR, (COleVariant*) &newValue);
m strFigure[n] = vaTemp.bstrVal; // преобразует двухбайтовую строку в однобайтовую
1
С этими функциями связан макрос DISP_PROPERTY_PARAMb таблицы диспетчеризации класса СЕх25сВос. Первый параметр функций — индекс, заданный последним параметром макроса типа короткое целое. Индексы свойств необязательно должны быть целыми числами и могут состоять из нескольких компонентов (например, номеров строки и столбца), Вызов CbongeTypee SetFigure необходим, иначе контроллер передаст вместо строк числа.
Мы только что рассмотрели свойства-наборы и индексируемые свойства. Чем они различаются? Контроллер не может добавлять и удалять элементы индексируемого свойства, но может делать это в наборе.
Какая функция рисует циферблат? Как Вы, наверное, и ожидали, этим занимается функция OnDraw класса <вид». Функция использует GetDocument, чтобы получить указатель на документ, а затем обращается к переменным-членам и функциям-членам, соответствующим свойствам и методам.
И, наконец, код макроса Excel:
IDirn Clock As Object
Dim Alarm As Object'
Sub LoadClockQ
Set Clock = Create0bject("Ex25c.Document")
Range("A3").Select n = 0;
Do Until n = 4
Clock.figure(n) = Selection.Value.
Selection. Off set (0, 1 >. Range ("A1"). Select n = n + 1
Loop
RefreshClock
Clock.ShowWin
End Sub.
Sub Refr'eshClack()
Clock.Time = Now()
Clock.RefreshWin'
End Sub
Sub CreateAlarmO
Range(“E3").SelecT‘
Set Alarm = Clock.CreateAlarm(Selection.Value) RefreshClock
End Sub
Sub UnloadClockO
Set Clock = Nothing
End Sub
'Обратите внимание на оператор Set Alam в макросе CreateAlam. Он вызывает метод CreateAkam, чтобы получить указатель на IDispatch, сохраняемый в объектной переменной. Запустив макрос повторно, Вы зададите новый будильник, но при этом уничтожите старый, так как значение его счетчика ссылок обнулится.
ПРЕДУПРЕЖДЕНИЕ Вы видели модальное диалоговое окно в DLL (пример ЕХ25В) и основное окно- рамку в ЕХЕ (пример ЕХ25С). Будьте осторожны с модальными диалоговыми окнами в EXE. Диалоговое окно About, вызываемое прямо из компонентной программы, — вещь нормальная, но активизация модального диалогового окна из функции-метода внешнего компонента — идея не из лучших. Проблема в том, что пользователь, когда на экране открыто модальное диалоговое окно, может вновь переключиться на клиентскую программу-контроллер. MFC-контроллеры реагируют на это, открывая окно с сообщением «Server Busy» («Сервер занят»). Excel поступает аналогично, но перед этим зачем-то выжидает 30 секунд, что часто сбивает с толку.
Пример EX25D
Итак, Вы познакомились с примерами компонентов Автоматизации. Теперь перейдем к примеру клиентской программы Автоматизации на C++, которая запускает все эти компоненты, а также управляет работой Microsoft Excel 97. Программа EX25D изначально сгенерирована AppWizard, но без установки каких-либо параметров, связанных с СОМ. Проще добавить код, связанный с СОМ, вручную, чем удалять код, специфичный для компонента. Если же Вы все- таки решите генерировать подобный контроллер Автоматизации с помощью AppWizard, добавьте в конец StdAfx.h строку:■
«include <afxdisp.h>
а в начало Initlnstance приложения — вызов;
AfxOlelnitQ;
Чтобы подготовить исполняемый файл EX25D, откройте проект \vcpp32\ex25d\ex25d и соберите его. Запустив программу под управлением отладчика, Вы увидите стандартное SDI- приложение, структура меню которого показана на рисунке 25-7.
Собрав и зарегистрировав все компоненты, можете протестировать их с помощью EX25D. Заметьте, что DLL-компоненты необязательно должны находиться в каталоге \winnt\system32, так как Windows отыскивает их по информации из системного Реестра. Для некоторых компонентов, чтобы убедиться в правильности результатов теста, придется воспользоваться ок-
ном Debug. Программа EX25D достаточно модульна. Команды меню и события, связанные с обновлением пользовательского интерфейса, направляются классу «вид». У каждого компонентного объекта свой С++-класс контроллера и внедренная переменная-член в ex25dView.h. Мы рассмотрим каждую часть программы по отдельности, но сначала разберемся с библиотеками типов.
Load
Load
Load
Load
—
Test
Get Data
Create Alarm
-Execute
Unload
Unload
Refresh Time
Unload
1Рис. 25-7. Пример структуры меню стандартного SDI-прилсжвния
Библиотеки типов и ODL-файлы
Мы уже говорили, что библиотеки типов в MFC-реализации IDispatch не нужны, но Visual C++ все равно втихую генерирует их для всех создаваемых компонентов. В чем их польза? Дело в том, что VBA может использовать их для просмотра методов и свойств компонента и для ускорения доступа к методам и свойствам в процессе так называемого раннего связывания (early binding), о котором мы поговорим чуть позже в этой главе. Но мы ведь создаем здесь клиентскую программу на C++, а не на VBA. Оказывается, Class Wizard способен — по информации из библиотеки типов компонента — генерировать С++-код, позволяющий контроллеру обращаться к компоненту Автоматизации.
ПРИМЕЧАНИЕ AppWizard инициализирует ODL-файл (Object Description Language — язык описания объектов) при создании проекта. ClassWizard обновляет этот файл всякий раз, когда Вы генерируете новый класс компонента Автоматизации или добавляете свойства и методы к уже существующему классу. ODL-файл, в отличие от CLW-файла, нельзя заново создать по содержимому исходных файлов, используя ClassWizard. Если с ODL-файлом что-то случится, его придется восстанавливать вручную.
Когда Вы добавляли свойства и методы к классам компонентов, ClassWizard обновлял и ODL-файл проекта. Это текстовый файл, описывающий компонент на языке описания объектов. Ниже приведен ODL-файл для банковского компонента (если Вы сгенерируете этот проект с помощью AppWizard, GUID в вашем файле будет другим).
// ex25a.odl : исходный файл библиотеки типов для ex25a.exe
// этот файл будет обработан компилятором MIDI для создания библиотеки типов (ех25а. tlb) [ uuld(85D56DE4-789D-11DO-92E1-D74D1B9CCD32), version(I.O) ]'
library Ех25а <
importlib( "stcole32. tlb" );
// Основной disp-интерфейс для CBank
[ uuid(89EA95E1-78A1-11D0-92E1-D74D1B9CC032) ]
dispinLerFace IBank (
properties:
// ПРИМЕЧАНИЕ: эта информация о свойствах редактируется ClassWizard.
// Будьте крайне осторожны при редактировании этого раздела.-
//{{AFX_ODL_PROP(CBank)
[id(1)] double Balance;
//}}AFX_ODL_PROP
methods:
// ПРИМЕЧАНИЕ: эта информация о методах редактируется ClassWtzard.
// Будьте крайне осторожны при редактировании этого раздела.
//{(AFX_ODL_METHODCCBank>
[ld(2)] double Wi7hdrawal(doueie dAmount);
Г14(3)] void Deposit(double dAmourt);
7/}}A"X ODL. METHOD
// информация о классе CBank
[ iJuid(93EA95E2-7aA1-11DO-32El-D74D1B9CCD32) ] coclass CSank
{
[default] disnynte^face IBank;
//{[AFX APPEND.ODLH
ODL-файл содержит уникальный GL'ID-идептификатор библиотеки типов 85D56DE4-789D- 11D0-92E1-D74D1B9CCD32, и полностью описывает свойства и методы в диспетчерском интерфейсе (dispinterface) с именем IBank. Кроме того, для dispinterface задан свой GUID (99EA95F.1-78A1 -11D0-92E1-D74D1B9CCD32), аналогичный идентификатору в таблице интерфейсов для класса CBank (см. листинг для сервера ЕХ25А). Смысл этого GUID станет понятен, когда Вы прочтете раздел «Раннее связывание в VBA» в конце этой главы. Для загрузки компонента VBA использует идентификатор класса (99EA95E2-78A1-11DO-92E1-D74D1B9CCD32).
В любом случае, Visual C++ при сборке проекта компонента вызывает утилиту MIDL, которая считывает ODL-файл и генерирует двоичный TLB-файл в каталоге Debug или Release проекта. Теперь, когда Вы создаете па C++ клиентскую программу, из TLB-файла проекта компонента можно сгенерировать с помощью Class Wizard управляющий (driver) класс.
ПРИМЕЧАНИЕ Утилита MIDL генерирует библиотеку типов как отдельный TLB-файл — именно его и ищут контроллеры Автоматизации типа Excel. В ActiveX-элементах библиотеки типов хранятся как ресурс.
Чтобы все это проделать, щелкните кнопку Add Class в Class Wizard и выберите в списке строку From A Type Library. Укажите TLB-файл проекта компонента, и Class Wizard откроет диалоговое окно, аналогичное приведенному ниже на рисунке.
IBank — это имя disp-интерфейса, заданное в ODL-файлв. Мажете, если хотите, сохранить это имя класса, а также задать имена Н- и СРР-файлов. Если в библиотеке типов не один интерфейс, можно выбрать сразу несколько интерфейсов. Сгенерированный класс контроллера показан в следующем разделе.
Класс контроллера для ex25a.exe
ClassWizard сгенерировал класс IBank, производный от COieDispatcbDriver (см. рис. 25-8). Присмотритесь к реализации функций-членов. Обратите внимание на первый параметр в вызовах функций GetProperty, SetProperty и InvokeHelper. Это жестко заданные DISPID-идсн- тификаторы свойств или методов компонента в порядке, определенном в таблице диспетчеризации компонента.
ПРЕДУПРЕЖДЕНИЕ Если Вы удалите свойство, используя ClassWizard, азатем введете его снова, IDISPID-идентификатор, по-видимому, изменится. Значит, придется повторно генерировать или редактировать класс контроллера, чтобы идентификаторы совпадали.
BANKDRIVER.Н
class IBank : public COieDispatcbDriver I!
public:
IBank() {} // вызывает конструктор по умолчанию COieDispatcbDriver
IBank(LPDISPATCH nOispatoh) s COleDispatchDriver(pDispatch) {}
IBank(const IBanki dispatchSrc) s COleDispatchDri ver(dispatchSrc) {}
.// Атрибуты
publics
■double GetBalance();
void SetBaiance(douQle);
ji Операции publics
■double Withdrawal (double dAmount);
void Deposit(double dAmount);
BANKDHIVER.CPP
«include 'StdAfx.n"
«include "banxdriver.h"
tf if def .DEBUG
«define new DEBUG NEW
flundefTHIS FILE
istatic cha^ THIS_FILE[] = „FILE.
,f lendif
//////////////////////////////////////////////У//////////////////////////////
// свойства IBank
■double IBank: :GelBalance()
double result;
GetProperty(Ox1, VT.R8, (void«)&result); return result;
Рис. 25-8. Листинг класса iBank
void IBank: :SetBalancc(double propVal)'
.1
SetProperty(0x1, VT Я8, propVal);
}"
/////////////////////////////////////////////////////////////////////////////
// операции IBank
double IBank::WLthdrawal(double dAmount)
{
double result;
static BYTE parms[] = VTS_R8;.'
InvokeHelper(0x2, DISPATCH_METHOD. VT_R8, (void*)&result, parms, dAmount);
return result;
}
void IBank ::Deposit(doubledAmount)
{
static' BYTE parms[) = VTS R8;
lnvokeHelper(Ox3, DISPATCH METHOD, VT_EMPTY, NULL, parms, dAmount);
}
В классе CEx25dViewecn> переменная-член m_bank класса IBank. Функции-члены GEx25- dViewдля компонента Ex25a.Bank приведены ниже. Они вызываются при выборе команд из основного меню контроллера. В частности, представляет интерес функция OfiBenholetoad. Функция COteDi$patcbDriver::CreateDispatcte^)y^ программу компонента вызовом CoGet- ClassObjecfa IClassFactory\CreateInstance3 она вызывает Queryinterface,чтобы. получить указатель на IDispatch, сохраняемый в переменной-члене mJpDispatcb. Функция COteDis- patcbDriver.-.ReleaseDtspatcb, вызываемая из OnBankoleUnload, освобождает этот указатель вызовом Release.
void CEx25dView: :OnBankoleLoad()
<
if(!m_bank.CreateDispatch("Ex25a.Bank")) {
AfxMessageBox("Компонент Ex25a.Bank не найден");
return;
void CEx25dView; :OnUpdateBankoleLoad(CCmdUI* pCmdUI) {
pCmdUI->Enable(m_bank.m_lpDispatch == NULL);
void CEx25dView::OnBankoleTest()
{ '.
m_bank.Deposit(20. 0);
m_bank.Withdrawal (15.0);
ТЯАСЕ("Новый баланс = %f\n", m bank. GetBalanceO);
}
void CEx25dView::OnUpdateBankaleTest(CCmdUI* pCmdUI)
{
pCmdUI->Enable(m bank.m IpDispatch l= NULL) ,-
}
void CEx25dView: :0nBankoleLlnload( )
{
m bank.ReleaseDispatchO;
}.
.CM. след. anp.
17 Зак.-№787
void CEx25dView::OnUpdateBankoleUnload(CCmdUI* pCmdUI)
i
pCmdUI->Enable(m_bank.m_lpDispatch != NULL);
Класс контроллера для ex25b.dll
На рисунке 25-9 показан заголовочный файл класса, сгенерированный ClassWizard.
AUTODRIVER.H
class IEx25bAuto : public COleDispatchDriver {
public:■
!Ex25bAuto() {} // вызывает конструктор по умолчанию класса COleDispatchDriver
IEx25bAuto(LPDISPATCH pDispatch) : COleDisaarchDriverCpDispatch) {}
IEx25bAuto(const IEx25bAuto& dispatchSrc) :
C01eDispatchDriver(dispatchSrc) {).
// Атрибуты public:
long GetLongDataO;
void SetLongData(long);
VARIANT GetTextData();
void SetTextData(const VARIANTS),;
// Операции'
public:
BOOL DisplayDialcgO;
Рис. 25-9. Заголовочный файл класса IEx25bAuto
Заметьте: для каждого свойства нужны отдельные функции Get и Set, даже если какое-то свойство представлено в компоненте как переменная-член.
Заголовочный файл класса ♦вид» задает переменную-член m_auto класса 1Ех25ЬАи1оН.иже приведены функции-члены из Ex25dVicw.cpp — обработчики команд, относящихся к DLL void CEx25dView::0n011oleGetdata() {
m_auto. DisplayDialogO;
COleVariant vaData = m_auto.GetText:Data();
ASSERT(vaData.vt == VT_BSTR);'
CString strTextData = vaDats bstrVal;
long IData = m_auto.GetLongData():
TRACE("CEx25dView: :OnDlloleGetdata - long = %ld, text = %s\n", IData, strTextData);
void CEx25dView::OnUpdateDlloleGetdata(CCmdUI« pCmdUI) {
pCmdUI->Enable(m_auto.m_lpDispatch != NULL);
void CEx25dView::OnDlloleLoad()
< if(!m„auto.CreateDispatch("Ex25b. Auto")) {
AfxMessageBox("Компонент Ex25b,Auto не найден"); return;
)
mi_auto.SetTextData(C01eVariant("test")); // тестируем, m_auto.SetLongData(79); // тестируем
// проверяем disp-интерфейс
// {A9515AD7-5B85-11D0-848F-OO4006263O58}
static const IID IID_IEx25bAuto =
{ Qxa9515ad7, 0x5b85. 0x11d0, { 0x84, Qx8f, 0x0, 0x40, 0x5,0x26, 0x30, 0x5b } }; LPDISPATCH p; ,
HRESULThr = m_auto.m_lpDispatch->QueryInterface(IID_lEx25bAuto. (void*»)ftp1};-■
TRACE("OnDlloleLoad - результат вызова Ouerylnterface = %x\n", hr);
p->Release();
void CEx25dView::0nUpdateDlloleLoad(C0mdUI* pCmdUI)
{
pCmdUI->Enable(m_auto.m lpDispatch == NULL);
}
void CEx25dView::0nDlloleUnload()
{
m_auto.ReleaseDispatch();
void CEx25dView::OnUpdateDlloIeUnload(CCmdUI* pCmdUI)
pCmdllI->Enable(m_auto. m_lpOispatch ! = NULL);
Класс контроллера для ex25c.exe
На рисунке 25-Ю показаны заголовки классов 1Ех25са LAlarm, управляющих компонентом Автоматизации ЕХ25С.
CLOCKDRIVER.Н
class IEx25c ; public COleDispatchDriver.. . .
public:■
IEx25c() {} // вызывает конструктор по умолчанию класса CQleDispatchDriver
IEx25c(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {}.
IEx25c(const IEx25c& dispatchSrc} :.
COleDispatchDriver(dispatchSrc) {}
// Атрибуты
public:■•
DATE GetTime();
void SetTime(DATE);■
,// Операции
public:
void ShowWinO;
LPDISPATCH CreateAlarm(DATE time);
void RefreshWinC);
void SetFigure(short n, const VARIANTS newValue);
VARIANT GetFigure (short n);
class lAlarm : public CQleDispatchDriver-
public:
IAlann() {} H вызывает конструктор по умолчанию класса COleDispatchDriver
1А1агщ( LPDISPATCH pDispatch) : COIeDispatchDriver(pDispatch) {}
lAlarm(const lAlarmS dispatchSrc) :
COleDispatchDriver(dispatchSrc) {}
Рис. 25-10. Заголовки классов 1Ех25си lAlarmси, след. стр.
Рис. 25-10. (продолжение)
// Атрибуты
public:
DATE GetTimeO;.
void SetTime(DATE);
// Операции
public:
};..
Особый интерес для нас представляет функция-член IEx25c::CreateAl«rn®3 ClockDriver.cpp:
LPDISPATCH IEx25c::CreateAlarm(0ATEtime)
LPDISPATCH result;
static BYTE parms[] = VTS DATE;
InvokeHelper(0x3, DISPATCH_METHOD, VT.DISPATC.i, (void«)&result,-
parms, time); return result;■
Эту функцию можно вызывать только после того, как создан объект (документ) «будильник». Функция создает объект «сигнал» и возвращает указатель на IDispatcbco счетчиком ссылок, равным 1. Функция СО1еОкра(сЬОпуег;:АПасЬО4$рагс№одсоединяет этот указатель к клиентскому объекту m_alarm, но если у этого объекта уже был диспетчерский указатель, то он освобождается. Именно поэтому Вы видите в окне отладки, что старый экземпляр ЕХ25С завершается немедленно, как только Бы запрашиваете новый- Протестировать подобное поведение с контроллером Excel нельзя, так как EX25D отключает в своем меню команду Load после запуска компонента «будильник».
В классе «вид» две переменных-члепа: m_clock и т_&1агт. Вот как. выглядят обработчики команд из этого класса:.,
void CEx25dView::OnClockoleCreatealarm.( ).
CAIarmDialog dig;'
if (dlg.DoModalO == IDOK) {
COleDateTime dt(1995, 12, 23, dlg.m_nHdLrs, dig.m_nMinutes, ■ ■■
■ - - 'dlg'.nn nSeeonds); •>-* ••• •. •
■ LPDISPATCH pAlarm -= m_cl,ook.GreateAlarm(dt) ;...
. (n_alarm.AttachDispa.tch(pAlacm); // освобовдает предыдущий объект! .,.,
m clock.RefreshWin();"
void CEx25dView: :OnUpdateClockQleCreatealarr(CCmdUI' pCmdUI) pCmdUI->Enable(m_clock. m_lpDispyt.c.h != NULL);
void CEx25dWew: lOnClockoleLoadO •■
{.- b ■ . ■ : ■•
if(! m_clock. CreateDispatch("Ex25c;. Document")) {
AfxMessageBox("Компонент Ex25c.Document не найден'); return;
}
m_clock.SetFlgure(0. C01eVariant(;XII"));
m_clock.SeTFigure(1, C01eVariant("HI"));
m_clock.SetFigure(2. COleVariant("VI"));
m_clock.SetFigure(3, C01eVariant("IX")):
OnClockoleRefreshtime(); m_clock.ShowWin();
void CEx25dView::OnUpdateClockoleLoad(CCmdUI* pCmdUI)
pCmdUI->Enable(m_clock.m_lpDispatch == NULL);
void CEx25dView::OnClockoleRefreshtime()
{.
COleDateTime now = COleDateTime: :GetCurrentTlme();
m_clock.$etTime(now);
m_clock.AefreshWin();
void CEx25dVlew::OnUpdateClockoleRefreshtime(CCmdUI* pCmdUI)
pCmdUI->Enable(m clock.m IpDispatch != NULL); }
void CEx25dView::OnClockoleUnload()
{'
m_clock.ReleaseDispatch();
void CEx25dView::OnUpdateClockoleUnload(CCmdUI* pCmdUI) {
pCmdUI->Enable(m clock.m IpDispatch J= NULL); }
Управление приложением Microsoft Excel"
В программе EX25D содержится кед, который, загрузив Excel и создав рабочую книгу, читает и изменяет содержимое ячеек активной таблицы. Управление Excel осуществляется так же, как и управление MFC-компонентом Автоматизации, но у Excel есть некоторые особенности, в которых Вам нужно разобраться.
Изучая Excel VBA, Вы обнаружите, что в программе можно использовать свыше 100 различных «объектов*. Все они доступны посредством Автоматизации, но при написании кот- роллера автоматизации на MFC надо знать о свойствах и методах этих объектов. В идеале у каждого объекта с функциями-членами, вызываемыми по диспетчерским идентификаторам, должен быть свой С-н-класс..
У Excel есть собственная библиотека типов, которая находится в файле Excels.olb, расположенном обычно в каталоге \Program Files\Microsoft Offlce\Office.ClassWizard способен считывать этот файл (так же, как и TLB-файлы) и создавать на его основе С++-классы контроллеров для отдельных объектов Excel. Поэтому имеет смысл отобрать нужные объекты и свести полученные классы в отдельный набор файлов (см. рис. 25-11).
Не исключено, что сгенерированный код придется редактировать вручную, чтобы подогнать его под свои потребности. Рассмотрим это на примере. Сформировав с помощью ClassWizard класс контроллера для объекта Worksheet, Вы получите такую функцию-член GetRange-. LPDISPATCH Worksheet:; Range (const VARIANTS CelU, const VARIANTS Cell2)
LPDISPATCH result;
static BYTE BASED CODE parms[] = VTS VARIANT VTS VARIANT;
‘ InvokeHelper(0xc5, DISPATCH_PR0PERTY6ET, VT_DISPATCH, (void*)Sresult, parms, &Celll, &Cell2); return result;
Рис. 25-11 . СозданиеСн-классов для объектов Excel, перечисленных в ЕхсеЙ.о1Ь, при помощи OassWizard
Как известно из документации на Excel, метод Range можно вызывать как для одной ячейки (един параметр), так и для прямоугольной области, определяемой двумя ячейками (два параметра). Вспомните: в вызове Invokellelpemoyaio не передавать необязательные параметры. В данном случае можно добавить вторую переопределенную функцию Range с одним параметром:
LPDISPATCH Worksheet: :GetRange(constVARIANTS Cell 1) //добавлено‘
{
LPDISPATCH result;
static BYTE parms[] = VTS_VARIANT;
InvokeHelper(OxcS, DISPA"TCH_PROPERTYGET, VT_DISPATCH, (void*)&result, parms.&Celll);
return result;
Какие же функции надо подправить? Да те, что Вы решили использовать в своей программе. Прочтите руководство по Excel VBA, чтобы выяснить требуемые параметры и возвращаемые значения. Может быть, кто-то скоро напишет набор контроллерных классов Excel,
Объекты Excel, используемые программой EX25D, и соответствующие им классы описаны в следующей таблице. (Код содержится, в файлах exce!8.h и ехсе18.срр.)
Объект/кпасс
Переменная-член класса -вид»
-Application Range -Worksheet Workbooks Worksheets
m_app m_range[5] m_worksheet m_work books m_worksheets
Команда Load меню Excel Comp в примере обрабатывается функцией-членом OnExcetote- Load класса *вид». Эта функция должна работать и в том случае, если Excel уже запущен пользователем. COM-функция GetActiveObject пытается вернуть указатель на IUnknown для Excel. GetActiveObject-rpe&^ei задать идентификатор класса, поэтому мы сначала вызываем CLSIDFromProgID Ъсли вызов GetActiveObject был успешным, вызывается Querylnterfacepto^H. получить указатель на IDispatch, который передается контроллерному объекту т_арр класса
Application. Если же вызов GetActiveObject3aK0H4Hiica неудачей, вызывается COleDispalcb- Driven.'CreatcDispatcb,как было в случаях с другими компонентами.■
void CEx25dViow: :OnExcelgleLoad()•’,,
{ // если Excel уже запущен, подключаемся к нему; нет - запускаем его':
LFOISPATCH pDisp; LPUNKNOWN pUnk;.'
CLSID clsid; TRACE("Входим в CFx25dView::OnExceloleload\n"); BeginWaitCursor(); ::CLSIDFromProgID(L"Excel,Application.8", Aclsid); // из Реестра if(;:GetActlveObject(clsid, NULL, SpUnk) == S_OK) {
VERIFY(pUnk->OLeryInterface(IID_IDispatch, (void**) ApDisp) == S_OK); m_app.AttachOispatch(pDisp);.'
pUnk->Release(); TRACE(" подсоединение завершено\п") } else (..;
if(!m_app.CreateDispatch("Excel.Application.8")){'
AfxMessageBox(“Excel 97 не найден");■■"
TRACER"создание завершено\п");1
EndWaitCursor();
ОпЕхсе1о1еЕхеси1вявляется функцией-обработчиком команды Execute в меню Excel Сотр. Ее главная задача в том, чтобы найти основное окно Excel и сделать его активным. Для этого придется писать свой код — соответствующего метода у Excel нет. Мы должны также создать рабочую книгу, если на данный момент ни од на не открыта.
Значения, возвращаемые методами, надо тщательно отслеживать. Например, метод Add набора Workbooks возвращает указатель на IDispatch для объекта Workbook и, естественно, увеличивает счетчик ссылок Если бы мы сгенерировали класс для Workbook, то могли бы KbsssaTbAttacbDispatcb, чтобы при уничтожении этого объекта вызывалась Release. Но поскольку класс Workbook нам не нужен, мы сами освобождаем указатель в конце функции. Если же Вы забудете освободить указатели, отладочная версия MFC сообщит об утечке памяти.
В остальной части функции OnExceloleExecute осуществляется доступ к ячейкам рабочей таблицы. Здесь Вы убедитесь, насколько просто считывать и задавать числа, даты, строки и формулы. С++-код очень похож на VBA-код, который можно было бы написать для той же цели.'
void CEx25dView: :OnExceloleExecute().
{.
LPDISPATCH pRange, pWorkbooks;
CWnd* pWnd = CWnd; : FindWindow("XLNAIN", NULL); if (pWhd 1= NULL) { TRACE("Окно Excel найдено\п"):..
" pWnd->ShowWindow(SW_SHOWNORMAL);.
pWnd->UpdateWindowO; pWnd->BringWindowToTop(); ) m_app. SetSheptsInNev*Jorkbook( 1); VERIFY(pWorkbooks = ni_app.GetWcrkbooks( ));-
m_workbooksAttachDispatch(pWorkbooks);■
LPDISPATCH pWorkbook = NULL;
if (m_workbooks.GetCount() = 0) {
// Add возвращает указатель на Workbook, но у нас
//нет класса Workbook
pWorkbook = m_workbooks.Add(); //Сохраняем указатель, чтобы освободить позже
LPDISPATCH pWorksheets = m_app.GetWorksheets();
ASSERT(pWorksheets • = mull) ;
«.worksheets.AttachDispatch(pWorksheets);
LPDISPATCH pWorksheet = m_worksheets.GetItem(C01eVarlant((short) 1));
m.worksheet.AttachDispatch(pWorksheet);
«.worksheet.Select();
VERIFY(pRange = m_worksheet.GetRange(C0leVariant("A1")));
m_range[O].AttachDispatch(pRange):
VERIFY(pRange -m_worksheet.GetRange(C01eVariant(‘A2")));
m.range[1].Attach0ispatch(pRange):- '
VERIFY(pRange = m_worksheet.GetRange(C01eVariant("A3")));
m_range[2].AttachDispatch(pRange);
VERIFY(pRange - m_worksheet.GetRange(C01eVarlant("A3"). C01eVariant("C5"))); m_range[3].AttachDispatch(pRange);
VER!FY(pRange = m_worksheet.GetRange(C01eVariant("A6")));
m_range[4].Attac hDispatch(pRange);
m_range[4].SetValue(C0leVariant(C0leDateTime(1998, 4. 24, 15, 47, 8)));
// получаем сохраненную дату и выводим ее как строку
COleVariant vaTimeDate = m_range[4].GetValue();
TRACE("возвращенный тип данных = %d\n", vaTimeDate.vt);
COleVariant vaTemp;
vaTemp.ChangeType(VT_BSTR, 4vaTimeDate);
CString str = vaTemp.bstrVal;
ТВАСЕ("дата = %s\n", (const char.) str):
m_range[0]. SetValu e(COleVariant("тестовая строка"));
COleVariant vaResultO = m range[O].GetValue();
if (vaResultO.vt == VT.BSTR) {
CString str = vaResultO.bstrVal;
TRACE("vaResultO = %s\n", (const char*) str);
m_range[1].SetValue(C01eVariant(3.14159)) ;
COleVariant vaResultl = m_range[1]. GetValueO;
if (vaResultl.vt == VT_R8) {
TRACE("vaResultl = %f\n", vaResultl.dblVal); }
m_range[2].SetFormula(C01eVariant("=$A2*2.0"));
COleVariant vaResult2 = m_range[2].GetValue();
if (vaResult2. vt =« VT_R8)" {
TRACE(“vaResult2 = %f\n", vaResult2.dblVal);
COleVarlant vaftesult2a = m_range[2].GetFormula();
if (vaResult2a.vt == VT_BSTR) {
CString str = vaResult2a.bstrVal;
TRACE("vaResult2a = %s\n". (const char*) str);
}•
m_range[3]. FiHRight ();
m_range[3].F1110own();
// очистка
if (pWorkbook*= NULL) { pWorkbook->Release();
} )
Пример EX25E
Эта программа использует директиву *<тро«для генерации smart-указателей. Ведет себя она практически так же, как и EX25D, разве что не запускает Excel Директиву *«трогГмы поместим в файл StdAfx.h, чтобы компилятор не создавал управляющие классы несколько раз. Вот какой код нужно добавить:1
«include <afxdisp.h>
fiimport". .\ex25a\debug\ex25a.tlb" rename_namespace("BankDriv")
using namespace RankDriv;
tfimport ". ,\ex25b\debug\ex25b. tlb" rename_namespace("Ex25bDriv")
using namespace Ex25bDriv;
tfimport". .\ex25c\debug\ex25c.tlb" rename_namespace("ClockDriv")
using namespace ClockDriv;
Разумеется, необходимо вызвать AjxOlelnita функции-члене Initlnstance класса приложения.
Заголовочный файл класса «вид* содержит следующие встроенные smart-указатели, как показано ниже:
I Ex25bAu to Ptr m_auto;
IBankPtr m_bank;
IEx25cPtr m_clock;
lAlarmPtr m_alarm;
А вот код обработчиков команд меню в классе «вид»:
void CEx25eView: :OnBankoleLcad() {
if(m_bank.Create!nstance(uuidof(Bank)) != S_OK) {
AfxMessageBoxCКомпонент Bank не найден"); return;
} }
void CEx25eView: :OnUpdateBankoleLoad(CCmdUI* pCmdUI)
pCmdUI->Enable(m_bank.GetInterfacePtr() == NULL); }
void CEx25eView: :OnBankoleTest()
try {
m_bank->Deposit(20.0);
m_bank->Withdrawal(15.0):
ТЯАСЕ("новый баланс = %f\n", in_bank->Gel3alance());
} catch(_con_error& е) {
AfxMossageBox(e. Er го Message ()):
void CEx2beVlew: :OnUpdateBarikoleTest(CCmdUI‘ pCrndUI) pCmdUI->Erable(n,_bank.GetInterfacePtr() !- NULL);
void CEx25eVlew::0n8arkoleUnload() m_bank.Pelease();
void CEx25eVlew: :OnUpdateBankoleUnload(CCmdUI» pCrndUI) (
pCmdUI->Enable(m_bank.GetInterfacePtr() != NULL);
void CEx25eVisw::0n01ockoleCreatealarm()
CAlarmDlalog dig;
try {
if (dig.DoModal() == TOOK) {
COIeDateTime dt(1995, 12, 23, dlg.rj:Hours, dlg.m_nMlnutes, dig.m_nSeconds);
LPDISPATCH pAlarm = m_clcck->OreateAlarm(dt);
m_alarm.Attach((IAlarm«} pAlarm); //освобождается предыдущий объект!
in clock->8sf reshWinO;
} "
} catch(_com_error& e) {
AfxMessageBox(e.E rrorMessage());
void CFx25eView::0nUpdateClockol8Createalarm(CCmdUI* pCmdUI) {
pCmdJI->Ena31e(in_clock.GerinterfacePtr() != NULL);
void CEx25eView. -.OnClockoleloadO
if(m_clock.CreateInstance(uuidof(pocument)) 1= S_OK) { AfxMessageBoxCКомпонент Clock не найден");
return;
}
try {
m-Clock-xPutFiguretO, C01eVariant("XII"));
m_clock->PutFigure(1, C01eVariant("III"));
m_clock->PutFigure(2, C01eVariant("VI"));
m_clock->Putrigure(3, COleVariant("IX"));
OnClockoleRefresht;me();
m_clock->ShowWin();
} carch(_com_crror& e) {
' AfxMessageBox(e. ErrorMessageO);
void CEx25eView: :OnUpdateClockoleLoad(CCmdUI* pCmdUI) pCmdUI->Enable(m_clock. GetInterfacePtr() == null) ;
void CEx25eView: :OnClockoleRefreshtime()
{
COleDateTime now = COleDateTime;:GetCurrentTime(); try {
m_clock->PutTime(now);
m_clock->RefreshWin();
} catch(_com_error& e) {
AfxMessageBox(e.ErrorMessage());
void CEx25eView: :OnUpdateClockoleRef reshtime(CCmdUI« pCmdUI) pCmdUI->Enable(m_clock.GerInterfacePtr() != NULL);
void CEx25eView: :OnClockoleUnload() {
m_clock.Release();
void CEx25eView: :OnUpdateClockoleUnload(CCir.dUI* pCmdUI) {
pCmdUI->Enable(mclock.GetInterfacePtr( ) i=NULL);
void CEx25eView: :OnDlloleGetdata()•
{
try{
m_autd->DisplayDialog();
COleVariant vaData = m auto->GetTextOata();
ASSERT(vaData.vt == VT_BSTR);.
CString strTextData = vaData. bstrVal;
long IData ^ m_auto->GetLcngData();
TRACE( 'CEx25dView: :OnDlloleGetdata - tong = »ld, text = Xs\n", IData, strTextData);
) catch(_com_errcr& e) {
AfxMessageBox(e.ErrorMessageO):
void CEx25eView::OnUpdateDlloleGetdata(CCmdUI* pCmdUI) ^ pCmdUI->Enable(m_auto.GetInterfacePtr() != NULL);
void CEx25eView: :OnDlloleLoad()
if(m_auto.CreateInstance(__uuidof(Ex25bAuto)) 1= S_OK) { AfxMessageBpx ( "КомпонентEx25bAuto не найден");
return;
void CEx25eView::OnUpdateDlloleLoad(CCmdUI* pCmdUI)
pCmdUI->Enable(m auto.GetlnterfacePtrO == NULL);
>
void CEx25eView: :OnDlloleUnload()
{
m_auto. Released:
}
void CEx25eView: :OnUpdateDl]oleUnload(CCmdUI* pCmdUI) ■i
pCmdUI->Enable(ni auto.GetlnterfacePtrO i= NULL);
}
■ Обратите внимание на использование блоков try/catcbB функциях, которые работают с компонентами. Они особенно необходимы для обработки ошибок, возникающих при прекращении работы программы компонента. В предыдущем примере, EX25D, этим занимается MFC-класс COieDispatcbDriver.
Раннее связывание в VBA
Запуская компоненты ЕХ25 А, ЕХ25В и ЕХ25С из Excel VBA, Вы применяли метод так называемого позднего связывания (late binding). VBA, обращаясь к какому-то свойству или методу, обычно вызывает IDispatcb;GetlDsOjNames, чтобы найти диспетчерский ид ентификатор по символьному имени. Это неэффективно, но важнее здесь то, что VBA не проверяет типы до фактического обращения к свойству или методу. Например, программа на VBA пытается получить значение свойства, которое она считает числом, а компонент вместо этого возвращает строку. В этом случае VBA выдаст ошибку в период выполнения программы, только дойдя до соответствующего оператора.
Однако раннее связывание (early binding) заставляет VBA предварительно обрабатывать исходный текст программы, преобразуя имена свойств и методов в DISPID-идентификаторы до запуска программы компонента. При этом он проверяет типы свойств, возвращаемых значений и параметров методов, выдавая при необходимости ошибки компиляции. Но откуда VBA берет нужную информацию? Конечно же, из библиотеки типов компонента. Благодаря этой библиотеке VBA позволяет программисту просмотреть описания свойств и методов сервера. VBA считывает библиотеку типов еще до загрузки компонентной программы.
Регистрация библиотеки типов
Вы уже видели, что Visual C++ генерирует TLB-файл для каждого компонента. Для того, чтобы VBA нашел библиотеку типов, ее адрес должен быть указ ан в Реестре Windows. Для этого надо написать текстовый REG-файл, который можно импортировать вредактор Реестра (Regedit). Вог файл ex25b.reg для DLL-компонента ЕХ25В:'
REGEDIT4
[ HKEY_CLASSES_ROOT\TypeLib\{ А9515 АСА- 5685-11 DO -848 F-00400526305В}}
[HKEY_CLASSES_R00ATypeLib\(A9515ACA-5B85-1100-848F-00400526305B)\1.0]
@="Ех25Ь"
[HKEY_CLASSES_ROOT\TypeLib\{А9515АСА-5В85-11DO-848F-00400526305В}\1.0\0]
|[HKEY_CLASSES_ROOT\TypeLib\{A95l5ACA-5fl85-11D0-848F-00400526305B}\1.O\0\win32]
Ф="с:\\vcpp32\\ex25b\\Debug\\ex25b.tlb"
[HKEY_CLASSFS_ROUT\TypeLib\(A9515ACA-533E-11[)0-848F-00400526305B}\1.0\FLAGS]
[HKEY_CLASSES_RS0ATypeLib\(A9515ACA 5В85-11 DD-348F-00^005263C5B}\1.0\HELPDIR] "
@= "с:\\vcpp32\\ex25b\\Debeg"
[IIKEY_CLASSES_RO0T\Interface\(A951bAD7-53B5-11DO-848F-DO4OO526305B})'
@="IEx25bAutc"
[HKEY CLASSES R00T\Interface\{A9515AD7-5B85-11D0-848F-C0400526305B}\ProxyStubClsid]
@=" (00020420-0000-С000-С000-0000000С0046Г..■
[HKEY_CLASSES_ROOT\Interface\{A9515AD7-5385-1lDO-848F-OO4OO525305B)\ProxyStubClsid32] ■•
@="{ooo?042o-oboo-ooac-cooo-oooooooooM6}"
[HKEY CLA$$ES_RD0T\Interface\{A9515A07-5H85-'1D0-848F-0040052B305B}\TypeLib] @="{A9515ACA-5B85-11D0-848F-00400526305Br■
“Version's" 1.O'-'■■■■ ; -, -
Заметьте: этот файл генерирует подразделы в разделах Реестра TypeLib и Interface; Третья запись указывает путь к файлу TLB версии 1.0. Подраздел 0, обозначает «нейтральный язык». Будь ваше приложение многоязычным, здесь бы присутствовали отдельные записи по каждому языку (английскому французскому и т. д.). Записи в разделе TypeLib используются про* граммами просмотра библиотек, а записи в разделе Interface служат для проверки типов в период выполнения и (в случае ЕХЕ-компонепта) в процессе маршалинга для диспетчерского интерфейса.
Регистрция компонентом своей библиотеки типов
Когда ЕХЁ-компонент запускается как автономное приложение, он может вызвать MFC-фун- кцшоЛАЯе#^ег7Уре£^дтя создания необходимых записей в Реестре: ■
VERIFY(Afx01eRegisterTypeLib(AfxGetInsTanceHandle(), theTypelibGUID, "ex25b.tlb"));
Здесь tbeTypeLibGUID — статическая неременная типа GUID;
И {А9515АСА-5885-11D0-848F 00400525305В> static const GUID theTypeLibGJID = { 0xa9515aca, Ox5b85, Ox11dO, { 0x84, Qx8f, 0x00, 0x40, 0x05, 0x26, 0x30, 0x5 b} );
Функция AJxRegtstefTypeLibDQrbHBiretta в файле afxwin.h, который требует определения мак- роса_ЛЁЖД£.Таким образом, эту функцию нельзя использовать в обычной DLL, если только Вы не скопируете се код из исходных файлов MFC.’ " 4•'* t< -г^
ODL-файл'..,. .
Теперь самое время обратиться к ODL-файлу того же проекта: .-> ■.
// ex25b.odl : исходный файл библиотеки типов для ex25b.dll ■-
// Этот файл обрабатывается коугилйтоэомMIDL для создания библиотеки типов (ex25b.tlb).
[ uuid(A9515ACA-5B85-11D0-848F-00400526305B), version(I.O) ]...
// GUID для библиотеки типов - соответствует разделу рестра TypeLib•
// и параметру AfxO'eRegisterTypeLib library Ex25b., .
{
// Имя библиотеки для средства просмотра объектов в Excel ,;:., :
importlib("stdale32.t]n");'
см, след, стр.
// основной dlsp-интерфейс для CEx25bAuto
[ uuid(A9515A07-5B85-11D0-848F-00400526305B) ]-
// GUID из таблицы интерфейсов компонента - совпадает с записью Interface в Реестре
dispinterface IEx25bAuto '
{
// Имя, используемое в операторе VBA Din и в списке Object properties:
// ПРИМЕЧАНИЕ: эту информацию о свойствах модифицирует ClassWizard.
’// Будьте крайне осторожны при редактировании этого раздела.
//{(AFX_0DL_PR0P(C£x25bAuto)
[id(1)J long LongData;
[id(2)J VARIANT TextData;
//}}AFX_ODL_PROP■
methods;•
// ПРИМЕЧАНИЕ: зту информаций о свойствах модифицирует Classwizard.
// Будьте крайне осторожны при редактировании этого раздела.
//{{AFX_0DL_METH0D(CEx25bAutO)
[id(3)] boolean DisplayDialogO: //}}AFX_ODL_METHOO
// ClSID компонента
[ uuid(A9515AD8-5085-11DO-846F-OO4OO5263O5B) ]
coclass Ex25bAuto
{
[default] dispinterface IEx25bAuto;
b
//{{AFX APPEND_ODL}}
Как видите, между Реестром, библиотекой типов, компонентом и клиентом VBA существует множество связей.
ПРИМЕЧАНИЕ Полезная утилита OLEVIEW, входящая в состав Visual C++, позволяет просматривать зарегистрированные компоненты и их библиотеки типов.
Использование библиотеки типов в Excel
Последовательность операций, выполняемых Excel для того, чтобы воспользоваться вашей библиотекой типов.
При запуске Excel считывает из Реестра раздел TypeLib и составляет список всех библиотек типов. Он загружает библиотеку типов VBA и библиотеку объектов Excel.
Пользователь (или автор рабочей книги) — после запуска Excel, загрузки рабочей книги и переключения на редактор макросов — выбирает из меню Tools команду References (ссылки) и помечает флажок в строке ЕХ25В LIB, как показано на первом рисунке на стр. 493- При сохранении рабочей книги информация о ссылках сохраняется вместе с ней.
Теперь пользователь Excel может просматривать свойства и методы ЕХ25В, выбрав из меню View команду Object Browser, как показано на втором рисунке на стр. 493
Чтобы ваша программа использовала библиотеку типов, просто замените строку
Dim OilComp As Object
на:
IDim DHComp As IEx25bAuto
Программа на VBA немедленно завершится, если не найдет IEx25bAuto в списке ссылок.
Как только VBA выполнит оператор CreateObject и загрузит компонент, он вызовет Query- Interjacgpra HD_IEx25bAutOjопределенного в Реестре, библиотеке типов и таблице интерфейсов класса компонента. (На самом деле 1Еэс25ЬАи1оявляеггса интерфейсом IDispatch?) Это делается для большей безопасности. Если компонент не сможет предоставить этот интерфейс, программана VBA завершится. Теоретически, Excel мог бы использовать для загрузки CLSID из библиотеки типов, но он использует CLSID из Реестра, как и в режиме позднего связывания.
Назначение раннего связывания
Вы, наверное, подумали, что раннее связывание ускорит работу компонента Автоматизации. Но, скорее всего, Вы не заметите этого из-за узкого места — вызовов 1В<5р«1сЬ::1п1>оке.Типич- ное обращение С++-клиента к MFC-функции Invoke в C++-компоненте занимает около 0.5 мс, что отнюдь немало. '
Возможность просмотра, предоставляемая библиотекой типов, вероятно, важнее, чем связывание на этапе компиляции. Если Вы пишете контроллер на C++, библиотеку типов можно загружать через разные OLE-функции, в том числе LoadT^pelib, после чего она становится доступна через интерфейсы ITypeLib и ГРуре/п/оНо реализация проектов с такими возможностями потребует больше времени, так как интерфейсы библиотеки типов весьма сложны.
Ускорение подсоединения контроллера к компоненту
Microsoft признает ограничения интерфейса IDispatch. Он медлителен по самой своей природе, потому что все данные пропускаются через переменные типа VARIAMlk зачастую преобразуются обеими сторонами (контроллером и компонентам). Сейчас появилась новая его разновидность — дуальный интерфейс (dual interface). [Обсуждение дуальных интерфейсов выходит за рамки этой книги. На эту тему см. книгу Крейга Брокшмидта (Kraig Brockschmidt) Inside OLE 2 (Microsoft Press, 1995, Second. Edition).] При работе с дуальным интерфейсом Вы определяете собственный интерфейс, производный от IDispatch. И него включаются не только Invoke и GetIDsOjNames,но и новые функции. Если клиент достаточно «сообразителен», он обойдет неэффективные вызовы Invoke и вместо этого воспользуется специализированными функциями. Дуальные интерфейсы могут поддерживать либо только стандартные типы данных Автоматизации, либо произвольные типы.
Прямая поддержка дуальных интерфейсов в MFC Visual C++ 6.0 не предусмотрена, но пример ACDUAL, приложенный к Visual C++, помажет получить первое представление о них.
ГЛАВА 26
Унифицированная передача данных
В технологии ActiveX заложен очень мощный механизм передачи данных как внутри Windows-приложений, так и между приложениями. Ключевым элементом механизма унифицированной передачи данных. (Uniform Data Transfer, UDT) в COM служит интерфейс IDataObject.
Как Вы увидите, этот механизм обеспечивает все виды форматирования и хранения передаваемых данных, выходя далеко за рамки стандартной передачи данных через буфер обмена.
MFC тоже поддерживает унифицированную передачу данных, но не в такой степени, чтобы скрыть все, что происходит на уровне интерфейсов СОМ. Одно из полезных применений единообразной передачи данных — операция drag-and-drop («перетащить и отпустить*). Многие разработчики включают ее в свои приложения как один из стандартов обмена информацией. Библиотека MFC тоже поддерживает операцию drag-and-drop, которая и является — вместе с передачей данных через буфер обмена — основной темой этой главы.
Интерфейс IDataObject
Интерфейс IDataObject применяются при передаче данных не только через буфер обмена и операцией drag-and-drop, но и в составных документах, ActiveX-элементах и специализированных средствах OLE В книге Inside OLE (Microsoft Press, 1995, Second Edition) Брокшмидт советует: «Рассматривайте объекты как крошечные пакеты данных*. Так вот, интерфейс IDataObject помогает перемещать эти пакеты независимо от их содержимого.
Программируя на уровне Win32 API, Вы написали бы С++-классдля поддержки IDataObject. Затем ваша программа создала бы обаекты данных (data objects) этого класса, и Вы обращались бы к ним через функции-члены IDataObject В этой главе Вы узнаете, как достичь того же результата, используя MFC-реализацию IDataObject Давайте начнем с того, что рассмотрим улучшения, которые OLE вносит в обычный буфер обмена Windows.
Преимущества IDataObject по сравнению со стандартной поддержкой буфера обмена
MFC никогда особо не поддерживала буфер обмена Windows. Если Вам уже работали с ним, то наверняка использовали Win 32-функции OpenCUpboard, CloseClipboard, GetClipboardData и SetClipboardData. Одна программа копирует данные в буфер обмена в определенном формате, а другая выбирает их оттуда по коду формата и вставляет в свой документ. Стандартные форматы буфера обмена включают участки глобальной памяти (определяемые по HGLO- BALj и GDI-объекты вроде растровых изображений и метафайлов (определяются их описателями). Глобальная память может включать данные в нестандартных форматах и текст.
Интерфейс IDataObjectдает то, чего не хватает буферу обмена Windows. Если коротко, то Вы передаете или считываете из буфера обмена указатель на IDataObject, а не отдельные дан-
ные в каком-то формате. Объект данных может содержать целый массив разных форматов, которые могут нести информацию об устройстве вывода, например, характеристики принтера, и определять так называемый аспект данных (data aspect), т. е. представление. Стандартный аспект — это само содержимое. Другими аспектами являются значок и микрокартинка (thumbnailpicture), отражающая хранимое изображение в уменьшенном виде.
Важно отметить, что интерфейс IDaiaObject задает носитель (storage medium) объекта данных в конкретном формате. Обычная передача через буфер обмена опирается исключительно на использование глобальной памяти. Интерфейс IDaiaObject позволяет передавать вместо глобальной памяти имя дискового файла или указатель на структурированное хранилище (structured storage). Таким образом, при передаче очень большого объема данных, хранящегося в файле надиске, нет необходимости тратить время на копирование его в оперативную память и обратно.
Между прочим, указатели IDaiaObjectсовместимы с программами, применяющими старые способы передачи данных через буфер обмена. Коды форматов совпадают. Windows обеспечивает автоматическое преобразование информации в объекты данных, и наоборот. Разумеется, если программа, поддерживающая OLE, поместит в объектданных указатель на IStoragem передаст объект в буфер обмена, старые программы не смогут читать данные в этом формате.
Структуры FORMATETCw STGMED1UM
Прежде, чем приступить к изучению функций-членов IDaiaObject, нужно рассмотреть две важные структуры СОМ, используемые в качестве типовпараметров: FORMATETC'aSTGMEDIUM.
FORMATETC
Эта структура часто используется вместо формата буфера обмена для описания формата данных. Она включает информацию об устройстве вывода, аспекте (представлении) и носителе данных. Ее поля описаны в таблице.
Тип
Имя
Описание '
CUPFORMAT
cfFormat
Структура, содержащая форматы буфера обмена: стандартные (например, CF^l'EXliyia текста или СР_В1Вдля сжатых изображений), нестандартные (например, RTF) и OLE (для создания связанных или внедренных объектов)
DVTARGETDEVICE-
ptd
Структура, содержащая информацию об устройстве вывода данных, включая имя драйвера устройства (может быть NULL),■
DWORD
dwAspect
Перечисляемая константа типа DVASPECT (DVASPECT CONTENT, DVASPECT.THUMBNAIL и т. д.)“
LONG
Undex
Обычно -1
DWORD
tymed
Определяет носитель, используемый для передачи данных _(TYMED_HGTOBAL, TYMED_FILE, TYMEDJSTORAGE и т. д.)
Индивидуальный объектданных содержит набор элементов FORMATETC, AlDataObjecto^ec- печивает способ их перечисления. Вог макрос, полезный для заполнения этой структуры: «define SETFORMATETC(fe, cf, asp, td, med, И) \
((fe).cfFormat=cf, \
<fe).'dwAspect=asp, \
<fe).ptd=td, \’
(fe).tymed=med, \■
(fe).lindex=li):
STGMEDIUM . .
Другаяважнаяструктура,используемая функциями-членами/£)<г/«О£>/есг—STGMEDIUM,пред- ставляющая собой описатель глобальной памяти, используемой в передаче данных. Вот поля этой структуры.
Тип
Имя
Описание
DWORD
tymed.
Определяет носитель; используется при маршалинге
НВГГМАР
bBUmap
Описатель растрового изображения*
HMETAFILEPICT
bMetaFitePict
Описатель метафайла*
HENHMETAFILE
bEnbMetaFile
Описатель расширенного метафайла*
HGLOBAL
hGlobal
Описатель глобальной памяти*
LPOLESTR .
IpszFUcName
Имя файла на диске (двухбайтовые символы)*
1STREAM*
pstm
Указатель на интерфейс {Stream*
ISTORAGE*
ps^
Указатель на интерфейс IStorage*
IUNKNOWN*
pUnkForRelease
Используется клиентами, чтобы вызвать Release для форматов с указателями на интерфейсы
* Эго иоле является частью объединения (union) вместе с другими, обозначенными аналогично.
Как видите, структура ЕТОМЕОШМопределяет, где хранятся данные. Поле tymed сообщает, какой элемент объединения используется.
Функции-члены интерфейса IDataObject
У интерфейса IDataObjectдевять функций-членов. Все они детально описаны в книге Брокш- мидта и в Books Online. Здесь упомянуты лишь те, которые важны для понимания материала этой главы.
HRESULT EnumFormatEtc(DWORD dwDirection, lEnumFORMATETC” ppEnumy,
Имея указатель на IDataObject для объекта данных, можно использовать EnumFormatEtcдля перебора всех форматов, поддерживаемых этим объектом. Библиотека MFC изолирует Вас от этого уродливого API. Как именно, Вы узнаете при рассмотрении класса COleDataObject.
HRESULT GetData(FORMATETC* pFEtn, STGMEDIUM* pSTM);
Самая важная функция интерфейса. Где-то «высоко-высоко в небесах» находится объект данных, а у Вас есть указатель на его интерфейс IDataObject. В переменной РОЕМАТЕТСВы задаете нужный формат и передаете пустую переменную STGMEDIUM.EcnH объект данных поддерживает заданный формат, GetData заполняет CTpyKVypySTGMEDIUM. В ином случае возвращается код ошибки.
HRESULT QueryGetData(FORMATETC* pfiE);
.Эту функцию вызывают, когда не уверены, может ли объект данных предоставить данные в нужном формате. Код результата сообщает «да, могу» (5'_О/С) или «нет, не могу» (SJRALSE) Вызов этой функции эффективнее, чем вызов GetData.
HRESULT SetData(FORMATETC* pFEfn, STGMEDIUM*ps™, BOOL fReleasey,
SetData редко поддерживается объектами данных. Обычно данные в них загружаются в серверном модуле, клиенты же выбирают данные вызовам GetData. Используя SetData, Вы передавали бы данные в обратном направлении — все равно что перекачивать воду из дома в водопровод.
Другие функции-члены iDatsObjech уведомления
Этот интерфейс содержит другие важные функции, позволяющие реализовать уведомления (advisory connections). Программа, которой нужно получать уведомления об изменении данных объекта, передает ему указатель lAdviseSink вызовом IDataObjectuDAdvise.J^anee объект вызывает функции-члены Z4rfpfce5/n&,реализуемые клиентской программой. Уведомления не нужны для drag-and-drop, но понадобятся, когда мы перейд ем к внедрению объектов в главе 27.
Поддержка унифицированной передачи данных в MFC •
В библиотеке MFC сделано многое, чтобы облегчить программирование объектов данных. При изучении MFC-классов объектов данных Вам станет понятен принцип, по которому построена поддержка СОМ в MFC. На стороне компонента библиотека обеспечивает базовый класс, реализующий один или несколько интерфейсов OLE. Функции-члены интерфейса вызывают виртуальные функции, которые Вы переопределяете в производном классе, а на стороне клиента библиотека предоставляет класс-оболочку указателя на интерфейс. Вы вызываете простые функции-члены, использующие этот указатель для обращения к СОМ.
Здесь стоит внести ясность в терминологию. Объект данных (data object) — это настоящий объект C++, создаваемый Вами, и именно в этом смысле данный термин используется Брокшмидтом. В документации по MFC объектданных — это то, что клиентское приложение «видит» через указатель на iDataObject. Объект, создаваемый программой компонента, называется там источником данных (data source).
Класс COleDataSource
Чтобы получить источник данных, Вы создаете объект класса COleDataSource, который реализует интерфейс IDataObject (без поддержки уведомлений), Этот класс создает и управляет набором форматов данных, хранимым в кэше (cache). Источник данных — это обычный объект СОМ, поддерживающий счетчик ссылок. Обычно Вы создаете и заполняете источник данных, после чего передаете его в буфер обмена или в функции, реализующие drag-and-drop, не беспокоясь о его дальнейшей суд ьбе. Если же Вы решили не передавать источник данных, можете вызвать его деструктор, выполняющий очистку всех форматов. Далее приведены некоторые наиболее полезные функции-члены COleDataSource: .
void CacheData(CLIPFORMAT cfiRvmef, STGMEDiUM* IpStgMadium, FORMATETC* tpFormatCtc= NULL);
Эта функция добавляет элемент к кэшу объекта данных для передачи данных. Параметр IpS^Medium задает, где находятся данные, а параметр IpFormatEtc описывает их. Например, если структура STGMEDIUM задает имя файла надиске, это имя и сохраняется в объекте данных. В том случае, если параметр IpFormatEtc не задан, функция заполняет структуру FOR- МАТЕТС значениями по умолчанию. Но безопаснее, если Вы создадите переменную FORMA- ТЕТС с установленным значением поля tymed.
void CacheGlobalData(CUPFORMAT cfFormat, HGLOBAL hGhbel, FORMATETC* /pFormatEtc = NULL);
Это специализированная версия CacbeData для размещения данных в глобальной памяти (определяемой параметром HGLOBAL). Владельцем блока глобальных данных становится объект — источник данных, так что Вам не надо освобождать блок после кэширования. Обычно параметр IpFormatEtc не применяют. Функция CacbeGlobalData не создает копию переданных ей данных.
DROPEFFECT DoDragDrop(DWORDdw£ff0Cfe= DROPEFFECT_COPY ! DROPEFFECT_MOVE!
DROPEFFECT-LINK, LPCRECT/pRectStartDrag = NULL, COIeDropSourca*pDropSource = NULL);
Эта функция вызывается для выполнения операции drag-and-drop. Ее использование Вы увидите в примере ЕХ26Б.■.■
void SetCHpboard();’'
Эта функция (см. пример ЕХ26А), вызывает OleSetClipboardpps. вставки источника данных в буфер обмена Windows. Буфер обмена отвечает за удаление источника данных и, тем самым, за освобождение глобальной памяти, связанной с форматами в кэше. Когда Вы создаете объект COleDataSource и вызываете SetClipboard, СОМ вызывает для объекта AddReJ.
Класс COIeDataObject
Этот класс (производный от ССтб^Тя^е^является принимающей стороной в передаче данных; у него есть открытая переменная-член m_lpDataObject,указывающая на IDataObject Вы должны установить эту переменную прежде, чем использовать объект. Деструктор класса лишь вызывает Release для указателя HalDataObject. Далее приведено несколько полезных функций COIeDataObject.
lBOOLAttachCllpboard();■
Как указывает Брокшмидг, внутренняя обработка буфера обмена в OLE достаточно сложна. Однако, с вашей точки зрения, она будет проста, если используются функции-члены COIeDataObject. Сначала создается «пустой» объект COIeDataObject, после чего вызывается функция AttachClipboard, которая обращается к глобальной функции OleGetClipboard. В результате, переменная-член mJpDataObjectyKasHBaerr на объект — источник данных (так это, по крайней мере, выглядит), и Вы получаете доступ к его форматам.
Вызывая функцию-член GetData, чтобы получить данные, помните: владелец данных - буфер обмена, и Вы не можете изменить его содержимое. Если форматом данных является указатель типа 1IGLOBAL, ж пытайтесь освобождать эту память или запоминать HGLOBAL для последующего использования. Вместо этого, если Вам нужен доступ к данным в глобальной памяти в течение продолжительного периода, подумайте об использовании GetGlobalData.
Если данные включены в буфер обмена программой, не поддерживающей СОМ, функция AttacbClipboard все равно действует, так как СОМ автоматически создает объект данных с форматами, соответствующими обычным данным в буфере обмена Windows.
void BeglnEnumFOrmate(};
BOOL QetNextFonna1(FORMATETC* IpFormatEtc);.
Эти две функции позволяют просматривать в цикле форматы, которые содержит объект данных. Сначала вызывается BeginEnumFormats, после чего на каждой итерации вызывается GetNextFonnatoo тех пор, пока ж вернет FALSE.
BOOL GetDa ta (CL IP FOR MAT cfFormef, STGMEDI UM*/pStgM»d/um, FORMATETC* IpFormatEtc = NULL);
Эта функция вызывает IDataObject: :GetDataa более ничего. Если источник данных содержит запрашиваемый формат, функция возвращает TRUE. Обычно требуется указывать параметр IpFormatEtc.., .
HGLOBAL GetGlobalData(CLIPFORMAT cfFormat, FORMATETC* IpFormatEtc = NULL);
Используйте эту функцию, если запрашиваемый Вами формат совместим с глобальной памятью. Функция копирует блок памяти в указанном формате, и возвращает описатель HGLOBAL, который Вы должны впоследствии освободить. Зачастую параметр IpFormatEtc можно ж указывать.■
BOOL IsDataAval lable(CLIPFOR MAT cfForma?, FORMATETC*IpFormatEtc» NULL);
Эта функция проверяет, содержит ли объект данных указанный формат.
Передача объекта данных через буфер обмена'■
Тежрь, познакомившись с классами COIeDataObject и COleDataSource, Вы можете запросто работать с буфером обмена. Но почему бы ж передать данные через буфер обмена старым методом — используя GetClipboardData и SetClipboardDdtalJIna наиболее распространенных форматов это действительно допустимо, но, написав функции, работающие с объектами данных, Вы сможете использовать эти же функции и для поддержки операции drag-and-drop. На рисунке 26-1 проиллюстрирована связь между буфером обмена и классами COleDataSourcen COIeDataObject.■
КопированиеКопированиеВставкаВставкаРис. 26-1. Обработка OLE-операций с буфером обмена в MFCРис. 26-1. Обработка OLE-операций с буфером обмена в MFC
Копирование
Копирование
Вставка
Вставка
Рис. 26-1. Обработка OLE-операций с буфером обмена в MFC
Рис. 26-1. Обработка OLE-операций с буфером обмена в MFC
Программа, копирующая данные, создает объект COIeDataSource, кэш которого заполняется форматами. Потом вызывается Setclipboard, и форматы помещаются в буфер обмена. Программа, вставляющая данные, вызывает A ttacbClipboardдда подключения указателя на IDataObject к объекту COleDataObject, после чего производит выборку отдельных форматов.
Допустим, в приложении, использующем архитектуру «документ-вид», у документа есть переменная-член /п_5ГгЗЫтипа CString. Нам нужны функции — обработчики команд в классе «вид», которые копируют и вставляют данные из буфера обмена. Прежде чем писать эти функции, следует написать две вспомогательные. Первая, SaveText, создает объект — источник данных на основе содержимого mjdrText. Она конструирует объект COIeDataSource, после чего копирует строку в глобальную память и, наконец, вызывает CacheGlobalData, чтобы сохранить в источнике данных описатель HGLOBAL. Вог код SaveText:
COIeDataSource* CMyView::SaveText() {
CEx26fDoc* pDoc = GetDocumentO;
if (!pDoc->m_strText.IsEmpty()) {
COIeDataSource- pSource = new COIeDataSource{);
int nTextSize = GetDocument()->m strText.GetLength() + 1;
HGLOBAL hText = ::GlobalAlloc(GMEM_SHARE, nTextSize);
LPSTR pText = (LPSTR) ::GlobalLock(hText);
ASSERT(pText);
stropy(pText. GetOooument()->m_strText):
::GlobalUnlock(hText);
pSource->CacheGlgbalData(CF_TEXT, hText);
returnpSource;-
}
return NULL;
)'
Вторая вспомогательная функция, DoPasteText, заполняет m_strTbxt содержимым объекта данных, переданного как параметр. Здесь мы используем COleDataObjectxGetData вместо функции GetGlobalData,так как последняя копирует глобальный блок памяти. Это копирование излишне, поскольку мы все равно скопируем текст в объект CString. Исходный блок памяти мы не освобождаем, потому что он принадлежит объекту данных. Вот код DoPasteText; BOOL CMyView::DoPasteText(C01eData0bject* pDataObject)
STGMEDIUM stg;
FORMATETC fmt;•
// функции, обновляющие командный пользовательский интерфейс,
// не должны допускать нас сюда, если CF.TEXT недоступен
if (!pDataObject->IsDataAvallable(CF_TEXT)) {
TRACE("Формат CF_TEXT недоступен\п");
return FALSE;
1 j Ц память является перемещаемой, поэтому надо использовать Global Lock! SETFORMATETC(fmt, CF_TEXT, DVASPECT_CONTENT, NULL, TYMED_HGLOBAL. -1); VERIFY(pDataObject->GetData(CF_TEXT, &stg, &fmt));
HGL08AL hText = stg.hGlobal;
GetDocument()->m_strText = (LPSTR) :;GlobalLock(hText);
: :GlobalUnlock(hText);
return TRUE;
А вот два обработчика команд:
void CMyView::OnEditCopy()
■I
COleDataSaurce* pSource = SaveText():
if (pSource) {
pSource->SetCl1pboa rd();
}
}
void CMyView::OnEditPaste() {
CDleDataObject dataObject;
VERIFY(dataObject.AttachClipboard());
DoPasteText(SdataObject);
// освобождаем dataObject
}
IM FC-класс CRectTracker
Класс CRectTracker полезен для программ, как использующих, так и не использующих OLE. Он позволяет пользователю перемещать прямоугольный объект и изменять его размеры в окне представления. В нем есть две важных переменных-члена: mjiStyle (определяет рамку объекта, маркеры его масштабирования и прочие характеристики) и mjrect (содержит аппаратные координаты объекта). Рассмотрим наиболее важные функции-члены.
void Draw(CDC* pDC) const;
Рисует прямоугольник (tracker), ограничивающий объект, в том числе рамку и маркеры масштабирования, но не прорисовывает собственно объект. Последнее — уже ваша задача.
BOOL Track(CWnd* pWnd, CPoirtt point, BOOL bA(lowinvert= FALSE, CWnd* pWndCHpTo= NULL);
Вызывается в обработчике сообщения WM_LBUTTONDOWN. Если курсор находится на рамке ограничивающего прямоугольника, пользователь может изменять размеры последнего, передвигая мышь при нажатой кнопке, а если курсор внутри прямоугольника — перемещать прямоугольник. Когда курсор оказывается за пределами прямоугольника, функция сразу же возвращает FALSE; в противном случае Track возвращает TRUE, но только после того, как пользователь отпустит кнопку мыши. Таким образом, поведение Track в чем-то схоже с поведением CDialog::DoModal. В нее заложена своя логика выборки сообщений.
int HrtTest(CPolntpo/nt) const;.
Вызывайте HitTest, если Вам надо различать нажатия кнопки мыши внутри и на ограничивающем прямоугольнике. Функция немедленно возвращает управление и сообщает код, определяющий положение курсора мыши относительно прямоугольника.
BOOL SetCur&or(CWnd‘pHW, UINTnHATesf) const;
Вызывайте ее в обработчике сообщения WM_SETCURSOR окна представления, чтобы изменять форму курсора при перемещении или масштабировании ограничивающего прямоугольника. Если возвращается FALSE, вызывайте функцию OnSetCursor из базового класса; а если TRUE, то возвращайте TRUE из своего обработчика,
Преобразование координат прямоугольника CRectTracker
Следует учитывать, что СКес$ТУаскег;:т_гес1содаржит аппаратные координаты. И Вам не избежать преобразований, если Вы используете прокрутку в окне представления, изменяете режим преобразования координат или смещаете начало координат окна. Общая стратегия следующая.
Определите в своем классе «вид» переменную-член типа CRectTracker с именем m_tracker.
Определите в том же классе отдельную переменную-член для хранения логических координат с именем m_rectTracher.
В функции OnDraw класса «вид» запишите в т_гескна^ые аппаратные координаты и нарисуйте ограничивающий прямоугольник. Это позволит скорректировать изображение, если с момента предыдущего вызова OnDraw окно было прокручено. Вот пример кода: n_tracker.m_rect = nLrectTracker:
pDC->LPtoDP(m_tracker.m_rect); // нужны аппаратные координаты
n_tracker. Draw(pDC);.
В обработчике левой кнопки мыши вызовите Track, сохраните обновленные логические координаты в т_гесгТгаскеги вызовите Invalidate:
if (m_tracker.Track(this, point, FALSE, NLLL)) {
CClientDC dc(this);
OnPreoarcDC(&dc);
rnjectTracker = m_tracker,m_rect;
dc.DPtoLP(m_rectTracker);
Invalidate^;
}■
Пример EX26A
В этом примере используется класс CDib из EXL1C. Вы сможете перемещать и масштабировать DIB-изображение с помощью ограничивающего прямоугольника, а также копировать DIB в буфер обмена и вставлять его оттуда в документ, используя объект данных СОМ. Кроме того, в примере реализованы функции для чтения и записи DIB в ВМР-файлы.
Если Вы создаете проект с самого начала, воспользуйтесь мастером AppWizard без всяких параметров, связанных с ActiveX и Автоматизацией, а потом добавьте в файл StdA£x.hcrpoKy: ttinclude <afxoleh>
а в функцию Initlnstance приложения — вызов:
Afx01eInlt();
Чтобы подготовить ЕХ26А к работе, откройте и соберите проект \vcpp32\ex26a\ex26a.dsw. Запустите программу и вставьте растровое изображение, используя команду Paste From в меню Edit, Вы увидите примерно такое MDI-приложение, как на рисунке 26-2.
Класс CMainFrame
В этом классе содержатся обработчики OnQueryNewPalette и OnPaletteCbanged сообщений WM_QUERYNEWPALETTE и WM_PALETTECHANGED соответственно. Эти обработчики посылают пользовательское сообщение WM_VIEWTALETTECIIANGED всем окнам представления,
а затем вызывается CDib;:UsePaiett<?для реализации палитры. Значение wParam сообщает, должна ли палитра реализовываться в фоновом режиме или нет.
Рис. 26-2. Программа ЕХ26А в действииРис. 26-2. Программа ЕХ26А в действии
Рис. 26-2. Программа ЕХ26А в действии
Рис. 26-2. Программа ЕХ26А в действии
Класс СЕх26аОос'
Весьма простой класс: содержит внедряемый СП1Ь-объектт_ЛЬ и обработчик команды Clear АН. Переопределенная функция-член DeleteContents вызывает функцию CDlb-Empty.
Класс CEx26aView
Содержит обработчики команд, связанные с буфером обмена, код, определяющий поведение ограничивающего прямоугольника, а также код прорисовки DIB. Листинг класса приведен на рис. 26-3; код, введенный вручную, выделен полужирным.
EX26AVIEW.H■
flif !defined(AFX EX26AVIEW_H_4F329B0F 5DF1_11D0 848F.00400526305B. INCLUDED > «define AFX_EX26AVIEK_H_4F329B0F_5DfLi 1D0.848F.00400526305B. .INCLUDED.
#if .MSC.VER > 1000
«pragma once
Wendif// .MSC.VER > 1000
«define WM.VIEWPALETTECHANGED WM.USER + 5
class CEx26aView : public CScrollVlew
’ // для ограничивающего прямоугольника
CRectTracker m.tracker;■
CRect m_rectiracker; // логические координаты
CSize m.sizeTotal; // размер документа protected: // создание только сериализацией!
CEx26aView();
DECLARE_DYNCREATE(CEx26aView)■
см. слей. стр.см. слей. стр.Рис. 26-3. Листинг класса CEx26aView
см. слей. стр.
см. слей. стр.
1Рис. 26-3. (продолжение)'
// Атрибуты
public;
CEx26aDoc* GetDocument();
И Операции
public:
// Переопределенные функции
// Переопределенные виртуальные функции, сгенерированные Classwizard
//{{AFX_VIRTUAL(CEx26aV: ew)
public:
virtual void OnOraw(CDC* pDC); // переопределна для отрисовки этого окна представления
virtual BOOL PreCreateWindow(CREATESTRL‘CT& cs);.
virtual void OnPrepareDCfCDO pDC, CPrintlnfo* plnfo = NULL);•
virtual void CnlnitialUpdateO;
protected:
virtual BOOL OnPreparePrinting(CPrintInfo* plnfo);
virtual void OnBeginPrlnting(CDC* pDC, CPrintlnfo* plnfo);
virtual void OnEndPrinting(CDC* pDC, CPrintlnfo* plnfo);
//}}AFX_VIRTUAL
.// Реализация
public:
virtual ~CEx26aView();
flifdef .DEBUG
virtual void AssertValidO const;
virtual void Dump(CDumpContext& de) const; itendif
protected:
,// сгенерированные функции таблицы сообщений protected:
//((AFX_MSG(CEx26aView)
afxjnsg void OnEditCopyf);
afx_msg void OnUpdateEditCopy(CCmdUI* pCmdUI);
afxjnsg void OnEditCbpyto();
afxjnsg void OnEditCutO;
afxjnsg void OnEditPasteO;
afxjnsg void OnUpdateEditPabte(CCmdUI* pCmdUI);
afxjnsg void OnEditPastefrom();
afxjnsg void OnLButtonDown(UINT nFlags, CPoint point);
afxjnsg BOOL OnSetCursor(CWnd* pWnd, UJNf nHitlest, UINT message);
afxjnsg LONG OnViewPaletteChanged(UINT wParam, LONG IParam);
afxjnsg void OnSetFocus(CWnd* pOldWnd);
//))AFX_MSG
DECLARE_MESSAGE_MAP()
private:
BOOL DoPasteDib(C01e0ata0bject* pDataObject);
COleDataSource* SaveDib();
•
tifndef ^DEBUG // отладочная версия в Ex26aVievi.cpp
inline CEx26aDoc* CEx26aView::GetDocument()
{ return (CEx26aDoc*)m_pDocument; }
ffendif
/////////////////////////////////////////////////////////////////////////
Н\{AFX.INSERT.LOCATION}}
И Microsoft Visual C++ вставит дополнительные объявления // непосредственно перед предыдущей строкой
flendif // ! defined(AFX_EX26AVIEW_H_4F329B0F_5DF1_11D0_848F_OO4O0526305B_INCLUDED_)
EX26AV1EW.CPP
«include "stdafx.h"
«include "ex26a.h"
«include "cdib.h"
«include "ex26aDoc,h"
«include ”ex26aView.h'’
flifdef_DEBUG
«define new DEBUG_NEW
«undef THIS„FILE
static char THIS_FILE[] = _FILE_;■
ffendif
/////////////////////////////////////////////////////////////////////// // CEx26aView
lMPLEMENT_DYNCREATE(CEx26aView, CScrollView)
BEGIN MESSAGE MAP(CEx26aView, CScrollView)
H\{AFX_HSG_MAP(CEx26a View)
ON COMMAND(ID EDIT COPY, OnEditCopy)
ON UPDATE COMMAND UK ID EDIT COPY, OnUpdateEditCopy)
ON COMMAND(ID_EDIT COPYTO. OnEditCopyto)
ON COMMANDED EDIT CUT, OnEditCut)
ON COMMAND(ID_EDIT PASTE, OnEditPaste)
ON~UP0ATE COMMAND UI (ID EDIT PASTE, OnUpdateEditPaste)
ON COMMANDED EDIT_PASTEFROM, OnEditPastefrom)
ON WM LBUTTONDOWNO
ON WM SETCURSOR()
ON MESSAGE(WM VIEWPALETTECHANGED, CnViewPaletteChanged)
ON_UPOATE_COMMAND_UI(ID_EDIT_COPYTO. OnUpdateEditCopy)■
ON UPDATE_COMMAND_UI(ID EDIT_CUT, OnUpdateEditCopy)
ON WM SETFOCUSO //]}AFX_MSG_MAP // стандартные команды печати ON_COMMAND(ID_FILE_PRINT, CScrollView; :OnFilePrint) ON COMMANDED FILE_PRINT DIRECT, CScrollView: :OnFilePrint) ON~COMMAND(ID FILE PRINT PREVIEW, CScrollView: :OnFilePrintPreview)
END_MESSAGE_MAP()
////////////////////////////////^ // CEx26aView создание/уничтожение
CEx26aView: :CEx26aView() : m_sizeTotal(800, 1050), // 8-на-10.5 дюймов при печати m_rectTracker(50 , 50 , 250, 250)
I
}•
CEx26aView: : ~CEx26aView()
{
}
Рис. 26-3. (продолжение)
BOOL CEx26aVlew.:PreCreatekindow(CREATESTRUCT& cs)
^ // ДОДЕЛАТЬ: Изменить классы или стили окна модификацией CREATESTRUCT cs return CScrollView::PreCreateWindow(cs);
////////////////////////////////////////////////////////////////////// // отрисоака CEx26aView
void CEx26aView::OnOraw(CDC* pDC) {
COib& dib = GetDocument()->m_dib;
m_tracker.m_rect = m_rectTracker;
pDC->LPtoDP^m_tracker,m_rect); // нужны аппаратные координаты
rntracker.Draw(pDC);
dib.Draw(pDC, m_rectTracker.TopLeft(), m_rectTracker.Size());
lllllllllllfill III МШНШНIIIП11П111111ПП111111ПП11П11П111
,// печать в CEx26aView
BOOL CEx26aView: :OnPreparePrinting(CPrlntInfo« plnfo) {
pinto->SetMaxPage(1);
return DoPreparePrinting(plnfo);
void CEx26aView: :OnBeginPrinting(CDC* /«pDC*/, CPriitlnfo* /*p!nfo*/> {
// ДОДЕЛАТЬ: добавить дополнительную инициализацию перед печатью
void CEx26aView: :OnEndPrinting(CDC* /«рОО/, CPrintlnfo* /«plnfo*/)
// ДОДЕЛАТЬ: добавить очистку после печати
ППППППППППППППППППППППППППППППППППП 11 диагностика CEx26aView ♦
#ifdef „DEBUG
void CEx26aView: :AssertValid() const
{
CScrollView::AssertValid();
void CEx26aView: :Dump(CDumpContext& de) const {
CScrollVievy; :Dump(dc);
CEx26aDoc* CEx26aView: :GetDocument() // неотладочная версия подставляется в строку {
ASSERT(m„pDocument->IsKindOf(RUNTIME„CLASS(CEx26aDoc)));
return (CEx26aDoc»)mpDocument;
flendif//„DEBUG
////////////////////////////////////////////////////////////// // вспомогательные функции для работы с буфером обмена и drag-drop BOOL CEx26aView: :DoPasteDib(C01eData0bject* pDataObject)
' // функции обновления командного пользовательского интерфейса не
// должны допускать нас сюда если CF_DIB недоступен
if (!pDataObject->IsDataAvailable(CF_DIB)) {
TRACE("Формат CF_DIB недоступен\п"): return FALSE;
}
CEx26aDoc* pDoc = GetDocument();
// Может быть ПЕРЕМЕЩАЕМАЯ память, поэтому мы должны использовать GlobalLockl // (hDib 1= IpDib) GetGlobalData копирует память, поэтому мы можем
// использовать его, пока не удален CDib.
HGLOBAL hoib = pDataObject->GetGlobalData(CF_DIB);
ASSERT(hDib != NULL);'
LPVOID IpDib = : ;GlobalLock(hDib);'
ASSERT(lpDib != NULL);
pDoc->m_dib. AttachHemory(lpDib, TRUE, hDib);
pDoc->SetModifiedFlag();
pDoc->UpdateAUViews(NULL);
return TRUE;
COleDataSource* CEx26aView: :SaveDib() {
CDib& dib = GetDocument()->m_dib;
if (dib.GetSizelmageO > 0) {
COleDataSource* pSource = new COleDataSourceQ;
int nHeaderSize = dib.GetSizeHeader();
int nlmageSize = dib.GetSizelmageO;
HGLOBAL hheader = : :GlobalAlloc(GMEM_SHARE, nHeaderSize + nlmageSize);
LPVOID pHeader = : :GlobalLock(hHeader);
ASSERT(pHeader != NULL);
LPVOID plmage = (LPBYTE) pHeader + nHeaderSize;
memcpy(pHeader, dib.m_lpBMIH, nHeaderSize);
memcpy(plmage, dib.m_lplmage, nlmageSize);
// Получатель должен освободить глобальную память
::GlobalUnlock(hHeader);
pSource->CacheGlobalData(CF_DIB, hHeader);
returnpSource;
}
return NULL;
////////////////////////////////////////////////////////////////////// // обработчики сообщений CEx26aView
void CEx26aView;;OnEditCopy()
COleDataSource* pSource = SaveDib();
if (pSource) {
pSource->SetClipboard(); // OLE удалит источник данных
void CEx26aView::OnUpdatREditCopy(CCmdUI* pCmdUI)
CH след. стр.CH след. стр.// обслуживает Copy, Cut и Copy To
CH след. стр.
CH след. стр.
|Рис. 26-3. (продолжение)
COibl dib = GetDocument()->m_dib;
pCmdUI->Enable(dib.GetSizeImage() > 0L);
void CEx26aView::DnEditCopyto()
CDib& dib = GetDocument()->n_dib;
CFileDialog dlg(FALSE, "bmp", "*.bmp"); if (dig.DoModal() != IDOK) return;
IBeginWaitCursorO; dib,CcpyToMapFile(dlg.GetPathName()); EndWaitCursor();
void CEx26aView: zOnEditCutO
OnEditCopyO;
GetOocument()->OnEditClearAll();
void CEx26aView:zOnEditPasteO
CEx26aDoc* pDoc = GetDocument();
COIeDataObject dataObject;
VERIFY(dataObject.AttachClipboard());
DoPasteDib(&dataObject);
CCIientDC dc(this);
pDoc->m_dib.UsePalette(&dc);
pDoc->SetModifiedFlag();
pDoc->UpdateAHVlews(NULL):
void CEx26aView: :OnUpdateEcitPaste(CCmdllI‘ pCmdUI) ■!
COIeDataObject dataObject;
BOOL bAvail = dataObject.AttachClipboardO && dataObject.IsDataAvailable(CF.DIB): pCmdUI->Enable(bAvail);~
void CEx26aView:zOnEditPastefrom()
{
СЕх2ва0ос» pDoc = GetDocument();
CFileDialog dlg(TRUE, "hnp", ”*.bmp");
if (dlg.DoModal(> j= TOOK) return;
if (pDoc->m_dib.AttachMapFile(dlg.GetPathNamB(), TRUE)) { // разделяемый
CCIientDC dc(this);
pDoc->m_dib.SetSystemPalette(&dc);
pDoc->m_dib.UsePalette(4dc);
pDoc->SetModifiedFlag():
pDoc->UpdateAHViews(NULL);
void CEx26aView: :OnPrepareDC(CDC* pDC, CPrintlrfo» plnfo)
* // нестандартный MM_L0ENGLISH; положительные у ВНИЗ if (pDC->IsPrinting()) {
int nHsize = pDC->GetDeviceCaps(HORZSIZE) * 1000 / 254;
int nVsize = pDC->GetDeviceCaps(VERTSIZE) * 1000 / 254;
pDC->SetMapMode(MM_ ANISOTROPIC);
pDC->SetWindowExt(nHsize, nVsize);
pDC->SetViewportExt(pDC->GetDeviceCaps(HORZRES), pDC->GetDevlceCaps(VERTRES));
} else {
CScrollView; :OnPrepareDC(pDC. plnfo):
void CEx26aView: :OnInitialUpdate()
{
SBtScrollSizes(MM_TEXT, m_sizeTotal);
m_t racket. m_nStyle = CRectTracker: :solidline | CRectTracker:: resizeQutside; CScrollView::OnInitialUpdate();
}'
void CEx26aView: :OnLButtonDown(UINT nFlags, CPoint point) <
if (m_tracker.Track(this, point, FALSE, NULL)) {
CCIientDC dc(this);
OnPrepareOC(&dc);
m_rectTracker = m_tracker.m_rect;
dc.DPtoLP(m_rectTracker); H Обновить логические координаты InvalidateO;
BOOL CEx26aView: ;OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
if (m_tracker.SetCursor(pWnd, nHitTest)) { return TRUE;
} else {
return CScrollView: :OnSetCursor(pWnd, nHitTest, message);
LONG CEx26aView: ^rViewPaletteChangecCUINTwParam, LONG IParam)
TRACE("CEx26aView::OnViewPaletteChanged, HWND = Kx, \ code = Kd\n", GetSafeHwnd(), wPararn);
CCIientDC dc(this);
GetDocument()->m_dib.UsePalette(&dc, wParam);
Invalidated;
return 0;
void CEx26aView: :OnSetFocus(CWnd- pCldWnd)
CScrollView::0nSetFocus(p01dWnd);
AfxGetApp()->m_pMainWnd->SendMessage(WM_PALETTECHANGED,
(UINT) GetSafeHwndO);
В классе «вид» происходит несколько интересных вен^й. Во вспомогательной функции DoPasteDib мы можем вызвать GetGlobalData, потому что мы можем подсоединить возвращаемую переменную типа HGLOBAL к объекту CDib в документе. Если бы мы воспользовались GetData, нам пришлось бы самим копировать блок памяти. Обработчики команд Paste From и СоруТо опираются па поддержку проецируемых в память файлов, которая заложена в класс CDib. Функция ОпРгерагеОСсоздает особый режим преобразования координат на принтере,
совпадающий с MM_LOENGUS№a исключением того, что положительные координаты по оси у откладываются вниз. Одна точка на экране соответствует 0,01 дюйма на принтере.
Поддержкас1гад-апс1-с1горвМРС
Самый веский аргумент в пользу кода объекта данных — возможность поддержки операции drag-and-drop. OLE предоставляет для этого интерфейсы IDropSource и iDropTarget, а также библиотечный код, управляющий процессом перемещения объектов. Библиотека MFC поддерживает drag-and-drop на уровне класса «вид», чем мы и воспользуемся. Учтите, что в процессе выполнения drag-and-drop данные передаются напрямую и независимо от буфера обмена. Если пользователь отменяет операцию, факт перемещения объекта не запоминается.
Передача данных операцией drag-and-drop между приложениями, окнами в одном приложении и в пределах конкретного окна осуществляется, с точки зрения пользователя, одинаково (единообразно). Когда начинается эта операция, форма курсора должна изменяться на стрелку с прямоугольником. Если пользователь удерживаетклавишу Ctrl, знакплюс (+) в изображении курсора указывает на то, что объект копируется, а не перемещается.
MFC поддерживает также drag-and-drop элементов составных документов. Это следующий, более высокий уровень поддержки OLE в MFC, который в этой главе не рассматривается. См. пример OCLIENT в интерактивнойдокументации на Visual C++.
Что происходит в источнике
Начиная операцию drag-and-drop для объекта данных, программа-источник вызывает COle- DataSource::DoDragDrop. Эта функция создает объект MFC-класса COleDropSource, реализующего интерфейс IDropSource. Функция DoDragDrop — из числа тех функций, что возвращают управление не сразу. Возврат из нее происходит, только когда пользователь «отпускает» объект или отменяет операцию, или когда проходит заданное количество миллисекунд.
Если Вы программируете drag-and-drop так, чтобы эта операция работала с объектом CRectTracher, то DoDragDrop следует вызывать, только если пользователь щелкает мышью внутри ограничивающего прямоугольника, но не на рамке. Нужную информацию даст функция CRectlVacker-HiiTestAIspep. вызовом DoDragDrop следует инициализировать флажок, который подскажет, не «отпущен» ли объект там же, откуда началось его перемещение.
Что происходит в приемнике
Чтобы задействовать для drag-and-drop поддержку из класса «вид» библиотеки MFC, добавьте в свой производный класс «вид» переменную-член класса COleDropTarget.Kna.ee COleDropTarget реализует интерфейс IDropTarget и хранит указатель на IDropSource, обеспечивающий обратную связь с объектом COleDropSource. В функции OnlnitialUpdateBamero класса «вид» вызовите функцию-член Register для внедряемого объекта COieDropTatget. .
Создав COleDropTarget для своего класса «вид», переопределите четыре виртуальные функции CVieiv, которые MFC вызывает в процессе выполнения drag-and-drop. Вот что должны делать эти функции, если Вы пользуетесь классом CRectTracker.
OnDragEnter OnDragOuerOnDragLeave OnDropOnDragEnter OnDragOuerOnDragLeave OnDropКорректирует фокусный прямоугольник и вызывает OnDragOver
OnDragEnter OnDragOuer
OnDragLeave OnDrop
OnDragEnter OnDragOuer
OnDragLeave OnDrop
Перемешает пунктирный фокусный прямоугольник и определяет эффект «отпускания» объекта (задает форму курсора)
Отменяет операцию и возвращает исходные размеры н координаты прямоугольника
Корректирует фокусный прямоугольник н вызывает вспомогательную функцию DoPaste, чтобы извлечь форматы из обьектаданных
Последовательность действий при drag-and-drop
На рисунке 26-4 проиллюстрирована обработка операции drag-and-drop в MFC.
Рис. 26-4. Обработка процесса drag-and-drop в MFC
Бот что происходит в процессе выполнения drag-and-drop.
Пользователь нажимает левую кнопку мыши в окне представления — источнике.
Обработчик этой кнопки вызывает CRectTracker::HitTestn определяет, что курсор находится внутри ограничивающего прямоугольника.
Обработчик сохраняет форматы в объекте COleDataSource.
Обработчик вызывает COleDataSource: :DoDragDrop;sjK$. источника данных.
Пользователь перемещает курсор в окно представления — приемник.
OLE вызывает !DropTarget::OnDrag^ntew. OnDragOversna объекта COleDropTarget^ результате чего вызываются соответствующие виртуальные функции класса «вид» приемника. Функции OnDragOver передается указатель на COleDataObjectp^a объекта-источника, который получатель использует для проверки наличия поддерживаемых им форматов.
OnDragOver возвращает кед эффекта ♦отпускания», используемый OLE для установки фермы курсора.
OLE вызывает IDropSource-QueryContinueDra^a стороне источника, чтобы определить, следует ли продолжать операцию перемещения. MFC-класс COleDataSource реагирует соответственно.
Пользователь отпускает кнопку мыши, чтобы «оставить» объект в окне представления - приемнике.
OLE вызываетIDropTarget::OnDrop?i та — OnDrop класса «вид» приемника. Так как OnDrop передается указатель на COleDataObject, она может выбрать из этого объекта данные в нужном формате.
Когда OnDrop возвращает управление в программе-приемнике, DoDragDrop в программе- источнике тоже может вернуть управление.
Пример ЕХ26В
Эта программа подхватывает эстафету у ЕХ26А. В нее добавлена поддержка drag-and-drop с использованием уже имеющихся вспомогательных функций SaveDib и DoPasteDib. Код для работы с буфером обмена тот же. Этот пример можно адаптировать под другие приложения, требующие применения drag-and-drop для объектов данных.
I 8 Зак. №787
Для подготовки ЕХ26В откройте и соберите проект \vcpp32\ex26b\ex26b.dsw. Запустите приложение и попробуйте что-нибудь переместить мышью между его дочерними окнами и между копиями этой программы.
Класс CEx26bDoc.
Этот класс почти такой же, как СЕх2баВос, кроме новой перемённой-члена mJbDragHere, используемой как флажок. Если его значение равно TRUE, идет операция drag-and-drop для данного документа. Флажок задан для документа, а не для окна представления, так как у одного документа может быть несколько представлений. Перемещение DIB из одного окна представления в другое бессмысленно, если оба они ссылаются на mdib из одного документа.
Класс CEx26bVtew
В этом классе три дополнительных перемет гных-члена и конструктор, их инициализирующий:
CRect m_rectTrackerEnter; // исходные логические координаты
COleDropTarget m_dropTarget;
CSize m_dragOffset; // аппаратные координаты
CEx25bView; :CEx25bView() : m_sizeTotal(800, 1050), //8 x 10.5 дюймов при печати
Hi_rectTracker(50, 50, 250, 250),
m_dragOffset(0, 0),
iT_rectTrackerEnter(50 , 50, 250, 250)
В функции OnlnitialUpdateHywHa. дополнительная строка для регистрации получателя: m_dropTarget.Register(this),
А теперь посмотрим на переопределенные виртуальные функции для drag-and-drop. Заметьте, что OnDrop заменяет DIB, только если флажок m_bDra^teres документе равен TRUE; поэтому, когда пользователь «отпустит» DIB в том же окне (или в другом окне, подсоединенном к тому же документу), ничего не произойдет.
DROPEFFECT CEx26bView: :"0n0ragEnter(C01eDataCbject« pDataObject,
DWORD dwKeyState, CPoint point)
{■
m_rectTrackerEnter = m_rectTracket; // сохранить начальные координаты на случай,
// если объект не будет "отпущен" на новом месте
CClientDC dc(this);
OnPrepareDC(&dc):
de. DrawFocusRect(m_rectTracket); // будет стерт в OnDragQver
return OnbragOver(pDataObject, dwKeyState, point);
DROPEFFECT CEx26bView: :0nDrag0ver(C01eData0bjcc> pDataObject, DWORD dwKeyState, CPoint point)
if (!pDataObject->IsDataAvailable(CF_DIB)) { returnDROPEFFECT NONE;
MovcTrackRect(point);
if ( (dwKeyState & MK CONTROL) == MK CONTROL) { return DROPEFFECT COPY;
// проверка на принудительное перемещение
if ((dwKeyState & MK ALT) == MK ALT) { return DROPEFFECT MOVE;'
// по умолчанию рекомендуется предполагать перемещение return DROPEFFECT.MOVE;
void CEx26bView: ;OnDragLeave().
' TRACECEnterlng CEx25bView: :OnDragLeave\n");■
CCIientDC dc(this):.
OnPrepareDC(Mc);.
de.DrawFocusRect(m_rectTracker);
m_rectTracker = m.rectTrackerEnter; // забудьте, что это было
BOOL CEx26bView: :0nDrop(C01eData0bject* pDataObject, DROPEFFECT dropEffect, CPoint point) ' ' {
TRACE("Входим в CEx26bView: :OnDrop - dropEffect = %d\n", dropEffect);
BOOL bRet;
CEx26bDoc* pOoc = GetDocumentO;
MoveTrackRect(point);
if(pDoc->m_bDragHere) {"
pDoc->m_bDragHere = FALSE;
bRet = TRUE;
1
else {
bRet = DoPasteDib(pDataObject);
}'
return bRet;
}'
Обработчик сообщения WM_LBUTTONDOWN требует существенной переработки. Он должен вызывать DoDragDrop, если курсор находится в прямоугольнике, и Track — если на его рамке. Вот новый вариант кода:
void CEx26bView::0nLButtonDown(UINT nFlags, CPoint point)
{'
CEx26bDoc* pDoc = GetDocumentO;
if(m_tracker.HitTest(point) == CRectTracker::hitMiddle) {
COleDataSource* pSource = SaveDib():
if(pSource) {
// DoDragDrop возвращает управление только по окончании перемещения'
CCIientDC de (this);'
OnPrepareDC(Sdc); CPoint topleft = m_rectTracker.TopLeft(); de.LPtoDP(Stopleft); // 'point" здесь не то же самое, что параметр point в OnDragEnter,.
// поэтому мы используем ее для вычисления смещения m_dragOffset = point - topleft; // аппаратные координаты pDoc->m_bDragHere = TRUE;
DROPEFFECT dropEffect - pSource->DaDragDroo(
DROPEFFECT.MOVE I DROPEFFECT.COPY, CRect(0, 0, 0, 0));’
TRACE("после DoDragDrop - dropEffect = Md\n", dropEffect);
if (dropEffect — DROPEFFECT.MOVE && pDoc->m_bDragHere) {'
pDoc->OnEditClearAll();
j
pDoc->m_bDragHere = FALSE;
delete pSource;
}
}
else {
if(m_tracker.Track(thLS, point, FALSE, NULL)) { CCIientOC dc(this);
0nPrepareDC(8dc):
// надо как-то не допустить выход за границы mrectTracker = m_tracker.m_rect;t -:
de. DPtoLP(m_rectTracker); /7 обновляем логические координаты*'‘
, }
Invalidate();
И, наконец, новая вспомогательная функция MoveTrackRect,показанная ниже, перемещает фокусный прямоугольник при каждом вызове OnDragOver. В примере ЕХ26А эту работу выполняла CRectTracker::Тгаск.
void CEx26bView::MoveTrackRect(CPoint point)
CClientDC dc(this);
OnPrepareDC(8dc);.
dc.DrawFocusRect(m_rectTracker);.
de. LPtoDP(m_rectTracket); CSize sizeTrack = m_rectTracker.Size();1
CPoint newTopleft ="point - m_dragOffeet; // по-прежнему аппаратные координаты m_rectTracker = CRect(newTopleft, sizeTrack); m_tracker.m_rect = m_rectTracker; de.DPtoLP(m_rectTracker); de.DrawFocusRect(m_rectTracker);
Приложения Windows и drag-and-drop: Dobjview. .
Мы тестировали EX26B с пакетом Microsoft Office 97, опробовав передачу данных как через drag-and-drop, так и через буфер обмена. Результаты показаны в таблице.
ЕХ26В Word Excel PowerPoint
Посылает данные через буфер обмена вхх (без палитр) х '
Принимает данные через буфер обмена из
Посылает данные операцией drag-and-drop в хх
Принимает данные операцией drag-and-drop из
Разбираясь, почему эти программы отказываются «сотрудничать* с нашей программой, мы открыли для себя очень полезную OLE-утилиту Dobjview (средство просмотра IDataObjecty Она позволяет исследовать объект данных, находящийся в буфере обмена Кроме того, объекты можно перетаскивать в окно этой программы. Вот что мы получили, перетащив картинку из Excel:
BSereen Fioture iEebedtiod Object U0b)eet DescriptorBSereen Fioture iEebedtiod Object U0b)eet Descriptorptd-fKUU,] ИД-(НПЩ ptd-<HULI] pid-tNOLt)ptd-fKUU,] ИД-(НПЩ ptd-<HULI] pid-tNOLt)drtpeoat ■ (СемемdvApeeet r-cCeeteetdvipeert *(Cc*t*ntdvlpeect■(CoMeMdrtpeoat ■ (СемемdvApeeet r-cCeeteetdvipeert *(Cc*t*ntdvlpeect■(CoMeMlxatefi-и ty»d”<wrieb } tyMd-<IStoreoe j liodeor-f-l) tyxed-lhSlobel }lxatefi-и ty»d”<wrieb } tyMd-<IStoreoe j liodeor-f-l) tyxed-lhSlobel }
BSereen Fioture iEebedtiod Object U0b)eet Descriptor
BSereen Fioture iEebedtiod Object U0b)eet Descriptor
ptd-fKUU,] ИД-(НПЩ ptd-<HULI] pid-tNOLt)
ptd-fKUU,] ИД-(НПЩ ptd-<HULI] pid-tNOLt)
drtpeoat ■ (СемемdvApeeet r-cCeeteetdvipeert *(Cc*t*ntdvlpeect■(CoMeM
drtpeoat ■ (СемемdvApeeet r-cCeeteetdvipeert *(Cc*t*ntdvlpeect■(CoMeM
lxatefi-и ty»d”<wrieb } tyMd-<IStoreoe j liodeor-f-l) tyxed-lhSlobel }
lxatefi-и ty»d”<wrieb } tyMd-<IStoreoe j liodeor-f-l) tyxed-lhSlobel }
Формат CFJDIB здесь отсутствует. Так что, если Вы хотите вставлять картинки из Excel, в ЕХ26В надо ввести поддержку метафайлов. Другой вариант — переписать программу с включением в нее поддержки составных документов, как описано в главе 28. Библиотеки OLE содержат код для показа растровых изображений и метафайлов. .
ГЛАВА 27
Структурированное хранилище
Лодобно Автоматизации и унифицированной ire редаче данных, структурированное хранилище — одно из средств СОМ, эффективное само по себе. В тоже время, оно служит основой других ActiveX-технологий, в частности составных документов (compound documents).
В этой главе Вы научитесь считывать и записывать составные файлы, используя интерфейсы IStorage и IStream. Интерфейс /Логану применяется для создания и управления объектами структурированных хранилищ, a IStream служит для манипуляции данными, содержащимися в объекте-хранилище. Эти интерфейсы, как и остальные интерфейсы СОМ, — просто объявления виртуальных функций. Составные файлы, напротив, реализуются в OLE32.DLL. Они являются стандартом Microsoft на файловый ввод/вывод, и их можно рассматривать как «файловую систему в файле*.
Познакомившись с интерфейсами IStorage и IStream, мы перейдем к изучению интерфейсов IPersistStorageK iPersistStream,которые позволяют запрограммировать класс на сохранение и загрузку его объектов из составного файла. Вы говорите объекту: «Сохрани себя сам», и он делает это.
Составные файлы
В этой книге обсуждается четыре способа файлового ввода/вывода. Вы можете читать и записывать последовательные файлы вроде MFC-архивов (глава 17); использовать систему управления базой данных (главы 31 и 32); самостоятельно программировать произвольный доступ к файлу; и, наконец, использовать составные файлы (compound files).
Рассматривайте составной файл как полноценную файловую систему внутри файла. На рисунке 27-1 изображен традиционный дисковый каталог, поддерживаемый операционными системами MS-DOS и Microsoft Windows. Он состоит из файлов и подкаталогов с корневым каталогом на вершине. Теперь представьте такую же структуру внутри одного дискового файла. Файлы в такой структуре называются потоками (streams), каталоги — храншиицами (storages). Каждый идентифицируется именем длиной до 32 широких символов. Поток - логически последовательный массив байтов, хранилище — набор потоков и вторичных хранилищ (substorages). (Хранилище может содержать другие хранилища точно так же, как каталог — подкаталоги.) Байты дискового файла необязательно хранятся в смежных кластерах. Аналогично, и байты потока в составном файле необязательно размещаются последовательно. (Это лишь кажется, что они расположены последовательно.)
ПРИМЕЧАНИЕ Символы /, \, : или I в именах хранилищ и потоков недопустимы. Если значение первого символа меньше ASCII-кода 32, соответствующий элемент помечается как управляемый другим агентом, отличным от владельца.
Г
Рис. 27-1. Дисковый каталог с файлами и подкаталогами
■Областей применения составных файлов очень много. Классический пример — документ, состоящий из глав и параграфов внутри глав. Размер документа достаточно велик, поэтому считывать его в память целиком нежелательно; кроме того, надо иметь возможность вставлять и удалять фрагменты документа. В таком случае Вы могли бы разработать составной файл, корневое хранилище которого содержит вторичные хранилища для глав; последние содержали бы потоки параграфов. Можно предусмотреть потоки и для индексной информации.
'Одно из полезных свойств составных файлов — способность обрабатывать транзакции (transactions). Когда Вы начинаете транзакцию для составного файла, все изменения сохраняются во временном файле и переносятся в основной только по завершении транзакции.
Хранилищам интерфейс/storage ,
Объектом «хранилище» можно управлять через интерфейс IStorage. Обратите внимание на функции этого интерфейса, так как MFC не поддерживает доступ к хранилищам. Рассмотрим важнейшие из его функций-членов (с описанием наиболее значимых параметров).
HRESULT СоттЩ...);
Сохраняет для данного хранилища и всех «нижестоящих» элементов изменения, совершенные при последней транзакции.
HRESULTCopyTo(...IStorage“pStgDesy;
Копирует хранилище под тем же именем — а также его вторичные потоки и хранилища (рекурсивно) — в другое существующее хранилище, При записи элементов в хранилище-приемник одноименные элементы последнего замещаются новыми.
IH RESULT CreateStorage(const OLECHAR*p/Vame,..., DWORD mode,IStorage** ppSfg);
Создает в данном хранилище новое, вторичное хранилище.
HRESULT CroateStreamfconst О LECH AR* pName,..., DWORD mode,..., IStream** ppStream);
Создает в данном хранилище новый поток.
IHRESULT Destr oyElement(const OLECHAR* pName);
Удаляет указанное хранилище или поток в данном хранилище. Хранилище не может удалить само себя.
HRESULT EnumElements(..., lEnumSTATSTG**ppEnumStatstgF);
Перечисляет все хранилища и потоки в данном хранилище. Интерфейс IEnumSTATSTG)MLK и другие интерфейсы-перечислители, содержит функции-члены Next, Skip и Clone.
HRESULT MoveEkyrientTo(conetOLECHAR* pMfame, IStorage*pStgDest, const OLECHAR*pNewName, DWORD flags)’, Перемещает элемент изданного хранилища в другое.
HRESULTOpenStream(const OLECHAR*pName...., DWORD mode,..., IStream"ppSfream);
Открывает указанный (существующий) поток, находящийся в данном хранилище.
HRESULT OpenStorage(const OLECHAR*p№me,..., DWORD mode,..., IStorage** ppStg);
Открывает в данном хранилище указанное (существую щее)хранилище.
DWORD Release^;
Уменьшает счетчик ссылок. Если хранилище является корневым, т. е. представляет дисковый файл, Release закрывает его, как только счетчик ссылок достигает О.
HRESULT RenameElament(const OLECHAR*pOldName, const OLECHAR*pNewName);
Присваивает новое имя существующему потоку или хранилищу, расположенному в данном хранилище.
HRESULT RevertO;
Отменяет транзакцию, не изменял составной файл.'
HRESULT SetClass(CLSID& c/s/d);
Вставляет в данное хранилище 128-разрядный идентификатор класса, который сможет считывать функция Stat.
IH RESULT Stat(STATSTG* pStatstg, DWORD flag);
Заполняет структуру информацией о данном хранилище, сообщая, в том числе, его имя и идентификатор класса,
Получениеуказателя на/Storage
Откуда взять первый указатель на IStorage? В СОМ предусмотрена глобальная функция StgCrea- teDocJUe,создающая надиске файл структурированного хранилища, и функция StgOpenStorage, открывающая существующий файл этого вида. Обе функции возвращают указатель на корневое хранилище (root storage) файла. Ниже приведен код, который открывает существующий файл MyStoresig и создает новое вторичное хранилище.
IStorage* pStgRoOt;
IStorage- pSubStg;
if (::StgCreateDocrile(L MyStare.stg". STGM_READWRITE ! STGM_SHARE_EXCLUSIVE I STGM.CREATE, .
0, &pSt gRoot) == S_OK) {'
if (pStgRoot->CreatcStcrage(L"MySubstorageName',STGM_REAOWRITE E STGM_SHARE EXCLUSIVE I STGM_CREATE. 0, 0, SpSubStg) == S_OK) {■
// делаем что-нибудь c pSubStg
pSubStg->Release();';' -
pStgRoo t - >Rel easeO ;
Освобождение памяти в STATSTG.
Когда IStorage.;StatBH3HBaeTca co значением 57И77^ж;_Р£7/1Л/7.й'араметра-флажка, COM выделяет память для имени элемента. Вы должны освобождать эту память способом, совместимым со способом ее выделения. В СОМ своя система распределения памяти, использующая объект-распределитель (allocator object) с интерфейсом IMaUoc. Ваша задача — получить от СОМ указатель на IMaUoc, вызвать для символьной строки IMaUoc: :Ргееи. освободить объект- распределитель (пример кода см. в следующем разделе). . .
Если Вас интересует только тип и размер элемента, но не его имя, можете вызвать Stat с флажком STATFLAG_NONAM1B этом случае память не выделяется, и ее не придется освобождать. Все эти правила слегка раздражают, но пренебрегать ими нельзя — иначе утечка памяти неизбежна.
{Перечисление элементов хранилища
Приведенный ниже код «проходит» по всем элементам хранилища, различая вторичные хранилища и потоки. Элементы, кажется, считываются произвольно, независимо от того, в какой последовательности они создавались; единственное, что мы подметили, — потоки считываются первыми. Функция lEnumSTATSTG-NexBano^ структуру STATSTG ишрормаци- ей, сообщая в том числе, является ли текущий элемент потокам или хранилищем.
lEnumSTATSTG» pEnum;
IMalloc* pMalloc;
STATSTG statstg;
extern IStorage» pStg; // возможно, получен вызовом OpenStorage
::CoGetMalldc(MEMCTX_TASK, &pMalloc); //предполагаем, что AfxOlelnit уже была вызвана yERIFY(pStg->EnumElements(O, NULL, 0, SpEnum) — S_OK)•
mhile (pEnum->Next(1, &stats7g. NULL) == NOERROR) {
if (statstg.type == STGTY STORAGE) {
if (pStg->OpenStorage(statstg.pwcsName, NULL, STGM READ ! STGM SHARE EXCLUSIVE.
NULL, 0, ApSubStg) == S_OK) {
// делаем что-то co вторичным хранилищем
}
else if (statstg.type == STGTY_STREAM) {
// обрабатываем поток
}
pMalloc->Free(statstg.pwcsName); //избегаем утечек памяти
}
oMalloc->Release();
Совместное использование хранилища несколькими процессами
Если Вы передаете указатель на IStorage другому процессу, код маршалинга гарантирует, что процесс-приемник сможет обращаться к данному хранилищу и его элементам. Это очень удобно для совместного использования несколькими процессами какой-то части файла. Один из стандартных типов носителей для объектов данных — TEMEOJSTO/MG^ Вы можете передавать указатель на IStorage через буфер обмена или при помощи операции drag-and-drop.
Потоки и интерфейс IStream
Объектом-потоком можно управлять через интерфейс IStream. Потоки всегда расположены внутри хранилища — корневого или вторичного. Потоки — по мере записи в них — автоматически «расширяются» 512-байтовыми порциями. В MFC есть класс потоков COleStreamFile, который делает поток сходным с объектом CFile. В этой главе мы не будем очень уж интенсивно пользоваться классам COleStreamFile. Далее приведен список всех функций IStream.
HRESULT CopyTo(IStream“ pStm, ULARGEJNTEGER cb,
Копирует cb байтов изданного потока в указанный. ULARGEJNTEGER — структура с двумя 32- разрядными полями: HigbPart(старшая часть) и LowPart (младшая часть).
HRESULT Clone(lStream** ppStmY,
Создает новый объект-поток с собственным указателем текущей позиции, который ссылается на байты данного потока. Сами байты не копируются, поэтому изменения в одном потоке видны в другом.
HRESULT Commit(...);
Транзакции для потоков пока не реализованы.
IHRESULT Read(void* pv, ULONGcb, ULONG* pcbflead);
Пытается прочесть cb байтов из потока в буфер, на который указываетpv. ПеремскняярсЬЛеаа сообщает фактическое число считанных байтов.
DWORD Release();
Закрывает данный поток.
HRESULT Revert();
Для потоков пока не реализована.
HRESULT Seek(LARGEJNTEGERdlibMove, DWORD dwOrtgln, ULARGEJNTEGER*NewPoetttony.
Перемещает указатель потока в заданную позицию. Параметр dwOrigin задает начало отсчета смещения, заданного в dlibMove.
HRESULT SetSize(ULARGE_INTEGER flbNewS/ze);
Расширяет или урезает поток. Потоки сами расширяются по мере записи в них, но вызов SetSige может оптимизировать этот процесс.
HRESULT Stat(STATSTG*pStatstg, DWORD flag);'
Заполняет структуру STATSTG информацией о потоке, в том числе его именем и размером. Размер может пригодиться при выделении памяти для считывания данных из потока.
HRESULT Write{constvoid*pv, ULONGcb, ULONG*pcbWritteny,
Пытается записать в поток cb байтов из буфера, на который указываетpv. Перемснная/?сйТй*- 1еп сообщает фактическое число записанных байтов.
Программирование/Stream
Вот пример кода, создающего поток в заданном хранилище и записывающего в него данные из буфера m_bu^er.
extern IStorage* pStg;
IStream* pStream;
ULONG nBytesWritten;
if (pStg->CreateStream(L"MyStreamName", STGM_CREATE ! STGH_READWRITE i STGH_SHARE_EXCLUSIVE, 0, 0, ipStream) == S_OK) {
ASSERT(pStream != NULL):
pStream->Wrlte(m_buffer, m^Length, SnSytesWritteri);
pStream->Release();
Интерфейс ILockBytes
Как уже упоминалось, поддержка составных файлов реализована в OLE32 DLL Однако интерфейсы структурированного хранилища дос; аточно гибки — они допускают изменение своей реализации. Ключевую роль зд есь играет интерфейс ILockBytes. Глобальные функции 5tgCre«- teDo^ilefH. S^OpenStorageacstottbsytoT файловую систему Windows. Вы можете написать свой код для доступа к файлам, реализующий интерфейс ILockBytes, и вызывать StgCreateDocfile- OnlLockBytes или StgOpenStorageOnlLockBytesBMecro других глобальных функций.
Чтобы не разрабатывать свой интерфейс ILockBytes, с помощью функции CreateLockBytes- QnHGlobalco3S3$ne составной файл в оперативной памяти. А если Вы захотите расположить составной файл в базе данных, придется реализовать интерфейс ILockBytes, использующий массивные двоичные объекты (binary large objects, или blobs) базы данных.
Пример ЕХ27А
При выборе из меню Storage команды Write программа просматривает весь диск, отыскивая ТХТ-файлы. При этом она создает составной файл (\direct.stg) с вторичными хранилищами, соответствующими дисковым подкаталогам. Для каждого ТХТ-файла, найденного в подкаталоге, она копирует текст в поток, находящийся в соответствующем хранилище, а когда Бы выберете из меню Storage команду Read, программа загрузит составной файл и выведет его содержимое в окно Debug.
Если Бы создаете эту программу с самого начала, запустите AppWizard без параметров, связанных с ActiveX и Автоматизацией, а затем добавьте в StdAfx.h следующие строки: ^include <afxclc.h>
^include <afxpriv.h> // для преобразования двухбайтовых символов
и удалите строку
ffdefipe VC_EXTRALEAN
Для подготовки ЕХ27А к работе откройте и соберите проект \vcpp32\ex27a\ex27a.dsw. Запустите программу под управлением отладчика, выберите команду Write из меню Storage и дождитесь сообщения «Write complete» («Запись завершена»). Затем выберите команду Read из того же меню. Понаблюдайте за содержимым окна Debug.
Меню
В примере ЕХ27А добавлено меню верхнего уровня Storage с командами Write и Read. 1
Класс CEx27aView
Этот класс обрабатывает команды Read и Write меню Storage, создавая рабочие потоки кода. Обработчики команд выглядят так:
void CEx2?aView::CnStorageRead()
CWinThread* pThread = AfxBeQinThresd(ReadTiHeadProc, GetSafeHwrdO);
}''
void CEx27aView: .OnStorageWriteQ
{
CWinThread* pThread = AfxBeginThread(WriteTnr2acProc. GetSafcHwnd());
Рабочие потоки кода
Листинг рабочих потоков, выполняющих команды Storage/Write и Storage/Read, приведен на рисунке 27-2.
THREAD.H
extern int g_nlndent;
extern const char* g_szBlanks;
extern const char* g_szRootStorageName;
JINT WriteThreadProcdPVOIO pParam);
JINT ReadThreadProc(LPVOID pParam);
/old ReadDlrectorytconst cha-* szPath, LPSTORAGE pStg):
/aid ReadStorace(LPSTORAGE pStg);
WRITETHREAD.CPP
^include "StdAfx.h"•
4include "Thread.h"
int g_nlnoent = 0;•
const char*- g_szBlanks = "
const char- g_szflootStorageName = "\\direct. stg";
UINT WriteThreadProc(LPVOID pParam) {
USES.CONVERSION;
LPSTORAGE pStgRoot = NULL;
g_nlndent = 0;
VERIFY(::StgCreateDocfile(T2COLE(g_szRootStorageName).
STGM_READWRITE ! STGM_SHARE_EXCLUSIVE ! STGM_CREATE, 0, &pStgRoot) == S^OK);
ReadDirectory("\\", pStgRoot);
pStgRoot->Release();
AfxMessageBox("Запись завершена");
return 0;
void ReadDirectory(const char* szPath. LPSTORAGE pStg)
{
// рекурсивная функция'
USES_CONVERStON;
WIN32 F1ND_DATA fData;
HANDLE h;
char szNe*Path[NAX_PATH];
char sz8toragcName[100];
char szStreamName[100];
char szData[81];.
char* pch;
LPSTORAGE pSubStg = NULL;
LPSTREAM pStream = NULL;
g_nlndent++;'
strcpy(szNewPath, szPath);
strcat(szNewPath, "».♦");
Ih = ::FindFirstFile(szNewPath, SfOata);
if (h == (HANDLE) OxFFFFFFFF) return; // каталог не найден do {
if (! strcmp(fData.cFileName, "..") i! •strcmp(fData.cFileName, ".") ) continue;
if (fData.dwFiLcAttributes& FILE.ARRIBUTE.DIRECTORy) {
//это каталог, поэтому создаем хранилище
strcpy(szNewPath, szPath);
strcat(szNewPath,fData. cFi)eName);
strcat(szNewPath, ”\\");
strcpy(szStorageName, fData.cFileName);
szStorageName[31] = "\0 ;//ограничение, накладываемое OLE
TRACE("96Q.*sStorage = «s\n". (g_nlndent- 1) *4, g_szBlanks, szStorageName);
VERI^tpStg-xCreateStorage^cOLECszStorageName),
SIGN CREATE ! STGM READWRITE I STGH_SHARE EXCLUSIVE, 0, 0. ApSubStg) == S_OK);
ASSERT(pSubStg != NULL);'
ReadDirectory(szNewPath, pSubStg): pSubStg->Release();
if ((pch= strrchr(fData.cFileName, '. )) I = NULL) {
if (!stricmp(pch, ■".TXT")) {
// это текстовый файл, поэтому создаем поток байтов strcpy(szStreamNamc. fData.cFileName);
st ropy(szNewPath, szPath);
strcat(szNewPath, szStreamName);
Рис. 27-2. (продолжение)
ТНАСЕ( '%0.*sStream = %s\n", (g_nlidenz - 1) * 4, g_szBlanks, szHewPath): CStdioFile file(szNewPath, CFile:;modeRead);
И пропускаем файлы нулевой длины
if (flle.ReacStringCszData. 80)) {
TRACER"%s\n", szData)
VERIFY(pS*:g->CreateStrea7;(T2C0LE(szStreamName),STGM_CREATF ! STGM READWRITE i STGM_S IARE EXCLUSIVE, 0, 0, ApStream) == S_OK);
ASSERT(oS'.:ream != NULL);
// включаем в поток завершающий нуль pStream->Write(lpvFLle, nFileSize, NULL); pStream->Release():
)
)
}
}
} while (:: FindNex*File(h. AfData));
g_nlndent--:
READTHREAD.CPP
rt_nclude "StdAfx.h"
^include "Th read.h“
UINT ReadThreadProc(LPVOID pParam)
' USES CONVERSION;'
LPSTORAGE pStgRoot = NULL
// без STGN_SHARE_exCLUSIVE не работает
у nlndent = 0;•
if (::Stg0penStorage(T2C0LE(g_szRootStorage4ane). NULL, STGM.READ 1 STGM_SHARE_EXCLUSIVE, NULL, 0, ApStgRoot) ==S_0K) {
ASSERT(pStgRoot!= NULL);
ReadStorage(pStgRoot);
pStgRoot->Rel,ease();
}
else {
АТхМеззадеВох("Файл хранилища недоступен или поврежден."); '
1
АТхМеззадеВохС'Чтениезавершено") ;
return 0:
void ReadStorage(LPSTORAGE pStg)
//считывает одно хранилище - рекурсивные вызовы для вторичных хранилищ
USES_C0NVERSION;
LPSTORAGE pSubStg = NULL;
LPSTREAM pStream = NULL;
LPENUMSTATSTG pErum = NULL;
LPMALLOC pUalloc = NULL; // для освобождения statstg
STATSTG statstg;
ULONG nLength;
BYTE bufferf101];
g_nlndent++;'
: :CoGetMalloc(MEMCTX_TASK. ApMalloc); // предполагаем, что AfxOlelnit уже вызвана
VERlFY(pStg->EnumEleine:its(O, NULL, 0. SpEnum) ==S_0K):
while (pEnum->Next(1, Astatstg, NULL) == S OK) {
if (s*atstg. type == STGTY_STORAGE) { ~
VERIFY(pStg->OpenStorage(statstg.pwcsName, NULL, SIGN REAO i STGM SHARE_EXCLUSIVE,
NULL, 0, ApSubStg) == S_OK);"
ASSERKpSubStg != NULL);
TRACE(“%0.«sStorage = %s\n", (g_nlndent - 1) * 4, g_szBlanks, 0LE2CT(statstg.pwcsName));
ReadStorage(pSubStg):
pSubStg->Release();
}
else if (statstg.type == STGTY STREAM) {
VERIFY(pStg->OpenStream(statstg.pwcsName, NULL, STGM_READ I STGM_SHARE_EXCLUSIVE,
0, ApStream) == S OK);
A§SERT(pStrcam != NULL);'
TRACE("%O.*sStream = %s\n", (g_nlndent - 1) * 4, g.szBlanks, OLE2CT(statstg, pwcsName»; pStream->Read(buffer, 100, AnLength);
butfer[nLermfh) = '\0';
TRACE("%s\n", buffer);
pStream->Release();
d
ASSERT(FALSE); // LockBytes?
pMalloc->Free(slatstg.pwcsName); // избегаем утечек памяти
)
pMaIIoc->Release();
pEnum->Release();
g nlndent—;.
I
Для упрощения программы мы не предусмотрели координацию основного и рабочих потоков. Используя два разных составных файла, Вы могли бы запускать оба рабочих потока одновременно.
Зная потоковую модель Win32, Вы могли бы предположить, что закрытие основного окна вызовет мгновенное уничтожение потока чтения или потока записи, возможно с утечками памяти. Но этого не происходит потому, что MFC знает об использовании СОМ-объектов рабочими потоками. Даже если окно закроется, программа не завершится до завершения всех потоков.
Оба потока используют рекурсивные функции. Функция Readstorage считывает основное хранилище и вызывает сама себя для считывания вторичных хранилищ. Функция ReadDirectory считывает каталог и вызывает сама себя для считывания подкаталогов, а чтобы перечитать элементы каталога, она вызывает ^пЗЗ-функции/Уи^га^У/еи FindNextFile.Ikme dwFUe- Attributes структуры ^РУЛ^ ^УАФ ^У^одержит информацию о том, является ли текущий элемент файлом или подкаталогом. Функция ReadDirectory использует MFC-класс CStdioFile, так как он идеален для чтения текстовых файлов.
Макрос USESjCONVERSIOttiyxeH для поддержки макросов OLE2CT"$l T2COIE,преобразующих двухбайтовые символы. Эти макросы используются из-за того, что в данном примере не задействован класс CStriwg со встроенной логикой таких преобразований.
Структурированные хранилища и устойчивые СОМ-объекты ■
Для чтения и записи составного файла программа ЕХ27А вызывает функции-члены IStorage и istreamявным образом. В «объектно-ориентированном» мире объекты должны знать, как сохранить и загрузить себя из составного файла. Для этого и предназначены интерфейсы IPersistStorageH. IPersistStream. Если они реализованы в COM-классе, контейнерная программа может «подсоединить» объект к составному файлу, передав указатель на IStorage для этого файла как параметр в функции-члены Save и Load интерфейса IPersistStorage.TaKKe объекты
называют устойчивыми (persistent). На рисунке 27-3 показано, как вызывается функция IPer- sistStorag&*:Save и что при этом происходит.
lUnmown
Рис. 27-3. Вызов IPerslstStorage::Save
COM-компоненты работают чаще с интерфейсом IStorage, нежели с IStream. Если объект связан с каким-то хранилищем, COM-компонент, получив указатель на IStorage, может управлять вторичными элементами этого хранилища. Интерфейс IStream используется, только если компонент сохраняет все свои данные в одном потоке. Интерфейс IStream поддерживается в ActiveX-элементах для сохранения и загрузки значений их свойств.
1Интерфейс IPersistStorage
Оба интерфейса IPersistStorage и IPersistStream производны от IPersist, содержащего функцию- член GetClassID. Рассмотрим вкратце функции-члены IPersistStorage.
. IRESULT GetClasslD(CLSID‘ pClskTy,
Возвращает 128-разрядный идентификатор класса СОМ-объекта.
I IRESULTInitNew(IStorage* pStg);,
Инициализируем только что созданный объект. Это хранилище может понадобиться компоненту для хранения временных данных, поэтому контейнер должен передать указатель на i Storage, который будет действителен все время жизни объекта. Компонент должен вызвать AddRef, если он собирается задействовать это хранилище, но ему не следует использовать данный указатель на IStorage при записи и загрузке; он должен ожидать вызовы Save и Load, после чего — на основе переданного с ними указателя на IStorage ~ вызывать IStoragenWrite и IStorage; ;Read.
Н RESU LT teOIrtyO;
Возвращает S_OK,если объект изменился с момента его последнего сохранения; в ином случае возвращает S_FALSE.•
IHRESULT Load(l Storage* pStgiy
Загружает данные СОМ-объекта из указанного хранилища.
НRESULT Sa ve(IStorage* pStg, BOOL fSemeAeLoadj;
Сохраняет данные СОМ-объекта в указанном хранилище..
Интерфейс IPersistStream
Перечислим теперь функции-члены IPersistStream.
HRESULT GetClasslD(CLSID* pC/sfc/);
Возвращает 128-разрядиый идентификатор класса СОМ-объекта.
HRESULT GatMaxSlze(ULARGEJNTEGER* pobS/ze);
Возвращает число байтов, необходимых для сохранения объекта.
HRESULT IsDirtyO;
Возвращает S_OK,если объект изменился с момента его последнего сохранения; в ином случае возвращает S_FATSE.'
Н R ESU LT Load(IStream* pStm);•
Загружает данные СОМ-объекта из указанного потока.■
HRESULT Save(IStream‘pSfm, BOOL fdearDIrty);
Сохраняет данные СОМ-объекта в указанном потоке. Если/С&егДЬТу равен TRUE, функция сбрасывает флажок — признак изменения объекта.
Программирование IPersistStream
Следующий фрагмент кеда контейнерной программы создает поток и сохраняет в нем данные СОМ-объекта. Указатель на IPersistStream для СОМ-объекта и указатель на IStorage устанавливаются как бы в другом месте.
extern IStorage- pStg;
extern IPersistStream* pPersistStream;
IStrearn* pStream;
if (pStg->C'eateStream(L"MyStreamName"l STGM_CREATE I STGM_READWRITE i STGM_SHARE_EXCLUSIVE,
0, 0, ApStream) == S_OK) {
ASSERT(pStream != NULL);
pPersistStream->Save(pStrear, TRUE);
pStream->Reiease();
Если Вы программируете свой СОМ-ктасс для использования в программе-контейнере, то, чтобы добавить интерфейс IPersistStream, придется задействовать макросы интерфейса из MFC Очень жаль, что для этого не предусмотрен какой-нибудь «интерфейсный > мастер.
Пример ЕХ27В
В примере ЕХ27В, который будет использоваться программой ЕХ27С, DLL содержит компонент CText. Это простой COM-класс, реализующий IDispatch^ IPersistStream. Интерфейс IDispatch предоставляет доступ к единственному свойству Tbxt, a IPersistStream позволяет объекту записывать и считывать свойство Text из файла структурированного хранилища.
Для подготовки ЕХ27В откройте рабочее пространство \vcpp32\ex27b\ex27b.dsw и соберите проект. Используя regsvr32 или REGCOMP, зарегистрируйте DLL.
На рисунке 27-4 приведены файлы TexLh и Text.cpp с кодом класса CText
ТЕХТ.Н
tifndef _ТЕХТ_Н__■..
tfdefine _ТЕХТ_Н__
// Получатель команд CText■
class CText : public CCmdTarget
Рис. 27-4. Листинг кода класса CTextСМ. след. стр.
Рис. 27-4. (продолжение)
private:
char* m_pchText;
DECLAREj)YNCREATE(CText)
CText{); // защищенный конструктор для динамического создания
// Атрибуты public:
// Операции public:-
// Переопределенные функции
// Переопределенные функции, сгенерированные ClassWizard //{(AFX_VIRTUAL(CText)' .
public:
virtual void OnFinalReleaseO;
//}}AFX„VIRTUAL
// Реализация
protected:.
virtual,~CText();
// Сгенерированные функции таблицы сообщений
//({AFX.MSG(CText)
// ПРИМЕЧАНИЕ ClassWizard будет вставлять и удалять здесь функции-члены.
//}}AFX_MSG
DECLARE MESSAGE МАР()
DECLARE_OLECREATE(CText)
// Сгенерированные функции таблицы диспетчеризации OLE
//{{AFX.DISPATCH(CText)
afx_msg VARIANT GetTextO;
afxjnsg void SetText(const VARIANT FAR& newValue);
//} iafxdispatch
DECLARE_DISPATCH MAPQ
DECLARE_INTERFACE_MAP()
В EG I NJ NTER FACE_PART (PersistStream, IPersistStream) STDMETHOD(GetClassID)(LPCLSID);
$TDMETHOD(IsOirty)();
STDMETHOD(Load)(LPSTREAM);
STDMETHOD(Save) (LPSTREAM, BOOL);
STDMETHOD(GetSizeMax)(ULARQE.INTEGER FAR*);
иШ111НШ11ШНШПШШ1Ш1ШН11ШШ111Ш1ШШ1Н1Ш1иШ111НШ11ШНШПШШ1Ш1ШН11ШШ111Ш1ШШ1Н1Ш1END INTERFACE_PART(PersistStream)
иШ111НШ11ШНШПШШ1Ш1ШН11ШШ111Ш1ШШ1Н1Ш1
иШ111НШ11ШНШПШШ1Ш1ШН11ШШ111Ш1ШШ1Н1Ш1
//<{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ будет вставлять дополнительные объявления
Н непосредственно перед предыдущей строкой.
tfendif//__ТЕХТ_Н„
ТЕХТ.СРР
«include "stdafx.h"■
«include "ex27b.h"
«include "Text.li”
ffifdef „DEBUG
«define new DEBUG NEW
flundefTHIS_FILE
static char THIS_FILE[] =_FILE_;
flendif
////////////////////////////////////////////////////////////////////// // CText
IMPLEMENT_DYNCREATE(CText, CCmdTarget)
CText::CText ()
{
EnableAutomat ion();
// Чтобы приложение выполнялось до тех пор, пока объект
// Автоматизации активен, конструктор вызывает AfxOlelockApp.
Afx01eLockApp();
m_pchText = NULL;
CText::-CText()
{
// Чтобы завершить приложение при удалении всех объектов, созданных
// Автоматизацией, деструктор вызывает AfxOleUnlockApp.
lf(m_pchText != NULL) {
delete [] m pchText;
}~
AfxOleUnlockAppO;
void CText: :OnFinalReleasc()
{
// При освобождении последней ссылки на объект автоматизации вызывается OnFinalRelease.
// Базовый класс автоматически удалит объект. Добавьте необходимую дополнительную очистку // перед вызовом базового класса.
CCmdTarget: :OnFinalRelease();
BEGIN HESSAGE_MAP(CText, CCmdTarget)
//{(AFX_MSG_MAP(CText)
// ПРИМЕЧАНИЕ - ClassWizard будет добавлять и удалять здесь макросы таблица сообщений.
//}}AFX MSG MAP
END MESSAGE MAP()
BEGIN DISPATCH MAP(CText, CCmdTarget)
//{{AFX DISPATCH MAP(CText)
DISP.PROPERTY EX(CText, "Text", GetText, SetText, VTJ/ARIANT)
//}}AFX DISPATCH_MAP
END_DISPATCH_MAP()
// Примечание: мы добавляем поддержку IID_IText для безопасной привязки из VBA.
Рис. 27-4, (продолжение)
Ц Этот IID должен соответствовать GUID который присвоен disp-интерфейсу в 001-файле.
Ц {4EBFDD71-5F7D-11D0-848F-00400526305B)
static const IID IID„IText -
{ Ox4ebfdd71, 0x5f7d? 0x11dO, { 0x84, Ox8f, 0x0, 0x40, 0x5, 0x26, 0x30, 0x5b } };
BEGIN_INTERFACE_MAP(CText, CCmdTarget)
INTERFACE_PART(CText, IID_IPersistStream, PersistStream)
INTERFACE PART(CText. IID fiext, Dispatch)
EHD_INrFRFACE>AP()"
// {4EBFDD/2-5F7D-11DO-848F-0040O526305B}
IMPLEMENT _OLECREATE(CText, ' Ex27b,Text", Ox4ebfdd72, Ox5f7d, 0x11dO, 0x84, Ox8f, 0x0, 0x40. 0x5, 0x26. 0x30, 0x5b)
НШ11НШПНПШПШ11Ш11Н111Ш1Ш1ШПП1ШШ1Н111Ш1
// Обработчики сообщений CText
VARIANT CText:: GetText()•
{
returnCOlcVarlant (m_pchText).Detach ();
void CText: :Set Text(const VARIANT FAR& newValue) {
CString strTemo;
ASSERT(newValue. vt == VT_BSTR);
lf(m_pchText != NULL) {
delete [] m pchText;
}" '
strTemp = newValue. bstrVal; // преобразуем в узкие символы
m_pchText = new char[strTemp.GetLength() + 1];
s~rcpy(m pchText, st'Terrp);
}
111Ш11И1Ш1111111Ш1Ш1Ш11Ш11111Г1Ш1Ш1ШШПН1111П11
STDMETHODIMP (ULONG) CText: ;XPersistStream: :AddRef() {
METHOD_PROLOGUE(CText, Pe rsistStream)
return (ULONG) pThis->ExternalAddRef ();
STDMETHODIMP_(ULCNG) CText: :XPe'SistStream: :Release() {
METHOD-PROLOGU E (CText, PersistStream)
return (ULONG) pThis->ExternalRelease.():
STMETHODIMP CText: :XPersistStream::QueryInlerrace(REFIIDiid, void FAR* FAR* ppvObj )
{'
METHOD_PROLOGUE (CText, PersistStream)
// ExternalOuerylnterface ищет IID в таблицах, созданных макросами return (HAESULT) pThls->ExternalQueryIhterrace(&;.ld, ppvObj);
}•
//////////////////////////////////////////////
STDMETHODIMP CText: :XPersistStream: :GetClassID(LPCLSIDlpClasslD)
TRACE("Входим в CText::XPersistStream::GetCiassID\n"); METHOD PR0L0GUE(CText, PersistStream)
ASSERT_VALID(pThis);
•IpClassID = CText::guid;
return NOERROR;
STDMETHODIMP CText::XPersistStream::IsDirty()
* TRACE("Входим в CText::XPersistStream::IsDirty\n");
METHOD PROLOGUE(CText. PersistStream)
ASSERT_VALID(pThis);
return NOERROR;
stdmethodimp CText: :XPersistStream: :Load(LPSTREAMpStm)
{■
ULONG nLength;
STATSTG statstg ;
METHOD PROLOGUE(CText, PersistStream)
ASSERT. VALID(pThis);
if(pThis->m_pchText i= NULL) {
delete n pThis->m pchText:
II не нужно освобождать statstg. pwcsName из-за флажка NONANE
VERIFY(pStm->Stat(&statstg, STATFLAG.NONAME) == noerror) ;
int nSize = statstg. cbSize. LowPart; // предполагаем < 4 гв
if(nSize > 0) {
pThis->m_pchText = new char[nSize];
pStm->Read(pThis->m_pchText, nSize, AnLength);
return NOERROR; }
STDMETHODIMP CText:-.XPerslstStream: :Save(LPSTREAM pStm, BOOL fClearDirty) {
METHOD PROLOGUE(CText, PersistStream)
ASSERT j/ALID(pThis);
int nSize = Strlen(pThis->m_pchText) + 1;
pStm->Write(pThis->m_pchText, nsize, null);
return NOERROR;
stdmethodimp CText: :XPersistStream::GetSizeMax(ULARGE_INTEGER far* pcbSlze)
ТНАСЕ("Входим 8 CText::XPersistStream;:GetSizeMax\n");
IMETHOD_PROLOGUE(CText. PersistStream)
ASSERT.VALID(pThis);
pcbSize->LowPart = strlen(pThis->m_pchText) + 1;
pcbSize->HighPart = 0; // предполагаем < 4 ГБ
return NOERROR;
ClassWizard генерирует класс CText как обычный компонент Автоматизации. Интерфейс IPersistStream добавлен вручную. Внимательно изучите функции XPersistStream::Loadn. XPersist- Stream::Save. Функция Load вьеделжт память в куче и вызывает IS^eam::Readxn^ загрузки содержимого из потока. Функция 52гие копирует данные объекта в поток, используя вызов IStream::Write.
Пример ЕХ27С
По своим функциональным возможностям эта программа сходна с ЕХ27А. Однако оба рабочих потока приложения используют для ТХТ-файлов устойчивый COM-класс CText (ЕХ27В).
Для подготовки ЕХ27С откройте рабочее пространство \vcpp32\ex27c\ex27cdsw и соберите проект. Запустите программу из-под отладчика, выберит е сначала Write из меню Storage, зятем Read из этого же меню, Изучите вывод в окно Debug.
Меню, классы «вид» и «приложение* такие же, как и и ЕХ27А. Отличается только код потоков.
На рис. 27-5 приведен код файлов WriteThread.cpp и ReadThread.cpp.
WRITETHREAD.CPP
itinclude "StdAfx.h"
Sinclude ""bread.h"-
«include "Ltcxt.h"
CLSID g clsid; // для сервера Text int gjrfnder- = 0: const char» g_szBlanks = " const cnar» g_szRootStorageName = "\\direct.stg':
UIN~ WrlteThreadProc(LPVOID pParam) {
USES.CONVERSICN;
LPSTORAGE pStgRoot = NULL;
g_rIndent = 0;
;:CoTnitialize(NULL);
: :LL$10'rcTiProgID(L"EX2'’B.TEXT", &g_clsid);
VERIFY!: rSbgCreateDocfiLe(T2C0LE(g szRootStorageName), STGM_READWRITE I STGM_SHARE_EXCLUSIVE STGM_CREATE. 0, ApStgRoot) == S_0K);
FeadDirectoryC W", pStgRoot);
pStgRcot->Release();
AfxNessage8ox("Запись завершена");
return 0;
}
void Read3:reclory(const char* szPath, LPSTORAGE pStg) ■{
// рекурсивная функция
USES CONVERSION;
WIN32_FIND_DATA fData;
HANDLE hr’
cnar szNewPatn[MAX_PA7H];
cnar szStorageName[100]
crar szStreamName[100J;
cnar szData[8l];
cnar* pch = NULL;
LPSTORAGE pSubStg = NULL;
LPSTREAM pStrcam = NULL
LPPERSISTSTREAM pPersistStream = NULL;
c_rlndent-n-;
strcoy(szNewPath. szPath);
si rcat;szN=wPath. "*.*?);_
h= : :FindFi rsTFlle(szNewPath, ftfData);■
if (h == (HANDLE) OxFFFFFFFF) return; // каталог не найден
Рис. 27-5. Листинг кода двух рабочих потоков в ЕХ27С
do {
if (!strcmp(fData. cFileName, " .. ") || tstrcmp(fData.cFileName, "V") ) continue; while((och = strchr(fData.cFileName, "!)}! = NULL) {
*pch= ‘ | ‘;
)
if (fData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {-
// это каталог, поэтому создаем хранилище
strcpy(szNewPath, szPath);
strcat(szNewPath,fData.cFileName);
strcat (szNewPath" \\” ) ;
strcpy(szStorageName, fData.cFileName):
szStorageName[31] = ’\0'; //ограничение, накладываемое OLE
TRACE("%0.*sStorage = %s\n", (g_nlndent - 1) M, g szBlanks. szStorageNamc);
VERIFYCpStg^CreateSforageCTSCOLEtszStorageName),
' STGM_CREATE I STGM_REA№RITE | STGM_SHARE_EXCLUSIVE, 0, 0, ApSuhStg) == ._OK) " ASSERT(pSubStg 1 = MULL);
ReadDirectory(szNewPath, pSubStg);
pSubStg->Release();
Ielse {if ((pch = strrchr(fData.cFileName, ’.)) != NULL) {if (!stricmp(pch, ".TXT"')) {// это текстовый файл, поэтому создаем потокstrcpyCszStreamName, fData.cFileName);strcpy(szNewPath, szPath) ;strcat(szNewPath, szStreamNarne);szStreamName[32] = '\0'; //максимальная длина в OLE .TRACE("%0.‘sStream = %s\n". (g_nlndent - 1) -4, g_sz81arks. szNewPath):CStdioFile file(szNewPath, CFile: :modeRcad),// игнорируем файлы нулевой длиныif(file.ReadString(szData. 80)) {""
TRACE( "Ks\n", szData);
VERIFY(pStg->CreateStream(T2C0LE(szStreamName). STGM_CREATE | STGM READWHIIF | STGM_SHARE EXCLUSIVE, 0, 0. SpStream) S OK)
ASSER1(pStream != NULL);
// вставляем в поток завершающий нуль IToxt text;
VERIFY(text.CreateDispatch(g.clsid));
text.m_lpDlspatch->QueryTnterface (TTD_TPersistStream. (void**) &pPersistStream);
ASSERT(pPersistStream *= NULL): text.8etText(C01eVariant(szData)); pPersistStream->Save(pStream, TRUE); p?crslstStream->Release(); oStream->Release();
} while (: :FindNextFile(h, fifData)); g_nlndent--;
READTHREAD.CPP
ttinclude "StdAfx.h" ftinclude "Thread, h" ttinclude "itext.h"
Рис. 27-5. (продолжение)
#ifdef „DEBUG.
#defirie new DEBUG_NEW
#uncef THIS_FILE
static char THIS_FILE[] = ,„FILE„;
flendif
UINT ReadThreauProc(LPVOID pParam) {
g_nlndent = 0;
: :CoInitialize(NULL);
::CLSIDFromProgID(L"EX279.TEXT", &g_clsid);
LPSTORAGE pStgRoot = NULL;
if(::StgOpenStorage(L"\\DIRECT.STG", NULL, STGM„READ|STGM_SHARE_EXCLUSIVE.
NULL. 0, ApStgRoot) == S_0K) {
ASSERT(pStgAoot!= NULL);
ReadStoragetpStgRoot);
pStgRoot->Release();
}
else {
AfxMessageBox( "Файл хранилища недоступен или поврежден.");
}
AfxMessageBox("Чтение завершено’');
return 0;
void ReadStorage(LPSTORAGE pStg)'
// считываем одно хранилище - рекурсивные вызовы для вторичных i
* LPSTORAGE pSlbStg = NULL;
LPSTREAM pStream = NULL;
LPENUMSTATSTG pEnum = NULL;
STATSTG statstg;
LPPERSISTSTREAM pPersistStream = NULL;
' g_nlrdent++;
if(pStg->EnumElements(O, NULL, 0, ApEnum) ! = NOERROR) {
ASSERT(FALSE); return;
}
while(pEnum->Next(1, Astatstg, NULL) == NOERROR) {
if(statstg.type == STGTY STORAGE) {
VERIFY(pStg->OperiStorage(statstg. pwcsName, NULL, STGM_READ|STGM_SHARE_EXCLUSIVE, NULL, 0, ApSubStg) == S_OK);
ASSERT(pSdbStg != NULL);
ReadStorage(pSubStg);
pSubStg->Release();•
}'
else if(statstg.type — STGTY_STREAM) {
VERlFY(pStg->OperStream(statstg.pwcsName, NULL, STGM_READ|STGM_SHARE_EXCLUSIVE, 0, ApStream) == S OK);
ASSERT(pStream !^ NULL);
IText text;
VERIFY(text.CreateDispatch(g_clsid));
text. m_lpDispatct->QueryInterface(nD_IPe'*slstStream, (void**) ApPersistStream);
ASSERT(pPersistStream !=NULL);
pPe rsistSt ream->Load(pStream);
pPersistStream->Release();
COleVariant va = text.GetTextO;
ASSFRT(va.vt == VT_BSTR);.
CString str = va.bstrVal;
TRACE("%s\n", str);
pStream->Release();
else
ASSERT(FALSE); // LockBytes?
}
::CoTaskMemFree(statstg.pwcsName);
pEnjr;->Release(): g nlndent--;
Обратимся ко второй половине функции ReadDirectorywa файла WriteThread.cpp. Для каждого текстового файла программа создает объект Ctext, создавая управляющий объектIText и вызова CreateDispatch,после чего вызывает функцию-член SetText, чтобы загрузить из ТХТ- файла первую строку. Далее она вызываетIPersistStream::Sav№» записи объекта в составной файл и удаляет объект Ctext, освобождая указатель на IDispatch после освобождения указателя на интерфейс IPersistStream и удаления объекта IText.
Теперь взгляните на вторую половину функции Readstorage в файле ReadTliread.cpp. Подобно ReadDirectory, она создает управляющий объект IText и вызывает CreateDispatch. Затем она вызывает Query Interface,чтобы получить указатель на IPersistStream,который использует для вызова Load. Наконец, программа вызывает GetText для получения строки текста.
Как Вы уже знаете, COM-компоненты обычно реализуютIPersistStorage,а не IPersistStream. Класс CText можно было бы реализовать таким же образом, но составной файл был бы более сложным, так как на каждый текстовый файл необходимо было бы иметь хранилище (для поддержки интерфейса) и поток (для хранения текста)..
Теперь приготовьтесь взять гигантский барьер. Допустим, у нас есть настоящий, создаваемый по CLSID COM-класс, который поддерживает интерфейс IPersistStorage Вспомните функции IStorage для идентификаторов классов. Если хранилище содержит идентификатор класса и все данные, необходимые для объекта, то OLE может загрузить соответствующий сервер, создать объект с помощью фабрики классов, получить указатель IPersistStorage и вызвать Load для загрузки данных из составного файла. Считайте сказанное предварительной демонстрацией составных документов, которые мы рассмотрим в главе 28.•
Фрагментация составных файлов
У структурированного хранилища есть недостаток. Как и диск, часто используемый составной файл подвержен фрагментации. Однако, если диск фрагментирован, объем его свободного пространства в общем-то не меняется. В случае же составного файла место, оставшееся после удаления элементов, не всегда удается использовать вновь. В результате составной файл может расти, даже если Бы удаляете из него данные.
К счастью, есть способ восстановить неиспользуемое пространство в составном файле. Вы просто создаете новый файл и копируете в него содержимое старого. Функция IStorage: :СоруТо может выполнить всю эту работу за один вызов, если Вы скопируете с ее помощью корневое хранилище. Можно или написать отдельную утилиту', которая будет заниматься этой работой, или встроить возможность регенерации составных файлов прямо в приложение.
Другие преимущества составных файлов
Вы уже видели, что составные файлы в каком-то смысле дополняют программу средствами произвольного доступа к данным, и можете оценить значение поддержки транзакций. Теперь представьте себе то светлое будущее, когда каждая программа сможет читать документы любой другой программы. Мы еще не пришли к этому, но движемся в нужном направлении. В
корневом хранилище составных файлов, создаваемых приложениями Microsoft, есть поток с именем \005SummaryInformation. Формат этого потока соответствует формату набора свойств (property set), определенному для элементов управления ActiveX. Если ваша программа распознает этот формат, то она откроет любой файл, отвечающий данному соглашению, и прочтет его сводку. ■
Вместе с Visual C++ поставляется утилита DocFilc Viewer (Dfview.exe) для просмотра составных файлов, которая отображает древовидную структуру хранилищ и потоков файла. Вог как выглядит окно Dfview для файла структурированного хранилища, созданного программой ЕХ27А.
Duehle Viewtn^
\ - CIndirect stg
| SOOS
I E) COUNTRY.TXT
I Edrvspace.txt
i BS64D
! SUTIL
j ВИешр
I SAOL20
I SCCL
I SlDB
| SMPM
| _ STOOL
ШШМ&Мш№мАы&ШММ1ММ^^
По сути, Dfview подходит для просмотра структуры любого составного файла. Ну что, теперь чувствуете потенциал «универсального формата файлов»?
ГЛАВА 28
Внедряемые компоненты и контейнеры OLE
О этой главе Вы изучите изнутри связывание и внедрение объектов (Object Linking and Embedding, OLE) и узнаете, как внедряемый компонент общается со своим контейнером. Эти сведения — основа, необходимая для изучения таких тем, как элементы управления ActiveX, активизация по месту (или Visual Editing) и связывание, которые изложены в книгах Эдама Деннинга (Adam Denning) ActiveXControls Inside Out (Microsoft Press, 1997), Крейга Брокшмидта (Kraig Brockschmidt) Inside OLE (Microsoft Press, 1995, Second Edition) и др.
Мы начнем с мини-сервера (mini-server) на базе MFC— процесса-компонента, поддерживающего активизацию по месту (in-place activation), но не допускающего независимого запуска. Работа с ним даст представление о том, как выглядит OLE для пользователя — на случай, если Вы еще не знаете этого. Кроме того, Вы познакомитесь с развитой поддержкой этого типа приложений со стороны MFC. Однако, работая только на верхнем уровне MFC, невозможно ни понять, ни оценить скрытые за ним механизмы OLE Так что придется «копать» глубже. Книга Шеферда и Уннгау MFC Internals (Addison-Wesley, 1996) предоставляет всеобъемлющее описание внутренней реализации OLE-документов в MFC.
Мы создадим также программу-контейнер, не только использующую уже знакомые компоненты библиотеки MFC, но и поддерживающую внедряемые OLE-объекты, которые можно редактировать в своих окнах. Конечно, этот контейнер смажет задействовать уже упомянутый мини-сервер, но по-настоящему Вы начнете изучать OLE, когда создадите мини-сервер с нуля и понаблюдаете за его взаимодействием с контейнером.
Внедрение и Visual Editing
Visual Editing (визуальное редактирование) — это торговая марка Microsoft, синоним активизации по месту. Компонент, поддерживающий активизацию по месту, поддерживает и внедрение. В обоих случаях данные объекта хранятся в документе контейнера, и он может активизировать этот объект. Контейнер, активизируемый по месту, может выполняться в основном окне приложения-контейнера, замещая его меню и панель инструментов, но при необходимости может выполняться и в своем окне верхнего уровня. Внедряемый компонент способен работать только в собственном окне с особым меню, не содержащим команд, связанных с файловыми операциями. На рисунке 28-1 показана таблица Microsoft Excel, активизированная по месту — в документе Microsoft Word. Обратите внимание на меню и панели управления Excel.
Одни приложения-контейнеры поддерживают только внедряемые компоненты, другие — как внедряемые компоненты, так и компоненты визуального редактирования. Обычно контейнер, поддерживающий активизацию по месту, позволяет активизировать компонент визуального редактирования как по месту, так и в отдельном окне. Вы, должно быть, уже дога-
дались, что внедрение — подмножество активизации по месту. Это справедливо не только с точки зрения пользователя, но и на уровне реализации OLE. Внедрение опирается на два ключевых интерфейса: lOleObjectm lOleClientSite, которые используются и при активизации по месту. '
ЖЖMEMORANDUM^MEMORANDUM^И ш an yweterdey’f fipm fox your dipertiexpLIИ ш an yweterdey’f fipm fox your dipertiexpLI8 *3*8 *3*ело 1ОЛБ..1ело 1ОЛБ..1"350 595 2346 Ж' 1035 1750"350 595 2346 Ж' 1035 1750IO3!IO3!1TO Book Dept 1 FROH UanagemcctJRE: Sales Figuresi1TO Book Dept 1 FROH UanagemcctJRE: Sales FiguresiШТГЙ-ЖИЕ^1 x ИШТГЙ-ЖИЕ^1 x И'251' 17.41 Um si ’ fasU*? 0122уЖЦ«'251' 17.41 Um si ’ fasU*? 0122уЖЦ«7* Г7Ж. ' лЙЯнй»7* Г7Ж. ' лЙЯнй»Рис. 28-1. Таблица Excel, активизированная внутри документа WordРис. 28-1. Таблица Excel, активизированная внутри документа WordAll Too Human”Dan Quxott1мйй(HfirtcriesK*af>ged .........Madam* dewy ' / ' MabyDkfc”.'.. FPiraditt LostjrW^gmMadwntl.KS j. spring Snow .1". *"1 The Brothers Karamaawi th» Jw otCcoicing ne oihie?"-‘“i"All Too Human”Dan Quxott1мйй(HfirtcriesK*af>ged .........Madam* dewy ' / ' MabyDkfc”.'.. FPiraditt LostjrW^gmMadwntl.KS j. spring Snow .1". *"1 The Brothers Karamaawi th» Jw otCcoicing ne oihie?"-‘“i"lusntiy iPrke (t) Stfa(%] Л4 Coal fl^uppl \ 9ОД 335 7XCnhirine ‘1^00_ 0;’ЗЖМ|\"
Ж
Ж
MEMORANDUM^
MEMORANDUM^
И ш an yweterdey’f fipm fox your dipertiexpLI
И ш an yweterdey’f fipm fox your dipertiexpLI
8 *3*
8 *3*
ело 1ОЛБ..1
ело 1ОЛБ..1
"350 595 2346 Ж' 1035 1750
"350 595 2346 Ж' 1035 1750
IO
3!
IO
3!
1
TO Book Dept 1 FROH UanagemcctJ
RE: Sales Figuresi
1
TO Book Dept 1 FROH UanagemcctJ
RE: Sales Figuresi
ШТГ
Й-ЖИЕ^1 x И
ШТГ
Й-ЖИЕ^1 x И
'251' 17.41 Um si ’ fasU*
? 0122уЖЦ«
'251' 17.41 Um si ’ fasU*
? 0122уЖЦ«
7* Г7Ж. ' лЙЯнй»
7* Г7Ж. ' лЙЯнй»
Рис. 28-1. Таблица Excel, активизированная внутри документа Word
Рис. 28-1. Таблица Excel, активизированная внутри документа Word
All Too Human”
Dan Quxott1
мйй(
Hfirtcries
K*af>ged .........
Madam* dewy ' / ' MabyDkfc”.'.. F
Piraditt Lostj
rW^gmMadwntl.KS j. spring Snow .1". *"1 The Brothers Karamaawi th» Jw otCcoicing ne oihie?"-‘“i"
All Too Human”
Dan Quxott1
мйй(
Hfirtcries
K*af>ged .........
Madam* dewy ' / ' MabyDkfc”.'.. F
Piraditt Lostj
rW^gmMadwntl.KS j. spring Snow .1". *"1 The Brothers Karamaawi th» Jw otCcoicing ne oihie?"-‘“i"
al..i.
jLl&M .
Полные и мини-серверы: связывание
Мини-сервер нельзя запускать как отдельную программу; его запуск осуществляется только приложением-контейнером. Он не поддерживает свой файловый ввод/вывод, а использует файлы контейнера. Но полный сервер запускается и из контейнера, и как отдельная программа. В последнем случае он способен считывать и записывать собственные файлы и, следовательно, поддерживает OLE-связывание. При внедрении (embedding) в документе-контейнере содержатся все данные, нужные серверу, а при связывании (linking) контейнер хранит лишь имя файла, который должен открыть сервер.
Обратная сторона Visual Editing
Мы действительно энтузиасты COM-архитектуры и искренне верим в то, что ActiveX-элементы завоюют мир. Однако у нас нет той же уверенности насчет Visual Editing, и в этом мы не одиноки. Из нашего общего опыта встреч с разработчиками со всего мира мы выяснили, что лишь немногие из них пишут приложения, соответствующие модели «объекты, внедряемые в документ». А как программисты мы знаем, что скоординировать размеры и масштаб внедренного объекта между контейнером и компонентом достаточно сложно. И, наконец, просто как пользователи, мы можем сказать, что активизация по месту бывает весьма медленной и неуклюжей, хотя ситуация улучшается с ростом производительности компьютеров. .
Не верите — попробуйте внедрить таблицу Excel в документ Word (см. рис. 28-1). Измените размеры таблицы в двух режимах: с предварительной активизацией и без нее. Обратите внимание на то, что результаты получаются разные, а обработка идет очень медленно.
Возьмем графику. Прежние версии Microsoft PowerPoint использовали для этого компонент с активизацией по месту — Microsoft Draw. Идея была в том, чтобы и другие приложения использовали этот компонент для всех своих нужд при работе с графикой, Однако из
этого ничего не выпит, и теперь код, работающий с графикой, встроен в PowerPoint Так что, если у Вас сохранились файлы, созданные в старых версиях PowerPoint и содержащие внедренные объекты Microsoft Draw, преобразование их будет весьма проблематично.
А печать? Вы получаете по сети из Сингапура документ Word с метафайлами внедренных объектов, для которых у Вас нет компонентов. Вы распечатываете документ на своем любимом цветном лазерном принтере с разрешением 1200 dpi, и метафайлы распечатываются вместе с документом. Метафайлы с внедренными объектами можно подготовить под определенный принтер, но крайне сомнительно, что кто-то в Сингапуре при создании документа использовал драйвер именно вашего принтера. Поэтому результат окажется не слишком утешительным — как минимум с некорректной разбивкой по строкам.
И все же мы считаем, что у технологии внедрения OLE-объектов большой потенциал. Ведь документы, воспроизводящие звук и видео, весьма привлекательны, а хранение объектов в базе данных — вещь очень интересная. Так или иначе, но то, чему Вы научитесь в этой главе, поможет Вам подумать о новых областях применения этого раздела OLE.
Метафайлы Windows и внедренные объекты
Чтобы понять, как внедряемые и активизируемые по месту компоненты рисуют в своих окнах, вернемся к теории Windows. До сих пор мы не работали с метафайлами, но они всегда были неотъемлемой частью Windows. Метафайл можно представить как кассету с лентой для записи команд GDI. Чтобы использовать кассету, необходимо устройство записи и воспроизведения, которым является контекст устройства метафайла, Если при его создании Вы задали имя файла, метафайл будет сохранен на диске; в ином случае метафайл сохраняется в оперативной памяти, а Вам возвращается описатель.
В мире OLE-внедрения метафайлы создаются компонентами и воспроизводятся контейнерами. Вот код компонента, создающий метафайл с текстом и прямоугольником:
CMetaFileDC dem; // MFC-класс для контекста устройства метафайла
VERIFY(dcm.CreateO);
dcm.SetMapMode(MM_ANlSOTROPIC);
dem.SetWindowOrg(0,0);
dem.SetWindowExt(5000, -5000);
// код для рисования
dcm.Rectangle(CRect(600, -1000, 1500, -2000));
dcm.TextOut(0. 0, m strlext);
«METAFILE hMF = dcm.CloseO;•
AS$ERT(hHF != NULL);
Можно создать метафайлы с фиксированным режимом преобразования координат, например, MM_LOENGFJSH)to в OLE мы всегда будем применять режим ММ_А^ОТЕОР1СМетафай.л содержит вызов SetWindowEx^a установки размеров окна по х и у, а программа, воспроизводящая его, вызывает SetViewportExtoxa установки размеров поля вывода (viewport). Вот код, который можно было бы включить в функцию OnDratv класса «вид» в контейнере:
pOC->SetMapMode(MM_HIMETRIC);
pDC->SetViewportExt(5000, 5000);
pDC->PlayMetaFile(hMF);
Здесь предполагается изобразить прямоугольник размером 1x1 см, поскольку компонент использует режим преобразования координат MMJWMETRICPasMep действительно получится таким, потому что размер поля вывода совпадает с размером окна. Если контейнер установит размер поля вывода 5000, 10000, прямоугольник вытянется по вертикали, но размер текста не изменится, так как он выводится немасштабируемым системным шрифтом. Установив режим преобразования координат, отличный ОТ ММ_Н1МЕТ1УС^ох$1ейнер может скорректировать размеры поля вывода, чтобы сохранить размер прямоугольника 1x1 см.
Итак, компонент устанавливает размер окна в соответствии с предполагаемым размером) отображаемой области и рисует в пределах ее границ. Если компонент использует отрицательные координаты по оси у, код рисования работает так же, как и в режиме MM_HIMt'l KlC Контейнер каким-то образом получает размеры компонента и пытается отрисовать метафайл внутри области с этими HIMETRIC-размерами.
Зачем мы вообще связались с метафайлами? А затем, что контейнеру надо рисовать ЧТО- то в прямоугольнике, занятом компонентом, даже если тот в данный момент и не выполняется. Компонент создает метафайл и «вручает* его объекту данных во внутрипроцессном модуле OLE-обработчнка (handler), присутствующем на контейнерной стороне RPC-соединения. Обработчик кэшируег метафайл и воспроизводит его по требованию, а также отвечает за пересылку метафайла в хранилище контейнера и из него. Однако, когда компонент активизируется по месту, он рисует непосредственно в окне, управляемом контейнером.
Архитектура OLE-компонента на базе MFC
Мы не станем здесь вдаваться в подробности, и рассмотрим лишь то, что нужно для понимания новых файлов в следующем примере. Вам нужно изучить три новых базовых класса MFC COlelPFrameWndyCOleServerDoc и COieServerltem.
Арр Wizard, создавая OLE-компонент, генерирует производный класс от каждого из упомянутых выше классов (помимо классов «приложение», «основное окно-рамка» и «вид»). Класс: COlelPFrameWnd похож на CFrameWnd. Это основное окно вашего приложения, содержащее окно представления. С ним связано меню IDR_SRVR_INPLACE которое будет объединено г меню программы-контейнера. При выполнении по месту ваш компонент использует COlelP- FrameWnd,a при запуске как отдельное приложение или как внедряемый компонент — обычное окно-рамку, которое является объектом класса, производного от CFrameWnd. Меню внедряемого компонента — это IDR_SRVR_EMBEDDED^ меню компонента, запущенного как отдельное приложение, — IDR_MAINFRAME.
Класс СО/еА'еп^егОосприменяется вместо CDocument. Он содержит дополнительные средства для поддержки OLE-связей с контейнером. Вместе с этим классом работает класс COieServerltem. Если бы компоненты никогда не поддерживали бы OLE-связывание, то функциональность этих классов можно было бы свести в один класс. Но поскольку компоненты, способные работать как отдельное приложение, поддерживают и связывание, архитектура MFC требует, чтобы оба класса присутствовали во всех серверах. Впрочем, в примере ЕХ28С Вы увидите, что простой мини-сервер можно создать и без такого разделения.
Вместе классы COieServerltem и СО1е5еп-егОосреэлизуют полный набор OLE-интерфейсов, включая lOleObject^lDataObjectJPersistStoi’age^i lOlelnPlaceActiveObject.’dra классы могут обращаться к контейнеру через указатели на интерфейсы, которые им передает контейнер. Однако важно иметь в виду, что класс, производный от CView, рисует в окне компонента, активизированного по месту, а класс, производный от COieServerltem, рисует в метафайле по команде от контейнера.
Пример ЕХ28А
Для создания мини-сервера на базе MFC особых познаний в OLE не требуется. Тем не менее, этот пример — неплохое начало, так как дает представление о взаимодействии компонентов и контейнеров. В данном случае компонент совсем не сложен. Он просто выводит в окно некоторый текст и графику. Текст хранится в документе, и для его редактирования предусмотрено диалоговое окно. Далее приведены этапы создания программы.
С помощью AppWizard создайте проект ЕХ28А в каталоге \VCPP32\EX28A. Выберите параметр Single Document Interface, а В диалоговом окне AppWizard Step 3 — параметр Mini-server.
Просмотрите сгенерированные файлы. Помимо уже знакомых Вам файловдля приложения, документа, основного окна и окна представления, Вы получили два новых.
ЗаголовокРеализацияКлассБазовый класс MFC•
Srvrltem.hSrvrkem.cppCEx28aSn>rItemCOleSeroerltem
IpFrame.hIpFrame.cppCInPlaceFrameCOlelPFrameWnd
Добавьте в класс документа переменную-член для текста. Введите в объявление класса в файле cx28aDoc.h открытую переменную-член:
CString m_strlext;
В функции-члене OnNewDocument класса документа задайте для этой переменной начальное значение «Initial default text» («Начальный текст по умолчанию»),
Добавьте диалоговое окно для редактирования текста. Создайте новый шаблон диалогового окна с полем ввода, а затем с помощью Class Wizard сгенерируйте класс CTextDia- log, производный от ('Dialog. Не забудьте включить заголовочный файл класса диалогового окна в файл ex28aDoc.cpp. Кроме того, создайте, используя Class Wizard, псрсмснпую- член т strText типа CString для поля ввода.
Добавьте новый элемент в меню внедряемого компонента и в меню компонента, активизируемого по месту. Дополните меню IDRSRVRJEMBEDDEDh IDR_SRVRJNPMCE командой Modify. Для этого при помощи редактора ресурсов вставьте в меню IDR_SRVR_F.M- BEDDED элемент верхнего уровня EX28A-EMBED и затемдобавьте команду Modify в новое подменю; аналогично добавьте элемент верхнего уровня EX28A-INPLACE в меню IDR_SRVRIN- РЕАСЕи команду Modify в получившееся подменю.
Команде Modify в каждом меню укажите идентификатор ID_MODIFY.Далее, используя ClassWizard, свяжите команду с функцией OnModij^ классе документа. Код обработчика команды ОпА^огй^ъыглядит так:-
' void CEx28aDoc: ^rWodifyO■
CTextDialog dig;
dlg.m strText = m strText;
if (dlg.DoModal() == I00K) {
m strText = dlg.m_strText;‘
- ■ UpdateAUViews(NULL); // инициирует вызов CEx28aView::0nDraw
см. след. стр.см. след. стр.UpdateAllItems(NULL); // инициирует вызов CEx28aSrvrItem;:0nDraw
см. след. стр.
см. след. стр.
SetModifledFlag();
Переопределите функцию OnPrepareDCs классе «вид». Создайте ее с помощью ClassWizard и замените ее кед на:
pDC->SetMapMode(MM_HIME-RIC);
Отредактируйте функцию OnDrewB классе «вид». Следующий код (из ex28aView.cpp) выводит в центр клиентского прямоугольника окружность диаметром 2 см и текст (с переносом строк, чтобы он умещался в окне):
void CEx28aView::OnOraw(CDC* pDC)
* CEx28aDoc» pDoc = GetDocumentO;
ASSERT.VALID(pDoc);'
CFont font;
font.CreateFont(-600, 0, 0, 0, 400, FALSE, FALSE. 0, ANSI-CHARSET, OUT-DEFAULT PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT-QUALITY, DEFAULT-PITCH ! FF_SWISS, "Arial");
CFont* pFont = pDC->SelectObject(&font);
CRect rectClient;
GetClientRect(rectClient);
CSize sizeClient = rectClient.Size();.
pDC->DPtoHIMETRIC(tsizeClient);
CRect rectEllipse(sizeClient.cx/ 2 - 1000, -sizeClient.cy / 2 + 1000, sizeClient.ex / 2 + 1000, -sizeClient.cy / 2 - 1000);
pDC->Ellipse(rectEllipse);
pDC->TextGut(O, 0, pDoc->m_strText);
pOC->SelectObject(pFant);
Отредактируйте функцию OnDraw серверного элемента. Следующий код из Srvrltem.cpp пытается изобразить то же, что и функция OnDraw класса «вид». О серверном элементе (server item) Вы узнаете чуть позже.
BOOL CEx28aSrvrItem::0n3raw(CDC* pDC, CSizeS rSize)
{.
H Удалите это, если используете rSize UMREFERENCED_PARAMETER(rSize);.
CEx28aDoc* pDoc = GetDocumentO;•
ASSERTVALID(pDoc);■
И ДОДЕЛАТЬ: установить режим преобразования и размеры,
// (размеры обычно соответствуют возвращенным из OnGetExtent) pDC->SetMapMode(MM_AMISOTROPIC);■
pDC->SetWindowOrg(0, 0);
pDC->SetWtndowExt(3000, -3000);
CFontfont;
font.CreateFont(-500, 0, 0, 0, 400, FALSE, FALSE, 0, ANSI_CHARSET, OUT DEFAULT PRECIS.
■CLIP_DEFAULT_PRECIS, DEFAULT-QUALITY, DEFAULT-PITCH ! FF.SWISS, “Arial");
CFont* pFont = pDC->SelectObject(Afont);
CRect rectEllipse(CRect(500, -500, 2500, -2500));
pDC->Ellipse(rectEllipae);
pDC->TextOut(O, 0, pDoc->m_strText);
pDC->SelectObject(pFont);
return TRUE;■
Отредактируйте функцию Serialize класса документа. О загрузке и сохранении текста документа в потоке данных Contents в основном хранилище объекта заботится каркас приложений. Все, что требуется от Вас, — написать обычный код сериализации:
void CEx28aDoc::Serialize(CArchive& ar) {
if (ar.IsStcringO)
ar « m strText;
else
{
ar » ms tri ext;
}
1
Здесь также присутствует функция СЕх28а$гогйет::Хепа11же1котораяп.росто вызывает функцию Serialize класса документа.
Соберите и зарегистрируйте приложение ЕХ28А. Для обновления Реестра надо один раз запустить приложение в автономном режиме.
Протестируйте приложение ЕХ28А. Для этого понадобится пр о грамма-контейнер, поддерживающая активизацию по месту. Воспользуйтесь Microsoft Excel 97, если он у Вас есть, или соберите проект MFC-примера DRAWCLI. Выберите команду Insert Object в контейнере. Эта команда может присутствовать либо в меню Edit, либо в меню Insert Затем выберите в списке строку Ех28а Document
ПРИМЕЧАНИЕ Отладка внедряемого компонента осуществляется так же, как и ЕХЕ компонента Автоматизации. Дополнительную информацию см. на врезке «Отладка программы ЕХЕ-компонен- та»(гл.25).
Сразу после вставки нового объекта ЕХ28А Вы увидите заштрихованную рамку, сообщающую, что объект активизирован по месту. Размер ограничивающего прямоугольника 3x3 см, в центре прямоугольника— окружность диаметром 2 см.
i Initial default 1 \
1 wwwwwwtwwy*
Теперь, если Вы щелкнете в любом месте окна контейнера, объект станет неактивным (см. рис)
Initial default'
В первом случае Вы видели результат вывода функцией OnDraw из класса «вид»-, во втором — результат вывода функцией OnDraw серверного элемента. В обоих случаях окружность одинакова, но текст форматируется по-разному, так как серверный элемент рисует в контексте устройства метафайла.
Попробуйте увеличить высоту объекта, используя маркеры масштабирования (для их отображения щелкните объект один раз), и Вы увидите, что окружность вытянулась, а текст стал
крупнее, как показано ниже на левом рисунке. Если Вы вновь активизируете объект двойным щелчком, произойдет повторное форматирование его содержимого, показанное на правом рисунке.
Щелкните в любом месте окна контейнера, потом щелкните объект (один раз) и выберите команду Ех28а Object из меню Edit В появившемся подменю выберите команду Open. В результате компонент запустится в режиме внедрения, без активизации по месту.
Л f i!?fU iri R-j^k2- cxZbd .: "••• Г
Initial default text
Видите — теперь отображается меню компонента IDRJSRVRJEMBEDDED.
Внедряемый MDI-компонент?
Пример ЕХ28А — мини-сервер с SDI-интерфейсом. Всякий раз, когда контроллер создает объект ЕХ28А, запускается новый процесс ЕХ28А. Вы можете предположить, что процесс мини-сервера с MDI-интерфейсом способен поддерживать несколько объектов-компонентов, каждый в своем документе. Но это не так. Если попросить Арр Wizard сгенерировать минисервер с MDI-ингерфейсом, он создаст SDI-программу, аналогичную ЕХ28А Теоретически один процесс может поддерживать несколько внедренных объектов в разных окнах, но такую программу на базе MFC так просто не создашь.
Стратегия масштабирования для компонента, активизируемого по месту
Результат работы программы ЕХ28А свидетельствует, что изображение метафайла не всегда совпадает с изображением в окне компонента, активизируемого по месту. Мы надеялись создать другой пример, где оба изображения будут одинаковы. Но, используя в качестве кон-
тейнеров приложения из Microsoft Office 97, добиться этого не удалось. Каждое из них вело себя чуть иначе и непредсказуемо. Плохо и то, что контейнеры по-разному масштабируют изображения,
Генерируя компонент, Арр Wizard создаете классе серверного элемента переопределенную функцию OnGctExtont. Она всегда возвращает жестко заданный размер (3000,3000). Конечно, это значение можно изменить, но при динамическом его изменении надо быть очень осторожным. Мы пытались использовать для хранения размеров компонента в документе свою переменную-член, но при изменении масштабного множителя в контейнере это приводило к неприятностям. Мы подумали, что контейнеры, может быть, больше используются другой виртуальной функцией серверного элемента — OnSetExtent, но они не делали и этого.
В общем, безопаснее оставить размеры компонента фиксированными и считать, что контейнер сделает все, как надо. Имейте в виду: котда контейнерное приложение печатает свой документ, на печать выводятся метафайлы компонента. Поэтому метафайлы важнее изображения, получаемого при активизации по месту.
Но, разрабатывая и контейнер и компонент, Вы получаете большую степень свободы. Вы можете создать систему обработки модульных документов с собственным протоколом масштабирования изображений. Если надо, можно задействовать даже другие OLE-интерфейсы.
Взаимодействие контейнер-компонент
Анализировать компонент и контейнер по отдельности бессмысленно — надо наблюдать за их совместной работой. Рассмотрим это взаимодействие поэтапно. Для начала предположим, что у нас есть ЕХЕ-компонент и ЕХЕ-контейнер и что последний управляет компонентом через OLE-интерфейсы. ■
ДокументДокументIPeraistStoragaW j^ggIPeraistStoragaW j^gglOleObject 1ёН * S-illlOleObject 1ёН * S-illIDataObject l.$®f '-; -^ ^ |1IDataObject l.$®f '-; -^ ^ |1Контейнер содержит указатель на объект IUnknownКонтейнер содержит указатель на объект IUnknownВернемся к примеру с моделированием космического корабля из главы 24. Для загрузки компонента космического корабля и создания объекта «космический корабль» клиентская программа вызывала GoGetClassObjecnn IClassFactory::CreateInstancesocn.e чего обращалась к Queryinterface,чтобы получить указатели на Motion и IVisual. Программа-контейнер, поддерживающая внедрение, работает аналогично клиенту из того примера: запускает программу- компонент по идентификатору класса компонента, и эта программа создает объект. Различия только в интерфейсах
Документ
Документ
IPeraistStoragaW j^gg
IPeraistStoragaW j^gg
lOleObject 1ёН * S-ill
lOleObject 1ёН * S-ill
IDataObject l.$®f '-; -^ ^ |1
IDataObject l.$®f '-; -^ ^ |1
Контейнер содержит указатель на объект IUnknown
Контейнер содержит указатель на объект IUnknown
^У-1 Фабрика IClassFactbry
Рис. 28-2. Компонент с точки зрения программы-контейнераРис. 28-2. Компонент с точки зрения программы-контейнера.uttoeMMHietititiine* Компонент получает IStotage* .от контейнера; вызывает (Storage::Save и IStoragezLoad
Рис. 28-2. Компонент с точки зрения программы-контейнера
Рис. 28-2. Компонент с точки зрения программы-контейнера
I" Даннье объекта" \Л ^хранилище) ।
19 Зак. №787
На рисунке 28-2 показано, как выглядит компонент со стороны программы-контейнера. Все показанные на схеме интерфейсы, кроме lOleObject, Вам уже знакомы.
Применение интерфейса компонента lOleObject
Загрузка компонента — не то же самое, что его активизация. В этом случае процесс просто запускается и ждет инструкций. Получив указатель на lOleObjec^pta объекта компонента, контейнер может вызвать функцию-член DoVerb с параметром — кодом команды, например OLEZVERBJSHOWllocne этого компонент должен вывести на экран свое основное окно и далее д ействовать как любое другое Windows-приложение. Взглянув на описание lOleObject:;Do- Verb (см. раздел «Интерфейс lOleObject»далее в этой главе), Вы заметите параметр lOleClienl- Site'. О клиентских посредниках (client sites) мы поговорим чуть позже, а пока можете установить этот параметр в NULL, и большинство компонентов будет работать нормально.
На этом этапе важна еще одна функция — IOleObject::Close.KaK. Вы, наверное, догадались, контейнер вызывает ее, собираясь завершить работу программы-компонента Если в данный момент процесс-компонент обслуживает только один внедренный объект (как в случае компонентов на базе MFC), этот процесс тут же завершается.'
Загрузкаисохранениевнутреннихданныхкомпонента:составныедокументы
На рисунке 28-2 показано, что контейнер управляет хранилищем через указатель на IStorage, а компонент реализует IPersistStorage. Это означает, что компонент может загружать и сохранять свои внутренние данные при вызове кон тейнером IPersistSiorage;Load и Save. Из главы 27 Вы уже знаете, как применяются эти интерфейсы, но на этот раз контейнер будет сохранять в хранилище идентификатор класса компонента. Теперь контейнер сможет считывать этот идентификатор из хранилища и на его основе запускать программу-компонент перед вызовом lPersistStorage::Load.
Хранилище действительно важно для внедренного объекта. Как вирус должен жить в живой клетке, так и внедренный объект должен жить в хранилище. Оно должно быть доступно Всегда, поскольку объект постоянно загружает и сохраняет себя, а также читает и записывает временные данные.
То, что изображено на рисунке 28-2, называется составным документом (compound document). Контейнер управляет всем файлом целиком, но за хранилища внутри файла отвечают внедряемые компоненты. У каждого внедренного объекта есть свое главное хранилище, и контейнер не знает, да и знать не желает, что там, в этом хранилище.
Передача данных через буфер обмена
Если Вы уже работали с программами — OLE-контеЙперами, в том числе с Microsoft Excel, Вы наверняка заметили, что в буфер обмена можно копировать (и вставлять оттуда) целые, внедренные объекты. Для них предусмотрен специальный формат объекта данных CFJEM- BEDDEDOBJECT. Если поместить в буфер обмена указатель на IDataObjectjym объекта данных, содержащего формат CF_lIMBRDDEDOBJlidXn. сопутствующий ему CF_OBJECTDESCRIPTOR), другая программа сможет воссоздать объект и загрузить нужную программу-компонент.
На самом деле, все не так уж и сложно, как кажется. Единственное, что находится в формате CFJBMBEDDEDOBJECT— указатель на IStorage. Программа, копирующая в буфер обмена, должна убедиться, что данные объекта были сохранены в этом хранилище функцией IPersistStorage: :Save, после чего передать в объекте данных указатель на IStorage. Программа, вставляющая данные из буфера обмена, считывает из исходного хранилища идентификатор класса, загружает компонент и вызывает IPersistStorage: :Load для загрузки данных из хранилища.
Объекты данных для буфера обмена создаются контейнерной программой по мере надобности. Интерфейс компонента IDaiaObjectHs используется для передачи внутренних данных объекта.
Получение метафайла от компонента .
Вы уже знаете, что компонент должен подготовить метафайл, который потом воспроизведет контейнер. Но как компонент доставляет метафайл контейнеру? Для этого и предназначен интерфейс IDataObject (см. рис. 28-2), Контейнер просто вызывает IDataObjecL-GetData, запрашивая формат CF_METAFlLEPICH.o не спешите — предполагается, что контейнер может получить метафайл, даже если компонент не загружен. Теперь Вы готовы подняться на следующий уровень сложности.
Роль внутрипроцессного обработчика
Если программа компонента выполняется, это — отдельный процесс. Иногда она вообще не выполняется. Но в любом случае к контейнерному процессу подключается OLE32DLL. Эта DLL называется обработчик объекта (object handler). .
ПРИМЕЧАНИЕ ЕХЕ-компонент может располагать собственным DLL-модулем — обработчиком, но большинство компонентов использует стандартный обработчик.■-
Теперь взаимодействие контейнера и компонента представляется нам по-новому (см. рис. 28-3). Обработчик связывается с компонентом по RPC-соединепию, осуществляя маршалинг всех вызовов функций интерфейса. Однако обработчик не просто заместитель компонента при маршалинге — он поддерживает кэш, содержащий метафайл объекта компонента. Обработчик загружает и сохраняет кэш в хранилище, а также заполняет кэш, обращаясь к функции компонента IDataObjectuGeiData.
ХранилищеХранилищеIUnknown ("")IUnknown ("")lOleObjectlOleObjectIDataObjectIDataObjectIDataObjectIDataObjectMewObjectZMewObjectZtPersistStoragetPersistStorageIPersistStorageIPersistStorage■соединение■соединениеidassFactory^idassFactory^IClassFactory (StorageIClassFactory (StoragetOteObjecttOteObjectОбъектОбъект5 Объект-*>5 Объект-*>классаклассаМетафайл Данные объею'аРис. 28-3. Внутрипроцессный обработчик и компонентМетафайл Данные объею'аРис. 28-3. Внутрипроцессный обработчик и компонент
Хранилище
Хранилище
IUnknown ("")
IUnknown ("")
lOleObject
lOleObject
IDataObject
IDataObject
IDataObject
IDataObject
MewObjectZ
MewObjectZ
tPersistStorage
tPersistStorage
IPersistStorage
IPersistStorage
■соединение
■соединение
idassFactory^
idassFactory^
IClassFactory (Storage
IClassFactory (Storage
tOteObject
tOteObject
Объект
Объект
5 Объект-*>
5 Объект-*>
класса
класса
Метафайл Данные объею'а
Рис. 28-3. Внутрипроцессный обработчик и компонент
Метафайл Данные объею'а
Рис. 28-3. Внутрипроцессный обработчик и компонент
Когда контейнеру надо нарисовать метафайл, он не делает этого сам, а просит обработчик воспроизвести метафайл, вызывая его функцию MewObject2;:Dmut. Обработчик пытается удовлетворить максимально возможное число запросов контейнера, не обращаясь к программе компонента, но, если ему потребуется функция компонента, он позаботится о загрузке программы (если она еще не загружена).
ПРИМЕЧАНИЕ Интерфейс IViewObject2 — пример эволюции OLE. Кто-то решил добавить новую фун- <цию, в данном случае GetExtent, к интерфейсу iViewObject IViawObject2является производным от I'ViewObjecfw содержит упомянутую функцию. Все новые компоненты должны реализовывать новый интерфейс и возвращать указатель на lViewObject2, когда Queryinterface вызывается либо для HDJView- Object. либо для HDJViewObject2. С помощью библиотеки MFC сделать это несложно: Вы пишете два элемента в таблице интерфейсов и связываете их с одним и тем же вложенным классом.
На рисунке 28-3 показано, что в хранилище объекта присутствуют данные как объекта, так и метафайла. Когда контейнер вызывает функцию обработчика IPersistStorage::S«ue,тот, записав в хранилище кэш (содержащий метафайл), вызывает одноименную функцию компонента, которая записывает внутренние данные объекта в то же хранилище. При загрузке объекта процесс идет в обратном направлении.
Состояния компонента
Теперь, когда Вы знаете, что такое обработчик, рассмотрим четыре состояния, в которых может находиться внедренный объект.•
/Storage/StorageКомпонент" поддерживает указатели на контейнерКомпонент" поддерживает указатели на контейнерПосредник поддерживает указатель на объект lOtoContainer .„i.■.>.<•><•>•я,,•.,., '^ "Посредник поддерживает указатель на объект lOtoContainer .„i.■.>.<•><•>•я,,•.,., '^ "toteCtientSitetoteCtientSitelAdviseSink• Посредник екает свое хранилищеlAdviseSink• Посредник екает свое хранилище1>М**а1О*’*<И*ЛНж1*><**Н1>М**а1О*’*<И*ЛНж1*><**НЮ!еОЮ!еО’•г’•гIDatatIDatatМе\Ме\. Фабрика?... Фабрика?..Компонент получает IStorage* от контейнераКомпонент получает IStorage* от контейнераРис. 28-4. Взаимодействие между контейнером и компонентомРис. 28-4. Взаимодействие между контейнером и компонентом
/Storage
/Storage
Компонент" поддерживает указатели на контейнер
Компонент" поддерживает указатели на контейнер
Посредник поддерживает указатель на объект lOtoContainer .„i.■.>.<•><•>•я,,•.,., '^ "
Посредник поддерживает указатель на объект lOtoContainer .„i.■.>.<•><•>•я,,•.,., '^ "
toteCtientSite
toteCtientSite
lAdviseSink
• Посредник екает свое хранилище
lAdviseSink
• Посредник екает свое хранилище
1>М**а1О*’*<И*ЛНж1*><**Н
1>М**а1О*’*<И*ЛНж1*><**Н
Ю!еО
Ю!еО
’•г
’•г
IDatat
IDatat
Ме\
Ме\
. Фабрика?..
. Фабрика?..
Компонент получает IStorage* от контейнера
Компонент получает IStorage* от контейнера
Рис. 28-4. Взаимодействие между контейнером и компонентом
Рис. 28-4. Взаимодействие между контейнером и компонентом
Состояние
Описание,.
(Пассивное
Загруженное
Объект существует только в хранилище
Обработчик объекта запущен и содержит метафайл в своем кэше, но ЕХЕ-компонент не выполняется
Исполняемое Активное
ЕХЕ-компонент загружен и выполняется, но окно невидимо Окно ЕХЕ-компонента видимо для пользователя
iUnknowni
Интерфейсы контейнера
Теперь перейдем на сторону контейнерной программы. Взгляните на рисунок 28-4. Контейнер состоит из документа и одного или нескольких посредников (sites). Интерфейс lOleCon- Лагпегсодержит функции, позволяющие перечислять этих посредников, но здесь они не понадобятся. Очень важен интерфейс lOleClientSite, Каждый посредник — это объект, доступ к
которому компонент получает через указатель на lOleCUentSite. Создавая внедренный объект, контейнер вызывает IOieObject::SetClientSite,чтобы установить одну из двух связей компонента с контейнером. Посредник сохраняет указатель на ГО/eObyecZдля своего объекта-компонента.
Важной функцией lOkCHentSite является SaveObject. Решив сохранить себя в хранилище, контейнер не делает это напрямую, а обращается к посреднику, вызывая lOleCUentSite; ;Saw- Object. «А почему так?» — спросите Вы. Обработчику нужно записать в хранилище метафайл - вот почему. IOleClientSite:SaveObject вызывает IPersistStorageaSave на уровне обработчика, поэтому обработчик может выполнить свою работу перед вызовом функции Save компонента.
Другая важная функция lOleCUentSite — это OnSbowWindow, Компонент вызывает ее при запуске и завершении своей программы. Клиент должен показывать внедренный объект заштрихованным прямоугольником, когда программа компонента исполняется или активна.
Уведомления
На рисунке 28-4 показан еще один интерфейс, прикрепленный к посреднику, — lAdviseSink. Это контейнерная сторона второго соединения с компонентом. Для чего нужно второе соединение? Связь через lOleCHentSite вдет непосредственно от компонента к контейнеру, но связь через lAdviseSink проходит через обработчик. Создав внедренный объект, посредник вызывает lVietvObject2::SetAdvisepeigep>aBa.x указатель на lAdviseSink, а тем временем обработчик уже забежал вперед и установил два уведомительных соединения с компонентом. При создании внедренного объекта обработчик вызывает iOleObjectnAdvise, а затем — IDateOb- ject::DAdvise. Как только данные компонента меняются, он уведомляет об этом обработчик через iDataObject.a когда пользователь сохраняет данные компонента или закрывает программу, компонент уведомляет обработчик через lOleObjeci. Эти соединения продемонстрирован на рисунке 28-5.
Рис. 28-5. Уведомления
Теперь, получая уведомление об изменении данных компонента (компонент вызывает MdviseSink-OnDataChange"), обработчик может уведомить контейнер вызовом lAdviseSink,:Оп- ViewCbangelB ответ контейнер вызывает функцию обработчика IVietvObject2::Draw.^^w. программа контейнера не выполняется, обработчик рисует метафайл из кэша, а если выполняется, — вызывает функцию компонента IDataObject::GetData, чтобы обновить рисуемый метафайл. Уведомления OnClose и OnSave передаются аналогично.
Метафайл для буфера обмена
Как Вы только что узнали, контейнер не оперирует с метафайлом напрямую, когда возникает необходимость нарисовать внедренный объект; вместо этого он вызывает lViewObject2;;Draw. Однако есть случай, когда контейнеру нужен прямой доступ к метафайлу. Копируя внедренный объект (и соответственно дескриптор объекта) в буфер обмена, контейнер должен вдобавок скопировать и метафайл. Для этого служит интерфейс IDataObject обработчика. Кон-
тейнер вызывает IDataObject.-.-GetData,запрашивая формат метафайла, и копирует этот формат в объект данных для буфера обмена.
Сводка интерфейсов
Теперь кратко о наиболее важных OLE-интерфейсах, которые пригодятся нам дальше. Списки функций, как и перечень их параметров, ни в коей мере не претендуют на полноту. Подробные их спецификации Вы найдете в Books Online или в книге Брокшмидта.
Интерфейс lOleObject
Этот интерфейс реализуют внедряемые компоненты, а указатель на lOleObject внедренного объекта хранит клиентский посредник.
HRESULTAdvise(IAdviseSink* pAdvSInk, DWORD* pdwConnectiony,
Обработчик вызывает эху функцию,- чтобы установить одно из двух соединений-уведомлений от компонента к обработчику. Обычно компонент реализует Advise за счет стандартного гбъекта-уведамитепя об OLE-изменениях (OLE advise holder object), который может подчеркивать несколько соединений-уведомлений.
HRESULT Close(DWORD dwSeveOptfonY,;
контейнер вызывает Close, чтобы завершить приложение компонента и в то же время оставить объект в загруженном состоянии. Обращение к этой функции происходит, когда пользо- затель щелкает за пределами окна компонента, активизированного по месту. Компоненты, поддерживающие активизацию по месту, должны при этом выполнить очистку и завершиться.
HRESULT DoVerb(LONG IVerb,... |OtoCllentSlte*pAc^eS/te,...);
<омпоненты поддерживают числовые коды команд (verbs), определенные в реестре. Например, аудиокомпонент может поддерживать команду «Р1ау». Внедряемые компоненты должны ] [оддерживать команду OLEIVERBJiHOWjto которой объект отображает себя для редактирования или просмотра пользователем. Если компонент поддерживает активизацию по месту, эта команда инициирует процесс визуального редактирования; в ином случае — приводит к запуску компонента в отдельном окне. Команда О1Л1УЕКЯ_ОРЕ№аставляет компонент, поддерживающий активизацию по месту, открыть объект в отдельном окне.'
HRESULT GetExtent(DWORD dwDrawAspect, SIZEL* pSizef);.
Компонент возвращает размеры объекта в единицах H1METRIC. На основе этой информации контейнер устанавливает размер ограничивающего прямоугольника для метафайла компонента. Иногда контейнер использует размеры из метафайловой картинки компонента.
HRESULT SetClientSite(IOIeClientSite* pCffwrtStfe);
Контейнер вызывает SetClientSite, чтобы компонент сохранил у себя указатель на посредник в контейнере.
HRESULT SetExtent(DWORD dwDrawAspect, SIZEL* pSiz^);■
Некоторые контейнеры вызывают эту функцию, чтобы задать размеры компонента.
HRESULT SetHostNemesfl-PCOLESTRezConte/nefApp, LPCOLESTR szContatnerObjy,
Контейнер вызывает эту функцию, чтобы компонент вывел имя контейнерной программы в заголовке своего окна.
Н RESU LT Unadvise(DWORD* dwConnecHony,.
Эта функция разрывает соединение, установленное вызовом Advise.
Интерфейс lViewObject2
Этот интерфейс реализуют обработчики внедряемых компонентов. Обработчики занимаются определенными деталями связывания и внедрения па стороне клиента. Обработчикпо умолчанию, предоставляемый Microsoft, находится в OLE32.DLL Контейнер вызывает его функции, но программа компонента не реализует его. Маршалинг интерфейса ZViewObject24epe3 границы процесса невозможен, так как этот интерфейс сопоставлен с контекстом устройства.
HRESULT Draw(DWORD dwAspect,.... const LPRECTL iprcBounds,...);
Контейнер вызывает ее, чтобы вывести метафайл компонента в заданный прямоугольник.
HRESULT SetAdvise(DWORD dwAspect,... (AdvIseSlnk* pAdvSinKi;
Контейнер вызывает SetAdvise, чтобы установить соединение-уведомление с обработчиком, а тот в свою очередь устанавливает такое же соединение с сервером.
Интерфейс iOteClientSite
Этот интерфейс реализуется контейнерами. При этом на каждый объект компонента приходится по одному объекту-посреднику.
HRESULT tietContainer((OleContalner“ ppConta/ner);
Функция GetContoiner возвращает указатель на контейнерный объект (документ), который можно использовать для перебора посредников в контейнере.
HRESULT OnShowWlndow(BOOL /Show);
Компонент вызывает эту функцию при переключении между состояниями «загружен» и «исполняется» (или «активен»). Когда объект активен, контейнер должен заштриховать его прямоугольник.
HRESULT SaveObject();
Компонент вызывает SaueObject, чтобы сохранить себя в хранилище. В ответ контейнер вызывает iPersistStorage;:Save.
Интерфейс tAdvIseSlnk.
Этот интерфейс реализуется контейнерами. Обработчики внедренных объектов вызывают его, функции в ответ на уведомления от компонентов.
void OnCloseO;
Программы-компоненты вызывают эту функцию в процессе своего завершения
void OnViewChange(BOOL dwAspect,...);
Обработчик вызывает OnVtetoCbangenpH изменении метафайла. Чтобы это уведомление было отправлено, компонент должен выполняться, поэтому, получив уведомление, обработчик может вызвать функцию компонента IDataObject-GetDatcm. сохранить в своем кэше обновленный метафайл. Даже контейнер может отрисовать этот метафайл, вызвав WiewObject2::Draw.
Вспомогательные функции OLE
Целый ряд глобальных OLE-функций позволяет инкапсулировать последовательности вызовов OLE-интерфейсов. Далее приведено описание некоторых из них (они используются в примере ЕХ28В).
HRESULT OleCreate(REFCLSIDrcto/d,REFIID Hid.... lOleCllentSite*pCilentSite, (Storage*pStg, void"ppvobb;
Эта функция сначала выполняет СОМ-последовательность создания объекта, используя заданный' идентификатор класса. Это приводит к загрузке программы компонента. Затем она вы-
зываст Quetylnteifflce^To6in получить указатель на /Perastftorage,используемый для вызова InitNewc параметром/)^. Кроме того, она обращается к Queryinterface, чтобы получить указатель на lOleObject^epes который вызывается SetClientSite с параметромpClientSite. И, наконец, она вызывает Queryinterface для интерфейса, заданного riid\ обычно это HDIOleObject.
HRESULT OleCreateFramData(IOataOb|ect*pSrcO0f0Ob/, REFIIDr//d, .„, lOleCHeniSHe^CfentS/te, IStorage*pStg, void** ppvObJ);
Эта функция создает внедренный объект по объекту данных. В примере ЕХ28В полученный извне объект данных содержит формат CFJEMBEDDEDOBJECE указателем на IStorage. Функция загружает программу компонента по идентификатору класса из хранилища, после чего вызываетIPerslstSeryer.-.locid,чтобы компонент загрузил внутренние данные объекта. Попутно функция обращается к lOleObjectrSetClientSite.
HRESULT OleDraw(IUnknown* pUnk, DWORD dwAspect, HOC hdcDraw, LPCRECT iprcSounds);
Функция вызывает Queryinterface через pUnk, чтобы получить указатель на IViewObject.a затем вызывает lViewObject::Draw,передавая как параметр указатель IprcBounds.
HRESULT OleLoad(IStarage*pSfg, REFIID riid, lOleClientSIte* pClientSite, void** ppvOb/);
Сначала O/y£o« с/выполняет COM-последовательность создания объекта, используя идентификатор класса из заданного хранилища. Затем вызывает IOleObject::SetClientSite'R IPersist- Storage::Load. И, наконец, вызывает Queryinterface для интерфейса, заданного riid, которым обычно бывает HD IOleObject.
HRESULT Q1eSave(iPerslstStorage‘ pPS, IStorage*pStg, -.);
Эта функция BU3HBaeTlPersistStorage::GetClassIDyTo6H получить идентификатор класса объекта, после чего записывает этот идентификатор в хранилище, заданноерЗ^. В заключение она обращается к IPersistStorage::Save.
Контейнерное OLE-приложение с поддержкой внедрения
Теперь, когда у нас есть работоспособный мини-сервер, поддерживающий внедрение (ЕХ28А), напишем для него программу-контейнер. Но использовать поддержку контейнеров со стороны MFC мы не станем, потому что хотим знать, что происходит на уровне OLE-интсрфей- ;ов. Конечно же, мы задействуем MFC-архитектуру «документ-вид», таблицы интерфейсов и классы объектов данных.
Поддержка MFC для OLE-контейнеров
Ясли Вы создадите OLE-контейнер на базе MFC с помощью AppWizard, то получите два класса: производный от GOleDocumenfa производный от COleClientltem. Эти базовые классы реализуют целый ряд важных интерфейсов OLE-контейнера для внедрения и активизации по месту. Идея заключается в том, что каждому объекту, внедренному в документ контейнера, соответствует один объект COleClientltem. Каждый объект COleClientltem является посредникам, определяющим место, занимаемое объектам компонента в окне.
Класс СО/е1)<?смтел/поддерживает список клиентских элементов, а Вы должны определить, как выделить элемент и как синхронизировать позицию метафайла с позицией окна компонента, активизируемого по месту. AppWizard генерирует базовое контейнерное приложение без поддержки связывания, работы с буфером обмена и операции drag-and-drop. Если Вам нужна поддержка этих средств, обратитесь к примерам из MFC: DRAWCLI и OCLIENT.
В нашем контейнере един MFC OLE-класс: COte/nwrtDirdog.dTarKnacc инкапсулирует OLE- функцию OleUIInsertObject, вызывающую стандартное диалоговое окно Insert Object Данное диалоговое окно позволяет выбирать объекты из списка зарегистрированных компонентов.
Некоторые ограничения контейнера
Поскольку наш контейнер — учебный, мы его несколько упростим, чтобы уменьшить объем исходных текстов. Во-первых, контейнер не будет поддерживать активизацию по месту — он позволит редактировать объект в отдельном окне. Кроме того, он разрешает внедрять в документ только один объект, следовательно, не поддерживает связывание. Контейнер хранит объект, внедренный в документ, в файле структурированного хранилища, но хранилище обрабатывается напрямую, в обход MFC-подсистемы сериализации. Поддержка буфера обмена предусмотрена, но операция drag-and-drop не поддерживается. Но, если не считать упомянутых ограничений, честное слово, это весьма приличный контейнер]
Возможности контейнера
Ну хорошо, так на что же он действительно способен? Вот его возможности:
I ♦ как и всякоеМОТ-приложение на базе MFC, оперирует сразу с несколькими документами; ♦ отображает метафайл компонента в ограничивающем прямоугольнике, размеры и пози- ' цию которого в окне представления можно изменять;
поддерживает временное хранилище для каждого внедренного объекта;
реализует команду Insert Object, позволяющуювыбирать зарегистрированный компонент. Выбранный компонент запускается в своем окне;
позволяет копировать (ивырезать) внедренные объекты в буфер обмена ивставлять объекты из него. Способен также обмениваться объектами с другими контейнерами, такими как Microsoft Word или Microsoft Excel;
I* допускает удаление внедренного объекта;
отслеживает переход компонента между состояниями «загружсн/активен» изаштриховы- вает ограничивающий прямоугольник объекта, когда компонент загружен или активен;
перерисовывает метафайл внедренного объекта, получив от компонента уведомление об изменениях;
сохраняет объект во временном хранилище, когда компонент обновляет объект или завершается;
копируетвременное хранилище внедренного объекта в заданные файлы-хранилища или из них при выборе пользователем из меню Edit команд Сору То или Paste From.
Пример ЕХ28В
Теперь перейдем собственно к программе. Сейчас самое время открыть рабочее пространство \vcpp32\ex28b\ex28b.dsw и собрать проект ЕХ28В, Если Вы выберете из меню Edit команду Insert Object, а потом — Ех28а Document из списка, запустится компонент ЕХ28А. Если изменить данные компонента, то контейнер и компонент будут выглядеть, как показано на рисунке на стр. 552.
Класс CEx28bView
Чтобы лучше понять программу, сначала обратитесь к классу «вид». Посмотрите на листинг (см. рис. 28-6), но не обращайте внимания на указатели на lOleClientSite. Эта контейнерная программа работает, даже если все параметры-указатели на lOleClientSite равны NULL. В этом случае она лишь не получит уведомления при изменении метафайла или внутренних данных объекта. Кроме того, компоненты будут отображать свое меню для работы в автономном режиме, а не специальное меню для редактирования внедренных объектов.
EX28BVIEW.H
«if !defined(AFX EX28BVIEW_H_1EAAB6E1 6011 11D0_848i:_00400526305B_INCLUDED )
«define AFX_EX28BVIEW_H_1EAAB6E1_6O11 _11 D0.848F_0040C626305B__INCLUDED_
«if _MSC_VER > 1000
«pragma once
ffendif // _MSC_VER > 1000
«define CF OBJECTDESCRIPTOR "Object Descriptor"
«define CF_EMBEDDEDOBJECT "Embedded Object"
«define SETFORMATETCffe, cf, asp, td, ned, к) V
<(fe). cfFormat=cr, \
(fe).dwAspect=aso, \
(fo).ptd=td, \
(fe) .tymea=mcd, \
(fe).llndex=li)
/////////////////////////in iniiiiiiiiiiiiiiHminrininiiiiini
class CEx28hView : public CScrollView
{
public;
CLIPFORMAT m cfObjDesc;
CLIPFORHAT m_cfEmbedded;
CSize m.sizeTotal; // размер документа
CRectTracker m_tracker;
CRect m_rectTracker; // логические координаты
protected: // создание только сериализацией
CEx28bView();
DECLARE_DYNCREATE(CEx28bView)
// Атрибуты *
ouolic:
CEx28bDcc* GetDocumentO;
private:
void GetSizeC);
void SetNames();
void Se*ViewAdvise();
IbqoL MaKaMetafilefictfCOleDatcSource* pSource);
Рис. 28-6. Листинг класса СЕхгвЬИеи'конгейнера
COJeDataScurce* SaveObject();
BOOL DoPaste0bjecL(u01eDataDbject» pDataObject);
BOOL DoPasteObjectDescriptor(COLeDataObject« pDataObject);
// Переопределенные функции
// Переопределенные виртуальные функции, сгенеоированные ClassWIzard
//{{AFX_VIRTUAL(CEx28bVlew>
public:
virtual void OnDraw(CDO pOC); // переопределена ДЛЯ отрМОВки этого окна представления
virtual 800L PreCreateWindow(CREATESTRUCT& cs);
virtual void OnInitialUpdate();
protected:
virtual BOOL OnPreparePrinting(CPrintInfo« plnfc):
virtual void OnBeginPrinting(CDC* pDC, CPrintlnfo* plnfo);
virtual void OnEndPrintlng(CDC* pDC, CPrintlnfo» plnfo);
//}}AFX_VIRTUAL
// Реализация
public:
virtual ~CEx28bView();
ttif def „DEBUG
virtual void AssertValidO const;
virtual void Dump(CDumpContext& de) const;
#endif
protected:
// Сгенерированные функции таблицы сообщений
protected:
//{{AFX_MSG(CEx28bView)
afxjnsg void OnEditCopy();
afx_msg void OnUpdateEditCopy(CCmdUI* pCmdUI);
afx_ir;sg void OnEditCopyto();
afx_msg void OnEditCutO;
afxjnsg void OnEditPasteO;
afx_msg void OnUpdateEditPaste(CCmdUI- pCmdUI);
afx_msg void OnEditPastefrom();
afxjnsg void OnEditlnsertobjeclO;
afx_msg void OiUpdatetdi:Irisertobject(CCnaUI* pCmdUI);
afxjnsg void CnLButtonDownfUINT nFlags, CPoint point);
afxjnsg void OnLBJttonDblClk(UINT nFlags, CPoint point);
afx msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitiest, UINT message);
//}}AFX MSG'
DECLARE„MESSAGE_MAP()
tfifndef „DEBUG // отладочная версия в ex28bView, epp inline CEx28b0oc* CEx28bView: :GetDocument()
{ return (CEx28bDoc*)m_pDocument; }
ffendif.'
////////////////////////////////////////////////////////////////////
//({AFX_INSERT_LOCATION}}
// Microsoft Visual C++ будет вставлять дополнительные объявления // непосредственно перед предыдущей строкой.
#endif //!'def3ned(AFX_EX2aBVIEW_H_1EAAB6El_6011_11D0_848F_00400526305B_INCLUDED_)
Рис. 28-8. (продолжение)
IEX28BVIEW.CPP
«include 'stdafx.h"
^include "ex28b,h”
«include "ex28b0oc.h"
«include "ex28bView.h"
4ifdef .DEBUG
«define new DEBUG NEW
«under THIS.FILE
static char THIS_FILE[] = __FILE..;
lendif
7//////////////////////////////////////////////////////////////////
7 CEx28bView
lM?LEMENT_DYNCREATE(CEx28bView, CScrollView)
BEGIN MESSAGE MAP(CEx28bVlew, CScrollView)
//{{AFX MSG MAP(CEx28bView)
ON COMMAND(ID.EDIT COPY, OnEditCopy)
ON_UPDATE_COMMAND_UI(ID_EDn_COPY, OnUpdateEditCopy)
ON COMMAND(ID EDIT.OOPYTO OnEditCopyto)
ON UPDATE.COMMAND UI(ID EDIT COPYTO, OnUpdateEditCopy)
ON COMMANDED EDIT CUT, OnEditCut)
ON UPDATE COMMAND UI(ID EDIT CUT, OnUpdateEditCopy)
ONCOMMANQIID EDIT PASTE, OnEditPaste)
ON UPDATE.COMMAND UI(ID EDIT PASTE, OnUpdateEditPaste)
ON COMMAND(ID EDIT_PASTEFROM, OnEditPastefrom)
ON_COMMAND(ID EDIT_INSERTOBJECT, OnEditlnsertobject)
ON_UPDATE_COMMAND UI(ID EDIT INSERTOBJECT, OnUpdateEditlnsertobject)
ON_WM_LBUTTONDOWN()
ON WM_LBUTTOHDBLCLK()
ON WM_SETCURSOR()
,//}}AFX_MSG_MAP
// Стандартные команды печати '
ON_COMMAND(ID_FILE_PRINT, CScrollView::OnFilePrint)
ON COMMANDED FILE PRINT.D1RECT, CScrollView::OnFilePrint)
ON_COMMAND(ID~FILE_PRINT PREVIEW, CScrollView::OnFilePrintPreview)
END_MESSAGE_MAP(y
,7//////////////////////////////////////////////////////////////////
// Создание/уничтожение CEx28bView
C:Ex28bView: :CEx28bView() : m.sizeTotal(20000, 25000),
U 20 x 25 см при печати
m rectTrackcr(O. 0. 0, 0)
m_cfObjDesc = ::RegisterClipboardFormat(CF_OBJFCTDESCRIPTOR);
m.cfEmbcdded = ::RcgisterClipboardFormat(CF_EMBEDDEDOBjECT);
CEx28bView::~CFx28bView()
.1
BOOL CEx28bView::PreCreateWindow(CREATESTRUCT8 cs)
// ДОДЕЛАТЬ: Изменить класс или стили окна изменением CREATESTRUCT cs return CScrollView::PreCreaLeWindow(cs);
////////////////////////////////////////////////////////////////////
// Отрисовка CEx2BbView
void CEx28bView::OnDraw(CDC* pDC)
CEx28bDoc* pCoc = GetDocumentO;
if(pDoc->m_lp01e0bj != NULL) {
VERIER; .'OleDraw(pDoc->n_lpOleObj. DVASPECT_CONTENT, pDC->GetSafeHdc(), m rectiracker) == S OK); }
m_tracker.m_rect = m_ rectiracker;
pDC->LPtoDP(m_tracker.m_rect); // в координатах устройства
if(pDoc->r._bHatch) {
m tracker, m nStyle |= CRectirackcr:inatchlnside;
1 else {
m tracker.m_nStyle &= -CRectiracker;:hatchinside;
}
m_tracker.Craw(pDC);
/////////////////////////////////////////////////////////////////Z//
// Печать CEx28bView
BOOL CEx28bView: :OnPreparePri'iting(CPrintInfo* plnfo)
plnfo->SetMaxPage(1);
return DoPreparePrinting(pInfo);
void CFx28bView::OnBeginPrinting(CDC* /*pDC*/, CPrintlnfo* /«plnfo*/)
// ДОДЕЛАТЬ: добавить дополнительный код инициализации перед печатью
void CEx28bView: :OnEndPrinting(CDC* /«pDC*/, CPrintlnfo* /«plnfo*/) {
,// ДОДЕЛАТЬ: добавить очистку после печати
////////////////////////////////////////////////////////////////////
// Диагностика CEx28bView
flifdef _DE3UG
void CEx2 8 bV i ew::Asse rtVa1id() const
{
CScroliyiew::AssertValid();
void CEx?.8bVievi: ;DLmp(CDunpContext& de) const
{
CScrollView::Dump(dc);
см. след. стр.см. след. стр.}
см. след. стр.
см. след. стр.
Зис. 28-6. (продолжение)
)Ex28bDoc* CEx28bView: ^etDocuwrtO // неотладочная версия подставляется в код
' ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CEx28b')oc)));
return (CEx28bDoc‘)m_pDocument;
rfandif //„DEBUG
HiiHiiiiiHiiHiiinimnmiiiiimiiimiinHiiiiHiiiHnnn // Обработчики сообщений CEx28bView
void CEx28bView: :CnInitialUpclate()
ТЯACE(" CEx28bView".: Onlnitia lUpdatc\n");
r._rectTracker = CRect(1000, -1000, 5000, -5000);
retracker. m_nStyle = CRectTracker::solidLine I CRectTracker::resizeOutside;
SetScrollSlzes(MN_HIMETRIC, m_slzeTotal);
CScrollView::OnInitialUpdate();
void CEx28bView: :Or<EditCopy()
COlcDataSource* pSource = SaveObject();
if(pSource) {
pSource->SotClipboard( i; // OLE удаляет источник данных
void CEx28bView. :OnUpdateEditCopy(CGmdUI* pCmdlll)
// обслуживает Copy, Cut и Copy To
oCmdUI->Enable(Get0ocument()->m_lp01e0bi 1= NULL);
void CEx28bView::OnEditCopyto()
{
// Копирование текста в файл STG (нет ничего особенного в расширении STG) CFileDialog dlg(FALSE, "stg\ ”*.stg");
if (dig. OoModaK) |= IDOK) {
return;
1
CEx28bDoc* pDoc = GetDocurentO;
// Создание структурированного хранилища для объекта ( pStgSub).
// Сначала создается корьевое хранилище, затем вторичное по имени "sub".
LPSTORAGE pStgfloot:
VFRIFY(::StgCreaTeDocflle(dlg.GetPathName().Alloc3vsString(),
STGM_READWRITE|STGM SHARE_EXCLUSIVE|SIGM_CPEATE, 0, 4pStgRoot) == S_OK);
ASSERT(pStgRoot != NULL);
LPSTORAGE pStgSub;
VtRlFY(pStgRoor->CreateStorage(CEx28bDoc::s szSub,
STGM_CREATE | S1GM_REAI>JRIТЕ| SIGN SHARE* EXCLUSIVE, 0. 0, ApStgSub) == S_OK);.
ASSERT(pStgSub I=NULL);.
// Получить IPersistStorage» для объекта
LPPERSISTSTORAGE pPS = NULL;
VERIFY(pDoc->m-lp01e0bj->QueryInterface(UD_IPersistStorage, (void**) &pPS) ==S_0K);
// Наконец, сохранить объект в новом хранилище в пользовательском файле
VERIFY(::01eSave(pPS, pStgSub, FALSE) == S_OK); // FALSE означает другое хранилище pPS->SaveCompletec(NULL); // Зачем это нужно? pPS->Release();
pStgSub->Releaae(): pStgRoot->Releasc();
void CEx28bVie,w: :OnEditCul()
OnEditCopyO;
GetDocument()->OnEditClcarAll();
void CEx28Wiew: :OrEditPaste()
I
CEx28bDoc‘ pDoc = GstDocument();
OOleDataObject dataObject;
VERIFYC dataObject.Attach01ipboard()); pDoc->DeletcContents();
DoPasteObjectDescriptor(&cataObj ect);
CoPastaOb ject(MataOhject);
SetViewAdvise(j;
GetSizeO;
pDoc->SetModifiecFlag(); pDoc->UodateAHVlews(NULL);
void CEx28bVl9w: :OnUpdateEditPaste(CCmdUI‘ pCmdUI)
// Убедимся, что объект данных доступен
COleDataObj ect dataObj ect;
if (oataOoject.AttachClipboardO &i
dataObject.IsDataAvailable(m_cfEmbedced)) { pCirdUI->Enable(TRUE);
} else { pCmdUI->Fnable(FALSE);
} )
void CEx28bView; :OnEditPastefrom()
{
CEx28bDoc* pDoc = GetCocument();
// Вставка из файла STG
CFileDialog dlg(lAuE, "stg", "*.sty");
if (dlg.DoModaK) ! = IDCK) { return;
// Открыть основное и вторичное хранилища
LPSTORAGE pStgRoot;
VERIFYC: :StgOpenStorage(dlg.GetPathName().AllocSysString(),
NULL, STGM_PEAD|STGM_SHARE_EXCLUSIVE, NULL, 0, SpStgRoot) ==S_OK);
ASSERKpStgRoot ! = NULL);
LPSTORAGE pStgSub;
VERIFYCoStgRoot->OpenStorage(CEx28tiCoc: :6_szS'Jb, NULL,
STGM READ|STGM_SHARE EXCLUSIVE, NULL, 0, &pStgSub) == S_OK);
ASSERT(pStg"Sub != NULL);
Рис. 28-6. (продолжение)
И Скопировать объект данных из пользовательского хранилища во временное
VERIFY(pStgSub->CopyTo(NULL, NULL, NULL, pDoc >m_oTempStgSuO) == S_OK):
И Наконец, загрузить объект - pCIientSite не нужен
LPOLECLIENTSITE pCIientSite = (LPOLECLIENTSITE) pDoc->GetInterface(4IID_I01eClientSite);
ASSERT(pCIientSite ! = NULL);
pDoc->DeleteContents();
VERIFY(: :01eLoad(pDoc->m..pTempStgSub, IID_IOleOt>j ect,
pCIientSite, (void-*) 4pDoc->m_lp01e0bj) == S_0K);
SetViewAdviseO;
pStgSub->Release();
pStgRoot->Release();
GetSize();
pDoc->SetModifiedFlag();
pDoc->UpdateAHViews(NULL);
void CEx28bView::OnEditInsertobject()
{'
CEx28bDoc* pDoc = GetDocument();
COlelnsertDialog dig;
If(dlg.DoHodaK) == IDCANCEL) return;
// Для Getlnterface не выполняется AddRef
LPOLECLIENTSITE pCIientSite = (LPOLECLIENTSITE) pDoc->GetInterface(&IID lOleClientSite);
ASSERT(pCIientSite != NULL);
pDoc->DeleteContents();
VERIFY(::01eCreate(dlg.GetClassID(), IID_I01e0bject,0LERENDER_DRAW,
NULL, pCIientSite, pDoc->m_pTempStgSub. (void»*) &pDoc->m_lp01eDbj) == S_OK);
SetViewAdviseO;
pDoc->m_lp01e0bj->DoVerb(0LEIVER8_SH0W, NULL, pCIientSite, 0, NULL, NULL); // OleRun не показывает объект
SetNamesO;
GetOocument()->SetModifiedFlag();
GetSizeO;'
pDoc->UpdateAllViews(NULL);
void CEx28bView: :OnUpdateEditInsertobject(CCmdJl* pCir.dUT) (
pCmdUI->Enable(GetDocument()->m_lp01e0bj == NULL);
void CEx28bView: :OnLButtDnDown(U.(NT nFlags, CPoint point) <
ТНАСЕ("**Входим в CEx28bView::OnLButtonDown - point = ” "(%d, %d)\n", point.x, point.y); if(m_tracker.Track(this, point, FALSE, NULL)) {
CClientDC dc(this);
OnPrepa reDC (Ade);
" m_rectTracker = m_t racks r.!n_rect;
de.DPtoLP(m_rectTracker); // Обновить логические координаты
GetDocument()->UpdateAllViews(NULL);
}
TRACEC*-Выходим из CEx28bView: :OnLButtonDown\n"):
void CEx28bView: :OnLButtonDblClk(UINT nFlags, CPoint point)
if(m_tracker.HitTest(poirt) == CRectTracker::hitNothing) return;
Н Активизировать обьект
CEx28bDoc* pDoc = GetDocument();
lf(pDoc->m IpOleObj != NULL) {
LPOLECLIENTSITE pCIientSite = (IPCLFCHENTSITE) pDoc->GetInterface(4HD lOleCIienlSite):
ASSE«T(pClientSite != NULL);''
VEflIFY(3Doc->m_lp01eObj->DoVerb(OLEIVERB_CPEN, NULL,
pCIientSite, O,GetSafeHwnd(), CRect(0, 0, 0, 0)) == S_OK):
SetNamesO;’
GotDocufrent()->SetModif:.edFlag();
BOOL CEx28bVLew: :OnSelCursor(CWnd« pWnd, UINT nHitTest, UINT message) {
if(m_tracker.SetCursor(pWnd, nHitTest)) {
return TRUE;
else {
return CScrollView: :OnSetCursor(pWrd, nHitTest, message);
lllllllinilllllllinillHlIIIIIHIIIIIIIHHIlHlHIIIIIHIHIIIin
void CEx28bView: :SetViewAdvise()
CEx28bDoc* pDoc = CetDocument();
if(pDoc->m_lp01e0bj ! = NULL) {
LPVIEW0BJECT2 pViewObj;
pDoc->m_lp01e0bj->0ueryInterface(IID_IView0Dject2, (void**) &pViewObj);
LPADVISESINK pAdviseSink = (LPADVISESINK) pDoc->GetInterface(&IID_lAdviseSink);
VERIFY(pViewObj->SetAdvise(DVASPECT_CONTENT, 0, pAdviseSink) == S_OK); pViewObj->Pelease();
void CEx28bView; ;SetNames() // задает имена владельцев
{
CEx28bDoc* pDoc = GetDocument();
CString strApp = AfxGetApp( )->m_ps2AppHare;
lf(pDoc->n_lp01e0bj J= NULL) {
pDoc->m_lp01e0bj->SetHostNames(strApp.AllocSysString(), NULL);
void CEx23bView::GetSize() t
CEx28bDoc* pDoc = GetDccumentO;
if(pDoc->m_lp01e0bj l-NULL) {
SIZEL size; // Запросить у компонента размеры pDoc->n_lp01e0bi->GetExtent(DVASPECT_C0NTENT, Ssize); m_rectTracker.right = m_rectTracker. left + size,ex: m_rectTracker.bottom = m_rectTracker.top - size.cy;
BOOL CEx28bView: :DoPaste0bject(C01e0ata0bject* pDataObject)
TRACE("Входим в CEx28bView: :DoPasteObject\n");.
,см. след. стр.
Рис. 28-6. (продолжение)
II Обновление кОмЗДНОГО пользовательского интерфейса должно предотвращать попадание сюда,
// если не CF_EMBEDDEDOBJECT
if (!pDataObject->IsDataAvailable(m_cfEmbedded)) {
TRACE("Формат CF,EM3EDDED08JECT недоступсн\п");
return FALSE;
}
CExSBbDoc* pDoc = GetDocnmentQ;
// Теперь создадим объек~ из IDataObject*.
// OleCreateFromData будет использовать формат CF EMBEDDEDOBJECT если он доступен.
LPOLECLIENTSITE pClientSite = (LPOLECLIENTSITE) p0oc->GetInterface(&IID_I01eClientSite);
ASSERT (pClientSite != NULL);
VERIFY(::01eCreateFromData(pData0bject->m_lpData0bject. IlD_I01e0bject. OLERENDER.DRAW, NULL, pClientSite, pDoc->m_pTempStgSub, (void") &pDoc->nr_lp01e0bj) == S_OK);
return TRUE;
BOOL CEx28bView: :DoPaste0bjectDescriptcr(C01eData0bject* pDataObject)
{
TRACE("Входим в CExZBbView:rDoPasteObjectDescrlptorXn");
STGMEDIUM stg;'
FORMATETC fmt;
CEx28bDcc» pDoc = GetDocurnentO;
if (!pDataObject->IsDataAvailable(m_cfObjDesc)) { ТВАСЕ("Формат CBJECTDF.SCRIFTOR недоступен\п"); return FALSE;
SETFORMATETC((mt, m_cfObjDesc, DVASPECT_CONTENT, NULL, TYMED_HGLOBAL, -1); VERIFY(pDataObject->GetDatam_cfObjDesc, &st.g, &fmt));
return TRUE;•
// Вспомогательная функция для буфера обмена и drag-and-drop
COleDataSource* CEx28bView::SaveObject()
TRACE("Входим в CEx28bView;:8ave0bject\n“);
CEx28bOoc» pDoc = GetDocurnentO;
if (pDoc->m_lp01e0bj != NULL) {
COleDataSource* pSource = new C01eDataSource();
// КОД ДЛЯ ДАННЫХ ОБЪЕКТА
FORMATETC fmte;■
SETFORMATETC(fmte, m cfEmbedded, DVASPECT_CONTENT, NULL, TYMED ISTORAGE, -1);
STGMEDIUM stgm;
stgm.tymed = TYMED_ISTORAGE;
stgm.pdtg = pDoc->m_pTempStgSub;
stgm.pUnkForRelease = NULL;
pDoc->m_pTempStgSub->AddRef(); // нужно сделать для обоих!
pDoc->m_pTempStgRoot->AddRef ();
pSource->CacneData(m_cfEmbedded, Astgm, Afmte);
// метафайл тоже нужен
HakeMetafilePict(pSource);
// КОД ДЛЯ ДАННЫХ ДЕСКРИПТОРА ОБЪЕКТА
HGLOBAL hObjOesc = : :Globa].Alloc(GMEM_SHARE, sizeof(OBJECTDESCRIPTOR));
LPOBJECTDESCRIPTOR pObjDesc = (LPOBJECTDESCRlPtOR) ::GlobalLock(hObjDesc);
pObjDesc->cbSize = sizeof(OBJECTDESCRIPTOR);
pObjDesc->clsid = CLSID_HIJLL;
pObjDesc->dwDrawAspect = 0;
pObjDesc->dw8tatus = 0;■
pObjDesc->dwFullUserTypeName = 0;
pObjDesc->dwSrcOfCopy = 0;
pObjDesc->sfzel.cx - 0;
pObjDesc->sizel.cy = 0;
pObjOesc->pointl.x = O;
pObjDesc->pointl.y = O;
::GlobalUnlock(hObjDesc);
pSource->Cach eGlobalData(m_cfObjDcsc, hObjDesc); return pSource;
return NULL;
IBOOL CEx28bView::MakeMetafilePict(C01eDataSource* pSource) {
CEx28bDcc* pDoc = GetDocumentO;
'OOleDataObject dataObject;
LPDATAOBJECT pDataObj; // Интерфейс IDateObject OLE-объекта
VERlFY(pDpc->m_lp01eObj->QiJeryInterface(nD_IDataObject. (void**) ApDataObj) == S_OK);.
dataObject.Attach(pDataObj);
FORMATETC fmtem;
SETFORMATETC( fmtem, CF_METAFILEPICT, DVASPECT.CONTENT, NULL, TYMED_MFPICT, -1);
if (!dataObject.IsDataAvailable(CF_METAFILEPICT, &fmtem)) {
TRACE("Формат CF_METAFILEPICT недоступен\п");
return FALSE;
^1 Просто копируем описатель метафайла из OLE-обьекта в объект данных буфера обмена
STGHEDIUM stgmm;
VERIFY(dataObject.GetData(CF_METAFILEPlCT, &stgmm, &fmtem)); pSource->CacheData(CF_METAFILEPICT, &stgmm. &fmten);
return TRUE;
j
Изучите таблицу сообщений и соответствующие обработчики команд самостоятельно. Они сравнительно короткие и в основном вызывают описанные выше OLE-функции. Но несколько внутренних вспомогательных функций необходимо поясненить.
ПРИМЕЧАНИЕ Вы увидите в тексте программы множество вызовов функции Getlnterface. Это функция-член класса CCmdTarget, возвращающая заданный указатель на интерфейс для класса в вашем проекте. Используется в основном, чтобы получить указатель на lOleCHentSite для документа. Эта функция работает быстрее, чем ExtemalQuerylnterface. но не увеличивает счетчик ссылок объекта.
GetSize
Вызывает IQleObject::GetSize, чтобы получить размеры внедренного объекта, которые затем преобразует в размеры для ограничивающего прямоугольника.
SetNames
Обращается к IOleObject::SetHostName^oL передачи компоненту имени контейнерного приложения.
SetVIewAdvIse
Вызывает функцию внедренного объекта MewObject2::SelAdt;isejyv% установки соединения- уведомления от компонента к документу контейнера.
MakeMetafilePict■
Вызывает //Лз/лО^е^л-б^ТЭгЦявнедренного объекта, чтобы получить метафайловую картинку, предназначенную для объекта данных буфера обмена. (Метафайловая картинка — это Windows-структура МЕТАГ11ЕР1СТродерж.ащая указатель на метафайл и информацию о размерах картинки).■■
SaveObject
Работает аналогично функции SaveDib из примера ЕХ25А. Создает объект COleDataSource с тремя форматами: внедренный объект, метафайл и дескриптор объекта.
DoPasteObjectDescriptor
Выбирает дескриптор объекта из буфера обмена, но больше ничего с ним не делает. Эту функцию следует вызывать перед вызовом DoPasteObject.
DoPasteObject
Вызывает OleCreateFromDfUajyui создания внедренного объекта по данным формата «внедренный объект» из буфера обмена.
Класс CEx28bDoc
Этот класс реализует интерфейсы lOleCUentSite и lAdviseSink. В связи с принятым нами упрощением «один внедренный элемент на документ» отпадает необходимость в отдельных объектах-посредниках. Документ и есть посредник. Мы используем стандартные интерфейсные макросы MFC и, как обычно, должны написать функцию (по крайней мере, ее заготовку) для каждого метода интерфейса — даже если она ничего делать не будет.
Приглядитесь к функциям XQleClientSitenSaveObjectXOleClientSite.-OnShoioWinclQWaL XAdvi- seSmk::OnViewCbange(cM. листинг на рис 28-7). Они очень важны. В остальных функциях имеется, по крайней мере, оператор TRACE, так что мы смажем наблюдать, какие функции вызывает обработчик. Обратите также внимание на функции OnNewDocument, OnCloseDocumenlH DeleteContents. Отметьте, как документ управляет временным хранилищем. Переменная-член т_рИ?гпр$1§5иЬдокуменга. содержит указатель на хранилище внедренного объекта, а переменная-член mJpOleObj— указатель на Ю1еОЬ}сс(ддя этого объекта.
EX28BDOC.H
#if !defir.ed(AFX EX28BD0C_H_1EAAB6DF 6011 11D0_848F 00400526305В „INCLUDED ) «define AFX_EX28BDCC_H„1 EAAB6DF_6011_11DO_848F_00400526305B_INCLUDED_“
#if _MSC_VER > 1000
flpragrna once1
ftendif //_MSC_VEH > 1000
void ITrace (REFIID iid, const char* str);1
class CEx28bDoc : public CDocument'
{ protected: // создание только сериализацией
CEx28bDoc();
DECLARE_DYNCREATE(CEx28bl)oc)
BEGINJNTERFACE_PART(O1 eClientSite, lOleCUentSite)
STDMETHOD(SaveObject)();■
STDMETHOD(GetMoniker)(DWORD, DWORD, LPMONIKER*);
S7DMETH0D(GcTContalner)(LP0LEC0NTAINER»);
STDMETIIOD(ShowObiect)0;'
STDMETHOD(OnShowWindow)(BOOL):
Рис. 28-7. Л истин г класса CEx28bDoc контейнера
STDMETHOD(RequestNewObjectLayout)();
END_INTERFACE_PART(OleClientSlte)
BEGIN INTERFACE PART(AdviscSink, lAdviseSink)
STDMETHOD (void,OnDataChange)(LPFORMATETC, LPSTGMEDIUM);
STDMETHOD (void,OnViewChange)(DWORD, LONG);
STDMETHOD (void.OnRename)(LPMONIKER);
STDMETHOD (void,OnSave)();
STDMETHOD_(void,OnClose)();
END_INTERFACE_PART(AdviseSink)
DECLARE_IMTERFACE_MAP()
friend class CEx28bView;
private:
LPOLEOBJECT m_lp01eObj;
LPSTORAGE m_pTempStgRcot;
LPSTORAGE m_pTempStgSub;
BOOL m_bHatch;
static const OLECHAR* s_szSub;
// Переопределенные функции
■ // Переопределенные виртуальные функции, сгенерированные ClassWizard //({AFX_VIRTUAL(CEx28bDoc) public:
virtual BOOL OnNewDocumentO;
virtual void Serialize(CArchive& ar);
virtual void OnCloseDocument();
virtual void DeleteContents();
protected:
virtual BOOL SaveModified();
//}}AFX.VIRTUAL
.// Реализация
public:
virtual -CEx28bDoc();
ftifdef „DEBUG
virtual void AssertValidO const;
virtual void Dump(CDumpContext& de) const;
tfendif
protected:'
// Сгенерированные функции таблицы сообщений protected."
. //{{AFX_MSG(CEx28bDoc)
afx msg void OnEditClearAllO;
//}}AFX MSG
OECLARE_MESSAGE_MAP(>
////////////////У///////////////////////////////////////////////////
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ будет вставлять дополнительные объявления
// непосредственно перед предыдущей строкой.
#endif // ! defined(AFX_EX288D0C_H„1EAAB6DF„6011_11D0_848F_004005263058_INCLUDED_)
Рис. 28-7. (продолжение)
EX28BDOC.CPP
«include ■stdafx.h"
«include "ex28b.h‘'
«include "ex28bDoc.h"
ffifdef .DEBUG
«define new DEBUG.NEW
flundef THIS_FILE
static Char THIS_FILE[] = ..FILE..:•
tfendif
const OLECHAR* CEx28bDoc: :s_szSub = L”sub": // магическая переменная
//////////////////////////////////////////////////////////////////// // CEx28bDoc
lMPLEMENT_DYNCREATE(CEx2flbDoc, CDocument)
BEGIN MESSAGE MAP(CEx28bDoc CDocument)
//{{AFX MSG MAP(CEx28bDoc)
ON_COMMANO(ID EDIT CLEAR ALL, OnEditClearAll)
//}}AFX MSG MAP
END_ME8SAGE_MAP()
BEGINJNTERFACE_HAP(CEx28bDoc, CDocument}
INTERFACE_PART(CEx28bDoc,IlDJ01eClientSite,OleClientSite)
INTEAFACE_PA8T(CEx28bDoc, IID.IAdviseSink, AdviseSink)
END_INTERFACE_MAP()
////////////////////////////////////////////////////////////////////
// реализация lOleClientSite
STDMETHODIMP (ULONG) CEx28bl)oc: :X01eClient$ite::AddRef()
TRACE("CEx23bDoc::X01eClientSite;:AddRe"\n");
MEm0D_PR0L0GUE(CEx28bDoc, OleClientSite)
return pThis->InternalAddflef ();
STDMETHODIMP.(ULONG} CEx28bDoc: rXOleClientSite: :Helease() {
TRACE("CEx28bDoc: :X01eClientSite: :Release\n") ;
METHOD_PROLOGUE(CEx28bDoc, OleClientSite)
return pTnis->InterrialRelease();
STDMETHODIMP CEx28bDoc; :XOleClientSite::QueryInterface( REFITD iid, LPVOID* ppvObj )
{
ITrace(iid, "CEx28bDcc: :XOleClientSite: :QuervInterface’’);
METH0D_PR0L0GUE(CEx28bDoc. OleClientSite) ’ return pThis->InternalOueryInterface(&iid. ppvObj);
)
STDMETHODIMP CEx28bDoc::X01eClientSlte::SaveObject() {
TRACE("CEx28bDoc: :XQleClientSite:: SaveObject\n");
METHOD PR0L0GUE(CEx28bDoc, OleClientSite)
ASSERTJ/ALID(pThis);
UPPERS 1STSTORAGE IpPersistStorage;-
lpThis->m_p01e0bj->0ueryInterface(IID_IPersistStorage,
(void**) ^IpPersistStorage);
ASSERT(IpPersistStorage != NULL);
HRESULT hr = NOERROR;
if (lpPersistStorage->IsDirty(> == NOERROR)
{ "
// NOERROR == S_OK != S_FALSE, следовательно, объект изменен! hr = ; :01eSave(lpPersistStorage, pThis->m pTempStgSub. TRUE); if (hr != NOERROR)
hr = lpPersistStorage->SaveCompleted(NULL);
.// Пометить документ как измененный, если запись успешная pThis->SetModifiedFlag();
1'’
lpPersistStorage->Release();
pThis->UpdateAHViews(NULL);(
return hr;'
STDMETHODIMP CEx28bDoc::XOleClientSite::GetMoniker(
DWORD dwAssign, DWORD dwWhichMoniker, LPMONIKER* ppMoniker) {
TRACE("CEx28bDoc::XOleClientSite::GetMonlker\n”);
return ENOTIMPL;
STDMETHODIMP CEx28bDoc::XOleClientSite::GetContainer( LPOLECONTAINER* ppContainer)
{
TRACE("CEx28bDoc::XOleClientSite::GetContainer\n");
return E_ NOTIMPL;
STDMETHODIMP CEx28bDoc::XOleClientSite: :ShowObject() {
TRACE("CEx28bDoc::XOleClientSite::ShowObject\n");
METHOD PRDL0GUE(CEx28bDoc, OleClientSite)
ASSERT_VALID(pThis);
pThis->UpdateAHViews(NULL);
return NOERROR;
STDMETHODIMP CEx28bDoc::XOleClientSlte::OnShowWindow(BOOL fShow)
TRACE("CEx28bDoc::XOleClientSite::OnShowWindow\n");
METHOD PR0L0GUE(CEx28bDoc, OleClientSite)
ASSERT_VAL10(pThis);
pThis->m Match = fShow; pThis->UpdateAHViews(NULL);
return NOERROR;
1
STDMETHODIMP CEx28bDoc::XOleClientSite::RequestHewObjectLayout()
TRACE('CEx28bDoc::XOleClientSite::RequeBtNewObjectLayout\n"); return E_NOTIMPL;
Рис. 28-7. (продолжение)
ИНН ПШШШШ ПН 11ШПШ1Н1П1ШППНШШ и НИНИН
И реализация lAdviseSink
STDMETHODIMP_(ULONG) CEx28bDoc::XAdviseSinc;AddRef() {
TRACE("'CEx28bDoc: -.XAdviseSink: :AddRef\n");
METH0D_PR0L0GUE(CEx28bDoo, AdviseSink) return pThls->InternalAddRef();
STDMETHODIMP (ULONG) CEx28bDoc::XAdviseSink:;Release() {
TRACE("CEx28bDoc::XAdviseSink::Release\n"):
METH0D_PR0L0GUE(CEx28bDoo, AdviseSink) return pThis->InterpalRelease();
STDMETHODIMP CEx28bDoc;:XAdviseSink::OueryInterfacc(
REFIID lid. LPVOID* ppvObj)
ITrace(iid, "CEx28bDoc:;XAdviseSink: :OueryInterfece");
METHOD_PROLOGUE(CEx28bDoc, AdviseSink)
return pThis->InternalQueryInterface(&iid, ppvObj);
STDMETHODIMP (void) CEx28bDoc:;XAdviseSink: eOnDataChange(
LPFORHATETC IpFormatEtc, LPSTGMEDIUM IpStgMedlum) i
TRACE( "CEx28bDoc::XAdviseSink::OnDataChange\n");
METHOD PROLOGUE(CEx28bDoc, AdviseSink)
ASSERT.VALID(pTfiis):
// используется только совершенными контейнерами. Игнорируем,так как // от нашего контейнера не требуется реализация всего интерфейса.
STDMETHODIMP_(void) CEx28bDoc;:XAdviseSink::OnViewChange( DWORD aspects, LONG /*lindex«/>
{
TRACE(“CEx28bDoc::XAdviseSink::OnViewChange\n“); METHOD PR0L0GUE(CEx28bDac, AdviseSink)
ASSERT_VALID(pThis);
pThis->UpdateAHViews(NULL); // это действительно важно
STDMETHODIMP (void) CEx28bDoc: sXAdviseSink: :0nRenare(
LPMONIKER /*lpMoniker*/>
{
TRACE("CEx28bDoc::XAdviseSink::OnRename\n
// Представляет интерес только для связанного OLE-обьекта.
// Контейнеры это игнорируют.
}
STDMETHODIMP_(void) CEx28bDoc: ;XAdviseSink: :OnSave()
{"
TRACE("CEx28bDoc::XAdviseSink::0nSave\n");
METH0D_PR0L0GUE(CEx28bDoc, AdviseSink)
ASSERT_VALID(pThis);
pThis->UpcatcAHViews(NULL):
i
STDMETflODIMP_(void) CEx?8bDoc: ;XAdviseSink;:0nClose() {
TRACE("CEx28bDoc::XAdviseSink::0nClose\n");
METHOD PROLCGUE(CEx28bDoc, AdviseSink) ASSERT _VALID(pThis);
pThis->UpdateAHVievis(NULL);
//////////////////////////////////////////////////////////////////// // Создание/уничтожение CEx28bDoc
CEx28bDoc:;CEx28bDoc()
{
m.lpOleObj = NULL;"
m_pTempStgRoot = NULL;
ni.pTempStgSub = NULL;
m'bHatch = FALSE;
}
CEx28bDoc::~CEx28bDoc()
{
}
BOOL CEx28bDoc: :OnNewDocument()
{
TRACE("Входим в CEx29bDoc: :Or.NewDocument\n"):
// Создание структурированного хранилища для объекта (m_pTempStgSub).
// Эго временный файл - имя предоставляет OLE.
VER1FY(::StgCreateDdcfile(NULL, STGM.READWRITE|STGM SHARE_EXCLUSIVE |STGM CREATE! STGM_DELETEONRELEASE, 0, &m_pTompStgRoot) == S_CK);
ASSERT(m_pTempStgRoot •= NULL);
VERIFY(m_pTerpStgRoot->CreateStorage(OLESTR(“sub")1
STGM_CREATE|STGM_READWRITE|STGM_SHARE_EXCLUSIVE, 0, 0, &m„pTempStgSub) == S.OK);
ASSERT(m_pfempStgSub != NULL);
(return CDocument: :0nMewDocument();
//////////////////////////////////////////////////////////////////// // Сериализация CEx28bDoc
void CEx28bDoc: :Serialize(CArctiive& ar)
{
// не подключаемся к MFC-сериалиэации.
if (ar.lsStoringO)
{
// ДОДЕЛАТЬ: добавить код сохранения
}'
■else
// ДОДЕЛАТЬ: добавить код загрузки
}
СМ» след. стр.
Рис. 28-7. (продолжение)
1ШШШП1Ш1ШНШН1Ш1ШШПШ1Н111ШН1ШПН111Ш
И Диагностика CEx28bDoc
#ifdef „DEBUG
void CEx28bDoc; :AssertValid() const
CDocument::AssertValid().
void CEx28bDcc: :Dump(CDumpContext4 de) const
CDocument: :Dump(dc);
#endlf //„DEBUG
//////////////////////////////////////////////////////////////////// // Команды CEx28bDoc
void CEx28bDoc: :OnCloseDocument()
' m_pTempStgSub->Release(); // необходимо освободить ДО вызова базового класса m_pTempStgRoot->flelease();
CDocument: :OnCloseDocument();
void CEx28bDoc: :DeleteContents() {
if(m_lpOleObj |= NULL) {
// Если объект исполняется, закрыть его, что освободит наш ICleClientSite m_lp01eObj ->Close(OLECLOSE_NOSAVE);
m_lp01e0bj->Release(); // должно быть последнее освобождение (а иначе...) m_lp01e0bj = NULL;
void CEx26bDoc: :OnEditClearAll()
DeleteContents();
UpdateAllViews(NULL);
SetModifiedFlagO; m_bHatch = FALSE;
BOOL CEx28b0oc::SaveModlfied()
// Устраняем сообщение "сохранить в файл" return TRUE;
void ITrace(REFIID iid, const char* str)
1 OLECHAR* IpszIID;'
: :StringFrqmIID(lid, 41pszIID);
CString strllD = IpszIID;
TRACE("%s - %s\n", (const char*) strllD, (const char*) str);
AfxFreeTaskMem(IpszIID);
Пример ЕХ28С
Вы уже видели внедряемый компонент на базе MFC, который поддерживал активизацию по месту (ЕХ28А). Теперь Вы познакомитесь с заготовкой программы-компонента, активизирующей внедренный объект в отдельном окне. Умеет она немногое, разве что отображает в окне текст и графику, но, изучив ее код, Вы многому научитесь. Приложение изначально сгенерировал AppWizard как SDI-компонент Автоматизации, документ которого является объектом, создаваемым по OLE. После этого интерфейс IDispatch документа был удален и заменен интерфейсами IOleObject,IDataObjectn. IPersistSforage. Код шаблона сервера сохранен, поэтому, когда контейнер запускает этот компонент, создаются объекты «документ», «вид» и «основное окно-рамка».
Откройте и соберите проект ЕХ28С. Запустите приложение один раз для регистрации, а затем попробуйте поработать с ним в паре с ЕХ28В или с каким-нибудь другим контейнером.
Класс CEx28cView.
Этот класс весьма прост. Единственными интересными его функциями-членами являются OnDraw и OnPrepareDG.
void CEx28cView: :OnDraw(CDC* pDC)
<■
CEx28cDoc* pDoc = GetDocumentO;
ASSERT_VALID(pDoc);
pDC->Rectangle(CRect(500, -1000, 1500, -2000));
pDC->TextOut(0, 0, pDoc->m strlext);
}
void CEx28cView: :OnPrepareOC(CDC* pDC, CPrintlnfo* plnfo)
{
pOC->SetMapMode(MM_HIHETAIC):
}'
Класс CEx28cDoc
Данный класс выполняет большую часть работы компонента и слишком велик, чтобы приводить его здесь полностью. На рисунке 28-8 показан заголовочный файл, но за кодом реализации Вам придется обратиться к прилагаемому компакт-диску. Тем не менее, здесь приведены важнейшие функции.
EX28CDOC.H
«if !dcfincd(AFX EX28CD0C_H„1EAA86F5_6011_11D0 848F_00400526305B „INCLUDED )
«define AFX_EX28CD0C_H_1EAAB6F5_6011_11D0_848FJ)0400526305B „INCLUDED. .
«if .MSC.VER > 1000.
«pragma once'•
•flendif // .MSC.VER > 1000'
extern const CLSID clsid; // определен в ex28c.cpp■.
void ITrace(RERID iid, const char* str);
ttdefine SETFOAMATETC(fe, cf, asp, td, med, 11) \,
((fe).cfFormat=cf, \
(fe).dwAspect=asp, \.-
(fc).ptd=td. \.
(fe),tymed=med, \'
(fa).lindex=li)
Рис. 28-8. Листинг заголовочного файла класса CEx28cDoc компонентасм. след. стр.
Рис. 28-8. (продолжение)
class CEx28cDqc : public CCocument {
friend class CEx28cView;
private:
C S tr in gm_strText;
LPOLECLIENTSITE m_lpClientSite;
L POlEADVISEHOLDER m_lp01eAdviseHolder;
LPDATAADVISEHOLDER m_lpDataAdviseHolder;
CString m_SLrContainerApp;
CString m_strContainerObj;
IHGLOBALMakeMetaFileO;
BEGIN INTERFACE PART(01e0bject, TOleObject)
STDMETHOD(SeTClientSite)(LPOLECLIENTSITE);
STDMETHOD(GeLClientSite)(LPOLECLIENTSITE»);
STOHETHOD(SetHostNamcs)(LPCOLESTR. LPCOLESTR);
STDMETHOD(Close)(DWORD):'
STDMETHOD($etMoniker)(OWORD, LPMONIKER);
STDMETHOD(GetMQniker)(DWORD, DWORD, LPMONIKER»);
STDMETHOD(InitFromData)(LPDATAOBJECT, BOOL, DWORD);
STDMETHOD( GeTC1i pboa rdOata)(DWORD, LPDATAOBJ ЕСТ»);
SrDMETHOD(DoVerb)(LONG, LPMSG, LPOLECLIENTSITE, LONG, HWND, LPCRECT);
STDMETHOD(EnuniVerbs)(LPENUMOLEVERB*);
STDMETHOD(Update)();
STDMETHOD(IsUpToDate)();
STDMETHOO(GetUserClassID)(LPCLSID);
STDMETHOD(GetUserType)(DWORD. LPOLESTR»);
STDMETHOD(SetExtent)(DWORD. LPSIZEL);
STDHETHOD(GetExtent)(DWORD. LPSIZEL);‘
STDMETHOD(Advlse)(LPADVISESINK, LPDWORD);
STDMETHOD(Unadvise)(DWORD);
STDMETHOD(EnumAdvise)(LPENUMSTATDATA*);
STDMETHOD(GetMiscSratus)(DWORD, LPDWORD);
STDMETHOD(SetColo rScheme)(LPLOGPALETTE);
END_INTERFACE_PART(OleObject)
BEGIN INTERFACE PART(DataObject. IDataObject) STDMETHOD(GetData)(LPFORMATETC, LPSTGMEDIUM);
STDMETHOD(GetOataHere)(LPFORMATETC. LPSTGMEDIUM);
STDMETHOO(OueryGetData)(LPFORMATETC);
STDMETHOD(GetCanonicalFormatEtc)(LPFORMATETC, LPFORMATETC);
STDMETHOD(SetData)(LPFORMATETO, LPSTGMEDIUM, BOO.);
STDMETHOO(EnumFormatEtc)(DWORD, LPENUMFORMATETC»);
STDMETHOD(DAdvise)(LPFORMATETC, DWORD, LPADVISESINK, LPDWORD);
STDMETHOO(DUnadvise)(DWORD);
STDHETHOD(EnumDAdvise}(LPENUMSTATDATA*);
END_INTERFACE_PART(DataObject)
BEGIN INTERFACE PART(PersistStorage, IPersistSTorage) STDMETHOD(GetClassID)(LPCLSID);
STDMETHOD(lsDirty)();
STDMETHOD( lnitNew)( LPSTORAGE);
STDMETHODt Load) (LPSTORAGE);
STDMETHOD(Save)(LPSTORAGE, BOOL);
STDMETHOD(SaveCompleted)(LPSTORAGE);
STDMETHOD(HandsOffStorage)();
ENDJNTERFACE PART(PerslstStorage)
DECLARE.INIERFACE_MAP()
protected: // создание только сериализацией
CEx28cDoc();
DECLARE, DYNCREATE(CEx28cDoc)
И Переопределенные функции
И Переопределенные виртуальные функции, сгенерированные ClassWizard
//{{AFX_VIRTUAL(CEx28cDoc)
public:
virtual BOOL OnNewDocument();
virtual void Seriallze(CArchive& ar);
virtual void OnFinalReleaso();
virtual void OnCloseDocument():
protected:
virtual BOOL SaveModifiedO;
//}}AFX_VIRTUAL
// Реализация
public:.
virtual' ~CEx28cDoc();
ttifdef_DE8UG
virtual void AssertValid() const:
virtual void Dump(CDumpContext& de) const;
flendif
// Сгенерированные функции таблицы сообщений
public:
//{{AFX_MSG(CEx28d)oc)
afxjnsg void OnModifyQ;
afx_msg void OnFileUpdateO;
afx msg void OnllpdateFileUpdate(CCrdUI* pCmdUI);
//}}AFX_M8G
DECLARE _MESSAGE_MAP() ^//////////////////////////W^^
//{{AFX_IMSERTJ.OCATION}}
// Microsoft Visual C++ будет вставлять дополнительные
// объявления непосредственно перед предыдущей строкой
tfendif // !defined(AFX_EX28CD0C_H_JEAAB6F5,6011_11DD_848F_00400526305B ^INCLUDED,)
Вот список важных функций интерфейса из ex27cDoc,cpp:
XOleObject::SetClientSire
XOleObject::DoVerb
XOleObjест::Advise,
XDataObject::GetData
XDataObject::OueryGetData
XDataObject::OAdvise
XPersistStorage::GetClassID
XPersistStorage:ilnitNew
XPersistStorage;:Load
XPersistStorage:.Savo
Вы уже видели код контейнера, который рисует метафайл. Здесь представлен код компонента, создающий метафайл. Когда обработчику объекта нужен метафайл, он вызывает функцию компонента lDataObject;:GetData Данная. реализация этой функции вызывает вспомогательную функцию MakeMetaFtie, создающую метафайловую картинку. Сравните этот код рисования с кодом рисования из Ex28cVieiV;:OnDr«№
STDMETHODIMP CEx28cDoc::XDataObject;:GetData(
LPFORMATETC IpFornatEtc, LPSTGMEOIUM IpStgMedium)
TRACE("CEx28cDoc;;XDataObject::GetData - %d\n", lpFormatEtc->cfFormat); METH0D_PR0L0GUE(CEx28cDoc, DataObject)
ASSERT_VALID(pThis);
if (lpFormatEtc->cfFormat != CF.METAFILEPICT) { return S.FALSE;
HGLOBAL hPict = pThis->MakeMetaFile();
lpStgMedium->tymed = TYMED.MFPICT;
lpStgMedljm->hHetaFilePict = hPict;
lpStgMedium->pUnkForRelease = NULL;
return S_0K;
HGLOBAL CEx28cDoc;:MakeMetaFile {
HGLOBAL hPict;
CMetaFileDC dem;
VERlFY(dcm.Create());
CSize size(5000, 5000); // начальный размер объекта в Excel & Word
dem.SetMapMode(MM_ANISOTROPIC):
dem.SetWi ndowO rg(0,0);
dem.SetWindowExt(si ze.ex, -size.cy);
// код отрисовки
dcm.Rectangle(CRect(500, -1000, 1500, -2000));
CFont font;
font.CreateFont(-500, 0, 0, 0, 400, FALSE, FALSE, 0, ANSI.CHARSET, OUT DEFAULT PRECIS,
CLIP.DEFAULT.PRECIS, DEFAULT.QUALITY, DEFAULT .PITCH FF.SWISS, ’'Arial");
CFont- pFont =■ dcm.Select0bject(8font);
dem.Text0ut(0, 0, m_str~ext);
dem.SelectObject(pFont);
HHETAFILE hMF = dem. Close();
ASSERT{hMF ! = NULL);
hPict - ;;GlobalAlloc(GHEM SHARE|GMEM MOVEABLE, sizeof(METAFILEPICT));
ASSERT(hPict != NULL);
LPMETAFILEPICT IpPict;'
IpPict = (LPMETAFILEPICT) : :GlobalLock(hPict):
ASSERT(IpPict I= NULL);
lpPict->mm = MM ANISOTROPIC;
lpPict->hMF = hMF;
lpPict->xExt - size.ex;
lpPict->yExt = size.cy; // высота в HIMETRIC
::GlobalUnlock(hPict);
return hPict;
Функции XOleObject^AdviseH XDataObject-DAdviseBecbMa. схожи. Обе вызывают глобальные функции OLE для создания объектов-уведомителей, способных управлять несколькими получателями уведомлений (advise sinks). (В этой программе на каждый объект-уведомитель приходится только один получатель.) Приводимая далее функция XOleObject::Adviseсоздает объект-уведомитель с интерфейсом KXeAdviseHolder. Другие функции документа вызывают функции lOleAdviseHolder. .SendOnClose и lOleAdviseHolder::SendOnSave,которые в свою очередь вызывают для каждого присоединенного получателя уведомлений IAdviseSinh;:OnCloseHlAdvi- seSink-OnSave.
STDMETHODIMP CEx28cDoc::XCleObject::Advlse(
JAdviseSink* pAdvSink, DWORD" pdwConnection) {
TRACE("CEx28cDoc::X01e0bject::Advise\n’);
METHOD PR0L0GUE(CEx28cDoc. OleObjecT):
ASSERTVALID(pThis);
♦pdwConnection = 0;
if (pThis->m IpOleAdviseHalder == NULL && : :Create01eAdviseHolder(&pThis->m_lp01eAdviseHolder)
!= NOERROR) {
return E_OUTOFMEMORY;
AsSERT(pThis->m_lp01eAdviseHolder != NULL);
return pThis->m lp01eAdvlseHolder->Advlse(pAdvSlnk, pdwConnection);
}
Каркас приложений вызывает функцию OnModijy, когда пользователь выбирает из меню EX28C-MAIN команду Modify. Он вводит в диалоговом окне строку, и функция посылает уведомление OnDataCbange получателю уведомлений в обработчике объекта. (Уведомления продемонстрированы на рисунке 28-5.) Вог как выглядит функция OnModijy:
void CEx28cDoc: rOnModifyQ
{
CTextDialog dig;
dlg.m strText = m strlext;
if (dlg.DoModalO == ID0K){
m_strText = dlg.m_strText;
UpdateAllViews(NULL); // перерисовать окно представления
// Уведомить клиента об изменении метафайла.
// Клиент должен вызвать IViewObject: :SetAdvise.
LPDATAOBJECT IpDataObject = (LPDATAOBJECT) Getlnterface(&IID_IDataObject);
HRESULT hr = m_lpDataAdviseHolder->SendOnDataChange(IpDataObject, 0, NULL);
ASSERT (hr = NOERROR);
SetModifiedFlagO; // без этого обновление не состоится
При выборе команды Update из меню File вызывается функция OnFiieUpdate.OHa. обращается к IOleClientSite;:SaveObject,4to в свою очередь заставляет контейнер сохранить метафайл и внутренние данные объекта в хранилище. Функция посылает также уведомление OnSeve получателю в клиенте. Вот кед OnFUeUpdate:
void CEx28cDoc: :OnFileUpdate() {
if (m_lpClientSite== NULL) return;
VERIFY(m_lpClientSite->SaveObject() == NOERROR);
if (m.lpOleAdviseHolder != NULL)
m Ip01eAdviseHolder->Send0nSave();
SetModifledFlag(FALSE);'
ГЛАВА 29
Основы ATL
В этой главе мы рассмотрим второй каркас приложений (MFC пусть будет первым), включенный в Microsoft Visu al C++, — Active Template Library (ATL). Для начала вспомним COM, затем рассмотрим альтернативный способ написать объект CSpacesbipwi главы 24, показывая тем самым, что есть разные методы написания класса СОМ. (Это будет важно при рассмотрении методов композиции классов ATL) Далее мы изучим ATL, сфокусировав свое внимание сначала на шаблонах и обычных smart-указателях C++ и их применении в разработке СОМ объектов. Потом мы рассмотрим программирование на ATL со стороны клиента и некоторые из smart-указателей ATL. Наконец, мы придем к программированию серверов на ATL, заново реализовав с ее помощью пример космического корабля из главы 24, что даст нам представление об архитектуре ATL♦
Снова СОМ
Наиболее важная концепция, на которой основана СОМ, — это интерфейсы, Как Вы узнали из главы 24, ни СОМ, ни какая-либо поддержка периода выполнения не нужны для программирования при помощи интерфейсов. Все, что Вам нужно, это дисциплина.
Вернемся к примеру космического корабля из главы 24. Мы начали с одного класса CSpace- sbip, реализующего несколько функций. Опытные разработчики на C++ обычно садятся за компьютер и начинают писать класс примерно так:
class CSpaceship {
void Fly();
int& GetPositionO;.
Однако при разработке на основе интерфейсов процедура немного отличается. Вместо написания класса напрямую интерфейс сначала обговаривается, а потом реализуется. В главе 24 функции Fly и GetPositton были переведены в абстрактный базовый класс Motion.
struct IMotion {
virtual void Fly() = 0;
virtual int& GetPositionO = 0;
};-
Затем класс CSpaceship был объявлен производным от интерфейса Motion;
class CSpaceship : IMotion {
void Fly();
irt&GetPositionO;
Обратите внимание: интерфейс движения (motion) отделен от своей реализации. При разработке интерфейсов Вы можете изменять их, стремясь сделать полными, но, в то же время, не слишком раздутыми. Но как только интерфейс опубликован (т е. другие разработчики начали его использовать), он замораживается и никогда не может быть изменен.
Такое небольшое различие между программированием на основе классов и на основе интерфейсов создает некоторые проблемы. Однако это — един из ключевых моментов для понимания СОМ. Собрав вместе функцииДу и GetPosition, Вы разработали двоичную структуру. Если есть предварительно определенный интерфейс, то общение с классом через интерфейс позволяет клиенту получить потенциально не зависящий от языка способ общения с классом.
Сведение функций вместе в интерфейс само по себе очень полезно. Допустим, Вы хотите описать что-нибудь, отличающееся от космического корабля, например, самолет. Ясно, что у него также должны быть функции Fly и GetPosition. Программирование на основе интерфейсов предоставляет более широкую форму полиморфизма — полиморфизм на уровне интерфейсов, а ее на уровне одной функции.
Разработка на основе интерфейсов базируется на отделении интерфейса от реализации, СОМ концентрируется на программировании интерфейсов и заставляетразделять интерфейс и реализацию. Единственный способ общения клиента с объектам, согласно СОМ, — интерфейс. Однако сведения функций вместе в интерфейс недостаточно. Нужен еще один ингредиент — механизм для выяснения функциональности во времявыполнения.
Базовый интерфейс (Unknown.
Основное правило, отличающее СОМ от обычного программирования на основе интерфейсов, следующее: первые три функции каждого интерфейса СОМ должны быть одинаковыми: Базовый интерфейс СОМ — IUnknown — выглядит таге
struct IUnknown {
virtual hRESULT QueryInterface(REFIID rile, void*» ppv) = 0;
virtual ULONS MdRefQ = 0;
virtual ULONG ReleaseO = 0;
Каждый интерфейс COM является производным от этого интерфейса (что делает QueryIn terface, AddRef и Release первыми тремя функциями каждого интерфейса). Для преобразования IMotionB интерфейс СОМ сделайте его производным от IUnknown:
struct IMotion : IUnknown {
void Fly();■
IntS, Ge'.Position();
ПРИМЕЧАНИЕ Если Вы хотите, чтобы интерфейс работал за пределами процесса, сделайте у каждой функции возвращаемое значение типа HRESULT. Мы рассмотрим это при изучении IDL (Interface Definition Language) далее в этой главе.
AddRef «Release требуют некоторого внимания, так как являются частью IUnknown. Они позволяют объекту контролировать свое время жизни. Как правило, клиенты трактуют указатели на интерфейсы как ресурсы; клиенты запрашивают интерфейсы, используют их и освобождают, когда они более не нужны. Объекты узнают о новых ссылках на себя через AddRef, а об освобождениях — через Release. Объекты часто используют эту информацию для управления временем своей жизни. Например, многие объекты удаляют себя в тот момент, когда их счетчик ссылок станет равен нулю.
Вог как код клиента маг бы использовать космический корабль:
void UseSoacestipO {,
IMotion* pMotlon = NULL;
20 Зак 7R7
pMotion = GetASpaceship(); // Это функция из гипотетического Spaceship API. Она, скорее всею, // является точкой входа в некоторую DLL. Возвращает IMotion* // и выполняет неявный вызов AddRef., .
if (pMotion) {.
pMotion->Fly();
int i = pMotion->GetPosition();
pMotion->Release(): // закончили с этим акэемглярон CSpaceship
}'
)
Последняя (и самая важная) функция IUnknown — это его первая функция Queryinterface. Она является механизмом выяснения функциональности во время выполнения. Если Вы получили указатель на интерфейс и не хотите использовать именно его, то Вы мажете при помощи указателя запросить у объекта другой интерфейс. Данный механизм, а также тот факт, что интерфейсы остаются неизменными с момента публикации, позволяют программному обеспечению на основе СОМ безопасно эволюционировать во времени. В результате, Вы можете добавлять функциональность в ПО без переделки старых версий клиентов, использующих это ПО. Кроме того, у клиентов есть широко известные средства получения этой функциональности, если они о ней знают. Например, Вы добавили функциональность в CSpace- sbip, реализовав новый интерфейс IVisual. Такой интерфейс имеет смысл, потому что у Вас есть объект, находящийся в трехмерном пространстве и двигающийся к и от наблюдателя. Кроме того, у Вас может быть невидимый объект (черная дыра, например). IVisual может выглядеть так:
struct IVisual : IUnknown {
virtual void DisplayO = 0;
Клиент может использовать IVisual следующим образам:
void UseSpaceship() {.
IMotion* pMotion = NULL;
pMotion = GetASpaceship(); // неявный вызов AddRef if(pMotion) {
pMotion->Fly();
int i = pNotion->GetPosition();
IVisual* pVisual = NULL;
pMotion->QueryInterface(IIO_IVisual, (void**) ApVisual);■
// неявный вызов AddRef внутри Query In terface
if(pVisual) {
pVisual->Display(); // теперь видимый
pVisual->Release(); // закончить с этим интерфейсом
}
pMotion->Release(); // закончить с этим экземпляром IMotion
Обратите внимание, что в этом коде указатели на интерфейсы используются очень осторожна они применяются, только если интерфейс был получен правильно, и освобождаются, когда интерфейсы более не нужны. Это обычное низкоуровневое СОМ-программирование: Вы запрашиваете указатель на интерфейс, Вы используете указатель на интерфейс, Вы освобождаете его, когда ан боже не нужен..
Написание кода'.
Как Вы видели, написание кода клиента СОМ не сильно отличается от написания обычного кода C++. Однако классы C++, с которыми имеет дело клиент, являются базовыми абстракт-
НЫМИ классами. Вместо вызова operator new, как это было в C++, Вы создаете объекты СОМ и запрашиваете интерфейсы СОМ путем явного вызова нескольких функций API; а вместо удаления объекта Вы просто следуете правилу СОМ о балансе вызовов AddRefit Release.
Что требуется дня создания класса СОМ? Вы видели в главе 24, как сделать такой класс при помощи MFC Здесь приводится другой пример реализации CSpaceship как класса СОМ, в котором используется множественное наследование. Класс C++ наследует несколько интерфейсов и реализует все их функции (включая IUnknown, естественно).
struct CSpaceship : IMotion. IDisplay {
ULONG m_cRef;
Int m_nPosition;
CSpaceshipO : n_cRef(0), m nPosition(O) {•
HRESULT Querylnterface(REFIID riid, void** ppv);
ULONG AddRe.f<> {
return Interlockedlncrement(&ni cRef);
ilONG ReleaseO {
ULONG cRef = InterlockedIncremenT(&m_cRef);
if (cRef — 0){
delete this;
return 0;
else,
return m cRef;.
}'
// функции IMotion!
void Fly() <
// Сделайте здесь, все что нужно для полета
int GetPositionO { return m nPosition;
}
// функции IVisual:
void DisplayO { // показать }
Классы COM на основе множественного наследования
Если Вы привыкли к обычному коду C++, то предыдущий код может показаться Вам слегка странным. Это не совсем обычная форма множественного наследования, называемая -наследование интерфейса (interface inheritance). Большинство разработчиков на C++ применяют насяедованиереализации (implementation inheritance), при котором производный класс наследует от базового вое, в т. ч. его реализацию, Наследование интерфейса просто означает, что производный класс наследует интерфейсы базового класса. Код, приведенный выше, добавляет в класс CSpaceship две переменные-члепа — указатели на каждою невидимую viable.
При использовании множественного наследования для реализации интерфейсов каждый из них совместно использует реализацию IUnknown в CSpaceship. Это иллюстрирует другое тайное, но важное понятие, известное как идентичность в смысле COM (COM identity). Основная идея идентичности в СОМ состоит в том, что IUnknown*» СОМ — это то же, что void*
в C++. IUnknown — это единственный интерфейс, который гарантированно есть в любом объекте, и указатель на него всегда можно получить. Клиент может вызвать Queryinterface через интерфейс IMotion объекта С$расе$Ырдля получения интерфейса IVisibte. И наоборот, клиент может вызвать Querylnteifaceiepes интерфейс IVisibte объекта CSpacesbip для получения интерфейса Motion. Наконец, клиент может вызвать Queryinterface через интерфейс IUnknown для получения Motion или IVisibte или через Motion или IVisibte для получения IUnknown. Дополнительную информацию об идентичности в смысле СОМ см. в книге Дона бокса (Don Box) Essential COM (Addison-Wesley, 1997) или в книге Дейла Роджерсона (Dale Itogerson) Inside COM V Microsoft Press, 1997) (Д Роджерсон. Основы COM, 2-е издание. M: Русская Редакция, 2000).
Часто можно видеть классы СОМ в виде диаграмм, изображающих интерфейсы, реализованные ими Пример такой диаграммы приведен в главе 24 в разделе «Интерфейс IUnknown и функция-член Queryinterface».•
Множественное наследование при реализации CSpacesbip автоматически удовлетворяет фавилам идентичности в СОМ. Обратите внимание, что все вызовы QueryInteifactAddRe/u Release передаются в одни и те же функции-члены С++-класса независимо от интерфейса, через который они вызваны.
Вот более или менее вся сущность СОМ. Ваша работа как разработчика состоит в создании полезных сервисов и предоставлении их клиенту через COM-интерфейсы. На самом низком уровне это означает наличие нескольких таблиц функций, удовлетворяющих правилам СОМ. К настоящему времени Вы видели два способа реализации этого. (В главе 24 показано, как это сделать при помощи вложенных классов и MFC В данной главе мы только что написали С ОМ-класс при помощи множественного наследования.) Однако в СОМ есть еще несколько важных вопросов помимо программирования интерфейсов и написания классов для их реализации.
Инфраструктура СОМ
Хотя концепция программирования на основе интерфейсов Вам известна, есть довольно много деталей, которые нужно реализовать, чтобы ваш класс стал частью системы. Эти детали часто затемняют фундаментальную красоту СОМ
Для начала классам СОМ нужно место для жизни, поэтому Вы должны поместить их в EXE или DLL. Кроме того, каждому классу СОМ нужен соответствующий объект класса, часто называемый фабрикой класса (class factory). Способ обращения к объекту класса СОМ-сервера зависит от размещения самого класса СОМ (в DLL или EXE). Необходимо также учесть время жизни сервера. Сервер должен оставаться в памяти, пока он нужен, и "удаляться, как только в нем больше нет необходимости, Для решения этой задачи серверы содержат глобальные счет* ики блокировок, соответствующие количеству объектов, имеющих указатели на интерфейсы. Наконец, правильные серверы добавляют в Реестр Windows параметры, облегчающие за- гуск клиентов.''
Вы затратили много времени, изучая MFC при чтении этой книги. Как Вы узнали в главе 24, MFC заботится о большинстве деталей программирования на основе СОМ. Например, в ССтсГГагре1есть реализация /Unknown. MFC даже создает С++-классы и макросы, реализующие объекты класса (например, COteObjectFaGtOTy,COtelbmplateServer,DECI^RE_QLE_CREATE и IMPLEMENT_OLE_CREATEXoTopne помещают в Реестр большинство необходимых элементов. В MFC есть самая простая в использовании версия IDispatcb^noaTOMy все, что Вам нужно — эго объект CCmdTarget и ClassWizard, Если Вам необходима реализация OLE-документов или ActiveX-документов, то MFC предоставляет стандартные реализации протоколов OLE и ActiveX Document. Наконец, MFC остается простейшей технологией для написания быстрых и мощных элементов управления ActiveX. (Ebe можно писать и на Microsoft Visual Basic, но в нем нет достаточной гибкости.)
Это были достоинства. Однако применение MFC имеет и недостатки. Во-первых, для использования всех этих возможностей необходимо знать MFC на 100%. Во-вторых, решившись на применение MFC, надо представлять себе цену этого решения. MFC огромна. Она должна быть такой, поскольку является каркасом приложений на C++ с большим количеством возможностей,
Новый каркас
Как Вы могли видеть из ранее рассмотренных примеров, реализация COM-классов и предоставление клиентам доступа к ним требуют писать много кода — кода, который не изменяется от реализации одного класса к реализации другого. Например, реализация/У^бпом^обыч- но одна и та же для любого СОМ-класса — основное различие между ними в интерфейсах, предоставляемых каждым классам.
Но также, как Вам не нужно в наше время понимать ассемблер для обеспечения работоспособности ПО, так нет необходимости разбираться во всех нюансах IUnknown и соотношении СОМ и C++ для создания ПО на основе СОМ. Вы не совсем вышли на такой уровень, но ATL от Microsoft — эго первый большой шаг в нужном направлении. (Однако ATL не освобождает Вас от изучения важнейших концепций СОМ, включая подразделения и удаленное выполнение.)
Прежде, чем «нырнуть» в глубины ATL, давайте бросим беглый взгляд на общую картину, частью которой являются СОМ и ATL
ActiveX, OLE и СОМ
СОМ — эго всего лишь «арматура» для нескольких высокоуровневых технологий, таких как элементы управления ActiveX и OLE-документы. Например, чтобы COM-объект можно было квалифицировать как OLE-документ, он должен поддерживать, как минимум, три интерфейса — [Persists tarage,lOleObjectn DDataObjeci.MoiKHO реализовать высокоуровневые возможности OLE-документов и элементов управления самостоятельно, однако более разумно предоставить эту работу какому-нибудь каркасу приложений, в первую очередь MFC.
ActiveX, MFC и COM
Хотя СОМ-«арматура» интересна сама по себе (например, просто изумительно наблюдать, как работает распределенная СОМ), именно высокоуровневые возможности создают продаваемые приложения. MFC — это огромный каркас, предназначенный для создания целых Windows-приложений. Внутри MFC Вы найд ете «тонны» полезных классов, механизм управления- представления данных (архитектура «документ-вид»), поддержку OLE-документов, технологии «drag-and-drop, Автоматизации и ActiveX-элементов. Вам, скорее всего, не захочется «с нуля» разрабатывать приложение OLE-документа—лучше использовать MFC. Однако если необходимо создать небольшой или средних размеров сервис на основе СОМ, то Вы вряд ли захотите включать в него весь «багаж», который MFC применяет для поддержки высокоуровневых возможностей.
Для создания компонентов СОМ можно использовать обычный код на C++, но в этом случае большая часть времени уйдет на написание стереотипного кода (например, для интерфейса IUnknown и объектов класса), Применение MFC для написания приложений на основе СОМ — наименее болезненный способ добавить крупные элементы в приложение, но на MFC тяжело писать простые классы СОМ АТЬпозиционируется между чистым C++и MFC как способ реализовать ПО на основе СОМ без ввода стереотипного кода или изучения всей архитектуры MFC. ATL представляет собой набор шаблонов C++ и других средств, облегчающих написаниеклассов СОМ.
Путеводитель по ATL
Если Вы посмотрите на исходный код библиотеки ATL, то увидите, что вся она содержится в нескольких файлах C++, большинство из которых находятся в подкаталоге ATL\Include. Далее приведено описание некоторых файлов ATL и того, что находится в них.
• ATLBASE.H. Б этом файле находятся:
т объявления функций ATL;
m определения структур и макросов;
m описания smart-указателей, управляющих указателями на интерфейсы СОМ;
* классы поддержки синхронизации потоков;
m определения классом: CComBSTR, Сбогп Variant, поддержки потоков и подразделений.
• ATLCOM.il. В этом файле находятся:
т шаблоны классов для поддержки фабрик классов (объектов класса);
■ реализации lunknown-,
т поддержка для обособленных (tear-off) интерфейсов;
т поддержка получения информации о типе;
тп реализация IDispatch;
тп шаблоны классов-перечислителей;
m поддержка точек соед инения.
• ATLGONV.CPP и ATLCONV.H. В этих файлах содержится поддержка преобразования Unicode-строк в ANSI и обратно.
■ ATLCTLCPP и ATLCTL.H. В этих двух файлах содержатся:
тп исходный код ATL для поддержки IDispatch со стороны клиента и поддержки генерации событий;
m ComControlBase-,.
тп поддержка механизма внедрения объектов;
тп поддержка страниц свойств.
ATLJFACE.IDL и ATLIFACE.H. ATLIFACE.IDL (и генерируемый из него ATLIFACE.H) включает специальный АТЬ-интерфейс/Ле^ггаг.
ATLIMPL.CPP, Реализует некоторые классы, напримерССотВ5ТЯ, объявленный в ATLBASE.H.
I* ATLWIN.CPP hATLWIN.H. Эти файлы предоставляют под держку для работы с окнами и пользовательским интерфейсом, включая:
тп механизм таблиц сообщений;
к оконный класс;
я диалоговые окна.
STATREG.CPP hSTATREG.H. ATL предоставляетСОМ-компонент Registrar, который производит регистрацию в Реестре. Код реализации находится в STATREG.H и STATREG.CPP.
А теперь начнем нашу «экскурсию» по ATL с изучения поддержки, которую ATL предоставляет для разработки СОМ-клиентов.
Программирование клиента с помощью ATL •
Есть две стороны ATL клиентская и серверная. Большая часть поддержки программирования находится на стороне сервера, так как ATL содержит весь код, необходимый для реализации
элементов управления ActiveX. Однако поддержка для клиентов, предоставляемая ATL, также весьма интересна и полезна, Давайте рассмотрим клиентскую сторону ATL Поскольку шаблоны C++ являются краеугольным камнем ATL, мы сначала рассмотрим их.
Шаблоны С++
Несмотря на пугающий синтаксис концепция шаблонов очень проста. Иногда их называют макросами компилятора, что является достаточно точным описанием. Вспомните: когда препроцессор встречает макрос, ои подставляет его содержимое в С++-код. Недостаток макросов состоит в том, что оии зачастую содержат ошибки и в них полностью отсутствует проверка типов. Если Вы передадите в макрос некорректный параметр, то компилятор не «заметит» этого, но ваша программа может очень легко «сломаться». Шаблоны похожи на макросы с проверкой типов. Когда компилятор встречает шаблон, ои подставляет его содержимое подобно макросу, однако производит при этом проверку типов. Тем самым пользователь избавлен от многих проблем.
Применение шаблонов для повторного использования кода отличается от всего, что Вы делали в обычной разработке на C++. Компоненты, написанные на основе шаблонов, повторно используют код путем подстановки параметров шаблона, а не наследуя функциональность базовых классов. Весь стереотипный код из шаблонов полностью вставляется в проект,
Типичным примером применения шаблона является динамический массив. Представьте, что Вам нужен массив для хранения целых чисел. Вместо того, чтобы объявить массив фиксированного размера, Вы хотите увеличивать его по мере необходимости. Вы создаете массив в виде С++-югасса, Затем ваш коллега говорит, что ему нужен такой же массив, но для чисел с плавающей запятой. Вместо того, чтобы создать тот же самый код, но с другим типом данных, Вы можете применить шаблон C++.
Пример решения проблемы — динамический массив, реализованный как шаблон:
template <class Т> class DynArray { publics
DynArrayO;■
~DynArray(); //очистка int Add(T Element); // добавляет элемент void Remove(int nlndex) // удаляет элемент T GetAt(nlndex) const; int tietSize();
private;
T* TArray;
int m nArraysize;
1;
void UseDynArrayO {
DynArray<int> intArray;
DynArray<float> floatArray;1
intArray.Add(4);
floatArray. Add(5.0):
intArray.Remove(O):
floatArray.Remove(0);
int x = intArray.GetAt(O);
float f = floatArray.GetAt(O);
}
Ясно, что создание шаблонов полезно для реализации стереотипного кода СОМ, и ATL использует их именно так. Приведенный пример — это только одно из многих применений шаблонов. Однако шаблоны позволяют не только указать информацию о типе структур данных, но и удобны для инкапсуляции алгоритмов. Вы это увидите, когда ближе познакомитесь с ATL Изучение ATL начнем со smart-указателей.
Smart-указатели
' )дно из типовых применений шаблонов — smart-указатели («интеллектуальные» указатели). И традиционной литературе по C++ обычные указатели называют «немыми» (dumb). Это не очень ласковое прозвище, но обычные указатели не делают ничего, кроме указания на что'. о. Клиент часто выполняет такие детали, как инициализация указателя.
В качестве примера давайте смоделируем с помощью С-ы-классов два разработчика ПО. Начнем с создания классов CVBDeveloper и CCPPDEveloper.
.class CVBDeveloper {
public:
CVBDeveloper() {
~CVBDeveloper() {
AfxMessageBox("fl использую VB, поэтому ухожу домой пораньше.");
virtual void DoTheWork() {
AfxMessageBox( Пишу формы");
) };
class CCPPDeveloper {
pjblic:
CCPPOeveloper() {
I
~CCPPDoveloper() {
AfxMessageBox( “Остался на работе и решаю проблемы с указателями");
virtual void DoTheWorkO {
AfxMessageBox("Разбираюсь в C++-коде");
)■ ‘
У обоих разработчиков есть функциидля достижения оптимальной производительности. Теперь представьте себе некий клиент примерно такого вида:
/ 'UseDeveloperg.CPP
void UseDevelopersO {
CVBDeveloper* pVBDeveloper;"
// Указатель на VB Developer нужно где-то инициализировать.
// Но что если Вы забыли пооинициалиаировать и позднее случится нечто подобное:
if(pVBDeveloper) {
// Готовьтесь к худшему, Так как pVBDeveloper НЕ РАВЕН NULL, он указывает
// на какие-то случайные данные.
pVBDevelopeг->DoTheWo гк');
} }
Б данном случае клиент забыл присвоить NULL указателюpVBDeveloper, (Конечно, этого никогда не бывает в реальной жизни!) Так KaxpVBDeveloper содержит ненулевое значение (а значение, которое оказалось на стеке в этот момент), то проверка указателя будет успешной, хотя она должна быть неудачной. Клиент «радостно» продолжит выполнение, веря, что все хорошо. Конечно, программа «рухнет», так как вызов уйдет в «темноту». (Кто знает, на что указывает pVBDeveloper — скорее всего, ни на что похожее на разработчик на Visual Basic) Естественно, Вам понравится механизм, который всегда инициализирует указатели. Вот где окажутся полезны smart-указатели.•
Теперь представьте себе другой сценарий. Вы хотите добавить в классы, описывающие разработчики, немного кода, который выполнял бы общие для всех разработчиков операции, Например, Вы желаете, чтобы все разработчики сначала создали проект, а потом начали кодировать. Посмотрите на вышеприведенные примеры разработчиков на VB и на C++, При вызове РоТйеМЪг&разработчик начнет кодирование без соответствующего проекта, и, вероятно, оставит несчастный клиент в беде. Вам хотелось бы добавить в классы разработчиков универсальную функцию-перехватчик Олобк), чтобы удостовериться в завершении проектирования до начала кодирования. С++-решение этих проблем называется smart-указатель. Давайте посмотрим, что это такое.
Наполнение указателей «интеллектом»
Вспомните, что smart-указатель — это С++-класс-оболочка обычного указателя. Создав класс- оболочку (точнее, шаблон), Вы автоматизируете определенные операции вместо того, чтобы заставлять клиент писать стереотипный код. Одним из примеров такой операции является инициализация указателя для того, чтобы не было случайно «рухнувших* программ. Другой пример — выполнение заданного кода перед вызовом функции с помощью указателя.
Давайте создадим smart-указатель для ранее описанной модели разработчиков. Вот класс- шаблон SmartDevehper;
template<clas$ Т>.
class SmartDeveloper {
Т* m_pDeveloper;
public:
SmartDeveloper(T* pdeveloper) {
ASSERT(pDeveloper !- NULL);
m_pDeveloper = pDeveloper;
J
~SmartOevelDper() {■
AfxMessageBox("fl умный, поэтому получу зарплату.");
SmartDeveloper &
operator-(const SmartDeveloperS rDeveloper) {
return «this;
I* opsrator->() const {
AfxMessage9ox("06 разыменовании указатели. Убедитесь, что все в порядке. "); return m_pOeveloper;
} };
Шаблон SmartDeveloper является оболочкой для указателя — любого указателя. Он может предоставить типовую функциональность независимо от типа указателя, ассоциированного с ним. Относитесь к шаблонам, как макросам для компилятора: объявлениям классов (или функций), код которых применим к данным любого типа.
Мы хотим, чтобы smart-указатель был применим к любым разработчикам, включая использующие VB, Visual C++, Java и Delphi. Это до. гигается использованием в начале класса оператора template <.class Т>. В шаблоне объявлен также указатель (m_pDeveloper) на тип разработчика, для которого определен класс. Конструктор получает указатель этого типа как параметр и присваивает его т_pDeveloper. Обратите внимание, что конструктор генерирует предупреждение, если клиент передает параметр, равный NULL.
SmartDeveloper реализует несколько операторов в дополнение к оболочке указателя. Наиболее важным из них является оператор *->» (оператор выбора функции- или переменной- члена). Этот оператор — «рабочая лошадка» любого класса smart-указателя. Именно переоп-
эеделение оператора выбора превращает обычный класс в smart-указатель. Обычное исполь- ювание этого оператора для «немого» С++-указателя говорит компилятору, что нужно выз- зать метод класса (или структуры), на который ссылается указатель. Переопределив оператор, Вы позволяете перехватить вызов и выполнить стереотипный код при всяком вызове метода. В классе SmanDeudoper «-умный» разработчик проверяет рабочее место перед началом работы. (Этот пример слегка искусственный. В реальной жизни можно, например, выполнить отладочный код)
Добавление оператора •-> в класс заставляет класс вести себя как встроенный С++-указа- тель. Чтобы быть похожим на С++-указатсли во всем остальном, класс smart-указателя должен реализовать другие стандартные операторы — разыменования и присваивания.
Применение smart-указателей
Применение smart-указателей не отличается от применения обычных встроенных С-н-ука- зателей. Начнем с клиента, использующего простые классы разработчиков:
' old UseDevelopers() {
CVB Developer VBDeveloper;
CCPPDeveloper CPPDeveloper;
VBDeveloper.DoTheWork();
CPPDeveloper.DoTheWork();
Никаких сюрпризов — выполнение этого кода заставит разработчики прийти и выполнить работу. Однако Вам нужны «умные» разработчики, т. е. такие, которые убедятся в наличии проекта прежде, чем начать кодирование. Вот код, который создаст вокруг классов разработчиков оболочку из класса smart-указателя:
void VseSmartDevelopers {
CVBDevelope г VBDeveloper;
CCPPDeveloper CPPDeveloper;
SmartDeveloper<CVBDeveloper> sma rtVBDcveloper(&VBDeveloper);
SmartDeveloper<CCPPDeveloper> smartCPPDeveloo=r(&CPPDeveloper);
smaгtVBOeveloper->0oTheWor k();
smartCPPDeve]oper->DoTheWcrk();
/ .
Вместо того, чтобы заставлять старые разработчики выполнить работу (как в предыдущем примере), клиент обращается к «умным* разработчикам, которые автоматически подготовят проект прежде, чем начать кодирование.
£ mart-указатели и СОМ
Хотя последний пример был всего лишь красивой повестью, smart-указатели могут быть полезны и в реальном мире. Одно из таких применений — облегчение программирования СОМ- клиентов.
Smart-указатели часто используют для реализации подсчета ссылок. Перенос подсчета ссылок на стороне клиента в smart-указатели имеет смысл, поскольку это одна из базовых операций СОМ.
Так как Вы уже знакомы с СОМ, то знаете, что COM-объекты предоставляют интерфейсы. Для клиентов, написанных на C++, интерфейсы — это просто чисто абстрактные базовые классы, поэтому интерфейсы часто трактуются как более-менее обычные классы C++. Однако, как Вы знаете из предыдущих частей, COM-объекты слегка отличаются от обычных С++- объектов тем, что существуют на двоичном уровне. Это означает, что они создаются и унич-
тожаются с использованием языково-независимых средств. СОМ-ббъекты создаются через вызовы функций API. Многие COM-объекты применяют подсчет ссылок для определения момента, когда они могут удалить себя из памяти. После создания объекта клиент может обращаться к нему множеством способов через его интерфейсы. Кроме того, с одним объектом могут взаимодействовать несколько клиентов. В этих ситуациях COM-объект должен оставаться в памяти до тех пор, пока на него есть ссылки. Для определения момента самоуничтожения применяется подсчет ссылок.
Для поддержки схемы подсчета ссылок СОМ определяет несколько правил управления COM-интерфейсами со стороны клиента. Первое правило: копирование интерфейса должно приводить к увеличению счетчика ссылок объекта иа единицу. Второе правило: клиент должен освободить указатель иа интерфейс по окончании работы с иим. Подсчет ссылок — это. одна из труднейших для правильной реализации сторон СОМ, особенно со стороны клиента, и поэтому является первым кандидатом иа применение smart-указателей.
Например, конструктор такого указателя мог бы получать в качестве аргумента указатель на реальный интерфейс и сохранять его во внутреннем указателе. Деструктор мог бы вызывать функцию Release для интерфейса, чтобы автоматически освободить интерфейс, когда smart-указатель будет удален или выйдет за пределы видимости.
Кроме того, smart-указатель может поддерживать копирование интерфейсов СОМ. Например, представьте, что Вы создали COM-объект и храните указатель на интерфейс Предположим, Вам нужно скопировать указатель на интерфейс (чтобы вернуть его как выходной параметр). На уровне обычной СОМ необходимо выполнить несколько действий. Сначала надо освободить старый указатель. Затем присвоить старому указателю новый. Наконец, вызвать AddReJдля новой копии указателя. Эти действия нужно выполнить независимо от используемого интерфейса, что делает описанный процесс идеальным кандидатом для стереотипного, кода. Чтобы реализовать копирование в классе smart-указателя, нужно всего лишь переопределить оператор присваивания, после чего клиент может просто присвоить старому указателю новый. Smart-указатель выполнит все операции с указателем на интерфейс, облегчив бремя клиента.
Smart-указатели в ATL
Большая часть поддержки в ATL клиентской стороны COM-программирования находится в двух классах smart-указателей: CComPtr tl CComQIPtr. CComPtr — это базовый smart-указатель, являющийся оболочкой указателя на COM-интерфейс. CComQIPtr чуть более интеллектуальный благодаря сопоставлению GUID (используемого как идентификатор интерфейса) с smart- указателем. Рассмотрим сначала CComPtr.
CComPtr
Перед Вами сокращенная версия CComPtr, в которой приведены его важнейшие части: template <class Т> class CComPtr { public:
typedef T _PtrClass;.
CComPtrO {p=NULL:}
CComPt^T- Ip} {•
if ((P = lp) !=NULL) p->AddRef();
i
см. след. стр.
CComPtr(const CComPtr<T>& Ip) {
if ((p = Ip.p) != NULL) p->AddRef();
~CComPtr() {if (p) p->Release();}
void Release() {if (p) p->Release(); p=WLL;}
operator T*() {return (T*)p;)
T& operator«() {_ASSERTE(p!=NULL); return *p; }
T** operator&() { _ASSERTE(p==HULL); return &d; }
T. operator->() < _ASSERTE(p!=NULL); return p; }
T* operator=(T* lp){return (T«)AtlComPtrAssign((IUnknown*»)&p, Ip);}
T* operator=(const CComPtr<T>4 Ip) {
return (T*)AtlComPtrAssign((IUnknown**)&p, Ip.p);.
i"
, T* p;
CComPtr— эго довольно простой smart-указатель. Обратите внимание на переменную-член р типа Т — типа, определяемого параметрам шаблона. Конструктор выполняетAddRefxna Указателя, в то время как деструктор освобождаетуказатель — никаких сюрпризов. В CComPtr есть также все необходимые операторы для реализации оболочки СОМ-ингерфеЙса. Спсци- 1ЛЫЮГО рассмотрения требует только оператор присваивания, в котором производится пв- реприсвоение произвольного указателя путем вызова функции AtlComPtrAssign-
ATLAPI (IUnknown*) AtlComPtrAssign(IUnknown** рр, IUnknown* Ip) {
if (Ip != NULL)
lp->AddRef();
if (*PP)
(*pp)->Release();
'PP = Ip;
return Ip;
I
Функция выполняет «слепое» присваивание указателя, вызывая Л<И/?#для нового указателя перед вызовом Release для старого. В дальнейшем мы познакомимся с версией этой функции, которая вызывает Queryinterface.
Основное достоинство CComPtr состоит в том, что он в некоторой степени помогает Вам вести подсчет ссылок.
Использование CComPtr
Помимо помощи в вызове AddRefa Release класс CComPtr поможет Вам в создании понятного кода. Следующий пример проиллюстрирует полезность CComPtr. Представьте, что для вы- г олнения определенных действий Вам нужны три указателя на интерфейсы:
void G9tLottaPointers(LPUNKNOWN pUnk){
HRESULThr;
LPPERSIST pPersist;
LPDISPATCH pDispatch;
LPDATAOBJECT pDataObject;
hr = pUnk->QueryInterface{IID IPersist, (LPVOID *)&pPerslst);
if(SUCCEEDED(hr)> {"
hr = pUnk->OueryInterface(IID IDispatch, (LPVOID *)&pDispatch);
If(SUCCEEOED(hr)> {
hr - pUnk->OueryInterface(IID IDataObject, (LPVOID *) SpDataObject);
if(SUCCEEDED(hr)) {
DoIt(pPerslst, pDispatch, pDataObjecr):
pDataOb ject->Reltiaee();
pDispatch->Release(); ,
pPersist->Rclease();'
Вы можете использовать спорный операторного (и получить насмешливые комментарии от своих коллег) для создания более понятного кода:
void GetLottaPointers(LPUNKNOWN pUnk){
HRESULThr;
LPPERSIST pPersist;
LPDISPATCH pDispatch;
LPDATAOBJECT pDataObject;
hr = pUnk->QueryInterface(IID_IPersist. (LPVOID *)SpPersist);
If<FAILED(hr)) goto cleanup;-
hr = pUnk->QueryInterface(IID_IDispatch, (LPVOID *} &pDispatch);
lf(FA.lLF.D(hr)) goto cleanup;
hr = pUnk->OueryInterface(IID_IDataObject, (LPVOID *) SpDataObject);
if(FAILED(hr)) goto cleanup;
Dolt(pPersist, pDispatch, pDataObject):
cleanup;
if (pDataObject) pDataObject-^eleaseO:
if (pDispatch) pDispatch->Release();
if (pPersist) pPersist->Release();
}
Такое решение может показаться Вам не слишком элегантным. Применение CComPtrделает код более красивым и понятным, как показано ниже:
void GetLottaPointers(LPUNKNOWN pl)nk){■
HRESULT hr;
CCcnPtr<IUnkriOwn> persist;
CComPtr<IUnknorfn> dispatch;
CCortPtr<IUnkncwn> dataobject;
hr = pUnK->QueryInterface(IID_IPersist, (LPVOID -)Spersist);
if(FAlLED(hr)) return;,
hr = pUnk->QueryIntcrface(IID_IDispatch1 (LPVOID +) Sdispatch);
if(FAILED(hr)) return;
nr = pUnk->QueryInterface(IID_IOataObject, (LPVOID *) &dataobject);
lf(FAILED(hr))return;
DoIt(pPersist, pDispatch, pDataObject);
// Деструкторы вызовут Release...
}
Сейчас у Вас может возникнуть вопрос: почему CComPtr не использует Query. Interface® конце концов, Queryinterface— эго основное место подсчета ссылок. Однако для добавления поддержки Queryinterface в smart-указатель нужно каким-то образом ассоциировать с ним GUID. Поскольку CCom/Tr существовал еще в первой версии ATL, Microsoft вместо изменения его кода добавила улучшенную версию CComQIPtr.
CComQlPtr
1К>Т часть определения CComQlPtr.
template <class Т, const IID* piid = &uuidof(T)>
class CComQlPtr {
public:
typedef T _PtrClass;
CComOIPtr() {n=NULL;}'
CComQIPtr(T* Ip) {
if ((p = Ip) != NULL)
p->AddRef();
foomQIPtr(const CComQIPtr<T. piid>t Ip) {
if ((p= lp.p) != NULL)
p->AddRef();
}
CComQlPtr(IUnknown* Ip) {
p=NULL;
if (lp != NULL)
lp->QueryInterface(*piid, (void *»)&p);
}
~CCpmQIPtr() (if (p) p->Release();>
void ReleaseQ (if (p) p->Release(); p=NULL;}
operator T*() {return p;)
T& operator*() { ASSERTE(p! =NULL); return *p; )
T** operator^) Г „ASSERTE(p==NULL); return &p; }
T* operater->() {_ASSERTE(p!=NULL); return p; }
T* operator=(T* lp){
return (T»)AtlComPtrAsslgn((IUnknown»*)4p, Ip);
1- operator=(const CComQIPtr<T. piid>& Ip) {
return (T*)AtlComPtrAssign((IUnknpwn**)&p, lp.p);
1* operator=(IUnknown* Ip) {
return (T«)AtlComQIPtrAssign((IUnknown**)&p, Ip, *piid);
bool operator!(){return (p == NULL);).
Г p;
}
Отличие CComQlPtr ar CComPtrзаключается в наличии второго параметра шаблона — piid, описывающего GUID интерфейса. У данного класса smart-указателя есть несколько конструкторов: конструктор по умолчанию, конструктор копии, конструктор с параметром типа указатель на произвольный интерфейс и конструктор с параметром типа указатель HaJUnknown. С братите внимание на последний: если разработчик создает объект и инициализирует его простым указателем на IUnknown, то вызывается Querylnterfacec параметром шаблона типа GU1D. Кроме того, заметьте, что присваивание указателя на IUnknown приводит к вызову AtZComQIPtrAssignДумаем, Вам понятно, что внутри этой функции происходит вызов Query- Interfacec использованием параметра шаблона типа GUID.
Использование CComQlPtr
Перед Вами пример использования CComQlPtr в коде СОМ-клиента:
void GetLottaPointers(ISomeInterface< p8omelnterface){
HRESULT hr;
CComQIPtrcIPersist, &IID_IPersist> persist;
CComQIPtr<IOispatch, &IID_IDispatch> dispatch;
CComPtr<IDataObject. &IID_IDataObject> dataobject;
dispatch = pSorelnterface; // неявный вызов Queryinterface
persist = pSomelnterface; // неявный вызов Queryinterface. .
daraobject = pSomelnterface; // неявный вызов Queryinterface
IDoIt(oersisT, dispatch, dataobject); // передача в функцию которой нужны IPersist*, И IDispatch* и IDataObject»
// Деструкторы вызовут Release...
CComQIPtr полезен, если Вам необходимо преобразование типов в стиле Java или Visual Basic. Обратите внимание, что в приведенном кеде нет вызовов Queryinterface и Release ~ все они выполняются автоматически.
Проблемы применения smart-указателей
Smart-указатели могут быть весьма удобны в некоторых ситуациях (как в примере с CComPtr, что позволило избавиться от операторов goto). К сожалению, smart-указатели не являются панацеей, избавляющей программистов от всех проблем с подсчетом ссылок и управлением указателями. Smart-указатели просто переносят эти проблемы на другой уровень.
Одна из ситуаций, в которой надо соблюдать осторожность при использовании smart-указателей, заключается в конвертации кеда без таких указателей в код, использующий их. Проблема в том, что smart-указатели в ATL не скрывают вызовов AddRefn Release. Эго означает, что Вам следует понимать, как работают smart-указатели, а не следить за тем, как вызываются AddRefvt Release. Например, представьте себе такой код
void UseAnInterface()(
IDispatch* pDispatch - NULL;
HRESULT hr = GetThe0bject(8pt)ispatch):
lf<Sb’CCFEDED<hr>> {■
DWORD dwTICount;
pDispatch->GetTypeInfoCount(8dwTICount);
pDlspatch->Relcase();
}
который был сконвертирован с использованием smart-указателя следующим образом:
void UseAnInterface( J {
CComPtr<IRispatc1> dispatch = NULL;■
HRESULT hr = GetTheObject(&dispatch);.
if(SUCCEEDED(nr)) {
DWORD dwTICount;
dispatch~>GctTypcInfoCount(&dwTICojnt);
dispatch->Release();"
}
}.
Так как CComPtr и CComQIPtr не скрывают вызовы AddRefu Release, то «слепая» конвертация создает проблему при освобождении smart-указателя dispatch. В приведенном коде Release будет вызываться дважды — первый раз явно при вызове dispatch-yReleaseQvt второй неявно при выходе smart-указателя из области видимости.
Кроме того, в smart-указатели в ATL включен неявный оператор приведения типа, позволяющий присваивать их обычным указателям. В этом случае реальный подсчет ссылок ста-
новитсяз апутанным.
Из сказанного можно сделать вывод: хотя smart-указатели облегчают некоторые стороны программирования СОМ-клиснта, сии не являются «защитой от дурака». Для безопасного применения smart-указателей Вам нужно в определенной степени знать, как они работают.
Программирование сервера с помощью ATL
Мы рассмотрели поддержку, которую ATL предоставляет со стороны клиента. Хотя значительная часть ATL относитсяк средствам разработки клиента (например, smart-указатели и BSTR оболочки), основная часть ATL, к изучению которой мы приступаем, предназначена для поддержки создания COM-серверов. Сначала мы дадим обзор ATL, чтобы Вам стало понятна общая картина. Затем мы заново реализуем модель космического корабля, чтобы исследовать ATL Object Wizard и почувствовать, что значит писать COM-классы с использованием ATL.
ATL и классы СОМ
Задача разработчика СОМ-класса — установить связь между таблицами функций и их реализациями и сделать необходимые вызовы QuerylnlerfaceAddReJ и Release. Как это реализовать — дело ваше. Для пользователей неважно, какие способы Вы применяете. Пока мы видели два основных подхода — использование множественного наследования интерфейсов в обычном C++ и применение макросов и вложенных классов в MFC. Подход ATL для реализации COM-классов отличается от этих двух.
Сравните подходы C++ и MFC Вспомните, что разработка COM-классов на C++ включает наследование С++-класса хотя бы от одного СОМ-интерфейса и написание всего кода С++- класса. Далее Вам придется вручную добавлять любые дополнительные возможности (например, поддержку/Р^рщс/тили агрегирования). Подход MFC к написанию COM-классов состоит в использовании макросов, определяющих вложенные классы (по одному на каждый интерфейс). MFC поддерживает IDispatch и агрегирование, поэтому Вам не нужно много кода, чтобы добавить эти возможности. Однако очень трудно вставить в COM-класс новый интерфейс без значительного кодирования. (Как Вы видели в главе 24, поддержка СОМ в MFC основана на нескольких больших макросах.)
Подход ATL к созданию СОМ-класса состоит в наследовании С++-класса от нескольких классов-шаблонов. Однако в этих классах уже есть реализ амия IUnknown.
Рис. 29-1. Выбор ATL COM AppWzard в д иалоговом окне New
Давайте создадим модель космического корабля в виде COM-класса. Как всегда, начнем с выбора New в меню File. Появится диалоговое окно New с активизированной вкладкой Projects, как показано на рис. 29-1. Выберите ATL COM AppWizard. Назовите проект, скажем, spaceshipsvr и щелкните ОК.
Параметры ATL COM AppWizard
На шаге 1 (см. рис. 29-2) Вы можете выбрать тип сервер a. ATL СОМ Арр Wizard предоставляет следующие варианты: DLL, исполняемый файл Executable (EXE) или служба [Service (EXE)]. При выборе ОЬЬможно включить в библиотеку код заместителя/заглушки и использовать MFC в ATL-проекте.
Рис. 29-2. Шаг 1 мастера ATL COM AppWizard
Выбор DLL в качестве типа сервера приводит к созданию всех необходимых элементов, вводящих DLL в среду СОМ. Среди них следующие хорошо известные функции COM: DUGet- ClassObject, DUCanUnloadNow,DllRegisterSeroet8L DllUnregislerServer. Включены также корректные механизмы для установления времени существования DLL
Если хотите, можете запустить DLL вне процесса в виде «суррогата», отметив флажок Allow Merging Of Proxy/Stub Code, который позволяет поместить все компоненты в один двоичный файл. (Обычно кодзаместителя/заглушкираспространяется как отдельная DLL.) Таким образом Вы сможете распространять только одну DLL Если Вам очень необходимо включить MFC в свою DLL установите флажок Support MFC. Это приведет к включению AfxWin.Ии AfxDisp.h в файл StdAfx.hH компоновке проекта с текущей версией библиотеки импорта MFC. Если нужна поддержка для Microsoft Transaction Server, то установите флажок Support MTS.
Если Вы выбрали создание сервера в виде исполняемого файла, то ATL COM AppWizard создаст кед, который компилируется в ЕХЕ-файл. Получаемый файл будет правильно регистрировать объекты классов в операционной системе с помощью CoRegisterClassObjecta CoRe- vokeCJassObject/B проект также включен код для управления временем существования исполняемого сервера. Наконец, если Вы выбрали параметр Service EXE, то мастер ATL COM AppWizard добавляет необходимый код поддержки службы Windows NT.
Применение ATL СОМ Арр Wizard для написания «легковесного* сервера имеет ряд преимуществ. Во-первых, Вы получаете файл проекта для сборки своего объекта. Проект сводит вместе весь исходный код и поддерживает необходимые инструкции для компиляции каждого файла. Во-вторых, Вы получаете готовый IDL-код. Файл IDL важен в качестве начальной точки при проектировании в рамках СОМ. '
Язык определения интерфейсов IDL (Interface Definition Language) — чисто декларативный язык для описания интерфейсов СОМ. Компилятор MIDL (Microsoft IDL) на основе опи- ,:ания COM-интерфейса создает полезные файлы, содержащие:
I* чисто абстрактные базовые классы, необходимые для написанияСОМ-классов;
библиотеку ТИПОВ;
исходный код для создания библиотеки заместителя/заглушки (необходимой для стандартной удаленной СОМ).
Создание СОМ-класса
После создания COM-сервера можно начать добавлять в него COM-классы. К счастью, благодаря мастеру ATL Object Wizard (см. рис. 29-3) это можно сделать очень легко. Для вызова мастера выберите New ATI, Object в меню Insert. .
При генерации нового объекта мастер ATL Object Wizard добавляет в проект файл исход- ioro кода и заголовочный файл на C++, содержащие реализацию и определение нового класса ..^ответственно. Кроме того, он добавляет интерфейс в IDL-файл. Хотя мастер заботится о •.одержимом этого файла, Вам все равно надо до некоторой степени понимать IDL, чтобы гасать эффективные СОМ-инТерфейсы (как Вы вскоре увидите).
Рис. 29-3. Использование мастера ATL Object Wizard д ля вставки в проект СОМ-класса на основе ATL
После выбора типа ATL объекта щелкните Next для вывода диалогового окна ATL Object Wizard Properties. В зависимости от выбранного объекта на вкладке Attributes Вы можете выбрать модель потоков (threading model), а также интерфейс — дуальный или произвольный. Можно также выбрать поддержку агрегирования. Кроме того, мастер позволяет легко включить в класс интерфейс ISupportErrorlnJas. точки соединения (connection points). Наконец, Вы можете агрегировать в свой классааршалерсвободных потоков (free-threadedmar- shaler).
Подразделения и потоки
Для понимания СОМ важно представлять себе, что эта модель построена на абстрагировании (abstraction), т. е. на сокрытии отклиента максимального количества информации. Один из видов информации, которую СОМ скрывает от клиента. — является ли COM-класс потокобезопасным (thread-safe). Клиент должен иметь возможность использовать объект как он есть, не задумываясь о том, правильно ли объект организует доступ к самому себе, т. е. правильно ли он защищает свои внутренние данные. Для описания этого абстрагирования СОМ вводит понятие подразделения (apartment).
Подразделение определяет контекст выполнения (поток), которому принадлежат указатели на интерфейсы. Поток создает подразделение вызывая одну из функций Coinitialize, Colni- tializeExwn/t deinitialize. Далее COM требует, чтобы все вызовы методов на основе указателя на интерфейс выполнялись внутри подразделения, в котором инициализирован этот указатель (другими словами, из того же потока, в котором была вызвана CoCreatelnstanceyB СОМ определены два типа подразделений — однопоточпые и многопоточные. В однопоточном подразделении может быть только один поток, а в многопоточном — несколько. В процессе может быть только одно многопоточное подразделение, ио произвольное число однспоточ- ных. Подразделению может принадлежать любое количество СОМ-объектов.
Однопоточное подразделение гарантирует созданным в ием COM-объектам, что вызовы их методов будут синхронизированы через механизм удаленных вызовов (remoting layer), а многопоточное — нет Разницу между типами подразделений можно пояснить так: создание COM-объекта внутри многопоточного подразделения подобно размещению данных в глобальной области видимости, с возможностью доступа нескольких потоков; а создание внутри однопоточного подразделения — в области видимости одного потока. Из этого следует, что COM-классы, которые будут размещены в многопоточных подразделениях, должны быть потокобезопасными, аСОМ-классы, предпочитающие собственные подразделения, могут не заботиться об одновременном доступе к своим данным.
Для COM-объекта, располагающегося в отдельном от клиента процессе, вызовы методов! синхронизируются автоматически через механизм удаленных вызовов. Однако СОМ-объект внутри DLL может обеспечить собственную пбтокобезопасность (например, при помощи критических секций), вместо того, чтобы полагаться на механизм удаленных вызовов. СОМ- класс сообщает о своей потокобезопасности через параметр Реестра ThreadingModel в подразделе CLSID раздела HKEY_CLASSES_ROOT-.
£HKCR\CLSID\<neKnfl GUID ...}\InprocServer32]
®="C:\SbmeServer.DLL"
TtireadingModel=<MOflenb потоков>
Модель потоков может быть одним из четырех значений: Single, Both, Free илиАраПтеп!, а также может быть не указана. ATL предоставляет поддержку для всех текущих потоковых моделей. Вот описание каждого значения:
I» Single или отсутствие означает, что класс может выполняться только в основном потоке (первом потоке, созданном клиентом);
i* Both означает, что класс является потокобезопасным и может выполняться как в однопоточном, таки в многопоточном подразделении. Данное значение позволяет СОМ использовать для объекта тот же тип подразделения, что и для клиента;
Free означает, что класс является потокобезопасным. Данное значение заставляет СОМ расположить объект внутри многопоточного подразделения;
Apartment означает, что класс не является потокобезопасным и должен быть расположен в собственном однопоточном подразделении.
В зависимости от выбранной потоковой мод ели мастер ATL Object Wizard помещает в класс различный код Например, при выборе модели Apartment класс будет производным от ССот- ObjectRootEx с параметром шаблона CComSingleThreadModel-.
class ATl_NO_VTA8LC CAtlSpaceship :
public cComObjectRaotEx<CCamSingleTbreadModel>1
public CComCoClasscCAtlSpaceshlp, &CLSID_AtlSpaceship>,..
CM. СЛ0О. ctup,
public IDispatchlmpKIAtlSpaceship, &IID_IAtlSpaceship, &LIBID_SPACESHIPSVRLib> {
Параметр шаблона CComSingleTbreadModeinpH^ORwr к более эффективным стандартным операциям инкремента/декремента в реализации IUnknown, поскольку доступ к классу син- ронизируется автоматически. Кроме того, мастер ATL Object Wizard генерирует код класса для добавления в реестр значения потоковой модели. При выборе в мастере модели Single класс также будет использовать CComSingleTbreadModel,но значение параметра ThreadingModel в Реестре останется пустым.
При выборе моделей Both или Free в классе будет применяться параметр шаблона ССот- MuitiTbreadModel,4io приводит к использованию потокобезопасных Win32-фyнкций инкремента/декремента InterlockedlncrementH InterlockedDecrement, Например, определение класса с моделью Free выглядит примерно так:
class ATL NO.VTA3LE CAtlSpaceship :
public CConQbjectRooiExcCComMulti ThreadModeD,
public CConCoClass<CAtlScaceshlp, &CLStD_AtlSpaceship>,
public IDispatchlmpKIAtlSpaceship, &IID lAtlSpaceship, &LIBID_SPACESHIPSVRLib>
{~
I I
При выборе в мастере моделей Both или Free одноименная строка становится значением параметра ThreadingModel в Реестре.
Точки соединения и ISupportErrorlnfo
Добавление точек соединения к COM-классу выполняется очень просто. Установите флажок Support Connection Points и класс станет производным отIConnectionPoiniIm{>l-B него также будет добавлена пустая таблица соединений (connection шар). Для добавления точек соединения (например, для поддержки событий) к классу просто выполните следующие действия,
Определите в IDL-файле интерфейс обратного вызова.
Используйте генератор заместителя из АТЬдля создания класса-заместителя.
Добавьте класс-заместитель к классу СОМ.
Добавьте точки соединения в таблицу точек соединения, (connection point map).
ATL также содержит поддержку для ISupportErrorlnfo. Этот интерфейс обеспечивает правильное распространение информации об ошибке по цепочке вызовов. Объекты Автоматизации, использующие интерфейсы с обработкой ошибок, должны реализовывать ISupport- ErvorlnfoIfc'raHGBK.a. флажка Support ISupportErrorlnfo в диалоговом окне ATL Object Wizard приводит к тому, что класс ATL становится производным от ISupportErrorlnfolmpl.
Маршалер свободных потоков
Установка флажка Free Threaded Marshaier приводит к агрегированию в классе маршалера свободных потоков СОМ- Это выполняется путем вызова CoCreatePreeTbreadedMarsbaleK функции FinalConstruct. Маршалер свободных потоков позволяет потокобезопасным объектам обойти стандартный маршалинг, который происходит при всяком вызове методов интерфейса между подразделениями, и вызывать методы интерфейса в другом подразделении так, будто они находятся в том же самом подразделении, Тем самым значительно ускоряются вызовы между подразделениями. Маршалер свободных потоков осуществляет это, реализуя интерфейс IMarsbal. Когда клиент запрашивает интерфейс у объекта, механизм удаленного вызова запрашиваетIMarsbalnepes Queryinterface .Ъапл. объект реализует этот интерфейс (а в
данном случае это так, поскольку мастер ATL Object Wizard добавляет также элемент в таблицу интерфейсов класса, позволяющий ^«еоАг/ег/дсвобработать запросы об IMarsbal)u есть запрос на маршалинг, то маршалер свободных потоков копирует указатель в пакет маршалинга. В этом случае клиент получает реальный указатель на объект и обращается к объекту напрямую, не используя заместители и заглушки. Конечно, при выборе параметра Free Threacted Marshaler было бы лучше, чтобы все данные в вашем объекте были потокобезопасными, поэтому будьте очень осторожны при установке этого флажка.
Реализация класса Spaceship с использованием ATL
Мы создали класс космического корабля, используя значения по умолчанию, установленные в диалоговом окне ATL Object Wizard Properties. Например, класс будет иметь дуальный интерфейс, благодаря чему к нему можно будет получить доступ из таких сред, как VBScript на Web-странице. Кроме того, класс будет иметь модель потоков Apartment, вследствие чего СОМ будет контролировать основные вопросы синхронизации доступа, Единственное, что Вам нужно указать мастеру ATL Object Wizard — это понятное имя. Введите что-нибудь вреде AtlSpaceship в поле ввода Short Name на вкладке Names.
Нет необходимости задавать другие параметры прямо сейчас. К примеру, не нужно устанавливать флажок Support Connection Points, потому что мы рассмотрим соединения в следующей главе. Вы всегда можете добавить точки соединения, набрав нужный кед вручную.
Если Вы захотите создать СОМ-класс AlLS/wcesb/ркатегории Simple Object при помощи мастера ATL Object Wizard, то он сгенерирует следующее определение класса:
class ATL_NO_VTABLE CAtlSpaceship :
public CComObjectRootEx<CComSingleThreadModel>.
public CComCoClass<CAtlSpaceshlp. &CLSID_AtlSpaceship>,
public IDispatchImpl<IAtlSpaceship, &IIDJAtlSpaceship. &LIBID SPACESHIPSVRLib>
{
I;
Хотя ATL включает в себя довольно много COM-ориентированных С++-классов, использованных для spaceship базовых классов достаточно для понимания того, как работает ATL.
Большинство типовых COM-объектов, созданных с помощью ATL, являются производными от трех базовых классов: CComObjeclRoot,CComCoClassK IDispatch. CComObjectRootpeasm- зуетIUnknown и поддерживает идентичность класса. Это означает, что в нем реализованы AddRef, Release и поддержка механизма ATL для Queryinterface. CComCoClass реализует объект класса и некоторую обработку ошибок Реализуемый объект класса знает, как создавать объекты типа САЙбрасезЫрНаканец, в код, создаваемый мастером, включена реализация TDispafcft, основанная на библиотеке типов, которая создается при компиляции IDL-файла. По умолчанию реализация, базируется на дуальном интерфейсе который является интерфейсом IDispatch, за которым следуют функции, определенные в IDL
Как Вы можете вцдеть, использование ATL для реализации СОМ-классов отличается от использования чистого C++. При использовании ATL главной частью проекта являются интерфейсы, которые описываются в IDL-файле. Добавив функции к интерфейсам в IDI.-кодс, Вы автоматически добавляете функции к конкретным классам, реализующим эти интерфейсы. Добавление будет автоматическим потому, что проекты настроены таким образом, что компиляция IDL-файла, создаетзаголовочный файл C++ с теми же функциями. Все, что остается Вам сделать после добавления функций к интерфейсу, — это реализовать их в С++-клас- се. Из IDL-файла также создается библиотека типов, благодаря чему COM-класс может реализовать IDispatch. Однако ATL полезна не только при реализации «легковесных» СОМ-объектов, но и является новым средством создания элементов управления ActiveX, что будет рассмотрено в следующей главе.
Базовая архитектура ATL
Если Бы некоторое время поэкспериментируете с ATL, то увидите, насколько она упрощает процесс реализации классов СОМ. Средства поддержки очень хороши: разрабатывать СОМ- классы с использованием Visual C++ 6.0 также легко, как создавать программы на основе MFC. Вы просто используете Арр Wizard для создания нового ATL-класса. Однако вместо Class Wizard (применявшегося для обработки сообщений и добавления переменных-членов диалогового окна) используйте ClassView для добавления к интерфейсу определения новых функций, а затем заполните кодом C++ функции, сгенерированные ClassView. Код, который создает AppWizard, содержит всю необходимую реализацию класса, включая IUnknown, серверный модуль для COM-класса и объект класса с интерфейсом IClassFactory.
Только что описанный способ написания COM-объектов определенно более удобен, чем большинство других. Но что точно происходит, когда AppWizard генерирует код? Понимать, как работает ATL, важно, если Вы собираетесь расширять свои COM-классы и серверы на основе ATL за пределы того, что предоставляют AppWizard и ClassView. Например, ATL предоставляет поддержку для такой современной технологии, как «обособленные» (tear-off) интерфейсы. К сожалению, нет мастера или флажка для реализации обособленного интерфейса. Хотя в ATL есть поддержка обособленного интерфейса, Вам придется часть работы проделать вручную. Понимание ATL-реализации IUnknown будет полезным в этой ситуации.
Давайте рассмотрим класс CASSpaceship более детально:
class ATL_NO_VTABLE CAtlSpaceship :
public CComObjectRootEx<CComSingleThreadltode1_>,
public CComCoClass<CAtlSpaceshlp, &CLSID_AtlSpaceship>,1
public IDispatchImpl<IAtlSpaceship, AIID_IAtlSpaceship. &LI8ID_SPACESHIPSVRLib>
public:
CAtlSpaceshipO
}
DECLAREJIEGIS.TRY_RESOURCEID(IDR_ATLSPACESIIIP)
BEGIN COM MAP(CAtlSpaceship)
COM INTERFACE ENTRY(IAtlSpaceship)
COM_INTERFACE ENTRY(IDispatch)
EM3_C3M„MAP()
H lAtlSpaceship'
public;
Хотя это обычный исходный С++-код, он во многом отличается от того кода, который применяется для реализации COM-объектов. Например, в большинстве случаев СОМ-класс является производным непосредственно от интерфейсов, а здесь он производный от шаблонов. Кроме того, в этом C++-классе используется несколько, на первый взгляд, странных макросов. По мере изучения кода Вы увидите ATL-реализацию IUnknown,узнаете способ управления размером vtbl и познакомитесь с необычным применением шаблонов. Давайте начнем с первого макроса, созданного мастером: ATL_NO_VTABLE.
Управление размером vtbl■
СОМ-ингёрфейсы легко описываются на C++ как чисто абстрактные базовые классы. Создание классов СОМ путем множественного наследования (есть и другие способы) состоит просто в добавлении интерфейса в качестве базового класса и реализации всех его функций. Конечно, это означает, что в выделяемую для нашего COM-сервера память будет включена зна-
чи'гельных размеров таблица виртуальных функций (vtbl) для каждого реализованного интерфейса. Это ж важно, если у Вас немного интерфейсов и иерархия классов C++ не очень глубока. Однако такой способ реализации интерфейсов имеет тенденцию к увеличению размера таблицы по мере добавления интерфейсов и увеличения глубины иерархии. ATL позволяет уменьшить потери памяти, вызываемые множеством виртуальных функций. ATL определяет следующий символ:
#define ATL_NO_VTABLE __declspec(novtable)•
Использование A1'L_NO_VTABLE предотвращает инициализацию в конструкторе vtbl объекта, тем самым удаляя ее и все указатели на виртуальные функции этого класса из процесса компоновки. Это может отчасти уменьшить размер COM-сервера до размеров последнего в иерархии класса, не использующего директиву declspec novtable. Но будьте внимательны: небезопасно вызывать виртуальные функции в конструкторе любого объекта, объявленного с этой директивой, поскольку указатель на таблицу виртуальных функций не инициализирован.
Вторая строка в объявлении класса говорит о том, что СИ^расе5&//производен от класса CComObjectRootEx-Bar мы и добрались до ATL-версии IUnknown.
ATL-версия IUnknown:CComObjectRootEx
Хотя CComObjectRootEx не находится на самом верху иерархии ATL, он очень близок к нему. Реальным базовым классам для СОМ-объекта в ATL является CComObjectRootBase. (Определения обоих классов находятся в ATLCOM.H.) При изучении кода CComObjectRootBase можно обнаружить, что ожидается от базового С++-класса СОМ Класс включает переменную-член mjitvRefK^t подсчета ссылок. В нем есть методы OuterAddReJ,OuterRelease и OuterQuery- Intetface^px поддержки агрегирования и обособленных интерфейсов. Изучение CComObjectRootEx позволяет обнаружить InternalAddRejlnternalRelease и InternalQueTj>Inteijdc@?K реального выполнения подсчета ссылок и механизмы реализации Querylnterjac^pi» экземпляров классов с объектной идентичностью,
Из определения класса CAtiSpacesfoipBHpao, что он производный от CComObjectRootEx и что последний является параметризованным классам-шаблонам. Ниже показано определение CComObjectRootEx.
template <class Thr"eadHodel>
class CComObjectRootEx : public CComObjectRootBase
public:
typeclef ThreadModel _ThreadModel;
typedef _ThreadHodel::AutoCriticalSection _CritSec;
typedef CComObjectLockT<_ThreadModel> ObjectLock;
ULONG InternalAddRefO {
ATLASSERT(m_dwffef i= -1L);
return _ThreadModel: :Increment(&m_dwRef);
}""
ULONG InternalReleaseO {
ATLASSERT(m_dwRef > 0);
return ThreadModel::Decrement(&m dwRef);
}""
void Lock() (m_critsec.Lock();}-
void Unlock() (m_critsec.Unlock();}
private:
Critsec m_crltsec;
}; ""
CComObjectRootEx — это класс-шаблон, который зависит от класса потоковой модели, передаваемого как параметр шаблона. Фактически ATL поддерживает несколько моделей пою-
ков: однопотоковые подразделения (Single-Threaded Apartments, STAs), многопотоковые подразделения (Multi-Threaded Apartments, MTAs) и свободные потоки (Free Threading). Для определения модели потоков в проекте может быть один из трех символов препроцессора: _ATL_SING1E_THREADED, ATLjtiARTMENTTHREADED или _ATL_FREE_THREADED.
Определение j^TL_S1NGLE_THREADE& файле stdafx.h задает поддержку только одного потока на основе ST А Эта настройка пригодна для внешних серверов, не создающих дополнительные потоки. Поскольку сервер поддсрживае'гтолько един поток, переменные глобального состояния ATL можно не защищать критическим секциями, вследствие чего сервер будет эффективнее. Недостаток — сервер может поддерживать только один поток. Определение JVTL_^ARTMEOT_THREADE£tes3'BEBact поддержку нескольких потоков на основе STA Это полезно для внутренних серверов с моделью подразделений (т. е. у которых в Реестре параметр TbreadingModelAparlment^oc&oi^^ сервер, применяющий эту потоковую модель, может поддерживать несколько потоков, ATL предохраняет переменные своего глобального состояния при помощи критических секций. Наконец, определение _ATLJ-REE-THREADED создает сервер, совместимый с любой потоковой средой. Поэтому ATL предохраняет переменные глобального состояния критическими секциями, плюс каждый объект имеет собственные критические секции для защиты своих данных.
Указанные символы препроцессора просто задают потоковый класс, который будет передан в CComObjectRootExB качестве параметра шаблона. ATL предоставляет три класса потоковых моделей, которые обеспечивают поддержку наиболее эффективного, но потокобезопасного поведения СОМ-классов внутри каждого из трех вышеперечисленных контекстов. Этими классами являются CComMultiTbreadModelNoCS.CComMultiTbreadModeta. CComSingle- TbreadModel.
-class CComMultiThreadModelNoCS < public:
static ULONG WINAPI Increment(LPLONG p)
(return Interlockedlncrement(p);}
static ULONG WINAPI Decrement(LPLO»G p)
(return InterlcckedDecrement(p);}
typedef CComFakeCriticalSection AutoCri*icalSaction;
typedef CComFakeCriticalSection CriticalSection;
typedef CComMultiThreadModelNoCS ThreadModelNoCS;
class CComMultiThreadModel { public;
static ULONG WINAPI Increment!LPLONG p)
(return InterLjckedlncrerert(p);}
static ULONG WINAPI Decrement!LPLONG p)
(return InterlockedDecrement(p);}
typedef CComAutoCriticalSection AutoCriticalSection;
typedef CComCriticalSection CriticalSection;'
typedef CComMultiThreadModelNoCS ThreadModelNoCS;
-class CComSingleThreadModel { public:
static ULONG WINAPI Increment!LPLONG P) (return +<-(*p);}
static ULONG WINAPI Decrement(LPLONG p) (return-(*p);}
typedef CComFakeCriticalSection AutoCriticalSection;
typedef CComFakeCriticalSection CriticalSection;
typedef CComSingleThreadModeL ThreadModelNoCS;
};•
Обратите внимание, что в каждом классе есть две статические функции — Increment и Decrement — и ряд синонимов для критических секций.
CComMultiTbreadModelH CComMuitiTbre«dModel№CSpeama3yKyr Increment и Decrement через вызов потокобезопасных Х^пЗг-функций InterlockedlncrementH InterlockedDecrement. CComSingleTbreadModelpeajnisyeT эти же функции при помощи обычных операторов ++ и —.
Помимо разной реализации иикремеита/декремента, три модели потоков по-разному используют критические секции. ATL предоставляет две оболочки критической-секции: ССот- CriticalSection (это обычная «обертка» вокруг Win32 API критической секции) и CComAuto- CriticalSection (то же, что и CComCriticalSection,no с автоматической инициализацией/очист- кой). ATL также определяет класс «фиктивной» критической секции, имеющей те же методы, что и другие классы критических секций, но ничего не делающей. Как видно из определений классов, CComMuliiThreadModel использует настоящие критические секции, в то время как CComMulUTbreadMadelNoCSa CComSingle'rbreadModetncHomsyeT фиктивные, ничего не делающие критические секции.
Теперь более понятно минимальное определение класса ATL При всяком указании класса CComObjectRootExe него передается класс модели потоков. CAtlSpaceshp определен с помощью класса CComSingleThreadModel, поэтому он использует методы этого класса для инкре- мента/декре мента, а также применяет фиктивные критические секции. Следовательно, CArlSpaceship ведет себя наиболее эффективно, поскольку не должен заботиться о защите данных. Однако Вы не привязаны к этой модели. Если Вам нужно (делать CAtlSpaceshp потокобезопасным, то просто укажите CComMultiThreadModel в качестве параметра шаблона для CComObjectRoatEx. Тогда вызовы AddRefn Release автоматически отобразятся на корректные функции Increment u Decrement.
ATL и Queryinterface
Похоже, что в реализации Queryinterface ATL берет пример с MFC, так как тоже использует поисковую таблицу. Взгляните в середину определения CAtlSpaceship — Вы увидите состоящую из макросов таблицу интерфейсов (interface шар). Таблицы интерфейсов и ATL создают механизм реализации Queryinterface.
Клиенты используют Querylnterfac^in произвольного расширения связи с объектом. Это значит, что когда клиенту необходим новый интерфейс, он вызывает Queryinterface для существующего интерфейса. Если объект реализует искомый интерфейс, он возвращает его клиенту. Если нет — возвращается ошибка, обозначающая, что интерфейс не найден.
Традиционные реализации Queryinterface обычно состоят из длинных операторов if-then. Например, для COM-класса с множественным наследованием она может выглядеть так class CAtlSpaceship: public IDispatch, lAtlSpaceship {
HRESULT Queryinterface(RIID riid, void** ppv) {
if(riid == IID_IDispatch)
ppv = (IDispatch*) this;
else if(riid == HfMAtlSpaceship | riid == IID_IUnknown)
ppv = (lAtlSpaceship *) this;
else {
ppv - 0;
return E NOINTERrACE;
}
((IUnknown*)(*ppv))->AddRef();
return NOERROR;
}
// AddRef, Release и другие функции
}:
Как Вы уже видели, ATL использует поисковую таблицу вместо обычного оператора if-then. Эта таблица начинается с макроса ВЕб1Н_С'ОМ_МАРНиже приведено его полное определение, «define BEGIN.COM.HAP(x) public:■
typedef x ComMapClass;■
static HRESULT WINAPI _Cache(void* pv, REFITD lid, void»» ppvObject, DWORD dw) !
.ComMapClass» p = (.ComMapClass*)pv;
p->Lock();
HRESULT hRes =
CComObjectRootBase: :_Cache(pv, iid, ppvQbject, dw);
p->Unlock(j;
return hRes;
}
IUnknown* GetRawUnknpwn() {
ATLASSERT(_GetEntries{)[0].pFunc ==
„ATL.SIMPLEMAPENTRY);
return (IUnknown*)((int)this+ GetEntries()->fiw);
_ATL_OECLARE_GET_UNKNOWN(x)
HRESULT _InternalQueryIr.Lerface(REFIIO iid, void** ppvObject) {
return InternalQuerylnterface(this, GetEntriesO, iid, ppvObject);
}
const static .ATL.INTMAP ENTRY- WINAPI _GetEntries() {
static const _ATL INTMAP ENTRY _entries[] = {
DEBUG_OI_ENTRY"(x)
ttdefine ENd'.COM.NAPO (NULL, 0, 0}};\
return .entries;}
Каждый класс, который использует ATL для реализации Шпкпогопдолжвн задать таблицу интерфейсов для реализации InternalQue^Interfacel^nra^ интерфейсов в ATL состоит из структур, содержащих тройку; идентификатор интерфейса (Gl/Ю)/DWORD/указатель на функцию. Далее показан тип .ATL.INTMAP.ENTRY, поддерживающий эту тройку.
struct .ATL.INTMAP.ENTRY {
const IID* piid;
DWORD dw;
.ATL.CREATORARGFUNC* pFunc;
};
Первый элемент — это идентификатор интерфейса (GUID), второй — признак действия, которое будет произведено при запросе интерфейса. Третий параметр интерпретируется по- разному. Если pFunc равно константе _АТЛ.5ЛИР1ЕЛЙР.ЕЛ/та1), то dw — это сме
щение внутри объекта. Если pFunc не ноль и не равен 1, то указывает на функцию, которая будет вызвана при запросе интерфейса. Если р^мпсравно NULL, то dw означает конец поисковой таблицы ^uerylnterjace..
Обратите внимание, что СА^ргзс^/риспользует макрос COMJN7EREACE_ENTR®to элемент таблицы для обычных интерфейсов. Вот этот макрос полностью:
«define offsetofclass (base, derived)
'((DWORD)(static_cast<base*>((derived»)8))-8)
«define COM_INTERFACE_ENTRY(x)\
{&_ATL_IIDOF(x). \
offsetofclass(x, .ComMapClass), \
.ATL.SIMPLEMAPENTRY)
COMJNTEREACEJSNTRUcMeiHiaeT идентификатор интерфейса в структуру _ATL_INTMAP_ ENTRY. Кроме того, обратите внимание, как о^еГо/с/ажреобразует йМ5-указатель в необходимый интерфейс и помещает полученное значение в поле dw. Наконец, COM, INTERFACE
ENTRY помещает в последнее поле значение ATLJEMPLEMAPENTRY длуказания на то, что dw является смещением внутри класса.
Например, таблица интерфейсов для СА115р«сезЫрвыглядат следующим образом после обработки препроцессором:
const static _ATL_INTMAP_ENTRY*
,_stdcall_GetEntries() {
static const _ATL_IWTMAP_EHTRY _entries[] = {
{MD-IAtlSpaceship,.
((DWORD)(static cast< IAtlSpaceship*>((_ComMapClass*)8))-B),
((_ATL_CREATORARGFUMO«)1)),
(&IID_IDispatch,
((DWORD)(static cast<IDispatch*>((_ComMapClass*}8) )-6).
(( ATI CREAT0RARGFUNC*)1)},
<0, 0, 0}
return _ent ries;
В настоящий момент класс CAtlSpaceship поддерживает два интерфейса lAtlSpaceship и IDispatch, поэтому в таблице два элемента.
Реализация IntemalQuevylnter/ac® ССопгОЬ]ес1Роо1Ехиолучает функцию jSetEntries в качестве второго параметра. В этой реализации используется глобальная АТЕ-футткцияЯЙ7п7ег- nalQueryInter/аседля поиска интерфейса в таблице путем ее простого перебора.
Помимо COMJNTERFACE_ENTRY, состав ATL входят еще 16 макросов, реализующих различные способы композиции объектов: от обособленных интерфейсов до агрегирования.
Далее Вы увидите, что нужно сделать для поддержки интерфейса lAtlSpaceship и добавления двух других интерфейсов — ШоПопи. IVisttal. Вы также узнаете о странном «звере» в мире СОМ по имени дуальный интерфейс.
Запуск космического корабля
Теперь, когда у Вас есть некоторый ATL-код, что делать дальше? Поскольку речь идет о СОМ, то начать нужно с IDL-файла. Это может быть новая, не знакомая Вам сторона разработки программного обеспечения. Помните: сейчас такое время, когда распространение и интеграция ПО становятся очень важными. Ранее Вы могли разобраться в деталях классов C++ и встроить их вместе в проект, поскольку Вы (как разработчик) видели всю картину целиком. Однако в компонентных технологиях (подобных СОМ) все иначе — Вы не видите перед собой всю картину целиком. Очень часто у Вас есть только компонент, без исходного кода для него. Единственный способ узнать, что может делать компонент, — вызвать интерфейсы, которые он предоставляет.
Имейте в виду, что современные разработчики ПО используют множество средств, а не только C++. Есть разработчики на Visual Basic, Java, Dolphin C. COM «отлаживает» все углы при интеграции программных блоков, созданных этими программными средствами. Кроме того, удаленное выполнение ПО (как внешнего процесса на этой же или другой машине) требует какого-либо межпроцессного взаимодействия. Для решения этих проблем и был создан язык определения интерфейсов (Interface Definition Language, IDL). BorIDL-файл, созданный мастером для нового класса spaceship:
import "oaidl.idl"; import "ocldl.idl";
object,
см. след. стр.см. след. стр.uuid(A9D750E1-51A1-11D1-8CAA-F010«7?CC837), dual, helpstring("lAtlSpaceship interface"),
см. след. стр.
см. след. стр.
pointer cefault(unique)
]
interfacelAtlSpaceship : IDispatch
uuid(A0736061-50DF-11D1-8CAA- FD10872CC837).
version(1.0),'
helpstring("spaceshipsvr 1.0 Type Library")
library SPACESHIPSVRLib
in’portlib("stdole32.tlb");
importlic("stdole2. tlb");
L
uuid(A9D750E2-51A1-11D1-8CAA-FD10872CC837), helpstring("AtISpaceship Class")
]
coclass AtlSpaceship
[default] interface lAtlSpaceship;
);
};
Ключевая концепция IDL состоит в том, что это чисто декларативный (declarative) язык. Он определяет, как клиенты могут общаться с объектом. Помните: этот код будет обработан MIDL-компиляторомдля получения чисто абстрактного базового класса, используемого клиентами на C++, и библиотеки типов, используемой клиентами на Visual Basic, Java и другими. Если Вы способны разобраться в обычном коде на С, то сможете понять и IDL Образно выражаясь, IDL — это С со сносками. Синтаксис IDL определяет, что атрибуты всегда предшествуют элементу', который они описывают. Например, атрибуты предшествуют объявлениям интерфейсов, библиотек и параметров методов.
Обратите внимание: IDL-файл начинается с импорта файлов oaidl.idl и ocidl.idl, который подобен включению windows.h внутри файлов С или C++. Указанные IDL-файлы содержат определения всех базовых элементов СОМ (включая определения IUnknown и IDispatch}.
За операторами z'^ort идет открывающая квадратная скобка ф. В IDL в квадратных скобках всегда помещаются атрибуты. Первым элементом этого IDL-файла является интерфейс lAtlSpaceship. Однако до описания этого интерфейса необходимо указать его атрибуты. Например, ему надо дать имя (GUID), указать MlDL-компилятору, что это СОМ-интерфейс, а не интерфейс для стандартного RPC, а также указать, что это дуальный интерфейс (подробнее о дуальных интерфейсах чуть ниже). Далее идет сам интерфейс. Обратите внимание, что он выглядит очень похожим на обычную структуру языка С.
После описания интерфейсов полезно собрать их вместе в библиотеке типов, что делает- ся'в следующем разделе IDL-файла. Обратите внимание: библиотека типов также начинается с открывающей квадратной скобки, обозначающей следующие за ней атрибуты. Как всегда, библиотека типов — как и всякая отдельная «вещь» в СОМ — требует имя (GUID). Оператор описания библиотеки сообщает компилятору MIDL, что эта библиотека включает СОМ-класс AtlSpaceship и что клиенты этого класса могут запросить интерфейс lAtlSpaceship.
Добавление методов к интерфейсу
В нынешнем виде интерфейс LAtlSpaceship неприменим — ему нужны один или два метода. Давайте добавим один. Учтите, что в Visual C++ теперь расширены возможности ClassView по показу СОМ-иптерфейсов. (Они определяются по маленькому «леденцу» — стрелочке с кру-
жочком — рядом с именем.) Обратите внмание: €AtiSpacesbipn.poH3»onw от jAtlSpaceship, который является СОМ-иптерфейсом. Двойной щелчок LAtlSpacesbipB окне ClassView приведет к выводу в окне редактора соответствующего раздела IDL-файла (см. рис. 29-4).
Рис. 29-4. Интерфейсы в ClassView
В данный момент Вы можете изменить СОМ-иитерфейс прямо в IDL-файле. Если Вы добавите таким образом функции и методы, то Вам придется обратиться к файлам AtlSpaceehipli и AtiSpaceship.cpp и ввести методы вручную. Более эффективно добавить функции к интерфейсам через ClassView. Для этого просто щелкните интерфейс правой кнопкой мыши. Б контекстном меню будут элементы Add Method и Add Property. Давайте добавим метод CsllStar- Fleet. На рисунке 29-5 показано диалоговое окно, которое появляется при добавлении метода,
Рис. 29-5. Добавление метода к интерфейсу
Для добавления метода наберите его имя в поле ввода Method Name, затем наберите его параметры в поле ввода Parameters. Здесь нужно объяснить одну деталь относительно IDL.
Помните, что цель IDL — предоставить однозначную информацию о вызове методов. В стандартном мире C++ Вы часто имеете дело с неоднозначными вещами, например, с массивами неизвестной длины, что не проблематично, поскольку вызывающий и вызываемый используют один и тот же фрейм стека, и всегда найдется нужное количество памяти. Но так
как вызовы методов могут происходить по сети, то важно точно сообщить механизму удаленных вызовов, что представляет из себя COM-интерфейс. Сделать это можно, добавив атрибуты к параметрам метода (опять квадратные скобки).
Метод CallStarFleet(вызвать звездный флот), имеет два параметра — число с плавающей запятой, обозначающее дату, и BSTR, описывающее получателя. Обратите внимание, что в методе описываются направления передачи параметров. Дата передается в метод при его вызове, что обозначается атрибутом [in]. Строка, описывающая получатель, передается обратно в виде указателя на BSTR. Атрибут [out] означает направление передачи параметра от объекта к клиенту, Атрибут [retval] означает' что результат вызова метода можно присвоить переменной в языке высокого уровня, поддерживающем такую возможность.
Дуальные интерфейсы,
В главе 25 Вы видели интерфейс IDispatch, Он позволяет представить функциональность объекта (на двоичном уровне) для языков высокого уровня, подобных VBScript, которые не имеют представления о vtbl Чтобы toispatchs аработал, клиенту нужно выполнить ряд хитроумных действий перед вызовом Invoke. Сначала клиент получает идентификаторы вызова1, затем настраивает аргументы типа VARIANT. На стороне объекта декодируются все VARIANT-параметры, проверяется их корректность, затем они размещаются на стеке, и происходит вызов функции. Ясно, что это — сложная и длительная работа. Если Вы пишете СОМ- обьект и предполагаете, что одна часть клиентов будет использовать языки сценариев, а другая — языки, подобные C++, то Вы получите дилемму: или Вы включаете интерфейс IDispatch, или лишаетесь клиентов на языках сценариев. Если Вы предоставите только IDispatch, то доступ к объекту из C++ будет очень неудобен. Конечно, можно предоставить доступ как через IDispatch,так. и через собственный интерфейс, но это требует дополнительного программирования. Данную проблему решают дуальные интерфейсы.
Дуальный интерфейс — это просто IDispatch с добавленными в конец функциями. Например, интерфейс IMotion, описываемый ниже — это дуальный интерфейс:
interface IMotion : public IDispatch {
virtual HRESULT Fly() = 0;
virtual HRESULT GetPosltion() = 0;
I;
Рис. 29-6, Формат vtbl дуального интерфейса
Так как IMotion происходит от IDispatch, то первые семь функций IMotion — это функции IDispatch. Клиенты, которые понимают только IDispatch (например, VBScript)., считают этот интерфейс одной из разновидностей IDispatch и для вызова функций передают их DISPID в
Автор имеет в виду dfsp-идентификаторы методов и свойств. — Прим, перев.
Invoke. Клиенты, которые понимают произвольные интерфейсы на основе vtbl, воспринимают этот интерфейс целиком, игнорируют четыре функции в середине, относящиеся непосредственна к IDispatch, и работают с первыми тремя (от IUnknown) и последними двумя (от самого Motion) функциями. На рисунке 29-6 показан формат vtbl для Motion.
Во многих реализациях на C++ производится загрузка библиотеки типов и делегирование интерфейсу ITypelnfo реализации вызовов Invoke и GetlDsOJNames. Подробности см. в книгах Брокшмидта и Роджерсона.
ATL и IDispatch
В ATL реализация IDispatch находится в классе LDispatcblmpla делегирует вызовы библиотеке типов. Объекты, которые реализуют дуальный интерфейс, должны включать шаблон IDispatcb- Im.pl в число базовых классов:
class ATL_NO_VTABLE CAtlSpaceship :
public CCorObjoctRootEx<CCor.SingleTlreacModel>.
public CComCoClass<CAtlSpaceship, &CLSI3_AtlSpaceship>
public IDispatchlmpKIAtlSpaceship, &no_IAtlSpaceship, Al TKID_SPACESHrPSVRi i3>,
public IDispatchlmuldVisual, AIID.IVisual, &Ll3IC_SPACESH*PSVRlib>, public IDispalchlmplclMotion, &IID Motion, &LIBID_S?ACESrlIPSVRLib>
Помимо добавления класса-шаблона IDispatchhnpl в список базовых, у объекта должны 'быть элементы таблицы интерфейсов дчя дуального интерфейса и для IDispacth, чтобы Queryl- nterface работала правильно..
BEGIN_COM MAP(CAtlSpaceship)
COM_IHTERFACE_EMTRY(IAtlSpaceship)
COM INTERFAOE_ENTRv(IDisoatch)
END_COM_MAP()
Как видите, аргументами класса-шаблона IDispatchlmplssstHKrica сам дуальный интерфейс, его GUID и GUID библиотеки типов, хранящей всю информацию об интерфейсе. Кроме того, есть ряд необязательных параметров, не показанных в описании САГЙряс^й^номер версии библиотеки типов и класс для управления информацией о типах. По умолчанию используется ATL-K.'iucc CComTypelnfoHolder.
В большинстве обычных реализаций IDispatch на C++ в конструкторе класса производится вызов LpadTypeLib и ITypeLib;:Getiype!nfoOfGuid последующим сохранением указателя на ГГуре$п/апа. весь период существования класса. В ATL реализация сделана чуть иначе — на основе класса CComTypelnfoHolderB этом классе есть переменная-член типа указатель на ITypelnfo и «обертки» основных функций IDispatch; GetlDsOfNamesu Invoke.
Клиенты запрашивают дуальный интерфейс, вызовая Queryinterface для ZZD_Z4?7S/wcesib4>. (Этот же интерфейс клиент может получить, вызвав Querylnietface для IDispatch.) При обращении к CallStarFleefsasoB этой функции произойдет напрямую (как для любого другого COM-ин терфсйса).
Когда клиент вызывает IDispatch.-.Invoke, вызов передается в функцию Invoke класса IDis- paichhnpl, которая делегирует вызов в Invoke класса CComTypelnfoHolder. В этом классе загрузка библиотеки типов (вызов LoadTypeLib) откладывается до первого обращения к Invoke или GetlDsOJNames. Для обращения к библиотеке типов па основе информации в Реестре (используя GUID и номер версии, переданные как параметры шаблона) в CComTypelnfoHolder есть функция-член GetTL Затем CComTypelnfoHolder вызывает lTypeLib::GetTypelnf<xna получения информации об интерфейсе (указатель иа liypeInfo)JIfl3S№ все вызовы делегируются этому интерфейсу. Реализация GetlDsOJNames в IDispatchlmpl выполнена аналогично.
Интерфейсы IMotion и IVIsuat
Чтобы сделать наш COM-класс похожим на другие его версии (C++ и MFC, описанные в главе 24), необходимо добавить в него и в проект интерфейсы IMotion и IVisible. К сожалению, в настоящее время единственный способ выполнить эго — кодировать вручную (ATL AppWizard создает по умолчанию только один интерфейс). Откройте IDL-файл, поместите курсор где- нибудь после операторов ^import, но до оператора library, и начните набирать определения интерфейсов, как описано в следующем параграфе.
Зная 1DL, Вы автоматически должны начать описание интерфейса с открывающей квадратной скобки. Помните, что в IDL различные элементы имеют атрибуты. Одним из важнейших атрибутов является имя (GUID). Кроме того, очень важно для интерфейса иметь атрибут object, обозначающий принадлежность к СОМ (а не к обычному RPC). Возможно, Вы захоти- 1 е сделать интерфейсы дуальными, В этом случае добавьте к атрибутам ключевое слово dual, которое приведет к размещению в Реестре соответствующих записей, необходимых для пра- ]. ильного маршалинга. После завершения атрибутов путем закрывающей квадратной скобки ] лючевос слово interface начинает описание самого интерфейса. Сделайте IMotion дуальным интерфейсом, a IVisual — обычным, чтобы показать, как интерфейсы разных типов подсоединяются к классу CSpacesbip.
object,
uu id<97B5C101-5299-11d1-8CAA-FD10872CC337),
dual,
helpstring("IMotion inter~ace")
]
interface IMotion : IDispatch
' HRESULT Fly();
HRESULT GetPosition([out,retval]long* nPcsition);
К
object.
uuid(56F58464-52A4-11d1-8CAA-FD10872CC837),
helostring("IVisual Interface")
I
interface IVisual : IUnknown
HRESULT DisplayO;
}:
После введения описаний интерфейсов заново пропустите IDL-файл через компилятор MIDL для создания новой копии spaceshipsvr.h с чисто абстрактными базовыми классами IMotion и IVisual
Теперь нам надо добавить эти интерфейсы к классу CSpacesbip.S-vo делается в два этапа. Первый этап состоите создании интерфейсной части класса. Давайте начнем сIMotion. Добавить интерфейс IMotion к CS^acesbip очень легко — просто используйте шаблон IDispatcb- Impj предоставляющий необходимую реализацию:
class ATL_NO_VTABLE CAtlSpaceship :
public CComObiectRootEx<CComSinglGThreadModC'l>,
public CComCoClass<CAtlSpaceship. &CLSIO_AtlSpaceship>,
pubic IDispatchlmplcIAtlSpaceship, &nD_IAtlSpaceship, &LIBID_SPACESHIPSVRLib>,
public lDispatchImpl<IMotlcn, &TID IMotion, &LIBID SPACESHIPSVRLib>
(""
Второй этап — заполнение таблицы интерфейсов, чтобы клиент мог запросить интерфейс IMotion. Однако наличие в одном COM-классе двух дуальных интерфейсов создает проблему, Когда клиент запрашивает IMotion, он должен получить IMotion. Но когда клиент запрашивает IDispatch, какую версию этого интерфейса он получит — lAtlSpacesbiptum IMotion?
Несколько дуальныхинтерфейсов
Вспомните, что все дуальные интерфейсы начинаются с семи функций интерфейса IDispatch. Проблема возникает, когда клиент вызывает Querylntetfac^a IIDJDispatch.'EyjiyTa разработчиком, Вы должны выбрать версию IDispatch, передаваемую клиенту. -
Интерфейс, возвращаемый ^uerylnterface^apsH в таблице интерфейсов. В ATL есть специфический макрос для обработки ситуации с дуальными интерфейсами. Сначала рассмотрим имеющуюся таблицу интерфейсов:
BEGIN_COM МАР(CAtlSpaceship)
COM INTERFACE_ENTRY(lAtlSpaceship)
COmZiNTERFACE ENTRY(IDispatch)
END_COM_MAP()
Когда клиент вызывает Qae^nte^ace, ATL проходит по таблице, пытаясь найти IID, совпадающий с запрошенным. Приведенная таблица содержит два интерфейса: lAtlSpacesbipH IDispatch. Если нужно добавить к классу CAtlSpacesbipeute один дуальный интерфейс, то нужен другой макрос.
Для обработки случая нескольких дуальных интерфейсов в ATL существует макрос COMJN- ТЕКРАСЕ_ЕМТКУ2Ч.тобы. OyerplnteTface работала правильно, нужно только решить, какую версию IDispatch будет получать клиент, например:
BEGIN COM MAP(CAtlSpaceship)
COM INTERFACE_ENTRY(lAtlSpaceship)
C0M.IN1ERFACE_ENTRY(IMotion)
COMJNTERFACE ENTRY2(IDispatch, lAtlSpaceship)
END_COM_MAP()
В данном случае клиент при запросе IDispatch получит указатель на LAtlSpaceship, первые семь функций которого являются функциями IDispatch.
Добавить недуальный интерфейс еще проще — просто добавьте интерфейс в качестве базового класса
class ATL_NO_VTA0LE CAtlSpaceship :
public CComOhjectRootEx<CComSingleThreadModel>,
public CComCoClasscCAtlSpaceship, &CLSID_AtlSpaceship>,
public IDispatchImpl<IAtlSpaceship, &IID_IAtlSpaceship, &LIBID_SPACESHIPSVRLib>,
public IDispatchlmpKIMotion, &IID_IMotion, &LIBID_SPACESHIPSVRLib>, public IVisual
и добавьте запись в таблицу интерфейсов
BEGIN COM МАР(CAtlSpaceship)
COM INTERFACE ENTRY(lAtlSpaceship)
COM_INTERFACE_ENTRY(IMotion)
C0M_INTERFACE_ENTRY2 (10 ispate h, I AllSpaceship)
COM INTERFACE ENTRY(IVisual)
EMO_COM_MAP()
ГЛАВА 30
ATL и элементы управления ActiveX
Если, закончив читать о СОМ и ATL, Вы раздумываете, как можно соотнести СОМ со своей повседневной программистской деятельностью, то Вы не одиноки. Не так просто понять, как использовать СОМ в реальной жизни. Помимо прочего, необходим значительный объем кода, чтобы COM-объект просто заработал. Однако есть совершенно реальное приложение для СОМ прямо у вас перед носом — элементы управления ActiveX, т. е. небольшие «приспособления» (обычно с пользовательским интерфейсом), написанные на основе СОМ.
В главе 29 мы рассмотрели COM-классы, созданные с использованием ATL В этой главе Вы узнаете, как писать COM-классы конкретного типа — ActiveX- элементы. В главе 8 Вы могли уже поработать с этими элементами в качестве клиента. Теперь настало время написать свои собственные.
Создание элемента упр авления ActiveX с использованием ATL включает в себя:
определение внешнего вида;
I* разработку входящих интерфейсов;
|* разработку исходящих интерфейсов (событий);
реализацию механизма устойчивости (сохранения свойств);
предоставление пользовательского интерфейса для изменения свойств.■
В дайной главе рассматриваются все этапы. Скоро Вы сможете при помощи ATL создавать элементы управления ActiveX, которые можно вставлять в свои программы.
Элементы управления ActiveX
Даже сегодня остается неразбериха по поводу того, что реально считать элементом управления ActiveX. В 1994 Microsoft добавила несколько новых интерфейсов к протоколу OLE, разместила их внутри DLL и назвала их эяежнтамиупр деления OLE (OLE Controls). В исходном виде эти элементы реализовывали почти весь протокол встраивания OLE-документов. Кроме того, OLE-элементы поддерживают:
динамические вызовы (Автоматизацию);
страницы свойств (чтобы пользователь мог изменять свойства элемента);.
исходящие интерфейсы (наборы событий);-
соединения (стандартный способ вызова событий для клиентов и элементов управления).
Когда Интернет стал доминировать в рыночныхпланах Microsoft, она анонсировала свои намерения по размещению ActiveX-элементов на Web-страницах. В этот момент размер компонентов стал важен. Microsoft взяла свою спецификацию элементов управления OLE, поме-
нала название на элементыуправления ActiveX (ActiveX Controls) и заявила, что все перечисленные выше возможности являются необязательными. Это означает, что, согласно новому определению, ActiveX-элемент должен только быть основан на СОМ и реализовывать IUnknown. Конечно, полезный элемент управления должен реализовывать большинство из вышеперечисленных возможностей. Так что, в конце концов, ActiveX-элемент и OLE-элемент означают практически одно и то же.
Разработчики могут использовать MFC для создания элементов управления ActiveX с середины 1994. Однако одна из отрицательных сторон применения MFC — элемент управления привязывается к MFC. Иногда нужно, чтобы элемент управления был как можно меньше или чтобы работал даже при отсутствии MFC DLL в системе пользователя. Кроме того, использование MFC для создания ActiveX- элементов заставляет Вас принять определенные архитектурные решения. Например, Вы ограничиваете себя в использовании дуальных интерфейсов (если не напишите много дополнительного кода). Кроме того, элемент управления и его страницы свойств будут вынуждены общаться друг с другом через интерфейс IDispatch.
Теперь, чтобы избежать указанных проблем, разработчики для создания ActiveX-элементов могут использовать ATL Эта библиотека предоставляет средства для создания полноценных элементов управления ActiveX, поддерживающих любую допустимую для них возможность, включая входящие интерфейсы, устойчивые свойства, страницы свойств и точки соединения. Если Вы ранее писали ActiveX-элементы при помощи MFC, то увидите, что применение ATL предоставляет гораздо больше гибкости.''
Использование ATL для создания элементов управления
Хотя создание ActiveX-элемента с использованием ATL весьма простой процесс, но в действительности более обременительный, чем при использовании MFC. Это вызвано тем, что в ATL не входят все «приятные» мелочи, которые есть в MFC Например, в ATL нет классов-оболочек контекстов устройства, поэтому при рисовании приходится использовать описатель контекста. Кроме того, ClassWizardHe понимает исходный код на основе ATL, поэтому при обработке сообщений приходится использовать «мастер-золотые руки» (*iypingWizard»),T. е. вручную заполнять таблицы сообщений.
С учетом указанных особенностей создание ActiveX-элемента при помощи ATL гораздо более легкий процесс, чем создание его с нуля. Кроме того, применение ATL дает определенную гибкость, которой нету MFC. Например, добавление дуальных интерфейсов к элементу управления является весьма утомительным в MFC, но очень простым в ATL Кроме того, ATL COM Object Wizard позволяет очень легко добавить в проект COM-классы (даже не ActiveX- элементы), в то время как добавить новые элементы в DLL на базе MFC гораздо труднее.
В этой главе мы представим пару игральных кубиков как ActiveX-элемент на основе ATL Будут проиллюстрированы наиболее важные стороны элементов управления Ac tlvreX, включая отрисовку, входящие интерфейсы, свойства, страницы свойств и события.
Создание элемента управления
Как обычно, самый простой способ создать СОМ-ссрнер в ATL— эго использовать мастер ATL COM Object Wizard. Сначала выберите New из меню File и перейдите на вкладку Project в диалоговом окне New. Выберите ATL СОМ Арр Wizard и назовите проект ATLDiceSvr. В мастере Арр Wizard примите все параметры, предлагаемые по умолчанию, чтобы сервер был создан в вид е DLL Далее выполните следующие действия.
Выберите New ATL Object в меню Insert для добавления в проект нового ATL-объекта.
В ATL Object Wizard выберите Controls в списке Category и Full Control в списке Objects.
Щелкните Next длявызова диалогового окна ATL Object Wizard Properties. В поле ввода Short Name на вкладке Names укажите имя элемента управления (например, ATLDiceOby Диалоговое окно будет выглядеть примерно как на рисунке 30-1.
Рис. 30-1. Диалоговое окно ATL Objectwizard Properties
Перейдите на вкладку Attributes. Здесь производится настройка элемента управления, Например, Вы можете:
т выбрать модель потоков;
• определить тип основного интерфейса — дуальный или.произвольный (custom);
т указать, будет ли поддерживаться агрегирование;
т решить, будут ли использоваться исключения и точки соединения,■
Для облегчения жизни установите флажок Support Connection Points. (Это избавит Вас от дополнительного кодирования.) Значения остальных атрибутов оставьте по умолчанию. На рисунке 30-2 показано, как должна теперь выглядеть вкладка Attributes,
Рис. 30-2. Вкладка Attributes диалогового окна ATL Object Wizard Properties
Перейдите на вкладку Miscellaneous, Здесь можно установить разнообразные параметры, относящиеся к элементу управления. Например, можно указать, что элемент будет подобен обычным элементам управления Windows, таким, как кнопки или метки. Можно также установить, что элемент будет невидимым в период выполнения и что у него будет непрозрачный фон. На рисунке. 30-3 показаны предлагаемые параметры,
Рис. 30-3. Вкладка Miscellanious д иалогового окна ATL Object Wizard Properties
Наконец, перейдите на вкладку Stock Properties, если хотите, чтобы у элемента управления были предопределенные (stock) свойства. Предопределенные свойства выделены потому, что они могут быть у любого элемента управления, например, цвет фона, цвет обрамления, цвет текста и заголовок. На рисунке 30-4 показана вкладка Stock Properties.
Рис. 30 -4. Вкладка Stock Properties диалогового окна ATL Object Wizard Properties
Завершив устанавливать атрибуты элемента управления, щелкните ОК
ATL Object Wizard добавит в проект заголовочный файл и файл с исходным кодом, которые описывают новый элемент управления. Кроме того, в IDL-файле будет выделено место для основного интерфейса элемента управления и этому интерфейсу будет присвоен GinD. Вот определение элемента управления, создаваемое мастером:
class ATI_HO_VTABLE CATLOiceObj :
public CComObjectRcotEx<CComSingleThreadModel>,
public IDispaTchlmpKlATi DteceObj, &HP_lATLDiecsObj. &LIBIO_ATLDICESVRLib>,
public CComControl<CATLDiceDbj>,
public lPersistStreamInitImpl<CATLDiceObj>,
public IGieControlImpl<CATLDiceObj>, .
public IQieOb;cctImpl<CATLDiceObj>,
public I01eInPIaceActive0bjectIfnpl<CATLDice0bj>,
см. след. стр.
public IViewObjectExImpl<CATLDiceObj>,
public I01eInPlace0bjectWlndowlessImpl<CATLDice0bj>,
public IConnectionPointContainerImpl<CATlJHceObj>,-
public IPersistStorageIinpl<CATLDiceObj>,
public ISpecifyPropertyPagesImpl<CATLDlceQbi>,
public IOuickActivateImpl<CATLOiceObj>,
public IDataObjectImpl<CATLDiceObj>,
public IProvideClassInfo2ImpK&CLSID_ATLDice0b, &DIID_DDiceEvenTs, &LIBID_ATLDICESVRLib>, public IPropertyNotifySinkCP<CATLDiceObj>,
public CComCoClass<CAT-DiceObj. 4CLSID ATLDiceOb> <
i;
Список базовых классов очень большой. Шаблоны для реализации IUnknown и объектов классов Вы уже видели — они находятся в классах CCornObjectRootExH. CComCoClass. Вы также видели ATL-реализацию IDispatch в шаблоне IDispatchlmpLOg^axo есть еще 11 интерфей- :ов, необходимых для поддержки работоспособности базового элемента управления. Ниже
теречислены эти интерфейсы, разделенные на категории.
КатегорияИнтерфейс
Интерфейсы для собственного описания
IProvideCltissInfo2
. 1нтсрфейсыдля обработки устойчивости
IPersistSrreamlnir IPersistSturage
Интерфейсы для обработки активизации
IQuichActivate (и часть ideObject)
Интерфейсыдля соответствия исходной спецификации • )LE элементов
tOleConlrol
Интерфейсы для соответствия спецификации OLE-документов
lOleObJect
. 1нтерфейсыдля предоставления данных
lOtelnPlaceActiveObject
IViewObject lOlelnPtaceObjectWinrfowless
IDalaObjecl
Интерфейсы в помощь контейнеру для поддержки страниц свойств
ISpeci^ProperryPages
Интерфейсы для обработки соединений
1 PropertyNotifySink IConnectionPointContainer
ПРИМЕЧАНИЕ Есть много интерфейсов, которые COM-класс должен реализовать, чтобы объявить себя ActiveX-элементом. Большинство реализаций стандартны и незначительно (если вообще) отличаются друг от друга. Достоинство ATL заключается в том, что она реализует стандартное поведение и предоставляет возможность вставить собственный код в нужных местах, поэтому Вам не нужно портить глаза, изучая код СОМ. Вы мажете жить полноценной и счастливой жизнью, не разбираясь досконально, как работают эти интерфейсы. Если же Вам хочется больше узнать о внутренней жизни ActiveX-элементов, то обратитесь к книгам: Крейга Брокшмидта (Kraig Brockschmidt) Inside OLE, (Microsoft Press, 1995), Адама Денинга (Adam Denning) ActiveX Controls Inside Out, (Microsoft Press, 1997) и Тома Амст- ронга (Tom Armstrong) Designing ami Using ActiveX Controls, (IDG Books Worldwide, 1997).
Архитектура элемента управления на основе ATL
У элемента управления ActiveX есть два состояния: внешнее (то, что он выводит на экран) и внутреннее (его свойства). После того, как ActiveX-элемент входит в состав какого-нибудь контейнера (например, формы Microsoft Visual Basic или диалогового окна MFC), он поддерживает взаимоотношения с этим контейнером. Клиент обращается к элементу управления че-
рез входящие COM-интерфейсы, например, IDispatch, или через интерфейсы OLE-документов lOleObject и IDataObjecl.
Элемент управления может также обращаться к клиенту. Один из способов создать это двунаправленное взаимодействие — реализация клиентом интерфейса IDispatch, представля- . ющего набор событий элемента управления. Контейнер поддерживает набор свойств, назы- ваемыхродительскгши (ambient), через которые элемент управления может получить информацию о своем владельце. Например, элемент управления может замаскировать себя внутри контейнера, поскольку контейнер предоставляет информацию об этих свойствах через /Dis- patch-Wi ерфейс со специальным именем. Контейнер может реализовать интерфейс IProper- tyNotifySink для получения уведомлений об изменении свойств элемента управления. Наконец, контейнер реализует интерфейсы lOleClientSiteK lOleCuntrolSile как. часть протокола внедрения объектов.
Перечисленные интерфейсы позволяют клиенту и объекту демонстрировать поведение, которое ожидают от ActiveX-элемента. Позже мы подробнее рассмотрим некоторые из этих интерфейсов. А начать лучше всего с класса CComConfrol и его базовых классов.
CComControl
Определение CComConfrol находится в файле ATLCTL.il в подкаталоге Include каталога ATL, CComControl — это класс-шаблон с одним параметром:
template <class Т>
■class ATL_NO_VTABLE CComControl : public CComControlBase. public CWindowImpl<T> {
i;.
Сам по себе этот класс мало что делает — он наследует функциональность от ССот- ControlBase и. CWindowImpl. Параметром шаблона должен быть COM-объект на основе ATL, производный от CComObjeclRootExiEcTb много причин, по которым необходим параметр шаблона, но основная заключается в том, что классу нужно обращаться к InternalQuerylnterface элемента управления.
CComConfrol реализует несколько функций, облегчающих элементу управления обращение к клиенту. Например, есть функцияFireOnRequestEdit,позволяющая сообщить клиенту, что будет изменено заданное свойство. Эта функция обращается к клиенту через его интерфейс IPropertyNotijySinkYx. объекту может быть подсоединено несколько клиентов с интерфейсами IPropertyNotijySinkg. ьсе они получат уведомление о предстоящем изменении свойства с определенным DISPID.•'’
Есть также функция FireOnChanged, которая тоже использует интерфейс IPropertyNotify Sink для уведомления клиента, однако она сообщает об уже произошедшем изменении свойства с определенным DISPID.
Помимо увязки интерфейса IPropertyNotpySink с некоторыми легко понимаемыми функциями, CComControl реализует функцию ControlQuerylnterface,которая просто переадресует вызов интерфейсу IUnknown элемента управления. (Таким образом Вы мажете получить интерфейс IUnknown элемента управления изнутри самого элемента.) Наконец, CComConfrol реализует функцию CreateControIWindow, которая по умолчанию вызывает CWindowImpl:: Create (обратите внимание, что CComConfrol производен и от CWindowImpl). Переопределите эту функцию, если нужно что-нибудь еще помимо создания единственного окна. Например, для своего элемента управления можете создать несколько окон.
Большинство возможностей CComConfrol наследует из базовых классов — CComControlBase и CWindowImpl, Давайте теперь взглянем на них.
< CComControlBase
'. ’оль этого класса значительнее, чем у CComControl. Начнем с того, что в нем содержаться все 1 тсазатели, используемые элементом управления для обращения к клиенту. CComControlBase , гспользует smart-указатели типа CComPtr для переменных-членов, являющихся оболочками интерфейсов вызова клиента:
II оболочка для IQlelnPluceSite (m_splnPlaceSitey
И уведомитель об изменениях данных (data advise holder) для клиентского получателя уведомлений об изменениях данных (data advise sink) (m_spDataAdviseHoldery
■ уведомитель об OLE-изменениях (OLE advise holder) для клиентского получателяуведом- лений об OLE-изменениях (OLE advise sink) (m_spOleAduiseHoldery,
и оболочка для lOleClientSite (m_spClientSiley
II оболочка для lAdviseSink (m_spAdviseSink').
Кроме того, CComControlBase использует CComDispatcbDrivery^a создания оболочки клиентского disp-интерфейса с целью доступа к родительским (ambient) свойствам.
В CComControlBase есть также переменные-члены для хранения информации о положении и размере элемента управления: m_sizeNatural,m_sizeExtentM. m_rcPos. Наконец, в нем есть также переменная-член для описателя окна. Большинству ActiveX-элементов нужен : [ользовательский интерфейс и, следовательно, необходимо окно. Все детали работы с окна- ?ли в элементах управления ActiveX на базе ATL обрабатывают CWindowImpl и । CWindowImplBase.
CWindowImpl и CWIndowImpiBase
< Ж/пгк>»2п?р/производен от CWindowlmplBase, который, в свою очередь, производен от CWin- dow и СлМе^^аИяр.Параметром класса-шаблона СЖигйлу/трйвляется создаваемый элемент ’-правления, к которому происходит обращение во время создания окна. Давайте поближе познакомимся с реализацией окон в ATL.
Окна в ATL
। "ComControl — эго относительно простой класс (большая часть работы происходит в ССот- < 'ontrolBase'). CWindowImpl тоже относительно прост — в нем обрабатывается только создание окна. Фактически, эго единственная явно определенная в этом классе функция. GWindow- ImpLCreateсоздает новое окно на основе информации, содержащейся в структуре_ATL_WND- CLASSINFOjphl которой есть версии для ASCII-символов и широких символов.
:truer _ATL_WNDCLASSINFOA
WNDCLASSEXA m.wc;
LPCSTR m_lpszOrigName;■
WNDPROC pWndProc;
LPCSTR m_lpszCursorID;
BOOL m_bSystemCursor;
ATOM ncatom;
CHAR m_szAutoName[13];
ATOM Register(WNDPROC* p)
{
return AtLModuleReglstcrWndClassInfoA(&_Module, this, p);
struct _ATL_WNDCLASSINFOW
WNDCLASSEXW m_wc:
lpcwstr m_lpszOrigName;
WNDPROC ph.idProc;
LPCWSTR m_lpszCursorID;
bool m_bSystemCursor;■'
ATOM HL atom;
WCHAR id szAutoNamef13];
ATOM Register(WNDPROC* p)
{.
return AtlModuleRegisterWndClassInfoW(& Module, this, p);
)
};
Далее ATL при помощи операторов typedef создает синонимы для каждой структуры и сводит их в один класс CWndClassInfo:.
typedef _ATL_WNDCLASSINFOA CWndClassInfoA;
typedef _ATL_WNDCLASSINFOW CWndClassInfoW;
tfifdefUNICODE
Mefine CWndClassInfo CWndClassInfoW
#else
#defineCWndClasslnfoCWndClasslnfoA fiendif
Для добавления информации о классе окна в производный класс используется макрос DEC1ARE_WND_CLASS.9tct же макрос добавляет функцию GetWndClassIn/o-.
tfdefine PECLARE_WND_CLASS(WndClassName) \
Static CWndClassInfoW GetWndClassInfo() \
static CWndClassInfo wc = \
\sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_D8LCLKS, \
StartWindowPrdc, 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW+ 1), \ NULL, WndClassName, NULL }, NULL, NULL, IDC_ARROW, TRUE, 0, _T(”-) \ }= \ return wc; \
}
При раскрытии этого макроса в классе элемента управления создается структура CWndClassInfo, Поскольку CWndClassInfo содержит информацию для одного класса окна, то каждое окпо, создаваемое при помощи конкретного экземпляра CWindowImpl, будет основано на одном и том же классе окна.
CWindowImplnpO№BOfiCH от CWindowImplBaseT, который производен от CWindowImplRoot с параметрами шаблона CWindow и CControlWinTVaits.
template Cclass TBase = CWindow, class TWinTraits = CControlWinTraits>
class ATL_NO_VTABLE CWindowImplBaseT :
public CWindowlmplRoot< TBase >
public:■
CWindowImplBoot производен (по умолчанию) от CWindow и от CMessageMap. CWindowImpIBaseT содержит оконную процедуру класса, производного от CWindowImpl. CWindow - это простой класс-оболочка описателя окна, похожий на GWndws MFC, но с меньшими возможностями. В CMessageMap определена только одна чисто виртуальная функция Process- WindowMessage.^AexaHHSM. таблиц сообщений в ATL предполагает наличие этой функции в классе, обрабатывающем сообщения, поэтому такой класс должен быть производным от CMessageMap. Давайте бросим беглый взгляд на таблицы сообщений в ATL.
Таблицы со общений bATL
। Основа механизма таблиц сообщений АП. находится в классе СМай^аМлр.Элементы управления, создаваемые на основе ATL, поддерживают таблицы сообщений, наследуя от CWindow- implBase. В MFC, для сравнения, это делается наследованием от CCmdTarget.Однако, каки в MFC, в ATL недостаточно просто быть производным от соответствующего класса — надо, чтобы таблицы сообщений были в классе, и это реализуется при помощи макросов.
Сначала нужно поместить в заголовочный файла класса элемента управления макрос tfEGINJdSGJMAP, который помечает начало таблицы сообщений по умолчанию. CWindow- bnpl:;W!ndouProcttcnoxb3^ эту таблицу для обработки сообщений, посылаемых окну путем вызова соответствующей функции-обработчика либо передачи сообщения в другую таблицу. , (ля пометки окончания таблицы используется макрос END_MSG_MAPMc«AyBEGIN_MSG_MAP и END_MSG_MA№.oexhh. располагаться другие макросы, отображающие сообщения на фун- 1 щии-члены элемента управления. Вот типичная таблица сообщений для элемента управле- ] [ия на основе ATL:
IEGrN_MSG_MAP(CAFullControl>
CHAIN_MSG_MAP(CComControl<CAFullConfol>)
DEFAULT_REFLECTION_HANDLEF()
MESSAGE hANDLER(W№_TIMEH, OnTi-пег);
MESSAGE HANDLER(hlM_LBUnONDOWN, OnLButtan);
END_MSG_MAP()
Данная таблица сообщений делегирует обработку большинства сообщений базовому классу при помощи макроса CHAIN_MSG_MAIM обрабатывает возвращенные сообщения благодаря макросу DEFAULT_REFLECTION_HANDLEK$OMe того, два сообщения обрабатываются напрямую: WM_TIMER и WM_LBL'TrONDOW>J. Это п андарч ныс Windows-сообщения, сопоставляемые функциям-членам класса с помощью макроса MESSAGE-HANDLERHomhmo обычных сообщений, таблицы могут поддерживать обработку других событий. В таблице перечислены макросы, которые могут быть в таблице сообщений.
Макрос
Описание
MESSAGE-HANDLER
Сопоставляет функцпю-обработчик сообщению Windows
Л tESSAGF. JIANGE_HANDLRR
Сопоставляет функцию-обработчик непрерывному диапазону сообщений Windows
C OMMAND-HANDLER
Сопоставляет функцию-обработчик сообщению WM_COMMAND на основе идентификатора и кода уведомления элемента меню, элемента управления или акселератора
с 'OMMANDJD-HANDLER
Сопоставляет функцию-обработчик сообщению WM COMMAND на ос- ■ Нове идентификатора элемента меню, элемента управления или акселератора
C OMMAND_CODE_HANDLER
Сопоставляет фунюдию-сбработчик сообщению WM^COMMAND на основе кода уведомления
COMMAND _RANGE-.HANDLER
Сопоставляет функиию-обрабстчик непрерывному диапазону сообщений \VM_COMMANP на основе идентификатора элемента меню, элемента управления или акселератора
? OT1FY-HANDLER
Сопоставляет функцию-обработчик сообщению "SX'M_NOTIFY па основе кода уведомления и идентификатора элемента управления
JI OTIFY JD_HANDI£R
Сопоставляет функцию-обработчик сообщению WM_NOTIFY на основе идентификатора элемента управления
NOUFY-CODE HANDLER
Сопоставляет функцию-обработчик сообщению WM_NOTIFY на основе кода уведомления■
notify_range_handler
Сопоставляет функцию-обработчик непрерывному диапазону сообщений WM_NOTIFY на основе идентификатора элемента управления
Обработка сообщений внутри ATL работает точно также, как в MFC. ATL поддерживает единую оконную процедуру, через которую распределяются сообщения, Несомненно, Вы можете создавать эффективные элементы управления без всеобъемлющего понимания ATL- архитектуры элементов управления. Но такое знание будет полезно при проектировании и особенно при отладке элемента управления.
Проектирование элемента управления
После размещения элемента управления в исполняемом модуле необходимо добавить к элементу код, чтобы он делал хоть что-нибудь Если Вы откомпилируете создаваемый по умолчанию элемент управления и загрузите его в контейнер, то результат будет не очень привлекателен — Вы увидите пустой прямоугольник с надписью «ATL 3.0: ATLDlceOb». Нужно добавить код для отрисовки элемента управления, описания его внутреннего состояния, отклика на события и генерации событий, отправляемых обратно контейнеру.
Отрисовка
Рекомендуем Вам начать работу над элементом управления с его кода отрисовки — Вы получите истинное удовольствие. Наш элемент управления визуально представляет пару игральных кубиков. Самый простой способ отрисовки такого элемента — создать растровые изображения каждой из шести сторон кубика и затем вывести изображения на экран. Подразумевается, что элемент управления поддерживает некоторые переменные-члены, описывающие его состояние. К ним относятся растровые изображения каждой из сторон, а также два числа, представляющие начальное значение, которое будет показано каждым кубиком. Вот код из ATLDTCEOBJ.H, описывающий состояние элемента управления:
«define MAXJIEFACES 6
HBITMAP m_dieBltmaps[MAX_DIEFACES];
unsigned short m_nFirstDieValue;
unsigned short m_nSecondDieValue;
Прежде, чем заняться кодом отрисовки, нужно провети некоторую подготовительную работу — загрузить растровые изображения. Каждый кубик будет показывать одну из шести .сторон, поэтому нужно по одному растровому изображению на каждую сторону. На рисунке 30-5 показана одна из сторон кубика.
Рио. 30-5. Растровое изображение для элемента управления «игральные кубики»
Если Вы будете готовить растровые изображения одно задругим, то в файле resource.!! они получат последовательные идентификаторы, что облегчит их последующую загрузку. В противном случае может потребоваться редактирование файла resource.ii, который должен содержать следующие идентификаторы:
«define IDB DICE1 207
«define IDB_DICE2 208
#definelOB_OICE3209
«define IDB.DICE4 210
«define IDB DICES 211
«define IDB_DICE6 212
Загрузить изображения довольно просто — пройдите по массиву и загрузите каждый ресурс. При сохранении растровых изображений в массиве их гораздо легче отрисовывать, чем без использования массива. Вот функция, которая помещает изображения в массив:
В)0L CATLDiceObj::LoadBitmaps() { BOOL bSuccess = TRUE;
for(int i=0; i<MAX_DIEFACES; i++) {
DeleteObjectCm_dieBitmaps[l]):
m_dieBitmaps(i] = LoadBitmap(_Mpdule.m_hInst, MAKEINTRESO'JRCE(IDB_DICE1+i)); if(!m_dieBitmaps[i]) {
::MessageBox(NULL, "Failed to load bitmaps", NULL, MB.OK);
bSuccess = FALSE;,
} } return bSuccess; }
Наилучшее место для вызова LoadBitmaps — конструктор элемента управления, показан- t ый далее. Для моделирования случайного вращения присвойте начальным значениям первого и второго кубика случайные величины между 0 и 5 (эти числа будут использованы для с трисовки элемента управления):
■class CATLDiceObj i // здесь большой список базовых классов {
CATLDiceObj С) {
LoadBitmapsO;
srand((unsigned)time(NULL));
r_nFirstDieValue = (rand() % (NAXJHE FACES)) + 1;
n nSecondOieValue = (rand() % <MAX_DIEFACES)) + 1; }
После загрузки растровых изображений нужно вывести их на экран. Элемент управления должен включать функцию отрисовки каждого кубика на основе своего текущего внутреннего состояния, Здесь Вы впервые встретитесь с механизмам отрисовки, встроенным в ATL
Одна из самых удобных вещей в элементах управления на основ ATL (и на основе MFC): весь код отрисовки размещается в одном месте — в функции OnDraw. Это виртуальная функция класса CComControlBase.
virtual HRESULT OnDraw(ATL_DRAWINFO& di);
OnDraw получает только един параметр — указатель на структуру ATL_DRAWIN’FOILomh.- мо прочего, в этой структуре содержится контекст устройства, на котором будет отрисован элемент управления.
struct ATL DRAWINFO {
UINT cbSlze;
DWORD dwDrawAspect;
LONG 1index;
DVTARGETDEVICE* ptd;
HOC HicTargetDev;
HDC hdcDraw;
LPCRECTL preBounds; //Ограничиэающий прямоугольник для отрисовки
LPCRECTL prcWBounds; //WindowOrg и Ext если метафайл
BOOL bOptimize:■
BOOL bZoomed;
BOOL bRectlnHimetrlc;
SIZEL ZoomNum; //ZoomX = ZoomNum. cx/ZoomNum.cy
SIZEL ZoomDen;
V.
Как видите, здесь гораздо больше информации, чем просто контекст устройства. Хотя Вы можете предполагать, что каркас правильно заполняет эту структуру, но все же хорошо бы знать, откуда поступает данная информация.
ActiveX-элементы интересны тем, что отрисовываются в двух контекстах. Первый и наиболее очевидный — элемент управления активен и отрисовывается внутри клиента. Другой, менее очевидный — элемент управления отрисовывается в период проектирования (например, ActiveX-элемент внутри формы Visual Basic в режиме проектирования). В первом случае элемент управления выводит себя в контекст устройства «экран»; во втором — в контекст устройства «метафайл».
Многие (но не все) элементы управления на базе ATL формируют, по крайней мере, одно окно. Поэтому им необходимо отрисовать себя в ответ на сообщение WM PAINT. При получении этого сообщения система распределения сообщений вызывает CComControlBase:. On- Paint. (Помните, что CComControlBase является одним из базовых классов.) Эта функция выполняет несколько действий. Сначала создается контекст устройства (используя BeginPaint}. Затем на стеке создается структура ATLJDRAWINFOh. инициализируется так, чтобы показать все содержимое объекта (полю ^ЛгамАзрес^присваивается значение DVASPEC7 CONTHN'i*^. Полю lindex присваивается значение -1, поле bdcDraw инициализируется созданным контекстом устройства, а ограничивающий прямоугольник устанавливается равным клиентской области окна элемента управления. Далее OnPaint вызывает OnDrawAdvattced.
По умолчанию функция Ои/^г^гсА^яисес/подготавливаетнормализованный контекстус- тройства для отрисовки. Можете переопределить эту функцию, если хотите использовать контекст устройства, переданный контейнером, а не нормализованный. Далее ATL вызывает метод OnDraw элемента управления.
Второй контекст, в котором вызывается функция OnDraw, — эго когда элемент управления рисует в метафайл. Такая отрисовка происходит при вызове $ViewObjectiix::Draw. (JView- ObjectEx — это один из интерфейсов, реализуемых элементом управления.) ATL реализует интерфейс HViewObjectExB классе-шаблоне IViewObjectExlmpl. Функция Draw этого класса вызывается всякий раз, когда элементу управления нужно передать контейнеру для сохранения «снимок» своего внешнего вида. В этом случае контейнер создает контекст устройства «метафайл» и передает его элементу управления. 1У;еиЮЬ}е<1Ех1тр<п.ом.ещает на стек структуру ATL_DRAWINFOvt инициализирует ее значениями ограничивающего прямоугольника, индекса, аспекта отрисовки и контекста устройства, переданными клиентом. Далее отрисовка выполняется аналогично — элемент управления вызывает функцию OnDrawAduanced, которая вызывает вашу версию OnDraw.
Вооружившись этим знанием, легко написать функции для отрисовки растровых изображений. Для рисования грани первого кубика создайте контекст устройства в памяти, выберите объект в контекст устройства и выполните операцию копирования битов iBitBlt)Ksa- текста устройства в памяти на реальный контекст устройства.
void CAU.DiceObj: :ShowFirstDieFace(ATl DRAWINFO& di) {
BITMAP brnlnfo;
GetOb"ect(m_dic9itmaps[m_nFlrstDieValue-1], slzacf(bmlnfo), AbmTn^o);
SIZE size;
size, ex = Mnfo.bmh'idth;
size.cy = bmlnfo. or.Height;
HDC hMemDC;
hMemDC = CreateCompatibleDC(ai.hdcDraw);.
FBITMAP hOldBitrap;
HBJTMAP hbm = .T_die3itmaps[m_nFirstDicVal'je-1];
HOldBitmap - (HBITMAP)SolectObject(htemDC, hbm);
if (HOldBitmap == NULL)
return; // деструкторы выполнят очистку
BitBlt(di.hdcDraw, di.prcBounds->left+1, di.nrcBounds->top+1, size.ox, size.cy, hMemDC, 0, 0, SRCCGPY);
SelectObject(di.hdcDraw, hOldBitmap);
DeleteDC(hMemOC);
Рисование грани второго кубика — эго практически тот же самый процесс; только убедитесь, что второй кубик выводится отдельно от первого. Скорее всего, Вам придется изменить код вызова BiiBlt так, чтобы два растровых изображения были показаны рядом.
'void CATLDiceObj v.ShowSecond()ieFac6(ATL_0RAWIUF0& di) {
// Код тот же самый, что и в ShowFirstDieFace. нс- второй кубик размещается рядом с первым.
//
BiTBlt(di.hdcDraw, di.prcBounds->lefT+size.ex + 2, di.prcBounds->top+1,
size.ex, size.cy, hMemDC, 0, 0, SRCCOPY);'
// Остальной код тот же самый, что и в ShowFirstDieFace
Последний этап — вызов этих двух функций каждый раз, когда элемент управления должен отрисовать себя, т. е. в функции OnDraw. ShowFirstDieFace и ShowSecondDieFace будут г оказывать необходимые растровые изображения, основываясь на состоянии m_nFirstDieValue и mjiSecondDieValue.
FRESULT CATLDiceObj::OnDraw(ATL_DRAWINFO& di) {
ShowFirst0ieFacc(di);
ShowSecondDieFace(di); return S_OK; .
Если Вы скомпилируете и загрузите этот элемент управления в какой-нибудь контейнер ActiveX-элементов (например, в форму Visual Basic или диалоговое окно на основе MFC), то } ввдите «лица» двух кубиков, «смотрящие» прямо на Вас. Теперь самое время добавить код для «оживления» элемента управления и вращения кубиков.
Отклик на оконные сообщения
Просто смотреть на два кубика не очень интересно — нужно заставить их работать. Лучший способ «расшевелить» кубики — использовать таймер для генерации событий, в ответна которые выводить новую пару граней кубиков. Создание Windows-таймера в элементе управле- t ия означает добавление функции для обработки сообщения и добавление макроса в таблицу сообщений. Чтобы добавить обработчик сообщения WM_TIMER, перейдите в ClassView, щелкните правой кнопкой мыши CAtlDiceOb и выберите из контекстного меню Add Windows Message Handler. В результате будет добавлен прототип функции ОпТгтег и элемент таблицы сообщений для обработки WM TIMER. Добавьте код в функцию ОпТгтег для соответствующей обработки сообщения:
L4ESULT CATLDiceObj::OnTimer(UIHT msg, WPARAM wParam, LPARAM IParam, BOOL& bHandled) { if(m_nTimesRollec > 15) {
m nTimesRolled = 0;
KlllTimer(l):
m_nFirstDieValue = (rand() % (MAX_DIEFACES)) + 1;
m_nSecondDieValue - (rand() % (MAX^DIEFACES)) + 1;
FireViewChangeO;
m_nTimesRolled++;
bHanoled = TRUE;
return 0;
Эта функция отвечает на сообщение от таймера, генерируя два случайных числа, изменяя состояние элемента управления и вызывая FireViewCbange,чтобы элемент управления отрисовал себя сам. Обратите внимание: функция уничтожает таймер, как только кубик повернется определенное число раз, Также заметьте, что обработчик сообщения сообщает каркасу, что функция успешно завершила работу, присвоив переменной ЬНашДе^ли'дЧенйе TRUE.
Теперь обратимся к элементу таблицы сообщений для WMJUMEll. Так как WM_T1MER — это обычное оконное сообщение, то для него применяется стандартный макрос MESSAGE ^HANDLER:
BEGIN MSG MAP(CATLOlceObj)
CHAIM MSG MAP(CComCofitrol<CA7L0ice0bp)
DEFAULT REFLECTIOM HANDLER}
MESSAGE HANOLER(Wi( T1IVER, OnTimer);
END_MSG_MA°()
Как ясно из этой таблицы сообщений, элемент управления уже обрабатывает многие со-. общения Windows при помощи макроса CHAIN_MSG МАР Одаака теперь пара кубиков может моделировать л ращение путем отклика на сообщение от таймера. Установка таймера вы- зываетперерисовкуновой пары граней кубиков каждые 0,25 с или около того. Конечно, нужно каким-то образом запустить вращение кубиков. Так как у нас ActiveX-элемент, то будет разумно предоставить клиенту начать этот процесс с вызова функции одного из входящих интерфейсов. При помощи ClassView добавьте функцию RollDiceK основному интерфейсу. Для этого щелкните правой кнопкой мыши интерфейс lATLDiceOhjsi выберите Add Method из появившегося меню. К элементу управления добавится функция RoilDice, в реализации которой будет устанавливаться интервал таймера. Добавьте код, выделенный полужирным:
STDMEIHODIHP CATLDiceCbj: :RollDiceO
SetTimerd, 250);
return SOK;
}
Если Бы загрузите элемент управления в контейнер, то сможете просмотреть и вызвать его методы, а, значит, и поворачивать кубики.'
В дополнение к использованию входящего интерфейса для вращения кубиков можно позволить пользователю делать это путем двойного щелчка мышью па элементе управления. Для этого просто добавьте обработчик сообщения:
LRESULT CATLDiceObj: :0nL9uttonDblCIick{UI№ uMsg, ИРАШ. «Parar, LPAfiAM IParam. 9C0L& bHandled) {
Rol ID) ce();
bHandled = TRUE;
return 0;
}
И не забудьте добавить элемент в таблицу сообщений для сообщения WM_LBUTfONDOWN
BEGIN_MSG_MАР(CATLDiceObj)
Ц Другие обработчики сообщений
MESSAGE HANDLEfl(WM_L0UT7OND8LCLK, QnLButtoaDbIClick)
ENO_MSG>AP()
При загрузке элемента управления в контейнер и двойном щелчке на нем Вы увидите поворот кубиков. Теперь, когда Вы создали код отрисовки и предоставили элементу управления возможность поворачиваться, самое время добавить несколько свойств.
Добавление свойств и страниц свойств
Вы только что видели внешнее представление ActiveX-элемента (внешнее представление - эго состояние элемента управления, отображаемое при отри совке). Кроме этого, многие ActiveX-элементы имеют внутреннее состояние, описываемое переменными, которые доступны внешнему миру через функции интерфейса. Эти внутренние переменные известны под названием свойства (properties).
Представьте простуютаблицу,реализованнуюкакэлементуправленияАсГ1УеХ. Онаимеет внешнее представление инабор внутренних переменных, описывающих ее состояние. Свойства могут включать в себя количество строк и столбцов, цвет линий, тип шрифта и т. д.
Как Вы видели в главе 29, добавление свойств в класс на основе ATL означает добавление переменных-членов и последующее использование CiassWizard для создания функций get и р идля доступа к этим переменным. Например, в элемент управления «игровые кубики» мож- н) добавить переменные-члены, описывающие цвет и допустимое количество поворотов, Эти два свойства можно легко представить как пару коротких целых:
class ATL_NO_VTABLE CATLDiceObj :
short m_nOiceColor;
short m_nTimesToRoll;
Чтобы эти свойства были доступны клиентам, нужно добавить к элементу управления функции get Hput Щелчок правой кнопкой мыши интерфейса в Class View вызывает контекстное меню, в котором выберите Add Property. Добавление свойств DiceColor и TimesToRoU при помощи ClassView создаст четыре новые <^yHM№HtgetJDiceColor,putJDiceColor,get_Times- ToRoll и putJTimesToRoll.
функция 8е1_ГМсеСо1огдолжна возвращать состояние m_?iDiceColor-.
STDMTTHODIMP CATLDiceObj;:get OiceColor(short * pVal) {
*pVal = m„nDiceColor;
returns OK;
}'
Чтобы элемент управления был интересным,рмМ9#сеСо/огдолжна изменять цвета растровых изображений и немедленно перерисовывать элемент управления. В этом примере используют :я красные и синие кубики, а также исходные черные и белые кубики. Функция загружает новое изображение в соответствии с указанным цветом и вызывает перерисовку:
STDWETHODIMP CATLDiceObj::put DiceColcr(short newVal) {
if( newVal < 3 M newVal >= 0)
e_nDiceColor = newVal;
LoadBitmaps();
FireViawChangeO;
returns OK;
Конечно, сказанное означает, что функция LoadBi1тар5должна загружать растровые изображения в соответствии со значением m_nDiceColor,поэтому нужно добавить в имеющуюся реализацию код, выделенный полужирным:
BOOL CATLDiceObj : :LoadBitmaps() {
int i;
BOOL bSuccess = TRUE; ■
int nID - I0B_DICE1;
ewitch(m_nDiceColor) {
case 0:
nID = IDB_0ICE1;
break;
case 1:
nID = IDB.BLUEDICE1;
break;
case 2;
nID - IDB_REDDICE1;
break;
for(i=0; i<MAX„DIEFACES: i++) {
DeleteObject(m„dieBitmaps[1]);
ffl_dieBitmapsii] . LoadBitmap(_Module.m_hInst, MAKEDITfiESOURCE(nID+i));
if(!m_dieBitmap3[i]) {
", :MessageBox(NULL, "Сбой при загрузке растров", NULL, МВ^ОК);
bSuccess = FALSE;
return bSuccess;
}
Точно так же, как свойство DiceCoior соответствует цвету кубика, количество допустимых поворотов кубика должно быть связано со свойством TimesToRoll. Функция get-TimesToRoH должна считывать значение m nTimesToRoll, а функцияputJTimesToRoUQ^aei изменять значение m_«Times7bRoilL Добавьте код, выделенный полужирным:
S7DMETHQ0IMP CATLOiceObj::get_TimesToRoll(short * pVal)
»pVal = m^nTieesToRoll;
return SOK;
STDHETROOINP CATLOiceObjput TimesToRoll(short newVal)
mjiTimesToRoll = newVal;
return S OK;
}“,
Наконец, вместо зашитого в код количества поворотов кубика будем использовать переменную mjiTimesTbRoll Д5«1 определения момента уничтожения таймера.
LRESULT CATLOiceObj :OnTi-ner(UIMT msg, WPARAM wParam, LPARAM IPara®, 800L& handled) {
if(m.nTimesRolled > m_nTimesToRoll) { m_nTimesflolled = 0;
KillTimerd);’
Fire_Diceflolled(m_nFirstDieValue, m_nSecondDieValue);
if(m_nFlrstDieValue == m_nSecondDieValue)
Fire_Doubles(m_nFirstOieValue);
if(ie_nFirstDieValua == 1 M m^nSecondDieValue == 1J
Fire_SnakeEyes();
> else f
®_nFirstDieValue = (rand() % (MAX DIEFACES)) + 1;
m nSecondDieValue = (rand() % (MAX_DIEFACES)> + 1;
см. след. стр.
FireVicwChangeO: m nTimesRolled++;
bHandled = TRUE;
return 0;
}
Теперь два свойства видимы внешнему миру. Когда клиент меняет цвет кубика, элемент управления загружает новый набор растровых изображений и перерисовывает кубики. Ког- даклиент меняет количество поворотов кубиков, элемент управления определяет, сколько раз нужно отвечать на сообщение WM TIMER. Далее следует естественный вопрос: как код клиента обращается к этим свойствам? Один из способов — через страницы свойств.
Страницы свойств
Поскольку ActiveX-элементы обычно имеют пользовательский интерфейс и помещаются в более крупные приложения, то они часто оказываются внутри форм Visual Basic и диалоговых окон MFC. После создания элемента управления код клиента может изменять его свойства, вызывая определенные функции входящих интерфейсов. Однако когда ActiveX-элемент находится в режиме проектирования, доступ к свойствам через функции интерфейса не всегда реален. Было бы весьма жестоко заставлять разработчиков обращаться к функциям интерфейсов всякий раз, когда надо просто взглянуть на некоторые свойства элемента управления. Почему создатель клиента должен предоставлять пользовательский интерфейс для изменения свойств элемента управления? Именно для этого предназначены страницы свойств - набор диалоговых окон, реализованных элементам управления для изменения свойств. При их использовании клиентам нет необходимости каждый раз заново создавать диалоговые окна для свойств элементов управления.
Использование страниц свойств. Есть два способа использовать страницы свойств. Первый — через интерфейс /О/еО/уес/объекта. Клиент вызывает функцию DoVerb этого интерфейса, передавая идентификатор ОЦ;Л^ЙВРК<)Рк^Т/Е^чиаювое значение -7), чтобы элемент управления показал свои страницы свойств. Элемент выводит диалоговое окно (решку свойств — property frame), содержащее все его страницы свойств. На рисунке 30-6 показано диалоговое окно Property Pages со страницами свойств элемента управления Microsoft FiexGrid 6,0.
Рис. 30-6. Сгранидь! свойств элемента управления Microsoft AexQid 6.0
Страницы свойств являются свидетельством мощи СОМ. Каждая страница свойств — отдельный COM-объект (со своим GUID и регистрацией, подобно другим COM-классам в вашей системе). Когда клиент запрашивает у ActiveX-элемента вывод его страниц свойств, элемент управления передает список соответствующих GUID в функцию OleCreatePropertyFrame, которая вызывает CoCreatelnstancepna каждой страницы свойств. Окно-рамка свойств получает копию интерфейса для изменения свойств элемента управления, Возврат из OteCreate- Ргарег^патепроисходит, когда пользователь щелкает кнопку ОК или Apply,
Второй способ использования страниц свойств клиентам состоит в запросе у элемента управления списка соответствующих GUID. Затем клиент сам вызывает CoCreateinstoncesna каждой страницы свойств и размещает страницу в собственном окне-рамке свойств. На рисунке 30-7 показан пример использования страниц свойств элемента управления Microsoft FlexGrid в диалоговом окне, которое создает Visual C++.
Рис. 30-7. Страницы свойств Microsoft FlexGrid 6.0 в д иалоговом окне Visual С-н-
Этот второй способ применяется наиболее часто. Обратите внимание: окно свойств на рисунке 30-7 содержит дополнительную вкладку General, вкладка General, показанная на рисунке 30-6, переименованавСоптгокДополнигельнаявкладкапринадлежигVisual C++. Вкладки Control, Style, Font, Color и Picture принадлежат элементу' управления (хотя они и показа- нывнутриконтекста Visual C++).
Для правильной работы страницы свойств элемент управления должен реализовать интерфейс ISpecijyProperlyPage^ сама страница свойств — интерфейс IPropertyPage. Учитывая это, давайте подробно рассмотрим, как ATL реализует страницы свойств.
Добавление страницы свойств к элементу управления. Для создания страниц свойств в проекте на основе ATL можно использовать ATL Object Wizard. Выполните следующие действия, 1. Выберите New ATL Objects меню Insert
В диалоговом окне ATL Object Wizard выберите Controls в списке Category.
Выберите Property Page в списке Objects.
Щелкните Next.
Заполните поля ввода в диалоговом окне ATL Object Wizard Properties необходимой информацией и щелкните ОК.-
Мастер ATL Object Wizard сгенерируетдиалоговый шаблон и включит его в ресурсы элемента управления. В примере с игральными кубиками мы используем два свойства: цвет кубика и количество поворотов. Создаваемый диалоговый ресурс пуст, поэтому нам нужно до-
бавить пару элементов управления, представляющих эти свойства. Пусть цвет кубика пользователь выбирает из комбинированного списка, а количество поворотов задает в поле ввода,, как показано на рисунке 30-8.
Рис. 30-8. Д иалоговый шаблон страницы свойств
Мастер ATL Object Wizard также создаст С-н--класс, реализующий интерфейсы, которые необходимы для страницы свойств, Этот класс будет сделан частью текущего проекта. Кроме Того, в раздел coclass IDL-файла будет помещен новый класс страницы свойств. А еще созданная страница свойств будет добавлена в таблицу объектов (object шар), чтобы DiKetCiass- Object могла ее найти и создать. Наконец, будет создан сценарий регистрации, чтобы в Реестре были сделаны корректные записи при регистрации элемента управления. Вот заголовочный файл, создаваемый мастером ATL Object Wizard для страницы свойств DiceMainPropPage: Uinclude "'esource.h" // main symbols
class ATL_NO_vrABLE CDiceMainPropPage :
public CComObjectPootEx<CComSingleThroadMbdel>,
public CComCoClasfxCDlceMainPropPage, &CLSID_01ceMainPropPage>, public IProDertyPageImpl<CDiceMainProDPage>,
public CDial oglmp I <CDlceMainPropPage>
public:
CDiceMainProoParaC)
m_dwTitleID - IDS„TITLEDiceMa:nPropPage;
m.dwHelpFilelD - IDS.HELPFlLEDiceMaLiPrapPage;
njjJwOocStringlD - IDS_DOCSTRINGOiceMainPropPage:
enum {100 = WO.OICEMAINPROPPAGE):
DECLARE_REG1STRY_RES0URCEID(IDR,MCEMAINPROPPA6E)
DECLAHE_PROTECT_F1NAL.CONSTRUCT()
BEGIN COM MAP(COiceMainPropPage)
COM„INTERFACE ENTRY(lPropertyPage)
END_COM_MAP()
8CGIN_YSG_MAP(CDiceMainPropPage)
CHAIN MSG XAPlIPrcperTyPagelmpKCOiceMatnPropPage?) E№)_MSG_MAP()
5TDMETH0D(Apply)(void)
ATLTRACE(_T( 'CDiceMainPropPage: :Азр1у\п“))^ for (UIHT i = 0; i < ^nObjects; i++)
{
// Сделайте здесь то, что нужно
// ICircCtl* рСггс;
П mjopUnk[i)->OueryInterface(nD.JCi rcCtl, (void«*)&pCirc).
// pGirc->put_Ca₽tion(CCamBSTR("something special"))-, // pCirc~>Release();
m.bOirty = FALSE;
return 8 OK;
Изучение этого листинга показывает, что классы страниц свойств в ATL основаны на нескольких шаблонах; CComObjeclRootEx^pva реализации IUnknown), CComCoCl«ss (фабрика класса для страницы свойств), IPropertyPagelmp^ia реализации IPropertyPage)H CUialoghnpl (для реализации специфики диалогового окна).
Как и в большинстве других COM-классов, созданных мастером ATL Object Wizard, большая часть кода для страницы свойств стандартна. За исключением конструктора и некоторых таблиц, единственной функцией является Apply.
Прежде чем начать реализовывать таблицу свойств, полезно уделить время для понимания архитектуры страниц свойств — тогда добавляемый код будет Вам более понятен.
Когда клиент решает, что нужно показать страницы свойств, он должен создать модальное диалоговое окно-рамку. Рамка создается либо клиентом, либо самим элементом управления. Если страницы свойств будут показаны через вызов функции DoVerb. то рамку создает .элемент управления. Если же страницы свойств будут показаны внутри контекста другого приложения (как,например, VisualC-н-показываетстраницысвойствэлеменговупрявления внутри IDE), то рамку создает клиент. Для каждой страницы диалоговое окно-рамка содержит посредника страницы свойств (property page site) — небольшой объект, реализующий IPropertyPageSite.
Код клиента (в данном случае модальное диалоговое окно-рамка) проходит по списку GUID, вызывает для каждого GoCreatefastance и запрашивает интерфейс IPropertyPage. Если COM-объект, созданный этимвызовом, является страницей свойств, то онреализуетуказан- ный интерфейс. Окно-рамка свойств использует интерфейс IPropertyPagegna обращения к .странице свойств. Вог его объявление.
interface IPropertyPage i public IUnknown {
HRESULT SetPageSitedPropertyPageSita-pPageSite) = 0;
HRESULT Activatfc(HWND hWndParent, LPCRECT pRect, BOOL bModal) = 0;
HRESULT Deactivate! void) = 0;
HRESULT GetPageInfo(PHOPPAGEINFO «pPagelnfo) = 0;
HRESULT SetGbjects(ULONG cObjects, IUnknown **ppUnk) = 0;
HRESULT Show( VINT nCmdShow) s 0;
HRESULT Move(LPCRECT pRect) * 0;
HRESULT IsPageDirty( void) = 0;
HRESULT Apply ( void) = 0;
HRESULT Help (LPCOLESTR pszHelpDir) = 0;
HRESULT TranslateAccelerator(MSG *pMsg) = 0;
После создания страницы свойств необходимы каналы связи между ней, клиентом и элементом управления. После успешного вызова Querylnterjac^ia IPropertyPage окно-рамка свойств вызывает для каждого указателя на IPropertyPage метод SetPageSite, передавая указатель на IPropertyPageSiteПосредники страницы свойств позволяют каждой странице свойств обратится к окну-рамке. Посредник предоставляет странице информацию и получает от нее уведомления, когда на ней происходят изменения. Вог интерфейс IPropertyPugeSite:
interface IPropertyPageSite : public IUnknown {
publjc:
virtual HRESULT GnStatusChange(DWORD A^lags) = 0;
virtual HRESULT GetLocaleID(LCID «pLocsleib) = 0;
virtual HRESULT GetPageContainerCIUnknown -ppUnk) = 0;
virtual HRESULT Transl8TeAccelerator(MS6 ‘pMsg> = 0;
Помимо связи рамки и страницы свойств через IPropertyPagen IPropertyPageSite, каждой странице нужен способ общения с элементом управления. Такая связь обычно устанавливается, когда рамка вызывает lPropertyPage::SetObjectenepesgBa& указатель на Юпкпоитэлеменга. управления. На рисунке 30-9 показана архитектура страниц свойств.
О /PropertyPageSiteО /PropertyPageSiteIUnknownIUnknownIPropertyPegeIPropertyPege1 IUnknown1 IUnknown/Kofi . .клиента/Kofi . .клиента■ ' (SpeckyPropertyPagea■ ' (SpeckyPropertyPageaРис. 30-9. Взаимодействие страниц свойств, их посредников и окна-рамкиРис. 30-9. Взаимодействие страниц свойств, их посредников и окна-рамкиРамка свойств-.Mr* .■(модальм^ёдиал^рзое окно) .Рамка свойств-.Mr* .■(модальм^ёдиал^рзое окно) .IPropertyPage Страница свойствIPropertyPage Страница свойствL ) IPropertyPageSiteL ) IPropertyPageSiteIUnknownIUnknown
О /PropertyPageSite
О /PropertyPageSite
IUnknown
IUnknown
IPropertyPege
IPropertyPege
1 IUnknown
1 IUnknown
/Kofi . .клиента
/Kofi . .клиента
■ ' (SpeckyPropertyPagea
■ ' (SpeckyPropertyPagea
Рис. 30-9. Взаимодействие страниц свойств, их посредников и окна-рамки
Рис. 30-9. Взаимодействие страниц свойств, их посредников и окна-рамки
Рамка свойств-.Mr* .■
(модальм^ёдиал^рзое окно) .
Рамка свойств-.Mr* .■
(модальм^ёдиал^рзое окно) .
IPropertyPage Страница свойств
IPropertyPage Страница свойств
L ) IPropertyPageSite
L ) IPropertyPageSite
IUnknown
IUnknown
Теперь, котда Вы понимаете, как в целом работают страницы свойств ActiveX-элемента, понять их реализацию в ATL будет гораздо легче. Сейчас мы увидим, как работают страницы свойств в ATL — как в случае, котда кед клиента дает команду элементу управления, так и в случае, когда среды типа Visual C++ интегрируют страницы свойств в ID Е.
ATL и команда Properties. Первый способ, которым ActiveX-элемент показывает свои страницы свойств, — это когда клиент дает команду properties, вызовая lOleObject.-.-DoVeiix параметром ОЬЕ1УЕРВРРОРЕРТ1Е5Ддя элемента управления на основе ATL это приводит к вызову функции CCotnControlBase::DoVerbPr6perties^.cnopas. просто вызывает функцию OleCreate- PropertyFrame,передавая в нее собственный указатель HatUnknown и список GUID страниц свойств. Для каждого GUID из списка вызывается CoCreatelnstance, и созданные страницы свойств размещаются в диалоговом окне-рамке. OleCreatePropertyFramsLayynsyet интерфейс IPropertyPage каждой страницы для управления ею, как описано в разделе «Использование страниц свойств».
Таблицы свойств. Несомненно, объяснение вызова OleOeatePropertyFrame внутри элемента управления на основе ATL вызывает вопрос; откуда берется список страниц свойств? ATL при помощи макросов создает список страниц свойств, называемый таблица свойств (property
// Сделайте здесь то, что нужно
// iCircCtJ* pCirc;
// in_ppUnk[i]->QueryInterface(IID "Ci'cCtl, (void*«)&3Circ):
Ц pCirc->put_Caption(CCom8STR("3cme’.tiing special"));
// pClrc->Release();
)
m_bDlrty = FALSE;
return S_OK;
}
Изучение этого листинга показывает, что классы страниц свойств в ATL основаны на нескольких шаблонах: CComObjectRootExljpra. реализации IUnknown), CComCoClass (фабрика класса для страницы свойств), IPropertyPagelmplj^a реализации IPropertyPage") и GDialoglmpl (для реализации специфики диалогового окна).
Как и в большинстве других COM-классов, созданных мастером ATL Object Wizard, большая часть кода для страницы свойств стандартна. За исключением конструктора и некоторых таблиц, единственной функцией является Apply.
Прежде чем начать реализовывать таблицу свойств, полезно уделить время для понимания архитектуры страниц свойств — тогда добавляемый код будет Вам более понятен.
Когда клиент решает, что нужно показать страницы свойств, он должен создать модальное диалоговое окно-рамку. Рамка создается либо клиентом, либо самим элементом управления. Если страницы свойств будут показаны через вызов функции DoVerb, то рамку создает элемент управления. Если же страницы свойств будут показаны внутри контекста другого приложения (как, например, Visual С+ + показывает страницы свойств элементов упр авления внутри IDE), то рамку создает клиент. Для каждой страницы диалоговое окно-рамка содержит посредника страницы свойств (property page site) — небольшой объект, реализующий IPrapertyPageSite.
Код клиента (в данном случае модальное диалоговое окно-рамка) проходит по списку GUID, вызывает для каждого CoCreatelnstanceH запрашивает интерфейс IProperi) 'Page.Если COM-объект, созданный этим вызовом, является страницей свойств, то он реализует указанный интерфейс. Окно-рамка свойств использует интерфейс IPropertyPage для обращения к странице свойств. Вот его объявление.
interface IPropertyPage : public IUnknown {
HRESULT SetPageSlte(IPropertyPageSlte *pPageSite) = 0;
«RESULT Activatp(HWNO hWndParent, LPCRECT pRect, BOOLbModal) = 0;
«RESULT Deactivate ( void) = 0;
«RESULT GetPagelnfо(PROPPAGEINFO *pPage!nfo) = 0;
HRESULT SetObjects(ULONG cObjects, IUnknown •♦poUnk) = 0;
HRESULT Show(UINT nCmdShow) = 0;
HRESULT Mcve(LPCRECT pRect) = 0;
iHRESULT IsPageDirtyC void) = 0;
HRESULT ApplyC void) = 0;
HRESULT Help(LPCOLESTR pszHelpDir) - 0;
HRESULT TranslateAccelerator(MSG *pMsg) = 0;
После создания страницы свойств необходимы каналы связи между ней, клиентом и элементом управления. После успешного вызова Querylnterjacwm IPropertyPage окно-рамка свойств вызывает для каждого указателя на IPropertyPage метод SetPageSite, передавая указатель на 1РгореггуРа§е5кеГ[ссредаики страницы свойств позволяют каждой странице свойств обратится к окну-рамке. Посредник предоставляет странице информацию и получает от нее уведомления, когда на ней происходят изменения. Вот интерфейс IPropertyPageSite-.
628628ЧАСТЬ IV СОМ, Автоматизация и OLE •■»■
628
628
interface IPropertyPageSite : public IUnknown {
public:
virtual HRESULT OnStatusChan0e(DWORD dwFlags) = 0;
virtual HRESULT GetLocaleID(LCID *pLocaleID) = 0;
virtual HRESULT GetPageCcntainer(IUnknown <ppUnk) = 0;
virtual HRESULT TranslateAccelerator(kSG *pHsg) = 0;
};
Помимо связи рамки и страницы свойств через IPropertyPagea IPropertyPageSite, каждой странице нужен способ общения с элементом управления. Такая связь обычно устанавливается, когда рамка вызываетIPropertyPagecSetObjectsRepeRaBas указатель на IUnknown элемента управления. На рисунке 3Q-9 показана архитектура страниц свойств.
Ric. 30-9. Взаимодействие страниц свойств, их посредников и окна-рамкиRic. 30-9. Взаимодействие страниц свойств, их посредников и окна-рамки
Ric. 30-9. Взаимодействие страниц свойств, их посредников и окна-рамки
Ric. 30-9. Взаимодействие страниц свойств, их посредников и окна-рамки
Теперь, когда Вы понимаете, как в целом работают страницы свойств ActiveX-элемента, понять их реализацию в ATL будет гораздо легче. Сейчас мы увидим, как работают страницы свойств в ATL — как в случае, когда код клиента дает команду элементу' управления, так и в случае, когда среды типа Visual C++ интегрируют страницы свойств в IDE
ATL и команда Properties. Первый способ, которым ActiveX-элемент показывает свои страницы свойств, — это кода клиент дает команду properties, визовая lOleObjectcDoVerbc параметром OLEIVERB_PROPERTIE%na элемента управления на основе ATL это приводит к вызову функции CComCuntrotBase^DuVerbPropmiesyLOTOpa» просто вызывает функцию OleCreate- РгорепуЕгате.передавая в нее собственный указатель наШпкпоити список GUID страниц свойств. Для каждого GUID из списка вызывается CoCreatelnstance, и созданные страницы свойств размещаются в диалоговом окне-рамке. OieCreatePropertyFramaicitonbsyeT интерфейс IPropertyPagg каждой страницы для управления ею, как описано в разделе «Использование страниц свойств».
Таблицы свойств. Несомненно, объяснение вызова OleCreatePropertyFrame внутри элемента управления на основе ATL вызывает вопрос: откуда берется список страниц свойств? ATL при помощи макросов создает список страниц свойств, называемый таблица свойств (property
map). Всякий раз, котда Вы добавляете к элементу управления новую страницу свойств, необходимо обновить список страниц свойств, формируемый этими макросами. ATI- включает следующие макросы для реализации таблиц свойств: BEGIN PROPERTY MAPPROP ENTRY, PROP_ENTRY_EX,PROP_PAGE и END-PROPERTY_MAP\---
struct ATL PROPMAP ENTRY {
LPC0LE8TR szDesc;
DISPID dispid;
const CLSID* pclsidPropPage;
const IID* piidDispatch;
DWORD dwOffsetData;
DWORD dwSlzeData:
VARTYPEvt;
};
«define BEGIN PROPERTY MAP(theClass) \
typedef .ATL.PROP_NOTlFY^EVENT.CLASS _ATL_PROP.NOTlFY_EVENT_CLASS; \ typedef theClass .PropMapClass; \
static ATL PROPMAP ENTRY- GetPropertyMap()\
<\'
static ATL PROPMAP.ENTRY pPropMap[] = \ {
«define PROP. PAGE (clsid) \
{NULL, NULL, Aclsid, &IID.NULL},
«define PROP_ENTRY(szDesc, dispid, clsid) \
{OLESTR(szDesc), dispid, &clsid, &IID_IDispatch).
«define PROP.ENTRY_EX(szDesc, dispid, clsid, iidQispatch) \,
{OLESTR(szDesc), dispid, Aclsid, AiidDispatch),
«define END PROPERTY MAP() \
{NULL, 0, NOLL, SIID NULL} \
}; \
return pPropMap; \
}.
Когда Вы решите добавить страницы свойств в COM-класс на основе ATL, поместите эти макросы в заголовочный файл класса. Например, если нужно добавить страницы свойств в элемент управления «игральные кубики», то поместите в С-н-кдасс следующий код
class ATL.NO.VTABLE CATLOiceObj :
•I
BEGIN JW_MAP(CATLDiceObj)
PROP_ENTRY("Caption goes here.,.”, 2, CLSID.MainPropPage)
PROP ENTRY_EX("Caption goes here-.", 3, CLSID.SecondPropPage, DIID.SecondDuallnterface) PROP PAGE(CLSID.StockColorPage)
ENO_PROPERTY_MAP()
}:
На самом деле эго список свойств, а не страниц, но в котором для каждого свойства может быть
указан GUID страницы свойств. — Прим.перев.
Макросы таблицы свойств задают список GU1D, соответствующих страницам свойств. Таблицы состоят из массива структур A TLJPROPMAPENTRYMaKpoc BEGIN_PROPERTY_MAR^bHB- ляет статический массив этих структур. РАО^/ИС&ставляет GUID в список страниц свойств. РЕОР_ЕМТНУвс1авляег в список GUID страницы свойств и свойство, ассоциированное со страницей. РРОР_ЕМГРУ_ЕХ1озволяет связать со страницей определенный дуальный интерфейс. Когда код клиента выдает команду properties, элемент управления просто проходит по списку GUID и передает их в OleCreatePropertyFramgyiK создания страниц свойств.
Страницу свойств и средства разработки. Выполнение команды properties — не единственный способ для ActiveX-элемента показать свои страницы свойств. Как уже говорилось ранее, авторам средств разработки (таким как Visual Basic и Visual C++) может потребоваться программный доступ к страницам свойств элемента управления. Например, при использовании MFC для создания диалогового окна с ActiveX-элементом щелчок прав ой кнопкой мыши на элементе позволит вызвать диалоговое окно-рамку, создаваемое Visual C++ (а не вызовом OleCreatePropertyFrame).
Visual C++ использует интерфейс ISpeciJyProperlyPag®ia получения списка GUID (создаваемого макросами таблицы свойств). Вог определение этого интерфейса:
interface ISpecifyPropertyPages : public IUnknown {
HRESULT GetPages(CAUUID -pPages);
typedef struct tagCAUUID
ULONG cElems;
GUID FAR* pElems;
} CAUUID;
ATL реализует функцию ISpecijyPropertyPages:;GetPageHppe6$Lpaa список GUID, формируемый макросами таблицы свойств, и возвращая их в структуре CAUUID. Среды подобные Visual C++ используют каждый GUID в вызове CoCrcatelnstance для создания новой страницы свойств. Посредник страницы свойств и сама страница обмениваются интерфейсами. Посредник сохраняет указатель на интерфейс 1РгореПуРарестранищя свойств, а страница свойств хранит указатель на интерфейс IPropertyPageSitmocpeOTHKa. После того, как диалоговое окно- рамка создаст страницы свойств, оно должно отобразить текущее состояние ActiveX-элемента. Для этого необходимо переопределить метод Sbow страницы свойств.
Отображение страницы свойств. Метод Sbow страницы свойств вызывается перед всяким отображением страницы свойств. Страница свойств в этот момент должна сделать выборку значений из ActiveX-элемента и передать их элементам управления страницы свойств, Вспомните, что страница свойств хранит массив указателей на IUnknown (в переменной-члене m^ррипккласса 1РгореиуРа§е1тр1)Дря доступа к свойствам ActiveX-элемента необходимо вызвать Querylntei^aceqepes зги указатели и запросить интерфейс, предоставляющий свойства. В нашем случае это будет LATLDiceObj. Получив интерфейс, страница свойств может выбрать свойства и поместить значения в элементы управления диалогового окна. Вог переопределенный метод Sbow:
^include "atldicesrvr.h"
class ATL_N0_VTA8LE CDiceMainPropPage :
public CComObjectRootEx<CComSingleThreadMadel>,•
public CComCoClass<GDiceMainPrapPage, &CLSID_OiceMainPropPage>,
public IPropertyPageImpl<CDiceMainPropPase>.
public COi alog Imp IcCDiceMai nP ropPage>
STDMETHOD(Show)( UINT nCmdShow ) {
HRESULT hr:
USES_CONVERSION;
if(nCmdShok == SW.SHOW 11
nCmdShow == SW_SHOWNDRMAL) {
for (UINT i ~ 0; i < m„nObjects; i++)
{"
CComQIPtr< lATLDieceObj, &IID_IATLDieceObj > pATLDiceOb(mj)pUnk[i]);
short nColor = 0;
if FAILED(pATLDiceOo->gct DiccCtlor(inColor)) ■i
CComPtr<IErrorInfo> pError;
CConBSTR strError;
GetErrorInfo(0, ipError);
pError->GetDescription(istrError);
Message8cx(0LE2T(strError), _T("Error"), MB_ICONEXClAMATION); return E FAIL;
HWND hWndCombo3ox - GetDlgItem(IDC_COLOR);
: :SendMessage(WndComboBox, CB^SHCURSEL, nColor, 0);
short nTiresToRoll = 0;
if FAILED(
pATLDiceOb->gct_TimesToRoll(&nTimesToRoll))
CComPtr<IErrorInfo> pError;
CComBSTR strError;
GetErrorInfo(0, ipError);
pError->GeTDescription(&strError);
Message8ox(0LE2T(strError), _T("Error" ), MB_ICONEXCLAMATI()N);
return E_FAIL;
SetDlgItemInt(IDC TIMESTOROLL. nTlnesToRoll, FALSE);
}
}
m_bOirty - FALSE;
hr = IPropertyPageImpl<CDiceMainPropPage>: :Show(nCmdShow);
return hr;
Помимо добавления кода подготовки к отображению нужно добавить код, позволяющий изменять свойства элемента управления. При всяком изменении свойства диалоговое окно активизирует кнопку Apply, сообщая, что пользователь может применить (apply) только что заданные свойства. Когда пользователь нажимает кнопку Apply, вызывается одноименная функция страницы свойств, поэтому в нее нужно добавить код
Обработка кнопки Apply. После того, как пользователь закончит установку свойств, он для сохранения изменений щелкает либо кнопку Apply либо кнопку ОК В ответ код клиента просит страницу свойств применить новые свойства к элементу управления. Вспомните, что ActiveX-элемент и страница свойств являются отдельными COM-объектами, поэтому они должны взаимодействовать через интерфейсы.
При создании страницы свойств с помощью ATL Object Wizard переопределяется функция Jppfy интерфейса IPropertyPage. Посредник страницы свойств использует эту функцию для уведомления страницы свойств об изменениях, которые нужно сделать в элементе управ-
ления. Когда вызывается функция Apply страницы свойств, необходимо синхронизировать состояние страницы свойств с состоянием элемента управления. Вспомните, что «в начале игры* указатель на интерфейс IUnknown элемента управления был передан странице свойств в вызове iPropertyPage: NetObjects. (Указатели на интерфейсы хранятся в массиве т_ppUnk страницы свойств.) Большинство страниц свойств при вызове функции Apply устанавливает состояние свойств элемента управления, используя переданный интерфейс. В нашем случае это означает, что нужно получить значения в комбинированном списке и поле ввода и установить новые значения внутри элемента управления:
«include "atldicesrvr.h-’-
class ATL_NO_VTABLE CDiceMainPropPage :
public CComObjectRootEx<CComSingleThreadModeI>,
public CComCoClass<CDiceMainPropPage, &CLSID_DiceMainPropPage>,
public IPropertyPageImpl<CDiceMainPropPage>,■ -
public CDialogImpKCDiceMainPropPage> { .
STOMETHOD(Apply)(void)
USES_CONVERSION;
ATLTRACE(_T( "CDiceMainPropPage:: ApplyXn" ) );
for (UINTi = 0; i Cm nObjects; i++) i
CComOIPtrcIATLDieceObj.
AIID_IATLpieceObj> pATLDiceOb(m_ppUnk[i]);
HWND hWndComboBox = GetDlgItem(lDC_COlOR);
short riColor = (short)::SendMessage(hWndComboBox, CB_GETCURSEL, 0, 0);
lf(nColor >= 0 && nColor <= 2) {'
if FAILED(pATLDiceOb->put DiceColor(nColor))
CComPtr<IErrorInfo> pError;
CComBSTRstrError;
GetErrorInfo(0, ApErrpr);
pError->GetDescrlption(&strError);
MessageBox(OLE2T(strError), _T("Error"), MB_ICONEXCLAMATION); return E_FAIL;
}
'
short nUmesToRoll = (short )GetOlgItemIriT (IDC.TIMESTOROLL);
if FAILED(pATLDiceOb->put_TImesToRol1(nlImesToRol1)) I
CComPtr<IErrorInfo> pError;
CComBSTRstrError;
GetErrorInfo(0, SpError);
pError->GBtDescription(8strError);
MessageBpx(0LE2T(strError), _T("Error"), MB_ICONEXCLAMATION); return E FAIL;
}'
} m_bDirty = FALSE;
return S_OK;'
Устойчивость свойств
11осле добавления свойств в элемент управления было бы логично сделать так, чтобы эти < войства не менялись внутри контейнера. Например, представьте, что Hasbro купила ваш эле-
мент управления «игральные кубики» для включения его в новую версии «Монополии». Элемент данных вставляется в одно из диалоговых окон «Монополии» и настраивается на синий цвет кубиков и на 23 допустимых поворота. Если бы у элемента управления было свойство «звук», то авторы «Монополии» могли бы настроить его на выдачу сигнала при каждом повороте. Когда игрок «бросает» кубики, он видит пару синих кубиков, поворачивающихся 23 раза прежде, чем остановиться, и он слышит звуковой сигнал до тех пор, пока кубики вращаются. Надеемся, Вы помните, что все эти возможности являются свойствами элемента управления. При добавлении элемента управления в приложение Вам наверняка захочется, чтобы эти свойства сохранялись вместе с приложением.
Добавить поддержку устойчивости к элементу управления очень легко, используя макросы таблицы свойств из ATL. Вы уже видели, как добавить страницы свойств к элементу управления при помощи этих макросов. Они же делают свойства устойчивыми.
Код ATL для поддержки устойчивости свойств элемента управления находится в классе CCotnCotiholBase. В этом классе есть функция lPersistStreatnlnit_Save#$3iapaa записывает свойства в поток (stream), предоставляемый клиентом. При всяком вызове контейнером IPersist- StreamIntt::Save, в конечном счете, ATL вызывает IPersisLStreamlnitJ^aue.KOTopas. использует таблииусвойств (property map) элемента управления, (Вы помните, что макрос BEGIN_PRO- PERTY_MAP добавляет к элементу управления функцию СегРгорег(уМйр.)Сначала IPersist- StreamInil_Sat>e3aitHCHBaeT размеры окна на экране, а затем проходит по таблице свойств и записывает ее содержимое в поток. Для каждого свойства элемент управления вызывает Querylnterfac^ самого себя, чтобы получить disp-интерфейс, а затем вызывает lDispatch::In- voke на основе DISPID, ассоциированного со свойством. (DISP1D каждого свойства хранится в таблице свойств.) Результат вызова/О«/>«гс&-;7иуо&е— значение типа Variant, которое IPersist- $1геат1пИ_5аиезаписывает в поток, предоставляемый клиентом.
Двустороннее взаимодействие (события)
Теперь элемент управления «игральные кубики* имеет свойства и страницы свойств, а также отрисовывает себя на контексте устройства. Последнее, что осталось сделать — добавить несколько событий (events). События позволяют элементу управления обратиться к коду клиента и информировать его о произошедшем.
Например, пользователь может повернуть кубик Когда кубик закончит вращение, клиентское приложение сможет получить от элемента управления выпавшее значение. Од нако другой способ реализации может заключаться в том, что элемент управления уведомил приложение о завершении вращения, используя событие. В этом разделе Вы увидите, какдобавить несколько событий к элементу управления. Для начала поймем, как работают события в ActiveX-эле ментах.
Как работают события. Когда элемент управления вставляется в контейнер (например, в форму Visual Basic или в диалоговое окно MFC), одно из действий клиента заключается в подсоединении к набору событий элемента управления, т. е. клиент реализует интерфейс, который описан элементом управления, и предоставляет элементу управления доступ к нему. Тем самым элемент управления может обращаться к контейнеру.
Разработка элемента управления включает в себя определение интерфейса, который можно использовать для обращения к клиенту. Например, при создании элемента управления на основе MFC мастер ClassWizard определит интерфейс и создаст несколько функций, которые можно вызывать изнутри элемента управления для генерации событий, направляемых клиенту. При разработке элемента управления на базе ATL можно достичь того же результата, определив интерфейс обратного вызова1 в lDL-файле и используя ClassVieWflrfl создания функций обратного вызова, генерирующих события для контейнера. Если интерфейс обрат-
Имеется в виду исходящий интерфейс. — Прим., персе.
ного вызова определен элементом управления, то контейнер должен реализовать этот интерфейс и передать его элементу управления. Клиент и элемент управления осуществляют это при помощи интерфейсов IConnectionPointContainer и IConnectionPoint. ■
IConnectionPointContainer — это интерфейс, который COM-объект реализует, чтобы показать, что он поддерживает исходящие соединения. Данный интерфейс представляет собой набор таких соединений, доступных клиенту. В контексте ActiveX-элемента каждое соединение обычно является набором основных событий.
interface IConnectionPointContainer : IUnknown {
HRESULT FlndConnectionPoint(REFIIO riid, IConnectionPoint **ppcp) = 0;
HRESULT EnumConnectionPai.nts(IEnumConnectionsPcint *»ppec) = 0;
IConnectionPointContainer предоставляет набор интерфейсов IConnectionPoint.
interface IConnectionPoint : IUnknown {
HRESULT GetConnectionInterfacedID *pid) - 0;
HRESULT GetConnpctionP9intContainer(IConrectionPointConrainer **ppcpc) - 0;
HRESULT Advise(IUnknown *pUnk. DWORD »pdwCookie) = 0;
HRESULT Unadvise(dwCookie) = 0;
HRESULT EnumConnectionsCIEnumConnections * ‘pp^c) = 0;•
}
Контейнер создает элемент управления вызовам CoCreatelnstance. При установлении взаимодействия между контейнером и элементом управления первый запрашивает интерфейс IConnectionPointContainer (т. е. вызывает Query Interface с параметром 1ID IConnectionPointContainer). Если элемент управления поддерживает точки соединения (т. е, отвечает «да» на запрос IConnectionPointContainer), то контейнер использует IConnectionPointContainer. ;Pind- ConnectionPoint, чтобы получить интерфейс IConnectionPoint, представляющий набор основных событий. Контейнер знает GUID этого набора из информации о типе, которая была получена при внедрении элемента управления в контейнер.
Если контейнер может подсоединиться к набору основных событий элемента управления (т. е. IConnectionPointContaineniFindConnectionPointBaapaiiiser указатель на интерфейс IConnectionPoint), то контейнер использует IConnectionPoint^Advise для подписки на получение обратных вызовов. Конечно, для этого контейнер должен реализовать интерфейс обратного вызова, определенный элементам управления (об этом интерфейсе контейнер может узнать из библиотеки типов элемента управления). После установления соединения элемент управления может обращаться к контейнеру всякий раз, когда нужно сгенерировать событие. Таким образом работают события в ActiveX-элементе на основе ATL
Добавление событий к элементу управления. Добавить набор событий к элементу управления можно за несколько шагов, причем некоторые из них скрытно выполняются мастером. Сначала используйте ТВЕдля описания событий. Далее добавьте класс-заместитель, инкапсулирующий точки соединения и события. Наконец, заполните таблицу соединений (connection шар) элемента управления, чтобы клиент и объект могли взаимодействовать друг с другом. Давайте рассмотрим каждый шаг детально.
При использовании ATL для создания ActiveX-элемента начинать добавление событий в элементуправления нужно с IDL-файла. Интерфейс обратного вызова должен быть описан в IDL-файле для того, чтобы клиент знал, как реализовать этот интерфейс. Библиотека типов, используя которую, клиент определяет реализацию интерфейса обратного вызова, создается при компиляции IDL- файла, ПрикомпиляцииГОЬ-файласоздаетсябиблиотекатипов, используя которую, клиент определяет реализацию интерфейса обратного вызова. Например, если Вы хотите добавить события, означающие бросание кубиков, выпадение одинаковых значений и выпадение двух единиц, то опишите такой интерфейс обратного вызова;
library ATLDICESRVRLib
< importlibC stdole32. tlb"),
importlib("stdolc2.tlb");
[
uuid(21C65C43-08FF-11dt-8CAA-F0l0872CC837).
helpstring("Events created from rolling dice")
]
dispinterface _IATlDiceObj Events { properties: methods;•
[id(1)J void DiceRolled([in]short x, [in] short y);
(id(2)] void Doubles([in] short x);
Г 16(3)1 void SnakeEyesO;
[
uuid(6AED4EBD-O991-1101-8CAA-FDl0872CC837), helpstring; ATLDiceOb Class")
coclass ATLDiceOo
[default] interface lATLDieceObj;'
(default, source] dispinterface I ATLDiceObjEvents;
Интерфейс обратного вызова определен как disp-иптерфейс (ключевое слово dispinterface^, так как эго наиболее общий тип интерфейса. В качестве интерфейсов обратного вызова боль-
i шинство сред понимают только IDispatch. Код описывает интерфейс, реализуемый клиентом [ (если клиент захочет получать эти обратные вызовы). Мы добавили интерфейс вручную, но ' Object Wizard может поместить его сам; при этом интерфейс, скорее всего, получит другое > имя (например, lATLObjEventS).
/ Реализация точки соединения. После того, как Вы описали интерфейс обратного вызова в ; IDL-файле и скомпилировали элемент управления, библиотека типов будет содержать опи/ сание этого интерфейса, поэтому любой клиент смажет узнать, как его реализовать. Однако Вы еще не разработали способ генерации этих событий из элемента управления. Можно, конечно, вставить вручную вызовы клиента через IDispatch.-Jnuoke. Одаако лучше сделать это, создав заместитель (набор функций-оболочек вызовов Г№ра(сЬ}дла выполнения тяжелой работы. Чтобы создать набор функций, при помощи которых можно генерировать события, используйте команду меню Implement Connection Point в Class View.
В Class View щелкните правой кнопкой мыши на интерфейсе CATLDiceOb, что приведет к появлению контекстного меню. Выберите команду Implement Connection Point для вызова одноименного диалогового окна. В этом окне Вам нужно указать библиотеку типов, описывающую интерфейс, который будет использоваться для обращения к контейнеру (в пашем случае JATLDiceObjEitents^Ho умолчанию диалоговое окно обращается к библиотеке типов элемента управления, считывает ее и показывает найденные в ней интерфейсы. Выберите lATLDiceObJEventsi щелкните ОК. Будет создан С++-класс, являющийся оболочкой интерфейса событий «игральных кубиков». В соответствии с вышеприведенным описанием интерфейса будет сгенерирован следующий код:
template <class Т>
class CProxy_IATlDjece6bjr-vents :
public fCorrection.Pointlr.pKT, &0П0 „lATiOieceObjEvents, CComOynamicUnkArray>
// Предупреждение - этот класс может быть заново сгенерирован мастером public:
VOID Fire Doubles!SHORT x)
T* pT = static_cast<T»>(this);
int nConnectionlndex;
OComVariant* pvars = new CComVariant! 1 ]; int nConnections = m_vec. GetSize();
for (nConnectionlndex = 0;
nConnectionlndex < nConnections; nConnectionIndex++)•
pT->Lock();
CComPtr<IUnknown> sp = ni_vec.GetAt(nConnectionIndex); pT->Unlock();
IDispatch* pDispatch = reinterpret_cast<IOispatch*>(sp.p);
if (pDispatch Г= NULL)
{
pvars[O].vt = VT_I2;
pvars[O].iVal= x;
DISPPAHAMS disp = { pvars, NULL, 1,0);-
pDispatch->Invoke(0x1, IID_NULL. LOCALE USER DEFAULT, DISPATCH METHOD, &disp. NULL, NULL, NULL);
. }
deleted pvars;
)
VOID Fire DiceRolled(SHORT x, SHORT y)
T* pT = static_cast<T*>(this): int nConnectionlndex;.
CComVariant* pvars = new CComVariant[2];
int nConnections = m^vec.GetSizeO;
for (nConnectionlndex = 0;
nConnectionlndex < nConnections;
nConnectionIndex++)
pT->Lock();
CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex); pT->Unlock();
IDispatch* pDispatch -
reinterpret_cast<IDispatch*>(sp.p).
if (pDispatch |= NULL) {
pvars[1].vt = VT_I2;
pvars[1].iVal= x;
pvars[0].vt я VT_I2;
pvars[0].iVal= y;
DISPPARAMS disp = { pvars, NULL, 2, 0 };
pDispatch->Invoke(0x2, IID NULL. LOCALE.USER DEFAULT, DISPATCH METHOD, Adisp, NULL, NULL, NULL);
deletef] pvars;
VOID Fire_SnakeEyes()
{
Т* pi = s!at_c_cast<T’>(this);
int nOonnect wnlndex;
int nConrections = m_vec.GelSiz9();
for (nConnectionlndex = 0;
nConnectionlndex < nConnections;
nConnectionlndex*+}
pT->Lock();
CComPtr<IUnknown> sp = mj.'ec.GetAt(nCo.nneci:ionIndsx):
pT->Unlock():
IDispatch* pDispatch =
reinterpre! _cast<lDispatch*>(s3.p):
if (pDispatch != NULL)
{
DISPPARAMS disp = { NULL, NULL, 0, 0 };
pDispatch->Invoke(0x3, IIDJIULL, LOCALE_USER_DEFAULT, CISPATCH METHOD, Sdisp, NULL, NULL, NULL); ' )
}
}
I;
Сгенерированный С++-класс имеет двойное назначение. Во-первых, он действует как конкретная точка соединения. (Обратите внимание, что он производен от iConnectionPointbnpl?) Во-вторых, он является заместителем интерфейса, реализованного контейнером. Например, если Вы хотите сообщить клиенту, что выпали одинаковые значения, то просто вызовите функцию Fire_Doubles. Обратите внимание, что заместитель создает обертку вокруг интерфейса IDispatch, тюзтам^ Вы освобождены от обработки типа Variant.
Установление соединения и генерация событий. Последний шаг в создании набора событий — добавление точки соединения к элементу управления и подключение интерфейса iComectionPoinlContainer. Диалоговое окно добавляет класс CProsylATLDiceObjEvents список базовых классов для элемента управления, что обеспечивает последнему реализацию IConnectionPoint. В свою очередь, ATL-класс 1СоппесНопРо1п1Соп1а1пег1тр1пред,аставляет реализацию IConnectionPointContainer.
class CATLDiceObj :
public CCo^ObjectAootEx<CCorSingleThreacModel>,
public CStockPropImpKCATLDiceObi, lATLDieceObj. &IID_IATLDieceObj, &LIBID_ATLDICESRVRLib>, public CComCor* -■olcCATLDiccObj >.
public I?ersistStreamInitrnDl<CATLDiceCbj>,
public I01eControlImpl<CATLDice0bj>,
public lOleObjsctlmpKCATLDlceObjx,
public 10ieInPlaceActiveCbjectIm31<CATLDiceObj>,
public IViewObjectExImpl<CATLDiceObj>,
public IQlelnPlaceObjeczWindowlessIropl<CATLGiceObi>,
public IConnectionPointContainerImpl<cATLDiceObj>,'
public IPersistStorageimpl<CATLDiceObj>,
public lSpecifyProperty?agesImpl<CATLDiceODj>,
public IQuickActivateImpl<CATLDiceODj>,
public IDataOojectImpl<CATLDiceObj >,
public IProvidcClassIrfo2Im31<ACLSID_ATLDice0b, &DIID IATI DiceCajEvents,
&l JBI^ATLDICESRVaLib^ “
public IPropertyNo*.Л fyS:nkCP<CATLDiceObj>,
см. след. cmp.
public CComCoClass<CATLDiceObj, &CLSID_ATLDlceOb>, public CProxy_DDLceEvents< CATLDiceObj >
{
Наличие этих классов в списке базовых добавляет в элемент управления механизм, обес- лечивамяций работу точек соединения. Всякий раз, когда Вы хотите сгенерировать событие для контейнера, то все, что нужно сделать — это вызвать одну из функций класса-замсстите- ля. Например, правильно было бы генерировать события в метода OnTimer элемента управления: событие DiceRollednpn остановке таймера, событие SnakeEyes при выпадении единиц на каждом кубике и событие Doubles при одинаковых зиачеииях на каждом кубике.
CATLDiceObj: ;OnTimer(UINT msg, WPARAM wParam, LPARAM IParam, BOOL& bHandled) {
it(m_nTimesRolled > m.nTimesToRoll) {
m.nTimesRolied = 0;
KillTimer(1);
Fire_DiceRolled(m_nFirstDieValue, m.nSecondDieValue):
if(m_nFlrstDieValue == m.nSecondDieValue)
Hre.Doubles(m.nFirstDieValue):
if(m_nFirstDieValue == 1 W m.nSecondDieValue == 1)
Fire^SnakeEyes();
m_nFirstDieValue = (rand() % (MAX.DIEFACES)) + 1;
m.nSecondDieValue = (rand() % (MAX.DIEFACES)) + 1;
FireViewChangeO;
m_nTimesRolled++;
bHandled = TRUE;
return 0'
Наконец, обратите внимание, что таблица соединений содержит записи для точек соединения элемента управления:
BEGIN CONNECTION POINT MAP(CATLDiceObj)
CONNECTION POINT_ENTRY(DIID_IATLDiceObj Events)
CONNECTION POINT_ENTRY(IID_IPropertyNotifySlnk)
END.CONNECTION.POINT.MAPO
Элемент управления использует эту таблицу для возврата точки соединения при запросе ее клиентом.
Использование элемента управления
Как же использовать написанный Вами элемент управления? Достоинство СОМ состоит в том, что как только клиент и объект договорились о совместно используемых интерфейсах, им не нужно знать друг о друге что-либо етде. Все интерфейсы, реализованные в элементе управления «игральные кубики», будут доступны во многих программных средах. Вы уже видели, как использовать ActiveX-элемент внутри диалогового окна на основе MFC. Только что созданный элемент управления будет вполне нормально работать в таком окне — просто выберите команду Add То Project в меню Project, затем откройте папку Registered ActiveX. Controls и вставьте компонент ATLDiceOb в свой проект. Visual C++ прочитает библиотеку типов элемента управления и вставит весь код, необходимый для совместной работы диалогового окна и элемента управления. (В том числе все интерфейсы OLE-внедрения, а также интерфейсы соединений и событий.) Кроме того, Вы мажете также легко вставить этот элемент управления в форму Visual Basic. В этом случае выберите команду References в меню. Project и добавьте элемент управления в проект Visual Basic.
Работа с базами данных
22 Зак №7«"22 Зак №7«"
22 Зак №7«"
22 Зак №7«"
Microsoft ODBC
Микрокомпьютеры стали популярны отчасти потому, что бизнес увидел в них недорогое средство для ведения инвентарных списков, обработки информации о заказах, распечатки платежных документов и т. д. Такого рода приложения требуют быстрого доступа к отдельным записям в больших базах данных. Одной из первых систем управления базами данных (СУБД) для микрокомпьютеров была dBASE II — однопользовательский продукт с собственным языком программирования и форматом файлов. Сегодня у Windows-программистов широкий выбор СУБД, включая Inprise Paradox, Microsoft Access, Microsoft FoxPro и Powersoft PowerBuilder. Большинство этих продуктов позволяет обращаться как к локальным, так и к удаленным данным на центральном компьютере. В последнем случае необходимо дополнительное программное обеспечение — сервер базы данных (database server), например ORACLE или Microsoft SQL Server.
ПРИМЕЧАНИЕ Microsoft SQL Server входит в состав Enterprise Edition Visual C++.
Есть ли здесь место программисту, опирающемуся на MFC? В Visual C++ есть все компоненты, необходимые для создания Windows-приложений, работающих с базами данных. В нем предусмотрены две независимые системы доступа к базам данных: стандарта ODBC (Open Database Connectivity — открытая связь с базами данных) и стандарта DAO (Data Access Objects — объекты доступа кданным). Эта глава посвящена стандарту ODBC, реализованному в виде расширяемого набора динамически подключаемых библиотек (DLLs), который предоставляет стандартный программный интерфейс для приложений баз данных, ODBC основан на стандартизированной версии языка структурированных запросов (Structured Query Language, SQL). Используя ODBC и SQL, можно писать приложения, независимые от конкретной базы данных.
В состав Visual C++ включены инструментальные средства и MFC-классы для ODBC, которые мы и рассмотрим здесь. Вы изучите основы ODBC и познакомитесь с четырьмя примерами программ: первая работает с набором строк (rowset) ODBC на основе MFC-класса CRecOrdsetCyx^XK), вторая использует MFC-класс CRecordVieia (ЕХ31В), третья — несколько наборов записей (recordset) (ЕХ31С). а четвертая — класс CRecordset без привязки (EX31D)
Преимущества СУБД
Процесс сериализации, с которым Вы познакомились в главах 17 и 18, связывает объект-документ с файлом на диске. При открытии документа все его данные должны быть считаны в память, а при закрытии измененного документа — записаны обратно надиск. Очевидно, что сериализация документа, размер которого превосходит размер доступной виртуальной па-
мяги, невозможна. Даже если документ достаточно мал, чтобы уместиться в памяти, не всегда нужно считывать и записывать его целиком.
Можно было бы, конечно, создать свой алгоритм произвольного доступа к дисковому файлу, изобретя тем самым собственную СУБД, но, наверное, у Вас хватает и другой работы. Кроме того, настоящие СУБД дают ряд следующих преимуществ.
I* Применение стандартных форматов файлов. Говоря о форматах баз данных, многие имеютв виду DBF- файлы dB AS E/Xbase, воспринимаемые большинством программ. Но это лишь один из существующих форматов, хоть и распространенный. В последнее время все большую популярность завоевывает MDB-формат Microsoft Access, при использовании которого все таблицы и индексы базы данных можно хранить в одном файле.
Индексированный доступ к файлу. Если Вам нужен быстрый доступ к записям по ключу (например, по имени заказчика), значит, Вам нужен индексированный доступ к файлу, Конечно, можно написать свои процедуры доступа к файлу со структурой двоичного дерева (B-tiee), но это работа весьма утомительна, к тому же ее давно сделали другие: во всех СУБД предусмотрены эффективные процедуры индексного доступа.
Защита целостности данных. Многие СУБД профессионального уровня обеспечивают защиту своих данных. Один из примеров — обработка транзакций. При транзакции осуществляется последовательность связанных друг с другом изменений. Если транзакцию нельзя обработать целикам, выполняется так называемый откат (roll back), и база данных возвращается к состоянию, предшествовавшему началу транзакции.
Управление многопользовательским доступом. Если вашему приложению не нужен многопользовательский доступ прямо сейчас, он может понадобиться в будущем. Большинство СУБД обеспечивает блокировку записей, предотвращающую взаимовлияние одновременно работающих пользователей. В некоторых многопользовательских СУБД применяется модель «клиент-сервер», в которой большую часть обработки выполняет один компьютер — сервер базы данных, а рабочие станции реализуют пользовательский интерфейс доступа к базе данных. Другие многопользовательские СУБД поддерживают обработку баз данных непосредственно на рабочих станциях и контролируют доступ каждой рабочей станции к совместно используемым файлам.
SQL
Вы наверняка слышали об SQL — стандартном языке доступа к базам данных со своим синтаксисом. В мире SQL база данных представляется набором таблиц, состоящих из строк и столбцов. SQL поддерживают многие СУБД, и его знают многие программисты. Этот стандарт постоянно развивается, а его синтаксис различен для разных продуктов. Расширения SQL, в частности поддержка массивных двоичных объектов (binary large objects, blobs), позволяют сохранять в базе данных картинки, звук и сложные структуры данных.
Стандарт ODBC
Стандарт Microsoft ODBC определяет не только синтаксис SQL, но и программный интерфейс с базами данных на языке С, Теперь одна программа на С или C++ может без перекомпиляции обращаться к любой базе данных, для которой существует ODBC-драйвер. ODBC Software1 Development Kit (SDK), поставляемый вместе c Visual C++, содержит 32-разрядные драйверы для DBF-файлов, М DB-баз данных Microsoft Access, XLS-файлов Microsoft Excel, файлов Microsoft FoxPro, текстовых ASCII-файлов и баз данных Microsoft SQL Server.
Другие компании, выпускающие СУБД (в том числе Oracle, Informix, Progress, Ingres и Centura Software), предоставляют ODBC-драйверы для своих баз данных. Если Вы разработали MFC-программу с использованием, например, драйвера dBASE/Xbase, она сможетработать
и с драйвером для базы данных Access. Перекомпиляция не нужна — программа просто загрузит другую DLL. ■
Не только программы на C++ могут задействовать ODBC, другие среды программирования тоже используют цреимуществанового стандарта. Написав программу на C++для обновления базы данных SQL Server, можно приобрести готовый, совместимый с ODBC генератор отчетов, чтобы программа форматировала и распечатывала эти данные. Таким образом, ODBC .отделяет пользовательский интерфейс от фактического управления базой данных, и поэтому больше нет нужды приобретать средства разработки пользовательского интерфейса у той же компании, что поставляет СУБД.
Кое-кто критиковал ODBC за то, что он не позволяет программистам пользоваться нестандартными возможностями конкретной СУБД Но это-то и есть главное преимущество ODBC! Программисту достаточно изучить лишь один API, а затем подобрать программные компоненты с учетом их цены, производительности и технической поддержки. Разработчик больше не ограничен необходимостью покупки инструментария у поставщика своей СУБД.
Каково будущее ODBC? Сложный вопрос. Microsoft контролирует стандарт, но не «продает» ODBC, а распространяет его бесплатно в целях продвижения других продуктов. Другие компании продают собственные библиотеки ODBC. А тем временем Microsoft выпустила так называемые Data Access Objects (DAO), разработанные на базе OLE и использующие ядро баз данных Jet из Microsoft Access. (О DAO и о его возможностях в сравнении с ODBC мы поговорим в главе 32) Если этого недостаточно, МюгозойцредлагаетОЕЕ DB — альтернативу ODBC, основанную на СОМ. В главе 33 описаны новые шаблоны классе® Visual C++, являющиеся оболочкой для OLE DB-кода потребителя и поставщика. "
Архитектура ODBC
У ODBC уникальная архитектура, основаннаяна применении DLL, что делает си стему полностью модульной. Компактная DLL верхнего уровня, ODBC32DLL, определяет API. В период выполнения программы ODBC32.DLL обращается к DLL-модулям, специфичным для базы данных и называемых драйверами. Через записи в Реестре Windows (поддерживаемые диспетчером ODBC в панели управления Windows) ODBC32.DLL отслеживает, для каких баз данных есть драйверы, и позволяет «одной программе осуществлять доступ к данным сразу в нескольких СУБД Программа, например, может хранить какие-толокальные таблицы в формате DBF и в то же время использовать другие таблицы, управляемые тем или иным сервером базы данных. Иерархия 32-разрядных DLL-модулей ODBC продемонстрирована на рисунке 31-1.
Как видно на этом рисунке, свободно распространяемое ядро баз данных Microsoft Jet, поставляемое вместе с Visual C++, обеспечивает доступ к базам данных во многих стандартных форматах. Например, обращаясь к DBF-файлу через ядро Jet, Вы используете тот же код. что и в Microsoft Access.
Использование ODBC SDK
Программируя непосредственно на уровне ODBC API языка С, Вы должны знать о трех важных понятиях ODBC: среда (environment), связь (connection) и оператор (statement). Доступ к ним осуществляется через описатели (handles).
Первое, что Вам понадобится, — среда, которая свяжет вашу программу с ODBC-системой. Обычно приложение использует лишь описатель среды. . . .
Затем Вы должны установить одну или несколько связей. Под связью подразумевается конкретная комбинация драйвера и источника данных (data source). Например, моясно установить несколько связей с подкаталогами, содержащими DBF-файлы, и связаться с несколькими серверами SQL Server в той же сети. Конкретную ODBC-связь можно жестко зашить в программу, но мояою и предусмотреть возможность выбора из списка доступных драйверов и источников данных.
Рис. 31-1. 32-разрядная архитектура ODBC
ODBC32.DLL предоставляет стандартное диалоговое окно Windows, в котором перечисляются связи, определенные в Реестре (в разделе HKEYJLOCAL_MACШNE\Software^ODBC). Установив связь, нужно выполнить оператор SQL Оператор может представлять собой запрос: SELECT FNAME, LNAME, CITY FROM AUTHORS WHERE STATED'UT' ORDER BY LNAME
или оператор обновления данных:
UPDATE AUTHORS SET PHONE-'801 232-5780'
WHERE 10^'367-86-4343'
Так как операторы запроса требуют наличия в программе цикла для обработки возвращенных строк, вашей протрамме может понадобиться сразу несколько активных операторов. Многие ODBC-драйверы поддерживают применение нескольких активных операторов для одной связи. .
Еще раз взгляните на приведенный выше оператор SQL Допустим, в Юте (Utah) проживает 10 авторов (authors). ODBC позволяет определить результат запроса как блок данных, называемый набором строк (rowset) и связываемый с оператором SQL Используя функцию SQLExtendedFetcbiis ODBC SDK, программа может «проходить» по десяти выбранным записям посредством ODBC-курсора (cursor) — программно управляемого указателя в наборе строк
А что если в многопользовательской среде какая-то другая программа изменит (или удалит) запись об авторе из Юты в тот момент, когда ваша программа просматривает набор строк? При использовании драйвера ODBC Level 2 набор строк был бы скорее всего динамическим, и ODBC смог бы обновлять его при любом изменении базы данных. Динамические наборы строк называются динамическими множествами (dynasets). Ядро баз данных Jet поддерживает ODBC Level 2, а значит, поддерживает и динамические множества.
С Visual C++ поставляется библиотека ODBC-курсоров ODBCCR32.DT1, поддерживающая статические наборы строк, называемые снимками (snapshots), для драйверов ODBC Level 1. В случае снимка оператор SELECT заставляет ODBC создать локальную копию 10 записей об авторах и сформировать в памяти список указателей на них. Эти записи не меняются, пока Вы просматриваете их, так что в многопользовательской среде может понадобиться периодический опрос базы данных для пересоздания снимка.
MFC-классы ODBC: CRecordset и CDatabase
В случае MFC-классов для Windows Вы используете объекты C++ вместо описателей окон и контекстов устройств; в случае классов MFC ODBC — объекты вместо описателей связей и операторов. Описатель среды хранится в глобальной переменной; он не представлен объектом C++. Основные ODBC-классы — CDatabase и CRecordset. Объекты класса CDatabase представляют связи с источниками данных, а объекты класса CRecordset — наборы строк с возможностью прокрутки. В документации на Visual C++ применяется термин «набор записей» {recordset) вместо «набор строк» (row-set), чтобы терминология не расходилась с той, что принята в Microsoft Visual Basic и Microsoft Access. Производные от CDatabase классы создаются редко, но от CRecordset классы порождаются всегда, подстраиваясь под столбцы в таблицах своей базы данных.
Рис. 31-2. Взаимоотношения между MFC-классами ODBC и базой данных
Если вернуться к запросу об авторах из предыдущего раздела, то Вы можете создать (с помощью ClassWizard) производный от CRecordset класс GAutborSet.B котором можно предусмотреть переменные-члены для имени автора, фамилии, города, штата и почтового индекса. Протрамма создавала бы объект САн1Ьог5е1(сбыяно внедряемый в класс документа) и вызывала бы наследуемую функцию-член Open. Используя значения параметров и переменных- членов, CRecordset: :Ореп конструирует и открывает объект CDatabase, выдает SQL-запрос SELECT иустанавливает курсор на первую запись. Затем программа вызывала бы другие функции-члены CRecordset — для перемещения ODBC-курсора и обмена данными между полями
базы и переменными-членами CAutborSet. При уничтожении объекта CAutborSetH&6op записей закрывается, а в некоторых случаях закрывается и удаляется объект CDalabase. Связь между объектами C++ и компонентами ODBC продемонстрирована на рисунке 31-2.
Важно, что переменные-члены объекта CAuthorSet представляют только одну строку таблицы, называемую текущей записью (current record). Класс CRecordsetH скрытый в нем код ODBC для наборов строк управляют динамическими множествами и снимками базы данных, ПРИМЕЧАНИЕ Для одного источника данных допускается несколько активных динамических множеств или снимков, а в одной программе можно использовать несколько источников данных. ,
Ниже в таблице перечислены важнейшие функции-члены класса CRecordset, используемые в примерах этой главы. ■
Функция
Описание
Open
Открывает набор записей
AddNew
Подготавливает добавление новой записи в таблицу;
Update
Завершает операции^йУем или-БМ записывая новые или измененные данные в источник данных
Delete
Удаляет текущую запись из набора записей
Edit
Подготавливает изменение текущей записи
IsBOF
Определяет, ие спозиционирован ли набор записей до первой записи
IsEOF
Определяет, ие спозиционирован ли набор записей за последней записью
MoveNext
Устанавливает текущую запись на следующую запись или на следующее множество строк
Move First
Устанавливает текущую запись на первую запись
Movelast
Устанавливает текущую запись на последнюю запись
MovePrev
Устанавливает текущую запись на предшествующую запись или на предшествующее множество строк
GetDefauitConnect
Возвращает стандартную строку подсоединения к источнику данных, на котором основан набор записей
GetDefaultSQL
Возвращает SQL-строку по умолчанию
DoPieldExcbange
Обменивает данные между полями набора записей и соответствующей записью в источнике данных
GetStatus
Возвращает индекс текущей записи и признак окончания подсчета записей
GetRecordCount
Определяет иомер последней записи, до которой добрался пользователь
GetODBCFteidCount
Возвращает количество полей в наборе записей
GetODBCFieidlnfo
Возвращает информацию о полях в наборе записей
Подсчет строк в наборе записей
Трудно узнать, сколько записей содержится в наборе записей ODBC, поскольку точный подсчет не осуществляется до тех пор, пока не достигнут конец набора. До этого момента значение, возвращаемое функцией CRecordset::GetRecordCount, является всего лишь «верхней точкой прилива», показывающей последнюю строку, достигнутую при помощи CRecordset::Move- Next Функция CRecordset::Get$talus возвращает объект CRecordsetStatus, в котором есть переменная-член m_bRecordCountPinal— признак окончания подсчета. ' ■
Функция CRecordset: -MoveLast не определяет количество записей, даже для динамических множеств. Если Вам нужно знать количество записей в наборе, пройдите по нему при помощи вызовов MoveNext, (Альтернатива, позволяющая сделать эго быстрее — применить функцию COUNT.) Если запись добавляется или удаляется, то счетчик записей не обновляется.
Обработка исключений ODBC
Многие вызовы ODBC в MFC не возвращают код ошибки, а генерируют объект CDBException, который содержит строку с описанием ошибки. Предположим, Вы пытаетесь удалить запись из таблицы в базе данных Access, однако правила ссылочной целостности не позволяют этого сделать, так как строка в другой таблице зависит от данной строки. При вызове CRecord- set;Delete Вы увидите окно сообщения об ошибке, поступившей из базовых классов MFC.
Вы, несомненно, оцените сообщение об ошибке, но теперь ODBC «потеряли свое место» в наборе записей, и текущей записи больше нет. Вашей программе необходимо определять ошибку так, чтобы не вызывать функции, зависящие от текущей записи, например, CRecord- set;;MoveNext. Обрабатывать исключения нужно так:
try {
rtLpSet->Delete();
catch(CDBException* е) {
AfxMessageSox(е->m_st rE г го г);
s->Delete();
m_pSet->MoveFlrst(); // потеряли свое место!
UpdatsDataCFALSE);
return;
}'
mrpSet->MoveNext();
База данных для учета студентов
В примере Enroll в Visual C++ как пример дана готовая база данных Access (STDREG32.MDB) с информацией о студентах, классах и инструкторах. (См. Tutorial Samples в разделе Visual C++ Documentation\Samples\MFC Samples\Database Samples электронной документации.) На рисунке 31-3 показаны четыре таблицы этой базы данных и их взаимосвязи. Проиндексированные поля выделены полужирным, а связи 1-Г описывают ограничения по ссылочной целостности (referential integrity). Например, если на курсе (course) MATH 101 есть хотя бы одна секция (section), Access не позволит удалить запись по этому курсу. ,
Рис. 31 -3. Схема базы данных о студентах
Пример ЕХ31А
AppWizard способен генерировать закопченные, основанные на применении форм приложения для работы с базами данных, и именно этому посвящен пример Enroll. Однако, если бы пользователям были нужны такие примитивные приложения, они, вероятно, не стали бы
обращаться к программисту на Visual C++, а взяли бы Microsoft Access. Visual C++ и MFC-классы ODBC больше подходят для сложных приложений, которые периодически обращаются к базе данных. На основе этих классов можно создать и универсальный программный модуль для запросов к базам данных.
В ЕХ31А код, обращающийся к базе данных, изолирован от кеда пользовательского интерфейса, так что Вы легко поймете, как можно добавить средства ODBC для работы с базами данных в любое MFC-приложение. Вы будете использовать ClassWizardflnn генерации класса, производного от CRecordset, но не класса, производного от CRecordView, уже сгенериро- " ванного AppWizard в ответ па просьбу создать приложение для просмотра баз данных.
Приложение ЕХ31Аочень просто. Оно показывает строки из таблицы Student в окне представления с прокруткой. Эта таблица — часть базы данных Student Registration, поставляемой в качестве примера с Visual C++. Даже приведены действия по сборке ЕХ31А.
Скопируйте базу данных Student Registration на жесткий д иск. Файл stdreg3 2.mdb Вы найдете на компакт-диске Visual C++ MSDN в каталоге \Samples\VC98\Mfc\Ditabase\Std- reg. Скопируйте его на жесткий диск в новый каталог \STDREG и убедитесь, что у файл не задан атрибут «только для чтения».
Запустите ODBC Data Source Administrator для установки нового источника данных. Щелкните значок ODBC в панели управжния Windows. Программа Visual C++ Setup должна была установить необходимые ODBC-драйверы. Если Вы работаете в Windows 95, щелкните кнопку Drivers, чтобы убедиться в наличии драйвера Microsoft Access. Если же Вы работаете в Windows 98, щелкните с той же целью вкладку Drivers. (Если драйвера нет, снова запустите Visual C++ Setup.) Щелкните кнопку Add (в Windows 98 она находится на вкладке User DSN), в диалоговом окне Add Data Source выберите Microsoft Access Driver (в Windows 98 выберите Microsoft Access Driver в диалоговом окне Create New Data Source и щелкните Finish) и заполните поля диалогового окна ODBC Microsoft Access 97 Setup.
Задайте, используя кнопку Select; что база данных должна указывать на stdreg32.mdb. В конце щелкните ОК.
ПРИМЕЧАНИЕ Если Вы используете Windows NT 4.0, то щелкните значок ODBC в панели управления Windows и затем щелкните вкладку ODBC Drivers, чтобы убедиться в наличии Microsoft Access Driver. На вкладке User DSN щелкните Add, выберите Microsoft Access Driver в диалоговом окне Create New Data Source, щелкните Finish и заполните поля в диалоговом окне, как показано выше.
С помощью AppWizard создайте проект \VCPP32\EX31A\EX31A. Выберите SDI-приложение (на шаге 1) с классом «вид» CScrollView (шаг 6). В окне AppWizard Step 2 задайте параметр Header Files Only.
Используя ClassWizard, создайте класс набора записей СЕхЗ1 aSetS ыберите New из меню Add Class и заполните поля диалогового окна New Class.
Выберите для класса СЕхЗ 1 оУеПаблицу Student из базы данных Student Registration. При щелчке ОК в диалоговом окне New Class мастер ClassWizard откроет диалоговое окно Database Options. Выберите источник данных Student Registration и установите переключатель Dynaset (см первый рисунок на стр. 649).
После того как Вы выберете источник данных, ClassWizard предложит выбрать таблицу Укажите Student, как на иллюстрации (см. второй рис. на стр. 649).
Просмотрите переменные-члены, сгенерированные ClassWizard. Щелкните вклад- icy Member Variables для только что сгенерированного класса CExSlaSet. ClassWizard должен был сформировать переменные-члены по заголовкам столбцов из таблицы Student (см. третий рисунок на стр. 649).
Объявите в ex31aDoc.h внедренный объект «набор залисей»Добавьте в объявление класса СЕхЗ 1 aDoc открытую переменную-член:
CEx31aSet m_ax31aSet;
Модифицируйте файл ex31aDoc.cpp. Добавьте строку:
flinclude "ex31aSet.h"
непосредственно перед строкой
«include "ех31а3сс,h"
Объявите в ex31aVlew.h указатель на набор записей. Добавьте в объявление класса СЕхЗ 1 eView следующую з акрытую переменную-член:.
CEx31aSet* m_pSet;
Отредактируйте в ex31aView.cpp функции OnDrawu О«1пШа1ир<1а1еДобавъ'1е код, выделенный полужирным:
void CEx31aView:;OnDraw(CDC* pDC) {
TEXTMETRIC tm;
pDC->GetTextMBtrics(&tm);
int nLineHeight=tm.tmHeight+tm.tmExternalLeading;
CPoint pText(O.O);
int у = 0;
CString str;
if (m_pSet->IsBOF()) { // определяем пустые наборы записей return;
}'
n pSet->MoveFirst(); // ошибка, если пустой набор записей
while (lm_pSet->IsEOF()) {
str. Format( "Hid", m_pSet->m_StudentID);
pDC->TextOut(pText.x, pText.y, str);
pDC->TextOut(pText.x+1OOO, pText.y, m_pSet->m_Name);
st r.Format("id", m_pSet->m„GradYear);
pDC->Text0ut(pText.x+4000, pText.y, str);
m_pSet->MoveNext();
pText.y -= nLineHeight; }
void CExSlaView::OnInitialUpdate()
CScrpllView::OnInitialUpdate();
CSize sizelbtal(8000, 10500);
SetScrollSizes(MM_HIENGLISH. sizeTotal);
m_pSat = iGetDocument()->m_ex31aSet;
H Помните, что пара документ-вид повторно используется в SDI-приложенийх!
if <m_pSet->Is0pen()) {
m pSet->Close(); ) m pSet->0pen();
}
Кроме того, добавьте в ex31aView.cpp строку
tfinclude "ex31aSet.h‘'
перед строкой
ttinclude "ex31aDoc.h"
Отредактируйте файл ех31а.срр. Добавьте строку:
«include "ex31aSet.fr"
перед строкой
«include "ex31a0oc,h"
Соберите и протестируйте приложение ЕХ31А. Получилось ли у Вас нечто подобное?
1081Smith, Randy061002Maples. Alex951003Jimes. Thomas951004Shannon. Eric961005Foster. Susan9Б1006Jefferson, Nancy951007Tuner. Bob96юоаHolm, David951003Reynolds, Don9Б1010Taylor. Robert9510ПKarr. Dave96ID12Tanriant, Tim961013Marcus, Susanas1014Butlerfleld. Rite9Б1015Aman. Craig951016Anderson, Sandra.961081Smith, Randy061002Maples. Alex951003Jimes. Thomas951004Shannon. Eric961005Foster. Susan9Б1006Jefferson, Nancy951007Tuner. Bob96юоаHolm, David951003Reynolds, Don9Б1010Taylor. Robert9510ПKarr. Dave96ID12Tanriant, Tim961013Marcus, Susanas1014Butlerfleld. Rite9Б1015Aman. Craig951016Anderson, Sandra.96"ТХобЗвлёйиероддержкйТюВС!^; и Если Вы хотите Добаёитв пЬдцЬ^Жку OEtB^ff существующее'^\ в проект следующие изменения.^ V,. t > з!^^ * 4 р* ${;^ В.^ В КОТЩв StSd^^BGrtfr^^^^^t -”'i «йА'&Н^ 1ИНН^^f №Й$ ^^И^У"ТХобЗвлёйиероддержкйТюВС!^; и Если Вы хотите Добаёитв пЬдцЬ^Жку OEtB^ff существующее'^\ в проект следующие изменения.^ V,. t > з!^^ * 4 р* ${;^ В.^ В КОТЩв StSd^^BGrtfr^^^^^t -”'i «йА'&Н^ 1ИНН^^f №Й$ ^^И^УItIt, '5<s, '5<s:; ШоХмбе t Эа^г^^Х^ дЬ^ВЪ¥^ строкул '*-?^^'^, .. ч' ЧЙЙС1Ш ТЙЪЙНк Й$ : У^ database т^Й^ЙЙ^^П,:= 1 ;-1 м-ЛЫ'.-,1 44.VI.4- ■, > ' Vs й' 4;VS IV'-4Vp.«4Л пос* строки < ,*^. ;.;< t*»1 , „?,^M X ■ ’ • ЖШ^ОТ^^1# Шй^-йГ^ЙШ^Х/. ^&^Й^МЙ^*ге1«1ЙМЙНЖй^Ш:; ШоХмбе t Эа^г^^Х^ дЬ^ВЪ¥^ строкул '*-?^^'^, .. ч' ЧЙЙС1Ш ТЙЪЙНк Й$ : У^ database т^Й^ЙЙ^^П,:= 1 ;-1 м-ЛЫ'.-,1 44.VI.4- ■, > ' Vs й' 4;VS IV'-4Vp.«4Л пос* строки < ,*^. ;.;< t*»1 , „?,^M X ■ ’ • ЖШ^ОТ^^1# Шй^-йГ^ЙШ^Х/. ^&^Й^МЙ^*ге1«1ЙМЙНЖй^Ш4 ^Ч4 ^Ч
1081
Smith, Randy
06
1002
Maples. Alex
95
1003
Jimes. Thomas
95
1004
Shannon. Eric
96
1005
Foster. Susan
9Б
1006
Jefferson, Nancy
95
1007
Tuner. Bob
96
юоа
Holm, David
95
1003
Reynolds, Don
9Б
1010
Taylor. Robert
95
10П
Karr. Dave
96
ID12
Tanriant, Tim
96
1013
Marcus, Susan
as
1014
Butlerfleld. Rite
9Б
1015
Aman. Craig
95
1016
Anderson, Sandra
.96
1081
Smith, Randy
06
1002
Maples. Alex
95
1003
Jimes. Thomas
95
1004
Shannon. Eric
96
1005
Foster. Susan
9Б
1006
Jefferson, Nancy
95
1007
Tuner. Bob
96
юоа
Holm, David
95
1003
Reynolds, Don
9Б
1010
Taylor. Robert
95
10П
Karr. Dave
96
ID12
Tanriant, Tim
96
1013
Marcus, Susan
as
1014
Butlerfleld. Rite
9Б
1015
Aman. Craig
95
1016
Anderson, Sandra
.96
"ТХобЗвлёйиероддержкйТюВС!^
; и Если Вы хотите Добаёитв пЬдцЬ^Жку OEtB^ff существующее'^
\ в проект следующие изменения.^ V,. t > з!^^ * 4 р* ${
;^ В.^ В КОТЩв StSd^^BGrtfr^^^^^
t -”'i «йА'&Н^ 1ИНН^^f №Й$ ^^И^У
"ТХобЗвлёйиероддержкйТюВС!^
; и Если Вы хотите Добаёитв пЬдцЬ^Жку OEtB^ff существующее'^
\ в проект следующие изменения.^ V,. t > з!^^ * 4 р* ${
;^ В.^ В КОТЩв StSd^^BGrtfr^^^^^
t -”'i «йА'&Н^ 1ИНН^^f №Й$ ^^И^У
It
It
, '5<s
, '5<s
:; ШоХмбе t Эа^г^^Х^ дЬ^ВЪ¥^ строкул '*-?^^'^, .. ч
' ЧЙЙС1Ш ТЙЪЙНк Й$ : У^ database т^Й^ЙЙ^^П
,:= 1 ;-1 м-ЛЫ'.-,1 44.VI.4- ■, > ' Vs й' 4;VS IV'-4Vp.«4
Л пос* строки < ,*^. ;.;< t*»1 , „?,^M X ■ ’ • ЖШ^ОТ^^1# Шй^-йГ^ЙШ^Х/. ^&^Й^МЙ^*ге1«1ЙМЙНЖй^Ш
:; ШоХмбе t Эа^г^^Х^ дЬ^ВЪ¥^ строкул '*-?^^'^, .. ч
' ЧЙЙС1Ш ТЙЪЙНк Й$ : У^ database т^Й^ЙЙ^^П
,:= 1 ;-1 м-ЛЫ'.-,1 44.VI.4- ■, > ' Vs й' 4;VS IV'-4Vp.«4
Л пос* строки < ,*^. ;.;< t*»1 , „?,^M X ■ ’ • ЖШ^ОТ^^1# Шй^-йГ^ЙШ^Х/. ^&^Й^МЙ^*ге1«1ЙМЙНЖй^Ш
4 ^Ч
4 ^Ч
Элементы программы EX31A
В этом разделе мы обсудим основные элементы программы ЕХ31А.
Интеграция класса набора записей в приложение
Создавая класс CEx31aSet, ClassWizard просто добавляет в проект СРР- и Н-файлы, вот и все. Соединение класса набора записей с классами «вцц» и «документ» — ваша задача Внедрение объекта CEx31aSetB класс СЕхЗ^«Z)oc гарантирует, что при запуске приложения будет создан объект «набор записей».
Объект «вид» всегда может получить доступ к набору записей через документ, но будет лучше, если у него будет свой указатель на набор. Обратите внимание на то, как функция OnlnitialUpdate-в классе «вид» устанавливает переменную-член тJ)SeL
ПРИМЕЧАНИЕ Если запустить AppWizard с любым из параметров Database View, он сгенерирует класс, производный от CRecordset, и класс, производный от CRecordView, а также все необходимые связи, [Которые были описаны ранее. Мы не используем AppWizard в этом режиме, так как нам не требуется приложение на базе формы.
Функция-член OnlnitialUpdate класса CExStaVlew
Задача этой функции — открывать набор записей, связанный сданным объектом «вид». Конструктор набора записей вызывается с указателем на объект «база данных», равным NULL, что подсказывает функции CRecordSet::Open создать объект CDotabose-и. установить между базой и набором записей связь «один к одному». Но откуда ей известно, какие источник данных и таблицу следует использовать? Она вызывает две виртуальные функции класса CRecordset- GetDefaultConnecM. GetDefaultSQLCYassWizaid генерирует следующие реализации этих функций в вашем производном классе набора записей:
CString CEx31aSet: :GetOefaultConnect()
return T("ODBC;DSN=Student Registration");
)
CString CEx31aSet::GetDefaultSOL()
। return J( “(Student)"):
ПРИМЕЧАНИЕ ClassWizard и AppWizard заключают имена столбцов и таблиц в квадратные скобки, которые нужны, только если в именах есть пробелы.
GetDefaultSQL — чисто виртуальная функция, поэтому ее надо реализовать в производном классе. С другой стороны, в базовом классе уже есть реализация GetDefaultConnect, которая открывает диалоговое окно ODBC, запрашивающее у пользователя имя источника данных.
Так как в SDI-приложениях объекты-документы можно использовать многократно, функция OnlnitialUpdate, прежде чем открывать новый набор записей, должна закрыть уже открытый (если такой есть). Проверка осуществляется через функцию-член CRecordseHsOpen.
Функция-член OnDraw класса CEx31aView
Как и в любом приложении с архитектурой «документ-вид», функция CEx31dView;:OnDraw вызывается всякий раз, когда окно представления объявляется недействительным, а также по одному разу для каждой распечатываемой страницы. В этой программе ОпОтаги весьма неэффективно просматривает все строки набора записей и отображает содержимое столбцов через функцию CDC::TextOut. Основные функции-члены CRecordset, используемые OnDraw, — MoveFirstH MotteNext; Функция AfoveFfrsf завершится неудачно, если набор записей пуст, поэтому необходим предварительный вызов IsBOFдля проверки: не находится ли текущая позиция в наборе перед его началом. Функция IsEOF проверяет, не достигнут ли конец набора и, как только это условие выполняется, завершает цикл просмотра строк.
Вспомните, что ClassWizard сгенерировал переменные-члены класса CExSlaSetrous. столбцов набора записей. Следовательно, этот класс набора записей, а теперь и класс «вид» жест- :то запрограммированы для работы только с записями о студентах. Функции-члены класса 1^есогейе/вызывают чисто виртуальную функцию DoFieldExcbange, реализацию которой ClassWizard генерирует на основе переменных-членов m_StudentID, m_Name и m_GradYear. Код для производного класса набора записей из нашего примера выглядит так:
void CEx31aSet::DoFieldExchange(CFieldExchange* pFX)
//{{AFX_FIELD_MAP(CEx31aSet)
pFX->SetFieldType(CFieldExchange::outputColurn);.
flFX_Long(pFX, _T("[StudentiD]"), m_StuoentlD);"
RFX_Text(pFX, _Т("[Матв]"), m_Nane);.
RFX_lnt(pFX, T("[GradYear]"), m GradYear);
//) )AFX FIELD MAP
У каждого типа данных SQL есть своя функция для обмена данными с полями записи (record field exchange, RFX). Эти функции весьма сложны и не раз вызываются при работе с базой данных. Бы, наверное, думаете, что /У'Х-функции сходны с DDX-функциями для CDielogH что они действительно передают данные между базой и переменными-членами. Но это не так. Основное назначение ЛРХ-фупкций — так «связать» поля базы данных с переменными-членами, чтобы ODBC-функции вроде SQf.ExlendedFeicbM.OTim передавать информацию из этих полей. Для этой цели DoFieldExcbangen вызывается из CRecordset::Open.OHa вызывается также из функций Move для повторного выделения памяти под строки и очистки бигов состояния
Поскольку DoFieidExcbangenry6aKO интегрирована в MFC-алгоритмы работы с базами данных, не советуем вызывать ее напрямую.
Строки для фильтрации и сортировки
В операторах запроса SQL допустимы конструкции ORDER BYи WHERE. Открытая переменная-член m_strSort класса CRecordset содержит текст конструкции ORDER BY (без слов «ORDER BY»). Другая открытая переменная-член, m_strFilter,содержит текст конструкции WHERE (без слова «WHERE»). Эти строки можно определить перед открытием набора записей.
Объединение двух таблиц базы данных
Большинство разработчиков приложений для баз данных знают, что объединение (join) — эго одна большая логическая таблица, составленная из полей двух (или более) связанных между собой таблиц. В базе данных для учета студентов Вы могли бы, объединив таблицы Student и Enrollment, получить список студентов и классов, в которые они зачислены.
Объединение легко реализуется в Visual C++, так как Class Wizard позволяет добавлять таблицы к существующему набору записей. Тем не менее, это потребует дополнительного программирования. Вот что надо сделать для объединения таблицы Enrollment с таблицей Students в примере ЕХ31А.
Выберите в ClassWizard на вкладке Member Variables класс СЕхЗ 1 «Set. Щелкните кнопку Update Columns и выберите таблицу Enrollment из базы данных Student Registration. Если Вы получите предупреждение, что источник данных не содержит все столбцы необходимые классам наборов записей, щелкните Yes для продолжения Щелкните кнопку Bind All, чтобы создать переменные-члены для всех полей Enrollment
Отредактируйте функцию CEx31aSet::GetDeJaultSQLi$TO&BL обеспечить доступ к двум таблицам:
CString CExSiaSet::GetDe*aultSOl()
* return "[("[Student],[Enrollment]");
}
Теперь в объединенной таблице два поля StudentID. Отредактируйте строку со StudentID в функции <CEx31«Ser::DoFieldExcbang^ia уточнения поля именем таблицы:
Rf-X_Long(prX, _T("[Stuoent],[StudentID]"), m_StudentID);
В функции СЕхЗ 1 aViewaOnlnitialUpdatsipTLCBO^ne переменной тзггЕШетнабора записей такое значение:
m_pSel->m_strFilter = "[Student].[StudentID] = [Enrollment],[StudentID]';
Добавьте в функцию GEx31 а¥геги::ОпПтаикод для отображения новых полей из Enrollment
Например:.
pDC->Text0ut(pText.x+5003, plext.y, m_pSet->m_CourseID);
MFC-класс CRecordView
CRecordView — это класс «представление в виде формы» (form view), связанный с набором записей. На рисунке 31-4 показано MFC-приложение для представления записей (record view). Кнопки на панели инструментов обеспечивают перемещение по таблице базы данных.
Рис. 31 -4. MFC-приложение на базе класса CRecordView
Каки класс CFormView,CRecordView использует шаблон диалогового окна. В классе CFormView есть переменные-члены, соответствующие элементам управления диалогового окна, но класс CRecordView работает с переменными-членами внешнего объекта (foreign object), обычно присоединенного к нему объекта CRecordset. Когда пользователь вводит или изменяет данные в элементах управления, DDX-код в окне представления записей перемещает их значения в переменные-члены класса набора записей, связанные с полями базы данных RFX-кодом.
При создании подобного приложения AppWizard генерирует класс, производный от CRecordView.H пустой шаблон диалогового окна. Кроме того, AppWizard генерирует класс, производный от CRecordset, и поэтому запрашивает у Вас имя таблицы базы данных. В период выполнения программы устанавливается связь между объектом-представлением записей и объектом-набором записей. Ваша задача — добавить к шаблону диалогового окна элементы управления и увязать их с переменными-членами набора записей; для создания подобных приложений на основе формы никакого программирования на C++ не требуется.
AppWizard генерирует приложение, способное лишь просматривать базу данных. Если Вы хотите редактировать, добавлять и удалять записи, придется писать код. Полученное в резуль-, тате приложение ведет себя несколько странно — так же, как Visual Basic и Access. Запись добавляется или изменяется, только когда пользователь переходит кдругой записи. Если это как раз то, что Вы хотели, используйте в качестве шаблона для своих приложений пример ENROLL с компакт-диска Visual C++ (каталог \SAMPLES\VC98\MFC\TUTORLAL\ENROLL).
Пример ЕХ31В
Пример ЕХ31В — это приложение для добавления/изменения/удаления записей в базе данных, и его поведение отличается от стандарта, принятого в Access. Добавление, обновление и удаление записей осуществляется по явной команде пользователя. Даже если Вы предпочитаете, чтобы программа вела себя в стиле Access, Вы многое узнаете о классе CRecordView. разобрав операции, необходимые для создания ЕХЗ1 В.
*C помощью AppWizard создайте проект \VCPP32\EX31B\EX31B. Выберите Single Document Interface и откажитесь от Printing and Print Preview. В диалоговом окне AppWizard Step 2 выберите Database View Without File Support и щелкните кнопку Data Source, чтобы выбрать таблицу Instructor из базы данных Student Registration. Установите переключатель Dynaset в разделе Recordset Туре. Параметры и имена классов по умолчанию показаны на рисунке.
Добавьте поля ввода к шаблону IDDEX31ВЮКМ. Используйте идентификаторы 1DCJD, IDC NAMEh. IDC_ROOMk расположите поля, как показано на рисунке.
. Используя ClassWizard, свяжите поля ввода с переменными-членами набора записей. Для добавления переменной-члена щелкните вкладку Member Variables и выберите идентификатор, соответствующий полю ввода для каждой переменной. Щелкните Add Variable и раскройте комбинированный список Member Variable Name. Выберите только нужную переменную, как показано на верхнем рисунке на стр. 656.
Результат показан на рисунке ниже.
Соберите и протестируйте приложение ЕХ31В. Это приложение позволяет только просматривать базы данных; его окно показано на рисунке 31-4. Для последовательного просмотра записей служат кнопки на панели инструментов.
Создайте резервную копию базы данных. Теперь мы начнем писать код, который будет добавлять, изменять и удалять записи. Стоит создать резервную копию файла . STDREG32.MDB — будет к чему вернуться после удаления всех записей..
Дополните меню новыми командами. Добавьте перечисленные ниже элементы в подменю Record меню $DR_MAINFRAM//Затем с помощью ClassWizard увяжите эти команды с представленными в таблице элементами класса СЕхЗ IbView.
Команда меню
Идентификатор
Обработчик команды1
Обработчик обновления пользовательского интерфейса команды
Add Record
!D_kHCORD_ADD
OnRecordAdd
Clear Fields
JD_RECORD_CLEARFIF.IDS
OnRecordCledrftelds
Delete Record
Ю-RECORD _DELETE
OnRecurdVdete
OnUpdcteReco rdDelete
Update Record
LD-RECORDDPDATE
OhRuco rdUpdate
OnUpdateReco rdUpdate
Переопределите функцию ОпМогев классе C^arJ 7hWew.Функция CRecordView::On- Move обновляет базу данных, когда пользователь переходит к другой записи. Поскольку такое поведение нас не устраивает, функцию надо переопределить:
IBOOL CEx31bVlew: :OnMove(UINT nlDHoveCommand) <
switch (nIDMoveCommand)
case IO_RECORD„PREV: m_pSet->MovePrev();
if (ln_pSet->IsBOF()) break;
case ID_RECORD_FIRST;
m_pSet->MoveFirst(); break;
case ID_RECORD„NEXT: m pSet->MoveNext(); if <!m_pSet->IsEOF())
break;
if (!m_pSet->CanScroll()) {'
// Очистить экран, поскольку мы находимся за концом набора m_pSet->SetFieldNull(NULL);
break;
case ID_RECORD_LAST;
m_pSet ->MoveLast () •, break;
default;
If непредусмотренное значение■
ASSERT(FALSE);
)
// Показать результаты операции Move
UpdateData(FALSE);.
return TRUE;
}
Добавьте также объявление для этой переопределенной функции в заголовочный файл ex31bView.h.
Отредактируйте обработчики команд меню. Приведенные ниже функции вызывают для редактирования базыданных функции-члены C/?ccordseA Чтобыдобавитьзапись, надо вызвать CRecordset-AddNew, потом Update-, а чтобы модифицировать запись — сначала CRecordset.Edit, затем Update. При добавлении новой записи нужно вызвать CRecordset;:M6ve- Last, так как новая запись всегда добавляется в конец динамического множества.
ПРИМЕЧАНИЕ Если у Вас отсортированный набор записей (или если ODBC-драйвер не помещает добавленные записи в набор), необходимо вызвать CRecordset:;Requeryдля полного переформирования набора записей. К сожалению, в этом случае способа установки ODBC-курсора на новую запись не существует, и это одна из основных проблем SQL.
Добавьте код, выделенный полужирным:
void CEx31bView:;OnRecordAdd()
{
■_pSet->AddNew():
см. след. стр.
UpdateData(TRUE);
if (m_pSet->CanUpdateO) { m_pSet->Update();
}'
if (!m_pSet->IsEOF()) { m„pSet->MoveLast();
m pSet->Requery(); // для отсортированных наборов UpdateData(FALSE):
void CEx31bView::OnRecordClearfields() { m_pSet->SetFieldNull(NULL);
UpdateData(FALSE);
void CEx31bView::OnRecordDelete()
CRecordsetStatus status; try {
n_pSet->Delete();
catch(CDBException* e) { AfxKessageBox(e->m_st rError):
e->Delete() ;
■_pSet->NoveFlrst(); // потеряли свое место!
UpdateData(FALSE); return;•
»_pSet->GetStatus(atatus);
if (status. m_lCurrentRecord == 0) { // Мы удалили 2 последние записи m pSet->MoveFirst();
else { m_pSet->MoveNext();
UpdateOata(FALSE);
void CEx31bView::OnUpdateRecordDelete(CCmdUI‘ pCmdUI) {
pCmdUI->Enable(!m_pSet->IsEOF());
void CEx31bView::OnReeordUpdate()
1 m_pSet->Edit();
UpdateData(TRUE);
if (m_pSet->CanUpdate()) { m_pSet->Update();
// необходимо аоссоэдать( requery) если изменено ключевое поле
void CEx3ibView: :OnUpdateRecordUpdate(CCmd'Jb pCmdUI) pCmd(JI->EnabIe( (m_pSet->IaEOF());
Снова соберите и протестируйте приложение ЕХ31В. Теперь оно позволяет добавлять, изменять и удалять записи. Проверьте, что получится, если ввести запись с неуникальным значением ключа. Появится сообщение об ошибке, генерируемое обработчиком исключений в коде MFC. Чтобы самому обрабатывать ошибки, добавьте в OnRecordAdd операторы try/catch.
Несколько наборовзаписей
Б примерах ЕХ31Аи ЕХ32В используется единственный набор записей. Но зачастую нужен одновременный доступ к нескольким наборам. Допустим, Вы пишете программу, которая позволяет добавить запись к таблице Section, и хотите, чтобы пользователь мог выбрать допустимые CourselD и InstructorlD. В дополнение к главному набору записей Section понадобятся вспомогательные наборы Course и Instructor.
В предыдущих примерах объект «вид» содержал внедренный набор записей, создаваемый конструктором по умолчанию класса CRecordset, что приводило к созданию объекта CDatabase. Функция OnInitialUpdat«Knacca «вид» вызывала CRecordset-:Ореп, та обращалась к виртуальной функции СегОе/аи1:Сотгес№крывала базуданных и вызывала виртуальную функцию GetDefauRSQLIYpoSraMa в том, что здесь может быть лишь один набор записей для одной базы, поскольку объект «база данных» внедрен в объект «набор записей».
Чтобы получить несколько наборов записей, надо поступить по-другому — создать сначала объект CDatabase. Сделав это, можно создавать столько наборов записей, сколько нужно, передавая конструктору CRecordset указатель на CDatabase Для начала встроим в документ вместо объекта CRecordset объект CDatabase, Включим также в класс документа указатель на главный набор записей. Вот переменные-члены класса документа:
CEx31bSet* m_pEx31bSet;.
CDatabase fn_data6ase;
В переопределенной функции CDocument::OnNewDocument Вы создаете в куче главный набор записей, передавая адрес объекта CDatabase как параметр конструктору набора. Вставьте следующий код:
If (m_pEx31bSet =- NULL) {
m_pEx31bSet = new CEx31bSet(&m_database);
CStrlng strConnect = m_pEx31bSet->GetDefaultConhect();'
m_database.Open(NULL, FALSE, FALSE, strConnect, FALSE);
Рис, 31 -5. Взаимосвязи между объектами при использовании нескольких наборов записей
Функция <2RecordView::OnInitialUpdateno-i$pexB.e^ открывает набор записей, но CRecord- set::Open на этот раз не открывает базу данных (она уже открыта). Теперь код, устанавливающий переменную-член tn_р5еГкла<хя. «вид», выглядит несколько иначе:
re_pSet = Ge1.Document()->m^pEx3lbSet;
На рисунке 31-5 показаны новью взаимосвязи между объектами «документ», «вид» и «главный набор записей». Здесь также показаны возможные вспомогательные наборы.
Пример ЕХ31С
Программа ЕХ31С похожа на ЕХ31В, за исключением того, что в ней реализованы новые взаимосвязи базы данных и набора записей, а вспомогательные наборы записей позволяют получать список секций, которые ведет конкретный инструктор. На рисунке представлено окно ЕХ31С.
Соберите и запустите проект ЕХ31С. Последовательно просматривайте записи об инструкторах, наблюдая за изменениями в списке Sections Taught.
Как видите, в форме присутствует новый элемент управления — список. Кроме того, в класс «вид» добавлена короткая вспомогательная функция LoadListbox, заполняющая окно списка строками из набора записей Section:
void CEx31cViow:: LoadListbox( >
CEx31cDoc* oDoc = GetDocument():
CListBox* plB = (CListBox*) GetDlgIter(ipc_SECTlONS):
CSectionSet sect(&pDoc->m_dat.abase); // db передается через конструктор
sect. m_strFilter. Format("InstrucrorID = '%s"‘, (LPCSTR) m_pSet->m_InstructorIO);
sect.Open();
pLB->ResetContent();
while (tsect.IsEOFO) {
pLB->AddString(sect,m_CourseID + " " + sect.m_SectionNo);
sect.Move^ext();
// sect закроется в деструкторе CRecordset }
Обратите внимание, что эта функция определяет строку-фильтр в соответствии со значением поля InstructorlDB главном наборе записей. LoadListbox вызывается из функций-членов OnlnitDiulog, OnMove, OnVpdateyOnRecord^ddTiL CtnRecordDelete.
Параметризованные запросы
В примере ЕХ31С при каждом обращении к вспомогательному набору записей создается и выполняется новый запрос. Однако более эффективно создать один параметризованный запрос, который позволит ODBC привязаться к параметру в вашей программе. Вы просто изменяете значение параметра и заново исполняете запрос. Далее приведены действия по выполнению запроса к набору section для всех секций, которые ведет выбранный инструктор.
Добавьте в класс набора записей section переменную-член для хранения параметра:
CString m_InstructorlDParam;
Добавьте следующую строку в конструктор набора записей section, которая присваивает переменной-члену mnParams значение обшрго количества параметров, равное 1 в данном случае:
m_nParams = 1;
Добавьте следующий код в функцию DoFieldExchange набора записей:
pFX->SetFieldType(CFieldExchange::param);
RFX_Text(pFX, "Param", m_InstructorIDParam); // Любое имя
Добавьте следующий код в класс представления перед вызовом Open для набора записей:
sect.m_strFilter = "InstructorlD = ?";
sect.m_InstructorIDParam = m_pSet->m_InstructorID;
Многопоточность в ODBC
ODBC сам по себе поддерживает многопоточное программирование, но не все ODBC-драйверы. В частности, драйвер Access, использованный в предыдущих примерах, не поддерживает многопоточность, а вот драйвер Microsoft SQL Server — поддерживает. Даже если ваш драйвер ODBC не поддерживает многопоточность, Вы мажете поместить весь код доступа к базе данных в рабочий поток (Многопоточное программирование и рабочие потоки обсуждаются в главе 12.)
Групповая выборка строк
Если Вы используете Microsoft SQL Server или другую клиент-серверную СУБД, то можете ускорить доступ к базе данных, используя имеющуюся в ODBC возможность групповой выборки строк (bulk row fetch), поддерживаемую сейчас в классе CRecordset. Как следует из названия, ваша программа выбирает из источника данных несколько записей за раз вместо одной. Источник данных привязан к элементам массива, который присоединен к объекту класса, производного от CRecordset. В настоящий момент в MFC нет поддержки для добавления, изменения или удаления записей из набора записей с поддержкой групповой выборки. (Подробности см. в разделе «Recordset: Fetching Records in Bulk (ODBC)» интерактивной документации.)
Испол ьзование наборов записей без привязки
Во всех трех примерах этой главы использовались классы наборов "записей, производные от CRecordset, Переменные-члены этих классов привязываются к столбцам базы данных во время создания набора записей при помощи механизма привязки ODBC. Когда программы вызывают CRecordset::Move,драйвер ODBC копирует данные прямо из источника данных в переменные-члены.'
ODBC всегда предоставляет поддержку доступа к данным без привязки через функции SQLGetData и SQLPulData. Класс CRecoVdsetB настоящий момент поддерживает доступ без привязки только для чтения, используя функцию-член GetFieldValue.Oppa переопределенная версия этой функции извлекает значение поля с заданным именем и сохраняет его в объекте
класса CDBVariant Этот класс похож на класс COieVariant, описанный в главе 25, но не использует никакого OLE-кода и не имеет столько переопределенных операторов и функций- членов. В классе CDBVariant есть переменная-член mjiwTyp@L объединение. Если значение mjiwTypeравно, например, DBVT_LONG, то можно обратиться к полю объединения mJVal.
Функцию CRecordset;;GetFteldValue»oxHO использовать в случаях, когда Вам в период проектирования неизвестна схема базы данных. Ваша программа «динамической базы данных» может создать объект класса CRecordsefa обратиться к значениям столбцов примерно так: void CExHdView: :DrawOataRow(CDC* pDC, int у) { int x = 0;
CString strTime, str; -
CEx31dDoc* pDoc = GctDocument();
for (int i = 0; i < pDoc->m_nFleids; i++) {
CDBVariant var; // нужно объявить внутри цикла m_pSet->GetFieldValue(i, var);
switch (var. in dwType) {
case DBVT.STRING:
str = «var.m_pstrlng; // узкие символы
break1•
case DBVT,SHORT:
str. Format("%d", (int) var.m_iVal); break;
case DBVT_LONG:
str.Format("%d", var.m_lVal);
case DBVT.SINGLE:
str.Fo''mat("%10.2f"1 (double) var .m_tltVal);.
break;
case OBVT_DOUBLE:
str. Format ("%10.2f", var.m_dblVal); break;
case DBVT_DATE;
str.FormatC'%d/W*d", var.m_pdate->month. var.m_pdate->day, var. m_pdate->year);
break;
case DBVT_BOOL:
str = (var.m_boolVal == 0) ? "FALSE" ; "TRUE"; break;
case DBVT_NULL: str = "--"; break;
default:
str. Fonnat("Unk type %d\n", var. m_dwType);
TRACE( "Неизвестный тип %d\n“, var.m_dwType);
}
pDC->TextOut(x. y, str);
x += pDoc->m arrayFieldSize[i] * m nCharWidth;
)"
}
Код, приведенный выше, взят из программы-примера EX31D, находящейся на прилагаемом к книге компакт-диске. Эта программа использует код CRowViewHS DAO-примера ЕХ32А следующей главы. Программы EX31D и ЕХ32А сходны по архитектуре и функциям, но EX31D использует ODBC, а ЕХ32А — DAO.
Хотя MFC предоставляет функции GetODBCFieldCaunta GetODBCFieldln/dKnacca CRecordset для получения типа и длины поля, Вы должны вызвать ODBC-функцию ХЦЬТаЫвздля получения «таблицы таблиц». Класс СТаЫеъ проекте EX31D инкапсулирует эту таблицу.
ГЛАВА 32
Microsoft DAO
В главе 31 мы рассмотрели программирование баз данных с использованием библиотеки MFC и Microsoft ODBC В этой главе Вы увидите совершенно иной подход к программированию баз данных — классы MFC DAO (Data Access Objects — объекты доступа к данным) и .скрытое за ними программное обеспечение DAO. Вообще различия этих двух подходов не так уж и велики. Вместо ODBC-классов CDatabaseH CRecordseiBbi будете работать с классами CDaoDatabase и CDaoRecordset. Классы ODBC и DAO настолько похожи (имена многих функций-членов просто совпадают), что ODBC-приложение (возьмите любой пример из главы 31) можно легко преобразовать в DAO-приложение, изменив всего лишь имена классов, ну и еще пару деталей, Таким образом, DAO можно рассматривать как своего рода заменитель ODBC. Но DAO, и Вы в этом убедитесь, вдет гораздо дальше ODBC.
Эта глава — не более чем поверхностное знакомство с DAO, рассказывающее о его возможностях и отличиях от ODBC. Попутно поясняется взаимосвязь между DAO, СОМ, ядром баз данных Jet, Visual Basic for Applications (VBA) и библиотекой MFC. В конце Вы увидите пример динамической работы с базой данных.
DAO, СОМ и ядро баз данных Microsoft Jet
Одна из особенностей DAO — набор COM-интерфейсов, которые, как и любые СОМ-интср- фейсы, являются лишь спецификацией — набором объявлений чисто виртуальных функций. Имена этих интерфейсов строятся по принципу DAOWorkspaceDAODatabase, DAORecordsetH не начинаются с буквы I в отличие от имен большинства других интерфейсов.
Другая особенность DAO — способ реализации этих интерфейсов. Microsoft поставляет СОМ-модуль DAQ35O.DLL, подсоединяемый к той же DLL ядра Jet, которая обслуживает Microsoft Access. Как разработчик на Visual C++, Вы получаете право на бесплатное распространение этих DLL со своими продуктами. В настоящий момент единственной реализацией DAO дня Jet является DAO350.DLL, но ничто не мешает другим компаниям, разработчикам СУБД, выпускать собственные реализации DAO.
DAO и VBA
В главе 25 ВыпознакомилисьсОЕЕ-Автоматизацией. VBA-контроллер Автоматизации (такой как Microsoft Excel или Visual Basic 4.0) способен загрузить любой компонент Автоматизации и использовать его для создания объектов. Сформировав объекты, контроллер может считывать и устанавливать свойства и вызывать методы. Все компоненты, которые Вы создавали в главе 25, работали через интерфейс IDispatch, Однако VBA может общаться с компонентом и через другие интерфейсы.
Заглянув в Реестр Windows, в раздел HKEY_CLASSES_ROOT\iypeLib, Вы обнаружите там идентификатор класса {00025Е01-О0О0-ОО0О-С00О-000ОО00ООО46} и путь к DAO350DLL, содержащей библиотеку типов DAO. Если эту библиотеку задать VBA для ссылок (выбрав, например, команду References из меню Tools в Excel), то ваши программы на VB А смогут использовать объекты DAO, а Вы — просматривать объекты библиотеки DAO (см. рисунок ниже).
Как и серверы, работающие через IDispatch, Microsoft DAO-компонент реализует объекты со свойствами и методами.
DAO и MFC
В библиотеке MFC пять классов для DAO.
Класс
Применение
CDaoWorkspace CDaoDatabase CDaoRecordset CDaoTableDef. CDaoQueryDef
Интерфейс для управления сеансам связи одного пользователя с базой данных
Интерфейс "для работы с базой данных
Интерфейс для работы с наборам записей (табличным, динамическим и статическим)
Интерфейс для управления определением базовой или присоединенной таблицы
Интерфейс для запросов к базе данных
Эти классы в определенной мере скрывают за собой одноименные СОМ-интсрфейсы (например, класс CDaoRecordsetскрывает интерфейс DAORecordsetyOs^asQ в класс CDaoWork- space на самом деле заключены два интерфейса: DAOWorhspacen. DAODBEngine. MFC-классы включают в себя почти все, поэтому прямые вызовы DAO-интерфейсов могут понадобиться только при использовании определенных средств защиты базы данных. Если Вы работаете с библиотекой MFC, о подсчете ссылок заботится она, а если Вы работаете с DAO напрямую, не забывайте вызывать Release для своих интерфейсов.
Как AppWizard, таки ClassWizard поддерживают DAO в полном объеме. С помощью первого можно сгенерировать законченное приложение (использующее формы), которое будет работать как программа ЕХ31В из предыдущей главы, а второй позволяет по конкретной таблице сгенерировать класс, производный от CDaoRecordset.
Варианты работы с базам и данных
DAO поддерживает четыре варианта работы с базами данных.
Открытие базы данных Access (MDB-файла). Такой файл содержит в себе всю базу данных, включая определения запросов, информацию о защите, индексы, взаимосвязи и, конечно же, сами таблицы данных. Вы просто указываете полное имя MDB-файла.
Прямое открытие источникаДаяных ODBC Здесь существенное ограничение: нельзя открыть источник данных ODBC, использующий в качестве драйвера ядро Jet — можно открывать только те источники данных, у которых есть свои DLL-модули ODBC-драйверов.
I* Открытие источников данных ISAM-типа, к каковым относятся файлы dBASE, FoxPro, Paradox, Btrieve, Excel и текстовые файлы, с помощью ядра Jet. Даже при наличии источника данных ODBC, использующего ядро Jet для доступа к файлам одного из этих типов, Выдолжны открывать его как источник данных ISAM-типа (indexed-sequen- tial access method — индексно-последовательный метод доступа), а не как источникданных ODBC
Присоединение внешнихтаблиц к базе данных Access. Это предпочтительный способ доступа к данным ODBC через DAO. Сначала Вы с помощью Access присоединяете ODBC-таблицы к MDB-файлу, а потом открываете MDB-файл, используя DAO как в первом варианте. С помощью Access к MDB-файлу можно присоединить и ISAM-файлы.
Использование DAO в режиме ODBC:
снимки и динамические множества
Как уже говорилось, DAO вдет гораздо дальше ODBC. Но не будем спешить и рассмотрим все по порядку. Мы начнем со снимков (snapshots) и динамических множеств (dynasets) DAO, которые ведут себя почти так же, как и в ODBC. Снимки и динамические множества пригодны для работы с источниками данных ODBC, файлами ISAM-типа и таблицами Access. Программы пишутся на базе MFC-классов GDaoDatabase и GDaoRecordset, очень похожих на ODBC-классы CDatabase и CRecordset. Однако у них есть несколько существенных различий, ,• функцияGDaoDatabase: :GetCurrentRecordработает не так, как функцияCDatabase,vGet- CurrentRecord. Для присоединенных таблиц и источников данных ODBC она всегда возвращает -1, а для таблиц Access и файлов ISAM-типа — число фактически считанных записей, совпадающее с истинным размером набора, только если Вы достигли последней записи. К сожалению, в DAO нет эквивалента ODBC-функции CRecordseteGetStatus, поэтому нельзя проверить, достигли ли Вы конца набора.
। • В DAO можно считать и задать как абсолютную, так и относительную (в процентах) позицию текущей записи в динамическом множестве или снимке, найти запись с определенной строкой и использовать закладки (bookmarks), чтобы пометить запись для последующей выборки.
DAO позволяет легко считывать и устанавливать значения столбцов без связывания (binding), Т. к. значения передаются как переменные типа И1№1АТ.можно писать динамические приложения, которые в период выполнения будут подстраиваться к схеме базы данных. Важно помнить, что в снимках (статических наборах записей) число записей никогда не изменяется. В динамических множествах число записей меняется, только если Вы удаляете или добавляете запись к набору; если запись добавляют другой пользователь, Вы не увидите это в своем наборе. Когда Вы добавляете запись в динамическое множество, она присоединяется к концу набора независимо от порядка сортировки.
Табличные наборы записей DAO
В DAO введен новый тип наборов записей, неизвестный в мире ODBC: табличный набор записей (table-type recordset), поддерживаемый классом CDaoRecordset, — непосредственное представление целей таблицы базы данных. Этот тип наборов можно использовать только для таблиц базы данных Access. Табличные наборы записей, по сравнению со статическими и динамическими множествами, обладают рядом особенностей,
ФункцияФвоЯвсогдкейтС/еЖесогс/СоипГ возвращает приблизительное число записей с учетом добавления и удаления их другими пользователями.
Нельзя использовать функции CD.aaRecordset, работающие сабсолкл’НОй или относительной (выраженной в процентах) позицией записи.
ФункцияСДяоЯесога&е£:.'5ейЬ позволяет искать запись (и переходить к ней) по ключу. Предварительно вызывается функция SetCuiTeritlndex^a выбора соответствующего индекса,
При добавлении записи к табличному набору ее место определяется в соответствии с порядком сортировки, задаваемым текущим индексом.
Табличный набор записей заметно отклоняется от ODBC и SQL Он позволяет выбрать отдельную запись без предварительного запроса, искать запись по одному индексу, а затем последовательно перемещаться, используя другой индекс. Прямо как при программировании на dBASE или FoxPro!
DAO-классы QueryDefw TableDef
В базе данных Access можно сохранять параметризованные запросы (parameterized queries), используя MFC-класс CDaoQueryDef. Кроме того, класс CDaoTableDejncsBanHsi определять таблицы в период выполнения, что удобнее оператора SQL CREATE.
Многопоточность в DAO
Ядро баз данных Microsoft Access Jet не является многопоточным, поэтому и DAO тоже не поддерживает многопоточность. Более того, все вызовы DAO должны находится в основном потоке вашего приложения.
Отображение строк базы данных в окне с прокруткой
Вог Вы и познакомились в самых общих чертах с основами теории DAO. Этого будет достаточно, чтобы перейти к практическому примеру. Но, прежде чем углубиться в изучение кода ЕХ32А, давайте рассмотрим проблему, связанную с отображением строк базы данных в окне с прокруткой. Если бы эту проблему можно было легко решить, разработчики MFC наверняка бы предусмотрели класс CScrollDalabaseViewyBH, такого класса нет, придется писать свой, Это не так уж и сложно, если принять кое-какие упрощения по базе данных. Во-первых, ниш класс, отображающий строки с прокруткой, будет использовать динамическое множество, а значит, сможет подстроиться под любую таблицу, включая таблицы в источниках данных ODBC и файлах ISAM-типа. Во-вторых, мы собираемся только просматривать базы данных, т. е. число строк в динамическом множестве изменяться не будет.
Варианты прокрутки
Реализовать прокрутку на Visual C++ можно множеством способов. Если Вы посмотрите на MFC-программу DAOVIEW (пример для работы с базой данных) с компакт-диска Visual C++, то обнаружите, что там используется MFC-класс CListView,который инкапсулирует стандартный элемент управления Windows — графический список Проблема здесь в том, что надо скопировать в элемент управления все выбранные строки, а эго процесс медленный, к тому же (что важнее) Вы не увидите в таблице изменения, вносимые в данный момент другими
программами. Таким образом, графический список фактически является снимком таблицы базы данных. •
Наше окно с прокруткой будет основано на MFC-классе CScroltView,a код буд ет достаточно шумным», чтобы считывать только те записи, которые нужны сейчас для клиентской части окна. Единственное ограничение — логический размер окна с прокруткой. В Windows 95 этот размер должен быть в пределах ±32767, что ограничивает число строк, которое можно вывести на экран. Если расстояние между строками равно 14 единицам, максимальное число строк составит 2340.
Класс строкового представления
Если Вы читали другие книги по программированию для Windows, то знаете, что их авторы много внимания уделяют прокрутке списков. Программировать их довольно сложно, а сталкиваешься с ними постоянно. Но почему бы не инкапсулировать прокрутку списков в базовом классе? Он скрыл бы от нас все неприятности, связанные с ними, а мы занялись бы непосредственно своим приложением.
Эту работу сделает класс CRowView — адаптированный вариант одноименного класса из MFC-примера СНКВООКс компакт-диска Visual C++. Он служит моделью для производных классов, переопределяющих виртуальные функции базового класса. CRowView свойствен ряд ограничений, так как он не предназначен для серьезных приложений, но вполне подойдет для нашего примера с DAO. На рисунке 32-1 показан текст заголовочного файла.
IROWVIEW.H
Л rowview.il : интерфейс класса CRowView Л
Л Этот класс реализует окно представления с прокруткой, отображающего строки фиксированной .// высоты. Внешне это строковое представление похоже на окно списка с программной отрисовкой;
//////////////////////////////////////////////////////////////х////////////////////////////////////////////////////////////////////////х//////////JJ но оно имеет все преимущества окна представления с прокруткой, // включая самое важное - печать и предварительный просмотр.
//////////////////////////////////////////////////////////////х//////////
//////////////////////////////////////////////////////////////х//////////
class CRowView ; public CScrollView
DECLARE_DYNAMIC(CRowView)
protected:
// Создание/уничтожение
CRowView();
virtual ~CRowView();
// Атрибуты
protected:.■
int m_nRowWidth;//ширина строки в логических единицах
int m_nRowHeight;//высота строки в логических единицах
int m_nCharWidth;// средняя ширина символов в логических единицах
int m_nPrevSelectedRow;//индекс последней выделенной строки
intm nPrevRowCount;//количество строк перед обновлением
int m_nRowsPerPrintedPage; // количество строк на печатной странице
Н Операции-Атрибуты
protected:
virtual void UpdateRow(int nlnvalidRow); // вызывается в OnUpdate производного класса
virtual void CalculateRowMetrics(CDC* pDC)•
{ GetRowWidthHeight(pDC, m_nRowWidth, m_nRowHeight, m_nCharWidth); } virtual void UpcateScrollSizesO;
. virtual CRect RowToWndRect(CDC* pDC, int nRow);
Рис, 32-1. Листинг заголовочного файла CRowViewсм. след. стр.
Рис. 32-1. (продолжение)
virtual int RowToYPos(int nRow);
virtual void RectLPtoRowRange(const CRecti rectLP, ints nFirstRow,
int& nlastRow, BOOL blncludePartiallyShovmRows);
virtual int LastViewableRow();
// Переопределяемые функции protected;
' virtual void GetRowWidthHeight(COC« pDC, lnt8 nflowWidth,
int& nRowHeight, int& nCharWidth) = 0;
virtual int GetActlvsRow() = 0;
virtual int GetRowCount() = 0;'
virtual void OnDrawRow(CDC* pDC, int nRow, int y, BOOL tSelected) = 0;
virtual void ChangeSelectionNextRow(BOOL bNext) = 0;
virtual void ChangeSelectionToRow(int nRow) = 0;
// Реализация.
protected:
// стандартные для MFC-классов переопределенные функции
virtual void OnInitialUpdate();
virtual void OnDraw(CDC* pDC); // переопределение для отрисовки этого окна
virtual void OnPrepareDC(COC* pDC, CPrintlnfo* plnfo = NULL);
virtual BOOL OnPreparePrinting(CPrlntInfo* plnfo);
virtual void 0n8eglnPrlntlng(CDC* pDC, CPrintlnfo- plnfo);
virtual void OnPrint(CDC* pDC, CPrintlnfo* plnfo);
// Сгенерированные функции таблицы сообщений protected:
//{(AFX_MSG(CRowView)
afxjnsg void OnKeyDown(UINT nChar, UINT nRepCnt, DINT nFlags);■ ’
afxjnsg void OnSize(UINT nType, int ex, int cy);
afxjnsg void 0nL8utton0own(UINT nFlags, CPoint point);
//}}AFX MSG
DECLARE MESSAGE MAP()
Разделение труда между базовым и производным классами
Поскольку классу CRowView (производному от CScrolIView') отведена роль базового, его код обобщен, насколько это было возможно. Для доступа к строкам и их представлениям CRawView полагается на производный класс. Хотя класс документа в примере ЕХ32А получает строки из базы DAO, а в примере СНКВООК используется дисковый файл с произвольным доступом, класс CRowView прекрасно подходит в обоих случаях. Он поддерживает концепцию выбранной строки, выделенной в окне представления. Производный класс уведомляется о выборе строки через виртуальные функции-члены CRowView.
Чисто виртуальные функции-члены CRowView
Классы, производные от CRowView, должны реализовать следующие чисто виртуальные функции-члены.
GetRowWidtbHeight. Возвращает ширину и высоту символа текущего шрифта, а также ширину строки на основе средней ширины символа. При переключении контекста устройства между принтером и дисплеем возвращаемые метрики шрифта соответственно! изменяются.
GetAciitteRow. Базовый класс часто вызывает эту функцию, поэтому, если другое окно представления изменит выбранную строку, данное окно может узнать об этом.
Cbe«geSeleccionNexcRow,Cb«ngeSelectionToKow.CnrnMH3HpyvoT производному классу о том, что пользователь выбрал другую строку. Производный класс при необходимости может обновить документ (и другие представления).
OnDrawRow. Вызывается из GRozrVtew. O»L>>wi<; для отрисовки конкретной строки.
Диалоговая панель в MFC; ‘‘! :;■ : - .
. До сих пор мы ничего не говорили о классе CDialogBar, так как не было смысла его использовать. (Диалоговая панель (dialog bar) — эго дочернее окно основного окна- ; рамки, создаваемое по шаблону диалогового окей, из ресурса й генерирующее s<oMaH- .
■ ды на манер панели инструментов.) Однако этот класс хорошо подходит к нашему ?
■ примеру с DAO. Взгляните на рисунок 32-2. Диалоговая панель содержит поле ввода 1 строки запроса SQL, а также кнопку для повторного исполнения запроса. Кнопка по-? а . сылает командное сообщение, обрабатываемое объектам «вид», и может быть отклю-; : чена обработчиком, обновляющим командный пользовательский интерфейс, Диалого
. выв панели располагаются обычно в верхней части окон-рамок, непосредственно под панелью инструментов. Добавить диалоговую строку и приложение на удивление про- ;
' ста Вам даже не понадобится новый производный класс. -'~
Создайте шаблон диалогового окна в графическом редакторе. Установите следую- •
■ ■ щие стили:. '• • ' -- / i ...■.—'
' . Style= Child ;" *7 ';■ ■.:' '
г-- Border - None. ' .•'. .,,
:. Visible - отключен (окно изначально невидимо) .'-
’" ' Можно выбрать либо горизонтально расположенную панель (вверху или внизу окна), '■ - 1 либо вертикально (слева Или справа). Добавьте элементы управления, в том числе
• кнопки и поля ввода.:.1
В объявлении производного класса окна-рамки объявите внедряемый объект клас
’ са CDialogBaf: /"."...•■'' . '.. • .. . :
:' , CDialpgBaf m_wndMyBar( у' - " "'';:
J. Добавьте в функцию-член OnCrente класса основного окна код, формирующий объ-
■ ект — Диалоговую панель: ,., - ^.у ; ■. ? ; • ; • v '. < :\5 ;у'Ч7
if (! ajmdHyBar. Crea^WfeJK-BAR» CBRS^TOp-, i^MY_BAR-)> {~\7у#;
■ :^ACe(" Ошибка издания диалоговой nW :--7'7.
1“ 'return -1; .'T-' " ' ’ ' 7 7/v'-'"' ■ ■':
■/ i л : ■ ■.
' Идентификатор диадогрвд^Р&^рЬаТФ^в графическом.;.:
1 редакторе. Стиль <^ВЙ5_ТОРзод®$расположение диалоговой панели вверху основнр-;
го окна. {D_jW_BAR— вдрнтификатор окна диалоговой панели, который должен укла- ' дыватъея в диапазон от ОхЕ8Об до 0хЕ820, чтобы окно предварительного просмотра ^ .
' перед печатью замещало диалоговую панежг * * . • *. .J ■: > ■.>'<« i*r№4 ^ А *, =.■ -v -"■• i-1r JH ' '
Другие функции CRowView
Производные классы и каркас приложений могутвызывать и три другие функции CRowView. m tipdateRoioBra, открытая функция инициирует обновление изображения при смене выбранной строки. Обычно в перерисовке нуждаются только новая и предыдущая строки, поэтому прямоугольник, подлежащий перерисовке, охватывает обе строки. Если изменилось общее число строк, UpdateRotaBKSHBaer UpdateScroHSizes.
UpdateScrollSizes^o виртуальная функция, так что при необходимости ее можно переопределить. Ее реализация в классе CRowVieu)обновляет размер представления, что вызывает перерисовку видимой части. К этой функции обращаются OnStze и Onllpdate (после выполнения нового запроса).
OnPrtnt. Класс CRowView переопределяет эту функцию для такой подгонки точки отсчета области вывода (viewport origin) и отсекающего прямоугольника (clipping rectangle), чтобы OnDraw выводила страницу иа печать точно также, как и в окно,
Программирование динамического набора записей
Если Арр Wizard используется для создания DAO-приложения, он генерирует класс, производный от CDaoRecordsetc функцией DoFieldExcbange,KOTopaa выполняет привязку переменных- членов к столбцам соответствующей таблицы базы данных. Однако в случае класса динамического набора записей нужно определять имена столбцов и типы данных в период выполнения программы. В примере ЕХЗ1А показано, как это сделать при работе с ODBC.
В случае DAO все проир — Вы просто создаете объект CDaoRecordset и вызываете функцию-член GetFieldVdlue, возвращающую значение столбца как VARIANT, а прочие функции- члены сообщают число столбцов в таблице, их имена, тип и ширину.
ПРИМЕЧАНИЕ Если поле со значением VARIANT содержит BSTR, то предполагайте, что строка состоит из 8-разрядных символов. Это исключение из правила — все В$ТЯ-строки состоят из двухбайтовых символов.
Пример ЕХ32А
Теперь соберем все воедино и попробуем создать MDI-приложение, способное подключаться к любому источнику данных DAO. Оно динамически выводит таблицы в окнах представления с прокруткой и позволяет вводить оператор SQL QUERY, сохраняемый в документе вместе с информацией об источнике данных и таблице. Арр Wizard генерирует обычные классы основного окна-рамки MDI, документа, приложения и представления, а мы изменяем базовый класс «вид» на CRowView и добавляем код, специфичный для DAO. На рисунке 32-2 показана программа ЕХ32А в действии.
Рис. 32 -2. Программа ЕХ32А в действии
Меню File для документа содержит команды:
DAO Open MDB
DAO Open ISAM
DAO Open ODBC
Открыв документ, пользователь должен выбрать одну из них команд. Код, открывающий базу данных, зависит от типа источника данных.
Вы мажете многое узнать об этом приложении, посмотрев на его окно с тремя окнами представления (см. рис. 32-2). Два верхних окна связаны с одним документом, а нижнее — с другим. В диалоговой панели показан оператор SQL, связанный с активным окном представления.
В таблице перечислены файлы и классы программы ЕХ32А.
Заголовочный файл
Файл с исходным кодом
Класс
Описание
Ex32a.h
Ex32a.cpp
CEx32aApp
Приложение
MainFrm.h
MainFrm.cpp
CMainFrame
Основное окно-рамка MDI
ChildFrnLh
ChildFrm.cpp
CChlidFrame
Дочернее окно-рамка MDI
Ex32aDoc.h
Ex32aDoc.cpp
CEx32«Doc
Документ ЕХ32А
Ex32aView.h
Ex32aView.cpp
CEx32aView
Класс представления базы данных с прокруткой
Rowvicw.h
Rowview.cpp
CRowView
Базовый класс строкового представления
Tablesel.h
Tablesel.cpp
CTableSeleci
Диалоговый класс выбора таблицы
IsamSelecth
IsamSelect.cpp
CIsamSelect
Диалоговый класс выбора источника данных ISAM-типа
StdAfx.h
StdAfxcpp
Предкомпилированные заголовки
Теперь последовательно рассмотрим классы этого приложения (кроме CRowVieufy. Вы познакомитесь с их важнейшими элементами данных и основными функциями-членами.
СЕх32аАрр
Класс приложения сгенерирован AppWizard и оставлен без изменений. В нем нет ничего особенного.
CMainFrame и CChlidFrame
Стандартные классы, сгенерированные AppWizard; добавлена только диалоговая панель, создаваемая в функции-члене CMainFrame::OnCreate.
CEx32aDoc
Класс документа управляет связями с базам данных и наборами записей. Каждый объект-документ поддерживает один основной набор записей, связанный с одним источником данных. К этому объекту можно присоединить несколько окон представления. У каждого документа свой источник данных (представленный объектом CDaoDatabasey
П временные -члены
В таблице представлены наиболее важные переменные-члены класса CEx32aDoc.
Переменные-члены
Описание
т_pRec.ordset
Указатель иа объект «набор записей*
mjtatabase
Внедренный объект CDaoDatabose
m_strDatabase
Путь к файлу базы данных (MDB-фаЙлу)
msirConnecI
Строка ODBC- или ISAM-соединения
23 Зак. 1*787
Переменные-члены
Описание
mstrQuery
Полный оператор SQL SELECT
mJbConnected
Признак, равный TRUE, когда документ подсоединен к набору записей
m_nFieZds
Количество полей (столбцов) в наборе записей
m_nRau>Count
Количество записей (строк) в наборе записей
m_nDatabascTypc
Одна из констант впит (UNK.MDR, ISAM, ODBC}
OnOpenDocument
Эта переопределенная СОосмтиепГ-функция вызывается при загрузке документа с диска. Документ содержит имя базы данных и строку запроса, поэтому программа может открыть базу данных и выполнить запрос во время загрузки.
BOOL CEx32aDoc::OnOpenDocument(LPCTSTR IpszPathNamo),
* if (ICDocument: :OnOpenDocument(lpszPathMans))
return FALSE;
PutQueryO;
switch (m_nDatabaseType) {
case UNK;
break;
case MDB:
DaoOpenMdbO;
break;
case ISAM:
DaoOpenIsam();
break;
case ODBC:
DaoOpenOdbcO;
break;
return TRUE;'
I
OnCioseDocument
Эта переопределенная СОосмгийЯ-функция закрывает базу данных, если она подсоединена:
void CEx32aDoc: :0r,CloseDocumentO {
m_strQuery.Empty();
PutQueryO;
if (m_bConnected) {
delete m_pRecordset; // Деструктор вызовет Close
m_databasAClose();
m_bConnected = FALSE;
m_pRecordset = NULL;
m_nRowCount = 0;
"
CDocument::OnCloseDocument();
1
OnFUeDaoOpenOdbcw DaoOpenOdbc
Эти функции вызываются по команде DAO Open ODBC из меню Fib. DaoOpenOdbc, которая также вызывается из OnOpenDocument, обращается к CDaoDatabaseoOpen, передавая ей строку подключения как параметр. Страка «ODBC, » заставляет открыть диалоговое окно ODBC для выбора источника данных. Обратите внимание на использование блока fry/catch для обнаружения ошибок обработки SQL
void CEx32aDoc: :CnFileDaoOpenOdbc()
{
m_strConnect.Empty();
m_strQuery.Empty();
DaoOpenOdbcO;
void CEx32a0oc: :DaoOpenOdbcO
// нельзя открывать ODBC через драйвер Access
if (m_strConnect.IsEmpty()) {
m strConnect = "ODBC: ";
} "
BeginWaitCursor();
tty {
// немонопольно, только для чтения
m database. Орел( "", FALSE, TRUE, m strConnect) ;
} "'
catch (CDaoExceptlon* e) {
::DaoErrorMsg(e);
EndWaitCursor();
e->Delete();
return;
}
m_strConnect = m_database.GetConnect();
TRACE("database name = %s, connect = %s\n", (const char*} m_strDatabase, (const char») m„strConnect);
OpenRecordset();
m_nDatabaseType= ODBC;
EndWaitCursorO;
OnRIeDaoOpenlsam и DaoOpenlsam
Эти функции вызываются по команде DAO Open ISAM из меню File. DaoOpenlsam, которая вызывается также из OnOpenDocument, запрашивает у пользователя имя каталога (через класс ClsamSelect)'a вызывает CDaoDatabase::Openc соответствующей строкой подключения. Строка С/удам^е/^.т^г/гЛвтшред^ файла, например: «dBASEIII», «FoxPro 26» или «Excel 8.0». void CEx32aD6c: :OnFileDaoOpenIsam() {
m_strConnect.Empty();
m_strQuery.Empty();
DaoOpenlsamO;
}
void CEx32aDoc: :DaoOpenIsem()'
BeginWaitCursorO;
if (m_strConnect. IsEmptyQ) {'
CIsamSelect isamDlg:
if (isamDlg. DoModal() != IDOK) {
return;
m_strConnect = isamDlg.m_strlsam + "; DATABASE=" + isamDlg.m_strDirectory;
TRACEC'm strConnect = %s\n", (const char*) m_strConnect);
}
CMwOied. cmp.
try {
// немонопольно, только для чтения
m database.Open("", FALSE, TRUE, m strConnect);
}
catch(CDaoException- e) {
::DaoErrorMsg(e);
EndWaitCursor();
e->Delete();
return;
m_strConnect = m. database.GetConnect();
TRACE("database name = %s, connect = %s\n“,
(const char-) m_strDatabase, (const char*) m_strConnect);
OpenRecordsetO;
m_nDatabaseType = ISAM;
EndWaitCursorQ;
OnFlleDaoOpenMdb и DaoOpenMdb
Эти команды вызываются по команде DAO Open MDB из меню File. Функция DaoOpenMdb, которая вызывается также из OnOpenDocument, запрашивает у пользователя полное имя MDB- файла (через МРС-классС^1еОйя/с^).Сравните вызов CDaoDatabase;:Openc аналогичными вызовами в двух предыдущих функциях. Заметьте: первым параметром передается полное имя MDB-файла.
void CEx3?aDoc::OnFjleDaoOpenMdb()
•I'
im_strDatabase.Enipty();■
m_strQuery. Empty();
DaoOpenMdbO;
void CEx32aDoc:: DaoOpenMdbO
{
if (m.strDatabase.IsEmptyO) {
CFileDialog dlg(TRUE, ".mdb", "*.mdn”);
. if (dlg.DoModal() ==IDCANCEL) return;
m_strDatabase - dlg.GetPathMame();
BeginWaitCursor();
try { // немонопольно, только для чтения
m database.Open(m strDatabase, FALSE, TRUE);
>
catch (CDaoException* e) {
: :DaoErrorMsg(e);
EndWaitCursor();
e->Delete();
return;
}
m_strDatabase = m_database.GetName();
TRACE("database name = %s, connect = %s\n",
(const char*) m_strOatabase, (const char*) m_strConnect);
■OpenRecordsetO;
m^nDatabaseType = MDB;
EndWaitCursorQ;
OnFileDaoDiseonnect
Эта функция закрывает базу данных DAO, разрешая запись документа.
void CEx32aDoc: :OiFileDaoDisconnect()
if (m_bConncctcd) {
delete r._pRecordset; // Деструктор вызовет Close
m_database.Close();
m_bConnected = FALSE;
m_pReccrdset = NULL;
m_nRowCount = 0;
UpdatcAllViewsfNULL);
}
OpenRecordset•
Вспомогательная функция, вызываемая из DaoOpenOdbc, DttoOpenlsamn DaoOpe«Mdb. Класс CTableSelect позволяет выбрать имя таблицы, используемое при сформировании оператора SELECT. Вызовы CDaoRecordset^Motielxidn. GetAbsolutePositiottycTaHawtrntaroT количество записей для источников данных ODBC, ISAM и MDB.
void CEx32aDoc: : OpenRecordsctO
{
GetQueryO;
if (m_strOLery.IsEmpty()) {
CTableSelect tab]ei);g(&m_database);
if (tableDlg.DoModal() ! = IDOK) ( m_database. Closet ); // выход no ESC return;
}
m_strQuery. Format("select * f'om [%s]", tableDlg.m_strSelectidh);
PutOueryO;
m_pRscordsct = new CDaoRecordset(&r_catabase);
try {
m pRecordset-xOpenfdbCpenDynaset, m_strGuery, dbFeadOnly);
} "
catch (CbaoExcepLioro e) {
::DaoErrdrMsg(e);
UpdateAHViewsf NULL);
ra.bCcnnoctcd = FALSE: e->Dclete();
return;
if (! m_pRecordsel:->lsBOF()) {
// может быть очень долго для большой таблицы
// Объект "вид" изменит свою переменную-член m_nRowCount
// если здесь окажется большое значение
m_pRecordset->MoveLast(); // для определения количества записей } m_nRowCount = m_pRecordset->GetAbsoldtePosition() + 2;
TRACE("m_nRowCount = %d\n", m_nRov<Court);
GetFieldSpecsO;
UpdateAllVlews(NULL);
m bConrected = TRUE;
ПРИМЕЧАНИЕ В MFC-классе CDaoRecordset есть переменпые-члепы mjstrFilteru m_strSort, как и в ODBC-классе CRecordset. Одпаго эти строки использовать нельзя, если в наборе записей нет связанных полей; Вы должны полностью сформировать оператор SELECT, как показано выше.
OnRequery''
Вызывается при щелчке кнопки Requery в диалоговой панели. Считывает строку запроса и заново формирует набор записей. Обратите внимание, что функция CDaoRecordset: .Requery не обрабатывает обновленный оператор SELECT, поэтому мы закрываем набор записей и открываем его заново.
void CEx32aDoc::OnRequery()■ 1
' GetQueryO;
Ц Requery не применима, так как мы изменили оператор SQL
ReginWaitCursorO;.
lf(m_pRecordset->IsOpen()) {
m pRecordset->Close();
}'
try {■.■
ra_pRecordset->Open(dbOpenOynaset,m_strQuery, dbReadOnly);
catch (CDagException* e) {
: :DaoErrorMsg(e);
m nRowCount = 0;
UpdateAllViews(NULL);
EndWaitCursor();•
e->Delete();
return;'
if (!m_pRecordset->IsBOF()) {
m pRecordset->MoveLast(); // для определения количества строк
m_nRowCount = m_pRecordset->GetAbsolutePasition() + 2;
TRACE("m_nRowCount = %d\n", m_nRowCount);
GetFieldSpecsO;
UpdateAllViews(NULL);
EndWaitCursorO;
}
PutQueryn GetQuery
Эти функции-утилиты переносят строку запроса в поле ввода и из него (в диалоговой панели).
Serialize
Осуществляет чтение-запись переменных-членов m_strConnect, m_strDatabose и mstrQuery.
CEx32aVlew
Это класс, производный от CRowViewa реализующий его виртуальные функции.
Переменные-члены'
Целочисленная переменная m_nSelectedRow используется для хранения номера выбранной строки. Указатель на набор записей содержится в т J)Set.
OnUpdate■
Эта виртуальная СУ/еи^-функция вызывается через каркас приложений при создании окна представления и при изменении содержимого документа в связи с открытием базы данных или запросом. Если для данного документа активны несколько окон представления, все они
показывают результаты последнего запроса, но в каждом из них может быть выделена своя строка. Функция ОпСрЖнеусганавливает также значение mJ)Set. OnlnitialUpdatecAenaTb этого не может, поскольку на момент ее вызова набор записей еще не открыт..
GetRowWidthHeight, GetActivaRow, ChangeSetectionNextRowM ChangeSelecttonToRow .
Это реализации чисто виртуальных функций класса СRowView. Они отвечают за отрисовку заданной строки, полученной в результате запроса, а также отслеживают текущую выделенную строку.
GetRowCount.
Эта виртуальная функция, вызываемая из CRowView, просто возвращает количество строк, хранящееся в объекте-документе.
OnDrawflowH DrawDataRow
Виртуальная функция OnDrowRow вызывается функциями-членами CRowViewftna отрисовки заданной строки. OnDrawRow считывает текущую строку в наборе записей и вызывает функцию CDaoRecordsetiMovepAa установки курсора и чтения данных. Блок try/caich отлавливает фатальные ошибки, связанные с невозможностью чтения данных. Вспомогательная функция DrawDataRow просматривает столбцы и выводит их значения. DrawDataRow выводит сообщение «‘‘RECORD DELETED"», обнаружив запись, удаленную сторонним пользователем после создания данного динамического множества. Текст функций OnDrawRow и DrawDataRow- void CEx32aView: :OnDrawRow(CDC* pDC, int nRow, int у, BOOL bSelected)
■i
Int x = 0;
int i;
CEx32aBoc* pDoc = GetOocumentO;
if (m_pSet== NULL) return;
if (nRow== 0) { // строка заголовка ,
for (i = 0; i < pDoc->m_nFields; i++) {
pDC->TextOut(x, y, pDoc->m_arrayFieldName[i]);
x += pDoc->m arrayFieldSize[i] * m nCharWidth;
}■
}.
else {
try {
m_pSet->SetAbsolutePosition(nflow - 1);■
// учесть строку заголовка // SetAbsolutePosition не генерирует исключение пока не окажется за концом набора if (m_pSet->GetAbsolutePosition() == (nRow - 1)) {. -
DrawDataRow(pDC, у);
}'
}.
catch (CDaoException* e) {
// Может быть задержана перед выводом информации об удалении
if (e->m pErrorInfo->eilErrorCode == 3167) {
pDC->fextOut(O, у, ”*«RECORD DELETED*»”);-
else (.
m_pSet->MoveLast(); // в случае, если M_nRowCount слишком велика pDoc->m nRowCount = m pSet->GetAbsolutePosition() + 2;
.
e->Delete();-1
}
iсм. след. стр.
void CEx32aView::DrawDataRow(CDC* pDC, int у)
{
int x = 0;
CString strTime;■
COleVariant var;
CString str;
CEx32aDoc* pDoc = GetDocument();
for (int i = 0; i < pDoc->m_nFields; i++) {'
var = m_pSet->GetFieldValue(i):
switch (var.vt) {
case VT_BSTR:
str = (LPCSTR) var.bstrVal; // в DAO узкие символы
break;i
case VT_I2:
str.Format("%d“. (int) var.iVal);
break;
case VT_I4:
str.Format("%d", var.iVal);
break;'
case VT_R4:
str.Formate"«10.2f", (double) var.fltVal);
break;
case VT_R6:
str. Format( "«10.2f", var.dblVal);
break;.
case VT_CY:
str = C01eCurrency(var).Format();
break;
case VT_DATE:
str = COleDateTime(var).Format();
break;■
case VT_BOQL;
str = (var. bool Vai ^= o) ? "FALSE" : "TRUE";
break;
case VT.NULL:'
str = break;
default:
str. Format( "Unk type *d\n", var.vt);
TRACE("Неизвестный тип %d\n", var.vt);
pDC->TextOut(x, y, str);
x += pDoc->m arrayFieldSize[i] * m nCharWidth;
)■
}.
OnbiitielUpdete и OnTImer
Поскольку мы работаем с динамическим множествам, нам нужно видеть изменения в базе данных, вносимые другими программами. Обработчик таймера вызывает CWncklnvalidate, что приводит к обновлению всех записей, отображаемых в клиентской части окна:
void CEx32aView: :OnInitialUpdate()
CRowView::OnInitialUpdate();
void CEx32aView::DnTimer(UINT nIDEvent)
(
Invalidate(); // обновить представление базы данных
CTableSelect
Это сгенерированный ClassWizard класс диалогового окна, содержащего список для выбора таблицы. В случае базы данных для учета студентов диалоговое окно выглядит, как показано на рисунке.
Переменные-члены
Класс CTableSelect содержит следующие переменные-члены.
Переменная-член
Описание
m_pDatabase
Указатель на объект CDaoDatabase
m_strSelection
Сгенерирована ClassWizard; соответствует строке, выбранной в окне списка
Конструктор
Конструктор принимает параметр — указатель на базу данных, используемый им для установки переменной-члена т_pDatabase:
CTableSelect::CTableSelect(CDaoOatabase* pOatabase,
CWnd* pParent /*=NULL*/)
: CDialog(CTableSelect::IDD, pParent)
a
//{{AFX_DATA_INIT(CTableSelect)
m strSelection = "";
//}}AFX_DATA_INIT
m pOatabase = pDatabase;
}
OnlnUDialog
Создает, открывает и считывает список таблиц в источнике данных, после чего помещает имена таблиц в окно списка:
BOOL CTableSelect::OnInitOialog()
{
CListBox* pLB = (CListBox*) GetDlgItem(IDC_LIST1);
int nTables = m_pDatabase->GetTableDefCount();
TRACE("CTableSelect::OnInitDialog, nTables = %d\n", nTables);
CDaoTableDeflnfo tdi;
for (int n = 0; n < nTables; n++) {
m_pDatabase->GetTableDefInfo(n, tdi);
ТЯАСЕ("иня таблицы = %s\n", (const char*) tdi.m_strName);
if (tdi.m_strName.Left(4) != "MSys") { pLB->AddStri ng(tdi.m_strName);
.)
return CDialog: :OnInitDialog();
OnDbklkLIstl■
Пользователю удобно выбирать элементы списка двойным щелчком. Эта функция обрабатывает соответствующее уведомляющее сообщение от окна списка:
void CTableSelect: :0nDblclkLlst1(){
OnOK(); // Двойной щелчок на элементе списка закрывает диалоговое окно
CisamSelect.
Это сгенерированный Class Wizard класс диалогового окна, содержащего список и поле ввода для выбора источника данных ISAM-типа, Пользователь должен ввести каталог, где содержатся нужные файлы.•
Переменные-члены
Класс CisamSelect содержит следующие переменные-члены.
Переменная-член
Описание
mstrlsam
Переменная, сгенерированная ClassWizard и соотвегствующаястроке, выбранной в списке■•.■
mstrDirectory
Переменная, сгенерированная ClassWizard и соответствующая содержимому поля ввода.
OnlnitDialog
Заполняет окно списка значениями, описанными в разделе Connect Property в Books Online:
BOOL CisamSelect;:OnInitDialog() '■
{
CListBox» pL8 = (CListBox») 6et01gItem(I0C LIST1);
pLB->AddString("dBASE III");
pLB->AddString("dBASE IVм);■
pLB->AddString("dBASE 5");
pLB->AddString("Paradox 3.x");
pLB->AddString("Paradox 4.x");
pLB->AddString("Paradox 5.x");
pLB->AddString("Btrieve");
pLB->AddString("FoxPro 2.0");
pLB->AddString("FoxPro 2.5");'
pLB->AddString("FoxPro 2.6");
pLB->AddString("Excel 3.0");'
pLB->AddString("Excel 4.0“);'
pLB->AddString("Excel 5.0");•
pLB->AddString("Excel 7.0");
pL8->AddStrlng("Text");
CDialog::OnInitDialog();
return TRUE; // Возвратите TRUE если не установлен фокус ввода.
// ИСКЛЮЧЕНИЕ: Страницы свойств OCX должны возвращать FALSE.
Файл ресурсов ЕХ32А
В приложении ЕХ32А используется диалоговая панель, а для нее нужен диалоговый ресурс.
Сама диалоговая панель показана на рисунке 32-2. Идентификатор диалогового ресурса - IDDjyjERYJBARN нее следующие элементы управления,
Элемент управления
Идентификатор
Кнопка
IDCJiEQUERY'
Попе ввода
IDCJjL’ERY
Для шаблона диалогового окна заданы следующие стили:
Style = Child
Border = None
Visible = отключен (окно изначально невидимо)
Кроме того, есть шаблон диалогового окна для выбора таблицы IDD_TABLE_SBLEd> котором содержится окно списка lDC_USTlt и шаблон диалогового окна для выбора ISAM-источника IDD_ISAM_SELECTB меню File добавлено четыре элемента.
Элемент меню
Идентификатор команды'
DAO Open MDB DAO Open ISAM DAO Open ODBC DAO Disconnect
ID_FILE_DAOOPEN_MDB
1D_F1LE_DAOOPEN_1SAM
H)_FILE_DAOOPEN_ODBC'
IDJFIUiJDAODlSCONNECT
Работа с программой ЕХ32А
Программа работает с любым источником данных DAO, но сначала попробуйте открыть базу данных STDREG32.MDB. Для проверки программы в многопользовательском режиме, запустите одновременно с ней и программу ЕХ31В. Измените с помощью ЕХ31В данные об инструкторах, когда эта таблица отображается в ЕХ32А.
Шаблоны OLE DB
О главах 31 и 32 рассмотрены альтернативные варианты управления базами данных с использованием MFC версии 6.0 и Visual C++ 6.0; ODBC и DAO. Корпорация Microsoft определила еще один способ доступа кданным, называемый OLE DB, В этой главе описываются шаблоны OLEDB (OLE DB templates), реализующие поддержку со стороны Visual C++ 6.0 прямого' доступа кданным через OLE DB. В то время, как ODBC разработан, в первую очередь, для предоставления доступа ко всем типам SQL-данных в многоплатформенной среде, OLE DB предназначен для обращения ко всем типам данных внутри одной системы. Для достижения этого OLE DB использует СОМ. Технология OLE DB очень гибка — она поддерживает всю функциональность SQL, определенную в ODBC, а также описывает интерфейсы, пригодные для получения доступа к не-SQL типам данных.
Доступ кданным через OLE DB разделен на две основные категории: потребители (consumers) и постатцики (providers). Сначала мы рассмотрим основы архитектуры OLE DB и увидим, как работают шаблоны потребителей, а затем обратимся к другой стороне OLE DB и изучим шаблоны поставщиков.
Понятие OLE DB
OLE DB предоставляет единообразный способ доступа ко всем типам источников данных. Например, представьте себе всевозможные источники данных в обычной организации: системы учета, файловые системы, электронные таблицы, персональные базы данных (XBase и Btrive) и электронную почту. Проблема состоит в том, что для каждого источника нужен свой протокол, который Вам придется изучить. (Уф1) OLE DB является промежуточным уровнем, который создает единообразие при доступе кданным из различных источников. При использовании OLE DB разработчикам клиентских приложений необходимо учитывать лишь мелкие детали для того, чтобы получить доступ кданным (вместо изучения множества различных протоколов).
Самое важное, что нужно знать об OLE DB, — в ее основе лежит СОМ. Другими словами, OLE DB — это набор Act i vcX-интсрфсйсов для доступа к данным через СОМ, Интерфейсы OLE DB являются общими для предоставления единообразного доступа кданным, независящего от метода хранения данных. Например, разработчики используют одни и те же интерфейсы для получения данных из источников информации, как типа СУБД, такидругих, В тоже время OLF DB по толяет использовать преимущества нижележащей технологии базы данных (такие, как скорость и гибкость) без необходимости перемещать данные только для получения доступа к этим преимуществам.
На самом верхнем уровне архитектура OLE DB состоит из потребителей и поставщиков. Потреби гель — это любая часть кода системы или приложения, которая использует интер-
фейс OLE DB, в том числе и сами компоненты OLE DB. Поставщик — это любой программный компонент, который предоставляет интерфейс OLE DB.
Есть два типа поставщиков: поставщики данных (data providers) и поставщики услуг (service providers). Названия говорят сами за себя. Поставщики данных владеют данными и предоставляют их внешнему миру в табличной форме в вида набора строк (rowset). Примеры поставщиков данных включают реляционные СУБД, диспетчеры хранилищ, электронные таблицы и ISAM- базы данных.
Поставщик услуг — это любой компонент, который не владеет данными, но инкапсулирует некоторую услугу путем переноса данных через интерфейсы OLE DB. В определенном смысле, поставщик услуг является одновременно и потребителем. Например, обработчик разнородных запросов (heterogeneous query processor) является компонентом, предоставляющим услугу. Когда потребитель пытается объединить данные из таблиц от двух разных источников, обработчик запросов сам в качестве потребителя извлекает строки из наборов, созданных на основе каждой из таблиц. Затем, уже в качестве поставщика, обработчик запросов создает единый набор строк и возвращает его своему потребителю.
В общем, в реальном мире есть много различных типов данных и способов доступа к ним. Однако многие разработчики знают, как манипулировать данными, используя стандартные: технологии управления базами данных. OLE DB определяет архитектуру, которая делает доступ к данным «компонентным». В качестве компонентной системы управления базами данных OLE DB предоставляет большую эффективность, чем традиционные СУБД, благодаря разделению функциональности базы данных на потребителей и поставщиков. Так как потребителям данных обычно нужна только часть функциональности управления базами данных, то разделение этой функциональности снижает потребность клиента в ресурсах,
По той же причине OLE DB снижает бремя, лежащее на поставщике, поскольку позволяет ему сосредоточиться только на предоставлении данных (и не думать о проблемах клиента). Например, OLE DB позволяет простому поставщику табличных данных реализовать функциональность, естественную для своего хранилища данных, но предоставить единый протокол доступа к данным. В этом случае минимальная реализация поставщика может использовать только интерфейсы, предоставляющие данные как таблицы. Это позволяет разрабатывать совершенно различные компоненты обработчиков запросов, которые смогут получать данные от любого поставщика, предоставляющего данные через OLE DB. Кроме того, СУБД на основе SQL, используя интерфейсы OLE DB, также могут разделить свою функциональность на уровни.
Основы архитектуры OLE DB
Помимо определения основ взаимоотношений между потребителями и поставщиками, OLE DB определяет следующие компоненты, каждый из которых является СОМ-объектом.
Перечислители (enumerators) ищут допустимые источники данных. Потребители, не привязанные к конкретному источнику данных, применяют перечислители для поиска подходящего источника данных. -
Объекты источников данных (data source objects) содержат механизм для соединения с источником данных, таким как файл или СУБД. Объекты источников данных генерируют сеансы (sessions),■
Сеансы (sessions) представляют соединения с базой данных. Например, сеансы обеспечивают контекст для транзакций с базой данных. Один объект источника данных может создать множество сеансов. Сеансы генерируют транзакции, команды и наборы строк.
Объекты транзакций (transaction objects) используются для управления транзакциями в целях поддержки защиты базы данных.
Команды (commands) выполняют текстовые файлы, такие как SQL-операторы. Если текст задает набор строк, как это делает оператор SELECT, команда генерирует набор строк. Один сеанс может создать множество команд.
f • Наборы строк (rowsets) предоставляют данные в табличном формате. Специальным случаем набора строк является индекс. Наборы строк могут быть созданы сеансом или командой.
Ошибки (errors) могут быть созданы любым интерфейсом любого OLE DB-объекта. Они содержат дополнительную информацию об ошибке, включая необязательный нестандартный объект ошибки (custom error object).
Вот пример того, как применить эти компоненты для создания OLE DB-потребителя. Если Вы не уверены в расположении источника данных, можете сначала использовать перечислитель для его нахождения. Определив источник данных, создайте сеанс соединения с ним, который позволит получить доступ к данным в виде наборов строк, а также создать команды, генерирующие такие наборы.
Преимущество применения OLE DB состоит в том, что Вы получаете превосходный единообразный способ доступа к разнообразным источникам данных. Недостаток — для этого нужно реализовать связку COM-интерфейсов. Здесь-то и помогают шаблоны OLE DR
Основы архитектуры шаблонов OLE DB
Теперь, когда Вы понимаете основы архитектуры OLE DB, пришло время взглянуть на конкретную реализацию интерфейсов OLE DB (предоставляемую шаблонами потребителя и поставщика). Подобно другим технологиям на основе COM, OLE DB требует реализации связки интерфейсов. Конечно, как и в случае элементов управления ActiveX, можно реализовать их вручную (зачастую неэффективный подход, за исключением случая, когда Вы пытаетесь детально понять технологию) или найти кого-нибудь другого для выполнения большинства черновой работы. Хотя OLE DB — это разнообразная и мощная технология доступа к данным, заставить ее работать вручную — весьма скучная работа.
Помимо библиотеки шаблонов (ATL) для реализации ActiveX-элементов, VisualC++ предоставляет библиотеку шаблонов, помогающую работать с OLE DB. Эта библиотека содержит классы, реализующие большинство часто используемых интерфейсов OLE DB. Кроме того,. Visual C++ предоставляет мастера для генерации кеда на основе типовых сценариев,
На самом верхнем уровне Вы можете разделить классы в библиотеке шаблонов на две группы, определенные самим стандартом OLE DR классы потребителей и классы поставщиков. Классы потребителей помогают реализовать приложения клиентов (потребителей) базы данных, а классы поставщиков — серверов баз данных. Вспомните, что OLE DB-потребители - это приложения, вызывающие COM-интерфейсы, которые предоставлены OLE DB-поставши- ками услуг (или обычными поставщиками) для доступа к данным. OLE DB-поставщики — это СОМ-серверы, предоставляющие данные и услуги в виде, который понятен потребителю.
Архитектура шаблонов OLE DB-потребителя
Microsoft постаралась сделать классы-шаблоны верхнего уровня для OLE DB-потребителя настолько близкими к спецификации OLE DB, насколько это возможно. Эго значит, что шаблоны OLE DB не определяют другую модель объектов — они просто являются оболочкой существующей модели объектов OLE DB. Для каждого из перечисленных компонентов, относящихся к потребителю, есть соответствующий C+i-класс-шаблон. Такой принцип построения 'сохраняет гибкость OLE DB и допускает расширение возможностей, например, несколько пользователей (accessors) у наборов строк.
Библиотека шаблонов OLE DB небольшая, но гибкая. Она сделана на основе шаблонов C++ и множественного наследования. Так как шаблоны OLE DB рассчитаны только на существую-
щую архитектуру, то каждый класс соответствует имеющемуся компоненту OLE DB. Например, CDataSource соответствует OLE DB-объекту «источник данных».
Архитектура шаблонов OLE DB-потребителя может быть разделена на .три части: классы базовой поддержки источников данных, классы для поддержки операций доступа к данным и работы с наборами строк и классы для обработки таблиц и команд. Далее приведен краткий обзор этих классов.'
Базовая поддержка источников данных
Источник данных — это та фундаментальная концепция, о которой надо помнить при обсуждении доступа к данным при помощи OLE DB. На самом деле, откуда берутся данные? Конечно, OLE DB предоставляет поддержку для источников данных. Базовая поддержка включает в себя три класса, перечисленные в таблице.
КлассПрименение
CDataSource Представляет компонент *источниКданных* и управляет соединениями с источником данных
CEnumerator Предоставляет способ выбрать поставщика, перебирая список поставщиков. Его функциональность эквивалентна функциям SQLBrou;seConnectH SQLDriverC&nnect
CSessionОбрабатывает транзакции. Вы можете использовать этот класс для создания наборов
строк, команд и многих других объектов. Объект CDataSource создает объект CSession, используя метод GSession-Open
Поддержка доступа к данным и наборов строк
Шаблоны OLE DB предоставляют поддержку связывания и работы с наборами строк Классы- помошники (accessors) обращаются к источнику данных, а наборы строк управляют данными в табличной форме. Компоненты доступа к данным и наборов строк реализуются при помощи класса CAccessorRowset, который является шаблоном с параметрами в виде класса-по- мошники и класса-набора строк В таблице приведены классы-помошники, определенные в библиотеке шаблонов OLE DB.,
КлассПрименение
CAccessorИспользуется, когда запись статически привязана к источнику данных,
так как он содержит готовый буфер данных и понимает заранее заданный формат данных. Полезен, если Вы заблаговременно знаете структуру и тип базы данных■.
CDynamicAccessorИспользуется, чтобы извлечь из источника данные, чья структура неизве
стна в период разработки. Для получения информации о столбце базы данных класс вызывает ICoiunmsInforrGetColumnlnj'dLsacc создает и поддерживает буфер данных
CDynamicParameterAccessor Подобен предыдущему, но применяется с командами, При подготовке команд может получить информацию о параметре через интерфейс ICommandWitbParameters,4ro особенно полезно при обработке неизвестных типов команд
CManualAccessorПозволяет Вам получить доступ к любым типам данных, которые постав
щик может сконвертировать. Обрабатывает как результирующие столбцы, так и параметры команд
Помимо помощников, шаблоны OLE DB определяют три типа наборов строк: одна выборка (single fetching), групповая выборка (bulk) и массив (array). Названия говорят сами за себя. Для перемещения по данным клиенты используют функцию MoueNext. Разница между этими типами наборов строк заключается в количестве описателей строк, возвращенных при вызове MoueNext-. наборы из одной выборки возвращают одну строку, а групповые наборы — несколько строк Наборы типа массив предоставляют для выборки данных соответствующий синтаксис. По умолчанию шаблоны OLE DB используют одностроковую выборку.
Поддержка таблиц и команд
Последний уровень архитектуры потребителя состоит из двух классов: таблиц и команд (СТаЫе и CCommand). Эти классы используются для открытия набора строк, выполнения команд и инициализации связывания. Оба класса производны от CAccessorRowset.
Класс СТаЫе — минимальная реализация класса, открывающего таблицу в источнике данных, который можно задать программно. Используйте этот класс в случае примитивного доступа к источнику, т. к. ои разработан для простых поставщиков, не поддерживающих команды.
Для источников данных, поддерживающих команды, предназначен класс CCommand, Как ясно из его имени, он используется по большей части для выполнения команд. В этом классе есть функция Open, выполняющая отдельные команды; есть также функция Prepare, подготавливающая команду к неоднократному выполнению.
При использовании класса CCommand иужио указать три аргумента шаблона: класс-По- мошник, класс-набор строк и третий, которым по умолчанию являются CNoMultipleResultsEam. третьим аргументом указать CMultipleResuiis,то класс CCommand будет поддерживать интерфейс iMultipleResultss^ команд, возвращающих множественные наборы строк.
Архитектура шаблонов OLE DB-поставщика
Вспомните, что OLE DB на самом деле — всего лишь набор интерфейсов, задающих протокол для управления данными. OLE DB определяет несколько интерфейсов (одни обязательные, другие нет) для следующих типов объектов: «источник данных», «сеанс», «набор строк» и «команда». Далее приведено описание каждого типа объекта вместе с образцом кода, демонстрирующим применение шаблонов для правильного функционирования каждого компонента.
Объект «источнивданных» (datasource о1уес0объединяетмножество сторон доступа к данным. Например, источник данных состоит из реальных данных и соответствующей СУБД, а также платформы, на которой работает СУБД, и сети, используемой для доступа к этой платформе. Источник данных — это COM-объект, реализующий ряд интерфейсов, показанных в таблице.
Интерфейс
Требуется?
Реализован?
IDBInitialize
Обязательно
Да
IDBCrecileSession
Обязательно
Да
IDBProperties
Обязательно
Да
IPersist
Обязательно
Да
IDBDataSourceAdmin
Необязательно
Нет
HDBInfo
Необязательно
Нет
IPersistFile'
Необязательно
Нет
ISupportErrorlnfo
Необязательно
Нет
ПРИМЕЧАНИЕ Таблицы раздела взяты из интерактивной документации Microsoft Visual Studio.
Вот образец кода, который ATL Object Wizard вставляют при создании источника данных для OLE DB-поставщика: ■
class ATL_NO_VTABLE CAProvlderSource :
public CCa.mObjectnootEx<CComSingleThreadModsl>.
public CCofliCoClass<CAProviderSource, &CLSID^AProvider>,
public IDBCreateSessionlmpKCAProviderSuurce, CAProviderSession>,
public IDBInitializeImpl<CAProviderSource>, public IDBPropertlesImpKCAProviderSourco,
publ ic I Pe rs i st!mpl<CAP rovide rSou rce>,
public IInternalConnectionImpl<CAProviderSource> {
Заметьте, что это обычный COM-класс с реализацией IUnknown на основе ATL Он создается путем наследования реализаций интерфейсов IDBCreateSessionJDBInitialize. ЮВРгорег- ties и {persist.Обратите внимание, что параметрами шаблонов являются классы GAProvider- Source и GAProuiderSession. Если нужно добавить дополнительную функциональность, то это можно сделать наследованием от других классов, реализующих интерфейсы OLE DB.
,• Объект «команда» (command object). Поставщики, которые поддерживают связывание и выполнение запросов, предоставляют этот объект. Команды позволяют задать, подготовить и выполнить запросы на DML (Database Manipulation Language, языкуправленияба- зойданных) или определения на DDL (Data Definition Language, язык определения данных) и ассоциированные сними свойства. Например, объект-команда транслирует SQL-подобную команду в операции, специфичные д ля источника данных. Сравнивая с ODBC, команда соответствует базовой функциональности оператора ODBC в невыполненном состоянии. С одним сеансом могут быть ассоциировано несколько команд. В следующей таблице перечислены интерфейсы, используемые в объекте «команда».
Интерфейс
Требуется?
Реализован?
lAccessor
Обязательно
Да
ICotumnsInjb
Обязательно-
Да
ICommand
Обязательно
Да
ICommandProperties
Обязательно-
Да
iCommandText
Обязательно
Да
IConverll'ype
■Обязательно >
Да-
IColMmnsRotvsct
Необязательно
Нет
ICommandPrepare
Необязательно
Нет
ICommandWithParameters
Необязательно
Нет
ISupportErrorlnJo
Необязательно
Нет
Вот образец кода, который ATL Object Wizard вставляет при реализации объекта «команда», когда Вы создаете OLE DB-поставщика.
class АТ1_МО_УГАВ1Е CAProviderCommand :
public CComObjectRootEx<CComSingleThreadModel>,
public IAccessorImpl<CAProviderCommand>,
public ICommandTextImpl<CAProviderComirand>,
public ICommandPropertiesImpl<CAProviderCommand>,
public lObj ectWithSiteImpl<CAProviderCommand>, public IConvertTypeImpl<CAProviderCommand>.
public IColumnsInfoImpl<CAProviderCommand> {
Как и в случае источника данных, это обычный COM-класс, реализующий интерфейсы благодаря наследованию. (Например, lAccessor реализуется шаблоном IAcces.sorImpl.3O6b- ект-команда используетlAccessor для задания параметров связывания. Потребители вызывают lAccessorcCredteAccessor, передавая массив структур DBBINDING, которые содержат информацию о связываемых столбцах (тип, длина и т. п.). Поставщик получает структуры и определяет, как должны передаваться данные и нужны ли преобразования. Интерфейс ICommcmdTextпозволяет задать текст команды, а интерфейс ICommandPropertiesобрабатывает все свойства команды.
Класскоманды — «сердце» поставщика данных. Большинство действий происходит внутри этого класса.
Объект «сеанс» (session object) определяет область действия транзакций и генерирует наборы строк из источника данных, а также создает объекты «команда», которые выполняют команды на наборах строк. Для поставщиков, которые поддерживают команды, сеанс — это «фабрика» команд. В сравнении с ODBC объект «сеанс» и объект «источник данных» инкапсулируют функциональность ODBC-соединения. Вызов IDBGreateSessiomCreate- Session создает сеанс на основе источника данных. С одним объектом «источник данных» может быть ассоциировано несколько сеансов. В таблице перечислены интерфейсы объекта «сеанс». •
Интерфейс
Требуется?
Реализован?
IGelDalaSource
Обязательно
Да
lOpenRowset
Обязательно
Да
ISessionPnjperties
Обязательно
Да
IDBCreateCommand
Необязательно
Да
WBScbemaUowset
Необязательно
Да
llndexDepnition
Необязательно
Нет
ISupportErrorln/o
Необязательно
Нет
ITableDefmition
Необязательно
Нет
nVansdctionJoin
Необязательно
Нет
JTransactionLocat
Необязательно
Нет
HYansactinnObject
Необязательно
Нет
Вот образец кода, который ATL Object Wizard вставляет при реализации объекта «сеанс», когда Вы создаете OLE DB-поставщика:
class ATL_NO_VTABLE CAProviderSession :
public CCorObjectRootEx<CCcTSingleThreadModel>,
public IGetDataSourceImpKCAProviderSession>,
public IOpenRowsetImpl<CAProvidcrSession>,
public ISessionPropertiesImpKCAProviderSession>.
public IObjectWithSiteSessiarImpl<CAProviderSession>,
public IDBSchemaRDwsetlrrpKCAProvjcerSessians,
public IGBCreateCoTOandlmpRCAProviderSession, CArroviderCoMand> {
li
■ Объект «набор строк» (rowset object) представляет собой табличные данные. На низшем уровне OLE DB наборы строк создаются вызовом IOpenRowset:;OpenRowse№s объекта «сеанс». Для поставщиков, поддерживающих команды, наборы строк используются для представления результатов запросов, возвращающих строки. Кроме lOpenRowset.;Open Rowset, в OLE DB есть и другие методы, возвращающие наборы строк. Сравнивая с ODBC, можно считать наборы строк инкапсуляцией базовой функциональности оператора ODBC в выполненном состоянии. Один сеанс может быть ассоциирован с несколькими наборами строк. Кроме того, одна команда может быть ассоциирована с несколькими наборам] [ строк. В таблице перечислены интерфейсы объекта «набор строк». . .
Интерфейс
Требуется?
Реализован?
lAccessor
Обязательно
Да
ICotumnslnfo ,
Обязательно
, Да .
IConvertType.
Обязательно ,•
. Да■
IRowset.
Обязательно
■ Да.
SRowseilnfo
Обязательно
Да•
IColumnsRowset
Необязательно
Нет
IC'imnectionPointContainer
Необязательно
Да, благодаря ATL
Интерфейс
Требуется?
Реализован?
JRowsetChange
Необязательно
Нет
IRowsetidentity
Обязательно для уровня 0
Да
IRowsetLocstc
Необязательно
Нет
IRowsetResynch
Необязательно
Нет
JRowsetScroit
Необязательно
Нет
IRowsetUpdate
Необязательно
Нет
ISupportErrorlnJo
Необязательно
Нет
Вот образец кода, который ATL Object Wizard вставляет при реализации объекта «набор строк», когда Вы создаете OLE DB-поставщика:
classCAProvi derWindows Fil e:
public WIN32 FIND DATA
public:
BEGIN PROVIDER COLUMN MAP(CAProviderWindowsFile)
PROVIDER COLUMN ENTRY("FileAttributes", 1, dwFileAttributes)
PROVIDER_COLUMN_ENTRY("FlleSizeHigh", 2, nFileSizeHigh)
PROVIDER COLUMN_ENTRY(’FileSizeLow", 3, nFileSizeLow)
PROVIDER COLUMN ENTRYCFileNsme", 4, cFileName)
PROVIDER COLUMN ENTRY("AltFileName", 5, cAlternateFileName)
lEND.PROVIDER COLUMN_MAP()
class CA Provider Rowset :
public CRowsetImpl<CAProviderRowset, CAProviderWindowsFile, CAProviderCommand> {
I'
Сгенерированный мастером объект реализует, помимо других, интерфейсы IAccessor,IRow- set и IRowsetlnjb. 1Ассе88ог1тр1ъътоля5кл связывание обоих выходных столбцов, IRowsetделает выборку строк и данных, a IRowsetlrfo обрабатывает свойства набора строк. Класс CAProviderWindowsFile представляет пользовательскую запись данных. Создаваемый мастером класс — всего лишь основа он делает не так уж много. Определив формат столбцов поставщика данных, этот класс следует модифицировать,
Совместная работа всех компонентов поставщика
Использование источника данных должно быть очевидно. Каждый поставщик должен включать объект «источник данных*. Когда приложению-потребителю нужны данные, оно вызывает CoCreatelnstancesna создания объекта и запуска поставщика. Внутри поставщика объект «источник данных» должен создать объект «сеанс» при помощи интерфейса IDBCreateSession. Потребитель использует этот интерфейс для соединения с объектом «источник данных». В сравнении с работой ODBC, объект «источник данных» эквивалентен HENV, а объект «сеанс» эквивалентен HDBC
Большую часть работы выполняет объект «команда». Чтобы поставщик данных мог реально что-нибудь сделать, модифицируйте функцию Execute класса команды.
Подобно большинству протоколов на основе СОМ, протокол OLE DB имеет смысл изучать понемногу. Кроме этого, каки другие COM-протоколы, OLE DB требует большого количества кода для работы — кода, который легко реализовать при помощи некоторого программного каркаса (framework), а это то, чем являются шаблоны потребителя данных (Data Consumer) и поставщика данных (Data Provider). Оставшаяся часть главы посвящена тому, что надо сделать для создания потребителей и поставщиков данных.
Создание OLE DB-потребителя
Создать OLE DB-потребитель очень просто, т. к. большую часть работы выполняет мастер ATL Object Wizard. Вы мажете видеть пример потребителя в каталоге ехЗЗа на прилагаемом компакт-диске. Ваши действия по созданию потребителя с использованием ATL Object Wizard. ' 1. Создайте приложение или элемент данных для управления потреблением данных, например, ActiveX-элемент.
В среде разработки (IDE) используйте ATL Object Wizard, чтобы добавить потребитель данных. Для этого выберите New ATL Object из меню Insert либо щелкните правой кнопкой мыши значок проекта в Class View и выберите New ATL Object из контекстного меню.
,3. В ATL Object Wizard выберите категорию объектов Data Access. Затем выберите Consumer и щелкните Next. Появится диалоговое окно ATL Object Wizard Properties, показанное на рисунке 33-1- В окне будет только одна страница — для названия класса и выбора источника данных,
Рис. 33-1. Диалоговое окно AIL Object wizard Properties
4. Щелкните Select Datasource для настройки потребителя данных. Выбрав источник данных, щелкните OK. ATL Object Wizard создаст готовый к использованию шаблон OLE DB-потре - битв ля.
В качестве примера возьмем базу данных BIBLIO.MDB (формата Microsoft Access), находящуюся в подкаталоге VB98 каталога Visual Studio, и создадим на ее основе потребитель данных. База данных BIBLIO содержит названия и имена авторов различных программных текстов. Использование ATL Object Wizard для создания OLE DB-потребителя таблицы authors дае г следующие классы:
// Authors.Н : объявление класса CAuthors
flifndef „AUTHORS Н
«define _AUTHORS_H_
class CAuthorsAccessor■
public:
LONG m_AuID;
TCHAR m_Autnor[51);'
SHORT m.YearBorn;
BEGIN COLUMN MAP(CAuthorsAccessor) COLUMN_ENTRY(1, m AuID) '‘
C0LUMN_ENTRY(2, m Author) C0LUMN_ENTRY(3. m YoarBorn) END_CDLUMN_HAP()
DErfNE_COMMAND(CAuthorsAccessor, Tf’SELECT * FROM Authors"")) }".~
class CAuthors ; public CC<wand<CAccessor<CAuthorsAccessor> > public: HRESULT OpenC) { HRESULT hr; hr = OpenDataSourceO; if (FAILED(hr)) return hr;"■
return OpenRowsetO; } HRESULT OpenDataSourceO { HRESULT hr; CDataSource db; CDBPropSet dbinit(DBPRQPSET_DB!NIT);"
dbinit.AddProperty(DBPROP_AUTH CACHE AUTHINFO, true); dbinit.AddProperty(DBPROP_AUTH ENCRYPT PASSWORD, false); dbinit. AddProperty(DBPROP_AUTII MASK_PASSWORD, false); dbinit.AddProperty(DBPROP AUTH PASSWORD, OLESTR("")): dbinit.AddProperty(DBPROP AUTH PERSIST_ENCRYPTED, false); dbinit.AddProperty(DBPROP AUTH PERSIST SENSITIVE AUTHINFO. false); dbinit.AddProperty(OBPROP AUTH USERID, OLESTR("Admin"));
dbinit.AddProperty(OBPROP INIT_DATASOURCE, 0LESTRC"c;\\bibli6.mdb"));
dbinit.AddProperty(DBPROP_INlT_MODE. (long)16);
dbinit.AddProperty(DBPROP INIT PROMPT, (short)4);
dbinit.AddProperty(DBPROP_INIT~PROVIDERSTRING, OLESTR (";COUNTRY=O;CP= 1252; LANGID=OxO4OS"")); dbinit.AdcProperty(DBPROP_INIT_LCID, <long)1033);
hr = db.0pen( T( "Microsoft. Jet. OLEDB. 3.51"), fidbinit); if (FAILED(hr)") return hr;
return m sassion.Open(db);
}~
HRESULT OpenRowsetO {'
return CCommand<CAcccssor<CAuthorsAccessor> >::0pen(m session, TCAuthc's'));
Csession m_session;
xiendlf // _AUTHORS_H_
Класс сАиГЬогаАссе$$о№п.ределяет структуру записи об авторе. Он включает поле с идентификатором автора, поле имени и поле даты рождения.
Класс CAuftors является классом потребителя данных, подсоединяющегося к базе данных. Обратите внимание, что он производный от CCommand. Вспомните, что объекты <команды* представляют собой команды (например, операторы SQL) и генерируют наборы строк. Таб-
лица столбцов СОШМ_МАР представляет данные, возвращаемые в наборе строк, Таблица параметров РАКАМ_МАР представляет параметры команды.
Таблицы столбцов и таблицы параметров описывают пользовательский взгляд на доступ к данным. Каки большинство структур данных в АТЕи MFC, эти таблицы созданы на основе макросов. Они работают следующим образом: при обращении к базе данных возвращенные данные содержатся в непрерывном блоке памяти. Шаблоны OLE DB извлекают данные из этого блока. Элементы таблицы описывают смещения до данных в этом блоке памяти, тем самым работая как фильтр данных из БД. Благодаря этому Вам ие нужно для извлечения информации выполнять неприятные действия подобные сложению указателей.
Использование OLE DB-потребителя
Использовать класс потребителя базы данных так же просто, как и его создать. Вот пример применения только что созданного класса.
Объявите экземпляр CAuthors там, где нужно его использовать:■
class CUseAuthors : public CDialog { CAuthors m_authors;
Откройте базу данных, вызвав Open для объекта-потребителя;
CUseAuthors:/OnlnltDialogO { m_authors.Open();
Используйте функции-члены для перемещения по базе данных и манипулирования данными. Вот несколько примеров:
CUseAuthors::OnNext() {
m_authors.MoveNext ():
CUseAuthors::0nFirst() {
m_authors.MoveFlrst();
)-
CUseAuthors: :0nlast() {
m authors.MoveLast();
CUseAuthors::0nInsert() {,.
m authors.Insert();'
}
По мере перемещения по БД данные попадают в переменные-члены. Например, вот как Вы мажете получить имя автора в следующей записи базы данных:
m_authors. MoveNext();
m_strAuthorName = m_authors.m_Author;
Как видите, использование шаблонов значительно упрощает извлечение данных из БД. Все, что вам нужно — найти базу данных, указать ее мастеру ATL Object Wizard и предоставить ему генерацию кода. В результате, класс-пособник будет иметь функции для перемещения по БД и извлечения данных.
На другой стороне «уравнения шаблонов OLE DB» находится поставщик данных. Настало время узнать, как с ним работать.
Создание OLE DB-поставщика
Очевидна полезность OLE DB-потребителей. Мы просто просим мастер создать класс-оболочку и получаем очень простей способ доступа к данным в БД. Однако менее понятно, зачем может потребоваться создание OLE DB-поставщика..
Цель создания OLE DB-поставщика
Создание OLE DB-поставщика позволяет создать промежуточный уровень между клиентом и данными. Вот несколько причин, чтобы написать поставщик:
клиенты не будут напрямую связаны с данными. Благодаря этому можно добавить к данным дополнительные возможности, например, обработку запросов;
в некоторых случаях можно увеличить производительность доступа к данным, контролируя их манипуляции;
увеличение числа потребителей данных. Например, если Вы имеете особый формат данных, который доступен только для одного языка программирования, то у Вас могут быть проблемы. OLE DB-поставщики предоставляют способ открыть этот особый формат многим программистам, независимо от используемого ими языка программирования.
Написание OLE DB-поставщика
Работа с OLE DB-поставщиками похожа на работу с потребителями. Мастеры сделают за Вас большую часть работы. Вам нужно только знать, как работать со сгенерированными классами. Вот шаги по созданию OLE DB-поставщика.
Сначала определите, что поставщик должен делать. Вспомните философию OLE DB: это единообразный путь доступа к разнообразным источникам данных. Например, можно написать поставщик, который рекурсивно перебирает содержимое файла структурированного хранилища. Или можно создать поставщик, предоставляющий клиентам доступ к системе электронной почты в стиле базы данных. Возможности практически бесконечны,
Также,какидляпотребителяданных,используйтеАТЕО^ес1 WizardjinHcosflaHHnnocTaB- щика. Просто запустите ATL Object Wizard из ClassVlew-или из меню Insert. Выберите категорию объектов Data Access, затем выберите Provider. Мастер запросит у Вас имя объекта и позволит модифицировать имена по умолчанию для создаваемых файлов.
После щелчка ОК мастер создаст код для поставщика, включая источник данных, набор строки сеанс. Помимо этих объектов, поставщик подцерживает одно или несколько свойств, которые определены в таблицах свойств (property maps) внутри файлов, созданных мастером. При создании файлов мастер вставляет таблицы для свойств, принадлежащих к группе свойств OLE DB, определенныхдля объектов, включенных в эти файлы. Например, заголовочный файл, содержащий объект-источник данных, содержит и таблицу свойств для свойств DataSource, а заголовочный файл сеанса — для свойств Session. Объекты набора строк и команды находятся в одном заголовочном файле, который включает свойства для объекта-команды.
Например, вот что ATL Object Wizard создает для OLE DB-поставщика, названного AProvider. Сначала ATL Object Wizard создает объект-источник данных, находящийся в файле AProvi- derDS.H:.
class ATL_NO_VTABLE CAProviderSource :"■
public" CComObjectRootEx<CComSingleThreadModel>...
public CComCoClass<CAProviderSource, &CLSID_AProvider>.
public IDBCreateSessionlmpl^AProviderSource, CAProviderSession>,
public IDBInitializelmpl <CAProviderSource>,
public IDBPropertiesImpl<CAProviderSource>,.
public IPe rsistImpl<CAP rovide rSou rce>, public IInternalConnectionImpl<CAProviderSource>
public: HRESULT FinalConstructO.
{
return Flnit();
1
DECLARE_REGISTRY_RESOURCEID(IOR_APROVIDER)
BEGIN PROPSET MAP(CAProviderSource)
BEGIN PROPERTY_SET(DBPROPSET_DATASOURCEINFO) PROPERTY INFO ENTRY(ACTIVESESSIONS) PROPERTY INFO ENTRY(DATASOURCEREADONLY) PROPERTY INFO ENTRY(BYREFACCESSORS) PROPERTY INFO ENTRY(OUTPUTPARAMETERAVAILABILITY) PROPERTY INFO ENTRY(PROVIDEROLEDBVER) PROPERTY _INFO_ENTRY( DSOTHREADMOOEL) PROPERTY INFO entry(supporteetxnisolevels) PROPERTY INFO ENTRY(USERNAME)
END_PROPERTY SETCDBPROPSET„DATASOURCEINFO) begin property. SET(DBPROPSET dbinit)■
PROPERTY.INFO ENTRY(AUTH„PASSWORD) PROPERTY„INFO_ENTRY(AUTH_PERSIST_SENSITIVE AUTHINFO) propertyjnfo ENTRY(AUTH_USERID)
PROPERTY INFO ENTRY(INIT DATASOURCE)
PROPERTY INFO ENTRY(INIT HWND) ,PROPERTY_INF6_ENTRY(INIT_LCID) PROPERTY.INFO ENTRY(INIT LOCATION) PROPERTY INFO ENTRY(INIT NODE) PROPERTY INFO ENTRY(INIT_PROMPT) PROPERTY" INFO ENTRY(INIT PROVIDERSTRING} PROPERTYINFO ENTRY(INIT TIMEOUT) END PROPERTY SET(DBPROPSET DBINIT) CHAIN PROPERTY SET(CAProviderCommand).
END PROPSET_MAP()
BEGIN COM MAP(CAProviderSource)
COM INTERFACE_ENTRY(IDBCreate8ession)
COH_INTERFACE_ENTRY(IDBInitlalize)
COM_INTERFACE_ENTRY(IDBProperties)•
COM„INTERFACE„ENTRY(IPsrsist)
COM INTERFACE ENTRY(IInternalConnectlon)■
END_COM_MAP() public:.
Помимо объекта данных, ATL Object Wizard создает объекты «команда» и «набор строк» — оба в файле AProviderRS.H: ’
class ATL_NO_VTA8LE CAProviderCommand ; public CComObjectRootEx<CComSingleThreadModel>, public IAccessorImpl<CAProviderCommand>,'
public ICommandTextImpl<CAProviderCommand>, public IComma n dP rope rt iesImpKCAProvide rComma nd>, public 10b jectWithSitelmpKCAP rovide rCommand>, public IConvertTypeImpl<CAProviderCoflimand>,,
public IColumnsInfoImpl<CAProviderCommand>
{ public:.
ВEGIN_COM_MAP(CAProviderCommand)
COH_INTERFACE_ENTRY(ICommand>
COM_lNTERFACE_ENTRY(lObjectWithSite)
COM,lNT£RFACE_ENTRY(IAccessor)
CbM_INTERFACE_ENTRY(ICommandProperties)
COM INTERFACE ENTRY2(ICommandText, ICommand}■
COMJNTERFACE~ENTRY(IColumnsInfo)
COM INTERFACE ENTRY(IConvertType)
END_CON_MAP()
H ICommand
public:
HRESULT FinalConstruct()
HRESULT hr = CConvertHelper: :FinalConstruct();
if (FAILED (hr)) return hr;
hr = IAccessorImpl<CAProvlderCommand>::FinalConstruct();
if (FAILED(hr)) return hr;
return CUtlProps<CAProviderCommand>::FInit():
}
void FinalRelease()
IAccessorImpl<CAProviderCommand>::FinalRelease();
HRESULT WINAPI Execute(IUnknown * pUnkOuter, REFIID riid, DBPARAMS * pParams, LONG * pcRowsAffected, IUnknown ** ppRowset);
static ATLCOLUMNINFO* GetColumnInfo(CAProviderCommand* pv, ULONG* pclnfo)
return CAProviderWindowsFile: :GetColumnInfo(pv, pclnfo);
beg™ PROPSET HAP(CAProviderCommand)
6EGIN_PR0PERTY_SET(DBPR0PSET_R0WSET)
PROPERTY, INFO ENTRY(lAccessor)
PROPERTY ГОРО ENTRY(IColumnsInfo)
PROPERTY ™F0 ENTRY(IConvertType}
PROPERTY INFO ENTRY(IRowset)■
PROPERTYJNFO ENTRY(lRowsetldentlty)
PROPERTY. INFO. ENTRY(IRowsetlnfo)
PROPERTY INFO ENTRY(IRowsetLocate)
PROPERTY„INFO ENTRY(BOOKMARKS)
PROPERTY ™F0 ENTRY(BOOKMARKSKIPPED)
PROPERTY INFO ENTRY(BOOKMARKTYPE)
PROPERTY_INFO ENTRY(CANFETCHBACKWARDS)
PROPERTY, MFO ENTRY(CANHOLOROWS)
PROPERTY INFO ENTRY(CANSCRQLLBACKWAROS)
property_™fo ENTRY( LITERaLBOOKHARKS)
PROPERTY INFO'ENTRY(ORDEREDBOOKMARKS)
ENO PROPERTY SET(DBPROPSET ROWSET)
END,PROPSET МАРО
class RAProvlderRowset ; public CRowsetImpl<RAProvlderRowset1 CWindowsFile, CAProviderCommand>
{
public:
HRESULT Execute(DBPARAMS * pParams, LONG* pcRowsAffected)
{
USES.CONVERSION;
BOOL bFound = FALSE;
HANDLE hFile;
iLPTSTR szDir = (m.strCommandText == _Т("")) ? _T(",.*") : 0LE2T(m_strConmiandText): CAProviderWindowsFile wf;
hFile - FindFirstFile(szDir, &wf):
if (hFile == INVALID HANDLE-VALUE)
ireturn DB_E_ERRORSINCOMMAND:
IL0№ cFiles - 1;• ■
BOOL bMoreFiles = TRUE;
-while (bMoreFiles)
{
if (Im rgRowData.Add(wf))
ireturn E.OUTOFMEMORY;
bMoreFiles = FindNextFile(hFile, &wf);
cFiles++;
IFindClose (hFile);
if (pcRowsAffected 1= NULL)
«pcRowsAffected - cFiles;•
ireturn S.OK;
AIL Object Wizard создает объект «сеанс» в файле AProviderSess.H:
class ATL_NO_VTABLE CAProviderSession :
public CComObjectRootEx<CComSingleThreadModel>,
public IGetDataSpurceImpl<CAProviderSessior>,.
public lOpenRowsetlmpKCAProviderSessioO,
public ISessionPropertiesImpl<CAProviderSession>, public IObjectWithSiteSessionImpl<CAProvicerSessicn>.
public IDBSchemaRowsetImpl<CAProviderSessidn>,.
public IDeCreateConimandImpl<CAProviderSession CAProviderCommand>
publics
CAProviderSession() { )
HRESULT FinalConstructO {
return Flhit ();
STDMETH00(0penRowset)(IUnknown *pUnk, DBIO ‘pTID, 0810 *plnlD, REFIID riid, ULONG c&ts, DBPROPSET rgSets[], IUnknown **ppRowset)
(
CAProviderRowset* pRcwset;
ireturn CreateRowaet(pUnk, pTID, plnlD, riid, cSets, rgSets, ppRowset, pRcwset); }
BEGIN.PROPSET MAP(CAProviderSession)
BEGIN PROPERTY SET(DBPROPSET SESSION)
PROPERTY INFO ENTRY(SESS AUTOCOHHITISOLEVELS)
END PROPERTY SET(DBPROPSET SESSION)
EHD_PROPSET_MAP()
BEGIN COM MAP(CAProviderSession)
COM INTERFACE ENTRY(IGetDataSource)
COM_INTERFACE_ENTRY(lOpenRowset)
CON INTERFACE ENTRY(ISessionProperties)
COM INTERFACE ENTRY(IObjectWithSite)
COM INTERFACE ENTRY(IOBCreateCommand)
COM INTERFACE ENTRY(IDBSchemaRowset)
END.COM MAP()
BEgIN.SCHEMA MAP(CAProviderSession)'
SCHEMA_ENTRY(08SCHEMA_TABLES, CAProviderSessionTRSchemaRowset)
SCHEMA_ENTRY(DBSCHEMA COLUMNS, CAProviderSeasionColSchemaRowset)
SCHEHA_ENTRY(DBSCHEMA PROVIOER_TYPES, CAProviderSessionPTSchemaRowset)
end schema mapo
1; "
■class CAProviderSessionTRSchemaRowset :
public CRowsetlmpK CAProviderSessionTRSchemaRowset, CTABLESRow, CAProviderSessior»
3
public:
HRESULT Execute(LDNG* pcfiowsAffected, ULONG, const VARIANT*)
{
USES_CONVERSION;
CAProviderWindowsFile wf;
C TABLE SRow t rDat a ;
1strcpyW(t rData.m_szType, OLESTR("TABLE"));
lstrcpyW(trData.m_szDesc, OLESTR("The Directory Table"));
HANDLE hFile - INVALID_HANDLE_VALUE;
TCHAR szDir[MAX_PATH + 1]:
□TORD cbCurDir - GetCurrentDirectory(MAX_PATH, szDir);
lstrcat(szDir, _T("\\«.*"));
HFile = FindFirstFile(szDir, &wf):
if (HFile == INVALID_HANDLE_VALUE)
return E_FAIL; // У пользователя нет диска с:\
FindClose(hFile);
lstrcpynW(trData.m szTable, T2OLE(szDir),
SIZEOF_MEMBER(CTABLESRow, m.szTable));
if (Im rgRowData.Add(trData))
return E_OUTOFMEMORY;
«pcfiowsAffected = 1;
return S OK;
}
class CAProviderSessionColSchemafiowset ;
public CRowsetImpl< CAProviderSessionColSchemaRowset, CCOLUMNSRow, CAProviderSession>
{
public;
HRESULT Execute!LONG* pcRowsAffected, ULONG, const VARIANT*)
{
USES_CONVERSION;
CAProviderWindowsFile wf;
HANDLE HFile = INVALID HANDLE VALUE:
TCHAR szDir(MAX_PATH + 1];
DWORD cbCurDir = GetCurrentOirectory(MAX_PATH, szDir);
lstrcat(szDir, _T("\\*. *"));
HFile = FindFirstFile(szDir, &wf);
if (HFile == INVALIO_HANDLE_VALUE)
return E_FAIL; // У пользователя нет диска с:\
FindClose(hFile);// szDir содержит имя таблицы
DBID dbid;
memset(&dbid, 0, sizeof(DBID));
dbid.uName.pwszName = T20LE (szDir):
dbid.eKind = OBKIND_NAME;
return InitFromRowset <RowsetArrayType> (m_rgRowData, Adbid, NULL, ci spUnkSite, pcRowsAffected);
}
class CAProviderSessionPTSchemaRowset ;
public CRowsetlrnoKCAProviderSessionPTSchemaRowset, CPROVIDER_TYPERow, CAProviderSession>
public:
HRESULT Execute(LONG* pcRowsAffected. ULONG, const VARIANT*)
return S OK;
Модификация кода поставщика
Как и в большинстве других случаев, код OLE DB-поставщика, сгенерированный мастером ATI. Object Wizard, — это только заглушка, которая почти ничего не делает. Для превращения этого кода в настоящий OLE DB-постлвщик нужно выполнить несколько действий. Два важных дополнения, которые необходимо сделать — пальзователъскаязапись (user record) и код для управления набором данных и для представления данных в виде строк и столбцов.
Пользовательская запись. По умолчанию ATL Object Wizard создаетпользовательскую запись CAProviderWindowsFUe. Скорее всего, Вы не будете ее использовать, а замените чем- то полезным для вашего домена. В качестве простого примера представьте, что хотите написать OLE DB-поставщик, осуществляющий перебор содержимого составного файла. В этом случае пользовательская запись может быть примерно такой:
struct CStglnfo {
BE6TN_PROVIDER COLUMN MAP(CStglnfo)
PROVIDER_COLUMN_ENTRY("StgName", 1, szName)
PROVIDER_COLUMN_ENTRY( "Size",2, CbSizcLo'*)
PROVIDER COLUMN_ENTRY("Size". 2, cbSizeHigh)
END_PROVIDER_COLUHN_MAP()
OLECHAR szName{256];
long cbSi^Low;,
long abSizuHigh;
}:
Эта структура содержит поля данных для имени и размера вторичного хранилища. Макросы таблицы столбцов поставщика (provider column map) отображают данные на столбцы. Можно сделать эту структуру производной ar STATSTG (применяемой для перебора структурированных хранилищ). В этом случае потребуется только добавить элементы в таблицу столбцов поставщика.
Код для открытия набора данных. Открытие набора данных происходит в функции Execute набора строк, Здесь может быть множество различий в функциональности. Например, если Вы хотите выполнить перебор вторичных хранилищ верхнего уровня в структурированном файле, сначала откройте хранилище, а затем просмотрите его содержимое, как показано в следующем коде:
class RStglnfoProviderRower :
public CRowsetImpl<RStcInfoProviderRowset, CStglnfo, CStgInfoProviderCommand>
{
public:..
HRESULT Execute (DBPARAMS * pParams, LONG- pcRowsAffected)
" USES.CONVERSION:
LPTSTR szFile - m.strCommandText == _T("")) ? „ТС") : 0LE2T(m_strCommandText);
IStorage* pStg = NULL;
HRESULT hr = StgOpenStorage(szFile, NULL. STGM_READ|STGM SHARE-EXCLUSIVE,
NULL, NULL, SpStg);
if(FAlLED(hr))
return OB.E.ERRORSINCOMMANO;
LONG cStgs = 0;
lErunSTATSTG* pEnumSTATSTG;
hr - pStg->EnumElements(O, 0, 0, SpEnumSTAlSTG);
if(PEnumSTATSTG) {
STATSTG rgSTATSTG[100]:
ULONG nFctched;
hr = pEnuwSTATSTG-^extClOO, rgSTATSTG, &nFetched);
forfULONG i = G: i < nFetched; i++) {
CStglnfo stglnfo;
stglnfo.obSizeLow = rgSTATSTG[i]. cbSize.LowPart;
stglnfo.cbSizeHigh = rgSTATSTG[iI. chSize. HignPart:
wcsncpyCstglnfo.szName, rgSTATS(G[i].pwcsMair.e. 255);
CoTaskMemFree(rgSTATSTG[i).pwcsName);
if (!m rgRowData.Add(stglnfo))
returnE.OVTOFMEMORY;
cStgs++;
}
pEnumSTATSTG->ReleaseO ;
}
if(pStg)■
pStg->Release();
if (pcRowsAffected != NULL) «pcRowsAffected = cStgs;
return S_OK;
}
>
Когда клиент пытается открыть OLE DB-поставщик, к конечном стете, вызывается эта функция. Она просто открывает файл структурированного хранилища, переданный в нее как текст команды, и использует стандартный перечислитель структурированного хранилища для нахождения вторичных хранилищ верхнего уровня, имена и размеры которых сохраняются в массиве. OLE DB-поставщик использует этот массив для выполнения запросов к данным столбцов.
Улучшение поставщика
Конечно, есть много путей улучшения этого OLE DB-поставщика. Мы только слегка коснулись того, что может делать поставщик. ATL Object Wizard по умолчанию создает поставщик только для чтения, поэтому пользователь не может изменять данные. Кроме того, шаблоны OLE DB предоставляют поддержку для определения положения наборов строк (locating rowsets) и установки закладок (bookmarks). В большинстве случаев улучшение поставщика достигается реализацией СОМ-интерфейсов на основе шаблонов OLE DB.
ЧАСТЬ VI
Программирование для Интернета
TCP/IP, Winsock и Winlnet
Вас, как программиста на C++, могут попросить сделать нечто большее, чем простую Web- страницу. Вы можете быть одним из тех, кто обеспечивает Интернету его истинный потенциал и кто создает распределенные приложения, которые недавно еще невозможно было себе представить. Чтобы достичь успеха, Вам нужно понимать, как работает Интернет и как писать программы, которые смогут получить доступ к другим компьютерам Интернета.
Эту главу мы начнем с примера протокола TCP ДР, повсеместно используемого в Интернете, а затем поднимемся на один уровень вверх и рассмотрим протокол HTTP. После этого наступит время создать что-нибудь работающее. Мы соберем нашу собственную интрасеть (локальную версию Интернета) и изучим клиент-серверную HTTP-программу, основанную ни Winsock — базовом API для ТСРДР. В конце главы мы перейдем к API более высокого уровня, чем Winsock, называемом Winlnet, который является частью ActiveX-технологии Microsoft
«СОМ иль не СОМ?»
Несомненно, Вы читали об ActiveX-элементах для Интернета. Возможно, Вы встречали такие концепции, как композитные моникеры и антимоникеры, являющиеся частью СОМ. Если Вы не готовы их воспринять, не переживайте — вполне возможно программировать для Интернета, не применяя СОМ, и начинать лучше именно так. Эта и следующая главы практически свободны от СОМ В главе Зб мы напишем сервер ActiveX-документов, основанный на СОМ, однако MFC эффективно скрывает детали СОМ, поэтому Вы можете сконцентрироваться на программировании с использованием Winsock и Winlnet. Это не означает, что ActiveX-элементы не важны, но в этой книге мы не оцениваем их должным образом. Лучше обратитесь к книге Адама Деннинга (Adam Denning) ActiveX Controls Inside Out (Microsoft Press, 1997). Информация о COM и Интернете из нашей книги прекрасно подготовит Вас к чтению книги Адама.
Основы Интернета
Невозможно написать хорошую программу с использованием Winsock, не разобравшись с понятием сокета (socket), используемого для передачи и приема пакетов данных по сети. Чтобы полностью понять сокеты, необходимо хорошо знать нижележащие протоколы Интернета. В этом разделе Вы получите хорошую порцию теории Интернета, которой должно быть достаточно, чтобы двигаться дальше. Если же вам нужно больше, то обратитесь к одной из книг по TCP/IP.
Сетевые протоколы и их уровни
Во всех сетях протоколы передачи размещаются на разных уровнях (layering); набор уровней часто называют стеком (stack) протоколов. Прикладная программа общается с верхним
Транспортный уровеньТранспортный уровеньСетевой уровеньСетевой уровеньнттр-протоколнттр-протоколУровень соединений.Уровень соединений.Рис. 34-1. Стек протоколов TCP/IP в локальной сетиРис. 34-1. Стек протоколов TCP/IP в локальной сети
Транспортный уровень
Транспортный уровень
Сетевой уровень
Сетевой уровень
нттр-протокол
нттр-протокол
Уровень соединений.
Уровень соединений.
Рис. 34-1. Стек протоколов TCP/IP в локальной сети
Рис. 34-1. Стек протоколов TCP/IP в локальной сети
уровнем, а нижний уровень общается с сетью. На рисунке 34-1 показан стек семейства протоколов TCP/IP в локальной сети. Каждый уровень логически соединен с соответствующим уровнем на другом конце коммуникационного канала. Программа-сервеДяетует), показанная справа на рис. 34-1, непрерывно ждет запросов на одном конце канала, в то время как nporpaMMa-*y«/eHm(client), показанная слева, периодически соединяется с сервером для обмена данными. Думайте о сервере как о WWW-сервере на основе HTTP и думайте о клиенте как о программе-браузере, запущенной на вашем компьютере.
Уровень приложения
Протокол IP
Уровень протокола IP — это лучшая отправная точка для начала путешествия по TCP/IP. IP определяет пакеты, называемые датаграммы (datagrams), являющиеся базовыми единицами Интернет-взаимодействия. Эти пакеты, обычно длиной менее 1000 байт, передаются по всему миру при открытии Web-страницы, загрузке файла и передаче электронной почты. На рисунке 34-2 показан упрощенный формат 1Р-датаграммы.
Рис.34-2. Упрощенный формат 1Р-датаграммыРис.34-2. Упрощенный формат 1Р-датаграммы
Рис.34-2. Упрощенный формат 1Р-датаграммы
Рис.34-2. Упрощенный формат 1Р-датаграммы
Обратите внимание: IP-датаграмма содержит 32-разрядные адреса компьютера-источника и компьютера-получателя. Эти IP-адреса идентифицируют компьютеры в Интернете и
24 Зак. № 787
используютсял«?рн<р>»г«зйторйл« (routers) — специализированными компьютерами, работающими подобно телефонным коммутаторам — для передачи получателям отдельных датаграмм. Маршрутизаторы не знают, что находится внутри датаграмм — их интересует только адрес получателя и длина, а их задача состоит в том, чтобы переслать датаграмму как можно быстрее.
Уровень IP не сообщает отправителю, достигла ли датаграмма получателя. Это задача стоящего над ним уровня в стеке. Получатель может только проверить контрольную сумму, чтобы определить целостность заголовка 1Р-датаграммы.
Протокол UDP
На самом деле, семейство TCP/IP должно называться TCP/UDP/LP, потому что оно включает в себя протокол UDP (User Datagram Protocol), находящийся на одном уровне с TCP. Все транспортные протоколы на основе IP сохраняют свои заголовки и данные внутри IP-блока. Сначала взгляните на формат UDP, показанный на рисунке 34-3-
Рис. 34-3. Упрощенный формат UDP
Полная датаграмма UDP/IP показана на рисунке 34-4
,Датаграмма IP
..Датаграмма UDP.
Обычно 20 байт д байт‘.
Рис. 34-4. Датаграмма UDP внутри датаграммы IP
UDP — это всего лишь небольшая надстройка над IP, поскольку приложения никогда не используют IP напрямую. Подобно IP, UDP не сообщает отправителю о доставке датаграммы. Решение оставлено на усмотрение приложения. Например, отправитель может потребовать, чтобы получатель прислал ответ, и может заново отправить датаграмму, если ответ не пришел в течение, скажем, 20 с. UDP хорош для простых однократных сообщений — он используется в системе доменных имен (Domain Name System, DNS), о которой речь пойдет далее в этой главе. (UDP также применяется для передачи в реальном времени звука и изображении, для которых потеря или непоследовательность данных не проблематичны.)
На рисунке 34-3 видно, что в заголовке UDP передается дополнительная информация -номера портов источника и получателя. Эти номера используются приложениями на обоих концах канала связи. Например, клиент посылает датаграмму, адресованную порту 1700 на сервере. Сервер ждет любые датаграммы, у которых номер порта получателя равен 1700, и, получив такую, отвечает клиенту другой датаграммой, которую клиент, в свою очередь, ожидает с номером порта получателя, например, 1701.
Формат IP-адреса и сетевой порядок байтов
Вы уже знаете, что IP-адреса имеют длину 32 разряда. Вы можете предположить, что в Интернете может существовать 2м (боже 4 млрд) уникальных адресов компьютеров, но это не так. Часть адреса идентифицирует локальную сеть, в которой находится хост-компьютер (host-computer), а другая часть — хост-компьютер внутри сети. Большинство IP-адресов являются адресами Класса С (Class С), формат которых показан на рисунке 34-5.
® 2'■ ■ ^ £ ¥ < « ? :':■ ■ • ИДйнгйфи?
ft Идектрфиквтурсети тягторхостш
21 разряд в разрядов
Рис. 34-5. Формат IP-адреса класса С
Как следует из формата, может существовать немногим более 2 миллионов сетей, в каждой из которых может быть 2® (256) хост-компьютеров. IP-адреса классов А и В, допускающие большее количество хост-компьютеров в сети, уже все израсходованы.
ПРИМЕЧАНИЕ «Хозяева» Интернета осознали недостаточную длину IP-адреса и предложили новый стандарт — протокол IPng (IP Next Generation, новое поколение IP), — в котором применяются датаграммы со 128-разрядными адресами вместо 32-разрядных. Используя IPng, Вы можете, к примеру, присвоить уникальные IP-адреса каждому электрическому выключателю в своей спальне и выключить в ней свет с портативного компьютера, находясь в любом месте земного шара Реализация IPng еще 1не запланирована.
По соглашению IP-адреса пишутся в десятичном формате с разделением точками (dotted- decimal). Четыре части адреса соответствуют отдельным байтам. Например, IP-адрес класса С может быть такой: 194.128.198.201. На компьютерах с процессором Intel байты адреса хранятся в формате «младший слева», называемом little-endian. На большинстве других компьютеров, в том числе под управлением UNIX, которые создали Интернет, байты хранятся в порядке «старший слева», называемом big-endian. Поскольку Интернету необходим машиннонезависимый стандарт для обменаданными, все многобайтовые значениядолжны передаваться в порядке «старший слева». Это означает, что программы на Intel-компьютерах должны выполнять преобразование из сетевого порядка байтов («старший слева») в порядок байтов хоста («младший слева») и обратно. Это относится как к 2-байтовым номерам портов, так и к 4-байтовым IP-адресам. '
Протокол TCP
Вы уже знакомы с ограничениями UDP, и Вы хотели бы иметь протокол, выполняющий безошибочную передачу больших блоков данных. Естественно, Вы хотите, чтобы программа-получатель принимала байты в той же последовательности, в какой они были переданы, пусть даже при этом отдельные датаграммы приходили бы в неправильной последовательности. Таким протоколом является TCP (Transmission Control Protocol) — главный протокол всех приложений Интернета, включая HTTP и FTP. На рисунке 34-6 показан формат сегмента TCP (Он не называется датаграмма.) TCP-сегмент располагается внутри IP-датаграммы, как показано на рисунке 34-7.
Протокол TCP устанавливает между двумя компьютерами дуплексное соединение типа «точка-точка». Программы на каждом конце соединения используют собственный порт. Комбинация IP-адреса и номера порта называется сокет (socket). Соединение устанавливается путем трехкратного квитирования (three-way handshake). Инициирующая программа посылает сегмент с установленным флажком SYN, отвечающая программа посылает сегмент с установленными флажками SKV и ЛСКи, наконец, инициирующая программа посылает сегмент с установленным флажком АСК. .
Рис. 34-6. Упрощенный формат сегмента TCP
,Датаграмма IP,
Iе< »1«ш«l•*«^<x<^<><>я•^^>«»eн><><><•[•:<{<$«м>4х«><хм>р<>х«<Х»*>1«Kt«1<»оо<х*оо<г<►<•<>l•••I-т«lt•j Сегмент ТОР:
s&&i«. s:»;®. saesst ■.- ’••.£■ ■ • ■ ., к ■ ■ .> 4 уу у
Обычно 20 байтОбычно 23 байт
Рис. 34-7. СегментТСР внутри датаграммы IP
После установления соединения каждая программа может посылать другой программе поток байтов. Для управления потоком TCP использует поля номеров последовательностей и флажки АСК. Программа-отправитель не ожидает подтверждения каждого сегмента, а посылает не сколько сегментов вместе и ждет первого подтверждения. Если программа-получатель должна отослать данные обратно отправителю, она может совместить подтверждения и данные в одних и тех же сегментах.
Номера последовательности программы-отправителя являются не индексами сегментов, а индексами в потоке байтов. Программа-получатель отсылает обратно номера последовательности (в поле номера подтверждении), удостоверяя тем самым, что все байты приняты и объединены в правильной последовательности. Программа-отправитель заново пересылает неподтвержденные сегменты..
Каждая программа со своей стороны закрывает TCP-соединение, отправляя сегмент с флажком FIN, что должно быть подтверждено программой на другой стороне соединения. Программа не может получать байты по соединению, которое закрыто на другой стороне.
Не переживайте по поведу сложности протокола TCP. API Winsock и Winlnet скрывают большинство деталей, поэтому Вы не будете иметь дело с флажками АСКи номерами последовательности. Ваша программа вызывает функцию для передачи блока данных, a Windows заботится о разделении блока на сегменты и размещении их в Ip-датаграммах. Windows также обеспечивает доставку байтов на приемный конец, однако, как Вы увидите далее в этой главе, это достаточно сложно.
Система доменных имен.
При работе с Web обычно используются не IP-адреса, а понятные имена — rnicrosojLcomwnM. www.cnn.com. На трансляцию такого имени хоста (host name) в IP-адрес затрачивается зца-
чительная доля ресурсов Интернета. Эту трансляцию выполняет распределенная сеть серверов имен (name server), обработывая запросы DNS (DNS queries). Все пространство имен Интернета объединено в домены (domains), входящие в безымянный корневой домен (root domain). Непосредственно под корнем находятся домены верхнего уровня — comt edu,govn org. ПРИМЕЧАНИЕ Не путайте домены Интернета и Microsoft Windows NT. Последние являются логическими группами сетевых компьютеров, совместно использующими общую базу данных системы защиты.
Серверы и им ена доменов ■
Сначала рассмотрим систему доменных имен со стороны сервера. Предположим, что у компании SlowSoft есть два компьютера, подсоединенных к Интернету, один для WWW-сервиса, а другой — для FTP. По соглашению эти хост-компьютеры называются wwwslowsojt.cotn'a ppsiowso^com соответственно; оба они входят в домен второго уровня (second-level domain) slowsoft, который SlowSoft зарегистрировала в InterNIC (см. http://wwwinternic.com/-).
a.root-servers.neta.root-servers.netns1 .expenslvenet.com ns.other.comns1 .expenslvenet.com ns.other.com
a.root-servers.net
a.root-servers.net
ns1 .expenslvenet.com ns.other.com
ns1 .expenslvenet.com ns.other.com
WWW
ftp
Рис. 34-8. Конфигурация домена SlowSoft
Теперь SlowSoft должна выделить два (или более) компьютера для серверов имен. В домене сот на серверах имен есть запись в базе данных для домена slowsoft# которой содержат-
;:я имена и IP-адреса двух серверов имен SlowSoft. На каждом сервере имен slowsoft есть записи базы данных для хост-компьютеров SlowSoft; на них также могут быть записи для хостов в других доменах и для серверов имен других доменов третьего уровня. Поэтому если сервер имен не может сам предоставить IP-адрес, он перенаправляет запрос серверу имен более низкого уровня. На рисунке 34-8 показана конфигурация доменов SlowSoft,
ПРИМЕЧАНИЕ Сервер имен верхнего уровня работает на собственном хост-компьютере. InterNIC использует (по последним данным) девять компьютеров, которые обслуживают корневой домен и домены верхнего уровня. Серверы имен нижнихуровней могут быть программами, выполняющимися на хост- компьютерах в любом месте Интернета. Служба доступа к Интернету (Internet service provider, ISP) может поддерживать серверы имен. Если ISP использует Windows NT Server, то сервером имен обычно является программа DNS, входящая в состав операционной системы. Если ISP для SlowSoft называется ExpensiveNet, то сервер имен может быть re1.expenswenet.com.
Клиенты и имена доменов
Теперь посмотрим со стороны клиента. Пользователь вводит в браузере bttp://u4owslow- soft.com. (Префикс bttp://fcoo6™@sr браузеру, что он должен использовать протокол HTTP, если в конечном счете обнаружит хост-компьютер.) Браузер должен разрешить (resolve) имя uwto.slowsoft.comB IP-адрес, поэтому он использует ТСРДР для отправки DNS-запроса по адресу стандартного шлюза (default gateway), заданного для ТСРДР. Этот адрес стандартного шлюза идентифицирует локальный сервер имен, у которого нужный адрес может оказаться в кэше. Если это не так, локальный сервер имен передает DNS-запрос одному из корневых серверов имен. Корневой сервер находит slowsoft в базе данных и отправляет запрос одному из серверов имен SlowSoft При этом IP-адрес для ги»а1л'/оцк(^.со?икзшируется для дальнейшего использования. Если пользователю потребуется обратное преобразование, серверы имен могут конвертировать IP-адрес в имя..
Основы HTTP
Вы собираетесь вскоре заняться программированием при помощи Winsock, но пересылать туда-сюда потоки байтов Вам не очень интересно? Для совместимости с существующими серверами и браузерами Интернета Вам нужен протокол более высокого уровня, HTTP как раз подходит для этого, так как он является протоколом для WWW и относительно прост.
HTTP построен на основе TCP. Он работает следующим образам: сервер ожидает (listens) запросы на порту 80; клиент (обычно браузер) соединяется (connects) с сервером, узнав его IP-адрес у сервера имен. Используя собственный номер порта, клиент устанавливает с сервером двустороннее TCP-соединение, после чего посылает ему запрос (request) Например: GET /customers/newproducts. html НТТР/1.0
Сервер определяет, что запрос относится к самому популярному типу GET, и решает, что клиенту нужен файл newproducts.html, находящийся в серверном каталоге /customers, который может совпадать или не совпадать с каталогом /customers на жестком диске сервера. Вот заголовки запросов (request headers), которые в основном описывают возможности клиента:: Accept: image/glf. imagc/x-xbitmap, inage/jpeg, image/pjpeg, image/x-jg, */* Accept-Language: en UA-pixels: 1024x768 UA-color; colord UA-OS: Windows NT UA-CPU: x86 User-Agent: Mozilla/2.0 (compatible; MSIE 3.0; AK; Windows NT) Host: ww.slowsoft.com Connection: Keep-Alive • If-Modified-Since:Wed, 26Mar 1997 20:23:04GMT (пустая строка)
Заголовок If-Modified-Since сообщает серверу, что тот должен передать файл newpto- ducts.html, если он был изменен после 26 марта 1997 года. Это подразумевает, что у браузера в кэше есть копия этого файла, датированная этим числам. Пустая строка в конце запроса критически важна — это единственный способ указать серверу, что надо закончить прием и начать передачу, поскольку TCP-соединение остается открытым.
Теперь сервер начинает действовать. Он посылает newproducts.html, но сначала передает ОК-ответ:
НТТР/1.0 200 ОК
за которым сразу следуют строки заголовка ответа (response header):
Server: Microsoft-Iis/2.0
Date: Thu. 03 Mar 1997 17:33:12 GMT
Content-Type: text/html■
Accept-Aanoes: bytes
Last-Modified: Wed, Mar 26 1997 20:23:04 GMT
Content-Lergth: 407
(пустая строка)■
Содержимое newproducts.html следует сразу за пустой строкой:
<Ьтш1>
<headxtitle>SlowSoft's New Products</titlex/head> cbodyxbody baokground="/images/clouds.jpg">
<h1xcenter>Welcome to SlowSoft's New Products List
</centerx/h1Xu>
unfortunately, budget constraints have prevented SlowSoft from introducing any new products this year. We suggest you keep enjoying the old products.<p>
<a href="dsfault. ht-n' >SlowSo"t's Home Page</axp>
</body>
</html>
Вы видите здесь элементарный код языка разметки гипертекста (HyperText Markup Language, HTML); данная Web-страница не поможет стать лучшим программистом года. Мы не будем вникать в детали потому, что об HTML имеются дюжины книг. Из этих книг Вы узнаете, что тэги (tags) HTML содержатся в угловых скобках и что часто для каждого «начального» тэга есть «конечный» (с символом ’/). Некоторые тэги, например <а> (днкергипертекста), имеют атрибуты. В вышеприведенном примере строка
<а href-"default.hrm">SlowSoft's Home Раде</а><р>,
создает ссылку на другой HTML-файл. Пользователь щелкает «SlowSoft’s Home Page* и браузер запрашивает default.htm с того же сервера.
На самом деле, newproducts .html ссылается на два серверных файла: default.htm и /images/ clouds.jpg. Файл clouds.jpg — файл формата JPEG, содержащий фоновую картинку для этой страницы. Браузер загружает каждый из этих файлов как отдельную транзакцию, каждый раз устанавливая и закрывая отдельное TCP-соединение. Сервер просто отправляет файлы любому клиенту, который их запросит. В данном случае сервер не знает, что newproducts.html и clouds.jpg запросил один и тог же клиент. Для сервера клиенты — это просто IP-адреса и номера портов. Для разных запросов от клиента номер порта может отличаться. Например, если десять программистов вашей компании просматривают Web через прокси-сервер (о котором речь пойдет позже в данной главе), сервер увидит для каждого клиента один и тог же IP-адрес (но разные номера портов).
ПРИМЕЧАНИЕ Web-страницы используют два графических формата: GIF и JPEG. Файлы GIF — это сжатые изображения, в которых сохранены все детали исходного несжатого изображения, но количество цветов обыч-ю ограничено 256. Файлы JPEG меньше, но они не сохраняют все детали исходного файла. Файлы GIF обычно используются для небольших изображений, например, кнопок; файлы JPEG
лрименяютсядля фотографий, в которых детали не так важны. Visual C++ может читать, писать и концертировать GIF- и JPEG-файлы, однако Win32 API не может работать с этими форматами без специального модуля'.
В стандарт HTTP включен также запрос PUT, который позволяет клиентузйг/ущщь (upload) файл на сервер. Клиенты и серверы редко реализуют PUT.
Основы FTP
Протокол FTP обрабатывает загрузку файлов с сервера и на сервер, а также просмотр ката- югови серверов. В состав Windows входит программа командной строки ftp (не работает через прокси-сервер Web), позволяющая соединиться с FTP-сервером при помощи UNlX- подобпых команд. Браузеры обычно поддерживают протокол FTP (только для загрузки файлов с сервера) более приятным для пользователя образом. Каталоги FTP-сервера можно защитить комбинацией имя_пользователя/пароль, но обе строки будут передаваться по Интернету в незашифрованном виде. FTP основан на TCP, Между сервером и клиентом устанавливается два соединения — одно для управления, другое для передачи данных.
!Интернет и интрасеть
Мы предполагали, что клиентский и серверный компьютеры соединены через всемирную сеть. Однако то же программное обеспечение клиента и сервера можно использовать в интрасети (intranet). Интрасеть часто реализуется в локальной сети предприятия и используется для распределенных приложений. В этом случае пользователи клиентских компьютеров видят знакомый интерфейс браузера, а серверные компьютеры выдают по запросу простые Web-страницы или выполняют сложную обработку данных.
Интрасеть имеет ряд преимуществ. Если, например, Вы знаете, что все ваши компьютеры работают на процессорах Intel, то Вы можете использовать ActiveX-элементы и серверы ActiveX-документов. Если нужно, на компьютерах можно запустить собственное программное обеспечение, позволяющее соединяться по TCP/IP без использования HTTP и FTP. Для защиты данных можно полностью отделить интрасеть от Интернета или подсоединить ее через ^«HcUrey3p(firewall), что является синонимом прокси-сервера.
Создание интрасети
Создание интрасети на основе Microsoft Windows — легкий и простой, а главное, дешевый процесс. Все необходимое для создания интрасети входит в состав Microsoft Windows 95/98 и Microsoft Windows NT, Если Вы не хотите тратить лишние деньги, то можете создать интрасеть на одном компьютере. Весь код из этой главы будет работать в такой однокомпью- тсрной конфигурации.
NTFS или FAT
При использовании Windows 95 или 98 Вы ограничены использованием одной файловой системы — FAT (точнее, VFAT для поддержки длинных имен). Применяя Windows NT, Вы можете во время установки выбирать между MT'S и FAT. Интрасеть будет лучше защищена при использовании NTFS, так как в этом случае возможна установка прав доступа к отдельным файлам и каталогам. Пользователи регистрируются на сервере (или на соединенной с ним рабочей станции), указывая имя и пароль.
Клиенты из Интернета и интрасети соответствуют этой схеме защиты, так как сервер может зарегистрировать их так, будто они локальные пользователи. Следовательно, Вы можете ограничить доступ к любому файлу или каталогу на сервере со стороны конкретных
Эго не совсем верно. Функции Win32 API могут работать с файлами JPEG, но нс GIF. — Прим.перев.
пользователей, которые должны указать пароли. Если рабочие станции пользователей являются сетевыми клиетами Windows (таки будет в случае интрасети на основе локальной сети), то имя и пароль передаются по сети при регистрации пользователя. .
Сетевое аппаратное обеспечение
Для создания сети, очевидно, необходимо более одного компьютера. Если ваш основной компьютер для работы — Pentium, Pentium Pro или Pentjum II, то есть шансы, что где-то рядом «завалялся» хотя бы один старый компьютер. Если он как минимум 486, то имеет смысл подсоединить его к основному компьютеру для тестирования интрасети и резервного копирования файлов.
На каждом компьютере нужна сетевая плата, но сейчас платы Ethernet на 10 МБит/с стоят менее $50. Выберите изготовителя, который либо поставляют свои драйверы для Windows 95, Windows 98 и Windows NT, либо уже поддерживается этими операционными системами. Для просмотра списка поддерживаемых плат выберите Network в Control Panel и щелкните Add н авкладке Adap ter.
На большинстве сетевых плат есть разъемы для коаксиального кабеля и витой пары стандарта lOBaseT. Для работы по витой паре Вам придется купить концентратор (hub), который стоит несколько сотен долларов и требует источника электроэнергии. Для работы по коаксиальному кабелю нужны только разъемы-терминаторы (и, естественно, сам кабель достаточной длины с разъемами на концах). В этом случае Вы соединяете компьютеры в цепочку, помещая на каждом конце по терминатору,
Сгедуйте инструкциям, приведенным в руководстве по установке сетевой платы. В большинстве случаев Вам придется запустить программу для MS-DOS, которая запишет настройки в электронную память (EEPROM) на плате, Запомните выбранные значения — они понадобятся Вам позже.
Настройка Windows для работы в сети
После вызова апплета Network в Control Panel Вы выбираете протоколы (protocols), адаптеры или сетевые платы (adapters or network boards) и службы (services). Диалоговые окна, которые Вы увидите на экране, зависят от используемой операционной системы — Windows 95, Windows 98 или Windows NT. Если Вам нужна интрасеть, то Вы должны выбрать ТСРДР в качестве одного из применяемых протоколов. Вы также должны установить драйвер для сетевой платы и убедиться, что заданные для него IRQ и адрес ввода/вывода соответствуют сохраненным в EEPROM. Кроме того, каждой плате нужно назначить IP-адрес. Если у Вас нет прямого выхода в Интернет, выберите любой уникальный адрес.
Сделанного достаточно для настройки интрасети, но Вы, вероятно, захотите совместно использовать в своей сети файлы и принтеры. Для этого в Windows NT надо установить Client And Server Services и настроить ихдля работы через TCP/IP; в Windows 95/98 установите Client For Microsoft Networks и File And Printer Sharing For Microsoft Networks. Если у Вас в сети уже установлен другой протокол (например, Novell IPX/SPX или Microsoft NetBEUI), то его можно использовать в сети совместно с TCP/IP. Тогда служба совместного использования файлов и принтеров будет работать на существующем протоколе, а интрасеть — на TCP/IP. Чтобы можно было использовать ресурсы компьютера, нужно разрешить это в Microsoft Windows Explorer (для файлов) или в папке Printers (для принтеров).
Именахостовдля интрасети: файл HOSTS .
Пользователи Интернета и интрасети предполагают, что в браузерах можно будет использовать имена хостов, а не их IP-адреса. Есть несколько способов разрешения имен в адреса, включая применение своего сервера DNS, являющегося частью Windows NT Server, Однако наиболее простым способом отображения имен на IP-адреса является файл HOSTS. В Win-
, lows NT этот текстовый файл находится в каталоге \Winnt\System32\DRIVERS\ETC; в Windows ‘>5/98 в каталоге \WINDOWS находится его прототип HOSTS.SAM, который надо просто ско- шровать в HOSTS и сделать изменения при помощи редактора Notepad. Убедитесь, что измененные файлы HOSTS распространены по всем компьютерам сети.
Тестирование интрасети: программа Ping
Для тестирования интрасети используйте программу Ping. В окне командной строке введите jimgu IP-адрес (в десятичном формате с разделением точками) или имя хоста другого компьютера сети. Если получите ответ, то TCP/IP настроен правильно. Если ответа нет или появилось сообщение об ошибке, то заново проверьте сетевые соединения и настройки.
1Интрасеть на одном компьютере: адрес локального хоста
Первая строка в файле HOSTS должна быть такой:
127.0.0.1 localhost
Это стандартный IP-ядреслокалъного хоста (loopback address). Если запустить сервер на ожидание запросов по этому адресу, то клиент на том же самом компьютере может подсоединиться к localhost и установить TCPДР-соедмнение с сервером. Эта схема работает независимо от наличия сетевой платы.
Winsock
Winsock — это Windows API нижнего уровня для программирования TCP/IP, основанный на Berkley Sockets API для UNIX. Часть кеда находится в wsock32.dll (в том числе экспортируемые функции, вызываемые вашей программой), а часть — в ядре Windows. При помощи этого API можно писать как клиентское, так и серверное программное обеспечение. Новая и более сложная версия Winsock 2 включена в состав Windows NT 4.0, но мы будем заниматься старой версией, поскольку она в настоящее время является стандартом как для Windows NT, так и для Windows 95/98.
Синхронноеиасинхронноепрограммирование
Winsock первоначально был определен для Win 16, в котором не поддерживается многопоточность. Поэтому большинство разработчиков использовали Winsock в асинхронном режиме. В этом режиме применяются различного рода скрытые окна и вызовы PeehMessage, которые позволяют однопоточной программе выполнять вызовы Winsock по передаче и приему данных, не блокируя работу пользовательского интерфейса. Асинхронные Winsock-программы сложны: они часто реализуют «машины состояния», которые обрабатывают функции обратного вызова, пытаясь определить следующее действие на основе того, что только что произошло. Однако мы сейчас находимся не в 16-разрядном мире, поэтому можем применить многопоточное программирование. Если это Вас пугает, перечитайте главу 12.. Вы полюбите многопоточное программирование, когда начнете его использовать.
В этой главе мы будем делать большинство вызовов Winsock из рабочих потоков, чтобы основной поток программы мог работать с пользовательским интерфейсом. Рабочие потоки основаны на красивой последовательной логике блокирующих вызовов Winsock.
Классы Winsock в MFC
Мы пытаемся использовать MFC-классы там, где это имеет смысл, но разработчики MFC проинформировали нас, что классы GAsyncSocfeetH CSocket не годятся для 32-разрядного синхронного программирования. В интерактивной справочной системе по Visual C++ говорится, что CSocket можно использовать для синхронного программирования, но если Вы загляните в исходные тексты, то увидите безобразный код на основе сообщений, оставшийся от Winl6.
Классы блокирующих сокетов
Так как MFC использовать нельзя, мы написали собственные классы Winsock. Класс CBlo- ckingSocket— это тонкая оболочка вокруг Winsock API, предназначенная только для синхронных вызовов в рабочем потоке. Единственными приятными исключениями являются генерация исключений в случае ошибок и тайм-ауты при передаче и приеме данных. Они помогают писать более ясный код, ибо не надо проверять ошибки после каждого вызова Winsock. Тайм-ауты, реализованные через вызов функции select, предотвращают бесконечное блокирование потока кода в случае сбоя связи.
Класс CHttpBlockingSocket, производный от CBlockingSocket, предоставляет функции для чтения HTTP-данных. Классы CSockAddni CBlockingSocketException— вспомогательные.
Вспомогательный класс CSockAddr
Многим функциям Winsock в качестве параметра нужен адрес сокета. Как Вы, возможно, помните, адрес сокета состоит из 32-разрядного IP-адреса и 16-разрядного номера портя. В Winsock для задания адреса используется 16-байтовая структура sockaddrin-.
struct sockaddr_in {
short sin.famlly;
u_short sin_port;
struct in_addr sin_addr;.
, char sin_zero[8);
IP-адрес хранится как значение типа in_addr, определенное следующим образом: struct in_addr { union {
struct { u_char s_o1.s_b2,s_b3.$_b4; } S_un_b;
struct { u_short s_wi,s_w2: } S_un_w: u_long S_addr;
} S_un;
Это весьма «страшные» структуры, поэтому мы решили создать производный от sockaddijn С++-класс, довольно приятный на вид. В файле \vcpp32\ex34a\Blocksock.h на прилагаемом компакт-диске содержится его кед, включая подставляемые (inline) функции: class CSockAddr : public sockaddr_ln { publics■
// конструкторы CSockAddr(),
{ sln_family = AF_INET; sin_port = 0; sin_addr.s_addr - 0;
} // стандартные
CSockAddr(const SOCKADDR& sa) { remcpy(this, &sa, sizeof(SOCKADDR)); }
CSockAdd■'(const SOCKADDR.IN& sin) { memcpy(this, Asin, sizeof(SOCKADDR_IN)); }
CSockAddr(const 'J LONG ulAddr, const USHORT ushPort - 0) // параметры должны иметь порядок байтов хоста {
sin_f amily = AFJNET;
sin_port = htons(ushPort); sin_addr.s_addr = htonl(ulAddr): '
CSockAddr(const char* pchIP, const USHORT ushPort = 0)
// строка IP-адреса с разделением точками
см. след. стр.
sin„family = AF_INET;
sin_port = htons(ushPbrt);"
sin_addr.s_addr = lnet_addr(pchIP);.
} // уже в сетевом порядке байтов
И Возвращает адрес 8 десятичном формате с разделением точками
CString DottedDecimalO
{ return inet„ntoa(sin_addr); }
jl создает новый объект CString
И Получение порта и адреса (даже если они открытью)
U3H0RT Port () const
{ return ntohs(sin port); }'
ULONG IPAddrO const “
{ return ntoh 1 (si n_add r. s_add r); }
И операторы добавлены для эффективности
const CSockAddrS operatar=(const SOCKADORS sa)
memcpy(this, &sa, sizeof(SOCKADDR));
return *this;
}
const CSockAddr& operator=(const SOCKADDR TN& sin)
{
memcpy(this, &sin, sizeof(SOCKADDR_IN));
return «this;
}
operator SOCKADDRO
{ return *((LPSOCKADDR) this); }
operator LPSOCKA№fl()■
{ return (LPSOCKADDR) this: }
operator lpsockaddr_ino
{ return (LPSOCKADDR_IN) this; }
Как видите, в этом классе есть несколько полезных конструкторов и операторов приведения типа, которые делают его взаимозаменяемым с типом sockaddrin и эквивалентными ему SOCKADDRJNsochaddry. SOCKADDR. Есть конструктор и функция-член дня IP-адреса в десятичном формате с разделением точками. Внутри адрес сокета хранится в сетевом порядке байтов, но функции-члены имеют параметры и возвращаемые значения в порядке байтов хоста. Для преобразования между двумя порядками байтов используются Winsock-функции btonl, btons, ntobs и ntobl.
Класс CBtockingSocketException
Все функции (СИосй^^осйеГгенерируют объект CBIockingSocketException, когда вызов Winsock возвращает ошибку. Этот класс является производным от MFC-класса CException и переопределяет функцию GetErrorMessage.^ra функция возвращает номер ошибки Winsock и текстовую строку, которые CBlochingSocfeet3afi.aer при генерации исключения.
Класс CBIockingSocket
На рисунке 34-9 показан отрывок из заголовочного файла для класса CBIockingSocket.
BLOCKSOCK.H'
class CBIockingSocket : public CObject {
DECLARE_DYNAMIC(CBlockingSocket)
Рис. 34-9. Отрывок из заголовочного файла для класса CBIockingSocket
public:
SOCKET m_hSocket;
CBlocxingSocketQ; { m.hSocket = NULL; }
void Cleanup();'
void Create(int nType = SOCK_STREAM);
void Ciose();
void Bind(LPCSOCKADDR psa);
void ListenQ;
void Connect(LPCSOCKAQDR psa);
BOOL Accept (CBlockingSocket& s, LPCSOCKADDR psa);
int Send(const char* pch, const int nSize, const int rSecs);
int Write(const char* pch, const int nSize, const int nSecs);
int Receivefchar* pch, const int nSize, const int nSecs);
int SendDatagram(const char* pch, const int nSize, LPCSOCKADDR psa, const int nSecs);
int ReceiveOatagram(char* pch, const int nSize, LPCSOCKADDR psa, const int nSecs);
void GetPeerAddr(LPCSOCKADDR psa);
void GetSockAddr(LPCSGCKADDR psa);
static CSockAddr GetHostByName(const char* pchName,
const USHORT ushPort = 0);
static const char* GetHostByAddr(LPCSOCKADDR psa);
operator S0CKET();
( return m_hSocket; } };
Список функций-членов CBlockingSocket, начиная с конструктора.
■• Конструктор создает неинициализированный объект. Для создания сокета и подсоединения его к объекту нужно вызвать Create.
tn Create вызывает Winsock-функцию socket и присваивает возвращенный 32-разрядный описатель типа SOCKET переменной-члену m_hSocket.
ПараметрОписание
NType Тип сокета; может быть SOCK_STREAM^no умолчанию) или SOCKJDGRAM
Close закрывает открытый сокет, вызывая Winsock-функцию closesocket. Предварительно должна быть вызвана функция Create. Деструктор не вызывает эту функцию, потому что он не сможет перехватить (catch) исключение для глобального объекта. Для сокета, ожидающего запросы, сервер может вызвать Close в любой момент.
Bind вызывает Winsock-функцию hind для привязки ранее созданного сокета к конкретному адресу. До вызова Listen сервер вызывает Bind с указанием адреса сокета, содержащего номер порта ожидания и IP-адрес сервера. Если в IP-адресе указать INADDR_ANY,to Winsock использует IP-адрес вашего компьютера.
ПараметрОписание
psa Объект CSockAddnmn указатель на переменную типа sodeaddr
Listen вызывает Winsock-функцию listen. Сервер вызывает Listen в начале ожидания запросов на порту, заданном предшествующим вызовом Bind. Функция завершается сразу.
Accept вызывает Winsock-функцию accept. Сервер вызывает Accept сразу после вызова Listen. Функция завершается, когда клиент подсоединяется к сокету, возвращая новый сокет (через переданный в нее объект С’Б/ос&ги^Уос^е^),соответствующий новому соединению.
ПараметрОписание
jСсылка на существующий объект CBiockingSocket, для которого не
была вызвана функция Create
psaОбъект CSockAddr^™ указатель на переменную типа sochaddrjpix
адреса подсоединенного сокета
Возвращаемое значение TRUE, если все хорошо
Connect вызывает Winsock-функцию connect. Клиент вызывает Connect после вызова Create. Возврат из Connect происходит после установления соединения.
ПараметрОписание
psaОбъект CSodeAddrroiH указатель на переменную типа sockaddr
Send вызывает Winsock-функцию send после установки тайм-аута при помощи select. Количество реально переданных байтов при вызове Send зависит от того, насколько быстро программа на другом конце соединения может получить байты. Send генерирует исключение, если сокет на другом конце соединения был закрыт до того, как были прочитаны все байты.
Параметр
Описание
pch
Указатель на буфер, содержащий передаваемые данные
«Size
Размер (в байтах) блока данных
nSecs
Значение тайм-аута (в секундах)
Возвращаемое значение
Количество переданных данных
Write постоянно вызывает Send до тех пор, пока не будут переданы все байты или пока получатель не закроет сокет.
ПараметрОписание
рсЬУказательна буфф, содержащий передаваемые данные
nS&eРазмер (в байтах) блока данных
nSecsЗначение тайм-аута (в секундах).
Возвращаемое значение Количество переданных данных
Receive вызывает Winsock-функцию recv после установки тайм-аута при помощи select. Возвращает полученные байты. Дополнительную информацию см. в описании класса CHttpBlockingSocketB следующем разделе.
Параметр
Описание
рек
Указатель на буфер, сохраняющий полученные данные
nSize
Размер (в байтах) буфера для данных
nSecs
Значение тайм-аута (в секундах)
Возвращаемое значение
Количество полученных данных
SewdDatagramвызывает Winsock-функцию sendto. Программа на другом конце соединения должна вызвать ReceiveDatagram. Для датаграмм не нужно вызывать Listen, Accept или Connect, но надо предварительно вызвать Create с параметром SOCKJDGRAM.
Параметр
Описание
рсЬ
Указатель на буфер, содержащий передаваемые данные
nSine
Размер (в байтах) блока данных
Параметр
Описание
psa
Адрес получателя датаграммы; объект CSockAddr или указатель на пе-
ременную типа sockaddr
nSecs
.'чачепие тайм-аута (в секундах)
В озвращаем ое значе ние
Количество переданных данных
ReceiveDatagram вызывает Winsock-функцию recvfrom. Возврат из функции происходит, когда программа па другом конце соединения вызывает SendDatagram. Предварительно надо вызвать Create с параметром SOCK_DGRAM,
Ларам етрОписание
pcbУказатель на буфер, сохраняющий полученные данные
nSi^eРазмер (в байтах) буфера для данных
psaАдрес отправителя датаграммы; объект CSockAddr или указатель на переменную типа sockaddr•
nSecsЗначение тайм-аута (в секундах)
Возвращаемое значениеКоличество полученных данных
Ge 1РеегА44№Ь1зывает Winsock-функцию getpeername, возвращает порт и IP-адрес сокета на другом конце соединения. Если Вы подсоединены к Интернету через прокси-сервер, то получите его IP-адрес.
ПараметрОписание
psaОбъект CSockAddr или указатель на переменную типа sockaddr
GetSockAddr’SHSHBaeT Winsock-функцию gelsockname, возвращает адрес сокета на этом конце соединения. Если программа на другом конце — сервер локальной сети, то IP-адрес — это адрес, присвоенный сетевой плате компьютера. Если же на другом конце работает Интернет-сервер, то IP-адрсс будет присвоен службой доступа (service provider) при дозвоне. В обоих случаях Winsock присваивает номер порта, свой для каждого соединения.
ПараметрОписание
psaОбъект CSocMddrHnH указатель на переменную типа sockaddr
СеШох1ВуМ«те(статическая?) вызывает Winsock-функцию getbostbyname, запрашивает сервер имен и возвращает адрес сокета, соответствующий имени хоста. Функция сама себе устан авливает тайм- аут.
Параметр_Описание
pcbNameУказатель на массив символов, содержащий имя хоста
usbPorlНомер порта (по умолчанию 0), который станет частью возвращенно
го адреса совета
Возвращаемое значение Адрес сокета, содержащий IP-адрес от DNS и номер порта ashPort
СеШс>81ВуАД<1К<^тическая') вызывает Winsock^yHK4HK,geribosrby<?<M-, запрашивает сервер имен и возвращает имя хоста, соответствующее адресу сокета. Функция сама себе устанавливает тайм-аут.''
Параметр *
Описание,
psa
Объект CSockAddr или указатель на переменную типа sockaddr
Возвращаемое значение
Адрес совета, содержащий IP-адрес от DNS и номер порта usbPort
Cleanup закрывает сокет, если он открыт. Не генерирует исключение, поэтому можно вызывать внутри блока catch при обработке исключения.
Iе operator SOCKET переопределенный оператор, который позволяетиспользоватй объект CBlockingSocketвместо параметра SOCKET.
Класс CHttpBlockingSocket
При вызове CBlockingSocket::Recewe Вы с трудом определите момент, когда необходимо закончить прием байтов. При каждом вызове возвращаются байты, находящиеся в этот момент на вашем конце соединения. Если байтов нет, вызов блокируется, но если отправитель закроет сокет, будет возвращено ноль байтов.
В разделе о HTTP Вы узнали, что клиент посылает запрос, заканчивающийся пустой строкой. Сервер предполагает, что надо отослать заголовки и данные ответа при обнаружении пустой строки, но клиент должен проанализировать заголовки ответа прежде, чем читать данные. Эго означает, что пока TCP-соединение остается открытым, программа-приемник должна обрабатывать получаемые данные по мере их поступления. Простой, но неэффективный прием — вызывать Receive для каждого байта. Лучше использовать буфер.
Класс <^йр5/осА»'л^осйеДобавляег буферизацию к CBlockingSocket и содержит две новые функции-члена. Вог часть файла \vcpp32\ex34A\Blocksock.h:
class CHttpBlockingSocket : public CBIockingSocket
public:
DECLARE_DYNAMIC(CHttpBlockingSocket)
enum {nSizeRecv = 1000); // макс, длина приемного буфера {> длины строки заголовка)
CHttpBlockingSocket();
-CHttpBlockingSocket();
int ReadHttpHeaderLine(char* pch, const int nSize, const int nSecs);
int ReadHttpResponse(char* pch, const int nSize, const int nSecs);
privates
char* m_pReadBuf; // буфер для чтения
int m nReadBuf; // количество байтов в буфере для чтения
Конструктор и деструктор занимаются выделением и освобождением буфера размером 1000 символов. Вот описание двух новых функций-членов.
Хед^^тфНеа^егЕ^шозвращает одну строку заголовка, заканчивающуюся парой символов <сг><1Г>. Функция вставляете конец строки нулевой символ. Если буфер строк полон, завершающий нуль вставляется в его конец.’
Параметр
Описание
pcb
Указатель на имеющийся буфер, в который будет записана принятая строка (с нулем на конце)
nSize
Размер буфера pcb
nSecs
Значение тайм-аута (и секундах)
Возвращаемое значение
Количество полученных байтов, исключая нуль на конце
ЯешйТГ9>Ке»роп$юозвращает остаток ответа сервера, принятый при закрытии сокета или при переполнении буфера. Не предполагайте, что в буфере на конце есть нуль.
Параметр
Описание
pcb
Указатель на имеющийся буфер, куда будет записана принятая строка
nSize
Максимальное количество принимаемых байтов
ПараметрОписание.
nSecsЗначение тайм-аута (в секундах)
Возвращаемое значениеКоличество полученных байтов
Упрощенныйсервер HTTP
Наступило время использовать класс блокирующего сокета, чтобы написать программу НТТР- сервера. Все липшее убрано, ио код действительно работает с браузером. Сервер всего лишь возвращает несколько жестко зашитых в него заголовков и HTML-операторов в ответ на любой запрос GET. (Далее в этой главе в программе ЕХ34А приведен пример более функционального НТТР-ссрвера.)
Инициализация Winsock
Перед вызовом любой Winsock-функции необходимо инициализировать Winsock-библиотеку. Два следующих оператора в функции-члене Initlnstanceделают это:
WSADATA wsd;
W$AStartup(OxO1O1, Awsd);
Запусксервера
Сервер запускается в ответ на некоторое действие пользователя, например, выбор из меню. Бот обработчик команды:
CBlockingSocket g_sListen; // один глобальный сокет для ожидания запросов void CSocketView::OnInternetStartServer() { try {
CSockAddr saServer(INADDR_ANY, 80);
g_sListen.Create();
g_sListen.9ind(saServer);
g_sListen.Listen();
A7xBeginThread(ServerThreedProc, GetSafeHwnd());
catch(CBLockingSockerException* e) { g_slisten.Cleanup();
// Делаем что-нибудь с исключением e->Delete();
}
I
Очень просто, не правда ли? В обработчике создается сокет и запускается рабочий поток, который ожидает клиент, пытающийся подсоединиться к порту 80. Если что-нибудь не так, генерируется исключение. Глобальный объект g_sListen имеет время жизни, совпадающее с временем жизни программы, и способен поддерживать множество параллельных соединений, каждое в отдельном потоке.
Серверный поток
Теперь взглянем на функцию ServcrThreadProc:
UINT ServerThreadProc(LPVOID pParam)
{’
CSockAddr saClient;
CHttpBlockingSocket sConnect;
char request[lC0]:
char headers!] = "НПР/1.0 200 OK\r\n"
"Server: Inside Visual C++ S0CKO1\r\n"
"Date: Thu, 05 Sep 1996 17:33:12 GMT\r\n"
"Content-Type: text/html\r\n"
"Accept-Ranges: bytes\r\n"
"Con tent-Length: 187\r\n"
' V\n"; // очень важная пустая строка
char html[] =
■'<htmlxheadxtitle>Inside Visual C++ Server</title></head>\r\n"
"cbodyxbody background=\"/samples/images/usa1. jpg\">\r\n"
■'<h1xcente.r>This is a custom home page</cenler></h1><p>\r\n"
■</body></htrnl>\r\n\r\n";
try {
if(!g_sListen.Accept(sConnect, saClient)) {
// Обработчик в классе "вид" закрыл ожидающий сокет return 0;
AfxBeginThread(ServerThreadProc, pPa ram);
// прочитать запрос от клиента
sConnect.ReadHttpHeaderLine(request, 100, 10);
TRACE("SERVER: %s", request); // Напечатать первый заголовок
lf(strnicmo(request, "GET11, 3) == 0) {
do { // Обработать остальные заголовки запроса
sConnect.ReadHttpHeaderLine(request, 100, 10);■
TRACE("SERVER: %s", request); // Напечатать остальные заголовки
} while(strcnip(request, "\r\n"));
sConnect.Wnte(headers, strlen(headers), 10); // заголовки ответа
sConnect.Write(html, strlen(html). 10); // код HTML.
else•
TRACE("SERVER: not a GET\n");
//не знаю, что делать
}
sConnect.Close(); // деструктор его не закрывает
}
catch(CBlockingSocketException* е) {
// Делаем что-то с исключением
e->Delete();■
1
return 0;
)
Наиболее важным является вызов Accept. Поток кода блокируется до тех пор, пока клиент не подсоединится к порту 80 сервера, после чего Accept вернет новый сокет sConnect. Текущий поток сразу же запускает другой поток.'
Между тем текущий поток должен обработать запрос клиента, который пришел на sConnect Сначала считываются все заголовки запроса путем вызова ReadHttpHeaderLin^o тех пор, пока не обнаружится пустая строка. Затем вызывается Write для отправки заголовков и HTML- операторов ответа. Наконец текущий поток вызывает Close для закрытия соединения через сокет. Конец жизни данного соединения. Следующий поток остается блокированным на вызове A ccept, ожидая следующего соединения.
Очистка
Чтобы избежать при завершении «утечек» памяти, программа должна убедиться, что завершены все рабочие потоки. Простейший способ выполнить это — закрыть ожидающий сокет Это приведет к тому, что все имеющиеся вызовы Accept вернут FALSE и потоки завершатся, try {
g_sListen.Close():
Sleep(340); // Ожидание завершения потока
WSACleahupO; // Отключение Winsock
1
catch(CUserException* e) {
e->Delete():
}
Проблема может возникнуть в том случае, если поток находится в процессе обработки клиентского запроса. Основному потоку придется оптимистично предположить, что все рабочие потоки завершились.
Упрощенный клиент HTTP
Теперь вариант со стороны клиента — простая программа, которая выполняет слепой (blind) запрос GET, Когда сервер принимает запрос GET с косой чертой, показанный ниже, он возвращает HTML-файл по умолчанию.
GET / НТТР/1.0
Если в браузере ввести http://wwwslowsojt.com>H отправит слепой запрос GET.
Программа-клиент может использовать тот же самый класс CHttpBlockingSocket, который мы уже видели; она должна инициализировать Winsock так же, как эго делает сервер, Обработчик команды просто запускает рабочий поток при помощи следующего вызова:
AfxBeginThread(ClientSocketThreadProc, Get8afeHwnd());
Вот код клиентского потока:
CString g_strServerName = "localhost"; // или другое имя хоста
UINT ClientSocketThreadProc(LPVOID pParan)
CHttpBlockingSocket sClient;
char* buffer = new char(MAXBUF);
int nBytesReceived = 0;
char request[] = "GET / HTTP/1.O\r\n";"
char headers!] - // Заголовки запроса •
"User-Agents Mozilla/1.22 (Windows; U; 32bit)\r\n".
' "Accepts «AW
"Accepts image/gif\r\n"
"Accept: image/x-xbitmap\r\n"
"Accepts image/jpeg\r\n"
“\r\n"; // это необходимо
CSockAddr saServer, saClient;
try (
sClient. CreateO;.
saServer = CBlockingSocket; ,GetHostByNanie(g_strServerName, 80);.
sClient.Connect(saServcr);
sClient.Write!request, strlen(request), 10);
sClient.Write(headers, strlen(headers), 10);,
do { // Прочитать все заголовки ответа сервера
nBytesReceived = sClient.ReadHttpHeaderLlne(buffer, 100, 10);
} while(strcmp(buffer, "\r\n")); // до первой пустой строки
nBytesReceived = sClient. ReadHttpResponse(buffer, 100, 10);
if(nBytesReceived == 0) {
AfxMe$sageBox("No response received"):
) else { buffer[nBytesReceived] = '\0';.
AfxMessageBox!buffer);
}
catch(CBlockingSocketExcepticn» e) {
И Зарегистрировать исключение
см, след. стр.
e->Delete();
sClient.Close():
delete [] buffer;‘
return 0; // Поток заверчен1
}' '
Поток сначала вызывает CBlockingSocket;:GetHostByNamgyva получения IP-адреса сервера. Затем он создает сокет и вызывает Connect. Теперь у нас есть двусторонний канал связи с сервером. Поток посылает запрос GET, за которым следуют несколько заголовков запроса, считывает заголовки ответа сервера и сам ответный файл, который предполагается текстовым. После вывода текста в окне сообщения поток завершается. .
Создание Web-сервера при помощи CHttpBlockingSocket '
Если Вам нужен Web-сервер, то лучше всего купить один из них или использовать Microsoft IIS, который входит в состав Windows NT Server. Однако Вы узна'ете больше, если создадите свой сервер; кроме того, Вы получите полезное диагностическое средство. А если Вам нуж ны возможности, которые IIS не предоставляет? Например, Вам нужно добавить возможности Web-сервера в существующее приложение или если у Вас есть свой ActiveX-элемент, который устанавливает TCP-соединение с сервером не на основе HTTP. Тщательно ознакомьтесь с кодом сервера в примере ЕХ34А на прилагаемом компакт-диске, который работает под Windows NT, Windows 95 и Windows 98. Он может служить основанием для создания ваших собственных серверных приложений.
О граничениясервераЕХ34А
■Серверная часть программы ЕХ34А выполняет GET-запросыдля файлов, а также может обра - батывать POST-запросы, которые будут объяснены в главе 35, Это два наиболее общих типа HTTP-запросов. Однако ЕХ34А не может выполнять CGI-сценарии или подгружать lSAPI-библиотеки. (Подробности об ISAPI см. в главе 35.) ЕХ34А не обеспечивает защиту и не имеет возможностей FTP. Без учета вышесказанного это отличный сервер’ Если нужны отсутствующие возможности, напишите код для них сами.
Архитектура сервера ЕХ34А
Вскоре Вы увидите, что ЕХ34А объединяет сервер HTTP, Winsock-клиента HTTP и двух Winlnet - клиентов HTTP. Все три клиента могут общаться со встроенным сервером или с любым другим сервером Интернета. Любая клиентская программа, включая утилиту Telnet и стандартные браузеры, например, Microsoft Internet Explorer 4.0, может подсоединиться к серверу ЕХ34А Мы рассмотрим клиенты чуть позже в этой главе,. ,'
ЕХ34А — это стандартное приложение MFC SDI с архитектурой «документ-вид», в котором класс «вид» является производным от CEditView. Основное меню включает элементы Start Server и Stop Server, а также команду Configuration, которая вызывает диалоговое окно для установки основного каталога, файла для ответа на слепой GET и номера порта (обычно 80).
Обработчик команды Start Server устанавливает глобальный сокет в режим ожидания и запускает поток, как и в ранее рассмотренном упрощенном HTTP-сервере. Подробности см. в файле \vcpp32\ex34a\ServerThread.cpp в проекте ЕХ34А. Каждый раз, когда поток сервера обрабатывает запрос, он посылает сообщение в окно класса CEditView. Сообщения также л >- сылаются в случае генерации исключений, например, при ошибках привязки к порту.
Основная задача сервера — доставить файлы клиенту. Сначала сервер открывает файл и сохраняет указатель на CFite в переменной pFile, затем читает 5-КВайтныс (SERVERMAXBUF) блоки и записывает их в сокет яСоппес$;как. показано ниже-
char* buffer = new char[SERVERMAXBUF];*
DWORD dwLength = pFlle->GetLcngth();
nBytesSent = 0;
DWORD dwBytesRead = 0;
UlNT uBytesToRead;
while(dwBytesRead < dwLength) {
uBytesToRead = min(SERVERMAXBUF, dwLength - dwBytesRead);
VERIFY(pFile->Read(buffer, uBytesToRead) == uBytesToRead);1
nBytesSent += sConnect.Write(buffer, uBytesToRead, 10);
dwBytesRead += uBytesToRead;.
Сервер запрограммирован отвечать на GET-запросдля «липового» файла по имени Custom. Он генерирует НТМЬ код, который выводит IP-адрес клиента, номер порта и последовательный номер соединения. Это — вариант подстройки сервера под нужды пользователя.
Сервер обычно ожидает запросы на сокете, привязанном к адресу 1NADDR_ANY.Qto IP-адрес сервера по умолчанию, определенный для Ethernet-платы или присвоенный по время подсоединения к вашему ISP. Если у серверного компьютера есть несколько IP-адресов, то Вы можете настроить сервер на использование одного из них, заполнив поле Server IP Address на странице Advanced Configuration. Можете также изменить номер порта на странице Server. Например, если Вы выберите порт 90, то пользователи будут подсоединяться к bttp://loculbost:90.
На самой левой индикаторной панели строки состояния во время работы сервера выводится «Listening».
Использование функции Win32 TransmitFlte
Если у Вас установлена Windows NT 4.0, то Вы можете сделать сервер более эффективным, используя в вышеприведенном коде вместо CFile.tRead №1п32-функцию TransmitFile, которая посылает байты из открытого файла напрямую в сокет. Функция SeroerTbreadProccaдepжит следующие строки:
if (: :TransniitFile(sConnect. (HANDLE) pFile->m_hFile, dwLength, 0, NULL, NULL, TF_DISC0N4ECT))
Для выполнения этого кода при наличии Windows NT уберите комментарии со строки
«define USE.TRANSMITFILE
сверху файла ServerThread.cpp.
Сборка и тестирование ЕХ34А
Откройте проект \vcpp32\ex34a в Visual C++ и соберите его. Подкаталог Website в каталоге проекта, содержащий несколько HTML-файлов, задается как основной каталог сервера ЕХ34А, что для его клиентов соответствует корневому каталогу сервера.
ПРИМЕЧАНИЕ Если на вашем компьютере работает другой HTTP-сервер, то остановите его. Например, если Вы вместе с Windows NT Server установили IIS, то он, скорее всего, сейчас исполняется, поэтому Вы должны запустить программу Internet Service Manager из меню Microsoft Internet Server. Выберите строку WWW Service и щелкните кнопку останова (с квадратом). ЕХ34А сообщит об ошибке привязки (10048), если другой сервер уже использует порт 80.
Запустите программу под отладчиком и выберите Start Server из меню Internet. Теперь войдите в свой Web-браузер и введите адрес localhost. Вы должны увидеть надпись «Welcome То The Inside Visual C++ Home Page» и графические изображения. Окно сервера ЕХ34А должно выглядеть как на рисунке на стр. 724.
В окне отладчика Visual C++ Вы мажете увидеть список заголовков клиентского запроса
Если Вы щелкнете кнопку браузера Refresh, то увидите примерно такое сообщение об ошибке ЕХ43А:
WINSOCK ERROR-SERVER: Send error #10054 - 10/05/96 04:34:10 GMT
Это говорит о том, что браузер прочитал из заголовка ответа сервера дату модификации файла и понял, что ему не нужны данные, так как этот файл у него уже есть в кэше. В этом случае браузер закрывает сокет, из-за чего сервер обнаруживает ошибку. Если бы сервер ЕХ34А был «умнее», он бы проверил заголовок клиентского запроса If-Modified-Since перед передачей файла.
Конечно, Вы можете тестировать сервер в своей интрасети. Запустите сервер на одном компьютере, а браузер на другом и введите имя хоста так же, как в файле HOSTS,
Использование Telnet
HlHlУтилита Telnet входит в состав Windows 95, Windows 98 и Windows NT. Она полезна для тестирования серверных программ, подобных ЕХ34А. При помощи Telnet Вы передаете по одному символу за раз, что означает получение серверной функцией CBlockingSocket: .Receive по одному символу за раз. Ниже показано окно Telnet
Hl
Hl
При первом запуске Telnet выберите Preferences из меню Terminal и установите флажок Local Echo. Далее каждый раз выбирайте Remote System из меню Connect и вводите имя сервера и номер порта 80. Теперь можете набирать GET-запрос, завершив его двойным нажатием Enter, но делайте это быстро, так как для вызова Receive сервера ЕХ34А установлен 10-се- купдный тайм-аут.
Создание Web-клиента с помощью CHttpBlockingSocket
Если бы Вы несколько лет назад написали бы свой браузер Интернета, то имели бы сейчас несколько миллиардов долларов. Но в наше время браузер можно свободно загрузить по Интернету, поэтому писать его не имеет смысла. Однако можно дополнить ваши Windows- приложения возможностями доступа в Интернет. Winsock не самое лучшее средство, если Вам нужен только доступ по протоколам HTTP или МР, но он хорошо подходит для обучения
Winsock-клиент EX34A
Программа EX34AреализуетWinsock-клиенгв файле \vcpp32\ex34a\ClientSockThread.cpp на прилагаемом компакт-диске. Код похож на код упрощенного НТТР-клиенга. Используются глобалт 1ыепеременные, установленные в окне свойств Configuration, в том числе имя файла сервера, имя хоста сервера, IP-адрес и номер порта сервера, а также IP-адрес клиента. Последний нужен, только если ваш компьютер поддерживает несколько IP-адресов. При запуске клиент подсоединяется к заданному серверу и посылает GET-запрос для заданного файла. Сообщения об ошибках Winsock-клиенг помещает в основное окно ЕХ34А
Поддержка прокси-серверов в ЕХ34А
Если ваш компьютер подсоединен к локальной сети, то он, скорее всего, будет иметь выход в Интернет не напрямую, а через прокси-сервер (proxy server), иногда называемый брандмауэрам (firewall). Есть два типа таких серверов: Web и Winsock. Web-прокси, иногда называемые CERN-прокси, поддерживают только протоколы HTTP, FTP и gopher. (Протоколgopher— предшественник HTTP — позволяет получить доступ к файлам Интернета с символьных терминалов.) Winsock-клиенг нужно специально настраивать для использования Web-прокси. Winsock-прокси более гибкий и может под держивать другие протоколы, например, RealAudio. Вместо модификации исходного кеда вашей клиентской программы Вы подключаетесь к специальной Remote Winsock DLL, которая может взаимодействовать с Winsock-прокси-сервером.
Клиент ЕХ34А может соединяться через прокси-сервер, если установить флажок Use Proxy на странице Client Configuration. В этом случае Вы должны знать и указать имя такого сервера. После этого клиент соединяется с прокси, а не с настоящим сервером. Однако во всех POST- и GET-запросах для файла должен быть указан полный URL (Uniform Resource Locator). Например, если Вы подсоединены напрямую к серверу SlowSoft.TO запрос GET может быть такой:
GET /cus'smers/newproducts.html НТТР/1.0
Но если Вы подсоединены через прокси-сервер, то запрос GET будет следующим:
GET http://slowsoft.com/cusromers/rewproducts. html НТТР/1.0
Тестирование Winsock-клиента ЕХ34А
Простейший способ протестировать Winsock-клиенг — использовать встроенный Winsock- сервер. Просто запустите сервер как прежде и выберите Request (Winsock) из меню Internet В окне сообщения Вы увидите некий HTML-код. Можно также протестировать клиент, используя П8, сервер ЕХ34А в другом процессе или на другом компьютере или любой сервер в Интернете. Пока не обращайте внимания на поле вывода «Address» в диалоговой панели — это для Winlnet-клиентов, Вы должны вводить имена сервера и файла на странице Client диалогового окна Configuration.
Winlnet
Winlnet — эго API более высокого уровня, чем Winsock, но он предназначен только для HTTP-, FTP- и gopher-клиентов, работающих в асинхронном или синхронном режимах. Winlnet нельзя использовать для создания серверов. WININET.DLL не зависит от WINSOCK32.DLL Microsoft Internet Explorer 3.0 (IE3) и ActiveX-элементы используют Winlnet.
Преимущества Winlnet по сравнению с Winsock
Winlnet значительно превосходит Winsock в поддержке профессиональных клиентских программ. Вог некоторые из достоинств Winlnet
Кэширование. Подобно ГЕЗ, Ваш Winlnet-клиент кэширует HTML- и другие файлы Интернета. Вам ничего не нужно делать, просто когда клиент во второй раз запросит какой- нибудь файл, он будет загружен с локального диска, а не из Интернета.
3anyrra.WlnInet поддерживает базовую аутентификацию, аутентификацию Windows NT challenge/response и SSL (Secure Sockets Layer). Аутентификация объяснена в главе 35-
Доступ KWeb-прокси. Вы вводите информацию о прокси-сервере в Control Panel (апплет Internet), которая сохраняется в Реестре. Winlnet читает Реестр и использует проксисервер по необходимости.
Буферизованный ввод/вывод. Из Winlnet-функций чтения нет возврата до тех пор, пока не будет получено запрашиваемое Вами количество байтов. (Естественно, что в случае закрытия сокета со стороны сервера возврат будет немедленным.) Кроме того, Вы можете читать отдельные строки текста.
Простота. Для обновления пользовательского интерфейса и прерывания операций имеются функции обратного вызова. Кроме того, при помощи одного вызова CIntemetSes- sion::OpenURI,M.a»HO найти IP-адрес сервера, установить соединение и сделать файл доступным для чтения. Есть функции для копирования Интернет-файлов напрямую надиск.
I* Дружественность. Winlnet сам разбирает и форматирует заголовки. Если сервер переместил файл в новое место, он посылает новый URL в заголовке HTTP Location, и тогда Winlnet незаметно для Вас сам обращается к новому серверу. Кроме того, "Winlnet сам помещает в заголовок запроса дату модификации файла.
Winlnet-классы в MFC
Winlnet - это современный API, доступный только в Win32. Оболочка MFC вокруг Winlnet достаточно хороша, поэтому нам не надо писать свою библиотеку Winlnet-классов. MFC Winlnet поддерживает блокирующие вызовы в многопоточных программах, и этого нам на данный момент достаточно.
MFC-классы полностью соответствуют нижележащей архитектуре Winlnet, но с добавлением обработки исключений. Далее кратко описаны эти классы.
CinternetSession
Для каждого потока, предоставляющего доступ в Интернет, нужен только один объект Gintert - netSession. После создания такого объекта Бы можете установить соединения по протоколам HTTP, FTP и gopher или напрямую открыть удаленные файлы вызовом функции-члена Ореп- URL. Если нужны функции обратного вызова (status callback), то объявите производный класс от CinternetSession.'
Конструктор объекта CinternetSession вызывает Winlnet-функцию Internetopen, которая возвращает описатель сеанса (session handle) типа HINTERNET,сохраняемый внутри объекта. Эта функция инициализирует Winlnct-библиотеку для применения в вашем приложении, а описатель сеанса будет использоваться как параметр при остальных вызовах Winlnet.
CHttpConnection
Объект класса CHttpConnection представляет собой «постоянное» HTTP-соединение с конкретным хостом, Вы уже знаете, что HTTP и FTP не поддерживают постоянные соединения. (Соединение существует только на время передачи файла.) Winlnet предоставляет постоянное соединение, потому что помнит имя хоста.
После создания объекта CinternetSession Вы вызываете функцию-член GetHttpConnection, которая возвращает указатель на объект CHttpConnection, (Efe забудьте удалить его по завершении работы с ним.)
Функция-член СеШМрСоппесХготгвызыва&т ^irdnet-^HKvxHKdnternetConnect, которая возвращает описатель соединения (connection handle) типа HINTERNET, сохраняемый внутри CHttpConnectionsi используемый как параметр при дальнейших вызовах Winlnet.
CFtpConnectlon, CGopherConnectlon
Эти классы похожи на CHttpCormection^LO используют протоколы FTP и gopher соответственно. Функции-члены GetFile и PutFite позволяют передавать файлы напрямую на ваш диск и с вашего диска,
ClnternetRle■
При использовании HTTP, FTP или gopher клиентская программа читает и пишет потоки байтов. Winlnet-классы MFC делают их похожими на обычные файлы. Если взглянуть на иерархию классов, то видно, что ClnternetFile произведен от CStdioFile, который, в свою очередь, произведен от CFile. Следовательно, ClnternetFile и производные от него классы переопреде- ляютизвестные функции CFile,Hai тримср, .йеа^и Write. Для файлов FTP используется сам класс ClnternetFile, а для файлов HTTP и gopher применяются производные классы CHttpFtieH ССо- pberFile. Объекты ClnternetFile не создаются напрямую — Бы должны вызвать CFtpConnec- иоп::ОрепЕНедля получения указателя на ClnternetFile.
При использовании объекта CFile в нем есть 32-разрядная переменная-член типа HANDLE, представляющая сам дисковый файл. В ClnternetFile та же переменная-член mJbFileхранит 32-разрядный описатель Интернет-файла (Internet file handle) типа HINTERNET, не взаимозаменяемый с HANDLE. Переопределенные функции-члены класса ClnternetFile используют этот описатель для вызова Winlnct-функций InternetReadFtleH IntemetWrileFite.
CHttpFile
У этого класса Интернет-файлов есть уникальные для HTTP функции-члены: AddRequestHeaders, SendRequest и GelFileURL.Объекты CHttpFile не создаются напрямую — Вы должны вызвать функцию CHttpConnection::OpenRequest^aropaK вызывает Winlnet-функцию HttpOpenRe- ^uestH возвращаетуказатель на CHttpFile. При вызове можно указать POST- или GET-запрос.
Получив указатель на CHttpFile, Вы вызываете функцию-член CHttpFile: :SendRequesty£>TO- рая в реальности посылает запрос серверу. Затем Вы вызываете Read.
CFtpFiieFind, CGopherFHeFind.
Эти классы позволяют клиентской программе просматривать FTP- и gopher-каталоги.
ClnternetException.
Winlnet-классы MFC генерируют объекты ClnternetException^xyroptsie ваша программа может обрабатывать, используя try/catch.
Функции обратного вызова
Winlnet и MFC предоставляют доступные в синхронном и в асинхронном режимах функции обратного вызова для уведомления о состоянии Winlnet-операций. В синхронном режиме (только он здесь и используется) вызов Winlnet блокируется, даже если включена возможность обратного вызова.•
В C++ функции обратного вызова реализуются легко — Вы просто создаете производный класс и переопределяете виртуальные функции. Баз овымклассомдля WinlnetaBnaeica C/nter- netSession. Давайте создадим производный класс CCallbacklntemetSession:
class CCallbacklntemetSession : public ClntornetSession {
public:
CCallbackInternetSesslon( LPCTSTR pstrAgent = NULL,
DWORD nwContsxt = 1,
см. след. стр.
DWORD dwAccessType = PRE_CONFIG_INTERNET_ACCESS,
LPCTSTR pstrFroxyName = NULL, LPCTSTR pstrProxyBypass = NULL,
DWORD dwFlags = 0 ) { EnableStatusCalloackO }'
protected;
virtual void OnStatusCallback(DWORD dwCoetext,
DWORD dwInternalStatys, LPVOID IpvStatLSlnformatlon, DWORD dwStatusInformationLength);
Необходимо ввести код только для конструктора и одной переопределенной функции OnStatusCallback. Конструктор вызывает QnternetSession::EnableStatusCallback,4To6H. ВКЛЮЧИТЬ возможности обратного вызова. Клиентская программа будет делать различные блокирующие вызовы Winlnet, и при каждом изменении состояния будет вызываться OnStatusCallback. Наша переопределенная функция быстро обновляет пользовательский интерфейс л завер - шается, после чего операция продолжается. Для HTTP большинство обратных вызовов порождаются в функции CHttpFile.-.SendRequest.
Какие события приводят к генерации обратных вызовов? Ниже приведен список кодов, передаваемых как значение параметра dwIntemalStatus.
Код '' 'INTERNET-STATUS-RESOLVING-NAMEКод '' 'INTERNET-STATUS-RESOLVING-NAMEINTERNET „STATUS_NAME_RESOLVEDINTERNET „STATUS_NAME_RESOLVEDДействие
Код '' '
INTERNET-STATUS-RESOLVING-NAME
Код '' '
INTERNET-STATUS-RESOLVING-NAME
INTERNET „STATUS_NAME_RESOLVED
INTERNET „STATUS_NAME_RESOLVED
Поиск IP-адреса по заданному имени. Имя передано в параметре IptStatuslnponnation
IP-адрес найден. Он передан в параметре
IpiStatusInformatton.
lNTERNET_SrATUS_CONNECTINC_TO_SERVER Идет соединение с сокетом
INTERNET_STATUS_CONNECTED_TO_SERVER INTERNET_STAWSjENDINGjtEQU£ST INTERNET „STATUS„REQUEST.SENT INTERNEr_STATUSJRECEMNG_RESPONSE INTERNET„STATUS„RESPONSE_RECEIVED iNTERNET^STATUSjCKlSlNG^ONNECriON INCERNET-STATL'S„CONNECTION„CLOSED INTERNET_STATUS„HANDLE_CREATED INTERNET_STATOS_HANDLE_CLOSING LNTERNET STATUS REQUEST COMPLETEINTERNET_STATUS_CONNECTED_TO_SERVER INTERNET_STAWSjENDINGjtEQU£ST INTERNET „STATUS„REQUEST.SENT INTERNEr_STATUSJRECEMNG_RESPONSE INTERNET„STATUS„RESPONSE_RECEIVED iNTERNET^STATUSjCKlSlNG^ONNECriON INCERNET-STATL'S„CONNECTION„CLOSED INTERNET_STATUS„HANDLE_CREATED INTERNET_STATOS_HANDLE_CLOSING LNTERNET STATUS REQUEST COMPLETEУспешно выполнено соединение с сокетом
INTERNET_STATUS_CONNECTED_TO_SERVER INTERNET_STAWSjENDINGjtEQU£ST INTERNET „STATUS„REQUEST.SENT INTERNEr_STATUSJRECEMNG_RESPONSE INTERNET„STATUS„RESPONSE_RECEIVED iNTERNET^STATUSjCKlSlNG^ONNECriON INCERNET-STATL'S„CONNECTION„CLOSED INTERNET_STATUS„HANDLE_CREATED INTERNET_STATOS_HANDLE_CLOSING LNTERNET STATUS REQUEST COMPLETE
INTERNET_STATUS_CONNECTED_TO_SERVER INTERNET_STAWSjENDINGjtEQU£ST INTERNET „STATUS„REQUEST.SENT INTERNEr_STATUSJRECEMNG_RESPONSE INTERNET„STATUS„RESPONSE_RECEIVED iNTERNET^STATUSjCKlSlNG^ONNECriON INCERNET-STATL'S„CONNECTION„CLOSED INTERNET_STATUS„HANDLE_CREATED INTERNET_STATOS_HANDLE_CLOSING LNTERNET STATUS REQUEST COMPLETE
Идет отправление запроса информации на сервер
Успешно отправлен запрос информации на сервер Идет ожидание ответа сервера на запрос
Успешно получен ответ сервера на запрос Идет закрытие соединения с серверам Успешно закрыто соединение с сервером
Теперь программа мажет закрыть описатель
Успешно уничтожен этот описатель
Успешно завершена асинхронная операция
Функцию обратного вызова можно использовать для прерывания Winlnet-операции. Вы можете, например, проверять состояние объекта-события, которое устанавливается г основном потоке программы при прерывании операции пользователем.
Упрощенный Winlnet-клиент
Теперь рассмотрим Winlnet-эквивалент нашего Winsock-клиента, реализующего слепой GET- запрос. Поскольку Winlnet используется в блокирующем режиме, необходимо поместить код в рабочий поток, который запускается в обработчике команды в основном потоке:
AfxBeginThread(ClienrlVlnInetThreadProc, GetSa'eHwndO);•
Вот код клиентского потока:
CString g_strServerName = "localhost"; Ц или другое имя хоста
UINT ClientWinInetThreadProc(LPVOID pParam)-
{
ClrternetSession session;
CHttpConnection*- pConnection = NULL;
CHttpFile* pFilel = NULL;
Char* buffer - new char[MAXBL'F]:
DINT nBytesRead = 0;
try {
pConnection = session.GetHttpConnection(g_strServerName, 80);
pFilel = pConnection->OperRequest(1, '7'); // слепой GET
pFile1->SendRequest();
nBytesRead = pFile1->Read(buffer, MAXBUF - 1);
buffer[nBytesRead] = '\0‘; // необходимо для окна сообщения char temp(ТО];
lf(pFilel->Read(temp, 10) !=0) {
// если чтение завершено, то кэш работает
AfxMessagcBoxC'File overran buffer - not cached");
}
AfxMessageBox(buffer);
}
catch(CInternetExcepticn* e) {
// Зарегистрировать исключение
e->DeIete();.
if(pFilel) delete pFilel;
if (pConnection) delete pConnection;
delete [] buffer;
return 0;
}
Второй вызов Read требует пояснений. Он преследует две цели. Если при первом вызове Read не прочитан весь файл, эго означает, что он больше MAXBUF-LВторой вызов Read позволяет получить несколько байтов и понять причину переполнения. Если же при первом вызове Read прочитан весь файл, то второй вызов нужен для того, чтобы Winlnet закэширо- вал файл на вашем жестком диске, Помните, что Winlnet пытается прочитать все запрошенные Вами байты, вплоть до конца файла, Поэтому Вам нужно прочитать О байт после этого, Создание Web-клиента при помощи Winlnet-классов MFC Существуетдва способа создания Web-клиента при помощи Winlnet. Первый, использующий класс CHttpConnection, похож на Winlnet-клиент с предыдущей страницы. Второй, основанный на применении ClnternetSessionccOperdJRLfw^ проще. Мы начнем с СНЯрСоппесйоп-версии.
Winlnet-клиент №1
Реализация Winlnet-клиента в программе ЕХ34А находится в файле \vcpp32\ex34a\ClientInet- Thread.cpp на прилагаемом компакт-диске. Помимо возможности использования IP-адреса наряду с именем хоста в программе также реализована функция обратного вызова. Эта функция — CCallbacklnternetSession-OnStatusCaUbackB файле \vcpp32\ex34a\utility.cpp — помещает текстовую строку в глобальную переменную я pchStatus, используя критическую секцию для синхронизации. Затем функция отправляет (post) пользовательское сообщение в основное окно приложения. Сообщение включает обработчик обновления пользовательского интерфейса, вызываемый в CWinApp::OnIdle.9iai обработчик выводит текст во второй текстовой панели строки состояния.
Тестирование Winlnet-клиента №1
Тестирование этого клиента можно провести так же, как и Winsock-клиента. Обратите внимание на сообщения в строке состояния по мере установления соединения. Обратите внимание также, что файл при повторном запросе появляется быстрее.
Winlnet-клиент №2
Программа ЕХ34А реализует другой Winlnet-клиент в файле ClientUrlThread.cpp на прилагаемом компакт-диске. Этот клиент использует поле вывода «Address» (в котором вводится URL для доступа к узлу Интернета). Вот его кед:
CString g_strURL = "http:// ";
UlNT ClientUrlThreadProc(LPVOID pParam)
char* buffer = new char[MAXBUFJ;
UINT nSytesRead = 0;
ClnxernetSession session; //не предоставляет функций обратного вызова для OpenURL
CStdioFUe'* pFilel = NULL; // можно вызывать fieedString для получения 1 строки try {
pFilel = session.OpehURL(g_strURL, О,
INTERNET_FLAG_TRANSFER_BINARY IINTERNET_FLAG_KEEP_CONNECTION) ;
// Если вызов OpenURL будет неудачным, мы сода не доберемся
nBytesRead = pFile1->Read(buffer, MAXBUF - 1);
buffer[nBytesReadJ = '\0'; // необходимо для окна сообщения
char temp[100];
if(pFilQl->Read(temp, 100) 1 = 0) {
// если чтение завершено, то кэш работает
AfxMessageBox("File overran buffer - not cacned");
}
::MessageBox(::GetTopWindow(::GetDesktopWindow()). buffer, "URL CLIENT", MB OK);
}■
catch(CInternetException« e) {
LogInternetExceptldn(pParam, e);
e->Delete();
}
if(pFilel) delete pFilel;
delete [] buffer;
return 0;
J
Обратите внимание: OpenURL возвращает указатель на объект CStdioFile. Этот указатель можно использовать для вызова Read, как показано здесь, или вызвать ReadString для получения одной строки. Каки в предыдущем Winlnet-клиенте, необходимо второй раз вызвать Read для кэширования файла. Параметр INTERNETJF1AG_KBEP_CONNECTIO6Fвызове OpenURL необходим для аутентификации Windows NT challenge/iesponse, описываемой в главе 35. Если добавить флажок INTERNET FIAG_RELOAD,to программа будет обходить кэш точно так же, как это делает браузер при щелчке кнопки Refresh.
Тестирование Wlnlnet-клиента №2
Вы мажете протестировать этот Winlnet-клиент с любым сервером HTTP. Запустите его, введя URL, а не выбрав элемент меню. В URL должен быть указан протокол (http-.// или ftp://). Например, введите bttp://locaIfaostBbi должны увидеть то же самый HTML-код в окне сообщения. Сообщения о состоянии не появляются, потому что функции обратного вызова не работают с OpenURL.
Асинхронные файловые моникеры'
Только Вы подумали, что знаете все способы загрузки файлов из Интернета, как Вам предлагают изучить еще один — при помощи асинхронных файловых моникеров Вы можете запрограммировать все в основном потоке приложения без блокирования пользовательского, интерфейса. Звучит волшебно, не так ли? Волшебство заключено внутри URLMON.DLL, кото-
рая зависит от Winlnet и применяется в Microsoft Internet Explorer. Класс CAsyncMonikerFUe в MFC облегчает программирование, но сначала нужно освоить немного теории.
Моникеры
Моникер — это «суррогатный» COM-объект, хранящий имя (URL) «реального» объекта, который может быть встроенным компонентом или, гораздо чаще, файлом Интернета (HTML, JPEG, GIF и т. п.). Моникеры реализуют интерфейс 1Моткег,ъ котором есть две важные функции BindToObjecta BintFTbStorage. Первая из иих переводит объект в выполняемое состояние, а вторая возвращает указатель на [Stream или [Storage, по которому можно прочитать данные объекта. С моникером связан интерфейс IBindStotusCallback, в котором есть такие функции-члены, как OnStartBindinga ОмЛяГДДуаЯяй/у, вызываемые в процессе чтения данных по заданному URL.
Функции обратного вызова выполняются в потоке, создавшем моникер. Это означает, что URLMON.DLL должна создать невидимое окно в вызвавшем ее потоке и посылать в него сообщения из другого потока, который иcпoльзyeтWinlnet'фyнкциидля чтения URL. Обработчики окопных сообщений вызывают функции обратного вызова.
Класс CAsyncMonikerFUe в MFC
К счастью, MFC может скрыть от Бас вышеописанные интерфейсы СОМ. Класс CAsyncMoni- kerFileявляется производным от CFile, поэтому он ведет себя подобно обычному файлу. Вместо открытия дискового файла функция-член Open этого класса получает указатель на IMoni- кег и сохраняет указатель на интерфейс [Stream, возвращенный при вызове BindToStoragg. Крометого, в классе есть виртуальные функции, связанные с функциями-членами [BindStatus- CaUback. Использовать этот класс очень просто: Вы создаете объект и вызываете функцию Open, которая сразу завершается. Затем Вы ждете вызовов переопределенных виртуальных функций, неслучайно названных так же, как и их эквиваленты в iBindStatusCallback.
Использование класса CAsyncMonikerFHeB программе
Предположим, ваше приложение загружает данные из дюжины URL, но имеет только один класс, производный от CAsyncMonikerFUe. Переопределенные функции обратного вызова должны знать, куда записывать данные, Это означает, что Вы должны ассоциировать каждый объект производного класса с некоторым элементом пользовательского интерфейса своей программы. Следующая последовательность действий иллюстрирует один из путей достижения этого. Предположим, Вы хотите вывести текст HTML-файла в поле ввода, которое является частью окна формы. Вот что можно сделать.
Используя ClassWizard, создайте класс, производный от CAsyncMonikerFUe,
Добавьте переменмую-член т_Ьи$ег ти показатель на символ. Вызовите new в конструкторе для этого указателя и delete в деструкторе.
Добавьте открытую переменную-член m_edit типа CEdit.
-\ Переопределите функцию OnDataAvailableтаким образом:
void CMyMonikerFile::OnDataAvailable(DWORD dwSize, DWORD bscfFlag)
try {
HINT oBytesRead = Read {^buffer, kAXBUF - 1);
TRACE("nBylesAead = %d\n”, nBytesRead);.
m_buffer[n8ylesRead] = '\0’; // необходимо для элемента управления "поле ввода"
' ' Ц Следующие две строки добавляет текст в поле ввода ■ m_edlt.SendMessage(EM_SETSEL. (WPARAM) 999999, 1000000);
rr edit.SenoMessage(EM REPLACESEL, (WPARAM) 0. (LPARAM)г buffer):
catch(CFlleExceptlon* ре) {
ТАаСЕ("File exception %d\n, pe->ir._cause");
pe->Delete();
}
Добавьте в класс «вид* встроенный объект вашего нового класса файлового моникера.
В функции OnlnitialUpdateKnacca. «вид* подсоедините переменную типа GEditK элементу управления:
in_myEmbeddedMonikerFlle.m_edit.SubClassDlgIter!(ID_MYEDIT, this);
В том же классе «вид* откройте файловый моникер:
m_myEmbeddedMonikerFlle.Open('http://host/filename");.
Для большого файла OnDataAvailable будет вызвана несколько раз, при каждом из них добавляя текст в поле ввода Если в своем производном классе Вы переопределили OnProgress или OnStopBinding, то программа может получить предупреждение об окончании передачи файла Можно также для определения завершения передачи проверять флажок bscJFlagw OnDataAvailable. Обратите внимание: все происходит в основном потоке программы и - самое важное — объект файлового моникера должен существовать, пока выполняется передача Вот почему он является переменной-членом класса «вид*.
Асинхронные файловые моникеры или программирование с помощью Winlnet
В примерах использования Winlnet ранее в этой главе мы запускали рабочий поток, который делал блокирующие вызовы и посылал сообщение в основной поток при завершении. С асинхронными файловыми моникерами все также — передача данных происходит в друге м потоке, который посылает сообщения в основной поток. Вы просто не видите другой поток Однако есть одно очень важное различие между асинхронными файловыми моникерами и использованием Winlnet: при блокирующих вызовах Winlnet нужен отдельный поток для каждой передачи данных, а при использовании моникеров один дополнительный поток обрабатывает все передачи данных. Например, если браузер должен загрузить одновременно 50 растровых изображений, то применение моникеров экономит 49 потоков, что делает работу программы гораздо более эффективной.
С другой стороны, использование Winlnet предоставляет дополнительные возможности управления, да и легче получить информацию из заголовков ответа, например, полную длину файла. Следовательно, выбор программного средства зависит от вашего приложения. Чем больше Вы будете знать, тем лучше будет ваш выбор.
ГЛАВА 35
ПрограммированиедляМ1сгоэоЙ1Щегпе1 Information Server
В главе 34 мы использовали «самодельный» Web-сервер на базе API Winsock. В этой главе мы изучим, как применять и дополнять Microsoft US 4.0, встроенный в Windows NT Server 4.0. IIS в действительности состоит из трех отдельных серверов — один для HTTP (WWW), один для FTP и один для gopher. В этой главе мы объясним, как написать расширение HTTP-сервера, используя интерфейс программирования приложений Интернет-серверафВ application programming interface, ISAPI) - одну из частей технологии Microsoft ActiveX. Вы изучите два типа расширений, реализуемых как DLL ISAPI-расширение сервера и ISAPI-фильтр. ISAPI-pac- ширение сервера (ISAPI server extension) может выполнять деловые транзакции в Интернете, например, оплату счетов ISAPl-фидыпр (ISAPI filter) перехватывает данные, передаваемые на сервер и с сервера, и в силу этого может выполнять различные специализированные задачи.
Альтернатива IIS
В упражнениях этой главы предполагается, что у Вас есть Windows NT Server 4.0 и US. Если Вы работаете с Windows NT Workstation, то можете использовать Peer Web Services, который поддерживает меньше соединений и не позволяет создавать виртуальные серверы. Если же Вы работаете с Windows 95/9S, то используйте Personal Web Server, входящий в состав Microsoft Frontpage. Любой из указанных серверов подцерживает расширения ISAPI. Подробности см. в документации на сервер. . .
Microsoft IIS
Microsoft IIS — это высокопроизводительный сервер Интернета и интрасети, использующий такие возможности Windows NT, как порты завершения ввода/вывода, Win3 2-функцию Traris- mitFile,кэширование и масштабируемость по потокам кода.
Установка и управление
При установке Windows NT Server 4.0 Вы можете выбрать установку IIS. Если Вы это сделаете, Л8 будет запускаться при всяком запуске Windows NT. IIS является специальной Win32-npo- граммой под названием служба (service), точнее, тремя службами — WWW, FTP и gopher — в одной программе inetinfo.exe. Такие программы не выводятся на панель задач. Для управления US можно использовать апплет Services из Control Panel, но лучше применять программу Internet Service Manager.
Запуск Internet Service Manager
Запустить Internet Service Manager можно из подменю Microsoft Internet Server меню Start ПРИМЕЧАНИЕ Из браузера можно запустить HTML-версию Internet Service Manager. Эта версия позволяет изменить параметры служб, но не позволяет включить или отключить их,
На рисунке 35-1 показано окно Microsoft Internet Service Manager. Служба WWW работает, а служба FTP остановлена.
Для выбора службы щелкните ее значок слева. Кнопки с треугольником и квадратом предназначены для запуска и останова службы соответственно.
Рис. 35-1 - Окно Microsoft Internet Service Manager
Защита IIS
После двойного щелчка значка службы WWW в окне Microsoft Internet Service Manager Вы увидите окно свойств. Страница Service позволяет настраивать защиту Ж Когда клиентский браузер запрашивает файл, сервер олицетворяет (impersonates) локального пользователя во время запроса, а имя пользователя определяет, к каким файлам клиент имеет доступ. Какого же локального пользователя олицетворяет сервер? Чаще всего того, чье имя Вы увидите в поле ввода Username, как показано на рисунке 35-2.
Рис. 35-2. Окно свойств службы WWW
Большинство посетителей Web-страниц не указывают имя и пароль, поэтому рассматриваются как анонимные пользователи (anonymous users). Эти пользователи будут иметь те же
права, что и локально зарегистрированные под именем IUSR_MYMACHINENAME пользователи. Это означает, что имя IUSR_MYMACHINENAME должно быть в списке пользователей в программе User Manager или User Manager For Domains, выбираемых из меню Administrative Tools, и пароль должен совпадать. Обычно программа установки IIS задает такого административного пользователя. Вы мажете определить другое имя пользователя, но оно должно быть в списке пользователей компьютера (или домена Windows NT).
Обратите внимание также на параметр Password Authentication, Пока оставьте установленным только флажок Allow Anonymous, который означает, что все Web-пользователи будут регистрироваться как IUSR_MYMACHINENAME. Далее в этой главе мы рассмотрим Windows NT Challenge/Response.
Каталоги IIS
Помните Web-узел SlowSoft из главы 34? Если запросить URL со значением http://slowsoft.com/ newproducts.html, то файл newproducts.html будет взят из основного каталога (home directory) сервера slows oft com. У каждого сервера должен быть основной каталог, даже если в нем есть только подкаталоги. Но этот основной каталог не обязательно должен быть корневым каталогом компьютера. Как видно на рисунке 35-3, основной каталог службы WWW в действительности — \WebHome, поэтому клиенты считывают файл \WebHome\newproducts.html.
Рис. 35-3. Окно корневого каталога \WebHome
Хотя сервер может работать только с основным каталогам, но полезнее будет создать виртуальные каталоги (virtual directory) IIS. Предположим, SlowSoft хочет предоставить доступ ккаталогу \BF надиске D. На рисунке 35-3 показан виртуальный каталог /BugsFixed, отображенный на каталог D:\BF. В этом случае клиенты смогут обращаться к файлам в нем примерно так: http://slowsoft.com/BugsFixcd/filcl.html.
ПРИМЕЧАНИЕ Если у вашего компьютера есть несколько IP-адресов (см. Network в Control Panel), то IIS позволит создать виртуальные Web-серверы (virtual Web servers). У каждого виртуального сервера будет свой основной каталог (и виртуальные каталоги), связанный с конкретным IP-адресом. Таким образом, будет создана видимость нескольких серверных компьютеров. К сожалению, Web-сервер в IIIS использует все IP-адреса компьютера, поэтому невозможно одновременное ожидание запросов серверами US и ЕХ34А на одном порту 80.
В главе 34 сказано, что Web-браузер может выдать «слепой» запрос. Как видно на рисунке 35-3, Internet Explorer Manager позволяет определить файл, который будет выдан по этому запросу — обычно это Dcfauh,htm, Если на странице Directories окна свойств службы установлен флажок Directory Browsing Allowed, то вместо файла клиенты увидят гипертекстовый список файлов каталога сервера.
Регистрация соединений IIS
11S может выполнятьрегистпрацию (logging) всех соединений. Для управления регистрацией используйте страницу свойств Logging в Internet Service Manager. Вы можете задать регистрацию в текстовых файлах или в базе данных SQL/ODBC. Регистрационные записи состоят из даты, времени, IP-адреса клиента, запрашиваемого файла, строки запроса и т. д.
Теспфованле IIS
Вы можете легко протестировать HS, используя браузер или любого клиента из примера ЕХ34А. Просто убедитесь, что IIS запущен, а сервер ЕХ34А — нет. По умолчанию основным каталогом IIS является \\X4nnt\System32\inetsrv\wwwroot, в котором есть несколько HTML- файлов. Если Вы используете один компьютер, то наберите имя localhost; если же Вы в сети, то введите имя из файла Hosts, При невозможности удаленного доступа к серверу запустите ping и убедитесь, что сеть настроена правильно. Не пытайтесь собрать и применить ISAPI- библиотеки до того, как успешно протестировали IIS на своем компьютере.
ISAPI-расширения сервера
ISAPI-расширение сервера — это программа (реализованная как DLL, загружаемая IIS), которая выполняется в ответ на POST- или GET-запрос от клиентской программы (браузера). Браузер может передать программе параметры, которые обычно пользователь вводит в полях ввода, выбирает из списков и т. д. ISAPI-расширение сервера обычно на основе значений этих параметров посылает обратно HTML-код Вы лучше поймете этот процесс, увидев соответствующий пример.
CGI и ISAPI.
Первые серверные программы были разработаны для компьютеров под управлением UNIX, поэтому стандарты появились задолго до того, как Microsoft создала IIS. Стандарт CGI (Common Gateway Interface — общий шлюзовой интерфейс), являющийся частью HTTP, служил основой для взаимодействия программ-браузеров со сценариями и программами, выполняющимися на сервере. Не изменяя спецификации HTTP/CGI, Microsoft разработала US таким образом, чтобы позволить любому браузеру загружать и запускать серверные DLL Эти DLL являются частью процесса, в котором выполняется IIS, и поэтому выполняются быстрее, чем сценарии, для которых необходимо загрузить отдельную программу. Учтите также, что ваши знания скорее облегчат Вам написание ISAPI DLL на C++, чем сценария на PERL — стандартном языке сценариев для Web-серверов,
CGI переносит бремя программирования на сервер. Используя CGI-параметры, браузер посылает немного информации на компьютер-сервер, а сервер может делать с ней что угодно, в том числе обращаться к базе данных, генерировать изображения или управлять периферийными устройствами. Сервер посылает обратно браузеру файл (HTML или другой). Файл может быть прочитан с диска или сгенерирован программно. Не нужно никаких ActiveX- элементов или Java-апплетов, а браузер может работать на любом типе компьютеров.
Простой запрос GET к ISAPI-расширению. .
Предположим, что в файле HTML есть такой тэг:
<а href="scripts/maps. all?State=Idaho">Idaho Weather Map</axp>
Когда пользователь щелкает Idaho WeatbeiMap, браузер посылает серверу CGl-запррс GET примерно такого вида:.
GET scripts/maps. dll?Statc=Iqaho НТТР/1.0
Затем IIS загружает maps.dll из (виртуального) каталога scripts, вызывает стандартную функцию (default function), обычно называемую Default,т передает ей Idaho в тагаестве значения параметра State. Далее DLL генерирует JPG-файл, содержащий самую свежую спутниковую карту погоды для штата Айдахо, и передает его клиенту.
Если в maps.dll есть несколько функций, то тэг может содержать имя конкретной функции:
<а href = "scripts/maps. dll?GetMap?State=Idaho&Res=5">Idaho WeatherMap</aXp>
В данном случае вызывается GetMap с двумя параметрами State и Res. Скоро Вы узнаете, как написать ISAPI-расширение, подобное maps, dll, но сначала рассмотрим HTML-формы, поскольку без них CGI-запросы используются редко.
HTML-формы: G ЕТили POST?
В вышеприведенном простейшем CGI-запросе название штата было закодировано. Но почему бы не позволить пользователю выбрать штат из раскрывающегося списка? Для этого нужна форма, и ниже приведен пример одной из них:
<html>
<headxtitle>Weathermap HTML Form</title>
</head> '
<body>
<h1xcenter>Welcome to the Satellite Weathermap Service</centerx/h1>
<form action="scripts/maps.dll?GetMap" method=GET>
<p>Select your state:
Cselect name="State">
<option> Alabama
<option> Alaska
<option> Idaho
<optlon> Washington
</select>
<pxinput type=“sub(Hit"xinput type="reset">
</form>
</bodyx/html>.
В браузере этот файл будет выглядеть так, как показано на рисунке 35-4.
Рис. 35-4. Окно HTML-формы Weathermap
Тэг select позволяет получить имя штата из списка четырех штатов, а тэг input типа submit выводит кнопку, щелчок которой приводит к передаче данных формы на сервер в виде при; герно такого CGI-запроса:
। ЕТ scripts/maps.dll?GetMap?State=Idaho НТГР/1.0 'разные заголовки запроса)''
пустая строка)
К сожалению, некоторые ранние версии обозревателя Netscape опускали имя функции в > лЕТ-запросах, генерируемых формами. Обойти это можно было, либо реализовав в ISAPI DLL только стандартную функцию либо использовав в форме метод POST вместо GET.
Если Вам нужен метод POST, измените одно строку в вышеприведенном описании формы: xforrn action="scripts/maps. dll?GetMap" method=POST>
В этом случае обозреватель передает на сервер такой запрос:
’OST scripts/maps.dll?GetMap
(разные заголовки запроса)
(пустая строка) '....
State=Idaho"
Заметьте: значение параметра передается в последней строке, а не в строке запроса.
ПРИМЕЧАНИЕ Обычно все ISAPI DLL хранятся в отдельном виртуальном каталоге на сервере, так как к ним должен бшь доступ для исполнения (execute permission), но не для чтения (read permission). Щелчок кнопки Edit Properties, показанной на рисунке 35-3, или двойной щелчок каталога позволит Вам изменить параметры доступа.
Создание DLL ISAPI-расширения сервера
Visual C++ позволяет быстро создать ISAPI-расширение сервера. Для этого выберите из списка проектов ISAPI Extension Wizard. Щелкнув ОК, Вы увидите такое диалоговое окно:
Рис. 35-5. Шаг 1 мастера ISAPI Extension Wizard
Установите флажок Generate A Server Extension Object и Вы получите проект DLL с классам, производным от MFC-класса CHttpSemeryi функцией-членом Default. DLLhb будет ничего делать без небольшой порции программирования. ■
Функции ISAPI нужно писать как функции-члены класса, производного от CHnpSerrer, и использовать таблицу синтаксиса (parse map) для привязки этих функций к ITS, «Мастера
таблицы синтаксиса» нет, поэтому программировать придется вручную. Хорошо бы использовать имеющуюся функцию Dejault^ao нам нужна также функция GetMap Сначала вставьте следующие строки и созданную мастерам таблицу синтаксиса;
ON_PARSE_COMMAND(GetMap, CWeatherExtension, ITS_PSTR)
ON_PARSE_COMHAND„PARAMS("State")
затем напишите саму функцию GetMap-.
void CWeatherExtension: :GetMap(CHttpServerContext* pCtxt, LPCTSTR pstrState) (
Startcontent(pCtxt);
klrlteTitle( pCtxt):
"pCtxt « "Visualize a weather map for the state of ";
*pCtxt « pstrState;
EndContent(pCtxt);
}
Функция не генерирует карту погоды (а Вы чего ждали?), но выводит выбранное название штата в создаваемом HTML-файле. Функции CHttpSewenStartContena CHitpServer::EndContent формируют HTML-шаблон, a CHttpSeruer:;WrtteTiltaasss$B2^ виртуальную функцию CHrtpSer- ver::GetTitle, которую Вам нужно переопределить:
LPCTSTR CWeatherExtension::GctTitie() const
I.
return "Your custd’iweather map"; // для заголовка окна браузера
В MFC-классе CHttpServerContext есть переопределенный оператор «, который применя- етсядляразмещения текстов создаваемом HTML- файле. Невидимый объект MFC-класса CHtml- Stream представляет собой выходной поток сокета на стороне сервера.
MFC-классы для ISAPI-расширениясервера
Пришло время рассмотреть три MFC-класса, используемые для создания ISAPI-расширения сервера. Помните, что они предназначены только для расширений. Даже не думайте использовать их в обычном Winsock- или Winlnet-приложении.
CHttpServer
Используя мастер 1SAPI Extension WizardipiH каждой DLL ISAPI-расширения, Вы создаете производный от CHttpServer класс. Для каждой функции расширения нужна одна функция-член, включая стандартную функцию, а также необходимо переопред елить функцию GetTitle, Каркас приложения вызывает функции расширения в ответ на клиентские запросы, используя связи, заданные в таблице синтаксиса. Мастер ISAPI Extension Wizard создаетпереоиределен- ную функцию GetExlensionVersion, которую можно отредактировать, если нужно выполнить кодинициализ ациипризагрузке DLL
Одной из полезных функций-членов CHttpServer является AddHeader, которая добавляет в ответ специальные заголовки, например, Set-Cookie.
CHttpServerContext
На каждую DLL есть только один объект CHttpServer, но объект CHttpServerContext создается на каждый запрос транзакции к серверу. Каждая функция расширения получает указатель на такой объект. Производный класс от CHttpServerContext не создается, поэтому нельзя на его основе создать отдельные переменные для каждой транзакции. Поскольку транзакции происходят в разных потоках IIS, Вы должны позаботиться о синхронизации доступа к переменным-членам производного класса CHttpServer а к глобальным переменным.,
Вы уже видели использование функций StarlContent, EndContent и 1Т>7^ГлУ<?класса СНпр- Servem переопределенного оператора ». Полезным будет также вызов функции CHttpServer- «'ontext-GetServetVariable,предаазначенной для чтения информации из заголовков запроса.
CHtmlStream
Скорее всего, Вам не придется использовать этот класс напрямую. В классе CHttpServerContext есть переменная-член т_pStream класса CHtmlStream, которая «перехватывает» оператор >> л служит получателем HTML-данных. Если нужно послать клиенту двоичные данные, то вызовите функцию-член Write этого класса. Так как объекты класса CHtmlStream аккумулируют байты в памяти перед отправкой их клиенту, то Вам потребуется другой подход при переда- те больших файлов прямо с диска.
Пример ЕХ35А"
Сервер карты погоды недостаточно интересен для включения его в реальный проект. Возможно, пример ЕХ35А придется Вам больше по вкусу. Эго настоящее коммерческое Интер- нег-приложение, реализующее систему заказов пиццы. Вообразите себе управляемую компьютером духовку и руку робота, выбирающую замороженные пиццы. (Предполагается, что Microsoft Internet Explorer версии 17.0 будет способен доставлять горячую пиццу прямо из клиентского монитора, но в настоящее время Вам придется нанять поставщиков.)
Первый шаг: получение заказа
Молодые стажеры на распродажах постоянно озабочены «получением заказа». Эго совершенно необходимо в любой форме бизнеса, в том числе в Интернете. Когда голодный клиент переходит на ваш узел (естественно, щелкнув картинку с пиццей), он или она просто загружает примерно такой HTML-файл:
<html>
<headxtitle>Inside Visual C++ HTML Form l</title>
</head>
<body>
<Mxcenter>Welcome to cyberPizza</centerx/hl>
<p> Enter your order.
<form action="scripts/ex35a.dll?ProcessPizzarorrr method=POST>
<p> Your Names cinput type="text" пате="пате" value="">"
<p> Your Address: cinput type="text" name="address" value="">
<p> Number of Hess cinput type="text" name="quantity" value=l>
<p>Pizza Sizes
<menu>
clixinput type="radio" name="size" value=8>8-inch.
clixinput type="radio" name-'size" valje=10»10-inch
clixinput type="radio" name="size" value=12 checked>l2-inch
clixinput type="radio" name="size“ value-14>14-inch
</menu>
cp> Toppings;
<p>
cinput type="checkbox" name="topr value-"Peoperoni" checked> Pepperoni
cinput type="checkbox" name="top2" value="Onions"> Onions
cinput type="cheokbox" name="top3" value="Mushrooms"> Mushrooms
cinput type="checkbox" name="top4" value="Sausage"> Sausage
■<P>
<em>(you can select multiple topplngs)</em>
cpxinput type="submit" value="Submit Order Now’xinput type="reset">
</form>
</body></html>
На рисунке 35-6 показано, в каком виде форма заказа будет в браузере.
Рис. 35-6. Формазаказа CyberPizza
Пока ISA И DLL не вызывается. Действие начинается, когда клиент щелкает кнопку Submit Order Now (Сделать заказ). Вог что получит сервер:
POST scripts/ex35a.dll'?ProcessPizzaFor^ НТТР/1.0
(заголовки запроса)
(пустая строка)
na.Te=Walter+Sullivan&address=Redmon<ffi2C+WA&quantity=2&slze=12 &top1=Pepperoni &top3=Mushrooms
Похоже, что Уолтер Салливан (Walter Sullivan) заказал две 12-дюймовые пиццы, одну с пеперони1 и одну с грибами. Обозреватель вставляет знак »+» вместо пробела, %2С вместо' запятой и & в качестве разделителя параметров. Теперь взглянем на таблицу синтаксиса в ех35асрр:.
ON PARSE_COMMANO(ProcessPizzaForm, CEx35aExtension,
~!TS_PSTR ITS_PSTR ITS_I4 ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR)
0N_ PARSE_.COM MANO_PARAMS(" name address quantity size top1=~ top2=~ top3=~ top4=~")
Функция ProcessPizzaForn® DLL считывает значения параметров и создает форму подтверждения, которую отсылает клиенту. Вот часть кода функции:
-pCtxt « vform action=V'ex35a.dll?ConflrmOrderV method-POST>";
•pCtxt « "<p><input typesViiddenV name=Vname\" value=\“";
*pCtxt « pstrNama « "\">"; // ссылка на исходный заказ
«pCtxt « "<pxlnput Type=V'submit.V value=\ 'Confirm and charge my credit card\">";
-pCtxt « "</form>";
// Сохраняем этот заказ в файле на диске или в базе данных, ссылаясь на него по имени клиента
else {•
«pCtxt « ’’You forgot to enter name or address. Back up and try again. "; }
EndCon ten t( pCtxt);
Итальянская колбаса — Прши.перев.
^Необязательные п.араЦетръ|.. „Д у. :<г . •.;. - :,_ /И ^-Ц'1. ч ^' ? 7.L;
При написании операторцвтДблицы синтаксиса Вы До^^ погашать да
рые браузер использует для^ передачи значений параметров* |Гзд&в ф^рМе; Для 'i а формы из примера ЕХ35Й браузервсегда ПередаетПараметрыдотекстовых Полей, даже . . если пользователь не ввел в них никаких данных.'.Екуп^нтрПк^рполы^не за-, '
■ ^полнил поле Name, то браузер отпр^ит строку й<&?»е”№ ^■,
| ‘ми -4 браузер шлет значение флажка только если онЙоЬ^ ’ ~
Ж *Ж *ассоциированные с флажкамй,'называются «вс&ейй^ф^^ J ;" -< *7
Ж *
Ж *
• ti ' ,Есай бы макрос таблицы'уинтаксиса выглядел так V: Ь>*хл£»1к' ^ ’ -^ ’ ' 4
^$^ибММАад^^ЙЖ$^Я|^ з ;^
то возникли бы проблемы^, случае, если пользователь не|отметил бы все варианты пиццы. ШТТ^зэпрос завершился бы неудачно и пркуц^^с!^^^ Другой Web* ‘
■■ тузед, где продают плоду, £^^ в коде ей35а,ср]| "||б!|В^|^
И’ параметра как необязательные, со Значением по умолчадазо)^ Если флажок устанОв-- лен, то форма передастзначенйе; в ином случае — символ ^^^^^наваецы^ DlL-Heo- » ' бИЙйтелънйё параметрыдт^гыб^ьпереодсл^й^^йс^^дд^Д^ ■г '^»' "' * ,1 4 ,
На рисунке 35-7 показано полученное окно браузера.
Рис. 35-7. Окно подтверждения заказа пиццыРис. 35-7. Окно подтверждения заказа пиццы
Рис. 35-7. Окно подтверждения заказа пиццы
Рис. 35-7. Окно подтверждения заказа пиццы
Как видите, мы быстренько подсчитали цену. Для подтверждения клиент щелкает кнопку Confirm And Charge Му Credit Card (Подтверждаю и оплачиваю кредитной картой).
Второй шаг: обработка подтверждения
Когда пользователь щелкает кнопку Confirm And Charge Му Credit Card, обозреватель посылает на сервер второй POST-запрос, задавая вызов функции CEx35«Extension:.:ConJinnOrder. Но теперь перед нами встает одна большая проблема. Каждое HTTP-соединение (запрос/от- вет) независимо от других. Как связать запрос на подтверждение с исходным заказом? Хотя есть разные способы сделать эго, наиболее часто используется передача некоего текста вме- 'сте с подтверждением в скрытом тэге ввода (hidden input tag). При получении параметров подтверждения сервер использует скрытый текст для увязки подтверждения с заказом, который хранится где-то на его жестком диске.
В примере ЕХ35А в скрытом поле используется имя клиента, хотя безопаснее применить комбинацию имени, даты и времени. Вог НТМЬкод, который CEx35«Extension::ProcessPizza- Form передает клиенту как часть формы подтверждения:
<1прит type^'diaden" name=’‘name" value="Walter Sullivan">
а вот код функции CEx35«Extension::ConJirmOrder.
void CEx3EaExtens1 or::ConfirmOrder(CHttpServerContext* pCtxt, LPCTSTR pstrName)
StartCcntent(pCtxt);
WriteTitle(pCtxt):
«pCtxt « "<p>0ur courteous delivery person will arrive within 30 minutes. ";
♦pCtxt « "<p>Thankyou, " « pstrName « ", for using CyberPizza. ";
// Теперь aaipysUH заказ с диска по имени клиента и сделаем пиццу.
// Подготовимся к удалению заказа через некоторое время если клиент его не подтвердит.
m_cs.Lock(); // будем по-ркобезопасными
long int niotal = ++r_nTotalPizzaOrders;■
m_cs. Unlock;);
♦pCtxt « "<p>Total pizza orders = " « nTotal;
EndContent(pCtxt);'
}
Имя клиента возвращается в параметреpstrName и используется для загрузки с диска исходного заказа. Функция также увеличивает счетчик общего количества заказов, используя критическую секцию (т_с$)для синхронизации потоков.
Сборка и тестирование ex35a.dll
Если Вы скопировали код с прилагаемого компакт-диска, то проект будетв каталоге \vcpp32\ ех35а. При сборке проекта DLL будет помещена в подкаталог Debug. Скопируйте ее в каталог, доступный серверу, и туда же скопируйте PizzaForm.html. Можно использовать подкаталоги scripts и wwwroot каталога \Winnt\System32\inetsrvnnH создать новые виртуальные каталоги.
ПРИМЕЧАНИЕ Если Вы сделаете изменения в ЕХЭ5А DLL, то используйте Internet Service Manager (см. рис. 35-1} для останова службы WWW (поскольку старая DLL загружена в процесс). Затем скопируйте новую DLL в каталог scripts и снова запустите службу WWW. Новая DLL будет загружена при первом запросе со стороны клиента."
Если все установлено правильно, Вы сможете загрузить форму PizzaForm.html в браузер и заказать пиццу. Приятного аппетита!
Отладка ЕХ35А DLL
То, что IIS является службой Windows NT, затрудняет отладку ISAPI-библ иотек. Службы обычно выполняются как часть операционной системы, управляемые диспетчером базы данных служб (service manager database). У них свои оконная станция (window station) и невидимый рабочий стол (hidden desktop). Эго весьма «мрачные» стороны Windows NT, по которым нет достаточной информации.
Тем не менее, вот что можно сделать для отладки ЕХ35А DLL (и любой другой ISAPI DLL).
Остановите все службы IIS, используя Internet Service Manager.
Выберите команду Settings из меню Build и в диалоговом окне Project Settings введите следующие данные (см. верхний рисунок на стр. 744).
Запустите User Manager или User Manager for Domains (из меню Administrative Tools). Выберите команду User Rights из меню Policies, установите флажок Show Advance d User Right,
выберите привилегию Act As Part OfThe Operating System и добавьте вашу пользовательскую группу, как показано на нижнем рисунке.
Повторите действия на этапе 3 для привилегии Generate Security Audits.
Заново зарегистрируйтесь в Windows NT для активизации новых привилегий. (Не забудьте этот этап.)
Убедитесь, что текущий файл ЕХ35А DLL скопирован в каталог scripts.
Начните отладку. Вы можете задавать точки останова, проходить код по шагам и видеть вывод сообщений макросов TRACE.
Доступ к базам данных из ISAPI-расширения
ISAPI-расширепие сервера может использовать ODBC для доступах базе данных SQL Однако прежде, чем писать страницы с ODBC-кодом, изучите Internet Database Connector (IDC) в документации на IIS. IDC — это DLL ("Rttpodhc.dll), которая собирает параметры запроса SQL и форматирует результат запроса. Вы контролируете процесс, создавая файл IDC, описывающий источник данных, и НТХ-файл, являющийся,шаблоном результирующего HTML-файла. Не нужно никакого программирования па C++.
IDC предназначен только для запросов. Если же необходимо обновлять базу данных, то напишите свое ISAPI-распгирение сервера, вызывающее ODBC. Убедитесь, что ODBC-драйвер поддерживает многопоточность, как, например, последний драйвер сервера SQL
Использование подсказок
Вы съели пиццу, и теперь прилито Время десерта. Однако подсказки (cookies), рассматриваемые в этом разделе, не созданы бисквитными микросхемами1, Они применяются для сохранения информации на жестком диске клиента, В примере ЕХ35А сервер хранил имя клиента в скрытом поле формы подтверждения. Это сработает для привязки подтверждения к заказу, но не поможет узнать, сколько пицц Уолтер заказал в этом году. Если бы Вы знали, что он постоянно заказывает пиццу с пеперони, то при избытке пеперони Вы могли бы послать ему письмо по электронной почте.
Работа подсказок
Используя подсказки при получении первого заказа от Уолтера, Вы присваиваете ему идентификатор клиента, который сохраняете на его компьютере. Сервер делает эго. посылая такой заголовок ответа;
Set-Cookie; customer_id=12345: path=/; expires=Monday, 02-Sep-99 00:00:00 GMT
Строка customer_id— это присвоенное Вами имя подсказки; значение /для параметрарЩй показывает, что браузер вышлет значение подсказки при любом запросе к вашему узлу (СуЬег- Pizza.com); параметр expires, задающий дату окончания действия подсказки, необходим обозревателю для ее сохранения.
Когда браузер видит заголовок ответа Set-Cookie, он создает (или изменяет) следующий элемент в своем файле cookies.txt:
customer_id
12345
cyberpizza.com/
0
2096697344
0
2093550622
35
Когда же браузер в дальнейшем запросит что-нибудь с CyberPizza.com, он поместит в запрос такой заголовок:
Cookie: customer_id=T2345
Обработка подсказок lSAPl-расширением сервера
Для сохранения подсказок на стороне браузера ISAPI-расширение сервера делает такой вызов: AddHeader(pCtxt, "Set-Cookie: session_id=12345; path-/;"
■'expires=Honday, 02-Sep-99 00:00:00 GMT\r\n");
Для получения значения подсказки в другой функции используется такой код -;
char strCookles[200];.
CWORD dwLength = 200;
ipCtxt->GetServerVariable("HTTP_COOKIE", strCookies, &dwlength):
Теперь в переменной strCookie содержится текст customerjd^12345-.1
Проблемы при использовании подсказок'
Не так давно было много шума, когда пользователи Интернета впервые обнаружили, что компании сохраняют какие-то данные на их персональных компьютерах. Новые версии браузе-
Игра слов. Cookies по-английски *печенье». — Прим.перев.
ров, прежде чем сохранить подсказку, запрашивают разрешение у пользователя. Клиенты могут отказаться от сохранения подсказки или могут удалить файл cookics.txt; файл может " аюке переполниться. Если Вы решите использовать подсказки на своем Web-узле, примите 1 о внимание эти особенности.
Аутентификация
До этого момента ваш ПЗ был настроен на разрешение анонимного входа, т. е. любой человек в мире мог получить доступ к вашему серверу, не указывая имя и пароль. Все пользователи регистрировались как TUSR_MYMACHINENAME и имели доступ к любому файлу, на который были права у этого имени.
ПРИМЕЧАНИЕ Как отмечалось в главе 34, для максимальной защиты своего сервера Вы должны использовать NTFS.
Базовая аутентификация.
Простейший способ ограничить доступ к серверу — использовать базовую аутентификацию (basic authentication). В этом случае, если клиент делает анонимный запрос, сервер от- •:ылает обратно ответ:
1ТТР/1.0 401 Unauthorized
вместе с примерно таким заголовкам:
«MW-Authenticate: Basic realm="xxxx"
Клиент запрашивает у пользователя имя и пароль и заново шлет запрос примерно с таким заголовком:
Authorization: Basic 2rc2341dfd8kdr
Строка после Basic — это псевдошифрованная версия имени и пароля, которую сервер расшифровывает и использует для олицетворения клиента.
Проблема базовой аутентификации состоит в том, что взломщики могут перехватить имя и пароль пользователя и использовать их для получения доступа к вашему серверу. ПЗ и большинство браузеров поддерживают базовую аутентификацию, но она не очень эффективна..
Аутентификация Windows NT Challenge/Response
Аутентификация Windows NT challenge/response (вызов/ответ) часто используется в интрасетях, работающих на основе сетей Microsoft, но ее можно применить и в Интернете. ПЗ поддерживает ее (см. рис. 35-2), чего нельзя сказать обо всех браузерах.
Если у сервера включена аутентификация вызов/ответ, то клиент, сделавший обычный запрос, получит такой заголовок ответа:
WW-Authenticate: NTLM
Authorization: NTLM T1RMTVNTUAABAAAAA5IAA ...
Строка после NTLM — это хорошо зашифрованное имя пользователя. Пароль по сети не пересылается вообще. Сервер выполняет вызов с примерно таким заголовком ответа: WWW-Authenticate: NTLM RPTUFJTgAAAAAA ...
Клиент, знающий пароль, выполняет некоторую математическую операцию над кодом вызова и паролем и высылает обратно ответ с таким примерно заголовком:
Authorization: NTLM AgACAAgAAAAAAAAAA ...
Сервер, получивший у себя пароль на основе имени пользователя, выполняет ту же операцию над паролем и кодом вызова и сравнивает результат с тем, что передано браузером. Если результаты совпали, то сервер выполняет клиентский запрос, олицетворяя клиента, и отсылает ему запрошенные данные.
Когда клиент снова передает запрос, диалог «вызов-ответ* выполняется по односокетно- му соединению с запретом закрытия (keep-alive) в соответствии с заголовком Connection клиентского запроса.
Winlnet поддерживает аутентификацию Windows NT challenge/response.. Следовательно, ее поддерживают как Internet Explorer 4.0, так и клиенты из примера ЕХ34А. Если клиентский компьютер входит в домен Windows NT, то для имени пользователя и пароля используется сквозная защита (pass-through). Если клиент находится в Интернете, то Winlnet запросит имя и пароль. При написании кода Winlnet Вы должны во всех вызовах CHttpConnection::Open- Request и C№ernetSessi6n::0penURL использовать флажок 1NTERNETJFLAG KEEPCONNECTION, как это сд елано в ЕХ34А..■.
Уровень защищенных сокетов'"
Аутентификация Windows NT challenge/response контролирует только регистрацию на сервере. Любой взломщик смажет прочитать содержимое сегментов ТСРДР. Уровенъзащищвн- ных сокетов (secure sockets layer, SSL) добавляет еще один уровень защиты путем шифрования запросов и ответов (конечно, ценой падения производительности). IIS и Winlnet поддерживают SSL. (Уровень защищенных сокетов описывается в документации по IIS) ISAPI-фильтры ' ’’'
Библиотека lSAPI-расширения загружается при первой ссылке на нее в POST- или GET-запросах, В отличие от нее, библиотека ISAPI-фильтра загружается (на основе записи в Реестре) при запуске службы WWW. После этого через фильтр проходят все HTTP-запросы, поэтому Вы получаете возможность читать и/или менять любые данные, которые приходят на сервер или уходят с него.
Создание DLL ISAPI-фильтра
Мастер ISAPI Extension Wizard делает создание фильтров таким же простым, как и создание расширений сервера. Выберите пункт Generate A Filter Object, и на втором шаге мастер будет выглядеть так как н а рисунке.
Список возможностей в разделе «Which Notifications Will Your Filter Process?* (Какие уведомления будет обрабатывать фильтр?) соответствует моментам, в которые фильтр может получить управление при обработке HTTP-запроса. Вы устанавливаете флажки, а мастер генерирует код . ,
MFC-классы для ISAPI-фильтра
I MFC есть два класса для ISAPI-фильтров: CHttpFilter и CHttpFilterContext.
CHttpHtter
Используя мастер ISAPI Extension Wizard, Вы создаете производный от CHttpFilter класс для каждого создаваемого фильтра. Должен быть только один объект этого класса. Для каждого уз семи уведомлений у класса есть виртуальная функция. Ниже показан их список в том пор ядке, в котором их вызывает IIS.
virtual DWORD OnReadRawData(CHttpFilterContex** pCtxt, PHTTP_FILTER_RAW_DATA pRawData);
virtual DWORD OnPreprocHeaders(CHttpFilterContext* pCtxt, PHTTP_FILTER_PREPROC_HEADERS pHeaderlnfo); virtual DWORD OnUrlMap(CHttpFilterContext« pCtxt, PHTTP_FILTER_URL_MAP pMapInfo);
virtual DWORD OnAuthenticationICHttpFilterContext* pCtxt, PHTTP_FILTER_AUT.HENT pAuthent);
virtual DWORD OnSendRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData); virtual DWORD OnLog(CHttpFilterContext» pfc, PHTTP_F!LTER_LOG plog);
virtual DWORD OnEndOfNetSession(CHttpFilterCcntext» pCtxt);
Если Вы переопределяете функцию, то Вы получаете контроль. Однако это может быть неэффективно, если ПЗ будет делать вызов виртуальной функции для каждого уведомления при каждой транзакции. Виртуальная функция Се1РИ1ег¥ег$юпвызывается один раз при загрузке фильтра. Мастер ISAPI Extension Wizard всегда переопределяет эту функцию и задает признаки в параметрер^гэтой функции, определяющие те уведомления, которые Вам нужны. Вот упрощенный пример, в котором установлены все признаки:
POOL CMyFHter: :GetFilterVersiqn(PHTTP_FlLT£ft_VERSION pVer)
CHttpFilter::GetFilterVersion(pVer);
pVer->dwFlags |= SF_NOTIFY_ORDER_LOW | SF_NOTIFY_NONSECURE_PORT |
SF NOTIFY_LOG 1 SF_NOTIFY AUTHENTICATION |
SF NOTIFY PREPROC HEADERS | SF NOTIFY READ_RAW_DATA |.
SF NOTIFY SEND_RAW_DATA | SF NOTIFY URL MAP |
SF_NOTIFY_END_OF_NET_SESSION:
return TRUE;
Если Вы задали только запросы на преобразование URL, то мастер установит только признак SF_NOTIFY_IJRI._MAB. переопределит только OnUrlMap. Другие виртуальные функции IIS не вызовет, даже если Вы переопределили их в производном классе.
CHttpFilterContext .
Объект этого класса существует для каждой серверной транзакции; каждая из функций уведомления получает указатель на него в качестве параметра. Вы можете вызывать следующие его функции-члены: GetSeruefVariable, AddResponselleadersa WriteClient.
Пример ISAPI-фильтра: ex35b.dll, ex35c.exe
Трудно найти подходящее применение для ISAPI-фильтра. То, которое придумали мы, — это полезная утилита просмотра журнала регистрации событий. Конечно, IIS сам регистрирует псе транзакции в файле (или базе данных), но для просмотра записей в нем нужно сначала остановить сервер. Этот пример предоставит в ваше распоряжение программу просмотра транзакций в реальном времени.
Выбор уведомления
Рассмотрим сначала список виртуальных функций-членов класса CHttpFilter, показанные выше. Изучите последовательность вызова и параметры. Для данного примера мы выберем OnRead- RawData, потому что она предоставляет полный доступ к текстам входящего запроса и заголовка (через pRawData) и. к IP-адресам источника и получателя (черезpCtxt-^GetSeroerVariable)
inередача данных о транзакции в программу просмотра
DLL ISAPI-фильтра не может сама отображать транзакции, потому что она выполняется (как часть процесса IIS) на невидимом рабочем столе. Для вывода текста в окно нужны отдельная программа и способ передачи данных из DLL в эту программу. Есть много методов передачи данных между процессами. Разговор с Джеффри Рихтером, «гуру* Windows, написавшем Windows для профессионалов, привел к выбору совместно используемой памяти. После размещения данных в программу отображения отправляется пользовательское сообщение WMJSEND- ТЕХТ. Эти сообщения помещаются в очередь, поэтому IIS может работать независимо от программы отображения.■.■
Мы объявляем две перемеииые-члеиа CEx35bFttter::m_bProces$Desta CEx35bFilter::m_hWnd- Dest и добавляем в конец функции GetFilterVersionKoa для присвоения этим переменным идентификатора процесса программы отображения и описателя ее основного окна. Код находит основное окно по его заголовку (ех35Ь) и затем получает идентификатор процесса. m_hProcessDest = NULL;'
If(<m_hWndDest = : :FindWindow(NULL. "cx35b")) !=NULL) {.
. DWORD dwProcessId;
GetWindowThreadProcessId(m_hWndOest. AdwProcessId);
m_bProcessDest = OpenProcess(PROCESS_DUP_HANOLE, FALSE, dwProcessId):
SendTextToWindowCEXSSB filter started\r\n");
}
Далее приведена вспомогательная функция SendTextToWindow, которая посылает сообщение WM_SENDTEXT программе отображения.
void CEx35bFilter: :SendTextToWindow(cbar> pchData) {
if(m_HProcessDest != NULL) {.-■
int nSize = strlen(pchData) + 1;
HANDLE bMMFRcceiver;
HANDLE bMMF = : :CreateFileMapping((HANDLE) OxFFFFFFFF, NULL, PAGE_READWRITE, 0, nSize, NULL);
ASSERT(hMMF J = NULL):
LPVOIO IpvFile = ::MapV16wOfFlle(hHMF, FILE_NAP_WRITE, 0, 0, nSize);
ASSERT (IpvFile != NULL);
memcpy((char*) IpvFile, pchData, nSize);.
: :DuDlicateHancle(: :GetCurrentProcess(),. hMMF, m_hProcessDest. ...
AhMMFReceiver, 0, FALSE, DUPLICATE_SAME_ACCESS I DUPLICATE_CLOSE_SOURCE);'
:. FDStMessage(m_hWndDest, WM SENDTEXT, (WPARAM) 0,v
(LPARAM) hMMFReceiver);'
::UnrapView0fFile(IpvFile);
Функция DuplicateHcmdlecosp^eT копию описателя файла, спроецированного в память, и • передает ее в программу ЕХ35С в качестве параметра сообщения. Идентификатор процесса ЕХ35С, определенный в GetFilterVersion,необходим для вызова DuplicateHandle. Теперь можно привести функцию фильтра OnReadRawData, которая вызывает SendTextToWindow:
DWORD CExSSbFilter: :OnReadRawData(CHttpFilterConText* pCtxt, PHTTP_FILTER_RAW_DATA pRawData)'''
TRACE ('Cbx35bFilter:;0nReadRawData\n“);-, . . .. .
Il посылает в окно дату/время, IP-адрес отправителя, IP-адрес получателя и данные запроса ' char pchVar[50] = "";'
char pch0u*[2000];■..
DWORD dwSize = 50;
см. след. стр.
BOOL bRet;
CString strGmt = Clime: :GetCurrentTime().FormalGmt("%m/%d/%y %H:%M:%SGMT ); strcpy(pchOut, strGmt);
bRet = pCtxt->GetServerVariable("REMOTE_AODR , pchVar, &dwSize);
If(bRet && dwSize > 1) {
strcat(pchOut, ", From ");
strcat(pchOut, pchVar);•
^Ret = pCtxt->GetServerVariable("S£RVER_NAME", pchVar, AdwSize);
ifCbRet && dwSize > 1) {
strcat(pchOut, ", To ");
strcat(pchOut, pchVar);
}
strcat(pchOut, "\r\n");
int nLength = strlen(pchOut); .
// Данные не заканчиваются нулем
strncat(pchOut. (const char*) pRawData->pvInData. pRawData->cbInDeta);
nLength += pRawData->cb!nData;
pchOut[nLength] - '\0';
SendTextToWindow(pchOut);
return 8F_STATUS_REQ_NEXT_N0TIFICATI0N;
Программа отображения
Программа отображения ex35c.exe не так интересна. Это стандартная программа на основе CRicbEdWiewc обработчиком сообщения WM_SENDTEXT в классе основного окна-рамки: LONG CMainFrame::OnSendText(UINT wParam, LONG IParam) {
TRACE("CMainFrame::OnSendText\n");
LPVOID IpvFile = : :MapViewOfFile((HANDLE) IParam, FILE MAP READ, 0, 0, 0);
GetActiveView()->SendMessage(EM SETSEL, (WPARAM) 999999, 1000000);
GetActiveView()->SendMessage(EM_REPLACESEL, (WPARAM) 0, (LPARAM) IpvFile);
s:UnmapViewOfFile(IpvFile);
::CloseHandle((HANDLE) IParam);
return 0;
)
Функция просто передает текст в класс «вид». ’
В классе CMainFrame примера ЕХ35С переопределена функция OnUpdateFrameTitleKM. удаления имени документа из заголовка основного окна. Это позволит DLL найти окно ЕХЗ 5С по его имени.
В классе «вид» определен обработчик сообщения WM_RBUTTONDOWN для реализации контекстного меню с командой стирания текста в окне. Очевидно, окна полей ввода с форматированием не поддерживают сообщения WM_CONTEXTMENU.
Сборка и тестирование ISAPI-фильтра ЕХ35В
Соберите проекты ЕХ35В и ЕХ35С и запустите программу ЕХ35С. Чтобы задать загрузку новой DLL фильтра, Вы должны вручную обновить реестр. Запустите приложение Regedit и дважды щелкните Filter DLLs в разделе \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Ser- vices\W3SVC\Parameters. Добавьте полные путь и имя DLL через запятую после других DLL
Есть еще одно действие, которое необходимо выполнить. Надо изменить режим IIS, чтобы позволить службе взаимодействовать с программой отображения. Щелкните апплет Services в Control Panel, дважды щелкните World Wide Web Publishing Service и установите флажок Allow Service To Interact With Desktop. Наконец, при помощи Internet Service Manager
■остановите и перезапустите WWW-службу для загрузки DLL-фильтра. На рисунке приведена информация, которую Вы увидите в программе ЕХ35С при запросе браузером страниц с сервера.
ЕХ35В filler maned
06/16/9817:11:21 GMT. From 127.0.8.1, to 1Z7.fl.IU
GET /plzzaform.html HTTP/1.0
Accept: ImagdgH. Image/x-xbhmap, ImageJJpeg, image/p|peg. */*
: Accept-Language: en
' UA-pIxelt: 1824x768
i UA-calon colerB
UA-OS: Windows NT
U*CPU:xB6
UserAgentiMozlHaM.il (compatible; MSIE 4.0; Windows Hl)
Host: localhost
Connection: Keep-Alive
86/16/98 17:11:51 GMT, From 127.8.0.1, To localhost
POST /ecripts/ex36a.dlitProcossRzzaForm HTTP/1.0
Accept: imege/дИ. Imagebcxbltmap. Image/Jpeg. Image/pjpeg. *1*
RelereK ilHpjWocalhosVhIzzeformJrtml
AccepbLanguaga: en
Content Type: ipp1katioa/x-ww»Horm-ut1encodnt
U*pkels: 1024x768
UA-color: coktrO
UMS: Windows NT
UA-CPU:x8B
UserAgeet Mezlila/4.0 (compatible; MSIE 4.0; Windows NT)
Hase locslhost
Connection: Keep-Alive
Content Length: 60
Pragma: No-Cache
name'DavlUaddressxSealHetquantily^l 4nlze=T2Alop1 xpnpperoel
Для отладки ISAPI-фильтра выполните те же действия, что и для отладки ISAPI-расширения сервера.
Интернет и серверы ActiveX-документов
Ау^аХ-дому-петя (ActiveX document) — это специальный файл, который Вы можете затру- зить с Web-сервера. Когда браузер видит файл ActiveX-документа, он автоматически загружает соответствующую программу — сервер ActiveX-документов (ActiveX document server) — с вашего жесткого диска, и эта программа целиком занимает окно браузера для вывода содержимого документа. Браузер Microsoft Internet Explorer — не единственная программа-контейнер ActiveX-документов. Программа Microsoft Office Binder также запускает программы-сер- веров ActiveX-документов, сохраняя несколько ActiveX-документов в одном файле надиске.
ПРИМЕЧАНИЕ В мире СОМ программа-сервер ActiveX-документов называется сервером, потому что она реализует COM-компонент. Программа-контейнер (Internet Explorer или Office Binder) создает СОМ- 1компонент и управляет им. В мире Интернета та же самая программа выступает в качестве клиента. поскольку может запрашивать информацию с удаленного сервера (в роли которого выступает Microsoft Internet Information Server).
В этой главе Вы узнаете о серверах ActiveX-документов и о самих ActiveX-документах. Вы создадите два сервера ActiveX-документов, которые работают через Интернет совместно с: браузером Internet Explorer. Обратите внимание на эту технологию, поскольку, с развитием Microsoft Windows, Вы будете все чаще встречаться с ней*.
Основы ActiveX-документов
Полезно рассматривать ActiveX-документы в контексте СОМ и OLE, которые Вам уже знакомы, если Вы читали другие главы этой книги. Тем не менее, Вы можете начать изучение серверов ActiveX-документов без полного понимания всех концепций СОМ, описанных в части VI
Сервер ActiveX-документов в сравнении с внедренным OLE-сервером
Как Вы видели в главе 28, программа внедренного OLE-сервера выполняется в дочернем окне приложения-контейнера OLE и занимает прямоугольную область страницы документа контейнера (см. рис. 28-1). Если внедренная программа-сервер не представляет собой мини-сервер, она может выполняться и автономно. Во внедренном режиме данные программы-сервера хранятся внутри файла приложения-контейнера. Внедренная программа-сервер замещает меню и инструментальную панель программы-контейнера, когда пользователь активизирует ее двойным щелчком.
В отличие от внедренного сервера, сервер ActiveX-документа полностью занимает окно- рамку приложения-контейнера, причем документ активен всегда. Приложение-сервер ActiveX
Во многих книгах по СОМ и OLE данная технология называется Active Document. — Прим., перев.
выполняется внутри окна-рамки контейнера практически так же, как оно выполнялось бы в автономном режиме. Вы можете сами увидеть это, если у Вас установлен Microsoft Office 97, который содержит программу-контейнер ActiveX Binder (Binder доступен из панели Microsoft Office). Другие приложения Microsoft Office (Microsoft Word, Microsoft Excel и т. д.) обладают возможностями сервера ActiveX. На рисунке 36-1 показаны документ Microsoft Word и диаграмма Microsoft Excel в одном окне программы Binder.
Рис. 36-1. Документ Word и диаграмма Excel в окне Microsoft Office BinderРис. 36-1. Документ Word и диаграмма Excel в окне Microsoft Office Binder
Рис. 36-1. Документ Word и диаграмма Excel в окне Microsoft Office Binder
Рис. 36-1. Документ Word и диаграмма Excel в окне Microsoft Office Binder
RR йRR йПодобно внедренному серверу, сервер ActiveX-документа записывает данные в хранилище внутри файла ActiveX-контейнера. Когда пользователь Office сохраняет программу Binder командой Save из меню File, Binder записывает на диск единый OBD-файл. Файл содержит одно хранилище для документа Word, а другое — для таблицы Excel. Вы можете увидеть эту структуру файла, используя утилиту DFVIEW, как показано на рисунке 36-2.
RR й
RR й
Duel rfe Vuowef
Wl-C:Vnydocs)Binder1.obd -
ЙЮ1с 0Book BlCompObJ ©iViewOOO ® IS umm arylnformati on jSjlDocumentSummaryintormatlon
щ101е ®ICompObj BMewOOO ®Word Document ®lSummaryl «formation EllDocumentSummarytnformatton El Binder !®ISummaiylnformatlon щ IDocumentSu mmarylnformation
Рис. 36-2. Структура файла в окне утилиты DocFile Viewer
Запуск сервера ActiveX-документов в браузере Internet Explorer
Выполнение сервера ActiveX-документов из браузера Internet Explorer гораздо интереснее, чем из Microsoft Office Binder (имеется в виду Internet Explorer 3.0 или более поздняя версия) Вместо загрузки хранилища только из OBD-файла сервер может загружать Хранилище С другого конца света. Вам надо только ввести URL, например, http://www.DaliLama.in/SecretsOf- TheUniverse.doc. и документ Microsoft Word открывается в окне вашего браузера, замещая его меню и панель инструментов. При условии, конечно, что Вы установили программу Microsoft Word. Если нет, загружается программа просмотра документов Word, но она должна быть на вашем жестком диске прежде, чем Вы начнете загрузку файла.
Сервер ActiveX-документов ие позволит Вам сохранить изменения на сервере Интернета, но позволит Вам сделать это на вашем собственном жестком диске. Другими словами, команда File Save отключена, но зато File Save As — доступна.
Если у Вас установлен Microsoft Office, попробуйте запустить Word или Excel в окне Internet Explorer. Сервер ЕХ34А способен доставить документы или рабочие листы в ваш Internet Explorer при условии, что они доступны из его собственного основного каталога. Учтите Internet Explorer распознает документы и рабочие листы не по расширениям файлов, а с помощью CLSID внутри файлов. Вы можете убедиться в этом, переименовав файл перед открытием.
Сервер ActiveX-документов в сравнении с ActiveX-элементом
Серверы ActiveX-документов и ActiveX-элементы могут выполняться как через Интернет, Tat: и без него. И те, и другие — скомпилированные программы, которые могут быть запущены в браузере. В следующей таблице перечислены некоторые различия между ними.
Различие
Сервер ActiveX-документов
Сервер ActiveX-управления
Тип модуля
ЕХБ
Чаще всего DLL
Может выполняться автономно
Да
Нет
Программа автоматически загружается и регистрируется браузером WWW
Нет
Да
Может быть внедрен в HTML-файл
Нет
Да
Занимает все окно браузера
Да
Иногда
Может быть многостраничным
Да
Обычно нет
Может осуществлять чтение/запись дисковых файлов
Да
Обычно ИСТ
Интерфейсы OLE в серверах и контейнерах ActiveX-документов
Серверы ActiveX-документов реализуют те же самые интерфейсы, что и внедренные OLE-серверы, включая lOleObjectJOlelnPlaceObjectH. Ю1е?пР1ясеАс1юеО$ес{Контейнеры ActiveX-документов реализуют lOleClientSite, lOlelnPlacePramen lOlelnPlaceSite. Согласование меню работает так же, как при визуальном редактировании.
Кроме этого, реализуются и некоторые дополнительные интерфейсы. Серверы ActiveX- документов реализуют IQleDocument, lOleDocutnentView, lOleCommandTargetR Iprint, Контейнеры ActiveX-документов реализуют lOleDocurnentSite. Архитектура допускает несколько представлений одного документа, подобно MFC-архитектуре «документ-вид», но большинство серверов ActiveX-документов допускает только одно представление для одного документа.
Важнейшей функцией для внедренного OLE-сервера является lOleObject::DoVerb,которая вызывается контейнером, когда пользователь дважды щелкает внедренный объект или активизирует его через меню. Однако для сервера ActiveX-документов основная функция — Ю1е- DocumentViewuUIActivate. (Перед вызовом этой функции контейнер вызывает IQleDocu- ment::CreateView, но обычно сервер просто возвращает указатель на интерфейс единствен-
ного объекта *вид».) ЫЫсй'шгенаходит посредник (site) и окно-рамку контейнера, делает это окно родительским окном для сервера, задает размеры окна сервера так, чтобы оно накрывало все окно-рамку контейнера, и активизирует окно сервера. -
ПРИМЕЧАНИЕ Важно понять, что взаимодействие в рамках СОМ происходит между программой-контейнером (Internet Explorer или Binder) и сервером ActiveX-документов (вашей программой) причем обе программы выполняются на клиентском компьютере. Нам неизвестны случаи, когда вызовы удаленных процедур (RPC) осуществлялись бы через Интернет. Это означает, что удаленный хост-компьютер (сервер) не использует COM-интерфейсы для связи с клиентом, но он может поставлять данные в виде хранилищ.
Поддержка серверов ActiveX-документов библиотекой MFC
MFC предоставляет возможность создавать ваши собственные программы — серверы ActiveX- документов. Кроме того, Visual C++ 6.0 позволяет Вам теперь создавать и контейнеры ActiveX- документов. Для разработки программы-сервера создайте новый ЕХЕ-проект с помощью MFC AppWizard, а затем установите флажок Active Document Server, как показано на рисунке Зб-З- Чтобы создать программу-контейнер, надо удостовериться, что отмечен флажок Active Document Container.
Рис. 36-3. Шаг 3 MFC AppWizard
Далее кратко описаны MFC-классы, входящие в архитектуру сервера ActiveX-документов.
CO/eServerDoc
Как и класс любого COM-компонента, класс документа вашего сервера ActiveX-документов будет унаследован от COIeServerDoc, который реализует JPemistSlorage, lOleObject, IDataObject, lOleinPlaceObject и lOleinPlaceActivcObject..
ПРИМЕЧАНИЕ COM-интерфейсы и MFC-классы, обсуждаемые здесь, получили свои названия прежде, чем Microsoft представила технологию ActiveX. Сервер ActiveX-документов был ранее известен как сервер объектов-документов (document object server или doc object server), поэтому эти названия можно увидеть в тексте исходных кодов и кое-где в интерактивной документации.
CDocObjectServerltem
Этот класс — производный от класса COleServeritem, используемого во внедренных серверах, В вашей программе сервера ActiveX-документов есть класс, производный от CDocObjectSer- rerltem, однако этот класс не используется, когда программа выполняется в режиме ActiveX- документа.
CDocObjectServer
Этот класс реализует новые интерфейсы сервера ActiveX. Ваше приложение создает экземпляр класса CDocObjectSeryera присоединяет его к объекту COteServerDoc. Если Вы рассмотрите код COteServerDoc..GetDocObjecLSeruem вашем производном классе документа, то увидите, что он делает именно это. Впоследствии объект документа и присоединенный объект СВос- ObjectSeruerpa6oraK>T совместно, обеспечивая функциональность сервера документов ActiveX. Данный класс реализует lOleDocumentи JOieDocumentView. Это означает, что в вашем MFC- сервере ActiveX-документов может быть только одно представление на каждый документ. Производные классы от CDocObjectServeraSiMHa не создаются.
COleOocIPFfame Wnd
Этот класс — производный от COlelPFrameWndB вашем приложении есть класс окна-рамки, производный от COleDocIPFrameWhd.KapK.ac приложений создает экземпляр этого класса, когда приложение запускается в режиме внедренного сервера или в режиме сервера ActiveX- документов. В последнем случае окно сервера полностью замещает окно-рамку контейнера и имеет собственный присоединенный ресурс меню с идентификатором IDR_SRVR_INPIACE (для SDl-приложения).
Пример ЕХ36А•
Построить пример ЕХЗбА можно в два атапа. Результатом первого этапа будет простой сервер ActiveX-документов, который загружает файл из контейнера. Базовым классом представления будет CRkbEditView.T. е. программа может загружать, редактировать и сохранять текс"! и внедренные объекты. На втором этапе мы усложним приложение, чтобы оно могло загружать отдельный текстовый файл из Интернета построчно, демонстрируя, что серверы ActiveX- документов могут делать произвольные вызовы Winlnet.
Этап 1 ЕХ36А: простой сервер■
Пример ЕХЗбА, находящийся на прилагаемом к книге компакт-диске, уже дополнен функцией загрузки текста из второго этапа. Вы мажете откомпилировать его, чтобы поупражняться с возможностями Этапа 1, или же создать новое приложение, используя Арр Wizard. Если Вы используете AppWizard, то Вам следует обратиться к рисунку 36-3, чтобы ознакомиться г диалоговым окном ЕХЕ-проекта AppWizard и выбрать нужные параметры. Все остальные па - раметры принимаются по умолчанию, за исключением того, что выбирается SDI-режим (шаг 1), расширение файла проекта путем щелчка кнопки «Advanced» на шаге 4 устанавливается -Зба», а базовый класс «вид» изменяется на CRkbEditViewHa последнем шаге мастера. Писать код на C++ Вам вообще не придется,
Для регистрации программы обязательно выполните ее один раз в автономном режиме. Во время автономного выполнения программы напечатайте любой текст (и вставьте какие- нибудь внедренные OLE-объекты) и затем сохраните документ под именем test-Зба в основном каталоге вашего Интернет-сервера (в каталоге \scripis или \wwwroot). Пробуйте загрузить tcst.36a из Internet Explorer и из Office Binder. Используйте меню Binder Section для загрузки и сохранения на диске документов ЕХЗбА
Вы должны настроить значки документов для ваших серверов ActiveX-документов, поскольку эти значки будут видны в правой части окна Office Binder.
Отладка сервера ActiveX-документов
Если Вы хотите отладить вашу программу в режиме сервера ActiveX-документов, выберите вкладку Debug в окне диалогового окна Build Settings. Впишите в поле Program Arguments аргумент/Embedding и запустите программу. Теперь выполните программу-контейнер и нс-
пользуйте ее для «запуска» сервера, который фактически уже запущен в отладчике и находится в режиме ожидания контейнера. •
Этап 2 ЕХ36А: добавление вызовов Winlnet.
Пример ЕХЗбА на прилагаемом компакт-диске включает два объекта диалоговых панелей: один для основного окна-рамки, а другой — для замещающего по месту окна-рамки. Оба присоединены к одному шаблону ресурса IDDJD1ALOGBAR, содержащему элемент управления «поле ввода», который принимает URL текстового файла, а также кнопки запуска и останова с растровыми изображениями зеленого и красного цветов. Если Вы щелкаете зеленую кнопку, обрабатываемую функцией-членом OnStartкласса СЕхЗбаУюгу.йы запускаете поток, который построчно считывает текстовый файл. Код потока из файла UrlThread.cpp приведен ниже; CString g_st rURL - "http;// ";.
volatile BOOL g_bThreadStarted = FALSE;
CEvent g_eKill;
UINT UrlThreadProc(LPVOID pParam)
g_bThreadStarted = TRUE;
CString strLine;
CInternetSession session;
CStdloFile* pFilel = NULL;
try {.
pFilel = session.DpenUAL(g_strURL, 0, IHTER№T_FLAG_TRANSFER_№1ARY
| IMTERMET„FLAG„KEEP_CONNECTION); // необходимо для аутентификации
// challenge/response в Windows NT
// Отображает текст из URL, пока не придет сообщение Kill
whilc(;;WaitForSingleObject(g_eKili.m_hObject, 0) !- WAIT_OBJECT_O) {■
// по одной строке
if(pFilet->ReadString(strLine) == FALSE) break;
strLine += "\n";
::SendMessage((HWND) pParam, EM.SETSEL, (WPARAM) 999999. 1000000);
::SendPessage((HWND) pParam, EM_REPLACESEL, (WPARAM) 0,(LPARAM) (const char-) strLine); Sleep(250); // Умышленно замедляет передачу
}
catch(CIntcrnetException* e) { LogInternetException(pParam. e); e->Delete();
}-
if(pFilet != NULL) delete pFilel; // закрывает файл выводит предупреждение gLbThreadStarted = FALSE;
Ц Посылает сообщение, чтобы изменить состояние кнопок панели инструментов ::PostMessage((HWND) pParam, EM_SETSEL, (WPARAM) 999999, 1000000);
TRACE( "Поток вывода файла завершен нормально\п");
return 0;
}
Этот код используетуказатель pFilel на CStdloFile, возвращаемый OpenURL. Функция-член Readstring считывает по одной строке, которую посылает в окно поля ввода с форматированием. Когда в основном потоке устанавливается событие *кйЬ («уничтожить*) (красная кнопка), поток чтения из URL закрывается.
Перед тестированием ЕХЗбА убедитесь, что сервер (ЕХ34Аили IIS) запущен и что текстовый файл находится в основном каталоге сервера. Протестируйте программу ЕХЗбА сначала в автономном режиме, вводя в диалоговое окно URL текстового файла. Затем попробуйте
выполнить программу в режиме сервера из Internet Explorer. Чтобы загрузить сервер, введите teste ба (документ, созданный при выполнении ЕХЗбА в автономном режиме) в поле Address. ПРИМЕЧАНИЕ Мы рассмотрела, как используется для чтения текстового файла класс CAsyncMoniker- Fite (см. раздел «Асинхронные файловые моникеры» в главе 34) вместо MFC-классов Winlnet. Однако мы отказались от Winlnet не полностью, чтобы программа могла использовать функцию-член ReadString класса CStdioFile для «вытаскивания» отдельных текстовых строк из сервера, когда требуется. Класс CAsyncMonikarFileмог бы «затолкать» произвольные блоки символов в программу (вызывая переопределенную функцию OnDataAvailable), как только символы были бы получены.
Нанесение на кнопки'растровь1х изображений, !■ '* ^^ 5 ^ '^?
I^В главе 11 Ъпис^лот|<^?^0^^ itccblipHjiyibij^Kiгруппу растровых 413<э6ражё-;;; ; ^ний^С кнопкой. MierSs^Q^^^^I^S, Microsoft Windows ^&И,Microsoft Windows NT 4.0 J ■шоддррживаютальТеркат^ныйспсюс^ который связывает с кнопкой одиночное изоб- г:.^йфе^я^ фйаУала Вы устанавливаете для данной кнопки* стиль Bitmap (в окне свойств ■ |1 кнопки), а затеьц ббьявляете’неремейну'Ю класса СВЙ;^^, которая будет существовать, - /по крайней мерд дб тех пор^ пока Кнопка доступна? Затем надо убедиться, .что функция • ,?t<§B^%^5^Mto^Только после того, как кнопка создана;. (' "/» ;.. •
^: / Приведенный ниже. код ^ассоциирует растровое И|рб|Ьа^^ие с кнопкой в файлах ^^^й-ф^рр и I^i^^'^ ^ ^Д/' V?^■/ ' t."/:' Д'Д 1
М1»1Шр^\-:
-ЧЙЕГМАЙ М$ЙЙ Я (i™ftMffl3utmap.Green, Ge^Saf6HahdMtj 1?■ . ; ' . .
5ы поместить подобный код ’ ■ в классе, производном от ■5ы поместить подобный код ’ ■ в классе, производном от ■М г . Если бы ваша кнопка была в
5ы поместить подобный код ’ ■ в классе, производном от ■
5ы поместить подобный код ’ ■ в классе, производном от ■
*:.• в функцию-член^ i^/»(№si»n
Пример ЕХ36В
Взгляните на форму заказа пиццы в примере ЕХ35А Обратите внимание: сервер (ISAPI DLL) обрабатывает заказ, только когда заказчик щелкнет кнопку «Submit Order Now'» (Сделать заказ). Это приемлемо в программе для заказа пиццы, поскольку Вы, вероятно, рады получить деньги от любого клиента, при этом неважно, каким браузером он пользуется.
Однако если речь идет о форме для приложения интрасети, можно проявить ббльшую избирательность. Вы мажете предписывать, какой браузер должен быть установлен у пользователей, а также производить сетевую установку клиентского программного обеспечения собственного изготовления. В этих условиях Вы мажете сделать процесс ввода данных более интеллектуальным, позволяя, например, клиентскому компьютеру проверять правильность текста, вводимого пользователем. Именно это и происходит в примере ЕХЗбВ, который, конечно же, представляет собой еще один сервер ActiveX-документов. ЕХЗбВ — программа с формой ввода графика работы служащих, которая работает как в браузере Internet Explorer (см. рис. 36-4), таки как автономное приложение. Походит на обычную HTML-форму, не так ли? На самом деле, это — MFC-класс «представление в виде формы», но обычный пользователь, скорее всего, не заметит разницы. Поле Name представляет собой раскрывающийся комбинированный список, который, однако, отличается от списка, который Вы могли видеть в HTML-форме, так как пользователь может в случае необходимости ввести значение вручную. Поле Job Number оснащено наборным счетчиком, который помогает пользователю выбрать значение. Возможно, это не самые идеальные элементы управления для ввода расписания. Но Вы можете использовать любые стандартные элементы управления Windows, включая древовидные отображения, списки, ползунки, а также ActiveX-элементы, иприэтом ничто не мешает Вам заставить эти элементы взаимодействовать любым образом.
|РИс. 36-4. Форма для ввода графика работы служащих
// Проверка правильности введенных значений поля в окне.\формы^МРС! рЧ ^л>^Д<^
• ^oSMi«^t ставдартная сжеъга проверки правильное™ MFC проверяет данйью• 5зМ|нкс^ ■ коща вызываетсяобытао при l^xb^^^
■ Приложения же часто долЗкны проверить п^^ильно^ж. д^п>п^ крхда. п^зовгСтель ] j ' только что покип)'л поле (поте ввода, окно списка, и т. д.> Проблему разрепшть непро- * я
^сго, поскольку Window допусл^ег свЬ&эйй.^ переход мевду палями в любой nbaie-f | : довательности при цемецци мыши, В идеальном случае для прбв^жи потя дотлюц нё-4?Ч! ; । шотзоваться стаццартный MFC-код DDX/pt>V (обмет^фотеркад^й^^ а йоовсиетая 7^ (нии транзатщий должна вызваться стадцарття фуыощя yq6dtaEi>6cj^^-5 f / * । /; ^ | ! ■’.,- Ре&ейиё; Наследуйте W’ftbbfii. Проверки подя в предртавле£1ий.:в вед( фс^ьй/ од ja#^|^ | ■ са <^Ш^/<Ьгхл,произвбднойр oifi^cnnW^.cdfcneinjnoii^'^lQ^? ^s11 li^ ^ ?Й
I ^«И»..п v,‘ ' ixHfKSWifcff’’!
WfnW LVAtlDFOFiM/ :» 7 • И
■ #define/J^DfW 4 .y *: .4-4 • I
? #defihe WM.VALIDATE WILUSER * "5\;
,1 Ж . *
p V "C
Г
i-y
п4
■ . class,CValidForm
риЫЙ ЙГсЙ^М ^ < /г ; : ?^.М^?
= £0
H- i
г л Ь ? <r. tv
У
-A t 4-Х
T * 5 "’; I? 3
v л ■ ут.зь-
^StwNrj
:' private; •• -:. ® 7? и ‘
• г. -"ДЮ; iC8^.WW@*tnt
j
4Г<1
.; & , -7
2 $
^^if^*:
public;Sд a
ЧЛСУаИ&Си^ ID), ..
. •• v V/ пефеопйедеяйть .в: flflopttollH^^ дйадейед' окна,^ чтоВы1
; ^ jJ-ySTttiai; void ¥a#$i.^|^.^Р^’. ®^.!Р^ч ^'
s '
г
И£Д;р H
//;1ДЛ*-А^т1Р I.UnL^ y Т^^ЛЧ^М/ t/l^ ^'V-?
■ Note^ea:? * 5 : к f ^ #Mf^J ^
M^awt wOL ;<to(joiw^W®^ftw^^*:li₽a|^
tililM^W:^®^^^
я
»/p;iii# 2 - 04 f.
^W-W^T^
Н^-=н^Н4Чы
"г fc
Г LONG
Ш
; 1ЛЛ Л^^' Я? ^^.Д ~ '^fc-^S^rrs
^ г л $-Л ' • i * ttlUiW | tfendif//_ _VALIDfWvAi: *ГД|
Этот класс содер^т с^^в^^туаз^ фунуц^ 1^$^!^_:
^ет идентификатор элемеш4 в йчестве второй партме^а.. Производный" класс «пред-’ ставлени§ в виде <^|^ реализует эту функфт/4гс^,вы&ат*футпсции DDX^ppV <для^ соответствующая' йоля.^Йижё представлен п^им|^^|^1и^дии -класс^Чнрё^авлёг^ф^вд^^фны* сдву1^’таёдовым1|йдАйог.&ода:'- • '•
Г т;
*№
И
■” ; switch №) .{•- X ^ ’ U L Иl?^ ' "
•/cm^M^ITI: 7 j*MM*i^‘*- 'v.1: Ш"№(ра« ЖВШ"'й ®#1’); ;' г л ййЖйМ8хйх(|ЯС: №О18Пс 0, ^о); » :.,,'.. Break* - „^4 < - ^№;/- ' "j • ■
:; case W£ «ОЩ ^$ е ^4 ;^ ’; >и ^=V: .
к Ше^рр\ рскет^хЙШ);' - ввУВйМхтаж ею#; . 20,-30)
hi" ■ i
Ж1М№
К
.;» ^NaT^^, ^ ‘-{-.break;.-..
.*-. ■ 44 • 7 ; X ■ >• .i^r :'*% ,T • ••'4 5 :VT ; :1 • Обратите* в
'Л - 4 • 1*Wa
•: Д ft ^4 t4 & 1Н | Н -т1^ 5., ЛЪ- ^ёт1 ^ Лэ.
4 а^Дт^ж^
*
Я 'i
• мастером:; v
у^ЩЖ- -™, .
-//(W-0№^fa№$%^i . ? «. PDX^Wtfpfi^f $®№|*i feOiti);
" DE|VW^^W№Wtol < ‘Ю):
- DDXJ№(pbi|" IDC ЕОГЁ."'m е#й2);
,00ОхпИах^ад< йЗЙсШ M 30)
1 г . , > U »1'
!SJ
хожесть с ф^^Щ^ ^^^^^^й^^еуернрйванной' ■ ^Д^пдеХ рОкЦ' ; ДТ Д1л ^ = ? / " ^ v
? t
ГЛj-S *
й
>3:; v V^lx f?
-Как-то все paopM^<§g«M®C|to®^mgiepe5pgivB|^выйти :
г. из поля.'К.6тда подтяоват^Шжймает клавишу Tub или)цеОб&ёт другой элементуправ- Ч
•ления, первый. ^1фнтХпоЬыл!^зСомащщоё1срЙ^ё^
, лщенЁкг элемё^.зНр^^^^ 1^^п^и^С^пйяй; формат сообщения .
! зависит от типа элемента упр^Поле ввода," например' посылает команду ВЙГКЖЬ- 4
. FOCUS; Когда окно формы получает сообщение кШйсив/оно вызываеткод рВХ/Ьву, ; ■ необходимый для зУого поля,, и если обнаружена* $йй&ф^ фр^ ввода опять устанав- ливаетсянаэтойодю ?4Ц^|р4 ^^xOWh'M^;
^ Однако здесь ^мы ст^лкийемфт с .некоторыми сложностяйй. B^-x^i^;, мы хотим,- , ■ чтобы пользоватетьм^ог «^д^бдйдпереключаться.на другоеприложение, ив этом слу- ./чае. не заинтерерова™;вперегаатесообщёниякШГос^мы должны быть ■
■ ■ дайэд«й$а^^вводя Цё*|е>^'^не мсь ,
; гжем просто ;вызывать1Яйбфемяв прямом ответе на "^сообщё^^ — взамен мы ; , ■ должны позволить /процессу "killfocus завершиться; Мы можем "дфЩшнуть этого, посы- ■
? ? Лая рпрёделяемоё пойзогатетем сообщёдеТ^ТУАЫр!^Об- Л
А1 работчик WMJ^AI^ вызывает нашу виртуальную функцию '^^^^^яяюеле 2 if 1жго/1щк факусЦэдё^^Кроме тогб/мыдблзк^М^
'2 рфобацение КШГосиЯлюзни^в’резуйтате выхода га зЯёйепй^^
• ,рый гпЬиался выбра^пблыбватель, атакжё'" мы должйы п’ойдлить rating £*|]|^®^Яр|^^ брз Пр^^ХИ Прййьйфт-Й!' л : /• £'Ч *f jp^r^^'^V^^^ jA |J еВбЙйая часть работы здей выполняется в виртуальном обработчике ^^ийиййй^’ ■ AкЬ^орыЙ? вызывается всеми уйДомЫющамй сообац^йми ^ёййр^#?. $1
j> ^авленйя; №ы могли бы;конечиб^индивидуально сботнойМ с JjM^^bf^^) 5
Шйё tillfocus каждого элемента управления в'ЦюиЙодном:^^^представлеЭше ^видё у
но* это 'будет' СЛШ1Й0М рбремеЫтёльно;>.i ^5 =) f ^г&У f^ ^i-fe^S ^^. дЙёё'тюкгайЯ^^Йй|Й^ко^ обр^огога<Йи®еда»Йм1^о >: р^; =^7^^
? ' 1!. ^ЙйиЫ^о Йпй tiiS32 ^о'бработка tiWSOtirW' ~ ‘'’"Jji ~—г -‘-' 1-'*--W
т^стйЖ1Шй)йй1= МЛУа1ШОп);
- ^Й Ш^Ш)^< t // ЙЩ^^ Ш1Гёсйё ■
7
р.1><ж?Жйавдвчода =«
ВШ#1 i [< (дю^^всМ *
Biti^vD ■ ^ MaiWW) Шл Ш^№№) 'Ш:'
$1
VI;
nw
t r<?
I if
IP
t*
r
№я
№nd’* pF&<|iB s CWndC 5QetR^9^1[^;. ^ вызов ©TdTtMec.RQR' ф^1кф§1: f 7 * ^ ,..".р ; -;f^ ' :” ^.^ I » .’ f" -‘ ' fe' т-рИЙ/Д pf; ’. \^1<-"£М&^аН
^ &W7MB МрВДавМхбокУО ДРУРОМУ -элементу s той же самой форме ь< З-.v ' - л.Ат ?#< 4®( pFoeys? 16 C^FraiUa^^P^-pjftil »= W^’> { / .'1 Р • ^'■: / >". \"? ■ 2^ < • $ 11
т 7 4
5
• v: т * Н^ ->. * 4 '.
1Т(р^отй£>йт|У1дСХ(Д^ ?* p^C® fy. у^^-'Узз^зММ; ♦ //ил.йёдй^фо^ нет’ кнопке &ml№ Сверить да^$^
Э№: г^в =- ЙМ$*ЙЙ^(.ШЖМ^Ж wParaiii’ f 7‘р t " '/.х г^ '№> rtn): ■ .'; '- I -'HVlA ;>V
■Уз' ii
)
p i kp £
63%
тл-^ц-г ii;1 \1^Й$1
retijfnj 6Fpi>Vi^wf;6nC0.mmand(wPfirap^ .IPai^B)..: ‘^Лк^ дшйМ.'*? (
№W№8 > -^i~^v2rf^ff. ’ , J ' 5 7 Я 4 fps Miff
^oiuviyiv. f^VT^wrttV/fV#* ^*V HVI^lflVl^UV! -VW13. •»»»*«* ^W^ .W^ K wr^y^^l^J j^
, ^ наконёц,, функЦй 00(M«7^ обрабатывающая оцределяемре. пользов^ел^м-©От.* 2;
Обй$ён^'iX^ty*^^ йШ1.^ : ? -Л Л - :' ;1:' - / f^pi £;Н-'; ~UHlp^ ~^
LONS CVkadFoW:0nVaddat^ljWf^Hft, AflhP'W<^;fti£f f
- 4s-'^te&^ a (№d^; WiidaW?" " 2' <У 1й'f/v*^
LpfCcEWWi <адз!^ TRDB)/f--
"Рт г Жйй^Й#^ = 1Ж . //- врйемй »тйкча<й проверку ’ '*; Н ^ f . 2^ ^ [К J| f^o
• >/ 6 -* Мкг' ё<й«'г» = №0" даавЯ W^ h, ?' .' i'' ■ • • г- w?? у ’ ? *«?? ^ ■
'VaiidsLeDl^ItegiCS^ oo^trallD); ■ . ._• ■ . . . 4 $ ^ .■ .р р ^.*
#1-^A £/f A
Ai
1&<M.^^h(SVsec&^i ^ • ■ ^А^аЙ5
?д к 1 АД ■ -ТААСЕо^^^перехватила исключение^п ‘^ л. 1«Ы
- с • V.... -;. ;^ ti*уже: оло^еден
^ИнЬ^укщииj^mhk>. класса
Добавьте в ваш-ахр$^^^^й^^уШ
2., Вставьте* следующийюператор в файл заголовка класса «bi
H^i^N
^ШМ413 S ^M > V X. i
Lf $ 1
'return
'*
"V
^вместо ^$$g;f' 14^
тем же самым ине летайте ваш
4. Переопределите : 4 зано, выше, S
,; >Вот и вое. -4 ч ?•
’ • •тт " л । r -Х' Ь Ам ^ '• ЗИ
* ;/ДлЖдиалогрвыхо№^
tei.
^‘■^ % ч A ;,^4 ~
я Мшего классу' »вид* 4
®@йдля элементов управления дашвй а фмы как пока
* т, Ч< <- ’Г ^ V_ового окна, от-
Программная генерация POST-запросов
'Основа программы ЕХЗбВ — рабочий поток, который, генерирует POST-запрос и посылает его удаленному серверу. Сервер не интересует, исходит ли POST-запрос от HTML-формы или от вашей программы. Он может обрабатывать POST-запрос с помощью ISAPI DLL, сценария PERL или с помощью CGI-программы.
Вог что получает сервер, когда пользователь шрлкает кнопку Submit в примере ЕХ36В:
IPOST scripts/ex35a.dll?ProcessTlmesheet НТТР/1.0
(заголовки запроса)
(пустая строка)
IPenod=12&Name=Dunkel&Hours=6.5&Job=S
А вот — код потока из файла PostTiiread.cpp:
// PostThread.ерр (использует вызозы MFC Wi rCnet)
#incljde <stoafx.h>
^include "PostThread.h"
«define MAXBUF 50000
CString g_strFile » '7scripts/ex35a.dll";
CString g_strServerName = "localhosl";
CString g_strParameters;
UINT PostThreadProc(LPVOID pParam)
CInternetSession session;
CHttpConnection* pConnection = NULL;
CHttpFile* pFilel = NULL;
char* buffer = new char[MAX8UF);
UlNT nBytesRead = 0;
№ORD dwStatusCode;
■ IBOOL DOkStatUS - FALSE;
try {
pConrection = session.GetHttpConnection(g_sIrSe^verName, (INTERNET-PORT) 80);
pFilol = pConnection->OpenRequest(0, g_strFile + “?ProcessTimesheet", // запрос POST
•NULL, 1, NULL, NULL, INTERNET„FLAG-KEEP CONNECTION I'
INTERNET_FLAG_RELOAD); // не брать из кэжа
pFile1->SendRequest(NULL, 0, (LPVOID) (const char*) g_strParameters, g .st rPa ramate rs. GetLeng tr, ());
pHle1->0ueryInfoStatusCgde(dw$tatusCode);
if(dwStatijsCode = 200) { // статус (Ж
H неважно, что возвратил сервер - мы проверяем ОК-статус bOkStatus = TRUE;
nBytesRead = pFile1->Rcad(buffcr, MAXBUF - 1);
buffer[nBytesRead] = '\0': // необходимо дгя TRACE•
TPACE(buf*er);
TRACE("\n');
} 5
catch(CInternetException* pe) { char text[100];
pe->GetErrorMessagc(text, 99),
TRACEC'WinInet-исключение %s\n", text);
pe->Delete();
if(prilel) delete pFilel; // закрывает файл - вьщает предупреждение if(pConneclion) delete pConnection; // А почему вьщает?
delete [] buffer;
::PostMessage((HWND) pParam, WM_POSTCCMPLETE, (WPARAM) bOkStatus, 0);
return O;
}
Основной поток формирует строку gjstrParameters, основанную на том, что напечатал пользователь, а рабочий поток посылает POST-запрос, вызывая CHttpFile::SendRequestQ,uery- InJbStatusCodeBbsscHsert, вернул ли сервер ответ *ОК». Перед выходом поток отправляет сообщение основному потоку, используя значение bOkStatus в wParam для обозначения успеха илинеудачи.
Класс «вид» в примере ЕХ36В
Класс CEx36bViewпроизведен от CVdlidForm, как описано во врезке несколькими страницами ранее. Класс Сй<?6йИгегупринимает данные от пользователя и запускает поток с запросом POST, когда пользователь нажимает кнопку Submit — после того, как все поля были успешно проверены. Проверка правильности поля не зависит от Интернет-приложения. Вы мажете использовать CVdlidFonKB любом приложении, использующем MFC-класс «представление в виде формы».
Ниже показан код для переопределенной функции-члена 1Ый^е£М£Л«идоторая вызывается всякий раз, когда пользователь перемещается от одного элемента управления к другому: void CEx36bView;.ValidateDlgItom(CDataExchange* pDX, UINT uID)
ASSERT(this);
TRACE("CEx36bV iew::VaiidateOlgltem\n");
switch (uID) {
case IDC EMPLOYEE:.
DDX_CBStrlng(pDX, IDC_EMPLOYEE, m„strEmployee);•
И требуется особый DDV для пустой строки
DDV_MaxChars(.pDX. m„strEmployee, 10);
if(m_strEmployee. IsEmptyO) {
AfxMessageBox("Необходимо задать имя сотрудника"); pDX->Fail{);
}
Ьг еак •
case IDCJiOURS:
DDX Text(pDX, IDC_HOURS, m_dHours);
DDV_MinMaxDouble(pOX, m_dHours, 0.1, 100.);■
break;
case ZDC_J06:
DDX fext(pDX, IDC_JOB, m_nJob);
DDV^MinMaxlntfpDX, m.nJob, 1, 20);
break;
default: break;
}
return;
Функция-член OnSubmit вызывается, когда пользователь щелкает кнопку Submit. CWnd::Up- dateData возвращает TRUE, только если все поля были успешно проверены. В этот момент функция отключает кнопку Submit, формируетg_strParametersH запускает поток, генерирующий запрос POST.
void CEx36bView: :QnSubmlt()
if(UpdateData(TRUE) == TRUE) {
GetDlgItem(IDC_SU8MIT)->EnableWindow(FALSE);
CString strHours, strjob, strPeriod;
strPeriod. Format( "%d", m_nPeriod):
st rHou rs.Format(“%3.2f", m_dHours):
strjob. Format("%d", m_nJob);
g_strParameters = "Period=" + strPeriod + "&Employee=" + m_st rEmployee +
"&Hours=" + strHours + "&Job=" + strjob + "\r\n";
TRACE("Строка параметров = %s", g strParameters);
AfxBeginThread(PostThreadProc, GetSafeHv<nd().THREAD_PRIORITY_NORHAL);
I
}
Функция-член OnCencel вызывается, когда пользователь нажимает кнопку Reset. Логика CValidForm требует, чтобы идентификатором кнопки был IDCANCEJL
void CEx36bView: :0nCancel()i
i
CEx36bDoc« pDoc = GetDocument();
m_dHours = 0;
m.strEmployee = “" ;
m_nJob = 0;.
m_nPeriod = pDoc->m_nPeriod;
UpdateData (FALSE);
}
Обработчик OnPostComplete вызывается в ответ на определяемое пользователем сообщение WM_POSTCOMPLBTE, посланное потоком запроса POST:
LONG CEx36bView: :OnPostComplete(UINTwParam, LONG 1 Pa ram) {■
TRACE("CEx36bView: .‘OnPostComplate - %d\n", wParam);
if(v<Parain == 0) {
AfxMessageBox("Сервер отверг транзакцию'):
else
OnCancel();
GetDlgIteTi(IDC_SUBMIT)->EnableWindow(IRUE); return 0;
}
Эта функция выводит окно сообщения, если сервер не вернул ответ <ОК», затем она включает кнопку Submit, позволяя отправить другие данные графика работы.
Сборка и тестирование ЕХ36В
Произведите сборку проекта /vcpp36/ex36b, а затем выполните его один раз в автономном режиме, чтобы зарегистрировать и записать файл документа с именем te.st.36b в основной каталог сервера WWW. Удостоверьтесь, что ЕХ35А DLL доступна в каталоге scripts (с разрешением на выполнение), поскольку эта библиотека содержит функцию ISAPI ProcessTimesheet, которая обрабатывает серверную часть запроса POST. Убедитесь, что на вашем компьютере запущен IIS или другой сервер с возможностями ISAPI-
Запустите Internet Explorer и загрузите test.36b с вашего сервера. Программа ЕХЗбВ должна выполняться в окне просмотра, и Вы должны иметь возможность производить транзакции.
Серверы ActiveX-документов и VB Script
Б HTML-файл можно вставить код сценария на VB Script (или на JavaScript). Мы не эксперты по VB Script, но мы видели некоторые типовые сценарии. Наверное, Вы можете переписать приложение ЕХЗбВ, применив VB Script, но тогда Вы будете ограничены стандартными элементами ввода HTML Было бы интересно посмотреть, как программист на VB Script решит проблему. (В любом случае, Вы программируете на C++, а не на Visual Basic, так что можете просто заниматься тем, что умеете.)
Дальнейшее знакомство с серверами ActiveX-документов
В примере ЕХЗбАдля чтения текстового файла с Интернет-сервера был использован рабочий поток Это обычно для MFC-классов Winlnet: в этом случае подразумевается, что имеется стандартный HTTP-сервер. Сервер ActiveX-документов может также легко производить вызовы Winsock, используя класс CBlockingSocketa.3 главы 34. Это означало бы, что мы использовали более низкоуровневые протоколы, чем HTTP и FTP. Вы могли бы, например, написать собственный сервер Интернета, который получает запросы на порт 81. (Такой сервер в случае необходимости может выполняться одновременно с IIS.) Ваш сервер ActiveX-документов может использовать специфичный TCP/IP-протокол для приема двоичных данных через открытое соединение. Сервер может использовать эти данные для модификации своего окна в реальном масштабе времени, или может посылать данные на другое устройство, такое как звуковая плата.'
Введение в Dynamic HTML
Динамический язык разметки гипертекста (Dynamic Hypertext Markup Language, DHTML) - превосходная технология, введенная в Microsoft Internet Explorer 4.0 (IE4) и предоставляющая большие возможности для Web-разработчиков. DHTML может полностью изменить способ разработки приложений для Windows. Что за шум поднят вокруг DHTML?
Это началось как «механизм отображения HTML» IE4, иногда называемый «Trident* в литературе от Microsoft Как часть разработки Internet Explorer 4, Trident был сделан СОМ-компонентом, предоставляющим доступ к своим внутренним объектам, которые используются для отображения HTML-страниц в Internet Explorer 4. Эта особенность позволяет просматривать части HTML-страницы в сценарии или коде, как если бы HTML-страница была структурой данных. В прошлом остаются дни, когда необходим был синтаксический разбор HTML или приходилось писать абсурдные CGI-сценарии для передачи данных в форму. Однако реальная мощь DHTML состоит не в возможности доступа к HTML-объектам, а в возможности менять HTML-страницу «налету», отчего он и называется «динамическим».
Поняв концепцию DHTML, можно придумать миллион приложений. Web-разработчики могут поместить большую часть логики управления Web-страницей в сценарии, загружаемые клиентом. С++-разработчики могут встроить DHTML в свои приложения и использовать его в качестве встроенного Web-клиента или как супергибкую д инамическую «форму», изменяемую приложением «налету». Разработчики на Microsoft Visual J++, использующие Windows Foundation Classes, WFC, могут реально запрограммировать DHIML на сервере, в то время как клиент Internet Explorer отвечает на команды — отличная альтернатива CGI и потенциально более мощная концепция, чем ASP-сценарии на стороне сервера.
К сожалению, DHTML так мощен и разнообразен, что требуется отдельная книга для его подробного описания. Например, чтобы реально постичь DHTML, нужно понимать все элементы HTML: формы, списки, таблицы стилей и т. д. Основным источником по изучению деталей DHTML является Inside Dynamic HTML (автор Scott Isaacs, Microsoft Press, 1997),
В данном издании мы кратко опишем модель объектов DHTML, работу с ней с точки зрения сценария, а затем покажем, как использовать эту модель из MFC версии 4.21 и ATL. Все это стало возможно благодаря прекрасной поддержке DHTML, встроенной в Visual C++ 6,0.
Модель DHTML-объектов
Если Вы работаете над проектам в Visual C++ и не имеете времени рассматривать HTML, то первое, что Вы должны знать, — HTML является форматом языка разметки. Вот код очень простойНТМЕ-страницы:
<li Lml>
<head>
<title>
This is an example very basic HTML page!
</title>
</1ead>
<body>
<h1>This is some text with H1!
</h1>
<h3>
This is some text with H3!</h3>.
</body>
</html>
Этот базовый HTML-«документ» состоит из следующих элементов.
Голова (head) (или заголовок [header]) — в этом примере заголовок содержит название «This is an example very basic HTML page».
Тело (body) — в этом примере содержит два текстовых элемента. Первый имеет стиль заголовок 1 (hl) и текст «This is some text with Hl!». Второй имеет стиль заголовок 3 (h3) и текст «This is some text with H)!».
В результате получится HTML-страница, которая при выводе в Internet Explorer 4 выглядит, как показано на рисунке 37-1.
Рис. 37-1. Очень простая страница HTML в Internet Explorer 4
Когда Internet Explorer 4 загружает эту HTML-страницу, он создает ее внутреннее представление, которое Бы можете просматривать, читать и изменять на основе модели DHTML-объектов. На рисунке 37-2 показана базовая иерархия DHTML-объектов.
В корне модели объектов находится объект window. Его можно использовать в сценарии для выполнения таких действий, как вызов диалогового окна. Вог пример на JScript:
<SCRIPT LA№UME=" JScript">-
function about()
window.showModalDlalog(“about.htm", “", "dialogWldth:25em;dialogHelgnt:13em") }
</SCRIPT>
26 Зак № 787
applets
body
frames
Images
links
selection ]
scripts
—4 stylesheets
anchors ~*|
Рио. 37-2. Базовая иерархия модели объектов DHTML
При вызове функции about будет вызвана функция sbowModalDialog объекта window для отображения диалогового окна. Этот пример также показывает, как сценарии обращаются к модели объектов — через глобально доступные объекты, которые напрямую соответствуют объектам в DHTML-модели.
Объект window содержит несколько «подобъектов», которые позволяют управлять частями Internet Explorer 4. Объект document будет обсуждаться наиболее подробно в этой главе, поскольку он предоставляет программный доступ к различным элементам текущего HTML- документа. Вог сценарий на JScript, который показывает, как создать динамическое содержимое, изменяющее объект document. '
<HTML>
<HEAD>
<TITLE>Welcome! <ДП1Е>
<SCRIPT LANGUAGE='JScript">
fraction changeMef) {
document.all.MyHeading.outerHTML =
"<H1 ID=MyReading>Dynamic HTML is magic!</Н1>";
document.all.MyHeading.style.color = "green";
document.all.MyText.innerText = "Presto Change-o! ";
document.all. MyText.align = "crater";
document, body. insertAdjacentHTMLC'BeforeEnd”.
"<P ALIGN=\"center\">Open Sesame!</P>“);
</SCRIPT>
•</HEAD>
<B0DY oncllck="changeMe()">
<H3 №MyHeading> Dynamic HTML demo!</H3>
<P ID=MyText>Click anywhere to see the power of DHTML! </P>
</BODY>
</HIML>
Этот сценарий «на. лету» изменяет объекты MyHeading и MyText в HTML-доку менте. Он не только изменяет текст, но и такие атрибуты элементов, как цвет и выравнивание. Если хотите увидеть сценарий в действии, загрузите файл ex37_Lhtml с прилагаемого компакт-диска.
Прежде чем разбирать модель DHTML-объектов дальше, давайте рассмотрим концепцию набора (collection) в DHTML Наборы в ОНТМЕлогически эквивалентны связанным спискам в C++, в реальности доступ к модели DHTML-объектов выполняется в основном перебором одного набора в поисках конкретного HTML-элемента, затем перебором другого набора для получения другого элемента. В наборах есть несколько методов, например, contains и length, которые используются для перебора.
Например, одним из подэлеменгов объекта document является набор под названием all, содержащий все элементы документа. Фактически, большинство подэлементов объекта document являются. наборами. Далее показан сценарий, в котором выполняется перечисление всех элементов набора ай данного документа.
<HTML>
^EADxHTLMterating through the all collection. <AITLE>
<SCRIPT LANGUAGE=“JScript">'
function listAHElementsO {
var tag_names =
for (1=0; Kdocument.all.length: i++)
tag_names = tag_names + document.all(i).tagName + " ";
alert("This document contains; " + tag_names);
}
</SCRIPT>.
</HEAD>
<BQDY onload='’listAllElements()">
<H1>DHTML Rocks!</H1>
<P>This document is <8>very</B> short.
</BODY>.
</HTML>
Обратите внимание, как легко в сценарии извлечь элементы. (Синтаксис вызова использует скобки аналогично обращению к массиву в C++.) Также обратите внимание, что каждый элемент в HTML-документе имеет свойства, например, tagName, которые можно использовать для программного поиска различных элементов. Допустим, Вы хотите написать сценарий, который отфильтровывает все элементы с полужирным начертанием — для этого Вы можете просканировать набор all в поисках элемента с tagName, равным В.
Теперь Вы знаете основы модели DHTML-объектов и понимаете, как получить доступ к ней из сценария, т. е. с точки зрения Web-мастера. Теперь рассмотрим, как Visual C++ предостав- ляетдоступ к DHTML с точки зрения разработчика приложения.
Visual C++ и DHTML
\jsual C++ поддерживает DHTML через MFC и ATL Обе библиотеки дают полный доступ к модели DHTML-объектов. К сожалению, доступ к модели объектов из языков, подобных C++, выполняется через Автоматизацию (IDispatcb^H. во многих случаях не так прост, как в ранее £ ассмотренных сценариях.
Модель DHTML-объектов представлена разработчикам на C++ в вид е COM-объектов с префиксом IHTML (IHTMLDocument^JTMLWindowJlHTMLElementJHTMLBociyElementvL т. д.). Полу- ьив интерфейс документа, Вы можете использовать любые методы интерфейса IHTMLDocu- мепйдля того, чтобы получить или изменепить свойства документа.
Для получения доступа к набору all вызовите метод lHTMIDocument2::i’et_alllKOT»pY$S. возвращает интерфейс IHTMLElementCoUeclionn^foo^, содержащего все элементы документа. После этого Вы можете перебрать все элементы набора, используя метод IHTMLEiement- CoUecli<№:item (подобно скобкам в вышеприведенном сценарии). Он возвращаетуказатель на IDispatch, для которого можно вызвать Querylnterjace, запросив интерфейс IHTMLElement. Последний используется, чтобы получить или установить информацию HTML-элемента.
Большинство элементов предоставляет также специфические интерфейсы для работы с конкретным типом элемента. Эти интерфейсы имеют имя формата Ufl'MLXXXElementvae XXX — имя элемента (например, 1НЕМЬВо4уШетепГ)Для получения нужного специфичного интерфейса вызовите QuerylnterfaaaQ’b&KYi iHTMLElemeHt.'Bce это может Вас запутать, но не переживайте — в разделах, посвященных MFC и ATL, содержится много примеров, демонстрирующих, как это все работает вместе, Мы немедлен но начнем писать DHTML-код.
Примеры ЕХ37А и ЕХ37В
Поддержка DHTML в MFC заключается в новом классе CHtmlView, производном от CView. СНггп$У1ештюзволя!бт встроить вывод HTML-кода в окно-рамку или разделяемое окно. При добавлении небольшого DHTML-кода это окно может стать динамической формой. В приме- ое ЕХ37 А показано, как использовать класс ClltmlVieu» MDI-приложении. Для создания ЕХ37А выполните следующие действия.
Запустите AppWizard и создайте проект \vcpp32\ex37a\ex37a. Выберите New из меню File. Перейдите на вкладку Projects и выберите MFC AppWizard (ехе). Согласитесь с параметрами по умолчанию, но на шаге 6 выберите CHWriViewa качестве базового класса, как показано на первом рисунке на стр. 771.
Измените URL, используемый для загрузки. В функции CEx37aVieia:On!niiiallfi^ Вы увидите такую строку:
Navigate2(JX "http://www.microsofL.com/visualc/"), NULL, NULL):
Отредактируйте данную строку, если нужно, так, чтобы приложение загружало локальную страницу или другой URL.
Скомпилируйте и запустите. На рисунке 37-3 показано это приложение, в которое загружена страница по умолчанию..
Теперь давайте создадим пример, который реально показывает, как использовать DHTML с MFC. В ЕХ37В в разделяемом окне создаются объекты типа CHbnlVie№ CListView. При помощи DHTML осуществляется перебор элементов в объекте CIitmlViet№ вывод результатов в окно CListView. Получается «DHTML explorer», который можно использова ть для просмотра моделиОНТМЕ-объекговлюбого HTML- файла.
Рис. 37-3. Пример ЕХ37А
Для создания ЕХ37В выполните следующие действия.
Запустите Арр Wizard и создайте проект \vcpp32\ex37b\ex37b. Выберите New из меню File. Перейдите на вкладку Projects и выберите MFC AppWizard (ехе). Согласитесь со всеми параметрами по умолчанию, за исключением трех: выберите Single Document, на шаге 5 выберите Windows Explorer и на шаге 6 выберите CHtmIViewB качестве базового класса. На рисунке (см. стр. 772) показан окончательный набор параметров.
Сделайте класс бХеДУ/еипроизводным от CLislView.no умолчанию AppWizard делает класс Шу^иг«<разделяемого окна производным от CTreeView. Откройте файл LeftView.h и сделайте глобальную замену CTreeVieicua CLislView. Затем откройте LeftView.cpp и сделайте тоже самое. (Подсказка: используйте Edit/Replace/Replace All.)
Измените URL, используемый для загрузки. В функции CEx37bVietv::OnInitialVpdaie измените URL на res://ie4tour.dll/wclcome.htm.
Добавьте в CMainFrame функцию ОоОНТМЕЕхр1оге.Сначала добавьте в файл Main-.
Frm.h следующее объявление:
virtual void OoDHTMLExpIore(void);
затем добавьте реализацию в файл MainFrm.cpp:
void CMainFrame::DoDHTMLExplore(vold) ■i
CLeftView *plistView= (CLeftView *)m_wndSplitter.GetPane(O,O);
CEx37bVlew * pDHTMLView = (CEx37bView,*)m_wndSplitter.Get.Pane(0,1);
//Очистка списка
pListView->GetlistCtrl().DeleteAllItems();
IDispatch* pDisp = pOHTMLView~>GetHtmlDocLment();
if (pDisp != NULL )
•I
IHTMLDocunierit2» pHTMLDocument2;
HRESULT hr;
hr = pDisp->OueryInterface( IID_IHTMLDocumeDt2, (void*»)&pRTHLDocument2);
if (hr== S_0K) {
IHTMLElementCollection* pColl = NULL;
hr = pHTMLDocument2->get all( &pColl );
if (hr == S OK && pColl != NULL) {
LONG celem;
hr = pColl->get_length( Acelem );
if ( hr == S_OK ) <
for( int i=0; i< celem; 1++ )
{
VARIANT varlndex;
varlndex.vt = VT_UINT;
varlndex. IVal - i;
VARIANT var2;
VariBntIrdT( Svii'2 );
IDispatch- pDisp;
h-1" = peon-uteri' varlndex, var2, ipDisp };
if ( hr ~ S.OK )
IHI Ml Clement’' pclen;
hr = pDisp->yueryInterface(lii)_IHTVl Element, (void **)ioEleni):
if ( hr == S.OK )
BSTRbso-,
hr - pElo“->get_taghame(&b$tr);
CString strTag - bstr:
IHTMLImgElemar*’ plngElem:
//Это элемент-изображение'*
hr = pDisp->OueryInterfaoe(IID IHTMLImgEleme.it, (void ■‘J&pIrgElem ): if ( hr == S.OK )
pltrgElem->got_t ref (&ost r);
strtag += " - ";
strTag += bstr;
pIi>gElem->Release();
}
else
IHTMLAnchorElement» p.ArchElcm;
//Это анкер?
hr = pOisp-^tieryJnterfaceCliD.IfrMLAncTjrElemerit, (void «'lepAnchElem );
if ( hr == S.OK )
{
pAnchElcm->get_hrcf(&bstr);
strTag += " "I strTag += bstr; pArchElem->Release():
}
\Цконец else
pListView->6etListCtrlO, InscrtltemC : pUstView- >GerListCtrl(). GetItemCount(), strTag);
pElor->Release();
}
pRisp->heiease();
pColl->Release();
}
pHTMLDocumeit2->Release(): }
pL4sp->ReleeseO;
Вот действия, которые функция выполняет для «изучения» HTML-документа при помощи DHTML.
■ Сначала она получает указатели на объекты-представления классов CListViewH CHtml- Viewb разделяемом окне.
Ш Затем функция делает вызов GetHtmlDocumentjyva получения указателя на IDispatch объекта document модели DHTML-объектов.
ш Далее она получает указатель на интерфейс ЩТМ11>оситпепг2.
в Используя интерфейс 1НТМШоситеп12Дфнкща.а получаетдоступ к набору all и выполняет его перебор. Для каждого элемента набора проверяет его тип. Если элемент является анкером или изображением, то извлекается дополнительная информация, например, путь к файлу изображения. Текстовое описание HTML-элемента помещается в объект CListView.
Убедитесь, что Mainfrm.cpp включает mshtmLh. Добавьте вверху Mainfrm.cpp следующую строку, необходимую для компиляции кода DoDHTMLExplore.
«include cmshtml h>
Добавьте вызов DoDHTMLExploreB данном примере для вызова DoDHTMLExplore мы изменим функцию СЕх37ЬАрр::ОпАррАЬоийэм.е™.те имеющийся код строками, выделенными полужирным:_
void CEx37bApp::OnAppAbout() (
CMainFrame * pFrane = (CMainFrame*)AfxGetMainWnd();
pFrame->DoDHTMLExplore(); }
Измените стиль вывода списка. В функции CLeJtView::PreCreateWindot^^a$uv Left-
View.cpp) добавьте следующую строку:
CS.style 1= LVS_LIST;'
Скомпилируйте и запустите пример. Щелкните на панели инструментов кнопку »?» или выберите в меню команду Help/About. На рисунке 37-4 — пример ЕХ37В в действии.
Рис. 37-4. Пример ЕХ37В в действии
Теперь Вы знаете, как использовать DHTML и MFC. Давайте посмотрим, как ATL реализует поддержку DHTML,
Пример ЕХ37С
Поддержка DHTML в ATLвыполнена в виде объекта HTML, который можно встроить в любой ActiveX-элемент на основе ATL В примере ЕХ37С создастся элемент управления, иллюстрирующий поддержку DHTML. Для создания примера ЕХ37С выполните следующие действия.
Запустите ATL COM AppWizard и создайте \vcpp32\ex37c\ex37c. Выберите New из меню File. Перейдите на вкладку Projects и выберите ATL COM AppWizard. В качестве типа сервера выберите Executable.
Вставьте элемент управления HTML Control. В Class View щелкните правой кнопкой список классов сх37с и выберите New ATL Object. Перейдите в список Controls и выберите HTML Control, как показано на рисунке.
Щелкните Next и заполните ShortName в разделе C++, как показано на рисунке,
ПРИМЕЧАНИЕ Если Вы посмотрите на объект IDHTMLUI, то увидите следующую стандартную реализацию обработчика ОпСИск:
STDMETHOD(OnClicX)(IDispatch* pdispBody, VARIANT varColor) (
CComQIPtr<IHTMLBedyElement> spBtidy(pdispRody);
if (spBody .’= NULL)
см. след. стр.
spBody->pLt_bgColor(varColor);
return S_OK;
}
По умолчанию обработчик ОпСНск использует Queryinterface для получения IHTMLBodyElement по указателю на IDispatch. Затем обработчик вызывает метод put_bgC6tor для изменения цвета фона.
А. Скомпилируйте, загрузите и запустите элемент управления для просмотра ATL- Кода в действии. После сборки проекта выберите в меню Tools команду ActiveX Control Test Container. В тестовом контейнере выберите в меню Edit команду Insert New Control и выберите в списке CDHTML Class. На рисунке 37-5 показан ActiveX-эле мент, использующий DHTML для изменения фона при щелчке пользователем соответствующей кнопки.
РИс. 37-5. ActiveX-элемент ЕХ37С
Дополнительная информация.
Мы надеемся, что эти основы DHTML позволят Вам найти применение этой превосходной технологии в своих приложениях, создаваемых в Visual C++. Преимущества ее огромны: полностью динамические приложения, обновляемые из Интернета приложения, клиент-серверные ActiveX-элементы и многое другое,’ '
За дополнительной информацией обращайтесь к следующим источникам:'
• Inside Dynamic HTML, Scott Isaacs (Microsoft Press, 1997);'
Visual C++для Windows СЕ
В начале 1997 года Microsoft выпустила Windows СЕ Основная категория покупателей этой системы — изготовители оригинального оборудования (original equipment manufacturers, OEM), создающие небольшие портативные устройства [например, карманные компьютеры (hand-held)] и встраиваемые системы. Мириады различных операционных систем, недостаток хороших средств разработки и лабиринт пользовательских интерфейсов замучили рынок портативных устройств и встраиваемых систем. В результате данные проблемы ограничили применение этих систем и возможности создания недорогих приложений.
ПРИМЕЧАНИЕ На момент написания книги поддержка Windows СЕ в Visual C++ 6.0 была еще недоступна. Все экранные изображения и примеры программ в этой главе сделаны с использованием Visual C++ for Windows СЕ версии 5.0.
Microsoft надеется, что Windows СЕ сделает для рынка встраиваемых и карманных компьютеров то же, что Windows сделала для индустрии настольных компьютеров. Исходя из предполагаемой клиентуры, можно считать, что разработка Windows СЕ имела другие цели, нежели Windows 98 и Windows NT. Одной из целей была модульность: если OEM применяет Windows СЕ в устройстве, встроенном в рефрижератор, то ему не требуются клавиатура и устройство графического вывода. OEM не платит за модули, не используемые приложением Windows СЕ.
В настоящее время есть две основные версии Windows СЕ Первая предназначена, прежде всего, для карманных PC (Н/РС) и ограничена нецветными приложениями. В Windows СЕ 1.0 отсутствуют многие дополнительные возможности Win32, например, СОМ и ActiveX, многие части GDI и большинство элементов управления. В Windows СЕ 20, вышедшей в конце 1997, добавлена поддержка рада новых устройств, цвета, технологий СОМ и ActiveX, а также виртуальная машина Java.
Прежде чем детально рассмотреть поддержку Win32 в Windows СЕ, давайте изучим некоторые устройства и получим представление о возможных приложениях Windows СЕ.
Устройства Windows СЕ
В настоящее время самыми известными устройствами Windows СЕ являются Н/РС. Их производят HP, Sharp, Casio и др. фирмы. На рисунке 38-1 показан типичный компьютер Н/РС.
Типичное разрешение экрана Н/РС находится в пределах от 480x240 до 640x240 пикселов. Обычно у них есть клавиатура, инфракрасный порт, последовательный порт и микрофон. В стандартное программное обеспечение входят: Pocket Word, Pocket Excel, Internet Explorer, Outlook Express и другие маломасштабные приложения Microsoft.
Рис. 38-1. Типичный карманный персональный компьютер
Более мелкое устройство, шшъа^нсеручной компьютер (Palm-size PC, Р/РС), показанное •та рисунке 38-2, полностью основано на использовании пера и не имеет клавиатуры, Экран также меньше (240x320 пикселов) и в настоящий момент использует для вывода только гра дации серого цвета.
Рис. 38-2. Ручной персональный компьютер
ПРИМЕЧАНИЕ Во время подготовки к публикации этой книги MFC не поддерживала ручные компьютеры. SDK для таких компьютеров и встроенная среда разработки не были включены в состав Visual C++ for Windows СЕ. SDK можно загрузить с Web-узла Microsoft http://www.microsoft.com. .
Вероятно самыми интересными устройствами для Windows СЕ, имеющимися сейчас на рынке, являются остраиваежиеприложения. Например, проигрыватель компакт-дисков от Clarion, показанный на рисунке 38-3, имеет графический интерфейс пользователя, распознавание голоса, поддержку сотового телефона и ряд других возможностей, изменяющих наше представление об электронных устройствах и приспособлениях. В отличие от Windows 95, поддерживающей только процессоры Intel, и Windows NT, поддерживающей только процессоры Intel и Alpha. Windows СЕ поддерживает огромное количество встраиваемых 32-разрядных процессоров, в том числе MIPS, Hitachi и другие. Такая гибкость значительно увеличивает возможности Windows СЕ на рынке встраиваемых устройств. ' ■
Рис. 38-3. Автомобильный проигрыватель компакт-дисков, управляемый Windows СЕ
Так как все эти устройства работают под управлением Windows СЕ, Вы можете писать приложения для них, используя подмножество знакомого Вам API Win32, которое Вы изучали на протяжении всей этой книга. Но прежде чем приступить к исследованию программирования для Windows СЕ при помощи Visual C++, давайте рассмотрим подмножество Win32, реализованное в Windows СЕ.
Windows СЕ и Windows 98/NT
Каждая платформа Windows СЕ (Н/РС, Р/РС и встраиваемые устройства) поддерживает различные подмножества Win32 API в зависимости от загруженных модулей Windows СЕ. Базовая функциональность остается неизменной — GDI, окна, элементы управления и т. д., но некоторые функции пользовательского ввода отличаются. (Например, на Р/РС не имеют смысла функции, относящиеся к клавиатуре.) ,.v '
Поддержка Win32 в Windows СЕ соответствует основной цели создания Windows СЕ: сделать все максимально малым. Везде, где есть дублирующие функций Win32, в Windows СЕ оставлена наиболее общая, Например, вместо реализации TextOuta ЕхтТехгОит-поуеда^жта- ется только вторая, как более гибкая и предоставляющая функциональность обеих.
Другой интересный момент реализации Win32 в Windows СЕ — подцержка только Unicode- функций и строк. Поэтому Вам будет необходимо поместить свои строки в макрос _Т.
На уровне GDI Windows СЕ поддерживает относительно небольшое подмножество того, что реализовано в Windows 95, Windows 98 и Windows NT. Основными нереализованными GDI-функциями являются режимы преобразования координат, дуги, хорды, метафайлы и кривые Безье. Если нужно провести линию, то вместо неподдерживаемых МоиеТо и Linelb г ридется использовать Polyline. Обработка курсора и мыши в Windows СЕ также будет отличаться от того, к чему Вы привыкли на больших системах.
В версию 2.0 добавлены многие возможности, позволяющие Windows СЕ приблизиться к своим «большим братьям», в том числе подцержка цвета, шрифтов TrueType, печати и контекстов устройства в памяти. Множество нюансов реализации GDI хорошо задокументированы в Windows СЕ SDK, который поставляется в составе Visual C++ for Windows СЕ
В Windows СЕ есть также большие отличия в работе с окнами. Возможно, самое большое из них состоит в том, что поддерживаются только SDI-приложения. Поэтому перенос суще- <твующих MDI-приложений в Windows СЕ достаточно труден. Другой интересный факт состоит в том, что нельзя менять размер окон Windows СЕ Из-за наличия целого ряда разрешений экрана Вы должны программно изменять размер окна на основе разрешения дисп- ; ея, а не использовать статические форматы.
В Windows СЕ есть большинство стандартных элементов управления Windows 95, Windows 98 и Internet Explorer 4.0, за исключением поля ввода с форматированием, поля ввода 1Р-ад- ] ieca, расширенного комбинированного списка и быстрой клавиши. Кроме того, Windows СЕ }:водит новый стандартный элемент управления — панель команд (command bar). Панель команд реализует гибрид панели меню и панели инструментов, занимающий значительно меньше места, чем обычные панели меню и инструментов в большинстве настольных приложений, На рисунке 38-4 показан пример панели команд Windows СЕ
I ЕИ» Blit Xlew Format №ьТ^^вв1Ш11|У|||||||||^^-
Рио. 38-4. Панель команд Windows СЕ
В Windows СЕ 2,0 введена подцержка ActiveX и СОМ, но только для внутрипроцессных COM-объектов, таких как ActiveX-элементы. Полностью — с некоторыми особенностями — поддерживаются многопоточность, управление памятью, обработка исключений и многие другие возможности Win32.
Теперь Вы знакомы с основами Windows СЕ и мы можем взглянуть на среду разработки Visual C++ для этой новой операционной системы.
Visual C++for Windows СЕ
Visual C++ for Windows СЕ — это надстройка над Visual C++. При установке она расширяет среду Visual C++ добавлением особенностей, специфичных для Windows СЕ Среди них:
среда эмуляции Windows СЕ на базе Intel;
новые конфигурации сборки для каждого из поддерживаемых Windows СЕ процессоров (MIPS/SH и среда эмуляции);
новые AppWizard дня приложений Windows СЕ;
совместимая с Windows СЕ версия MFC;
совместимая с Windows СЕ версияАТЬ,
средства для удаленного выполнения и отладки приложений Windows СЕ на реальных устройствах.
Одна интересная деталь Visual C++ for Windows СЕ состоит в том, что он также поддерживает старые версии 1.0 и 1.01 Windows СЕ На рисунке 38-5 показаны панели конфигураций операционной системы и процессора, добавленные в Visual C++,
Хотя среда позволяет Вам удаленно запускать и отлаживать приложения на подсоединенном Windows СЕ-устройстве, она также включает очень мощную среду эмуляции Windows СЕ, Эмулятор Windows СЕ (Windows СЕ emulator, WCE) — это программная версия Windows СЕ на базе Intel, которая выполняется на вашем рабочем столе и позволяет запускать и тестировать приложения на своей машине. Конечно, чтобы быть уверенным, что приложение выполняется правильно, нужно протестировать ее на реальных устройствах, но эмулятор берет на себя большинство проблем на ранних стадиях компиляции и отладки. На рисунке 38-6 показана среда эмуляции в действии.।•
Вместе с Visual C++ for WCE поставляется четыре Арр Wizard, специфных для Windows СЕ • WCE ATL COM AppWizard — проект COM-объекта на основе АТЕ;
I • WCE MFC ActiveXControlWizard — проект ActiveX-элементана основе MFC;
WCE MFCAppWizard(dll)—проект DLL на основе MFC; ...
WCE MFC AppWizard (exe) — проект EXE на основе MFC.
MaciepbiAppWizardAnfl Windows CE
Рис. 38-5. Среда Visual C++ (or Window® CE
Мастеры AppWizard для WCE в основном те же, что и их «старшие братья» для Win32, за исключением того, что они имеют различные особенности, относящиеся к операционной системе Windows СЕ, например, справочную среду Windows СЕ На рисунке 38-7 показаны первые три шага WCE MFC AppWizard (ехе). Обратите внимание, что есть только два типа проектов: SDT-проскт и проект на основе диалогового окна. Также заметьте ряд специфичных для Windows СЕ параметров, которые Вы можете выбрать. - .
Рис. 38-7. Windows СЕ MFC AppWizard
MFC для Windows СЕ
Visual C++ for Windows СЕ поставляется с небольшой версией MFC, называемой Mini MFC. Чтобы понять, какие MFC-классы поддерживаются, акакие нет, взгляните на рисунок 38-8 (см. стр. 784-785), где приведена иерархия MFC для Windows СЕ Классы, отмеченные серым цветом, не поддерживаются в Windows СЕ
В Mini MFC добавлено несколько классов, в том числе для панели команд, хранилища объектов (object store) и сокета. Функции Windows СЕ для панелей команд обеспечиваются в классе CFrameWnct.
Вместо реализации метафоры файла Windows СЕ предоставляет хранилище объектов. Несколько новых MFC-классов добавлено для доступа к этому хранилищу:
CCeDBDatabase инкапсулирует базу данных в хранилище объектов;
CCeDBEnum перечисляет базы данных в хранилище объектов;
CCeDBProp инкапсулирует свойство базы данных (свойства базы данных — это элементы данных, состоящие из определяемого приложением идентификатора свойства, идентификатора типа данных и значения данных);
CCeDBRecord предоставляет доступ к записи в базе данных.
Б дополнение к этим классам хранения данных есть класс CCeSochet, который реализован как асинхронный наследник CSocket.
Пример ЕХ38А
Рассмотрим пару примеров, дающих представление о версии Mini библиотеки MFC. В примере ЕХ38А мы создадим базовое SDI приложение, которое рисует текст в окне и выводит диалоговое окно при щелчке пользователем левой кнопки мыши (или при касании экрана на машине Windows СЕ). Данный пример похож на ЕХОбА, поэтому Вы можете сравнить шаги, необходимые для создания одинаковых приложений для Windows 98, Windows NT и Windows СЕ В этом примере мы создадим приложение, имитирующее контроль расходов. Согласитесь, это прекрасный кандидат для работы на переносных компьютерах. В демонстрационных целях мы сфокусируемся на диалоговом окне, которое позволяет пользователю ввести информацию о расходах. На рисунке 38-9 показано приложение, выполняющееся в среде эмуляции.
Рис. 38-9. Приложение по контролю за расходами ЕХ38А
Ниже приведены этапы создания примера ЕХ38А:
Запустите мастер MFC Executable for Windows CEAppWizard и создайте \vcpp32\ ex38a\ex38a. Выберите проект WCE MFC Арр Wizard и согласитесь co всеми параметрами, предлагаемыми по умолчанию. Ниже на рисунке (см. стр. 786) показаны параметры и имена классов проекта,
Выберите конфигурацию WCE x86em Debug. В меню Set выберите команду Set Active Configuration и затем выберите из списка Ex38a-Win32 (WCEx86em) Debug. Щелкните OK Эта конфигурация позволит Вам работать с эмулятором на вашем рабочем столе вместо удаленной отладки с подсоединенным устройством Windows СЕ (Если есть такое устрой-
Иерархия MFC-классов для Windows СЕ
1 полеа-кие диалоговые окна|
W Класстл,огмечсиниеааездочкой. недоступны
Рио. 38-8. Иерархия MFC- классов для Windows СЕ
^ ^ & Я i^ 4 . t5 ^ L>;^ ^ & Я i^ 4 . t5 ^ L>;1»1»Команднаис трокаКоманднаис трокаТВЛЕЗбв8Т8№5КЯе наборызаписейТВЛЕЗбв8Т8№5КЯе наборызаписей1 словари польз-них типов | П оддержка Интернета1 словари польз-них типов | П оддержка ИнтернетаОбъекты графического выводаОбъекты графического выводаПоддержкабазданных * ODBCПоддержкабазданных * ODBCIl списки польз-ких типов СловамIl списки польз-ких типов Словам^массивы польз+их типов 1 Списки^массивы польз+их типов 1 СпискиМенюМенюПоддержка баз данных* ОАОПоддержка баз данных* ОАОСинхронизация iCSyncObject : ;Синхронизация iCSyncObject : ;iCCMalSecfkm :' IiCCMalSecfkm :' ICoKenaWIndows *CoKenaWIndows *I cfeverrt : / {CMutexI cfeverrt : / {CMutexКлассы, неявляю1цкесжпронзводв1Л1КотСОЬ)есС ЛИ Ишернст-сервераВспомогательные классыКлассы, неявляю1цкесжпронзводв1Л1КотСОЬ)есС ЛИ Ишернст-сервераВспомогательные классыСМияротзатмСМияротзатм
^ ^ & Я i^ 4 . t5 ^ L>;
^ ^ & Я i^ 4 . t5 ^ L>;
1»
1»
Команднаис трока
Команднаис трока
ТВЛЕЗбв8Т8№5КЯе наборызаписей
ТВЛЕЗбв8Т8№5КЯе наборызаписей
1 словари польз-них типов | П оддержка Интернета
1 словари польз-них типов | П оддержка Интернета
Объекты графического вывода
Объекты графического вывода
Поддержкабазданных * ODBC
Поддержкабазданных * ODBC
Il списки польз-ких типов Словам
Il списки польз-ких типов Словам
^массивы польз+их типов 1 Списки
^массивы польз+их типов 1 Списки
Меню
Меню
Поддержка баз данных* ОАО
Поддержка баз данных* ОАО
Синхронизация iCSyncObject : ;
Синхронизация iCSyncObject : ;
iCCMalSecfkm :' I
iCCMalSecfkm :' I
CoKenaWIndows *
CoKenaWIndows *
I cfeverrt : / {CMutex
I cfeverrt : / {CMutex
Классы, неявляю1цкесжпронзводв1Л1КотСОЬ)есС ЛИ Ишернст-сервераВспомогательные классы
Классы, неявляю1цкесжпронзводв1Л1КотСОЬ)есС ЛИ Ишернст-сервераВспомогательные классы
СМияротзатм
СМияротзатм
Графический вывод
Гсрс ■J |СС11«|фС
{CRaMO"-1 -/fj I ^CWlrWowPC "^ <
Поддержка элементов управления
Массивы
{ CArtay (шаблонК ^
COWordAtray , , .„"^ i ВОЬАлау: ■«. > -5 л . П
{[^^^^^ЮЗЗЗ
ство, то Вы можете выбрать соответствующую конфигурацию, например, Ех38а - Win32 [WCE SH) Debug в случае HP 620LX.)
Используя редактор диалоговых окон, создайте диалоговый ресурс. Выберите Resource в меню Insert, выберите Dialog и щелкните New. Новому диалоговому окну будет присвоен идентификатор TDDJDIAI.OGI. Измените заголовок окна на The Dialog that Ate Windows СЕ!"М.ожйте изменить размеры окна, чтобы оно было шире, но не очень высокое. (Дисплеи Windows СЕ широкие, но невысокие.)■
Удалите кнопки ОК и CANCEL и добавьте заголовочную кнопку ОК. Поскольку реальные размеры экрана являются платой за использование Windows СЕ, то Вы сможете сохранитьмного местав диалоговом окне, удалив кнопки OKhCANCEL. Функциональность Cancel реализует кнопка закрытия в заголовке окна. Кроме того, Windows СЕ также поддерживает заголовочную кнопку ОК, которую можно создать установкой соответствующего свойства. Откройте редактор свойств диалогового окна, перейдите па вкладку Extended S tyles и установите флажок Caption Bur OK (WCE only), как показано ниже на рисунке. (Возможно, Вам также потребуется устанонить свойство Visible на вкладке More Styles.)
Добавьте элементы управления. Добавьте следующие элементы управления, показанные на рисунке 38-9, и согласитесь с именами по умолчанию:
m статический элемент и поле ввода для имени;
m статический элемент и поле ввода для количества;
m статический элемент и раскрывающийся комбинированный список для типа расходов е группирующий элемент City с тремя переключателями Atlanta, Detroit и Chicago
m группирующий элемент Payment Option с тремя флажками Check, Credit Card и Cash.
Добавьте класс CEx38aDiaIog.Hoc3Le добавления элементов управления дважды щелкните диалоговое окно. ClassWizard определит, что Вы создали диалоговый ресурс и спросит, не хотите ли Вы создать для него класс. Щелкните ОК и согласитесь с параметрами по умолчашно. Будет создан класс CEx38aDtolog.
Запрограммируйте элементы управления. При помощи ClassWizard создайте следующие переменные-члены класса диалогового окна:
я т_pComboBox— для настройки комбинированного списка типов расхода;
Я т-pProgressCtrf- для элемента управления «индикатор продвижения*. ПРИМЕЧАНИЕ Если Вы хотите вспомнить, как программировать модальные диалоговые окна, обратитесь к главе 6.
Теперь добавьте в СЕх38ыО15«!о§\Юп1ш1Ог«1о^дедующий. код для инициализации элементов управления:
BOOL CEx38aDialcg::OnlnltDialog() .1
CDialog: .OnlnitDialogC);
m_pComboBox. AddSt ring (_T( "Travel")) ;
m_pComboBox.AddSt ring(_T("Meal"));
m_pCombo8ox.AddString(_T("Cab Fare"));
m_pComboBox.AddString(_T("Entertainment")); m_pComboBox.AddString(_T("Other”));
m_pProgressCtrl.SetPos(SO); return TRUE;
i
Заметьте: необходимо использовать макрос _Т везде, где есть вставляемые в код строки.
Подсоедините диалоговое окно к классу «вид». Используя ClassWizard, выберите класс CEx38aVieu»K добавьте функцию-член OnLBuUtmDotm.
Напишите код для обработчика OnLButlonDown. Добавьте код, выделенный полужирным:.
void CEx38aView:^nLButtonOownCUINT nFlags. CPoint point) < CWindowsCEOlg dig; dig. DoModalO; CView::0nL8uttonDown(nFlags, point);'
}
Добавьте код в виртуальную функцию OnDrawB exSSaView.cpp. Функция CDCiiTtort- Oul, использовавшаяся в предыдущем примере, не поддерживается в Windows СЕ, поэтому мы вынуждены применить GDC::DrawTexZ.
void CEx38aView::OrD'aw(CDC* pDC) {
CRect rect;
GetClientRect(rect);
pDC->SetTextColor(::GetSysColor(COLOR_WINDOWTEXT)); pDC->3etBkMode(TRANSPARENT);
pDC->DrawText( Г("Щелкните здесь левой кнопкой мыши."), rect, DT_SINGLELINE);
Включите в ex38aView.cpp заголовочный файл ex38aDialog.h. Вставьте оператор
«include "ex38aDlalog.h"
вверху файла ex38aView.cpp после оператора
«include ‘ ex38aView.h"
Соберите и запустите приложение. Приложение ЕХ38А должно выглядеть, как показано на рисунке 38-10. Если все работает удовлетворительно, измените конфигурацию на реальное Windows СЕ-устройство. После этого Вы мажете запустить приложение на устройстве, чтобы убедиться в его работоспособности в реальном мире.
Press the left mouse button here.
Рис. 38-10. Приложение EX38A, работающее в эмуляторе Windows СЕ
Как видно из примера приложения ЕХ38А, программирование для Windows СЕ очень похоже на программирование для Windows 98 и Windows NT. Многие разработчики заинтересованы в переносе имеющихся приложений в среду Windows СЕ. В следующем разделе показано, как взяться за решение этой проблемы.
Пример ЕХ38В
В примере ЕХ38В мы перенесем имеющееся приложение (ЕХОбВ из главы 6) из Windows 98/NT в Windows СЕ
ПРИМЕЧАНИЕ Мы выбрали пример ЕХОбВ потому, что он является SDI-приложением. Если нужно перенести MDI-приложение, то мы рекомендуем сначала преобразовать его в SDI (или в серию SDI-приложений при наличии нескольких представлений документа) и потом перенести в Windows СЕ.
Последовательность действий по конвертированию ЕХОбВ.
Запустите MFC Арр Wizard и создайте проект \vcpp32\ex38b\ex38b. Выберите тип проекта WCE MFC Арр Wizard и согласитесь с параметрами по умолчанию. (Вы могли подумать, что мы скопируем рабочее пространство ЕХОбВ и добавим конфигурацию для Windows СЕ В действительности проще Ha4aTbcnpoeKTaWCEMFCAppWizard,noioMy4TO есть много сложных параметров сборки, которые мастер устанавливает автоматически.)
Используя Windows Explorer, скопируйте файлы ЕХОбВ в каталог ЕХ38В. Скопируйте следующие файлы: Ex06bDialog.h, Ex06bDialog.cpp, ExOtibDoc.h, Ex06bDoc.cpp, ex06b- View.h и ex06bView.cpp.
Добавьте в проект новые файлы. Выберите в меню Project команду Add То Project/Files и добавьте файлы, скопированные на этапе 2.
Скопируйте ресурсы диалогового окна и значков из ЕХОбВ. Выберите в меню File команду Open и откройте \ycpp32\exO6b\exO6b\exO6b.rc. Перетащите в проект ЕХ38В ресурс IDDDIALOG1. Затем перетащите ресурсы значков цветов (ТГЛ_В1АСК, IDIBLUE и т. д.).
Соберите приложение и исправьте все ошибки и предупреждения, выданные компилятором. Вы скопировали основные файлы (классы документа, вида и диалогового окна) из приложения ЕХОбВ в Windows СЕ-приложение ЕХ38В; теперь попытайтесь собрать проект. Вы получите несколько сообщений об ошибках, которые можно исправить следующим образом:
И измените все ссылки на файл ехОбЬ.Ь на ex38b.h;
m убедитесь, что все подставляемые строки используют макрос _Г Например, строка в Ex06bDiaIog.cpp
TRACE(."updating trackbar data members\n );
должна быть изменена так:
TRACE(_T("updating trackbar data mambers\n"));
m преобразуйте все остальные строки в формат Unicode (это наиболее часто встречающаяся проблема при переносе);
Ж вместо неподдерживаемой функции CDC::TextOutb СЕхОбЬУгеи^ЮпРгяитспольз^ DrawText;
CRect rect;
GetClientRect(rect);
pDC->SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
pDC->SetBkMode(TRANSPARENT);
рОС->ОганТехг(_Т("Щелкните здесь левой кнопкой мыши,"), rect, DT_SINGLELINE);
Замените класс «вид», сгенерированный мастером, на настоящий. Откройте файл ех38Ь.срр и выполните глобальные поиск и замену CEx38bDoc на CEx06bDoc и CEx38bView на CEx06bView. Убедитесь, что в ех38Ь.срр включены файлы ex06bView.h и ex06bDoc,h.
Используя редактор диалоговых окон, измените внешний вццдиалогового окна.
В существующем виде диалоговое окно слишком велико по высоте для Windows СЕ. Его можно перестроить следующим образом:■•
н удалите кнопки ОК и CANCEL й используйте заголовочную кнопку ОК (см. п. 4 в примере ЕХ38А);■
Я Переместите индикатор продвижения, ползунки и наборный счетчик ближе к верхнему краю диалогового окна;
m разместите обычный список и древовидный список ближе друг к другу. Для уменьшения места по вертикали разместите статические элементы слева от списков;
m уменьшите размер диалогового окна по вертикали.
Очистите проект. Удалите классы «документ» и «вид», созданные мастером, выбрав их файлы в File View и нажав клавишу Delete.
Соберите и протестируйте пример. Вы легко преобразовали за восемь этапов небольшое приложение для Windows 98/NT в приложение для Windows СК'
На рисунке 38-11 показано приложение ЕХ38В, выполняющееся в режиме эмуляции Windows СЕ,,
Рис. 38-11. Приложение ЕХ38В, работающее в эмуляторе Windows СЕ
ATL и Windows СЕ
В дополнение к Mini MFC надстройка Visual C++ for Windows СЕ предоставляет версию ATI. для Windows СЕ. Поскольку АТЬитаклегкийкаркас,то М1сго8ойнепотребовалосьуменьшатЕ, набор возможностей из-за ограничений на размер. Однако так как некоторые части СОМ не поддерживаются в Windows СЕ 2.0, то это повлияло на реализацию ATL для Windows СЕ
Windows СЕ не поддерживает модель разделенных потакав, поэтому в ATL для Windows СЕ не вошли классы CComApartment, CComAutoThreadModule.CComiCkissFactoryAutoTbretu^t CComSimpleTbreadAHocator.
Windows СЕ также не поддерживает асинхронные моникеры, поэтому в ATL для Windows СЕ не реализованы классы iBindStatusCaUbacklmplH. CBindStalusCailback. Есть также ряд функций-членов ATL-классов, поведение которых отличается под Windows СЕ, о чем сказано и документации по Visual C++ for Windows СЕ
Помимо ATL для Windows СЕ предоставляется также и версия мастера ATL Wizard для Windows СЕ, показанная на рисунке 38-12. У мастера есть только един параметр: использовать MFC или нет.
Аде. 38-12. Мастер WC Е ATL COM AppWizard
При создании ActiveX-элементов для Windows СЕ помните, что они являются двоичными объектами и, следовательно, процессорпозависимы. Поэтому, в зависимости от устройств,
которые Вы планируете поддерживать, Вам может потребоваться несколько версий элемента управления (например, для MIPS и SH).
Дополнительная информация
Windows СЕ — эго прекрасная новая среда для Windows-разработчиков. Возможность применить свои знания по Windows, Win32 и Visual C++- делает Windows СЕ чрезвычайно привлекательной и легкой в использовании. Книга Programming Windows СЕ (Doug Boling, 1998) — прекрасный источник для изучения д еталей Windows СЕ
Microsoft разрабатывает Windows СЕ и связанные с ней технологии в невероятном темпе.
Чтобы не отстать, мы рекомендуем регулярно обращаться к следующим Web-узлам:
http://www.microsoft.com/windowsce. Новости и информация о Windows СЕ, устройствах и средствах разработки. Подписка на полезный информационный бюллетень.
http://www.microsoft.com/nisdn. MSDN (Microsoft Developer Network) содержит «тонны» статей о Windows СЕ
ПРИЛОЖЕНИЕПРИЛОЖЕНИЕААafx_msg void memberFxnQ;afx_msg void memberFxn (UI NT);afx_msgvoid memberFxnQ;afxmsg BOOL memberFxnQ;afx_msg void memberFxn (NMHDR", LRESULT);afx_msg BOOL membcrFxn(UINT, NMHDR", LRESULT);afx_msgBOOL memberFxn(UINT; NMHDR*, LRESULT);’afx_msg void memberFxn(inNT, NMHDR", LRESULT);afx_msg void memberFxn(NMHDR*, LRESULT);afx_msg BOOL memberFxn(NMHDR‘, LRESULT");afx_msg void memberFxnQ;afx_msg void memberFxn (UI NT);afx_msgvoid memberFxnQ;afxmsg BOOL memberFxnQ;afx_msg void memberFxn (NMHDR", LRESULT);afx_msg BOOL membcrFxn(UINT, NMHDR", LRESULT);afx_msgBOOL memberFxn(UINT; NMHDR*, LRESULT);’afx_msg void memberFxn(inNT, NMHDR", LRESULT);afx_msg void memberFxn(NMHDR*, LRESULT);afx_msg BOOL memberFxn(NMHDR‘, LRESULT");
ПРИЛОЖЕНИЕ
ПРИЛОЖЕНИЕ
А
А
afx_msg void memberFxnQ;
afx_msg void memberFxn (UI NT);
afx_msgvoid memberFxnQ;
afxmsg BOOL memberFxnQ;
afx_msg void memberFxn (NMHDR", LRESULT);
afx_msg BOOL membcrFxn(UINT, NMHDR", LRESULT);
afx_msgBOOL memberFxn(UINT; NMHDR*, LRESULT);’
afx_msg void memberFxn(inNT, NMHDR", LRESULT);
afx_msg void memberFxn(NMHDR*, LRESULT);
afx_msg BOOL memberFxn(NMHDR‘, LRESULT");
afx_msg void memberFxnQ;
afx_msg void memberFxn (UI NT);
afx_msgvoid memberFxnQ;
afxmsg BOOL memberFxnQ;
afx_msg void memberFxn (NMHDR", LRESULT);
afx_msg BOOL membcrFxn(UINT, NMHDR", LRESULT);
afx_msgBOOL memberFxn(UINT; NMHDR*, LRESULT);’
afx_msg void memberFxn(inNT, NMHDR", LRESULT);
afx_msg void memberFxn(NMHDR*, LRESULT);
afx_msg BOOL memberFxn(NMHDR‘, LRESULT");
Функции таблицы сообщений в MFC
Обработчики сообщений WM_ COMMAND
Элемент таблицы
Прототип функции
ON_COMMAND(<id>, <memberFxn>) ON_COMMAND_EX(<id>, <memherFxn>) ON_COMMAND_EX_RANGE(<id>,<idLast>, <ntemberFxn>) ON_COMMAND_RANGE(<id>,<idI^st>, <memberFxn>)
ON_UPDATB_COMMAND_UI(<id>, <memberFxn> ) ON_UPDATE_COMMAND_UI_RANGE(<id>,<idLasr>,
<membcrFxn>)
ON_UPDATE_COMMAND_UI_REFLECT (<memberFxn>)
afx_msgvoid memberFxnQ;
afx msg BOOL membcrFxn(UINT) afx_msgBOOL memberFxn(UINT) afx__msg void memberFxn(UINT) afx^msg void memberFxn(CCmdUr); afX_msgvoid memberFxnfCCmdUr);
afx_msgvoid memberFxn(CCmdDI");
Обработчики уведомляющих сообщений от дочерних окон
Элемент таблицы Прототип функции
Уведомления общего тапа
ON_CONTROL(<wNotifyCode>,<id>, <memberFxn>)
ON_CONTROL_RANGE(<wNorifyCode>, <id> <idLasr>,
<mcmberFxn>)
ON_CONTROL_REFLECT(<wNotlfyCode>, CmemherFxr»)
ON .CONTROL,REFLECT_EX (<wNotifyCode>,
<memberRzn>)
ON_NOTtFY(<wNotifyCodc> ,<id>, <mcmberFxn>)
ON NOTIFY_EX(<wNotifyCodc>,<ld>, <mcrnberFxn>)
ON-NOTlFY_EX_RANGE(<wNotifyCode>, <id>, <idLast>,
<memberFxn>y
ON_NOTTFY_RANGE(<wNotifyCode>, <id>, CidLasO,
<memberFxn>)
‘ON_NOTTFY_REFT,FCT(<wNotifyCode>1 <memberFxn>)
ON_NOTIFY_REFLECT_EX(<wNotifyCode>l <memberFxn>)
Элемент таблицы .
Прототип функции
Уведомления от кнопок
ON_BN_CLICKED(<id>, <memberFxn>)
ON_BN_DOUBLECLlCKED(<id>, <memberFxn>)
ON_BN_KILLFOCUS(<id>, <memberFxn>)
ON_BN_SETFOCUS(<id>, <memberFxn>)
Уведомления от комбинированных списков
nfx_msgvoid memberFxnQ; afx msg void memberFxn(): afx m^ void memberFxnQ; afx_msgvoid memberFxnQ;
ON_CBN_CLOSELrP(<id>, <mcmberFxn>) ON_CBN_DBLCLK(<id>, <memberFxn>) ON_CBN_DROPDOWN(<id>, <memberFxn>) ON_CBN _EDITCHANGE(<id>> <memberFxn>) ON_CBN_EDITUPDATE(<id>, <memberFxn>) ON_CBN_ERRSPACE(<id>, <memberFxn>) ON_CBN_KILLFOCUS(<id>, <memberFxn>) ON_CBN_SELCHANGE(<id>, <memberFxn>) ON_CBN_SELENDCANCEL(<id>, <memberFxn>) ON_CBN_SELENDOK(<ld>, <membcrFxn>) ON_CBN_SETFOCUS(<id>, <memberFxn>) Уведомления от списков с флажками
afx msgvoid memberFxnQ; afx_msgvoid memberFxnQ; afx msg void memberFxnQ; afx_msgvoid memberFxnQ; afx msg void memberFxnQ; afx_msgvoid memberFxnQ; afx_msgvoid memberFxnQ; afx_msgvoid memberFxnQ; afx_msgvoid memberFxnQ; afx_msgvoid memberFxnQ; afx_msg void memberFxnQ;
ON_CLBN_CHKCHANGE(<id>, <memberFxn>)
Уведомления от полей ввода
afx_msg void memberFxnQ;
ON_EN_CHANGE(<id>, <memberFxn>) ON_EN_ERRSPACE(<id>, <memberFxn>) ON_EN_HSCROLL(<ld>, <memberFxn>) ON_EN_KILLFOCUS(<id>, <memberFxn>) ON_EN_MAXTEXT(<id>, <memberFxn>) ON_EN_SETFOCCS(<id>, <memberFxn>) ON_EN_UPDATE(<id>, <memberFxn>) ON_EN_VSCROLL(<id>, <mcmberFxn>)
Уведомления от списков
afx_msgvoid memberFxnQ; afx msg void memberFxnQ; afx_msg void memberFxnQ; afx_msg void memberFxnQ; afx_msg void memberFxnQ; afx_msgvoid memberFxnQ ; afx msg void memberFxnQ; afx_msg void memberFxnQ;
ON_LBN_DBLCLK(<id>, <memberFxn>)
ON_LBN_ERKSPACE(<id>, <memberFxn>)
ON_LBN_KTLLFOCUS(<id>, <memberFxn>)
ON_LBN_SELCANCEL(<id>, <memberFxn>) ON_LBN_SELCHANGE(<id>. <memberFxn>) ON_LBN_SETFOCUS(<id>, <inemberFxn>) Уведомления от статических элементов
afx msg void memberFxnQ; afx_msg void memberFxnQ; a& msg void memberFxnQ; afx_msg void memberFxnQ; afx_msgvoid memberFxnQ; afx msg void memberFxnQ;
ON_STN_CUCKED(<id>, <memberFxn>) ON_STN_DBLCLK(<id>, <memberFxn>) ON_STN_DISABLE(<id>, <membcrFxn>) ON_STN_ENABLE(<id>, <memberFxn>)
afx_msg void memberFxnQ; afx msg void memberFxnQ; afx msg void memberFxnQ; afx msg void memberFxnQ;
Обработчики оконных сообщений
Элементтабпицы
Прототип функции
ON WM ACnVATBO
BOOL);
ON_WM_ACTIVATEAPP()
ON_WM_ASKCBFORMATNAME()
afx_msgvoid OnActivatefUINT, CWnd‘,
afx_msgvoid OnActivaleApp(BOOL, HTASK); afx msg void OnAskCbFormatNamefLllNT, LPTSTR); .
on_wm_cancelmodeo ON_WM_CAPTURECHANGED0
ON_WM. CHANGECBCHAINQ
afx_msg void OnCancelModeQ;
afx_msg void OnCapturcChanged(CWnd');
afx msg void GnChangeCbChain(HWND, HWND);
ON_WM_CHAR()
ON_WM_CHARTOITEM()
alx iAsg void OnChar(UlNT„ UINT, UINT);
afx_msg int OnCharTohemflHNT, CListBox', HINT);
ON_WM_CHARTO1TEM_REFLECT()
ON_WM_CHTT.DACTTVATE ()
ON_WM_CLOSE()
ON_WM_COMPACTING()
ON WM_COMPAREITEM()
afx_msginc CharToItem(L4NT, LUNT); afx msg void OnChildAcrivatcQ;
afx_msg void OnClose();
afx_msgvoid OnCompacrlng(UlNT);
afx msg int OnCompareItem(int, LPCOMPARETTEMSTRUCT);
ONWMCOMPAREITEMREELECTO
afx msg int Compareitem (LPCOMPAREITEMSTRUCT);
ONJW'MCONTEXTMENUQ
afx msg void OnContexiMenu(CWnd‘, CPoint);
ON_WM COPYDATAO
afx msg BOOL OnCopvData(CWnd‘, COPYDATASTRUCT");
ONWMCREATEQ
ON_WM_CTLCOLOR()
afx_msg int OnCreatc(LPCREATESTRI)CT);
afx msg HBRUSH OnCtlColorfCDC, CWnd", UINT);
ON _WM_ CTLCOLOR REFLECT()
ON_WM_DEADCHAR()
afx_msg HBRUSH CtlColorfCDC*, UINT); afx_msgvoid OnDeadCharfUINT, UINT, UINT);
ON_WM_DELETEITEM()
afx_msg void OnDeleteltem(int,'
LPDELETElTEMSTRUCT);
ON_WM_DELETE[TEM_REFLECT0
afx msg void Deletehem (LPDBLBTBITBMSTRUCT)
ON_WM_ DESTROY()
ONWMDESTROYCLIPBOARDO
ON_WM_DEVICECHANGE0
afx_msgvoid OnDestroyQ;
afx_msg void OfiDestroyClipboardO;
afx msg BOOL OnDeviceChangefUINT.
• DWORD);
ON_WM_DEVMODECHANGE()
ON_WM .DRAWCLIPBOARDO
ON_WM_D RAWITEM (}
afx_msg void OnDevModeChange(LP'PSTR);
afx msg void OnDrawClipboard();
afic msg void OnDra*vItem(int, T.PDRAWITEMSTRUCT);
ON WM_DRAWITEM_REFLECT()
afx msg void Drawitem (LPDRAWITBMSTRUCT);
ON_WM_DROPF1LESQ
ONJWMJiNABLEQ
afx msg void OnDropFiles(HDROP); afx_msg void OnEnable(BOOL);
ON WM ENDSESS1ON0 >afx msg void OnEiKlScsswn(BOOL);
Элемент таблицы
Прототип функции
ON_WM_ENTERIDLE() on_wm_entermenuloopo ON_WM_ERASEBKGND0 ON_WM_EXITMENULOOP() ON_WM_FONTCHANGEQ'
ON_WM_GETDLGCODF.() ON_WM_GETMINMAXINFO0
afx_msg void OnEnterldle(LIINT, CWnd"); afx msg void OnEiiteiMenuLoop(BOOL); afx. msg BOOL OnEia»eBkgnd(CDC*);
afx_msg void OnExitMenul.oop(BOOL);
alx.msg void OnFontChangeQ;
alx.msg UINT OnGciDlgCodcO;
afx msg void OnGeiMinMuxInfo
(MINMAXINFO*);
ON_WM_HELP1NFO()
ON_WM„HSCROLL()
afx insg BOOL OnHelphifo(HELPINFO‘); afx.msgvoid OnHScrollfUINT, LUNT, CScroliBar');
ON_WM_HSCROLL_REFLECTO
ON_WM„HSCROLLCL(PBOARD0
afx.msgvoid HSimllfUINT, HINT);
afx.msg void OnIJScrollClipboard(CWnd’, UINT, UINT);
ON_WM_ICONERASE3KGND0
ON_WMJNITMENL"0
ON_WMJNITMENUPOPUP0
afx.msg void OnlconEraseBkgnd(CDC);
afx insg void OnlnitMenu(CMenu’);
afx msg void OnlnitMcnuPopuptCMenu*, UINT BOOL);
ON WMKEYDOWNQ
afx_msg void OnKevDownfClNT, UINT, UINT);'
ON_WM..KEYUP()
ON_WM_JCILLFOCUS()
ON_WM_LBbTrONbBECLK0
afx msgvoid OnKeyUp(UlNT, UINT, UINT);
afx msg void OnKillFocus(CWnd'): afx_nisg void OnLBuitonDblCikfUlNT, CPoint);
ON_WM_LBUTTONDOWN 0
GNWM^LBLTTONUP()
ON.WM_MBUTTONDBLCI.K0
afx msg void OnLButtonDowo(UINT, CPoint);
afx_msgvoid OnLButtonUp(UINT, CPoint);
afx_msg void OnMButtonDblCik(UINT,
CPoint);
ON_WM_MBUTTONDOWN0
ON.WM_MBUTTONUP()
ON_WM_MDIACT№TF.()
afx msg void OnMButtonDownfUINT, CPoint); afx_msgvoid OnMBnrronUp(UINT, CPoint); afx msg void OnMDlAclivatc(BOOL, CWnd1, CWnd');
ON_WM„MEASU REITEM ()
afx msg void OnMeasureltemfint, LPMEASUREITEMSTRUCT);
ON_WM_MEASUREITEM_REFLECT()
afx msg void Measureitem
(Ll'MEASU REITEMSTRUCT);
ONWM_MENUCHAR0
afx msg LRESULT OnMenuChar((JINT, UINT, CMenu’);
ON_WM_MENUSF.LECT0•
afx insg void OnMenuSeJect(UINT, UINT, HMBNU);
ON_WM_MOUSEACTIVATE0
afx_insgint OnMouseActivatc(CWnd‘, UINT, UINT);
О N_WM_MOUSEMOVE()
ON_WM_MOUSEWHEEL0
afx_msgvoid OnMouseMovefUINT. CPoint); afx msg BOOL OnMouscWheel(U!NT, short, CPoint);
ON.WM_MOVE()
ON_WM_MOVING0
ON_WM NCACTIVATE()
afx_msgvoid OnMovc(lnt, ini);
afxjnsgvoid OnMovingfUINT, LPRECT); afx_msg BOOL OnNcActivatc(BOOL);
см. след. стр.
Элемент таблицы
Прототип функции
ON_WM NCCALCSIZEQ
afx msg void OnNcCalcSize(BOOL, NCCALCSIZE_PARAMS");
ONWMNCCREATEQ
afx_msg BOOL OnNcCreate (LPCREATESTRUCT);
ON_WM_NCDESTROY()
ON_\VM_NCHITTEST()
О N_WM_NCLBUTTON DBLCLKQ
- afx_msg void OnNcDestroy();
afx_msg UINT OnNcHitTest(CPoint);
afx_msg void OnNcLButtonDblClk(UTNT, CPoint);
ON_WM_NCLBUTTONDOWN0
afx_msg void OnNcLButtonDown(UlNT, CPoint);
ON_WM_NCLBUTTONUPQ
ON_WM_NCMBUTTONDBLCLK()
afx_msg void OnNcLBurtonUp(UINT, CPoint);
afx msg void 0 nNcM В uttonDbl Cl k(UI NT, CPoint);
ON_WM_NCMBUTTONDOWNO
afx_msg void OnNcMButtonDown(UINT, CPoint);
ON_WM_NCMBUTTONUP()
ON_WM_NCMOUSEMOVE()
ON_WM_NCPAINT0
on_wm_ncrbuttomdblclko
afx_msgvoid OnNcMButtonUp(UINT, CPoint); afx_msgvoid OnNcMouseMove(UINT, CPoint); afx_msg void OnNcPaint();
afx msg void OnNcRBuUonDblC'lk(UlNT, CPoint);
ON_WM_NCRBUTTONDOWN0
afx_msg void OnNcRBuitonDown(UINT, CPoint);
ON_WM_NCRBUTTONUP0
ON_WM_PA1NTO
ON_WM^PAINTCUPBQARD0
afx_msgvoid OnNcRButtonUp(LHNT, CPoint);
afx_msg void OnPaintO;
afx_msg void OnPaintClipboard(CWnd", HGLOBAL);
OX_WM_PALETTEC HANGED 0
ON_WM_PALETTEISC4IANG1NGO
ON_WM_PARENTNOTIFY0
ON_WM_PARENTNOTIFY_REFLECT()
ON_WM_QUERYDRAGICON0
ON_WM_QUERYENDSESSION0
ON_W‘M_QUERYKEWPALETTE0
ON_WM_QUERYOPEN()
ON_WM_RBUTTONDBLCLK0
afx_msg void OnPaletteChanged(CWnd*);
afx msg void OnPaIctteisChanging(CWhd*);
afx_msg void OnParentNotify(UINT, LPARAM); afx_msg void ParentNotify(lHNT, LPARAM);
afx_msgHCURSOR OnQueryDragIcon(),- afx_msg BOOL OnQueryEndSessionQ;
afx^msgBOOL OnQueryNewPaletteQ;
afx_msgBOOL OnQuenOpen();
afx_msg void OnRButlonDblClk(UINT, CPoint);
ON_WM_RBUTTONDOWN()
ON_WM_RBUTTONUPQ
ON_WM_RENDERALLFORMATS() ON_WM_RF.NDERFORMAT() ON_WM_SETCURSOR()
afx_msg void OnRButtonpown(UINT, CPoint); afx_msgvoid OnRButtonUp(UINT, CPoint);
afx_msg void OnRenderAllFormatsO;
afx_msg void OnRenderFormat(UINT);
afx msg BOOL OnSctCursor(CWnd’, UINT, UINT);
ON_WM_SETFOCUSQ
ON_WM_SETnNGCHANGE()
afx_msg void OnSetFocus(CWnd*);
afx msg void OnSettingChange(LTNT, LPCTSTR);
ON_WM_SHOWWINDOWQ
ON_WM_SlZE()
ON_WM_8I2ECUPBOARD()
afx_msg void OnShowWindow(BOOL, UINT);
afx_msg void OnSize(UINT, int, int);
afx msg void OnSizeCiipboardfCWnd", HGLOBAL);
Элемент таблицы
Прототип функции
ON_WM_SIZ1NGQ
О N_W M_SPO О LERSTATI JS()
ON_WM_STYLECHANGED0
afx_msgvoid OnSizing(UlNT, LPRECT);
afx_msgvoid OnSpoolerSrarus(UINT UINT);
afx_msg void OnSlyieChangedfim, LPSTYLESTRUCT);
ON WMSTYLEGHANGINGQ
afX_msg void OnSrylcChanging(im, LPSTYLESTRUCT);
ON_WM_SYSCHAR()
ON_WM_SYSCOLORCHANGEQ
ON_WM_SYSCOMMANDQ
afx_msgvoid OnSysChar(UINT, I [NT, HINT);
afx_msg void OnSysColorChangeQ;
afx msg void OnSysCommand(UTNT, LPARAM);
ON_WM_SYS D EADC HAR()
afX_msg void OnSvsDcadChar(UTNT, UINT, UINT);‘
ON_WM_S YS KEYDOWN ()
afx_msg void OnSysKeyDown(UTNT, UINT, UINT);
ON_WM_SYSKEYUPQ
afX_msg void OnSysKeyUp(UINT, UINT, UINT);
ON_WM_TCaRD()
О N_WM_TIM ЕСГ r A NG EQ
ON_WM_TIMERQ
О N_WM_VKBYTOITBM ()
afX_msg void OnTCard(UINT, DWORD);
afx_msgvoid OnTimeChangcf);
afX_msg void OnTimer(UINT);
afx_msgint OnVKeyToItem(UINT, CListBox*, UINT);
ON_WM_VKEYTOITEM_REFI.ECTQ
ON_WM VSCROLLQ
afx_msg in t VKeyToI tem(U INT, UI NT); afX_msg void OnVScroll(UlNT, UINT,
CScrollBar*);
ON_WM_VSCROLL_REFLECT'0
ON_WM_VSCROLLCLIPBOARD()
afx_msg void VScroil(UlNT, UINT);
afx_msg void OnVScrollClipboard(CWnd*, UINT, UINT);.
ON_WM_W1NDOWPOSCHaNGEDQ
afx_msg void OnWindowPosChanged (WINDOWPOSQ;
ON_WMW1NDOWPOSCHANGING()
afx msg void OnWindowPosChanging (WINDOWPOS-);■
ON_WM_WIN!NKHANGE()
afx_msg void OnWinlniChange(LPCTSTR);
Сообщения, определяемые пользователем
Эле мент таблицы
.Прототип функции
ON_MESSAGE(<mcssagc>, <memberFxn>)
afx_msg LRE5ULT mcmberFxn(WPARAM, LPARAM);
ON_REGISTERED_MHSSAGE (<nMessageVariable>,
<memberFxn>)
ON_REGTSTERED_THREADMESSAGE(<nMessagcVariahle>,
<memberFxn>)
ON_THREAD_MESSAGE (<message>, <meniberFxn>)
afx_msg LRESULT memberFxn(WPARAM, LPARAM);
afx_msg void mcmbcrFxn(WPARAM, LPARAM);
afx_msgvoid membcrFxn(WPARAM, LPARAM);
ПРИЛОЖЕНИЕПРИЛОЖЕНИЕББ
ПРИЛОЖЕНИЕ
ПРИЛОЖЕНИЕ
Б
Б
Идентификация классов вовремя выполнения и динамическое создание объектов
Задолго до того, как в спецификацию языка C++ была добавлена информация о типах в период выполнения (runtime type information, RTTI), разработчики библиотеки MFC реализовали необходимый им доступ во время выполнения к имени класса объекта и к положению класса в иерархии. Кроме того, архитектура «документ-вид» и фабрики COM-классов нуждаются в создании объектов класса, который будет известен только в период выполнения. Поэтому команда разработчиков MFC создала на основе макросов встроенную систему идентификации классов и динамического создания объектов, использующую универсальный базовый класс CObject. И несмотря на то что компилятор Visual C++ версии 6.0 поддерживает синтаксис ANSI RTTI, библиотека MFC продолжает использовать первоначальную систему, которая, на самом деле, имеет больше возможностей.
В этом приложении объясняется реализация идентификации классов и динамического создания в библиотеке MFC. Вы увидите в работе макросы DECLARE _DYNAMICDECLARE_DYN- CREATEh им подобные и изучите макрос RUNTIM1_CLASS и структуру CRuntimeClass.
On ределение имени классаобъектавпериодвы пол нения
Если Вам нужно только имя класса объекта, то у Вас не будет проблем, если все классы производны от общего базового класса CObject. Вот как это можно сделать:
class CObject
{•
public:
virtual char* GetClassName() const { return NULL; } К
class CMyClass : public CObject { public:
static char s_lpszClassName[];
virtual char* GetClassName() const { returrt s IpszCIassName; }
}; •'
char CMyClass: :s_szClassName[] = "CMyClass";
Каждый производный класс переопределяет виртуальную функцию GetClassName, которая возвращает статическую строку. Получить действительное имя класса можно, даже если использовать для вызова GetClassNameyKasarenb на CObject. Если определять имя нужно во многих классах, то для экономии времени можно написать макросы. Пусть макрос DECLARE_ CL^SS^AMEвcтавляeт в определение класса статическую переменную-член и функцию GetClassName, а макрос IMPLEMENT'_СЬА55НААЙЩ№дел»ет строку с именем класса в файле реализации
Структура CRuntimeClass и макрос RUNTIME „CLASS в MFC
В реальной MFC-программе вместо статической переменной-член a sJp$zCtassNamenpHM.e- няется структура CRuntimeClass. В этой структуре хранится имя класса и размер объекта; в ней также есть специальная статическая функция CreateObjeci, которая должна быть реализована в соответствующем классе. Вот упрощенная версия CRuntimeClass: structCRuntimeClass {■
char m_lpszClassName[21];
int m_n0b5ectSlze; // используется для проверки памяти
CObject* (*m_pfnCreateObject) ();
CObject» CreateObject();
};
ПРИМЕЧАНИЕ В настоящей MFC-структуре CRuntimeClass есть дополнительные переменные- и функции-члены для перемещения го иерархии классов. Официальная реализация RTT1 в C++ такую возможность не поддерживает.
Эта структура поддерживает не только получение имени класса, но и динамическое создание. В каждом производном от CObject классе есть статическая переменная-член типа CRuntimeClass (если в объявлении класса использован макрос DECLAREJDYNAMICDECLAREJDYN- СКЕАТЕнли DECLARE_SERIAL, а в файле реализации — соответствующий макрос IMPLEMENT), По соглашению имя этой переменной формируется в виде dass<UMnjuiacca>. Например, для класса СМуС/йжтеременная-член будет называться ctossCMyClass.
Если Вам нужен указатель на статический объект CRuntimeClass какого-либо класса, используйте макрос Еи№ЛМЕ_СЕА55рпределенны.й следующим образам:
^define RUNTIME_CLASS(class_name) (&class_name::class##class_naee). ■
Вот пример получения имени класса:
ASSERT(RUNTIME_CLASS(CMyClass)->m_LpszClassName == "CMyClass")
Если же нужно узнать имя класса для объект», то вызовите виртуальную функцию CObject: GetRuntimeClass. Функция просто возвращает указатель на статический объект CRuntimeClass данного класса, так же, как ранее функция GetClassName возвращала строку с именем. Вот как выглядит эта функция для CMyClass:
VI rtual CRuntimeClass* GetRuntimeClass( )
const { raturn &classCMyClass; /
и ее вызов:
ASSERT(pMyObject->GetRuntimeClass()->m,lpszClassNa»e == "CMyClass"):
Динамическое создание объектов
Вы уже знаете, что макросы DECLARE и IMPLEMENT добавляют в класс статический объект типа CRuntimeClass. Если используются макросы DECLARE_DYNCREATEhhm. DECLARE_SERIAL (и со-
ответствующие макросы IMPLEMENT)1}» в класс добавляется статическая функция-член Сгеа- teObject, отличающаяся от CRuntimeClass:; CreaieObject. Вот ее пример;
CObject- CHyClass::CreateObject() {
return new CMyClass;1
)
Ясно, что в классе CMyClass должен быть конструктор по умолчанию. Если класс, генерируемый ClassWizard, поддерживает динамическое создание объектов, то такой конструктор в нем объявляется защищенным.
Теперь взгляните на код функции CRuntimeClassaCreateObject:
CObject* CRuntimeClass: :CreateObject()
return (»m_pfrCreateObject)();
Эта функция делает косвенный вызов функции СгеягеОб/есщелевого класса. Вот пример динамического создания объекта класса CMyClass:
CRuntimeClass* pRTC = RU№fME_CLASS(CMyObject);
CMyClass* pMyObject = (CMyClass*)pRTC->CreateObject();
Теперь Вы знаете, как работают шаблоны документов. Объект шаблона документа содержит три перемен ные-члена типа CRuntimeClass’, которым при создании этого объекта присваиваются указатели на статические переменные-члены CRuntimeClass классов документа, рамки и вида. При вызове CWiMpp-OnFlleNettKapKac приложения вызывает функции Create- Object для этих трех объектов, используя сохраненные указатели.
Пример программы
Ниже приведен код программы, запускаемой из командной строки и создающей динамически объекты двух классов. Обратите внимание, что это не настоящий MFC-код — класс CObject является упрощенной версией одноименного класса из MFC-библиотеки. Текст программы содержится в файле dyncreat.cpp в каталоге \vcpp32\appendb.
«include <stdio.h>
«define RUNTIME_CLASS(class_name) (&class_name::class##class_name)
class CObject;
struct CRuntimeClass
char m_lpszClassNaine[21];
int m_nObjectSize;
CObject* <«m_pfnCreateObject)();
(Object* CreateObjectQ;
};
// это не настоящий абстрактный класс потоку. что в нем нет
// чисто виртуальных функций, однако пользователь не может создавать
// объекты этого класса, так как у него защищенный конструктор class CObject
{
public:
// не чисто виртуальная, так как производные классы необязательноее реализуют
virtual CRuntimeClass* GetRuntimeClass() const { return NULL; }
// Мы никогда не создаем объекты класса CObject, но в MFC мы используем // эту переменную для получения информации об иерархии классов static CRuntimeClass classCObJect; // DYNAMIC
virtual ~C0bjectO {};// должен быть
protected:
CObjectO { printff'CObject constructor\n"); } };
CRuntimeClass CObject: :classCObject = { "CObject”, slzeof(CObject), NULL };
CObject- CRuntimeClass::CreateObjectO
return («m pfnCreateObject)(); // косвенный вызов функции
}
class CAlpha : public CObject
<
public:
virtual CRuntimeClass» GetRuntimeClass()
const { return SclassCAlpha: }
static CRuntimeClass classCAlpha; // DYNAMIC
static CObject- CreateObjectO; // DYNCREATE
protected:
, CAlphaO { printf( "CAlpha constructor^"); }
CRuntimeClass CAlpha::classCAlpha = { "CAlpha", sizeof(CAlpha), CAlpha::CreateObject };
CObject* CAlpha::CreateObject() // статическая функция
{
return new CAlpha;
class CBeta : public CObject
{
public:
virtual CRuntimeClass* GetRuntimeClassO
const { return SclassCBeta; }
static CRuntimeClass classCBeta;// DYNAMIC
static CObject* CreateObjectO;// DYNCREATE
protected:
CBeta() { printf("CBeta constructor\n"); }
CRuntimeClass CBeta..classCBeta = { "CBeta", sizeof(CBeta), CBeta::CreateObject };
CObject»-CBeta: .’CreateObjectO // статическая функция .
{
return new CBeta;
}
int main()
см. след. стр.см. след. стр.printfC'Entering dyncreate main\n");
см. след. стр.
см. след. стр.
CRuntimeGless* pRTCAlpha = RUNTIME_CLASS(CAlpha);
CObject* pObjl =pRTCAlpha->CreateObject():
printf("class of pObjl =: %s\n", p0bj1->GctRuntimeGlass()->m„lpszClassNaroe):
CRuntimed ass* pRTCBeta = RUNTIME_CLASS(O0eta);
CObject* pObj2 = pRTCBeta->CreateObject();
printf("class of pObj2 = ^s\n", p0bj2->GetRuntireClass()->m-lpszClassName).;
delete pObj'1;
■delete pObj2:
return 0;
Предметный указатель
А
ActiveX 11,410,607
ActiveX-документ 752 —и ActiveX-элемент 754 ATL 4, 11, 122 —для Windows СЕ 790 —добавление свойств 621 —добавление соединений 593 —дуальный интерфейс 606 —иМРС 590, 615 —отрисовка 617 — поддержка DHTML 775 —реализация точек соединения 634 — создание обработчика сообщения 619 —таблица интерфейсов 598, 606 —таблица объектов 625 —таблица свойств 629, 632 —таблица соединений 593 —таблица сообщений 615 —таблица точек соединения 637 —устойчивость свойств 632
В
blobs 641
С
CLS1D 137 СОМ 122, 411 —BSTR 452 -OLE32.DLL 545 -VARIANT 451 —агрегирование 440 — архитектура 430 — взаимодействие клиент-компонент 428, 431 — вложение 439 —внешний компонент 426 — внутренний компонент 426, 428 — внутрипроцессный обработчик 538 —выделение памяти 517
—достоинства 411
- и HTML 770
-mOLEDB 682
— и каркас приложения 470
— и Реестр Windows 427
—идентификатор интерфейса 599
— идентичность объектов 414, 577
— интерфейс 412
—класс 417
— класс объекта 418
—маршалинг 430
—модель программирования 575
—моникер 731
— отладка компонента 462, 464
—перечислители 474
— подсчет ссылок 418, 584
—получатели уведомлений 572
—посредник 544
—посредник клиента 546
—реализация в MFC 420
— регистрация в Реестре 490
—составные файлы 515■
—структурированное хранилище 496
—уведомления 497, 547
—устойчивый объект 524
— фабрика класса 418
D
DAO 640
—динамические множества 665
—для ядра Jet 663
—закладки 665
-и ISAM 665
— и Microsoft Access 665
-и ODBC 665
—параметризованные запросы 666
— снимки 665
DHTML 766
—модель объектов 767
— наборы 769
DIB-секция 183
DLL 4
-ISAPI 738
отладка 743
-MFC 29
-базовый адрес 381
-заглушки 430, 590
—заместителя 430, 590
—неявное связывание 383
— обращение к функциям MFC 390
—отладка 464
-экспортируемые функции 381
-явное связывание 383
DNS 706
Dobjview 514
DocFile Viewer 534
Е
extern «С» 382
G
GDI 3,56
-DIB-секция 183
—PALETTERGB 62
-RGB 61
-выбор объекта 59
-высота символа 65
—кисть 59
трафаретная 72
—контекст устройства 56, 60, 343
в памяти 172
создание и уничтожение 57
состояние 58
координаты устройства 44
—курсор 75
—логическая палитра 180
—логические координаты 44
—метафайл 537
— палитра 59
— перо 59
—подсистема шрифтов 68
—преобразование координат 44
переменный масштаб 46
постоянный масштаб 45
правила 48
формулы 46
растровое изображение 58, 171, 193
на кнопках 193
пикселы 181
цветовая таблица 182
— регион 59
—системная палитра 180
— создание шрифта 69
—составной цвет 61
—стандартные объекты 60
ширина символа 64 — шрифт 59, 71 TrueType 63 GUID 427
н
HTML 709
Dynamic 766
и СОМ 770"
11 IDL 430, 591 — атрибуты 601•
—добавление интерфейса 605 IIS 733 — виртуальный каталог 735 —основной каталог 735 —регистрация соединений 736 Internet Explorer 32, 138, 144, 238, 250 — стандартные элементы управления 144 IP-адрес 703 —локального хоста 712 — по умолчанию 723 — формат 705 IP-протокол 703 ISAPI 733 -DLL 738 отладка 743 — расширение 736 отладка 743 — фильтр 747 J
JScript 767
м ■
MDI 23
MFC 2, 11, 122 — «клонирование» функций 364 —DDX/DDV-функции 91 -DLL 29 — GDI-объекты 59 — RFX-функции 653 — архитектура «документ-вид» 19 — базовые классы 18, 56, 255, 420
—взаимодействие документ-вид 268
—вложенные классы 432
—возможности 131
—вызов из обычной DLL 390 -диагностические макросы 31, 272 —диагностический вывод 273, 285 —диалоговая панель 669
—диалоговое окно 402
—для Windows СЕ 782
—достоинства 12«
—и АТТ 590,615
-и ODBC 651
-и Win32 4, 75
— и Winsock 712
и меню Windows 214
и обработка сообщений 18, 19, 35, 44
константы препроцессора 386
модули 386
обработка сообщений
—и ClassWizard19
-оконный подкласс 274
—поддержка DHTML 770
поддержка drag-and-drop 321, 510
—поддержка агрегирования 440
поддержка работы с файлами 270
поддержка уведомлений об изменении ■свойств 142
предварительный просмотр 342
—приложение 305
реализация функций-членов 97
—ресурсы 29
—таблица диспетчеризации 448
—таблица идентификаторов интерфейсов 432
—таблица интерфейсов 410
—таблица получателей событий 130, 142
—таблица синтаксиса 738
——необязательные параметры 742
—таблица сообщений 34, 92, 236
структура 35
Microsoft Access 641
Microsoft Excel 442, 455, 475
— библиотека типов 483
— объекты 484.
Microsoft Word 23, 65, 323, 366
MIDL 430, 477, 591
О
ODBC 640
-и DLL 642
-и MFC 651
—источник данных 642
— курсор 643, 657
—обработка исключений 646
— подсчет записей 645
■— связь 642
— среда 642
ODL 476
OLE 410
— drag-and-drop 510
—активизация по месту 535
— внедрение 536
—внутрипроцессный обработчик 545
— компонент 410
— контейнер 5 35
-мини-сервер 535
—полный сервер 536
— посредник клиента 546
—работа с буфером обмена 500
—связывание 536
—уведомления 547
OLE DB
-и СОМ 682
—источник данных 686
— команда 687
— компоненты 689
— набор строк 688
—пользовательская запись 698
—поставщик 683
—потребитель 682
—сеанс 688
—таблица параметров 692
—таблица столбцов 692
OLEVIEW 492
Р
Print Preview 44■
R
Rcgsvr32 124
RPC 430■
s
SDI 23
smart-указатель 450, 582
SPY++ 160, 209. .
SQL 640
•—оператор 643
—параметризованный запрос 661. ■
SSL см. уровень защищенных сокетов
ГСР/1Р 705
-Winlnet 725
—Winsock 712
—стандартный шлюз 708
Felnet 724
и
UDP-протокол 704
JDT
—аспект данных 496
источник данных 498
носитель данных 496
-объект данных 495
V'
VARIANT 126, 130
VGA 61
Visual Basic 441
-передача параметров 463
Visual C++ 2, 323, 366
-AppWizard 9, 23, 32, 41, 51, 237, 244, 269,
271, 311, 325, 357, 371, 385, 402, 569
создание меню 216
-ATL COM AppWizard 590
-ATL Object Wizard 591, 608, 690
-ClassView 27, 595
-ClassWizard 9, 35, 41, 49, 51, 78, 138, 2 1 8,
235, 271, 396, 402, 449, 460
и диалоговое окно 86, 88
и пользовательские сообщения 114
и стандартные элементы управле
ния 97
и таблица сообщений 92
—FileView 26
—Resource View 172
-WizardBar 43
—для Windows СЕ 780
—интерактивная документация 10
—компилятор 7
параметры 32
—компилятор ресурсов 172
—компоновщик 3, 8, 169, 172
—контроль исходного кода 11
—конфигурации сборки 30
—отладка DLL 464
—отладчик 8
—предкомпиляция 31- .
—проект 5
просмотр исходного кода 9
рабочее пространство б
редактор быстрых клавиш 218
редактор диалоговых окон 29, 78
и ClassWizard 86
палитра элементов управления 81
редактор значков 104
редактор исходного текста 7
—редактор меню 213, 218
редактор ресурсов 7
собственные DDX-функции 91
—утилиты 10
W
Win32 2
-и MFC 4,75
—и вложение диалоговых окон 116
и память GDI 59
и переменные состояния 36
куча 160
—параметры wParam и IParam 115
поддержка в Windows СЕ 779
—разделяемая память 168
сегментные регистры 164
—управление памятью 160
Windows 2
-COMCTL32.DLL 97, 144
COMDLG32.DLL 109, 115
—DlB-секция 183
DLL 381
-drag-and-drop 321, 329
Explorer 99
—MDI 323
-OLE32.DLL 430
—PALETTERGB 62
-RGB 61, 172
-SDI 300
— Unicode 258
—Video for Windows 192
— анимация 179
— буфер обмена 168, 495
■и объект данных 499
— быстрая клавиша 29, 214
— взаимодействие между модулями 411
—виртуальная память 163, 165
— всплывающие подсказки 240
—диалоговое окно 3, 29, 77, 271, 402
DLU 81
модальное 77
немодальное 77, 109
ресурс 78
системное модальное 77,
стандартное 115
—значок 3, 29, 142
—и интрасеть 710
—изображение на экране и принтере 64
— кисть 59
—кнопка по умолчанию 91
контекст устройства 56
в памяти 172
создание и уничтожение 57
состояние 58
контекстное меню 235
координаты устройства 44
—критическая секция 207, 597
— курсор 75
—логическая палитра 180
—логические координаты 44
—меню 3, 29, 213
метафайл 142, 537
—модель программирования 2
— модульность 4
— мьютекс 209
—недействительный прямоугольник 36
—объекты ядра 205
—оконная процедура 44
—оконный класс 44, 393
— оконный подютасс 274
—палитра 59, 180
—панель задач 104, 160
— периоды простоя 200, 240'
— перо 59
— поток кода 196
пользовательского интерфейса 201,209
рабочий 201
— преобразование координат 34, 44, 76
логические твипы 65
переменный масштаб 46
постоянный масштаб 45
правила 48
формулы 46
—процесс 160
адресное пространство 161
куча 161
стек 161 •
хранение констант 169
—размеры дисплея 65
—размеры окна по умолчанию 261
—растровое изображение 3, 58, 142, 171, 238
аппаратно-независимое 171
——и составные цвета 172, 192
на кнопках 193 '
пикселы 181
цветовая таблица 182
— регион 37, 59'
-Реестр 124,257,320,427
—ресурсы 3, 29, 168
—семафор 209
—синхронизация между потоками 202, 205
—системная палитра 180
—событие 205
— сообщения 2
—MDI 325
WM_COPYDATA 168
— — выбор цвета 94
командные 3, 92
особенности обработки 196
от клавиатуры 51
от наборного счетчика 98
от полосы прокрутки 50, 93
палитры 180
■пользовательские 3, 110
создание и уничтожение окна 54
уведомляющие 92, 99, 394
—составной цвет 61
— стандартные объекты 60
— стили окна 257
—страница свойств 222
-таймер 197, 619■
—файл, спроецированный в память 160,167
—фокус ввода 91
—шрифт 59, 71
TrueType 63
—эмулятор СЕ 781
—ядро 161
WinHelp 366
Winsock
-и MFC 712
WYSIWYG 3
А
Автоматизация 441•
—MDI и SDI 446
—библиотека типов 449
—диспетчерский идентификатор 449
—клиент 442
—компонент 442
—контроллер 442, 475.
—необязательные параметры метода 455
—отладка компонента 462, 464
—позднее связывание 490
—применение 446, 456
—раннее связывание 490
— сервер 442
создание объекта 460
—таблица диспетчеризации 472 .
анонимные пользователи 734
архитектура «документ-вид» 19
аутентификация 746'
Б
библиотека типов 447, 449, 476, 601, 634
регистрация в Реестре 490
битовая карта См. растровое изображение
брандмауэр 725
буфер обмена 168, 495
и объект данных 499
быстрая клавиша 29
IB
ввд 19, 22, 356, 474
—базовый класс 20, 217, 271
—и AppWizard 67
—и документ 268, 281;
—и набор записей 651'
—и немодальное диалоговое окно 110
—отрисовка 28
— переменные состояния 35
—поддержка drag-and-drop 510
виртуальная память. 163
внедренный объект *
—и ActiveX-документ 753 '
—и метафайл 547’
—и хранилище 544
—отладка 541
внутрипроцессный обработчик 545
вызов удаленных процедур см.. RPC ■
гипертекст 366
глобально уникальный идентификатор
см. GUID■
д
датаграмма 703
диалоговое окно 3, 29, 77, 271,402
— и Class Wizard 88
— модальное 77
вызов из компонента 475
—немодальное 77, 109
— ресурс 78
— системное модальное 77
— стандартное 115
динамически подключаемые библиотеки см. DLL
динамическое множество 643
диспетчерский идентификатор 128
документ 19
—ActiveX 752
базовый класс 20, 212, 217, 268
HMDI 323
—и Автоматизация 470
и его представления 19
и контейнер 546
—несколько представлений 328, 356
—несколько шаблонов 328, 363
связь с программой 320
сериализация 304
домен 707
дуплексное соединение 705
3
запрос
— CGI 737
— DNS 707
-HTTP 708, 747
SQL 643
значок 3, 29, 142
И
изготовители оригинального оборудования 777
Интернет 702
—сервер 733
основной каталог 735
интерфейс 410
-DAO 663
—lAdviseSink 497, 547
— IBindStatusCallback 731
— IClassFactory 418, 595
—IConnectionPoint 633'
—IConnectlonPointContainer 611, 633
—IDataObject 495, 538, 544. 611
-IDispatch 441,445, 594, 603, 636
—IDropSource 510
—IDropTarget 510
lEnumSTATSTG 516
lEnumVARIANT 474-
— IHTMLDocument2 770
- IHTMLElementCollection 770
-IMalloc .517
— IMarshal 593
— IMoniker 731
—lOleClientSite 536, 546, 612, 754
—IQleCommandTarget 754
—TOIeContainer 546
—lOleControl 611
—lOleGontrolSite 612
—lOIeDocument 754
— lOleDocumentSite 754
— lOleDocumentView 754
—lOlelnPlaceActiveObject 538,611, 754
—lOlelnPlaceFramc 754
—lOlelnPlaceObject 754
lOlelnPlaceObjectWindowkss 611
—lOlelnPlaceSite 754
—TOleObjcct 536, 538, 544, 611, 623 , 754
-IPersistStorage 515, 524, 538, 544,611
—IPersistStream 515, 525
—IPersistStreamlnit 611
-IPrint 754
—IPropertyNotifySink 611
IPropertyPage 624, 626
IPropertyPageSite 626
— IProvideClasslnfo 611
—IQuickActivate 611
—ISpeclfyPropertyPages 611, 624
— IStoiage 515, 544
-IStream 515
—iSupportErrorlnfo 593
—Iiypelnfo 447, 494, 604
—ПУреГлЬ 494
—IUnknown 416, 575, 594
внешний управляющий 440
-IViewObject 546, 611
—IViewObject2 545
IViewObjectEx 618
атрибуты 605
—диспетчерский 477, 634
-дуальный 494, 594, 603
—и полиморфизм 575
—обособленный 595
—обратного вызова 632
интерфейс графического устройства
см. GDI
интрасеть 710
К
каркас приложения 3, 16
архитектура «документ-вид» 19.
—взаимодействие документ-вид 268
возвращенные сообщения 275
временные объекты 60
—вызов DDX в окне свойств 223
—вызов OnlnitialUpdate 50
—ВЫЗОВ OnPrepareDC 49, 75
—динамический обмен данными 91
—документ и приложение 310
—закрытие окна 54
—запуск приложения 305
—и библиотека классов 16
—и меню Windows 214
—и обработка сообщений 19, 44
—и панели элементов управления 237
—и пользовательские сообщения 112
—и пользовательский элемент управления 394
и структура программы 18
и функция WinMain 17
идентификаторы сообщений 214
—множественное представление дан - ных 281■
-наборы 282, 304
обновление представления документа 269
—обработчик команды 215
—обработчик обновления интерфейса 215
—окно представления 212
— определение контекста справки 373
— основное окно-рамка 212
— отключение команды меню 217
—печать 343
признак окончания 343
размеры страницы 349
—поддержка drag-and-drop 321
—поддержка работы с файлами 270
—предварительный просмотр 342
—разделяемое окно 356
динамически 357
статически 360
—распределение сообщений 214
— рисование в окне 28
—сериализация 301
— создание документа 308, 327
—создание окна 54
—таблица получателей событий 130, 142
—таблица сообщений 34, 92, 236
структура 35
—удаление CDC 57
—удаление объекта «окно* 55
—устойчивое окно-рамка 256
карманный компьютер 777
квитирование 705
кисть 59
класс 4
— CAccessor 685
-CArchive 301
—CAsyncMonikerFile 731
—CAsyncSocket 712
-CBitmap 58, 171, 758
—CBitmapBiitton 193, 758
—CBlockingSockct 713
CBrush 59■
—CClientDC 56
—CCmdTarget 420, 433
—CCmdUI 215
-CColorDialog 116
—CComAutoCriticalSection 598
—CComboBoxEx 147
-CComCoCIass 594
—CComControl 612
—CComControlBase 613, 632
—CComDispatchDriver 613
CCommonDialog 115
—CComMuJUThreadModei 593, 597
—CComMultiThreadModclNoCS 597
CComObjectRoot 594
—CComObjectRootBase 596
—CComObjectRootEx 592, 596
—CComPtr 584
-CComQIPtr 587
-CComSingleThreadModel 593, 597
CComTypelnfpHclder 604
— CControlBar 237
—CCriticalSection 207, 598
—CDaoDatabase 664
—CDaoQueryDcf 664
CDaoRecordset 664
—CDaoTableDef 664
CDaoWorkspace 664
-CDatabase 644, 659
—CDataSource 685
—CDateTimcCtrl 145
—CDBException 646
— CDBVariant 662
CDC 27, 36, 45, 48, 56, 343
CDialog 88, 109, 271
CDialogBai 669
—CDialoglmpl 626
— CDib 180, 183, 509
—CDocObjectSeivei 756
—CDocObjectServerltem 755
—CDocTemplate 306, 328, 363
— CDocument 212
- CDumpContext 273
—CDynamicAccessor 685
— CDynamicPaiameterAccessor 685
—CEditVjew 217
—-CEnumerator 685
—CEvent 205
-CFile 301
—CFile Dialog 116
—CFindReplaceDialog 116
—CFont 59
—CFontDialog 116
—CFormView 115, 268, 271
—CFramcWnd 2 1 2, 217 , 246, 356, 405
— CGdiObject 58
—CHtmlStream 740
—CHtmlView 770
—CHttpBlockingSocket 718
—CHttpConnection 726
—CHttpFile 727
—CHttpFUiei . 748
—CHttpFilteiContext 748
—CHttpServer 739
CHttpServerContext 739
—CImageList 99, 147
—CInternetFile 727
—CInternetSession 726
—CIPAddressCtrl 146
—CListCtri 99
—CLogScrollView 348
—CManualAccessor 685
—CMDiChildWnd 267, 325, 356
—CMDIFrameWnd 328, 364
— CMenu 235
—CMessageMap 614
—CMetaFileDC 56
—CMonthCalCtrl 146
—CMultiDocTemplate 325
CMultipleResults 686
CNoMultipleResults 686
—CObArray 342
—CObject 271, 273, 420
—CObList 282
—COleDataObject 499. .
—COleDataSource 498
—COleDateTime 145, 455. .
COleDispatchDriver 448
—COleDocIPFrameWnd 756
COIeDropSource 510
COleDropTarget 510
—COlelnsertDialog 550
— COlelPFrameWnd 538
COleObjectFactory 433
— COleServerDoc 538, 755
—COleServerltem 538
—COleStreamFile 518
—COLelbmplateServer 472
-COleVariant 129, 453
—CPageSetupDialog 116
—CPaintDC 58
—CPalette 59, 62
-CPen 59
—CPicture 142
— CPicturcIIolder 142
—CPoint 36
— CPreviewDC 343
— • CPiintDialog 116
—CProgressCtrl 98
—CPropertyPage 222
— CPropertySheet 222
—CPrrList 282•
—CReBar 250
—CReBarCtrl 250
—CRecordset 644, 661,
—CRecordView 651, 654
—CRect 28,36,47,350
—CRectTpacker 501
—CRgn 37, 59
—CRichEditCtrl 217
—CRichEditView 217.
— C Row View 667
—CRuntimeClass 418
—CScrollView 34, 49, 75, 76, 271, 342, 343
—CSession 685
—CSharedFiie 168
—CSingleLock 205
—CSize 36
—CSliderCtrl 98
—CSockAddr 713
—CSocket 712.
—CSpinButtonCtrl 98
—CSplitterWnd 356
-CStatusBar 237, 244
-CStdioFile 523
-CString 116, 168, 259
и BSTR 454,
-CStringArray 345
— CSyncObject 205
- CTable 686
—CTime 145
-CToolBar 237
—CTreeCtrl 99
—CTypedPtrList 284
—CVicw 27, 49, 343
—CWinApp 17, 200, 240, 258, 305, 325, 328, 371, 390..
—CWindowDC 56
—CWindowImpl 613
—CWindowImplBase 615
-CWinThread 201
—CWnd 27, 48, 237, 275, 407
контейнеры и посредники 137
—IConnectionPointContainerlmpl 636
—JConnectionPointJmpl 593, 636
—IDispatchlmpl 604
IPropertyPagelmpl 626-
—IViewObjectExImpl 618
класс объекта 418
клиент 412, 703
EXE 430
—Автоматизации 442
—базы данных 684
и контейнер 752'
на базе MFC 434.'
кнопка по умолчанию 91
компонент 410
—Автоматизации 442
активизация по месту 535
—внедрение 535
—внешний 426, 430.
внутренний 426
и модальное диалоговое окно 475
и хранилище 544.
—связывание 536
компонентный объект 418
контейнер 123. .
внедрение <536.
и клиент 752 ■
и хранилище 544
и элемент управления 612, 633
•812
ПрограммированиенаVisual C++6.0
—посредник ActiveX-элемента 137
—связывание 536
контекст устройства 27
—В памяти 172
—создание и уничтожение 57
—состояние 58
контекстное меню 235
контроллер 442
конфигурации сборки 30, 272
критическая секция 207, 597
курсор 75
—MoveElementTo 516
—OnCiose 549
—OnGetExtent 543
— OnShowWindow 547, 549
— OnVlewChange 549
— OpenStorage 517
.— OpenStream 517
— QueryGetData 497
—Queryinterface 416, 440, 543, 578
-Read 519
— Release 417
м
—RenameElement 517
-Revert 517, 519
маршалинг 430
маршрутизатор 704
массивные двоичные объекты см. blobs
меню 3, 29, 213
метафайл 142, 537
метод 123
—_NewEnum 474
—AddRef 417
—Advise 548
—Apply 631
—BindToObject 731
— BindToS torage 731
—Clone 518
-Close 544, 548
-Commit 516, 519
—CopyTo 516, 518, 533
—Createlnstancc 418, 446, 472, 543
— Creates torage 516
—CreateStream 516
—DestroyElement 516
-DoVerb 544, 548, 627
—Draw 549
—EnumElements 516
— EnumFormatEtc 497
—GetClassID 524, 525
— GetContainer 549
—GetData 497
—GetExtent 548
—GetIDsOfNames 445, 604
—GetMaxSize 525
—GetPages 629
—GetTypelnfoOfGuid 604
—InitNew 524
—Invoke 445, 604
—IsDirty 524, 525
—Load 524, 525
-Save 524,525
—SaveObject 547, 549
-Seek 519
— SetAdvise 549
—SetClass 517
—SetClientSite 547, 548
—SetData 497
—SetExtent 548
— SctHostNames 548
— SetObjects 627
— SetPageSite 626
— SetSize 519
—Show 629
-Stat 517, 519
—Unadvise 548
-Write 519
мини-сервер 535
многодокументный интерфейс см MDT
модель многокомпонентных объектов см.
СОМ
модель потоков 592, 597
модель программирования
-СОМ 575
-DOS 2
—Windows 2
—модульность 4
—обычный C++ 412
моникер 731
н
набор записей 644
набор свойств 534
набор строк 643, 683
наследование интерфейса 577
наследование реализации 577
номер порта 704
О
объект данных 498
— и буфер обмена 499
объект-уведомитель 613
объекты доступа к данным см. DAO
однодокументный интерфейс см. SDI окно
—и область вывода 50
— клиентская область 36
—отрисовка 36
— размеры по умолчанию 261
—стили 257
окно-рамка 308, 326, 405
—основное 18
олицетворение 734
оператор
—> 582
— delete 166
-LPCRECT 37
—new 166
— области видимости 75, 415
—присваивания 585
открытая связь с базами данных см. ODBC
п
палитра 59
панель инструментов 29
передача параметров 382
перо 59
подразделение 591
подсказки 745
порядок байтов 705
поток данных 515
поток кода 196
—пользовательского интерфейса 209
—рабочий 201
— синхронизация 202, 205
предварительный просмотр перед печатью см. Print Preview
представление см. вид
приложение
— на базе диалогового окна 404
—список шаблонов документов 328
проект 5
—IDL-файл 594, 600
-make-файл 6
-библиотека импорта 1
-конфигурации связки 30 272
—параметры компилятора 32
—сборочный файл 6
—файл MninFrm.cpp 212, 237
—файл MainFrm.h 212, 237
—файл resource,h 29, 112
-файл StdAfx.h 32, 129, 142, 434
—файл ресурсов 29
прокси-сервер 725
Р
растровое изображение 3, 58, 142
— и составные цвета 192
— на кнопках 193,758
—цветовая таблица 182
растровые изображения 616
расширенное имя функции 382
регион 59
ресурсы 3, 29
быстрая клавиша 29
—версия 29
—диалоговое окно 3, 29, 78
—значок 3, 29
—курсор 75
-меню 3, 29, 213
—панель инструментов 29
—растровое изображение 3
—строки 3, 29
ручной компьютер 778
С
свойства
предопределенные 610 .
—родительские 612
свойство 123
связывание и внедрение объектов см. OLE сегмент 705
сервер 703
ActivcX-докуменгов 752
DLL 428
-EXE 430, 431
-Web 735
—Автоматизации 442
—баз данных 640, 684
—имен 707
—Интернета 733
основной каталог 735
—мини- 535’
—полный 536>.
—прокси- 725
серверный элемент 541
сериализация 300
сетевые протоколы 702
CGI 736
FTP 710
—gopher 725
—HTTP 708
IP 703
-IPng 705
-TCP 705
—UDP 704
—клиент 703
—сервер 703
—стек 702
система доменных имен см. DNS
скрытый тэг ввода 742
служба 733
снимок 644
событие 124, 205
сокет 705
сообщения 2
—выбор цвета 94
—командные 3, 92
—особенности обработки 196
—от клавиатуры 51
—от наборного счетчика 98
—от полосы прокрутки 50, 93
—палитры 180
—пользовательские 3, ПО
—создание и уничтожение окна 54
—уведомляющие 92, 99, 394
составной документ 544
страница свойств 222
—посредник 626
—создание 623
СУБД 640
—достоинства 641
т
таблица виртуальных функций 19,415
таймер 197, 619
текст с форматированием 366
Текущая запись 645
унифицированная передача данных gm-UDT
у ровень защищенных сокетов 747
устойчивость 524. см. сериализация
Ф
фабрика класса 418, 578
файл
-BMP 172, 182
-HOSTS 711"
-HTML 137, 143
-IDL 594
—ODL 476
REG 490
—resource.h 213
—ассоциации в реестре 320
— заголовочный 16
компоновки 169
подкачки 164.
—проекта б
—рабочего пространства 6
—реализации 16
—ресурсов 7, 29
—сборочный 6
— составной 515
— спроецированный в память 160, 167
фокус ввода 91
•функция
—_heapmin 167
—_malloc_dbg 167
—accept 715
—AfxBeginThread 202
—AfxEnablcControlContainer 129
—AfxGetApp 240
—AfxGetlnstanceHandle 387
—AfxGetResourceHandle 387
—AfxInltRichEdit 218
—AfxLocklbmpMaps 469
—AfxMessageBox 121, 259, 374
—AfxOlelnit 438, 475
—AfxOleLockControl 130
—AfxOIeUnlockControl 130
—AfxRegisteriypeLib 491
—AfxSetResourceHandle 387
—AfxUnlockTbmpMaps 469
-BeginPaint 28, 58
-bind 715
-BitBlt 193,618
— closesocket 715
—CLSIDFromProgID 427
—CoCrcateFreeThreadedMarshaler 593
— CoCreatelnstance 427,'592
— CoFreeUnusedLibraries 469
— CoGetCl ass Object 426, 543
— Coinitialize 592
— CoInitializeEx 592
— connect 716
— CoRegisteiClassObject 428, 433
— CreateDIBitmap 183
— Create DIBSection 171, 182, 183
—CreateFile 168, 301
—CrcateFileMapping 168
—CreateHalftonePalette 181
— CreatefiatchBrush 95
—CreateSolidBrush 95
CreateStdDispatch 447
—CreateWindow 44
DeleteCriticaiSection 207
DispatchMessage 197
—DllGetClassObject 430, 625
DllMain 384
— DlIRegisterSeiver 434
-DrawDibDraw 192
—EndPaint 58
— EnterCritlcalSection 207
—FindFirstFile 523
-FindNextFile 523
—FindWindow 209, 305
-fopen 301
—free 166
— GetActiveObject 446, 484
—GetDIBits 183
—gethostbyaddr 717
—gethostbyname 717
— GetMessage 196
-GetModuleHandle 384, 387
—getpcername 717
—GetProcAddress 383
— getsockname 717
—GlobalAlloc 166
— GlobalLock 166
— GlobalReAlloc 166
—HeapAlloc 166
—HeapCompact 167
—HeapFrce 166
—HttpOpenRequest 727
—InitializeCriticalSection 207
—InterlockedDecrement 593
—Interlockedlncrement 203, 593
— InternetConnect 1T1
InternetOpen 726
—LeaveCritlcalSectfon 207
—listen 715
—LoadCursor 75
—Loadimage 191
LoadLibrary 383
—LoadResource 168
— LoadiypeLib 494, 604
— inalloc 166
—MapViewOfFile 168
— OleCreate 549
—OleCreateFromData 550
OleCreatePropertyFrame 624
—OleDraw 550
-OleGetClipboard 499
—Olelnitialize 592
—OleLoad 550
—OleSave 550
OleSetClipboard 498
— PeekMessage 197
—RealizePalette 180
—recv 716
—recvfrom 717
—RcleaseCapture 75
— SelectPalette 180
—send 716
— SendMessage 95
—send to 716
—SerializeElements 350
— SetCuisoi 75
—SetDIBitsToDevice 183
-Sleep 207
—socket 715
—SQLBrowseConnect 685
—SQLDriverConnect 685
—SQLGetData 661
—SQLPutData 661
—StgCreateDocfile 517
StgOpenStoiage 517
—StretchDIBits 183
SysAlIocStiing 452
— SysFreeStiing 452
—lerminatcThread 207
—TranslateMessage 197
—TransmitFile 723
—UpdateRegistryAU 459
—VariantClear 452
—VariantCopy 453
—Variantlnit 452
—VirtualAlloc 165
—VirtualFrcc IS
—WaitForMultiplcObjects 206
—WaitForSingleObject 205
-WinMain 2, 305
и каркас приложения 17 функция-член
—ActivateFrame 256, 328
—AddDocTemplate 306, 325
—AddNew 645
—AddPage 222
—AtcachClipboard 499
—AttachDispatch 482
—AutoLoad 193
— BcginEhtmiFonnats 499
-BitBlt 172
—CacheData 498
—CacheGlobalData 498
-Create 18, 109, 138, 217 , 245, 613
— CreateCompatibleDC 172
— Crc a tcControl Window 612
—CreateDlspatch 479
—CreateEllipticRgnlndirect 37
—CreateFont 59
—CreatePolygonRgn 59
—CreateStatic 360
—CreateView 360
—DefWindowProc 275
— Delete 645
-DeleteContents 282, 308, 328
—DestroyWindow 113
— Detach 235
—DoDataExchange 90
—DoDragDrop 498, 510
—DoFieldExchange 645, 653, 670
-DoModal 90, 109, 222, 404
—DoVerbProperties 627
—DPtoLP 48
—DragAcceptFiles 321
— Draw 501
-Edit 645
— Ellipse 28
-Enable 239t
—EndDialog 91
—Exitinstance 305
— FinalConstruct 593
—FiieOnChanged 612
FireOnRequestEdit 612
FireViewChange 620
—Formal 260'
—GctAt 284
—GetBuffer 260
GetClientRect 36
GetClipBox 36
— GetCurrentRecord 665
•GetData 499
GetDC 57
GetDefaultConnect 645, 652
GetDefauitSQL 645, 652
GetDeviceCaps 64
— GctDlgCtrllD 94
—GetDlgltem 94
—GetDocument 213, 268
—GetFieldValue 661
—GetGlobalData 499
—GetHttpConnection 726
— GetlDsOfNames 490
—Getlnteiface 561’
—Getltem 107
—GetMenu 235
—GetModify 218
—GetNext 283
—GetNextFormat 499
—GetNextView 269
—GetObject 174
—GetODBCFieldCount 645
—GetODBCFieldlnfo 645
—GetParentFrame 240
— • GetPathName 116
—GetPos 98
—'GetProperty 448
— • GetReBarCtrl 250
—GetRecordCount 645
—GetSafeHandle 60
—GetSel 218
— GetStatus 645
—GetSubMenu 235
—GetSystemMetrics 261
— GetTextExtent 64
—GetTime 145
— GetWlndowPlacemcnt 261
— GctWindowRect 260
—GetWindowTfext 123, 217
—HitTest 501
—Initlnstance 18, 201, 258, 305, 308, 404
—Insertitem 99
— IntersectClipRect 178
—Invalidate 27, 96
— InvalidateRect 36
— InvokeHelper 448
— IsBOF 645
- OnUpdate 269, 281—OnWindowNcw 364— Open 6'15—OpenDocumentFile 308— OpenRequcst 727—PatBlt 178— PostMessage 110,214—PostNcDesnoy 55, 396—PreCreateWindow 257— PtlnRect 37— PtlnRegion 37-Read 729Rectangle 44Register 510-RegisterAll 433RcgistcrShellFile'iypes 320—ReglsterWndClass 396ReleaseBuffer 260-ReleaseDC 57RclcascDispatch 479Requery 657ResumeThread 202— Run 18Save BarState 261— SavcModificd 54, 310— ScrollWindow 50— SelcctClipRgn 178— SelectStockObject 60— ScndMcssage 110,214— Serialize 300— SetAccel 98— SetAddress 146— SetBitmap 758—SetBkColor 62, 172—SetBrushOrg 76—SetCapture 75— SctCheck 239— SecClipboard 498— SetCursor 502—SetDefaultCharFormat 218—SetEvent 205— SetFormat 145— Setindicators 244— SetMenu 235—SetModify 218— SetPaneText 244— SetPos 98SetProperty 448SetRange 98- OnUpdate 269, 281—OnWindowNcw 364— Open 6'15—OpenDocumentFile 308— OpenRequcst 727—PatBlt 178— PostMessage 110,214—PostNcDesnoy 55, 396—PreCreateWindow 257— PtlnRect 37— PtlnRegion 37-Read 729Rectangle 44Register 510-RegisterAll 433RcgistcrShellFile'iypes 320—ReglsterWndClass 396ReleaseBuffer 260-ReleaseDC 57RclcascDispatch 479Requery 657ResumeThread 202— Run 18Save BarState 261— SavcModificd 54, 310— ScrollWindow 50— SelcctClipRgn 178— SelectStockObject 60— ScndMcssage 110,214— Serialize 300— SetAccel 98— SetAddress 146— SetBitmap 758—SetBkColor 62, 172—SetBrushOrg 76—SetCapture 75— SctCheck 239— SecClipboard 498— SetCursor 502—SetDefaultCharFormat 218—SetEvent 205— SetFormat 145— Setindicators 244— SetMenu 235—SetModify 218— SetPaneText 244— SetPos 98SetProperty 448SetRange 98
- OnUpdate 269, 281
—OnWindowNcw 364
— Open 6'15
—OpenDocumentFile 308
— OpenRequcst 727
—PatBlt 178
— PostMessage 110,214
—PostNcDesnoy 55, 396
—PreCreateWindow 257
— PtlnRect 37
— PtlnRegion 37
-Read 729
Rectangle 44
Register 510
-RegisterAll 433
RcgistcrShellFile'iypes 320
—ReglsterWndClass 396
ReleaseBuffer 260
-ReleaseDC 57
RclcascDispatch 479
Requery 657
ResumeThread 202
— Run 18
Save BarState 261
— SavcModificd 54, 310
— ScrollWindow 50
— SelcctClipRgn 178
— SelectStockObject 60
— ScndMcssage 110,214
— Serialize 300
— SetAccel 98
— SetAddress 146
— SetBitmap 758
—SetBkColor 62, 172
—SetBrushOrg 76
—SetCapture 75
— SctCheck 239
— SecClipboard 498
— SetCursor 502
—SetDefaultCharFormat 218
—SetEvent 205
— SetFormat 145
— Setindicators 244
— SetMenu 235
—SetModify 218
— SetPaneText 244
— SetPos 98
SetProperty 448
SetRange 98
- OnUpdate 269, 281
—OnWindowNcw 364
— Open 6'15
—OpenDocumentFile 308
— OpenRequcst 727
—PatBlt 178
— PostMessage 110,214
—PostNcDesnoy 55, 396
—PreCreateWindow 257
— PtlnRect 37
— PtlnRegion 37
-Read 729
Rectangle 44
Register 510
-RegisterAll 433
RcgistcrShellFile'iypes 320
—ReglsterWndClass 396
ReleaseBuffer 260
-ReleaseDC 57
RclcascDispatch 479
Requery 657
ResumeThread 202
— Run 18
Save BarState 261
— SavcModificd 54, 310
— ScrollWindow 50
— SelcctClipRgn 178
— SelectStockObject 60
— ScndMcssage 110,214
— Serialize 300
— SetAccel 98
— SetAddress 146
— SetBitmap 758
—SetBkColor 62, 172
—SetBrushOrg 76
—SetCapture 75
— SctCheck 239
— SecClipboard 498
— SetCursor 502
—SetDefaultCharFormat 218
—SetEvent 205
— SetFormat 145
— Setindicators 244
— SetMenu 235
—SetModify 218
— SetPaneText 244
— SetPos 98
SetProperty 448
SetRange 98
— IsDataAvailable 499
-IsEOF 645
—IsModified 310
—KillTimcr 197
—LoadBarState 261
—LoadBitmap 172
— LoadMenu 235
-Lock 207
—LPtoDP 48
— MoveFirst 645
— MoveLast 645
— MoveNext 645
— MovePrev 645
—NormalizeRect 47
—On Apply 223.
OnBeginPrinting 344
— OnCancel
и немодальное диалоговое окно ИЗ
OnCommandHclp 378
OnContextHelp 378
-OnCreate 221, 270
—OnCreateClient 359
—OnCtlColor 95
OnDraw 27, 36, 56, 68, 74, 172, 188, 269,
474, 541, 617, 652
—OnDrawAdvanced 618
—OnEndPrinting 344
-OnFileNew 308, 327•
-OnFileOpen 309
—OnFileSave 309
—OnHelp 377
-OnHelpFinder 372
-OnHelpHitTest 378
—Onldle 200
—OnTnitDialog 88, 96, 98, 129, 130, 138
—OnlnitialUpdatc 50, 74, 173, 270, 309, 652
—OnLButtonDown 18, 35, 73
—OnLButtonUp 73
—OnLMouseMovc 73
—OnNewDocumcnt 220, 270, 308, 356, 659
-OnOK 88, 129
и немодальное диалоговое окно ИЗ
-OnPaint 18, 28, 58, 96, 178, 343, 407, 618
—OnPrepareDC 48, 68, 343, 352, 509
класс CScrollView 75
—OnPreparePrinting 344
—OnPrint 28, 343
—OnSize 221
—OnStatusCallback 728
—SetRcgistryKey 258—SetScalcToFitSize 76—SetScrollPos 93—SetScrollSizes 50—SetSelectionCharFormat 218—SetStockObject 28—SetStrctchBltMode 173—Setlfext Color 62, 172—SetTime 145—SetTimer 197-SctTitie 222—SetToday 146—SetViewportExt 46—SetViewportOrg 45— SctWindowExt 46—SetWindowOrg 45, 50—SetWindowPlacement 256—SetWindowPos 217—SetWindowText 218—ShowWindow 18, 256— StretchBlr 172—SubclassDlgltem 275—SuspendThread 202-TextOut 28, 260—Track 501—TrackPopupMenu 235—UlActivate 754-Unlock 207— Update 645-UpdateAUViews 269, 281—UpdateData 91, ИЗ, 129— UpdateRegistry 472— UpdateRegistryAll 428—UpdatcWindow 18, 96-Winllelp 371-WriteProfilelnt 259—WriteProfileString 259Xхост-компьютер 705хранилище 515—корневое 517хранилище объектов 783Шшаблон документа 306, 325— список документов 328шаблоны C++ 284, 581, 684шрифт 59, 71—SetRcgistryKey 258—SetScalcToFitSize 76—SetScrollPos 93—SetScrollSizes 50—SetSelectionCharFormat 218—SetStockObject 28—SetStrctchBltMode 173—Setlfext Color 62, 172—SetTime 145—SetTimer 197-SctTitie 222—SetToday 146—SetViewportExt 46—SetViewportOrg 45— SctWindowExt 46—SetWindowOrg 45, 50—SetWindowPlacement 256—SetWindowPos 217—SetWindowText 218—ShowWindow 18, 256— StretchBlr 172—SubclassDlgltem 275—SuspendThread 202-TextOut 28, 260—Track 501—TrackPopupMenu 235—UlActivate 754-Unlock 207— Update 645-UpdateAUViews 269, 281—UpdateData 91, ИЗ, 129— UpdateRegistry 472— UpdateRegistryAll 428—UpdatcWindow 18, 96-Winllelp 371-WriteProfilelnt 259—WriteProfileString 259Xхост-компьютер 705хранилище 515—корневое 517хранилище объектов 783Шшаблон документа 306, 325— список документов 328шаблоны C++ 284, 581, 684шрифт 59, 71
—SetRcgistryKey 258
—SetScalcToFitSize 76
—SetScrollPos 93
—SetScrollSizes 50
—SetSelectionCharFormat 218
—SetStockObject 28
—SetStrctchBltMode 173
—Setlfext Color 62, 172
—SetTime 145
—SetTimer 197
-SctTitie 222
—SetToday 146
—SetViewportExt 46
—SetViewportOrg 45
— SctWindowExt 46
—SetWindowOrg 45, 50
—SetWindowPlacement 256
—SetWindowPos 217
—SetWindowText 218
—ShowWindow 18, 256
— StretchBlr 172
—SubclassDlgltem 275
—SuspendThread 202
-TextOut 28, 260
—Track 501
—TrackPopupMenu 235
—UlActivate 754
-Unlock 207
— Update 645
-UpdateAUViews 269, 281
—UpdateData 91, ИЗ, 129
— UpdateRegistry 472
— UpdateRegistryAll 428
—UpdatcWindow 18, 96
-Winllelp 371
-WriteProfilelnt 259
—WriteProfileString 259
X
хост-компьютер 705
хранилище 515
—корневое 517
хранилище объектов 783
Ш
шаблон документа 306, 325
— список документов 328
шаблоны C++ 284, 581, 684
шрифт 59, 71
—SetRcgistryKey 258
—SetScalcToFitSize 76
—SetScrollPos 93
—SetScrollSizes 50
—SetSelectionCharFormat 218
—SetStockObject 28
—SetStrctchBltMode 173
—Setlfext Color 62, 172
—SetTime 145
—SetTimer 197
-SctTitie 222
—SetToday 146
—SetViewportExt 46
—SetViewportOrg 45
— SctWindowExt 46
—SetWindowOrg 45, 50
—SetWindowPlacement 256
—SetWindowPos 217
—SetWindowText 218
—ShowWindow 18, 256
— StretchBlr 172
—SubclassDlgltem 275
—SuspendThread 202
-TextOut 28, 260
—Track 501
—TrackPopupMenu 235
—UlActivate 754
-Unlock 207
— Update 645
-UpdateAUViews 269, 281
—UpdateData 91, ИЗ, 129
— UpdateRegistry 472
— UpdateRegistryAll 428
—UpdatcWindow 18, 96
-Winllelp 371
-WriteProfilelnt 259
—WriteProfileString 259
X
хост-компьютер 705
хранилище 515
—корневое 517
хранилище объектов 783
Ш
шаблон документа 306, 325
— список документов 328
шаблоны C++ 284, 581, 684
шрифт 59, 71
э
элемент управления 78
-ActiveX 122, 129, 607
CLSID 137
— —в файлах HTML 137
и ActiveX-документ 754
и AppWizard 129
и ClassWizard 129
и окно 124
регистрация 124
событие 124
события 130
создание 138
создание в диалоговом окне 137
—Calendar 125
— Marquee 143
—rebar 250
-VBX 122
-WebBrowser 138
—анимация 108
—быстрая клавиша 108
—ввод IP-адреса 146
— вкладка 108
—выбор даты и времени 144
—выбор фона и цвета 95
—выравнивание 83
— графический список 97,99, 105
—группирующая рамка 82
—добавление событий 633
—доступ к свойствам 127
-древовидный список 78, 97, 99, 106
—значок 85, 104
—и Internet Explorer 144
—и контейнер 612, 633
—индикатор продвижения 78, 97, 98, 101
—календарь на месяц 146
-класс-оболочка 127
—кнопка 78
—комбинированный список 78, 83, 96
—координаты 81
—набор вкладок 222
—наборный счетчик 97, 98, 103
и поле ввода 98
—отрисовка 616
—панель инструментов 237
—панель команд 780
—переключатель 83
— подключение к оконной процедуре 97
— поле ввода 7%, 82, 217
и наборный счетчик 98
—поле ввода с форматированием 217
— ползунок 78, 97, 98, 102
—полоса прокрутки 85
— пользовательский 393
уведомления 394
—расширенный комбированный спи
сок 146
— свойства 621
-свойства-картинки 142
— связываемое свойство 142
— события 632
сохдание таймера 619
список 78, 96
— список изображений 99, 105, 147
—ссылка на 78, 94
—статический текст 78, 82
знак <6 85
—страница свойств 623
— уведомляющие сообщения 92
-флажок 83
характеристики 123
Я
язык описания объектов см, ODL
язык определния интерфейсов см. IDL
язык разметки гипертекста см. HTML
язык структурированных запросов см. SQL языки сценариев 441
Дэвид Дж. Круглински
Дэвид Круглински был самоучкой и талантли-вым программистом, писателем, учителем и пу-
1947-1997
тишественником.
Его карьера программиста началась в университете Purdue в 1966 году. Он начал писать приложения для микрокомпьютеров в 1976 году, пос
ле того как его приятель выловил материнскую плату с процессором 8080 из мусорной корзины.
Первое издание своей заслужившей широкое признание книги Дэвид написал для Microsoft Press в 1992 году. Сейчас перед Вами пятое издание этого бестселлера.
х !■:.
■г
$
*
Опыт Дэвида как писателя и программиста позволили ему получить работу в составе команды разработки документации для библиотеки MFC-классов в середине 1991 года. В большей части справочного руководства по MFC, особенно в документации П^ базовым классам, таким как CString, по-прежйему чувствуется его „рука * . эксперта. ОЙ остабнл й!НД311Й1зраб©*^ - <’ife^-^w:’jMg^<iM»r^Js^e^wlgS?®®^
; пре ■ -Н
rj ’ • ’Л
ЦГ5 г^
^ t
НИКОЛЯ
не
местах ноот
(Okanogan1)^^
.. - ГШИ №Р9 во ^ ИЯ f^ ®9в Ш^году^ЩЛ I из газ , I № ; 1 > d^sp -t
тал iy Visual® поезда ' СТВИЯГй
не в N
■ 1 ч < Путеп^. ^' ■ ^»ольст1
Г[ачал j 80-?K,f' ; репу
ЛО*,, бил ■ ■ вопи , ЮяКИ ; . • время и вщс' .17 ай
Скотт Уингоу и Джордж Шеферд
Скотт Уингоу работал с C++ и MFC, начиная с версии 1.0 обоих продуктов. После получения степени магистра информационных технологий (Computer Engineering) в университете плата Северная Каролина он начал работать с компании Bristol Technology, Inc., разрабатывавшей средства переноса программ Windows на UNIX. В 1995 году он оставил эту фирму и стал соучредителем Stingray Software — лидера в разработке библиотек классов MFC/ATL, таких как Objective Grid и ObjectiveToolkit. В начале 1998 года Stingray была куплена Rogue Wave Software, и теперь Скотт является вице-президентом, отвечающим за подразделение Stingray.
В период между Bristol и Stingray Скотт написал первую версию всемирно знаменитых MFC FAQ. а также стал соавтором (вместе с Джорджем Шефердом) MFC Internals (Addison-Wesley, 1996) — единственной книги, в которой подробно описано внутреннее устройство MFC. Каки Джон Шеферд, Скотт Уингоу регулярно пишет статьи для журналов Dr. Dobbs, Microsoft Systems Journal и Visual C++ Developer'sjournal.
Когда Джордж Шеферд не занят созданием средств разработки Windows-программ в Stingray Software, он ведет занятия в DcvelopMcntor — одной из лучших обучающих фирм. Вместе со Скоттом Уингоу Джордж написал MFC Internals (Addison-Wesley, 1996). Между компиляциями Джордж любит играть на своей гитаре Paul Reed Smith.
ЛИЦЕНЗИОННОЕ СОГЛАШЕНИЕ MICROSOFT
(прилагаем ый к книге компакт-диск)
ЭТО ВАЖНО - ПРОЧИТАЙТЕ ВНИМАТЕЛЬНО. Настоящее лицензионное соглашение (далее -«Соглашение» ) является юридическим документом, оно заключается мещду Вами (физическим или юридическим лицом) и Microsoft Corporation (далее -«корпорация Microsoft») на указанный выше продукт Microsoft, который включает программное обеспечение и может включать сопутствующие мультимедийные и печатные материалы, а также электронную документацию (далее «Программный Продукт»-). Любой компонент, входящий в Программный Продукт, который сопровождается отдельным Соглашением, подпадает под действие именно того Соглашения, а не условий, изложенных ниже. Установка, копирование или иное использование данного Программного Продукта означает принятие Вами данного Соглашения. Если Вы не принимаете его условия, то не имеете права устанавливать, копировать или как-то иначе использовать этот Программный Продукт.
ЛИЦЕНЗИЯ НА ПРОГРАММНЫЙ ПРОДУКТ
Программный Продукт защищен законами Соединенных Штатов Америки по авторскому праву и международными договорами по авторскому праву, а также другими законами и договорами по правам на интеллектуальную собственность..
ОБЪЕМ ЛИЦЕНЗИИ. Настоящее Соглашение предоставляет Вам следующие права:
Программный продукт. Вы можете установить и использовать одну копию Программного Продукта на одном компьютере. Основной пользователь компьютера, на котором установлен данный Программный Продукт, может сделать только для себя вторую копию и использовать ее на портативном компьютере.
'• Хранение или использование в сети. Вы можете также скопировать или установить экземпляр Программного Продукта па устройстве храпения, например, на сетевом сервере, исключительно для установки или запуска данного Программного Продукта на других компьютерах в своей внутренней сети, но тоща Вы должны приобрести лицензии иа каждый такой компьютер. Лицензию изданный Программный продукт нельзя использовать совместно или одновременно на других компьютерах.
License Рак. Если Вы купили эту лицензию в составе Microsoft License Рак, можете сделать ряддо- полпительных копий программного обеспечения, входящего в данный Программный Продукт, и использовать каждую копию так, как было описано выше, Кроме того, Вы получаете право сделать соответствующее число вторичных копий для портативного компьютера в целях, также оговоренных выше.
Примеры года. Это относится исключительно котдельным частям Программного Продукта,заявленным как примеры года (далее «Примеры»), если таковые входят в состав Программного Продукта.
Использование и модификация. Microsoft дает Вам право использовать и модифицировать исходный код Примеров при условии соблюдения пункта «Требования к распространению файлов» ниже. Вы не имеете права распространить в виде исходного года ни Примеры, ни их модифицированную версию.
- Распространяемые файлы. При соблюдении пункта «Требования к распространению файлов» ■ Microsoft дает Вам право на свободное от отчислений копирование и распространение в виде объект ного кода Примеров или их модифицированной версии, кроме тех частей (или их модифицированным версий), которые оговорены в файле Readme, относящемся к данному Программному Продукту, как не подлежащие распространению.
- Требования к распространению файлов. Вы можете распространять файлы, разрешенные к распространению, при условии, что: а) распространяете их в виде объектного кода только в сочетании со своим приложением и как его часть; б) ие используете название, эмблему или товарные знаки Microsoft для продвижения своего приложения; в) включаете имеющуюся в Программном Продукте ссылку на авторские права в состав этикетки и заставки своего приложения; г) согласны освободить от ответственности и взять на себя защиту корпорации Microsoft от любых претензий или преследований по закону, включая судебные издержки, если таковые возникнут в результате использования или распространения вашего приложения; и д) ие допускаете дальнейшего распространения конечным пользователем своего приложения. По поводу отчислений и других условий лицензии применительно к иным видам использования или распространения распространяемых файлов обращайтесь и Microsoft.
ПРОЧИЕ ПРАВА И ОГРАНИЧЕНИЯ
Ограничения иа реконструкцию, декомпиляцию и дизассемблирование. Вы не имеете права ре конструировать, декомпилировать или дизассемблировать данный Программный Продукт, кроме того случая, когда такая деятельность (только в той мере, которая необходима) явно разрешается соответствующим законам несмотря на это ограничение.
Разделение компонентов. Данный Программный Продукт лицензируется как единый продукт. Его компоненты нельзя отделять друг от друга для использования более чем на одном компьютере.
Аренда. Данный Программный Продукт Нельзя сдавать в Прокат, передавать во временное пользование или уступать для использования в иных целях.
Услуги по технической поддержке. Microsoft может (ио ие обязана) предоставить Вам услуги по тех- иической поддержке данного Программного Продукта (далее «Услуги»), Предоставление Услуг регулируется соответствующими правилами и программами Microsoft, описанными в руководстве пользователя, электронной документации и/или других материалах, публикуемых Microsoft. Любой дополнительный программный код, предоставленный в рамках Услуг, следует считать частью данного Программного Продукта и подпадающим под действие настоящего Соглашения. Что касается технической информации, предоставляемой Вами корпорации Microsoft при использовании ее Услуг, то Microsoft может задействовать эту информацию в деловых целях, в том числе для технической поддержки продукта и разработки. Используя такую техническую информацию, Microsoft не будет ссылаться па Вас.
Передача прав иа программное обеспечение. Вы можете безвозвратно уступить все права, регулируемые настоящим Соглашением, при условии, что не оставите себе никаких копий, передадите все составные части данного Программного Продукта (включая компоненты, мультимедийные и печатные материалы, любые обновления, Соглашение и сертификат подлинности, если таковой имеется) и принимающая сторона согласится с условиями настоящего Соглашения.
Прекращение действия Соглашения. Без ущерба для любых других прав Microsoft может прекратить действие настоящего Соглашения, если Вы нарушите его условия. В этом случае Вы должны будете уничтожить все копии данного Программного Продукта вместе со всеми его компонентами.
АВТОРСКОЕ ПРАВО.
Все авторские права и право собственности на Программный Продукт (в том числе любые изображения, фотографии, анимации, видео, аудио, музыку, текст, примеры кода, распространяемые файлы и апплеты, включенные в состав Программного Продукта) и любые его копии принадлежат корпорации Microsoft или се поставщикам. Программный Продукт охраняется законодательством го авторскому праву и положениями международных договоров. Таким образом, Вы должны обращаться с данным Программным Продуктом, каке любым другим материалом, охраняемым авторскими правами, с тем исключением, что Вы можете установить Программный Продукт на один компьютер при условии, что храните оригинал исключительно как резервную или архивную копию. Копирование печатных материалов, поставляемых вместе с Программным Продуктом, запрещается.
ОГРАНИЧЕНИЕ ГАРАНТИИ
ДАННЫЙ ПРОГРАММНЫЙ ПРОДУКТ(ВКЛЮЧАЯ ИНСТРУКЦИИ ПО ЕГО ИСПОЛЬЗОВАНИЮ) ПРЕДОСТАВЛЯЕТСЯ БЕЗ КАКОЙ- ЛИБО ГАРАНТИИ. КОРПОРАЦИЯ MICROSOFTСНИМ АЕТ С СЕБЯ ЛЮБУЮ ВОЗМОЖНУЮ ОТВЕТСТВЕННОСТЬ. В ТОМ ЧИСЛЕ ОТВЕТСТВЕННОСТЬ ЗА КОММЕРЧЕСКУЮ ЦЕННОСТЬ ИЛИ СООТВЕТСТВИЕ ОПРЕДЕЛЕННЫМ ЦЕЛЯМ. ВЕСЬ РИСК ПО ИСПОЛЬЗОВАНИЮ ИЛ И РАБОТЕ С ПРОГРАММНЫМ ПРОДУКТОМ ЛОЖИТСЯ НА ВАС.
НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ КОРПОРАЦИЯ MICROSOFT, ЕЕ РАЗРАБОТЧИКИ, А ТАКЖЕ ВСЕ, ЗАНЯТЫЕ В СОЗДАНИИ, ПРОИЗВОДСТВЕ И РАСПРОСТРАНЕНИИ ДАННОГО ПРОГРАММНОГО ПРОДУКТА, НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА КАКОЙ-ЛИБО УЩЕРБ (ВКЛЮЧАЯ ВСЕ, БЕЗИСКЛЮЧЕНИЯ, СЛУЧАИ УПУЩЕННОЙ ВЫГОДЫ. НАРУШЕНИЯ ХОЗЯЙСТВЕННОЙ ДЕЯТЕЛЬНОСТИ, ПОТЕРИ ИНФОРМАЦИИ ИЛИ ДРУГИХ УБЫТКОВ) ВСЛЕДСТВИЕ ИСПОЛЬЗОВАНИЯ ИЛИ НЕВОЗМОЖНОСТИ ИСПОЛЬЗОВАНИЯ ДАННОГО ПРОГРАММНОГО ПРОДУКТА ИЛИ ДОКУМЕНТАЦИИ, ДАЖЕ ЕСЛИ КОРПОРАЦИЯ MICROSOFT БЫЛА ИЗВЕЩЕНА О ВОЗМОЖНОСТИ ТАКИХ ПОТЕРЬ, ТАК КАК В НЕКОТОРЫХ СТРАНАХ НЕ РАЗРЕШЕНО ИСКЛЮЧЕНИЕ ИЛИ ОГРАНИЧЕНИЕ ОТВЕТСТВЕННОСТИ ЗА НЕПРЕДНАМЕРЕННЫЙ УЩЕРБ, УКАЗАННОЕ ОГРАНИЧЕНИЕ МОЖЕТ ВАС НЕ КОСНУТЬСЯ.
РАЗНОЕ'
Настоящее Соглащеинерегулируется законодательством штата Вашингтон (США), кроме случаев (и лишь в той мере, насколько ;ПО необходимо) исключительной юрисдикции того государства, иа территории которого используется Программный Продукт. Если у Вас возникли какие-либо вопросы, касающиеся настоящего Соглашения, или если Вы желаете связаться с Microsoft по любой другой причине, пожалуйста, обращайтесь в местное представительство Microsoft или пишите ИО адресу: Microsoft Sales Information Center. One Microsoft Way, Redmond, WA 98052-6399.
спеши
достичь
Издательство«Русская Редакция» —Издательство«Русская Редакция» —партнер Microsoft Press в России—партнер Microsoft Press в России—
Издательство«Русская Редакция» —
Издательство«Русская Редакция» —
партнер Microsoft Press в России—
партнер Microsoft Press в России—
предлагает широкий выбор литературы по современным информационным технологиям. Мы переводим на русский язык бестселлеры ведущих издательств мира, а также сотрудничаем с компетентными {российскими авторами.
ADO.NET IADO.NET I
ADO.NET I
ADO.NET I
м Visual Hnsic AFI и V^sua/ Ср АН
"■«А^ы
VISUAL STUDIO
.net ;'Г'<1,ч
WINDOWS
SERVER 2003
Windows ХР
Professional
М.ПССШ нщщ
тел.: (095) 256-5120, тел./факс: (095) 256-4541
e-mail: info@ruscdiLru; wwwrasedlt.ru
Из статьи Ольга Дергуновой, главы представительства Microsoft в СНГ (MSDN Magazine/Руоская Редакция, спецвыпуск № 1. апрель 2002 г.)Из статьи Ольга Дергуновой, главы представительства Microsoft в СНГ (MSDN Magazine/Руоская Редакция, спецвыпуск № 1. апрель 2002 г.)Подпиской индекс по каталогу Агентства "Роспечать. — 01240 Интернет-магазин издательства http://www.ITbook.ru, тел.: (095) 256-5120 Представитель в Украине «Техкнигв на Петровке» тел.: (044) 268-5346 Представитель в Казахстане ЧП БолатАмреев тел.: (3272) 76-1404Подпиской индекс по каталогу Агентства "Роспечать. — 01240 Интернет-магазин издательства http://www.ITbook.ru, тел.: (095) 256-5120 Представитель в Украине «Техкнигв на Петровке» тел.: (044) 268-5346 Представитель в Казахстане ЧП БолатАмреев тел.: (3272) 76-1404
Из статьи Ольга Дергуновой, главы представительства Microsoft в СНГ (MSDN Magazine/Руоская Редакция, спецвыпуск № 1. апрель 2002 г.)
Из статьи Ольга Дергуновой, главы представительства Microsoft в СНГ (MSDN Magazine/Руоская Редакция, спецвыпуск № 1. апрель 2002 г.)
Подпиской индекс по каталогу Агентства "Роспечать. — 01240 Интернет-магазин издательства http://www.ITbook.ru, тел.: (095) 256-5120 Представитель в Украине «Техкнигв на Петровке» тел.: (044) 268-5346 Представитель в Казахстане ЧП БолатАмреев тел.: (3272) 76-1404
Подпиской индекс по каталогу Агентства "Роспечать. — 01240 Интернет-магазин издательства http://www.ITbook.ru, тел.: (095) 256-5120 Представитель в Украине «Техкнигв на Петровке» тел.: (044) 268-5346 Представитель в Казахстане ЧП БолатАмреев тел.: (3272) 76-1404
Оперативная информация для профессионалов в журнале «MSDN Magazine/Русская Редакция»
Своей популярностью этот журнал обязан содержанию: читатель получает самую свежую и актуальную информацию как от ведущих разработчиков корпорации Microsoft,
так и от известных специалистов в сфере разработки приложений. Глубокие и содержательные статьи оказывают неоценимую помощь профессиональному разработчику, регулярно использующему средства разработки Microsoft.
Дэвид Круглински ,
Скотт Уингоу, Джордж Шеферд
Программирование на Microsoft Visual C++ 6.0для профессионалов
Перевод с английского под общей редакцией Д. В. Кобернмиенко
Переводчик А. Р. Врублевский
. Редактор Н. Е. Емельянова
Компьютерная верстка В. Б. Хильченко
Художник Н. М.Биржаков
Оригинал-макет выполнен с использованиемиздательской системы Adobe PageMaker 6.0
TypeMarketFontLibrary гёяэявптлегальный пользователь
Совместное издание издательства «Русская Редакция»и издательства «Питер»
Лицензия ИД№05784от07.09.2001.
Налоговая льгота — общероссийский классификатор продукции ОК005-93,том 2; 95 3005 — литература учебная.
Подписано к печати 27.10,2003- Формат84х1081/1Ц.
Усл.п.л.90,72.Доп.тираж3000экз.Заказ№787.
ООО «Питер Принт»
■196105, Санкт-Петербург, ул. Благодатная, д. 67.
■ Отпечатано с готовых диапозитивов в ФГУП ИПК «Лениздат»
.(типография им. Володарского)
Министерства РФ по делам печати, телерадиовещания .
и средств массовыхкоммуникаций.
'19Ю23,Санкт-Пстербург,наб. р. Фонтанки, 59.
КНИГА-ПОЧТОЙ
ЗАКАЗАТЬ книги издательского дома «ПИТЕР» можно любым удобным
ДЛЯ вас СПОСОБОМ:• по телефону: (812) 103-73-74;
поэлеюроннсмуадресу: postbook@pher.com;
на нашем сервере: www.piter.com;
по почте: 197198, Санкт-Петербург, а/я 619■
ЗАО «Питер Пост».
ВЫ МОЖЕТЕ ВЫБРАТЬ ОДИН ИЗ ДВУХ СПОСОБОВ ДОСТАВКИ И ОПЛАТЫ ИЗДАНИЙ:
^ Наложенным платежом с оплатой заказа при получении посылки на ближайшем почтовом отделении. Цены на издания приведены ориентировочно и включают в себя стоимость пересылки по почте (но без учета авиатарифа). Книги будут высланы нашей службой «Книга-почтой» в течение двух недель после получения заказа или выхода книги из печати.
Оплата наличными гри курьерской доставке (для Санкт-Петербурга и Москвы). Курьер бесплатно доставит заказ по указанному адресу в удобное для вас время в течение трех дней. Такой заказ лучше оформлять по телефону.
ПРИ ОФОРМЛЕНИИ ЗАКАЗА УКАЖИТЕ:
фамилию имя отчество
телефон факс' e-mail
почтовый индекс ретон район
населенный пункт улицу домкорпус квартиру
название книги автора
код:количество заказываемых экземпляров
В 1997 году по инициативе генерального директора Издательского дои «Питер» Валерия Степанова и при поддержке деловых кругов города в Санкт-Петербурге был основан «Книжный клуб Профессионал». Он собрал под флагом клуба профессионалов своего дела, которых объединяет постоянна? тяга к знаниям и любовь к книгам.
Членами клуба являются лучшие студенты и известные практики из разньксфер деятельности, которые хотят стать игм уже слали профессионалами в той или
иной области. Как и все развивающиеся проекты, с течением времени книжный клуб вырос в «Клуб Профессионал». Идею клуба сегодня формируют три основные «клубные» функции:
неформальное общение и совместный досуг интересных людей;
* участие в подготовке специалистов высокого класса (семинары, пакеты книг по специальной литературе);
формирование и высказываше шешй ссвременнсго грофессиснала - Как вступить в клуб?.(ПРИ встречах и на страница^урид^
Для вступления в «Ютуб Профессионал» вам необходимо:
ознакомиться с правилами вступления в «Клуб Профессионал» на страницах журнала или на сайте www.piter.com;
выразить свое желание вступить в «Клуб Профессионал» по электронной почте postbook@piter.com итм потел. (812) 387-01 -04;
■• заказать книги на сумму не менее 500 рублей в течение любого времени
или приобрести комплект «Библиотека грофессионала».
«Библиотека профессионала»
Мы предлагаем вам потучить все необходимые знания, подписавшись на «Библиотеку профессионала». Она для тех, кто экономит не только время, но и деньги. Покупая комплект - книжную полку "Библиотека профессионала», вы получаете:
скидку 15% от розничной цены издания, без учета почтовых расходов; • при покупке двух или более комплектов - дополнительную скидку 3%;
ЗАКАЖИТЕ БЕСПЛАТНЫЙ ЖУРНАЛ «КЛУБ ПРОФЕССИОНАЛ».WWW.PITER.COM
пзлАтельскпй пом
СПЕЦИАЛИСТАМКНИЖНОГО БИЗНЕСА!СПЕЦИАЛИСТАМКНИЖНОГО БИЗНЕСА!ЮПИТЕР
СПЕЦИАЛИСТАМКНИЖНОГО БИЗНЕСА!
СПЕЦИАЛИСТАМКНИЖНОГО БИЗНЕСА!
WWW. PITER .'С‘ОМ
ПРЕДСТАВИТЕЛЬСТВА ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР*предлагают эксклюзивный ассортимент компьютерной, медицинской,психологической, экономической и популярной литературы
РОССИЯ
Москва м. «Калужская*, ул. Бутлерова, д. 176, офис 207,240; тел./факс (095) 777-54-67;e-mail: sales@piter, msk.ru
Санкт-Петербург м. «Выборгская», Б. Сампсониевский пр., д. 29а;тел. (812) 103-73-73, факс (812) 103-73-83; e-mail: sales@piter.com
Воронеж ул. 25 января, д. 4; тел. (0732) 27-18-86;
e-mail: piter-vm@vmail.ru; piterv@comch.ru
Екатеринбург ул. 8 Марта, д. 2676; тел./факс (3432) 25-39-94; e-mail: piter-ural@r66.ru
Нижний Новгород ул. Премудрова,д. 31а; тел. (8312) 58-50-15, 58-50-25;
e-mail: piter@infonet.nnov.ru
Новосибирск ул. Немировича-Данченко, д. 104, офис 502;тел/факс (3832) 54-13-09, (3832) 47-92-93; e-mail: piter-sib@risp.ruРостов-на-Дону ул. Капитвинская.д. 17в;тел. (8632)95-36-31, (8632) 95-36-32;e-mail: jupiter@rost.nj
Самара ул. Новосадовая, д. 4; тел. (8462)37-06-07; e-mail: piter-volga@sama.ru ,
УКРАИНА
Харьков ул. Суздальские ряд>1, д. 12, офис 10-11, т. (057) 712-27-05;
e-mail: piter@tender.kharkov.ua
Киев пр. Красных Казаков, д. 6, корп. 1; тел./факс (044) 490-35-68,490-35-69;
e-mail: office@piter-press.kiev.ua
БЕЛАРУСЬ
Минск ул. Бобруйская д., 21, офисЗ; тел./факс (37517) 226-19-53; e-mail: piter@mail.by
МОЛДОВА
ИСишинев «Ауратип-Питер»; ул. Митропол/гт Варлаам, 65, офис 345; тел. (3732) 22-69-52, _ факс (3732)27-24-82; e-mai:lili@auratip.mldnet.com _
Ищем зарубежных партнеров или посредников, имеющих выход на зарубежный рынок.
Телефон для связи: (812) 103-73-73.
E-mail: grigorjan@piter.com
Издательский дом «Питер» приглашает к сотрудничеству авторов.
Обращайтесь по телефонам: Санкт-Петербург - (812)327-13-11,
Москва - (095) 777-54-67.
Заказ книгдля вузов и библиотек: (812) 103-73-73.
Специальное предложение - e-mail: kozin@piter.com
издательский дом
СОЛИТЕР
^^ WWW.PiTER.COM
УВАЖАЕМЫЕ ГОСПОДА!
КНИГИ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР» Л‘ВЫ МОЖЕТЕ ПРИОБРЕСТИ
' ОПТОМ И В РОЗНИЦУ
У НАШИХ РЕГИОНАЛЬНЫХ ПАРТНЕРОВ.
Красноярск, «Кшжтый мц>, тел./факс (3912) 27-39-71.E-mail: book-wortd@public.krasnet.ruНижневартовск, «Дом книги», тел. (3466) 23-27-14, факс 23-59-50.E-mai: book@nvartovsk.wsnet.ruНовосибирск, «Топ-кнтэ>, тел. (3832) 36-10-26, факс 36-10-27.E-mail: office@top-kniga.ru http://www.top-kniga.ruТюмень, «Друг», телефакс (3452) 21-34-82. E-mail: drug@tyimen.ruТюмень, «Фолиант», тел. (3452) 27-36-06, фасс 27-36-11.E-mail: foliant@tyumen.ruЧелябинск, ТД «Эврика», ул. Барбюса, д. 61, тел./факс (3512) 52-49-23.E-mail: evrika@chel.surnet.ruТатарстанКазань, «Таис», тел. (8432) 72-34-55, факс 72-27-82.E-mail: tais@bancorp.ruУралЕкатеринбург, магазин № 14, ул. Челоскинцев, д. 23, телефакс (3432) 53-24-90.E-mail: gvardia@mail.ur.ruЕкатеринбург, «Валео-книга»,ул. Ключевская, д. 5, тел./факс (3432) 42-56-00, E-mal: vaieo@etel.ruКрасноярск, «Кшжтый мц>, тел./факс (3912) 27-39-71.E-mail: book-wortd@public.krasnet.ruНижневартовск, «Дом книги», тел. (3466) 23-27-14, факс 23-59-50.E-mai: book@nvartovsk.wsnet.ruНовосибирск, «Топ-кнтэ>, тел. (3832) 36-10-26, факс 36-10-27.E-mail: office@top-kniga.ru http://www.top-kniga.ruТюмень, «Друг», телефакс (3452) 21-34-82. E-mail: drug@tyimen.ruТюмень, «Фолиант», тел. (3452) 27-36-06, фасс 27-36-11.E-mail: foliant@tyumen.ruЧелябинск, ТД «Эврика», ул. Барбюса, д. 61, тел./факс (3512) 52-49-23.E-mail: evrika@chel.surnet.ruТатарстанКазань, «Таис», тел. (8432) 72-34-55, факс 72-27-82.E-mail: tais@bancorp.ruУралЕкатеринбург, магазин № 14, ул. Челоскинцев, д. 23, телефакс (3432) 53-24-90.E-mail: gvardia@mail.ur.ruЕкатеринбург, «Валео-книга»,ул. Ключевская, д. 5, тел./факс (3432) 42-56-00, E-mal: vaieo@etel.ru
Красноярск, «Кшжтый мц>, тел./факс (3912) 27-39-71.
E-mail: book-wortd@public.krasnet.ru
Нижневартовск, «Дом книги», тел. (3466) 23-27-14, факс 23-59-50.
E-mai: book@nvartovsk.wsnet.ru
Новосибирск, «Топ-кнтэ>, тел. (3832) 36-10-26, факс 36-10-27.
E-mail: office@top-kniga.ru http://www.top-kniga.ru
Тюмень, «Друг», телефакс (3452) 21-34-82. E-mail: drug@tyimen.ru
Тюмень, «Фолиант», тел. (3452) 27-36-06, фасс 27-36-11.
E-mail: foliant@tyumen.ru
Челябинск, ТД «Эврика», ул. Барбюса, д. 61, тел./факс (3512) 52-49-23.
E-mail: evrika@chel.surnet.ru
Татарстан
Казань, «Таис», тел. (8432) 72-34-55, факс 72-27-82.
E-mail: tais@bancorp.ru
Урал
Екатеринбург, магазин № 14, ул. Челоскинцев, д. 23, телефакс (3432) 53-24-90.
E-mail: gvardia@mail.ur.ru
Екатеринбург, «Валео-книга»,
ул. Ключевская, д. 5, тел./факс (3432) 42-56-00, E-mal: vaieo@etel.ru
Красноярск, «Кшжтый мц>, тел./факс (3912) 27-39-71.
E-mail: book-wortd@public.krasnet.ru
Нижневартовск, «Дом книги», тел. (3466) 23-27-14, факс 23-59-50.
E-mai: book@nvartovsk.wsnet.ru
Новосибирск, «Топ-кнтэ>, тел. (3832) 36-10-26, факс 36-10-27.
E-mail: office@top-kniga.ru http://www.top-kniga.ru
Тюмень, «Друг», телефакс (3452) 21-34-82. E-mail: drug@tyimen.ru
Тюмень, «Фолиант», тел. (3452) 27-36-06, фасс 27-36-11.
E-mail: foliant@tyumen.ru
Челябинск, ТД «Эврика», ул. Барбюса, д. 61, тел./факс (3512) 52-49-23.
E-mail: evrika@chel.surnet.ru
Татарстан
Казань, «Таис», тел. (8432) 72-34-55, факс 72-27-82.
E-mail: tais@bancorp.ru
Урал
Екатеринбург, магазин № 14, ул. Челоскинцев, д. 23, телефакс (3432) 53-24-90.
E-mail: gvardia@mail.ur.ru
Екатеринбург, «Валео-книга»,
ул. Ключевская, д. 5, тел./факс (3432) 42-56-00, E-mal: vaieo@etel.ru
Башкортостан
Уфа. «Азия», ул. Зенцова, д. 70 (оптовая продажа), маг. «Оазис», ул. Чернышевского, д. 88, тел./факс (3472) 50-39-00.
E-mail: asiaufa@ufanet.ru
/альний Восток
Владивосток, «Приморский торговый дом книги», тел./факс (4232)23-82-12.
E-mail: bookbase@mail.primorye.ru
Хабаровск, «Миро, тел. (4212) 30-54-47, факс 22-73-30. E-mail: sale_book@bookmirs.khv.ru
Хабаровск, «Книиный мир», чел. (4212) 32-85-51, факс 32-82-50.
--mail: postmaster@worldbooks.kht.ru
Европейские регионы России
Архангельск, «Дом книги», тел. (8182) 65-41-34, факс 65-41-34. E-mai: book@a1net.nj
Калининград, «Вестер», тел./факс (0112)21 -56-28,21-62-07. E-mail: nshibkova@vester.ru http://www.vester.ru
Северньй Кавказ
Ессентуки, «Россы»,ул. Октябрьская, 424,
тел ./факс (87934) 6-93-09.
E-mai: rossy@kmw.ru
Сибирь
Иркутск, «ПродаЛитЬ»,,
тел. (3952) 59-13-70, факс51 -30-70.
E-mail: prodalit@irk.ru
http://www.prodalit.irk.ru
Иркутск, «Антей-книга»,- тел ./факс (3952) 33-42-47. E-mail: antey@irk.nj
2 Целочисленные индексы методов и свойств необходимы постацдарту Автоматизации. — 11рим.перев.
Шону
— Скотт Уингоу
Моей любящей жене Сэндии самому замечательному сыну Теду
—Джордж Шеферд
Дэвид Дж. Круглински,Скотт Уингоу, Джордж Шеферд
ДЛЯ ПРОФЕССИОНАЛОВ
Программирование
Microsoft"
на VISUAL С+^.О
МЛУССШ РЕДАКЦИЯС^ПИТЕР1
Москва - Санкт-Петербург - Нижний Новгород - ВоронежРостов-на-Дону - Екатеринбург - Самара - Новосибирск
Киев - Харьков - Минск ■ ■
УДК00443 ББК 32.973,26-018 К84.•-
КруглиискиД., Уннгоу С., Шеферд Дж.
К84 Программирование на Microsoft Visual C++ 6.0 для профессионалов/Пер. с англ. - СПб: Питер; М: Издательско-торговыйдом «Русская Редакция», 2004. —861 с.: ил.
ISBN5-272-00385-3
Эта книга — настоящая «библия» программирования на Microsoft Visual C++ 6.0 с применением библиотеки классов MFC. Ориентирована на профессиональных программистов, владеющихязыкомС, имеющих представление о C++ и приступающих к разработке 32-х разрядных приложений для ' Microsoft Windows 95/98 и Microsoft Windows NT 4.0.
В книге рассмотрены разработка приложений для новейших 32-х разрядных операционных систем (обработка событий, управление памятью, печать- GDI, DIB, SDI/MDI, ActiveX, COM, ATL, последние расширения СОМ), поддержка баз данных, программирование для Интернета с использованием Windows Sockets, MFC Winlnet и ISAPI.•
Прилагаемый к книге компакт-диск содержит образцы кода и дополнительное программное обеспечение.
•’ '• ' \: ' УДК 004.43
ББК 32.973.26-018
Подготовлено к изданию по лицензионному договору с Microsoft Corporation, Редмонд, Вашингтон, США ч .
ActiveX, JScript, MSDN, Visual Basic, Visual C++, Visual FoxPro, Visual J++, Visual SourceSafe, Visual Studio, Windows, Windows NT и Word являются либо товарными знаками, либо охраняемыми товарными знаками Microsoft Corporation. Все другие товарные знаки являются собственностью соответствующих фирм. ,
■■■•
ISBN 5-272-00385-3ISBN 1-57231-857-0 (англ)ISBN 5-272-00385-3ISBN 1-57231-857-0 (англ)© Оригинальное издание на английском языке, Д. Круглински, С. Уингоу, Дж- Шеферд; 1998
ISBN 5-272-00385-3
ISBN 1-57231-857-0 (англ)
ISBN 5-272-00385-3
ISBN 1-57231-857-0 (англ)
© Перевод на русский язык, Microsoft Corporation, 2000
Содержание
Благодарностич....XXVI
ВведениеXXVIII
MFC, ATL и WFC - MFC мертва?.XXVIII
C++ и JavaXXVIII
Кому адресована эта книгаXXIX
Что не вошло в книгу.XXX
Как пользоваться книгой.„XXX
Структура книгиXXX
Часть 1: Основные сведения о Windows, Visual C++ и каркасе приложенияXXX
Часть 2: Класс «вид» библиотеки MFCXXX
Часть 3: Архитектура «документ-вид».XXXI
Часть 4: СОМ, Автоматизация и OLEXXXI
Часть 5: Работа с базами данных.XXXI
Часть 6: Программирование для ИнтернетаXXXI
ПриложенияXXXI
Win32 или Win16?XXXII
Windows 98 или Windows NT 4.0?.XXXII
Использование возможностей Windows: вставки «Для тех, кто программирует в Win32» ., .XXXII Прилагаемый компакт-диск. .. . . XXXII
Технические замечания и примеры программXXXIV
ПоддержкаXXXIV