GTK-rs:如何从另一个线程更新视图

6
我正在使用 gtk-rs 创建一个 UI 应用程序。在应用程序中,我必须生成一个线程来持续与另一个进程通信。有时,我需要根据该线程中发生的情况更新 UI。但是,我不确定如何做到这一点,因为我无法跨线程持有对 UI 的任何部分的引用。以下是我尝试的代码:
use gtk;

fn main() {
    let application =
        gtk::Application::new(Some("com.github.gtk-rs.examples.basic"), Default::default()).unwrap()

    application.connect_activate(|app| {
        let ui_model = build_ui(app);
        setup(ui_model);
    });

    application.run(&[]);
}

struct UiModel { main_buffer: gtk::TextBuffer }

fn build_ui(application: &gtk::Application) -> UiModel {
    let glade_src = include_str!("test.glade");
    let builder = gtk::Builder::new();
    builder
        .add_from_string(glade_src)
        .expect("Couldn't add from string");

    let window: gtk::ApplicationWindow = builder.get_object("window").unwrap();
    window.set_application(Some(application));
    window.show_all();

    let main_text_view: gtk::TextView = builder.get_object("main_text_view")

    return UiModel {
        main_buffer: main_text_view.get_buffer().unwrap(),
    };
}

fn setup(ui: UiModel) {
    let child_process = Command::new("sh")
        .args(&["-c", "while true; do date; sleep 2; done"])
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let incoming = child_process.stdout.unwrap();

    std::thread::spawn(move || {                              // <- This is the part to pay
        &BufReader::new(incoming).lines().for_each(|line| {   //    attention to.
            ui.main_buffer.set_text(&line.unwrap());          //    I am trying to update the
        });                                                   //    UI text from another thread.
    });
}

但是,我遇到了错误:

    |       std::thread::spawn(move || {
    |  _____^^^^^^^^^^^^^^^^^^_-
    | |     |
    | |     `*mut *mut gtk_sys::_GtkTextBufferPrivate` cannot be sent between threads safely

这很有道理,我可以理解Gtk小部件不是线程安全的。但是那么该如何更新它们?是否有一种安全地向UI线程发送信号的方法?或者是否有一种在同一线程中以不阻塞UI的方式运行.lines().for_each(循环的方法?
无论我采取哪种解决方案,都必须具有非常高的性能。我将发送比示例中更多的数据,并希望刷新屏幕的延迟非常低。
谢谢您的帮助!

我在Rust中没有做过这个,所以给一些一般性的建议:如果你有一个快速处理大量数据的线程,可能很难为GUI创建数据快照。因此,请确保您可以将其分块锁定,以便工作线程不会被频繁阻塞。然后,在GUI中只读取实际屏幕上显示的项目的数据。不要创建所有数据的列表或表视图。创建一个带有滚动条的视图,当滚动时显示模糊效果,然后进行回调以拉取20个可见项目的数据进行显示。这是某个控件选项。 - Zan Lynx
哦,还有这里:http://gtk-rs.org/docs/gtk/#threads - Zan Lynx
要从线程更新UI,您必须使用g_idle_add()函数。详见C文档:https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#g-idle-add - Siva Guru
1个回答

6

好的,我已经解决了问题。对于未来可能有同样问题的人,这是解决方案。

glib::idle_add(|| {}) 让你可以在UI线程中运行来自另一个线程的闭包(感谢@Zan Lynx)。这已经足以解决线程安全问题,但仅此还不足以绕过借用检查器。没有GTKObject是可以安全发送到其他线程的,因此即使其他线程永远不会使用它,它也不能持有对它的引用。因此,您需要在UI线程上全局存储UI引用,并在线程之间设置通信渠道。以下是我逐步执行的操作:

  1. 创建一种不涉及传递闭包的线程间数据传递方式。我现在使用的是 std::sync::mpsc ,但长期来看可能会有更好的选择。
  2. 创建一些线程本地的全局存储。在启动第二个线程之前,在主线程上全局存储您的UI引用和通信管道的接收端。
  3. 通过闭包将通道的发送端传递给第二个线程。通过该发送端传递所需的数据。
  4. 在通过数据后,使用 glib::idle_add() —— 不是闭包而是静态函数 —— 告诉UI线程检查通道中是否有新消息。
  5. 在UI线程上的该静态函数中,访问全局的UI和接收器变量并更新UI。

感谢这个线程帮助我找到答案。以下是我的代码:

extern crate gio;
extern crate gtk;
extern crate pango;

use gio::prelude::*;
use gtk::prelude::*;
use std::cell::RefCell;
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use std::sync::mpsc;

fn main() {
    let application =
        gtk::Application::new(Some("com.github.gtk-rs.examples.basic"), Default::default())
            .unwrap();

    application.connect_activate(|app| {
        let ui_model = build_ui(app);
        setup(ui_model);
    });

    application.run(&[]);
}

struct UiModel {
    main_buffer: gtk::TextBuffer,
}

fn build_ui(application: &gtk::Application) -> UiModel {
    let glade_src = include_str!("test.glade");
    let builder = gtk::Builder::new();
    builder
        .add_from_string(glade_src)
        .expect("Couldn't add from string");

    let window: gtk::ApplicationWindow = builder.get_object("window").unwrap();
    window.set_application(Some(application));
    window.show_all();

    let main_text_view: gtk::TextView = builder.get_object("main_text_view").unwrap();

    return UiModel {
        main_buffer: main_text_view.get_buffer().unwrap(),
    };
}

fn setup(ui: UiModel) {
    let (tx, rx) = mpsc::channel();
    GLOBAL.with(|global| {
        *global.borrow_mut() = Some((ui, rx));
    });
    let child_process = Command::new("sh")
        .args(&["-c", "while true; do date; sleep 2; done"])
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let incoming = child_process.stdout.unwrap();

    std::thread::spawn(move || {
        &BufReader::new(incoming).lines().for_each(|line| {
            let data = line.unwrap();
            // send data through channel
            tx.send(data).unwrap();
            // then tell the UI thread to read from that channel
            glib::source::idle_add(|| {
                check_for_new_message();
                return glib::source::Continue(false);
            });
        });
    });
}

// global variable to store  the ui and an input channel
// on the main thread only
thread_local!(
    static GLOBAL: RefCell<Option<(UiModel, mpsc::Receiver<String>)>> = RefCell::new(None);
);

// function to check if a new message has been passed through the
// global receiver and, if so, add it to the UI.
fn check_for_new_message() {
    GLOBAL.with(|global| {
        if let Some((ui, rx)) = &*global.borrow() {
            let received: String = rx.recv().unwrap();
            ui.main_buffer.set_text(&received);
        }
    });
}

1
供其他人参考,还有glib :: source :: idle_add_once,这可能更适合您特定的用例。 - cyqsimon
1
值得注意的是,idle_add 容易占用你的 CPU,因为它在线程空闲时运行。对于需要定期更新的 GUI 相关内容,使用 glib::source::timeout_add 可能更合适,以避免不必要的高 CPU 使用率。 - cyqsimon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接