簡介

本文是《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軸的,以後的文章濤哥再講)

多級組件和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 國際許可協議進行許可, 轉載請註明出處, 謝謝合作 ? 濤哥

如果覺得濤哥寫的還不錯,還請為濤哥打個賞,您的讚賞是濤哥持續創作的源泉。

推薦閱讀:

相关文章