PyQt6 und QTDesigner

Ein­bin­dung von Cus­tom Widgets

PyQt6, die aktu­el­le Ver­si­on der Qt-Biblio­thek für Python, lässt sich ohne Pro­ble­me mit dem QtDe­si­gner zusam­men nutzen.

Mit dem QtDe­si­gner lässt sich die Bedien­ober­flä­che einer Python-Appli­ka­ti­on in einem gra­fi­schen Tool rela­tiv ein­fach zusam­men­set­zen. Es hakt zwar an der einen oder ande­ren Stel­le, aber not­falls bleibt immer noch das hän­di­sche Edi­tie­ren der vom Desi­gner erzeug­ten *.ui Datei.

In Python lässt sich dann die Beschrei­bungs­da­tei der Ober­flä­che (hier: myGui.ui) mit einer ein­zi­gen Anwei­sung (unten fett dar­ge­stellt) laden:

from PyQt6 import QtWidgets, uic
from myWidgets import myWidget # Custom Widget

class Ui(QtWidgets.QMainWindow):

  def __init__(self):
    super(Ui, self).__init__()
    uic.loadUi(join(cwd(), 'myGui.ui'), self)

app = QtWidgets.QApplication(sys.argv)
window = Ui()
app.exec()

Auch benut­zer­de­fi­nier­te Bedien­ele­men­te, in Qt „Cus­tom Wid­gets” genannt, las­sen sich rela­tiv ein­fach nutzen. 

Der Desi­gner „kennt” aber die­se Wid­gets natür­lich zunächst nicht. Des­we­gen plat­ziert man an die Stel­le, die spä­ter das eige­ne selbst pro­gram­mier­te Wid­get ein­neh­men soll, ein mög­lichst ähn­li­ches Standard-Widget.

Im Kon­text­me­nü des Wid­gets klickt man dann auf „Als Platzhalter für benutzerdefinierte Klasse festlegen” und trägt den Namen der Cus­tom-Klas­se unter „Klassenname” und „Include-Datei” ein.

Nach einem Klick auf „Hin­zu­fü­gen” und „Anwen­den” wird in der Über­sicht schon die neue Klas­se für die­ses Wid­get ange­zeigt. Damit Python die Cus­tom-Wid­get-Klas­se beim Laden der .ui-Datei fin­det, muss sie in der Datei mit dem uic.loadUi Auf­ruf impor­tiert wer­den (sie­he oben in Zei­le 2).

Beim spä­te­ren Laden der Desi­gner-Datei im Python-Pro­gramm ver­sucht der Qt-Lader, alle Attri­bu­te, die man im Desi­gner gesetzt hat, mit den ent­spre­chen­den getattr/setattr Metho­den der Cus­tom-Klas­se zu set­zen. Die­se Attri­bu­te müs­sen in der Cus­tom-Klas­se exis­tie­ren, sonst bricht das Laden mit Feh­lern ab.

Dyna­mic Properties

Etwas ver­wir­rend ist, dass im Desi­gner wei­ter­hin die Attri­bu­te der Platz­hal­ter­klas­se ange­zeigt wer­den, obwohl die Cus­tom-Klas­se die­se Eigen­schaf­ten even­tu­ell gar nicht besitzt. Setzt man bei­spiels­wei­se im Desi­gner die readOnly-Eigen­schaft, erwar­tet der Qt-Lader, dass die Cus­tom-Klas­se auch eine setReadOnly-Metho­de besitzt.

Den ande­ren Fall, näm­lich dass die Platz­hal­ter-Klas­se Eigen­schaf­ten des Cus­tom Wid­get nicht besitzt, kann man mit den „Dyna­mic Pro­per­ties” lösen.

In der Objektan­zei­ge fin­det man neben dem Fil­ter für die Eigen­schaf­ten ein Plus-Zei­chen. Nach einem Klick dar­auf muss zunächst der Daten­typ der Eigen­schaft gewählt wer­den, dann öff­net sich ein Fens­ter, in dem man den Namen der Eigen­schaft ein­tra­gen kann.

Nach dem Klick auf „OK” fin­det man die neu­en dyna­mi­schen Eigen­schaf­ten am Ende der Objektanzeige.

In Python gestal­tet sich aller­dings der Zugriff auf die­se Eigen­schaf­ten schwie­ri­ger als erwar­tet. Es reicht nicht, die­se Eigen­schaf­ten als Python-Pro­per­ties des Cus­tom Wid­gets anzu­le­gen; Qt fin­det die­se beim Anle­gen der Objekt­in­stanz nicht.

Statt­des­sen muss man das pyqtProperty Makro bemü­hen, um PyQt die Objek­tei­gen­schaf­ten bekannt zu machen. Dem Makro gibt man den Daten­typ und die Refe­ren­zen auf Get­ter und Set­ter der Pro­per­ty als Para­me­ter mit (wei­te­re Para­me­ter sind mög­lich, voll­stän­di­ge Beschrei­bung sie­he PyQt-Doku­men­ta­ti­on).

from PyQt6.QtCore import pyqtProperty

...

class myCustomWidget(QWidget):

    def get_read_only(self):
        return self._readOnly

    def set_read_only(self, ro):
        self._readOnly = ro

    readOnly = pyqtProperty(bool, get_read_only, set_read_only)

...

Auf die­se Wei­se kann man die Eigen­schaft wie eine nor­ma­le Objek­tei­gen­schaft nut­zen, wäh­rend im Hin­ter­grund der Get­ter den Inhalt der im QtDe­si­gner defi­nier­ten Eigen­schaft aus­liest oder der Set­ter sie ändert.

Vor­sicht: der Wert der Dyna­mic Pro­per­ty wird gna­den­los auf den in pyqt­Pro­per­ty ange­ge­be­nen Wert gecas­tet. Aus einem nicht lee­ren String wird also z.B. immer True, wenn bool ange­ge­ben wird.

Spe­zi­el­le Typen wie die enums der Qt::Alignment Flags las­sen sich im Desi­gner nicht direkt ein­ge­ben, da sie in der Lis­te der aus­wähl­ba­ren Typen feh­len. Dafür müss­te man ein Desi­gner-Plug­in für das Cus­tom Wid­get erzeu­gen. Als Aus­weg bleibt die Nut­zung eines Strings und das Map­ping in der Setter-Methode.

alignment_flag = {
    'right': Qt.AlignmentFlag.AlignRight,
    'left': Qt.AlignmentFlag.AlignLeft,
    'center': Qt.AlignmentFlag.AlignCenter,
    'top': Qt.AlignmentFlag.AlignTop,
    'bottom': Qt.AlignmentFlag.AlignBottom,
    'middle': Qt.AlignmentFlag.AlignVCenter,
}

...

    def get_alignment(self):
        return self._alignment

    def set_alignment(self, align):
        if align in alignment_flag:
            self._alignment = align
            self.input.setAlignment(alignment_flag[align])

    alignment = pyqtProperty(str, get_alignment, set_alignment)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

18 + zehn =