QRowTable表格控件(二)-红涨绿跌

原文链接:QRowTable表格控件(二)-红涨绿跌

一、开心一刻

一天,五娃和六娃去跟蛇精决斗,决斗前有这样一段对话。

五娃:“妖精!今天我俩就要消灭你!今天就是你的死期!”

蛇精:“呵呵呵,真是可笑。你们自己个儿都是从树上长出来的,凭什么叫我妖精?!”

五娃:“你也说了,我们是从树上长出来的,是葫芦变的,自然不是妖精。”

蛇精:“你们不是妖精,难道还是神仙了,再难不成你把自己当人了?”

五娃和六娃异口同声道:“哈哈哈哈哈哈!你说对了,我就是人,是植!物!人!”

二、概述

最近工作比较忙,不过还是抽时间把这个表格组件继续完善下去。

Qt的自带的表格控件非常强大,支持各种方便操作,上一篇文章QRowTable表格控件-支持hover整行、checked整行、指定列排序等介绍了一个简单的demo,主要是做一个股票表格控件,而他天然的就是支持hover和checked行特性,对Qt比较熟悉的同学可能都知道Qt其实有两个接口,可以设置交互行为为选择行。

接口就是这两个货了。

setSelectionBehavior(QAbstractItemView::SelectRows);
setSelectionMode(QTableView::SingleSelection);//不能多选

既然Qt已经有接口了,为什么我们还要自己写这个控件呢!

尝试过用Qt的接口设置相关行为的同学我相信最后都会发现是什么原因,这里我直接把原因放出来。

  1. 首先我们的需求是每一行的文字颜色是不一样的,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>());
  1. 最重要的时候我们自己还是实现了一堆的小需求,下一小节我们来一个个分析

三、效果展示

以下是红涨绿跌效果图,实现功能一样。

UI展现形式不一样,但是都实现了红涨绿跌

  1. 背景色不同
  2. 排序效果不同

纯白版






腹黑版






四、任务需求

看过效果图之后,有没有发现我们这里的表格控件和Qt自带的控件有什么区别呢?其实这里我们要达到gif展示的那种效果,还是使用了很多实现技巧。

控件都包含哪些功能呢?

  1. 指定列排序

指定列排序,这个版本的代码经过了优化,比QRowTable表格控件-支持hover整行、checked整行、指定列排序等这篇文章中将的版本要更优雅一些。

  1. 排序图标

纯白版使用的是Qt排序图标

腹黑版排序图标使我们自己去绘制的,并且绘制水平表头时文本有特殊处理

  1. 列内容对其方式

看到这里的同学不放自己也可以先思考下,看这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 clash 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);
    }
}

八、相关文章

  1. Qt实现表格控件-支持多级列表头、多级行表头、单元格合并、字体设置等

  2. Qt高仿Excel表格组件-支持冻结列、冻结行、内容自适应和合并单元格

  3. 属性浏览器控件QtTreePropertyBrowser编译成动态库(设计师插件)

  4. 超级实用的属性浏览器控件--QtTreePropertyBrowser

  5. Qt之表格控件蚂蚁线

  6. QRowTable表格控件-支持hover整行、checked整行、指定列排序等





如果您觉得文章不错,不妨给个打赏,写作不易,感谢各位的支持。您的支持是我最大的动力,谢谢!!!














很重要--转载声明

  1. 本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords

  2. 如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。


posted @ 2019-07-18 01:02 朝十晚八 阅读(...) 评论(...) 编辑 收藏

返回顶部