12

私は現在 Rust を学んでおり、GTK+ を使用した GUI ベースのアプリケーションの開発に使用したいと考えています。私の問題は、コールバックを登録して 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();
}

上記は、クロージャー が作成され、メソッドRenderingAPITestWindow::newへの呼び出しに渡され て Borrow が試行されるため、コンパイルに失敗します。コンパイラは、クロージャーが宣言され、外部関数によって所有されている関数よりも長く存続する可能性があると述べているため、問題が発生します。GTK が不特定の時間の間、これらのクロージャへの参照を保持する可能性があることを考えると、実行時に有効期間を決定できるアプローチが必要です 。GtkWidget::connect*instanceinstanceRenderingAPITestWindowRc<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::newRc<RefCell<RenderingAPITestWindow>>ではなく を 返します RenderingAPITestWindow
  • のフィールドとメソッドへのアクセスは、を開く必要がRenderingAPITestWindowあるため複雑です。Rc<RefCell<...>>だけで wrapped_instance.borrow().some_method(...)なく、 instance.some_method(...)
  • 各クロージャには、独自のwrapped_instance;のクローンが必要です。使用しようとすると、以前と同じように所有されているオブジェクト (今回wrapped_instanceではなくラッパー) を借用しようとします。RenderingAPITestWindowRenderingAPITestWindow::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()必要がありました。戻り時に借用が終了する前に、GTK は描画領域のイベント ハンドラーを呼び出します。これにより、それに接続されているクロージャー (上記の 4 行) が呼び出されます。クロージャーは、メソッドを呼び出すために、インスタンス ( )への変更可能な参照を借用しようとします。これは、最初の不変参照がまだスコープ内にある間に、可変参照を借用しようとします。この 2 回目の借用により、ランタイム パニックが発生します。Rc<RefCell<...>>wrapped_instance.borrow().window.show_all();show_all()size-allocateRenderingAPITestWindowwrapped_instance_for_sizealloc.borrow_mut().on_size_allocate(rect);on_size_allocate

私がこれまでに何とか機能させてきた、機能しているが-IMHO-洗練されていない解決策は、RenderingAPITestWindow2つの構造体に分割し、コールバックによって変更される可変状態を別の構造体に移動することです。

構造体を分割する、機能しているが洗練されていないソリューション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<...>>Rustの借用規則を満たすために構造体を使用および分割する必要があるため、上記はプログラミングプロセスをかなり複雑にするため、より良いアプローチを誰かが知っているかどうかを尋ねたいと思います。

4

1 に答える 1

13

これが私が思いついた実用的なバージョンです:

#![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使用できます。CellRefCell
于 2015-08-12T14:15:36.130 に答える