一、在单独文件中自定义组件
我们可以在终端中使用 pip
安装 PySide6 模块。默认是从国外的主站上下载,因此,我们可能会遇到网络不好的情况导致下载失败。我们可以在 pip
指令后通过 -i
指定国内镜像源下载。
pip install pyside6 -i https://mirrors.aliyun.com/pypi/simple
国内常用的 pip
下载源列表:
- 阿里云 https://mirrors.aliyun.com/pypi/simple
- 中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple
- 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple
- 中国科学技术大学 http://pypi.mirrors.ustc.edu.cn/simple
虽然 QML 中提供了多个样式可供使用,但是用户有时还是想实现自定义的外观。这里,我们新建一个 MyButton.qml 文件,用来存放自定义的组件。
import QtQuickRectangle {id: containerRectIdwidth: buttonTextId.implicitWidth + 60height: buttonTextId.implicitHeight + 20color: "#99CCFF"// 为内部的Text元素提供一个别名属性,方便在外部设置按钮的文本property alias buttonText: buttonTextId.textsignal buttonClicked() // 自定义一个信号border {width: 1color: "#666666"}Text {id: buttonTextIdanchors.centerIn: parenttext: "按钮"font.pointSize: 16color: "#FF6666"}MouseArea {id: mouseAreaIdanchors.fill: parentonClicked: {containerRectId.buttonClicked() // 触发信号}}
}
在 QML 中,我们无法使用自定义组件内部元素的属性,因此,我们需要使用 alias
关键字给内部元素的属性起一个别名,方便在外部设置该内部元素的属性值。
我们新建一个 template.qml 文件,在该文件中调用自定义的 QML 组件。
import QtQuick.Window
import QtQuick.Layouts// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {width: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray" // 窗口的背景颜色Text {id: textIdanchors.centerIn: parentfont.pointSize: 32color: "#FF6666"}Column {MyButton {buttonText: "按钮1"onButtonClicked: {textId.text = "按钮1被点击了"}}MyButton {buttonText: "按钮2"onButtonClicked: {textId.text = "按钮2被点击了"}}}
}
这里,我们将 template.qml 文件和 MyButton.qml 文件放置到同一级目录下。然后,我们可以在 template.qml 文件中直接用存放自定义组件的文件名创建自定义组件。
自定义组件的名称一定要与存放自定义组件的文件名首字母一致,否则会报如下错误:
file:///E:/Software/Python/PySide6/Template/template.qml:13:9: MyButton is not a type
file:///E:/Software/Python/PySide6/Template/template.qml: File name case mismatch
自定义组件的存放文件的首字母一定要大写,否则会报如下错误:
file:///E:/Software/Python/PySide6/Template/template.qml:22:9: Cannot assign to non-existent property "myButton"
我们新建一个 template.py 文件,用来加载 template.qml 文件。
import sysfrom PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngineif __name__ == "__main__":app = QApplication(sys.argv) # 1.创建一个QApplication类的实例engine = QQmlApplicationEngine() # 2.创建QML引擎对象engine.load("template.qml") # 3.加载QML文件sys.exit(app.exec()) # 4.进入程序的主循环并通过exit()函数确保主循环安全结束
这里自定义组件的 QML 文件名的首字母一定要大写。
自定义的组件名与 QML 文件名一致。
组件实例的
id
和组成组件的顶层Item
的id
是各自独立的。
二、使用component自定义组件
我们修改 template.qml 文件的内容,使用 component 自定义一个组件。
import QtQuick.Window// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {width: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray"// MyButton为自定义的组件名component MyButton: Rectangle {id: containerRectIdwidth: buttonTextId.implicitWidth + 60height: buttonTextId.implicitHeight + 20color: "#99CCFF"// 为内部的Text元素提供一个别名属性,方便在外部设置按钮的文本property alias buttonText: buttonTextId.textsignal buttonClicked() // 自定义一个信号border {color: "#666666"width: 1}Text {id: buttonTextIdtext: "按钮"font.pointSize: 16color: "#FF6666"anchors.centerIn: parent}MouseArea {id: mouseAreaIdanchors.fill: parentonClicked: {containerRectId.buttonClicked() // 触发信号}}}Text {id: textIdanchors.centerIn: parentfont.pointSize: 32color: "#FF6666"}Row {spacing: 20MyButton {buttonText: "按钮1"onButtonClicked: {textId.text = "按钮1被点击了"}}MyButton {buttonText: "按钮2"onButtonClicked: {textId.text = "按钮2被点击了"}}}
}
三、Component组件
Component
是由 Qt 框架或开发者封装好的、只暴露了必要接口的 QML 类型,可以重复利用。一个 QML 组件就像一个黑盒子,它通过属性、信号、函数和外部世界交互。
Component
有两个信号:completed()
信号会在 对象实例化后 发出的信号,destruction()
信号会在 在对象开始销毁 时发出x信号。
修改 template.qml 文件的内容。
import QtQuick.Window// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {width: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray"// 对象实例化后发出的信号。一旦建立了完整的 QML 环境,它就可用于在启动时执行脚本代码。Component.onCompleted: {console.log("Component.onCompleted")console.log("width: " + width, ", height: " + height, ", color: " + color)}// 在对象开始销毁时发出信号Component.onDestruction: {console.log("Component.onDestruction")}
}
我们还可以自定义一个 Component
。Component
只能包含一个顶层 Item
,而且在这个顶层 Item
之外不能定义除了 id
属性以外的任何数据。而在顶层 Item
之内,则可以包含更多的子元素来协同工作,最终形成一个具有特定功能的组件。
import QtQuick.Window// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {width: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray"// 自定义一个Componoent,需要手动加载才会显示Component {// Component不能定义除id以外的其它属性id: buttonComponent// Component只能有一个顶层ItemRectangle {id: containerRectIdwidth: buttonTextId.implicitWidth + 60height: buttonTextId.implicitHeight + 20color: "#99CCFF"// 为内部的Text元素提供一个别名属性,方便在外部设置按钮的文本property alias buttonText: buttonTextId.textsignal buttonClicked() // 自定义一个信号border {width: 1color: "#666666"}Text {id: buttonTextIdanchors.centerIn: parenttext: "按钮"font.pointSize: 16color: "#FF6666"}MouseArea {id: mouseAreaIdanchors.fill: parentonClicked: {containerRectId.buttonClicked() // 触发信号}}}}
}
四、使用Loader动态加载组件
Loader
用来动态加载 QML 组件。我们可以把 Loader
作为占位符使用,在需要显示某个元素时,才使用 Loader
把它加载进来。
我们可以使用 Loader
的 source
属性 加载一个 QML 文档,也可以通过其 sourceComponent
属性 加载一个 Component 对象。当 Loader
的 source
或 sourceComponent
属性发生变化时,它之前加载的 Component
会自动销毁,新对象会被加载。将 source
设置为一个 空字符串 或将 sourceComponent
设置为 undefined
,将会 销毁当前加载的对象,相关的资源也会被释放,而 Loader
对象则变成一个空对象。
Loader
的 item
属性 指向它加载的组件的顶层 Item
。对于 Loader
加载的 Item
,它暴露出来的接口,如属性、信号等,都可以通过 Loader
的 item
属性来访问。
虽然 Loader
本身是 Item
的派生类,但没有加载 Component
的 Loader
对象是不可见的,没什么实际的意义,只是个占位符号,而一旦你加载了一个 Component
,Loader
的大小、位置等属性却可以影响它所加载的 Component
。
如果你没有显式指定 Loader
的大小,那么 Loader
会将自己的尺寸调整为与它加载的可见 Item
的尺寸一致。如果 Loader
的大小通过 width
、height
或锚布局显式设置了,那么它加载的可见 Item
的尺寸会被调整以便适应 Loader
的大小。不管是哪种情况,Loader
和它所加载的 Item
具有相同的尺寸,这确保你使用锚来布局 Loader
就等同于布局它加载的 Item
。
如果我们要加载的 Component
比较大,则可能比较卡顿。此时你可以设置 asynchronous
属性为 true
来 开启异步加载模式,当 status
(枚举值)的值为 Loader.Ready
时表示 已经加载完毕。status
属性的取值如下:
Loader.Null
Loader.Ready
Loader.Loading
Loader.Error
如果 Loader
加载的 Item
想 处理按键事件,那么必须将 Loader
对象的 focus
属性设置为 true
。又因为 Loader
本身也是一个焦点敏感的对象,所以如果它加载的 Item
处理了按键事件,则应当将事件的 accepted
属性设置为 true
,以免已经被吃掉的事件再传递给 Loader
。
修改 template.qml 文件的内容。
import QtQuick.Window// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {width: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray"// 自定义一个Componoent,需要手动加载才会显示Component {// Component不能定义除id以外的其它属性id: buttonComponentId// Component只能有一个顶层ItemRectangle {id: containerRectIdwidth: buttonTextId.implicitWidth + 60height: buttonTextId.implicitHeight + 20color: "#99CCFF"// 为内部的Text元素提供一个别名属性,方便在外部设置按钮的文本property alias buttonText: buttonTextId.textsignal buttonClicked() // 自定义一个信号border {width: 1color: "#666666"}Text {id: buttonTextIdanchors.centerIn: parenttext: "按钮"font.pointSize: 16color: "#FF6666"}MouseArea {id: mouseAreaIdanchors.fill: parentonClicked: {containerRectId.buttonClicked() // 触发信号}}}}Text {id: textIdanchors.centerIn: parentfont.pointSize: 32color: "#FF6666"}Row {spacing: 20Loader {id: firstButtonIdsourceComponent: buttonComponentId // 加载一个Component对象asynchronous: true // 异步加载// 加载完成后会触发loaded信号onLoaded: {var customButton = firstButtonId.item // 获取加载组件顶层的Item对象customButton.buttonText = "第一个按钮"customButton.buttonClicked.connect(function() {textId.text = "第一个按钮被点击了"})}}Loader {id: secondButtonIdsourceComponent: buttonComponentId // 加载一个Component对象asynchronous: true // 异步加载// 加载完成后会触发loaded信号onLoaded: {var customButton = secondButtonId.item // 获取加载组件顶层的Item对象customButton.buttonText = "第二个按钮"customButton.buttonClicked.connect(function() {textId.text = "第二个按钮被点击了"})}}}
}