Chapter 12 :- Drawing
In this chapter we take a closer look at drawing as it is kind of a compulsary subject for one of these things but one that most people hardly use in there careers. Drawing code can often be very long and look very complicated but as you've seen in various chapters in this book it isn't really as hard as it looks. By now you have already seen how to draw you own button and how to draw text to directly onto a form and in this chapter we will expand on what we have learned so far an add a few details about KDE while we do it.
KDE Colours
To start with we are going to look at how the KDE colour scheme works, as you'll see this is largely a function of the Qt libraries that KDE is built from. The main point of interest to start with is the class KGlobalSettings which you will find in the help. It works from the idea that you don't specify a specific colour to do your drawing but you select a system colour that then draws the in the selected colour. You saw this in the KSquareButton class with the code,
painter->setBrush( backgroundColor().dark() );
Which returns the background colour and then calls the QColor dark() function. With the KGlobalSettings class we can access the colours specified by the KDE system.
KDEChapterTwelveMenusRunning1.pngThe project starts with just six text labels on a form, it is a drawing example after all. These text labels are for the KDE system fonts so let's look at them first.
The code for the fonts is set up in the constructor so if you change the system fonts you will need to restart the application for the change. It is a pretty basic procedure to display the fonts. We assign a few strings to display and then get the fonts
QFont currentFont = KGlobalSettings::generalFont(); QFont fixedFont = KGlobalSettings::fixedFont(); QFont toolBarFont = KGlobalSettings::toolBarFont(); QFont menuFont = KGlobalSettings::menuFont(); QFont windowTitleFont = KGlobalSettings::windowTitleFont(); QFont taskBarFont = KGlobalSettings::taskbarFont(); QFont largeFont = KGlobalSettings::largeFont();
then set the fonts on the labels with the code,
currentFontLabel->setFont( currentFont ); fixedFontLabel->setFont( fixedFont ); toolBarFontLabel->setFont( toolBarFont ); menuFontLabel->setFont( menuFont ); windowTitleFontLabel->setFont( windowTitleFont ); taskBarFontLabel->setFont( taskBarFont ); largeFontLabel->setFont( largeFont );
by calling setFont on each label we get,
on my system. The system colours on the other hand are all shown during the paintEvent function and this at first looks quite complex. There are two aspects to this complexity in drawing code the first is the calculation code which at first glance can be quite confusing and the second is the wealth of unfamiliar functions and the number of parameters they take. So for this example there is precisely one drawing function that you haven't seen before and two font related functions which means that in about 150 lines of code there are about three lines that should be completely new, the rest is simple a case of wash, rinse and repeat.
We'll start with the basic maths which calulates where we are going to place the text and the rectangles that we are using to display the colours.
int nCenter = width() / 2; int nQuarter = nCenter / 2; int nThreeQuarters = nCenter + nQuarter; int nStartHeight = 240; int nStartWidth = 10; int nDefaultHeight = 20; int nRectWidth = nQuarter - 20; int nRectHeight = height() / 10; int nFirstRectLine = nStartHeight + ( nDefaultHeight *2 ); int nSecondTextHeight = nStartHeight + ( nDefaultHeight * 2 ) + nRectHeight + 20; int nSecondRectLine = nSecondTextHeight + nDefaultHeight; int nThirdTextHeight = nSecondTextHeight + ( nDefaultHeight * 2 ) + nRectHeight + 20; int nThirdRectLine = nThirdTextHeight + nDefaultHeight; int nFourthTextHeight = nThirdTextHeight + ( nDefaultHeight * 2 ) + nRectHeight + 20; int nFourthRectLine = nFourthTextHeight + nDefaultHeight;
Frightening huh? First we find a few basic positions on the widget.
int nCenter = width() / 2; int nQuarter = nCenter / 2; int nThreeQuarters = nCenter + nQuarter;
The nCenter variable is the exact center of the widget, the nQuarter is exactly half that and the nThreeQuarters is three quarter of the way across the widget. Now we have a few basic points lets define some defaults.
int nStartHeight = 240; int nStartWidth = 10; int nDefaultHeight = 20;
The nStartHeight is where we start drawing text below the text labels for the fonts. The nStartWidth gives us a little space from the left hand edge of the widget and the nDefaultHeight is the value we will use as spacer between the text and the rectangles we are going to draw.
The final variables we set with are original values are
int nRectWidth = nQuarter - 20; int nRectHeight = height() / 10;
These define the height and the width of the rectangles that we are going to draw. The variables are defined like this so that the rectangles that we draw are based on the size of the widget that we are drawing to, this means that if you resize the program the rectangles will resize along with the widget.
The variables,
int nFirstRectLine = nStartHeight + ( nDefaultHeight *2 ); int nSecondTextHeight = nStartHeight + ( nDefaultHeight * 2 ) + nRectHeight + 20; int nSecondRectLine = nSecondTextHeight + nDefaultHeight; int nThirdTextHeight = nSecondTextHeight + ( nDefaultHeight * 2 ) + nRectHeight + 20; int nThirdRectLine = nThirdTextHeight + nDefaultHeight; int nFourthTextHeight = nThirdTextHeight + ( nDefaultHeight * 2 ) + nRectHeight + 20; int nFourthRectLine = nFourthTextHeight + nDefaultHeight;
are all set to variables that we have already initialised and calulate the heights to start drawing the text and the rectangles.
Once we have set up the variables we need to set the title for this section on the widget.
QFontMetrics generalFontMetrics( KGlobalSettings::generalFont() ); int nGeneralFontWidth = generalFontMetrics.width( strTitle ); painter.drawText( nCenter - ( nGeneralFontWidth / 2 ), 50, strTitle );
We use the QFontMetrics class to get the information for the current font which is the standard or default font which is returned by calling KGlobalSettings::generalFont(). Once we have got the font information we can calculate the exact width of the string we are using for the title by passing it to the QFontMetrics width function. Then we draw the string to the center of the widget using drawText.
The rest of the function is a repetition of this code,
painter.drawText( nStartWidth, nStartHeight, strColours ); painter.drawText( nStartWidth, nStartHeight + nDefaultHeight, strToolBarHighlightColour ); painter.fillRect( nStartWidth, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::toolBarHighlightColor() ) ); int nFirstFontWidth = generalFontMetrics.width( strToolBarHighlightColour ) + 20; if( nFirstFontWidth > nQuarter ) { painter.drawText( nFirstFontWidth, nStartHeight + nDefaultHeight, strInactiveTitleColour ); painter.fillRect( nFirstFontWidth, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::inactiveTitleColor() ) ); } else { painter.drawText( nQuarter, nStartHeight + nDefaultHeight, strInactiveTitleColour ); painter.fillRect( nQuarter, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::inactiveTitleColor() ) ); } int nSecondFontWidth = nFirstFontWidth + generalFontMetrics.width( strInactiveTextColour ) + 20; if( nSecondFontWidth > nCenter ) { painter.drawText( nSecondFontWidth, nStartHeight + nDefaultHeight, strInactiveTextColour ); painter.fillRect( nSecondFontWidth, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::inactiveTextColor() ) ); } else { painter.drawText( nCenter, nStartHeight + nDefaultHeight, strInactiveTextColour ); painter.fillRect( nCenter, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::inactiveTextColor() ) ); } int nThirdFontWidth = nSecondFontWidth + generalFontMetrics.width( strActiveTitleColour ) + 20; if( nThirdFontWidth > nThreeQuarters ) { painter.drawText( nThirdFontWidth, nStartHeight + nDefaultHeight, strActiveTitleColour ); painter.fillRect( nThirdFontWidth, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::activeTitleColor() ) ); } else { painter.drawText( nThreeQuarters, nStartHeight + nDefaultHeight, strActiveTitleColour ); painter.fillRect( nThreeQuarters, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::activeTitleColor() ) ); }
This page of code is repeated four times with the only changes being the string that is output and the colour of the rectangles being drawn. Actually if you looked closely at the code above you'll notice that this page itself draws the first line of rectangles and is made up of this code,
painter.drawText( nStartWidth, nStartHeight + nDefaultHeight, strToolBarHighlightColour ); painter.fillRect( nStartWidth, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::toolBarHighlightColor() ) ); int nFirstFontWidth = generalFontMetrics.width( strToolBarHighlightColour ) + 20; if( nFirstFontWidth > nQuarter ) { painter.drawText( nFirstFontWidth, nStartHeight + nDefaultHeight, strInactiveTitleColour ); painter.fillRect( nFirstFontWidth, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::inactiveTitleColor() ) ); } else { painter.drawText( nQuarter, nStartHeight + nDefaultHeight, strInactiveTitleColour ); painter.fillRect( nQuarter, nFirstRectLine, nRectWidth, nRectHeight, QBrush( KGlobalSettings::inactiveTitleColor() ) ); }
This code draws the label and the first rectangle, before drawing the label and second rectangle. It starts by drawing the text for the first label at the correct width and height and then draws the rectangle directly below it. The second rectangle is drawn differently because we have to first of all calculate where we are going to draw it. We do this by getting the width of the string that labels the rectangle and seeing if it is larger than a quarter of the way across the screen, the reason for this being that if the screen is too small for the text it will overlap and it looks better if we draw outside the bounds of the widget than it does if we try to squash everything in on itself. So if the width of the string is greater we draw it regardless if not we draw it at the required location which in this case is nQuarter in the next case it will be nCenter and in the final case it will be nThreeQuarters.
The end result is that when we run the program we get.
When we run it with the KDE default colours. If we go to the main menu and select Control Center/Appearance and Themes/Colors and select Blue Slate we get
Remember though that as the fonts are set during the constructor you will need to stop and restart the program before you will notice any changes to them. The colours will change whenever the colours are reset and you set the focus to the application which will force it to redraw thus changing the colours.
Tip Of the Day
Probably the most important thing to remember when painting to a widget is never call a function that is going to issue a repaint to the widget. This is the reason why the fonts are set up in the constructor as changing the font forces the widget to repaint itself so drawing the fonts by hand on the widget and then changing the drawing font for each one just means that the widget is constantly getting told to redraw itself. Actually as the different fonts are displayed in labels it would be perfectly acceptable to move them into the paint function and have them update at the same time as the coloured rectangles because the paint messages would be issued to the labels and not to the widget itself.
Drawing Example
In every book like this it is almost an unwritten rule that there will be some mostly pointless drawing example that you will never ever need to write in the real world. The reason these are included is because when you do need to do this sort of thing it can be a complete pain to get right and it does help to give an understanding of how the programs are working. So in the spirit of things we are going to look at some drawing examples the first being the ChapterTwelveDrawing example. This is a deliberately restricted example for two reasons, one I wanted to include owner draw menus and because I use them the design doesn't scale up into a completely workable application and two I wanted to show an example that displays the mechanics of how these work better than I have seen in some other beginners books. This is not to say that the other books are wrong it is just that the mechanics of how things work tend to get lost in the complications of getting everything working correctly.
So the plan is this with the first program we will concentrate on how to get the basics working and in the next program we will aim for a more functional application.
The first program is the ChapterTwelveDrawing example and it looks like this,
It is a plain and simple dialog to start with because we are going to use the surface to draw on and don't want to clutter it up with widgets. The idea here is that we have a minimal interface and do eveything through a single popup menu that is activated when we right click on the qwidget. You have seen a popup menu before but not one quite like this for a start it looks like,
and
Yes it is a single menu with the display being chosen by the check boxes on the menu. Admittedly this could have been better implemented as two seperate menus but the idea is to show what can be done here, not what is necessarily best from a design point of view. Of course the first thing that should strike you about the menus is that apart from the check box widgets they are entirely owner drawn so let's have a look at how it's done.
Owner Draw Menus.
We've already seen how versatile popup menu's can be and a quick glance at the help page for KPopupMenu and/or QPopupMenu shows us that we can use pictures as menu items but what if we want to be more expressive and do something that isn't already catered for in that case we can use the QCustomMenuItem class. This class is the class object that is used as a single menu item. The QCustomMenuItem class contains all the information required to qualify as a menu item while allowing us to specify exactly what happens when the menu item is drawn. So given the two views of the menu above we need six distinct classes. Five classes are to draw the shapes and one class to draw the coloured rectangles.
You should note that the check boxes are standard check boxes added in the normal way with,
coloursCheckBox = new QCheckBox( popupMenu, "Colours" ); coloursCheckBox->setText( "Colours" ); coloursCheckBox->setChecked( false ); popupMenu->insertItem( coloursCheckBox, ColoursCheckBoxItem );
The signals are then connected using,
connect( coloursCheckBox, SIGNAL( clicked() ), this, SLOT( coloursClicked() ) );
To start with the standard rectangle we would define the class as,
class KRectMenuItem : public QCustomMenuItem { public: KRectMenuItem(); virtual ~KRectMenuItem(); virtual void paint( QPainter *p, const QColorGroup& cg, bool act, bool enabled, int x, int y, int w, int h ); virtual QSize sizeHint(); };
As you can see the KRectMenuItem contains only a constructor, a destructor and the two functions required to control the drawing, these being the sizeHint function which reserves space in the menu and the paint function. The important one for us being the paint function,
void KRectMenuItem::paint( QPainter *p, const QColorGroup& /*cg*/, bool /*act*/, bool /*enabled*/, int x, int y, int w, int h ) { p->drawRect( x, y, w*2, h-2 ); }
When the paint function is called it passes more than enough information than we need here for what we are trying to do. We are given a valid painter to the menu item that we are painting and the colours available in the QColorGroup object. The active and the enabled parameters are the menus status while the x and y are the starting point for our menu item, with the w being the width and the h being the height. In all the calculations for the owner draw menu items shown here we use w*2 as we are drawing over the space reserved for the short cut keys as well.
Given this information we can see that in order to draw an owner drawn round rect we need to declare an identical class and in the constructor use the function.
p->drawRoundRect( x, y, w*2, h-2 );
and for the ellipse,
p->drawEllipse( x, y, w*2, h-2 );
In order to draw the KLineMenuItem we have to be a little more creative,
p->drawLine( x, y+10, x+( w*2 ), y+10 );
increasing the height given in the y parameter to center the line drawing and then using x+( w*2 ) to specify exactly where we want the line drawing to. With the KTriangleMenuItem we have to be a lot more creative,
QPointArray points; points.setPoints( 4, x + w, y, x, y + ( h-2 ), x + ( w*2 ), y + ( h-2 ), x + w, y ); p->drawPolyline( points );
Here we need to create a QPointsArray and then declared four distinct points, these being x+w, y which is exactly halfway across the top of the drawing space at the height of y, then x, y + ( h -2 ), which is the point at the x at the height of y + ( h-2 ) which in English translates to the bottom left hand corner as you look at it. Then the next point is the bottom right hand corner as you look at it which translates to the code x + ( w*2 ), y + ( h-2 ), with the final point being back at the top in the middle of the menu item or x + w, y. once the points are set we pass the points array to the drawPolyline function.
The KColourRectMenuItem is dealt with in exactly the same way first we define the class as,
class KColourRectMenuItem : public QCustomMenuItem { public: KColourRectMenuItem(); KColourRectMenuItem( QColor colour ); virtual ~KColourRectMenuItem(); virtual void paint( QPainter *p, const QColorGroup& cg, bool act, bool enabled, int x, int y, int w, int h ); virtual QSize sizeHint(); private: QColor cColour; };
Then the paint function contains the code,
p->fillRect( x, y, w*2, h-2, QBrush( cColour ) );
which draw a rectangle the same as the drawRect function and fills it with the passed in colour in a standard brush. The standard brush is a solid pattern to see the available brush styles see the QBrush reference documentation.
Once they are created the new custom menu items are added to the popup menu exactly as you would expect.
rectMenuItem = new KRectMenuItem(); popupMenu->insertItem( rectMenuItem, RectMenuItem ); roundRectMenuItem = new KRoundRectMenuItem(); popupMenu->insertItem( roundRectMenuItem, RoundRectMenuItem ); ellipseMenuItem = new KEllipseMenuItem(); popupMenu->insertItem( ellipseMenuItem, EllipseMenuItem );
Each menu item is created and then added to the QPopupMenu with the insertItem function which takes the newly created menu item and an identifier that is returned when a menu item is selected on the popup menu.
The identifiers are defined in the ChapterTwelveDrawingWidget.h class file as,
enum PopupMenuIdentifiers{ ShapesCheckBoxItem, ColoursCheckBoxItem, RectMenuItem, RoundRectMenuItem, EllipseMenuItem, LineMenuItem, TriangleMenuItem, BlackColourMenuItem, WhiteColourMenuItem, DarkGreyColourMenuItem, GreyColourMenuItem, LightGreyColourMenuItem, RedColourMenuItem, GreenColourMenuItem, BlueColourMenuItem, CyanColourMenuItem, MagentaColourMenuItem, YellowColourMenuItem, DarkRedColourMenuItem, DarkGreenColourMenuItem, DarkBlueColourMenuItem, DarkCyanColourMenuItem, DarkMagentaColourMenuItem, DarkYellowColourMenuItem };
These are used not only to identify what colour or shape has been selected but also to set what is being displayed, so initially the colour menu items are set up as.
blackRectMenuItem = new KColourRectMenuItem(); popupMenu->insertItem( blackRectMenuItem, BlackColourMenuItem ); popupMenu->setItemVisible( BlackColourMenuItem, false ); whiteRectMenuItem = new KColourRectMenuItem( Qt::white ); popupMenu->insertItem( whiteRectMenuItem, WhiteColourMenuItem ); popupMenu->setItemVisible( WhiteColourMenuItem, false );
As you can see we add the colour menu items to the QPopupMenu but set them to be hidden by default and then we use the check boxes to set which menu items we are going to see.
void ChapterTwelveDrawingWidget::coloursClicked() { bColours = true; popupMenu->changeTitle( 1, "Colours" ); // remove the shapes popupMenu->setItemVisible( RectMenuItem, false ); popupMenu->setItemVisible( RoundRectMenuItem, false ); popupMenu->setItemVisible( EllipseMenuItem, false ); popupMenu->setItemVisible( LineMenuItem, false ); popupMenu->setItemVisible( TriangleMenuItem, false );
With the above code showing the start of the coloursClicked function which hides all the shape menu items and unhides all the colour menu items.
Drawing The Shapes
In order that we can draw the shapes on the widget we must first track them when they are drawn by the user of the application. This is done by installing and eventFilter,
installEventFilter( this );
and the capturing the mouse button presses and movements.
bool ChapterTwelveDrawingWidget::eventFilter( QObject *watched, QEvent *e ) { if( watched == this ) { if( e->type() == QEvent::MouseButtonPress ) { } if( e->type() == QEvent::MouseButtonRelease ) { } if( e->type() == QEvent::MouseMove ) { } } return false; }
As you can see from the skeleton code above we are interested in the button press the button release and the mouse move events. The button press events are used to control the popup menu and to start the drawing code. The code is,
QMouseEvent *mouseEvent = static_cast< QMouseEvent * >( e ); if( mouseEvent->button() == Qt::LeftButton ) { bDrawOutline = true; startPosition = mouseEvent->pos(); } if( mouseEvent->button() == Qt::RightButton ) { DisplayPopup(); } return true;
Pressing the right mouse button on the widget calls the DisplayPopup function which displays the popup menu with it's custom objects,
nPopupReturn = popupMenu->exec( QCursor::pos() ); /// selected a shape if( bColours == false ) { nDrawItem = nPopupReturn; } else /// selected a colour { switch( nPopupReturn ) { case BlackColourMenuItem : cDrawingColour = Qt::black; break; case WhiteColourMenuItem : cDrawingColour = Qt::white; break; case DarkGreyColourMenuItem : cDrawingColour = Qt::darkGray; break; case GreyColourMenuItem : cDrawingColour = Qt::gray; break; case LightGreyColourMenuItem : cDrawingColour = Qt::lightGray; break; case RedColourMenuItem : cDrawingColour = Qt::red; break; case GreenColourMenuItem : cDrawingColour = Qt::green; break; case BlueColourMenuItem : cDrawingColour = Qt::blue; break; case CyanColourMenuItem : cDrawingColour = Qt::cyan; break; case MagentaColourMenuItem : cDrawingColour = Qt::magenta; break; case YellowColourMenuItem : cDrawingColour = Qt::yellow; break; case DarkRedColourMenuItem : cDrawingColour = Qt::darkRed; break; case DarkGreenColourMenuItem : cDrawingColour = Qt::darkGreen; break; case DarkBlueColourMenuItem : cDrawingColour = Qt::darkBlue; break; case DarkCyanColourMenuItem : cDrawingColour = Qt::darkCyan; break; case DarkMagentaColourMenuItem : cDrawingColour = Qt::darkMagenta; break; case DarkYellowColourMenuItem : cDrawingColour = Qt::darkYellow; break; }; }
The result is the stored in the a class variable either the nDrawItem or the cDrawingColour depending on which section of the menu is being displayed.
Pressing the left mouse button doesn't call any functions it simply sets the boolean variable bDrawOutline to true and stores the current mouse position. In order to make something happen the user has to press the left mouse button and then move the mouse whilst holding the button down, thus giving us the MouseMove event which we respond to with the code,
if( bDrawOutline == true ) { QMouseEvent *mouseEvent = static_cast< QMouseEvent * >( e ); DrawOutLine( mouseEvent->pos() ); } return true;
The DrawOutLine function draws a dotted outline of the currently selected shape so the user can see the size and position,
QPainter painter( this ); int nWidth = currentPos.x() - startPosition.x(); int nHeight = currentPos.y() - startPosition.y(); QRect rect( startPosition.x(), startPosition.y(), nWidth, nHeight ); if( nDrawItem == LineMenuItem ) { painter.eraseRect( rect ); repaint( rect ); } else { repaint( rect ); } painter.setPen( Qt::DotLine ); switch( nDrawItem ) { case RectMenuItem: { painter.drawRect( rect ); painter.drawRect( startPosition.x(), startPosition.y(), nWidth, nHeight ); }; break; case RoundRectMenuItem: { painter.drawRect( rect ); painter.drawRoundRect( rect ); }; break; case EllipseMenuItem: { painter.drawRect( rect ); painter.drawEllipse( rect ); }; break; case LineMenuItem: { painter.drawLine( startPosition.x(), startPosition.y(), currentPos.x(), currentPos.y() ); }; break; case TriangleMenuItem: { painter.drawRect( rect ); QPointArray points; points.setPoints( 4, startPosition.x() + nWidth/2, startPosition.y(), startPosition.x(), startPosition.y() + ( nHeight-2 ), startPosition.x() + ( nWidth ), startPosition.y() + ( nHeight-2 ), startPosition.x() + nWidth/2, startPosition.y() ); painter.drawPolyline( points ); }; break; }
We start by calculating the current rectangle from the start position which was saved when the mouse button was first pressed down and then we erase that rectangle on the widget with a call to repaint. This erases the old dotted line drawing for the shape we are drawing so that we can then redraw the it with the new size. To see how this works properly comment out the repaint and run the program then. You can see that the line drawing function is treated differently this is because of how we are trying to reduce the amount of drawing that we are doing at anyone point, in order to reduce screen flicker. There are other, better ways of reducing screen flicker that we will look at shortly.
This is what drawing a triangle will look like at the start.
And this drawing a triangle over other shapes,
The shape itself is not drawn until after the mouse button is released. The code executed on the MouseButtonRelease event is,
QMouseEvent *mouseEvent = static_cast< QMouseEvent * >( e ); if( mouseEvent->button() == Qt::LeftButton ) { bDrawOutline = false; /// save the current shape and repaint the drawing area KShape save; if( nDrawItem != LineMenuItem ) { save.setX( startPosition.x() ); save.setY( startPosition.y() ); save.setWidth( mouseEvent->x() - startPosition.x() ); save.setHeight( mouseEvent->y() - startPosition.y() ); save.setShape( nDrawItem ); save.setColour( cDrawingColour ); } else { save.setX( startPosition.x() ); save.setY( startPosition.y() ); save.setWidth( mouseEvent->x() ); save.setHeight( mouseEvent->y() ); save.setShape( nDrawItem ); save.setColour( cDrawingColour ); } shapeList.append( save ); repaint(); return true;
All the MouseButtonRelease does is save the current shape and position, with the extra code being to handle the line where we are saving specific points and not the whole rectangle which is required to draw the other shapes. The shape details are saved in the KShape class which as we have seen previously is a simple data class that looks like,
class KShape { public: KShape(); KShape( int x, int y, int width, int height, int shape, QColor colour ); virtual ~KShape(); inline int x(){ return nX; }; inline void setX( int x ){ nX = x; }; inline int y(){ return nY; }; inline void setY( int y ){ nY = y; }; inline int width(){ return nWidth; }; inline void setWidth( int width ){ nWidth = width; }; inline int height(){ return nHeight; }; inline void setHeight( int height ){ nHeight = height; }; inline int shape(){ return nShape; }; inline void setShape( int shape ){ nShape = shape; }; inline QColor colour(){ return cColour; }; inline void setColour( QColor colour ){ cColour = colour; }; private: int nX; int nY; int nWidth; int nHeight; int nShape; QColor cColour; };
As previously the data class is then stored in a collection which is defined as,
typedef QValueList< KShape > ShapeList;
The shapes are then appended to the collection and the MouseButtonRelease code then issues a repaint call which will issue a paintEvent to the widget.
The paintEvent function is then pretty much what we should expect,
void ChapterTwelveDrawingWidget::paintEvent( QPaintEvent */*paintEvent*/ ) { QPainter painter( this ); /// draw the saved shapes ShapeList::iterator it; KShape shape; painter.setPen( Qt::SolidLine ); for( it = shapeList.begin(); it != shapeList.end(); it++ ) { shape = ( KShape )( *it ); painter.setBrush( shape.colour() ); painter.setPen( shape.colour() ); switch( shape.shape() ) {
We set the current pen to a solid line and then iterate through ShapeList collection of KShapes using the switch statement to identify which shape is to be drawn and then drawing it in the currently selected colour.
We can then draw things like,
if you really want to.
Menus And Double Buffering
In the previous example the demonstration code had obvious limitations which were largely caused by the way in which the menus for the application were chosen, using only the popup menus it was clear that trying to implement more options would have led to a more and more unmanageable menu system that would just get confusing and irritating for the user. So for the next application we will add the menus by hand to see what is required when we want to write an application that looks like a proper program. Another problem is that the more images you draw on the widget the more the application flickers when the widget is drawn, although this has been kept to a minimum it is still noticable and basically no flicker is good. So the next program the ChapterTwelveMenus project also includes double buffering in such a way that you can run the program and turn it on and off to see the improvement for yourself. But first to the menus.
Menus
The first thing you should notice about the ChapterTwelveMenus project is that this is the point where we start to write applications that look like real applications, by having menus etc. The project looks like,
For the purposes of this project we have four menu options and we add them all to the project by hand. To do this we need these objects,
KMenuBar *menuBar; KColoursMenu *coloursMenu; KDrawingMenu *drawingMenu; KPopupMenu *viewPopup; KPopupMenu *filePopup;
This introduces three new classes that need a little explanation firstly the KColoursMenu and the KDrawingMenu classes are classes that I have written to encapsulate what we have already done before in that they control the options for the colours and the drawing options from the previous ChapterTwelveDrawing project. The header for the KColoursMenu class looks like this,
class KColoursMenu : public KPopupMenu { public: KColoursMenu(); ~KColoursMenu(); inline void setDrawingColour( QColor colour ){ cDrawingColour = colour; }; inline QColor drawingColour(){ return cDrawingColour; }; private: KColourRectMenuItem *blackRectMenuItem; KColourRectMenuItem *whiteRectMenuItem; KColourRectMenuItem *darkGreyRectMenuItem; KColourRectMenuItem *greyRectMenuItem; KColourRectMenuItem *lightGreyRectMenuItem; KColourRectMenuItem *redRectMenuItem; KColourRectMenuItem *greenRectMenuItem; KColourRectMenuItem *blueRectMenuItem; KColourRectMenuItem *cyanRectMenuItem; KColourRectMenuItem *magentaRectMenuItem; KColourRectMenuItem *yellowRectMenuItem; KColourRectMenuItem *darkRedRectMenuItem; KColourRectMenuItem *darkGreenRectMenuItem; KColourRectMenuItem *darkBlueRectMenuItem; KColourRectMenuItem *darkCyanRectMenuItem; KColourRectMenuItem *darkMagentaRectMenuItem; KColourRectMenuItem *darkYellowRectMenuItem; QColor cDrawingColour; };
As you can see it simple encapsulates the colour items from the previous example and the implementation is identical also with the constructor containing code along the lines of,
blackRectMenuItem = new KColourRectMenuItem(); insertItem( blackRectMenuItem, BlackColourMenuItem ); whiteRectMenuItem = new KColourRectMenuItem( Qt::white ); insertItem( whiteRectMenuItem, WhiteColourMenuItem ); darkGreyRectMenuItem = new KColourRectMenuItem( Qt::darkGray ); insertItem( darkGreyRectMenuItem, DarkGreyColourMenuItem );
As you can imagine the KDrawingMenu is likewise an implementation of the Drawing options available in the previous project.
The KMenuBar on the other hand is completely new and draws a blank menu for us across the top of the widget. We do this with the code,
menuBar = new KMenuBar( this );
The way the menus work is that you start out with a KMenuBar and then you add the popup menus to it and then add the actual menu options to the popupmenu, so with the file menu we add the file KPopupMenu object filePopup with the code,
filePopup = new KPopupMenu(); menuBar->insertItem( strFile, filePopup );
Actions
The preferred way of doing things in KDE is with actions. An action is an object set up to define a menu option and what image and text it shows. The action is then added to the menu or the toolbar and yes the same action object can be used for both. A call to connect is then used to define what happens when that action is chosen as a menu or toolbar option. We will see how they work with toolbars later but for now we will just define them to work with a menu. So taking the first one first the File New menu option is set up with the code.
fileNewAction = new QAction( strFileNew, 0, filePopup ); fileNewAction->setIconSet( BarIconSet( "filenew.png" ) ); fileNewAction->setStatusTip( strFileNewStatusTip ); connect( fileNewAction, SIGNAL( activated() ), this, SLOT( fileNew() ) ); fileNewAction->addTo( filePopup );
To start with we create the fileNewAction object giving it the string “File New”, we don't specify a keyboard accelerator but we do specify the filePopup menu as the parent.
We then set the icon using the BarIconSet function which is a function exported by the KDE core library and is documented as a related function of KIconLoader. This function gets the icon file named and displays it on the menu. On Suse 10 the icons are to be found in root/opt
The default KDE icons are shown here but the BarIconSet function will get whatever files you have selected in the Control Center/Apperance And Themes/Icons section. Next we connect the actions activated signal to our fileNew function before adding the action to the filePopup menu.
The rest of the file menu is set up in exactly the same way, create the Action setting the text in the constructor and then adding the icon for the menu item before setting the tool tip and then connecting it to a function.
The view menu is slightly different in that the view menu contains two items,
These both work as check box options that are used within the code, we'll look at what they do later first of all you set up a check box menu item with the code,
viewPopup = new KPopupMenu(); menuBar->insertItem( strView, viewPopup ); doubleBufferAction = new QAction( strDoubleBuffer, 0, viewPopup ); doubleBufferAction->setToggleAction( true ); doubleBufferAction->setOn( true ); doubleBufferAction->setStatusTip( strDoubleBufferStatusTip ); connect( doubleBufferAction, SIGNAL( toggled( bool ) ), this, SLOT( toggleDoubleBuffer( bool ) ) ); doubleBufferAction->addTo( viewPopup );
We set up the menu the same as previously by creating a KPopupMenu item and adding it to the menu bar, then we call the setToggleAction function on the action to true and in the case of the double buffer action set it to true by default.
For the colours and the drawing menus there is a difference as we have already created the classes to hold the menus.
coloursMenu = new KColoursMenu(); menuBar->insertItem( i18n( "Colours" ), coloursMenu ); coloursAction = new QAction( coloursMenu, "ColoursAction" ); connect( coloursMenu, SIGNAL( activated( int ) ), this, SLOT( coloursClicked( int ) ) );
Here we create the entire menu as one object and as it is derived from KPopupMenu we insert it into the QMenuBar as normal. The difference is that when we create the action we create it for the whole menu not for each specific menu option which means that the activated function will be fired whenever an itme on the menu is clicked which works exactly the way it would if we were using a standard popup menu so the code that is called is the repeated code,
switch( id ) { case BlackColourMenuItem : coloursMenu->setDrawingColour( Qt::black ); break; case WhiteColourMenuItem : coloursMenu->setDrawingColour( Qt::white ); break; case DarkGreyColourMenuItem : coloursMenu->setDrawingColour( Qt::darkGray ); break; case GreyColourMenuItem : coloursMenu->setDrawingColour( Qt::gray ); break; case LightGreyColourMenuItem : coloursMenu->setDrawingColour( Qt::lightGray ); break; case RedColourMenuItem : coloursMenu->setDrawingColour( Qt::red ); break; case GreenColourMenuItem : coloursMenu->setDrawingColour( Qt::green ); break; case BlueColourMenuItem : coloursMenu->setDrawingColour( Qt::blue ); break; case CyanColourMenuItem : coloursMenu->setDrawingColour( Qt::cyan ); break; case MagentaColourMenuItem : coloursMenu->setDrawingColour( Qt::magenta ); break; case YellowColourMenuItem : coloursMenu->setDrawingColour( Qt::yellow ); break; case DarkRedColourMenuItem : coloursMenu->setDrawingColour( Qt::darkRed ); break; case DarkGreenColourMenuItem : coloursMenu->setDrawingColour( Qt::darkGreen ); break; case DarkBlueColourMenuItem : coloursMenu->setDrawingColour( Qt::darkBlue ); break; case DarkCyanColourMenuItem : coloursMenu->setDrawingColour( Qt::darkCyan ); break; case DarkMagentaColourMenuItem : coloursMenu->setDrawingColour( Qt::darkMagenta ); break; case DarkYellowColourMenuItem : coloursMenu->setDrawingColour( Qt::darkYellow ); break; };
from the ChapterTwelveDrawing project.
Saving and Loading QPixmaps
The project is capable of loading and saving files of the type .png through the file menu and it does through the KFileDialog to set the name of the file and by calling the QPixmap save and load functions, the restriction to .png files is largely laziness on my part. So the save function is,
setDontDrawOnSave( true ); QString strOpenFile = KFileDialog::getSaveFileName( strCurrentFile, "*.png|PNG Files"); qpBufferPixmap.save( strOpenFile, "PNG" ); /// NEEDS TO BE UPPERCASE; setLoadedPixmap( true ); qpLoadedPixmap = qpBufferPixmap; shapeList.clear(); setDontDrawOnSave( false ); issueRepaint();
The main lines here are the second and third lines which calls the KFileDialog getSaveFileName and the QPixmap save function, that is all there is to it really the rest of the code is concerned with making sure that the drawing is done properly when the paint function called. The setDontDrawOnSave function simply tells the program not to try and draw the picture until it is ready and the setLoadedPixmap to true tells the code to draw the saved picture as the background to the image and so clears the shapeList as we don't need it any more.
Double Buffering
The project has been specifically written to show the effects of double buffering so it allows you to turn double buffering on and off in the view menu. Double buffering is itself an attempt to reduce screen flicker when drawing by drawing as little to the screen as possible. You can get really complicated with this and draw the screen in sections but we are just going to use a single double buffer that draws to the whole screen. The idea is that when you draw to the screen what you in effect drawing is a picture, it doesn't matter if it's a picture of a mountain, a photo of someone you know or an on screen button to the computer screen it is an image to be displayed. The problem is that when you are constantly drawing to the screen the screen flickers as you draw over the same pixels again and again so what you do is draw the image off screen to a blank page, or in this case a QPixmap object, and then just draw the one image to the screen as quickly as you can. So in the header for the ChapterTwelveMenusWidget.h we have,
/// Double Buffering /// QPixmap qpBufferPixmap; bool bDoubleBuffer;
the idea being that when you set the double buffer to true in the view menu the shapes you draw on the screen are drawn to the qpBufferPixmap which is then drawn to the screen. This means that the paintEvent function now contains code like,
QPainter painter( this ); QPainter bufferPainter( &qpBufferPixmap ); ... case RoundRectMenuItem: { if( doubleBuffer() == true ) { bufferPainter.drawRoundRect( shape.x(), shape.y(), shape.width(), shape.height() ); } else painter.drawRoundRect( shape.x(), shape.y(), shape.width(), shape.height() ) ; }; break;
Which sets up two painter objects one to the widget and one to the buffer pixmap and draws to whichever one is required.
The paintEvent function now ends with the code,
/// draw the doubleBuffer pixmap to the widget /// if( doubleBuffer() == true ) bitBlt( this, paintEvent->rect().topLeft(), &qpBufferPixmap, paintEvent->rect() );
which draws the buffer pixmap to the screen if the double buffer option is set.
As the double buffering option is the default, the project starts up with it switched on yet you can use the option in the view menu to turn it off. This will allow the program to work without double buffering and enable the menu drawing fix option on the view menu. This option is a quick fix to draw over the menu images by blanking out the widget before it is redrawn, this is not a good idea to do it this way if you are trying to reduce flicker.
Enabling and disabling a menu is simply a call to,
menuDrawingFixAction->setEnabled( false );
using true to enable and false to disable.
Adding A Main Window
If we are ever going to consider writing so called proper applications then we need to use a proper main window that contains a widget, a toolbar and a status bar as well as a menu, sure we can get away without these things for small applications but eventually there comes a time when a small application is well, just not good enough to do what we want.
Unfortuanetly at the moment ( Suse 10 ) there is no way to create an application in KDevelop from scratch that uses a main window. You could use a Simple KDE Application project but there is no .ui file to edit with it so it you don't want to write all the gui code by hand then you will need to add a main window to the project in exactly the same way that we are going to add a main window to the standard Simple Designer Based KDE Application that we have been using all along and will continue to use for the time being.
First of all we create the ChapterTwelveMainWindow application the same way as before, only this time we don't need to delete the generated hello world code as we wont be using it anyway. Open the New File tab,
Like so, and click on the Main Window option which will give you this dialog,
So we call the File the ChapterTwelveMainWindow, which when we click O.K. opens the automake manager dialog,
Just click O.K. for this dialog. Now if you click on the ChapterTwelvemainWindow.ui file that is added to the Automake Manager then you will see,
the action editor. We looked at actions recently and this is just a visual way of implementing them.
Right clicking on the Action Editor gives to the options dialog,
What happens here is that you create menus in the traditional windows style way by clicking on the “new menu” option on the menu itself and then typing in the name of the menu,
and to add menu items you do exactly the same thing on the menu.
Positioning of the menus and the items on the menus is done by dragging them around the menu or menubar. So with a bit of practice we get,
It should be noted that the toggle action property shows up in the object editor so there is no problem setting the Double buffer and the Menu Item Fix options as check boxes the way they were in the previous program. The Shapes and the Colours Menu will have to be added to the menus by hand as they were in the ChapterTwelveDrawing example so that we can get the owner draw working correctly. This is done solely so that we can position the menu where we want them without having them showing up on the wrong side of the help menu.
If we look at the Object Explorer we can see,
That the Actions have already been created for us, so all we have to do is connect them to an implemetation function.
At least that's the theory in practice if we try it we will notice that we haven't got an implementation class for the ChapterTwelveMainWindow.ui file so we shall have to create one. Open the Automake Manager and right click on the ChapterTwelveMainWindow.ui file,
andf select the Subclassing Wizard ( I know we've been through this before but I figured it would be best to have the main window process all in one placc as it will be more convenient. )
I've added “Impl”, for implementation to the end of the file name just so that we don't end up with two files and classes named identically. Once we fill out the class name and hit create the confirmation dialog pops up,
As we want to add this to the current project we can just click on O.K. and continue.
The generated class will be a standard implementation class that will also contain the headers and the empty implementation functions for the menu items that are already set up for us when we add the main window,
virtual void helpAbout(); virtual void helpContents(); virtual void helpIndex(); virtual void editFind(); virtual void editPaste(); virtual void editCopy(); virtual void editCut(); virtual void editRedo(); virtual void editUndo(); virtual void fileExit(); virtual void filePrint(); virtual void fileSaveAs(); virtual void fileSave(); virtual void fileOpen(); virtual void fileNew();
So all we have to do is implement the functions and connect up any menu items that we have added. We do this as always by selecting the menu item action we want to connect,
and then selecting the implementation file,
The main thing we need to do now is replace the standard widget with the main widnow ui that we have added to the project.
We do this in the main.cpp file, the part we are interested in reads,
KApplication app; ChapterTwelveMainWindow *mainWin = 0; if (app.isRestored()) { RESTORE(ChapterTwelveMainWindow); } else { // no session.. just start up normally KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); /// @todo do something with the command line args here mainWin = new ChapterTwelveMainWindow(); app.setMainWidget( mainWin ); mainWin->show(); args->clear(); }
All we do is change all the references for the ChapterTwelveMainWindow class to the ChapterTwelveMainWindowImpl class, so it reads,
KApplication app; ChapterTwelveMainWindowImpl *mainWin = 0; if (app.isRestored()) { RESTORE(ChapterTwelveMainWindow); } else { // no session.. just start up normally KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); /// @todo do something with the command line args here mainWin = new ChapterTwelveMainWindowImpl(); app.setMainWidget( mainWin ); mainWin->show(); args->clear(); }
You may notice that we haven't changed the text in the restore macro, there is a reason for this. To start with the restore macro is or was defined as,
#define RESTORE(type) { int n = 1;\ while (KMainWindow::canBeRestored(n)){\ (new type)->restore(n);\ n++;}}
This functionality is entirely for KDE session management and has been replaced by templated functions with the one we would use defined as,
template <typename T> inline void kRestoreMainWindows() { for ( int n = 1 ; KMainWindow::canBeRestored( n ) ; ++n ) { const QString className = KMainWindow::classNameOfToplevel( n ); if ( className == QString::fromLatin1( T::staticMetaObject()->className() ) ) (new T)->restore( n ); } }
The first problem here is the one that is reported by the compiler if you actually try to change the ChapterTwelveMainWindow to ChapterTwelveMainWindowImpl in the call to the restore macro. That is that the QMainWindow class doesn't have a restore function. This is because the ChapterTwelveMainWindowImpl class in this project is derived from the Form1 class which in turn is derived from QMainWindow. The QMainWindow class doesn't have the restore function as it is KDE functionality.
The KMainWindow class does have a restore function and if we were using a class derived from KMainWindow then there would be no problem, there are two ways of dealling with this. One we could wait until the coding for the project is finished and then when we know there is going to be no more work done we can edit the generated files so that the class derives from KMainWindow. Or alternatively we could just wait and ignore it until KDevelop gives us a way to specify the derived class for the generated files.
Then we include the header file.
#include "chaptertwelvemainwindowimpl.h"
and we're ready to go
All we need to do now is implement the program. the code itself is a mixture of the previous two programs in that we are using the menu code from the drawing example and the drawing code, complete with the double buffer option from the menu's example.
The only thing that we need to bear in mind is that some of the code is already written fro use so if we look in the ChapterTwelveMainWindow.h file we see that all the QActions have already been defined.
QAction* fileNewAction; QAction* fileOpenAction; QAction* fileSaveAction; QAction* fileSaveAsAction; QAction* filePrintAction;
Along with the required popups and the function connections are made in the ChapterTwelveMainWindow.cpp file.
// signals and slots connections connect( fileNewAction, SIGNAL( activated() ), this, SLOT( fileNew() ) ); connect( fileOpenAction, SIGNAL( activated() ), this, SLOT( fileOpen() ) ); connect( fileSaveAction, SIGNAL( activated() ), this, SLOT( fileSave() ) );
Setting The Menu Text
Unfortuantely there is a minor problem with the gui editor for the menus in that it doesn't allow you specify the name for the QPopupMenu that will implement the menu you add. If you add a label for a Popup Menu as “Shapes” then the QPopupMenu object that is created will be called Shapes. A name which totally messes up our naming conventions so to fix it call the the menus by the names you want the variables to be.
Like so, and then in the constructor for the class use the menuBar to find the menu and change the name,
/// Set the text correctly for the menus if( menubar->findItem( 3 ) != 0 ) menubar->findItem( 3 )->setText( "&View" ); if( menubar->findItem( 4 ) != 0 ) menubar->findItem( 4 )->setText( "&Shapes" ); if( menubar->findItem( 5 ) != 0 ) menubar->findItem( 5 )->setText( "&Colours" );
now our variables will be listed in the generated ChapterTwelvemainWindow.h file as,
QPopupMenu *viewMenu; QPopupMenu *shapesMenu; QPopupMenu *coloursMenu;
Adding The Status Bar
Adding the status bar to a main window application is really simple the QMainWindow class contains a function called statusBar(). This function returns the status bar for the main window. If the status bar doesnt exist it creates it and any subsequent calls to statusBar() reference the newly created QStatusBar. The QStatusBar has one main function that you will use, this is the message function. The message function takes a string for the text to be displayed and if you want a number of milliseconds for the amount of time for the message to be displayed.
statusBar()->message( "Initialising", 5000 );
will display the message initialising for five seconds. it sould be remembered that the status bar is to tell the user of the application what is going on so they have no interest in what function you have called or how fancy the code is. It should only display messages that are relevant to the user of the application. In this example application the status bar is only used to confirm selections that the user has made, giving a running comentary if you like on what the user is doing.
One thing you will notice on the status bar is that as you scroll through a menu the status bar text changes to reflect the currently highlighted item. So the when you highlight the File menu option New the status bar will show the text “New” and the short cut for this menu option. As we are using custom menus for the shapes and the colours menu this doesn;t work for us so we are going to have to fake it.
Simply do a connection to the QPopupMenu highlighted signal
connect( shapesMenu, SIGNAL( highlighted( int ) ), this, SLOT( shapesHighLighted( int ) ) );
and the implement the status bar text manually,
switch( id ) { case RectMenuItem: statusBar()->message( "Rect", 5000 ); break; case RoundRectMenuItem: statusBar()->message( "Round Rect", 5000 ); break; case EllipseMenuItem: statusBar()->message( "Ellipse", 5000 ); break; case LineMenuItem: statusBar()->message( "Line", 5000 ); break; case TriangleMenuItem: statusBar()->message( "Triangle", 5000 ); break; };
In the code this is done for both the Shapes menu and the Colours menu.
Adding ToolBars
Back in the old days of programming a toolbar was a single item and it was considered design if you had a seperator between the different sections, nowadays each tool bar is made up of mulitple toolbars each of which contains the icons for it's own section and can be displayed or not at will. As we are using a main window we don;t have to worry about most of the details about how a toolbar works we can focus primarly on setting it up and making sure that it does and looks the way we want.
For a quick bit of background though a toolbar works as a docking window which can be slotted into place when required, think of it as a widget that can be moved around programatically, with the tool bar we just tell it to stick to the top section of the main window, and let the QMainWindow take care of the details of maintaining the main window space. i.e. making sure that something appearing at x = 3 y = 3 actually appears below the toolbar rather than behind or over it.
To start with then we have to declare some toolbars
KToolBar *fileToolBar; KToolBar *editToolBar; KToolBar *shapesToolBar; KToolBar *coloursToolBar;
We define one for the file menu operations, one for the edit menu operations, one for the shape menu operations and one for the colours menu operations. Creating the toolbar should be what you'd expect by now.
fileToolBar = new KToolBar( this, "File Toolbar" )
Setting the options on the toolbar is actually really simple at this stage because we did all the required set up when defining the menu's well to be more acurate the options for the file and edit menu where all predefined for us with the main window but you can see the principal for your own menus. All you need to do to add buttons to the toolbar is add the QAction items that you set up for the menus so for the file tool bar we add the following actions,
fileNewAction->addTo( fileToolBar ); fileOpenAction->addTo( fileToolBar ); fileSaveAction->addTo( fileToolBar );
The more observant may notice that these aren't all the default options provided by the default implementation of a QMainWindow. The reason for this is that I've removed the items that don't really make sense for the current application, or as in the case of the print that I just couldn't be bothered with.
That is all you need to do as the setup for the action should contain all the imformation that is need for the display and the function call from the menu setup.
Adding the toolbars to the main window is done with,
addDockWindow( fileToolBar ); addDockWindow( editToolBar );
which will give us,
We then add a ToolBars option to the view menu,
By default the four toolbars are turned on but the code to turn them off or back on again when the menu option is clicked is,
if( toggle == true ) coloursToolBar->show(); else coloursToolBar->hide();
which is all really straight forward if we are just using standard menu items. Unfortuanetly we aren't we are using custom drawn menu items and being as it's a demonstration program we aren't smart enough to just have the images drawn to a jpg file but are drawing them ourselves the hard way. So.
Custom ToolBar Buttons
We have to create our own custom toolbar buttons pretty much in the same way that we created the menu items in the first place. We start by adding a new class
As you can see we use the class wizard to do this which makes things much simpler than adding things by hand as it puts the files in the correct place and uses the correct naming conventions. In the above example we are defining a class called KRectButton which will be our custom implementation for the rect shape in the same way that we previously implemented the drawing of the rect on the menu.
When using the class wizard if you select the Generate QWidget child class option then you can add the base class information as shown in the dialog. Once you have the class set up correctly then you hit OK and accept the confirmation dialog that follows, the class will look something like this.
#ifndef KRECTBUTTON_H #define KRECTBUTTON_H #include <ktoolbarbutton.h> /** @author pseudonym67 */ class KRectButton : public KToolBarButton { Q_OBJECT public: KRectButton(QWidget *parent = 0, const char *name = 0); ~KRectButton(); protected: virtual void drawButton( QPainter *painter ); }; #endif
All I have added is the drawButton function override so that we can draw the required shape when required. The code for the function as you should expect by now is,
painter->drawRect( 2, 5, width()-2, height()-10 );
One thing that you should notice is the constructor which reads,
KRectButton::KRectButton(QWidget *parent, const char *name) : KToolBarButton(parent, name) { setText( "Rect" ); }
There is an important reason for drawing your attention to this and that is that if you derive the class from QToolButton and dont put any text here then the button when displayed will be about three pixels wide. If you derive the class from KToolBarButton as we have and don't put any text here then the button will not display at all.
The text itself it displayed if you hold the mouse over the button, like so.
As with the custom menu items the rest is just a matter of repeating the same set up for the different drawing and then using a single class to draw the filled rectangles for the colours.
As you'd expect though setting up the buttons properly is a bit more long winded than it is with the standard implementations.
First we create the button objects using,
shapesToolBar = new KToolBar( this, "Shapes Toolbar" ); rectButton = new KRectButton( shapesToolBar ); roundRectButton = new KRoundRectButton( shapesToolBar ); ellipseButton = new KEllipseButton( shapesToolBar ); lineButton = new KLineButton( shapesToolBar ); triangleButton = new KTriangleButton( shapesToolBar );
Note here that while we set the parent of the toolbar as the main window we set the parent of the buttons as the toolbar itself.
Next we add the Buttons to the toolbar with,
shapesToolBar->insertWidget( RectWidgetItem, 20, rectButton ); shapesToolBar->insertWidget( RoundRectWidgetItem, 20, roundRectButton ); shapesToolBar->insertWidget( EllipseWidgetItem, 20, ellipseButton ); shapesToolBar->insertWidget( LineWidgetItem, 20, lineButton ); shapesToolBar->insertWidget( TriangleWidgetItem, 20, triangleButton );
Using the insertWidget function which takes an id the suggested size and the widget itself. This function allows you to add just about any widget you want to the toolbar though obviously you should only add widgets that make sense to be used in a toolbar context.
Once the Buttons are added to the toolbar we add the toolbars to the window.
addDockWindow( shapesToolBar ); addDockWindow( coloursToolBar );
Of course at the moment they don't do anything so we have to write some connections.
First we want to display a text message to the status bar when the button is highlighted and this is where we hit a minor snag in that the value used as the id in the call to insertWidget isn't the one that we get when we connect to the highlight function,
connect( blackButton, SIGNAL( highlighted( int, bool ) ), this, SLOT( blackButtonHighLighted() ) ); connect( whiteButton, SIGNAL( highlighted( int, bool ) ), this, SLOT( whiteButtonHighLighted() ) );
So we just add a new function and add the message to the status bar ourselves,
void ChapterTwelveMainWindowImpl::blackButtonHighLighted() { statusBar()->message( "Black", 5000 ); }
Once this is done all we need to do is connect up the buttons with the correct functionality.
connect( blackButton, SIGNAL( buttonClicked( int, Qt::ButtonState ) ), this, SLOT( blackButtonClicked() ) ); connect( whiteButton, SIGNAL( buttonClicked( int, Qt::ButtonState ) ), this, SLOT( whiteButtonClicked() ) );
As with the previous call to connect we just add a function and then call the required function ourselves with the correct parameter.
void ChapterTwelveMainWindowImpl::blackButtonClicked() { coloursMenuSelected( BlackColourMenuItem ); }
And now everything is wired up the only thing left to do is implement the code for the pregenerated file and edit menu options.
The Generated Functions
One of the first thing you are going to want to do when adding menu items is remove one, because it isn't required for the program or because you didn't add it in the right place or duplicated functionality. The problem is that through the gui you can add and edit menu items but you can't delete them so the easiest way to do it is to close KDevelop and open the .ui file in Kxml Editor.
Select the item you want and then simply delete it. Open up KDevelop again and you'll see,
At this point you should delete the filePrint function from the header file and the source file although they wont cause any trouble if you don't there's no way to activate them now. It's just neater to get rid of them.
The edit menu is deleted in it's entirety from the final menu which just gives us the file menu and the help menu as the menu's that we haven't set up ourselves. The file menu we have set up previously and the code is almost identical to earlier versions.
Help
One thing that anyone moving from Windows to Linux will miss almost instantly is the unified help system on windows. On Windows there is only one way to do help and anything that doesn't follow this way is an anomaly rather than the rule. On Linux the help is basically all over the place. KDE goes someway to fix this with the DocBook idea but this is not exactly user friendly and there are no real tools for generating the files, there are plenty of tutorials to be found but essentially it boils down to hand coding the xml tags yourself.
This is far from ideal from the point of view of a new developer writing their latest and greatest idea and then finding they get bogged down in writing the help, so we'll sort of come to a compromise. Basically a DocBook is a html file that has some xml style tags in it to set up formatting etc but and this is the important bit you need to write the html file first. So to start with we'll use a html viewer to display help pages that are written and then gently suggest that if the writer wants the application to be included as part of KDE then they should look up DocBooks or get someone who knows how to do it to edit the help pages.
If you do a search for “Simple HTML Help Browser” in the help you'll find a Qt class that the help window with this project is based on. It is a very basic html browser that uses the QTextBrowser class to display the html so it is limited in what it can do.
This version of the class has been changed to use KDE classes more than the default Qt classes and some of the minor functionality has been changed. For example it no longer allows you to open new help windows and closing the help window doesn't shut down the application anymore. If you wish to check the nature of the changes the source is available for both the original project in the help and for the KDE version in the ChapterTwelveMainWindow source code.
The help window will display pretty much any basic html files the examples shown were simply written in open office and saved as html files.
Summary
In this chapter we have started with the basics required for a simple drawing application and built upon it so that we ended up using standard and custom menus, standard and custom toolbars and in the process have gone from using only dialog based applications to having all the the tools we need to create full working programs, complete with help.