如何声明多个 QML 属性而不单独处理每个属性?

Posted

技术标签:

【中文标题】如何声明多个 QML 属性而不单独处理每个属性?【英文标题】:How to declare multiple QML properties without handling each one individually? 【发布时间】:2016-07-07 17:17:47 【问题描述】:

我有一个玩具 QML 应用程序 (Qt 5.7),它由以下 QML 页面和一些 C++ 代码组成,这些代码允许 QML 订阅音频设备上的某些状态并在 QML GUI 中反映该状态。玩具 QML 看起来像这样:

import QtQuick 2.6
import QtQuick.Controls 1.5
import QtQuick.Window 2.2

Item 
   id: deviceState

   property bool mute1: false
   property bool mute2: false
   property bool mute3: false
   property bool mute4: false

   Component.onCompleted: 
      // Call out to C++ land, to tell the server what 
      // control points we are interested in tracking the state of
      topLevelWindow.subscribeToControlPoints("Input 1-4 Mute", deviceState);
   

   // Called by C++ land to tell us the current state of
   // a control point we previously subscribed to.
   function onControlPointValueUpdated(address, value) 
      if (address == "Input 1 Mute") 
         mute1 = value
      
      else if (address == "Input 2 Mute") 
         mute2 = value
      
      else if (address == "Input 3 Mute") 
         mute3 = value
      
      else if (address == "Input 4 Mute") 
         mute4 = value
      
   

   // Absurdly minimalist example GUI
   Text 
      text: parent.mute4 ? "MUTED" : "unmuted"
   

当我运行此程序时,只要我的音频设备上的四个静音状态之一发生更改,我的 C++ 代码就会调用 onControlPointValueUpdated(),并且基于调用中的“地址”参数,四个 muteN QML 属性之一发生更改.而且,每当 mute4 属性更改时,文本区域中的文本都会更新以反映这一点。

就目前而言,这一切都很好,但在某些时候我需要扩大规模;特别是我将需要跟踪成百上千个值,而不仅仅是四个,并且必须为每个值手动声明一个单独的、手工命名的 QML 属性很快就会变得很困难并且难以维护;如果必须手动 if/elseif 遍历每个属性,onControlPointValueUpdated() 的实现将变得非常低效。

所以我的问题是,在 QML 中是否有某种方法可以让我声明一个属性数组(或者更好的是一个字典/映射),以便我可以一次声明大量 QML 属性?即这样的事情:

property bool mutes[256]: false   // would declare mutes[0] through mutes[255], all default to false

...如果没有,是否有其他推荐的方法来在 QML 文档中保存大量可观察状态?我喜欢将我的 GUI 小部件绑定到 QML 属性以在必要时自动更新的能力,但似乎 QML 属性可能不打算用于大规模

【问题讨论】:

这取决于您打算如何呈现所有变量。目前,您已经有了“极简主义的示例 GUI”——实际的 GUI 会是什么样子?会是显示这些值的Text 项目列表吗? 现在,也许我不明白,但你不能只创建一个 javascript 对象吗?然后你从像text: parent.obj.mute4 ? "MUTED" : "unmuted" 这样的文本访问它,你认为undefinedfalse?还有更新obj[property] = value? @Mitch 可能类似于一个大的按钮网格,当静音处于活动状态时会变成红色,或类似的东西。但我正在寻找一种能够很好地工作(即高效,并且 QML 编码复杂度最低)的机制,用于我将来可能提出的任何 GUI。 @rpadovani 可能;我对 QML 或 Javascript 还不是很熟悉,所以我不清楚这是否可行。特别是,JavaScript 对象的成员是否充当 QML 属性? (即设置 obj[property]=value 会导致 Text 小部件自行更新吗?) 你确定你不想要一个数据驱动的设计吗? (例如某个模型上的中继器) 【参考方案1】:

一旦您获得大量需要显示的数据(尤其是如果它们都共享一种通用格式),您就应该考虑使用QAbstractItemModel 派生数据。

另外需要指出的是,从 C++ 调用用户界面的 QML 并不是一个好主意,因为它往往会限制您在 QML 中可以做的事情,并将 QML 与 C++ 联系起来。

例如,如果您要在列表中显示数据:

视图中的项目是按需创建的,并且在任何时候分配的数量都是有限的,这在您拥有大量数据和潜在的复杂委托时会有所帮助。

下面是带有自定义角色的简单自定义QAbstractListModel 的外观:

main.cpp:

#include <QtGui>
#include <QtQuick>

class UserModel : public QAbstractListModel

public:
    UserModel() 
        for (int i = 0; i < 100; ++i) 
            mData.append(qMakePair(QString::fromLatin1("Input %1").arg(i), i % 5 == 0));
        
    

    enum Roles
    
        NameRole = Qt::UserRole,
        MutedRole
    ;

    QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE
    
        if (!index.isValid())
            return QVariant();

        switch (role) 
        case Qt::DisplayRole:
        case NameRole:
            return mData.at(index.row()).first;
        case MutedRole:
            return mData.at(index.row()).second;
        

        return QVariant();
    

    int rowCount(const QModelIndex &/*parent*/ = QModelIndex()) const Q_DECL_OVERRIDE
    
        return mData.size();
    

    int columnCount(const QModelIndex &/*parent*/ = QModelIndex()) const Q_DECL_OVERRIDE
    
        return 1;
    

    virtual QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE
    
        QHash<int, QByteArray> names;
        names[NameRole] = "name";
        names[MutedRole] = "muted";
        return names;
    

private:
    QVector<QPair<QString, int> > mData;
;

int main(int argc, char *argv[])

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    UserModel userModel;
    engine.rootContext()->setContextProperty("userModel", &userModel);
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();

main.qml:

import QtQuick 2.3
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2

Window 
    width: 300
    height: 300
    visible: true

    ListView 
        id: listView
        anchors.fill: parent
        anchors.margins: 10
        model: userModel
        spacing: 20

        delegate: ColumnLayout 
            width: ListView.view.width

            Text 
                text: name
                width: parent.width / 2
                font.pixelSize: 14
                horizontalAlignment: Text.AlignHCenter
            

            RowLayout 
                Rectangle 
                    width: 16
                    height: 16
                    radius: width / 2
                    color: muted ? "red" : "green"
                

                Text 
                    text: muted ? qsTr("Muted") : qsTr("Unmuted")
                    width: parent.width / 2
                    horizontalAlignment: Text.AlignHCenter
                
            
        
    

例如,您可以轻松地将 ListView 替换为 GridView。最重要的是,C++ 对 QML 一无所知。

您可以阅读更多关于在 Qt Quick 中使用 C++ 模型的信息here。


查看您在 cmets 中发布的图片,可以通过多种方式制作这种类型的 UI。您可以使用上述方法,但在委托中使用 LoaderLoader 可以根据模型中的一些数据来确定要显示的component 的类型。然而,看起来你的 UI 上显示的数据量虽然很大,但如果没有视图确实可能会更好。

您仍然可以使用模型(例如,使用 @peppe 提到的 Repeater),但您也可以以类似的方式列出每个“面板”(我不知道它们叫什么)来逃避你现在是怎么做的。看起来一个面板中的大部分数据都是相同的,所以每个都可以是自己的类(下面是未经测试的代码):

#include <QtGui>
#include <QtQuick>

class Panel : public QObject

    Q_OBJECT
    Q_PROPERTY(bool muted READ muted WRITE setMuted NOTIFY mutedChanged)

    Panel()  /* stuff */ 

    // getters
    // setters

signals:
    // change signals

private:
    // members
;

class WeirdPanel : public QObject

    Q_OBJECT
    Q_PROPERTY(int level READ level WRITE setLevel NOTIFY levelChanged)

    WeirdPanel()  /* stuff */ 

    // getters
    // setters

signals:
    // change signals

private:
    // members
;

class Board : public QObject

    Q_OBJECT
    Q_PROPERTY(Panel *panel1 READ panel1 CONSTANT)
    Q_PROPERTY(Panel *panel2 READ panel2 CONSTANT)
    Q_PROPERTY(Panel *panel3 READ panel3 CONSTANT)
    // etc.
    Q_PROPERTY(WeirdPanel *weirdPanel READ weirdPanel WRITE setWeirdPanel NOTIFY weirdPanelChanged)

public:

    Board() 
    

    // getters
    // setters

signals:
    // change signals

private:
    Panel panel1;
    Panel panel2;
    Panel panel3;
    Panel panel4;
    WeirdPanel weirdPanel;
;

int main(int argc, char *argv[])

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    Board board;
    engine.rootContext()->setContextProperty("board", &board);
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();

main.qml:

import QtQuick 2.3
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2

Window 
    width: 300
    height: 300
    visible: true

    RowLayout 
        anchors.fill: parent

        Panel 
            id: panel1
            panel: board.panel1
        

        Panel 
            id: panel2
            panel: board.panel2
        

        Panel 
            id: panel3
            panel: board.panel3
        

        WeirdPanel 
            id: weirdPanel
            panel: board.weirdPanel
        
    

那么,Panel.qml 可能只是一列按钮之类的:

import QtQuick 2.0
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.0

Rectangle 
    property var panel

    color: "#444"

    implicitWidth: columnLayout.implicitWidth
    implicitHeight: columnLayout.implicitHeight

    ColumnLayout 
        anchors.fill: parent

        Text 
            text: panel.name
            width: parent.width / 2
            font.pixelSize: 14
            horizontalAlignment: Text.AlignHCenter
        

        RowLayout 
            Rectangle 
                width: 16
                height: 16
                radius: width / 2
                color: panel.muted ? "red" : "green"
            

            Text 
                text: panel.muted ? qsTr("Muted") : qsTr("Unmuted")
                width: parent.width / 2
                horizontalAlignment: Text.AlignHCenter
            
        
    

【讨论】:

我认为这种方法的一个问题是,在 ListView(或 GridView 等)中一起显示所有值对于我的 GUI 来说是不够的;而是 GUI 需要为与其关联的每种类型的数据项提供适当类型的控件(即用于静音的切换按钮、用于电平控制的推子、用于平移控制的 panpots 等)。此外,它还需要能够以各种 QML 文件定义的方式布置这些控件,我认为 View 不允许这样做。 那么这是您应该在问题中提及的内容。 :) 毕竟,我们在这里谈论的是 GUI 工具包。 但我很难相信除了视图之外的任何东西都是“数百或数千个值”的正确选择。我会用一些可能有帮助的额外信息来更新我的答案。 这是一个(随机选择的)GUI 示例,可能在某些时候需要:wheatstone.com/images/NAB2016%20PR%20IMAGES/… 用 Licecap 制作它们非常容易,而且它们让枯燥的旧文字墙变得更加有趣。 :)【参考方案2】:

经过一番搜索,似乎最适合我当前需求的机制是 QQmlPropertyMap 类。

特别是,我将QQmlPropertyMap 添加到我的QQmlContext,并通过setContextProperty() 将其公开给QML 世界:

_sessionContext = new QQmlContext(_engine.rootContext());
_propertyMap = new QQmlPropertyMap(_sessionContext);
_sessionContext->setContextProperty("cps", _propertyMap);

...然后当我收到 C++ 代码中的值更新时,我没有调用 QML 函数,而是用它更新 QQmlPropertyMap 对象:

_propertyMap->insert("input_4_mute", QVariant(true));  // arguments hard-coded here for clarity

... QML 代码中剩下要做的就是绑定到属性,但是我想:

Rectangle 
    width: 0; height: 0
    x:160; y:120
    color: cps.input_4_mute ? "red" : "blue"

【讨论】:

酷!我什至不知道那门课!

以上是关于如何声明多个 QML 属性而不单独处理每个属性?的主要内容,如果未能解决你的问题,请参考以下文章

QML Swipeview 附加属性

如何导出 qmlitem 的所有属性/信号?

Qt:如何在 C++ 端而不是 QML 上监视 Q_PROPERTY 更改

如何将具有自定义属性的组件移动到 QML 中的单独文件

翻译 | QML编码约定

在 QML 中设置子项属性的正确方法