Tkinter窗口事件<visibility>

时间:2018-01-15 12:27:12

标签: python tkinter visibility

如果窗口可见性发生变化,我试图获取一个事件。 我发现有一个名为“可见性”的事件。 操作系统是Windows 64bit。 所以我通过以下方式实现:

root.bind('<Visibility>', visibilityChanged)

但无论是否有窗户,我总是得到“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()

1 个答案:

答案 0 :(得分:2)

  

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

docs X服务器在可见性更改状态和任何窗口时生成VisibilityNotify事件。< / em>的

  

我该如何实现这样的功能?

这取决于你的愿望会走多远,因为这不是一项微不足道的任务。因此,不要将该答案视为完整的解决方案,而应将其视为问题概述和一系列建议。

事件问题

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

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

主要问题是获取事件,因为Windows WM没有VisibilityNotify这样的消息。正如我在评论部分所说 - 我们可以依赖的一个选项是z-order  (只要此窗口改变了它在z-order中的位置),就有可能检查窗口的可见性。
因此,我们的目标消息是WM_WINDOWPOSCHANGING或{{ 3}}

天真的实施:

import ctypes
import ctypes.wintypes as wintypes
import tkinter as tk


class CWPRETSTRUCT(ctypes.Structure):
    ''' a class to represent CWPRETSTRUCT structure
    https://msdn.microsoft.com/en-us/library/windows/desktop/ms644963(v=vs.85).aspx '''

    _fields_ = [('lResult', wintypes.LPARAM),
                ('lParam', wintypes.LPARAM),
                ('wParam', wintypes.WPARAM),
                ('message', wintypes.UINT),
                ('hwnd', wintypes.HWND)]


class WINDOWPOS(ctypes.Structure):
    ''' a class to represent WINDOWPOS structure
    https://msdn.microsoft.com/en-gb/library/windows/desktop/ms632612(v=vs.85).aspx '''

    _fields_ = [('hwnd', wintypes.HWND),
                ('hwndInsertAfter', wintypes.HWND),
                ('x', wintypes.INT),
                ('y', wintypes.INT),
                ('cx', wintypes.INT),
                ('cy', wintypes.INT),
                ('flags', wintypes.UINT)]


class App(tk.Tk):
    ''' generic tk app with win api interaction '''

    wm_windowposschanged = 71
    wh_callwndprocret = 12
    swp_noownerzorder = 512
    set_hook = ctypes.windll.user32.SetWindowsHookExW
    call_next_hook = ctypes.windll.user32.CallNextHookEx
    un_hook = ctypes.windll.user32.UnhookWindowsHookEx
    get_thread = ctypes.windll.kernel32.GetCurrentThreadId
    get_error = ctypes.windll.kernel32.GetLastError
    get_parent = ctypes.windll.user32.GetParent
    wnd_ret_proc = ctypes.WINFUNCTYPE(ctypes.c_long, wintypes.INT, wintypes.WPARAM, wintypes.LPARAM)

    def __init__(self):
        ''' generic __init__ '''

        super().__init__()
        self.minsize(350, 200)
        self.hook = self.setup_hook()
        self.protocol('WM_DELETE_WINDOW', self.on_closing)

    def setup_hook(self):
        ''' setting up the hook '''

        thread = self.get_thread()
        hook = self.set_hook(self.wh_callwndprocret, self.call_wnd_ret_proc, wintypes.HINSTANCE(0), thread)

        if not hook:
            raise ctypes.WinError(self.get_error())

        return hook

    def on_closing(self):
        ''' releasing the hook '''
        if self.hook:
            self.un_hook(self.hook)
        self.destroy()

    @staticmethod
    @wnd_ret_proc
    def call_wnd_ret_proc(nCode, wParam, lParam):
        ''' an implementation of the CallWndRetProc callback
        https://msdn.microsoft.com/en-us/library/windows/desktop/ms644976(v=vs.85).aspx'''

        #   get a message
        msg = ctypes.cast(lParam, ctypes.POINTER(CWPRETSTRUCT)).contents
        if msg.message == App.wm_windowposschanged and msg.hwnd == App.get_parent(app.winfo_id()):
            #   if message, which belongs to owner hwnd, is signaling that windows position is changed - check z-order
            wnd_pos = ctypes.cast(msg.lParam, ctypes.POINTER(WINDOWPOS)).contents
            print('z-order changed: %r' % ((wnd_pos.flags & App.swp_noownerzorder) != App.swp_noownerzorder))

        return App.call_next_hook(None, nCode, wParam, lParam)


app = App()
app.mainloop()

正如您所看到的,此实现具有与&#34;破坏&#34;相似的行为。 Visibility活动。

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

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

可见性问题

确定窗口遮挡不是问题,因为我们可以依赖WM_WINDOWPOSCHANGED

逻辑很简单:

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

如果最终的几何减法是:

  • ...一个空矩形 - return 'VisibilityFullyObscured'
  • ...一组矩形 - return 'VisibilityPartiallyObscured'
  • ...一个矩形:
    • 如果结果与原始矩形之间的几何差异为:
      1. ...一个空矩形 - return 'VisibilityUnobscured'
      2. ...一个矩形 - return 'VisibilityPartiallyObscured'

一个天真的实现(具有自调度循环):

import ctypes
import ctypes.wintypes as wintypes
import tkinter as tk


class App(tk.Tk):
    ''' generic tk app with win api interaction '''
    enum_windows = ctypes.windll.user32.EnumWindows
    is_window_visible = ctypes.windll.user32.IsWindowVisible
    get_window_rect = ctypes.windll.user32.GetWindowRect
    create_rect_rgn = ctypes.windll.gdi32.CreateRectRgn
    combine_rgn = ctypes.windll.gdi32.CombineRgn
    del_rgn = ctypes.windll.gdi32.DeleteObject
    get_parent = ctypes.windll.user32.GetParent
    enum_windows_proc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)

    def __init__(self):
        ''' generic __init__ '''
        super().__init__()
        self.minsize(350, 200)

        self.status_label = tk.Label(self)
        self.status_label.pack()

        self.after(100, self.continuous_check)
        self.state = ''

    def continuous_check(self):
        ''' continuous (self-scheduled) check '''
        state = self.determine_obscuration()

        if self.state != state:
            #   mimic the event - fire only when state changes
            print(state)
            self.status_label.config(text=state)
            self.state = state
        self.after(100, self.continuous_check)

    def enumerate_higher_windows(self, self_hwnd):
        ''' enumerate window, which has a higher position in z-order '''

        @self.enum_windows_proc
        def enum_func(hwnd, lParam):
            ''' clojure-callback for enumeration '''
            rect = wintypes.RECT()
            if hwnd == lParam:
                #   stop enumeration if hwnd is equal to lParam (self_hwnd)
                return False
            else:
                #   continue enumeration
                if self.is_window_visible(hwnd):
                    self.get_window_rect(hwnd, ctypes.byref(rect))
                    rgn = self.create_rect_rgn(rect.left, rect.top, rect.right, rect.bottom)
                    #   append region
                    rgns.append(rgn)
            return True

        rgns = []
        self.enum_windows(enum_func, self_hwnd)

        return rgns

    def determine_obscuration(self):
        ''' determine obscuration via CombineRgn '''
        hwnd = self.get_parent(self.winfo_id())
        results = {1: 'VisibilityFullyObscured', 2: 'VisibilityUnobscured', 3: 'VisibilityPartiallyObscured'}
        rgns = self.enumerate_higher_windows(hwnd)
        result = 2

        if len(rgns):
            rect = wintypes.RECT()
            self.get_window_rect(hwnd, ctypes.byref(rect))

            #   region of tk-window
            reference_rgn = self.create_rect_rgn(rect.left, rect.top, rect.right, rect.bottom)
            #   temp region for storing diff and xor rgn-results
            rgn = self.create_rect_rgn(0, 0, 0, 0)

            #   iterate over stored results
            for _ in range(len(rgns)):
                _rgn = rgn if _ != 0 else reference_rgn
                result = self.combine_rgn(rgn, _rgn, rgns[_], 4)
                self.del_rgn(rgns[_])

            if result != 2:
                #   if result isn't a single rectangle
                #   (NULLREGION - 'VisibilityFullyObscured' or COMPLEXREGION - 'VisibilityPartiallyObscured')
                pass
            elif self.combine_rgn(rgn, reference_rgn, rgn, 3) == 1:
                #   if result of XOR is NULLREGION - 'VisibilityUnobscured'
                result = 2
            else:
                #   'VisibilityPartiallyObscured'
                result = 3

            #   clear up regions to prevent memory leaking
            self.del_rgn(rgn)
            self.del_rgn(reference_rgn)

        return results[result]

app = App()
app.mainloop()

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