Пишем окно для windows
На этом я заканчиваю обсуждение теории, лежащей в основе программирования для Windows. Пришло время написать вашу первую программу для Windows. Во всех, рассматриваемых в этой книге примерах, я использую компилятор Microsoft Visual C++ 6.0. Если у вас еще нет этой программы, я настоятельно рекомендую пойти и приобрести ее.
Начало работы с Visual C++ 6.0
Давайте продолжим. Запустите вашу копию Visual C++ 6.0. Вы должны увидеть экран, аналогичный представленному на рис. 2.2.
Рис. 2.2. Интерфейс Visual C++ 6.0
В интерфейсе, представленном на рис. 2.2, нет ничего особенного. Основные элементы управления сосредоточены в верхнем меню: File , Edit , View , Insert и т.д. Все, что вы делаете в Visual C++, должно быть связано с проектом . Проект — это верхний уровень иерархии в среде разработки.
Как создать проект
Лучший способ обучения — практика, так что давайте создадим новый проект, для чего щелкнем по меню File и выберем пункт New . Вам будет предоставлено множество вариантов, изображенных на рис. 2.3.
Я храню весь исходный код в папке C:\Source\. Рекомендую вам создать аналогичную структуру каталогов, где будут храниться исходные тексты ваших программ. Лучше хранить весь код в единственном месте, чем разбрасывать его по всему жесткому диску. |
Рабочее пространство — это второй уровень иерархии в среде разработки. В начале цепочки расположен проект, затем следует рабочее пространство. Файлы вашего проекта расположены в конце цепочки. В одном проекте может быть несколько рабочих пространств, точно также как в одном рабочем пространстве может быть несколько исходных файлов. | Ресурсы удобны, но не стоит включать в программу слишком много их. Если поступать так, полученные исполняемые файлы будут слишком большими. Это вызовет проблемы, когда для пользователей будет выпускаться обновление исполняемых файлов. Вместо того, чтобы загрузить компактный исполняемый файл, им придется загружать огромный файл, полный ресурсов. |
Вы не обязаны давать исходному файлу и проекту одинаковые имена. Я делаю это только ради организации информации. Не стесняйтесь называть файл с исходным кодом, как вы желаете. (Некоторые люди используют имя Main.) | Когда вы создаете новый файл для проекта к нему добавляется требуемое расширение. Вам не надо вводить его. Например, если вы создадите файл с исходным кодом C++ и укажете имя Main, на жестком диске он будет сохранен как файл Main.cpp. | Если вам непонятно ключевое слово UINT — оно обозначает целое число без знака. Это означает, что переменная может принимать только положительные значения. | ||
Значение | Действие | |||
CS_BYTEALIGNCLIENT | Выравнивает клиентскую область окна (область с содержимым) по границе байта в направлении оси X. Оказывает влияние на позицию окна по горизонтали и на его ширину. Лично я никогда не использовал этот стиль. | |||
CS_BYTEALIGNWINDOW | Выравнивает окно по границе байта в направлении оси X. Оказывает влияние на границу окна по горизонтали и на его ширину. Этот стиль я также не использую. | |||
CS_CLASSDC | Размещает в структуре класса единый контекст устройства для использования всеми окнами. Я пока не рассказывал о контекстах устройств, но сделаю это позже, так что нет причин для волнения. В Windows можно создать несколько классов окон одного и того же типа в разных потоках. Так как у вас может быть несколько копий класса, несколько потоков могут попытаться использовать контекст устройства одновременно. Это вызовет блокировку всех потоков, кроме одного, пока этот поток не завершит работу с контекстом. | |||
CS_DBLCLKS | Очень простой стиль. Когда он указан, Windows будет посылать вашему окну сообщение о двойном щелчке каждый раз, когда пользователь выполняет двойной щелчок кнопкой мыши в пределах области окна. Это может показаться глупым, но многие приложения запоминают время каждого щелчка кнопки мыши, чтобы определить был ли выполнен двойной щелчок. Я, чтобы добиться того же, просто использую данный стиль. | |||
CS_GLOBALCLASS | Этот стиль разрешает создание глобального класса окна. Чтобы получить дополнительную информацию, обратитесь к документации, поставляемой с Microsoft Visual C++. При разработке игр нет необходимости использовать данный стиль. | |||
CS_HREDRAW | Этот стиль заставляет перерисовывать все окно в случае изменения его ширины. | |||
CS_NOCLOSE | Запрещает выполнение закрытия окна через системное меню. | |||
CS_OWNDC | Выделяет уникальный контекст устройства для каждого окна, созданного с использованием класса. | |||
CS_PARENTDC | Устанавливается, когда дочернее окно использует ту же самую область отсечения, что и родительское. Это позволяет дочернему окну рисовать на родительском. Это не означает, что потомок использует контекст устройства родителя. В действительности потомок получает свой собственный контекст устройства из системного пула. Единственное, что делает этот флаг — увеличение производительности приложения. | |||
CS_SAVEBITS | Сохраняет в памяти растровое изображение находящейся под окном области экрана. В дальнейшем при перемещении окна скрытая область копируется на экран. Это предотвращает отправку сообщения WM_PAINT всем окнам, которые были скрыты данным. | |||
CS_VREDRAW | Заставляет перерисовывать все содержимое окна в случае изменения высоты окна. |
Третий член структуры имеет тип WNDPROC, является указателем на функцию и называется lpfnWndProc. Помещаемый сюда указатель должен указывать на функцию обработки сообщений Windows, которую окно использует чтобы принимать сообщения. Это очень важно, и функция, на которую ссылаются здесь должна полностью соответствовать прототипу, приведенному в моем коде. Взгляните на рис. 2.9, чтобы увидеть взаимоотношения между классом и обработчиком сообщений.
Рис. 2.9. Взаимоотношения между классом окна и обработчиком сообщений
Четвертый член структуры относится к типу int и называется cbClsExtra. Это целое число задает количество байт, которые будут выделены сразу за структурой данных класса окна. Я не имею ни малейшего представления, для чего это может быть нужно, поскольку во всех примерах, которые я видел, это значение устанавливалось равным 0. Я рекомендую вам поступать точно так же. Если желаете, можете просто игнорировать этот член данных, поскольку система сама по умолчанию присваивает ему нулевое значение.
Пятый элемент также относится к типу int и называется cbWndExtra. Это целое число задает количество байтов, которые будут выделены сразу за экземпляром окна. Работа с ним аналогична обращению с предыдущим членом структуры, и система также по умолчанию присваивает ему нулевое значение.
Шестой член структуры имеет тип HANDLE и называется hInstance. Дескриптор, который вы задаете здесь, является дескриптором экземпляра к которому относится окнонная процедура класса. В большинстве случаев можно задать значение дескриптора hInstance, получаемого функцией WinMain() в одном из параметров.
Седьмой параметр называется hIcon и имеет тип HICON. Тип HICON это ни что иное, как замаскированный тип HANDLE, поэтому он не должен смущать вас. Данный дескриптор указывает на класс значка, используемого окном. Класс значка в действительности является ресурсом значка. Не присваивайте этой переменной значение NULL, поскольку если сделать так, то программа будет перерисовывать изображение значка, при каждом свертывании окна.
Если вы еще раз взглянете на код, то увидите, что для инициализации седьмого параметра я использую вызов функции LoadIcon().
Функция LoadIcon() загружает ресурс значка из исполняемой программы. Хотя ресурсы компилируются внутрь вашей программы для Windows, вам все равно необходимо загружать их, поэтому и требуется вызов данной функции. Вот как выглядит ее прототип:
К счастью, у этой функции всего два прарметра — HINSTANCE и LPCTSTR. Первый параметр с именем hInstance, содержит дескриптор экземпляра модуля чей исполняемый файл содержит значок, который вы желаете использовать. В моих примерах программ я присваиваю этому параметру значение NULL. Делая так вы разрешите использование встроенных значов Windows, которые часто также называют стандартными значками.
Второй параметр является указателем на строку, содержащую имя загружаемого значка. Взглянув на разбираемый пример, вы увидите, что для инициализации данного параметра я использую константу IDI_APPLICATION. Ее значение соответствует значку, используемому по умолчанию для приложений, который вы могли видеть во многих программах для Windows. Остальные стандартные значения перечислены в таблице 2.3.
Таблица 2.3. Константы для стандартных значков
Вот и все, что можно сказать о функции LoadIcon() . Теперь вернемся к разбираемой программе. Ох, подождите, прежде чем продолжить, взгляните на рис. 2.10, где изображены некоторые стандартные значки.
Рис. 2.10. Некоторые стандартные значки Windows. ©2002 Microsoft, All Rights Reserved.
Восьмой член структуры данных WNDCLASSEX очень похож на седьмой, за исключением того, что он задает используемый окном курсор. Его тип HCURSOR , а имя — hCursor . Тип HCURSOR это еще один замаскированный дескриптор. Обычно здесь указывается значение дескриптора класса курсора, который будет использован в вашей программе для ее собственного курсора. Но вместо того, чтобы создавать нестандартный курсор, я воспользуюсь функцией LoadCursor() .
Функция LoadCursor() похожа на функцию LoadIcon() за исключением того, что она загружает ресурсы курсора, а не ресурсы значка. Вот ее прототип:
Первый параметр называется hInstance , и содержит дескриптор экземпляра модуля, чей исполняемый файл содержит курсор, который вы собираетесь использовать. В своем примере я присваиваю данному параметру значение NULL . Это позволяет использовать встроенные курсоры Windows. Их также часто называют стандартными курсорами. Не чувствуете ли вы, что уже читали что-то похожее?
Второй параметр — это указатель на строку, содержащую имя загружаемого курсора. Как можно видеть, в рассматриваемом примере я присваиваю этому параметру значение IDC_ARROW . Оно соответствует стандартному курсору Windows, который вы могли видеть во многих программах. Оставшиеся стандартные значения перечислены в таблице 2.4.
Таблица 2.4. Константы для стандартных курсоров
После того, как вы освоились с относящейся к курсорам частью структуры данных окна, пришло время поговорить о цвете фона. Чтобы задать цвет фона вашего окна, вы просто указываете желаемый цвет в члене данных hbrBackground . Это девятый элемент структуры, и он имеет тип HBRUSH . Возможно, вы подумали, что тип HBRUSH это всего лишь дескриптор класса кисти. Но этот член данных более гибкий и позволяет задать как дескриптор кисти, которая будет использоваться, так и значение используемого цвета. Существует одно требование — задавая цвет, вы должны использовать одно из следующих значений:
- COLOR_ACTIVECAPTION
- COLOR_APPWORKSPACE
- COLOR_BACKGROUND
- COLOR_BTNFACE
- COLOR_BTNSHADOW
- COLOR_BTNTEXT
- COLOR_CAPTIONTEXT
- COLOR_GRAYTEXT
- COLOR_HIGHLIGHT
- COLOR_HIGHLIGHTTEXT
- COLOR_INACTIVEBORDER
- COLOR_INACTIVECAPTION
- COLOR_MENU
- COLOR_MENUTEXT
- COLOR_SCROLLBAR
- COLOR_WINDOW
- COLOR_WINDOWFRAME
- COLOR_WINDOWTEXT
В рассматриваемом примере я не использую этот метод. Но если вы захотите установить для фона один из стандартных цветов, просто замените задающую фоновый цвет строку кода примера на ту, которая приведена ниже:
В результате выполнения этого кода фон окна будет окрашен в серый цвет. Вы можете заменить COLOR_GRAYTEXT на любое из перечисленных выше значений. В моем коде вместо этого используется другой метод, включающий вызов функции GetStockObject() .
Функция GetStockObject() часто используется для того, чтобы получить дескриптор одной из встроенных кистей, шрифтов, палитр или перьев. Дело в том, что в Windows есть несколько предопределенных типов, которые могут быть использованы в ваших приложениях. Давайте взглянем на прототип функции:
Если вам нравятся функции у которых только один параметр, то вот одна из них. Ее единственный параметр представляет собой целое число идентифицирующее предопределенный объект системы, который вы хотите использовать. Если вы укажете значение, соответствующее существующему объекту, то возвращенное функцией значение не будет равно нулю.
Моей программе посчастливилось использовать встроенный объект WHITE_BRUSH . Он окрашивает фон окна в белый цвет. В таблице 2.5 приведены еще несколько значений, которые можно использовать в качестве аргумента функции GetStockObject() .
Таблица 2.5. Предопределенные объекты Windows
Десятый член структуры данных WNDCLASSEX — это завершающаяся нулевым символом строка с именем lpszMenuName . Она содержит имя ресурса меню, используемого окном. В моих окнах нет меню, поэтому я присваиваю данной переменной значение NULL .
Одиннадцатый член структуры данных также является строкой, которая должна завершаться нулевым символом. Его имя — lpszClassName . Как сказано в имени, эта строка используется для задания имени класса окна. Имя класса является уникальным идентификатором типа класса. Поэтому очень важно, чтобы вы не использовали заданное здесь имя для других классов вашей программы.
Двенадцатый, и последний, член структуры WNDCLASSEX — это переменная с именем hIconSm . Она аналогична члену данных hIcon , за исключением того, что здесь задается используемый программой маленький значок. В моем примере для этого применяется уже знакомая вам функция LoadIcon() .
Ну что? Мы завершили изучение структуры данных WNDCLASSEX ! Теперь пришло время зарегистрировать ее.
Функция RegisterClassEx()
Сейчас мы достигли момента, когда класс окна должен быть зарегистрирован. Ниже приведен фрагмент кода, выполняющий это действие.
Вызов функции RegisterClassEx() необходим, чтобы потом мы смогли создать окно. Эта функция регистрирует класс в системе Windows. Если класс не зарегистрирован, то вы не сможете использовать его для создания окна.
Функция очень проста. Вот ее прототип:
Первый и единственный необходимый для нее параметр является указателем на структуру данных WNDCLASSEX . Здесь все просто, — в нашем примере достаточно только передать функции значение &wndclass .
Не следует слишком волноваться из-за того, что функция возвращает значение типа ATOM . Важно только проверить равно возвращаемое функцией значение NULL или нет. Если возвращаемое функцией RegisterClassEx() значение не равно нулю, ее выполнение завершилось успешно.
Итак, класс окна зарегистрирован, и программа переходит к действительному созданию окна.
Функция CreateWindowEx()
Для создания окна используется функция CreateWindowEx() . Ее можно применять для создания дочерних, всплывающих или перекрывающихся окон. При создании окна вы указываете используемый класс, имя приложения и некоторые другие параметры. Прототип функции выглядит следующим образом:
Первый параметр имеет тип DWORD и называется dwExStyle . Он похож на определяющий стиль член структуры WNDCLASSEX , но задает дополнительные стили окна. Список доступных дополнительных стилей приведен в таблице 2.6.
Таблица 2.6. Дополнительные стили окна
В рассматриваемом примере я указываю в первом параметре дополнительный стиль WS_EX_OVERLAPPEDWINDOW . Благодаря использованию этого стиля, окно программы будет выглядеть подобно большинству приложений Windows.
Второй параметр представляет собой завершающуюся нулевым символом строку, содержащую имя класса для создаваемого окна. Все, что необходимо сделать — указать то же имя, которое вы использовали при инициализации члена lpszClassName структуры WNDCLASSEX . В моем примере я использую строку «Window Class».
Третий параметр — это еще одна завершающаяся нулевым символом строка. Вместо того, чтобы определять имя класса, она задает текст, который будет выведен в заголовке окна. Вы можете назвать свою программу как пожелаете, а я свою назвал «Create Window Example».
Четвертый параметр, dwStyle , позволяет задать различные комбинации стилей для вашего окна. Доступные стили перечислены в таблице 2.7.
Таблица 2.7. Cтили окна
В программе CreateWindow в этом параметре я указываю стиль WS_OVERLAPPEDWINDOW . Это придает окну вид, похожий на большинство других приложений Windows.
Пятый параметр функции CreateWindowEx() — это целое число, задающее позицию окна на экране по горизонтали. Если вы создаете дочернее окно, координата отсчитывается относительно координаты по горизонтали родительского окна. Поскольку в примере нет дочерних окон, используемое значение координаты, 0, приведет к размещению окна вплотную к левой границе экрана.
Шестой параметро задает позицию окна на экране по вертикали. Подобно позиции по горизонтали, позиция по вертикали представляет координату на экране только в том случае, если окно не является дочерним. Для дочернего окна это значение задает смещение относительно позиции по вертикали родительского окна. Чтобы лучше понять эту концепцию взгляните на рис. 2.11.
Значение | Описание |
SW_HIDE | Окно скрыто и затем окно активируется. |
SW_MAXIMIZE | Окно развернуто на весь экран. |
SW_MINIMIZE | Окно свернуто и затем окно активируется. |
SW_RESTORE | Восстанавливает окно из свернутого или развернутого состояния. Это полезно, если требуется вернуть окну его первоначальные размеры и местоположение. |
SW_SHOW | Активирует окно и отображает его. |
SW_SHOWMAXIMIZED | Окно активируется и отображается развернутым на весь экран. |
SW_SHOWMINIMIZED | Окно активируется и отображается свернутым. |
SW_SHOWNA | Окно отображается в его текущем состоянии. Не оказывает никакого влияния если окно в данный момент активно. |
SW_SHOWNORMAL | Отображает окно в его нормальном состоянии. Это значение используется, когда функция ShowWindow() вызывается в первый раз. |
Хотя есть много значений, которые вы можете указать во втором параметре, простейший способ заключается в передаче значения целочисленной переменной iCmdShow , которая является одним из параметров функции WinMain() . Именно так я и поступаю в коде примера.
Итак, ваша программа полностью завершила свою основную задачу — отображение окна. Однако, она пока не может принимать никаких входных данных. Здесь в игру вступает код цикла обработки сообщений.
Получение сообщений функцией GetMessage()
Чтобы приложения Windows знали когда их окна закрываются, перемещаются или изменяют размеры, они должны принимать сообщения Windows. В общем случае ваши приложения для Windows должны всегда иметь цикл сообщений. Вспомните сведения о событиях и цикле сообщений, которые я приводил в начале главы. В примере я использую простейший метод проверки наличия сообщений и обработки любых обнаруженных сообщений.
Чтобы проверить наличие ожидающих обработки сообщений вызывается функция GetMessage() . Вот ее прототип:
В первом параметре функция ожидает указатель на объект MSG . Структура данных MSG содержит всю информацию о любом обнаруженном сообщении.
Второй параметр указывает для какого окна проводится проверка наличия сообщений. Он необходим потому, что ваша программа может управлять несколькими окнами. В рассматриваемом примере есть только одно окно, поэтому я просто передаю дескриптор окна, созданного функцией CreateWindow() .
Третий параметр задает нижнюю границу кодов получаемых сообщений. Мы хотим получать все сообщения, поэтому присваиваем данному параметру значение 0.
Четвертый параметр позволяет задать верхнюю границу кодов получаемых сообщений. Поскольку мы хотим получать все сообщения, значение этого параметра также равно 0.
Трансляция сообщений функцией TranslateMessage()
Перед тем, как отправить сообщение в вашу очередь сообщений, вы должны транслировать его в символьные данные. Это делает функция TranslateMessage() . Для нее требуется единственный параметр — указатель на транслируемое сообщение. В примере я передаю функции адрес переменной, содержащей сообщение.
После того, как сообщение транслировано в символьные данные, вы можете поместить его в очередь сообщений с помощью функции DispatchMessage() .
Помещение сообщений в очередь функцией DispatchMessage()
Подобно функции TranslateMessage() , функция DispatchMessage() требует единственный параметр. Цель этой функции — отправить прошедшее трансляцию сообщение в очередь сообщений программы. После вызова этой функции сообщение попадает в функцию обработки сообщений.
Последняя строка кода функции WinMain() возвращает значение wParam последнего сообщения Windows извлеченного функцией получения сообщений. Как говорил поросенок Порки, «Вот и все, ребята!».
Функция обработки сообщений
Вы вероятно задаетесь вопросом, где обрабатываются сообщения. Когда функция DispatchMessage() отправляет сообщения, они направляются функции обработки сообщений, указанной в классе окна. В рассматриваемом примере все сообщения отправляются написанной мной функции fnMessageProcessor() .
Взгляните на код функции обработки сообщений, и вы увидите, что она представляет собой одну инструкцию switch средних размеров. Это вызвано тем, что единственная задача данной функции — перебрать все типы сообщений, которые интересуют нас и проверить, соответствует ли полученное сообщение одному из них.
Мой пример проверяет сообщения WM_CREATE , WM_DESTROY и WM_PAINT . Дополнительная работа производится только для сообщения WM_DESTROY .
Когда приложение получает сообщение WM_DESTROY , оно вызывает функцию PostQuitMessage() , которая завершает работу программы Windows. Это происходит, когда вы завершаете работу, щелкнув по кнопке закрытия окна.
Если полученное сообщение не соответствует ни одному из проверяемых программой, оно возвращается функции WinMain() с помощью функции DefWindowProc() . Это стандартная практика, поэтому я не рекомендую пытаться изменить ее.
Рассматриваемый пример проверяет лишь несколько типов сообщений, но примеры, встечающиеся дальше в этой книге проверяют гораздо больше сообщений. Помните об этом, когда будете просматривать код последующих примеров — вы можете столкнуться с абсолютно незнакомыми типами сообщений.
Компиляция и выполнение кода
Итак, ваша программа целиком введена и вы готовы идти дальше. Чтобы скомпилировать программу в Visual C++ 6.0 вам необходимо сделать активным окно с компилируемой программой и нажать клавишу F7 . Если все закончится успешнно, то в окне с выходными данными компилятора вы увидите следующий текст:
Если во время компиляции будут обнаружены какие-либо ошибки, заново проверьте код и убедитесь, что все набрано правильно. Когда будут исправлены все ошибки, вы сможете запустить программу, нажав комбинацию клавиш Ctrl+F5 . В результате начнет выполняться программа активного проекта. На рис. 2.13 показано, как должна выглядеть работающая программа рассматриваемого примера.
Рис. 2.13. Результат выполнения программы CreateWindow