Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

Intercepting dialog focus events

В рамках моего мини-проекта BITS Manager решил сделать удобный и красивый диалог. Основная изюминка - заголовки элементов управления, которые меняют свой внешний вид в зависимости от движения фокуса. Если конкретнее - меняется шрифт у заголовка активного элемента:
MFC dialog example

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

Я увидел следующие решения данной проблемы:

  1. В глобальном фильтре сообщений приложения ловить WM_SETFOCUS и WM_KILLFOCUS.

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

  2. Использовать перехват сообщений - Win32 Hooks.

    Ловушка WH_CBT с кодом HCBT_SETFOCUS позволяет отловить момент, когда система собирается установить фокус на окно. Это хорошо, но пришлось бы сохранять и обновлять идентификатор окна, которое будет терять фокус. Можно использовать и другие ловушки для перехвата сообщений WM_SETFOCUS и WM_KILLFOCUS.

  3. Воспользоваться тем, что все известные Win32 элементы управления шлют родителю уведомления о некоторых событиях, среди которых ***_SETFOCUS и ***_KILLFOCUS.

Я остановился на 3-м варианте, первые два мне показались черезчур сложными для такой задачи.

How to intercept dialog focus events in one place

Пришлось слегка почитать документацию и покопаться в коде MFC. Итак, есть два типа элементов управления. Первый тип использует сообщение WM_NOTIFY для уведомления родителя, второй тип использует WM_COMMAND. Оказалось, MFC в любом случае вызывает виртуальный метод BOOL OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO *pHandlerInfo). После медитаций у меня получился такой код:

BOOL CJobProfileDlg::OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO *pHandlerInfo) {
    const MSG &lastSentMsg = AfxGetThreadState()->m_lastSentMsg;

    LPCTSTR szEditClass = _T("EDIT");
    LPCTSTR szComboBoxClass = _T("COMBOBOX");
    LPCTSTR szListBoxClass = _T("LISTBOX");
    if (
        ((nCode == EN_KILLFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szEditClass))
        || ((nCode == CBN_KILLFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szComboBoxClass))
        || ((nCode == LBN_KILLFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szListBoxClass))
        || ((HIWORD(nCode) == WM_NOTIFY)) && (LOWORD(nCode) == LOWORD(NM_KILLFOCUS))) {
        OnControlLostFocus(nID);
    }
    if (((nCode == EN_SETFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szEditClass))
        || ((nCode == CBN_SETFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szComboBoxClass))
        || ((nCode == LBN_SETFOCUS) && Utility::CompareWindowClass(reinterpret_cast<HWND>(lastSentMsg.lParam), szListBoxClass))
        || ((HIWORD(nCode) == WM_NOTIFY) && (LOWORD(nCode) == LOWORD(NM_SETFOCUS)))) {
        OnControlGotFocus(nID);
    }

    return(__super::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo));
}

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

  1. #define CBN_KILLFOCUS       4
    #define LBN_SETFOCUS        4
    

    В итоге пришлось добавить еще код проверки класса окна.

  2. #define NM_FIRST                (0U-  0U)       // generic to all controls
    #define NM_SETFOCUS             (NM_FIRST-7)
    #define NM_KILLFOCUS            (NM_FIRST-8)
    

    MFC пакует два значения в одно с помощью MAKELONG, в результате чего отрицательные значения NM_*** после извлечения теряют старшее слово. Нужно при сравнении обрезать оба операнда.

Приведу весь код для потомков:

bool Utility::CompareWindowClass(HWND hwnd, LPCTSTR pszWindowClass) {
    const int cch = _tcslen(pszWindowClass) + 1;
    LPTSTR pszClassName = reinterpret_cast<LPTSTR>(_alloca(sizeof(TCHAR) * cch));
    if (GetClassName(hwnd, pszClassName, cch)) {
        if (_tcsncicmp(pszClassName, pszWindowClass, -1) == 0)
            return(true);
    }
    return(false);
}
void CJobProfileDlg::OnControlGotFocus(UINT nID) {
    CWnd *pStatic = GetDlgItem(nID)->GetNextWindow(GW_HWNDPREV);
    if (pStatic && Utility::CompareWindowClass(pStatic->m_hWnd, _T("STATIC")))
        pStatic->SetFont(&m_fntStaticActive);
}

void CJobProfileDlg::OnControlLostFocus(UINT nID) {
    CWnd *pStatic = GetDlgItem(nID)->GetNextWindow(GW_HWNDPREV);
    if (pStatic && Utility::CompareWindowClass(pStatic->m_hWnd, _T("STATIC")))
        pStatic->SetFont(&m_fntStatic);
}

Предполагается, что порядок обхода задан таким образом, что заголовок элемента управления предшествует ему.

0 коммент.:

Отправить комментарий

Copyright 2007-2011 Chabster