☰
Qt控件大全:从标准控件到自定义Delegate的实战指南
2026/10/14 10:56:01 网站建设 项目流程

简介:这是一份面向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", userData=1) # userData 存业务 ID,显示文本和实际值分离 combo.addItem("选项B", userData=2) 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 是 Expanding,QPushButton 是 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, parent=None): 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, parent=None): 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 继承自 QPushButton,qss 默认不匹配子类。解决:用 QPushButton 匹配所有子类,或者用 MyButton 精确匹配。另一个原因是父控件的样式表覆盖了子控件,需要在子控件上显式设置或使用更具体的选择器。

5.4 信号重复连接:槽函数被执行多次

现象:一个按钮点一次,槽函数执行了两次甚至更多。原因:connect 被调用了多次,常见于在循环里或每次打开对话框时都重新连接。解决:连接前先 disconnect,或者把连接放在只执行一次的地方。如果用的是 lambda,disconnect 无法直接指定,建议改用独立方法。

5.5 跨线程更新界面崩溃:直接调用 vs 信号槽

现象:在子线程里直接调用控件的 setText 或 setValue,程序随机崩溃。原因:Qt 的界面控件不是线程安全的,只能在主线程操作。解决:子线程通过信号把数据发回主线程,由主线程的槽函数更新界面。连接时用 Qt.QueuedConnection 确保槽在主线程执行。

6. 进阶技巧:用 QStyledItemDelegate 定制表格单元格

标准控件和自定义控件的边界,在数据展示类控件上体现得最明显。QTableWidget 能显示文本,但如果你想在单元格里画进度条、放评分星星、或者让某一列只能从下拉里选,就需要用到 QStyledItemDelegate。这是 Qt 控件体系里从“会用”到“用透”的一个分水岭。

6.1 为什么用 Delegate 而不是 setCellWidget

QTableWidget 提供了 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 返回一个编辑用的控件,比如 QSpinBox;setEditorData 把模型里的值填进去;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 只负责画。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询