You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Python中user32.GetMessage无法退出,如何优雅终止消息循环线程?

如何优雅退出运行Windows消息循环的后台线程?

我在Python应用中通过ctypes调用Windows API安装系统钩子,把消息循环放在后台线程执行时功能正常,但终止线程时遇到了麻烦:按设计,GetMessage收到WM_QUIT消息应该返回0退出循环,但实际不管用。我试过SendMessagePostMessagePostThreadMessagePostQuitMessage的各种组合(包括A/W版本)都没成功,也尝试过在线程上下文抛异常,但线程阻塞在DLL里,GIL已经释放,这个方法也失效了。推测问题出在没法获取监听消息的线程句柄,也没找到相关文档,请问该怎么让这个线程优雅退出?

附原始代码

#!python3
import tkinter as tk
import ctypes
import ctypes.wintypes
from threading import Thread

user32 = ctypes.windll.user32

def loop():
    msg = ctypes.wintypes.MSG()
    while user32.GetMessageW(ctypes.byref(msg), 0, 0, 0) != 0:
        print('msg')
        user32.TranslateMessageW(msg)
        user32.DispatchMessageW(msg)
    print('end of loop')

def SendMessage1():
    user32.SendMessageA(0xFFFF, 0x001C, 0, 0) # broadcast handle, app_activate message
def SendMessage2():
    user32.SendMessageA(0, 0x001C, 0, 0) # null handle
def PostMessage1():
    user32.PostMessageA(0xFFFF, 0x001C, 0, 0)
def PostMessage2():
    user32.PostMessageA(0, 0x001C, 0, 0)
def PostThreadMessage1():
    user32.PostThreadMessageA(0xFFFF, 0x001C, 0, 0)
def PostThreadMessage2():
    user32.PostThreadMessageA(0, 0x001C, 0, 0)
def PostQuitMessage1():
    user32.PostQuitMessage(0)
def PostQuitMessage2():
    user32.PostQuitMessage(0)

root = tk.Tk()
t = Thread(target=loop)
t.daemon=True
t.start()

tk.Button(root, text='SendMessage 1', command=SendMessage1).pack()
tk.Button(root, text='SendMessage 2', command=SendMessage2).pack()
tk.Button(root, text='PostMessage 1', command=PostMessage1).pack()
tk.Button(root, text='PostMessage 2', command=PostMessage2).pack()
tk.Button(root, text='PostThreadMessage 1', command=PostThreadMessage1).pack()
tk.Button(root, text='PostThreadMessage 2', command=PostThreadMessage2).pack()
tk.Button(root, text='PostQuitMessage 1', command=PostQuitMessage1).pack()
tk.Button(root, text='PostQuitMessage 2', command=PostQuitMessage2).pack()

root.mainloop()

问题根源与解决方案

搞定这个问题的核心在于精准定位消息循环所在的线程ID,然后给它发送正确的退出消息。我来一步步拆解你之前的误区和正确做法:

1. 为什么你的尝试都没用?

  • PostQuitMessage(0):这个函数是给当前调用线程的消息队列发WM_QUIT,你在主线程点击按钮调用它,只会给主线程发消息,后台线程根本收不到,自然不会退出。
  • SendMessage/PostMessage:这两个函数需要目标窗口句柄,你的后台线程没有创建窗口,所以不管用0还是0xFFFF广播,消息都到不了后台线程的队列。
  • PostThreadMessage:你用了0(当前线程)或0xFFFF(广播),但都不是后台线程的真实ID,所以消息发错了对象。

2. 正确做法:获取后台线程ID并发送WM_QUIT

你需要先拿到后台线程的ID,然后用PostThreadMessage给它发送WM_QUIT(消息ID为0x0012)。具体步骤:

  • 在后台线程启动时,获取自身的线程ID并保存下来(可以用GetCurrentThreadIdAPI)。
  • 新增一个退出函数,用保存的线程ID调用PostThreadMessageW发送WM_QUIT。

3. 修改后的完整代码

#!python3
import tkinter as tk
import ctypes
import ctypes.wintypes
from threading import Thread

user32 = ctypes.windll.user32
# 定义全局变量保存后台线程ID
background_thread_id = 0

def loop():
    global background_thread_id
    # 获取当前线程ID
    background_thread_id = user32.GetCurrentThreadId()
    msg = ctypes.wintypes.MSG()
    # GetMessageW收到WM_QUIT会返回0,退出循环
    while user32.GetMessageW(ctypes.byref(msg), 0, 0, 0) != 0:
        print('msg')
        user32.TranslateMessageW(msg)
        user32.DispatchMessageW(msg)
    print('end of loop')

def QuitBackgroundThread():
    # 给后台线程发送WM_QUIT消息
    if background_thread_id != 0:
        user32.PostThreadMessageW(background_thread_id, 0x0012, 0, 0)

# 保留你原来的测试函数(可选,用于对比)
def SendMessage1():
    user32.SendMessageA(0xFFFF, 0x001C, 0, 0) # broadcast handle, app_activate message
def SendMessage2():
    user32.SendMessageA(0, 0x001C, 0, 0) # null handle
def PostMessage1():
    user32.PostMessageA(0xFFFF, 0x001C, 0, 0)
def PostMessage2():
    user32.PostMessageA(0, 0x001C, 0, 0)
def PostThreadMessage1():
    user32.PostThreadMessageA(0xFFFF, 0x001C, 0, 0)
def PostThreadMessage2():
    user32.PostThreadMessageA(0, 0x001C, 0, 0)
def PostQuitMessage1():
    user32.PostQuitMessage(0)
def PostQuitMessage2():
    user32.PostQuitMessage(0)

root = tk.Tk()
t = Thread(target=loop)
t.daemon=True
t.start()

# 新增退出按钮
tk.Button(root, text='Quit Background Thread', command=QuitBackgroundThread, bg='red').pack()

# 保留原来的测试按钮
tk.Button(root, text='SendMessage 1', command=SendMessage1).pack()
tk.Button(root, text='SendMessage 2', command=SendMessage2).pack()
tk.Button(root, text='PostMessage 1', command=PostMessage1).pack()
tk.Button(root, text='PostMessage 2', command=PostMessage2).pack()
tk.Button(root, text='PostThreadMessage 1', command=PostThreadMessage1).pack()
tk.Button(root, text='PostThreadMessage 2', command=PostThreadMessage2).pack()
tk.Button(root, text='PostQuitMessage 1', command=PostQuitMessage1).pack()
tk.Button(root, text='PostQuitMessage 2', command=PostQuitMessage2).pack()

root.mainloop()

4. 额外注意事项

  • 确保后台线程已经初始化了消息队列:GetMessage第一次调用时会自动创建线程的消息队列,所以只要线程启动后再点击退出按钮就没问题。如果担心线程还没启动,可以加个线程就绪的标记。
  • 必须用WM_QUIT消息:只有这个消息会让GetMessage返回0,其他消息只会被TranslateMessageDispatchMessage处理,不会触发循环退出。

内容的提问来源于stack exchange,提问作者James Kent

火山引擎 最新活动