Пример "Elastic Nodes"Файлы:
Этот пример GraphicsView показывает как реализовать рёбра между узлами графа с базовым взаимодействием. Вы можете щёлкнуть для перетаскивания узла, увеличить и уменьшить масштаб используя колёсико мыши или клавиатуру. Нажатие на клавишу пробела приведёт к выбору случайного узла. Пример также не зависит от разрешения; как вы не изменяете масштаб, графика остаётся чёткой. Графическое представление предоставляет класс QGraphicsScene для управления и взаимодействия с большим числом изготовленных пользователем двухмерных элементов производных от класса QGraphicsItem, а также виджет QGraphicsView для их визуализации с поддержкой масштабирования и вращения. Этот пример состоит из класса Node, класса Edge, теста GraphWidget и функции main: класс Node представляет перетаскиваемые жёлтые узлы в сетке, класс Edge представляет линии между узлами, класс GraphWidget представляет окно приложения, а функция main() создаёт и показывает это окно, а также запускает цикл обработки событий. Определение класса NodeКласс Node служит трём целям:
Давайте начнём рассмотрение с объявления класса Node. class Node : public QGraphicsItem { public: Node(GraphWidget *graphWidget); void addEdge(Edge *edge); QList<Edge *> edges() const; enum { Type = UserType + 1 }; int type() const { return Type; } void calculateForces(); bool advance(); QRectF boundingRect() const; QPainterPath shape() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value); void mousePressEvent(QGraphicsSceneMouseEvent *event); void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); private: QList<Edge *> edgeList; QPointF newPos; GraphWidget *graph; }; Класс Node унаследован от QGraphicsItem, и переопределяет две обязательные функции boundingRect() и paint() для предоставления своего визуального внешнего вида. Также он переопределяет shape() чтобы обеспечить эллиптическую форму своей области для нажатий (в отличие от ограничивающего прямоугольника по умолчанию). В целях управления рёбрами узел предоставляет простой API для добавления рёбер к узлу и для перечисления всех соединённых рёбер. Переопределённая функция advance() вызывается всякий раз, когда состояние сцены продвинется на один шаг. Функция calculateForces() вызывается для вычисления сил, которые притягивают и отталкивают этот узел от его соседей. Класс Node также переопределяет itemChange() для реагирования на изменение состояния (в данном случае, изменение позиции), а также mousePressEvent() и mouseReleaseEvent() - для обновления визуального внешнего вида элемента. Начнём рассмотрение реализации класса Node с изучения его конструктора: Node::Node(GraphWidget *graphWidget) : graph(graphWidget) { setFlag(ItemIsMovable); setFlag(ItemSendsGeometryChanges); setCacheMode(DeviceCoordinateCache); setZValue(-1); } В конструкторе мы устанавливаем флаг ItemIsMovable чтобы разрешить элементу для перемещения в ответ на перетаскивание мышью, а также ItemSendsGeometryChanges чтобы включить уведомления itemChange() при изменении позиции и преобразований. Также мы включаем DeviceCoordinateCache для ускорения визуализации. Для обеспечения того, чтобы узлы всегда располагались на вершинах рёбер в заключение мы установим значение Z элемента равным -1. Конструктор Node'а принимает указатель GraphWidget и сохраняет его как переменную-член. Мы снова вернёмся к этому указателю позднее. void Node::addEdge(Edge *edge) { edgeList << edge; edge->adjust(); } QList<Edge *> Node::edges() const { return edgeList; } Функция addEdge() добавляет к списку присоединённых рёбер входящее ребро. Ребро затем подгоняется таким образом, чтобы точки конечны точки ребра совпадали с позициями узла-источника и узла-приёмника. Функция edges() просто возвращает список присоединённых рёбер. void Node::calculateForces() { if (!scene() || scene()->mouseGrabberItem() == this) { newPos = pos(); return; } Имеется два способа переместить узел. Функция calculateForces() реализует эффект упругости, который растягивает и сжимает узлы в сетке. Кроме того, пользователь может непосредственно переместить один узел с помощью мыши. Поскольку нам не нужно выполнение двух методов для выполнения в одно и то же время на конкретном узле, начинаем calculateForces() с проверки является ли этот Node текущим элементом, перетаскиваемым мышью (т.е., QGraphicsScene::mouseGrabberItem()). Поскольку нам нужно найти все соседние узлы (не обязательное соединённые), мы также убеждаемся, что элемент является частью сцены в первом месте. // Суммируем все силы отталкивающие данный элемент qreal xvel = 0; qreal yvel = 0; foreach (QGraphicsItem *item, scene()->items()) { Node *node = qgraphicsitem_cast<Node *>(item); if (!node) continue; QPointF vec = mapToItem(node, 0, 0); qreal dx = vec.x(); qreal dy = vec.y(); double l = 2.0 * (dx * dx + dy * dy); if (l > 0) { xvel += (dx * 150.0) / l; yvel += (dy * 150.0) / l; } } Эффект "упругости" проистекает от алгоритма, который применяет силы растяжения и сжатия. Эффект производит впечатление и на удивление просто реализуем. В алгоритме два этапа: на первом вычисляются силы, отталкивающие узлы друг от друга, а на втором - вычитание сил, притягивающих узлы друг к другу. Сначала нам нужно найти все узлы графа. Для нахождения всех элементов на сцене вызываем QGraphicsScene::items(), а затем используем qgraphicsitem_cast() чтобы найти экземпляры класса Node. Мы воспользовались mapFromItem() для создания временного вектора, направленного от данного узла ко всем остальным узлам в локальных координатах. Мы используем разложенные компоненты этого вектора чтобы определить направление и величину силы, которая будет применяться к узлу. Силы складываются в каждом узле, а затем корректируются таким образом, чтобы ближайший узел давал максимальную силу, быстро уменьшающуюся с увеличением расстояния. Сумма всех сил сохраняется в xvel (X-скорость) и yvel (Y-скорость). // Теперь вычитаем все силы, притягивающие элементы double weight = (edgeList.size() + 1) * 10; foreach (Edge *edge, edgeList) { QPointF vec; if (edge->sourceNode() == this) vec = mapToItem(edge->destNode(), 0, 0); else vec = mapToItem(edge->sourceNode(), 0, 0); xvel -= vec.x() / weight; yvel -= vec.y() / weight; } Рёбра между узлами представляют силы, которые притягивают узлы друг к другу. Пройдя по всем рёбрам, которые соединены с данным узлом, можно использовать простой метод приведённый выше для нахождения направления и величины всех сил притяжения. Эти силы вычитаются из xvel и yvel. if (qAbs(xvel) < 0.1 && qAbs(yvel) < 0.1) xvel = yvel = 0; В теории сумма притягивающих и отталкивающих сил должна быть приблизительно равна 0. Однако на практике их сумма никогда не равна нулю. Для обхода ошибок с точностью чисел, мы просто принимаем сумму сил равной нуля когда они становятся меньше, чем 0.1. QRectF sceneRect = scene()->sceneRect(); newPos = pos() + QPointF(xvel, yvel); newPos.setX(qMin(qMax(newPos.x(), sceneRect.left() + 10), sceneRect.right() - 10)); newPos.setY(qMin(qMax(newPos.y(), sceneRect.top() + 10), sceneRect.bottom() - 10)); } На последнем шаге calculateForces() определяет новую позицию узла. Мы добавили силу к текущей позиции узла. Также мы убедились, что новая позиция находится внутри определённых нами границ. В действительности в этой функции узел не перемещался; это делается в отдельном шаге, в функции advance(). bool Node::advance() { if (newPos == pos()) return false; setPos(newPos); return true; } Функция advance() обновляет текущую позицию элемента. Вызывается она из GraphWidget::timerEvent(). Если позиция элемента изменилась, то функция возвращает true; в противном случае возвращается false. QRectF Node::boundingRect() const { qreal adjust = 2; return QRectF(-10 - adjust, -10 - adjust, 23 + adjust, 23 + adjust); } Ограничивающий прямоугольник Node'а - это прямоугольник размером 20x20 с центром возле его исходной точки (0, 0), скорректированный на 2 единицы по всем измерениям для компенсации визуальной границы, и на 3 единицы вниз и вправо - чтобы создать пространство для отбрасываемой тени. QPainterPath Node::shape() const { QPainterPath path; path.addEllipse(-10, -10, 20, 20); return path; } Формой является простой эллипс. Этим гарантируется, что вы щёлкните внутри эллиптической фигуры узла при его перетаскивании. Можно протестировать этот эффект запустив пример и увеличив масштаб настолько, чтобы узлы стали очень большими. Без переопределения shape() область нажатия элемента будет совпадать с его ограничивающим прямоугольником (т.е., будет прямоугольной). void Node::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) { painter->setPen(Qt::NoPen); painter->setBrush(Qt::darkGray); painter->drawEllipse(-7, -7, 20, 20); QRadialGradient gradient(-3, -3, 10); if (option->state & QStyle::State_Sunken) { gradient.setCenter(3, 3); gradient.setFocalPoint(3, 3); gradient.setColorAt(1, QColor(Qt::yellow).light(120)); gradient.setColorAt(0, QColor(Qt::darkYellow).light(120)); } else { gradient.setColorAt(0, Qt::yellow); gradient.setColorAt(1, Qt::darkYellow); } painter->setBrush(gradient); painter->setPen(QPen(Qt::black, 0)); painter->drawEllipse(-10, -10, 20, 20); } Эта функция реализует отрисовку узла. Начинаем рисование с простой тёмно-серой тени в (-7, -7), которая простирается на (3, 3) единиц вниз и вправо от верхнего левого угла (-10, -10) эллипса. Затем рисуем эллипс с радиальной градиентной заливкой. Заливаем от Qt::yellow до Qt::darkYellow когда приподнят, или напротив - когда вдавлен. Во вдавленном состоянии также сдвигаем центр и точку фокуса на (3, 3) чтобы усилить впечатление от чего-то вдавленного. Отрисовка эллипсов залитых с градиентом может выполняться достаточно медленно, особенно если используются сложные градиенты, например QRadialGradient. Вот почему в этом примере используется DeviceCoordinateCache, простая, но эффективная мера, предотвращающая лишнюю отрисовку. QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value) { switch (change) { case ItemPositionHasChanged: foreach (Edge *edge, edgeList) edge->adjust(); graph->itemMoved(); break; default: break; }; return QGraphicsItem::itemChange(change, value); } Мы переопределили itemChange() чтобы настроить позицию всех соединяющих рёбер, и для уведомления сцены о том, что элемент передвинулся (т.е., "что-то произошло"). Это запустит новый перерасчёт сил. Это уведомление является единственной причиной, по которой узлам нужно хранить указатель на GraphWidget. В другом подходе для такого уведомления используется сигнал; в этом случае, Node необходимо унаследовать от QGraphicsObject. void Node::mousePressEvent(QGraphicsSceneMouseEvent *event) { update(); QGraphicsItem::mousePressEvent(event); } void Node::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { update(); QGraphicsItem::mouseReleaseEvent(event); } Поскольку мы установили флаг ItemIsMovable, нам не нужно реализовывать логику, которая будет перемещать узел в соответствии с вводом с мыши; нам это уже предоставлено. Однако, нам всё же нужно переопределить обработчики событий нажатия и отпускания кнопок мыши для обновления визуального внешнего вида узлов (т.е., вдавлен или приподнят). Определение класса EdgeКласс Edge представляет в этом примере линии со стрелками между узлами. Класс очень простой: он поддерживает указатели на узлы источника и приёмника и предоставляет функцию adjust(), которая гарантирует, что линия начинается в позиции источника и заканчивается в позиции приёмника. Рёбра - это только элементы, которые изменяются постоянно по мере того, как силы притягивают и отталкивают узлы. Давайте рассмотрим объявление класса: class Edge : public QGraphicsItem { public: Edge(Node *sourceNode, Node *destNode); Node *sourceNode() const; Node *destNode() const; void adjust(); enum { Type = UserType + 2 }; int type() const { return Type; } protected: QRectF boundingRect() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); private: Node *source, *dest; QPointF sourcePoint; QPointF destPoint; qreal arrowSize; }; Edge унаследован от QGraphicsItem, поскольку это простой класс, который не использует сигналы, слоты и свойства (сравните с QGraphicsObject). Конструктор принимает два указателя на узлы. Оба указателя являются обязательными в данном примере. Также для каждого узла представлены get-функции. Функция adjust() перемещает ребро, а также элемент реализует boundingRect() и paint(). Теперь рассмотрим его реализацию. Edge::Edge(Node *sourceNode, Node *destNode) : arrowSize(10) { setAcceptedMouseButtons(0); source = sourceNode; dest = destNode; source->addEdge(this); dest->addEdge(this); adjust(); } Конструктор Edge инициализирует свой член данных arrowSize значением в 10 единиц; это определяет размер стрелки, которая рисуется в paint(). В теле конструктора вызываем setAcceptedMouseButtons(0). Этим гарантируется, что краевые элементы не задействуются мышью вообще (т.е., вы не можете щёлкать по рёбрам). Затем, указатели на источник и приёмник обновляются, это ребро регистрируется с каждым узлом, и мы вызываем функцию adjust() для обновления начальной и конечной позиции данного ребра. Node *Edge::sourceNode() const { return source; } Node *Edge::destNode() const { return dest; } Get-функции источника и приёмника просто возвращают соответствующие указатели. void Edge::adjust() { if (!source || !dest) return; QLineF line(mapFromItem(source, 0, 0), mapFromItem(dest, 0, 0)); qreal length = line.length(); prepareGeometryChange(); if (length > qreal(20.)) { QPointF edgeOffset((line.dx() * 10) / length, (line.dy() * 10) / length); sourcePoint = line.p1() + edgeOffset; destPoint = line.p2() - edgeOffset; } else { sourcePoint = destPoint = line.p1(); } } В adjust() определяем две точки: sourcePoint и destPoint, указывающие на исходные точки узлов источника и приёмника, соответственно. Все точки вычисляются с использованием локальных координат. Мы хотим, чтобы наконечник рёбер указывали на точную границу узлов, а не на центры узлов. Чтобы найти эту точку, мы сначала разложим вектор указывающий из центра источника на центр узла приёмника по X и Y, а затем нормализуем компоненты поделив на длину вектора. Это даст нам единичную дельту X и Y, умножив на которую радиус узла (равный 10), получим смещение, которое нужно прибавить к одной точки ребра и вычесть из другой. Если длина вектора меньше 20 (т.е., если два узла перекрываются), тогда исправляем указатели на источника и приёмника на центр узла приёмника. На практике этот случай очень сложно воспроизвести вручную, поскольку силы между двумя узлами максимальны. Важно предупредить, что в этой функции вызывается prepareGeometryChange(). Причина заключается в том, что переменные sourcePoint и destPoint используются при рисовании непосредственно, и они возвращаются из переопределённой boundingRect(). Нужно всегда вызывать prepareGeometryChange() перед изменением того, что возвращается boundingRect(), а до этого эти переменные можно использовать в paint(), для сохранения бухгалтерской чистоты графического представления. Безопаснее вызывать эту функцию один раз, непосредственно перед изменением любой переменной. QRectF Edge::boundingRect() const { if (!source || !dest) return QRectF(); qreal penWidth = 1; qreal extra = (penWidth + arrowSize) / 2.0; return QRectF(sourcePoint, QSizeF(destPoint.x() - sourcePoint.x(), destPoint.y() - sourcePoint.y())) .normalized() .adjusted(-extra, -extra, extra, extra); } Ограничивающий прямоугольник ребра определяется как минимальный прямоугольник, который включает начальную и конечную точки ребра. Поскольку мы рисуем стрелку на каждом ребре, то необходимо также произвести коррекцию на половину размера стрелки и половину толщины пера по всем направлениям. Перо используется для отрисовки границы стрелки, и мы можем предположить, что половина границы можно нарисовать вне области стрелки, а половину - внутри. void Edge::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (!source || !dest) return; QLineF line(sourcePoint, destPoint); if (qFuzzyCompare(line.length(), qreal(0.))) return; Начнём переопределённую функцию paint() с проверки нескольких предварительных условий. Во-первых, если узел источника или приёмника не установлен, тогда немедленно возвращаемся; рисовать нечего. В то же время проверяем, не равна приблизительно ли длина ребра 0, и если это так, то также возвращаемся. // Рисуем саму линию painter->setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); painter->drawLine(line); Рисуем линию используя перо, которое имеет круглые точки соединения и края. Если вы запустите пример, увеличите масштаб и изучите ребро подробно, то вы увидите, что там нет резких/прямолинейных рёбер. // Рисуем стрелки double angle = ::acos(line.dx() / line.length()); if (line.dy() >= 0) angle = TwoPi - angle; QPointF sourceArrowP1 = sourcePoint + QPointF(sin(angle + Pi / 3) * arrowSize, cos(angle + Pi / 3) * arrowSize); QPointF sourceArrowP2 = sourcePoint + QPointF(sin(angle + Pi - Pi / 3) * arrowSize, cos(angle + Pi - Pi / 3) * arrowSize); QPointF destArrowP1 = destPoint + QPointF(sin(angle - Pi / 3) * arrowSize, cos(angle - Pi / 3) * arrowSize); QPointF destArrowP2 = destPoint + QPointF(sin(angle - Pi + Pi / 3) * arrowSize, cos(angle - Pi + Pi / 3) * arrowSize); painter->setBrush(Qt::black); painter->drawPolygon(QPolygonF() << line.p1() << sourceArrowP1 << sourceArrowP2); painter->drawPolygon(QPolygonF() << line.p2() << destArrowP1 << destArrowP2); } Переходим к рисованию по одной стрелке на каждом конце ребра. Все стрелки рисуем в виде многоугольника с заливкой чёрным цветом. Координаты стрелки определяем используя простейшую тригонометрию. Определение класса GraphWidgetGraphWidget - подкласс QGraphicsView, который предоставляет главное окно с полосами прокрутки. class GraphWidget : public QGraphicsView { Q_OBJECT public: GraphWidget(QWidget *parent = 0); void itemMoved(); protected: void keyPressEvent(QKeyEvent *event); void timerEvent(QTimerEvent *event); void wheelEvent(QWheelEvent *event); void drawBackground(QPainter *painter, const QRectF &rect); void scaleView(qreal scaleFactor); private: int timerId; Node *centerNode; }; Класс предоставляет базовый конструктор, который инициализирует сцену, функцию itemMoved() для уведомления об изменениях в графе узлов сцены, несколько обработчиков событий, переопределённая функция drawBackground() и вспомогательная функция для масштабирования области просмотра используя колёсико мыши или клавиатуру. GraphWidget::GraphWidget(QWidget *parent) : QGraphicsView(parent), timerId(0) { QGraphicsScene *scene = new QGraphicsScene(this); scene->setItemIndexMethod(QGraphicsScene::NoIndex); scene->setSceneRect(-200, -200, 400, 400); setScene(scene); setCacheMode(CacheBackground); setViewportUpdateMode(BoundingRectViewportUpdate); setRenderHint(QPainter::Antialiasing); setTransformationAnchor(AnchorUnderMouse); scale(qreal(0.8), qreal(0.8)); setMinimumSize(400, 400); setWindowTitle(tr("Elastic Nodes")); Конструктор GraphicsWidget'а создаёт сцену и, поскольку большинство элементов двигаются большую часть времени, устанавливает QGraphicsScene::NoIndex. Затем сцена получает фиксированный прямоугольник сцены, и присваивается представлению GraphWidget. Представление включает QGraphicsView::CacheBackground для кэширования визуализации своего статического, и довольного сложного, фона. Поскольку граф визуализирует замкнутую совокупность небольших элементов, которые всё время двигаются, у графического представления нет необходимости терять время на поиск точных областей обновления, поэтому устанавливаем режим обновления области просмотра QGraphicsView::BoundingRectViewportUpdate. Режим по умолчанию работает отлично, но этот режим недостаточно быстр в данном примере. Для улучшения качества визуализации устанавливаем QPainter::Antialiasing. Указатель преобразования (transformation anchor) решает, как представление будет прокручиваться когда вы преобразуете представление, или, в нашем случае, когда мы увеличиваем или уменьшаем масштаб. Мы выбрали QGraphicsView::AnchorUnderMouse, который центрирует представление в точке, находящейся под курсором мыши. Это облегчает масштабирование в области точки на сцене перемещая мышь над ней, и затем прокручивая колёсико мыши. В заключение задаём минимальный размер окна, который совпадает с размером сцены по умолчанию, и устанавливаем соответствующий заголовок окна. Node *node1 = new Node(this); Node *node2 = new Node(this); Node *node3 = new Node(this); Node *node4 = new Node(this); centerNode = new Node(this); Node *node6 = new Node(this); Node *node7 = new Node(this); Node *node8 = new Node(this); Node *node9 = new Node(this); scene->addItem(node1); scene->addItem(node2); scene->addItem(node3); scene->addItem(node4); scene->addItem(centerNode); scene->addItem(node6); scene->addItem(node7); scene->addItem(node8); scene->addItem(node9); scene->addItem(new Edge(node1, node2)); scene->addItem(new Edge(node2, node3)); scene->addItem(new Edge(node2, centerNode)); scene->addItem(new Edge(node3, node6)); scene->addItem(new Edge(node4, node1)); scene->addItem(new Edge(node4, centerNode)); scene->addItem(new Edge(centerNode, node6)); scene->addItem(new Edge(centerNode, node8)); scene->addItem(new Edge(node6, node9)); scene->addItem(new Edge(node7, node4)); scene->addItem(new Edge(node8, node7)); scene->addItem(new Edge(node9, node8)); node1->setPos(-50, -50); node2->setPos(0, -50); node3->setPos(50, -50); node4->setPos(-50, 0); centerNode->setPos(0, 0); node6->setPos(50, 0); node7->setPos(-50, 50); node8->setPos(0, 50); node9->setPos(50, 50); } Последняя часть конструктора создаёт сетку из узлов и рёбер, и назначает каждому узлу исходную позицию. void GraphWidget::itemMoved() { if (!timerId) timerId = startTimer(1000 / 25); } GraphWidget уведомляется о перемещении узла посредством данной функции itemMoved(). Её работа заключается просто в перезапуске главного таймера в случае, если он ещё не запущен. Таймер спроектирован для остановки когда граф стабилизируется, и запускается когда граф снова становится нестабильным. void GraphWidget::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Up: centerNode->moveBy(0, -20); break; case Qt::Key_Down: centerNode->moveBy(0, 20); break; case Qt::Key_Left: centerNode->moveBy(-20, 0); break; case Qt::Key_Right: centerNode->moveBy(20, 0); break; case Qt::Key_Plus: scaleView(qreal(1.2)); break; case Qt::Key_Minus: scaleView(1 / qreal(1.2)); break; case Qt::Key_Space: case Qt::Key_Enter: foreach (QGraphicsItem *item, scene()->items()) { if (qgraphicsitem_cast<Node *>(item)) item->setPos(-150 + qrand() % 300, -150 + qrand() % 300); } break; default: QGraphicsView::keyPressEvent(event); } } Это обработчик GraphWidget'а события клавиши. Клавиши стрелок перемещают центральный узел, клавиши '+' и '-' увеличивают и уменьшают масштаб, вызывая scaleView(), а клавиши enter и пробел располагаются узлы в случайных позициях. Все остальные события клавиш (например, page up и page down) обрабатываются стандартной реализацией QGraphicsView'а. void GraphWidget::timerEvent(QTimerEvent *event) { Q_UNUSED(event); QList<Node *> nodes; foreach (QGraphicsItem *item, scene()->items()) { if (Node *node = qgraphicsitem_cast<Node *>(item)) nodes << node; } foreach (Node *node, nodes) node->calculateForces(); bool itemsMoved = false; foreach (Node *node, nodes) { if (node->advance()) itemsMoved = true; } if (!itemsMoved) { killTimer(timerId); timerId = 0; } } Работа обработчика событий таймера заключается в запуске алгоритмов вычисления всех сил в виде плавной анимации. При каждом срабатывании таймера обработчик находит все узлы в сцене и вызывает Node::calculateForces() для каждого из них, по одному за раз. Затем, на заключительном этапе он вызывает Node::advance() для перемещения всех узлов в их новые позиции. Проверяя возвращаемое значение функции advance() можно решить стабилизировалась ли сетка (т.е., отсутствие перемещений узлов). Если это так, то останавливаем таймер. void GraphWidget::wheelEvent(QWheelEvent *event) { scaleView(pow((double)2, -event->delta() / 240.0)); } В обработчике событий колёсика мыши конвертируем дельту колёсика мыши в масштабный коэффициент и передаём его в scaleView(). Этот подход учитывает скорость прокрутки колёсика. Чем быстрее вы прокручиваете колёсико мыши, тем быстрее меняется масштаб представления. void GraphWidget::drawBackground(QPainter *painter, const QRectF &rect) { Q_UNUSED(rect); // Тень QRectF sceneRect = this->sceneRect(); QRectF rightShadow(sceneRect.right(), sceneRect.top() + 5, 5, sceneRect.height()); QRectF bottomShadow(sceneRect.left() + 5, sceneRect.bottom(), sceneRect.width(), 5); if (rightShadow.intersects(rect) || rightShadow.contains(rect)) painter->fillRect(rightShadow, Qt::darkGray); if (bottomShadow.intersects(rect) || bottomShadow.contains(rect)) painter->fillRect(bottomShadow, Qt::darkGray); // Заливка QLinearGradient gradient(sceneRect.topLeft(), sceneRect.bottomRight()); gradient.setColorAt(0, Qt::white); gradient.setColorAt(1, Qt::lightGray); painter->fillRect(rect.intersect(sceneRect), gradient); painter->setBrush(Qt::NoBrush); painter->drawRect(sceneRect); // Текст QRectF textRect(sceneRect.left() + 4, sceneRect.top() + 4, sceneRect.width() - 4, sceneRect.height() - 4); QString message(tr("Click and drag the nodes around, and zoom with the mouse " "wheel or the '+' and '-' keys")); QFont font = painter->font(); font.setBold(true); font.setPointSize(14); painter->setFont(font); painter->setPen(Qt::lightGray); painter->drawText(textRect.translated(2, 2), message); painter->setPen(Qt::black); painter->drawText(textRect, message); } Фон представления визуализируется в переопределённой функции QGraphicsView::drawBackground(). Мы рисуем большой прямоугольник, с линейной градиентной заливкой, и тенью, а затем визуализируем поверх него текст. Текст визуализируется дважды для создания эффект отбрасываемой тени. Визуализация фона достаточно дорогостояща; поэтому представление включает QGraphicsView::CacheBackground. void GraphWidget::scaleView(qreal scaleFactor) { qreal factor = transform().scale(scaleFactor, scaleFactor).mapRect(QRectF(0, 0, 1, 1)).width(); if (factor < 0.07 || factor > 100) return; scale(scaleFactor, scaleFactor); } Вспомогательная функция scaleView() проверяет нахождение масштабного коэффициента в заданных границах (т.е., вы не сможете изменить масштаб на слишком крупный или слишком мелкий), и затем применяет этот масштаб к представлению. Функция main()В отличие от сложности остальной части данного примера функция main() очень проста: Мы создаём экземпляр класса QApplication, запускаем генератор случайных чисел, используя qsrand(), а затем создаём и делаем видимым экземпляр класса GraphWidget. Поскольку все узлы в сетке изначально перемещаются, то после возвращения управления в цикл обработки событий немедленно запускается таймер GraphWidget. |
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |