一、开心一刻
一天,五娃和六娃去跟蛇精决斗,决斗前有这样一段对话。
五娃:“妖精!今天我俩就要消灭你!今天就是你的死期!”
蛇精:“呵呵呵,真是可笑。你们自己个儿都是从树上长出来的,凭什么叫我妖精?!”
五娃:“你也说了,我们是从树上长出来的,是葫芦变的,自然不是妖精。”
蛇精:“你们不是妖精,难道还是神仙了,再难不成你把自己当人了?”
五娃和六娃异口同声道:“哈哈哈哈哈哈!你说对了,我就是人,是植!物!人!”
二、概述
最近工作比较忙,不过还是抽时间把这个表格组件继续完善下去。
Qt的自带的表格控件非常强大,支持各种方便操作,上一篇文章QRowTable表格控件-支持hover整行、checked整行、指定列排序等介绍了一个简单的demo,主要是做一个股票表格控件,而他天然的就是支持hover和checked行特性,对Qt比较熟悉的同学可能都知道Qt其实有两个接口,可以设置交互行为为选择行。
接口就是这两个货了。
setSelectionBehavior(QAbstractItemView::SelectRows);
setSelectionMode(QTableView::SingleSelection);//不能多选
既然Qt已经有接口了,为什么我们还要自己写这个控件呢!
尝试过用Qt的接口设置相关行为的同学我相信最后都会发现是什么原因,这里我直接把原因放出来。
- 首先我们的需求是每一行的文字颜色是不一样的,Qt表格默认的行为是:表格cell如果被选中,前景色和背景色则是从高亮role中拿到的色值,以下代码是一个示例代码,其中的QPalette::HighlightedText和QPalette::Highlight就是存储单元格被选中时,绘制的颜色。
view_option.palette.setColor(QPalette::HighlightedText, index.data(Qt::ForegroundRole).value<QColor>());
view_option.palette.setColor(QPalette::Highlight, index.data(Qt::BackgroundRole).value<QColor>());
- 最重要的时候我们自己还是实现了一堆的小需求,下一小节我们来一个个分析
三、效果展示
以下是红涨绿跌效果图,实现功能一样。
UI展现形式不一样,但是都实现了红涨绿跌
- 背景色不同
- 排序效果不同
纯白版
腹黑版
四、任务需求
看过效果图之后,有没有发现我们这里的表格控件和Qt自带的控件有什么区别呢?其实这里我们要达到gif展示的那种效果,还是使用了很多实现技巧。
控件都包含哪些功能呢?
- 指定列排序
指定列排序,这个版本的代码经过了优化,比QRowTable表格控件-支持hover整行、checked整行、指定列排序等这篇文章中将的版本要更优雅一些。
- 排序图标
纯白版使用的是Qt排序图标
腹黑版排序图标使我们自己去绘制的,并且绘制水平表头时文本有特殊处理
- 列内容对其方式
看到这里的同学不放自己也可以先思考下,看这3种需求的实现方式,有了大致思路后在继续往下看。
这样带着思路看,理解起来应该更加的容易。
五、指定列排序
还记得上一篇文章QRowTable表格控件-支持hover整行、checked整行、指定列排序等中怎么禁用指定列排序吗?忘记的同学可以到这篇文章中去熟悉下,我记着应该是发现排序列不允许被排序时,直接禁用排序功能。
本篇文章我们使用了一种更加优雅的方式来阻止指定列排序。
首先我们重写了表头组件,并且重写了mouseReleaseEvent这个函数,因为这个函数中才触发了排序,因此这个函数中足以过滤掉不让排序的列。
class QRowHeader : public QHeaderView
{
Q_OBJECT
public:
QRowHeader(Qt::Orientation orientation, QWidget * parent = nullptr);
public:
//设置是否支持排序
void SetSortEnable(int logicalIndex, bool enable);
signals:
void MouseMove();
protected:
virtual void mouseMoveEvent(QMouseEvent * event) override;
virtual void mouseReleaseEvent(QMouseEvent *e) override;
virtual void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const override;
private:
QMap<int, bool> m_Indicator;
};
然后代码是这样处理的,代码量不大,就是判断鼠标点击的位置如果是禁止排序的列,我们直接把事件循环中断啦!!!
是不是很坏,哈哈哈
void QRowHeader::mouseReleaseEvent(QMouseEvent * event)
{
int column = logicalIndexAt(event->pos().x());
if (m_Indicator.contains(column) && m_Indicator[column] == false)
{
return;
}
QHeaderView::mouseReleaseEvent(event);
}
六、排序
上边讲完了怎么去阻止排序,重写了QHeaderView。
virtual void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const override;
里边有paintSection这样的函数,他就是绘制表头一个单元格所产生的回调。
在这个函数里边首先调用父窗口绘制表头,然后如果发现自己是排序列时,顺道去绘制一个排序图标.
代码很容易理解,绘制朝上还是朝下的图标取决于我们当前的排序方式
void QRowHeader::paintSection(QPainter * painter, const QRect & rect, int logicalIndex) const
{
...
//绘制三角形
if (sortIndicatorSection() == logicalIndex)
{
QRect indicator = rect;
indicator.setLeft(indicator.right() - 6);
indicator.setHeight(10);
indicator.moveTop((rect.height() - indicator.height()) / 2);
indicator.moveLeft(indicator.left() - 10);//距离左边界10像素
if (sortIndicatorOrder() == Qt::AscendingOrder)
{
painter->drawPixmap(indicator, QPixmap(":/QRowTable/down_arrow.png.png"));
}
else if (sortIndicatorOrder() == Qt::DescendingOrder)
{
painter->drawPixmap(indicator, QPixmap(":/QRowTable/up_arrow.png.png"));
}
}
}
绘制列头中的项时,如果当前列是排序列,那么这里就需要绘制排序图标。如果刚好这一列的文字是右对齐呢,正常情况下我们的图标还有文字可能是没有问题的。
如果该列很窄,那么排序图标和列cell中的文字可能会重叠
既然有概率重叠,这里我们就需要处理这个异常,当出现上述这种情况时,我们需要改变原有的文字绘制区域,不让他在图标的绘制区域绘制文本。
还是重写paintSection方法,这个是绘制列头一个单元格的方法。
看如下代码,首先我们填充了这个cell,为什么呢?不着急回答这个问题,接着往下看,我们把原有的rect向左便宜了16个像素,然后绘制列头cell。
void QRowHeader::paintSection(QPainter * painter, const QRect & rect, int logicalIndex) const
{
painter->fillRect(rect.adjusted(-1, 0, -1, -1), QColor("#212121"));
painter->save();
QRect r = rect;
if (sortIndicatorSection() == logicalIndex)
{
r.adjust(0, 0, -16, 0);
}
QHeaderView::paintSection(painter, r, logicalIndex);
painter->restore();
painter->setPen(QColor("#2B2B2B"));
painter->drawRect(rect.adjusted(-1, 0, -1, -1));
...
这样会有什么问题?仔细想想,文字绘制没问题,其实有问题的是背景色绘制会发现错误
因此我们在函数开头先把列头cell背景色进行了填充,这个背景色其实就是列头cell的背景色
setStyleSheet("QTableView{background:#333333;}"
"QHeaderView{background:#212121;color:gray;}"
"QHeaderView:section{padding:6px;border:0;background:#212121;color:gray;}");
上边是这个组件的qss样式,表头cell的背景色其实就是#2212121,因此填充区域我们也使用了这个色值,最终达到我们预期的效果。
这里插一句:绘制时一定要注意绘制的顺序,否则我们自定义的绘制可能会和Qt自己的绘制有冲突。这里尽量考虑清楚,免得产生冲突。
当列表头被点击时,QRowHeader对象会发出列cell被点击信号sectionClicked,这个信号也是从鼠标弹起函数mouseReleaseEvent中触发。
connect(m_pHHeader, &QRowHeader::sectionClicked, m_pFilter, &QFilterModel::setFilterKeyColumn);
connect(m_pHHeader, &QRowHeader::sectionClicked, this, [this](int column){
if (column == 0 || column == 1 || column == 2)
{
GetFilterModel()->SetCompareType(QFilterModel::CT_INT);
}
else
{
GetFilterModel()->SetCompareType(QFilterModel::CT_STRING);
}
});
sectionClicked信号触发后,这里干了两件事:设置当前的排序列和当前的排序方式。
SetCompareType
接口用于设置当前排序方式。
目前这个组件支持3中排序方式,数字、百分比和字符串
bool QFilterModel::lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const
{
if (m_eType == CT_INT)
{
double l = source_left.data().toDouble();
double r = source_right.data().toDouble();
return l < r;
}
else if (m_eType == CT_PERCENT)
{
double l = source_left.data(Qt::UserRole + 2).toString().remove("%").toDouble();
double r = source_right.data(Qt::UserRole + 2).toString().remove("%").toDouble();
return l < r;
}
else
{
QString l = source_left.data().toString();
QString r = source_right.data().toString();
return l < r;
}
}
这里需要说明一下,百分比为什么要单独拿出来分一类,设计之初,百分比和数字是放在一类中去比较的,但是后来发现百分比没有办法存储在double中,因此添加了百分比分类。这里也不强制非要添加一个新类,其他能实现比较需求的办法均可。
七、列对其方式
Qt的文字对其方式有如下这么多,然后我们这个组件支持比较主流的集中排序方式,分别是:水平居左、水平居中、水平居右
水平居右时,如果当前列时排序列,我们文字绘制的位置区域不能包含排序图标的大小,否则文字可能和图标重叠
enum AlignmentFlag {
AlignLeft = 0x0001,
AlignLeading = AlignLeft,
AlignRight = 0x0002,
AlignTrailing = AlignRight,
AlignHCenter = 0x0004,
AlignJustify = 0x0008,
AlignAbsolute = 0x0010,
AlignHorizontal_Mask = AlignLeft | AlignRight | AlignHCenter | AlignJustify | AlignAbsolute,
AlignTop = 0x0020,
AlignBottom = 0x0040,
AlignVCenter = 0x0080,
AlignBaseline = 0x0100,
// Note that 0x100 will * with Qt::TextSingleLine = 0x100 due to what the comment above
// this enum declaration states. However, since Qt::AlignBaseline is only used by layouts,
// it doesn't make sense to pass Qt::AlignBaseline to QPainter::drawText(), so there
// shouldn't really be any ambiguity between the two overlapping enum values.
AlignVertical_Mask = AlignTop | AlignBottom | AlignVCenter | AlignBaseline,
AlignCenter = AlignVCenter | AlignHCenter
};
控件之外,我们通过SetAlignment接口设置了该列的排序方式,排序方式对列的内容和列头都起作用。也就是说列头和列内容排序方式是一致的。
model->SetAlignment(0, Qt::AlignRight | Qt::AlignVCenter);
model->SetAlignment(1, Qt::AlignRight | Qt::AlignVCenter);
model->SetAlignment(2, Qt::AlignRight | Qt::AlignVCenter);
model->SetAlignment(3, Qt::AlignLeft | Qt::AlignVCenter);
model->SetAlignment(4, Qt::AlignLeft | Qt::AlignVCenter);
model->SetAlignment(5, Qt::AlignLeft | Qt::AlignVCenter);
列头绘制时,会通过headerData接口拿文字对其方式,然后这里只需要返回之前使用SetAlignment接口设置的对其方式即可。
QVariant QRowModel::headerData(int section, Qt::Orientation orientation, int role /*= Qt::DisplayRole*/) const
{
if (Qt::TextAlignmentRole == role)
{
//Q::AlignLeft | Qt::AlignVCenter
auto iter = m_AlignmentList.find(section);
if (iter != m_AlignmentList.end())
{
return iter->second;
}
return Qt::AlignCenter;
//return m_AlignmentList.at(section);
}
return QStandardItemModel::headerData(section, orientation, role);
}
补充
重新换了一种方式实现第五节的mouseReleaseEvent函数。
之前的方法在开启了列拖拽之后会出现问题
void QRowHeader::mouseReleaseEvent(QMouseEvent * event)
{
int column = logicalIndexAt(event->pos().x());
if (m_Indicator.contains(column) && m_Indicator[column] == false)
{
setSectionsClickable(false);
QHeaderView::mouseReleaseEvent(event);
setSectionsClickable(true);
}
else
{
QHeaderView::mouseReleaseEvent(event);
}
}
八、相关文章
很重要--转载声明
本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords
如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。