本文来源于项目预研,根据项目需求,需要新的客户端软件且使用qml实现。之前没有使用过qml,也是通过这个demo进行学习。以下时项目需求:
1.界面分模块,可调整模块大小
2.模块可通过拖拽为独立窗口
最终效果如下图所示:
首先是分模块可调整大小,可以使用Qt已经封装好的组件SplitView,使用的版本是QtQuick.Controls 2.14,效果上来说和QSplitter相同。代码如下:
SplitView {
id: splitView
anchors.fill: parent
anchors.margins: 4
orientation: Qt.Vertical
handle: splitHandle
SplitView {
SplitView.fillHeight: true
SplitView.minimumHeight: 200
orientation: Qt.Horizontal
handle: splitHandle
FootageView {
id: footageView
SplitView.preferredWidth: 200
SplitView.maximumWidth: 250
SplitView.minimumWidth: 100
}
PlayerView {
id: playerView
SplitView.fillWidth: true
SplitView.minimumWidth: 100
}
ParamView {
id: paramView
SplitView.preferredWidth: 200
SplitView.fillWidth: true
//SplitView.maximumWidth: 250
SplitView.minimumWidth: 100
}
}
TimelineView {
id: timelineView
SplitView.preferredHeight: 160
SplitView.minimumHeight: 100
}
}
Component {
id: splitHandle
Rectangle {
implicitWidth: 4
implicitHeight: 4
opacity: 0
}
}
可以看到有两SplitView部分组成,上半部分的横向分割,和整体的竖向风格,横向和竖向的控制是通过属性orientation实现(Qt.Horizontal为横向、Qt.Vertical为竖向)。同时布局里面还放置了演示用的四个组件(FootageView、PlayerView、ParamView、TimelineView),其布局的策略如下:
SplitView.fillHeight: 高度自动拉伸
SplitView.fillWidth: 宽度自动拉伸
SplitView.minimumHeight: 最小高度
SplitView.maximumWidth: 最大宽度
SplitView.minimumWidth: 最小宽度
SplitView.preferredHeight: 默认高度
SplitView.preferredWidth: 默认宽度
默认是有用于调整大小的控制条,需要对其进行隐藏,也就是用组件设置handle属性,此处设置了透明的Rectangle达到隐藏的效果,如下所示:
Component {
id: splitHandle
Rectangle {
implicitWidth: 4
implicitHeight: 4
opacity: 0
}
}
这里需要解决两个问题,一个就是模块拖拽顶部特定区域对外发送信号,另一个就是独立窗口展示。拖拽的交互其实就是鼠标按下和鼠标松开的联动,这里可以使用MouseArea实现。独立窗口展示意味着在接收到拖拽信号后,在悬浮窗口中展示模块内容,同时隐藏主窗体中的模块。
此处有4个模块,目前只是背景颜色和文字不同,因此应该有共同的原型,用于提供拖拽信号,代码如下:
Item {
property var backgroundColor: "#1b1b1b"
property var customText: "View"
signal dragFinished(var postion)
// 背景
Rectangle {
anchors.fill: parent
color: backgroundColor
radius: 8
Text {
color: "#ffffffff"
text: customText
font.bold: true
anchors.centerIn: parent
}
}
// 工具栏背景
Rectangle {
width: parent.width
height: toolbar.radius
anchors.bottom: toolbar.bottom
color: "#2d2d2d"
}
// 工具栏
Rectangle {
id: toolbar
width: parent.width
height: 30
color: "#2d2d2d"
radius: 8
MouseArea {
id: dragArea
anchors.fill: parent;
property int pressedX: -1
property int pressedY: -1
onPressed: (mouse)=>{
console.log("Pressed", mouse.x, mouse.y)
pressedX = mouse.x
pressedY = mouse.y
}
onReleased: (mouse)=>{
console.log("Released", mouse.x, mouse.y)
if(pressedX >= 0 && pressedY >= 0){
let distance = Math.pow(pressedX - mouse.x, 2) + Math.pow(pressedY - mouse.y, 2);
distance = Math.pow(distance, 0.5)
if(distance > 10){
dragFinished(mapToGlobal(mouse.x, mouse.y))
}
}
}
}
}
}
通过代码可以看到组件里面有3个Rectangle,一个是主背景,一个是工具栏,还有一个是为了制造出只有上半部分圆角的效果,做的一个底部直角,因为radius属性没法单独设置。在工具栏处锚定了一个MouseArea,用于实现拖拽信号,在鼠标按下时记录初始位置,松开是计算当前位置与初始位置距离,大于10则视为正常操作,将当前坐标转换为屏幕坐标后发送出去。
独立窗口为主窗体中的一个默认隐藏的悬浮窗体,当模块方式拖拽动作之后触发悬浮窗体显示,动态加载对应模块的.qml文件,同时隐藏主窗体中的模块,代码如下:
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14
Window {
visible: true
width: 800
height: 480
title: qsTr("Hello World")
Component {
id: splitHandle
Rectangle {
implicitWidth: 4
implicitHeight: 4
opacity: 0
}
}
SplitView {
id: splitView
anchors.fill: parent
anchors.margins: 4
orientation: Qt.Vertical
handle: splitHandle
SplitView {
SplitView.fillHeight: true
SplitView.minimumHeight: 200
orientation: Qt.Horizontal
handle: splitHandle
FootageView {
id: footageView
SplitView.preferredWidth: 200
SplitView.maximumWidth: 250
SplitView.minimumWidth: 100
}
PlayerView {
id: playerView
SplitView.fillWidth: true
SplitView.minimumWidth: 100
}
ParamView {
id: paramView
SplitView.preferredWidth: 200
SplitView.fillWidth: true
//SplitView.maximumWidth: 250
SplitView.minimumWidth: 100
}
}
TimelineView {
id: timelineView
SplitView.preferredHeight: 160
SplitView.minimumHeight: 100
}
}
Window {
id: floatWindow
visible: false
width: 330
height: 380
property var source: ""
property var sourceLoader: loader
Loader {
id: loader
anchors.fill: parent
}
}
Connections {
target: floatWindow
onVisibleChanged: {
if(floatWindow.visible){
if(floatWindow.source != ""){
floatWindow.sourceLoader.source = floatWindow.source
}
}
else{
footageView.visible = true;
playerView.visible = true;
paramView.visible = true;
timelineView.visible = true;
}
}
}
Connections {
target: footageView
onDragFinished : (postion)=>{
diplayFloat(0, postion)
}
}
Connections {
target: playerView
onDragFinished : (postion)=>{
diplayFloat(1, postion)
}
}
Connections {
target: paramView
onDragFinished : (postion)=>{
diplayFloat(2, postion)
}
}
Connections {
target: timelineView
onDragFinished : (postion)=>{
diplayFloat(3, postion)
}
}
function diplayFloat(viewType, postion){
if(floatWindow.visible){
return false
}
let qmlSource
switch(viewType){
case 0:
qmlSource = "FootageView.qml"
footageView.visible = false;
break
case 1:
qmlSource = "PlayerView.qml"
playerView.visible = false;
break
case 2:
qmlSource = "ParamView.qml"
paramView.visible = false;
break
case 3:
qmlSource = "TimelineView.qml"
timelineView.visible = false;
break
}
floatWindow.source = qmlSource
if(postion){
floatWindow.x = postion.x - floatWindow.width / 2
floatWindow.y = postion.y
}
floatWindow.show()
return true
}
}
动态加载使用到Loader ,在悬浮窗体显示后将对应资源显示的路径替换Loader 的source属性