The K Desktop Environment

Next Previous Table of Contents

11. Extended Views

In this chapter we´re going to extend the functionality of our view widget by two enhancements: syncronized views and scrollviews.

11.1 Syncronizing Views

Let´s first explain what this will bring us and how we´re going to do it. While playing with KScribble, you may have noticed, that if you open another view of a document by calling "Window"->"New Window", this new view works with the same data as the first view, and does like any other view you create with that command. But when it comes to painting into the document, you can only do that in one view - the other views are not displaying the document contents at the same time. If you obscure one view that doesn´t contain the actual contents with another window and then bring it up to the front again, it will display the acutal contents. That comes because after a widget has been obscured and then activated again, it receives a paint event from the window system, which will call KScribbleView::paintEvent() again and that finally redraws the contents of the area that has been obscured. What we want to achieve is that all views should paint syncronous with the one the user actually paints to. In fact, you will see that this enhancement is a really easy task. The document class already provides us a method updateAllViews(), which calls the update() method on each view in the document´s view list. This makes it very easy to syncronize the document contents - every time the contents is changed, here by mouse movements (where we copy the changings to the buffer with bitBlt()), we just have to call updateAllViews(this). The this pointer is needed, because the calling view doesn´t need a repaint and the update() method is only executed if the sender view is not the same as it´s own.

What you´ve got to do here is only to call updateAllViews(this) at the end of the virtual methods mousePressEvent(), mouseMoveEvent() and mouseReleaseEvent() - and you´re done ! Take this as a general rule in your applications: each time the contents of the document is changed by a view, call updateAllViews(). How the update has to be executed has to be implemented in the widget´s update() method; one may be content by setting e.g. the changed text in an editor, in our application we just call repaint(), which generates a paint event and copies the contents of the document into the view again.

11.2 Scrolled Views

In this section we will add a functionality that is most often a thread to developers - if you can´t use an already implemented widget that provides the scrolling already. What does scrolling mean ? In our context, the problem begins where we want to open a picture that is bigger than a view can display. therefore, the result will be that you can only see as much as the view provides, beginning from the topleft corner; the rest will be cut away from the user´s view. A scrollview on the other hand is a widget that provides a scrollbar on the right side and on the bottom of the widget by which the user can "move" the contents. In fact, it shows the same size of the document contents, but the view area can be moved within the document, so each part can be displayed if the user wants to by moving the scrollbar sliders up and down, left and right. Fortunately, Qt provides a class QScrollView that itself inherits from QWidget and offers the same base functionality as an ordinary widget but manages the contents by scrollbars automatically - with the additional option that the programmer can either just use an instance of the QScrollView, create the child widgets to manage with the scrollview as parent and add them to the scrollview with addChild() or create a view by inheriting QScrollView and draw into the viewport, which is a defined area inside the scrollview, instead of directly to the widget. The difference here is that QScrollView provides a set of event handlers similar to the QWidget event handlers especially for the viewport. So what was formerly a mousePressEvent() in our view will become a viewportMousePressEvent, a paintEvent() will become a viewportPaintEvent etc. The second possibility will suite our needs to make KScribbleView a scrollable widget and so we will have to make the following modifications:

Sizing the Document Contents

As already mentioned, we have to set a size to the document contents as well as to initialize this size and provide a method to retrieve the size by the views. For this, we add a variable QSize size to KScribbleDoc as well as the method docSize():


kscribbledoc.h:

#include <qsize.h>

...
public:
  const QSize docSize(){ return size;};

private:
  QSize size;

Now we have to modify all methods that deal with initializing and opening the document contents - newDocument() and 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(), we initialize the size with a default value of 300 pixels wide and 200 pixels high. This is enough for a small picture for now and we could add a dialog for resizing as well if we want. When it comes to opening a picture, we have to set the size to the size of the picture. This can be done by calling QPixmap::size(), which we used in openDocument(). Then we´re done with setting the sizes and we can move on to reimplementing KScribbleView and make it a scrollview.

11.3 Adapting the View

As said above, we first have to change some things in the interface of KScribbleView. The following code shows these changings:


#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* );
}

Here, we changed the inheritance from QWidget to QScrollView first and added the according include file we need. Also we changed all implemented event handlers that deal with interaction on the contents of the scrollview to the according methods QScrollView provides for this purpose and commented out the resizeEvent. Now we can go over to the implementation of these methods and make use of the size our picture has. As a view is always created after the document exists, we can resize the widget directly in the constructor to fit this size and as well resize the contents (which is the viewport size):


#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);
}

Note that formerly, the resizeEvent() took care of resizing the drawing area to the same as the widget size. At the same time, this changed the document size as well, so the document picture had always the same size as the widget. With the already initialized size of the document (which we set in newDocument() and openDocument()), we just resize the contents by calling resizeContents() provided by QScrollView with the size of the document. You may also notice that we changed the cursor over the widget from the overall widget to the viewport widget, which we can retrieve with viewport(). Now we can reimplement the event handlers. At first, we should take care for the paintEvent, as this is one of the most important ones, because it gets called whenever the widget shows up or is resized.

Attention: take care to comment out the resizeEvent() implementation!

Now, the paint event will have to copy the pixmap in the buffer to the according position in the view. For this, we have to change the destination of bitBlt() from this to viewport(), set the topleft position to 0,0 and set the target (the buffer) to copy from the contentsX and contentsY position on into the viewport:


void KScribbleView::viewportPaintEvent( QPaintEvent *e )
{
  bitBlt( viewport(),0,0, &doc->buffer,contentsX() ,contentsY() );
}

The contentsX() thereby is the position in x-direction of the scrollview´s contents - which goes to position 0 in the viewport´s absolute position, which is the topleft point visible in the scrollview. The same applies to the y-direction. This part is sometimes hard to understand and you may have to do a bit "try and error" when implementing your own scrollviews. The other possible call of bitBlt() would be to switch the values of the positions and inverting the contents values:

bitBlt( viewport(), -contentsX(), -contentsY(), &doc->buffer, 0, 0 );

The last changes we need to do are changing the mouse event handlers. First, the mouseMoveEvent(), which changes to viewportMouseMoveEvent(), has a bitBlt() call as well. Here, we have to apply the same chages as in the paint event. Further, in the mousePressEvent() and the mouseMoveEvent(), we have retrieved the position of the mouse events with e->pos(). This position now will deliver us a widget position - not the contents position, so we have to translate this to draw into the correct position of the document with viewportToContents():


  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 the viewportMouseMoveEvent(), we had to change the destination again from this to viewport() - and with that translate the positions. This time, we used the second version of the call we used in viewportPaintEvent(), with subtracting the contentsX and contentsY values to copy the rectangle containing the current painting into the correct position of the viewport.

At last, we will apply a small change in conjunction with the update() method: why should we repaint the whole widget every time ? This will reduce performance most often and lead to a so-called "flicker" effect. This effect sometimes occurs with widgets, but there are some ways to reduce this behavoir. Instead of calling repaint(), we could call repaint(false) as well. This will not erase the widget contents before redrawing it. As we copy the document contents directly into the widget, we don´t need to erase it anyway, because all the data will be overwritten anyway. In conjunction with QScrollView, we will reduce the painting even more: we limit the update method to call repaint() on the viewport() widget, because that will call viewportPaintEvent(). On the other hand, the painting area we use is the rectangle containing the document contents, which, when the document size is smaller than the viewport size. So we can limit the paint event to the rectangle of the viewport where the document is displayed, whose visible width and height we can retrieve and compose to the rectangle. Additionally, we use the erase parameter with false, so the document area doesn´t get erased:


void KScribbleView::update(KScribbleView* pSender){
  if(pSender != this)
    viewport()->repaint(0,0,visibleWidth(), visibleHeight(), false);
}

Now you´re ready ! This chapter has been one of the hardest to implement and understand - especially when it comes to the geometries that change. But on the other hand, we gave our application a whole new functionality by the new scrollview and the syncronized views.

With that, we´re moving on to the last chapter of our tutorial. There, we will apply only a few changes by making use of some new methods of the KDE2 libraries, but as usual, this will bring us some interesting functionality - KScribble will be able to open and save a whole range of picture formats and thereby we will remove the restriction of operating only on the png file format.

Next Previous Table of Contents