Qt自定义表格实现与源码阅读一

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的大佬们是如何让这套系统稳定运行起来的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值