4253

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

Книга

Информатика, кибернетика и программирование

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

Русский

2012-11-15

1.17 MB

112 чел.

Введение

Изучение программирования на Си++ для современных операционных систем семейства 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.

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


Лекция 1. Архитектура 32-разрядных ОС Windows

1. Введение

32-разрядные операционные системы Windows (Win32) отличаются от более старых 16-разрядных версий тем, что предназначены для работы на 32-разрядных процессорах (обычно это Intel-совместимые процессоры i386-Pentium III) и максимально полно используют их возможности. Одна из основных возможностей – более простой, по сравнению с 16-разрядными процессорами, способ адресации памяти, позволяющий пронумеровать все ячейки памяти адресного пространства программы с помощью 32-разрядных адресов.

Существует большое количество различных версий Windows. 32-разрядные ОС можно разделить на два семейства: (1) Windows NT/2000 и (2) Windows 95/98/ME.

При разработке ОС Windows NT главное внимание уделялось надежности, безопасности (защите данных и программ от несанкционированного доступа) и переносимости. Последнее свойство подразумевает возможность работы ОС на различных платформах, а не только на ПК с Intel-совместимыми процессорами. В эти ОС встроен графический интерфейс пользователя и различные средства для использования ОС в качестве сервера. В Windows NT есть эмулятор старых ОС, позволяющий запускать программы для Win16 и MS-DOS (если только они не используют каких-либо недокументированных возможностей и не обращаются напрямую к устройствам ПК).

ОС второго семейства (Windows 95) предназначались, в первую очередь, для домашнего применения и должны были обеспечить безболезненный переход от 16-разрядных ОС к 32-разрядным. Совместимость со старыми программами для MS-DOS и Windows 3.1 была одним из главных критериев при разработке этих ОС. Поэтому, чтобы работали как можно больше старых программ, в том числе использующие недокументированные особенности старых ОС и аппаратуры ПК, в эти ОС было включено много 16-разрядного кода (практически и MS-DOS, и Win16 как подсистемы). Поэтому ОС Windows 95 являются менее надежными, чем Windows NT (хотя и гораздо более надежными, чем старые 16-разрядные ОС).

Несмотря на различия между двумя семействами ОС, у них есть и большое количество общих свойств (например, интерфейс пользователя). Для программиста важно, что у всех ОС Win32 есть общий набор системных вызовов (функций), доступных для вызова из программ для обращения к ОС. Эти функции составляют Win32 API. Отличие состоит в том, что у некоторых функций в Windows NT используются параметры, которые в Windows 95 игнорируются. Например, это параметры, касающиеся безопасности и ограничивающие доступ к некоторым ресурсам программы.

2. Окна и сообщения

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

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

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

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

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

Окно всегда "принадлежит" потоку. Поток может владеть одним или несколькими окнами, а также может не иметь ни одного окна. Окна потока сами находятся в иерархической связи: некоторые из них являются окнами верхнего уровня, а некоторые – дочерними окнами других окон (рис. 1.1).

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

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

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

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

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

Оконные классы – это шаблоны, хранящие информацию о свойствах окна. Среди этих свойств – начальные размеры окна, его пиктограмма, курсор и меню. Вероятно, самое главное свойство – это адрес функции, называемой оконной процедурой. Приложение обычно выполняет обработку полученных сообщений с помощью вызова функции DispatchMessage из Win API. Функция DispatchMessage, в свою очередь, вызывает соответствующую оконную процедуру. Адрес оконной процедуры при этом извлекается из оконного класса окна, которому послано сообщение. Именно оконная процедура выполняет обработку всех сообщений, посылаемых окну.

В Windows есть много стандартных оконных классов, например, стандартные элементы управления вроде кнопок (класс Button) и строк ввода (класс Edit).

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

Windows позволяет создавать подклассы и суперклассы для существующих оконных классов. При создании подкласса выполняется замена оконной процедуры класса. Это делается с помощью функции SetWindowLong (подкласс экземпляра) или SetClassLong (глобальный подкласс). Различие между двумя функциями в том, что в первом случае изменяется поведение только одного экземпляра окна, а во втором случае – поведение всех окон данного класса (в пределах приложения).

При создании суперкласса новый класс основывается на существующем, и запоминается адрес старой оконной процедуры. Для создания суперкласса приложение получает информацию о существующем классе с помощью функции GetClassInfo, запоминает адрес старой оконной процедуры, затем модифицирует полученную структуру WNDCLASS и использует ее при вызове RegisterClass. Сообщения, не обрабатываемые новой оконной процедурой, должны передаваться в старую.

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

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 – для сообщений от кнопок. Набор системных сообщений не зафиксирован, новые сообщения могут добавляться по мере роста возможностей новых версий ОС.

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

Сообщения остальных групп относятся к специфическим типам окон. Есть сообщения, определенные для строк ввода (EM), кнопок (BM), списков (LB), комбинированных списков (CB), полос прокрутки (SBM), деревьев (TVM) и др. Эти сообщения, за редким исключением, обычно обрабатываются оконной процедурой самого элемента управления и не слишком интересны для прикладного программиста.

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

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

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

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

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

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

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

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

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 и реализован в виде набора системных динамических библиотек, но рассматривается как часть "ядра" Windows. Остальные API, например, WinSock, можно рассматривать как дополнительные.

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

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

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

Для доступа к файлам в Windows можно пользоваться обычными потоками Си++ или функциями библиотеки Си. Но функции модуля Kernel имеют больше возможностей. Эти функции работают с файлами как с файловыми объектами. Например, они позволяют создавать файлы, отображаемые в памяти.

В отличие файлового доступа, библиотечных возможностей Си (функция malloc()) или Си++ (оператор new) для большинства приложений оказывается вполне достаточно. В Win32 эти вызовы автоматически преобразуются в соответствующие вызовы Win32 API. Приложения со специфическими требованиями к управлению памятью могут пользоваться более сложными функциями, например, для работы с виртуальной памятью (например, чтобы выделить блок адресного пространства размером несколько сотен мегабайт, но не передавать под него физическую память).

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

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

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

4.2 Функции пользовательского интерфейса (модуль User)

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

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

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

Перечислим еще несколько групп функций модуля User:

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

Приложения могут использовать функции User для проверки содержимого очередей сообщений, для получения и обработки сообщений, а также для посылки сообщений каким-либо окнам. Сообщения любому окну можно посылать либо синхронно (функция SendMessage), либо асинхронно (PostMessage). При асинхронной посылке сообщение просто помещается в очередь сообщений потока, который владеет окном-адресатом. В отличие от него, синхронное сообщение передается непосредственно в оконную процедуру окна-адресата. Функция SendMessage возвращает управление только после обработки сообщения окном-адресатом. Этот позволяет приложению-передатчику получить результат обработки перед продолжением выполнения.

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

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

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

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

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

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

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

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

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

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

  •  Стандартные элементы управления. Эти функции позволяют работать с элементами управления, появившимися в Windows 95.
  •  Стандартные диалоговые окна. Приложение может пользоваться стандартными окнами для открытия файла для чтения или записи, для выбора цвета из цветовой палитры, для выбора шрифта из набора установленных шрифтов, и некоторыми др. Эти диалоги можно использовать в стандартном виде или модифицировать их поведение путем замены оконных процедур.
  •  MAPI служит стандартным интерфейсом для доступа к различным программам электронной почты и электронных сообщений.
  •  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.

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


Лекция 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.

Окно новой версии 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, содержание оконной процедуры "по умолчанию" скрыто, т.к. она является частью операционной системы.

Обратите внимание, что приложение из п.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 )

    {

    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. Т.о. гарантируется, что функция 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).

Оконная процедура – это функция, ответственна за обработку всех сообщений, получаемых окном. Она может обрабатывать эти сообщения самостоятельно, или передавать их оконной процедуре "по умолчанию", 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 всегда есть заголовок, а у окон 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 хранит координаты наименьшего прямоугольника, описывающего область, нуждающуюся в перерисовке. Ограничивая отрисовку этой областью, приложения могут ускорить процесс отображения.

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. Сообщение уведомляет окно верхнего уровня о том, что оно станет активным или неактивным. При смене активного окна это сообщение сначала посылается окну, которое будет неактивным, а потом окну, которое станет активным.

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).

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

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

В программе 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:

    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, которая в зависимости от своего третьего логического параметра, или перемещает текущую позицию рисования в точку указателя мыши из последнего сообщения, или рисует в эту точку отрезок из текущей позиции. Чтобы узнать координаты указателя мыши от последнего сообщения, применяется функция 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!". Например, комментарии должны отмечать такие элементы программы, как цикл обработки сообщений, регистрация оконного класса или пояснять смысл отдельных функций API (назначение неизвестных функций API выясните в справочной системе).
  6.  Выясните, сохраняется ли содержимое окна 3-й и 4-й версии "Hello, World!" при перемещении окна приложения по экрану или при изменении его размеров. Объясните, что происходит.
  7.  Добавьте в 4-ю версию "Hello, World!" массив для хранения координат точек рисунка (в качестве типа элементов массива удобно использовать структуру из API POINT). Обеспечьте рисование содержимого окна при обработке сообщения WM_PAINT. Теперь при изменении размеров окно не должно очищаться.

Перед написанием функции-обработчика сообщения WM_PAINT проанализируйте содержимое функции DrawHello из п.3 и AddSegmentAtMessagePos из п.4.

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

MessageBox( hwnd, "Вы хотите завершить работу программы?",

"Завершение работы", MB_YESNO | MB_ICONQUESTION )

Этот вызов создает окно сообщения двумя кнопками "Да" и "Нет" и пиктограммой в виде вопросительного знака. В зависимости от нажатой кнопки, функция вернет IDYES или IDNO. Приложение должно завершать работу только при нажатии пользователем кнопки Да.

  1.  Изучите англо-русский словарь терминов по теме 2-й лекции (см. CD-ROM).


Лекция 3. Иерархия окон Windows. Типы окон

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

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

У каждого окна в Windows есть уникальный дескриптор окна (это число, которое можно рассматривать как имя окна, доступное и приложению, и самой Windows). Переменные для хранения оконных дескрипторов обычно имеют тип HWND.

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

1. Иерархия окон

Для управления окнами Windows хранит информацию о них в иерархической структуре, упорядоченной по отношению принадлежности. Принадлежность бывает двух типов: родительское/дочернее окно и владелец/собственное окно.

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

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

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

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

В Win32 API есть специальный набор функций для перебора окон в соответствии с их иерархией. Некоторые из этих функций перечислены ниже.

Функция GetDesktopWindow возвращает дескриптор окна рабочего стола.

Функция EnumWindows перебирает все окна верхнего уровня. При вызове приложение должно передать ей адрес функции обратного вызова. Эта функция будет вызываться изнутри EnumWindows для каждого окна верхнего уровня.

Функция EnumChildWindows перебирает все дочерние окна у заданного родительского окна. В процессе перебора вызывается пользовательская функция обратного вызова. EnumChildWindows при переборе учитывает порожденные дочерние окна, т.е. дочерние окна, которые сами принадлежат дочерним окнам заданного окна.

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

Функцию FindWindow можно применять для поиска окна верхнего уровня по заданному оконному классу или заголовку окна.

Функция GetParent возвращает дескриптор родительского окна для заданного дочернего окна.

Функция GetWindow предоставляет наиболее гибкий способ доступа к иерархии окон. В зависимости от второго параметра, uCmd, она может возвратить для заданного окна дескриптор родительского окна, окна-владельца, сиблинга, или дочернего окна.

2. Диалоговые окна

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

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

Диалоговые окна Windows делятся на два типа: модальные и немодальные.

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

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

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

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

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

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

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

Немодальное окно не возвращает никакого значения своему владельцу. Но при необходимости немодальное окно и его владелец могут взаимодействовать с помощью функции 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, причем только после того, как созданы все элементы управления, но перед тем, как окно будет выведено на экран. Поэтому процедура диалогового окна может корректно проинциализировать элементы управления до того, как их увидит пользователь.

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

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

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

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

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

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

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

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

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

Окно для сохранения файла (рис. 3.3) создается функцией GetSaveFileName. Параметром этой функции также является указатель на структуру OPENFILENAME.

3.2 Диалоговое окно выбора цвета 

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

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

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

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

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

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

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

Рис. 3.6. Диалоговое окно печати.

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

Диалоговое окно печати создается функцией 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 будет существовать в течение всего времени работы окон. Если эта переменная будет уничтожена до уничтожения диалогового окна, то приложение будет завершено вследствие недопустимой операции доступа к несуществующей области памяти.

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. Кнопка с независимой фиксацией может пребывать в одном из двух состояний: включенном или выключенном. Существует разновидность такой кнопки с тремя состояниями (третье – запрещенное). Кнопки с зависимой фиксацией часто используются в виде группы кнопок, позволяющей выбрать одно из нескольких взаимно исключающих состояний.

4.3 Элементы редактирования 

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

4.4 Окно списка 

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

4.5 Комбинированное окно списка 

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

4.6 Полосы прокрутки 

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

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

В Windows 95, по сравнению с предыдущими версиями Windows, был определен новый набор стандартных элементов управления (рис. 3.11).

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

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

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

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

Индикаторы заполнения позволяют проинформировать пользователя о ходе выполнения какой-либо длительной операции. Они служат только в информационных целях и не обрабатывают событий от пользователя.

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

Элемент редактирования сложного текста имеет больше возможностей, чем старый элемент редактирования. Этот элемент позволяет работать с файлами формата Microsoft RTF (Rich Text Format). По сути дела, этот элемент является текстовым редактором средней сложности.

Элемент "горячая клавиша" реагирует на нажатие пользователем определенной комбинации клавиш. Приложение может задать эту комбинацию с помощью сообщения WM_SETHOTKEY.

Среди других элементов управления Windows 95 можно назвать элементы "анимационный ролик", "заголовок", "панель инструментов", "подсказка" и др.

5. Резюме 

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

Окна в Windows упорядочены в иерархическую структуру по отношению принадлежности. В корне иерархии находится окно рабочего стола. Окна верхнего уровня – это такие окна, для которых родительским окном является рабочий стол, а также те, у которых нет родительского окна. У дочерних окон родительским окном является какое-либо окно верхнего уровня или другое дочернее окно. Окна с одним и тем же родительским окном называются сиблингами (окнами одного уровня). Порядок, в котором происходит отображение сиблингов, называется Z-порядком.

У окон верхнего уровня может быть окно-владелец. отличное от его родительского окна, а у дочерних окон окно-владелец и родительское окно одинаковы.

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

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

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

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

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

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

6. Упражнения.

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


Лекция 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 можно сказать, что без готовой библиотеки классов ООП дает весьма незначительное уменьшение количества исходного текста, который должен написать программист. Основные преимущества ООП проявляются при использовании библиотеки классов – т.е. набора повторно используемых компонент. Эти компоненты облегчают решение типичных задач, например, для добавления в приложение 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, чтобы обеспечить функционирование приложения в целом. Сейчас пока рано обсуждать детали архитектуры "документ/вид", но вы должны, как минимум, знать термин "документ/вид", часто упоминаемый при рассмотрении 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. Функции-члены классов можно вызывать только применительно к объектами этих классов, а функции каркаса приложения можно вызывать из любого места программы.

В табл. 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:

  afx_msg void OnPaint();

  DECLARE_MESSAGE_MAP()

};

#endif

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

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

#include "Hello.h"

CMyApp myApp;

// Функции-члены CMyApp

BOOL CMyApp::InitInstance()

{

  m_pMainWnd = new CMainWindow;

  m_pMainWnd->ShowWindow( m_nCmdShow );

  m_pMainWnd->UpdateWindow();

  return TRUE;

}

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

BEGIN_MESSAGE_MAP( CMainWindow, CFrameWnd )

  ON_WM_PAINT()

END_MESSAGE_MAP()

CMainWindow::CMainWindow()

{

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

}

void CMainWindow::OnPaint()

{

  CPaintDC dc( this );

  CRect rect;

  GetClientRect( &rect );

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

}

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

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

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.

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

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 )

Параметр 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 с помощью карты сообщений преобразует поступающие сообщения 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++ надо выполнить следующие действия:
  3.  Выберите команду FileNew и затем перейдите на закладку Projects.
  4.  Выберите Win32 Application и в строке ввода Project Name укажите имя проекта При необходимости измените путь к папке проекта. Затем нажмите кнопку OK.
  5.  Добавьте в проект файлы с исходным текстом: заголовочный файл Hello.h и файл реализации Hello.cpp. Для добавления каждого файла выбирайте команду FileNew, затем указывайте тип и имя файла. Убедитесь, что флажок Add To Project включен, так что этот файл будет добавлен в проект. Затем нажмите OK, и введите содержимое файла.
  6.  Выберите команду ProjectSettings и перейдите на закладку 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 или нет.

  1.  Прочитайте документ "Венгерская форма записи имен переменных и типы данных Windows" (см. CD-ROM). Какой тип имеют переменные с именами bRepaint, szMsg, nAge, cxLength, clrBtn? Запишите операторы описания этих переменных.
  2.  Откройте файл WinMain.cpp (хранится в папке \DevStudio\Vc\Mfc\Src) и разберитесь с функцией AfxWinMain по описанию из п.2.4 лекции. Зачем в ней нужен оператор goto?

Посмотрите исходный текст функций CWinApp::Run (файл AppCore.cpp) и CWinThread::Run (файл ThrdCore.cpp) и найдите, где именно вызываются OnIdle и ExitInstance.

  1.  В приложении Hello обеспечьте вывод символьной строки красным цветом внутри зеленого описывающего прямоугольника. В контексте устройства для задания цвета текста и фонового цвета предназначены функции-члены SetTextColor и SetBkColor. Значение цвета имеет тип COLORREF. Это значение можно сформировать с помощью макроса RGB(r, g, b), например, красный цвет записывается так: RGB(255, 0, 0).
  2.  На основе приложения Hello разработайте приложение, которое будет реагировать на следующие сообщения Windows:

WM_LBUTTONDOWN

щелчок левой кнопкой мыши

WM_RBUTTONDOWN

щелчок правой кнопкой мыши

WM_KEYDOWN

нажатие клавиши на клавиатуре

WM_MOVE

перемещение окна

WM_SIZE

изменение размеров окна

WM_NCRBUTTONDOWN

щелчок правой кнопкой мыши в неклиентской области окна

WM_CLOSE

закрытие окна

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

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

  1.  Изучите англо-русский словарь терминов по теме 4-й лекции (см. CD-ROM).


Лекция 5. Отображение информации с помощью модуля GDI

1. Контекст устройства

В однозадачных ОС (MS-DOS), любая программа может рисовать непосредственно на экране. В многозадачных ОС программы так действовать не должны, т.к. при одновременной работе нескольких программ пользователь должен видеть на экране согласованную картину, сформированную в результате их совместной работы. Экранная область, принадлежащая программе A, должна быть защищена от информации, выводимой программой B. Доступом к видеоадаптеру, как и к другим устройствам, управляет ОС. Она позволяет программам выводить информацию только в пределах их окон. В Windows графическое отображение выполняет модуль GDI.

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

MFC избавляет программиста от необходимости непосредственной работы с дескрипторами контекстов устройств. Дескриптор контекста устройства и функции рисования GDI инкапсулированы в класс "Контекст устройства" – CDC. От него унаследованы классы для представления различных контекстов устройств (см. табл. 5.1).

Таблица 5.1. Классы различных контекстов устройств

Имя класса

С чем связан этот контекст устройства

CPaintDC

Клиентская область окна (только в обработчиках OnPaint)

CClientDC

Клиентская область окна

CWindowDC

Окно, включая неклиентскую область

CMetaFileDC

Метафайл GDI (аналог макроса, в котором запоминаются вызовы функций GDI и потом их можно многократно воспроизводить)

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

CPaintDC dc(this);

// Вызовы каких-либо функций-членов для рисования примитивов

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

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

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

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

{

 CRect rect;

 GetClientRect(&rect);

 CClientDC dc(this);

 dc.MoveTo( rect.left, rect.top );

 dc.LineTo( rect.right, rect.bottom );

 dc.MoveTo( rect.right, rect.top );

 dc.LineTo( rect.left, rect.bottom );

}

В редких случаях программе требуется получить доступ ко всему экрану (например, в программе захвата экрана). Тогда можно создать контекст устройства как объект класса CClientDC или CWindowDC, но в конструктор передать нулевой указатель. Например, нарисовать окружность в левой верхней части экрана можно так:

CClientDC dc( NULL );

dc.Ellipse( 0, 0, 100, 100 );

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

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

Таблица 5.2. Основные атрибуты контекста устройства

Атрибут

Значение по умолчанию

Функция-член CDC для задания значения

Функция-член СDC для получения значения

Цвет текста

Черный

SetTextColor

GetTextColor

Цвет фона

Белый

SetBkColor

GetBkColor

Режим фона

OPAQUE

SetBkMode

GetBkMode

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

MM_TEXT

SetMapMode

GetMapMode

Режим рисования

R2_COPYPEN

SetROP2

GetROP2

Текущая позиция

(0,0)

MoveTo

GetCurrentPosition

Текущее перо 

BLACK_PEN

SelectObject

SelectObject

Текущая кисть

WHITE_BRUSH

SelectObject

SelectObject

Текущий шрифт

SYSTEM_FONT

SelectObject

SelectObject

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

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, удобно применять для рисования в одинаковом масштабе на различных устройствах вывода.

Таблица 5.3. Режимы преобразования координат, поддерживаемые модулем GDI

Константа для обозначения режима

Расстояние, соответствующее логической единице

Ориентация координатных осей

MM_TEXT

1 пиксел

x вправо, у вниз

MM_LOMETRIC

0.1 мм

x вправо, у вверх

MM_HIMETRIC

0.01 мм

x вправо, у вверх

MM_LOENGLISH

0.01 дюйма

x вправо, у вверх

MM_HIENGLISH

0.001 дюйма

x вправо, у вверх

MM_TWIPS

1/1440 дюйма (0.0007 дюйма)

x вправо, у вверх

MM_ISOTROPIC

Определяется пользователем (масштаб по осям x и y одинаков)

Определяется пользователем

MM_ANISOTROPIC

Определяется пользователем (масштаб по осям x и y задается независимо)

Определяется пользователем

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

Рис. 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 );

dc.DPtoLP( &point );

Функции LPtoDP и DPtoLP часто применяются при обработке сообщений мыши. Windows помещает в структуру сообщения координаты указателя  в физической системе координат. Поэтому, если вы ходите "нарисовать мышью" прямоугольник в режиме MM_LOMETRIC, то перед рисованием необходимо преобразовать координаты указателя из физических координат устройства в логические координаты контекста.

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

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

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

Предположим, требуется поместить начало логической системы координат в центр окна. Это можно сделать так (считая, что dc – объект подкласса CDC):

CRect rect;

GetClientRect( &rect );

dc.SetViewportOrg( rect.Width()/2, rect.Height()/2 );

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

Иногда бывает полезно узнать характеристики устройства, с которым связан контекст. Для этого предназначена функция CDC::GetDeviceCaps. Например, получить ширину и высоту экрана (например, 1024х768) можно так:

CClientDC dc( this );

int cx = dc.GetDeviceCaps( HORZRES );

int cy = dc.GetDeviceCaps( VERTRES );

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

Таблица 5.4. Часто используемые параметры функции GetDeviceCaps 

Параметр

Значение, возвращаемое функцией GetDeviceCaps

HORZRES

Ширина поверхности изображения, в пикселах

VERTRES

Высота поверхности изображения, в пикселах

HORZSIZE

Ширина поверхности изображения, в миллиметрах

VERTSIZE

Высота поверхности изображения, в миллиметрах

LOGPIXELSX

Количество пикселов на логический дюйм по горизонтали

LOGPIXELSY

Количество пикселов на логический дюйм по вертикали

NUMCOLORS

Для дисплея – количество статических цветов, для принтера или плоттера – количество поддерживаемых цветов

TECHNOLOGY

Получение битовых флагов, идентифицирующих тип устройства – дисплей, принтер, плоттер и др.

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);

Важная особенность всех функций 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)

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.

Рис.  5.3. Стили пера. 

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

Рис. 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 достаточно указать значение цвета:

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 );

Чтобы функция TextOut вместо явно указанных координат пользовалась текущей позицией, надо вызвать SetTextAlign с указанием стиля и установленным флагом TA_UPDATECP. Тогда TextOut после вывода каждой строки будет изменять текущую позицию. Так можно вывести несколько строк подряд с сохранением корректного расстояния между ними.

Таблица 5.7. Функции-члены CDC для вывода текста 

Функция

Описание

DrawText

Выводит текст с заданным форматированием внутри описывающего прямоугольника

TextOut

Выводит символьную строку в текущей или заданной позиции

TabbedTextOut

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

ExtTextOut

Выводит символьную строку с возможным заполнением описывающего прямоугольника фоновым цветом или изменением межсимвольного расстояния

GetTextExtent

Вычисляет ширину строки с учетом параметров текущего шрифта

GetTabbedTextExtent

Вычисляет ширину строки с табуляциями с учетом текущего шрифта

GetTextMetrics

Возвращает свойства текущего шрифта (высоту символа, среднюю ширину символа и т.п.)

SetTextAlign

Задает параметры выравнивания для функции TextOut и некоторых других функций вывода

SetTextJustification

Задает дополнительную ширину, необходимую для выравнивания символьной строки

SetTextColor

Задает в контексте устройства цвет вывода текста

SetBkColor

Задает в контексте устройства цвет фона, которым заполняется область между символами при выводе текста

Функции GetTextMetrics и GetTextExtent предназначены для получения свойств текущего шрифта, выбранного в контексте устройства. GetTextMetrics возвращает эти свойства в виде структуры TEXTMETRIC. GetTextExtent (или GetTabbedTextExtent) вычисляет в логических единицах ширину заданной строки с учетом текущего шрифта. Пример использования GetTextExtent – вычисление ширины промежутка между словами, чтобы равномерно распределить текст по заданной ширине. Допустим, надо вывести строку в участке шириной nWidth. Для выравнивания строки по обеим границам можно использовать следующие вызовы:

CString string = "Строка с тремя пробелами ";

CSize size = dc.GetTextExtent( string );

dc.SetTextJustification( nWidth - size.cx, 3 );

dc.TextOut( 0, y, string );

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

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

Все текстовые функции-члены CDC пользуются текущим шрифтом, выбранным в контексте устройства. Шрифт – это набор символов определенного размера (высоты) и начертания, у которых есть общие свойства, например, толщина символа (нормальная или жирная). В типографии размер шрифта измеряется в специальных единицах – пунктах. 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.

Таблица 5.8. Часто используемые стандартные объекты GDI

Объект

Описание

NULL_PEN

Пустое (прозрачное) перо

BLACK_PEN

Черное сплошное перо толщиной 1 пиксел

WHITE_PEN

Белое сплошное перо толщиной 1 пиксел

NULL_BRUSH

Пустая (прозрачная) кисть

HOLLOW_BRUSH

То же, что NULL_BRUSH

BLACK_BRUSH

Черная кисть

DKGRAY_BRUSH

Темно-серая кисть

GRAY_BRUSH

Серая кисть

LTGRAY_BRUSH

Светло-серая кисть

WHITE_BRUSH

Белая кисть

ANSI_FIXED_FONT

Моноширинный системный шрифт ANSI

ANSI_VAR_FONT

Пропорциональный системный шрифт ANSI

SYSTEM_FONT

Системный шрифт для пунктов меню, элементов управления и т.п.

SYSTEM_FIXED_FONT

Моноширинный системный шрифт (для совместимости со старыми версиями Windows)

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

CPen pen( PS_NULL, 0, (RGB (0, 0, 0)) );

dc.SelectObject( &pen );

CBrush brush( RGB(192, 192, 192) );

dc.SelectObject(&brush);

dc.Ellipse( 0, 0, 100, 100 );

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

dc.SelectStockObject( NULL_PEN );

dc.SelectStockObject( LTGRAY_BRUSH );

dc.Ellipse( 0, 0, 100, 100 );

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

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

Visual C++ может автоматически отслеживать объекты GDI, которые вы забыли удалить. Для этого применяется перегрузка оператора new. Чтобы разрешить такое слежение в конкретном исходном файле, после директивы включения заголовочного файла Afxwin.h надо добавить директиву определения макроса:

#define new DEBUG_NEW

После завершения работы приложения номера строк и имена файлов, содержащие не удаленные объекты GDI, будут показаны в отладочном окне Visual C++.

Для удаления объектов 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 для рисования пикселей и графических примитивов. Контекст устройства в 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).


Лекция 6. Работа с устройствами ввода. Использование меню

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

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

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

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

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

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

  •  нажатие или отпускание кнопки мыши;
  •  двойной щелчок кнопкой мыши;
  •  перемещение мыши.

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

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

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

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

Сообщение

Когда посылается

WM_LBUTTONDOWN

Нажата левая кнопка мыши

WM_LBUTTONUP

Левая кнопка мыши отпущена

WM_LBUTTONDBLCLK

Двойной щелчок левой кнопкой мыши

WM_MBUTTONDOWN

Нажата средняя кнопка мыши

WM_MBUTTONUP

Средняя кнопка мыши отпущена

WM_MBUTTONDBLCLK

Двойной щелчок средней кнопкой мыши

WM_RBUTTONDOWN

Нажата правая кнопка мыши

WM_RBUTTONUP

Правая кнопка мыши отпущена

WM_RBUTTONDBLCLK

Двойной щелчок правой кнопкой мыши

WM_MOUSEMOVE

Указатель мыши перемещается над клиентской областью окна

Сообщение WM_xBUTTONUP обычно (но не всегда) идет после WM_xBUTTONDOWN. Сообщения мыши посылаются в окно, над которым располагается указатель. Поэтому, если пользователь нажмет левую кнопку над клиентской областью некоторого окна, а отпустит ее за пределами окна, то это окно получит только сообщение WM_LBUTTONDOWN. Многие программы реагируют только на сообщения о нажатии кнопок мыши. Если важно обрабатывать и нажатие, и отпускание кнопки, приложение должно использовать режим "захвата" мыши (см. п.1.2).

Таблица 6.2. Макросы карты сообщений и имена обработчиков для сообщений мыши, связанных с клиентской областью окна.

Сообщение

Макрос карты сообщений

Имя функции-обработчика

WM_xBUTTONDOWN

ON_WM_xBUTTONDOWN

OnxButtonDown

WM_xBUTTONUP

ON_WM_xBUTTONUP

OnxButtonUp

WM_xBUTTONDBLCLK

ON_WM_xBUTTONDBLCLK

OnxButtonDblClk

WM_MOUSEMOVE

ON_WM_MOUSEMOVE

OnMouseMove

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

afx_msg void OnMsgName( UINT nFlags, CPoint point )

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

Параметр nFlags содержит состояние кнопок мыши и клавиш клавиатуры Shift и Ctrl в момент генерации сообщения. Состояние конкретной кнопки или клавиши можно извлечь из параметра nFlags с помощью операции побитового ИЛИ и масок, перечисленных в табл. 6.3.

Таблица 6.3. Возможные флаги, составляющие значение параметра nFlags

Битовый флаг

Когда флаг установлен

MK_LBUTTON

Нажата левая кнопка мыши

MK_MBUTTON

Нажата средняя кнопка мыши

MK_RBUTTON

Нажата правая кнопка мыши

MK_CONTROL

Нажата клавиша Ctrl

MK_SHIFT

Нажата клавиша Shift

Сообщения мыши, связанные с неклиентской областью, аналогичны описанным выше, только в именах констант добавляются символы NC (от слова nonclient). Например, вместо WM_LBUTTONDOWNWM_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 с дескриптором указателя в качестве параметра. Допустим, дескриптор указателя мыши хранится в переменной-члене 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 узнают о клавиатурных событиях так же, как и о событиях мыши: посредством сообщений. Как и мышь, клавиатура является ресурсом, который с помощью операционной системы разделяется между несколькими приложениями. Сообщения мыши посылаются окну, над которым находится указатель. Сообщения клавиатуры посылаются окну, находящемуся в "фокусе ввода". Такое окно может быть только одно.

При каждом нажатии или отпускании клавиши приложение в "фокусе ввода" получает сообщение. Если требуется знать, когда нажата или отпущена конкретная клавиша, например, 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

VK_CANCEL

Ctrl-Break

VK_END

End

VK_RETURN

Enter

VK_HOME

Home

VK_BACK

Backspace

VK_LEFT

стрелка влево

VK_TAB

Tab

VK_UP

стрелка вверх

VK_SHIFT

Shift

VK_RIGHT

стрелка вправо

VK_CONTROL

Ctrl

VK_DOWN

стрелка вниз

VK_MENU

Alt

VK_INSERT

Ins

VK_PAUSE

Pause

VK_DELETE

Del

VK_ESCAPE

Esc

VK_CAPITAL

Caps Lock

VK_SPACE

Spacebar

VK_NUMLOCK

Num Lock

VK_PRIOR

PgUp

VK_SCROLL

Scroll Lock

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

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

::GetKeyState( VK_SHIFT )

Она вернет отрицательное значение, если Shift нажата, и неотрицательное – если не нажата (признак нажатия обозначается старшим битом возвращаемого числа). Чтобы выполнить некоторые действия по комбинации клавиш Ctrl+стрелка влево, можно в обработчике OnKeyDown выполнить проверку:

if ( (nChar == VK_LEFT) && (::GetKeyState(VK_CONTROL) < 0) )

 {

   

 }

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

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

Часто в программах не требуется обрабатывать сообщения о нажатии/отпускании клавиш, но необходимо получать с клавиатуры символы в соответствии с состоянием клавиш Caps Lock, Shift и текущей раскладкой клавиатуры. В данном случае программа может обрабатывать сообщения WM_CHAR. Они генерируются Windows в результате обработки сообщений WM_KEYDOWN/WM_KEYUP системной функцией::TranslateMessage. Эта функция вызывается во внутреннем цикле обработки сообщений MFC.

Для обработки сообщения WM_CHAR надо занести в карту сообщений макрос ON_WM_CHAR и добавить в класс функцию-член OnChar со следующим прототипом:

afx_msg void OnChar( UINT nChar, UINT nRepCnt, UINT nFlags )

Назначение параметров nRepCnt и nFlags то же, что и у сообщений WM_KEYDONW/WM_KEYUP. Ниже приведен фрагмент исходного текста, обрабатывающий английские буквенные клавиши, клавишу Enter и Backspace:

// Фрагмент карты сообщений класса 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.
  •  Создать ресурс меню с помощью редактора ресурсов и загружать это меню во время выполнения программы. Этот метод используется наиболее часто.

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"). Ускоряющая клавиша  – это клавиша или комбинация клавиш, нажатие которой аналогично выбору команды меню.

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 Обновление состояния пунктов меню

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

В 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, но добавляет или удаляет маркер-окружность, а не метку в виде галочки. Обычно маркер применяется для отметки текущего выбора из группы взаимно исключающих команд, а метка – для выбора независимой команды.

3.5 Ускоряющие клавиши

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

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

IDR_MAINFRAME ACCELERATORS PRELOAD MOVEABLE

BEGIN

 "N",          ID_FILE_NEW,       VIRTKEY,CONTROL

 "Z",          ID_EDIT_UNDO,      VIRTKEY,CONTROL

 "X",          ID_EDIT_CUT,       VIRTKEY,CONTROL

 ...

 VK_DELETE,    ID_EDIT_CUT,       VIRTKEY,SHIFT

 ...

END

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

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

LoadAccelTable( MAKEINTRESOURCE( IDR_MAINFRAME ) );

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

LoadFrame( IDR_MAINFRAME, WS_OVERLAPPEDWINDOW, NULL, NULL );

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

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

Для вывода контекстного меню на экран можно использовать функцию CMenu::TrackPopupMenu:

BOOL TrackPopupMenu( UINT nFlags, int x, int y, CWnd* pWnd,

      LPCRECT lpRect = NULL )

Параметры 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() );

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).


Лекция 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

Класс MFC

Кнопка

BUTTON

CButton

Список

LISTBOX

CListBox

Элемент редактирования

EDIT

CEdit

Комбинированный список

COMBOBOX

CComboBox

Полоса прокрутки

SCROLLBAR

CScrollBar

Статический элемент

STATIC

CStatic

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

CButton m_wndPushButton;

m_wndPushButton.Create( "Запуск", WS_CHILD ¦ WS_VISIBLE ¦ BS_PUSHBUTTON,

  rect, this, IDC_BUTTON );

В этом примере создается нажимаемая кнопка (стиль BS_PUSHBUTTON), которая будет дочерним окном окна this и будет занимать в его клиентской области область rect. Целочисленный идентификатор IDC_BUTTON часто называется идентификатором дочернего окна или идентификатором элемента управления. В данном окне все дочерние ЭУ, на сообщения которых требуется реагировать, должны иметь уникальные идентификаторы.

ЭУ посылают окну-владельцу уведомления о событиях в виде сообщений WM_COMMAND. Смысл этих сообщений зависит от типа элемента, но в любом случае, уточняющая информация хранится в параметрах сообщения wParam и lParam. В них передается идентификатор ЭУ и код уведомления. Так, при нажатии нажимаемой кнопки она посылает сообщение 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 и др.).

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 );

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

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. Т.е. у списка будет рамка, вертикальная полоса прокрутки, он будет посылать уведомления родительскому окну при смене выделения или при двойном щелчке на элементе, и он будет сортировать элементы по алфавиту. По умолчанию полоса прокрутки включается, только если элементы не умещаются в области списка.

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

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

Определение длины строки элемента с заданным индексом

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

m_wndListBox.SelectString( -1, "Times" );

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

CString string;

int nIndex = m_wndListBox.GetCurSel();

if ( nIndex != LB_ERR )

 m_wndListBox.GetText( nIndex, string );

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

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

Таблица 7.3. Уведомления списка

Код уведомления

Когда посылается

Макрос карты сообщений

Необходим ли у списка стиль LBS_NOTIFY?

LBN_SETFOCUS

Список получил фокус ввода

ON_LBN_SETFOCUS

Нет

LBN_KILLFOCUS

Список потерял фокус ввода

ON_LBN_KILLFOCUS

Нет

LBN_ERRSPACE

Операция отменена из-за нехватки памяти

ON_LBN_ERRSPACE

Нет

LBN_DBLCLK

Двойной щелчок на элементе

ON_LBN_DBLCLK

Да

LBN_SELCHANGE

В спис