GNU/Linux >> Linux Esercitazione >  >> Linux

Crea un'applicazione desktop Linux con Ruby

Recentemente, mentre sperimentavo con GTK e i suoi attacchi Ruby, ho deciso di scrivere un tutorial che introducesse questa funzionalità. In questo post creeremo una semplice applicazione ToDo (qualcosa come quella che abbiamo creato con Ruby on Rails) usando il gtk3 gem (ovvero gli attacchi GTK+ Ruby).

Puoi trovare il codice del tutorial su GitHub.

Cos'è GTK+?

Secondo il sito Web di GTK+:

GTK+, o GIMP Toolkit, è un toolkit multipiattaforma per la creazione di interfacce utente grafiche. Offrendo un set completo di widget, GTK+ è adatto per progetti che vanno da piccoli strumenti una tantum a suite di applicazioni complete.

Il sito spiega anche perché è stato creato GTK+:

GTK+ è stato inizialmente sviluppato e utilizzato da GIMP, il GNU Image Manipulation Program. Si chiama "The GIMP ToolKit" in modo che vengano ricordate le origini del progetto. Oggi è più comunemente noto come GTK+ in breve ed è utilizzato da un gran numero di applicazioni, incluso il desktop GNOME del progetto GNU.

Prerequisiti

GTK+:

Contenuti correlati

Assicurati di aver installato GTK+. Ho sviluppato l'applicazione del tutorial in Ubuntu 16.04, che ha GTK+ (versione 3.18) installato per impostazione predefinita.

Puoi controllare la tua versione con il seguente comando: dpkg -l libgtk-3-0 .

Rubino:

Dovresti avere Ruby installato sul tuo sistema. Uso RVM per gestire più versioni di Ruby installate sul mio sistema. Se vuoi farlo anche tu, puoi trovare le istruzioni per l'installazione di RVM sulla sua homepage e le istruzioni per l'installazione delle versioni di Ruby (aka Rubies) nella relativa pagina della documentazione.

Questo tutorial utilizza Ruby 2.4.2. Puoi controllare la tua versione utilizzando ruby --version o tramite RVM con lista rvm .

Radura:

Per il sito Web di Glade, "Glade è uno strumento RAD per consentire lo sviluppo rapido e semplice di interfacce utente per il toolkit GTK+ e l'ambiente desktop GNOME".

Useremo Glade per progettare l'interfaccia utente della nostra applicazione. Se sei su Ubuntu, installa glade con sudo apt install glade .

Gemma GTK3:

Questa gemma fornisce i collegamenti Ruby per il toolkit GTK+. In altre parole, ci consente di parlare con l'API GTK+ utilizzando il linguaggio Ruby.

Installa la gem con gem install gtk3 .

Definizione delle specifiche dell'applicazione

L'applicazione che creeremo in questo tutorial:

  • Disporre di un'interfaccia utente (ad esempio, un'applicazione desktop)
  • Consenti agli utenti di impostare proprietà varie per ciascun elemento (ad es. priorità)
  • Consenti agli utenti di creare e modificare elementi ToDo
    • Tutti gli elementi verranno salvati come file nella home directory dell'utente in una cartella denominata .gtk-todo-tutorial
  • Consenti agli utenti di archiviare gli elementi ToDo
    • Gli elementi archiviati devono essere inseriti nella propria cartella denominata archiviati

Struttura dell'applicazione

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

Creazione dell'applicazione ToDo

Inizializzazione dell'applicazione

Creare una directory per salvare tutti i file necessari all'applicazione. Come puoi vedere nella struttura sopra, ho chiamato il mio gtk-todo-tutorial .

Crea un file chiamato gtk-todo (esatto, nessuna estensione) e aggiungi quanto segue:

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

Questo sarà lo script che avvia l'applicazione.

Nota lo shebang (#! ) nella prima riga. Questo è il modo in cui definiamo quale interprete eseguirà lo script nei sistemi operativi Unix/Linux. In questo modo, non dobbiamo usare ruby gtk-todo; possiamo semplicemente usare il nome dello script:gtk-todo .

Non provarlo ancora, però, perché non abbiamo cambiato la modalità del file in modo che sia eseguibile. Per farlo, digita il seguente comando in un terminale dopo essere passato alla directory principale dell'applicazione:

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

Dalla console, esegui:

./gtk-todo # execute the script

Note:

  • L'oggetto applicazione che abbiamo definito sopra (e tutti i widget GTK+ in generale) emettono segnali per attivare eventi. Una volta che un'applicazione inizia a funzionare, ad esempio, emette un segnale per attivare il activate evento. Tutto quello che dobbiamo fare è definire cosa vogliamo che accada quando questo segnale viene emesso. Ci siamo riusciti utilizzando signal_connect metodo di istanza e passandogli un blocco il cui codice verrà eseguito sull'evento specificato. Lo faremo molto durante il tutorial.
  • Quando abbiamo inizializzato il Gtk::Application oggetto, abbiamo passato due parametri:
    • com.iridakos.gtk-todo :Questo è l'ID della nostra applicazione e, in generale, dovrebbe essere un identificatore di stile DNS inverso. Puoi saperne di più sul suo utilizzo e sulle migliori pratiche sul wiki di GNOME.
    • :flags_none :Questo flag definisce il comportamento dell'applicazione. Abbiamo usato il comportamento predefinito. Scopri tutti i flag e i tipi di applicazioni che definiscono. Possiamo usare i flag equivalenti a Ruby, come definito in Gio::ApplicationFlags.constants . Ad esempio, invece di utilizzare :flags_none , potremmo usare Gio::ApplicationFlags::FLAGS_NONE .

Supponiamo che l'oggetto applicazione che abbiamo creato in precedenza (Gtk::Application ) aveva molte cose da fare quando activate segnale è stato emesso o che volevamo connetterci a più segnali. Finiremmo per creare un enorme gtk-todo script, rendendolo difficile da leggere/mantenere. È tempo di refactoring.

Come descritto nella struttura dell'applicazione sopra, creeremo una cartella denominata applicazione e sottocartelle ui , modelli e lib .

  • Nell'interfaccia utente cartella, collocheremo tutti i file relativi alla nostra interfaccia utente.
  • Nei modelli cartella, collocheremo tutti i file relativi ai nostri modelli.
  • Nella lib cartella, collocheremo tutti i file che non appartengono a nessuna di queste categorie.

Definiremo una nuova sottoclasse di Gtk::Application classe per la nostra applicazione. Creeremo un file chiamato application.rb in application/ui/todo con i seguenti contenuti:

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

Cambieremo il gtk-todo script di conseguenza:

#!/usr/bin/env ruby

require 'gtk3'

app = ToDo::Application.new

puts app.run

Molto più pulito, vero? Sì, ma non funziona. Otteniamo qualcosa come:

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

Il problema è che non abbiamo richiesto nessuno dei file Ruby inseriti nell'applicazione cartella. Dobbiamo modificare il file di script come segue ed eseguirlo di nuovo.

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

Ora dovrebbe andare bene.

Risorse

All'inizio di questo tutorial, abbiamo detto che avremmo usato Glade per progettare l'interfaccia utente dell'applicazione. Glade produce xml file con gli elementi e gli attributi appropriati che riflettono ciò che abbiamo progettato tramite la sua interfaccia utente. Dobbiamo utilizzare quei file per la nostra applicazione per ottenere l'interfaccia utente che abbiamo progettato.

Questi file sono risorse per l'applicazione e il GResource L'API fornisce un modo per comprimerli tutti insieme in un file binario a cui in seguito è possibile accedere dall'interno dell'applicazione con vantaggi, invece di dover gestire manualmente le risorse già caricate, la loro posizione nel file system, ecc. Ulteriori informazioni sul GRrisorsa API.

Descrizione delle risorse

Innanzitutto, dobbiamo creare un file che descriva le risorse dell'applicazione. Crea un file chiamato gresources.xml e posizionalo direttamente sotto le risorse cartella.

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

Questa descrizione fondamentalmente dice:"Abbiamo una risorsa che si trova sotto ui directory (relativa a questo xml file) con il nome application_window.ui . Prima di caricare questa risorsa, rimuovi gli spazi vuoti." Ovviamente, questo non funzionerà ancora, dal momento che non abbiamo creato la risorsa tramite Glade. Non preoccuparti, una cosa alla volta.

Nota :Il xml-stripblanks la direttiva utilizzerà xmllint comando per rimuovere gli spazi vuoti. In Ubuntu, devi installare il pacchetto libxml2-utils .

Costruzione del file binario delle risorse

Per produrre il file delle risorse binarie, utilizzeremo un'altra utilità della libreria GLib chiamata glib-compile-resources . Verifica di averlo installato con dpkg -l libglib2.0-bin . Dovresti vedere qualcosa del genere:

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

In caso contrario, installa il pacchetto (sudo apt install libglib2.0-bin in Ubuntu).

Costruiamo il file. Aggiungeremo codice al nostro script in modo che le risorse vengano create ogni volta che lo eseguiamo. Cambia il gtk-todo script come segue:

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

Quando lo eseguiamo, nella console accade quanto segue; lo sistemeremo più tardi:

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

Ecco cosa abbiamo fatto:

  • Aggiunto un richiedi istruzione per fileutils libreria in modo da poterla utilizzare nel at_exit chiama
  • Definiti i file di origine e di destinazione di glib-compile-resources comando
  • Eseguito glib-compile-resources comando
  • Imposta un hook in modo che il file binario venga eliminato prima di uscire dallo script (cioè prima che l'applicazione esca) in modo che la prossima volta venga compilato di nuovo

Caricamento del file binario delle risorse

Abbiamo descritto le risorse e le abbiamo impacchettate in un file binario. Ora dobbiamo caricarli e registrarli nell'applicazione in modo da poterli utilizzare. È facile come aggiungere le seguenti due righe prima di at_exit gancio:

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

Questo è tutto. D'ora in poi, possiamo utilizzare le risorse da qualsiasi punto all'interno dell'applicazione. (Vedremo come più avanti.) Per ora, lo script non riesce poiché non può caricare un binario che non è prodotto. Essere pazientare; arriveremo presto alla parte interessante. In realtà adesso.

Progettazione della finestra principale dell'applicazione

Introduzione a Glade

Per iniziare, apri Glade.

Ecco cosa vediamo:

  • A sinistra, c'è un elenco di widget che possono essere trascinati e rilasciati nella sezione centrale. (Non puoi aggiungere una finestra di primo livello all'interno di un widget etichetta.) La chiamerò sezione widget .
  • La sezione centrale contiene i nostri widget così come appariranno (il più delle volte) nell'applicazione. La chiamerò sezione Design .
  • A destra ci sono due sottosezioni:
    • La sezione superiore contiene la gerarchia dei widget man mano che vengono aggiunti alla risorsa. La chiamerò sezione Gerarchia .
    • La sezione inferiore contiene tutte le proprietà che possono essere configurate tramite Glade per un widget selezionato sopra. La chiamerò sezione Proprietà .

Descriverò i passaggi per creare l'interfaccia utente di questo tutorial utilizzando Glade, ma se sei interessato a creare applicazioni GTK+, dovresti consultare le risorse e i tutorial ufficiali dello strumento.

Crea il design della finestra dell'applicazione

Creiamo la finestra dell'applicazione semplicemente trascinando la Finestra dell'applicazione widget dalla sezione Widget alla sezione Design.

Gtk::Builder è un oggetto utilizzato nelle applicazioni GTK+ per leggere le descrizioni testuali di un'interfaccia utente (come quella che costruiremo tramite Glade) e costruire i widget degli oggetti descritti.

La prima cosa nella sezione Proprietà è l'ID e ha un valore predefinito applicationWindow1 . Se lasciamo questa proprietà così com'è, creeremo in seguito un Gtk::Builder tramite il nostro codice che caricherebbe il file prodotto da Glade. Per ottenere la finestra dell'applicazione, dovremmo usare qualcosa come:

application_window = builder.get_object('applicationWindow1')

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

La finestra_applicazione l'oggetto sarebbe di classe Gtk::ApplicationWindow; quindi qualsiasi cosa dovessimo aggiungere al suo comportamento (come impostare il suo titolo) avverrebbe al di fuori della classe originale. Inoltre, come mostrato nello snippet sopra, il codice per connettersi al segnale di una finestra verrebbe inserito all'interno del file che ne ha creato l'istanza.

La buona notizia è che GTK+ ha introdotto una funzionalità nel 2013 che consente la creazione di modelli di widget compositi, che (tra gli altri vantaggi) ci consentono di definire la classe personalizzata per il widget (che alla fine deriva da un GTK::Widget classe in generale). Non preoccuparti se sei confuso. Capirai cosa sta succedendo dopo aver scritto del codice e visualizzato i risultati.

Per definire il nostro design come modello, controlla il Composito casella di controllo nel widget della proprietà. Nota che il ID proprietà modificata in Nome classe . Compila TodoApplicationWindow . Questa è la classe che creeremo nel nostro codice per rappresentare questo widget.

Salva il file con il nome application_window.ui in una nuova cartella denominata ui all'interno delle risorse . Ecco cosa vediamo se apriamo il file da un editor:

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

Il nostro widget ha una classe e un attributo genitore. Seguendo la convenzione degli attributi della classe genitore, la nostra classe deve essere definita all'interno di un modulo chiamato Todo . Prima di arrivarci, proviamo ad avviare l'applicazione eseguendo lo script (./gtk-todo ).

Sì! Si comincia!

Crea la classe della finestra dell'applicazione

Se controlliamo il contenuto della directory principale dell'applicazione durante l'esecuzione dell'applicazione, possiamo vedere il gresource.bin file lì. Anche se l'applicazione viene avviata correttamente perché il cestino delle risorse è presente e può essere registrato, non lo utilizzeremo ancora. Inizieremo comunque un normale Gtk::ApplicationWindow nel nostro application.rb file. Ora è il momento di creare la nostra classe di finestra dell'applicazione personalizzata.

Crea un file chiamato application_window.rb in application/ui/todo cartella e aggiungi il seguente contenuto:

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

Abbiamo definito init metodo come metodo singleton sulla classe dopo aver aperto la eigenclass per associare il modello di questo widget al file di risorse precedentemente registrato.

Prima di ciò, abbiamo chiamato il type_register class, che registra e rende disponibile la nostra classe widget personalizzata al GLib mondo.

Infine, ogni volta che creiamo un'istanza di questa finestra, impostiamo il suo titolo su GTK+ Simple ToDo .

Ora torniamo a application.rb archiviare e utilizzare ciò che abbiamo appena implementato:

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

Esegui lo script.

Definisci il modello

Per semplicità, salveremo gli elementi ToDo in file in formato JSON in una cartella nascosta dedicata nella home directory del nostro utente. In un'applicazione reale, utilizzeremmo un database, ma questo esula dallo scopo di questo tutorial.

Il nostro Todo::Item il modello avrà le seguenti proprietà:

  • id :ID dell'elemento
  • titolo :Il titolo
  • note :Eventuali note
  • priorità :La sua priorità
  • data_creazione :la data e l'ora di creazione dell'elemento
  • nome file :il nome del file in cui viene salvato un elemento

Creeremo un file chiamato item.rb sotto l'applicazione/modelli directory con i seguenti contenuti:

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

Qui abbiamo definito i metodi per:

  • Inizializza un elemento:
    • Come "nuovo" definendo il :user_data_path in cui verrà salvato in seguito
    • Come "esistente" definendo il :filename da cui caricare. Il nome del file deve essere un file JSON precedentemente generato da un elemento
  • Carica un elemento da un file
  • Risolvi se un elemento è nuovo o meno (cioè, salvato almeno una volta nel :user_data_path oppure no)
  • Salva un elemento scrivendo la sua stringa JSON in un file
  • Elimina un elemento
  • Produci la stringa JSON di un elemento come hash delle sue proprietà

Aggiungi un nuovo elemento

Crea il pulsante

Aggiungiamo un pulsante alla finestra dell'applicazione per aggiungere un nuovo elemento. Apri resources/ui/application_window.ui file in Radura.

  • Trascina un pulsante dalla sezione Widget alla sezione Design.
  • Nella sezione Proprietà, imposta il suo ID valore in add_new_item_button .
  • Vicino alla fine del Generale scheda nella sezione Proprietà, c'è un'area di testo appena sotto l'Etichetta con immagine opzionale opzione. Modificane il valore da Pulsante per Aggiungi nuovo elemento .
  • Salva il file ed esegui lo script.

Non preoccuparti; miglioreremo il design in seguito. Ora vediamo come connetterci funzionalità al cliccato del nostro pulsante evento.

Innanzitutto, dobbiamo aggiornare la nostra classe della finestra dell'applicazione in modo che venga a conoscenza del suo nuovo figlio, il pulsante con id add_new_item_button . Quindi possiamo accedere al bambino per modificarne il comportamento.

Cambia inizializzazione metodo come segue:

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

Abbastanza semplice, vero? Il bind_template_child il metodo fa esattamente quello che dice, e d'ora in poi ogni istanza del nostro Todo::ApplicationWindow la classe avrà un add_new_item_button metodo per accedere al relativo pulsante. Quindi, modifichiamo inizializza metodo come segue:

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

Come puoi vedere, accederemo al pulsante tramite il add_new_item_button metodo e definiamo cosa vogliamo che avvenga quando viene cliccato. Riavvia l'applicazione e prova a fare clic sul pulsante. Nella console dovresti vedere il messaggio OMG! SONO CLICCATO quando fai clic sul pulsante.

Tuttavia, ciò che vogliamo che accada quando facciamo clic su questo pulsante è mostrare una nuova finestra per salvare un elemento ToDo. Hai indovinato:sono le ore di Glade.

Crea la finestra del nuovo elemento

  • Crea un nuovo progetto in Glade premendo l'icona più a sinistra nella barra in alto o selezionando File> Nuovo dal menu dell'applicazione.
  • Trascina una Finestra dalla sezione Widget all'area Design.
  • Controlla il suo Composito e denomina la classe TodoNewItemWindow .

  • Trascina una Griglia dalla sezione Widget e posizionarlo nella finestra che abbiamo aggiunto in precedenza.
  • Imposta 5 righe e 2 colonne nella finestra che si apre.
  • Nel Generale scheda della sezione Proprietà, imposta la spaziatura di righe e colonne su 10 (pixel).
  • Nel Comune scheda della sezione Proprietà, imposta Spaziatura widget> Margini> Alto, Basso, Sinistra, Destra tutto a 10 in modo che i contenuti non siano attaccati ai bordi della griglia.

  • Trascina quattro Etichetta widget dalla sezione Widget e posizionarne uno in ogni riga della griglia.
  • Cambia la loro Etichetta proprietà, dall'alto verso il basso, come segue:
    • ID:
    • Titolo:
    • Note:
    • Priorità:
  • Nel Generale scheda della sezione Proprietà, cambia Allineamento e riempimento> Allineamento> Orizzontale proprietà da 0,50 a 1 per ogni proprietà per allineare a destra il testo dell'etichetta.
  • Questo passaggio è facoltativo ma consigliato. Non legheremo quelle etichette nella nostra finestra poiché non abbiamo bisogno di alterare il loro stato o comportamento. In questo contesto, non è necessario impostare un ID descrittivo per loro come abbiamo fatto per il add_new_item_button pulsante nella finestra dell'applicazione. MA aggiungeremo più elementi al nostro design e la gerarchia dei widget in Glade sarà difficile da leggere se dicono label1 , etichetta2 , ecc. Impostazione di ID descrittivi (come id_label , etichetta_titolo , note_etichetta , etichetta_priorità ) ci semplificherà la vita. Ho persino impostato l'ID della griglia su main_grid perché non mi piace vedere numeri o nomi di variabili negli ID.

  • Trascina un Etichetta dalla sezione Widget alla seconda colonna della prima riga della griglia. L'ID verrà generato automaticamente dal nostro modello; non consentiamo la modifica, quindi un'etichetta per visualizzarla è più che sufficiente.
  • Imposta il ID proprietà su id_value_label .
  • Imposta Allineamento e riempimento> Allineamento> Orizzontale proprietà a 0 quindi il testo si allinea a sinistra.
  • Legheremo questo widget alla nostra classe Window in modo da poter cambiare il suo testo ogni volta che carichiamo la finestra. Pertanto, l'impostazione di un'etichetta tramite Glade non è richiesta, ma rende il design più vicino a come apparirà quando viene eseguito il rendering con i dati effettivi. Puoi impostare un'etichetta su ciò che ti si addice meglio; Ho impostato il mio su id-of-the-todo-item-here .

  • Trascina un Inserimento di testo dalla sezione Widget alla seconda colonna della seconda riga della griglia.
  • Imposta la sua proprietà ID su title_text_entry . Come avrai notato, preferisco ottenere il tipo di widget nell'ID per rendere più leggibile il codice della classe.
  • Nel Comune scheda della sezione Proprietà, controlla Spaziatura widget> Espandi> Orizzontale casella di controllo e attivare l'interruttore accanto ad essa. In questo modo, il widget si espanderà orizzontalmente ogni volta che il suo genitore (ovvero la griglia) viene ridimensionato.

  • Trascina una Visualizzazione testo dalla sezione Widget alla seconda colonna della terza riga della griglia.
  • Imposta il suo ID a note . No, ti sto solo mettendo alla prova. Imposta il suo ID proprietà a notes_text_view .
  • Nel Comune scheda della sezione Proprietà, controlla Spaziatura widget> Espandi> Orizzontale, Verticale caselle di controllo e attivare gli interruttori accanto a loro. In questo modo, il widget si espanderà orizzontalmente e verticalmente ogni volta che il suo genitore (la griglia) viene ridimensionato.

  • Trascina una Casella combinata dalla sezione Widget alla seconda colonna della quarta riga della griglia.
  • Imposta il suo ID a priority_combo_box .
  • Nel Comune tab of the Properties section, check the Widget Spacing> Expand> Horizontal checkbox and turn on the switch to its right. This allows the widget to expand horizontally every time its parent (the grid) is resized.
  • This widget is a drop-down element. 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 value. 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 ~ directory. 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

Note: 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. Tieni sotto controllo le specifiche del tuo computer Linux con questa applicazione desktop

  2. Crea un SDN su Linux con open source

  3. Crea un'esperienza Linux unica con l'ambiente desktop Unix

  4. Personalizza il tuo desktop Linux con KDE Plasma

  5. Come abbiamo creato un'app desktop Linux con Electron

Come creare collegamenti su desktop Linux

Crea un'unità USB avviabile con USBImager in Linux

Come utilizzare PostgreSQL con l'applicazione Ruby On Rails

Come creare un gruppo di volumi in Linux con LVM

Divertiti con Twitch su Linux con l'applicazione GNOME Twitch

Come creare un collegamento sul desktop?