KDE Anwendungs Tutorials: Erweiterte Ansichten

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:

- Zunächst müssen wir das Dokument der Größe seines Inhalts anpassen. Dies kann erreicht werden, indem wir uns die Größe des geöffneten Bildes holen, für ein neues Bild müssen wir eine Standardgröße definieren. In anderen Zeichenanwendungen kann diese Größe meistens durch ein Kommando des Benutzerinterfaces, z.B. in einem Dialog, der nach der neuen Höhe und Breite fragt, sowie einer Methode zur Anpassung des Bildes an diese Werte, geändert werden.
- Ändern der Elternklasse für KScribbleView von `QWidget` nach `QScrollView`.
- Umbenennen der virtuellen Methoden in die entsprechenden Viewportmethoden aus `QScrollView`.
- Adaptieren der virtuellen Event Handler, sodaß sie die Geometrie des Viewports beeinflussen. Das bedeutet, daß die alte Implementation auf die Geometrie von `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.

#### 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