図のシーン例
Graphics View フレームワークの使用方法を紹介します。

「Diagram Scene」のサンプルは、フローチャートを作成できるアプリケーションです。上の画像のように、フローチャートの図形やテキストを追加し、図形同士を矢印でつなぐことができます。図形、矢印、テキストにはそれぞれ異なる色を割り当てることができ、テキストのフォント、スタイル、下線を変更することも可能です。
QGraphicsItem QGraphicsScene QGraphicsViewXML-ph-0000@deepl.internal、XML-ph-0001@deepl.internal、およびXML-ph-0002@deepl.internalです。Graphics Sceneはアイテムを管理し、それらを表示するための表面を提供します。QGraphicsView は、シーンを画面上にレンダリングするために使用されるウィジェットです。フレームワークの詳細については、「Graphics Viewフレームワーク」を参照してください。
この例では、QGraphicsScene およびQGraphicsItem を継承するクラスを実装することで、このようなカスタムグラフィックスシーンやアイテムを作成する方法を示します。
具体的には、以下の方法について説明します:
- カスタムグラフィックスアイテムの作成。
- マウスイベントやアイテムの移動を処理する。
- カスタムアイテムを管理できるグラフィックスシーンの実装。
- アイテムのカスタム描画。
- 移動および編集可能なテキストアイテムの作成。
このサンプルは、以下のクラスで構成されています:
MainWindowウィジェットを作成し、QMainWindow 内に表示します。また、ウィジェットとグラフィックスシーン、ビュー、アイテム間の相互作用を管理します。DiagramItemQGraphicsPolygonItem を継承し、フローチャートの図形を表します。TextDiagramItemQGraphicsTextItem を継承し、ダイアグラム内のテキストアイテムを表します。このクラスは、 ではサポートされていない、マウスによるアイテムの移動機能を追加しています。QGraphicsTextItemArrowQGraphicsLineItem を継承し、2つの DiagramItem を結ぶ矢印です。DiagramSceneQGraphicsDiagramSceneを継承し、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() 関数は、例にあるツールバーを設定します。colorToolBar にある3つのQToolButton(fontColorToolButton 、fillColorToolButton 、lineColorToolButton )は、QPainter を使用してQPixmap 上に描画することでアイコンを作成しているため、興味深いものです。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 (フローチャート図形)のいずれかの画像を持つQToolButton を含むQWidget を返します。この画像は、DiagramItem がimage() 関数を通じて生成します。QButtonGroup クラスを使用すると、各ボタンにID(int型)を割り当てることができます。ここでは、ダイアグラムのタイプ、つまりDiagramItem::DiagramType列挙型を格納します。シーン用に新しいダイアグラムアイテムを作成する際、この格納されたダイアグラムタイプを使用します。この関数で作成されたウィジェットは、ツールボックスで使用されます。
以下に、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 、およびlineColorToolButton のQIcon を作成するために使用されます。imageFile という文字列は、ボタンに使用されるtext、flood-fill、またはlineシンボルのいずれかです。画像の下には、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)
{
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();
}この関数では、diagramsceneの背景を描画するために使用されるQBrush を設定します。背景は、青、灰色、または白のタイルで構成されたグリッド、あるいはグリッドなしのいずれかになります。PNGファイルから取得したタイルのQPixmapを用意しており、それらを使用してブラシを作成します。
背景タブ付きウィジェット項目のボタンのいずれかがクリックされると、ブラシを変更します。どのボタンがクリックされたかは、そのテキストを確認することで判別します。
以下に、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 内のボタンがチェックされたときに呼び出されます。ボタンがチェックされると、ユーザーはグラフィックスビューをクリックでき、選択されたタイプのDiagramItem がDiagramScene に挿入されます。一度にチェックできるボタンは1つだけであるため、グループ内のボタンをループ処理して、他のボタンのチェックを解除する必要があります。
QButtonGroup 各ボタンにIDを割り当てます。各ボタンのIDには、クリック時にシーンに挿入される が示すDiagramItem::DiagramTypeの値を設定しています。 その後、setItemType() を使用してダイアグラムタイプを設定する際に、このボタンIDを使用できます。テキストの場合は、DiagramType列挙型に含まれていない値を持つIDを割り当てました。
以下に、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列挙型の値に対応するIDをボタンに割り当てており、その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値が小さいアイテムの上に描画されます。アイテムを最前面に持ってくる際には、そのアイテムと重なり合うアイテムをすべてループで処理し、それらすべてよりも大きな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 」内のウィジェットのいずれかを使用してフォントの変更を要求した際、新しい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;
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」メニュー項目を選択した際に、この例のアバウトボックスを表示します。
DiagramScene クラスの定義
DiagramScene クラスはQGraphicsScene を継承し、スーパークラスが処理する項目に加えて、DiagramItems 、Arrows 、およびDiagramTextItems を処理する機能を追加しています。
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つの異なるアクションが発生します。マウスカーソルの下にあるアイテムが移動したり、アイテムが挿入されたり、図のアイテム間に矢印が接続されたりします。マウスクリックによってどのアクションが発生するかは、シーンが設定されているモード(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 = 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 アイテムが選択されている場合にtrueを返します。その場合、その色を変更します。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());
//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);`Qt::TextEditorInteraction `フラグが設定されている場合、`DiagramTextItem `は編集可能になります。そうでない場合は、マウスで移動可能です。テキストは常にシーン内の他のアイテムの上に描画されるようにしたいので、その値をシーン内の他のアイテムよりも大きい数値に設定します。
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 モードでマウスボタンが押されている場合(line が 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();
}
}次に、線の始点と終点の下に2つの異なる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つだけです。したがって、forループは、選択されたアイテムがある場合はそのアイテムに対して1回、アイテムが選択されていない場合は0回実行されます。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 のツールボタンで使用されており、MainWindow のcreateColorToolButtonIcon() を参照してください。
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() Arrow アイテム、またはそれらが接続されている がシーンから削除された際に、 アイテムを削除するために使用されます。DiagramItems
以下は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 をitemsの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() を使用して項目を選択するように設定しています。これは、bringToFront およびsendToBack アクションで項目の標高を変更するには、項目が選択されている必要があるためです。
以下は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 を継承し、編集可能なテキストアイテムを移動させる機能を追加しています。編集可能なQGraphicsTextItemは、所定の位置に固定されるように設計されており、ユーザーがアイテムをシングルクリックすると編集が開始されます。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;
};テキストアイテムがフォーカスを失ったり、選択されたりした際に、DiagramScene に通知するために、itemChange() およびfocusOutEvent() を使用します。
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クラスの定義
Arrow クラスは、2つのDiagramItems を接続するグラフィックスアイテムです。このクラスは、いずれかのアイテムに矢印の先端を描画します。これを実現するには、アイテムが自身を描画するとともに、グラフィックスシーンが衝突や選択のチェックに使用するメソッドを再実装する必要があります。このクラスはQGraphicsLineItemを継承しており、矢印の先端を描画し、接続されたアイテムとともに移動します。
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 およびmyEndItem は、矢印が接続するダイアグラム項目です。矢印は、矢印の先端が終端項目に向くように描画されます。arrowHead は、矢印の先端を描画するために使用する、3つの頂点を持つポリゴンです。
Arrow クラスの実装
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() を使用するデフォルトの実装は使用しません。
© 2026 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.