使用QGraphicsView思想做一个简单图片查看器
如果要做一个图片查看器,支持放大、滚动操作,比较直接的方法是,使用QWidget来显示完整图片,将QWidget放入QScrollArea。缩放时调整QWidget的尺寸,QScrollArea会自动调整滚动范围,超出视口区域图片自然就不会显示。
如果要使用QGraphicsView的思想呢?
原理
QGraphicsScenes是固定不变的,QGraphicsView使用一个变换矩阵来实现QWidget区域与QGraphicsScene区域之间的转换。缩放和滚动相对直接,任意角度旋转涉及到的问题还是挺麻烦的,这里不考虑。
所以只要能实现图像坐标、区域,到QWidget的坐标、区域转换,和反向转换,剩下就非常简单了。只考虑缩放和滚动,只需要维护变换矩阵(QTransform)和滚动距离,交互上还需要考虑滚动范围,避免超出。
主要代码
以下代码全部使用了浮点值,防止精度损失和溢出
-
根据缩放,重设变换矩阵
void resetView() { // QSizeF scrollRange; //缓存滚动范围 // qreal scale; //当前缩放 QRectF rect(this->rect()); scrollRange = imageRect.size() / scale - rect.size(); QPointF offset(0, 0); // 当图片显示小于视口,用于居中 if(scrollRange.width() < 0) { // 水平居中 offset.rx() = - scrollRange.rwidth() / 2; scrollRange.rwidth() = 0; } if(scrollRange.height() < 0) { // 垂直居中 offset.ry() = - scrollRange.height() / 2; scrollRange.rheight() = 0; } // 变换矩阵 transform = QTransform().scale(scale, scale).translate(-offset.x(), -offset.y()); }
-
坐标与矩阵映射
QGraphicsView内部,滚动范围值是场景区域经过变换后的区域范围,并非从0起始。
由于滚动代表实际的偏移位置,直接写入transform不方便// QPointF scrollValue; // 当前滚动,为了方便使用坐标点 QPointF mapToImage(QPointF pos){ return transform.map(pos + scrollValue); } QPointF mapFromImage(QPointF pos){ return transform.inverted().map(pos) - scrollValue; } QRectF mapToImage(QRectF rect){ rect.moveTopLeft(rect.topLeft() + scrollValue); return transform.mapRect(rect); } QRectF mapFromImage(QRectF rect){ rect = transform.inverted().mapRect(rect); rect.moveTopLeft(rect.topLeft() - scrollValue); return rect; }
-
绘制图片
QRectF rect(this->rect()); QRectF img_rect = mapToImage(rect).intersected(imageRect); QRectF paint_rect = mapFromImage(img_rect); QPainter painter(this); painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.drawImage(paint_rect, image, img_rect);
绘制时,先将窗口区域变换到图片区域,求取交集,再反算到视口区域。QPainter支持将图片重某区域绘制到指定区域。
-
滚动和缩放图片
滚动相对简单,监听鼠标事件,修改当前滚动。
缩放直接修改scale,调用resetView重新计算滚动范围、变换矩阵。void scollView(QPointF dp){ scrollValue.rx() = qBound(0.0, scrollValue.x() + dp.x(), scrollRange.width()); scrollValue.ry() = qBound(0.0, scrollValue.y() + dp.y(), scrollRange.height()); }
最终效果
其他细节
上述代码基本包含了主要逻辑,一些细节可能需要根据实际需要再增加逻辑。
-
缩放限制
尽管浮点数的运算能最大程度保留精度,但最好考虑在修改scale时,限定范围。 -
缩放时同步缩放图片
很少有软件会做这样的支持,毕竟支持滚动了。
但Windows自带的照片有这样功能,具体原理可以再研究。 -
缩放或者调整窗口时,锚定某个坐标不动
当变换矩阵变化、窗口resize时,QGraphicsView支持锚定某个坐标在视口中不变。具体可以文档QGraphicsView::ViewportAnchor。
可以简单这样实现:// QPointF view_pos; //指定一个视口坐标 QPointF anch_pos = mapToImage(view_pos); // ... // 其他触发变换矩阵变化的逻辑 // ... // 调整前后差异,重新滚动对齐 QPointF new_pos = mapFromImage(anch_pos); scollView(new_pos - view_pos);
-
旋转、翻转
如果只支持90°倍数旋转,直接对原图修改应该比较简单(变换矩阵是否可以做到) -
高分屏适配
将原始图像的信息除以缩放,与QWidget一致,在最后即将绘制时再换算到实际像素区域 -
抗锯齿
即便QPainter开启抗锯齿和平滑缩放图片,依然得不到好的效果……属于Qt问题,可以复制一份图片待绘制区域的像素,并手动缩放再绘制。这就需要优化效率了