In this chapter we´re going to extend the functionality of our view widget by two enhancements: syncronized views and scrollviews.
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 ifyou open another view of a document by calling "Window"->"New Window", this new view works with the same data as the first view, anddoes like any other view you create with that command. But when it comes to painting into the document, you can only do that in oneview - the other views are not displaying the document contents at the same time. If you obscure one view that doesn´t contain theactual contents with another window and then bring it up to the front again, it will display the acutal contents. That comes becauseafter a widget has been obscured and then activated again, it receives a paint event from the window system, which will callKScribbleView::paintEvent()
again and that finally redraws the contents of the area that has been obscured. What we want toachieve is that all views should paint syncronous with the one the user actually paints to. In fact, you will see that this enhancementis 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 contentsis changed, here by mouse movements (where we copy the changings to the buffer with bitBlt()
), we just have to callupdateAllViews(this). The this pointer is needed, because the calling view doesn´t need a repaint and the update()
method isonly 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: eachtime the contents of the document is changed by a view, call updateAllViews()
. How the update has to be executed has to beimplemented in the widget´s update()
method; one may be content by setting e.g. the changed text in an editor, in ourapplication we just call repaint()
, which generates a paint event and copies the contents of the document into the view again.
In this section we will add a functionality that is most often a thread to developers - if you can´t use an already implemented widgetthat provides the scrolling already. What does scrolling mean ? In our context, the problem begins where we want to open a picture thatis bigger than a view can display. therefore, the result will be that you can only see as much as the view provides, beginning from thetopleft 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 onthe 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 thedocument contents, but the view area can be moved within the document, so each part can be displayed if the user wants to by moving thescrollbar sliders up and down, left and right. Fortunately, Qt provides a class QScrollView
that itself inherits fromQWidget
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 widgetsto manage with the scrollview as parent and add them to the scrollview with addChild()
or create a view by inheritingQScrollView
and draw into the viewport, which is a defined area inside the scrollview, instead of directly to the widget. Thedifference here is that QScrollView
provides a set of event handlers similar to the QWidget
event handlers especially for theviewport. 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 wewill have to make the following modifications:
QWidget
to QScrollView
QScrollView
QWidget
starting at the topleft corner of a widget. If the view is scrolled and the topleft corner is not visible, wehave to ensure the positions retrieved from the QWidget
coordinates are translated to viewport coordinatesAs already mentioned, we have to set a size to the document contents as well as to initialize this size and provide a method toretrieve the size by the views. For this, we add a variable QSize size
to KScribbleDoc
as well as the methoddocSize()
:
kscribbledoc.h:#include <qsize.h>...public: const QSize docSize(){ return size;};private: QSize size;
newDocument()
andopenDocument()
:
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; }
newDocument()
, we initialize the size with a default value of 300 pixels wide and 200 pixels high. This is enough for asmall 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 callingQPixmap::size()
, which we used in openDocument()
. Then we´re done with setting the sizes and we can move on toreimplementing KScribbleView and make it a scrollview.
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* );}
QWidget
to QScrollView
first and added the according include file we need. Also we changed allimplemented event handlers that deal with interaction on the contents of the scrollview to the according methods QScrollView
providesfor this purpose and commented out the resizeEvent. Now we can go over to the implementation of these methods and make use of the sizeour picture has. As a view is always created after the document exists, we can resize the widget directly in the constructor to fitthis 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);}
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 initializedsize of the document (which we set in newDocument()
and openDocument()
), we just resize the contents by callingresizeContents()
provided by QScrollView
with the size of the document. You may also notice that we changed thecursor over the widget from the overall widget to the viewport widget, which we can retrieve with viewport()
. Now we canreimplement the event handlers. At first, we should take care for the paintEvent, as this is one of the most important ones, because itgets 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 thedestination of bitBlt()
from this to viewport()
, set the topleft position to 0,0 and set the target (the buffer) tocopy from the contentsX and contentsY position on into the viewport:
void KScribbleView::viewportPaintEvent( QPaintEvent *e ){ bitBlt( viewport(),0,0, &doc->buffer,contentsX() ,contentsY() );}
contentsX()
thereby is the position in x-direction of the scrollview´s contents - which goes to position 0 in theviewport´s absolute position, which is the topleft point visible in the scrollview. The same applies to the y-direction. This part issometimes hard to understand and you may have to do a bit "try and error" when implementing your own scrollviews. The other possiblecall 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 toviewportMouseMoveEvent()
, 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 withe->pos()
. This position now will deliver us a widget position - not the contents position, so we have to translate this todraw 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); } }
viewportMouseMoveEvent()
, we had to change the destination again from this to viewport()
- and with thattranslate the positions. This time, we used the second version of the call we used in viewportPaintEvent()
, with subtractingthe 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 everytime ? This will reduce performance most often and lead to a so-called "flicker" effect. This effect sometimes occurs with widgets, butthere are some ways to reduce this behavoir. Instead of calling repaint()
, we could call repaint(false)
as well. Thiswill not erase the widget contents before redrawing it. As we copy the document contents directly into the widget, we don´t need toerase it anyway, because all the data will be overwritten anyway. In conjunction with QScrollView
, we will reduce the paintingeven more: we limit the update method to call repaint()
on the viewport() widget, because that will callviewportPaintEvent()
. On the other hand, the painting area we use is the rectangle containing the document contents, which, when thedocument 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);}