Преамбула
Так получилось, что я с давних пор пользуюсь мышками Logitech — MX300 и MX310. У них над колёсиком есть дополнительная кнопка, на которую можно повесить различные функции. В старых драйверах (MouseWare) в числе этих функций была «Recall Application», по которой происходило переключение на предыдущее активное окно — примерно то же, что происходит, если однократно нажать Alt+Tab. Мне эта возможность сразу же пришлась по душе: нередко возникает ситуация, когда нужно переключиться в какое-нибудь окно, что-то там сделать (например, скопировать строку) и вернуться назад (соответственно, чтобы вставить эту скопированную строку). Alt+Tab в данном случае оказывается менее удобен (т.к. левую руку надо снимать с сочетания Ctrl+C, а потом возвращать в прежнее положение для нажатия Ctrl+V).
Но вот я поставил себе Windows XP x64, и оказалось, что MouseWare для 64-битных систем недоступен. Для MX310 обнаружилась более современная утилита SetPoint, но функции «Recall Application» в ней больше нет. К счастью, удалось настроить на нужную кнопку отправку сочетания клавиш Alt+Tab, однако мигание окошка со списком задач в момент переключения немного раздражало. Так что, преодолев лень, я сподобился написать небольшую утилитку, которая помогла устранить этот недостаток.
Амбула
Фактически от утилиты требовалось только одно: висеть в фоне и, получив сигнал о нажатии кнопки, выполнить переключение в «соседнее» окно. Задачка оказалась не настолько тривиальной, как я предполагал поначалу. Сигнал о нажатии кнопки напрямую получить не удаётся, поскольку в списке доступных действий в SetPoint нет такого действия «послать сигнал о нажатии шестой кнопки». Так что пришлось немножко схитрить: по нажатию кнопки эмулировать какое-нибудь сочетание клавиш, а программой отлавливать уже это сочетание. Естественно, выбрать нужно что-нибудь неиспользуемое в обычной работе; я выбрал Ctrl+Alt+Shift+Z.
Вторая трудность заключалась в выборе нужного окна. Передвигаться по Z-стеку приложений можно с помощью вызова GetWindow(hwnd, GW_HWNDNEXT), но среди этих окон попадается большое количество тех, на которые переключаться не нужно. Например, невидимые. Даже если оставить только видимые окна, остаётся множество других окон верхнего уровня, которые отсутствуют в обычном списке Alt+Tab. Здесь я не смог найти удовлетворительного решения. Один вариант удалось нагуглить на Stack overflow, но правильного перечисления окон я с ним не добился. Также есть исходники TaskSwitchXP, однако попытка адаптации кода под мои нужды не удалась (в список попадали лишние окна). Полностью в коде я разобраться не смог, так что либо я сделал что-то не так, либо код изначально не рассчитан на такое нецелевое применение. (Впрочем, я с ним ещё продолжу разбираться.) В конце концов я провёл собственное исследование и определил эмпирические условия для выбора «правильных» окон (эти условия я перечислю в конце). Результирующий код программы уместился на одной страничке:
int WinMainCRTStartup(void)
{
if (!RegisterHotKey(NULL, 0, MOD_CONTROL | MOD_SHIFT | MOD_ALT, 'Z'))
return 1;
MSG msg = {0};
while (GetMessage(&msg, NULL, 0, 0))
{
if (msg.message == WM_HOTKEY)
{
HWND current_wnd = GetForegroundWindow();
if (current_wnd == NULL)
continue;
// Find top-level owner of the current window
HWND owner = current_wnd;
do
{
current_wnd = owner;
owner = GetWindow(current_wnd, GW_OWNER);
} while ((owner != NULL) && IsWindowVisible(owner));
// Find next window in Z-stack to switch to
do {
current_wnd = GetWindow(current_wnd, GW_HWNDNEXT);
if (current_wnd == NULL)
break;
owner = GetWindow(current_wnd, GW_OWNER);
} while (!IsWindowVisible(current_wnd) ||
((GetWindowLongPtr(current_wnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) != 0) ||
((owner != NULL) && IsWindowVisible(owner)));
if (current_wnd != NULL)
SetForegroundWindow(current_wnd);
}
}
UnregisterHotKey(NULL, 0);
return 0;
}
Небольшие технические комментарии
- Функция называется WinMainCRTStartup, потому что здесь не задействован CRT, так что я его отключил. В результате скомпилированная программа занимает 3072 байта. Подробнее об этом можно почитать на RSDN.
- Эмпирические условия выбора «правильного» окна выглядят следующим образом. Окно должно:
- быть видимым;
- не иметь расширенного стиля WS_EX_TOOLWINDOW;
- окно-владелец для данного окна должно отсутствовать или быть невидимым.
- Первый цикл (поиск «корневого» окна-владельца) нужен для того, чтобы справиться с ситуацией, когда текущим окном является «неправильное» окно. Например, в программе ABBYY Lingvo все окна, включая окна-карточки, являются «неправильными»: это окна верхнего уровня, и владельцем каждого из них является некое фиктивное окно, имеющее флаг видимости, но с нулевыми размерами. Если текущим окном является такое окно-карточка, то цикл GetWindow(current_wnd, GW_HWNDNEXT) первым делом попадает в это самое фиктивное окошко Lingvo и, поскольку оно формально является «правильным», активизирует его. Т.е. переключения в другое приложение не происходит. Можно было бы вместо цикла использовать GetAncestor(current_wnd, GA_ROOTOWNER), но в этом случае не учитывается видимость окон и результат получается неправильный.
- К сожалению, данный эвристический алгоритм неидеален. В частности, с окнами Excel 2003 он работает неправильно (легко зацикливается на одном окне). Правда, частично это вина и самого Excel, который организует окна каким-то совершенно невразумительным образом, так что даже стандартный Alt+Tab может зациклиться на одном окне и потребуется двукратное нажатие Tab, чтобы проскочить этот цикл. С этой загадкой я ещё планирую поразбираться. Также нужно быть осторожным с виртуальными машинами, поскольку в случае перехвата клавиатурного ввода сочетание клавиш пойдёт в виртуалку, а не в хостовую систему.
- В текущем варианте корректного выхода из программы не предусмотрено (поэтому, вообще говоря, вызов UnregisterHotKey в конце программы лишний, но я его оставил для красоты :-) ). Если требуется периодически завершать выполнение программы, а убивать процесс не хочется, можно добавить регистрацию ещё одного глобального сочетания клавиш, а в цикл обработки сообщений воткнуть проверку, какое именно сочетание было нажато, и, если это не Ctrl+Alt+Shift+Z, — выходить из цикла.