Python unter Linux: Qt4

Aus Wikibooks

In diesem Kapitel geht es um grafische Benutzeroberflächen. Wir geben einen Einblick in die Programmierung mit Qt Version 4, zu der Wikibooks das Buch Qt für C++-Anfänger als zusätzliches Angebot hat.

Fenster, öffne Dich![Bearbeiten]

Die erste Anwendung öffnet ein Fenster und hat einen Fenstertitel:

#!/usr/bin/python

import sys
from PyQt4 import QtGui

class Fenster(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.setWindowTitle("Python unter Linux")
        self.resize(300, 300)
        self.show()

app = QtGui.QApplication(sys.argv)
f = Fenster()
app.exec_()
guiqt1.py

Wir binden PyQt4.QtGui ein, um auf Elemente der grafischen Benutzeroberfläche zugreifen zu können. Die Fensterklasse wird von QMainWindow abgeleitet, einer Klasse, die schon die wichtigsten Elemente eines Hauptfensters mitbringt. Es ist recht einfach, wie wir später sehen werden, auf Grundlage dieser Klasse Menüs und eine Statusleiste hinzuzufügen. setWindowTitle() legt den Namen für das Fenster fest, resize() eine Anfangsgröße, die wir dynamisch ändern können. Mit show() wird das Objekt angezeigt.

Im Hauptprogramm erzeugen wir ein QApplication()-Objekt und unser Fenster. exec_() startet die Anwendung und wartet, bis das Fenster geschlossen wird.

Signale empfangen[Bearbeiten]

Grafische Nutzeroberflächen bestehen aus Elementen, in Qt Widgets genannt, die sich um die Interaktion mit Benutzern kümmern. Widgets sind zum Beispiel Textfelder (QLabel), Editoren (QTextEdit) oder Knöpfe (QPushButton). Viele dieser Widgets nehmen Benutzereingaben entgegen, so kann ein Knopf gedrückt werden, ein Menüpunkt aktiviert werden. Qts Widgets senden bei Interaktionen Signale aus, die man verarbeiten kann. Die folgende Anwendung zeigt, wie man Menüs erzeugt und reagiert, wenn sie aktiviert werden:

#!/usr/bin/python
import sys
from PyQt4 import QtCore, QtGui

class Fenster(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.setWindowTitle("Python unter Linux")
        self.makeActions()
        self.makeMenu()
        self.show()

    def makeActions(self):
        self._exitAction = QtGui.QAction("&Ende", None)
        self._helpAction = QtGui.QAction("Hilfe!", None)
        self.connect(self._exitAction, QtCore.SIGNAL('triggered()'), self.slotClose)
        self.connect(self._helpAction, QtCore.SIGNAL('triggered()'), self.slotHelp)

    def makeMenu(self):
        menuBar = self.menuBar()
        fileMenu = menuBar.addMenu("&Datei")
        fileMenu.addAction(self._exitAction)
        helpMenu = menuBar.addMenu("&Hilfe")
        helpMenu.addAction(self._helpAction)

    def slotClose(self):
        self.close()

    def slotHelp(self):
        QtGui.QMessageBox.information(None, "Dies ist die Hilfe", "Hilf dir selbst, sonst hilft dir keiner!")

app = QtGui.QApplication(sys.argv)
f = Fenster()
app.exec_()
guiqt2.py

Innerhalb von makeActions() definieren wir zwei QAction()-Objekte. Diese Objekte haben einen Namen und optional ein Bild. Wann immer QAction()-Objekte aktiviert werden, sie also das Signal QtCore.SIGNAL('triggered()') senden, soll eine Funktion aufgerufen werden, die wir in der Klasse definiert haben, also beispielsweise slotClose(). Diese Verknüpfung zwischen den beiden Objekten, QAction() auf der einen Seite und dem Fenster()-Objekt auf der anderen Seite übernimmt die Funktion connect().

makeMenu() erzeugt die Menüs. QtGui.QMainWindow enthält selbst schon eine Menüzeile, die man sich mit menuBar() beschafft. An diese Menüzeile fügt man mit addMenu() einen Menüeintrag hinzu. Diese Menüs findet man zuoberst in der Menüzeile einer jeden Anwendung. Jedes Menü kann beliebig viele QAction()-Einträge haben. Da diese einen eigenen Titel haben, werden sie sogleich angezeigt. Die weiteren Methoden der Klasse beinhalten die Ereignisse: close() schließt das Fenster und QMessageBox.information() zeigt einen informativen Text an.

Mehr Widgets[Bearbeiten]

Hauptfenster haben üblicherweise zentrale Widgets. Das sind zumeist selbst geschriebene Widget-Klassen, in denen die hauptsächliche Interaktion stattfindet, wie zum Beispiel die Anzeige einer Webseite im Browser. Das folgende Beispiel implementiert einen Einkaufsberater. Der eigentliche Einkauf findet in einem eigenen Dialog statt, die Zusammenfassung erfolgt in einer Tabelle im Hauptfenster. Zwischen Dialog und Hauptfenster wird eine Nachricht über den Kaufwunsch und die Zahlungsweise ausgetauscht.

#!/usr/bin/python
# -*- coding:utf-8 -*-
import sys
from PyQt4 import QtCore, QtGui

class Dialog(QtGui.QDialog):
    """Der Einkaufsdialog"""
    def __init__(self):
        QtGui.QDialog.__init__(self)
        self.setWindowTitle("Elektronischer Einkauf")
        vbox = QtGui.QVBoxLayout()
        self._liste = QtGui.QListWidget()
        self._liste.addItem(u"Milch")
        self._liste.addItem(u"Eier")
        self._liste.addItem(u"Käse")
        vbox.addWidget(self._liste)
        group = QtGui.QGroupBox("Zahlungsweise")
        vboxGroup = QtGui.QVBoxLayout()
        self._r1 = QtGui.QRadioButton("Kreditkarte")
        vboxGroup.addWidget(self._r1)
        self._r2 = QtGui.QRadioButton("Bargeld")
        vboxGroup.addWidget(self._r2)
        self._r3 = QtGui.QRadioButton("Nachnahme")
        vboxGroup.addWidget(self._r3)
        self._r2.setChecked(True)
        group.setLayout(vboxGroup)
        vbox.addWidget(group)
        fertigButton = QtGui.QPushButton("Fertig")
        self.connect(fertigButton, QtCore.SIGNAL('clicked()'), self.fertig)
        vbox.addWidget(fertigButton)
        self.setLayout(vbox)
        self.show()

    def fertig(self):
        ware = u"%s" % self._liste.currentItem().text()
        label = ""
        for rb in [self._r1, self._r2, self._r3]:
            if rb.isChecked():
                label = "%s" % rb.text()
                break
        self.emit(QtCore.SIGNAL('signalEinkaufFertig'), (ware, label))
        self.accept()


class Fenster(QtGui.QMainWindow):
    """Diese Klasse stellt das Hauptfenster dar, das zentrale Widget ist eine Tabelle"""
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.setWindowTitle("Python unter Linux")
        self.makeActions()
        self.makeMenu()
        # Statuszeile
        self._label = QtGui.QLabel(u"Einkaufszähler")
        self.statusBar().addWidget(self._label)
        # Tabelle
        self._tableWidget = QtGui.QTableWidget(0, 2)
        self._tableWidget.setHorizontalHeaderLabels(["Ware", "Zahlungsweise"])
        self.setCentralWidget(self._tableWidget)
        self.show()

    def makeActions(self):
        self._dialogAction = QtGui.QAction("Einkauf", None)
        self._exitAction = QtGui.QAction("&Ende", None)
        self.connect(self._dialogAction, QtCore.SIGNAL('triggered()'), self.slotEinkauf)
        self.connect(self._exitAction, QtCore.SIGNAL('triggered()'), self.slotClose)

    def makeMenu(self):
        menuBar = self.menuBar()
        fileMenu = menuBar.addMenu("&Datei")
        fileMenu.addAction(self._dialogAction)
        fileMenu.addSeparator()
        fileMenu.addAction(self._exitAction)

    def slotEinkauf(self):
        """Ruft den Einkaufsdialog auf"""
        d = Dialog()
        self.connect(d, QtCore.SIGNAL('signalEinkaufFertig'), self.slotEinkaufFertig)
        d.exec_()

    def slotClose(self):
        """Wird aufgerufen, wenn das Fenster geschlossen wird"""
        ret = QtGui.QMessageBox.question(None, "Ende?", "Wollen Sie wirklich schon gehen?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
        if ret == QtGui.QMessageBox.Yes:
            self.close()

    def slotEinkaufFertig(self, ware):
        """Dieser Slot fügt eine Tabellenzeile an und stellt in dieser die gekauften Waren vor"""
        numRows = self._tableWidget.rowCount()
        # Neue Zeile
        self._tableWidget.insertRow(numRows)
        # Inhalte einfügen
        t1 = QtGui.QTableWidgetItem(ware[0])
        t2 = QtGui.QTableWidgetItem(ware[1])
        self._tableWidget.setItem(numRows, 0, t1)
        self._tableWidget.setItem(numRows, 1, t2)
        # Update Statuszeile 
        text = u"Heute wurden %d Aktionen getätigt" % (numRows + 1)
        self._label.setText(text)


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    f = Fenster()
    app.exec_()
guiqt3.py Anwendung
guiqt3.py Dialogfenster

Der Einkaufsberater besteht aus zwei Elementen, nämlich dem Dialog, einer von QDialog abgeleiteten Klasse, und dem Hauptfenster. Der Dialog wird angezeigt, wenn die Methode slotEinkauf() vom Hauptfenster aufgerufen wurde. Der Dialog besteht aus einigen ineinander geschachtelten Elementen: Innerhalb des vertikalen Layouts (QVBoxLayout) werden ein List-Widget (QListWidget), eine Group-Box (QGroupBox) und ein Knopf (QPushButton) eingefügt. Der Listbox werden mit addItem() einige Beispielwaren hinzugefügt. Die Group-Box enthält ein eigenes vertikales Layout, mit dem sie drei Auswahlschalter (QRadioButton) zur Angabe der Zahlungsweise verwaltet, von denen einer vorselektiert (self._r2.setChecked(True)) ist.

Wird der Knopf ("Fertig") gedrückt, soll die Methode Dialog.fertig() aufgerufen werden. In dieser Methode werden die Auswahlknöpfe abgefragt, genau einer von ihnen ist immer aktiviert. Ebenfalls wird der im List-Widget aktivierte Text abgefragt. Sobald beide Informationen vorliegen, wird ein Signal abgeschickt. Dieses besorgt die Methode emit(), die das Signal wie auch ein Tupel mit Informationen erwartet. Anschließend wird der Dialog geschlossen.

Das Hauptfenster enthält Menüs, eine Statuszeile, in der sich eine Textfeld (QLabel) befindet wie auch als zentrales Element eine Tabelle. Wird per Menü die Methode slotEinkauf() aufgerufen, erstellt sie den beschriebenen Dialog, verknüft das von diesem ausgehende Signal mit dem Slot slotEinkaufFertig() und ruft dessen exec_()-Methode auf.

slotEinkaufFertig() enthält im Parameter ware das Tupel mit den aktuellen Einkaufsinformationen. Diese Informationen sollen in die Tabelle geschrieben werden, wobei diese zuerst um eine Zeile wachsen muss. Dann werden pro Spalte ein QTableWidgetItem erstellt, diese werden per setItem(Reihe, Spalte, Item) in die Tabelle eingefügt. Zu guter Letzt wird das Label der Statuszeile auf den neusten Stand gebracht.

Design-Server[Bearbeiten]

Der Einkaufsführer brachte es an den Tag: Das händische schreiben von Widgets und das passende Plazieren derselbigen auf einem Fenster macht viel Mühe. Viel leichter geht es, wenn man sich die Fenster und deren Inhalte zusammenklickt. Das Programm hierzu heißt Designer (designer-qt4). Mit seiner Hilfe plaziert man auf Fenstern die Widgets, die man benötigt und layoutet sie auf grafische Weise.

Designer
Unser Dialogfenster

Das folgende Programm benötigt nur drei Labels in einem Dialog-Fenster, von denen zwei label_client1 und label_client2 heißen. Auf diese beiden Labels wollen wir im Quelltext verweisen. Schauen Sie sich hierzu bitte das Bild Unser Dialogfenster an. Speichern Sie die Designer-Datei unter dem Namen server.ui ab. Sie enthält in XML-Notation alle Informationen über das Dialog-Fenster.

Um den Dialog für Python benutzbar zu machen, ist noch ein Zwischenschritt nötig:

Align=none Ausgabe

user@localhost:~$ pyuic4 -o server_ui.py server.ui
(keine Ausgabe)

Sie erhalten eine Datei mit dem Namen server_ui.py. Diese können Sie direkt in den Python-Code importieren.

Das Programm, welches wir nun besprechen wollen, ist ein Server, der Verbindungsinformationen bereit hält. Man kann sich an ihm anmelden, wobei er zwei Clients akzeptiert. Diesen sendet er einen Willkommensgruß und schreibt auf den Dialog einen Verbindungshinweis. Wird ein Client beendet, so registriert der Server das ist bereit, eine weitere Verbindung zu akzeptieren. Der Server tut also nichts als zu merken, ob gerade ein Client verbunden ist oder nicht.

#!/usr/bin/python

import sys
from server_ui import Ui_Dialog
from PyQt4 import QtCore, QtGui, QtNetwork

class Dialog(QtGui.QDialog, Ui_Dialog):
    def __init__(self):
        QtGui.QDialog.__init__(self)
        self.setupUi(self)

    def connect(self, num):
        if num == 1:
            self.label_client1.setText ("Client1 connected")
        else:
            self.label_client2.setText ("Client2 connected")

    def disconnect(self, num):
        if num == 1:
            self.label_client1.setText ("Client1 disconnected")
        else:
            self.label_client2.setText ("Client2 disconnected")

class Server(QtNetwork.QTcpServer):
    def __init__(self, numClients):
        QtNetwork.QTcpServer.__init__(self)
        self._numClientsWanted = numClients
        self._numClients = 0
        self.address = QtNetwork.QHostAddress(QtNetwork.QHostAddress.LocalHost)
        # socketInfo: [(socket, connected, num), ...]
        self.socketInfo = []
        for i in xrange(numClients):
            self.socketInfo.append((None, False, i+1))
        ret = self.listen(self.address, 6000)
        self.connect(self, QtCore.SIGNAL("newConnection()"), self.connection)

    def connection(self):
        if self.hasPendingConnections():
            print "connection available",
            socket = self.nextPendingConnection()
            if self._numClients < self._numClientsWanted:
                self.connect(socket, QtCore.SIGNAL("disconnected()"), self.dis)
                for i, (p, c, n) in enumerate(self.socketInfo):
                    if c == False:
                        self.socketInfo[i] = (socket, True, n)
                        print "accepted"
                        msg = "You are accepted. Client %d" % n
                        socket.write(msg)
                        self.emit(QtCore.SIGNAL("connected"), n)
                        break
                self._numClients += 1
            else:  
                print "rejected"
                socket.close()

    def dis(self):
        print "socket disconnected"
        self._numClients = 0
        for i, (p, c, n) in enumerate(self.socketInfo):
            if c:
                if p.state() == QtNetwork.QAbstractSocket.UnconnectedState:
                    p.close()
                    self.socketInfo[i] = (None, False, n)
                    self.emit(QtCore.SIGNAL("disconnected"), n)
                else:
                    self._numClients += 1

app = QtGui.QApplication(sys.argv)
window = Dialog()
server = Server(2)
app.connect(server, QtCore.SIGNAL("connected"), window.connect)
app.connect(server, QtCore.SIGNAL("disconnected"), window.disconnect)
window.show()
sys.exit(app.exec_())

Die Designer-Datei wird mit from server_ui import Ui_Dialog eingebunden. Der Name ist in der Python-Datei server_ui.py bekannt. Ein Dialog auf der Basis wird erstellt, in dem er vom QDialog und dem Ui_Dialog abgeleitet wird: class Dialog(QtGui.QDialog, Ui_Dialog). Die Methode setupUi(self) initialisiert diesen Dialog anschließend. Der Rest der Klasse dient dazu, die Verbindungsinformationen anzuzeigen.

Der Server wird von der Basisklasse QTcpServer abgeleitet. Der Server soll maximal 2 Verbingungen gleichzeitig haben, und auf dem eigenen Host (Local host, QHostAddress(QtNetwork.QHostAddress.LocalHost)) laufen, seine Portadresse lautet 6000. socketInfo ist eine Liste, die die Clients speichert, wobei jeder Client durch einen TCP-Socket, einen Hinweis darauf, ob die Verbindung aktiv ist und eine eindeutige Nummer gekennzeichnet ist. Mit listen(self.address, 6000) wird auf Verbindungswünsche der Clients gewartet. Treffen diese ein, wird die Methode connection() aufgerufen. Falls wir den Verbindungswunsch akzeptieren, wird mit nextPendingConnection() der Socket des Clients geholt und an passender Stelle in die Struktur socketInfo eingefügt. Dem Client wird eine Nachricht geschickt und dem Dialog wird angezeigt, dass sich ein Client verbunden hat.

Falls sich ein Client verabschiedet, beispielsweise, weil die DSL-Leitung unterbrochen wurde oder der Nutzer das Client-Programm auf andere Weise beendete, wird die Methode dis() aufgerufen. Sie findet heraus, welcher Client sich abgemeldet hat und gibt entsprechend Nachricht darüber aus.

Um das Programm zu testen, benutzen Sie einfach das Programm Telnet. Starten Sie hierzu den Server und rufen auf einer anderen Konsole folgendes auf:

Align=none Ausgabe

user@localhost:~$ telnet localhost 6000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
You are accepted. Client 1

Sobald Sie sich verbunden haben, wird dieses auch im Dialogfenster angezeigt.

Zusammenfassung[Bearbeiten]

In diesem Kapitel haben Sie die Grundzüge der Programmierung mit dem Toolkit Qt Version 4 kennen gelernt. Wir haben einige Widgets besprochen, das Signal-Slot-Konzept kennen gelernt und Server-Programmierung als Thema für Fortgeschrittene Benutzer besprochen. Das Kapitel wurde mit einer Einführung in den Qt-Designer abgerundet.