部门启动了一个项目,用Qt做界面,美工毫无意外地又把界面设计得花里胡哨。由于对QML不熟,只好再次祭出QSS,并辅以QPainter的绘制。这里碰到一个问题,要做得和效果图一样,必须去掉边框(Qt::FramelessWindowHint属性),于是只好自己处理窗体的鼠标事件,控制拖动和缩放。之前做过类似的事情,但是实现的不好,因为要把大量的代码塞到各种鼠标事件函数里,和类里其他的事务代码掺杂在一起,很难看。这次想了想,分离出一个代理类,利用installEventFilter把窗体事件挂到这个代理类中实现,大大解耦了。
先简单讲下原理。拖动很简单,此处略过。至于缩放,我的做法是,把窗体区域划分成一个九宫格(如下图所示),鼠标在区域1、区域5拖拽改变窗体高度,在区域3、区域7拖拽改变宽度,2、4、6、8同时改变宽高,这个也不用多说了,还是看代码吧。
DragProxy.h
#ifndef DRAGPROXY_H #define DRAGPROXY_H #include <QObject> #include <QWidget> #include <QEvent> #include <QRect> #include <QPoint> class DragProxy : public QObject { Q_OBJECT public: DragProxy(QWidget *parent); ~DragProxy(); protected: enum WidgetRegion { Top = 0, TopRight, Right, RightBottom, Bottom, LeftBottom, Left, LeftTop, Inner, Unknown }; public: // 设置四周边框宽度 void SetBorderWidth(int top, int right, int bottom, int left); protected: virtual bool eventFilter(QObject* obj, QEvent* event); void MakeRegions(); WidgetRegion HitTest(const QPoint& pos); void UpdateGeometry(int x, int y, int w, int h); // 鼠标从边框快速移到窗体内子控件上,可能会造成鼠标样式未改变,这里使用计时器监控 void StartCursorTimer(); void StopCursorTimer(); private: QWidget* m_proxyWidget; // 代理的窗体 int m_top, m_right, m_bottom, m_left; // 四周宽度 QRect m_regions[9]; // 九宫格,对应9个区域 QPoint m_originPosGlobal; // 拖拽前鼠标位置 QRect m_originGeo; // 拖拽前窗体位置和大小 bool m_mousePressed; // 鼠标是否按下 WidgetRegion m_regionPressed; // 记录鼠标按下时所点击的区域 int m_cursorTimerId; }; #endif // DRAGPROXY_H
DragProxy.cpp
#include "DragProxy.h" #include <QMouseEvent> #include <QTimerEvent> #include <QCursor> DragProxy::DragProxy(QWidget *parent) : QObject((QObject*)parent) { m_proxyWidget = parent; m_top = m_right = m_bottom = m_left = 0; m_proxyWidget->setMouseTracking(true); m_proxyWidget->installEventFilter(this); // 代理窗体事件 m_mousePressed = false; m_regionPressed = Unknown; m_cursorTimerId = 0; } DragProxy::~DragProxy() { } void DragProxy::SetBorderWidth(int top, int right, int bottom, int left) { m_top = top; m_right = right; m_bottom = bottom; m_left = left; MakeRegions(); } void DragProxy::UpdateGeometry(int x, int y, int w, int h) { int minWidth = m_proxyWidget->minimumWidth(); int minHeight = m_proxyWidget->minimumHeight(); int maxWidth = m_proxyWidget->maximumWidth(); int maxHeight = m_proxyWidget->maximumHeight(); if (w < minWidth || w > maxWidth || h < minHeight || h > maxHeight) { return; } m_proxyWidget->setGeometry(x, y, w, h); } bool DragProxy::eventFilter(QObject* obj, QEvent* event) { QEvent::Type eventType = event->type(); if (eventType == QEvent::MouseMove) { QMouseEvent* mouseEvent = (QMouseEvent*)event; QPoint curPosLocal = mouseEvent->pos(); DragProxy::WidgetRegion regionType = HitTest(curPosLocal); QPoint curPosGlobal = m_proxyWidget->mapToGlobal(curPosLocal); if (!m_mousePressed) // 鼠标未按下 { switch (regionType) { case Top: case Bottom: m_proxyWidget->setCursor(Qt::SizeVerCursor); break; case TopRight: case LeftBottom: m_proxyWidget->setCursor(Qt::SizeBDiagCursor); break; case Right: case Left: m_proxyWidget->setCursor(Qt::SizeHorCursor); break; case RightBottom: case LeftTop: m_proxyWidget->setCursor(Qt::SizeFDiagCursor); break; default: m_proxyWidget->setCursor(Qt::ArrowCursor); break; } StartCursorTimer(); } else // 鼠标已按下 { QRect geo = m_proxyWidget->geometry(); if (m_regionPressed == Inner) { m_proxyWidget->move(m_originGeo.topLeft() + curPosGlobal - m_originPosGlobal); } else if (m_regionPressed == Top) { int dY = curPosGlobal.y() - m_originPosGlobal.y(); UpdateGeometry(m_originGeo.x(), m_originGeo.y() + dY, m_originGeo.width(), m_originGeo.height() - dY); } else if (m_regionPressed == TopRight) { QPoint dXY = curPosGlobal - m_originPosGlobal; UpdateGeometry(m_originGeo.x(), m_originGeo.y() + dXY.y(), m_originGeo.width() + dXY.x(), m_originGeo.height() - dXY.y()); } else if (m_regionPressed == Right) { int dX = curPosGlobal.x() - m_originPosGlobal.x(); UpdateGeometry(m_originGeo.x(), m_originGeo.y(), m_originGeo.width() + dX, m_originGeo.height()); } else if (m_regionPressed == RightBottom) { QPoint dXY = curPosGlobal - m_originPosGlobal; UpdateGeometry(m_originGeo.x(), m_originGeo.y(), m_originGeo.width() + dXY.x(), m_originGeo.height() + dXY.y()); } else if (m_regionPressed == Bottom) { int dY = curPosGlobal.y() - m_originPosGlobal.y(); UpdateGeometry(m_originGeo.x(), m_originGeo.y(), m_originGeo.width(), m_originGeo.height() + dY); } else if (m_regionPressed == LeftBottom) { QPoint dXY = curPosGlobal - m_originPosGlobal; UpdateGeometry(m_originGeo.x() + dXY.x(), m_originGeo.y(), m_originGeo.width() - dXY.x(), m_originGeo.height() + dXY.y()); } else if (m_regionPressed == Left) { int dX = curPosGlobal.x() - m_originPosGlobal.x(); UpdateGeometry(m_originGeo.x() + dX, m_originGeo.y(), m_originGeo.width() - dX, m_originGeo.height()); } else if (m_regionPressed == LeftTop) { QPoint dXY = curPosGlobal - m_originPosGlobal; UpdateGeometry(m_originGeo.x() + dXY.x(), m_originGeo.y() + dXY.y(), m_originGeo.width() - dXY.x(), m_originGeo.height() - dXY.y()); } } } else if (eventType == QEvent::MouseButtonPress) { QMouseEvent* mouseEvent = (QMouseEvent*)event; if (mouseEvent->button() == Qt::LeftButton) { m_mousePressed = true; QPoint curPos = mouseEvent->pos(); m_regionPressed = HitTest(curPos); m_originPosGlobal = m_proxyWidget->mapToGlobal(curPos); m_originGeo = m_proxyWidget->geometry(); StopCursorTimer(); } } else if (eventType == QEvent::MouseButtonRelease) { m_mousePressed = false; m_regionPressed = Unknown; m_proxyWidget->setCursor(Qt::ArrowCursor); } else if (eventType == QEvent::Resize) { MakeRegions(); } else if (eventType == QEvent::Leave) { m_proxyWidget->setCursor(Qt::ArrowCursor); StopCursorTimer(); } else if (eventType == QEvent::Timer) { QTimerEvent* timerEvent = (QTimerEvent*)event; if (timerEvent->timerId() == m_cursorTimerId) { if (m_regions[Inner].contains(m_proxyWidget->mapFromGlobal(QCursor::pos()))) { m_proxyWidget->setCursor(Qt::ArrowCursor); StopCursorTimer(); } } } return QObject::eventFilter(obj, event); } void DragProxy::StartCursorTimer() { StopCursorTimer(); m_cursorTimerId = m_proxyWidget->startTimer(50); } void DragProxy::StopCursorTimer() { if (m_cursorTimerId != 0) { m_proxyWidget->killTimer(m_cursorTimerId); m_cursorTimerId = 0; } } void DragProxy::MakeRegions() { int width = m_proxyWidget->width(); int height = m_proxyWidget->height(); m_regions[Top] = QRect(m_left, 0, width - m_left - m_right, m_top); m_regions[TopRight] = QRect(width - m_right, 0, m_right, m_top); m_regions[Right] = QRect(width - m_right, m_top, m_right, height - m_top - m_bottom); m_regions[RightBottom] = QRect(width - m_right, height - m_bottom, m_right, m_bottom); m_regions[Bottom] = QRect(m_left, height - m_bottom, width - m_left - m_right, m_bottom); m_regions[LeftBottom] = QRect(0, height - m_bottom, m_left, m_bottom); m_regions[Left] = QRect(0, m_top, m_left, height - m_top - m_bottom); m_regions[LeftTop] = QRect(0, 0, m_left, m_top); m_regions[Inner] = QRect(m_left, m_top, width - m_left - m_right, height - m_top - m_bottom); } DragProxy::WidgetRegion DragProxy::HitTest(const QPoint& pos) { for (int i = 0; i < 9; i++) { const QRect rect = m_regions[i]; if (rect.contains(pos)) { return DragProxy::WidgetRegion(i); } } return Unknown; }
引入那个计时器,是为了解决一个难缠的鼠标样式问题。我发现,如果从鼠标从窗体边框快速移到窗体边缘的一个子控件上,而此控件又没有处理鼠标样式,鼠标样式不会恢复成默认的箭头。所以我只好很别扭地使用了一个计时器,发现鼠标已不在窗体边缘时,将鼠标样式重置为箭头,同时停止计时器。这种方法显然很生硬,如果有更好方案请赐教。使用代码如下:
FramelessDialog::FramelessDialog(QWidget *parent) : QDialog(parent) { ui.setupUi(this); setWindowFlags(Qt::FramelessWindowHint); setMinimumSize(50, 50); setMaximumSize(800, 800); DragProxy* dragProxy = new DragProxy(this); dragProxy->SetBorderWidth(8, 8, 8, 8); }
刚录了个gif,可以看动态效果,不知道为什么上传上来就显示成静止图片了,于是作罢,各位有兴趣自己试验下。