Springe zum Hauptinhalt

Dateiauswahldialog

FileChooserDialog

Der Gtk.FileChooserDialog ist eine Subclass von Gtk.Dialog (siehe Artikel zu Dialogen) und ermöglicht das Auswählen und Speichern von Dateien oder Ordnern.

/images/16_fcd.thumbnail.png

Glade

Den Dialog findet man in der Widget-Seitenleiste oben unter "Oberste Ebene". Neben dem Dateibrowser besitzt er eine erweiterbare interne Gtk.Box für weitere Widgets sowie eine Gtk.ButtonBox als interne "action area" für Buttons.

Es ist erforderlich anzugeben, für welche Aktion der Dialog gedacht ist, was Gtk.FileChooserAction entspricht (siehe Python GI API Reference): Datei öffnen oder speichern, Ordner auswählen oder anlegen.

Action area und Responses

Responses sind Antwortkennungen, die beim Auslösen des Signals response übergeben werden. Buttons in der "action area" werden jeweils Response-Werte zugewiesen anstatt das clicked-Signal der Buttons zu nutzen (weitere Erklärungen dazu im Artikel zu Dialogen).

Standardmäßig wird die "action area" unter dem Dateibrowserbereich angelegt.

/images/16_fcd_glade.thumbnail.png

Verwendet man den FileChooserDialog ohne Glade (siehe unten), werden die Buttons in der Headerbar angezeigt. Letzteres sollte aber vermutlich der Standard sein, da es eine Warnung ausgegeben wird, die die Funktionalität des Dialogs allerdings nicht beeinträchtigt:

Gtk-WARNING **: Content added to the action area of a dialog using header bars

Diese Meldung wird nicht angezeigt, wenn man darauf verzichtet, in Glade Buttons zur intern action area hinzuzufügen. Dies betrifft auch andere Dialogarten.

Legt man nun in Glade eine Headerbar mit Buttons an, ist es standardmäßig nicht möglich, diesen Buttons Response-Werte zuzuweisen.

Dafür gibt es (mindestens) zwei Lösungsmöglichkeiten:

XML-Datei

Man legt die Headerbar mit Button(s) an, anschließend öffnet man die Glade-Datei in einem Texteditor und fügt dem Element <action-widgets> die entsprechenden Zeilen hinzu:

<object class="GtkFileChooserDialog" id="filechooser_dialog">
  <property abc ></property>
  <property xyz ></property>
  <!-- usw. -->
  <action-widgets>
    <!-- Buttons innerhalb der action area -->
    <action-widget response="0">button1</action-widget>
    <action-widget response="1">button2</action-widget>
    <!-- Button in Headerbar -->
    <action-widget response="-1">hb_button</action-widget>
  </action-widgets>
  <!-- usw. -->
</object>

Dies funktioniert zwar, ist aber ganz sicher nicht so gedacht, weil diese Änderung beim erneuten Bearbeiten der Glade-Datei wieder rückgängig gemacht wird.

add_action_widget-Funktion

Mit der Funktion add_action_widget können aktivierbare Widgets zur action area hinzugefügt und damit ebenfalls per response-Signal verarbeitet werden. Dies sind Widgets der Gtk.Activatable-Klasse und beinhaltet die Widgets Buttons, MenuItem, RecentChooserMenu, Switch und ToolItem.

Ein Button wird nach dem Schema

widget.add_action_widget(button, response)

hinzugefügt. Wichtig ist es, beim Button die Widget-Eigenschaft "can-default" zu aktivieren:

button.set_property("can-default", True)

Im Beispiel erhält der Dialog die beiden Standardbuttons "OK"/"Cancel":

button = Gtk.Button.new_with_label("Cancel")
button.set_property("can-default", True)
self.obj("filechooser_dialog").add_action_widget(button, Gtk.ResponseType.CANCEL)
button = Gtk.Button.new_with_label("OK")
button.set_property("can-default", True)
self.obj("filechooser_dialog").add_action_widget(button, Gtk.ResponseType.OK)

Um die Dateiauswahl auch auf Doppelklick zu ermöglichen, wird neben des response-Signals noch das Signal file-activated benötigt.

Vorschau-Widget

Der Dialog besitzt die Option, ein Vorschau-Widget einzubinden. Dafür aktiviert man in den Dialog-Eigenschaften "Vorschau-Widget aktiv" und wählt unter "Vorschau-Widget" ein freies Widget (z.B. ein GtkImage). Möglicherweise muss man dieses Widget zunächst in ein leeres Container-Widget erstellen und dort in einen freien Bereich ziehen.

Wenn eine Aktualisierung der Vorschau angefordert wird, wird das Signal update-preview ausgelöst.

FileFilter

FileFilter dienen dazu, Dateien bestimmten Musters anzuzeigen. Pro Filter können mehrere (shell style glob) Patterns oder MIME-Types angegeben werden.

Den Filter findet man in Glade unter "Sonstiges". Im Dialog kann man in den allgemeinen Widget-Einstellungen den gewünschten Filter auswählen. Dies entspricht der set_filter-Funktion.

Python

Dialog ohne Glade

Der FileChooserDialog lässt sich auch ziemlich einfach ohne Glade realisieren, zudem lassen sich die oben genannten Probleme mit Buttons in der Headerbar vermeiden. Der Dialog wird nach folgendem Schema erstellt:

dialog = Gtk.FileChooserDialog(title="window title",
                               parent=parent_window,
                               action=file_chooser_action)
dialog.add_buttons(button1, response1,
                   button2, response2)

Der Dialog wird dann direkt aufgerufen und verarbeitet:

response = dialog.run()
if response == response1:
    ...
elif response == response2:
    ...
dialog.destroy()

FileFilter

Es gibt zwei Möglichkeiten, einen Filefilter anzuwenden:

  1. Ohne Wahl. Der anzuwendende Filter ist voreingestellt:

dialog.set_filter(filter)
  1. Wahl per Dropdown-Menü: Der Nutzer kann zwischen mehreren vorgegebenen Filtern wählen:

dialog.add_filter(filter1)
dialog.add_filter(filter2)
...

Listings

Python

16_filechooser.py (Source)

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

import os
import sys

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GdkPixbuf


class Handler:

    def on_window_destroy(self, window):
        window.close()

    def on_dialog_close(self, widget, *event):
        widget.hide_on_delete()
        return True

    def on_filechooser_dialog_response(self, widget, response):
        if response == -6:
            print("Cancel")
        elif response == -5:
            print("File selection: {}".format(widget.get_filename()))
        self.on_dialog_close(widget)

    def on_filechooser_dialog_file_activated(self, widget):
        self.on_filechooser_dialog_response(widget, -5)

    def on_filechooser_dialog_update_preview(self, widget):
        if widget.get_filename() != None and os.path.isfile(widget.get_filename()):
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(widget.get_filename(),
                                                             200,
                                                             200,
                                                             True,
                                                            )
            app.obj("preview").set_from_pixbuf(pixbuf)

    def on_file_button_clicked(self,widget):
        app.obj("filechooser_dialog").show_all()

    def on_dir_button_clicked(self,widget):
        dialog = Gtk.FileChooserDialog(title="Choose a folder",
                                       parent=app.obj("window"),
                                       action=Gtk.FileChooserAction.SELECT_FOLDER,
                                       )
        dialog.set_default_size(600, 300)
        dialog.add_buttons("Cancel", Gtk.ResponseType.CANCEL,
                           "OK", Gtk.ResponseType.OK)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            print("Folder selection: {}".format(dialog.get_filename()))
        elif response == Gtk.ResponseType.CANCEL:
            print("Cancel")

        dialog.destroy()

class ExampleApp:

    def __init__(self):

        self.app = Gtk.Application.new("org.application.test", Gio.ApplicationFlags(0))
        self.app.connect("activate", self.on_app_activate)
        self.app.connect("shutdown", self.on_app_shutdown)

    def on_app_activate(self, app):
        builder = Gtk.Builder()
        builder.add_from_file("16_filechooser.glade")
        builder.connect_signals(Handler())

        self.obj = builder.get_object
        self.obj("window").set_application(app)
        self.obj("window").show_all()

        #add filters to filechooser dialog
        self.obj("filefilter").set_name("Image files")
        self.obj("filechooser_dialog").add_filter(self.obj("filefilter"))
        self.obj("png_filter").set_name("PNG files")
        self.obj("filechooser_dialog").add_filter(self.obj("png_filter"))
        self.obj("jpg_filter").set_name("JPG files")
        self.obj("filechooser_dialog").add_filter(self.obj("jpg_filter"))

        #add buttons to headerbar of Glade generated dialog
        button = Gtk.Button.new_with_label("Cancel")
        button.set_property("can-default", True)
        self.obj("filechooser_dialog").add_action_widget(button, Gtk.ResponseType.CANCEL)
        button = Gtk.Button.new_with_label("OK")
        button.set_property("can-default", True)
        self.obj("filechooser_dialog").add_action_widget(button, Gtk.ResponseType.OK)

    def on_app_shutdown(self, app):
        self.app.quit()

    def run(self, argv):
        self.app.run(argv)


app = ExampleApp()
app.run(sys.argv)

Glade

16_filechooser.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkFileFilter" id="filefilter">
    <mime-types>
      <mime-type>image/*</mime-type>
    </mime-types>
  </object>
  <object class="GtkFileFilter" id="jpg_filter">
    <mime-types>
      <mime-type>image/jpeg</mime-type>
    </mime-types>
  </object>
  <object class="GtkFileFilter" id="png_filter">
    <mime-types>
      <mime-type>image/png</mime-type>
    </mime-types>
  </object>
  <object class="GtkImage" id="preview">
    <property name="width_request">200</property>
    <property name="visible">True</property>
    <property name="can_focus">False</property>
    <property name="margin_right">5</property>
  </object>
  <object class="GtkApplicationWindow" id="window">
    <property name="width_request">300</property>
    <property name="height_request">200</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>
        <property name="homogeneous">True</property>
        <child>
          <object class="GtkButton" id="file_button">
            <property name="label" translatable="yes">Choose an image file...</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_file_button_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="dir_button">
            <property name="label" translatable="yes">Choose folder...</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_dir_button_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
      </object>
    </child>
    <child type="titlebar">
      <placeholder/>
    </child>
  </object>
  <object class="GtkFileChooserDialog" id="filechooser_dialog">
    <property name="width_request">800</property>
    <property name="height_request">500</property>
    <property name="can_focus">False</property>
    <property name="type_hint">dialog</property>
    <property name="transient_for">window</property>
    <property name="attached_to">window</property>
    <property name="preview_widget">preview</property>
    <property name="use_preview_label">False</property>
    <signal name="delete-event" handler="on_dialog_close" swapped="no"/>
    <signal name="file-activated" handler="on_filechooser_dialog_file_activated" swapped="no"/>
    <signal name="response" handler="on_filechooser_dialog_response" swapped="no"/>
    <signal name="update-preview" handler="on_filechooser_dialog_update_preview" swapped="no"/>
    <child internal-child="vbox">
      <object class="GtkBox" id="fcbox">
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <child internal-child="action_area">
          <object class="GtkButtonBox">
            <property name="can_focus">False</property>
            <child>
              <object class="GtkButton" id="button2">
                <property name="label">gtk-cancel</property>
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="receives_default">True</property>
                <property name="use_stock">True</property>
              </object>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">2</property>
              </packing>
            </child>
            <child>
              <object class="GtkButton" id="button1">
                <property name="label">gtk-apply</property>
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="receives_default">True</property>
                <property name="use_stock">True</property>
              </object>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">3</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">False</property>
            <property name="position">0</property>
          </packing>
        </child>
      </object>
    </child>
    <action-widgets>
      <action-widget response="-6">button2</action-widget>
      <action-widget response="-5">button1</action-widget>
    </action-widgets>
    <child type="titlebar">
      <object class="GtkHeaderBar">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="title">Choose image...</property>
        <property name="show_close_button">True</property>
      </object>
    </child>
    <action-widgets>
      <action-widget response="-6">button2</action-widget>
      <action-widget response="-5">button1</action-widget>
    </action-widgets>
  </object>
</interface>

Desktopintegrationsbemühungen

Desktopintegration: Icon, Headerbar, Kommandozeilenoptionen

(Fortsetzung zum Artikel Gtk.Application)

/images/15_application.thumbnail.png

Glade

Icon

Einem Fenster lässt sich direkt in Glade unter "Allgemein > Darstellung > Symboldatei" ein Icon auswählen. Das Problem dabei ist, dass Glade Bilddateien nur anzeigt, wenn sie sich im selben Verzeichnis wie die Glade-Datei selbst befinden, auch wenn man ein anderes Verzeichnis auswählt.

Am einfachsten behebt man dies, indem man die Gladedatei in einem Texteditor bearbeitet und den (relativen) Pfad zum Icon angibt. Diese Einstellung bleibt auch erhalten, wenn die Datei später wieder mit Glade bearbeitet und gespeichert wird:

<object class="GtkApplicationWindow" id="window">
  ...
  <!-- <property name="icon">duckyou.svg</property> -->
  <property name="icon">../files/duckyou.svg</property>
  ...

Headerbar

Die Headerbar wurde mit GNOME 3.10 eingeführt und vereint Titelleiste und Toolbar in einem Widget, d.h neben Titel und Untertitel können rechts und/oder links verschiedene Widgets (Menüs, Buttons) angelegt sowie clientseitige Fensterkontrollknöpfe angezeigt werden.

Die Headerbar ist optional. Möchte man sie nutzen, muss in den Fenstereinstellungen "Allgemein > Darstellung > Klienseitige Fensterdekoration" ausgewählt werden. Daraufhin erscheint im oberen Bereich des Fensters ein reservierter Bereich, in dem die Headerbar platziert wird. Wird die Headerbar außerhalb davon platziert, wird weiterhin zusätzlich die normale Titelleiste angezeigt.

/images/15_headerbarglade.png

Kommandozeilenoptionen

Gtk.Application stellt die erforderlichen Mittel für anwendungseigene Kommandozeilenoptionen zur Verfügung (Handling command line options in GApplication).

Optionen anlegen

Verfügbare Optionen werden mit der Funktion add_main_option_entries(entrylist) hinzugefügt. Diese Einträge haben das Format GLib.OptionEntry, welches allerlei Parameter besitzt.

def __init__(self):
    self.app = Gtk.Application.new("org.application.test", Gio.ApplicationFlags(0))
    self.app.add_main_option_entries([
        self.create_option_entry("--version", description="Show version numbers and exit"),
        self.create_option_entry("--setlabel", description="Set label widget", arg=GLib.OptionArg.STRING,),
        self.create_option_entry("--bollocks", description="Additional test option - exit"),
    ])

def create_option_entry(self,
                        long_name,
                        short_name=None,
                        flags=0,
                        arg=GLib.OptionArg.NONE,
                        arg_data=None,
                        description=None,
                        arg_description=None):
    option = GLib.OptionEntry()
    option.long_name = long_name.lstrip("-")
    option.short_name = 0 if not short_name else ord(short_name.lstrip("-"))
    option.flags = flags
    option.arg = arg
    option.arg_data = arg_data
    option.description = description
    option.arg_description = arg_description
    return option

Shortnames

Eine Option kann ein aus einem Buchstaben (oder besser gesagt "printable ASCII character different from ‘-‘") bestehenden Synonmym besitzen, den Shortname. Bei der Option --help ist dies gemeinhin -h.

Die short_name-Variable von OptionEntry ist allerdings integer. Die in der Dokumentation nicht ersichtliche Lösung besteht darin, in der Variable die Dezimalkodierung des entsprechenden Zeichens zu übergeben, also etwa 97 für "a". Bei ungültigen Werten wird eine Fehlermeldung ausgegeben. Optionen ohne Shortname erhalten den Wert 0.

Signal verbinden

Der Gtk.Application-eigene "handle-local-options"-Handler verarbeitet die Optionen. Sobald Optionen angelegt sind, wird dieses Signal noch vor dem "startup"-Signal ausgelöst

self.app.connect("handle-local-options", self.on_local_option)

Optionen verarbeiten

Die an die Handler-Funktion übergebene option ist ein Element der Klasse GLib.VariantDict. Mit contains("option") lässt sich nach der übergebenen Option suchen.

def on_local_option(self, app, option):
    if option.contains("option1"):
        #do something and exit normally
        return 0
    elif option.contains("option2"):
        #do something different and exit
        return 0
    elif option.contains("option3"):
        #do more and continue
    return -1

Ein übergebener String kann extrahiert werden, indem GLib.VariantDict mit end() in GLib.Variant konvertiert wird, das sich wiederum mit keys() auslesen lässt:

var = GLib.VariantDict.end(option)
option_string = var[var.keys()[0]]
Ein Return-Wert ist zwingend erforderlich, er entspricht dabei dem Exit-Status:
  • -1: Anwendung wird weiter ausgeführt

  • 0: erfolgreiche Ausführung, Anwendung wird beendet, "startup/activate" werden nicht ausgeführt

  • 1 bzw. positiver Wert: nicht erfolgreiche Ausführung, Anwendung wird beendet

Optionen übergeben

Die Option, die immer verfügbar ist, ist --help. Hier werden unter "Anwendungsoptionen" die angelegten Optionen samt Beschreibung aufgeführt. Die Optionen können wie definiert angegeben werden:

$ python script.py --version
Python: 3.6.0
GTK+:   3.22.6

oder mit --setlabel einen String an Gtk.Label übergeben:

$ python script.py --setlabel "I can haz options!"

Listings

Python

15_application.py (Source)

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

import sys
import setproctitle

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GLib


class ExampleApp:

    def __init__(self):

        setproctitle.setproctitle("Application test")
        GLib.set_prgname("testapp")

        self.app = Gtk.Application.new("org.application.test", Gio.ApplicationFlags(0))

        self.app.add_main_option_entries([
            self.create_option_entry("--version",
                                     description="Show version numbers and exit",
                                    ),
            self.create_option_entry("--setlabel",
                                     description="Set label widget",
                                     arg=GLib.OptionArg.STRING,
                                    ),
            self.create_option_entry("--bollocks",
                                     description="Additional test option - exit",
                                    ),
        ])

        self.app.connect("handle-local-options", self.on_local_option)
        self.app.connect("activate", self.on_app_activate)

    def on_local_option(self, app, option):
        self.option_string = ""
        if option.contains("version"):
            var = GLib.VariantDict.end(option)
            print("Python: {}".format(sys.version[:5]))
            print("GTK+:   {}.{}.{}".format(Gtk.MAJOR_VERSION,
                                             Gtk.MINOR_VERSION,
                                             Gtk.MICRO_VERSION,
                                             ))
            return 0
        elif option.contains("bollocks"):
            return 1
        elif option.contains("setlabel"):
            var = GLib.VariantDict.end(option)
            self.option_string = var[var.keys()[0]]
        return -1

    def create_option_entry(self,
                            long_name,
                            short_name=None,
                            flags=0,
                            arg=GLib.OptionArg.NONE,
                            arg_data=None,
                            description=None,
                            arg_description=None,
                            ):
        option = GLib.OptionEntry()
        option.long_name = long_name.lstrip("-")
        option.short_name = 0 if not short_name else ord(short_name.lstrip("-"))
        option.flags = flags
        option.arg = arg
        option.arg_data = arg_data
        option.description = description
        option.arg_description = arg_description
        return option

    def on_app_activate(self, app):
        builder = Gtk.Builder()
        builder.add_from_file("15_application.glade")

        self.obj = builder.get_object
        self.obj("window").set_application(app)
        self.obj("label").set_text(self.option_string)
        self.obj("window").show_all()

    def run(self, argv):
        self.app.run(argv)


app = ExampleApp()
app.run(sys.argv)

Glade

15_application.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkMenu" id="menu">
    <property name="visible">True</property>
    <property name="can_focus">False</property>
    <child>
      <object class="GtkMenuItem">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="label" translatable="yes">MenuItem 1</property>
        <property name="use_underline">True</property>
      </object>
    </child>
    <child>
      <object class="GtkMenuItem">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="label" translatable="yes">MenuItem 2</property>
        <property name="use_underline">True</property>
      </object>
    </child>
    <child>
      <object class="GtkMenuItem">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="label" translatable="yes">MenuItem 3</property>
        <property name="use_underline">True</property>
      </object>
    </child>
  </object>
  <object class="GtkApplicationWindow" id="window">
    <property name="width_request">400</property>
    <property name="height_request">300</property>
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">Titel</property>
    <property name="icon">../files/duckyou.svg</property>
    <property name="show_menubar">False</property>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkImage">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="pixbuf">../files/duckyou.svg</property>
          </object>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkLabel" id="label">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="pack_type">end</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkSeparator">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
    <child type="titlebar">
      <object class="GtkHeaderBar">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="title">Titel</property>
        <property name="subtitle">Untertitel</property>
        <property name="show_close_button">True</property>
        <child>
          <object class="GtkMenuButton">
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <property name="popup">menu</property>
            <child>
              <placeholder/>
            </child>
          </object>
        </child>
        <child>
          <object class="GtkButton">
            <property name="label">gtk-no</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>
          </object>
          <packing>
            <property name="pack_type">end</property>
            <property name="position">2</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton">
            <property name="label">gtk-yes</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>
          </object>
          <packing>
            <property name="pack_type">end</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>

Selbständig

Programm als Gtk.Application laufen lassen

Gtk.Application handhabt verschiedene wichtige Aspekte einer GTK+-Anwendung, wie etwa der GTK+-Initialisierung, dem Sessionmanagement und der Desktopintegration.

/images/14_application.thumbnail.png

XML-Dateien

Glade

In Glade verändert sich im Prinzip nichts. Als Hauptfenster sollten Gtk.ApplicationWindows zum Einsatz kommen. Als Beispiel wird hier das Gladefile aus dem Artikel zu Dialogen wieder verwendet.

GMenu

Die GNOME-Shell unterstützt Appmenüs, erreichbar über das obere Panel. Die XML-Datei muss so formatiert sein, dass sie als GioMenu erkannt wird:

<?xml version="1.0"?>
<interface>
 <menu id="appmenu">
    <section>
      <item>
        <attribute name="label" translatable="yes">Menu Item</attribute>
        <attribute name="action">app.item</attribute>
      </item>
    </section>
  </menu>
</interface>

Von Glade selbst würde diese XML-Datei als veraltetes Format erkannt, aber sie lässt sich trotzdem von GtkBuilder laden und anschließend kann man die Identifier nutzen.

Bemerkung

Es ist geplant, dass die Appmenüs aufgrund der fehlenden Akzeptanz in naher Zukunft aus dem GNOME-Desktop entfernt werden. Siehe Ankündigung Farewell, application menus!.

Python

Initialisierung von GtkApplication

Bei der Initialisierung wird eine application_id- und flags-Angabe benötigt. Letztere können in der Regel bei 0 bzw. FLAGS_NONE belassen werden (siehe Gio.ApplicationFlags), die Konventionen für die application_id sind hier dokumentiert.

Die Application kann nun mit verschiedenen Signalen verbunden werden, die zu bestimmten Ereignissen ausgelöst werden, aber es muss mindestens activate verbunden werden:

def __init__(self):

    self.app = Gtk.Application.new("org.application.test", 0)
    # self.app.connect("startup", self.on_app_startup) # optional
    self.app.connect("activate", self.on_app_activate)
    # self.app.connect("shutdown", self.on_app_shutdown) # optional

def on_app_activate(self, app):

    # setting up GtkBuilder etc.
    ...
    ...
    ...

Appmenu

Wie oben bereits erwähnt, lässt sich die GMenu-XML von GtkBuilder laden, dann wird das Menü der Application zugewiesen:

builder.add_from_file("menu.ui")
app.set_app_menu(builder.get_object("appmenu"))

Die zu den Menüeinträgen verknüpften Funktionen müssen nun als Actions, genauer GioSimpleActions, erstellt und analog zur herkömmlichen Signalverknüpfung über connect verbunden werden.

def add_simple_action(self, name, callback):
    action = Gio.SimpleAction.new(name)
    action.connect("activate", callback)
    self.app.add_action(action)

Im Beispiel werden Actions zum Aufrufen der Dialoge erstellt.

Starten und Beenden

GtkApplication übernimmt die Handhabung des GTK+-Mainloops, das heißt, es nicht mehr notwendig GTK+ manuell zu starten oder zu beenden. Stattdessen werden run() und quit() verwendet:

Gtk.main()      ->  app.run(argv)
Gtk.main_quit() ->  app.quit()

Beendet man das Programm über den [X]-Button oder den "Schließen"-Eintrag des Appmenus (immer vorhanden), wird automatisch das "shutdown"-Signal ausgelöst (siehe oben). Das heißt, es müssen keine entsprechenden Signale definiert werden. "Shutdown" wird auch ausgelöst, wenn es bei der Initialisierung nicht mit einer Funktion verbunden wird.

Listings

Python

14_application.py (Source)

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

import sys

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio


class Handler:

    def on_window_destroy(self,window):
        window.close()

    def on_dialog_delete_event(self,widget,event):
        widget.hide_on_delete()
        return True

    def on_aboutbutton_clicked(self,widget):
        app.obj("aboutdialog").show_all()

    def on_messagebutton_clicked(self,widget):
        app.obj("messdialog").format_secondary_text("")
        app.obj("messdialog").show_all()

    def on_dialog_response(self,widget,response):
        if response == -8:
            widget.hide_on_delete()
        elif response == -9:
            widget.format_secondary_text("Doch!")

class ExampleApp:

    def __init__(self):

        self.app = Gtk.Application.new("org.application.test",
                                        Gio.ApplicationFlags(0),
                                       )
        self.app.connect("startup", self.on_app_startup)
        self.app.connect("activate", self.on_app_activate)
        self.app.connect("shutdown", self.on_app_shutdown)

    def on_app_startup(self, app):
        print("Gio.Application startup signal emitted")

    def on_app_activate(self, app):
        print("Gio.Application activate signal emitted")
        builder = Gtk.Builder()
        builder.add_from_file("13_dialoge.glade")
        builder.add_from_file("14_giomenu.ui")
        builder.connect_signals(Handler())

        app.set_app_menu(builder.get_object("appmenu"))
        self.obj = builder.get_object
        self.obj("window").set_application(app)

        # display application name in upper panel of the GNOME Shell (deprecated)
        # self.obj("window").set_wmclass("Application test","Application test")
        self.obj("window").show_all()

        self.add_simple_action("about", self.on_action_about_activated)
        self.add_simple_action("message", self.on_action_message_activated)
        self.add_simple_action("quit", self.on_action_quit_activated)

    def on_app_shutdown(self, app):
        print("Gio.Application shutdown signal emitted")

    def add_simple_action(self, name, callback):
        action = Gio.SimpleAction.new(name)
        action.connect("activate", callback)
        self.app.add_action(action)

    def on_action_about_activated(self, action, user_data):
        self.obj("aboutdialog").show_all()

    def on_action_message_activated(self, action, user_data):
        Handler().on_messagebutton_clicked(self)

    def on_action_quit_activated(self, action, user_data):
        self.app.quit()

    def run(self, argv):
        self.app.run(argv)


app = ExampleApp()
app.run(sys.argv)

Glade

13_dialoge.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkApplicationWindow" id="window">
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">Titel</property>
    <signal name="destroy" handler="on_window_destroy" swapped="no"/>
    <child type="titlebar">
      <placeholder/>
    </child>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkButton" id="aboutbutton">
            <property name="label" translatable="yes">GtkAboutDialog</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_aboutbutton_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="messagebutton">
            <property name="label" translatable="yes">GtkMessageDialog</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_messagebutton_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
  <object class="GtkAboutDialog" id="aboutdialog">
    <property name="can_focus">False</property>
    <property name="type_hint">dialog</property>
    <property name="transient_for">window</property>
    <property name="attached_to">window</property>
    <property name="program_name">Glade-Tutorial</property>
    <property name="version">1.0</property>
    <property name="logo_icon_name">help-about</property>
    <property name="license_type">mit-x11</property>
    <signal name="delete-event" handler="on_dialog_delete_event" swapped="no"/>
    <signal name="response" handler="on_dialog_response" swapped="no"/>
    <child type="titlebar">
      <placeholder/>
    </child>
    <child internal-child="vbox">
      <object class="GtkBox">
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <property name="spacing">2</property>
        <child internal-child="action_area">
          <object class="GtkButtonBox">
            <property name="can_focus">False</property>
            <property name="layout_style">end</property>
            <child>
              <object class="GtkButton" id="button1">
                <property name="label">gtk-ok</property>
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="receives_default">True</property>
                <property name="use_stock">True</property>
              </object>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">False</property>
            <property name="position">0</property>
          </packing>
        </child>
      </object>
    </child>
    <action-widgets>
      <action-widget response="-8">button1</action-widget>
    </action-widgets>
  </object>
  <object class="GtkMessageDialog" id="messdialog">
    <property name="can_focus">False</property>
    <property name="type_hint">dialog</property>
    <property name="transient_for">window</property>
    <property name="buttons">yes-no</property>
    <property name="text" translatable="yes">&lt;b&gt;MessageDialog schließen?&lt;/b&gt;</property>
    <property name="use_markup">True</property>
    <signal name="response" handler="on_dialog_response" swapped="no"/>
    <child type="titlebar">
      <placeholder/>
    </child>
    <child internal-child="vbox">
      <object class="GtkBox">
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <property name="spacing">2</property>
        <child internal-child="action_area">
          <object class="GtkButtonBox">
            <property name="can_focus">False</property>
            <property name="homogeneous">True</property>
            <property name="layout_style">end</property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">False</property>
            <property name="position">0</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>

GMenu

14_giomenu.ui (Source)

<?xml version="1.0"?>
<interface>
  <menu id="appmenu">
    <section>
      <item>
        <attribute name="label" translatable="yes">About</attribute>
        <attribute name="action">app.about</attribute>
      </item>
      <item>
        <attribute name="label" translatable="yes">Message</attribute>
        <attribute name="action">app.message</attribute>
      </item>
    </section>
    <section>
      <item>
        <attribute name="label" translatable="yes">Quit</attribute>
        <attribute name="action">app.quit</attribute>
        <attribute name="accel">&lt;Primary&gt;q</attribute>
      </item>
    </section>
  </menu>
</interface>

NoN: Fortschritte

Knights of Ni - Fortschritte

Wie bereits erwähnt, bastel ich aktuell an einer GUI für Nikola, die einige Fortschritte vorzuweisen hat:

  • Titelleiste entfernt, da Headerbar benutzt wird

  • Wechseln zwischen Nikola-Instanzen (zuletzt benutzte wird gespeichert)

  • Bookmarkfunktion (bislang können nur Bookmarks hinzugefügt werden, bei Bedarf manuell aus der config entfernen)

  • Untersützung für mehrsprachige Seiten:

  • Anzeige der Standardsprache und der konfigurierten anderen Sprachen

  • Posts/Pages-Tab zeigt Vorhandensein von Übersetzungen an

  • Tab "Translations" zeigt vorhandene Dateien an

  • per Rechtsklick können neue Übersetzungen angelegt werden, dabei wird der Ausgangsbeitrag gemäß entsprechendem Dateimuster gespeichert

  • ich weiß noch nicht, wie sinnvoll dieser separate Tab ist (Redundanz) und ob ich ihn beibehalte

  • hat der Artikel keinen Titel, wird das interne Kürzel (slug) oder, falls ebenfalls nicht vorhanden, der Dateiname angezeigt

  • gesprächiges Log, um vorzutäuschen, dass ganz viel wichtiger Kram passiert

/images/non/non2.thumbnail.png

Das läuft aus meiner Sicht schon alles erstaunlich gut. Dummerweise habe ich bei der Verwendung von Glade (und auch Python, aber vor allem Glade) inzwischen ebenfalls einige Fortschritte gemacht, was mich ernsthaft zur Überlegung geführt hat, das GoProTool nochmal zu überarbeiten. Die Oberfläche würde ich belassen, aber viel Funktionalität könnte effizienter umgesetzt werden. Ich stelle das mal hinten an.

Geplant ist nun ein simples Nikola-unabhängiges Vorlagensystem und anschließend ist etwas Fleißarbeit bei der Lokalisation erforderlich.

Artikelübersetzungen

Sobald ich herausgefunden habe, wie Multilingualität in Nikola funktioniert, werde ich die Tutorialartikel ins Englische übersetzen.

Nachtrag: okay, war war einfach...

Dialoge

Anzeige von Dialogfenstern

Dialoge sind ergänzende Fenster zur Anwendung und dienen der Interaktion mit dem Benutzer, in denen Informationen angezeigt werden oder Eingaben vom Benutzer abgefragt werden können. Die GtkDialog-Klasse bietet einige Unterklassen für gebräuchliche Anzeigen und Abfragen, wie die im Beispiel verwendeten AboutDialog- und MessageDialog (Artikel zum FileChooserDialog).

/images/13_dialoge.thumbnail.png

Glade

Dialog-Widgets findet man unter "Oberste Ebene" neben den Fenster-Widgets.

Dialoge sind ergänzende Fenster, um den Fokus des Nutzers zu lenken. Sie können direkt an ein übergeordnetes Fenster angeheftet werden, mindestens aber müssen sie unter "Allgemein > Fensterattribute > Vorübergehend für:" einem Eltern-Fenster zugeordnet werden. Sie erscheinen dadurch nicht als separates Fenster in der Übersicht und erben ein vorhandenes Icon.

AboutDialog

Das "About"-Dialogfenster bietet in der Regel Informationen zum Projekt, darunter Version, Lizenz, beteiligte Programmierer, Übersetzer etc. Dies alles lässt sich sehr einfach direkt in Glade angeben.

MessageDialog

Der MessageDialog ist ein Standarddialog zum Anzeigen oder Abfragen von Informationen. Er ist so konfiguriert, dass er keine eigene Fensterdekoration besitzt und nicht als Fenster in der Taskbar erscheint. Außerdem bietet er die Möglichkeit, Standardbuttons einzurichten.

Buttons und Responses

Dialoge verfügen bereits intern über eine Gtk.ButtonBox, die mit beliebigen Buttons befüllt werden kann. Dieser Bereich ist als "intern action_area" gekennzeichnet.

Im Gegensatz zu Buttons in normalen Fenstern müssen in Dialogen keine Signale auf clicked angelegt werden, sondern man legt in den Button-Eigenschaften unter "Allgemein" eine Antwortkennung (Response) fest (int) und belegt das Signal response des GtkDialog.

Standardbuttons wie im MessageDialog auswählbar besitzen vorgegebene Response-Kennungen (siehe Python GI API Reference):

  • Ok -5

  • Abbrechen -6

  • Schließen -7

  • Ja -8

  • Nein -9

  • [X] -4

Der große Vorteil der Responses besteht darin, dass sie sich direkt auf das Dialog-Objekt beziehen; man kann die Responses in einer Funktion verarbeiten und muss dies nicht für jeden einzelnen Button vornehmen.

Wiederherstellbare Dialoge

Das Problem von per destroy-Signal geschlossenen Fenstern besteht darin, dass sie sich nicht wieder aufrufen lassen. Deshalb wird stattdessen das Signal delete-event belegt.

Python

Dialog aufrufen

Da Dialoge auch Gtk.Windows sind, lassen sie sich mit show_all() aufrufen. Die Funktion von Dialogen besteht allerdings in der Regel darin, Nutzereingaben zu erfassen oder Informationen zu vermitteln. Deshalb ruft man die Fenster am besten mit run() auf. Dies bewirkt, dass das Dialogfenster über dem Elternfenster fixiert wird und jenes nicht aktiv ist, bis ein Response-Signal ausgeführt wird.

Responses

Beim Auslösen des response-Signals wird die Antwortkennung als Parameter übergeben, so kann, wie bereits erwähnt, jede Kennung innerhalb einer einzelnen Funktion verarbeitet werden:

def on_dialog_response(self, widget, response):
    if response == 0:
        widget.hide_on_delete()
    elif response == 1:
        do.something()
    elif response == (2 or 3):
        do.something.different()

Delete-event

Mit der Funktion hide_on_delete() ausgeblendete Dialoge oder reguläre Fenster lassen sich mit show_all() wieder anzeigen:

def on_dialog_delete_event(self, widget, event):
    widget.hide_on_delete()
    return True

Mehrere Glade-Dateien

Wie bereits erwähnt, können mehrere Dateien für Fenster und Dialoge innerhalb eines Projektes verwendet werden. Allerdings ist es nicht möglich, diese dateiübergreifend aneinanderzubinden. Hierzu wird die set_transient_for-Funktion von Gtk.Window benötigt:

dialog.set_transient_for(mainwindow)

Die Zugehörigkeit zum Elternwidget wird in Glade in den Eigenschaften unter "Allgemein > Vorübergehend für:" angegeben.

Listings

Python

13_dialoge.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_dialog_delete_event(self, widget, event):
        widget.hide_on_delete()
        return True

    def on_aboutbutton_clicked(self, widget):
        x.obj("aboutdialog").run()

    def on_messagebutton_clicked(self, widget):
        x.obj("messdialog").format_secondary_text("")
        x.obj("messdialog").run()

    def on_dialog_response(self, widget, response):
        if response == -8:
            widget.hide_on_delete()
        elif response == -9:
            widget.format_secondary_text("Doch!")


class Example:

    def __init__(self):

        builder = Gtk.Builder()
        builder.add_from_file("13_dialoge.glade")
        builder.connect_signals(Handler())

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

    def main(self):
        Gtk.main()


x = Example()
x.main()

Glade

13_dialoge.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkApplicationWindow" id="window">
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">Titel</property>
    <signal name="destroy" handler="on_window_destroy" swapped="no"/>
    <child type="titlebar">
      <placeholder/>
    </child>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkButton" id="aboutbutton">
            <property name="label" translatable="yes">GtkAboutDialog</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_aboutbutton_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="messagebutton">
            <property name="label" translatable="yes">GtkMessageDialog</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_messagebutton_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
  <object class="GtkAboutDialog" id="aboutdialog">
    <property name="can_focus">False</property>
    <property name="type_hint">dialog</property>
    <property name="transient_for">window</property>
    <property name="attached_to">window</property>
    <property name="program_name">Glade-Tutorial</property>
    <property name="version">1.0</property>
    <property name="logo_icon_name">help-about</property>
    <property name="license_type">mit-x11</property>
    <signal name="delete-event" handler="on_dialog_delete_event" swapped="no"/>
    <signal name="response" handler="on_dialog_response" swapped="no"/>
    <child type="titlebar">
      <placeholder/>
    </child>
    <child internal-child="vbox">
      <object class="GtkBox">
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <property name="spacing">2</property>
        <child internal-child="action_area">
          <object class="GtkButtonBox">
            <property name="can_focus">False</property>
            <property name="layout_style">end</property>
            <child>
              <object class="GtkButton" id="button1">
                <property name="label">gtk-ok</property>
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="receives_default">True</property>
                <property name="use_stock">True</property>
              </object>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">False</property>
            <property name="position">0</property>
          </packing>
        </child>
      </object>
    </child>
    <action-widgets>
      <action-widget response="-8">button1</action-widget>
    </action-widgets>
  </object>
  <object class="GtkMessageDialog" id="messdialog">
    <property name="can_focus">False</property>
    <property name="type_hint">dialog</property>
    <property name="transient_for">window</property>
    <property name="buttons">yes-no</property>
    <property name="text" translatable="yes">&lt;b&gt;MessageDialog schließen?&lt;/b&gt;</property>
    <property name="use_markup">True</property>
    <signal name="response" handler="on_dialog_response" swapped="no"/>
    <child type="titlebar">
      <placeholder/>
    </child>
    <child internal-child="vbox">
      <object class="GtkBox">
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <property name="spacing">2</property>
        <child internal-child="action_area">
          <object class="GtkButtonBox">
            <property name="can_focus">False</property>
            <property name="homogeneous">True</property>
            <property name="layout_style">end</property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">False</property>
            <property name="position">0</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>

Ansichtssache

Daten anzeigen mit TreeStore

(Fortsetzung zum ListStore-Artikel)

/images/12_treestore.thumbnail.png

TreeStore vs. ListStore

Im Gegensatz zum ListStore können Zeilen eines TreeStores ihrerseits Kind-Elemente besitzen, die append-Funktion benötigt demzufolge ein weiteren Parameter, der einen Bezug zu einer anderen Datenzeile anzeigt:

#append row to liststore
store.append([value1, value2, value3])

#append row to treestore
store.append(parent, [value1, value2, value3])

Der Wert der Variable parent ist entweder

  • None, wenn die Zeile keine übergeordnete Zeile besitzt, oder

  • TreeIter, der zur übergeordneten Zeile zeigt

Der TreeIter wird beim Erstellen einer Zeile erzeugt, untergeordnete Zeilen werden nach folgendem Schema angelegt:

row1 = store.append(None, [value1, value2, value3])
row2 = store.append(row1, [value1, value2, value3])

Man erhält den TreeIter-Wert einer Zeile am einfachsten über die get_selection-Funktion des Gtk.TreeSelection-Widgets von TreeView (wird automatisch angelegt).

Glade

Im Beispiel werden zwei TreeStores und die jeweils enthaltenen Spalten angelegt, dazu die TreeView-Widgets zur Anzeige.

TreeModelSort

Spalten lassen sich mit der Funktion set_sort_column_id einfach sortieren. Wendet man diese Funktion direkt auf TreeStore an, werden logischerweise alle TreeView-Widgets, die darauf zurückgreifen, sortiert.

Für diese Fälle muss man TreeModelSort-Elemente "zwischenschalten", d.h. man erstellt aus der Widget-Seitenleiste unter "Sonstiges > Sortierung für Baumansichtsmodell" (4. Eintrag) ein Widget und weist ihm den gewünschten TreeStore zu (einzige Option unter "Allgemein"). Anschließend ersetzt man im TreeView das Modell mit dem eben erstellten TreeModelSort.

Die Sortierungsfunktion führt man wie zuvor, nur auf das TreeModelSort-Objekt, aus.

TreeModelFilter

TreeModelFilter ermöglicht die Darstellung bestimmter Zeilen, in Glade wird wie bei TreeModelSort verfahren, zuerst das Element anlegen (3. Eintrag unter "Sonstige"), anschließend erfolgen die Zuweisungen zum Modell und TreeView.

Im gewählten Beispiel sollen Sorten nach der Fruchtfarbe sortiert werden, es wird also noch ein Container für Buttons benötigt, also eine GtkButtonBox.

Formatierung aus dem Modell laden

Neben den anzuzeigenden Spalten gibt es im ersten TreeStore eine Spalte "weight". Der Wert in dieser Spalte wird dazu verwendet, die Zelle in Fettschrift darzustellen. Dazu wird in den Eigenschaften des CellRenderers unter Schriftgewicht die entsprechende Spalte angegeben (der Wert für normale Schrift ist 400). Analog dazu können beispielsweise auch Zellen eingefärbt oder weitere Schriftformatierungen vorgenommen werden.

Python

TreeModelSort

Durch die Positionsabfrage von GtkTreeSelection.get_selected() erhält man ein Tupel (model,pos), pos von model zeigt dabei auf TreeModelSort (bzw. analog auf TreeModelFilter), nicht auf TreeStore und erfordert eine Konvertierung:

model, pos = selection.get_selected()
converted_iter = treesort.convert_iter_to_child_iter(pos)
store.set_value(converted_iter, column, value)

TreeModelFilter

Zunächst muss eine Filterfunktion erstellt werden, in der die Sichtbarkeit von Zeilen definiert wird, im Beispiel also die Variable self.color:

def color_filter_func(self, model, iter, data):
    if model[iter][2] == self.color:
        return True
    else:
        return False

Die Funktion wird zunächst nach dem Schema

treefilter.set_visible_func(filter_func)

zugewiesen, jede Filterung wird dann per refilter() ausgelöst, also wenn das Button-Signal ausgelöst wird:

def on_button_clicked(self, widget):
    x.color = widget.get_label()
    x.obj("treefilter").refilter()

Listings

Glade

12_treestore.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_button_clicked(self, widget):
        x.color = widget.get_label()
        x.obj("treefilter").refilter()

    def on_cellrenderer_note_edited(self, widget, row, edit):
        model, pos = x.obj("selection").get_selected()
        conv_iter = x.obj("treesort").convert_iter_to_child_iter(pos)
        x.obj("store").set_value(conv_iter,3,edit)


class Example:

    def __init__(self):

        self.builder = Gtk.Builder()
        self.builder.add_from_file("12_treestore.glade")
        self.builder.connect_signals(Handler())
        self.obj = self.builder.get_object

        #read data from text file
        with open("12_capsicum.txt") as f:
            data = [line.strip("\n") for line in f]

        colors = set()
        species = set()
        varieties = []
        for line in data:
            variety = line.split(";")
            colors.add(variety[2])
            species.add(variety[1])
            try:
                variety[3]
            except IndexError:
                variety.append("")
            varieties.append(variety)

        #append lines to 1st treestore
        for s in species:
            counter = 0
            row = self.obj("store").append(None,
                                            [None, None, None, None, 800, None],
                                            )
            for v in varieties:
                if v[1] == s:
                    self.obj("store").append(row,
                                             [v[0], v[1], v[2], v[3], 400, None],
                                             )
                    counter += 1
            self.obj("store").set_value(row, 0, "{} ({})".format(s, counter))
            self.obj("store").set_value(row, 5, counter)

        #append lines to 2nd treestore
        [self.obj("filterstore").append(None, [v[0], v[1], v[2]]) for v in varieties]

        #create buttons in buttonbox
        for c in colors:
            button = Gtk.Button.new_with_label(c)
            button.connect("clicked",Handler().on_button_clicked)
            self.obj("buttonbox").add(button)

        self.obj("view").expand_all()
        self.obj("treesort").set_sort_column_id(5, Gtk.SortType.DESCENDING)
        self.obj("treefilter").set_visible_func(self.color_filter_func)
        self.obj("window").show_all()

    def color_filter_func(self, model, iter, data):
        if model[iter][2] == self.color:
            return True
        else:
            return False

    def main(self):
        Gtk.main()


x = Example()
x.main()

Python

12_treestore.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkTreeStore" id="filterstore">
    <columns>
      <!-- column-name var -->
      <column type="gchararray"/>
      <!-- column-name spec -->
      <column type="gchararray"/>
      <!-- column-name color -->
      <column type="gchararray"/>
    </columns>
  </object>
  <object class="GtkTreeModelFilter" id="treefilter">
    <property name="child_model">filterstore</property>
  </object>
  <object class="GtkTreeStore" id="store">
    <columns>
      <!-- column-name name -->
      <column type="gchararray"/>
      <!-- column-name spec -->
      <column type="gchararray"/>
      <!-- column-name color -->
      <column type="gchararray"/>
      <!-- column-name note -->
      <column type="gchararray"/>
      <!-- column-name weight -->
      <column type="gint"/>
      <!-- column-name counter -->
      <column type="gint"/>
    </columns>
  </object>
  <object class="GtkTreeModelSort" id="treesort">
    <property name="model">store</property>
  </object>
  <object class="GtkApplicationWindow" id="window">
    <property name="width_request">600</property>
    <property name="height_request">500</property>
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">Titel</property>
    <signal name="destroy" handler="on_window_destroy" swapped="no"/>
    <child>
      <object class="GtkBox" id="box">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <property name="spacing">12</property>
        <property name="homogeneous">True</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" id="view">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="model">treesort</property>
                <property name="headers_clickable">False</property>
                <property name="rules_hint">True</property>
                <child internal-child="selection">
                  <object class="GtkTreeSelection" id="selection"/>
                </child>
                <child>
                  <object class="GtkTreeViewColumn">
                    <property name="title" translatable="yes">Name</property>
                    <property name="sort_order">descending</property>
                    <property name="sort_column_id">5</property>
                    <child>
                      <object class="GtkCellRendererText"/>
                      <attributes>
                        <attribute name="text">0</attribute>
                        <attribute name="weight">4</attribute>
                      </attributes>
                    </child>
                  </object>
                </child>
                <child>
                  <object class="GtkTreeViewColumn">
                    <property name="title" translatable="yes">Farbe</property>
                    <child>
                      <object class="GtkCellRendererText"/>
                      <attributes>
                        <attribute name="text">2</attribute>
                      </attributes>
                    </child>
                  </object>
                </child>
                <child>
                  <object class="GtkTreeViewColumn">
                    <property name="title" translatable="yes">Bemerkung</property>
                    <child>
                      <object class="GtkCellRendererText" id="cellrenderer_note">
                        <property name="editable">True</property>
                        <signal name="edited" handler="on_cellrenderer_note_edited" swapped="no"/>
                      </object>
                      <attributes>
                        <attribute name="text">3</attribute>
                      </attributes>
                    </child>
                  </object>
                </child>
              </object>
            </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="visible">True</property>
            <property name="can_focus">False</property>
            <child>
              <object class="GtkScrolledWindow">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="shadow_type">in</property>
                <property name="propagate_natural_width">True</property>
                <child>
                  <object class="GtkTreeView">
                    <property name="visible">True</property>
                    <property name="can_focus">True</property>
                    <property name="model">treefilter</property>
                    <child internal-child="selection">
                      <object class="GtkTreeSelection"/>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Sorte</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">0</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Art</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">1</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="GtkButtonBox" id="buttonbox">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <property name="orientation">vertical</property>
                <property name="layout_style">start</property>
                <child>
                  <placeholder/>
                </child>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="padding">5</property>
                <property name="pack_type">end</property>
                <property name="position">1</property>
              </packing>
            </child>
          </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>

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 diverse 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,
        )

Um eine Eingabe an die Konsole zu schicken, bedarf es der Funktion feed_child. Als Parameter muss der auszuführende Befehl als UTF-8-kodierter String inklusive newline, also dem "Enter" übergeben werden:

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

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()

Listings

Python

11_terminal.py (Source)

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

import os

import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Vte", "2.91")
from gi.repository import Gtk, Vte, GObject, GLib


class Handler:

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

    def on_button_clicked(self, widget):
        command = "python\n"
        x.terminal.feed_child(command.encode())


class Example:

    def __init__(self):

        self.builder = Gtk.Builder()
        GObject.type_register(Vte.Terminal)

        self.builder.add_from_file("11_terminal.glade")
        self.builder.connect_signals(Handler())

        self.terminal = self.builder.get_object("term")
        self.terminal.spawn_sync(
            Vte.PtyFlags.DEFAULT,
            None,
            ["/bin/bash"],
            None,
            GLib.SpawnFlags.DEFAULT,
            )

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

    def main(self):
        Gtk.main()


x = Example()
x.main()

Glade

11_terminal.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <requires lib="vte-2.91" version="0.46"/>
  <object class="GtkApplicationWindow" id="window">
    <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="VteTerminal" id="term">
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="has_focus">True</property>
            <property name="hscroll_policy">natural</property>
            <property name="vscroll_policy">natural</property>
            <property name="encoding">UTF-8</property>
            <property name="scroll_on_keystroke">True</property>
            <property name="scroll_on_output">False</property>
            <signal name="child-exited" handler="on_window_destroy" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="button">
            <property name="label" translatable="yes">start python console</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_button_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">False</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
    <child type="titlebar">
      <placeholder/>
    </child>
  </object>
</interface>

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

--sort-output

Alphabetische Sortierung der Strings

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.

Listings

Python

10_lokalisation.py (Source)

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

import gettext
import locale
import os

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

_ = gettext.gettext


class Handler:

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


class Example:

    def __init__(self):

        #setting up localization
        locales_dir = os.path.join(os.getcwd(),
                                    "10_localization",
                                    "locale",
                                    )
        appname = "TUT"

        #required for showing Glade file translations
        locale.bindtextdomain(appname, locales_dir)
        locale.textdomain(locales_dir)
        #required for code translations
        gettext.bindtextdomain(appname, locales_dir)
        gettext.textdomain(appname)

        self.builder = Gtk.Builder()
        self.builder.set_translation_domain(appname)

        self.builder.add_from_file("10_lokalisation.glade")
        self.builder.connect_signals(Handler())

        #translatable strings
        print(_("It's a trap!"))
        print(_("""These aren't the droids you're looking for.\n"""))

        #not translatable
        nonono = """\"Jar Jar is the key to all of this.\""""
        george = "...ruined it."
        print(nonono, george)

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

    def main(self):
        Gtk.main()


x = Example()
x.main()

Glade

10_lokalisation.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkApplicationWindow" id="window">
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">Localization example (English)</property>
    <property name="default_width">400</property>
    <property name="default_height">400</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="GtkMenuBar">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <child>
              <object class="GtkMenuItem">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <property name="label" translatable="yes">_File</property>
                <property name="use_underline">True</property>
                <child type="submenu">
                  <object class="GtkMenu">
                    <property name="visible">True</property>
                    <property name="can_focus">False</property>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-new</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-open</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-save</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-save-as</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkSeparatorMenuItem">
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-quit</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
            </child>
            <child>
              <object class="GtkMenuItem">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <property name="label" translatable="yes">_Edit</property>
                <property name="use_underline">True</property>
                <child type="submenu">
                  <object class="GtkMenu">
                    <property name="visible">True</property>
                    <property name="can_focus">False</property>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-cut</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-copy</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-paste</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-delete</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
            </child>
            <child>
              <object class="GtkMenuItem">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <property name="label" translatable="yes">_View</property>
                <property name="use_underline">True</property>
              </object>
            </child>
            <child>
              <object class="GtkMenuItem">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <property name="label" translatable="yes">_Help</property>
                <property name="use_underline">True</property>
                <child type="submenu">
                  <object class="GtkMenu">
                    <property name="visible">True</property>
                    <property name="can_focus">False</property>
                    <child>
                      <object class="GtkImageMenuItem">
                        <property name="label">gtk-about</property>
                        <property name="visible">True</property>
                        <property name="can_focus">False</property>
                        <property name="use_underline">True</property>
                        <property name="use_stock">True</property>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkScrolledWindow">
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="shadow_type">in</property>
            <child>
              <object class="GtkViewport">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <child>
                  <object class="GtkLabel">
                    <property name="visible">True</property>
                    <property name="can_focus">False</property>
                    <property name="label" translatable="yes">Good morning. In less than an hour, aircraft from here will join others from around the world. And you will be launching the largest aerial battle in this history of mankind.

Mankind -- that word should have new meaning for all of us today.

We can't be consumed by our petty differences anymore.

We will be united in our common interests.

Perhaps its fate that today is the 4th of July, and you will once again be fighting for our freedom, not from tyranny, oppression, or persecution -- but from annihilation.

We're fighting for our right to live, to exist.

And should we win the day, the 4th of July will no longer be known as an American holiday, but as the day when the world declared in one voice:

"We will not go quietly into the night!
We will not vanish without a fight!
We're going to live on!
We're going to survive!"

Today, we celebrate our Independence Day!</property>
                    <property name="wrap">True</property>
                  </object>
                </child>
              </object>
            </child>
          </object>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
      </object>
    </child>
    <child>
      <placeholder/>
    </child>
  </object>
</interface>

POT

10_localization/TUT.pot (Source)

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-11-28 13:06+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

msgid ""
"Good morning. In less than an hour, aircraft from here will join others from "
"around the world. And you will be launching the largest aerial battle in "
"this history of mankind.\n"
"\n"
"Mankind -- that word should have new meaning for all of us today.\n"
"\n"
"We can't be consumed by our petty differences anymore.\n"
"\n"
"We will be united in our common interests.\n"
"\n"
"Perhaps its fate that today is the 4th of July, and you will once again be "
"fighting for our freedom, not from tyranny, oppression, or persecution -- "
"but from annihilation.\n"
"\n"
"We're fighting for our right to live, to exist.\n"
"\n"
"And should we win the day, the 4th of July will no longer be known as an "
"American holiday, but as the day when the world declared in one voice:\n"
"\n"
"\"We will not go quietly into the night!\n"
"We will not vanish without a fight!\n"
"We're going to live on!\n"
"We're going to survive!\"\n"
"\n"
"Today, we celebrate our Independence Day!"
msgstr ""

msgid "It's a trap!"
msgstr ""

msgid "Localization example (English)"
msgstr ""

msgid "These aren't the droids you're looking for.\n"
msgstr ""

msgid "_Edit"
msgstr ""

msgid "_File"
msgstr ""

msgid "_Help"
msgstr ""

msgid "_View"
msgstr ""