Qt 4.3: Qt Tutorial 12 - Hanging in the Air the Way Bricks Don't

|  | Home · All Classes · Main Classes · Grouped Classes · Modules · Functions |

[Previous: Chapter 11] [Qt Tutorial] [Next: Chapter 13]

# Qt Tutorial 12 - Hanging in the Air the Way Bricks Don't

Files:

- tutorial/t12/cannonfield.cpp
- tutorial/t12/cannonfield.h
- tutorial/t12/lcdrange.cpp
- tutorial/t12/lcdrange.h
- tutorial/t12/main.cpp

In this example, we extend our `LCDRange` class to include a text label. We also provide something to shoot at.

## Line by Line Walkthrough

### t12/lcdrange.h

The `LCDRange` now has a text label.

     class QLabel;
     class QSlider;

We forward declare QLabel and QSlider because we want to use pointers to them in the class definition. We could also use `#include`, but that would slow down compilation for nothing.

     class LCDRange : public QWidget
     {
         Q_OBJECT

     public:
         LCDRange(QWidget *parent = 0);
         LCDRange(const QString &text, QWidget *parent = 0);

We have added a new constructor that sets the label text in addition to the parent.

         QString text() const;

This function returns the label text.

         void setText(const QString &text);

This slot sets the label text.

     private:
         void init();

Because we now have two constructors, we have chosen to put the common initialization in the private `init()` function.

         QLabel *label;

We also have a new private variable: a QLabel. QLabel is one of Qt's standard widgets and can show a text or a QPixmap with or without a frame.

### t12/lcdrange.cpp

     LCDRange::LCDRange(QWidget *parent)
         : QWidget(parent)
     {
         init();
     }

This constructor calls the `init()` function, which contains the common initialization code.

     LCDRange::LCDRange(const QString &text, QWidget *parent)
         : QWidget(parent)
     {
         init();
         setText(text);
     }

This constructor first calls `init()` and then sets the label text.

     void LCDRange::init()
     {
         QLCDNumber *lcd = new QLCDNumber(2);
         lcd->setSegmentStyle(QLCDNumber::Filled);

         slider = new QSlider(Qt::Horizontal);
         slider->setRange(0, 99);
         slider->setValue(0);
         label = new QLabel;
         label->setAlignment(Qt::AlignHCenter | Qt::AlignTop);

         connect(slider, SIGNAL(valueChanged(int)),
                 lcd, SLOT(display(int)));
         connect(slider, SIGNAL(valueChanged(int)),
                 this, SIGNAL(valueChanged(int)));

         QVBoxLayout *layout = new QVBoxLayout;
         layout->addWidget(lcd);
         layout->addWidget(slider);
         layout->addWidget(label);
         setLayout(layout);

         setFocusProxy(slider);
     }

The setup of `lcd` and `slider` is the same as in the previous chapter. Next we create a QLabel and tell it to align the contents centered horizontally and to the top vertically. The QObject::connect() calls have also been taken from the previous chapter.

     QString LCDRange::text() const
     {
         return label->text();
     }

This function returns the label text.

     void LCDRange::setText(const QString &text)
     {
         label->setText(text);
     }

This function sets the label text.

### t12/cannonfield.h

The `CannonField` now has two new signals: `hit()` and `missed()`. In addition, it contains a target.

         void newTarget();

This slot creates a target at a new position.

     signals:
         void hit();
         void missed();

The `hit()` signal is emitted when a shot hits the target. The `missed()` signal is emitted when the shot moves beyond the right or bottom edge of the widget (i.e., it is certain that it has not and will not hit the target).

         void paintTarget(QPainter &painter);

This private function paints the target.

         QRect targetRect() const;

This private function returns the enclosing rectangle of the target.

         QPoint target;

This private variable contains the center point of the target.

### t12/cannonfield.cpp

     #include <stdlib.h>

We include the `<stdlib.h>` header file because we need the `rand()` function.

         newTarget();

This line has been added to the constructor. It creates a "random" position for the target. In fact, the `newTarget()` function will try to paint the target. Because we are in a constructor, the `CannonField` widget is invisible. Qt guarantees that no harm is done when calling QWidget::update() on a hidden widget.

     void CannonField::newTarget()
     {
         static bool firstTime = true;

         if (firstTime) {
             firstTime = false;
             QTime midnight(0, 0, 0);
             qsrand(midnight.secsTo(QTime::currentTime()));
         }
         target = QPoint(200 + qrand() % 190, 10 + qrand() % 255);
         update();
     }

This private function creates a target center point at a new random position.

We use the `rand()` function to fetch random integers. The `rand()` function normally returns the same series of numbers each time you run a program. This would make the target appear at the same position every time. To avoid this, we must set a random seed the first time this function is called. The random seed must also be random in order to avoid equal random number series. The solution is to use the number of seconds that have passed since midnight as a pseudo-random value.

First we create a static `bool` local variable. A static variable like this one is guaranteed to keep its value between calls to the function.

The `if` test will succeed only the first time this function is called because we set `firstTime` to `false` inside the `if` block.

Then we create the QTime object `midnight`, which represents the time 00:00:00. Next we fetch the number of seconds from midnight until now and use it as a random seed. See the documentation for QDate, QTime, and QDateTime for more information.

Finally we calculate the target's center point. We keep it within the rectangle (x = 200, y = 35, width = 190, height = 255), i.e., the possible x and y values are 200 to 389 and 35 to 289, respectively) in a coordinate system where we put y position 0 at the bottom edge of the widget and let y values increase upwards x is as normal, with 0 at the left edge and with x values increasing to the right.

By experimentation we have found this to always be in reach of the shot.

     void CannonField::moveShot()
     {
         QRegion region = shotRect();
         ++timerCount;

         QRect shotR = shotRect();

This part of the timer event has not changed from the previous chapter.

         if (shotR.intersects(targetRect())) {
             autoShootTimer->stop();
             emit hit();

This `if` statement checks whether the shot rectangle intersects the target rectangle. If it does, the shot has hit the target (ouch!). We stop the shoot timer and emit the `hit()` signal to tell the outside world that a target was destroyed, and return.

Note that we could have created a new target on the spot, but because the `CannonField` is a component we leave such decisions to the user of the component.

         } else if (shotR.x() > width() || shotR.y() > height()) {
             autoShootTimer->stop();
             emit missed();

This `if` statement is the same as in the previous chapter, except that it now emits the `missed()` signal to tell the outside world about the failure.

         } else {
             region = region.unite(shotR);
         }
         update(region);
     }

And the rest of the function is as before.

`CannonField::paintEvent()` is as before, except that this has been added:

         paintTarget(painter);

This line makes sure that the target is also painted when necessary.

     void CannonField::paintTarget(QPainter &painter)
     {
         painter.setPen(Qt::black);
         painter.setBrush(Qt::red);
         painter.drawRect(targetRect());
     }

This private function paints the target; a rectangle filled with red and with a black outline.

     QRect CannonField::targetRect() const
     {
         QRect result(0, 0, 20, 10);
         result.moveCenter(QPoint(target.x(), height() - 1 - target.y()));
         return result;
     }

This private function returns the enclosing rectangle of the target. Remember from `newTarget()` that the `target` point uses y coordinate 0 at the bottom of the widget. We calculate the point in widget coordinates before we call QRect::moveCenter().

The reason we have chosen this coordinate mapping is to fix the distance between the target and the bottom of the widget. Remember that the widget can be resized by the user or the program at any time.

### t12/main.cpp

There are no new members in the `MyWidget` class, but we have slightly changed the constructor to set the new `LCDRange` text labels.

         LCDRange *angle = new LCDRange(tr("ANGLE"));

We set the angle text label to "ANGLE".

         LCDRange *force = new LCDRange(tr("FORCE"));

We set the force text label to "FORCE".

## Running the Application

The `LCDRange` widgets look a bit strange: When resizing `MyWidget`, the built-in layout management in QVBoxLayout gives the labels too much space and the rest not enough; making the space between the two `LCDRange` widgets change size. We'll fix that in the next chapter.

## Exercises

Make a cheat button that, when pressed, makes the `CannonField` display the shot trajectory for five seconds.

If you did the "round shot" exercise from the previous chapter, try changing the `shotRect()` to a `shotRegion()` that returns a QRegion so you can have really accurate collision detection.

Make a moving target.

Make sure that the target is always created entirely on-screen.

Make sure that the widget cannot be resized so that the target isn't visible. [Hint: QWidget::setMinimumSize() is your friend.]

Not easy; make it possible to have several shots in the air at the same time. [Hint: Make a `Shot` class.]

[Previous: Chapter 11] [Qt Tutorial] [Next: Chapter 13]