Сообщений 2    Оценка 78        Оценить  
Система Orphus

Как добавить горячие клавиши в модальный диалог?

Автор: Александр Шаргин
Опубликовано: 21.06.2001
Исправлено: 13.03.2005
Версия текста: 1.1

Демонстрационный проект DlgAccel
Демонстрационный проект MfcDlgAccel

В обычном приложении добавить горячие клавиши (акселераторы) достаточно просто. Для этого нужно проделать три действия:

Но когда горячие требуется добавить в модальный диалог, возникает проблема. Дело в том, что цикл сообщений в этом случае "запрятан" в функции DialogBox, и вставить в него вызов TranslateAccelerator не удаётся. Посмотрим, как можно обойти эту проблему.

Win32 API

Когда дело доходит до вмешательства во внутреннюю работу функций Win32 API, спасти положение помогут хуки. В данном случае можно использовать хук типа WH_GETMESSAGE. Связанная с ним функция-фильтр вызывается всякий раз, когда очередное сообщение извлекается из очереди посредством GetMessage или PeekMessage. Перехваченное таким образом сообщение можно передать функции TranslateAccelerator, как и обычно. Если преобразование акселератора прошло успешно (TranslateAccelerator вернула TRUE), можно подменить код сообщения на NULL, чтобы его больше никто не обрабатывал.

Установить хук можно в обработчике WM_INITDIALOG. Это делается с помощью функции SetWindowHookEx. Когда диалог будет закрываться, хук следует снять с помощью UnhookWindowsHookEx. Например:

HWND hDlg          = NULL;
HACCEL hAccelTable = NULL;
HHOOK hHook        = NULL;

...

INT_PTR CALLBACK DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg)
    {
    case WM_INITDIALOG:
        // Устанавливаем локальный хук WH_GETMESSAGE на текущий поток.
        hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, NULL, GetCurrentThreadId());

        // Сохраняем хэндл диалога, чтобы функция хука могла на него ссылаться.
        ::hDlg = hDlg;
        return TRUE;

    case WM_COMMAND:
        if(LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
            // Снимаем хук.
            UnhookWindowsHookEx(hHook);

            // Закрываем диалог.
            EndDialog(hDlg, 0);
        }

        // Обрабатываем другие команды...
        ...


        return TRUE;
    }

    return FALSE;
}

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

LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
    // Указатель на сообщение "лежит" в lParam.
    MSG *pMsg = (MSG *)lParam;

    if(pMsg->message >= WM_KEYFIRST && pMsg->message <= WM_KEYLAST)
    {
        // Преобразуем нажатия клавиш в WM_COMMAND.
        if(TranslateAccelerator(hDlg, hAccelTable, pMsg))
        {
            // Если сообщение преобразовано, уничтожаем его, устанавливая код в WM_NULL.
            // Если этого не сделать, сообщение попадёт в диалог, и он недовольно "запищит".
            pMsg->message = WM_NULL;
        }
    }

    // Вызываем следующий фильтр.
    return CallNextHookEx(hHook, code, wParam, lParam);
}

Обратите внимание на вызов CallNextHookEx. Использование этой функции является хорошим тоном, так как позволит и другим функциям-фильтрам (если они есть) обработать сообщение.

Теперь можно создавать диалог, как обычно. Предварительно загружается таблица акселераторов. После того, как функция DialogBox вернёт управление, её можно будет удалить.

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
    // Загружаем таблицу акселераторов.
    hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR));

     // Создаём диалог.
    DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG), NULL, (DLGPROC)DialogProc);

    // Уничтожаем таблицу акселераторов.
    DestroyAcceleratorTable(hAccelTable);

    return 0;
}

MFC

В MFC решение задачи неожиданно упрощается. Дело в том, что MFC никогда не вызывает DialogBox, а значит не использует модальные диалоги. Вместо этого функция CWnd::DoModal создаёт немодальный диалог, а затем имитирует поведение модального, отключая родительское окно и создавая новый цикл сообщений. В этот цикл сообщений можно легко внедриться, переопределяя в классе диалога функцию PreTranslateMessage.

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

// Implementation
protected:
    HACCEL m_hAccel;

Затем таблица загружается в обработчике OnInitDialog.

BOOL CMfcDlgAccelDlg::OnInitDialog()
{
   ...

    // Загружаем таблицу акселераторов.
    m_hAccel = LoadAccelerators(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDR_ACCELERATOR));

   ...    
}

В функции PreTranslateMessage мы передаём текущее сообщение функции TranslateAccelerator. Если преобразование прошло успешно, эта функция возвращает TRUE. Тогда мы тоже возвращаем TRUE, отменяя дальнейшую обработку сообщения, и цикл сообщений переходит к следующему.

BOOL CMfcDlgAccelDlg::PreTranslateMessage(MSG* pMsg) 
{
    ...

    if(TranslateAccelerator(m_hWnd, m_hAccel, pMsg))
        return TRUE;

    ...
}

Вот и всё. Осталось удалить таблицу акселераторов в обработчике OnDestroy.

void CMfcDlgAccelDlg::OnDestroy() 
{
    ...
    
    DestroyAcceleratorTable(m_hAccel);

    ...
}

В заключение замечу, что вы можете последовать примеру разработчиков MFC в собственных программах на "чистом" API. Если хуки вам не нравятся, вы всегда можете заменить модальный диалог немодальным. Вы даже можете написать собственные версии функций DialogBox и EndDialog и использовать вместо стандартных. Но это уже совсем другая история...


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