QML 自定义窗口简易实现:使用过滤 WINDOW 事件的方式
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
}
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」