如何使用QWidgets设计一个类似于Web Toast的控件?
如何使用QWidgets设计一个类似于Web Toast的控件?
前言
笔者这段时间沉迷于给我的下位机I.MX6ULL做桌面,这里抽空更新一下QT的东西。这篇文章是跟随CCMoveWidget一样的文章,尝试分享自己如何书写这份代码的思考的过程,和笔者自己反思的不足之处。
核心组件是继承自 QWidget 的 DesktopToast 类,其通过 QLabel 作为消息展示载体,并结合 QPropertyAnimation 完成提示动画效果。该设计首先考虑了窗口的非侵入性,使用 Qt 的无边框、Tool 类型窗口标志,并启用透明背景和非激活显示属性,从而实现一个不打断用户操作、不占用任务栏的浮动消息框
接口设计
相对于上一篇文章的StackWidget_SwitchAnimations而言,这个会好一些,这里笔者设计的接口是这样的:
#ifndef DESKTOPTOAST_H
#define DESKTOPTOAST_H
#include <QPointer>
#include <QWidget>
#include <QQueue>
class QLabel;
class QPropertyAnimation;
class DesktopToast : public QWidget
{Q_OBJECT
public:explicit DesktopToast(QWidget *parent = nullptr);/* enqueue the message */void set_message(const QString& message);
signals:void do_show_toast(QString msg);
private:void adjust_place();void start_animation();void start_close_animation();/* fetch from pool and display */void set_message_impl(const QString& message);QLabel* label;QPoint startPos, endPos;int animation_maintain_msec{500};int wait_time{1000};QPointer<QPropertyAnimation> moveAnimation{nullptr};QPointer<QPropertyAnimation> fadeAnimation{nullptr};bool isHandling{false};/** when large amount of messages smash in,* pools do the job of Buffering the message* warning: Queue itself is not thread safe, add* lock if in multithread*/QQueue<QString> pools;
};#endif // DESKTOPTOAST_H
这里区分几个点:第一个事情是label,这个是信息显示的一个载体,startPos, endPos是用来标记控制我们的Toast的位置的。animation_maintain_msec控制动画的时常,wait_time是稳定的事件消息显示。moveAnimation这个是笔者用来控制出现的动画,fadeAnimation是消失的动画。
#include <QLabel>
#include <QGuiApplication>
#include <QPropertyAnimation>
#include <QScreen>
#include <QTimer>
#include "desktoptoast.h"// Constructor: configure the window flags and label style
DesktopToast::DesktopToast(QWidget *parent): QWidget{parent}
{// Make the window frameless, floating and always on topsetWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);// Enable translucent background for rounded corners and alpha gradientsetAttribute(Qt::WA_TranslucentBackground);// Do not grab focus or activate the windowsetAttribute(Qt::WA_ShowWithoutActivating);// Create the label to display the toast messagelabel = new QLabel(this);// Use gradient background and rounded corners for better UI appearancesetStyleSheet("QLabel {""background: qlineargradient(spread:pad, ""x1:0, y1:0, x2:1, y2:1, ""stop:0 rgba(250, 250, 250, 100), ""stop:1 rgba(230, 230, 230, 100));""border-radius: 10px;""}");// Connect the internal signal to the implementation slotconnect(this, &DesktopToast::do_show_toast,this, &DesktopToast::set_message_impl);
}// Play the entry animation to slide the toast into view
void DesktopToast::start_animation()
{show(); // ensure the widget is visibleif (moveAnimation) {moveAnimation->stop(); // stop any ongoing animationmoveAnimation->deleteLater(); // clean up old animation}// Animate the widget's position from startPos to endPosmoveAnimation = new QPropertyAnimation(this, "pos");moveAnimation->setDuration(animation_maintain_msec);moveAnimation->setStartValue(startPos);moveAnimation->setEndValue(endPos);moveAnimation->setEasingCurve(QEasingCurve::OutCubic); // smooth-out easingmoveAnimation->start(QAbstractAnimation::DeleteWhenStopped);
}// Play the exit animation to slide the toast out and prepare for next message
void DesktopToast::start_close_animation()
{show(); // required to animate out properlyif (fadeAnimation) {fadeAnimation->stop();fadeAnimation->deleteLater();}// Reuse the position animation for simplicity, sliding back to startPosfadeAnimation = new QPropertyAnimation(this, "pos");fadeAnimation->setDuration(animation_maintain_msec);fadeAnimation->setStartValue(endPos);fadeAnimation->setEndValue(startPos);// When animation finishes, hide the widget and check the message queueconnect(fadeAnimation, &QPropertyAnimation::finished, this, [this]() {isHandling = false;hide();// If there are still messages in the queue, show the next oneif (!pools.isEmpty()) {isHandling = true;QString msg = pools.dequeue();emit do_show_toast(msg);}});fadeAnimation->start(QAbstractAnimation::DeleteWhenStopped);
}// Calculate and apply the toast position based on parent or screen geometry
void DesktopToast::adjust_place()
{QWidget* referenceWidget = parentWidget();if (referenceWidget) {// Position relative to parent if availableQRect parentRect = referenceWidget->rect();QPoint topCenter(parentRect.width() / 2 - width() / 2, 30);endPos = referenceWidget->mapToGlobal(topCenter);} else {// Otherwise, position at top center of primary screen
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)QRect screenGeometry = QGuiApplication::primaryScreen()->availableGeometry();
#elseQRect screenGeometry = QApplication::desktop()->availableGeometry();
#endifint screenWidth = screenGeometry.width();int screenX = screenGeometry.x();QPoint topCenter(screenX + (screenWidth - width()) / 2, screenGeometry.top() + 30);endPos = topCenter;}// Slide animation will start from above the final positionstartPos = QPoint(endPos.x(), endPos.y() - 70);move(startPos);
}// Enqueue a message to be displayed; if idle, trigger it immediately
void DesktopToast::set_message(const QString& message)
{pools.enqueue(message);if (!isHandling) {isHandling = true;QString msg = pools.dequeue();emit do_show_toast(msg);}
}// Display the message and start both animations and close timer
void DesktopToast::set_message_impl(const QString& message)
{label->setText(message);label->adjustSize();resize(label->size()); // fit to label sizeadjust_place(); // determine where to show the toastshow();raise(); // bring on top of sibling widgetsstart_animation(); // enter animation// Start close animation after wait time + animation durationQTimer::singleShot(wait_time + animation_maintain_msec, this, &DesktopToast::start_close_animation);
}
这里的QString 队列 pools 实现了简单但实用的消息缓冲机制,使该提示框具备顺序展示大量消息的能力,并在注释中清楚地提醒了其非线程安全性。
为了避免动画冲突,我是用 QPointer 包装动画对象,在启动前清理旧动画,确保每一次动画都是全新的过程,并用 isHandling 标志位控制消息的串行处理。位置信息由 adjust_place 函数动态调整,无论是否有父窗口,这样总是居中定位在屏幕上方;而实际的展示逻辑通过 set_message_impl 驱动,该函数不仅设置 QLabel 文本并自适应尺寸,还协调动画播放和自动关闭,呈现出一种自然的消息过渡过程。整个机制通过信号 do_show_toast 解耦用户接口与实际执行逻辑,使 set_message 可以无阻塞地写入消息,而真正的展示交给内部状态控制来完成。
演示一下: