이 페이지에서

드릴다운 예제

드릴다운 예제는 ` QSqlRelationalTableModel ` 및 ` QDataWidgetMapper ` 클래스를 사용하여 데이터베이스에서 데이터를 읽어오고 변경 사항을 전송하는 방법을 보여줍니다.

메인 창에는 Qt, Qt Quick, Qt Creator 및 Qt Project 제품 중 사용자가 선택할 수 있는 이미지가 표시됩니다.

예제 애플리케이션을 실행하면 사용자는 해당 이미지를 클릭하여 각 항목에 대한 정보를 조회할 수 있습니다. 애플리케이션은 데이터를 표시하는 정보 창을 띄우며, 사용자가 설명과 이미지를 수정할 수 있도록 합니다. 사용자가 변경 사항을 제출하면 메인 뷰가 업데이트됩니다.

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

  • ImageItem 는 이미지를 표시하는 데 사용되는 사용자 정의 그래픽 항목 클래스입니다.
  • View 는 사용자가 다양한 항목을 탐색할 수 있게 해주는 메인 애플리케이션 위젯입니다.
  • InformationWindow 요청된 정보를 표시하며, 사용자가 정보를 수정하고 변경 사항을 데이터베이스에 제출할 수 있게 해줍니다.

먼저 ` InformationWindow ` 클래스를 살펴보며 데이터베이스에서 데이터를 읽고 수정하는 방법을 알아보겠습니다. 그런 다음 메인 애플리케이션 위젯인 ` View ` 클래스와 관련 ` ImageItem ` 클래스를 검토할 것입니다.

InformationWindow 클래스 정의

InformationWindow 클래스는 QWidget 을 상속받은 사용자 정의 위젯입니다:

class InformationWindow : public QDialog
{
    Q_OBJECT
public:
    InformationWindow(int id, QSqlRelationalTableModel *items,
                      QWidget *parent = nullptr);
    int id() const;

Q_SIGNALS:
    void imageChanged(int id, const QString &fileName);

정보 창을 생성할 때, 관련 항목 ID, 모델 포인터, 그리고 부모 객체를 생성자에 전달합니다. 모델 포인터를 사용하여 창에 데이터를 채우고, 부모 매개변수는 기본 클래스로 전달합니다. ID는 나중에 참조할 수 있도록 저장됩니다.

창이 생성되면, 주어진 위치에 대한 정보가 요청될 때마다 public id() 함수를 사용하여 해당 창을 찾아냅니다. 또한 사용자가 변경 사항을 데이터베이스에 제출할 때 ID를 사용하여 메인 애플리케이션 위젯을 업데이트합니다. 즉, 사용자가 연관된 이미지를 변경할 때마다 ID와 파일 이름을 매개변수로 포함하는 신호를 발신합니다.

private Q_SLOTS:
    void revert();
    void submit();
    void enableButtons(bool enable);

사용자가 일부 데이터를 수정할 수 있도록 허용하므로, 변경 내용을 되돌리거나 제출할 수 있는 기능을 제공해야 합니다. ` enableButtons() ` 슬롯은 필요에 따라 다양한 버튼을 활성화하거나 비활성화할 수 있도록 편의성을 위해 제공됩니다.

private:
    void createButtons();

    int itemId;
    QString displayedImage;

    QComboBox *imageFileEditor = nullptr;
    QLabel *itemText = nullptr;
    QTextEdit *descriptionEditor = nullptr;

    QPushButton *closeButton = nullptr;
    QPushButton *submitButton = nullptr;
    QPushButton *revertButton = nullptr;
    QDialogButtonBox *buttonBox = nullptr;

    QDataWidgetMapper *mapper = nullptr;
};

createButtons() 함수 역시 생성자를 간소화하기 위해 제공된 편의 함수입니다. 앞서 언급한 바와 같이, 향후 참조를 위해 항목 ID를 저장합니다. 또한 imageChanged() 신호를 언제 발산해야 할지 판단할 수 있도록 현재 표시된 이미지 파일의 이름도 저장합니다.

정보 창은 QLabel 클래스를 사용하여 항목의 이름을 표시합니다. 관련 이미지 파일은 QComboBox 인스턴스를 사용하여 표시되는 반면, 설명은 QTextEdit 을 사용하여 표시됩니다. 또한, 이 창에는 데이터 흐름을 제어하고 창 표시 여부를 결정하는 세 개의 버튼이 있습니다.

마지막으로 매퍼를 선언합니다. ` QDataWidgetMapper ` 클래스는 데이터 모델의 특정 부분과 위젯 간의 매핑을 제공합니다. 이 매퍼를 사용하여 지정된 데이터베이스에서 데이터를 추출하고, 사용자가 데이터를 수정할 때마다 데이터베이스를 업데이트할 것입니다.

InformationWindow 클래스 구현

생성자는 세 가지 인자, 즉 항목 ID, 데이터베이스 포인터, 부모 위젯을 받습니다. 데이터베이스 포인터는 실제로 데이터베이스 테이블에 대해 (외래 키를 지원하는) 편집 가능한 데이터 모델을 제공하는 ` QSqlRelationalTableModel ` 객체에 대한 포인터입니다.

InformationWindow::InformationWindow(int id, QSqlRelationalTableModel *items,
                                     QWidget *parent)
    : QDialog(parent)
{
    QLabel *itemLabel = new QLabel(tr("Item:"));
    QLabel *descriptionLabel = new QLabel(tr("Description:"));
    QLabel *imageFileLabel = new QLabel(tr("Image file:"));

    createButtons();

    itemText = new QLabel;
    descriptionEditor = new QTextEdit;

먼저 데이터베이스에 포함된 데이터를 표시하는 데 필요한 다양한 위젯을 생성합니다. 대부분의 위젯은 간단한 방식으로 생성됩니다. 하지만 이미지 파일 이름을 표시하는 콤보박스에 주목해 보십시오:

    imageFileEditor = new QComboBox;
    imageFileEditor->setModel(items->relationModel(1));
    imageFileEditor->setModelColumn(items->relationModel(1)->fieldIndex("file"));

이 예제에서 항목에 대한 정보는 “items”라는 데이터베이스 테이블에 저장됩니다. 모델을 생성할 때, 외래 키를 사용하여 이 테이블과 사용 가능한 이미지 파일의 이름을 포함하는 두 번째 데이터베이스 테이블인 “images” 간의 관계를 설정할 것입니다. 이 작업이 어떻게 이루어지는지에 대해서는 View 클래스를 검토할 때 다시 다루겠습니다. 이러한 관계를 설정하는 이유는 사용자가 미리 정의된 이미지 파일 중에서만 선택할 수 있도록 보장하기 위함입니다.

"images" 데이터베이스 테이블에 해당하는 모델은 QSqlRelationalTableModel 의 relationModel() 함수를 통해 사용할 수 있으며, 이 함수는 외래 키(이 경우 "imagefile" 열 번호)를 인수로 요구합니다. QComboBox 의 setModel() 함수를 사용하여 콤보박스가 "images" 모델을 사용하도록 설정합니다. 또한, 이 모델에는 두 개의 열("itemid"와 "file")이 있으므로, QComboBox::setModelColumn() 함수를 사용하여 표시할 열을 지정합니다.

    mapper = new QDataWidgetMapper(this);
    mapper->setModel(items);
    mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit);
    mapper->setItemDelegate(new QSqlRelationalDelegate(mapper));
    mapper->addMapping(imageFileEditor, 1);
    mapper->addMapping(itemText, 2, "text");
    mapper->addMapping(descriptionEditor, 3);
    mapper->setCurrentIndex(id);

그런 다음 매퍼를 생성합니다. ` QDataWidgetMapper ` 클래스를 사용하면 위젯을 항목 모델의 섹션에 매핑하여 데이터 인식 위젯을 만들 수 있습니다.

addMapping() 함수는 주어진 위젯과 모델의 지정된 섹션 간의 매핑을 추가합니다. 매퍼의 방향이 가로(기본값)인 경우 섹션은 모델의 열을 의미하며, 그렇지 않은 경우 행을 의미합니다. setCurrentIndex() 함수를 호출하여 주어진 항목 ID와 연관된 데이터로 위젯을 초기화합니다. 현재 인덱스가 변경될 때마다 모든 위젯이 모델의 내용으로 업데이트됩니다.

또한 매퍼의 제출 정책을 ` QDataWidgetMapper::ManualSubmit`로 설정합니다. 이는 사용자가 명시적으로 제출을 요청할 때까지 데이터가 데이터베이스로 전송되지 않음을 의미합니다(대안은 ` QDataWidgetMapper::AutoSubmit`이며, 해당 위젯이 포커스를 잃을 때 변경 사항을 자동으로 제출합니다). 마지막으로, 매퍼 뷰가 항목에 대해 사용해야 할 항목 델리게이트를 지정합니다. ` QSqlRelationalDelegate ` 클래스는 기본 델리게이트와 달리, 다른 테이블로의 외래 키인 필드(예: “items” 테이블의 “imagefile”)에 대해 콤보박스 기능을 활성화하는 델리게이트를 나타냅니다.

    connect(descriptionEditor, &QTextEdit::textChanged, this, [this]() { enableButtons(true); });
    connect(imageFileEditor, &QComboBox::currentIndexChanged, this, [this]() { enableButtons(true); });

    QFormLayout *formLayout = new QFormLayout;
    formLayout->addRow(itemLabel, itemText);
    formLayout->addRow(imageFileLabel, imageFileEditor);
    formLayout->addRow(descriptionLabel, descriptionEditor);

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addLayout(formLayout);
    layout->addWidget(buttonBox);
    setLayout(layout);

    itemId = id;
    displayedImage = imageFileEditor->currentText();

    setWindowFlags(Qt::Window);
    enableButtons(false);
    setWindowTitle(itemText->text());
}

마지막으로, 편집기의 “변경 사항 발생” 신호를 사용자 정의 enableButtons 슬롯에 연결하여 사용자가 변경 사항을 적용하거나 되돌릴 수 있도록 합니다. enableButtons 슬롯을 연결할 때는 람다 표현식을 사용해야 하는데, 이는 해당 슬롯의 시그니처가 QTextEdit::textChanged 및 QComboBox::currentIndexChanged 와 일치하지 않기 때문입니다.

모든 위젯을 레이아웃에 추가하고, 나중에 참조할 수 있도록 항목 ID와 표시된 이미지 파일의 이름을 저장하며, 창 제목과 초기 크기를 설정합니다.

또한, 위젯이 실제로 창 시스템 프레임과 제목 표시줄을 갖춘 창임을 나타내기 위해 Qt::Window 창 플래그도 설정합니다.

int InformationWindow::id() const
{
    return itemId;
}

창이 생성되면 메인 애플리케이션이 종료될 때까지 삭제되지 않습니다(즉, 사용자가 정보 창을 닫아도 창은 숨겨질 뿐입니다). 이러한 이유로 각 항목에 대해 하나 이상의 InformationWindow 객체를 생성해서는 안 되며, 사용자가 특정 위치에 대한 정보를 요청할 때 해당 위치에 대한 창이 이미 존재하는지 여부를 확인할 수 있도록 public id() 함수를 제공합니다.

void InformationWindow::revert()
{
    mapper->revert();
    enableButtons(false);
}

사용자가 Revert 버튼을 누를 때마다 revert() 슬롯이 트리거됩니다.

QDataWidgetMapper::ManualSubmit 의 제출 정책을 설정했기 때문에, 사용자가 모든 변경 사항을 명시적으로 제출하기로 선택하지 않는 한 사용자의 변경 사항은 모델에 반영되지 않습니다. 그럼에도 불구하고, QDataWidgetMapper 의 revert() 슬롯을 사용하여 편집기 위젯을 재설정하고, 모든 위젯을 모델의 현재 데이터로 다시 채울 수 있습니다.

void InformationWindow::submit()
{
    QString newImage(imageFileEditor->currentText());

    if (displayedImage != newImage) {
        displayedImage = newImage;
        emit imageChanged(itemId, newImage);
    }

    mapper->submit();
    mapper->setCurrentIndex(itemId);

    enableButtons(false);
}

마찬가지로, 사용자가 Submit 버튼을 눌러 변경 사항을 제출하기로 결정할 때마다 submit() 슬롯이 트리거됩니다.

우리는 QDataWidgetMapper 의 submit() 슬롯을 사용하여 매핑된 위젯의 모든 변경 사항을 모델, 즉 데이터베이스로 제출합니다. 매핑된 각 섹션에 대해, 항목 델리게이트는 위젯에서 현재 값을 읽어와 모델에 설정합니다. 마지막으로, 모델의` submit()` 함수가 호출되어 모델이 캐시된 모든 데이터를 영구 저장소에 제출해야 함을 알립니다.

데이터를 제출하기 전에, 이전에 저장된 ` displayedImage ` 변수를 참조하여 사용자가 다른 이미지 파일을 선택했는지 확인합니다. 현재 파일 이름과 저장된 파일 이름이 다르면, 새로운 파일 이름을 저장하고 ` imageChanged() ` 신호를 발생시킵니다.

void InformationWindow::createButtons()
{
    closeButton = new QPushButton(tr("&Close"));
    revertButton = new QPushButton(tr("&Revert"));
    submitButton = new QPushButton(tr("&Submit"));

    closeButton->setDefault(true);

    connect(closeButton, &QPushButton::clicked, this, &InformationWindow::close);
    connect(revertButton, &QPushButton::clicked, this, &InformationWindow::revert);
    connect(submitButton, &QPushButton::clicked, this, &InformationWindow::submit);

createButtons() 함수는 편의상, 즉 생성자를 단순화하기 위해 제공됩니다.

Close 버튼을 기본 버튼, 즉 사용자가 Enter 를 눌렀을 때 작동하는 버튼으로 설정하고, 이 버튼의 clicked() 신호를 위젯의 close() 슬롯에 연결합니다. 앞서 언급했듯이 창을 닫으면 위젯이 숨겨질 뿐 삭제되지는 않습니다. 또한 Submit 및 Revert 버튼을 각각 submit() 및 revert() 슬롯에 연결합니다.

    buttonBox = new QDialogButtonBox(this);
    buttonBox->addButton(submitButton, QDialogButtonBox::AcceptRole);
    buttonBox->addButton(revertButton, QDialogButtonBox::ResetRole);
    buttonBox->addButton(closeButton, QDialogButtonBox::RejectRole);
}

QDialogButtonBox 클래스는 현재 위젯 스타일에 적합한 레이아웃으로 버튼을 표시하는 위젯입니다. 정보 창과 같은 대화 상자는 일반적으로 해당 플랫폼의 인터페이스 지침을 따르는 레이아웃으로 버튼을 표시합니다. 플랫폼마다 대화 상자의 레이아웃은 항상 다릅니다. ` QDialogButtonBox `을 사용하면 사용자의 데스크톱 환경에 적합한 레이아웃을 자동으로 적용하여 버튼을 추가할 수 있습니다.

대화 상자의 대부분의 버튼은 특정 역할을 따릅니다. 우리는 ‘ Submit ’ 및 ‘ Revert ’ 버튼에 ‘ reset ’ 역할을 부여합니다. 즉, 이 버튼을 누르면 필드가 기본값(이 경우 데이터베이스에 포함된 정보)으로 재설정됨을 나타냅니다. reject 역할은 버튼을 클릭하면 대화 상자가 취소됨을 나타냅니다. 반면, 정보 창만 숨겨지기 때문에 사용자가 변경한 내용은 사용자가 명시적으로 취소하거나 제출할 때까지 유지됩니다.

void InformationWindow::enableButtons(bool enable)
{
    revertButton->setEnabled(enable);
    submitButton->setEnabled(enable);
}

enableButtons() 슬롯은 사용자가 표시된 데이터를 변경할 때마다 버튼을 활성화하기 위해 호출됩니다. 마찬가지로, 사용자가 변경 사항을 제출하기로 선택하면 버튼이 비활성화되어 현재 데이터가 데이터베이스에 저장되었음을 나타냅니다.

이것으로 ` InformationWindow ` 클래스 구현을 마칩니다. 이제 예제 애플리케이션에서 이 클래스를 어떻게 사용했는지 살펴보겠습니다.

뷰 클래스 정의

View 클래스는 메인 애플리케이션 창을 나타내며, QGraphicsView 를 상속받습니다:

class View : public QGraphicsView
{
    Q_OBJECT
public:
    View(const QString &items, const QString &images, QWidget *parent = nullptr);

protected:
    void mouseReleaseEvent(QMouseEvent *event) override;

private Q_SLOTS:
    void updateImage(int id, const QString &fileName);

QGraphicsView 클래스는 이미지를 표시하는 데 사용할 Graphics View Framework의 일부입니다. 이미지를 클릭했을 때 적절한 정보 창을 표시하여 사용자 상호작용에 대응할 수 있도록, QGraphicsView 의 mouseReleaseEvent() 함수를 재구현합니다.

생성자는 두 개의 데이터베이스 테이블 이름을 매개변수로 받습니다. 하나는 항목에 대한 상세 정보를 포함하고, 다른 하나는 사용 가능한 이미지 파일의 이름을 포함합니다. 또한 사용자가 항목과 연결된 이미지를 변경할 때마다 발생되는 InformationWindow 의 imageChanged() 신호를 수신하기 위해 비공개 updateImage() 슬롯을 제공합니다.

private:
    void addItems();
    InformationWindow *findWindow(int id) const;
    void showInformation(ImageItem *image);

    QGraphicsScene *scene;
    QList<InformationWindow *> informationWindows;

addItems() 함수는 생성자 사용을 간소화하기 위해 제공되는 편의 함수입니다. 이 함수는 단 한 번만 호출되며, 다양한 항목을 생성하여 뷰에 추가합니다.

반면, ` findWindow() ` 함수는 자주 사용됩니다. 이 함수는 ` showInformation() ` 함수에서 호출되어 주어진 항목에 대해 이미 창이 생성되었는지 확인합니다( InformationWindow 객체를 생성할 때마다 해당 객체에 대한 참조를 ` informationWindows ` 목록에 저장합니다). 후자의 함수는 다시 우리가 직접 구현한 ` mouseReleaseEvent() `에서 호출됩니다.

    QSqlRelationalTableModel *itemTable;
};

마지막으로, ` QSqlRelationalTableModel ` 포인터를 선언합니다. 앞서 언급했듯이, ` QSqlRelationalTableModel ` 클래스는 외래 키를 지원하는 편집 가능한 데이터 모델을 제공합니다. ` QSqlRelationalTableModel ` 클래스를 사용할 때 유의해야 할 몇 가지 사항이 있습니다. 테이블에는 기본 키가 선언되어 있어야 하며, 이 키는 다른 테이블과의 관계를 포함할 수 없습니다. 즉, 외래 키일 수 없습니다. 또한, 관계형 테이블에 참조 대상 테이블에 존재하지 않는 행을 참조하는 키가 포함된 경우, 해당 무효한 키를 포함하는 행은 모델을 통해 노출되지 않는다는 점에 유의하십시오. 참조 무결성을 유지하는 것은 사용자나 데이터베이스의 책임입니다.

뷰 클래스 구현

생성자는 사무실 세부 정보가 포함된 테이블과 사용 가능한 이미지 파일 이름이 포함된 테이블의 이름을 모두 요청하지만, 우리는 “items” 테이블에 대해서만 QSqlRelationalTableModel 객체를 생성하면 됩니다:

View::View(const QString &items, const QString &images, QWidget *parent)
    : QGraphicsView(parent)
{
    itemTable = new QSqlRelationalTableModel(this);
    itemTable->setTable(items);
    itemTable->setRelation(1, QSqlRelation(images, "itemid", "file"));
    itemTable->select();

그 이유는 항목 세부 정보를 가진 모델이 생성되면, ` QSqlRelationalTableModel`의 ` setRelation()` 메서드를 사용하여 사용 가능한 이미지 파일과의 관계를 생성할 수 있기 때문입니다. 이 메서드는 지정된 모델 열에 대한 외래 키를 생성합니다. 키는 키가 참조하는 테이블의 이름, 키가 매핑되는 필드, 그리고 사용자에게 표시될 필드로 구성된 제공된 ` QSqlRelation ` 객체를 통해 지정됩니다.

테이블을 설정하는 것은 모델이 어떤 테이블에서 작동하는지만 지정할 뿐이라는 점에 유의하십시오. 즉, 모델을 채우려면 모델의 ` select()` 함수를 명시적으로 호출해야 합니다.

    scene = new QGraphicsScene(this);
    scene->setSceneRect(0, 0, 465, 365);
    setScene(scene);

    addItems();

    setMinimumSize(470, 370);
    setMaximumSize(470, 370);

    QLinearGradient gradient(QPointF(0, 0), QPointF(0, 370));
    gradient.setColorAt(0, QColor("#868482"));
    gradient.setColorAt(1, QColor("#5d5b59"));
    setBackgroundBrush(gradient);
}

그런 다음 뷰의 내용, 즉 장면과 그 항목들을 생성합니다. 레이블은 일반적인 QGraphicsTextItem 객체인 반면, 이미지는 QGraphicsPixmapItem 에서 파생된 ImageItem 클래스의 인스턴스입니다. 이 부분에 대해서는 addItems() 함수를 살펴볼 때 잠시 후에 다시 다루겠습니다.

마지막으로, 메인 애플리케이션 위젯의 크기 제약 조건과 창 제목을 설정합니다.

void View::addItems()
{
    int itemCount = itemTable->rowCount();

    int imageOffset = 150;
    int leftMargin = 70;
    int topMargin = 40;

    for (int i = 0; i < itemCount; i++) {
        QSqlRecord record = itemTable->record(i);

        int id = record.value("id").toInt();
        QString file = record.value("file").toString();
        QString item = record.value("itemtype").toString();

        int columnOffset = ((i % 2) * 37);
        int x = ((i % 2) * imageOffset) + leftMargin + columnOffset;
        int y = ((i / 2) * imageOffset) + topMargin;

        ImageItem *image = new ImageItem(id, QPixmap(":/" + file));
        image->setData(0, i);
        image->setPos(x, y);
        scene->addItem(image);

        QGraphicsTextItem *label = scene->addText(item);
        label->setDefaultTextColor(QColor("#d7d6d5"));
        QPointF labelOffset((120 - label->boundingRect().width()) / 2, 120.0);
        label->setPos(QPointF(x, y) + labelOffset);
    }
}

addItems() 함수는 메인 애플리케이션 창을 생성할 때 단 한 번만 호출됩니다. 데이터베이스 테이블의 각 행에 대해, 먼저 모델의 record() 함수를 사용하여 해당 레코드를 추출합니다. QSqlRecord 클래스는 데이터베이스 레코드의 기능과 특성을 모두 캡슐화하며, 필드 추가 및 제거는 물론 필드 값 설정 및 조회도 지원합니다. QSqlRecord::value() 함수는 주어진 이름이나 인덱스를 가진 필드의 값을 ` QVariant ` 객체로 반환합니다.

각 레코드에 대해 레이블 아이템과 이미지 아이템을 생성하고, 위치를 계산한 후 씬에 추가합니다. 이미지 아이템은 ` ImageItem ` 클래스의 인스턴스로 표현됩니다. 사용자 정의 아이템 클래스를 생성해야 하는 이유는 아이템의 호버 이벤트를 포착하여 마우스 커서가 이미지 위에 있을 때 아이템에 애니메이션을 적용하기 위함입니다(기본적으로 어떤 아이템도 호버 이벤트를 수신하지 않습니다). 자세한 내용은 Graphics View Framework 문서와 Graphics View 예제를 참조하십시오.

void View::mouseReleaseEvent(QMouseEvent *event)
{
    if (QGraphicsItem *item = itemAt(event->position().toPoint())) {
        if (ImageItem *image = qgraphicsitem_cast<ImageItem *>(item))
            showInformation(image);
    }
    QGraphicsView::mouseReleaseEvent(event);
}

사용자 상호작용에 대응하기 위해 ` QGraphicsView`의 ` mouseReleaseEvent()` 이벤트 핸들러를 재구현합니다. 사용자가 이미지 항목 중 하나를 클릭하면, 이 함수는 비공개 함수 ` showInformation() `를 호출하여 관련 정보 창을 표시합니다.

Graphics View Framework는 주어진 QGraphicsItem 인스턴스가 특정 유형인지 확인하기 위해 qgraphicsitem_cast() 함수를 제공합니다. 이벤트가 우리의 이미지 항목 중 어느 것과도 관련이 없는 경우, 해당 이벤트를 기본 클래스 구현으로 전달한다는 점에 유의하십시오.

void View::showInformation(ImageItem *image)
{
    int id = image->id();
    if (id < 0 || id >= itemTable->rowCount())
        return;

    InformationWindow *window = findWindow(id);
    if (!window) {
        window = new InformationWindow(id, itemTable, this);

        connect(window, &InformationWindow::imageChanged,
                this, &View::updateImage);

        window->move(pos() + QPoint(20, 40));
        window->show();
        informationWindows.append(window);
    }

    if (window->isVisible()) {
        window->raise();
        window->activateWindow();
    } else
        window->show();
}

showInformation() 함수는 ImageItem 객체를 인수로 받아, 먼저 해당 항목의 항목 ID를 추출합니다.

그런 다음 해당 위치에 대한 정보 창이 이미 생성되었는지 확인합니다. 지정된 위치에 대한 창이 존재하지 않으면, 항목 ID, 모델에 대한 포인터, 그리고 부모로 지정된 뷰를 ` InformationWindow ` 생성자에 전달하여 창을 생성합니다. 정보 창( imageChanged() )의 신호를 이 위젯의 updateImage() 슬롯에 연결한 후, 적절한 위치를 지정하고 기존 창 목록에 추가합니다. 지정된 위치에 창이 있고 해당 창이 표시되어 있다면, 이 메서드는 창이 위젯 스택의 최상단으로 올라와 활성화되도록 합니다. 창이 숨겨져 있는 경우, 해당 창에 대한 ` show()` 슬롯을 호출하면 동일한 결과를 얻을 수 있습니다.

void View::updateImage(int id, const QString &fileName)
{
    QList<QGraphicsItem *> items = scene->items();

    while(!items.empty()) {
        QGraphicsItem *item = items.takeFirst();

        if (ImageItem *image = qgraphicsitem_cast<ImageItem *>(item)) {
            if (image->id() == id){
                image->setPixmap(QPixmap(":/" +fileName));
                image->adjust();
                break;
            }
        }
    }
}

updateImage() 슬롯은 항목 ID와 이미지 파일 이름을 인자로 받습니다. 이 슬롯은 이미지 항목을 필터링하여, 주어진 항목 ID에 해당하는 이미지를 제공된 이미지 파일로 업데이트합니다.

InformationWindow *View::findWindow(int id) const
{
    for (auto window : informationWindows) {
        if (window && (window->id() == id))
            return window;
    }
    return nullptr;
}

findWindow() 함수는 단순히 기존 창 목록을 검색하여, 주어진 항목 ID와 일치하는 창에 대한 포인터를 반환하거나, 해당 창이 존재하지 않을 경우 ` nullptr `를 반환합니다.

마지막으로, 사용자 정의 ImageItem 클래스를 간단히 살펴보겠습니다:

ImageItem 클래스 정의

ImageItem 클래스는 이미지 항목의 애니메이션을 용이하게 하기 위해 제공됩니다. 이 클래스는 QGraphicsPixmapItem 를 상속받으며, 해당 클래스의 호버 이벤트 핸들러를 재구현합니다:

class ImageItem : public QObject, public QGraphicsPixmapItem
{
    Q_OBJECT
public:
    enum { Type = UserType + 1 };

    ImageItem(int id, const QPixmap &pixmap, QGraphicsItem *parent = nullptr);

    int type() const override { return Type; }
    void adjust();
    int id() const;

protected:
    void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
    void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;

private Q_SLOTS:
    void setFrame(int frame);
    void updateItemPosition();

private:
    QTimeLine timeLine;
    int recordId;
    double z;
};

사용자 정의 항목을 위해 ` Type ` 열거형 값을 선언하고 ` type()`를 재구현합니다. 이는 ` qgraphicsitem_cast()`를 안전하게 사용할 수 있도록 하기 위함입니다. 또한, 연관된 위치를 식별할 수 있도록 ` id() ` 공용 함수를 구현하고, 원본 이미지 파일과 관계없이 이미지 항목에 선호하는 크기가 적용되도록 보장하기 위해 호출할 수 있는 ` adjust() ` 공용 함수를 구현합니다.

애니메이션은 QTimeLine 클래스와 이벤트 핸들러, 그리고 비공개 setFrame() 슬롯을 사용하여 구현됩니다. 마우스 커서가 이미지 항목 위로 호버하면 항목이 확장되고, 커서가 항목의 경계를 벗어나면 원래 크기로 돌아갑니다.

마지막으로, 이 특정 레코드와 연관된 항목 ID와 z-값을 저장합니다. 그래픽스 뷰 프레임워크에서 항목의 z-값은 항목 스택 내에서의 위치를 결정합니다. 동일한 부모 항목을 공유하는 경우, z-값이 높은 항목은 z-값이 낮은 항목 위에 그려집니다. 또한 필요할 때 뷰를 새로 고칠 수 있도록 ` updateItemPosition() ` 함수를 제공합니다.

ImageItem 클래스 구현

ImageItem 클래스는 사실 몇 가지 추가 기능이 포함된 QGraphicsPixmapItem 에 불과합니다. 즉, 생성자의 인자 대부분(픽스맵, 부모, 씬)을 기본 클래스 생성자로 전달할 수 있습니다:

ImageItem::ImageItem(int id, const QPixmap &pixmap, QGraphicsItem *parent)
    : QGraphicsPixmapItem(pixmap, parent)
{
    recordId = id;
    setAcceptHoverEvents(true);

    timeLine.setDuration(150);
    timeLine.setFrameRange(0, 150);

    connect(&timeLine, &QTimeLine::frameChanged, this, &ImageItem::setFrame);
    connect(&timeLine, &QTimeLine::finished, this, &ImageItem::updateItemPosition);

    adjust();
}

그런 다음 나중에 참조할 수 있도록 ID를 저장하고, 이미지 항목이 호버 이벤트를 수신할 수 있도록 설정합니다. 호버 이벤트는 현재 마우스 그라버 항목이 없을 때 전달됩니다. 이 이벤트는 마우스 커서가 항목에 들어갈 때, 항목 내부에서 이동할 때, 그리고 커서가 항목을 떠날 때 전송됩니다. 앞서 언급했듯이, Graphics View Framework의 어떤 항목도 기본적으로 호버 이벤트를 수신하지 않습니다.

QTimeLine 클래스는 애니메이션을 제어하기 위한 타임라인을 제공합니다. 이 클래스의 ` duration ` 속성은 타임라인의 총 지속 시간을 밀리초 단위로 저장합니다. 기본적으로 타임라인은 시작부터 끝까지 한 번 실행됩니다. ` QTimeLine::setFrameRange()` 함수는 타임라인의 프레임 카운터를 설정하며, 타임라인이 실행 중일 때 프레임이 바뀔 때마다 ` frameChanged()` 신호가 발생합니다. 애니메이션의 지속 시간과 프레임 범위를 설정하고, 타임라인의 ` frameChanged()` 및 ` finished()` 신호를 비공개 슬롯인 ` setFrame() ` 및 ` updateItemPosition() `에 연결합니다.

마지막으로, adjust() 를 호출하여 항목이 선호하는 크기를 갖도록 합니다.

void ImageItem::hoverEnterEvent(QGraphicsSceneHoverEvent * /*event*/)
{
    timeLine.setDirection(QTimeLine::Forward);

    if (z != 1.0) {
        z = 1.0;
        updateItemPosition();
    }

    if (timeLine.state() == QTimeLine::NotRunning)
        timeLine.start();
}

void ImageItem::hoverLeaveEvent(QGraphicsSceneHoverEvent * /*event*/)
{
    timeLine.setDirection(QTimeLine::Backward);
    if (z != 0.0)
        z = 0.0;

    if (timeLine.state() == QTimeLine::NotRunning)
        timeLine.start();
}

마우스 커서가 이미지 항목에 진입하거나 이탈할 때마다 해당 이벤트 핸들러가 트리거됩니다. 먼저 타임라인의 방향을 설정하여 항목을 각각 확장하거나 축소시킵니다. 그런 다음 항목의 z-값이 예상된 값으로 설정되어 있지 않은 경우 이를 변경합니다.

호버 진입 이벤트의 경우, 항목이 확장되기 시작하는 즉시 다른 모든 항목 위에 표시되도록 하기 위해 항목의 위치를 즉시 업데이트합니다. 반면, 호버 이탈 이벤트의 경우 동일한 결과를 얻기 위해 실제 업데이트를 잠시 미룹니다. 하지만 항목을 생성할 때 타임라인의 ` finished()` 신호를 ` updateItemPosition() ` 슬롯에 연결했다는 점을 기억해야 합니다. 이렇게 하면 애니메이션이 완료되는 즉시 항목이 항목 스택 내에서 올바른 위치를 차지하게 됩니다. 마지막으로, 타임라인이 아직 실행 중이 아니라면 이를 시작합니다.

void ImageItem::setFrame(int frame)
{
    adjust();
    QPointF center = boundingRect().center();

    setTransform(QTransform::fromTranslate(center.x(), center.y()), true);
    setTransform(QTransform::fromScale(1 + frame / 300.0, 1 + frame / 300.0), true);
    setTransform(QTransform::fromTranslate(-center.x(), -center.y()), true);
}

타임라인이 실행 중일 때, 아이템 생성자에서 생성한 연결 덕분에 현재 프레임이 변경될 때마다 ` setFrame() ` 슬롯이 트리거됩니다. 바로 이 슬롯이 애니메이션을 제어하여 이미지 아이템을 단계적으로 확대하거나 축소합니다.

먼저 adjust() 함수를 호출하여 아이템이 원래 크기로 시작하도록 합니다. 그런 다음 애니메이션 진행 상황에 따라 ( frame 매개변수를 사용하여) 아이템의 크기를 조정합니다. 기본적으로 변형은 항목의 왼쪽 상단 모서리를 기준으로 이루어진다는 점에 유의하십시오. 항목을 중심을 기준으로 변형하고 싶으므로, 항목의 크기를 조정하기 전에 좌표계를 변환해야 합니다.

결국 다음과 같은 편의 함수들만 남게 됩니다:

void ImageItem::adjust()
{
    setTransform(QTransform::fromScale(120.0 / boundingRect().width(),
                                       120.0 / boundingRect().height()));
}

int ImageItem::id() const
{
    return recordId;
}

void ImageItem::updateItemPosition()
{
    setZValue(z);
}

adjust() 함수는 변환 행렬을 정의하고 적용하여, 원본 이미지의 크기와 관계없이 이미지 항목이 원하는 크기로 표시되도록 보장합니다. id() 함수는 매우 간단하며, 단순히 항목을 식별할 수 있도록 제공된 것입니다. updateItemPosition() 슬롯에서는 QGraphicsItem::setZValue() 함수를 호출하여 항목의 높이를 설정합니다.

예제 프로젝트 @ code.qt.io

© 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.