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

Geo*_*nch 9 gtk idiomatic callback rust rust-gnome

我目前正在学习Rust,并希望用它来开发基于GUI的GTK +应用程序.我的问题涉及注册回调以响应GTK事件/信号并在这些回调中改变状态.我有一个工作但不优雅的解决方案,所以我想问一下是否有更清洁,更惯用的解决方案.

我已经将我的代码实现为具有方法实现的结构,其中结构维护对GTK小部件的引用以及它所需的其他状态.它构造一个传递给GtkWidget::connect*函数的闭包, 以便接收事件,绘制到画布等.这可能会导致借用检查器出现问题,我现在将解释.我有一些工作但(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();
}
Run Code Online (Sandbox Code Playgroud)

上面的编译无法编译,因为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 = 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();
}
Run Code Online (Sandbox Code Playgroud)

上面的解决方案编译但不是特别漂亮:

  • 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
Run Code Online (Sandbox Code Playgroud)

这是因为调用window.show_all()导致GTK初始化窗口小部件层次结构,导致绘图区域窗口小部件接收size-allocate事件.访问要调用的窗口 show_all()需要Rc<RefCell<...>>打开(因此 wrapped_instance.borrow().window.show_all();)并借用实例.在借用结束show_all()返回之前,GTK调用绘图区域的size-allocate事件处理程序,这会导致与它连接的闭包(上面4行)被调用.闭包试图借用对RenderingAPITestWindowinstance(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 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();
}
Run Code Online (Sandbox Code Playgroud)

虽然上面的代码按要求运行,但我想找到一个更好的方法来继续前进; 我想问一下是否有人知道一个更好的方法,因为上面的编程过程相当复杂,需要使用Rc<RefCell<...>>和拆分结构来满足Rust的借用规则.

Bur*_*hi5 11

这是我提出的一个工作版本:

#![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();
}
Run Code Online (Sandbox Code Playgroud)

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

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