Главная · Все классы · Основные классы · Классы по группам · Модули · Функции

[Предыдущий: Урок 11] [Учебное пособие] [Следующий: Урок 13]

Урок 12 - Висеть в воздухе для блоков не свойственно

Файлы:

Снимок экрана к Уроку 12

В следующем примере мы расширим наш класс LCDRange для включения в него текстовой метки. Также мы предоставим что-нибудь, по чему можно пострелять.

Разберем программу строка за строкой

t12/lcdrange.h

Теперь LCDRange содержит текстовую метку.

 class QLabel;
 class QSlider;

Мы делаем опережающее объявление QLabel и QSlider поскольку мы хотим использовать указатели на них в определении класса. Мы можем также использовать #include, но это лишь увеличит время компиляции.

 class LCDRange : public QWidget
 {
     Q_OBJECT

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

Мы добавили новый конструктор, который помимо указания родительского виджета устанавливает текст метки.

     QString text() const;

Эта функция возвращает текст метки.

     void setText(const QString &text);

Этот слот устанавливает текст метки.

 private:
     void init();

Поскольку теперь мы имеем два конструктора, мы решили поместить общую инициализирующую часть в закрытую функцию init().

     QLabel *label;

Мы также имеем в распоряжении новую закрытую переменную: типа QLabel. QLabel - один из стандартных виджетов Qt и он может отображать текст или QPixmap как с рамкой, так и без рамки.

t12/lcdrange.cpp

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

Данный конструктор вызывает функцию init(), которая содержит общий инициализирующий код.

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

Данный конструктор сначала вызывает init(), а затем устанавливает текст метки.

 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);
 }

Установка lcd и slider такая же как и в предыдущем уроке. Дальше мы создаем QLabel и сообщаем ей о необходимости выровнять содержимое по центру (по горизонтали) и по верхнему краю (по вертикали). Вызовы QObject::connect() могут быть также взяты из предыдущего урока.

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

Эта функция возвращает текст метки.

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

Эта функция устанавливает текст метки.

t12/cannonfield.h

CannonField теперь имеет два новых сигнала: hit() и missed(). Кроме того, он содержит цель.

     void newTarget();

Этот слот создает цель в новой позиции.

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

Сигнал hit() испускается когда снаряд попадает в цель. Сигнал missed() испускается когда снаряд переместится далеко за правый или верхний край виджета (т.е., несомненно не попал и не попадет в цель).

     void paintTarget(QPainter &painter);

Эта закрытая функция рисует цель.

     QRect targetRect() const;

Эта закрытая функция возвращает ограничивающий прямоугольник цели.

     QPoint target;

Эта закрытая переменная содержит центральную точку цели.

t12/cannonfield.cpp

 #include <stdlib.h>

Мы подключили заголовочный файл <stdlib.h> из-за нужной нам функции rand().

     newTarget();

Эта строчка была добавлена в конструктор. Она создает "случайную" позицию цели. Фактически, функция newTarget() будет пытаться нарисовать цель. Поскольку мы находимся в конструкторе, то виджет CannonField невидим. Qt гарантирует, что при вызове QWidget::update() для скрытого виджета никакого вреда нанесено не будет.

 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();
 }

Эта закрытая функция создает центральную точку цели в новой случайной позиции.

Мы используем функцию rand() для получения случайных чисел. Функция rand() обычно возвращает одни и те же серии чисел при каждом запуске программы. Это приводит к тому, что каждый раз цели появляются на одних и тех же позициях. Чтобы этого избежать мы должны установить начальное случайное число перед первым вызовом функции. Начальное случайное число должно также выбираться случайным образом, чтобы избежать генерирования одинаковых серий случайных чисел. Решением является использование в качестве псевдо-случайного значения количества секунд, прошедших с полуночи.

Сначала мы создаем статическую локальную переменную типа bool. Такая статическая переменная гарантирует сохранение своего значения между вызовами функции.

Проверка условия if будет успешной только при первом вызове этой функции, потому что мы установили firstTime равным false внутри блока if.

Затем мы создаем объект типа QTime midnight, который отображает время 00:00:00. Далее мы получаем число секунд, прошедших с полуночи до настоящего момента и используем его в качестве начального случайного числа. Для получения дополнительной информации смотрите документацию по QDate, QTime и QDateTime.

В завершение мы вычисляем центральную точку цели. Мы удерживаем ее внутри прямоугольника (x = 200, y = 35, width = 190, height = 255, т.е., x и y могут принимать значения от 200 до 389 и от 35 до 289, соответственно) в системе координат, где мы поместили точку 0 по оси y на нижнюю кромку виджета, а сама ось y направлена вверх; ось x - нормальная, с 0 на левой кромке виджета и увеличением значений x при движении вправо.

Поэкспериментировав мы нашли, что возможность попасть в цель снарядом существует всегда.

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

     QRect shotR = shotRect();

Эта часть события таймера не изменилась с прошлого урока.

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

Этот оператор if проверяет не пересеклись ли прямоугольник снаряда и прямоугольник цели. Если пересеклись, то снаряд попал в цель (ой!). Мы останавливаем таймер выстрела и испускаем сигнал hit(), чтобы сообщить остальным виджетам о том, что цель была уничтожена, и возвращаемся.

Обратите внимание на то, что мы немедленно создаем новую цель, но поскольку CannonField является компонентом, принятие подобных решений мы предоставляем пользователю компонента.

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

Этот оператор if такой же, как и в предыдущем уроке за исключением того, что он теперь испускает сигнал missed() чтобы сообщить остальным виджетам о промахе.

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

Оставшаяся часть функции не претерпела изменений.

CannonField::paintEvent() - такая же что и раньше, за исключением того что было добавлено следующее:

     paintTarget(painter);

Эта строчка гарантирует, что цель будет нарисована когда это будет необходимо.

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

Эта закрытая функция рисует цель; прямоугольник заливается красным цветом, а контур - черным.

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

Эта закрытая функция возвращает ограничивающий прямоугольник цели. Вспомните по newTarget() о том, что точка target использует координату y с 0 в нижней части виджета. Мы вычисляем точку в координатах виджета перед вызовом QRect::moveCenter().

Причиной выбора такого отображения координат заключается в закреплении расстояния между целью и нижней частью виджета. Помните о том, что размеры виджета могут быть изменены пользователем или программой в любой момент.

t12/main.cpp

Новых членов в классе MyWidget нет, но мы слегка изменили конструктор чтобы установить новые текстовые метки LCDRange.

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

Мы установили текст метки угла возвышения равной "ANGLE".

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

Мы установили текст метке силы выстрела равной "FORCE".

Запуск приложения

Виджеты LCDRange выглядят немного странно: При изменении размеров MyWidget встроенное в QVBoxLayout управление компоновкой предоставляет слишком много пространства меткам и недостаточно - остальным виджетам; увеличение расстояния между двумя виджетами LCDRange изменяет их размер. Мы исправим это на следующем уроке.

Домашнее задание

Сделайте жульническую кнопку, при нажатии на которую на CannonField будет показана траектория выстрела в течение пяти секунд полета снаряда.

Если вы сделали упражнение "круглый снаряд" из предыдущего урока, то попробуйте заменить shotRect() на shotRegion(), которая возвращает QRegion, так что вы получите действительно точное обнаружение столкновений.

Создайте движущуюся цель.

Обеспечьте появление цели только в пределах видимой части экрана.

Убедитесь, что размеры виджета нельзя изменить таким образом, чтобы цель стала невидимой. [Подсказка: Вам поможет QWidget::setMinimumSize().]

Не самое легкое; сделайте возможным нахождение в воздухе сразу нескольких снарядов. [Подсказка: Создайте класс Shot.]

[Предыдущий: Урок 11] [Учебное пособие] [Следующий: Урок 13]


Copyright © 2008 Trolltech Торговые марки
Qt 4.3.5