Журнал LinuxFormat - перейти на главную

LXF105:Qt4

Материал из Linuxformat
Версия от 20:26, 22 августа 2009; Yaleks (обсуждение | вклад)

(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск
Программирование в стиле Qt Осваиваем технологии, лежащие в основе нашумевшего KDE4

Содержание

Новый взгляд на старую графику

Qt4
ЧАСТЬ 4 Представьте себе Сокобан, а потом – замените ящики на кнопки и поля ввода. Ничего себе дизайнер интерфейсов? Со средой Graphics View возможно и не такое, утверждает Андрей Боровский.
– А что у них за игра? Шахматы? Или какой-нибудь «стартрек»?
– Нет, здесь игра для профессионалов... Садишься за штурвал воображаемого космолета и определяешь гравитацию незнакомой тебе планеты. Ее автомат подбирает случайным образом.
М. Пухов, Путь к Земле («Кон-Тики»).

Среда Graphics View Framework, появившаяся в Qt начиная с версии 4.2, пришла на смену графической системе, основанной на классе QCanvas. Graphics View Framework – это не только система вывода графики с широкими возможностями, но и готовая реализация парадигмы «модель-вид-контроллер» (Model-View-Controller, MVC) для программ, работающих с 2D-изображениями. Мы уже встречались с шаблоном MVC, когда изучали каркас Interview Framework, предназначенный для работы с данными, хранящимися в форме таблиц. Graphics View Framework распространяет те же идеи на двумерную графику. Для объяснения преимуществ Interview Framework мы пользовались программой, работающей с базой данных. Возможности же Graphics View Framework проще всего продемонстрировать на примере компьютерной аркады.

Предположим, вы решили написать двумерную видеоигру. Применение подхода «модель-контроллер-вид» может существенно упростить процесс создания такой программы. Описание игрового мира представляет собой модель данных программы. Визуализацию сцены выполняет объект отображения (вид). Контроллер транслирует действия пользователя в события модели. Система Graphics View Framework предоставляет вам заготовки для создания модели, контроллера и объекта отображения, изначально наделенные широкой функциональностью. Кроме того, Graphics View Framework берет на себя решение таких задач, как обнаружение столкновений (collision detection) и геометрические преобразования изображений.

Разумеется, Graphics View Framework может найти применение не только в играх, но и в любых программах, которым приходится отображать интерактивные графические модели, состоящие из большого числа элементов.

Рис.1Рис. 1. Схема Graphics View Framework.

Основу Graphics View Framework составляют три Qt-класса, представленные на схеме (рис. 1).

Модель данных реализована с помощью объекта класса QGraphicsScene. Элементами модели данных являются графические примитивы (геометрические фигуры и растровые изображения). Все графические примитивы реализованы с помощью классов-потомков класса QGraphicsItem. Таким образом, объект класса QGraphicsScene можно рассматривать как контейнер для набора объектов классов- потомков QGraphicsItem. Для отображения модели, созданной в QGraphicsScene, служит объект класса QGraphicsView. Работая в среде Graphics View Framework, вы не рисуете изображение непосредственно в окне QGraphicsView (хотя в принципе это делать можно). Вместо этого вы управляете объектами, хранящимися в модели QGraphicsScene. Все изменения объектов модели автоматически отображаются в окне QGraphicsView. При этом вам не нужно заботиться о таких вещах, как перерисовка изображения при изменении размеров окна. Поскольку объект класса QGraphicsView связан с моделью, он «знает», что нужно отображать в окне, и обновляет содержимое автоматически.

Вторая важная задача, которую решает связка объектов QGraphicsView и QGraphicsScene – преобразование действий пользователя (таких, как щелчок мышью, перемещение курсора над объектом или нажатие клавиши) в события модели. Последние могут быть переданы отдельным примитивам, формирующим модель. Эта система передачи событий между разными уровнями Graphics View Framework именуется в документации Qt термином «event propagation».

Упомянутые выше функции обнаружения столкновений и геометрических преобразований реализованы в классах QGraphicsScene и QGraphicsItem. Все эти операции выполняются независимо от уровня отображения (на него передается только конечный результат операций). Так же, как и в системе Interview Framework, с одной моделью Graphics View может быть связано несколько объектов отображения.

Первая проба

Рассмотрим работу простейшего приложения Graphics View Framework, выводящего на экран статическое изображение. Эта программа должна выполнить минимальную последовательность операций, необходимых для работы с Graphics View Framework: создать объекты QGraphicsScene и QGraphicsView и связать их между собой, затем заполнить объект QGraphicsScene графическими примитивами и сделать объект QGraphicsView видимым. Написание программы мы начнем с редактирования визуальной части.

Виджет QGraphicsView расположен на панели виджетов Qt Designer в разделе Display Widgets. Класс QGraphicsView является потомком Qframe, и его удобно сделать центральным визуальным элементом главного окна. Далее в программе следует создать объект класса QGraphicsScene (это можно сделать, например, в конструкторе главного окна). С помощью метода setScene() объекта QGraphicsView мы связываем объект QGraphicsScene с объектом QGraphicsView.

  QGraphicsScene * scene = new QGraphicsScene;
  graphicsView->setScene(scene);

Добавлять графические примитивы в объект QGraphicsScene можно разными способами, в том числе с помощью методов группы Add* класса QGraphicsScene. Например, для того, чтобы добавить в сцену эллипс, следует вызвать:

scene->addEllipse(QRectF(-100.0, -100.0, 100.0, 100.0));

где scene – объект QGraphicsScene. Обратите внимание на то, что параметры эллипса (точнее, координаты углов прямоугольника, в который он вписан) задаются числами с плавающей точкой, а не целыми, как обычно принято в растровой графике. Ниже мы увидим, что встроенная в Graphics View система геометрических преобразований, а также наличие нескольких систем координат, делают использование чисел с плавающей точкой совершенно необходимым. Координаты, которые мы указали при добавлении эллипса, являются координатами модели, а не графического окна. При отображении модели объектом QGraphicsView они будут автоматически переведены в координаты окна QgraphicsView.

Как соотносятся точки начала координат модели и начала координат окна? Ответ на этот вопрос может показаться неожиданным: соотношение систем координат зависит от размеров изображения и размеров окна. По умолчанию графическая система располагает изображение, созданное в QGraphicsView, таким образом, чтобы его геометрический центр совпадал с центром окна QGraphicsView. Если размеры изображения превышают размеры окна, в окне появляются полосы прокрутки. Все это означает, что не существует простой формулы для перевода координат окна в координаты модели и обратно. Если вас не увлекают занятия аналитической геометрией, для пересчета координат лучше воспользоваться специальными функциями, предоставляемыми системой.

Коль скоро речь зашла о координатах, следует отметить, что помимо систем координат окна и модели нам придется иметь дело с еще одной системой координат – системой координат примитива. У каждого графического примитива есть своя система координат, которая используется при выполнении над ним геометрических преобразований (перенос, вращение, масштабирование). Мы подробно займемся ею ниже.

В процессе передачи событий от одного уровня к другому система Graphics View Framework выполняет преобразования координат. Например, если ваша модель обрабатывает щелчки мыши, координаты курсора мыши в окне QGraphicsView в момент щелчка будут автоматически переведены в координаты модели. Если событие мыши связано с одним из графических примитивов, то координаты курсора будут отображены также в систему координат примитива. Таким образом, в среде Graphics View зачастую приходится иметь дело с тремя наборами координат одной и той же точки (правда, не все эти координаты будут нам нужны).

Теперь мы должны сделать объект QGraphicsView видимым с помощью метода show(). Далее можно скомпилировать пограмму. Система Graphics View является частью ядра Qt, поэтому подключать дополнительные модули нам не требуется. В результате работы нашей программы мы получаем окно, в котором на белом фоне изображена черная окружность. Рисунок этот, конечно, не особенно впечатляет, но зато наше знакомство с Graphics View Framework можно считать состоявшимся.

Пишем свою игру

Рис.2 Рис. 2. Наш Сокобан.

Для более подробного знакомства с возможностями Graphics View мы напишем обещанную игру – подобие всем известного «Сокобана» (рис. 2). Напомню правила этой древней и мудрой игры: по лабиринту ходит грузчик, задача которого заключается в том, чтобы перенести хаотично разбросанные ящики в заранее определенное место. Грузчик может только толкать ящик перед собой (тащить его он не умеет), причем в каждый момент времени он может толкать только один ящик. Полный исходный текст программы вы найдете на диске в файле sokoban.tar.gz.

Для реализации игры нам понадобится создать потомка класса QGraphicsScene:

 class MvScene : public QGraphicsScene
 {
 public:
    MvScene(QObject *parent = 0);
 protected:
    virtual void mousePressEvent(QGraphicsSceneMouseEvent *
 mouseEvent);
    virtual void keyPressEvent(QKeyEvent * keyEvent);
 private:
    QGraphicsPixmapItem * worker;
    void makeWalls();
    QGraphicsItem * itemCollidesWith(QGraphicsItem * item);
    void placeBox(float x, float y);
    void setBoxes();
 };

В отличие от обычной картинки, сцена из игрового мира должна реагировать на действия пользователя. В нашем классе MvScene мы переопределяем функции-обработчики событий mousePressEvent() и keyPressEvent() (для этого, собственно говоря, мы и создаем новый класс). Кроме того, в нашем классе реализовано несколько вспомогательных функций. Метод makeWalls() создает стены лабиринта, метод setBoxes() размещает ящики, метод placeBox() нужен для добавления одного ящика в лабиринт, а метод itemCollidesWith() используется для обнаружения столкновений.

Метод makeWalls() добавляет в объект-сцену прямоугольники, заполненные рисунком текстуры стены.

 void MvScene::makeWalls()
 {
             float walls[11][4] = {{0, 0, 25, 245}, {25, 0, 425, 25}, {425, 0, 25,
 245}, ...};
             QBrush brush(QColor(255, 255, 255), QPixmap(“wall.jpg));
             QPen pen(Qt::NoPen);
             for (int i = 0; i < 11; i++) {
                           QGraphicsItem * item =
                           addRect(QRectF(walls[i][0], walls[i][1], walls[i][2],
 walls[i][3]), pen, brush);
                           item->setData(0, “Wall”);
             }
 }

Прямоугольники добавляются в сцену с помощью метода addRect(). В Qt 4.3 и 4.4 этот метод доступен в нескольких перегруженных вариантах. Мы используем разновидность, которая доступна во всех версиях Qt, начиная с 4.2. Первым аргументом метода addRect() является объект QRectF, который содержит координаты верхнего левого угла прямоугольника, его ширину и высоту. Второй и третий аргументы – соответственно перо и кисть, с помощью которых рисуется прямоугольник. Метод addRect() возвращает указатель на объект класса QGraphicsRectItem, являющегося потомком QGraphicsItem.

Рассмотрим подробнее метод setData() класса QGraphicsItem. Помимо графических свойств, таких как координаты и параметры кисти и пера, примитивы среды Graphics View могут быть наделены дополнительными свойствами, определяющими их поведение в модели данных. Мы можем добиться этого, создавая новые классы на базе классов графических примитивов, но система Graphics View предлагает нам и более простой путь. Каждый объект класса-потомка QGrapihcsItem является контейнером, в который можно добавлять произвольные данные. Именно это и делает метод setData(). Первым аргументом метода является численный идентификатор элемента данных (ключ), вторым аргументом – сами данные, представленные в виде значения типа QVariant. В нашей программе мы добавляем в каждый графический примитив один дополнительный элемент данных с ключом 0 и строковым значением. В строке записывается название предмета, которому соответствует данный примитив – стена (Wall) или ящик (Box). Эта информация понадобится нам для ответа на вопрос, как грузчик (объект worker) должен реагировать на столкновение с соответствующим примитивом.

Изображение грузчика добавляется в графическую сцену с помощью метода addPixmap():

 worker = addPixmap(QPixmap(“Worker.gif));

Рассмотрим теперь метод keyPressEvent(), который является движущей силой всей нашей игры:

 void MvScene::keyPressEvent(QKeyEvent * keyEvent)
 {
             QPointF np;
             np.setX(0);
             np.setY(0);
             switch (keyEvent->key()) {
                          case Qt::Key_Left:
                                      np.setX(-10);
                                      break;
                          case Qt::Key_Right:
                                      np.setX(10);
                                      break;
                          case Qt::Key_Up:
                                      np.setY(-10);
                                      break;
                          case Qt::Key_Down:
                                      np.setY(10);
                                      break;
             }
             worker->translate(np.x(), np.y());
             QGraphicsItem * obstacle = itemCollidesWith(worker);
             if (obstacle) {
         if (obstacle->data(0) == “Wall”) {
                                      worker->translate(-np.x(), -np.y());
                                      printf(“Hello wall!\n”);
             }
             else
             if (obstacle->data(0) == “Box”)
             obstacle->translate(np.x(), np.y());
             if (itemCollidesWith(obstacle) || itemCollidesWith(worker))
 {
                          obstacle->translate(-np.x(), -np.y());
                          worker->translate(-np.x(), -np.y());
                          printf(“Cannot move!\n”);
                                      }
                          }
             }
 }

В этом методе мы решаем несколько задач: перемещаем грузчика по игровому полю в направлении, заданном нажатой клавишей (для управления грузчиком используются кнопки со стрелками), выявляем столкновения грузчика с предметами игрового мира и обрабатываем эти столкновения согласно правилам игры. Перемещение грузчика по сцене выполняется с помощью метода translate() класса QGraphicsItem. Этот метод, наряду с методами rotate() и scale(), входит в базовый интерфейс геометрических преобразований Graphics View Framework.

Чтобы понять, как работают эти методы, нужно вернуться к описанию различных систем координат, которые используются в графической системе Qt 4. Методы, выполняющие геометрические преобразования примитива, работают в его системе координат. Особенностью данной системы координат являются то, что координаты примитива в ней никогда не меняются. Иначе говоря, при переносе, вращении и масштабировании примитива его система координат также подвергается переносу, вращению и масштабированию относительно других систем координат. Например, после поворота примитива на 60 градусов оси его системы координат также будут повернуты на 60 градусов, и в результате перенос примитива вдоль одной из осей будет выполняться под углом к границе экрана. Начиная с Qt 4.3, у класса QGraphicsItem появились методы, позволяющие напрямую манипулировать матрицей преобразований (мы рассмотрим их далее, в разделе, посвященном встраиваемым виджетам). При таких сложных отношениях между системами координат функции, предназначенные для перевода значений из одной системы координат в другую, играют особую роль. Метод mapToScene() класса QGraphicsItem выполняет перевод значений из системы координат примитива в систему координат сцены, а метод mapToItem() – перевод из системы координат сцены в систему координат примитива.

Вернемся к нашей программе. Мы перемещаем грузчика в новую позицию и с помощью вспомогательной функции itemCollidesWith() проверяем, не столкнулся ли он с другим примитивом. Если грузчик натолкнулся на стену, мы просто возвращаем его в исходную позицию. Если препятствием оказался ящик, мы перемещаем ящик в новую позицию и проверяем, не натолкнулся ли ящик на препятствие. После перемещения ящика мы также проверяем, не сталкивается ли грузчик еще с каким-нибудь препятствием. В классическом Сокобане грузчик, ящик и сегмент стены имеют одинаковые размеры, поэтому в каждый момент времени грузчик может столкнуться только с одним препятствием. Наш вариант сложнее, поскольку все объекты имеют разные размеры, и грузчик может натолкнуться на ящик и стену одновременно. Если хотя бы одно из перечисленных выше условий не выполнено, и грузчик, и ящик, который он сдвинул, возвращаются в исходные позиции. Поскольку перерисовка сцены выполняется только после выхода из метода, пользователь не увидит всех этих пробных перемещений.

Обнаружение столкновений в нашей игре выполняет вспомогательная функция itemCollidesWith():

 QGraphicsItem * MvScene::itemCollidesWith(QGraphicsItem * item)
 {
            QList<QGraphicsItem *> collisions = collidingItems(item);
            foreach (QGraphicsItem * it, collisions) {
            if (it == item)
                 continue;
                          return it;
            }
            return NULL;
 }

Функция возвращает первый примитив, с которым столкнулся интересующий нас объект, или NULL, если проверяемый примитив ни с чем не столкнулся. В основе нашей функции лежит метод collidingItems() класса QGraphicsScene. Этот метод возвращает список примитивов, находящихся в состоянии столкновения с примитивом, переданным методу в качестве параметра (под столкновением понимается частичное или полное перекрытие примитивов в системе координат сцены). Однако список, возвращаемый методом collidingItems(), никогда не бывает пустым. В нем всегда содержится как минимум один примитив – тот, который мы проверяем на столкновения. С точки зрения графической системы примитив всегда сталкивается с самим собой. Любители философской диалектики могут увидеть в этом глубокий смысл, нам же при обнаружении столкновения просто приходится пропускать один из элементов списка. Обратите внимание на конструкцию foreach(). Это не новый оператор языка C++, а макрос Qt 4, упрощающий перебор элементов списка, созданного на основе шаблона.

Наша программа обрабатывает также щелчки мыши. Вообще-то в игре Сокобан мыши делать нечего, но в нашем варианте щелчок левой кнопкой мыши позволяет добавить ящик в лабиринт, а щелчок правой кнопкой – удалить уже существующий ящик. С помощью метода itemAt() класса QGraphicsScene можно проверить, попал ли указатель в какой-нибудь графический примитив (в этом случае метод itemAt() возвращает указатель на соответствующий объект). В качестве аргумента методу itemAt() передаются координаты указателя мыши в системе сцены. Координаты указателя мыши в системе координат сцены мы можем получить с помощью метода scenePos() объекта mouseEvent (указатель на этот объект передается методуобработчику события мыши mousePressEvent()). Помимо метода itemAt(), у нас есть еще один способ заставить сцену реагировать на события мыши. Мы можем назначать собственные обработчики событий мыши графическим примитивам (объектам QGraphicsItem). Благодаря системе «event propagation», обработчик будет вызываться только в том случае, если указатель мыши попал в соответствующий примитив, однако подробное описание этого способа выходит за рамки статьи.

У двумерных примитивов Graphics View Framework есть и третья координата – z. Она определяет, какой из примитивов будет виден на экране, если несколько объектов частично или полностью перекрываются. Кроме того, от значения третей координаты зависит порядок, в котором располагаются примитивы в списке, возвращаемом методом collidingItems() (первым в этом наборе располагается примитив с наименьшим значением z). Если данный примитив полностью скрыт другим примитивом с более высоким значением z, метод QGraphicsItem::isObscured() возвращает значение true. Изменить значение координаты z графического примитива можно с помощью метода setZValue() класса QGraphicsItem.

Встраивание виджетов

Начиная с Qt 4.4, система Graphics View обогатилась еще одной весьма интересной возможностью. Речь идет о встраивании виджетов в графическую сцену. В Qt 4.4 у класса QGraphicsScene появился метод addWidget(), который позволяет добавлять в сцену виджеты как обычные графические примитивы. При этом элементы управления, встроенные в графическую сцену, не теряют своей функциональности. Благодаря механизму передачи событий Graphics View Framework встроенные виджеты реагируют на действия пользователя точно так же, как и их обычные собратья. Впрочем, некоторые отличия в поведении встроенных виджетов все-таки присутствуют. Например, диалоговое окно, встроенное в графическую сцену, будет вести себя не совсем так, как независимое. Одновременно с этим встроенные виджеты обладают свойствами графических примитивов среды Graphics View.

Рис.3Рис. 3. Психоделический интерфейс пользователя средствами Qt 4.4.

Со встроенными виджетами можно выполнять те же геометрические преобразования, что и с остальными примитивами, для них так же работает обнаружение столкновений и другие функции графической системы. Встраивание виджетов является логическим развитием одной из основных идей системы Graphics View – использования возможностей двумерной графики для построения сложных пользовательских интерфейсов. В то же время, с помощью встраивания виджетов можно создать интерфейсы, которые будут выглядеть, мягко говоря, необычно. На диске вы найдете программу crasyiface, демонстрирующую некоторые возможности встраивания виджетов (рис. 3).

Рассмотрим фрагмент конструктора объекта-сцены программы crasyiface:

           QPushButton * button = new QPushButton(trUtf8(“Кнопочка”), 0);
           QGraphicsProxyWidget * item = addWidget(button);
           button->show();
           button = new QPushButton(trUtf8(“Кнопочка”), 0);
           item = addWidget(button);
           button->show();
           QTransform transform = item->transform();
           transform.translate(50., 30.);
           transform.rotate(60.0);
           item->setTransform(transform);
           button = new QPushButton(trUtf8(“Еще кнопочка”), 0);
           item = addWidget(button);
           button->show();
           transform = item->transform();
           transform.rotate(80.0, Qt::YAxis);
           transform.translate(-10., 90.);
           transform.scale(5., 2.);
           item->setTransform(transform);
           QProgressDialog * dialog = new QProgressDialog(trUtf8(“Прогресс”), trUtf8(“Отмена”), 0, 100);
           dialog->setWindowTitle(trUtf8(“Progress Dialog”));
           item = addWidget(dialog);
           dialog->show();
           dialog->setValue(66);
           transform = item->transform();
           transform.translate(200., 75.);
           transform.rotate(-45.0, Qt::YAxis);
           transform.scale(2.5, 2.);
           item->setTransform(transform);

Чтобы добавить виджет в графическую сцену, мы сначала создаем объект соответствующего виджету класса, а потом вызываем метод QGraphicsScene::addWidget(). Он возвращает указател на объект класса QGraphicsProxyWidget. Этот класс является отдаленным потомком класса QGraphicsItem и представляет встроенный виджет в графической сцене. По умолчанию виджеты создаются невидимыми, и вызов addWidget() не изменяет их состояния, поэтому мы вызываем метод show(). Для выполнения геометрических преобразований виджета мы воспользуемся матрицей преобразований, которая, напомню, появилась в Qt 4.3.

Матрица может быть создана многими способами (да, Нео, это так). Мы получаем ссылку на объект, инкапсулирующий матрицу (экземпляр класса QTransform) с помощью метода transform() объекта класса QGraphicsProxyWidget. У класса QTransform есть методы translate(), rotate() и scale(), которые работают не совсем так, как одноименные методы класса QGraphicsItem. При вызове метода rotate() мы, помимо угла поворота, можем указать ось, вокруг которой должно выполняться вращение. Вращать примитивы можно не только вокруг оси z (что соответствует вращению в плоскости x–y), но и вокруг осей x и y. В результате графической сцене можно придать трехмерный вид. Разумеется, это не настоящая «трехмерность», так как координата z не является по-настоящему независимой, но если в качестве основы графического вывода используется портал OpenGL (в документации Qt описано, как можно задействовать OpenGL при работе с виджетом QGraphicsView), то для визуализации примитивов будут задействованы наличные возможности 3D-ускорителя. После того, как мы внесли изменения в матрицу преобразований, мы снова назначаем эту матрицу примитиву с помощью метода setTransform().

Обратите внимание на то, что виджеты, встроенные в окно программы crasyiface, сохраняют свою функциональность. Кнопки реагируют на щелчки мыши, а встроенное диалоговое окно можно даже закрыть, щелкнув соответствующую кнопку в его заголовке.

В последнее время мы много занимались графикой в Qt 4, однако новая версия Qt может пригодиться и тем, кто пишет консольные программы. Следующая статья начнется с описания системы Qt Console. LXF

Персональные инструменты
купить
подписаться
Яндекс.Метрика