HomeHome

Chapter 8: Preparing for Battle


Screenshot of tutorial eight

In this example, we introduce the first custom widget that can paint itself. We also add a useful keyboard interface (with two lines of code).

Line by Line Walk-Through

lcdrange.h

This file is very similar to the lcdrange.h in chapter 7. We have added one slot, setRange().

        void setRange( int minVal, int maxVal );

We now add the possibility of setting the range of the LCDRange. Until now, it has been fixed at 0..99.

lcdrange.cpp

There is a change to the constructor, we'll discuss that later.

    void LCDRange::setRange( int minVal, int maxVal )
    {
        if ( minVal < 0 || maxVal > 99 || minVal > maxVal ) {
          qWarning( "LCDRange::setRange(%d,%d)\n"
                   "\tRange must be 0..99\n"
                   "\tand minVal must not be greater than maxVal",
                   minVal, maxVal );
          return;
        }
        slider->setRange( minVal, maxVal );
    }

setRange() sets the range of the slider in the LCDRange. Since we have set up the QLCDNumber to always display two digits, we want to limit the possible range of minVal and maxVal to 0..99 to avoid overflow of the QLCDNumber. (We could have allowed values down to -9 but chose not to.) If the arguments are illegal, we use Qt's qWarning() function to issue a warning to the user and return immediately. qWarning() is a printf-like function that by default sends its output to stderr. You can install your own handler function using ::qInstallMsgHandler() if you want.

cannon.h

CannonField is a new custom widget that knows how to display itself.

    class CannonField : public QWidget
    {
        Q_OBJECT
    public:
        CannonField( QWidget *parent=0, const char *name=0 );

CannonField inherits QWidget and we use the same idiom as for LCDRange.

        int angle() const { return ang; }
        QSizePolicy sizePolicy() const;
    
    public slots:
        void setAngle( int degrees );
    
    signals:
        void angleChanged( int );

For the time being, CannonField only contains an angle value for which we provide an interface using the same idiom as for value in LCDRange.

    protected:
        void paintEvent( QPaintEvent * );

This is the second of the many event handlers in QWidget that we encounter. This virtual function is called by Qt whenever a widget needs to update itself (i.e. paint the widget's surface).

cannon.cpp

    CannonField::CannonField( QWidget *parent, const char *name )
            : QWidget( parent, name )
    {

Again, we use the same idiom as for LCDRange in the previous chapter.

        ang = 45;
        setPalette( QPalette( QColor( 250, 250, 200) ) );
    }

The constructor initializes the angle value to 45 degrees, and sets a custom palette for this widget.

This palette uses the indicated color as background, and picks other colors suitably. (For this widget, only the background and text colors will be used.)

    void CannonField::setAngle( int degrees )
    {
        if ( degrees < 5 )
            degrees = 5;
        if ( degrees > 70 )
            degrees = 70;
        if ( ang == degrees )
            return;
        ang = degrees;
        repaint();
        emit angleChanged( ang );
    }

This function sets the angle value. We have chosen a legal range of 5..70 and adjust the given number of degrees accordingly. We have chosen not to issue a warning if the new angle is out of range.

If the new angle equals the old one, we return immediately. It is important to only emit the signal angleChanged() when the angle really has changed.

Then we set the new angle value and repaint our widget. The QWidget::repaint() function clears the widget (usually filling it with its background color) and send a paint event to the widget. This results in a call the paint event function of the widget.

Finally, we emit the angleChanged() signal to tell the outside world that the angle has changed. The emit keyword is unique to Qt and not regular C++ syntax. In fact, it is a macro.

    void CannonField::paintEvent( QPaintEvent * )
    {
        QString s = "Angle = " + QString::number( ang );
        QPainter p( this );
        p.drawText( 200, 200, s );
    }

This is our first attempt to write a paint event handler. The event argument contains a description of the paint event. QPaintEvent contains the region in the widget that must be updated. For the time being, we will be lazy and just paint everything.

Our code displays the angle value in the widget at a fixed position. First we create a QString with some text and the angle, then we create a QPainter operating on this widget, and use it to paint the string. We'll come back to QPainter later; it can do a great many things.

main.cpp

    #include "cannon.h"

We include our new class.

    class MyWidget: public QWidget
    {
    public:
        MyWidget( QWidget *parent=0, const char *name=0 );
    };

This time, we include a single LCDRange and a CannonField in our top level widget.

        LCDRange *angle = new LCDRange( this, "angle" );

In the constructor, we create and set up our LCDRange.

        angle->setRange( 5, 70 );

We set the LCDRange to accept ranges from 5 to 70 degrees.

        CannonField *cannonField 
            = new CannonField( this, "cannonField" );

We create our CannonField.

        connect( angle, SIGNAL(valueChanged(int)),
                 cannonField, SLOT(setAngle(int)) );
        connect( cannonField, SIGNAL(angleChanged(int)),
                 angle, SLOT(setValue(int)) );

Here we connect the valueChanged() signal of the LCDRange to the setAngle() slot of the CannonField. This will update CannonField's angle value whenever the user operates the LCDRange. We also make the reverse connection, so that changing the angle in the CannonField will update the LCDRange value. In our example, we never change the angle of the CannonField directly, but by doing the last connect(), we ensure that no future changes will disrupt the synchronization between those two values.

This illustrates the power of component programming and proper encapsulation.

Notice how important it is to only emit the angleChanged() signal when the angle actually changes. If both the LCDRange and the CannonField had omitted this check, the program would have entered an infinite loop upon the first change of one of the values.

        QGridLayout *grid = new QGridLayout( this, 2, 2, 10 );
        //2x2, 10 pixel border

Until now we've used the no-assembly-required QVBox and QGrid widgets for geometry management. However, now we want to have a little more control over the layout, and switch to the more powerful QGridLayout class. QGridLayout isn't a widget, it is a different class that can manage the children of any widget.

As the comment indicates, we create a two-by-two array, with ten pixel borders. (The constructor for QGridLayout can be a little cryptic, so it's good to put in such comments.)

        grid->addWidget( quit, 0, 0 );

We add the Quit button in the top-left cell of the grid: 0, 0.

        grid->addWidget( angle, 1, 0, Qt::AlignTop );

We put the angle LCDRange in the bottom-left cell, aligned to the top of its cell. (This alignment is one of the things QGridLayout allows but QGrid does not.)

        grid->addWidget( cannonField, 1, 1 );

We put the CannonField object in the bottom right cell. (The top right cell is empty, yes.)

        grid->setColStretch( 1, 10 );

We tell QGridLayout that the right column (column 1) is stretchable. Since the left column isn't (it has stretch factor 0 - the default value), QGridLayout will try to let the left-hand widgets' sizes be unchanged and resize just the CannonField when the MyWidget is resized.

        angle->setValue( 60 );

We set an initial angle value. Note that this will trigger the connection from LCDRange to CannonField.

        angle->setFocus();

And our last action is to set angle to have keyboard focus, so that by default, keyboard input will go to the LCDRange widget.

LCDRange does not contain any keyPressEvent(), so that would seem not to be terribly useful. However, its constructor just got a new line:

        setFocusProxy( slider );

The LCDRange sets the slider to be its focus proxy. That means that when someone (the program or the user) wants to give the LCDRange keyboard focus, the slider should take care of it. QSlider has a decent keyboard interface, so with just one line of code we've give LCDRange one.

Behavior

The keyboard now does something - the arrow keys, Home, End, PageUp and PageDown now all do something vaguely sensible.

When the slider is operated, the CannonField displays the new angle value. Upon resize, CannonField is gives as much space as possible.

On Windows machines with an 8-bit display, the new background color is dithered to death. The next chapter works around this.

Exercises

Try to resize the window. What happens if you make it really narrow or really squat?

If you remove the AlignTop, what happens to the LCDRange's position and size? Why?

If you give the left-hand column a non-zero stretch factor, what happens when you resize the window?

Leave out the setFocus() call. Which behavior do you prefer?

Try to change "Quit" to "&Quit" in the QButton::setText() call. How does the button's look change? What happens if you press Alt-Q while the program's running? (Meta-Q on a few keyboards.)

Center the text in the CannonField.

You may now go on to chapter nine.

[Previous tutorial] [Next tutorial] [Main tutorial page]


Copyright © 2005 TrolltechTrademarks
Qt version 2.3.10