Qt开源无边框窗口项目解析

Qt开源无边框窗口项目解析


《西江月·夜行黄沙道中》 -- 辛弃疾

明月别枝惊鹊,清风半夜鸣蝉。
稻花香里说丰年,听取蛙声一片。

七八个星天外,两三点雨山前。
旧时茅店社林边,路转溪桥忽见。

Yukiro

无边框窗口的来源

无边框窗口指的是移除了操作系统提供的默认标题栏、边框和窗口控件的窗口。这种窗口形式为应用程序提供了完全的界面自定义能力,开发者可以:

  1. 设计独特的窗口样式和布局
  2. 实现自定义的窗口拖动和调整大小逻辑
  3. 创建与应用程序风格完全一致的标题栏控件
  4. 支持非矩形窗口形状(如圆角窗口、异形窗口)

本文是对开源项目无边框黑色主题窗口(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);
}

image-20251202230928912

拖动窗口

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;
};

核心功能实现:
一般而言,这个界面是作为无边框界面的标题栏,因此,需要通过父对象来获取无边框整体窗口。
界面移动逻辑的计算为:

  1. 需要判断无边框窗口(父窗口)的有效性,同时当处于最大化和全屏幕时,禁止移动。
  2. 逻辑:假设左上角坐标为(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;
};

image-20251203000455963

方法三

方法二每次都需要新建一个界面作用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

image-20251203001025665

总结

该开源项目(无边框黑色主题窗口)(https://github.com/Jorgen-VikingGod/Qt-Frameless-Window-DarkStyle)从效果上看是十分不错的。但是实现感觉有点复杂,因此做了一些简化(导致在拖动上标题栏时,有些区域拖动和移动向冲突,后续还得进行优化,O(∩_∩)O)。

posted on 2025-12-03 00:41  Hakuon  阅读(0)  评论(0)    收藏  举报