QT Quick-QML中窗口设置

前言

QT文档:https://runebook.dev/zh-CN/docs/qt/

QT QuickQt widgets这两种技术,官方是强推QT Quick的。

https://player.bilibili.com/player.html?aid=582748160&bvid=BV1p64y1T79J&cid=177627338&page=1

窗口设置

窗口的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15

Window {
title: qsTr("一个普通标题窗口") //窗口标题
width: 640 //宽度
height: 480 //高度
visible: true //是否可见,缺省为true
color: "#ffffff" //窗口背景色
//#00000000 为窗口透明
//QML支持black 等颜色样式(没有#)
//QML支持#11cfff 等颜色样式
//QML同样支持RGB格式
flags: Qt.Window //窗口标志 说明是什么窗口 使用 | 分割,缺省为Qt.Window
//Qt.Window 普通窗口模式,带标题栏
//Qt.FramelessWindowHint 隐藏标题栏窗口
opacity: 1 //透明度 数值区间为0~1 支持小数,缺省为1
x:0 //位于父窗体的x位置,以左上角为起点,缺省为0 (此时window的父窗体就是桌面了)
y:0 //位于父窗体的y位置,以左上角为起点,缺省为0 (此时window的父窗体就是桌面了)
}

无边框

1
2
3
4
5
6
7
8
Window {
width: 640
height: 480
visible: true
color: "#fefefe"
title: qsTr("主页面")
flags: Qt.FramelessWindowHint
}

注意

flags: Qt.FramelessWindowHint会让窗口无边框并且不再任务栏显示。

如果想无边框并在任务栏显示可以设置:flags: Qt.Window | Qt.FramelessWindowHint

只显示标题栏

显示标题栏,但是没有关闭最大最小化按钮

1
2
3
4
5
6
7
8
Window {
width: 640
height: 480
visible: true
color: "#fefefe"
title: qsTr("主页面")
flags: "CustomizeWindowHint"
}

背景透明无边框窗口

1
2
3
4
5
6
7
8
9
Window {
width: 640
height: 480
visible: true
color: "#00000000"
title: qsTr("主页面")
flags: Qt.FramelessWindowHint|Qt.Window|Qt.WindowStaysOnTopHint
opacity:1
}

opacity这个属性是对当前组件以及子组件都设置不透明度,所以不太适用

color: Qt.rgba(0,0,0,0)是对当前设置透明度,不会影响子组件

注意

  • Qt.FramelessWindowHint 无边框
  • Qt.Window 显示在状态栏
  • Qt.WindowStaysOnTopHint 窗口置顶

窗口显示

窗口置顶-属性设置

Qt.WindowStaysOnTopHint: 这个标志使得窗口保持在所有其他常规窗口之上,但可能仍然会在一些系统窗口(如任务栏、通知窗格等)之下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
id: appWindow
visible: true
width: 640
height: 480
title: "Topmost Window"

flags: Qt.WindowStaysOnTopHint

Button {
text: "Close"
anchors.centerIn: parent
onClicked: appWindow.close()
}
}

窗口置顶-事件触发

永远置顶

1
mainWin.flags = Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint;

临时置顶

1
2
3
mainWin.flags = Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint;
mainWin.requestActivate();
mainWin.flags = Qt.Window | Qt.FramelessWindowHint;

窗口激活

窗口激活会让窗口处于激活状态,但是并不会显示在最前方。

1
mainWin.requestActivate();

窗口恢复

最小化

1
mainWin.showMinimized();

恢复

1
2
3
if (mainWin.windowState === Qt.WindowMinimized){
mainWin.showNormal();
}

万能窗口临时置顶

无论窗口处于什么状态都能临时置顶的状态

1
2
3
4
5
6
mainWin.flags = Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint;
mainWin.requestActivate();
mainWin.flags = Qt.Window | Qt.FramelessWindowHint;
if (mainWin.windowState === Qt.WindowMinimized){
mainWin.showNormal();
}

窗口穿透

局部穿透

https://doc.qt.io/qt-5/qwindow.html#setMask

官网是这么说的

The mask is a hint to the windowing system that the application does not want to receive mouse or touch input outside the given region.

意思是

应用不接收给定的区域之外的事件。

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from PySide2 import QtCore
from PySide2.QtCore import Slot, QPoint
from PySide2.QtGui import QPolygon, QRegion, QWindow


class ZMaskUtil(QtCore.QObject):
points = []

@Slot()
def clearPoints(self):
self.points.clear()

@Slot(QPoint)
def addPoint(self, point):
self.points.append(point)

@Slot(QWindow)
def setMask(self, window):
if window:
polygon = QPolygon(self.points)
region = QRegion(polygon)
# fillscreen = window.screen().geometry()
# region = region.xored(fillscreen)
window.setMask(region)

布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15

Window {
id:mywin
visible: true
width: 600; height: 400
color: "#33000000"
title: qsTr("主页面")
flags: Qt.FramelessWindowHint|Qt.Window|Qt.WindowStaysOnTopHint

function renderMask(){
maskutil.clearPoints();
maskutil.addPoint(Qt.point(0,0));
maskutil.addPoint(Qt.point(100,0));
maskutil.addPoint(Qt.point(100,100));
maskutil.addPoint(Qt.point(0,100));
maskutil.addPoint(Qt.point(0,0));
maskutil.setMask(mywin)
}

Component.onCompleted: {
renderMask()
}

Button {
text: "hello"
width: 100
height: 100
onClicked: {

}
}
}

程序入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sys

from PySide2.QtCore import *
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtWidgets import QApplication

from ZMaskUtil import ZMaskUtil

if __name__ == '__main__':
app = QApplication()
engine = QQmlApplicationEngine()
maskutil = ZMaskUtil()
engine.rootContext().setContextProperty('maskutil', maskutil)
engine.load(QUrl.fromLocalFile('./main.qml'))
sys.exit(app.exec_())

注意

这样其实窗口背景不是透明的也没关系,因为这个方法相当于把穿透的地方都裁剪掉了。

注意上面的例子中传入的坐标区域是不穿透的,

如果想要传入的地方穿透

1
2
3
>fillscreen = window.screen().geometry()
> region = region.xored(fillscreen)
>

把选择的区域反向就行了。

整体穿透

如果整体穿透直接用flags属性设置就行了

1
2
3
4
5
6
7
8
Window {
id:mywin
visible: true
width: 600; height: 400
color: "#33000000"
title: qsTr("主页面")
flags: Qt.WindowTransparentForInput | Qt.FramelessWindowHint|Qt.Window|Qt.WindowStaysOnTopHint
}

主要是这个属性Qt.WindowTransparentForInput

窗口拖动

DraggableBar.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15

Rectangle {
id: draggableBar
width: parent.width
height: parent.height
color: "transparent"

MouseArea {
id: dragMouseArea
anchors.fill: parent
cursorShape: Qt.SizeAllCursor

onPressed: {
var m_win = draggableBar.Window.window
if (m_win) {
m_win.startSystemMove() // 使用系统 API 开始拖动窗口
}
}
}
}

拖动区域直接添加

1
2
3
4
import "components" as Components
Components.DraggableBar {
anchors.fill: parent
}

窗口拖动/菜单/托盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15 as Controls
import Qt.labs.platform 1.1


//包含系统托盘的导包
Window {
id: mainWindow
visible: true

width: 200
height: 200
title: qsTr("tiny monitor")
//无边框的window flags
flags: Qt.FramelessWindowHint
| Qt.WindowSystemMenuHint
| Qt.WindowStaysOnTopHint
| Qt.X11BypassWindowManagerHint
//灰色0.9透明度
color: Qt.rgba(0.5, 0.5, 0.5, 0.9)

//鼠标可控制区域
MouseArea {
property point clickPos: "0,0"
anchors.fill: parent
onPressed: {
mainWindow.requestActivate()
clickPos = Qt.point(mouseX, mouseY)
}

onPositionChanged: {
//鼠标偏移量
var delta = Qt.point(mouse.x - clickPos.x, mouse.y - clickPos.y)
var tempx = mainWindow.x + delta.x;
var tempy = mainWindow.y + delta.y

if(tempx<0){
tempx = 0;
}

if(tempx>Screen.desktopAvailableWidth -mainWindow.width){
tempx = Screen.desktopAvailableWidth -mainWindow.width;
}

if(tempy<0){
tempy = 0;
}

if(tempy>Screen.desktopAvailableHeight -mainWindow.height){
tempy = Screen.desktopAvailableHeight -mainWindow.height;
}

mainWindow.x = tempx
mainWindow.y = tempy
}
//添加右键菜单
acceptedButtons: Qt.LeftButton | Qt.RightButton // 激活右键(别落下这个)
onClicked: {
if (mouse.button === Qt.RightButton) {
// 右键菜单
contentMenu.popup()
}
}
}
//不是托盘的菜单类
Controls.Menu {
id: contentMenu
// 右键菜单
Controls.MenuItem {
id:hideItem
text: qsTr("隐藏")
onTriggered: {
if(trayIcon==null){
console.log("系统托盘不存在");
contentMenu.removeItem(hideItem);
return;
}else{
if(trayIcon.available){
console.log("系统托盘存在");
}else{
console.log("系统托盘不存在");
contentMenu.removeItem(hideItem)
}
}
mainWindow.hide()
}
}
Controls.MenuItem {
text: qsTr("退出")
onTriggered: Qt.quit()
}
}

//使用系统托盘的菜单组件
Menu {
id: systemTrayMenu
// 右键菜单
MenuItem {
text: qsTr("隐藏")
shortcut: "Ctrl+z"
onTriggered: mainWindow.hide()
}
MenuItem {
text: qsTr("退出")
onTriggered: Qt.quit()
}
}
//系统托盘
SystemTrayIcon {
id:trayIcon
visible: true
icon.source: "qrc:/imgs/logo.png"
tooltip: "客户端"
onActivated: {
mainWindow.show()
mainWindow.raise()
mainWindow.requestActivate()
}
menu: systemTrayMenu
}
}

多窗口

在使用 Qt Quick 和 PySide2 创建多个窗口时,可以通过 QML 和 Python 代码相结合的方式来实现。

以下是一个详细的示例,演示如何在 Python 代码中创建多个窗口。

安装 PySide2

首先确保你已经安装了 PySide2。

你可以使用 pip 来安装:

1
pip install PySide2

创建 QML 文件

main.qml

这是主窗口的 QML 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.12

ApplicationWindow {
visible: true
width: 640
height: 480
title: "主窗口"

RowLayout {
Button {
text: "打开次窗口"
onClicked: windowManager.openSecondaryWindow()
}

Button {
text: "打开第三个窗口"

onClicked: windowManager.openTertiaryWindow()
}
}
}

secondary.qml

这是次窗口的 QML 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import QtQuick.Layouts 1.12

ApplicationWindow {
id: secondaryWindow
visible: true
width: 400
height: 300
title: "次窗口"

Button {
id:rootView
text: "关闭"
anchors.centerIn: parent
onClicked: secondaryWindow.close()
}
Component.onCompleted: {
console.log("onCompleted");
}

onClosing: {
console.log("窗口即将关闭")
windowManager.onWindowClose(secondaryWindow);
}

// 监听窗口销毁事件
Component.onDestruction: {
console.log("窗口被销毁")
}
}

tertiary.qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
id: tertiaryWindow
visible: true
width: 300
height: 200
title: "第三个窗口"

Button {
text: "关闭"
anchors.centerIn: parent
onClicked: tertiaryWindow.close()
}

onClosing: {
console.log("窗口即将关闭")
windowManager.onWindowClose(tertiaryWindow);
}

// 监听窗口销毁事件
Component.onDestruction: {
console.log("窗口被销毁")
}
}

Python管理窗口

window_manager.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from PySide2.QtCore import QObject, Slot, QUrl
from PySide2.QtQuick import QQuickWindow

class WindowManager(QObject):
def __init__(self, engine, parent=None):
super().__init__(parent)
self.engine = engine
self.windows = []
self.engine.objectCreated.connect(self.onObjectCreated)

@Slot()
def openSecondaryWindow(self):
win_name = "secondaryWindow"
if any(window.objectName() == win_name for window in self.windows):
return

self.engine.load(QUrl("qrc:/win/secondary.qml"))
root_objects = self.engine.rootObjects()
if root_objects:
window = root_objects[-1]
window.setObjectName(win_name)
window.destroyed.connect(self.onWindowDestroyed)
self.windows.append(window)

@Slot()
def openTertiaryWindow(self):
win_name = "tertiaryWindow"
if any(window.objectName() == win_name for window in self.windows):
return

self.engine.load(QUrl("qrc:/win/tertiary.qml"))

root_objects = self.engine.rootObjects()
if root_objects:
window = root_objects[-1]
window.setObjectName(win_name)
window.destroyed.connect(self.onWindowDestroyed)
self.windows.append(window)

@Slot(QQuickWindow)
def onWindowClose(self, window):
if window in self.windows:
print(f"窗口关闭: {window.objectName()}")
window.deleteLater()

@Slot(QQuickWindow)
def onWindowDestroyed(self, window):
if window in self.windows:
print(f"窗口销毁: {window.objectName()}")
self.windows.remove(window)
root_objects = self.engine.rootObjects()
print(len(root_objects))

@Slot(QObject, QUrl)
def onObjectCreated(self, obj, url):
if obj:
print(f"窗口创建: {url}")

主类

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import sys
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtCore import QUrl
from window_manager import WindowManager
import resource_qrc

if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()

# 创建 WindowManager 实例
window_manager = WindowManager(engine)

# 将 WindowManager 实例注入到 QML 上下文中
engine.rootContext().setContextProperty("windowManager", window_manager)

# 加载主窗口
engine.load(QUrl("qrc:/win/main.qml"))

if not engine.rootObjects():
sys.exit(-1)

sys.exit(app.exec_())

窗口的销毁机制

只有一个顶层窗口的情况:

顶层窗口调用close,本窗口销毁并销毁所有子部件;非顶层窗口调用close,窗口只是隐藏,并不销毁。

对于有多个顶层窗口的程序:

情况并不是如此,由于所有窗口都生存在一个QApplication中,只有最后一个窗口关闭close()之后 程序才会调用主窗口的析构函数来销毁窗口。

Qt Quick中设置窗口关闭自动销毁的属性是无效的,窗口关闭并不会销毁,需要调用deleteLater()进行销毁。

获取窗口及窗口句柄

1
2
3
4
root_window = engine.rootObjects()[0]
# 获取窗口句柄
hwnd = root_window.winId()
hwnd_int = int(hwnd)