QT:Widgets中的模型/视图架构
模型 / 视图架构的定义:
Qt Widgets 中的模型 / 视图(Model/View)架构是一种用于管理数据和用户界面之间交互的设计模式。它将数据存储和数据展示分离,使得同一个数据可以通过不同的视图以多种方式呈现,同时也方便对数据进行维护和更新,而不会过多地纠缠于复杂的用户界面代码。
三个主要组件:
模型(Model):
模型是数据存储和访问的核心组件。它负责存储数据,并为视图提供数据接口。模型可以是简单的列表数据,也可以是复杂的数据库查询结果或者树形结构的数据等。例如,QStandardItemModel是一个常用的通用模型,它可以存储表格形式的数据,像行和列的数据元素。
视图(View):
视图用于显示模型中的数据,它从模型获取数据并将其展示给用户。视图可以是不同的形式,如QListView用于以列表形式展示数据,QTableView用于以表格形式展示数据,QTreeView用于以树形结构展示数据。视图决定了数据在屏幕上的呈现方式,包括外观、布局等。
委托(Delegate):
委托是模型和视图之间的一个中间层,它用于控制数据在视图中的编辑和显示细节。例如,委托可以定义如何在视图中绘制数据项,如何处理用户对数据项的编辑操作,像QStyledItemDelegate可以根据不同的数据类型自定义数据的显示和编辑方式。
工作原理
数据获取与更新:
视图从模型获取数据来进行显示。当视图需要显示数据时,它会调用模型的接口(如data函数)来获取相应的数据项。模型负责根据视图的请求提供正确的数据。例如,一个QTableView会向其关联的模型询问每一个单元格的数据内容,模型根据请求返回对应的表格数据行和列中的元素。
当数据发生更新时,模型会发出信号(如dataChanged信号)通知所有关联的视图数据已经改变。视图接收到信号后,会重新从模型获取更新后的数据并刷新显示。这样可以保证视图始终展示最新的数据。
用户交互处理:
当用户与视图进行交互(如点击、编辑数据等操作)时,视图会处理一部分交互逻辑,但对于数据的修改等操作,视图会通过模型提供的接口将用户的操作传递给模型。例如,当用户在QListView中双击一个项目进行编辑时,视图会通过委托(如果有)来获取用户输入的新数据,然后将其传递给模型进行数据更新。
示例场景和优势
示例场景:
假设一个音乐播放器应用,有一个播放列表。模型可以是一个存储音乐文件信息(如歌名、歌手、时长等)的QStandardItemModel。QListView视图可以用来展示播放列表中的歌曲名称,当用户选择一首歌曲时,视图获取选择信息并通知模型,模型返回该歌曲的详细信息用于播放。同时,如果在播放过程中更新了歌曲的播放次数等信息,模型更新数据并通知视图刷新,视图会正确地显示更新后的播放次数。
优势:
代码分离与复用:通过将数据存储和显示分离,使得数据相关的代码(模型)和用户界面相关的代码(视图和委托)可以独立开发和维护。例如,可以在不改变数据存储和处理逻辑的情况下,更换视图来提供不同的用户界面风格,如从列表视图切换到表格视图。
高效的数据处理和显示:对于大型数据集,模型 / 视图架构能够高效地处理数据的获取和显示。视图只请求和显示当前可见部分的数据,模型根据视图的请求动态地提供数据,避免了一次性加载和处理大量不必要的数据,提高了应用程序的性能。
灵活性和可扩展性:可以方便地添加新的视图类型或者修改视图的显示方式(通过委托)来满足不同的用户需求或者应用场景。例如,为了适应不同的设备屏幕尺寸或者用户偏好,可以很容易地创建新的视图来展示数据。
树视图中显示模型
完成后在.pro文件中添加QT += widgets并保存该文件
加新的main.cpp文件,并更改其内容如下。
#include <QApplication>
#include <QTreeView>
#include <QDebug>
#include <QStandardItemModel>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// 创建标准项模型
QStandardItemModel model;
// 获取模型的根项(Root Item),根项是不可见的
QStandardItem *parentItem = model.invisibleRootItem();
// 创建标准项item0,并设置显示文本、图标和工具提示
QStandardItem *item0 = new QStandardItem;
item0->setText("A");
QPixmap pixmap0(50, 50);
pixmap0.fill("red");
item0->setIcon(QIcon(pixmap0));
item0->setToolTip("indexA");
// 将创建的标准项作为根项的子项
parentItem->appendRow(item0);
// 将创建的标准项作为新的父项
parentItem = item0;
// 创建新的标准项,它将作为item0的子项
QStandardItem *item1 = new QStandardItem;
item1->setText("B");
QPixmap pixmap1(50,50);
pixmap1.fill("blue");
item1->setIcon(QIcon(pixmap1));
item1->setToolTip("indexB");
parentItem->appendRow(item1);
// 创建新的标准项,这里使用了另一种方法来设置文本、图标和工具提示
QStandardItem *item2 = new QStandardItem;
QPixmap pixmap2(50,50);
pixmap2.fill("green");
item2->setData("C", Qt::EditRole);
item2->setData("indexC", Qt::ToolTipRole);
item2->setData(QIcon(pixmap2), Qt::DecorationRole);
parentItem->appendRow(item2);
// 将创建的标准项作为新的父项
parentItem = item1;
// 创建新的标准项,它将作为item1的子项
QStandardItem *item3 = new QStandardItem;
item3->setText("D");
QPixmap pixmap3(50,50);
pixmap3.fill("yellow");
item3->setIcon(QIcon(pixmap3));
item3->setToolTip("indexD");
parentItem->appendRow(item3);
// 在树视图中显示模型
QTreeView view;
view.setModel(&model);
view.show();
// 获取item0的索引并输出子项数目,然后输出item1的显示文本和工具提示
QModelIndex indexA = model.index(0, 0, QModelIndex());
qDebug() << "indexA row count: " << model.rowCount(indexA);
QModelIndex indexB = model.index(0, 0, indexA);
qDebug() << "indexB text: " << model.data(indexB, Qt::EditRole).toString();
qDebug() << "indexB toolTip: "
<< model.data(indexB, Qt::ToolTipRole).toString();
return app.exec();
}
选择模型的使用
完成后在mainwindow.h文件中添加类的前置声明,再添加一个私有对象指针
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class QTableView;
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QTableView *tableView;
};
#endif // MAINWINDOW_H
mainwindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QStandardItemModel>
#include <QTableView>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//构建模型数据
QStandardItemModel *model = new QStandardItemModel(7, 4, this);
for (int row = 0; row < 7; ++row) {
for (int column = 0; column < 4; ++column) {
QStandardItem *item = new QStandardItem(QString("%1")
.arg(row * 4 + column));
model->setItem(row, column, item);
}
}
//将模型添加到视图
tableView = new QTableView;
tableView->setModel(model);
setCentralWidget(tableView);
//选中视图当中指定部分
// 获取视图的项目选择模型
QItemSelectionModel *selectionModel = tableView->selectionModel();
// 定义左上角和右下角的索引,然后使用这两个索引创建选择
QModelIndex topLeft;
QModelIndex bottomRight;
topLeft = model->index(1, 0, QModelIndex());
bottomRight = model->index(5, 2, QModelIndex());
QItemSelection selection(topLeft, bottomRight);
// 使用指定的选择模式来选择项目
selectionModel->select(selection, QItemSelectionModel::Select);
}
MainWindow::~MainWindow()
{
delete ui;
}
如何使用现成的部件自定义委托
在上一个程序中继续添加新的类
spinboxdelegate.h:
#ifndef SPINBOXDELEGATE_H
#define SPINBOXDELEGATE_H
#include <QStyledItemDelegate>
class SpinBoxDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
SpinBoxDelegate(QObject *parent = nullptr);
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const override;
void updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};
#endif // SPINBOXDELEGATE_H
spinboxdelegate.cpp:
#include "spinboxdelegate.h"
#include<QSpinBox>
SpinBoxDelegate::SpinBoxDelegate(QObject *parent):QStyledItemDelegate(parent)
{
}
/*
* 这个函数是委托类的一个重要成员函数,
* 它的任务是创建用于编辑对应单元格数据的实际编辑器控件。
* 在这里,它创建了一个 QSpinBox(整数输入框)作为编辑器,
* 这个 QSpinBox 将在用户想要编辑某个符合该委托要求的单元格数据时显示出来供用户操作。
*/
// 创建编辑器
QWidget *SpinBoxDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &/* option */,
const QModelIndex &/* index */) const
{
QSpinBox *editor = new QSpinBox(parent);
editor->setFrame(false);// 去掉了 QSpinBox 的边框
//QSpinBox 可输入的数值范围是从 0 到 100
editor->setMinimum(0);
editor->setMaximum(100);
return editor;
}
/*
* 当编辑器(也就是前面创建的 QSpinBox )被创建并显示出来准备让用户编辑时,
* 这个函数负责从模型(一般是和视图关联的 QAbstractItemModel 派生类的模型对象)中获取对应单元格的数据,
* 并将该数据设置到编辑器中,使得编辑器初始显示的数值就是单元格原本存储的值。
*/
// 为编辑器设置初始数据
void SpinBoxDelegate::setEditorData(QWidget *editor,
const QModelIndex &index) const
{
//从模型中获取指定索引位置单元格的数据,并且将其转换为整数类型
int value = index.model()->data(index, Qt::EditRole).toInt();
//将传入的通用 QWidget 类型的 editor 指针安全地转换为 QSpinBox 类型的指针
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
//将获取到的数值设置到 QSpinBox 中
spinBox->setValue(value);
}
/*
* 在用户完成对编辑器(QSpinBox )的数值输入操作并确认(比如按下回车键等触发保存操作的行为)后,
* 这个函数负责将编辑器中用户输入修改后的数值重新写回到模型对应的单元格中,完成数据的更新。
*/
// 将数据写入模型
void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const
{
//将传入的 editor 转换为 QSpinBox 类型指针
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
/*
* QSpinBox 根据用户输入的文本内容(比如用户输入了 50 ,它会解析这个文本并确定对应的数值)
* 正确解析并更新其内部的数值状态,确保获取到的 value 是准确的用户输入修改后的数值。
*/
spinBox->interpretText();
int value = spinBox->value();//获取到这个准确的数值
//将该数值按照 Qt::EditRole 这个数据角色写回到模型中指定索引的单元格位置,从而完成数据更新。
model->setData(index, value, Qt::EditRole);
}
/*用于设置编辑器(QSpinBox )在视图中的几何位置和大小,
* 确保它能准确地覆盖在对应的要编辑的单元格位置上,并且显示的大小和样式符合视图的整体布局要求。
*/
// 更新编辑器几何布局
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &/* index */) const
{
/*
* 让编辑器(QSpinBox )的几何形状(位置和大小)与 option 中指定的单元格矩形区域保持一致,
* 保证其在视图中正确显示和布局。
*/
editor->setGeometry(option.rect);
}
没有添加编辑器
添加了编辑器
mainwindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QStandardItemModel>
#include <QTableView>
#include "spinboxdelegate.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//构建模型数据
QStandardItemModel *model = new QStandardItemModel(7, 4, this);
for (int row = 0; row < 7; ++row) {
for (int column = 0; column < 4; ++column) {
QStandardItem *item = new QStandardItem(QString("%1")
.arg(row * 4 + column));
model->setItem(row, column, item);
}
}
//将模型添加到视图
tableView = new QTableView;
tableView->setModel(model);
setCentralWidget(tableView);
//选中视图当中指定部分
// 获取视图的项目选择模型
QItemSelectionModel *selectionModel = tableView->selectionModel();
// 定义左上角和右下角的索引,然后使用这两个索引创建选择
QModelIndex topLeft;
QModelIndex bottomRight;
topLeft = model->index(1, 0, QModelIndex());
bottomRight = model->index(5, 2, QModelIndex());
QItemSelection selection(topLeft, bottomRight);
// 使用指定的选择模式来选择项目
selectionModel->select(selection, QItemSelectionModel::Select);
SpinBoxDelegate *delegate = new SpinBoxDelegate(this);
tableView->setItemDelegate(delegate);
}
MainWindow::~MainWindow()
{
delete ui;
}
QListWidget的例子
pro文件中添加QT +=widgets并保存该文件
QT +=widgets
添加新的main.cpp文件,并更改内容
#include <QApplication>
#include <QDebug>
#include <QListWidget>
#include <QTreeWidget>
#include <QTableWidget>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QListWidget listWidget;
// 一种添加项目的简便方法
new QListWidgetItem("a", &listWidget);
// 添加项目的另一种方法,这样还可以进行各种设置
QListWidgetItem *listWidgetItem = new QListWidgetItem;
listWidgetItem->setText("b");
listWidgetItem->setIcon(QIcon("../../image_robot.png"));
listWidgetItem->setToolTip("this is b!");
listWidget.insertItem(1, listWidgetItem);
QListWidgetItem *listWidgetItemC = new QListWidgetItem("c", &listWidget);
listWidgetItemC->setIcon(QIcon("../../image_path_1.png"));
new QListWidgetItem("d", &listWidget);
// 设置排序为倒序
listWidget.sortItems(Qt::DescendingOrder);
// 显示列表部件
listWidget.show();
return app.exec();
}