Исходные тексты элемента управления (winapi)
Демонстрационный проект (winapi)
Исходные тексты (MFC)
Демонстрационный проект (MFC)
WinHotkeyCtrl – элемент управления, предназначенный для задания и управления «горячими клавишами» (hotkey`s). В отличие от стандартного элемента управления Windows (HotKeyCtrl), WinHotkeyCtrl обладает рядом преимуществ:
WinHotkeyCtrl строится на базе стандартного элемента управления EditCtrl методом сабклассирования (subclassing), что обеспечивает удобство и легкость его использования с шаблонами окон диалогов.
С помощью директив препроцессора в одном исходном файле реализованы 2 версии WinHotkeyCtrl: для Windows 98/NT и для Windows 2000 (и выше).
Для начала необходимо сабклассировать окно элемента управления 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 суть реализации остается прежней, меняется только форма в соответсвии с принципами объектно-ориентированного программирования и самой библиотеки 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. |