Как стать автором
Обновить

Небольшой трюк для быстрого переключения между приложениями

Время на прочтение4 мин
Количество просмотров12K

Преамбула


Так получилось, что я с давних пор пользуюсь мышками 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.
  • Эмпирические условия выбора «правильного» окна выглядят следующим образом. Окно должно:
    1. быть видимым;
    2. не иметь расширенного стиля WS_EX_TOOLWINDOW;
    3. окно-владелец для данного окна должно отсутствовать или быть невидимым.
  • Первый цикл (поиск «корневого» окна-владельца) нужен для того, чтобы справиться с ситуацией, когда текущим окном является «неправильное» окно. Например, в программе ABBYY Lingvo все окна, включая окна-карточки, являются «неправильными»: это окна верхнего уровня, и владельцем каждого из них является некое фиктивное окно, имеющее флаг видимости, но с нулевыми размерами. Если текущим окном является такое окно-карточка, то цикл GetWindow(current_wnd, GW_HWNDNEXT) первым делом попадает в это самое фиктивное окошко Lingvo и, поскольку оно формально является «правильным», активизирует его. Т.е. переключения в другое приложение не происходит. Можно было бы вместо цикла использовать GetAncestor(current_wnd, GA_ROOTOWNER), но в этом случае не учитывается видимость окон и результат получается неправильный.
  • К сожалению, данный эвристический алгоритм неидеален. В частности, с окнами Excel 2003 он работает неправильно (легко зацикливается на одном окне). Правда, частично это вина и самого Excel, который организует окна каким-то совершенно невразумительным образом, так что даже стандартный Alt+Tab может зациклиться на одном окне и потребуется двукратное нажатие Tab, чтобы проскочить этот цикл. С этой загадкой я ещё планирую поразбираться. Также нужно быть осторожным с виртуальными машинами, поскольку в случае перехвата клавиатурного ввода сочетание клавиш пойдёт в виртуалку, а не в хостовую систему.
  • В текущем варианте корректного выхода из программы не предусмотрено (поэтому, вообще говоря, вызов UnregisterHotKey в конце программы лишний, но я его оставил для красоты :-) ). Если требуется периодически завершать выполнение программы, а убивать процесс не хочется, можно добавить регистрацию ещё одного глобального сочетания клавиш, а в цикл обработки сообщений воткнуть проверку, какое именно сочетание было нажато, и, если это не Ctrl+Alt+Shift+Z, — выходить из цикла.
Теги:
Хабы:
Всего голосов 24: ↑15 и ↓9+6
Комментарии19

Публикации

Ближайшие события