1 简介
最近在工作中需要做一个表格控件,借鉴了qt表格的设计思想,在这里记录下学习所得。
本篇文章主要谈一下对Qt表格整体的理解与具体的实现方式,后续再通过以点带面的形式分析Qt表格的源码实现。
TableView对比TableWidget的优势一在于灵活,二在于 节约资源。灵活自不必多说,节约资源方面,TableWidget是基于Item,比如有10万条数据需要展示,在TableWidget中就会有10万个Item;而TableView是根据计算出视口要展示多少条数据来绘制表格,比如同样10万条数据,视口只需要展示第99000到99500条数据共500条数据,view就会去模型中取到这500条数据并绘制,这样就保持了一个比较固定的内存资源。
我认为Qt表格的实现主要分为model(模型)、view(视图)、delegate(代理)这三个部分:
1)model: 主要负责管理数据并为整个场景提供咨询服务。
2)view:主要负责管理布局、响应事件, 除了计算布局之外并不太爱动脑经,别的事都是交给代理去做。
3)delegate:主要负责具体的绘制工作,把数据以用户想要的方式绘制出来 (当然更深层次是交给QStyle类来确定平台下绘制的样式,然后再交给QPainter绘制,这个在以后我们分析源码的时候会看到 )。
所以说要制作一个自定义的表格, 首先要给View提供一个自定义的Model(继承自QAbstractTableModel),并做好必要的咨询接口(重写必要的虚函数).当完成这项工作后,表格已经能够正常工作了,这是因为在View中都有一个默认的Delegate来负责绘制数据.当然,如果你想让模型中的数据按照你自己的想法来展示,比如在第一列,当数据为1时,绘制的是一个飞机图标;数据为2时,绘制的是一个汽车图标等等. 那么你就得制作一个自定义的delegate来代替默认的delegate工作.
好了,说了这么多,让我们开始上代码.
2 一个自定义的Model
CustomModel.h
#pragma once
#include <QAbstractTableModel>
#include <vector>
//每行的数据
typedef struct _CustomData
{
QString _name;
QString _id;
int _grade;
}CustomData;
class CustomModel : public QAbstractTableModel
{
public:
CustomModel(const QStringList &headers, QObject *parent = nullptr);
~CustomModel();
//让View知道这个表总共有几行
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override;
//让View知道这个表总共有几列
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override;
//根据role的不同 返回想要展示的数据
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
//顾名思义,根据role与section返回表头信息
virtual QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
void setData(const std::vector<CustomData> datas);
private:
std::vector<CustomData> m_vecDataSourc;
QStringList m_headers;
};
CustomModel.cpp
#include "CustomModel.h"
#include <QColor>
CustomModel::CustomModel(const QStringList &headers, QObject * parent)
{
m_headers = headers;
}
CustomModel::~CustomModel()
{
}
int CustomModel::rowCount(const QModelIndex & parent) const
{
return m_vecDataSourc.size();
}
int CustomModel::columnCount(const QModelIndex & parent) const
{
return m_headers.size();
}
QVariant CustomModel::data(const QModelIndex & index, int role) const
{
if (Qt::DisplayRole == role)
{
const CustomData& data = m_vecDataSourc.at(index.row());
if (index.column() == 0)
{
return data._name;
}
else if (index.column() == 1)
{
return data._id;
}
else if (index.column() == 2)
{
return data._grade;
}
}
else if (Qt::BackgroundRole == role)
{
const CustomData& data = m_vecDataSourc.at(index.row());
if (data._grade >= 90)
{
return QColor(0, 255, 0, 255);
}
else if (data._grade >= 80)
{
return QColor(0, 255, 255, 255);
}
else
{
return QColor(255, 0, 0, 255);
}
}
return QVariant();
}
QVariant CustomModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (Qt::DisplayRole == role)
{
if (Qt::Horizontal == orientation)
{
return m_headers.at(section);
}
else
{
return (section + 1);
}
}
return QVariant();
}
Qt::ItemFlags CustomModel::flags(const QModelIndex & index) const
{
return QAbstractTableModel::flags(index);
}
void CustomModel::setData(const std::vector<CustomData> datas)
{
beginResetModel();
m_vecDataSourc = datas;
endResetModel();
}
MainWindow.h
#pragma once
#include <QWidget>
#include <QTableView>
class CustomModel;
class MainWindow : public QWidget
{
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
QTableView *m_pTableView;
CustomModel *m_pCustomModel;
};
MainWindow.cpp
#include "MainWindow.h"
#include "Models/CustomModel.h"
MainWindow::MainWindow(QWidget * parent) : QWidget(parent)
{
m_pTableView = new QTableView(this);
m_pTableView->setGeometry(100, 50, 400, 300);
QStringList headers;
headers << u8"姓名" << u8"学号" << u8"成绩";
m_pCustomModel = new CustomModel(headers);
m_pTableView->setModel(m_pCustomModel);
{
std::vector<CustomData> vecDatas;
CustomData st1;
st1._name = u8"李明";
st1._id = "123456";
st1._grade = 92;
vecDatas.push_back(st1);
CustomData st2;
st2._name = u8"张三";
st2._id = "223456";
st2._grade = 80;
vecDatas.push_back(st2);
CustomData st3;
st3._name = u8"李四";
st3._id = "323456";
st3._grade = 66;
vecDatas.push_back(st3);
m_pCustomModel->setData(vecDatas);
}
}
MainWindow::~MainWindow()
{
}
这里创建了继承自QAbstractTableModel的自定义model类,通过重写rowCount、columnCount、data、headerData等函数为整套系统提供咨询。在data函数中返回了要展示的信息,并且根据成绩显示不同的背景色。
3 一个自定义的Delegate
这个时候,如果我们想新增加一列,让这一列根据分数来显示不同朵数的小花花, 光靠默认的delegate是无法实现这个需求的, 这个时候就需要自己实现一个能绘制小花花的delegate了!
一般继承QStyledItemDelegate类来实现自定义的Delegate,在我看来,Delegate实现自定义绘制的方式有2种:
1)第一种,直接在paint函数里面绘制,比较像在paintEvent里面做的事情。
// painting
void paint(QPainter *painter,
const QStyleOptionViewItem &option, const QModelIndex &index) const override;
2)第二种,创建一个继承自QWidget的控件,从函数名可以看出,这个控件是要在编辑的时候才会出现,比如说我们以这种方式返回一个CheckBox,那么必须打开单元格的editable属性,并且双击单元格进入编辑模式的时候才会出现。所以如果想要以常态显示,可以采用第一种方法直接绘制。
// editing
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
话不多说,让我们看看代码里的实现:
CustomDelegate.h
#pragma once
#include <QStyledItemDelegate>
#include <QPixmap>
class CustomDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
CustomDelegate(QObject *parent = nullptr);
~CustomDelegate();
void paint(QPainter *painter,
const QStyleOptionViewItem &option, const QModelIndex &index) const override;
protected:
bool editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option, const QModelIndex &index) override;
private:
void _drawFlowers(QPainter * painter, int flowerCnt, const QRect& totalRect) const ;
private:
QPixmap m_flowerPixmap;
};
CustomDelegate.cpp
#include "CustomDelegate.h"
#include "Models/CustomModel.h"
#include <QPainter>
#include <QMouseEvent>
#include <QApplication>
#include <QToolTip>
CustomDelegate::CustomDelegate(QObject * parent) : QStyledItemDelegate(parent)
{
m_flowerPixmap = QPixmap("Images/flower.png");
}
CustomDelegate::~CustomDelegate()
{
}
static int _getFlowerCount(int grade)
{
int flowerCnt = 0;
if (grade >= 90)
{
//5朵小红花
flowerCnt = 5;
}
else if (grade >= 80)
{
//4朵小红花
flowerCnt = 4;
}
else if (grade >= 60)
{
//3朵小红花
flowerCnt = 3;
}
else
{
flowerCnt = 1;
}
return flowerCnt;
}
void CustomDelegate::_drawFlowers(QPainter * painter, int flowerCnt, const QRect& totalRect) const
{
int flowerSize = totalRect.height() - 4;
QPixmap flowerPixScaled = m_flowerPixmap.scaled(flowerSize, flowerSize, Qt::KeepAspectRatio);
int xOffset = 5;
for (int i = 0; i < flowerCnt; ++i)
{
QRect flowerRect = QRect(totalRect.left() + flowerPixScaled.width() * i + xOffset + i + 1, totalRect.top() + 2, flowerPixScaled.width(), flowerPixScaled.height());
painter->drawPixmap(flowerRect, flowerPixScaled);
}
}
void CustomDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
{
QStyleOptionViewItem viewOption(option);
initStyleOption(&viewOption, index);
if (option.state.testFlag(QStyle::State_HasFocus))
viewOption.state = viewOption.state ^ QStyle::State_HasFocus;
QStyledItemDelegate::paint(painter, viewOption, index);
int grade = index.data(Qt::UserRole).toInt();
int flowerCnt = _getFlowerCount(grade);
_drawFlowers(painter, flowerCnt, option.rect);
}
bool CustomDelegate::editorEvent(QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index)
{
bool repaint = false;
QMouseEvent *pMouseEvent = static_cast<QMouseEvent *>(event);
QRect decorationRect = option.rect;
QPoint mousePos = pMouseEvent->pos();
//还原鼠标样式
QApplication::restoreOverrideCursor();
if (event->type() == QEvent::MouseMove && decorationRect.contains(mousePos))
{
QApplication::setOverrideCursor(Qt::PointingHandCursor);
int grade = index.data(Qt::UserRole).toInt();
QToolTip::showText(pMouseEvent->globalPos(), QString::number(grade) + u8"分!");
repaint = true;
}
return repaint;
}
在Mainwindow构造函数中添加:
m_pCustomDelegate = new CustomDelegate();
m_pTableView->setItemDelegateForColumn(3, m_pCustomDelegate);
m_pTableView->resizeColumnToContents(3); //使模型返回的SizeHint生效
m_pTableView->setMouseTracking(true); //在这里相当于启用mousemove事件
在CustomModel中添加:
QVariant CustomModel::headerData(int section, Qt::Orientation orientation, int role) const
{
//...
if (Qt::SizeHintRole == role)
{
if (Qt::Horizontal == orientation && section ==3)
{
return QSize(250, 40);
}
}
return QVariant();
}
因为view的单元格宽高是从header获取的,所以我们在headerData函数中为第三行返回了一个SizeHint(这里因为是Horizontal的,所以只有宽度生效了),让单元格能够装得下小花花。
我们在paint的时候通过咨询模型获得当前单元格的成绩数据,然后根据成绩在第4列画了不同朵数的小花花,并在鼠标放上去的时候提示成绩。
我们作为一个权限狗,是不是应该具备修改成绩的能力?接下来让看看另一种形式,创建一个继承自QWidget的Editor。
创建Editor的方法也有很多, 可以重写createEditor函数,但是在QItemEditorFactory中,Qt已经为我们实现了比较常用的几种Editor:
首先打开成绩列的ItemIsEditable属性,然后data函数在EditRole中以QString的形式返回成绩,就可以编辑成绩了, 当然,想要使编辑后的成绩真正修改到我们源数据里的成绩,还需要重写setData函数。
QVariant CustomModel::data(const QModelIndex & index, int role) const
{
if (Qt::DisplayRole == role)
{
//...
}
else if (Qt::BackgroundRole == role)
{
//...
}
else if (Qt::UserRole == role)
{
const CustomData& data = m_vecDataSourc.at(index.row());
return data._grade;
}
else if (Qt::EditRole == role && index.column() == 2)
{
//如果这里不返回成绩,那么在双击单元格后编辑框里是无内容的,如果返回一个Int,那么会默认创建一个QSpinBox而不是QLineEdit
const CustomData& data = m_vecDataSourc.at(index.row());
return QString::number(data._grade);
}
return QVariant();
}
Qt::ItemFlags CustomModel::flags(const QModelIndex & index) const
{
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
if (index.column() == 2)
{
//如果是成绩列,则让单元格变得可编辑
flags |= Qt::ItemIsEditable;
}
return flags;
}
bool CustomModel::setData(const QModelIndex & index, const QVariant & value, int role)
{
if (index.isValid() && index.column() == 2 && Qt::EditRole == role)
{
int newGrade = value.toInt();
if (newGrade > 100 || newGrade < 0)
{
return false;
}
CustomData& data = m_vecDataSourc[index.row()];
data._grade = newGrade;
return true;
}
return false;
}
最终达到了我们想要的权限效果:
4 结语
以上是个人对于Qt自定义表格的理解,如有不准确的地方,还请各位看官指出。
下期我将从源代码的视角来进一步理解Qt这一套Model/View实现的原理,看看Qt的大佬们是如何让这套系统稳定运行起来的。