Создание приложений (С++, Win32)

Создание приложений (С++, Win32)

В 80-90-х годах XX века игрокодеры полагались только на свои навыки программирования под MS DOS (если игра разрабатывалась под эту ОС), стремясь любыми путями получить плавное движение игровой анимации. 1 . В те времена Windows являлась операционной системой, ориентированной на бизнес-приложения. Но никак не на игры. Ситуация изменилась с выходом ОС MS Windows 95 и появлением первых версий DirectX.

Под платформой Win32 подразумеваются все 32-разрядные (32-битные) операционные системы семейства MS Windows (9x/NT/2000/XP/Vista/7). На тему программирования под MS Windows написана не одна сотня книг (многие из которых на русском языке). Настоятельно рекомендуется прочитать хотя бы пару из них. Не факт, что всё усвоишь, но основные моменты всё равно запомнишь.

Программирование под Windows

Для создания приложений под ОС MS Windows необходимо:

  • Компьютер с установленной ОС Windows.
  • Компилятор языка.

В нашем случае C++. Удобнее использовать интегрированную среду разработки (ИСР, IDE), в состав которой входит нужный компилятор. Хороший пример - MS Visual C++ 2010. О том, где её взять и как установить читай здесь: Устанавливаем Microsoft Visual C plus plus 2010 Express.

  • Windows SDK (Software Development Kit).

Обычно всего устанавливается в наборе с MS Visual C++. Содержит необходимые заголовочные файлы и библиотеки для создания Windows-приложений. Компания Microsoft прилагает немало усилий для того, чтобы ОС Windows была удобной и стабильной платформой, доступной для программирования. Собственно для этого и был придуман Microsoft SDK, представляющий собой набор стандартов, на основе которых должны создаваться Windows-приложения. Одним из таких стандартов является стандарт оформления кода (Coding covention).

Стандарт оформления кода (Coding convention)

. от Microsoft весьма объёмен. Не нужно во всём строго ему следовать. Но рациональное зерно в нём есть. Цель данного стандарта - приучить программеров к единообразному объявлению переменных, однотипному написанию имён функций, указанию типов. Стандарт призван повысить читабельность исходного кода, который в крупных компаниях разрабатывается (и, следовательно, должен быть понят) группой программеров.

Венгерская нотация (Hungarian Notation)

Соглашения по написанию кода от Microsoft включают в себя свод указаний по объявлению переменных, названный впоследствии Венгерской нотацией (в честь своего венгерского создателя Чарльза Симоний (Charles Simonyi)). Этот без преувеличения гениальный метод предлагает обозначать имена переменных специальными префиксами, отражающими соответствующие типы данных. В литературе по Windows-программингу авторы стараются придерживаться Венгерской нотации. Но, тем не менее, строго ей не придерживаются. Вот далеко не полный список данных префиксов:

Префикс Тип данных Пример f Boolean BOOL fFlag; b Byte char bVariable; dw Double word (long) long dwValue; h 32-bit идентификатор экземпляра (handle) long hWindow; i Integer int iNumber; p Pointer void *pData; I Interface IUnknown *IInterface; g_ Global char g_GlobalVariable; m_ Member short m_MemberData;

Работа со "странными" типами данных Windows

Пожалуй самое сложное в изучении Windows-программировании - это усвоение специфических типов данных, встречающихся в каждом оконном приложении. 2 Один из наиболее часто встречающихся типов - handle, представляющий собой уникальный номер, присваиваемый окнам либо его объектам (значок, кнопка, заголовок и т.д.). Хэндлы очень важны, т.к. окна и его объекты часто перемещаются внутри системной памяти Windows, т.е. их адрес постоянно изменяется. Хэндл предоставляет объекту постоянную ссылку, не зависящую от реального расположения объекта в памяти. Хэндлы используются повсеместно в Win32 API. Помимо этого Win32 API предоставляет множество уникальных типов данных (data types), которые на первый взгляд вообще не похожи на типы данных. Их главной особенностью является то, что их имена всегда пишутся заглавными (большими) буквами. Поэтому их легко найти в исходном коде. Пример: RECT, HWND. Тип HWND представляет собой хэндл (handle) окна. Для того, чтобы начать работать типами данных Win32, в исходный код необходимо подключить специальный заголовочный файл:

(Идёт в наборе с любой IDE под Windows.) Наиболее часто применяемые типы данных Windows:

Макрос Описание BOOL Булево значение (TRUE или FALSE). BYTE 8-битное целое число без знака (unsigned int). DWORD 32-битное целое число без знака (unsigned long). LONG 32-битное целое число сознаком (signed long). LPARAM 32-битное значение, передаваемое в качестве параметра в оконную процедуру (window procedure) или функцию обратного вызова (callback function). WPARAM Значение, передаваемое в качестве параметра в оконную процедуру (window procedure) или функцию обратного вызова (callback function). LPCSTR 32-битный указатель на неизменяемую строку символов (constant character string). LPSTR 32-битный указатель на строку символов (character string). LPVOID 32-битный указатель на неуказанный тип (unspecified type). LRESULT 32-битное значение, возвращаемое из оконной процедуры (window procedure) или функции обратного вызова (callback function). UINT 32-битное беззнаковое целое в Win32 (unsigned integer). WNDPROC 32-битный указатель на оконную процедуру (window procedure). WORD 16-битное беззнаковое целое (unsigned short).

Базовое приложение Windows

Базовое приложение:

  • Проект, содержащий в себе минимальную, необходимую для работы (простого отображения окна) функциональность.
  • Имеет минимальный объём исходного кода. Часто весь исходный код содержится в единственном файле (традиционно он называется Main.cpp или WinMain.cpp).
  • Служит "отправной точкой" для создания любых других приложений (включая компьютерные игры) путём добавления в него различных функций, переменных, классов и т.д.
  • Иногда в литературе его также называют шаблоном Windows-приложения.

Для создания базового приложения в MS Visual C++ (все версии) существуют специальные мастера (wizards). Для вызова списка этих мастеров достаточно создать новый проект, выбрав в Главном меню IDE Файл->Создать->Проект. После ввода имени Проекта и нажатия <ОК>, Мастер приложений последовательно покажет окно приветствия и окно Параметры приложения, в котором (для автоматической генерации исходного кода) в разделе Дополнительные параметры необходимо снять флажок с пункта "Пустой проект". Вся процедура (на примере создания консольного приложения) неплохо описана здесь . После этого созданный Проект можно просмотреть, отредактировать или сразу отправить на компиляцию (F5).

Генерируемый MS Visual C++ исходный код базового приложения далёк от идеала (он состоит из 4 и более файлов, включает в себя дополнительные аргументы, "крайне необходимые" по мнению разработчиков IDE) и не является единственно правильным. Более того, исходный код базового приложения сильно разнится в разных изданиях и учебных курсах. Все эти версии, в общем-то, равноценны (по быстродействию, эффективности и т.д.). Главное здесь - чтобы код был минимален по объёму и доступен для понимания даже начинающим программистам. Та же ситуация с оконными базовыми приложениями (только вариантов кода здесь значительно больше). Во всех случаях рекомендуется начинать новый проект с "Пустого проекта" и потом уже самостоятельно добавлять в него необходимые исходные и заголовочные файлы.

Все приложения Windows делятся на 2 основных типа:

  • Консольные приложения. Внешне они выглядят как программы с текстовым интерфейсом, но способны обращаться к большинству функций Windows. Практически не изменились со времён MS-DOS: всё те же серые строки символов на чёрном фоне. В OS Windows консольные приложения обычно запускают из Командной строки (Пуск->Все программы->Стандартные->Командная строка), которая тоже является консольным приложением. В MS Windows 98 Командная строка называется Сеанс MS-DOS.
  • Оконные приложения. Собственно, для чего и задумывалась MS Windows, начиная с её первой версии. Окна могут изменять свои размеры, приобретать и терять фокус, перекрывать друг друга. Также они имеют стандартные атрибуты: кнопки "Свернуть", "Развернуть", "Закрыть"; строку заголовка; строку состояния и т.д. Все современные игры для ОС MS Windows также являются оконными приложениями (как правило, развёрнутыми на весь экран).
Консольное базовое приложение

Исходный код простейшего консольного приложения содержится в одном файле (в нашем случае это Main.cpp) и выглядит так:

Да, это весь код.)) Приложение, ведь, ничего не делает. Нам даже не понадобилось подключать заголовочные файлы с помощью директивы #include. Скомпилируем его. Для этого:

  • Запустим MS Visual C++ 2010 (подойдут и другие версии этой IDE) и cоздадим новый проект, выбрав в Главном меню IDE Файл->Создать->Проект.

О том, где взять и как установить MS Visual C++ 2010 Express читай в статье Устанавливаем Microsoft Visual C plus plus 2010 Express.

  • В окне "Создать проект" выбираем мастер: "Консольное приложение Win32", в строке "Имя" пишем название проекта (в нашем случае Test01). Строки "Расположение" и "Имя Решения" заполняются автоматически (при необходимости изменяем). Жмём "OK", "Далее".
  • На странице "Параметры приложения": оставляем "Консольное приложение" и отмечаем пункт "Пустой проект". Жмём "Готово".

Проект создан. Так как это "Пустой проект", он не содержит в себе никаких файлов. В левой части расположен "Обозреватель решений". Если его нет, в главном меню выбираем: Вид->Другие окна->Обозреватель решений. Или комбинация горячих клавиш Ctrl+Alt+L. В Обозревателе решений видна древовидная структура Проектов, входящих в данное Решение. Чуть ниже названия Проекта видим специально заготовленные папки (в MSVC++2010 они называются "фильтры") для файлов Проекта: Таблица 1

  • Правой кнопкой мыши по фильтру "Файлы исходного кода" -> во всплывающем меню выбираем: Добавить -> Создать элемент.
  • В окне "Добавление нового элемента" отмечаем "Файл C++ (.cpp)" и в строке "Имя" вводим Main. Жмём "Добавить".

Добавленный файл Main.cpp сразу же открывается в редакторе кода.

  • Последовательно вводим все строки исходного кода из Листинга 1, представленного чуть выше (без номеров строк на сером фоне). Настоятельно рекомендуется вводить код вручную (не с помощью копировать/вставить), - только так можно научиться программировать (лучше запоминаешь структуру исходного кода, ключевые слова C++ и, конечно, привыкаешь ставить символ ";" в конце каждой строки).
  • Запускаем компиляцию (кнопка с зелёным треугольником на Панели инструментов или <F5> на клавиатуре).

Скомпилированная программа при запуске в среде Windows на долю секунды покажет чёрное окно консоли и сразу закроется.

Немного изменим нашу программу, добавив возможность выводить на экран текстовое сообщение:

Комментарии при компилировании отбрасываются, так что, в принципе, они могут быть любыми. Комментируй всё, что только можно в своём коде, - и ты сам, и твои последователи скажут тебе потом спасибо.

  • Снова компилируем Проект.

По окончании компиляции мы также лишь на мгновение увидим окно программы, так и не увидев текстовую строку. Чтобы её всё-таки увидеть, запустим программу через интерпретатор командной строки, который есть во всех версиях MS Windows:

  • Пуск->Все программы->Стандартные->Командная строка.
  • Проводником открываем каталог с исполняемым (Test01.exe) файлом программы, расположенном в нашем случае по адресу: C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\Test01\Debug.
  • Перетаскиваем мышью Test01.exe в открытое окно Командной строки и жмём <Enter>.

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

Оконное базовое приложение (WinAPI)

Вся суть Windows-программирования заключается в двух шагах:

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

Исходный код простейшего оконного приложения:

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

Что мы и сделаем. Один из вариантов исходного кода выглядит так (реально компилируется в MSVC++2010 при указании в настройках Проекта пункта "Набор символов: Использовать многобайтовую кодировку"):

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

Пример простейшего оконного приложения

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

Создаём Проект WinTest01
  • Стартуй MS Visual C++ 2010, если не сделал этого раньше (подойдут и другие версии этой IDE).
  • Создай новый Проект, выбрав в Главном меню IDE Файл->Создать->Проект.

О том, где взять и как установить MS Visual C++ 2010 читай в статье Устанавливаем Microsoft Visual C plus plus 2010 Express.

  • В окне "Создать проект" выбираем: "Проект Win32", в строке "Имя" пишем название проекта (в нашем случае WinTest01). Строки "Расположение" и "Имя Решения" заполняются автоматически (при необходимости изменяем). Жмём "OK", "Далее".
  • На странице "Параметры приложения": оставляем "Приложение Windows" и отмечаем пункт "Пустой проект". Жмём "Готово".

Проект создан. Так как это "Пустой проект", он не содержит в себе никаких файлов. В левой части главного окна MS Visual C++ 2010 расположен "Обозреватель решений". (Если его нет, в главном меню выбираем: Вид->Другие окна->Обозреватель решений. Или комбинация горячих клавиш Ctrl+Alt+L.) В Обозревателе решений видна древовидная структура Проектов, входящих в данное Решение. Чуть ниже названия Проекта видим специально заготовленные папки (в MSVC++2010 они называются "фильтры") для файлов Проекта (см Таблицу 1).

  • В фильтр "Файлы исходного кода" добавляем файл WinMain.cpp, точно так же, как это описано выше для консольного приложения.

Добавленный файл WinMain.cpp сразу же открывается в редакторе кода.

  • Вводим весь исходный код Листинга 3, показанного выше в только что созданный WinMain.cpp.

Можно просто всё скопировать и вставить.

  • Сохрани Решение (Файл -> Сохранить все).
Готовим Проект WinTest01 к компиляции

Для успешной компиляции Проекта WinTest01 изменим пару его свойств.

Устанавливаем многобайтовую кодировку (multibyte encoding)

В MS Visual C++ 2010 в настройках по умолчанию стоит набор (кодировка) символов UNICODE. В MS Visual C++ 6.0 - напротив, по умолчанию стоит кодировка ANSI (многобайтовая). Данная настройка сильно влияет на типы используемых переменных, что приводит к заметным различиям в исходном коде. Несмотря на то, что во всех случаях рекомендуется использовать кодировку UNICODE, поддерживаемую во всех современных ОС семейства MS Windows (начиная с Win 2000/XP), большинство книг по программированию игр на классическом C++ придерживаются именно многобайтовой кодировки. Чтобы сильно не переделывать исходные коды под UNICODE, все наши игровые Проекты мы настроим под многобайтовую кодировку. Для этого.

  • В Обозревателе решений щёлкаем по названию только что созданного Проекта WinTest01.
  • Во всплывающем меню выбираем "Свойства"
  • В появившемся окне установки свойств Проекта жмём "Свойства конфигурации", в правой части в строке "Набор символов" выставляем значение "Использовать многобайтовую кодировку".
  • Жмём ОК.

Отключаем инкрементную компоновку (incremental linking) Иначе при копиляции возможны ошибки вроде этой:

  • В Главном меню MS Visual C++ 2010 выбираем Проект -> Свойства.
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации -> Компоновщик -> Общие (Configuration Properties -> Linker -> General), в правой части в строке "Включить инкрементную компоновку" ставим значение "Нет (/INCREMENTAL:NO)".
  • Жмём ОК.
  • Сохрани Решение (File -> Save All).
Компилируем проект WinTest01
  • Скомпилируй Проект/Решение (кнопка с зелёным треугольником на Панели инструментов MSVC++ или <F5> на клавиатуре).

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

Если попытаться запустить скомпилированный .exe-файл на компьютере без установленной MSVC++2010, то экзешник (скорее всего) "ругнётся" на отсутствующую библиотеку MSVCR100D.dll. Буква D в её имени означает Debug, т.е. данное приложение скомпилировано с отладочной конфигурацией (профилем), которая выставляется по умолчанию у каждого вновь создаваемого Проекта/Решения. При релизе игры приложение напротив, компилируют с конфигурацией Release (релиз). При этом при создании инсталлятора в дистрибутив с игрой добавляют т.н. набор библиотек времени выполнения (в нашем слчае MS Visual C++ 2010 Runtime Redistributable). Он устанавливается вместе с игрой, т.к. без него игра тупо не стартанёт. Если для Release-профиля такой набор нетрудно найти даже на сайте Microsoft (например здесь: https://www.microsoft.com/en-us/download/details.aspx?id=5555 , 4,8 Мб для 64-разрядной версии ОС), то для запуска Debug-версии на отдельном компе на него потребуется установить целую MSVC++2010. Всё так сложно в том числе с целью не допустить утечек предрелизных разработок с компов игрокодерских компаний. Релиз есть релиз. А с дебаг-версиями, как правило, работают только сами игрокодеры, на своих компах отлавливая ошибки. Конфигурации Debug и Release легко переключаются на странице свойств открытого Проекта (в MSVC++2010 в главном меню выбираем Проект->Свойства, в верхней части диалога видим всплывающий список "Конфигурация").

Исследуем код WinMain.cpp

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

  • WinMain - аналог функции Main, обязательной для каждого консольного приложения. Является точкой входа в программу.
  • WindowProc - называется оконной процедурой (название WindowProc, в принципе, выбрано произвольно, но желательно чтобы оно было "говорящим"). Обеспечивает реальную функциональность приложения.

На эту тему неплохо написано здесь: http://gamesmaker.ru/programming/c/vvedenie-v-winapi-chast-pervaya-sozdanie-okna/ . Остальные небольшие функции реализуют реакцию программы на различные сообщения. Весь процесс создания оконного Windows-приложения состоит из нескольких шагов:

  1. Регистрируем класс окна в ОС MS Windows. Для этого объявляем структуру типа WNDCLASSEX, заполняем должным образом её поля, после чего вызываем функцию RegisterClassEx.
  2. Создаём на базе зарегистрированного класса окна конкретный экземпляр окна (в нашем случае "GameClass") при помощи функции CreateWindowEx.
  3. Выводим окно на экран при помощи функции ShowWindow. При необходимости вызываем оконную процедуру для обновления рабочей области функцией UpdateWindow.
  4. Запускаем цикл выборки сообщений.

Но обо всём по порядку. В начале Листинга 3 первое, что мы делаем - это указываем пару макроопределений и подключаем заголовочные файлы:

Определения (define - от англ. "определить") специальных макросов перед включением файла windows.h служат для:

  • STRICT обеспечивает более строгую проверку типов. Например, при определённом STRICT компилятор выдаст сообщение об ошибке при присваивании объекта типа HBRUSH объекту типа HCURSOR. Если же STRICT не определён, то никакой ошибки компилятор не увидит. Таким образом можно обезопасить себя от некоторых распространённых ошибок, связанных с присваиванием объекту одного типа значения объекта другого типа (обычно вследствие невнимательности).
  • WIN32_LEAN_AND_MEAN уменьшает количество используемых компонентов и тем самым сокращает время на компиляцию и конечный размер исполняемого файла.
Заголовочные файлы и директива include

Заголовочный файл windows.h является обязательным для любой Windows-программы. Для предыдущих версий ОС MS Windows он содержал огромное количество объявлений типов, прототипов функций и т.д. Сейчас он включает в себя лишь ссылки на другие заголовочные файлы (их достаточно много). Налицо децентрализация и разбиение исходного кода на несколько независимых файлов-модулей. В MSVC++ есть расширенная версия windows.h - windowsx.h. В нём также содержится множество полезных макросов и, в частности, распаковщики сообщений. Например он нужен для смены цвета фона окна средствами WinAPI. Но в нашем случае в соответствующей строке стоит NULL:

. т.к. "рисовать" в окне будет DirectX. Поэтому директива подключения windowsx.h в WinMain.cpp отсутствует. Оба этих "заголовка" расположены в одном из подкаталогов установленной MS Visual C++. Так, например, если открыть windows.h с помощью блокнота или любого другого текстового редактора, в нём можно обнаружить ссылку на другой заголовочный файл windef.h. В нём определены практически все специальные типы Windows, многие из которых встречаются в Листинге 3. Там также видим определения:

Функции CALLBACK и WINAPI будут вызываться с помощью __stdcall - стандартного метода вызова функций Win32, представляющего собой нечто среднее между __cdecl и __pascal (традиционные методы для MS-DOS-программ). Часто типы, определённые в windef.h являются просто новыми именами для старых, хорошо знакомых типов. Например:

С другими, специфичными для Windows типами, такими как HWND, HCURSOR, HBRUSH и многими другими, всё сложнее.

Функция WinMain
  • Вызывается операционной системой и является точкой входа в программу (Вообще она вызывается некоей стартовой функцией из стандартной библиотеки C/C++, но сейчас это неважно).
  • Представляет собой аналог функции main, которую используют при создании консольных приложений.
  • Возвращает целое значение. В случае наличия цикла выборки сообщений это должно быть поле структуры msg.wParam. Если его нет, то возвращается 0 (или не 0).
  • Её прототип (шаблон с указанием типов аргументов) выглядит так:
  • Спецификатор WINAPI в заголовке WinMain указывает на то, что используется способ вызова, принятый в Win32 API. Способы вызова отличаются, например, порядком передачи аргументов (их соответствие принятым в ОС соглашениям очень важно).

''Таблица 3. Параметры функции WinMain"

Параметр Значение HINSTANCE hInst Дескриптор текущего экземпляра приложения. Имеет тип HINSTANCE. HINSTANCE hPrevInst Дескриптор предыдущего экземпляра приложения (если есть). Имеет тип HINSTANCE. В приложениях Win32 всегда равен NULL. LPSTR lpzCmdLine Содержит командную строку запущенного приложения (исключая название этого приложения), со списком дополнительных аргументов (если есть). Имеет тип LPSTR (в конце всегда должен стоять символ "0"). int nCmdShow Указывает на способ отображения главного окна программы.

Дескриптор экземпляра представляет собой уникальное значение и служит своеобразным идентификатором для поиска данной программы среди других запущенных приложений. Всякий раз при запуске оконного приложения Windows автоматически присваивает ему идентификатор экземпляра (instance handler), с помощью которого можно обращатся к процессу, созданному данным приложением. Самое ценное, что даёт идентификатор экземпляра - это возможность запускать несколько копий одного и того же приложения одновременно. При этом к ним надо как то обращаться. Обращение к ним как раз и проиходит через идентификатор экземпляра. Windows-приложение может получать опции командной строки (command-line options; прямо как DOS-приложения). Оно получает их в виде указателя на строку (string pointer) lpCmdLine, которая парсится. В то же время параметры в Windows-приложениях довольно редки. Последний параметр int nCmdShow может принимать следующие значения: Таблица 4

Значение Описание SW_SHOW Окно активируется, используя текущие значения размера и положения. SW_MAXIMIZE Окно развёрнуто на весь экран (максимизировано). SW_MINIMIZE Окно свёрнуто (минимизировано) и активировано следующее окно. SW_SHOWMINIMIZED Окно сворачивается (отображается в виде кнопки на Панели задач). SW_SHOWMAXIMIZED Окно разворачивается на весь экран. SW_RESTORE Восстанавливает исходные размеры окна как после минимизирования, так и после разворачивания на весь экран. SW_HIDE Окно скрывается и активизируется другое окно. SW_SHOWNORMAL Окно активизируется и отображается на экране. Если оно было развёрнуто или свёрнуто, то восстанавливается прежнее состояние. По умолчанию именно этот флаг используется при первом вызове функции ShowWindow(). SW_SHOWNA Окно отображаетося как есть (с текущими установками размера и положения на экране). Данный флаг никак не влияет на окно, активное в данный момент.

На данном этапе мы заполнили данные, контролирующие выполнение приложения. После этого идёт процесс создания окна.

Заполняем структуру оконного класса

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

Сразу после объявления структуры (WNDCLASSEX wndClass;), начинаем заполнять все 12 её полей.

Структура данных WNDCLASSEX является немного расширенной версией своей предшественницы - WNDCLASS. В WNDCLASSEX появилось всего 2 новых элемента: cbSize (для указания размера структуры; здесь почти всегда стоит вызов функции sizeof(WNDCLASSEX)) и hIconSm (для указания малого значка приложения).

В Листинге 3 структура данных (data structure) WNDCLASSEX характеризует класс приложения, на базе которого будет создано окно программы:

Прототип структуры данных WNDCLASSEX выглядит так:

Напомню, что тип данных UINT - это Unsigned Integer (беззнаковое целое). Это означает, что числа данного типа не могут быть отрицательными. Рассмотрим описание полей структуры WNDCLASSEX: Таблица 5

Поле структуры Назначение поля cbSize Беззнаковое целое, в котором содержится размер структуры в байтах. В нашем случае в значении стоит служебная функция sizeof, самостоятельно подсчитывающая размер всей структуры wndClass. style Беззнаковое целое, указывающее на стиль класса окна. Несколько стилей могут объединяться логической операцией ИЛИ (символ прямой вертикакльной черты "|"). Два самых распространённых стиля - CS_HREDRAW и CS_VREDRAW указывают на необходимость перерисовки окна при изменении его ширины и высоты. Стиль CS_DBLCLKS позволяет окну, созданному на базе этого класса, обрабатывать двойные щелчки мыши. Полный список значений смотри в Таблице 6. lpfnWndProc Указатель на оконную процедуру (в нашем случае она называется WindowProc). Окна, созданные на базе одного класса, используют одну и ту же оконную процедуру. Именно поэтому прототип оконной процедуры был объявлен ПЕРЕД функцией WinMain. cbClsExtra, cbWndExtra Дополнительные параметры оконного класса и окна соответственно. В теории здесь указывается число байт, дополнительно ("экстренно") выделяемых оконному классу и его окну соответственно. На практике данные флаги, в общем-то, нигде в программировании не используются. И в нашем случае оба параметра выставляем в 0. hInstance Дескриптор окна приложения, внутри которого располагается оконная процедура для оконного класса. Значение берётся из первого параметра функции WinMain. hIcon, hIconSm Дескрипторы значков класса окна (большого и малого). Малый значок отображается в заголовке окна и на кнопке в панели задач. Для загрузки ресурсов в обоих случаях используется функция LoadIcon, где первый параметр - дескриптор приложения, второй - имя ресурса, содержащего значок (иконку). В нашем случае дескриптор равен NULL. Значит в этом случае будут использоваться стандартные значки. Возможные значения второго праметра для стандартных ресурсов: IDI_APPLICATION, IDI_ASTERISK, IDI_ERROR, IDI_EXCLAMATION, IDI_HAND, IDI_INFORMATION, IDI_QUESTION, IDI_WARNING, IDI_WINLOGO. hCursor Дескриптор курсора (указателя) мыши класса окна. Для загрузки соответствующего ресурса используется функция LoadCursor с параметрами, аналогичными LoadIcon. Если первый параметр равен NULL, то в качестве второго параметра могут использоваться стандартные ресурсы указателей мыши: IDC_ARROW, IDC_UPARROW, IDC_CROSS, IDC_IBEAM, IDC_APPSTARTING, IDC_WAIT, IDC_HELP, IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE. hbrBackground Дескриптор кисти для заливки фона окна. В нашем случае стоит значение GetStockBrush(BLACK_BRUSH), которое заливает клиентскую область окна чёрным цветом. lpszMenuName Указатель на строку с именем ресурса меню (lpsz означает, что эта строка всегда должна оканчиваться нулём). В нашем случае он равен NULL, то есть окно будет без меню. lpszClassName Указатель на имя вновь создаваемого класса окна (lpsz означает, что эта строка всегда должна оканчиваться нулём). В нашем случае GameClass. Его мы передадим в качестве второго параметра функции CreateWindowEx.

Таблица 6. Возможные значения параметра style структуры WNDCLASSEX 3

Значение Описание CS_BYTEALIGNCLIENT Выравнивает клиентскую область окна (window's client area; content area) на основании байтовой границы (byte boundary) по оси X. Это влияет на положение окна по горизонтали и на его ширину (width). Используется редко. CS_BYTEALIGNWINDOW Выравнивает окно на основании байтовой границы (byte boundary) по оси X. Это влияет на положение окна по горизонтали и на его ширину (width). Используется редко. CS_CLASSDC Говорит Windows выделять ресурсы прорисовки всем окнам данного класса. Используется при создании DirectX-приложений. Данный стиль выделяет (allocates) единый контекст устройства (device context), который будет применяться во всех окнах, созданных на основе данной структуры оконного класса. В общих чертах, в ОС Windows можно создавать несколько оконных классов одного типа в разных потоках (threads). В то время, как в ОС могут одновременно существовать несколько копий оконного класса, возможна ситуация, когда несколько потоков попытаются одновременно получить доступ к общему контексту устройства. При использовании данного стиля, доступ к контексту устройства в данный момент времени получает только 1 поток, в то время как остальные блокируются, ожидая своей очереди. CS_DBLCLKS Здесь всё просто. При указании данного стиля, всякий раз, когда пользователь сделает двойной щелчок мышью в области окна, ОС Windows будет посылать сообщение о двойном щелчке мышью (double click). Сперва это может показаться ненужным, но многие приложения замеряют время каждого щелчка мышью в области окна для определения двойных щелчков, вместо того, чтобы просто использовать данный стиль. CS_GLOBALCLASS Данный стиль разрешает создание глобального класса окна. В игрокодинге не используется. Более подробная информация по данному стилю есть в MSDN. CS_HREDRAW Заставляет перерисовывать клиентскую область окна всякий раз при изменении его ширины. В программировании DirectX-приложений не применяется. CS_VREDRAW Заставляет перерисовывать клиентскую область окна всякий раз при изменении его высоты. В программировании DirectX-приложений не применяется. CS_NOCLOSE Скрывает кнопку с крестиком в верхнем правом углу тулбара окна. CS_OWNDC Выделяет отдельный уникальный контекст устройства для каждого окна, создаваемого на основе данного оконного класса. CS_PARENTDC При указании данного стиля дочернее окно (child window) имеет общую область отсечения (clipping area) с родительским окном (parent window). Это позволяет дочернему окну рисовать в родительском окне. Но это не значит, что дочернее окно использует контекст устройства родительского окна. Вместо этого дочернее окно получает свой собственный контекст устройства из системного пула. Данный флаг применяется в основном для увеличения быстродействия приложения. CS_SAVEBITS Сохраняет в памяти картинку (bitmap) области, расположенной под окном. Данный флаг препятствует отправке сообщения WM_PAINT любым окнам, расположенным под данным окном.

В некоторых источниках по DirectX-программированию структуру оконного класса не расписывают так подробно, выставив "декоративные" параметры в NULL:

При создании окна обычного (не DirectX) приложения структура оконного класса будет выглядеть по-другому:

Регистрация оконного класса

После заполнения полей структуры WNDCLASSEX происходит её регистрация путём вызова функции RegisterClassEx, где в качестве параметра стоит указатель на имя уже заполненной структуры (wndClass):

Сама функция "обрамлена" условным переходом if, которая возвращает FALSE в случае неудачи и досрочно завершает работу программы.

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

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

Создание окна

Создание окна осуществляется с помощью функции CreateWindow. Вот её прототип:

Сравним этот прототип с реальной функцией CreateWindow из Листинга 3:

В комментариях всё подробно объясняется. Но отметим несколько моментов:

  • Третий параметр DWORD dwStyle (в нашем случае он имеет значение WS_OVERLAPPEDWINDOW) указывает на основной стиль окна.

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

Значений может быть несколько. В этом случае их разделяют знаком логического ИЛИ (символ прямой черты "|"). Так вот, этот параметр может принимать следующие значения: Таблица 7

Значение параметра Описание WS_BORDER Создание окна с рамкой. WS_CAPTION Создание окна с заголовком (невозможно использовать одновременно со стилем WS_DLGFRAME). WS_CHILD Окно является дочерним. Запрещено использовать вместе со стилем WS_POPUP. WS_CHILDWINDOW Окно является дочерним. Запрещено использовать вместе со стилем WS_POPUP. WS_CLIPSIBLINGS Используется совместно со стилем WS_CHILD для отрисовки в дочернем окне областей клипа, перекрываемых другими окнами. WS_CHILDWINDOW Создание дочернего окна (невозможно использовать одновременно со стилем WS_POPUP). WS_POPUP Создает popup-окно (невозможно использовать совместно со стилем WS_CHILD. WS_POPUPWINDOW Создает popup-окно, имеющее стили WS_BORDER, WS_POPUP, WS_SYSMENU. WS_CLIPCHILDREN Исключает область, занятую дочерним окном, при отрисовке элементов в родительском окне. WS_DISABLED Создает окно, которое недоступно (любой пользовательский ввод запрещён). WS_DLGFRAME Создает окно с двойной рамкой, без заголовка. WS_GROUP Назначает окно в качестве первого в группе окон. Используется для переключения между окнами с помощью клавиши Tab. Все последующие окна данного класса определяется в эту же группу. WS_HSCROLL Создает окно с горизонтальной полосой прокрутки. WS_MAXIMIZE Создает окно максимального размера. WS_MAXIMIZEBOX Создает окно с кнопкой развертывания окна. WS_MINIMIZE Создает первоначально свернутое окно (используется только со стилем WS_OWERLAPPED). WS_ICONIC Создает первоначально свернутое окно (используется только со стилем WS_OWERLAPPED). WS_OVERLAPPED Создает перекрывающееся окно (которое, как правило, имеет заголовок и WS_TILED рамку). WS_OVERLAPPEDWINDOW Создает перекрывающееся окно, имеющее стили WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, WS_MAXIMIZEBOX. WS_TILED Тоже самое что WS_OVERLAPPEDWINDOW. WS_MINIMIZEBOX Создает окно с кнопкой свертывания. WS_SYSMENU Создает окно с кнопкой системного меню (можно использовать только с окнами имеющими строку заголовка). WS_TABSTOP Определяет элементы управления, переход к которым может быть выполнен по клавише TAB. WS_THICKFRAME Создает окно с рамкой, используемой для изменения размера окна. WS_SIZEBOX Пользователь может изменять размеры окна. Используется совместно с WS_THICKFRAME. WS_VISIBLE Создает первоначально неотображаемое окно. WS_VSCROLL Создает окно с вертикальной полосой прокрутки.

Поэкспериментируй с исходным кодом из Листинга 3, подставляя различные значения этого параметра и отправляя после этого код на перекомпиляцию. В нашем случае здесь стоит значение WS_OVERLAPPEDWINDOW.

У функции CreateWindow есть "расширенная" версия - CreateWindowEx, в которую добавлен всего один дополнительный параметр т.н. "улучшенного" стиля окна DWORD dwExStyle. Последний может принимать более 20 различных параметров, описание которых нетрудно найти в Интернете и MSDN. В игрокодинге улучшенные стили окна не применяются. Поэтому наш выбор - функция CreateWindow.

  • В четвёртом и пятом параметрах (int x, int y) в качестве координаты левого верхнего угла указывается точка (0,0). Это необходимо для того, чтобы окно занимало весь экран.
  • В шестом и седьмом параметрах (int nWidth, int nHeight) указывается ширина и высота окна. В нашем случае эти размеры совпадают с шириной и высотой экрана (т.к. у нас окно развёрнуто на весь экран). Узнаём ширину и высоту экрана с помощью вспомогательной функции int GetSystemMetrics(int nIndex), получающей в качестве параметра специальные макросы SM_CXSCREEN (для ширины) или SM_CYSCREEN (для высоты), указывающие, что нужно узнать ширину и высоту в точках экрана первичного монитора (Primary display).

В результате успешного выполнения функция CreateWindow возвращает дескриптор типа HWND. В случае неудачи возвращается NULL. Именно поэтому, сразу после описания функции CreateWindow, идёт проверка с условным оператором if:

Если по окончании выполнения дескриптор приложения равен NULL, то досрочно завершаем выполнение программы.

Окно создано, но на экране оно не появится до тех пор, пока не будет вызвана функция ShowWindow (HWND hWnd, int nCmdShow):

В первом параметре она получает дескриптор вновь созданного окна (hWnd), а во втором - одну из констант с префиксом SW_ из Таблицы 4, указывающую на способ отображения окна. Но чаще всего здесь просто указывают параметр nCmdShow. Функция UpdateWindow заставляет окно обновить свою клиентскую область всякий раз, когда происходит перемещение окна или изменяются его размеры.

Конвейер (цикл) выборки сообщений (Message pump)
  • Является "сердцем" любой Windows-программы.
  • Может строиться по-разному (часто на основе всевозможных ветвящихся алгоритмов).
  • Конструируется (объявляется) внутри функции WinMain.
  • В нашей программе выглядит так:

Здесь наше приложение входит в бесконечный цикл, ожидая сообщения от ОС Windows. Как только они поступают, то через функции GetMessage или PeekMessage переправляются в оконную процедуру для обработки. В самом "сердце" цикла выборки сообщений расположена функция PeekMessage(). Вот её прототип:

Последний параметр здесь может принимать следующие значения:

Значение параметра Описание PM_REMOVE Удалять сообщение из очереди. PM_NOREMOVE Не удалять сообщение из очереди.

Функция PeekMessage вернёт ненулевое значение, если в очереди есть сообщения. Если фильтра нет, то будут получены все сообщения, при наличии фильтра только по фильтру. Также вместо неё можно применить аналогичную по действию функцию GetMessage().

Функция GetMessage() является блокирующей. То есть она ожидает пока не будет получено сообщение из очереди. До этого момента программа стоит. Что делать, если надо просто проверить есть ли сообщение в очереди? (Например, чтобы занять программу в этот момент другими действиями, например, упаковкой базы данных.) Для этого и есть функция PeekMessage(). Она проверяет есть ли в очереди сообщение и программа выполняется дальше.

Обрати внимание, что GetMessage() вернет WM_QUIT даже при наличии фильтра (оператора switch. case). MS Windows является событийно-ориентированной операционной системой. Она устроена так, что на протяжении всего времени "жизни" запущенного приложения оно постоянно "бомбардируюется" всевозможными сообщениями, информирующими обо всех изменениях, происходящих в рабочем окружении ОС (сдвинулся курсор мыши, запустили другое приложение и т.д.). Эти сообщения выбираются из очереди сообщений (Message queue), которую ОС организует для каждой программы, а затем передаются в оконную процедуру (в нашем случае это функция WindowProc) для обработки. БОльшая часть из этих сообщений абсолютно "не интересна" нашему приложению и они передаются обратно Windows для так называемой обработки по умолчанию. Чтобы сообщение было получено программой, в наличии должны иметься 2 компонента:

  • Точка назначения (участок кода, которому собственно и передаются сообщения). В нашем случае это оконная процедура WindowProc). Именно она получает в качестве аргумента сообщение, адресованное окну, с которым она связана (+ параметры этого сообщения).
  • Механизм передачи (программа должна сама организовать выборку сообщений из всей очереди). Может быть синхронным и асинхронным:

Таблица 9. Виды механизмов (способов) передачи сообщений

Синхронный Реализуется в случае вызова оконной процедуры непосредственно операционной системой Windows. Яркий пример - функция UpdateWindow, вызывающей оконную процедуру, и передавая ей в качестве параметра сообщение WM_PAINT (оно требует прорисовать рабочую область окна). Другой способ послать оконной процедуре синхронное сообщение - воспользоваться функцией SendMessage (см. её прототип сразу под этой таблицей). Она посылает указанное сообщение в окно, определяемое дескриптором hWnd, и НЕ возвращает управление до тех пор, пока не произойдёт возврат из оконной процедуры. Синхронность в данном случае подразумевает возможность предсказать, в какой момент времени (точнее, после какого оператора) произойдёт обработка сообщения. Асинхронный Реализуется при помощи очереди сообщений, организуемой для каждого приложения (точнее, для каждого потока приложения, который может принимать пользовательский ввод). ОС MS Windows помещает в эту очередь все сообщения. Лишь небольшая их часть может относиться к работающей программе. Приложение выбирает из этой очереди адресованные ему сообщения и обрабатывает их по мере поступления.

БОльшую часть сообщений в очередь помещает операционная система (например, в ответ на действия пользователя). Но есть функции, позволяющие это сделать и самому приложению. Одна из них PostMessage. Вот её прототип:

Разница между SendMessage и PostMessage состоит в том, что:

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

В нашем случае в этом цикле стоит функция PeekMessage. В конце этого цикла стоят две функции со следующими прототипами: BOOL TranslateMessage(CONST MSG *lpMsg) LONG DispatchMessage(CONST MSG *lpMsg) Обе они получают в качестве параметра адрес структуры типа MSG, поля которой заполнены предварительным вызовом функции PeekMessage.

  • TranslateMessage обычно "транслирует" сообщения, связанные с пользовательским вводом (нажатие клавиш, перемещение мыши) в символьные данные (character data).

Как только сообщение было транслировано в символьные данные, его можно "запостить" в очередь сообщений (message queue):

  • DispatchMessage вызывает оконную процедуру, передавая ей информацию о сообщении, полученную при помощи функции PeekMessage. Функция передаёт (помещает) оттранслированные сообщения в очередь сообщений оконной процедуры приложения.

Сам цикл выборки сообщений "обрамлён" циклом while do else, где в условии стоит ненаступление события WM_QUIT, прерывающего выполнение цикла и завершающего работу приложения, что и делает оператор return (msg.wParam).

Оконная процедура и обработка сообщений

Из-за используемой Windows схемы "Не зови меня, я сама тебе позову" нам необходимо оснастить наше приложение процедурой выборки сообщений (window message procedure более известная как оконная процедура), которая будет принимать входящий поток сообщений. Если функция WinMain представляет своего рода "мотор" программы, то оконная процедура наделяет приложение настоящей функциональностью. Именно в ней обрабатываются сообщения, переданные с помощью функции DispatchMessage. Большинство приложений отличаются друг от друга главным образом реакцией на сообщения, которые им посылает ОС MS Windows. Вот прототип оконной процедуры (название произвольное; в нашем случае - WindowProc):

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

Внутри неё видим единственный оператор switch. А всё потому, что всё, что делает данная функция, это "просматривает" различные типы сообщений, которые в неё указаны, и проверяет, не совпадает ли какое-либо из этих сообщений с передаваемым. В нашем случае мы проверяем всего 1 сообщение на совпадение - WM_DESTROY. Получив данное сообщение, наше приложение вызывает внутреннюю функцию Windows PostQuitMessage, закрывающую его окно. То же самое происходит и при нажатии кнопки закрытия в правом верхнем углу окна приложения (в виде крестика). При этом код сообщения передаётся в оконную процедуру в качестве параметра uMsg. Обычная (созданная нами) оконная процедура обрабатывает отдельные (указанные в ней) сообщения. А те, что не обрабатывает сама, передаёт в оконную процедуру по умолчанию, которая имеет прототип:

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

В начале Листинга 3 при заполнении структуры класса приложения мы произвели присваивание:

Благодаря этому MS Windows знает оконную процедуру класса и пересылает в неё все адресуемые окну сообщения. Каждое сообщение сопровождается двумя параметрами с типами WPARAM, LPARAM. Они содержат упакованные в них характеристики сообщений. В Win32 оба эти параметра имеют размер 32 бита. Встроенные в ОС MS Windows распаковщики сообщений избавляют нас от необходимости работать с данными параметрами напрямую.

📎📎📎📎📎📎📎📎📎📎