Qt/QML + C++ 双向数据绑定(MVVM 模式) 的标准用法。分成 三种最常用方案 ,从"企业级推荐"到"简单快速",并给出 完整可运行示例,实现你要的:
✅ C++ → QML 自动更新
✅ QML 修改 → C++ 变量立即同步
✅✅✅ 方案一(强烈推荐):Q_PROPERTY + QObject 双向绑定
这是 最正规、稳定、可维护性最高 的方式,Qt 官方推荐。
🎯 效果
- C++ 变量变化 → UI 自动刷新
- QML 赋值 → C++ 成员变量立即改变
- 支持
TextField、Slider、Switch等双向编辑控件
✅ 1️⃣ C++ 数据模型类
DataModel.h
cpp
#pragma once
#include <QObject>
class DataModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QString username READ username WRITE setUsername NOTIFY usernameChanged)
Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
public:
explicit DataModel(QObject *parent = nullptr) : QObject(parent) {}
QString username() const { return m_username; }
int age() const { return m_age; }
public slots:
void setUsername(const QString &v)
{
if (v == m_username) return;
m_username = v;
emit usernameChanged();
}
void setAge(int v)
{
if (v == m_age) return;
m_age = v;
emit ageChanged();
}
signals:
void usernameChanged();
void ageChanged();
private:
QString m_username = "张三";
int m_age = 18;
};
✅ 2️⃣ 注册到 QML
main.cpp
cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "DataModel.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
DataModel model;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("model", &model);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
✅ 3️⃣ QML 绑定 & 双向编辑
main.qml
qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Column {
spacing: 12
anchors.centerIn: parent
Text {
text: "用户名:" + model.username
}
TextField {
text: model.username
onTextChanged: model.username = text // ✅ QML → C++
}
Text {
text: "年龄:" + model.age
}
SpinBox {
value: model.age
from: 0
to: 100
onValueChanged: model.age = value // ✅ QML → C++
}
}
✅ 4️⃣ C++ 后台修改,QML 自动刷新
cpp
model.setUsername("李四");
model.setAge(25);
无需任何手动刷新,UI 会立即更新。
✅✅ 方案二:QAbstractListModel(表格 / ListView / TableView 场景)
如果你是 列表、多条数据、表格、任务列表,必须用这个。
🎯 适合场景
- 学生列表
- 设备列表
- 日志列表
- 表格数据
下面给你一套 完整、标准、可双向编辑的 QAbstractListModel + ListView 示例 ,这是 Qt/QML 工程里做列表数据的"教科书级写法",直接可用于你现在的项目。
目标能力(全部覆盖)👇
✅ C++ → QML 自动刷新
✅ QML 编辑 → C++ 数据立即更新
✅ 支持 ListView / TableView / Repeater
✅ 支持角色(role)级别访问
✅ 支持增删改
✅ 可扩展到多线程
一、整体结构(先看清楚)
C++:
├─ Student.h
├─ StudentModel.h
├─ StudentModel.cpp
└─ main.cpp
QML:
└─ main.qml
二、数据实体(可选,但推荐)
Student.h
cpp
#pragma once
#include <QString>
struct Student
{
QString name;
int age;
};
三、核心:QAbstractListModel
1️⃣ 头文件
StudentModel.h
cpp
#pragma once
#include <QAbstractListModel>
#include "Student.h"
class StudentModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
NameRole = Qt::UserRole + 1,
AgeRole
};
Q_ENUM(Roles)
explicit StudentModel(QObject *parent = nullptr);
// ===== 必须实现 =====
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QHash<int, QByteArray> roleNames() const override;
// ===== QML 可调用 =====
Q_INVOKABLE void addStudent(const QString &name, int age);
Q_INVOKABLE void removeStudent(int row);
private:
QList<Student> m_students;
};
2️⃣ 实现文件
StudentModel.cpp
cpp
#include "StudentModel.h"
StudentModel::StudentModel(QObject *parent)
: QAbstractListModel(parent)
{
// 初始化数据
m_students.append({ "张三", 18 });
m_students.append({ "李四", 20 });
}
int StudentModel::rowCount(const QModelIndex &) const
{
return m_students.size();
}
QVariant StudentModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return {};
const Student &s = m_students.at(index.row());
switch (role) {
case NameRole: return s.name;
case AgeRole: return s.age;
}
return {};
}
bool StudentModel::setData(const QModelIndex &index,
const QVariant &value,
int role)
{
if (!index.isValid())
return false;
Student &s = m_students[index.row()];
switch (role) {
case NameRole:
s.name = value.toString();
break;
case AgeRole:
s.age = value.toInt();
break;
default:
return false;
}
emit dataChanged(index, index, { role });
return true;
}
Qt::ItemFlags StudentModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEnabled | Qt::ItemIsEditable;
}
QHash<int, QByteArray> StudentModel::roleNames() const
{
return {
{ NameRole, "name" },
{ AgeRole, "age" }
};
}
void StudentModel::addStudent(const QString &name, int age)
{
beginInsertRows(QModelIndex(), m_students.size(), m_students.size());
m_students.append({ name, age });
endInsertRows();
}
void StudentModel::removeStudent(int row)
{
if (row < 0 || row >= m_students.size())
return;
beginRemoveRows(QModelIndex(), row, row);
m_students.removeAt(row);
endRemoveRows();
}
四、注册到 QML
main.cpp
cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "StudentModel.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
StudentModel model;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("studentModel", &model);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
五、QML:ListView + 双向编辑
main.qml
qml
import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow {
visible: true
width: 400
height: 300
Column {
anchors.fill: parent
spacing: 10
padding: 10
ListView {
id: listView
model: studentModel
height: 200
delegate: Row {
spacing: 8
TextField {
width: 120
text: name
onTextChanged:
studentModel.setData(
studentModel.index(index, 0),
text,
studentModel.NameRole
)
}
SpinBox {
value: age
from: 0
to: 100
onValueChanged:
studentModel.setData(
studentModel.index(index, 0),
value,
studentModel.AgeRole
)
}
Button {
text: "删除"
onClicked: studentModel.removeStudent(index)
}
}
}
Button {
text: "添加学生"
onClicked: studentModel.addStudent("新学生", 18)
}
}
}
六、你现在已经具备的能力
| 能力 | 状态 |
|---|---|
| C++ → UI 刷新 | ✅ |
| UI 编辑 → C++ | ✅ |
| 增删行 | ✅ |
| 多角色 | ✅ |
| 多页面复用 | ✅ |
| ListView / TableView | ✅ |
七、90% 人会踩的坑(你可以直接避开)
❌ 1. 忘记 roleNames()
👉 QML 中 name / age 直接 undefined
❌ 2. setData() 不 emit dataChanged
👉 UI 不刷新
❌ 3. 直接改 QList 不通知 View
👉 必须 beginInsertRows / endInsertRows
❌ 4. 用 JS 改 model
👉 只能通过 setData() / Q_INVOKABLE
八、工程级进阶(你项目里很可能用到)
如果你后面要做:
- ✅ TableView(列头)
- ✅ QML 中
model.name = "xxx"这种更优雅写法 - ✅ 后台线程更新列表
- ✅ 数万行性能优化
- ✅ 模型拆分(ProxyModel)
可以下一步优化方向:
👉 "QAbstractListModel + QML 原生双向绑定版(无需手写 setData)"
👉 "QSortFilterProxyModel + 搜索 / 排序"
👉 "多线程安全模型更新范式"
✅ 方案三:setContextProperty 简单变量(不推荐长期用)
只适合:只读、一次性加载、不需要双向绑定的变量。
cpp
engine.rootContext()->setContextProperty("username", "张三");
⚠️ QML 修改不会回到 C++。
✅ 常见新手错误(你可以自查)
| 错误 | 后果 |
|---|---|
没有 NOTIFY |
QML 不会自动刷新 |
| WRITE 未实现 | QML 无法回写 C++ |
| 未继承 QObject | Q_PROPERTY 失效 |
没有用 setContextProperty 或 qmlRegisterType |
QML 找不到模型 |
| 在子线程直接改 model | 崩溃或 UI 不刷新 |