Вы находитесь на странице: 1из 124

А.А. Богуславский, С.М.

Соколов

Основы программирования
на языке Си++
Часть IV. Программирование для Microsoft Windows
с использованием Visual C++ и библиотеки
классов MFC

(для студентов физико-математических факультетов


педагогических институтов)

Коломна, 2002
ББК 32.97я73 Рекомендовано к изданию
УДК 681.142.2(075.8) редакционно-издательским советом
Б 73 Коломенского государственного
педагогического института

Богуславский А.А., Соколов С.М.


Б73 Основы программирования на языке Си++: Для студентов физико-
математических факультетов педагогических институтов. – Коломна: КГПИ,
2002. – 490 с.

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


зовательской работы на персональном компьютере, основным понятиям и методам
современного практического программирования. Предметом изучения курса является
объектно-ориентированное программирование на языке Си++ в среде современных
32-х разрядных операционных систем семейства Windows. Программа курса разбита
на 4 части: (1) Введение в программирование на языке Си++; (2) Основы программи-
рования трехмерной графики; (3) Объектно-ориентированное программирование на
языке Си++ и (4) Программирование для Microsoft Windows с использованием Visual
C++ и библиотеки классов MFC.
После изучения курса студент получает достаточно полное представление о
содержании современного объектно-ориентированного программирования, об уст-
ройстве современных операционных систем Win32 и о событийно-управляемом про-
граммировании. На практических занятиях вырабатываются навыки программирова-
ния на Си++ в интегрированной среде разработки Microsoft Visual C++ 5.0.

Рецензенты:

И.П. Гиривенко – к.т.н., доцент, зав. кафедрой информатики и вычислительной тех-


ники Рязанского государственного педагогического университета
им. С.А. Есенина.
А.А. Шамов – к.х.н., доцент кафедры теоретической физики Коломенского госу-
дарственного педагогического института.

2
СОДЕРЖАНИЕ
ВВЕДЕНИЕ............................................................................................................................5
ЛЕКЦИЯ 1. АРХИТЕКТУРА 32-РАЗРЯДНЫХ ОС WINDOWS ................................6
1. ВВЕДЕНИЕ .........................................................................................................................6
2. ОКНА И СООБЩЕНИЯ ........................................................................................................6
3. СООБЩЕНИЯ И МНОГОЗАДАЧНОСТЬ ..............................................................................10
4. ВЫЗОВЫ ФУНКЦИЙ WINDOWS API ................................................................................11
5. РАЗЛИЧИЯ МЕЖДУ ПРОГРАММНЫМИ ПЛАТФОРМАМИ ..................................................15
6. РЕЗЮМЕ ..........................................................................................................................17
7. УПРАЖНЕНИЯ .................................................................................................................17
ЛЕКЦИЯ 2. СТРУКТУРА ПРИЛОЖЕНИЯ WINDOWS ...........................................19
1. ПРОСТЕЙШЕЕ WINDOWS-ПРИЛОЖЕНИЕ "HELLO, WORLD!" .........................................19
2. ПРИЛОЖЕНИЕ С ЦИКЛОМ ОБРАБОТКИ СООБЩЕНИЙ ......................................................19
3. ПРИЛОЖЕНИЕ С ЦИКЛОМ ОБРАБОТКИ СООБЩЕНИЙ И ОКОННОЙ ПРОЦЕДУРОЙ ...........21
4. РЕГИСТРАЦИЯ ОКОННОГО КЛАССА И СОЗДАНИЕ ОКНА.................................................23
5. РИСОВАНИЕ СОДЕРЖИМОГО ОКНА ................................................................................25
6. ЧАСТО ИСПОЛЬЗУЕМЫЕ СООБЩЕНИЯ УПРАВЛЕНИЯ ОКНАМИ ......................................26
7. ПРИЛОЖЕНИЕ С НЕСКОЛЬКИМИ ЦИКЛАМИ ОБРАБОТКИ СООБЩЕНИЙ ..........................27
8. РЕЗЮМЕ ..........................................................................................................................30
9. УПРАЖНЕНИЯ. ................................................................................................................30
ЛЕКЦИЯ 3. ИЕРАРХИЯ ОКОН WINDOWS. ТИПЫ ОКОН....................................32
1. ИЕРАРХИЯ ОКОН .............................................................................................................32
2. ДИАЛОГОВЫЕ ОКНА .......................................................................................................33
3. СТАНДАРТНЫЕ ДИАЛОГОВЫЕ ОКНА ..............................................................................36
4. ЭЛЕМЕНТЫ УПРАВЛЕНИЯ ...............................................................................................39
5. РЕЗЮМЕ ..........................................................................................................................41
6. УПРАЖНЕНИЯ. ................................................................................................................42
ЛЕКЦИЯ 4. ОБЗОР БИБЛИОТЕКИ MFC....................................................................43
1. НАЗНАЧЕНИЕ БИБЛИОТЕКИ MFC...................................................................................43
2. ПРОСТЕЙШЕЕ ПРИЛОЖЕНИЕ НА MFC............................................................................46
3. РЕЗЮМЕ ..........................................................................................................................53
4. УПРАЖНЕНИЯ .................................................................................................................54
ЛЕКЦИЯ 5. ОТОБРАЖЕНИЕ ИНФОРМАЦИИ С ПОМОЩЬЮ МОДУЛЯ GDI
................................................................................................................................................56
1. КОНТЕКСТ УСТРОЙСТВА ................................................................................................56
2. РИСОВАНИЕ ГРАФИЧЕСКИХ ПРИМИТИВОВ С ПОМОЩЬЮ ФУНКЦИЙ GDI.....................61
3. РЕЗЮМЕ ..........................................................................................................................69
4. УПРАЖНЕНИЯ .................................................................................................................70
ЛЕКЦИЯ 6. РАБОТА С УСТРОЙСТВАМИ ВВОДА. ИСПОЛЬЗОВАНИЕ
МЕНЮ ..................................................................................................................................71
1. ПОЛУЧЕНИЕ ДАННЫХ ОТ МЫШИ....................................................................................71
2. ПОЛУЧЕНИЕ ДАННЫХ С КЛАВИАТУРЫ...........................................................................74
3. ОСНОВНЫЕ ПРИЕМЫ ПРОГРАММИРОВАНИЯ МЕНЮ ......................................................77
3
4. УПРАЖНЕНИЯ .................................................................................................................83
ЛЕКЦИЯ 7. ЭЛЕМЕНТЫ УПРАВЛЕНИЯ...................................................................84
1. СТАНДАРТНЫЕ ЭЛЕМЕНТЫ УПРАВЛЕНИЯ ......................................................................84
2. НЕОЧЕВИДНЫЕ АСПЕКТЫ ПРОГРАММИРОВАНИЯ ЭЛЕМЕНТОВ УПРАВЛЕНИЯ ..............91
3. УПРАЖНЕНИЯ .................................................................................................................93
ЛЕКЦИЯ 8. ДИАЛОГОВЫЕ ОКНА ..............................................................................94
1. МОДАЛЬНЫЕ ДИАЛОГОВЫЕ ОКНА И КЛАСС CDIALOG ..................................................94
1.5 ВЗАИМОДЕЙСТВИЕ С ЭЛЕМЕНТАМИ УПРАВЛЕНИЯ ДИАЛОГОВОГО ОКНА ................103
2. ОКНА СВОЙСТВ .............................................................................................................105
3. СТАНДАРТНЫЕ ДИАЛОГОВЫЕ ОКНА WINDOWS ...........................................................106
ЛЕКЦИЯ 9. АРХИТЕКТУРА ОДНОДОКУМЕНТНЫХ ПРИЛОЖЕНИЙ
ДОКУМЕНТ/ВИД ............................................................................................................108
1. ОСНОВНЫЕ ПОНЯТИЯ АРХИТЕКТУРЫ ДОКУМЕНТ/ВИД ...............................................108
2. ФУНКЦИЯ ИНИЦИАЛИЗАЦИИ ПРИЛОЖЕНИЯ CWINAPP::INITINSTANCE .....................109
3. КЛАСС-ДОКУМЕНТ .......................................................................................................111
4. КЛАСС-ВИД ...................................................................................................................114
5. КЛАСС "ОКНО-РАМКА".................................................................................................116
6. ДИНАМИЧЕСКОЕ СОЗДАНИЕ ОБЪЕКТОВ .......................................................................116
7. МАРШРУТИЗАЦИЯ КОМАНДНЫХ СООБЩЕНИЙ ............................................................118
7.1 СТАНДАРТНЫЕ КОМАНДНЫЕ ИДЕНТИФИКАТОРЫ И ОБРАБОТЧИКИ ..........................120
ЛИТЕРАТУРА ..................................................................................................................122

4
Введение
Изучение программирования на Си++ для современных операционных систем
семейства MS Windows сопряжено со сложностями, связанными с большим количе-
ством технических подробностей устройства приложения и операционной системы, а
также вопросов их взаимодействия. Применение визуальных сред разработки, напри-
мер, MS Visual Basic или Borland Delphi, существенно упрощает задачу разработки
типичных приложений. Но при изучении только подобных инструментов возможно,
что программист будет ориентироваться в некоторой специфической библиотеке
классов или функций конкретной среды разработки и не будет детально представлять,
как устроено приложение Windows и какие возможности есть у самой ОС, а не у кон-
кретной библиотеки классов.
Возрастающая сложность Windows API привела к широкому распространению
объектно-ориентированных языков программирования и появлению надежных биб-
лиотек классов для взаимодействия с ОС. Поэтому программирование только на
уровне API представляется для большинства задач слишком сложным и неэффектив-
ным с точки зрения требуемых программистских усилий.
Для учебных целей в данной части курса выбрана среда разработки MS Visual
C++ и библиотека классов MFC (Microsoft Foundation Classes) – одно из наиболее
распространенных промышленных решений. Тем не менее, эти инструменты доста-
точно "типичные" и "низкоуровневые", чтобы впоследствии программист при необ-
ходимости смог достаточно быстро перейти к использованию других инструменталь-
ных средств.
Цель учебного курса состоит в усвоении студентом начальных навыков про-
фессиональной разработки приложений Windows. Для этого необходимо иметь пред-
ставление об архитектуре ОС, основных возможностях API, об архитектуре и воз-
можностях MFC, а также надо уметь пользоваться средой разработки. В среде Visual
C++ студенты должны научиться разрабатывать программы, содержащие основные
типы окон (родительские, дочерние, диалоговые и др.), различные компоненты ресур-
сов (меню, пиктограммы, курсоры, горячие клавиши и т.п.), реализующие основные
операции по выводу текста и графических элементов (отрезков, окружностей и т.п.) с
помощью функций GDI.
В данной части курса программирование в MFC рассматривается не как про-
цесс нажатия кнопок в окнах AppWizard. Конечно, средства автоматизации написания
исходного текста тоже упоминаются, но только после того, как эти же средства будут
освоены в ручном режиме и будет показано, как соотносятся возможности MFC и
каркаса приложения MFC с возможностями Windows и Windows API.
Кроме лекционного материала, в данном курсе приведены несколько лабора-
торных работ (они находятся на прилагаемом компакт-диске). Часть из них построе-
ны по принципу "собрать приложение из готовых частей исходного текста", что по-
зволяет лучше усвоить структуру изучаемых приложений. Часть заданий в лабора-
торных работах и упражнениях в лекциях требует написания новых приложений или
фрагментов готовых приложений.

5
Лекция 1. Архитектура 32-разрядных ОС Windows
1. Введение
32-разрядные операционные системы Windows (Win32) отличаются от более
старых 16-разрядных версий тем, что предназначены для работы на 32-разрядных
процессорах (обычно это Intel-совместимые процессоры i386-Pentium III) и макси-
мально полно используют их возможности. Одна из основных возможностей – более
простой, по сравнению с 16-разрядными процессорами, способ адресации памяти, по-
зволяющий пронумеровать все ячейки памяти адресного пространства программы с
помощью 32-разрядных адресов.
Существует большое количество различных версий Windows. 32-разрядные ОС
можно разделить на два семейства: (1) Windows NT/2000 и (2) Windows 95/98/ME.
При разработке ОС Windows NT главное внимание уделялось надежности,
безопасности (защите данных и программ от несанкционированного доступа) и пере-
носимости. Последнее свойство подразумевает возможность работы ОС на различных
платформах, а не только на ПК с Intel-совместимыми процессорами. В эти ОС встро-
ен графический интерфейс пользователя и различные средства для использования ОС
в качестве сервера. В Windows NT есть эмулятор старых ОС, позволяющий запускать
программы для Win16 и MS-DOS (если только они не используют каких-либо недо-
кументированных возможностей и не обращаются напрямую к устройствам ПК).
ОС второго семейства (Windows 95) предназначались, в первую очередь, для
домашнего применения и должны были обеспечить безболезненный переход от 16-
разрядных ОС к 32-разрядным. Совместимость со старыми программами для MS-
DOS и Windows 3.1 была одним из главных критериев при разработке этих ОС. По-
этому, чтобы работали как можно больше старых программ, в том числе использую-
щие недокументированные особенности старых ОС и аппаратуры ПК, в эти ОС было
включено много 16-разрядного кода (практически и MS-DOS, и Win16 как подсисте-
мы). Поэтому ОС Windows 95 являются менее надежными, чем Windows NT (хотя и
гораздо более надежными, чем старые 16-разрядные ОС).
Несмотря на различия между двумя семействами ОС, у них есть и большое ко-
личество общих свойств (например, интерфейс пользователя). Для программиста
важно, что у всех ОС Win32 есть общий набор системных вызовов (функций), дос-
тупных для вызова из программ для обращения к ОС. Эти функции составляют Win32
API. Отличие состоит в том, что у некоторых функций в Windows NT используются
параметры, которые в Windows 95 игнорируются. Например, это параметры, касаю-
щиеся безопасности и ограничивающие доступ к некоторым ресурсам программы.

2. Окна и сообщения
Windows можно отнести к классу многозадачных ОС, основанных на передаче
сообщений. В "глубине" ОС реализован механизм, преобразующий информацию от
различных устройств ввода/вывода (события) в некоторую структуру данных – со-
общение. Примерами событий являются нажатие клавиши, перемещение мыши, тик
таймера. Типичное приложение (так обычно называются прикладные программы для
Windows) строится на базе каркаса, содержащего цикл обработки сообщений. В этом
цикле выполняется прием сообщений и передача их в соответствующие функции-
обработчики сообщений.

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

2.1 Приложения, процессы, потоки и окна


При запуске приложения в Windows происходит создание процесса. Но ОС не
выделяет для него процессорного времени. Процессу принадлежат открытые файлы,
участки оперативной памяти и другие ресурсы. Кроме того, ему принадлежит про-
граммный поток. Поток, фактически, – это набор значений внутренних регистров
процессора. Поток содержит информацию о том, какая машинная команда выполня-
ется процессором в данный момент и где расположены локальные переменные. ОС
выделяет квант времени каждому из работающих на компьютере потоков, т.о. в ОС
обеспечивается многозадачность.
У одного процесса может быть несколько потоков. Например, в текстовом ре-
дакторе один поток может обрабатывать ввод данных от пользователя, а другой пере-
давать документ на принтер.
Окно всегда "принадлежит" потоку. Поток может владеть одним или несколь-
кими окнами, а также может не иметь ни одного окна. Окна потока сами находятся в
иерархической связи: некоторые из них являются окнами верхнего уровня, а некото-
рые – дочерними окнами других окон (рис. 1.1).

Процесс 1 Поток 1А
Окно

Поток 1Б

Окно

Процесс 2 Поток 2А
Окно

Поток 2Б

Поток 2В Окно

Рис. 1.1. Процессы, потоки и окна.

В Windows существует большое количество разных типов окон. Некоторые из


них очевидны, например, главное окно приложения (о котором пользователь обычно
думает, что это и есть приложение) и диалоговые окна. Менее очевидно, что боль-
шинство элементов управления в окнах приложений и диалоговых окнах тоже явля-
ются окнами. Каждая кнопка, строка ввода, полоса прокрутки, список, пиктограмма и
даже фон рабочего стола рассматриваются ОС как окна.
На рис. 1.2 показан рабочий стол Windows 95 c двумя запущенными приложе-
ниями (Блокнот и Калькулятор). Каждое окно, в том числе кнопки, выделено черной
рамкой (изображение получено с помощью утилиты Spy++ из комплекта Visual C++).

7
Рис. 1.2. Окна различных типов.

2.2 Оконные классы


Оконные классы – это шаблоны, хранящие информацию о свойствах окна. Сре-
ди этих свойств – начальные размеры окна, его пиктограмма, курсор и меню. Вероят-
но, самое главное свойство – это адрес функции, называемой оконной процедурой.
Приложение обычно выполняет обработку полученных сообщений с помощью вызо-
ва функции DispatchMessage из Win API. Функция DispatchMessage, в свою оче-
редь, вызывает соответствующую оконную процедуру. Адрес оконной процедуры при
этом извлекается из оконного класса окна, которому послано сообщение. Именно
оконная процедура выполняет обработку всех сообщений, посылаемых окну.
В Windows есть много стандартных оконных классов, например, стандартные
элементы управления вроде кнопок (класс Button) и строк ввода (класс Edit).
Для регистрации новых оконных классов предназначена функция
RegisterClass. Т.о. программист может реализовать окно с поведением, которого
нет ни у одного из стандартных оконных классов. Например, именно так обычно реа-
лизуется главное окно приложения и выполняется регистрация пиктограммы и глав-
ного меню приложения.
Windows позволяет создавать подклассы и суперклассы для существующих
оконных классов. При создании подкласса выполняется замена оконной процедуры
класса. Это делается с помощью функции SetWindowLong (подкласс экземпляра) или
SetClassLong (глобальный подкласс). Различие между двумя функциями в том, что
в первом случае изменяется поведение только одного экземпляра окна, а во втором
случае – поведение всех окон данного класса (в пределах приложения).
При создании суперкласса новый класс основывается на существующем, и за-
поминается адрес старой оконной процедуры. Для создания суперкласса приложение
получает информацию о существующем классе с помощью функции GetClassInfo,
запоминает адрес старой оконной процедуры, затем модифицирует полученную
структуру WNDCLASS и использует ее при вызове RegisterClass. Сообщения, не об-
рабатываемые новой оконной процедурой, должны передаваться в старую.
Используемые термины похожи на термины объектно-ориентированного про-
граммирования, но отличаются от них по смыслу. Не надо путать оконный класс с
понятием класса в Си++ (например, с классами библиотеки MFC). Понятие оконного
класса было введено в Windows несколькими годами раньше, чем в этой ОС распро-
странились объектно-ориентированные языки.

8
2.3 Типы сообщений
Сообщения приходят от разных источников, информируя окна о событиях на
различных уровнях ОС. Действия, которые для пользователя могут выглядеть прими-
тивными, на системном уровне могут сопровождаться большим количеством различ-
ных сообщений. В качестве примера в табл. 1.1 приведен протокол сообщений, полу-
чаемых диалоговым окном при закрытии по нажатию кнопки OK. Этот протокол по-
лучен с помощью утилиты Spy++.
Приложение может обрабатывать не все сообщения, а только некоторые. Необ-
работанные сообщения передаются обработчику сообщений "по умолчанию" в ОС.

Таблица 1.1. Сообщения, посылаемые окну "О программе" приложения MS Word при закрытии окна
по нажатию пользователем кнопки OK.
Символич. идентификатор Описание
WM_LBUTTONDOWN Была нажата левая кнопка мыши.
WM_PAINT Требуется перерисовать кнопку OK, т.к. она теперь нажата.
WM_LBUTTONUP Левая кнопка мыши была отпущена.
WM_PAINT Требуется перерисовать кнопку OK, т.к. она теперь отпущена.
WM_WINDOWPOSCHANGING Положение окна на экране собирается изменяться.
WM_WINDOWPOSCHANGED Положение окна на экране только что было изменено.
WM_NCACTIVATE Была активизирована область строки заголовка окна.
WM_ACTIVATE Была активизирована клиентская область окна.
WM_WINDOWPOSCHANGING Положение окна на экране собирается изменяться.
WM_KILLFOCUS У окна будет отключен фокус ввода.
WM_DESTROY Окно уничтожается.
WM_NCDESTROY Уничтожается область заголовка окна.

Сообщения в Windows описываются с помощью структуры MSG:


typedef struct tagMSG {
HWND hwnd; // Идентификатор окна-получателя
UINT message; // Идентификатор сообщения
WPARAM wParam; // Дополнительная информация, смысл
LPARAM lParam; // которой зависит от типа сообщения
DWORD time; // Время посылки сообщения
POINT pt; // Местоположение указателя мыши
} MSG;
Переменная hwnd – это уникальный идентификатор окна, которому было по-
слано сообщение. У каждого окна Windows есть свой числовой идентификатор. Пе-
ременная message является идентификатором самого сообщения. Различных сооб-
щений в Windows несколько сотен, и у каждого собственный идентификатор. Для
удобства вместо численных идентификаторов используются символические (напри-
мер, WM_PAINT, WM_TIMER). Они определены в стандартных заголовочных файлах
Windows (в программы на Си можно включать только файл windows.h; в нем, в свою
очередь, содержатся директивы #include для включения остальных файлов).
По назначению системные сообщения можно разбить на несколько групп.
Имена сообщений каждой группы начинаются с одинакового префикса, например, WM
для сообщений, связанных с управлением окнами или BM – для сообщений от кнопок.
Набор системных сообщений не зафиксирован, новые сообщения могут добавляться
по мере роста возможностей новых версий ОС.

9
Чаще всего используются оконные сообщения (WM_...). Эта группа настолько
большая, что можно разделить ее еще на несколько категорий. Среди них – сообще-
ния буфера обмена, мыши, клавиатуры, сообщения MDI (многодокументный интер-
фейс) и многие другие. Деление сообщений на категории условно, т.о. программисту
легче классифицировать большой набор сообщений.
Сообщения остальных групп относятся к специфическим типам окон. Есть со-
общения, определенные для строк ввода (EM), кнопок (BM), списков (LB), комбиниро-
ванных списков (CB), полос прокрутки (SBM), деревьев (TVM) и др. Эти сообщения, за
редким исключением, обычно обрабатываются оконной процедурой самого элемента
управления и не слишком интересны для прикладного программиста.
Кроме системных сообщений, в Windows допускается передача сообщений, оп-
ределенных приложением. Для получения уникального идентификатора нового со-
общения служит функция RegisterWindowMessage. Подобные сообщения часто
применяются для взаимодействия между различными частями одного приложения
или для обмена информацией между несколькими приложениями.

3. Сообщения и многозадачность

3.1 Процессы и потоки


Многозадачность в Windows обеспечивается посредством выделения квантов
времени запущенным в системе потокам. Потоки принадлежат процессам. Одному
процессу могут принадлежать несколько потоков. Они обладают правом доступа к
памяти и другим ресурсам, принадлежащим этому процессу. Поэтому между потока-
ми одного процесса легче организовать обмен данными и взаимодействие, чем между
потоками разных процессов.
В начале работы каждый процесс обладает единственным первичным потоком.
Информация о первичном потоке передается ОС в виде адреса функции. Поэтому все
Windows-приложения содержат вызываемую при запуске функцию WinMain(), адрес
которой и передается в качестве адреса первичного потока. Первичный поток может
создать дополнительные потоки, и т.д. Потоки одного процесса имеют доступ ко всем
его объектам. Такие потоки отличаются друг от друга лишь точкой входа и локаль-
ными переменными, расположенными в адресном пространстве процесса.
Потоки, принадлежащие разным процессам, не имеют между собой ничего об-
щего, однако они могут получить доступ к общим ресурсам и памяти, используя ме-
ханизмы межпроцессного взаимодействия.
В немногопоточных ОС (например, в большинстве версий UNIX) наименьшая
исполняемая системная единица называется задачей или процессом. Алгоритм дис-
петчеризации задач в ОС переключает эти задачи, т.о., достигается многозадачность в
среде двух и более процессов. Если приложению требуется выполнить одновременно
несколько действий, то это приложение необходимо разбить на несколько задач (на-
пример, с помощью системного вызова fork в UNIX). У этого подхода есть несколь-
ко серьезных недостатков: 1) задачи являются ограниченным ресурсом (большинство
ОС могут управлять лишь несколькими сотнями одновременно выполняющихся за-
дач); 2) запуск новой задачи требует много времени и системных ресурсов; 3) новая
задача не имеет доступа к памяти родительского процесса.
В ОС с многопоточной многозадачностью наименьшей исполняемой единицей
является поток, а не процесс. Процесс может состоять из одного или нескольких по-

10
токов. Создание нового потока требует немного системных ресурсов; потоки в одном
процессе имеют доступ к одному адресному пространству; переключение между по-
токами одного процесса требует небольших системных затрат.

3.2 Процессы и сообщения


В Windows окна принадлежат потокам. У каждого потока есть собственная
очередь сообщений. В нее ОС помещает сообщения для окон данного потока. Очере-
ди разных потоков независимы. Т.о. Windows обеспечивает каждому потоку среду, в
которой он может считать себя единственным и самостоятельно управлять фокусом
ввода с клавиатуры, активизировать окна, захватывать мышь и т.д.
Поток Windows не обязательно должен владеть окнами и содержать цикл обра-
ботки сообщений. Например, рассмотрим некоторое математическое приложение, в
котором надо выполнить вычисления с элементами большого двумерного массива
(матрицы). Проще всего сделать это с помощью цикла. В Win32 этот цикл можно по-
местить в отдельный поток, параллельно с которым первичный поток приложения
продолжит обработку поступающих сообщений. Поток для вычислений не имеет
окон, очереди сообщений и цикла обработки сообщений. При таком подходе прило-
жение не будет выглядеть "зависшим" в течение выполнения вычислений.
Хотя на уровне ОС потоки не делятся на типы, но в библиотеке классов MFC
на Си++ они называются и оформляются по разному: рабочие потоки (без окон и об-
работки сообщений) и потоки пользовательского интерфейса.

4. Вызовы функций Windows API


Приложение обращается к Windows при помощи так называемых системных
вызовов. Они составляют интерфейс прикладного программирования (Application
Programming Interfaces, API). Для программистов вместо термина "вызов" м.б. удоб-
нее использовать термин "функция". Функции API располагаются в системных дина-
мических библиотеках (DLL). Существуют функции для выделения памяти, управле-
ния процессами, окнами, файлами, для рисования графических примитивов и др.
Обращение к функциям API из большинства сред разработки на Си++ осуще-
ствляется очень просто, т.к. API специально спроектирован для использования в сре-
де Си/Си++. В текст программы надо включить заголовочный файл, содержащий
описания функций API (windows.h) и в процессе компоновки использовать необхо-
димые библиотеки (Visual C++ обычно подключает их автоматически). После этого в
текст программы можно включать любые обращения к API.
"Базовый" набор системных вызовов Windows можно разделить на три группы:
• функции модуля KERNEL.DLL (управление процессами, потоками, ресур-
сами, файлами и памятью);
• функции модуля USER.DLL (работа с пользовательским интерфейсом,
например, с окнами, элементами управления, диалогами и сообщениями);
• функции модуля GDI.DLL (аппаратно-независимый графический вывод).
Windows содержит большое количество вспомогательных API. Есть отдельные
API для работы с электронной почтой (MAPI), модемами (TAPI), базами данных
(ODBC). Степень интеграции этих API в системное ядро различна. Например, хотя
интерфейс OLE и реализован в виде набора системных динамических библиотек, но

11
рассматривается как часть "ядра" Windows. Остальные API, например, WinSock, мож-
но рассматривать как дополнительные.
Различие между тем, что следует считать "ядром" Windows, а что – дополни-
тельными модулями, довольно произвольно. C точки зрения приложения практически
нет разницы между функцией API из ядра, например, из модуля Kernel, и функцией,
реализованной в одной из библиотек DLL.

4.1 Функции ядра (модуль Kernel)


Функции ядра обычно делятся на категории: управление файлами, памятью,
процессами, потоками и ресурсами. В эти категории попадают далеко не все, а только
наиболее часто используемые функции модуля Kernel.
Для доступа к файлам в Windows можно пользоваться обычными потоками
Си++ или функциями библиотеки Си. Но функции модуля Kernel имеют больше воз-
можностей. Эти функции работают с файлами как с файловыми объектами. Напри-
мер, они позволяют создавать файлы, отображаемые в памяти.
В отличие файлового доступа, библиотечных возможностей Си (функция
malloc()) или Си++ (оператор new) для большинства приложений оказывается
вполне достаточно. В Win32 эти вызовы автоматически преобразуются в соответст-
вующие вызовы Win32 API. Приложения со специфическими требованиями к управ-
лению памятью могут пользоваться более сложными функциями, например, для рабо-
ты с виртуальной памятью (например, чтобы выделить блок адресного пространства
размером несколько сотен мегабайт, но не передавать под него физическую память).
Один из важнейших аспектов в управлении потоками – синхронизация. Т.к.
Windows является системой с вытесняющей многозадачностью, то потоки не могут
получить никаких сведений о состоянии выполнения других потоков. Чтобы гаранти-
ровать, что несколько потоков всегда выполняются в заданном порядке, например, по
очереди, и чтобы избежать ситуации блокировки (когда два или более потоков приос-
танавливаются навсегда), требуется применять один из механизмов синхронизации. В
Win32 это делается с помощью объектов синхронизации, которыми потоки пользуют-
ся, чтобы проинформировать другие потоки о своем состоянии, для защиты фрагмен-
тов кода от повторного вхождения, или для получения информации о других потоках.
В Win32 многие ресурсы ядра (не следует путать их с ресурсами пользователь-
ского интерфейса) представляются в виде объектов ядра. Примерами являются фай-
лы, потоки, процессы, объекты синхронизации. Для обращения к объекту применяют-
ся уникальные идентификаторы – дескрипторы (описатели). Некоторые функции
предназначены для выполнения действий, общих для всех объектов (например, от-
крытие или закрытие), а другие – для манипуляций с объектами специфического типа.
Модуль Kernel обеспечивает функции для управления ресурсами пользова-
тельского интерфейса. Они включают в себя пиктограммы, курсоры, шаблоны диа-
логов, строковые ресурсы, битовые карты и др. Функции Kernel не учитывают назна-
чение ресурса. Они обеспечивают выделение памяти для ресурсов, загрузку ресурсов
с диска (обычно из исполняемого файла приложения), удаление ресурсов из памяти.

12
4.2 Функции пользовательского интерфейса (модуль User)
Функции модуля User предназначены для управления компонентами пользова-
тельского интерфейса: окнами, диалогами, меню, курсорами, элементами управления,
буфером обмена и др. Эти ресурсы называются объектами модуля User.
В модуле Kernel есть функции для выделения памяти, управления памятью и
некоторые другие, необходимые для функционирования окон. Модуль GDI обеспечи-
вает рисование графических примитивов. Но именно модуль User, используя возмож-
ности двух других модулей, реализует высокоуровневые компоненты пользователь-
ского интерфейса, например, окно.
Функции управления окнами позволяют изменять размеры окна, его местопо-
ложение на экране и внешний вид, заменять оконную процедуру, разрешать и запре-
щать работу окна, получать информацию о свойствах окна. Эти функции также ис-
пользуются для работы с элементами управления, такими, как кнопки, полосы про-
крутки или строки ввода. В модуле User есть функции для управления дочерними ок-
нами программ с многодокументным интерфейсом (MDI).
Перечислим еще несколько групп функций модуля User:
• работа с меню (создание, отображение, изменение строки меню и выпа-
дающих меню);
• управление формой и положением указателя мыши и текстового курсора;
• работа с буфером обмена;
• управление сообщениями и очередью сообщений потока.
Приложения могут использовать функции User для проверки содержимого оче-
редей сообщений, для получения и обработки сообщений, а также для посылки сооб-
щений каким-либо окнам. Сообщения любому окну можно посылать либо синхронно
(функция SendMessage), либо асинхронно (PostMessage). При асинхронной посыл-
ке сообщение просто помещается в очередь сообщений потока, который владеет ок-
ном-адресатом. В отличие от него, синхронное сообщение передается непосредствен-
но в оконную процедуру окна-адресата. Функция SendMessage возвращает управле-
ние только после обработки сообщения окном-адресатом. Этот позволяет приложе-
нию-передатчику получить результат обработки перед продолжением выполнения.

4.3 Функции графического вывода (модуль GDI)


Функции модуля GDI (Graphics Device Interface, интерфейс графических уст-
ройств) обеспечивают рисование графических примитивов аппаратно-независимым
образом. Для абстракции от конкретного устройства применяется понятие контекст
устройства. Это системный объект, обеспечивающий интерфейс с конкретным гра-
фическим устройством. С помощью контекста можно выполнять графический вывод
на устройство или получать информацию о его свойствах (имя устройства, разреше-
ние, цветовые возможности и др.).
При рисовании примитивов (отрезков, прямоугольников, эллипсов, текста и
т.п.) функциям рисования передается дескриптор контекста устройства. Контекст
устройства преобразует общие, независимые от устройства, графические команды в
набор команд конкретного устройства. Например, когда приложение вызывает из GDI
функцию Ellipse, то сначала контекст устройства определяет, какой драйвер будет
выполнять это действие. Затем драйвер устройства может передать вызов аппаратно-
му ускорителю (если он есть в видеоадаптере) или нарисовать эллипс по точкам.

13
Контексты устройств GDI представляют широкий спектр устройств. Типичны-
ми контекстами являются дисплейный контекст устройства (для выполнения вывода
непосредственно на экран компьютера), контекст устройства в памяти (для рисо-
вания поверх изображения, хранящегося в оперативной памяти), и принтерный кон-
текст устройства (при выводе в этот контекст вызовы приложения в конечном сче-
те преобразуются в управляющие коды принтера и посылаются на принтер).
Рисование в контексте устройства обычно производится в логических коорди-
натах. Эти координаты описывают примитивы посредством аппаратно-независимых
физических единиц. Например, при задании прямоугольника можно указать его раз-
меры в сантиметрах. GDI автоматически выполняет преобразование логических ко-
ординат в физические координаты устройства. В качестве параметров преобразования
приложение может задать начало координат и размер области вывода в логических и
физических координатах. От положения начала координат в обеих системах зависит
горизонтальное и вертикальное смещение примитивов в области вывода. Размер об-
ласти вывода определяет ориентацию и масштаб примитивов после преобразования.
При рисовании примитивов часто указываются их свойства: цвет, толщина и
стиль линии и т.п. Для этого служат объекты GDI: перья, кисти, шрифты, растровые
изображения и палитры. Их можно создавать и выбирать в контексте устройства для
того, чтобы рисуемые впоследствии примитивы выглядели нужным образом.
Кроме рисования графических примитивов, часто используются функции GDI
для бит-блиттинга, предназначенные для быстрого копирования растровых изобра-
жений. В некоторых приложениях их скорости недостаточно, т.к. при рисовании эти
функции используют контекст устройства в качестве посредника между приложением
и устройством. Поэтому для непосредственного обращения к видеосистеме был раз-
работан менее безопасный, но существенно более быстрый набор функций для мани-
пуляций с растровыми изображениями – DirectDraw API.
Модуль GDI обеспечивает работу с регионами (структурами, описывающими
экранные области, возможно, непрямоугольные) и выполняет отсечение (clipping).
Отсечение – это очень важная операция для среды Windows, т.к. она позволяет при-
ложениям рисовать на экране, не отслеживая границ области рисования (например,
границы клиентской области окна). Отсечение применяется для корректного отобра-
жения на экране перекрывающихся окон.

4.4 Дополнительные API


Возможности Windows не исчерпываются набором функций, содержащихся в
трех модулях "ядра". Существует также большое количество других динамических
библиотек с дополнительными API, например:
• Стандартные элементы управления. Эти функции позволяют работать с элемен-
тами управления, появившимися в Windows 95.
• Стандартные диалоговые окна. Приложение может пользоваться стандартными
окнами для открытия файла для чтения или записи, для выбора цвета из цветовой
палитры, для выбора шрифта из набора установленных шрифтов, и некоторыми
др. Эти диалоги можно использовать в стандартном виде или модифицировать их
поведение путем замены оконных процедур.
• MAPI служит стандартным интерфейсом для доступа к различным программам
электронной почты и электронных сообщений.

14
• DirectX API. Для некоторых приложений (в особенности, игр) опосредованный
доступ к аппаратным устройствам через драйверы Windows оказывается неэффек-
тивным. Поэтому Microsoft разработала набор технологий под общим названием
DirectX, который ускоряет доступ к аппаратуре. В набор библиотек DirectX входит
библиотека DirectDraw для экранных операций, DirectInput для чтения информа-
ции с устройств ввода, DirectSound для работы со звуком и Direct3D для построе-
ния трехмерных сцен.
• ActiveX, COM и OLE. Эти технологии предназначены для создания объектов, рас-
пределенных между приложениями. Они включают в себя создание контейнеров и
серверов OLE, реализующих вставку объектов OLE в документы приложений-
контейнеров (например, как редактор формул в MS Word), автоматизацию OLE,
интерфейс "перетащи и оставь" и элементы управления ActiveX. В настоящее вре-
мя Microsoft использует ActiveX в качестве основного механизма взаимодействия
прикладных программ с системными службами Windows.
• TAPI предоставляет доступ к телефонному оборудованию. Это аппаратно-
независимый интерфейс для работы с модемами, факс-модемами, аппаратурой го-
лосовой телефонии.
В Windows есть отдельные API для работы с сетями, например, WinSock (биб-
лиотека сокетов Windows), RAS (Remote Access Service, сервис удаленного доступа) и
RPC (библиотека вызова удаленных процедур).
При программировании на Си с использованием API исходный текст программ
получается довольно громоздким. Программирование существенно упрощается при
использовании библиотек классов вроде MFC и языка Си++. Но еще одно изменение
стиля программирования происходит на уровне API. В прошлом, когда компания Mi-
crosoft включала в Windows новую возможность, она также расширяла описание ин-
терфейса API – набора системных вызовов. Сейчас многие новые механизмы Win-
dows (например, DirectX) не поддерживают традиционного API. Вместо этого они ис-
пользуют технологию ActiveX. Термином ActiveX теперь принято обозначать по-
следние версии стандартов, которые ранее назывались OLE и COM.

4.5 Соглашение о кодах ошибок


Большинство функций Windows API применяют единый способ возврата оши-
бок. Когда происходит ошибка, эти функции записывают ее код в специальную пере-
менную потока. Приложение может получить значение этой переменной с помощью
функции GetLastError. 32-разрядные значения кодов ошибок определены в заголо-
вочном файле winerror.h и в заголовочных файлах дополнительных API.
Функции приложения также могут записывать собственные коды ошибок в эту
переменную с помощью функции SetLastError. Внутренние ошибки приложения
должны иметь коды с установленным 29-м битом. Этот диапазон кодов специально
зарезервирован для использования приложениями в собственных целях.

5. Различия между программными платформами

5.1 Windows NT
В Windows NT реализован наиболее полный вариант Win32 API. Начиная с
версии 4.0, в Windows NT тот же пользовательский интерфейс, что и у Windows 95.

15
В WinNT реализована полная внутренняя поддержка двухбайтной символьной
кодировки Unicode, мощные средства безопасности и серверные возможности. WinNT
предоставляет больше удобств программам-серверам, чем Win95. Полностью 32-
разрядная система, WinNT оказывается наиболее устойчивой и лучше всего подходя-
щей для разработки программного обеспечения.
С другой стороны, WinNT является более медленной и требовательной к аппа-
ратной ресурсам. Для работы WinNT необходимо 32 Мб ОЗУ и порядка 1 Гб жестко-
го диска (хотя в настоящее время это не чрезмерные требования).
Некоторые части API модуля Kernel специфичны для WinNT. В NT Kernel есть
набор функций для проверки и модификации свойств безопасности объектов ядра.
Например, поток не сможет работать с файловым объектом, если он не имеет прав
доступа, соответствующих свойствам безопасности файлового объекта.
У модулей GDI WinNT и Win95 есть различия в области преобразования коор-
динат. В Win95 координаты задаются 16-разрядными числами (для обеспечения со-
вместимости со старыми программами для Windows 3.1). В WinNT координаты явля-
ются 32-разрядными, что делает эту ОС более удобной для сложных графических
приложений, например, для программ САПР.

5.2 Windows 95
Хотя в Win95 нет многих возможностей WinNT, но она обеспечивает более вы-
сокую производительность и совместимость со старыми приложениями и дешевым и
старым оборудованием. Большинство возможностей, отсутствующих в Win95, не
важны для домашнего применения или для рабочих станций.
В Win95 нет средств безопасности NT и поддержки Unicode. С другой стороны,
поддержка DirectX API в Win95 реализована полнее и эффективнее, чем в WinNT.
Для обеспечения переносимости на разные процессоры большая часть кода
WinNT была разработана на относительно высоком уровне – на языках Си/Си++. В
Win95 включено большое количество специфического для микропроцессоров кода из
Windows 3.1. Существенный объем нового кода также был оптимизирован именно
для этих микропроцессоров. Поэтому требования к ресурсам у Win95 меньше и эта
ОС неплохо работает на старых машинах, например, на ПК с процессором 486.
Для Visual C++ система Win95 обеспечивает полностью работоспособную сре-
ду разработки. Эта ОС стабильна, хотя и не настолько, как WinNT. Все 32-разрядные
утилиты разработки Visual C++, включая консольные приложения, работают и в
Win95. Для малых и средних проектов оказывается достаточно даже очень медленной
машины (вроде ноутбука 25 MHz 486 CPU, 8MB RAM и 120MB HDD).

5.3 Другие платформы


Существуют версии Windows NT для компьютеров с процессорами PowerPC,
DEC Alpha и MIPS. Эти реализации полностью совместимы с версией для процессо-
ров Intel. Приложения, написанные в соответствии с документацией по API, будут пе-
рекомпилироваться для других платформ без каких-либо изменений исходного тек-
ста. Для этого необходима версия Visual C++, соответствующая версии Windows NT.
Кросс-платформная разработка для Windows NT не поддерживается.
Visual C++ можно применять для разработки программ для встроенных систем
и компактных компьютеров, работающих под управлением усеченной версии Win-

16
dows – Windows CE. ОС Windows CE была выпущена в 1997 г. Она предназначена для
портативных устройств, например, ручных компьютеров и автомобильных проигры-
вателей компакт-дисков. Основное назначение Windows CE – "сделать все макси-
мально малым и компактным". В Windows CE реализовано небольшое подмножество
Win32 API. Разработка программ для этой ОС производится в кросс-платформном
режиме, с помощью надстройки Visual C++ for Windows CE. Эту надстройку можно
использовать и в Windows NT, и в Windows 95.

6. Резюме
32-разрядные ОС Windows делятся на два семейства: Windows NT/2000 и Win-
dows 95/98/ME. У этих ОС есть общий набор функций, доступных для вызова из при-
ложений – Win32 API. Два семейства ОС различаются полнотой реализации Win32
API. Наиболее полная реализация выполнена в Windows NT. Для разработки про-
грамм для 32-разрядных ОС Windows можно использовать среду Visual C++.
Главной частью любого приложения Windows является цикл обработки сооб-
щений. ОС Windows передает совместно работающим приложениям информацию о
различных событиях в форме сообщений. Приложения обрабатывают сообщения, на-
правляя их в соответствующих оконные процедуры.
Окно – это не только прямоугольная экранная область, это системный объект,
обеспечивающий прием и обработку сообщений. Окна принадлежат потокам. Потоки
– это одновременно исполняемые части внутри одного процесса (т.е. приложения).
Потоки принадлежат процессам.
Приложения взаимодействуют с Windows, вызывая функции API. Они реализо-
ваны или в "ядре" Windows, или в одном из множества дополнительных модулей. В
"ядре" можно выделить три основных части: модуль Kernel (управление памятью,
файлами, потоками и процессами), модуль User (управление элементами пользова-
тельского интерфейса, в т.ч. окнами и обработкой сообщений) и модуль GDI (функ-
ции графического отображения на различных устройствах вывода).
Остальные системные модули обеспечивают специфические возможности, на-
пример, ActiveX, MAPI, работа с сетью, стандартные элементы управления и диало-
говые окна, средства мультимедиа.

7. Упражнения
1) В [9] прочитайте приложение 2, "Основные типы сообщений Windows". В Visual
C++ откройте файл winuser.h и с помощью контекстного поиска найдите описа-
ние символических идентификаторов каких-нибудь оконных сообщений WM_... ,
а также структуры сообщения MSG.
2) В справочной системе Visual C++ найдите описание сообщения с требованием пе-
рерисовки окна WM_PAINT, сообщения о нажатии левой кнопки мыши
WM_LBUTTONDOWN и какого-нибудь сообщения, найденного вами в файле
winuser.h. Обратите внимание на смысл переменных wParam и lParam в струк-
туре MSG для этих сообщений.
3) Найдите в справочной системе Visual C++ описание функции API GetMessage.
Выясните, как в окне справки работают кнопки "Quick Info (краткая сводка о свой-
ствах функции)", "Overview (краткое описание группы функций)" и "Group (вызов
списка функций данной категории ". Получите таким же образом справку по

17
функции GetKeyboardState и посмотрите, какие функции входят в группу
функций API для работы с клавиатурой.
4) В [1] прочитайте Гл.3, занятие 2 "Архитектура Win32-приложения".
5) В [1] прочитайте и выполните задания из Гл.13, занятия 6 "Применение Spy++".
Особое внимание обратите на то, как получить информацию о каком-либо из
имеющихся окон с помощью инструмента Finder Tool и как просмотреть прото-
кол сообщений, посылаемых какому-либо окну.
6) Изучите англо-русский словарь терминов по теме 1-й лекции (см. CD-ROM).

18
Лекция 2. Структура приложения Windows
1. Простейшее Windows-приложение "Hello, World!"
Приложение (программа 2.1), которое выводит на экран диалоговое окно, пока-
занное на рис. 2.1, вполне можно считать аналогом классической программы "Hello,
World!".

Рис. 2.1. Простейшее Windows-приложение "Hello, World!"

#include <windows.h>

int WINAPI WinMain( HINSTANCE d1, HINSTANCE d2, LPSTR d3, int d4 )
{
MessageBox( NULL, "Hello, World!", "", MB_OK );
return 0;
}
Программа 2.1. Простейшее Windows-приложения "Hello, World!".

C точки зрения программирования у этого "приложения" довольно сложное


поведение. Традиционная консольная версия "Hello, World!" выводит сообщение на
экран и заканчивает работу. Windows-приложение после вывода сообщения продол-
жает работать. На экране присутствует диалоговое окно, которое можно переместить
по экрану мышью. Мышью можно нажать кнопку OK для завершения работы прило-
жения. Если нажать мышью кнопку OK, но не отпускать левую кнопку мыши, то на
экране будет видна кнопка OK, нарисованная в нажатом состоянии. Если, не отпуская
кнопку мыши, перемещать указатель то на кнопку OK, то вне ее, эта кнопка будет
менять свой внешний вид. У окна приложения есть небольшое меню (с единственной
командой Переместить), которое можно вызвать нажатием комбинации Alt+Пробел
или щелчком правой кнопки на заголовке окна. Приложение можно завершить кла-
вишей Enter, Escape или пробел.
Как же такое сложное поведение обеспечивается всего шестью строками ис-
ходного текста? Суть скрыта в понятиях цикл обработки сообщений и оконная проце-
дура. В данном простейшем приложении не видно ни того, ни другого. Приложение
создает стандартное диалоговое окно, внутри которого скрыта реализация наблюдае-
мого поведения. Поэтому для ясного понимания структуры этих компонент приложе-
ния следует рассмотреть более сложный пример.

2. Приложение с циклом обработки сообщений


В программе 2.1 практически вся функциональность приложения сосредоточе-
на (и скрыта) внутри функции MessageBox. Для лучшего понимания структуры при-
ложения сделаем реализацию поведения видимой, другими словами, создадим окно,
которым будем управлять сами, а не доверять это функции MessageBox.

19
Окно новой версии hello.cpp показано на рис. 2.2. Теперь слова "Hello,
World!" выводятся как надпись на кнопке, занимающей всю клиентскую область окна
(они также появляются в строке заголовка окна).

Рис. 2.2. Приложение "Hello, World!" с собственным циклом обработки сообщений.

"Типичное" приложение Windows в процессе инициализации сначала регист-


рирует новый оконный класс для своего главного окна. Затем приложение создает
свое главное окно. Сейчас пока не будем регистрировать новый оконный класс, а ис-
пользуем один из стандартных оконных классов, BUTTON ("Кнопка"). Поведение этого
класса не позволит в точности повторить приложение из п.1. В данный момент это не
важно, главное, что в приложении можно будет увидеть назначение цикла обработки
сообщений (программа 2.2).
#include <windows.h>

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE d2, LPSTR d3, int d4 )
{
HWND hwnd;
hwnd = CreateWindow( "BUTTON", "Hello, World!",
WS_VISIBLE | BS_CENTER, 100, 100, 100, 80,
NULL, NULL, hInstance, NULL );
MSG msg;
while ( GetMessage( &msg, NULL, 0, 0 ) )
{
if ( msg.message == WM_LBUTTONUP )
{
DestroyWindow( hwnd );
PostQuitMessage( 0 );
}
DispatchMessage( &msg );
}
return msg.wParam;
}
Программа 2.2. Приложение "Hello, World!" с циклом обработки сообщений.

В данном примере виден цикл обработки сообщений. После создания окна про-
грамма входит в цикл while, в котором выполняется вызов функции Windows API
для получения очередного сообщения – GetMessage. Эта функция возвращает значе-
ние FALSE только при получении сообщения WM_QUIT о завершении приложения. В
цикле обрабатывается единственное сообщение, WM_LBUTTONUP, об отпускании левой
кнопки мыши. Функция DestroyWindow уничтожает окно приложения, а
PostQuitMessage посылает приложению сообщение WM_QUIT. Поэтому при оче-
редном вызове GetMessage цикл обработки сообщений завершится. Все сообщения,
кроме WM_LBUTTONUP, передаются функции DispatchMessage.
Диспетчеризация с помощью функции DispatchMessage означает передачу
сообщений в оконную процедуру, "по умолчанию" приписанную оконному классу
BUTTON. Как и в случае функции MessageBox, содержание оконной процедуры "по
умолчанию" скрыто, т.к. она является частью операционной системы.

20
Обратите внимание, что приложение из п.1 завершает работу, только если ука-
затель в момент отпускания левой кнопки находится над кнопкой. В новой версии
приложения выход из программы осуществляется по сообщению об отпускании ле-
вой кнопки, независимо от положения указателя.
Рассмотренный пример не продемонстрировал строение оконной процедуры.
Поэтому еще раз усложним "Hello, World!", чтобы в этом приложении был виден и
цикл обработки сообщений, и оконная процедура.

3. Приложение с циклом обработки сообщений и оконной процедурой


Новая версия hello.cpp (программа 2.3) регистрирует собственный оконный
класс. Это делается частично для косметических улучшений (чтобы отказаться от не-
естественного применения класса BUTTON для вывода сообщения), но главным обра-
зом для того, чтобы установить собственную оконную процедуру.

Рис. 2.3. Версия приложения "Hello, World!" с собственным оконным классом.

#include <windows.h>

void DrawHello( HWND hwnd )


{
PAINTSTRUCT paintStruct;
HDC hDC = BeginPaint( hwnd, &paintStruct );

if ( hDC != NULL )
{
RECT clientRect;
GetClientRect( hwnd, &clientRect );
DPtoLP( hDC, (LPPOINT)&clientRect, 2 );
DrawText( hDC, "Hello, World!", -1, &clientRect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE );
EndPaint( hwnd, &paintStruct );
}
}

LRESULT CALLBACK WndProc( HWND hwnd, UINT uMsg,


WPARAM wParam, LPARAM lParam )
{
switch(uMsg)
{
case WM_PAINT : DrawHello( hwnd ); break;
case WM_DESTROY : PostQuitMessage( 0 ); break;
default : return DefWindowProc( hwnd, uMsg, wParam, lParam );
}
return 0;
}

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,


LPSTR d3, int nCmdShow )
{
if ( hPrevInstance == NULL )
{

21
WNDCLASS wndClass;
memset( &wndClass, 0, sizeof(wndClass) );
wndClass.style = CS_HREDRAW | CS_VREDRAW;
wndClass.lpfnWndProc = WndProc;
wndClass.hInstance = hInstance;
wndClass.hCursor = LoadCursor( NULL, IDC_ARROW );
wndClass.hbrBackground = (HBRUSH)( COLOR_WINDOW + 1 );
wndClass.lpszClassName = "HELLO";
if ( !RegisterClass( &wndClass ) )
return FALSE;
}

HWND hwnd;
hwnd = CreateWindow( "HELLO", "HELLO", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,
NULL, NULL, hInstance, NULL );
ShowWindow( hwnd, nCmdShow );
UpdateWindow( hwnd );

MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) )
DispatchMessage( &msg );

return msg.wParam;
}
Программа 2.3. Приложение "Hello, World!" с собственным оконным классом.

Новая версия приложения содержит примерно 60 строк исходного текста. Но


оно выглядит как "полноценное" Windows-приложение (рис. 2.3), у которого есть сис-
темное меню, окно которого можно перемещать, изменять размер, сворачивать и
разворачивать, оно умеет перерисовывать себя, реагировать на команду меню За-
крыть и на комбинацию клавиш Alt+F4.
Как и раньше, выполнение программы начинается с функции WinMain. Снача-
ла приложение проверяет, есть ли уже запущенный экземпляр данного приложения.
Если есть, то оконный класс повторно регистрировать не надо. Иначе выполняется
регистрация оконного класса, свойства и поведение которого описываются с помо-
щью структуры WNDCLASS. В переменную lpfnWndProc этой структуры помещается
адрес оконной процедуры. В нашем примере это будет функция WndProc.
Далее, вызывается функция CreateWindow для создания окна. После вывода
окна на экран WinMain входит в цикл обработки сообщений. Этот цикл завершится,
когда GetMessage вернет FALSE в результате получения сообщения WM_QUIT.
Функция WndProc демонстрирует назначение и структуру оконной процедуры,
которая не была видна в предыдущих версиях "Hello, World!". Типичная оконная
процедура на языке Си состоит из большого оператора switch. В зависимости от по-
лученного сообщения, из этого оператора вызываются различные функции для обра-
ботки конкретных сообщений. В нашем примере обрабатываются только два сообще-
ния: WM_PAINT и WM_DESTROY.
Сообщение WM_PAINT требует от приложения частично или полностью пере-
рисовать содержимое окна. Большинство приложений перерисовывают только те об-
ласти окна, которые нуждаются в перерисовке. В нашем случае, для простоты, на ка-
ждое сообщение WM_PAINT всегда выполняется вывод всей строки "Hello, World!".
Сообщение WM_DESTROY поступает в результате действий пользователя, кото-
рые приводят к уничтожению окна приложения. В качестве реакции наше приложе-
ние вызывает функцию PostQuitMessage. Т.о. гарантируется, что функция

22
GetMessage в WinMain получит сообщение WM_QUIT и главный цикл обработки со-
общений завершится.
Сообщения, которые не обрабатываются нашей оконной процедурой, с помо-
щью функции DefWindowProc передаются в оконную процедуру по умолчанию. Эта
функция реализует поведение окна приложения и многих компонент его неклиент-
ской области (например, строки заголовка).

4. Регистрация оконного класса и создание окна

4.1 Функция RegisterClass и структура WNDCLASS


Оконный класс задает общее поведение окон нового типа, главное, он содер-
жит адрес оконной процедуры. Для регистрации нового оконного класса предназначе-
на функция:
ATOM RegisterClass( CONST WNDCLASS* lpwc );
Единственный параметр этой функции, lpwc, является указателем на структуру
типа WNDCLASS, описывающую новый тип окна. Возвращаемое значение имеет тип
Windows atom, это 16-разрядное число, являющееся идентификатором уникальной
символьной строки в служебной внутренней таблице Windows. Структура WNDCLASS
имеет следующее описание:
typedef struct _WNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS;

Смысл некоторых переменных очевиден. Например, hIcon является дескрип-


тором пиктограммы, используемой для отображения окон данного класса в свернутом
виде. hCursor – это дескриптор стандартного указателя мыши, который устанавлива-
ется при перемещении указателя над областью окна; hbrBackground – дескриптор
кисти (это объект модуля GDI), применяемой для рисования фона окна. Cтрока
lpszMenuName является идентификатором ресурса меню (символьное имя меню или
целочисленный идентификатор, присваиваемый с помощью макроса
MAKEINTRESOURCE), которое будет стандартным верхним меню для окон данного
класса. Строка lpszClassName является именем оконного класса.
Переменные cbClsExtra и cbWndExtra можно использовать для выделения
под оконный класс или для каждого экземпляра окна некоторой дополнительной па-
мяти. Приложения могут пользоваться ею для хранения некоторой собственной ин-
формации, имеющей отношение к оконному классу или конкретным окнам.
Особенно важны первые две переменные структуры WNDCLASS. Большая часть
свойств, делающих окно уникальным и сложным объектом, управляется именно эти-
ми переменными. В них хранится стиль (style) оконного класса и адрес оконной
процедуры (lpfnWndProc).

23
Оконная процедура – это функция, ответственна за обработку всех сообщений,
получаемых окном. Она может обрабатывать эти сообщения самостоятельно, или пе-
редавать их оконной процедуре "по умолчанию", DefWindowProc. Сообщения несут
самую разнообразную информацию: об изменении размеров и местоположения окна,
о событиях мыши, клавиатуры, командах пользователя, требования перерисовки, со-
бытия таймера и других аппаратных устройств и т.п.
Существует аналог DefWindowProc, применяемый для диалоговых окон –
функция DefDlgProc. Эта оконная процедура "по умолчанию" разработана специ-
ально для диалоговых окон. Она обеспечивает обслуживание элементов управления,
например, переключение фокуса ввода.
С помощью стиля оконного класса, переменной style, задаются некоторые
глобальные свойства оконного класса. Значение стиля является комбинацией значе-
ний битовых флагов (эта комбинация получается с помощью побитовой операции
ИЛИ, т.е. оператора |). Например, флаг CS_DBLCLKS указывает Windows, что для
окон данного класса надо генерировать сообщения о двойном щелчке мышью. Пара
флагов CS_HREDRAW и CS_VREDRAW означают, что окно должно полностью перерисо-
вываться после любого изменения горизонтального или вертикального размера.

4.2 Создание окна с помощью функции CreateWindow


Регистрация нового оконного класса – это только первый шаг в создании окна.
Затем приложения обычно создают окна с помощью функции CreateWindow. Пара-
метры этой функции задают более частные свойства экземпляра нового окна, напри-
мер, его размеры, местоположение и внешний вид.
HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName,
DWORD dwStyle, int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU hMenu, HANDLE hInstance,
LPVOID lpParam );
Параметр lpClassName – это имя класса, чье поведение и свойства унаследует
новое окно. Это может быть класс, зарегистрированный функцией RegisterClass,
или один из стандартных оконных классов (например, классы элементов управления:
BUTTON, COMBOBOX, EDIT, SCROLLBAR, STATIC).
Параметр dwStyle задает стиль окна. Его не следует путать со стилем оконно-
го класса, который при регистрации оконного класса передается функции
RegisterClass внутри структуры WNDCLASS. Стиль класса задает некоторые посто-
янные свойства окон данного класса, общие для всех окон. Стиль окна, передаваемый
в CreateWindow, используется для инициализации более кратковременных свойств
конкретного окна. Например, dwStyle можно применять для задания начального ви-
да окна (свернутое, развернутое, видимое или скрытое). Как и для стиля класса, стиль
окна обычно является комбинацией битовых флагов (которая строится с помощью
оператора |). Кроме общих флагов, имеющих смысл для окон всех классов, некото-
рые флаги имеют смысл только для стандартных оконных классов. Например, стиль
BS_PUSHBUTTON используется для окон класса BUTTON, которые должны выглядеть
как нажимаемые кнопки и посылать по щелчку мыши своим родительским окнам со-
общения WM_COMMAND.
Стили WS_POPUP и WS_OVERLAPPED задаются окнам верхнего уровня. Основ-
ное различие в том, что у окон WS_OVERLAPPED всегда есть заголовок, а у окон

24
WS_POPUP он не обязателен. Перекрывающиеся окна обычно используются в качестве
главных окон приложений, а всплывающие окна – как диалоговые окна.
При создании окна верхнего уровня вызывающее приложение задает его роди-
тельское окно с помощью параметра hwndParent. Родительским окном для окна
верхнего уровня служит окно рабочего стола.
Дочерние окна создаются с использованием стиля WS_CHILD. Основное разли-
чие между дочерним окном и окном верхнего уровня в том, что дочернее окно заклю-
чено внутри клиентской области своего родительского окна.
В Windows определены некоторые комбинации стилей, удобные для создания
"типичных" окон. Стиль WS_OVERLAPPEDWINDOW является комбинацией флагов
WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX и
WS_MAXIMIZEBOX. Такая комбинация применяется при создании типичного главного
окна приложения. Стиль WS_POPUPWINDOW является комбинацией флагов WS_POPUP,
WS_BORDER и WS_SYSMENU. Этот стиль применяется для создания диалоговых окон.

5. Рисование содержимого окна


Рисование в окне выполняется с помощью функций модуля GDI. Приложение
обычно получает дескриптор контекста устройства, связанного с клиентской обла-
стью окна, (например, с помощью функции GetDC) и затем вызывает функции GDI
вроде LineTo, Rectangle или TextOut.

5.1 Сообщение WM_PAINT


Сообщение WM_PAINT посылается окну, когда его части нуждаются в перери-
совке и при этом в очереди сообщений потока-владельца окна больше нет никаких
сообщений. Приложения выполняют обработку WM_PAINT с помощью функций рисо-
вания, вызываемых между вызовами функций BeginPaint и EndPaint. Функция
BeginPaint возвращает набор параметров в виде структуры PAINTSTRUCT:
typedef struct tagPAINTSTRUCT {
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT;

BeginPaint при необходимости выполняет очистку фона окна. Для этого при-
ложению посылается синхронное сообщение WM_ERASEBKGND. Функция BeginPaint
должна вызываться только для обработки сообщения WM_PAINT. Каждому вызову
BeginPaint должен соответствовать последующий вызов EndPaint.
Приложения могут использовать переменную этой структуры hDC для рисова-
ния в клиентской области окна. Переменная rcPaint хранит координаты наименьше-
го прямоугольника, описывающего область, нуждающуюся в перерисовке. Ограничи-
вая отрисовку этой областью, приложения могут ускорить процесс отображения.

25
5.2 Перерисовка окна по требованию
Функции InvalidateRect и InvalidateRgn позволяют приложению объя-
вить все окно или его части "недействительными". В ответ Windows пошлет прило-
жению сообщение WM_PAINT с требованием перерисовать эти области.
Данные функции обеспечивают приложениям эффективный способ полного
или частичного обновления содержимого окон. Вместо немедленной перерисовки ок-
на, приложение может объявить область окна недействительной. При обработке со-
общения WM_PAINT приложение может учесть координаты обновляемого участка (пе-
ременную rcPaint в структуре PAINTSTRUCT) и перерисовать элементы только
внутри этой области.

6. Часто используемые сообщения управления окнами


Типичное окно реагирует не только на WM_PAINT, но и на многие другие сооб-
щения. Некоторые наиболее часто используемые сообщения перечислены ниже.
WM_CREATE. Это первое сообщение, получаемое оконной процедурой вновь
созданного окна. Оно посылается до того, как окно станет видимым и перед тем, как
функция CreateWindow вернет управление. При обработке этого сообщения прило-
жение может выполнить некоторую инициализацию, необходимую перед тем, как ок-
но станет видимым.
WM_DESTROY. Это сообщение посылается в оконную процедуру окна, которое
уже удалено с экрана и вскоре будет уничтожено.
WM_CLOSE. Сообщение посылается в окно как признак того, что оно должно
быть закрыто. При обработке по умолчанию в DefWindowProc вызывается
DestroyWindow. Приложение может, например, вывести окно подтверждения выхо-
да и вызвать DestroyWindow только если пользователь подтвердит закрытие окна.
WM_QUIT. Это сообщение является требованием завершения приложения и
обычно является последним сообщением, которое получает главное окно приложе-
ния. При его получении функция GetMessage возвращает FALSE, что в большинстве
приложений приводит к завершению цикла обработки сообщений. WM_QUIT генери-
руется в результате вызова функции PostQuitMessage.
WM_QUERYENDSESSION. Сообщение уведомляет приложение о том, что сеанс
работы Windows будет завершен. В ответ приложение может вернуть FALSE, чтобы
предотвратить закрытие Windows. После обработки WM_QUERYENDSESSION Windows
посылает всем приложениям сообщение WM_ENDSESSION с результатами обработки
сообщения WM_QUERYENDSESSION.
WM_ENDSESSION. Сообщение посылается всем приложениям после обработки
сообщения WM_QUERYENDSESSION. Оно уведомляет приложения, что Windows будет
закрыта или что процесс закрытия был прерван. Если закрытие состоится, то оно мо-
жет произойти в любой момент после того, как сообщение WM_ENDSESSION будет об-
работано всеми приложениями. Поэтому важно, чтобы приложения завершали все
свои действия для обеспечения безопасного завершения работы.
WM_ACTIVATE. Сообщение уведомляет окно верхнего уровня о том, что оно
станет активным или неактивным. При смене активного окна это сообщение сначала
посылается окну, которое будет неактивным, а потом окну, которое станет активным.

26
WM_SHOWWINDOW. Это сообщение извещает окно о том, что оно будет скрыто
или показано на экране. Окно м.б. скрыто путем вызова функции ShowWindow или в
результате перекрытия другим развернутым окном.
WM_ENABLE. Посылается окну, когда оно разрешается или запрещается. Окно
может быть разрешено или запрещено с помощью функции EnableWindow. В запре-
щенном состоянии окно не получает сообщений мыши и клавиатуры.
WM_MOVE. Извещает окно об изменении его местоположения на экране.
WM_SIZE. Сообщение WM_SIZE уведомляет окно об изменении его размеров.
WM_SETFOCUS. Это сообщение извещает окно о том, что оно получило клавиа-
турный фокус ввода. Приложение может в ответ на это сообщение включить клавиа-
турный курсор.
WM_KILLFOCUS. Уведомляет окно о потере клавиатурного фокуса ввода. Если
приложение включало курсор, то при обработке WM_KILLFOCUS его надо выключить.
WM_GETTEXT. Сообщение посылается окну как запрос на копирование текста
окна в буфер. У большинства окон текст окна – это его заголовок. Для элементов
управления вроде кнопок, строк ввода, статического текста и т.п. текст окна – это
текст, отображаемый в элементе управления. Это сообщение обычно обрабатывается
процедурой по умолчанию DefWindowProc.
WM_SETTEXT. Это сообщение требует, чтобы окно запомнило текст, передан-
ный в буфере, в качестве своего текста. При обработке WM_SETTEXT функцией
DefWindowProc выполняется запоминание и отображение текста окна.

7. Приложение с несколькими циклами обработки сообщений


В рассмотренных ранее примерах (т.е., трех версиях hello.cpp) в приложении
был только один цикл обработки сообщений. В первой версии hello.cpp он был
скрыт в системной функции MessageBox.
Приложения могут содержать любое количество циклов обработки сообщений.
Рассмотрим простейшую из подобных ситуаций, когда приложение со своим циклом
обработки сообщений вызывает функцию MessageBox. Естественно, при этом при-
ложение временно входит в цикл обработки сообщений внутри MessageBox, который
будет работать, пока диалоговое окно присутствует на экране.
Аналогичным образом, вы можете реализовать второй (или третий, или четвер-
тый) цикл обработки сообщений тогда, когда на некотором этапе выполнения вашего
приложения требуется обрабатывать сообщения иным способом, нежели это делается
при нормальном функционировании.
В качестве примера рассмотрим рисование в режиме захвата мыши. В 4-й вер-
сии hello.cpp обеспечивается рисование от руки с помощью мыши. Т.е. пользова-
тель может сам написать "Hello, World!" мышью (рис. 2.4).
Приложение обрабатывает события мыши. По нажатию левой кнопки в кли-
ентской области окна происходит захват мыши. Пока мышь захвачена, ее сообщения
передаются напрямую в окно, выполнившее захват. Сообщения информируют окно о
каждом перемещении мыши. Т.о. приложение может выполнять рисование, соединяя
отрезками текущее и предыдущее местоположения указателя мыши.
Освобождение мыши в нашем приложении происходит, когда пользователь
отпускает левую кнопку мыши.

27
В программе 2.4 есть два цикла обработки сообщений, в которых вызывается
функция GetMessage. Главный цикл расположен в WinMain, а дополнительный раз-
мещен в функции DrawHello.

Рис. 2.4. Графическая версия приложения "Hello, World!".

#include <windows.h>

void AddSegmentAtMessagePos( HDC hDC, HWND hwnd, BOOL bDraw )


{
DWORD dwPos;
POINTS points;
POINT point;

dwPos = GetMessagePos();
points = MAKEPOINTS( dwPos );
point.x = points.x;
point.y = points.y;
ScreenToClient( hwnd, &point );
DPtoLP( hDC, &point, 1 );

if ( bDraw )
LineTo( hDC, point.x, point.y );
else
MoveToEx( hDC, point.x, point.y, NULL );
}

void DrawHello( HWND hwnd )


{
if ( GetCapture() != NULL )
return;

HDC hDC = GetDC( hwnd );


if ( hDC != NULL )
{
SetCapture( hwnd );
AddSegmentAtMessagePos( hDC, hwnd, FALSE );

MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) )
{
if ( GetCapture() != hwnd )
break;

switch ( msg.message )
{
case WM_MOUSEMOVE :
AddSegmentAtMessagePos( hDC, hwnd, TRUE );
break;
case WM_LBUTTONUP:
goto ExitLoop;
default:
DispatchMessage( &msg );
}
}

ExitLoop:

28
ReleaseCapture();
ReleaseDC( hwnd, hDC );
}
}

LRESULT CALLBACK WndProc( HWND hwnd, UINT uMsg,


WPARAM wParam, LPARAM lParam )
{
switch( uMsg )
{
case WM_LBUTTONDOWN : DrawHello( hwnd ); break;
case WM_DESTROY : PostQuitMessage( 0 ); break;
default : return DefWindowProc( hwnd, uMsg, wParam, lParam );
}
return 0;
}

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,


LPSTR d3, int nCmdShow )
{
if ( hPrevInstance == NULL )
{
WNDCLASS wndClass;
memset( &wndClass, 0, sizeof( wndClass ) );
wndClass.style = CS_HREDRAW | CS_VREDRAW;
wndClass.lpfnWndProc = WndProc;
wndClass.hInstance = hInstance;
wndClass.hCursor = LoadCursor( NULL, IDC_ARROW );
wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wndClass.lpszClassName = "HELLO";
if ( !RegisterClass( &wndClass ) )
return FALSE;
}

HWND hwnd;
hwnd = CreateWindow( "HELLO", "HELLO", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,
NULL, NULL, hInstance, NULL);
ShowWindow( hwnd, nCmdShow );
UpdateWindow( hwnd );

MSG msg;
while ( GetMessage( &msg, NULL, 0, 0 ) )
DispatchMessage( &msg );

return msg.wParam;
}
Программа 2.4. Графическая версия приложения "Hello, World!".

В предыдущей версии функция DrawHello просто выводила текстовую строку


"Hello, World!" в контекст устройства, связанный с окном приложения. В новой вер-
сии эта функция устроена сложнее. Сначала она проверяет, не захвачена ли мышь ка-
ким-либо окном. Затем функция получает дескриптор контекста устройства, связан-
ного с клиентской областью главного окна приложения. Затем выполняется захват
мыши с помощью функции SetCapture. В режиме захвата мыши Windows будет по-
сылать сообщения мыши непосредственно в окно, выполнившее захват.
Функция DrawHello также вызывает вспомогательную функцию
AddSegmentAtMessagePos, которая в зависимости от своего третьего логического
параметра, или перемещает текущую позицию рисования в точку указателя мыши из
последнего сообщения, или рисует в эту точку отрезок из текущей позиции. Чтобы
29
узнать координаты указателя мыши от последнего сообщения, применяется функция
GetMessagePos. Функция AddSegmentAtMessagePos выполняет преобразование
координат из экранной системы координат, в которой заданы координаты указателя
мыши, в логическую систему координат окна, в которой выполняется рисование.
После вызова AddSegmentAtMessagePos функция DrawHello входит в цикл
обработки сообщений. Пока мышь захвачена, мы ожидаем особого поведения от при-
ложения, а именно, что траектория мыши будет отображаться внутри окна отрезками.
Это обеспечивается функцией AddSegmentAtMessagePos, которая вызывается с
третьим параметром TRUE при получении каждого сообщения WM_MOUSEMOVE.
Этот цикл обработки сообщений завершается, когда отпускается левая кнопка
мыши, или когда приложение теряет захват мыши по какой-то другой причине. Тогда
функция DrawHello возвращает управление и возобновляется выполнение главного
цикла обработки сообщений в функции WinMain.
Действительно ли необходимы в этом приложении два цикла обработки сооб-
щений? Вполне возможно обрабатывать сообщения WM_MOUSEMOVE в оконной проце-
дуре, получая их в результате диспетчеризации из главного цикла обработки сообще-
ний. Но предложенная структура программы делает исходный текст более понятным
и позволяет избежать излишне громоздкой оконной процедуры.

8. Резюме
Каждое Windows-приложение строится на основе цикла обработки сообщений.
В нем выполняется вызов функции для получения сообщения (GetMessage или
PeekMessage), и последующая диспетчеризация сообщения в соответствующую
оконную процедуру с помощью DispatchMessage.
Оконные процедуры приписываются оконным классам в момент регистрации с
помощью функции RegisterClass. Типичная оконная процедура содержит опера-
тор switch с ветвями case для всех сообщений, которые интересуют данное прило-
жение. Остальные сообщения передаются оконной процедуре "по умолчанию" с по-
мощью вызова функции DefWindowProc (или DefDlgProc для диалоговых окон).
Сообщения можно посылать окнам либо синхронно, либо асинхронно. При
асинхронной посылке сообщение помещается в очередь сообщений, откуда извлека-
ется впоследствии функцией GetMessage или PeekMessage. При синхронной пере-
даче происходит по-другому: выполняется немедленный вызов оконной процедуры с
передачей ей структуры сообщения. При этом игнорируется очередь сообщений и
цикл обработки сообщений.

9. Упражнения.
1) В Visual C++ заведите новый проект типа Win32 Application и добавьте в него ис-
ходный файл с текстом программа 2.1. Скомпилируйте и запустите ее.
2) Повторите действия, аналогичные заданию 1), для остальных версий приложения.
3) Во 2-й версии "Hello, World!" попробуйте вместо стандартного класса BUTTON
("Кнопка") создать окно класса STATIC ("Статический текст").
4) Посмотрите разделы справочной системы по функциям API, встречающимся в
тексте приложений "Hello, World!".
5) Разместите комментарии, поясняющие отдельные части 4-й версии "Hello,
World!". Например, комментарии должны отмечать такие элементы программы,
30
как цикл обработки сообщений, регистрация оконного класса или пояснять смысл
отдельных функций API (назначение неизвестных функций API выясните в спра-
вочной системе).
6) Выясните, сохраняется ли содержимое окна 3-й и 4-й версии "Hello, World!" при
перемещении окна приложения по экрану или при изменении его размеров. Объ-
ясните, что происходит.
7) Добавьте в 4-ю версию "Hello, World!" массив для хранения координат точек ри-
сунка (в качестве типа элементов массива удобно использовать структуру из API
POINT). Обеспечьте рисование содержимого окна при обработке сообщения
WM_PAINT. Теперь при изменении размеров окно не должно очищаться.
Перед написанием функции-обработчика сообщения WM_PAINT проанализируйте
содержимое функции DrawHello из п.3 и AddSegmentAtMessagePos из п.4.
8) Добавьте в одну из программ обработчик события WM_CLOSE, который бы запра-
шивал у пользователя подтверждение о выходе из программы. Для организации
запроса можно использовать стандартное окно сообщения:
MessageBox( hwnd, "Вы хотите завершить работу программы?",
"Завершение работы", MB_YESNO | MB_ICONQUESTION )
Этот вызов создает окно сообщения двумя кнопками "Да" и "Нет" и пиктограммой
в виде вопросительного знака. В зависимости от нажатой кнопки, функция вернет
IDYES или IDNO. Приложение должно завершать работу только при нажатии поль-
зователем кнопки Да.
9) Изучите англо-русский словарь терминов по теме 2-й лекции (см. CD-ROM).

31
Лекция 3. Иерархия окон Windows. Типы окон
Для пользователя окно в Windows выглядит как прямоугольная экранная об-
ласть. С системной точки зрения окно – это абстрактное понятие, обозначающее про-
стейший элемент, с помощью которого взаимодействуют пользователь и приложение.
Окна Windows разнообразны: есть и "очевидные" окна приложений и диалоговые ок-
на, и "менее очевидные", такие, как рабочий стол, пиктограммы и кнопки.
Окно – это не только область, в которую приложение выводит свои данные, но
и получатель сообщений, несущих информацию о произошедших в среде Windows
событиях. Хотя понятие окна в Windows было введено за несколько лет до широкого
распространения на ПК объектно-ориентированных языков программирования, для
описания окон очень удобно применять ОО-терминологию: свойства окна определя-
ют его внешний вид, а методы ответственны за реакцию на команды пользователя.
У каждого окна в Windows есть уникальный дескриптор окна (это число, кото-
рое можно рассматривать как имя окна, доступное и приложению, и самой Windows).
Переменные для хранения оконных дескрипторов обычно имеют тип HWND.
Windows отслеживает события пользовательского интерфейса и преобразует их
в сообщения. В структуру сообщения помещается и дескриптор окна-получателя. За-
тем сообщение помещается в очередь потока, владеющего этим окном, или передает-
ся непосредственно в оконную процедуру окна-получателя сообщения.

1. Иерархия окон
Для управления окнами Windows хранит информацию о них в иерархической
структуре, упорядоченной по отношению принадлежности. Принадлежность бывает
двух типов: родительское/дочернее окно и владелец/собственное окно.
У каждого окна есть родительское окно и могут быть окна того же уровня (сиб-
линги). В корне иерархии находится окно рабочего стола, которое Windows создает в
процессе загрузки. Рабочий стол является родительским окном для окон верхнего
уровня. У дочерних окон родительским окном может быть окно верхнего уровня или
другое дочернее окно, расположенное выше по иерархии. На рис. 3.1 показана иерар-
хия окон для типичного сеанса работы Windows.

Окно рабочего стола


(Desktop Window)

Родительское Родительское
окно (parent) окно (parent)

Название приложения Заголовок окна


Окно приложения Сиблинг Диалоговое окно
(перекрываемое) (всплывающее)
Владелец
(owner) Родительское окно
Родительское
окно Кнопка 1 Кнопка 2
Клиентское окно
(дочернее)

Рис. 3.1. Иерархическое упорядочение окон в типичном сеансе работы Windows.

32
Окна одного уровня на экране могут перекрывать друг друга. Т.о., пользова-
тель видит окна упорядоченными "по дальности". Обычно видимая иерархия окон со-
ответствует их логической иерархии. Для окон-сиблингов порядок отображения на-
зывается Z-порядком. Для окон верхнего уровня этот порядок может быть изменен
(например, пользователь может извлечь окно одного из приложений на передний
план). Если назначить окну верхнего уровня оконный стиль WM_EX_TOPMOST, то оно
всегда будет располагаться поверх своих сиблингов, не имеющих этого стиля.
Отношения родительское окно/дочернее и владелец/собственное окно отлича-
ются тем, что дочернее окно ограничено областью своего родительского окна. Отно-
шение владелец/собственное окно существует между окнами верхнего уровня для
реализации Z-порядка. Собственное окно выводится на экран поверх окна-владельца
и исчезает, когда окно-владелец сворачивается. Типичный пример отношения владе-
лец/собственное окно наблюдается при отображении диалогового окна. Диалоговое
окно не является дочерним окном (т.е. оно не ограничено клиентской областью глав-
ного окна приложения), но принадлежит главному окну приложения.
В Win32 API есть специальный набор функций для перебора окон в соответст-
вии с их иерархией. Некоторые из этих функций перечислены ниже.
Функция GetDesktopWindow возвращает дескриптор окна рабочего стола.
Функция EnumWindows перебирает все окна верхнего уровня. При вызове при-
ложение должно передать ей адрес функции обратного вызова. Эта функция будет
вызываться изнутри EnumWindows для каждого окна верхнего уровня.
Функция EnumChildWindows перебирает все дочерние окна у заданного роди-
тельского окна. В процессе перебора вызывается пользовательская функция обратно-
го вызова. EnumChildWindows при переборе учитывает порожденные дочерние окна,
т.е. дочерние окна, которые сами принадлежат дочерним окнам заданного окна.
Функция EnumThreadWindows перебирает все окна, принадлежащие заданно-
му потоку. При этом для каждого такого окна вызывается функция обратного вызова.
В качестве параметров функции передаются адрес этой функции, а также дескриптор
потока. При переборе учитываются окна верхнего уровня, дочерние окна и порож-
денные дочерние окна.
Функцию FindWindow можно применять для поиска окна верхнего уровня по
заданному оконному классу или заголовку окна.
Функция GetParent возвращает дескриптор родительского окна для заданного
дочернего окна.
Функция GetWindow предоставляет наиболее гибкий способ доступа к иерар-
хии окон. В зависимости от второго параметра, uCmd, она может возвратить для за-
данного окна дескриптор родительского окна, окна-владельца, сиблинга, или дочер-
него окна.

2. Диалоговые окна
В большинстве приложений, кроме главного окна приложения со строкой ме-
ню и специфическим для приложения содержимым, применяются диалоговые окна.
Они служат для обмена данными между пользователем и приложением. Обычно
главное окно присутствует на экране в течение всего сеанса работы приложения, а
диалоговые окна появляются на небольшое время после выбора какой-либо команды
приложения. Однако длительность пребывания на экране не является отличительной
33
особенностью главного и диалогового окна. Бывают приложения, использующие диа-
логовое окно в качестве своего главного окна. В других приложениях диалоговое ок-
но может присутствовать на экране большую часть сеанса работы.
Диалоговое окно содержит набор элементов управления, которые сами являют-
ся дочерними окнами. С их помощью пользователь и приложение обмениваются дан-
ными. В Win32 API есть набор функций для создания, отображения и управления со-
держимым диалогового окна. Программисту обычно не приходится заботиться о пе-
рерисовке элементов управления в соответствии с сообщениями от пользователя.
Программист может сосредоточиться именно на вопросах обмена данными между
элементами управления диалогового окна и приложением, а не на реализации види-
мой части интерфейса.
Диалоговые окна Windows делятся на два типа: модальные и немодальные.

2.1 Модальные диалоговые окна


При отображении модального диалогового окна его окно-владелец запрещает-
ся, что, по сути дела, означает приостановку приложения. Пользователь сможет про-
должить работу с приложением только после завершения работы с модальным окном.
Для создания и активизации модального окна предназначена функция
DialogBox. Эта функция создает диалоговое окно по данным из ресурсного файла
(используется ресурс специального типа – шаблон диалогового окна) и выводит это
окно на экран в модальном режиме. Приложение при обращении к DialogBox пере-
дает ей адрес функции обратного вызова. Эта функция (процедура диалогового окна)
является оконной процедурой. DialogBox возвратит управление только после завер-
шения окна из этой процедуры (обычно это делается с помощью функции EndDialog
при обработке какого-то сообщения от пользователя, например, по нажатию кнопки
OK).
Хотя можно создать модальное окно без окна-владельца, так делать не реко-
мендуется. При работе подобного окна главное окно приложения не запрещается, по-
этому надо обеспечить обработку сообщений, посылаемых главному окну. Кроме то-
го, при уничтожении окон приложенияWindows автоматически не уничтожает и не
убирает с экрана диалоговые окна без окон-владельцев.

2.2 Немодальные диалоговые окна


В отличие от модальных диалоговых окон, при отображении немодального ок-
на его окно-владелец не запрещается, т.е. приложение продолжает работать в обыч-
ном режиме. Но немодальное окно выводится поверх своего владельца, даже когда
окно-владелец получает фокус ввода. Немодальные окна удобны для непрерывного
отображения информации, важной для пользователя.
Немодальное окно создается функцией CreateDialog. В Win32 API нет ана-
лога функции DialogBox для немодальных окон, поэтому приложения должны само-
стоятельно выполнять получение и диспетчеризацию сообщений для немодальных
окон. Большинство приложений делают это в своем главном цикле обработки сооб-
щений с помощью функции IsDialogMessage. Эта функция проверяет, предназна-
чено ли сообщение заданному диалоговому окну, и при необходимости передает его в
процедуру диалогового окна.

34
Немодальное окно не возвращает никакого значения своему владельцу. Но при
необходимости немодальное окно и его владелец могут взаимодействовать с помо-
щью функции SendMessage.
В процедуре немодального диалогового окна не надо вызывать функцию
EndDialog. Такие окна уничтожаются вызовом функции DestroyWindow, например,
при обработке пользовательского сообщения в процедуре диалогового окна.
Приложения обязаны следить за тем, чтобы перед завершением приложения
были уничтожены все немодальные окна.

2.3 Информационные диалоговые окна


Информационные окна – это специальные модальные диалоговые окна, в кото-
рых выводится короткое сообщение для пользователя, заголовок и некоторая комби-
нация стандартных кнопок и пиктограмм. Эти окна предназначены для вывода корот-
ких текстовых сообщений и запроса у пользователя ответа из нескольких стандарт-
ных вариантов (Да, Нет, Отмена, OK). Например, информационные окна часто при-
меняются для уведомления пользователя об ошибках программы и для запроса вари-
анта реакции на ошибку: повторение или отмена операции.
Информационное окно создается и выводится на экран функцией MessageBox.
При вызове этой функции приложение передает ей строку сообщения и набор флагов,
определяющих тип и внешний вид информационного окна.

2.4 Шаблоны диалоговых окон


Диалоговое окно можно создать в оперативной памяти, вызывая
CreateWindow для каждого элемента управления. Но это слишком громоздкий спо-
соб. Большинство приложений пользуются ресурсами шаблонов диалоговых окон.
Шаблон диалогового окна задает стиль, местоположение и размер окна и всех эле-
ментов управления внутри него.
Шаблоны диалоговых окон являются частью файла ресурсов приложения, ко-
торый входит в проект для сборки файла приложения. Эти шаблоны создаются с по-
мощью редактора ресурсов. В MS Developer Studio редактор ресурсов интегрирован
в среду разработки.

2.5 Процедура диалогового окна


Процедура диалогового окна – это специальное название оконной процедуры,
обслуживающей модальное диалоговое окно. У нее нет принципиальных отличий от
обычной оконной процедуры, за исключением того, что в качестве процедуры "по
умолчанию" вызывается DefDlgProc, а не DefWindowProc.
Типичная диалоговая процедура реагирует на сообщения WM_INITDIALOG и
WM_COMMAND. В ответ на WM_INITDIALOG выполняется инициализация элементов
управления диалогового окна. Windows не посылает в процедуру диалогового окна
сообщение WM_CREATE, а посылает вместо него WM_INITDIALOG, причем только
после того, как созданы все элементы управления, но перед тем, как окно будет
выведено на экран. Поэтому процедура диалогового окна может корректно
проинциализировать элементы управления до того, как их увидит пользователь.

35
Большинство элементов управления посылают своим окнам-владельцам (т.е.
диалоговому окну) сообщения WM_COMMAND. Чтобы реализовать функцию, представ-
ляемую на экране с помощью элемента управления, процедура диалогового окна реа-
гирует на сообщения WM_COMMAND. При этом необходимо определить, какой именно
элемент послал сообщение и выполнить соответствующее действие.

3. Стандартные диалоговые окна


В Win32 API есть набор часто используемых диалоговых окон, которыми про-
граммист может пользоваться уже в готовом виде. Эти стандартные диалоговые ок-
на хорошо знакомы каждому пользователю Windows: окна для открытия и сохране-
ния файлов, для выбора цвета и шрифта, для печати и настройки принтера, для выбо-
ра размера страницы, для поиска и замены текста.
Стандартные диалоговые окна можно использовать двумя способами: или при-
менять в готовом виде, вызывая соответствующие функции API; или модифицировать
их поведения с помощью специальной функции-ловушки или собственного шаблона
диалогового окна.

3.1 Диалоговые окна для открытия и сохранения файлов


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

Рис. 3.2. Диалоговое окно для открытия файла. Рис. 3.3. Диалоговое окно для сохранения файла.

Окно для сохранения файла (рис. 3.3) создается функцией GetSaveFileName.


Параметром этой функции также является указатель на структуру OPENFILENAME.

36
3.2 Диалоговое окно выбора цвета
Окно выбора цвета (рис. 3.4) позволяет выбрать цвет из системной палитры
или задать новый цвет. Окно вызывается функцией ChooseColor, которой передает-
ся указатель на структуру CHOOSECOLOR. В этой структуре задаются параметры окна,
а после закрытия приложение может извлечь из нее (переменная rgbResult) сведе-
ния о выбранном цвете.

Рис. 3.4. Диалоговое окно выбора цвета. Рис. 3.5. Диалоговое окно выбора шрифта.

3.3 Диалоговое окно выбора шрифта


В окне выбора шрифта (рис. 3.5) пользователь может указать имя шрифта, его
стиль, размер, особые эффекты отображения и цвет. Этой функции передается указа-
тель на структуру CHOOSEFONT. Ее переменная lpLogFont является указателем на
структуру LOGFONT, которую можно использовать для инициализации диалогового
окна и для получения информации о выбранном шрифте после закрытия окна. Для
создания шрифта (это одна из разновидностей объектов модуля GDI) структуру
LOGFONT можно непосредственно передать функции GDI – CreateFontIndirect.

3.4 Диалоговые окна для печати и настройки параметров страницы


В диалоговом окне печати (рис. 3.6) объединены возможности печати и
настройки принтера. Для выбора формата и источника бумаги предназначено
отдельное окно, окно макета страницы (рис. 3.7).

Рис. 3.6. Диалоговое окно печати. Рис. 3.7. Диалоговое окно макета страницы.

37
Диалоговое окно печати создается функцией PrintDlg, а инициализируется с
помощью структуры PRINTDLG.
Окно макета страницы вызывается функцией PageSetupDlg, которая в качест-
ве параметра принимает параметра указатель на структуру PAGESETUPDLG. С помо-
щью этой структуры приложение может управлять содержимым элементов управле-
ния и после закрытия окна считывать данные, введенные пользователем.

3.5 Диалоговые окна для контекстного поиска и замены текста


Окна для поиска (рис. 3.8) и замены (рис. 3.9) текста обеспечивают удобный
интерфейс для выполнения этих операций в приложениях, работающих с текстовыми
документами. Это немодальные окна, в отличие от всех остальных стандартных диа-
логовых окон. Поэтому приложение, создавшее окно поиска или замены, ответствен-
но за диспетчеризацию сообщений для этого окна функцией IsDialogMessage.
Окно поиска выводится на экран функцией FindText. Она возвращает деск-
риптор диалогового окна, который приложение может использовать в цикле обработ-
ки сообщений при вызове IsDialogMessage. Окно поиска инициализируется и со-
храняет введенные пользователем значения в структуре типа FINDREPLACE.
Немодальное диалоговое окно общается с окном-владельцем через набор со-
общений. Перед вызовом FindText, приложение должно функцией
RegisterWindowMessage зарегистрировать новую строку-сообщение
"FINDMSGSTRING". Окно поиска будет посылать это сообщение приложению каждый
раз, когда пользователь введет новую строку для поиска.

Рис. 3.8. Диалоговое окно для поиска текста. Рис. 3.9. Диалоговое окно для замены текста.

Окно замены (рис. 3.9) похоже на окно поиска и инициализируется тоже с по-
мощью структуры FINDREPLACE. Для вывода этого окна на экран предназначена
функция ReplaceText.
Когда приложение получает сообщение от окна поиска или замены, оно может
проверить переменную Flags в структуре FINDREPLACE, чтобы определить, какое
именно действие было запрошено пользователем.
Окна поиска и замены не уничтожаются после возврата из функции FindText
или ReplaceText. Поэтому приложение должно гарантировать, что переданная этим
функциям переменная типа FINDREPLACE будет существовать в течение всего време-
ни работы окон. Если эта переменная будет уничтожена до уничтожения диалогового
окна, то приложение будет завершено вследствие недопустимой операции доступа к
несуществующей области памяти.

38
4. Элементы управления
Элемент управления – это дочернее окно специального типа, обычно приме-
няемое для того, чтобы пользователь мог с его помощью выполнить какое-то простое
действие. В результате этого действия элемент управления посылает окну-владельцу
сообщение. Например, у нажимаемой кнопки единственная простая функция, а имен-
но, когда пользователь нажимает кнопку, то она посылает своему окну-владельцу
(диалоговому окну) сообщение WM_COMMAND.
В Windows есть набор стандартных классов элементов управления. Некоторые
из них показаны в диалоговом окне на рис. 3.10.

Рис. 3.10. Набор стандартных элементов управ- Рис. 3.11. Некоторые стандартные элементы
ления Windows. управления Windows 95.

В Windows 95 появился набор новых элементов, которые, чтобы отличать их от


элементов управления старых версий Windows, иногда называются стандартными
элементами управления Windows 95 (рис. 3.11).
Приложения могут создавать собственные элементы управления. Их можно на-
следовать от стандартных оконных классов или разработать "с нуля".
Класс элемента управления и стиль (например, стиль задает разновидности
кнопки – нажимаемая, с зависимой фиксацией и др.) задаются в файле ресурсов. При
необходимости приложения могут создавать элементы управления как обычные окна,
функцией CreateWindow. При этом необходимо в явном виде, как параметры функ-
ции, указывать имя оконного класса и стиль элемента управления.

4.1 Статические элементы управления


Наверное, это простейший тип элементов управления. Они предназначены для
отображения небольшого текстового фрагмента, например, в качестве метки другого
элемента управления. Статические элементы не реагируют на события пользователя и
не посылают сообщений своим окнам-владельцам.
4.2 Кнопки
Кнопки – это элементы управления, которые реагируют на однократное нажа-
тие мышью. Есть несколько типов кнопок. Нажимаемые кнопки по нажатию посы-
лают своим окнам-владельцам сообщение WM_COMMAND. Кнопка с независимой фик-
сацией может пребывать в одном из двух состояний: включенном или выключенном.
Существует разновидность такой кнопки с тремя состояниями (третье – запрещен-
ное). Кнопки с зависимой фиксацией часто используются в виде группы кнопок, по-
зволяющей выбрать одно из нескольких взаимно исключающих состояний.

39
4.3 Элементы редактирования
Элемент редактирования обычно выглядит как прямоугольная область, в кото-
рой пользователь может набрать неформатированный текст. Это может быть несколь-
ко символов (например, имя файла) или целый текстовый файл (например, в клиент-
ской области приложения Блокнот расположен один большой элемент редактирова-
ния). Приложения взаимодействуют с элементом редактирования с помощью набора
сообщений, позволяющих задать или получить текст из элемента.
4.4 Окно списка
Окно списка содержит набор значений, упорядоченных в строки. Пользователь
с помощью мыши может выбрать некоторое значение из списка. Если в списке
хранится больше значений, чем может уместиться в его окне, то внутри окна списка
выводится вертикальная полоса прокрутки.
4.5 Комбинированное окно списка
Комбинированный с элементом редактирования список сочетает в себе воз-
можности этих двух элементов управления. Пользователь может ввести в элемент ре-
дактирования значение с клавиатуры или выбрать значение из списка, раскрываемого
щелчком по кнопке "стрелка вниз".
4.6 Полосы прокрутки
Полоса прокрутки состоит из прямоугольной области, по краям которой выво-
дятся кнопки со стрелками, и ползунка. Полосы прокрутки бывают вертикальные и
горизонтальные. Они применяются для показа позиции и доли видимых данных внут-
ри большой области. Раньше приложения использовали полосы прокрутки для реали-
зации ползунков, но в Windows 95 для этого был введен отдельный элемент управле-
ния.

4.7 Стандартные элементы управления Windows 95


В Windows 95, по сравнению с предыдущими версиями Windows, был опреде-
лен новый набор стандартных элементов управления (рис. 3.11).
Элемент "ярлычок" помогает разрабатывать диалоговые окна с закладками
(иногда они называются окнами свойств). Этот элемент позволяет создать интерфейс,
при котором пользователь может выбрать страницу диалогового окна (страницу
свойств) щелчком на небольшом ярлычке. В результате окно выглядит так, будто в
нем расположено несколько листов, один поверх другого, и щелчком на ярлычках
листов их можно извлекать на передний план.
Древовидные списки предназначены для представления иерархически упорядо-
ченных наборов значений. Они удобны для отображения иерархических списков, на-
пример, списка каталогов диска. Такие списки эффективны для отображения большо-
го количества элементов, т.к. позволяют сворачивать и разворачивать отдельные
уровни дерева.
Графический список расширяет поведение окна списка, позволяя отображать
значения списка в одном или нескольких форматах. Значение списка состоит из пик-
тограммы и некоторого текста. Элемент управления может показывать такие значе-
ния в нескольких форматах, например, в виде крупных пиктограмм или в виде списка
значений, упорядоченных по строкам.

40
Элемент "ползунок" ведет себя подобно регулятору-ползунку в бытовой аудио-
аппаратуре. Пользователь может перетащить ползунок мышью, чтобы выбрать неко-
торое значение из ограниченного диапазона. Этот элемент часто используется в муль-
тимедиа-приложениях для настройки громкости, прокрутки видео- и звуковых фай-
лов и т.п.
Индикаторы заполнения позволяют проинформировать пользователя о ходе
выполнения какой-либо длительной операции. Они служат только в информационных
целях и не обрабатывают событий от пользователя.
Наборные счетчики выглядят как маленькие кнопки-стрелки, которые выво-
дятся рядом с элементом редактирования и позволяют с фиксированным шагом
уменьшать или увеличивать значение в этом элементе.
Элемент редактирования сложного текста имеет больше возможностей, чем
старый элемент редактирования. Этот элемент позволяет работать с файлами формата
Microsoft RTF (Rich Text Format). По сути дела, этот элемент является текстовым ре-
дактором средней сложности.
Элемент "горячая клавиша" реагирует на нажатие пользователем определенной
комбинации клавиш. Приложение может задать эту комбинацию с помощью сообще-
ния WM_SETHOTKEY.
Среди других элементов управления Windows 95 можно назвать элементы
"анимационный ролик", "заголовок", "панель инструментов", "подсказка" и др.

5. Резюме
Окно – это простейший элемент, посредством которого взаимодействуют поль-
зователь и приложение. Windows при посылке сообщения в окно помещает в структу-
ру сообщения дескриптор этого окна-получателя. Оконные сообщения обрабатыва-
ются в оконной процедуре. Ее адрес, как и некоторые другие свойства окна, задаются
оконным классом, который наследуется окнами при создании.
Окна в Windows упорядочены в иерархическую структуру по отношению при-
надлежности. В корне иерархии находится окно рабочего стола. Окна верхнего уров-
ня – это такие окна, для которых родительским окном является рабочий стол, а также
те, у которых нет родительского окна. У дочерних окон родительским окном является
какое-либо окно верхнего уровня или другое дочернее окно. Окна с одним и тем же
родительским окном называются сиблингами (окнами одного уровня). Порядок, в ко-
тором происходит отображение сиблингов, называется Z-порядком.
У окон верхнего уровня может быть окно-владелец. отличное от его родитель-
ского окна, а у дочерних окон окно-владелец и родительское окно одинаковы.
Типичными окнами пользовательского интерфейса являются перекрывающиеся
окна (главные окна приложений); всплывающие окна (диалоговые окна) и элементы
управления (дочерние окна диалоговых окон).
В Win32 API определен набор функций для создания, отображения и управле-
ния диалоговыми окнами. В Windows есть два типа диалоговых окон: модальные и
немодальные. Модальное окно, пока присутствует на экране, запрещает свое окно-
владелец. Поэтому приложение приостанавливается до тех пор, пока пользователь не
закроет модальное окно.
При отображении немодального окна его окно-владелец не запрещается. При-
ложения должны в своем цикле обработки сообщений предусматривать диспетчери-

41
зацию сообщений в диалоговую процедуру немодального окна с помощью функции
IsDialogMessage.
В Windows есть набор стандартных диалоговых окон для типичных примене-
ний, например, для открытия и сохранения файла, для печати и настройки параметров
страницы, для выбора цвета и шрифта, для операций контекстного поиска и замены.
В диалоговых окнах располагаются элементы управления, например, кнопки,
статический текст, элементы редактирования, окна списков, комбинированные спи-
ски и полосы прокрутки. Приложения могут создавать собственные типы элементов
управления. В Windows 95 был определен дополнительный набор стандартных эле-
ментов: графические и древовидные списки, ярлычки, горячие клавиши, ползунки,
индикаторы, наборные счетчики и элемент редактирования сложного текста.
Типы и расположение элементов управления в диалоговом окне задаются в
шаблонах диалоговых окон в файле ресурсов приложения. Элементы управления
взаимодействуют с приложением путем посылки сообщений (например,
WM_COMMAND) своему окну-владельцу (т.е. диалоговому окну).

6. Упражнения.
1) Изучите англо-русский словарь терминов по теме 3-й лекции (см. CD-ROM).
2) Выполните лабораторную работу №1, "Типы окон Windows" (см. CD-ROM).

42
Лекция 4. Обзор библиотеки MFC
1. Назначение библиотеки MFC
Microsoft Foundation Classes (сокращенно MFC) – это библиотека классов на
языке Си++, разработанная фирмой Microsoft в качестве объектно-ориентированной
оболочки для Windows API. Существуют и другие библиотеки классов для Windows,
но преимущество MFC в том, что она написана компанией-разработчиком ОС. MFC
постоянно развивается, чтобы соответствовать возможностям новых версий Windows.
MFC содержит около 200 классов, представляющих практически все необхо-
димое для написания Windows-приложений: от окон до элементов управления
ActiveX. Одни классы можно использовать непосредственно, а другие – в качестве
базовых для создания новых классов. Некоторые классы MFC очень просты, напри-
мер, класс CPoint для хранения двумерных координат точки. Другие классы являют-
ся более сложными, например, класс CWnd инкапсулирует функциональность окна
Windows. В приложении MFC напрямую вызывать функции Windows API приходится
редко. Вместо этого программист создает объекты классов MFC и вызывает их функ-
ции-члены. В MFC определены сотни функций-членов, которые служат оболочкой
функций API, и часто их имена совпадают с именами соответствующих функций API.
Например, для изменения местоположения окна в API есть функция SetWindowPos.
В MFC это действие выполняется с помощью функции-члена CWnd::SetWindowPos.
MFC является не просто библиотекой классов, она также предоставляет про-
граммисту каркас приложения. Это заготовка приложения, содержащая набор клас-
сов и функций для выполнения типичных операций приложения Windows, например,
по созданию главного окна, работе с главным меню и т.п. Программист может разра-
батывать собственное приложение, перегружая виртуальные функции классов карка-
са и добавляя в него новые классы. Центральное место в каркасе приложения MFC
занимает класс-приложение CWinApp. В нем скрыты самые общие аспекты работы
приложения, например, главный цикл обработки сообщений.
В каркасе приложения MFC есть понятия высокого уровня, которых нет в Win-
dows API. Например, архитектура "документ/вид" является мощной инфраструктурой,
надстроенной над API и позволяющей отделить данные программы от их графическо-
го представления. Эта архитектура отсутствует в API и полностью реализована в кар-
касе приложения с помощью классов MFC.

1.1 Преимущества использования Си++/MFC по сравнению с Си/Windows API


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

43
пользуемых компонент. Эти компоненты облегчают решение типичных задач, напри-
мер, для добавления в приложение MFC стыкуемой панели инструментов можно ис-
пользовать класс CToolBar, которому надо только указать параметры кнопок панели.
Использовать сложные технологии Windows, например, технологии ActiveX (в том
числе COM и OLE) без готовых классов практически невозможно.
Еще одно преимущество, предоставляемое MFC, – это готовый каркас прило-
жения. Он устроен таким образом, что объекты Windows (окна, диалоговые окна,
элементы управления и др.) выглядят в программах как объекты классов Си++.

1.2 Основные задачи проектирования MFC


При проектировании MFC перед разработчиками Microsoft стояли две основ-
ных задачи:
1) MFC должна служить объектно-ориентированным интерфейсом для дос-
тупа к API операционных систем семейства Windows с помощью повторно
используемых компонент – классов.
2) накладные расходы по времени вычислений и по объему памяти при ис-
пользовании MFC должны быть минимальны.
Для достижения первой цели были разработаны классы, инкапсулирующих ок-
на, диалоговые окна и другие объекты операционной системы. В этих классах было
предусмотрено много виртуальных функций, которые можно перегружать в произ-
водных классах и таким образом модифицировать поведение объектов ОС.
Уменьшение накладных расходов на библиотеку MFC было достигнуто за счет
решений, определяющих способ реализации классов MFC – о том, как именно объек-
ты ОС будут оформлены в виде классов. Одно из этих решений – способ связи между
объектами MFC и объектами Windows. В Windows информация о свойствах и теку-
щем состоянии окна хранится в служебной памяти, принадлежащей ОС. Эта инфор-
мация скрыта от приложений, которые работают с окнами исключительно посредст-
вом дескрипторов (переменных типа HWND). В MFC "оболочкой" окна является класс
CWnd. Но в нем нет переменных-членов, дублирующих все свойства окна с заданным
HWND. В классе CWnd хранится только дескриптор окна. Для этого заведена открытая
переменная-член CWnd::m_hWnd типа HWND. Когда программист запрашивает у объ-
екта CWnd какое-нибудь свойство окна (напр., заголовок), то этот объект вызывает со-
ответствующую функцию API, а затем возвращает полученный результат.
Описанная схема применяется в MFC для реализации практически всех клас-
сов, служащих оболочками объектов Windows, т.е. внутри этих классов хранятся
только дескрипторы объектов.

1.3 Архитектура "документ/вид"


В устройстве каркаса приложения MFC важнейшую роль играет архитектура
"документ/вид". Это такой способ проектирования приложения, когда в нем отдельно
создаются объекты-документы, ответственные за хранение данных приложения, и
объекты-виды, ответственные за отображение этих данных различными способами.
Базовыми классами для документов и видов в MFC служат классы CDocument и
CView. Классы каркаса приложения CWinApp, CFrameWnd и др. работают совместно с
CDocument и CView, чтобы обеспечить функционирование приложения в целом.

44
Сейчас пока рано обсуждать детали архитектуры "документ/вид", но вы должны, как
минимум, знать термин "документ/вид", часто упоминаемый при рассмотрении MFC.
Приложения MFC можно писать и без использования документов и видов (на-
пример, при изучении основ MFC). Но доступ к большинству возможностей каркаса
возможен только при поддержке архитектурs "документ/вид". В действительности это
не является жестким ограничением структуры приложения, и большинство программ,
обрабатывающих документы какого-либо типа, могут быть преобразованы в эту ар-
хитектуру. Не следует думать (по аналогии с термином "документ"), что эта архитек-
тура полезна только для написания текстовых редакторов и электронных таблиц.
"Документ" – это абстрактное представление данных программы в памяти компьюте-
ра. Например, документ может быть просто массивом байт для хранения игрового по-
ля компьютерной игры, или он действительно может быть электронной таблицей.
Какие именно преимущества получают в MFC приложения "документ/вид"? В
частности, это значительное упрощение печати и предварительного просмотра, гото-
вый механизм сохранения и чтения документов с диска, преобразование приложений
в серверы документов ActiveX (приложения, документы которых можно открывать в
Internet Explorer). Подробно архитектура документ/вид будет рассмотрена позже.

1.4 Иерархия классов MFC


Большинство классов MFC явно или неявно унаследованы от класса CObject.
Класс CObject обеспечивает для своих подклассов три важных возможности:
• сериализация (запись или чтение данных объекта на диск);
• средства динамического получения информации о классе;
• диагностическая и отладочная поддержка.
Под термином "сериализация" подразумевается преобразование данных объек-
та в последовательную форму, пригодную для записи или чтения из файла. Используя
CObject в качестве базового класса, легко создавать сериализуемые классы, объекты
которых можно записывать и считывать с диска стандартными средствами MFC.
Динамическая информация о классе (Run-time class information, RTCI) позволя-
ет получить во время выполнения программы название класса и некоторую другую
информацию об объекте. Механизм RTCI реализован независимо от механизма дина-
мической идентификации типа (RTTI), встроенного в Си++. Во многом эти средства
похожи, но RTCI был разработан на несколько лет раньше.
Диагностические и отладочные возможности CObject позволяют проверять
состояние объектов подклассов CObject на выполнение некоторых условий
корректности и выдавать дампы состояния объектов в отладочное окно Visual C++.
CObject предоставляет подклассам еще ряд полезных возможностей. Напри-
мер, для защиты от утечек памяти в отладочном режиме в классе перегружены опера-
торы new и delete. Если вы динамически создали объект подкласса CObject, и за-
были удалить его до завершения программы, то MFC выдаст в отладочное окно Visual
C++ предупреждающее сообщение.

1.5 Вспомогательные функции каркаса приложения


В MFC не все функции являются членами классов. Есть набор функций-утилит,
существующих независимо от каких-либо классов. Они называются функциями кар-
каса приложения, их имена начинаются с Afx. Функции-члены классов можно вызы-
45
вать только применительно к объектами этих классов, а функции каркаса приложения
можно вызывать из любого места программы.
В табл. 4.1 перечислены несколько наиболее часто используемых функций
AFX. AfxBeginThread упрощает создание новых исполняемых потоков. Функция
AfxMessageBox является аналогом функции MessageBox из Windows API. Функции
AfxGetApp и AfxGetMainWnd возвращают указатели на объект-приложение и на
главное окно приложения. Они полезны, когда вы хотите вызвать функцию-член или
обратиться к переменным этих объектов, но не знаете указателя на них. Функция
AfxGetInstanceHandle позволяет получить дескриптор экземпляра EXE-файла для
передачи его функции Windows API (в программах MFC иногда тоже приходится вы-
зывать функции API).

Таблица. 4.1. Часто используемые функции семейства AFX


Имя функции Назначение
AfxAbort Безусловное завершение работы приложения (обычно при
возникновении серьезной ошибки)
AfxBeginThread Создает новый поток и начинает его исполнение
AfxEndThread Завершает текущий исполняемый поток
AfxMessageBox Выводит информационное окно Windows
AfxGetApp Возвращает указатель на объект-приложение
AfxGetAppName Возвращает имя приложения
AfxGetMainWnd Возвращает указатель на главное окно приложения
AfxGetInstanceHandle Возвращает дескриптор экземпляра EXE-файла приложения
AfxRegisterWndClass Регистрирует пользовательский оконный класс WNDCLASS
для использования в приложении MFC

2. Простейшее приложение на MFC


Конечно, в качестве простейшего примера рассмотрим модифицированное
приложение "Hello, world" – "Hello, MFC". В нем будет продемонстрирован ряд осо-
бенностей разработки Windows-приложений на базе MFC:
• наследование новых классов от MFC-классов CWinApp и CFrameWnd;
• использование класса CPaintDC при обработке сообщения WM_PAINT.
• применение карт сообщений.
Исходный текст приложения Hello приведен в виде фрагментов программы
4.1а и 4.1б. В заголовочном файле Hello.h содержатся описания двух унаследован-
ных классов. В Hello.cpp размещена реализация этих классов.

Фрагмент программы 4.1а. Приложение Hello – заголовочный файл Hello.h

#if !defined( __HELLO_H )


# define __HELLO_H

class CMyApp : public CWinApp {


public:
virtual BOOL InitInstance();
};

class CMainWindow : public CFrameWnd {


public:
CMainWindow();

protected:

46
afx_msg void OnPaint();
DECLARE_MESSAGE_MAP()
};

#endif

Фрагмент программы 4.1б. Приложение Hello – файл реализации Hello.cpp

#include <afxwin.h> // Описание CWinApp и других классов каркаса приложения MFC


#include "Hello.h"

CMyApp myApp;

// Функции-члены CMyApp
BOOL CMyApp::InitInstance()
{
m_pMainWnd = new CMainWindow;
m_pMainWnd->ShowWindow( m_nCmdShow );
m_pMainWnd->UpdateWindow();
return TRUE;
}

// Карта сообщений и функции-члены CMainWindow


BEGIN_MESSAGE_MAP( CMainWindow, CFrameWnd )
ON_WM_PAINT()
END_MESSAGE_MAP()

CMainWindow::CMainWindow()
{
Create( NULL, "Приложение Hello" );
}

void CMainWindow::OnPaint()
{
CPaintDC dc( this );

CRect rect;
GetClientRect( &rect );

dc.DrawText("Hello, MFC", -1, &rect, DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER );


}

Главное окно приложения Hello показано на рис. 4.1. Это окно является пол-
ноценным перекрываемым окном Windows: его можно перемещать, изменять разме-
ры, сворачивать, разворачивать и закрывать. При любом размере окна строка "Hello,
MFC" все равно выводится в центре клиентской области.

Рис. 4.1. Главное окно приложения Hello.

47
2.1 Объект-приложение
Центральная компонента MFC-приложения – объект-приложение подкласса
CWinApp. CWinApp содержит цикл обработки сообщений, в котором выполняется вы-
борка и диспетчеризация сообщений в оконную процедуру главного окна приложе-
ния. В этом классе есть виртуальные функции, которые можно перегружать для реа-
лизации поведения конкретного приложения.
В приложении MFC должен быть ТОЛЬКО ОДИН объект-приложение. Он
объявляется в глобальной области видимости, чтобы создание объекта производилось
сразу после запуска приложения.
Класс-приложение в программе Hello называется CMyApp. Объект этого клас-
са создается в файле Hello.cpp с помощью оператора описания переменной:
CMyApp myApp;
В классе CMyApp нет переменных членов и есть только одна перегруженная
функция, унаследованная от CWinApp – функция-член InitInstance. Она вызыва-
ется каркасом сразу после запуска приложения. В InitInstance должно создаваться
главное окно приложения. Поэтому даже самое маленькое MFC-приложение должно
унаследовать класс от CWinApp и перегрузить в нем функцию InitInstance.

2.2 Функция InitInstance


По умолчанию виртуальная функция CWinApp::InitInstance состоит из
единственного оператора возврата:
return TRUE;
InitInstance предназначена для выполнения инициализации, необходимой
при каждом запуске программы (как минимум, должно создаваться главное окно при-
ложения). Возвращаемое значение InitInstance является признаком удач-
ной/неудачной инициализации. При неудачной инициализации (значение FALSE)
приложение будет завершено.
В CMyApp::InitInstance главное окно приложения является объектом клас-
са CMainWindow, адрес этого объекта сохраняется в переменной-члене
CWinApp::m_pMainWnd:
m_pMainWnd = new CMainWindow;
После создания главного окна InitInstance выводит его на экран с помощью
функций-членов класса CMainWindow:
m_pMainWnd->ShowWindow( m_nCmdShow ); // Вывод окна на экран
m_pMainWnd->UpdateWindow(); // Обновление содержимого окна
Виртуальные функции ShowWindow и UpdateWindow унаследованы от CWnd –
базового класса для всех оконных классов MFC, в том числе и для CFrameWnd, от ко-
торого унаследован CMainWindow.
Функция ShowWindow в качестве параметра принимает целочисленный код со-
стояния окна: свернутое, развернутое или обычное (значение по умолчанию
SW_SHOWNORMAL). Приложение Hello передает в ShowWindow значение переменной
CWinApp::m_nCmdShow, в которой каркас приложения сохраняет параметр
nCmdShow функции WinMain.

48
2.3 Виртуальные функции CWinApp
Кроме InitInstance, в классе CWinApp есть и другие виртуальные функции-
члены, которые можно перегружать для выполнения специфических действий при-
ложения. В справочной системе в описании класса CWinApp вы можете увидеть более
десяти виртуальных функций, например, WinHelp и ProcessWndProcException, но
большинство из низ используются редко.
Функцию ExitInstance можно использовать для освобождения ресурсов при
завершении приложения (например, ресурсов и памяти, выделенных в InitIn-
stance). В реализации "по умолчанию" функция ExitInstance выполняет некото-
рые действия по очистке, предусмотренные в каркасе приложения, поэтому при
перегрузке обязательно надо вызывать ExitInstance из базового класса. Значение,
возвращенное ExitInstance, является кодом выхода, возвращаемым из WinMain.

Среди других полезных виртуальных функций CWinApp можно назвать


OnIdle, Run и PreTranslateMessage. OnIdle удобна для выполнения некоторой
фоновой обработки, вроде обновления каких-либо индикаторов. Слово "idle" перево-
дится как "ожидание, простой". Эта функция вызывается. когда очередь сообщений
потока пуста. Поэтому OnIdle является удобным механизмом выполнения фоновых
задач с низким приоритетом, не требующих отдельного исполняемого потока.
Функцию Run можно перегрузить с целью модификации цикла обработки со-
общений, но это делается редко. Если надо выполнить некоторую специфическую
предварительную обработку некоторых сообщений до их диспетчеризации, то доста-
точно перегрузить PreTranslateMessage и не изменять цикл обработки сообщений.

2.4 Порядок использования объекта-приложения каркасом MFC


В исходном тексте приложения Hello заметна характерная особенность MFC-
приложений – отсутствие исполняемого кода за пределами классов. В приложении
Hello нет ни функции main, ни WinMain. Единственный оператор за пределами
классов – это оператор создания объекта-приложения в глобальной области видимо-
сти. Чтобы понять, где же в самом деле начинается исполнение программы, надо ра-
зобраться в структуре каркаса приложения.
В одном из исходных файлов MFC (они поставляются в комплекте Visual C++),
в Winmain.cpp, находится функция AfxWinMain. Она является аналогом WinMain в
MFC-приложениях. Из AfxWinMain вызываются функции-члены объекта-
приложения – отсюда ясно, почему он должен быть глобальным объектом (глобаль-
ные переменные и объекты создаются до исполнения какого-либо кода, а объект-
приложение должен быть создан до начала исполнения функции AfxWinMain).
После запуска AfxWinMain для инициализации каркаса приложения вызывает
функцию AfxWinInit, которая копирует полученные от Windows значения
hInstance, nCmdShow и другие параметры AfxWinMain в переменные-члены объек-
та-приложения. Затем вызываются функции-члены InitApplication и
InitInstance (InitApplication в 32-разрядных приложениях использовать не
следует, она нужна для совместимости с Windows 3.x). Если AfxWinInit,
InitApplication или InitInstance возвращает FALSE, то AfxWinMain завершает
приложение. При условии успешного выполнения всех перечисленных функций
AfxWinMain выполняет следующий, крайне важный шаг. У объекта-приложения вы-

49
зывается функция-член Run и т.о. выполняется вход в цикл обработки сообщений
главного окна приложения:
pApp->Run();
Цикл обработки сообщений завершится при получении из очереди сообщения
WM_QUIT. Тогда Run вызовет функцию ExitInstance и вернет управление в
AfxWinMain. Она выполнит освобождение служебных ресурсов каркаса и затем опе-
ратором return завершит работу приложения.

2.5 Класс "окно-рамка" CFrameWnd


В MFC базовым оконным классом является класс CWnd. Этот класс и его по-
томки предоставляют объектно-ориентированный интерфейс для работы со всеми ок-
нами, создаваемыми приложением. В приложении Hello класс главного окна назы-
вается CMainWindow. Он является подклассом CFrameWnd, а тот, в свою очередь,
подклассом CWnd. Класс CFrameWnd реализует понятие "окна-рамки". Окна-рамки
играют важную роль контейнеров для видов, панелей инструментов, строк состояния
и других объектов пользовательского интерфейса в архитектуре "документ/вид". Пока
об окне-рамке можно думать как об окне верхнего уровня, которое обеспечивает ос-
новной интерфейс приложения с внешним миром.
MFC-приложение для создания окна вызывает его функцию-член Create.
Приложение Hello создает объект CMainWindow в функции InitInstance, а в кон-
структоре CMainWindow как раз и выполняется создание окна Windows, которое по-
том будет выведено на экран:
Create( NULL, "Приложение Hello" );
Функция-член Create, наследуемая в CMainWindow от CFrameWnd, имеет сле-
дующий прототип:
BOOL Create( LPCTSTR lpszClassName,
LPCTSTR lpszWindowName,
DWORD dwStyle = WS_OVERLAPPEDWINDOW,
const RECT& rect = rectDefault,
CWnd* pParentWnd = NULL,
LPCTSTR lpszMenuName = NULL,
DWORD dwExStyle = 0,
CCreateContext* pContext = NULL )
Применение Create упрощается за счет того, что для 6-ти из 8-ми ее парамет-
ров определены значения "по умолчанию". Приложение Hello при вызове Create
указывает только два первых параметра. Параметр lpszClassName задает имя окон-
ного класса (которое хранится в структуре WNDCLASS), на основе которого операци-
онная система будет создавать новое окно. Если этот параметр задать равным NULL,
то будет создано окно-рамка на основе оконного класса, зарегистрированного карка-
сом приложения. Параметр lpszWindowName задает текст строки заголовка окна.

2.6 Рисование содержимого окна


Приложение Hello выводит текст на экран только по требованию Windows,
при обработке сообщения WM_PAINT. Это сообщение генерируется по разным причи-
нам, например, при перекрытии окон или при изменении размеров окна. В любом

50
случае, само приложение ответственно за перерисовку клиентской области окна в от-
вет на WM_PAINT.
В приложении Hello сообщения WM_PAINT обрабатываются функцией
CMainWindow::OnPaint, которая вызывается каркасом приложения при получении
каждого сообщения WM_PAINT. Эта функция выводит строку "Hello, MFC" в центре
клиентской области окна. Функция начинается с создания объекта класса CPaintDC:
CPaintDC dc( this );
В MFC класс CPaintDC является подклассом более абстрактного класса CDC,
инкапсулирующего контекст устройства Windows. В CDC есть множество функций-
членов для рисования на экране, принтере и других устройствах. Класс CPaintDC
является специфической разновидностью CDC, которая используется только в обра-
ботчиках сообщения WM_PAINT.
В приложениях на Windows API при обработке сообщения WM_PAINT
приложение сначала должно вызвать функцию ::BeginPaint для получения
контекста устройства, связанного с недействительной областью клиентской области
окна. После выполнения в этом контексте всех необходимых операций рисования,
приложение должно вызвать ::EndPaint для освобождения контекста и информиро-
вания Windows о завершении обновления окна. Если приложение при обработке
WM_PAINT не будет вызывать функции ::BeginPaint и ::EndPaint, то Windows не
будет удалять сообщение WM_PAINT из очереди и это сообщение будет поступать в
окно постоянно.
Объекты класса CPaintDC вызывают ::BeginPaint из конструктора, а
::EndPaint – из деструктора.
После создания объекта CPaintDC в OnPaint создается объект CRect и вызо-
вом CWnd::GetClientRect в него помещаются координаты клиентской области
окна:
CRect rect;
GetClientRect( &rect );
Затем OnPaint вызывает CDC::DrawText для вывода строки "Hello, MFC":
dc.DrawText( "Hello, MFC", -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER );
DrawText – это функция вывода текста в контекст устройства. У нее 4 пара-
метра: указатель на отображаемую строку, количество символов в строке (или -1, ес-
ли строка заканчивается нулем), указатель на структуру RECT или объект CRect с
координатами области вывода, и флаги вывода. В приложении Hello используется
комбинация из трех флагов, указывающих, что текст надо выводить в одну строку и
центрировать и по горизонтали, и по вертикали внутри области rect.
Заметно, что среди параметров DrawText нет характеристик шрифта и цвета
текста. Эти и другие параметры вывода являются атрибутами контекста устройства и
управляются специальными функциями-членами CDC, например, SelectObject и
SetTextColor. Т.к. приложение Hello не изменяет никаких атрибутов контекста
устройства, то используется шрифт и цвет "по умолчанию" (черный). DrawText за-
полняет прямоугольник, описывающий текст, текущим фоновым цветом контекста
устройства (по умолчанию – белый).

51
2.7 Карта сообщений
Как сообщение WM_PAINT, полученное от Windows, преобразуется в вызов
функции-члена CMainWindow::OnPaint? Это делается с помощью карты сообще-
ний. Карта сообщений – это таблица, связывающая сообщения и функции-члены для
их обработки. Когда окно-рамка приложения Hello получает сообщение, то MFC
просматривает карту сообщений, ищет в ней обработчик для сообщения WM_PAINT и
вызывает OnPaint. Карты сообщений в MFC введены для того, чтобы избежать
больших таблиц виртуальных функций, которые были бы необходимы, если в каждом
классе завести виртуальную функцию для каждого возможного сообщения. Карту со-
общений может содержать любой подкласс класса CCmdTarget.
Карты сообщений в MFC реализованы так, что в исходном тексте видны только
макросы, которые использовать достаточно просто, а сложная обработка карт скрыта
внутри MFC. Для добавления карты сообщений в класс надо сделать следующее:
1) Объявить карту сообщений в интерфейсной части класса с помощью макро-
са DECLARE_MESSAGE_MAP.
2) Создать карту сообщений в файле реализации. Она ограничена макросами
BEGIN_MESSAGE_MAP и END_MESSAGE_MAP. Между ними размещаются
макросы, идентифицирующие конкретные сообщения.
3) Добавить в класс функции-члены для обработки сообщений.
В приложении Hello класс CMainWindow обрабатывает только одно сообще-
ние, WM_PAINT, поэтому карта сообщений выглядит так:
BEGIN_MESSAGE_MAP( CMainWindow, CFrameWnd )
ON_WM_PAINT()
END_MESSAGE_MAP()
Карта сообщений начинается с макроса BEGIN_MESSAGE_MAP, в котором
задается имя класса-владельца карты и имя его базового класса. (Карты сообщений
наращиваются путем наследования. Имя базового класса необходимо, чтобы каркас
приложений мог продолжить поиск обработчика сообщений и в карте базового
класса, если его нет в карте текущего.). Макрос END_MESSAGE_MAP завершает карту
сообщений. Между BEGIN_MESSAGE_MAP и END_MESSAGE_MAP располагаются эле-
менты карты сообщений. Макрос ON_WM_PAINT определен в заголовочном файле
MFC Afxmsg_.h. Этот макрос добавляет в карту сообщений элемент для обработки
сообщения WM_PAINT. У этого макроса нет параметров, в нем жестко задана связь
между сообщением WM_PAINT и функцией-членом OnPaint.
В MFC есть макросы для более чем 100 сообщений Windows, начиная от
WM_ACTIVATE до WM_WININICHANGE. Узнать имя функции-обработчика сообщения
для некоторого макроса ON_WM можно из документации по MFC, но правила обозна-
чений прозрачны и можно просто заменить в имени сообщения префикс WM_ на On и
преобразовать остальные символы имени сообщения, кроме первых символов от-
дельных слов, в нижний регистр. Например, WM_PAINT преобразуется в имя обработ-
чика OnPaint, WM_LBUTTONDOWN в OnLButtonDown и т.п.
Типы параметров функции-обработчика сообщения можно узнать в справочной
системе по MFC. В обработчик OnPaint не передается никаких параметров и у него
нет возвращаемого значения. Но может быть и иначе, например, прототип обработчи-
ка OnLButtonDown выглядит так:
afx_msg void OnLButtonDown( UINT nFlags, CPoint point )

52
Параметр nFlags является набором битовых флагов, отражающих состояние
кнопок мыши, клавиш Ctrl и Shift. В объекте point хранятся координаты указателя
мыши в момент щелчка левой кнопкой. Параметры, передаваемые в обработчик со-
общений, первоначально приходят в приложение в виде параметров сообщения
wParam и lParam. В Windows API параметры wParam и lParam служат общим спо-
собом передачи информации о сообщении и не учитывают его специфику. Поэтому с
обработчиками сообщений MFC работать гораздо удобнее, т.к. каркас приложения
передает в них параметры в виде, наиболее удобном для конкретного сообщения.
Что будет, если вы хотите обработать сообщение, для которого в MFC нет мак-
роса карты сообщений? Вы можете создать элемент карты для такого сообщения с
помощью макроса ON_MESSAGE. У него два параметра: идентификатор сообщения и
адрес соответствующей функции-члена класса. Например, для обработки сообщения
WM_SETTEXT с помощью функции-члена OnSetText надо создать следующую запись
в карте сообщений:
ON_MESSAGE( WM_SETTEXT, OnSetText )
Функция-член OnSetText должна быть объявлена так:
afx_msg LRESULT OnSetText( WPARAM wParam, LPARAM lParam );
В MFC есть еще ряд служебных макросов карты сообщений. Например,
ON_COMMAND связывает с функциями-членами класса команды меню и события дру-
гих элементов пользовательского интерфейса. Макрос ON_UPDATE_COMMAND_UI свя-
зывает элементы меню и другие объекты интерфейса с обработчиками обновления,
которые синхронизируют состояние объектов интерфейса с внутренним состоянием
приложения. Эти и другие макросы карты сообщений будут рассматриваться позже.
Еще раз вернемся к приложению Hello. В классе CMainWindow функция-член
OnPaint и карта сообщений описываются в Hello.h:
afx_msg void OnPaint();
DECLARE_MESSAGE_MAP()
Макрос afx_msg применяется для удобочитаемости исходного текста, он на-
поминает о том, что OnPaint является обработчиком сообщений. Этот макрос указы-
вать не обязательно, т.к. при компиляции он заменяется пробелом.
Макрос DECLARE_MESSAGE_MAP обычно идет последним оператором в объяв-
лении класса, т.к. в него встроены модификаторы доступа Си++. Вы можете объяв-
лять компоненты класса и после DECLARE_MESSAGE_MAP, но обязательно указывайте
для них соответствующий модификатор доступа public, protected или private.

3. Резюме
Перечислим наиболее важные особенности устройства MFC-приложения
Hello. Сразу после запуска приложения создается глобальный объект-приложение
подкласса CWinApp. Функция каркаса MFC AfxWinMain вызывает функцию объекта-
приложения InitInstance. Эта функция создает объект-главное окно приложения, а
его конструктор создает и выводит на экран окно Windows. После создания окна,
InitInstance делает окно видимым с помощью функции-члена ShowWindow и за-
тем посылает этому окну сообщение WM_PAINT с помощью функции UpdateWindow.
Затем InitInstance возвращает управление и AfxWinMain вызывает у объекта-
приложения функцию-член Run, внутри которой реализован цикл обработки сообще-
ний. MFC с помощью карты сообщений преобразует поступающие сообщения

53
WM_PAINT в вызовы функции-члена CMainWindow::OnPaint, а OnPaint выводит в
клиентскую область окна символьную строку "Hello, MFC". Вывод текста выполняет-
ся с помощью функции-члена DrawText объекта-контекста устройства CPaintDC.
В MFC, по сравнению с программированием в Windows API, заметны новые
сложности. Окно создается в два этапа. Нужен объект-приложение. Отсутствует
функция WinMain. Все это отличается от программирования для Windows на уровне
API. Но, если сравнить исходный текст MFC-приложения Hello и текст аналогичной
программы, пользующейся API, станет заметно бесспорное преимущество MFC. MFC
уменьшает размер исходного текста, и он становится проще для понимания, т.к. зна-
чительная часть исходного текста располагается внутри библиотеки классов. Поведе-
ние классов MFC можно изменять путем наследования от них своих собственных
подклассов. В результате MFC оказывается очень эффективным средством програм-
мирования для Windows. Преимущества MFC становятся особенно очевидны при ис-
пользовании сложных возможностей Windows, например, элементов управления
ActiveX или при связи с другими приложениями через интерфейс OLE.

4. Упражнения
1) Ознакомьтесь с иерархией классов, приведенной в документе "Иерархия классов
MFC" или в справочной системе Visual C++ по теме "Hierarchy Chart".
2) На основе приведенных в лекции исходных файлов соберите приложение Hello.
Для этого в Visual C++ надо выполнить следующие действия:
1. Выберите команду File⇒New и затем перейдите на закладку Projects.
2. Выберите Win32 Application и в строке ввода Project Name укажите имя
проекта При необходимости измените путь к папке проекта. Затем нажмите
кнопку OK.
3. Добавьте в проект файлы с исходным текстом: заголовочный файл Hello.h
и файл реализации Hello.cpp. Для добавления каждого файла выбирайте
команду File⇒New, затем указывайте тип и имя файла. Убедитесь, что
флажок Add To Project включен, так что этот файл будет добавлен в про-
ект. Затем нажмите OK, и введите содержимое файла.
4. Выберите команду Project⇒Settings и перейдите на закладку General. В
списке Microsoft Foundation Classes выберите вариант компоновки Use
MFC In A Shared DLL и затем нажмите OK.
Параметр связи с MFC типа Use MFC In A Shared DLL приводит к умень-
шению исполняемого файла, т.к. позволяет приложению обращаться к MFC
посредством DLL. Если вы выберете вариант компоновки Use MFC In A
Static Library, то Visual C++ присоединит к исполняемому EXE-файлу ва-
шего приложения двоичный код MFC, что приведет к значительному увели-
чению объема EXE-файла. С другой стороны, приложение, статически
скомпонованное с MFC, можно запустить на любом компьютере, независи-
мо от того, есть на нем MFC DLL или нет.
3) Прочитайте документ "Венгерская форма записи имен переменных и типы данных
Windows" (см. CD-ROM). Какой тип имеют переменные с именами bRepaint,
szMsg, nAge, cxLength, clrBtn? Запишите операторы описания этих перемен-
ных.

54
4) Откройте файл WinMain.cpp (хранится в папке \DevStudio\Vc\Mfc\Src) и
разберитесь с функцией AfxWinMain по описанию из п.2.4 лекции. Зачем в ней
нужен оператор goto?
Посмотрите исходный текст функций CWinApp::Run (файл AppCore.cpp) и
CWinThread::Run (файл ThrdCore.cpp) и найдите, где именно вызываются
OnIdle и ExitInstance.
5) В приложении Hello обеспечьте вывод символьной строки красным цветом внут-
ри зеленого описывающего прямоугольника. В контексте устройства для задания
цвета текста и фонового цвета предназначены функции-члены SetTextColor и
SetBkColor. Значение цвета имеет тип COLORREF. Это значение можно сформи-
ровать с помощью макроса RGB(r, g, b), например, красный цвет записывается
так: RGB(255, 0, 0).
6) На основе приложения Hello разработайте приложение, которое будет реагиро-
вать на следующие сообщения Windows:
WM_LBUTTONDOWN щелчок левой кнопкой мыши
WM_RBUTTONDOWN щелчок правой кнопкой мыши
WM_KEYDOWN нажатие клавиши на клавиатуре
WM_MOVE перемещение окна
WM_SIZE изменение размеров окна
WM_NCRBUTTONDOWN щелчок правой кнопкой мыши в
неклиентской области окна
WM_CLOSE закрытие окна
При обработке всех сообщений приложение с помощью функции каркаса
AfxMessageBox должно выдавать информационное окно с названием сообщения.
В конце обработчиков сообщений вызывайте обработчик из родительского класса
CFrameWnd, чтобы не изменять общепринятое поведение окна (иначе, например,
его не удастся закрыть при помощи мыши).
Прототипы функций-членов CMainWnd для обработки указанных сообщений уз-
найте в справочной системе, выполняя поиск по именам соответствующих макро-
сов карты сообщений (ON_WM_LBUTTONDOWN и т.п.)
7) Изучите англо-русский словарь терминов по теме 4-й лекции (см. CD-ROM).

55
Лекция 5. Отображение информации с помощью модуля
GDI
1. Контекст устройства
В однозадачных ОС (MS-DOS), любая программа может рисовать непосредст-
венно на экране. В многозадачных ОС программы так действовать не должны, т.к.
при одновременной работе нескольких программ пользователь должен видеть на эк-
ране согласованную картину, сформированную в результате их совместной работы.
Экранная область, принадлежащая программе A, должна быть защищена от информа-
ции, выводимой программой B. Доступом к видеоадаптеру, как и к другим устройст-
вам, управляет ОС. Она позволяет программам выводить информацию только в пре-
делах их окон. В Windows графическое отображение выполняет модуль GDI.
Windows-приложение не может рисовать что-либо непосредственно на экране,
принтере или каком-нибудь другом устройстве вывода. Все операции рисования про-
изводятся на воображаемом "устройстве", представляемом с помощью контекста уст-
ройства. Контекст устройства – эта служебная внутренняя структура Windows, в
которой хранятся все характеристики устройства и его текущего состояния, необхо-
димые модулю GDI для рисования. До начала рисования приложение должно полу-
чить от модуля GDI дескриптор контекста устройства. Этот дескриптор надо переда-
вать в качестве первого параметра всем функциям рисования GDI. Без корректного
дескриптора контекста устройства, GDI не будет знать, на каком именно устройстве и
в какой его области рисовать пикселы. В контексте устройства хранятся параметры,
позволяющие GDI выполнять отсечение и рисовать графические примитивы только
внутри заданных областей. Одни и те же функции GDI могут рисовать примитивы на
различных устройствах, т.к. специфика устройства скрыта в контексте устройства.
MFC избавляет программиста от необходимости непосредственной работы с
дескрипторами контекстов устройств. Дескриптор контекста устройства и функции
рисования GDI инкапсулированы в класс "Контекст устройства" – CDC. От него унас-
ледованы классы для представления различных контекстов устройств (см. табл. 5.1).
Таблица 5.1. Классы различных контекстов устройств
Имя класса С чем связан этот контекст устройства
CPaintDC Клиентская область окна (только в обработчиках OnPaint)
CClientDC Клиентская область окна
CWindowDC Окно, включая неклиентскую область
CMetaFileDC Метафайл GDI (аналог макроса, в котором запоминаются вызовы функ-
ций GDI и потом их можно многократно воспроизводить)
Объекты этих классов можно создавать как автоматически, так и динамически.
В конструкторе и деструкторе каждого класса вызываются функции GDI для получе-
ния и освобождения дескрипторов контекста устройства. Например, создать контекст
устройства в обработчике OnPaint можно так:
CPaintDC dc(this);
// Вызовы каких-либо функций-членов для рисования примитивов

Конструктору CPaintDC передается указатель на окно, с которым будет связан


контекст устройства.
Класс CPaintDC предназначен для рисования в клиентской области окна при
обработке сообщений WM_PAINT. Но приложения Windows могут выполнять графиче-
56
ское отображение не только в OnPaint. Например, требуется рисовать в окне окруж-
ность при каждом щелчке мышью. Это можно делать в обработчике сообщения мы-
ши, не дожидаясь очередного сообщения WM_PAINT. Для подобных операций отобра-
жения предназначен класс CClientDC. Он создает контекст устройства, связанный с
клиентской областью окна, которым можно пользоваться за пределами OnPaint.
Ниже приведен пример рисования диагоналей в клиентской области окна с по-
мощью CClientDC и двух функций-членов, унаследованных от CDC.
void CMainWindow::OnLButtonDown( UINT nFlags, CPoint point )
{
CRect rect;
GetClientRect(&rect);
CClientDC dc(this);
dc.MoveTo( rect.left, rect.top );
dc.LineTo( rect.right, rect.bottom );
dc.MoveTo( rect.right, rect.top );
dc.LineTo( rect.left, rect.bottom );
}

В редких случаях программе требуется получить доступ ко всему экрану (на-


пример, в программе захвата экрана). Тогда можно создать контекст устройства как
объект класса CClientDC или CWindowDC, но в конструктор передать нулевой указа-
тель. Например, нарисовать окружность в левой верхней части экрана можно так:
CClientDC dc( NULL );
dc.Ellipse( 0, 0, 100, 100 );

1.1 Атрибуты контекста устройства


В контексте устройства хранится ряд атрибутов, влияющих на работу функций
рисования. В классе CDC есть функции-члены для чтения текущих значений и для из-
менения этих атрибутов (табл. 5.2).
Таблица 5.2. Основные атрибуты контекста устройства
Атрибут Значение по Функция-член CDC для Функция-член СDC для
умолчанию задания значения получения значения
Цвет текста Черный SetTextColor GetTextColor
Цвет фона Белый SetBkColor GetBkColor
Режим фона OPAQUE SetBkMode GetBkMode
Режим преобразова- MM_TEXT SetMapMode GetMapMode
ния координат
Режим рисования R2_COPYPEN SetROP2 GetROP2
Текущая позиция (0,0) MoveTo GetCurrentPosition
Текущее перо BLACK_PEN SelectObject SelectObject
Текущая кисть WHITE_BRUSH SelectObject SelectObject
Текущий шрифт SYSTEM_FONT SelectObject SelectObject

Различные функции рисования CDC пользуются атрибутами по-разному. На-


пример, цвет, ширина и стиль (сплошная, штриховая и т.п.) линии для рисования от-
резка функцией LineTo определяются текущим пером. При рисовании прямоуголь-
ника функцией Rectangle модуль GDI рисует контур текущим пером, а внутреннюю
область заполняет текущей кистью. Цвет текста, фона и шрифт используются всеми
функциями отображения текста. Фоновый цвет применяется также при заполнении

57
промежутков в несплошных линиях. Если фоновый цвет не нужен, его можно отклю-
чить (сделать "прозрачным"):
dc.SetBkMode( TRANSPARENT );

Атрибуты CDC чаще всего изменяются с помощью функции SelectObject.


Она предназначена для "выбора" в контексте устройства объектов GDI 6-ти типов:
• перья (pens)
• кисти (brushes)
• шрифты (fonts)
• битовые карты (bitmaps)
• палитры (palettes)
• области (regions).
В MFC перья, кисти и шрифты представлены классами CPen, CBrush и CFont.
Свойства пера "по умолчанию": сплошная черная линия толщиной 1 пиксел; кисть
"по умолчанию": сплошная белая; шрифт "по умолчанию": пропорциональный
высотой примерно 12 пт. Вы можете создавать объекты-перья, кисти и шрифты с
нужными вам свойствами и выбирать их в любом контексте устройства.
Допустим, динамически были созданы объекты pPen и pBrush – черное перо
толщиной 10 пикселов и сплошная красная кисть. Для рисования эллипса с черным
контуром и красным заполнением можно вызвать следующие функции-члены:
dc.SelectObject( pPen );
dc.SelectObject( pBrush );
dc.Ellipse( 0, 0, 100, 100 );

Функция-член SelectObject перегружена для работы с указателями на раз-


личные объекты GDI. Она возвращает указатель на предыдущий выбранный в кон-
тексте устройства объект того же типа, что и объект, переданный функции в качестве
параметра.

1.2 Режимы преобразования координат


Один из самых сложных для освоения аспектов GDI – применение режимов
преобразования координат. Режим преобразования координат – это атрибут контек-
ста устройства, задающий способ пересчета логических координат в физические ко-
ординаты устройства. Логические координаты передаются функциям рисования CDC.
Физические координаты – это координаты пикселов в экранном окне или на листе
принтера (т.е. на поверхности изображения).
Допустим, вызывается функция Rectangle:
dc.Rectangle( 0, 0, 200, 100 );

Нельзя сказать, что эта функция нарисует прямоугольник шириной 200 пиксе-
лов и высотой 100 пикселов. Она нарисует прямоугольник шириной 200 логических
единиц и высотой 100 единиц. В режиме преобразования координат по умолчанию,
MM_TEXT, 1 логическая единица равна 1-му пикселу. В других режимах масштаб мо-
жет быть иным (см. табл. 5.3). Например, в режиме MM_LOMETRIC 1 логическая еди-
ница равна 1/10 мм. Следовательно, в показанном вызове Rectangle будет нарисован
прямоугольника шириной 20 мм и высотой 10 мм. Режимы, отличные от MM_TEXT,
удобно применять для рисования в одинаковом масштабе на различных устройствах
вывода.

58
Таблица 5.3. Режимы преобразования координат, поддерживаемые модулем GDI
Константа для обозначе- Расстояние, соответствующее Ориентация координат-
ния режима логической единице ных осей
MM_TEXT 1 пиксел x вправо, у вниз
MM_LOMETRIC 0.1 мм x вправо, у вверх
MM_HIMETRIC 0.01 мм x вправо, у вверх
MM_LOENGLISH 0.01 дюйма x вправо, у вверх
MM_HIENGLISH 0.001 дюйма x вправо, у вверх
MM_TWIPS 1/1440 дюйма (0.0007 дюйма) x вправо, у вверх
MM_ISOTROPIC Определяется пользователем Определяется пользовате-
(масштаб по осям x и y одина- лем
ков)
MM_ANISOTROPIC Определяется пользователем Определяется пользовате-
(масштаб по осям x и y задается лем
независимо)

Система координат в режиме MM_TEXT показана на рис. 5.1. Начало координат


располагается в левом верхнем углу поверхности изображения (в зависимости от кон-
текста устройства, это может быть левый верхний угол экрана, окна, клиентской об-
ласти окна). Ось х направлена вправо, ось y вниз, 1 логическая единица равна 1-му
пикселу. В остальных, "метрических", системах ось y направлена вверх, так что сис-
тема координат оказывается правой, но начало координат по умолчанию всегда по-
мещается в левый верхний угол поверхности изображения.
(0, 0)
x
Поверхность изображения

1 единица = 1 пиксел

Рис. 5.1. Система координат в режиме MM_TEXT.

1.3 Функции преобразования координат


Для преобразования логических координат в координаты устройства (физиче-
ские координаты) предназначена функция CDC::LPtoDP. Для обратного преобразо-
вания есть функция CDC::DPtoLP.
Допустим, надо вычислить координаты центра клиентской области окна в фи-
зических координатах. Для этого не требуется никаких преобразований, т.к. размеры
клиентской области в пикселах возвращает функция CWnd::GetClientRect:
CRect rect;
GetClientRect( &rect );
CPoint point( rect.Width()/2, rect.Height()/2 );

Для вычисления координат этой точки в режиме MM_LOMETRIC потребуется


функция DPtoLP:
CRect rect;
GetClientRect( &rect );
CPoint point( rect.Width()/2, rect.Height()/2 );
CClientDC dc( this );
dc.SetMapMode( MM_LOMETRIC );

59
dc.DPtoLP( &point );

Функции LPtoDP и DPtoLP часто применяются при обработке сообщений мы-


ши. Windows помещает в структуру сообщения координаты указателя в физической
системе координат. Поэтому, если вы ходите "нарисовать мышью" прямоугольник в
режиме MM_LOMETRIC, то перед рисованием необходимо преобразовать координаты
указателя из физических координат устройства в логические координаты контекста.
Иногда Windows-программисты употребляют термины "клиентские координа-
ты" и "экранные координаты". Клиентские координаты – это физические координаты,
заданные относительно левого верхнего угла клиентской области окна. Экранные ко-
ординаты – это физические координаты, заданные относительно левого верхнего угла
экрана. Преобразование между двумя этими системами выполняется с помощью
функций CWnd::ClientToScreen и CWnd::ScreenToClient.

1.4 Изменение положения начала координат


По умолчанию во всех режимах преобразования координат начало логической
системы координат располагается в левом верхнем углу поверхности изображения.
При работе с правыми системами координат м.б. удобнее поместить начало системы
в другую точку, например, в центр или левый нижний угол окна. Для этого можно
использовать одну из двух функций: CDC::SetWindowOrg (смещение левого верхне-
го угла поверхности изображения) или CDC::SetViewportOrg (смещение начала ло-
гической системы координат).
Предположим, требуется поместить начало логической системы координат в
центр окна. Это можно сделать так (считая, что dc – объект подкласса CDC):
CRect rect;
GetClientRect( &rect );
dc.SetViewportOrg( rect.Width()/2, rect.Height()/2 );

1.5 Получение характеристик устройства


Иногда бывает полезно узнать характеристики устройства, с которым связан
контекст. Для этого предназначена функция CDC::GetDeviceCaps. Например, полу-
чить ширину и высоту экрана (например, 1024х768) можно так:
CClientDC dc( this );
int cx = dc.GetDeviceCaps( HORZRES );
int cy = dc.GetDeviceCaps( VERTRES );

Некоторые возможные параметры функции GetDeviceCaps приведены в табл. 5.4.


Таблица 5.4. Часто используемые параметры функции GetDeviceCaps
Параметр Значение, возвращаемое функцией GetDeviceCaps
HORZRES Ширина поверхности изображения, в пикселах
VERTRES Высота поверхности изображения, в пикселах
HORZSIZE Ширина поверхности изображения, в миллиметрах
VERTSIZE Высота поверхности изображения, в миллиметрах
LOGPIXELSX Количество пикселов на логический дюйм по горизонтали
LOGPIXELSY Количество пикселов на логический дюйм по вертикали
NUMCOLORS Для дисплея – количество статических цветов, для принтера или плоттера –
количество поддерживаемых цветов
TECHNOLOGY Получение битовых флагов, идентифицирующих тип устройства – дисплей,
принтер, плоттер и др.

60
2. Рисование графических примитивов с помощью функций GDI

2.1 Рисование отрезков и кривых


Основные (хотя и не все) функции-члены CDC, предназначенные для рисования
отрезков и кривых, приведены в таблице 5.5.
Таблица 5.5. Функции-члены CDC для рисования отрезков и кривых
Функция Назначение
MoveTo Задает текущую позицию
LineTo Рисует отрезок из текущей позиции в заданную точку и смещает в нее
текущую позицию
Polyline Последовательно соединяет набор точек отрезками
PolylineTo Соединяет набор точек отрезками прямых, начиная с текущей позиции.
Текущая позиция смещается в последнюю точку набора.
Arc Рисует дугу
ArcTo Рисует дугу и смещает текущую позицию в конец дуги
PolyBezier Рисует один или несколько сплайнов Безье
PolyBezierTo Рисует один или несколько сплайнов Безье и помещает текущую пози-
цию в конец последнего сплайна
PolyDraw Рисует набор отрезков и сплайнов Безье через набор точек и смещает
текущую позицию в конец последнего отрезка или сплайна
Для рисования отрезка надо поместить текущую позицию в один из концов от-
резка и вызвать LineTo с координатами второго конца:
dc.MoveTo( 0, 0 );
dc.LineTo( 0, 100 );

При выводе нескольких соединяющихся отрезков MoveTo достаточно вызвать


только для одного из концов первого отрезка, например:
dc.MoveTo( 0, 0 );
dc.LineTo( 0, 100 );
dc.LineTo( 100, 100 );

Несколько отрезков можно построить одним вызовом Polyline или


PolylineTo (отличие между ними в том, что PolylineTo пользуется текущей
позицией, а Polyline – нет). Например, квадрат можно нарисовать так:
POINT aPoint[5] = { 0, 0, 0, 100, 100, 100, 100, 0, 0, 0 };
dc.Polyline( aPoint, 5 );

или с помощью PolylineTo:


dc.MoveTo( 0, 0 );
POINT aPoint[4] = { 0, 100, 100, 100, 100, 0, 0, 0 };
dc.PolylineTo( aPoint, 4 );

Для рисования дуг окружностей и эллипсов предназначена функция CDC::Arc.


В качестве параметров ей передаются координаты описывающего эллипс прямо-
угольника и координаты начальной и конечной точек дуги (эти точки задают углы
для вырезания дуги из эллипса, поэтому могут точно на него не попадать). Ниже при-
веден пример для рисования левой верхней четверти эллипса шириной 200 единиц и
высотой 100 единиц:
CRect rect(0, 0, 200, 100);
CPoint point1(0, -500);
CPoint point2(-500, 0);
dc.Arc(rect, point1, point2);

61
Важная особенность всех функций GDI для рисования отрезков и кривых в
том, что последняя точка не рисуется. Т.е. при рисовании отрезка из точки (0, 0) в
точку (100, 100):
dc.MoveTo( 0, 0 );
dc.LineTo( 100, 100 );

пиксел (100, 100) принадлежать отрезку не будет. Если необходимо, чтобы последний
пиксел тоже был закрашен цветом отрезка, это можно сделать с помощью функции
CDC::SetPixel, предназначенной для закраски отдельных пикселов.

2.2 Рисование эллипсов, многоугольников и других фигур


В GDI есть функции для рисования более сложных примитивов, чем отрезки и
кривые. Некоторые из них перечислены в табл. 5.6.
Таблица 5.6. Функции-члены CDC для рисования замкнутых фигур
Функция Описание
Chord Замкнутая фигура, образованная пересечением эллипса и отрезка
Ellipse Эллипс или окружность
Pie Сектор круговой диаграммы
Polygon Многоугольник
Rectangle Прямоугольник
RoundRect Прямоугольник с закругленными углами
Функциям GDI, рисующим замкнутые фигуры, передаются координаты описы-
вающего прямоугольника. Например, чтобы функцией Ellipse нарисовать окруж-
ность, надо указать не центр и радиус, а описывающий квадрат, например:
dc.Ellipse( 0, 0, 100, 100 );

Координаты описывающего прямоугольника можно передавать в виде структу-


ры RECT или как объект CRect:
CRect rect( 0, 0, 100, 100 );
dc.Ellipse( rect );

Как и последняя точка отрезка, нижняя строка и правый столбец описывающе-


го прямоугольника не заполняются. Т.е. при вызове CDC::Rectangle:
dc.Rectangle( 0, 0, 8, 4 );

результат будет такой, как на рис. 5.2.

Рис. 5.2. Прямоугольник, нарисованный вызовом dc.Rectangle(0,0,8,4)

62
2.3 Перья GDI и класс CPen
Для рисования отрезков, кривых и контуров фигур GDI использует объект-
перо, выбранное в контексте устройства. По умолчанию перо рисует сплошную чер-
ную линию толщиной 1 пиксел. Изменить вид линий можно, если создать соответст-
вующий объект-перо и выбрать его в контексте устройства функцией
CDC::SelectObject.
В MFC перья GDI представляются в виде объектов класса CPen. Проще всего
указать характеристика пера в конструкторе CPen, например:
CPen pen( PS_SOLID, 1, RGB(255, 0, 0) );

Второй способ: создать неинициализированный объект CPen, а затем создать


перо GDI вызовом CPen::CreatePen:
CPen pen;
pen.CreatePen( PS_SOLID, 1, RGB(255, 0, 0) );

Третий способ: создать неинициализированный объект CPen, заполнить струк-


туру LOGPEN характеристиками пера, а затем вызвать CPen::CreatePenIndirect
для создания пера GDI:
CPen pen;
LOGPEN lp;
lp.lopnStyle = PS_SOLID;
lp.lopnWidth.x = 1;
lp.lopnColor = RGB(255, 0, 0);
pen.CreatePenIndirect(&lp);

В структуре LOGPEN поле lopnWidth имеет тип POINT, но координата y не ис-


пользуется, а x задает толщину пера.
Функции CreatePen и CreatePenIndirect возвращают TRUE, если перо бы-
ло успешно создано (FALSE – если перо создать не удалось).
У пера есть три параметра: стиль, толщина и цвет. Возможные стили показаны
на рис. 5.3.
PS_DASHDOT
PS_SOLID PS_DASHDOTDOT
PS_DASH PS_NULL
PS_DOT PS_INSIDEFRAME
Рис. 5.3. Стили пера.

Стиль PS_INSIDEFRAME предназначен для рисования линий, которые всегда


располагаются внутри описывающего прямоугольника фигуры. Допустим, вы рисуе-
те окружность диаметром 100 единиц пером PS_SOLID толщиной 20 единиц. Тогда
реальный диаметр окружности по внешней границе будет 120 единиц (см. рис.5.4).
Если ту же окружность нарисовать пером стиля PS_INSIDEFRAME, то диаметр ок-
ружности будет действительно 100 единиц. На рисование отрезков и других прими-
тивов, не имеющих описывающего прямоугольника, стиль PS_INSIDEFRAME не влия-
ет.

63
Рис. 5.4. Стиль пера PS_INSIDEFRAME.

Стиль PS_NULL бывает нужен для рисования фигур без контура (например, эл-
липсов), только с заполнением внутренней области.
Толщина пера задается в логических единицах. Перья стилей PS_DASH,
PS_DOT, PS_DASHDOT и PS_DASHDOTDOT должны быть обязательно толщиной 1 еди-
ница. Если задать толщину 0 единиц, то будет создано перо шириной 1 пиксел неза-
висимо от режима преобразования координат.
Чтобы использовать новое перо, его надо выбрать в контексте устройства. На-
пример, чтобы нарисовать эллипс красным пером толщиной 10 единиц, можно вы-
полнить следующие действия:
CPen pen( PS_SOLID, 10, RGB(255, 0, 0) );
CPen* pOldPen = dc.SelectObject( &pen );
dc.Ellipse( 0, 0, 100, 100 );

2.4 Кисти GDI и класс CBrush


По умолчанию внутренняя область замкнутых фигур (Rectangle, Ellipse и
т.п.) заполняется белыми пикселами. Цвет и стиль заливки определяется параметрами
кисти, выбранной в контексте устройства.
В MFC кисть представляется классом CBrush. Бывают три типа кистей: сплош-
ные, штриховые и шаблонные. Сплошные кисти рисуют одним цветом. Для штрихо-
вых кистей есть 6 предопределенных стилей, они чаще всего используются в инже-
нерных и архитектурных чертежах (рис. 5.5). Шаблонные кисти рисуют путем повто-
рения небольшой битовой карты. У класса CBrush есть конструкторы для создания
кистей каждого типа.

Рис. 5.5. Стили штриховых кистей.

Для создания сплошной кисти в конструкторе CBrush достаточно указать зна-


чение цвета:

64
CBrush brush( RGB(255, 0, 0) );

или создать кисть в два этапа (сначала объект MFC, затем объект GDI):
CBrush brush;
brush.CreateSolidBrush( RGB(255, 0, 0) );

При создании штриховых кистей в конструкторе CBrush указываются стиль и


цвет кисти, например:
CBrush brush( HS_DIAGCROSS, RGB(255, 0, 0) );

или:
CBrush brush;
brush.CreateHatchBrush( HS_DIAGCROSS, RGB(255, 0, 0) );

При рисовании штриховой кистью GDI заполняет "пустые" места цветом фона
(по умолчанию белый, его можно изменить функцией CDC::SetBkColor или
включить/выключить заполнение фона режимом OPAQUE или TRANSPARENT с помо-
щью CDC::SetBkMode). Например, заштрихованный квадрат со стороной 100 единиц
можно нарисовать так:
CBrush brush( HS_DIAGCROSS, RGB (255, 255, 255) );
dc.SelectObject( &brush );
dc.SetBkColor( RGB(192, 192, 192) );
dc.Rectangle( 0, 0, 100, 100 );

2.5 Отображение текста


В предыдущей лекции уже упоминался один из способов вывода текста в окно
с помощью функции CDC::DrawText. Ей можно указать прямоугольник, внутри
которого выводить текст, и флаги, указывающие, как именно располагать текст
внутри прямоугольника. Например, для вывода текста в виде одной строки по центру
прямоугольника rect использовался вызов:
dc.DrawText( "Hello, MFC", -1, &rect,
DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER );

Кроме DrawText, в классе CDC есть еще несколько функций для работы с тек-
стом. Некоторые из них приведены в табл. 5.7. Одна из самых часто используемых –
функция TextOut, которая выводит текст подобно DrawText, но принимает в каче-
стве параметров координаты точки начала вывода текста или использует для этого
текущую позицию. Оператор:
dc.TextOut( 0, 0, "Hello, MFC" );
выведет строку "Hello, MFC", начиная с левого верхнего угла окна, связанного с кон-
текстом dc. Функция TabbedTextOut при выводе строки заменяет символы табуля-
ции на пробелы (массив позиций табуляции передается в качестве параметра).
По умолчанию, координаты, переданные в TextOut, TabbedTextOut и
ExtTextOut, считаются левым верхнем углом описывающего прямоугольника для
первого символа строки. Однако интерпретацию координат можно изменить, задав в
контексте устройства свойство выравнивания текста. Для этого используется функ-
ция CDC::SetTextAlign, например, для выравнивания текста по правой границе:
dc.SetTextAlign( TA_RIGHT );

65
Чтобы функция TextOut вместо явно указанных координат пользовалась те-
кущей позицией, надо вызвать SetTextAlign с указанием стиля и установленным
флагом TA_UPDATECP. Тогда TextOut после вывода каждой строки будет изменять
текущую позицию. Так можно вывести несколько строк подряд с сохранением
корректного расстояния между ними.
Таблица 5.7. Функции-члены CDC для вывода текста
Функция Описание
DrawText Выводит текст с заданным форматированием внутри описывающего
прямоугольника
TextOut Выводит символьную строку в текущей или заданной позиции
TabbedTextOut Выводит символьную строку, содержащую табуляции
ExtTextOut Выводит символьную строку с возможным заполнением описывающе-
го прямоугольника фоновым цветом или изменением межсимвольного
расстояния
GetTextExtent Вычисляет ширину строки с учетом параметров текущего шрифта
GetTabbedText Вычисляет ширину строки с табуляциями с учетом текущего шрифта
Extent
GetTextMetric Возвращает свойства текущего шрифта (высоту символа, среднюю
s ширину символа и т.п.)
SetTextAlign Задает параметры выравнивания для функции TextOut и некоторых
других функций вывода
SetTextJustif Задает дополнительную ширину, необходимую для выравнивания сим-
ication вольной строки
SetTextColor Задает в контексте устройства цвет вывода текста
SetBkColor Задает в контексте устройства цвет фона, которым заполняется область
между символами при выводе текста

Функции GetTextMetrics и GetTextExtent предназначены для получения


свойств текущего шрифта, выбранного в контексте устройства. GetTextMetrics
возвращает эти свойства в виде структуры TEXTMETRIC. GetTextExtent (или
GetTabbedTextExtent) вычисляет в логических единицах ширину заданной строки
с учетом текущего шрифта. Пример использования GetTextExtent – вычисление
ширины промежутка между словами, чтобы равномерно распределить текст по за-
данной ширине. Допустим, надо вывести строку в участке шириной nWidth. Для вы-
равнивания строки по обеим границам можно использовать следующие вызовы:
CString string = "Строка с тремя пробелами ";
CSize size = dc.GetTextExtent( string );
dc.SetTextJustification( nWidth - size.cx, 3 );
dc.TextOut( 0, y, string );

Второй параметр SetTextJustification задает число символов-


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

2.6 Шрифты GDI и класс CFont


Все текстовые функции-члены CDC пользуются текущим шрифтом, выбранным
в контексте устройства. Шрифт – это набор символов определенного размера (высо-
ты) и начертания, у которых есть общие свойства, например, толщина символа (нор-
66
мальная или жирная). В типографии размер шрифта измеряется в специальных еди-
ницах – пунктах. 1 пункт примерно равен 1/72 дюйма. Высота символа шрифта 12 пт
равна примерно 1/6 дюйма, но в Windows реальная высота несколько зависит от
свойств устройства вывода. Термин "начертание" определяет общий стиль шрифта.
Например, Times New Roman и Courier New являются различными начертаниями.
Шрифт – один из типов объектов модуля GDI. В MFC для работы со шрифтами
есть класс CFont. Сначала надо создать объект этого класса, а затем с помощью од-
ной из его функций-членов CreateFont, CreateFontIndirect, CreatePointFont
или CreatePointFontIndirect создать шрифт в модуле GDI. Функциям
CreateFont и CreateFontIndirect можно задавать размер шрифта в пикселах, а
CreatePointFont и CreatePointFontIndirect – в пунктах. Например, для соз-
дания 12-пунктного экранного шрифта Times New Roman функцией
CreatePointFont надо выполнить вызовы (размер задается в 1/10 пункта):
CFont font;
font.CreatePointFont( 120, "Times New Roman" );

Сделать то же самое с помощью CreateFont несколько сложнее, т.к. требует-


ся узнать, сколько в контексте устройства логических единиц приходится на один
дюйм по вертикали и затем перевести высоту из пунктов в пикселы:
CClientDC dc(this);
int nHeight = -((dc.GetDeviceCaps(LOGPIXELSY)*12)/72);
CFont font;
font.CreateFont( nHeight, 0, 0, 0, FW_NORMAL, 0, 0, 0,
DEFAULT_CHARSET, OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH ¦ FF_DONTCARE, "Times New Roman" );

Среди множества параметров CreateFont есть толщина, признак курсива и др


свойства. Эти же свойства шрифта можно хранить в специальной структуре LOGFONT
и передавать ее для создания шрифта в CreatePointFontIndirect, например:
LOGFONT lf;
memset( &lf, 0, sizeof(lf) );
lf.lfHeight = 120;
lf.lfWeight = FW_BOLD;
lf.lfItalic = TRUE;
strcpy( lf.lfFaceName, "Times New Roman" );
CFont font;
font.CreatePointFontIndirect( &lf );

Если вы попытаетесь создать шрифт, не установленный в системе, то GDI по-


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

2.7 Стандартные объекты GDI


В Windows есть набор предопределенных часто используемых перьев, кистей,
шрифтов и других объектов GDI, которые не надо создавать, а можно использовать
уже готовые. Они называются стандартными объектами GDI (табл. 5.8). Их можно
выбирать в контексте устройства с помощью функции CDC::SelectStockObject
или присваивать их существующим объектам CPen, CBrush, и др. с помощью
CGdiObject::CreateStockObject. Класс CGdiObject является базовым классом
для CPen, CBrush, CFont и других MFC-классов, представляющих объекты GDI.

67
Таблица 5.8. Часто используемые стандартные объекты GDI
Объект Описание
NULL_PEN Пустое (прозрачное) перо
BLACK_PEN Черное сплошное перо толщиной 1 пиксел
WHITE_PEN Белое сплошное перо толщиной 1 пиксел
NULL_BRUSH Пустая (прозрачная) кисть
HOLLOW_BRUSH То же, что NULL_BRUSH
BLACK_BRUSH Черная кисть
DKGRAY_BRUSH Темно-серая кисть
GRAY_BRUSH Серая кисть
LTGRAY_BRUSH Светло-серая кисть
WHITE_BRUSH Белая кисть
ANSI_FIXED_FONT Моноширинный системный шрифт ANSI
ANSI_VAR_FONT Пропорциональный системный шрифт ANSI
SYSTEM_FONT Системный шрифт для пунктов меню, элементов управления и т.п.
SYSTEM_FIXED_FONT Моноширинный системный шрифт (для совместимости со стары-
ми версиями Windows)
Допустим, требуется нарисовать светло-серый круг без контура. Это можно
сделать двумя способами, во-первых:
CPen pen( PS_NULL, 0, (RGB (0, 0, 0)) );
dc.SelectObject( &pen );
CBrush brush( RGB(192, 192, 192) );
dc.SelectObject(&brush);
dc.Ellipse( 0, 0, 100, 100 );

Т.к. прозрачное перо и светло-серая кисть есть среди стандартных объектов


GDI, ту же фигуру можно нарисовать так:
dc.SelectStockObject( NULL_PEN );
dc.SelectStockObject( LTGRAY_BRUSH );
dc.Ellipse( 0, 0, 100, 100 );

2.8 Удаление объектов GDI


Перья, кисти и другие объекты GDI занимают не только память программы, но
и служебную память GDI, объем которой ограничен. Поэтому крайне важно удалять
объекты GDI, которые больше не нужны. При автоматическом создании объектов
CPen, CBrush, CFont и др. подклассов CGdiObject соответствующие объекты GDI
автоматически удаляются из деструкторов этих классов. Если же объекты
CGdiObject создавались динамически оператором new, то обязательно надо вызы-
вать для них оператор delete. Явно удалить объект GDI, не уничтожая объект
CGdiObject, можно вызовом функции CGdiObject::DeleteObject. Стандартные
объекты GDI, даже "созданные" функцией CreateStockObject, удалять не надо.
Visual C++ может автоматически отслеживать объекты GDI, которые вы забы-
ли удалить. Для этого применяется перегрузка оператора new. Чтобы разрешить такое
слежение в конкретном исходном файле, после директивы включения заголовочного
файла Afxwin.h надо добавить директиву определения макроса:
#define new DEBUG_NEW

После завершения работы приложения номера строк и имена файлов, содер-


жащие не удаленные объекты GDI, будут показаны в отладочном окне Visual C++.

68
Для удаления объектов GDI важно знать, что нельзя удалить объект, который
выбран в контексте устройства. Следующий пример является ошибочным:
void CMainWindow::OnPaint()
{
CPaintDC dc( this );
CBrush brush( RGB(255, 0, 0) );
dc.SelectObject( &brush );
dc.Ellipse( 0, 0, 200, 100 );
}

Ошибка заключается в том, что объект CPaintDC создается раньше CBrush.


Т.к. оба объекта созданы автоматически, и CBrush – вторым, то его деструктор будет
вызван первым. Следовательно, соответствующая кисть GDI будет удаляться до того,
как будет удален объект dc. Эта попытка будет неудачной. Вы можете исправить по-
ложение, если создадите кисть первой. Но везде соблюдать подобное правило в про-
грамме тяжело, и очень утомительно искать такие ошибки.
В GDI нет функции для отмены выбора объекта в контексте, вроде
UnselectObject. Решение заключается в том, чтобы перед удалением объекта
CPaintDC выбрать в нем другие объекты GDI, например, стандартную кисть GDI.
Многие программисты поступают по-другому: при первом выборе в контексте уст-
ройства собственного объекта GDI сохраняют указатель на предыдущий объект, ко-
торый возвращается функцией SelectObject. Затем, перед удалением контекста, в
нем выбираются те объекты, которые были в нем "по умолчанию". Например:
CPen pen( PS_SOLID, 1, RGB(255, 0, 0) );
CPen* pOldPen = dc.SelectObject(&pen);
CBrush brush( RGB(0, 0, 255) );
CBrush* pOldBrush = dc.SelectObject( &brush );

dc.SelectObject( pOldPen );
dc.SelectObject( pOldBrush );

Способ с использованием стандартных объектов GDI реализуется так:


CPen pen( PS_SOLID, 1, RGB(255, 0, 0) );
dc.SelectObject( &pen );
CBrush brush( RGB(0, 0, 255) );
dc.SelectObject( &brush );

dc.SelectStockObject( BLACK_PEN );
dc.SelectStockObject( WHITE_BRUSH );

При динамическом создании объектов GDI нельзя забывать про оператор delete:
CPen* pPen = new CPen( PS_SOLID, 1, RGB(255, 0, 0) );
CPen* pOldPen = dc.SelectObject( pPen );

dc.SelectObject( pOldPen );
delete pPen;

3. Резюме
Чтобы обеспечить доступ к устройствам графического вывода одновременно
нескольким программам, в Windows применяется специальный системный механизм
– контекст устройства. Все операции рисования приложения выполняют с помощью
контекста устройства. Это служебная структура, в которой хранятся все характери-
стики конкретного устройства, необходимые модулю GDI для рисования пикселей и

69
графических примитивов. Контекст устройства в MFC представлен классом CDC, от
которого унаследованы подклассы для разновидностей контекстов устройств Win-
dows, например, CPaintDC, CClientDC, CWindowDC.
Приложение при вызове функций рисования указывает координаты примити-
вов в логической системе координат. Соответствие между логической системой коор-
динат контекста устройства и физической системой координат, связанной с поверхно-
стью изображения, задается режимом преобразования координат.
В физической системе координат устройства расстояния измеряются в пиксе-
лах. Точка (0, 0) всегда располагается в левом верхнем углу поверхности отображе-
ния, ось x направлена вправо, а y – вниз. У логической системы координат эти пара-
метры могут быть другими. Начало координат можно разместить в любом месте по-
верхности изображения, можно изменить ориентацию осей и масштаб (этим управля-
ет режим преобразования координат).
Функции рисования GDI условно делятся на несколько групп: рисование от-
резков и кривых, рисование замкнутых фигур, отображение текста и др. Полный спи-
сок функций рисования есть в разделе справочной системе Visual C++ по классу CDC,
в котором эти функции оформлены в виде функций-членов.
При рисовании графических примитивов свойства отображения, например,
цвет и стиль линии, задаются параметрами контекста устройства, среди которых наи-
более важные – текущие выбранные объекты GDI (перо для рисования линий, кисть
для заполнения областей, шрифт для вывода текста). Все классы-объекты GDI в MFC
унаследованы от базового класса CGdiObject: CPen для перьев, CBrush для кистей,
CFont для шрифтов. В каждом классе хранится дескриптор объекта GDI (в перемен-
ной-члене m_hObject).
Созданные программистом объекты GDI необходимо удалять. Перед удалени-
ем надо выбрать в контексте другой объект, т.к. текущий выбранный объект GDI уда-
лить нельзя.

4. Упражнения
1) Попробуйте выполнить примеры рисования, приведенные в лекции, подставляя
фрагменты исходного текста в обработчик OnPaint приложения Hello (оно было
рассмотрено в предыдущей лекции).
2) Изучите англо-русский словарь терминов по теме 5-й лекции (см. CD-ROM).
3) Выполните лабораторную работу №2, "Работа с модулем GDI" (см. CD-ROM).

70
Лекция 6. Работа с устройствами ввода. Использование
меню
В Windows клавиатура и мышь являются основными устройствами ввода. Мно-
гие операции с мышью и клавиатурой Windows выполняет автоматически, например,
выводит меню и отслеживает выбор в нем пункта, а затем посылает программе сооб-
щение WM_COMMAND с кодом выбранной команды. Можно написать полноценное при-
ложение, непосредственно не обрабатывая в нем сообщения мыши и клавиатуры.
При нажатии клавиш или при перемещении мыши эти устройства генерируют
прерывания. Прерывания обрабатываются драйверами устройств. Они помещают ин-
формацию о произошедших событиях в единую системную очередь, называемую
очередью необработанного ввода. Специальный системный поток отслеживает со-
держимое очереди необработанного ввода и перемещает каждое обнаруженное сооб-
щение в очередь сообщений подходящего потока.
Изучение способов обработки ввода с мыши и клавиатуры в Windows в основ-
ном сводится к ознакомлению с сообщениями мыши и клавиатуры, а также с набором
функций API (и MFC), полезных для их обработки.
В данной лекции также рассматривается использование меню в MFC-
приложениях. Практически все действия по обеспечению работы меню реализованы в
модуле USER Windows (вывод на экран, перемещение по меню и уведомление при-
ложения о выбранной команде). Приложения должны создать меню и обеспечить об-
работку выбираемых из них команд. В MFC предусмотрена маршрутизация команд
меню в специально назначенные для их обработки функции-члены классов. С помо-
щью обработчиков обновления меню приложение могло запрещать и помечать пунк-
ты меню в соответствии со своим текущим состоянием.

1. Получение данных от мыши


В целом в Windows с событиями мыши связаны более 20 сообщений, которые
можно разделить на две категории: сообщения мыши, связанные с клиентской обла-
стью окна, и сообщения, связанные с неклиентской областью окна. Они одинаковы по
смыслу, но различаются положением указателя в момент возникновения события.
События бывают следующими:
• нажатие или отпускание кнопки мыши;
• двойной щелчок кнопкой мыши;
• перемещение мыши.
В большинстве приложений сообщения неклиентской области игнорируются и
Windows обрабатывает их автоматически (например, выполняет перетаскивание окна
за строку заголовка).

1.1 Сообщения мыши, связанные с клиентской областью окна


Сообщения мыши данной группы приведены в таблице 6.1. Сообщения, имена
которых начинаются с WM_LBUTTON, относятся к левой кнопке мыши, WM_MBUTTON –к
средней кнопке, а WM_RBUTTON – к правой кнопке. Макросы карты сообщений и име-
на функций-членов CWnd для обработки сообщений мыши приведены в табл. 6.2.

71
Таблица 6.1. Сообщения мыши, связанные с клиентской областью окна
Сообщение Когда посылается
WM_LBUTTONDOWN Нажата левая кнопка мыши
WM_LBUTTONUP Левая кнопка мыши отпущена
WM_LBUTTONDBLCLK Двойной щелчок левой кнопкой мыши
WM_MBUTTONDOWN Нажата средняя кнопка мыши
WM_MBUTTONUP Средняя кнопка мыши отпущена
WM_MBUTTONDBLCLK Двойной щелчок средней кнопкой мыши
WM_RBUTTONDOWN Нажата правая кнопка мыши
WM_RBUTTONUP Правая кнопка мыши отпущена
WM_RBUTTONDBLCLK Двойной щелчок правой кнопкой мыши
WM_MOUSEMOVE Указатель мыши перемещается над клиентской областью окна
Сообщение WM_xBUTTONUP обычно (но не всегда) идет после
WM_xBUTTONDOWN. Сообщения мыши посылаются в окно, над которым располагается
указатель. Поэтому, если пользователь нажмет левую кнопку над клиентской обла-
стью некоторого окна, а отпустит ее за пределами окна, то это окно получит только
сообщение WM_LBUTTONDOWN. Многие программы реагируют только на сообщения о
нажатии кнопок мыши. Если важно обрабатывать и нажатие, и отпускание кнопки,
приложение должно использовать режим "захвата" мыши (см. п.1.2).
Таблица 6.2. Макросы карты сообщений и имена обработчиков для сообщений мыши, связан-
ных с клиентской областью окна.
Сообщение Макрос карты сообщений Имя функции-обработчика
WM_xBUTTONDOWN ON_WM_xBUTTONDOWN OnxButtonDown
WM_xBUTTONUP ON_WM_xBUTTONUP OnxButtonUp
WM_xBUTTONDBLCLK ON_WM_xBUTTONDBLCLK OnxButtonDblClk
WM_MOUSEMOVE ON_WM_MOUSEMOVE OnMouseMove

Прототипы у всех обработчиков сообщений мыши, например,


OnLButtonDown, одинаковы и имеют следующий вид:
afx_msg void OnMsgName( UINT nFlags, CPoint point )

point содержит координаты указателя в момент возникновения события мыши. Эти


координаты задаются в физической системе координат, связанной с левым верхним
углом клиентской области окна. При необходимости их можно преобразовать в логи-
ческие координаты контекста устройства с помощью функции CDC::DPtoLP.
Параметр nFlags содержит состояние кнопок мыши и клавиш клавиатуры
Shift и Ctrl в момент генерации сообщения. Состояние конкретной кнопки или кла-
виши можно извлечь из параметра nFlags с помощью операции побитового ИЛИ и
масок, перечисленных в табл. 6.3.
Таблица 6.3. Возможные флаги, составляющие значение параметра nFlags
Битовый флаг Когда флаг установлен
MK_LBUTTON Нажата левая кнопка мыши
MK_MBUTTON Нажата средняя кнопка мыши
MK_RBUTTON Нажата правая кнопка мыши
MK_CONTROL Нажата клавиша Ctrl
MK_SHIFT Нажата клавиша Shift
Сообщения мыши, связанные с неклиентской областью, аналогичны описан-
ным выше, только в именах констант добавляются символы NC (от слова nonclient).

72
Например, вместо WM_LBUTTONDOWN – WM_NCLBUTTONDOWN. Но эти сообщения
обрабатываются в приложениях значительно реже.

1.2 Режим захвата мыши


Выше была упомянута проблема, возникающая, когда программе требуется об-
рабатывать сообщения и о нажатии. и об отпускании кнопки мыши (например, при
рисовании или при выделении с помощью "резинового контура"). Если пользователь
отпустил кнопку мыши, когда указатель находится за пределами окна, то окно не по-
лучит сообщение об отпускании кнопки. Тогда, например, рисование не закончится
вовремя или "резиновый контур" окажется в неопределенном состоянии.
Для решения данной проблемы в Windows предусмотрен режим "захвата" мы-
ши. Приложение (точнее, окно приложения) может захватить мышь при получении
сообщения о нажатии кнопки. Это окно будет получать все сообщения мыши, незави-
симо от того, где находится указатель. При получении сообщения об отпускании
кнопки приложение может "освободить" мышь.
Захват мыши выполняется функцией CWnd::SetCapture, а освобождение –
функцией API ::ReleaseCapture. Обычно вызовы этих функций располагаются в
обработчиках нажатия и отпускания кнопки мыши, например:
// Фрагмент карты сообщений CMainWindow
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()

void CMainWindow::OnLButtonDown( UINT nFlags, CPoint point )


{
SetCapture();
}

void CMainWindow::OnLButtonUp( UINT nFlags, CPoint point )


{
::ReleaseCapture();
}

Между этими сообщениями, CMainWindow будет получать сообщения


WM_MOUSEMOVE, даже если указатель выйдет за пределы окна. В таком случае коорди-
наты указателя мыши могут стать отрицательными или превышать размеры клиент-
ской области окна.
У класса CWnd есть функция CWnd::GetCapture, возвращающая указатель на
окно, захватившее мышь (как на объект CWnd). В Win32 GetCapture возвращает
NULL, если мышь не захвачена или захвачена окном другого потока. Наиболее часто
GetCapture применяется для определения, захватило ли текущее окно мышь, сле-
дующим образом:
if ( GetCapture() == this )

1.3 Изменение формы указателя мыши


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

73
дескриптор указателя мыши хранится в переменной-члене
CMainWindow::m_hCursor. Тогда включить этот указатель над клиентской обла-
стью CMainWindow можно так:
// Фрагмент карты сообщений CMainWindow
ON_WM_SETCURSOR()

BOOL CMainWindow::OnSetCursor( CWnd* pWnd, UINT nHitTest, UINT message)


{
if ( nHitTest == HTCLIENT )
{
::SetCursor( m_hCursor );
return TRUE;
}
return CFrameWnd::OnSetCursor( pWnd, nHitTest, message );
}

Дескриптор указателя мыши генерируется либо при загрузке стандартного ука-


зателя, либо указателя, нарисованного в редакторе пиктограмм Developer Studio. За-
грузка стандартных указателей (у них есть предопределенные числовые идентифика-
торы, например IDC_ARROW или IDC_CROSS) выполняется функцией
CWinApp::LoadStandardCursor, которой передается один из идентификаторов
стандартных указателей. При вызове:
AfxGetApp()->LoadStandardCursor( IDC_ARROW );

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


Windows. Полный список стандартных указателей можно получить в справке по
функции CWinApp::LoadStandardCursor. Функции CWinApp::LoadCursor мож-
но передать идентификатор указателя, который вы самостоятельно разработали в ре-
дакторе пиктограмм Developer Studio.
В приложениях принято во время выполнения длительных действий показы-
вать указатель в виде песочных часов, обозначающий, что приложение "занято". Для
песочных часов есть стандартный указатель с идентификатором IDC_WAIT. Такой
указатель можно создать даже проще, чем функцией LoadStandardCursor – с
помощью специального класса MFC CWaitCursor. Можно создать объект этого
класса в стеке, например:
CWaitCursor wait;

В конструкторе CWaitCursor указатель в форме песочных часов выводится на


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

2. Получение данных с клавиатуры


Приложения Windows узнают о клавиатурных событиях так же, как и о собы-
тиях мыши: посредством сообщений. Как и мышь, клавиатура является ресурсом, ко-
торый с помощью операционной системы разделяется между несколькими приложе-
ниями. Сообщения мыши посылаются окну, над которым находится указатель. Сооб-
щения клавиатуры посылаются окну, находящемуся в "фокусе ввода". Такое окно
может быть только одно.

74
При каждом нажатии или отпускании клавиши приложение в "фокусе ввода"
получает сообщение. Если требуется знать, когда нажата или отпущена конкретная
клавиша, например, PgUp, программа может выполнять обработку сообщений
WM_KEYDOWN/WM_KEYUP с проверкой кода клавиши, сопровождающего это сообще-
ние. Если же программе требуются не коды клавиш, а символы, то она должна игно-
рировать сообщения о нажатии/отпускании клавиш, а обрабатывать сообщения
WM_CHAR, в которых передаются печатаемые символы. Они уже сформированы с уче-
том раскладки клавиатуры, текущего языка и состояния клавиш Shift и Caps Lock.
Текстовый курсор в Windows называется caret.. Обычно курсор выглядит как
вертикальная мерцающая черточка. Приложения, которые пользуются им, должны
включать курсор при получении фокуса (сообщение WM_SETFOCUS) и выключать при
потере (WM_KILLFOCUS). В классе CWnd есть набор функций для работы с текстовым
курсором, например, ShowCaret (включение курсора), HideCaret (выключение кур-
сора) и SetCaretPos (задание позиции курсора). Эти функции используются до-
вольно редко, поэтому подробно не рассматриваются.

2.1 Сообщения о нажатии клавиш


Windows информирует окно, находящееся в фокусе ввода, о нажатии и отпус-
кании клавиш сообщениями WM_KEYDOWN и WM_KEYUP. Эти сообщения генерируются
всеми клавишами, кроме Alt и F10 – "системных" клавиш, выполняющих в Windows
служебные действия. Эти клавиши генерируют сообщения WM_SYSKEYDOWN и
WM_SYSKEYUP. При нажатой клавише Alt любые другие клавиши тоже генерируют
сообщения WM_SYSKEYDOWN и WM_SYSKEYUP (вместо WM_KEYDOWN/WM_KEYUP).
Обработчики клавиатурных сообщений в MFC называются OnKeyDown,
OnKeyUp, OnSysKeyDown и OnSysKeyUp (им соответствуют макросы карты сообще-
ний ON_WM_KEYDOWN, ON_WM_KEYUP, ON_WM_SYSKEYDOWN и ON_WM_SYSKEYUP). Этим
обработчикам передается вспомогательная информация, в том числе код клавиши.
Все клавиатурные обработчики имеют одинаковый прототип:
afx_msg void OnMsgName( UINT nChar, UINT nRepCnt, UINT nFlags )

nChar – это код виртуальной клавиши, которая была нажата или отпущена, nRepCnt
– количество повторений нажатия/отпускания (обычно равно 1 для WM_KEYDOWN и
всегда равно 1 для WM_KEYUP). Большинство программ nRepCnt игнорируют. Значе-
ние nFlags содержит аппаратный скан-код клавиши и, возможно некоторые битовые
флаги, например, признак нажатой клавиши Alt.
Коды виртуальных клавиш позволяют идентифицировать клавиши независимо
от кодов, посылаемых клавиатурой конкретной модели. Для буквенных клавиш эти
коды совпадают с кодами символов ASCII, например, от 0x41 до 0x5A для англий-
ских заглавных букв от A до Z.
Остальные коды виртуальных клавиш определены как константы в файле
Winuser.h. Имена констант начинаются с VK_ (см. табл. 6.4).

Таблица 6.4. Некоторые коды виртуальных клавиш


Код виртуаль- Соответствующая кла- Код виртуаль- Соответствующая кла-
ной клавиши виша ной клавиши виша
VK_F1 - Функциональные клавиши VK_NEXT PgDn
VK_F12 F1 - F12

75
Код виртуаль- Соответствующая кла- Код виртуаль- Соответствующая кла-
ной клавиши виша ной клавиши виша
VK_CANCEL Ctrl-Break VK_END End
VK_RETURN Enter VK_HOME Home
VK_BACK Backspace VK_LEFT стрелка влево
VK_TAB Tab VK_UP стрелка вверх
VK_SHIFT Shift VK_RIGHT стрелка вправо
VK_CONTROL Ctrl VK_DOWN стрелка вниз
VK_MENU Alt VK_INSERT Ins
VK_PAUSE Pause VK_DELETE Del
VK_ESCAPE Esc VK_CAPITAL Caps Lock
VK_SPACE Spacebar VK_NUMLOCK Num Lock
VK_PRIOR PgUp VK_SCROLL Scroll Lock

2.2 Состояние клавиш


Внутри обработчиков клавиатурных сообщений иногда бывает нужно узнать
состояние клавиш Shift, Ctrl или Alt. Это можно сделать с помощью функции
::GetKeyState. которой передается код виртуальной клавиши,. Например, чтобы
узнать, нажата ли клавиша Shift, надо вызвать функцию:
::GetKeyState( VK_SHIFT )

Она вернет отрицательное значение, если Shift нажата, и неотрицательное – если не


нажата (признак нажатия обозначается старшим битом возвращаемого числа). Чтобы
выполнить некоторые действия по комбинации клавиш Ctrl+стрелка влево, можно в
обработчике OnKeyDown выполнить проверку:
if ( (nChar == VK_LEFT) && (::GetKeyState(VK_CONTROL) < 0) )
{

Функция ::GetKeyState возвращает состояние клавиши или кнопки мыши на мо-


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

2.3 Символьные сообщения


Часто в программах не требуется обрабатывать сообщения о нажа-
тии/отпускании клавиш, но необходимо получать с клавиатуры символы в соответст-
вии с состоянием клавиш Caps Lock, Shift и текущей раскладкой клавиатуры. В дан-
ном случае программа может обрабатывать сообщения WM_CHAR. Они генерируются
Windows в результате обработки сообщений WM_KEYDOWN/WM_KEYUP системной
функцией::TranslateMessage. Эта функция вызывается во внутреннем цикле об-
работки сообщений MFC.
Для обработки сообщения WM_CHAR надо занести в карту сообщений макрос
ON_WM_CHAR и добавить в класс функцию-член OnChar со следующим прототипом:
afx_msg void OnChar( UINT nChar, UINT nRepCnt, UINT nFlags )

Назначение параметров nRepCnt и nFlags то же, что и у сообщений


WM_KEYDONW/WM_KEYUP. Ниже приведен фрагмент исходного текста, обрабатываю-
щий английские буквенные клавиши, клавишу Enter и Backspace:
76
// Фрагмент карты сообщений класса CMainWindow
ON_WM_CHAR()

void CMainWindow::OnChar( UINT nChar, UINT nRepCnt, UINT nFlags )


{
if ( ( (nChar >= `A') && (nChar <= `Z') ) ||
( (nChar >= `a') && (nChar <= `z') ) )
{
// Отображение символа
}
else if ( nChar == VK_RETURN )
{
// Действия, связанные с клавишей Enter
}
else if ( nChar == VK_BACK )
{
// Действия, связанные с клавишей Backspace
}
}

3. Основные приемы программирования меню


Строка меню обычно располагается в верхней части главного окна приложе-
ния. Это меню называется главным меню (или меню верхнего уровня), а его команды
– пунктами главного меню. При выборе пунктов главного меню появляются выпа-
дающие меню, элементы которых называются просто пунктами меню. Пункты меню
отличаются друг от друга целочисленными значениями – идентификаторами пунктов
меню (идентификаторами команд). Windows также поддерживает всплывающие ме-
ню, которые выглядят так же, как выпадающие меню, но могут появляться в любом
месте экрана. Примером таких меню являются контекстные меню, вызываемые щелч-
ком правой кнопкой на некотором объекте. Выпадающие меню – это всплывающие
меню, которые являются подменю для меню верхнего уровня.
В большинстве окон верхнего уровня есть системное меню с командами пере-
мещения, изменения размеров окна, свертывания и развертывания. Это меню выгля-
дит как маленькая пиктограмма в левой части строки заголовка окна. Меню можно
открыть щелчком левой кнопки мыши по пиктограмме или клавишами Alt+пробел.
В MFC меню и сопутствующие функции инкапсулированы в класс CMenu. В
нем есть одна открытая переменная-член m_hMenu – дескриптор меню типа HMENU.
Среди функций-членов есть CMenu::TrackPopupMenu для вывода контекстного ме-
ню и CMenu::EnableMenuItem для разрешения/запрещения пункта меню.
Меню в MFC-приложении можно создать одним из трех способов:
• Программно, с помощью функций-членов класса CMenu, таких, как
CreateMenu и InsertMenu.
• Поместить описание меню в специальные структуры данных и затем пере-
дать эти структуры функции CMenu::LoadMenuIndirect.
• Создать ресурс меню с помощью редактора ресурсов и загружать это меню
во время выполнения программы. Этот метод используется наиболее часто.

77
3.1 Создание меню
Простейший способ создания меню – добавление шаблона меню в файл ресур-
сов приложения. Файл ресурсов – это текстовый файл, содержащий описание ресур-
сов приложения на специальном языке. У этого файла обычно расширение *.rc, по-
этому он часто упоминается как "RC-файл". Ресурс – это некоторый массив числовых
данных, описывающих, например, меню или пиктограмму. Windows поддерживает
ресурсы нескольких типов, в том числе меню, пиктограммы, растровые изображения
и строки. Специальная программа, компилятор ресурсов (входит в состав Visual C++),
компилирует текстовое содержимое RC-файла в двоичный вид. Затем компоновщик
присоединяет эти данные к исполняемому EXE-файлу приложения. Каждому ресурсу
в качестве идентификатора присвоена строка или число, например, "MyMenu" (строка)
или IDR_MYMENU (целое число). Целочисленным идентификаторам даются более по-
нятные человеку имена-константы, например, IDR_MYMENU. Эти константы определе-
ны директивами #define в заголовочном файле. После того, как ресурсу скомпили-
рованы и скомпонованы с EXE-файлом приложения, их можно загружать специаль-
ными функциями API или MFC.
Фрагмент описания меню в RC-файле приведен в виде фрагмента 6.1. Подоб-
ные описания редко формируются вручную, обычно ресурсы создаются с помощью
специальных редакторов ресурсов (в Visual C++ он встроен в среду разработки).
Фрагмент исходного текста 6.1. Часть описания шаблона меню.
IDR_MAINFRAME MENU PRELOAD DISCARDABLE
BEGIN
POPUP "&Файл"
BEGIN
MENUITEM "Созд&ать\tCtrl+N", ID_FILE_NEW
MENUITEM "&Открыть...\tCtrl+O", ID_FILE_OPEN
MENUITEM "&Сохранить\tCtrl+S", ID_FILE_SAVE
MENUITEM "Сохранить &как...", ID_FILE_SAVE_AS
MENUITEM SEPARATOR
MENUITEM "Последние открывавшиеся файлы", ID_FILE_MRU_FILE1,GRAYED
MENUITEM SEPARATOR
MENUITEM "В&ыход", ID_APP_EXIT
END
POPUP "&Правка"
BEGIN
MENUITEM "&Отменить\tCtrl+Z", ID_EDIT_UNDO
MENUITEM SEPARATOR
...
END

Значения ID_..., указанные после названий пунктов меню – это идентифика-


торы команд меню. Каждому пункту меню должен быть присвоен уникальный иден-
тификатор команды, чтобы приложение могло среагировать на выбор этого пункта.
По соглашению, эти идентификаторы определяются как числовые константы (через
директивы #define), имена которых начинаются с префикса ID_ или IDM_, за кото-
рым заглавными английскими буквами указывается имя пункта меню.
В имени пункта меню амперсанд обозначает горячую клавишу для выбора это-
го пункта. После символа табуляции принято (если есть) указывать ускоряющую кла-
вишу (например, Ctrl+O в "&Открыть…\tCtrl+O"). Ускоряющая клавиша – это кла-
виша или комбинация клавиш, нажатие которой аналогично выбору команды меню.

78
3.2 Загрузка и отображение меню
В начале выполнения программы ресурс меню надо загрузить и присоединить к
окну. Меню будет автоматически отображаться вместе внутри окна.
Во-первых, это можно сделать с помощью функции CFrameWnd::Create, ко-
торой надо передать идентификатор меню, например (если идентификатором ресурса
меню IDR_MAINFRAME):
Create( NULL, "Название приложения", WS_OVERLAPPEDWINDOW,
rectDefault, NULL, MAKEINTRESOURCE( IDR_MAINFRAME ) );

Макрос MAKEINTRESOURCE преобразует целочисленный идентификатор ресур-


са в значение типа LPTSTR, которое требуется большинству функций API для загруз-
ки ресурсов.
Второй способ – использовать функцию CFrameWnd::LoadFrame. Ей тоже на-
до передать идентификатор ресурса, например:
LoadFrame( IDR_MAINFRAME, WS_OVERLAPPEDWINDOW, NULL, NULL );

LoadFrame создает окно-рамку и присоединяет к нему меню, так же, как и функция
Create. Многие приложения MFC, в частности, сгенерированные с помощью масте-
ра AppWizard, используют функцию LoadFrame, потому что она загружает не толь-
ко меню, но и пиктограммы приложения, таблицу ускоряющих клавиш и некоторые
другие ресурсы. Для LoadFrame макрос MAKEINTRESOURCE не нужен.

3.3 Реакция на команды меню


Самым важным сообщением, связанным с меню, является WM_COMMAND. Оно
посылается после выбора пользователем пункта меню. В младшем слове параметра
сообщения wParam содержится идентификатор команды для выбранного пункта ме-
ню. MFC автоматически вызывает обработчик, зарегистрированный в карте сообще-
ний для этой команды меню. Например, чтобы зарегистрировать обработчик коман-
ды для пункта меню ID_FILE_SAVE, надо поместить в карту сообщений запись:
ON_COMMAND( ID_FILE_SAVE, OnFileSave )

У обработчиков команд нет параметров и возвращаемого значения. Например,


обработчик команды Файл⇒Выход обычно реализуется так:
void CMainWindow::OnFileExit()
{
PostMessage( WM_CLOSE, 0, 0 );
}

Имена обработчиков команд меню можно выбирать произвольным образом,


они не заданы жестко, как обработчики сообщений WM_....

3.4 Обновление состояния пунктов меню


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

79
В MFC есть специальный механизм для обновления пунктов меню – макрос
карты сообщений ON_UPDATE_COMMAND_UI. Он предназначен для регистрации обра-
ботчиков обновления для отдельных пунктов меню. Эти обработчики вызываются пе-
ред каждым выводом пункта на экран. Обработчику обновления передается указатель
на объект CCmdUI, функции-члены которого можно использовать для модификации
пункта меню. Класс CCmdUI не специфичен именно для пунктов меню, поэтому дан-
ный способ обновления можно использовать для обновления кнопок панели инстру-
ментов и других элементов пользовательского интерфейса.
Допустим, в приложении есть меню Цвет и глобальная переменная для хране-
ния текущего цвета m_nCurrentColor (0/1/2 – красный, зеленый, синий). Обработ-
чики команд и обновления пунктов меню Цвет можно записать следующим образом:
// Фрагмент карты сообщений CMainWindow
ON_COMMAND( ID_COLOR_RED, OnColorRed )
ON_COMMAND( ID_COLOR_GREEN, OnColorGreen )
ON_COMMAND( ID_COLOR_BLUE, OnColorBlue )
ON_UPDATE_COMMAND_UI( ID_COLOR_RED, OnUpdateColorRed )
ON_UPDATE_COMMAND_UI( ID_COLOR_GREEN, OnUpdateColorGreen )
ON_UPDATE_COMMAND_UI( ID_COLOR_BLUE, OnUpdateColorBlue )

void CMainWindow::OnColorRed()
{
m_nCurrentColor = 0;
}

...

void CMainWindow::OnUpdateColorRed( CCmdUI* pCmdUI )


{
pCmdUI->SetCheck( m_nCurrentColor == 0 );
}

...

void CMainWindow::OnUpdateColorBlue( CCmdUI* pCmdUI )


{
pCmdUI->SetCheck( m_nCurrentColor == 2 );
}

Макрос ON_UPDATE_COMMAND_UI связывает пункты меню с обработчиками


обновления, аналогично тому, как ON_COMMAND связывает их с обработчиками ко-
манд. Вызов CCmdUI::SetCheck позволяет включить/выключить пометку соответст-
вующего пункта меню. Кроме SetCheck, в классе CCmdUI есть еще несколько функ-
ций-членов, полезных для изменения пунктов меню. Они перечислены в таблице::
Функция-член Описание
CCmdUI::Enable Разрешает/запрещает пункт меню
CCmdUI::SetCheck Включает/выключает пометку пункта меню
CCmdUI::SetRadio Включает/выключает маркер возле пункта меню
CCmdUI::SetText Изменяет текст пункта меню
Функция SetRadio похожа на SetCheck, но добавляет или удаляет маркер-
окружность, а не метку в виде галочки. Обычно маркер применяется для отметки те-
кущего выбора из группы взаимно исключающих команд, а метка – для выбора неза-
висимой команды.

80
3.5 Ускоряющие клавиши
При разработке меню приложения можно завести ускоряющие клавиши, по-
зволяющие с помощью комбинации клавиш быстро выбирать команды меню, даже не
входя в него. Для этого надо создать специальный ресурс – таблицу ускоряющих кла-
виш, в которой сопоставлены идентификаторы пунктов меню и комбинации клавиш.
В программе этот ресурс надо загрузить. Если у приложения главным окном служит
окно-рамка, то Windows и это окно выполнят всю обработку ускоряющих клавиш ав-
томатически. Приложение будет получать сообщения WM_COMMAND так же, как и в
случае выбора команд меню.
В RC-файле таблица ускоряющих клавиш выглядит примерно так (как и шаб-
лон меню, она не записывается вручную, а создается в редакторе ресурсов):
IDR_MAINFRAME ACCELERATORS PRELOAD MOVEABLE
BEGIN
"N", ID_FILE_NEW, VIRTKEY,CONTROL
"Z", ID_EDIT_UNDO, VIRTKEY,CONTROL
"X", ID_EDIT_CUT, VIRTKEY,CONTROL
...
VK_DELETE, ID_EDIT_CUT, VIRTKEY,SHIFT
...
END

В данном примере IDR_MAINFRAME является идентификатором ресурса. Обыч-


но идентификаторы меню и таблицы ускоряющих клавиш одинаковы. В каждой стро-
ке таблицы определяется одна ускоряющая клавиша. Сначала указывается клавиша,
затем идентификатор соответствующего пункта меню, а затем, после слова VIRTKEY,
коды виртуальных клавиш-модификаторов для указания комбинации клавиш
(CONTROL, ALT или SHIFT). В приведенном примере для команды ID_FILE_NEW оп-
ределена ускоряющая клавиша Ctrl+N и т.д.
Подобно меню, ускоряющие клавиши должны быть загружены и присоедине-
ны к окну перед тем, как оно будет выведено на экран. У окна-рамки это делает
функция-член LoadAccelTable:
LoadAccelTable( MAKEINTRESOURCE( IDR_MAINFRAME ) );

Эту функцию можно не вызывать явно, она вызывается изнутри LoadFrame,


которая загружает меню и таблицу ускоряющих клавиш, если у них одинаковый
идентификатор ресурса.:
LoadFrame( IDR_MAINFRAME, WS_OVERLAPPEDWINDOW, NULL, NULL );

3.6 Контекстные меню


Во многих приложениях Windows правая кнопка мыши используется для вызо-
ва контекстных меню с командами, применимыми к объекту, на котором произведен
щелчок мышью. При щелчке правой кнопкой Windows посылает окну сообщение
WM_CONTEXTMENU (если только щелчок правой кнопкой не был обработан по сообще-
нию WM_MOUSEDOWN).
Для вывода контекстного меню на экран можно использовать функцию
CMenu::TrackPopupMenu:
BOOL TrackPopupMenu( UINT nFlags, int x, int y, CWnd* pWnd,
LPCRECT lpRect = NULL )

81
Параметры x и y содержат экранные координаты для вывода меню, nFlags – бито-
вые флаги, задающие горизонтальное выравнивание меню относительно координаты
x, а также то, какие кнопки мыши (или клавиши) могут быть использованы для выбо-
ра пунктов меню. Допустимые флаги выравнивания: TPM_LEFTALIGN,
TPM_CENTERALIGN и TPM_RIGHTALIGN; флаги кнопок мыши: TPM_LEFTBUTTON и
TPM_RIGHTBUTTON. Указатель pWnd задает окно, которому будут послано сообщение
о выбранной команде. Допустим, pMenu является указателем на объект CMenu, пред-
ставляющий контекстное меню. Тогда вызов:
pMenu->TrackPopupMenu( TPM_LEFTALIGN ¦ TPM_LEFTBUTTON ¦ TPM_RIGHTBUTTON,
32, 64, AfxGetMainWnd() );

выведет меню, левый верхний угол которого располагается в точке (32, 64) относи-
тельно левого верхнего угла экрана. Пользователь может выбирать команды в меню
любой кнопкой мыши. Сообщения меню будут посланы главному окну приложения.
Функция-член TrackPopupMenu обычно вызывается из обработчиков сообще-
ний WM_CONTEXTMENU (ему соответствует макрос карты сообщений
ON_WM_CONTEXTMENU). Обработчик сообщения должен иметь имя OnContextMenu и
соответствовать прототипу:
afx_msg void OnContextMenu( CWnd* pWnd, CPoint point )

Параметр pWnd указывает на окно, в котором произошел щелчок правой кнопкой. а


point содержит экранные координаты указателя мыши.
При необходимости экранные координаты point можно преобразовать в коор-
динаты клиентской области окна вызовом CWnd::ScreenToClient. Если обработ-
чик OnContextMenu не обработал сообщение, он должен вызвать обработчик базово-
го класса. Ниже приведен обработчик, в котором контекстное меню pContextMenu
вызывается только при щелчке в верхней половине окна:
void CChildView::OnContextMenu( CWnd* pWnd, CPoint point )
{
CPoint pos = point;
ScreenToClient(&pos);
CRect rect;
GetClientRect(&rect);
rect.bottom /= 2;
if ( rect.PtInRect(pos) )
{
pContextMenu->TrackPopupMenu( TPM_LEFTALIGN ¦
TPM_LEFTBUTTON ¦ TPM_RIGHTBUTTON, point.x, point.y,
AfxGetMainWnd() );
return;
}
CWnd::OnContextMenu( pWnd, point );
}

Загрузить контекстное меню, разработанное в редакторе ресурсов, удобнее все-


го с помощью функции CMenu::LoadMenu, например:
CMenu menu;
menu.LoadMenu( IDR_CONTEXTMENU );
menu.GetSubMenu(0)->TrackPopupMenu( TPM_LEFTALIGN ¦ TPM_LEFTBUTTON ¦
TPM_RIGHTBUTTON, point.x, point.y, AfxGetMainWnd() );

82
4. Упражнения
1) Напишите приложение, в центре клиентской области которого выводится красная
окружность с черным контуром (диаметр окружности – 50 пикселов, толщина
контура –5 пикселов). Окружность можно перетаскивать мышью "за внутреннюю
область" в пределах клиентской области окна. При перетаскивании окружности
приложение должно выполнять захват мыши. Стрелками курсора окружность
можно перемещать на 1 пиксел в заданном направлении, а в комбинации с Ctrl –
на 10 пикселов в заданном направлении. По нажатию пробела или правой кнопки
мыши над окружностью она должна возвращаться в центр окна.
2) Добавьте в приложение из п.1) изменение формы указателя мыши на направлен-
ные в 4 стороны стрелки, когда указатель находится над окружностью. Это стан-
дартный указатель с идентификатором IDC_SIZEALL.
3) Сделайте так, чтобы окружность в процессе перетаскивания отображалась без
контура (но диаметр оставался 50 пикселов). Для этого можно проверять в обра-
ботчике OnPaint, не захвачена ли мышь главным окном приложения.
4) Добавьте в приложение вывод в центре окружности последнего символа, получен-
ного вместе с сообщением WM_CHAR. Для отображения символа используйте полу-
жирный шрифт Arial.
5) Сделайте так, чтобы при нажатой клавише Ctrl окружность выводилась без конту-
ра (как при перетаскивании), чтобы показывать пользователю, что программа го-
това к перемещению окружности (по нажатию клавиш курсора).
6) Изучите англо-русский словарь терминов по теме 6-й лекции (см. CD-ROM).
7) Выполните лабораторную работу №3, "Работа с меню в MFC-приложениях" (см.
CD-ROM).

83
Лекция 7. Элементы управления
Элемент управления (ЭУ) – это дочернее окно специального типа, обычно
применяемое для того, чтобы пользователь мог с его помощью выполнить какое-то
простое действие (например, выполнить команду). В результате этого действия эле-
мент управления посылает окну-владельцу сообщение. Например, у нажимаемой
кнопки есть единственная простая функция – когда пользователь нажимает кнопку, то
она посылает своему родительскому окну (диалоговому окну) сообщение
WM_COMMAND. Чаще всего ЭУ встречаются в диалоговых окнах, но их можно исполь-
зовать и в любых других окнах, в т.ч. в окнах верхнего уровня.
В Windows есть набор стандартных классов ЭУ (6 типов). Они появились в са-
мой первой версии Windows и реализованы в модуле User.exe. Еще примерно 15
типов ЭУ появились в Windows 95. Их, чтобы отличать от ЭУ старых версий Win-
dows, иногда называются стандартными элементами управления Windows 95. Они
реализованы в динамической библиотеке Comctl32.dll.
Т.к. ЭУ являются дочерними окнами, они автоматически перемещаются вместе
с родительским окном, автоматически уничтожаются вместе с ним, а также ограниче-
ны при отображении областью родительского окна. Все сообщения от ЭУ посылают-
ся родительским окнам.

1. Стандартные элементы управления


В таблице 7.1 перечислены 6 типов ЭУ, для которых Windows автоматически
регистрирует оконные классы, вместе с соответствующими классами-оболочками
MFC.
Таблица 7.1. Стандартные элементы управления
Тип элемента Имя оконного класса WNDCLASS Класс MFC
Кнопка BUTTON CButton
Список LISTBOX CListBox
Элемент редактирования EDIT CEdit
Комбинированный список COMBOBOX CComboBox
Полоса прокрутки SCROLLBAR CScrollBar
Статический элемент STATIC CStatic

ЭУ можно создать как объект класса MFC и вызвать у него функцию-член


Create, например, для создания кнопки с надписью Запуск:
CButton m_wndPushButton;
m_wndPushButton.Create( "Запуск", WS_CHILD ¦ WS_VISIBLE ¦ BS_PUSHBUTTON,
rect, this, IDC_BUTTON );

В этом примере создается нажимаемая кнопка (стиль BS_PUSHBUTTON), кото-


рая будет дочерним окном окна this и будет занимать в его клиентской области об-
ласть rect. Целочисленный идентификатор IDC_BUTTON часто называется иденти-
фикатором дочернего окна или идентификатором элемента управления. В данном
окне все дочерние ЭУ, на сообщения которых требуется реагировать, должны иметь
уникальные идентификаторы.
ЭУ посылают окну-владельцу уведомления о событиях в виде сообщений
WM_COMMAND. Смысл этих сообщений зависит от типа элемента, но в любом случае,
уточняющая информация хранится в параметрах сообщения wParam и lParam. В них
передается идентификатор ЭУ и код уведомления. Так, при нажатии нажимаемой
84
кнопки она посылает сообщение WM_COMMAND с кодом уведомления BN_CLICKED, ко-
торый занимает старшие 16 бит слова wParam. Идентификатор кнопки помещается в
младшие 16 бит слова wParam. В lParam передается оконный идентификатор кнопки.
Чтобы не разбирать сообщения WM_COMMAND "поразрядно", в большинстве
MFC-приложений для связи уведомлений ЭУ с функциями-членами для их обработки
используется карта сообщений. Например, чтобы при нажатии кнопки IDC_BUTTON
вызывалась функция-член OnButtonClicked, в карту сообщений надо внести запись:
ON_BN_CLICKED( IDC_BUTTON, OnButtonClicked )

ON_BN_CLICKED – один из нескольких макросов карты сообщений MFC, свя-


занных с уведомлениями ЭУ. Есть набор макросов ON_EN_... для элементов редак-
тирования и ON_LBN_... для списков. Также есть общий макрос ON_CONTROL, кото-
рый позволяет обрабатывать любые уведомления от ЭУ любого типа. ЭУ посылают
сообщения своим окнам-владельцам, но очень часто сообщения посылаются и в об-
ратном направлении. Например, в кнопке с независимой фиксацией можно поставить
флажок, если послать ей уведомление BM_SETCHECK с параметром
wParam=BST_CHECKED. MFC упрощает посылку сообщений ЭУ за счет того, что в их
классах-оболочках есть функции-члены с понятными названиями. Например, чтобы
послать сообщение BM_SETCHECK, можно вызвать функцию-член CButton:
m_wndCheckBox.SetCheck( BST_CHECKED );

Т.к. ЭУ являются окнами, для работы с ними полезны некоторые функции-


члены, унаследованные от CWnd. Например, функция SetWindowText меняет заголо-
вок окна верхнего уровня, но также помещает текст в элемент редактирования. Есть и
другие полезные функции CWnd: GetWindowText для получения текста от ЭУ,
EnableWindow для включения/выключения ЭУ, SetFont для изменения шрифта ЭУ.
Если вы хотите сделать что-то с ЭУ, но не находите подходящей функции-члена в
классе-оболочке ЭУ, может быть, вы найдете нужную функцию в классе CWnd.

1.1 Кнопки: класс CButton


Класс CButton представляет ЭУ "кнопка". Есть четыре разновидности кнопок
(рис. 7.1): нажимаемые кнопки, кнопки с независимой фиксацией, кнопки с зависи-
мой фиксацией и групповые блоки.

Рис. 7.1. Четыре разновидности ЭУ "Кнопка".


При создании кнопки определенного типа ей вместе с флагами оконного стиля
указывается один из стилей кнопки, например, BS_PUSHBUTTON или BS_CHECKBOX.
Некоторые стили влияют на способ расположения текста на кнопке (BS_LEFT,
BS_CENTER и др.).

85
1.1.1 Нажимаемые кнопки
Нажимаемая кнопка – это ЭУ кнопка со стилем BS_PUSHBUTTON. При нажатии
она посылает родительскому окну уведомление BN_CLICKED. Например, обработку
нажатия кнопки можно выполнить так:
// Фрагмент карты сообщений CMainWindow
ON_BN_CLICKED( IDC_BUTTON, OnButtonClicked )
...
void CMainWindow::OnButtonClicked()
{
MessageBox( "Кнопка была нажата!" );
}

Как и командные обработчики пунктов меню, обработчики BN_CLICKED не


имеют ни параметров, ни возвращаемого значения.

1.1.2 Кнопки с независимой фиксацией


Эти кнопки (флажки) создаются со стилем BS_CHECKBOX, BS_AUTOCHECKBOX,
BS_3STATE или BS_AUTO3STATE. Кнопки стилей BS_CHECKBOX и BS_AUTOCHECKBOX
имеют два состояния: флажок установлен или нет. Состояния кнопки изменяется
функцией CButton::SetCheck:
m_wndCheckBox.SetCheck( BST_CHECKED ); // Установить флажок
m_wndCheckBox.SetCheck( BST_UNCHECKED ); // Снять флажок

Для проверки текущего состояния кнопки служит функция


CButton::GetCheck. Возвращаемое значение равно BST_CHECKED или
BST_UNCHECKED.
Как и нажимаемые кнопки, флажки при нажатии посылают родительским ок-
нами уведомления BN_CLICKED. Кнопки со стилем BS_AUTOCHECKBOX изменяют со-
стояния при щелчке мышью автоматически, а со стилем BS_CHECKBOX – нет. Поэтому
для кнопок со стилем BS_CHECKBOX требуется обработчик BN_CLICKED, который бу-
дет управлять состоянием кнопки, например:
void CMainWindow::OnCheckBoxClicked()
{
m_wndCheckBox.SetCheck( m_wndCheckBox.GetCheck() ==
BST_CHECKED ? BST_UNCHECKED : BST_CHECKED );
}

Кнопки со стилем BS_3STATE или BS_AUTO3STATE имеют не два, а три со-


стояние. Добавочное состояние – "неопределенное", обозначается константой
BST_INDETERMINATE:
m_wndCheckBox.SetCheck( BST_INDETERMINATE );

Кнопка в неопределенном состоянии выглядит как флажок на сером фоне. Это


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

86
1.1.3 Кнопки с зависимой фиксацией
Кнопки с зависимой фиксацией (радиокнопки) имеют стиль BS_RADIOBUTTON
или BS_AUTORADIOBUTTON. Обычно они используются в виде групп кнопок, когда
каждая кнопка обозначает один из нескольких взаимно исключающих параметров.
При нажатии кнопка BS_AUTORADIOBUTTON включает свою пометку, и отключает
пометку у другой кнопки в группе. При использовании кнопки со стилем
BS_RADIOBUTTON включать/выключать пометку придется программно с помощью
CButton::SetCheck.
Радиокнопки, как и другие кнопки, посылают уведомления BN_CLICKED. Для
кнопок со стилем BS_AUTORADIOBUTTON обработка этих уведомлений необязательна,
они автоматически меняют свое состояние.

1.1.4 Групповые блоки


Групповой блок – это ЭУ "кнопка" со стилем BS_GROUPBOX. В отличие от кно-
пок других типов, групповой блок никогда не получает фокуса ввода и не посылает
уведомлений родительскому окну.
Его единственная функция – визуально отделять группы ЭУ друг от друга. За-
ключив группу ЭУ внутрь группового блока, можно ясно показать пользователю, что
эти ЭУ имеют некоторое общее назначение. На логическое группирование ЭУ блоки
не влияют, поэтому недостаточно просто поместить набор ЭУ внутрь блока, надо
обеспечить и соответствующую программную обработку для них.

1.2 Списки: класс CListBox


Элемент "список" предназначен для отображения списка текстовых строк, на-
зываемых элементами списка. У списка есть возможности сортировки элементов и
прокрутки, если они не помещаются в области списка на экране.
Списки полезны для представления пользователю информации и для выбора
одного или нескольких элементов из них. При щелчке или двойном щелчке на эле-
менте список (если у него установлен стиль LBS_NOTIFY) посылает родительскому
окну уведомление в виде сообщения WM_COMMAND.
Обычно список выводит элементы в виде вертикального столбца и позволяет
выбрать только один из них. Выбранный (выделенный) элемент подсвечивается сис-
темным цветом COLOR_HIGHLIGHT. В Windows есть разновидности списка: список с
выбором нескольких элементов, многоколоночных список, список с собственным
отображением.

1.2.1 Создание списка


Обычный список с одиночным выбором можно создать следующим образом:
CListBox m_wndListBox;
m_wndListBox.Create( WS_CHILD ¦ WS_VISIBLE ¦ LBS_STANDARD,
rect, this, IDC_LISTBOX );

Стиль LBS_STANDARD является объединением стилей WS_BORDER,


WS_VSCROLL, LBS_NOTIFY и LBS_SORT. Т.е. у списка будет рамка, вертикальная по-
лоса прокрутки, он будет посылать уведомления родительскому окну при смене вы-
деления или при двойном щелчке на элементе, и он будет сортировать элементы по
87
алфавиту. По умолчанию полоса прокрутки включается, только если элементы не
умещаются в области списка.
По умолчанию список перерисовывает себя при добавлении или удалении эле-
мента. Если добавляется несколько сотен элементов, это может замедлять работу про-
граммы и приводить к мерцанию списка на экране. Перед добавлением большого ко-
личества элементов можно запретить рисование списка, а затем снова разрешить:
m_wndListBox.SendMessage(WM_SETREDRAW,FALSE,0); // Запрещение рисования
...
m_wndListBox.SendMessage(WM_SETREDRAW,TRUE,0); // Разрешение рисования

1.2.2 Добавление и удаление элементов


Добавление элементов в список выполняется функциями
CListBox::AddString и CListBox::InsertString (при вставке строки указыва-
ется индекс элемента, начиная с 0):
m_wndListBox.AddString( string );
m_wndListBox.InsertString( 3, string ); // Вставка 4-го элемента

Текущее количество элементов в списке возвращает функция CListBox::GetCount.


CListBox::DeleteString удаляет из списка элемент с заданным индексом.
Она возвращает количество элементов, оставшихся в списке. Очистить список полно-
стью может функция CListBox::ResetContent.
Бывают полезными функции CListBox::SetItemDataPtr и
CListBox::SetItemData, позволяющие сопоставить каждому элементу списка ука-
затель на значение типа DWORD. Этот указатель, связанный с заданным элементом
списка, можно получить функцией CListBox::GetItemDataPtr или
CListBox::GetItemData. Эта возможность полезна для связи элемента списка с не-
которыми дополнительными данными. Например, вы можете поместить в список
имена людей, и хранить в элементах списка указатели на структуры, содержащие ад-
реса и телефоны этих людей. Т.к. GetItemDataPtr возвращает указатель типа
void*, то потребуется явное преобразование указателя к нужному типу.

1.2.3 Поиск и извлечение элементов списка


В CListBox есть функции-члены для получения и изменения текущей позиции
выделения, для поиска и извлечения элементов списка. Перечень этих наиболее часто
используемых функций (применительно к списку с одиночным выделением) приве-
дены в табл. 7.2.
Таблица 7.2. Некоторые функции-члены CListBox для работы с элементами списка
Функция CListBox Назначение
GetCurSel Возвращает индекс (начиная с 0) текущего выделенного элемента или
LB_ERR, если ни один элемент не выделен.
SetCurSel Выделение элемента по индексу (или -1 для снятия выделения)
GetSel Проверка, выделен ли элемент с заданным индексом
SelectString Поиск и выделение элемента, начинающегося с заданной строки
FindString Определение индекса элемента, начинающегося с заданной строки
FindStringExact Определение индекса элемента, совпадающего с заданной строкой
GetText Получение строки элемента с заданным индексом
GetTextLen Определение длины строки элемента с заданным индексом

88
Например, чтобы найти с начала списка и выделить элемент, начинающийся со
слова Times (вроде "Times New Roman" или "Times Roman"), можно выполнить сле-
дующие вызовы:
m_wndListBox.SelectString( -1, "Times" );

Получить строку текущего выделенного элемента можно так:


CString string;
int nIndex = m_wndListBox.GetCurSel();
if ( nIndex != LB_ERR )
m_wndListBox.GetText( nIndex, string );

1.2.4 Уведомления, посылаемые списком


В MFC-приложениях уведомления от списка можно обрабатывать в функциях-
членах класса, зарегистрированных в карте сообщений с помощью макросов
ON_LBN_... (табл. 7.3).
Таблица 7.3. Уведомления списка
Код уведомления Когда посылается Макрос карты сооб- Необходим ли у
щений списка стиль
LBS_NOTIFY?
LBN_SETFOCUS Список получил фокус ввода ON_LBN_SETFOCUS Нет
LBN_KILLFOCUS Список потерял фокус ввода ON_LBN_KILLFOCUS Нет
LBN_ERRSPACE Операция отменена из-за нехват- ON_LBN_ERRSPACE Нет
ки памяти
LBN_DBLCLK Двойной щелчок на элементе ON_LBN_DBLCLK Да
LBN_SELCHANGE В списке изменено выделение ON_LBN_SELCHANGE Да
LBN_SELCANCEL Выделение снято ON_LBN_SELCANCEL Да
Из перечисленных уведомлений чаще всего приложения обрабатывают
LBN_DBLCLK и LBN_SELCHANGE. Ниже приведен пример обработчика LBN_DBLCLK, в
котором выполняется вывод выделенного элемента в информационном окне:
// Фрагмент карты сообщений CMainWindow
ON_LBN_DBLCLK( IDC_LISTBOX, OnItemDoubleClicked )
...
void CMainWindow::OnItemDoubleClicked()
{
CString string;
int nIndex = m_wndListBox.GetCurSel();
m_wndListBox.GetText( nIndex, string );
MessageBox( string );
}

1.3 Статические элементы: класс CStatic


Статические ЭУ бывают трех видов: текст, прямоугольные рамки и изображе-
ния. Текстовые статические ЭУ часто используются в качестве меток для других ЭУ.
Например, создать текстовую метку Имя можно так:
m_wndStatic.Create( "Имя", WS_CHILD ¦ WS_VISIBLE ¦ SS_LEFT,
rect, this, IDC_STATIC );

При использовании статических ЭУ для рисования прямоугольных рамок воз-


можны около 10-ти различных вариантов прямоугольников. Они определяются раз-

89
личными стилями SS_, например, прямоугольник с "выдавленными" границами имеет
стиль SS_ETCHEDFRAME (его можно использовать вместо группового блока):
m_wndStatic.Create( "", WS_CHILD ¦ WS_VISIBLE ¦ SS_ETCHEDFRAME,
rect, this, IDC_STATIC );

При выводе изображений (например, изображение дисковода в программе


ScanDisk) статическому ЭУ можно передать дескриптор растрового изображения,
указателя мыши, пиктограммы или метафайла GDI. Эти возможности ЭУ задаются
стилями: SS_BITMAP, SS_ICON или SS_ENHMETAFILE. Пример ЭУ для вывода пикто-
граммы:
m_wndStatic.Create( "", WS_CHILD ¦ WS_VISIBLE ¦ SS_ICON,
rect, this, IDC_STATIC );
m_wndStatic.SetIcon( hIcon );

По умолчанию статические ЭУ уведомлений не посылают, но если создать их


со стилем SS_NOTIFY, то возможны 4 типа уведомлений, приведенных в табл. 7.4.
Таблица 7.4. Уведомления статических элементов управления
Код уведомления Когда посылается Макрос карты сообще-
ний
STN_CLICKED Щелчок мышью на элементе ON_STN_CLICKED
STN_DBLCLK Двойной щелчок на элементе ON_STN_DBLCLK
STN_DISABLE ЭУ выключен (запрещен) ON_STN_DISABLE
STN_ENABLE ЭУ включен ON_STN_ENABLE

1.4 Элементы редактирования: класс CEdit


Элементы редактирования в MFC представлены классом CEdit. Можно выде-
лить два вида элементов редактирования: однострочные и многострочные. На рис. 7.2
показано диалоговое окно с двумя однострочными элементами редактирования (стро-
ками ввода). Многострочный элемент редактирования можно увидеть, запустив про-
грамму Блокнот из группы стандартных программ Windows. Клиентская область
Блокнота – это как раз один многострочный элемент редактирования.

Рис. 7.2. Диалоговое окно с двумя строками ввода.


Объем данных в этом ЭУ ограничен примерно 60 кб, что не является ограниче-
нием строк ввода, но существенно для многострочных элементов. Тогда можно вос-
пользоваться другим ЭУ – элементом редактирования сложного текста (он входит
в набор ЭУ Windows 95). Он применяется в программе WordPad.
В классе CEdit есть набор функций-членов для работы с элементами редакти-
рования. В MFC есть набор макросов для обработки уведомлений от этих ЭУ. Наибо-
лее часто используются строки ввода, и две функции для получения или задания
строки в этом ЭУ: SetWindowText и GetWindowText. Они унаследованы классом
CEdit от CWnd.

90
1.5 Комбинированные списки: класс CComboBox
Комбинированный список объединяет строку ввода и список в один ЭУ. Есть
три разновидности: "простой", "выпадающий" и "выпадающий список".
Простые комбинированные списки используются реже всего – они всегда изо-
бражаются в открытом состоянии. Когда пользователь выбирает в списке элемент, он
автоматически копируется в строку ввода. Пользователь может также напечатать
текст непосредственно в строке ввода. Если введенный пользователем элемент совпа-
дает с некоторым элементом списка, этот элемент автоматически выделяется.
Выпадающий комбинированный список отличается тем, что список выводится
на экран только по требованию пользователя. Остальные возможности ручного ввода
в строку списка остаются. Третий вид списка не позволяет вводить текст вручную,
можно только выбирать элементы из списка.
Функции-члены CComboBox похожи на функции-члены CEdit и CListBox.
Например, элементы добавляются в список функциями CComboBox::AddString и
CComboBox::InsertString. Максимальное количество символов для строки ввода
задается функцией CComboBox::LimitText. Функции GetWindowText и
SetWindowText, унаследованные от CWnd, считывают или устанавливают текст
строки редактирования. Есть и функции, уникальные для комбинированного списка,
например, ShowDropDown для скрытия или показа выпадающего списка, или
GetDroppedState для получения текущего состояния выпадающего списка – открыт
он или нет.

1.6 Полосы прокрутки: класс CScrollBar


Класс CScrollBar является оболочкой для полос прокрутки. Полосы прокрут-
ки аналогичны тем, которые добавляются в главное окно приложения при использо-
вании оконных стилей WS_VSCROLL и WS_HSCROLL. ЭУ "Полоса прокрутки" создает-
ся вызовом CScrollBar::Create. Ее можно поместить в любое место родительско-
го окна и сделать ее любого размера.
В отличие от других ЭУ, полоса прокрутки не посылает сообщений
WM_COMMAND, а посылает сообщения WM_VSCROLL и WM_HSCROLL. В MFC-
приложениях эти сообщения обрабатываются функциями-членами OnVScroll и
OnHScroll, как описывалось в предыдущих лекциях.
Класс CScrollBar содержит набор функций для работы с полосами прокрут-
ки, большинство из которых похожи на функции-члены CWnd для работы с полосами
прокрутки окна верхнего уровня. CScrollBar::GetScrollPos и
CScrollBar::SetScrollPos возвращают и устанавливают позицию ползунка по-
лосы прокрутки, GetScrollRange и SetScrollRange возвращает или задает диапа-
зон полосы. Все параметры полосы прокрутки можно установить одним вызовом
CScrollBar::SetScrollInfo.

2. Неочевидные аспекты программирования элементов управления


Одно из преимуществ MFC при программировании ЭУ – простота изменения
поведения ЭУ путем создания подкласса от одного из готовых классов ЭУ. Например,
легко создать строку ввода для ввода только чисел или список для отображения изо-
бражений вместо текста. Вы также можете разрабатывать повторно используемые,

91
самодостаточные классы ЭУ, которые сами обрабатывают свои сообщения с уведом-
лениями.

2.1 Строка ввода для приема числовых значений


MFC-классы для ЭУ полезны как объектно-ориентированный интерфейс для
доступа к ЭУ. Но они также дают возможность создания подклассов, модифицирую-
щих поведение стандартных ЭУ посредством добавления новых обработчиков сооб-
щений или перегрузки имеющихся обработчиков.
В качестве примера рассмотрим числовую строку ввода. Обычная строка ввода
позволяет вводить любые символы, а эта должна принимать только цифры (например,
для ввода телефонов, серийных номеров и т.п.).
Унаследуем класс от CEdit и модифицируем обработчик OnChar для приема
только цифровых символов:
class CNumEdit : public CEdit {
protected:
afx_msg void OnChar( UINT nChar, UINT nRepCnt, UINT nFlags );
DECLARE_MESSAGE_MAP()
};

BEGIN_MESSAGE_MAP( CNumEdit, CEdit )


ON_WM_CHAR()
END_MESSAGE_MAP()

void CNumEdit::OnChar( UINT nChar, UINT nRepCnt, UINT nFlags )


{
if ( (( nChar >= `0' ) && ( nChar <= `9' )) ¦¦ ( nChar == VK_BACK ) )
CEdit::OnChar( nChar, nRepCnt, nFlags );
}

Клавиша с кодом VK_BACK в CNumEdit тоже принимается, чтобы пользователь


мог удалять символы в строке клавишей Backspace. Проверять коды других клавиш
редактирования (например, Home и Del) не надо, т.к. они не генерируют сообщений
WM_CHAR.

2.3 Отражение сообщений


В MFC 4.0 появился набор макросов карты сообщений (табл. 7.5), предназна-
ченных для передачи сообщений с уведомлениями обратно в элементы управления,
пославшие эти сообщения – для "отражения сообщений". Отражение сообщений –
важный прием для разработки классов повторно используемых ЭУ, т.к. он позволяет
реализовать поведение ЭУ независимо от поведения окна-владельца.
С помощью макросов отражения сообщений можно сделать так, чтобы уведом-
ление от ЭУ обрабатывалось функцией-членом класса ЭУ.
Таблица 7.5. Макросы карты сообщений для отражения сообщений ЭУ
Макрос Какие сообщения отражаются
ON_CONTROL_REFLECT Отражение уведомлений, посылаемых в виде сообщений
WM_COMMAND
ON_NOTIFY_REFLECT Отражение уведомлений, посылаемых в виде сообщений
WM_NOTIFY
ON_UPDATE_COMMAND_UI_REFLECT Отражение уведомлений обновления панелей инструмен-
тов, строк состояния и других ЭУ
ON_WM_CTLCOLOR_REFLECT Отражение сообщений WM_CTLCOLOR

92
Макрос Какие сообщения отражаются
ON_WM_DRAWITEM_REFLECT WM_DRAWITEM от ЭУ с собственным отображением
ON_WM_MEASUREITEM_REFLECT WM_MEASUREITEM от ЭУ с собственным отображением
ON_WM_COMPAREITEM_REFLECT WM_COMPAREITEM от ЭУ с собственным отображением
ON_WM_DELETEITEM_REFLECT WM_DELETEITEM от ЭУ с собственным отображением
ON_WM_CHARTOITEM_REFLECT WM_CHARTOITEM от списков
ON_WM_VKEYTOITEM_REFLECT WM_VKEYTOITEM от списков
ON_WM_HSCROLL_REFLECT WM_HSCROLL от полос прокрутки
ON_WM_VSCROLL_REFLECT WM_VSCROLL от полос прокрутки
ON_WM_PARENTNOTIFY_REFLECT Отражение сообщений WM_PARENTNOTIFY

Предположим, что требуется разработать класс списка, самостоятельно обраба-


тывающий собственное уведомление LBN_DBLCLK для вывода информационного окна
с текстом элемента, на котором был двойной щелчок. Ниже приведено описание под-
класса CListBox с использованием отраженного сообщения:
class CMyListBox : public CListBox {
protected:
afx_msg void OnDoubleClick();
DECLARE_MESSAGE_MAP()
};

BEGIN_MESSAGE_MAP( CMyListBox, CListBox )


ON_CONTROL_REFLECT( LBN_DBLCLK, OnDoubleClick )
END_MESSAGE_MAP()

void CMyListBox::OnDoubleClick()
{
CString string;
int nIndex = GetCurSel();
GetText( nIndex, string );
MessageBox( string );
}

Макрос ON_CONTROL_REFLECT говорит MFC, что надо вызывать


CMyListBox::OnDoubleClick каждый раз, когда список посылает уведомление
LBN_DBLCLK своему родительскому окну. Важно отметить, что отражение работает
только тогда, когда родительское окно само не обрабатывает уведомление – т.е. в
карте сообщения родительского окна не должно быть записи ON_LBN_DBLCLK для
данного списка. Родительское окно при обработке уведомления имеет больший при-
оритет. Это объясняется тем, что Windows ожидает выполнения обработки уведомле-
ния от ЭУ в его родительском окне, а не в самом ЭУ.

3. Упражнения
8) Изучите англо-русский словарь терминов по теме 7-й лекции (см. CD-ROM).
9) Выполните лабораторную работу №4, "Использование стандартных элементов
управления" (см. CD-ROM).

93
Лекция 8. Диалоговые окна
В большинстве приложений элементы управления используются не в окнах
верхнего уровня, а в диалоговых окнах. Диалоговое окно (dialog box), или, диалог –
это окно, обычно появляющееся на короткий промежуток времени для получения
данных от пользователя. Диалоговые окна создавать гораздо проще, чем окна верхне-
го уровня, т.к. шаблон диалогового окна с расположением всех его ЭУ можно разра-
ботать в интерактивном режиме, поместить в RC-файл, а затем во время выполнения
программы создать диалоговое окно на основе шаблона.
Есть две основных разновидности диалоговых окон: модальные и немодаль-
ные. Модальные окна запрещают во время работы свое окно-владельца. Немодальное
окно больше похоже на обычное окно верхнего уровня (например, окно контекстного
поиска и замены в MS Word). Во время работы такого окна пользователь может рабо-
тать и с его окном-владельцем. Чаще в приложениях используются модельные окна,
поэтому далее будут подробно рассматриваться именно они.
В MFC диалоговые окна представлены классом CDialog. Также в MFC есть
удобные классы-оболочки для работы со стандартными диалоговыми окнами Win-
dows (для открытия/сохранения файлов, диалоговые окна печати и др.).
Особым видом диалоговых окон являются окна свойств (окна с закладками,
property sheet). Это окно выглядит как окно с закладками, каждая из которых является
отдельным диалоговым окном. Окна свойств позволяют компактно представить
большое количество элементов управления. Они хорошо подходят для разработки
объектно-ориентированного пользовательского интерфейса, в котором интенсивно
используются контекстные меню. В MFC для работы с окнами свойств есть классы
CPropertySheet и CPropertyPage.

1. Модальные диалоговые окна и класс CDialog


В процессе создания модального диалогового окна выделяются три этапа:
1) Разработка шаблона диалогового окна, описывающего внешний вид окна и его
элементов управления.
2) Создание объекта класса или подкласса CDialog, являющегося оболочкой для
шаблона диалогового окна.
3) Вызов функции-члена CDialog::DoModal для вывода окна на экран.
Для простых диалоговых окон можно непосредственно пользоваться классом
CDialog. Однако более часто наследуется подкласс CDialog, в котором реализуется
поведение конкретного окна. Сначала рассмотрим отдельные компоненты диалогово-
го окна, а затем создание подклассов CDialog.

1.1 Шаблон диалогового окна


Первый шаг в создании диалогового окна – разработка шаблона. В нем описы-
ваются основные характеристики окна, начиная от его размеров, до свойств элемен-
тов управления. Хотя можно создавать шаблон окна программно, обычно они хранят-
ся в виде ресурсов приложения, скомпилированных по содержимому RC-файла.
Ниже в качестве примера приведен шаблон диалогового окна с идентификато-
ром ресурса IDD_MYDIALOG. В этом окне есть 4 ЭУ: строка ввода, статический эле-
мент-метка строки ввода, кнопка OK и кнопка Отмена:
94
IDD_MYDIALOG DIALOG 0, 0, 160, 68
STYLE DS_MODALFRAME ¦ WS_POPUP ¦ WS_VISIBLE ¦ WS_CAPTION ¦ WS_SYSMENU
CAPTION "Введите свое имя"
FONT 8, "MS Sans Serif"
BEGIN
LTEXT "&Имя", -1, 8, 14, 24, 8
EDITTEXT IDC_NAME, 34, 12, 118, 12, ES_AUTOHSCROLL
DEFPUSHBUTTON "OK", IDOK, 60, 34, 40, 14, WS_GROUP
PUSHBUTTON "Отмена", IDCANCEL, 112, 34, 40, 14, WS_GROUP
END

Все координаты и размеры задаются в единицах диалогового окна (dialog box


units). Горизонтальная единица равна четверти средней ширины символа шрифта
диалога. Вертикальная единица равна одной восьмой высоты символа. Т.к. высота
символов с среднем в 2 раза больше ширины, то эти единицы примерно равны. Ис-
пользование таких единиц позволяет описать окно независимо от экранного разреше-
ния.
В шаблоне окна можно вместо служебных слов вроде LTEXT или EDITTEXT
пользоваться словом CONTROL и указывать имя оконного класса элемента управления
явно, например:
IDD_MYDIALOG DIALOG 0, 0, 160, 68
STYLE DS_MODALFRAME ¦ WS_POPUP ¦ WS_VISIBLE ¦ WS_CAPTION ¦ WS_SYSMENU
CAPTION "Введите свое имя"
BEGIN
CONTROL "&Имя", -1, "STATIC", SS_LEFT, 8, 14, 24, 8
CONTROL "", IDC_NAME, "EDIT", WS_BORDER ¦ ES_AUTOHSCROLL ¦
ES_LEFT ¦ WS_TABSTOP, 34, 12, 118, 12
CONTROL "OK", IDOK, "BUTTON", BS_DEFPUSHBUTTON ¦
WS_TABSTOP ¦ WS_GROUP, 60, 34, 40, 14
CONTROL "Отмена", IDCANCEL, "BUTTON", BS_PUSHBUTTON ¦
WS_TABSTOP ¦ WS_GROUP, 112, 34, 40, 14
END

Вручную такие описания диалоговых окон делаются очень редко. В среде Vis-
ual C++ команда меню Insert⇒Resource позволяет добавить в проект пустой шаблон
диалогового окна и затем отредактировать его в редакторе ресурсов. На рис. 8.1 пока-
зано окно редактора диалоговых окон, встроенного в Visual C++. Элементы управле-
ния можно выбирать в панели инструментов Controls и "рисовать" их в диалоговом
окне (если панель инструментов Controls отсутствует на экране, ее можно включить
командой меню Tools⇒Customize⇒Toolbars). Свойства диалогового окна (заголовок,
шрифт, стиль) и свойства ЭУ доступны в окнах свойств, вызываемых командой Prop-
erties из контекстных меню.

95
Рис. 8.1. Редактор диалоговых окон Visual C++.

1.1.1 Клавиатурный интерфейс диалогового окна


В Windows каждому диалоговому окну обеспечивается клавиатурный интер-
фейс, позволяющий пользователю перемещать фокус ввода по очереди между всеми
ЭУ клавишей Tab, циклически перемещаться внутри группы ЭУ клавишами курсора,
выбирать ЭУ нажатием его клавиши быстрого выбора (подчеркнутого символа в тек-
сте метки). На клавиатурный интерфейс влияют следующие компоненты шаблона
диалогового окна:
• порядок создания элементов управления;
• использование символов амперсанда (&) в тексте меток для обозначения кла-
виш быстрого выбора;
• использование стиля WS_GROUP для объединения ЭУ в группы;
• использование стиля DEFPUSHBUTTON для обозначения нажимаемой кнопки,
выбираемой по умолчанию (по нажатию Enter).
Порядок создания ЭУ задает очередность передачи меду ними фокуса ввода
клавишей Tab или Shift-Tab (tab order). В большинстве редакторов диалогов этот по-
рядок можно задать визуально.
Важным вопросом клавиатурного интерфейса, особенно для кнопок с зависи-
мой фиксацией, является объединение ЭУ в группы. Кнопки со стилем
BS_AUTORADIOBUTTON должны быть сгруппированы, чтобы Windows могла автома-
тически перемещать отметку между кнопками этой группы. Чтобы объединить кноп-
ки (или другие ЭУ) в группу, надо сначала расположить их последовательно в поряд-
ке обхода клавишей Tab, затем первой из этих кнопок в окне свойств присвоить стиль
WS_GROUP, а также присвоить этот стиль первому ЭУ, которой располагается после
создаваемой группы.
Все компоненты клавиатурного интерфейса можно задать в редакторе диалогов
Visual C++. Порядок обхода клавишей Tab задается после выбора команды Lay-
out⇒Tab Order, последовательными щелчками мыши на всех ЭУ. В диалоговом ре-
дакторе порядок обхода показывается в виде прямоугольников с порядковыми числа-
ми (рис. 8.2).

96
Рис. 8.2. Редактор диалогов Visual C++ в режиме задания порядка обхода ЭУ по клавише Tab.

1.2 Класс CDialog


Для всех, за исключением самых примитивных, диалоговых окон, кроме разра-
ботки шаблона требуется описать класс-оболочку как подкласс CDialog. В этом под-
классе надо реализовать поведение конкретного окна. В целом, у подклассов
CDialog часто перегружаются три виртуальные функции: инициализация элементов
управления (OnInitDialog) и обработчики кнопок OK (OnOK) и Отмена
(OnCancel). Хотя каждая из этих функций соответствует определенному сообщению
диалогового окна, для их обработки не требуется карта сообщений – она скрыта внут-
ри CDialog и обработчики реализованы в виде обычных виртуальных функций. У
CDialog есть версии этих функций "по умолчанию", поэтому иногда можно обойтись
без их перегрузки, а для обмена данными с ЭУ пользоваться механизмом обмена дан-
ными диалоговых окон MFC (Dialog Data Exchange and Dialog Data Validation).
При создании диалогового окна оно получает сообщение WM_CREATE, как и
любое другое окно. Но в момент получения WM_CREATE в диалоговом окне еще не
созданы ЭУ, описанные в шаблоне окна. Поэтому их нельзя проинициализировать в
обработчике данного сообщения. Внутренняя оконная процедура диалоговых окон
Windows обрабатывает сообщение WM_CREATE, как раз чтобы выполнить создание
ЭУ. После того. как все они созданы, диалоговому окну посылается сообщение
WM_INITDIALOG, чтобы окно смогло произвести необходимую инициализацию ЭУ. В
подклассах CDialog сообщение WM_INITDIALOG приводит к вызову функции-члена
OnInitDialog:
virtual BOOL OnInitDialog()

В OnInitDialog можно выполнить все действия, необходимые для подготов-


ки окна к работе – например, пометить кнопку с зависимой фиксацией или поместить
текст в строку ввода В момент вызова OnInitDialog диалоговое окно на экран еще
не выведено. Возвращаемое значение OnInitDialog указывает Windows, что делать
с фокусом ввода. Если OnInitDialog возвращает TRUE, то Windows устанавливает
фокус ввода на первый по порядку обхода ЭУ окна. Если требуется установить фокус
на другой элемент, надо сделать это в OnInitDialog вызовом SetFocus у класса ЭУ

97
и вернуть из OnInitDialog значение FALSE. Получить указатель на CWnd для ЭУ с
известным идентификатором можно функцией GetDlgItem, например:
GetDlgItem( IDC_EDIT )->SetFocus();

При перегрузке OnInitDialog надо обязательно вызывать OnInitDialog ба-


зового класса.
Чтобы по нажатию кнопок OK и Отмена вызывались виртуальные функции
CDialog::OnOK и OnCancel, у этих кнопок в шаблоне окна должны быть идентифи-
каторы IDOK и IDCANCEL.
Функцию-член OnOK можно перегрузить для выполнения специализированной
обработки перед закрытием окна, например, для извлечения данных из ЭУ и, возмож-
но, для проверки корректности этих данных (например, что число попадает в допус-
тимый диапазон). В собственной реализации OnOK обязательно надо закрыть диало-
говое окно вызовом EndDialog или вызывать для этого OnOK из базового класса. Ес-
ли этого не сделать, при нажатии кнопки OK окно не будет закрываться.
Обработчик OnCancel вызывается не только при нажатии кнопки с идентифи-
катором IDCANCEL, но и по нажатию клавиши Esc или при закрытии окна кнопкой в
строке заголовка. OnCancel перегружается редко, т.к. по нажатию кнопки Отмена
обычно не требуется считывать данные из ЭУ. По умолчанию CDialog::OnCancel
вызывает EndDialog с параметром IDCANCEL, чтобы закрыть диалоговое окно и
проинформировать вызывающую функцию, что изменения в диалоговом окне долж-
ны быть проигнорированы.
За исключением специфического сообщения WM_INITDIALOG, диалоговые ок-
на получают те же самые сообщения, что и все остальные окна. Вы можете добавить
записи в карту сообщений диалогового окна для обработки любых необходимых со-
общений и уведомлений. Допустим, в диалоговом окне есть кнопка Сброс с иденти-
фикатором IDC_RESET. Чтобы при нажатии этой кнопки вызывался обработчик
OnReset, надо добавить в карту сообщений запись:
ON_BN_CLICKED( IDC_RESET, OnReset )

В диалоговых окнах можно обрабатывать даже сообщения WM_PAINT (напри-


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

1.2.1 Применение ClassWizard для создания подкласса CDialog


Вполне возможно добавить в проект подкласс CDialog вручную, но эта ти-
пичная операция выполняется гораздо быстрее с помощью ClassWizard. Сначала на-
до вызвать его командой меню View⇒ClassWizard, нажать кнопку Add Class,
выбрать из появившегося меню вариант New и заполнить окно с параметрами нового
класса. В нем надо указать имя класса, имя базового класса (CDialog), и идентифика-
тор ресурса для шаблона окна (рис. 8.3).

98
Рис. 8.3. Использование ClassWizard'а для создания подкласса CDialog.

ClassWizard'ом удобно пользоваться для добавления обработчиков уведомле-


ний от элементов управления окна. Допустим, вы хотите написать обработчик
BN_CLICKED для нажимаемой кнопки с идентификатором IDC_RESET. Вот что для
этого нужно сделать:
1) В окне проекта на закладке ClassView щелкнуть правой кнопкой на имени класса
диалогового окна.
2) Выбрать из контекстного меню команду Add Windows Message Handler.
3) В появившемся окне в списке Class Or Object To Handle выделить идентифика-
тор кнопки (IDC_RESET).
4) В списке New Windows Messages/Events выделить уведомление BN_CLICKED.
5) Нажать кнопку Add Handler и ввести имя функции-обработчика.
После выполнения этих действий, в класс диалогового окна будет добавлена
новая функция-член с указанным вами именем, и для нее в карту сообщений класса
будет внесена запись ON_BN_CLICKED.

1.3 Создание модального диалогового окна


После того, как вы разработали шаблон диалогового окна и объявили подкласс
CDialog, для создания и вывода модального диалогового окна осталось немного:
создать объект вашего подкласса CDialog и вызвать у него функцию-член DoModal.
DoModal вернет управление только после закрытия диалогового окна. В качестве воз-
вращаемого значение будет передан параметр функции EndDialog. Приложения
обычно проверяют значение, возвращенное DoModal, чтобы выполнить некоторые
действия только при возврате значения IDOK. При другом значении (обычно
IDCANCEL), информация, введенная в диалогом окне, игнорируется.
Конструктору CDialog в качестве параметров передается идентификатор ре-
сурса шаблона и указатель на окно-владельца диалогового окна. Если указатель
CWnd* не задавать, владельцем станет главное окно приложения. Обычно в подклас-
сах MFC заводится конструктор, который можно использовать вообще без парамет-
ров, например:
CMyDialog::CMyDialog( CWnd* pOwnerWnd = NULL ) :
CDialog( IDD_MYDIALOG, pOwnerWnd ) {}

99
Такой конструктор позволяет создать и вызвать диалоговое окно всего двумя
операторами:
CMyDialog dlg;
if ( dlg.DoModal() == IDOK )
{
// Пользователь нажал кнопку OK
}

1.4 Механизм обмена данными с элементами управления (DDX/DDV)


Типичное диалоговое окно предоставляет пользователю для выбора некоторый
набор параметров, собирает введенные данные и делает их доступными приложению,
создавшему окно. Удобный способ хранения введенных данных – открытые перемен-
ные-члены класса диалогового окна. Приложение, пользующееся окном, может изме-
нять эти переменные-члены для инициализации окна или для получения данных по-
сле его закрытия.
Допустим, есть диалоговое окно с двумя строками ввода, в которых пользова-
тель может ввести имя и номер телефона. Заведем для этих характеристик две откры-
тых переменных-члена в классе диалогового окна:
class CMyDialog : public CDialog {
public:
CMyDialog( CWnd* pParentWnd = NULL ) :
CDialog( IDD_MYDIALOG, pParentWnd ) {}
CString m_strName;
CString m_strPhone;
};

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


OK, надо получить введенные параметры, например, так:
CMyDialog dlg;
if ( dlg.DoModal() == IDOK )
{
CString strName = dlg.m_strName;
CString strPhone = dlg.m_strPhone;
TRACE( "Имя=%s, телефон=%s", strName, strPhone );
}

Этот текст можно немного изменить, чтобы окно выводилось на экран проини-
циализированным некоторыми значениями "по умолчанию":
CMyDialog dlg;
dlg.m_strName = "Иванов";
dlg.m_strPhone = "111-1111";
if ( dlg.DoModal() == IDOK )
{
CString strName = dlg.m_strName;
CString strPhone = dlg.m_strPhone;
TRACE( "Имя=%s, телефон=%s", strName, strPhone );
}

В этом примере предполагается, что m_strName и m_strPhone каким-то обра-


зом связаны с ЭУ окна – так, что присвоение значений переменным как-то приводит к
вставке соответствующего текста в строки ввода или что чтение значений перемен-
ных аналогично считыванию текста из строк ввода.
Такая связь переменных-членов и ЭУ не обеспечивается автоматически: ее
должен создать программист. Во-первых, можно перегрузить OnInitDialog и OnOK
100
и включить в них операторы для передачи данных между переменными-членами
класса и ЭУ. Допустим, строкам ввода были присвоены идентификаторы IDC_NAME и
IDC_PHONE. Тогда класс CMyDialog может быть реализован так:
class CMyDialog : public CDialog {
public:
CMyDialog::CMyDialog( CWnd* pParentWnd = NULL ) :
CDialog( IDD_MYDIALOG, pParentWnd ) {}
CString m_strName;
CString m_strPhone;
protected:
virtual BOOL OnInitDialog();
virtual void OnOK();
};

BOOL CMyDialog::OnInitDialog()
{
CDialog::OnInitDialog();
SetDlgItemText( IDC_NAME, m_strName );
SetDlgItemText( IDC_PHONE, m_strPhone );
return TRUE;
}

void CMyDialog::OnOK()
{
GetDlgItemText( IDC_NAME, m_strName );
GetDlgItemText( IDC_PHONE, m_strPhone );
CDialog::OnOK();
}

Представьте, насколько простой бы стала разработка классов диалоговых окон,


если бы не пришлось заботиться об инициализации ЭУ в OnInitDialog и считыва-
нии данных из них в OnOK – например, если бы для сопоставления ЭУ и переменных
членов можно было пользоваться некоторой "картой данных". Именно в этом в MFC
состоит назначение механизма диалогового информационного обмена (Dialog Data
Exchange, DDX). DDX прост в использовании, и во многих случаях он позволяет не
перегружать функций OnInitDialog и OnOK, даже если в диалоговом окне несколь-
ко десятков ЭУ.
Для активизации DDX требуется перегрузить виртуальную функцию
CDialog::DoDataExchange. В нее надо внести специальные функции DDX, предна-
значенные для обмена данными между ЭУ и переменными-членами различных типов
данных. Например, в нашем классе CMyDialog функция-член DoDataExchange мо-
жет быть реализована так:
void CMyDialog::DoDataExchange( CDataExchange* pDX )
{
DDX_Text( pDX, IDC_NAME, m_strName );
DDX_Text( pDX, IDC_PHONE, m_strPhone );
}

MFC вызывает DoDataExchange один раз при создании диалогового окна (из
обработчика сообщения WM_INITDIALOG) и еще раз при нажатии кнопки OK. Пара-
метр pDX указывает на объект класса CDataExchange, который, например, задает
функциям наподобие DDX_Text направление копирования данных – из ЭУ в пере-
менные-члены или наоборот. Т.о., одного вызова DoDataExchange достаточно для

101
передачи данных во все ЭУ, данные от которых требуется получать в данном окне.
Список функций передачи данных DDX приведен в табл. 8.1.
Таблица 8.1. Функции диалогового информационного обмена (DDX)
Функция DDX Описание связи ЭУ и переменных-членов
DDX_Text Элемент редактирование и переменная-член одного из следующих ти-
пов: BYTE, int, short, UINT, long, DWORD, CString, string,
float, double, COleDateTime, COleCurrency
DDX_Check Кнопка с независимой фиксацией и переменная int
DDX_Radio Группа кнопок с независимой фиксацией и переменная int
DDX_LBIndex Список и переменная int
DDX_LBString Список и переменная CString
DDX_LBStringExact Список и переменная CString
DDX_CBIndex Комбинированный список и переменная int
DDX_CBString Комбинированный список и переменная CString
DDX_CBStringExact Комбинированный список и переменная CString
DDX_Scroll Полоса прокрутки и переменная int

Кроме DDX, в MFC есть родственный механизм DDV – диалоговый информа-


ционный обмен с проверкой данных (Dialog Data Validation, DDV). Он позволяет перед
закрытием диалогового окна проверить данные ЭУ на корректность. Функции DDV
делятся на две группы: проверка численных значений на попадание в диапазон и про-
верка переменных типа CString для того, чтобы строки были заданной длины. Ниже
приведен пример функции DoDataExchange, в которой DDX_Text применяется для
связи строки ввода и целочисленной переменной, а DDV_MinMaxInt – для проверки
попадания значения этой переменной в диапазон от 0 до 100 (проверка выполняется
только при нажатии кнопки OK):
void CMyDialog::DoDataExchange( CDataExchange* pDX )
{
DDX_Text( pDX, IDC_COUNT, m_nCount );
DDV_MinMaxInt( pDX, m_nCount, 0, 100 );
}

Если значение m_nCount будет меньше 0 или больше 100, то DDV_MinMaxInt


установит фокус ввода строку ввода и выведет сообщение об ошибке. Для данного
ЭУ вызов функции DDV должен следовать непосредственно после вызова функции
DDX, чтобы MFC смогла правильно установить фокус ввода при обнаружении ошиб-
ки.
Сами средства DDX/ DDV реализованы внутри CDialog. Когда диалоговое ок-
но создается, CDialog::OnInitDialog вызывает функцию CWnd::UpdateData с
параметром FALSE. В свою очередь, UpdateData создает объект CDataExchange и
передает указатель на него функции диалогового окна DoDataExchange. Внутри
DoDataExchange вызываются функции DDX для инициализации ЭУ значениями из
переменных-членов. Позднее, когда пользователь нажимает OK, из CDialog::OnOK
вызывается UpdateData с параметром TRUE, что приводит к копированию функция-
ми DDX данных из ЭУ в переменные-члены. Если в DoDataExchange есть вызовы
функций DDV, то именно на этом этапе они проверяют корректность введенных
пользователем данных. Описанный порядок работы DDX/DDV объясняет, почему из
перегруженных функций OnOK и OnInitDialog обязательно надо вызывать эти
функции из базового класса – иначе не будет вызвана UpdateData и DDX/DDV не
будет работать.

102
1.4.1 Поддержка механизма DDX/DDV в ClassWizard'е
Если при разработке приложения вы пользуетесь мастерами MFC, то вы може-
те не добавлять в DoDataExchange вызовы функций DDX/DDV вручную, а делать
это с помощью ClassWizard. ClassWizard может даже автоматически добавлять в
описание класса диалогового окна все необходимые для DDX/DDV переменные-
члены. Ниже описан порядок добавления в класс диалогового окна переменной-члена
и ее связи с ЭУ через механизм DDX/ DDV:
1) Вызовите ClassWizard (командой меню View) и перейдите на закладку
Member Variables (рис. 8.4).

Рис. 8.4. Окно ClassWizard'а: закладка с переменными- Рис. 8.5. Диалоговое окно Class-
членами класса диалогового окна. Wizard для добавления новой пере-
менной-члена в диалоговый класс.

2) В списке Class Name выберите имя класса диалогового окна.


3) В списке Control ID выделите идентификатор ЭУ, который вы хотите свя-
зать с переменной членом, и нажмите кнопку Add Variable.
4) В диалоговом окне Add Member Variable (рис. 8.5) введите имя новой пе-
ременной-члена и выберите ее тип в списке Variable Type. Затем нажмите
кнопку OK.
Если вы посмотрите на исходный текст класса диалогового окна после закры-
тия окна ClassWizard, то увидите, что ClassWizard добавил в класс переменную-
член, а в функцию DoDataExchange поместил вызов функции DDX для связи пере-
менной-члена с ЭУ. Если переменная числовая (как на рис. 8.4), то в нижней части
закладки Member Variables окна ClassWizard вы можете указать минимальное и
максимальное значение диапазона, попадание в который будет контролироваться с
помощью функции DDV_MinMax.

1.5 Взаимодействие с элементами управления диалогового окна


Несмотря на удобство средств DDX/DDV, полностью отказаться от обращения
к ЭУ с помощью классов-оболочек ЭУ удается далеко не всегда. Например, может
потребоваться вызывать функции CListBox для заполнения списка строками из

103
OnInitDialog. Для этого необходим указатель на объект CListBox, представляю-
щий список – ЭУ окна. Как его получить?
Указатель типа (CWnd*) для любого ЭУ окна можно получить функцией
CWnd::GetDlgItem. Например, для включения запрещенной кнопки с независимой
фиксацией IDC_CHECK, можно записать следующие вызовы:
CWnd* pWnd = GetDlgItem( IDC_CHECK );
pWnd->EnableWindow( TRUE );

Этот исходный текст правилен, т.к. GetDlgItem возвращает указатель на


CWnd, а функция EnableWindow является членом класса CWnd. Теперь рассмотрим
следующий фрагмент текста:
CListBox* pListBox = (CListBox*)GetDlgItem( IDC_LIST );
pListBox->AddString( "Первый элемент" );
pListBox->AddString( "Второй элемент" );
pListBox->AddString( "Третий элемент" );

Здесь для вызова функций-членов CListBox потребовалось выполнить преоб-


разование типа для указателя, полученного от GetDlgItem. Явное преобразование
типа указателей – плохой стиль программирования, и если ЭУ с идентификатором
IDC_LIST в окне не окажется, может произойти серьезная ошибка доступа по нуле-
вому указателю.
Лучшим решением является присоединение оконного дескриптора ЭУ к клас-
су-оболочке ЭУ с помощью CWnd::Attach. Сначала надо создать объект класса ЭУ
(например, CListBox), а затем динамически присоединить к нему ЭУ, например:
CListBox wndListBox;
wndListBox.Attach( GetDlgItem( IDC_LIST )->m_hWnd );
wndListBox.AddString( "Первый элемент" );
wndListBox.AddString( "Второй элемент" );
wndListBox.AddString( "Третий элемент" );
wndListBox.Detach();

Т.к. объект CListBox был создан в стеке, очень важно вызывать Detach до то-
го, как объект CListBox будет уничтожен. Иначе деструктор CListBox удалит ЭУ и
список исчезнет из диалогового окна.
Чтобы не писать вызовы функций присоединения/отсоединения ЭУ от объекта-
оболочки, можно сопоставить их с помощью функции DDX_Control. Например, что-
бы связать переменную-член m_wndListBox класса CListBox со списком с иденти-
фикатором IDC_LIST, в DoDataExchange надо внести вызов:
DDX_Control( pDX, IDC_LIST, m_wndListBox );

Теперь добавлять строки в список можно, просто вызывая AddString у


m_wndListBox:
m_wndListBox.AddString( "Первый элемент" );
m_wndListBox.AddString( "Второй элемент" );
m_wndListBox.AddString( "Третий элемент" );

Вызовы DDX_Control в функцию DoDataExchange удобно добавлять с по-


мощью ClassWizard. Для этого надо перейти на закладку Member Variables и с по-
мощью кнопки Add Variable добавить в класс переменную-член. Но в окне Add
Member Variable (рис. 8.5) в списке Category надо вместо Value выбрать Control.
Затем в списке Variable Type выберите класс ЭУ и нажмите кнопку OK. Если после
выхода из ClassWizard вы посмотрите на исходный текст диалогового класса, то

104
увидите, что ClassWizard добавил в класс переменную-член, а также вызов функции
DDX_Control для связи этой переменной с элементом управления.

2. Окна свойств
Окна свойств (диалоговые окна с закладками) часто используются в Windows
для компактного представления большого количества ЭУ. Эти диалоговые окна реа-
лизованы в библиотеке стандартных ЭУ Windows 95. На уровне API для реализации
окон свойств требуется довольно много усилий, которые существенно упрощаются
при использовании MFC. Оказывается, что добавить окно свойств почти также легко,
как и обычное диалоговое окно.
Окна свойств в MFC представлены двумя классами: CPropertySheet и
CPropertyPage. CPropertySheet (унаследован от CWnd) представляет собственно
окно свойств, а CPropertyPage (унаследован от CDialog) – отдельную страницу
свойств. Оба класса описаны в файле Afxdlgs.h. Как и диалоговые окна, окна
свойств могут быть модальными и немодальными. Модальное окно создается вызо-
вом CPropertySheet::DoModal, а немодальное –CPropertySheet::Create.
Общий порядок создания модального окна свойств следующий:
1) Для каждой страницы свойств надо создать отдельный шаблон диалогового окна,
задающий содержимое и свойства страницы. Указанный в шаблоне заголовок окна
будет выводиться как название закладки в окне свойств.
2) Для каждой страницы свойств надо создать подкласс CPropertyPage, в котором,
аналогично обычному диалоговому окну, можно завести открытые переменные-
члены и связать их с ЭУ страницы с помощью DDX/DDV.
3) Для представления окна свойств создается подкласс CPropertySheet. Для рабо-
ты окна должны быть созданы объекты этого класса, а также объекты всех клас-
сов-страниц, определенных на шаге 2). Для добавления страниц в окно свойств
следует пользоваться функцией CPropertySheet::AddPage.
4) Окно свойств выводится на экран вызовом его функции-члена DoModal.
Для упрощения создания окон свойств большинство программистов объявляют
объекты страниц свойств как переменные-члены класса окна свойств. Эти объекты
привязываются к окну свойств из конструктора вызовами AddPage для каждой стра-
ницы свойств. Пример подобной реализации окна свойств приведен ниже:
class CFirstPage : public CPropertyPage {
public:
CFirstPage() : CPropertyPage( IDD_FIRSTPAGE ) {;}
// Объявление переменных-членов CFirstPage
protected:
virtual void DoDataExchange( CDataExchange* );
};

class CSecondPage : public CPropertyPage {


public:
CSecondPage() : CPropertyPage( IDD_SECONDPAGE ) {;}
// Объявление переменных-членов CSecondPage
protected:
virtual void DoDataExchange( CDataExchange* );
};

class CMyPropertySheet : public CPropertySheet {

105
public:
CFirstPage m_firstPage; // Первая страница
CSecondPage m_secondPage; // Вторая страница
// Страницы добавляются в окно из конструктора
CMyPropertySheet( LPCTSTR pszCaption, CWnd* pParentWnd = NULL ) :
CPropertySheet( pszCaption, pParentWnd, 0 )
{
AddPage( &m_firstPage );
AddPage( &m_secondPage );
}
};

В данном примере страницы свойств представлены классами CFirstPage и


CSecondPage. С ними связаны ресурсы шаблонов диалоговых окон с идентификато-
рами IDD_FIRSTPAGE и IDD_SECONDPAGE. При такой структуре классов окна
свойств, вызвать его из приложения можно всего двумя операторами:
CMyPropertySheet ps( "Свойства" );
ps.DoModal();

Как и CDialog::DoModal, функция CPropertySheet::DoModal возвращает


IDOK или IDCANCEL.
В шаблонах диалоговых окон для страниц свойств не должно быть кнопок OK
и Отмена, т.к. MFC добавляет их в окно свойств автоматически. Также в него добав-
ляется кнопка Применить (Apply) и, возможно, Помощь (Help). При выводе окна
свойств на экран кнопка Применить сначала запрещена, а включается она после то-
го, как какая-либо страница свойств вызовет функцию-член
CPropertyPage::SetModified с параметром TRUE. SetModified надо вызывать
при каждом изменении параметров страницы свойств – например, при изменении
текста в строке ввода или при переключении кнопки с зависимой фиксацией. Для об-
работки нажатия кнопки Применить надо добавить в класс страницы свойств обра-
ботчик ON_BN_CLICKED для кнопки с идентификатором ID_APPLY_NOW. Этот обра-
ботчик должен вызвать UpdateData( TRUE ) для обновления значений перемен-
ных-членов страницы свойств и, возможно, передать эти значения окну-владельцу
окна свойств. После этого, считая, что изменения учтены, надо запретить кнопку
Применить вызовом SetModified( FALSE ) – по одному разу для каждой страни-
цы свойств.

3. Стандартные диалоговые окна Windows


Ряд диалоговых окон, предназначенных для выполнения часто встречающихся
в различных приложениях команд, был реализован как часть операционной системы.
В MFC есть классы-оболочки для этих окон, они перечислены в табл. 8.2.
Таблица 8.2. Классы стандартных диалоговых окон
Имя класса Представляемые диалоговые окна
CFileDialog Открыть и Сохранить как
CPrintDialog Печать
CPageSetupDialog Макет страницы
CFindReplaceDialog Поиск и Замена (текста)
CColorDialog Выбор цвета
CFontDialog Выбор шрифта
При программировании на уровне API для вызова стандартного окна требуется
заполнить некоторую структуру и передать ее функции API, например,
106
::GetOpenFileName. После возврата из этой функции введенные пользователем па-
раметры будут сохранены в передававшейся функции структуре. MFC упрощает ин-
терфейс стандартных окон, заполняя большинство полей структуры "по умолчанию"
и автоматически извлекая из нее данных после закрытия диалогового окна. Например,
получить имя открываемого файла в MFC-приложении можно так:
TCHAR szFilters[] = "Текстовые файлы (*.txt)¦*.txt¦Все файлы (*.*)¦*.*¦¦";
CFileDialog dlg( TRUE, "txt", "*.txt",
OFN_FILEMUSTEXIST ¦ OFN_HIDEREADONLY, szFilters );
if ( dlg.DoModal() == IDOK )
{
filename = dlg.GetPathName();
// Открытие файла и чтение данных из него
...
}

У конструктора CFileDialog параметр TRUE обозначает, что надо выводить


окно Открыть, а не Сохранить как. Параметры "txt" и "*.txt" задают расширение
файла "по умолчанию" – то расширение, которое будет добавлено к имени файла, ес-
ли пользователь не укажет его явно. Значения OFN_... – это битовые флаги, задаю-
щие свойства диалогового окна. OFN_FILEMUSTEXIST задает, что требуется прове-
рять наличие файла с введенным пользователем именем и не принимать имена несу-
ществующих файлов. OFN_HIDEREADONLY скрывает флажок Только чтение, который
по умолчанию появляется в диалоговом окне. Строка szFilters задает типы файлов,
которые может выбирать пользователь. После возврата из DoModal, полный путь к
выбранному файлу можно получить функцией CFileDialog::GetPathName. Функ-
ция CFileDialog::GetFileName возвращает часть имени файла без пути, а
GetFileTitle – часть имени файла без пути и без расширения.
Изменить поведение класса CFileDialog или других стандартных диалоговых
классов можно несколькими способами. Во-первых, можно задать параметры окна в
конструкторе класса (битовых флагов OFN_... больше 10-ти, все они влияют на
внешний вид и поведение окна). Например, чтобы вызвать окно Открыть для полу-
чения имени только одного существующего файла, конструктор вызывается так:
CFileDialog dlg( TRUE, "txt", "*.txt",
OFN_FILEMUSTEXIST ¦ OFN_HIDEREADONLY, szFilters );

Чтобы пользователь мог выделить несколько файлов, добавляется всего один флаг:
CFileDialog dlg( TRUE, "txt", "*.txt",
OFN_FILEMUSTEXIST ¦ OFN_HIDEREADONLY ¦ OFN_ALLOWMULTISELECT,
szFilters );
После возврата из DoModal список имен выбранных файлов хранится в буфере,
на который указывает поле структуры m_ofn.lpstrFile. Их можно перебрать с по-
мощью функций CFileDialog::GetStartPosition и GetNextPathName.
В целом, классы MFC для стандартных диалоговых окон очень просты в ис-
пользовании, особенно если пользоваться ими непосредственно и не создавать собст-
венных подклассов.

107
Лекция 9. Архитектура однодокументных приложений до-
кумент/вид
В MFC версии 1.0 приложения содержали две обязательных компоненты: объ-
ект-приложение и объект-окно, представляющий главное окно приложения. Основ-
ной задачей объекта-приложения было создание объекта-окна, а этот объект выпол-
нял обработку сообщений. MFC 1.0 фактически являлась библиотекой-оболочкой для
Windows API, обеспечивающей объектно-ориентированный интерфейс для окон, диа-
логовых окон, контекстов устройств и других компонент, уже имеющихся в Windows,
хотя и в иной форме.
В MFC 2.0 стиль разработки Windows-приложений был изменен – в качестве
архитектуры приложения была предложена архитектура документ/вид (доку-
мент/представление). В приложении документ/вид (document/view application), дан-
ные приложения хранятся внутри объекта-документа (document object), а различные
способы отображения этих данных реализуются объектами-видами (view objects).
Объекты-документы и объекты-виды совместно обеспечивают обработку ко-
манд пользователя и отображение данных приложения в различной форме. Базовым
классом для объектов-документов в MFC является класс CDocument, а для объектов-
видов – класс CView. Главным окном приложения остается окно подкласса
CFrameWnd или CMDIFrameWnd, но оно в основном ответственно не за обработку со-
общений, а служит контейнером для окна-вида, панелей инструментов, полос про-
крутки и других компонент пользовательского интерфейса.
Модель приложения, в которой данные структурно отделены от пользователь-
ского интерфейса, имеет ряд преимуществ. Одно из них – улучшение изоляции про-
граммных компонент, что облегчает повторное использование классов. Но более
важное преимущество архитектуры документ/вид в MFC – упрощение процесса раз-
работки. Исходный текст для выполнения типичных действий, например, для запроса
пользователя о необходимости сохранить данные перед выходом из приложения,
обеспечивается каркасом приложения. В каркасе предусмотрены возможности сохра-
нения и чтения документов из файлов, упрощение печати, преобразование приложе-
ния в сервер документов Active Document, и др.
В MFC поддерживаются два типа приложений документ/вид. Однодокумент-
ные приложения (single document interface (SDI) applications) рассчитаны на открытие
только одного документа. Многодокументные приложения (multiple document inter-
face (MDI)) позволяют одновременно открыть несколько документов и поддерживают
несколько видов для одного документа. Приложение WordPad – пример SDI-
приложения, а Microsoft Word 97 – MDI-приложение. Каркас MFC-приложения уст-
роен т.о., чтобы максимально скрыть различие между написанием SDI- и MDI-
приложений. В последние годы MDI-приложения считаются устаревшими, и новая
версия Word 2000 тоже стала SDI-приложением (при открытии нового документа в
нем создается еще один экземпляр приложения). В данной лекции в основном будет
рассматриваться структура SDI-приложений, но практически все остается верным и
для MDI-приложений.

1. Основные понятия архитектуры документ/вид


Рассмотрим основные объекты однодокументного приложения документ/вид и
связи между этими объектами (рис. 9.1). Окно-рамка – это окно приложения верхнего
108
уровня, обычно это окно со стилем WS_OVERLAPPEDWINDOW (с рамкой для изменения
размеров, строкой заголовка, системным меню и кнопками свер-
нуть/развернуть/закрыть). Окно-вид – это дочернее окно окна-рамки, занимающее
всю его клиентскую область, за исключением панелей инструментов и строки состоя-
ния. Данные приложения хранятся в объекте-документе, для которого визуальным
представлением является окно-вид. У SDI-приложения класс окна-рамки унаследован
от CFrameWnd, класс документа – от CDocument, а класс-вид – от CView или одного
из его подклассов, например, CScrollView.

Рис. 9.1. Основные объекты SDI-приложения в архитектуре документ/вид.

На рис. 9.1 стрелками обозначены направления потоков данных. Объект-


приложение содержит цикл обработки сообщений, который направляет поступающие
сообщения в окно-рамку и в окно-вид. Объект-вид преобразует сообщения мыши и
клавиатуры в команды, которые воздействуют на данные, хранящиеся в объекте-
документе. Объект-документ предоставляет объекту-виду информацию, необходи-
мую для вывода изображения внутри окна.
В схеме на рис. 9.1 пропущено много деталей, важных для разработки и функ-
ционирования приложения. В приложении MFC 1.0 данные программы часто храни-
лись в переменных-членах класса окна-рамки. Окно-рамка рисует "виды" этих дан-
ных в своей клиентской области на основе значений переменных-членов с помощью
функций GDI, инкапсулированных в классе CDC. В архитектуре документ/вид про-
грамма оказывается более модульной, т.к. все данные хранятся в отдельном объекте-
документе и есть отдельный объект-вид для выполнения всех операций графического
отображения. В приложениях документ/вид никогда не происходит рисования в кон-
тексте окна-рамки – только в контексте окна-вида, но это выглядит, как будто рисо-
вание происходит внутри окна-рамки.

2. Функция инициализации приложения CWinApp::InitInstance


Один из интересных аспектов приложения документ/вид – способ создания
объектов окна-рамки, документа и вида. В функции InitInstance, сгенерированной
с помощью AppWizard, содержится примерно следующее:
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,

109
RUNTIME_CLASS( CMyDoc ),
RUNTIME_CLASS( CMainFrame ),
RUNTIME_CLASS( CMyView )
);
AddDocTemplate( pDocTemplate );
...
CCommandLineInfo cmdInfo;
ParseCommandLine( cmdInfo );
if ( !ProcessShellCommand( cmdInfo ) )
return FALSE;
m_pMainWnd->ShowWindow( SW_SHOW );
m_pMainWnd->UpdateWindow();

Следующие операторы:
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS( CMyDoc ),
RUNTIME_CLASS( CMainFrame ),
RUNTIME_CLASS( CMyView )
);

создают SDI-шаблон документа как объект MFC-класса CSingleDocTemplate.


Шаблон документа (document template) – это очень важная компонента SDI-
приложения документ/вид. С его помощью сопоставляются классы документа, окна-
рамки и вида. В шаблоне документа также хранится идентификатор ресурса (по
умолчанию IDR_MAINFRAME), по которому каркас приложения загружает меню, таб-
лицу ускоряющих клавиш и другие ресурсы. Макрос RUNTIME_CLASS, которому в
качестве параметра передается имя класса, возвращает указатель на объект
CRuntimeClass. Он обеспечивает динамическое создание объектов класса каркасом
приложения. После создания шаблона документа он добавляется в список шаблонов,
хранящийся в объекте-приложении:
AddDocTemplate( pDocTemplate );

Каждый зарегистрированный шаблон определяет один тип документа, поддер-


живаемого данным приложением. SDI-приложения регистрируют только один тип
документа, а MDI-приложения могут зарегистрировать несколько.
Операторы:
CCommandLineInfo cmdInfo;
ParseCommandLine( cmdInfo );

вызывают CWinApp::ParseCommandLine для инициализации объекта


CCommandLineInfo значениями, переданными через командную строку операцион-
ной системы (иногда так передается имя открываемого файла с документом). Далее
выполняется "обработка" командной строки:
if ( !ProcessShellCommand(cmdInfo) )
return FALSE;

В частности, ProcessShellCommand вызывает CWinApp::OnFileNew для за-


пуска приложения с пустым новым документом, если в командной строке не было
указано имени файла. Если же имя было указано, то вызывается
CWinApp::OpenDocumentFile для загрузки документа из файла. Именно на этой
стадии выполнения программы каркас приложения создает объект-документ, окно-
рамку и окно-вид на основе информации из шаблона документа.

110
ProcessShellCommand возвращает TRUE в случае успешной инициализации или
FALSE в случае ошибки. После успешной инициализации окно-рамка (и его дочернее
окно-вид) выводятся на экран:
m_pMainWnd->ShowWindow( SW_SHOW );
m_pMainWnd->UpdateWindow();

После запуска приложения и создания объектов документа, окна-рамки и вида,


запускается цикл обработки сообщений. За обработку сообщений совместно отвечают
все эти объекты, и большая часть служебных действий в этой области выполняется
каркасом приложения. В Windows сообщения могут получать только окна, поэтому в
MFC реализован собственный механизм маршрутизации для передачи сообщений не-
которых типов от одного объекта другому, пока сообщение не будет обработано или
передано на обработку по умолчанию ::DefWindowProc. Этот механизм является
одной из наиболее мощных возможностей архитектуры документ/вид в MFC.

3. Класс-документ
В приложении документ/вид данные хранятся в объекте-документе. Это объект
класса, унаследованного от CDocument (создается AppWizard'ом). Термин "доку-
мент" несколько неоднозначен, т.к. сразу вызывает аналогию с документами тексто-
вых редакторов или электронных таблиц. В действительности "документ" в архитек-
туре документ/вид – это просто некоторая структура данных, которая может описы-
вать что-либо, например, колоду карт в карточной игре или имена и пароли пользова-
телей в сетевой программе. Документ – это абстрактное представление данных
программы, которое должно быть четко отделено от визуального представле-
ния. Обычно у объекта-документа есть открытые функции-члены, с помощью кото-
рых другие объекты, в первую очередь, окна-виды, могут обращаться к данным доку-
мента. Вся обработка данных выполняется только объектом-документом.
Данные документа часто хранятся в виде переменных-членов подкласса
CDocument. Можно сделать их открытыми, но, в целях лучшей защищенности, пере-
менные-члены лучше описать защищенными и завести для доступа к данным специ-
альные функции-члены. Например, в текстовом редакторе объект-документ может
хранить символы в виде объекта CByteArray и предоставлять доступ к ним с помо-
щью функций-членов AddChar и RemoveChar. Для обслуживания объекта-вида мо-
гут потребоваться и более специализированные функции-члены, например, AddLine
и DeleteLine.

3.1 Операции CDocument


В документации по MFC невиртуальные функции-члены часто называются
"операциями". Подклассы CDocument наследуют от него несколько важных опера-
ций, перечисленных в следующей таблице.
Таблица 9.1. Основные операции класса CDocument
Функция-член Описание
GetFirstViewPosition Возвращает значение типа POSITION, которое можно передавать
функции GetNextView для перебора всех окон-видов, связанных с
данным документом.
GetNextView Возвращает указатель на CView – на следующее окно-вид в списке
видов, связанных с данным документом.

111
Функция-член Описание
GetPathName Возвращает имя файла документа (включая путь), например,
"C:\Documents\Personal\MyFile.doc". Если у документа нет имени,
возвращает пустую строку.
GetTitle Возвращает заголовок документа, например, "MyFile" (или пустую
строку, если документу не было присвоено имени).
IsModified Возвращает флаг модификации документа – ненулевое значение, если
в документе есть данные, несохраненные в файле.
SetModifiedFlagS Устанавливает или сбрасывает флаг модификации документа.
UpdateAllViews Обновляет все виды, связанные с данным документом (у каждого ок-
на-вида вызывается функция OnUpdate)

Среди функций табл. 9.1 чаще всего используются SetModifiedFlag и


UpdateAllViews. Функцию SetModifiedFlag надо вызывать при каждом измене-
нии данных документа. Она устанавливает флаг, по значению которого MFC при за-
крытии документа выдает пользователю запрос на сохранение данных.
UpdateAllViews вызывает перерисовку всех окон-видов (в MDI-приложениях их
может быть несколько), связанных с этим документом. Функция UpdateAllViews
вызывает у каждого вида функцию OnUpdate, которая по умолчанию объявляет ок-
но-вид недействительным, а это приводит к его перерисовке.

3.2 Виртуальные функции CDocument


В CDocument есть несколько виртуальных функций, позволяющих настроить
поведение документа в конкретном приложении. Некоторые из них практически все-
гда перегружаются в подклассах CDocument (табл. 9.2).
Таблица 9.2. Часто используемые виртуальные функции CDocument
Функция-член Описание
OnNewDocument Вызывается изнутри каркаса при создании нового документа. Перегружается
для выполнения специфической инициализации, необходимой при создании
каждого нового документа.
OnOpenDocument Вызывается каркасом при загрузке документа из файла.
DeleteContents Вызывается каркасом для удаления содержимого документа. Перегружается
для освобождения памяти и других ресурсов, выделенных объекту-документу.
Serialize Вызывается каркасом для записи/чтения данных документа из файла. Пере-
гружается почти всегда, чтобы документы можно было хранить в файлах.
В SDI-приложении MFC создает объект-документ только один раз – при запус-
ке приложения. При открытии/закрытии файлов этот объект используется повторно.
Поэтому заведены две виртуальные функции – OnNewDocument и OnOpenDocument.
MFC вызывает OnNewDocument при создании нового документа (например, при вы-
боре команды меню Файл⇒Создать). Функцию OnOpenDocument MFC вызывает
при загрузке документа с диска (при выборе команды Файл⇒Открыть). В SDI-
приложении вы можете выполнить однократную инициализацию документа в конст-
рукторе класса, а также выполнять специфическую инициализацию в перегруженных
функциях OnNewDocument и/или OnOpenDocument.
В MFC по умолчанию OnNewDocument и OnOpenDocument выполняют все ос-
новные действия по созданию новых документов или открытию существующих из
файлов. Если вы перегрузите OnNewDocument или OnOpenDocument, то обязательно
надо вызвать эти функции из базового класса, например:
BOOL CMyDoc::OnNewDocument ()

112
{
if ( !CDocument::OnNewDocument() )
return FALSE;
// Некоторые действия по инициализации данных документа
return TRUE;
}

BOOL CMyDoc::OnOpenDocument( LPCTSTR lpszPathName )


{
if ( !CDocument::OnOpenDocument( lpszPathName ) )
return FALSE;
// Некоторые действия по инициализации данных документа
return TRUE;
}

В MFC-приложениях чаще перегружается OnNewDocument, а не


OnOpenDocument. Т.к. OnOpenDocument неявно вызывает функцию документа
Serialize, то в ней обычно и выполняется инициализация данных документа значе-
ниями из файла. В OnOpenDocument надо инициализировать только те данные, кото-
рые не сохраняются на диске, а они в приложениях есть не всегда. В отличие от этой
функции, OnNewDocument по умолчанию никакой инициализации данных приложе-
ния не выполняет. Если вы добавите в класс документа какие-либо переменные-
члены, то проинициализировать их для каждого нового документа можно в перегру-
женной функции OnNewDocument.
Перед созданием или открытием нового документа, каркас приложения вызы-
вает у объекта-документа виртуальную функцию DeleteContents, предназначен-
ную для удаления текущего содержимого документа.
Для непосредственного выполнения операций чтения/записи документа на
диск, каркас вызывает функцию документа Serialize. Serialize должна быть
реализована так, чтобы помещать данные документа в файловый поток или считывать
их оттуда. Каркас приложения делает все остальное, в т.ч. открывает файл для чтения
или записи и создает объект класса CArchive, представляющий файловый поток. На-
пример, Serialize в подклассе CDocument обычно выглядит так:
void CMyDoc::Serialize( CArchive& ar )
{
if ( ar.IsStoring() ) {
// Запись данных документа в поток ar
}
else {
// Чтение данных документа из потока ar
}
}

В местах комментариев вы должны расположить операторы для чтения или за-


писи данных документа в поток. Допустим, что в некотором объекте-документе хра-
нятся две строки (объекты класса CString) с именами m_strName и m_strPhone.
Тогда функция Serialize может быть написана примерно так:
void CMyDoc::Serialize( CArchive& ar )
{
if ( ar.IsStoring() ) { // Запись в файловый поток
ar << m_strName << m_strPhone;
}
else { // Чтение из файлового потока
ar >> m_strName >> m_strPhone;
}

113
}

Если данные вашего документа состоят из переменных основных типов и се-


риализуемых (для которых у CArchive перегружены операторы ввода/вывода) клас-
сов, например, CString, то написать функцию Serialize особенно просто – доста-
точно применить к каждой переменной оператор << и >>. Для сохранения структур и
других несериализуемых типов данных можно пользоваться функциями
CArchive::Read и CArchive::Write. В классе CArchive есть функции
ReadString и WriteString для сериализации строк произвольной структуры (на-
пример, с сохранением пробелов). Если возможностей CArchive для сохранения до-
кумента недостаточно, можно вызывать функцию CArchive::GetFile и получить
указатель на объект CFile, посредством которого можно напрямую обращаться к
файлу, с которым связан поток CArchive.
У CDocument есть и реже используемые виртуальные функции, например,
OnCloseDocument (вызывается при закрытии документа) и OnSaveDocument (вызы-
вается при сохранении документа).

4. Класс-вид
Назначение объекта-документа – управление данными приложения. Объекты-
виды выполняют две задачи: генерируют визуальное представление документа на эк-
ране и преобразуют сообщения от пользователя (в основном сообщения от мыши и
клавиатуры) в команды, влияющие на данные документа. Следовательно, документы
и виды тесно взаимосвязаны, и между ними происходит двунаправленный обмен ин-
формацией.
В MFC основные свойства объектов-видов определены в классе CView. В MFC
также есть набор подклассов CView, расширяющих его функциональные возможно-
сти, например, в CScrollView добавлены возможности прокрутки окна-вида.
C объектом-документом может быть связано любое количество объек-
тов-видов, но каждый вид принадлежит единственному документу. Каркас при-
ложения хранит указатель на объект-документ в переменной-члене m_pDocument у
каждого объекта-вида. Для доступа к этому указателю у объекта-вида есть функция-
член GetDocument. Объект-документ может перебрать все связанные с ним виды,
просматривая список функциями GetFirstViewPosition и GetNextView, а вид
может получить указатель на свой документ простым вызовом GetDocument.

4.1 Виртуальные функции CView


Как и у класса CDocument, у класса CView есть несколько виртуальных функ-
ций для настройки поведения конкретного объекта-вида (табл. 9.3). Самой важной
функцией является OnDraw, которая вызывается объектом-видом при получении со-
общения WM_PAINT. В приложениях, не поддерживающих архитектуру документ/вид,
сообщения WM_PAINT обрабатываются в обработчиках OnPaint и рисование выпол-
няется посредством объектов CPaintDC. В приложениях документ/вид сообщение
WM_PAINT обрабатывается каркасом приложения. В этом обработчике создается объ-
ект CPaintDC и вызывается виртуальная функция объекта-вида OnDraw. Например,
для вывода в центре окна-вида строки, хранящейся в объекте-документе, функция
OnDraw может быть реализована так:

114
void CMyView::OnDraw( CDC* pDC )
{
CMyDoc* pDoc = GetDocument();
CString string = pDoc->GetString();
CRect rect;
GetClientRect( &rect );
pDC->DrawText( string, rect, DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER );
}

Обратите внимание, что OnDraw использует контекст устройства, переданный в


функцию в качестве параметра, а не создает собственный контекст.
Таблица 9.3. Важнейшие виртуальные функции CView
Функция-член Описание
OnDraw Вызывается для рисования данных документа внутри окна-вида.
OnInitialUpdate Вызывается при присоединении окна-вида к объекту-документу. Перегру-
жается для инициализации вида при загрузке документа из файла или при
создании нового документа.
OnUpdate Вызывается при любом изменении данных документа, когда необходимо
перерисовать окно-вид. Перегружается для реализации "интеллектуально-
го" обновления окна-вида, когда перерисовывается не все окно, а только
некоторая часть, связанная с измененными данными.
То, что окно-вид не создает собственного контекста устройства, вызвано не не-
большим сокращением исходного текста, а тем, что каркас приложения использует
одну и ту же функцию OnDraw и для вывода в окно, и при печати, и на предваритель-
ном просмотре перед печатью. В зависимости от выбранной пользователем команды,
каркас приложения передает в OnDraw различные контексты устройства. Т.о. в при-
ложениях документ/вид существенно упрощается вывод данных на принтер.
Две других часто перегружаемых виртуальных функции CView –
OnInitialUpdate и OnUpdate. Вид, как и документ, в SDI-приложении создается
только один раз и затем многократно используется. В SDI-приложениях
OnInitialUpdate вызывается каждый раз, когда документ создается или открывает-
ся с диска. По умолчанию OnInitialUpdate вызывает OnUpdate, а OnUpdate по
умолчанию объявляет все окно-вид недействительным для его перерисовки. В
OnInitialUpdate удобно поместить инициализацию переменных-членов окна-вида,
а также другие операции инициализации, необходимые при заведении нового доку-
мента. Например, в подклассах CScrollView в OnInitialUpdate обычно вызыва-
ется функция-член SetScrollSizes для задания границ полос прокрутки. В
OnInitialUpdate надо вызывать функцию-член базового класса, иначе окно-вид не
будет перерисовано.
OnUpdate вызывается, когда происходит изменение данных документа, а так-
же когда кто-нибудь (документ или один из видов) вызывает функцию документа
UpdateAllViews. OnUpdate иногда перегружается для ускорения перерисовки с
учетом границ областей, связанных с изменившимися данными документа.
В MDI-приложениях видов документа может быть несколько, и один из них
является активным, а остальные – неактивными. Фокус ввода принадлежит активному
виду. Для отслеживания, когда вид становится активным или неактивным, в нем
можно перегрузить функцию CView::OnActivateView. Окно-рамка может полу-
чить указатель на активный вид или сделать какой-либо вид активным функциями
CFrameWnd::GetActiveView и CFrameWnd::SetActiveView.

115
5. Класс "окно-рамка"
До сих пор было рассмотрено назначение трех объектов: приложение, доку-
мент и вид. Осталось рассмотреть еще один объект – окно-рамку, которое определяет
рабочую область приложения на экране и служит контейнером для вида. В SDI-
приложении есть только одно окно-рамка подкласса CFrameWnd, которое служит
главным окном приложения и содержит внутри себя окно-вид. В MDI-приложениях
есть окна-рамки двух типов – CMDIFrameWnd для главного окна и CMDIChildWnd для
окон-видов.
Окна-рамки очень важны для приложений документ/вид. Это не просто главное
окно приложения, а объект, который реализует значительную часть функционально-
сти приложения документ/вид. Например, в классе CFrameWnd есть обработчики
OnClose и OnQueryEndSession, которые дают пользователю возможность записать
несохраненный документ перед завершением приложения или перед закрытием Win-
dows. В CFrameWnd реализовано автоматическое изменение окна-вида при изменении
размеров окна-рамки с учетом панелей инструментов, строки состояния и других
компонент пользовательского интерфейса. В нем есть также функции-члены для ра-
боты с панелями инструментов, строкой состояния, для получения активного доку-
мента и видов и др.
Для лучшего понимания роли класса CFrameWnd можно сравнить его с общим
классом окна CWnd. Класс CWnd – это оболочка на языке Си++ для работы с окном
Windows. CFrameWnd унаследован от CWnd и в нем добавлено много новых средств,
выполняющих типичные действия в приложениях документ/вид.

6. Динамическое создание объектов


Чтобы каркас приложения мог автоматически создавать объекты документ,
вид, и окно-рамку, эти классы должны поддерживать специальную возможность MFC
– динамическое создание (dynamic creation). Для описания динамически создаваемых
классов в MFC предназначены два макроса – DECLARE_DYNCREATE и
IMPLEMENT_DYNCREATE. Они применяются следующим образом:
1) Создается подкласс CObject.
2) В интерфейсной части класса записывается макрос DECLARE_DYNCREATE.
Ему указывается один параметр – имя динамически создаваемого класса.
3) В реализации класса размещается вызов макроса IMPLEMENT_DYNCREATE с
двумя параметрами – именем динамически создаваемого класса и именем
его родительского класса.
Объект динамически создаваемого класса можно создавать так:
RUNTIME_CLASS( CMyClass )->CreateObject();

Этот вызов в приложении по сути приводит к вызову оператора new. Этот ме-
ханизм сделан, поскольку в Си++ нельзя динамически создавать объекты по имени
класса, которое хранится в какой-либо переменной, например:
CString strClassName = "CMyClass";
CMyClass* ptr = new strClassName; // Так объект CMyClass создать нельзя

Механизм динамического создания класса MFC позволяет зарегистрировать


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

116
Макрос DECLARE_DYNCREATE добавляет в описание класса три компонента:
статическую переменную CRuntimeClass, виртуальную функцию
GetRuntimeClass и статическую функцию CreateObject. Например, если запи-
сать в интерфейсе класса:
DECLARE_DYNCREATE( CMyClass )

то препроцессор Си++ раскроет этот макрос так:


public:
static const AFX_DATA CRuntimeClass classCMyClass;
virtual CRuntimeClass* GetRuntimeClass() const;
static CObject* PASCAL CreateObject();

Макрос IMPLEMENT_DYNCREATE обеспечивает инициализацию структуры


CRuntimeClass (информацией вроде имени класса и размера объекта класса) и соз-
дает функции GetRuntimeClass и CreateObject. Допустим,
IMPLEMENT_DYNCREATE вызывается так:
IMPLEMENT_DYNCREATE( CMyClass, CBaseClass )

Тогда CreateObject будет реализована так:


CObject* PASCAL CMyClass::CreateObject()
{ return new CMyClass; }

6.1 Назначение шаблона SDI-документа


При рассмотрении функции CWinApp::InitInstance уже встречался вызов
для создания объекта CSingleDocTemplate – шаблона SDI-документа. Конструкто-
ру CSingleDocTemplate передаются 4 параметра: целочисленный идентификатор
(IDR_MAINFRAME) и три указателя RUNTIME_CLASS. Сначала опишем смысл первого
параметра. Это идентификатор ресурса, который присвоен ресурсам четырех типов:
• пиктограмма (значок) приложения;
• верхнее меню приложения;
• таблица ускоряющих клавиш для команд верхнего меню;
• строка параметров документа (document string), которая задает, в частности,
расширение файлов документов "по умолчанию" и имя "по умолчанию" для
новых документов.
В SDI-приложениях каркас создает главное окно приложения как окно-рамку
класса, который указан в шаблоне документа. Затем у окна-рамки вызывается функ-
ция-член LoadFrame. Ей передается идентификатор ресурса, указывающий на ресур-
сы перечисленных выше типов. LoadFrame загружает все эти ресурсы, но чтобы это
получилось удачно, действительно в RC-файле должны быть такие ресурсы с одина-
ковыми идентификаторами (AppWizard генерирует их автоматически).
В строке параметров документа отдельные параметры хранятся в подстроках,
отделенных друг от друга служебными символами "\n". В порядке "слева-направо"
могут быть указаны следующие параметры:
• Текст заголовка окна-рамки. Обычно это название приложения, например,
"Microsoft Draw".
• Имя, присваиваемое новым документам. Если эта подстрока не заполнена (сра-
зу идет "\n"), то в качестве имени будет использоваться "Untitled".

117
• Краткое описание типа документа, которое выводится в диалоговом окне по
команде File⇒New в MDI-приложениях, чтобы пользователь мог выбрать один
из нескольких документов. В SDI-приложениях не используется.
• Краткое описание типа документа с маской имени файла, например, "Drawing
Files (*.drw)". Эта подстрока используется в диалоговых окна открытия и
сохранения файлов.
• Расширение имени файла "по умолчанию", например, ".drw".
• Имя без пробелов, идентифицирующее тип документа в реестре, например,
"Draw.Document". Если приложение вызовет CWinApp::Register-
ShellFileTypes для регистрации типа документа в оболочке Windows,. то эта
подстрока запишется в реестр в раздел HKEY_CLASSES_ROOT после расшире-
ния имени файла документа.
• Краткое описание типа документа, например, "Microsoft Draw Document".
Может содержать пробелы. Если приложение выполнит регистрацию типа до-
кумента вызовом CWinApp::RegisterShellFileTypes, то это описание бу-
дет выводиться в качестве типа файла в его окне свойств (например, в про-
грамме Проводник).
В строке параметров документа необязательно указывать все семь подстрок,
некоторые можно пропускать, только ставить для них разделитель "\n". При генера-
ции приложения с помощью AppWizard строка параметров документа создается ав-
томатически на основе данных из диалогового окна Advanced Options, которое мож-
но вызвать на 4-м шаге создания приложения (AppWizard's Step 4). Типичная строка
параметров документа для SDI-приложения в RC-файле выглядит так:
STRINGTABLE
BEGIN
IDR_MAINFRAME "Microsoft Draw\n\n\nDraw Files(*.drw)\n.drw\n
Draw.Document\nMicrosoft Draw Document"
END

В данном примере после запуска окно-рамка будет иметь заголовок "Untitled


- Microsoft Draw". Расширение имени файла "по умолчанию" для документов
приложения – ".drw", а в окнах открытия и сохранения файла будет выбрана строка
типа файлов "Draw Files (*.drw)".

7. Маршрутизация командных сообщений


Одна из наиболее заметных особенностей архитектуры документ/вид в том, что
приложение может обрабатывать командные сообщения "почти везде". Командными
сообщениями (command messages) в MFC называются сообщения WM_COMMAND, кото-
рые генерируются после выбора команд меню, по нажатию ускоряющих клавиш и
при нажатии кнопок панелей инструментов. Окно-рамка – это физический получатель
большинства командных сообщений, но их можно также обрабатывать в окне-виде, в
документе или даже в объекте-приложении. Для этого надо только добавить соответ-
ствующие записи в карту сообщений класса. Маршрутизация команд позволяет по-
мещать командные обработчики там, где их разумнее разместить по структуре при-
ложения, а не собирать все обработчики в классе окна-рамки. Обработчики обновле-
ния для команд меню, панелей инструментов и других компонент пользовательского
интерфейса также включены в механизм маршрутизации, поэтому вы можете поме-
щать обработчики ON_UPDATE_COMMAND_UI за пределами окна-рамки.
118
Механизм маршрутизации команд скрыт глубоко в MFC. Когда окно-рамка по-
лучает сообщение WM_COMMAND, оно вызывает виртуальную функцию OnCmdMsg, ко-
торая есть у всех подклассов CCmdTarget и именно с нее начинается процедура мар-
шрутизации. В CFrameWnd функция OnCmdMsg реализована примерно так:
BOOL CFrameWnd::OnCmdMsg(...)
{
// Сначала пытаемся обработать сообщение в активном виде
CView* pView = GetActiveView();
if ( pView != NULL && pView->OnCmdMsg(...) )
return TRUE;
// Затем пытаемся обработать сообщение в окне-рамке
if ( CWnd::OnCmdMsg(...) )
return TRUE;
// Если сообщение не обработано, то оно передается в объект-приложение
CWinApp* pApp = AfxGetApp();
if ( pApp != NULL && pApp->OnCmdMsg(...) )
return TRUE;
return FALSE;
}

Если ни один объект, в т.ч. объект-приложение, сообщение не обработал, то


CFrameWnd::OnCmdMsg возвращает FALSE и каркас приложения передаст сообще-
ние функции ::DefWindowProc для обработки "по умолчанию".
Теперь ясно, что командные сообщения, получаемые окном-рамкой, направля-
ются в активное окно-вид, а затем в объект-приложение. Но как они достигают объ-
ект-документ? Когда CFrameWnd::OnCmdMsg вызывает функцию OnCmdMsg у актив-
ного вида, то этот вид сначала пытается обработать сообщение самостоятельно. Но
если обработчика сообщения в нем нет, то окно-вид вызовет функцию-член
OnCmdMsg у своего документа. Если документ не может обработать сообщение, то он
передает его объекту-шаблону документа. Путь командного сообщения, полученного
окном-рамкой SDI-приложения, показан на рис. 9.2. Процедура маршрутизации пре-
кращается, если один из объектов обработал сообщение, или, если обработки не было,
то сообщение попадает в ::DefWindowProc.
Значение маршрутизации команд станет понятным, если вы посмотрите, как
типичное приложение документ/вид обрабатывает команды меню, ускоряющих кла-
виш и панелей инструментов. По соглашению, команды File⇒New, File⇒Open и
File⇒Exit обрабатываются в объекте-приложении, в котором есть командные обра-
ботчики OnFileNew, OnFileOpen и OnAppExit. Команды File⇒Save и File⇒Save As
обычно обрабатываются объектом-документом и в нем есть обработчики "по умолча-
нию" CDocument::OnFileSave и CDocument::OnFileSaveAs. Команды для
включения/выключения панелей инструментов и строки состояния обрабатываются в
окне-рамке с помощью функций-членов CFrameWnd, а большинство остальных
типичных команд обрабатываются в окне-виде или в объекте-документе.
Когда вы создаете собственные обработчики сообщений, важно помнить, что
маршрутизация выполняется только для командных сообщений и для обработчиков
обновления. Остальные сообщения Windows, например, WM_CHAR, WM_LBUTTONDOWN,
WM_CREATE или WM_SIZE должны обрабатываться в окне-получателе сообщения.
Обычно сообщения мыши и клавиатуры поступают в окно-вид, а большинство ос-
тальных сообщений – в окно-рамку. Объект-документ и объект-приложение никогда
не получают никаких сообщений, кроме командных.

119
Рис. 9.2. Маршрутизация командных сообщений, посылаемых окну-рамке SDI-приложения.

7.1 Стандартные командные идентификаторы и обработчики


При написании приложения документ/вид обычно нет необходимости само-
стоятельно писать обработчики для всех команд меню. CWinApp, CDocument,
CFrameWnd и другие классы MFC содержат обработчики "по умолчанию" для типич-
ных команд меню, вроде File⇒Open и File⇒Save. Кроме того, каркас сгенерирован-
ного приложения по умолчанию обеспечивает связь команд с идентификаторами вро-
де ID_FILE_OPEN и ID_FILE_SAVE с обработчиками "по умолчанию".
В табл. 9.4 приведены часто используемые стандартные командные идентифи-
каторы и соответствующие командные обработчики. В столбце "Установлен?" ука-
зано, надо ли добавлять макрос карты сообщений для этого сообщения или его обра-
ботчик уже установлен в каркасе приложения. Например, у команды ID_APP_EXIT
обработчик не установлен, поэтому в карту сообщений класса приложения надо доба-
вить запись:
ON_COMMAND( ID_APP_EXIT, СWinApp::OnAppExit )

Таблица 9.4. Стандартные командные идентификаторы и обработчики


Идентификатор команды Пункт меню Обработчик "по умолчанию" Установ-
лен?
Меню Файл
ID_FILE_NEW New CWinApp::OnFileNew Нет
ID_FILE_OPEN Open CWinApp::OnFileOpen Нет
ID_FILE_SAVE Save CDocument::OnFileSave Да
ID_FILE_SAVE_AS Save As CDocument::OnFileSaveAs Да
ID_FILE_PAGE_SETUP Page Setup Отсутствует N/A
ID_FILE_PRINT_SETUP Print Setup CWinApp::OnFilePrintSetup Нет
ID_FILE_PRINT Print CView::OnFilePrint Нет
ID_FILE_PRINT_PREVIEW Print Preview CView::OnFilePrintPreview Нет
ID_FILE_SEND_MAIL Send Mail CDocument::OnFileSendMail Нет
ID_FILE_MRU_FILE1_ N/A CWinApp::OnOpenRecentFile Да
ID_FILE_MRU_FILE16
ID_APP_EXIT Exit CWinApp::OnAppExit Да
Меню Правка
ID_EDIT_CLEAR Clear Отсутствует N/A

120
Идентификатор команды Пункт меню Обработчик "по умолчанию" Установ-
лен?
ID_EDIT_CLEAR_ALL Clear All Отсутствует N/A
ID_EDIT_CUT Cut Отсутствует N/A
ID_EDIT_COPY Copy Отсутствует N/A
ID_EDIT_PASTE Paste Отсутствует N/A
ID_EDIT_PASTE_LINK Paste Link Отсутствует N/A
ID_EDIT_PASTE_SPECIAL Paste Special Отсутствует N/A
ID_EDIT_FIND Find Отсутствует N/A
ID_EDIT_REPLACE Replace Отсутствует N/A
ID_EDIT_UNDO Undo Отсутствует N/A
ID_EDIT_REDO Redo Отсутствует N/A
ID_EDIT_REPEAT Repeat Отсутствует N/A
ID_EDIT_SELECT_ALL SelectAll Отсутствует N/A
Меню Вид
ID_VIEW_TOOLBAR Toolbar CFrameWnd::OnBarCheck Да
ID_VIEW_STATUS_BAR Status Bar CFrameWnd::OnBarCheck Да
Меню Окно (есть только в MDI приложениях)
ID_WINDOW_NEW New Window CMDIFrameWnd::OnWindowNew Да
ID_WINDOW_ARRANGE Arrange All CMDIFrameWnd::OnMDIWindowCmd Да
ID_WINDOW_CASCADE Cascade CMDIFrameWnd::OnMDIWindowCmd Да
ID_WINDOW_TILE_HORZ Tile Horizontal CMDIFrameWnd::OnMDIWindowCmd Да
ID_WINDOW_TILE_VERT Tile Vertical CMDIFrameWnd::OnMDIWindowCmd Да
Меню Помощь
ID_APP_ABOUT About App- Отсутствует N/A
Name

В MFC для некоторых команд есть стандартные обработчики обновления:


• CFrameWnd::OnUpdateControlBarMenu для команд ID_VIEW_TOOLBAR и
ID_VIEW_STATUS_BAR;
• CMDIFrameWnd::OnUpdateMDIWindowCmd для команд меню Окно.
• CDocument::OnUpdateFileSendMail для ID_FILE_SEND_MAIL.
Классы-виды CEditView и CRichEditView содержат собственные командные
обработчики для команд меню Правка, но в других окнах-видах их надо добавлять
самостоятельно (если они нужны).
Для своих собственных команд меню не следует использовать стандартные
идентификаторы и обработчики каркаса приложения. Для стандартных команд можно
заменять обработчики по умолчанию на свои собственные. Т.е. вы можете пользо-
ваться готовыми средствами каркаса в той мере, в которой они подходят для вашего
приложения.

121
Литература
1) Microsoft Corporation. Разработка приложений на Microsoft Visual C++ 6.0. Учеб-
ный курс: Официальное пособие Microsoft для самостоятельной подготовки. М.:
"Русская Редакция", 2000. (В этом учебном пособии приведены инструкции по ис-
пользованию различных возможностей MFC и среды Visual C++ 6. Некоторым не-
достатком является отсутствие подробной описательной части, но удачные поша-
говые инструкции позволяют отработать выполнение большого количества типич-
ных операций в Visual C++).
2) Petzold C. Programming Windows. Microsoft Press. 1990. (Наверное, самая известная
книга по программированию для Windows на уровне API)
3) Prosise J. Programming Windows with MFC. Microsoft Press. 1999. (В некотором
смысле, аналог книги Petzold'а, но по программированию для Windows с использо-
ванием библиотеки классов MFC. Часть лабораторных работ и лекционного мате-
риала данного курса основаны на этой книге).
4) Toth V. Visual C++ 4 Unleashed. Sams Publishing, 1996 (Учебник по программиро-
ванию для Windows с использованием Visual C++ версии 4.0. Рассчитан на доста-
точно опытных программистов. Часть глав посвящены описанию архитектуры
Windows с точки зрения программиста).
5) Вильямс А. Системное программирование в Windows 2000 для профессионалов.
СПб: Питер, 2001. (В этой книге описан ряд средств, доступных в Windows 2000
на уровне API – технология COM, межпроцессное взаимодействие, работа с обо-
лочкой и др. Интересно краткое и доступное введение в технологию COM, причем
приведены исходные тексты программ, удачно иллюстрирующие описываемые
понятия.)
6) Круглински Д., Уингоу С., Шеферд Дж. Программирование на Microsoft Visual
C++ 6.0 для профессионалов. СПб: Питер, 2000. (Книга, напоминающая по стилю
изложения пособие для самостоятельной подготовки. Подробная энциклопедия
приемов практического программирования в Visual C++ и MFC.)
7) Пройдаков Э.М., Теплицкий Л.А. Англо-русский словарь по вычислительной тех-
нике, Интернету и программированию. М.: "Русская Редакция", 2000. (Толковый
англо-русский словарь. В данном курсе на CD-ROM приведен перечень исполь-
зуемых терминов, сформированный в основном на основе этого словаря.)
8) Рихтер Дж. Windows для профессионалов: создание эффективных Win32-
приложений с учетом специфики 64-разрядной версии Windows. СПб: Питер,
2001. (Очень известная книга, в которой описаны различные вопросы программи-
рования для 32-разрядных версий Windows 95/NT/2000 на уровне API.)
9) Тихомиров Ю.В. Самоучитель MFC. СПб: БХВ – Санкт-Петербург, 2000. (Под-
робное руководство начального уровня по библиотеке MFC, в основном имеющее
справочный характер.)

122
Учебно-методическое издание

А.А. Богуславский, С.М. Соколов

Основы программирования на языке Си++


В 4-х частях.
(для студентов физико-математических факультетов
педагогических институтов)

Компьютерная верстка Богуславский А.А.


Технический редактор Пономарева В.В.
Сдано в набор 12.04.2002 Подписано в печать 16.04.2002
Формат 60х84х1/16 Бумага офсетная
Печ. л. 20,5 Учетно-изд.л. ____ Тираж 100
Лицензия ИД №06076 от 19.10.2001

140410 г.Коломна, Моск.обл., ул.Зеленая, 30. Коломенский государственный педаго-


гический институт.

123
124