微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

GtkFlowBox不能使用水平方向在列中对齐

如何解决GtkFlowBox不能使用水平方向在列中对齐

在GTK中,我试图将一个框内不同宽度的小部件对齐,以使它们在一行中彼此相邻,如果没有剩余空间,则流入下一行。基本上,我试图得到与Qt的Flow Layout类似的结果,如下所示:

Qt Flow Layout Example

(来源:This Qt文档页面

我需要每个小部件都尽可能小(沿宽度方向),甚至允许宽度仅为20个像素。

要在需要更多空间的情况下自动转到下一行,GTK提供了GtkFlowBox。我的问题是,它看起来像GtkFlowBox一样将所有小部件对齐在动态宽度的网格中(按单元排列)。下图显示了即使非常宽的小部件也迫使其正下方的小部件使用完全相同的宽度:

GTK aligning items grid wise

(请注意,每个“ ...”与每个文本序列一样都在其自己的Label中。)

图片中,第二个长标签明显导致最后一个标签获得的空间超出了所需。 GTK文档并没有真正使我清楚地知道GtkFlowBox就像动态网格一样工作,但是互联网上的其他图像每次也都显示出网格方向的对齐方式。

我正在寻找一种方法,如果没有剩余空间,但所有行在其他方面都彼此独立,则小部件会流入下一行。 Qt似乎实现了这一点,因为最后一个按钮将与在网格中对齐的小部件矛盾。 是否可以通过GtkFlowBox或其他现有Layout或仅通过手动实现来实现我正在尝试的方法?我认为最简便的手动方法是使用水平Box并将动态地将小部件映射到不同的盒子,以使任何盒子都不满。但是显然,这将比使用现有的Layout更加不方便和优雅。

(请注意,如果有人为特定环境编写了第三方解决方案:我正在使用Rust(Gtk-rs))

解决方法

我的问题的解决方案是实现我自己的容器。我想尝试记录如何解决这个问题,并将在此处提供我的代码。

主要概念是对容器进行子类化。 This guide 使用 C 提供了一个很好的概述。如果您了解自己在语言绑定中的子类化,则可以轻松地将主要思想转移到其他语言。有两个主要问题需要解决。一是容器所需的尺寸计算。另一个是子元素在容器中的实际对齐方式。

关于大小计算,请求模式应该设置为 HeightForWidth,因为宽度会影响有多少子元素可以放入行中,因此也会影响需要多少行。这里的一个问题是,虽然较大的宽度意味着较小的高度,反之亦然,gtk 使用最小宽度来计算最小高度。此问题记录在 this post 中。我通过添加一个属性来解决它,该属性指示至少应该在一行中放置多少个孩子,以便可以近似实际宽度,从而产生可接受的高度要求。我的 get_preferred_width 假设在每行中放置最小数量的孩子来计算最小宽度。更自然的方法是将其设置为最大孩子的宽度,但这会导致更大的最小高度。 get_preferred_height_for_width 直接取决于它使用的分配算法,而没有真正为其子项分配大小。

我在实现关于最小和自然尺寸的尺寸分配时遇到了一些问题。以标准方式在孩子之间均匀使用最小高度以达到自然大小时,无法使用剩余空间,因为孩子可能随时流动到其他行,并且分布应在全球范围内公平,而不仅仅是每行。我为每个孩子分配最小宽度并添加剩余宽度的比率以达到所有孩子都相同的自然宽度。由于回流,我发现没有很好的算法来做到这一点,这不是简单的反复试验。因此,我只是尝试可用高度是否足以满足不同的比率,我将这些比率测试到足够低的分辨率。这产生了质量和效率之间的权衡。我使用二分搜索来提高效率。

如果您有任何具体问题,请随时提出。作为代码示例,我想提供 a third party solution in C 在我的其他帖子中建议的用户 James Westman。我想提供它,因为我自己的代码是在 Rust 中的。因此,这对于这些想法可能很有用,而例如,子类化与在 C 中使用 gtk 进行子类化有很多不同,这使得仅在其他语言中使用此代码就变得非常重要。我的示例还包含一个虚拟容器,允许测试容器子项的不同自然大小和最小大小。它作为独立工作。另请注意,某些功能未包含在 gtk-rs 的最新稳定版本中,请使用最新版本,例如

[dependencies.gtk]
git = "https://github.com/gtk-rs/gtk"

(另一个答案中的代码,因为答案大小有限,我目前无法共享 git repo)

,

首先阅读另一个答案中的描述。由于答案长度有限,此代码被排除在外;代码和文本不能合二为一。

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

use gio::prelude::*;
use glib::subclass;
use glib::subclass::prelude::*;
use glib::translate::*;
use gtk::prelude::*;

mod dummy {
    use super::*;
    glib_wrapper! {
        pub struct GAspectFillFrame(
            Object<subclass::simple::InstanceStruct<internal::GAspectFillFrame>,subclass::simple::ClassStruct<internal::GAspectFillFrame>,SimpleAppWindowClass>)
            @extends gtk::Bin,gtk::Container,gtk::Widget;

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

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

    mod internal {
        use super::*;
        use gtk::subclass::{bin::BinImpl,container::ContainerImpl,widget::WidgetImpl};
        use gtk::SizeRequestMode;

        pub struct GAspectFillFrame {}

        static PROPERTIES: [subclass::Property; 0] = [];

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

            glib_object_subclass!();

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

            fn new() -> Self {
                Self {}
            }
        }

        impl ObjectImpl for GAspectFillFrame {
            fn set_property(&self,_obj: &glib::Object,_id: usize,_value: &glib::Value) {
                panic!();
            }

            fn get_property(&self,_id: usize) -> Result<glib::Value,()> {
                panic!();
            }
        }

        impl WidgetImpl for GAspectFillFrame {
            fn get_preferred_width(&self,_widget: &gtk::Widget) -> (i32,i32) {
                (50,100)
            }

            fn get_preferred_height(&self,100)
            }

            fn get_request_mode(&self,_widget: &gtk::Widget) -> gtk::SizeRequestMode {
                SizeRequestMode::ConstantSize
            }
        }

        impl ContainerImpl for GAspectFillFrame {}

        impl BinImpl for GAspectFillFrame {}

        impl GAspectFillFrame {}
    }
}

glib_wrapper! {
    /// A container displaying its children rowwise and dynamically reflowing children to the next row
    /// if there is no space left. In contrast to the `gtk::FlowBox` this container will not act like a grid and instead will
    /// display its rows independent of each other besides of the assignment of the children to the rows.
    ///
    /// The container allows to set the horizontal spacing between children and the vertical spacing between rows.
    ///
    /// While the height of the container is queried for given widths as an increase in the latter decreases the height,/// gtk uses the minimal width to calculate the minimal height. This could result in unexpected large heights.
    /// This behaviour can be controlled by influencing the minimal width such that it is reasonably high.
    /// To do so,set the `min-row-children` property. Using it to require a row to be able to contain a minimum of children which is
    /// higher than the default value `1` leads to higher minimum widths and therefore also smaller minimum heights.
    /// Note that the property is used for min width calculations but not actually enforced in size allocation.
    /// It is a soft limit to allow an easier front to back calculation for the allocation.
    /// Requiring a minimal number of children per line would imply new problems,as the minimum width calculation places a constant number
    /// of elements in one line while the real allocation does more calculations allowing a non constant number.
    /// Setting the min number of children as a hard limit could lead to situations where a for the min width calculations e.g. 4 children
    /// are in each row while for the real allocation one row may have some remaining place for one more child than necessary.
    /// In this case,the child would be in this row,the next row requires another child to reach the hard limit which could then overflow
    /// the row if it had a large size.
    /// Note that while the property is not enforced in final allocation,the min space requirements always are sufficient to place
    /// all children with their min width or larger.
    ///
    /// The container expands h-expand children with respect to the remaining space in their row which is evenly
    /// distributed among all expanding children of that row.
    ///
    /// In the case where the natural size requirements of the children cannot be satisfied,the container tries
    /// to set their size such that they get their minimal size and an additional fraction/ratio of the difference between
    /// their minimal and natural size such that this fraction is constant among all children.
    /// Note that this operation is non-trivial as changes in this ratio may lead to children being shifted into
    /// different rows and as rows may have some remaining space if others are full.
    /// The implemented solution implements a tradeoff between a visually nice rescaling behaviour and efficiency.
    pub struct ProperFlowBox(
        Object<subclass::simple::InstanceStruct<internal::ProperFlowBox>,subclass::simple::ClassStruct<internal::ProperFlowBox>,SimpleAppWindowClass>)
        @extends gtk::Container,gtk::Widget;

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

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

    /// Sets the spacing between two children in a line and between children and the container's borders
    ///
    /// # Arguments
    ///
    /// * `spacing` - Horizontal spacing in pixels
    pub fn set_h_spacing(&self,spacing: i32) {
        self.set_property("h-spacing",&spacing)
            .expect("Error setting h-spacing of ProperFlowBox");
    }

    /// Sets the spacing between two rows and between children and the container's borders
    ///
    /// # Arguments
    ///
    /// * `spacing` - Vertical spacing in pixels
    pub fn set_v_spacing(&self,spacing: i32) {
        self.set_property("v-spacing",&spacing)
            .expect("Error setting v-spacing of ProperFlowBox");
    }

    /// Sets the minimum number of children used to approximate the required width
    ///
    /// The set property is not enforced by the size allocation.
    /// Setting this to higher values may result in less required minimum height of the container.
    ///
    /// # Arguments
    ///
    /// * `min_children` - Minimum number of children per row
    pub fn set_min_row_children(&self,min_children: u32) {
        self.set_property("min-row-children",&min_children)
            .expect("Error setting min-row-children of ProperFlowBox");
    }
}

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

    use super::*;
    use gtk::subclass::{
        container::Callback,widget::{WidgetImpl,WidgetImplExt},};

    const DEFAULT_MIN_ROW_CHILDREN: u32 = 5;

    pub struct ProperFlowBox {
        children: RefCell<Vec<gtk::Widget>>,h_spacing: RefCell<i32>,v_spacing: RefCell<i32>,min_row_children: RefCell<u32>,}

    static PROPERTIES: [subclass::Property; 3] = [
        subclass::Property("h-spacing",|h_spacing| {
            glib::ParamSpec::int(
                h_spacing,"Horizontal spacing","Space between two children in a row and between children and the container's borders",i32::MAX,glib::ParamFlags::READWRITE,)
        }),subclass::Property("v-spacing",|v_spacing| {
            glib::ParamSpec::int(
                v_spacing,"Vertical spacing","Space between two rows and between rows and the container's borders",subclass::Property("min-row-children",|min_row_children| {
            glib::ParamSpec::uint(
                min_row_children,"Minimal number of children in one row","Setting this to larger numbers increases the minumum width and decreases the minimum height",1,u32::MAX,DEFAULT_MIN_ROW_CHILDREN,];

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

        glib_object_subclass!();

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

        fn new() -> Self {
            Self {
                children: RefCell::new(Vec::new()),h_spacing: RefCell::new(0),v_spacing: RefCell::new(0),min_row_children: RefCell::new(DEFAULT_MIN_ROW_CHILDREN),}
        }
    }

    impl ObjectImpl for ProperFlowBox {
        fn set_property(&self,id: usize,value: &glib::Value) {
            let prop = &PROPERTIES[id];
            match *prop {
                subclass::Property("h-spacing",..) => {
                    *self.h_spacing.borrow_mut() = value.get_some().unwrap();
                }
                subclass::Property("v-spacing",..) => {
                    *self.v_spacing.borrow_mut() = value.get_some().unwrap();
                }
                subclass::Property("min-row-children",..) => {
                    *self.min_row_children.borrow_mut() = value.get_some().unwrap();
                }
                _ => panic!("Tried to set unknown property of ProperFlowBox"),}
        }

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

        fn constructed(&self,obj: &glib::Object) {
            self.parent_constructed(obj);
            obj.downcast_ref::<gtk::Widget>()
                .unwrap()
                .set_has_window(false);
        }
    }

    impl WidgetImpl for ProperFlowBox {
        fn size_allocate(&self,widget: &gtk::Widget,allocation: &gtk::Allocation) {
            self.parent_size_allocate(widget,&allocation);

            // Search for maximal working natural ratio.
            // Sort out most likely cases (enough for 1.0 or not for more than 0.0) and do
            // binary search otherwise.
            if self.check_height_for_natural_ratio(allocation.width,1.0,false)
                <= allocation.height
            {
                // Do 1.0
                self.check_height_for_natural_ratio(allocation.width,true);
            } else if self.check_height_for_natural_ratio(allocation.width,0.0,false)
                >= allocation.height
            {
                // Do 0.0
                self.check_height_for_natural_ratio(allocation.width,true);
            } else {
                // Do binary search
                // A lower stopping eps yields higher quality by finer transitions but less performance.
                // 0.001 should be a reasonable tradeoff as binary search implies that ca. log_2(0.25/0.001) ~= 8 iterations. 

                let mut current_ratio = 0.5;
                let mut current_step_width = 0.25;
                const STOPPING_EPS: f64 = 0.001;
                let mut max_ratio: f64 = 0.0;
                while current_step_width > STOPPING_EPS {
                    let required_height =
                        self.check_height_for_natural_ratio(allocation.width,current_ratio,false);
                    if allocation.height >= required_height {
                        max_ratio = max_ratio.max(current_ratio);
                        current_ratio += current_step_width;
                    } else {
                        current_ratio -= current_step_width;
                    }
                    current_step_width /= 2.0;
                }
                self.check_height_for_natural_ratio(allocation.width,max_ratio,true);
            }
        }

        fn get_request_mode(&self,_widget: &gtk::Widget) -> gtk::SizeRequestMode {
            gtk::SizeRequestMode::HeightForWidth
        }

        fn get_preferred_height(&self,widget: &gtk::Widget) -> (i32,i32) {
            widget.get_preferred_height_for_width(widget.get_preferred_width().0)
        }

        fn get_preferred_width_for_height(&self,_height: i32) -> (i32,i32) {
            widget.get_preferred_width()
        }

        fn get_preferred_width(&self,i32) {
            // Calculate an approximation of the required width by exactly placing `min_row_children` many
            // children in each row

            let mut min_width = 0;
            let mut natural_width = 0;

            let mut current_min_width = 0;
            let mut current_natural_width = 0;
            for (index,child) in self
                .children
                .borrow()
                .iter()
                .filter(|c| c.is_visible())
                .enumerate()
            {
                if index as u32 % *self.min_row_children.borrow() == 0 {
                    // Begin a new row
                    current_min_width = *self.h_spacing.borrow();
                    current_natural_width = *self.h_spacing.borrow();
                }

                current_min_width += child.get_preferred_width().0 + *self.h_spacing.borrow();
                current_natural_width += child.get_preferred_width().1 + *self.h_spacing.borrow();

                // Max each time for more consistent code as last row may not contain `min_row_children` children
                min_width = min_width.max(current_min_width);
                natural_width = natural_width.max(current_natural_width);
            }

            (min_width,natural_width)
        }

        fn get_preferred_height_for_width(&self,_widget: &gtk::Widget,width: i32) -> (i32,i32) {
            (
                self.check_height_for_natural_ratio(width,false),self.check_height_for_natural_ratio(width,)
        }
    }

    impl ContainerImpl for ProperFlowBox {
        fn add(&self,container: &gtk::Container,widget: &gtk::Widget) {
            self.children.borrow_mut().push(widget.clone());
            widget.set_parent(container);
            if container.get_visible() {
                container.queue_resize();
            }
        }

        fn remove(&self,widget: &gtk::Widget) {
            let index = self.children.borrow().iter().position(|c| c == widget);
            if let Some(index) = index {
                self.children.borrow_mut().remove(index);
                widget.unparent();
            } else {
                println!("Tried to remove non-child from ProperFlowBox")
            }

            if container.get_visible() {
                container.queue_resize();
            }
        }

        fn forall(
            &self,_container: &gtk::Container,_include_internals: bool,callback: &Callback,) {
            // Need to deepcopy children as callbacks may also borrow children
            let children = (*self.children.borrow()).clone();
            for child in children.iter() {
                callback.call(child);
            }
        }
    }

    impl ProperFlowBox {
        /// Tries to fit the visible children for the given available width.
        ///
        /// Given the available width,all visible children get their minimum size plus
        /// the fraction defined by `natural_ratio` of the additional size bringing them to
        /// their natural size. The function then returns the required height for the given width.
        /// It is possible to directly call `size_allocate` for the visible children by enabling
        /// it with the corresponding parameter. Real allocating also respects the `h-expand` property
        /// of the children which does not influence the returned height of the function.
        ///
        /// # Arguments
        ///
        /// * `available_width` - Width available to be filled
        /// * `natural_ratio` - Fraction of the additional size to meet the natural size coming from the minimum size.
        ///                     For a ratio `x` the allocated width and height will be `min + x * (max - min) == (1 - x) * min + x * max`
        /// * `allocate` - Call `size-allocate` on visible children if true
        fn check_height_for_natural_ratio(
            &self,available_width: i32,natural_ratio: f64,allocate: bool,) -> i32 {
            // Coordinates of next child
            let mut x = *self.h_spacing.borrow();
            let mut y = *self.v_spacing.borrow();

            let mut line_height = 0;
            let mut number_row_children = 0;
            let mut number_hexpand_row_children = 0;
            let mut row_start_index = 0;
            for (index,child) in self
                .children
                .borrow()
                .iter()
                .enumerate()
                .filter(|(_,c)| c.is_visible())
            {
                let width = ProperFlowBox::get_barycentric_combination(
                    child.get_preferred_width(),natural_ratio,);
                let height = ProperFlowBox::get_barycentric_combination(
                    match child.get_request_mode() {
                        gtk::SizeRequestMode::ConstantSize => child.get_preferred_height(),gtk::SizeRequestMode::HeightForWidth
                        | gtk::SizeRequestMode::WidthForHeight => {
                            child.get_preferred_height_for_width(width)
                        }
                        _ => panic!("Unknown size request mode"),},);
                if number_row_children > 0 && x + width + *self.h_spacing.borrow() > available_width
                {
                    // Not enough space in current line => Go to next line
                    // Exception: Current child will be only one in line,then assign as there is no
                    // valid assignment to any line for this child

                    // Allocate finished line
                    if allocate {
                        self.allocate_row(
                            &self.children.borrow()[row_start_index..index],y,line_height,number_hexpand_row_children,available_width - x,);
                    }

                    // Start next line
                    x = *self.h_spacing.borrow();
                    y += line_height + *self.v_spacing.borrow();
                    line_height = 0;
                    number_row_children = 0;
                    number_hexpand_row_children = 0;
                    row_start_index = index;
                }
                line_height = line_height.max(height);
                x += width + *self.h_spacing.borrow();
                number_row_children += 1;
                if child.get_hexpand() {
                    number_hexpand_row_children += 1;
                }
            }

            // Allocate last line
            if allocate {
                self.allocate_row(
                    &self.children.borrow()[row_start_index..],);
            }

            y + line_height + *self.v_spacing.borrow()
        }

        /// Allocates the size for the children in the given slice
        ///
        /// # Arguments
        ///
        /// * `children` - Slice of which the visible children inside form a row
        /// * `y` - y coordinate of the row
        /// * `height` - height of the row
        /// * `number_hexpand_children` - Number of children with h-expand
        /// * `remaining_space` - Unneeded space to be distributed among h-expand children
        /// * `natural_ratio` - Fraction of the additional size to meet the natural size coming from the minimum size.
        ///                     For a ratio `x` the allocated width and height will be `min + x * (max - min) == (1 - x) * min + x * max`
        fn allocate_row(
            &self,children: &[gtk::Widget],y: i32,height: i32,number_hexpand_children: i32,remaining_space: i32,) {
            let mut x = *self.h_spacing.borrow();
            let additional_width_per_child = if number_hexpand_children > 0 {
                remaining_space / number_hexpand_children
            } else {
                0
            };
            for child in children.iter().filter(|c| c.is_visible()) {
                let mut width = ProperFlowBox::get_barycentric_combination(
                    child.get_preferred_width(),);
                if child.get_hexpand() {
                    width += additional_width_per_child;
                }
                child.size_allocate(&gtk::Allocation {
                    x,width,height,});
                x += width + *self.h_spacing.borrow();
            }
        }

        /// Returns the barycentric combination of `min` and `max` with the given ratio,/// namely `min + ratio * (max - min) == (1 - ratio) * min + ratio * max`
        ///
        /// # Arguments
        ///
        /// * `min` - Min value
        /// * `max` - Max value
        /// * `ratio` - barycentric parameter,should be in `[0,1]` such that result is in `[min,max]`
        fn get_barycentric_combination((min,max): (i32,i32),ratio: f64) -> i32 {
            ((1.0 - ratio) * min as f64 + ratio * max as f64) as i32
        }
    }
}

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("Custom FlowBox Demo");
        let style = ".bordered {border: 1px solid black;}";
        let provider = gtk::CssProvider::new();
        provider
            .load_from_data(style.as_bytes())
            .expect("Failed to load CSS");
        gtk::StyleContext::add_provider_for_screen(
            &gdk::Screen::get_default().expect("Failed to load css provider"),&provider,gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,);

        let flow = ProperFlowBox::new();
        flow.set_h_spacing(10);
        flow.set_v_spacing(10);

        for i in 1..15 {
            let dummy = dummy::GAspectFillFrame::new();
            let label = gtk::Label::new(Some("Hello world"));
            dummy.add(&label);
            if i % 3 == 0 {
                dummy.set_hexpand(true);
            }
            label.get_style_context().add_class("bordered");
            flow.add(&dummy);
        }

        window.add(&flow);
        window.show_all();
    });

    application.run(&[]);
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。