Seite 1 von 2

QAbstractSlider u.a.: wie Loops vermeiden?

Verfasst: 26. Mai 2009 15:40
von olebole
Hallo,

ich habe folgendes Problem: in meinem Programm gibt es verschiedene Möglichkeiten, eine Darstellung zu zoomen:

* ComboBox
* QSlider

die natürlich auch immer den aktuellen Zustand anzeigen sollen.
Die entsprechenden Signale (valueChanged(int) beim QSlider) sind verbunden mit Funktionen, die das andere Inputwidget updaten und dann den Zoom ausführen.

Das Problem hier ist: Wenn ich z.B. in der ComboBox einen Wert eintrage, dann wird das mittels Signal an diese Funktion übertragen, und diese passt den QSlider an. Das führt dann aber dazu, dass der QSlider ein valueChanged()-Signal emittiert und somit die damit verbundene Funktion ausführt. Diese trägt einen neuen Wert in die ComboBox ein (neu, weil er sich ja wegen der internen Darstellung als "int" im QSlider nicht exakt speicherbar war) -- was einerseits dazu führt, dass da ein anderer Wert drinsteht, als ursprünglich ausgewählt. Andererseits emittiert jetzt die QComboBox wieder ein "changed"-Signal, sodass wir hier eine potentielle Endlosschleife erhalten.

Irgendwie muss sieht das doch nach einem prinzipiellen Mißverständnis von Qt bei mir aus? Welchen Denkfehler habe ich hier? Wie kann ich das gesagte vermeiden?

Viele Grüße

olebole

Verfasst: 26. Mai 2009 18:53
von upsala
Falsches Signal für deinen Einsatz, du suchst sliderMoved(int)

Verfasst: 27. Mai 2009 08:48
von AuE
alternativ kannst du auch signale blockieren!

Verfasst: 27. Mai 2009 09:03
von olebole
upsala hat geschrieben:Falsches Signal für deinen Einsatz, du suchst sliderMoved(int)
Schön wär's, aber das geht ebenfalls nicht: das sieht zwar sehr schön aus, wenn man den Slider direkt verschiebt, aber sobald man mit den Slider mittels Scrollrad verändert, verschiebt sich zwar der Slider, aber es kommt kein Event an.
Auch wenn man die Position des Sliders durch Klicken links bzw. rechts davon verändere, kommt kein Event an.

Soo einfach ist das eben nicht.
AuE hat geschrieben:alternativ kannst du auch signale blockieren!
Wie mache ich das möglichst robust?
Ein Problem ist ja, dass beim schnellen Verschieben möglicherweise mehrere Signale hintereinander kommen: während das erste valueChanged() noch verarbeitet wird, trudelt bereits das nächste ein. Die kann man nicht einfach ignorieren, weil dann eventuell die Endposition des Sliders nicht korrekt gesetzt wird.

Ergänzung: Komplizierter wird das noch dadurch, dass ich gerne um das Zentrum zoomen will. Das heißt, dass ich bei einem eingeblendetem Scrollbar während des Zoomens auch noch die Größe und die Position (!) des Scrollbars anpassen muss. Das führt zusätzlich zu (völlig unnötigen) Scroll-Events, die gemeinsam mit den Zoomevents die Anwendung recht zähflüssig machen.

Irgendwie habe ich das Gefühl, dass ich hier einen Designfehler habe. Aber wie löst man derartiges "richtig", also mit der Verarbeitung nur der nötigen Events? Ich bin doch sicher nicht der einzige, der das Problem hat, dass die programmseitige Anpassung von UI-Elementen zu unerwünschten Signalen führt?

Verfasst: 27. Mai 2009 13:29
von der.grisu
olebole hat geschrieben: Irgendwie habe ich das Gefühl, dass ich hier einen Designfehler habe. Aber wie löst man derartiges "richtig", also mit der Verarbeitung nur der nötigen Events? Ich bin doch sicher nicht der einzige, der das Problem hat, dass die programmseitige Anpassung von UI-Elementen zu unerwünschten Signalen führt?
Du hast recht, ich habe/hatte das gleiche Problem. Scrollen soll mittels Mausrad möglich sein, ebenso wie die direkte Betätigung des Sliders.
In dem einen Fall habe ich das so gelöst (bitte um allf. Nachsicht, ich rekonstruiere das jetzt aus dem Gedächtnis):

Zuerst das Mausrad einfangen:

Code: Alles auswählen

void wheelEvent(...)
{
   ...
   /* bool */ ignoreZoomValueChanged = true;
   slider->setValue(newValue);
   ...
}
und beim Abfangen des valueChanged-Signals:

Code: Alles auswählen

if (ignoreZoomValueChanged) {
   ignoreZoomValueChanged = false;
   return;
}
Ist sicherlich auch nicht der Weisheit letzter Schluss, aber wenn jemand etwas Besseres weiß (ich glaube auch, dass das vom Design her noch eleganter geht), wäre ich auch sehr dankbar.

lg,
Christian

Verfasst: 27. Mai 2009 15:42
von olebole
der.grisu hat geschrieben:Zuerst das Mausrad einfangen: [...] und beim Abfangen des valueChanged-Signals:
Naja, das Problem hier ist, dass das Mausrad schneller Events produzieren kann, als sie vom Slider verarbeitet werden, dass man also zuerst zwei Events vom Mausrad hat, und erst danach die beiden verarbeiteten Events vom Slider ankommen. Dann wird zwar das erste verschluckt, das zweite aber schon nicht mehr.
Genauso kann es sein, dass der Slider eben doch kein Event produziert (weil es keine Veränderung im integer-Wert gab). Dann bleibt der Zustand auf "ignoreZoomValueChanged = true;" und das nächste Slider-Event wird verschluckt.

Hat keiner eine "saubere" Lösung dafür?

Verfasst: 27. Mai 2009 16:58
von upsala
Ok, anderer Ansatz:

Alle Signale auf einen Slot zusammenführen.
Anhang vom sender() ermitteln wer gesendet hat und dessen Wert auslesen.
An alle diesen Wert verteilen, die einen anderen Wert haben.

Verfasst: 27. Mai 2009 18:02
von olebole
upsala hat geschrieben:Alle Signale auf einen Slot zusammenführen.
Anhang vom sender() ermitteln wer gesendet hat
Blöde Frage: wie mache ich das?
dessen Wert auslesen. An alle diesen Wert verteilen, die einen anderen Wert haben.
Wirklich helfen wird das wohl nicht, da ja die Scrollbars wirklich einen etwas anderen Wert haben (ansonsten würden sie ja keine valueChanged()-Events aussenden, wenn man per setValue() einen setzt).

Verfasst: 27. Mai 2009 19:30
von upsala

Verfasst: 27. Mai 2009 20:48
von olebole
upsala hat geschrieben:QObject::sender()
Danke. Aber, wie gesagt, das hilft nicht recht weiter. Denn dem neuen Signal vom Slider ist ja nicht anzusehen, dass es aufgrund eines alten (z.B. vom Mausrad) erzeugt wurde.

Verfasst: 3. Juni 2009 13:40
von olebole
Hat hier wirklich niemand eine Idee?

Das Problem wird inzwischen noch dadurch verschärft, dass Panning unterstützt werden soll, d.h. dass man durch Draggen mit der Maus direkt in die "Content Area" verschieben (scrollen) möchte.

d.h. fürs Scrolling sollten folgende Möglichkeiten vorhanden sein:
  • Scrolling per Scrollbar mit allen Möglichkeiten: direktes Verschieben, Tastatur, Klick links bzw. rechts neben dem Scrollbalken und auf die Pfeiltasten
  • Scrolling per Mausrad über der Content Area
  • Panning mit der Maus
  • zusätzlich ändert zoom in/out die Breite der QScrollBar und die Lage des linken Randes (da das Zentrum konstant bleibt), das darf das kein Scrolling durch die ScrollBar verursachen
und fürs Zoomen:
  • Tasten für zoom-in und zoom-out
  • Slider
  • Ctrl + Mausrad
  • Ctrl + Markieren eines Bereiches (zoom in markierten Bereich nach Loslassen)
Ich bin bald verzweifelt: wie bekomme ich das unter einen Hut? Ich dachte immer, dass Qt ein flexibles Widget-Set ist; da sollten solche quasi-Standard-Anforderungen doch möglich sein??

Sind meine Wünsche wirklich so exotisch?

Verfasst: 3. Juni 2009 14:26
von solarix
olebole hat geschrieben: Sind meine Wünsche wirklich so exotisch?
Falls ich dich richtig verstanden habe, hast du irgend einen aktuellen Wert (Zoomfaktor oder sonstwas) und einfach nur ein paar verschiedene Widgets, die diesen liefern können und auch anzeigen sollen. Worin besteht denn das Problem? Ich meine, wenn du einfach nur reagierst, wenn sich der Wert ändert, kann es dir doch egal sein, wie oft solche Signals kommen:

Code: Alles auswählen

void foo::habeNeuenWert(int val) 
{
  if (actualValue != val) {
     actualValue = val;
     machWas(actualValue);
     widget1->setValue(actualValue);
     widget2->setValue(actualValue);
     widget3->setValue(actualValue);
  }
}
Dann ist es doch auch agal, wenn zwei der Widgets nochmals den gleichen senden...Oder verstehe ich was falsch?
Ein Problem ist ja, dass beim schnellen Verschieben möglicherweise mehrere Signale hintereinander kommen: während das erste valueChanged() noch verarbeitet wird
GUIs sind singelthreaded.. während der Ausführung eines Slots können keine weiteren Signals generiert werden.. Es können sehr viele Events in eine Queue gespeichert werden.. das ja, aber es können sich keine Events "überholen" oder so.

Was Aue dachte ist, dass du vor einem "setValue()" die Signals dieses Widgets blockiest, und daher keine weiteren emitiert werden:

Code: Alles auswählen

   widget1->blockSignals(true);
   widget1->setValue(actualValue);
   widget1->blockSignals(false);

Verfasst: 3. Juni 2009 14:49
von olebole
Das Problem ist, dass die einzelnen Widgets (insbesondere die QScrollBar) auch dann ein Signal schicken, wenn man den Wert programmseitig ändert. Und da die Scrollbars intern nur eine Integer-Darstellung der Position verwenden, stimmen die gesendeten Werte der Signale i.allg. auch nicht überein, sondern differieren ein wenig (aber sichtbar).

Konkret verschickt die QScrollBar auch dann ein valueChanged-Event, wenn ich setValue() mit einem neuen Wert aufrufe:
  • Man dreht am Mausrad mit gedrückter Ctrl-Taste
  • es werden Zoom-Events generiert, die die Darstellung anpassen (OK)
  • diese ändern u.a. auch Größe und Position des Scrollbalkens (*).
  • und sie ändern die Position des Zoom-Sliders (#)
  • die Änderung der Position des Scrollbalkens (*) löst ein valueChanged()-Event aus, der eine erneute Änderung der Darstellung zur Folge hat (schlecht)
  • sie Änderung der Position des Zoom-Sliders (#) löst ein weiteres Zoom-Event aus, welches eine erneute Änderung der Darstellung zur Folge hat (schlecht)
  • er führt außerdem zu einer Anpassung von Position und Größe des Scrollbalkens (**)
  • die erneute Änderung der Position des Scrollbalkens (**) löst ein valueChanged()-Event aus, der wiederum eine Änderung der Darstellung zur Folge hat (schlecht)
das heißt: jedes Signal vom Scrollrad hat vier Events zur Folge, die alle die Darstellung neuzeichnen. Das Verhalten des Programms wirkt dadurch sehr zäh und ruckelig (bedingt durch die interne Integer-Darstellung in QSlider und QScrollBar). Flüssiges Arbeiten sieht definitiv anders aus;damit werde ich keine Ahhs und Ooohs beim Anwender erzielen.
GUIs sind singelthreaded.. während der Ausführung eines Slots können keine weiteren Signals generiert werden..
Ist diese Eigenschaft Teil der API? In http://www.qtforum.de/forum/viewtopic.php?t=9196 hatte ich ja beschrieben, wie bei [Py]Qt4 die Reihenfolge durchaus durcheinanderkommt und mit dort das Resizing von Fenstern kaputt macht...
Edit: auch in dem hier gezeigten Fall kommen die Events asynchron. Wenn ich (wie von Dir vorgeschlagen) ein Veto einbaue und z.B. den Zoom-Slider schnell bewege, dann kommt das letzte Zoom-Event des Sliders eventuell schon an, während er noch an der Verarbeitung eines vorherigen Events ist, was dazu führt, dass ein falscher Zoom angezeigt wird. Das kann man letztlich auch deutlich sehen, wenn man sich (wie in meinem anderen Beitrag gezeigt) die einzelnen Signale ausprinten lässt.

Verfasst: 3. Juni 2009 17:05
von solarix
Ist diese Eigenschaft Teil der API?
Nein... das ist eine Eigenschaft einer Singlethreadapplikation. Wenn eine Applikation was tut, kann sie nichts anderes tun. Events koennen (vor der Ausfuehrung eines Slots) gesammelt werden, das schon.. Methoden koennen auch rekursiv aufgerufen werden (bei Qt z.B. bei einer DirectConnection). Aber _nicht_ "asynchron".

Code: Alles auswählen

Das kann man letztlich auch deutlich sehen, wenn man sich (wie in meinem anderen Beitrag gezeigt) die einzelnen Signale ausprinten lässt.
Versuch mal, im Slot des Sliders einfach nur den aktuellen Wert mittels qDebug() auszugeben. Ohne irgendwelchen weiteren Code. Bekommst du da wirklich Zahlenfolgen, welche _nicht_ 100% auf- oder absteigend (also z.B. 0-1-2-3-10-5-20) sind?

Verfasst: 3. Juni 2009 18:25
von olebole
solarix hat geschrieben:
Ist diese Eigenschaft Teil der API?
Nein... das ist eine Eigenschaft einer Singlethreadapplikation.
Woher weiß ich (bzw. weißt Du) denn, dass eine Qt-Eventloop eine Singlethreadapplikation ist?
Aber _nicht_ "asynchron".
Dem widerspricht doch aber das gezeigte Beispiel: Layoutmanager, zwei Widgets (1. Widget: langsames Diagram; 2. Widget: QScrollBar). Resize-Event, ausgelöst duch Größenänderung mit der Maus. Reihenfolge:
  • 1. Resize-Event an 1. Widget
  • 2. Resize-Event an 1. Widget
  • 2. Resize-Event an 2. Widget
  • 1. Resize-Event an 2. Widget
Das ist die Reihenfolge, in der sie intern verarbeitet werden und das ist auch die Reihenfolge, in der sie ausgedruckt werden.
Das ist eindeutig nicht synchron. Und es ist nur zu erklären, wenn man annimmt, dass es mehr als einen eventauslösenden Thread gibt.
Versuch mal, im Slot des Sliders einfach nur den aktuellen Wert mittels qDebug() auszugeben. Ohne irgendwelchen weiteren Code. Bekommst du da wirklich Zahlenfolgen, welche _nicht_ 100% auf- oder absteigend (also z.B. 0-1-2-3-10-5-20) sind?
Ich habe einfach [Python] "print" verwendet. Und da kommt es vor, dass diese Reihenfolge nicht stimmt. Das hängt aber davon ab, ob die anderen Widgets (insbesondere das Update des Diagramms) "langsam genug" sind, dass sich Events "überholen" können.

Wie schon geschrieben: das kürzeste (und IMO instruktivste) Beispiel ist incl. Code in dem anderen Thread zu sehen; ich kann ihn ja noch mit dem Printout der Eventreihenfolge versehen. Hier ist das ein Nebenproblem, welches aber beachtet werden möchte, wenn ich keine Darstellungsfehler will.

Das Hauptproblem bleibt, dass die Scrollbars/Sliders auch Signale versenden, wenn sie programmseitig angesprochen werden.