偶然看到 web
端某个应用,右下角的错误信息弹窗配合上动画看起来很不错,于是想通过 Qt
自己实现一个类似的功能
初定需求
- 弹窗信息从右下角屏幕外,移动到右下角,停留 3s 左右后,逐渐消失
- 鼠标悬浮在弹窗上时,即使弹窗已经停留了 3s,依旧不能消失, 等鼠标移出弹窗后,逐渐消失
- 弹窗上添加超链接,用户点击的时候,可以有对应操作
- 旧的弹窗信息没有消失时候,出现新的弹窗的话,新弹窗动画方式不变,旧的弹窗向上移动一个弹窗的单位
先定这几个,以后想到可以在额外实现
功能的实现
先放一个最后实现的 gif,之后在逐步解释实现的过程
右下角动画弹出
这个功能大概的逻辑
- 初始化一个弹窗,移动到右下角的屏幕外
- 一个
m_showTimer
定时器,每隔 1 ms, 弹窗向上移动 1 个单位,直到移动到指定位置 - 移动到指定位置后,
m_stayTimer
定时器,每隔 1000 ms 触发一次,3次之后,启动关闭的m_closeTimer
的定时器 m_closeTimer
定时器, 每隔 100 ms, 窗口的透明度 (完全不透明为1) 减少 0.2,直到透明度为 0 时,关闭窗口
messageshow.h
// 窗口最后稳定不动时的坐标
QPoint end_showPoint;
// 因为是从右下角滑出, x 方向是固定的,所以需要记录一下当前窗口的实时 y 坐标
int m_currentHeight;
// 3 个定时器
QTimer* m_showTimer;
QTimer* m_stayTimer;
QTimer* m_closeTimer;
// 记录 m_stayTimer 执行的次数
int m_stayExeTime;
// 记录当前窗口的透明度
double m_transparent;
messageshow.cpp
// 所有定时器对应执行的信号槽
connect(m_showTimer, SIGNAL(timeout()), this, SLOT(slotMsgMove()));
connect(m_stayTimer, SIGNAL(timeout()), this, SLOT(slotMsgStay()));
connect(m_closeTimer, SIGNAL(timeout()), this, SLOT(slotMsgClose()));
// 显示弹窗的初始化函数
void MessageShow::showMessage()
{
m_showTimer->stop();
m_stayTimer->stop();
m_closeTimer->stop();
setWindowOpacity(1);
// 获取当前桌面的位置
QRect desk_rect = QApplication::desktop()->availableGeometry();
// 计算最后呈现的位置
end_showPoint.setX(desk_rect.width() - rect().width());
end_showPoint.setY(desk_rect.height() - rect().height());
// 先将弹窗移动到桌面外
m_currentHeight = desk_rect.height();
move(desk_rect.width(), desk_rect.height());
this->open();
m_showTimer->start(1);
}
// m_showTimer 每次 timeout 后,需要执行的函数
void MessageShow::slotMsgMove()
{
// Qt 默认的坐标系原点是左上角,所以弹窗需要从右下角向上弹出,x 坐标一直不变, y 坐标一直减
m_currentHeight--;
move(end_showPoint.x(), m_currentHeight);
// 如果移动到制定位置后,m_showTimer 定时器停止,开启 m_stayTimer 定时器
if(m_currentHeight <= end_showPoint.y() )
{
m_showTimer->stop();
// m_enterEvent 是为了标识之后的另一个功能,鼠标是否在弹窗内
if (!m_enterEvent)
m_stayTimer->start(1000);
}
}
// m_stayTimer 每次 timeout 后, 需要执行的函数
void MessageShow::slotMsgStay()
{
// 记录 m_stayTimer 运行次数
m_stayExeTime++;
// 当执行了3次,每次1000ms, 也就是停留 3s 之后, 启动 m_closeTimer 定时器
if(m_stayExeTime >= 3)
{
m_stayTimer->stop();
m_closeTimer->start(100);
}
}
// m_closeTimer 每次 timeout 后, 需要执行的函数
void MessageShow::slotMsgClose()
{
// 每次执行 1次后, 透明度减少 0.2, 直到透明度为0后,停止 m_closeTimer, 并关闭窗口
m_transparent -= 0.2;
if(m_transparent <= 0.0)
{
m_closeTimer->stop();
emit sigClose(this);
}
else
{
setWindowOpacity(m_transparent);
}
}
这样,一个完整的从显示,到停留,到最后的消失,代码逻辑就完成
鼠标进入和离开的逻辑
鼠标进入后,弹窗一直停留, 鼠标离开弹窗,停留 1s 后,逐渐淡化消失
void MessageShow::enterEvent(QEvent *event)
{
Q_UNUSED(event);
m_enterEvent = true;
// 弹窗一直停留,停止 m_stayTimer 定时器即可
m_stayTimer->stop();
m_transparent = 1.0;
setWindowOpacity(1.0);
m_closeTimer->stop();
}
void MessageShow::leaveEvent(QEvent *event)
{
Q_UNUSED(event);
m_enterEvent = false;
// 鼠标离开时后, 重新开启 m_stayTimer 定时器
// 因为要停留 1s, 将 m_stayExeTime 设置成已经执行2次, 再运行1次,也就是 1000ms 后,就会开启 m_closeTimer 定时器
m_stayExeTime = 2;
m_stayTimer->start(1000);
}
弹窗上添加超链接
上面介绍了完整的弹窗显示动画逻辑,现在只剩下设置弹窗中的内容设置
messageshow.h
void setInfomation(QString titleInfo, QString msg);
void setInfomation(QString titleInfo, QString msg, QString extraInfo);
messageshow.cpp
// ui 界面里简单用 label_titleInfo 显示 title 内容, label_msg 里显示具体的信息
// 此接口主要显示简单的弹窗内容
void MessageShow::setInfomation(QString titleInfo, QString msg)
{
ui->label_titleInfo->setText(titleInfo);
ui->label_msg->setText(msg);
ui->label_msg->setToolTip(msg);
}
// 弹窗上携带超链接的接口
void MessageShow::setInfomation(QString titleInfo, QString msg, QString extraInfo)
{
ui->label_titleInfo->setText(titleInfo);
ui->label_msg->setText(QString("<a style='color: gray;'href=\"") + extraInfo + QString("\">")+msg);
ui->label_msg->setToolTip(msg);
// void sigClickUrl(QString), 使用弹窗的对象捕获 sigClickUrl 信号,处理用户自定义的超链接
connect(ui->label_msg,SIGNAL(linkActivated(QString)),this,SIGNAL(sigClickUrl(QString)));
}
多个弹窗同时出现的处理
涉及到对多个弹窗同时管理,这里使用 单例 的形式实现一个管理弹窗的管理类 PopupManage
,PopupManage
负责 MessageShow
对象的创建和销毁,现在的主要问题是,如何计算每个弹窗在稳定的时候位置
先简单理一下逻辑
- 第一个弹窗出现时,从右下角向上滑出,滑出到指定位置停留若干秒
- 此时弹出第二个弹窗,第二个弹窗依旧从右下角向上滑出,但是此时第一个弹窗应该继续向上移动一个弹窗的身位
所以,整体的代码逻辑可以这样,使用 PopupManage
创建一个新的 MessageShow
的弹窗, 将 MessageShow
对象存入 QList<MessageShow *> m_popupList
中, 当再次创建新的弹窗时,通知 m_popupList
中所有的 MessageShow
对象, 让它们向上移动一个弹窗的身位
现在看一下简单的代码逻辑
popupmanage.h
class PopupManage : public QObject
{
Q_OBJECT
public:
// 单例实现
static PopupManage * getInstance();
// 创建 MessageShow 对象的接口
void setInfomation(QString titleInfo, QString msg);
void setInfomation(QString titleInfo, QString msg, QString extraInfo);
private slots:
// 创建 MessageShow 对象之后,进行一些额外的操作
void addMessageShow(MessageShow *popup);
// MessageShow 关闭的时候,PopupManage 需要执行的操作
void deleteMessageShow(MessageShow *popup);
// 通知 m_popupList 所有的 MessageShow 的对象
void notifyMessageShow();
signals:
void sigClickUrl(QString);
private:
PopupManage();
~PopupManage();
static PopupManage * m_popupManager;
QList<MessageShow *> m_popupList;
}
popupmanage.cpp
```C++
// 创建 MessageShow 对象后,需要执行的一些操作
void PopupManage::addMessageShow(MessageShow *popup)
{
connect(popup, &MessageShow::sigClose, this, &PopupManage::deleteMessageShow);
connect(popup, &MessageShow::sigClickUrl, this, &PopupManage::sigClickUrl);
m_popupList.append(popup);
this->notifyMessageShow();
}
// MessageShow 关闭的时候,PopupManage 需要执行的操作
void PopupManage::deleteMessageShow(MessageShow *popup)
{
disconnect(popup, &MessageShow::sigClose, this, &PopupManage::deleteMessageShow);
disconnect(popup, &MessageShow::sigClickUrl, this, &PopupManage::sigClickUrl);
m_popupList.removeOne(popup);
delete popup;
popup = nullptr;
}
// 通知所有的 MessageShow 改变位置
void PopupManage::notifyMessageShow()
{
for(int i = 0; i < m_popupList.length();i++){
m_popupList.at(i)->updatePosition();
}
}
PopupManage *PopupManage::getInstance()
{
return m_popupManager;
}
void PopupManage::setInfomation(QString titleInfo, QString msg)
{
MessageShow * popup = new MessageShow();
popup->setInfomation(titleInfo, msg);
addMessageShow(popup);
}
void PopupManage::setInfomation(QString titleInfo, QString msg, QString extraInfo)
{
MessageShow * popup = new MessageShow();
popup->setInfomation(titleInfo, msg, extraInfo);
addMessageShow(popup);
}
这里 MessageShow
有个函数 updatePosition()
, 在创建 MessageShow
对象之后,直接调用了 updatePosition()
, 所有这里的 updatePosition()
一方面的作用是在弹窗第一次显示的 showMessage
功能,另一方面是创建新的弹窗之后的改变位置功能
void MessageShow::updatePosition()
{
// 如果是第一次显示,调用 showMessage 函数, 否则是改变位置
if(m_firstShow){
this->showMessage();
m_firstShow = false;
}else{
end_showPoint.setY(end_showPoint.y()- this->height()-2);
m_currentHeight = m_currentHeight - this->height()-2;
move(end_showPoint.x(), m_currentHeight);
}
}
这里 PopupManage
也可以这样实现
void PopupManage::setInfomation(QString titleInfo, QString msg)
{
MessageShow * popup = new MessageShow();
popup->setInfomation(titleInfo, msg);
connect(popup, &MessageShow::sigClose, this, &PopupManage::deleteMessageShow);
connect(popup, &MessageShow::sigClickUrl, this, &PopupManage::sigClickUrl);
for(int i = 0; i < m_popupList.length();i++){
m_popupList.at(i)->updatePosition();
}
popup->showMessage();
m_popupList.append(popup);
}
这里 updatePosition()
就可以只是改变位置,因为在通知所有 MessageShow
对象改变位置的时候,新创建的对象还没有存储到 m_popupList
中
还可以优化的地方
- 这里没有对弹窗的数量进行控制,可能会出现,弹窗超出界面的现象
- 目前弹窗的样式和布局设置的最简单的方式,也可以进一步改进
一些疑问
不知道有没有注意到这里的弹窗用的显示方法是 this->open()
, 而不是 show()
或者 exec()
exec()
是模态的弹窗,这个肯定直接被 pass 了,不可能让一个弹窗阻塞了主窗口- 当当前焦点的窗口是
QWidget
等非模态窗口时,show()
和open()
这两种显示的方法都可以,整体的弹窗功能都是完整 - 但是当当前焦点窗口是模态窗口时,使用
show()
时,MessageShow
的对象是无法获取鼠标事件事件的,这个我们是懂得,模态窗口实际上是新创建了一个事件循环,MessageShow
的对象的鼠标事件肯定不能获取,但是open
却可以,难道是新的事件循环里将MessageShow
也添加里进来了? 这里自己不是很懂,先给自己挖个新一篇博客的坑,有时间好好研究一些Qt
的事件循环机制
代码地址
github 地址 :https://github.com/catcheroftime/PopMessage