Что такое сообщение winapi

Обновлено: 18.05.2024

Данная статья является введением в программирование на WinAPI. Важно понимать, что она носит скорее информационный характер, чем служит примером кода для реальных приложений. Дело в том, что WinAPI создавался для языка С, и имеет целый ряд недостатков в применении, как-то: невысокая безопасность, большой объем ручного кодирования для решения простейших задач, С-стиль, плохо выглядящий в C++-приложениях. Практически любое средство разработки на языке C++ для Windows включает те или иные высокоуровневые C++-библиотеки (MFC, ATL/WTL для Visual C++, VCL для C++ Builder), значительно упрощающие разработку Windows-приложений, и делающие ее более безопасной.

Предисловие

First Blood

После создания нового проекта Win32 Application, в зависимости от выбранных опций, мастер генерирует стартовый код. Из этого кода программисту впоследствии, и придется писать программу. Создавая новый проект Win32 Application, выберите в окне мастера опцию An empty project и добавьте в раздел Source Files новый файл с расширением .cpp.

В этом файле добавьте функцию WinMain вида:

  • HINSTANCE hInstance – дескриптор экземпляра приложения. Этот дескриптор содержит адрес начала кода программы в ее адресном пространстве. Дескриптор hInstance чаще всего требуется функциям, работающим с ресурсами программы.
  • HINSTANCE hPrevInstance – дескриптор предыдущего экземпляра приложения. Этот дескриптор остался от старых версий Windows - скорее всего, вам он никогда не пригодится.
  • LPSTR lpCmdLine – указатель на начало командной строки, введенной при запуске программы.
  • int nCmdShow – это значение содержит желаемый вид окна (например, свернутый или развернутый)

Значение, которое возвращается функцией WinMain (тип int ) – код завершения программы. Принято, что если программа завершила свое выполнение без ошибок, возвращается 0.

ПРИМЕЧАНИЕ

Функция WinMain – это первая функция, которую вы можете увидеть и заполнить кодом. На самом деле до этой функции выполняется достаточно много кода из библиотеки C++

You have a Message!

СОВЕТ

Функция GetMessage принимает следующие параметры:

СОВЕТ

При вызове этой функции ей передаются следующие параметры:

CLASSные окна

Итак, регистрация! За нее отвечает функция RegisterClass . В ее параметре необходимо передать указатель на структуру WNDCLASS . Обычно для заполнения структуры и вызова RegisterClass создают отдельную функцию. Но это - дело вкуса.

Вот простейший пример такой функции:

Существует также функция RegisterClassEx. Это аналог функции RegisterClass с возможностью присвоения окнам маленькой иконки. При работе с этой функцией необходимо пользоваться структурой WNDCLASSEX.

ПРИМЕЧАНИЕ

Если вы решились работать с GUI Windows вручную, то пользоваться нужно именно RegisterClassEx, поскольку приложение, не имеющее маленькой иконки, сегодня выглядит в Windows как минимум странно. – прим.ред.

СОВЕТ

Следите, чтобы имя вашего класса не совпадало с именами системных классов (например: button или edit).

ПРИМЕЧАНИЕ

Я описал не всю структуру. Все незаполненные поля, которых нет в примере, сейчас равны нулю. Об их значениях можно узнать из MSDN.

Our Windows

На вашем месте у меня возникло бы желание увидеть те самые пресловутые окна, из-за которых столько шума. Окно в Windows создается функцией CreateWindow . Вот ее прототип:

Как видите, у функции множество параметров:

Функция CreateWindow возвращает уникальный описатель окна HWND . Если функция вернула ноль, значит, во время создания окна произошла ошибка. Какая именно, можно узнать, вызвав функцию GetLastError .

ПРИМЕЧАНИЕ

Существует также функция CreateWindowEx, в которой дополнительно присутствует параметр dwExStyle. С его помощью можно создать окно с дополнительными стилями.

План полета

Итак, сейчас я упрощенно расскажу, что же произойдет, если щелкнуть по окну левой кнопкой мыши.

Итого

Вот, в принципе, и все! Это полноценное приложение на WinAPI.

Признаю, что данная статья и программа опускает очень много деталей! Многие вещи были не раскрыты (например, остальные переменные структуры WNDCLASS ). Все это сделано для того, чтобы максимально упростить статью и уменьшить код программы.

оглавление

Создание Windows Forms делится на три этапа: объявление экземпляра WNDCLASS, регистрация формы и создание формы.


Функция регистрации формы - это RegisterClass или RegisterClassEx, который является классом окна, используемым при вызове функции CreateWindow () или CreatewindowEx после регистрации. Прототип выглядит следующим образом:


Для создания окна используется функция CreateWindow () или CreatewindowEx. Эта функция основана на классе окна, поэтому вам нужно указать несколько параметров, чтобы указать конкретное окно. И как создать несколько окон без рамок, тоже довольно умело, то есть создавать окна без заголовков и рамок, а затем самостоятельно рисовать содержимое программы в клиентской области, чтобы создавать персонализированные приложения. Прототип функции выглядит следующим образом: обратите внимание, что возвращаемое значение функции - дескриптор для создания формы, который требует особого внимания.




Этот код реализует простейшую форму. Эффект такой:



Выбранное окно данных является структурой WNDCLASS. Последние четыре байта - это адрес имени класса окна, адрес 0xF3A000, и окно данных следует для проверки, результат согласован, результат будет следующим:


Адрес функции оконной обработки - вторые четыре байта структуры WNDCLASS, то есть 0XF31352. Установите точку останова на этой функции. Продолжайте отладку.


Соответствующие параметры можно увидеть в разделе стека, где класс формы называется Register. Отладка еще на один шаг:


Возвращаемое значение регистра EAX - это дескриптор 0XB1346. Форма просмотра:


Вы можете видеть, что дескриптор и имя класса в первой строке согласованы. Продолжайте нажимать F9 для отладки.


Можно обнаружить, что программа останавливается на 0XF31352, что означает, что это вход оконной функции.


Только что представленный небольшой пример представляет собой простой анализ без углубленного анализа, особенно позиционирования функций обработки окна. Позвольте мне взять CrackMe в качестве примера для анализа. На форуме Wuai я нашел для анализа очень простой CM (без сложного алгоритма, без оболочки). Вам необходимо ввести правильный регистрационный код и нажать кнопку для взлома. Эффект такой:



В соответствии с обычной процедурой вызовите точку останова между двумя модулями RegisterClass и CreateWindow:









Наблюдая за структурой WNDCLASS в двух функциях RegisterClass, было обнаружено, что адрес функции процедуры формы трех форм - 0X77782280, а код расположен в сегменте системного кода (ntdll.dll), что не соответствует интуитивному пониманию. . Почему это?? Во-первых, решите первый вопрос, а именно, почему функции процедуры формы для двух функций регистрации формы одинаковы. Это связано с тем, что класс окна имеет только одну функцию окна, и все окна, созданные с помощью этого класса окна, используют одну и ту же функцию окна , если позже функция окна не была изменена с помощью SetWindowLong. Окно просмотра выглядит следующим образом, вы можете видеть, что все окна созданы с помощью WTWindow.



С помощью IDA может быть более интуитивно понятно:


Возвращаясь к функции DispatchMessage, прототип функции выглядит следующим образом:


Вышеупомянутый обычный анализ, теперь основное внимание уделяется тому, как найти настоящую функцию оконной обработки для взлома CM?


Все приведенные выше коды в 0X45AB89 являются сегментами системного кода, так что не беспокойтесь. Используйте IDA для отслеживания 0X45AB89 во фрагменте кода:



Далее продолжаем анализ функции sub_45AB89. Функция sub_45AB04 выглядит следующим образом:


Мы используем OD, чтобы установить точку останова на 0X45A912 для просмотра значений параметров.


Вы можете видеть, что v5 [7] = 0X321336, что соответствует ожиданиям. Давайте воспользуемся IDA для анализа функции sub_45A912:


Красные кружки - это ключевые функции, на которых нужно сосредоточиться. Используйте OD, чтобы установить там точку останова и наблюдать за функцией и параметрами. Можно догадаться, что Cwnd - это значение адреса, а адрес функции находится путем добавления смещения к адресу.



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

Казалось бы, что WinAPI уходит в прошлое. Давно уже существует огромное количество кросс-платформенных фреймфорков, Windows не только на десктопах, да и сами Microsoft в свой магазин не жалуют приложения, которые используют этого монстра. Помимо этого статей о том, как создать окошки на WinAPI, не только здесь, но и по всему интернету, исчисляется тысячами по уровню от дошколят и выше. Весь этот процесс разобран уже даже не по атомам, а по субатомным частицам. Что может быть проще и понятнее? А тут я еще…

Но не все так просто, как кажется.

Почему о WinAPI сейчас?

В один прекрасный момент, изучая потроха одной из игр в весьма неплохом эмуляторе NES, я подумал: Вроде неплохой такой эмуль, а в отладчике нет такой простой вещи, как навигация по кнопкам клавиатуры, которая есть в любом нормальном отладчике.

Здесь я не зря дал ссылку на репозиторий, т.к. видно, что ребята столкнулись с проблемой, о которой речь пойдет ниже, но так и не решили ее.

О чем это я? А вот об этом кусочке кода:

Ответ такой: так делать нельзя!

И, возвращаясь, к изначальному вопросу о WinAPI: очень много популярных, и не очень, проектов продолжают его использовать и в настоящее время, т.к. лучше, чем на чистом API многие вещи не сделать (тут можно бесконечно приводить аналогии вроде сравнения высокоуровневых языков и ассемблера, но сейчас не об этом). Да и мало ли почему? Просто используют и все тут.

О проблеме

Обойти такие мелкие неприятности просто. Есть, как минимум, два вполне легальных способа:

Tutorials?


Здесь действительно все просто:

И ниже приводится пример правильного цикла.

Стоит сказать, что в шаблонах VS для Win32 приложений, написан именно такой неправильный цикл. И это очень печально. Ведь мало кто будет вникать в то, что сделали сами авторы, ведь это априори правильно. И неправильный код множится вместе с багами, которые очень сложно отловить.

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


Этот вариант я видел чаще всего. И он (та-дам) снова неправильный!

Сперва о том, что изменилось (потом о проблемах этого кода):

Ясно, что TranslateAccelerator надо вызывать для нашего созданного окна:


И вроде все хорошо и замечательно теперь: мы разобрали все детально и все должно работать идеально.

И снова нет. :-) Это будет работать правильно, пока у нас ровно одно окно — наше. Как только появится немодальное новое окно (диалог), все клавиши, которые будут в нем нажаты оттранслируются в WM_COMMAND и отправляться куда? И опять же правильно: в наше главное окно.

На этом этапе предлагаю не городить костылей по решению этой тупиковой ситуации, а предлагаю рассмотреть вещи, которые уже реже (или почти не встречаются) в туториалах.

IsDialogMessage

На самом деле, делает она чуть больше, чем следует из названия. А именно:

  • Осуществляет навигацию по дочерним контролам кнопками Tab/Shift+Tab/вверх/вниз/вправо/влево. Плюс еще кое-что, но этого нам достаточно
  • По нажатии на ESC формирует WM_COMMAND( IDCANCEL )
  • По нажатии на Enter формирует WM_COMMAND( IDOK ) или нажатие на текущую кнопку по умолчанию
  • Переключает кнопки по умолчанию (рамочка у таких кнопок чуть ярче остальных)
  • Ну и еще разные штуки, которые облегчают пользователю работу с диалогом

Во-вторых, она нам облегчит жизнь по всем остальным пунктам, перечисленным в списке (и даже немного больше).

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

Although the IsDialogMessage function is intended for modeless dialog boxes, you can use it with any window that contains controls, enabling the windows to provide the same keyboard selection as is used in a dialog box.

Т.е. теперь, если мы оформим цикл так:


То наше окошко будет иметь навигацию, как в родном диалоге Windows. Но теперь мы получили два недостатка:

Пора поговорить о том, чего нет в туториалах и ответах.

Как правило (как правило! Если кому-то захочется большего, то можно регистрировать свой класс для диалогов и работать так. И, если же, кому-то это интересно, я могу дополнить этим статью) WM_KEYDOWN хотят тогда, когда хотят обработать нажатие на клавишу, которая выполнит функцию в независимости от выбранного контрола в окне — т.е. некая общая функция для всего данного конкретного диалога. А раз так, то почему бы не воспользоваться богатыми возможностями, которые нам сама WinAPI и предлагает: TranslateAccelerator.

Везде используют ровно одну таблицу акселераторов, и только для главного окна. Ну действительно: цикл GetMessage-loop один, значит и таблица одна. Куда еще их девать?

На самом деле, циклы GetMessage-loop могут быть вложенными. Давайте еще раз посмотрим описание PostQuitMessage:

The PostQuitMessage function posts a WM_QUIT message to the thread's message queue and returns immediately; the function simply indicates to the system that the thread is requesting to quit at some time in the future.

Таким образом, выход из GetMessage-loop осуществится, если мы вызовем PostQuitMessage в процедуре окна. Что это значит?

Мы можем для каждого немодального окна в нашей программе создавать свой собственный подобный цикл. В данном случае DialogBoxParam нам не подходит, т.к. оно крутит свой собственный цикл и повлиять мы на него не можем. Однако если создадим диалог через CreateDialogBoxParam или окно через CreateWindow, то можно закрутить еще один цикл. При этом в каждом таком окне и диалоге мы должны вызывать PostQuitMessage:


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

Делаем красиво

Т.к. правильная постановка задачи является половиной решения, то сперва надо эту самую задачу правильно же и поставить.

Создадим простой std::map, который будет мапить дескриптор окна в дескриптор таблицы акселераторов. Вот так:


И по мере создания окон будем в него добавлять новые окна с дескриптором на свою любимую таблицу (или нуль, если такая обработка не требуется).


Ну и после закрытия окна удалять. Вот так:


Теперь, как создаем новый диалог/окно, вызываем AddAccelerators( hNewDialog, IDR_MY_ACCEL_TABLE ). Как закрываем: DelAccel( hNewDialog ).


Значительно лучше! Что же там в HandleAccelArray и зачем там GetActiveWindow()?

Есть две функции, возвращающих дескриптор активного окна GetForegroundWindow и GetActiveWindow. Отличие первой от второй вполне доходчиво описано в описании второй:

The return value is the handle to the active window attached to the calling thread's message queue. Otherwise, the return value is NULL.


Теперь каждое дочернее окно вправе добавить себе любимую таблицу акселераторов и спокойно ловить и обрабатывать WM_COMMAND с нужным кодом.

А что там еще об одной строчке в коде обработчика WM_COMMAND?

To differentiate the message that this function sends from messages sent by menus or controls, the high-order word of the wParam parameter of the WM_COMMAND or WM_SYSCOMMAND message contains the value 1.

Обычно код обработки WM_COMMAND выглядит так:


Теперь можно написать так:

P.S.: Мало кто знает, но можно создавать свою собственную таблицу акселераторов, а теперь и применять ее прямо налету.

P.P.P.S.: После вызова HandleAccelWindow мап l_mAccelTable может измениться, т.к. TranslateAccelerator или IsDialogMessage вызывают DispatchMessage, а там может встретиться AddAccelerators или DelAccel в наших обработчиках! Поэтому лучше его после этой функции не трогать.

Пощупать код можно здесь. За основу был взят код, генерируемый из стандартного шаблона MS VS 2017.

Проекты писать только на Unicode. Для этого в Microsoft Visual Studio Unicode нужно выставить в свойствах проекта или в теле программы определить макрос UNICODE.

Алгоритм создания окна приложения на WinAPI:

Регистрируем класс главного окна в Windows. (Для встроенных классов (например BUTTON) регистрация не нужна)

Диалоговые окна С++ модальные(DialogBox)и немодальные (CreateDialog). MessageBox WinAPI- стандартные диалоговые окна.

CreateWindowEx дополнительный параметр dwExStyle определяющий расширенный стиль создаваемого окна. Обычные стили окна:

AllocConsole - Пример: из графического приложения запускается консоль, в которую можно выводить например отладочную информацию.

Читайте также: