Что такое дескриптор окна приложения
Создание окна
Классы окон
Класс окна определяет набор поведений, которые могут встречаться в нескольких окнах. Например, в группе кнопок каждая кнопка имеет аналогичное поведение, когда пользователь нажимает кнопку. Конечно, кнопки не полностью идентичны; Каждая кнопка отображает собственную текстовую строку и имеет собственные экранные координаты. Данные, уникальные для каждого окна, называются данными экземпляра.
Каждое окно должно быть связано с классом окна, даже если программа создает только один экземпляр этого класса. Важно понимать, что класс окна не является «классом» в смысле C++. Вместо этого это структура данных, используемая внутри операционной системы. Классы окон регистрируются в системе во время выполнения. Чтобы зарегистрировать новый класс окна, начните с заполнения структуры вндкласс :
Необходимо задать следующие члены структуры:
Имена классов являются локальными для текущего процесса, поэтому имя должно быть уникальным только в пределах процесса. однако стандартные элементы управления Windows также имеют классы. При использовании любого из этих элементов управления необходимо выбрать имена классов, которые не конфликтуют с именами классов элементов управления. Например, класс окна для элемента управления Button называется «Button».
Создание окна
Чтобы создать новый экземпляр окна, вызовите функцию CreateWindowEx :
CreateWindowEx возвращает маркер в новое окно или нуль, если функция завершается ошибкой. Чтобы отобразить окно, т. е. сделать окно видимым, передайте в функцию ShowWindow маркер окна:
Ниже приведен полный код для создания окна. Помните, что WindowProc все еще является прямым объявлением функции.
Поздравляем, вы создали окно! Сейчас окно не содержит никакого содержимого или взаимодействует с пользователем. В реальном приложении GUI окно будет реагировать на события пользователя и операционной системы. В следующем разделе описывается, как сообщения в окне предоставляют такую сортировку.
Что такое окно?
Что такое окно?
Очевидно, что Windows является центральной для Windows. Так важно, чтобы они называли операционную систему после них. Но что такое окно? Если вы считаете окно, вы, вероятно, думаете примерно так:
Вот еще один тип окна:
если вы не знакомы с Windows программированием, это может заудивлено тем, что элементы управления пользовательского интерфейса, такие как кнопки и поля редактирования, являются окнами. Основное различие между элементом управления пользовательского интерфейса и окном приложения заключается в том, что сам элемент управления не существует. Вместо этого элемент управления располагается относительно окна приложения. При перетаскивании окна приложения элемент управления перемещается вместе с ним, как и хотелось бы. Кроме того, элемент управления и окно приложения могут взаимодействовать друг с другом. (Например, окно приложения получает команду уведомления от кнопки.)
Таким образом, при обдумывании окна не следует просто думать окно приложения. Вместо этого представьте окно как конструкцию программирования, которая:
родительский Windows и владелец Windows
В случае элемента управления пользовательского интерфейса окно элемента управления называется дочерним элементом окна приложения. Окно приложения является родительским для окна управления. Родительское окно предоставляет систему координат, используемую для размещения дочернего окна. Наличие родительского окна влияет на аспекты внешнего вида окна; Например, дочернее окно обрезается таким образом, что никакая часть дочернего окна не может отображаться вне границ родительского окна.
На следующем рисунке показано приложение, которое отображает диалоговое окно с двумя кнопками:
Окно приложения владеет диалоговым окном, а диалоговое окно является родительским для обоих окон кнопок. Эти связи показаны на следующей схеме:
Дескрипторы окон
Windows являются объектами — они содержат как код, так и данные, но они не являются классами C++. Вместо этого программа ссылается на окно, используя значение, называемое маркером. Маркер является непрозрачным типом. По сути, это просто число, используемое операционной системой для обнаружения объекта. вы можете создать изображение Windows как наличие большой таблицы всех созданных окон. Она использует эту таблицу для поиска окон по их дескрипторам. (Будь это именно то, как он работает внутренне, не имеет значения.) Для дескрипторов окон используется тип данных HWND, который обычно произносится как «аитч-ветер». Дескрипторы окон возвращаются функциями, которые создают окна: CreateWindow и CreateWindowEx.
Первый параметр — это маркер окна, которое требуется переместить. Другие параметры указывают новое расположение окна и необходимость перерисовки окна.
Помните, что дескрипторы не являются указателями. Если HWND является переменной, содержащей дескриптор, попытка разыменования дескриптора с помощью записи *hwnd является ошибкой.
Координаты экрана и окна
В зависимости от задачи можно измерять координаты относительно экрана, относительно окна (включая рамку) или относительно клиентской области окна. Например, можно разместить окно на экране с помощью экранных координат, но нарисовать внутри окна с помощью клиентских координат. В каждом случае источник (0, 0) всегда находится в левом верхнем углу области.
Структура оконного приложения
Оконные приложения строятся по принципам событийно-управляемого программирования (event-driven programming) — стиля программирования, при котором поведение компонента системы определяется набором возможных внешних событий и ответных реакций компонента на них. Такими компонентами в Windows являются окна.
Задача любого оконного приложения — создать главное окно и сообщить Windows функцию обработки событий для этого окна. Все самое интересное для приложения будет происходить именно в функции обработки событий главного окна.
В Windows программа пассивна. После запуска она ждет, когда ей уделит внимание операционная система. Операционная система делает это посылкой сообщений. Сообщения могут быть разного типа, они функционируют в системе достаточно хаотично, и приложение не знает, какого типа сообщение придет следующим. Логика построения Windows-приложения должна обеспечивать корректную и предсказуемую работу при поступлении сообщений любого типа.
Классическое оконное приложение, как правило, состоит по крайней мере из двух функций:
Стартовая функция WinMain
Эта функция использует последовательность вызовов API и при завершении возвращает операционной системе целое число.
Аргументы функции:
В структуре стартовой функции Windows можно выделить следующие операции, образующие «скелет» программы:
Регистрация класса окна
Регистрация класса окна осуществляется функцией
Члены структуры
style — устанавливает стиль(и) класса. Этот член структуры может быть любой комбинацией стилей класса.
Имя | Значение | Описание |
CS_VREDRAW | 0x01 | Вертикальная перерисовка: осуществлять перерисовку окна при перемещении или изменении высоты окна. |
CS_HREDRAW | 0x02 | Горизонтальная перерисовка: осуществлять перерисовку окна при перемещении или изменении ширины окна. |
CS_KEYCVTWINDOW | 0x04 | В окне будет выполняться преобразование виртуальных клавиш. |
CS_DBLCLKS | 0x08 | Окну будут посылаться сообщения о двойном щелчке кнопки мыши. |
CS_OWNDC | 0x20 | Каждому экземпляру окна присваивается собственный контекст изображения. |
CS_CLASSDC | 0x40 | Классу окна присваивается собственный контекст изображения,который можно разделить между копиями. |
CS_PARENTDC | 0x80 | Классу окна передается контекст изображения родительского окна. |
CS_NOKEYCVT | 0x100 | Отключается преобразование виртуальных клавиш. |
CS_NOCLOSE | 0x200 | Незакрываемое окно: в системном меню блокируется выбор пункта закрытия окна. |
CS_SAVEBITS | 0x800 | Часть изображения на экране, закрытая окном, сохраняется. |
CS_BYTEALIGNCLIENT | 0x1000 | Выравнивание клиентской области окна: использование границы по байту по оси x. |
CS_BYTEALIGNWINDOW | 0x2000 | Выравнивание окна: bспользование границы по байту по оси x. |
CS_PUBLICCLASS CS_GLOBALCLASS | 0x4000 | Определяется глобальный класс окон. |
lpfnWndProc — указатель на оконную процедуру.
cbClsExtra — устанавливает число дополнительных байт, которые размещаются вслед за структурой класса окна. Система инициализирует эти байты нулями, в большинстве случаев равен 0.
cbWndExtra — устанавливает число дополнительных байтов, которые размещаются вслед за экземпляром окна. Система инициализирует байты нулями.
hInstance — дескриптор экземпляра, который содержит оконную процедуру для класса.
hIcon — дескриптор значка класса, дескриптор ресурса значка. Если этот член структуры — NULL, система предоставляет заданный по умолчанию значок.
hCursor — дескриптор курсора класса, дескриптор ресурса курсора. Если этот член структуры — NULL, приложение устанавливает форму курсора всякий раз, когда мышь перемещается в окно прикладной программы.
Создание окна
Создание окна осуществляется функцией
Прототип функции находится в файле библиотеки user32.dll.
Возвращаемое значение – дескриптор создаваемого окна. В случае невозможности создать окно возвращается NULL.
Аргументы функции :
lpClassName – указывает на строку с ‘\0’ в конце, которая определяет имя класса окна. Имя класса может быть зарегистрированным функцией RegisterClass или любым из предопределенных имен класса элементов управления.
lpWindowName — указывает на строку с ‘\0’ в конце, которая определяет имя окна.
dwStyle — определяет стиль создаваемого окна.
Имя | Значение | Описание |
WS_BORDER | 0x00800000 | Окно имеет тонкую границу в виде линии. |
WS_CAPTION | 0x00C00000 | Окно имеет строку заголовка. |
WS_CHILD | 0x40000000 | Окно является дочерним. |
WS_DISABLED | 0x08000000 | Окно является изначально неактивным. |
WS_GROUP | 0x00020000 | Окно группирует другие управляющие элементы. |
WS_HSCROLL | 0x00100000 | Окно содержит горизонтальную полосу прокрутки. |
WS_MAXIMIZE | 0x01000000 | Исходный размер окна – во весь экран. |
WS_MINIMIZE | 0x20000000 | Исходно окно свернуто. |
WS_OVERLAPPED | 0x00000000 | Окно может быть перекрыто другими окнами. |
WS_POPUP | 0x80000000 | Всплывающее окно. |
WS_SYSMENU | 0x00080000 | Окно имеет системное меню в строке заголовка. |
WS_VISIBLE | 0x10000000 | Окно изначально видимое. |
WS_VSCROLL | 0x00200000 | Окно имеет вертикальную полосу прокрутки. |
у – определяет координату верхней стороны окна относительно верхней стороны экрана. Измеряется в единицах измерения устройства, чаще всего в точках (pt). Для дочернего окна определяет координату верхней стороны относительно начальной координаты родительского окна.
nHeight – определяет высоту окна в единицах измерения устройства.
hWndParent – дескриптор родительского окна.
hInstance — идентифицирует экземпляр модуля, который будет связан с окном.
lpParam — указывает на значение, переданное окну при создании.
Отображение и перерисовка окна
Отображение окна осуществляется функцией
Прототип функции находится в файле библиотеки user32.dll.
Возвращаемое значение: 1 – успешное отображение окна, 0 – ошибка.
Аргументы функции :
hWnd – дескриптор отображаемого окна.
nCmdShow – константа, определяющая, как будет отображаться окно согласно таблице.
Перерисовка окна осуществляется функцией
Прототип функции находится в файле библиотеки user32.dll.
Возвращаемое значение: 1 – успешная перерисовка окна, 0 – ошибка.
Аргумент функции hWnd – дескриптор окна.
Цикл обработки сообщений
Для получения сообщения из очереди используется функция:
Аргументы функции :
lpMsg — указатель на структуру сообщения.
Структура POINT имеет вид
hWnd — дескриптор окна, очередь для которого просматривается.
wMsgFilterMin — нижняя граница фильтра идентификаторов сообщений.
wMsgFilterMax — верхняя граница фильтра идентификаторов сообщений.
передает аргумент — структуру msg обратно в Windows для преобразования какого-либо сообщения с клавиатуры. Возвращает ненулевое значение в случае успешной расшифровки сообщения, 0 – ошибка.
передает аргумент — структуру msg обратно в Windows. Windows отправляет сообщение для его обработки соответствующей оконной процедуре — таким образом, Windows вызывает соответствующую оконную функцию, указанную при регистрации класса окна.
Пример стартовой функции, создающей и выводящей окно размером 500х300 точек:
Примечание : Для корректной сборки приложения используется многобайтовая кодировка.
Оконная функция — обработка сообщений окна
Оконная функция предназначена для обработки сообщений окна. Функция обработки сообщений окна организована по принципу ветвления, состоящего из последовательной проверки типа сообщения. При совпадении типа сообщения, переданного в структуре Message с соответствующей веткой, осуществляется его обработка. Минимальный вид оконной функции представлен ниже.
Вызов функции DefWindowProc() обрабатывает по умолчанию все сообщения, которые не обрабатывает оконная процедура.
Функция PostQuitMessage() сообщает Windows, что данный поток запрашивает завершение. Аргументом является целочисленное значение, которое функция вернет операционной системе.
Результат выполнения программы, выводящей окно:
Комментариев к записи: 13
Окна на чистом WinAPI. Или просто о сложном
Казалось бы, что WinAPI уходит в прошлое. Давно уже существует огромное количество кросс-платформенных фреймфорков, Windows не только на десктопах, да и сами Microsoft в свой магазин не жалуют приложения, которые используют этого монстра. Помимо этого статей о том, как создать окошки на WinAPI, не только здесь, но и по всему интернету, исчисляется тысячами по уровню от дошколят и выше. Весь этот процесс разобран уже даже не по атомам, а по субатомным частицам. Что может быть проще и понятнее? А тут я еще…
Но не все так просто, как кажется.
Почему о WinAPI сейчас?
В один прекрасный момент, изучая потроха одной из игр в весьма неплохом эмуляторе NES, я подумал: Вроде неплохой такой эмуль, а в отладчике нет такой простой вещи, как навигация по кнопкам клавиатуры, которая есть в любом нормальном отладчике.
Здесь я не зря дал ссылку на репозиторий, т.к. видно, что ребята столкнулись с проблемой, о которой речь пойдет ниже, но так и не решили ее.
О чем это я? А вот об этом кусочке кода:
Таким образом, авторы хотели добавить поддержку клавиатуры, но суровая реальность недр архитектуры диалоговых окон в Windows жестко пресекла такую самодеятельность. Те, кто пользовался эмулятором и отладчиком в нем, хоть раз видели это сообщение?
В чем же проблема?
Ответ такой: так делать нельзя!
И, возвращаясь, к изначальному вопросу о WinAPI: очень много популярных, и не очень, проектов продолжают его использовать и в настоящее время, т.к. лучше, чем на чистом API многие вещи не сделать (тут можно бесконечно приводить аналогии вроде сравнения высокоуровневых языков и ассемблера, но сейчас не об этом). Да и мало ли почему? Просто используют и все тут.
О проблеме
Диалоговые окна упрощают работу с GUI, одновременно лишая нас возможности сделать что-то самостоятельно. Например, сообщения WM_KEYDOWN/WM_KEYUP, приходящие в оконную процедуру, «съедаются» в недрах DefDlgProc, беря на себя такие вещи, как: Навигация по Tab, обработка клавиш Esc, Enter, и т.д. Кроме того, диалоги не нужно создавать вручную: проще, ведь, набросать кнопок, списков, в редакторе ресурсов, вызвать в WinMain CreateDialog/DialogBox и все готово.
Обойти такие мелкие неприятности просто. Есть, как минимум, два вполне легальных способа:
Tutorials?
Не побоюсь сказать, что все туториалы по созданию окон через WinAPI начинаются с такого незамысловатого кода, обозначая его, как «цикл обработки сообщений» (опущу детали по подготовке класса окна и прочую обвязку):
Здесь действительно все просто:
И ниже приводится пример правильного цикла.
Стоит сказать, что в шаблонах VS для Win32 приложений, написан именно такой неправильный цикл. И это очень печально. Ведь мало кто будет вникать в то, что сделали сами авторы, ведь это априори правильно. И неправильный код множится вместе с багами, которые очень сложно отловить.
После этого фрагмента кода, как правило, следует рассказ про акселераторы, и добавляется пара новых строчек (учитывая замечание в MSDN, предлагаю сразу писать правильный цикл):
Этот вариант я видел чаще всего. И он (та-дам) снова неправильный!
Сперва о том, что изменилось (потом о проблемах этого кода):
В первой строчке из ресурсов загружается таблица клавиш, при нажатии на которые, будет формироваться сообщение WM_COMMAND с соответствующим id команды.
Собственно TranslateAccelerator этим и занимается: если видит WM_KEYDOWN и код клавиши, которые есть в этом списке, то (опять же ключевой момент) будет формировать сообщение WM_COMMAND (MAKEWPARAM(id, 1)) и отправлять в соответствующую для дескриптора окна, указанного в первом аргументе, процедуру обработки.
Из последней фразы, думаю, стало понятно, в чем проблема предыдущего кода.
Поясню: GetMessage выхватывает сообщения для ВСЕХ объектов типа «окно» (в число которых входят и дочерние: кнопки, списки и прочее), а TranslateAccelerator будет отправлять сформированную WM_COMMAND куда? Правильно: обратно в кнопку/список и т.д. Но мы обрабатываем WM_COMMAND в своей процедуре, а значит нам интересно ее получать в ней же.
Ясно, что TranslateAccelerator надо вызывать для нашего созданного окна:
И вроде все хорошо и замечательно теперь: мы разобрали все детально и все должно работать идеально.
И снова нет. 🙂 Это будет работать правильно, пока у нас ровно одно окно — наше. Как только появится немодальное новое окно (диалог), все клавиши, которые будут в нем нажаты оттранслируются в WM_COMMAND и отправляться куда? И опять же правильно: в наше главное окно.
На этом этапе предлагаю не городить костылей по решению этой тупиковой ситуации, а предлагаю рассмотреть вещи, которые уже реже (или почти не встречаются) в туториалах.
IsDialogMessage
По названию этой функции можно подумать, что она зачем-то определяет: относится данное сообщение диалогу или нет. Но, во-первых, зачем нам это знать? А во-вторых, что с этой информацией нам делать дальше?
На самом деле, делает она чуть больше, чем следует из названия. А именно:
Во-вторых, она нам облегчит жизнь по всем остальным пунктам, перечисленным в списке (и даже немного больше).
Вообще, она используется где-то в недрах 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.
If the function retrieves the WM_QUIT message, the return value is zero.
Таким образом, выход из GetMessage-loop осуществится, если мы вызовем PostQuitMessage в процедуре окна. Что это значит?
Мы можем для каждого немодального окна в нашей программе создавать свой собственный подобный цикл. В данном случае DialogBoxParam нам не подходит, т.к. оно крутит свой собственный цикл и повлиять мы на него не можем. Однако если создадим диалог через CreateDialogBoxParam или окно через CreateWindow, то можно закрутить еще один цикл. При этом в каждом таком окне и диалоге мы должны вызывать PostQuitMessage:
Обратите внимание: теперь для каждого нового окна в нашей программе мы можем добавить в обработку собственную таблицу акселераторов. WM_QUIT будет выхватывать GetMessage из цикла для диалога, а внешний цикл его даже не увидит. Почему так происходит?
Дело в том, что внешний цикл «встал» на вызове DispatchMessage, который вызвал нашу процедуру, которая крутит свой собственный внутренний цикл GetMessage с таким же DispatchMessage. Классический вложенный вызов (в данном случае DispatchMessage). Посему внешний цикл не получит WM_QUIT и не завершится на этом этапе. Все будет работать стройно.
Но и тут есть свои недостатки:
Каждый такой цикл будет обрабатывать сообщения только для «своего» окна. Про другие-то мы здесь не знаем. А значит, если где-то объявится еще один цикл, то все остальные окна не будут получать нужной обработки своих сообщений парой TranslateAccelerator/IsDialogMessage.
Что ж, пора учесть все эти замечание и написать наконец правильную обработку всех сообщений от всех окон нашей программы. Хочу заметить, что ниже рассматривается случай для одного потока. Т.к. каждый поток имеет свою очередь сообщений, то для каждого потока придется создавать свои структуры. Делается это весьма тривиальными изменениями в коде.
Делаем красиво
Т.к. правильная постановка задачи является половиной решения, то сперва надо эту самую задачу правильно же и поставить.
Во-первых, было бы логично, что только активное окно принимает сообщения. Т.е. для неактивного окна мы не будем транслировать акселераторы и передавать сообщения в IsDialogMessage.
Во-вторых, если для окна не задана таблица акселераторов, то транслировать нечего, будем просто отдавать сообщение в IsDialogMessage.
Создадим простой 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.
Если первая будет возвращать дескриптор любого окна в системе, то последняя только того, которое использует очередь сообщений нашего потока. Т.к. нас интересуют окна только нашего потока (а значит те, которые будут попадать в нашу очередь сообщений), то и возьмем последнюю.
Так вот HandleAccelArray, руководствуясь переданным ей дескриптором на активное окно, ищет это самое окно в нашей мапе, и если оно там есть, отдает это сообщение на трансляцию в TranslateAccelerator, а затем (если первый не увидел нужного) в IsDialogMessage. Если и последняя не обработала сообщение, то возвращаем FALSE, чтобы пройти по стандартной процедуре TranslateMessage/DispatchMessage.
Теперь каждое дочернее окно вправе добавить себе любимую таблицу акселераторов и спокойно ловить и обрабатывать 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 выглядит так:
Теперь можно написать так:
И теперь, возвращаясь к тому же fceux, добавив всего одну строчку в код обработки команд от кнопок, мы получим желаемое: управлять дебагером с клавиатуры. Достаточно добавить небольшую обертку вокруг главного цикла сообщений и новую таблицу акселераторов с нужными соответствиями VK_KEY => IDC_DEBUGGER_BUTTON.
P.S.: Мало кто знает, но можно создавать свою собственную таблицу акселераторов, а теперь и применять ее прямо налету.
P.P.S.: Т.к. DialogBox/DialogBoxParam крутит собственный цикл, то от при вызове диалога через них акселераторы работать не будут и наш цикл (или циклы) будет «простаивать».
P.P.P.S.: После вызова HandleAccelWindow мап l_mAccelTable может измениться, т.к. TranslateAccelerator или IsDialogMessage вызывают DispatchMessage, а там может встретиться AddAccelerators или DelAccel в наших обработчиках! Поэтому лучше его после этой функции не трогать.
Пощупать код можно здесь. За основу был взят код, генерируемый из стандартного шаблона MS VS 2017.