А.А. Богуславский, С.М. Соколов
Основы программирования на языке Си++ Часть 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-разрядным. Совместимость со старыми программами для MSDOS и 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. В прошлом, когда компания Microsoft включала в Windows новую возможность, она также расширяла описание интерфейса API – набора системных вызовов. Сейчас многие новые механизмы Windows (например, 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++ можно применять для разработки программ для встроенных систем и компактных компьютеров, работающих под управлением усеченной версии Win16
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 и Windows 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. Набор стандартных элементов управления Windows.
Рис. 3.11. Некоторые стандартные элементы управления 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 есть понятия высокого уровня, которых нет в Windows 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
// Описание 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 можно использовать для освобождения ресурсов при завершении приложения (например, ресурсов и памяти, выделенных в InitInstance). В реализации "по умолчанию" функция 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 CClientDC CWindowDC CMetaFileDC
С чем связан этот контекст устройства Клиентская область окна (только в обработчиках OnPaint) Клиентская область окна Окно, включая неклиентскую область Метафайл 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. Основные атрибуты контекста устройства Атрибут Цвет текста Цвет фона Режим фона Режим преобразования координат Режим рисования Текущая позиция Текущее перо Текущая кисть Текущий шрифт
Значение по умолчанию Черный Белый OPAQUE MM_TEXT
Функция-член CDC для задания значения SetTextColor SetBkColor SetBkMode SetMapMode
Функция-член СDC для получения значения GetTextColor GetBkColor GetBkMode GetMapMode
R2_COPYPEN (0,0) BLACK_PEN WHITE_BRUSH SYSTEM_FONT
SetROP2 MoveTo SelectObject SelectObject SelectObject
GetROP2 GetCurrentPosition SelectObject 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 MM_LOMETRIC MM_HIMETRIC MM_LOENGLISH MM_HIENGLISH MM_TWIPS MM_ISOTROPIC MM_ANISOTROPIC
Расстояние, соответствующее логической единице 1 пиксел 0.1 мм 0.01 мм 0.01 дюйма 0.001 дюйма 1/1440 дюйма (0.0007 дюйма) Определяется пользователем (масштаб по осям x и y одинаков) Определяется пользователем (масштаб по осям x и y задается независимо)
Ориентация координатных осей x вправо, у вниз x вправо, у вверх x вправо, у вверх x вправо, у вверх x вправо, у вверх x вправо, у вверх Определяется пользователем Определяется пользователем
Система координат в режиме MM_TEXT показана на рис. 5.1. Начало координат располагается в левом верхнем углу поверхности изображения (в зависимости от контекста устройства, это может быть левый верхний угол экрана, окна, клиентской области окна). Ось х направлена вправо, ось y вниз, 1 логическая единица равна 1-му пикселу. В остальных, "метрических", системах ось y направлена вверх, так что система координат оказывается правой, но начало координат по умолчанию всегда помещается в левый верхний угол поверхности изображения. (0, 0) x Поверхность изображения 1 единица = 1 пиксел y
Рис. 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 Параметр HORZRES VERTRES HORZSIZE VERTSIZE LOGPIXELSX LOGPIXELSY NUMCOLORS TECHNOLOGY
Значение, возвращаемое функцией GetDeviceCaps Ширина поверхности изображения, в пикселах Высота поверхности изображения, в пикселах Ширина поверхности изображения, в миллиметрах Высота поверхности изображения, в миллиметрах Количество пикселов на логический дюйм по горизонтали Количество пикселов на логический дюйм по вертикали Для дисплея – количество статических цветов, для принтера или плоттера – количество поддерживаемых цветов Получение битовых флагов, идентифицирующих тип устройства – дисплей, принтер, плоттер и др.
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_SOLID PS_DASH PS_DOT
PS_DASHDOT PS_DASHDOTDOT PS_NULL 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 SetTextJustif ication SetTextColor SetBkColor
Описание Выводит текст с заданным форматированием внутри описывающего прямоугольника Выводит символьную строку в текущей или заданной позиции Выводит символьную строку, содержащую табуляции Выводит символьную строку с возможным заполнением описывающего прямоугольника фоновым цветом или изменением межсимвольного расстояния Вычисляет ширину строки с учетом параметров текущего шрифта Вычисляет ширину строки с табуляциями с учетом текущего шрифта Возвращает свойства текущего шрифта (высоту символа, среднюю ширину символа и т.п.) Задает параметры выравнивания для функции TextOut и некоторых других функций вывода Задает дополнительную ширину, необходимую для выравнивания символьной строки Задает в контексте устройства цвет вывода текста Задает в контексте устройства цвет фона, которым заполняется область между символами при выводе текста
Функции 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 WHITE_PEN NULL_BRUSH HOLLOW_BRUSH BLACK_BRUSH DKGRAY_BRUSH GRAY_BRUSH LTGRAY_BRUSH WHITE_BRUSH ANSI_FIXED_FONT ANSI_VAR_FONT SYSTEM_FONT SYSTEM_FIXED_FONT
Описание Пустое (прозрачное) перо Черное сплошное перо толщиной 1 пиксел Белое сплошное перо толщиной 1 пиксел Пустая (прозрачная) кисть То же, что NULL_BRUSH Черная кисть Темно-серая кисть Серая кисть Светло-серая кисть Белая кисть Моноширинный системный шрифт ANSI Пропорциональный системный шрифт ANSI Системный шрифт для пунктов меню, элементов управления и т.п. Моноширинный системный шрифт (для совместимости со старыми версиями 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, от которого унаследованы подклассы для разновидностей контекстов устройств Windows, например, 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_xBUTTONDOWN. Сообщения мыши посылаются в окно, над которым располагается указатель. Поэтому, если пользователь нажмет левую кнопку над клиентской областью некоторого окна, а отпустит ее за пределами окна, то это окно получит только сообщение WM_LBUTTONDOWN. Многие программы реагируют только на сообщения о нажатии кнопок мыши. Если важно обрабатывать и нажатие, и отпускание кнопки, приложение должно использовать режим "захвата" мыши (см. п.1.2). WM_xBUTTONUP
Таблица 6.2. Макросы карты сообщений и имена обработчиков для сообщений мыши, связанных с клиентской областью окна. Сообщение WM_xBUTTONDOWN WM_xBUTTONUP WM_xBUTTONDBLCLK WM_MOUSEMOVE
Макрос карты сообщений ON_WM_xBUTTONDOWN ON_WM_xBUTTONUP ON_WM_xBUTTONDBLCLK ON_WM_MOUSEMOVE
Имя функции-обработчика OnxButtonDown OnxButtonUp OnxButtonDblClk 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 MK_SHIFT
Когда флаг установлен Нажата левая кнопка мыши Нажата средняя кнопка мыши Нажата правая кнопка мыши Нажата клавиша Ctrl Нажата клавиша 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_F12 F1 - F12
Код виртуальСоответствующая кланой клавиши виша VK_NEXT PgDn
75
Код виртуальной клавиши VK_CANCEL VK_RETURN VK_BACK VK_TAB VK_SHIFT VK_CONTROL VK_MENU VK_PAUSE VK_ESCAPE VK_SPACE VK_PRIOR
Соответствующая клавиша Ctrl-Break Enter Backspace Tab Shift Ctrl Alt Pause Esc Spacebar PgUp
Код виртуальной клавиши VK_END VK_HOME VK_LEFT VK_UP VK_RIGHT VK_DOWN VK_INSERT VK_DELETE VK_CAPITAL VK_NUMLOCK VK_SCROLL
Соответствующая клавиша End Home стрелка влево стрелка вверх стрелка вправо стрелка вниз Ins Del Caps Lock Num Lock 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 BEGIN "N", ID_FILE_NEW, "Z", ID_EDIT_UNDO, "X", ID_EDIT_CUT, ... VK_DELETE, ID_EDIT_CUT, ... END
MOVEABLE VIRTKEY,CONTROL VIRTKEY,CONTROL VIRTKEY,CONTROL VIRTKEY,SHIFT
В данном примере 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. Их, чтобы отличать от ЭУ старых версий Windows, иногда называются стандартными элементами управления Windows 95. Они реализованы в динамической библиотеке Comctl32.dll. Т.к. ЭУ являются дочерними окнами, они автоматически перемещаются вместе с родительским окном, автоматически уничтожаются вместе с ним, а также ограничены при отображении областью родительского окна. Все сообщения от ЭУ посылаются родительским окнам. 1. Стандартные элементы управления
В таблице 7.1 перечислены 6 типов ЭУ, для которых Windows автоматически регистрирует оконные классы, вместе с соответствующими классами-оболочками MFC. Таблица 7.1. Стандартные элементы управления Тип элемента Кнопка Список Элемент редактирования Комбинированный список Полоса прокрутки Статический элемент
Имя оконного класса WNDCLASS BUTTON LISTBOX EDIT COMBOBOX SCROLLBAR STATIC
Класс MFC CButton CListBox CEdit CComboBox CScrollBar 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. Уведомления списка Код уведомления LBN_SETFOCUS LBN_KILLFOCUS LBN_ERRSPACE LBN_DBLCLK LBN_SELCHANGE LBN_SELCANCEL
Когда посылается
Макрос карты сообщений
Список получил фокус ввода Список потерял фокус ввода Операция отменена из-за нехватки памяти Двойной щелчок на элементе В списке изменено выделение Выделение снято
ON_LBN_SETFOCUS ON_LBN_KILLFOCUS ON_LBN_ERRSPACE
Необходим ли у списка стиль LBS_NOTIFY? Нет Нет Нет
ON_LBN_DBLCLK ON_LBN_SELCHANGE 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 STN_DBLCLK STN_DISABLE STN_ENABLE
Когда посылается Щелчок мышью на элементе Двойной щелчок на элементе ЭУ выключен (запрещен) ЭУ включен
Макрос карты сообщений ON_STN_CLICKED ON_STN_DBLCLK ON_STN_DISABLE 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 ON_NOTIFY_REFLECT ON_UPDATE_COMMAND_UI_REFLECT ON_WM_CTLCOLOR_REFLECT
Какие сообщения отражаются Отражение уведомлений, посылаемых в виде сообщений WM_COMMAND Отражение уведомлений, посылаемых в виде сообщений WM_NOTIFY Отражение уведомлений обновления панелей инструментов, строк состояния и других ЭУ Отражение сообщений WM_CTLCOLOR
92
Макрос ON_WM_DRAWITEM_REFLECT ON_WM_MEASUREITEM_REFLECT ON_WM_COMPAREITEM_REFLECT ON_WM_DELETEITEM_REFLECT ON_WM_CHARTOITEM_REFLECT ON_WM_VKEYTOITEM_REFLECT ON_WM_HSCROLL_REFLECT ON_WM_VSCROLL_REFLECT ON_WM_PARENTNOTIFY_REFLECT
Какие сообщения отражаются WM_DRAWITEM от ЭУ с собственным отображением WM_MEASUREITEM от ЭУ с собственным отображением WM_COMPAREITEM от ЭУ с собственным отображением WM_DELETEITEM от ЭУ с собственным отображением WM_CHARTOITEM от списков WM_VKEYTOITEM от списков WM_HSCROLL от полос прокрутки WM_VSCROLL от полос прокрутки Отражение сообщений 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 есть удобные классы-оболочки для работы со стандартными диалоговыми окнами Windows (для открытия/сохранения файлов, диалоговые окна печати и др.). Особым видом диалоговых окон являются окна свойств (окна с закладками, 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
Вручную такие описания диалоговых окон делаются очень редко. В среде Visual C++ команда меню Insert⇒Resource позволяет добавить в проект пустой шаблон диалогового окна и затем отредактировать его в редакторе ресурсов. На рис. 8.1 показано окно редактора диалоговых окон, встроенного в Visual C++. Элементы управления можно выбирать в панели инструментов Controls и "рисовать" их в диалоговом окне (если панель инструментов Controls отсутствует на экране, ее можно включить командой меню Tools⇒Customize⇒Toolbars). Свойства диалогового окна (заголовок, шрифт, стиль) и свойства ЭУ доступны в окнах свойств, вызываемых командой Properties из контекстных меню.
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 задается после выбора команды Layout⇒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. Диалоговое окно ClassWizard для добавления новой переменной-члена в диалоговый класс.
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 = pListBox->AddString( pListBox->AddString( pListBox->AddString(
(CListBox*)GetDlgItem( IDC_LIST ); "Первый элемент" ); "Второй элемент" ); "Третий элемент" );
Здесь для вызова функций-членов 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 interface (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 GetTitle IsModified SetModifiedFlagS UpdateAllViews
Описание Возвращает имя файла документа (включая путь), например, "C:\Documents\Personal\MyFile.doc". Если у документа нет имени, возвращает пустую строку. Возвращает заголовок документа, например, "MyFile" (или пустую строку, если документу не было присвоено имени). Возвращает флаг модификации документа – ненулевое значение, если в документе есть данные, несохраненные в файле. Устанавливает или сбрасывает флаг модификации документа. Обновляет все виды, связанные с данным документом (у каждого окна-вида вызывается функция 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, которые дают пользователю возможность записать несохраненный документ перед завершением приложения или перед закрытием Windows. В 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::RegisterShellFileTypes для регистрации типа документа в оболочке 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 ID_FILE_OPEN ID_FILE_SAVE ID_FILE_SAVE_AS ID_FILE_PAGE_SETUP ID_FILE_PRINT_SETUP ID_FILE_PRINT ID_FILE_PRINT_PREVIEW ID_FILE_SEND_MAIL ID_FILE_MRU_FILE1_ ID_FILE_MRU_FILE16 ID_APP_EXIT Меню Правка ID_EDIT_CLEAR
Установлен?
New Open Save Save As Page Setup Print Setup Print Print Preview Send Mail N/A
CWinApp::OnFileNew CWinApp::OnFileOpen CDocument::OnFileSave CDocument::OnFileSaveAs Отсутствует CWinApp::OnFilePrintSetup CView::OnFilePrint CView::OnFilePrintPreview CDocument::OnFileSendMail CWinApp::OnOpenRecentFile
Нет Нет Да Да N/A Нет Нет Нет Нет Да
Exit
CWinApp::OnAppExit
Да
Clear
Отсутствует
N/A
120
Идентификатор команды
Пункт меню
ID_EDIT_CLEAR_ALL Clear All ID_EDIT_CUT Cut ID_EDIT_COPY Copy ID_EDIT_PASTE Paste ID_EDIT_PASTE_LINK Paste Link ID_EDIT_PASTE_SPECIAL Paste Special ID_EDIT_FIND Find ID_EDIT_REPLACE Replace ID_EDIT_UNDO Undo ID_EDIT_REDO Redo ID_EDIT_REPEAT Repeat ID_EDIT_SELECT_ALL SelectAll Меню Вид ID_VIEW_TOOLBAR Toolbar ID_VIEW_STATUS_BAR Status Bar Меню Окно (есть только в MDI приложениях) ID_WINDOW_NEW New Window ID_WINDOW_ARRANGE Arrange All ID_WINDOW_CASCADE Cascade ID_WINDOW_TILE_HORZ Tile Horizontal ID_WINDOW_TILE_VERT Tile Vertical Меню Помощь ID_APP_ABOUT About AppName
Обработчик "по умолчанию" Отсутствует Отсутствует Отсутствует Отсутствует Отсутствует Отсутствует Отсутствует Отсутствует Отсутствует Отсутствует Отсутствует Отсутствует
Установлен? N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A
CFrameWnd::OnBarCheck CFrameWnd::OnBarCheck
Да Да
CMDIFrameWnd::OnWindowNew CMDIFrameWnd::OnMDIWindowCmd CMDIFrameWnd::OnMDIWindowCmd CMDIFrameWnd::OnMDIWindowCmd CMDIFrameWnd::OnMDIWindowCmd
Да Да Да Да Да
Отсутствует
N/A
В 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 Учетно-изд.л. ____ Лицензия ИД №06076 от 19.10.2001
Тираж 100
140410 г.Коломна, Моск.обл., ул.Зеленая, 30. Коломенский государственный педагогический институт. 123
124