| |
А что у них за игра? Шахматы? Или какой-нибудь стартрек?
Нет, здесь игра для профессионалов...
Садишься за штурвал воображаемого космолета
и определяешь гравитацию незнакомой тебе планеты.
Ее автомат подбирает случайным образом.
М. Пухов, Путь к Земле (Кон-Тики).
Система Graphics View Framework, появившаяся в Qt, начиная с версии 4.2, пришла на смену графической системе, основанной на классе QCanvas. Graphics View Framework это не только система вывода графики с широкими возможностями, но и готовая реализация парадигмы модель-контроллер-вид для программ, работающих с двухмерной графикой. Мы уже встречались с парадигмой модель-контроллер-вид, когда изучали систему Interview Framework, предназначенную для работы с данными, хранящимися в форме таблиц. Graphics View Framework распространяет те же идеи на двухмерную графику. Для объяснения преимуществ Interview Framework мы пользовались программой, работающей с базой данных. Возможности Graphics View Framework проще всего продемонстрировать на примере компьютерной аркады. Предположим, вы решили написать двухмерную компьютерную игру. Применение подхода модель-контроллер-вид может существенно упростить процесс создания такой игры. Описание игрового мира представляет собой модель данных программы. Визуализацию игрового мира выполняет объект отображения (вид). Контроллер транслирует действия пользователя в события модели. Система Graphics View Framework предоставляет вам заготовки для создания модели, контроллера и объекта отображения, изначально наделенные широкой функциональностью. Кроме того Graphics View Framework берет на себя решение таких задач как обнаружение столкновений (collision detection) и геометрические преобразования изображений.
Разумеется, Graphics View Framework может найти применение не только в играх, но и в любых программах, которым приходится отображать интерактивные графические модели, состоящие из большого числа элементов. Основу Graphics View Framework составляют три класса Qt Library, представленные на схеме (рис. 1).
Рисунок 1. Схема Graphics View Framework.
Модель данных реализована с помощью объекта класса 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 Framework может быть связано несколько объектов отображения.
Рассмотрим работу простейшей программы 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 Framework система геометрических преобразований, а также наличие нескольких систем координат, делают использование чисел с плавающей точкой совершенно необходимым. Координаты, которые мы указали при добавлении эллипса, являются координатами модели, а не графического окна. При отображении модели объектом QGraphicsView они будут автоматически переведены в координаты окна QGraphicsView. Как соотносятся точки начала координат модели и начала координат окна? Ответ на этот вопрос может показаться неожиданным: соотношение систем координат зависит от размеров изображения и размеров окна. По умолчанию графическая система располагает изображение, созданное в QGraphicsView, таким образом, чтобы его геометрический центр совпадал с центром окна QGraphicsView. Если размеры изображения превышают размеры окна, в окне появляются полосы прокрутки. Все это означает, что не существует простой формулы для перевода координат окна в координаты модели и обратно. Если вас не увлекают занятия аналитической геометрией, для пересчета координат лучше воспользоваться специальными функциями, предоставляемыми системой.
Коль скоро речь зашла о координатах, следует отметить, что помимо систем координат окна и модели нам придется иметь дело с еще одной системой координат системой координат примитива. У каждого графического примитива есть своя система координат, которая используется при выполнении геометрических преобразований примитива (перенос, вращение, масштабирование) и мы подробно займемся ею ниже.
В процессе передачи событий от одного уровня к другому система Graphics View Framework выполняет преобразования координат. Например, если ваша модель обрабатывает щелчки мыши, координаты курсора мыши в окне QGraphicsView в момент щелчка будут автоматически переведены в координаты модели. Если событие мыши связано с одним из графических примитивов, то координаты курсора будут отображены также в систему координат примитива. Таким образом, в Graphics View Framework зачастую приходится иметь дело с тремя наборами координат одной и той же точки (правда, не все эти координаты будут нам нужны).
Теперь мы должны сделать объект QGraphicsView видимым с помощью метода show(). Далее можно скомпилировать программу. Система Graphics View Framework является частью ядра Qt, поэтому подключать дополнительные модули нам не требуется. В результате работы нашей программы мы получаем окно, в котором на белом фоне изображена черная окружность. Рисунок этот, конечно, не особенно впечатляет, но наше знакомство с Graphics View Framework состоялось.
Для более подробного знакомства с возможностями Graphics View Framework мы напишем обещанную игру, подобие всем известного Сокобана (рис. 2). Напомню правила этой древней и мудрой игры: по лабиринту ходит грузчик, задача которого заключается в том, чтобы перенести хаотично разбросанные ящики в заранее определенное место. Грузчик может только толкать ящик перед собой (тащить за собой не может), причем в каждый момент времени он может толкать только один ящик. Полный исходный текст программы вы найдете по ссылке в конце станицы.
Рисунок 2. Наш Сокобан
Для реализации игры нам понадобится создать потомка класса 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 Framework могут быть наделены дополнительными свойствами, определяющими их поведение в модели данных. Мы можем наделить объекты дополнительными свойствами, создавая новые классы на базе классов графических примитивов, но Graphics View Framework предлагает нам и более простой путь. Каждый объект класса-потомка 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, метод isObscured() возвращает значение true. Изменить значение координаты z графического примитива можно с помощью метода setZValue() класса QGraphicsItem.
Начиная с Qt 4.4, система Graphics View Framework обогатилась еще одной весьма интересной возможностью. Речь идет о встраивании виджетов в графическую сцену. В Qt 4.4 у класса QGraphicsScene появился метод addWidget(), который позволяет добавлять в сцену виджеты как обычные графические примитивы. Виджеты, встроенные в графическую сцену, не теряют своей функциональности. Благодаря механизму передачи событий Graphics View Framework встроенные виджеты реагируют на действия пользователя точно так же, как и их обычные собратья. Впрочем, некоторые отличия в поведении встроенных виджетов все таки присутствуют. Например, диалоговое окно, встроенное в графическую сцену, будет вести себя не совсем так, как независимое диалоговое окно. Одновременно с этим встроенные виджеты обладают свойствами графических примитивов Graphics View Framework. Со встроенными виджетами можно выполнять те же геометрические преобразования, что и с остальными примитивами, для них так же работает обнаружение столкновений и другие функции графической системы. Встраивание виджетов является логическим развитием одной из основных идей Graphics View Framework использования возможностей двухмерной графики для построения сложных пользовательских интерфейсов. В то же время с помощью встраивания виджетов можно создать интерфейсы, которые будут выглядеть, мягко говоря, необычно. На диске вы найдете программу crasyiface, демонстрирующую некоторые возможности встраивания виджетов (рис. 3).
Рисунок 3. Психоделический интерфейс пользователя средствами Qt 4.4.
Рассмотрим фрагмент конструктора объекта-сцены программы 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);
Для того чтобы добавить виджет в графическую сцену, мы сначала создаем объект соответствующего виджету класса, а потом вызываем метод addWidget(). Метод addWidget() класса QGraphicsScene возвращает указатель на объект класса 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.
Исходные тексты примеров (Сокобан)
Исходные тексты примеров (Встраиваемые виджеты)
Закладки на сайте Проследить за страницей |
Created 1996-2024 by Maxim Chirkov Добавить, Поддержать, Вебмастеру |