- games maker Все о создании игр и не только
- Введение в WinAPI. Часть первая. Создание окна.
- Первая программа на WinAPI
- Типы данных в WinAPI
- Соглашения по вызову (Calling Conventions) — __stdcall
- Описатели/дескрипторы (Handles) в WinAPI
- WinAPI окна
- Функция WinMain
- Классы окон (Window Classes)
- Первая WinAPI программа — Пустое окно
- Главный цикл WinAPI
- Очередь сообщений (Message Queue) в WinAPI
- Оконная процедура (Window Procedure) WindowProc
- Заключение
games maker Все о создании игр и не только
18 января 2010 в 22:06
Введение в WinAPI. Часть первая. Создание окна.
Наконец-то! Наконец-то! Сегодня мы начнём создавать полноценное окно Windows. Прощай убогая консоль.
К этому моменту вы уже должны неплохо знать синтаксис C++, уметь работать с ветвлениями и циклами, хорошо понимать работу функций. Если вы справились с морским боем, можете считать, что всё это вы усвоили.
Венгерская форма записи
Весь код, который мы встретим в WinAPI написан в венгерской форме. Это такое соглашение по написанию кода.
При этом перед именем переменной ставится начальная буква типа. Все слова в именах переменных и функций начинаются с заглавной буквы.
Вот несколько префиксов:
b — переменная типа bool.
l — переменная типа long integer.
w — от word (слово) — 16 бит. Переменная типа unsigned short.
dw — от double word (двойное слово) — 32 бита. Переменная типа unsigned long.
sz — строка заканчивающаяся нулём (string terminated zero). Просто обычная строка, которую мы постоянно использовали.
p или lp — указатель (от pointer). lp (от long pointer) — данные указатели перешли из прошлого. Сейчас lp и p означают одно и то же.
h — описатель (от handle).
Например, указатель будет называться вот так:
Данная форма записи используется Microsoft. Многие критикуют этот способ именования переменных. Но подобные вещи (соглашения о кодировании) в больших компаниях жизненно необходимы.
Напомню, что идентификаторы констант обычно состоят только из заглавных букв: WM_DESTROY. WM_DESTOY — это 2, константа определена через define.
Кроме того, в winAPI используется очень много переопределённых типов. Вот на этой страничке — http://msdn.microsoft.com/en-us/library/aa383751(VS.85).aspx , можете найти описания всех типов Windows (на английском).
И ещё одна вещь, которую мы не разбирали. Указателям часто присваивается значение NULL. Считайте, что это просто 0 и указатели которым присвоено значение NULL (ноль), не указывают ни на какой участок памяти.
Windows API (WinAPI)
Все программы под Windows используют специальный интерфейс программирования WinAPI. Это набор функций и структур на языке C, благодаря которым ваша программа становится совместимой с Windows.
Windows API обладает огромными возможностями по работе с операционной системой. Можно даже сказать — безграничными.
Мы не рассмотрим даже один процент всех возможностей WinAPI. Первоначально я хотел взять больше материала, но это заняло бы слишком много времени, и увязнув в болоте WinAPI, до DirectX’а мы добрались бы через пару лет. Описание WinAPI займёт два урока (включая этот). В них мы рассмотрим только каркас приложения под Windows.
Программа под Windows точно так же как и программа под DOS, имеет главную функцию. Здесь эта функция называется WinMain.
Программа под Windows состоит из следующих частей (всё это происходит внутри WinMain):
Создание и регистрация класса окна. Не путайте с классами C++. WinAPI написана на C, здесь нет классов в привычном для нас понимании этого слова.
Создание окна программы.
Основной цикл, в котором обрабатываются сообщения.
Обработка сообщений программы в оконной процедуре. Оконная процедура представляет собой обычную функцию.
Вот эти четыре пункта — основа программы Windows. В течение этого и следующего урока мы разберём всё это подробно. Если вы запутаетесь в описании программы, то вернитесь к этим пунктам.
Теперь разберём всё это подробно:
WinAPI: Структура WNDCLASS
Прежде всего нужно создать и заполнить структурную переменную WNDCLASS, а затем на её основе зарегистрировать оконный класс.
Вот как выглядит эта структура:
Структура WNDCLASS в составе WinAPI определяет базовые свойства создаваемого окна: иконки, вид курсора мыши, есть ли меню у окна, какому приложению будет принадлежать окно.
После того как вы заполните эту структуру, на её основе можно зарегистрировать оконный класс. Речь идёт не о таких классах как в C++. Скорее можно считать, что оконный класс это такой шаблон, вы его зарегистрировали в системе, и теперь на основе этого шаблона можно создать несколько окон. И все эти окна будут обладать свойствами, которые вы определили в структурной переменной WNDCLASS.
WinAPI: Функция CreateWindow
После регистрации оконного класса, на его основе создаётся главное окно приложения (мы сейчас приступили ко второму пункту). Делается это с помощью функции CreateWindow. Она имеет следующий прототип:
Если в оконном классе (структуре WNDCLASS) задаются базовые свойства окна, то здесь — более специфические для каждого окна: размер окна, координаты.
Данная функция возвращает описатель окна. С помощью описателя можно обращаться к окну, это примерно как идентификатор.
Обратите внимание, что здесь очень много новых типов. На самом деле они все старые, просто переопределены. Например: HWND — это переопределение типа HANDLE, который в свою очередь является переопределением PVOID, который в свою очередь является переопределением void*. Как глубоко закопана правда! Но всё же тип HWND — это указатель на void.
Окно состоит из нескольких частей. Практически в каждой программе вы увидите: заголовок окна, системное меню (если нажать на иконку приложения в левой верхней части окна), три системные кнопки для работы с окном: свернуть, развернуть на весь экран и закрыть. Также, практически всегда в приложении присутствует меню. Вот как раз последнего у нас точно не будет. И, конечно же, большую часть окна занимает т.н. клиентская область, в которой обычно и работает пользователь.
Это что касается оконного режима. Довольно долго мы будем практиковаться с DiectX именно в окне — не будем пользоваться полноэкранным режимом.
Обработка сообщений (Message handling)
Основным отличием всех наших предыдущих программ от программ под Windows является обработка сообщений.
Например, когда пользователь нажимает какую-нибудь клавишу на клавиатуре, генерируется сообщение, что была нажата клавиша. Затем это сообщение поступает в приложение, которое было активным, когда пользователь нажал клавишу.
Здесь у нас произошло событие (event) — была нажата клавиша.
Событием может быть: перемещение курсора мыши, смена фокуса приложения, нажатие клавиши клавиатуры, закрытие окна. Событий очень много. Очень! За секунду в операционной системе может происходить десятки событий.
Так вот, когда происходит какое-либо событие, операционная система создаёт сообщение: была нажата такая-то клавиша, координаты курсора мыши изменились, открылось новое окно.
Сообщения может создавать как операционная система, так и различные приложения.
Сообщение представляет собой структуру, и выглядят следующим образом:
Обратите внимание, как с помощью typedef переопределяются структуры.
Чтобы создать данную структуру можно воспользоваться следующим кодом:
Всё. Дальше можете использовать эту структуру как обычно.
Здесь, поле, в котором содержится код сообщения (имя сообщения, сравнивается с константой WM_DESTROY. WM — от Windows Message (сообщение Windows). WM_DESTROY — это сообщение, которое генерируется при закрытии окна (destroy — уничтожить).
Коды сообщений определены с помощью констант и имеют префикс WM_: WM_CLOSE, WM_CREATE и др.
В структуре MSG встречается тип HWND — от Window Handle (дескриптор окна или описатель окна). Это такая штука, которая «описывает» окно. Это что-то вроде идентификатора (имени окна).
Запомните это слово — handle (описатель, дескриптор). В Windows это понятие используется очень часто. Почти все типы Windows, которые начинаются с H — описатели: описатель иконки, описатель шрифта, описатель экземпляра приложения. Их штук тридцать насколько я помню.
Все взаимодействия между приложениями в Windows осуществляются с помощью этих самых описателей окон (HWND).
Существует ещё один важный описатель — описатель приложения (HINSTANCE — первый параметр WinMain) — это уникальный идентификатор приложения, благодаря которому операционная система не сможет перепутать две разных программы. Это примерно как штрих-код. Мы рассмотрим его позже.
Каждый раз, когда пользователь совершает какое-то действие, создаётся и заполняется сообщение: задаётся описатель окна, которое должно получить данное сообщение, задаётся идентификатор сообщения, заполняются параметры, заполняется время (текущее) и указываются координаты курсора мыши (смотрите структуру).
После этого данное сообщение помещается в очередь сообщений операционной системы. Когда доходит очередь до нашего сообщения, оно отправляется нужному окну (windows знает какому окну отправлять каждое сообщение благодаря описателям). Когда сообщение приходит в приложение, оно помещается в очередь сообщений приложения. Как только до него доходит очередь, оно обрабатывается.
Смотрите, между тем моментом, когда пользователь совершил какое-либо действие (произошло событие и сгенерировалось сообщение) и тем моментом, когда программа среагировала на это действие (сообщение было обработано программой) происходит много событий. Ведь как в очереди сообщений Windows так и в очереди сообщений приложения может быть много сообщений. В первом случае речь может идти о сотнях, во втором случае как минимум о нескольких.
Оконная процедура (Window procedure — WndProc)
Продолжаем с того момента, как сообщение попало в очередь сообщений приложения. Как только до него дошла очередь, оно обрабатывается. Для обработки сообщений в каждой программе должна существовать специальная функция — оконная процедура. Обычно она называется WndProc (от Window Procedure). Вызов оконной процедуры расположен в основном цикле программы и выполняется при каждой итерации цикла.
Сообщения (в виде структурных переменных MSG) попадают в данную функцию в виде параметров: описатель окна, идентификатор сообщения и два параметра. Обратите внимание, что в оконную процедуру не передаются поля time и pt. То есть сообщение уже «разобрано».
Внутри оконной процедуры расположено ветвление switch, в котором идёт проверка идентификатора сообщения. Вот пример простой оконной процедуры (она полностью рабочая):
И последнее — основной цикл. Он очень прост. Каждую итерацию цикла проверяется очередь сообщений приложения. Если в очереди сообщений есть сообщение, оно вытаскивается из очереди. Затем в теле цикла происходит вызов оконной процедуры, чтобы обработать взятое из очереди сообщение.
Вот, в общем-то, и всё на сегодня. Уже видно, что программа под WinAPI намного сложнее программы под DOS. Как я уже писал выше, в следующем уроке мы разберём код работающей программы.
В качестве упражнения создайте новый проект. В окне New Project (Новый проект) выберите шаблон (template) — Win32Project (до сих пор мы выбирали Win32 Console Application). В одном из следующих окон не ставьте флажок Empty Project (пустой проект) и IDE сгенерирует заготовку программы.
Если вы внимательно посмотрите на код файла имя_проекта.cpp, то вы обнаружите все вещи, которые мы обсуждали: структурную переменную MSG, заполнение структуры WNDCLASS, создание окна функцией CreateWindow, основной цикл программы. Кроме того, в файле определена функция WndProc. В ней происходит обработка нескольких сообщений в ветвях switch: WM_COMMAND, WM_PAINT, WM_DESTROY. Найдите всё это в файле.
Кроме того что мы рассмотрели, в программе содержится много дополнительного кода. В следующем выпуске мы рассмотрим код программы, в котором будет вырезано всё лишнее. Он будет намного проще и понятнее того, что генерирует IDE.
Первая программа на WinAPI
WinAPI или Windows API (Application Programming Interface) — это библиотека для создания классических приложений Windows. Сама библиотека WinAPI написана на языке C и представляет собой коллекцию функций, структур и констант. Она объявлена в определённых заголовочных файлах и реализована в статических (.lib) и динамических (.dll) библиотеках.
В данном уроке мы создадим нашу первую программу на WinAPI. Для начала создайте проект. Выберите меню File -> New -> Project:
В открытом окне в левой панели выберите Other, затем Empty Project (пустой проект). Там же доступен шаблон Windows Desktop Application, но мы напишем программу с нуля, так как шаблон по умолчанию пока слишком сложен для нас. В нижней части выберите имя проекта, его местоположение и хотите ли вы создать решение для него. Местоположение может быть любым, для меня это C:\prog\cpp\
В обозревателе решений щёлкните правой кнопкой мышки и выберите Add -> New Item.
В открывшемся окне выберите C++ File (.cpp) и введите имя файла в нижней части — main.cpp.
Перед тем как мы начнём рассматривать код, давайте поговорим о типах данных в WinAPI и соглашениях вызова (calling conventions).
Типы данных в WinAPI
WinAPI переопределяет множество стандартных типов языка C. Некоторые переопределения зависят от платформы для которой создаётся программа. Например, тип LRESULT, если его скомпилировать для x86, будет типом long. Но если скомпилировать программу для x64, то LRESULT будет типом __int64. Вот так LRESULT определяется на самом деле (он зависит от LONG_PTR, а LONG_PTR может уже быть или __int64, или long):
Соглашения по вызову (Calling Conventions) — __stdcall
В коде ниже перед именами функций вы встретите __stdcall. Это одно из соглашений по вызову функций. Соглашение по вызову функций определяет каким образом аргументы будут добавляться в стек. Для __stdcall аргументы помещаются в стек в обратном порядке — справа налево. Также, __stdcall говорит, что после того как функция завершится, она сама (а не вызывающая функция) удалит свои аргументы из стека. Все функции WinAPI используют __stdcall соглашение.
WinAPI переопределяет __stdcall в WINAPI, CALLBACK или APIENTRY, которые используются в разных ситуациях. Поэтому в примерах из MSDN вы не увидите __stdcall, но нужно помнить что именно оно будет использоваться.
Типы WinAPI пишутся в верхнем регистре.
Описатели/дескрипторы (Handles) в WinAPI
Handle на русский язык сложно перевести однозначно. Наверное, наиболее частое употребление в русском имеет слово дескриптор. По сути это ссылка на ресурс в памяти. Например, вы создаёте окно. Это окно хранится в памяти и оно имеет запись в таблице, которая хранит указатели на все созданные системные ресурсы: окна, шрифты, файлы, картинки. Указатель на ваше окно в данной таблице называется дескриптором окна (handle of the window).
Любой указатель это просто переопределение типа void*. Примеры дескрипторных типов в WinAPI: HWND, HINSTANCE, HBITMAP, HCURSOR, HFILE, HMENU.
Подытожим: дескрипторы используются для получения доступа к каким-либо системным ресурсам.
WinAPI окна
Давайте посмотрим на код самой простой WinAPI программы:
Вначале нужно добавить WinAPI: статичную библиотеку, которая содержит определения различных функций и включить заголовочный файл с объявлениями этих функций, структур и констант. user32.lib содержит основные возможности Windows — всё, что касается окон и обработки событий.
На следующей строке мы объявляем функцию обратного вызова (callback), которая будет вызываться, когда наше приложение получает какое-либо сообщение от операционной системы. Мы вернёмся к этому ниже.
Функция WinMain
WinMain — точка входа в программу, а как мы помним такая функция вызывается операционной системой.
Главная функция приложений под Windows отличается от консольной версии. Она возвращает целое число и это всегда ноль. __sdtcall говорит, что аргументы добавляются в стек в обратном порядке и WinMain сама удаляет их из стека по завершении. WinMain принимает 4 аргумента:
hInstance — дескриптор экземпляра приложения. Можете думать о нём, как о представлении вашего приложения в памяти. Он используется для создания окон.
Второй аргумент — наследие шестнадцатибитных версий Windows. Уже давно не используется.
Третий аргумент представляет аргументы командной строки. Пока мы не будем им пользоваться.
nCmdShow — специальный флаг, который можно использовать при создании окон. Он говорит о состоянии окна: должно ли оно показываться нормально, на полный экран или быть свёрнутым.
Теперь давайте посмотрим как создаются окна.
Классы окон (Window Classes)
Для создания окна нужно определить и зарегистрировать его класс. Windows создаёт свои классы таким же образом. Все стандартные элементы, которые вы видите в Windows являются классами: кнопки, поля редактирования, полосы прокрутки. Windows хранит список всех зарегистрированных классов. Обязательно нужно заполнить только три поля класса: имя класса, дескриптор экземпляра приложения (передаётся в WinAPI в виде параметра) и оконная процедура (адрес функции).
Сначала нужно заполнить структуру WNDCLASS. Пусть вас не смущает название WNDCLASS — это не C++ класс. В данном случае, класс — всего лишь термин используемый в WinAPI:
Здесь мы инициализируем структуру WNDCLASS нулями, определяем обязательные поля и регистрируем класс.
lpfnWndProc имеет тип WNDPROC. Как говорилось выше, это указатель на функцию WindowProc, которую мы объявили в самом начале. У каждого оконного класса должна быть своя оконная процедура.
hInstance — дескриптор экземпляра приложения. Все оконные классы должны сообщать, какое приложение их зарегистрировало. Мы используем первый параметр функции WinMain.
lpszClassName — имя класса, задаётся пользователем. В Windows все классы называются в верхнем регистре (примеры: BUTTON, EDIT, LISTBOX), мы будем делать также в наших уроках.
WNDCLASS содержит больше полей: стиль, иконка, имя меню, но мы можем пропустить их. Некоторые из них мы рассмотрим в следующих уроках. Вы можете посмотреть полный список в документации к WinAPI на MSDN (официальном сайте Microsoft с документацией).
В конце мы регистрируем наш класс с помощью функции RegisterClass. Мы передаём адрес структуры WNDCLASS. Теперь мы можем создать окно.
Первая WinAPI программа — Пустое окно
В WinAPI есть функция для создания окон — CreateWindow:
Первый параметр — имя класса. В данном случае он совпадает с именем класса, который мы зарегистрировали. Второй — имя окна, это та строка, которую пользователи программы будут видеть в заголовке. Следующий — стиль. WS_OVERLAPPEDWINDOW говорит, что WinAPI окно имеет заголовок (caption), кнопки сворачивания и разворачивания, системное меню и рамку.
Четыре числа определяют позицию левого верхнего угла окна и ширину/высоту.
Затем идут два указателя nullptr. Первый — дескриптор родительского окна, второй — меню. У нашего окна нет ни того, ни другого.
hInstance — дескриптор на экземпляр приложения, с которым связано окно.
В последний аргумент мы передаём nullptr. Он используется для специальных случаев — MDI (Multiple Document Interface ) — окно в окне.
CreateWindow возвращает дескриптор окна. Мы можем использовать его для обращения к окну в коде. Теперь мы можем показать и обновить окно.:
ShowWindow (показать окно) использует параметр nCmdShow функции WinMain для контроля начального состояния (развёрнуто на весь экран, минимизировано, обычный размер). UpdateWindow (обновить окно) мы обсудим в следующих уроках.
Главный цикл WinAPI
Далее идёт бесконечный цикл. В этом цикле мы будем реагировать на разные события, возникающие при взаимодействии пользователя с нашей программой.
Перед циклом мы объявляем переменную типа MSG (от message — сообщение) — в этой структуре Windows кодирует события. Если произошло событие WM_QUIT, то мы завершаем цикл.
Операционная система генерирует множество различных сообщений. Они генерируются, когда происходит какое-то событие: изменился размер окна, нажата клавиша мышки или клавиатуры.
Очередь сообщений (Message Queue) в WinAPI
Все оконные приложения управляются событиями (event-driven). В операционной системе существует очередь сообщений. Когда происходит какое-либо событие в любой программе, в эту очередь посылается сообщение. Также, любая программа имеет свою очередь сообщений. Windows проверяет каждое сообщение в системной очереди и посылает их в программные очереди. В действительности всё немного сложнее, так как очереди сообщений связаны с потоками, но мы обсудим это позже. На данный момент запомните, что каждое приложение имеет свою собственную очередь сообщений.
Сообщение это просто структурная переменная MSG. Давайте посмотрим на определение этой структуры:
hwnd — дескриптор окна, которому принадлежит сообщение.
message — идентификатор сообщения (UINT — unsigned integer).
wParam и lParam содержат дополнительную информацию и зависят от идентификатора сообщения.
time — понятно из названия — время, когда было создано сообщение.
pt — позиция курсора на экране в момент генерации сообщения. POINT — тип WinAPI для описания точек с координатами (x,y).
Приложение в бесконечном цикле проверяет свою очередь сообщений, смотрит на свойство message и решает, что делать с данным сообщением (как его обработать).
Функция PeekMessage проверяет очередь и берёт последнее сообщение. Затем, она берёт информацию о сообщении и помещает её в переменную msg. Последний аргумент заставляет удалить сообщение из очереди.Итак, в условии мы проверяем, содержит ли очередь сообщений приложения какое-либо сообщение. Если содержит, мы заполняем переменную msg и удаляем сообщение из очереди. Затем вызываем две функции.
TranslateMessage — генерирует дополнительное сообщение если произошёл ввод с клавиатуры (клавиша с символом была нажата или отпущена). По умолчанию, клавиатура генерирует так называемые сообщения виртуального ключа (virtual key). TranslateMessage генерирует ещё одно сообщение, которое сообщает информацию о символе. Мы поговорим об этом позже. Пока что: вызов TranslationMessage нужен, чтобы обработать ввод символов с клавиатуры.
Функция DispatchMessage посылает сообщение в функцию WindowProc.
Оконная процедура (Window Procedure) WindowProc
Оконная процедура — это специальная функция в которой мы обрабатываем сообщения. В данный момент нам нужно обработать только одно важное сообщение, а для всех остальных выполнить стандартное действие. Давайте посмотрим на наш вариант WindowProc:
Обратите внимание, что мы сами нигде не вызываем WindowProc. Оконная процедура привязана к классу окна. И когда мы вызываем DispatchMessage, система сама вызывает оконную процедуру, связанную с окном. Такие функции называются функциями обратного вызова (callback functions) — они не вызываются напрямую. Также обратите внимание, что данная функция получает только часть свойств MSG структуры.
Внутри WindowProc мы проверяем поле message структуры MSG. Оно содержит идентификатор сообщения. В Windows определено много констант для разных сообщений. В данной программе мы проверяем WM_DESTROY. Это сообщение посылается, когда окно уничтожается, в момент, когда оно уже удалено с экрана. В ответ на это сообщение мы вызываем PostQuitMessage — она говорит системе, что приложение будет закрыто и посылает сообщение WM_QUIT в очередь сообщений программы (не в системную).
Если пришло сообщение, которое мы не хотим обрабатывать сами, мы передаём его в DefWindowProc — действие по умолчанию.
Заключение
В данном уроке мы создали пустое но полностью функциональное стандартное окно операционной системы Windows. В следующих уроках мы обсудим разные части WinAPI, а шаблон из данного урока станет основой для наших программ на DirectX и OpenGL.