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

Преамбула


Так получилось, что я с давних пор пользуюсь мышками 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;
}

Небольшие технические комментарии
@
30.09.2010 22:59 UTC
Первоисточник