QT C++桌面应用开发实战:从登录界面到多窗口跳转的完整指南

1. 项目概述与核心价值

最近在带几个刚入行的C++开发新人做项目,发现他们虽然语法过关,但一到图形界面和工程化实战就有点懵。很多人学了C++,却不知道如何用它做出一个“有界面”的、能交互的完整程序。这让我想起自己刚接触QT时的经历,一个简单的登录跳转功能,背后串联的是信号槽、界面布局、资源管理、多窗口切换等一系列核心概念。今天,我就以“实现用户登录页面及多个界面跳转”这个经典且实用的实战项目为例,手把手拆解如何用QT C++搭建一个具备基础业务逻辑的桌面应用骨架。这个项目麻雀虽小,五脏俱全,它不仅是QT入门的最佳练手项目,更是理解桌面应用开发流程、掌握模块化设计思想的绝佳切入点。无论你是想为你的C++算法套上一个可视化外壳,还是打算踏入客户端开发领域,这篇从零到一的实战指南都能让你避开我当年踩过的坑,快速构建出稳定、可维护的图形界面程序。

2. 项目整体设计与架构思路

2.1 为什么选择QT框架?

在C++的图形界面库中,MFC略显陈旧,wxWidgets学习曲线陡峭,而QT凭借其“一次编写,随处编译”的特性、优雅的信号与槽机制、以及丰富的内置组件库,成为了工业级C++桌面开发的事实标准。对于我们要实现的登录和界面跳转功能,QT提供了QWidget作为所有窗口的基类,QMainWindow用于主窗口,QDialog用于对话框(如登录框),这种清晰的类层次结构让界面管理变得直观。更重要的是,其元对象系统(Meta-Object System)支持的信号与槽,是实现界面间解耦通信的关键,比如登录按钮点击(信号)触发验证逻辑(槽),验证成功后再发射另一个信号通知主窗口跳转,整个过程无需复杂的回调函数,代码清晰易维护。

2.2 核心功能模块拆解

我们的目标是一个简单的应用:启动后显示登录窗口,输入正确凭证后,登录窗口关闭,显示主窗口;主窗口上可以通过按钮等控件,跳转到其他功能界面(如设置页、信息展示页)。据此,我们可以拆解出以下几个核心模块:

  1. 登录界面模块(LoginDialog):负责用户身份认证的UI和逻辑。包含账号密码输入框、登录按钮、取消按钮。核心是验证逻辑和跳转触发。
  2. 主界面模块(MainWindow):应用的主舞台,通常包含菜单栏、工具栏、状态栏和中心部件。它是其他功能界面的承载和调度中心。
  3. 子功能界面模块(SubPage1, SubPage2...):具体的业务功能界面,例如“用户管理”、“数据报表”等。它们通常以独立窗口或嵌入主窗口中心区域的形式存在。
  4. 界面跳转管理器:这不是一个具体的类,而是一种设计思路。负责协调各个界面之间的创建、显示、隐藏和销毁逻辑,以及界面间的数据传递。

2.3 工程结构与类设计

一个清晰的工程结构是项目可维护性的基础。建议采用如下目录结构:

MyApp/ ├── MyApp.pro # QT项目文件 ├── src/ # 源代码目录 │ ├── main.cpp # 程序入口 │ ├── logindialog.h/cpp # 登录界面类 │ ├── mainwindow.h/cpp # 主界面类 │ └── subpage1.h/cpp # 子界面1类 ├── ui/ # 界面文件目录(如果使用设计师) │ ├── logindialog.ui │ ├── mainwindow.ui │ └── subpage1.ui └── resources/ # 资源文件目录(如图标、图片) └── images/

在类设计上,遵循QT的惯例:

  • LoginDialog继承自QDialog,采用模态方式弹出,阻塞直到登录完成。
  • MainWindow继承自QMainWindow,作为应用的主容器。
  • SubPage1等子界面可以继承自QWidgetQDialog,取决于其使用方式(是嵌入还是弹出)。

注意:很多新手喜欢把所有界面代码都写在mainwindow里,导致这个类迅速膨胀,难以维护。务必坚持“单一职责原则”,每个界面类只管理自己的UI和逻辑。

3. 登录界面的详细实现与安全考量

3.1 UI设计与布局管理

登录界面虽然简单,但良好的布局能提升用户体验。我们可以使用QT设计师(Qt Designer)拖拽完成,也可以纯代码编写。这里以代码为例,展示如何构建一个标准的登录对话框。

首先,在logindialog.h中定义类:

#ifndef LOGINDIALOG_H #define LOGINDIALOG_H #include <QDialog> #include <QLineEdit> #include <QPushButton> #include <QLabel> class LoginDialog : public QDialog { Q_OBJECT // 必须的宏,用于启用信号与槽 public: explicit LoginDialog(QWidget *parent = nullptr); signals: void loginSuccess(const QString &username); // 登录成功的信号 private slots: void onLoginClicked(); // 处理登录按钮点击 void onCancelClicked(); // 处理取消按钮点击 private: void setupUI(); // 初始化界面 bool validateUser(const QString &user, const QString &pass); // 验证逻辑 QLineEdit *m_userEdit; QLineEdit *m_passEdit; QPushButton *m_loginBtn; QPushButton *m_cancelBtn; QLabel *m_statusLabel; }; #endif // LOGINDIALOG_H

logindialog.cpp中实现界面布局和逻辑:

#include “logindialog.h” #include <QFormLayout> #include <QVBoxLayout> #include <QHBoxLayout> #include <QMessageBox> #include <QDebug> LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(tr(“用户登录”)); setFixedSize(300, 180); // 固定大小,防止拉伸变形 setupUI(); // 连接信号与槽 connect(m_loginBtn, &QPushButton::clicked, this, &LoginDialog::onLoginClicked); connect(m_cancelBtn, &QPushButton::clicked, this, &LoginDialog::onCancelClicked); // 回车键触发登录 connect(m_passEdit, &QLineEdit::returnPressed, m_loginBtn, &QPushButton::click); } void LoginDialog::setupUI() { m_userEdit = new QLineEdit(this); m_passEdit = new QLineEdit(this); m_passEdit->setEchoMode(QLineEdit::Password); // 密码模式 m_loginBtn = new QPushButton(tr(“登录”), this); m_cancelBtn = new QPushButton(tr(“取消”), this); m_statusLabel = new QLabel(this); m_statusLabel->setStyleSheet(“color: gray;”); // 使用布局管理器自动排列 QFormLayout *formLayout = new QFormLayout; formLayout->addRow(tr(“用户名:”), m_userEdit); formLayout->addRow(tr(“密 码:”), m_passEdit); QHBoxLayout *btnLayout = new QHBoxLayout; btnLayout->addStretch(); // 添加弹性空间 btnLayout->addWidget(m_loginBtn); btnLayout->addWidget(m_cancelBtn); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->addLayout(formLayout); mainLayout->addWidget(m_statusLabel); mainLayout->addLayout(btnLayout); setLayout(mainLayout); }

使用布局管理器(QVBoxLayout,QHBoxLayout,QFormLayout)而非绝对坐标,能让界面在不同系统和分辨率下自适应,这是专业GUI开发的基本素养。

3.2 身份验证逻辑与数据安全

onLoginClicked槽函数中,我们需要处理验证。切记,绝对不要在客户端存储明文密码或进行硬编码验证。对于实战演示,我们可以模拟一个简单的验证,但在真实项目中,这里应该向服务器发起安全请求。

void LoginDialog::onLoginClicked() { QString username = m_userEdit->text().trimmed(); QString password = m_passEdit->text(); if(username.isEmpty() || password.isEmpty()) { m_statusLabel->setText(tr(“用户名和密码不能为空!”)); m_statusLabel->setStyleSheet(“color: red;”); return; } // 模拟验证逻辑 - 实际项目中替换为网络请求 if(validateUser(username, password)) { m_statusLabel->setText(tr(“登录成功,正在跳转...”)); m_statusLabel->setStyleSheet(“color: green;”); Q_EMIT loginSuccess(username); // 发射成功信号 accept(); // 关闭对话框并返回 QDialog::Accepted } else { m_statusLabel->setText(tr(“用户名或密码错误!”)); m_statusLabel->setStyleSheet(“color: red;”); m_passEdit->clear(); // 清空密码框 m_passEdit->setFocus(); // 焦点回到密码框 } } bool LoginDialog::validateUser(const QString &user, const QString &pass) { // !!! 警告:此处仅为演示,绝对不可用于生产环境 !!! // 生产环境应使用HTTPS将密码哈希值(如加盐的bcrypt)发送到服务器验证 QMap<QString, QString> userMap; // 模拟用户数据库 userMap[“admin”] = “123456”; // 明文密码,极不安全! userMap[“user1”] = “password”; return userMap.contains(user) && userMap[user] == pass; } void LoginDialog::onCancelClicked() { reject(); // 关闭对话框并返回 QDialog::Rejected }

关键安全提示:上述代码中的validateUser函数是极不安全的示例。在实际开发中,必须遵循:

  1. 传输安全:使用HTTPS协议。
  2. 密码存储:服务器端不应存储明文密码,应存储经过强哈希算法(如Argon2、bcrypt)加盐处理后的哈希值。
  3. 客户端处理:客户端也不应处理核心验证逻辑,仅负责收集信息并安全传输。可以考虑对密码进行客户端哈希,但要注意防止重放攻击,通常这需要与服务器协商一套完整的认证协议(如SCRAM)。

3.3 用户体验细节打磨

  1. 输入框焦点:在构造函数或showEvent中,调用m_userEdit->setFocus()让光标初始停留在用户名框。
  2. 密码框显示:可以提供一个小眼睛按钮,通过切换QLineEdit::EchoModePasswordNormal之间切换,但要注意其摆放位置不影响主要布局。
  3. 状态反馈:使用m_statusLabel实时反馈状态(如“正在验证...”),比用阻塞式的QMessageBox体验更流畅。
  4. 回车键支持:已通过connect(m_passEdit, &QLineEdit::returnPressed, m_loginBtn, &QPushButton::click)实现,符合用户习惯。

4. 主窗口搭建与界面跳转的核心机制

4.1 主窗口框架构建

主窗口MainWindow通常更复杂。我们使用QMainWindow,并为其添加菜单、工具栏、状态栏和一个中心部件。中心部件将成为我们切换不同子页面的“容器”。

mainwindow.h中:

#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QStackedWidget> // 用于界面堆叠切换 QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class SubPage1; // 前向声明 class SubPage2; class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(const QString &username, QWidget *parent = nullptr); ~MainWindow(); private slots: void switchToPage1(); void switchToPage2(); void onAbout(); private: void setupUI(); void createMenus(); void createToolBars(); void createStatusBar(const QString &username); Ui::MainWindow *ui; QStackedWidget *m_stackedWidget; // 堆叠窗口部件,核心! SubPage1 *m_page1; SubPage2 *m_page2; }; #endif // MAINWINDOW_H

这里引入了一个关键组件:QStackedWidget。它可以容纳多个子部件(即我们的各个功能界面),但一次只显示其中一个,非常适合于标签页或导航式的内容切换,是实现无闪烁界面跳转的理想选择。

4.2 使用QStackedWidget管理多界面

mainwindow.cpp的构造函数和setupUI函数中初始化:

#include “mainwindow.h” #include “ui_mainwindow.h” // 由.ui文件生成 #include “subpage1.h” #include “subpage2.h” #include <QMenuBar> #include <QToolBar> #include <QStatusBar> #include <QAction> #include <QLabel> MainWindow::MainWindow(const QString &username, QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // 初始化设计师生成的UI setWindowTitle(tr(“我的QT应用 - 主窗口”)); // 初始化堆叠窗口和子页面 m_stackedWidget = new QStackedWidget(this); m_page1 = new SubPage1(this); m_page2 = new SubPage2(this); m_stackedWidget->addWidget(m_page1); // 索引0 m_stackedWidget->addWidget(m_page2); // 索引1 // 将堆叠窗口设置为中心部件 setCentralWidget(m_stackedWidget); // 默认显示第一页 m_stackedWidget->setCurrentIndex(0); createMenus(); createToolBars(); createStatusBar(username); }

createMenuscreateToolBars函数用于创建菜单和工具栏,并关联切换页面的槽函数:

void MainWindow::createMenus() { QMenu *viewMenu = menuBar()->addMenu(tr(“视图(&V)”)); QAction *page1Action = new QAction(tr(“页面一(&1)”), this); QAction *page2Action = new QAction(tr(“页面二(&2)”), this); connect(page1Action, &QAction::triggered, this, &MainWindow::switchToPage1); connect(page2Action, &QAction::triggered, this, &MainWindow::switchToPage2); viewMenu->addAction(page1Action); viewMenu->addAction(page2Action); viewMenu->addSeparator(); viewMenu->addAction(tr(“关于(&A)”), this, &MainWindow::onAbout); } void MainWindow::switchToPage1() { m_stackedWidget->setCurrentWidget(m_page1); // 切换到页面一 statusBar()->showMessage(tr(“已切换到页面一”), 2000); } void MainWindow::switchToPage2() { m_stackedWidget->setCurrentWidget(m_page2); // 切换到页面二 statusBar()->showMessage(tr(“已切换到页面二”), 2000); }

通过QStackedWidget::setCurrentWidgetsetCurrentIndex,可以平滑地在不同界面间切换,所有子界面在程序生命周期内只创建一次,避免了反复new/delete带来的性能开销和潜在的内存问题。

4.3 子功能界面的设计与集成

子页面SubPage1SubPage2是普通的QWidget子类。以SubPage1为例:

// subpage1.h #include <QWidget> #include <QLabel> #include <QPushButton> class SubPage1 : public QWidget { Q_OBJECT public: explicit SubPage1(QWidget *parent = nullptr); };
// subpage1.cpp #include “subpage1.h” #include <QVBoxLayout> SubPage1::SubPage1(QWidget *parent) : QWidget(parent) { QLabel *label = new QLabel(tr(“这是功能页面一”), this); label->setAlignment(Qt::AlignCenter); QFont font = label->font(); font.setPointSize(16); label->setFont(font); QPushButton *testBtn = new QPushButton(tr(“测试按钮”), this); // 可以在此连接按钮的信号到本页面的业务逻辑 QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(label); layout->addWidget(testBtn); layout->addStretch(); setLayout(layout); }

这样,每个子页面独立管理自己的UI和内部逻辑,通过主窗口的QStackedWidget进行调度,实现了高内聚、低耦合的架构。

5. 程序入口与界面生命周期管理

5.1 应用启动流程与登录判断

程序的入口点main.cpp负责初始化应用,并决定首先显示哪个窗口。这是串联整个流程的“导演”。

#include <QApplication> #include “logindialog.h” #include “mainwindow.h” #include <QMessageBox> int main(int argc, char *argv[]) { QApplication a(argc, argv); // 设置应用基本信息(可选) QApplication::setApplicationName(“MyQTApp”); QApplication::setOrganizationName(“MyCompany”); // 创建并显示登录对话框 LoginDialog loginDlg; // 以模态方式执行,阻塞直到对话框关闭 if (loginDlg.exec() == QDialog::Accepted) { // 登录成功,获取用户名(通过信号传递,这里需要修改,见下文) // 为了演示,我们先假设一个用户名。实际中,LoginDialog应提供获取用户名的方法。 QString username = “AdminUser”; // 创建并显示主窗口 MainWindow w(username); w.show(); // 进入主事件循环 return a.exec(); } else { // 用户取消登录,程序退出 return 0; } }

但这里有个问题:main函数如何获取登录成功的用户名?我们需要修改LoginDialog,使其在登录成功后能向外传递信息。通常有两种方式:

  1. 公共方法:在LoginDialog中增加一个getLoggedInUser()的公共函数,在exec()返回Accepted后调用。
  2. 信号传递:如我们之前定义的loginSuccess信号。但信号需要接收对象,在main函数中直接连接不太方便。我们可以结合QDialog::exec()的返回值和一个公共方法来处理。

让我们优化一下LoginDialog

// 在logindialog.h的类定义中增加 public: QString getLoggedInUsername() const { return m_loggedInUser; } private: QString m_loggedInUser; // 新增私有成员变量
// 在logindialog.cpp的onLoginClicked成功分支中 if(validateUser(username, password)) { m_loggedInUser = username; // 保存用户名 // ... 其他代码 ... accept(); }

然后修改main.cpp

LoginDialog loginDlg; if (loginDlg.exec() == QDialog::Accepted) { QString username = loginDlg.getLoggedInUsername(); // 获取用户名 MainWindow w(username); w.show(); return a.exec(); }

5.2 界面间的数据传递与通信

除了从登录窗口向主窗口传递用户名这种“一次性”数据,界面间更常见的是动态的数据通信。例如,子页面SubPage1的操作需要更新SubPage2的显示。这需要用到QT的核心机制:信号与槽。

场景:假设SubPage1有一个按钮,点击后需要通知MainWindow切换页面并同时告诉SubPage2一条消息。

  1. 在SubPage1中定义信号
    // subpage1.h signals: void requestSwitchToPage2WithMsg(const QString &msg);
  2. 在SubPage1的按钮点击槽中发射信号
    // 在subpage1.cpp的构造函数中连接 connect(testBtn, &QPushButton::clicked, this, [this](){ Q_EMIT requestSwitchToPage2WithMsg(“来自页面一的消息!”); });
  3. 在MainWindow中连接信号
    // 在MainWindow构造函数中,创建m_page1后 connect(m_page1, &SubPage1::requestSwitchToPage2WithMsg, this, [this](const QString &msg){ // 先切换页面 switchToPage2(); // 再调用SubPage2的方法设置消息 m_page2->setDisplayMessage(msg); });
  4. 在SubPage2中提供设置消息的公共槽函数
    // subpage2.h public slots: void setDisplayMessage(const QString &msg);

这种方式实现了界面间的完全解耦:SubPage1只知道“发出一个切换请求并附带消息”,它不关心谁接收、如何处理;MainWindow作为协调者,负责路由这个请求;SubPage2提供一个设置消息的接口。任何一方修改内部实现,只要接口不变,就不会影响其他方。

6. 项目构建、部署与常见问题排查

6.1 项目文件(.pro)配置详解

QT使用.pro文件来管理项目构建。一个典型的项目文件如下:

# MyApp.pro QT += core gui # 添加核心和GUI模块 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets # QT5以上需要widgets模块 CONFIG += c++11 # 你可以在此关闭某些特性,如需关闭,取消注释下一行 # DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # 禁用所有在QT6.0.0之前废弃的API SOURCES += \ src/main.cpp \ src/logindialog.cpp \ src/mainwindow.cpp \ src/subpage1.cpp \ src/subpage2.cpp HEADERS += \ src/logindialog.h \ src/mainwindow.h \ src/subpage1.h \ src/subpage2.h FORMS += \ ui/logindialog.ui \ ui/mainwindow.ui \ ui/subpage1.ui # 指定资源文件 RESOURCES += resources/images.qrc # 设置目标文件名和类型 TARGET = MyApp TEMPLATE = app # 发布版本优化 CONFIG(release, debug|release): { DEFINES += QT_NO_DEBUG_OUTPUT # 发布版关闭调试输出 } # 设置输出目录(可选) DESTDIR = $$PWD/bin OBJECTS_DIR = $$PWD/build/.obj MOC_DIR = $$PWD/build/.moc RCC_DIR = $$PWD/build/.rcc UI_DIR = $$PWD/build/.ui

注意TEMPLATE = app表示生成可执行程序。如果项目很大,可以考虑将一些模块编译成库(TEMPLATE = lib)。

6.2 编译、运行与发布

  1. 生成构建系统:在项目根目录执行qmake MyApp.pro(如果使用Qt Creator,它会自动处理)。这会根据.pro文件生成Makefile(Linux/macOS)或.vcxproj等(Windows)。
  2. 编译:执行make(或nmakejom,或在IDE中点击构建)。
  3. 运行:在构建输出目录(如./bin)找到可执行文件MyApp(或MyApp.exe)并运行。
  4. 发布:QT程序发布需要带上相关的动态链接库(DLL)。在Windows上,最方便的方法是使用QT自带的windeployqt工具。在构建目录下执行:
    windeployqt MyApp.exe
    该工具会自动扫描可执行文件依赖的QT库,并复制到当前目录。你只需要将这个文件夹打包分发给用户即可。Linux下依赖管理通常通过包管理器,macOS下可以制作.appbundle。

6.3 常见问题与调试技巧实录

问题1:编译时报错“undefined reference tovtable for ClassName

  • 原因:这是QT元对象系统(MOC)的经典问题。通常是因为在头文件中声明了Q_OBJECT宏,或者声明了信号/槽,但没有在编译前运行moc(元对象编译器)预处理对应的头文件。
  • 解决
    • 确保.pro文件中正确列出了所有包含Q_OBJECT.h文件。
    • 执行qmake后,moc工具会自动为这些头文件生成moc_*.cpp文件并参与编译。
    • 最彻底的解决方法是:在Qt Creator中,清理项目,然后执行“构建 -> 运行qmake”,最后重新构建。

问题2:界面切换时,子页面布局错乱或显示空白

  • 原因QStackedWidgetaddWidget时,如果子页面自身没有设置有效的布局管理器(Layout),或者其大小策略(SizePolicy)不合适,就可能显示异常。
  • 解决
    • 确保每个添加到QStackedWidget的子页面QWidget,都在其构造函数中设置了正确的布局(例如setLayout(new QVBoxLayout))。
    • 检查子页面中的控件是否被正确添加到布局中。
    • 可以在MainWindow切换页面后,尝试调用m_stackedWidget->currentWidget()->adjustSize()来强制调整大小。

问题3:点击按钮后,程序无响应或崩溃

  • 原因:大概率是信号与槽连接错误,或者槽函数中访问了非法内存(如空指针)。
  • 调试
    • main.cppQApplication a(argc, argv);之前,加入qputenv(“QT_ASSUME_STDERR_HAS_CONSOLE”, “1”);(Windows下)确保能看到qDebug()输出。
    • 在怀疑的代码段前后加入qDebug() << “Debug point 1”;来跟踪执行流。
    • 检查所有connect语句:
      • 发送者、信号、接收者、槽函数的参数类型必须匹配。
      • 使用新的Qt5语法connect(sender, &Sender::signal, receiver, &Receiver::slot),它在编译时就能检查类型,更安全。
      • 如果槽函数是lambda表达式,注意其捕获的变量生命周期,避免悬垂引用。
    • 在可能出错的指针访问前进行判空:if(m_ptr) { m_ptr->doSomething(); }

问题4:发布后的程序在其他电脑上无法运行,提示缺少*.dll

  • 原因:没有将程序依赖的所有QT运行时库和必要的系统库(如msvcp140.dll,vcruntime140.dll)一并打包。
  • 解决
    • 使用windeployqt工具是第一步,但它可能不会捕获非QT的依赖(如VC++运行时)。
    • 对于VC++运行时,可以让用户自行安装对应的“Visual C++ Redistributable”,或者将msvcp140.dll等文件(需确认许可证允许)复制到程序目录。
    • 使用Dependency WalkerProcess Explorer工具检查运行时的确切依赖。
    • 在开发机上,将程序复制到一个全新的、没有开发环境的虚拟机中测试,是检验发布包是否完整的最佳方法。

问题5:中文显示乱码

  • 原因:源代码文件编码、QT内部字符串编码、系统本地编码不一致。
  • 解决
    • 统一使用UTF-8编码。确保所有源代码文件(.h, .cpp, .ui)以UTF-8 with BOM(Windows下)或UTF-8(Linux/macOS下)保存。
    • main函数开头,设置正确的编码转换器(QT5以上推荐方式):
      #include <QTextCodec> int main(...) { QApplication a(...); // QT5+ 推荐方式:使用QStringLiteral或tr(),并确保源文件UTF-8编码 // 如果仍有问题,可以尝试(但不推荐长期使用): // QTextCodec::setCodecForLocale(QTextCodec::codecForName(“UTF-8”)); // ... }
    • 在UI文件中,所有需要显示文本的控件,在其属性编辑器里直接输入中文即可,QT Designer会处理好编码。

通过这个完整的实战项目,我们不仅实现了一个具有登录和界面跳转功能的QT应用,更深入理解了QT的核心机制:元对象系统、信号与槽、布局管理、以及模块化设计思想。记住,好的架构是迭代出来的,先从这样的小项目开始,逐步增加功能(如网络请求、数据库操作、多语言支持),你的QT开发技能树就会越来越丰满。