ダイアグラムシーンの例

Graphics Viewフレームワークの使い方を説明します。

ダイアグラムシーンの例は、フローチャート図を作成できるアプリケーションです。上の画像のように、フローチャートの図形とテキストを追加し、図形を矢印でつなぐことができます。図形、矢印、テキストには異なる色を付けることができ、テキストのフォント、スタイル、下線を変更することも可能です。

Qtグラフィックス・ビュー・フレームワークは、カスタムの2Dグラフィックス・アイテムを管理・表示するために設計されています。このフレームワークの主なクラスはQGraphicsItemQGraphicsSceneQGraphicsView です。QGraphicsView は、画面上にシーンをレンダリングするために使用されるウィジェットです。グラフィックス・ビュー・フレームワークの詳細については、グラフィックス・ビュー・フレームワークを参照してください。

この例では、QGraphicsSceneQGraphicsItem を継承したクラスを実装することで、このようなカスタム・グラフィック・シーンとアイテムを作成する方法を示します。

特に、以下の方法を示します:

  • カスタム・グラフィックス・アイテムの作成。
  • マウスイベントとアイテムの移動を処理する。
  • カスタムアイテムを管理できるグラフィックシーンの実装。
  • アイテムのカスタムペイント
  • 移動可能で編集可能なテキスト・アイテムの作成。

この例は、以下のクラスで構成されています:

  • MainWindow ウィジェットを作成し、 に表示する。また、ウィジェットとグラフィック・シーン、ビュー、アイテムの間のインタラクションを管理する。QMainWindow
  • DiagramItem QGraphicsPolygonItem を継承し、フローチャートのシェイプを表します。
  • TextDiagramItem QGraphicsTextItem を継承し、ダイアグラム内のテキスト・アイテムを表します。このクラスは、 ではサポートされていない、マウスによるアイテムの移動をサポートします。QGraphicsTextItem
  • Arrow QGraphicsLineItem を継承し、2 つの DiagramItems を結ぶ矢印を表します。
  • DiagramScene このクラスは、QGraphicsDiagramScene を継承し、 、 、 のサポートを提供します(すでに によって処理されているサポートに加えて)。DiagramItem Arrow DiagramTextItem QGraphicsScene

MainWindow クラスの定義

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
   MainWindow();

private slots:
    void backgroundButtonGroupClicked(QAbstractButton *button);
    void buttonGroupClicked(QAbstractButton *button);
    void deleteItem();
    void pointerGroupClicked();
    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);

    template<typename PointerToMemberFunction>
    QMenu *createColorMenu(const PointerToMemberFunction &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 に転送します。また、ダイアグラム・シーンのテキスト項目が変更されたり、ダイアグラム項目やダイアグラム・テキスト項目がシーンに挿入されたりすると、ウィジェットを更新します。

このクラスはまた、シーンからアイテムを削除し、アイテムが互いに重なっているときに描画される順序を決定する Z オーダーを処理します。

MainWindowクラスの実装

まずコンストラクタを見てみよう:

MainWindow::MainWindow()
{
    createActions();
    createToolBox();
    createMenus();

    scene = new DiagramScene(itemMenu, this);
    scene->setSceneRect(QRectF(0, 0, 5000, 5000));
    connect(scene, &DiagramScene::itemInserted,
            this, &MainWindow::itemInserted);
    connect(scene, &DiagramScene::textInserted,
            this, &MainWindow::textInserted);
    connect(scene, &DiagramScene::itemSelected,
            this, &MainWindow::itemSelected);
    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, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked),
            this, &MainWindow::buttonGroupClicked);
    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 は、常に1つのボタンをチェックしたままにします。このグループでは、すべてのボタンのチェックを外すことができるようにします。ダイアグラム・タイプを格納するために使用するユーザー・データを各ボタンに関連付けることができるので、ボタン・グループを使用します。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, &QAction::triggered, this, &MainWindow::bringToFront);

アクションの作成例を示します。アクションがトリガーする機能は、アクションを接続するスロットで説明します。

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

例の3つのメニューを作成します。

createToolbars() 関数は例のツールバーを設定します。colorToolBarfontColorToolButtonfillColorToolButtonlineColorToolButton の3つのQToolButtonは、QPainterQPixmap に描画することによってアイコンを作成するので、興味深いものです。fillColorToolButton の作成方法を示します。このボタンは、ユーザーがダイアグラムのアイテムの色を選択できるようにします。

void MainWindow::createToolbars()
{
    ...
    fillColorToolButton = new QToolButton;
    fillColorToolButton->setPopupMode(QToolButton::MenuButtonPopup);
    fillColorToolButton->setMenu(createColorMenu(&MainWindow::itemColorChanged, Qt::white));
    fillAction = fillColorToolButton->menu()->defaultAction();
    fillColorToolButton->setIcon(createColorToolButtonIcon(
                                     ":/images/floodfill.png", Qt::white));
    connect(fillColorToolButton, &QAbstractButton::clicked,
            this, &MainWindow::fillButtonTriggered);

ツールボタンのメニューはsetMenu() で設定します。fillAction QAction オブジェクトが、常にメニューの選択されたアクションを指している必要があります。メニューはcreateColorMenu() 関数で作成され、後で見るように、アイテムが持つことができる色ごとに1つのメニュー・アイテムが含まれています。ユーザーがボタンを押すと、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;
}

この関数は、DiagramItems の1つ、すなわちフローチャートの形状の画像を含むQToolButton を含むQWidget を返す。画像はimage() 関数を通してDiagramItem によって作成されます。QButtonGroup クラスでは、各ボタンに id(int) を付けることができます。私たちは、ダイアグラムの型、つまり、DiagramItem::DiagramType enum を格納します。シーンに対して新しいダイアグラム・アイテムを作成するときには、格納されているダイアグラム・タイプを使用します。この関数で作成されたウィジェットは、ツールボックスで使用されます。

以下は、createColorMenu() 関数です:

template<typename PointerToMemberFunction>
QMenu *MainWindow::createColorMenu(const PointerToMemberFunction &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, &QAction::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);
    // Draw icon centred horizontally on button.
    QRect target(4, 0, 42, 43);
    QRect source(0, 0, 42, 43);
    painter.fillRect(QRect(0, 60, 50, 80), color);
    painter.drawPixmap(target, image, source);

    return QIcon(pixmap);
}

この関数はfillColorToolButton,fontColorToolButton,lineColorToolButtonQIcon を作成するために使用されます。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 の色で塗りつぶされた矩形のアイコンを作成します。これは、fillColorToolButtonfontColorToolButtonlineColorToolButton のカラーメニューのアイコンを作成するために使用されます。

ここにbackgroundButtonGroupClicked() スロットがあります:

void MainWindow::backgroundButtonGroupClicked(QAbstractButton *button)
{
    const QList<QAbstractButton *> buttons = backgroundButtonGroup->buttons();
    for (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 を設定します。背景は、青、灰色、または白のタイルの正方形のグリッドにすることもできますし、まったくグリッドにしないこともできます。ブラシを作成するために、pngファイルからタイルのQPixmap

背景のタブ付きウィジェット・アイテムのボタンの1つがクリックされると、ブラシが変更されます。

以下はbuttonGroupClicked() の実装です:

void MainWindow::buttonGroupClicked(QAbstractButton *button)
{
    const QList<QAbstractButton *> buttons = buttonGroup->buttons();
    for (QAbstractButton *myButton : buttons) {
        if (myButton != button)
            button->setChecked(false);
    }
    const int id = buttonGroup->id(button);
    if (id == InsertTextButton) {
        scene->setMode(DiagramScene::InsertText);
    } else {
        scene->setItemType(DiagramItem::DiagramType(id));
        scene->setMode(DiagramScene::InsertItem);
    }
}

このスロットは、buttonGroup のボタンがチェックされたときに呼び出されます。ボタンがチェックされると、ユーザーはグラフィックビューをクリックすることができ、選択されたタイプのDiagramItemDiagramScene に挿入されます。一度にチェックできるボタンは1つだけなので、グループ内のボタンをループして他のボタンのチェックを外す必要があります。

QButtonGroup は各ボタンにidを割り当てます。各ボタンの id を、DiagramItem::DiagramType で指定された、クリックされたときにシーンに挿入されるダイアグラムのタイプに設定しました。テキストの場合、DiagramType 列挙型にない値を持つ id を割り当てました。setItemType()

以下は、deleteItem() の実装です:

void MainWindow::deleteItem()
{
    QList<QGraphicsItem *> selectedItems = scene->selectedItems();
    for (QGraphicsItem *item : std::as_const(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;
        }
    }

    selectedItems = scene->selectedItems();
    for (QGraphicsItem *item : std::as_const(selectedItems)) {
         if (item->type() == DiagramItem::Type)
             qgraphicsitem_cast<DiagramItem *>(item)->removeArrows();
         scene->removeItem(item);
         delete item;
     }
}

このスロットは、シーンから選択されたアイテム(もしあれば)を削除します。2回削除するのを避けるために、矢印を最初に削除します。削除するアイテムがDiagramItem の場合、それに接続されている矢印も削除する必要があります。両端のアイテムに接続されていない矢印はシーンに必要ありません。

これはpointerGroupClicked()の実装である:

void MainWindow::pointerGroupClicked()
{
    scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId()));
}

pointerTypeGroup 、シーンがItemMoveモードかInsertLineモードかを決定する。このボタン・グループは排他的で、常に1つのボタンだけがチェックされます。上記のbuttonGroup と同様に、DiagramScene::Mode enum の値に一致する id をボタンに割り当てました。

これがbringToFront() スロットです:

void MainWindow::bringToFront()
{
    if (scene->selectedItems().isEmpty())
        return;

    QGraphicsItem *selectedItem = scene->selectedItems().first();
    const QList<QGraphicsItem *> overlapItems = selectedItem->collidingItems();

    qreal zValue = 0;
    for (const QGraphicsItem *item : overlapItems) {
        if (item->zValue() >= zValue && item->type() == DiagramItem::Type)
            zValue = item->zValue() + 0.1;
    }
    selectedItem->setZValue(zValue);
}

いくつかのアイテムは、シーン内で互いに衝突する、すなわち、重なり合う可能性があります。このスロットは、ユーザーがアイテムを衝突するアイテムの上に置くように要求したときに呼び出されます。QGrapicsItems 、シーン内でアイテムが積み重ねられる順番を決めるz値を持っています。3D座標系のz軸のように考えることができます。アイテムが衝突すると、Z値の高いアイテムが低いアイテムの上に描かれます。アイテムを前面に持ってくるとき、衝突するアイテムをループして、それらすべてよりも高いz値を設定することができます。

ここにsendToBack() スロットがあります:

void MainWindow::sendToBack()
{
    if (scene->selectedItems().isEmpty())
        return;

    QGraphicsItem *selectedItem = scene->selectedItems().first();
    const QList<QGraphicsItem *> overlapItems = selectedItem->collidingItems();

    qreal zValue = 0;
    for (const QGraphicsItem *item : overlapItems) {
        if (item->zValue() <= zValue && item->type() == DiagramItem::Type)
            zValue = item->zValue() - 0.1;
    }
    selectedItem->setZValue(zValue);
}

このスロットは上で説明したbringToFront() と同じように働きますが、後方に送られるべきアイテムが衝突するアイテムよりも低いz値を設定します。

これは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 から呼び出されます。シーンのモードを、アイテムが挿入される前のモードに戻します。pointerTypeGroup のどのボタンがチェックされているかによって、ItemMoveかInsertTextになります。また、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 のウィジェットの1つを使用して、新しいQFont オブジェクトを作成し、そのプロパティをウィジェットの状態と一致するように設定します。これはhandleFontChange() で行われるので、単純にそのスロットを呼び出します。

これがfontSizeChanged() スロットです:

void MainWindow::fontSizeChanged(const QString &)
{
    handleFontChange();
}

ユーザがfontToolBar のウィジェットの1つを使用して、フォントの変更を要求すると、新しいQFont オブジェクトを作成し、そのプロパティをウィジェットの状態と一致するように設定します。これはhandleFontChange() で行われるので、単純にそのスロットを呼び出します。

以下はsceneScaleChanged() の実装です:

void MainWindow::sceneScaleChanged(const QString &scale)
{
    double newScale = scale.left(scale.indexOf(tr("%"))).toDouble() / 100.0;
    QTransform oldMatrix = view->transform();
    view->resetTransform();
    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",
                                     qvariant_cast<QColor>(textAction->data())));
    textButtonTriggered();
}

このスロットは、fontColorToolButton のドロップダウンメニューのアイテムが押されたときに呼び出されます。ボタンのアイコンを、選択されたQAction の色に変更する必要があります。選択されたアクションへのポインタをtextAction に保持します。textButtonTriggered() で、テキストの色をtextAction の色に変更するので、そのスロットを呼び出します。

以下はitemColorChanged() の実装です:

void MainWindow::itemColorChanged()
{
    fillAction = qobject_cast<QAction *>(sender());
    fillColorToolButton->setIcon(createColorToolButtonIcon(
                                     ":/images/floodfill.png",
                                     qvariant_cast<QColor>(fillAction->data())));
    fillButtonTriggered();
}

このスロットは、textColorChanged()DiagramTextItems に対して行うのと同じように、DiagramItems の色を変更するリクエストを処理します。

以下はlineColorChanged() の実装です:

void MainWindow::lineColorChanged()
{
    lineAction = qobject_cast<QAction *>(sender());
    lineColorToolButton->setIcon(createColorToolButtonIcon(
                                     ":/images/linecolor.png",
                                     qvariant_cast<QColor>(lineAction->data())));
    lineButtonTriggered();
}

このスロットは、textColorChanged()DiagramTextItems に対して行うのと同じ方法で、Arrows の色を変更するリクエストを扱います。

以下はtextButtonTriggered() スロットです:

void MainWindow::textButtonTriggered()
{
    scene->setTextColor(qvariant_cast<QColor>(textAction->data()));
}

textAction fontColorToolButton の色のドロップダウンメニューで現在選択されているメニューアイテムの を指します。アクションのデータをアクションが表す に設定したので、 でテキストの色を設定するときに、単純にこれを取得できます。QAction QColor setTextColor()

fillButtonTriggered()

void MainWindow::fillButtonTriggered()
{
    scene->setItemColor(qvariant_cast<QColor>(fillAction->data()));
}

fillAction fillColorToolButton()のドロップダウンメニューで選択されたメニューアイテムを指します。 したがって、 でアイテムの色を設定するときに、このアクションのデータを使うことができます。setItemColor()

lineButtonTriggered()

void MainWindow::lineButtonTriggered()
{
    scene->setLineColor(qvariant_cast<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 DiagramScene setFont() DiagramTextItems

ここに、itemSelected() スロットがあります:

void MainWindow::itemSelected(QGraphicsItem *item)
{
    DiagramTextItem *textItem =
    qgraphicsitem_cast<DiagramTextItem *>(item);

    QFont font = textItem->font();
    fontCombo->setCurrentFont(font);
    fontSizeCombo->setEditText(QString().setNum(font.pointSize()));
    boldAction->setChecked(font.weight() == QFont::Bold);
    italicAction->setChecked(font.italic());
    underlineAction->setChecked(font.underline());
}

このスロットは、DiagramScene のアイテムが選択されると呼び出されます。この例の場合、選択されたときにシグナルを発するのはテキスト・アイテムだけなので、item がどのようなグラフィックであるかをチェックする必要はない。

選択されたテキスト・アイテムのフォントのプロパティに合わせて、ウィジェットの状態を設定します。

これは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メニュー項目を選択すると、例のaboutボックスを表示します。

DiagramScene クラスの定義

DiagramScene クラスは、QGraphicsScene を継承し、そのスーパークラスによって処理される項目に加えて、DiagramItemsArrowsDiagramTextItems を処理する機能を追加しています。

class DiagramScene : public QGraphicsScene
{
    Q_OBJECT

public:
    enum Mode { InsertItem, InsertLine, InsertText, MoveItem };

    explicit DiagramScene(QMenu *itemMenu, QObject *parent = nullptr);
    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) override;
    void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) override;

private:
    bool isItemChange(int type) const;

    DiagramItem::DiagramType myItemType;
    QMenu *myItemMenu;
    Mode myMode;
    bool leftButtonDown;
    QPointF startPoint;
    QGraphicsLineItem *line;
    QFont myFont;
    DiagramTextItem *textItem;
    QColor myTextColor;
    QColor myItemColor;
    QColor myLineColor;
};

DiagramScene では、マウスをクリックすると、3つの異なるアクションを与えることができます。マウスの下にあるアイテムが移動したり、アイテムが挿入されたり、ダイアグラムのアイテム間に矢印が接続されたりします。マウス・クリックがどのアクションを持つかは、モード列挙型によって与えられる、シーンが置かれているモードに依存します。モードは、setMode() 関数で設定します。

シーンは、アイテムの色とテキストアイテムのフォントも設定します。シーンが使用する色とフォントは、setLineColor()setTextColor()setItemColor()setFont() 関数で設定できる。アイテムが挿入されるときに作成される、DiagramItem::DiagramType関数によって与えられるDiagramItem のタイプは、setItemType() スロットで設定されます。

MainWindowDiagramScene は、例の機能を分担します。MainWindow は、次のタスクを処理します:アイテム、テキスト、矢印の削除、ダイアグラム・アイテムの後ろと前への移動、シーンのスケールの設定。

DiagramScene クラスの実装

コンストラクターから始めます:

DiagramScene::DiagramScene(QMenu *itemMenu, QObject *parent)
    : QGraphicsScene(parent)
{
    myItemMenu = itemMenu;
    myMode = MoveItem;
    myItemType = DiagramItem::Step;
    line = nullptr;
    textItem = nullptr;
    myItemColor = Qt::white;
    myTextColor = Qt::black;
    myLineColor = Qt::black;
}

このシーンは、DiagramItems を作成するときに、myItemMenu を使用してコンテキスト・メニューを設定します。デフォルト・モードを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 関数は、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);
    }
}

この関数は、setLineColor()Arrows の色を設定するのと同じように、DiagramTextItems の色を設定します。

以下は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());
        //At this point the selection can change so the first selected item might not be a 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 、矢印が接続するアイテムの間に線を伸ばしてシーンに追加します。線の始点はユーザーがマウスをクリックした場所に固定され、終点はボタンが押されている限りマウスポインタに従います。ユーザーがマウスボタンを離すと、線の始点と終点の下にDiagramItem があれば、Arrow がシーンに追加されます。これがどのように実装されるかは後で説明する。ここでは単に行を追加する。

        case InsertText:
            textItem = new DiagramTextItem();
            textItem->setFont(myFont);
            textItem->setTextInteractionFlags(Qt::TextEditorInteraction);
            textItem->setZValue(1000.0);
            connect(textItem, &DiagramTextItem::lostFocus,
                    this, &DiagramScene::editorLostFocus);
            connect(textItem, &DiagramTextItem::selectedChange,
                    this, &DiagramScene::itemSelected);
            addItem(textItem);
            textItem->setDefaultTextColor(myTextColor);
            textItem->setPos(mouseEvent->scenePos());
            emit textInserted(textItem);

DiagramTextItemQt::TextEditorInteraction フラグがセットされていれば編集可能で、セットされていなければマウスで移動できる。テキストは常にシーン内の他のアイテムの上に描画されるようにしたいので、値をシーン内の他のアイテムよりも高い数値に設定する。

    default:
        ;
    }
    QGraphicsScene::mousePressEvent(mouseEvent);
}

デフォルトのスイッチに到達すれば、MoveItemモードになる。そうすれば、マウスによるアイテムの移動を処理するQGraphicsScene の実装を呼び出すことができる。アイテムを追加し、マウスボタンを押し下げたままアイテムの移動を開始することができます。テキスト・アイテムの場合、編集可能な状態ではマウス・イベントを伝搬しないので、これは不可能です。

これがmouseMoveEvent()

void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
    if (myMode == InsertLine && line != nullptr) {
        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 != nullptr && 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 。もしあれば、その2つの項目でArrow 。次に、矢印を各アイテムに追加し、最後にシーンに追加する。矢印の始点と終点をアイテムに合わせるために、矢印を更新する必要があります。矢印のz値を-1000.0に設定するのは、常にアイテムの下に描画されるようにするためです。

    line = nullptr;
    QGraphicsScene::mouseReleaseEvent(mouseEvent);
}

isItemChange()

bool DiagramScene::isItemChange(int type) const
{
    const QList<QGraphicsItem *> items = selectedItems();
    const auto cb = [type](const QGraphicsItem *item) { return item->type() == type; };
    return std::find_if(items.begin(), items.end(), cb) != items.end();
}

シーンはシングル・セレクション、つまり、いつでも1つのアイテムしか選択できない。isItemChange() は、選択されたアイテムが存在し、指定されたダイアグラムのものであるかどうかをチェックするために使用されますtype

DiagramItem クラスの定義

class DiagramItem : public QGraphicsPolygonItem
{
public:
    enum { Type = UserType + 15 };
    enum DiagramType { Step, Conditional, StartEnd, Io };

    DiagramItem(DiagramType diagramType, QMenu *contextMenu, QGraphicsItem *parent = nullptr);

    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 override { return Type; }

protected:
    void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
    QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;

private:
    DiagramType myDiagramType;
    QPolygonF myPolygon;
    QMenu *myContextMenu;
    QList<Arrow *> arrows;
};

DiagramItem は、DiagramScene のフローチャートの形状を表します。QGraphicsPolygonItem を継承し、各形状に対して多角形を持ちます。列挙型 DiagramType は、フローチャートの各形状に対応する値を持っています。

このクラスは、それに接続されている矢印のリストを持っています。これは、アイテムだけが、(itemChanged() 関数で)いつ移動されるかを知っていて、その時に矢印を更新しなければならないので必要である。アイテムは、image() 関数を使って、QPixmap 上に描画することもできます。これはMainWindow のツールボタンに使用されます。MainWindowcreateColorToolButtonIcon() を参照してください。

Type列挙型はクラスのユニークな識別子です。これはqgraphicsitem_cast() で使用され、グラフィック・アイテムの動的キャストを行います。UserType定数は、カスタム・グラフィックス・アイテムのタイプが取り得る最小値です。

DiagramItemクラスの実装

まず、コンストラクタを見てみましょう:

DiagramItem::DiagramItem(DiagramType diagramType, QMenu *contextMenu,
                         QGraphicsItem *parent)
    : QGraphicsPolygonItem(parent), 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)
{
    arrows.removeAll(arrow);
}

removeArrow() DiagramItems は、 アイテムをシーンから削除するときに使用します。Arrow

以下はremoveArrows() 関数です:

void DiagramItem::removeArrows()
{
    // need a copy here since removeArrow() will
    // modify the arrows container
    const auto arrowsCopy = arrows;
    for (Arrow *arrow : arrowsCopy) {
        arrow->startItem()->removeArrow(arrow);
        arrow->endItem()->removeArrow(arrow);
        scene()->removeItem(arrow);
        delete arrow;
    }
}

この関数は、アイテムがシーンから削除されたときに呼び出され、このアイテムに接続されているすべての矢印を削除します。矢印は、その開始アイテムと終了アイテムの両方のarrows リストから削除されなければならない。開始アイテムか終了アイテムのどちらかが、現在この関数が呼び出されているオブジェクトなので、removeArrow()はこのコンテナを変更するので、矢印のコピーで動作することを確認する必要があります。

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

コンテキストメニューを表示します。メニューを表示するマウスの右クリックは、デフォルトではアイテムを選択しないので、setSelected() で選択されたアイテムを設定します。これは、bringToFrontsendToBack のアクションでアイテムの高さを変更するには、アイテムを選択する必要があるためです。

これはitemChange() の実装です:

QVariant DiagramItem::itemChange(GraphicsItemChange change, const QVariant &value)
{
    if (change == QGraphicsItem::ItemPositionChange) {
        for (Arrow *arrow : std::as_const(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 = nullptr);

    int type() const override { return Type; }

signals:
    void lostFocus(DiagramTextItem *item);
    void selectedChange(QGraphicsItem *item);

protected:
    QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
    void focusOutEvent(QFocusEvent *event) override;
    void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
};

itemChange()focusOutEvent() を使って、テキスト・アイテムがフォーカスを失い選択されたときにDiagramScene に通知します。

マウス・イベントを処理する関数を再実装して、QGraphicsTextItem のマウス動作を変更できるようにしています。

DiagramTextItemの実装

コンストラクタから始めます:

DiagramTextItem::DiagramTextItem(QGraphicsItem *parent)
    : QGraphicsTextItem(parent)
{
    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 クラスは、2 つのDiagramItems を接続するグラフィック・アイテムです。これは、アイテムの1つに矢印の頭を描画します。これを実現するために、アイテムはそれ自身をペイントし、衝突や選択をチェックするためにグラフィックシーンで使用されるメソッドを再実装する必要があります。このクラスは QGraphicsLine item を継承しており、矢印を描画し、接続するアイテムと一緒に移動します。

class Arrow : public QGraphicsLineItem
{
public:
    enum { Type = UserType + 4 };

    Arrow(DiagramItem *startItem, DiagramItem *endItem,
          QGraphicsItem *parent = nullptr);

    int type() const override { return Type; }
    QRectF boundingRect() const override;
    QPainterPath shape() const override;
    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 = nullptr) override;

private:
    DiagramItem *myStartItem;
    DiagramItem *myEndItem;
    QPolygonF arrowHead;
    QColor myColor = Qt::black;
};

アイテムの色はsetColor() で設定できる。

boundingRect() と は から再実装されたもので、シーンが衝突と選択をチェックするために使用します。shape() QGraphicsLineItem

updatePosition() を呼び出すと、矢印の位置と矢印の頭の角度が再計算されます。paint() は再実装されているので、アイテム間の単なる線ではなく、矢印を描くことができます。

myStartItem と は、矢印が接続するダイアグラムの項目です。 は、矢印の頭を描画するために使用する、3 つの頂点を持つ多角形です。myEndItem arrowHead

矢印クラスの実装

Arrow クラスのコンストラクタは次のようになっています:

Arrow::Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent)
    : QGraphicsLineItem(parent), myStartItem(startItem), myEndItem(endItem)
{
    setFlag(QGraphicsItem::ItemIsSelectable, true);
    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;
}

shape 関数は、アイテムの正確な形状である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 intersectPoint;
    for (int i = 1; i < endPolygon.count(); ++i) {
        QPointF p2 = endPolygon.at(i) + myEndItem->pos();
        QLineF polyLine = QLineF(p1, p2);
        QLineF::IntersectionType intersectionType =
            polyLine.intersects(centerLine, &intersectPoint);
        if (intersectionType == QLineF::BoundedIntersection)
            break;
        p1 = p2;
    }

    setLine(QLineF(intersectPoint, myStartItem->pos()));

次に、矢の頭を描く位置を見つける必要がある。矢印の頭は、線と端のアイテムが交差する位置に描く。これは、ポリゴンの各点を結ぶ線をとり、それが矢印の線と交差するかどうかをチェックすることで行う。線の始点と終点はアイテムの中心に設定されているので、矢印の線はポリゴンの線と1本だけ交差するはずである。多角形の点は、アイテムのローカル座標系からの相対点であることに注意。したがって、座標をシーンからの相対座標にするために、端のアイテムの位置を加えなければならない。

    double angle = std::atan2(-line().dy(), line().dx());

    QPointF arrowP1 = line().p1() + QPointF(sin(angle + M_PI / 3) * arrowSize,
                                    cos(angle + M_PI / 3) * arrowSize);
    QPointF arrowP2 = line().p1() + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize,
                                    cos(angle + M_PI - M_PI / 3) * arrowSize);

    arrowHead.clear();
    arrowHead << line().p1() << arrowP1 << arrowP2;

X軸と矢印の線の角度を計算する。矢印の方向に沿うように、矢印の頭をこの角度まで回転させる必要があります。角度が負の場合は、矢印の方向を変えなければならない。

次に、矢尻の多角形の3点を計算する。そのうちの1点は線の終点で、矢印の線と終点ポリゴンの交点になる。次に、前回計算した矢尻のポリゴン(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);
    }
}

線が選択されている場合は、矢印の線と平行な2本の点線を描きます。QRect の外接矩形は線よりもかなり大きいので、boundingRect() を使用するデフォルトの実装は使用しません。

プロジェクト例 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.