드릴다운 예제

드릴다운 예제에서는 QSqlRelationalTableModelQDataWidgetMapper 클래스를 사용하여 데이터베이스에서 데이터를 읽고 변경 사항을 제출하는 방법을 보여 줍니다.

드릴다운 예제 스크린샷

예제 애플리케이션을 실행할 때 사용자는 해당 이미지를 클릭하여 각 항목에 대한 정보를 검색할 수 있습니다. 애플리케이션은 데이터를 표시하는 정보 창을 띄우고 사용자가 이미지뿐만 아니라 설명도 변경할 수 있도록 합니다. 사용자가 변경 사항을 제출하면 기본 보기가 업데이트됩니다.

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

  • ImageItem 는 이미지를 표시하는 데 사용되는 사용자 지정 그래픽 항목 클래스입니다.
  • View 는 사용자가 다양한 항목을 탐색할 수 있는 기본 애플리케이션 위젯입니다.
  • InformationWindow 는 요청된 정보를 표시하여 사용자가 정보를 변경하고 변경 사항을 데이터베이스에 제출할 수 있도록 합니다.

먼저 InformationWindow 클래스를 살펴보고 데이터베이스에서 데이터를 읽고 수정하는 방법을 알아보겠습니다. 그런 다음 기본 애플리케이션 위젯, 즉 View 클래스와 관련 ImageItem 클래스를 살펴보겠습니다.

정보창 클래스 정의

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는 나중에 참조할 수 있도록 저장됩니다.

창이 생성되면 지정된 위치에 대한 정보가 요청될 때마다 공개 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 클래스는 데이터 모델의 섹션과 위젯 간의 매핑을 제공합니다. 매퍼를 사용해 주어진 데이터베이스에서 데이터를 추출하고, 사용자가 데이터를 수정할 때마다 데이터베이스를 업데이트합니다.

정보창 클래스 구현

생성자는 항목 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" 데이터베이스 테이블에 해당하는 모델은 QSqlRelationalTableModelrelationModel() 함수를 통해 사용할 수 있으며, 외래 키(이 경우 "imagefile" 열 번호)를 인수로 필요로 합니다. QComboBoxsetModel() 함수를 사용하여 콤보박스가 "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::textChangedQComboBox::currentIndexChanged 과 일치하지 않기 때문에 슬롯을 연결할 때 람다를 사용해야 합니다.

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

또한 Qt::Window 창 플래그를 설정하여 위젯이 실제로 창 시스템 프레임과 제목 표시줄이 있는 창임을 나타냅니다.

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

창이 생성되면 기본 애플리케이션이 종료될 때까지 삭제되지 않습니다(즉, 사용자가 정보 창을 닫으면 숨겨지기만 합니다). 이러한 이유로 각 항목에 대해 InformationWindow 개체를 두 개 이상 만들지 않으며, 사용자가 특정 위치에 대한 정보를 요청할 때 창이 이미 존재하는지 여부를 확인할 수 있도록 공개 id() 함수를 제공합니다.

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

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

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

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 버튼을 눌러 변경 사항을 제출하기로 결정할 때마다 트리거됩니다.

QDataWidgetMappersubmit() 슬롯을 사용하여 매핑된 위젯의 모든 변경 사항을 모델, 즉 데이터베이스에 제출합니다. 그러면 매핑된 모든 섹션에 대해 항목 델리게이트가 위젯에서 현재 값을 읽고 모델에 설정합니다. 마지막으로 모델의 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() 슬롯에 연결합니다. 위에서 언급했듯이 창을 닫으면 위젯이 숨겨질 뿐 삭제되지 않습니다. 또한 SubmitRevert 버튼을 해당 submit()revert() 슬롯에 연결합니다.

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

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

대화 상자의 대부분의 버튼은 특정 역할을 따릅니다. SubmitRevert 버튼에는 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 클래스는 이미지를 표시하는 데 사용할 그래픽스 뷰 프레임워크의 일부입니다. 이미지를 클릭하면 적절한 정보 창을 표시하여 사용자 상호 작용에 응답할 수 있도록 QGraphicsViewmouseReleaseEvent() 함수를 다시 구현합니다.

생성자에는 두 개의 데이터베이스 테이블 이름이 필요합니다: 하나는 항목에 대한 자세한 정보가 포함된 테이블이고 다른 하나는 사용 가능한 이미지 파일의 이름이 포함된 테이블입니다. 또한 사용자가 항목과 관련된 이미지를 변경할 때마다 발생하는 InformationWindowimageChanged() 신호를 포착하기 위해 비공개 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 클래스를 사용할 때 염두에 두어야 할 몇 가지 사항이 있습니다: 테이블에 기본 키가 선언되어 있어야 하며 이 키는 다른 테이블과의 관계를 포함할 수 없습니다. 즉, 외래 키가 될 수 없습니다. 또한 관계형 테이블에 참조 테이블에 존재하지 않는 행을 참조하는 키가 포함된 경우 유효하지 않은 키가 포함된 행은 모델을 통해 노출되지 않는다는 점에 유의하세요. 참조 무결성을 유지하는 것은 사용자 또는 데이터베이스의 책임입니다.

뷰 클래스 구현

생성자는 사무실 세부 정보가 포함된 테이블과 사용 가능한 이미지 파일 이름이 포함된 테이블의 이름을 모두 요청하지만, '항목' 테이블에 대해서는 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();

그 이유는 항목 세부 정보가 포함된 모델이 있으면 QSqlRelationalTableModelsetRelation() 함수를 사용하여 사용 가능한 이미지 파일에 대한 관계를 만들 수 있기 때문입니다. 이 함수는 지정된 모델 열에 대한 외래 키를 만듭니다. 키는 키가 참조하는 테이블의 이름, 키가 매핑되는 필드 및 사용자에게 표시되어야 하는 필드로 구성된 제공된 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 클래스의 인스턴스로 표시됩니다. 사용자 지정 항목 클래스를 만들어야 하는 이유는 마우스 커서가 이미지 위에 있을 때 항목에 애니메이션을 적용하여 항목의 호버 이벤트를 포착하기 위해서입니다(기본적으로 어떤 항목도 호버 이벤트를 허용하지 않음). 자세한 내용은 그래픽 보기 프레임워크 문서 및 그래픽 보기 예시를 참조하세요.

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

QGraphicsViewmouseReleaseEvent() 이벤트 핸들러를 다시 구현하여 사용자 상호작용에 응답합니다. 사용자가 이미지 항목을 클릭하면 이 함수는 비공개 showInformation() 함수를 호출하여 관련 정보 창을 팝업으로 표시합니다.

그래픽 보기 프레임워크는 qgraphicsitem_cast() 함수를 제공하여 지정된 QGraphicsItem 인스턴스가 지정된 유형인지 여부를 확인합니다. 이벤트가 이미지 항목과 관련이 없는 경우 기본 클래스 구현에 전달합니다.

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를 추출하는 것으로 시작합니다.

그런 다음 이 위치에 대한 정보 창이 이미 생성되어 있는지 확인합니다. 지정된 위치에 대한 창이 존재하지 않으면 InformationWindow 생성자에 항목 ID, 모델에 대한 포인터 및 뷰를 부모로 전달하여 창을 생성합니다. 정보 창의 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 클래스는 이미지 항목의 애니메이션을 용이하게 하기 위해 제공됩니다. 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() 함수를 구현합니다.

애니메이션은 이벤트 핸들러 및 비공개 setFrame() 슬롯과 함께 QTimeLine 클래스를 사용하여 구현됩니다: 마우스 커서가 이미지 항목 위로 이동하면 이미지 항목이 확장되고 커서가 테두리를 벗어나면 원래 크기로 돌아갑니다.

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

이미지 항목 클래스 구현

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를 저장하고 이미지 항목이 마우스오버 이벤트를 받도록 합니다. 현재 마우스 그래버 항목이 없을 때 호버 이벤트가 전달됩니다. 마우스 커서가 항목에 들어갈 때, 항목 내부를 이동할 때, 커서가 항목에서 벗어날 때 이벤트가 전송됩니다. 앞서 언급했듯이 그래픽 보기 프레임워크의 어떤 항목도 기본적으로 호버 이벤트를 허용하지 않습니다.

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

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