Qt-自定义右下角提示信息弹窗

偶然看到 web 端某个应用,右下角的错误信息弹窗配合上动画看起来很不错,于是想通过 Qt 自己实现一个类似的功能

初定需求

  1. 弹窗信息从右下角屏幕外,移动到右下角,停留 3s 左右后,逐渐消失
  2. 鼠标悬浮在弹窗上时,即使弹窗已经停留了 3s,依旧不能消失, 等鼠标移出弹窗后,逐渐消失
  3. 弹窗上添加超链接,用户点击的时候,可以有对应操作
  4. 旧的弹窗信息没有消失时候,出现新的弹窗的话,新弹窗动画方式不变,旧的弹窗向上移动一个弹窗的单位

先定这几个,以后想到可以在额外实现

功能的实现

先放一个最后实现的 gif,之后在逐步解释实现的过程

创建

右下角动画弹出

这个功能大概的逻辑

  1. 初始化一个弹窗,移动到右下角的屏幕外
  2. 一个 m_showTimer 定时器,每隔 1 ms, 弹窗向上移动 1 个单位,直到移动到指定位置
  3. 移动到指定位置后,m_stayTimer 定时器,每隔 1000 ms 触发一次,3次之后,启动关闭的 m_closeTimer 的定时器
  4. 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)));
}

多个弹窗同时出现的处理

涉及到对多个弹窗同时管理,这里使用 单例 的形式实现一个管理弹窗的管理类 PopupManagePopupManage 负责 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