gtk-rs:如何从另一个线程更新视图
gtk-rs: how to update view from another thread
我正在使用 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: >k::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(
循环?
无论我采用什么解决方案,都必须具有非常高的性能。我将发送比示例中更多的数据,并且我希望屏幕刷新延迟非常低。
感谢您的帮助!
好的,我解决了问题。对于未来的任何人,这里是解决方案。
glib::idle_add(|| {})
让你 运行 从 UI 线程上的另一个线程闭包(thansk @Zan Lynx)。这足以解决线程安全问题,但还不足以绕过借用检查器。没有 GTKObject
在线程之间发送是安全的,所以另一个线程永远不能持有对它的引用,即使它永远不会使用它。所以需要在UI线程上全局存储UI引用,并建立线程间的通信通道。这是我一步一步做的:
- 创建一种在不涉及传递闭包的线程之间发送数据的方法。我现在使用
std::sync::mpsc
,但从长远来看,另一个选择可能更好。
- 创建一些线程本地全局存储。在启动第二个线程之前,将 UI 引用和该通信管道的接收端全局存储在主线程上。
- 通过闭包将通道的发送端传递给第二个线程。通过该发件人传递您想要的数据。
- 传递数据后,使用
glib::idle_add()
-- 不是闭包而是静态函数 -- 告诉 UI 线程检查通道中的新消息。
- 在 UI 线程的那个静态函数中,访问全局 UI 和接收器变量并更新 UI.
感谢 this thread 帮我解决了这个问题。这是我的代码:
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: >k::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);
}
});
}
我正在使用 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: >k::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(
循环?
无论我采用什么解决方案,都必须具有非常高的性能。我将发送比示例中更多的数据,并且我希望屏幕刷新延迟非常低。
感谢您的帮助!
好的,我解决了问题。对于未来的任何人,这里是解决方案。
glib::idle_add(|| {})
让你 运行 从 UI 线程上的另一个线程闭包(thansk @Zan Lynx)。这足以解决线程安全问题,但还不足以绕过借用检查器。没有 GTKObject
在线程之间发送是安全的,所以另一个线程永远不能持有对它的引用,即使它永远不会使用它。所以需要在UI线程上全局存储UI引用,并建立线程间的通信通道。这是我一步一步做的:
- 创建一种在不涉及传递闭包的线程之间发送数据的方法。我现在使用
std::sync::mpsc
,但从长远来看,另一个选择可能更好。 - 创建一些线程本地全局存储。在启动第二个线程之前,将 UI 引用和该通信管道的接收端全局存储在主线程上。
- 通过闭包将通道的发送端传递给第二个线程。通过该发件人传递您想要的数据。
- 传递数据后,使用
glib::idle_add()
-- 不是闭包而是静态函数 -- 告诉 UI 线程检查通道中的新消息。 - 在 UI 线程的那个静态函数中,访问全局 UI 和接收器变量并更新 UI.
感谢 this thread 帮我解决了这个问题。这是我的代码:
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: >k::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);
}
});
}