
简介这是一份面向Qt开发者的控件学习与参考资源包适合正在使用Qt4进行桌面应用开发、希望快速掌握各类控件用法或寻找自定义控件实现思路的初学者与进阶者。包内共60个文件以26个cpp源文件与25个h头文件为主体另含ui界面文件、qrc资源文件、pro工程文件及png、ico图标等压缩包约101KB结构紧凑便于直接编译研究。内容覆盖QLabel、QPushButton、QLineEdit、QTextEdit、QScrollBar、QDateTimeEdit、QTableView、QTreeView等基础与高级控件并包含myValueControl等自定义控件示例涉及信号与槽、布局管理、事件处理及模型视图架构等核心机制。已有2695人学习下载读者可从中获取可运行的控件实现代码、自定义绘图与交互逻辑参考以及组织复杂界面的布局思路适合作为日常开发查阅与练手素材。1. Qt 控件大全从“能跑”到“好用”的那道分水岭很多人第一次接触 Qt 控件大全是在一个很具体的场景里界面能显示按钮能点但一旦要做得像样——表格要排序、列表要拖拽、输入框要校验、窗口要自适应——就发现手头那点控件知识不够用了。Qt 控件大全不是一份背下来就完事的清单它更像一张地图告诉你哪些控件天生适合做什么哪些控件需要组合才能达到效果哪些控件看着简单但参数一改就翻车。我带过几个刚入行的同学他们最大的误区是把控件当成“拖上去就行”的积木结果布局一塌糊涂、信号槽乱连、性能在数据量上来后直接崩掉。这篇笔记就按我实际做项目的顺序把 Qt 控件从选型、布局、数据绑定到自定义绘制的路径拆开讲重点放在参数怎么设、坑在哪、怎么验证。如果你正在用 Qt 做桌面端工具或者准备把一套老 MFC 界面迁移过来下面的内容应该能帮你省掉几个通宵。2. Qt 控件选型先分清“展示型”和“交互型”2.1 展示型控件QLabel、QTextBrowser、QGraphicsView 的边界展示型控件的核心任务是“把数据摆出来”但摆的方式差别很大。QLabel 适合短文本、图标、少量富文本它的性能在几百个实例以内没问题但如果你用它去拼一个几千行的日志窗口滚动就会明显卡顿。QTextBrowser 继承自 QTextEdit支持 HTML 子集和锚点跳转适合做帮助文档或带格式的日志但它内部维护的是富文本文档模型频繁 append 大量纯文本时文档对象会越来越重。我一般会在日志超过 5000 行时改用 QPlainTextEdit它专门为纯文本优化追加操作是 O(1) 级别的。QGraphicsView 是另一类它面向的是“场景 图元”模型适合画布、流程图、拓扑图。它的优势在于视图变换缩放、旋转和图元拾取都是内置的但代价是每个图元都是独立对象数量上万后内存和渲染开销会陡增。选型时先问自己数据是静态展示还是频繁更新需不需要用户直接操作图元如果只是画个曲线QChart 或自定义 QWidget 的 paintEvent 往往更轻。// 展示型控件选型的一个简单判断 // 场景需要显示 2000 行只读日志且支持关键字高亮 // 不推荐QLabel 逐个创建内存和布局开销大 // 推荐QPlainTextEdit QSyntaxHighlighter QPlainTextEdit *logView new QPlainTextEdit(this); logView-setReadOnly(true); logView-setMaximumBlockCount(5000); // 关键参数限制块数量防止内存无限增长 logView-setLineWrapMode(QPlainTextEdit::NoWrap); // 日志通常不换行提升滚动性能上面代码里setMaximumBlockCount是很多新手会忽略的参数。不设它日志跑一天下来内存能涨到几百 MB。NoWrap则避免了长行折行带来的重排开销。逻辑说明QPlainTextEdit 内部用块block管理段落限制块数量等于给内存上了上限超出后自动丢弃最旧的块。参数说明maximumBlockCount设为 0 表示不限制一般日志场景设 3000 到 10000 之间比较平衡。2.2 交互型控件QPushButton、QCheckBox、QComboBox 的信号语义交互型控件的关键不在外观而在信号发出的时机和语义。QPushButton 的 clicked 信号在鼠标释放且仍在按钮内时发出pressed 在按下瞬间发出toggled 只在 checkable 为 true 时才有意义。我见过有人在 pressed 里做耗时操作结果用户还没松手界面就卡住了。正确做法是把耗时逻辑放到 clicked或者用 QTimer::singleShot 延迟到事件循环下一轮。QCheckBox 和 QRadioButton 的语义区别是前者独立后者互斥。但 QRadioButton 的互斥是靠父窗口或 QButtonGroup 实现的如果你把它们放在不同的布局容器里互斥就会失效。QComboBox 的 currentIndexChanged 信号有两个重载版本一个带 int一个带 QString。在 Qt 5 之后推荐用带 int 的版本因为显示文本可能重复而索引是唯一的。另外可编辑的 QComboBox 要小心 editTextChanged 和 activated 的触发顺序用户输入自定义文本时activated 可能不会发出。// 交互型控件的信号连接示例 QComboBox *combo new QComboBox(this); combo-addItem(选项A, 100); // 第二个参数是 userData combo-addItem(选项B, 200); combo-setEditable(true); // 推荐用带 int 的重载通过 userData 取业务值 connect(combo, QOverloadint::of(QComboBox::currentIndexChanged), this, [](int index) { QVariant data combo-itemData(index); if (data.isValid()) { int bizId data.toInt(); // 处理业务逻辑 } });逻辑说明QOverloadint::of用于在 Qt 5 风格连接中明确选择重载版本避免歧义。参数说明addItem的第二个参数 userData 可以存任意 QVariant这样界面显示和业务数据解耦后续改文案不影响逻辑。注意如果 combo 被清空clearcurrentIndexChanged 会发出 -1业务代码里要处理这个边界。2.3 模型/视图类控件QTableView 与 QListView 的选型理由当数据量超过几百条或者需要排序、过滤、多列展示时就该从 QTableWidget 转向 QTableView QAbstractItemModel。QTableWidget 是 QTableView 的便利封装内部帮你创建了 item适合小规模静态表格。但它的 item 是逐个 new 出来的几千行就会明显吃内存而且排序要自己实现。QTableView 配合自定义模型数据存在你自己的容器里视图只按需取。关键参数是setUniformRowHeights(true)如果行高一致能大幅提升滚动性能和setSortingEnabled(true)配合模型的 sort 实现。QListView 适合单列列表如果列表项要显示图标 多行文本用 QListWidget 更省事但同样有数据量上限。我一般的原则是数据超过 1000 条或者需要动态增删改查直接上模型/视图。// 自定义模型的最小骨架 class MyModel : public QAbstractTableModel { Q_OBJECT public: int rowCount(const QModelIndex parent QModelIndex()) const override { return m_data.size(); // m_data 是 QVectorQVectorQVariant } int columnCount(const QModelIndex parent QModelIndex()) const override { return m_headers.size(); } QVariant data(const QModelIndex index, int role) const override { if (role Qt::DisplayRole) { return m_data[index.row()][index.column()]; } return QVariant(); } // 排序时调用通知视图刷新 void sort(int column, Qt::SortOrder order) override { beginResetModel(); std::sort(m_data.begin(), m_data.end(), [](const auto a, const auto b) { return order Qt::AscendingOrder ? a[column] b[column] : a[column] b[column]; }); endResetModel(); } private: QVectorQVectorQVariant m_data; QStringList m_headers; };逻辑说明beginResetModel和endResetModel必须成对出现它们告诉视图“数据要整体重置了别缓存了”。如果只是改一行应该用dataChanged信号否则整个视图重绘性能差很多。参数说明rowCount和columnCount里的 parent 参数在表格模型中通常忽略但返回 0 表示无效索引。注意排序时如果数据量大std::sort的比较函数要尽量轻量避免在比较里做字符串转换。3. 布局与自适应让控件在窗口缩放时不“散架”3.1 QHBoxLayout、QVBoxLayout、QGridLayout 的嵌套策略布局的核心不是“把控件放上去”而是“定义伸缩规则”。QHBoxLayout 和 QVBoxLayout 是最常用的但很多人不知道addStretch和setStretchFactor的区别。addStretch是插入一个弹簧占据剩余空间setStretchFactor是给已有控件分配伸缩比例。比如一个水平布局里有两个按钮你想让它们靠右就在左边加addStretch()。如果你想让两个按钮按 1:2 分配宽度就用setStretchFactor(btn1, 1)和setStretchFactor(btn2, 2)。QGridLayout 适合表单类界面但要注意行号和列号的连续性。我见过有人在网格里跳着放控件结果中间空行空列也占空间。正确做法是用setRowStretch和setColumnStretch控制空白区域的伸缩或者用setSpacing统一间距。嵌套布局时尽量让每一层职责单一外层管整体分区内层管局部对齐。嵌套过深超过 4 层会让布局计算变慢而且调试时很难看出哪一层在起作用。// 一个常见的表单布局标签右对齐输入框左对齐并拉伸 QGridLayout *grid new QGridLayout; QLabel *nameLabel new QLabel(名称); QLineEdit *nameEdit new QLineEdit; nameLabel-setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid-addWidget(nameLabel, 0, 0); grid-addWidget(nameEdit, 0, 1); grid-setColumnStretch(0, 0); // 标签列不拉伸 grid-setColumnStretch(1, 1); // 输入框列拉伸 grid-setSpacing(8); // 控件间距逻辑说明setColumnStretch的参数是比例0 表示不分配额外空间。标签列设为 0输入框列设为 1这样窗口拉宽时只有输入框变宽。参数说明setSpacing控制单元格之间的间距如果想让某一行更高可以用setRowMinimumHeight。注意如果标签文本很长要设置setWordWrap(true)或者用setMinimumWidth防止被压缩。3.2 尺寸策略QSizePolicy 的 Expanding、Preferred、Fixed 怎么选QSizePolicy 是布局的“宪法”它决定了一个控件在空间富余时是扩张还是保持在空间不足时是压缩还是撑住。常用值有 Fixed固定、Minimum可扩不可缩、Maximum可缩不可扩、Preferred可扩可缩但优先保持、Expanding尽量占满。很多人把按钮设成 Expanding结果按钮被拉得巨大很难看。按钮一般用 Fixed 或 Preferred输入框用 Expanding标签用 Preferred。还有一个隐藏参数setMinimumSize和setMaximumSize会覆盖 QSizePolicy 的部分行为。如果你给一个 Expanding 的控件设了 maximumWidth它扩到那个宽度就不再扩了。我一般会在自定义控件里重写sizeHint来给出推荐尺寸而不是硬编码 minimumSize这样布局更灵活。// 尺寸策略设置示例 QLineEdit *edit new QLineEdit; QSizePolicy policy edit-sizePolicy(); policy.setHorizontalPolicy(QSizePolicy::Expanding); policy.setVerticalPolicy(QSizePolicy::Fixed); edit-setSizePolicy(policy); edit-setMinimumWidth(120); // 防止被压得太窄逻辑说明水平 Expanding 让输入框占满可用宽度垂直 Fixed 让它保持一行高度。参数说明setMinimumWidth是硬约束布局不会把控件压到比它更窄。注意如果父布局空间不够所有控件都到 minimumSize 后窗口会出现滚动条或裁剪这时候要考虑用 QScrollArea 包一层。3.3 高 DPI 与字体缩放别让界面在 4K 屏上糊掉高 DPI 是 Qt 控件大全里最容易翻车的部分。Qt 5.6 之后引入了AA_EnableHighDpiScaling属性需要在 QApplication 创建前设置。但光开这个还不够图片资源要用 SVG 或者提供 2x 版本图标要用 QIcon 的多分辨率机制。字体的点大小pointSize在高 DPI 下会自动缩放但像素大小pixelSize不会所以样式表里尽量用 pt 而不是 px。还有一个坑自定义绘制时如果直接用固定像素坐标在高 DPI 下会显得很小。正确做法是用devicePixelRatio换算或者用 QPainter 的scale方法。我一般会在 paintEvent 里先取qApp-devicePixelRatio()然后按比例调整绘制区域。// 高 DPI 下的自定义绘制 void MyWidget::paintEvent(QPaintEvent *) { QPainter painter(this); qreal dpr devicePixelRatioF(); // 获取设备像素比 painter.scale(1.0 / dpr, 1.0 / dpr); // 如果坐标是按逻辑像素算的这里反向缩放 // 或者直接用逻辑坐标绘制Qt 会自动处理 painter.drawRect(QRectF(10, 10, 100, 50)); }逻辑说明devicePixelRatioF返回物理像素与逻辑像素的比值4K 屏上可能是 2.0。参数说明如果绘制代码里用的是逻辑坐标通常不需要手动 scaleQt 的 backing store 会处理但如果你在 QImage 上绘制再显示就要自己乘 dpr。注意样式表里的border-radius等像素值在高 DPI 下不会自动缩放需要用em或动态计算。4. 数据绑定与委托QTableView 里那些“看起来简单”的坑4.1 自定义 QAbstractItemModel 的四个必须实现的方法自定义模型时rowCount、columnCount、data、headerData是必须实现的。data里要处理Qt::DisplayRole、Qt::EditRole、Qt::TextAlignmentRole等。很多人只处理 DisplayRole结果编辑时取不到值。setData要发出dataChanged信号否则视图不会刷新。flags要返回Qt::ItemIsEditable才能编辑。还有一个容易忽略的insertRows和removeRows必须用beginInsertRows/endInsertRows包裹否则视图和模型会不同步表现为“数据插入了但界面没变”或者“界面错位”。我一般会在这些方法里加断言确保索引范围合法。bool MyModel::setData(const QModelIndex index, const QVariant value, int role) { if (!index.isValid() || role ! Qt::EditRole) return false; m_data[index.row()][index.column()] value; emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); return true; } bool MyModel::insertRows(int row, int count, const QModelIndex parent) { beginInsertRows(parent, row, row count - 1); for (int i 0; i count; i) { m_data.insert(row, QVectorQVariant(columnCount())); } endInsertRows(); return true; }逻辑说明dataChanged的第三个参数是角色列表告诉视图哪些角色变了不传则默认所有角色。参数说明beginInsertRows的 row 是插入位置count 是数量parent 在表格模型中通常是无效索引。注意如果插入位置越界beginInsertRows 会断言失败所以要先检查 row 是否在 0 到 rowCount 之间。4.2 QStyledItemDelegate 重写 paint 和 sizeHint 的时机当默认的显示效果不够用——比如要在单元格里画进度条、星级评分、多行富文本——就需要自定义委托。QStyledItemDelegate 的paint方法负责绘制sizeHint负责告诉视图这个单元格需要多大。重写 paint 时记得先调用QStyleOptionViewItem的初始化然后根据option.state判断是否选中、是否有焦点否则选中效果会丢失。一个常见的坑在 paint 里直接用option.rect绘制但没考虑option.decorationSize和边距导致内容贴边。正确做法是用QStyle::PM_FocusFrameHMargin等度量值来留白。另外如果自定义绘制的内容高度不固定sizeHint 要返回准确的高度否则行高会不对。void ProgressDelegate::paint(QPainter *painter, const QStyleOptionViewItem option, const QModelIndex index) const { if (index.column() ! 2) { // 只对第三列自定义 QStyledItemDelegate::paint(painter, option, index); return; } int progress index.data(Qt::UserRole).toInt(); QStyleOptionProgressBar opt; opt.rect option.rect.adjusted(2, 2, -2, -2); opt.minimum 0; opt.maximum 100; opt.progress progress; opt.text QString(%1%).arg(progress); opt.textVisible true; QApplication::style()-drawControl(QStyle::CE_ProgressBar, opt, painter); }逻辑说明这里借用了 QStyle 的进度条绘制避免自己画。参数说明option.rect.adjusted留出 2 像素边距防止进度条贴边。注意如果表格支持排序委托里的 index 在排序后会变所以数据要从index.data取不要缓存行号。4.3 排序与过滤QSortFilterProxyModel 的 setFilterKeyColumn 与正则QSortFilterProxyModel 是模型/视图里的瑞士军刀它可以在不修改源模型的情况下实现排序和过滤。setFilterKeyColumn指定按哪一列过滤setFilterFixedString做子串匹配setFilterRegExp做正则匹配。但要注意过滤是针对源模型的每一行调用filterAcceptsRow如果数据量大且正则复杂会明显卡顿。我一般会在过滤前先做一次快速排除比如按首字母筛选。另一个坑排序时默认是按字符串比较数字会按字典序排10 排在 2 前面。解决办法是自定义代理模型重写lessThan方法或者给数据设置Qt::UserRole存排序用的数值。class NumericSortProxy : public QSortFilterProxyModel { protected: bool lessThan(const QModelIndex left, const QModelIndex right) const override { QVariant l sourceModel()-data(left, Qt::UserRole); QVariant r sourceModel()-data(right, Qt::UserRole); if (l.isValid() r.isValid()) { return l.toDouble() r.toDouble(); } return QSortFilterProxyModel::lessThan(left, right); } };逻辑说明lessThan在排序时被调用返回 true 表示 left 应排在 right 前面。参数说明Qt::UserRole里存的是原始数值DisplayRole 里存的是格式化后的字符串。注意如果 UserRole 没设置会回退到默认比较所以源模型的 data 方法要同时处理这两个角色。5. 避坑与排查Qt 控件调试中常见的五类翻车5.1 界面卡死耗时操作放到了主线程现象点击按钮后界面无响应进度条不刷新甚至窗口变灰。原因Qt 的事件循环在主线程任何超过几十毫秒的同步操作都会阻塞事件处理。解决把耗时逻辑放到 QThread 或 QtConcurrent::run通过信号槽回传结果。注意子线程不能直接操作 UI 控件所有 UI 更新必须在主线程。5.2 信号槽重复连接一次点击触发多次现象按钮点一次槽函数执行了两次或更多。原因connect 被调用了多次或者用了默认的 AutoConnection 在对象生命周期内重复连接。解决在 connect 时加Qt::UniqueConnection或者在对象创建时只连接一次。排查方法在槽函数里打印sender()和计数器。5.3 布局不生效控件被遮挡或尺寸为零现象控件明明 addWidget 了但看不到或者大小是 0。原因父控件没有设置布局或者控件的 sizePolicy 是 Ignored或者被其他控件覆盖。解决检查父控件是否setLayout检查控件是否show()检查 z-order。用 Qt Designer 的对象查看器可以快速定位。5.4 内存泄漏new 了控件没指定父对象现象反复打开关闭对话框内存持续增长。原因new QWidget时没传 parent导致没有自动析构。解决创建时指定父对象或者用智能指针。注意QObject 的父子关系会自动管理内存但前提是 parent 在子对象之前析构。5.5 样式表不生效选择器写错或优先级冲突现象设置了setStyleSheet但控件外观没变。原因选择器写错比如类名大小写、被更具体的选择器覆盖、或者控件是原生绘制的如 QComboBox 的下拉列表。解决用QApplication::setStyleSheet全局设置或者给控件设objectName后用#name选择器。排查方法在 Qt Designer 里预览样式表或者用qDebug() widget-styleSheet()。6. 进阶技巧用 QProxyStyle 统一改控件外观当你需要全局修改某类控件的行为或绘制又不想逐个设置样式表时QProxyStyle 是更彻底的办法。它代理了 QStyle 的所有绘制和度量你可以在不重写整个样式的情况下只覆盖需要改的方法。比如你想让所有 QComboBox 的下拉箭头宽一点或者让 QTabBar 的标签间距大一点都可以在 QProxyStyle 里做。class MyProxyStyle : public QProxyStyle { public: int pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const override { if (metric QStyle::PM_ComboBoxFrameWidth) { return 2; // 加粗组合框边框 } if (metric QStyle::PM_TabBarTabHSpace) { return 24; // 增大标签水平间距 } return QProxyStyle::pixelMetric(metric, option, widget); } void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const override { if (element QStyle::PE_IndicatorArrowDown) { // 自定义下拉箭头绘制 painter-save(); painter-setRenderHint(QPainter::Antialiasing); // ... 绘制逻辑 painter-restore(); return; } QProxyStyle::drawPrimitive(element, option, painter, widget); } }; // 应用 QApplication::setStyle(new MyProxyStyle);逻辑说明pixelMetric返回各种尺寸度量覆盖它就能统一调整间距、边框、图标大小。drawPrimitive负责绘制基本元素比如箭头、复选框标记。参数说明PM_ComboBoxFrameWidth是组合框边框宽度PM_TabBarTabHSpace是标签水平留白。注意QProxyStyle 要设置在 QApplication 创建之后、主窗口显示之前否则部分控件已经用旧样式初始化了。验证方法改完后用QApplication::style()-metaObject()-className()确认样式已生效然后逐个检查目标控件。如果某个控件没变可能是它自己设置了样式表样式表的优先级高于 QProxyStyle。我自己的习惯是先用 QProxyStyle 做全局微调再用样式表做局部特例。这样既保持了整体一致性又不会让样式表变成一坨难以维护的字符串。另外QProxyStyle 的改动会影响所有平台如果要做平台差异化记得在方法里判断QOperatingSystemVersion或者QGuiApplication::platformName()。最后说一个血泪教训不要在 QProxyStyle 里做耗时操作它会在每次绘制时被调用一个不小心就把界面拖卡了。希望帮到你。本文还有配套的精品资源点击获取