文章

从 Widgets 迁移到 QML:一个真实页面的重构对比

通过一个设置页的真实重构案例,对比 Qt Widgets 与 QML 在结构、性能、协作与维护成本上的差异,并给出可执行迁移路径。

从 Widgets 迁移到 QML:一个真实页面的重构对比

在很多 Qt 老项目里,UI 主要由 Widgets 构建。随着产品迭代,大家常会遇到这些信号:

  • 新需求强调动画与交互反馈;
  • UI 改版频繁,前端同学也想参与;
  • 复杂页面改一个布局要动很多 C++ 代码。

这时,“要不要迁移到 QML”就成了一个真实工程问题。

本文不谈空泛概念,而是基于一个设置中心页面(Settings Page),展示从 Widgets 到 QML 的重构过程、收益与坑点。

一、改造背景:原 Widgets 页面长什么样

原页面功能:

  • 左侧导航(通用 / 网络 / 外观 / 高级);
  • 右侧配置表单;
  • 底部“应用、取消、恢复默认”。

原实现特点(Widgets)

  • QStackedWidget + QListWidget 实现导航切换;
  • 各分组通过 QGroupBox + QFormLayout 拼装;
  • 逻辑与界面交织在 SettingsPage.cpp

遇到的问题

  1. 样式维护成本高:大量 QSS,跨平台细节容易不一致。
  2. 交互动效弱:切换和状态反馈比较“硬”。
  3. 职责边界不清:UI 控件读写配置、校验、业务分支都在一个类里。

二、迁移目标:不是“全重写”,而是“逐步替换”

我们设定了三个目标:

  1. UI 声明式化:布局与样式放在 QML;
  2. 业务 C++ 保留:配置读写、校验、持久化继续用 C++;
  3. 可灰度迁移:支持单页替换,不影响其他 Widgets 页面。

三、重构方案:桥接层 + 页面拆分

3.1 架构分层

  • Domain 层(C++)SettingsService 负责配置读写;
  • ViewModel 层(C++ QObject)SettingsViewModel 暴露 Q_PROPERTY 和命令槽;
  • View 层(QML)SettingsPage.qml 负责布局、绑定、动画。

3.2 关键桥接代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SettingsViewModel : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString apiEndpoint READ apiEndpoint WRITE setApiEndpoint NOTIFY apiEndpointChanged)
    Q_PROPERTY(bool autoUpdate READ autoUpdate WRITE setAutoUpdate NOTIFY autoUpdateChanged)
public:
    explicit SettingsViewModel(SettingsService *service, QObject *parent = nullptr)
        : QObject(parent), m_service(service) {
        load();
    }

    Q_INVOKABLE void apply() {
        m_service->setApiEndpoint(m_apiEndpoint);
        m_service->setAutoUpdate(m_autoUpdate);
        m_service->save();
        emit saved();
    }

signals:
    void apiEndpointChanged();
    void autoUpdateChanged();
    void saved();
};
1
2
3
4
5
6
// main.cpp or page bootstrap
QQmlApplicationEngine engine;
SettingsService service;
SettingsViewModel vm(&service);
engine.rootContext()->setContextProperty("settingsVM", &vm);
engine.loadFromModule("App", "SettingsPage");

3.3 QML 页面示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Page {
    id: root

    Column {
        spacing: 12

        TextField {
            text: settingsVM.apiEndpoint
            placeholderText: "API Endpoint"
            onTextChanged: settingsVM.apiEndpoint = text
        }

        Switch {
            checked: settingsVM.autoUpdate
            text: "自动更新"
            onToggled: settingsVM.autoUpdate = checked
        }

        Button {
            text: "应用"
            onClicked: settingsVM.apply()
        }
    }

    Connections {
        target: settingsVM
        function onSaved() {
            toast.show("设置已保存")
        }
    }
}

四、对比结果:Widgets vs QML(真实收益)

4.1 代码结构对比

  • 重构前:一个 SettingsPage.cpp/.h 超过 1200 行;
  • 重构后:
    • SettingsPage.qml(视图)约 280 行;
    • SettingsViewModel(状态与命令)约 220 行;
    • SettingsService(业务)约 300 行。

结果:模块边界清晰,改动更聚焦。

4.2 交互体验对比

  • 页面切换增加过渡动画,用户感知更自然;
  • 输入校验提示可直接绑定状态,减少手写 UI 同步代码;
  • 深色主题切换更统一(使用 QML 主题变量)。

4.3 团队协作对比

  • Widgets 阶段:UI 迭代高度依赖 C++ 客户端工程师;
  • QML 阶段:UI 逻辑可以由前端/设计工程师协作完成,C++ 侧主要关注能力暴露。

五、迁移中的坑与解法

1)把业务逻辑写进 QML JavaScript

短期快,长期维护会失控。建议:QML 只做展示与轻交互,业务收敛到 C++ ViewModel/Service。

2)上下文属性滥用

setContextProperty 太多会让依赖关系不透明。建议逐步过渡到 qmlRegisterType 或模块化注册。

3)性能误判

QML 并不“天然更慢”或“天然更快”。关键在于:

  • 减少不必要的绑定连锁;
  • 避免大列表中复杂 delegate;
  • 使用 QML Profiler 定位热点。

4)一次性全量迁移风险高

建议采用“页面级增量替换”:

  1. 新需求页面优先 QML;
  2. 老页面按迭代窗口逐步迁;
  3. 保留 Widgets 容器期,确保可回滚。

六、一套可执行迁移清单

  1. 选一页高频改动页面做试点;
  2. 明确 ViewModel 边界与属性命名;
  3. 建立 QML 组件规范(按钮、输入框、卡片等);
  4. 接入 QML Profiler 与基础 UI 自动化;
  5. 形成“Widgets 与 QML 共存”工程模板。

七、什么时候不该迁移?

以下场景可暂缓:

  • 项目已进入维护尾期;
  • 页面稳定、几乎无交互改版需求;
  • 团队缺乏 QML 经验,且短期无法投入学习成本。

技术迁移不是“追新”,而是成本收益决策。

八、结语

从 Widgets 到 QML,不是“推翻重来”,而是重构 UI 表达方式

只要你坚持三条原则:

  • 业务留在 C++;
  • UI 放在 QML;
  • 迁移按页面增量进行;

就能在可控风险下获得更现代的交互与更高的迭代效率。

本文由作者按照 CC BY 4.0 进行授权