Dateiauswahldialog
Inhalt
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.

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.

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:
Ohne Wahl. Der anzuwendende Filter ist voreingestellt:
dialog.set_filter(filter)
Wahl per Dropdown-Menü: Der Nutzer kann zwischen mehreren vorgegebenen Filtern wählen:
dialog.add_filter(filter1) dialog.add_filter(filter2) ...
Listings
Python
#!/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
<?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
Inhalt
Desktopintegration: Icon, Headerbar, Kommandozeilenoptionen
(Fortsetzung zum Artikel Gtk.Application)

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.

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
#!/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
<?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
Inhalt
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.

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.
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. ... ... ...
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.
Links
Listings
Python
#!/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
<?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"><b>MessageDialog schließen?</b></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
<?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"><Primary>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

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
Inhalt
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).

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
#!/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
<?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"><b>MessageDialog schließen?</b></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
Inhalt
Daten anzeigen mit TreeStore
(Fortsetzung zum ListStore-Artikel)

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

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

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
#!/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
<?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
Inhalt
Lokalisation mit gettext und locale

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