Drill-Down-Beispiel

Das Drill-Down-Beispiel zeigt, wie man mit den Klassen QSqlRelationalTableModel und QDataWidgetMapper Daten aus einer Datenbank lesen und Änderungen übermitteln kann.

Screenshot des Drill-Down-Beispiels

Wenn die Beispielanwendung ausgeführt wird, kann ein Benutzer Informationen über jedes Element abrufen, indem er auf das entsprechende Bild klickt. Die Anwendung öffnet ein Informationsfenster, in dem die Daten angezeigt werden, und ermöglicht es dem Benutzer, die Beschreibung und das Bild zu ändern. Die Hauptansicht wird aktualisiert, wenn der Benutzer seine Änderungen eingibt.

Das Beispiel besteht aus drei Klassen:

  • ImageItem ist eine benutzerdefinierte Grafikelementklasse, die zur Anzeige der Bilder verwendet wird.
  • View ist das Hauptwidget der Anwendung, mit dem der Benutzer durch die verschiedenen Elemente blättern kann.
  • InformationWindow zeigt die angeforderten Informationen an und ermöglicht es den Benutzern, sie zu ändern und ihre Änderungen an die Datenbank zu übermitteln.

Wir werden zunächst einen Blick auf die Klasse InformationWindow werfen, um zu sehen, wie man Daten aus einer Datenbank lesen und ändern kann. Danach werden wir das Hauptanwendungs-Widget, d. h. die Klasse View, und die zugehörige Klasse ImageItem betrachten.

Definition der Klasse InformationWindow

Die Klasse InformationWindow ist ein benutzerdefiniertes Widget, das von QWidget erbt:

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

Wenn wir ein Informationsfenster erstellen, übergeben wir die zugehörige Element-ID, einen Zeiger auf das Modell und ein Elternteil an den Konstruktor. Wir verwenden den Modellzeiger, um unser Fenster mit Daten zu füllen, während wir den übergeordneten Parameter an die Basisklasse weitergeben. Die ID wird für zukünftige Referenzen gespeichert.

Sobald ein Fenster erstellt ist, werden wir die öffentliche Funktion id() verwenden, um es zu finden, wenn Informationen für den angegebenen Ort angefordert werden. Wir verwenden die ID auch, um das Hauptwidget der Anwendung zu aktualisieren, wenn die Benutzer ihre Änderungen an die Datenbank übermitteln, d. h. wir senden ein Signal mit der ID und dem Dateinamen als Parameter, wenn der Benutzer das zugehörige Bild ändert.

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

Da wir den Benutzern die Möglichkeit geben, einige der Daten zu ändern, müssen wir Funktionen für die Rückgängigmachung und Übermittlung ihrer Änderungen bereitstellen. Der Slot enableButtons() dient dazu, die verschiedenen Schaltflächen bei Bedarf zu aktivieren und zu deaktivieren.

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

Die Funktion createButtons() ist ebenfalls eine Komfortfunktion, die zur Vereinfachung des Konstruktors bereitgestellt wird. Wie bereits erwähnt, speichern wir die Element-ID für zukünftige Referenzen. Wir speichern auch den Namen der aktuell angezeigten Bilddatei, um feststellen zu können, wann das Signal imageChanged() ausgegeben werden soll.

Das Informationsfenster verwendet die Klasse QLabel, um den Namen eines Eintrags anzuzeigen. Die zugehörige Bilddatei wird mit einer QComboBox -Instanz angezeigt, während die Beschreibung mit QTextEdit angezeigt wird. Darüber hinaus verfügt das Fenster über drei Schaltflächen, mit denen der Datenfluss gesteuert und festgelegt werden kann, ob das Fenster angezeigt wird oder nicht.

Schließlich deklarieren wir einen Mapper. Die Klasse QDataWidgetMapper bietet eine Abbildung zwischen einem Abschnitt eines Datenmodells und Widgets. Wir werden den Mapper verwenden, um Daten aus der gegebenen Datenbank zu extrahieren und die Datenbank zu aktualisieren, wenn der Benutzer die Daten ändert.

Implementierung der Klasse InformationWindow

Der Konstruktor nimmt drei Argumente entgegen: eine Element-ID, einen Datenbankzeiger und ein übergeordnetes Widget. Der Datenbankzeiger ist eigentlich ein Zeiger auf ein QSqlRelationalTableModel Objekt, das ein editierbares Datenmodell (mit Fremdschlüsselunterstützung) für unsere Datenbanktabelle bereitstellt.

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;

Zunächst erstellen wir die verschiedenen Widgets, die zur Anzeige der in der Datenbank enthaltenen Daten erforderlich sind. Die meisten Widgets werden auf einfache Art und Weise erstellt. Beachten Sie jedoch die Combobox, die den Namen der Bilddatei anzeigt:

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

In diesem Beispiel werden die Informationen über die Artikel in einer Datenbanktabelle namens "items" gespeichert. Bei der Erstellung des Modells werden wir einen Fremdschlüssel verwenden, um eine Beziehung zwischen dieser Tabelle und einer zweiten Datenbanktabelle, "images", herzustellen, die die Namen der verfügbaren Bilddateien enthält. Wir werden bei der Überprüfung der Klasse View darauf zurückkommen, wie das gemacht wird. Der Grund für die Erstellung einer solchen Beziehung ist jedoch, dass wir sicherstellen wollen, dass der Benutzer nur zwischen vordefinierten Bilddateien wählen kann.

Das Modell, das der Datenbanktabelle "images" entspricht, ist über die Funktion relationModel() von QSqlRelationalTableModel verfügbar, die den Fremdschlüssel (in diesem Fall die Spaltennummer "imagefile") als Argument benötigt. Wir verwenden die Funktion setModel() von QComboBox, damit die Combobox das Modell "images" verwendet. Und da dieses Modell zwei Spalten hat ("itemid" und "file"), geben wir mit der Funktion QComboBox::setModelColumn() auch an, welche Spalte sichtbar sein soll.

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

Dann erstellen wir den Mapper. Mit der Klasse QDataWidgetMapper können wir datengesteuerte Widgets erstellen, indem wir sie auf Abschnitte eines Elementmodells abbilden.

Die Funktion addMapping() fügt eine Zuordnung zwischen dem angegebenen Widget und dem angegebenen Abschnitt des Modells hinzu. Wenn die Ausrichtung des Mappers horizontal ist (Standard), ist der Abschnitt eine Spalte im Modell, andernfalls ist es eine Zeile. Wir rufen die Funktion setCurrentIndex() auf, um die Widgets mit den Daten zu initialisieren, die mit der angegebenen Element-ID verbunden sind. Jedes Mal, wenn sich der aktuelle Index ändert, werden alle Widgets mit den Inhalten aus dem Modell aktualisiert.

Wir setzen auch die Übermittlungsrichtlinie des Mappers auf QDataWidgetMapper::ManualSubmit. Das bedeutet, dass keine Daten an die Datenbank übermittelt werden, bis der Benutzer explizit eine Übermittlung anfordert (die Alternative ist QDataWidgetMapper::AutoSubmit, wobei Änderungen automatisch übermittelt werden, wenn das entsprechende Widget den Fokus verliert). Schließlich legen wir den Elementdelegaten fest, den die Mapper-Ansicht für ihre Elemente verwenden soll. Die Klasse QSqlRelationalDelegate stellt einen Delegaten dar, der im Gegensatz zum Standarddelegaten die Combobox-Funktionalität für Felder ermöglicht, die Fremdschlüssel in anderen Tabellen sind (wie "imagefile" in unserer Tabelle "items").

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

Schließlich verbinden wir die "Etwas hat sich geändert"-Signale in den Editoren mit unserem benutzerdefinierten enableButtons Slot, der es den Benutzern ermöglicht, ihre Änderungen entweder zu übermitteln oder zu verwerfen. Für die Verbindung des Slots enableButtons müssen wir Lambdas verwenden, da seine Signatur nicht mit QTextEdit::textChanged und QComboBox::currentIndexChanged übereinstimmt.

Wir fügen alle Widgets in ein Layout ein, speichern die Element-ID und den Namen der angezeigten Bilddatei für künftige Referenzen und legen den Fenstertitel und die Anfangsgröße fest.

Beachten Sie, dass wir auch das Qt::Window window flag setzen, um anzuzeigen, dass unser Widget tatsächlich ein Fenster ist, mit einem Fenstersystemrahmen und einer Titelleiste.

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

Wenn ein Fenster erstellt wird, wird es erst gelöscht, wenn die Hauptanwendung beendet wird (d. h. wenn der Benutzer das Informationsfenster schließt, wird es nur ausgeblendet). Aus diesem Grund wollen wir nicht mehr als ein InformationWindow Objekt für jedes Element erstellen, und wir stellen die öffentliche Funktion id() zur Verfügung, um feststellen zu können, ob für einen bestimmten Ort bereits ein Fenster existiert, wenn der Benutzer Informationen darüber anfordert.

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

Der Slot revert() wird immer dann ausgelöst, wenn der Benutzer die Schaltfläche Revert drückt.

Da wir die QDataWidgetMapper::ManualSubmit submit policy festgelegt haben, wird keine der Änderungen des Benutzers in das Modell zurückgeschrieben, es sei denn, der Benutzer entscheidet sich ausdrücklich dafür, alle Änderungen zu übertragen. Nichtsdestotrotz können wir den QDataWidgetMapper's revert() Slot verwenden, um die Editor-Widgets zurückzusetzen und alle Widgets mit den aktuellen Daten des Modells neu zu füllen.

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

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

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

    enableButtons(false);
}

Ebenso wird der Slot submit() immer dann ausgelöst, wenn die Benutzer beschließen, ihre Änderungen durch Drücken der Schaltfläche Submit zu übermitteln.

Wir verwenden QDataWidgetMapper's submit() Slot, um alle Änderungen von den zugeordneten Widgets an das Modell, d.h. an die Datenbank, zu übermitteln. Für jeden gemappten Abschnitt liest der Item-Delegierte dann den aktuellen Wert aus dem Widget und setzt ihn im Modell. Schließlich wird die Funktion submit() des Modells aufgerufen, um dem Modell mitzuteilen, dass es die Daten, die es zwischengespeichert hat, an den permanenten Speicher übergeben soll.

Bevor die Daten übermittelt werden, wird geprüft, ob der Benutzer eine andere Bilddatei ausgewählt hat, indem die zuvor gespeicherte Variable displayedImage als Referenz verwendet wird. Wenn sich der aktuelle und der gespeicherte Dateiname unterscheiden, speichern wir den neuen Dateinamen und geben das Signal imageChanged() aus.

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

Die Funktion createButtons() wird zur Vereinfachung des Konstruktors bereitgestellt.

Wir machen die Schaltfläche Close zur Standardschaltfläche, d. h. zu der Schaltfläche, die gedrückt wird, wenn der Benutzer Enter drückt, und verbinden ihr clicked()-Signal mit dem close()-Slot des Widgets. Wie bereits erwähnt, wird das Widget beim Schließen des Fensters nur ausgeblendet, aber nicht gelöscht. Wir verbinden auch die Tasten Submit und Revert mit den entsprechenden Slots submit() und revert().

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

Die Klasse QDialogButtonBox ist ein Widget, das Schaltflächen in einem Layout präsentiert, das dem aktuellen Widget-Stil entspricht. In Dialogen wie unserem Informationsfenster werden die Schaltflächen normalerweise in einem Layout dargestellt, das den Schnittstellenrichtlinien für die jeweilige Plattform entspricht. Es ist unvermeidlich, dass verschiedene Plattformen unterschiedliche Layouts für ihre Dialoge haben. QDialogButtonBox ermöglicht es uns, Schaltflächen hinzuzufügen und dabei automatisch das passende Layout für die Desktop-Umgebung des Benutzers zu verwenden.

Die meisten Schaltflächen für einen Dialog folgen bestimmten Rollen. Wir geben den Schaltflächen Submit und Revert die Rolle reset, d. h., sie zeigen an, dass das Drücken der Schaltfläche die Felder auf die Standardwerte zurücksetzt (in unserem Fall die in der Datenbank enthaltenen Informationen). Die Rolle reject zeigt an, dass das Anklicken der Schaltfläche dazu führt, dass der Dialog abgelehnt wird. Da wir jedoch nur das Informationsfenster ausblenden, bleiben alle Änderungen, die der Benutzer vorgenommen hat, erhalten, bis der Benutzer sie explizit widerruft oder abschickt.

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

Der Slot enableButtons() wird aufgerufen, um die Schaltflächen zu aktivieren, wenn der Benutzer die angezeigten Daten ändert. Ebenso werden die Schaltflächen deaktiviert, wenn der Benutzer die Änderungen abschickt, um anzuzeigen, dass die aktuellen Daten in der Datenbank gespeichert sind.

Damit ist die Klasse InformationWindow fertiggestellt. Schauen wir uns nun an, wie wir sie in unserer Beispielanwendung verwendet haben.

Definition der Ansichtsklasse

Die Klasse View stellt das Hauptfenster der Anwendung dar und erbt von 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);

Die Klasse QGraphicsView ist Teil des Graphics View Frameworks, das wir für die Anzeige der Bilder verwenden werden. Um auf Benutzerinteraktionen reagieren zu können, indem das entsprechende Informationsfenster angezeigt wird, wenn das Bild angeklickt wird, reimplementieren wir die Funktion mouseReleaseEvent() von QGraphicsView.

Beachten Sie, dass der Konstruktor die Namen von zwei Datenbanktabellen erwartet: Eine, die die detaillierten Informationen über die Objekte enthält, und eine weitere, die die Namen der verfügbaren Bilddateien enthält. Wir stellen auch einen privaten updateImage() Slot zur Verfügung, um das Signal InformationWindow's imageChanged() abzufangen, das immer dann ausgegeben wird, wenn der Benutzer ein mit dem Element verbundenes Bild ändert.

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

    QGraphicsScene *scene;
    QList<InformationWindow *> informationWindows;

Die Funktion addItems() ist eine Komfortfunktion, die zur Vereinfachung des Konstruktors dient. Sie wird nur einmal aufgerufen, um die verschiedenen Elemente zu erstellen und sie der Ansicht hinzuzufügen.

Die Funktion findWindow() hingegen wird häufig verwendet. Sie wird von der Funktion showInformation() aufgerufen, um festzustellen, ob für das angegebene Element bereits ein Fenster erstellt wurde (jedes Mal, wenn wir ein InformationWindow Objekt erstellen, speichern wir einen Verweis darauf in der Liste informationWindows ). Die letztgenannte Funktion wird wiederum von unserer eigenen mouseReleaseEvent() -Implementierung aufgerufen.

    QSqlRelationalTableModel *itemTable;
};

Schließlich deklarieren wir einen QSqlRelationalTableModel -Zeiger. Wie bereits erwähnt, bietet die Klasse QSqlRelationalTableModel ein editierbares Datenmodell mit Fremdschlüsselunterstützung. Es gibt ein paar Dinge, die Sie bei der Verwendung der Klasse QSqlRelationalTableModel beachten sollten: Die Tabelle muss einen deklarierten Primärschlüssel haben, und dieser Schlüssel darf keine Beziehung zu einer anderen Tabelle enthalten, d. h. er darf kein Fremdschlüssel sein. Beachten Sie auch, dass, wenn eine relationale Tabelle Schlüssel enthält, die sich auf nicht existierende Zeilen in der referenzierten Tabelle beziehen, die Zeilen, die die ungültigen Schlüssel enthalten, nicht durch das Modell sichtbar gemacht werden. Es liegt in der Verantwortung des Benutzers oder der Datenbank, die referentielle Integrität zu wahren.

Implementierung der View-Klasse

Obwohl der Konstruktor die Namen sowohl der Tabelle mit den Bürodetails als auch der Tabelle mit den Namen der verfügbaren Bilddateien abfragt, müssen wir nur ein QSqlRelationalTableModel Objekt für die Tabelle "items" erstellen:

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

Der Grund dafür ist, dass wir, sobald wir ein Modell mit den Artikeldetails haben, mit der Funktion setRelation() von QSqlRelationalTableModel eine Beziehung zu den verfügbaren Bilddateien herstellen können. Diese Funktion erstellt einen Fremdschlüssel für die angegebene Modellspalte. Der Schlüssel wird durch das bereitgestellte QSqlRelation Objekt spezifiziert, das aus dem Namen der Tabelle, auf die sich der Schlüssel bezieht, dem Feld, auf das der Schlüssel abgebildet wird, und dem Feld, das dem Benutzer angezeigt werden soll, besteht.

Beachten Sie, dass das Setzen der Tabelle nur angibt, mit welcher Tabelle das Modell arbeitet, d.h. wir müssen die Funktion select() des Modells explizit aufrufen, um unser Modell zu füllen.

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

Anschließend erstellen wir den Inhalt unserer Ansicht, d. h. die Szene und ihre Elemente. Bei den Beschriftungen handelt es sich um reguläre QGraphicsTextItem -Objekte, während die Bilder Instanzen der Klasse ImageItem sind, die von QGraphicsPixmapItem abgeleitet ist. Wir werden in Kürze darauf zurückkommen, wenn wir uns die Funktion addItems() ansehen.

Schließlich legen wir die Größenbeschränkungen und den Fenstertitel für das Hauptwidget der Anwendung fest.

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

Die Funktion addItems() wird nur einmal beim Erstellen des Hauptanwendungsfensters aufgerufen. Für jede Zeile in der Datenbanktabelle extrahieren wir zunächst den entsprechenden Datensatz mithilfe der Funktion record() des Modells. Die Klasse QSqlRecord kapselt sowohl die Funktionalität als auch die Eigenschaften eines Datenbankdatensatzes und unterstützt das Hinzufügen und Entfernen von Feldern sowie das Setzen und Abrufen von Feldwerten. Die Funktion QSqlRecord::value() gibt den Wert des Feldes mit dem angegebenen Namen oder Index als QVariant Objekt zurück.

Für jeden Datensatz erstellen wir ein Beschriftungselement und ein Bildelement, berechnen ihre Position und fügen sie der Szene hinzu. Die Bildelemente werden durch Instanzen der Klasse ImageItem dargestellt. Der Grund, warum wir eine benutzerdefinierte Elementklasse erstellen müssen, ist, dass wir die Hover-Ereignisse des Elements abfangen und das Element animieren wollen, wenn der Mauszeiger über dem Bild schwebt (standardmäßig akzeptieren keine Elemente Hover-Ereignisse). Weitere Einzelheiten finden Sie in der Dokumentation zum Graphics View Framework und in den Graphics View Examples.

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

Wir reimplementieren QGraphicsView's mouseReleaseEvent() Event-Handler, um auf Benutzerinteraktion zu reagieren. Wenn der Benutzer auf eines der Bildelemente klickt, ruft diese Funktion die private Funktion showInformation() auf, um das zugehörige Informationsfenster zu öffnen.

Das Graphics View Framework stellt die Funktion qgraphicsitem_cast() zur Verfügung, um festzustellen, ob die gegebene QGraphicsItem Instanz von einem bestimmten Typ ist. Wenn sich das Ereignis nicht auf eines unserer Bildelemente bezieht, übergeben wir es an die Implementierung der Basisklasse.

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

Die Funktion showInformation() erhält ein ImageItem Objekt als Argument und extrahiert zunächst die Element-ID des Elements.

Dann stellt sie fest, ob bereits ein Informationsfenster für diesen Ort erstellt wurde. Wenn kein Fenster für den angegebenen Ort existiert, erstellen wir eines, indem wir die Element-ID, einen Zeiger auf das Modell und unsere Ansicht als Elternteil an den InformationWindow Konstruktor übergeben. Beachten Sie, dass wir das imageChanged() Signal des Informationsfensters mit dem updateImage() Slot dieses Widgets verbinden, bevor wir ihm eine geeignete Position geben und es zur Liste der vorhandenen Fenster hinzufügen. Wenn es ein Fenster für die angegebene Position gibt und dieses Fenster sichtbar ist, wird sichergestellt, dass das Fenster an die Spitze des Widget-Stapels gehoben und aktiviert wird. Wenn es versteckt ist, führt der Aufruf des Slots show() zum gleichen Ergebnis.

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

Der updateImage() Slot nimmt eine Element-ID und den Namen einer Bilddatei als Argumente entgegen. Sie filtert die Bildelemente heraus und aktualisiert dasjenige, das der angegebenen Element-ID entspricht, mit der angegebenen Bilddatei.

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

Die Funktion findWindow() durchsucht einfach die Liste der vorhandenen Fenster und gibt einen Zeiger auf das Fenster zurück, das der angegebenen Element-ID entspricht, oder nullptr, wenn das Fenster nicht existiert.

Werfen wir abschließend noch einen kurzen Blick auf unsere benutzerdefinierte Klasse ImageItem:

ImageItem-Klassendefinition

Die Klasse ImageItem wird bereitgestellt, um die Animation der Bildelemente zu erleichtern. Sie erbt QGraphicsPixmapItem und implementiert dessen Hover-Ereignishandler neu:

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

Wir deklarieren einen Type enum-Wert für unser benutzerdefiniertes Element und reimplementieren type(). Dies geschieht, damit wir qgraphicsitem_cast() sicher verwenden können. Außerdem implementieren wir eine öffentliche Funktion id(), um den zugehörigen Ort zu identifizieren, und eine öffentliche Funktion adjust(), die aufgerufen werden kann, um sicherzustellen, dass das Bildelement unabhängig von der ursprünglichen Bilddatei die bevorzugte Größe erhält.

Die Animation wird mit Hilfe der Klasse QTimeLine zusammen mit den Event-Handlern und dem privaten setFrame() Slot implementiert: Das Bildelement vergrößert sich, wenn der Mauszeiger darüber schwebt, und kehrt zu seiner ursprünglichen Größe zurück, wenn der Mauszeiger die Ränder verlässt.

Schließlich speichern wir die Element-ID, mit der dieser spezielle Datensatz verbunden ist, sowie einen z-Wert. Im Graphics View Framework bestimmt der z-Wert eines Elements seine Position im Elementstapel. Ein Element mit einem hohen z-Wert wird über einem Element mit einem niedrigeren z-Wert gezeichnet, wenn sie das gleiche übergeordnete Element haben. Wir bieten auch eine updateItemPosition() Funktion, um die Ansicht bei Bedarf zu aktualisieren.

Implementierung der Klasse ImageItem

Die Klasse ImageItem ist eigentlich nur eine QGraphicsPixmapItem mit einigen zusätzlichen Funktionen, d. h. wir können die meisten Argumente des Konstruktors (die Pixmap, das übergeordnete Element und die Szene) an den Konstruktor der Basisklasse weitergeben:

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

Dann speichern wir die ID für zukünftige Referenzen und stellen sicher, dass unser Bildelement Hover-Ereignisse akzeptiert. Hover-Ereignisse werden ausgelöst, wenn es kein aktuelles Mousegrabber-Element gibt. Sie werden gesendet, wenn der Mauszeiger ein Element betritt, wenn er sich innerhalb des Elements bewegt und wenn der Mauszeiger ein Element verlässt. Wie bereits erwähnt, akzeptiert keines der Elemente des Graphics View Frameworks standardmäßig Hover-Ereignisse.

Die Klasse QTimeLine bietet eine Zeitleiste zur Steuerung von Animationen. Ihre Eigenschaft duration enthält die Gesamtdauer der Zeitleiste in Millisekunden. Standardmäßig läuft die Zeitleiste einmal vom Anfang und zum Ende hin. Die Funktion QTimeLine::setFrameRange() setzt den Frame-Zähler der Zeitleiste; wenn die Zeitleiste läuft, wird das Signal frameChanged() bei jeder Änderung des Frames ausgegeben. Wir legen die Dauer und den Frame-Bereich für unsere Animation fest und verbinden die Signale frameChanged() und finished() der Zeitleiste mit unseren privaten Slots setFrame() und updateItemPosition().

Schließlich rufen wir adjust() auf, um sicherzustellen, dass das Element die gewünschte Größe erhält.

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

Immer wenn der Mauszeiger das Bildelement betritt oder verlässt, werden die entsprechenden Ereignishandler ausgelöst: Zunächst wird die Richtung der Zeitleiste festgelegt, so dass sich das Element ausdehnt bzw. schrumpft. Dann ändern wir den z-Wert des Elements, wenn er nicht bereits auf den erwarteten Wert eingestellt ist.

Bei Hover-Entry-Ereignissen wird die Position des Eintrags sofort aktualisiert, da der Eintrag über allen anderen Einträgen erscheinen soll, sobald er zu expandieren beginnt. Bei Hover Leave-Ereignissen hingegen verschieben wir die eigentliche Aktualisierung, um das gleiche Ergebnis zu erzielen. Aber denken Sie daran, dass wir bei der Konstruktion unseres Elements das Signal finished() der Zeitleiste mit dem Slot updateItemPosition() verbunden haben. Auf diese Weise erhält das Element die richtige Position im Elementstapel, sobald die Animation abgeschlossen ist. Wenn die Zeitleiste noch nicht läuft, starten wir sie.

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

Wenn die Zeitleiste läuft, löst sie den Slot setFrame() immer dann aus, wenn sich das aktuelle Bild aufgrund der Verbindung, die wir im Item-Konstruktor erstellt haben, ändert. Dieser Slot steuert die Animation und vergrößert oder verkleinert das Bildelement Schritt für Schritt.

Zunächst rufen wir die Funktion adjust() auf, um sicherzustellen, dass wir mit der Originalgröße des Elements beginnen. Dann skalieren wir das Element mit einem Faktor, der vom Fortschritt der Animation abhängt (unter Verwendung des Parameters frame ). Beachten Sie, dass sich die Transformation standardmäßig auf die linke obere Ecke des Objekts bezieht. Da wir das Element relativ zu seinem Mittelpunkt transformieren wollen, müssen wir das Koordinatensystem übersetzen, bevor wir das Element skalieren.

Am Ende bleiben nur die folgenden Komfortfunktionen übrig:

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

Die Funktion adjust() definiert eine Transformationsmatrix und wendet sie an, um sicherzustellen, dass unser Bildelement unabhängig von der Größe des Quellbildes in der gewünschten Größe angezeigt wird. Die Funktion id() ist trivial und wird einfach bereitgestellt, um das Element zu identifizieren. Im Slot updateItemPosition() rufen wir die Funktion QGraphicsItem::setZValue() auf, die die Höhe des Objekts festlegt.

Beispielprojekt @ 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.