QtQuick实现无边框窗口的拉伸,拖拽和自定义标题栏

由于Qt的原生窗口带有的标题栏无法定制,然而通常情况下我们需要自定义窗体上的关闭、最小化等按钮、背景,甚至需要不需要标题栏。在QtQuick实现去除标题栏,也即无边框很简单,只需要在Qml-Window中设置:

flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowMinimizeButtonHint

但是这个时候的窗体不能拖拽,也不能在窗体的边缘进行拉伸。所以需要实现窗体的边缘事件进行拉伸,同时在这里也实现了一个带有最小化,最大化和关闭的自定义标题栏

  • 首先看自定义标题栏代码TitleBar.qml
import QtQuick 2.7
import QtQuick.Controls 2.13
import QtQuick.Templates 2.12 as T
import QtQuick.Window 2.2

Rectangle{
    property bool  isMaximized: false
    Connections{
        target: mainWindow
        onVisibilityChanged: {//解决Qt窗口最大化的时候最小化,再恢复窗口变为普通窗口的bug
            if(isMaximized && visibility === 2) {
                mainWindow.showMaximized()
            }
        }
    }
    MouseArea{
        anchors.fill: parent
        acceptedButtons: Qt.LeftButton //只处理鼠标左键
        property bool   isDoubleClicked:false
        property point clickPos: "0,0"
         onPressed:
         {
             isDoubleClicked = false;
             clickPos = Qt.point(mouse.x,mouse.y)
         }
         onPositionChanged: {
             if(!isDoubleClicked && pressed && mainWindow.visibility !== Window.Maximized && mainWindow.visibility !== Window.FullScreen) {
                 var delta = Qt.point(mouse.x-clickPos.x, mouse.y-clickPos.y)
                 mainWindow.x += delta.x
                 mainWindow.y += delta.y
             }
             if(mainWindow.visibility === Window.Maximized && pressed && !isDoubleClicked)
             {
                 isMaximized = false;
                 mainWindow.showNormal();
                 normBtnBg.source  = "qrc:/res/maximinze_btn.png"
             }
         }
         onDoubleClicked :
         {
             isDoubleClicked = true; // 这个时候一定不能响应onPositionChanged不然会一直置顶。
             if(isMaximized){
                 isMaximized = false;
                 mainWindow.showNormal();
                 normBtnBg.source  = "qrc:/res/maximinze_btn.png"
             }else{
                 isMaximized = true;
                 mainWindow.showMaximized();
                 normBtnBg.source = "qrc:/res/norm_btn.png"
             }
         }
     }

    Button{
        id:closeBtn
        anchors.right: parent.right
        width: parent.height
        height: parent.height - 1
        hoverEnabled : true
        background: Rectangle{
            color: closeBtn.hovered ? (closeBtn.pressed ? "#DE0000" : "#F91515") : "transparent"
            Image{
                id: closeBtnImg
                width: parent.height * 0.3; height: parent.height * 0.3
                anchors.centerIn: parent
                source: "qrc:/res/close_btn.png"
            }
        }
        onClicked: {
            mainWindow.close()
        }
    }
    Button{
        id: normBtn
        anchors.right: closeBtn.left
        width: parent.height
        height: parent.height - 1
        hoverEnabled : true
        background: Rectangle{
            color: normBtn.hovered ? (normBtn.pressed ? "#4E4E4E" : "#666666") : "transparent"
            //color: "black"
            Image{
                id: normBtnBg
                width: parent.height * 0.3; height: parent.height * 0.3
                anchors.centerIn: parent
                source: "qrc:/res/maximinze_btn.png"
            }
        }
        onClicked:{
            if(isMaximized){
                isMaximized = false;
                mainWindow.showNormal();
                normBtnBg.source = "qrc:/res/maximinze_btn.png"
            }else{
                isMaximized = true;
                mainWindow.showMaximized();
                normBtnBg.source = "qrc:/res/norm_btn.png"
            }
        }
    }
    Button{
        id: minBtn
        anchors.right: normBtn.left
        width: parent.height
        height: parent.height - 1
        hoverEnabled : true
        background: Rectangle{
            color: minBtn.hovered ? (minBtn.pressed ? "#4E4E4E" : "#666666") : "transparent"
            Image{
                width: parent.height * 0.3; height: parent.height * 0.3
                anchors.centerIn: parent
                source: "qrc:/res/min_btn.png"
            }
        }
        onClicked:{
            mainWindow.showMinimized();
        }
    }
}

标题栏实现起来比较简单,主要有几个功能:最大化,最小化、普通化(normal)以及双击最大化、双击恢复普通窗口。这里用一个两个变量:isMaximized和isDoubleClicked来记录窗体状态和双击状态。然后分别实现其事件逻辑即可。这里需要主要的是Qt的一个bug,也就是在窗口时最大化,然后再恢复窗口,这时窗体变成普通窗口了(正常应该还是最大窗口),这个bug在5.13和5.13.1还没被修复,bug请看这里
在这里我们可以用自己用代码修复,即我们可以判断窗体在最小化恢复时候,如果这个时候窗体是最大化并且是Windowed(值为2)这个时候将其最大化即可。具体代码可看上面onVisibilityChanged这一行代码

  • 窗体的边缘拉伸和拖拽主要实现在ResizeItem和ResizeQmlWindow,ResizeQmlWindow类主要实现设置鼠标的指针状态而已,具体检测鼠标的边缘事件在ResizeItem.qml里,如下所示
import QtQuick 2.0
import QtQuick.Window 2.2

Item {
    property int enableSize: 4
    property bool isPressed: false
    property point customPoint

    //左上角
    Item {
        id: leftTop
        width: enableSize
        height: enableSize
        anchors.left: parent.left
        anchors.top: parent.top
        z: 1000
        MouseArea {
            anchors.fill: parent
            hoverEnabled: true

            onPressed: press(mouse)
            onEntered: enter(1)
            onReleased: release()
            onPositionChanged: positionChange(mouse, -1, -1)
        }
    }

    //Top
    Item {
        id: top
        height: enableSize
        anchors.left: leftTop.right
        anchors.right: rightTop.left
        anchors.top: parent.top
        z: 1000
        MouseArea {
            anchors.fill: parent
            hoverEnabled: true

            onPressed: press(mouse)
            onEntered: enter(2)
            onReleased: release()

            onMouseYChanged: positionChange(Qt.point(customPoint.x, mouseY), 1, -1)
        }
    }

    //右上角
    Item {
        id: rightTop
        width: enableSize
        height: enableSize
        anchors.right: parent.right
        anchors.top: parent.top
        z: 1000
        MouseArea {
            anchors.fill: parent
            hoverEnabled: true

            onPressed: press(mouse)
            onEntered: enter(3)
            onReleased: release()
            onPositionChanged: positionChange(mouse, 1, -1)
        }
    }

    //Left
    Item {
        id: left
        width: enableSize
        anchors.left: parent.left
        anchors.top: leftTop.bottom
        anchors.bottom: leftBottom.top
        z: 1000
        MouseArea {
            anchors.fill: parent
            hoverEnabled: true

            onPressed: press(mouse)
            onEntered: enter(4)
            onReleased: release()

            onMouseXChanged: positionChange(Qt.point(mouseX, customPoint.y), -1, 1)
        }
    }

    //Center - 5
    Item {
        id: center
        anchors.left: left.right
        anchors.right: right.left
        anchors.top: top.bottom
        anchors.bottom: bottom.top
        MouseArea {
            anchors.fill: parent

            property point clickPos
            onPressed: clickPos = Qt.point(mouse.x,mouse.y)
            onPositionChanged: {
                if(pressed && mainWindow.visibility !== Window.Maximized && mainWindow.visibility !== Window.FullScreen) {
                    var delta = Qt.point(mouse.x-clickPos.x, mouse.y-clickPos.y)
                    mainWindow.x += delta.x
                    mainWindow.y += delta.y
                }
            }
        }
    }

    //Right
    Item {
        id: right
        width: enableSize
        anchors.right: parent.right
        anchors.top: rightTop.bottom
        anchors.bottom: rightBottom.top
        z: 1000
        MouseArea {
            anchors.fill: parent
            hoverEnabled: true

            onPressed: press(mouse)
            onEntered: enter(6)
            onReleased: release()

            onMouseXChanged: positionChange(Qt.point(mouseX, customPoint.y), 1, 1)
        }
    }

    //左下角
    Item {
        id: leftBottom
        width: enableSize
        height: enableSize
        anchors.left: parent.left
        anchors.bottom: parent.bottom
        z: 1000
        MouseArea {
            anchors.fill: parent
            hoverEnabled: true

            onPressed: press(mouse)
            onEntered: enter(7)
            onReleased: release()

            onPositionChanged: positionChange(mouse, -1, 1)
        }
    }

    //bottom
    Item {
        id: bottom
        height: enableSize
        anchors.left: leftBottom.right
        anchors.right: rightBottom.left
        anchors.bottom: parent.bottom
        z: 1000
        MouseArea {
            anchors.fill: parent
            hoverEnabled: true

            onPressed: press(mouse)
            onEntered: enter(8)
            onReleased: release()

            onMouseYChanged: positionChange(Qt.point(customPoint.x, mouseY), 1, 1)
        }
    }

    //右下角
    Item {
        id:rightBottom
        width: enableSize
        height: enableSize
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        z: 1000
        MouseArea {
            anchors.fill: parent
            hoverEnabled: true

            onPressed: press(mouse)
            onEntered: enter(9)
            onReleased: release()

            onPositionChanged: positionChange(mouse,1,1)
        }
    }

    function enter(direct) {
        Resize.setMyCursor(direct)
    }

    function press(mouse) {
        isPressed = true
        customPoint = Qt.point(mouse.x, mouse.y)
    }

    function release() {
        isPressed = false
        //customPoint = undefined
    }

    function positionChange(newPosition, directX/*x轴方向*/, directY/*y轴方向*/) {
        if(!isPressed) return

        var delta = Qt.point(newPosition.x-customPoint.x, newPosition.y-customPoint.y)
        var tmpW,tmpH

        if(directX >= 0)
            tmpW = mainWindow.width + delta.x
        else
            tmpW = mainWindow.width - delta.x

        if(directY >= 0)
            tmpH = mainWindow.height + delta.y
        else
            tmpH = mainWindow.height - delta.y

        if(tmpW < mainWindow.minimumWidth) {
            if(directX < 0)
                mainWindow.x += (mainWindow.width - mainWindow.minimumWidth)
            mainWindow.width = mainWindow.minimumWidth
        }
        else {
            mainWindow.width = tmpW
            if(directX < 0)
                mainWindow.x += delta.x
        }

        if(tmpH < mainWindow.minimumHeight) {
            if(directY < 0)
                mainWindow.y += (mainWindow.height - mainWindow.minimumHeight)
            mainWindow.height = mainWindow.minimumHeight
        }
        else {
            mainWindow.height = tmpH
            if(directY < 0)
                mainWindow.y += delta.y
        }
    }
}

ResizeQmlWindow.cpp

#include "ResizeQmlWindow.h"


ResizeQmlWindow::ResizeQmlWindow(QObject *parent) : QObject(parent)
{}


void ResizeQmlWindow::setWindow(QWindow *win)
{
    m_win = win;
}


void ResizeQmlWindow::setMyCursor(int direct)
{
    switch (direct) {
    case 1:
        m_win->setCursor(QCursor(Qt::SizeFDiagCursor));
        break;
    case 2:
        m_win->setCursor(QCursor(Qt::SizeVerCursor));
        break;
    case 3:
        m_win->setCursor(QCursor(Qt::SizeBDiagCursor));
        break;
    case 4:
        m_win->setCursor(QCursor(Qt::SizeHorCursor));
        break;
    case 5:
        m_win->setCursor(QCursor(Qt::ArrowCursor));
        break;
    case 6:
        m_win->setCursor(QCursor(Qt::SizeHorCursor));
        break;
    case 7:
        m_win->setCursor(QCursor(Qt::SizeBDiagCursor));
        break;
    case 8:
        m_win->setCursor(QCursor(Qt::SizeVerCursor));
        break;
    case 9:
        m_win->setCursor(QCursor(Qt::SizeFDiagCursor));
        break;
    }
}

因为我们要在C++设置鼠标的状态,所以我们还须将ResizeQmlWindow注册传递到Qml中,如下

    QObject * obj = engine.rootObjects().at(0);
    QWindow * w = qobject_cast<QWindow *>(obj);
    ResizeQmlWindow resize;
    if(w) {
        resize.setWindow(w);
        engine.rootContext()->setContextProperty("Resize", &resize);
    }

对比效果如图所示


原生边框
定制边框

示例工程下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351