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

Glade:
在Glade网站上 ,“ Glade是一种RAD工具,可以快速,轻松地开发GTK +工具箱和GNOME桌面环境的用户界面。”
我们将使用Glade设计应用程序的用户界面。 如果你是在Ubuntu上安装glade
用sudo apt install glade
。
GTK3宝石:
该gem为GTK +工具箱提供了Ruby绑定。 换句话说,它使我们可以使用Ruby语言与GTK + API对话。
使用gem install gtk3
。
定义应用规格
我们将在本教程中构建的应用程序将:
- 具有用户界面(即桌面应用程序)
- 允许用户为每个项目设置其他属性(例如,优先级)
- 允许用户创建和编辑待办事项
- 所有项目都将作为文件保存在用户主目录中名为
.gtk-todo-tutorial
的文件夹中
- 所有项目都将作为文件保存在用户主目录中名为
- 允许用户存档待办事项
- 归档项目应放在自己的文件夹中,称为“
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

笔记:
- 我们上面定义的应用程序对象(以及所有GTK +小部件)发出触发事件的信号。 例如,一旦应用程序开始运行,它就会发出信号来触发
activate
事件。 我们要做的就是定义发射该信号时想要发生的事情。 我们通过使用signal_connect
实例方法并向其传递一个将在给定事件中执行其代码的块来实现此signal_connect
。 在整个教程中,我们将做很多事情。 - 当我们初始化
Gtk::Application
对象时,我们传递了两个参数:-
com.iridakos.gtk-todo
:这是我们应用程序的ID,通常应为反向DNS样式标识符。 您可以在GNOME Wiki上了解有关其用法和最佳实践的更多信息。 -
:flags_none
:此标志定义应用程序的行为。 我们使用默认行为。 检查所有标志及其定义的应用程序类型。 我们可以使用Gio::ApplicationFlags.constants
定义的等效于Ruby的标志。 例如,代替使用:flags_none
,我们可以使用Gio::ApplicationFlags::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)
问题在于,我们不需要放置在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。

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

Gtk::Builder
是GTK +应用程序中使用的对象,用于读取用户界面的文本描述(如我们将通过Glade构建的描述)并构建描述的对象小部件。
“属性”部分中的第一件事是ID
,它具有默认值applicationWindow1
。 如果我们将此属性保持Gtk::Builder
,则稍后我们将通过代码创建一个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
类中派生) 。 如果您感到困惑,请不要担心。 在我们编写一些代码并查看结果之后,您将了解发生了什么。
要将我们的设计定义为模板,请选中属性窗口小部件中的Composite
复选框。 注意, ID
属性更改为Class Name
。 填写TodoApplicationWindow
。 这是我们将在代码中创建的代表该小部件的类。

将名称为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
看到gresource.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
打开本征类后,我们将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
执行脚本。

定义模型
为简单起见,我们将ToDo项目以JSON格式的文件保存在用户主目录下的专用隐藏文件夹下。 在实际的应用程序中,我们将使用数据库,但这超出了本教程的范围。
我们的Todo::Item
模型将具有以下属性:
- id :商品的ID
- title :标题
- 笔记 :任何笔记
- 优先级 :其优先级
- 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
作为“ existing”。 文件名必须是项目先前生成的JSON文件
- 通过定义
- 从文件加载项目
- 解析项目是否是新的(即,是否在
:user_data_path
中至少保存了一次) - 通过将项目的JSON字符串写入文件来保存项目
- 删除项目
- 生成项目的JSON字符串作为其属性的哈希值
新增项目
创建按钮
让我们在应用程序窗口中添加一个按钮以添加新项目。 在Glade中打开resources/ui/application_window.ui
文件。
- 将
Button
从“窗口小部件”部分拖到“设计”部分。 - 在“属性”部分中,将其ID值设置为
add_new_item_button
。 - 在“属性”部分的“ 常规”选项卡底部附近,“ 带有可选图像的标签”选项正下方有一个文本区域。 将其值从Button更改为Add new item 。
- 保存文件并执行脚本。

不用担心 我们将在以后改进设计。 现在,让我们看看如何将功能连接到按钮的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
单击按钮时, OMG! I AM CLICKED
。
但是,当我们单击此按钮时,我们想要发生的是显示一个新窗口来保存待办事项。 您猜对了:现在是Glade。
创建新项目窗口
- 通过按顶部栏中最左侧的图标或从应用程序菜单中选择“ 文件”>“新建” ,在Glade中创建一个新项目。
- 将
Window
从“Window
小部件”部分拖到“设计”区域。 - 检查其
Composite
属性,并将类命名为TodoNewItemWindow
。

- 从“小部件”部分中拖动一个
Grid
并将其放置在我们先前添加的窗口中。 - 在弹出的窗口中设置5行和2列。
- 在“属性”部分的“ 常规”选项卡中,将行和列的间距设置为10 (像素)。
- 在“属性”部分的“ 常用”选项卡中,将“
Widget Spacing > Margins > Top, Bottom, Left, Right
全部设置为10 ,以使内容不会粘在网格的边界上。

- 从“小部件”部分中拖动四个
Label
小部件,然后在网格的每一行中放置一个。 - 从顶部到底部更改其“
Label
属性,如下所示:-
Id:
-
Title:
-
Notes:
-
Priority:
-
- 在“属性”部分的“ 常规”选项卡中,将每个属性的“ 对齐和填充”>“对齐”>“水平”属性从0.50更改为1 ,以使标签文本右对齐。
- 此步骤是可选的,但建议这样做。 我们不会在窗口中绑定这些标签,因为我们不需要更改它们的状态或行为。 在这种情况下,我们不需要像在应用程序窗口中的
add_new_item_button
按钮那样为它们设置描述性ID。 但是,我们将在设计中添加更多元素,如果Glade中的小部件说出label1
,label2
等,则很难理解它们的层次结构。设置描述性ID(例如id_label
,title_label
,notes_label
,notes_label
,priority_label
)将使我们的生活更轻松。 我什至将网格的ID设置为main_grid
因为我不喜欢看到ID中的数字或变量名。

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

- 将“
Text Entry
从“窗口小部件”部分拖到网格第二行的第二列。 - 将其ID属性设置为
title_text_entry
。 您可能已经注意到,我更喜欢在ID中获取小部件类型,以使该类中的代码更具可读性。 - 在“属性”部分的“ 常用”选项卡中,选中“
Widget Spacing > Expand > Horizontal
复选框,然后打开它旁边的开关。 这样,小部件将在每次调整其父级(也就是网格)大小时水平扩展。

- 将“
Text View
从“窗口小部件”部分拖到网格第三行的第二列。 - 将其
ID
设置为notes
。 不,只是测试一下。 将其ID
属性设置为notes_text_view
。 - 在“属性”部分的“ 常用”选项卡中,选中“
Widget Spacing > Expand > Horizontal, Vertical
复选框,然后打开它们旁边的开关。 这样,小部件每次调整其父级(网格)的大小时都会在水平和垂直方向上扩展。

- 将“
Combo Box
从“小部件”部分拖到网格第四行的第二列。 - 将其
ID
设置为priority_combo_box
。 - 在“属性”部分的“ 常用”选项卡中,选中“
Widget Spacing > Expand > Horizontal
复选框,然后将开关打开到右侧。 这使小部件每次调整其父级(网格)的大小时都可以水平扩展。 - 该小部件是一个下拉元素。 当窗口类中显示它时,我们将填充用户可以选择的值。

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

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

窗口已准备就绪。 将文件保存在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
这里没什么特别的。 我们只是将模板资源更改为指向资源的正确文件。
我们必须更改在clicked
信号上执行的add_new_item_button
代码以显示新项目窗口。 我们将继续将application_window.rb
中的代码更改为:
add_new_item_button.
signal_connect
'clicked'
do
| button
|
new_item_window = NewItemWindow.
new
( application
)
new_item_window.
present
end
让我们看看我们做了什么。 启动应用程序,然后单击添加新项按钮。 多田

但是当我们按下按钮时什么也没有发生。 让我们修复它。
首先,我们将UI小部件绑定在Todo::NewItemWindow
类中。
将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
创建或编辑待办事项时将显示此窗口,因此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
然后,我们将添加常数PRIORITIES
在application/models/item.rb
文件刚刚下PROPERTIES
不变:
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
是每个子数组的第一个元素。 我现在将停止对model-combo-text-renderer的解释……
配置用户数据路径
请记住,在初始化新的Todo::Item
(不是现有的)时,我们必须定义一个:user_data_path
,将其保存在其中。 当应用程序启动时,我们将解析此路径,并使其可从所有小部件访问。
所有我们要做的是,如果检查.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
启动应用程序,然后单击添加新项按钮。 多田 (请注意标题中的-创建模式部分)。

取消项目创建/更新
要在用户单击cancel_button
时关闭Todo::NewItemWindow
窗口,我们只需将其添加到窗口的initialize
方法中:
cancel_button.
signal_connect
'clicked'
do
| button
|
close
end
close
是关闭窗口的Gtk::Window
类的实例方法。
保存物品
保存项目涉及两个步骤:
- 根据窗口小部件的值更新项目的属性。
- 致电
save!
Todo::Item
实例上的方法。
同样,我们的代码将放置在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
再次,保存项目后窗口关闭。
让我们尝试一下。

现在,通过~/.gtk-todo-tutorial
保存并导航到我们的~/.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
仅包含一个按钮。 现在该改变它了。
我们希望窗口在顶部具有“ 添加新项 ”,并在下面具有所有“待办事项”项的列表。 我们将在我们的设计中添加一个Gtk::ListBox
,它可以包含任意数量的行。
更新应用程序窗口
- 在Glade中打开
resources/ui/application_window.ui
文件。 - 如果直接从窗口的“窗口小部件”部分拖动
List Box
窗口小部件,则不会发生任何事情。 那很正常。 首先,我们必须将窗口分为两部分:一个用于按钮,另一个用于列表框。 忍受我 - 右键单击“层次结构”部分中的
new_item_window
,然后选择添加父项>框 。 - 在弹出窗口中,将项目数设置为2 。
- 盒子的方向已经是垂直的,所以我们很好。

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

设计待办事项项目列表框行
我们在上一步中创建的列表框的每一行都比一行文本要复杂。 每个控件都将包含小部件,这些小部件使用户可以展开项目的注释并删除或编辑该项目。
- 像在
new_item_window.ui
一样,在Glade中创建一个新项目。 将其保存在resources/ui/todo_item_list_box_row.ui
。 - 不幸的是(至少在我的Glade版本中),“小部件”部分中没有“列表框行”小部件。 因此,我们将以一种有点怪异的方式将其中一个添加为项目的顶级小部件。
- 将
List Box
从“小部件”部分拖到“设计”区域。 - 在“层次结构”部分中,右键单击“
List Box
然后选择“Add Row

- 在“层次结构”部分中,右键单击嵌套在
List Box
下方的新添加的List Box Row
然后选择Remove parent
。 在那里!List Box Row
现在是项目的顶级窗口小部件。

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

- 将另一个“
Box
从“小部件”部分拖到先前添加的盒子的第一行。 - 在弹出窗口中设置2个项目。
- 将其
ID
属性设置为todo_item_top_box
。 - 将其Orientation属性设置为Horizontal 。
- 将其“
Spacing
(“ 常规”选项卡)属性设置为10 。

- 将“
Label
从“todo_item_top_box
小部件”部分todo_item_top_box
的第一列。 - 将其
ID
属性设置为todo_item_title_label
。 - 将其“ 对齐和填充”>“对齐”>“水平”属性设置为0.00 。
- 在“属性”部分的“ 常用”选项卡中,选中“ 小部件间距”>“展开”>“水平”复选框,然后打开其旁边的开关,以便标签将扩展到可用空间。

- 将“
Button
从“小部件”部分todo_item_top_box
的第二列。 - 将其
ID
属性设置为details_button
。 - 检查“ 按钮内容”>“带有可选图像收音机的标签”,然后键入
...
(三个点)。

- 将
Revealer
小部件从“小部件”部分拖动到main_box
的第二行。 - 关闭 常规选项卡中的
Reveal Child
开关。 - 将其
ID
属性设置为todo_item_details_revealer
。 - 将其
Transition type
属性设置为Slide Down
。

- 将一个
Box
从“小部件”部分拖到显示空间。 - 在弹出窗口中将其项目设置为2 。
- 将其
ID
属性设置为details_box
。 - 在“ 常用”选项卡中,将其“ 窗口小部件间距”>“边距”>“顶部”属性设置为10 。

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

- 将
Button
窗口小部件拖动到todo_item_action_box
的第一和第二列。 - 将其
ID
属性分别设置为delete_button
和edit_button
。 - 将其“ 按钮内容”>“带有可选图像属性的标签”分别设置为“ 删除”和“ 编辑” 。

- 将“
Viewport
窗口小部件从“窗口小部件”部分拖动到details_box
的第二行。 - 将其
ID
属性设置为todo_action_notes_viewport
。 - 将“
Text View
窗口小部件从“窗口小部件”部分todo_action_notes_viewport
我们刚刚添加的todo_action_notes_viewport
中。 - 将其
ID
设置为todo_item_notes_text_view
。 - 在“属性”部分的“
General
选项卡中取消选中其“Editable
”属性。

创建待办事项列表框行类
现在,我们将创建反映刚创建的列表框行的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::Item
通过signal_connect
中的save_button
的的Todo::NewItemWindow
,和父应用程序窗口将加载! 这是更新的代码(在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项目。 按下保存按钮后,您应该看到父Todo::ApplicationWindow
自动用新项目更新了!

剩下的就是完成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
,该application
在小部件的父级中导航,直到到达一个可以从中获取应用程序对象的窗口为止。
保存并运行该应用程序。 在那里!

这是一个很长的教程,即使我们没有涉及很多内容,我还是认为最好在这里结束。
长发,猫照片。

有用的链接
该文件最初发布在Lazarus Lazaridis的博客iridakos.com上 ,并经许可重新发布。
翻译自: https://opensource.com/article/18/4/creating-linux-desktop-application-ruby
ruby写应用程序