PyQt und Pyside mögen sich nicht?

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Dein oben genannter Quelltext funktioniert. Da ich den Quelltext nicht verstehe, kannst du mir mal bitte erklären, was du da genau gemacht hast? Kurz zu meinem Quelltext. Was meinst du, das importierte Widget (Label und pushButton) wird nicht eingehängt? Die Widgets, die in der ui-Datei sind? Die werden doch mit geladen, sobald man die ui-Datei lädt?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Sophus hat geschrieben:Was meinst du, das importierte Widget (Label und pushButton) wird nicht eingehängt? Die Widgets, die in der ui-Datei sind? Die werden doch mit geladen, sobald man die ui-Datei lädt?
Das ist das, was ich meinte - QuiLoader funktioniert sehr anders als PyQts `uic.loadUi`. Damit die Widgets aus der ui-Datei auch gerendert werden, ist mehr nötig (Bsp. aus PySide Doku):

Code: Alles auswählen

class MyWidget(QWidget):
    def __init__(self, parent):
        super(QWidget, self).__init__(parent)
        loader = QUiLoader()
        file = QFile(":/forms/myform.ui")
        file.open(QFile.ReadOnly)
        myWidget = loader.load(file, self)
        file.close()

        layout = QVBoxLayout()
        layout.addWidget(myWidget)
        self.setLayout(layout)
Wie Du siehst, können mit QUiLoader nur Subwidgets erstellt werden, welche zusätzlich in das Elternwidget eingehängt werden müssen. Das ist für Dich ein Riesenproblem, wenn Du transparent zwischen PyQt und PySide wechseln können möchtest, da Du alle Methoden (signal und slots) umbiegen müsstest.

Mit dem von mir gezeigten Ladecode kannst Du das Problem umgehen, da dieser recht ähnlich zu PyQt tickt. Ich hab diesen nochmal etwas aufgeräumt und ein paar Tests dazu geschrieben:

Code: Alles auswählen

from PyQt4 import QtGui, QtCore
from PyQt4.uic import loadUi, loadUiType
#from PySide import QtGui, QtCore
#from uic_fix import loadUi, loadUiType


FormClass, BaseClass = loadUiType('./test.ui')

class TestBase(QtGui.QWidget):
    """Base test class with one slot"""
    def testSlot(self):
        print 'button pressed'


class Test1(TestBase):
    def __init__(self, parent=None):
        TestBase.__init__(self, parent)
        assert(loadUi('./test.ui', self) == self)

    def testSlot(self):
        print 'button pressed'

def test1(app):
    print 'TEST 1: applying form class to initializing class with loadUi'
    w = Test1()
    w.show()
    app.exec_()


class Test2(TestBase, FormClass):
    def __init__(self, parent=None):
        TestBase.__init__(self, parent)
        self.setupUi(self)

    def testSlot(self):
        print 'button pressed'

def test2(app):
    print 'TEST 2: multi inheritance with form class'
    w = Test2()
    w.show()
    app.exec_()


class Test3(TestBase):
    def __init__(self, parent=None):
        TestBase.__init__(self, parent)
        self.ui = FormClass()
        self.ui.setupUi(self)

    def testSlot(self):
        print 'button pressed'

def test3(app):
    print 'TEST 3: explicit form class instantiation (C++ default way)'
    w = Test3()
    w.show()
    app.exec_()


def test4(app):
    print 'TEST 4: late styling with form class'
    w = TestBase()
    f = FormClass()
    f.setupUi(w)
    w.show()
    app.exec_()


def test5(app):
    print 'TEST 5: late styling with loadUi'
    w = TestBase()
    assert(loadUi('./test.ui', w) == w)
    w.show()
    app.exec_()


def test6(app):
    print 'TEST 6: loadUi without baseinstance'
    try:
        loadUi('./test.ui')
    except AttributeError as e:
        assert(e.message == "'QWidget' object has no attribute 'testSlot'")


if __name__ == '__main__':
    app = QtGui.QApplication([])
    test1(app)
    test2(app)
    test3(app)
    test4(app)
    test5(app)
    test6(app)
Wie Du siehst, sind die API-SChnittstellen quasi gleich, für den Wechsel zwischen PySide und PyQt musst Du nur die Importe ändern. PySide braucht hierfür zusätzlich die Datei uic_fix.py:

Code: Alles auswählen

import pysideuic
from PySide import QtGui, QtUiTools
import xml.etree.ElementTree as xml
from cStringIO import StringIO

def loadUiType(filename):
    """Load form class from ui file."""
    parsed = xml.parse(filename)
    widget_classname = parsed.find('widget').get('class')
    form_classname = parsed.find('class').text
    with open(filename, 'r') as f:
        o = StringIO()
        frame = {}
        pysideuic.compileUi(f, o, indent=0)
        pyc = compile(o.getvalue(), '<string>', 'exec')
        exec pyc in frame
        form_class = frame['Ui_%s'%form_classname]
        base_class = getattr(QtGui, widget_classname)
    return form_class, base_class

def loadUi(filename, instance=None):
    """Load ui form class onto a given QWidget object."""
    form_cls, base_cls = loadUiType(filename)
    if not instance:
        instance = type(base_cls.__name__, (base_cls,),{})()
    cls = instance.__class__
    instance.__class__ = cls.__class__(cls.__name__, (cls, form_cls), {})
    instance.setupUi(instance)
    return instance
Zur Erklärung: PyQt kennt QUiLoader nicht. Das kommt direkt aus Qt und ist als Lademechanismus für Formularteile gedacht, wird allerdings selten genutzt in C++. Auch unterliegt es bestimmten Beschränkung - so ist es nicht möglich, den Formularstyle auf eine bestehendes Objekt anzuwenden. Genau das aber macht PyQt mit `uic.loadUi`.
Zum Nachladen der ui-Dateien nutzt PyQt die Fähigkeit von Python, zur Laufzeit Klassen erstellen, Code nachladen oder gar existierende Objekte modifizieren zu können. Und genau das machen die beiden Funktionen `loadUiType` und `loadUi`:
- `loadUiType` lädt eine ui-Datei, holt sich die Klassennamen des Formulars und der Basisklasse, generiert Pythoncode daraus und führt diesen aus, um an die Formularklasse als Pythontypen zu kommen (mit compile :twisted: )
- `loadUi` erweitert die Elternklassen des übergebenen Objektes zur Laufzeit (!) um die Formularklasse und ruft `setupUi` auf, was das Layout anwendet und Signal-/Slotverbindungen aktiviert. Das geht so nicht in C++, weshalb QUiLoader das nicht kann.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Selbst das Einhängen der Widget klappt bei mir nicht. Auch da hängt sich mein Interpreter auf. Einzig und allein dein genannter Quelltext klappt. Da wird die '.ui-Datei ordnungsgemäß geladen. Kommt es mir nur so vor, oder ist PySide etwas anspruchsvoller bzw. umständlicher als PyQt? Denn mit PyQt habe ich quasi gleich sofort angefangen, als ich mich mit Python auseinander gesetzt habe. Denn ich habe mir am Anfang noch nicht die Frage gestellt, ob PySide oder PyQt. Da ich mit PyQt von Anfang an gelernt habe, kommt mit diese Bindung "normal" vor. Nun dachte ich (da PySide und PyQt den gleichen Rahmenwerk benutzen), ich könnte mal mit PySide probieren. Aber irgendwie fühle ich mich leicht verschreckt, von PySide :shock:
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Hab grad rausgefunden, dass PySide ein Segfault beim Beenden des Testskriptes wirft, wenn Test6 der letzte ist und in einem vorherigen Test der Slot `testSlot` angesprochen wurde. Offensichtlich hat PySide Probleme, dass QApplication-Objekt ordentlich abzuräumen, wenn eine Signal-Slotverbindung fehlgeschlagen ist. Ich vermute einen hängenden Zeiger in der Slotlogik, leider hab ich keine Debugsymbole in PySide um der Sache weiter auf den Grund gehen zu können. Evtl. ist das auch die Ursache Deines Segfaults.

Ich glaube nicht, dass PySide per se schwieriger ist. Es ist einfach anders, IMHO ist PySide näher an Qt dran, was die Umsetzung von Qt-Klassen angeht, während PyQt vielfach mit Pythonmitteln abkürzt. Auf der anderen Seite ist PySide vermutlich weniger gut getestet und jünger als PyQt, was den ein oder anderen Segfault erklären könnte.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Es war ein Problem des Lifecycles der Formularklassen-Objekte in Python, wenn die Referenz hierauf bis Programmende voegehalten wird, verschwindet der Segfault. Ich werde das Skript updaten und in ein Repo packen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

An welcher Stelle wird die Referenz gehalten?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Also, das öffnen der "reinen" *-ui-Datei ist mit deinem Quelltext kein Problem, allerdings habe ich ein Problem ein Kompilat zu öffnen.

Code: Alles auswählen

from uic_fix import loadUi, loadUiType
from PySide import QtGui, QtCore
import ui_rc

class TestWidget(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        UI_PATH = QtCore.QFile(":/ui_file/CustomDialog.ui")

        UI_PATH.open(QtCore.QFile.ReadOnly)
        self.ui_mdi = loadUi(UI_PATH), self
        UI_PATH.close()
        
        self.pushButton.clicked.connect(self.close)
        
if __name__ == '__main__':
    app = QtGui.QApplication([])
    w = TestWidget()
    print type(w)
    w.show()
    app.exec_()
Traceback:
Traceback (most recent call last):
File "D:\Dan\Python\xarphus_project\examples\minimal_example\jerch_test_pyside.py", line 19, in <module>
w = TestWidget()
File "D:\Dan\Python\xarphus_project\examples\minimal_example\jerch_test_pyside.py", line 12, in __init__
self.ui_mdi = loadUi(UI_PATH), self
File "D:\Dan\Python\xarphus_project\examples\minimal_example\uic_fix.py", line 23, in loadUi
form_cls, base_cls = loadUiType(filename)
File "D:\Dan\Python\xarphus_project\examples\minimal_example\uic_fix.py", line 11, in loadUiType
with open(filename, 'r') as f:
TypeError: coercing to Unicode: need string or buffer, PySide.QtCore.QFile found
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Hier ist die aktualisierte Version --> https://github.com/jerch/pyside-uicfix

Ja bisher geht nur das Laden per Dateinamen. Ich weiss gar nicht, was PyQts `uic.loadUi` da alles unterstützt, Du kannst das gerne zusammentragen, dann patche ich das nach.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Ich habe mich eben ein wenig umgesehen. Ich fand dieses Forum. Hierbei wird im Zusammenhang von Maya 2014 debattiert. Aber im Wesentlichen versuchen die loadUI-Funktion zu überschreiben. Vielleicht kannst du was Nützliches daraus entnehmen. Aber die werkeln auch nur mit der reinen *.ui-Datei - nicht mit dem Kompilat. Und dann fand ich diese GitHub-Seite. Allerdings auch nur mit der reinen *.ui.Datei.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Dein erster Link macht es analog mit `loadUiType`. Der zweite Link kann nur `loadUi`.

Du musst mir im Prinzip nur sagen, welche Dateiangaben PyQt's `loadUi` alles kann. Oder zeig mir einfach, wie Du es mit PyQt bisher gemacht hast. Wahrscheinlich fehlen Pythons Dateiobjekt und Qts QFile bzw. QIODevice-Objekte als erlaubte Parameter.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Also, die kompilierte *.ui-Dateien habe ich immer wie folgt geladen:

Code: Alles auswählen

import sys
from PyQt4.QtCore import QFile
from PyQt4.QtGui import QDialog, QApplication
from PyQt4.uic import loadUi
import ui_rc

class MyCustomDialog(QDialog):
    def __init__(self, parent=None):
        QDialog.__init__(self, parent)
        
        UI_PATH = QFile(":/ui_file/CustomDialog.ui")

        UI_PATH.open(QFile.ReadOnly)
        self.ui_form = loadUi(UI_PATH, self)
        UI_PATH.close()

def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Qts und Pythons file like objects sollten jetzt auch funktionieren.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Wenn ich folgendes probiere:

Code: Alles auswählen

from PySide import QtGui, QtCore
from pyside_uicfix import loadUi, loadUiType
import ui_rc

class TestWidget(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        UI_PATH = QtCore.QFile(":/ui_file/CustomDialog.ui")

        UI_PATH.open(QtCore.QFile.ReadOnly)
        myWidget  = loadUi(UI_PATH), self
        UI_PATH.close()

        layout = QtGui.QVBoxLayout()
        layout.addWidget(myWidget)
        self.setLayout(layout)

if __name__ == '__main__':
    app = QtGui.QApplication([])
    w = TestWidget()
    w.show()
    app.exec_()
bekomme ich folgendes (Traceback):
Traceback (most recent call last):
File "D:\Dan\Python\xarphus_project\examples\minimal_example\main_test_pyside.py", line 21, in <module>
w = TestWidget()
File "D:\Dan\Python\xarphus_project\examples\minimal_example\main_test_pyside.py", line 16, in __init__
layout.addWidget(myWidget)
TypeError: 'PySide.QtGui.QBoxLayout.addWidget' called with wrong argument types:
PySide.QtGui.QBoxLayout.addWidget(tuple)
Supported signatures:
PySide.QtGui.QBoxLayout.addWidget(PySide.QtGui.QWidget, int = 0, PySide.QtCore.Qt.Alignment = 0)
EDIT: Wenn ich das Layout komplett weglasse, wird zwar ein Fenster erzeugt, jedoch nicht aus dem Kompilat, sondern ein eigenes. Denn es fehlen die beiden Widgets (Label und pushButton) in der *.ui-Datei.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ok, ich ziehe meinen vorherigen Beitrag zurück.

Ich habe einen Flüchtigkeitsfehler in meinem Quelltext gemacht. Hier der richtige und funktionierende Quelltext:

Code: Alles auswählen

from PySide import QtGui, QtCore, QtUiTools
from pyside_uicfix import loadUi, loadUiType
import ui_rc

class TestWidget(QtGui.QDialog):
    def __init__(self, parent=None):
        QtGui.QDialog.__init__(self, parent)

        UI_PATH = QtCore.QFile(":/ui_file/CustomDialog.ui")

        UI_PATH.open(QtCore.QFile.ReadOnly)
        myWidget = loadUi(UI_PATH, self)
        UI_PATH.close()

if __name__ == '__main__':
    app = QtGui.QApplication([])
    w = TestWidget()
    w.show()
    app.exec_()
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Allerdings habe ich ein kleines Problem mit den Widgets.

Code: Alles auswählen

from PySide import QtGui, QtCore, QtUiTools
from pyside_uicfix import loadUi, loadUiType
import ui_rc

class TestWidget(QtGui.QDialog):
    def __init__(self, parent=None):
        QtGui.QDialog.__init__(self, parent)

        UI_PATH = QtCore.QFile(":/ui_file/CustomDialog.ui")

        UI_PATH.open(QtCore.QFile.ReadOnly)
        self.myWidget = loadUi(UI_PATH, self)
        UI_PATH.close()

        self.myWidget.pushButton.clicked.connect(self.close)

if __name__ == '__main__':
    app = QtGui.QApplication([])
    w = TestWidget()
    w.show()
    app.exec_()
Sobald ich versuche, über die self.myWidget-Referenz auf den pushButton zuzugreifen bekomme ich die Meldung, dass das TestWidget-Objekt das Attribut pushButton nicht besitzt. Immerhin bin ich so bei PyQt4 vorgegangen um auf die UI befindlichen Widgets zuzugreifen.
Traceback (most recent call last):
File "D:\Dan\Python\xarphus_project\examples\minimal_example\main_test_pyside.py", line 19, in <module>
w = TestWidget()
File "D:\Dan\Python\xarphus_project\examples\minimal_example\main_test_pyside.py", line 15, in __init__
self.myWidget.pushButton.clicked.connect(self.close)
AttributeError: 'TestWidget' object has no attribute 'pushButton'
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Ich hatte gehofft, um den Basisklassen-Hack herum zu kommen. Jetzt ist er wieder drin, damit sollten die Formklassen-Member sichtbar sein.

Edit: Funktioniert allerdings nicht mit Qt-Basisklassen, hmm. Muss ich nochmal investigieren...
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hab das Problem mittels Übertragung der Methoden von der Formularklasse auf das aktuelle Widgetobjekt lösen können. Damit funktioniert es auch mit Objekten der Qt-Klassen direkt ala `loadUi('/path/to/uifile', QWidget())`.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Abseits von jerch habe ich mir erlaubt ein paar Tests zu machen. Bei PySide kann man die Veröffentlichungen der letzten Versionen einsehen und herunterladen. Ich setzte meine Tests bei PySide 1.2.0 an. Bei jedem Wechsel der Versionen wurden die vorherigen Versionen ordnungsgemäß deinstalliert.

Auf meiner Maschine ist derzeit Qt 4.8.6 und Python 2.7.10

Mit diesem Quelltext habe ich den Test gestartet:

Code: Alles auswählen

import sys
from PySide import QtCore, QtGui, QtUiTools
import ui_rc

class MyCustomDialog(QtGui.QDialog):
    def __init__(self, parent=None):
        QtGui.QDialog.__init__(self, parent)

        loader = QtUiTools.QUiLoader()
        file = QtCore.QFile(":/ui_file/CustomDialog.ui")
        file.open(QtCore.QFile.ReadOnly)
        myWidget = loader.load(file, self)
        file.close()

        layout = QtGui.QVBoxLayout()
        layout.addWidget(myWidget)
        self.setLayout(layout)

def main():
    app = QtGui.QApplication(sys.argv)
    window = MyCustomDialog()
    window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
PySide 1.2.0:
- Python-Interpreter hängt sich nicht auf, jedoch wird das Kompilat nicht geladen. PySide kreiert sein eigenes Fenster.

PySide 1.2.1:
-Python-Interpreter hängt sich auf.

PySide 1.2.2:
- Python-Interpreter hängt sich auf

Python 1.2.3:
- konnte nicht gefunden werden

Python 1.2.4:
- Python-Interpreter hängt sich auf
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Ich kann nun den pushButton ansprechen. Es klappt. Ich werde mein Projekt zunächst separat auf PySide umsatteln und mal schauen, ob es Probleme geben wird. An dieser Stelle ein dickes Dankeschön.

P.S. Vielleicht könntest du das der PySide-Community mitteilen und denen deine Arbeit vorlegen? Oder ist es mal wieder nur ein spezielles Sophus-Problem?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Eine weitere Frage bezüglich deines Patches. Ist dein Patch Version-Abhängig? Das heißt, sollte ich mich eines Tages (so schnell wird es nicht passieren) dazu entscheiden von Python 2.7.10 auf Python 3 zu wechseln und von Qt4 auf Qt5, wäre dein Patch dann immer noch kompatible?
Antworten