도식 장면 예시
Graphics View 프레임워크 사용 방법을 보여줍니다.

‘다이어그램 장면’ 예제는 플로우차트 다이어그램을 만들 수 있는 애플리케이션입니다. 위 이미지와 같이 플로우차트 도형과 텍스트를 추가하고, 화살표로 도형들을 연결할 수 있습니다. 도형, 화살표 및 텍스트에 서로 다른 색상을 지정할 수 있으며, 텍스트의 글꼴, 스타일 및 밑줄을 변경할 수도 있습니다.
Qt 그래픽 뷰 프레임워크는 사용자 정의 2D 그래픽 항목을 관리하고 표시하도록 설계되었습니다. 이 프레임워크의 주요 클래스는 QGraphicsItem, QGraphicsScene 및 QGraphicsView 입니다. 그래픽 씬은 항목을 관리하고 이를 위한 표면을 제공합니다. QGraphicsView 는 화면에 씬을 렌더링하는 데 사용되는 위젯입니다. 프레임워크에 대한 더 자세한 설명은 그래픽 뷰 프레임워크를 참조하십시오.
이 예제에서는 ` QGraphicsScene ` 및 ` QGraphicsItem`을 상속하는 클래스를 구현하여 이러한 사용자 정의 그래픽 씬과 항목을 만드는 방법을 보여줍니다.
특히 다음을 수행하는 방법을 보여줍니다:
- 사용자 정의 그래픽 항목을 생성하는 방법.
- 마우스 이벤트 및 아이템의 이동을 처리하는 방법.
- 사용자 정의 항목을 관리할 수 있는 그래픽 씬 구현.
- 아이템의 사용자 정의 그리기.
- 이동 및 편집이 가능한 텍스트 항목을 생성하는 방법.
이 예제는 다음 클래스로 구성되어 있습니다:
MainWindow위젯을 생성하여 ` QMainWindow`에 표시합니다. 또한 위젯과 그래픽 씬, 뷰, 아이템 간의 상호작용을 관리합니다.DiagramItemQGraphicsPolygonItem 을 상속하며 플로우차트 도형을 나타냅니다.TextDiagramItemQGraphicsTextItem 을 상속하며 다이어그램 내의 텍스트 항목을 나타냅니다. 이 클래스는 에서 지원하지 않는 마우스를 이용한 항목 이동 기능을 추가합니다. QGraphicsTextItemArrowQGraphicsLineItem 를 상속하며, 두 개의 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);함수의 이 부분은 플로우차트 도형을 포함하는 탭형 위젯 항목을 설정합니다. 배타적(exclusive)인 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, &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);
}예제에 나오는 세 개의 메뉴를 생성합니다.
createToolbars() 함수는 예제의 툴바를 설정합니다. colorToolBar 내의 세 개의 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() ` 함수를 사용하여 생성되며, 나중에 살펴보겠지만 항목이 가질 수 있는 각 색상마다 하나의 메뉴 항목을 포함합니다. 사용자가 버튼을 누르면 ` 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 문자열은 버튼에 사용되는 텍스트, 채우기 또는 선 심볼 중 하나입니다. 이미지 아래에는 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();
}QPixmap이 함수에서는 다이어그램 장면(diagramscene)의 배경을 그리는 데 사용되는 QBrush 를 설정합니다. 배경은 파란색, 회색 또는 흰색 타일로 구성된 격자 형태이거나, 격자가 전혀 없을 수도 있습니다. 우리는 PNG 파일에서 가져온 타일 이미지를 사용하여 브러시를 생성합니다.
배경 탭 위젯 항목의 버튼 중 하나가 클릭되면 브러시를 변경합니다. 버튼의 텍스트를 확인하여 어떤 버튼인지 파악합니다.
다음은 ` 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 에 삽입됩니다. 한 번에 하나의 버튼만 선택될 수 있으므로, 그룹 내의 버튼을 순차적으로 처리하여 다른 버튼의 선택을 해제해야 합니다.
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;
}
}이 슬롯은 선택된 항목이 있는 경우, 해당 항목을 씬에서 삭제합니다. 화살표가 두 번 삭제되는 것을 방지하기 위해 화살표를 먼저 삭제합니다. 삭제할 항목이 DiagramItem 인 경우, 해당 항목에 연결된 화살표도 함께 삭제해야 합니다. 씬에는 양쪽 끝이 모두 항목에 연결되지 않은 화살표가 남아 있어서는 안 되기 때문입니다.
다음은 pointerGroupClicked()의 구현입니다:
void MainWindow::pointerGroupClicked()
{
scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId()));
}pointerTypeGroup 는 장면이 ItemMove 모드인지 InsertLine 모드인지 결정합니다. 이 버튼 그룹은 배타적이며, 즉 언제든 하나의 버튼만 선택된 상태를 유지합니다. 위의 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."));
}이 슬롯은 사용자가 도움말 메뉴에서 ‘정보’ 메뉴 항목을 선택할 때 예제에 대한 정보 상자를 표시합니다.
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 setMode() 에서는 마우스 클릭으로 세 가지 다른 동작이 발생할 수 있습니다. 마우스 커서가 위치한 항목을 이동하거나, 항목을 삽입하거나, 다이어그램 항목들 사이에 화살표를 연결할 수 있습니다. 마우스 클릭이 어떤 동작을 유발할지는 함수를 통해 설정된 Mode 열거형에 따라 결정됩니다.
또한 장면은 항목의 색상과 텍스트 항목의 글꼴을 설정합니다. 장면에서 사용하는 색상과 글꼴은 ` 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();
}
}이제 선의 시작점과 끝점 아래에 서로 다른 두 개의 ` DiagramItems `가 있는지 확인합니다. 있다면 두 항목을 사용하여 ` 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();
}이 장면은 단일 선택 방식을 채택하고 있습니다. 즉, 한 번에 하나의 항목만 선택할 수 있습니다. 따라서 for 루프는 선택된 항목이 있을 경우 한 번, 선택된 항목이 없을 경우 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 클래스는 두 개의 DiagramItems 를 연결하는 그래픽스 항목입니다. 이 클래스는 항목 중 하나에 화살표 머리를 그립니다. 이를 위해 해당 항목은 스스로를 렌더링해야 할 뿐만 아니라, 그래픽스 씬에서 충돌 및 선택을 확인하는 데 사용하는 메서드들도 재구현해야 합니다. 이 클래스는 QGraphicsLine 항목을 상속받으며, 화살표 머리를 그리고 연결된 항목과 함께 이동합니다.
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
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()));그런 다음 화살표 끝을 그릴 위치를 찾아야 합니다. 화살표 끝은 선과 끝 항목이 교차하는 지점에 그려져야 합니다. 이를 위해 다각형의 각 점 사이를 연결하는 선을 구한 후, 이 선이 화살표 선과 교차하는지 확인합니다. 선의 시작점과 끝점이 항목의 중심에 설정되어 있으므로, 화살표 선은 다각형의 선 중 딱 하나와만 교차해야 합니다. 다각형 내의 점들은 해당 항목의 로컬 좌표계를 기준으로 한다는 점에 유의하십시오. 따라서 좌표를 씬 기준 좌표계로 변환하기 위해 끝 항목의 위치를 더해야 합니다.
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축과 화살표 선 사이의 각도를 계산합니다. 화살표가 화살표 방향을 따르도록 화살표 머리의 방향을 이 각도로 돌려야 합니다. 각도가 음수인 경우 화살표의 방향을 반대로 돌려야 합니다.
그런 다음 화살표 머리 다각형의 세 점을 계산할 수 있습니다. 점 중 하나는 선분의 끝점이며, 이는 이제 화살표 선분과 끝 다각형의 교차점입니다. 그런 다음 이전에 계산된 화살표 머리의 ` 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);
}
}선이 선택된 경우, 화살표 선과 평행한 두 개의 점선을 그립니다. 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.