You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让线程通道消息触发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状态并触发界面重绘。具体修改如下:

  1. 给App添加isRunning字段,用于控制消息检查循环的启停
  2. 实现checkForServerMessages过程,作为GTK的空闲回调函数,在UI线程中读取服务器消息、更新状态并触发UI更新
  3. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 19:44:59