创建一个简单的待办事项应用
在我们学习了这么多概念之后,终于到了付诸实践的时候了。 我们要制作一个待办事项应用程序!
目前,我们只需制作一个最小版本即可。 一个输入新任务的输入框和一个显示任务的列表视图就足够了。 类似这样:
窗口
这个模型可以用下面的复合模板来描述。
文件:listings/todo/1/resources/window.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="TodoWindow" parent="GtkApplicationWindow">
<property name="width-request">360</property>
<property name="title" translatable="yes">To-Do</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="spacing">6</property>
<child>
<object class="GtkEntry" id="entry">
<property name="placeholder-text" translatable="yes">Enter a Task…</property>
<property name="secondary-icon-name">list-add-symbolic</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="min-content-height">360</property>
<property name="vexpand">true</property>
<child>
<object class="GtkListView" id="tasks_list">
<property name="valign">start</property>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>
为了使用复合模板,我们创建了一个自定义控件。 它的父类是 gtk::ApplicationWindow
,因此我们继承自它。 像往常一样,我们必须列出除了 GObject
和 GInitiallyUnowned
之外的所有祖先和接口。
文件名:listings/todo/1/window/mod.rs
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list view
let selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
}
然后初始化 imp::Window
的复合模板。 我们存储输入框(entry)、列表视图(list view)和列表模型(list model)的引用。 这将在我们以后为窗口添加方法时派上用场。 之后,我们将添加用于初始化复合模板的典型模板。 我们只需确保 window.ui
中模板的 class
属性与 NAME
匹配即可。
文件名:listings/todo/1/window/imp.rs
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, CompositeTemplate, Entry, ListView};
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo1/window.ui")]
pub struct Window {
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListView>,
pub tasks: RefCell<Option<gio::ListStore>>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "TodoWindow";
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Setup
let obj = self.obj();
obj.setup_tasks();
obj.setup_callbacks();
obj.setup_factory();
}
}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {}
// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}
main.rs
也没有什么新东西。
mod task_object;
mod task_row;
mod window;
use gtk::prelude::*;
use gtk::{gio, glib, Application};
use window::Window;
fn main() -> glib::ExitCode {
// Register and include resources
gio::resources_register_include!("todo_1.gresource")
.expect("Failed to register resources.");
// Create a new application
let app = Application::builder()
.application_id("org.gtk_rs.Todo1")
.build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a new custom window and present it
let window = Window::new(app);
window.present();
}
最后,我们指定资源。 这里已经包含了 task_row.ui
,我们将在本章稍后处理。
文件名:listings/todo/1/resources/resources.gresource.xml
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gtk_rs/Todo1/">
<file compressed="true" preprocess="xml-stripblanks">task_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
</gresource>
</gresources>
任务对象
目前还不错。 主用户界面已经完成,但输入框还不能对输入做出反应。 另外,输入内容会放在哪里呢? 我们甚至还没有建立列表模型。 让我们开始吧!
正如在列表部件一章中所讨论的,我们首先要创建一个自定义 GObject。 该对象将存储任务的状态,其中包括:
- 一个布尔值用于描述任务是否完成
- 一个字符串存储任务名称
文件名:listings/todo/1/task_object/mod.rs
mod imp;
use glib::Object;
use gtk::glib;
glib::wrapper! {
pub struct TaskObject(ObjectSubclass<imp::TaskObject>);
}
impl TaskObject {
pub fn new(completed: bool, content: String) -> Self {
Object::builder()
.property("completed", completed)
.property("content", content)
.build()
}
}
#[derive(Default)]
pub struct TaskData {
pub completed: bool,
pub content: String,
}
与列表一章不同的是,状态存储在一个结构体中,而不是 imp::TaskObject
的单个成员中。 这对于在后续章节中保存状态非常方便。
文件名:listings/todo/1/task_object/mod.rs
mod imp;
use glib::Object;
use gtk::glib;
glib::wrapper! {
pub struct TaskObject(ObjectSubclass<imp::TaskObject>);
}
impl TaskObject {
pub fn new(completed: bool, content: String) -> Self {
Object::builder()
.property("completed", completed)
.property("content", content)
.build()
}
}
#[derive(Default)]
pub struct TaskData {
pub completed: bool,
pub content: String,
}
我们将把 completed
和 content
作为属性公开。 由于数据现在是在一个结构体中,而不是在单个成员变量中,因此我们必须添加更多注解。 对于每个属性,我们都要额外指定名称、类型和要访问的 TaskData
成员变量。
文件名:listings/todo/1/task_object/imp.rs
use std::cell::RefCell;
use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use super::TaskData;
// Object holding the state
#[derive(Properties, Default)]
#[properties(wrapper_type = super::TaskObject)]
pub struct TaskObject {
#[property(name = "completed", get, set, type = bool, member = completed)]
#[property(name = "content", get, set, type = String, member = content)]
pub data: RefCell<TaskData>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for TaskObject {
const NAME: &'static str = "TodoTaskObject";
type Type = super::TaskObject;
}
// Trait shared by all GObjects
#[glib::derived_properties]
impl ObjectImpl for TaskObject {}
任务行
下面我们来看看各个任务。 任务的行应该是这样的:
同样,我们用一个复合模板来描述模型。
文件名:listings/todo/1/resources/task_row.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="TodoTaskRow" parent="GtkBox">
<child>
<object class="GtkCheckButton" id="completed_button">
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
</object>
</child>
<child>
<object class="GtkLabel" id="content_label">
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
</object>
</child>
</template>
</interface>
在这个代码中,我们用 gtk:Box
实现 TaskRow
:
文件名:listings/todo/1/task_row/mod.rs
mod imp;
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{glib, pango};
use pango::{AttrInt, AttrList};
use crate::task_object::TaskObject;
glib::wrapper! {
pub struct TaskRow(ObjectSubclass<imp::TaskRow>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl Default for TaskRow {
fn default() -> Self {
Self::new()
}
}
impl TaskRow {
pub fn new() -> Self {
Object::builder().build()
}
pub fn bind(&self, task_object: &TaskObject) {
// Get state
let completed_button = self.imp().completed_button.get();
let content_label = self.imp().content_label.get();
let mut bindings = self.imp().bindings.borrow_mut();
// Bind `task_object.completed` to `task_row.completed_button.active`
let completed_button_binding = task_object
.bind_property("completed", &completed_button, "active")
.bidirectional()
.sync_create()
.build();
// Save binding
bindings.push(completed_button_binding);
// Bind `task_object.content` to `task_row.content_label.label`
let content_label_binding = task_object
.bind_property("content", &content_label, "label")
.sync_create()
.build();
// Save binding
bindings.push(content_label_binding);
// Bind `task_object.completed` to `task_row.content_label.attributes`
let content_label_binding = task_object
.bind_property("completed", &content_label, "attributes")
.sync_create()
.transform_to(|_, active| {
let attribute_list = AttrList::new();
if active {
// If "active" is true, content of the label will be strikethrough
let attribute = AttrInt::new_strikethrough(true);
attribute_list.insert(attribute);
}
Some(attribute_list.to_value())
})
.build();
// Save binding
bindings.push(content_label_binding);
}
pub fn unbind(&self) {
// Unbind all stored bindings
for binding in self.imp().bindings.borrow_mut().drain(..) {
binding.unbind();
}
}
}
在 imp::TaskRow
中,我们保存了 completed_button
和 content_label
的引用。 我们还存储了绑定的可变vector。 当我们把 TaskObject
的状态绑定到相应的 TaskRow
时,就会明白为什么需要这样做了。
文件名:listings/todo/1/task_row/imp.rs
use std::cell::RefCell;
use glib::Binding;
use gtk::subclass::prelude::*;
use gtk::{glib, CheckButton, CompositeTemplate, Label};
// Object holding the state
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/gtk_rs/Todo1/task_row.ui")]
pub struct TaskRow {
#[template_child]
pub completed_button: TemplateChild<CheckButton>,
#[template_child]
pub content_label: TemplateChild<Label>,
// Vector holding the bindings to properties of `TaskObject`
pub bindings: RefCell<Vec<Binding>>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for TaskRow {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "TodoTaskRow";
type Type = super::TaskRow;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for TaskRow {}
// Trait shared by all widgets
impl WidgetImpl for TaskRow {}
// Trait shared by all boxes
impl BoxImpl for TaskRow {}
现在我们可以将所有内容整合在一起。 我们重写 imp::Window::constructed
,以便在创建窗口时设置窗口内容。
文件名:listings/todo/1/window/imp.rs
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, CompositeTemplate, Entry, ListView};
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo1/window.ui")]
pub struct Window {
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListView>,
pub tasks: RefCell<Option<gio::ListStore>>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "TodoWindow";
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Setup
let obj = self.obj();
obj.setup_tasks();
obj.setup_callbacks();
obj.setup_factory();
}
}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {}
// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}
由于我们需要经常访问列表模型,为此我们添加了便捷方法 Window::tasks
. 在 Window::setup_tasks
中,我们创建了一个新模型。 然后在 imp::Window
和 gtk::ListView
中存储对模型的引用。
文件名:listings/todo/1/window/mod.rs
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list view
let selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
}
我们还创建了一个方法 new_task
,它可以获取条目的内容、清除条目并使用内容创建新任务。
文件名:listings/todo/1/window/mod.rs
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list view
let selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
}
在 Window::setup_callbacks
中,我们连接到输入框的 "activate"(激活)信号。 当我们在输入框中按下回车键时,就会触发该信号。 然后会创建一个包含内容的新 TaskObject
,并将其附加到模型中。 最后,输入框将被清空。
文件名:listings/todo/1/window/mod.rs
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list view
let selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
}
gtk::ListView
的列表元素是由一个工厂生产的。 在继续实现之前,让我们退一步思考一下我们期望的行为。 我们还希望 TaskRow
的 completed_button
跟随 TaskObject
的 completed
。 这可以通过类似于列表一章中的表达式来实现。
但是,如果我们切换 TaskRow
的 completed_button
的状态,TaskObject
的 completed
也应随之改变。 遗憾的是,表达式无法处理双向关系。 这意味着我们必须使用属性绑定。 当不再需要时,我们需要手动解除绑定。
我们将在 Window::setup_factory
的"设置(setup)"步骤中创建空的 TaskRow
对象,并在"绑定(bind)"和"解除绑定(unbind)"步骤中处理绑定问题。
文件名:listings/todo/1/window/mod.rs
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list view
let selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
}
在 TaskRow::bind
中绑定属性的工作原理与前几章相同。 唯一不同的是,我们将绑定存储在一个vector(vector)中。 这一点很有必要,因为当您滚动列表时,任务行( TaskRow
)会被重复使用。 这意味着随着时间的推移,任务行需要绑定到一个新的任务对象,并且必须从旧的任务对象中解除绑定。 只有在能访问存储的 glib::Binding
时,解除绑定才会起作用。
文件名:listings/todo/1/task_row/mod.rs
mod imp;
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{glib, pango};
use pango::{AttrInt, AttrList};
use crate::task_object::TaskObject;
glib::wrapper! {
pub struct TaskRow(ObjectSubclass<imp::TaskRow>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl Default for TaskRow {
fn default() -> Self {
Self::new()
}
}
impl TaskRow {
pub fn new() -> Self {
Object::builder().build()
}
pub fn bind(&self, task_object: &TaskObject) {
// Get state
let completed_button = self.imp().completed_button.get();
let content_label = self.imp().content_label.get();
let mut bindings = self.imp().bindings.borrow_mut();
// Bind `task_object.completed` to `task_row.completed_button.active`
let completed_button_binding = task_object
.bind_property("completed", &completed_button, "active")
.bidirectional()
.sync_create()
.build();
// Save binding
bindings.push(completed_button_binding);
// Bind `task_object.content` to `task_row.content_label.label`
let content_label_binding = task_object
.bind_property("content", &content_label, "label")
.sync_create()
.build();
// Save binding
bindings.push(content_label_binding);
// Bind `task_object.completed` to `task_row.content_label.attributes`
let content_label_binding = task_object
.bind_property("completed", &content_label, "attributes")
.sync_create()
.transform_to(|_, active| {
let attribute_list = AttrList::new();
if active {
// If "active" is true, content of the label will be strikethrough
let attribute = AttrInt::new_strikethrough(true);
attribute_list.insert(attribute);
}
Some(attribute_list.to_value())
})
.build();
// Save binding
bindings.push(content_label_binding);
}
pub fn unbind(&self) {
// Unbind all stored bindings
for binding in self.imp().bindings.borrow_mut().drain(..) {
binding.unbind();
}
}
}
TaskRow::unbind
负责清理工作。 它会遍历 vector 并解除每个绑定。 最后,它会清除该 vector.
文件名:listings/todo/1/task_row/mod.rs
mod imp;
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{glib, pango};
use pango::{AttrInt, AttrList};
use crate::task_object::TaskObject;
glib::wrapper! {
pub struct TaskRow(ObjectSubclass<imp::TaskRow>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl Default for TaskRow {
fn default() -> Self {
Self::new()
}
}
impl TaskRow {
pub fn new() -> Self {
Object::builder().build()
}
pub fn bind(&self, task_object: &TaskObject) {
// Get state
let completed_button = self.imp().completed_button.get();
let content_label = self.imp().content_label.get();
let mut bindings = self.imp().bindings.borrow_mut();
// Bind `task_object.completed` to `task_row.completed_button.active`
let completed_button_binding = task_object
.bind_property("completed", &completed_button, "active")
.bidirectional()
.sync_create()
.build();
// Save binding
bindings.push(completed_button_binding);
// Bind `task_object.content` to `task_row.content_label.label`
let content_label_binding = task_object
.bind_property("content", &content_label, "label")
.sync_create()
.build();
// Save binding
bindings.push(content_label_binding);
// Bind `task_object.completed` to `task_row.content_label.attributes`
let content_label_binding = task_object
.bind_property("completed", &content_label, "attributes")
.sync_create()
.transform_to(|_, active| {
let attribute_list = AttrList::new();
if active {
// If "active" is true, content of the label will be strikethrough
let attribute = AttrInt::new_strikethrough(true);
attribute_list.insert(attribute);
}
Some(attribute_list.to_value())
})
.build();
// Save binding
bindings.push(content_label_binding);
}
pub fn unbind(&self) {
// Unbind all stored bindings
for binding in self.imp().bindings.borrow_mut().drain(..) {
binding.unbind();
}
}
}
就这样,我们创建了一个基本的待办事项应用程序! 在接下来的章节中,我们将添加其他功能以对其进行扩展。