List Widgets
Sometimes you want to display a list of elements in a certain arrangement.
gtk::ListBox
and gtk::FlowBox
are two container widgets which allow you to do this.
ListBox
describes a vertical list and FlowBox
describes a grid.
Let's explore this concept by adding labels to a ListBox
.
Each label will display an integer starting from 0 and ranging up to 100.
Filename: listings/list_widgets/1/main.rs
use gtk::prelude::*;
use gtk::{
glib, Application, ApplicationWindow, Label, ListBox, PolicyType, ScrolledWindow,
};
const APP_ID: &str = "org.gtk_rs.ListWidgets1";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `ListBox` and add labels with integers from 0 to 100
let list_box = ListBox::new();
for number in 0..=100 {
let label = Label::new(Some(&number.to_string()));
list_box.append(&label);
}
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_box)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
We cannot display so many widgets at once.
Therefore, we add ListBox
to a gtk::ScrolledWindow
.
Now we can scroll through our elements.
Filename: listings/list_widgets/1/main.rs
use gtk::prelude::*;
use gtk::{
glib, Application, ApplicationWindow, Label, ListBox, PolicyType, ScrolledWindow,
};
const APP_ID: &str = "org.gtk_rs.ListWidgets1";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `ListBox` and add labels with integers from 0 to 100
let list_box = ListBox::new();
for number in 0..=100 {
let label = Label::new(Some(&number.to_string()));
list_box.append(&label);
}
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_box)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
Views
That was easy enough. However, we currently create one widget per element. Since each widget takes up a bit of resources, many of them can lead to slow and unresponsive user interfaces. Depending on the widget type even thousands of elements might not be a problem. But how could we possibly deal with the infinite amount of posts in a social media timeline?
We use scalable lists instead!
- The model holds our data, filters it and describes its order.
- The list item factory defines how the data transforms into widgets.
- The view specifies how the widgets are then arranged.
What makes this concept scalable is that GTK only has to create slightly more widgets than we can currently look at. As we scroll through our elements, the widgets which become invisible will be reused. The following figure demonstrates how this works in practice.
100 000 elements is something ListBox
will struggle with, so let's use this to demonstrate scalable lists.
We start by defining and filling up our model.
The model is an instance of gio::ListStore
.
The main limitation here is that gio::ListStore
only accepts GObjects.
So let's create a custom GObject IntegerObject
that is initialized with a number.
Filename: listings/list_widgets/2/integer_object/mod.rs
mod imp;
use glib::Object;
use gtk::glib;
glib::wrapper! {
pub struct IntegerObject(ObjectSubclass<imp::IntegerObject>);
}
impl IntegerObject {
pub fn new(number: i32) -> Self {
Object::builder().property("number", number).build()
}
}
This number represents the internal state of IntegerObject
.
Filename: listings/list_widgets/2/integer_object/imp.rs
use std::cell::Cell;
use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
// Object holding the state
#[derive(Properties, Default)]
#[properties(wrapper_type = super::IntegerObject)]
pub struct IntegerObject {
#[property(get, set)]
number: Cell<i32>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for IntegerObject {
const NAME: &'static str = "MyGtkAppIntegerObject";
type Type = super::IntegerObject;
}
// Trait shared by all GObjects
#[glib::derived_properties]
impl ObjectImpl for IntegerObject {}
We now fill the model with integers from 0 to 100 000.
Please note that models only takes care of the data.
Neither Label
nor any other widget is mentioned here.
Filename: listings/list_widgets/2/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, Label, ListView, PolicyType,
ScrolledWindow, SignalListItemFactory, SingleSelection,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets2";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let label = Label::new(None);
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&label));
});
factory.connect_bind(move |_, list_item| {
// Get `IntegerObject` from `ListItem`
let integer_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Get `Label` from `ListItem`
let label = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<Label>()
.expect("The child has to be a `Label`.");
// Set "label" to "number"
label.set_label(&integer_object.number().to_string());
});
let selection_model = SingleSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
The ListItemFactory
takes care of the widgets as well as their relationship to the model.
Here, we use the SignalListItemFactory
which emits a signal for every relevant step in the life of a ListItem
.
The "setup" signal will be emitted when new widgets have to be created.
We connect to it to create a Label
for every requested widget.
Filename: listings/list_widgets/2/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, Label, ListView, PolicyType,
ScrolledWindow, SignalListItemFactory, SingleSelection,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets2";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let label = Label::new(None);
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&label));
});
factory.connect_bind(move |_, list_item| {
// Get `IntegerObject` from `ListItem`
let integer_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Get `Label` from `ListItem`
let label = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<Label>()
.expect("The child has to be a `Label`.");
// Set "label" to "number"
label.set_label(&integer_object.number().to_string());
});
let selection_model = SingleSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
In the "bind" step we bind the data in our model to the individual list items.
Filename: listings/list_widgets/2/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, Label, ListView, PolicyType,
ScrolledWindow, SignalListItemFactory, SingleSelection,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets2";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let label = Label::new(None);
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&label));
});
factory.connect_bind(move |_, list_item| {
// Get `IntegerObject` from `ListItem`
let integer_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Get `Label` from `ListItem`
let label = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<Label>()
.expect("The child has to be a `Label`.");
// Set "label" to "number"
label.set_label(&integer_object.number().to_string());
});
let selection_model = SingleSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
We only want single items to be selectable, so we choose SingleSelection
.
The other options would have been MultiSelection
or NoSelection
.
Then we pass the model and the factory to the ListView
.
Filename: listings/list_widgets/2/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, Label, ListView, PolicyType,
ScrolledWindow, SignalListItemFactory, SingleSelection,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets2";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let label = Label::new(None);
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&label));
});
factory.connect_bind(move |_, list_item| {
// Get `IntegerObject` from `ListItem`
let integer_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Get `Label` from `ListItem`
let label = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<Label>()
.expect("The child has to be a `Label`.");
// Set "label" to "number"
label.set_label(&integer_object.number().to_string());
});
let selection_model = SingleSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
Every ListView
has to be a direct child of a ScrolledWindow
, so we are adding it to one.
Filename: listings/list_widgets/2/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, Label, ListView, PolicyType,
ScrolledWindow, SignalListItemFactory, SingleSelection,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets2";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let label = Label::new(None);
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&label));
});
factory.connect_bind(move |_, list_item| {
// Get `IntegerObject` from `ListItem`
let integer_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Get `Label` from `ListItem`
let label = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<Label>()
.expect("The child has to be a `Label`.");
// Set "label" to "number"
label.set_label(&integer_object.number().to_string());
});
let selection_model = SingleSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
We can now easily scroll through our long list of integers.
Let's see what else we can do.
We might want to increase the number every time we activate its row.
For that we first add the method increase_number
to our IntegerObject
.
Filename: listings/list_widgets/3/integer_object/mod.rs
mod imp;
use glib::Object;
use gtk::glib;
glib::wrapper! {
pub struct IntegerObject(ObjectSubclass<imp::IntegerObject>);
}
impl IntegerObject {
pub fn new(number: i32) -> Self {
Object::builder().property("number", number).build()
}
pub fn increase_number(self) {
self.set_number(self.number() + 1);
}
}
In order to interact with our ListView
, we connect to its "activate" signal.
Filename: listings/list_widgets/3/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, Label, ListView, PolicyType,
ScrolledWindow, SignalListItemFactory, SingleSelection,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets3";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let label = Label::new(None);
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&label));
});
factory.connect_bind(move |_, list_item| {
// Get `IntegerObject` from `ListItem`
let integer_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Get `Label` from `ListItem`
let label = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<Label>()
.expect("The child has to be a `Label`.");
// Bind "label" to "number"
integer_object
.bind_property("number", &label, "label")
.sync_create()
.build();
});
let selection_model = SingleSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
list_view.connect_activate(move |list_view, position| {
// Get `IntegerObject` from model
let model = list_view.model().expect("The model has to exist.");
let integer_object = model
.item(position)
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Increase "number" of `IntegerObject`
integer_object.increase_number();
});
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
Now every time we activate an element, for example by double-clicking on it,
the corresponding "number" property of the IntegerObject
in the model will be increased by 1.
However, just because the IntegerObject
has been modified the corresponding Label
does not immediately change.
One naive approach would be to bind the properties in the "bind" step of the SignalListItemFactory
.
Filename: listings/list_widgets/3/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, Label, ListView, PolicyType,
ScrolledWindow, SignalListItemFactory, SingleSelection,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets3";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let label = Label::new(None);
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&label));
});
factory.connect_bind(move |_, list_item| {
// Get `IntegerObject` from `ListItem`
let integer_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Get `Label` from `ListItem`
let label = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<Label>()
.expect("The child has to be a `Label`.");
// Bind "label" to "number"
integer_object
.bind_property("number", &label, "label")
.sync_create()
.build();
});
let selection_model = SingleSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
list_view.connect_activate(move |list_view, position| {
// Get `IntegerObject` from model
let model = list_view.model().expect("The model has to exist.");
let integer_object = model
.item(position)
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Increase "number" of `IntegerObject`
integer_object.increase_number();
});
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
At first glance, that seems to work. However, as you scroll around and activate a few list elements, you will notice that sometimes multiple numbers change even though you only activated a single one. This relates to how the view works internally. Not every model item belongs to a single widget, but the widgets get recycled instead as you scroll through the view. That also means that in our case, multiple numbers will be bound to the same widget.
Expressions
Situations like these are so common that GTK offers an alternative to property binding: expressions. As a first step it allows us to remove the "bind" step. Let's see how the "setup" step now works.
Filename: listings/list_widgets/4/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, Label, ListView, PolicyType,
ScrolledWindow, SignalListItemFactory, SingleSelection, Widget,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets4";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
// Create label
let label = Label::new(None);
let list_item = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem");
list_item.set_child(Some(&label));
// Bind `list_item->item->number` to `label->label`
list_item
.property_expression("item")
.chain_property::<IntegerObject>("number")
.bind(&label, "label", Widget::NONE);
});
let selection_model = SingleSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
list_view.connect_activate(move |list_view, position| {
// Get `IntegerObject` from model
let model = list_view.model().expect("The model has to exist.");
let integer_object = model
.item(position)
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Increase "number" of `IntegerObject`
integer_object.increase_number();
});
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
An expression provides a way to describe references to values.
One interesting part here is that these references can be several steps away.
This allowed us in the snippet above to bind the property "number" of the property "item" of list_item
to the property "label" of label
.
It is also worth noting that at the "setup" stage there is no way of knowing which list item belongs to which label, simply because this changes as we scroll through the list. Here, another power of expressions becomes evident. Expressions allow us to describe relationships between objects or properties that might not even exist yet. We just had to tell it to change the label whenever the number that belongs to it changes. That way, we also don't face the problem that multiple labels are bound to the same number. When we now activate a label, only the corresponding number visibly changes.
Let's extend our app a bit more.
We can, for example, filter our model to only allow even numbers.
We do that by passing it to a gtk::FilterListModel
together with a gtk::CustomFilter
Filename: listings/list_widgets/5/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, CustomFilter, CustomSorter,
FilterChange, FilterListModel, Label, ListView, PolicyType, ScrolledWindow,
SignalListItemFactory, SingleSelection, SortListModel, SorterChange, Widget,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets5";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
// Create label
let label = Label::new(None);
let list_item = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem");
list_item.set_child(Some(&label));
// Bind `list_item->item->number` to `label->label`
list_item
.property_expression("item")
.chain_property::<IntegerObject>("number")
.bind(&label, "label", Widget::NONE);
});
let filter = CustomFilter::new(move |obj| {
// Get `IntegerObject` from `glib::Object`
let integer_object = obj
.downcast_ref::<IntegerObject>()
.expect("The object needs to be of type `IntegerObject`.");
// Only allow even numbers
integer_object.number() % 2 == 0
});
let filter_model = FilterListModel::new(Some(model), Some(filter.clone()));
let sorter = CustomSorter::new(move |obj1, obj2| {
// Get `IntegerObject` from `glib::Object`
let integer_object_1 = obj1
.downcast_ref::<IntegerObject>()
.expect("The object needs to be of type `IntegerObject`.");
let integer_object_2 = obj2
.downcast_ref::<IntegerObject>()
.expect("The object needs to be of type `IntegerObject`.");
// Get property "number" from `IntegerObject`
let number_1 = integer_object_1.number();
let number_2 = integer_object_2.number();
// Reverse sorting order -> large numbers come first
number_2.cmp(&number_1).into()
});
let sort_model = SortListModel::new(Some(filter_model), Some(sorter.clone()));
let selection_model = SingleSelection::new(Some(sort_model));
let list_view = ListView::new(Some(selection_model), Some(factory));
list_view.connect_activate(move |list_view, position| {
// Get `IntegerObject` from model
let model = list_view.model().expect("The model has to exist.");
let integer_object = model
.item(position)
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Increase "number" of `IntegerObject`
integer_object.increase_number();
// Notify that the filter and sorter have been changed
filter.changed(FilterChange::Different);
sorter.changed(SorterChange::Different);
});
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
Additionally, we can reverse the order of our model.
Now we pass the filtered model to gtk::SortListModel
together with gtk::CustomSorter
.
Filename: listings/list_widgets/5/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, CustomFilter, CustomSorter,
FilterChange, FilterListModel, Label, ListView, PolicyType, ScrolledWindow,
SignalListItemFactory, SingleSelection, SortListModel, SorterChange, Widget,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets5";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
// Create label
let label = Label::new(None);
let list_item = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem");
list_item.set_child(Some(&label));
// Bind `list_item->item->number` to `label->label`
list_item
.property_expression("item")
.chain_property::<IntegerObject>("number")
.bind(&label, "label", Widget::NONE);
});
let filter = CustomFilter::new(move |obj| {
// Get `IntegerObject` from `glib::Object`
let integer_object = obj
.downcast_ref::<IntegerObject>()
.expect("The object needs to be of type `IntegerObject`.");
// Only allow even numbers
integer_object.number() % 2 == 0
});
let filter_model = FilterListModel::new(Some(model), Some(filter.clone()));
let sorter = CustomSorter::new(move |obj1, obj2| {
// Get `IntegerObject` from `glib::Object`
let integer_object_1 = obj1
.downcast_ref::<IntegerObject>()
.expect("The object needs to be of type `IntegerObject`.");
let integer_object_2 = obj2
.downcast_ref::<IntegerObject>()
.expect("The object needs to be of type `IntegerObject`.");
// Get property "number" from `IntegerObject`
let number_1 = integer_object_1.number();
let number_2 = integer_object_2.number();
// Reverse sorting order -> large numbers come first
number_2.cmp(&number_1).into()
});
let sort_model = SortListModel::new(Some(filter_model), Some(sorter.clone()));
let selection_model = SingleSelection::new(Some(sort_model));
let list_view = ListView::new(Some(selection_model), Some(factory));
list_view.connect_activate(move |list_view, position| {
// Get `IntegerObject` from model
let model = list_view.model().expect("The model has to exist.");
let integer_object = model
.item(position)
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Increase "number" of `IntegerObject`
integer_object.increase_number();
// Notify that the filter and sorter have been changed
filter.changed(FilterChange::Different);
sorter.changed(SorterChange::Different);
});
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
To ensure that our filter and sorter get updated when we modify the numbers, we call the changed
method on them.
Filename: listings/list_widgets/5/main.rs
mod integer_object;
use gtk::{
gio, glib, Application, ApplicationWindow, CustomFilter, CustomSorter,
FilterChange, FilterListModel, Label, ListView, PolicyType, ScrolledWindow,
SignalListItemFactory, SingleSelection, SortListModel, SorterChange, Widget,
};
use gtk::{prelude::*, ListItem};
use integer_object::IntegerObject;
const APP_ID: &str = "org.gtk_rs.ListWidgets5";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `Vec<IntegerObject>` with numbers from 0 to 100_000
let vector: Vec<IntegerObject> = (0..=100_000).map(IntegerObject::new).collect();
// Create new model
let model = gio::ListStore::new::<IntegerObject>();
// Add the vector to the model
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
// Create label
let label = Label::new(None);
let list_item = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem");
list_item.set_child(Some(&label));
// Bind `list_item->item->number` to `label->label`
list_item
.property_expression("item")
.chain_property::<IntegerObject>("number")
.bind(&label, "label", Widget::NONE);
});
let filter = CustomFilter::new(move |obj| {
// Get `IntegerObject` from `glib::Object`
let integer_object = obj
.downcast_ref::<IntegerObject>()
.expect("The object needs to be of type `IntegerObject`.");
// Only allow even numbers
integer_object.number() % 2 == 0
});
let filter_model = FilterListModel::new(Some(model), Some(filter.clone()));
let sorter = CustomSorter::new(move |obj1, obj2| {
// Get `IntegerObject` from `glib::Object`
let integer_object_1 = obj1
.downcast_ref::<IntegerObject>()
.expect("The object needs to be of type `IntegerObject`.");
let integer_object_2 = obj2
.downcast_ref::<IntegerObject>()
.expect("The object needs to be of type `IntegerObject`.");
// Get property "number" from `IntegerObject`
let number_1 = integer_object_1.number();
let number_2 = integer_object_2.number();
// Reverse sorting order -> large numbers come first
number_2.cmp(&number_1).into()
});
let sort_model = SortListModel::new(Some(filter_model), Some(sorter.clone()));
let selection_model = SingleSelection::new(Some(sort_model));
let list_view = ListView::new(Some(selection_model), Some(factory));
list_view.connect_activate(move |list_view, position| {
// Get `IntegerObject` from model
let model = list_view.model().expect("The model has to exist.");
let integer_object = model
.item(position)
.and_downcast::<IntegerObject>()
.expect("The item has to be an `IntegerObject`.");
// Increase "number" of `IntegerObject`
integer_object.increase_number();
// Notify that the filter and sorter have been changed
filter.changed(FilterChange::Different);
sorter.changed(SorterChange::Different);
});
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
After our changes, the application looks like this:
String List
Often, all you want is to display a list of strings.
However, if you either need to filter and sort your displayed data or have too many elements to be displayed by ListBox
, you will still want to use a view.
GTK provides a convenient model for this use case: gtk::StringList
.
Let's see with a small example how to use this API. Filter and sorter is controlled by the factory, so nothing changes here. This is why we will skip this topic here.
First, we add a bunch of strings to our model.
Filename: listings/list_widgets/6/main.rs
use gtk::{glib, prelude::*, ListItem};
use gtk::{
Application, ApplicationWindow, Label, ListView, NoSelection, PolicyType,
ScrolledWindow, SignalListItemFactory, StringList, StringObject, Widget,
};
const APP_ID: &str = "org.gtk_rs.ListWidgets6";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `StringList` with number from 0 to 100_000
// `StringList` implements FromIterator<String>
let model: StringList = (0..=100_000).map(|number| number.to_string()).collect();
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
// Create label
let label = Label::new(None);
let list_item = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem");
list_item.set_child(Some(&label));
// Bind `list_item->item->string` to `label->label`
list_item
.property_expression("item")
.chain_property::<StringObject>("string")
.bind(&label, "label", Widget::NONE);
});
let selection_model = NoSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
Note that we can create a StringList
directly from an iterator over strings.
This means we don't have to create a custom GObject for our model anymore.
As usual, we connect the label to the list item via an expression.
Here we can use StringObject
, which exposes its content via the property "string".
Filename: listings/list_widgets/6/main.rs
use gtk::{glib, prelude::*, ListItem};
use gtk::{
Application, ApplicationWindow, Label, ListView, NoSelection, PolicyType,
ScrolledWindow, SignalListItemFactory, StringList, StringObject, Widget,
};
const APP_ID: &str = "org.gtk_rs.ListWidgets6";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a `StringList` with number from 0 to 100_000
// `StringList` implements FromIterator<String>
let model: StringList = (0..=100_000).map(|number| number.to_string()).collect();
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
// Create label
let label = Label::new(None);
let list_item = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem");
list_item.set_child(Some(&label));
// Bind `list_item->item->string` to `label->label`
list_item
.property_expression("item")
.chain_property::<StringObject>("string")
.bind(&label, "label", Widget::NONE);
});
let selection_model = NoSelection::new(Some(model));
let list_view = ListView::new(Some(selection_model), Some(factory));
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
// Present window
window.present();
}
Conclusion
We now know how to display a list of data.
Small amount of elements can be handled by ListBox
or FlowBox
.
These widgets are easy to use and can, if necessary, be bound to a model such as gio::ListStore
.
Their data can then be modified, sorted and filtered more easily.
However, if we need the widgets to be scalable, we still need to use ListView
, ColumnView
or GridView
instead.