1.引子
众多语言框架中均有MVC架构,对数据与UI的双向绑定有成熟的解决方案。
- 以Android为例,谷歌最新推出的Jetpack Compose UI框架,其中声明式UI与其附带的数据双向绑定相当突出。实现声明式UI ≈ 实现一个 编译器,难度可想而知,但是利用Qt实现 UI与数据双向绑定是可以期待的。
- 在Web前端开发中会应用到MVVM模式,其目的就是通过ViewModel层,将Model层和View层进行双向绑定。使得Model层的数据发生改变,View层也同样改变;用户如果通过View层进行修改,也会反馈到Model层的数据。
对于Qt这一具体框架而言,略显特殊。以下汇总了搜集的数据绑定方法,供Qter们斟酌。
2.发布者-订阅者模式
3.脏值检查
4.数据劫持
5.数据、界面封装+重载赋值运算符实现数据感知+信号槽通知
目标:
实现一个UI组件,支持数据双向绑定。我们更新绑定变量的时候,绑定这个变量的UI组件,数据也会同步更新。同理,我们直接修改UI组件的属性,例如滑动条的位置亦会同步修改变量值。
在 Qt - 一文理解信号槽机制(万字剖析整理) 中讲到 Qt的信号槽机制就有用到 发布-订阅模型。那么我们将通过信号槽实现。
随着而来的是,信号槽机制依赖于 QObject,元对象系统。而我们绑定的变量可能是 int、float之类。为了能使用信号槽的通讯机制,我们必须将基本数据类型再封装一次。然后在变量类改变时通知控件类,在控件类改变时通知变量类。
在Qt中,控件类的改变本身就有相应的 changed() 信号,所以我们只需要接起来即可。
接下来的问题是如何知晓变量类改变,答案是:重载赋值运算符。
源码
/*! hslider.h */
#ifndef HSLIDER_H
#define HSLIDER_H
#include <QWidget>
#include <QSlider>
#include <QDebug>
class HInt : public QObject
{
Q_OBJECT
public:
explicit HInt(int x = 0){
number = x;}
void operator=(const HInt &newValue )
{
Q_EMIT(change(newValue.number)); number = newValue.number;}
signals:
void change(int newValue);
public slots:
void update(int newValue){
number = newValue; qDebug() << "HInt::update: " << newValue;}
private:
int number;
};
class HSlider : public QWidget
{
Q_OBJECT
public:
explicit HSlider(QWidget *parent = nullptr);
void Buid(HInt *newValue);
signals:
void change(int newValue);
public slots:
void update(int newValue);
private:
QSlider* mSlider;
HInt *mValue;
};
#endif // HSLIDER_H
/*! hslider.cpp */
#include "hslider.h"
HSlider::HSlider(QWidget *parent) : QWidget(parent)
{
mSlider = new QSlider(this);
connect(mSlider,&QSlider::valueChanged,this,&HSlider::change);
}
void HSlider::Buid(HInt *newValue){
if(newValue){
if(!mValue){
disconnect(mValue,&HInt::change,this,&HSlider::update);
disconnect(this,&HSlider::change,mValue,&HInt::update);
}
mValue = newValue;
connect(mValue,&HInt::change,this,&HSlider::update);
connect(this,&HSlider::change,mValue,&HInt::update);
}
}
void HSlider::update(int newValue){
mSlider->setValue(newValue);
qDebug() << "HSlider::update" << newValue;
}
效果
本方法对于数据的封装及其数据变化感知的方式比较原始,借鉴数据劫持、脏值检查希望能找出灵感。
6.QStandardItemModel+QDataWidgetMapper+QStyledItemDelegate
相比于MVC、MVVM、MVP,Qt所提供的是View-Model-Delegate模式,模型类例如QStandardItemModel,但是Qt普通的组件没有拆分出View,因此还得自己去写对应的View,幸而Qt提供了QDataWidgetMapper 组件,支持实现绑定。
QStandardItemModel *model=new QStandardItemModel(1,1);
QDataWidgetMapper *mapper=new QDataWidgetMapper(this);
mapper->setModel(model);
auto *editA=new QLineEdit(this);
auto *editB=new QLineEdit(this);
mapper->addMapping(editA,0);
mapper->addMapping(editB,0);
mapper->toFirst();
ui->gridLayout->addWidget(editA);
ui->gridLayout->addWidget(editB);
上面的代码中,如果改变了editA中文字,发射editingFinished 信号之后,model改变,editB就会改变自己的文字。其实也可以改变mapper的SubmitPolicy,手动调用submit实现更新。
查看源代码,其原理是将QItemDelegate 中的eventFilter 安装到关联的组件中。因此我们可以继承QAbstractItemDelegate 来自定义,然后调用mapper 的setItemDelegate() ,这样就能引用上自定义的Widget,或者自定义显示的规则了。
例如我们如果将editB 改成一个按钮pushButtonB ,那么改变editA 的内容并不能如愿以偿地让按钮的文字发生变化,这不是默认行为。不过我们只要自定义委托即可:
MyItemDelegate.h
#pragma once
#include <QStyledItemDelegate>
class MyItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
MyItemDelegate(QObject *parent);
~MyItemDelegate();
virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override;
virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
};
MyDelegate.cpp
#include "MyItemDelegate.h"
#include <QLineEdit>
#include <QPushButton>
MyItemDelegate::MyItemDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
}
MyItemDelegate::~MyItemDelegate()
{
}
void MyItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
auto text = index.data().toString();
if (qobject_cast<QPushButton*>(editor))
{
qobject_cast<QPushButton*>(editor)->setText(text);
}
else
{
qobject_cast<QLineEdit*>(editor)->setText(text);
}
}
void MyItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
if (qobject_cast<QLineEdit*>(editor))
{
model->setData(index, qobject_cast<QLineEdit*>(editor)->text(), Qt::EditRole);
}
}
调用
QStandardItemModel *model = new QStandardItemModel(1, 1);
QDataWidgetMapper *mapper = new QDataWidgetMapper(this);
mapper->setModel(model);
auto *editA = new QLineEdit(this);
auto *pushButtonB = new QPushButton(this);
mapper->addMapping(editA, 0);
mapper->addMapping(pushButtonB, 0);
mapper->toFirst();
mapper->setItemDelegate(new MyItemDelegate(this));
ui.centralWidget->layout()->addWidget(editA);
ui.centralWidget->layout()->addWidget(pushButtonB);
此时文字就能在按钮上显示了。通过这种方式我们能很方便地进行更复杂的设计,例如通过调色板和输入数字同时控制颜色等等。但这种方式较少被书籍和教程重点提到,估计与Qt开发者数量与封闭开发的现实有关系。但确实不失为Qt官方提供的一种UI与数据双向绑定的较为理想的成体系的解决方案,值得推荐。
7.参考资料
【1】Vue.js数据双向绑定实现
【2】Qt - UI数据双向绑定简易实现
【3】初学Qt的小坑记录(6)——数据与组件的绑定