【Qt】可爱的窗口关闭确认弹窗实现
文章目录
- 实现思路
- 界面构建
- 交互逻辑实现
- 颜色渐变处理
- 圆形部件绘制
- 代码
- 在主窗口的构造函数中创建弹窗实例
- ExitConfirmDialog 类代码
- ColorCircleWidget 类代码

今天在Qt实现了这样一个可互动的窗口(上图由于录屏工具限制没有录制到鼠标)
实现思路
实现这样一个可爱的退出确认弹窗,整体思路是结合多个组件和交互逻辑来打造具有吸引力和交互性的界面。具体如下:
界面构建
- 布局设计:在
ExitConfirmDialog
类的构造函数中,使用多种布局(如QVBoxLayout
和QHBoxLayout
)将各个组件有序排列。首先创建提示信息标签显示询问文本,然后添加圆形部件ColorCircleWidget
作为视觉焦点,最后设置“确认退出”和“取消”两个按钮,方便用户进行操作选择。 - 组件创建与属性设置:对每个组件进行个性化设置,如调整标签的字体大小和对齐方式,设置按钮的文本内容,固定圆形部件的大小等,同时为部分组件设置鼠标穿透属性,避免影响整体交互体验。
交互逻辑实现
- 按钮点击响应:使用
connect
函数将按钮的点击信号与相应的槽函数连接。当点击“确认退出”按钮时,触发onRightButtonClicked
槽函数,发出rightButtonClicked
信号并接受对话框;点击“取消”按钮时,直接拒绝对话框。 - 鼠标交互效果:通过重写
mouseMoveEvent
、enterEvent
和leaveEvent
事件处理函数,实现与鼠标的交互效果。当鼠标移动或进入对话框时,调用updateCircleGradientBasedOnMouse
函数根据鼠标位置更新圆形部件的渐变颜色和嘴巴表情,同时更新眼珠位置,使圆形部件更生动;当鼠标离开对话框时,将圆形部件的渐变颜色、嘴巴表情和眼珠位置恢复到初始状态。
颜色渐变处理
- 获取渐变颜色:定义
getColorFromGradient
函数,用于从线性渐变中获取指定位置的颜色。通过遍历渐变的颜色停靠点,计算指定位置的颜色值,实现颜色的平滑过渡。 - 混合渐变颜色:在
updateCircleGradientBasedOnMouse
函数中,根据鼠标到两个按钮的距离计算权重,对权重进行平滑处理后,根据权重混合不同的渐变颜色,生成最终的渐变效果,并应用到圆形部件上。
圆形部件绘制
- 初始化与属性设置:在
ColorCircleWidget
类的构造函数中,初始化圆形部件的基本属性,如嘴巴表情、渐变颜色等,并开启鼠标跟踪功能。 - 绘制图形:重写
paintEvent
函数,使用QPainter
绘制圆形、眼睛、眼珠和嘴巴等图形元素。根据当前的渐变颜色填充圆形,根据嘴巴表情调整嘴巴宽度,根据鼠标位置更新眼珠位置,实现动态绘制效果。
代码
在主窗口的构造函数中创建弹窗实例
// 拦截默认关闭事件
_closeDialog = new ExitConfirmDialog(this);
_closeDialog->setFixedSize(400,300);
connect(_closeDialog, &ExitConfirmDialog::rightButtonClicked, this, &MainWindow::closeWindow);
this->setIsDefaultClosed(false);
connect(this, &MainWindow::closeButtonClicked, this, [=]() {
_closeDialog->exec();
});
ExitConfirmDialog 类代码
#ifndef EXITCONFIRMDIALOG_H
#define EXITCONFIRMDIALOG_H
#include <QDialog>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include "ColorCircleWidget.h"
class ExitConfirmDialog : public QDialog
{
Q_OBJECT
public:
explicit ExitConfirmDialog(QWidget *parent = nullptr);
~ExitConfirmDialog();
signals:
void rightButtonClicked();
protected:
void mouseMoveEvent(QMouseEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
private slots:
void onRightButtonClicked();
private:
QLabel *messageLabel;
QPushButton *rightButton;
QPushButton *cancelButton;
ColorCircleWidget *circleWidget;
void updateCircleGradientBasedOnMouse();
QColor getColorFromGradient(const QLinearGradient& gradient, double position);
};
#endif // EXITCONFIRMDIALOG_H
#include "ExitConfirmDialog.h"
#include <QPalette>
#include <QSpacerItem>
#include <QLinearGradient>
#include <QDebug>
#include <QEasingCurve>
#include <QMouseEvent>
ExitConfirmDialog::ExitConfirmDialog(QWidget *parent) : QDialog(parent)
{
setWindowTitle("确认退出");
// 创建提示信息标签
messageLabel = new QLabel("确定要退出应用程序吗?", this);
messageLabel->setAlignment(Qt::AlignHCenter);
QFont font = messageLabel->font();
font.setPointSize(18);
messageLabel->setFont(font);
messageLabel->setAttribute(Qt::WA_TransparentForMouseEvents); // 设置鼠标穿透
// 创建按钮
rightButton = new QPushButton("确认退出", this);
cancelButton = new QPushButton("取消", this);
// 连接按钮点击信号到相应槽函数
connect(rightButton, &QPushButton::clicked, this, &ExitConfirmDialog::onRightButtonClicked);
connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
// 创建圆形部件
circleWidget = new ColorCircleWidget(this);
circleWidget->setFixedSize(150, 150);
circleWidget->setAttribute(Qt::WA_TransparentForMouseEvents); // 设置鼠标穿透
// 创建主布局
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(messageLabel, 0, Qt::AlignTop | Qt::AlignHCenter);
// 创建圆形布局
QVBoxLayout *circleLayout = new QVBoxLayout;
circleLayout->addStretch();
circleLayout->addWidget(circleWidget, 0, Qt::AlignHCenter);
circleLayout->addStretch();
mainLayout->addLayout(circleLayout);
// 创建按钮布局
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(cancelButton);
buttonLayout->addStretch();
buttonLayout->addWidget(rightButton);
mainLayout->addLayout(buttonLayout);
setLayout(mainLayout);
// 设置鼠标跟踪
setMouseTracking(true);
rightButton->setMouseTracking(true);
cancelButton->setMouseTracking(true);
circleWidget->setMouseTracking(true);
}
ExitConfirmDialog::~ExitConfirmDialog()
{
// 析构函数,这里没有动态分配的资源需要手动释放,Qt会自动处理
}
// 从渐变中获取指定位置的颜色
QColor ExitConfirmDialog::getColorFromGradient(const QLinearGradient& gradient, double position)
{
position = qBound(0.0, position, 1.0);
QGradientStops stops = gradient.stops();
for (int i = 0; i < stops.size() - 1; ++i) {
if (position >= stops[i].first && position < stops[i + 1].first) {
double ratio = (position - stops[i].first) / (stops[i + 1].first - stops[i].first);
QColor startColor = stops[i].second;
QColor endColor = stops[i + 1].second;
int red = static_cast<int>(startColor.red() + (endColor.red() - startColor.red()) * ratio);
int green = static_cast<int>(startColor.green() + (endColor.green() - startColor.green()) * ratio);
int blue = static_cast<int>(startColor.blue() + (endColor.blue() - startColor.blue()) * ratio);
return QColor(red, green, blue);
}
}
return stops.back().second;
}
// 鼠标移动事件处理
void ExitConfirmDialog::mouseMoveEvent(QMouseEvent *event)
{
updateCircleGradientBasedOnMouse();
QPointF localPos = circleWidget->mapFromGlobal(QCursor::pos());
circleWidget->updatePupilPosition(localPos);
QDialog::mouseMoveEvent(event);
}
// 鼠标进入事件处理
void ExitConfirmDialog::enterEvent(QEnterEvent *event)
{
updateCircleGradientBasedOnMouse();
QPointF localPos = circleWidget->mapFromGlobal(QCursor::pos());
circleWidget->updatePupilPosition(localPos);
QDialog::enterEvent(event);
}
// 鼠标离开事件处理
void ExitConfirmDialog::leaveEvent(QEvent *event)
{
QLinearGradient yellowGradient(0, 0, 0, circleWidget->height());
yellowGradient.setColorAt(0, QColor(243, 203, 130));
yellowGradient.setColorAt(1, QColor(243, 203, 130));
circleWidget->setGradient(yellowGradient);
circleWidget->setMouthExpression(1);
circleWidget->resetPupilPosition(); // 眼珠复原
QDialog::leaveEvent(event);
}
// 根据鼠标位置更新圆形部件的渐变
void ExitConfirmDialog::updateCircleGradientBasedOnMouse()
{
// 检查鼠标是否在对话框内,如果不在则直接返回
if (!underMouse()) {
return;
}
// 获取鼠标在对话框内的全局位置
QPoint mousePos = mapFromGlobal(QCursor::pos());
// 获取取消按钮的中心点位置
QPoint cancelCenter = cancelButton->geometry().center();
// 获取确认退出按钮的中心点位置
QPoint rightCenter = rightButton->geometry().center();
// 计算鼠标位置到取消按钮中心点的距离
double distToCancel = QLineF(mousePos, cancelCenter).length();
// 计算鼠标位置到确认退出按钮中心点的距离
double distToRight = QLineF(mousePos, rightCenter).length();
// 计算对话框对角线的长度,作为最大距离
double maxDist = std::sqrt(std::pow(width(), 2) + std::pow(height(), 2));
// 对鼠标到取消按钮的距离进行归一化处理,确保值在 0 到 1 之间
double normalizedDistToCancel = qMin(distToCancel / maxDist, 1.0);
// 对鼠标到确认退出按钮的距离进行归一化处理,确保值在 0 到 1 之间
double normalizedDistToRight = qMin(distToRight / maxDist, 1.0);
// 计算取消按钮的权重,距离越近权重越大
double weightCancel = 0.70 - normalizedDistToCancel;
// 计算确认退出按钮的权重,距离越近权重越大
double weightRight = 0.70 - normalizedDistToRight;
// 计算总权重
double totalWeight = weightCancel + weightRight;
// 如果总权重大于 0,则对两个按钮的权重进行归一化处理
if (totalWeight > 0) {
weightCancel /= totalWeight;
weightRight /= totalWeight;
}
// 使用 InOutQuad 曲线对取消按钮的权重进行平滑处理
QEasingCurve easing(QEasingCurve::InOutQuad);
double smoothWeightCancel = easing.valueForProgress(weightCancel);
// 根据平滑后的取消按钮权重设置圆形部件的嘴巴表情
circleWidget->setMouthExpression(smoothWeightCancel);
// 如果平滑后的取消按钮权重超过 0.8,则将圆形部件的渐变设置为纯黄色
if (smoothWeightCancel > 0.80) {
QLinearGradient yellowGradient(0, 0, 0, circleWidget->height());
yellowGradient.setColorAt(0, QColor(243, 203, 130));
yellowGradient.setColorAt(1, QColor(243, 203, 130));
circleWidget->setGradient(yellowGradient);
return;
}
// 创建一个绿黄渐变对象
QLinearGradient greenYellowGradient(0, 0, 0, circleWidget->height());
greenYellowGradient.setColorAt(0, QColor(118, 174, 141));
greenYellowGradient.setColorAt(0.8, QColor(243, 203, 130));
greenYellowGradient.setColorAt(1, QColor(243, 203, 130));
// 创建一个混合渐变对象,用于存储最终的渐变
QLinearGradient mixedGradient(0, 0, 0, circleWidget->height());
// 遍历渐变的起始和结束位置(0 到 1)
for (int y = 0; y <= 1; y += 1) {
// 定义左侧颜色为黄色
QColor leftColor = QColor(243, 203, 130);
// 从绿黄渐变中获取指定位置的颜色
QColor rightColor = getColorFromGradient(greenYellowGradient, y);
// 根据平滑后的取消按钮权重对左右颜色进行混合
QColor mixedColor(
qBound(0, int(leftColor.red() * smoothWeightCancel + rightColor.red() * (1 - smoothWeightCancel)), 255),
qBound(0, int(leftColor.green() * smoothWeightCancel + rightColor.green() * (1 - smoothWeightCancel)), 255),
qBound(0, int(leftColor.blue() * smoothWeightCancel + rightColor.blue() * (1 - smoothWeightCancel)), 255)
);
// 将混合后的颜色设置到混合渐变对象的指定位置
mixedGradient.setColorAt(y, mixedColor);
}
// 将最终的混合渐变设置到圆形部件上
circleWidget->setGradient(mixedGradient);
}
// 确认退出按钮点击事件处理
void ExitConfirmDialog::onRightButtonClicked()
{
emit rightButtonClicked();
accept();
}
ColorCircleWidget 类代码
#ifndef COLORCIRCLEWIDGET_H
#define COLORCIRCLEWIDGET_H
#include <QWidget>
#include <QGradient>
#include <QPointF>
class ColorCircleWidget : public QWidget
{
Q_OBJECT
public:
explicit ColorCircleWidget(QWidget *parent = nullptr);
~ColorCircleWidget();
void setMouthExpression(qreal expression);
void setGradient(const QGradient &gradient);
void updatePupilPosition(const QPointF& mousePos);
void resetPupilPosition();
protected:
void paintEvent(QPaintEvent *event) override;
private:
QGradient m_gradient;
qreal m_mouthExpression;
int mouthWidth;
int mouthHeight;
int originalMouthWidth;
bool isFirstOpen = true;
QPointF leftEyeCenter;
QPointF rightEyeCenter;
QPointF leftPupilPos;
QPointF rightPupilPos;
int eyeRadius;
};
#endif // COLORCIRCLEWIDGET_H
#include "ColorCircleWidget.h"
#include <QPainter>
// 构造函数,初始化 ColorCircleWidget 对象
ColorCircleWidget::ColorCircleWidget(QWidget *parent) : QWidget(parent),
// 初始化嘴巴表情,范围在 0 到 1 之间
m_mouthExpression(0),
// 初始化嘴巴宽度
mouthWidth(0),
// 初始化嘴巴高度
mouthHeight(0),
// 初始化嘴巴原始宽度
originalMouthWidth(0)
{
// 创建一个线性渐变对象,渐变方向从顶部到底部
QLinearGradient gradient(0, 0, 0, height());
// 设置渐变起始颜色
gradient.setColorAt(0, QColor(243, 203, 130));
// 设置渐变结束颜色
gradient.setColorAt(1, QColor(243, 203, 130));
// 将渐变对象赋值给成员变量 m_gradient
m_gradient = gradient;
// 开启鼠标跟踪功能,以便能够接收鼠标移动事件
setMouseTracking(true);
}
// 析构函数,由于没有动态分配的资源,这里为空
ColorCircleWidget::~ColorCircleWidget()
{
}
// 设置嘴巴表情的函数
void ColorCircleWidget::setMouthExpression(qreal expression)
{
// 检查传入的表情值是否与当前表情值不同
if (m_mouthExpression != expression) {
// 将表情值限制在 0 到 1 的范围内
m_mouthExpression = qBound(0.0, expression, 1.0);
// 如果原始嘴巴宽度还未初始化,则将当前嘴巴宽度赋值给原始嘴巴宽度
if (originalMouthWidth == 0) {
originalMouthWidth = mouthWidth;
}
// 根据表情值计算新的嘴巴宽度
mouthWidth = static_cast<int>(originalMouthWidth * (1 - 0.5 * m_mouthExpression));
// 触发重绘事件,更新界面显示
update();
}
}
// 设置渐变的函数
void ColorCircleWidget::setGradient(const QGradient &gradient)
{
// 检查传入的渐变对象是否与当前渐变对象不同
if (m_gradient != gradient) {
// 将传入的渐变对象赋值给成员变量 m_gradient
m_gradient = gradient;
// 触发重绘事件,更新界面显示
update();
}
}
// 更新眼珠位置的函数,根据鼠标位置计算眼珠的偏移量
void ColorCircleWidget::updatePupilPosition(const QPointF& mousePos)
{
// 获取窗口宽度和高度中的较小值,作为圆形的边长
int side = qMin(width(), height());
// 计算眼睛半径,为边长的 1/8
eyeRadius = side / 8;
// 计算眼睛的垂直位置,为边长的 1/3
int eyeY = side / 3;
// 计算左眼的水平位置
int leftEyeX = side / 3 - eyeRadius;
// 计算右眼的水平位置
int rightEyeX = 2 * side / 3 - eyeRadius;
// 计算左眼的中心点位置
leftEyeCenter = QPointF(leftEyeX + eyeRadius + (width() - side) / 2, eyeY + eyeRadius + (height() - side) / 2);
// 计算右眼的中心点位置
rightEyeCenter = QPointF(rightEyeX + eyeRadius + (width() - side) / 2, eyeY + eyeRadius + (height() - side) / 2);
// 计算鼠标位置相对于左眼中心点的偏移量
QPointF leftOffset = mousePos - leftEyeCenter;
// 计算鼠标位置相对于右眼中心点的偏移量
QPointF rightOffset = mousePos - rightEyeCenter;
// 计算左眼偏移量的长度
double leftLength = std::sqrt(leftOffset.x() * leftOffset.x() + leftOffset.y() * leftOffset.y());
// 计算右眼偏移量的长度
double rightLength = std::sqrt(rightOffset.x() * rightOffset.x() + rightOffset.y() * rightOffset.y());
// 定义左眼偏移量的最大长度
double leftMaxLength = eyeRadius / 2;
// 定义右眼偏移量的最大长度
double rightMaxLength = eyeRadius / 2;
// 如果左眼偏移量的长度超过最大长度,则对偏移量进行缩放
if (leftLength > leftMaxLength) {
leftOffset *= leftMaxLength / leftLength;
}
// 如果右眼偏移量的长度超过最大长度,则对偏移量进行缩放
if (rightLength > rightMaxLength) {
rightOffset *= rightMaxLength / rightLength;
}
// 计算左眼眼珠的最终位置
leftPupilPos = leftEyeCenter + leftOffset;
// 计算右眼眼珠的最终位置
rightPupilPos = rightEyeCenter + rightOffset;
// 触发重绘事件,更新界面显示
update();
}
// 重置眼珠位置的函数,将眼珠位置恢复到初始状态
void ColorCircleWidget::resetPupilPosition()
{
// 获取窗口宽度和高度中的较小值,作为圆形的边长
int side = qMin(width(), height());
// 计算眼睛半径,为边长的 1/8
eyeRadius = side / 8;
// 计算眼睛的垂直位置,为边长的 1/3
int eyeY = side / 3;
// 计算左眼的水平位置
int leftEyeX = side / 3 - eyeRadius;
// 计算右眼的水平位置
int rightEyeX = 2 * side / 3 - eyeRadius;
// 计算左眼的中心点位置
leftEyeCenter = QPointF(leftEyeX + eyeRadius + (width() - side) / 2, eyeY + eyeRadius + (height() - side) / 2);
// 计算右眼的中心点位置
rightEyeCenter = QPointF(rightEyeX + eyeRadius + (width() - side) / 2, eyeY + eyeRadius + (height() - side) / 2);
// 将左眼眼珠位置重置为左眼中心点位置
leftPupilPos = leftEyeCenter;
// 将右眼眼珠位置重置为右眼中心点位置
rightPupilPos = rightEyeCenter;
// 触发重绘事件,更新界面显示
update();
}
// 绘制事件处理函数,当需要重绘界面时调用
void ColorCircleWidget::paintEvent(QPaintEvent *event)
{
// 忽略事件参数,因为在本函数中不需要使用
Q_UNUSED(event);
// 创建一个 QPainter 对象,用于绘制图形
QPainter painter(this);
// 开启抗锯齿功能,使绘制的图形更加平滑
painter.setRenderHint(QPainter::Antialiasing);
// 获取窗口宽度和高度中的较小值,作为圆形的边长
int side = qMin(width(), height());
// 计算圆形的绘制区域
QRectF rect((width() - side) / 2, (height() - side) / 2, side, side);
// 创建一个画刷对象,使用当前的渐变填充
QBrush brush(m_gradient);
// 设置画刷
painter.setBrush(brush);
// 不使用画笔,即绘制的图形没有边框
painter.setPen(Qt::NoPen);
// 绘制圆形
painter.drawEllipse(rect);
// 创建一个画笔对象,设置颜色和宽度
QPen pen(QColor(233, 200, 127), 1);
// 设置画笔
painter.setPen(pen);
// 不使用画刷,即绘制的图形没有填充颜色
painter.setBrush(Qt::NoBrush);
// 绘制圆形的边框
painter.drawEllipse(rect.adjusted(0.5, 0.5, -0.5, -0.5));
// 计算眼睛半径,为边长的 1/8
eyeRadius = side / 8;
// 计算眼睛的垂直位置,为边长的 1/3
int eyeY = side / 3;
// 计算左眼的水平位置
int leftEyeX = side / 3 - eyeRadius;
// 计算右眼的水平位置
int rightEyeX = 2 * side / 3 - eyeRadius;
// 如果原始嘴巴宽度还未初始化,则将嘴巴宽度初始化为边长的 1/3,并将其赋值给原始嘴巴宽度
if (originalMouthWidth == 0) {
mouthWidth = side / 3;
originalMouthWidth = mouthWidth;
}
// 计算嘴巴的高度,为边长的 1/10
mouthHeight = side / 10;
// 计算嘴巴的垂直位置,为边长的 2/3
int mouthY = 2 * side / 3;
// 计算嘴巴的水平位置
int mouthX = side / 2 - mouthWidth / 2;
// 设置画刷为白色,用于绘制眼睛的白色部分
painter.setBrush(Qt::white);
// 不使用画笔,即绘制的图形没有边框
painter.setPen(Qt::NoPen);
// 计算左眼的绘制区域
QRectF leftEyeRect(leftEyeX + (width() - side) / 2, eyeY + (height() - side) / 2, 2 * eyeRadius, 2 * eyeRadius);
// 绘制左眼的白色部分
painter.drawEllipse(leftEyeRect);
// 设置画刷为黑色,用于绘制左眼的眼珠
painter.setBrush(Qt::black);
// 如果左眼眼珠位置为空,则将其初始化为左眼中心点位置
if (leftPupilPos.isNull()) {
leftPupilPos = QPointF(leftEyeX + eyeRadius + (width() - side) / 2, eyeY + eyeRadius + (height() - side) / 2);
}
// 计算左眼眼珠的绘制区域
QRectF leftPupilRect(leftPupilPos.x() - eyeRadius / 2, leftPupilPos.y() - eyeRadius / 2, eyeRadius, eyeRadius);
// 绘制左眼的眼珠
painter.drawEllipse(leftPupilRect);
// 设置画刷为白色,用于绘制右眼的白色部分
painter.setBrush(Qt::white);
// 不使用画笔,即绘制的图形没有边框
painter.setPen(Qt::NoPen);
// 计算右眼的绘制区域
QRectF rightEyeRect(rightEyeX + (width() - side) / 2, eyeY + (height() - side) / 2, 2 * eyeRadius, 2 * eyeRadius);
// 绘制右眼的白色部分
painter.drawEllipse(rightEyeRect);
// 设置画刷为黑色,用于绘制右眼的眼珠
painter.setBrush(Qt::black);
// 如果右眼眼珠位置为空,则将其初始化为右眼中心点位置
if (rightPupilPos.isNull()) {
rightPupilPos = QPointF(rightEyeX + eyeRadius + (width() - side) / 2, eyeY + eyeRadius + (height() - side) / 2);
}
// 计算右眼眼珠的绘制区域
QRectF rightPupilRect(rightPupilPos.x() - eyeRadius / 2, rightPupilPos.y() - eyeRadius / 2, eyeRadius, eyeRadius);
// 绘制右眼的眼珠
painter.drawEllipse(rightPupilRect);
// 创建一个画笔对象,设置颜色和宽度,用于绘制嘴巴
QPen mouthPen(QColor(139, 0, 0), 2);
// 设置画笔
painter.setPen(mouthPen);
// 设置画刷为红色,用于填充嘴巴
painter.setBrush(QColor(255, 0, 0));
// 计算嘴巴的绘制区域
QRectF mouthRect(mouthX + (width() - side) / 2, mouthY + (height() - side) / 2, mouthWidth, mouthHeight);
// 绘制带有圆角的矩形作为嘴巴
painter.drawRoundedRect(mouthRect, 7, 7);
// 如果是第一次打开,则设置嘴巴表情为 1,并将标志位设置为 false
if (isFirstOpen) {
setMouthExpression(1);
isFirstOpen = false;
}
}