向下钻取示例
下钻示例展示了如何使用QSqlRelationalTableModel 和QDataWidgetMapper 类从数据库读取数据并提交更改。
向下钻取示例的屏幕截图
运行示例应用程序时,用户可以通过单击相应的图片来检索每个项目的信息。应用程序会弹出一个信息窗口显示数据,并允许用户更改描述和图片。当用户提交修改内容时,主视图也会随之更新。
该示例由三个类组成:
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"));
在本例中,有关项目的信息存储在一个名为 "项目 "的数据库表中。创建模型时,我们将使用外键在该表和第二个数据库表 "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
槽,使用户可以提交或还原改动。我们需要使用 lambdas 来连接enableButtons
插槽,因为它的签名与QTextEdit::textChanged
和QComboBox::currentIndexChanged
不匹配。
我们将所有部件添加到布局中,存储项目 ID 和显示的图像文件名称以备将来参考,并设置窗口标题和初始大小。
请注意,我们还设置了Qt::Window 窗口标志,以表明我们的窗口部件实际上是一个窗口,具有窗口系统框架和标题栏。
int InformationWindow::id() const { return itemId; }
窗口创建后,在主程序退出之前不会被删除(也就是说,如果用户关闭了信息窗口,它只会被隐藏)。因此,我们不想为每个项目创建一个以上的InformationWindow
对象,我们提供了公共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() 信号连接到 widget 的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 类是图形视图框架的一部分,我们将用它来显示图像。为响应用户交互,在点击图片时显示相应的信息窗口,我们重新实现了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 类时,有几件事需要注意:表必须声明一个主键,并且这个键不能包含与另一个表的关系,也就是说,它不能是外键。还要注意的是,如果关系表中包含的键指向引用表中不存在的行,则包含无效键的行不会通过模型显示出来。维护参照完整性是用户或数据库的责任。
视图类的实现
虽然构造函数要求提供包含办公室详细信息的表和包含可用图像文件名称的表的名称,但我们只需为 "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 对象,而图像则是ImageItem
类的实例,从QGraphicsPixmapItem 派生。我们很快就会在查看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); }
我们重新实现了QGraphicsView 的mouseReleaseEvent() 事件处理程序,以响应用户交互。如果用户点击任何图像项,该函数就会调用私有的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 类定义
提供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 值的项目共享同一个父项目,那么它们就会被绘制在低 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 以供将来参考,并确保我们的图像项可以接受悬停事件。悬停事件在当前没有鼠标抓取项时发生。当鼠标光标进入项目、在项目内移动以及离开项目时,都会发送悬停事件。如前所述,图形视图框架的所有项目默认情况下都不接受悬停事件。
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() 函数,设置项目的高度。
© 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.