GNU/Linux >> Znalost Linux >  >> Linux

Vytvořte linuxovou desktopovou aplikaci s Ruby

Nedávno, když jsem experimentoval s GTK a jeho Ruby vazbami, rozhodl jsem se napsat tutoriál, který tuto funkcionalitu představí. V tomto příspěvku vytvoříme jednoduchou aplikaci ToDo (něco jako to, co jsme vytvořili s Ruby on Rails) pomocí gtk3 drahokam (také znám jako vázání GTK+ Ruby).

Kód výukového programu najdete na GitHubu.

Co je GTK+?

Podle webu GTK+:

GTK+, neboli GIMP Toolkit, je multiplatformní sada nástrojů pro vytváření grafických uživatelských rozhraní. GTK+ nabízí kompletní sadu widgetů a je vhodný pro projekty od malých jednorázových nástrojů až po kompletní sady aplikací.

Stránka také vysvětluje, proč bylo GTK+ vytvořeno:

GTK+ byl původně vyvinut a používán pro GIMP, GNU Image Manipulation Program. Nazývá se „The GIMP ToolKit“, aby byl zapamatován původ projektu. Dnes je běžněji známý jako zkráceně GTK+ a používá ho velké množství aplikací včetně pracovní plochy GNOME projektu GNU.

Předpoklady

GTK+:

Související obsah

Ujistěte se, že máte nainstalovanou GTK+. Aplikaci tutoriálu jsem vyvinul v Ubuntu 16.04, které má ve výchozím nastavení nainstalované GTK+ (verze 3.18).

Svou verzi můžete zkontrolovat pomocí následujícího příkazu: dpkg -l libgtk-3-0 .

Ruby:

V systému byste měli mít nainstalovanou Ruby. Používám RVM ke správě více verzí Ruby nainstalovaných v mém systému. Pokud to chcete udělat také, můžete najít pokyny k instalaci RVM na jeho domovské stránce a pokyny k instalaci verzí Ruby (aka, Rubies) na stránce související dokumentace.

Tento tutoriál používá Ruby 2.4.2. Verzi můžete zkontrolovat pomocí ruby --version nebo prostřednictvím RVM pomocí rvm list .

Glade:

Na webových stránkách Glade:"Glade je nástroj RAD, který umožňuje rychlý a snadný vývoj uživatelských rozhraní pro sadu nástrojů GTK+ a desktopové prostředí GNOME."

Glade použijeme k návrhu uživatelského rozhraní naší aplikace. Pokud používáte Ubuntu, nainstalujte glade pomocí sudo apt install glade .

Gem GTK3:

Tento klenot poskytuje vazby Ruby pro sadu nástrojů GTK+. Jinými slovy, umožňuje nám mluvit s GTK+ API pomocí jazyka Ruby.

Nainstalujte drahokam pomocí gem install gtk3 .

Definování specifikací aplikace

Aplikace, kterou vytvoříme v tomto tutoriálu:

  • Mít uživatelské rozhraní (tj. počítačovou aplikaci)
  • Umožněte uživatelům nastavit různé vlastnosti pro každou položku (např. prioritu)
  • Umožněte uživatelům vytvářet a upravovat položky úkolů
    • Všechny položky budou uloženy jako soubory v domovském adresáři uživatele ve složce s názvem .gtk-todo-tutorial
  • Povolit uživatelům archivovat položky úkolů
    • Archivované položky by měly být umístěny do vlastní složky s názvem archived

Struktura aplikace

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

Sestavení aplikace ToDo

Inicializace aplikace

Vytvořte adresář pro uložení všech souborů, které bude aplikace potřebovat. Jak můžete vidět ve struktuře výše, pojmenoval jsem svůj gtk-todo-tutorial .

Vytvořte soubor s názvem gtk-todo (správně, bez přípony) a přidejte následující:

#!/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

Toto bude skript, který spustí aplikaci.

Všimněte si shebang (#! ) v prvním řádku. Takto definujeme, který interpret provede skript pod operačními systémy Unix/Linux. Tímto způsobem nemusíme používat ruby gtk-todo; stačí použít název skriptu:gtk-todo .

Zatím to ale nezkoušejte, protože jsme režim souboru nezměnili tak, aby byl spustitelný. Chcete-li tak učinit, zadejte po přechodu do kořenového adresáře aplikace do terminálu následující příkaz:

chmod +x ./gtk-todo # make the script executable

Z konzoly spusťte:

./gtk-todo # execute the script

Poznámky:

  • Aplikační objekt, který jsme definovali výše (a obecně všechny widgety GTK+) vysílají signály ke spouštění událostí. Jakmile se aplikace spustí, například vyšle signál ke spuštění activate událost. Jediné, co musíme udělat, je definovat, co chceme, aby se stalo, když je tento signál vysílán. Dosáhli jsme toho pomocí signal_connect instance a předat jí blok, jehož kód bude proveden při dané události. Budeme to dělat často v průběhu kurzu.
  • Když jsme inicializovali Gtk::Application objekt, předali jsme dva parametry:
    • com.iridakos.gtk-todo :Toto je ID naší aplikace a obecně by to měl být reverzní identifikátor stylu DNS. Více o jeho použití a osvědčených postupech se můžete dozvědět na wiki GNOME.
    • :flags_none :Tento příznak definuje chování aplikace. Použili jsme výchozí chování. Podívejte se na všechny příznaky a typy aplikací, které definují. Můžeme použít příznaky ekvivalentní Ruby, jak jsou definovány v Gio::ApplicationFlags.constants . Například místo použití :flags_none , mohli bychom použít Gio::ApplicationFlags::FLAGS_NONE .

Předpokládejme, že objekt aplikace, který jsme dříve vytvořili (Gtk::Application ) měl při activate spoustu věcí na práci byl vysílán signál nebo že jsme se chtěli připojit k více signálům. Nakonec bychom vytvořili obrovské gtk-todo soubor skriptu, což ztěžuje čtení/údržbu. Je čas refaktorovat.

Jak je popsáno ve struktuře aplikace výše, vytvoříme složku s názvem application a podsložky ui , models a lib .

  • V ui složku, umístíme všechny soubory související s naším uživatelským rozhraním.
  • V models složky, umístíme všechny soubory související s našimi modely.
  • V lib složky, umístíme všechny soubory, které nepatří do žádné z těchto kategorií.

Definujeme novou podtřídu Gtk::Application třídy pro naši aplikaci. Vytvoříme soubor s názvem application.rb pod application/ui/todo s následujícím obsahem:

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

Změníme gtk-todo podle toho skript:

#!/usr/bin/env ruby

require 'gtk3'

app = ToDo::Application.new

puts app.run

Mnohem čistší, že? Jo, ale nejde to. Dostaneme něco jako:

./gtk-todo:5:in `<main>': uninitialized constant ToDo (NameError)

Problém je v tom, že jsme nepožadovali žádný ze souborů Ruby umístěných v application složka. Musíme změnit soubor skriptu následovně a spustit jej znovu.

#!/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

Nyní by to mělo být v pořádku.

Zdroje

Na začátku tohoto tutoriálu jsme si řekli, že použijeme Glade k návrhu uživatelského rozhraní aplikace. Glade vytváří xml soubory s příslušnými prvky a atributy, které odrážejí to, co jsme navrhli prostřednictvím jeho uživatelského rozhraní. Potřebujeme tyto soubory použít pro naši aplikaci, abychom získali uživatelské rozhraní, které jsme navrhli.

Tyto soubory jsou prostředky pro aplikaci a GResource API poskytuje způsob, jak je všechny sbalit do binárního souboru, ke kterému lze později přistupovat z aplikace s výhodami – na rozdíl od ručního zpracování již načtených zdrojů, jejich umístění v systému souborů atd. Přečtěte si více o GResource API.

Popis zdrojů

Nejprve musíme vytvořit soubor popisující prostředky aplikace. Vytvořte soubor s názvem gresources.xml a umístěte jej přímo pod resources složka.

<?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>

Tento popis v podstatě říká:„Máme zdroj, který se nachází pod ui adresář (relativně k tomuto xml soubor) s názvem application_window.ui . Před načtením tohoto zdroje prosím odstraňte mezery." To samozřejmě zatím nebude fungovat, protože jsme zdroj nevytvořili přes Glade. Ale nebojte se, jednu věc po druhé.

Poznámka :xml-stripblanks direktiva bude používat xmllint příkaz k odstranění polotovarů. V Ubuntu musíte nainstalovat balíček libxml2-utils .

Vytvoření binárního souboru zdrojů

K vytvoření souboru binárních zdrojů použijeme další nástroj knihovny GLib nazvaný glib-compile-resources . Zkontrolujte, zda jej máte nainstalovaný pomocí dpkg -l libglib2.0-bin . Měli byste vidět něco takového:

ii  libglib2.0-bin     2.48.2-0ubuntu amd64          Programs for the GLib library

Pokud ne, nainstalujte balíček (sudo apt install libglib2.0-bin v Ubuntu).

Pojďme sestavit soubor. Do našeho skriptu přidáme kód, takže zdroje budou vytvořeny pokaždé, když jej spustíme. Změňte gtk-todo skript takto:

#!/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

Když jej spustíme, v konzoli se stane následující:opravíme to později:

/.../gtk-todo-tutorial/resources/gresources.xml: Failed to locate 'ui/application_window.ui' in any source directory.

Udělali jsme toto:

  • Přidána require příkaz pro fileutils knihovny, abychom ji mohli použít v at_exit zavolat
  • Definoval zdrojové a cílové soubory glib-compile-resources příkaz
  • Byly provedeny glib-compile-resources příkaz
  • Nastavte háček, aby byl binární soubor smazán před ukončením skriptu (tj. před ukončením aplikace), takže při příštím vytvoření bude znovu vytvořen

Načítání binárního souboru zdrojů

Popsali jsme zdroje a zabalili je do binárního souboru. Nyní je musíme načíst a zaregistrovat v aplikaci, abychom je mohli používat. Je to stejně snadné jako přidání následujících dvou řádků před at_exit háček:

resource = Gio::Resource.load(resource_bin)
Gio::Resources.register(resource)

A je to. Od nynějška můžeme využívat zdroje odkudkoli uvnitř aplikace. (Uvidíme, jak později.) Skript zatím selže, protože nemůže načíst binární soubor, který není vytvořen. Buď trpělivý; brzy se dostaneme k zajímavé části. Vlastně teď.

Návrh hlavního okna aplikace

Představujeme Glade

Chcete-li začít, otevřete Glade.

Zde je to, co vidíme:

  • Vlevo je ve střední části seznam widgetů, které lze přetáhnout. (Do widgetu štítků nelze přidat okno nejvyšší úrovně.) Budu to nazývat část widgetů .
  • Prostřední část obsahuje naše widgety tak, jak se budou (většinou) zobrazovat v aplikaci. Budu to nazývat sekce Design .
  • Vpravo jsou dvě podsekce:
    • Horní část obsahuje hierarchii widgetů při jejich přidávání do zdroje. Budu to nazývat část Hierarchie .
    • Spodní část obsahuje všechny vlastnosti, které lze konfigurovat pomocí Glade pro widget vybraný výše. Budu to nazývat část Vlastnosti .

Popíšu kroky pro vytvoření uživatelského rozhraní tohoto návodu pomocí Glade, ale pokud máte zájem o vytváření aplikací GTK+, měli byste se podívat na oficiální zdroje a návody tohoto nástroje.

Vytvoření návrhu okna aplikace

Pojďme vytvořit okno aplikace pouhým přetažením Application Window widget ze sekce Widget do sekce Návrh.

Gtk::Builder je objekt používaný v aplikacích GTK+ ke čtení textových popisů uživatelského rozhraní (jako je to, které vytvoříme přes Glade) a sestavování popsaných objektových widgetů.

První věcí v sekci Vlastnosti je ID a má výchozí hodnotu applicationWindow1 . Pokud tuto vlastnost necháme tak, jak je, později bychom vytvořili Gtk::Builder prostřednictvím našeho kódu, který by načetl soubor vytvořený Glade. K získání okna aplikace bychom museli použít něco jako:

application_window = builder.get_object('applicationWindow1')

application_window.signal_connect 'whatever' do |a,b|
...

application_window objekt by byl třídy Gtk::ApplicationWindow; takže cokoli, co jsme museli přidat k jeho chování (jako nastavení jeho názvu), by se odehrávalo mimo původní třídu. Také, jak je ukázáno ve úryvku výše, kód pro připojení k signálu okna by byl umístěn do souboru, který jej vytvořil.

Dobrou zprávou je, že GTK+ v roce 2013 představilo funkci, která umožňuje vytvářet složené šablony widgetů, což nám (mimo jiné výhody) umožňuje definovat vlastní třídu pro widget (který se nakonec odvozuje od existujícího GTK::Widget třída obecně). Nedělejte si starosti, pokud jste zmatení. Po napsání kódu a zobrazení výsledků pochopíte, co se děje.

Chcete-li definovat náš design jako šablonu, zaškrtněte Composite zaškrtávací políčko ve widgetu vlastností. Všimněte si, že ID vlastnost změněna na Class Name . Vyplňte TodoApplicationWindow . Toto je třída, kterou vytvoříme v našem kódu, aby reprezentovala tento widget.

Uložte soubor s názvem application_window.ui v nové složce s názvem ui uvnitř resources . Zde je to, co uvidíme, když soubor otevřeme z editoru:

<?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>

Náš widget má třídu a nadřazený atribut. V souladu s konvencí atributů nadřazené třídy musí být naše třída definována uvnitř modulu s názvem Todo . Než se tam dostaneme, zkusme spustit aplikaci spuštěním skriptu (./gtk-todo ).

To jo! Začíná to!

Vytvoření třídy okna aplikace

Pokud za běhu aplikace zkontrolujeme obsah kořenového adresáře aplikace, můžeme vidět gresource.bin soubor tam. I když se aplikace úspěšně spustí, protože zásobník zdrojů je přítomen a lze jej zaregistrovat, zatím jej nepoužijeme. Stále spustíme běžné Gtk::ApplicationWindow v našem application.rb soubor. Nyní je čas vytvořit naši vlastní třídu okna aplikace.

Vytvořte soubor s názvem application_window.rb v application/ui/todo složku a přidejte následující obsah:

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

Definovali jsme init metoda jako singleton metoda na třídě po otevření eigenclass za účelem svázání šablony tohoto widgetu s dříve registrovaným zdrojovým souborem.

Předtím jsme zavolali type_register class, která registruje a zpřístupňuje naši vlastní třídu widgetů GLib svět.

Nakonec pokaždé, když vytvoříme instanci tohoto okna, nastavíme jeho název na GTK+ Simple ToDo .

Nyní se vraťme k application.rb soubor a použijte to, co jsme právě implementovali:

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

Spusťte skript.

Definujte model

Pro jednoduchost uložíme položky ToDo do souborů ve formátu JSON do vyhrazené skryté složky v domovském adresáři našeho uživatele. Ve skutečné aplikaci bychom použili databázi, ale to je mimo rozsah tohoto tutoriálu.

Naše Todo::Item model bude mít následující vlastnosti:

  • id :ID položky
  • název :Název
  • poznámky :Jakékoli poznámky
  • priorita :Jeho priorita
  • creation_datetime :Datum a čas vytvoření položky
  • název souboru :Název souboru, do kterého je položka uložena

Vytvoříme soubor s názvem item.rb v části application/models adresář s následujícím obsahem:

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

Zde jsme definovali metody:

  • Inicializovat položku:
    • Jako "nové" definováním :user_data_path ve kterém bude později uložen
    • Jako "existující" definováním :filename načíst z. Název souboru musí být soubor JSON, který byl dříve vygenerován položkou
  • Načíst položku ze souboru
  • Určete, zda je položka nová nebo ne (tj. uložena alespoň jednou v :user_data_path nebo ne)
  • Uložte položku zapsáním jejího řetězce JSON do souboru
  • Smazat položku
  • Vytvořte řetězec JSON položky jako hash jejích vlastností

Přidat novou položku

Vytvořit tlačítko

Pojďme si do okna naší aplikace přidat tlačítko pro přidání nové položky. Otevřete resources/ui/application_window.ui soubor v Glade.

  • Přetáhněte Button ze sekce Widget do sekce Design.
  • V části Vlastnosti nastavte jeho ID hodnotu add_new_item_button .
  • V dolní části Obecné na kartě Vlastnosti v části Vlastnosti je textová oblast přímo pod Štítek s volitelným obrázkem volba. Změňte jeho hodnotu z Tlačítko Přidat novou položku .
  • Uložte soubor a spusťte skript.

Nebojte se; design později vylepšíme. Nyní se podívejme, jak se připojit kliknutím na naše tlačítko událost.

Nejprve musíme aktualizovat naši třídu okna aplikace, aby se dozvěděla o svém novém potomkovi, tlačítku s id add_new_item_button . Potom můžeme získat přístup k dítěti, abychom mohli změnit jeho chování.

Změňte init následujícím způsobem:

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

Docela jednoduché, že? bind_template_child metoda dělá přesně to, co říká, a od této chvíle každá instance našeho Todo::ApplicationWindow třída bude mít add_new_item_button způsob přístupu k příslušnému tlačítku. Pojďme tedy změnit initialize následujícím způsobem:

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

Jak vidíte, k tlačítku se dostaneme pomocí add_new_item_button a definujeme, co chceme, aby se stalo, když se na to klikne. Restartujte aplikaci a zkuste kliknout na tlačítko. V konzole byste měli vidět zprávu OMG! I AM CLICKED když  kliknete na tlačítko.

Co však chceme, aby se stalo, když klikneme na toto tlačítko, je zobrazit nové okno pro uložení položky ToDo. Hádáte správně:Jsou hodiny Glade.

Vytvořit okno nové položky

  • Vytvořte nový projekt v Glade stisknutím ikony zcela vlevo v horní liště nebo výběrem Soubor> Nový z nabídky aplikace.
  • Přetáhněte Window ze sekce Widget do oblasti Návrh.
  • Zkontrolujte jeho Composite vlastnost a pojmenujte třídu TodoNewItemWindow .

  • Přetáhněte Grid ze sekce Widget a umístěte jej do okna, které jsme přidali dříve.
  • Nastavte 5 řádky a 2 sloupců v okně, které se objeví.
  • V Obecné na kartě Vlastnosti v části Vlastnosti nastavte rozestupy řádků a sloupců na 10 (pixely).
  • V Běžné na kartě Vlastnosti v části Vlastnosti nastavte Widget Spacing > Margins > Top, Bottom, Left, Right vše do 10 aby obsah nebyl přilepen k okrajům mřížky.

  • Přetáhněte čtyři Label widgety ze sekce Widget a umístěte jeden do každého řádku mřížky.
  • Změňte jejich Label vlastnosti, shora dolů, následovně:
    • Id:
    • Title:
    • Notes:
    • Priority:
  • V Obecné na kartě Vlastnosti v části Vlastnosti změňte Zarovnání a odsazení> Zarovnání> Vodorovně vlastnost od 0,50 do 1 pro každou vlastnost, aby se text štítku zarovnal vpravo.
  • Tento krok je volitelný, ale doporučený. Tyto štítky v našem okně nesvážeme, protože nepotřebujeme měnit jejich stav nebo chování. V tomto kontextu pro ně nemusíme nastavovat popisné ID, jako jsme to udělali pro add_new_item_button tlačítko v okně aplikace. ALE do našeho návrhu přidáme další prvky a hierarchie widgetů v Glade bude těžko čitelná, pokud budou říkat label1 , label2 atd. Nastavení popisných ID (např. id_label , title_label , notes_label , priority_label ) nám usnadní život. Dokonce jsem nastavil ID mřížky na main_grid protože nemám rád čísla nebo názvy proměnných v ID.

  • Přetáhněte Label z části Widget do druhého sloupce prvního řádku mřížky. ID bude automaticky vygenerováno naším modelem; neumožníme úpravy, takže štítek pro jeho zobrazení je více než dostatečný.
  • Nastavte ID vlastnost na id_value_label .
  • Nastavte Zarovnání a odsazení> Zarovnání> Vodorovně vlastnost na 0 takže text se zarovnává vlevo.
  • Připojíme tento widget k naší třídě Window, abychom mohli změnit jeho text při každém načtení okna. Nastavení štítku prostřednictvím Glade tedy není vyžadováno, ale přibližuje návrh tomu, jak bude vypadat při vykreslování se skutečnými daty. Štítek můžete nastavit podle toho, co vám nejlépe vyhovuje; Nastavil jsem to na id-of-the-todo-item-here .

  • Přetáhněte Text Entry z části Widget do druhého sloupce druhého řádku mřížky.
  • Nastavte jeho vlastnost ID na title_text_entry . Jak jste si možná všimli, dávám přednost získání typu widgetu v ID, aby byl kód ve třídě čitelnější.
  • V Běžné na kartě Vlastnosti v části Vlastnosti zaškrtněte Widget Spacing > Expand > Horizontal zaškrtávací políčko a zapněte vypínač vedle něj. Tímto způsobem se widget rozbalí vodorovně pokaždé, když se změní velikost jeho rodiče (neboli mřížky).

  • Přetáhněte Text View z části Widget do druhého sloupce třetího řádku mřížky.
  • Nastavte jeho ID do notes . Ne, jen tě zkouším. Nastavte jeho ID vlastnost na notes_text_view .
  • V Běžné na kartě Vlastnosti v části Vlastnosti zkontrolujte Widget Spacing > Expand > Horizontal, Vertical zaškrtávací políčka a zapněte přepínače vedle nich. Tímto způsobem se widget roztáhne vodorovně a svisle pokaždé, když se změní velikost jeho rodiče (mřížky).

  • Přetáhněte Combo Box z části Widget do druhého sloupce čtvrtého řádku mřížky.
  • Nastavte jeho ID do priority_combo_box .
  • V Běžné na kartě Vlastnosti v části Vlastnosti zaškrtněte Widget Spacing > Expand > Horizontal zaškrtněte políčko a zapněte přepínač napravo od něj. To umožňuje widgetu rozbalit se vodorovně pokaždé, když se změní velikost jeho rodiče (mřížky).
  • Tento widget je rozbalovací prvek. We will populate its values that can be selected by the user when it shows up inside our window class.

  • Drag a Button Box from the Widget section to the second column of the last row of the grid.
  • In the pop-up window, select 2 items.
  • In the General tab of the Properties section, set the Box Attributes> Orientation property to Horizontal .
  • In the General tab of the Properties section, set the Box Attributes> Spacing property to 10 .
  • In the Common tab of the Properties section, set the Widget Spacing> Alignment> Horizontal to Center .
  • Again, our code won't alter this widget, but you can give it a descriptive ID for readability. I named mine actions_box .

  • Drag two Button widgets and place one in each box of the button box widget we added in the previous step.
  • Set their ID properties to cancel_button and save_button , respectively.
  • In the General tab of the Properties window, set their Button Content> Label with option image property to Cancel and Save , respectively.

The window is ready. Save the file under resources/ui/new_item_window.ui .

It's time to port it into our application.

Implement the new item window class

Before implementing the new class, we must update our GResource description file (resources/gresources.xml ) to obtain the new resource:

<?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>

Now we can create the new window class. Create a file under application/ui/todo named new_item_window.rb and set its contents as follows:

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

There's nothing special here. We just changed the template resource to point to the correct file of our resources.

We have to change the add_new_item_button code that executes on the clicked signal to show the new item window. We'll go ahead and change that code in application_window.rb to this:

add_new_item_button.signal_connect 'clicked' do |button|
  new_item_window = NewItemWindow.new(application)
  new_item_window.present
end

Let's see what we have done. Start the application and click on the Add new item button. Tadaa!

But nothing happens when we press the buttons. Let's fix that.

First, we'll bind the UI widgets in the Todo::NewItemWindow class.

Change the init method to this:

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

This window will be shown when either creating or editing a ToDo item, so the new_item_window naming is not very valid. We'll refactor that later.

For now, we will update the window's initialize method to require one extra parameter for the Todo::Item to be created or edited. We can then set a more meaningful window title and change the child widgets to reflect the current item.

We'll change the initialize method to this:

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

Then we'll add the constant PRIORITIES in the application/models/item.rb file just below the PROPERTIES constant:

PRIORITIES = ['high', 'medium', 'normal', 'low'].freeze

What did we do here?

  • We set the window's title to a string containing the current item's ID and the mode (depending on whether the item is being created or edited).
  • We set the id_value_label text to display the current item's ID.
  • We set the title_text_entry text to display the current item's title.
  • We set the notes_text_view text to display the current item's notes.
  • We created a model for the priority_combo_box whose entries are going to have only one String hodnota. At first sight, a Gtk::ListStore model might look a little confusing. Here's how it works.
    • Suppose we want to display in a combo box a list of country codes and their respective country names.
    • We would create a Gtk::ListStore defining that its entries would consist of two string values:one for the country code and one for the country name. Thus we would initialize the ListStore as: 
      model = Gtk::ListStore.new(String, String)
    • To fill the model with data, we would do something like the following (make sure you don't miss the comments in the snippet): 
      [['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
    • We also configured the combo box to render two text columns/cells (again, make sure you don't miss the comments in the snippet): 
      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)
    • I hope that made it a little clearer.
  • We added a simple text renderer in the combo box and instructed it to display the only value of each model's entry (a.k.a., position 0 ). Imagine that our model is something like [['high'],['medium'],['normal'],['low']] and 0 is the first element of each sub-array. I will stop with the model-combo-text-renderer explanations now…

Configure the user data path

Remember that when initializing a new Todo::Item (not an existing one), we had to define a :user_data_path in which it would be saved. We are going to resolve this path when the application starts and make it accessible from all the widgets.

All we have to do is check if the .gtk-todo-tutorial path exists inside the user's home ~ adresář. If not, we will create it. Then we'll set this as an instance variable of the application. All widgets have access to the application instance. So, all widgets have access to this user path variable.

Change the application/application.rb file to this:

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

One last thing we need to do before testing what we have done so far is to instantiate the Todo::NewItemWindow when the add_new_item_button is clicked complying with the changes we made. In other words, change the code in application_window.rb to this:

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

Start the application and click on the Add new item button. Tadaa! (Note the - Create mode part in the title).

Cancel item creation/update

To close the Todo::NewItemWindow window when a user clicks the cancel_button , we only have to add this to the window's initialize method:

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

close is an instance method of the Gtk::Window class that closes the window.

Save the item

Saving an item involves two steps:

  • Update the item's properties based on the widgets' values.
  • Call the save! method on the Todo::Item instance.

Again, our code will be placed in the initialize method of the Todo::NewItemWindow :

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

Once again, the window closes after saving the item.

Let's try that out.

Now, by pressing Save and navigating to our ~/.gtk-todo-tutorial folder, we should see a file. Mine had the following contents:

{
        "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"
}

Don't forget to try out the Cancel button as well.

View ToDo items

The Todo::ApplicationWindow contains only one button. It's time to change that.

We want the window to have Add new item on the top and a list below with all of our ToDo items. We'll add a Gtk::ListBox to our design that can contain any number of rows.

Update the application window

  • Open the resources/ui/application_window.ui file in Glade.
  • Nothing happens if we drag a List Box widget from the Widget section directly on the window. That is normal. First, we have to split the window into two parts:one for the button and one for the list box. Bear with me.
  • Right-click on the new_item_window in the Hierarchy section and select Add parent> Box .
  • In the pop-up window, set 2 for the number of items.
  • The orientation of the box is already vertical, so we are fine.

  • Now, drag a List Box and place it on the free area of the previously added box.
  • Set its ID property to todo_items_list_box .
  • Set its Selection mode to None since we won't provide that functionality.

Design the ToDo item list box row

Each row of the list box we created in the previous step will be more complex than a row of text. Each will contain widgets that allow the user to expand an item's notes and to delete or edit the item.

  • Create a new project in Glade, as we did for the new_item_window.ui . Save it under resources/ui/todo_item_list_box_row.ui .
  • Unfortunately (at least in my version of Glade), there is no List Box Row widget in the Widget section. So, we'll add one as the top-level widget of our project in a kinda hackish way.
  • Drag a List Box from the Widget section to the Design area.
  • Inside the Hierarchy section, right-click on the List Box and select Add Row

  • In the Hierarchy section, right-click on the newly added List Box Row nested under the List Box and select Remove parent . There it is! The List Box Row is the top-level widget of the project now.

  • Check the widget's Composite property and set its name to TodoItemListBoxRow .
  • Drag a Box from the Widget section to the Design area inside our List Box Row .
  • Set 2 items in the pop-up window.
  • Set its ID property to main_box .

  • Drag another Box from the Widget section to the first row of the previously added box.
  • Set 2 items in the pop-up window.
  • Set its ID property to todo_item_top_box .
  • Set its Orientation property to Horizontal .
  • Set its Spacing (General tab) property to 10 .

  • Drag a Label from the Widget section to the first column of the todo_item_top_box .
  • Set its ID property to todo_item_title_label .
  • Set its Alignment and Padding> Alignment> Horizontal property to 0.00 .
  • In the Common tab of the Properties section, check the Widget Spacing> Expand> Horizontal checkbox and turn on the switch next to it so the label will expand to the available space.

  • Drag a Button from the Widget section to the second column of the todo_item_top_box .
  • Set its ID property to details_button .
  • Check the Button Content> Label with optional image radio and type ... (three dots).

  • Drag a Revealer widget from the Widget section to the second row of the main_box .
  • Turn off the Reveal Child switch in the General tab.
  • Set its ID property to todo_item_details_revealer .
  • Set its Transition type property to Slide Down .

  • Drag a Box from the Widget section to the reveal space.
  • Set its items to 2 in the pop-up window.
  • Set its ID property to details_box .
  • In the Common tab, set its Widget Spacing> Margins> Top property to 10 .

  • Drag a Button Box from the Widget section to the first row of the details_box .
  • Set its ID property to todo_item_action_box .
  • Set its Layout style property to expand .

  • Drag Button widgets to the first and second columns of the todo_item_action_box .
  • Set their ID properties to delete_button and edit_button , respectively.
  • Set their Button Content> Label with optional image properties to Delete and Edit , respectively.

  • Drag a Viewport widget from the Widget section to the second row of the details_box .
  • Set its ID property to todo_action_notes_viewport .
  • Drag a Text View widget from the Widget section to the todo_action_notes_viewport that we just added.
  • Set its ID to todo_item_notes_text_view .
  • Uncheck its Editable property in the General tab of the Properties section.

Create the ToDo item list-box row class

Now we will create the class reflecting the UI of the list-box row we just created.

First we have to update our GResource description file to include the newly created design. Change the resources/gresources.xml file as follows:

<?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>

Create a file named item_list_box_row.rb inside the application/ui folder and add the following:

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

We will not bind any children at the moment.

When starting the application, we have to search for files in the :user_data_path , and we must create a Todo::Item instance for each file. For each instance, we must also add a new Todo::ItemListBoxRow to the Todo::ApplicationWindow 's todo_items_list_box list box. One thing at a time.

First, let's bind the todo_items_list_box in the Todo::ApplicationWindow class. Change the init method as follows:

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

Next, we'll add an instance method in the same class that will be responsible to load the ToDo list items in the related list box. Add this code in 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

Then we'll call this method at the end of the initialize method:

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

Poznámka: We must first empty the list box of its current children rows then refill it. This way, we will call this method after saving a Todo::Item via the signal_connect of the save_button of the Todo::NewItemWindow , and the parent application window will be reloaded! Here's the updated code (in 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

Previously, we used this code:

json_files = Dir[File.join(File.expand_path(application.user_data_path), '*.json')]

to find the names of all the files in the application-user data path with a JSON extension.

Let's see what we've created. Start the application and try adding a new ToDo item. After pressing the Save button, you should see the parent Todo::ApplicationWindow automatically updated with the new item!

What's left is to complete the functionality of the Todo::ItemListBoxRow .

First, we will bind the widgets. Change the init method of the Todo::ItemListBoxRow class as follows:

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

Then, we'll set up the widgets based on the item of each row.

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
  • As you can see, when the details_button is clicked, we instruct the todo_item_details_revealer to swap the visibility of its contents.
  • After deleting an item, we find the application's Todo::ApplicationWindow to call its load_todo_items , as we did after saving an item.
  • When clicking to edit a button, we create a new instance of the Todo::NewItemWindow passing an item as the current item. Works like a charm!
  • Finally, to reach the application parent of a list-box row, we defined a simple instance method application that navigates through the widget's parents until it reaches a window from which it can obtain the application object.

Save and run the application. There it is!

This has been a really long tutorial and, even though there are so many items that we haven't covered, I think we better end it here.

Long post, cat photo.

  • This tutorial's code
  • A set of bindings for the GNOME-2.x libraries to use from Ruby
  • Gtk3 tutorial for Ruby based on the official C version
  • GTK+ 3 Reference Manual

This was originally published on Lazarus Lazaridis's blog, iridakos.com, and is republished with permission.


Linux
  1. Mějte přehled o specifikacích svého počítače se systémem Linux pomocí této desktopové aplikace

  2. Vytvořte SDN na Linuxu s otevřeným zdrojovým kódem

  3. Jak jsme vytvořili desktopovou aplikaci pro Linux s Electronem

  1. Vytvořte jedinečné prostředí Linuxu s prostředím Unix Desktop Environment

  2. Přizpůsobte si plochu Linuxu pomocí KDE Plasma

  3. Prožijte znovu historii Linuxu s desktopem ROX

  1. Přizpůsobte si plochu Linuxu pomocí FVWM

  2. Začínáme s pracovním prostředím GNOME Linux

  3. Okořeňte si plochu Linuxu pomocí Cinnamon