Phrogz
Phrogz

Reputation: 303540

Component with code-controlled properties (without default value)

Summary

How can I create a component in QML that allows me to specify custom properties with default values, which can be overridden by command-line parameters, but which do NOT initially use the default value?

Background

I've created a QML component that can be used to parse command-line properties easily and bind to their values. Its usage looks like this:

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)

It works quite well, except...

Problem

All bindings that are created to my component initially use the default value. For example, if I launch my app with -w 800 the window's width initially starts with a value of 1920, and then ~immediately resizes to 800 (when the Component.onCompleted code runs). This problem is unnoticeable 90% of the time, mildly annoying 8% of the time...and unusable in the final 2%.

Sometimes the properties that I want to control can only be set once. For example, a network port to connect to using fragile code that cannot disconnect and reconnect to a new port when it changes. Or a mapping library that loads an enormous set of resources for one visual style, and then throws errors if I attempt to change the style.

So, I need the properties to get the command-line value—if specified—the very first time they are created (and otherwise use the default value). How can I make this happen?

Upvotes: 2

Views: 237

Answers (2)

Phrogz
Phrogz

Reputation: 303540

If I completely change the interface to my component, such that the default value is passed to a function that returns a value, then I can achieve my goals.

So, instead of:

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

I must use something like:

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

This new interface has some disadvantages:

  • I can no longer specify the order I want arguments to appear in the help, as the properties may invoke arg() in any order.
  • I can no longer require positional arguments with no flags (e.g. app filename1 filename2) for the same reason.
  • I have to repeat the type of the property in the descriptor.

It has other benefits, however:

  • The names of properties do not have to be repeated.
  • It is fewer lines of code (one line per property instead of 2).
  • It actually solves my problem stated above.

Example usage:

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 });
    }
}

The Code:

// 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(' ') }
  }
}

Upvotes: 1

dtech
dtech

Reputation: 49329

UPDATE: Actually, in that particular case it is actually very easy to avoid the resizing - just set visibility to false, then set the properties to the desired values, and set visibility to 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
  }
}

In this case it is convenient since you can simply hide the window until you set the desired property values. In case you actually need to create the component with the correct initial values, you can do something like this:

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)
}

In this case the application will start without any window, the desired values will be set on prototype level, so that its creation will be delayed and have the right values right from the start.


It is understandable that this happens, after all you don't read in the arguments until after your application has loaded. Thus it will load up the defaults and then switch to the supplied arguments.

If you want to avoid that, the most straightforward solution would be to read in the arguments and expose them as context properties before the main qml file is loaded, such as this (posting fullly working code since you mentioned you are not a C++ guy):

#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();
}

And then in your main.qml file:

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

As the component is created it picks up the correct values right away.

Upvotes: 2

Related Questions