鉴于当前的 gevent API,我认为没有通用的解决方案,但我认为每个平台可能都有特定的解决方案。
Posix解决方案(在Linux下测试)
由于 GLib 的主循环接口允许我们设置 poll 函数,即获取一组文件描述符并在其中一个准备好时返回的函数,我们定义了一个 poll 函数,它依赖于 gevent 的 select 来知道文件描述符何时准备好.
Gevent 没有暴露poll()
接口,select()
接口有点不同,所以我们在调用的时候要翻译参数和返回值gevent.select.select()
。
使事情复杂一点的是,GLib 没有通过 Python 的接口公开g_main_set_poll_func()
允许该技巧的特定功能。所以我们必须直接使用C函数,为此,ctypes
模块就派上用场了。
import ctypes
from gi.repository import GLib
from gevent import select
# Python representation of C struct
class _GPollFD(ctypes.Structure):
_fields_ = [("fd", ctypes.c_int),
("events", ctypes.c_short),
("revents", ctypes.c_short)]
# Poll function signature
_poll_func_builder = ctypes.CFUNCTYPE(None, ctypes.POINTER(_GPollFD), ctypes.c_uint, ctypes.c_int)
# Pool function
def _poll(ufds, nfsd, timeout):
rlist = []
wlist = []
xlist = []
for i in xrange(nfsd):
wfd = ufds[i]
if wfd.events & GLib.IOCondition.IN.real:
rlist.append(wfd.fd)
if wfd.events & GLib.IOCondition.OUT.real:
wlist.append(wfd.fd)
if wfd.events & (GLib.IOCondition.ERR.real | GLib.IOCondition.HUP.real):
xlist.append(wfd.fd)
if timeout < 0:
timeout = None
else:
timeout = timeout / 1000.0
(rlist, wlist, xlist) = select.select(rlist, wlist, xlist, timeout)
for i in xrange(nfsd):
wfd = ufds[i]
wfd.revents = 0
if wfd.fd in rlist:
wfd.revents = GLib.IOCondition.IN.real
if wfd.fd in wlist:
wfd.revents |= GLib.IOCondition.OUT.real
if wfd.fd in xlist:
wfd.revents |= GLib.IOCondition.HUP.real
ufds[i] = wfd
_poll_func = _poll_func_builder(_poll)
glib = ctypes.CDLL('libglib-2.0.so.0')
glib.g_main_context_set_poll_func(None, _poll_func)
我觉得应该有更好的解决方案,因为这样我们需要知道正在使用的 GLib 的具体版本/名称。g_main_set_poll_func()
如果 GLib在 Python 中公开,则可以避免这种情况。此外,如果gevent
实施select()
,它可以很好地实施poll()
,什么会使这个解决方案更简单。
Windows 部分解决方案(又丑又坏)
Posix 解决方案在 Windows 上失败,因为select()
只能使用网络套接字,而给定的 Gtk 句柄则不是。所以我想在另一个线程中使用 GLib 自己的g_poll()
实现(Posix 上的瘦包装器,它是 Windows 上相当复杂的实现)来等待 UI 事件,并通过 TCP 套接字将其与主线程中的 gevent 端同步. 这是一个非常难看的方法,因为它需要真正的线程(除了你可能会使用的greenlets,如果你正在使用gevent)和等待线程端的普通(非gevent)套接字。
太糟糕了 Windows 上的 UI 事件是由线程分割的,因此默认情况下一个线程不能等待另一个线程上的事件。在您执行一些 UI 操作之前,不会创建特定线程上的消息队列。所以我不得不MessageBoxA()
在等待线程上创建一个空的 WinAPI 消息框()(当然有更好的方法),并用它来处理线程消息队列,AttachThreadInput()
以便它可以看到主线程的事件。这一切都通过ctypes
.
import ctypes
import ctypes.wintypes
import gevent
from gevent_patcher import orig_socket as socket
from gi.repository import GLib
from threading import Thread
_poll_args = None
_sock = None
_running = True
def _poll_thread(glib, addr, main_tid):
global _poll_args
# Needed to create a message queue on this thread:
ctypes.windll.user32.MessageBoxA(None, ctypes.c_char_p('Ugly hack'),
ctypes.c_char_p('Just click'), 0)
this_tid = ctypes.wintypes.DWORD(ctypes.windll.kernel32.GetCurrentThreadId())
w_true = ctypes.wintypes.BOOL(True)
w_false = ctypes.wintypes.BOOL(False)
sock = socket()
sock.connect(addr)
del addr
try:
while _running:
sock.recv(1)
ctypes.windll.user32.AttachThreadInput(main_tid, this_tid, w_true)
glib.g_poll(*_poll_args)
ctypes.windll.user32.AttachThreadInput(main_tid, this_tid, w_false)
sock.send('a')
except IOError:
pass
sock.close()
class _GPollFD(ctypes.Structure):
_fields_ = [("fd", ctypes.c_int),
("events", ctypes.c_short),
("revents", ctypes.c_short)]
_poll_func_builder = ctypes.CFUNCTYPE(None, ctypes.POINTER(_GPollFD), ctypes.c_uint, ctypes.c_int)
def _poll(*args):
global _poll_args
_poll_args = args
_sock.send('a')
_sock.recv(1)
_poll_func = _poll_func_builder(_poll)
# Must be called before Gtk.main()
def register_poll():
global _sock
sock = gevent.socket.socket()
sock.bind(('127.0.0.1', 0))
addr = sock.getsockname()
sock.listen(1)
this_tid = ctypes.wintypes.DWORD(ctypes.windll.kernel32.GetCurrentThreadId())
glib = ctypes.CDLL('libglib-2.0-0.dll')
Thread(target=_poll_thread, args=(glib, addr, this_tid)).start()
_sock, _ = sock.accept()
sock.close()
glib.g_main_context_set_poll_func(None, _poll_func)
# Must be called after Gtk.main()
def clean_poll():
global _sock, _running
_running = False
_sock.close()
del _sock
到目前为止,应用程序运行并正确响应点击和其他用户事件,但在窗口内没有绘制任何内容(我可以看到框架和粘贴到其中的背景缓冲区)。在线程和消息队列的处理中可能缺少一些重绘命令。我不知道如何解决它。有什么帮助吗?关于如何做到这一点有更好的主意吗?