Архив за месяц: Апрель 2021



WinAPI: Message Loop

Добрался до статьи Understanding the Message Loop и решил сделать её небольшой перевод. Читал её, читал, но что-то от меня постоянно ускользает. Если читать в оригинале, то какие-то важные моменты я упускаю. Поскольку я стал изучать статьи с этого сайта с некоторой «коварной» целью :), то мне нужно понять из неё как можно больше. Значит будем делать перевод (не всего подряд конечно), плюс что-то из других источников.

Что такое сообщение (message)?

Сообщение (message) — это некое целочисленное значение. Если взглянуть на заголовочные файлы, можно обнаружить что-то похожее на такую запись:

#define WM_LBUTTONDOWN    0x0201

Сообщения используются для взаимодействия внутри Windows. Щелчок кнопкой мышки, нажатие кнопки на клавиатуре — любое взаимодействие пользователя с программой, всё это использует сообщения. Даже со стороны системы используются сообщения, например при подключении USB или даже при переходе ОС в режим пониженного энергопотребления.
Чтобы не было путаницы с этими сообщениями, в Windows реализована модель передачи сообщений.

Каждое такое сообщение (message) может иметь до двух параметров, wParam и lParam. Не все сообщения используют эти параметры. Например, WM_CLOSE не использует их вовсе, поэтому их нужно игнорировать. А вот WM_COMMAND использует оба параметра.

BOOL AboutDlg (
    HWND hDlg, 
    UINT message, 
    WPARAM wParam, 
    LPARAM lParam)
{
    BOOL bRet = FALSE;
    
    switch (message) 
    {
        case WM_INITDIALOG:
            bRet = TRUE;
            break;

        case WM_COMMAND:
            if (wParam == IDOK ||
                wParam == IDCANCEL) 
            {
                EndDialog(hDlg, TRUE);
                bRet = TRUE;
            }
            break;
    }

    return bRet;
}

Menu:
wParam (high word) = 0
wParam (low word) = Menu identifier (IDM_*)
lParam = 0

Accelerator:
wParam (high word) = 1
wParam (low word) = Accelerator identifier (IDM_*)
lParam = 0

Control:
wParam (high word) = Control-defined notification code
wParam (low word) = Control identifier
lParam = Handle to the control window

Для отправки сообщения могут быть использованы функции PostMessage() или SendMessage().
PostMessage() помещает сообщение в Очередь Сообщений и сразу же возвращает управление. Это означает, что, как только обращение к PostMessage() завершено, обработка сообщения может быть уже закончена, или же может всё ещё обрабатываться.
SendMessage() в отличие от неё отправляет сообщение напрямую окну и ждёт, пока оно не завершит обработку этого сообщения.

В данном случае поведение этих двух функций можно сравнить с UDP и TCP соответственно.

 

Например, если бы мы хотели закрыть окно, то отправили бы окну сообщение WM_CLOSE следующим образом:

PostMessage(hwnd, WM_CLOSE, 0, 0);

Эффект будет такой же, как клик на копке закрытия окна x. Обратите внимание, что здесь параметры wParam и lParam равны нулю.

Диалоги

При использовании диалоговых окон возникает необходимость отсылать сообщения их элементам управления (controls), чтобы взаимодействовать с ними. Это можно сделать так:
1. Используя GetDlgItem(), получить указатель (handle) на элемент управления, зная его ID
2. Отправить ему сообщение с помощью SendMessage()
Либо можно объединить эти действия в одно с помощью функции SendDlgItemMessage(), которая комбинирует эти два шага. В этом случае мы указываем handle окна и ID дочернего элемента и затем получаем указатель (handle) дочернего элемента, которому отсылается сообщение.

Что такое Очередь Сообщений

Рассмотрим такой случай — если идёт обработка отрисовски, и вдруг пользователь набирает на клавиатуре какое-то длинное предложение. Что должно произойти? Будет ли прерван процесс отрисовки, чтобы обработать ввод с клавиатуры или ввод с клавиатуры должен быть проигнорирован? Нет! Вообще-то ни один из вариантов не является приемлемым. Именно поэтому у нас есть Очередь Сообщений. Когда появляется сообщение (например событие нажатия кнопки клавиатуры), оно помещается в очередь сообщений, а когда оно обрабатывается, после этого оно удаляется из очереди.

Что такое Цикл обработки Сообщений

while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&Msg);
    DispatchMessage(&Msg);
}

1. В условии цикла вызывается функция GetMessage(), которая просматривает Очередь сообщений. Если очередь пуста, то программа останавливается и ждёт, когда там что-то появится.
2. Когда происходит какое-то событие (Event), сообщение об этом добавляется в очередь (например, система регистрирует клик кнопкой мышки). В этом случае функция GetMessages() возвращает положительное значение, указывая на новое сообщение, которое нужно обработать. Или возвращается 0, если было отправлено сообщение WM_QUIT — выход из программы. Может произойти и третий вариант, когда возникла какая-то ошибка. Тогда будет возвращено отрицательное значение.
3. Мы получаем сообщение (оно сохраняется в переменной Msg), и затем передаём его функции TranslateMessage(), которая выполняет ряд дополнительных действий, переводя нажатия клавиш в символы. Этот шаг вообще-то опциональный, но без него что-то может работать не так, как нужно.
4. Следующим шагом выполняется DispatchMessage(), эта функция берёт сообщение и проверяет, для какого окна оно предназначено. Выяснив это, она обращается к функции «Процедура Окна» (WindowProc()) данного окна. Затем она передаёт для неё в качестве параметров указатель (handle) на окно, само сообщение и два параметра wParam и lParam.
5. В «Процедуре окна» (WindowProc) происходит проверка сообщения и его параметров. В принципе там и располагается пользовательский код для обработки этих данных. Если пользователь этого не делает, то почти всегда вызывается Дефолтовая Процедура окна, которая обрабатывает действия по умолчанию за вас.
6. Закончив с обработкой сообщения, происходит выход из «Процедуры окна», потом завершается DispatchMessage(), и мы возвращаемся в начало цикла.
Это очень важная концепция для программ Windows. «Процедура Окна» не вызывается волшебным образом сама по себе, а неявным образом её вызывает DispatchMessage(). Если бы вы хотели, можно было бы воспользоваться GetWindowLong(), вызвав её с указателем на окно.

while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
    WNDPROC fWndProc = (WNDPROC)GetWindowLong(Msg.hwnd, GWL_WNDPROC);
    fWndProc(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
}
Однако этот метод подходит только для самых простых случаев, т.к. в сложном случае можно легко получить кучу ошибок!

 

Вы обратили внимание, что мы воспользовались GetWindowLong() для доступа к процедуре данного окна? Почему же нельзя было использовать WndProc() напрямую? Всё это потому, что Цикл обработки Сообщений отвечает за ВСЕ окна нашей программы. Как это не странно, но элементы типа кнопок, списков и т.п. — у всех них есть свои собственные Процедуры окна. Поэтому нам нужно быть уверенными, что мы обращаемся к Процедуре нужного нам окна! Поскольку не только главное окно использует эту же Процедуру Окна, нам нужен первый параметр (указатель на окно), который говорит, для какого из окон было данное сообщение.

Как видим, ваше приложение тратит большую часть своего времени просто прокручивая сообщения в Цикле обработки сообщений где вы рассылаете сообщения окнам, которые обрабатывают их. Но что вы делаете, если нужно завершение программы? Т.к. мы используем цикл while(), то из него можно выйти, если в результате условия вернётся FALSE (то же что и 0). Выйдя из цикла, мы завершим нашу главную функцию: WinMain(). Это как раз то, что делает функция PostQuitMessage(). Она помещает сообщение WM_QUIT в очередь. И, вместо того, чтобы вернуть положительное значение, GetMessage() заполняет нулями структуру Msg и возвращает 0. С этого момента wParam в переменной Msg содержит значение, которое вы передали PostQuitMessage() и его можно игнорировать, либо вернуть его из WinMain(), что будет использовано как код выхода при завершении процесса.

Важное замечание

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

Хотя функция GetMessage() по определению возвращает BOOL, тем не менее она может возвращать значения отличные от TRUE или FALSE!

 

Может создаться впечатление, что ошибки нет и всё работает:

  1.     while(GetMessage(&Msg, NULL, 0, 0))
  2.     while(GetMessage(&Msg, NULL, 0, 0) != 0)
  3.     while(GetMessage(&Msg, NULL, 0, 0) == TRUE)

Но это не так! Все три примера выше с ошибкой. Данные примеры работают до тех пор, пока GetMessage() не вернёт ошибку. Но, если что-то пойдёт не так, данную ошибку очень трудно потом будет отследить. Поэтому рекомендуется делать проверку следующим образом:

    while(GetMessage(&Msg, NULL, 0, 0) > 0)