Qt 5.9 实战:实现一个自动图片轮播器

一、前言

图片轮播是桌面软件开发中比较常见的一个功能,例如:

  • 相册自动播放
  • 广告图片轮播
  • 产品展示
  • 大屏图片切换
  • 教学课件演示

本文使用 Qt 5.9、Qt Widgets 和 C++ 实现一个完整的图片轮播器。

项目支持:

  • 自动轮播图片
  • 循环播放
  • 开始播放
  • 暂停播放
  • 显示当前图片序号
  • 图片等比缩放
  • 窗口尺寸变化后自动调整图片
  • 使用 Qt 资源系统嵌入图片
  • 程序换电脑后图片路径不会失效

运行环境:

复制代码
Qt 5.9.0
Qt Creator 4.3
MinGW 5.3.0 32bit
C++11

二、项目功能效果

程序启动后显示第一张图片,并显示状态:

复制代码
第 1 / 5 张    准备播放

点击"开始"按钮后,程序每秒切换一张图片:

复制代码
第 1 / 5 张    正在播放
第 2 / 5 张    正在播放
第 3 / 5 张    正在播放
第 4 / 5 张    正在播放
第 5 / 5 张    正在播放
第 1 / 5 张    正在播放

点击"暂停"按钮后,当前图片停止切换:

复制代码
第 3 / 5 张    已暂停

再次点击"开始",程序会从当前图片继续播放。


三、项目目录结构

项目名称:

复制代码
objectTimer

目录结构:

复制代码
objectTimer/
├── objectTimer.pro
├── main.cpp
├── widget.h
├── widget.cpp
├── widget.ui
├── resources.qrc
└── images/
    ├── 1.jpg
    ├── 2.jpg
    ├── 3.jpg
    ├── 4.jpg
    └── 5.jpg

文件作用:

文件 作用
objectTimer.pro qmake 工程配置
main.cpp 程序入口
widget.h Widget 类声明
widget.cpp 图片轮播逻辑
widget.ui 程序界面
resources.qrc Qt 图片资源配置
images 存放轮播图片

四、图片资源配置

Qt 项目可以使用 .qrc 文件将图片、图标、样式等资源编译进可执行程序。

创建:

复制代码
resources.qrc

内容如下:

复制代码
<RCC>
    <qresource prefix="/">
        <file>images/1.jpg</file>
        <file>images/2.jpg</file>
        <file>images/3.jpg</file>
        <file>images/4.jpg</file>
        <file>images/5.jpg</file>
    </qresource>
</RCC>

因为资源前缀是:

复制代码
<qresource prefix="/">

所以程序中的资源路径是:

复制代码
:/images/1.jpg
:/images/2.jpg
:/images/3.jpg
:/images/4.jpg
:/images/5.jpg

使用 Qt 资源系统有以下优点:

  1. 图片会被编译进程序。
  2. 不需要依赖外部相对路径。
  3. 更换电脑后图片不会丢失。
  4. 程序发布时管理更加方便。
  5. 不需要手动复制图片目录。

五、修改 objectTimer.pro

为了让 qmake 编译 .qrc 文件,需要在工程文件中加入:

复制代码
RESOURCES += resources.qrc

完整工程文件:

复制代码
QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = objectTimer
TEMPLATE = app

DEFINES += QT_DEPRECATED_WARNINGS

SOURCES += \
    main.cpp \
    widget.cpp

HEADERS += \
    widget.h

FORMS += \
    widget.ui

RESOURCES += resources.qrc

其中:

  • SOURCES 表示 C++ 源文件。
  • HEADERS 表示头文件。
  • FORMS 表示 .ui 界面文件。
  • RESOURCES 表示 Qt 资源文件。

六、main.cpp

复制代码
#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();

    return a.exec();
}

主要流程:

复制代码
创建 QApplication
    ↓
创建 Widget
    ↓
显示 Widget
    ↓
进入事件循环 exec()

QApplication::exec() 会持续处理鼠标、键盘、定时器和窗口事件。


七、widget.h

复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QStringList>
#include <QWidget>

class QResizeEvent;
class QTimerEvent;

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

protected:
    void timerEvent(QTimerEvent *event) override;
    void resizeEvent(QResizeEvent *event) override;

private slots:
    void on_startButton_clicked();
    void on_pauseButton_clicked();

private:
    void showImage(int index);
    void updateStatus();

    Ui::Widget *ui;
    QStringList m_images;
    int m_timerId;
    int m_currentIndex;
    bool m_hasStarted;
};

#endif // WIDGET_H

重点成员变量说明:

成员 作用
m_images 保存所有图片资源路径
m_timerId 保存定时器 ID,0 表示未运行
m_currentIndex 当前图片下标
m_hasStarted 判断是否播放过,用于显示"已暂停"

八、widget.cpp 完整实现

复制代码
#include "widget.h"
#include "ui_widget.h"

#include <QPixmap>
#include <QResizeEvent>
#include <QTimerEvent>

namespace {

const int kSlideshowIntervalMs = 1000;

} // namespace

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget),
    m_timerId(0),
    m_currentIndex(0),
    m_hasStarted(false)
{
    ui->setupUi(this);
    setWindowTitle(QStringLiteral("图片轮播器"));

    m_images << QStringLiteral(":/images/1.jpg")
             << QStringLiteral(":/images/2.jpg")
             << QStringLiteral(":/images/3.jpg")
             << QStringLiteral(":/images/4.jpg")
             << QStringLiteral(":/images/5.jpg");

    if (m_images.isEmpty()) {
        ui->imageLabel->setText(QStringLiteral("没有可播放的图片"));
        ui->startButton->setEnabled(false);
        ui->pauseButton->setEnabled(false);
        return;
    }

    showImage(0);
}

Widget::~Widget()
{
    if (m_timerId != 0) {
        killTimer(m_timerId);
    }

    delete ui;
}

void Widget::on_startButton_clicked()
{
    m_hasStarted = true;

    if (m_timerId == 0) {
        m_timerId = startTimer(kSlideshowIntervalMs);
    }

    updateStatus();
}

void Widget::on_pauseButton_clicked()
{
    if (m_timerId != 0) {
        killTimer(m_timerId);
        m_timerId = 0;
    }

    updateStatus();
}

void Widget::timerEvent(QTimerEvent *event)
{
    if (event && m_timerId != 0 && event->timerId() == m_timerId) {
        const int nextIndex = (m_currentIndex + 1) % m_images.size();
        showImage(nextIndex);
        return;
    }

    QWidget::timerEvent(event);
}

void Widget::resizeEvent(QResizeEvent *event)
{
    QWidget::resizeEvent(event);

    if (!m_images.isEmpty()) {
        showImage(m_currentIndex);
    }
}

void Widget::showImage(int index)
{
    if (m_images.isEmpty()) {
        return;
    }

    const int imageCount = m_images.size();
    m_currentIndex = (index % imageCount + imageCount) % imageCount;

    QPixmap source(m_images.at(m_currentIndex));

    if (source.isNull()) {
        ui->imageLabel->setText(QStringLiteral("图片加载失败"));
        return;
    }

    const QSize targetSize = ui->imageLabel->size() - QSize(12, 12);

    if (targetSize.width() > 0 && targetSize.height() > 0) {
        ui->imageLabel->setPixmap(
            source.scaled(targetSize,
                          Qt::KeepAspectRatio,
                          Qt::SmoothTransformation));
        ui->imageLabel->setText(QString());
    }

    updateStatus();
}

void Widget::updateStatus()
{
    const bool isPlaying = (m_timerId != 0);

    ui->startButton->setEnabled(!isPlaying);
    ui->pauseButton->setEnabled(isPlaying);

    QString stateText;

    if (isPlaying) {
        stateText = QStringLiteral("正在播放");
    } else if (m_hasStarted) {
        stateText = QStringLiteral("已暂停");
    } else {
        stateText = QStringLiteral("准备播放");
    }

    ui->statusLabel->setText(
        QStringLiteral("第 %1 / %2 张    %3")
            .arg(m_currentIndex + 1)
            .arg(m_images.size())
            .arg(stateText));
}

九、startTimer() 与 timerEvent() 原理

Qt 提供了两种常用的定时器方式。

方式一:QTimer

使用独立的 QTimer 对象:

复制代码
QTimer *timer = new QTimer(this);

connect(timer, &QTimer::timeout, this, [this]() {
    // 定时执行
});

timer->start(1000);

方式二:startTimer() 与 timerEvent()

复制代码
m_timerId = startTimer(1000);

当定时时间到达后,Qt 会调用:

复制代码
void Widget::timerEvent(QTimerEvent *event)
{
}

本项目使用第二种方式。

需要注意:

  1. 一个对象可以同时启动多个定时器。
  2. 每个定时器都有不同的定时器 ID。
  3. 需要在 timerEvent() 中判断 timerId。
  4. 不再使用时需要调用 killTimer()。

代码中:

复制代码
if (event && m_timerId != 0 && event->timerId() == m_timerId) {
    const int nextIndex = (m_currentIndex + 1) % m_images.size();
    showImage(nextIndex);
    return;
}

m_currentIndex + 1 表示下一张图片。

取模运算:

复制代码
% m_images.size()

可以实现循环播放。

例如一共有 5 张图片:

复制代码
0 → 1 → 2 → 3 → 4 → 0 → 1 ...

十、开始与暂停功能

1. 开始播放

复制代码
void Widget::on_startButton_clicked()
{
    m_hasStarted = true;

    if (m_timerId == 0) {
        m_timerId = startTimer(kSlideshowIntervalMs);
    }

    updateStatus();
}

判断:

复制代码
if (m_timerId == 0)

可以避免重复点击"开始"时创建多个定时器。

2. 暂停播放

复制代码
void Widget::on_pauseButton_clicked()
{
    if (m_timerId != 0) {
        killTimer(m_timerId);
        m_timerId = 0;
    }

    updateStatus();
}

暂停后:

  • 定时器被销毁。
  • 当前图片保持不变。
  • 再次点击开始时从当前图片继续播放。

十一、图片等比缩放

直接显示大尺寸图片,可能会:

  • 超出窗口范围。
  • 图片被拉伸变形。
  • 内存占用过大。
  • 不同分辨率电脑显示效果不一致。

因此代码中使用了:

复制代码
source.scaled(targetSize,
              Qt::KeepAspectRatio,
              Qt::SmoothTransformation)

参数说明:

参数 作用
targetSize 目标显示尺寸
Qt::KeepAspectRatio 保持原始宽高比
Qt::SmoothTransformation 使用平滑缩放

为了避免图片紧贴边框,增加了少量内边距:

复制代码
const QSize targetSize = ui->imageLabel->size() - QSize(12, 12);

当窗口大小改变时:

复制代码
void Widget::resizeEvent(QResizeEvent *event)
{
    QWidget::resizeEvent(event);

    if (!m_images.isEmpty()) {
        showImage(m_currentIndex);
    }
}

程序会重新缩放当前图片,使其始终适应当前窗口。


十二、widget.ui 界面文件

界面主要包含:

  • 一个图片显示标签
  • 一个状态标签
  • 一个开始按钮
  • 一个暂停按钮
  • 一个垂直布局
  • 一个水平按钮布局

完整 .ui 文件如下:

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Widget</class>
 <widget class="QWidget" name="Widget">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>640</width>
    <height>520</height>
   </rect>
  </property>
  <property name="minimumSize">
   <size>
    <width>520</width>
    <height>420</height>
   </size>
  </property>
  <property name="windowTitle">
   <string>图片轮播器</string>
  </property>
  <property name="styleSheet">
   <string>QWidget {
    background-color: #f3f4f6;
    color: #1f2937;
    font-family: 'Microsoft YaHei';
    font-size: 10pt;
}
QLabel#imageLabel {
    background-color: #ffffff;
    border: 1px solid #cbd5e1;
    border-radius: 10px;
}
QLabel#statusLabel {
    color: #64748b;
    font-size: 9pt;
}
QPushButton {
    color: #ffffff;
    border: none;
    border-radius: 7px;
    padding: 9px 24px;
    font-weight: bold;
}
QPushButton#startButton {
    background-color: #2563eb;
}
QPushButton#startButton:hover {
    background-color: #1d4ed8;
}
QPushButton#startButton:pressed {
    background-color: #1e40af;
}
QPushButton#pauseButton {
    background-color: #64748b;
}
QPushButton#pauseButton:hover {
    background-color: #475569;
}
QPushButton#pauseButton:pressed {
    background-color: #334155;
}
QPushButton:disabled {
    background-color: #cbd5e1;
    color: #94a3b8;
}</string>
  </property>
  <layout class="QVBoxLayout" name="mainLayout">
   <property name="spacing">
    <number>12</number>
   </property>
   <property name="leftMargin">
    <number>16</number>
   </property>
   <property name="topMargin">
    <number>16</number>
   </property>
   <property name="rightMargin">
    <number>16</number>
   </property>
   <property name="bottomMargin">
    <number>16</number>
   </property>
   <item>
    <widget class="QLabel" name="imageLabel">
     <property name="minimumSize">
      <size>
       <width>480</width>
       <height>340</height>
      </size>
     </property>
     <property name="sizePolicy">
      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
       <horstretch>1</horstretch>
       <verstretch>1</verstretch>
      </sizepolicy>
     </property>
     <property name="alignment">
      <set>Qt::AlignCenter</set>
     </property>
     <property name="text">
      <string>图片加载中...</string>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QLabel" name="statusLabel">
     <property name="alignment">
      <set>Qt::AlignCenter</set>
     </property>
     <property name="text">
      <string>准备播放</string>
     </property>
    </widget>
   </item>
   <item>
    <layout class="QHBoxLayout" name="buttonLayout">
     <property name="spacing">
      <number>12</number>
     </property>
     <item>
      <widget class="QPushButton" name="startButton">
       <property name="minimumSize">
        <size>
         <width>120</width>
         <height>38</height>
        </size>
       </property>
       <property name="text">
        <string>开始</string>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QPushButton" name="pauseButton">
       <property name="minimumSize">
        <size>
         <width>120</width>
         <height>38</height>
        </size>
       </property>
       <property name="text">
        <string>暂停</string>
       </property>
      </widget>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

十三、信号与槽自动连接

.ui 文件中按钮对象名分别是:

复制代码
startButton
pauseButton

头文件中声明的槽函数分别是:

复制代码
void on_startButton_clicked();
void on_pauseButton_clicked();

Qt Designer 生成的 setupUi() 中会调用:

复制代码
QMetaObject::connectSlotsByName(Widget);

它会自动完成连接:

复制代码
startButton.clicked()
        ↓
on_startButton_clicked()

pauseButton.clicked()
        ↓
on_pauseButton_clicked()

因此不需要手动再写:

复制代码
connect(ui->startButton,
        &QPushButton::clicked,
        this,
        &Widget::on_startButton_clicked);

前提是:

  1. 类中必须有 Q_OBJECT。
  2. 槽函数必须声明在 private slots 或 public slots 中。
  3. 函数名必须符合 on_对象名_信号名() 规则。

十四、状态栏更新

状态栏使用 statusLabel 显示:

复制代码
ui->statusLabel->setText(
    QStringLiteral("第 %1 / %2 张    %3")
        .arg(m_currentIndex + 1)
        .arg(m_images.size())
        .arg(stateText));

效果:

复制代码
第 3 / 5 张    正在播放

QString::arg() 可以依次替换字符串中的 %1、%2、%3。


十五、按钮状态控制

播放时禁用"开始"按钮,启用"暂停"按钮:

复制代码
ui->startButton->setEnabled(!isPlaying);
ui->pauseButton->setEnabled(isPlaying);

状态关系:

播放状态 开始按钮 暂停按钮
未播放 可用 不可用
正在播放 不可用 可用
已暂停 可用 不可用

这样可以防止用户重复点击"开始"而创建多个定时器。


十六、资源路径常见错误

1. 使用普通文件路径

复制代码
QPixmap pix("images/1.jpg");

这种写法依赖程序运行目录。

如果工作目录改变,图片就可能无法加载。

2. 使用 Qt 资源路径

复制代码
QPixmap pix(":/images/1.jpg");

以冒号开头,表示从 Qt 资源系统加载。

推荐使用:

复制代码
QStringLiteral(":/images/1.jpg")

3. .qrc 没有加入 .pro

只创建 resources.qrc 是不够的。

必须在 .pro 中加入:

复制代码
RESOURCES += resources.qrc

然后重新执行 qmake,再编译项目。


十七、编译和运行

在 Qt Creator 中打开:

复制代码
objectTimer.pro

选择:

复制代码
Desktop Qt 5.9.0 MinGW 32bit

执行:

复制代码
Ctrl+B  编译
Ctrl+R  运行

编译过程中,Qt 会自动调用:

复制代码
uic.exe
moc.exe
rcc.exe

作用如下:

工具 作用
uic 将 .ui 转换为 ui_widget.h
moc 处理 Q_OBJECT,生成元对象代码
rcc 将 .qrc 中的图片编译成 C++ 资源代码

最终生成为:

复制代码
objectTimer.exe

十八、完整运行流程

复制代码
启动程序
    ↓
创建 Widget
    ↓
setupUi()
    ↓
读取 5 张图片资源路径
    ↓
显示第 1 张图片
    ↓
点击"开始"
    ↓
startTimer(1000)
    ↓
每隔 1 秒触发 timerEvent()
    ↓
计算下一张图片下标
    ↓
重新加载并缩放图片
    ↓
更新状态栏
    ↓
到达最后一张后回到第一张

暂停流程:

复制代码
点击"暂停"
    ↓
killTimer(m_timerId)
    ↓
m_timerId = 0
    ↓
保留当前图片
    ↓
状态改为"已暂停"

十九、可以继续扩展的功能

1. 上一张和下一张

增加两个按钮:

复制代码
上一张
下一张

点击后手动切换:

复制代码
m_currentIndex = (m_currentIndex - 1 + m_images.size())
                 % m_images.size();

2. 设置轮播间隔

增加一个 QSpinBox:

复制代码
QSpinBox *intervalSpinBox;

用户可以自定义图片切换时间。

3. 显示缩略图列表

使用 QListWidget 显示所有图片的小图,点击即可切换。

4. 添加淡入淡出动画

使用:

复制代码
QPropertyAnimation
QGraphicsOpacityEffect

实现图片切换动画。

5. 支持拖拽图片

重写:

复制代码
dragEnterEvent()
dropEvent()

将本地图片拖入窗口后自动播放。

6. 全屏播放

增加全屏按钮:

复制代码
showFullScreen();

7. 读取文件夹中的全部图片

使用:

复制代码
QDir
QFileInfoList

扫描指定目录中的 JPG、PNG、BMP 等图片。

8. 保存播放状态

使用:

复制代码
QSettings

保存上次播放到的图片和轮播间隔。


二十、总结

本文使用 Qt 5.9 实现了一个完整的图片轮播器,涉及以下知识点:

复制代码
QWidget
QLabel
QPushButton
QPixmap
QStringList
QVBoxLayout
QHBoxLayout
QTimerEvent
startTimer()
killTimer()
timerEvent()
resizeEvent()
Q_OBJECT
信号与槽
qmake
uic
moc
rcc
Qt 资源系统
图片等比缩放

核心实现思路是:

  1. 使用 QStringList 保存图片资源路径。
  2. 使用 startTimer() 创建定时器。
  3. 在 timerEvent() 中计算下一张图片。
  4. 使用取模运算实现循环播放。
  5. 使用 QPixmap::scaled() 自动缩放图片。
  6. 使用 killTimer() 暂停播放。
  7. 使用 .qrc 将图片编译进程序。
  8. 使用 QLabel 显示图片和播放状态。

这个项目结构比较简单,适合作为 Qt 定时器、信号与槽、图片资源和界面布局的综合练习,也可以继续扩展成相册、广告轮播或大屏图片展示程序。

相关推荐
l1t1 小时前
DeepSeek总结的pg_clickhouse 与 chdb 更新:编码、嵌套与类型
数据库·clickhouse·postgresql
傲世仙尊1 小时前
从HTTPS到MySQL-会话保持的攻防与数据库的真面目
数据库·mysql·https
jyOverQ1 小时前
MySQL 崩溃以后怎么恢复数据?redo log、undo log 与 Crash Recovery
数据库·mysql
盟道科技1 小时前
电商小程序购物车系统设计:缓存与数据库双写一致性实战
数据库·缓存·小程序
java1234_小锋1 小时前
【技术专题】Mysql8 数据库 - Mysql8 客户端sqlyog安装以及配置
数据库·sqlyog
奇牙coding1 小时前
Codex C接 配置教程:的 字段从迁移时必须写完整后缀,填旧值或省略 会静默回退默认模型
java·c语言·数据库·ai
Nturmoils1 小时前
别急着 JOIN,子查询有些场景更顺手
数据库
喜欢的名字被抢了1 小时前
09-Redis 进阶原理篇:单线程、多线程、过期、LRU-LFU、Fork 与 Lua
数据库·redis·lua
Omics Pro2 小时前
计算虚拟扰动:网络毒理+虚拟敲除
数据库·人工智能·算法·机器学习·自然语言处理