Пример "Diagram Scene"Файлы:
Изображения:
Этот пример демонстрирует использование графического каркаса Qt. Пример "Diagram Scene" это приложение в котором вы можете создавать блок-схемы. В нём возможно добавлять блоки и текст и соединять блоки стрелками как показано на рисунке выше. Блоки, стрелки и текст могут иметь разный цвет и возможно изменять шрифт, стиль и подчеркивание текста. Каркас графического представления Qt спроектирован для управления и отображения пользовательских двухмерных графических объектов. Главными классами каркаса являются QGraphicsItem, QGraphicsScene и QGraphicsView. Графическая сцена управляет элементами и предоставляет поверхность для них. QGraphicsView это виджет, который используется для отображения сцены на экране. Для получения более подробной информации о каркасе смотрите Каркас графического представления. В этом примере мы покажем как создавать такие собственные графические сцены и элементы реализовывая классы, которые наследуют QGraphicsScene и QGraphicsItem. В частности, мы покажем как:
Пример состоит из следующих классов:
Определение класса MainWindowclass MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(); private slots: void backgroundButtonGroupClicked(QAbstractButton *button); void buttonGroupClicked(int id); void deleteItem(); void pointerGroupClicked(int id); void bringToFront(); void sendToBack(); void itemInserted(DiagramItem *item); void textInserted(QGraphicsTextItem *item); void currentFontChanged(const QFont &font); void fontSizeChanged(const QString &size); void sceneScaleChanged(const QString &scale); void textColorChanged(); void itemColorChanged(); void lineColorChanged(); void textButtonTriggered(); void fillButtonTriggered(); void lineButtonTriggered(); void handleFontChange(); void itemSelected(QGraphicsItem *item); void about(); private: void createToolBox(); void createActions(); void createMenus(); void createToolbars(); QWidget *createBackgroundCellWidget(const QString &text, const QString &image); QWidget *createCellWidget(const QString &text, DiagramItem::DiagramType type); QMenu *createColorMenu(const char *slot, QColor defaultColor); QIcon createColorToolButtonIcon(const QString &image, QColor color); QIcon createColorIcon(QColor color); DiagramScene *scene; QGraphicsView *view; QAction *exitAction; QAction *addAction; QAction *deleteAction; QAction *toFrontAction; QAction *sendBackAction; QAction *aboutAction; QMenu *fileMenu; QMenu *itemMenu; QMenu *aboutMenu; QToolBar *textToolBar; QToolBar *editToolBar; QToolBar *colorToolBar; QToolBar *pointerToolbar; QComboBox *sceneScaleCombo; QComboBox *itemColorCombo; QComboBox *textColorCombo; QComboBox *fontSizeCombo; QFontComboBox *fontCombo; QToolBox *toolBox; QButtonGroup *buttonGroup; QButtonGroup *pointerTypeGroup; QButtonGroup *backgroundButtonGroup; QToolButton *fontColorToolButton; QToolButton *fillColorToolButton; QToolButton *lineColorToolButton; QAction *boldAction; QAction *underlineAction; QAction *italicAction; QAction *textAction; QAction *fillAction; QAction *lineAction; }; Класс MainWindow создаёт и размещает виджеты в QMainWindow. Класс передаёт ввод от виджетов в DiagramScene. Он также обновляет свои виджеты когда изменяется текстовый элемент диаграммы или элемент диаграммы или текстовый элемент добавляется на сцену. Класс также удаляет элементы со сцены и управляет порядком наложения, что определяет порядок, в котором элементы отрисовываются когда они накладываются друг на друга. Реализация класса MainWindowМы начнем с осмотра конструктора: MainWindow::MainWindow() { createActions(); createToolBox(); createMenus(); scene = new DiagramScene(itemMenu, this); scene->setSceneRect(QRectF(0, 0, 5000, 5000)); connect(scene, SIGNAL(itemInserted(DiagramItem*)), this, SLOT(itemInserted(DiagramItem*))); connect(scene, SIGNAL(textInserted(QGraphicsTextItem*)), this, SLOT(textInserted(QGraphicsTextItem*))); connect(scene, SIGNAL(itemSelected(QGraphicsItem*)), this, SLOT(itemSelected(QGraphicsItem*))); createToolbars(); QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(toolBox); view = new QGraphicsView(scene); layout->addWidget(view); QWidget *widget = new QWidget; widget->setLayout(layout); setCentralWidget(widget); setWindowTitle(tr("Diagramscene")); setUnifiedTitleAndToolBarOnMac(true); } В конструкторе мы вызываем методы для создания виджетов и компоновок примера прежде чем мы создадим сцену диаграммы. Панель инструментов должна быть создана после сцены, так как она соединяется с её сигналами. Затем мы размещаем виджеты на окне. Мы соединяем слоты сцены диаграммы itemInserted() и textInserted() так как мы хотим отжать кнопки в панели инструментов когда добавляется элемент. Когда на сцене выделяется элемент, мы получаем сигнал itemSelected(). Мы используем его для обновления виджетов, которые отображают свойства шрифта, если выбранный элемент является DiagramTextItem. Функция createToolBox() создаёт и размещает виджеты toolBox QToolBox. Мы не рассматриваем её подробно, так как она не имеет отношения к функциональности графического каркаса. Вот её реализация: void MainWindow::createToolBox() { buttonGroup = new QButtonGroup(this); buttonGroup->setExclusive(false); connect(buttonGroup, SIGNAL(buttonClicked(int)), this, SLOT(buttonGroupClicked(int))); QGridLayout *layout = new QGridLayout; layout->addWidget(createCellWidget(tr("Conditional"), DiagramItem::Conditional), 0, 0); layout->addWidget(createCellWidget(tr("Process"), DiagramItem::Step),0, 1); layout->addWidget(createCellWidget(tr("Input/Output"), DiagramItem::Io), 1, 0); Эта часть функции настраивает виджет с вкладками, который содержит блоки графика. QButtonGroup всегда держит одну кнопку нажатой; мы хотим чтобы группа позволял отжать все кнопки. Мы всё равно используем группу кнопок, так как мы можем ассоциировать данные пользователя, которые мы используем для хранения типа диаграммы, с каждой кнопкой. Функция createCellWidget() настраивает кнопки в элементе виджета с вкладками и рассмотрена позже. Кнопки элемента виджета с вкладками фона настраиваются аналогично, так что мы перейдём к созданию панели инструментов: toolBox = new QToolBox; toolBox->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Ignored)); toolBox->setMinimumWidth(itemWidget->sizeHint().width()); toolBox->addItem(itemWidget, tr("Basic Flowchart Shapes")); toolBox->addItem(backgroundWidget, tr("Backgrounds")); } Мы устанавливаем желаемый размер панели инструментов равным максимальному. Таким образом графической сцене достанется больше пространства. Вот функция createActions(): void MainWindow::createActions() { toFrontAction = new QAction(QIcon(":/images/bringtofront.png"), tr("Bring to &Front"), this); toFrontAction->setShortcut(tr("Ctrl+F")); toFrontAction->setStatusTip(tr("Bring item to front")); connect(toFrontAction, SIGNAL(triggered()), this, SLOT(bringToFront())); Мы покажем пример создания действия. Функциональность триггера действия обсуждается в слотах, к которым мы подключаем действия. Вы можете посмотреть пример "Application", если вам необходимо введение о действиях. Это функция createMenus(): void MainWindow::createMenus() { fileMenu = menuBar()->addMenu(tr("&File")); fileMenu->addAction(exitAction); itemMenu = menuBar()->addMenu(tr("&Item")); itemMenu->addAction(deleteAction); itemMenu->addSeparator(); itemMenu->addAction(toFrontAction); itemMenu->addAction(sendBackAction); aboutMenu = menuBar()->addMenu(tr("&Help")); aboutMenu->addAction(aboutAction); } Мы создаём три меню в примере. Функция createToolbars() настраивает панели инструментов примера. Три QToolButton в colorToolBar, fontColorToolButton, fillColorToolButton и lineColorToolButton, являются интересными, так как мы создаём иконки для них путём рисования на QPixmap с помощью QPainter. Мы покажем как создаётся fillColorToolButton. Эта кнопка позволяет пользователю выбирать цвет для элемента диаграммы. void MainWindow::createToolbars() { ... fillColorToolButton = new QToolButton; fillColorToolButton->setPopupMode(QToolButton::MenuButtonPopup); fillColorToolButton->setMenu(createColorMenu(SLOT(itemColorChanged()), Qt::white)); fillAction = fillColorToolButton->menu()->defaultAction(); fillColorToolButton->setIcon(createColorToolButtonIcon( ":/images/floodfill.png", Qt::white)); connect(fillColorToolButton, SIGNAL(clicked()), this, SLOT(fillButtonTriggered())); Мы настраиваем меню кнопки с помощью setMenu(). Нам нужно чтобы объект fillAction QAction всегда указывал на выбранное действие меню. Меню создаётся с помощью функции createColorMenu() и, как мы увидим позднее, содержит по одному элементу меню для каждого цвета, который могут иметь элементы. Когда пользователь нажимает кнопку, которая выпускает сигнал clicked(), мы можем установить цвет выбранного элемента в цвет fillAction. С помощью createColorToolButtonIcon() мы создаём иконку для кнопки. ... } Вот функция createBackgroundCellWidget(): QWidget *MainWindow::createBackgroundCellWidget(const QString &text, const QString &image) { QToolButton *button = new QToolButton; button->setText(text); button->setIcon(QIcon(image)); button->setIconSize(QSize(50, 50)); button->setCheckable(true); backgroundButtonGroup->addButton(button); QGridLayout *layout = new QGridLayout; layout->addWidget(button, 0, 0, Qt::AlignHCenter); layout->addWidget(new QLabel(text), 1, 0, Qt::AlignCenter); QWidget *widget = new QWidget; widget->setLayout(layout); return widget; } Функция создаёт QWidget, содержащий кнопку и метку. Виджет, созданный с помощью этой функции, используется для элемента виджета фона с вкладками в панели инструментов. Вот функция createCellWidget(): QWidget *MainWindow::createCellWidget(const QString &text, DiagramItem::DiagramType type) { DiagramItem item(type, itemMenu); QIcon icon(item.image()); QToolButton *button = new QToolButton; button->setIcon(icon); button->setIconSize(QSize(50, 50)); button->setCheckable(true); buttonGroup->addButton(button, int(type)); QGridLayout *layout = new QGridLayout; layout->addWidget(button, 0, 0, Qt::AlignHCenter); layout->addWidget(new QLabel(text), 1, 0, Qt::AlignCenter); QWidget *widget = new QWidget; widget->setLayout(layout); return widget; } Эта функция возвращает QWidget, содержащий QToolButton с изображением одного из DiagramItems, т.е. блока диаграммы. Изображение создаётся DiagramItem с помощью функции image(). Класс QButtonGroup позволяет нам присоединить идентификатор id (int) к каждой кнопке; мы храним тип диаграммы, т.е., перечисление DiagramItem::DiagramType. Мы используем хранимый тип диаграммы при создании нового элемента для сцены. Виджет, созданный с использованием данной функции, используется в панели инструментов. Вот функция createColorMenu(): QMenu *MainWindow::createColorMenu(const char *slot, QColor defaultColor) { QList<QColor> colors; colors << Qt::black << Qt::white << Qt::red << Qt::blue << Qt::yellow; QStringList names; names << tr("black") << tr("white") << tr("red") << tr("blue") << tr("yellow"); QMenu *colorMenu = new QMenu(this); for (int i = 0; i < colors.count(); ++i) { QAction *action = new QAction(names.at(i), this); action->setData(colors.at(i)); action->setIcon(createColorIcon(colors.at(i))); connect(action, SIGNAL(triggered()), this, slot); colorMenu->addAction(action); if (colors.at(i) == defaultColor) { colorMenu->setDefaultAction(action); } } return colorMenu; } Эта функция создаёт меню цветов, которая используется как выпадающее меню для кнопки в colorToolBar. Мы создаём действие для каждого цвета, который мы добавляем в меню. Мы получаем данные действия когда устанавливаем цвет элементов, линий и текста. Вот функция createColorToolButtonIcon(): QIcon MainWindow::createColorToolButtonIcon(const QString &imageFile, QColor color) { QPixmap pixmap(50, 80); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); QPixmap image(imageFile); QRect target(0, 0, 50, 60); QRect source(0, 0, 42, 42); painter.fillRect(QRect(0, 60, 50, 80), color); painter.drawPixmap(target, image, source); return QIcon(pixmap); } Эта функция используется для создания QIcon для fillColorToolButton, fontColorToolButton и lineColorToolButton. Строка imageFile является или текстом, или заливкой, или символом линии, которые используются для кнопок. Под изображением мы рисуем прямоугольник, залитый цветом color. Вот функция createColorIcon(): QIcon MainWindow::createColorIcon(QColor color) { QPixmap pixmap(20, 20); QPainter painter(&pixmap); painter.setPen(Qt::NoPen); painter.fillRect(QRect(0, 0, 20, 20), color); return QIcon(pixmap); } Эта функция создаёт иконку с заполненным прямоугольником цвета color. Он используется для создания иконок для меню цветов в fillColorToolButton, fontColorToolButton и lineColorToolButton. Вот слот backgroundButtonGroupClicked(): void MainWindow::backgroundButtonGroupClicked(QAbstractButton *button) { QList<QAbstractButton *> buttons = backgroundButtonGroup->buttons(); foreach (QAbstractButton *myButton, buttons) { if (myButton != button) button->setChecked(false); } QString text = button->text(); if (text == tr("Blue Grid")) scene->setBackgroundBrush(QPixmap(":/images/background1.png")); else if (text == tr("White Grid")) scene->setBackgroundBrush(QPixmap(":/images/background2.png")); else if (text == tr("Gray Grid")) scene->setBackgroundBrush(QPixmap(":/images/background3.png")); else scene->setBackgroundBrush(QPixmap(":/images/background4.png")); scene->update(); view->update(); } В этой функции мы устанавливаем QBrush, которая используется для отрисовки фона сцены. Фоном может быть сетка квадратов голубого, серого или белого цветов, или же фон может отсутствовать вообще. У нас есть QPixmap квадратов из файлов png, с помощью которых мы создаём кисть. Когда нажимается одна из кнопок виджета фона с вкладками, мы выясняем что это за кнопка, проверяя её текст. Вот реализация buttonGroupClicked(): void MainWindow::buttonGroupClicked(int id) { QList<QAbstractButton *> buttons = buttonGroup->buttons(); foreach (QAbstractButton *button, buttons) { if (buttonGroup->button(id) != button) button->setChecked(false); } if (id == InsertTextButton) { scene->setMode(DiagramScene::InsertText); } else { scene->setItemType(DiagramItem::DiagramType(id)); scene->setMode(DiagramScene::InsertItem); } } Этот слот вызывается при нажатии кнопки в buttonGroup. Когда кнопка нажата, пользователь может кликнуть на вид и DiagramItem выбранного типа будет вставлен в DiagramScene. Мы должны пройти по всем кнопкам в группе и отжать из, так как только одна кнопка может быть нажата одновременно. QButtonGroup назначает уникальный номер каждой кнопка. Мы должны установить уникальный номер каждой кнопки равным типу диаграммы, заданному DiagramItem::DiagramType, которая будет вставлена при нажатии данной кнопки. Теперь мы можем использовать уникальный номер кнопки когда мы устанавливаем тип диаграммы с помощью функции setItemType(). В случае текста мы назначаем уникальный номер, который не входит в перечисление DiagramType. Вот реализация deleteItem(): void MainWindow::deleteItem() { foreach (QGraphicsItem *item, scene->selectedItems()) { if (item->type() == Arrow::Type) { scene->removeItem(item); Arrow *arrow = qgraphicsitem_cast<Arrow *>(item); arrow->startItem()->removeArrow(arrow); arrow->endItem()->removeArrow(arrow); delete item; } } foreach (QGraphicsItem *item, scene->selectedItems()) { if (item->type() == DiagramItem::Type) { qgraphicsitem_cast<DiagramItem *>(item)->removeArrows(); } scene->removeItem(item); delete item; } } Этот слот удаляет выбранный элемент со сцены. Он удаляет сначала стрелки для того, чтобы избежать их повторного удаления. Если удаляемый элемент является DiagramItem, то нам также надо удалить стрелки, которые с ним соединены; мы не хотим чтобы на сцене были стрелки, которые не соединены с элементами на обоих концах. Это реализация pointerGroupClicked(): void MainWindow::pointerGroupClicked(int) { scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId())); } Функция pointerTypeGroup решает в каком режиме находится сцена: ItemMove или InsertLine. Эта группа кнопок является исключительной, т.е. только одна кнопка может быть выбрана одновременно. Так же как и в buttonGroup выше, мы назначаем уникальный номер кнопкам, который совпадает со значением перечисления DiagramScene::Mode чтобы мы могли использовать уникальный номер для установки корректного режима. Вот слот bringToFront(): void MainWindow::bringToFront() { if (scene->selectedItems().isEmpty()) return; QGraphicsItem *selectedItem = scene->selectedItems().first(); QList<QGraphicsItem *> overlapItems = selectedItem->collidingItems(); qreal zValue = 0; foreach (QGraphicsItem *item, overlapItems) { if (item->zValue() >= zValue && item->type() == DiagramItem::Type) zValue = item->zValue() + 0.1; } selectedItem->setZValue(zValue); } Некоторые элементы могут сталкиваться, т.е. перекрываться друг другом на сцене. Этот слот вызывается когда пользователь требуется чтобы элемент был расположен выше всех элементов с которыми он сталкивается. QGrapicsItems имеют значение высоты, которое определяет порядок, в котором элементы размещаются на сцене; вы можете думать о нём как об оси Z в трёхмерной координатной системе. Когда элементы сталкиваются, элементы с большим значением высоты будут нарисованы выше элементов с более низким значением. Когда мы перемещаем элемент вперед, мы можем пройти по всем элементам с которыми он сталкивается, и установить значение высоты больше чем у них всех. Вот слот sendToBack(): void MainWindow::sendToBack() { if (scene->selectedItems().isEmpty()) return; QGraphicsItem *selectedItem = scene->selectedItems().first(); QList<QGraphicsItem *> overlapItems = selectedItem->collidingItems(); qreal zValue = 0; foreach (QGraphicsItem *item, overlapItems) { if (item->zValue() <= zValue && item->type() == DiagramItem::Type) zValue = item->zValue() - 0.1; } selectedItem->setZValue(zValue); } Этот слот работает таким же образом как описанный выше bringToFront(), но устанавливает значение высоты меньше чем у элементов, с которыми он сталкивается. Это реализация itemInserted(): void MainWindow::itemInserted(DiagramItem *item) { pointerTypeGroup->button(int(DiagramScene::MoveItem))->setChecked(true); scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId())); buttonGroup->button(int(item->diagramType()))->setChecked(false); } Этот слот вызывается из DiagramScene когда элемент добавляется на сцену. Мы устанавливаем режим сцены обратно на режим который был до того, как элемент был добавлен: ItemMove или InsertText в зависимости от того какая кнопка выбрана в pointerTypeGroup. Мы должны также отжать кнопки в buttonGroup. Вот реализация textInserted(): void MainWindow::textInserted(QGraphicsTextItem *) { buttonGroup->button(InsertTextButton)->setChecked(false); scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId())); } Мы просто устанавливаем режим сцены обратно на тот режим, который был прежде чем текст был вставлен. Вот слот currentFontChanged(): void MainWindow::currentFontChanged(const QFont &) { handleFontChange(); } Когда пользователь запрашивает смену шрифта используя один из виджетов в fontToolBar, мы создаём новый объект QFont и устанавливает его свойства в соответствии с состоянием виджетов. Это делается в handleFontChange(), так что нам надо просто вызвать этот слот. Вот слот fontSizeChanged(): void MainWindow::fontSizeChanged(const QString &) { handleFontChange(); } Когда пользователь запрашивает смену шрифта используя один из виджетов в fontToolBar, мы создаём новый объект QFont и устанавливает его свойства в соответствии с состоянием виджетов. Это делается в handleFontChange(), так что нам надо просто вызвать этот слот. Вот реализация sceneScaleChanged(): void MainWindow::sceneScaleChanged(const QString &scale) { double newScale = scale.left(scale.indexOf(tr("%"))).toDouble() / 100.0; QMatrix oldMatrix = view->matrix(); view->resetMatrix(); view->translate(oldMatrix.dx(), oldMatrix.dy()); view->scale(newScale, newScale); } Пользователь может увеличить или уменьшить масштаб, с которым будет отрисована сцена, в sceneScaleCombo. Сама сцена не меняет свой масштаб, только ее вид. Вот слот textColorChanged(): void MainWindow::textColorChanged() { textAction = qobject_cast<QAction *>(sender()); fontColorToolButton->setIcon(createColorToolButtonIcon( ":/images/textpointer.png", qVariantValue<QColor>(textAction->data()))); textButtonTriggered(); } Этот слот вызывается когда нажимается элемент в выпадающем меню fontColorToolButton. Мы должны изменять иконку кнопки на цвет выбранного QAction. Мы храним указатель на выбранное действие в textAction. В textButtonTriggered() мы меняем цвет текста на цвет textAction, так что мы вызываем этот слот. Вот реализация itemColorChanged(): void MainWindow::itemColorChanged() { fillAction = qobject_cast<QAction *>(sender()); fillColorToolButton->setIcon(createColorToolButtonIcon( ":/images/floodfill.png", qVariantValue<QColor>(fillAction->data()))); fillButtonTriggered(); } Этот слот управляет запросами на смену цвета DiagramItems таким же образом как textColorChanged() делает для DiagramTextItems. Вот реализация lineColorChanged(): void MainWindow::lineColorChanged() { lineAction = qobject_cast<QAction *>(sender()); lineColorToolButton->setIcon(createColorToolButtonIcon( ":/images/linecolor.png", qVariantValue<QColor>(lineAction->data()))); lineButtonTriggered(); } Этот слот управляет запросами на смену цвета для Arrows таким же образом как textColorChanged() делает для DiagramTextItems. Вот слот textButtonTriggered(): void MainWindow::textButtonTriggered() { scene->setTextColor(qVariantValue<QColor>(textAction->data())); } textAction указывает на QAction текущего выбранного элемента меню в выпадающем меню цветов fontColorToolButton. Мы должны установить данные действия в QColor, который представляет действие, так что мы просто получаем его когда мы устанавливаем цвет текста в setTextColor(). Вот слот fillButtonTriggered(): void MainWindow::fillButtonTriggered() { scene->setItemColor(qVariantValue<QColor>(fillAction->data())); } fillAction указывает на выбранный элемент меню выпадающего меню fillColorToolButton(). Таким образом, мы можем использовать данные этого действия когда устанавливаем цвет элемента в setItemColor(). Вот слот lineButtonTriggered(): void MainWindow::lineButtonTriggered() { scene->setLineColor(qVariantValue<QColor>(lineAction->data())); } lineAction указывает на выбранный элемент выпадающего меню lineColorToolButton. Мы используем его данные когда устанавливаем цвет стрелки в setLineColor(). Вот функция handleFontChange(): void MainWindow::handleFontChange() { QFont font = fontCombo->currentFont(); font.setPointSize(fontSizeCombo->currentText().toInt()); font.setWeight(boldAction->isChecked() ? QFont::Bold : QFont::Normal); font.setItalic(italicAction->isChecked()); font.setUnderline(underlineAction->isChecked()); scene->setFont(font); } handleFontChange() вызывается когда изменяется любой виджет, показывающий свойства шрифта. Мы создаём новый объект QFont и устанавливаем его свойства основываясь на виджетах. Затем мы вызываем функцию setFont() DiagramScene; это сцена, устанавливающая шрифт DiagramTextItems, которыми она управляет. Вот слот itemSelected(): void MainWindow::itemSelected(QGraphicsItem *item) { DiagramTextItem *textItem = qgraphicsitem_cast<DiagramTextItem *>(item); QFont font = textItem->font(); QColor color = textItem->defaultTextColor(); fontCombo->setCurrentFont(font); fontSizeCombo->setEditText(QString().setNum(font.pointSize())); boldAction->setChecked(font.weight() == QFont::Bold); italicAction->setChecked(font.italic()); underlineAction->setChecked(font.underline()); } Этот слот вызывается когда выбирается элемент в DiagramScene. В данном примере данный сигнал вырабатывается только текстовыми элементами, поэтому нам не надо проверять каким типом элемента он является. Устанавливаем состояние виджетов в соответствии со свойствами шрифта выбранного текстового элемента. Это слот about(): void MainWindow::about() { QMessageBox::about(this, tr("About Diagram Scene"), tr("The <b>Diagram Scene</b> example shows " "use of the graphics framework.")); } Этот слот отображает диалогового "О программе" для примера, когда пользователь выбирает элемент about меню help. Определение класса DiagramSceneКласс DiagramScene наследует QGraphicsScene и добавляет функциональность для управления DiagramItems, Arrows и DiagramTextItems в дополнение к элементам, управляемым его базовым классом. class DiagramScene : public QGraphicsScene { Q_OBJECT public: enum Mode { InsertItem, InsertLine, InsertText, MoveItem }; DiagramScene(QMenu *itemMenu, QObject *parent = 0); QFont font() const { return myFont; } QColor textColor() const { return myTextColor; } QColor itemColor() const { return myItemColor; } QColor lineColor() const { return myLineColor; } void setLineColor(const QColor &color); void setTextColor(const QColor &color); void setItemColor(const QColor &color); void setFont(const QFont &font); public slots: void setMode(Mode mode); void setItemType(DiagramItem::DiagramType type); void editorLostFocus(DiagramTextItem *item); signals: void itemInserted(DiagramItem *item); void textInserted(QGraphicsTextItem *item); void itemSelected(QGraphicsItem *item); protected: void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent); void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent); void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent); private: bool isItemChange(int type); DiagramItem::DiagramType myItemType; QMenu *myItemMenu; Mode myMode; bool leftButtonDown; QPointF startPoint; QGraphicsLineItem *line; QFont myFont; DiagramTextItem *textItem; QColor myTextColor; QColor myItemColor; QColor myLineColor; }; В DiagramScene клик мыши может произвести три разных действия: элемент под курсором может быть перемещён, элемент может быть вставлен или стрелка может соединить элементы диаграммы. Каждое действие мышки зависит от режима в котором находится сцена, заданного перечислением Mode. Режим устанавливается функцией setMode(). Сцена также устанавливает цвет её элементов и шрифт её текстовых элементов. Цвет и шрифт, используемый сценой, могут быть установлены с помощью функций setLineColor(), setTextColor(), setItemColor() и setFont(). Тип создаваемого при вставке элемента DiagramItem, заданный перечислением DiagramItem::DiagramType, устанавливается с помощью слота setItemType(). MainWindow и DiagramScene делят ответственность за функциональность данного примера. MainWindow обрабатывает следующие задачи: удаление элементов, текста и стрелок; перемещение элементов диаграммы на зад и вперед; и установка масштаба сцены. Реализация класса DiagramSceneМы начинаем с конструктора: DiagramScene::DiagramScene(QMenu *itemMenu, QObject *parent) : QGraphicsScene(parent) { myItemMenu = itemMenu; myMode = MoveItem; myItemType = DiagramItem::Step; line = 0; textItem = 0; myItemColor = Qt::white; myTextColor = Qt::black; myLineColor = Qt::black; } Сцена использует myItemMenu для установки контекстного меню, когда она создаёт DiagramItems. Мы устанавливаем режим по умолчанию DiagramScene::MoveItem так как он предоставляет поведение QGraphicsScene по умолчанию. Вот функция setLineColor(): void DiagramScene::setLineColor(const QColor &color) { myLineColor = color; if (isItemChange(Arrow::Type)) { Arrow *item = qgraphicsitem_cast<Arrow *>(selectedItems().first()); item->setColor(myLineColor); update(); } } Функция isItemChange возвращает true если в сцене выбран элемент Arrow в случае когда мы хотим изменить её цвет. Когда DiagramScene создаёт и добавляет новые стрелки на сцене, он так же будет использовать новый цвет color. Вот функция setTextColor(): void DiagramScene::setTextColor(const QColor &color) { myTextColor = color; if (isItemChange(DiagramTextItem::Type)) { DiagramTextItem *item = qgraphicsitem_cast<DiagramTextItem *>(selectedItems().first()); item->setDefaultTextColor(myTextColor); } } Эта функция устанавливает цвет DiagramTextItems таким же образом как setLineColor() устанавливает цвет Arrows. Вот функция setItemColor(): void DiagramScene::setItemColor(const QColor &color) { myItemColor = color; if (isItemChange(DiagramItem::Type)) { DiagramItem *item = qgraphicsitem_cast<DiagramItem *>(selectedItems().first()); item->setBrush(myItemColor); } } Эта функция устанавливает цвет, который сцена будет использовать при создании DiagramItems. Она так же изменяет цвет выбранного DiagramItem. Это реализация setFont(): void DiagramScene::setFont(const QFont &font) { myFont = font; if (isItemChange(DiagramTextItem::Type)) { QGraphicsTextItem *item = qgraphicsitem_cast<DiagramTextItem *>(selectedItems().first()); //В этом месте выделение может измениться, поэтому первый выделенный элемент может быть не DiagramTextItem if (item) item->setFont(myFont); } } Устанавливает шрифт для новых и выбранной, если текстовый элемент выбран, DiagramTextItems. Это реализация слота editorLostFocus(): void DiagramScene::editorLostFocus(DiagramTextItem *item) { QTextCursor cursor = item->textCursor(); cursor.clearSelection(); item->setTextCursor(cursor); if (item->toPlainText().isEmpty()) { removeItem(item); item->deleteLater(); } } DiagramTextItems вырабатывают сигнал когда они теряют фокус, который соединён с этим слотом. Мы удаляем элемент, если у него нет текста. В противном случае у нас была бы утечка памяти и пользователь был бы запутался, так как элементы бы редактировались при клике мышкой. Функция mousePressEvent() управляет событиями нажатия кнопок мыши в зависимости от того, в каком режиме находится DiagramScene. Мы рассмотрим его реализацию для каждого случая: void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) { if (mouseEvent->button() != Qt::LeftButton) return; DiagramItem *item; switch (myMode) { case InsertItem: item = new DiagramItem(myItemType, myItemMenu); item->setBrush(myItemColor); addItem(item); item->setPos(mouseEvent->scenePos()); emit itemInserted(item); break; Мы просто создаём новый DiagramItem и добавляемего на сцену в точку, где был клик мышки. Заметьте, что центр его локальной координатной системы будет под указателем мышки. case InsertLine: line = new QGraphicsLineItem(QLineF(mouseEvent->scenePos(), mouseEvent->scenePos())); line->setPen(QPen(myLineColor, 2)); addItem(line); break; Пользователь добавляет Arrows на сцене рисуя линию между элементами, которые должна эта линия соединить. Начало линии фиксируется в точке где пользователь кликает мышкой, а конец следует за указателем мыши до тех пор пока кнопка нажата. Когда пользователь отпускает кнопку мыши, Arrow будет добавлена на сцену в случае, когда есть DiagramItem под начальной и конечной точкой линии. Мы посмотрим как это реализовано позже; здесь мы просто добавляем линию. case InsertText: textItem = new DiagramTextItem(); textItem->setFont(myFont); textItem->setTextInteractionFlags(Qt::TextEditorInteraction); textItem->setZValue(1000.0); connect(textItem, SIGNAL(lostFocus(DiagramTextItem*)), this, SLOT(editorLostFocus(DiagramTextItem*))); connect(textItem, SIGNAL(selectedChange(QGraphicsItem*)), this, SIGNAL(itemSelected(QGraphicsItem*))); addItem(textItem); textItem->setDefaultTextColor(myTextColor); textItem->setPos(mouseEvent->scenePos()); emit textInserted(textItem); DiagramTextItem редактируемая когда установлен флаг Qt::TextEditorInteraction, в противном случае он перемещается мышкой. Мы всегда хотим чтобы текст был отрисован выше чем любой другой элемент на сцене, поэтому мы устанавливаем значение высоты больше чем у всех остальных элементов. default: ; } QGraphicsScene::mousePressEvent(mouseEvent); } Мы в режиме MoveItem mode если мы попадаем в ветку по умолчанию; в таком случае мы можем вызвать реализацию QGraphicsScene, которая обработает перемещение элементов мышкой. Мы делаем этот вызов даже если мы в другом режиме чтобы сделать возможным добавить элемент и затем, продолжая держать кнопку мыши, начать перемещать элемент. В случае текстового элемента это невозможно, так как они не передают события мыши когда они редактируемы. Это функция mouseMoveEvent(): void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) { if (myMode == InsertLine && line != 0) { QLineF newLine(line->line().p1(), mouseEvent->scenePos()); line->setLine(newLine); } else if (myMode == MoveItem) { QGraphicsScene::mouseMoveEvent(mouseEvent); } } Мы должны отрисовывать линию если мы в режиме InsertMode и кнопка мыши нажата (линия не равно 0). Как описано в mousePressEvent(), линия рисуется от позиция где мышь была нажата до текущей позиции мыши. Если мы в режиме MoveItem, мы вызываем реализацию QGraphicsScene, которая обрабатывает перемещение элементов. В функции mouseReleaseEvent() нам надо проверить нужно ли добавлена на сцену стрелку: void DiagramScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) { if (line != 0 && myMode == InsertLine) { QList<QGraphicsItem *> startItems = items(line->line().p1()); if (startItems.count() && startItems.first() == line) startItems.removeFirst(); QList<QGraphicsItem *> endItems = items(line->line().p2()); if (endItems.count() && endItems.first() == line) endItems.removeFirst(); removeItem(line); delete line; Сначала нам надо получить элементы (если такие есть) под начальной и конечной точками. Сама линия это первый элемент на этих координатах, поэтому мы удаляем ее из списка. В качестве предосторожности мы проверяем не пустой ли список, но это не должно никогда произойти. if (startItems.count() > 0 && endItems.count() > 0 && startItems.first()->type() == DiagramItem::Type && endItems.first()->type() == DiagramItem::Type && startItems.first() != endItems.first()) { DiagramItem *startItem = qgraphicsitem_cast<DiagramItem *>(startItems.first()); DiagramItem *endItem = qgraphicsitem_cast<DiagramItem *>(endItems.first()); Arrow *arrow = new Arrow(startItem, endItem); arrow->setColor(myLineColor); startItem->addArrow(arrow); endItem->addArrow(arrow); arrow->setZValue(-1000.0); addItem(arrow); arrow->updatePosition(); } } Теперь мы проверяем являются ли двумя разными элементы DiagramItems под начальными и конечными точками линии. Если они разные, мы можем создать Arrow между двумя элементами. Стрелка добавляется к каждому элементу, а затем, на сцену. Стрелка должна быть обновлена для корректировки ее начальной и конечной точек на элементах. Мы устанавливаем высоту стрелки равной -1000.0 потому что мы хотим чтобы она была нарисована под элементами. line = 0; QGraphicsScene::mouseReleaseEvent(mouseEvent); } Вот функция isItemChange(): bool DiagramScene::isItemChange(int type) { foreach (QGraphicsItem *item, selectedItems()) { if (item->type() == type) return true; } return false; } У сцены одиночное выделение, т.е. только один элемент может быть выделен одновременно. В таком случае цикл проходит один раз с выбранным элементом или ни одного, если элементов не выбрано. isItemChange() используется для проверки существует ли выбранных элемент и является ли он так же указанным типом диаграммы type. Определение класса DiagramItemclass DiagramItem : public QGraphicsPolygonItem { public: enum { Type = UserType + 15 }; enum DiagramType { Step, Conditional, StartEnd, Io }; DiagramItem(DiagramType diagramType, QMenu *contextMenu, QGraphicsItem *parent = 0, QGraphicsScene *scene = 0); void removeArrow(Arrow *arrow); void removeArrows(); DiagramType diagramType() const { return myDiagramType; } QPolygonF polygon() const { return myPolygon; } void addArrow(Arrow *arrow); QPixmap image() const; int type() const { return Type;} protected: void contextMenuEvent(QGraphicsSceneContextMenuEvent *event); QVariant itemChange(GraphicsItemChange change, const QVariant &value); private: DiagramType myDiagramType; QPolygonF myPolygon; QMenu *myContextMenu; QList<Arrow *> arrows; }; DiagramItem представляет фигуру схемы в DiagramScene. Он наследует QGraphicsPolygonItem и имеет полигон для каждой фигуры. Перечисление enum DiagramType имеет значение для каждой фигуры. Класс имеет список стрелок которые с ним соединены. Это необходимо, так как только элемент знает когда он удаляется (с функцией itemChanged()), во время которого стрелки должны быть обновлены. Элемент может отрисовывать себя на QPixmap с функцией image(). Это используется для кнопки в MainWindow, смотрите createColorToolButtonIcon() в MainWindow. Перечисление Type является уникальным идентификатором класса. Он используется qgraphicsitem_cast(), который делает динамическое приведение графических элементов. Константа UserType является минимальным значением, которым может быть пользовательский тип графического элемента. Реализация класса DiagramItemМы начнем с осмотра конструктора: DiagramItem::DiagramItem(DiagramType diagramType, QMenu *contextMenu, QGraphicsItem *parent, QGraphicsScene *scene) : QGraphicsPolygonItem(parent, scene) { myDiagramType = diagramType; myContextMenu = contextMenu; QPainterPath path; switch (myDiagramType) { case StartEnd: path.moveTo(200, 50); path.arcTo(150, 0, 50, 50, 0, 90); path.arcTo(50, 0, 50, 50, 90, 90); path.arcTo(50, 50, 50, 50, 180, 90); path.arcTo(150, 50, 50, 50, 270, 90); path.lineTo(200, 25); myPolygon = path.toFillPolygon(); break; case Conditional: myPolygon << QPointF(-100, 0) << QPointF(0, 100) << QPointF(100, 0) << QPointF(0, -100) << QPointF(-100, 0); break; case Step: myPolygon << QPointF(-100, -100) << QPointF(100, -100) << QPointF(100, 100) << QPointF(-100, 100) << QPointF(-100, -100); break; default: myPolygon << QPointF(-120, -80) << QPointF(-70, 80) << QPointF(120, 80) << QPointF(70, -80) << QPointF(-120, -80); break; } setPolygon(myPolygon); setFlag(QGraphicsItem::ItemIsMovable, true); setFlag(QGraphicsItem::ItemIsSelectable, true); setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); } В конструкторе мы создаём полигон элемента в соответствии с типом диаграммы diagramType. QGraphicsItem не являются выделяемыми или перемещаемыми по умолчанию, поэтому мы должны установить эти значения. Вот функция removeArrow(): void DiagramItem::removeArrow(Arrow *arrow) { int index = arrows.indexOf(arrow); if (index != -1) arrows.removeAt(index); } removeArrow() используется для удаления элементов Arrow когда они или DiagramItems, к которым они подключены, удаляются со сцены. Вот функция removeArrows(): void DiagramItem::removeArrows() { foreach (Arrow *arrow, arrows) { arrow->startItem()->removeArrow(arrow); arrow->endItem()->removeArrow(arrow); scene()->removeItem(arrow); delete arrow; } } Эта функция вызывается когда элемент удаляется со сцены и удаляет все стрелки, которые с ним соединены. Стрелка должна быть удалена из списка arrows начального и конечного элемента. Вот функция addArrow(): void DiagramItem::addArrow(Arrow *arrow) { arrows.append(arrow); } Эта функция просто добавляет стрелку arrow в список arrows элементов. Вот функция image(): QPixmap DiagramItem::image() const { QPixmap pixmap(250, 250); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); painter.setPen(QPen(Qt::black, 8)); painter.translate(125, 125); painter.drawPolyline(myPolygon); return pixmap; } Эта функция рисует полигон элемента на QPixmap. В данном примере мы используем это для создания иконок кнопок панели инструментов. Вот функция contextMenuEvent(): void DiagramItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { scene()->clearSelection(); setSelected(true); myContextMenu->exec(event->screenPos()); } Мы показываем контекстное меню. Так как правая кнопка мыши, которая показывает меню, не выделяет элемента по умолчанию, мы устанавливаем выделенный элемент с помощью функции setSelected(). Это обязательно так как элемент должен быть выделен для изменения его положения с помощью действий bringToFront и sendToBack. Это реализация itemChange(): QVariant DiagramItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == QGraphicsItem::ItemPositionChange) { foreach (Arrow *arrow, arrows) { arrow->updatePosition(); } } return value; } Если элемент переместился, нам надо обновить позицию стрелок, которые с ним соединены. Реализация QGraphicsItem ничего не делает, поэтому мы можем просто вернуть значение value. Определение класса DiagramTextItemКласс TextDiagramItem наследует QGraphicsTextItem и добавляет возможность перемещать редактируемые текстовые элементы. Редактируемые QGraphicsTextItems спроектированы так, чтобы они фиксировались на месте и их редактирование начиналось при одиночном клике пользователя на элементе. С DiagramTextItem редактирование начинается после двойного клика, оставляя одиночный клик для взаимодействия с ней и её перемещения. class DiagramTextItem : public QGraphicsTextItem { Q_OBJECT public: enum { Type = UserType + 3 }; DiagramTextItem(QGraphicsItem *parent = 0, QGraphicsScene *scene = 0); int type() const { return Type; } signals: void lostFocus(DiagramTextItem *item); void selectedChange(QGraphicsItem *item); protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value); void focusOutEvent(QFocusEvent *event); void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event); }; Мы используем itemChange() и focusOutEvent() для сообщения DiagramScene когда текстовый элемент теряет фокус и становится выделенным. Мы переопределили управляющие событиями мыши функции чтобы изменить поведение мыши элемента QGraphicsTextItem. Реализация DiagramTextItemМы начинаем с конструктора: DiagramTextItem::DiagramTextItem(QGraphicsItem *parent, QGraphicsScene *scene) : QGraphicsTextItem(parent, scene) { setFlag(QGraphicsItem::ItemIsMovable); setFlag(QGraphicsItem::ItemIsSelectable); } Мы просто делаем элемент перемещаемым и выделяемым, так как эти флаги отключены по умолчанию. Вот функция itemChange(): QVariant DiagramTextItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == QGraphicsItem::ItemSelectedHasChanged) emit selectedChange(this); return value; } Когда элемент выделен, мы вырабатываем сигнал selectedChanged. MainWindow использует этот сигнал для обновления виджетов которые отображают свойства шрифта, используя параметры шрифта выделенного текстового элемента. Вот функция focusOutEvent(): void DiagramTextItem::focusOutEvent(QFocusEvent *event) { setTextInteractionFlags(Qt::NoTextInteraction); emit lostFocus(this); QGraphicsTextItem::focusOutEvent(event); } DiagramScene использует сигнал, вырабатываемый когда элемент теряет фокус, чтобы удалить текстовый элемент если он пустой, т.е. он не содержит текст. Это реализация mouseDoubleClickEvent(): void DiagramTextItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { if (textInteractionFlags() == Qt::NoTextInteraction) setTextInteractionFlags(Qt::TextEditorInteraction); QGraphicsTextItem::mouseDoubleClickEvent(event); } Когда мы получаем событие двойного клика, мы делаем элемент редактируемым вызывая QGraphicsTextItem::setTextInteractionFlags(). Затем мы передаём двойной клик самому элементу. Определение класса ArrowКласс Arrow это графический элемент, соединяющий два DiagramItems. Он рисует стрелку, направленную к одному из элементов. Чтобы добиться этого, этот элемент должен отрисовывать себя сам и также переопределить методы, используемые графической сценой для проверки столкновений и выделений. Класс наследует элемент QGraphicsLine и рисует стрелку и перемещается с элементами, которые он соединяет. class Arrow : public QGraphicsLineItem { public: enum { Type = UserType + 4 }; Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent = 0, QGraphicsScene *scene = 0); int type() const { return Type; } QRectF boundingRect() const; QPainterPath shape() const; void setColor(const QColor &color) { myColor = color; } DiagramItem *startItem() const { return myStartItem; } DiagramItem *endItem() const { return myEndItem; } void updatePosition(); protected: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); private: DiagramItem *myStartItem; DiagramItem *myEndItem; QColor myColor; QPolygonF arrowHead; }; Цвет элемента может быть установлен с помощью setColor(). boundingRect() и shape() переопределены от QGraphicsLineItem и используются сценой для столкновений и выделений. Вызов updatePosition() приводит к обновлению стрелкой её позиции и угла наклона стрелки. paint() переопределён так, что мы можем рисовать стрелку, а не только линию между элементами. myStartItem и myEndItem это элементы диаграммы, которые соединяет стрелка. Наконечник стрелки отрисовывается на конце элемента. arrowHead является полигоном с тремя вершинами, который мы используем для отрисовки наконечника стрелки. Реализация класса ArrowКонструктор класса Arrow выглядит следующим образом: Arrow::Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent, QGraphicsScene *scene) : QGraphicsLineItem(parent, scene) { myStartItem = startItem; myEndItem = endItem; setFlag(QGraphicsItem::ItemIsSelectable, true); myColor = Qt::black; setPen(QPen(myColor, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); } Мы устанавливаем начальный и конечный элемент диаграммы стрелки. Наконечник стрелки будет отрисован там, где линия пересекается с конечным элементом. Вот функция boundingRect(): QRectF Arrow::boundingRect() const { qreal extra = (pen().width() + 20) / 2.0; return QRectF(line().p1(), QSizeF(line().p2().x() - line().p1().x(), line().p2().y() - line().p1().y())) .normalized() .adjusted(-extra, -extra, extra, extra); } Нам надо переопределить эту функцию потому что стрелка больше ограничивающего прямоугольника QGraphicsLineItem. Графическая сцена использует ограничивающий прямоугольник для определения какие области сцены надо перерисовать. Вот функция shape(): QPainterPath Arrow::shape() const { QPainterPath path = QGraphicsLineItem::shape(); path.addPolygon(arrowHead); return path; } Эта функция возвращает QPainterPath который является точной фигурой элемента. QGraphicsLineItem::shape() возвращает путь с линией, нарисованной текущим пером, поэтому мы только добавляем наконечник стрелки. Эта функция используется для проверки на столкновения и выделения с помощью мышки. Вот слот updatePosition(): void Arrow::updatePosition() { QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0)); setLine(line); } Этот слот обновляет стрелку, устанавливая начальную и конечную точки её линии на центр элементов, которые она соединяет. Вот функция paint(): void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (myStartItem->collidesWithItem(myEndItem)) return; QPen myPen = pen(); myPen.setColor(myColor); qreal arrowSize = 20; painter->setPen(myPen); painter->setBrush(myColor); Если начальный и конечный элемент сталкиваются, мы не рисуем стрелку; алгоритм, который мы используем для нахождения точек стрелки, может не сработать если элементы сталкиваются. Сначала мы устанавливаем перо и кисть, которые мы будем использовать для отрисовки стрелки. QLineF centerLine(myStartItem->pos(), myEndItem->pos()); QPolygonF endPolygon = myEndItem->polygon(); QPointF p1 = endPolygon.first() + myEndItem->pos(); QPointF p2; QPointF intersectPoint; QLineF polyLine; for (int i = 1; i < endPolygon.count(); ++i) { p2 = endPolygon.at(i) + myEndItem->pos(); polyLine = QLineF(p1, p2); QLineF::IntersectType intersectType = polyLine.intersect(centerLine, &intersectPoint); if (intersectType == QLineF::BoundedIntersection) break; p1 = p2; } setLine(QLineF(intersectPoint, myStartItem->pos())); Затем нам надо найти позицию, где мы отрисуем наконечник стрелки. Наконечник должен быть отрисован где линия и конечный элемент пересекаются. Это делается путём взятия линии между каждыми точками полигона и проверкой не пересекается ли она с линией стрелки. Так как начальная и конечная точки установлены в центр элементов, линия должна пересекать одну и только одну линию полигона. Заметьте, что точки полигона относительны к локальной координатной системе элемента. Поэтому нам надо добавить позицию конечного элемента чтобы получить координаты в отношении сцены. double angle = ::acos(line().dx() / line().length()); if (line().dy() >= 0) angle = (Pi * 2) - angle; QPointF arrowP1 = line().p1() + QPointF(sin(angle + Pi / 3) * arrowSize, cos(angle + Pi / 3) * arrowSize); QPointF arrowP2 = line().p1() + QPointF(sin(angle + Pi - Pi / 3) * arrowSize, cos(angle + Pi - Pi / 3) * arrowSize); arrowHead.clear(); arrowHead << line().p1() << arrowP1 << arrowP2; Мы вычисляем угол между осью Х и линией стрелки. Нам необходимо получить наконечник стрелки на этот угол чтобы он следовал направлению стрелки. Если угол отрицательный, мы должны повернуть направление стрелки. Затем мы можем вычислить три точки полигона наконечника стрелки. Одна из этих точек это конец линии, которая теперь равна точке пересечения между стрелкой и конечным полигоном. Затем мы очищаем полигон arrowHead от ранее вычисленного наконечника стрелки и устанавливаем эти новые точки. painter->drawLine(line()); painter->drawPolygon(arrowHead); if (isSelected()) { painter->setPen(QPen(myColor, 1, Qt::DashLine)); QLineF myLine = line(); myLine.translate(0, 4.0); painter->drawLine(myLine); myLine.translate(0,-8.0); painter->drawLine(myLine); } } Если линия выделена, мы рисуем две пунктирные линии, параллельные стрелке Мы не используем реализацию по умолчанию, которая использует boundingRect(), потому что ограничивающий прямоугольник QRect заметно больше чем линия. |
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |