这个事件的正常行为是什么?
在文档中已经很好地描述了:
每当可见性状态发生变化并且对于任何窗口,X
服务器都会生成VisibilityNotify
事件。
我该如何实现类似的功能?
这取决于你想要实现多少功能,因为这不是一个简单的任务。因此,请不要将该答案视为完整的解决方案,而是作为问题概述和一组建议。
事件问题
Windows操作系统
采用消息传递模型 - 系统通过消息与您的应用程序窗口通信,其中每个消息是指定特定事件的数字代码。应用程序窗口有一个关联的窗口过程 - 处理(响应或忽略)所有发送的消息的函数。
最通用的解决方法是设置钩子以捕获特定事件/消息,可以通过
SetWindowsHookEx 或
pyHook 实现。
主要问题是如何获取事件,因为
Windows WM
没有诸如
VisibilityNotify
的消息。正如我在评论部分所说的那样,我们可以依赖于一个选项,即
z-order
(每当该窗口在
z-order
中改变位置时,有可能检查窗口的可见性)。因此,我们的目标消息是
WM_WINDOWPOSCHANGING 或
WM_WINDOWPOSCHANGED。
一个天真的实现:
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'''
msg = ctypes.cast(lParam, ctypes.POINTER(CWPRETSTRUCT)).contents
if msg.message == App.wm_windowposschanged and msg.hwnd == App.get_parent(app.winfo_id()):
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()
正如您所见,该实现具有类似于“损坏”的
Visibility
事件的行为。
这个问题源于一个事实,即您只能捕获特定线程的消息,因此应用程序不知道堆栈中的更改。这只是我的假设,但我认为破碎的
Visibility
的原因是相同的。
当然,我们可以为所有消息设置全局钩子,而不管线程如何,但这种方法需要进行DLL注入,这是另一个故事。
可见性问题
确定窗口的遮挡并不是问题,因为我们可以依靠
图形设备接口。
逻辑很简单:
- 将窗口(以及在
z-order
中更高的每个可见窗口)表示为矩形。
- 从主矩形中减去每个矩形并存储结果。
- ...一个空矩形 -
返回 'VisibilityFullyObscured'
- ...一组矩形 -
返回 'VisibilityPartiallyObscured'
- ...一个单独的矩形:
- 如果结果和原始矩形之间的几何差异是:
- ...一个空矩形 -
返回 'VisibilityUnobscured'
- ...一个单独的矩形 -
返回 '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:
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:
return False
else:
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)
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))
reference_rgn = self.create_rect_rgn(rect.left, rect.top, rect.right, rect.bottom)
rgn = self.create_rect_rgn(0, 0, 0, 0)
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:
pass
elif self.combine_rgn(rgn, reference_rgn, rgn, 3) == 1:
result = 2
else:
result = 3
self.del_rgn(rgn)
self.del_rgn(reference_rgn)
return results[result]
app = App()
app.mainloop()
不幸的是,这种方法远非一个可行的解决方案,但从长远来看还有改进的空间。
Visibility
在非 X11 平台上并不是特别有用。在您的情况下,只有当窗口(实际上是一个“根”,而不是带标题的“边框”)被映射(重绘)时才会收到事件。无论如何,在ms
word 中可见性是另一回事,而且即使您将其最小化,窗口(带标题的“边框”)也是可见的,因为ws_visible
是窗口的样式参数,很少更改。您真正要问的是如何确定交集,因为windows
永远不会告诉您这一点,仅使用tkinter
是不可能的。但是,当然,可以通过ctypes
库作为 Windows 特定解决方案来实现。 - CommonSenseIsWindowVisible
则是另一回事,正如我之前的评论所说 - 它检查窗口是否具有ws_visible
样式,这与您的期望相去甚远。而且,root.winfo_id()
返回客户区域的hwnd
,“带标题”的“边框”具有不同的hwnd
,这对于tkinter
来说是未知的。我猜想,您期望得到包括该边框在内的“整个窗口”的hwnd
。我认为,您应该依赖于z-order
堆栈和IntersectRect
功能。 - CommonSenseGetWindowRect
获取可见窗口的顺序和矩形,所以现在我拥有了这些数据。如何使用 ctypes 在 Python 中访问IntersectRect
? - Toroid