具有代码控制属性的组件(没有默认值)

Component with code-controlled properties (without default value)

总结

如何在 QML 中创建一个允许我使用默认值指定自定义属性的组件,这些属性可以被命令行参数覆盖,但最初不使用默认值?

背景

我创建了一个 QML 组件,可用于轻松解析命令行属性并绑定到它们的值。它的用法如下所示:

Window {
    visibility: appArgs.fullscreen ? "FullScreen"  : "Windowed"
    width: appArgs.width
    height: width*9/16

    CommandLineArguments {
        id: appArgs

        property real width: Screen.width
        property bool fullscreen: false

        Component.onCompleted: args(
          ['width',      ['-w', '--width'],      'initial window width'],
          ['fullscreen', ['-f', '--fullscreen'], 'use full-screen mode']
        )
    }
}
$ ./myapp --help
qml: Command-line arguments supported:
-w, --width        : initial window width    (default:1920)
-f, --fullscreen   : use full-screen mode    (default:false)

效果很好,除了...

问题

为我的组件创建的所有绑定最初都使用默认值。例如,如果我使用 -w 800 启动我的应用程序,window 的 width 最初以值 1920 开始,然后立即调整为 800(当 Component.onCompleted代码运行)。这个问题在 90% 的时间里是不明显的,在 8% 的时间里有点烦人......最后 2% 的时间无法使用。

有时我要控制的属性只能设置一次。例如,要连接到的网络端口使用脆弱的代码,这些代码在更改时无法断开连接并重新连接到新端口。或者一个映射库,它为一种视觉样式加载大量资源,然后在我尝试更改样式时抛出错误。

因此,我需要这些属性来获取命令行值(如果已指定),这是在它们第一次创建时(否则使用默认值)。我怎样才能做到这一点?

更新:实际上,在那种特殊情况下,实际上很容易避免调整大小 - 只需将可见性设置为 false,然后将属性设置为所需的值,并将可见性设置为 true:

Window {
  id: main
  visible: false

  Component.onCompleted: {
    main.width = ARG_Width // replace with 
    main.height = ARG_Width * 9/16 // your stuff
    main.visibility = ARG_Fullscreen ? Window.FullScreen : Window.Windowed
    main.visible = true
  }
}

在这种情况下很方便,因为您可以简单地隐藏 window,直到您设置了所需的 属性 值。如果您确实需要使用正确的初始值创建组件,您可以这样做:

Item {
  id: main
  Component {
    id: win
    Window {
      visible: true
      width: ARG_Width
      height: width*9/16
      visibility: ARG_Fullscreen ? Window.FullScreen : Window.Windowed
    }
  }

  Component.onCompleted: win.createObject(main)
}

在这种情况下,应用程序将在没有任何 window 的情况下启动,所需的值将在原型级别设置,因此它的创建将被延迟并从一开始就有正确的值。


发生这种情况是可以理解的,毕竟您要等到应用程序加载后才读入参数。因此它将加载默认值,然后切换到提供的参数。

如果你想避免这种情况,最直接的解决方案是在加载主 qml 文件之前读入参数并将它们作为上下文属性公开,例如这个(发布完整的工作代码,因为你提到你是不是 C++ 人):

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>

int main(int argc, char *argv[])
{
  QGuiApplication app(argc, argv);
  QQmlApplicationEngine engine;

  int w = 1920; // initial
  bool f = false; // values

  QStringList args = app.arguments();
  if (args.size() > 1) { // we have arguments
    QString a1 = args.at(1);
    if (a1 == "-w") w = args.at(2).toInt(); // we have a -w, read in the value
    else if (a1 == "-f") f = true; // we have a -f
  }
  engine.rootContext()->setContextProperty("ARG_Width", w); // expose as context 
  engine.rootContext()->setContextProperty("ARG_Fullscreen", f); // properties

  engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); // load main qml

  return app.exec();
}

然后在您的 main.qml 文件中:

Window {
  id: main
  visible: true
  width: ARG_Width
  height: width*9/16
  visibility: ARG_Fullscreen ? Window.FullScreen : Window.Windowed
}

创建组件时,它会立即获取正确的值。

如果我完全改变我的组件的接口,这样默认值被传递给一个函数returns一个值,那么我就可以实现我的目标.

所以,而不是:

property real width: Screen.width
...
Component.onCompleted: args(
    ['width', ['-w', '--width'], 'initial window width'],
)

我必须使用类似的东西:

property real width: arg(Screen.width, ['-w', '--width'], 'real', 'initial window width')

这个新界面有一些缺点:

  • 我无法再指定我希望参数在帮助中出现的顺序,因为属性可以按任何顺序调用 arg()
  • 出于同样的原因,我不能再要求没有标志的位置参数(例如 app filename1 filename2)。
  • 我必须在描述符中重复 属性 的类型。

它还有其他好处,但是:

  • 属性名称不必重复。
  • 更少的代码行(每个 属性 一行,而不是 2 行)。
  • 它实际上解决了我上面提到的问题。

用法示例:

CommandLineParameters {
    id: appArgs
    property string message:    arg('hi mom',  '--message',           'string', 'message to print')
    property real   width:      arg(400,      ['-w', '--width'],      'real',   'initial window width')
    property bool   fullscreen: arg(false,    ['-f', '--fullscreen'], 'bool',   'use full screen?')
    property var    resolution: arg('100x200', '--resolution',        getResolution)

    function getResolution(str) {
        return str.split('x').map(function(s){ return s*1 });
    }
}

代码:

// CommandLineParameters.qml
import QtQml 2.2

QtObject {
  property var _argVals
  property var _help: []

  function arg(value, flags, type, help) {
    if (!_argVals) { // Parse the command line once only
      _argVals = {};
      var key;
      for (var i=1,a=Qt.application.arguments;i<a.length;++i){
        if (/^--?\S/.test(a[i])) _argVals[key=a[i]] = true;
        else if (key) _argVals[key]=a[i], key=0;
        else console.log('Unexpected command-line parameter "'+a[i]+'');
      }
    }

    _help.push([flags.join?flags.join(", "):flags, help||'', '(default:'+value+')']);

    // Replace the default value with one from command line
    if (flags.forEach) flags.forEach(lookForFlag);
    else         lookForFlag(flags);

    // Convert types to appropriate values
    if (typeof type==='function') value = type(value);
    else if (type=='real' || type=='int') value *= 1;

    return value;

    function lookForFlag(f) { if (_argVals[f] !== undefined) value=_argVals[f] }
  }

  Component.onCompleted: {
    // Give help, if requested
    if (_argVals['-h'] || _argVals['--help']) {
      var maxF=Math.max.apply(Math,_help.map(function(a){return a[0].length}));
      var maxH=Math.max.apply(Math,_help.map(function(a){return a[1].length}));
      var lines=_help.map(function(a){
        return pad(a[0],maxF)+" : "+pad(a[1],maxH)+" "+a[2];
      });
      console.log("Command-line arguments supported:\n"+lines.join("\n"));
      Qt.quit(); // Requires connecting the slot in the main application
    }
    function pad(s,n){ return s+Array(n-s.length+1).join(' ') }
  }
}