news 2026/4/23 10:12:21

QListView自定义委托绘制完整示例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QListView自定义委托绘制完整示例

手把手教你实现 QListView 高度自定义绘制:从模型到委托的完整实践

你有没有遇到过这样的需求?
一个简单的任务列表,不仅要显示标题,还要根据类型用不同颜色标识,左侧加个状态徽章,右侧留出操作箭头,鼠标悬停时有微妙反馈,选中后高亮——而且数据量可能上千条,滚动必须流畅。

这时候,传统的“一堆QLabelQHBoxLayout”方式就捉襟见肘了:内存爆炸、卡顿、维护困难。
QListView+ 自定义委托,正是为这类场景量身打造的解决方案。

本文不讲理论套话,带你从零开始搭建一套完整的自定义绘制系统,涵盖模型设计、委托绘制、视觉优化和实战技巧,让你真正掌握QListView的高级玩法。


为什么非要用 QListView?别再堆 Widget 了!

先说清楚一件事:QListView不是一个容器控件,它是个“虚拟化渲染器”

什么意思?

假设你要展示 10,000 条日志。如果用垂直布局放 10,000 个QWidget,那你的程序早就崩了。但QListView只会为当前屏幕上可见的几十个条目创建绘制对象,其余的“看不见”,就不画。

这就是所谓的虚拟滚动(Virtual Scrolling)—— Qt 模型-视图架构的核心优势。

它把三件事彻底分开:
-数据在哪?→ 模型(Model)
-长啥样?→ 委托(Delegate)
-怎么排布?点哪里?→ 视图(View)

这种解耦让你可以自由替换任意一环,而不影响其他部分。比如同一个任务模型,既能用在列表里,也能塞进下拉框或树形结构中。

✅ 小贴士:如果你还在用QVBoxLayout动态添加控件做列表,请立刻停下来。这不是“灵活”,是给自己挖坑。


先搭骨架:构建支持多角色的数据模型

要让委托知道该怎么画,模型得能提供足够的信息。

我们来写一个典型的任务模型,不只是返回字符串,而是携带类型、时间戳等额外数据:

class TaskModel : public QAbstractListModel { Q_OBJECT public: // 自定义角色,用于传递非显示数据 enum TaskRoles { TitleRole = Qt::DisplayRole, // 主文本(兼容默认行为) TypeRole = Qt::UserRole + 1, // 类型:"info" / "warning" / "error" TimestampRole // 时间戳,可用于排序 }; Q_ENUM(TaskRoles) int rowCount(const QModelIndex &parent = {}) const override { return parent.isValid() ? 0 : m_tasks.size(); } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if (!index.isValid() || index.row() >= m_tasks.count()) return {}; const auto &task = m_tasks.at(index.row()); switch (role) { case TitleRole: return task.title; case TypeRole: return task.type; case TimestampRole: return task.timestamp; default: return {}; } } bool setData(const QModelIndex &index, const QVariant &value, int role) override { if (!index.isValid() || role != TitleRole) return false; m_tasks[index.row()].title = value.toString(); emit dataChanged(index, index, {role}); // 只刷新这一项 return true; } QHash<int, QByteArray> roleNames() const override { QHash<int, QByteArray> roles; roles[TitleRole] = "title"; roles[TypeRole] = "type"; roles[TimestampRole] = "timestamp"; return roles; } void addTask(const QString &title, const QString &type) { beginInsertRows({}, m_tasks.size(), m_tasks.size()); m_tasks.append({title, type, QDateTime::currentSecsSinceEpoch()}); endInsertRows(); } private: struct Task { QString title; QString type; qint64 timestamp; }; QList<Task> m_tasks; };

重点来了:

  • roleNames()让你在 QML 中可以直接写model.type,非常方便。
  • 插入数据时使用beginInsertRows()/endInsertRows(),这是线程安全和索引同步的关键。
  • 修改数据后调用dataChanged(),通知视图局部重绘,而不是整个刷新。

这个模型现在不仅能告诉委托“画什么文字”,还能说清“这是警告还是错误”。


核心突破:手写一个全能型自定义委托

接下来才是重头戏 —— 绘制逻辑。

我们继承QStyledItemDelegate,因为它比QItemDelegate更尊重系统样式,适配暗色主题也更轻松。

#include <QStyledItemDelegate> #include <QPainter> #include <QApplication> class CustomItemDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit CustomItemDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {} QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { return QSize(200, 48); // 固定高度,宽度随容器 } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { // 开启抗锯齿,线条更平滑 painter->setRenderHint(QPainter::Antialiasing); // 获取数据 QString title = index.data(Qt::DisplayRole).toString(); QString type = index.data(TypeRole).toString(); // === 背景绘制 === drawBackground(painter, option); // === 左侧彩色标识块 === QRect colorRect(option.rect.left() + 12, option.rect.center().y() - 7, 14, 14); QColor badgeColor = getColorForType(type); painter->setBrush(badgeColor); painter->setPen(Qt::NoPen); painter->drawRoundedRect(colorRect, 3, 3); // === 文字区域 === QRect textRect = option.rect.adjusted(35, 5, -30, -5); // 避开左右元素 painter->setPen(option.palette.text().color()); painter->setFont(getTitleFont(option)); painter->drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, elidedText(painter->fontMetrics(), textRect.width(), title)); // === 右侧箭头(模拟可点击)=== if (option.features.testFlag(QStyleOptionViewItem::HasDecoration)) { drawArrow(painter, option); } } private: void drawBackground(QPainter *painter, const QStyleOptionViewItem &option) const { if (option.state & QStyle::State_Selected) { painter->fillRect(option.rect, option.palette.highlight()); painter->setPen(option.palette.highlightedText().color()); } else if (option.state & QStyle::State_MouseOver) { // 悬停效果:浅灰色背景 + 圆角边框 painter->setBrush(QColor(240, 240, 240)); painter->setPen(QPen(QColor(200, 200, 200), 1)); painter->drawRoundedRect(option.rect.adjusted(1, 1, -1, -1), 6, 6); } else { painter->fillRect(option.rect, option.palette.base()); painter->setPen(option.palette.text().color()); } } QColor getColorForType(const QString &type) const { if (type == "error") return QColor("#d32f2f"); if (type == "warning") return QColor("#f57c00"); if (type == "info") return QColor("#1976d2"); return QColor("#4caf50"); // default success } QFont getTitleFont(const QStyleOptionViewItem &option) const { QFont font = option.font; font.setPointSize(font.pointSize() + 1); font.setBold(false); return font; } QString elidedText(const QFontMetrics &fm, int maxWidth, const QString &text) const { return fm.elidedText(text, Qt::ElideRight, maxWidth); } void drawArrow(QPainter *painter, const QStyleOptionViewItem &option) const { QPolygon arrow; QPoint centerRight(option.rect.right() - 15, option.rect.center().y()); arrow << QPoint(centerRight.x(), centerRight.y()) << QPoint(centerRight.x() - 5, centerRight.y() - 5) << QPoint(centerRight.x() - 5, centerRight.y() + 5); painter->setPen(QPen(Qt::gray, 1.5)); painter->setBrush(Qt::NoBrush); painter->drawPolyline(arrow); } static constexpr int TypeRole = Qt::UserRole + 1; };

关键细节解析:

🎯 状态感知绘制
if (option.state & QStyle::State_Selected) { ... } else if (option.state & QStyle::State_MouseOver) { ... }

这行代码实现了真正的交互感。选中变蓝,悬停加边框,用户一眼就知道自己在哪。

🎨 颜色策略

没直接用Qt::red这种原始色,而是用了 Material Design 的标准色值(如#d32f2f),这样整体风格更协调,也容易统一 UI 调性。

🔤 文本截断

长标题怎么办?用fontMetrics.elidedText()自动加省略号,避免文字溢出。

⚙️ 性能考虑

字体、颜色等都应在函数内快速计算,不要每次去查配置文件或数据库。如果有复杂资源(比如图标),建议提前缓存成QPixmap


接入 UI:三行代码完成绑定

模型和委托都写好了,接入QListView就像拼积木一样简单:

// 创建模型 TaskModel *model = new TaskModel(this); // 添加几条测试数据 model->addTask("系统启动成功", "info"); model->addTask("磁盘空间不足", "warning"); model->addTask("网络连接失败", "error"); // 设置到视图 QListView *listView = new QListView(this); listView->setModel(model); listView->setItemDelegate(new CustomItemDelegate(listView)); // 可选:关闭默认焦点虚线框 listView->setFocusPolicy(Qt::StrongFocus); listView->setEditTriggers(QListView::NoEditTriggers);

就这么几行,一个带状态标识、悬停反馈、专业配色的任务列表就出来了。


实战避坑指南:那些文档不会告诉你的事

❌ 别在paint()里做耗时操作

有人喜欢在paint()里加载图片、解析 JSON、甚至发网络请求……
结果就是:一滚动就卡成幻灯片。

✅ 正确做法:
- 图片提前解码并缓存为QPixmap
- 复杂布局尺寸提前算好存入私有类
- 使用QCacheQMap缓存已计算的结果

🖼️ 高分屏适配别忘了 DPI

如果你的应用要在 4K 屏上运行,记得获取设备像素比:

qreal ratio = option.widget ? option.widget->devicePixelRatioF() : qApp->devicePixelRatio(); int size = 16 * ratio;

否则图标会模糊。

🌐 国际化支持 RTL 布局

中东用户从右往左读,你的“右侧箭头”就得变成“左侧箭头”。可以用:

bool isRtl = (option.direction == Qt::RightToLeft); int margin = isRtl ? 10 : -30;

动态调整位置。

🛠️ 调试小技巧:临时画出矩形边界

当你搞不清option.rect到底在哪,可以在paint()最后加上:

#ifdef DEBUG_DELEGATE painter->setPen(QPen(Qt::magenta, 1, Qt::DashLine)); painter->drawRect(option.rect.adjusted(0, 0, -1, -1)); #endif

编译时加DEBUG_DELEGATE宏就能看到每个 item 的真实范围,排查错位问题超有用。


还能怎么玩?扩展思路推荐

掌握了基础之后,你可以轻松实现更多酷炫效果:

效果实现方式
带缩略图的文件列表在左侧绘制QPixmap缩略图
进度条任务项在文字下方画QLinearGradient渐变条
可开关的条目重写editorEvent()响应点击,切换布尔状态
分组标题悬浮结合QAbstractItemView::indexAt()实现吸顶效果

甚至可以把这套机制迁移到QTableViewQTreeView上,做出企业级管理后台常见的复杂表格。


写在最后:别小看这一行列表

很多人觉得“不就是个列表嘛”,直到项目做大了才发现:
当初随手堆的十几个QLabel,现在成了性能瓶颈;
临时写的样式代码,根本没法换皮肤;
想加个新功能,牵一发动全身……

而今天这一整套基于模型-视图-委托的方案,从一开始就做到了:

  • 数据与界面分离
  • 样式集中可控
  • 性能经得起考验
  • 易于单元测试和复用

这才是专业级 Qt 开发该有的样子。

下次当你又要“新建一个垂直布局”之前,不妨问问自己:
我是不是其实需要一个QListView

如果你正在做日志系统、消息中心、配置面板或者任何涉及大量条目的界面,欢迎试试这套模式。实际用起来你会发现,它不仅更高效,连代码都变得清爽多了。

💬 互动时间:你在项目中用过哪些惊艳的QListView自定义效果?欢迎在评论区分享你的实战经验!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 9:29:45

ModbusRTU报文详解实战演示:温控仪表数据读取全过程

ModbusRTU报文详解实战&#xff1a;从零开始读懂温控仪表通信全过程一个真实的问题场景你刚接手一个工业现场调试任务&#xff0c;面前是一台正在运行的温控仪表&#xff0c;连接着PLC和上位机。但数据显示异常——当前温度明明是100C&#xff0c;系统却显示“NaN”。老板催问&…

作者头像 李华
网站建设 2026/4/17 23:37:31

基于es查询语法的时间范围日志查询:核心要点解析

如何用 Elasticsearch 精准查出“那段时间”的日志&#xff1f;一线工程师的实战指南你有没有过这样的经历&#xff1a;线上服务突然报错&#xff0c;监控面板一片红&#xff0c;而你坐在屏幕前&#xff0c;手指悬在键盘上&#xff0c;只问一句——“到底是什么时候开始的&…

作者头像 李华
网站建设 2026/4/23 10:10:09

LSTM语音合成过时了吗?新一代Sambert架构优势分析

LSTM语音合成过时了吗&#xff1f;新一代Sambert架构优势分析 引言&#xff1a;中文多情感语音合成的技术演进 在语音合成&#xff08;Text-to-Speech, TTS&#xff09;领域&#xff0c;中文多情感语音合成一直是极具挑战性的任务。传统方法依赖于复杂的声学模型与参数化波形生…

作者头像 李华
网站建设 2026/4/23 10:09:35

9款AI辅助工具推荐,优化Java毕业论文的代码实现与格式规范

针对 Java 毕业论文&#xff0c;我们推荐以下 9 款 AI 工具&#xff1a; aibiye - 学术专用&#xff0c;强项降 AIGC 率&#xff0c;适配高校检测平台。 aicheck - 侧重降重和保持语义完整性&#xff0c;支持快速优化。 askpaper - 高效降 AI 生成内容&#xff0c;处理时间短…

作者头像 李华
网站建设 2026/4/22 19:54:59

CRNN模型源码解读:OCR识别的实现原理

CRNN模型源码解读&#xff1a;OCR识别的实现原理 &#x1f4d6; 项目背景与技术选型动因 光学字符识别&#xff08;OCR&#xff09;作为连接物理世界与数字信息的关键桥梁&#xff0c;广泛应用于文档数字化、票据识别、车牌提取、工业质检等多个领域。传统OCR依赖于复杂的图像处…

作者头像 李华
网站建设 2026/4/23 9:15:57

如何用Sambert-HifiGan为APP添加智能语音功能

如何用Sambert-HifiGan为APP添加智能语音功能 &#x1f4cc; 业务场景与技术痛点 在当前的智能应用生态中&#xff0c;语音交互正逐步成为提升用户体验的核心能力之一。无论是客服机器人、教育类APP、阅读助手&#xff0c;还是车载系统和智能家居设备&#xff0c;自然流畅的中文…

作者头像 李华