ウィンドウを確実にアクティブにする方法

(背景) やりたいこと

ウィンドウハンドル hwnd に対し、activate(hwnd) を実行すると、hwnd が確実にアクティブ(Altabで選択した時の状態)になる……このような activate 関数を実装したい。

Windows 7 を想定。

答え

AttachThreadInput → BringWindowToTop をした後に、当該ウィンドウの左上をクリックする というやり方にした。

ソース(一部オレオレライブラリがあるけど割愛):

def activate_by_click(target_hwnd):
    rect = windowlib.GetWindowRect(target_hwnd)
    curmx, curmy = mouselib.get_cursorpos()
    mouselib.set_cursorpos(rect.xpos, rect.ypos)
    mouselib.left_click()
    # restore cursor pos.
    mouselib.set_cursorpos(curmx, curmy)

def activate_by_attach(hwnd):
    import win32process
    import win32api
    import win32gui
    import ctypes

    forehwnd = windowlib.GetForegroundWindow()
    fore_threadid, processid = win32process.GetWindowThreadProcessId(forehwnd)
    current_threadid = win32api.GetCurrentThreadId()

    # foreground なスレッドにアタッチする
    if fore_threadid != current_threadid:
        try:
            # たまに error:87 が起きるので吸収.
            win32process.AttachThreadInput(current_threadid, fore_threadid, True)
        except:
            pass

    try:
        ctypes.windll.user32.BringWindowToTop(hwnd)
    except:
        pass

    if fore_threadid != current_threadid:
        try:
            win32process.AttachThreadInput(current_threadid, fore_threadid, False)
        except:
            pass

def activete(target_hwnd):
    activate_by_attach(target_hwnd)
    activate_by_click(target_hwnd)

activate_by_attach() だけだとアクティブにならないケースがどうしても出てくる。原因もよーわからん。

そもそも Windows におけるウィンドウ制御では、あらゆるウィンドウのあらゆる状況を考慮して 100% 成功する挙動を実装するのは非現実的(*1)である。ある程度は諦めるしかないのだが、ふと「クリック使えばいいんじゃね?w」とひらめき、試したところ、かなり精度が良いので採用した。

activate_by_click() 関数として実装している。やってることはアクティブ対象ウィンドウの左上座標をゲットして、そこをクリックするだけ。ただしマウスカーソルが動いちゃうので、後で元に戻す処理も入れている。

  • *1 Windows におけるウィンドウの諸性質は本質的に複雑であり(winapi を触ったことがあるなら関数の多さとパラメータの多さがわかるはずだ)、完全に把握できている人などこの世に一人もいないのではないかと思うほどである

参考

おわりに

クリックは邪道だけど、今のところ、ほぼ確実にアクティブ化できているので良しとする。