如何在QML中定义一个带有子占位符的“模板”?

31

我真的很喜欢QML。我喜欢如何定义组件(类似于类)及其属性,并从其他地方实例化它们(类似于对象)。

我可以定义一个按钮,具有某种外观和感觉,以及标签文本。例如,可以使用此组件定义(Button.qml)完成此操作:

Item {
    id: button
    property string label

    anchors.fill: parent

    Rectangle {
        anchors.fill: parent
        radius: 10
        color: "gray"

        Text {
            anchors.centerIn: parent
            font.pixelSize: 20
            text: button.label
            color: "white"
        }
    }
}

并在这个主文件 (main.qml) 中实例化:

Rectangle {
    width: 300
    height: 200

    Button {
        anchors.centerIn: parent
        anchors.margins: 50
        label: "Hello button!"
    }
}

但是我看到了以下限制:我只能定义一个带有一些属性的按钮模板,而不能有一些占位符。所有在实例中定义的子元素将成为直接子元素,至少默认情况下是这样的,而我想改变这种行为。

假设我想在按钮中放置一个项目(比方说一张图片,但我不想告诉Button的定义它将是一张图片),我想象一下类似于这样:

Item {
    id: button
    property Item contents   <-- the client can set the placeholder content here

    anchors.fill: parent

    Rectangle {
        anchors.fill: parent
        radius: 10
        color: "gray"

        Item {
            id: placeholder     <-- where the placeholder should be inserted
        }
    }

    Component.onCompleted: {
        // move the contents into the placeholder...
    }
}

我该如何实现这个?我不知道是否应该使用 Component.onCompleted 的方法。需要注意的是,在我的情况下,这些内容以后永远不会改变(至少在应用程序的当前设计中...)。

此外,我希望锚定在占位符内起作用。例如,如果我将内容定义为文本元素,并使其居中于其父元素中(该父元素首先将是模板本身)。然后,我的代码将此文本实例移动到占位符中,父级锚点应该是占位符项的锚点,而不是模板项的锚点。

5个回答

41
我找到了一个更好的答案,建议在Qt Developer Days 2011 "Qt Quick Best Practices and Design Patterns"的演示中使用default property alias ...将子项与任何项目的任何属性进行别名。如果您不想为子项设置别名,而是给别名属性命名,请删除default。(按QML定义,字面意义上的子项是默认属性的值。)
Item {
    id: button
    default property alias contents: placeholder.children

    anchors.fill: parent

    Rectangle {
        anchors.fill: parent
        radius: 10
        color: "gray"

        Item {
            id: placeholder     <-- where the placeholder should be inserted
        }
    }
}

顺便提一下,如果您仍在纠结于演讲中的QML作用域难题,请参见:https://dev59.com/mGgu5IYBdhLWcg3wZma8 - mlvljr
1
太棒了,谢谢你提供这个链接!;) 顺便说一下,“d-pointer”(也在这次讲座中)可以用来更好地隐藏私有成员(也是JS函数)。我认为它比双下划线更好。我甚至将其命名为“priv”,以确保没有人会意外访问此子对象的成员。 - leemes
3
将Connections QML对象以这种方式添加到模板中会导致错误:“无法将对象分配给列表属性“contents”-如何处理这种情况?” - Larpon
@Larpon:你可以像这里所示的那样显式地添加到资源连接http://doc.qt.io/qt-5/qml-qtquick-item.html#data-prop。在你的情况下,它会是类似于resources: [ Connections {...} ]的东西。(编辑:我刚刚注意到你下面的答案) - Marc Van Daele
1
@MarcVanDaele 谢谢 - 是的,最终我找到了并添加了一个答案 :) 未来的谷歌搜索者:请看下面的答案。 - Larpon
显示剩余3条评论

13

如果有其他人像我一样来到这里,Necro来回答。

在Qt5的某个版本中,默认属性变成了“数据”,而不是“children”。 这使得可以添加其他对象类型,而不仅仅是“Item”。 例如,也可以添加连接(回答我自己上面的问题)。

因此,在Qt5中,您应该这样做:

Item {
    id: button
    default property alias contents: placeholder.data

    anchors.fill: parent

    Rectangle {
        anchors.fill: parent
        radius: 10
        color: "gray"

        Item {
            id: placeholder     <-- where the placeholder should be inserted
        }
    }
}

请注意: 将placeholder.children替换为placeholder.data 同时,请注意您不必使用别名contents——可以使用任何您喜欢的名称。例如:

Item {
    id: button
    default property alias foo: placeholder.data
    ...

}

https://dev59.com/Hm445IYBdhLWcg3wIG19#10031214 提供了一个示例,还展示了对占位符的添加。 - Zitrax
1
太好了!非常好的笔记。根据官方文档,我们应该使用data而不是children来实现这个目的。 - S.M.Mousavi

3

实际上,根据我所听到的,正确的答案是使用QML Loader来实现您想要的。

[话虽如此; 我还没有尝试过,但它在我的近期尝试列表中,看起来相当简单]

此外,在stackoverflow上搜索其他"QML Loader"问题,因为有很多问题可以帮助您入门。


谢谢,我不知道 QML Loader 是什么,也不知道它在这种情况下有帮助。我会去看一下的。 - leemes
与Misch的答案一样,您的解决方案没有使用定位。例如:Loader { sourceComponent: Rectangle { color: "red"; anchors.fill: parent } }。矩形应填充加载程序的父级,但它不会这样做。(除非我明确设置宽度和高度,否则它是不可见的。) - leemes

1
您可以使用以下代码移动项目(如果要在占位符中支持多个项目):
property list<Item> contents

Component.onCompleted: {
    var contentItems = [];
    for(var i = 0; i < contents.length; ++i)
        contentItems.push(contents[i]);
    placeholder.children = contentItems;
}

请注意,对于列表属性,QML引擎也会接受单个值,因此您无需为内容属性提供项目列表。

是的,这似乎是正确的代码片段来移动项目。它可以工作,但它不尊重锚定。 (例如,如果我提供一个带有“anchors.centerIn:parent”的文本,则结果看起来像我没有设置锚定属性。) - leemes
我不知道如何“更新”锚定属性(也许内容项仍然认为它们的父级是模板?),但您可以调整它们的大小以填充内容区域。只需将宽度和高度设置为占位符的大小即可。 - Misch

1
简而言之(以表达思想):
import QtQuick 1.1

Item {
    width: 200
    height: 100

    //<-- the client can set the placeholder content here
    property Item contents: Rectangle {
        anchors.fill: parent
        anchors.margins: 25
        color: "red"
    }

    Rectangle {
        id: container

        anchors.fill: parent
        radius: 10
        color: "gray"

        //<-- where the placeholder should be inserted
    }

    Component.onCompleted: {
        contents.parent = container
    }
}

稍微长一点的版本(支持内容重新分配):

import QtQuick 1.1

Item {
    width: 200
    height: 100

    //<-- the client can set the placeholder content here
    property Item contents: Rectangle {
        //anchors can be "presupplied", or set within the insertion code
        //anchors.fill: parent
        //anchors.margins: 25
        color: "red"
    }

    Rectangle {
        id: container

        anchors.fill: parent
        radius: 10
        color: "gray"

        //<-- where the placeholder should be inserted
        //Item {
        //    id: placeholder
        //}
    }

    //"__" means private by QML convention
    function __insertContents() {
        // move the contents into the placeholder...
        contents.parent = container
        contents.anchors.fill = container
        contents.anchors.margins = 25
    }

    onContentsChanged: {
        if (contents !== null)
            __insertContents()
    }

    Component.onCompleted: {
        __insertContents()
    }
}

希望这可以帮到您 :)

如果您有任何问题或额外的要求,请随时提出(或请求更改):) - mlvljr
2
谢谢您提供的解决方案,但我自己添加了另一个答案,引用了我几天前在Qt开发者日活动中发现的一些非常好的内容。 - leemes
是的,这是另一个技巧,但要注意以下几点:1)您只能静态地添加项目(除非直接操作children属性,这有点违背了别名的初衷),2)(一个小问题)继承QtObject(如计时器)的实例不能以这种方式添加(因为children是Item的列表,而不是纯QtObjects)。此外,使用重新父级化方法,您可以任意定位添加的项目(将每个项目放在自己的容器中等)。 - mlvljr
对于问题2),我认为有属性resources或类似的名称。我不知道问题1),感谢您的提示。但在QML中,我们很少遇到想要将子项移除的情况,至少在我描述的特定情况下是这样(当我们想要使组件的“标签”比仅仅是一个字符串更具动态性时)。 - leemes
@leems 顺便说一下,子别名技巧很好用,可以实际上遍历子项并以某种编程方式设置它们(一个经典的例子是屏幕堆栈:你声明性地设置屏幕作为子项,然后项目会正确地设置它们并开始管理它们(特别是如果屏幕实际上将页脚/页眉等部分公开为 QML 属性,并且简单的重新父级关系不足以满足需求))。 - mlvljr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接