Tkinter窗口事件<可见性>

Hol*_*urm 5 python visibility tkinter

如果Windows可见性已更改,我尝试获取一个事件。我发现有一个名为“可见性”的事件。操作系统是Windows 64位。因此,我以以下方式实现:

root.bind('<Visibility>', visibilityChanged)
Run Code Online (Sandbox Code Playgroud)

但是,无论是否有窗口,我总是处于“ VisibilityUnobscured”状态。此事件的正常行为是什么?如何实现这样的功能?

示例编:

import tkinter as tk

class GUI:
    def __init__(self, master):
        self.master = master
        master.title("Test GUI")
        self.master.bind('<Visibility>', self.visibilityChanged)
        self.label = tk.Label(master, text="GUI")
        self.label.pack()

        self.close_button = tk.Button(master, text="Close", command=master.quit)
        self.close_button.pack()

    def visibilityChanged(self, event):
        if (str(event.type) == "Visibility"):
            print(event.state)

root = tk.Tk()
my_gui = GUI(root)
root.mainloop()
Run Code Online (Sandbox Code Playgroud)

Com*_*nse 5

\n

此事件的正常行为是什么?

\n
\n\n

文档中对此进行了很好的描述:每当可见性更改状态以及任何窗口时,
服务器X都会生成事件。VisibilityNotify

\n\n
\n

我怎样才能实现这样的功能?

\n
\n\n

这取决于您要实现自己的愿望有多远,因为这不是一项微不足道的任务。因此,不要将该答案视为完整的解决方案,而是将其视为问题概述和一组建议。

\n\n

事件问题

\n\n

Windows OS使用消息传递模型- 系统通过消息与应用程序窗口进行通信,其中每条消息都是指定特定事件的数字代码。应用程序窗口有一个关联的窗口过程\xe2\x80\x94,该函数处理(响应或忽略)所有发送的消息。

\n\n

最通用的解决方案是设置一个钩子来捕获某些事件/消息,可以通过SetWindowsHookExpyHook来实现。

\n\n

主要问题是获取事件,因为Windows WM没有诸如 之类的消息VisibilityNotify。正如我在评论部分中所说 - 我们可以依赖的一个选项是z-order\n (每当此窗口更改其在 中的位置时,都可以检查窗口的可见性z-order)。
因此,我们的目标消息是WM_WINDOWPOSCHANGINGWM_WINDOWPOSCHANGED

\n\n

一个幼稚的实现:

\n\n
import ctypes\nimport ctypes.wintypes as wintypes\nimport tkinter as tk\n\n\nclass CWPRETSTRUCT(ctypes.Structure):\n    \'\'\' a class to represent CWPRETSTRUCT structure\n    https://msdn.microsoft.com/en-us/library/windows/desktop/ms644963(v=vs.85).aspx \'\'\'\n\n    _fields_ = [(\'lResult\', wintypes.LPARAM),\n                (\'lParam\', wintypes.LPARAM),\n                (\'wParam\', wintypes.WPARAM),\n                (\'message\', wintypes.UINT),\n                (\'hwnd\', wintypes.HWND)]\n\n\nclass WINDOWPOS(ctypes.Structure):\n    \'\'\' a class to represent WINDOWPOS structure\n    https://msdn.microsoft.com/en-gb/library/windows/desktop/ms632612(v=vs.85).aspx \'\'\'\n\n    _fields_ = [(\'hwnd\', wintypes.HWND),\n                (\'hwndInsertAfter\', wintypes.HWND),\n                (\'x\', wintypes.INT),\n                (\'y\', wintypes.INT),\n                (\'cx\', wintypes.INT),\n                (\'cy\', wintypes.INT),\n                (\'flags\', wintypes.UINT)]\n\n\nclass App(tk.Tk):\n    \'\'\' generic tk app with win api interaction \'\'\'\n\n    wm_windowposschanged = 71\n    wh_callwndprocret = 12\n    swp_noownerzorder = 512\n    set_hook = ctypes.windll.user32.SetWindowsHookExW\n    call_next_hook = ctypes.windll.user32.CallNextHookEx\n    un_hook = ctypes.windll.user32.UnhookWindowsHookEx\n    get_thread = ctypes.windll.kernel32.GetCurrentThreadId\n    get_error = ctypes.windll.kernel32.GetLastError\n    get_parent = ctypes.windll.user32.GetParent\n    wnd_ret_proc = ctypes.WINFUNCTYPE(ctypes.c_long, wintypes.INT, wintypes.WPARAM, wintypes.LPARAM)\n\n    def __init__(self):\n        \'\'\' generic __init__ \'\'\'\n\n        super().__init__()\n        self.minsize(350, 200)\n        self.hook = self.setup_hook()\n        self.protocol(\'WM_DELETE_WINDOW\', self.on_closing)\n\n    def setup_hook(self):\n        \'\'\' setting up the hook \'\'\'\n\n        thread = self.get_thread()\n        hook = self.set_hook(self.wh_callwndprocret, self.call_wnd_ret_proc, wintypes.HINSTANCE(0), thread)\n\n        if not hook:\n            raise ctypes.WinError(self.get_error())\n\n        return hook\n\n    def on_closing(self):\n        \'\'\' releasing the hook \'\'\'\n        if self.hook:\n            self.un_hook(self.hook)\n        self.destroy()\n\n    @staticmethod\n    @wnd_ret_proc\n    def call_wnd_ret_proc(nCode, wParam, lParam):\n        \'\'\' an implementation of the CallWndRetProc callback\n        https://msdn.microsoft.com/en-us/library/windows/desktop/ms644976(v=vs.85).aspx\'\'\'\n\n        #   get a message\n        msg = ctypes.cast(lParam, ctypes.POINTER(CWPRETSTRUCT)).contents\n        if msg.message == App.wm_windowposschanged and msg.hwnd == App.get_parent(app.winfo_id()):\n            #   if message, which belongs to owner hwnd, is signaling that windows position is changed - check z-order\n            wnd_pos = ctypes.cast(msg.lParam, ctypes.POINTER(WINDOWPOS)).contents\n            print(\'z-order changed: %r\' % ((wnd_pos.flags & App.swp_noownerzorder) != App.swp_noownerzorder))\n\n        return App.call_next_hook(None, nCode, wParam, lParam)\n\n\napp = App()\napp.mainloop()\n
Run Code Online (Sandbox Code Playgroud)\n\n

正如您所看到的,此实现具有与“损坏”事件类似的行为Visibility

\n\n

这个问题源于这样一个事实,即您只能捕获线程指定的消息,因此应用程序不知道堆栈中的更改。这只是我的假设,但我认为损坏的原因Visibility是相同的。

\n\n

当然,我们可以为所有消息设置一个全局钩子,无论是哪个线程,但这种方法需要 DLL 注入,这肯定是另一回事了。

\n\n

可见性问题

\n\n

确定窗口的遮挡不是问题,因为我们可以依赖图形设备接口

\n\n

逻辑很简单:

\n\n
    \n
  • 将窗口(以及每个可见窗口,位于 中较高的窗口z-order)表示为矩形。
  • \n
  • 从主矩形中减去每个矩形并存储结果。
  • \n
\n\n

如果最终的几何减法是:

\n\n
    \n
  • ...一个空矩形 \xe2\x80\x94return \'VisibilityFullyObscured\'
  • \n
  • ...一组矩形 \xe2\x80\x94return \'VisibilityPartiallyObscured\'
  • \n
  • ...单个矩形:\n\n
      \n
    • 如果结果与原始矩形之间的几何差异为:\n\n
        \n
      1. ...一个空矩形 \xe2\x80\x94return \'VisibilityUnobscured\'
      2. \n
      3. ...单个矩形 \xe2\x80\x94return \'VisibilityPartiallyObscured\'
      4. \n
    • \n
  • \n
\n\n

一个简单的实现(带有自调度循环):

\n\n
import ctypes\nimport ctypes.wintypes as wintypes\nimport tkinter as tk\n\n\nclass App(tk.Tk):\n    \'\'\' generic tk app with win api interaction \'\'\'\n    enum_windows = ctypes.windll.user32.EnumWindows\n    is_window_visible = ctypes.windll.user32.IsWindowVisible\n    get_window_rect = ctypes.windll.user32.GetWindowRect\n    create_rect_rgn = ctypes.windll.gdi32.CreateRectRgn\n    combine_rgn = ctypes.windll.gdi32.CombineRgn\n    del_rgn = ctypes.windll.gdi32.DeleteObject\n    get_parent = ctypes.windll.user32.GetParent\n    enum_windows_proc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)\n\n    def __init__(self):\n        \'\'\' generic __init__ \'\'\'\n        super().__init__()\n        self.minsize(350, 200)\n\n        self.status_label = tk.Label(self)\n        self.status_label.pack()\n\n        self.after(100, self.continuous_check)\n        self.state = \'\'\n\n    def continuous_check(self):\n        \'\'\' continuous (self-scheduled) check \'\'\'\n        state = self.determine_obscuration()\n\n        if self.state != state:\n            #   mimic the event - fire only when state changes\n            print(state)\n            self.status_label.config(text=state)\n            self.state = state\n        self.after(100, self.continuous_check)\n\n    def enumerate_higher_windows(self, self_hwnd):\n        \'\'\' enumerate window, which has a higher position in z-order \'\'\'\n\n        @self.enum_windows_proc\n        def enum_func(hwnd, lParam):\n            \'\'\' clojure-callback for enumeration \'\'\'\n            rect = wintypes.RECT()\n            if hwnd == lParam:\n                #   stop enumeration if hwnd is equal to lParam (self_hwnd)\n                return False\n            else:\n                #   continue enumeration\n                if self.is_window_visible(hwnd):\n                    self.get_window_rect(hwnd, ctypes.byref(rect))\n                    rgn = self.create_rect_rgn(rect.left, rect.top, rect.right, rect.bottom)\n                    #   append region\n                    rgns.append(rgn)\n            return True\n\n        rgns = []\n        self.enum_windows(enum_func, self_hwnd)\n\n        return rgns\n\n    def determine_obscuration(self):\n        \'\'\' determine obscuration via CombineRgn \'\'\'\n        hwnd = self.get_parent(self.winfo_id())\n        results = {1: \'VisibilityFullyObscured\', 2: \'VisibilityUnobscured\', 3: \'VisibilityPartiallyObscured\'}\n        rgns = self.enumerate_higher_windows(hwnd)\n        result = 2\n\n        if len(rgns):\n            rect = wintypes.RECT()\n            self.get_window_rect(hwnd, ctypes.byref(rect))\n\n            #   region of tk-window\n            reference_rgn = self.create_rect_rgn(rect.left, rect.top, rect.right, rect.bottom)\n            #   temp region for storing diff and xor rgn-results\n            rgn = self.create_rect_rgn(0, 0, 0, 0)\n\n            #   iterate over stored results\n            for _ in range(len(rgns)):\n                _rgn = rgn if _ != 0 else reference_rgn\n                result = self.combine_rgn(rgn, _rgn, rgns[_], 4)\n                self.del_rgn(rgns[_])\n\n            if result != 2:\n                #   if result isn\'t a single rectangle\n                #   (NULLREGION - \'VisibilityFullyObscured\' or COMPLEXREGION - \'VisibilityPartiallyObscured\')\n                pass\n            elif self.combine_rgn(rgn, reference_rgn, rgn, 3) == 1:\n                #   if result of XOR is NULLREGION - \'VisibilityUnobscured\'\n                result = 2\n            else:\n                #   \'VisibilityPartiallyObscured\'\n                result = 3\n\n            #   clear up regions to prevent memory leaking\n            self.del_rgn(rgn)\n            self.del_rgn(reference_rgn)\n\n        return results[result]\n\napp = App()\napp.mainloop()\n
Run Code Online (Sandbox Code Playgroud)\n\n

不幸的是,这种方法远不是一个有效的解决方案,但从某种角度来看它是可以调整的。

\n