
一、前言
在学习 Qt 的过程中,很多初学者都会遇到这样一种情况:
- 头文件里声明的函数,在源文件中没有实现;
- 源文件中实现的函数,头文件里又没有声明;
- 使用旧式
SIGNAL/SLOT和新式connect()混用; - 点击一次按钮,槽函数却执行了两次。
本文以 Qt 5.9 的一个 QWidget 项目为例,完整分析 widget.h 与 widget.cpp 两个文件,并实现以下功能:
- 输入程序路径后按回车启动;
- 点击"确定"按钮启动程序;
- 点击"取消"按钮关闭窗口;
- 点击"浏览"按钮弹出提示框;
- 使用
QProcess::startDetached()启动外部程序。
二、项目界面中的控件
当前 .ui 文件中主要包含以下控件:
| 控件类型 | 对象名 | 功能 |
|---|---|---|
QLineEdit |
cmdLineEdit |
输入程序路径 |
QPushButton |
pushButtocommitButtonn |
确定 |
QPushButton |
cancelButton |
取消 |
QPushButton |
browseButton |
浏览 |
在 Qt 中,.ui 文件中的对象名会自动变成 ui 指针下面的成员:
ui->cmdLineEdit
ui->pushButtocommitButtonn
ui->cancelButton
ui->browseButton
其中 pushButtocommitButtonn 的拼写并不规范,但为了保证当前代码能够和 .ui 文件中的对象名一致,暂时需要保持同名。
三、原本存在的问题
1. 头文件和源文件声明不一致
源文件中实现的是:
void Widget::handleCommit()
{
}
void Widget::handleCancel()
{
}
但头文件中声明的是:
private slots:
void on_pushButtocommitButtonn_clicked();
void on_cancelButton_clicked();
这会导致编译器报类似错误:
error: no declaration matches 'void Widget::handleCommit()'
undefined reference to 'Widget::handleCommit()'
在 C++ 中,成员函数必须满足:
头文件中的声明
=
源文件中的定义
因此以下写法必须保持一致:
// widget.h
void handleCommit();
// widget.cpp
void Widget::handleCommit()
{
}
2. QmessageBox 大小写错误
错误写法:
QmessageBox::information(this, "信息", "点击浏览");
正确写法:
QMessageBox::information(this, "信息", "点击浏览");
Qt 类名是:
QMessageBox
其中 M 和 B 都需要大写。
对应头文件:
#include <QMessageBox>
3. 类名拼写错误
错误写法:
void Weiget::on_cancelButton_clicked()
正确写法:
void Widget::on_cancelButton_clicked()
类名必须是:
Widget
否则会报:
error: 'Weiget' has not been declared
4. 取消按钮可能重复连接
使用 Qt Designer 创建的界面,在 ui_widget.h 中通常会自动生成:
QMetaObject::connectSlotsByName(Widget);
如果你的头文件中有:
private slots:
void on_cancelButton_clicked();
而按钮对象名是:
cancelButton
那么 Qt 会自动建立连接:
cancelButton.clicked()
↓
on_cancelButton_clicked()
此时如果代码中再写一次:
connect(ui->cancelButton,
&QPushButton::clicked,
this,
&Widget::on_cancelButton_clicked);
点击一次取消按钮,就可能导致槽函数执行两次。
解决方式有两种:
方式一:保留自动连接,删除手动连接
private slots:
void on_cancelButton_clicked();
源文件中只定义,不再手动 connect()。
方式二:使用新的处理函数名,全部显式连接
private:
void handleCommit();
void handleCancel();
然后在构造函数中:
connect(ui->cancelButton,
&QPushButton::clicked,
this,
&Widget::handleCancel);
本文推荐第二种方式,因为命名更加清晰,而且所有连接都是显式建立的。
5. QProcess 对象没有及时释放
原来的写法:
QProcess *myProcess = new QProcess(this);
myProcess->start(program);
虽然指定了父对象 this,最终会随窗口销毁,但每次点击按钮都会创建一个新的 QProcess,在窗口关闭前可能不断累积。
如果不需要读取进程输出,推荐使用:
QProcess::startDetached(program, QStringList());
它可以在当前程序退出后继续运行被启动的进程。
四、推荐使用的 widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
private:
void handleCommit();
void handleCancel();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
重点说明:
Q_OBJECT必须保留。handleCommit()和handleCancel()只是普通私有成员函数。- 使用新式
connect()时,处理函数不一定需要写成slots。 QProcess和QMessageBox在.cpp中使用,不需要放进头文件。- 析构函数必须是
~Widget(),不能受 Markdown 转义符号影响。
五、推荐使用的 widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QLineEdit>
#include <QMessageBox>
#include <QProcess>
#include <QPushButton>
#include <QStringList>
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
// 输入框按回车时执行
connect(ui->cmdLineEdit,
&QLineEdit::returnPressed,
this,
&Widget::handleCommit);
// 点击确定按钮时执行
connect(ui->pushButtocommitButtonn,
&QPushButton::clicked,
this,
&Widget::handleCommit);
// 点击取消按钮时关闭窗口
connect(ui->cancelButton,
&QPushButton::clicked,
this,
&Widget::handleCancel);
// 点击浏览按钮
connect(ui->browseButton,
&QPushButton::clicked,
this,
[this]() {
QMessageBox::information(this, "信息", "点击浏览");
});
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleCommit()
{
const QString program = ui->cmdLineEdit->text().trimmed();
if (program.isEmpty()) {
QMessageBox::warning(this, "提示", "请输入要启动的程序");
return;
}
if (!QProcess::startDetached(program, QStringList())) {
QMessageBox::warning(this,
"错误",
"程序启动失败:" + program);
}
}
void Widget::handleCancel()
{
close();
}
六、信号与槽连接详解
1. 输入框回车启动程序
connect(ui->cmdLineEdit,
&QLineEdit::returnPressed,
this,
&Widget::handleCommit);
参数含义:
| 参数 | 含义 |
|---|---|
ui->cmdLineEdit |
信号发送者 |
&QLineEdit::returnPressed |
输入框的回车信号 |
this |
信号接收者 |
&Widget::handleCommit |
接收信号后执行的成员函数 |
当用户在输入框中按回车时,就会执行:
handleCommit();
2. 点击确定按钮启动程序
connect(ui->pushButtocommitButtonn,
&QPushButton::clicked,
this,
&Widget::handleCommit);
当"确定"按钮被点击时,同样执行:
handleCommit();
因此,按回车和点击确定按钮,最终都会进入同一个函数,代码复用性更好。
3. 点击取消按钮关闭窗口
connect(ui->cancelButton,
&QPushButton::clicked,
this,
&Widget::handleCancel);
槽函数:
void Widget::handleCancel()
{
close();
}
close() 会关闭当前窗口。
由于主函数中使用的是:
Widget w;
w.show();
return a.exec();
当最后一个窗口关闭后,Qt 应用程序通常会退出事件循环。
4. 使用 Lambda 表达式处理浏览按钮
connect(ui->browseButton,
&QPushButton::clicked,
this,
[this]() {
QMessageBox::information(this, "信息", "点击浏览");
});
Lambda 表达式中的:
[this]
表示捕获当前对象,这样就可以使用:
this
作为 QMessageBox 的父窗口。
如果捕获列表写成:
[]
那么 Lambda 内部不能直接使用 this。
七、QProcess::startDetached() 语法说明
当前代码使用:
QProcess::startDetached(program, QStringList());
其中:
program:要启动的程序路径QStringList():空的命令行参数列表- 返回值:
bool类型
返回 true 表示启动成功,返回 false 表示启动失败。
因此可以写成:
if (!QProcess::startDetached(program, QStringList())) {
QMessageBox::warning(this,
"错误",
"程序启动失败:" + program);
}
需要启动带参数的程序时,可以这样写:
QString program = "C:/Tools/demo.exe";
QStringList arguments;
arguments << "--mode" << "debug";
QProcess::startDetached(program, arguments);
不过,当前界面只有一个输入框,因此暂时只适合输入单个程序路径,不适合输入"程序路径 + 参数"。
八、完整运行流程
程序启动后:
main.cpp
↓
创建 QApplication
↓
创建 Widget
↓
调用 ui->setupUi(this)
↓
建立信号与槽连接
↓
进入 a.exec() 事件循环
用户操作流程:
输入程序路径
↓
按回车或点击"确定"
↓
读取 cmdLineEdit 内容
↓
检查内容是否为空
↓
QProcess::startDetached()
↓
启动外部程序
如果路径为空:
输入框为空
↓
弹出"请输入要启动的程序"
↓
直接返回,不启动进程
九、常见编译错误汇总
1. 声明和定义不一致
no declaration matches
检查 .h 和 .cpp 中函数名、参数和返回类型是否完全一致。
2. 未定义引用
undefined reference to 'Widget::handleCommit()'
通常是因为:
- 头文件声明了函数,但没有实现;
.cpp没有加入.pro;- 函数名拼写不一致;
- 参数列表不一致。
3. 找不到 QMessageBox
'QmessageBox' has not been declared
修改为:
QMessageBox
并引入:
#include <QMessageBox>
4. 无法连接到槽函数
旧式写法:
connect(ui->cmdLineEdit,
SIGNAL(returnPressed()),
this,
SLOT(on_pushButtocommitButtonn_clicked()));
这种写法使用字符串匹配,如果函数名或参数写错,编译阶段不一定能发现。
新式写法:
connect(ui->cmdLineEdit,
&QLineEdit::returnPressed,
this,
&Widget::handleCommit);
新式写法会在编译阶段检查类型,因此更加安全。
十、后续优化方向
1. 修改按钮对象名
建议将:
pushButtocommitButtonn
改成:
commitButton
修改 .ui 后,C++ 中也要同步修改:
ui->commitButton
2. 真正实现"浏览"功能
当前"浏览"按钮只是弹出提示信息,可以改成选择可执行程序:
QString file = QFileDialog::getOpenFileName(
this,
"选择程序",
QString(),
"可执行程序 (*.exe);;所有文件 (*)"
);
if (!file.isEmpty()) {
ui->cmdLineEdit->setText(file);
}
需要引入:
#include <QFileDialog>
3. 增加进程错误处理
如果需要在程序启动后读取输出、错误信息或退出状态,可以改为成员形式的 QProcess:
QProcess *process = new QProcess(this);
connect(process,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
process,
&QObject::deleteLater);
process->start(program);
十一、总结
本文围绕 Qt 中的 widget.h 和 widget.cpp,解决了以下几个核心问题:
- 头文件声明与源文件实现必须一致;
QMessageBox类名必须正确;- 类名
Widget不能拼写成Weiget; - 新式
connect()比旧式SIGNAL/SLOT更安全; connectSlotsByName()可能产生自动连接;- 启动外部程序可以使用
QProcess::startDetached(); - 使用 Lambda 表达式可以方便地处理按钮点击事件。
最终代码结构可以概括为:
widget.h
├── 声明 handleCommit()
├── 声明 handleCancel()
└── 保存 Ui::Widget *ui
widget.cpp
├── 调用 setupUi()
├── 建立回车、确定、取消、浏览连接
├── 使用 QProcess 启动程序
├── 检查空输入和启动失败
└── 实现窗口关闭
按照本文修改后,widget.h 和 widget.cpp 的声明、定义和信号槽连接就能保持一致,程序也可以正常编译运行。