Python als Ersatz für Classic Visual Basic

Die Eignung von Python als Ersatz für Classic Visual Basic wird anhand praktischer Beispiele zu Benutzeroberflächen und Datenbanken untersucht.

Inhaltsverzeichnis

Einleitung

Das vorliegende Dokument faßt die Diskussionen im Python-Forum von ActiveVB zusammen. Darin wurde untersucht, inwieweit sich Python anhand typischer Beispielaufgaben als Ersatz für Classic Visual Basic eignet.

Python im Vergleich zu Classic Visual Basic

Zum Bearbeiten von Python-Quellcodes genügt im Grunde jeder einfache Texteditor. Mehr Komfort bieten schlanke Codeeditoren wie Geany: Sie unterstützen Syntaxhervorhebung und erlauben es, Programme direkt aus dem Editor heraus zu starten. Zudem wird mit Python die spartanische Entwicklungsumgebung IDLE mitgeliefert. Auch Visual Studio Code – oder dessen telemetriefreie Alternative VSCodium – bietet eine gute Unterstützung für die Python-Entwicklung. Eine freie IDE, deren Funktionsumfang mit dem von Classic Visual Basic vergleichbar wäre, fehlt jedoch.

Für die häufig genutzten GUI-Bibliotheken GTK und wxWidgets existieren grafische Formulardesigner. Mit ihnen lassen sich Steuerelemente auf Fenstern plazieren und deren Eigenschaften visuell konfigurieren. Diese Werkzeuge erzeugen Python-Code oder XML-Dateien, um die Formulare aufzubauen, und sind im Ansatz mit Classic Visual Basic vergleichbar. Darüber hinaus unterstützt Python weitere GUI-Toolkits wie Qt sowie die Entwicklung von Konsolenanwendungen.

Während Classic Visual Basic (abgesehen von Wine) nur unter Windows läuft, sind Python-Programme plattformunabhängig und laufen ebenso unter Linux. Da Python eine Skriptsprache ist, läßt sich der Quellcode nicht direkt in native Windows-EXE-Dateien kompilieren. Mithilfe von Werkzeugen wie PyInstaller ist es jedoch möglich, alle Programmdateien in ein einziges EXE-Archiv zu packen. Dieses läßt sich dann wie ein gewöhnliches Windows-Programm ausführen.

Python bietet unter anderem folgende Vorteile:

Grafische Benutzeroberflächen mit GTK

In Python läßt sich das GUI-Toolkit GTK nutzen, um rasch Anwendungen mit grafischer Benutzeroberfläche zu entwickeln. Die Formulare können dabei mit dem grafischen Formulardesigner Glade entworfen werden. Alternativ lassen sich Oberflächen natürlich auch direkt per Python-Quellcode erstellen. Einen guten Überblick über die Möglichkeiten von GTK unter Python bietet das The Python GTK+ 3 Tutorial samt Beispielen (Quellcode, Bildschirmfotos, Beschreibungen).

Unter Windows ist die Python-Entwicklung mit GTK am einfachsten mithilfe von MSYS2 möglich, wenngleich dies nicht ganz so nahtlos integriert und komfortabel wie unter Linux gelingt. Die nötigen Pakete lassen sich über ein MinGW-Terminal installieren:

pacman -Suy
pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-python3-gobject

Anschließend läßt sich die Python-Datei über folgenden Aufruf ausführen:

python3 test.py

Ein erstes Programm mit grafischer Benutzeroberfläche

Zum Einstieg soll ein Beispielprogramm mit einem Hauptfenster dienen, auf dem sich eine Schaltfläche mit der Aufschrift Say “Hello World” befindet. Klickt der Benutzer auf diese Schaltfläche, wird ein Meldungsdialogfeld mit dem Text Hello World! angezeigt.

Unter Linux Mint läßt sich der Python-Code im Editor Geany schreiben. Dazu erstellt man eine neue Datei helloworld.py und fügt den Python-Quelltext ein. Das Python-Programm wird anschließend durch Drücken der Taste F5 oder über den entsprechenden Menübefehl gestartet:

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

class MainWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="Hello World from Python+GTK")
        
        # Create button.
        self.hello_world_button = Gtk.Button(label="Say “Hello World”")
        self.hello_world_button.connect(
            'clicked',
            self.on_hello_world_button_clicked
        )
        self.add(self.hello_world_button)
    
    # Event handler called when the button gets clicked.
    def on_hello_world_button_clicked(self, widget):
        info_dialog = Gtk.MessageDialog(
            self, 
            Gtk.DialogFlags.MODAL,
            Gtk.MessageType.INFO,
            Gtk.ButtonsType.CLOSE,
            "Hello World!"
        )
        info_dialog.run()
        info_dialog.destroy()

main_window = MainWindow()
main_window.connect('destroy', Gtk.main_quit)
main_window.show_all()
Gtk.main()

MainWindow ist die Klasse des Hauptfensters mit der Schaltfläche. Anders als in Classic Visual Basic gibt es keine Anweisungen zum Schließen eines Blocks, wie z. B. End Sub. In Python entscheidet die Einrückungstiefe der Codezeilen darüber, zu welchem Block sie gehören und wo Blöcke enden.

Mit main_window = MainWindow() wird eine Instanz des Hauptfensters erstellt. Anschließend wird das Fenster angezeigt und die Nachrichtenverarbeitungsschleife (message loop) gestartet. Übrigens bieten einfache Codeeditoren für Python keine Funktion wie Edit and Continue zum Debuggen.

Zeichnen auf einem Fenster

In GTK läßt sich ähnlich einfach wie unter Classic Visual Basic direkt auf das Formular zeichnen. Immer dann, wenn das Formular neu gezeichnet werden muß, wird ein Ereignis ausgelöst oder die Methode do_draw aufgerufen. Dort kann der Zeichencode plaziert werden:

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

class MainWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="Drawing Window")
    
    def do_draw(self, cr):
        
        # Print to the console when the window gets drawn.
        print('do_draw')
        
        # Fill background in blue.
        cr.set_source_rgba(0, 0, 255)
        cr.paint()
        
        # Draw diagonal line from top left to bottom right.
        allocation = self.get_allocation()
        cr.set_source_rgba(255, 0, 0);
        cr.set_line_width(3)
        cr.move_to(0, 0)
        cr.line_to(allocation.width, allocation.height)
        cr.stroke()

win = MainWindow()
win.connect('destroy', Gtk.main_quit)
win.show_all()
Gtk.main()

Das fertiggestellte Formular stellt sich auf dem Bildschirm wie folgt dar:

GTK-Beispielformular mit Zeichencode in der Methode do_draw

Das Zeichnen erfolgt in GTK über die Grafikbibliothek Cairo. Diese Bibliothek ist für verschiedene Betriebssysteme verfügbar und nutzt, sofern vorhanden, die Hardwarebeschleunigung, was ein rasches Zeichnen ermöglicht. Cairo besitzt einen mächtigen Funktionsumfang und ist den Bordmitteln von Classic Visual Basic weit überlegen; so unterstützt Cairo etwa die Kantenglättung (Antialiasing).

Der Parameter cr der Methode do_draw enthält ein Objekt des Typs cairo.Context, das zahlreiche Zeichenfunktionen bereitstellt (siehe die Dokumentation zu cairo.Context).

GTK bietet zudem ein Steuerelement, das lediglich als Zeichenfläche dient – ähnlich dem PictureBox-Steuerelement in Classic Visual Basic. Darüber hinaus lassen sich eigene Steuerelemente (Widgets in der GTK-Terminologie) erstellen, deren Oberflächen analog zum obigen Beispiel selbst gezeichnet werden.

Formularentwurf mit dem Designer Glade

Classic Visual Basic bietet einen komfortablen Formulardesigner, der direkt in die IDE integriert ist. Bei Python und GTK muß auf diesen Komfort verzichtet werden. Das bedeutet jedoch nicht, daß alle Steuerelemente per Code zur Laufzeit dem Fenster hinzugefügt werden müssen. Der Formularentwurf kann stattdessen mittels des grafischen Designers Glade erfolgen.

Das folgende Beispielprogramm stellt eine filterbare Liste von Namen bereit. Klickt der Benutzer auf eine Schaltfläche, wird ein personalisierter Gruß für den ausgewählten Namen angezeigt. Das Formular stellt sich in der Entwurfsansicht von Glade wie folgt dar:

Formular im Formulardesigner Glade

Die Entwurfsansicht ähnelt jener aus Classic Visual Basic, wobei Glade aufgrund des GTK-Unterbaus deutlich mehr Möglichkeiten bietet als der Formulardesigner von Classic Visual Basic. Besonders hervorzuheben sind dabei Container, welche die Größe der Steuerelemente automatisch anpassen. Mit Glade läßt sich die Datei MainWindow.glade erstellen. Dabei handelt es sich um eine XML-Datei, in welcher das Formular und die Steuerelemente definiert werden:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkWindow" id="MainWindow">
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">Glade Greeter</property>
    <signal name="destroy" handler="on_destroy" swapped="no"/>
    <child>
      <placeholder/>
    </child>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="margin_left">6</property>
        <property name="margin_right">6</property>
        <property name="margin_top">6</property>
        <property name="margin_bottom">6</property>
        <property name="orientation">vertical</property>
        <property name="spacing">6</property>
        <child>
          <object class="GtkLabel">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="halign">start</property>
            <property name="label" translatable="yes">
              Select a name and press the button to greet the person.
            </property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkSearchEntry" id="names_filter_searchentry">
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="primary_icon_name">edit-find-symbolic</property>
            <property name="primary_icon_activatable">False</property>
            <property name="primary_icon_sensitive">False</property>
            <signal
              name="search-changed"
              handler="on_names_filter_searchentry_changed"
              swapped="no"
            />
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkTreeView" id="names_treeview">
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="headers_visible">False</property>
            <property name="headers_clickable">False</property>
            <property name="enable_search">False</property>
            <child internal-child="selection">
              <object class="GtkTreeSelection"/>
            </child>
          </object>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">2</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="greet_button">
            <property name="label" translatable="yes">_Greet</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <property name="halign">center</property>
            <property name="use_underline">True</property>
            <signal
              name="clicked"
              handler="on_greet_button_clicked"
              swapped="no"
            />
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">3</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>

Über die Zeilen <signal name="…" handler="…"/> werden die Ereignisse (in der GTK-Terminologie Signale) mit den Ereignisbehandlungsprozeduren verbunden. Der zugehörige Programmcode wird in einer Python-Datei abgelegt.

Innerhalb des Python-Skripts läßt sich über den Gtk.Builder das Fenster aus der Glade-Datei laden. Der dafür erforderliche Python-Quelltext stellt sich wie folgt dar:

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

class MainWindow:
    def __init__(self):
        
        # Load window from file.
        builder = Gtk.Builder()
        builder.add_from_file('MainWindow.glade')
        
        # Names treeview.
        self.names_treeview = builder.get_object('names_treeview')
        
        # Add a text column.
        cellrenderer = Gtk.CellRendererText()
        name_column = Gtk.TreeViewColumn("Name", cellrenderer, text=0)
        name_column.set_sort_column_id(0)
        self.names_treeview.append_column(name_column)
        
        # Add some names.
        model = Gtk.ListStore(str)
        model.append(["Benjamin"])
        model.append(["Charles"])
        model.append(["alfred"])
        model.append(["Alfred"])
        model.append(["David"])
        model.append(["charles"])
        model.append(["david"])
        model.append(["benjamin"])
        
        # Create filter.
        self.names_filter = model.filter_new()
        self.names_treeview.set_model(self.names_filter)
        self.current_names_filter = ""
        self.names_filter.set_visible_func(self.names_filter_func)
        
        # Connect signal handlers.
        builder.connect_signals(self)
        
        # Expose the GTK window object.
        self.window = builder.get_object('MainWindow')
    
    def on_destroy(self, *args):
        Gtk.main_quit()
    
    def on_greet_button_clicked(self, button):
        names_selection = self.names_treeview.get_selection()
        (tm, ti) = names_selection.get_selected()
        if ti is None:
            warning_dialog = Gtk.MessageDialog(
                self.window,
                Gtk.DialogFlags.MODAL,
                Gtk.MessageType.WARNING,
                Gtk.ButtonsType.CLOSE,
                "Please select a name."
            )
            warning_dialog.run()
            warning_dialog.destroy()
            return
        selected_name = tm.get_value(ti, 0)
        info_dialog = Gtk.MessageDialog(
            self.window,
            Gtk.DialogFlags.MODAL,
            Gtk.MessageType.INFO,
            Gtk.ButtonsType.CLOSE,
            "Hello " + selected_name + "!"
        )
        info_dialog.run()
        info_dialog.destroy()
    
    # Update the filter for the names treeview when the content of the
    # search textbox changes.
    def on_names_filter_searchentry_changed(self, edit):
        self.current_names_filter = edit.get_text()
        self.names_filter.refilter()
    
    # Filter data in the names treeview.
    def names_filter_func(self, model, iter, data):
        
        # No filter set.
        if (
            self.current_names_filter is None or
            self.current_names_filter == ""
        ):
            return True
        
        # Check if the item should be shown.
        return model[iter][0].lower().startswith(
            self.current_names_filter.lower()
        )

win = MainWindow().window
win.show_all()
Gtk.main()

Die Klasse MainWindow wird in diesem Falle nicht von Gtk.Window abgeleitet, sondern dient lediglich zur Behandlung der Ereignisse jenes Fensters, das mittels des Builders aus der Glade-Datei geladen wird. Das gestartete Programm stellt sich wie folgt dar:

Das mit Glade erstellte Beispielfenster zur Laufzeit

3D-Visualisierung mit OpenGL

In Python läßt sich innerhalb von GTK-Fenstern auch OpenGL nutzen. Dadurch werden grafikbeschleunigte 3D-Visualisierungen möglich, wie sie etwa in Spielen und wissenschaftlichen Anwendungen üblich sind. OpenGL-Oberflächen können über das Steuerelement (Widget) GLArea in eine GTK-Benutzeroberfläche eingebettet werden.

Exkurs für Linux Mint in einer virtuellen Maschine unter VirtualBox: Damit OpenGL funktioniert, muß zunächst in den Einstellungen der virtuellen Maschine die 3D-Grafikbeschleunigung deaktiviert werden. Der Treiber, welcher bei aktivierter 3D-Grafikbeschleunigung zum Einsatz kommt, weist nämlich zahlreiche Limitierungen auf. Ist die Hardwarebeschleunigung deaktiviert, erfolgen die OpenGL-Berechnungen zwar auf der CPU, dafür wird jedoch die benötigte 3D-Funktionalität vollständig unterstützt.

Unter Linux Mint sind die nötigen Python-OpenGL-Pakete nicht vorinstalliert. Dies läßt sich auf der Konsole wie folgt nachholen:

sudo apt install python3-opengl

Der OpenGL-Wrapper für Python erwartet die Eckpunkte der Geometrie in einem float32-Array. Um ein derartiges Array in Python erstellen und übergeben zu können, wird das Paket NumPy benötigt. Dieses läßt sich wie folgt installieren:

sudo apt install python3-numpy

Im OpenGL-Beispiel soll ein rotes Dreieck auf einem blauen Hintergrund ausgegeben werden:

GTK-Fenster mit OpenGL-Widget GLArea

Damit die darzustellenden Oberflächen auf dem Bildschirm sichtbar werden, werden Shader-Programme benötigt, welche die Farbinformationen berechnen. Diese Shader werden in einem C-ähnlichen Format definiert und über spezielle OpenGL-Aufrufe kompiliert. Der Einfachheit halber gibt der Shader im Beispiel stets dieselbe Farbe (Rot bzw. Blau) zurück.

Der OpenGL-Code in Python entspricht im Wesentlichen der Vorgehensweise bei der OpenGL-Entwicklung in anderen Programmiersprachen. Aus diesem Grunde wird an dieser Stelle darauf verzichtet, die Konzepte ausführlich zu erläutern. Der vollständige Quelltext der OpenGL-Testanwendung stellt sich wie folgt dar:

# Sample losely based on:
# http://www.opengl-tutorial.org/beginners-tutorials/tutorial-2-the-first-triangle/

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from OpenGL.GL import *
from OpenGL.GL import shaders
import numpy as np

FRAGMENT_SHADER_SOURCE = '''\
#version 330
out vec3 color;
void main() {
  color = vec3(1.0, 0.0, 0.0);   // Constant red.
}'''

VERTEX_SHADER_SOURCE = '''\
#version 330
in vec4 position;
void main() {
  gl_Position = position;
}'''

class TestGLArea(Gtk.GLArea):
    def __init__(self):
        Gtk.GLArea.__init__(self)
        self.connect('realize', self.on_realize)
        self.connect('render', self.on_render)
    
    # The `realize` signal is emitted when the widget is added to a window.
    def on_realize(self, area):
        ctx = self.get_context()
        ctx.make_current()
        err = self.get_error()
        if err:
            print("Error occurred: {}".format(err))
            Gtk.main_quit()
        
        # Create shader programs.
        vertex_shader_program = shaders.compileShader(
            VERTEX_SHADER_SOURCE, GL_VERTEX_SHADER
        )
        fragment_shader_program = shaders.compileShader(
            FRAGMENT_SHADER_SOURCE, GL_FRAGMENT_SHADER
        )
        self.shader_program = shaders.compileProgram(
            vertex_shader_program, fragment_shader_program
        )
        
        # Create Vertex Array Object (VAO).
        self.create_object()
    
    # Creates the VAO.
    def create_object(self):
        
        # Create a new VAO and bind it.
        vertex_array_object = glGenVertexArrays(1)
        glBindVertexArray(vertex_array_object)
        
        # Generate buffers to hold our vertices.
        vertex_buffer = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer)
        
        # Get the position of the `position` in parameter of our shader and bind
        # it.
        position = glGetAttribLocation(self.shader_program, 'position')
        glEnableVertexAttribArray(position)
        
        # Describe the position data layout in the buffer.
        # 3 elements per vertex of type float.
        glVertexAttribPointer(
            position, 3, GL_FLOAT, False, 0, ctypes.c_void_p(0)
        )
        
        # Send the vertex data over to the buffer.  We define only one triangle.
        # Currently lists cannot yet be passed to `glBufferData` directly, thus
        # we have to use a NumPy array.
        vertices = np.array(
            [
                -0.6, -0.6, 0.0,
                 0.0,  0.6, 0.0,
                 0.6, -0.6, 0.0
            ],
            dtype=np.float32
        )
        
        # The second parameter of `glBufferData` expects the size of the array
        # buffer in bytes.
        glBufferData(GL_ARRAY_BUFFER, 36, vertices, GL_STATIC_DRAW)
        
        # Unbind the VAO and other stuff.
        glBindVertexArray(0)
        glDisableVertexAttribArray(position)
        glBindBuffer(GL_ARRAY_BUFFER, 0)
        
        self.vob = vertex_array_object
    
    # `render` signal is emitted when the GLArea is ready to draw its content.
    def on_render(self, area, ctx):
        
        # Fill background in blue.
        glClearColor(0, 0, 1, 1)
        glClear(GL_COLOR_BUFFER_BIT)
        
        # Draw rectangle filled in red.
        glUseProgram(self.shader_program)
        glBindVertexArray(self.vob)
        
        # Draw triangle, start index in position data, count of array elements.
        glDrawArrays(GL_TRIANGLES, 0, 3)
        
        glBindVertexArray(0)
        glUseProgram(0)

class MainWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="OpenGL Triangle Demo")
        self.set_default_size(800, 600)
        glarea = TestGLArea()
        self.add(glarea)

win = MainWindow()
win.connect('destroy', Gtk.main_quit)
win.show_all()
Gtk.main()

Importe der Form from OpenGL import * sind in Python übrigens eher verpönt. In diesem Fall soll jedoch eine Ausnahme gemacht werden, da die Namen aller OpenGL-Funktionen und -Konstanten mit gl beziehungsweise GL_ beginnen und somit klar ersichtlich ist, daß sie aus OpenGL stammen.

Exkurs: Grundsätzlich ist die Einbettung von OpenGL auch in wxWidgets über das Steuerelement GLCanvas möglich. Allerdings erfordert dies, daß wxPython mit BUILD_GLCANVAS kompiliert wurde, was bei den vorkompilierten Bibliotheken weder für Windows noch für Linux der Fall ist.

Zeitgesteuerter Programmablauf mit Timer

In GTK-Programmen läßt sich der Timer aus der GLib nutzen, welcher intern auf Basis einer Nachrichtenverarbeitungsschleife arbeitet. Dieser Timer ist mit dem Timer-Steuerelement aus Classic Visual Basic vergleichbar. Im folgenden Beispiel wird der Timer dazu verwendet, regelmäßig Text auf der Konsole auszugeben (das Programm läßt sich durch das Schließen der Konsole beenden):

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

def timer_tick(*args):
    print("Timer fired.")
    return True

# Make the timer fire every 1200 ms.
GLib.timeout_add(1200, timer_tick)

# Run main loop.
Gtk.main()

Grafische Benutzeroberflächen mit wxPython

Das GUI-Toolkit wxWidgets läßt sich in Python über den Wrapper wxPython nutzen. WxWidgets ist für Windows, Linux und macOS verfügbar und zeichnet sich dadurch aus, daß jeweils die nativen Steuerelemente des Betriebssystems verwendet werden; nur im Bedarfsfall wird auf eigene Implementierungen zurückgegriffen. Dadurch wird sichergestellt, daß sich mit wxWidgets entwickelte Anwendungen auf allen Betriebssystemen optisch bestmöglich einfügen.

Eine erste Herausforderung stellt die Installation von wxPython unter Windows 7 und Linux Mint 19.1 LTS dar. Unter Windows ist dies in Verbindung mit CPython einfach über den Befehl pip in der Eingabeaufforderung (cmd) möglich. Unter Linux ist es hingegen zunächst erforderlich, die Entwicklungswerkzeuge (C++-Compiler und zugehörige Pakete) zu installieren, da für die erwähnte Linux-Version keine vorkompilierten Binärpakete für wxPython bereitstehen.

Die Installation von wxPython unter Linux Mint 19.1 LTS ist derzeit (Stand: Mai 2019) ausschließlich via pip möglich:

sudo apt install g++
sudo apt install python3-dev
sudo apt install python3-pip
sudo apt install python3-setuptools
sudo pip3 install wxPython

Installation unter Windows:

pip3 install wxPython --user

Ein erstes Programm mit grafischer Benutzeroberfläche

Als Beispielprojekt soll eine Greeter-Anwendung entwickelt werden. Der Benutzer kann seinen Namen in ein Textfeld eingeben und wird bei einem Klick auf eine Schaltfläche mit einer personalisierten Nachricht begrüßt. Das Hauptfenster des Programms stellt sich unter Linux Mint wie folgt dar:

Hauptfenster des wxPython-Beispielprogramms unter Linux Mint

Das folgende Listing zeigt den zugehörigen Quellcode:

import wx

class MainFrame(wx.Frame):
    def __init__(self, parent, title):
        super(MainFrame, self).__init__(parent, title=title)
        
        self.panel = wx.Panel(self)
        
        # Add a vertical box with two rows:
        # First row: Horizontal box containing the name textbox label and the
        #    name textbox.
        # Second row: "Greet" button centered horizontally.
        vbox = wx.BoxSizer(wx.VERTICAL)
        hbox = wx.BoxSizer(wx.HORIZONTAL)
        
        # Name textbox label.
        self.info_static = wx.StaticText(
            self.panel,
            wx.ID_ANY,
            "Your &name:"
        ) 
        hbox.Add(
            self.info_static,
            0,
            wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT | wx.RIGHT,
            border=10
        )
        
        # Name textbox.
        self.name_textbox = wx.TextCtrl(self.panel)
        hbox.Add(
            self.name_textbox,
            1,
            wx.EXPAND | wx.ALIGN_LEFT
        )
        
        vbox.Add(
            hbox,
            0,
            wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.ALL,
            border=10
        )
        
        # Greet button.
        self.greet_button = wx.Button(
            self.panel,
            wx.ID_OK,
            "&Greet",
        )
        self.greet_button.SetDefault()
        self.greet_button.Bind(
            wx.EVT_BUTTON,
            self.on_greet_button_clicked
        )
        vbox.Add(
            self.greet_button,
            0,
            wx.CENTER | wx.BOTTOM,
            border=10
        )
        
        self.panel.SetSizer(vbox)
        
        self.Show()
    
    def on_greet_button_clicked(self, event):
        wx.MessageBox(
            message="Hello " + self.name_textbox.GetValue() + "!",
            caption="Welcome!",
            style=wx.OK | wx.ICON_INFORMATION
        )

app = wx.App(False)
frame = MainFrame(None, "Greeter")
app.MainLoop()

Die BoxSizer dienen dem Positionieren der Steuerelemente. Nach Möglichkeit sollten Steuerelemente nicht fix an Pixelkoordinaten plaziert, sondern mittels eines Layoutmanagers – in diesem Fall des BoxSizer – angeordnet werden.

Alternativ zum Aufbau von Formular und Steuerelementen im Quellcode läßt sich auch ein grafischer Designer für wxPython, wie etwa der wxFormBuilder oder wxGlade, nutzen.

Konfetti-Animation mit Bitmap-Puffer

Ziel dieses Beispiels ist das Erstellen einer Animation, bei welcher Konfettipunkte mit zufälliger Farbe und Größe an einer zufällig ausgewählten Position auf einen farbigen Untergrund fallen. Das gesamte Fenster soll als Zeichenfläche dienen; alle 10 Millisekunden soll mittels eines Timers ein weiteres Konfettipartikel hinzukommen.

Benutzerschnittstelle der Konfetti-Animation

Die Konfettipunkte lassen sich zeichnen, indem ein Device Context (DC) für das Fenster angefordert und das neue Konfetti anschließend mittels der Zeichenfunktionen dargestellt wird. Diese Lösung hat jedoch einen Nachteil: Wird ein Teil des Fensters zum Beispiel durch ein anderes Fenster verdeckt, muß beim Wegrücken dieses überlappenden Fensters der ehemals verdeckte Bereich neu gezeichnet werden. Das erfordert, daß Reihenfolge, Position und Farbe aller Konfettipunkte im Speicher verfügbar sind. Ist dies nicht der Fall, bleibt an den betroffenen Regionen eine sichtbare Lücke.

Im Grunde gibt es zwei mögliche Lösungen:

  1. Die Positonen, Größen und Farben der Konfettis werden in einer Datenstruktur, etwa einer Liste, gespeichert. Immer dann, wenn ein Teil des Fensters neu gezeichnet werden muß, wird der Inhalt der Zeichenfläche gelöscht und alle Konfettis werden auf einen Schlag neu gezeichnet. Jedoch nimmt die Anzahl der Konfettis aber mit der Zeit zu und auch das Zeichnen kostet Zeit. Daher ist diese Lösung weniger geeignet.

  2. Anstatt Konfettis direkt auf das Fenster zu zeichnen wird im Hintergrund als Puffer eine Bitmap in der Größe der Zeichenfläche erstellt, auf welche die Konfettis ausgegeben werden. Diese Bitmap wird anschließend auf das Fenster übertragen, wenn das Betriebssystem das Neuzeichnen verlangt oder ein Konfetti hinzukommt.

Im Folgenden soll die Lösungsvariante mit dem Puffer umgesetzt werden. Relevant sind folgende Ereignisse des Fensters:

Die Funktion random_color gibt eine neue Pastellfarbe zurück, die für den Hintergrund der Fläche oder ein Konfetti genutzt wird. Die Methode create_new_buffer erstellt eine neue Puffer-Bitmap und füllt sie sogleich mit einer zufälligen Hintergrundfarbe.

Im Konstruktor unserer Fensterklasse MainFrame wird außerdem der Timer mit einem Intervall von 10 Millisekunden hinzugefügt. Der Timer löst bei Ablauf das Ereignis EVT_TIMER aus. In der zugehörigen Ereignisbehandlungsmethode wird ein MemoryDC für die Puffer-Bitmap erstellt und ein neues Konfetti hinzugefügt. Damit das neue Konfetti auf dem Bildschirm erscheint, muß ein Neuzeichnen des Fensters veranlaßt werden.

Der recht kompakte Quellcode des Konfetti-Programms stellt sich wie folgt dar:

# More information:
# https://wiki.wxpython.org/DoubleBufferedDrawing
# https://wiki.wxpython.org/BufferedCanvas

import random
import wx

class MainFrame(wx.Frame):
    _buffer = None
    
    def __init__(self):
        super().__init__(
            None,
            wx.ID_ANY,
            "wxPython Confetti",
            style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE,
            size=(600, 400)
        )
        
        # Recommended when `BufferedPaintDC` is used on `EVT_PAINT`.
        # https://wxpython.org/Phoenix/docs/html/wx.BufferedPaintDC.html
        self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
        
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_SIZE, self.on_size)
        
        self.create_new_buffer()
        
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
        self.timer.Start(10)
    
    # Blit buffer to the screen.
    def on_paint(self, event):
        
        # `BufferedPaintDC` copies its content to the screen before it gets
        # deleted.
        dc = wx.BufferedPaintDC(self, self._buffer)
    
    # Create a new buffer and blit it to the screen.
    def on_size(self, event):
        self.create_new_buffer()
        self.update()
    
    # Add a new confetti.
    def on_timer(self, event):
        self.draw_confetti()
        self.update()
    
    # Update screen from buffer. 
    def update(self):
        self.Refresh(eraseBackground=False)
    
    # Create a new buffer according to the client size and fill it with a random
    # color.
    def create_new_buffer(self):
        width, height = self.ClientSize
        self._buffer = wx.Bitmap(
            width if width > 0 else 1,
            height if height > 0 else 1
        )
        dc = wx.MemoryDC()
        dc.SelectObject(self._buffer)
        dc.SetBackground(wx.Brush(self.random_color()))
        dc.Clear()
    
    # Draw a new confetti with random position, size, and color on the buffer.
    # TODO:  Handle client size (0, 0).
    def draw_confetti(self):
        width, height = self.ClientSize
        confetti_max_size = int(min(width, height) * .2)
        posx = random.randint(
            -confetti_max_size,
            width + confetti_max_size - 1
        )
        posy = random.randint(
            -confetti_max_size,
            height + confetti_max_size - 1
        )
        confetti_size = random.randint(
            int(confetti_max_size * .5),
            confetti_max_size
        )
        
        dc = wx.MemoryDC()
        dc.SelectObject(self._buffer)
        
        # We use a `GraphicsContext` to draw onto the bitmap because it supports
        # anti-aliasing even on Windows.
        # TODO:  Handle the case where the creation of the `GraphicsContext`
        # fails because it is not supported by the OS. 
        gc = wx.GraphicsContext.Create(dc)
        gc.SetBrush(wx.Brush(self.random_color()))
        gc.DrawEllipse(posx, posy, confetti_size, confetti_size)
        
        # The memory DC must be deleted before we call `Refresh` or the system
        # requests repaint of the window; this is done automatically here
        # because the variable goes out of scope.  Otherwise we would have to
        # use `del dc` or `dc.SelectObject(wx.NullBitmap)`.
    
    # Returns a random pastel color.
    def random_color(self):
        return wx.Colour(
            random.randint(100, 255),
            random.randint(100, 255),
            random.randint(100, 255)
        )

if __name__ == '__main__':
    app = wx.App(False)
    frame = MainFrame()
    frame.Show()
    app.MainLoop()

In einem nächsten Schritt könnte diese Puffer-Technik in ein eigenes, wiederverwendbares Canvas-Steuerelement gekapselt werden.

Entwurf von Formularen mit wxGlade

Unter Classic Visual Basic ist der GUI-Designer (Formular-Editor) fest in die Entwicklungsumgebung integriert. Die Thunder-Forms bilden die Basis fast jeder Anwendung mit grafischer Benutzeroberfläche, die mit Classic Visual Basic entwickelt wird. Eine derart nahtlose Integration ist für wxWidgets und Python nicht verfügbar.

Für wxPython lassen sich Formulare dennoch komfortabel in einem visuellen Editor entwerfen. Dafür stehen unter anderem die Formulardesigner wxGlade (der Name entstand in Anlehnung an Glade für GTK) und wxFormBuilder bereit. Der GUI-Designer wxGlade ist ein Python-Programm, das selbst auf wxWidgets basiert; daher kann er sowohl unter Windows als auch unter Linux eingesetzt werden.

Zur Demonstration soll wieder das bereits bekannte Greeter-Programm aufgegriffen werden, bei welchem durch den Klick auf eine Schaltfläche eine personalisierte Begrüßung mit dem in einem Textfeld eingegebenen Namen angezeigt wird.

Unter Linux Mint läßt sich wxGlade über folgendes Kommando im Terminal installieren:

sudo apt install wxglade

Anschließend läßt sich das gewünschte Fenster in wxGlade entwerfen. Der im folgenden Bild sichtbare Mehrfenstermodus von wxGlade wurde in einer späteren Version durch einen praktischeren Einfenstermodus ersetzt:

Beispielformular in der Entwurfsansicht von wxGlade

Der Designer wxGlade speichert das GUI-Projekt in einer WXG-Datei. Dabei handelt es sich um eine XML-Datei mit folgendem Inhalt:

<?xml version="1.0"?>
<!-- generated by wxGlade 0.8.0 on … -->

<application encoding="UTF-8" for_version="3.0" header_extension=".h" indent_amount="4" indent_symbol="space" is_template="0" language="python" option="0" overwrite="1" path="./gui.py" source_extension=".cpp" top_window="frame" use_gettext="0" use_new_namespace="1">
    <object class="MainFrame" name="frame" base="EditFrame">
        <title>wxGlade Greeter</title>
        <style>wxCAPTION|wxMINIMIZE_BOX|wxCLOSE_BOX|wxSYSTEM_MENU|wxCLIP_CHILDREN</style>
        <object class="wxBoxSizer" name="sizer_1" base="EditBoxSizer">
            <orient>wxVERTICAL</orient>
            <object class="sizeritem">
                <option>0</option>
                <border>10</border>
                <flag>wxLEFT|wxRIGHT|wxTOP</flag>
                <object class="wxStaticText" name="label_1" base="EditStaticText">
                    <label>Enter a &amp;name:</label>
                </object>
            </object>
            <object class="sizeritem">
                <option>0</option>
                <border>10</border>
                <flag>wxALL|wxEXPAND</flag>
                <object class="wxTextCtrl" name="name_textbox" base="EditTextCtrl">
                    <size>300, -1</size>
                </object>
            </object>
            <object class="sizeritem">
                <option>0</option>
                <border>10</border>
                <flag>wxALL|wxALIGN_CENTER</flag>
                <object class="wxButton" name="greet_button" base="EditButton">
                    <events>
                        <handler event="EVT_BUTTON">on_greet_button_clicked</handler>
                    </events>
                    <label>&amp;Greet</label>
                    <default>1</default>
                </object>
            </object>
        </object>
    </object>
</application>

Über den Menübefehl FileGenerate Code erstellt wxGlade sodann eine Python-Datei gui.py. Diese Datei enthält den Python-Quellcode für den Aufbau des Formulars samt darauf enthaltenen Steuerelementen:

# -*- coding: UTF-8 -*-
#
# generated by wxGlade 0.8.0 on …
#

import wx

# begin wxGlade: dependencies
# end wxGlade

# begin wxGlade: extracode
# end wxGlade


class MainFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # begin wxGlade: MainFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.MINIMIZE_BOX | wx.SYSTEM_MENU
        wx.Frame.__init__(self, *args, **kwds)
        self.name_textbox = wx.TextCtrl(self, wx.ID_ANY, "")
        self.greet_button = wx.Button(self, wx.ID_ANY, "&Greet")

        self.__set_properties()
        self.__do_layout()

        self.Bind(wx.EVT_BUTTON, self.on_greet_button_clicked, self.greet_button)
        # end wxGlade

    def __set_properties(self):
        # begin wxGlade: MainFrame.__set_properties
        self.SetTitle("wxGlade Greeter")
        self.name_textbox.SetMinSize((300, -1))
        self.greet_button.SetDefault()
        # end wxGlade

    def __do_layout(self):
        # begin wxGlade: MainFrame.__do_layout
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        label_1 = wx.StaticText(self, wx.ID_ANY, "Enter a &name:")
        sizer_1.Add(label_1, 0, wx.LEFT | wx.RIGHT | wx.TOP, 10)
        sizer_1.Add(self.name_textbox, 0, wx.ALL | wx.EXPAND, 10)
        sizer_1.Add(self.greet_button, 0, wx.ALIGN_CENTER | wx.ALL, 10)
        self.SetSizer(sizer_1)
        sizer_1.Fit(self)
        self.Layout()
        # end wxGlade

    def on_greet_button_clicked(self, event):  # wxGlade: MainFrame.<event_handler>
        print("Event handler 'on_greet_button_clicked' not implemented!")
        event.Skip()

# end of class MainFrame

Bemerkenswert ist dabei die Klasse MainFrame, welche dem gleichnamigen Fenster aus dem entsprechenden wxGlade-Projekt entspricht.

Das in wxGlade entworfene Formular läßt sich im eigenen Python-Quellcode nutzen, indem eine Klasse hinzugefügt wird, die von der durch den Designer erstellten Klasse erbt. Dort werden die im wxGlade-Designer dem Klickereignis der Schaltfläche zugeordneten Ereignisbehandlungsmethoden hinterlegt – im Greeter-Programm geschieht dies durch das Anzeigen eines Meldungsdialogs:

import wx

# Import `gui.py` generated by wxGlade.
import gui

class MainFrame(gui.MainFrame):
    def __init__(self, *args, **kwds):
        gui.MainFrame.__init__(self, *args, **kwds)
    
    def on_greet_button_clicked(self, event):
        wx.MessageBox(
            message="Hello " + self.name_textbox.GetValue() + "!",
            caption="Welcome!",
            style=wx.OK | wx.ICON_INFORMATION
        )

class GreeterApp(wx.App):
    def OnInit(self):
        self.frame = MainFrame(None)
        self.frame.Show()
        return True

if __name__ == '__main__':
    app = GreeterApp(False)
    app.MainLoop()

Der selbstgeschriebene Quellcode ähnelt stark der ursprünglichen wxPython-Greeter-Anwendung. Der Einsatz von wxGlade weist jedoch einige Unterschiede bzw. Vorteile gegenüber dem manuellen Formularaufbau im Quellcode auf:

Formulare aus einer XRC-Datei laden

Der GUI-Designer wxGlade kann nicht nur Python-Quelltext zum Erstellen der Fenster und Steuerelemente generieren, sondern auch XRC-Dateien. Dabei handelt es sich um XML-Dateien, welche die Fensterdefinitionen enthalten und in Python dazu genutzt werden können, Formulare samt Steuerelementen aus einer externen Datei zu laden.

Im vorigen Beispiel wurde wxGlade dazu verwendet, ein Formular zu entwerfen und Python-Dateien zu erstellen, die sich in das Python-Programm einbinden ließen. Der Nachteil dieses Ansatzes besteht darin, daß wxGlade Python-Quellcode mit Klassendefinitionen für die Formulare erzeugt. Soll nun eigener Programmcode zur Ereignisbehandlung hinzugefügt werden, müssen Klassen erstellt werden, die von den durch wxGlade generierten Klassen erben und die Ereignisbehandlungsmethoden enthalten.

Sauberer wäre es, die Formularklassen selbst zu erstellen und mittels eines Mechanismus zur Laufzeit mit den in wxGlade entworfenen Oberflächen zu kombinieren. Dies läßt sich mit XRC umsetzen. Bei XRC-Dateien handelt es sich um XML-Dateien, welche die Formulardefinitionen enthalten. Sie können anstelle der Python-Dateien von wxGlade erstellt werden, indem in den wxGlade-Projekteigenschaften bei Language die Einstellung XRC statt Python ausgewählt wird.

Mittels der wxPython-XRC-Klasse XmlResource läßt sich anschließend im Python-Programm das Formular aus der XRC-Datei laden. Das folgende Listing zeigt den Inhalt der wxGlade-Datei wxgladegreeter.wxg des bereits vorgestellten Greeter-Programms:

<?xml version="1.0"?>
<application encoding="UTF-8" for_version="3.0" header_extension=".h" indent_amount="4" indent_symbol="space" is_template="0" language="XRC" option="0" overwrite="1" path="./gui.xrc" source_extension=".cpp" top_window="frame" use_gettext="0" use_new_namespace="1">
    <object class="xrcgreeter.MainFrame" name="frame" base="EditFrame">
        <title>wxGlade Greeter</title>
        <style>wxCAPTION|wxMINIMIZE_BOX|wxCLOSE_BOX|wxSYSTEM_MENU|wxCLIP_CHILDREN</style>
        <object class="wxBoxSizer" name="sizer_1" base="EditBoxSizer">
            <orient>wxVERTICAL</orient>
            <object class="sizeritem">
                <option>0</option>
                <border>10</border>
                <flag>wxLEFT|wxRIGHT|wxTOP</flag>
                <object class="wxStaticText" name="label_1" base="EditStaticText">
                    <label>Enter a &amp;name:</label>
                </object>
            </object>
            <object class="sizeritem">
                <option>0</option>
                <border>10</border>
                <flag>wxALL|wxEXPAND</flag>
                <object class="wxTextCtrl" name="name_textbox" base="EditTextCtrl">
                    <size>300, -1</size>
                </object>
            </object>
            <object class="sizeritem">
                <option>0</option>
                <border>10</border>
                <flag>wxALL|wxALIGN_CENTER</flag>
                <object class="wxButton" name="greet_button" base="EditButton">
                    <events>
                        <handler event="EVT_BUTTON">on_greet_button_clicked</handler>
                    </events>
                    <label>&amp;Greet</label>
                    <default>1</default>
                </object>
            </object>
        </object>
    </object>
</application>

Die XRC-Datei läßt sich in wxGlade über den Menübefehl FileGenerate Code erstellen. Das folgende Listing zeigt den Inhalt der generierten gui.xrc:

<?xml version="1.0" encoding="UTF-8"?>
<resource version="2.3.0.1">
    <object class="wxFrame" name="frame" subclass="xrcgreeter.MainFrame">
        <style>wxCAPTION|wxCLIP_CHILDREN|wxCLOSE_BOX|wxMINIMIZE_BOX|wxSYSTEM_MENU</style>
        <title>wxGlade Greeter</title>
        <object class="wxBoxSizer">
            <orient>wxVERTICAL</orient>
            <object class="sizeritem">
                <flag>wxLEFT|wxRIGHT|wxTOP</flag>
                <border>10</border>
                <object class="wxStaticText" name="label_1">
                    <label>Enter a &amp;name:</label>
                </object>
            </object>
            <object class="sizeritem">
                <flag>wxALL|wxEXPAND</flag>
                <border>10</border>
                <object class="wxTextCtrl" name="name_textbox">
                    <size>300, -1</size>
                </object>
            </object>
            <object class="sizeritem">
                <flag>wxALIGN_CENTER|wxALL</flag>
                <border>10</border>
                <object class="wxButton" name="greet_button">
                    <handler event="EVT_BUTTON">on_greet_button_clicked</handler>
                    <default>1</default>
                    <label>_Greet</label>
                </object>
            </object>
        </object>
    </object>
</resource>

Der Inhalt der XRC-Datei ist der wxGlade-Projektdatei sehr ähnlich. Der Python-Quelltext zur Ereignisbehandlung wird in einer Klasse MainFrame hinterlegt: In diesem Beispiel soll bei einem Klick auf eine Schaltfläche ein Meldungsdialogfeld mit einer Begrüßung erscheinen. Der Quelltext stellt sich wie folgt dar:

import wx
from wx import xrc

class MainFrame(wx.Frame):
    def __init__(self):
        super().__init__()
        
        # We cannot access XRC content and bind event handlers here because the
        # window has not yet been created.  Therefore we handle the appropriate
        # event and perform initialization there.
        self.Bind(wx.EVT_WINDOW_CREATE, self.on_create)
    
    def on_create(self, event):
        self.Unbind(wx.EVT_WINDOW_CREATE)
        wx.CallAfter(self.post_init)
        event.Skip()
        return True
    
    def post_init(self):
        self.name_textbox = xrc.XRCCTRL(self, 'name_textbox')
        
        self.Bind(
            wx.EVT_BUTTON,
            self.on_greet_button_clicked,
            id=xrc.XRCID('greet_button')
        )
        
        # Alternative:
        #self.greet_button = xrc.XRCCTRL(self.wxframe, 'greet_button')
        #self.Bind(
        #    wx.EVT_BUTTON,
        #    self.on_greet_button_clicked,
        #    self.greet_button
        #)
    
    def on_greet_button_clicked(self, event):
        wx.MessageBox(
            message="Hello " + self.name_textbox.GetValue() + "!",
            caption="Welcome!",
            style=wx.OK | wx.ICON_INFORMATION
        )

class GreeterApp(wx.App):
    def OnInit(self):
        res = xrc.XmlResource('gui.xrc')
        frame = res.LoadFrame(None, 'frame')
        frame.Show()
        return True

if __name__ == '__main__':
    app = GreeterApp(False)
    app.MainLoop()

Innerhalb der Methode OnInit der Klasse GreeterApp wird die XRC-Datei mittels eines XmlResource-Objektes geladen. Die Methode LoadFrame erstellt die Instanz unserer Klasse MainFrame, wodurch die in der XRC-Datei definierten Steuerelemente erzeugt werden. (Mit XmlResource ließen sich bei Bedarf auch lediglich einzelne Steuerelemente aus der XRC-Datei laden.)

Beim Laden des Formulars über die Methode LoadFrame ist zu beachten, daß der Aufruf zwar eine Instanz der Klasse MainFrame zurückgibt, sich das Fenster jedoch noch in einem halbfertigen Zustand befindet. Das bedeutet, daß wxPython das Fenster noch nicht tatsächlich erstellt hat. Um auf die Steuerelemente des Formulars zugreifen zu können, muß dies aber der Fall sein. Dieses Problem läßt sich lösen, indem das Ereignis EVT_WINDOW_CREATE behandelt wird. Dieses Ereignis wird ausgelöst, sobald die Initialisierung des Formulars abgeschlossen ist.

Mittels xrc.XRCCTRL kann auf die Steuerelemente aus der XRC-Datei zugegriffen werden. Dies ermöglicht es, über Variablen auf die Steuerelemente zuzugreifen und die Ereignisbehandlungsmethoden zu verdrahten. Das folgende Listing zeigt den entsprechenden Quellcode des Beispiels:

self.Bind(
    wx.EVT_BUTTON,
    self.on_greet_button_clicked,
    id=xrc.XRCID('greet_button')
)

XRC-Dateien können in wxWidgets-Projekten wiederverwendet werden, die in anderen Programmiersprachen – wie z. B. C++ – verfaßt wurden. Da es sich um XML-Dateien handelt, lassen sich XRC-Dateien auch mit einem XML- oder Texteditor schreiben und bearbeiten. Für einen einfachen Arbeitsablauf empfiehlt sich jedoch der Entwurf der Formulare mit wxGlade und der anschließende Export als XRC-Datei.

Zeitgesteuerter Programmablauf mit Timer

In wxPython-Programmen kann wx.Timer als Zeitgeber genutzt werden. Im folgenden Beispiel wechselt die Hintergrundfarbe eines Fensters per Timer zwischen Rot und Blau:

import wx

class MainFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, wx.ID_ANY, "wx.Timer Demo")
        
        self.SetBackgroundColour(wx.RED) 
        
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
        self.timer.Start(800)
        
        self.Show()
    
    def on_timer(self, event):
        if self.GetBackgroundColour() == wx.RED:
            self.SetBackgroundColour(wx.BLUE)
        else:
            self.SetBackgroundColour(wx.RED)

app = wx.App(False)
frame = MainFrame(None)
app.MainLoop()

Grafische Benutzeroberflächen mit Tkinter

Python enthält standardmäßig bereits Tkinter, einen Python-Wrapper für Tcl/Tk, mit welchem sich einfache Benutzeroberflächen plattformunabhängig entwickeln lassen. Tkinter ist von seinen Fähigkeiten her nicht mit GTK, wxPython oder Qt vergleichbar und eignet sich lediglich für einfache Formulare. So werden unter Windows beispielsweise keine Barrierefreiheitsdaten (Accessibility-Daten), etwa zur Nutzung mit einem Screenreader, bereitgestellt.

Wenn das CPython-Installationsprogramm verwendet wird, wird Tkinter automatisch mitinstalliert. Andernfalls läßt sich Tkinter unter Linux Mint wie folgt installieren:

sudo apt install python3-tk

Im folgenden Beispiel wird ein Fenster mit einem Label-Steuerelement und einer Schaltfläche erstellt. Bei einem Klick auf die Schaltfläche wird ein Meldungsdialogfeld angezeigt:

from tkinter import Button, Label, messagebox, Tk

class MainWindow:
    def __init__(self, master):
        self.master = master
        master.title("Tkinter Demo")
        master.geometry('320x160')
        
        pad = 10
        
        self.label = Label(
            master,
            text="Click the button to say \"Hello World!\""
        )
        self.label.pack(
            expand=True,
            padx=pad,
            pady=pad
        )
        
        self.greet_button = Button(
            master,
            text="Greet",
            command=self.on_greet_button_clicked
        )
        self.greet_button.pack(
            side='bottom',
            padx=pad,
            pady=pad,
            ipadx=pad * 1.5
        )
    
    def on_greet_button_clicked(self):
        messagebox.showinfo("Welcome", "Hello World!")

root = Tk()
win = MainWindow(root)
root.mainloop()

Die letzten drei Codezeilen des obigen Listings entsprechen der Sub Main in Classic Visual Basic und werden als erstes aufgerufen. Sie dienen dem Erstellen des Formulars und dem Start der Nachrichtenverarbeitungsschleife.

Innerhalb des Konstruktors der Klasse MainWindow werden die Eigenschaften des Fensters, wie Titel und Größe, festgelegt und die Steuerelemente hinzugefügt. Die Parameter padx und pady definieren die Außenabstände, ipadx und ipady die Innenabstände.

Mittels command=self.on_greet_button_clicked läßt sich das Klickereignis der Schaltfläche mit einer Ereignisbehandlungsmethode verbinden.

Das Beispiel läuft sowohl unter Windows als auch unter Linux. Die Benutzeroberfläche des Beispielprogramms stellt sich unter Linux Mint wie folgt dar:

Hauptfenster des Tkinter-Beispielprogramms

Tkinter bietet mehrere Themes, zu deren Nutzung die Steuerelemente aus dem Modul ttk anstelle von jenen aus tkinter verwendet werden müssen. Je nach Betriebssystem werden unterschiedliche Themes mitgeliefert; auch eigene Themes lassen sich erstellen.

Für Windows existiert u. a. das Theme vista, welches unter Windows 7 im Lieferumfang von CPython bereits voreingestellt ist und nicht explizit aktiviert werden muß. Das nachstehende Beispiel veranschaulicht die Aktivierung des Themes vista:

from tkinter import messagebox, Tk
from tkinter.ttk import Button, Label, Style

class MainWindow:
    def __init__(self, master):
        self.master = master
        master.title("Tkinter Demo")
        master.geometry('320x160')
        
        pad = 10
        
        self.label = Label(
            master,
            text="Click the button to say \"Hello World!\""
        )
        self.label.pack(
            expand=True,
            padx=pad,
            pady=pad
        )
        
        self.greet_button = Button(
            master,
            text="Greet",
            command=self.on_greet_button_clicked
        )
        self.greet_button.pack(
            side='bottom',
            padx=pad,
            pady=pad,
            ipadx=pad * 1.5
        )
    
    def on_greet_button_clicked(self):
        messagebox.showinfo("Welcome", "Hello World!")

root = Tk()

style = Style()
if 'vista' in style.theme_names():
    style.theme_use('vista')

win = MainWindow(root)
root.mainloop()

Zeichnen auf dem Canvas

Mit Canvas stellt Tkinter eine Zeichenfläche bereit, die dem PictureBox-Steuerelement in Classic Visual Basic ähnelt. Eine Kantenglättung wird hierbei unter Windows und Linux jedoch nicht unterstützt. Im folgenden Beispiel wird ein Formular mit einer Zeichenfläche erstellt, auf welcher geometrische Formen ausgegeben werden:

from tkinter import Tk, Canvas

main_window = Tk()
main_window.title("Tkinter Canvas Demo")
main_window.geometry('800x600')
canvas = Canvas(
    main_window,
    bg='blue',
    width=770,
    height=580,
    bd=10,
    relief='groove'
)
canvas.pack()
arc = canvas.create_arc(
    50, 50, 400, 400, start=0, extent=300, fill='red'
)
line = canvas.create_line(
    25, 25, 770, 580, width=2, fill='white'
)
oval = canvas.create_oval(
    500, 100, 700, 400, fill='yellow'
)
main_window.mainloop()

Datenbankzugriffe mit SQLite

Mit Python lassen sich verschiedene Datenbankmanagementsysteme sowie Microsoft Access nutzen (siehe die Dokumentation zu Datenbankschnittstellen von Python). Eine der Optionen stellt SQLite dar – eine serverlose Datenbank, die sich bequem mit den eigenen Programmen ausliefern läßt. Unter Linux Mint müssen zur Verwendung von SQLite in Python keine zusätzlichen Pakete installiert werden.

SQLite-Datenbanken können entweder in Dateien abgelegt oder vorübergehend im Arbeitsspeicher erstellt werden. Um eine flüchtige Datenbank anzulegen, ist anstelle eines Dateinamens beim Aufruf von connect der Wert :memory: zu übergeben.

Das folgende Beispiel veranschaulicht den Umgang mit einer SQLite-Datenbank und basiert auf der offiziellen Dokumentation zu SQLite. Dabei werden eine Datenbank angelegt, eine Tabelle für Aktientransaktionen erstellt, Beispieleinträge eingefügt und anschließend wieder abgefragt:

# Inspired by https://docs.python.org/2/library/sqlite3.html

import sqlite3

# Connect to database.
conn = sqlite3.connect('stocks.db')
c = conn.cursor()

# Create table.
c.execute('''
    CREATE TABLE stocks(
        date text, trans text, symbol text, qty real, price real
    )'''
)

# Insert a row of data.
c.execute(
    'INSERT INTO stocks VALUES (?, ?, ?, ?, ?)',
    ('2006-01-05', 'BUY', 'RHAT', 100, 35.14)
)

# Insert multiple rows of data at a time.
purchases = [
    ('2006-03-28', 'BUY', 'IBM', 1000, 45.00),
    ('2006-04-05', 'BUY', 'MSFT', 1000, 72.00),
    ('2006-04-06', 'SELL', 'IBM', 500, 53.00)
]
c.executemany('INSERT INTO stocks VALUES (?, ?, ?, ?, ?)', purchases)

# Select a single row based on the value of a certain column.
symbol = ('MSFT',)   # Tuple with just one element.
c.execute('SELECT * FROM stocks WHERE symbol = ?', symbol)
row = c.fetchone()
if row is None:
    print("No row for symbol 'MSFT' found.")
else:
    print(row)

# Print all rows.
for row in c.execute('SELECT * FROM stocks ORDER BY price'):
    print(row)

# Persist changes.
conn.commit()

# Close connection.
conn.close()

Mit runden Klammern (1, 2, 3) definiert man in Python übrigens ein Tupel, mit eckigen Klammern [1, 2, 3] eine Liste und mit geschweiften Klammern ein Wörterbuch (Schlüssel–Wert-Paare).

Unter Linux Mint läßt sich die mittels Python erstellte SQLite-Datenbankdatei im SQLite Browser auf einer grafischen Benutzeroberfläche betrachten. Die Installation erfolgt im Terminal über das folgende Kommando:

sudo apt install sqlitebrowser

Das Werkzeug wird durch den Aufruf von sqlitebrowser gestartet. Das Programm mit der geöffneten Datenbankdatei stocks.db stellt sich wie folgt dar:

Benutzerschnittstelle von SQLite Browser

PDF-Dateien mit Cairo erstellen

Die Grafikbibliothek Cairo kapselt unter Windows nicht lediglich GDI-Funktionen, sondern bietet einen wesentlich größeren Funktionsumfang. So läßt sich Cairo etwa dazu nutzen, PDF-Dateien zu erstellen. Der Zeichencode ist dabei identisch mit jenem für das Zeichnen auf ein Fenster:

import cairo

height = 600
width =  800

surface = cairo.PDFSurface('sample.pdf', width, height)
context = cairo.Context(ps)

# Fill background in blue.
context.set_source_rgba(0, 0, 255)
context.paint()

# Draw diagonal line from top left to bottom right.
context.set_source_rgba(255, 0, 0);
context.set_line_width(3)
context.move_to(0, 0)
context.line_to(width, height)
context.stroke()

surface.show_page()