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

Элемент управления WinHotkeyCtrl

Поддержка мультимедийных «горячих клавиш»

Автор: Александр Авдонин
Опубликовано: 05.08.2004
Исправлено: 10.12.2016
Версия текста: 1.1

Введение
Win32 API
Перехват клавиатурного ввода
Контекстное меню
Использование в приложениях
MFC класс CWinHotkeyCtrl

Исходные тексты элемента управления (winapi)
Демонстрационный проект (winapi)
Исходные тексты (MFC)
Демонстрационный проект (MFC)

Введение

WinHotkeyCtrl – элемент управления, предназначенный для задания и управления «горячими клавишами» (hotkey`s). В отличие от стандартного элемента управления Windows (HotKeyCtrl), WinHotkeyCtrl обладает рядом преимуществ:


WinHotkeyCtrl строится на базе стандартного элемента управления EditCtrl методом сабклассирования (subclassing), что обеспечивает удобство и легкость его использования с шаблонами окон диалогов.

С помощью директив препроцессора в одном исходном файле реализованы 2 версии WinHotkeyCtrl: для Windows 98/NT и для Windows 2000 (и выше).

Win32 API

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

WNDPROC _wpEditProc = NULL;


BOOL InitWinHotkeyCtrls() {

    WNDCLASSEX wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    if (!GetClassInfoEx(GetModuleHandle(NULL), _T("Edit"), &wcex))
        return(FALSE);
    _wpEditProc = wcex.lpfnWndProc;

    return(_wpEditProc != NULL);
}


BOOL SubClassWinHotkeyCtrl(HWND hwndWhc) {

    _ASSERT(hwndWhc);

    if (!_wpEditProc) // инициализация всех WinHokeyCtrlif (!InitWinHotkeyCtrls())
            return(FALSE);

    SetWindowLongPtr(hwndWhc, GWLP_WNDPROC, (LONG_PTR)(WNDPROC)_WinHotkeyCtrlProc);
    SetWinHotkey(hwndWhc, MAKEWHCDATA(0, 0, 0, 0)); // см. ниже
return(TRUE);
}


LRESULT CALLBACK _WinHotkeyCtrlProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

    switch (uMsg) {
        // переопределяем или дополняем обработчики сообщений
    }
    return(CallWindowProc(_wpEditProc, hwnd, uMsg, wParam, lParam));
}

Перехват клавиатурного ввода

Для перехвата ввода с клавиатуры используются хуки (hook`s). В случае с Windows 98/NT – локальный WH_KEYBOARD, что несколько ограничивает функциональность WinHotkeyCtrl. А в Windows 2000 (и выше) используется RAW INPUT глобальный системный хук WH_KEYBOARD_LL.

ПРИМЕЧАНИЕ

Как написано в документации PlatformSDK, большинство глобальных системных хуков должны обязательно находиться в динамически подключаемой библиотеке DLL. При этом DLL подгружается в адресное пространство процесса производящего какое-либо «отлавливаемое» действие (например, посылку сообщений окну в случае WH_GETMESSAGE).

Всё вышесказанное не относится к так называемым RAW INPUT хукам (WH_KEYBOARD_LL и WH_MOUSE_LL), появившимся в Windows NT 4.0 SP3. Их фильтрующая функция вызывается в том же потоке, который установил хук, методом посылки сообщения этому потоку. Таким образом, фильтрующая функция RAW INPUT хука может находиться и в EXE, а SetWindowsHookEx должна вызываться из GUI потока, имеющего очередь сообщений (окно).

Хук устанавливается при получении одним из элементов управления WinHotkeyCtrl фокуса ввода (сообщение WM_SETFOCUS), а снимается, соответственно, при потере этим элементом фокуса ввода (WM_KILLFOCUS) или его разрушении (WM_DESTROY). Задача хука – отлавливать все сообщения от клавиатуры (WM_KEYDOWN, WM_SYSKEYDOWN, WM_KEYUP и WM_SYSKEYUP) и направлять сообщения о них активному элементу управления.

HWND _hwndWhc = NULL; // описатель окна активного элемента управления
HHOOK _hhookKb = NULL; // описатель хука

#if _WIN32_WINNT < 0x500

LRESULT CALLBACK _KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {

    if (nCode == HC_ACTION) {
        PostMessage(_hwndWhc, WM_KEY, wParam, (lParam & 0x80000000));
    }
    return(1); // запрещаем дальнейшую обработку сообщения
}

#else// _WIN32_WINNT >= 0x500

LRESULT CALLBACK _LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {

    if (nCode == HC_ACTION && (wParam == WM_KEYDOWN || 
        wParam == WM_SYSKEYDOWN || wParam == WM_KEYUP || wParam == WM_SYSKEYUP)) {

        PostMessage(_hwndWhc, WM_KEY, 
            ((PKBDLLHOOKSTRUCT)lParam)->vkCode, (wParam & 1));
    }
    return(1); // запрещаем дальнейшую обработку сообщения
}

#endif// _WIN32_WINNT >= 0x500


BOOL _UninstallKbHook() {

    BOOL fOk = FALSE;
    if (_hhookKb) {
        fOk = UnhookWindowsHookEx(_hhookKb);
        _hhookKb = NULL;
    }
    _hwndWhc = NULL;
    return(fOk);
}


BOOL _InstallKbHook(HWND hwndHkc) {

    if (_hhookKb)
        _UninstallKbHook();

    _hwndWhc = hwndHkc;

#if _WIN32_WINNT < 0x500
    _hhookKb = SetWindowsHookEx(WH_KEYBOARD, 
        (HOOKPROC)_KeyboardProc, NULL, GetCurrentThreadId());
#else// _WIN32_WINNT >= 0x500
    _hhookKb = SetWindowsHookEx(WH_KEYBOARD_LL, 
        (HOOKPROC)_LowLevelKeyboardProc, GetModuleHandle(NULL), NULL);
#endif// _WIN32_WINNT >= 0x500return(_hhookKb != NULL);
}

WinHotkeyCtrl, получив сообщение о клавиатурном вводе (WM_KEY = WM_USER + XXX), соответсвенно изменяет своё состояние. При этом wParam содержит виртуальный код нажатой (отпущенной) клавиши, а lParam – булево значение её текущего состояния (TRUE – если клавиша отпущена).

ПРЕДУПРЕЖДЕНИЕ

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

Собственно комбинация «горячих клавиш» определяется виртуальным кодом одной из алфавитно-цифровых клавиш, клавиш перемещения курсора и любых других, за исключением 4 клавиш-модификаторов (Win, Ctrl, Alt и Shift).

Так как WinHotkeyCtrl в данной реализации предполагает перехват абсолютно всех комбинаций «горячих клавиш», для хранения информации о текущем состоянии элемента управления вполне достаточно всего 4 байт (DWORD):

        #define MAKEWHCDATA(vkCode, fModSet, fModRel, fIsPressed) \
    ((DWORD)(((BYTE)((DWORD_PTR)(vkCode) & 0xff)) | \
    (((DWORD)((BYTE)((DWORD_PTR)(fModSet) & 0xff))) << 8) | \
    (((DWORD)((BYTE)((DWORD_PTR)(fModRel) & 0xff))) << 16) | \
    (((DWORD)((BYTE)((DWORD_PTR)(fIsPressed) & 0xff))) << 24)))

Где vkCode – виртуальный код клавиши, fModSet – флаги нажатых в данный момент клавиш-модификаторов (Win, Ctrl, Alt и Shift), fModRel – флаги отпущенных модификаторов, fIsPressed – булево значение, определяющее нажата ли в данный момент клавиша.

Таким образом, всю информацию о текущем состоянии WinHotkeyCtrl можно записать в блок пользовательских данных окна элемента управления GWLP_USERDATA (см. функцию SetWindowLongPtr в документации PlatformSDK). В противном случае пришлось бы выделять блок в куче процесса под структуру (new, malloc, HeapAlloc) и сохранять уже указатель на него. Желающие реализовать WinHotkeyCtrl с запрещенными комбинациями могут так и поступить.

ПРИМЕЧАНИЕ

Дополнительную информацию об элементе управления можно и в свойствах окна (см. функции SetProp, GetProp и RemoveProp в документации PlatformSDK).

Алгоритм обработки сообщения от хука (WM_KEY) довольно прост, хотя и имеет ряд нюансов:

        case WM_KEY: {

    DWORD dwWhcData = (DWORD)GetWindowLongPtr(hwnd, GWLP_USERDATA);

    DWORD vkCode    = LOBYTE(LOWORD(dwWhcData));
    DWORD fModSet   = HIBYTE(LOWORD(dwWhcData));
    DWORD fModRel   = LOBYTE(HIWORD(dwWhcData));
    BOOL fIsPressed = HIBYTE(HIWORD(dwWhcData));

    DWORD fMod = 0;
    BOOL fRedraw = TRUE;

    switch (wParam) {
        case VK_LWIN:
        case VK_RWIN:        fMod = MOD_WIN; break;
        case VK_CONTROL:
        case VK_LCONTROL:
        case VK_RCONTROL:    fMod = MOD_CONTROL; break;
        case VK_MENU:
        case VK_LMENU: 
        case VK_RMENU:       fMod = MOD_ALT; break;
        case VK_SHIFT:
        case VK_LSHIFT:
        case VK_RSHIFT:      fMod = MOD_SHIFT; break;
    }

    if (fMod) { // клавиша-модификаторif (!lParam) { // нажатаif(!fIsPressed && vkCode) {
                fModSet = fModRel = 0;
                vkCode = 0;
            } 
            fModRel &= ~fMod;
        } elseif (fModSet & fMod) // отпущена
            fModRel |= fMod;

        if (fIsPressed || !vkCode) {
            if (!lParam) {
                if (!(fModSet & fMod)) { // новый модификатор
                    fModSet |= fMod;
                } else
                    fRedraw = FALSE;
            } else fModSet &= ~fMod;
        }
    } else { // обычная клавишаif (wParam == VK_DELETE && fModSet == (MOD_CONTROL | MOD_ALT)) {
            fModSet = fModRel = 0; // пропустить Ctrl+Alt+Del
            vkCode = 0;
            fIsPressed = FALSE;
        } elseif (wParam == vkCode && lParam) {
            fIsPressed = FALSE;
            fRedraw = FALSE;
        } else {
            if (!fIsPressed && !lParam) { // была нажата сначала одна, а теперь - другаяif (fModRel & fModSet) {
                    fModSet = fModRel = 0;
                }
                vkCode = (DWORD)wParam;
                fIsPressed = TRUE;
            }
        }
    }
    dwWhcData = MAKEWHCDATA(vkCode, fModSet, fModRel, fIsPressed);
    SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG)dwWhcData);

    if (fRedraw) // чтобы избежать мерцания
        _SetWhcText(hwnd, dwWhcData);

    return(0);
             }

Контекстное меню

Стандартное меню EditCtrl`а (с командами «Вырезать», «Вставить» и т. д.) нужно либо вообще убрать, либо заменить своим. В обоих случаях необходимо переопределить обработчик сообщения элемента управления WM_CONTEXTMENU, например, так:

        case WM_CONTEXTMENU: {

    UINT id;
    HMENU hmenu, hmenu2;
    hmenu = CreatePopupMenu();

#if _WIN32_WINNT >= 0x500
    hmenu2 = CreatePopupMenu();
    for (id = VK_BROWSER_BACK; id <= VK_LAUNCH_APP2; id++)
        AppendMenu(hmenu2, MF_STRING, id, GetKeyName(id));
    AppendMenu(hmenu, MF_STRING | MF_POPUP, (UINT_PTR)hmenu2, _T("Multimedia"));
#endif// _WIN32_WINNT >= 0x500

    hmenu2 = CreatePopupMenu();
    AppendMenu(hmenu2, MF_STRING, VK_RETURN, GetKeyName(VK_RETURN));
    AppendMenu(hmenu2, MF_STRING, VK_ESCAPE, GetKeyName(VK_ESCAPE));
    AppendMenu(hmenu2, MF_STRING, VK_TAB, GetKeyName(VK_TAB));
    AppendMenu(hmenu2, MF_STRING, VK_CAPITAL, GetKeyName(VK_CAPITAL));
    AppendMenu(hmenu2, MF_STRING, VK_BACK, GetKeyName(VK_BACK));
    AppendMenu(hmenu2, MF_STRING, VK_INSERT, GetKeyName(VK_INSERT));
    AppendMenu(hmenu2, MF_STRING, VK_DELETE, GetKeyName(VK_DELETE));
    for (id = VK_SPACE; id <= VK_DOWN; id++)
        AppendMenu(hmenu2, MF_STRING, id, GetKeyName(id));
    AppendMenu(hmenu, MF_STRING | MF_POPUP, (UINT_PTR)hmenu2, _T("Standard"));

    hmenu2 = CreatePopupMenu();
    for (id = VK_F1; id <= VK_F24; id++)
        AppendMenu(hmenu2, MF_STRING, id, GetKeyName(id));
    AppendMenu(hmenu, MF_STRING | MF_POPUP, (UINT_PTR)hmenu2, _T("Functionality"));


    DWORD dwWhcData = (DWORD)GetWindowLongPtr(hwnd, GWLP_USERDATA);
    DWORD vkCode = LOBYTE(LOWORD(dwWhcData));
    DWORD fModSet = HIBYTE(LOWORD(dwWhcData));
    DWORD fModRel = LOBYTE(HIWORD(dwWhcData));
    BOOL fIsPressed = HIBYTE(HIWORD(dwWhcData));

    AppendMenu(hmenu, MF_SEPARATOR, 0, NULL);
    AppendMenu(hmenu, (fModSet & MOD_WIN) ? 
        (MF_STRING | MF_CHECKED) : MF_STRING, VK_LWIN, _T("Win-key"));
    AppendMenu(hmenu, (fModSet & MOD_CONTROL) ? 
        (MF_STRING | MF_CHECKED) : MF_STRING, VK_CONTROL, _T("Control-key"));
    AppendMenu(hmenu, (fModSet & MOD_SHIFT) ? 
        (MF_STRING | MF_CHECKED) : MF_STRING, VK_SHIFT, _T("Shift-key"));
    AppendMenu(hmenu, (fModSet & MOD_ALT) ? 
        (MF_STRING | MF_CHECKED) : MF_STRING, VK_MENU, _T("Alt-key"));
    
    UINT uMenuID = TrackPopupMenu(hmenu, 
        TPM_RIGHTALIGN | TPM_RIGHTBUTTON | TPM_NONOTIFY | TPM_RETURNCMD,
        LOWORD(lParam), HIWORD(lParam), 0, hwnd, NULL);

    if (uMenuID && uMenuID < 256) {
        switch (uMenuID) {
            case VK_LWIN:
                if (vkCode) {
                    fModSet ^= MOD_WIN;
                    fModRel |= fModSet & MOD_WIN;
                }
                break;
            case VK_CONTROL:
                if (vkCode) {
                    fModSet ^= MOD_CONTROL;
                    fModRel |= fModSet & MOD_CONTROL;
                }
                break;
            case VK_SHIFT:
                if (vkCode) {
                    fModSet ^= MOD_SHIFT;
                    fModRel |= fModSet & MOD_SHIFT;
                }
                break;
            case VK_MENU:
                if (vkCode) {
                    fModSet ^= MOD_ALT;
                    fModRel |= fModSet & MOD_ALT;
                }
                break;

            default:
                vkCode = uMenuID;
                fIsPressed = FALSE;
                break;
        }
        dwWhcData = MAKEWHCDATA(vkCode, fModSet, fModRel, fIsPressed);
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG)dwWhcData);
        _SetWhcText(hwnd, dwWhcData);
        SetFocus(hwnd);
    }
    DestroyMenu(hmenu);

    return(0);
                     }

Использование в приложениях

Чтобы отслеживать изменение состояния WinHotkeyCtrl достаточно в обработчик EN_CHANGE сообщения WM_COMMAND родительского окна (диалога) вставить проверку:

        void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {

    if (codeNotify == EN_CHANGE) {
        if (id == IDC_WINHOTKEY) {
            BOOL fEnable = (BOOL)(LOBYTE(LOWORD(GetWinHotkey(hwndCtl))) != 0);
            EnableWindow(GetDlgItem(hwnd, IDC_BUTTON), fEnable);
        }
    } elseif (codeNotify == BN_CLICKED) {
        switch (id) {
            case IDC_BUTTON:
                // ...break;
        }
    }
}

Благодаря совместимому формату «горячую клавишу» легко зарегистровать:

DWORD hk = GetWinHotkey(hwndWhc);
UINT fModifiers = HIBYTE(LOWORD(hk)),
    vkCode = LOBYTE(LOWORD(hk));

if (vkCode)
    RegisterHotKey(hwnd, IDH_HOTKEY, fModifiers, vkCode);

MFC класс CWinHotkeyCtrl

При использовании библиотеки MFC суть реализации остается прежней, меняется только форма в соответсвии с принципами объектно-ориентированного программирования и самой библиотеки MFC.

      class CWinHotkeyCtrl : public CEdit
{
    DECLARE_DYNAMIC(CWinHotkeyCtrl)

public:
    CWinHotkeyCtrl();
    virtual ~CWinHotkeyCtrl();

    void UpdateText();
    DWORD GetWinHotkey();
    BOOL GetWinHotkey(UINT* pvkCode, UINT* pfModifiers);
    void SetWinHotkey(DWORD dwHk);
    void SetWinHotkey(UINT vkCode, UINT fModifiers);

private:
    static HHOOK sm_hhookKb;
    static CWinHotkeyCtrl* sm_pwhcFocus;

    UINT m_vkCode;
    DWORD m_fModSet;
    DWORD m_fModRel;
    BOOL m_fIsPressed;

private:
    BOOL InstallKbHook();
    BOOL UninstallKbHook();
    
#if _WIN32_WINNT < 0x500
    static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
#else// _WIN32_WINNT >= 0x500static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
#endif// _WIN32_WINNT >= 0x500

    afx_msg LRESULT OnKey(WPARAM wParam, LPARAM lParam);

protected:
    DECLARE_MESSAGE_MAP()
public:
    afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
    afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
    afx_msg void OnSetFocus(CWnd* pOldWnd);
    afx_msg void OnKillFocus(CWnd* pNewWnd);
    afx_msg void OnContextMenu(CWnd* /*pWnd*/, CPoint /*point*/);
    afx_msg void OnDestroy();
protected:
    virtualvoid PreSubclassWindow();
};

При этом единственное кардинальное отличие MFC-версии WinHotkeyCtrl состоит в том, что вместо описателя окна элемента управления в статической переменной сохраняется указатель на его класс.

ПРИМЕЧАНИЕ

Все проекты демонстрационных версий собраны в Microsoft Visual C++ 7.1.


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