RenderingAPITestWindow

RenderingAPITestWindow

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

我已经将我的代码实现为具有方法实现的结构,其中
该结构维护对GTK小部件的引用以及其他状态
它需要的。它构造了一个闭包,该闭包将传递给GtkWidget::connect*函数为了接收事件,画一个
Canvas 等。这可能会导致借阅检查器出现问题,就像我现在要说的那样
解释。我有一些有效的但(IMHO)非理想的代码
表演。

最初的无效解决方案:

#![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 = RenderingAPITestWindow{window: 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::newGtkWidget::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 = RenderingAPITestWindow{window: 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<RefCell<RenderingAPITestWindow>>而不是RenderingAPITestWindow
  • 访问RenderingAPITestWindow的字段和方法很复杂
    由于必须打开Rc<RefCell<...>>;现在需要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<RefCell<...>>(因此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方法。这试图借用
    可变引用,而第一个不可变引用仍在范围内。
    这第二次借用导致运行时 panic 。

    我设法获得的可行但恕我直言的优雅解决方案
    到目前为止,工作是将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 RenderingAPITestWindowState{width: 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 = RenderingAPITestWindow{window: 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<RefCell<...>>和split结构来满足Rust的借用规则。

    最佳答案

    这是我想到的一个工作版本:

    #![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唯一真正突变的部分是您的状态。由于您的实例是共享的,因此无法使用标准&mut指针可变地借用它。因此,您必须使用内部可变性。这是RefCell提供的。不过请注意,您只需要在要更改的状态上使用RefCell。因此,这仍然将状态分为一个单独的结构,但在IMO上运行良好。
  • 对此代码的可能修改是将#[derive(Clone, Copy)]添加到RenderingState结构的定义中。由于它可以是Copy(因为它的所有组件类型都是Copy),所以可以使用 Cell 而不是RefCell
  • 关于callback - HOWTO : Idiomatic Rust for callbacks with gtk (rust-gnome),我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/31966497/

    10-11 04:50