简介这是一份面向Qt界面开发者的控件学习与参考资源包汇集了基于Qt4开发的多种精美自定义控件适合初学者熟悉基础控件用法也适合有经验的开发者借鉴自定义控件的实现思路。压缩包共60个文件以26个cpp源文件与25个h头文件为核心另含ui界面文件、qrc与rc资源脚本、pro工程文件及png、ico图标素材整体约101KB结构紧凑便于直接编译研究。内容覆盖QLabel、QPushButton、QLineEdit、QTextEdit、QScrollBar、QDateTimeEdit、QTableView、QTreeView等常用控件并延伸至仪表盘、波形图、进度条、LED指示、音量条、滑动按钮等自绘组件可帮助读者理解信号与槽通信、事件处理、模型视图架构及布局管理器的实际运用。目前已有2695人学习下载通过研读与改造这些示例读者能快速掌握控件定制与界面美化的方法提升Qt应用开发技能。1. Qt 控件大全从“拖控件”到“写控件”的分水岭很多人第一次打开 Qt Designer看到左侧那一长串控件列表第一反应是“这不就是拖拖拽拽的事”。可真到了项目里需求一上来——要一个带搜索框的下拉列表、要一个能折叠的分组面板、要一个表格里嵌进度条——才发现标准控件根本不够用。Qt 控件大全这个标题讲的不是把 QPushButton、QLabel 这些名字背下来而是搞清楚三件事标准控件各自解决什么问题、它们的属性怎么调、不够用时怎么组合或继承出新的控件。这篇文章面向的是已经能写基本 Qt 窗口、但面对复杂界面时不知道从哪下手的开发者。我会按“标准控件分类 → 布局与属性 → 自定义控件 → 避坑 → 进阶技巧”的顺序把每个环节落到能直接抄的代码和参数上。读完你至少能判断一个界面需求是该用现成控件拼还是该自己写一个。2. 标准控件到底分几类按交互模型拆比按字母背有用Qt 的标准控件有几十个按字母顺序背没有意义。真正有用的是按“用户怎么跟它交互”来分类一次性触发的、持续输入的、从集合里选的、展示结构化数据的、只负责容器和布局的。分类清楚了选型就是查表的事。2.1 按钮类与输入类QPushButton、QLineEdit、QSpinBox 的边界按钮类里最常用的是 QPushButton、QToolButton、QRadioButton、QCheckBox。QPushButton 是标准点击按钮QToolButton 通常放在 QToolBar 里适合只显示图标QRadioButton 和 QCheckBox 的区别是前者在一组里互斥后者可以多选。输入类里QLineEdit 管单行文本QTextEdit 管富文本多行QPlainTextEdit 管纯文本多行且性能更好QSpinBox 管整数QDoubleSpinBox 管浮点数QSlider 管范围拖动。选型时最容易翻车的是 QLineEdit 和 QTextEdit 混用。如果只是让用户输入一行路径或名称用 QLineEdit如果要支持换行、复制大段文本用 QPlainTextEdit别用 QTextEdit因为后者默认带富文本解析粘贴大内容时性能差距很明显。from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit, QPlainTextEdit, QSpinBox app QApplication([]) w QWidget() layout QVBoxLayout(w) name_edit QLineEdit() name_edit.setPlaceholderText(输入名称单行) name_edit.setMaxLength(32) # 限制长度防止超长输入 layout.addWidget(name_edit) desc_edit QPlainTextEdit() desc_edit.setPlaceholderText(输入描述多行纯文本) desc_edit.setMaximumBlockCount(200) # 限制行数避免内存膨胀 layout.addWidget(desc_edit) count_spin QSpinBox() count_spin.setRange(1, 999) # 范围必须显式设置 count_spin.setSingleStep(1) layout.addWidget(count_spin) w.show() app.exec()这段代码里setMaxLength 和 setMaximumBlockCount 是两个容易被忽略但很关键的参数。前者防止用户粘贴超长字符串导致后续处理异常后者防止多行输入无限增长。setRange 对 QSpinBox 是必须的默认范围是 0 到 99很多人在这一步被坑过——用户输入 100 发现输不进去以为是控件坏了。2.2 选择类与数据展示类QComboBox、QListWidget、QTableView 怎么选选择类控件包括 QComboBox、QListWidget、QTreeWidget、QTableWidget。这里有一个核心分界线带 Widget 后缀的QListWidget、QTreeWidget、QTableWidget是“自带数据模型”的简易版适合数据量小、不需要复杂过滤和排序的场景不带 Widget 后缀的QListView、QTreeView、QTableView是“模型-视图”架构适合数据量大、需要自定义数据源或代理的场景。常见做法是数据少于几百条、不需要跟数据库或自定义结构绑定时直接用 QTableWidget一旦数据上千条或者需要排序、过滤、多视图共享同一份数据就换成 QTableView 加 QAbstractTableModel。这个切换不是性能玄学是实打实的内存和刷新开销差异。from PySide6.QtWidgets import QApplication, QComboBox, QTableWidget, QTableWidgetItem app QApplication([]) combo QComboBox() combo.addItem(选项A, userData1) # userData 存业务 ID显示文本和实际值分离 combo.addItem(选项B, userData2) combo.setEditable(False) # 默认不可编辑需要搜索时再开 combo.currentIndexChanged.connect(lambda i: print(combo.itemData(i))) table QTableWidget(3, 2) # 3 行 2 列 table.setHorizontalHeaderLabels([名称, 数值]) for row in range(3): table.setItem(row, 0, QTableWidgetItem(f项{row})) table.setItem(row, 1, QTableWidgetItem(str(row * 10))) table.resizeColumnsToContents() # 按内容自适应列宽QComboBox 的 addItem 第二个参数 userData 是实战里最值得养成的习惯显示给用户看的文本和程序内部用的 ID 分开存避免以后改文案时到处改判断逻辑。QTableWidget 的 setItem 每次都会创建新对象批量填充时建议先关闭刷新或直接用模型否则大数据量下会明显卡顿。2.3 容器与布局类QGroupBox、QTabWidget、QSplitter 的组合逻辑容器类控件本身不处理业务但决定了界面的组织方式。QGroupBox 带标题边框适合把相关控件圈在一起QTabWidget 分页适合功能多但不需要同时看到的场景QSplitter 可拖动分割适合左右或上下需要用户自己调比例的场景QScrollArea 提供滚动适合内容可能超出可视区域的面板。布局管理器里QHBoxLayout 和 QVBoxLayout 是基础QGridLayout 适合表单QFormLayout 专门做“标签-输入框”两列布局。一个常见误区是到处用绝对坐标 setGeometry结果窗口一缩放就全乱。正确做法是每层容器都设布局让 Qt 自己算。from PySide6.QtWidgets import QApplication, QWidget, QSplitter, QTextEdit, QListWidget from PySide6.QtCore import Qt app QApplication([]) splitter QSplitter(Qt.Horizontal) splitter.addWidget(QListWidget()) splitter.addWidget(QTextEdit()) splitter.setStretchFactor(0, 1) # 左侧拉伸权重 1 splitter.setStretchFactor(1, 3) # 右侧拉伸权重 3 splitter.setSizes([200, 600]) # 初始尺寸 splitter.show() app.exec()setStretchFactor 和 setSizes 配合使用前者决定窗口缩放时谁分到更多空间后者决定初始比例。只设一个往往达不到预期效果这是布局调试里最常见的“玄学”来源。3. 属性、信号与样式让标准控件听话的三个抓手选好控件只是第一步真正让界面符合预期靠的是属性设置、信号连接和样式表这三件事。这三者分别管“长什么样”“怎么响应”“怎么美化”。3.1 必调属性清单enabled、visible、sizePolicy 与 toolTip每个 QWidget 都有一组通用属性其中四个在实战里几乎必调。setEnabled(false) 禁用控件用户能看见但点不了setVisible(false) 直接隐藏setSizePolicy 决定控件在布局里怎么伸缩setToolTip 设置悬停提示。sizePolicy 是最容易被忽视的。默认情况下QLineEdit 是 ExpandingQPushButton 是 Fixed。如果你把按钮放进水平布局发现它被拉得很宽就是因为没设 QSizePolicy.Fixed。反过来如果希望某个输入框占满剩余宽度就要设 Expanding。from PySide6.QtWidgets import QPushButton, QSizePolicy btn QPushButton(提交) btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) # 不随布局拉伸 btn.setToolTip(点击后提交当前表单) btn.setEnabled(False) # 表单未填完时禁用参数说明setSizePolicy 两个参数分别是水平和垂直方向策略常用值有 Fixed、Minimum、Maximum、Preferred、Expanding。按钮一般用 Fixed输入框用 Expanding标签用 Preferred。3.2 信号与槽的连接方式默认连接、队列连接与断开Qt 的信号槽机制是它跟其他界面库拉开差距的地方。连接方式有 AutoConnection、DirectConnection、QueuedConnection。默认 AutoConnection 在同一个线程时是直接调用跨线程时自动变成队列调用。绝大多数界面代码不需要手动指定但跨线程更新界面时必须用 QueuedConnection否则会崩。from PySide6.QtCore import QObject, Signal, Slot, Qt class Worker(QObject): progress Signal(int) def run(self): for i in range(100): self.progress.emit(i) worker Worker() # 跨线程时显式指定队列连接保证槽函数在接收者线程执行 worker.progress.connect(lambda v: print(v), Qt.QueuedConnection)断开连接用 disconnect()但更常见的做法是连接时用 lambda 捕获局部变量导致无法断开。如果需要在运行时动态断开建议把槽函数定义成独立方法而不是匿名 lambda。3.3 样式表能改什么、不能改什么qss 的边界Qt 样式表语法类似 CSS但只支持一部分属性。能改的包括背景色、边框、圆角、字体、内边距、悬停和按下状态。不能改的包括控件内部子控件的布局结构比如 QComboBox 下拉箭头的位置只能有限调整。btn.setStyleSheet( QPushButton { background-color: #2d7d46; color: white; border-radius: 4px; padding: 6px 16px; } QPushButton:hover { background-color: #3a9d5a; } QPushButton:disabled { background-color: #cccccc; } )这段样式里:hover 和 :disabled 是伪状态分别对应悬停和禁用。注意样式表会覆盖系统默认外观一旦用了 qss就要把正常、悬停、按下、禁用四个状态都写全否则会出现某个状态下样式突变的问题。4. 自定义控件从组合到继承的落地路径标准控件不够用时有两条路组合现有控件成一个新控件或者继承某个控件重写绘制和事件。前者适合“把几个控件打包”后者适合“改变一个控件的行为或外观”。4.1 组合式自定义用 QWidget 打包搜索框加下拉列表组合式自定义的核心是新建一个继承 QWidget 的类内部放多个标准控件用布局排好对外暴露信号和接口。from PySide6.QtWidgets import QWidget, QHBoxLayout, QLineEdit, QListWidget from PySide6.QtCore import Signal class SearchCombo(QWidget): selected Signal(str) def __init__(self, items, parentNone): super().__init__(parent) self.edit QLineEdit() self.list QListWidget() self.list.addItems(items) self.list.hide() # 初始隐藏输入时再显示 layout QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.edit) layout.addWidget(self.list) self.edit.textChanged.connect(self.filter_items) self.list.itemClicked.connect(self.on_item_clicked) def filter_items(self, text): for i in range(self.list.count()): item self.list.item(i) item.setHidden(text.lower() not in item.text().lower()) self.list.setVisible(bool(text)) def on_item_clicked(self, item): self.edit.setText(item.text()) self.list.hide() self.selected.emit(item.text())这个 SearchCombo 对外只暴露一个 selected 信号内部怎么过滤、怎么显示都是自己的事。参数上setContentsMargins(0,0,0,0) 去掉默认边距让内部控件贴合外框filter_items 里用 lower() 做大小写不敏感匹配这是搜索框的基本要求。4.2 继承式自定义重写 paintEvent 画一个环形进度条继承式自定义适合需要完全控制绘制的场景。以环形进度条为例继承 QWidget重写 paintEvent用 QPainter 画弧。from PySide6.QtWidgets import QWidget from PySide6.QtGui import QPainter, QPen, QColor from PySide6.QtCore import Qt, QRectF class RingProgress(QWidget): def __init__(self, parentNone): super().__init__(parent) self._value 0 def setValue(self, v): self._value max(0, min(100, v)) self.update() # 触发重绘 def paintEvent(self, event): painter QPainter(self) painter.setRenderHint(QPainter.Antialiasing) # 抗锯齿否则边缘有锯齿 rect QRectF(10, 10, self.width() - 20, self.height() - 20) pen_bg QPen(QColor(#e0e0e0), 8) painter.setPen(pen_bg) painter.drawArc(rect, 0, 360 * 16) # 背景整圆 pen_fg QPen(QColor(#2d7d46), 8) painter.setPen(pen_fg) span int(-self._value / 100 * 360 * 16) # 负值表示顺时针 painter.drawArc(rect, 90 * 16, span)关键参数drawArc 的角度单位是 1/16 度所以 360 度要写 36016起始角度 9016 表示从 12 点方向开始span 为负表示顺时针绘制。setRenderHint(QPainter.Antialiasing) 必须开否则圆弧边缘会有明显锯齿。setValue 里调用 update() 是触发重绘的标准做法不要直接调 paintEvent。4.3 自定义控件的信号设计与对外接口自定义控件对外暴露什么决定了它好不好用。原则是内部状态变化用信号发出外部设置状态用公开方法。信号命名用过去式或名词比如 selected、valueChanged方法命名用动词比如 setValue、setItems。一个常见错误是把内部子控件直接暴露出去比如提供 getLineEdit() 让外部随便改。这样一旦内部结构调整所有调用处都要改。正确做法是只暴露必要的信号和方法内部实现细节保持私有。5. 避坑与排查控件不显示、布局错乱、样式失效的常见原因这一章记录的是我在实际项目里反复遇到的五类问题每条按现象、原因、解决来写。5.1 控件设置了却看不见父控件和布局的顺序问题现象代码里创建了控件也调用了 show()但界面上就是没有。原因通常有两个一是控件没有父控件创建后没有被加入任何布局成为独立窗口但尺寸为零二是先 show() 了父窗口之后才 addWidget导致新控件没有触发重新布局。解决确保每个控件都被 addWidget 或 setParent 加入某个已显示的容器如果是在运行时动态添加添加后不需要手动调 show()布局会自动处理但父容器需要 update() 或 adjustSize()。5.2 布局拉伸不符合预期sizePolicy 和 stretch 的优先级现象两个控件放在水平布局里希望左边固定、右边拉伸结果两个都被拉伸了。原因没有设置 sizePolicy默认策略下两者都参与拉伸。解决给左边控件设 setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)或者用 layout.addWidget(left, 0) 和 layout.addWidget(right, 1) 指定 stretch 因子。注意 stretch 因子和 sizePolicy 同时存在时stretch 因子优先决定剩余空间的分配。5.3 样式表不生效选择器写错和继承覆盖现象写了 qss 但控件外观没变化。原因选择器写成了类名但控件实际是子类比如给 QPushButton 写样式但实际用的是自定义的 MyButton 继承自 QPushButtonqss 默认不匹配子类。解决用 QPushButton 匹配所有子类或者用 MyButton 精确匹配。另一个原因是父控件的样式表覆盖了子控件需要在子控件上显式设置或使用更具体的选择器。5.4 信号重复连接槽函数被执行多次现象一个按钮点一次槽函数执行了两次甚至更多。原因connect 被调用了多次常见于在循环里或每次打开对话框时都重新连接。解决连接前先 disconnect或者把连接放在只执行一次的地方。如果用的是 lambdadisconnect 无法直接指定建议改用独立方法。5.5 跨线程更新界面崩溃直接调用 vs 信号槽现象在子线程里直接调用控件的 setText 或 setValue程序随机崩溃。原因Qt 的界面控件不是线程安全的只能在主线程操作。解决子线程通过信号把数据发回主线程由主线程的槽函数更新界面。连接时用 Qt.QueuedConnection 确保槽在主线程执行。6. 进阶技巧用 QStyledItemDelegate 定制表格单元格标准控件和自定义控件的边界在数据展示类控件上体现得最明显。QTableWidget 能显示文本但如果你想在单元格里画进度条、放评分星星、或者让某一列只能从下拉里选就需要用到 QStyledItemDelegate。这是 Qt 控件体系里从“会用”到“用透”的一个分水岭。6.1 为什么用 Delegate 而不是 setCellWidgetQTableWidget 提供了 setCellWidget可以往单元格里塞任意控件。但每个 setCellWidget 都会创建一个真实的 QWidget 对象一千行就是一千个控件实例内存和刷新开销都很大。Delegate 的方式是“按需绘制”只在单元格可见时调用 paint不创建额外控件。数据量大时两者性能差距非常明显。6.2 一个进度条 Delegate 的完整实现下面这个例子让表格的某一列显示进度条数据存在 item 的 UserRole 里。from PySide6.QtWidgets import QStyledItemDelegate, QApplication, QTableWidget, QTableWidgetItem from PySide6.QtCore import Qt, QRect class ProgressDelegate(QStyledItemDelegate): def paint(self, painter, option, index): value index.data(Qt.UserRole) # 从 UserRole 取进度值 if value is None: super().paint(painter, option, index) return # 画背景 painter.save() rect option.rect.adjusted(2, 2, -2, -2) painter.fillRect(rect, Qt.lightGray) # 画进度 progress_rect QRect(rect.x(), rect.y(), int(rect.width() * value / 100), rect.height()) painter.fillRect(progress_rect, Qt.darkGreen) # 画文字 painter.setPen(Qt.black) painter.drawText(rect, Qt.AlignCenter, f{value}%) painter.restore() app QApplication([]) table QTableWidget(3, 2) table.setHorizontalHeaderLabels([名称, 进度]) table.setItemDelegateForColumn(1, ProgressDelegate()) # 只对第 1 列生效 for row, val in enumerate([30, 65, 90]): table.setItem(row, 0, QTableWidgetItem(f任务{row})) item QTableWidgetItem() item.setData(Qt.UserRole, val) # 进度值存在 UserRole table.setItem(row, 1, item) table.show() app.exec()逻辑说明paint 方法里先取 UserRole 的数据没有就走默认绘制。然后保存 painter 状态画背景矩形按百分比画进度矩形最后画文字恢复 painter。参数上option.rect 是当前单元格的绘制区域adjusted(2,2,-2,-2) 留出内边距setItemDelegateForColumn(1, ...) 只对第二列生效不影响其他列。6.3 Delegate 的编辑与显示分离createEditor 和 setEditorData如果不仅想显示进度条还想让用户能编辑就需要实现 createEditor、setEditorData、setModelData、updateEditorGeometry 四个方法。createEditor 返回一个编辑用的控件比如 QSpinBoxsetEditorData 把模型里的值填进去setModelData 把编辑结果写回模型updateEditorGeometry 决定编辑器在单元格里的位置。from PySide6.QtWidgets import QSpinBox class ProgressEditDelegate(ProgressDelegate): def createEditor(self, parent, option, index): editor QSpinBox(parent) editor.setRange(0, 100) return editor def setEditorData(self, editor, index): value index.data(Qt.UserRole) or 0 editor.setValue(value) def setModelData(self, editor, model, index): model.setData(index, editor.value(), Qt.UserRole)这四个方法里最容易出错的是 setModelData 里写错了角色导致编辑后数据没更新。记住显示用 UserRole编辑写回也用 UserRole两边要一致。6.4 什么时候该停手Delegate 的适用边界Delegate 适合“同一列所有单元格用同一种绘制或编辑方式”的场景。如果每一行的单元格绘制逻辑都不一样或者需要嵌入复杂的交互控件Delegate 的代码会变得很难维护这时候反而应该考虑用 QScrollArea 加自定义行控件或者直接用 QListView 加自定义 item。我自己的习惯是能用 Delegate 解决的优先用 Delegate一旦发现 paint 里出现超过三层 if-else 分支就停下来重新想结构。最后说一个我踩过的坑Delegate 里不要做耗时操作paint 会被频繁调用任何在 paint 里查数据库或读文件的行为都会让界面卡死。数据应该在模型层准备好Delegate 只负责画。希望帮到你。本文还有配套的精品资源点击获取