使用 Ruby 创建 Linux 桌面应用程序

使用 GTK+ Ruby 绑定构建一个简单的 ToDo 列表应用程序。
345 位读者喜欢这个。
penguins

Internet Archive Book Images。由 Opensource.com 修改。CC BY-SA 4.0

最近,在尝试 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 检查您的版本。

RVM list screenshot

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

First GTK+ Ruby screenshot

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 的文件夹和子文件夹 uimodelslib

  • 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 库实用程序,名为 glib-compile-resources。使用 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 empty project screen

opensource.com

这是我们看到的

  • 左侧是一个小部件列表,可以拖放到中间部分。(您不能在标签小部件内添加顶级窗口。)我将此称为小部件部分
  • 中间部分包含我们的小部件,它们将(大多数时候)出现在应用程序中。我将此称为设计部分
  • 右侧有两个子部分
    • 顶部部分包含小部件添加到资源中的层次结构。我将此称为层次结构部分
    • 底部部分包含可以通过 Glade 为上面选择的小部件配置的所有属性。我将此称为属性部分

我将描述使用 Glade 构建本教程 UI 的步骤,但如果您有兴趣构建 GTK+ 应用程序,您应该查看该工具的官方 资源教程

创建应用程序窗口设计

让我们通过简单地将“小部件”部分中的“应用程序窗口”小部件拖到“设计”部分来创建应用程序窗口。

Glade application window

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。这是我们将在代码中创建的类,用于表示此小部件。

Glade application window composite

opensource.com

将文件保存为 resources 内的 ui 新文件夹中的 application_window.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

我们在打开特征类后将 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

执行脚本。

GTK+ ToDo window

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 作为“new”,稍后将在其中保存
    • 通过定义要从中加载的 :filename 作为“existing”。文件名必须是先前由项目生成的 JSON 文件
  • 从文件加载项目
  • 解析项目是新的还是不是新的(即,是否至少在 :user_data_path 中保存过一次)
  • 通过将其 JSON 字符串写入文件来保存项目
  • 删除项目
  • 生成项目的 JSON 字符串作为其属性的哈希

添加新项目

创建按钮

让我们在我们的应用程序窗口中添加一个按钮来添加新项目。在 Glade 中打开 resources/ui/application_window.ui 文件。

  • 将一个 Button 从“小部件”部分拖到“设计”部分。
  • 在“属性”部分中,将其ID 值设置为 add_new_item_button
  • 在“属性”部分的“常规”选项卡底部附近,在“带有可选图像的标签”选项下方有一个文本区域。将其值从“Button”更改为“添加新项目”。
  • 保存文件并执行脚本。

Add new item button in the application window

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

GTK+ ToDo new item window empty

opensource.com

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

GTK+ ToDo new item window with grid

opensource.com

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

GTK+ ToDo new item with grid and labels

opensource.com

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

GTK+ ToDo new item with grid and labels

opensource.com

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

GTK+ ToDo new item with grid and labels

opensource.com

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

GTK+ ToDo new item with grid and labels

opensource.com

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

GTK+ ToDo new item with grid and labels

opensource.com

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

GTK+ ToDo new item with grid and labels

opensource.com

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

GTK+ ToDo new item with grid and labels

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

这里没有什么特别的。我们只是更改了模板资源以指向我们资源的正确文件。

我们必须更改在 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

让我们看看我们做了什么。启动应用程序并单击“添加新项目”按钮。哒哒!

GTK+ ToDo new item with grid and labels

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(而不是现有的项目)时,我们必须定义要保存它的 :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

启动应用程序并单击“添加新项目”按钮。哒哒!(请注意标题中的“- 创建模式”部分)。

New item window

opensource.com

取消项目创建/更新

要在用户单击 cancel_button 时关闭 Todo::NewItemWindow 窗口,我们只需将其添加到窗口的 initialize 方法中

cancel_button.signal_connect 'clicked' do |button|
  close
end

closeGtk::Window 类的实例方法,用于关闭窗口。

保存项目

保存项目涉及两个步骤

  • 根据小部件的值更新项目的属性。
  • Todo::Item 实例上调用 save! 方法。

同样,我们的代码将放置在 Todo::NewItemWindowinitialize 方法中

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

再次,窗口在保存项目后关闭。

让我们试一试。

New item window

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 项目

Todo::ApplicationWindow 仅包含一个按钮。现在是时候改变这一点了。

我们希望窗口顶部有“添加新项目”,下面有一个列表,其中包含我们所有的 ToDo 项目。我们将向我们的设计添加一个 Gtk::ListBox,它可以包含任意数量的行。

更新应用程序窗口

  • 在 Glade 中打开 resources/ui/application_window.ui 文件。
  • 如果我们直接将“列表框”小部件从“小部件”部分拖到窗口上,则不会发生任何事情。这是正常的。首先,我们必须将窗口分成两部分:一部分用于按钮,一部分用于列表框。请耐心等待。
  • 右键单击“层次结构”部分中的 new_item_window,然后选择“添加父项 > 框”。
  • 在弹出窗口中,将项目数设置为 2
  • 框的方向已经是垂直的,所以我们很好。

View todo items

opensource.com

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

View todo items

opensource.com

设计 ToDo 项目列表框行

我们在上一步创建的列表框的每一行都将比文本行更复杂。每一行都将包含允许用户展开项目注释以及删除或编辑项目的微件。

  • 在 Glade 中创建一个新项目,就像我们为 new_item_window.ui 所做的那样。将其保存在 resources/ui/todo_item_list_box_row.ui 下。
  • 不幸的是(至少在我的 Glade 版本中),微件部分中没有列表框行微件。因此,我们将以一种有点 hack 的方式将其作为我们项目的顶层微件添加。
  • 从微件部分拖拽一个 List Box 到设计区域。
  • 在层级部分中,右键单击 List Box 并选择 Add Row(添加行)

View todo items

opensource.com

  • 在层级部分中,右键单击新添加的嵌套在 List Box 下的 List Box Row,然后选择 Remove parent(移除父项)。它就在那里!List Box Row 现在是项目的顶层微件了。

View todo items

opensource.com

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

View todo items

opensource.com

  • 从微件部分拖拽另一个 Box 到之前添加的框的第一行。
  • 在弹出窗口中设置 2 个项目。
  • 将其 ID 属性设置为 todo_item_top_box
  • 将其 Orientation(方向)属性设置为 Horizontal(水平)。
  • 将其 Spacing(间距)(General(常规)选项卡)属性设置为 10

View todo items

opensource.com

  • 从微件部分拖拽一个 Labeltodo_item_top_box 的第一列。
  • 将其 ID 属性设置为 todo_item_title_label
  • 将其 Alignment and Padding > Alignment > Horizontal(对齐和填充 > 对齐 > 水平)属性设置为 0.00
  • 在属性部分的 Common(通用)选项卡中,选中 Widget Spacing > Expand > Horizontal(微件间距 > 展开 > 水平)复选框,并打开旁边的开关,以便标签将扩展到可用空间。

View todo items

opensource.com

  • 从微件部分拖拽一个 Buttontodo_item_top_box 的第二列。
  • 将其 ID 属性设置为 details_button
  • 选中 Button Content > Label with optional image(按钮内容 > 带可选图像的标签)单选按钮,并键入 ...(三个点)。

View todo items

opensource.com

  • 从微件部分拖拽一个 Revealer 微件到 main_box 的第二行。
  • General(常规)选项卡中,关闭 Reveal Child(显示子项)开关。
  • 将其 ID 属性设置为 todo_item_details_revealer
  • 将其 Transition type(过渡类型)属性设置为 Slide Down(向下滑动)。

View todo items

opensource.com

  • 从微件部分拖拽一个 Box 到显示空间。
  • 在弹出窗口中将其项目设置为 2
  • 将其 ID 属性设置为 details_box
  • Common(通用)选项卡中,将其 Widget Spacing > Margins > Top(微件间距 > 边距 > 顶部)属性设置为 10

View todo items

opensource.com

  • 从微件部分拖拽一个 Button Boxdetails_box 的第一行。
  • 将其 ID 属性设置为 todo_item_action_box
  • 将其 Layout style(布局样式)属性设置为 expand(展开)。

View todo items

opensource.com

  • 拖拽 Button 微件到 todo_item_action_box 的第一列和第二列。
  • 分别将其 ID 属性设置为 delete_buttonedit_button
  • 分别将其 Button Content > Label with optional image(按钮内容 > 带可选图像的标签)属性设置为 Delete(删除)和 Edit(编辑)。

View todo items

opensource.com

  • 从微件部分拖拽一个 Viewport 微件到 details_box 的第二行。
  • 将其 ID 属性设置为 todo_action_notes_viewport
  • 从微件部分拖拽一个 Text View 微件到我们刚刚添加的 todo_action_notes_viewport
  • 将其 ID 设置为 todo_item_notes_text_view
  • 在属性部分的 General(常规)选项卡中,取消选中其 Editable(可编辑)属性。

View todo items

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::ItemListBoxRow 添加到 Todo::ApplicationWindowtodo_items_list_box 列表框。一次处理一件事。

首先,让我们在 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::NewItemWindowsave_buttonsignal_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 自动更新了新项目!

View todo items

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,它遍历微件的父项,直到到达可以从中获取应用程序对象的窗口。

保存并运行应用程序。它就在那里!

Completed app

opensource.com

这是一个非常长的教程,即使还有很多项目我们没有涵盖,但我认为我们最好在这里结束它。

长文,猫咪照片。

Cat

opensource.com

这最初发表在 Lazarus Lazaridis 的博客 iridakos.com 上,并已获得许可转载。

标签
User profile image.
我是一名软件开发人员。我曾在雅典经济与商业大学学习计算机科学,居住在希腊雅典。我通常使用 <strong>Ruby</strong> 编码,尤其是在 Rails 上,但我也精通 Java、Go、bash 和 C#。我热爱开源,喜欢编写教程和创建工具和实用程序。

6 条评论

这非常酷。你能说说它的跨平台性如何吗?例如,有可能在 OS/X 或 Windows 上运行它吗?

谢谢。不幸的是,我还没有在其他平台上尝试过。但是 GTK+ 适用于 Windows 和 Mac OS,因此如果 Ruby 绑定安装成功,这可能会起作用。如果我尝试了,我会回复您 ;)

回复 作者 pilhuhn

我还没有尝试过,所以这只是推测,但根据我使用其他类似设置(特别是 Python+wxPython)的经验,与 OS X 的兼容性还可以,但需要大量手动干预。OS X 的 Ruby 版本可能滞后,您需要为 GTK 版本找到可靠的安装程序,而这反过来又需要 Xcode。我敢打赌这是可能的,但要做好为此工作的准备,并且更新可能会破坏某些东西。Windows(再次,主要根据 wxPython 经验来说)更容易。

</推测>

回复 作者 iridakos

非常感谢!对我来说非常有用的教程。

多么棒的教程。谢谢,我迫不及待想试试!

© . All rights reserved.