现代Qt开发教程(新手篇)2.3——QImage、QPixmap、QIcon 图像处理基础
现代Qt开发教程新手篇2.3——QImage、QPixmap、QIcon 图像处理基础相关仓库仍然已经开源正在积极火热的建设之中欢迎各位大佬提Issue和PR链接地址https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeQt1. 前言 / 为什么需要搞清楚这几个类我第一次在 Qt 里加载一张图片显示到界面上的时候直接面对了三个长得差不多的类QImage、QPixmap、QIcon。当时的心情就是——不就是显示一张图吗为什么搞出这么多类后来做项目做多了才明白Qt 把图像处理拆成这几个类是有原因的它们各自的定位完全不同用错了地方性能和效果都会出问题。咱们开门见山吧QPixmap 是专门为屏幕显示优化的它在底层直接存储的是显卡能用的像素数据往屏幕上画的时候速度飞快。QImage 则反过来它存储的是原始像素数据你可以用代码一个像素一个像素地读写做图像处理的时候就靠它。QIcon 不是一张图而是一组图——它可以根据控件的状态正常、禁用、选中和尺寸自动选择最合适的图片来显示。这篇文章我们一起来弄清楚QPixmap 和 QImage 的本质区别是什么、怎么从文件和资源系统加载图像、怎么缩放图片保持比例不变形、怎么用 QIcon 给按钮和工具栏配图标。搞明白这些以后在 Qt 里处理任何图像相关的需求你都不会再纠结该用哪个类了。2. 环境说明本篇代码适用于 Qt 6.5 版本示例基于 Qt 6.9.1 验证CMake 3.26C17 标准。示例代码依赖 QtGui 和 QtWidgets 模块QImage 和 QPixmap 在 QtGui 中QIcon 也属于 QtGui但我们的示例用到了 QWidget 和 QPushButton 来展示效果所以需要同时链接 QtWidgets。所有代码在 Linux、Windows、macOS 上都可以编译运行不过你需要注意准备几张测试图片——我们会用代码生成一些也会演示从文件加载的方式。3. 核心概念讲解3.1 QPixmap vs QImage先搞清楚再动手这是新手最容易搞混的地方。我们直接从底层存储机制来说QPixmap 底层存储的是跟显示设备相关的像素数据你可以理解为它已经是渲染好了的位图直接交给显卡去显示就行所以QPainter::drawPixmap()的速度非常快。但正因为它跟显示设备绑定你没法直接访问里面的像素数据——没有pixel()函数给你用。QImage 就不一样了它存储的是设备无关的原始像素数据底层就是一个二维数组。你可以用pixel(x, y)读取任意位置的像素颜色也可以用setPixel(x, y, color)逐像素修改甚至可以用scanLine()拿到每一行的原始内存指针来做批量操作。代价是它显示到屏幕上时需要先转换成显示设备格式所以drawImage()比drawPixmap()稍慢一些。// QPixmap显示速度快不能直接操作像素QPixmappixmap(200,200);pixmap.fill(Qt::blue);// pixmap 没有 pixel() / setPixel() 接口// QImage可以直接读写像素显示稍慢QImageimage(200,200,QImage::Format_RGB32);image.fill(Qt::red);// 可以逐像素操作image.setPixel(100,100,qRgb(0,255,0));// 在 (100,100) 画一个绿点QRgb colorimage.pixel(100,100);// 读取像素颜色那什么时候用哪个呢规则很简单如果只是加载一张图片显示到界面上用 QPixmap如果需要读取或修改像素数据比如截图、图像滤镜、生成图片用 QImage处理完了要显示的话用QPixmap::fromImage()转一下就行// 先用 QImage 处理像素QImageimage(photo.png);// 做一些像素操作……imageimage.convertToFormat(QImage::Format_RGB32);// 处理完了要显示转成 QPixmapQPixmap pixmapQPixmap::fromImage(image);// 然后在 paintEvent 里用 drawPixmap 画出来还有一个很多人忽略的性能细节如果你要在 paintEvent 里反复画同一张 QPixmap最好把它存成类的成员变量不要每次 paintEvent 都重新加载。QImage 也一样如果图片不变加载一次就够了。每帧都从磁盘读文件这种事就算 SSD 也扛不住。3.2 从文件和资源系统加载图像最直接的方式就是从文件路径加载。QPixmap 和 QImage 都有接受文件路径的构造函数用起来非常方便// 从文件加载QPixmappixmap(images/photo.png);if(pixmap.isNull()){qDebug()图片加载失败检查路径是否正确;}QImageimage(images/photo.png);if(image.isNull()){qDebug()图片加载失败;}这里有个坑你可能会踩到文件路径是相对于程序的工作目录的不是相对于源代码文件的。这意味着你在 IDE 里运行和双击可执行文件运行工作目录很可能不一样图片路径就找不到了。调试的时候建议先qDebug() QDir::currentPath()看一下当前工作目录到底在哪里。说到图片路径的问题Qt 提供了一个更好的解决方案Qt Resource SystemQRC。它可以把图片、配置文件等资源直接编译进可执行文件里这样不管程序在哪里运行资源路径都是稳定的不存在找不到文件的问题。使用 QRC 分三步首先创建一个.qrc文件这是一个 XML 格式的资源描述文件里面列出你要打包的文件然后在 CMake 里用qt_add_resources或者依赖CMAKE_AUTORCC自动编译最后在代码里用:/前缀的路径来访问资源。!-- resources.qrc --RCCqresourceprefix/imagesfilelogo.png/filefileicon_edit.png/filefileicon_delete.png/file/qresource/RCC// 用 :/ 前缀访问 QRC 中的资源QPixmaplogo(:/images/logo.png);QIconeditIcon(:/images/icon_edit.png);你可能会问什么时候用 QRC什么时候用外部文件简单来说小图标、logo 这些不会经常变的东西放 QRC 最好程序一个文件就能跑。但如果是用户数据、大量高清图片、视频这种体积大的东西就不要往 QRC 里塞了——它们会让可执行文件膨胀到离谱。3.3 QPixmap::scaled() 与缩放加载了一张图片但显示区域大小不一定刚好合适这时候就需要缩放。QPixmap 提供了scaled()函数功能非常丰富QPixmaporiginal(photo.png);// 指定目标尺寸忽略比例可能变形QPixmap stretchedoriginal.scaled(200,100);// 保持宽高比缩放到不超过 200x100 的最大尺寸QPixmap scaledoriginal.scaled(200,100,Qt::KeepAspectRatio);// 保持宽高比缩放到完全覆盖 200x100 区域可能裁剪QPixmap coveredoriginal.scaled(200,100,Qt::KeepAspectRatioByExpanding);// 平滑缩放质量更好速度稍慢QPixmap smoothoriginal.scaled(200,100,Qt::KeepAspectRatio,Qt::SmoothTransformation);Qt::KeepAspectRatio是你 90% 情况下要用的模式——它保证图片不变形缩放到目标区域内能放下的最大尺寸。Qt::KeepAspectRatioByExpanding则是反过来保证整个目标区域被图片填满多出来的部分被裁掉做背景图的时候常用。Qt::SmoothTransformation这个参数值得一提默认的缩放用的是快速算法近邻采样放大后会看到明显的锯齿和马赛克加上这个参数后会使用双线性滤波放大后的效果平滑得多代价是计算量稍大。对于静态图片一次性缩放来说这点性能损失完全可以忽略所以建议默认就加上。在实际开发中你可能需要让图片跟随窗口大小自适应。一个典型的做法是在 paintEvent 里根据 Widget 当前尺寸动态缩放voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);if(!m_pixmap.isNull()){// 根据 Widget 尺寸缩放图片保持比例居中显示QPixmap scaledm_pixmap.scaled(width(),height(),Qt::KeepAspectRatio,Qt::SmoothTransformation);// 居中偏移intx(width()-scaled.width())/2;inty(height()-scaled.height())/2;painter.drawPixmap(x,y,scaled);}}不过这里有一个性能隐患paintEvent每次窗口大小改变都会被调用每次都做一次缩放计算。如果图片很大这个开销不小。更好的做法是在 resizeEvent 里做一次缩放把结果缓存起来voidresizeEvent(QResizeEvent*)override{if(!m_original.isNull()){// 在 resize 时缓存缩放结果m_scaledm_original.scaled(width(),height(),Qt::KeepAspectRatio,Qt::SmoothTransformation);}update();// 触发重绘}voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);if(!m_scaled.isNull()){intx(width()-m_scaled.width())/2;inty(height()-m_scaled.height())/2;painter.drawPixmap(x,y,m_scaled);}}这样 paintEvent 里只做一次 drawPixmap没有任何计算重绘速度极快。3.4 QIcon 多尺寸与状态图标QIcon 和前面两个类不太一样它不是一张图而是一组图的容器。你可以给同一个 QIcon 添加不同尺寸、不同状态、不同模式下的图片Qt 在使用的时候会自动挑选最合适的那一张来显示。QIcon icon;icon.addFile(:/icons/icon_16.png,QSize(16,16));// 小尺寸菜单、工具栏icon.addFile(:/icons/icon_32.png,QSize(32,32));// 中尺寸按钮icon.addFile(:/icons/icon_64.png,QSize(64,64));// 大尺寸对话框// 不同状态可以配不同的图icon.addFile(:/icons/icon_32_disabled.png,QSize(32,32),QIcon::Disabled);// 禁用状态icon.addFile(:/icons/icon_32_active.png,QSize(32,32),QIcon::Active);// 激活/按下状态如果你只提供了一张图Qt 会自动帮你缩放到需要的尺寸效果也能用但肯定不如手动准备的各尺寸图片清晰。所以对于经常出现在界面上的图标特别是工具栏图标建议至少提供 16x16 和 32x32 两个尺寸。QIcon 最常见的用途就是给 QPushButton、QAction、QToolBar 配图标// 给按钮设置图标QPushButton*btnnewQPushButton;btn-setIcon(QIcon(:/icons/open.png));btn-setIconSize(QSize(24,24));// 指定显示尺寸btn-setText(打开文件);// 给 QAction 设置图标菜单和工具栏QAction*openActionnewQAction(QIcon(:/icons/open.png),打开,this);你可能会好奇iconSize 设多大合适这个取决于你的使用场景。工具栏图标通常是 22x22 或 24x24菜单图标通常是 16x16对话框里的大图标可能是 48x48 或 64x64。如果你不确定可以参考你所处平台的 Human Interface Guidelines——macOS、Windows、GNOME 都有各自的推荐值。还有一个实用技巧QIcon 可以直接从 QPixmap 构造这样你可以用代码动态生成图标不一定非要有图片文件// 用代码画一个简单的彩色圆点图标QPixmapdot(24,24);dot.fill(Qt::transparent);// 透明背景QPainterp(dot);p.setRenderHint(QPainter::Antialiasing);p.setBrush(Qt::red);p.setPen(Qt::NoPen);p.drawEllipse(2,2,20,20);QIcondotIcon(dot);// 用在按钮上QPushButton*statusBtnnewQPushButton;statusBtn-setIcon(dotIcon);到这里你可以停下来想一想如果你要实现一个图片浏览器让用户打开一张图片并自适应窗口大小显示同时有一个缩放到 1:1 原始尺寸的按钮你会怎么组织 QPixmap 和 QImage 的使用窗口缩放的时候应该在哪里做缩放计算想清楚这个说明你真的理解了这几个类各自该在什么时候出场。4. 踩坑预防第一个坑是路径问题刚才已经提过了用相对路径加载图片程序换个地方运行就找不到了。尤其在 Windows 上双击 exe 和在 IDE 里运行的工作目录经常不一样。如果你不想用 QRC至少用QCoreApplication::applicationDirPath()拼一个绝对路径出来或者让用户通过文件对话框选择图片。第二个坑是图片加载失败但不报错。QPixmap 和 QImage 的构造函数接受一个不存在的文件路径时不会抛异常也不会输出警告——它们只是默默创建一个 null 对象。你只有在调isNull()的时候才会发现图片没加载成功。所以每次加载图片之后一定要检查isNull()别画了半天不知道为什么画面上什么都没有。QPixmappixmap(nonexistent.png);// 不检查直接画画面空白你排查半天找不到原因painter.drawPixmap(0,0,pixmap);// 正确做法加载后检查QPixmappixmap(nonexistent.png);if(pixmap.isNull()){qDebug()图片加载失败;// 画一个占位符或者错误提示painter.setPen(Qt::red);painter.drawText(rect(),Qt::AlignCenter,图片加载失败);return;}第三个坑是在 paintEvent 里做重量级的图片操作。有些人习惯在 paintEvent 里从文件加载图片、做缩放、做格式转换——这些操作每帧都重复执行窗口稍微拖动一下就触发好几次 paintEvent程序直接卡成幻灯片。图片加载和预处理都应该在初始化阶段或者数据变化时做一次paintEvent 里只做最轻量的绘制操作。第四个坑是 QPixmap 的线程安全问题。QPixmap 是跟 GUI 线程绑定的你不能在非 GUI 线程里创建或操作 QPixmap——调用就直接崩溃。如果你需要在后台线程里做图像处理请用 QImage它是线程安全的每个线程操作自己的 QImage 对象互不影响处理完了再到 GUI 线程转成 QPixmap 显示。5. 练习项目我们来做一个实战练习实现一个简易图片查看器。程序打开后显示一个窗口窗口中央有一张图片自适应显示保持比例居中窗口上方有一排工具按钮——打开文件按钮让用户选择一张图片加载适应窗口按钮切换自适应显示模式原始尺寸按钮以 1:1 比例显示图片图片超出窗口时需要滚动条提示还有一个标签显示当前图片的文件名和尺寸信息。完成标准是继承 QWidget 实现自定义的图片显示控件在 paintEvent 中用 drawPixmap 绘制图片支持拖拽窗口改变大小图片自适应跟随缩放在 resizeEvent 中缓存缩放结果通过 QPushButton QFileDialog 让用户选择图片文件加载用 QLabel 显示图片信息文件名、原始尺寸、缩放比例加载失败时显示错误提示而不是空白画面。几个提示用QPixmap::fromImage()把 QImage 转成 QPixmap 用于显示图片信息可以用QPixmap::size()和QFileInfo::fileName()获取自适应缩放用Qt::KeepAspectRatioQt::SmoothTransformation。6. 官方文档参考链接Qt 文档 · QImage Class – QImage 的完整 API像素读写、格式转换、图像处理相关接口Qt 文档 · QPixmap Class – QPixmap 的完整 API加载、缩放、转换相关接口Qt 文档 · QIcon Class – QIcon 多尺寸多状态图标的完整文档Qt 文档 · The Qt Resource System – QRC 资源系统的详细说明怎么创建和使用资源文件Qt 文档 · QPixmap::fromImage – QImage 到 QPixmap 的转换函数包含格式转换相关的参数说明到这里QImage、QPixmap、QIcon 三个类各自的角色你应该清楚了QPixmap 负责高效显示、QImage 负责像素级操作、QIcon 负责多尺寸多状态的图标管理。记住最核心的一条——只显示用 QPixmap要操作像素用 QImage最后别忘了在 paintEvent 之外做好预处理别把重量级操作塞进每帧的重绘里。下一篇文章我们来搞定 QFont 和文本渲染让文字在 Qt 里也能排版得漂漂亮亮。相关阅读现代Qt开发教程新手篇2.1——QPainter 绘图基础 - 相似度 100%通用GUI编程技术——Win32 原生编程实战五十三——子类化与超类化 - 相似度 82%现代Qt开发教程新手篇1.15——正则与文本处理 - 相似度 71%