Öffne zweite Form auch als Unterfenster (MDI)

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
C++ hat normalerweise keinen Aufräummechanismus ala garbage collector oder reference counter (schon wieder englisch, die englischen Begriffe sind in der Programmierdomäne eindeutig belegt, ins Deutsche übertragen, müsste man erst erklären, was gemeint ist). Darum haben die Qt-Leute Sicherheitsnetze eingebaut: 1. über den Objektbaum, welcher mit parent-child-Beziehungen aufgebaut wird. Darüber kann Qt die Objekte ordentlich abräumen (also in der richtigen Reihenfolge children --> parent). Werden QObjects ohne parent erstellt, werden diese spätestens von QApplication abgeräumt (2. Variante, ist u.a. der Grund, warum es ein QApplication-Objekt vor der Benutzung anderer QObjects geben muss).
Beim QWorkspace wirds schwieriger. Die Doku zu `addWindow` gibt indirekt den Hinweis, dass die Elternschaft sich ändert. Was genau passiert, sieht man erst, wenn man den Sourcecode von `addWindow` anschaut: es wird ein Proxyobjekt vom Typen QWorkspaceChild erstellt und das Subfenster dort reingehängt (Objektbaum: QWorkspace->QWorkspaceChild->Subwidget). Diese Verschleierung war offensichtlich selbst den Qt-Leuten zuviel und sie haben QWorkspace fallen gelassen und durch MdiArea ersetzt. Schaut man sich die Schnittstellen beider Klassen an, so unterscheiden sie sich hauptsächlich dadurch, dass MdiArea das vorher undurchsichtige Verhalten durch die Spezifikation von QMdiSubWindow offen legt und Benutzern Kontrolle zurück gibt.

Nun zur Frage `close`:
close macht genau nur das - es schliesst das Fenster. Kurzer Gegencheck:

Code: Alles auswählen

from PyQt4 import QtGui

def showQWidgetsChildren(qobj):
    def wrapped():
        for obj in qobj.children():
            if isinstance(obj, QtGui.QWidget):
                obj.show()
    return wrapped

app = QtGui.QApplication([])
win = QtGui.QWorkspace()
but = QtGui.QPushButton(win)
but.clicked.connect(showQWidgetsChildren(win))
win.addWindow(QtGui.QWidget())
win.show()
app.exec_()
Egal wie oft Du das Subfenster schliesst, es bleibt in QWorkspace.children() und lässt sich per Klick auf den Button wieder herstellen. Und das ist der Punkt, wo PyQt schwierig wird, weil 2 Welten der Objektbehandlung/-aufräumung aufeinander treffen und Du genau wissen solltest, wann Objekte wiederverwendet werden können/sollen oder einfach neue erstellt und zerstört werden (Lifecycle). Der klassische Pythonmechanismus (Markieren des Objektes zur Abräumung, wenn ausserhalb der Sichtbarkeit des Pythonnamens) greift hier nicht mehr automagisch, da Qt die Objektreferenzen hält. Du musst Qt explizit sagen, dass es das Subfenster zerstören soll. Das geht z.B. mit mit dem Windowsattribut Qt::WA_DeleteOnClose.

Du fragst Dich wahrscheinlich, warum das für Dich wichtig ist. Mit MDI kannst Du prinziell unendlich Fenster öffnen und schliessen. Machst Du knappe Ressourcen alter Fenster nicht frei, laufen diese voll (z.B. Speicher) oder blockieren (z.B. offene Datei im Subfenster unter Windows). Das gilt übrigens für alle QWidgets, nicht nur innerhalb QWorkspace. Daher sollte man abwägen, ob ein Fenster wiederbenutzbar ist (neue Daten in Widgets laden + show) oder das Fenster beim Schliessen zerstören und bei Bedarf neu erstellen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Besten Dank für die ausführlich Erklärung. Das muss ich erst einmal alles auf mich wirken lassen. Ich werde die Tage mein Beispiel nochmal überarbeiten, und es hier nochmal präsentieren, und hätte dazu eure Meinung nochmal.

Jedoch hätte ich eine weitere Frage: Du sagtest, ich müsse mich abwägen, ob ich ein Fenster zerstöre oder eben nicht. Ich kann ja leider das verhalten eines Anwenders nicht voraussehen. An welchen Kriterien hält man sich hierbei fest? Oder wäre es eher günstig, die Fenster grundlegend zu zerstören und neu zu erstellen?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Die Entscheidung liegt nicht beim Anwender Deines Programmes, das musst Du schon selbst festlegen.

Bei nicht modalen Fenstern tendiere ich eher zum Wiederverwenden, sofern das möglich ist. Dann stricke ich aber auch die Anwendung so, dass diese Fensterobjekte hauptfenster-weit sichtbar sind (geht prima mittels initializer lists in C++ bzw. in Python Importe nach oben und in `__init__` Exemplare erstellen). Brauche ich ein nicht modales Fenster selten bis gar nicht, wirds zur Laufzeit erst bei Anforderung kreiert und lazy gelöscht (Nullpointer im Konstruktor, nach Benutzung zeigt dieser aufs Fensterobjekt). Das betrifft aber alles Fenster mit sehr spezieller Funktionalität. Bei MDI würde ich eher Erstellen/Löschen nutzen, da die Exemplare ja prinzipiell nebeneinander extistieren können müssen.

Bei modalen Fenstern (z.B. kleine Dialoge) bleibt alles lokal und damit auch das Zwischenfenster (idR auf dem Stack), also Erstellen/Löschen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@ jerch: Wie kann ich zu Testzwecke per Print-Anweisung ausgeben lassen, um zu sehen ob das Fenster-Objekt nun wirklich nach dem Schließen gelöscht wurde?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Siehe im Bsp. oben, per children()-Methode des parent.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ich weiß schon, warum ich das Widget QWorkspace eher mag als MDiArea. Denn letzteres ist verdammt umständlich. Ich stelle beide Versionen gegenüber, einmal QWorkspace und QMdiArea. In beiden Versionen geht es um das Öffnen und Schließen eines Unterfensters.

Arbeiten mit QMdiArea:

Code: Alles auswählen


# Die Importe dienen nur zu Testzwecken. Mir ist klar, dass man nur das
# importieren soll, was man auch tatsächlich braucht
 
from PyQt4.QtGui import *
from PyQt4.QtCore import *

class Mdi_Main(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)

        self.mdiArea = QMdiArea()
        self.mdiArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.mdiArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.setCentralWidget(self.mdiArea)

        self.actionTestWindow.triggered.connect(self.createMdiChild)

    def createMdiChild(self):
        self.check_window = Check_Window(self)
        self.check_window.setAttribute(Qt.WA_DeleteOnClose)
        self.check_window.pushButtonCancel.clicked.connect(self.close_it)

        self.subwindow = QMdiSubWindow()
        self.subwindow.setWidget(self.check_window)
        self.mdiArea.addSubWindow(self.subwindow)

        self.check_window.show()
        self.subwindow.show()
        self.subwindow.widget().show()

    def close_it(self):
        self.mdiArea.removeSubWindow(self.subwindow)
Also, wenn ich mir allein für das Öffnen eines Fensters betrachte, dann muss ich mit dem Kopf schütteln. Auf diesem "Rattenschwanz" bin ich gekommen, weil in der Qt-Dokumentation folgendes steht:
Removes widget from the MDI area. The widget must be either a QMdiSubWindow or a widget that is the internal widget of a subwindow. Note widget is never actually deleted by QMdiArea. If a QMdiSubWindow is passed in its parent is set to 0 and it is removed, but if an internal widget is passed in the child widget is set to 0 but the QMdiSubWindow is not removed.
Das heißt, ich brauche eine QMdiSubWindow -Klasse, damit ich am Ende das Fenster auch schließen kann.

Jetzt schaue ich mir mal QWorkspace an:
Arbeiten mit QWorkspace :

Code: Alles auswählen


# Die Importe dienen nur zu Testzwecken. Mir ist klar, dass man nur das
# importieren soll, was man auch tatsächlich braucht
 
from PyQt4.QtGui import *
from PyQt4.QtCore import *

class Mdi_Main(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)

        self.workspace = QWorkspace()
        self.workspace.setScrollBarsEnabled(True)
        self.setCentralWidget(self.workspace)

        self.actionTestWindow.triggered.connect(self.createMdiChild)

    def createMdiChild(self):
        self.check_window = Check_Window(self)
        self.check_window.setAttribute(Qt.WA_DeleteOnClose)
        self.check_window.pushButtonCancel.clicked.connect(self.close_it)

        self.workspace.addWindow(self.check_window)
        self.check_window.show()

    def close_it(self):
        self.check_window.close()
Mein persönliches Fazit:
Wenn ich mir die beiden Möglichkeiten anschaue, einmal das Arbeiten mit MdiArea und dann mit QWorkspace, dann fällt doch deutlich auf, dass im QWorkspace erheblich weniger Code nötig ist, allein für das Öffnen des Unterfenster. Mal davon abgesehen, dass auf in Qt-Dokumentation ausdrücklich behauptet wird, dass diese Klasse angeblich veraltet sei, frage ich mich, wieso ich auf MdiArea umsteigen sollte, wenn es doch so umständlich ist? Dagegen ist das Arbeiten mit QWorkspace wesentlich angenehmer. Oder aber ich habe im Code was gewaltig falsch gemacht. Ich will ja auch keine Grundsatz Diskussion starten, aber ich sitze hier und rolle mit den Augen und frage mich, wieso das so umständlich sein muss? Und allgemein, welchen Nachteil hätte ich, wenn ich QWorkspace weiterhin benutze? Ich meine, die Klasse funktioniert ja bisher anstandslos. Mal abgesehen davon, dass mit dem Objektbaum innerhalb des QWorkspace-Widgets. Vielleicht mag mir da wer mehr Licht ins Dunkle bringen, außer mit der Aussage "Aber ind er Qt-Dokumentation steht doch, dass ist veraltet....". Lesen kann ich ja selbst :-)
BlackJack

@Sophus: Diese Diskussion fängst Du jetzt im Forum zum dritten mal an. :roll:

Benutz doch `QWorkspace`, ignoriere das man das nicht mehr benutzen soll, freu Dich das Du kein guter Soldat bist der alles ungefragt hinnimmt (hast Du mir in einer der letzten Diskussionen über das Thema vorgeworfen), sondern ein individueller Freigeist der gerne gegen den Strom schwimmt. Ganz tolle Idee beim Programmieren…

Oder ich versuch mal wieder meine Vermutung das Du trollst. Denn das ist wohl die umständlichste Art in der man das MDI-Fenster erstellen kann und es enthält auch offensichtlich redundanten Code. Ich würde auch weder den Abbruch-Button an dieser Stelle verbinden, noch `close_it()` überhaupt implementieren. Und auch das alles nicht an `self` binden denn das funktioniert ja ganz offensichtlich so nicht korrekt wenn man mehr als ein MDI-Fenster erstellt. Das hier sollte alles sein was man braucht:

Code: Alles auswählen

    def createMdiChild(self):
        self.mdiArea.addSubWindow(CheckWindow(), Qt.WA_DeleteOnClose)
Wahnsinnig kompliziert…
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: So einfach ist es eben nicht. Versuch mal über die Schließ-Schaltfläche das Unterfenster zu schließen. Nur die Widgets auf der Form verschinden. Erst durch meinen "umständlichen" Code bin ich in der Lage das Fenster zu schließen. Ich weiß, dass viele von euch auf einem Unterfenster keine Schaltfläche sehen wollt, die das Fenster schließt. Und nein, ich trolle nicht. Keine Ahnung wie du auf diese Vermutung kommst. Ich habe eher gehofft, dass man mich mehr aufklären könnte, als die bloße Tatsache, dass das QWorkspace veraltet sei. Und wenn mein Code so drollig aussieht, liegt es daran, das ich kein Profi bin, sondern ein richtiger, blutiger Anfänger.
BlackJack

@Sophus: Kleine Korrektur:

Code: Alles auswählen

    def new_child_window(self):
        self.mdi_area.addSubWindow(ChildWindow()).show()
(Das `Qt.WA_DeleteOnClose` setze ich in der `ChildWindow.__init__()`). Wenn nur die Widgets innerhalb des MDI-Fensters verschwinden, dann hast Du wohl auch nur das innere Widget geschlossen und nicht das Fenster. *Das* hat jetzt aber nicht wirklich etwas damit zu tun wie man das Kindfenster erzeugt, sondern was die Schaltfläche an Aktionen auslöst.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Du meinst, die removeSubWindow-Methode ist die falsche Wahl? Also in diesem Kontext? Mit der bloßen close()-Methode komme ich ebenso nicht weiter.
BlackJack

@Sophus: Man muss halt `close()` auf dem richtigen Objekt aufrufen. Das Thema hatten wir aber schon mal. Du musst Dir klar machen welche Objekte es dort gibt, wie der Objektbaum aussieht, und wie man den navigiert.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Nur aus reine Neugier. Was hat mein Weg für einen Nachteil. Du hast ja gesehen, dass ich über Umstände mit der QMdiSubWindow-Klasse gearbeitet habe. Mal davon abgesehen, dass es umständlich ist, hätte ich gerne gewusst, worin in meinem Weg die Grenze liegen. Oder kann man unter Umständen meinen Weg auch als "Alternative" betrachten? So nach dem Motto "Es führen viele Wege nach Rom".
BlackJack

@Sophus: Ich hatte ja schon geschrieben dass das so nicht funktioniert sobald man mehr als ein Fenster öffnet. Und ein MDI-Bereich mit nur einem Fenster macht nicht viel Sinn.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Du kannst es halten wie ein Dachdecker. Viele Wege führen nach Rom, fast nichts ist alternativlos. Hoffnungslos dagegen ist weitaus häufiger.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Zu diesem Thema fand ich diese Seite:

https://qt.gitorious.org/pyside/pyside- ... y#LNaN-NaN

Ich werde sie mal genauer anschauen müssen.
BlackJack

@Sophus: Das ist das Beispiel aus der Qt-Dokumentation in Python umgesetzt. Da kommt Dein Problem nicht drin vor. Du willst ja unbedingt eine Schaltfläche *in* dem MDI-Fenster zum Schliessen haben. Das Beispiel verwendet den üblichen Weg die MDI-Fenster über das entsprechende Icon in der Fensterleiste zu schliessen oder das aktive MDI-Fenster über ein Menü/eine Aktion.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Wenn du MDI-Fenster sagst, dann meinst du sicherlich das Unterfenster, also das Kind-Fenster, richtig? Und ja, ich will da unbedingt eine Schaltfläche drin haben. :-) So ungewöhnlich ist es ja nicht, dass man eine Schaltfläche zum Schließen direkt auf dem Unterfenster haben will :-) Und das Beispiel beschreibt zwar nicht mein Problem, jedoch hat es weitere interessante Ansätze.
Antworten