如何让线程通道消息触发Owlkettle界面更新?
问题
基于Owlkettle实现客户端-服务器多线程架构,设计如下:
- 客户端线程:运行Owlkettle构建的GTK应用
- 服务器线程:执行繁重计算任务
- 双向通道:客户端→服务器、服务器→客户端的消息通道
当前遇到的问题:服务器发送的消息无法触发UI即时更新,消息会滞留在通道中,只有当Owlkettle自行触发更新(如点击按钮)时才会被读取并刷新界面。示例代码中,点击按钮后服务器会回复消息,但UI不会立即显示,需再次点击按钮才会更新。
示例代码:
import owlkettle, owlkettle/[playground, adw] import std/[options, os] var counter: int = 1 type ChannelHub = ref object serverChannel: Channel[string] clientChannel: Channel[string] proc sendToServer(hub: ChannelHub, msg: string): bool = echo "send client => server: ", msg hub.clientChannel.trySend(msg) proc sendToClient(hub: ChannelHub, msg: string): bool = echo "send client <= server: ", msg hub.serverChannel.trySend(msg) proc readClientMsg(hub: ChannelHub): Option[string] = let response: tuple[dataAvailable: bool, msg: string] = hub.clientChannel.tryRecv() return if response.dataAvailable: echo "read client => server: ", response.repr some(response.msg) else: none(string) proc readServerMsg(hub: ChannelHub): Option[string] = let response: tuple[dataAvailable: bool, msg: string] = hub.serverChannel.tryRecv() return if response.dataAvailable: echo "read client <= server: ", response.repr some(response.msg) else: none(string) proc setupServer(channels: ChannelHub): Thread[ChannelHub] = proc serverLoop(hub: ChannelHub) = while true: let msg = hub.readClientMsg() if msg.isSome(): discard hub.sendToClient("Received Message " & $counter) counter.inc sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency createThread(result, serverLoop, channels) viewable App: hub: ChannelHub backendMsg: string = "" method view(app: AppState): Widget = let msg: Option[string] = app.hub.readServerMsg() if msg.isSome(): app.backendMsg = msg.get() result = gui: Window: defaultSize = (500, 150) title = "Client Server Example" Box: orient = OrientY margin = 12 spacing = 6 Button {.hAlign: AlignCenter, vAlign: AlignCenter.}: Label(text = "Click me") proc clicked() = discard app.hub.sendToServer("Frontend message!") Label(text = "Message sent by Backend: ") Label(text = app.backendMsg) proc setupClient(channels: ChannelHub): Thread[ChannelHub] = proc startOwlkettle(hub: ChannelHub) = adw.brew(gui(App(hub = hub))) createThread(result, startOwlkettle, channels) proc main() = var serverToClientChannel: Channel[string] var clientToServerChannel: Channel[string] serverToClientChannel.open() clientToServerChannel.open() let hub = ChannelHub(serverChannel: serverToClientChannel, clientChannel: clientToServerChannel) let client = setupClient(hub) let server = setupServer(hub) joinThreads(server, client) main()
解决方案
核心思路是利用GTK的空闲回调机制,在UI线程的空闲时段定期检查服务器通道的消息,一旦有新消息就更新UI状态并触发界面重绘。具体修改如下:
- 给
App添加isRunning字段,用于控制消息检查循环的启停 - 实现
checkForServerMessages过程,作为GTK的空闲回调函数,在UI线程中读取服务器消息、更新状态并触发UI更新 - 在
App的初始化方法中启动这个空闲回调
修改后的完整代码:
import owlkettle, owlkettle/[playground, adw] import std/[options, os, gtk] var counter: int = 1 type ChannelHub = ref object serverChannel: Channel[string] clientChannel: Channel[string] proc sendToServer(hub: ChannelHub, msg: string): bool = echo "send client => server: ", msg hub.clientChannel.trySend(msg) proc sendToClient(hub: ChannelHub, msg: string): bool = echo "send client <= server: ", msg hub.serverChannel.trySend(msg) proc readClientMsg(hub: ChannelHub): Option[string] = let response: tuple[dataAvailable: bool, msg: string] = hub.clientChannel.tryRecv() return if response.dataAvailable: echo "read client => server: ", response.repr some(response.msg) else: none(string) proc readServerMsg(hub: ChannelHub): Option[string] = let response: tuple[dataAvailable: bool, msg: string] = hub.serverChannel.tryRecv() return if response.dataAvailable: echo "read client <= server: ", response.repr some(response.msg) else: none(string) proc setupServer(channels: ChannelHub): Thread[ChannelHub] = proc serverLoop(hub: ChannelHub) = while true: let msg = hub.readClientMsg() if msg.isSome(): discard hub.sendToClient("Received Message " & $counter) counter.inc sleep(0) # 降低空闲时CPU占用,可根据延迟需求调整时长 createThread(result, serverLoop, channels) viewable App: hub: ChannelHub backendMsg: string = "" isRunning: bool = true # 控制消息检查循环 # 空闲回调:在UI线程中检查服务器消息 proc checkForServerMessages(app: AppState): bool = let msg = app.hub.readServerMsg() if msg.isSome(): app.backendMsg = msg.get() app.update() # 触发UI更新 return app.isRunning # 返回true继续循环,false停止 method init(app: var AppState) = # 注册GTK空闲回调,确保在UI线程执行 discard g_idle_add(proc(): bool = checkForServerMessages(app) , nil) method view(app: AppState): Widget = result = gui: Window: defaultSize = (500, 150) title = "Client Server Example" # 窗口关闭时停止检查循环 proc deleteEvent(): bool = app.isRunning = false return false Box: orient = OrientY margin = 12 spacing = 6 Button {.hAlign: AlignCenter, vAlign: AlignCenter.}: Label(text = "Click me") proc clicked() = discard app.hub.sendToServer("Frontend message!") Label(text = "Message sent by Backend: ") Label(text = app.backendMsg) proc setupClient(channels: ChannelHub): Thread[ChannelHub] = proc startOwlkettle(hub: ChannelHub) = adw.brew(gui(App(hub = hub))) createThread(result, startOwlkettle, channels) proc main() = var serverToClientChannel: Channel[string] var clientToServerChannel: Channel[string] serverToClientChannel.open() clientToServerChannel.open() let hub = ChannelHub(serverChannel: serverToClientChannel, clientChannel: clientToServerChannel) let client = setupClient(hub) let server = setupServer(hub) joinThreads(server, client) main()
关键说明:
g_idle_add会将指定的过程加入GTK的空闲任务队列,在UI线程空闲时自动执行,确保所有UI操作都在主线程完成(GTK要求UI操作必须在主线程执行)app.update()会触发Owlkettle的界面重绘,立即更新显示内容- 窗口关闭时设置
isRunning = false,让空闲回调停止循环,避免资源泄漏
内容的提问来源于stack exchange,提问作者Philipp Doerner
相关产品推荐
相关产品推荐

