다이어그램 장면 예시

그래픽 보기 프레임워크 사용 방법을 시연합니다.

다이어그램 장면 예제는 플로우차트 다이어그램을 만들 수 있는 애플리케이션입니다. 위 이미지와 같이 순서도 도형과 텍스트를 추가하고 도형을 화살표로 연결할 수 있습니다. 도형, 화살표, 텍스트에 서로 다른 색상을 지정할 수 있으며 텍스트의 글꼴, 스타일, 밑줄을 변경할 수 있습니다.

Qt 그래픽 뷰 프레임워크는 사용자 정의 2D 그래픽 항목을 관리하고 표시하도록 설계되었습니다. 프레임워크의 주요 클래스는 QGraphicsItem, QGraphicsSceneQGraphicsView 입니다. 그래픽 장면은 항목을 관리하고 해당 항목에 대한 표면을 제공합니다. QGraphicsView 은 화면에 장면을 렌더링하는 데 사용되는 위젯입니다. 프레임워크에 대한 자세한 설명은 그래픽 보기 프레임워크를 참조하세요.

이 예에서는 QGraphicsSceneQGraphicsItem 을 상속하는 클래스를 구현하여 이러한 사용자 지정 그래픽 장면 및 항목을 만드는 방법을 보여줍니다.

특히 다음과 같은 방법을 보여드립니다:

  • 사용자 지정 그래픽 항목 만들기.
  • 마우스 이벤트와 아이템의 움직임을 처리합니다.
  • 사용자 지정 항목을 관리할 수 있는 그래픽 장면을 구현합니다.
  • 항목의 사용자 지정 페인팅.
  • 움직이고 편집 가능한 텍스트 항목 만들기.

이 예제는 다음 클래스로 구성됩니다:

  • MainWindow 위젯을 생성하고 QMainWindow 에 표시합니다. 또한 위젯과 그래픽 장면, 보기 및 항목 간의 상호 작용을 관리합니다.
  • DiagramItem QGraphicsPolygonItem 을 상속하고 순서도 모양을 나타냅니다.
  • TextDiagramItem QGraphicsTextItem 을 상속하고 다이어그램의 텍스트 항목을 나타냅니다. 이 클래스는 QGraphicsTextItem 에서 지원하지 않는 마우스로 항목 이동에 대한 지원을 추가합니다.
  • Arrow QGraphicsLineItem 를 상속하며 두 개의 DiagramItems를 연결하는 화살표입니다.
  • DiagramScene 에서 이미 처리하는 지원 외에 DiagramItem, ArrowDiagramTextItem 에 대한 지원을 제공합니다( QGraphicsScene 에서 이미 처리하는 지원 추가).

메인 윈도우 클래스 정의

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 은 항상 하나의 버튼이 선택된 상태로 유지되며, 그룹에서는 모든 버튼의 선택을 해제할 수 있도록 합니다. 다이어그램 유형을 저장하는 데 사용하는 사용자 데이터를 각 버튼과 연결할 수 있기 때문에 여전히 버튼 그룹을 사용합니다. 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 , fontColorToolButton, fillColorToolButton, lineColorToolButton 에 있는 세 개의 QToolButtonQPixmapQPainter 을 그려서 아이콘을 만드는 것이 흥미롭습니다. 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 을 반환합니다. 이미지는 image() 함수를 통해 DiagramItem 에 의해 생성됩니다. 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, 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 의 색상으로 채워진 직사각형 아이콘을 만듭니다. 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();
}

이 함수에서는 다이어그램 장면의 배경을 그리는 데 사용되는 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 의 버튼이 선택되면 호출됩니다. 버튼이 체크되면 사용자는 그래픽 보기를 클릭할 수 있고 선택한 유형의 DiagramItemDiagramScene 에 삽입됩니다. 한 번에 하나의 버튼만 체크할 수 있으므로 다른 버튼을 체크 해제하려면 그룹의 버튼을 반복해야 합니다.

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값을 설정할 수 있습니다.

다음은 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()));
}

fillActionfillColorToolButton() 의 드롭다운 메뉴에서 선택한 메뉴 항목을 가리킵니다. 따라서 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 객체를 만들고 위젯에 따라 속성을 설정합니다. 그런 다음 DiagramScenesetFont() 함수를 호출하여 이 함수가 관리하는 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 클래스는 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() 함수로 설정합니다.

장면은 항목의 색상과 텍스트 항목의 글꼴도 설정합니다. 씬에서 사용하는 색상과 글꼴은 setLineColor(), setTextColor(), setItemColor()setFont() 함수를 사용하여 설정할 수 있습니다. 항목이 삽입될 때 생성될 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 을 사용하여 컨텍스트 메뉴를 설정합니다. QGraphicsScene 의 기본 동작을 제공하므로 기본 모드를 DiagramScene::MoveItem 로 설정합니다.

다음은 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);

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

삽입 모드에 있고 마우스 버튼을 누르고 있는 경우(선이 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 루프는 선택된 항목과 함께 한 번 반복되거나 선택된 항목이 없으면 반복되지 않습니다. 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;
};

DiagramItemDiagramScene 의 순서도 모양을 나타냅니다. 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()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);
}

이 함수는 arrows 목록에 arrow 항목을 추가하기만 하면 됩니다.

다음은 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 클래스는 두 개의 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() 은 항목 사이에 단순한 선이 아닌 화살표를 그릴 수 있도록 다시 구현되었습니다.

myStartItemmyEndItem 는 화살표가 연결하는 다이어그램 항목입니다. 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;
}

모양 함수는 항목의 정확한 모양인 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()를 사용하는 기본 구현은 사용하지 않습니다.

예제 프로젝트 @ 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.