Seite 1 von 1

QWidget - Repaint

Verfasst: 2. Dezember 2009 18:28
von suamikim
Hallo zusammen,

ich bin gerade dabei mir einen Splash-Screen zu basteln. Dazu hab ich einfach eine von QWidget abgeleitete Klasse erstellt, die ich vor meinem MainWindow anzeige.

Das ganze passt auch soweit bis zu folgendem Punkt:

In meiner Splash-Klasse habe ich eine Methode startup(), die ich zum Schluss im Konstruktor aufrufe.
Diese Methode soll ein paar Dinge erledigen, bevor das Programm angezeigt wird. Nebenbei wird halt auch eine ProcessBar und ein Label aktualisiert.

Zwischen den einzelnen Schritten brauche ich immer wieder ein sleep, damit der Splash nicht zu schnell fertig ist.

Dafür benutze ich den QTimer:

Code: Alles auswählen

void SplashScreen::on_timer()
{
  m_bNextWaitFinished = true;
}

void SplashScreen::wait(int iMsToWait)
{
  m_bNextWaitFinished = false;
  QTimer::singleShot(iMsToWait, this, SLOT(on_timer()));

  while (!m_bNextWaitFinished)
    QApplication::processEvents();
}
Meine startup-Methode sieht jetzt also zB. so aus:

Code: Alles auswählen

void SplashScreen::startup()
{
  m_ui->progressBar->setValue(0);
  m_ui->lblStatus->setText(tr("Downloading database..."));

  wait(1000);

  m_ui->progressBar->setValue(20);
  m_ui->lblStatus->setText(tr("Opening database..."));

  wait(1000);
  //...
Mein Problem ist nun, dass die Oberfläche anscheinend nicht aktualisiert wird, während der Timer läuft.
Nun wird mein Widget erst sichtbar, wenn alle waits beendet sind, dh. ich sehe zum Schluss nur 100% und den letzten Status-Text, bevor die Methode fertig ist und mein MainWindow "startet".

Danke, mfg

mik

Gleich vorweg:

1) Ich verwende QTest::qSleep deshalb nicht, weil ich in "weiser Voraussicht" eine eigene Klasse mit dem Namen QTest in meinem Projekt erstellt habe und diese nicht ändern will...

2) Ich hab auch überlegt meine Klasse von QSplashScreen abzuleiten, sehe darin aber keinen wirklichen Vorteil bzw. stört es mich sogar, dass der Splash dann bei Mausklick automatisch verschwindet (was man natürlich auch abfangen könnte...).

Verfasst: 3. Dezember 2009 10:42
von RHBaum
sobald sich auf der oberflaeche was aktualisieren soll, brauchst du eine laufende eventloop.

ein sleep blockiert den thread -> ergo deine eventloop laeuft ned

du benutzt aber schon qtimer. nun nutze die timer auch um dein sleep zu ersetzen ....

- bau dein fenster auf, setz nen timer auf 100ms geh in die eventloop ---
- nach 100ms feuert dein timer, -> progressbar aktualisieren
check, ob deine "Zeit um ist", wenn nicht -> neuen timer auf 100ms setzen
wieder in die eventloop gehen (timer behandlungsroutine einfach rauslaufen lassn)
....
....
- nach 100ms feuert dein timer, -> progressbar aktualisieren
check, ob deine "Zeit um ist", wenn um, dann Deiner form das close schicken
wieder in die eventloop gehen (timer behandlungsroutine einfach rauslaufen lassn)

- das Splash fenster selber muss natuerlich modal sein und du musst es mit exec aufrufen
- nonmodal mit show gaenge auch, nur brauchst dazu dann ne übergeordnete instanz, die regelt dass dein mainfenster erst nach dem Splashscreen angezeigt wird. Vorteil waere hier aber, dass du waehrend dem splachsreen im mainwindow und global schon initialisierungen machen koenntest.

Prinzip verstanden ?

Ciao ...

Verfasst: 3. Dezember 2009 11:08
von suamikim
RHBaum hat geschrieben:Prinzip verstanden ?
Weiß nicht recht ;)
RHBaum hat geschrieben:sobald sich auf der oberflaeche was aktualisieren soll, brauchst du eine laufende eventloop.
Soweit so klar...
RHBaum hat geschrieben:ein sleep blockiert den thread -> ergo deine eventloop laeuft ned

du benutzt aber schon qtimer. nun nutze die timer auch um dein sleep zu ersetzen ....
Ich benutze ja gar kein sleep, sondern habe dieses eben schon durch meinen Timer (die wait-Methode) ersetzt.
In dieser wait-Methode wird ja auch processEvents() aufgerufen solange auf den Timer gewartet wird, damit die EventLoop eben nicht blockiert wird. Was ist hier an meinem Gedankengang falsch?

Code: Alles auswählen

void SplashScreen::on_timer()
{
  m_bNextWaitFinished = true;
}

void SplashScreen::wait(int iMsToWait)
{
  m_bNextWaitFinished = false;
  QTimer::singleShot(iMsToWait, this, SLOT(on_timer()));

  while (!m_bNextWaitFinished)
    QApplication::processEvents();
}
RHBaum hat geschrieben:- nonmodal mit show gaenge auch, nur brauchst dazu dann ne übergeordnete instanz, die regelt dass dein mainfenster erst nach dem Splashscreen angezeigt wird. Vorteil waere hier aber, dass du waehrend dem splachsreen im mainwindow und global schon initialisierungen machen koenntest.
Diese Instanz hab ich schon in meiner main.cpp umgesetzt:

Code: Alles auswählen

int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  a.setOrganizationName("OrgName");
  a.setApplicationName("AppName");

  SplashScreen s;
  MainWindow w;

  s.show();
  while (!s.finished() && s.isVisible())
    a.processEvents();
  s.hide();

  w.show();
  return a.exec();
}
Stimmt das soweit?

Verfasst: 3. Dezember 2009 12:48
von suamikim
Soda, ich hab mein Problem jetzt gelöst, indem ich mit einem Thread arbeite.

Das ganze sieht jetzt folgendermaßen aus, falls es mal jemanden interessiert:

main.cpp:

Code: Alles auswählen

int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  a.setOrganizationName("Mephisto");
  a.setApplicationName("QTestMe");

  SplashScreen s;
  MainWindow w;

  s.show();
  while (!s.finished() && s.isVisible())
    a.processEvents();
  s.hide();

  w.show();
  return a.exec();
}
splashscreen.h (in diesem File ist auch die Thread-Klasse deklariert):

Code: Alles auswählen

#ifndef SPLASHSCREEN_H
#define SPLASHSCREEN_H

#include <QtGui/QWidget>
#include <QThread>

namespace Ui
{
  class SplashScreen;
}

//-----------------------------------------------------------------
class QSplashThread : public QThread
{
  Q_OBJECT

public:
  QSplashThread(QObject *parent = 0);
  ~QSplashThread();

protected:
  void run();

signals:
  void newStatus(QString qStrStatusText, int iPercDone, bool bFinished);
};

//-----------------------------------------------------------------
class SplashScreen : public QWidget
{
  Q_OBJECT

public:
  SplashScreen(QWidget *parent = 0);
  ~SplashScreen();

  const bool finished() { return m_bFinished; }

private slots:
  void on_newStatus(QString qStrStatusText, int iPercDone, bool bFinished);

private:
  Ui::SplashScreen *m_ui;
  bool m_bFinished;
  QSplashThread thread;
};

#endif // SPLASHSCREEN_H
splashscreen.cpp (hier ist natürlich auch die Implementierung der Thread-Klasse):

Code: Alles auswählen

#include "splashscreen.h"
#include "ui_splashscreen.h"
#include <QTimer>
#include <QDesktopWidget>

//-----------------------------------------------------------------
QSplashThread::QSplashThread(QObject *parent/*=0*/) : QThread(parent)
{
}

QSplashThread::~QSplashThread()
{
}

void QSplashThread::run()
{
  emit newStatus(tr("Initialization 1..."), 0, false);
  // Initialisierungs-Arbeit...
  QThread::msleep(1000);

  emit newStatus(tr("Initialization 2..."), 20, false);
  // Initialisierungs-Arbeit...
  QThread::msleep(1000);

  //...

  emit newStatus(tr("Initialization 5..."), 80, false);
  // Initialisierungs-Arbeit...
  QThread::msleep(1000);

  emit newStatus(tr("Initialization done..."), 100, false);
  QThread::msleep(1000);
  emit newStatus(tr("Initialization done..."), 100, true);
}

//-----------------------------------------------------------------
SplashScreen::SplashScreen(QWidget *parent) : QWidget(parent, Qt::FramelessWindowHint  | Qt::WindowSystemMenuHint | Qt::Tool), m_ui(new Ui::SplashScreen)
{
  m_ui->setupUi(this);
  
  // Hintergrundbild festlegen
  QPalette qPalette;
  qPalette.setBrush(this->backgroundRole(), QBrush(QImage(":/image_splash")));
  this->setPalette(qPalette);

  // Splash in mitte von Bildschirm anzeigen
  QRect qRectDesktop  = QDesktopWidget().availableGeometry();
  QRect qRectApp      = this->frameGeometry();
  this->setGeometry((qRectDesktop.width() - qRectApp.width()) / 2, (qRectDesktop.height() - qRectApp.height()) / 2, qRectApp.width(), qRectApp.height());

  m_bFinished = false;

  connect(&thread, SIGNAL(newStatus(QString,int,bool)), this, SLOT(on_newStatus(QString,int,bool)));
  thread.start();
}

SplashScreen::~SplashScreen()
{
  delete m_ui;
}

void SplashScreen::on_newStatus(QString qStrStatusText, int iPercDone, bool bFinished)
{
  m_ui->progressBar->setValue(iPercDone);
  m_ui->lblStatus->setText(qStrStatusText); 
  m_bFinished = bFinished;
}
Gefällt mir eigentlich ganz gut und macht genau dass, was ich möchte.
Das ganze ist mein erster Versuch mit Threads in Qt, daher würde ich jeden, dem etwas im Code nicht gefällt, bitten sich mit Kritiken & Verbesserungsvorschlägen nicht zurückzuhalten, damit ich vielleicht auf diesem Wege wieder etwas dazulerne!

danke, mfg

Verfasst: 3. Dezember 2009 12:49
von RHBaum
Ohje :-)

s.show();
setzt eigentlich nur die Fensterflags so, das das fenster angezeigt wird aka schickt der winapi die entsprechenden Windows nachrichten. Iss das direkt, also nix entkoppelt und das Klassenobject beschiesst die WINAPI mit sendmessage (statt postmessage) werden die entsprechenden funktionen sofort aufgerufen und du siehst es sofort.

Aber dein Object ist noch tot, d.h. es regiert auf gar nix ....

return a.exec();
hier wird Deine QT interne Eventloop erst gestartet. die ganzen queued verbindungen sowie Qtimer etc duerften vorher gar ned laufen. Also alles was du in die eventloop spuslt, wird nie abgearbeitet, sondern nur das was direkt durch die messsage queue durchgeschossen wird (QCoreApplication::notify im selben thread im gegensatz zu QCoreApplication::postEvent) kommt an. Analog zur winapi.
erst mit den exec werden die queues abgearbeiten, wobei die QT eventloop die winapi msgpump mit bedienen wird.

das heisst, du musst das exec viel viel frueher initieren !

erstmal Mittag ! ^^

Ciao ....

Verfasst: 3. Dezember 2009 13:02
von suamikim
RHBaum hat geschrieben:return a.exec();
hier wird Deine QT interne Eventloop erst gestartet. die ganzen queued verbindungen sowie Qtimer etc duerften vorher gar ned laufen.
Die Doku zum QSplashScreen sagt dazu folgendes:

"The user can hide the splash screen by clicking on it with the mouse. Since the splash screen is typically displayed before the event loop has started running, it is necessary to periodically call QApplication::processEvents() to receive the mouse clicks."

Deshalb dachte ich, dass das in meinem Fall auch reichen sollte. Die QTimer-Events wurden ja richtig gefeuert und vom Programm verarbeitet, nur das Zeichnen der Oberfläche hat nicht funktioniert...

Vielleicht irgendwelche Kritiken/Verbesserungsvorschläge zu der Variante mit dem Thread?

danke, mfg & mahlzeit ;)

Verfasst: 3. Dezember 2009 13:51
von RHBaum
naja Kritik :

while (!s.finished() && s.isVisible())
a.processEvents();
s.hide();

was das ding macht, weisst scho ^^ selbst das processEvents machts ned viel besser.
Deine Prozessorlasst duerfte bei 100% auf einem Kern stehen richtig?


Du hasst prinzipiell 2 Mögligkeiten.
1: du startest 2 mal eine Eventloop
2: du integrierst deinen Splashscreen in die globale eventqueue, und erzeugst event(s) fuers beenden des ersten und starten des 2ten, damit da aus der queue rauskommst klar.

1. Variante ....

die naivste Form:

Code: Alles auswählen

int main(int argc, char *argv[])
{ 
QApplication a(argc, argv);
 a.setOrganizationName("Mephisto");
 a.setApplicationName("QTestMe");

SplashScreen s; 
s.show(); 
a.exec();  

MainWindow w; 
w.show();
 return a.exec(); 
}
ned wirklich tolles design, aber sollt funktionieren

bessere Version:
WinApi Grundlagen ... naja eigentlich Grundlage zu allen eventgesteuerten Oberflachen (also alle mit evenqueues).
Was ist der unterschied zwischen einem modalen und einem Nichtmodalen Dialog ?

Richtig, nen Modaler dialog blockiert die eventloop des aufrufenden fensters indem er nicht zurueckspringt, sondern einfach selber fuer seine lebensdauer eine eigene Loop erzeugt.

Passender weisse, wird die startfunktion des modalen dialogs, welche eigene Loop startet, in der Qt auch exec() genannt.

leite deinen Splashscreen von QDialog
und dann geht sowas:

Code: Alles auswählen

int main(int argc, char *argv[])
{ 
QApplication a(argc, argv);
 a.setOrganizationName("Mephisto");
 a.setApplicationName("QTestMe");

SplashScreen s; 
s.exec(); 

MainWindow w; 
w.show();
 return a.exec(); 
}

fuer die 2. grundlegende variante braeuchtest du ein Object, was sich in die eventloops einklinken kann, und die steuerung der fesnter uebernimmt.
das koennte man machen indem man von QApplication ableitet und die events an das app object reicht, welches deine fenster aufruft.
diese version bietet sich an, wenn du in der zeit des Splashscreens vorbereitungen in nem anderen Fenster oder der Applikation ausführen woelltest ....

Auch im Zeitalter von modernen GUI frameworks sind die Basic's immer noch aktuell und von Vorteil. Der Petzold (Win 32 programmierung) hat noch immer seine daseinsberechtigung.

Ciao ...

Verfasst: 3. Dezember 2009 14:47
von RHBaum
Noch was generelles:

Es gibt 2 grosse Arten von Programmen.
Das eine sind die nützlichen, das andere sind unnütze oder Schadsoftware.

zur ersten gruppe gehören viele Tools und Applikationen. Zur 2ten gruppe gehoeren Viren und auch AdWare / Shareware etc.

viele Programme bewegen sich auch irgendwo dazwischen.

für Programmierer die leidenschaftlich Fan ersteren Programmen sind, kommt eigentlich nur eine Version des Splashscreens in der nichtmodalen form, bzw auch multithread in Frage.
Da hat der Splashscreen die Aufgabe, irgendwas anzuzeigen während im Hintergrund "vorbereitende Tätigkeiten" ausgefuehrt werden. und der Splashscreen bleibt auch ned unwesentlich länger als wie diese Tätigkeiten brauchen (Eventsteuerung ! )
Gibt es keine Initialisierungen zu tun oder gehn diese nur sehr sehr kurz, schreibt man Informationen dahin wo sie eigentlich auch noch hingehoeren, in den About Teil.

Programmierer die mit Ihren Programmen abweichende Intressen vom User pflegen, die haben natuerlich auch kein Problem künstlich Ladezeiten zu erzeugen, um den User so viel wie möglich "Informationen im eigenenen Intresse" anzuzeigen. Die werden das Ding sicherlich modal machen ....

Ciao ...

Verfasst: 3. Dezember 2009 15:09
von suamikim
Zum einen mal herzlichen dank für deine ausführlichen Erklärungen im vorletzten Post! Ich werde mir deine Kritikpunkte zu Herzen nehmen und das ganze noch ein wenig umbauen, bis es meinen Vorstellungen entspricht.

Zum letzten Post muss ich mich natürlich auch etwas äußern bzw. rechtfertigen:

Da wird mir jetzt etwas unterstellt, dass so bestimmt nicht stimmt. Nämlich, dass ich den Benutzer mit irgendwelchen unnötigen Informationen beim Programmstart zumüllen will.

1.) Wird das Programm, wenn es mal fertig ist nirgends veröffentlicht (weder als Freeware, noch kommerziell), was wiederum heißt, dass es gar keine Benutzerbasis gibt, die ich zumüllen könnte.
Das Programm ist ausschließlich für mich und vielleicht Freunde, die es benötigen könnten.

2.) Ist alles, was ich in diesem Post geschrieben habe lediglich exemplarisch. Die ganzen Sachen mit den Sleeps usw. hab ich nur zu Testzwecken verwendet, um mal einen Splash gebastelt zu haben, auf dem ich dann aufbauen und richtige Funktionalität einbauen kann.

Dh. während der Splash angezeigt wird, wird in Zukunft durchaus etwas getan und NICHT geschlafen um dem Benutzer (der dann sowieso nur ich bin) "Informationen im eigenen Interesse" anzuzeigen...

Das ganze ist wohl etwas falsch rübergekommen, weil ich in meinen Beispielen die ganze Zeit mit den Sleeps herumgewerkt habe. Wie gesagt: die sind in der 1. Phase nur mal drinnen, damit ich sehen konnte, ob der Splash richtig angezeigt wird, sich was bewegt usw...

mfg

Verfasst: 3. Dezember 2009 16:05
von RHBaum
Hey, das sollt und darf eigentlich keine "direkte" Kritik sein. Weil ich weiss ja wirklich ned zu was Du dein Programm brauchst. Im ersten Blick sah es wirklich so aus, als wölltest du "Ladezeiten" erzeugen, nur fuer den Splash.

Das koennt ich auch nichtmal verurteilen, weil, Du bist der Programmierer, du bestimmst was der User in Kauf zu nehmen hat ^^ . Ich wollt nur drauf hinweissen dass die User evtl. andere Intressen hat, und du programmierst ziemlich an dem thema rum. Du sollst Dir nur dessen bewusst sein ^^
Und eigentlich sollt sich daraus auch gewisse Programmierstrategien ableiten. Nen "unfreundliches" programm wird sich ned viel um Events scheren und viel Pollen wenn halt nötig. Oberflaeche Blockiert halt, kein thema.

Freundlichere programme versuchen das zu vermeiden, informieren den user uber zustaende, nehmen immer befehle entgegen, blenden Befehls-Objekte aus, wenn die Befehle ned verfügbar sind.
Dafür nehmen die viel in kauf halt, viel viel mehr programmieraufwand teilweisse. Multithraeding macht nen thema ned grad einfacher :-)
Asynchronitaet und Events sind da nen grosses thema.
Das ganze laesst sich natuerlich auch fuer deinen Splashscreen anwenden.

Aber zum Thema:

Dh. während der Splash angezeigt wird, wird in Zukunft durchaus etwas getan und NICHT geschlafen
Danach solltest du aber dann auch dein Design bauen.
wann und wie startet denn das was er am Anfang macht. Ist das eine funktion die du in der Main aufrufst ???
Der Ansatzpunkt den ich wählen wuerd, wär halt Abhanegig davon, ob du multithreading brauchst oder nicht. Das wiedum kann nur bestimmt werden durch das was du halt tun must ...


Du wirst definitiv eine deinem Splash und deinem 2. Fenster uebergeordnete Instanz brauchen, die die Abfolge der Fenster steuert.
Im guenstigsten Fall kann das deine QApp machen ...
Notfalls musst du selbst was schreiben ....

Ciao ...