具有代码控制属性的组件(没有默认值)
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(' ') }
}
}
总结
如何在 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(' ') }
}
}