QML 自定义窗口简易实现:使用过滤 Window 事件的方式

本文介绍了使用QML实现无边框窗口的两种方法,包括纯QML和结合本地API。作者分享了一种通过事件过滤器实现的自定义窗口类,该类能够处理拖拽和边框缩放,并解决了窗口闪烁问题。代码示例展示了如何在QML中使用这个类,同时指出了在实现过程中遇到的鼠标事件传递、窗口闪烁等问题。
摘要由CSDN通过智能技术生成

1.前言

QML 自定义窗口目前看到的主要有两种方式,一种是纯 QML 实现,使用 MouseArea 来处理鼠标相关事件;另一种是事件过滤,用系统本地 API 进行操作。前两天看了涛哥的自定义窗口(https://github.com/jaredtao/TaoQuick),是继承 QQuickWindow + 本地 API 的方式实现的。我本来也想借鉴下,但是发现 QML 的 Window 在 Qt5 后面的版本改为了 QQuickWindow 的子类 QQuickWindowQmlImpl ,还是个没导出的类。所以我就改了下自己的思路,由继承 Window 改为过滤其事件。

2.实现

我使用的方式是:创建一个 QObject 子类过滤 Window 的事件,过滤到移动和拉伸等操作的时候就去设置 Window 相关参数。

目前实现的功能只有拖拽标题栏和边框缩放,最终效果如下:

这里面也遇到不少问题:

整个 Window 上的鼠标事件都会被传递进来,即使上面放了个 MouseArea 或者 Button ,而 QWidget 就不会接收小部件的事件。

鼠标在不同的子部件移动时,Window 里没有特殊的信号,有时会有 CursorChange 的事件。

MouseArea 来作为标题栏的区域绑定,不然用 Rectangle 也不知道是点击在了按钮还是标题栏空白处,毕竟都会传递到 Window。

QML 拉伸的时候窗口会闪烁,这是原生就有的问题,做个橡皮筋效果应该会舒服点。

3.代码 

完整代码链接:https://github.com/gongjianbo/MyTestCode/tree/master/Qml/QmlFramelessWindow

这里贴出过滤器类和 QML 使用示例的代码:

#ifndef FRAMELESSHELPER_H
#define FRAMELESSHELPER_H

#include <QQuickItem>
#include <QQuickWindow>
#include <QEvent>

/**
 * @brief 一个简易的无边框辅助类
 * @author 龚建波
 * @date 2020-11-15
 * @details
 * 之前看有网友用的Window+本地事件来做的
 * Qt5 QML 中的 Window类型
 * 可能是QQuickWindow或者QQuickWindowQmlImpl(子类),后者未导出
 * 所以思路由重写事件改为过滤,但最终的实现效果感觉不大好,resize得时候一样会有闪烁
 * (拉伸时闪烁也是老问题了)
 * 感觉解决拉伸时闪烁问题还是用橡皮筋好一点
 *
 * 使用说明:
 * 1.注册为QML类型
 * 2.锚定window(Window)和title(MouseArea)两个区域,且设置borderWidth边距
 * FramelessHelper {
 *      window: root
 *      title: title_area
 *      borderWidth: 6
 * }
 * 3.Window设置flags为无边框
 * flags: Qt.Window|Qt.FramelessWindowHint|Qt.WindowMinMaxButtonsHint
 */
class FramelessHelper : public QObject
{
    Q_OBJECT
    //边距
    Q_PROPERTY(int borderWidth READ getBorderWidth WRITE setBorderWidth NOTIFY borderWidthChanged)
    //拖动使能
    Q_PROPERTY(bool moveEnable READ getMoveEnable WRITE setMoveEnable NOTIFY moveEnableChanged)
    //正在拖动
    Q_PROPERTY(bool moving READ getMoving NOTIFY movingChanged)
    //拉伸缩放使能
    Q_PROPERTY(bool resizeEnable READ getResizeEnable WRITE setResizeEnable NOTIFY resizeEnableChanged)
    //正在拖边框改变大小
    Q_PROPERTY(bool resizing READ getResizing NOTIFY resizingChanged)
    //绑定主窗口,Window类型
    Q_PROPERTY(QQuickWindow* window MEMBER window WRITE setWindow NOTIFY windowChanged)
    //绑定标题栏,MouseArea类型
    Q_PROPERTY(QQuickItem* title MEMBER title WRITE setTitle NOTIFY titleChanged)
private:
    //区域划分-九宫格,便于判断当前点击位置
    //竖向上中下0x01-0x02-0x04
    //横向左中右0x10-0x20-0x40
    //判断时分别取pos-x和y判断区域进行叠加
    enum FramelessArea
    {
        FContentArea = 0x00 //内容区域
        ,FLeftArea = 0x10 //左侧
        ,FRightArea = 0x20 //右侧
        ,FTopArea = 0x01 //顶部
        ,FBottomArea = 0x02 //底部
        ,FLeftTopCorner = 0x11 //左上角
        ,FRightTopCorner = 0x21 //右上角
        ,FLeftBottomCorner = 0x12 //左下角
        ,FRightBottomCorner = 0x22 //右下角
    };
public:
    explicit FramelessHelper(QObject *parent = nullptr);

    int getBorderWidth() const;
    void setBorderWidth(int width);

    bool getMoveEnable() const;
    void setMoveEnable(bool enable);

    bool getMoving() const;
    void setMoving(bool state);

    bool getResizeEnable() const;
    void setResizeEnable(bool enable);

    bool getResizing() const;
    void setResizing(bool state);

    void setWindow(QQuickWindow *newWindow);
    void setTitle(QQuickItem *newTitle);

protected:
    bool eventFilter(QObject *watched, QEvent *event) override;

private:
    //处理窗口相关事件
    void filterWindowEvent(QEvent *event);
    //处理标题栏相关事件
    void filterTitleEvent(QEvent *event);
    //鼠标移动
    void mouseMoveEvent(QMouseEvent *event);
    //判断是否最大化
    bool windowIsMaxed() const;
    //更新鼠标位置
    void updatePosition(const QPoint &pos);
    //判断鼠标位置更新鼠标形状
    void updateCursor(int area);
    //重置为默认鼠标形状
    void resetCuror();

signals:
    void borderWidthChanged();
    void moveEnableChanged();
    void movingChanged();
    void resizeEnableChanged();
    void resizingChanged();
    void windowChanged();
    void titleChanged();

private:
    //边框
    int borderWidth=6;
    //移动标志
    bool moveEnable=true;
    bool moving=false;
    //缩放标志
    bool resizeEnable=true;
    bool resizing=false;
    //暂存鼠标位置信息
    QPoint screenPosTemp=QPoint(0, 0);
    //暂存窗体位置、大小信息
    QRect geometryTemp;
    //当前区域
    FramelessArea cursorArea=FContentArea;
    //窗口-需要绑定Window
    QQuickWindow *window=nullptr;
    //标题栏-需要绑定MouseArea
    QQuickItem *title=nullptr;
};

#endif // FRAMELESSHELPER_H
#include "FramelessHelper.h"

#include <QCursor>
#include <QEnterEvent>
#include <QMouseEvent>
#include <QDebug>

FramelessHelper::FramelessHelper(QObject *parent)
    : QObject(parent)
{

}

int FramelessHelper::getBorderWidth() const
{
    return borderWidth;
}

void FramelessHelper::setBorderWidth(int width)
{
    if(borderWidth!=width){
        borderWidth=width;
        emit borderWidthChanged();
    }
}

bool FramelessHelper::getMoveEnable() const
{
    return moveEnable;
}

void FramelessHelper::setMoveEnable(bool enable)
{
    if(moveEnable!=enable){
        moveEnable=enable;
        emit moveEnableChanged();
    }
}

bool FramelessHelper::getMoving() const
{
    return moveEnable&&moving;
}

void FramelessHelper::setMoving(bool state)
{
    if(moving!=state){
        moving=moveEnable&&state;
        emit movingChanged();
    }
}

bool FramelessHelper::getResizeEnable() const
{
    return resizeEnable;
}

void FramelessHelper::setResizeEnable(bool enable)
{
    if(resizeEnable!=enable){
        resizeEnable=enable;
        emit resizeEnableChanged();
    }
}

bool FramelessHelper::getResizing() const
{
    return resizeEnable&&resizing;
}

void FramelessHelper::setResizing(bool state)
{
    if(resizing!=state){
        resizing=resizeEnable&&state;
        emit resizingChanged();
    }
}

void FramelessHelper::setWindow(QQuickWindow *newWindow)
{
    if(newWindow&&newWindow!=window){
        if(window)
            window->removeEventFilter(this);
        window=newWindow;
        window->installEventFilter(this);
        emit windowChanged();
    }
}

void FramelessHelper::setTitle(QQuickItem *newTitle)
{
    if(newTitle&&newTitle!=title){
        if(title)
            title->removeEventFilter(this);
        title=newTitle;
        title->installEventFilter(this);
        emit titleChanged();
    }
}

bool FramelessHelper::eventFilter(QObject *watched, QEvent *event)
{
    //qDebug()<<watched<<event;
    if(watched==window){
        filterWindowEvent(event);
    }else if(watched==title){
        filterTitleEvent(event);
    }
    return false;
}

void FramelessHelper::filterWindowEvent(QEvent *event)
{
    if(!window)
        return;
    switch (event->type()) {
    //case QEvent::CursorChange: break;
    case QEvent::Enter:
        //根据位置更新鼠标样式
        updatePosition(static_cast<QEnterEvent*>(event)->pos());
        break;
    case QEvent::Leave:
        //恢复鼠标样式
        resetCuror();
        break;
    case QEvent::CursorChange:
        //跑到别的区域上了
    case QEvent::UpdateRequest:
        //QExposeEvent的时候会设置为默认样式,这里重置回来
        if(getResizing()||getMoving())
            updateCursor(cursorArea);
        break;
    case QEvent::MouseButtonPress:
        //screenPosTemp = static_cast<QMouseEvent*>(event)->screenPos().toPoint();
        screenPosTemp=QCursor::pos();
        geometryTemp = window->geometry();
        //非边框区域FContentArea
        if (getResizeEnable()&&cursorArea!=FContentArea&&!windowIsMaxed()) {
            //非最大化时点击了边框,且允许缩放
            setResizing(true);
        }
        break;
    case QEvent::MouseButtonRelease:
        geometryTemp = window->geometry();
        //非拖动标题栏时释放鼠标
        if(!getMoving()){
            setResizing(false);
            updatePosition(static_cast<QMouseEvent*>(event)->pos());
        }
        break;
    case QEvent::MouseMove:
        mouseMoveEvent(static_cast<QMouseEvent*>(event));
        break;
    default: break;
    }
}

void FramelessHelper::filterTitleEvent(QEvent *event)
{
    if(!window||!title)
        return;
    switch (event->type()) {
    case QEvent::MouseButtonPress:
        //点击标题栏
        if (getMoveEnable()) {
            if (windowIsMaxed()) {
                //最大化状态下点击标题栏
            }else if(cursorArea==FContentArea){
                //非边框区域时可以拖动
                setMoving(true);
            }
        }
        break;
    case QEvent::MouseButtonDblClick:
        //双击标题栏,切换最大和普通大小
        if(windowIsMaxed()){
            window->showNormal();
        }else{
            window->showMaximized();
        }
        break;
    case QEvent::MouseButtonRelease:
        //拖动标题栏时释放鼠标
        if(getMoving()){
            setMoving(false);
            updatePosition(static_cast<QMouseEvent*>(event)->pos());
            QRect geometry=window->geometry();
            //如果拖到了标题栏就最大化
            if(geometry.y()<0){
                geometry.moveTop(0);
                window->setGeometry(geometry);
                window->showMaximized();
            }
        }
        break;
    case QEvent::MouseMove:
        //最大化时拖动标题栏就恢复为普通大小并可拖动
        if (getMoveEnable()&&windowIsMaxed()) {
            QMouseEvent *mouse_event=static_cast<QMouseEvent*>(event);
            const int old_width=window->width();
            const int old_pos=mouse_event->pos().x();
            window->showNormal();
            QRect geometry=window->geometry();
            const QPoint cursor_pos=QCursor::pos();
            //标题栏上,鼠标所在x按照原来的比例设置,y不变
            geometry.moveLeft(cursor_pos.x()-geometry.width()*(old_pos/(double)old_width));
            geometry.moveTop(cursor_pos.y()-title->height()/2);
            window->setGeometry(geometry);
            geometryTemp=geometry;
            setMoving(true);
        }
        break;
    default: break;
    }
}

void FramelessHelper::mouseMoveEvent(QMouseEvent *event)
{
    if(getMoving()){
        //按住标题栏拖动
        event->accept();
        const QPoint move_vec=QCursor::pos()-screenPosTemp;
        window->setGeometry(QRect(geometryTemp.topLeft()+move_vec,geometryTemp.size()));
    }else if(getResizing()){
        //按住边框拖拽大小
        event->accept();
        const QPoint move_vec=QCursor::pos()-screenPosTemp;
        //每个方向单独计算
        //然后判断计算出来的pos和size是否有效,大于最小尺寸
        //因为每个方向固定的边不一样,所以单独处理
        QRect new_geometry=geometryTemp;
        //横项调整
        switch (cursorArea&0xF0) {
        case FLeftArea: //左侧
            new_geometry.setLeft(geometryTemp.left()+move_vec.x());
            if (new_geometry.width()<window->minimumWidth()){
                new_geometry.setLeft(geometryTemp.right()-window->minimumWidth());
            }
            break;
        case FRightArea: //右侧
            new_geometry.setRight(geometryTemp.right()+move_vec.x());
            if (new_geometry.width()<window->minimumWidth()){
                new_geometry.setRight(geometryTemp.left()+window->minimumWidth());
            }
            break;
        default: break;
        }
        //竖向调整
        switch (cursorArea&0x0F) {
        case FTopArea: //顶部
            new_geometry.setTop(geometryTemp.top()+move_vec.y());
            if(new_geometry.height()<window->minimumHeight())
                new_geometry.setTop(geometryTemp.bottom()-window->minimumHeight());
            break;
        case FBottomArea: //底部
            new_geometry.setBottom(geometryTemp.bottom()+move_vec.y());
            if(new_geometry.height()<window->minimumHeight())
                new_geometry.setBottom(geometryTemp.top()+window->minimumHeight());
            break;
        default: break;
        }
        window->setGeometry(new_geometry);
    }else if(getResizeEnable()){
        //根据位置更新鼠标样式
        updatePosition(event->pos());
    }
}

bool FramelessHelper::windowIsMaxed() const
{
    //判断窗口是否最大化
    return (window&&(window->visibility()==QWindow::Maximized
                     ||window->visibility()==QWindow::FullScreen));
}

void FramelessHelper::updatePosition(const QPoint &pos)
{
    if(!window||windowIsMaxed())
        return;
    //根据鼠标坐标判断所在区域
    int pos_area=cursorArea;
    //可拖拽大小时才判断
    if (resizeEnable)
    {
        if (pos.x()<borderWidth) {
            pos_area=0x10;
        }else if (pos.x()>window->width()-borderWidth) {
            pos_area=0x20;
        }else {
            pos_area=0x00;
        }
        if (pos.y()<borderWidth) {
            pos_area+=0x01;
        }else if (pos.y()>window->height()-borderWidth) {
            pos_area+=0x02;
        }else {
            pos_area+=0x00;
        }
    }
    if (pos_area == cursorArea)
        return;
    cursorArea=(FramelessArea)pos_area;
    updateCursor(cursorArea);
}

void FramelessHelper::updateCursor(int area)
{
    //根据鼠标悬停位置更换鼠标形状
    switch (area) {
    case FLeftArea:
    case FRightArea:
        window->setCursor(Qt::SizeHorCursor);
        break;
    case FTopArea:
    case FBottomArea:
        window->setCursor(Qt::SizeVerCursor);
        break;
    case FLeftTopCorner:
    case FRightBottomCorner:
        window->setCursor(Qt::SizeFDiagCursor);
        break;
    case FRightTopCorner:
    case FLeftBottomCorner:
        window->setCursor(Qt::SizeBDiagCursor);
        break;
    default:
        window->setCursor(Qt::ArrowCursor);
        break;
    }
}

void FramelessHelper::resetCuror()
{
    if(!window)
        return;
    //重置为默认鼠标样式
    cursorArea=FContentArea;
    window->setCursor(Qt::ArrowCursor);
}
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import GongJianBo 1.0

//演示FramelessHelper的使用
Window {
    id: root
    width: 640
    height: 480
    minimumHeight: 200
    minimumWidth: 500
    visible: true
    title: qsTr("Qml 简易无边框 (by 龚建波)")
    flags: Qt.Window
           |Qt.FramelessWindowHint
           |Qt.WindowMinMaxButtonsHint

    FramelessHelper{
        window: root
        title: title_area
        borderWidth: 6
    }

    //边框
    Rectangle{
        anchors.fill: parent
        border.color: "gray"
        border.width: 6
    }

    //标题栏
    MouseArea{
        id: title_area
        height: 50
        width: root.width
        Rectangle{
            anchors.fill: parent
            color: "darkCyan"
            opacity: 0.5
        }

        Text{
            anchors{
                left: parent.left
                verticalCenter: parent.verticalCenter
                margins: 20
            }
            font.pixelSize: 20
            color: "white"
            text: root.title
        }

        Row{
            anchors.right: parent.right
            anchors.verticalCenter: parent.verticalCenter
            anchors.margins: 10
            spacing: 10

            Button{
                width: 70
                height: 30
                text: "min"
                onClicked: root.showMinimized()
            }
            Button{
                width: 70
                height: 30
                text: "max"
                onClicked: {
                    if(root.visibility==Window.Maximized)
                        root.showNormal()
                    else
                        root.showMaximized()
                }
            }
            Button{
                width: 70
                height: 30
                text: "close"
                onClicked: Qt.quit()
            }
        }
    }

    ListView{
        anchors.fill: parent
        anchors.margins: 20
        anchors.topMargin: 70
        clip: true
        model: 50
        spacing: 5
        delegate: Rectangle{
            height: 40
            width: ListView.view.width
            border.color: "gray"
            Text {
                anchors.centerIn: parent
                text: index
            }
        }
    }
}

 

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值