一、前言图片轮播是桌面软件开发中比较常见的一个功能例如相册自动播放广告图片轮播产品展示大屏图片切换教学课件演示本文使用 Qt 5.9、Qt Widgets 和 C 实现一个完整的图片轮播器。项目支持自动轮播图片循环播放开始播放暂停播放显示当前图片序号图片等比缩放窗口尺寸变化后自动调整图片使用 Qt 资源系统嵌入图片程序换电脑后图片路径不会失效运行环境Qt 5.9.0 Qt Creator 4.3 MinGW 5.3.0 32bit C11二、项目功能效果程序启动后显示第一张图片并显示状态第 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.proqmake 工程配置main.cpp程序入口widget.hWidget 类声明widget.cpp图片轮播逻辑widget.ui程序界面resources.qrcQt 图片资源配置images存放轮播图片四、图片资源配置Qt 项目可以使用.qrc文件将图片、图标、样式等资源编译进可执行程序。创建resources.qrc内容如下RCC qresource prefix/ fileimages/1.jpg/file fileimages/2.jpg/file fileimages/3.jpg/file fileimages/4.jpg/file fileimages/5.jpg/file /qresource /RCC因为资源前缀是qresource prefix/所以程序中的资源路径是:/images/1.jpg :/images/2.jpg :/images/3.jpg :/images/4.jpg :/images/5.jpg使用 Qt 资源系统有以下优点图片会被编译进程序。不需要依赖外部相对路径。更换电脑后图片不会丢失。程序发布时管理更加方便。不需要手动复制图片目录。五、修改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保存定时器 ID0 表示未运行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) { }本项目使用第二种方式。需要注意一个对象可以同时启动多个定时器。每个定时器都有不同的定时器 ID。需要在timerEvent()中判断timerId。不再使用时需要调用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 version1.0 encodingUTF-8? ui version4.0 classWidget/class widget classQWidget nameWidget property namegeometry rect x0/x y0/y width640/width height520/height /rect /property property nameminimumSize size width520/width height420/height /size /property property namewindowTitle string图片轮播器/string /property property namestyleSheet stringQWidget { 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 classQVBoxLayout namemainLayout property namespacing number12/number /property property nameleftMargin number16/number /property property nametopMargin number16/number /property property namerightMargin number16/number /property property namebottomMargin number16/number /property item widget classQLabel nameimageLabel property nameminimumSize size width480/width height340/height /size /property property namesizePolicy sizepolicy hsizetypeExpanding vsizetypeExpanding horstretch1/horstretch verstretch1/verstretch /sizepolicy /property property namealignment setQt::AlignCenter/set /property property nametext string图片加载中.../string /property /widget /item item widget classQLabel namestatusLabel property namealignment setQt::AlignCenter/set /property property nametext string准备播放/string /property /widget /item item layout classQHBoxLayout namebuttonLayout property namespacing number12/number /property item widget classQPushButton namestartButton property nameminimumSize size width120/width height38/height /size /property property nametext string开始/string /property /widget /item item widget classQPushButton namepauseButton property nameminimumSize size width120/width height38/height /size /property property nametext string暂停/string /property /widget /item /layout /item /layout /widget layoutdefault spacing6 margin11/ 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);前提是类中必须有Q_OBJECT。槽函数必须声明在private slots或public slots中。函数名必须符合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执行CtrlB 编译 CtrlR 运行编译过程中Qt 会自动调用uic.exe moc.exe rcc.exe作用如下工具作用uic将.ui转换为ui_widget.hmoc处理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. 设置轮播间隔增加一个QSpinBoxQSpinBox *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 资源系统 图片等比缩放核心实现思路是使用QStringList保存图片资源路径。使用startTimer()创建定时器。在timerEvent()中计算下一张图片。使用取模运算实现循环播放。使用QPixmap::scaled()自动缩放图片。使用killTimer()暂停播放。使用.qrc将图片编译进程序。使用QLabel显示图片和播放状态。这个项目结构比较简单适合作为 Qt 定时器、信号与槽、图片资源和界面布局的综合练习也可以继续扩展成相册、广告轮播或大屏图片展示程序。