首页 > 解决方案 > 让 GtkComboBox 条目为 Pixbuf 或 String

问题描述

我正在尝试实现一个下拉菜单,其中每个条目都是文本或图像。看起来,底层模型中的每一列都有一种且只有一种类型。是否仍然可以允许条目为文本或图像?我的想法是使用两列,其中一列是空的和匹配的渲染器。我只是不知道怎么做。

这个问题的答案显示了如何将两个渲染器添加到一个列中以用于TreeStore不同的上下文。当我使用 a 时,ListStore我想我可以用两个渲染器中的一个渲染一些 emtpy 的方式来解决它。但是对于 ComboBox,我遇到了一个问题,即非扩展 ComboBox 的宽度仅取决于第一个渲染器(第二个渲染器的结果流出),并且即使对于空内容,第一个渲染器仍然会创建很多间距。

一些消息来源暗示,实现我自己的渲染器可能是一个好主意,它动态地将渲染器用于 Pixbuf 或文本。我的问题是我将 Rust 与 gtk-rs 一起使用。虽然可以找到一些关于实现您自己的渲染器的示例,但 gtk-rs 文档似乎没有此类文档。由于概念上的差异(由 Traits 等建模的 gtk 继承),将示例转移到 Rust 并不简单,并且对这种语言和 gtk 没有太多经验,我必须承认我不知道从哪里开始处理这种情况。

非常感谢任何有关前进方向的帮助和/或信息!

标签: rustgtkgtk3

解决方案


似乎最好的选择是实现自定义类型以用作模型列以及自定义渲染器。自定义类型包含一个可选的 String 和一个可选的 Pixbuf,这两个参数之一应该设置为某个值。我没有使用枚举来确保与 glib 类型的兼容性(也许这种方式也可以,但我不确定)。自定义渲染器包含用于字符串和 Pixbuf 的渲染器,并且可以根据给定的自定义类型动态选择要使用的渲染器。

基本上,除了自定义类型的存储之外,唯一要添加的功能是提供渲染单元格和提供条目首选大小的功能。在这些情况下,自定义类型的内容用于决定应该从哪个已经给定的渲染器复制有关渲染和大小请求功能的行为。

实现自定义类型和渲染器有点挑战性,因为 gtk-rs 的文档并不是最好的,尤其是在使用渲染器的子类方面。我的代码基于gtk-rs 示例 repo中的子类示例和自定义模型。由于我缺乏 Rust 和 gtk 方面的经验,可能有很多部分可以更优雅和一致地解决,但我想分享我的解决方案,以防遇到类似问题的人正在寻找关于如何进行的方向:

#[macro_use]
extern crate glib;
extern crate gdk_pixbuf;
extern crate gio;
extern crate gtk;

use gio::prelude::*;
use gtk::prelude::*;

use custom_glib_string_or_pixbuf::StringOrPixbuf;
use custom_glib_string_or_pixbuf_renderer::StringOrPixbufRenderer;
use gdk_pixbuf::Pixbuf;

// Content type
mod custom_glib_string_or_pixbuf {
    use gdk_pixbuf::Pixbuf;
    use gio::prelude::*;
    use glib::subclass;
    use glib::subclass::prelude::*;
    use glib::translate::*;

    mod internal {
        use super::*;
        use std::cell::RefCell;

        pub struct StringOrPixbuf {
            string: RefCell<Option<String>>,
            pixbuf: RefCell<Option<Pixbuf>>,
        }

        static PROPERTIES: [subclass::Property; 2] = [
            subclass::Property("string", |name| {
                glib::ParamSpec::string(name, "String", "String", None, glib::ParamFlags::READWRITE)
            }),
            subclass::Property("pixbuf", |name| {
                glib::ParamSpec::object(
                    name,
                    "Pixbuf",
                    "Pixbuf",
                    Pixbuf::static_type(),
                    glib::ParamFlags::READWRITE,
                )
            }),
        ];

        impl ObjectSubclass for StringOrPixbuf {
            const NAME: &'static str = "StringOrPixbuf";
            type ParentType = glib::Object;
            type Instance = subclass::simple::InstanceStruct<Self>;
            type Class = subclass::simple::ClassStruct<Self>;

            glib_object_subclass!();

            fn class_init(class: &mut Self::Class) {
                class.install_properties(&PROPERTIES);
            }

            fn new() -> Self {
                Self {
                    string: RefCell::new(None),
                    pixbuf: RefCell::new(None),
                }
            }
        }

        impl ObjectImpl for StringOrPixbuf {
            glib_object_impl!();

            fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) {
                let prop = &PROPERTIES[id];

                match *prop {
                    subclass::Property("string", ..) => {
                        self.string.replace(value.get().unwrap());
                    }
                    subclass::Property("pixbuf", ..) => {
                        self.pixbuf.replace(value.get().unwrap());
                    }
                    _ => panic!("Tried to set unknown property of StringOrPixbuf"),
                }
            }

            fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> {
                let prop = &PROPERTIES[id];

                match *prop {
                    subclass::Property("string", ..) => Ok(self.string.borrow().to_value()),
                    subclass::Property("pixbuf", ..) => Ok(self.pixbuf.borrow().to_value()),
                    _ => panic!("Tried to get unknown property of StringOrPixbuf"),
                }
            }
        }
    }

    glib_wrapper! {
        pub struct StringOrPixbuf(Object<subclass::simple::InstanceStruct<internal::StringOrPixbuf>,subclass::simple::ClassStruct<internal::StringOrPixbuf>, StringOrPixbufClass>);

        match fn {
            get_type => || internal::StringOrPixbuf::get_type().to_glib(),
        }
    }

    impl StringOrPixbuf {
        pub fn new(string: Option<String>, pixbuf: Option<Pixbuf>) -> StringOrPixbuf {
            glib::Object::new(
                StringOrPixbuf::static_type(),
                &[("string", &string), ("pixbuf", &pixbuf)],
            )
            .expect("Failed to create StringOrPixbuf instance")
            .downcast()
            .unwrap()
        }

        pub fn is_string(&self) -> bool {
            let string_option = self
                .get_property("string")
                .unwrap()
                .get::<String>()
                .unwrap();
            let pixbuf_option = self
                .get_property("pixbuf")
                .unwrap()
                .get::<Pixbuf>()
                .unwrap();

            if string_option.is_some() == pixbuf_option.is_some() {
                panic!("Illegal StringOrPixbuf-state")
            } else if let Some(_) = string_option {
                true
            } else {
                false
            }
        }
    }
}

// Renderer
mod custom_glib_string_or_pixbuf_renderer {
    use custom_glib_string_or_pixbuf::StringOrPixbuf;
    use gdk_pixbuf::Pixbuf;
    use gio::prelude::*;
    use glib::subclass;
    use glib::subclass::prelude::*;
    use glib::translate::*;
    use gtk::prelude::*;

    mod internal {
        use super::*;
        use std::cell::RefCell;

        pub struct StringOrPixbufRenderer {
            text_renderer: gtk::CellRendererText,
            pixbuf_renderer: gtk::CellRendererPixbuf,
            string_or_pixbuf: RefCell<Option<StringOrPixbuf>>,
        }

        static PROPERTIES: [subclass::Property; 1] =
            [subclass::Property("string_or_pixbuf", |name| {
                glib::ParamSpec::object(
                    name,
                    "string_or_pixbuf",
                    "string_or_pixbuf",
                    StringOrPixbuf::static_type(),
                    glib::ParamFlags::READWRITE,
                )
            })];

        impl ObjectSubclass for StringOrPixbufRenderer {
            const NAME: &'static str = "StringOrPixbufRenderer";
            type ParentType = gtk::CellRenderer;
            type Instance = subclass::simple::InstanceStruct<Self>;
            type Class = subclass::simple::ClassStruct<Self>;
            glib_object_subclass!();
            fn class_init(class: &mut Self::Class) {
                class.install_properties(&PROPERTIES);
            }
            fn new() -> Self {
                Self {
                    text_renderer: gtk::CellRendererText::new(),
                    pixbuf_renderer: gtk::CellRendererPixbuf::new(),
                    string_or_pixbuf: RefCell::new(None),
                }
            }
        }

        impl ObjectImpl for StringOrPixbufRenderer {
            glib_object_impl!();

            fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) {
                let prop = &PROPERTIES[id];
                match *prop {
                    subclass::Property("string_or_pixbuf", ..) => {
                        self.string_or_pixbuf.replace(value.get().unwrap());
                    }
                    _ => panic!("Tried to set unknown property of StringOrPixbufRenderer"),
                }
            }

            fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> {
                let prop = &PROPERTIES[id];
                match *prop {
                    subclass::Property("string_or_pixbuf", ..) => {
                        Ok(self.string_or_pixbuf.borrow().to_value())
                    }
                    _ => panic!("Tried to get unknown property of StringOrPixbufRenderer"),
                }
            }
        }

        impl gtk::subclass::cell_renderer::CellRendererImpl for StringOrPixbufRenderer {
            fn render<P: IsA<gtk::Widget>>(
                &self,
                _renderer: &gtk::CellRenderer,
                cr: &cairo::Context,
                widget: &P,
                background_area: &gdk::Rectangle,
                cell_area: &gdk::Rectangle,
                flags: gtk::CellRendererState,
            ) {
                self.update_renderers();
                if self.is_content_string() {
                    self.text_renderer
                        .render(cr, widget, background_area, cell_area, flags);
                } else {
                    self.pixbuf_renderer
                        .render(cr, widget, background_area, cell_area, flags);
                }
            }

            fn get_preferred_width<P: IsA<gtk::Widget>>(
                &self,
                _renderer: &gtk::CellRenderer,
                widget: &P,
            ) -> (i32, i32) {
                self.update_renderers();
                if self.is_content_string() {
                    self.text_renderer.get_preferred_width(widget)
                } else {
                    self.pixbuf_renderer.get_preferred_width(widget)
                }
            }

            fn get_preferred_height<P: IsA<gtk::Widget>>(
                &self,
                _renderer: &gtk::CellRenderer,
                widget: &P,
            ) -> (i32, i32) {
                self.update_renderers();
                if self.is_content_string() {
                    self.text_renderer.get_preferred_height(widget)
                } else {
                    self.pixbuf_renderer.get_preferred_height(widget)
                }
            }
        }

        impl StringOrPixbufRenderer {
            fn is_content_string(&self) -> bool {
                self.string_or_pixbuf
                    .borrow()
                    .as_ref()
                    .expect("No StringOrPixbuf known to StringOrPixbufRenderer")
                    .is_string()
            }

            fn update_renderers(&self) {
                if self.is_content_string() {
                    self.text_renderer.set_property_text(Some(
                        &self
                            .string_or_pixbuf
                            .borrow()
                            .as_ref()
                            .unwrap()
                            .get_property("string")
                            .unwrap()
                            .get::<String>()
                            .unwrap()
                            .unwrap()[..],
                    ));
                } else {
                    self.pixbuf_renderer.set_property_pixbuf(Some(
                        &self
                            .string_or_pixbuf
                            .borrow()
                            .as_ref()
                            .unwrap()
                            .get_property("pixbuf")
                            .unwrap()
                            .get::<Pixbuf>()
                            .unwrap()
                            .unwrap(),
                    ));
                }
            }
        }
    }

    glib_wrapper! {
        pub struct StringOrPixbufRenderer(
            Object<subclass::simple::InstanceStruct<internal::StringOrPixbufRenderer>,
            subclass::simple::ClassStruct<internal::StringOrPixbufRenderer>,
            SimpleAppWindowClass>)
            @extends gtk::CellRenderer;
        match fn {
            get_type => || internal::StringOrPixbufRenderer::get_type().to_glib(),
        }
    }

    impl StringOrPixbufRenderer {
        pub fn new() -> StringOrPixbufRenderer {
            glib::Object::new(StringOrPixbufRenderer::static_type(), &[])
                .expect("Failed to create StringOrPixbufRenderer instance")
                .downcast::<StringOrPixbufRenderer>()
                .unwrap()
        }
    }
}

fn main() {
    let application =
        gtk::Application::new(None, Default::default())
            .expect("failed to initialize GTK application");

    application.connect_activate(|app| {
        let window = gtk::ApplicationWindow::new(app);
        window.set_title("Mixed Pixbuf and String ComboBox Demo");
        window.set_default_size(350, 70);

        let model = gtk::ListStore::new(&[StringOrPixbuf::static_type()]);
        let combo = gtk::ComboBox::with_model(&model);

        let row = model.append();
        let image = Pixbuf::from_file("image.png")
            .unwrap()
            .scale_simple(100, 70, gdk_pixbuf::InterpType::Bilinear)
            .unwrap();
        model.set(&row, &[0], &[&StringOrPixbuf::new(None, Some(image))]);
        combo.set_active(Some(0));

        let row = model.append();
        model.set(
            &row,
            &[0],
            &[&StringOrPixbuf::new(
                Some(String::from("Hello World")),
                None,
            )],
        );

        let row = model.append();
        let image = Pixbuf::from_file("another_image.png")
            .unwrap()
            .scale_simple(100, 70, gdk_pixbuf::InterpType::Bilinear)
            .unwrap();
        model.set(&row, &[0], &[&StringOrPixbuf::new(None, Some(image))]);

        let renderer = StringOrPixbufRenderer::new();
        combo.pack_start(&renderer, true);
        combo.add_attribute(&renderer, "string_or_pixbuf", 0);

        window.add(&combo);

        window.show_all();
    });

    application.run(&[]);
}

推荐阅读