QTreeView自绘实现酷炫样式

发布时间 2023-08-17 21:16:29作者: 师从名剑山

本篇文章结合笔者的经历,介绍一种通过重写QTreeView绘制事件,使用QPainter来实现好看的列表的方式。

导语

Hi?,各位读者朋友,大家好。相信大家在日常的工作中,经常会接触到QTreeView这个控件吧!

QTreeView,顾名思义,就是一种树形的控件,在我们需要做类似于文件列表的视图时,是一个不错的选择。然而,仅通过设置样式表,往往无法完全满足我们的需求。迫不得已,我们只能选择自实现QTreeView的绘制事件,通过画笔,逐个绘制我们想要的效果。

关于QTreeView的样式表部分,Qt官方给出了一些示例:Customizing QTreeView , 本文不作具体介绍,感兴趣的读者可以自行观看。

话不多说,咱们先上效果图:
QtWidgetsApplication1_Mm5guzma7Z.gif

接下来,咱们通过一下几个步骤,逐个分析,怎么达到上图的效果。

  1. 整个控件的背景色
  2. 单元格的效果
  3. 单击和双击选择的效果

整个控件的背景色

话不多说,码来!


void MyTreeView::paintEvent(QPaintEvent* event)
{
    int count = m_pModel->rowCount(this->rootIndex());
    QPainter painter(this->viewport());

    painter.setPen(Qt::NoPen);
    painter.setBrush(QColor(40, 44, 52));
    painter.drawRect(this->viewport()->rect());
    
    // 列表为空时,绘制空状态
    if (count == 0) {
        QPoint startPos(this->width() / 2 - 72, this->height() / 2);

        QFont font("Microsoft YaHei UI");
        font.setPixelSize(12);
        painter.setFont(font);
        painter.setPen(QColor(153, 154, 161));
        painter.setBrush(Qt::NoBrush);
        painter.drawText(startPos.x(), startPos.y() + 64 + 8, 148, 32, Qt::AlignCenter | Qt::TextWordWrap, "文件夹为空");

        return;
    }

    QTreeView::paintEvent(event);
}

在paintEvent中,首先在整个QTreeView的区域,绘制了一个背景色。其次判断model中是否没有数据,如果没有数据,则居中绘制一个空状态。


单元格的效果

绘制单元格,我们通过drawRow去为每一个单元格绘制我们想要的效果。

void MyTreeView::drawRow(QPainter* painter, const QStyleOptionViewItem& options, const QModelIndex& index) const
{
    if (!index.isValid())
        return;

    painter->save();
    painter->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform);
    QString path = m_pModel->filePath(index);
    QFileInfo file(path);
    QRect rect = options.rect;

    painter->setPen(Qt::NoPen);

    //判断状态
    bool bNormal = false, bDClick = false, bClick = false;

    if (path == m_selectPath)
    {
        bDClick = true;
    } else if (index == currentIndex()) {
        bClick = true;
    } else {
        bNormal = true;
    }
    
    // 计算当前Index的目录相对root目录的层级
    int rootPathSize = m_pModel->rootPath().size();
    int indexPathSize = m_pModel->filePath(index).size();
    QString relativePath = m_pModel->filePath(index).mid(rootPathSize, indexPathSize - rootPathSize);
    int indexTier = relativePath.split('/').size() - 1;
    // 当前index的缩进
    int indexIndentation = (indexTier - 1) * TierIndentation + 26;

    //QString theme = theme_manager::Instance()->GetThemeName();

#pragma region 绘制底色
    if (bNormal)
    {
        QBrush brush = QBrush(QColor(40, 44, 52));
        painter->setBrush(brush);
        painter->drawRect(rect);
    }
#pragma endregion

#pragma region 绘制选中样式

    if (!file.isDir() && bDClick)
    {
        QBrush brush = QBrush(QColor(50, 56, 66));
        painter->setBrush(brush);
        painter->drawRect(rect);
    }

#pragma endregion

#pragma region 绘制单击样式
    if (bClick)
    {
        QBrush brush = QBrush(QColor(44, 49, 58));
        painter->setBrush(brush);
        painter->drawRect(rect);
    }
#pragma endregion

#pragma region 绘制图标
    // 画图标
    int iconWidth = file.isDir() ? FOLDER_WIDTH : ARROW_WIDTH;
    int iconHeight = file.isDir() ? FOLDER_HEIGHT : ARROW_HEIGHT;
    
    // (iconWidth - ARROW_HEIGHT)是绘制普通文件时,应该多往右边移动一点
    int xIcon = rect.x() + indexIndentation - (iconWidth - FOLDER_WIDTH);
    int yIcon = rect.y() + (SectionHeight - iconHeight) / 2;
    QRect iconRec(QPoint(xIcon, yIcon), QSize(iconWidth, iconHeight));

    if (file.isDir())
    {
        // 绘制文件夹展开与收起样式
        isExpanded(index) ? painter->drawImage(iconRec, m_ImageTreeFolderExpand)
            : painter->drawImage(iconRec, m_ImageTreeFolder);
    } 
    else
    {
        QRect iconRec(QPoint(xIcon, yIcon), QSize(14, 14));
        painter->drawImage(iconRec, m_ImageCppFile);
    }
#pragma endregion

#pragma region 绘制文字
    QString text = index.data(Qt::DisplayRole).toString();
    // x坐标为当前层次缩进+图标宽度(20)+文字与图标间距(5)
    QRect leverRect(QPoint(rect.x() + indexIndentation + FOLDER_WIDTH + 4, rect.y()),
                    QSize(rect.width(), rect.height()));

    // 绘制文字
    QColor themeTextColor(204, 204, 204);

    painter->setFont(m_textFont);
    painter->setBrush(Qt::NoBrush);
    painter->setPen(QPen(themeTextColor));

    QFontMetrics fm(m_textFont);
    text = fm.elidedText(text, Qt::ElideRight, m_width);

    QRect boundingRect;
    painter->drawText(leverRect, Qt::AlignLeft | Qt::AlignVCenter, text, &boundingRect);
#pragma endregion

#pragma region 绘制箭头
    //绘制箭头
    bool bExpand = this->isExpanded(index);
    int xArrow = rect.x() + MARGIN_WIDTH;
    int yArrow = rect.topLeft().y() + (SectionHeight - 8) / 2;
    QRect border(xArrow, yArrow, 8, 8);
    bool bDir = file.isDir(), bEmpty = true;
    if (bDir)
    {
        QDir dir(path);
        bEmpty = dir.isEmpty();
    }

    if (bDir && !bExpand && !bEmpty)
    {
        painter->drawImage(border, m_ImageExpand);
    } else if (bDir && bExpand && !bEmpty)
    {
        painter->drawImage(border, m_ImageClose);
    }
#pragma endregion

    painter->restore();
}

?️绘制的顺序,需要根据各个部分实际所在的图层顺序去绘制,比如说先要绘制底色,再去绘制上面的内容。


单击选择和双击选择

这一个部分,我们通过重载 mouseDoubleClickEventmousePressEvent 来监控鼠标的事件。


void MyTreeView::mousePressEvent(QMouseEvent* event)
{
    QTreeView::mousePressEvent(event);

    QModelIndex index = indexAt(event->pos());
    if (!index.isValid())
        return;

    //是否为文件夹
    const auto& path = m_pModel->filePath(index);
    QFileInfo info(path);
    if (!info.isDir())
        return;

    //是否在箭头区域
    QPoint point = event->pos();
    int  width = this->viewport()->width();
    QRect rect(width - MARGIN_WIDTH - ARROW_WIDTH, SectionHeight - MARGIN_HEIGHT - ARROW_HEIGHT, ARROW_WIDTH, ARROW_HEIGHT);
    bool bContain = rect.contains(QPoint(event->pos().x(), event->pos().y() % SectionHeight));
    if (!bContain)
        return;

    // 如果目录为空,不触发箭头交互效果
    QDir tmpDir(path);
    if (tmpDir.isEmpty())
        return;

    //展开收缩节点
    this->isExpanded(index) ? this->collapse(index)
        : this->expand(index);

}

void MyTreeView::mouseDoubleClickEvent(QMouseEvent* event)
{
    QModelIndex index = this->indexAt(event->pos());
    if (index != this->currentIndex())
        return;

    // 处理普通文件
    QString path = m_pModel->filePath(index);
    if (!m_pModel->isDir(index)) {
        m_selectPath = path;

    }

    update();
    QTreeView::mouseDoubleClickEvent(event);
}

通过上面的几个函数,我们就可以实现自定义绘制QTreeView的需求,如果还需要绘制更多的东西,则只要自己往上面添加绘制的区域即可。


一些碎碎念

本来觉得绘制一个QTreeView比较麻烦,包括在这篇博客的时候仍然是这种感觉,但是通过输出这篇博客,发现其实也很简单(当然,不考虑绘制效率的情况下)。就像我刚开始接触Qt时,总觉得自绘是一件很麻烦、很困难的事,想着全部都要自己去画。

等到后面真正接触到的时候,更慌了。但没办法,任务来了,怎么办?硬着头皮上呗?,一顿捯饬之后,我发现,其实也没那么难。

所以说,刚接触Qt的同学,如果样式表达不到自己需求,就勇敢的去尝试自绘吧!?‍?️

创作不易,如果对您有帮助,点赞、关注、收藏支持一下!不甚感激!?