如果你正在学习C++,并且希望用它来开发桌面应用、嵌入式界面或者跨平台软件,那么“Qt”这个名字你一定不陌生。但你可能也听过这样的困惑:Qt到底是不是MFC的替代品?为什么从C++基础到能做出一个像样的Qt应用,中间感觉隔着一道鸿沟?网上教程要么是零散的“Hello World”,要么是过于庞大的开源项目,让人无从下手。
这篇文章要解决的,正是这个核心痛点:如何将扎实的C++知识,高效、无痛地转化为Qt桌面开发的实际能力,并避开那些新手必然会踩的99%的坑。我们不会空谈“Qt很强大”,而是直接切入实战。你将看到,从环境搭建的第一个命令开始,到最终打包发布一个可安装的程序,每一个步骤都有明确的代码、配置和排错指南。本文的目标是成为一份“最佳实践指南”,让你在学习和求职路上,把时间花在刀刃上,而不是在环境配置和诡异Bug上反复折腾。
我们将从最关键的判断开始:Qt不是一个简单的GUI库,它是一个完整的应用程序框架。这意味着学习Qt,不仅仅是学习怎么画窗口和按钮,更是学习一种以“信号与槽”为核心的事件驱动编程模型,以及一套成熟的工程化开发流程。理解了这一点,你就知道为什么不能只学语法,而必须动手构建项目。
1. 环境搭建:从“能用”到“好用”的精准配置
环境是学习的第一道门槛。一个稳定、高效的开发环境能让你后续的学习事半功倍。对于Qt开发,我们强烈推荐使用Qt Creator作为集成开发环境(IDE),它由Qt官方维护,对Qt的支持最为完善。
1.1 安装Qt开发套件
不要从零开始编译Qt源码,对于初学者和绝大多数开发者,使用官方安装器是最佳选择。
下载Qt安装器:访问Qt官网,下载对应操作系统的在线安装程序。Windows用户通常下载
.exe,macOS用户下载.dmg,Linux用户下载.run文件。运行安装器并登录/注册:运行安装程序,你需要一个Qt账户(免费注册)。
选择组件:这是最关键的一步,选错会导致后续开发无法进行。
- Qt版本:建议选择长期支持(LTS)版本,如
Qt 6.6 LTS或Qt 6.8。它们更稳定,社区资源更丰富。对于历史项目,可能需要Qt 5.15 LTS。 - 编译器:
- Windows: 勾选
MSVC 2019 64-bit或MinGW 11.2.0 64-bit。MSVC与Visual Studio兼容性好;MinGW更轻量,打包方便。二选一即可,建议新手选MinGW。 - macOS: 勾选
macOS。 - Linux: 勾选
GCC。
- Windows: 勾选
- 额外组件:务必勾选
Qt Creator(默认已勾选)。对于初学者,Qt Design Studio可以先不装。Qt Debugging Tools建议勾选。 - Archives:可以跳过,这是源码包。
- Qt版本:建议选择长期支持(LTS)版本,如
完成安装:选择安装路径(路径不要有中文或空格),然后等待安装完成。
1.2 配置Qt Creator与第一个项目
安装完成后,打开Qt Creator。
检查Kits(套件):点击
工具->选项->Kits。你应该能看到一个自动检测到的套件,包含了正确的Qt版本和编译器。这是项目能编译运行的基础。创建第一个项目:点击
文件->新建文件或项目。- 选择
Application->Qt Widgets Application。 - 为项目命名(如
HelloQt),选择不含中文的路径。 - 在
Kit Selection页面,勾选你刚才检查到的套件。 - 在
类信息页面,可以修改主窗口的类名(如MainWindow),基类保持QMainWindow不变。 - 点击完成。
- 选择
运行项目:点击Qt Creator左下角的绿色运行按钮(或按
Ctrl+R)。如果一切顺利,你将看到一个空白的窗口。恭喜,你的Qt开发环境已经就绪。
关键避坑点:如果运行时提示类似“this application failed to start because no qt platform plugin could be initialized”的错误,这通常是程序运行时找不到Qt的动态链接库。解决方案有两种:
- 开发阶段:在Qt Creator中运行,环境是配置好的。
- 发布阶段:需要使用
windeployqt(Windows)或macdeployqt(macOS)工具来打包所有依赖库。我们会在第8章详细讲解。
2. Qt核心概念:信号与槽、元对象系统与内存管理
在写代码之前,必须理解Qt框架的三大基石。这能让你知其然,更知其所以然。
2.1 信号与槽(Signals & Slots):Qt的事件通信机制
这是Qt最核心的特性,用于对象间的通信。它替代了传统的回调函数,更安全、灵活。
- 信号(Signal):由对象在某种事件发生时“发出”。比如,按钮被点击时会发出
clicked()信号。 - 槽(Slot):是一个普通的成员函数,用于“响应”特定的信号。比如,定义一个函数用于关闭窗口。
它们通过QObject::connect函数进行连接。这种连接是类型安全的,并且在运行时可以建立或断开。
// 示例:点击按钮,关闭窗口 // 在某个类(如MainWindow)的构造函数中 QPushButton *button = new QPushButton("退出", this); connect(button, &QPushButton::clicked, this, &MainWindow::close); // 解释:将 button 的 clicked 信号,连接到 this(主窗口)的 close 槽。2.2 元对象系统(Meta-Object System)
信号与槽、运行时类型信息(RTTI)、动态属性系统等功能的基础。任何使用信号与槽的类,都必须满足两个条件:
- 继承自
QObject(或它的子类)。 - 在类声明的私有部分添加
Q_OBJECT宏。
// 文件:mydialog.h #ifndef MYDIALOG_H #define MYDIALOG_H #include <QDialog> class MyDialog : public QDialog { Q_OBJECT // 必须添加的宏 public: explicit MyDialog(QWidget *parent = nullptr); signals: // 声明信号 void dataReady(const QString &data); public slots: // 声明槽函数 void processButtonClick(); private: Ui::MyDialog *ui; }; #endif // MYDIALOG_H2.3 内存管理:父子对象机制
Qt通过对象树(Object Tree)来简化内存管理。当一个QObject对象被创建时,可以指定一个父对象(Parent)。
- 父对象删除时,会自动删除其所有子对象。这极大地防止了内存泄漏。
- 在Qt中,几乎所有可视化组件(
QWidget及其子类)都可以通过设置父对象来自动管理其生命周期。
// 正确做法:指定父对象,无需手动delete QLabel *label = new QLabel("Hello", this); // `this` 是父窗口 // 错误做法:没有父对象,且未手动删除,会导致内存泄漏 QLabel *label = new QLabel("Hello");3. 使用Qt Designer进行可视化界面设计
Qt Designer是一个强大的GUI布局工具,可以拖拽组件生成.ui文件,再通过uic工具将其转换为C++代码。这能极大提升界面开发效率。
3.1 设计一个简单的登录对话框
- 在Qt Creator中,右键项目 ->
添加新文件->Qt->Qt Designer Form Class。选择Dialog without Buttons,命名为LoginDialog。 - Qt Creator会自动生成三个文件:
logindialog.h,logindialog.cpp,logindialog.ui。双击.ui文件,打开Qt Designer。 - 从左侧组件面板拖拽以下组件到窗体上:
QLabel(两个):文本改为“用户名:“和”密码:“。QLineEdit(两个):一个用于用户名,另一个用于密码。选中密码输入框,在右侧属性编辑器中找到echoMode,将其改为Password。QPushButton(两个):文本改为“登录”和“取消”。
- 使用布局管理器(Layouts)进行排版。选中所有组件,点击工具栏上的“垂直布局”或“水平布局”按钮,或者使用弹簧(Spacers)让界面更美观。
- 保存
.ui文件。
3.2 将UI组件与代码关联
.ui文件会被编译成ui_xxxx.h头文件。在对话框类的头文件和源文件中,可以通过ui指针来访问界面上的组件。
// 文件:logindialog.h (部分) #include <QDialog> namespace Ui { class LoginDialog; // 前向声明 } class LoginDialog : public QDialog { Q_OBJECT public: explicit LoginDialog(QWidget *parent = nullptr); ~LoginDialog(); private slots: void on_loginButton_clicked(); // 命名规则:on_对象名_信号名() private: Ui::LoginDialog *ui; // UI指针 };// 文件:logindialog.cpp #include "logindialog.h" #include "ui_logindialog.h" #include <QMessageBox> LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::LoginDialog) // 初始化ui指针 { ui->setupUi(this); // 关键!建立UI与类的关联 // 可以在这里进行额外的初始化,例如设置输入框限制 } LoginDialog::~LoginDialog() { delete ui; // 清理ui } void LoginDialog::on_loginButton_clicked() { QString username = ui->userLineEdit->text(); QString password = ui->pwdLineEdit->text(); // 简单的验证逻辑 if(username == "admin" && password == "123456") { QMessageBox::information(this, "成功", "登录成功!"); accept(); // 关闭对话框并返回 QDialog::Accepted } else { QMessageBox::warning(this, "失败", "用户名或密码错误!"); ui->pwdLineEdit->clear(); ui->pwdLineEdit->setFocus(); } } // 注意:`on_loginButton_clicked` 这个函数名是Qt的特定命名规则,Qt的元对象系统会自动将其与 loginButton 的 clicked() 信号连接。这是一种隐式连接。4. 模型/视图(Model/View)编程:高效处理数据展示
对于表格、列表、树形等复杂数据展示,Qt提供了模型/视图架构,将数据(Model)和显示(View)分离,这是Qt高级开发必须掌握的技能。
4.1 使用QStandardItemModel和QTableView
假设我们需要显示一个学生信息表格(姓名,年龄,成绩)。
// 在某个窗口类的方法中(如MainWindow的初始化函数) #include <QStandardItemModel> #include <QTableView> void MainWindow::initTableView() { // 1. 创建数据模型 QStandardItemModel *model = new QStandardItemModel(this); model->setHorizontalHeaderLabels({"姓名", "年龄", "成绩"}); // 2. 填充数据 QList<QStandardItem*> row1; row1 << new QStandardItem("张三") << new QStandardItem("20") << new QStandardItem("85"); model->appendRow(row1); QList<QStandardItem*> row2; row2 << new QStandardItem("李四") << new QStandardItem("22") << new QStandardItem("92"); model->appendRow(row2); // 3. 创建视图并设置模型 QTableView *tableView = new QTableView(this); tableView->setModel(model); // 4. 可选:设置表格属性 tableView->setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选择 tableView->horizontalHeader()->setStretchLastSection(true); // 最后一列填充 // 5. 将视图添加到布局中 setCentralWidget(tableView); // 如果MainWindow是主窗口 }4.2 自定义模型(QAbstractTableModel)
当数据源来自数据库、网络或复杂数据结构时,需要自定义模型以提高效率和灵活性。
// 文件:studentmodel.h #ifndef STUDENTMODEL_H #define STUDENTMODEL_H #include <QAbstractTableModel> #include <QVector> struct Student { QString name; int age; double score; }; class StudentModel : public QAbstractTableModel { Q_OBJECT public: explicit StudentModel(QObject *parent = nullptr); // 必须重写的纯虚函数 int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; // 可选:支持编辑 bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Qt::ItemFlags flags(const QModelIndex &index) const override; // 自定义方法:操作数据 void addStudent(const Student &student); void removeStudent(int row); private: QVector<Student> m_students; }; #endif // STUDENTMODEL_H// 文件:studentmodel.cpp #include "studentmodel.h" StudentModel::StudentModel(QObject *parent) : QAbstractTableModel(parent) {} int StudentModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_students.size(); } int StudentModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 3; // 姓名、年龄、成绩三列 } QVariant StudentModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= m_students.size()) return QVariant(); const Student &student = m_students.at(index.row()); if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (index.column()) { case 0: return student.name; case 1: return student.age; case 2: return student.score; default: return QVariant(); } } return QVariant(); } QVariant StudentModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { switch (section) { case 0: return "姓名"; case 1: return "年龄"; case 2: return "成绩"; default: return QVariant(); } } return QVariant(); } void StudentModel::addStudent(const Student &student) { beginInsertRows(QModelIndex(), m_students.size(), m_students.size()); m_students.append(student); endInsertRows(); } // 其他函数实现略...使用自定义模型,视图(如QTableView)会自动感知数据变化并更新显示,这是模型/视图架构的强大之处。
5. 多线程编程:使用QThread与QtConcurrent
GUI程序必须保持界面响应流畅,耗时操作(如文件读写、网络请求、复杂计算)必须放在后台线程中。Qt提供了多种多线程方案。
5.1 继承QThread(传统方式)
// 文件:workthread.h #ifndef WORKTHREAD_H #define WORKTHREAD_H #include <QThread> class WorkThread : public QThread { Q_OBJECT public: explicit WorkThread(QObject *parent = nullptr); signals: void progressUpdated(int value); // 用于报告进度 void workFinished(const QString &result); // 用于报告完成 protected: void run() override; // 线程入口函数 }; #endif // WORKTHREAD_H// 文件:workthread.cpp #include "workthread.h" #include <QDebug> #include <QThread> WorkThread::WorkThread(QObject *parent) : QThread(parent) {} void WorkThread::run() { for (int i = 0; i <= 100; ++i) { QThread::msleep(50); // 模拟耗时操作 emit progressUpdated(i); // 发射进度信号 if (isInterruptionRequested()) { // 检查是否被请求中断 qDebug() << "Thread interrupted."; return; } } emit workFinished("Task completed."); }在主线程中使用:
// 创建并启动线程 WorkThread *thread = new WorkThread(this); connect(thread, &WorkThread::progressUpdated, ui->progressBar, &QProgressBar::setValue); connect(thread, &WorkThread::workFinished, this, [](const QString &msg){ qDebug() << msg; }); connect(thread, &WorkThread::finished, thread, &QObject::deleteLater); // 线程结束后自动删除对象 thread->start(); // 需要停止线程时(例如在窗口关闭时) void MainWindow::closeEvent(QCloseEvent *event) { if (thread && thread->isRunning()) { thread->requestInterruption(); // 请求中断 thread->wait(); // 等待线程结束(可选,注意可能阻塞) } event->accept(); }5.2 使用QtConcurrent(更现代、简洁)
对于简单的函数或Lambda表达式,QtConcurrent::run是更轻量的选择。
#include <QtConcurrent/QtConcurrent> #include <QFutureWatcher> void MainWindow::startConcurrentTask() { // 定义一个耗时的函数 auto heavyTask = []() -> QString { QThread::sleep(2); // 模拟2秒工作 return "Result from concurrent task"; }; // 在后台线程中运行该函数 QFuture<QString> future = QtConcurrent::run(heavyTask); // 使用QFutureWatcher来监控结果 QFutureWatcher<QString> *watcher = new QFutureWatcher<QString>(this); connect(watcher, &QFutureWatcher<QString>::finished, this, [this, watcher](){ QString result = watcher->result(); ui->resultLabel->setText(result); watcher->deleteLater(); }); watcher->setFuture(future); }6. 文件与网络操作
6.1 文件读写:QFile与QTextStream
#include <QFile> #include <QTextStream> #include <QDebug> // 写文件 void writeFile(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qDebug() << "Cannot open file for writing:" << file.errorString(); return; } QTextStream out(&file); out << "Hello, Qt!\n"; out << "This is a text file.\n"; file.close(); // 析构时也会自动关闭,但显式关闭是好习惯 } // 读文件 QString readFile(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return "Error: " + file.errorString(); } QTextStream in(&file); QString content = in.readAll(); file.close(); return content; }6.2 网络请求:使用QNetworkAccessManager(推荐)
Qt的网络模块是异步的,基于信号与槽,非常适合GUI程序。
// 在头文件中声明 #include <QNetworkAccessManager> #include <QNetworkReply> // ... private slots: void onNetworkReplyFinished(QNetworkReply *reply); private: QNetworkAccessManager *m_networkManager;// 在源文件中实现 #include <QUrl> #include <QJsonDocument> #include <QJsonObject> // 初始化 m_networkManager = new QNetworkAccessManager(this); connect(m_networkManager, &QNetworkAccessManager::finished, this, &MainWindow::onNetworkReplyFinished); // 发起一个GET请求 void MainWindow::fetchData() { QUrl url("https://api.example.com/data"); QNetworkRequest request(url); // 可以设置请求头 request.setHeader(QNetworkRequest::UserAgentHeader, "MyQtApp/1.0"); m_networkManager->get(request); // 异步请求,结果通过信号返回 } // 处理请求完成 void MainWindow::onNetworkReplyFinished(QNetworkReply *reply) { if (reply->error() == QNetworkReply::NoError) { QByteArray data = reply->readAll(); // 处理数据,例如解析JSON QJsonDocument doc = QJsonDocument::fromJson(data); if (!doc.isNull() && doc.isObject()) { QJsonObject obj = doc.object(); QString result = obj["key"].toString(); ui->textEdit->setText(result); } } else { ui->textEdit->setText("Error: " + reply->errorString()); } reply->deleteLater(); // 重要!必须删除reply对象 }7. 样式表(QSS)美化界面
Qt支持使用类似CSS的样式表(QSS)来美化控件外观,这是让应用脱颖而出的关键。
// 为整个应用程序设置样式 qApp->setStyleSheet("QPushButton {" " background-color: #4CAF50;" " border: none;" " color: white;" " padding: 10px 24px;" " border-radius: 5px;" "}" "QPushButton:hover {" " background-color: #45a049;" "}" "QPushButton:pressed {" " background-color: #3d8b40;" "}" "QLineEdit {" " border: 2px solid #ccc;" " border-radius: 4px;" " padding: 5px;" "}" "QLineEdit:focus {" " border-color: #66afe9;" "}");也可以为单个控件设置样式:
ui->loginButton->setStyleSheet("font-weight: bold; font-size: 14pt;");最佳实践:将复杂的QSS样式写在单独的.qss文件中,在程序启动时加载,便于管理和维护。
void loadStyleSheet(const QString &filePath) { QFile file(filePath); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QString styleSheet = QLatin1String(file.readAll()); qApp->setStyleSheet(styleSheet); file.close(); } }8. 项目打包与发布
开发完成后,如何将程序分发给没有Qt环境的用户?这是新手最容易卡住的一步。
8.1 Windows平台(使用windeployqt)
- 以Release模式编译项目:在Qt Creator中,将构建套件切换到
Release,然后构建项目。 - 找到生成的可执行文件(.exe):通常在项目构建目录的
release文件夹下。 - 打开Qt命令行工具:在开始菜单找到
Qt 6.6.0 (MinGW 11.2.0 64-bit)或你对应的版本,打开其下的Qt 6.6.0 (MinGW 11.2.0 64-bit)命令行。 - 使用windeployqt工具:在命令行中,切换到你的
.exe文件所在目录,执行:
这个命令会自动将程序运行所需的Qt DLL、插件、翻译文件等复制到当前目录。windeployqt --compiler-runtime your_app_name.exe - 测试:双击
.exe文件,看是否能独立运行。 - 制作安装包:可以使用
Inno Setup或NSIS等工具,将整个文件夹(包含.exe和所有依赖)打包成安装程序。
8.2 macOS平台(使用macdeployqt)
- 在Qt Creator中以Release模式构建。
- 打开终端,进入项目构建目录。
- 执行:
这会生成一个包含应用程序的macdeployqt YourApp.app -dmg.dmg磁盘映像文件,可以直接分发。
8.3 Linux平台
Linux分发相对复杂,通常需要告知用户安装对应的Qt运行时库,或者使用AppImage、Snap、Flatpak等打包技术。一个相对简单的方法是使用linuxdeployqt工具(第三方),其思路与windeployqt类似。
9. 常见问题与深度排错指南
即使按照教程操作,你也可能会遇到问题。以下是高频问题及解决方案。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 程序编译成功,但运行时崩溃或提示缺少DLL | 1. 运行时环境缺少Qt库。 2. 使用了Debug模式编译,但Release环境运行。 | 1. 检查系统PATH环境变量是否包含Qt的bin目录。 2. 使用 Dependency Walker(Windows)或ldd(Linux)检查exe依赖。 | 1. 使用windeployqt等部署工具打包。2. 确保发布的是Release版本。 |
| Qt Creator调试时输出中文乱码 | 源代码文件编码与控制台/编译器编码不匹配。 | 检查Qt Creator的文本编码设置(工具->选项->文本编辑器->行为)。检查项目文件(.pro)中是否指定了编码。 | 1. 将源代码文件保存为UTF-8 with BOM(Windows下推荐)。 2. 在 .pro文件中添加:MSVCCFLAGS += /source-charset:utf-8 /execution-charset:utf-8(MSVC) 或QMAKE_CXXFLAGS += -finput-charset=UTF-8 -fexec-charset=UTF-8(MinGW)。 |
| 信号与槽连接后不生效 | 1. 类声明中缺少Q_OBJECT宏。2. 信号或槽的参数类型不匹配。 3. 使用了旧式 SIGNAL()/SLOT()语法且函数签名写错。 | 1. 检查类声明。 2. 使用 connect的新语法(函数指针)能在编译期检查。3. 运行程序,查看Qt Creator的“应用程序输出”面板是否有连接错误提示。 | 1. 确保类继承自QObject且包含Q_OBJECT。2. 使用新式连接语法: connect(sender, &Sender::signal, receiver, &Receiver::slot)。3. 检查qmake/make后是否重新构建了moc文件(清理并重新构建项目)。 |
| 界面修改了.ui文件,但运行没变化 | .ui文件对应的ui_xxx.h头文件未更新。 | 检查项目构建目录下ui_xxx.h的修改时间。 | 1. 在Qt Creator中,右键项目 ->执行qmake。2. 清理项目,然后重新构建。 |
| 多线程中更新UI导致程序崩溃 | 在非主线程(非GUI线程)中直接操作UI组件。 | 回顾代码,确认所有直接调用QWidget或其子类方法的代码都在主线程。 | 使用信号与槽进行跨线程通信。Qt的信号槽是线程安全的,可以自动在不同线程间传递消息。例如,在工作线程中发射一个携带数据的信号,在主窗口的槽函数中接收并更新UI。 |
| 发布后程序图标不显示 | 图标资源未正确打包。 | 检查.rc文件(Windows)或.icns文件(macOS)是否被正确包含和引用。 | 1. Windows: 确保.rc文件在.pro中被引用 (RC_FILE = app.rc),并且图标文件路径正确。2. 通用:考虑将图标作为Qt资源文件( .qrc)嵌入。 |
10. 进阶学习与最佳实践
掌握了以上内容,你已经可以应对大多数Qt桌面开发需求。但要写出更健壮、更易维护的代码,还需要关注以下方面:
- 国际化(i18n):使用
tr()函数包裹所有用户可见的字符串,利用Qt Linguist工具进行翻译,让你的应用支持多语言。 - 单元测试:为你的核心业务逻辑编写单元测试。Qt提供了
QTest框架,可以集成到你的开发流程中。 - 插件架构:对于大型应用,考虑使用Qt的插件(Plugin)机制来动态加载功能模块,提高可扩展性。
- 图形视图框架(Graphics View Framework):如果需要开发绘图、图表、CAD类应用,这是比QWidget更强大的选择。
- QML与Qt Quick:对于追求炫酷界面、动画和移动端风格的现代应用,QML(一种声明式语言)是未来的方向。它与C++后端可以完美结合。
- 版本控制与协作:将
.pro、源代码、资源文件纳入Git管理,但忽略构建目录(build-*)和用户配置文件(.user)。 - 性能优化:对于频繁更新的UI,考虑使用
QPainter的直接绘制或模型/视图的批量更新。使用性能分析工具(如QElapsedTimer,Heob)定位瓶颈。
学习Qt是一个循序渐进的过程。从拖拽界面开始,到理解信号槽,再到掌握模型视图、多线程,最后能独立完成一个功能完整、界面美观、稳定可发布的软件。这条路径上的每一个坑,本文都试图为你标出。真正的精通,源于在理解原理的基础上进行大量的实践。建议你从模仿一个小工具开始(如记事本、计算器),逐步增加功能(如文件操作、网络请求),最终形成自己的作品集。这将是你求职时最有力的证明。