Qt开源无边框窗口项目解析
Qt开源无边框窗口项目解析
《西江月·夜行黄沙道中》 -- 辛弃疾
明月别枝惊鹊,清风半夜鸣蝉。
稻花香里说丰年,听取蛙声一片。七八个星天外,两三点雨山前。
旧时茅店社林边,路转溪桥忽见。

无边框窗口的来源
无边框窗口指的是移除了操作系统提供的默认标题栏、边框和窗口控件的窗口。这种窗口形式为应用程序提供了完全的界面自定义能力,开发者可以:
- 设计独特的窗口样式和布局
- 实现自定义的窗口拖动和调整大小逻辑
- 创建与应用程序风格完全一致的标题栏控件
- 支持非矩形窗口形状(如圆角窗口、异形窗口)
本文是对开源项目无边框黑色主题窗口(https://github.com/Jorgen-VikingGod/Qt-Frameless-Window-DarkStyle)的说明、修改以及优化。
无边框窗口的构成
在无边框的界面中,由于没有了系统自带的标题栏,导致无边框窗口的移动和缩放功能不能正确生效。针对此,需要自定义实现移动和缩放功能。
如下图所示,无边框窗口(下图为开源项目修改代码, 内部结构一致,修改了名称和简化了代码,修改部分逻辑)其实有下面几个部分构成。首先时最底层,UFramelessWindow为最底层的界面类,该界面包含padding为5的间隔,用于显示高光阴影特效,同时用于鼠标拖动区域的响应。
// 用于配置 UFramelessWindow的无边框 + 半透明效果
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint);
#if defined(Q_OS_WIN)
setWindowFlags(windowFlags() | Qt::WindowMinimizeButtonHint);
#endif
setAttribute(Qt::WA_NoSystemBackground, true);
setAttribute(Qt::WA_TranslucentBackground, true);
接下来需要监听UFramelessWindow的鼠标按下事件,在这个padding为5的空隙矩形区域,才能响应鼠标的允许拖动计算事件。
setMouseTracking(true);
// important to watch mouse move from all child widgets
QApplication::instance()->installEventFilter(this);
// xxx
if (event->type() == QEvent::MouseButtonPress && obj == this) {
if (QMouseEvent *pMouse = dynamic_cast<QMouseEvent *>(event)) {
mousePressEvent(pMouse);
}
}
windowFrame为主体界面类,padding为1,用于高亮显示border(当不显示界面四周的高光阴影效果时)。windowFrame包好两个子界面,一个是标题栏(使用UWindowDragger类进行提升)用于控制界面的整体移动,一个是内容区域windowContent,用于动态加载需要标题栏的子界面。
windowContent界面不要使用在ui中添加任意控件,该界面是用于加载需要被标题栏包裹的子界面的。且需要进行布局。
void UFramelessWindow::setContent(QWidget *w)
{
ui->windowContent->layout()->addWidget(w);
}

拖动窗口
UWindowDragger这个类用于标题栏的基类,用于控制拖动标题栏的逻辑。
class UFRAMELESSWINDOW_EXPORT UWindowDragger : public QWidget
{
Q_OBJECT
public:
explicit UWindowDragger(QWidget *parent = nullptr);
virtual ~UWindowDragger() {}
signals:
void doubleClicked();
protected:
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void mouseDoubleClickEvent(QMouseEvent *event);
void paintEvent(QPaintEvent *event);
protected:
QPoint mousePos;
QPoint wndPos;
bool mousePressed;
};
核心功能实现:
一般而言,这个界面是作为无边框界面的标题栏,因此,需要通过父对象来获取无边框整体窗口。
界面移动逻辑的计算为:
- 需要判断无边框窗口(父窗口)的有效性,同时当处于最大化和全屏幕时,禁止移动。
- 逻辑:假设左上角坐标为(x0, y0), 当前鼠标的全局坐标G1(x, y), 窗口点击时的全局坐标G2(x1, y1), event->globalPos() - mousePos)等同与,以鼠标点击时的坐标为中心,鼠标实时移动在该中心移动的差值, 用这个差值+整体窗口的左上角,就可以进行移动了。
void UWindowDragger::mousePressEvent(QMouseEvent *event) {
mousePressed = true;
mousePos = event->globalPos();
QWidget *parent = parentWidget();
if (parent) parent = parent->parentWidget();
if (parent) wndPos = parent->pos();
}
void UWindowDragger::mouseMoveEvent(QMouseEvent *event) {
QWidget *parent = parentWidget();
if (parent) parent = parent->parentWidget();
if (parent && mousePressed)
parent->move(wndPos + (event->globalPos() - mousePos));
}
缩放窗口
缩放界面的头文件代码如下,下面简单介绍一下重要的实现逻辑。
class UFRAMELESSWINDOW_EXPORT UFramelessWindow : public QWidget
{
Q_OBJECT
public:
explicit UFramelessWindow(QWidget *parent = nullptr);
~UFramelessWindow();
void setContent(QWidget *w);
QWidget* windowContent() const;
// 控制按钮显隐
void setMinimizeButtonVisible(bool visible);
void setMaximizeButtonVisible(bool visible);
// 控制是否可拖拽改变大小
void setResizable(bool resizable) { m_bResizable = resizable; }
bool isResizable() const { return m_bResizable; }
// 设置 边框特效是否开启
void setBorderEffectEnable(bool enable);
public slots:
void setWindowTitle(const QString &text);
void setWindowIcon(const QIcon &ico);
private slots:
void on_minimizeButton_clicked();
void on_restoreButton_clicked();
void on_maximizeButton_clicked();
void on_closeButton_clicked();
void on_windowTitlebar_doubleClicked();
protected:
virtual void changeEvent(QEvent *event) override;
virtual void mousePressEvent(QMouseEvent *event) override;
virtual void mouseReleaseEvent(QMouseEvent *event) override;
virtual bool eventFilter(QObject *obj, QEvent *event) override;
protected:
virtual void checkBorderDragging(QMouseEvent *event);
private:
bool leftBorderHit(const QPoint &pos);
bool rightBorderHit(const QPoint &pos);
bool topBorderHit(const QPoint &pos);
bool bottomBorderHit(const QPoint &pos);
void updateCursorShape(const QPoint &globalMousePos);
void styleWindow(bool bActive, bool bNoState);
private:
Ui::UFramelessWindow *ui;
QRect m_StartGeometry;
const quint8 CONST_DRAG_BORDER_SIZE = 10;
bool m_bMousePressed{false};
bool m_bDragTop{false};
bool m_bDragLeft{false};
bool m_bDragRight{false};
bool m_bDragBottom{false};
bool m_bResizable{true}; // 是否允许拖拽边框调整大小
};
事件过滤器的逻辑,用于响应鼠标按下和移动逻辑。当鼠标位于UFramelessWindow上,并且满足边界要求时,则允许对界面进行缩放。在checkBorderDragging逻辑中,实现缩放逻辑。
bool UFramelessWindow::eventFilter(QObject *obj, QEvent *event)
{
if (isMaximized() || isFullScreen())
{
return QWidget::eventFilter(obj, event);
}
// check mouse move event when mouse is moved on any object
if (event->type() == QEvent::MouseMove)
{
if (QMouseEvent *pMouse = dynamic_cast<QMouseEvent *>(event))
{
checkBorderDragging(pMouse);
}
}
// press is triggered only on frame window
else if (event->type() == QEvent::MouseButtonPress && obj == this)
{
if (QMouseEvent *pMouse = dynamic_cast<QMouseEvent *>(event))
{
mousePressEvent(pMouse);
}
}
else if (event->type() == QEvent::MouseButtonRelease)
{
if (m_bMousePressed)
{
if (QMouseEvent *pMouse = dynamic_cast<QMouseEvent *>(event))
{
mouseReleaseEvent(pMouse);
}
}
}
return QWidget::eventFilter(obj, event);
}
void UFramelessWindow::checkBorderDragging(QMouseEvent *event)
{
if (!m_bResizable || isMaximized() || isFullScreen())
return;
QPoint globalMousePos = event->globalPos();
if (m_bMousePressed)
{
QRect newGeom = m_StartGeometry;
// 计算水平变化
if (m_bDragLeft)
{
int dx = globalMousePos.x() - m_StartGeometry.left();
newGeom.setLeft(m_StartGeometry.left() + dx);
}
else if (m_bDragRight)
{
int dx = globalMousePos.x() - m_StartGeometry.right();
newGeom.setWidth(m_StartGeometry.width() + dx);
}
// 计算垂直变化
if (m_bDragTop)
{
int dy = globalMousePos.y() - m_StartGeometry.top();
newGeom.setTop(m_StartGeometry.top() + dy);
}
else if (m_bDragBottom)
{
int dy = globalMousePos.y() - m_StartGeometry.bottom();
newGeom.setHeight(m_StartGeometry.height() + dy);
}
// 限制最小尺寸
if (newGeom.width() >= 50 && newGeom.height() >= 50)
{
setGeometry(newGeom);
}
}
else
{
updateCursorShape(globalMousePos);
// 清除拖动标志
m_bDragTop = m_bDragLeft = m_bDragRight = m_bDragBottom = false;
}
}
需要注意的是,由于UFramelessWindow界面是包含padding,因此在最大化需要额外处理逻辑。原始逻辑做了大量的处理,关于边距和高光阴影模糊效果的处理。简化实现为下方,只要最大化时,界面覆盖屏幕即可。
void UFramelessWindow::styleWindow(bool bActive, bool bNoState)
{
// 布局边距
layout()->setMargin(bNoState ? CONST_DRAG_BORDER_SIZE : 0);
}
如何使用
关于该无边框界面窗口的使用方法。可以简单分为3种方法。
方法一:
使用组合方式,使用UFramelessWindow中的方法,将需要无边框界面的类进行添加进来。
// 实际内容窗口
UDemoWgt w;
w.show();
UFramelessWindow helper;
helper.setContent(&w);
helper.show();
helper.setBorderEffectEnable(true);
方法二
方法一在针对一些小型对话框窗口的出现,每次需要配置UFramelessWindow,会有点麻烦。因此提供一种继承的方法。在子界面内部创建界面用于接收ui内容。
class UDemoWgt : public UFramelessWindow
{
Q_OBJECT
public:
explicit UDemoWgt(QWidget *parent = nullptr);
~UDemoWgt();
private:
Ui::UDemoWgt *ui;
};

方法三
方法二每次都需要新建一个界面作用ui的填充界面,相比而言需要耗费额外的资源。可以利用无边框界面中的内容窗口部分windowContent,将需要子界面的ui填充到该界面上。
windowContent界面不能包含布局,需要和方法二进行区分。该方法可行,效果正常。但是未经过多测试,需要注意。
#if 0
// 方法二
QWidget* innerWgt = new QWidget(this);
ui->setupUi(innerWgt);
setContent(innerWgt);
#else
// 方法三, m_pWgtContent在父类中需要赋值为 ui->windowContent;
ui->setupUi(m_pWgtContent);
#endif

总结
该开源项目(无边框黑色主题窗口)(https://github.com/Jorgen-VikingGod/Qt-Frameless-Window-DarkStyle)从效果上看是十分不错的。但是实现感觉有点复杂,因此做了一些简化(导致在拖动上标题栏时,有些区域拖动和移动向冲突,后续还得进行优化,O(∩_∩)O)。
本文来自博客园,作者:Hakuon,转载请注明原文链接:https://www.cnblogs.com/Hakuon/p/19299799
浙公网安备 33010602011771号