ruby写应用程序_使用Ruby创建Linux桌面应用程序

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来检查版本。

RVM list screenshot

Glade:

Glade网站上 ,“ Glade是一种RAD工具,可以快速,轻松地开发GTK +工具箱和GNOME桌面环境的用户界面。”

我们将使用Glade设计应用程序的用户界面。 如果你是在Ubuntu上安装gladesudo 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 
First GTK+ Ruby screenshot
笔记:
  • 我们上面定义的应用程序对象(以及所有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和子文件夹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) 

问题在于,我们不需要放置在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 empty project screen

这是我们看到的:

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

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

创建应用程序窗口设计

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

Glade 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 。 这是我们将在代码中创建的代表该小部件的类。

Glade application window composite

将名称为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

执行脚本。

GTK+ ToDo window

定义模型

为简单起见,我们将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
  • 保存文件并执行脚本。
Add new item button in the application window

不用担心 我们将在以后改进设计。 现在,让我们看看如何功能连接到按钮的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
GTK+ ToDo new item window empty
  • 从“小部件”部分中拖动一个Grid并将其放置在我们先前添加的窗口中。
  • 在弹出的窗口中设置5行和2列。
  • 在“属性”部分的“ 常规”选项卡中,将行和列的间距设置为10 (像素)。
  • 在“属性”部分的“ 常用”选项卡中,将“ Widget Spacing > Margins > Top, Bottom, Left, Right全部设置为10 ,以使内容不会粘在网格的边界上。
GTK+ ToDo new item window with grid
  • 从“小部件”部分中拖动四个Label小部件,然后在网格的每一行中放置一个。
  • 从顶部到底部更改其“ Label属性,如下所示:
    • Id:
    • Title:
    • Notes:
    • Priority:
  • 在“属性”部分的“ 常规”选项卡中,将每个属性的“ 对齐和填充”>“对齐”>“水平”属性从0.50更改为1 ,以使标签文本右对齐。
  • 此步骤是可选的,但建议这样做。 我们不会在窗口中绑定这些标签,因为我们不需要更改它们的状态或行为。 在这种情况下,我们不需要像在应用程序窗口中的add_new_item_button按钮那样为它们设置描述性ID。 但是,我们将在设计中添加更多元素,如果Glade中的小部件说出label1label2等,则很难理解它们的层次结构。设置描述性ID(例如id_labeltitle_labelnotes_labelnotes_labelpriority_label )将使我们的生活更轻松。 我什至将网格的ID设置为main_grid因为我不喜欢看到ID中的数字或变量名。
GTK+ ToDo new item with grid and labels
  • 将“ Label从“小部件”部分拖到网格第一行的第二列。 该ID将由我们的模型自动生成; 我们不允许进行编辑,因此仅显示标签就足够了。
  • ID属性设置为id_value_label
  • 将“ 对齐方式和填充”>“对齐方式”>“水平”属性设置为0,以使文本在左侧对齐。
  • 我们将这个小部件绑定到我们的Window类,以便每次加载窗口时都可以更改其文本。 因此,不需要通过Glade设置标签,但这会使设计更接近使用实际数据呈现时的外观。 您可以根据自己的需要设置标签。 我在这里将我的代码设置为id-of-the-todo-item-here
GTK+ ToDo new item with grid and labels
  • 将“ Text Entry从“窗口小部件”部分拖到网格第二行的第二列。
  • 将其ID属性设置为title_text_entry 。 您可能已经注意到,我更喜欢在ID中获取小部件类型,以使该类中的代码更具可读性。
  • 在“属性”部分的“ 常用”选项卡中,选中“ Widget Spacing > Expand > Horizontal复选框,然后打开它旁边的开关。 这样,小部件将在每次调整其父级(也就是网格)大小时水平扩展。
GTK+ ToDo new item with grid and labels
  • 将“ Text View从“窗口小部件”部分拖到网格第三行的第二列。
  • 将其ID设置为notes 。 不,只是测试一下。 将其ID属性设置为notes_text_view
  • 在“属性”部分的“ 常用”选项卡中,选中“ Widget Spacing > Expand > Horizontal, Vertical复选框,然后打开它们旁边的开关。 这样,小部件每次调整其父级(网格)的大小时都会在水平和垂直方向上扩展。
GTK+ ToDo new item with grid and labels
  • 将“ Combo Box从“小部件”部分拖到网格第四行的第二列。
  • 将其ID设置为priority_combo_box
  • 在“属性”部分的“ 常用”选项卡中,选中“ Widget Spacing > Expand > Horizontal复选框,然后将开关打开到右侧。 这使小部件每次调整其父级(网格)的大小时都可以水平扩展。
  • 该小部件是一个下拉元素。 当窗口类中显示它时,我们将填充用户可以选择的值。
GTK+ ToDo new item with grid and labels
  • 将“ Button Box从“小部件”部分拖到网格最后一行的第二列。
  • 在弹出窗口中,选择2个项目。
  • 在“属性”部分的“ 常规”选项卡中,将“ 框属性”>“方向”属性设置为“ 水平”
  • 在“属性”部分的“ 常规”选项卡中,将“ 框属性”>“间距”属性设置为10
  • 在“属性”部分的“ 常用”选项卡中,将“ 小部件间距”>“对齐”>“水平”设置为“ 居中”
  • 同样,我们的代码不会更改此小部件,但是您可以为它提供一个描述性ID以提高可读性。 我将其命名为mine actions_box
GTK+ ToDo new item with grid and labels
  • 拖动两个Button小部件,并在上一步中添加的按钮框小部件的每个框中放置一个。
  • 将其ID属性分别设置为cancel_buttonsave_button
  • 在“属性”窗口的“ 常规”选项卡中,将其“ 按钮内容”>“带有选项图像的标签”图像属性分别设置为“ 取消”和“ 保存”
GTK+ ToDo new item with grid and labels

窗口已准备就绪。 将文件保存在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

但是当我们按下按钮时什么也没有发生。 让我们修复它。

首先,我们将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

然后,我们将添加常数PRIORITIESapplication/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

启动应用程序,然后单击添加新项按钮。 多田 (请注意标题中的-创建模式部分)。

New item window
取消项目创建/更新

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



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

close是关闭窗口的Gtk::Window类的实例方法。

保存物品

保存项目涉及两个步骤:

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

同样,我们的代码将放置在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

现在,通过~/.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
  • 盒子的方向已经是垂直的,所以我们很好。
View todo items
  • 现在,将一个List Box拖到先前添加的框的空闲区域。
  • 将其ID属性设置为todo_items_list_box
  • 将其Selection mode设置为None因为我们将不提供该功能。
View todo items
设计待办事项项目列表框行

我们在上一步中创建的列表框的每一行都比一行文本要复杂。 每个控件都将包含小部件,这些小部件使用户可以展开项目的注释并删除或编辑该项目。

  • 像在new_item_window.ui一样,在Glade中创建一个新项目。 将其保存在resources/ui/todo_item_list_box_row.ui
  • 不幸的是(至少在我的Glade版本中),“小部件”部分中没有“列表框行”小部件。 因此,我们将以一种有点怪异的方式将其中一个添加为项目的顶级小部件。
  • List Box从“小部件”部分拖到“设计”区域。
  • 在“层次结构”部分中,右键单击“ List Box然后选择“ Add Row
View todo items
  • 在“层次结构”部分中,右键单击嵌套在List Box下方的新添加的List Box Row然后选择Remove parent 。 在那里! List Box Row现在是项目的顶级窗口小部件。
View todo items
  • 检查窗口小部件的Composite属性,并将其名称设置为TodoItemListBoxRow
  • 将一个Box从“小部件”部分拖到“ List Box Row内的“设计”区域。
  • 在弹出窗口中设置2个项目。
  • 将其ID属性设置为main_box
View todo items
  • 将另一个“ Box从“小部件”部分拖到先前添加的盒子的第一行。
  • 在弹出窗口中设置2个项目。
  • 将其ID属性设置为todo_item_top_box
  • 将其Orientation属性设置为Horizo​​ntal
  • 将其“ Spacing (“ 常规”选项卡)属性设置为10
View todo items
  • 将“ Label从“ todo_item_top_box小部件”部分todo_item_top_box的第一列。
  • 将其ID属性设置为todo_item_title_label
  • 将其“ 对齐和填充”>“对齐”>“水平”属性设置为0.00
  • 在“属性”部分的“ 常用”选项卡中,选中“ 小部件间距”>“展开”>“水平”复选框,然后打开其旁边的开关,以便标签将扩展到可用空间。
View todo items
  • 将“ Button从“小部件”部分todo_item_top_box的第二列。
  • 将其ID属性设置为details_button
  • 检查“ 按钮内容”>“带有可选图像收音机的标签”,然后键入... (三个点)。
View todo items
  • Revealer小部件从“小部件”部分拖动到main_box的第二行。
  • 关闭 常规选项卡中的Reveal Child开关。
  • 将其ID属性设置为todo_item_details_revealer
  • 将其Transition type属性设置为Slide Down
View todo items
  • 将一个Box从“小部件”部分拖到显示空间。
  • 在弹出窗口中将其项目设置为2
  • 将其ID属性设置为details_box
  • 在“ 常用”选项卡中,将其“ 窗口小部件间距”>“边距”>“顶部”属性设置为10
View todo items
  • 将“ Button Box从“小部件”部分拖动到details_box的第一行。
  • 将其ID属性设置为todo_item_action_box
  • 将其Layout style属性设置为expand
View todo items
  • Button窗口小部件拖动到todo_item_action_box的第一和第二列。
  • 将其ID属性分别设置为delete_buttonedit_button
  • 将其“ 按钮内容”>“带有可选图像属性的标签”分别设置为“ 删除”和“ 编辑”
View todo items
  • 将“ 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 ”属性。
View todo items

创建待办事项列表框行类

现在,我们将创建反映刚创建的列表框行的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::ApplicationWindowtodo_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自动用新项目更新了!

View todo items

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

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

Completed app

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

长发,猫照片。

Cat

该文件最初发布在Lazarus Lazaridis的博客iridakos.com上 ,并经许可重新发布。

翻译自: https://opensource.com/article/18/4/creating-linux-desktop-application-ruby

ruby写应用程序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值