简介:面向Qt图形开发者的QOpenGLWidget图像显示与变换示例工程,围绕将QImage上传为OpenGL纹理并完成显示、缩放、旋转、平移等交互展开,适合已掌握Qt基础、希望从QPainter转向GPU渲染的初中级开发者,也可作为图像查看器原型的起步代码。压缩包共10个文件、约347KB,以3个cpp和2个h源文件为核心,配合1个ui界面文件、1个qrc资源文件、1个pro工程文件及示例jpg图片,另有1个user本地配置,结构紧凑,可直接编译对照阅读。目前已有783人学习下载。代码把OpenGL上下文初始化、纹理参数设置、模型视图矩阵变换与鼠标键盘事件响应串成完整链路,读者可据此理解缩放因子维护、旋转矩阵累乘和拖拽平移的具体写法,并在此基础上扩展定时刷新与更复杂的交互功能,减少从零摸索纹理上传和坐标变换的调试成本。
1. QOpenGLWidget 显示 QImage:先别急着写矩阵,纹理链路才是地基
把 QImage 丢进 QOpenGLWidget 做显示,再补上缩放、旋转、平移,看起来只是三组矩阵,实际落地时最容易翻车的是纹理坐标和鼠标锚点。我用这套模板接过几个图像查看器,图片从几百像素到上亿像素都跑过,最稳的路径不是每帧用 QPainter 重绘,而是把 QImage 转成纹理,交互只改一个 MVP 矩阵。下面按“渲染链路—最小实现—鼠标交互—避坑—验收”拆开,代码可以直接抄,参数按你的图源调。适合正在做图像预览、标注工具、工业视觉界面的开发者。
2. 纹理管线拆解:QImage 到 QOpenGLWidget 的格式、UV 与正交投影
2.1 为什么不用 QPainter 直接画:纹理路径与适用边界
QOpenGLWidget 里确实可以用 QPainter 画 QImage,代码量还少:在 paintGL 里 begin 一个 QPainter,drawImage 带个变换矩阵,缩放旋转平移都能做。这个方案适合图片少、变换不频繁、对帧率没有硬要求的界面。可一旦图片是几千万像素、鼠标滚轮要连续缩放、还要叠加多层标注,QPainter 的 CPU 合成就会变成瓶颈,而且它和原生 OpenGL 的状态混用容易出现黑屏、纹理丢失这类黑匣子问题。
纹理路径的思路很直接:图片只在加载或切换时上传一次,之后每帧只更新 uniform 矩阵,GPU 采样纹理完成显示。缩放、旋转、平移都落在同一个 4x4 矩阵里,鼠标交互只改几个标量,不需要重新生成像素。这个方案对新手不友好的一点是坐标系多:QImage 的 y 向下,OpenGL 纹理的 v 向上,窗口坐标 y 向下,正交投影又可以自定义 y 方向。坐标系没统一,后面每个交互都会还债。
我一般把场景坐标定成“图片中心为原点,x 向右,y 向上,单位是图片像素”。四边形顶点直接用图片宽高的一半,投影矩阵用视口逻辑宽高做正交投影,鼠标坐标进事件后先转成以视口中心为原点的坐标。这样模型矩阵的平移量就是场景像素,缩放锚点的推导也干净。
| 方案 | 优点 | 边界 | 适用场景 |
|---|---|---|---|
| QPainter 直接绘制 | 代码少,2D 变换直观 | 大图每帧 CPU 合成,和原生 GL 混用状态复杂 | 小图预览、低频变换 |
| QImage 转纹理 + 着色器 | 上传一次,交互只改矩阵,帧率稳 | 要处理格式、UV、DPR、上下文 | 图像查看器、标注工具、工业视觉 |
| QOpenGLWidget 内嵌 QGraphicsView | 复用场景图 | 还是 CPU 路径,深度定制受限 | 轻量展示 |
选纹理路径的理由不是“OpenGL 更快”这种空话,而是交互模型更干净:缩放就是 scale,旋转就是 rotate,平移就是 translate,三个参数可以序列化、可以回放、可以做动画。第 6 章的验收也是围绕这三个参数做回归。
2.2 QImage 的格式、行对齐与纹理上传参数
QImage 的内存布局和 OpenGL 的纹理格式不是天然对齐的。最常见的翻车是直接把 Format_ARGB32 的 constBits() 传给 glTexImage2D,还按 GL_RGBA 解释。ARGB32 在小端机器上的字节序是 BGRA,结果就是红蓝互换,图片发蓝或者人脸变青。另一个坑是扫描行对齐:QImage 每行按 4 字节对齐,Format_RGBA8888 每像素 4 字节,通常 bytesPerLine 等于 width*4,但换成 Format_RGB888 就不一定,glPixelStorei(GL_UNPACK_ALIGNMENT, 1) 不设置就会错行。
我的习惯是上传前统一转成 QImage::Format_RGBA8888,不直接信任源图格式。转换一次的成本可以接受,换来的是后面所有纹理参数固定:内部格式 GL_RGBA8,源格式 GL_RGBA,源类型 GL_UNSIGNED_BYTE,行对齐 1。如果图片来源是相机、截图、PDF 渲染,格式五花八门,这一步更不能省。
// 上传前统一格式,避免 ARGB32/BGRA 的红蓝互换 QImage rgba = source.convertToFormat(QImage::Format_RGBA8888); GLuint tex = 0; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); // RGBA8888 每像素 4 字节,行对齐设为 1 最稳 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // 缩放过滤:放大用 LINEAR,缩小用 LINEAR_MIPMAP_LINEAR 需要先生成 mipmap glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, rgba.width(), rgba.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.constBits()); glBindTexture(GL_TEXTURE_2D, 0);逻辑说明:先转换格式,再绑定纹理对象,设置采样和环绕参数,最后一次性上传像素。参数说明:GL_UNPACK_ALIGNMENT 设为 1 是为了不让驱动按 4 字节对齐去猜行尾;GL_CLAMP_TO_EDGE 防止缩放后边缘采样到重复像素;GL_LINEAR 在放大时比 GL_NEAREST 柔和,但缩小到很小的时候会出现锯齿和闪烁,这时要么生成 mip
本文还有配套的精品资源,点击获取