QT-Anwendungssoftware: Best Practices?
-
Hi,
ich bin wieder an einem Punkt angekommen, an dem ich merke, dass mein immer größer werdendes Projekt eine noch elegantere Struktur gebrauchen könnte. Es gibt ein paar "Standardprobleme", die ich in vielen OpenSource-Anwendungs-Softwares, welche QT nutzen, nicht gelöst sehe, weil die einfach nicht benutzerfreundlich genug sind (viel im Bereich undo/redo beispielsweise). Zusätzlich kann man ja nun viele einzelne Widgets erstellen, welche Daten darstellen, trotzdem bleibt interessant, wie man das Projekt sauber organisiert, um die entstanden Views/Models/Delegates auf einer nächst-höheren Ebene zu verwalten. Man könnte beispielsweise mehrere übergeordnete Widgets haben, welche einige Views vereinen. Und die Organisation aller Models könnte man vermutlich auch an einer Stelle bündeln... oder wiederum nach Kontext an mehreren Stellen, doch dazu findet man in kleinen QT-Projekten nicht viel.
Daher bräuchte ich mal Einblick in ein paar sauber programmierte OSS-QT-Projekte, um zu sehen, wie die das aufziehen. Kennt ihr welche?
Gruß
Eisflamme
-
Nicht wirklich... Ich vermute, dass KDE halbwegs sauber ist, aber ich blick da nicht wirklich durch. Ansonsten gefallen mir die meisten Projekte, über die ich stolpere nicht. Wenn ich nach bestimmten Klassen google, in der Hoffnung paar Implementierungen zu finden, in denen sie verwendet werden, finde ich öfter mal Projekte wie Scribus, aber gerade Scribus Code gefällt mir auf den ersten Blick überhaupt nicht. FreeCad benutzt auch Qt, aber das ist irgendwie zu modular, lauter unabhängige Module, die über viel Python Code verbunden werden, da wirst du wahrscheinlich auch nicht finden, was du suchst.
Ansonsten versteh ich erstmal auch nicht, was du genau meinst. Eigentlich seh ich die ganzen Bestandteile wie Models und Delegates als unabhängige Komponenten, die man nicht verwalten muss. Wo man sie braucht, baut man sie halt ein. Grad sowas wie Delegates implementiere ich öfter mal direkt in einer cpp und geb sie im Header gar nicht an, Implementierungsdetails.
Aber unsere Software ist auch nicht "sauber", ist in über 20 Jahren von 100 verschiedenen Leuten geschrieben worden, ursprünglich natürlich auch nicht mit Qt. Ist als Gesamtpaket jetzt nicht gerade das Paradebeispiel für eine durchdachte Architektur. Und wir haben auch nichts so zentrales, das sich durch die gesamte Software durchziehen würde. Gibt z.B. sicher paar Stellen, wo es undo&redo gibt, aber wenn man das im Gesamtkontext betrachtet, sind das alles auch wieder nur irgendwelche Randprojekte. Und genauso alle Widgets und Models usw...
-
Ja, Delegates verwendet man einfach, die machen mir keinen Ärger, habe ich auch stets nur in der cpp.
Aber zwei Widgets, welche beide dasselbe Model aber unterschiedliche Views nutzen... dann stellt sich schon die Frage, wer die Hoheit über das Model hat. Niemand, also lasse ich das Model lieber zentral verwalten. Die Models beeinflussen sich auch noch gegenseitig, das wird mitunter recht kompliziert, benötigt also auch eine eigene Verwaltungseinheit.
Und wenn undo/redo kommt, muss sichergestellt werden, dass alle Models benachrichtigt werden, somit sollte so etwas auch zentral geschehen. Zudem gibt es UI-Teile des Mementos und Model-Teile des Mementos. Das Model sollte nichts davon wissen, wohin ich im Tree gescrollt habe. Und der Tree (View) sollte sich nicht um die Daten scheren. Also brauche ich zwei verbundene Mementos... wirkt unschön, aber mir fällt da nichts Sauberes ein.
Das sind nur einige Beispiele. Drag&Drop ist zum Beispiel ebenfalls schlecht implementiert in QT. Auf welches signal soll ich connecten, um mein SaveState (Memento) zu triggern? Da passt keines so wirklich, also muss ich mir wieder etwas drum herum basteln. Ich frage mich, ob jemand Drag&Drop sauber mit undo/redo implementiert hat, meiner Meinung nach geht das nicht ohne (in meinen Augen unschöne) Tricks, Workarounds oder Boilerplate-Code. Aber vielleicht hat ja jemand doch rausgefunden, ob es geht.
Ein richtig benutzerfreundliches QT-Großprojekt mit sauberen Code würde mir hier gewaltig helfen...
-
Ich denke, vieles davon sind Details... Zumindest habe ich immer sehr viele Detailprobleme, wenn ich was in Qt machen will, was über die einfachen 0815 Sachen hinausgeht. Teilweise finde ich dann schon in allen möglichen Open Source Projekten Lösungen oder Anregungen, aber nicht zu Architekturfragen.
Zumindest für mich wars immer eine brauchbare Lösung, die Abhängigkeiten in übergeordneten Widgets zu verwalten. Nichts davon ist bei uns wie gesagt zentral. So ein Widget kann schon sehr komplex sein und evtl. steckt sehr viel Code dahinter, aber sowas ist meist ein Fenster, das aufgeht, wenn man irgendwo draufklickt. Hat mit dem Rest nichts zu tun. Und dann gibts halt meist ein zentrales Widget, das das Fenster an sich darstellt und alles verwaltet und alle Unterwidgets erstellt und denen die Daten wie Models und Handler weitergibt.
Wenn du ein "zentrales" Model hast, dann kann man das evtl. auch vom MainWindow verwalten lassen.Bei Mementos würde ich nicht in Model und View Teile aufteilen. Ich würde sagen, das gehört eher in die View Schicht (und das darf die Daten notfalls kennen). Und dem Verbinden von Signalen sollte es ja auch sauber funktionieren. Du hast ein Model, das schon irgendwo verwendet wird. Du machst ein neues Widget, das bekommt dieses Model rein und verbindet die ganzen Signale. In dem Widget wird eine GUI Aktion ausgeführt, die verändert die Daten im Model, es kommt ein dataChanged und die anderen Widgets können aktualisiert werden. Dann machst du die Aktion in deinem Widget rückgängig, die alten Daten werden in das Model geschrieben, wieder ein dataChanged. Das Widget wird dann wieder zestört und die zusätzlichen Signale werden disconnected. Soweit seh ich erstmal kein Problem.
Drag&Drop ist schon ok implementiert, das spigelt in etwa die drunterliegenden Implementierungen wieder. Wenn du direkt mit der Windows API arbeiten würdest, würde der Workflow so ähnlich ausschauen. Und anders kriegst es zumindest unter Windows nicht hin (wie das unter X11 ausschaut weiß ich nicht, aber ich vermute so ähnlich). Wann du deinen SaveState triggerst, kommt drauf an, was du genau machen und rückgängig machen willst. Normalerweise werden die Daten erst im Drop Event verändert. Davor kann man die Operation mit Escape abbrechen, dann ist nichts passiert.
-
Hi Mechanics,
ja, du hast schon recht, die größten Zeitfresser bei QT sind in der Tat die Details. Das ist in meinen Augen eine Folge von ein paar Denkfehlern bzw. von einem fehlenden Durchdenken und teilweise auch von der Architektur (Model enthält Darstellinformationen?!), größeres Beispiel dazu kommt gleich unten.
Aufpoppende Fenster vom Hauptfenster ausgelöst erhalten Views, genau, das ist bei mir ja auch so. Bei mir ist es auch so, dass das Hauptfenster im Grunde die Models verwaltet, nur sammle ich alle Models eben in einer Extraklasse. Denn auch hier möchte ich strikt die Darstellung von der Logik trennen. Mein Model könnte ich z.B. auch für mobile Apps weiterverwenden, ein künftig realistischer Anwendungsfall. Die Views sind mit Sicherheit nur für Desktop geeignet. Na ja, vermutlich ist QT selbst nicht so sauber designed, dass ich die Models einfach für mobile Apps nutzen könnte und daher ist das Konzept möglicherweise sinnlos, aber es fühlt sich dennoch sauberer an.
Bei Mementos würde ich nicht in Model und View Teile aufteilen. Ich würde sagen, das gehört eher in die View Schicht (und das darf die Daten notfalls kennen)
Das empfinde ich nicht so, View sollte in meinen Augen, was Daten-Logik anbetrifft, möglichst schlank sein und sich auf die Darstellung fokussieren... ein View erhält vom Model die Daten und stellt sie dar und liefert Klicks, Selektionen usw. wieder zurück ans Model. Ein undo sollte im Model etwas ändern, das wiederum (automatisch) in den View übertragen wird.
In meinem Programm habe ich sogar eine reine Logik-Schicht, dort sind Klassen und einige Konzepte enthalten, mit denen ich die Software auch ganz ohne QT laufen lassen könnte. Viel ergibt sich natürlich erst durch die Oberfläche und dann kommen die QT-Models- und -Views ins Spiel. Aber bereits in der Konsole könnte ich undo/redo sinnvoll verwenden, daher gibt es diese Konzepte auch bereits dort. Das dazugehörige undo/redo-Model in QT dient nur als Adapter und die Views triggern hier nur... einziges Problem sind dann eben ui-bezogene Eigenschaften, die nicht in die Logik-Schicht gehören. Daher hat bei mir die History für jeden Snapshot/State/Memento eine ID und in der View-Schicht werden UI-Infos (Scrollposition, selektiertes Element im Baum) dagegen gemappt. Das ist nicht besonders schön, aber meine Logik-Schicht klappt nach wie vor in der Konsole oder für Nicht-QT-Applikationen. Mich würde nicht stören, wenn View die Daten der Models kennt (Views dürfen Models ja grundsätzlich kennen), jedoch will ich undo/redo eben auch ohne UI haben. Genau das hätte ich gerne mit einer "best practice" gelöst.
Und dem Verbinden von Signalen sollte es ja auch sauber funktionieren.
Ja, das ist kein Problem, wie in deinem Absatz beschrieben funktioniert das in der Tat bestens.
Drag&Drop ist schon ok implementiert, das spigelt in etwa die drunterliegenden Implementierungen wieder.
Trotzdem gibt es ein paar Designfehler im Detail, die mich zur Weißglut bringen.
Ich habe zwei Bäume. Wenn ich von Baum A in Baum A etwas d&d, soll es verschoben werden. Wenn ich von Baum A nach Baum B etwas d&d, soll es kopiert werden. Das ist in meinen Augen relativ intuitiv, zumindest aber ein denkbarer Use-Case. Das in QT umzusetzen war ein unglaublicher Aufwand. Man muss dragEnterEvent, dragMoveEvent und dropEvent HandRangeTreeView überladen, die Event-Source prüfen und dann die Aktion umbauen... das könnte eine Stelle sein, die Fallunterscheidung bei den drein ist in meinen Augen ein Spezialfall. Das ist jedoch natürlich erstmal nicht der wirkliche Aufwand dabei gewesen...
Ja, QT bietet InternalMove an, was genau das macht, was ich möchte. Problem: dataDropped wird emittiert, BEVOR die Zeilen, die mich interessieren gelöscht werden. Wenn ich also auf dataDropped connecte, um an die Stelle zu scrollen und das Element zu selektieren (das passiert nämlich nicht automatisch, weil Selektion kein Teil vom Model, sondern vom SelectionModel ist, was zum TreeView gehört und die nicht richtig miteinander kommunizieren), dann lande ich an einer falschen Stelle. Wieso? Weil zum Löschen beim Verschieben natürlich beginRemoveRows genutzt wird und das Element, was verschoben werden soll, selektiert ist, daher ändert der die Selektion. Stellt sich die Frage, wieso nach einem Verschieben noch das alte Element selektiert ist... weil das vorangegangene insert eben nichts selektiert.
Und das ist eben die Sache: Es ist absolut notwendig, dass beim Verschieben eines Elements in den Ordner an anderer Stelle im Tree das Element dann auch selektiert wird und dorthin gescrollt werden muss (was übrigens bei korrekter Kenntnis des QModelIndex auch mit scrollTo nicht klappt, weil's buggy ist, wenn es während einer D&D-Operation eingesetzt wird). Wenn der Ordner 30 Elemente hat, landet es sonst unten und man sieht es nicht.
Schlimmer noch: bei undo/redo benötige ich das aktuell selektierte Item ja auch. Ich gehe sonst auf zurück/vorwärts und sehe gar nicht, was sich geändert hat. Visuelles Feedback für den Nutzer? Nö. Das hatte ich übrigens zunächst nicht drin, weil ich es für sinnlos hielt. Dann gab es viel Nutzerfeedback zu exakt dem Punkt. Daraufhin habe ich es selbst mal ausführlich ausprobiert und stellte wirklich fest: Es nervt unglaublich, wenn das nicht klappt.
Daher muss ich die Move-Geschichte nachbauen, selbst Signals emittieren, bei denen ich den richtigen Index habe, um das wiederum im Memento richtig zu speichern... hätten die QT-Mitarbeiter mal komplett Drag&Drop mit undo/redo durchgedacht, hätten die schon längst gemerkt, dass das Konzept so nicht aufgeht. undo/redo ist Standard in einer benutzerfreundlichen End-Benutzer-Software, ja, auch mit Displayinfos, nicht nur Daten. Von QT5.4 zu QT5.5 wurden noch Bugs in Drag&Drop behoben (canDropMimeData wurde nicht aufgerufen), das zeigt ja, dass darauf kein Wert gelegt wurde... aber Hauptsache QtQuick weiterentwickeln, was für größere Applikationen ohnehin noch lange nicht ausgereift genug ist.
Na ja, du sagst es ja selbst, der Teufel liegt im Detail... passiert mir aber leider häufig und dann muss ich große Teile von QT umschiffen oder nachbasteln. Mit x11 oder Windows dahinter hat das nichts zu tun, man kann das, was man nicht selbst anfassen möchte, doch ganz gut webabstrahieren.
Jetzt bin ich abgeschweift... ich gehe mal davon aus, dass andere Leute auch schon Shortcuts einbauen wollten, ein intuitives undo/redo nutzen wollten und dabei den Anreiz hatten das mit einem sauberen MVC umzusetzen, das aber auch zulässt, dass man die logische Basis auch für andere Plattformen (z.B. auch Smartphone) nutzen können sollte. Oder es gibt fast nur unintuitive, mittelmäßig benutzerfreundliche Software, bei denen die Details alle nicht fein abgestimmt sind... oder die verstecken sich. Wenn es ein paar solcher best practices gäbe, wäre ich jedenfalls sehr froh.
-
Eisflamme schrieb:
Und das ist eben die Sache: Es ist absolut notwendig, dass beim Verschieben eines Elements in den Ordner an anderer Stelle im Tree das Element dann auch selektiert wird und dorthin gescrollt werden muss (was übrigens bei korrekter Kenntnis des QModelIndex auch mit scrollTo nicht klappt, weil's buggy ist, wenn es während einer D&D-Operation eingesetzt wird).
Evtl. in einer Queued Connection ausführen.
Aber das geht auch zu sehr ins Detail... Ich kann zu den konkreten Problemen jetzt konkret auch nicht viel sagen, weil wir sowas z.B. nicht gebraucht haben. Ich seh das aber immer noch hauptsächlich als Detail Probleme. Könnte mir auch vorstellen, dass ich sowas wie "Undo/Redo einer Verschiebeoperation selektiert das Element in der Baumansicht nicht" als Ticket bekommen könnte (jetzt nicht genau das, aber ich krieg dutzende ähnlicher Tickets). Das ist dann halt etwas, was man austüfteln muss, sehe ich nicht unbedingt als zentrales Architekturproblem. Eher ein Problem in der Qt.
Mit "GUI" meinte ich bei Undo/Redo nicht unbedingt die GUI an sich, sondern irgendeine Schicht über dem Modell, die die Aktion steuert. Also z.B. GUI oder von mir aus eine Scripting API. Und das Selektieren der entsprechenden Elemente könnte vielleicht automatisch erfolgen. Wenn ein gelöschtes Element wiederhergestellt wird, dann soll das auch selektiert werden. Das ist aber natürlich zu pauschal, muss man sich auch immer im Detail anschauen.
Vielleicht kannst du dir den Qt Creator selber anschauen? Aber ich bezweifle, dass du da die Antwort auf deine Fragen finden wirst...
-
Bzw., ich muss noch sagen, dass Undo & Redo tatsächlich nicht so einfach sauber zu implementieren sind, auch ohne Qt. Ist mir vor nicht all zu langer Zeit bei einem privaten Projekt aufgefallen, ohne Qt. Das Problem sind vor allem Abhängigkeiten in dem "Modell". Wenn ich ein Element lösche und es verweist auf andere Elemente und umgekehrt, muss man diese ganzen Beziehungen (und es kann ja unterschiedliche Arten von Beziehungen geben) in den State Objekten verwalten und damit wissen die schon sehr viel über die interne Logik. Ist schon vertretbar, aber andererseits auch nicht wirklich schön, weil man da immer aufpassen und die Änderungen durchziehen muss. Und dann gabs noch Objekte in einer drüber liegenden Schicht, die ebenfalls diese Elemente referenziert haben, und wenn ein Element gelöscht wird, dann muss auch das Objekt in der drüberliegenden Schicht gelöscht werden. Also, DOM-Elemente in der Logikschicht und Renderer in der GUI Schicht. Habe ich dann über Signale (nicht Qt Signale) gelöst, die Schicht drüber hat darauf reagiert und die Renderer auch gelöscht. Dann gibts aber Probleme mit dem Wiederherstellen, weil man die Renderer nicht einfach erstellen kann. Also habe ich die nicht wirklich gelöscht, sondern erstmal in eine andere Liste verschoben. Da das ein privates Projekt ist, hat mich das auch nicht so gestört, aber elegant ist das alles noch nicht.
-
Qt Undo Framework, falls nicht schon bekannt.
http://doc.qt.io/qt-5/qundo.html
Habe ich selbst noch nicht benutzt.
-
Softwaremaker:
Ja, kenne ich, nutze ich aber nicht, weil es in meinem Fall mehr Sinn macht einen gesamten Snapshot zu speichern. Tut auch weder im Detail noch in der Architektur wirklich was zur Sache. Dennoch danke.Mechanics:
Ja, ich bin jetzt von der Architektur etwas abgedriftet... Ich finde auch, QT hat einfach einige Probleme.Zu deiner undo/redo-Beschreibung: Ja, die Probleme kenne ich. Meine Architektur war in meinen Augen sauber und schön, dann kam undo/redo dazu und es wurde schlagartig ekliger.
Ich speichere bei mir nicht die Änderungen, sondern einfach den gesamten Space als Snapshot. Da orientiere ich mich einfach ein wenig an Git, unveränderte Objekte kann man ja einfach referenzieren statt nochmals neu zu speichern.
Verweise muss man dann neu setzen, das ist klar. Wie man das am geschicktesten macht, hängt wohl wieder vom Aufbau der Anwendung ab... alles view-unspezifische ist bei mir eben zentral an einem MainModelController aufgehängt (so heißt der bei mir). Und der muss natürlich die Hoheit über undo/redo haben, dann kann er alle informieren, die sich interessieren, und gleichzeitig alle Informationen aufsaugen, die irgendjemand für ihn haben könnte. Streng genommen habe ich sogar zwei unterschiedliche Historys. Damit wäre ich halbwegs zufrieden, nur eben den View-Teil von undo/redo, den mag ich noch nicht so ganz... aber noch fällt mir auch nichts Besseres ein, daher werde ich damit wohl leben.
Gut zu hören jedenfalls, dass auch andere ihre Probleme in diesem Bereich haben.