Автор: Яковлев Игорь Сергеевич Источник: RSDN Magazine #4-2005
Опубликовано: 30.12.2005 Исправлено: 16.03.2006 Версия текста: 1.0
От редакции
Данная статья является введением в программирование на WinAPI. Важно понимать, что она носит скорее информационный характер, чем служит примером кода для реальных приложений. Дело в том, что WinAPI создавался для языка С, и имеет целый ряд недостатков в применении, как-то: невысокая безопасность, большой объем ручного кодирования для решения простейших задач, С-стиль, плохо выглядящий в C++-приложениях. Практически любое средство разработки на языке C++ для Windows включает те или иные высокоуровневые C++-библиотеки (MFC, ATL/WTL для Visual C++, VCL для C++ Builder), значительно упрощающие разработку Windows-приложений, и делающие ее более безопасной.
Предисловие
Эта статья посвящена описанию программирования приложений на «чистом» Win32 API. Она написана в основном для начинающих программистов, пишущих программы на Visual C++ 6 с использованием библиотеки MFC, но я надеюсь, может пригодиться и более опытным людям.
First Blood
После создания нового проекта Win32 Application, в зависимости от выбранных опций, мастер генерирует стартовый код. Из этого кода программисту впоследствии, и придется писать программу. Создавая новый проект Win32 Application, выберите в окне мастера опцию An empty project и добавьте в раздел Source Files новый файл с расширением .cpp.
В этом файле добавьте функцию WinMain вида:
Вот и готова первая программа на WinAPI. Она выводит сообщение, после чего завершает свою работу. Обратите внимание на параметры функции WinMain :
HINSTANCE hInstance – дескриптор экземпляра приложения. Этот дескриптор содержит адрес начала кода программы в ее адресном пространстве. Дескриптор hInstance чаще всего требуется функциям, работающим с ресурсами программы.
HINSTANCE hPrevInstance – дескриптор предыдущего экземпляра приложения. Этот дескриптор остался от старых версий Windows — скорее всего, вам он никогда не пригодится.
LPSTR lpCmdLine – указатель на начало командной строки, введенной при запуске программы.
int nCmdShow – это значение содержит желаемый вид окна (например, свернутый или развернутый)
Значение, которое возвращается функцией WinMain (тип int ) – код завершения программы. Принято, что если программа завершила свое выполнение без ошибок, возвращается 0.
Функция WinMain – первая функция, которая выполнятся в программе (ее еще называют «точка входа» или «entry point»). С нее все начинается, и ею (желательно) все должно закончиться.
ПРИМЕЧАНИЕ
Функция WinMain – это первая функция, которую вы можете увидеть и заполнить кодом. На самом деле до этой функции выполняется достаточно много кода из библиотеки C++
You have a Message!
Программисты, незнакомые с программированием на WinAPI, спросят: «Что с этим делать?!», — или: «Где создавать CDialog?». В данном случае ответ прост – нигде! В нашем проекте нет класса CDialog или, предположим, CButton – ведь эта статья посвящена тому, как обойтись без них.
В Windows при каждом событии, произошедшем в системе, отсылается «сообщение Windows» («windows message»). Эти сообщения уведомляют программу о событиях в системе, а программа в свою очередь, может на них реагировать. Сообщения может отсылать не только Windows, но и сами приложения. Это является одним из способов организации связи между процессами в системе. Конечно, программа может отсылать сообщения и самой себе.
СОВЕТ
Сообщение можно отослать функцией SendMessage или ее асинхронным аналогом PostMessage.
Для приема сообщений в программе должен находиться «цикл сообщений» («message loop») который обычно выглядит так:
Функция GetMessage принимает следующие параметры:
LPMSG lpMsg – указатель на структуру сообщения, в которую GetMessage вернет результат.
HWND hWnd – описатель окна, от которого GetMessage примет сообщение ( NULL означает, что GetMessage принимает сообщения от всех окон, принадлежащих потоку).
UINT wMsgFilterMin – наименьший идентификатор сообщения, которое примет GetMessage.
UINT wMsgFilterMax – наибольший идентификатор сообщения, которое примет GetMessage (если в значениях параметров wMsgFilterMin и wMsgFilterMax передать 0, функция будет принимать ВСЕ сообщения).
Функция GetMessage не отдает управление программе, пока не придет какое-либо сообщение. Если пришедшее сообщение – WM_QUIT , функция GetMessage вернет 0 . Тогда цикл прервется, и программа завершит свою работу. При любом другом сообщении функция GetMessage возвращает значение больше нуля, и начинатся выполнение тела цикла. При ошибке GetMessage возвращает -1.
СОВЕТ
Сообщение WM_QUIT лучше посылать с помощью специальной функции PostQuitMessage(int iExitCode). Эта функция отошлет сообщение WM_QUIT, а в параметре wParam передаст код завершения программы, указанный в iExitCode.
Функция DispatchMessage должна вызвать «функцию обработки сообщений». В простейшем варианте она выглядит так:
При вызове этой функции ей передаются следующие параметры:
HWND hWnd – описатель окна, от которого пришло сообщение.
UINT message – идентификатор сообщения.
WPARAM wParam и LPARAM lParam – параметры сообщения.
Функция обработки сообщений не обязательно должна иметь имя WndProc . Таких функций в программе может быть несколько, но их прототипы обязательно должны выглядеть так:
При вызове этой функции DispatchMessage передает в параметре message идентификатор сообщения. По этому идентификатору производится выборка и выполняется какое-либо действие ( «реакция на сообщение» ).
В Windows существует очень много сообщений! Писать обработчики для всех сообщений – нереальная задача. Чтобы Windows сама обработала бесполезное для вас сообщение, необходимо вызвать функцию DefWindowProc :
Желательно передавать все необработанные сообщения этой функции, а результат ее выполнения возвращать при выходе из WndProc . Это очень важно, так как от обработки некоторых сообщений Windows ждет возврата конкретных результатов или действий.
Одной функцией обработки сообщений могут пользоваться несколько окон, но для одного окна может существовать только одна функция обработки сообщений! Как же система определяет, какой именно функцией обработки сообщения пользоваться для конкретного окна и где она находится?! За это отвечает «класс окна» («window class»).
CLASSные окна
При создании нового окна ему присваивается «Класс окна» (window class). Класс окна задает оконную функцию, используемую по умолчанию. Кроме этого, класс окна задает другие параметры окна, такие, как стиль, меню окна, цвет рабочей области и т.д. Разные классы окон могут указывать на одну и ту же функцию обработки сообщений. Для создания класса его необходимо зарегистрировать.
Итак, регистрация! За нее отвечает функция RegisterClass . В ее параметре необходимо передать указатель на структуру WNDCLASS . Обычно для заполнения структуры и вызова RegisterClass создают отдельную функцию. Но это — дело вкуса.
Вот простейший пример такой функции:
WNDPROC lpfnWndProc – адрес функции обработки сообщений.
HINSTANCE hInstance – уже знакомая переменная, описывающая экземпляр.
LPCTSTR lpszClassName – имя нового класса.
HICON hCursor – описатель курсора мыши.
HBRUSH hbrBackground – цвет рабочей области окна.
Функция RegisterClass возвращает уникальный «описатель класса окна» типа ATOM . Если при регистрации класса произошла ошибка, это значение будет равно нулю. Чтобы узнать, что произошло, можно вызвать функцию GetLastError() .
Существует также функция RegisterClassEx. Это аналог функции RegisterClass с возможностью присвоения окнам маленькой иконки. При работе с этой функцией необходимо пользоваться структурой WNDCLASSEX.
ПРИМЕЧАНИЕ
Если вы решились работать с GUI Windows вручную, то пользоваться нужно именно RegisterClassEx, поскольку приложение, не имеющее маленькой иконки, сегодня выглядит в Windows как минимум странно. – прим.ред.
СОВЕТ
Следите, чтобы имя вашего класса не совпадало с именами системных классов (например: button или edit).
ПРИМЕЧАНИЕ
Я описал не всю структуру. Все незаполненные поля, которых нет в примере, сейчас равны нулю. Об их значениях можно узнать из MSDN.
Сообщения от окон, созданных на базе класса, зарегистрированного описанной выше функцией RegMyWindowClass, будут обрабатываться функцией с именем WndProc. Чтобы функция WndProc поняла, от какого именно окна пришло сообщение, ей передается уникальный описатель окна HWND .
Our Windows
На вашем месте у меня возникло бы желание увидеть те самые пресловутые окна, из-за которых столько шума. Окно в Windows создается функцией CreateWindow . Вот ее прототип:
Как видите, у функции множество параметров:
LPCTSTR lpClassName – имя класса для создаваемого окна (это имя использовалось при регистрации класса).
LPCTSTR lpWindowName – имя окна.
DWORD dwStyle – стиль окна.
int x – позиция по горизонтали верхнего левого угла окна.
int y – позиция по вертикали.
int nWidth – ширина окна.
int nHeight – высота окна.
HWND hWndParent – используется для создания «дочернего окна» («child window»). Сюда передается описатель «родительского окна» («parent window»).
HMENU hMenu – описатель меню (если hMenu равно нулю, используется меню класса, указанного в lpClassName ).
HINSTANCE hInstance – экземпляр приложения.
LPVOID lpParam – указатель на пользовательский параметр окна. Этот указатель со всеми остальными параметрами функции CreateWindow будет занесен в структуру CREATESTRUCT . В сообщениях WM_CREATE или WM_NCCREATE параметр lParam будет содержать указатель на эту структуру.
Функция CreateWindow возвращает уникальный описатель окна HWND . Если функция вернула ноль, значит, во время создания окна произошла ошибка. Какая именно, можно узнать, вызвав функцию GetLastError .
ПРИМЕЧАНИЕ
Существует также функция CreateWindowEx, в которой дополнительно присутствует параметр dwExStyle. С его помощью можно создать окно с дополнительными стилями.
План полета
Итак, сейчас я упрощенно расскажу, что же произойдет, если щелкнуть по окну левой кнопкой мыши.
Пользователь нажимает левую кнопку мыши в то время когда курсор мыши находится над рабочей областью окна.
Windows помещает сообщение WM_LBUTTONDOWN в очередь потока.
Цикл обработки сообщения должен вынуть сообщение с помощью функции GetMessage и передать его на обработку функции DispatchMessage .
Функция DispatchMessage находит окно, которому предназначено сообщение и помещает сообщение в его очередь.
Функция окна обрабатывает сообщение WM_LBUTTONDOWN и возвращает результат.
Тело цикла заканчивается, и управление снова передается функции GetMessage для ожидания новых сообщений.
Итого
WinMain, регистрация класса, цикл сообщений, функция обработки сообщений, создание окна. Как все это связать?! Вот код, который объединяет все написанное выше в одну программу:
Вот, в принципе, и все! Это полноценное приложение на WinAPI.
Программа регистрирует класс, создает окно этого класса и обслуживает сообщение WM_LBUTTONUP (оно приходит по событию отпускания левой кнопки мыши), показывает окно, и после обработки сообщения снова возвращается в цикл сообщений, находящийся в WinMain .
Признаю, что данная статья и программа опускает очень много деталей! Многие вещи были не раскрыты (например, остальные переменные структуры WNDCLASS ). Все это сделано для того, чтобы максимально упростить статью и уменьшить код программы.
Первая программа на 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.