Qml组件化编程2-可拖动组件和定制窗体



  • 简介

    本文是《Qml组件化编程》系列文章的第二篇,涛哥将教大家,如何在Qml中实现可拖动组件,通过拖动

    改变组件的大小和位置;以及实现定制窗体(无边框和标题栏), 并把拖动组件应用在顶层窗体。

    文章主要发布在涛哥的博客知乎专栏-涛哥的Qt进阶之路

    拖动组件

    拖动改变坐标

    拖动改变坐标的原理很简单,鼠标移动的时候改变目标Item的坐标即可。

    说话的功夫,涛哥就造了个轮子出来

    (其实是太常用了,涛哥已经写了很多遍)

    import QtQuick 2.9
    import QtQuick.Controls 2.5
    Item {
       width: 800
       height: 600
    
       Rectangle {
           id: moveItem
    
           //注意拖动目标不要使用锚布局或者Layout,而是使用相对坐标
           x: 100
           y: 100
           width: 300
           height: 200
    
           color: "lightblue"
           MouseArea {
               anchors.fill: parent
               property real lastX: 0
               property real lastY: 0
               onPressed: {
                   //鼠标按下时,记录鼠标初始位置
                   lastX = mouseX
                   lastY = mouseY
               }
               onPositionChanged: {
                   if (pressed) { 
                       //鼠标按住的前提下,坐标改变时,计算偏移量,应用到目标item的坐标上即可
                       moveItem.x += mouseX - lastX
                       moveItem.y += mouseY - lastY
                   }
               }
           }
       }
    }
    

    预览

    上面例子中的MouseArea是拖动区域,Rectangle是要拖动的目标Item。

    为了实现高度的可复用性,涛哥将MouseArea独立封装成一个组件,并提供一个control属性,

    让外部使用组件实例的时候指定要拖动的目标。

    // TMoveArea.qml
    
    import QtQuick 2.9
    
    MouseArea {
        id: root
    
        property real lastX: 0
        property real lastY: 0
        property bool mask: false       //有时候外面需要屏蔽拖动,导出一个mask属性, 默认false。
        property var control: parent   //导出一个control属性,指定要拖动的目标, 默认就用parent好了。注意目标要有x和y属性并且可修改
    
        onPressed: {
            lastX = mouseX;
            lastY = mouseY;
        }
        onContainsMouseChanged: { //修改一下鼠标样式,以示区别
            if (containsMouse) {
                cursorShape = Qt.SizeAllCursor;
            } else {
                cursorShape = Qt.ArrowCursor;
            }
        }
        onPositionChanged: {
            if (!mask && pressed && control)
            {
                control.x +=mouseX - lastX
                control.y +=mouseY - lastY
            }
        }
    }
    
    

    TMoveArea组件的用法

    Item {
        anchors.fill: parent
    
        Rectangle {
            x: 100
            y: 200
            width: 400
            height: 300
            color: "darkred"
            //实例化一个MoveArea
            TMoveArea {
                //指定control为parent。 其实默认就是parent,写出来示意一下
                control: parent
                anchors.fill: parent
            }
        }
    }
    
    

    一般来说,将

      property var control: parent
    

    中的var换成确切的类型比如Item会更好一些,Qml底层引擎处理var会慢一些,但是这样就限制了

    目标必须是Item或者其子类。var是把双刃剑,有利有弊。涛哥后面要拖动的目标还包括QQuickView

    这种类型,所以这里用var就好了。

    拖动改变大小

    拖动改变大小,原理参考下面这张示意图:

    预览

    就是在要拖动的目标Item的8个位置分别放一个拖动组件,并在拖动时计算相应的坐标和大小变化即可。

    涛哥先是把TMoveArea改造成了TDragRect

    // TDragRect.qml
    import QtQuick 2.9
    import QtQuick.Controls 2.0
    Item {
        id: root
        property alias containsMouse: mouseArea.containsMouse
        signal posChange(int xOffset, int yOffset)
        implicitWidth: 12   //这里隐式的宽为12
        implicitHeight: 12  //这里隐式的高为12
        property int posType: Qt.ArrowCursor
    
        //5.10之前, qml是不能定义枚举的,用只读的int属性代替一下。
        readonly property int posLeftTop: Qt.SizeFDiagCursor
        readonly property int posLeft: Qt.SizeHorCursor
        readonly property int posLeftBottom: Qt.SizeBDiagCursor
        readonly property int posTop: Qt.SizeVerCursor
        readonly property int posBottom: Qt.SizeVerCursor
        readonly property int posRightTop: Qt.SizeBDiagCursor
        readonly property int posRight: Qt.SizeHorCursor
        readonly property int posRightBottom: Qt.SizeFDiagCursor
        MouseArea {
            id: mouseArea
            anchors.fill: parent
            hoverEnabled: true
            property int lastX: 0
            property int lastY: 0
            onContainsMouseChanged: {
                if (containsMouse) {
                    cursorShape = posType;
                } else {
                    cursorShape = Qt.ArrowCursor;
                }
            }
            onPressedChanged: {
                if (containsPress) {
                    lastX = mouseX;
                    lastY = mouseY;
                }
            }
            onPositionChanged: {
                if (pressed) {
                    posChange(mouseX - lastX, mouseY - lastY)
                }
            }
        }
    }
    
    

    就是把前面的鼠标拖动时的处理逻辑,换成了带参数的信号发送出去,由外面决定怎么用这两个坐标

    同时也定义了一组枚举,用来表示拖动区域的位置。位置不同,则鼠标样式不同。

    之后涛哥写了一个叫TResizeBorder的组件,里面实例化了8个TDragRect组件,分别放在前面示意图

    所示的位置,并实现了不同的处理逻辑。

    (后来涛哥把上下左右四个中心点换成了四个边)

    // TResizeBorder.qml
    import QtQuick 2.7
    
    Rectangle {
        id: root
        color: "transparent"
        border.width: 4
        border.color: "black"
        width: parent.width
        height: parent.height
        property var control: parent
        TDragRect {
            posType: posLeftTop
            onPosChange: {
                //不要简化这个判断条件,至少让以后维护的人能看懂。化简过后我自己都看不懂了。
                if (control.x + xOffset < control.x + control.width)
                    control.x += xOffset;
                if (control.y + yOffset < control.y + control.height)
                    control.y += yOffset;
                if (control.width - xOffset > 0)
                    control.width-= xOffset;
                if (control.height -yOffset > 0)
                    control.height -= yOffset;
            }
        }
        TDragRect {
            posType: posMidTop
            x: (parent.width - width) / 2
            onPosChange: {
                if (control.y + yOffset < control.y + control.height)
                    control.y += yOffset;
                if (control.height - yOffset > 0)
                    control.height -= yOffset;
            }
        }
        TDragRect {
            posType: posRightTop
            x: parent.width - width
            onPosChange: {
                //向左拖动时,xOffset为负数
                if (control.width + xOffset > 0)
                    control.width += xOffset;
                if (control.height - yOffset > 0)
                    control.height -= yOffset;
                if (control.y + yOffset < control.y + control.height)
                    control.y += yOffset;
            }
        }
        TDragRect {
            posType: posLeftMid
            y: (parent.height - height) / 2
            onPosChange: {
                if (control.x + xOffset < control.x + control.width)
                    control.x += xOffset;
                if (control.width - xOffset > 0)
                    control.width-= xOffset;
            }
        }
        TDragRect {
            posType: posRightMid
            x: parent.width - width
            y: (parent.height - height) / 2
            onPosChange: {
                if (control.width + xOffset > 0)
                    control.width += xOffset;
            }
        }
        TDragRect {
            posType: posLeftBottom
            y: parent.height - height
            onPosChange: {
                if (control.x + xOffset < control.x + control.width)
                    control.x += xOffset;
                if (control.width - xOffset > 0)
                    control.width-= xOffset;
                if (control.height + yOffset > 0)
                    control.height += yOffset;
            }
        }
        TDragRect {
            posType: posMidBottom
            x: (parent.width - width) / 2
            y: parent.height - height
            onPosChange: {
                if (control.height + yOffset > 0)
                    control.height += yOffset;
            }
        }
        TDragRect {
            posType: posRightBottom
            x: parent.width - width
            y: parent.height - height
            onPosChange: {
                if (control.width + xOffset > 0)
                    control.width += xOffset;
                if (control.height + yOffset > 0)
                    control.height += yOffset;
            }
        }
    }
    
    

    注意组件的顶层,使用的是透明的Rectangle,这样做的目的是,外面可以给这个组件设置

    不同的颜色、边框等。无论哪种UI框架,透明处理都是需要一定的性能消耗的,所以在不需要显示

    出来的情况下,组件顶层最好还是用Item替代。

    融合

    我们来实例化一个能拖动改变大小和位置的Item

    Item {
        width: 800
        height: 600
        Rectangle {
            x: 300
            y: 200
            width: 120
            height: 80
            color: "darkred"
            TMoveArea {
                anchors.fill: parent
                control: parent     //默认就是parent,可以不写。这里写出来示意一下。
            }
            TResizeBorder {
                control: parent     //默认就是parent,可以不写。这里写出来示意一下。
                anchors.fill: parent
    
            }
        }
    }
    

    预览

    用起来还是挺方便的,直接在目标Item里面实例化一个TResizeBorder组件,指定control即可。

    这里同时实例化了TMoveArea和TResizeBorder两个组件,作为目标Item的child,就把两种功能 融合起来了。

    注意前后顺序,如果反过来写则TMoveArea会把ResizeBorder遮盖住。(Qml是有z轴的,以后的文章涛哥再讲)

    多级组件和Qml应用的框架结构

    回过头来看一下,先是封装了两个组件:TMoveArea和TDragRect,之后又封装了一个组件:TResizeBorder,

    而这个TResizeBorder里面使用了多个TDragRect组件,显然是有层级结构在里面的。

    涛哥把TMoveArea和TDragRect这样的最基础的组件叫做一级组件,那么TResizeBorder就是一个二级组件。

    涛哥大量的实战经验后,总结出了这样一种Qml应用框架结构:

    一级和二级组件可以单独做成一个插件(或者叫Qml通用库)。
    
    实际的Qml项目,在这些基础上,做一些功能性或者业务性的组件,即三级组件。
    
    由这些三级组件组成一堆的页面(Page)。
    
    最终的main.qml中,只剩下Page的布局。
    

    示意图如下:

    预览

    自定义窗口

    自定义窗口,这里以QQuickView

    无边框

    去掉边框,需要在C++中设置flag为Qt::FramelessWindowHint

    同时我们注册view到qml上下文环境,给后面的功能来使用。

        ...
        QQuickView view;
        view.setFlag(Qt::FramelessWindowHint);
        view.rootContext()->setContextProperty("view", &view);
        ...
    

    可拖动窗口

    将我们前面做的两种拖动框放在main.qml中,填满顶层Item,并指定control为view。

    //main.qml
    
    import QtQuick 2.0
    
    #import TaoQuick 1.0      //这里是做成插件的情况下,引用了插件
    #import "qrc:/Tao/Qml"    //没有做插件的情况下,只要引用qml文件的资源路径即可
    
    Item {
        //标题栏
        TitlePage {
            id: titleRect
            width: root.width
            height: 60
            ...
            //标题栏区域,实例化一个可以拖动位置的组件
            TMoveArea {
                height: parent.height
                anchors {
                    left: parent.left
                    right: parent.right
                    rightMargin: 170 //留一点右边距,给最小化、最大化、关闭等按钮用
                }
                //指定拖动目标为view
                control: view
            }
            ...
        }
        //实例化一个拖动改大小的组件
        TResizeBorder {
            //指定拖动目标为view
            control: view
            anchors.fill: parent
        }
        ...
    }
    
    

    自定义标题栏

    标题栏的关键就是实现右侧的三个按钮,如果你看了《Qml组件化编程1-按钮的定制与封装》

    这都没有什么难度了。涛哥这里用图片按钮的方式实现。

    注意最大化按钮在最大化状态下变成标准化按钮。

    最小化:view.showMinimized()

    最大化:view.showMaximized()

    标准化:view.showNormal()

    关闭: view.close()

    这里给出关键代码

    Item{
        ...
        property bool isMaxed: false
        Row {
            id: controlButtons
            height: 20
            anchors.verticalCenter: parent.verticalCenter
            anchors.right: parent.right
            anchors.rightMargin: 12
            spacing: 10
            TImageBtn {
                width: 20
                height: 20
                imageUrl: containsMouse ? "qrc:/Image/Window/minimal_white.png" : "qrc:/Image/Window/minimal_gray.png"
                onClicked: {
                    view.showMinimized()
                }
            }
            TImageBtn {
                width: 20
                height: 20
                visible: !isMaxed
                imageUrl: containsMouse ? "qrc:/Image/Window/max_white.png" : "qrc:/Image/Window/max_gray.png"
                onClicked: {
                    view.showMaximized()
                    isMaxed = true
                }
            }
            TImageBtn {
                width: 20
                height: 20
                visible: isMaxed
                imageUrl: containsMouse ? "qrc:/Image/Window/normal_white.png" : "qrc:/Image/Window/normal_gray.png"
                onClicked: {
                    view.showNormal()
                    isMaxed = false
                }
            }
            TImageBtn {
                width: 20
                height: 20
                imageUrl: containsMouse ? "qrc:/Image/Window/close_white.png" : "qrc:/Image/Window/close_gray.png"
                onClicked: {
                    view.close()
                }
            }
        }
    }
    

    效果

    最后,我们来看一下效果吧

    预览

    转载声明

    文章出自涛哥的博客 -- 点击这里查看涛哥的博客
    文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作 © 涛哥

    联系方式


    作者 涛哥
    开发理念 弘扬鲁班文化,传承工匠精神
    博客 https://jaredtao.github.io
    知乎 https://www.zhihu.com/people/wentao-jia
    邮箱 jared2020@163.com
    微信 xsd2410421
    QQ 759378563

    请放心联系我,乐于提供咨询服务,也可洽谈商务合作相关事宜。

    打赏

    weixin
    zhifubao


    如果觉得涛哥写的还不错,还请为涛哥打个赏,您的赞赏是涛哥持续创作的源泉。



Log in to reply
 

走马观花

最近的回复

  • 这张图或许阐述了这个问题。
    有效打造个人品牌.png

    其实对于我们职场人员,尤其是程序员,都有很强的作用。我们虽然产品意识比较弱,但是我们对产品的敏感性强,尤其是我们有制作过产品的经验,这反而是一个优势。

    read more
  • @jiangcaiyang 给你一个大大的赞!
    论坛最近打算再次启用iframely服务了。试试看!

    https://community.nodebb.org/topic/4401

    read more

关注我们

微博
QQ群