Python als Ersatz für Classic Visual Basic

Inhaltsverzeichnis

Einleitung

Das vorliegende Dokument ist eine Zusammenfassung einer Serie von Diskussionen im Python-Forum von ActiveVB, in denen die Eignung von Python als Ersatz für Classic Visual Basic anhand einiger Beispielaufgaben untersucht wurde.

Python im Vergleich zu Classic Visual Basic

Zum Bearbeiten von Python-Quellcode ist grundsätzlich jeder Texteditor geeignet. Mehr Komfort bieten schlanke Codeeditoren wie Geany, die Syntaxhervorhebung unterstützen und das Starten der erstellten Programme direkt aus dem Editor ermöglichen. Mit Python wird die spartanische Entwicklungsumgebung IDLE mitgeliefert. Visual Studio Code (alternative Distribution ohne Telemetrie: VSCodium) bietet gute Unterstützung für die Python-Entwicklung. Eine freie IDE mit zu Classic Visual Basic vergleichbarem Funktionsumfang fehlt für Python.

Für die häufig benutzten GUI-Bibliotheken GTK und wxWidgets sind grafische Formulardesigner verfügbar, mit denen sich Steuerelemente auf Fenstern plazieren und deren Eingenschaften einstellen lassen. Diese Designer erstellen Python-Code oder XML-Dateien zum Aufbau der Formulare und Steuerelemente. Die verfügbaren Formulareditoren sind im Ansatz vergleichbar mit Classic Visual Basic. Daneben können mit Python auch andere GUI-Toolkits, z. B. Qt, eingesetzt werden. Auch Konsolenanwendungen lassen sich mit Python entwickeln.

Während Classic Visual Basic nur unter Windows läuft (von Wine abgesehen), können Python-Programme auf verschiedenen Betriebssystemen wie Windows und Linux ausgeführt werden. Da Python eine Skriptsprache ist, kann der Quellcode von Haus aus nicht in native EXE-Dateien von Windows kompiliert werden. Mithilfe von Werkzeugen wie PyInstaller ist jedoch möglich, alle Dateien eines Programms in eine EXE-Datei zu packen. Diese EXE-Datei lässt sich analog zu den anderen Windows-Programmen starten.

Python hat unter anderem folgende Vorteile:

Grafische Benutzeroberflächen mit GTK

In Python kann das GUI-Toolkit GTK genutzt werden, um rasch Anwendungen mit grafischer Benutzeroberfläche zu entwickeln. Die Formulare können dabei mit dem grafischen Formulardesigner Glade entworfen werden. Natürlich lassen sich alternativ Formulare auch direkt per Python-Quellcode erstellen. Einen guten Überblick über die Möglichkeiten von GTK unter Python samt Beispielen (Quellcode, Bildschirmfotos, Beschreibung) findet sich in The Python GTK+ 3 Tutorial.

Unter Windows ist die Python-Entwicklung unter Verwendung von GTK am Einfachsten mit MSYS2 möglich, allerdings nicht ganz so gut integriert und komfortabel wie unter Linux. Die nötigen Pakete können in einem MinGW-Terminal installiert werden:

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

Anschließend kann man die Python-Datei über folgenden Aufruf ausführen:

python3 test.py

Ein erstes Programm mit GUI

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

Unter Linux Mint lässt sich Python-Code im Editor Geany schreiben. Dazu erstellt man eine neue Datei helloworld.py, fügt den Python-Quellcode ein und startet das Python-Programm durch Drücken der Taste F5 bzw. den entsprechenden Menübefehl:

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ücktiefe der Codezeilen, zu welchem Block sie gehören und wo Blöcke enden.

Mit main_window = MainWindow() wird eine Instanz des Hauptfensters erstellt. Anschließend zeigen wir das Fenster an und starten die Nachrichtenverarbeitungschleife (message loop). Übrigens gibt es in den einfachen Codeeditoren für Python kein Edit and Continue zum Debuggen.

Zeichnen auf einem Fenster

In GTK kann ähnlich einfach wie unter Classic Visual Basic direkt auf das Formular gezeichnet werden. Immer dann, wenn das Formular neu gezeichnet werden muß, wird ein Ereignis ausgelöst bzw. 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 erstellte Formular sieht auf dem Bildschirm folgendermaßen aus:

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 verwendet, sofern vorhanden, 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 Kantenglättung (Antialiasing).

Der Parameter cr der Methode do_draw enthält ein Objekt des Typs cairo.Context, das zahlreiche Methoden zum Zeichnen bietet (Dokumentation zu cairo.Context).

GTK bietet auch ein Steuerelement, das lediglich eine Zeichenfläche bietet (ähnlich dem PictureBox-Steuerelement in Classic Visual Basic). Darüber hinaus können eigene Steuerelemente (Widgets in GTK-Terminologie) erstellt werden, deren Oberflächen ähnlich dem Beispiel oben selbst gezeichnet werden.

Formularentwurf mit dem Designer Glade

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

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

Formular im Formulardesigner Glade

Die Entwurfsansicht ähnelt jener aus Classic Visual Basic, wobei Glade aufgrund des GTK-Unterbaus deutlich mehr mehr Möglichkeiten bietet als der Formulardesigner von Classic Visual Basic. Besonders hervorzuheben sind dabei Container, die Steuerelemente automatisch in ihrer Größe anpassen. Mit Glade lässt sich die Datei MainWindow.glade erstellen. Dabei handelt es sich um eine XML-Datei, in der Formular und 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 GTK-Terminologie Signale) mit den Ereignisbehandlungsprozeduren verbunden. Der zugehörige Programmcode wird in einer Python-Datei abgelegt.

Innerhalb des Python-Skripts kann über den Gtk.Builder das Fenster aus der Glade-Datei geladen werden. Der dafür erforderliche Python-Code sieht wie folgt aus:

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 nur zur Behandlung der Ereignisse des Fensters, das mittels des Builders aus der Glade-Datei geladen wird. Das gestartete Programm sieht wie folgt aus:

Das mit Glade erstellte Beispielfenster zur Laufzeit

3D-Visualisierung mit OpenGL

In Python läßt sich in GTK-Fenstern auch OpenGL nutzen. Dadurch werden grafikbeschleunigte 3D-Visualisierungen möglich gemacht, wie sie etwa in Spielen und wissenschaftlichen Visualisierungen üblich sind. OpenGL-Oberflächen können über das 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, der bei aktivierter 3D-Grafikbeschleunigung zum Einsatz kommt, weist nämlich zahlreiche Limitierungen auf. Ist der Treiber deaktiviert, erfolgen die OpenGL-Berechnungen zwar auf der CPU, dafür wird die benötigte 3D-Funktionalität unterstützt.

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

sudo apt install python3-opengl

Der OpenGL-Wrapper für Python erwartet die Eckpunkte von Geometrie in einem float32-Array. Um ein derartiges Array in Python erstellen und übergeben zu können, wird das Paket NumPy benötigt, das wie folgt installiert werden kann:

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 auszugebenden Oberflächen am Bildschirm sichtbar werden, werden Shader-Programme benötigt, welche die Farbinformation 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 die gleiche Farbe, also Rot bzw. Blau, zurück.

Der OpenGL-Code in Python ähnelt der Vorgehensweise bei der OpenGL-Entwicklung in anderen Programmiersprachen, weshalb hier darauf verzichtet wird, die Konzepte ausführlich zu erläutern. Der vollständige Quellcode der OpenGL-Testanwendung sieht wie folgt aus:

# 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 aber eine Ausnahme gemacht werden, da die Namen aller OpenGL-Funktionen und -Konstanten mit gl bzw. GL_ beginnen und somit klar 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, dass wxPython mit BUILD_GLCANVAS kompiliert wird, was bei den vorkompilierten Bibliotheken weder für Windows noch für Linux der Fall ist.

Zeitgesteuerter Programmablauf mit Timer

In GTK-Programmen kann der Timer aus der GLib benutzt werden, der intern auf Basis einer Nachrichtenverarbeitungsschleife funktionieren dürfte. Dieser Timer ist mit dem Timer-Steuerelement aus Classic Visual Basic vergleichbar. Im folgenden Beispiel wird der Timer benutzt, um regelmäßig Text auf die Konsole auszugeben (das Programm kann durch Schließen der Konsole beendet werden):

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 kann in Python über den Wrapper wxPython benutzt werden. WxWidgets gibt es für Linux, macOS und Windows und zeichnet sich dadurch aus, daß jeweils die nativen Steuerelemente des Betriebssystems verwendet werden und nur im Bedarfsfall auf eigene Implementierungen zurückgegriffen wird. Dadurch wird sichergestellt, daß sich mit wxWidgets entwickelte Anwendungen auf allen Betriebssytemen optisch möglichst gut einfügen.

Eine erste Herausforderung ist die Installation von wxPython unter Linux Mint 19.1 LTS und Windows 7. Unter Windows mit CPython ist das einfach über den Befehl pip auf der Eingabeaufforderung (cmd) möglich. Unter Linux ist zunächst erforderlich, die Entwicklungstools (C++-Compiler etc.) zu installieren, da es für die erwähnte Linux-Version keine vorkompilierten Binärpakete für wxPython gibt.

Die Installation von wxPython unter Linux Mint 19.1 LTS ist derzeit (05/2019) nur 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 GUI

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

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 positioniert werden, sondern mittels eines Layoutmanagers (in diesem Fall dem BoxSizer).

Alternativ zum Aufbau von Formular und Steuerelementen im Code kann auch ein grafischer GUI-Designer für wxPython wie etwa wxFormBuilder oder wxGlade benutzen.

Konfetti-Animation mit Bitmap-Puffer

Ziel dieses Beispiels ist das Erstellen einer Animation, bei der Konfettis 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 Konfetti hinzukommen.

Benutzerschnittstelle der Konfetti-Animation

Die Konfettis lassen sich zeichnen, indem man einen Device Context (DC) für das Fenster holt und anschließend mit den Zeichenfunktionen das neue Konfetti zeichnet. Diese Lösung hat jedoch einen Nachteil: Wird ein Teil des Fensters z. B. durch ein anderes Fenster verdeckt, muß beim Wegschieben dieses überlappenden Fensters der ehemals verdeckte Bereich neu gezeichnet werden. Das erfprdert, dass Reihenfolge, Position und Farbe der Konfettis verfügbar sind. Ist das 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 gleich 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 Ereignisbehandlungsmethode werden ein MemoryDC für die Puffer-Bitmap erstellt und ein neues Konfetti hinzugefügt. Damit das neue Konfetti am Bildschirm erscheint, muss ein Neuzeichnen des Fensters ausgelöst werden.

Der recht kompakte Code des Konfetti-Programms sieht wie folgt aus:

# 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 sind 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 gibt es unter anderem die Formulardesigner wxGlade (Name in Anlehnung an Glade für GTK) und wxFormBuilder. 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 dem durch Klick auf einen Button eine personalisierte Begrüßung mit dem in einem Textfeld eingegeben Namen angezeigt wird.

Unter Linux Mint kann wxGlade über folgendes Kommando im Terminal installiert werden:

sudo apt install wxglade

Anschließend kann das gewünschte Fenster in wxGlade entworfen werden. Der im folgenden Bild sichtbare Mehrfenstermodus von wxGlade wurde in einer späteren Version von 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 dann 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, die dem gleichnamigen Fenster aus dem entsprechenden wxGlade-Projekt entspricht.

Das in wxGlade entworfe Formular kann im eigenen Python-Code genutzt werden, indem eine Klasse hinzugefügt wird, die von der vom Designer erstellten Klasse erbt. Dort werden die im wxGlade-Designer dem Klickereignis der Schaltfläche zugeordnete Ereignisbehandlungsmethoden hinterlegt – im Greeter-Programm durch 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 Code oben ähnelt stark der ursprünglichen wxPython-Greeter-Anwendung. Der Einsatz von wxGlade bietet jedoch einige Unterschiede bzw. Vorteile gegenüber dem Formularaufbau durch eigenen Code:

Formulare aus einer XRC-Datei laden

Der GUI-Designer wxGlade kann nicht nur Python-Code zum Erstellen der Fenster/Steuerelemente generieren, sondern auch XRC-Dateien. Das sind XML-Dateien, die Fensterdefinitionen enthalten und in Python genutzt werden können, um Formulare samt Steuerelementen aus einer externen Datei zu laden.

Im vorigen Beispiel wurde wxGlade genutzt, um ein Formular zu entwerfen und Python-Dateien zu erstellen, die in das Python-Programm eingebunden werden konnten. Der Nachteil dieses Ansatzes ist, dass wxGlade Python-Code mit Klassendefinitionen für die Formulare erstellt. Soll nun eigener Code zur Ereignisbehandlung hinzugefügt werden, müssen Klassen erstellt werden, die von den durch wxGlade erstellten Klassen erben und die Ereignisbehandlungsmethoden enthalten.

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

Mittels der wxPython-XRC-Klasse XmlResource kann anschließend im Python-Programm das Formular aus der XRC-Datei geladen werden. Folgendes 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. Folgendes 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-Code zur Ereignisbehandlung wird in einer Klasse MainFrame hinzugefügt: In diesem Beispiel soll bei Klick auf eine Schaltfläche ein Meldungsdialogfeld mit einer Begrüßung angezeigt werden. Der Quellcode sieht wie folgt aus:

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 erstellt werden. (Mit XmlResource könnten auch nur einzelne Steuerelemente aus der XRC-Datei geladen werden.)

Beim Laden des Formulars über die Methode LoadFrame ist zu beachten, dass 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 Steuerelemente des Formulars zugreifen zu können, muß das 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 wurde.

Mittels xrc.XRCCTRL kann auf die Steuerelemente aus der XRC-Datei zugegriffen werden. Dies ermöglicht, über Variablen auf die Steuerelemente zuzugreifen und Ereignisbehandlungsmethoden zu verdrahten. Folgendes Listing zeigt den entsprechenden Code 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++ erstellt wurden. Da es sich um XML-Dateien handelt, lassen sich XRC-Dateien auch mit einem XML- bzw. Texteditor schreiben und bearbeiten. Für einen einfachen Arbeitsablauf empfiehlt sich aber der Entwurf der Formulare mit wxGlade und der Export der 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 dem sich einfache Benutzeroberflächen plattformunabhängig entwickeln lassen. Tkinter ist von den Fähigkeiten her nicht mit GTK, wxPython und Qt vergleichbar und eignet sich nur für einfache Formulare. So werden unter Windows keine Accessibility-Daten (z. B. zur Benutzung mit einem Screenreader) bereitgestellt.

Wenn man das CPython-Installationsprogramm benutzt, wird Tkinter automatisch installiert. Andernfalls lässt 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 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 im obenstehenden Listing entsprechen der Sub Main in Classic Visual Basic und werden als erstes aufgerufen. Sie dienen dem Erstellen des Formulars und dem Start der Nachrichtenverarbeitungschleife.

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 Außenabstände, ipadx und ipady Innenabstände.

Mittels command=self.on_greet_button_clicked lässt 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 sieht unter Linux Mint wie folgt aus:

Hauptfenster des Tkinter-Beispielprogramms

Tkinter bietet mehrere Themes, zu deren Nutzung die Steuerelemente aus ttk anstatt aus tkinter benutzt werden müssen. Je nach Betriebssystem werden unterschiedliche Themes mitgeliefert, auch eigene Themes lassen sich erstellen.

Für Windows gibt es u.a. das Theme vista, das unter Windows 7 mit Tkinter aus CPython bereits voreingestellt ist und nicht explizit aktiviert werden muss. Das nachstehende Beispiel zeigt die Aktivierung des Thems 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 bietet Tkinter eine Zeichenfläche ähnlich dem PictureBox-Steuerelement in Classic Visual Basic. Kantenglättung wird allerdings unter Windows und Linux nicht unterstützt. Im folgenden Beispiel wird ein Formular mit einer Zeichenfläche erstellt, auf der 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 können verschiedene Datenbankmanagementsysteme und Access genutzt werden (Dokumentation zu Datenbankschnittstellen von Python). Eine der Optionen ist SQLite, eine serverlose Datenbank, die bequem mit Programmen ausgeliefert werden kann. Unter Linux Mint müssen zur Verwendung von SQLite in Python keine zusätzlichen Pakete installiert werden.

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

Folgendes Beispiel zeigt den Umgang mit einer SQLite-Datenbank und basiert auf der Dokumentation zu SQLite. Dabei werden eine Datenbank angelegt, eine Tabelle für Aktientransaktionen erstellt, Beispieleinträge eingefügt und 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] ein Array und mit geschweiften Klammern ein Wörterbuch (Schlüssel–Wert-Paare).

In Linux Mint kann man die mittels Python erstellte SQL-Datenbankdatei in SQLite Browser in einer grafischen Benutzeroberfläche ansehen. Die Installation läuft im Terminal über das folgende Kommando:

sudo apt install sqlitebrowser

SQLite Browser wird durch Aufruf von sqlitebrowser gestartet. Das Programm mit der geöffneten stocks.db sieht wie folgt aus:

Benutzerschnittstelle von SQLite Browser

PDF-Dateien mit Cairo erstellen

Die Grafikbibliothek Cairo kapselt unter Windows nicht einfach GDI-Funktionen, sondern bietet wesentlich mehr. So kann Cairo etwa genutzt werden, um PDF-Dateien zu erstellen. Der Zeichencode ist dabei gleich wie beim 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()