The K Desktop Environment

Weiter Zurück Inhaltsverzeichnis

11. Erweiterte Ansichten

In diesem Kapitel werden wir die Funktionalität unseres Widgets durch zwei Verbesserungen erweitern: synchronisierte Ansichten und rollbare Ansichten.

11.1 Synchronisierung der 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.

11.2 Rollbare Ansichten

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:

Größe des Dokumentinhalts verändern

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.

11.3 Adaptieren der Ansicht

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