本文是《Qml組件化編程》系列文章的第二篇,濤哥將教大家,如何在Qml中實現可拖動組件,通過拖動
改變組件的大小和位置;以及實現定製窗體(無邊框和標題欄), 並把拖動組件應用在頂層窗體。
拖動改變坐標的原理很簡單,滑鼠移動的時候改變目標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軸的,以後的文章濤哥再講)
回過頭來看一下,先是封裝了兩個組件: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 國際許可協議進行許可, 轉載請註明出處, 謝謝合作 ? 濤哥
如果覺得濤哥寫的還不錯,還請為濤哥打個賞,您的讚賞是濤哥持續創作的源泉。
推薦閱讀: