Neues Projekt: Knights of Ni

Knights of Ni - kleiner Manager für den statischen Webseitengenerator Nikola

/images/non/non_window.thumbnail.png

Neues kleines GitHub-Übungsprojekt aus Glade und Python zusammengezimmert: Knights of Ni.

Inwiefern ich das weiter ausbaue, entscheide ich operativ; jetzt widme ich mich erstmal wieder weiter den Tutorial-Artikeln.

Exterminate!

Das VTE-Terminal-Widget

/images/11_terminal.thumbnail.png

Glade

Das Widget findet man in der Widget-Seitenleiste ganz unten und stellt ein fertiges Terminal bereit. Um das Terminal auf exit zu schließen, muss das Signal child-exited abgefangen werden.

Ein Klick auf den Button soll innerhalb dieses Terminals eine Python-Konsole starten, hier wird also das clicked-Signal belegt.

Python

Elemente außerhalb des Gtk-Moduls, die mit Glade verwendet werden, müssen als GObject-Typ registriert werden (dies betrifft beispielsweise auch das GtkSource.View-Widget (Modul GtkSource):

GObject.type_register(Vte.Terminal)

Das Terminal wird mit der Funktion spawn_sync initiiert, die ganze 7 Parameter erwartet. Die Dokumentation liefert Details, für eine einfache Bash kommt man mit viel Defaults und Nones aus:

terminal.spawn_sync(
        Vte.PtyFlags.DEFAULT,
        None,
        ["/bin/bash"],
        None,
        GLib.SpawnFlags.DEFAULT,
        None,
        None,
        )

Um eine Eingabe an die Konsole zu schicken, bedarf es der Funktion feed_child. Als Parameter müssen übergeben werden zum einen der String (inklusive newline, um einen Befehl auszuführen) und die Länge des Strings:

command = "python\n"
x.terminal.feed_child(command,len(command))

Die Ausgabe ins Terminal kann mit der Funktion get_text() abgefangen werden. Die Funktion gibt ein Tupel zurück, dessen erstes Element der Ausgabestring ist. Dieser enthält allerdings den gesamten Terminalinhalt, also auch viele Leerzeilen, die sich mit herkömmlichen String-Operationen beseitigen lassen.

widget.get_text()[0].rstrip()

Weiterlesen…

YouNiversity: Transforming Code into Beautiful, Idiomatic Python

Romani ite domum

Lokalisation mit gettext und locale

/images/10_lokalisation.thumbnail.png

Glade

Strings von Labels oder Menüs sind standardmäßig als übersetzbar konfiguriert (Checkbox unter "Beschriftung"), insofern muss hier nichts weiter beachtet werden. Glade-Projektdateien werden direkt von GetText verarbeitet.

Python

Übersetzbare Strings

Zur Übersetzung freigegebene Strings werden durch eine Einklammerung mit vorausgehendem Unterstrich markiert und beim Aufruf von xgettext erkannt:

_ = gettext.gettext
translatable_string = _("translate me")

(bind)textdomain einrichten

Nun muss man Python noch zeigen, unter welchem Namen und Pfad die MO-Dateien (siehe unten) zu finden sind:

locale.bindtextdomain(appname,locales_dir)
locale.textdomain(locales_dir)
gettext.bindtextdomain(appname,locales_dir)
gettext.textdomain(appname)
builder.set_translation_domain(appname)

set_translation_domain muss vor dem Laden der Glade-Datei(en) aufgerufen werden.

GetText

POT

POT steht für Portable Object Template und ist dem Namen zufolge die Vorlage für Übersetzungen. Diese Datei enthält alle übersetzbaren Strings. Nachdem eine leere POT-Datei erstellt wurde, ruft man nun xgettext nach folgendem Muster für alle Quelldateien auf:

$ xgettext --options -o output.pot sourcefile.ext

Die erkannten Strings werden nach dem Schema

#: sourcefile.ext:line number
msgid "translatable string"
msgstr ""

der angegebenen POT-Datei hinzugefügt. Die Markierung der Fundstelle(n) des Strings kann mit der Option --no-location verhindert werden.

Für das Beispiel wird also je ein Aufruf für die Glade- und Python-Datei benötgt:

$ xgettext --sort-output --keyword=translatable --language=Glade -j -o 10_localization/TUT.pot 10_lokalisation.glade
$ xgettext --language=Python -j -o 10_localization/TUT.pot 10_lokalisation.py

Mit der Option -j (--join-existing) wird eine bestehende Datei um zusätzliche Strings ergänzt und funktioniert deshalb sowohl bei der Initiierung (vorher einfach mit touch template.pot die leere Datei erstellen) als auch bei erneutem Aufruf zum Aktualisieren neuer Strings.

PO

Die übersetzten Strings werden in jeweils einer PO-Datei gespeichert. Eine neue Übersetzung legt man mit

$ msginit --input=source.pot --locale=xx
# xx=language code

an, das eine PO-Datei mit dem Namen xx.po (z.B. de.po) anlegt. Diese kann direkt im Texteditor oder mittels Tools wie PoEdit bearbeitet werden. Die deutschsprachige Lokalisation wird also angelegt mit

$ msginit --input=TUT.pot --locale=de

Wird die POT-Datei verändert, kann man die PO-Dateien mit msgmerge abgleichen und anschließend die neuen Strings übesetzen:

$ msgmerge lang.po template.pot > new_lang.po

MO

MO-Dateien sind auf Maschinenlesbarkeit optimierte PO-Dateien und letztlich die, die vom Programm benutzt werden. Unterhalb der angegebenen bindtextdomain liegen die Lokalisationsdateien nach der Verzeichnisstruktur (path/to/bindtextdomain)/locale/language code/LC_MESSAGES/appname.po

Im Beispiel wird die bindtextdomain einfach im lokalen Verzeichnis angelegt, die erzeugte de.po wird mit msgfmt in die MO-Datei überführt:

$ msgfmt --output locale/de/LC_MESSAGES/TUT.mo de.po

Tipps

xgettext-Optionen

--no-location
Ausgabe der Zeilennummer(n) und Datei (als Kommentar) des Strings verhindern
--omit-header
Überschreiben der Header-Informationen verhindern

Obsolete Strings entfernen

Strings, die aus der POT entfernt werden, bleiben in den Übersetzungen erhalten. Dies lässt sich durch den Aufruf von

$ msgattrib --set-obsolete --ignore-file=PRJ.pot -o xx.po xx.po

beheben.

Weiterlesen…

Überlistet

Daten in ListStore speichern und mit ComboBox und TreeView anzeigen

Für die Speicherung und Anzeige von Daten in Listen- oder Tabellenform benötigt man in GTK+-Anwendungen verschiedene Elemente:

  1. Im Modell werden die Daten verwaltet, es gibt zwei Typen:

    • ListStore: flache Liste, die Spalten können neben Text-, Zahlenwerten auch GTK+-Elemente (z.B. Buttons, Checkboxen) enthalten
    • TreeStore: funktioniert prinzipiell wie ListStore, Zeilen können ihrerseits Kind-Einträge besitzen, Daten können im Gegensatz zu ListStore nicht in Glade angegeben werden (TreeStore-Artikel)
  2. Widgets:

    • TreeView: dieses Widget eignet sich zum Anzeigen, Sortieren, Bearbeiten von Daten, wird von beiden Modelltypen verwendet; es können parallel mehrere TreeView-Widgets angelegt werden, die auf die selbe Datenbasis (Modell) zurückgreifen, aber zum Beispiel verschiedene Spalten anzeigen
    • ComboBox: Comboboxen dienen der Auswahl aus einer gegebenen Liste, deren Datenbasis ein List- oder TreeStore sein kann (siehe Artikel zu Spinbutton und Combobox)
    • CellRenderers: Unterwidgets, in denen die anzuzeigenden Daten, deren Layout und weitere Optionen wie Bearbeitbarkeit festgelegt werden
/images/09_treestore2.thumbnail.png

Glade

ListStore

Um die Vielseitigkeit von ListStore zu skizzieren, wird im Beispiel ein Gtk.ListStore (zu finden in der Elementauswahl links unter "Sonstiges > Listenverwahrung") erstellt und von drei Widgets verwendet.

Zunächst werden ein paar Spalten erstellt. ListStore-Daten lassen sich direkt in Glade eingeben. Dies ist allerdings nur für wenige Zeilen und Spalten praktikabel und übersichtlich. Selbst wenige Daten würde ich immer direkt im Python-Code einlesen.

Wie man sieht, werden Änderungen im ListStore (Sortierung, Inhalt) sofort in allen Widgets aktualisiert, die auf dieses Objekt zugreifen. Für verschiedene Sortierungen des selben List-/TreeStores muss man Gtk.TreeModelSort anwenden (Beispiel siehe TreeStore-Artikel).

/images/09_treestore1.thumbnail.png

Widgets

ComboBox
Als "Baumansichtsmodell" wird wie auch bei den folgenden Widgets der ListStore ausgewählt. Über "Edit > Hierarchie" ein CellRendererText hinzugefügt. Im ersten Feld ("Text") stellt man ein, aus welcher Spalte das Dropdown-Menü angezeigt werden soll. Um die Auswahl zu verarbeiten, wird das Signal changed belegt.
TreeView #1
Das erste TreeView-Widget wird innerhalb eines Gtk.ScrolledWindow-Containers angelegt. Wie bei ComboBox werden nun beliebige CellRenderer angelegt. Wird der Sortierungsanzeiger aktiviert, können die Spalten mit Klick auf den Spaltenkopf sortiert werden. In der Sortierspaltenkennung wird die Spalte angegeben, nach der sortiert werden soll, auf diese Weise kann man eine Spalte auch gemäß einer anderen Spalte sortieren (hier im Beispiel wird die mittlere Spalte nach der letzten sortiert, die Sortierung der beiden hinteren Spalten liefert also das gleiche Ergebnis.
TreeView #2
Das zweite TreeView-Widget wird innerhalb eines Sichtfeldes (Gtk.Viewport) erstellt. Dieser Container bietet keine Scrollbalken, das Widget vergrößert automatisch, so dass alle Zeilen sichtbar sind. Bei größeren Tabellen ist ein ScrolledWindow also praktikabler. Es werden die gleichen Daten angezeigt wie zuvor, allerdings ohne Sortierungsanzeiger, dafür wird die mittlere Spalte ("Description") editierbar gemacht und erhält eine Funktion für das Signal edited.
Button
Ein Klick auf den Button soll jeweils eine weitere Zeile zum ListStore hinzufügen, es wird also das clicked-Signal belegt.

Python

TreeStore

Die in TreeStore vorhandenen Zeilen lassen sich einfach über for row in store abrufen. Neue Zeilen lassen sich mit append hinzufügen, andere Optionen wären insert oder remove, um Zeilen an bestimmten Positionen einzufügen oder zu entfernen.

ComboBox

Normalerweise benötigt man für den Zugang zu einer Datenzeile einen TreeIter, das Objekt, das auf den Pfad im Modell zeigt (alternativ kann man diese auch über TreePath ansprechen).

iter,model = widget.get_active_iter(),widget.get_model()
row = model[iter]
print("Selection:",row[0])

Zellen bearbeiten

Das edited-Signal übergibt als Parameter die bearbeitete Zeile und den neuen Zelleninhalt. Dieser muss allerdings explizit als neuer Zelleninhalt übergeben werden, sonst zeigt die Zelle nach der Bearbeitung wieder den alten Inhalt an. Dafür kann man einfach die vom Widget übergebene Position (TreePath) statt des TreeIters verwenden.

def on_cellrenderer_descr_edited(self,widget,pos,edit):
    x.store[int(pos)][1] = edit

Listings

Glade

09_liststore.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkListStore" id="liststore">
    <columns>
      <!-- column-name name -->
      <column type="gchararray"/>
      <!-- column-name descr -->
      <column type="gchararray"/>
      <!-- column-name num -->
      <column type="gint"/>
    </columns>
    <data>
      <row>
        <col id="0" translatable="yes">one</col>
        <col id="1" translatable="yes">textextext</col>
        <col id="2">12345</col>
      </row>
      <row>
        <col id="0" translatable="yes">two</col>
        <col id="1" translatable="yes">bla blubb</col>
        <col id="2">479</col>
      </row>
      <row>
        <col id="0" translatable="yes">three</col>
        <col id="1" translatable="yes"></col>
        <col id="2">0</col>
      </row>
    </data>
  </object>
  <object class="GtkWindow" id="window">
    <property name="width_request">300</property>
    <property name="can_focus">False</property>
    <signal name="destroy" handler="on_window_destroy" swapped="no"/>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkComboBox" id="cbox">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="model">liststore</property>
            <property name="entry_text_column">0</property>
            <signal name="changed" handler="on_cbox_changed" swapped="no"/>
            <child>
              <object class="GtkCellRendererText"/>
              <attributes>
                <attribute name="text">0</attribute>
              </attributes>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkBox">
            <property name="width_request">150</property>
            <property name="height_request">250</property>
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="orientation">vertical</property>
            <child>
              <object class="GtkScrolledWindow">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="shadow_type">in</property>
                <child>
                  <object class="GtkTreeView">
                    <property name="visible">True</property>
                    <property name="can_focus">True</property>
                    <property name="model">liststore</property>
                    <property name="headers_clickable">False</property>
                    <child internal-child="selection">
                      <object class="GtkTreeSelection"/>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Name</property>
                        <property name="sort_indicator">True</property>
                        <property name="sort_column_id">0</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">0</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Description</property>
                        <property name="sort_indicator">True</property>
                        <property name="sort_column_id">2</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">1</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Number</property>
                        <property name="sort_indicator">True</property>
                        <property name="sort_column_id">2</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">2</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkViewport">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <child>
                  <object class="GtkTreeView">
                    <property name="visible">True</property>
                    <property name="can_focus">True</property>
                    <property name="model">liststore</property>
                    <child internal-child="selection">
                      <object class="GtkTreeSelection"/>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Name</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">0</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Description</property>
                        <child>
                          <object class="GtkCellRendererText" id="cellrenderer_descr">
                            <property name="editable">True</property>
                            <signal name="edited" handler="on_cellrenderer_descr_edited" swapped="no"/>
                          </object>
                          <attributes>
                            <attribute name="text">1</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Number</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">2</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="add_row_button">
            <property name="label">gtk-add</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <property name="use_stock">True</property>
            <property name="always_show_image">True</property>
            <signal name="clicked" handler="on_add_row_button_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
    <child type="titlebar">
      <placeholder/>
    </child>
  </object>
</interface>

Python

09_liststore.py (Source)

#!/usr/bin/python
# -*- coding: utf-8 -*-

import gi
gi.require_version('Gtk','3.0')
from gi.repository import Gtk

class Handler:

    def on_window_destroy(self,*args):
        Gtk.main_quit()

    def on_cbox_changed(self,widget):
        iter,model = widget.get_active_iter(),widget.get_model()
        row = model[iter]
        print("Selection:",row[0])

    def on_cellrenderer_descr_edited(self,widget,pos,edit):
        x.store[int(pos)][1] = edit

    def on_add_row_button_clicked(self,widget):
        x.store.append(list(x.more_rows[len(x.store)-3]))
        #set button inactive when all rows are appended
        if len(x.store) == 7:
            x.button.set_sensitive(False)

class Example:

    def __init__(self):

        self.builder = Gtk.Builder()
        self.builder.add_from_file("09_liststore.glade")
        self.builder.connect_signals(Handler())

        window = self.builder.get_object("window")
        window.show_all()

        self.button = self.builder.get_object("add_row_button")
        self.store = self.builder.get_object("liststore")

        #print all values
        [print(row[:]) for row in self.store]

        self.more_rows = [("four","",5739),
                            ("five","",120),
                            ("six","",4),
                            ("seven","lucky number",7)]

    def main(self):
        Gtk.main()

x = Example()
x.main()