向下钻取示例
向下钻取示例展示了如何使用《QSqlRelationalTableModel》和《QDataWidgetMapper》类从数据库中读取数据以及提交更改。
向下钻取示例截图
当运行示例应用程序时,用户可以通过单击相应的图片来检索每个项目的信息。应用程序将弹出一个信息窗口显示数据,并允许用户更改描述以及图片。当用户提交更改时,主视图将更新。
该示例由三个类组成
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 以供将来引用。
一旦创建了一个窗口,我们将使用公共的 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;
我们首先创建了显示数据库中包含数据的各种小部件。大多数小部件都是直接创建的。但请注意显示图像文件名称的ComboBox。
imageFileEditor = new QComboBox; imageFileEditor->setModel(items->relationModel(1)); imageFileEditor->setModelColumn(items->relationModel(1)->fieldIndex("file"));
在这个例子中,项目信息存储在名为"items"的数据库表中。在创建模型时,我们将使用外键在"items"表和包含可用图像文件名称的第二个数据库表"images"之间建立关系。我们将在查看View
类时回到如何做到这一点。创建此类关系的原因是,我们想确保用户只能从预定义的图像文件中进行选择。
对应于"images"数据库表的模型通过QSqlRelationalTableModel的relationModel()函数提供,该函数需要一个外键(在这种情况下是"imagefile"列的编号)作为参数。我们使用QComboBox的setModel()函数使ComboBox使用"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
插槽,使用户能够提交或撤销他们的更改。我们需要使用 lambda 来连接 enableButtons
插槽,因为它的签名不匹配 QTextEdit::textChanged
和 QComboBox::currentIndexChanged
。
我们将所有小部件添加到一个布局中,存储项目 ID 和显示图像文件的名称供将来参考,并设置窗口标题和初始大小。
注意,我们还设置了 Qt::Window 窗口标志以指示我们的小部件实际上是一个窗口,具有窗口系统边框和标题栏。
int InformationWindow::id() const { return itemId; }
当创建一个窗口时,它不会被删除,直到主应用程序退出(即,如果用户关闭信息窗口,它只是被隐藏)。因此,我们不希望为每个项目创建多个 InformationWindow
实例,并提供公共的 id()
函数,以便在用户请求关于该位置的信息时确定是否已经为该位置创建了窗口。
void InformationWindow::revert() { mapper->revert(); enableButtons(false); }
每当用户点击 恢复 按钮时,都会触发 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()
插槽。
我们使用 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()
函数是为了方便,即简化构造函数。
我们将 关闭 按钮设置为默认按钮,即用户按下 Enter 键时按下的按钮,并将它的 clicked() 信号连接到小部件的 close() 插槽。如上所述,关闭窗口只会隐藏小部件;它不会被删除。我们还把 提交 和 恢复 按钮连接到相应的 submit()
和 revert()
插槽。
buttonBox = new QDialogButtonBox(this); buttonBox->addButton(submitButton, QDialogButtonBox::AcceptRole); buttonBox->addButton(revertButton, QDialogButtonBox::ResetRole); buttonBox->addButton(closeButton, QDialogButtonBox::RejectRole); }
QDialogButtonBox 类是一个小部件,它以适合当前小部件样式的布局显示按钮。类似我们的信息窗口这样的对话框,通常以符合该平台界面指南的布局显示按钮。不同平台有不同的对话框布局。 QDialogButtonBox 允许我们添加按钮,并自动使用适合用户桌面环境的布局。
大多数对话框按钮遵循某些角色。我们给予提交和重置按钮重置角色,即表示按下按钮将字段重置为默认值(在本例中是数据库中的信息)。拒绝角色表示单击按钮会使对话框被拒绝。另一方面,由于我们只隐藏信息窗口,用户所做的任何更改都将在用户显式撤销或提交之前被保留。
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()函数。
请注意,构造函数期望提供两个数据库表名:一个包含有关物品的详细信息,另一个包含可用图像文件的名称。我们还提供了一个私有的updateImage()
槽,用于捕捉InformationWindow
的imageChanged()
信号,该信号在任何用户更改与项目相关的图像时都会发出。
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类时,您应该注意以下几点:表中必须声明一个主密钥,并且此密钥不能包含到另一个表的关联,即它不能是外键。另外,请注意,如果关系表包含引用不存在于被引用表中的行的键,则包含无效键的行将通过模型不会公开。维护引用完整性是用户或数据库的责任。
View类实现
尽管构造函数请求包含办公室详细信息的表以及包含可用图像文件名称的表名称,但我们必须为"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框架的文档和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框架提供了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。
然后,它会确定是否已经为该位置创建了一个信息窗口。如果没有该位置的窗口,我们将通过传递项目ID、模式指针和我们的视图作为父窗口到InformationWindow
构造函数来创建一个。注意,在我们为其提供合适的位置并将其添加到现有窗口列表之前,我们将信息窗口的imageChanged()
信号连接到this小部件的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值。在Graphics View Framework中,一项的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值,我们则修改其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(),设置项目的海拔。
© 2024 Qt公司有限公司。本文档中包含的文档贡献的著作权归各自所有者所有。本提供的文档受Free Software Foundation发布的GNU自由文档许可证第1.3版条款许可。Qt和相关的标志是The Qt Company Ltd在芬兰和其他国家和地区的商标。所有其他商标均归各自所有者所有。