HOWTO:使用 gtk (rust-gnome) 回调的惯用 Rust

Posted

技术标签:

【中文标题】HOWTO:使用 gtk (rust-gnome) 回调的惯用 Rust【英文标题】:HOWTO: Idiomatic Rust for callbacks with gtk (rust-gnome) 【发布时间】:2015-11-05 03:19:03 【问题描述】:

我目前正在学习 Rust 并希望将其用于开发 GUI 基于 GTK+ 的应用程序。我的问题与注册回调有关 在这些回调中响应 GTK 事件/信号和变异状态。 我有一个可行但不优雅的解决方案,所以我想问一下是否有 是一种更简洁、更惯用的解决方案。

我已将我的代码实现为带有方法实现的结构,其中 该结构维护对 GTK 小部件的引用以及其他状态 它需要。它构造一个传递给 GtkWidget::connect*函数为了接收事件,绘制到一个 画布等。这可能会导致借用检查器出现问题,我现在将 解释。我有一些 工作 但(恕我直言)非理想代码,我会 显示。

初始的、无效的解决方案:

#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]

extern crate gtk;
extern crate cairo;

use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::Context, RectangleInt;


struct RenderingAPITestWindow 
    window: gtk::Window,
    drawing_area: gtk::DrawingArea,
    width: i32,
    height: i32


impl RenderingAPITestWindow 
    fn new(width: i32, height: i32) -> RenderingAPITestWindow 
        let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
        let drawing_area = gtk::DrawingArea::new().unwrap();
        drawing_area.set_size_request(width, height);
        window.set_title("Cairo API test");
        window.add(&drawing_area);

        let instance = RenderingAPITestWindowwindow: window,
            drawing_area: drawing_area,
            width: width,
            height: height,
        ;

        instance.drawing_area.connect_draw(|widget, cairo_context| 
            instance.on_draw(cairo_context);
            instance.drawing_area.queue_draw();
            Inhibit(true)
        );

        instance.drawing_area.connect_size_allocate(|widget, rect| 
            instance.on_size_allocate(rect);
        );

        instance.window.show_all();

        return instance;
    

    fn exit_on_close(&self) 
        self.window.connect_delete_event(|_, _| 
            gtk::main_quit();
            Inhibit(true)
        );
    


    fn on_draw(&mut self, cairo_ctx: Context) 
        cairo_ctx.save();
        cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
        cairo_ctx.set_font_size(18.0);
        cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
        cairo_ctx.restore();
    

    fn on_size_allocate(&mut self, rect: &RectangleInt) 
        self.width = rect.width as i32;
        self.height = rect.height as i32;
    



fn main() 
    gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
    println!("Major: , Minor: ", gtk::get_major_version(), gtk::get_minor_version());

    let window = RenderingAPITestWindow::new(800, 500);
    window.exit_on_close();
    gtk::main();

上面的代码无法编译为闭包 RenderingAPITestWindow::new 创建并传递给调用 GtkWidget::connect* 方法尝试借用 instance。这 编译器声明闭包可能比其中的函数寿命更长 它们被声明并且instance 归外部函数所有, 因此问题。鉴于 GTK 可能会保留对这些闭包的引用 在不确定的时间内,我们需要一种方法 生命周期可以在运行时确定,因此我的下一个问题是 RenderingAPITestWindow 实例包含在其中 Rc<RefCell<...>>.

包装 RenderingAPITestWindow 实例编译但在运行时终止:

#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]

extern crate gtk;
extern crate cairo;

use std::rc::Rc;
use std::cell::RefCell;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::Context, RectangleInt;


struct RenderingAPITestWindow 
    window: gtk::Window,
    drawing_area: gtk::DrawingArea,
    width: i32,
    height: i32


impl RenderingAPITestWindow 
    fn new(width: i32, height: i32) -> Rc<RefCell<RenderingAPITestWindow>> 
        let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
        let drawing_area = gtk::DrawingArea::new().unwrap();
        drawing_area.set_size_request(width, height);
        window.set_title("Cairo API test");
        window.add(&drawing_area);

        let instance = RenderingAPITestWindowwindow: window,
            drawing_area: drawing_area,
            width: width,
            height: height,
        ;
        let wrapped_instance = Rc::new(RefCell::new(instance));

        let wrapped_instance_for_draw = wrapped_instance.clone();
        wrapped_instance.borrow().drawing_area.connect_draw(move |widget, cairo_context| 
            wrapped_instance_for_draw.borrow_mut().on_draw(cairo_context);

            wrapped_instance_for_draw.borrow().drawing_area.queue_draw();
            Inhibit(true)
        );

        let wrapped_instance_for_sizealloc = wrapped_instance.clone();
        wrapped_instance.borrow().drawing_area.connect_size_allocate(move |widget, rect| 
            wrapped_instance_for_sizealloc.borrow_mut().on_size_allocate(rect);
        );

        wrapped_instance.borrow().window.show_all();

        return wrapped_instance;
    

    fn exit_on_close(&self) 
        self.window.connect_delete_event(|_, _| 
            gtk::main_quit();
            Inhibit(true)
        );
    


    fn on_draw(&mut self, cairo_ctx: Context) 
        cairo_ctx.save();
        cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
        cairo_ctx.set_font_size(18.0);
        cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
        cairo_ctx.restore();
    

    fn on_size_allocate(&mut self, rect: &RectangleInt) 
        self.width = rect.width as i32;
        self.height = rect.height as i32;
    



fn main() 
    gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
    println!("Major: , Minor: ", gtk::get_major_version(), gtk::get_minor_version());

    let wrapped_window = RenderingAPITestWindow::new(800, 500);
    wrapped_window.borrow().exit_on_close();
    gtk::main();

上述解决方案可以编译,但不是特别漂亮:

RenderingAPITestWindow::new 返回一个 Rc&lt;RefCell&lt;RenderingAPITestWindow&gt;&gt; 而不是 RenderingAPITestWindow 访问RenderingAPITestWindow的字段和方法很复杂 由于Rc&lt;RefCell&lt;...&gt;&gt; 必须打开;现在需要 wrapped_instance.borrow().some_method(...) 而不仅仅是 instance.some_method(...) 每个闭包都需要它自己的wrapped_instance 的克隆;尝试 使用wrapped_instance 会尝试借用一个对象—— 这次是包装器而不是RenderingAPITestWindow——也就是说 RenderingAPITestWindow::new 和以前一样拥有

虽然上述编译,但它在运行时死亡:

thread '<main>' panicked at 'RefCell<T> already borrowed', ../src/libcore/cell.rs:442
An unknown error occurred

这是由于对window.show_all() 的调用导致 GTK 初始化小部件层次结构,生成绘图区小部件 接收size-allocate 事件。访问窗口调用 show_all() 要求打开 Rc&lt;RefCell&lt;...&gt;&gt;(因此 wrapped_instance.borrow().window.show_all();) 和实例 借来的。在show_all() 返回时借用结束之前,GTK 调用 绘图区的size-allocate 事件处理程序,导致关闭 连接到它(上面的 4 行)被调用。关闭试图 借用对RenderingAPITestWindow 实例的可变引用 (wrapped_instance_for_sizealloc.borrow_mut().on_size_allocate(rect);) 为了调用on_size_allocate 方法。这试图借用一个 可变引用,而第一个不可变引用仍在范围内。 第二次借用会导致运行时恐慌。

工作但是 - 恕我直言 - 我设法得到的不优雅的解决方案 到目前为止的工作是将RenderingAPITestWindow 拆分为两个结构,其中 由回调修改的可变状态移动到 单独的结构。

拆分 RenderingAPITestWindow 结构的可行但不优雅的解决方案:

#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]

extern crate gtk;
extern crate cairo;

use std::rc::Rc;
use std::cell::RefCell;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::Context, RectangleInt;


struct RenderingAPITestWindowState 
    width: i32,
    height: i32


impl RenderingAPITestWindowState 
    fn new(width: i32, height: i32) -> RenderingAPITestWindowState 
        return RenderingAPITestWindowStatewidth: width, height: height;
    

    fn on_draw(&mut self, cairo_ctx: Context) 
        cairo_ctx.save();
        cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
        cairo_ctx.set_font_size(18.0);
        cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
        cairo_ctx.restore();
    

    fn on_size_allocate(&mut self, rect: &RectangleInt) 
        self.width = rect.width as i32;
        self.height = rect.height as i32;
    



struct RenderingAPITestWindow 
    window: gtk::Window,
    drawing_area: gtk::DrawingArea,
    state: Rc<RefCell<RenderingAPITestWindowState>>


impl RenderingAPITestWindow 
    fn new(width: i32, height: i32) -> Rc<RefCell<RenderingAPITestWindow>> 
        let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
        let drawing_area = gtk::DrawingArea::new().unwrap();
        drawing_area.set_size_request(width, height);
        window.set_title("Cairo API test");
        window.add(&drawing_area);

        let wrapped_state = Rc::new(RefCell::new(RenderingAPITestWindowState::new(width, height)))
        ;

        let instance = RenderingAPITestWindowwindow: window,
            drawing_area: drawing_area,
            state: wrapped_state.clone()
        ;
        let wrapped_instance = Rc::new(RefCell::new(instance));

        let wrapped_state_for_draw = wrapped_state.clone();
        let wrapped_instance_for_draw = wrapped_instance.clone();
        wrapped_instance.borrow().drawing_area.connect_draw(move |widget, cairo_context| 
            wrapped_state_for_draw.borrow_mut().on_draw(cairo_context);

            wrapped_instance_for_draw.borrow().drawing_area.queue_draw();
            Inhibit(true)
        );

        let wrapped_state_for_sizealloc = wrapped_state.clone();
        wrapped_instance.borrow().drawing_area.connect_size_allocate(move |widget, rect| 
            wrapped_state_for_sizealloc.borrow_mut().on_size_allocate(rect);
        );

        wrapped_instance.borrow().window.show_all();

        return wrapped_instance;
    

    fn exit_on_close(&self) 
        self.window.connect_delete_event(|_, _| 
            gtk::main_quit();
            Inhibit(true)
        );
    



fn main() 
    gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
    println!("Major: , Minor: ", gtk::get_major_version(), gtk::get_minor_version());

    let wrapped_window = RenderingAPITestWindow::new(800, 500);
    wrapped_window.borrow().exit_on_close();
    gtk::main();

虽然上面的代码可以按要求工作,但我想找到一个更好的方法 为前进;我想问是否有人知道更好的方法 以上使编程过程相当复杂,需要 使用 Rc&lt;RefCell&lt;...&gt;&gt; 并拆分结构以满足 Rust 的借用规则。

【问题讨论】:

由于自动取消引用,我很确定(*val.borrow())(*val.borrow_mut()) 的所有实例都可以替换为val.borrow()val.borrow_mut() “鉴于 GTK 可能会在未指定的时间内保留对这些闭包的引用” --- 对我来说,这是您需要使用的提示 @987654361 @如果多个闭包需要访问内存中的相同位置。 感谢您的提示; (*val.borrow()) 的出现已被 val.borrow() 替换。这让事情变得更漂亮了! :) 【参考方案1】:

这是我想出的工作版本:

#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]

extern crate gtk;
extern crate cairo;

use std::rc::Rc;
use std::cell::RefCell;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::Context, RectangleInt;


struct RenderingAPITestWindow 
    window: gtk::Window,
    drawing_area: gtk::DrawingArea,
    state: RefCell<RenderingState>,


struct RenderingState 
    width: i32,
    height: i32,


impl RenderingAPITestWindow 
    fn new(width: i32, height: i32) -> Rc<RenderingAPITestWindow> 
        let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
        let drawing_area = gtk::DrawingArea::new().unwrap();
        drawing_area.set_size_request(width, height);
        window.set_title("Cairo API test");
        window.add(&drawing_area);

        let instance = Rc::new(RenderingAPITestWindow 
            window: window,
            drawing_area: drawing_area,
            state: RefCell::new(RenderingState 
                width: width,
                height: height,
            ),
        );

        
            let instance2 = instance.clone();
            instance.drawing_area.connect_draw(move |widget, cairo_context| 
                instance2.state.borrow().on_draw(cairo_context);
                instance2.drawing_area.queue_draw();
                Inhibit(true)
            );
        
        
            let instance2 = instance.clone();
            instance.drawing_area.connect_size_allocate(move |widget, rect| 
                instance2.state.borrow_mut().on_size_allocate(rect);
            );
        
        instance.window.show_all();
        instance
    

    fn exit_on_close(&self) 
        self.window.connect_delete_event(|_, _| 
            gtk::main_quit();
            Inhibit(true)
        );
    


impl RenderingState 
    fn on_draw(&self, cairo_ctx: Context) 
        cairo_ctx.save();
        cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
        cairo_ctx.set_font_size(18.0);
        cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
        cairo_ctx.restore();
    

    fn on_size_allocate(&mut self, rect: &RectangleInt) 
        self.width = rect.width as i32;
        self.height = rect.height as i32;
    


fn main() 
    gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
    println!("Major: , Minor: ", gtk::get_major_version(), gtk::get_minor_version());

    let window = RenderingAPITestWindow::new(800, 500);
    window.exit_on_close();
    gtk::main();

我通过一些观察得出了这个结论:

实例在多个闭包之间共享不确定的时间。 Rc 是该场景的正确答案,因为它提供共享所有权。 Rc 使用起来非常符合人体工程学;它与任何其他指针类型一样工作。 instance 中唯一实际发生突变的部分是您的状态。由于您的实例是共享的,因此不能使用标准的&amp;mut 指针可变地借用它。因此,您必须使用内部可变性。这就是RefCell 提供的。但请注意,您只需要在您正在变异的状态上使用RefCell。所以这仍然将状态分离到一个单独的结构中,但它在 IMO 中运行良好。 对此代码的一个可能修改是将#[derive(Clone, Copy)] 添加到RenderingState 结构的定义中。由于它可以是Copy(因为它的所有组件类型都是Copy),你可以使用Cell而不是RefCell

【讨论】:

谢谢!这显着改善了问题! :) @GeoffFrench 看起来您是 *** 的新手,但如果您认为我已经充分回答了您的问题,那么您应该将其标记为已接受。 :-) 谢谢!

以上是关于HOWTO:使用 gtk (rust-gnome) 回调的惯用 Rust的主要内容,如果未能解决你的问题,请参考以下文章

Python 和 Gtk - 正在使用哪个 GTK 版本?

使用gtk +播放视频

在Pygobject GTK3中使用Gtk.GLArea

GTK+ 3.0:如何将 Gtk.TreeStore 与自定义模型项一起使用?

Windows下安装并使用 GTK4

Windows下安装并使用 GTK4