QML可拉伸、可拖拽为独立窗口的UI框架

目录

  • 引言
  • 代码实现
    • 窗口分割
    • 拖拽为独立窗口
      • 拖拽信号
      • 独立窗口

引言

本文来源于项目预研,根据项目需求,需要新的客户端软件且使用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属性

你可能感兴趣的:(qml,ui,qt,开发语言)