QT利用QTableView和自定义Model实现一个增删改查等功能超全的复用型表格
目录
一、表格功能总结与预览
1.功能总结
2.尚欠缺的功能(后续可能会补充)
二、功能设计选择原因
1.QTableWidget与MVC模式的QTableview的选择
2.处理数据的方式
所以后续将会选择第二种方式,直接操作数据库,并且通过数据库来管理同步数据,此Demo为未链接数据库版本
三、功能实现方法
注:头文件和源文件在一个代码框中注意分辨,有些代码可能无法全部剥离,还请理解功能读懂代码或者跳转到最后直接拿到完整Demo。
1.与QT自带的QTableWIdget不同可以承受大量的数据量(基于MVC模式的显示模式)
2.可以自定义数据的传入格式与数据操作,让数据和视图隔离(继承自QAbstractTableModel类的自定义model)
2.1 数据类
2.2 水平表头类
2.3 TableModel类
3.可自定义编辑时,单元格中的编辑效果以及表格背景间隔颜色(继承自QStyledItemDelegate类的自定义delegate)
4.可以控制那些鼠标事件和按键事件是否可用和点击效果(继承自QTableView类的自定义TableView)
5.新增
6.删除
7.编辑
8.复制
9.查询或搜索(简单匹配版,遍历所有行)
10.上移和下移
11.第一列是否有复选框,选中行则复选框选中(表头为三态复选框)
总:以上的内容均由封装表格类UtilTableWidget融合MVC模式实现
四、完整Demo与总结
1.遇到的问题
1.1筛选model的使用问题
2.可以优化的地方
2.1数据库接入
2.2数据通用性优化
2.3数据修改方式优化
2.4搜索筛选方式优化
3.框架修改
4.Demo链接
一、表格功能总结与预览
1.功能总结
1.1与QT自带的QTableWIdget不同可以承受大量的数据量(基于MVC模式的显示模式)
1.2.可以自定义数据的传入格式与数据操作,让数据和视图隔离(继承自QAbstractTableModel类的自定义model)
1.3.可自定义编辑时,单元格中的编辑效果以及表格背景间隔颜色(继承自QStyledItemDelegate类的自定义delegate)
1.4.可以控制那些鼠标事件和按键事件是否可用和点击效果(继承自QTableView类的自定义TableView)
1.5.新增
1.6.删除
1.7.编辑
1.8.复制
1.9.查询或搜索(简单匹配版,遍历所有行)
1.10.上移和下移
1.11.第一列是否有复选框,选中行则复选框选中(表头为三态复选框)
2.尚欠缺的功能(后续可能会补充)
2.1.数据库中取出不同的表数据转换为规定格式塞入model的功能
2.2.表格更改内容与数据库同步功能,及sql语句连接
2.3.有可能需要恢复更改前的数据功能,所以可能需要备份一份数据能够恢复的功能
二、功能设计选择原因
1.QTableWidget与MVC模式的QTableview的选择
QT自带的QTableWidget与MVC模式中的自定义model、TableView、delegate在功能和使用方式上有一些显著的不同。
首先,QTableWidget是QT对话框设计中常用的显示数据表格的控件。它继承自QTableView,但只能使用标准的数据模型。QTableWidget中的单元格数据是通过QTableWidgetItem对象来实现的,这意味着不需要数据源,单元格内的信息需要逐个填充。因此,QTableWidget在处理小型数据集或需要直接操作单元格内容的场景时非常有用。
相比之下,MVC模式(Model-View-Controller)是一种更灵活和可扩展的架构,用于实现大量的数据存储、处理及显示。在MVC模式中,model是应用对象,用来表示数据;view是模型的用户界面,用来显示数据;controller定义了用户界面对用户输入的反应方式。这种模式使得数据的表示、处理和显示可以分开管理,提高了代码的可维护性和可重用性。
在MVC模式的自定义实现中,model可以是任何自定义的数据模型,它负责处理数据的存储和逻辑。TableView作为视图组件,负责数据的展示。与QTableWidget不同,TableView本身不直接处理数据,而是通过绑定到model来获取和显示数据。这种方式使得数据的处理和展示更加灵活和可定制。
此外,delegate在MVC模式中扮演了重要角色。它用于定制数据的渲染和编辑方式。通过自定义delegate,你可以控制单元格的显示样式、编辑行为等。这与QTableWidget中直接操作单元格的方式有所不同,提供了更高级别的定制能力。
总结来说,QTableWidget适合简单的数据展示和编辑需求,而MVC模式的自定义model、TableView、delegate则提供了更灵活、可扩展和可定制的数据处理、展示和编辑能力。在选择使用哪种方式时,需要根据具体的应用场景和需求进行权衡。
查询或搜索,可以从数据库直接查询,但是每次查询都要去访问数据库,会对性能有要求,而放入QSortFilterProxyModel进行筛选,则是操作已经从数据库读出来拷贝的一本显示数据,所以不会访问数据库
这里对代理和模型做了如下选择
a. 继承自QAbstractTableModel类的自定义model
b. 继承自QStyledItemDelegate类的自定义delegate
c. 继承自QTableView类的自定义TableView
d. 使用QT源生的QItemSelectionModel
2.处理数据的方式
在处理数据的时候,可以通过两种方式来处理数据的变化
其一是通过获取选择的行传入自定义model中由model来处理(方便并且适用于有选择行的操作,并且不用刷新和拷贝数据,只是改变data但是有无法进行没办法获取行的情况,并且最后其实还是要和数据库的数据同步,无疑有一些绕弯路。)
其二是统一处理传入自定义model中的data数据,而数据来源于数据库,相当于直接操作数据库(使得整个结构清晰,始终以数据库的数据为准,model只是取出数据显示,任何操作都会马上更新入数据库,实时性。在操作上,就需要有数据的唯一值进行匹配数据,找到数据的位置进行操作而不能根据行)
对数据库的操作,建立的表,最好是有主键的,这样才能方便查找提升性能
数据库中的表可以没有主键。当表没有主键时,查找操作仍然可以执行,但可能会受到一些限制和影响。
在数据库中,主键的主要作用是唯一标识表中的每一行数据,并确保数据的完整性和一致性。没有主键的表意味着没有这样的唯一标识符,这可能会导致一些潜在的问题,如数据重复或不一致。
当表没有主键时,你仍然可以使用其他列进行查找。这通常涉及到在查询中使用WHERE子句来指定搜索条件。例如,你可以根据某个特定列的值来检索数据。
然而,没有主键的表在数据更新和删除方面可能会更加复杂和困难。由于没有唯一标识符来准确定位要更新的行或要删除的行,你可能需要依赖于其他列的组合或使用更复杂的查询逻辑来实现这些操作。
此外,没有主键的表在性能上也可能会受到一定的影响。主键通常与索引相关联,这有助于加速查询操作。没有主键的表可能无法充分利用索引的优势,从而导致查询性能下降。
因此,虽然数据库中的表可以没有主键,但在设计数据库时,建议尽可能为每个表指定一个主键。主键不仅有助于确保数据的完整性和一致性,还能简化数据更新、删除和查询操作,并提高性能。
所以后续将会选择第二种方式,直接操作数据库,并且通过数据库来管理同步数据,此Demo为未链接数据库版本
三、功能实现方法
注:头文件和源文件在一个代码框中注意分辨,有些代码可能无法全部剥离,还请理解功能读懂代码或者跳转到最后直接拿到完整Demo。
1.与QT自带的QTableWIdget不同可以承受大量的数据量(基于MVC模式的显示模式)
QTableview有setModel和SetItemDelegate的方法,就可以很简单的完成这一模式的搭建,需要的是实现功能而重写不同的类继承自那些默认的类,下面三个就是继承了不同的QT类重写的。
m_tableview->setModel(m_tablemodel); m_selectionModel = m_tableview->selectionModel(); m_tableview->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_tableview->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_tableview->setItemDelegate(m_tableDelegate); m_tableview->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); m_tableview->verticalHeader()->setVisible(false); m_tableview->setAlternatingRowColors(true); m_tableview->setSelectionBehavior(QAbstractItemView::SelectRows);
2.可以自定义数据的传入格式与数据操作,让数据和视图隔离(继承自QAbstractTableModel类的自定义model)
2.1 数据类
//后续数据的传入,可以通过模版来控制 //template class TableData { // explicit TableData(const int& column){ // m_column = column; // } public: //第一列的复选框参数 bool m_bChecked; //数据,后期数据可以通过模板数据类或别的结构进来 QVector m_data; };
2.2 水平表头类
class TableHeaderView:public QHeaderView { Q_OBJECT public: TableHeaderView(Qt::Orientation orientation, QWidget *parent); ~TableHeaderView(); public: void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const; bool event(QEvent *e) Q_DECL_OVERRIDE; void mousePressEvent(QMouseEvent *e) Q_DECL_OVERRIDE; void mouseReleaseEvent(QMouseEvent *e) Q_DECL_OVERRIDE; public slots: void onStateChanged(int state); signals: void stateChanged(int); public: bool m_bPressed; bool m_bChecked; bool m_bTristate; bool m_bNoChange; bool m_bMoving; }; TableHeaderView::TableHeaderView(Qt::Orientation orientation, QWidget *parent) : QHeaderView(orientation, parent), m_bPressed(false), m_bChecked(false), m_bTristate(false), m_bNoChange(false), m_bMoving(false) { setSectionsClickable(true); } TableHeaderView::~TableHeaderView() { } // 槽函数,用于更新复选框状态 void TableHeaderView::onStateChanged(int state) { // qDebug() save(); QHeaderView::paintSection(painter, rect, logicalIndex); painter->restore(); if (logicalIndex == CHECK_BOX_COLUMN) { QStyleOptionButton option; option.initFrom(this); if (m_bChecked) option.state |= QStyle::State_Sunken; if (m_bTristate && m_bNoChange) option.state |= QStyle::State_NoChange; else option.state |= m_bChecked ? QStyle::State_On : QStyle::State_Off; if (testAttribute(Qt::WA_Hover) && underMouse()) { if (m_bMoving) option.state |= QStyle::State_MouseOver; else option.state &= ~QStyle::State_MouseOver; } QCheckBox checkBox; option.rect = QRect(4,5,15,15);//绘制复选框的位置与大小 style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &option, painter, &checkBox); } } // 鼠标按下表头 void TableHeaderView::mousePressEvent(QMouseEvent *event) { int nColumn = logicalIndexAt(event->pos()); if ((event->buttons() & Qt::LeftButton) && (nColumn == CHECK_BOX_COLUMN)) { m_bPressed = true; } else { QHeaderView::mousePressEvent(event); } } // 鼠标从表头释放,发送信号,更新model数据 void TableHeaderView::mouseReleaseEvent(QMouseEvent *event) { if (m_bPressed) { if (m_bTristate && m_bNoChange) { m_bChecked = true; m_bNoChange = false; } else { m_bChecked = !m_bChecked; } update(); Qt::CheckState state = m_bChecked ? Qt::Checked : Qt::Unchecked; emit stateChanged(state); } else { QHeaderView::mouseReleaseEvent(event); } m_bPressed = false; } // 鼠标滑过、离开,更新复选框状态 bool TableHeaderView::event(QEvent *event) { updateSection(0); if (event->type() == QEvent::Enter || event->type() == QEvent::Leave) { QMouseEvent *pEvent = static_cast(event); int nColumn = logicalIndexAt(pEvent->x()); if (nColumn == CHECK_BOX_COLUMN) { m_bMoving = (event->type() == QEvent::Enter); update(); return true; } } return QHeaderView::event(event); }
2.3 TableModel类
#define CHECK_BOX_COLUMN 0 class TableModel : public QAbstractTableModel { Q_OBJECT public: explicit TableModel(QObject *parent = nullptr); // 实现QAbstractTableModel的虚函数 int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Qt::ItemFlags flags(const QModelIndex &index) const override; //插入行 bool insertRows(int row, int count, const QModelIndex &parent) override; //删除行 bool removeRows(int row, int count, const QModelIndex &parent) override; // bool setTableData(const QVector& data); //给model传入data bool setTableData(const QVector& data); //给model传入表头 void setTableHeaderData(const QStringList& headerData); //设置第一列是否为checkbox void setFirstColumnCheckBox(bool state); //设置某行的checkbox选中状态 void setCheckBoxSelect(const int& row, bool state); //设置表格是否可以编辑 void setTableisEditable(bool editable); void onStateChanged(); //插入行 bool insertRowData(int row, int count, const QModelIndex &parent, TableData data); //更新指定行数据 void updateData(int row, TableData data); //获取一行的数据 TableData getOneRowData(int row); //获取所有数据 QVector getALLRowData(); //上移一行 void moveRowUp(int row); //下移一行 void moveRowDown(int row); public slots: void onStateChanged(int state); signals: void stateChanged(int); void signalRowChecked(int row, bool bchecked); void signalSelectAll(); void signalDeSelectALL(); private: QStringList m_headerData; bool m_bEnableCheckBox; bool m_bEditable; int m_nfirstcolumn; // QList m_bCheckedlist; // QVector m_data; // 表格数据 QVector m_tabledata; //传入的表格数据模版不同格式 }; TableModel::TableModel(QObject *parent) : QAbstractTableModel(parent) { m_nfirstcolumn = 0; m_bEnableCheckBox = false; m_bEditable = false; } int TableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) // return m_data.size(); return m_tabledata.size(); } int TableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) // return m_data.isEmpty() ? 0 : m_data.first().size()+m_nfirstcolumn; return m_tabledata.isEmpty() ? 0 : m_tabledata.first().m_data.size() + m_nfirstcolumn; } QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation != Qt::Horizontal) return QVariant("only support horizontal"); if(role != Qt::DisplayRole) return QVariant(); switch (role) { case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);// 表头内容位置 case Qt::DisplayRole: { if (orientation == Qt::Horizontal) { if (m_bEnableCheckBox && section == CHECK_BOX_COLUMN) return QStringLiteral(""); // return QStringLiteral("全选"); if(section-m_nfirstcolumn >= m_headerData.size()) return QVariant("NoName"); return m_headerData.at(section-m_nfirstcolumn); } } default: return QVariant(); } return QVariant(); } QVariant TableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); int nRow = index.row(); int nColumn = index.column(); // bool bChecked = m_bCheckedlist.at(nRow); bool bChecked = m_tabledata.at(nRow).m_bChecked; switch (role) { case Qt::TextColorRole: return QColor(Qt::white); case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);// 表格数据内容位置 case Qt::DisplayRole: { if (m_bEnableCheckBox && nColumn == CHECK_BOX_COLUMN) return QVariant(); if(nColumn != CHECK_BOX_COLUMN) { // qDebug() ignore(); return; } QTableView::mousePressEvent(event); } void mouseMoveEvent(QMouseEvent *event) override { // 禁用鼠标移动事件导致的选中 event->ignore(); } void keyPressEvent(QKeyEvent *event) override { // 禁用 Shift + 鼠标左键等组合键 // if ((event->modifiers() == Qt::ShiftModifier) && (event->button() == Qt::LeftButton)) { // event->ignore(); // return; // } QTableView::keyPressEvent(event); } };
5.新增
bool UtilTableWidget::insertTableRows(const int& insertRow, const TableData& newRowData) { QModelIndex insertindex = m_tablemodel->index(insertRow, 0); bool insertresult = m_tablemodel->insertRowData(insertRow, 1, insertindex, newRowData); // m_selectionModel->clearSelection(); selectRow(insertRow); m_tableview->scrollTo(insertindex); return insertresult; }
6.删除
bool UtilTableWidget::deleteTableRows(const QList &deleterows) { bool deleteresult = true; QList sortdeleterows = deleterows; //删除行的时候要从大往小的编号进行删除,因为删除一行就会更新表格数据 for(int i = sortdeleterows.size() - 1; i >= 0; --i) { int deleterow = sortdeleterows[i]; if(deleterow = m_tablemodel->rowCount()) continue; qDebug() removeRows(deleterow, 1, deleteindex)) { deleteresult = false; } //调整后续行的索引,防止还有更大的删除行,就需要进行调整 for(int j = i + 1; j deleterow) sortdeleterows[j]--; } } // m_selectionModel = m_tableview->selectionModel(); clearAllSelectedRows(); return deleteresult; }
7.编辑
void UtilTableWidget::editTableRow(const int &editRow, const TableData &editRowData) { m_tablemodel->updateData(editRow, editRowData); }
8.复制
bool UtilTableWidget::copyTableRows(const QList ©rows) { clearAllSelectedRows(); bool copyresult = true; //需要先将所有需要复制的数据拷贝一份 QVector vecCopydata; for(int i = 0; i getOneRowData(copyrows.at(i))); } //开始插入 for(int i = 0; i rowCount(); QList matchingRows; // 存储匹配行的索引 QVector tempsreachRow; // 遍历所有行来查找匹配项 for (int row = 0; row columnCount(); ++col) { QModelIndex index = m_tablemodel->index(row, col); QString cellText = m_tablemodel->data(index, Qt::DisplayRole).toString(); qDebug() moveRowDown(row); selectRow(row + 1); }
11.第一列是否有复选框,选中行则复选框选中(表头为三态复选框)
利用2.2提到的TableHeaderView实现水平表头的复选框,然后利用21提到的数据结构将第一列设置为复选框,然后连接选择的槽函数实现
connect(m_tablemodel, &TableModel::signalRowChecked, this, &UtilTableWidget::onSelectRow); connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &UtilTableWidget::onselectionModelChangeSelection); connect(m_tablemodel, &TableModel::signalSelectAll, this, &UtilTableWidget::onSelectALLRows); connect(m_tablemodel, &TableModel::signalDeSelectALL, this, &UtilTableWidget::onDeSelectALLRows);
void UtilTableWidget::enableCheckBox(bool state) { // 设置垂直表头代理 // m_headerview = new SCheckBoxHeaderView(10, Qt::Horizontal, m_tableview); // m_tableview->setHorizontalHeader(m_headerview); // connect(m_headerview, &SCheckBoxHeaderView::checkStausChange, this, [=](bool status){ // //控制所有目标的显示与隐藏 // for (int i = 0; i rowCount(); i++) { // QModelIndex index = m_tablemodel->index(i, 0); // m_tablemodel->setData(index, status, Qt::CheckStateRole); // } // }); m_headerview = new TableHeaderView(Qt::Horizontal, this); // 设置表头 m_tableview->setHorizontalHeader(m_headerview); m_tablemodel->setFirstColumnCheckBox(state); // 关联表头复选框与第一列复选框 connect(m_headerview, SIGNAL(stateChanged(int)), m_tablemodel, SLOT(onStateChanged(int))); connect(m_tablemodel, SIGNAL(stateChanged(int)), m_headerview, SLOT(onStateChanged(int))); }
总:以上的内容均由封装表格类UtilTableWidget融合MVC模式实现
#include #include #include #include #include #include #include #include #include #include #include #include #include #include "tablemodel.h" #include "tabledelegate.h" #include "tableview.h" class UtilTableWidget : public QWidget { Q_OBJECT public: explicit UtilTableWidget(QWidget *parent = 0); ~UtilTableWidget(); void setupTableWidget(); void setTableHeader(const QStringList& horizontalHeader); void setTableData(const QVector& data); QTableView *gettableView(); //是否启动第一列为checkbox void enableCheckBox(bool state); //是否启用编辑 void enableEditable(bool state); int getTableViewRow(); int getTableViewColumn(); //获取某行数据 TableData getTableRowData(int row); //获取表格所有数据 QVector getTableAllData(); //新增 void addTableData(); //插入 bool insertTableRows(const int& insertRow, const TableData& newRowData); //删除 bool deleteTableRows(const QList& deleterows); //编辑 void editTableRow(const int& editRow, const TableData& editRowData); //复制 bool copyTableRows(const QList& copyrows); //上移 void moveTableRowUp(int row); //下移 void moveTableRowDown(int row); //取消 //清除选中行 void clearAllSelectedRows(); //获取选中的行 QList getSelectedRows(); //选中一行 void selectRow(const int& row); //取消选中一行 void deSelectRow(const int& row); //选中多行 void selectRows(const QList& rows); //根据某行是否选中更新复选框是否选中 void updateCheckboxbySelectRows(); //搜索 void sreachTableRows(const QString &searchText); public slots: void onFilterChanged(const QString& text); void onSelectRow(int row, bool bchecked); void onSelectALLRows(); void onDeSelectALLRows(); void onselectionModelChangeSelection(const QItemSelection &selected, const QItemSelection &deselected); void onCurrentSelectRowChange(const QModelIndex ¤t, const QModelIndex &previous); private: TableView *m_tableview; TableModel *m_tablemodel; QSortFilterProxyModel *m_proxyModel; QItemSelectionModel *m_selectionModel; TableDelegate *m_tableDelegate; TableHeaderView *m_headerview; QList m_listSelectRows; QVector m_oldData; bool firstsreach = true; };
四、完整Demo与总结
1.遇到的问题
1.1筛选model的使用问题
// m_proxyModel = new QSortFilterProxyModel(this); // m_proxyModel->setSourceModel(m_tablemodel); // m_tableview->setModel(m_proxyModel);
2.可以优化的地方
可以优化的地方很多
2.1数据库接入
现在的demo中只写了一个概念类,是怎么实现数据库访问和转为表格展示数据的,后续我会进行优化
2.2数据通用性优化
就是如果是不同的结构体的表格数据,应该可以利用模版的形式来完成这样的转化,不然多一种表格数据,就需要多出一个转化为对应结构体的函数重写,这方面应该是可以提升的。
2.3数据修改方式优化
不再直接操作TableModel中的数据,而是直接操作数据库,但由于每一个操作都是操作数据库,所以需要放入线程中防止卡界面,此时需要数据表中有主键,不能再根据行来操作数据。
而TableModel则不再有增删改查对Model中数据的修改,而只有设置整体数据的接口。
2.4搜索筛选方式优化
QSortFilterProxyModel实现搜索,可以重写控制筛选搜索条件等这个方法应该启用,并解决索引问题,甚至可以根据数据库来筛选等优化方向
3.框架修改
4.Demo链接
百度网盘:
http://链接:https://pan.baidu.com/s/1HVplXOWVnuar3oalqd6k2A?pwd=7j9r 提取码:7j9r
这个表格本作者还是想把它做得更加复用化,功能更加强大,并且性能能承受大数据量的压力,虽然还有很多不足,但是希望大家能谅解本作者的水平有限,后续会根据框架修改和制作自定义Model实现一个增删改查等功能的复用型表格TableView——链接数据库版,跟大家互相学习了,有问题可以在下方留言讨论,谢谢!