1. 目标
我们目标是做一个在各个平台上样式高度统一的软件,所以最好的选择是自定义一个基本窗口(QWidget),去掉平台附加的平台样式(边框、按钮等),最后的成品需要具备以下特性:
-
无边框
-
窗口阴影
-
自定义标题栏
-
可拖动
2. 实现
创建一个basewidget模块,包含一个继承QWidget的BaseWidget类:
basewidget
- basewidget.pri
- basewidget.h
- basewidget.cpp
2.1 无边框
这个实现比较简单,在构造函数设置windowFlag就行
BaseWidget::BaseWidget(QWidget *parent) : QWidget{parent} {
setWindowFlags(Qt::FramelessWindowHint);
}
2.2 窗口阴影
所有的无边框的窗口阴影实现方案都需要将最下面的widget背景透明:
setAttribute(Qt::WA_TranslucentBackground);
有些方案需要预留出绘制阴影的margin(比如预留8px):
setContentsMargins(8, 8, 8, 8);
之后绘制阴影有以下几种方法:
-
预留margin,在paintEvent中用贴图的方式绘制四周阴影(需要一张渐变png)
-
预留margin,在paintEvent或qss中用渐变色绘制阴影
-
无需预留margin,使用QGraphicsDropShadowEffect设置阴影
这里我们采用第三种方案(因为足够简单)。
首先添加一个带ui文件的QWidget类(Qt 设计器界面类),继承BaseWidget类:
#pragma once
#include <basewidget/basewidget.h>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public BaseWidget {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
};
在ui上添加一个QFrame,叫做container吧,设置布局,留出margin:
然后设置阴影:
MainWindow::MainWindow(QWidget *parent) : BaseWidget(parent), ui(new Ui::MainWindow) {
ui->setupUi(this);
QGraphicsDropShadowEffect *shadow = new QGraphicsDropShadowEffect(this);
shadow->setOffset(0, 0);
shadow->setColor(Qt::gray);
shadow->setBlurRadius(32);
ui->container->setGraphicsEffect(shadow);
}
阴影效果就出来了:
在部分linux发行版上,透明效果可能会被系统关闭(比如统信上可以关闭“窗口特效”),此时只能去除阴影效果。
2.3 自定义标题栏
一般标题栏就是左侧logo图片+title,右侧最小化/最大化/还原/关闭按钮,按需添加即可。这里有一个需要注意的事项,当窗口在最大化/非最大化之间切换时,最大化/还原按钮需要进行切换,可以在最大化/还原按钮的点击槽函数中进行判断处理,但是稍显麻烦,比如:
void MainWindow::maxBtnClicked() {
showMaximized();
ui->maxBtn->hide();
ui->restoreBtn->show();
}
void MainWindow::restoreBtnClicked() {
showNormal();
ui->maxBtn->show();
ui->restoreBtn->hide();
}
我们可以利用QWidget的resizeEvent重载函数来实现这一需求:
MainWindow::MainWindow(QWidget *parent) : BaseWidget(parent), ui(new Ui::MainWindow) {
...
connect(ui->maxBtn, &QPushButton::clicked, this, &MainWindow::showMaximized);
connect(ui->restoreBtn, &QPushButton::clicked, this, &MainWindow::showNormal);
}
void MainWindow::resizeEvent(QResizeEvent *ev) {
Q_UNUSED(ev)
ui->maxBtn->setHidden(isMaximized());
ui->restoreBtn->setHidden(!isMaximized());
}
另外最大化时,窗口阴影一般要关闭,也可以在这里处理。
2.4 可拖动
安装窗口拖动这个需求可以通过mousePressEvent、mouseMoveEvent和mouseReleaseEvent这三个重载函数实现:
-
定义一个QPoint类型成员变量m_pos,在鼠标按下时(mousePressEvent)记录点击坐标,并设置按下状态为true
-
鼠标移动时(mouseMoveEvent),计算移动距离,用move设置窗口位置
-
鼠标松开时(mouseReleaseEvent),取消按下状态
这一部分比较通用,我们写在BaseWidget里:
// basewidget.h
class BaseWidget : public QWidget {
Q_OBJECT
public:
explicit BaseWidget(QWidget *parent = nullptr);
protected:
void mousePressEvent(QMouseEvent *ev) override;
void mouseMoveEvent(QMouseEvent *ev) override;
void mouseReleaseEvent(QMouseEvent *ev) override;
private:
QPoint m_pos;
bool m_pressed = false;
};
// basewidget.cpp
void BaseWidget::mousePressEvent(QMouseEvent* ev) {
if ((Qt::LeftButton == ev->button()) && (!isMaximized())) {
m_pressed = true;
m_pos = ev->pos();
}
QWidget::mousePressEvent(ev);
}
void BaseWidget::mouseMoveEvent(QMouseEvent* ev) {
if (m_pressed) {
move(ev->pos() - m_pos + pos());
}
QWidget::mouseMoveEvent(ev);
}
void BaseWidget::mouseReleaseEvent(QMouseEvent* ev) {
m_pressed = false;
QWidget::mouseReleaseEvent(ev);
}
PS: 代码已经开源在github:linqiaqun/music-player: A cross platform music player (github.com) 欢迎star/fork/issue