Chapter Ten - Custom Widgets
At some point or other when developing software you find that the provided tools don't quite do what you would like them to do. Personally I liked the look of the KKeyButton but wasn't over impressed with it's built in functionality wanting instead a button that looked like that but behaved differently. So the only option was to rewrite the existing button and create the KSquareButton class.
Building Your Own Widgets
One thing about open source is that by using the help you can see the source code for any KDE class that you want to copy/change the behaviour of so using the KKeyButton as a template I wrote the KSquareButton. In this case the main task was not implement more functionality to the class but to remove the shortcut key code from the widget.
So where the KKeyButton declares a lot of functions that manage the shortcut our class header is a lot simpler
#include <qpushbutton.h> class KSquareButton : public QPushButton { Q_OBJECT public: KSquareButton( QWidget *parent, const char *name ); virtual ~KSquareButton(); void setText( const QString &text ); void drawButton( QPainter *painter ); };
The main things that we are concerned with in this class is to set the text on the button and to draw the button itself. The setText function is simplicity itself in that all we do is,
QPushButton::setText( text ); setFixedSize( sizeHint().width() + 12, sizeHint().height() + 8 );
use the parent version of setText to set the actual text and then call setFixedSize which resizes the button so that the text fits within the button space, if you wanted a fixed size button you would remove this line but would then be running the risk of having them look stupid because longer text strings would not display properly.
The paibnt function takes a bit more thought though, as with any widget we build up the image in layers so we start with,
QPointArray pArray( 4 ); pArray.setPoint( 0, 0, 0 ); pArray.setPoint( 1, width(), 0 ); pArray.setPoint( 2, 0, height() ); pArray.setPoint( 3, 0, 0 ); /// draw the top and the left light border /// QRegion regionOne( pArray ); painter->setClipRegion( regionOne ); painter->setBrush( backgroundColor().light() ); painter->drawRoundRect( 0, 0, width(), height(), 20, 20 );
which defines a point array that draws the first region, this region is a diagonal region from the top left to the top right and down to the bottom left of the button. The colour white in the picture is set by setting the brush to the backgroundColor().light(). Note that we don't use and exact colour here as the colours are set by the system.
Notice that although in the call to drawRoundRect we passed in the parameters for the entire button size the area drawn is only the region specified by the point array and passed to the setClipRegion function.
Next we draw the bottom right triangle which is shaded blue grey in the above picture,
pArray.setPoint( 0, width(), height() ); pArray.setPoint( 1, width(), 0 ); pArray.setPoint( 2, 0, height() ); pArray.setPoint( 3, width(), height() ); /// draw the bottom right triangle dark /// QRegion regionTwo( pArray ); painter->setClipRegion( regionTwo ); painter->setBrush( backgroundColor().dark() ); painter->drawRoundRect( 0, 0, width(), height(), 20, 20 );
The only difference between this code and the above code is the definition for the regoin that we are drawing and the setting of the brush to backgroundColor().dark(), which gives us,
This completes the background for the button, now all we need to do is draw the button, we start by turning off the clipping as we actually want to draw over what we have drawn so far.
painter->setClipping( false );
and then draw the button surface with,
painter->setPen( backgroundColor().dark() ); painter->setBrush( colorGroup().button() ); if( width() > 14 && height() > 10 ) painter->drawRect( 7, 5, width() - 14, height() - 10 )
which will draw a standard button coloured panel with a dark border.
which is pretty much the button image we want. We just need to add a few finishing details. First we draw the button label
drawButtonLabel( painter );
which is the QButton function that draws whatever is set to appear on the button be a picture or text. Then we draw the little rectangle that you see appear when you click on the button.
painter->setPen( colorGroup().text() ); painter->setBrush( NoBrush ); if( hasFocus() == true ) { if( width() > 16 > height() > 12 ) painter->drawRect( 18, 12, width() - 16, height() - 12 ); }
giving us,
So all we need to do now is test it.
Testing The Widget
We'll start with a simple application that has a KTextEdit widget and then add some buttons with the code. As the inspiration for this class came from the KKeyButton it's only fitting that we do a keyboard,
KSquareButton *kSquareButtonQ; KSquareButton *kSquareButtonW; KSquareButton *kSquareButtonE; KSquareButton *kSquareButtonR;
declaring the buttons in the header file for the widget and adding the definitions for slots for when the button is clicked.
void kSquareButtonQ_clicked(); void kSquareButtonW_clicked(); void kSquareButtonE_clicked(); void kSquareButtonR_clicked(); void kSquareButtonT_clicked();
Now we need to create the widgets and set up the connections, in the constructor for the KSquareButtonDemoWidget class
kSquareButtonQ = new KSquareButton( this, "KSquareButtonQ" ); kSquareButtonQ->setGeometry( QRect( 10, 270, 64, 39 ) ); kSquareButtonQ->setText( "Q" ); connect( kSquareButtonQ, SIGNAL( clicked() ), this, SLOT( kSquareButtonQ_clicked() ) );
Each button is created and setGeometry is called to place the button on the widget. I should point out that coding the QRect's by hand at this point would be a total pain so I set up a test project and placed some KKeyButtons on the form and then compiled the code and took the QRect values from the generated widget base cpp file. The text for the button is then set with a call to setText and finally a connection is made. All the button does when clicked is add the letter to the KEditBox with,
void KSquareButtonDemoWidget::kSquareButtonQ_clicked() { textEdit->insert( "Q" ); }
and the running application is,
Tip Of The Day
it is important to note that if you set up a slot connection incorrectly the compiler will not tell you anything about it. What you will get is a message when you run your application.
This example shows that the kSquareButtonM is not set up properly and that if we click it then we will not receive the signal. the message tells us that there is something wrong with the kSquareButtonM connection so let's take a look at it.
void kSquareButtonM_Clicked();
The error is that the slot is defined as Clicked with a capital c and the connection is defined as
connect( kSquareButtonM, SIGNAL( clicked() ), this, SLOT( kSquareButtonM_clicked() ) );
which is what it should be and is in keeping with all the other definitions so we need to change the header and the implementation. and once this is done the progem runs without any error messages.
Of course now that we've tested the new button there is one thing that is immeadiately apparent and that is that the demo widget is rubbish so we shall have to see if we can turn it into a proper functional widget.
Adding A Custom Widget
Initially the Custom Widgets panel is empty when we start KDevelop,
and it will stay that way unless you add something yourself, so lets see if we can turn the KSquareButton into a custom widget to start with. We start at Tools\Edit Custom Widgets in KDevelop,
which gives us this dialog,
Defining The Widget
As you can see we start by defining the widget, the class we are using is the KSquareButton class and we point the file to the header file for the class. The pixmap option allows us to set an image for the widget that will be displayed in the Custom Widgets section of KDevelop. You can see the default to the left of the above image. For the size hints we add the size that we want the button to be, these figures are taken from the generated file for the KKeyButton I mentioned earlier. There is also a checkbox to select if the widget is going to be used as a Container Widget this means will you be adding widgets to your widget after it is created, if the widget is a completely self contained object we can leave this unchecked.
The other important button this screen is the save descriptions button which saves the information about your widget. This is the file that KDevelop or rather the Qt Designer embedded in KDevelop will read to add your widget to the custom widgets panel. You should remember where you save this as the custom widget will not be loaded automatically into the development environment for projects that you haven't specified it for, although adding it once you have it setup is simply a matter of loading the saved file and adding the class files to your project.
Defining the Signals
Here you can see that the slot for clicked is defined. Even though this is a standard signal from our QPushButton base class I've added it here to show how it's done as this will have no impact on the widget whatsoever.
Technically there is room for confusion here if you get into a situation where you are defining every signal and slot that your widgets handles then you should rethink it and follow two simple rules. Basically only define a Signal if the custom widget emits it and it will be handled by an outside class. And only define a slot if the signal for the slot is emitted from a widget that is not a part of your custom widget.
The handling of the emitted signal for clicked is as you would expect,
connect( kSquareButtonTest, SIGNAL( clicked() ), this, SLOT( kSquareButtonTest_clicked() ) );
Is used in the constructor of the .cpp file and I should mention that you need to add the header for the widget to the .cpp file or the compiler will say that the call to connect is invalid. In fact you need to add all the implementation and header files for the widget to the project in which you intend to use it.
Defining The Slots
There are no slots implemented for this widget but as you can imagine you would implement a slot in exactly the same way that you normally would in any widget class.
Defining The Properties
To set up a property in a widget you would use the standard methods for implementing your proprty so the the text that will be placed on the button we would get and set the properties with the code,
void setText( const QString &text ); QString text();
With the implementations being,
void KSquareButton::setText( const QString &text ) { QPushButton::setText( text ); setFixedSize( sizeHint().width() + 12, sizeHint().height() + 8 ); } QString KSquareButton::text() { return QPushButton::text(); }
We can now set and get our property as normal. The thing is we are using a Gui environment and Qt provides us with a mechanism where we can get the property in question to show up in the Gui. We do this using the Q_PROPERTY macro which takes the form Q_PROPERTY( type property_name read_function read_function_name write_function write_function_name and is written into the header file as,
Q_PROPERTY( QString text READ text WRITE setText )
So when we look at the KSquareButton in the designer we get,
and if we run it,
OK we now have a functional custom widget so let's see if we can uise it in anger.
Create A Virtual Keyboard
To start off turning the KSquareButtons into a virtual keyboard we could if we had loads of time and patience sit and code everything by hand, or alternatively we could simple do the layout on a widget let the moc compiler generate the code and then copy it, like so,
We then create a KVirtualKeyBoard class and copy the generated widget it code to it,
class KSquareButton; class KVirtualKeyBoard : public QFrame { Q_OBJECT
There are more than twenty declarations of KSquareButtons so I haven't included them here, if you really want to see them look at the header file, kvirtualkeyboard.h in the source code.
I've also added the functions.
inline void setCapsLocked( bool capsLocked ){ bCapsLocked = capsLocked; }; inline bool capsLocked(){ return bCapsLocked; };
for setting the caps lock and the signal,
signals: void virtualKeyPressed( QString key );
which is the signal that is emitted by the widget when a button is pressed.
void KVirtualKeyBoard::kSquareButtonB_clicked() { if( capsLocked() == true ) emit virtualKeyPressed( i18n( "B" ) ); else emit virtualKeyPressed( i18n( "b" ) ); }
Changes To KSquareButton
When using the KSquareButton there are a couple of refinements that needed to be made, firstly they aren't exactly square until we add
setFixedSize( 45, 45 );
To the constructor. Secondly the code,
void KSquareButton::setText( const QString &text ) { QPushButton::setText( text ); setFixedSize( sizeHint().width() + 12, sizeHint().height() + 8 ); }
Causes the code to resize every time the text on the button is changed. This is ugly and not strictly necessary for example when the caps button is pressed all the buttons adjust their sizes, so the code was changed to.
void KSquareButton::setText( const QString &text ) { QPushButton::setText( text ); if( text.length() > 3 ) setFixedSize( sizeHint().width() + 12, sizeHint().height() + 8 ); }
which means that the buttons are only resized if there are more than three characters in the text string which means that the buttons wont resize unless it is strictly necessary.
Creating the KVirtualKeyBoard Widget
The creation for the widget once we have the class is exactly the same as with the KSquareButton described earlier,
Of course as we are emitting a signal we have to define it.
Now all we need to do is test it with,
We place the virtual keyboard widget on the form and add a text widget, it is then a simple matter to tie them together with the code,
public slots: /*$PUBLIC_SLOTS$*/ void virtualKeyPressed( QString key );
added to the header and the function,
void ChapterTenCustomWidgetWidget::virtualKeyPressed( QString key ) { if( key != 0 && key == "BS" ) { textEdit->moveCursor( QTextEdit::MoveBackward, false ); textEdit->del(); } else { textEdit->insert( key ); textEdit->moveCursor( QTextEdit::MoveForward, false ); } }
which if the Back button is pressed the code moves one space back and calls delete which deletes the character directly infront of it, leaving the cursor in the correct position for the next character. Then there's the connection,
connect( kVirtualKeyBoard1, SIGNAL( virtualKeyPressed( QString ) ), this, SLOT( virtualKeyPressed( QString ) ) );
in the constructor. Which gives us,
Summary
That should be enough to get you going with you're own custom widgets, of course this one still isn't perfect it doesn't have a question mark for a start and there's no way to move the cursor through the text.