最近,在尝试 GTK 及其 Ruby 绑定的过程中,我决定编写一个教程来介绍此功能。在这篇文章中,我们将使用 gtk3
gem(又名 GTK+ Ruby 绑定)创建一个简单的 ToDo 应用程序(类似于 我们使用 Ruby on Rails 创建的)。
您可以在 GitHub 上找到本教程的代码。
什么是 GTK+?
根据 GTK+ 的网站
GTK+,或称 GIMP 工具包,是一个用于创建图形用户界面的多平台工具包。GTK+ 提供了一整套完整的窗口小部件,适用于从小型一次性工具到完整应用程序套件的项目。
该网站还解释了 GTK+ 创建 的原因
GTK+ 最初是为 GIMP(GNU 图像处理程序)开发和使用的。它被称为“The GIMP ToolKit”,以便记住该项目的起源。如今,它更常被称为 GTK+ 的简称,并被包括 GNU 项目的 GNOME 桌面在内的许多应用程序使用。
先决条件
GTK+
确保您已安装 GTK+。我在 Ubuntu 16.04 中开发了本教程的应用程序,该版本默认安装了 GTK+(版本 3.18)。
您可以使用以下命令检查您的版本:dpkg -l libgtk-3-0
。
Ruby
您的系统应该已安装 Ruby。我使用 RVM 来管理系统上安装的多个 Ruby 版本。如果您也想这样做,您可以在 RVM 的主页上找到 RVM 安装说明,并在 相关文档页面 上找到安装 Ruby 版本(又名 Rubies)的说明。
本教程使用 Ruby 2.4.2。您可以使用 ruby --version
或通过 RVM 使用 rvm list
检查您的版本。

opensource.com
Glade
根据 Glade 的网站,“Glade 是一个 RAD 工具,可以快速轻松地开发用于 GTK+ 工具包和 GNOME 桌面环境的用户界面。”
我们将使用 Glade 来设计我们应用程序的用户界面。如果您使用的是 Ubuntu,请使用 sudo apt install glade
安装 glade
。
GTK3 gem
这个 gem 提供了 GTK+ 工具包的 Ruby 绑定。换句话说,它允许我们使用 Ruby 语言与 GTK+ API 通信。
使用 gem install gtk3
安装 gem。
定义应用程序规范
我们将在本教程中构建的应用程序将
- 拥有用户界面(即桌面应用程序)
- 允许用户为每个项目设置各种属性(例如,优先级)
- 允许用户创建和编辑 ToDo 项目
- 所有项目都将作为文件保存在用户主目录中名为
.gtk-todo-tutorial
的文件夹中
- 所有项目都将作为文件保存在用户主目录中名为
- 允许用户存档 ToDo 项目
- 存档的项目应放在名为
archived
的单独文件夹中
- 存档的项目应放在名为
应用程序结构
gtk-todo-tutorial # root directory
|-- application
|-- ui # everything related to the ui of the application
|-- models # our models
|-- lib # the directory to host any utilities we might need
|-- resources # directory to host the resources of our application
gtk-todo # the executable that will start our application
构建 ToDo 应用程序
初始化应用程序
创建一个目录来保存应用程序需要的所有文件。正如您在上面的结构中看到的,我将其命名为 gtk-todo-tutorial
。
创建一个名为 gtk-todo
的文件(没错,没有扩展名)并添加以下内容
#!/usr/bin/env ruby
require 'gtk3'
app = Gtk::Application.new 'com.iridakos.gtk-todo', :flags_none
app.signal_connect :activate do |application|
window = Gtk::ApplicationWindow.new(application)
window.set_title 'Hello GTK+Ruby!'
window.present
end
puts app.run
这将是启动应用程序的脚本。
请注意第一行中的 shebang (#!
)。这是我们定义哪个解释器将在 Unix/Linux 操作系统下执行脚本的方式。这样,我们就不必使用 ruby gtk-todo
;我们可以直接使用脚本的名称:gtk-todo
。
但先不要尝试,因为我们还没有将文件的模式更改为可执行。要执行此操作,请在导航到应用程序的根目录后在终端中键入以下命令
chmod +x ./gtk-todo # make the script executable
从控制台,执行
./gtk-todo # execute the script

opensource.com
注释
- 我们上面定义的应用程序对象(以及一般的 GTK+ 窗口小部件)发出信号来触发事件。例如,一旦应用程序开始运行,它就会发出一个信号来触发
activate
事件。我们所要做的就是定义当发出此信号时我们想要发生什么。我们通过使用signal_connect
实例方法并向其传递一个代码块来实现这一点,该代码块将在给定事件发生时执行。在整个教程中,我们将大量使用这种方法。 - 当我们初始化
Gtk::Application
对象时,我们传递了两个参数com.iridakos.gtk-todo
:这是我们应用程序的 ID,通常,它应该是一个反向 DNS 样式的标识符。您可以在 GNOME 的 wiki 上了解有关其用法和最佳实践的更多信息。:flags_none
:此标志定义了应用程序的行为。我们使用了默认行为。查看所有 标志 以及它们定义的应用程序类型。我们可以使用 Ruby 等效标志,如Gio::ApplicationFlags.constants
中定义的那样。例如,我们可以使用Gio::ApplicationFlags::FLAGS_NONE
而不是使用:flags_none
。
假设我们先前创建的应用程序对象 (Gtk::Application
) 在发出 activate
信号时有很多事情要做,或者我们想要连接到更多信号。最终我们会创建一个巨大的 gtk-todo
脚本文件,使其难以阅读/维护。现在是时候重构了。
如上面的应用程序结构所述,我们将创建一个名为 application
的文件夹和子文件夹 ui
、models
和 lib
。
- 在
ui
文件夹中,我们将放置所有与用户界面相关的文件。 - 在
models
文件夹中,我们将放置所有与模型相关的文件。 - 在
lib
文件夹中,我们将放置所有不属于这两个类别的文件。
我们将为我们的应用程序定义 Gtk::Application
类的新子类。我们将在 application/ui/todo
下创建一个名为 application.rb
的文件,内容如下
module ToDo
class Application < Gtk::Application
def initialize
super 'com.iridakos.gtk-todo', Gio::ApplicationFlags::FLAGS_NONE
signal_connect :activate do |application|
window = Gtk::ApplicationWindow.new(application)
window.set_title 'Hello GTK+Ruby!'
window.present
end
end
end
end
我们将相应地更改 gtk-todo
脚本
#!/usr/bin/env ruby
require 'gtk3'
app = ToDo::Application.new
puts app.run
更简洁了,不是吗?是的,但它不起作用。我们得到类似这样的东西
./gtk-todo:5:in `<main>': uninitialized constant ToDo (NameError)
问题是我们没有 require 任何放置在 application
文件夹中的 Ruby 文件。我们需要按如下方式更改脚本文件并再次执行它。
#!/usr/bin/env ruby
require 'gtk3'
# Require all ruby files in the application folder recursively
application_root_path = File.expand_path(__dir__)
Dir[File.join(application_root_path, '**', '*.rb')].each { |file| require file }
app = ToDo::Application.new
puts app.run
现在应该没问题了。
资源
在本教程的开头,我们说过我们将使用 Glade 来设计应用程序的用户界面。Glade 生成 xml
文件,其中包含适当的元素和属性,反映了我们通过其用户界面设计的内容。我们需要使用这些文件来使我们的应用程序获得我们设计的 UI。
这些文件是应用程序的资源,GResource
API 提供了一种将它们全部打包到一个二进制文件中的方法,以后可以从应用程序内部访问它们,与手动处理已加载的资源、它们在文件系统上的位置等相比,具有优势。阅读有关 GResource
API 的更多信息。
描述资源
首先,我们需要创建一个文件来描述应用程序的资源。创建一个名为 gresources.xml
的文件,并将其直接放在 resources
文件夹下。
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/iridakos/gtk-todo">
<file preprocess="xml-stripblanks">ui/application_window.ui</file>
</gresource>
</gresources>
此描述基本上说:“我们有一个资源位于 ui
目录(相对于此 xml
文件)下,名称为 application_window.ui
。在加载此资源之前,请删除空白。” 当然,这还不能工作,因为我们还没有通过 Glade 创建资源。不过别担心,一次做一件事。
注意:xml-stripblanks
指令将使用 xmllint
命令删除空白。在 Ubuntu 中,您必须安装软件包 libxml2-utils
。
构建资源二进制文件
为了生成二进制资源文件,我们将使用另一个名为 glib-compile-resources
的 GLib 库实用程序。使用 dpkg -l libglib2.0-bin
检查您是否已安装它。您应该看到类似这样的内容
ii libglib2.0-bin 2.48.2-0ubuntu amd64 Programs for the GLib library
如果未安装,请安装该软件包(在 Ubuntu 中为 sudo apt install libglib2.0-bin
)。
让我们构建文件。我们将向我们的脚本添加代码,以便每次执行脚本时都会构建资源。按如下方式更改 gtk-todo
脚本
#!/usr/bin/env ruby
require 'gtk3'
require 'fileutils'
# Require all ruby files in the application folder recursively
application_root_path = File.expand_path(__dir__)
Dir[File.join(application_root_path, '**', '*.rb')].each { |file| require file }
# Define the source & target files of the glib-compile-resources command
resource_xml = File.join(application_root_path, 'resources', 'gresources.xml')
resource_bin = File.join(application_root_path, 'gresource.bin')
# Build the binary
system("glib-compile-resources",
"--target", resource_bin,
"--sourcedir", File.dirname(resource_xml),
resource_xml)
at_exit do
# Before existing, please remove the binary we produced, thanks.
FileUtils.rm_f(resource_bin)
end
app = ToDo::Application.new
puts app.run
当我们执行它时,控制台中会发生以下情况;我们稍后会修复它
/.../gtk-todo-tutorial/resources/gresources.xml: Failed to locate 'ui/application_window.ui' in any source directory.
这是我们所做的
- 为
fileutils
库添加了一个require
语句,以便我们可以在at_exit
调用中使用它 - 定义了
glib-compile-resources
命令的源文件和目标文件 - 执行了
glib-compile-resources
命令 - 设置了一个钩子,以便在退出脚本之前(即在应用程序退出之前)删除二进制文件,以便下次再次构建它
加载资源二进制文件
我们已经描述了资源并将它们打包在一个二进制文件中。现在我们必须加载它们并将它们注册到应用程序中,以便我们可以使用它们。这就像在 at_exit
钩子之前添加以下两行一样简单
resource = Gio::Resource.load(resource_bin)
Gio::Resources.register(resource)
就是这样。从现在开始,我们可以从应用程序内部的任何地方使用资源。(我们稍后会看到如何使用。)目前,脚本失败,因为它无法加载未生成的二进制文件。请耐心等待;我们很快就会进入有趣的部分。实际上就是现在。
设计主应用程序窗口
Glade 简介
首先,打开 Glade。

opensource.com
这是我们看到的
- 在左侧,有一个窗口小部件列表,可以将其拖放到中间部分。(您无法在标签窗口小部件内添加顶级窗口。)我将其称为窗口小部件部分。
- 中间部分包含我们的窗口小部件,它们将(大多数时候)出现在应用程序中。我将其称为设计部分。
- 右侧有两个子部分
- 顶部部分包含添加到资源中的窗口小部件的层次结构。我将其称为层次结构部分。
- 底部部分包含可以通过 Glade 为上面选择的窗口小部件配置的所有属性。我将其称为属性部分。
我将描述使用 Glade 构建本教程 UI 的步骤,但如果您对构建 GTK+ 应用程序感兴趣,则应查看该工具的官方 资源 和 教程。
创建应用程序窗口设计
让我们通过简单地将“窗口小部件”部分中的“应用程序窗口”窗口小部件拖到“设计”部分来创建应用程序窗口。

opensource.com
Gtk::Builder
是 GTK+ 应用程序中使用的对象,用于读取用户界面的文本描述(例如我们将通过 Glade 构建的描述)并构建描述的对象窗口小部件。
“属性”部分中的第一项是“ID”,它具有默认值 applicationWindow1
。如果我们将此属性保持原样,我们稍后将通过我们的代码创建一个 Gtk::Builder
,它将加载 Glade 生成的文件。为了获得应用程序窗口,我们将不得不使用类似这样的东西
application_window = builder.get_object('applicationWindow1')
application_window.signal_connect 'whatever' do |a,b|
...
application_window
对象将是 Gtk::ApplicationWindow
类;因此,我们必须添加到其行为中的任何内容(例如设置其标题)都将在原始类之外发生。此外,如上面的代码片段所示,连接到窗口信号的代码将放置在实例化它的文件中。
好消息是 GTK+ 在 2013 年引入了一项 功能,该功能允许创建复合窗口小部件模板,这些模板(除其他优点外)允许我们定义窗口小部件的自定义类(最终通常从现有的 GTK::Widget
类派生而来)。如果您感到困惑,请不要担心。在编写一些代码并查看结果后,您将了解正在发生的事情。
要将我们的设计定义为模板,请选中属性窗口小部件中的“复合”复选框。请注意,“ID”属性已更改为“类名”。填写 TodoApplicationWindow
。这是我们将在代码中创建的类来表示此窗口小部件。

opensource.com
将文件命名为 application_window.ui
,并保存在 resources
内名为 ui
的新文件夹中。如果我们从编辑器中打开文件,会看到以下内容
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<template class="TodoApplicationWindow" parent="GtkApplicationWindow">
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
</template>
</interface>
我们的窗口小部件有一个类和一个父属性。按照父类属性约定,我们的类必须在名为 Todo
的模块中定义。在开始之前,让我们尝试通过执行脚本 (./gtk-todo
) 来启动应用程序。
耶!它启动了!
创建应用程序窗口类
如果我们检查应用程序运行时应用程序根目录的内容,我们可以看到 gresource.bin
文件在那里。即使应用程序成功启动,因为资源 bin 存在并且可以注册,但我们尚未使用它。我们仍然会在 application.rb
文件中启动一个普通的 Gtk::ApplicationWindow
。现在是时候创建我们的自定义应用程序窗口类了。
在 application/ui/todo
文件夹中创建一个名为 application_window.rb
的文件,并添加以下内容
module Todo
class ApplicationWindow < Gtk::ApplicationWindow
# Register the class in the GLib world
type_register
class << self
def init
# Set the template from the resources binary
set_template resource: '/com/iridakos/gtk-todo/ui/application_window.ui'
end
end
def initialize(application)
super application: application
set_title 'GTK+ Simple ToDo'
end
end
end
我们在打开 eigenclass 后将 init
方法定义为类上的单例方法,以便将此窗口小部件的模板绑定到先前注册的资源文件。
在此之前,我们调用了 type_register
类方法,该方法注册我们的自定义窗口小部件类并使其可用于 GLib
世界。
最后,每次我们创建此窗口的实例时,我们都会将其标题设置为 GTK+ Simple ToDo
。
现在,让我们回到 application.rb
文件并使用我们刚刚实现的内容
module ToDo
class Application < Gtk::Application
def initialize
super 'com.iridakos.gtk-todo', Gio::ApplicationFlags::FLAGS_NONE
signal_connect :activate do |application|
window = Todo::ApplicationWindow.new(application)
window.present
end
end
end
end
执行脚本。

opensource.com
定义模型
为了简单起见,我们将以 JSON 格式将 ToDo 项目保存在用户主目录中一个专用的隐藏文件夹中。在实际应用程序中,我们将使用数据库,但这超出了本教程的范围。
我们的 Todo::Item
模型将具有以下属性
- id:项目的 id
- title:标题
- notes:任何注释
- priority:其优先级
- creation_datetime:项目创建的日期和时间
- filename:项目保存到的文件名
我们将在 application/models
目录下创建一个名为 item.rb
的文件,内容如下
require 'securerandom'
require 'json'
module Todo
class Item
PROPERTIES = [:id, :title, :notes, :priority, :filename, :creation_datetime].freeze
PRIORITIES = ['high', 'medium', 'normal', 'low'].freeze
attr_accessor *PROPERTIES
def initialize(options = {})
if user_data_path = options[:user_data_path]
# New item. When saved, it will be placed under the :user_data_path value
@id = SecureRandom.uuid
@creation_datetime = Time.now.to_s
@filename = "#{user_data_path}/#{id}.json"
elsif filename = options[:filename]
# Load an existing item
load_from_file filename
else
raise ArgumentError, 'Please specify the :user_data_path for new item or the :filename to load existing'
end
end
# Loads an item from a file
def load_from_file(filename)
properties = JSON.parse(File.read(filename))
# Assign the properties
PROPERTIES.each do |property|
self.send "#{property}=", properties[property.to_s]
end
rescue => e
raise ArgumentError, "Failed to load existing item: #{e.message}"
end
# Resolves if an item is new
def is_new?
!File.exists? @filename
end
# Saves an item to its `filename` location
def save!
File.open(@filename, 'w') do |file|
file.write self.to_json
end
end
# Deletes an item
def delete!
raise 'Item is not saved!' if is_new?
File.delete(@filename)
end
# Produces a json string for the item
def to_json
result = {}
PROPERTIES.each do |prop|
result[prop] = self.send prop
end
result.to_json
end
end
end
在这里我们定义了以下方法
- 初始化项目
- 通过在其中定义
:user_data_path
将其保存为“新”项目 - 通过定义要从中加载的
:filename
将其保存为“现有”项目。文件名必须是先前由项目生成的 JSON 文件
- 通过在其中定义
- 从文件加载项目
- 解决项目是新的还是旧的(即,是否至少在
:user_data_path
中保存过) - 通过将其 JSON 字符串写入文件来保存项目
- 删除项目
- 生成项目的 JSON 字符串作为其属性的哈希值
添加新项目
创建按钮
让我们在应用程序窗口中添加一个按钮,用于添加新项目。在 Glade 中打开 resources/ui/application_window.ui
文件。
- 将“窗口小部件”部分中的“按钮”拖到“设计”部分。
- 在“属性”部分中,将其ID值设置为
add_new_item_button
。 - 在“属性”部分“常规”选项卡的底部附近,在“带有可选图像的标签”选项下方有一个文本区域。将其值从Button更改为添加新项目。
- 保存文件并执行脚本。

opensource.com
别担心;我们稍后会改进设计。现在,让我们看看如何将功能连接到我们按钮的clicked事件。
首先,我们必须更新我们的应用程序窗口类,以便它了解它的新子项,即 ID 为 add_new_item_button
的按钮。然后我们可以访问子项以更改其行为。
按如下方式更改 init
方法
def init
# Set the template from the resources binary
set_template resource: '/com/iridakos/gtk-todo/ui/application_window.ui'
bind_template_child 'add_new_item_button'
end
非常简单,对吧?bind_template_child
方法完全按照它说的做,从现在开始,我们 Todo::ApplicationWindow
类的每个实例都将具有一个 add_new_item_button
方法来访问相关的按钮。因此,让我们按如下方式更改 initialize
方法
def initialize(application)
super application: application
set_title 'GTK+ Simple ToDo'
add_new_item_button.signal_connect 'clicked' do |button, application|
puts "OMG! I AM CLICKED"
end
end
如您所见,我们将通过 add_new_item_button
方法访问按钮,并且我们定义了单击按钮时要发生的事情。重新启动应用程序并尝试单击按钮。在控制台中,当您单击按钮时,您应该看到消息 OMG! I AM CLICKED
。
但是,我们希望在单击此按钮时发生的事情是显示一个新窗口来保存 ToDo 项目。您猜对了:现在是 Glade 时间。
创建新项目窗口
- 通过按顶部栏中最左侧的图标或从应用程序菜单中选择“文件 > 新建”在 Glade 中创建一个新项目。
- 从“窗口小部件”部分拖动“窗口”到“设计区域”。
- 检查其“复合”属性并将类命名为
TodoNewItemWindow
。

opensource.com
- 从“窗口小部件”部分拖动“网格”并将其放在我们先前添加的窗口中。
- 在弹出的窗口中设置 5 行和 2 列。
- 在“属性”部分的“常规”选项卡中,将行和列间距设置为 10(像素)。
- 在“属性”部分的“通用”选项卡中,将“窗口小部件间距 > 边距 > 上、下、左、右”全部设置为 10,以便内容不会粘在网格的边框上。

opensource.com
- 从“窗口小部件”部分拖动四个“标签”窗口小部件,并将每个窗口小部件放在网格的每一行中。
- 从上到下,按如下方式更改其“标签”属性
Id
标题
注释
优先级
- 在“属性”部分的“常规”选项卡中,将每个属性的“对齐和填充 > 对齐 > 水平”属性从 0.50 更改为 1,以便右对齐标签文本。
- 此步骤是可选的,但建议执行。我们不会将这些标签绑定到我们的窗口中,因为我们不需要更改其状态或行为。在这种情况下,我们不需要像为应用程序窗口中的
add_new_item_button
按钮那样为它们设置描述性 ID。但是我们将向我们的设计添加更多元素,如果它们显示label1
、label2
等,则 Glade 中窗口小部件的层次结构将难以阅读。设置描述性 ID(例如id_label
、title_label
、notes_label
、priority_label
)将使我们的生活更轻松。我甚至将网格的 ID 设置为main_grid
,因为我不喜欢在 ID 中看到数字或变量名。

opensource.com
- 从“窗口小部件”部分拖动一个“标签”到网格第一行的第二列。ID 将由我们的模型自动生成;我们不允许编辑,因此显示它的标签就足够了。
- 将“ID”属性设置为
id_value_label
。 - 将“对齐和填充 > 对齐 > 水平”属性设置为 0,以便文本在左侧对齐。
- 我们将此窗口小部件绑定到我们的窗口类,以便我们可以在每次加载窗口时更改其文本。因此,不需要通过 Glade 设置标签,但这会使设计更接近于使用实际数据呈现时的外观。您可以将标签设置为最适合您的内容;我将其设置为
id-of-the-todo-item-here
。

opensource.com
- 从“窗口小部件”部分拖动一个“文本条目”到网格第二行的第二列。
- 将其 ID 属性设置为
title_text_entry
。您可能已经注意到,我更喜欢在 ID 中获取窗口小部件类型,以使类中的代码更具可读性。 - 在“属性”部分的“通用”选项卡中,选中“窗口小部件间距 > 展开 > 水平”复选框,并打开旁边的开关。这样,每次调整其父项(又名网格)大小时,窗口小部件都会水平展开。

opensource.com
- 从“窗口小部件”部分拖动一个“文本视图”到网格第三行的第二列。
- 将其“ID”设置为
notes
。不,只是在测试您。将其“ID”属性设置为notes_text_view
。 - 在“属性”部分的“通用”选项卡中,选中“窗口小部件间距 > 展开 > 水平、垂直”复选框,并打开旁边的开关。这样,每次调整其父项(网格)大小时,窗口小部件都会水平和垂直展开。

opensource.com
- 从“窗口小部件”部分拖动一个“组合框”到网格第四行的第二列。
- 将其“ID”设置为
priority_combo_box
。 - 在“属性”部分的“通用”选项卡中,选中“窗口小部件间距 > 展开 > 水平”复选框,并打开其右侧的开关。这允许每次调整其父项(网格)大小时,窗口小部件都会水平展开。
- 此窗口小部件是一个下拉元素。我们将在窗口类中显示它时填充用户可以选择的值。

opensource.com
- 从“窗口小部件”部分拖动一个“按钮框”到网格最后一行的第二列。
- 在弹出窗口中,选择 2 个项目。
- 在“属性”部分的“常规”选项卡中,将“框属性 > 方向”属性设置为 水平。
- 在“属性”部分的“常规”选项卡中,将“框属性 > 间距”属性设置为 10。
- 在“属性”部分的“通用”选项卡中,将“窗口小部件间距 > 对齐 > 水平”设置为 居中。
- 同样,我们的代码不会更改此窗口小部件,但您可以为其提供描述性
ID
以提高可读性。我将其命名为actions_box
。

opensource.com
- 拖动两个“按钮”窗口小部件,并将一个窗口小部件放在我们在上一步中添加的按钮框窗口小部件的每个框中。
- 分别将其“ID”属性设置为
cancel_button
和save_button
。 - 在“属性”窗口的“常规”选项卡中,分别将其“按钮内容 > 带有可选图像的标签”属性设置为 取消 和 保存。

opensource.com
窗口已准备就绪。将文件保存在 resources/ui/new_item_window.ui
下。
现在是时候将其移植到我们的应用程序中了。
实现新项目窗口类
在实现新类之前,我们必须更新我们的 GResource
描述文件 (resources/gresources.xml
) 以获取新资源
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/iridakos/gtk-todo">
<file preprocess="xml-stripblanks">ui/application_window.ui</file>
<file preprocess="xml-stripblanks">ui/new_item_window.ui</file>
</gresource>
</gresources>
现在我们可以创建新的窗口类。在 application/ui/todo
下创建一个名为 new_item_window.rb
的文件,并将其内容设置为如下
module Todo
class NewItemWindow < Gtk::Window
# Register the class in the GLib world
type_register
class << self
def init
# Set the template from the resources binary
set_template resource: '/com/iridakos/gtk-todo/ui/new_item_window.ui'
end
end
def initialize(application)
super application: application
end
end
end
这里没有什么特别之处。我们只是更改了模板资源以指向我们资源的正确文件。
我们必须更改 add_new_item_button
代码,该代码在 clicked
信号上执行以显示新项目窗口。我们将继续在 application_window.rb
中将该代码更改为以下内容
add_new_item_button.signal_connect 'clicked' do |button|
new_item_window = NewItemWindow.new(application)
new_item_window.present
end
让我们看看我们做了什么。启动应用程序并单击 添加新项目 按钮。哒哒!

opensource.com
但是当我们按下按钮时,什么也没有发生。让我们修复它。
首先,我们将在 Todo::NewItemWindow
类中绑定 UI 窗口小部件。
将 init
方法更改为以下内容
def init
# Set the template from the resources binary
set_template resource: '/com/iridakos/gtk-todo/ui/new_item_window.ui'
# Bind the window's widgets
bind_template_child 'id_value_label'
bind_template_child 'title_text_entry'
bind_template_child 'notes_text_view'
bind_template_child 'priority_combo_box'
bind_template_child 'cancel_button'
bind_template_child 'save_button'
end
此窗口将在创建或编辑 ToDo 项目时显示,因此 new_item_window
命名不是很有效。我们稍后会重构它。
现在,我们将更新窗口的 initialize
方法,使其需要一个额外的参数,用于要创建或编辑的 Todo::Item
。然后我们可以设置更有意义的窗口标题并更改子窗口小部件以反映当前项目。
我们将 initialize
方法更改为以下内容
def initialize(application, item)
super application: application
set_title "ToDo item #{item.id} - #{item.is_new? ? 'Create' : 'Edit' } Mode"
id_value_label.text = item.id
title_text_entry.text = item.title if item.title
notes_text_view.buffer.text = item.notes if item.notes
# Configure the combo box
model = Gtk::ListStore.new(String)
Todo::Item::PRIORITIES.each do |priority|
iterator = model.append
iterator[0] = priority
end
priority_combo_box.model = model
renderer = Gtk::CellRendererText.new
priority_combo_box.pack_start(renderer, true)
priority_combo_box.set_attributes(renderer, "text" => 0)
priority_combo_box.set_active(Todo::Item::PRIORITIES.index(item.priority)) if item.priority
end
然后我们将在 application/models/item.rb
文件中的常量 PROPERTIES
下方添加常量 PRIORITIES
PRIORITIES = ['high', 'medium', 'normal', 'low'].freeze
我们在这里做了什么?
- 我们将窗口的标题设置为一个字符串,其中包含当前项目的 ID 和模式(取决于项目是正在创建还是正在编辑)。
- 我们将
id_value_label
文本设置为显示当前项目的 ID。 - 我们将
title_text_entry
文本设置为显示当前项目的标题。 - 我们将
notes_text_view
文本设置为显示当前项目的注释。 - 我们为
priority_combo_box
创建了一个模型,该模型的条目将只有一个String
值。乍一看,Gtk::ListStore
模型可能看起来有点令人困惑。以下是它的工作原理。- 假设我们想在组合框中显示国家代码列表及其各自的国家名称。
- 我们将创建一个
Gtk::ListStore
,定义其条目将包含两个字符串值:一个用于国家代码,另一个用于国家名称。因此,我们将 ListStore 初始化为:
model = Gtk::ListStore.new(String, String)
- 要使用数据填充模型,我们将执行以下操作(确保您不要错过代码片段中的注释):
[['gr', 'Greece'], ['jp','Japan'], ['nl', 'Netherlands']].each do |country_pair| entry = model.append # Each entry has two string positions since that's how we initialized the Gtk::ListStore # Store the country code in position 0 entry[0] = country_pair[0] # Store the country name in position 1 entry[1] = country_pair[1] end
- 我们还配置了组合框以呈现两个文本列/单元格(再次,确保您不要错过代码片段中的注释):
country_code_renderer = Gtk::CellRendererText.new # Add the first renderer combo.pack_start(country_code_renderer, true) # Use the value in index 0 of each model entry a.k.a. the country code combo.set_attributes(country_code_renderer, 'text' => 0) country_name_renderer = Gtk::CellRendererText.new # Add the second renderer combo.pack_start(country_name_renderer, true) # Use the value in index 1 of each model entry a.k.a. the country name combo.set_attributes(country_name_renderer, 'text' => 1)
- 我希望这能使它更清楚一些。
- 我们在组合框中添加了一个简单的文本渲染器,并指示它显示每个模型条目的唯一值(又名位置
0
)。想象一下,我们的模型类似于[['high'],['medium'],['normal'],['low']]
,而0
是每个子数组的第一个元素。我现在就停止模型-组合-文本渲染器解释……
配置用户数据路径
请记住,在初始化新的 Todo::Item
(而不是现有的 Todo::Item
)时,我们必须定义 :user_data_path
,它将在其中保存。我们将在应用程序启动时解析此路径,并使其可以从所有窗口小部件访问。
我们所要做的就是检查用户的 home ~
目录中是否存在 .gtk-todo-tutorial
路径。如果不存在,我们将创建它。然后,我们将其设置为应用程序的实例变量。所有窗口小部件都可以访问应用程序实例。因此,所有窗口小部件都可以访问此用户路径变量。
将 application/application.rb
文件更改为此
module ToDo
class Application < Gtk::Application
attr_reader :user_data_path
def initialize
super 'com.iridakos.gtk-todo', Gio::ApplicationFlags::FLAGS_NONE
@user_data_path = File.expand_path('~/.gtk-todo-tutorial')
unless File.directory?(@user_data_path)
puts "First run. Creating user's application path: #{@user_data_path}"
FileUtils.mkdir_p(@user_data_path)
end
signal_connect :activate do |application|
window = Todo::ApplicationWindow.new(application)
window.present
end
end
end
end
在测试我们目前所做的工作之前,我们需要做的最后一件事是,当点击 add_new_item_button
时,根据我们所做的更改来实例化 Todo::NewItemWindow
。换句话说,将 application_window.rb
中的代码更改为此
add_new_item_button.signal_connect 'clicked' do |button|
new_item_window = NewItemWindow.new(application, Todo::Item.new(user_data_path: application.user_data_path))
new_item_window.present
end
启动应用程序并点击 Add new item 按钮。 哒哒! (注意标题中的- Create mode 部分)。

opensource.com
取消项目创建/更新
要在用户点击 cancel_button
时关闭 Todo::NewItemWindow
窗口,我们只需要将其添加到窗口的 initialize
方法中
cancel_button.signal_connect 'clicked' do |button|
close
end
close
是 Gtk::Window
类的一个实例方法,用于关闭窗口。
保存项目
保存项目涉及两个步骤
- 根据控件的值更新项目的属性。
- 在
Todo::Item
实例上调用save!
方法。
同样,我们的代码将放在 Todo::NewItemWindow
的 initialize
方法中
save_button.signal_connect 'clicked' do |button|
item.title = title_text_entry.text
item.notes = notes_text_view.buffer.text
item.priority = priority_combo_box.active_iter.get_value(0) if priority_combo_box.active_iter
item.save!
close
end
再次,窗口在保存项目后关闭。
让我们试试看。

opensource.com
现在,通过按“保存”并导航到我们的 ~/.gtk-todo-tutorial
文件夹,我们应该看到一个文件。 我的文件内容如下
{
"id": "3d635839-66d0-4ce6-af31-e81b47b3e585",
"title": "Optimize the priorities model creation",
"notes": "It doesn't have to be initialized upon each window creation.",
"priority": "high",
"filename": "/home/iridakos/.gtk-todo-tutorial/3d635839-66d0-4ce6-af31-e81b47b3e585.json",
"creation_datetime": "2018-01-25 18:09:51 +0200"
}
不要忘记也尝试一下“取消”按钮。
查看待办事项
Todo::ApplicationWindow
仅包含一个按钮。 是时候改变这一点了。
我们希望窗口顶部有 Add new item,下面有一个列表,其中包含我们所有的待办事项。 我们将在我们的设计中添加一个 Gtk::ListBox
,它可以包含任意数量的行。
更新应用程序窗口
- 在 Glade 中打开
resources/ui/application_window.ui
文件。 - 如果我们将
List Box
控件从“Widget”部分直接拖到窗口上,则不会发生任何事情。 这是正常的。 首先,我们必须将窗口分成两部分:一部分用于按钮,一部分用于列表框。 请耐心等待。 - 在“Hierarchy”部分中右键单击
new_item_window
,然后选择Add parent > Box。 - 在弹出窗口中,将项目数设置为 2。
- 框的 orientation 已经是 vertical,所以我们很好。

opensource.com
- 现在,拖动一个
List Box
并将其放在先前添加的框的空闲区域上。 - 将其
ID
属性设置为todo_items_list_box
。 - 将其
Selection mode
设置为None
,因为我们不会提供该功能。

opensource.com
设计待办事项列表框行
我们在上一步中创建的列表框的每一行都将比文本行更复杂。 每个行都将包含允许用户展开项目注释以及删除或编辑项目的控件。
- 在 Glade 中创建一个新项目,就像我们为
new_item_window.ui
所做的那样。 将其保存在resources/ui/todo_item_list_box_row.ui
下。 - 不幸的是(至少在我的 Glade 版本中),“Widget”部分中没有 List Box Row 控件。 因此,我们将以一种有点 hackish 的方式将其添加为我们项目的顶级控件。
- 将
List Box
从“Widget”部分拖到“Design”区域。 - 在“Hierarchy”部分中,右键单击
List Box
并选择Add Row

opensource.com
- 在“Hierarchy”部分中,右键单击嵌套在
List Box
下的新添加的List Box Row
,然后选择Remove parent
。 就在那里! 现在List Box Row
是项目的顶级控件。

opensource.com
- 检查控件的
Composite
属性,并将其名称设置为TodoItemListBoxRow
。 - 将
Box
从“Widget”部分拖到List Box Row
内的“Design”区域。 - 在弹出窗口中设置 2 个项目。
- 将其
ID
属性设置为main_box
。

opensource.com
- 将另一个
Box
从“Widget”部分拖到先前添加的框的第一行。 - 在弹出窗口中设置 2 个项目。
- 将其
ID
属性设置为todo_item_top_box
。 - 将其Orientation 属性设置为 Horizontal。
- 将其
Spacing
(General 选项卡)属性设置为 10。

opensource.com
- 将
Label
从“Widget”部分拖到todo_item_top_box
的第一列。 - 将其
ID
属性设置为todo_item_title_label
。 - 将其Alignment and Padding > Alignment > Horizontal 属性设置为 0.00。
- 在“Properties”部分的Common 选项卡中,选中Widget Spacing > Expand > Horizontal 复选框,并打开旁边的开关,以便标签将扩展到可用空间。

opensource.com
- 将
Button
从“Widget”部分拖到todo_item_top_box
的第二列。 - 将其
ID
属性设置为details_button
。 - 选中Button Content > Label with optional image 单选按钮,然后键入
...
(三个点)。

opensource.com
- 将
Revealer
控件从“Widget”部分拖到main_box
的第二行。 - 在General 选项卡中关闭
Reveal Child
开关。 - 将其
ID
属性设置为todo_item_details_revealer
。 - 将其
Transition type
属性设置为Slide Down
。

opensource.com
- 将
Box
从“Widget”部分拖到 reveal 空间。 - 在弹出窗口中将其项目设置为 2。
- 将其
ID
属性设置为details_box
。 - 在Common 选项卡中,将其Widget Spacing > Margins > Top 属性设置为 10。

opensource.com
- 将
Button Box
从“Widget”部分拖到details_box
的第一行。 - 将其
ID
属性设置为todo_item_action_box
。 - 将其
Layout style
属性设置为expand
。

opensource.com
- 将
Button
控件拖到todo_item_action_box
的第一列和第二列。 - 分别将其
ID
属性设置为delete_button
和edit_button
。 - 分别将其Button Content > Label with optional image 属性设置为 Delete 和 Edit。

opensource.com
- 将
Viewport
控件从“Widget”部分拖到details_box
的第二行。 - 将其
ID
属性设置为todo_action_notes_viewport
。 - 将
Text View
控件从“Widget”部分拖到我们刚刚添加的todo_action_notes_viewport
。 - 将其
ID
设置为todo_item_notes_text_view
。 - 在“Properties”部分的“General”选项卡中,取消选中其
Editable
属性。

opensource.com
创建 ToDo 项目列表框行类
现在,我们将创建反映我们刚刚创建的列表框行的 UI 的类。
首先,我们必须更新我们的 GResource
描述文件,以包含新创建的设计。 将 resources/gresources.xml
文件更改如下
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/iridakos/gtk-todo">
<file preprocess="xml-stripblanks">ui/application_window.ui</file>
<file preprocess="xml-stripblanks">ui/new_item_window.ui</file>
<file preprocess="xml-stripblanks">ui/todo_item_list_box_row.ui</file>
</gresource>
</gresources>
在 application/ui
文件夹中创建一个名为 item_list_box_row.rb
的文件,并添加以下内容
module Todo
class ItemListBoxRow < Gtk::ListBoxRow
type_register
class << self
def init
set_template resource: '/com/iridakos/gtk-todo/ui/todo_item_list_box_row.ui'
end
end
def initialize(item)
super()
end
end
end
我们目前不会绑定任何子项。
启动应用程序时,我们必须在 :user_data_path
中搜索文件,并且必须为每个文件创建一个 Todo::Item
实例。 对于每个实例,我们还必须向 Todo::ApplicationWindow
的 todo_items_list_box
列表框添加一个新的 Todo::ItemListBoxRow
。 一次做一件事。
首先,让我们在 Todo::ApplicationWindow
类中绑定 todo_items_list_box
。 将 init
方法更改如下
def init
# Set the template from the resources binary
set_template resource: '/com/iridakos/gtk-todo/ui/application_window.ui'
bind_template_child 'add_new_item_button'
bind_template_child 'todo_items_list_box'
end
接下来,我们将在同一个类中添加一个实例方法,该方法将负责在相关的列表框中加载 ToDo 列表项。 在 Todo::ApplicationWindow
中添加此代码
def load_todo_items
todo_items_list_box.children.each { |child| todo_items_list_box.remove child }
json_files = Dir[File.join(File.expand_path(application.user_data_path), '*.json')]
items = json_files.map{ |filename| Todo::Item.new(filename: filename) }
items.each do |item|
todo_items_list_box.add Todo::ItemListBoxRow.new(item)
end
end
然后,我们将在 initialize
方法的末尾调用此方法
def initialize(application)
super application: application
set_title 'GTK+ Simple ToDo'
add_new_item_button.signal_connect 'clicked' do |button|
new_item_window = NewItemWindow.new(application, Todo::Item.new(user_data_path: application.user_data_path))
new_item_window.present
end
load_todo_items
end
注意: 我们必须首先清空列表框的当前子行,然后再重新填充它。 这样,我们将在通过 Todo::NewItemWindow
的 save_button
的 signal_connect
保存 Todo::Item
后调用此方法,并且父应用程序窗口将被重新加载! 这是更新后的代码(在 application/ui/new_item_window.rb
中)
save_button.signal_connect 'clicked' do |button|
item.title = title_text_entry.text
item.notes = notes_text_view.buffer.text
item.priority = priority_combo_box.active_iter.get_value(0) if priority_combo_box.active_iter
item.save!
close
# Locate the application window
application_window = application.windows.find { |w| w.is_a? Todo::ApplicationWindow }
application_window.load_todo_items
end
之前,我们使用此代码
json_files = Dir[File.join(File.expand_path(application.user_data_path), '*.json')]
查找应用程序用户数据路径中带有 JSON 扩展名的所有文件的名称。
让我们看看我们创建了什么。 启动应用程序并尝试添加新的 ToDo 项目。 按下 Save 按钮后,您应该看到父 Todo::ApplicationWindow
自动更新了新项目!

opensource.com
剩下要做的是完成 Todo::ItemListBoxRow
的功能。
首先,我们将绑定控件。 将 Todo::ItemListBoxRow
类的 init
方法更改如下
def init
set_template resource: '/com/iridakos/gtk-todo/ui/todo_item_list_box_row.ui'
bind_template_child 'details_button'
bind_template_child 'todo_item_title_label'
bind_template_child 'todo_item_details_revealer'
bind_template_child 'todo_item_notes_text_view'
bind_template_child 'delete_button'
bind_template_child 'edit_button'
end
然后,我们将根据每行的项目设置控件。
def initialize(item)
super()
todo_item_title_label.text = item.title || ''
todo_item_notes_text_view.buffer.text = item.notes
details_button.signal_connect 'clicked' do
todo_item_details_revealer.set_reveal_child !todo_item_details_revealer.reveal_child?
end
delete_button.signal_connect 'clicked' do
item.delete!
# Locate the application window
application_window = application.windows.find { |w| w.is_a? Todo::ApplicationWindow }
application_window.load_todo_items
end
edit_button.signal_connect 'clicked' do
new_item_window = NewItemWindow.new(application, item)
new_item_window.present
end
end
def application
parent = self.parent
parent = parent.parent while !parent.is_a? Gtk::Window
parent.application
end
- 如您所见,当单击
details_button
时,我们指示todo_item_details_revealer
切换其内容的可见性。 - 删除项目后,我们找到应用程序的
Todo::ApplicationWindow
以调用其load_todo_items
,就像我们在保存项目后所做的那样。 - 当单击编辑按钮时,我们创建一个新的
Todo::NewItemWindow
实例,并将项目作为当前项目传递。 效果很好! - 最后,为了到达列表框行的应用程序父级,我们定义了一个简单的实例方法
application
,该方法导航控件的父级,直到到达可以从中获取应用程序对象的窗口。
保存并运行应用程序。 就在那里!

opensource.com
这是一个非常长的教程,即使有很多项目我们尚未涵盖,但我认为我们最好在这里结束它。
长文,猫照片。

opensource.com
有用的链接
最初发布在 Lazarus Lazaridis 的博客 iridakos.com 上,并经许可重新发布。
6 条评论