4.2 Qt 中的界面数据绑定

        本文是《用 Qt 实现电子白板》的其中一节,建议全章阅读。


        在电子白板中,一个页面(ResourcePage)包含多个资源(比如图片、文字、视频等),资源有先后顺序,决定其对于控件的展示层级关系。

        我们希望对资源的管理是数据驱动的,业务层只能操作数据,也就是只能调用 ResourcePage 的方法,至于控件的创建、销毁、层级改变都是内部自动管理的。

        在 Qt 中,实现数据驱动界面,有一个工具,即 QAbstractItemModel 。虽然界面管理也是要自己实现,完全可以自定义数据驱动模型,但是使用 QAbstractItemModel 可以方便地与 Qt 其他组件对接,所以我们还是决定数据、界面都对接到这个工具上。

数据模型

        QAbstractItemModel 是一个相当复杂的类,它里面既支持普通的一维(List)数据,也支持描述二维(Grid)数据,还能描述树形(Tree)数据。在最复杂的场景下,它是一个以 Tree 为基础,每个 Tree 节点又是一个 Row 的数据模型。

        在我们的场景中 (ResourcePage),只需要作为最简单的 List 使用,所以 Tree 只有一个根节点,根节点的 Grid 只有一列数据。

class ResourcePage : public QAbstractItemModel
{
    Q_OBJECT
private:
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &child) const override;
};

        实现 QAbstractItemModel,最主要的是需要实现下面这些方法:

  • rowCount,columnCount 方法

        这个比较简单,rowCount 返回资源的个数,columnCount 返回固定值 1,参数 parent 表示所属的树节点,显然在这里没有作用。

int ResourcePage::rowCount(const QModelIndex &parent) const
{
    (void) parent;
    return resources_.size();
}

int ResourcePage::columnCount(const QModelIndex &parent) const
{
    (void) parent;
    return 1;
}
  • parent 方法

        方法 parent 返回一个节点的父节点,是因为只有一个虚拟 Tree 根节点,所以返回一个无效 QModelIndex。

QModelIndex ResourcePage::parent(const QModelIndex &child) const
{
    (void) child;
    return QModelIndex();
}
  • index 方法

        方法 index 用于构造一个 QModelIndex 对象,用来唯一表示一个节点,并且与实际的数据关联。后续很多操作可以直接在该 QModelIndex 对象上进行。

QModelIndex ResourcePage::index(int row, int column, const QModelIndex &parent) const
{
    (void) parent;
    return createIndex(row, column, resources_[row]);
}
  • data 方法

        方法 data 也是一种谜之操作,一个节点上的数据可以拆分为多个角色(字段),Qt 定义了好多基础的角色,但是在 ResourcePage 上不关心角色,容易角色都返回相同的数据。

QVariant ResourcePage::data(const QModelIndex &index, int role) const
{
    (void)role;
    return QVariant::fromValue(resources_[index.row()]);
}

        除了继承 QAbstractItemModel,更建议继承实现 QAbstractItemListModel,这样只要实现 rowCount 和 data 这两个方法就可以了。

修改数据

        上面我们实现了一个静态的数据模型,但是要动态增加、删除、修改资源,上面的实现还不够。

        动态数据模式,需要我们主动通知数据的变化。下面从增加、删除、移动节点三个方面来看一下怎么通知。

  • 增加资源

        增加资源的操作需要在 beginInsertRows、endInsertRows 之间执行。虽然 startIndex 与我们的资源数组下标一致,但是 endIndex 其实是最后一个新增节点的下标,所以别忘了减 1。

void ResourcePage::insertResource(int index, QList<ResourceView *> ress)
{
    beginInsertRows(QModelIndex(), index, index + ress.size() - 1);
    for (auto r : ress) {
        resources_.insert(index++, r);
        r->setParent(this);
    }
    endInsertRows();
}
  • 删除资源

       删除资源的操作需要在 beginRemoveRows、endRemoveRows 之间执行。

void ResourcePage::removeResource(int index, QList<ResourceView *> ress)
{
    beginRemoveRows(QModelIndex(), index, index + ress.size() - 1);
    for (auto r : ress) {
        resources_.removeAt(index);
        r->setParent(nullptr);
    }
    endRemoveRows();
}
  • 移动资源

       移动资源的操作需要在 beginMoveRows、endMoveRows 之间执行。相对前面两个操作,这个的实现比较费脑筋。很多人都会困惑,这几个下标,到底要怎么计算?

        首先要分向前移动还是向后移动两种情况,另外 ItemModel 的移动下标与 QList 的 move 方法的下标的意义并不一致。所以唯一的建议就仔细阅读官方文档:

        QAbstractItemModel::beginMoveRows 和 QList::move

void ResourcePage::moveResource(int pos1, int count, int newPos)
{
    int pos = pos1 + count;
    int newPos2 = newPos > pos2 ? newPos + 1 : newPos; // ItemModel diffs from QList
    beginMoveRows(QModelIndex(), pos1, pos2, QModelIndex(), newPos2);
    while (pos2 >= pos1) {
        resources_.move(pos1, newPos);
        --pos2;
    }
    endMoveRows();
}

界面绑定数据

        有了数据为基础,接下来就是看界面怎么实现了。

        与 ResourcePage 对应的 UI 模块是 PageCanvas。PageCanvas 派生与 QQGraphicsItem,管理一个页面的所有控件(每个资源对应一个控件)实例。控件也是派生于 QQGraphicsItem,作为子 Item 存在于 PageCanvas 中。

        借着数据绑定的任务,先说明一下 QQGraphicsItem 子节点管理的一些方法:

        首先是增加子节点,QQGraphicsItem 并没有 addChild 或者 insertChild 之类的方法,而是通过修改 child 的 parentItem 来实现的:

child->setParentItem(parent)

        设置 parent 会从原来的 parent 中删除自己(如果有的话),然后加入到新的 parent 子节点列表的最后。

        然后是删除子节点,还是调用 setParent,设置 parent 为 null。

        而移动子节点的位置,同样要从 child 的角度来做,通过 stackBefore 方法完成,下面的方法移动下标从 start 到 end 的所有节点,移动到下标 dest:

void PageCanvas::resourceMoved(QModelIndex const &parent, int start, int end,
                                QModelIndex const &destination, int dest)
{
    (void) parent;
    (void) destination;
    if (dest < start) {
        ControlView * dest = childItems()[dest];
        while (start <= end) {
            ControlView * item = childItems()[start];
            item->stackBefore(dest);
            ++start;
        }
        dest->update();
    } else if (dest > end) {
        ControlView * first = childItems()[start];
        while (++end < dest) {
            ControlView * item = childItems()[end];
            item->stackBefore(first);
        }
        first->update();
    }
}

       PageCanvas 与数据的动态绑定主要是通过信号连接来实现。QAbstractItemModel 提供数据一系列变化信号:

void PageCanvas::switchPage(ResourcePage * page)
{
    if (page_ != nullptr) {
        page_->disconnect(this);
        for (int i = page_->resources().size() - 1; i >= 0; --i) {
            removeResource(i);
        }
    }
    page_ = page;
    if (page_ != nullptr) {
        for (int i = 0; i < page_->resources().size(); ++i)
            insertResource(i);
        QObject::connect(page_, &ResourcePage::rowsInserted,
                         this, &PageCanvas::resourceInserted);
        QObject::connect(page_, &ResourcePage::rowsRemoved,
                         this, &PageCanvas::resourceRemoved);
        QObject::connect(page_, &ResourcePage::rowsMoved,
                         this, &PageCanvas::resourceMoved);
    }
}

        在绑定信号之前,我们先同步已有的数据(for 循环 insertResource),这是常见的数据同步操作。

数据角色

        上面的例子并没有涉及到具体的数据(角色),也没有数据的更新。为了完整性考虑,接下来补充一些与数据角色相关知识。

        下面是一个学生 Model 的数据实现:

enum Roles {
    StudentRole = Qt::UserRole + 1,
    IdRole,
    NameRole,
    NumberRole,
    SexRole,
    IconRole,
};

QVariant StudentModel::data(const QModelIndex &index, int role) const
{
    Student * s = student(index.row());
    switch (role) {
    case Qt::DisplayRole:
    case StudentRole:
        return QVariant::fromValue(s);
    case IdRole:
        return QVariant::fromValue(s->id());
    case NameRole:
        return s->name();
    case NumberRole:
        return s->number();
    case SexRole:
        return QVariant::fromValue(s->sex());
    case IconRole:
        return QVariant::fromValue(s->icon());
    }
    return QVariant();
}

        这里自定义了一些 Role,data 方法为每种 Role 返回不同的值。

        如果要在 Qml 使用这些数据角色,最好的方法是为每个角色起个名字,具体做法是实现 roleNames 方法:

QHash<int, QByteArray> StudentModel::roleNames() const
{
    static QHash<int, QByteArray> roles;
    if (roles.empty()) {
        roles = QAbstractItemModel::roleNames();
        roles[StudentRole] = "student";
        roles[IdRole] = "stuId";
        roles[NameRole] = "name";
        roles[NumberRole] = "number";
        roles[SexRole] = "sex";
        roles[IconRole] = "icon";
    }
    return roles;
}

        这样在 Qml 就可以很方便是使用这些数据了:

ListView {
    id: listView
    model: viewModel.studentListModel

    Text {
        id: name
        text: name
    }
    Text {
        id: number
        text: numer
    }
}

        当列表的某一项数据有变化时,使用 dataChanged 方法通知外部,通知可以附加角色列表,表示哪些角色有变化,以便减少不必要的更新,提高处理效率。

int n1 = 9; // row 9
dataChanged(index(n1, 0), index(n1, 0), {NameRole, NumberRole});

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Qt QGC 4.2 是一个图形用户界面开发框架,用于创建功能丰富且可自定义的应用程序。要修改Qt QGC 4.2界面,你可以按照以下步骤进行: 1. 打开Qt QGC 4.2源代码。可以通过Git克隆Qt QGC 4.2的存储库,或者从官方网站上下载源代码。 2. 找到需要修改的界面文件。Qt QGC 4.2使用Qt Quick作为默认的用户界面技术,所以界面文件通常以.qml文件的形式存在。根据你想要修改的部分,找到对应的.qml文件。 3. 使用Qt Creator打开.qml文件。Qt Creator是Qt官方提供的一个集成开发环境,用于图形界面和应用程序的开发。 4. 在.qml文件进行修改。根据你的需求,可以通过修改.qml文件来改变界面的布局、颜色、字体等。你可以修改元素的属性值、添加新的元素、删除不需要的元素等。 5. 保存修改并重新编译应用程序。在完成界面修改后,保存.qml文件并重新编译应用程序。使用Qt Creator的构建功能可以方便地进行编译和构建应用程序。 6. 运行应用程序并测试修改。启动应用程序并测试修改后的界面。确保修改的界面符合预期,并且没有引入任何问题或错误。 总结,要修改Qt QGC 4.2界面,你需要找到.qml文件并使用Qt Creator进行修改。通过修改.qml文件的属性和元素,可以改变界面的外观和行为。完成修改后,重新编译和运行应用程序,以确保修改的界面能够正常工作。希望这个回答对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting Horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值