• 2130阅读
  • 3回复

Qml组件化编程2-可拖动组件和定制窗体 [复制链接]

上一主题 下一主题
离线dd759378563
 

只看楼主 倒序阅读 楼主  发表于: 2019-05-11

简介

本文是《Qml组件化编程》系列文章的第二篇,涛哥将教大家,如何在Qml中实现可拖动组件,通过拖动
改变组件的大小和位置;以及实现定制窗体(无边框和标题栏), 并把拖动组件应用在顶层窗体。
文章主要发布在涛哥的博客知乎专栏-涛哥的Qt进阶之路

拖动组件



拖动改变坐标

拖动改变坐标的原理很简单,鼠标移动的时候改变目标Item的坐标即可。
说话的功夫,涛哥就造了个轮子出来
(其实是太常用了,涛哥已经写了很多遍)
  1. import QtQuick 2.9
  2. import QtQuick.Controls 2.5
  3. Item {
  4.     width: 800
  5.     height: 600
  6.     Rectangle {
  7.         id: moveItem
  8.         //注意拖动目标不要使用锚布局或者Layout,而是使用相对坐标
  9.         x: 100
  10.         y: 100
  11.         width: 300
  12.         height: 200
  13.         color: "lightblue"
  14.         MouseArea {
  15.             anchors.fill: parent
  16.             property real lastX: 0
  17.             property real lastY: 0
  18.             onPressed: {
  19.                 //鼠标按下时,记录鼠标初始位置
  20.                 lastX = mouseX
  21.                 lastY = mouseY
  22.             }
  23.             onPositionChanged: {
  24.                 if (pressed) {
  25.                     //鼠标按住的前提下,坐标改变时,计算偏移量,应用到目标item的坐标上即可
  26.                     moveItem.x += mouseX - lastX
  27.                     moveItem.y += mouseY - lastY
  28.                 }
  29.             }
  30.         }
  31.     }
  32. }






上面例子中的MouseArea是拖动区域,Rectangle是要拖动的目标Item。
为了实现高度的可复用性,涛哥将MouseArea独立封装成一个组件,并提供一个control属性,
让外部使用组件实例的时候指定要拖动的目标。
  1. // TMoveArea.qml
  2. import QtQuick 2.9
  3. MouseArea {
  4.     id: root
  5.     property real lastX: 0
  6.     property real lastY: 0
  7.     property bool mask: false       //有时候外面需要屏蔽拖动,导出一个mask属性, 默认false。
  8.     property var control: parent   //导出一个control属性,指定要拖动的目标, 默认就用parent好了。注意目标要有x和y属性并且可修改
  9.     onPressed: {
  10.         lastX = mouseX;
  11.         lastY = mouseY;
  12.     }
  13.     onContainsMouseChanged: { //修改一下鼠标样式,以示区别
  14.         if (containsMouse) {
  15.             cursorShape = Qt.SizeAllCursor;
  16.         } else {
  17.             cursorShape = Qt.ArrowCursor;
  18.         }
  19.     }
  20.     onPositionChanged: {
  21.         if (!mask && pressed && control)
  22.         {
  23.             control.x +=mouseX - lastX
  24.             control.y +=mouseY - lastY
  25.         }
  26.     }
  27. }



TMoveArea组件的用法
  1. Item {
  2.     anchors.fill: parent
  3.     Rectangle {
  4.         x: 100
  5.         y: 200
  6.         width: 400
  7.         height: 300
  8.         color: "darkred"
  9.         //实例化一个MoveArea
  10.         TMoveArea {
  11.             //指定control为parent。 其实默认就是parent,写出来示意一下
  12.             control: parent
  13.             anchors.fill: parent
  14.         }
  15.     }
  16. }



一般来说,将
  property var control: parent中的var换成确切的类型比如Item会更好一些,Qml底层引擎处理var会慢一些,但是这样就限制了
目标必须是Item或者其子类。var是把双刃剑,有利有弊。涛哥后面要拖动的目标还包括QQuickView
这种类型,所以这里用var就好了。


拖动改变大小

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

就是在要拖动的目标Item的8个位置分别放一个拖动组件,并在拖动时计算相应的坐标和大小变化即可。
涛哥先是把TMoveArea改造成了TDragRect
  1. // TDragRect.qml
  2. import QtQuick 2.9
  3. import QtQuick.Controls 2.0
  4. Item {
  5.     id: root
  6.     property alias containsMouse: mouseArea.containsMouse
  7.     signal posChange(int xOffset, int yOffset)
  8.     implicitWidth: 12   //这里隐式的宽为12
  9.     implicitHeight: 12  //这里隐式的高为12
  10.     property int posType: Qt.ArrowCursor
  11.     //5.10之前, qml是不能定义枚举的,用只读的int属性代替一下。
  12.     readonly property int posLeftTop: Qt.SizeFDiagCursor
  13.     readonly property int posLeft: Qt.SizeHorCursor
  14.     readonly property int posLeftBottom: Qt.SizeBDiagCursor
  15.     readonly property int posTop: Qt.SizeVerCursor
  16.     readonly property int posBottom: Qt.SizeVerCursor
  17.     readonly property int posRightTop: Qt.SizeBDiagCursor
  18.     readonly property int posRight: Qt.SizeHorCursor
  19.     readonly property int posRightBottom: Qt.SizeFDiagCursor
  20.     MouseArea {
  21.         id: mouseArea
  22.         anchors.fill: parent
  23.         hoverEnabled: true
  24.         property int lastX: 0
  25.         property int lastY: 0
  26.         onContainsMouseChanged: {
  27.             if (containsMouse) {
  28.                 cursorShape = posType;
  29.             } else {
  30.                 cursorShape = Qt.ArrowCursor;
  31.             }
  32.         }
  33.         onPressedChanged: {
  34.             if (containsPress) {
  35.                 lastX = mouseX;
  36.                 lastY = mouseY;
  37.             }
  38.         }
  39.         onPositionChanged: {
  40.             if (pressed) {
  41.                 posChange(mouseX - lastX, mouseY - lastY)
  42.             }
  43.         }
  44.     }
  45. }



就是把前面的鼠标拖动时的处理逻辑,换成了带参数的信号发送出去,由外面决定怎么用这两个坐标
同时也定义了一组枚举,用来表示拖动区域的位置。位置不同,则鼠标样式不同。
之后涛哥写了一个叫TResizeBorder的组件,里面实例化了8个TDragRect组件,分别放在前面示意图
所示的位置,并实现了不同的处理逻辑。
(后来涛哥把上下左右四个中心点换成了四个边)
  1. // TResizeBorder.qml
  2. import QtQuick 2.7
  3. Rectangle {
  4.     id: root
  5.     color: "transparent"
  6.     border.width: 4
  7.     border.color: "black"
  8.     width: parent.width
  9.     height: parent.height
  10.     property var control: parent
  11.     TDragRect {
  12.         posType: posLeftTop
  13.         onPosChange: {
  14.             //不要简化这个判断条件,至少让以后维护的人能看懂。化简过后我自己都看不懂了。
  15.             if (control.x + xOffset < control.x + control.width)
  16.                 control.x += xOffset;
  17.             if (control.y + yOffset < control.y + control.height)
  18.                 control.y += yOffset;
  19.             if (control.width - xOffset > 0)
  20.                 control.width-= xOffset;
  21.             if (control.height -yOffset > 0)
  22.                 control.height -= yOffset;
  23.         }
  24.     }
  25.     TDragRect {
  26.         posType: posMidTop
  27.         x: (parent.width - width) / 2
  28.         onPosChange: {
  29.             if (control.y + yOffset < control.y + control.height)
  30.                 control.y += yOffset;
  31.             if (control.height - yOffset > 0)
  32.                 control.height -= yOffset;
  33.         }
  34.     }
  35.     TDragRect {
  36.         posType: posRightTop
  37.         x: parent.width - width
  38.         onPosChange: {
  39.             //向左拖动时,xOffset为负数
  40.             if (control.width + xOffset > 0)
  41.                 control.width += xOffset;
  42.             if (control.height - yOffset > 0)
  43.                 control.height -= yOffset;
  44.             if (control.y + yOffset < control.y + control.height)
  45.                 control.y += yOffset;
  46.         }
  47.     }
  48.     TDragRect {
  49.         posType: posLeftMid
  50.         y: (parent.height - height) / 2
  51.         onPosChange: {
  52.             if (control.x + xOffset < control.x + control.width)
  53.                 control.x += xOffset;
  54.             if (control.width - xOffset > 0)
  55.                 control.width-= xOffset;
  56.         }
  57.     }
  58.     TDragRect {
  59.         posType: posRightMid
  60.         x: parent.width - width
  61.         y: (parent.height - height) / 2
  62.         onPosChange: {
  63.             if (control.width + xOffset > 0)
  64.                 control.width += xOffset;
  65.         }
  66.     }
  67.     TDragRect {
  68.         posType: posLeftBottom
  69.         y: parent.height - height
  70.         onPosChange: {
  71.             if (control.x + xOffset < control.x + control.width)
  72.                 control.x += xOffset;
  73.             if (control.width - xOffset > 0)
  74.                 control.width-= xOffset;
  75.             if (control.height + yOffset > 0)
  76.                 control.height += yOffset;
  77.         }
  78.     }
  79.     TDragRect {
  80.         posType: posMidBottom
  81.         x: (parent.width - width) / 2
  82.         y: parent.height - height
  83.         onPosChange: {
  84.             if (control.height + yOffset > 0)
  85.                 control.height += yOffset;
  86.         }
  87.     }
  88.     TDragRect {
  89.         posType: posRightBottom
  90.         x: parent.width - width
  91.         y: parent.height - height
  92.         onPosChange: {
  93.             if (control.width + xOffset > 0)
  94.                 control.width += xOffset;
  95.             if (control.height + yOffset > 0)
  96.                 control.height += yOffset;
  97.         }
  98.     }
  99. }



注意组件的顶层,使用的是透明的Rectangle,这样做的目的是,外面可以给这个组件设置
不同的颜色、边框等。无论哪种UI框架,透明处理都是需要一定的性能消耗的,所以在不需要显示
出来的情况下,组件顶层最好还是用Item替代。


融合

我们来实例化一个能拖动改变大小和位置的Item
  1. Item {
  2.     width: 800
  3.     height: 600
  4.     Rectangle {
  5.         x: 300
  6.         y: 200
  7.         width: 120
  8.         height: 80
  9.         color: "darkred"
  10.         TMoveArea {
  11.             anchors.fill: parent
  12.             control: parent     //默认就是parent,可以不写。这里写出来示意一下。
  13.         }
  14.         TResizeBorder {
  15.             control: parent     //默认就是parent,可以不写。这里写出来示意一下。
  16.             anchors.fill: parent
  17.         }
  18.     }
  19. }




用起来还是挺方便的,直接在目标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上下文环境,给后面的功能来使用。
  1. ...
  2.     QQuickView view;
  3.     view.setFlag(Qt::FramelessWindowHint);
  4.     view.rootContext()->setContextProperty("view", &view);
  5.     ...




可拖动窗口

将我们前面做的两种拖动框放在main.qml中,填满顶层Item,并指定control为view。
  1. //main.qml
  2. import QtQuick 2.0
  3. #import TaoQuick 1.0      //这里是做成插件的情况下,引用了插件
  4. #import "qrc:/Tao/Qml"    //没有做插件的情况下,只要引用qml文件的资源路径即可
  5. Item {
  6.     //标题栏
  7.     TitlePage {
  8.         id: titleRect
  9.         width: root.width
  10.         height: 60
  11.         ...
  12.         //标题栏区域,实例化一个可以拖动位置的组件
  13.         TMoveArea {
  14.             height: parent.height
  15.             anchors {
  16.                 left: parent.left
  17.                 right: parent.right
  18.                 rightMargin: 170 //留一点右边距,给最小化、最大化、关闭等按钮用
  19.             }
  20.             //指定拖动目标为view
  21.             control: view
  22.         }
  23.         ...
  24.     }
  25.     //实例化一个拖动改大小的组件
  26.     TResizeBorder {
  27.         //指定拖动目标为view
  28.         control: view
  29.         anchors.fill: parent
  30.     }
  31.     ...
  32. }



自定义标题栏

标题栏的关键就是实现右侧的三个按钮,如果你看了《Qml组件化编程1-按钮的定制与封装》
这都没有什么难度了。涛哥这里用图片按钮的方式实现。
注意最大化按钮在最大化状态下变成标准化按钮。
最小化:view.showMinimized()
最大化:view.showMaximized()
标准化:view.showNormal()
关闭: view.close()
这里给出关键代码
  1. Item{
  2.     ...
  3.     property bool isMaxed: false
  4.     Row {
  5.         id: controlButtons
  6.         height: 20
  7.         anchors.verticalCenter: parent.verticalCenter
  8.         anchors.right: parent.right
  9.         anchors.rightMargin: 12
  10.         spacing: 10
  11.         TImageBtn {
  12.             width: 20
  13.             height: 20
  14.             imageUrl: containsMouse ? "qrc:/Image/Window/minimal_white.png" : "qrc:/Image/Window/minimal_gray.png"
  15.             onClicked: {
  16.                 view.showMinimized()
  17.             }
  18.         }
  19.         TImageBtn {
  20.             width: 20
  21.             height: 20
  22.             visible: !isMaxed
  23.             imageUrl: containsMouse ? "qrc:/Image/Window/max_white.png" : "qrc:/Image/Window/max_gray.png"
  24.             onClicked: {
  25.                 view.showMaximized()
  26.                 isMaxed = true
  27.             }
  28.         }
  29.         TImageBtn {
  30.             width: 20
  31.             height: 20
  32.             visible: isMaxed
  33.             imageUrl: containsMouse ? "qrc:/Image/Window/normal_white.png" : "qrc:/Image/Window/normal_gray.png"
  34.             onClicked: {
  35.                 view.showNormal()
  36.                 isMaxed = false
  37.             }
  38.         }
  39.         TImageBtn {
  40.             width: 20
  41.             height: 20
  42.             imageUrl: containsMouse ? "qrc:/Image/Window/close_white.png" : "qrc:/Image/Window/close_gray.png"
  43.             onClicked: {
  44.                 view.close()
  45.             }
  46.         }
  47.     }
  48. }




效果

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



转载声明

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


联系方式

  
作者涛哥
开发理念弘扬鲁班文化,传承工匠精神
博客https://jaredtao.github.io
知乎https://www.zhihu.com/people/wentao-jia
邮箱jared2020@163.com
微信xsd2410421
QQ759378563
  
请放心联系我,乐于提供咨询服务,也可洽谈商务合作相关事宜。


打赏

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



涛哥是个Qml高手,著有《Qml组件化编程》《Qml特效》系列教程,见知乎专栏-Qt进阶之路:https://zhuanlan.zhihu.com/TaoQt
或微信公众号:Qt进阶之路
在线toby520

只看该作者 1楼 发表于: 2019-05-12
      代码格式 需要整理下
QtQML多多指教开发社区 http://qtclub.heilqt.com
将QtCoding进行到底
关注移动互联网,关注金融
开发跨平台客户端,服务于金融行业
专业定制界面
群号:312125701   373955953(qml控件定做)
离线dd759378563

只看该作者 2楼 发表于: 2019-05-18
代码格式已经整理
涛哥是个Qml高手,著有《Qml组件化编程》《Qml特效》系列教程,见知乎专栏-Qt进阶之路:https://zhuanlan.zhihu.com/TaoQt
或微信公众号:Qt进阶之路
离线big_mouse

只看该作者 3楼 发表于: 2020-04-15
快速回复
限100 字节
 
上一个 下一个