这篇是QT学习笔记的第二篇。第一篇我把环境搭建、第一个QWidget窗口、信号槽的基本写法过了一遍,今天开始正式进入控件层面。QT做界面,说白了就是往窗口上摆控件、调属性、连信号。控件属性理解透了,后面不管做工业上位机、数据采集工具还是普通的桌面软件,都会顺手很多。这篇我会把按钮、Label、三种输入框、以及QListWidget、QTreeWidget、QTableWidget这些view系列放在一起讲,因为这些控件的属性有不少共通点,串起来学效率最高。内容适合刚跑通HelloWorld、准备开始写真实界面的新手,也适合想系统把控件用法补一遍的同学。
1. 控件属性系统:先搞懂QT的"属性"到底是什么
1.1 属性就是对象的“外部配置接口”
很多初学者看到setFixedSize、setText、setStyleSheet,感觉这些都是零散函数,背完就完了。实际上在QT里,一个控件就是一个C++对象,类里的成员变量用来保存内部状态,而属性就是通过Q_PROPERTY宏暴露给外部的一套读写接口。比如QLineEdit的text属性,对应的是内部那个QString变量,配套text()和setText()两个方法。用Q_PROPERTY声明之后,这个属性就有了统一的名字,QVariant和元对象系统就能动态操作它。
class MyWidget : public QWidget { Q_OBJECT Q_PROPERTY(QString caption READ caption WRITE setCaption) public: QString caption() const { return m_caption; } void setCaption(const QString &c) { m_caption = c; } private: QString m_caption; };这段代码乍一看好像没什么用,但它背后有很实际的价值:属性一旦注册进元对象系统,你就可以在QSS里用attr(...)选择器、可以在QML访问、可以在Qt Designer的属性表里看到、也可以通过property("caption")动态读写。也就是说,属性不是单纯的getter/setter,它是控件对外界开放的统一配置入口。
我个人的习惯是:自定义控件时,只要某个配置项需要使用者修改,就优先考虑用Q_PROPERTY暴露,而不是只写一个set函数。这样做的好处是,后续别人把你的控件放进Designer,或者写QSS的时候,根本不需要翻代码就能发现可用配置,这也是QT生态里默认的控件交互方式。
1.2 控件属性的常见分类与设计建议
把常用控件的属性梳理一遍,你会发现其实可以归成四类:几何类、行为类、外观类、数据类。理解分类比死记硬背有效得多,因为同一类属性在任何控件上都差不多。
| 分类 | 典型属性 | 作用 |
|---|---|---|
| 几何类 | setGeometry、setFixedSize、setMinimumSize、setMaximumSize、setSizePolicy | 控制控件大小和位置 |
| 行为类 | setEnabled、setVisible、setFocusPolicy、setContextMenuPolicy | 控制控件是否可用、可见、能否响应键盘焦点和右键菜单 |
| 外观类 | setStyleSheet、setObjectName、setFont、setCursor、setPalette | 控制控件的视觉效果 |
| 数据类 | setText、setIcon、setCurrentIndex、setValue | 控制控件承载的数据内容 |
这四类属性在设计界面时的优先级不同。以我自己的开发顺序为例:先把数据类和行为类属性设好,保证功能正确,最后再统一调外观。有很多新手喜欢一开始就疯狂写setStyleSheet,结果界面改一次颜色就要翻遍所有代码,非常痛苦。正确做法是先给控件起好objectName,比如saveButton、ipEdit,然后把QSS集中放在一个样式表文件里,用#saveButton这种id选择器去定向美化。
几何属性里面,最容易被忽视的是QSizePolicy。它决定的是控件在布局中如何伸缩,比如水平策略是Expanding,那当窗口变宽时控件会跟着变宽;如果是Fixed,就始终保持自己的sizeHint。很多界面乱了,就是因为你给每一个控件都写了setFixedSize,却忘了放在setLayout之后,最后布局管理器没有伸缩空间,窗口一拉就变形。关于布局的问题,我后面会专门讲,这里先记住一句话:写死尺寸只会让你维护起来越来越累,能用布局和sizePolicy解决的事情,不要用固定几何属性去硬顶。
2. 按钮系列:属性、信号、样式
2.1 按钮家族四个成员怎么选
QT里的按钮不是只有QPushButton一个。实际开发中经常用到的有四个:QPushButton、QToolButton、QRadioButton、QCheckBox。选错的情况很常见,比如有人为了做“选中态”效果,硬用QPushButton去模拟QCheckBox的勾选,结果又要自己维护状态,又要改样式,得不偿失。
最简单的选型原则是这样的:普通点击操作,用QPushButton;放在工具栏里、需要显示图标或下拉菜单,用QToolButton;同一组里只能选一个,用QRadioButton;可以独立勾选或互不影响的多个选项,用QCheckBox。QToolButton是很多人忽略的控件,但它非常灵活。当你需要一个既可以执行主操作、又能在旁边弹出下拉菜单的按钮时,QToolButton的setPopupMode(QToolButton::MenuButtonPopup)比QPushButton的setMenu更可控,菜单按钮会被单独画出来,用户也更习惯。
QRadioButton和QCheckBox都属于可勾选按钮。QRadioButton的互斥规则是:同一个父控件下的QRadioButton默认互斥,但如果你把它们拆到不同的GroupBox里,那两组之间就互不影响。想让不同层级的按钮也能互斥,就要用QButtonGroup把它们归到一组,然后setExclusive(true)。这个细节在做个多选参数面板的时候特别容易踩坑。
2.2 QPushButton核心属性和clicked信号的细节
QPushButton的常用属性并不多,但每个都值得认真理解。看下面这段代码:
QPushButton *btn = new QPushButton("保存", this); btn->setObjectName("saveButton"); btn->setIcon(QIcon(":/icons/save.png")); btn->setIconSize(QSize(20, 20)); btn->setShortcut(QKeySequence("Ctrl+S")); btn->setToolTip("保存当前文件"); btn->setFocusPolicy(Qt::StrongFocus); connect(btn, &QPushButton::clicked, this, &MainWindow::saveFile);setText和setIcon是最基础的属性,但很多人忽略了按钮文本里的&符号。如果写成new QPushButton("保存(&S)", this),QT会自动把S作为按钮的快捷键字母,在Windows上配合Alt键就能激活这个按钮。这个功能在菜单、工具栏按钮中很实用,比手动setShortcut更符合平台习惯。
信号方面,QPushButton最常用的是clicked(bool checked),注意它带一个bool参数,如果按钮是Checkable的,传出的就是这个按钮当前的选中状态。clicked和pressed/released的区别要搞清楚:clicked必须在按钮上按下之后、还在按钮内部释放才触发,是一套完整的“点击”动作;pressed是按下瞬间触发。做按压动画、录音这种按下就开始响应的功能时,用pressed会更准。toggled和clicked很容易混淆,toggled只有在checkable按钮状态发生切换时才会触发,普通按钮不会触发。
还有一点经验:不要把按钮的showEvent或构造代码写在connect里重复触发。我见过有人把按钮初始化放在一个被反复调用的函数里,导致信号被connect了好多次,点一下按钮执行了多次保存。解决方法是把connect放到构造函数或者initUi里,保证每个信号槽连接只建立一次。
2.3 让按钮点击区域更大、禁用与防误触
论坛里经常有人问“如何增大按钮点击范围”,这个问题在QT里其实很简单。按钮的实际可点击区域一般就是按钮本身的矩形范围,如果QToolButton只显示一个小图标,四周就会有一大片空白很难点。解决方法是给按钮设置最小尺寸,比如setMinimumWidth(40)、setMinimumHeight(40),或者把图标尺寸调大一点,而不是在样式表里只改背景。
另一个常见需求是“禁用后按钮不该响应”。很多人直接把setEnabled(false)和setVisible(false)混用,结果界面布局变化,按钮消失。这两个属性的区别要分清:setEnabled(false)只是让按钮变灰、不可点击,但仍占着布局空间;setVisible(false)是彻底隐藏,布局会重新排列。禁用状态下,按钮不响应clicked信号,但样式上需要你用QSS的:disabled伪状态把文字颜色调低,不然用户会分不清是按钮坏了还是本来就不可用。
防误触也是很现实的问题,尤其在工业软件里。我常在“删除”“格式化”这类危险按钮上做三重保护:先setEnabled(false),等用户在主表格选中一条记录后才启用;再用QMessageBox确认;最后要求输入特定字符才能执行。界面上的按钮只是最后一步,不要把判断逻辑全写在按钮clicked里,那样别人接手代码时会非常头疼。
3. Label与输入框:显示与输入的那点小事
3.1 QLabel不只是显示文字
很多人把QLabel当成一个“静态文字标签”,其实它能干的事情不少:显示文本、显示图片、显示GIF动图、甚至显示富文本HTML。setText()可以自动识别文本是不是HTML格式,如果你传入一串带<b>标签的字符串,QLabel默认就会按富文本渲染。这既是方便也是坑,如果你要显示的内容本身包含尖括号,记得先把文本设成普通文本,或者用Qt::escape处理。
图片方面,setPixmap()和setText()同时存在时,只能显示最后一次设置的内容。显示大图时要注意setScaledContents(true)会让图片自动缩放到QLabel大小,但这破坏了图片比例,最好在resizeEvent或者布局里按比例调整。动图用QMovie,先movie->start(),如果界面卡住、动图不播,很可能是GIF本身编码问题,或者你忘了调用start。
超链接也是QLabel的常用场景。如果只想点开链接就让系统浏览器打开,可以setOpenExternalLinks(true);如果想拦截点击做自定义逻辑,比如跳转到自己程序的某个页面,就把这个属性设成false,然后connectlinkActivated信号。还有一个细节:QLabel默认不接收焦点,某些平台下你想让它可以被Tab选中、或者响应键盘事件,需要自己改focusPolicy,很多做无障碍适配的人会在这里踩坑。
3.2 QLineEdit:单行输入的常用坑与技巧
QLineEdit是单行输入框里的绝对主力。它的属性非常贴近实际需求:placeholder是给用户提示,clearButton是清空按钮,echoMode处理密码,validator控制输入合法性,inputMask限制格式。下面这段基本可以当作一个标准模板:
QLineEdit *edit = new QLineEdit(this); edit->setPlaceholderText("请输入IP地址"); edit->setInputMask("000.000.000.000"); edit->setClearButtonEnabled(true); edit->setMaxLength(15); edit->setEchoMode(QLineEdit::Password);先说占位符。有人觉得placeholder和setText差不多,其实placeholder是灰色提示,只在输入框为空时显示,用户一输入就消失,用来引导用户非常合适。清空按钮setClearButtonEnabled是QT 5.2之后加入的属性,用户输入内容后右侧会出现一个小叉,点击即清空。这个功能在登录框、搜索框里体验很好,不用自己造按钮。
密码框的echoMode支持Normal、Password、PasswordEchoOnEdit和NoEcho。最常用的是Password,但如果你希望用户正在输入时能看到字符、停下来立刻变黑点,可以用PasswordEchoOnEdit,这个体验更好。另一个坑是:密码框占位符里的提示文字也会被显示成黑点,很多新手以为是BUG,其实把focusPolicy设成StrongFocus,再在自定义控件里重写paintEvent就能解决,但通常没人会去纠结这一点,因为实际影响不大。
输入校验方面,validator和inputMask是两套思路。inputMask是按位置限制字符格式,比如IP地址用000.000.000.000就很方便;validator是写QValidator子类来自定义规则,QIntValidator、QDoubleValidator、QRegExpValidator是最常用的。两者不建议同时开,因为mask会先拦掉某些非法字符,validator又判定一次边界,逻辑会互相冲突。最多输入长度限制setMaxLength是纯字符数量限制,和校验不冲突,可以同时用。
3.3 QTextEdit与QPlainTextEdit怎么选
多行文本控件里有两位长相相似但定位完全不同的选手:QTextEdit支持富文本,可以插入图片、调整字体颜色、保存HTML;QPlainTextEdit只支持纯文本,没有富文本的复杂渲染逻辑。新手选错很常见,比如日志窗口用QTextEdit,结果日志一多就卡得不行。
我的建议是:需要富文本编辑能力,比如实现一个简易聊天框或笔记工具,用QTextEdit;需要显示大量纯文本,尤其是持续追加的日志、调试信息、代码内容,用QPlainTextEdit。QPlainTextEdit内部用的是QTextDocument,但只做纯文本文档处理,渲染效率和内存占用都比QTextEdit好很多。
高频日志还有一个重要属性:setMaximumBlockCount(int)。这个属性限制文档里最多保留多少行块,超出后自动丢弃最旧的行。日志动辄几千上万行,如果不限制,光滚动条计算就会越来越慢,内存也会涨。配合appendPlainText()追加文本,几乎不会卡顿。我还习惯在日志窗口里禁用编辑,用setReadOnly(true),同时设置setTextInteractionFlags(Qt::TextSelectableByMouse),这样既能选中复制日志,又不会被用户误改。
4. view系列:从便捷控件到model/view架构
4.1 六个控件分两家
QT的view系列很容易让新手晕菜,因为它有两套长得差不多的控件:QListWidget、QTreeWidget、QTableWidget,以及QListView、QTreeView、QTableView。前三个叫做Item Widget便捷类,后三个是标准的View类。区别不在于名字里有没有Item,而在于数据和视图的耦合方式。
便捷类内部自带一个默认的model,数据通过setItem方式直接塞进去,使用非常简单。View类本身不存数据,你要自己提供一个model,再让view去读取model里的数据。这种拆分方式就是常说的model/view架构,数据、界面、编辑逻辑分离,其实也是MVVM思想在QT里的原生实现。两者不是谁替代谁的关系,项目里完全可以混用。
| 对比项 | QListWidget / QTreeWidget / QTableWidget | QListView / QTreeView / QTableView |
|---|---|---|
| 数据存储 | 控件内部model自动管理 | 需要外部自定义model |
| 开发难度 | 低,适合小数据量 | 较高,适合大数据或复杂结构 |
| 性能 | 几百行内还行,再大会卡 | 按可见区域取数,轻松支撑几万行 |
| 灵活性 | 内部model固定,不好扩展 | 可自定义排序、过滤、树形结构、拖拽 |
| 适用场景 | 表单、小配置页、原型界面 | 数据报表、文件树、日志列表 |
我见过很多人一上来就在QTableWidget里塞了几千条数据,然后抱怨卡顿。这其实不是QT不行,而是用错了工具。本文后面会专门讲怎么从QTableWidget平滑过渡到QTableView加自定义model,这条优化路径在实战中价值极高。
4.2 QTableWidget的常用属性和数据填充
QTableWidget用起来最直观,先建一个表格,再往里填QTableWidgetItem。它的常用属性一定要掌握,不然做出来的表格既不可选中也不可排序,体验很差:
QTableWidget *table = new QTableWidget(this); table->setColumnCount(3); table->setHorizontalHeaderLabels({"名称", "数量", "备注"}); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->setAlternatingRowColors(true); table->verticalHeader()->setVisible(false); QTableWidgetItem *item = new QTableWidgetItem("Qt"); table->setItem(row, 0, item);创建item时有一个很容易出错的点:不要把同一个QTableWidgetItem对象同时塞给两个单元格,比如把同一个指针分别setItem到row0和row1,那么前一个单元格会自动失去引用,可能造成逻辑错误。item一旦通过setItem交给表格,后续就由表格负责销毁,不要自己在代码里去delete,否则崩溃概率很高。
选择行为setSelectionBehavior有SelectItems(选中单个单元格)、SelectRows(选中整行)、SelectColumns(选中整列)。数据报表场景绝大多数都该用SelectRows,因为后续拿到选中行号要操作整行数据。编辑触发器setEditTriggers默认是双击或键盘输入都能编辑,如果表格只是展示用,必须设成NoEditTriggers,不然用户不小心双击就改了数据,又不知道怎么撤销。
右键菜单是表格的标配。正确做法是setContextMenuPolicy(Qt::CustomContextMenu),然后连接customContextMenuRequested(const QPoint &pos)信号,在槽函数里用itemAt(pos)判断当前右键到哪个单元格。这个写法比重写contextMenuEvent更简洁,也更容易复用。
4.3 从QTableWidget到QTableView加自定义model
当表格数据量超过一两千行,或者需要频繁更新数据时,QTableWidget的遍历插入和全量刷新都会拖慢界面。这个时候就该换成QTableView加QAbstractTableModel。QTableView本身只向model请求当前可见区域的若干行数据,滚动时会动态加载,而不是一次性把所有数据都准备好。
自定义model的核心是重写四个方法:rowCount、columnCount、data、headerData。下面是一个只读表格model的最小示例:
class MyTableModel : public QAbstractTableModel { Q_OBJECT public: int rowCount(const QModelIndex &parent = QModelIndex()) const override { if (parent.isValid()) return 0; return m_items.size(); } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return parent.isValid() ? 0 : 3; } QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid()) return {}; if (role == Qt::DisplayRole) { if (index.column() == 0) return m_items[index.row()].name; if (index.column() == 1) return m_items[index.row()].count; } return {}; } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if (role != Qt::DisplayRole) return {}; if (orientation == Qt::Horizontal) { if (section == 0) return "名称"; if (section == 1) return "数量"; } return {}; } private: QVector<ItemData> m_items; };真正让数据改动反映到界面上,不是直接调用view的update,而是在model里发通知。插入一行用beginInsertRows(parent, first, last),插完再endInsertRows();整体数据变了用beginResetModel()和endResetModel()。很多初学者直接在model内部加一个emit dataChanged(index, index),但index范围写错了,结果界面不刷新,一直找不到原因。记住这个原则:结构性变更用begin/end系列,局部值变化用dataChanged。
这个架构还有一个额外好处:你可以轻松加一个QSortFilterProxyModel做排序和过滤,而不用改原始数据。比如表格上方放个QLineEdit,输入过滤条件,让proxy model动态过滤行,再让view显示proxy model。这个模式我几乎每个管理端项目都会用,真实效率比在QTableWidget里逐行setRowHidden高一个量级。
4.4 树和列表:QTreeWidget到QTreeView
QTreeWidget的常见属性和QTableWidget很像,但它多了一个“父子层级”概念。创建树节点时要分清顶层节点和子节点:
QTreeWidget *tree = new QTreeWidget(this); tree->setColumnCount(2); tree->setHeaderLabels({"设备", "状态"}); QTreeWidgetItem *parentItem = new QTreeWidgetItem(tree, {"PLC-01", "在线"}); QTreeWidgetItem *childItem = new QTreeWidgetItem(parentItem, {"模块A", "正常"}); tree->expandAll();QTreeWidgetItem的父子关系是在构造时传入父指针决定的,注意两个构造重载:第一个参数是tree,创建顶层项;第二个参数是另一个item,创建子项。如果你发现节点没挂上去,先检查是不是把父指针传错了。
QListView相对简单,最常用的model是QStringListModel和自定义QAbstractListModel。列表项的拖拽、检查框、工具提示这些能力,自定义model时通过flags()返回对应枚举实现,比如想支持勾选,就返回Qt::ItemIsUserCheckable,然后在data里响应Qt::CheckStateRole。
最后说一点model/view的深层逻辑:QListView/QTreeView/QTableView都遵循同一个接口,所以同一个model可以同时被多个视图显示,比如左边一个QListView,右边一个QTableView,显示同一份数据的不同视角。这种能力在复杂业务系统里非常关键,也是从“摆控件”思维升级到“数据驱动界面”思维的一道坎。
4.5 大数据的避坑:只加载可见数据
回到开头那个热搜索问题:QTableWidget大数据卡顿优化。我实际踩过这个坑之后总结了一套优化路径。第一步,确定你是否真的需要一次性显示全部数据。如果是报表语义,用户可能只看前100行,那分页是最合适的;如果必须滚动浏览,就用QTableView加自定义model,让view按需取数。第二步,打开setUniformRowHeights(true)。这个属性告诉view:所有行高一样,滚动时不需要逐个计算行高,能省掉大量时间。第三步,避免在model的data里做耗时操作,比如数据库查询、文件IO,应该提前把数据缓存到内存结构里,data只管快速返回。第四步,数据更新时尽量用dataChanged局部刷新可见区域,不要动不动beginResetModel,否则滚动条位置、选中状态都会被重置。
这套组合拳做完,几万行的表格滚动基本能流畅到接近原生速度。如果再要更快,就得考虑把大文本字段做延迟加载或者用代理模型做分块取数,但那已经是另一个主题了。
5. 布局、样式和编译期常见的坑
5.1 布局管理器的正确使用
初学者最容易犯的错误是手动给控件设置坐标和大小,然后窗口一变尺寸就乱成一团。QT里正确的做法永远是:创建控件,设置好控件自身的属性,然后把它add到布局里,让布局管理器负责排布。
常见的布局是QVBoxLayout、QHBoxLayout、QGridLayout、QFormLayout。表单场景用QFormLayout比手动拼两个QHBoxLayout方便很多。使用布局时有一个容易踩的坑:同一个控件不能同时被add到多个布局里,也不能在加入布局之后又调用setLayout。如果有人告诉你“界面上控件不见了”,先检查是不是你自己把布局对象在栈上创建,然后忘了传递给窗口。
布局中还有两个高频属性:stretch和sizePolicy。layout->addWidget(widget, stretch)里的stretch表示控件的伸缩权重,比如左侧列表stretch为1,右侧编辑区stretch为3,窗口拉大时右侧会占更多空间。setSizePolicy则控制单个控件是否愿意伸缩。希望中间表格随窗口变大、底部按钮保持固定,就把按钮的垂直sizePolicy设为Fixed,表格设为Expanding。这样写出来的界面,不管窗口怎么拉伸都不会变形。
5.2 QSS让控件颜值提升的写法
QSS是QT样式表,语法很像CSS,但它不是完整的CSS,很多属性不支持。给按钮写QSS时,最常用的伪状态是:hover、:pressed、:disabled、:checked。下面这段是我常用的按钮样式模板:
#saveButton { background-color: #2d6cdf; color: white; border: none; border-radius: 6px; padding: 6px 16px; } #saveButton:hover { background-color: #4a83ef; } #saveButton:pressed { background-color: #2056b8; } #saveButton:disabled { background-color: #a8b8d0; color: #d0d0d0; }写QSS时,对象名选择器#saveButton是最稳的,因为它只对指定控件生效。不要频繁用QPushButton {}这种全局选择器,不然两个不同样式的按钮就会被互相覆盖。还有一点:QSS设置了颜色和背景之后,按钮在Windows上的原生样式会丢失,这是正常现象,不需要担心。
QSS对复杂控件支持有限,比如QCheckBox的勾选框、QComboBox下拉箭头,这些子控件的绘制需要单独用::indicator、::drop-down这些子控件选择器。如果调不动,别死磕QSS,可以考虑用QProxyStyle自定义绘制,或者换一个更轻量的UI方案。样式表一时不到位的,先保功能,再保视觉,不要卡在样式上半天。
5.3 控件不刷新或崩溃的排查思路
界面卡在旧状态、多次点击后崩溃,这类问题多半和线程、对象生命周期有关。先说线程:QT的界面操作必须在主线程执行,如果在工作线程里直接调用setText、update,轻则不刷新,重则直接崩溃。正确做法是工作线程发出信号,主线程的槽函数再更新界面,或者使用QTimer、QMetaObject::invokeMethod把更新请求投递到主线程。
再说对象生命周期。控件一旦把某个对象作为父对象,比如new QLabel(this),子对象就归父对象管了,父对象析构时会自动删除子对象。这时候如果你在成员变量里又手动delete一次,就会double-free崩溃。类似的坑还发生在QTableWidgetItem、QListWidgetItem上,它们属于对应的控件,交给控件之后不要手动delete。
如果控件不是不刷新,而是刷新太慢,优先怀疑是不是在循环里频繁设置属性。比如在for循环里挨个setItem,每次setItem都会触发一次布局和重绘,可以先用QSignalBlocker阻断信号,或者把操作放到model的begin/end批次里,一次性提交。
5.4 编译错误:dependent qtwidget include找不到怎么办
很多新手在.pro里只写了QT += core gui,然后代码里用到了QPushButton、QTableView,编译时就会报类似这样一串错误::-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets/qpushbutton' does not exist.这个报错的本质不是头文件真的丢了,而是Qt Widgets模块没有被引入,编译器根本找不到对应的头文件路径。
解决方法是:在qmake工程里给.pro加一行QT += widgets;在CMake工程里,用find_package(Qt6 COMPONENTS Widgets)并且target_link_libraries(项目名 Qt6::Widgets)。如果用的是QTableView、QSortFilterProxyModel,还需要QT += widgets是基础;用到网络、SQL、打印时,再分别加network、sql、printsupport。
另外,Windows下常见的Qt 5.15.2 msvc2019_64套件是给MSVC用的,如果你用MinGW编译器去加载这个库,也会出现各种链接错误。下载时看清套件版本,项目里保持MSVC或MinGW其中一套即可,别混着来。还有路径问题:项目工程的完整路径如果又长又深,中间还有多层..\,编译器在某些情况下会报“路径太长”的奇怪错误,尽量把项目放到一个短路径目录下,比如D:\QtProjects\demo01。
5.5 用Designer还是纯代码写界面
这个问题几乎每期都会被问。我的个人建议是:学习阶段优先用Qt Designer拖控件、看右侧属性表,这是理解属性最快的方式。但真正上了复杂项目,我基本都是手写代码或结合Designer生成的ui配合使用,因为纯代码可以更方便地动态增删控件、批量创建、通过循环配置属性。
如果使用Designer生成的ui界面,记得在构造函数里ui->setupUi(this),之后就可以通过ui->对象名访问控件。但要注意,Designer里设置的属性是初始值,代码里后设置的属性会覆盖它。想要在Designer里快速找某个属性位置,右侧属性表里搜索框非常有用,比如输入“text”,所有text相关属性都会列出来。
6. 写控件代码的几条实操建议
6.1 搭一个“控件试验台”
我个人学控件时最受益的办法,是新建一个空窗口,把要学的控件全部摆上去,每组控件旁边放一个“显示状态”的QLabel,把所有相关信号都connect到同一个槽函数,在槽里用qDebug打印参数变化。比如把QCheckBox的toggled、QRadioButton的toggled、QComboBox的currentIndexChanged全打出来,跑一遍就知道每个信号在什么时候触发、参数是什么。
试验台不需要做得好看,重点是能实时观察。很多细节是文档里看不到的,比如QLineEdit输入过程中什么时候发textChanged、什么时候发editingFinished,必须亲手点一遍才有体感。这个方法尤其适合跨平台开发,Windows和Linux上按钮的focus、键盘操作会有细微差异,跑一遍就都清楚了。
6.2 接线和命名规范
信号槽连接时,现在应该优先用指针语法:connect(btn, &QPushButton::clicked, this, &MainWindow::saveFile)。这种写法在编译期就能检查信号和槽是否存在,比旧的SIGNAL/SLOT宏安全得多。如果要在connect里写lambda,注意捕获this时要小心,lambda会捕获上下文,窗口销毁后如果信号还从别的线程发出来,就可能访问悬空指针。
控件命名直接影响代码可读性。我的习惯是:普通按钮btnSave、输入框editIp、标签labelTitle、表格tableDevices,然后在Designer或代码里保持一致。命名不用过分冗长,但一定让人一眼看出它是什么控件、干什么用的。这个习惯早期可能看不出价值,等项目到几万行代码时,检索和定位会快好几倍。
6.3 每次只改一个属性,观察变化
界面调试时最忌讳一次改一堆属性,然后出了问题不知道是哪一步引起的。我踩过的教训是:有一次按钮怎么都不显示文字,我先后检查了setText、setStyleSheet、字体颜色、对象名,结果发现只是控件被一个透明的默认图片盖住了,pixmap把文字挡住了。这个问题如果每次只改一个属性,第一眼就能发现。
正确的调试方法是:在改动前后各截图或运行一次,只保留一个变量,逐步逼近问题源头。如果怀疑是样式问题,可以暂时清空setStyleSheet;如果怀疑是布局问题,可以给控件加一个临时背景色,比如setStyleSheet("background: red"),立刻就能看到控件实际占的区域。这招在解决控件“不见了”和“位置不对”时非常管用。
最后分享一个我常用的收尾小技巧:控件属性全都调完后,记得统一考虑键盘操作。比如表单提交按钮设置default属性,用户按回车就能触发;输入框之间用tab顺序排列,让用户不碰鼠标也能完成整张表单。很多人只关注视觉,忽略了这一层,但实际项目里,尤其是工业现场键盘操作环境,这个细节体验差距特别明显。QT的控件学习和任何工具类知识一样,光看不练没用,建议你照着这篇把按钮、Label、输入框、view系列都拖到一个窗口里试一遍,踩过几个坑之后基本就长成肌肉记忆了。