Автор: Яковлев Игорь Сергеевич Источник: 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 ). Все это сделано для того, чтобы максимально упростить статью и уменьшить код программы.
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.