Как выйти / прервать цикл сообщений Windows в консольном приложении и отличия от настольного приложения Windows?

Я хочу выйти из цикла сообщений Windows. Как и C ++, как разорвать цикл сообщений в хуке Windows. Я придумал одно решение, которое отлично работает для приложения Window Desktop, но не работает для консольного приложения. Как такое могло случиться?

РЕДАКТИРОВАТЬ: я загружаю свои коды в https://github.com/hellohawaii3/Experiment, клонирую его и тогда вы сможете быстро воспроизвести мою проблему. Спасибо!

1. Почему я хочу это сделать?

Пишу консольное приложение. Во-первых, пользователей просят установить некоторые параметры, которые определяют поведение приложения. Затем приложение запускает цикл сообщений и отслеживает ввод с клавиатуры / мыши. Пользователи могут захотеть изменить настройку, поэтому я хочу, чтобы пользователи нажимали горячую клавишу, выходили из цикла сообщений и возвращались к началу приложения.

Если вы знаете, как реализовать эту функцию, не беспокоясь о выходе из цикла обработки сообщений, сообщите мне! Однако я также хочу знать, почему мое решение не работает для консольного приложения и хорошо работает для настольного приложения. Спасибо за вашу помощь!

2. Что я пробовал.

Я использую переменную типа bool recieve_quit. Когда на клавиатуре нажимается определенная клавиша, функция обратного вызова ловушки устанавливает для переменной значение True. Для каждого цикла получения сообщения сначала проверяйте переменную 'recieve_quit' и завершайте работу, когда переменная имеет значение False.

3. Результат моего эксперимента

Для консольного приложения переменная «recieve_quit» устанавливается правильно при нажатии определенной клавиши, однако цикл сообщений продолжается.

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

4. Настройки эксперимента

Я использую VS2019, C ++, windows 10 home 1909.

Я использую dll inject, чтобы установить ловушку для своего консольного приложения.

5.Мой код

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

(а) Мое консольное приложение

Консоль:

// Console.cpp

#include <iostream>
#include "dll_func.h"
#include <windows.h>

int main()
{
    MSG msg;
    HHOOK hhook_tmp2 = SetWindowsHookEx(WH_KEYBOARD_LL, HandleKeyboardEvent, hDllModule, 0);
    while (recieve_quit == false)
    {
        if (GetMessage(&msg, nullptr, 0, 0)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    MessageBox(NULL, TEXT("APP QUIT"), TEXT(" "), MB_OK);
}

(б) моя dll, содержащая функцию перехвата

Мой файл dll_func.h после doc https://docs.microsoft.com/en-us/cpp/build/walkthrough-creating-and-using-a-dynamic-link-library-cpp?view=msvc-160

#pragma once

#ifdef DLL1_EXPORTS
#define DLLFUNC_API __declspec(dllexport)
#else
#define DLLFUNC_API __declspec(dllimport)
#endif

#include "framework.h"

extern "C" DLLFUNC_API bool recieve_quit;
extern "C" DLLFUNC_API LRESULT CALLBACK HandleKeyboardEvent(int nCode, WPARAM wParam, LPARAM lParam);// refer to https://stackoverflow.com/a/60913531/9758790
extern "C" DLLFUNC_API HMODULE hDllModule;//or whatever name you like 

Мой файл dll_func.cpp, содержащий определение функции перехвата.

#include "pch.h"
#include <windows.h>
#include "dll_func.h"

bool recieve_quit = false;
LRESULT CALLBACK HandleKeyboardEvent(int nCode, WPARAM wParam, LPARAM lParam)
{
    PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
    if (wParam == WM_KEYDOWN) {
        if (p->vkCode == VK_F8) {
            recieve_quit = true;
        }
    }

    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

Dllmain.cpp также немного изменяется после его создания в Visual Studio, как было предложено https://stackoverflow.com/a/60913531/9758790, чтобы получить подсказку о DLL для внедрения.

// dllmain.cpp
#include "pch.h"
#include "dll_func.h"

// refer to https://stackoverflow.com/a/60913531/9758790
HMODULE hDllModule;//or whatever name you like 
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    hDllModule = hModule;
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

(c) Приложение "Мой рабочий стол".

Я выбираю «Создание настольного приложения Windows» при создании решения с помощью Visual Studio. Большинство кодов автоматически генерируется VS. Коды, которые я добавил и изменил, заключены в символы *****.

// GUI.cpp
//

#include "framework.h"
#include "GUI.h"

#define MAX_LOADSTRING 100

HINSTANCE hInst;
WCHAR szTitle[MAX_LOADSTRING];
WCHAR szWindowClass[MAX_LOADSTRING];

// *********************** ADDED BY ME! ***********************
// same as dll_func.cpp
bool recieve_quit = false;
LRESULT CALLBACK HandleKeyboardEvent(int nCode, WPARAM wParam, LPARAM lParam)
{
    //FILE* file;
    //fopen_s(&file, "./temp0210222.txt", "a+");
    //fprintf(file, "Function keyboard_hook called.n");
    //fclose(file);
    PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
    if (wParam == WM_KEYDOWN) {
        if (p->vkCode == VK_F8) {
            recieve_quit = true;
        }
    }

    return CallNextHookEx(NULL, nCode, wParam, lParam);
}
// *********************** END OF MY CODES ***********************

ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // *********************** ADDED BY ME! ***********************
    HHOOK hhook_tmp2 = SetWindowsHookEx(WH_KEYBOARD_LL, HandleKeyboardEvent, hInst, 0);
    // *********************** END OF MY CODES ***********************

    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_GUI, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_GUI));

    MSG msg;

    // main message loop:
    // *********************** MODIFIED BY ME! ***********************
    //while (GetMessage(&msg, nullptr, 0, 0))
    //{
    //    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
    //    {
    //        TranslateMessage(&msg);
    //        DispatchMessage(&msg);
    //    }
    //}
    while (recieve_quit == false)
    {
        if (GetMessage(&msg, nullptr, 0, 0)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    MessageBox(NULL, TEXT("APP QUIT"), TEXT(" "), MB_OK);
    // *********************** END OF MODIFICATION ***********************

    return (int) msg.wParam;
}



//
//  Function: MyRegisterClass()
ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = 0;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_GUI));
    wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_GUI);
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&wcex);
}

//
//   Function: InitInstance(HINSTANCE, int)
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance;

   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

//
//  Function: WndProc(HWND, UINT, WPARAM, LPARAM)
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_COMMAND:
        {
            int wmId = LOWORD(wParam);
            switch (wmId)
            {
            case IDM_ABOUT:
                DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
                break;
            case IDM_EXIT:
                DestroyWindow(hWnd);
                break;
            default:
                return DefWindowProc(hWnd, message, wParam, lParam);
            }
        }
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hWnd, &ps);
            EndPaint(hWnd, &ps);
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

// "About"
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        return (INT_PTR)TRUE;

    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
            EndDialog(hDlg, LOWORD(wParam));
            return (INT_PTR)TRUE;
        }
        break;
    }
    return (INT_PTR)FALSE;
}

(г) Советы по воспроизведению моих результатов

Для консольного приложения: создайте новое решение консольного приложения с именем Console и скопируйте мои коды, чтобы заменить Console.cpp. Добавьте новый проект в это решение, выберите Создать новый проект - ›DLL, назовите его Dll1. Создайте dll_func.h, dll_func.c и скопируйте мои коды. Не забудьте также изменить dllmain.cpp. Задайте путь к AdditionalIncludeDirectories, добавьте dll1.lib в AdditionalDependencies. Также укажите путь к файлам dll и lib.

Для настольного приложения для Windows: создайте решение для настольного приложения для Windows, назовите его GUI. Скопируйте мои коды в GUI.cpp и просто запустите. Нажмите клавишу F8, приложение закроется и появится окно сообщения, как и ожидалось.


person hellohawaii    schedule 14.02.2021    source источник
comment
Как вы узнали, что для консольного приложения recieve_quit устанавливается правильно при нажатии определенной клавиши?   -  person nevilad    schedule 14.02.2021
comment
@nevilad Спасибо за внимание! Я использую Visual Studio для отладки. Я добавляю точку останова в первую строку функции перехвата HandleKeyboardEvent, чтобы программа приостанавливалась при нажатии любой клавиши. Я нажимаю F8, затем продолжаю и нажимаю другую произвольную клавишу, чтобы снова запустить точку останова. Тогда я использую команду? recieve_quit и обнаружил, что для него установлено значение True, чего я и ожидал. Однако цикл обработки сообщений не завершается.   -  person hellohawaii    schedule 14.02.2021
comment
Откройте окно Debug- ›Windows-› Processes при достижении точки останова. В каком процессе он был нанесен?   -  person nevilad    schedule 14.02.2021
comment
@nevilad Так как я добавил точку останова в файл dll, а процесс в окне Debug- ›Windows-› Processes - Dll1, dll. Вдохновленный вашим комментарием, я попытался добавить точку останова в цикле while в основной функции Console.cpp, однако она никогда не срабатывает. И если я добавлю точку останова в начале основной функции в Console.cpp, точка останова может сработать. Я использую команду? recieve_quit, когда срабатывает точка останова в основной функции, и ему сообщили, что неопределенный идентификатор recieve_quit. Еще раз спасибо!   -  person hellohawaii    schedule 14.02.2021
comment
Вы можете попробовать использовать Qt или FLTK. Оба являются мощными фреймворками графического интерфейса на C ++.   -  person Basile Starynkevitch    schedule 14.02.2021


Ответы (1)


В этом сообщении объясняется, почему консольные приложения не получают сообщения клавиатуры и как они могут их обрабатывать: не входит в цикл Windows GetMessage в консольном приложении

В вашем консольном случае выполнение входит в GetMessage и никогда не выходит из него. Хук получает уведомление и правильно устанавливает recieve_quit. Поскольку выполнение никогда не завершается GetMessage, recieve_quit не проверяется.

Этот ответ касается обработки нажатия клавиш в консольном приложении: https://stackoverflow.com/a/6479673/4240951

В общем - добавьте скрытое окно в консольное приложение или отметьте GetAsyncState нужного ключа.

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

while (recieve_quit == false)
{
    if (GetMessage(&msg, nullptr, 0, 0)) {
        if (msg.message == WM_KEYDOWN && msg.wParam == VK_F8)
            recieve_quit = true;
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}
person nevilad    schedule 14.02.2021
comment
Спасибо за Ваш ответ! Теперь я могу понять, как ведет себя моя программа. Однако что вы подразумеваете под добавлением скрытого окна? Вы имеете в виду добавить к нему графический интерфейс? - person hellohawaii; 14.02.2021
comment
Вы можете создать окно, но сделать его невидимым. Пользователь не увидит никакого графического интерфейса, но у вас будет доступ к очереди сообщений. Не используйте этот сценарий, если вам нужно только проверить нажатую клавишу. - person nevilad; 14.02.2021
comment
Предоставленный вами фрагмент кода не работает для меня, и я думаю, что он может работать только для приложения с графическим интерфейсом пользователя по той же причине, по которой GetMessage никогда не завершается. Я использую ловушку, потому что мое приложение хочет отслеживать глобальный ввод с клавиатуры / мыши, и это лишь часть моих кодов. Поэтому использование GetAsyncState для меня не работает. Что касается: вы можете создать окно, но сделать его невидимым, вы имеете в виду, что мое приложение имеет графический интерфейс и консоль одновременно и скрывает окно? Спасибо за ваше объяснение! - person hellohawaii; 14.02.2021
comment
Предоставленный фрагмент кода полезен, когда у вас есть цикл сообщений - у вас должно быть окно, скрытое или нет. В случае консольного приложения было бы лучше использовать события - обработчик клавиатуры установит событие вместо переменной bool, main создаст поток, который будет ждать этого события. - person nevilad; 14.02.2021