实现效果
这次制作的桌面弹窗,主要功能有透明度和位置动画、定时关闭。
实现细节
首先,我们需要获取桌面大小,然后 move 到右下角去,这里借助的 QScreen:
QScreen * screen = QGuiApplication::primaryScreen();
const QRect desk_rect = screen->availableGeometry();
对于动画,我用的 QPropertyAnimation 属性动画配合动画组。然后根据设置来决定启用哪些动画、是否定时关闭等。
还有要注意的是模态框的问题,如果有一个 ApplicationModal 的窗口显示了,就没法点击其他窗口。而且关闭模态窗口时,可能会导致主窗口跑到其他窗口后面去了。这里我时先设置为 WindowModal ,关闭时再设置程 NoModal 非模态。
目前还没有做鼠标放在上面不自动关闭的功能;目前的实现只有单个弹窗实例,可自行修改为允许多个弹框同时存在。
实现代码
完整代码链接(CuteDesktopTip类):https://github.com/gongjianbo/QtWidgetsComponent
主要实现代码:
#pragma once
#include <QDialog>
#include <QPropertyAnimation>
#include <QParallelAnimationGroup>
#include <QTimer>
#include "CuteComponentExport.h"
#include "CuteDesktopTipForm.h"
/**
* @brief 桌面弹框
* @author 龚建波
* @date 2020-6-28
* @note
* 可以增加 show 的 parent 参数,使模态有父子关系
* 目前没有样式设置接口,可以增加一个 setForm 接口设置内部窗口
* @details
* show 时创建,close 时释放
* 分为外层透明区域限定位置,和内层窗口样式
*/
class Cute_API CuteDesktopTip : public QDialog
{
Q_OBJECT
public:
// 动画模式枚举
enum AnimationMode
{
// 无动画
NoAnimation = 0x00,
// 仅透明度动画
OpacityAnimation = 0x01,
// 仅位置动画
PosAnimation = 0x02,
// 全部动画
AllAnimation = 0xFF
};
// 显示位置枚举
enum DisplayArea
{
// 当前窗口所在屏幕居中
CenterArea = 0x00,
// 主屏幕右下角
RightBottomArea = 0x01
};
private:
explicit CuteDesktopTip(QWidget *parent = nullptr);
public:
~CuteDesktopTip();
// 设置动画模式
static CuteDesktopTip::AnimationMode getMode();
static void setMode(CuteDesktopTip::AnimationMode mode);
// 设置显示位置
static CuteDesktopTip::DisplayArea getArea();
static void setArea(CuteDesktopTip::DisplayArea area);
// 显示弹框 -已显示动画重新开始, timeout <= 0 不会定时消失
static void showTip(const QString &title, const QString &text, int timeout = 0);
// 显示弹框 -已显示不重复动画
static void keepTip(const QString &title, const QString &text);
// 隐藏弹框
static void hideTip();
private:
// 初始化动画设置
void initAnimation();
// 初始化定时器设置
void initTimer();
// 准备定时器
void readyTimer(int timeout);
// 启动显示动画-已显示动画重新开始
void showAnimation();
// 启动显示动画-已显示不重复动画
void keepAnimation();
// 启动隐藏动画
void hideAnimation();
// 显示的标题和文本
void setText(const QString &title, const QString &text);
private:
// form 为独立的 widget,封装 ui 相关接口
CuteDesktopTipForm *form{ nullptr };
// 单例
static CuteDesktopTip *instance;
// 动画设置
static AnimationMode aniMode;
// 显示位置
static DisplayArea aniArea;
// 动画组
QParallelAnimationGroup *aniGroup{ nullptr };
// 透明度属性动画
QPropertyAnimation *aniOpacity{ nullptr };
// 位置属性动画
QPropertyAnimation *aniPos{ nullptr };
// 动画结束标志,因为每次都会创建新的弹框,所以默认 false 表示正在动画
bool showAniEnd{ false };
// 定时关闭
QTimer *hideTimer{ nullptr };
// 定时计数
int hideCount{ 0 };
};
#include "CuteDesktopTip.h"
#include <QApplication>
#include <QScreen>
//#include <QDebug>
CuteDesktopTip *CuteDesktopTip::instance = nullptr;
CuteDesktopTip::AnimationMode CuteDesktopTip::aniMode = CuteDesktopTip::AllAnimation;
CuteDesktopTip::DisplayArea CuteDesktopTip::aniArea = CuteDesktopTip::RightBottomArea;
CuteDesktopTip::CuteDesktopTip(QWidget *parent)
: QDialog{parent}
, form{new CuteDesktopTipForm(this)}
, aniGroup{new QParallelAnimationGroup(this)}
{
setWindowFlags(Qt::FramelessWindowHint | Qt::ToolTip);
setAttribute(Qt::WA_TranslucentBackground);
setAttribute(Qt::WA_DeleteOnClose);
// setWindowModality(Qt::WindowModal);
// resize 会被 label 撑开
this->setFixedSize(310,210);
form->setFixedSize(310,210);
// 关闭
QPushButton *btn_close = form->findChild<QPushButton *>("close");
if (btn_close) {
connect(btn_close, &QPushButton::clicked, this, &CuteDesktopTip::hideTip);
}
// 程序退出时释放
connect(qApp, &QApplication::aboutToQuit, this, &CuteDesktopTip::close);
// 动画设置
initAnimation();
// 定时器设置
initTimer();
// qDebug() << __FUNCTION__;
}
CuteDesktopTip::~CuteDesktopTip()
{
// qDebug() << __FUNCTION__;
}
CuteDesktopTip::AnimationMode CuteDesktopTip::getMode()
{
return aniMode;
}
void CuteDesktopTip::setMode(AnimationMode mode)
{
if (aniMode != mode) {
aniMode = mode;
}
}
CuteDesktopTip::DisplayArea CuteDesktopTip::getArea()
{
return aniArea;
}
void CuteDesktopTip::setArea(DisplayArea area)
{
if (aniArea != area) {
aniArea = area;
}
}
void CuteDesktopTip::showTip(const QString &title, const QString &text, int timeout)
{
if (!instance) {
// 仅在ui线程
instance = new CuteDesktopTip;
}
instance->readyTimer(timeout);
// 模态框
instance->setWindowModality(Qt::WindowModal);
instance->setText(title, text);
instance->showAnimation();
}
void CuteDesktopTip::keepTip(const QString &title, const QString &text)
{
if (!instance) {
// 仅在ui线程
instance = new CuteDesktopTip;
}
instance->readyTimer(0);
// 模态框
instance->setWindowModality(Qt::WindowModal);
instance->setText(title, text);
instance->keepAnimation();
}
void CuteDesktopTip::hideTip()
{
if (!instance) {
return;
}
instance->hideAnimation();
}
void CuteDesktopTip::initAnimation()
{
// 透明度动画,窗口才能设置透明度
aniOpacity = new QPropertyAnimation(this, "windowOpacity");
// 判断是否设置了此模式的动画
if (aniMode & AnimationMode::OpacityAnimation) {
aniOpacity->setDuration(1500);
aniOpacity->setStartValue(0);
} else {
aniOpacity->setDuration(0);
aniOpacity->setStartValue(1);
}
aniOpacity->setEndValue(1);
aniGroup->addAnimation(aniOpacity);
// 位置动画
aniPos = new QPropertyAnimation(form, "pos");
// 右下角放主屏,居中默认在当前窗口所在屏幕
if (aniArea == DisplayArea::RightBottomArea) {
QScreen *screen = QGuiApplication::primaryScreen();
if (screen) {
QRect rect = screen->availableGeometry();
move(rect.right() - width(), rect.bottom() - height());
}
}
const QPoint hide_pos{0, form->height()};
const QPoint show_pos{0, 0};
// 判断是否设置了此模式的动画
if (aniMode & AnimationMode::PosAnimation){
aniPos->setDuration(1500);
aniPos->setStartValue(hide_pos);
} else {
aniPos->setDuration(0);
aniPos->setStartValue(show_pos);
}
aniPos->setEndValue(show_pos);
aniGroup->addAnimation(aniPos);
//
connect(aniGroup, &QParallelAnimationGroup::finished, [this]{
// back 消失动画结束关闭窗口
if (aniGroup->direction() == QAbstractAnimation::Backward) {
// Qt::WA_DeleteOnClose 后手动设置为 nullptr
instance = nullptr;
qApp->disconnect(this);
// 关闭时设置为非模态,方式主窗口被遮挡,待测试
this->setWindowModality(Qt::NonModal);
this->close();
} else {
// 配合 keepAnimation
showAniEnd = true;
//配合定时关闭
if (hideCount > 0) {
hideTimer->start();
}
}
});
}
void CuteDesktopTip::initTimer()
{
hideTimer = new QTimer(this);
// 1s 间隔
hideTimer->setInterval(1000);
connect(hideTimer, &QTimer::timeout, [this]{
QPushButton *btn_close = form->findChild<QPushButton *>("close");
if (hideCount > 1) {
hideCount--;
if (btn_close) {
btn_close->setText(QString("%1 S").arg(hideCount));
}
} else {
if (btn_close) {
btn_close->setText("Close");
}
hideTimer->stop();
hideTip();
}
});
}
void CuteDesktopTip::readyTimer(int timeout)
{
// 先设置,在显示动画结束再 start 开始计时器
hideCount = timeout;
hideTimer->stop();
QPushButton *btn_close = form->findChild<QPushButton *>("close");
if (hideCount > 0) {
if (btn_close) {
btn_close->setText(QString("%1 S").arg(hideCount));
}
} else {
if (btn_close) {
btn_close->setText("Close");
}
}
}
void CuteDesktopTip::showAnimation()
{
aniGroup->setDirection(QAbstractAnimation::Forward);
// 停止正在进行的动画重新
if (aniGroup->state() == QAbstractAnimation::Running) {
aniGroup->stop();
}
aniGroup->start();
show();
}
void CuteDesktopTip::keepAnimation()
{
// show 没有完成,或者正在动画中才进入
if (!showAniEnd || aniGroup->state()!=QAbstractAnimation::Stopped) {
aniGroup->setDirection(QAbstractAnimation::Forward);
aniGroup->start();
show();
}
}
void CuteDesktopTip::hideAnimation()
{
// Backward 反向执行动画
aniGroup->setDirection(QAbstractAnimation::Backward);
aniGroup->start();
}
void CuteDesktopTip::setText(const QString &title, const QString &text)
{
QLabel *title_label = form->findChild<QLabel *>("title");
if (title_label) {
title_label->setText(title);
}
QLabel *content_label = form->findChild<QLabel *>("content");
if (content_label) {
QString tip_text("<p style='line-height:120%'>");
QStringList tip_list = text.split("\n");
for (const QString &line : tip_list)
{
if (line.isEmpty())
continue;
tip_text += line + "<br>";
}
tip_text += "</p>";
content_label->setText(tip_text);
}
}
使用方式:
void ToolTipDemo::initDesktopTip()
{
// 选择启用的动画
ui->boxDesktopTipAni->setView(new QListView(this));
// 第一项用默认值
ui->boxDesktopTipAni->addItems({"All", "No", "Opacity", "Pos"});
connect(ui->boxDesktopTipAni, QOverload<int>::of(&QComboBox::currentIndexChanged),
[](int index){
switch (index) {
case 0: CuteDesktopTip::setMode(CuteDesktopTip::AllAnimation); break;
case 1: CuteDesktopTip::setMode(CuteDesktopTip::NoAnimation); break;
case 2: CuteDesktopTip::setMode(CuteDesktopTip::OpacityAnimation); break;
case 3: CuteDesktopTip::setMode(CuteDesktopTip::PosAnimation); break;
default: break;
}
});
// 选择弹出位置
ui->boxDesktopTipArea->setView(new QListView(this));
// 第一项用默认值
ui->boxDesktopTipArea->addItems({"RightBottom", "Center"});
connect(ui->boxDesktopTipArea, QOverload<int>::of(&QComboBox::currentIndexChanged),
[](int index){
switch (index) {
case 0: CuteDesktopTip::setArea(CuteDesktopTip::RightBottomArea); break;
case 1: CuteDesktopTip::setArea(CuteDesktopTip::CenterArea); break;
default: break;
}
});
connect(ui->btnDesktopTipShow, &QPushButton::clicked, [=]{
//只显示 n 秒就消失
CuteDesktopTip::showTip("测试 showTip"
, "这是 1 条信息\n这是 1 条信息\n这是 1 条信息\n这是 1 条信息"
, 5);
});
connect(ui->btnDesktopTipKeep, &QPushButton::clicked, [=]{
// 一直显示,直到点关闭
CuteDesktopTip::keepTip("测试 keepTip"
, "这是 1 条信息\n这是 1 条信息\n这是 1 条信息\n这是 1 条信息");
});
connect(ui->btnDesktopTipHide, &QPushButton::clicked, [=]{
CuteDesktopTip::hideTip();
});
}