Windows почему оконная функция не вешает интерфейс

Обновлено: 02.05.2024

В этом пошаговом руководстве показано, как создать традиционное классическое приложение Windows в Visual Studio. В примере приложения, которое вы создадите, используется API Windows для отображения "Hello, Windows desktop!" в окне. Код, созданный в этом пошаговом руководстве, можно использовать в качестве шаблона для создания других классических приложений Windows.

Для краткости некоторые операторы кода опущены в тексте. В разделе "Сборка кода" в конце этого документа показан полный код.

Предварительные требования

Компьютер под управлением Microsoft Windows 7 или более поздних версий. Мы рекомендуем Windows 10 или более поздней версии для оптимальной разработки.

Копия Visual Studio. Сведения о скачивании и установке Visual Studio см. в этой статье. Когда вы запускаете установщик, убедитесь, что установлена рабочая нагрузка Разработка классических приложений на C++ . Не беспокойтесь, если вы не установили эту рабочую нагрузку при установке Visual Studio. Вы можете снова запустить установщик и установить ее сейчас.

Detail of the Desktop development with C++ workload in the Visual Studio Installer.

Базовые значения об использовании интегрированной среды разработки Visual Studio. Если вы уже использовали классические приложения для Windows, вы, вероятно, справитесь. Общие сведения см. в обзоре возможностей интегрированной среды разработки Visual Studio.

Основные навыки владения языком C++. Не волнуйтесь, мы не будем делать ничего сложного.

Создание классического проекта Windows

Выполните следующие действия, чтобы создать свой первый Windows классический проект. По мере использования вы введете код для рабочего Windows классического приложения. Чтобы ознакомиться с документацией по предпочтительной версии Visual Studio, используйте селектор Версия. Он находится в верхней части оглавления на этой странице.

Создание классического проекта Windows в Visual Studio

В главном меню выберите Файл >Создать >Проект, чтобы открыть диалоговое окно Создание проекта.

В верхней части диалогового окна задайте языкC++, задайте для платформызначение Windows и задайте для Project тип "Рабочий стол".

В отфильтрованный список типов проектов выберите Windows мастер рабочего стола и нажмите кнопку "Далее". На следующей странице введите имя проекта, например DesktopApp.

В Обозреватель решений щелкните правой кнопкой мыши проект DesktopApp, выберите "Добавить" и выберите "Создать элемент".

В диалоговом окне Добавление нового элемента выберите Файл C++ (.cpp). В поле "Имя" введите имя файла, например HelloWindowsDesktop.cpp. Выберите Добавить.

Screenshot of the Add New Item dialog box in Visual Studio 2019 with Installed ></p>
<p> Visual C plus plus selected and the C plus plus File option highlighted.

Теперь проект создан, и исходный файл откроется в редакторе. Чтобы продолжить, перейдите к созданию кода.

Создание классического проекта Windows в Visual Studio 2017 г.

В меню Файл выберите команду Создать, а затем пункт Проект.

В диалоговом окне "Создать Project" в области слева разверните узел InstalledVisual>C++, а затем выберите Windows Desktop. В средней области выберите мастер Windows рабочего стола.

Screenshot of the New Project dialog box in Visual Studio 2017 with Installed ></p>
<p> Visual C plus plus > Windows Desktop selected, the Windows Desktop Wizard option highlighted, and DesktopApp typed in the Name text box.

В Обозреватель решений щелкните правой кнопкой мыши проект DesktopApp, выберите "Добавить" и выберите "Создать элемент".

В диалоговом окне Добавление нового элемента выберите Файл C++ (.cpp). В поле "Имя" введите имя файла, например HelloWindowsDesktop.cpp. Выберите Добавить.

Screenshot of the Add New Item dialog box in Visual Studio 2017 with Installed ></p>
<p> Visual C plus plus selected and the C plus plus File option highlighted.

Теперь проект создан, и исходный файл откроется в редакторе. Чтобы продолжить, перейдите к созданию кода.

Создание классического проекта Windows в Visual Studio 2015 г.

В меню Файл выберите команду Создать, а затем пункт Проект.

В диалоговом окне "Создать Project" в области слева разверните узел InstalledTemplatesVisual> >C++, а затем выберите Win32. В средней области выберите шаблон Проект Win32.

Screenshot of the New Project dialog box in Visual Studio 2015 with Installed ></p>
<p> Templates > Visual C plus plus > Win32 selected, the Win32 Project option highlighted, and DesktopApp typed in the Name text box.

На странице "Обзор " мастера приложений Win32 нажмите кнопку "Далее".

Create DesktopApp in Win32 Application Wizard Overview page.

На странице "Приложение Параметры" в разделе "Тип приложения" выберите Windows приложение. В разделе "Дополнительные параметры" снимите флажок предварительно скомпилированного заголовка, а затем выберите " Пустой проект". Чтобы создать проект, нажмите кнопку Готово.

В Обозреватель решений щелкните правой кнопкой мыши проект DesktopApp, выберите "Добавить" и выберите "Создать элемент".

В диалоговом окне Добавление нового элемента выберите Файл C++ (.cpp). В поле "Имя" введите имя файла, например HelloWindowsDesktop.cpp. Выберите Добавить.

Screenshot of the Add New Item dialog box in Visual Studio 2015 with Installed ></p>
<p> Visual C plus plus selected and the C plus plus File option highlighted.

Теперь проект создан, и исходный файл откроется в редакторе.

Создание кода

Далее вы узнаете, как создать код для классического приложения Windows в Visual Studio.

Запуск классического приложения Windows

Так же, как каждое приложение C и приложение C++ должны иметь функцию в качестве отправной main точки, каждое Windows классическое приложение должно иметь WinMain функцию. WinMain имеет следующий синтаксис:

Сведения о параметрах и возвращаемых значениях этой функции см. в разделе "Точка входа WinMain".

Что такое все эти дополнительные слова, такие как WINAPI , или CALLBACK , или, или HINSTANCE , или _In_ ? Традиционный API Windows использует методы typedefs и макросы препроцессора для абстрагирования некоторых подробностей типов и кода для конкретной платформы, таких как соглашения о вызовах, __declspec объявления и прагмы компилятора. В Visual Studio можно использовать функцию быстрой информации IntelliSense, чтобы узнать, что определяют эти определения типов и макросов. Наведите указатель мыши на интересующее слово или нажмите клавиши CTRLK, CTRLI++ для небольшого всплывающего окна, содержащего определение. Дополнительные сведения см. в разделе Using IntelliSense. Параметры и типы возвращаемых значений часто используют заметки SAL для перехвата ошибок программирования. Дополнительные сведения см. в статье "Использование заметок SAL для уменьшения дефектов кода C/C++".

Windows требуются классические программы. TCHAR определяет макрос, который разрешается в конечном счете wchar_t , если символ ЮНИКОД определен в проекте, в противном случае он разрешается в char . Если вы всегда создаете сборку с поддержкой ЮНИКОДа, вам не требуется TCHAR и может использоваться напрямую wchar_t .

Наряду с функцией WinMain каждый Windows классическое приложение также должно иметь функцию window-procedure. Обычно эта функция называется WndProc , но ее можно присвоить любое имя. WndProc имеет следующий синтаксис:

Дополнительные сведения см. в разделе Процедуры окна.

Добавление функциональных возможностей в функцию WinMain

WinMain В функции вы заполняете структуру типа WNDCLASSEX. Структура содержит сведения о окне: значок приложения, цвет фона окна, имя, отображаемое в заголовке строки, помимо прочего. Важно отметить, что он содержит указатель функции на процедуру окна. В приведенном ниже примере показана типичная структура WNDCLASSEX .

Сведения о полях приведенной выше структуры см. в разделе WNDCLASSEX.

Теперь можно создать окно. Используйте функцию CreateWindowEx .

Эта функция возвращает HWND дескриптор окна. Дескриптор немного похож на указатель, который Windows используется для отслеживания открытых окон. Дополнительные сведения см. в разделе Типы данных Windows.

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

На этом этапе функция WinMain должна напоминать приведенный ниже код.

Добавление функциональных возможностей в функцию WndProc

HDC в коде — это дескриптор контекста устройства, который используется для рисования в клиентской области окна. Используйте функции для EndPaint подготовки BeginPaint и завершения рисования в клиентской области. BeginPaint возвращает дескриптор контексту устройства отображения, используемому для рисования в клиентской области; EndPaint завершает запрос на рисование и освобождает контекст устройства.

Сборка кода

Как и было обещано, ниже приведен полный код для рабочего приложения.

Сборка примера

Удалите любой код, введенный в HelloWindowsDesktop.cpp в редакторе. Скопируйте этот пример кода и вставьте его в HelloWindowsDesktop.cpp:

В меню Построение выберите Построить решение. Результаты компиляции должны отображаться в окне вывода в Visual Studio.

Чтобы запустить приложение, нажмите клавишу F5. Окно, содержащее текст "Hello, Windows desktop!", должно появиться в левом верхнем углу экрана.

Screenshot of the running DesktopApp Project.

Поздравляем! Вы выполнили это пошаговое руководство и создали традиционное классическое приложение Windows.

Казалось бы, что 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 из цикла для диалога, а внешний цикл его даже не увидит. Почему так происходит?

Дело в том, что внешний цикл «встал» на вызове DispatchMessage, который вызвал нашу процедуру, которая крутит свой собственный внутренний цикл GetMessage с таким же DispatchMessage. Классический вложенный вызов (в данном случае DispatchMessage). Посему внешний цикл не получит WM_QUIT и не завершится на этом этапе. Все будет работать стройно.

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

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

Создадим простой 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.S.: Т.к. DialogBox/DialogBoxParam крутит собственный цикл, то от при вызове диалога через них акселераторы работать не будут и наш цикл (или циклы) будет «простаивать».

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

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

Чтобы статья воспринималась максимально полезной и практичной, в ней предлагается заготовка (почти готовый к употреблению шаблон) службы, обрабатывающей очередь неких задач (или заданий – кому как больше нравится); после того, как все из них обработаны, служба тут же завершается. Если представить графически, то читатель познакомится со следующей конструкцией:

Взаимодействие службы с очередью и управляющим приложением

Техническое задание

Предложенное решение будет обладать перечисленными возможностями, а также предполагать следующее:

    Очередь рассматривается как некая абстрактная структура, то есть кем она реализована, где хранится (в файле, БД или где-то ещё) и как конкретно с ней взаимодействовать (в виде программного кода) – всё это непринципиально и слабо пересекается с темой материала, однако предполагается, что задачи в ней обладают как минимум двумя свойствами:
      Приоритетом, задающим порядок обработки. Статусом, допускающим три значения:
      1. Ожидает обработки.
      2. Успешно обработана.
      3. Ошибка (не удалось обработать).
    • Сразу после старта принимается за тяжкие труды и начинает, с учётом приоритета, извлекать из очереди задачи с первым статусом (который «ожидающий»), после чего, в зависимости от результата обработки, обновляет статус у каждой из них; работа прекращается после того, как в очереди не осталось необработанных элементов. Если поступает команда на остановку, то обработка текущей задачи прерывается и служба завершается.
    • Во время работы может принять особую (нестандартную) команду от управляющего приложения (УП), суть которой описана чуть ниже.
    • Дабы не наделять службу чрезмерным набором прав, из-за которых может пострадать безопасность всей ОС, вместо обычно применяемого аккаунта LocalSystem станет использоваться специальный пользователь, создаваемый на лету.
    • При установке происходит автоматическое назначение минимально необходимых прав как пользователю самой службы (от имени которого она должна запускаться – о нём шла речь в предыдущем пункте), так и пользователю управляющего приложения.

    Кнопки управления службой в Диспетчере

    Служба

    В данном случае, веских причин изобретать велосипед для реализации службы не имеется, поэтому основа дальнейшего кода – это стандартный для IDE подход к созданию, основанный на классе TService . Также необходимо отметить, что автор использует не самую новую версию Delphi (10.1 Berlin), в связи с чем в иных выпусках могут иметься свои особенности (в более свежих, к примеру, часть предложенного функционала может быть уже реализована, однако подобное маловероятно, учитывая стойкое нежелание разработчиков Delphi развивать TService ).

    Описание кода службы логично вести в соответствии с циклом её жизни в системе – то есть начать с момента установки (регистрации).

    Установка

    Собственно самостоятельно реализовывать регистрацию и не требуется, т. к. запуск исполняемого файла службы с ключом /install сделает всё необходимое – программист от данной рутины избавлен. Намного интересней выглядит момент сразу после установки (чему соответствует событие AfterInstall ), где и удобно приступить к воплощению части означенного в ТЗ ; однако, хотелось бы начать с малого и показать на простом примере как происходит изменение параметра установленной службы – будет сделано то, чего уже так давно не добавляют в Delphi – реализована возможность указать описание, отображаемое, например, в Диспетчере:

    Описание службы в Диспетчере

    Основа обработчика указанного события, постепенно расширяемая далее, выглядит так:


    Здесь, прежде всего, выполняется получение дескриптора Менеджера служб (Service Control Manager), после чего у него запрашивается дескриптор уже нашей (только что установленной) службы по её имени; доступ к обоим объектам выбран минимально необходимый – SC_MANAGER_CONNECT и SERVICE_CHANGE_CONFIG , причём SC_MANAGER_CONNECT не требуется указывать, т. к. он подразумевается неявно (именно поэтому последний параметр функции OpenSCManager равен нулю).

    Пользователь

    Далее, чтобы непосредственно перейти к реализации описанных в начале требований, определимся с пользователем, от имени которого служба станет выполняться: до Windows 7 и Windows Server 2008 R2, если требовалось максимально ограничить службу в правах, дав ей исключительно те, что действительно нужны, было необходимо самостоятельно создавать обычного пользователя ОС – а теперь же появился виртуальный пользователь (virtual account), все заботы по управлению которым берёт на себя Windows. Применительно к службе (если делать это вручную через Диспетчер), для создания такого пользователя нужно лишь при указании его имени добавить префикс NT Service\, а пароль оставить пустым:

    Создание виртуального пользователя через свойства службы в Диспетчере

    Казалось бы, чего проще – действуем аналогично в Инспекторе объектов Delphi и получаем тот же результат:

    Создание виртуального пользователя через Инспектор объектов в Delphi

    Но не тут-то было! В случае виртуального пользователя, WinAPI-функция CreateService , применяемая в модуле Vcl.SvcMgr для установки службы, в последнем параметре, содержащем пароль, должна получить значение nil , а не пустую строку,


    Надо сказать, что имя виртуального пользователя, указываемое после префикса, совсем не обязательно должно совпадать с именем службы – главное обеспечить его уникальность.

    Права

    На следующем этапе необходимо позаботиться о правах двух пользователей:

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

    Права на исполняемый файл службы


    Константа SERVICE_USER_DEFINED_CONTROL у пользователя УП отвечает за право на передачу нестандартной команды, указанной в требованиях. Реализация же GrantAccess основана на C++-примере из документации Microsoft:


    Завершая изыскания с AfterInstall , необходимо отметить, что любое исключение в этом событии приведёт к удалению только что установленной службы (с записью текста исключения в журнал Windows), а в приведённом коде его может сгенерировать, к примеру, функция Win32Check .

    В заключение подраздела также хочется остановиться на моменте, связанном с правами, назначенными выше пользователю УП: если, предположим в целях отладки, их необходимо поменять, то совершенно не обязательно для этого удалять и заново устанавливать службу – достаточно воспользоваться всем известной утилитой Process Explorer: когда служба запущена, следует открыть её свойства и перейти на вкладку Services, после чего пройтись по показанным шагам:

    Права на службу

    Обработка очереди

    Как известно, Delphi предлагает два подхода к реализации службы (подробнее о них можно узнать в материале на другом ресурсе в разделе «3. События службы»):

    1. На основе событий OnStart и OnStop , что подразумевает самостоятельное создание потоков, содержащих нужный функционал.
    2. На основе события OnExecute , обработчик которого выполняется в заранее заботливо созданном TService потоке, причём служба сразу же остановится после выхода из события; именно данный вариант хорошо подходит под поставленную в статье цель – как только в очереди обработаны все задачи, делать больше нечего и необходимо завершиться.

    Основа события

    В первом приближении код OnExecute прост и незатейлив – идёт извлечение задач до тех пор, пока они имеются в очереди:


    Стоит пояснить, что задачи берутся не по одиночке, а именно порциями исходя из соображения, что в реальном мире обычно затраты на получение сразу нескольких элементов из хранилища значительно ниже, чем их выборка по одному (именно так, скажем, обстоит дело с базами данных).

    Прерывание обработки

    Несложно заметить, что в текущем виде не предусмотрено никакого механизма по прекращению цикла извлечения задач, а ведь причин такого прерывания, согласно ТЗ, может быть две:

      службы, после которой никакого ожидания обработки текущей задачи быть не должно – она прерывается как можно быстрее, после чего все оставшиеся в порции задачи тоже отбрасываются и служба завершается. Команда на повторную обработку задач с третьим статусом, для чего необходимо прервать работу по текущей (как и в случае команды на остановку), обновить статус всех означенных задач на первый, запросить новую порцию и далее действовать как обычно; надобность прерывать обработку текущей порции связана с тем, что среди задач с только что установленным первым статусом могут иметься обладающие бо́льшим приоритетом.


    Это исключение станет генерироваться в новой локальной процедуре CheckInterruption (как – об этом чуть позже), а реакция на него имеет следующий вид:


    От разработчика требуется лишь вставлять вызов CheckInterruption периодически, через небольшие этапы обработки задачи в ProcessTask , навроде такого:

    Взаимодействие с Менеджером служб


    Теперь можно полностью реализовать процедуру:


    Здесь методы ReportStatus и ProcessRequests отвечают за взаимодействие с Менеджером, а константа RESET_QUEUE_ERRORS_CONTROL_CODE (её допустимые значения см. в описании параметра dwControl ) объявлена в новом модуле Services.Queue.Constants :


    Полезность добавления модуля проистекает из того факта, что управляющее приложение в нашем случае тоже написано на Delphi и при отправке специальной команды эта константа в нём тоже потребуется:

    Зависимости от модуля Services.Queue.Constants

    Кстати, если читатель задаётся вопросом о целесообразности добавления поля FCustomCode , когда, казалось бы, можно сгенерировать исключение прямо в методе DoCustomControl ,

    то ответ довольно прост – в модуле Vcl.SvcMgr вызов DoCustomControl окружён конструкцией try. except , перехватывающей любые исключения без разбора (а вся обработка сводится к добавлению записей с их текстом в Windows-лог).

    Окончательный вариант

    В качестве последнего штриха к реализации службы, необходимо разобраться хоть и с небольшой (в плане устранения), но всё же загвоздкой, а именно: в текущем виде, если в очереди все задачи обработаны, но некоторые из них имеют третий статус (завершились ошибкой), то заново такие взять в работу не получится – служба после старта станет сразу завершаться, а, соответственно, и не сможет никогда принять команду от УП на повторную обработку ошибок. К счастью, при запуске службы можно передать ей произвольное количество текстовых параметров, хотя в данном случае достаточно одного параметра-флага – факт его наличия будет говорить о том, что ещё перед циклом по очереди требуется вызвать уже применявшуюся процедуру ResetQueueErrors :


    Важно понимать, что эти параметры не имеют ничего общего с ключами, использующимися при установке и удалении, – те применяются при самостоятельном запуске исполняемого файла службы, а свойство Param содержит то, что было передано специальной WinAPI-функции, предназначенной для старта служб (она будет упомянута в следующем разделе). Что касается константы ResetQueueErrorsParam , то она объявлена в модуле Services.Queue.Constants :

    Управляющее приложение

    В целях сосредоточения на главном, и дабы не отвлекаться на второстепенные нюансы, УП представляет собой обычный VCL-проект из одной простейшей формы, состоящей из 4-х кнопок; вместе с тем, весь приводимый код использует только WinAPI, поэтому применять его можно где угодно – хоть в другой службе, хоть вообще поместить в DLL.

    Окно управляющего приложения

    Кнопки отвечают за уже знакомые действия:

    1. Запуск без изысков (как будто через Диспетчер служб).
    2. Аналогично первой кнопке, но с параметром, отвечающим за предварительный сброс у задач третьего статуса.
    3. Передача службе специальной команды (см. константу RESET_QUEUE_ERRORS_CONTROL_CODE ).
    4. Остановка службы (как будто через Диспетчер служб).

    Предварительные действия

    В дальнейшем довольно часто будет требоваться дескриптор Менеджера служб, поэтому, чтобы не получать его каждый раз заново, сделаем это при создании формы; также сотворим полезный метод OpenService , избавляющий далее от дублирования кода и возвращающий дескриптор службы:

    Основной код

    Запуск службы – без параметров и с ними – отличается незначительно (и там и там применяется одна и та же WinAPI-функция), поэтому видится разумным создать у формы метод, который затем и вызывать при нажатии на первые две кнопки:


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


    Две последние кнопки тоже позволяют обойтись вызовом одного и того же дополнительного метода, с совсем уж простой реализацией:


    Здесь в переменной ServiceStatus возвращается последнее, самое свежее состояние службы, однако оно в данном контексте неинтересно, поэтому полученное значение просто игнорируется. Таким образом, 3-я и 4-я кнопки на нажатие реагируют так:


    Последнее, о чём хочется сказать, касается нестандартных команд (рассмотренная служба реагирует только на одну – RESET_QUEUE_ERRORS_CONTROL_CODE ): если они в Вашем случае являются более сложными, требующими для выполнения дополнительную информацию, а не просто факт получения службой одного числового кода, то для передачи таких сведений придётся задействовать механизмы межпроцессного обмена – разделяемую память, неименованные каналы и т. п.

    Эта статья адресована таким же, как и я новичкам в программировании на С++ которые по воле случая или по желанию решили изучать WinAPI.
    Хочу сразу предупредить:
    Я не претендую на звание гуру по C++ или WinAPI.
    Я только учусь и хочу привести здесь несколько примеров и советов которые облегчают мне изучение функций и механизмов WinAPI.

    В данной статье я предполагаю что вы уже достаточно ознакомились с С++, что бы уметь создавать классы и перегружать для них различные операторы и что вы уже «прятали» какие-то свои механизмы в класс.

    Создание и использование консоли

    Для отладки Win32 приложения или просто для того что посмотреть как оно там всё внутри происходит я всегда пользуюсь консолью.
    Так как вы создаете GUI приложение, а не консольное, то консоль не подключается. Для того что бы её вызвать в недрах интернета был найден вот этот код

    if (AllocConsole())
    <
    int hCrt = _open_osfhandle((long)GetStdHandle(STD_OUTPUT_HANDLE), 4);
    *stdout = *(::_fdopen(hCrt, "w"));
    ::setvbuf(stdout, NULL, _IONBF, 0);
    *stderr = *(::_fdopen(hCrt, "w"));
    ::setvbuf(stderr, NULL, _IONBF, 0);
    std::ios::sync_with_stdio();
    >
    Для удобства советую обернуть его в функцию. Например:
    void CreateConsole()
    if (AllocConsole())
    <
    int hCrt = _open_osfhandle((long)GetStdHandle(STD_OUTPUT_HANDLE), 4);
    *stdout = *(::_fdopen(hCrt, "w"));
    ::setvbuf(stdout, NULL, _IONBF, 0);
    *stderr = *(::_fdopen(hCrt, "w"));
    ::setvbuf(stderr, NULL, _IONBF, 0);
    std::ios::sync_with_stdio();
    >

    Наследование объектов для вывода и арифм. операций

    При создании и изучении самих «окошек» мне всегда требовалось выводить в консоль какое-нибудь значение.
    Например:
    Вы получаете размер клиентской области окна с помощью функции GetClientRect куда как параметр передается адрес объекта структуры RECT, что бы заполнить этот объект данными. Если вам нужно узнать размер полученной клиентский области вы просто можете вывести его в уже подключённую консоль

    Но делать так каждый раз (особенно если вам часто приходиться делать что-то подобное) очень неудобно.
    Здесь нам на помощь приходит наследование.
    Создайте класс который открыто наследуется от структуры RECT и перегрузите оператор вывода Например:

    Теперь просто выводите обьект с помощью cout/wcout:

    И вам в удобном виде будет выводиться всё так, как вам требуется.
    Так же вы можете сделать с любыми нужными вам операторами.
    Например, если надо сравнивать или присваивать структуры (допустим тот же RECT или POINT) — перегрузите operator==() и operator=() соответственно.
    Если хотите реализовать оператор меньше < что бы быстро сравнивать размеры окна и т.д. перегрузите operatorТак вы можете делать, я предполагаю, почти с любыми структурами и самое главное, что все функции которые работают с обычным объектом структуры RECT так же хорошо будут работать и с его наследником.
    И еще рекомендую всю эту красоту вынести в отдельный подключаемый файл и использовать при необходимости.

    Свой класс

    Не знаю как у остальных, но так как я совсем зелёный, я решил для каждой изученной функции или для каждой главы/под главы книги создавать новый проект, что бы всё было по полочкам и можно было в любой момент вернуться и освежить в памяти необходимые моменты.
    Так как в WinAPI даже для создания простейшего окна нужно заполнить структуру класса, зарегистрировать её и написать тривиальную оконную процедуру, я после третьего или четвертого проекта вспомнил что я всё таки на С++ пишу.
    В итоге я всё спрятал в простенький класс. Хендл окна, его имя, имя класса, адрес оконной процедуры, класс окна (WNDCLASS) всё спрятано в private секцию класса.
    Для их получения достаточно описать простенькие методы-Get'еры, к примеру:
    HWND GetHWND()
    LPCTSTR GetClsName() и т.д.
    Заполнение и регистрация оконного класса, создание самого окна и его показ производиться в конструкторе.
    Для удобства можно перегрузить конструктор, а заполнение и регистрацию оконного класса спрятать в отдельную private функцию класса и вызывать в каждом из конструкторов. Удобство перегрузки состоит в том, что мне иногда необходимо создать совсем простенькое окно и я вызываю конструктор с двумя параметрами — имя окна и hinstance приложения.
    В другой раз мне нужно создать окно с особыми размерами, не с дефолтной процедурой окна и с каким-то другим определённым стилем — я вызываю конструктор с сопутствующими параметрами.
    Этот класс у меня определён в отдельно подключаемом файле, который лежит в include папке IDE.
    Шаблон такого класса:
    class BaseWindow
    WNDCLASSEX _wcex;
    TCHAR _className[30];
    TCHAR _windowName[40];
    HWND _hwnd;
    bool _WindowCreation();
    public:
    BaseWindow(LPCTSTR windowName,HINSTANCE hInstance,DWORD style,UINT x,UINT y,UINT height,UINT width);
    BaseWIndow(LPCTSTR windowName,HINSTANCE hInstance);
    const HWND GetHWND()const
    LPCTSTR GetWndName()const
    >;

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


    Всё описанное справедливо для:
    Платформа — Windows 7 32 bit
    IDE — Visual Studio 2010
    Может у кого-то эти советы будут вызывать смех и иронию, но всё-таки мы все когда-то в чём-то были новичками/стажерами/junior'ами.
    Прошу к посту отнестись с понимаем. Конструктивная критика, конечно же приветствуется.

    Системный класс Предназначение
    BUTTON Кнопка.
    COMBOBOX Комбинированное окно (окно со списком и поля выбора).
    EDIT Окно редактирования текста.
    LISTBOX Окно со списком
    SCROLLBAR Полоса прокрутки
    STATIC Статический элемент (текст)


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

    HWND WINAPI CreateWindow (
    _In_opt_ LPCTSTR lpClassName, // имя предопределенного класса
    _In_opt_ LPCTSTR lpWindowName, // текст
    _In_ D WORD dwStyle, // стиль
    _In_ int x, // координата x
    _In_ int y, // координата y
    _In_ int nWidth, // ширина
    _In_ int nHeight, // высота
    _In_opt_ HWND hWndParent, // дескриптор родительского окна
    _In_opt_ HMENU hMenu, // номер пункта меню
    _In_opt_ HINSTANCE hInstance, // дескриптор приложения
    _In_opt_ LPVOID lpParam ); // NULL

    Указанная функция возвращает дескриптор элемента управления окна, который может быть впоследствии использован для анализа элемента управления, с которым связано обрабатываемое событие. Дескриптор кнопки, например, передается в оконную функцию в качестве параметра lparam .

    Таблицу стилей элементов управления окна можно устанавливать в параметре dwStyle как и для создания родительского окна. При этом обязательно указывается, что создаваемое окно является дочерним — WS_CHILD .

    Кнопка

    Обработка нажатия кнопки:

    LONG WINAPI WndProc( HWND hwnd, UINT Message, WPARAM wparam, LPARAM lparam) <
    .
    switch (Message) <
    case WM_COMMAND :
    if (lparam == ( LPARAM )hBtn) <
    //обработка нажатия кнопки
    >
    break ;
    .
    >
    >

    Поле редактирования

    Поле редактирования

    Поле редактирования — прямоугольное дочернее окно, внутри которого пользователь может напечатать с клавиатуры текст. Пользователь выбирает орган управления и дает ему фокус клавиатуры, щелкая по нему мышью или перемещая в него, каретку путем нажатия клавиши ТАБУЛЯЦИИ (TAB). Пользователь может вводить текст, когда окно редактирования текста отображает мигающую каретку.

    Для считывания информации из поля редактирования используется функция

    int WINAPI GetWindowText(
    _In_ HWND hWnd, // дескриптор поля
    _Out_ LPTSTR lpString, // указатель на текстовую строку
    _In_ int nMaxCount ); // максимальное количество символов

    Возвращаемое значение – длина считанной текстовой строки.

    Для установки текста в поле редактирования используется функция

    BOOL WINAPI SetWindowText(
    _In_ HWND hWnd, // дескриптор поля
    _In_opt_ LPCTSTR lpString ); // указатель на текстовую строку

    В случае успешного завершения функция возвращает ненулевое значение.

    Статический текст

    Статический текст — текстовое поле, окно или прямоугольник, используемый для надписей, не подлежащих редактированию.

    Для установки статического текста используется та же функция SetWindowText .

    Пример на С++ Найти сумму двух чисел
    Программа создает два поля редактирования для ввода чисел и кнопку «Рассчитать», при нажатии на которую выводится статический текст, соответствующий сумме.
    Для перевода строки в целое число и обратно использованы функции StrToInt и IntToStr , описанные в статье Преобразование строки в число и обратно. Также использовалась многобайтовая кодировка.

    Сумма двух чисел


    Результат выполнения

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