Weiter Zurück Inhaltsverzeichnis
In diesem Kapitel werden wir die Funktionalität unseres Widgets durch zwei Verbesserungen erweitern: synchronisierte Ansichten und rollbare Ansichten.
Lassen Sie uns zuerst erklären, was uns das Ganze bringen wird und wie wir es erreichen
können. Während Sie mit KScribble experimentiert haben, werden Sie vielleicht
bemerkt haben, daß, wenn Sie eine neue Ansicht des Dokuments durch Aufruf von
"Fenster"->"neues Fenster" öffnen, diese neue Ansicht mit denselben Daten arbeitet, wie
die erste Ansicht und sich genauso verhält, wie jede Ansicht, die Sie mit diesem Kommando
erzeugen. Aber wenn es darum geht, in das Dokument zu zeichnen, können Sie das nur in
einer Ansicht tun- die anderen Ansichten zeigen nicht den jeweils aktuellen Inhalt. Wenn
Sie eine Ansicht, die nicht die aktuellen Daten enthält, verdecken und sie dann wieder in
den Vordergrund holen, werden die richtigen Daten angezeigt. Das liegt daran, das jede
Ansicht, wenn Sie verdeckt war, ein paint-Ereignis vom Fenstersystem erhält, wenn Sie
wieder in den Vordergrund kommt, dann KScribbleView::paintEvent()
aufruft und
dadurch den Bereich, der verdeckt war schließlich neuzeichnet. Was wir erreichen wollen,
ist daß alle Ansichten synchron mit der Ansicht gezeichnet werden, in der der Benutzer
gerade arbeitet. Tatsächlich werden Sie sehen, daß diese Erweiterung leicht zu
bewerkstelligen ist. Die Dokumentklasse bietet uns bereits eine Methode
updateAllViews()
, welche die Methode update()
für jede Ansicht in der
Ansichtenliste des Dokuments aufruft. Das macht es sehr einfach die Inhalte der Ansichten
zu synchronisieren - jedes Mal, wenn der Inhalt verändert wird, hier durch Mausbewegungen
( durch die wir die Änderungen mittels bitBlt()
in den Puffer kopieren), müssen
wir nur updateAllViews(this) aufrufen. Der this Zeiger ist nötig, weil die aufrufende
Ansicht nicht neugezeichnet werden muß und update()
nur ausgeführt werden soll,
wenn Sender und Empfänger verschieden sind.
Was Sie hier tun müssen, ist nur updateAllViews(this) am Ende der virtuellen Methoden
mousePressEvent()
, mouseMoveEvent()
und
mouseReleaseEvent()
aufzurufen- und Sie sind fertig ! Nehmen Sie dies als
allgemeine Regel in Ihren Anwendungen: jedesmal wenn sich der Inhalt eines Dokuments
in einer Ansicht ändert, rufen Sie updateAllViews()
auf. Wie die Aktualisierung
durchgeführt wird, muß in der update()
Methode des Widgets implementiert werden;
man mag zufrieden sein, indem man z.B. den geänderten Text in einem Editor setzt, in
unserer Anwendung rufen wir nur die Funktion repaint()
auf, die ein paint-Event
auslöst und den Inhalt des Dokuments wieder in die Ansicht kopiert.
In diesem Abschnitt werden wir eine Funktionalität implementieren, die meistens zum Kreuz
für die Entwickler wird - wenn Sie nicht auf bereits implementierte Widgets
zurückgreifen können, die das Rollen schon mitbringen. Was bedeutet Rollen? In unserem
Kontext beginnt das Problem damit, das wir ein Bild öffnen wollen, das viel größer als der
darstellbare Bereich ist. Beginnend von der oberen, linken Ecke, wird der Rest des Bildes
aus der Sicht des Benutzers ausgeschnitten. Eine Rollansicht ist ein Widget, das auf der
rechten und unteren Seite Rollbalken zur Verfügung stellt, mit deren Hilfe der Benutzer
den Inhalt des Fensters verschieben kann. Tatsächlich wird das Dokument in seiner ganzen
Größe gezeigt, aber die Ansicht kann innerhalb des Dokuments bewegt werden, sodaß jeder
Teil mit Hilfe der Scrollbars angezeigt werden kann. Glücklicherweise gibt es in Qt die
Klasse QScrollView
,die selber von QWidget
abgeleitet ist und die gleiche
Grundfunktionalität wie ein gewöhnliches Widget bietet, jedoch den Inhalt der Ansicht bei
der Verwendung von Scrollbars verwaltet - mit der zusätzlichen Option, daß der
Programmierer entweder einfach nur eine Instanz von QScrollView
verwendet, die zu
verwaltenden Kindfenster mit der Rollansicht als Eltern erstellt und sie mit
addChild()
der Rollansicht hinzufügt, oder eine Ansicht von QScrollView
ableitet und dann, statt direkt in das Widget, in den Viewport zeichnet, der ein
definierter Bereich innerhalb der Rollansicht ist. Der Unterschied hier ist, daß
QScrollView
einen Satz von Ereignisbehandlungsroutinen, ähnlich denen von
QWidget
, extra für den Viewport bereitstellt. Das was also vorher
mousePressEvent()
in unserer Ansicht war, ist jetzt
ein viewportMousePressEvent, ein paintEvent()
wird zu einem
viewportPaintEvent usw. . Die zweite der genannten Möglichkeiten passt auf unsere
Anforderungen, wir werden also folgendene Modifikationen vornehmen, um KScribbleView zu
einem rollbaren Widget zu machen:
QWidget
nach
QScrollView
. QScrollView
. QWidget
vertraut, die in der linken, oberen Ecke eines Widgets beginnt. Wenn die
Ansicht gerollt wird und diese Ecke ist nicht sichtbar, müssen wir dafür sorgen, daß die
von QWidget
gelieferten Positionen in Viewportkoordinaten übersetzt werden.
Wie bereits erwähnt, müssen wir sowohl die Größe eines Dokuments ändern, als auch diese Größe initialisieren, und wir müssen eine
Methode bereitstellen, die diese Größe für die Ansichten holt. Dazu fügen wir die Variable QSize size
sowie die Methode
docSize()
in KScribbleDoc
ein:
kscribbledoc.h: #include <qsize.h> ... public: const QSize docSize(){ return size;}; private: QSize size;
Jetzt müssen wir alle Methoden, die Dokumentinhalte öffnen oder initialisieren, modifizieren.Dies sind newDocument()
und
openDocument()
:
bool KScribbleDoc::newDocument() { ///////////////////////////////////////////////// // TODO: Add your document initialization code here -> size=QSize(300,200 ); pen=QPen( Qt::black, 3 ); -> buffer.resize(size); -> buffer.fill( Qt::white ); ///////////////////////////////////////////////// modified=false; return true; } bool KScribbleDoc::openDocument(const QString &filename, const char *format /*=0*/) { QFile f( filename ); // if ( !f.open( IO_ReadOnly ) ) // return false; ///////////////////////////////////////////////// // TODO: Add your document opening code here if(!buffer.load( filename, format )) return false; -> size=buffer.size(); ///////////////////////////////////////////////// // f.close(); modified=false; m_filename=filename; m_title=QFileInfo(f).fileName(); return true; }
In newDocument()
initialisieren wir die Größe mit Standardwerten von 300 Pixeln Breite und 200 Pixeln Höhe. Das ist genug
für ein kleines Bild und wir könnten auch immer noch einen Dialog zur Größenänderung hinzufügen. Wenn es daran geht, das Bild zu
öffnen, müssen wir die Größe auf die des Bildes setzen. Dies kann man erreichen, indem man QPixmap::size()
aufruft, was
wir bereits in openDocument()
verwendet haben. Dann sind mit dem Setzen der Größen fertig, und wir können damit beginnen,
KScribbleView zu reimplementieren und eine Rollansicht daraus zu machen.
Wie oben gesagt, müssen wir zuerst einige Dinge in der Schnittstelle von KScribbleView ändern. Der folgende Code zeigt diese Änderungen:
#include <qscrollview.h> class KScribbleView : public QScrollView { Q_OBJECT protected: /** changed from mousePressEvent() overwriting QScrollView method */ virtual void viewportMousePressEvent( QMouseEvent* ); /** changed from mouseReleaseEvent() overwriting QScrollView method */ virtual void viewportMouseReleaseEvent( QMouseEvent* ); /** changed from mouseMoveEvent() overwriting QScrollView method */ virtual void viewportMouseMoveEvent( QMouseEvent* ); /** commeted out because we have a document size defined */ // resizeEvent( QResizeEvent* ); /** changed from paintEvent() overwriting QScrollView method */ virtual void viewportPaintEvent( QPaintEvent* ); }
An dieser Stelle haben wir zuerst QWidget
durch QScrollView
als Basisklasse ersetzt und in die erforderliche Headerdatei
eingesetzt.Außerdem haben wir alle implementierten Event Handler, die mit Interaktion bezüglich des Inhalts der Rollansicht zu tun haben,
durch die entsprechenden Methoden aus QScrollView
ersetzt und haben das resizeEvent auskommentiert. Nun können wir mit der
Implementation dieser Methoden beginnen und die Größe unseres Bildes verwenden. Da eine Ansicht immer erst nach dem Dokument
existiert, können wir sowohl die Größe des Widgets (die Viewportgröße), als auch die des Inhalts direkt im Konstruktor anpassen.
#include <qsize.h> KScribbleView::KScribbleView(KScribbleDoc* pDoc, QWidget *parent, const char* name, int wflags) : QScrollView(parent, name, wflags | WPaintClever | WNorthWestGravity | WRepaintNoErase) { doc=pDoc; mousePressed=false; polyline=QPointArray(3); -> setResizePolicy ( QScrollView::ResizeOne ); -> viewport()->setCursor( Qt::crossCursor ); -> QSize size=doc->docSize(); // resize the viewport - this makes the resizeEvent obsolete -> resizeContents(size.width(), size.height()); // resize the widget to show up with the document size -> resize(size); }
Beachten Sie, daß sich vorher resizeEvent()
um die Gleichheit der Größe der Zeichenfläche und des Widgets gekümmert hat.
Zur gleichen Zeit wurde dabei auch die Größe des Dokuments geändert, sodaß das Bild immer die gleiche Größe wie das Widget hatte.
Da wir jetzt bereits die Größe des Dokuments initialisiert haben (in newDocument()
und openDocument()
), passen
wir jetzt die Größe des Inhalts einfach durch Aufruf von resizeContents()
aus QScrollView
an die Größe des
Dokuments an. Sie sehen auch, daß wir den Cursor für das Widget von dem allumfassenden Widget auf den Viewport geändert haben, den
wir mit viewport()
ermitteln können. Jetzt können wir wieder die Event Handler implementieren. Zuerst sollten wir auf
das paintEvent achten, da dies eines der wichtigsten Events ist, weil es immer aufgerufen wird, wenn das Widget sichtbar wird oder seine
Größe ändert.
Achtung: Denken Sie daran die resizeEvent()
Implementation auszukommentieren!
Jetzt wird der paintEvent das Pixmap aus dem Puffer an die entsprechende Position der Ansicht kopieren müssen. Zu diesem Zweck
müssen wir das Ziel von bitBlt()
von this nach viewport()
ändern, die linke, obere Position auf 0,0 setzen und
das Ziel (den Puffer) so einstellen, daß von den contentsX und contentsY Positionen in den Viewport kopiert wird:
void KScribbleView::viewportPaintEvent( QPaintEvent *e ) { bitBlt( viewport(),0,0, &doc->buffer,contentsX() ,contentsY() ); }
contentsX()
ist die Position in X-Richtung der Rollansicht - die absolut der Position 0 des Viewports entspricht, und
damit der linken, oberen Ecke der Ansicht. Das gleiche gilt auch für die Y-Richtung. Dieser Teil ist manchmal schwer zu verstehen
und Sie müssen vielleicht ein wenig "Versuch und Irrtum" bei der Implementation Ihrer eigenen rollbaren Ansichten spielen. Der
andere mögliche Aufruf von bitBlt()
würde sein, die Werte der Positionen zu switchen und die Werte der Inhalte zu
vertauschen:
bitBlt( viewport(), -contentsX(), -contentsY(), &doc->buffer, 0, 0 );
Die letzte Änderung, die wir noch brauchen, ist die Änderung des Mauseventhandlers. Im Moment hat mouseMoveEvent()
,
das zu viewportMouseMoveEvent()
wird, auch einen bitBlt()
Aufruf. Hier müssen wir die gleichen Änderungen
vornehmen, wie beim paint Event. Weiterhin haben wir im mousePressEvent()
und dem mouseMoveEvent()
die Position
der Events mit e->pos()
geholt. Diese Abfrage liefert uns nun die Position eines Widgets - nicht die des Inhalts, also
müssen wir dies mit viewportToContents()
übersetzen, damit an die korrekte Position des Dokuments gezeichnet wird:
void KScribbleView::viewportMousePressEvent( QMouseEvent *e ) { mousePressed = TRUE; -> doc->polyline[2] = doc->polyline[1] = doc->polyline[0] = viewportToContents(e->pos()); doc->updateAllViews(this); } void KScribbleView::viewportMouseMoveEvent( QMouseEvent *e ) { if ( mousePressed ) { .... doc->polyline[1] = doc->polyline[0]; -> doc->polyline[0] = viewportToContents(e->pos()); painter.drawPolyline( doc->polyline ); .... r.setBottom( r.bottom() + doc->penWidth() ); doc->setModified(); -> bitBlt(viewport(), r.x()-contentsX(), r.y()-contentsY() , -> &doc->buffer, r.x(), r.y(), r.width(), r.height() ); doc->updateAllViews(this); } }
In viewportMouseMoveEvent()
mußten wir wieder das Ziel von this nach viewport()
ändern - und damit die
Positionen übersetzen. Diesmal haben wir die zweite Variante des Aufrufs, den wir in viewportPaintEvent()
verwendet
haben, benutzt, indem wir contentsX und contentsY subtrahiert haben um das Rechteck, das die aktuelle Zeichnung enthält, an
die korrekte Stelle des Viewports zu kopieren.
Schließlich werden wir noch eine kleine Änderung an der update()
Methode vornehmen: warum sollten wir jedesmal das ganze
Widget neuzeichnen? Dies wird in den meisten Fällen die Performance herabsetzen und zu dem sogenannten "Flicker" Effekt führen.
Dieser Effekt tritt manchmal bei Widgets auf, aber es gibt einige Möglichkeiten dieses Verhalten zu verringern. Statt
repaint()
aufzurufen, könnten wir ebensogut repaint(false)
verwenden. Dadurch werden die Inhalte nicht vor dem
Zeichnen gelöscht. Da wir den Inhalt des Dokuments direkt in das Widget kopieren, müssen wir die Daten nicht löschen, weil sie
sowieso überschrieben werden. In Verbindung mit QScrollView
können wir die Zeichenaktionen sogar noch weiter reduzieren:
wir beschränken die update Methode darauf repaint()
auf dem viewport() Widget aufzurufen, da dies wiederum
viewportPaintEvent()
aufruft. Außerdem können wir, wenn das Dokument kleiner als der Viewport ist, das paint Event noch
auf das Rechteck des Viewports beschränken, in dem das Dokument angezeigt wird. Die sichtbare Höhe und Breite können wir uns holen
und zu dem zu zeichnenden Rechteck zusammensetzen. Zusätzlich verwenden wir false als erase Parameter, so daß die Dokumentenfläche
nicht gelöscht wird:
void KScribbleView::update(KScribbleView* pSender){ if(pSender != this) viewport()->repaint(0,0,visibleWidth(), visibleHeight(), false); }
Jetzt sind wir fertig ! Dieses Kapitel war sicher eines, der am schwierigsten zu implementierenden und zu verstehenden - insbesondere dort, wo es um die, sich ändernden Geometrien ging. Andererseits haben wir unserer Anwendung durch die rollbaren und synchronisierten Ansichten, eine vollkommen andere Funktionalität verliehen.
Damit begeben wir uns in das letzte Kapitel unserer Einführung. Dort werden wir nur noch einige, wenige Änderungen vornehmen und Gebrauch von einigen neuen Methoden der KDE 2 Bibliotheken machen, doch wie immer, wird uns dies wieder eine interessante Funktionalität eröffnen - KScribble wird in der Lage sein, eine ganze Reihe von Bildformaten zu öffnen und zu speichern, und damit wird die Einschränkung, nur mit png Dateien arbeiten zu können, entfallen.
Weiter Zurück Inhaltsverzeichnis