背景
实现一个图片浏览器,可以支持放大/缩小查看图片。主要组件如下:
// canvaswidget.h
#ifndef CANVASWIDGET_H
#define CANVASWIDGET_H
#include <QWidget>
class CanvasWidget : public QWidget
{
Q_OBJECT
public:
explicit CanvasWidget(QImage img, QWidget *parent = nullptr);
void zoomIn();
void zoomOut();
signals:
protected:
QSize sizeHint();
void paintEvent(QPaintEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
private:
qreal scale;
QPixmap pixmap;
};
#endif // CANVASWIDGET_H
// canvaswidget.cpp
#include "canvaswidget.h"
#include <QWheelEvent>
#include <QPainter>
#include <QPixmap>
CanvasWidget::CanvasWidget(QImage img, QWidget *parent)
: QWidget{parent}, scale(1.0)
{
pixmap = QPixmap::fromImage(img);
}
void CanvasWidget::zoomIn() {
scale = fmin(scale + 0.1, 10);
update();
}
void CanvasWidget::zoomOut() {
scale = fmax(scale - 0.1, 0.1);
update();
}
void CanvasWidget::paintEvent(QPaintEvent *event) {
if(!pixmap) {
return QWidget::paintEvent(event);
}
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.setRenderHint(QPainter::SmoothPixmapTransform);
p.scale(scale, scale);
p.drawPixmap(0,0,pixmap); // draw image
}
void CanvasWidget::wheelEvent(QWheelEvent *event)
{
if(event->modifiers() == Qt::ControlModifier) {
QPointF delta = event->angleDelta();
int v_delta = delta.y();
if(v_delta > 0) {
zoomIn();
} else {
zoomOut();
}
update();
adjustSize();
} else {
QWidget::wheelEvent(event);
}
}
QSize CanvasWidget::sizeHint()
{
return QSize(800,800);
}
问题
在这种实现方式下,缩小图片时,图片会变得非常模糊,有非常明显的锯齿问题。
如下图所示,A是Windows自带图片查看器的效果,B是上述实现的效果。可以看出虽然B比A更大,但却更不清晰,有明显的锯齿。
尝试解决
为了解决这个不清晰的问题,尝试了很多种方案,方案及其实现方法如下:
不scale QPainter,而是在指定区域绘制Pixmap
p.drawPixmap(0,0,pixmap.size().width() * scale, pixmap.size().height * scale, pixmap);
使用QGraphicsView绘制图片
QPixmap pixmap("/path/to/image.png");
QGraphicsScene scene;
QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap);
scene.addItem(item);
QGraphicsView view;
view.resize(800,600);
view.setScene(&scene);
// Optionally set view properties
view.setRenderHint(QPainter::Antialiasing); // Improve rendering quality
view.setDragMode(QGraphicsView::ScrollHandDrag); // Enable dragging
view.setAlignment(Qt::AlignCenter); // Center the image
view.fitInView(item, Qt::KeepAspectRatio); // Scale to fit the view
// Show the view
view.show();
使用QWebEngineView绘制图片
QWebEngineView web_view;
QString htmlContent = R"(
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; }
img { max-width: 100%; max-height: 100%; }
</style>
</head>
<body>
<img src="/path/to/image.png" alt="Image Not Found">
</body>
</html>
)";
web_view.setHtml(htmlContent, QUrl::fromLocalFile(QCoreApplication::applicationDirPath() + "/"));
web_view.resize(800, 600);
web_view.show();
将图片作为texture在QOpenGLWidget中绘制图片
#ifndef OPENGLIMAGE_H
#define OPENGLIMAGE_H
#include <QOpenGLTexture>
#include <QOpenGLShaderProgram>
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <memory>
class OpenGLImage : public QOpenGLWidget, protected QOpenGLFunctions
{
Q_OBJECT
public:
explicit OpenGLImage(QWidget *parent = nullptr);
~OpenGLImage();
QSize minimumSizeHint() const override;
QSize sizeHint() const override;
void loadImage(QString& path);
QMatrix4x4 getViewMatrix() const;
QMatrix4x4 getModelMatrix() const;
protected:
void initializeGL() override;
void paintGL() override;
void resizeGL(int width, int height) override;
void wheelEvent(QWheelEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void keyReleaseEvent(QKeyEvent* event) override;
private:
void setupDefaultShaderProgram();
void setupDefaultTransform();
void drawImage();
void moveImage(const QPointF& cursorPos);
void rotateImage(const QPointF& cursorPos);
std::unique_ptr<QOpenGLShaderProgram> shaderProgram;
std::unique_ptr<QOpenGLTexture> texture;
std::unique_ptr<QImage> image;
QOpenGLBuffer vbo;
QOpenGLVertexArrayObject vao;
QOpenGLBuffer ebo;
bool isTextureSync;
QColor clearColor;
float norm_h;
QSize viewSize;
QVector3D cameraPos;
QVector3D imagePos;
QVector3D imageAngle;
float viewAngle;
float focalLength;
QPointF lastClickPos;
bool isRotMode;
};
#endif // OPENGLIMAGE_H
#include "glimageview.h"
#include <vector>
#include <QtMath>
#include <iostream>
#include <QResizeEvent>
#define PROGRAM_VERTEX_ATTRIBUTE 0
#define PROGRAM_TEXCOORD_ATTRIBUTE 1
#define DEFAULT_CAMERA_POS_X (0.0f)
#define DEFAULT_CAMERA_POS_Y (0.0f)
#define DEFAULT_CAMERA_POS_Z (-2.0f)
#define CLIP_NEAR (0.01f)
#define CLIP_FAR (100.0f)
#define MIN_FOCAL 1.0f
#define MAX_FOCAL 150.0f
OpenGLImage::OpenGLImage(QWidget *parent)
: QOpenGLWidget(parent),
shaderProgram(nullptr),
texture(nullptr),
image(nullptr),
isTextureSync(false),
clearColor(Qt::gray),
norm_h(-1.0f),
viewSize(640,640),
ebo(QOpenGLBuffer::Type::IndexBuffer),
viewAngle(45.0f),
isRotMode(false)
{
focalLength = 1/qTan(qDegreesToRadians(viewAngle/2.0f));
}
OpenGLImage::~OpenGLImage()
{
}
void OpenGLImage::initializeGL()
{
initializeOpenGLFunctions();
setupDefaultShaderProgram();
}
void OpenGLImage::paintGL()
{
glClearColor(clearColor.redF(), clearColor.greenF(), clearColor.blueF(), clearColor.alphaF());
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawImage();
}
void OpenGLImage::resizeGL(int width, int height)
{
viewSize = QSize(width, height);
}
QSize OpenGLImage::minimumSizeHint() const
{
int min_h = (int)(320.0f * norm_h);
return QSize(320, min_h);
}
QSize OpenGLImage::sizeHint() const
{
return viewSize;
}
void OpenGLImage::wheelEvent(QWheelEvent *event)
{
QPoint numDegrees = event->angleDelta() / 8;
float degree = (float)numDegrees.y() * -1.0f;
degree /= 2.0f;
if (viewAngle+degree > MIN_FOCAL && viewAngle+degree < MAX_FOCAL) {
viewAngle += degree;
focalLength = 1/qTan(qDegreesToRadians(viewAngle/2.0f));
}
event->accept();
update();
}
void OpenGLImage::drawImage() {
if (image.get() == nullptr) return;
glViewport(0, 0, viewSize.width(), viewSize.height());
// qDebug() << viewSize.width() << ", " << viewSize.height() << "\n";
// setup vertex array object
if (!vao.isCreated())
{
vao.create();
}
vao.bind();
// setup vertex buffer object
std::vector<GLfloat> coords;
// bottom left;
coords.push_back(-1.0f);
coords.push_back(-1.0f * norm_h);
coords.push_back(0.0f);
// tex coordinate
coords.push_back(0.0f);
coords.push_back(0.0f);
// bottom right
coords.push_back(1.0f);
coords.push_back(-1.0f * norm_h);
coords.push_back(0.0f);
// tex coordinate
coords.push_back(1.0f);
coords.push_back(0.0f);
// top right
coords.push_back(1.0f);
coords.push_back(1.0f * norm_h);
coords.push_back(0.0f);
// tex coordinate
coords.push_back(1.0f);
coords.push_back(1.0f);
// top left
coords.push_back(-1.0f);
coords.push_back(1.0f * norm_h);
coords.push_back(0.0f);
// tex coordinate
coords.push_back(0.0f);
coords.push_back(1.0f);
if (!vbo.isCreated())
{
vbo.create();
}
vbo.bind();
vbo.allocate(coords.data(), coords.size()*sizeof(GLfloat));
// setup vertex element object
// [bl, br, tr, tl]
static const std::vector<GLuint> indices {
0, 1, 2,
2, 3, 0
};
if (!ebo.isCreated())
{
ebo.create();
}
ebo.bind();
ebo.allocate(indices.data(), indices.size()*sizeof(GLuint));
// associate vertex and buffer
shaderProgram->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE);
shaderProgram->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE);
shaderProgram->setAttributeBuffer(PROGRAM_VERTEX_ATTRIBUTE, GL_FLOAT, 0, 3, 5 * sizeof(GLfloat));
shaderProgram->setAttributeBuffer(PROGRAM_TEXCOORD_ATTRIBUTE, GL_FLOAT, 3 * sizeof(GLfloat), 2, 5 * sizeof(GLfloat));
// assign transform matrices
QMatrix4x4 projection; // projection matrxi must update everytime!
float ratio = ((float)viewSize.width())/((float)viewSize.height());
projection.perspective(viewAngle, ratio, CLIP_NEAR, CLIP_FAR);
QMatrix4x4 model = getModelMatrix();
model.rotate(imageAngle.x(), 0.0f, 1.0f, 0.0f);
model.rotate(imageAngle.y()*-1.0f, 1.0f, 0.0f, 0.0f);
shaderProgram->setUniformValue("model", model);
QMatrix4x4 viewMat = getViewMatrix();
shaderProgram->setUniformValue("view", viewMat);
shaderProgram->setUniformValue("projection", projection);
// setup texture
if (texture.get() == nullptr || !isTextureSync) {
QImage& img = *image.get();
texture = std::unique_ptr<QOpenGLTexture>(new QOpenGLTexture(img));
isTextureSync = true;
}
texture->bind();
glDrawElements( GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0 );
}
void OpenGLImage::setupDefaultShaderProgram()
{
QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
const char *vsrc =
"attribute highp vec3 vertex;\n"
"uniform mediump mat4 model;\n"
"uniform mediump mat4 view;\n"
"uniform mediump mat4 projection;\n"
"\n"
"attribute mediump vec2 texCoord;\n"
"varying mediump vec2 texc;\n"
"void main(void)\n"
"{\n"
" gl_Position = projection * view * model * vec4(vertex, 1.0f);\n"
" texc = texCoord;\n"
"}\n";
vshader->compileSourceCode(vsrc);
QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment, this);
const char *fsrc =
"uniform sampler2D texture;\n"
"varying mediump vec2 texc;\n"
"void main(void)\n"
"{\n"
" gl_FragColor = texture2D(texture, texc);\n"
"}\n";
fshader->compileSourceCode(fsrc);
shaderProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram(this));
shaderProgram->addShader(vshader);
shaderProgram->addShader(fshader);
// assign locations of vertex and texture coordinates
shaderProgram->bindAttributeLocation("vertex", PROGRAM_VERTEX_ATTRIBUTE);
shaderProgram->bindAttributeLocation("texCoord", PROGRAM_TEXCOORD_ATTRIBUTE);
shaderProgram->link();
shaderProgram->bind();
shaderProgram->setUniformValue("texture", 0);
}
void OpenGLImage::setupDefaultTransform() {
cameraPos = QVector3D(DEFAULT_CAMERA_POS_X, DEFAULT_CAMERA_POS_Y, DEFAULT_CAMERA_POS_Z);
imagePos = QVector3D();
imageAngle = QVector3D();
}
void OpenGLImage::loadImage(QString& path) {
QImage* p = new QImage(QImage(path).mirrored());
image = std::unique_ptr<QImage>(p);
isTextureSync = false;
norm_h = (float)((float)image->height()/(float)image->width());
int h = (int)((float)viewSize.width()*norm_h);
viewSize = QSize(viewSize.width(), h);
resize(viewSize);
setupDefaultTransform();
}
QMatrix4x4 OpenGLImage::getViewMatrix() const {
QVector3D up(0.0f, 1.0f, 0.0f);
QMatrix4x4 ret;
ret.translate(cameraPos);
QVector3D center(cameraPos.x(), cameraPos.y(), imagePos.z());
ret.lookAt(QVector3D(), center, up);
return ret;
}
QMatrix4x4 OpenGLImage::getModelMatrix() const {
QMatrix4x4 ret;
ret.translate(imagePos);
return ret;
}
void OpenGLImage::mousePressEvent(QMouseEvent *event) {
lastClickPos = event->localPos();
qDebug() << lastClickPos;
}
// movement is weird somehow...
void OpenGLImage::mouseMoveEvent(QMouseEvent *event) {
if (isRotMode) {
rotateImage(event->localPos());
} else {
moveImage(event->localPos());
}
lastClickPos = event->pos();
event->accept();
update();
}
void OpenGLImage::moveImage(const QPointF &cursorPos) {
QPointF delta = cursorPos-lastClickPos;
float factor = qAbs(imagePos.z()-cameraPos.z()) / focalLength;
factor /= (qMax(viewSize.width(), viewSize.height()));
factor *= 3.5f;
qDebug() << "dx=" << delta.x();
qDebug() << "dy=" << delta.y();
qDebug() << "L=" << (imagePos.z()-cameraPos.z());
qDebug() << "focalLength=" << focalLength;
qDebug() << "factor" << factor;
delta *= factor;
imagePos += QVector3D(delta.x(), -1.0f*delta.y(), 0.0f);
}
void OpenGLImage::rotateImage(const QPointF &cursorPos) {
QPointF delta = cursorPos-lastClickPos;
delta.setX(delta.x() / (qreal)viewSize.width());
delta.setX(delta.x() * 180.0f);
delta.setY(delta.y() / (qreal)viewSize.height());
delta.setY(delta.y() * -180.0f);
qDebug() << delta;
imageAngle += QVector3D(delta.x(), delta.y(), 0.0f);
}
void OpenGLImage::mouseReleaseEvent(QMouseEvent *event) {
}
void OpenGLImage::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_Control) {
qDebug() << "ctrl is pressed";
isRotMode = true;
} else {
// call base class method as event is not handled.
QOpenGLWidget::keyPressEvent(event);
}
}
void OpenGLImage::keyReleaseEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_Control) {
qDebug() << "ctrl is released";
isRotMode = false;
} else {
// call base class method as event is not handled.
QOpenGLWidget::keyReleaseEvent(event);
}
}
如下图所示,不同方案的效果略有不同,但所有方案都会出现缩小后图片变模糊的问题:
问题所在
最终在网友们的帮助下,发现了问题所在:这些实现方法在修改图片大小时都会对图片进行压缩。
比如void QPainter::drawPixmap(const QRectF &target, const QPixmap &pixmap, const QRectF &source)
,在指定矩形区域内绘制图片,如果指定的矩形区域比图片本身尺寸小,绘制过程中就会对图片进行压缩,导致图片变得模糊。
如果想要将图片变小的同时,保持图片的清晰度,应该直接使用QPixmap
的scaled函数:
p.drawPixmap(0,0,pixmap.scaled(pixmap.size() * scale, Qt::KeepAspectRatio, Qt::SmoothTransformation));
效果如下,左边是新的实现方法的效果,右边是Windows自带的图片查看软件的效果:
其实我一开始的实现方法不算错,甚至是官方建议的,在QPixmap的文档中提到:
In some cases it can be more beneficial to draw the pixmap to a painter with a scale set rather than scaling the pixmap. This is the case when the painter is for instance based on OpenGL or when the scale factor changes rapidly.
图片查看器其实就会频繁改变scale
,按照建议就是应该采用修改QPainter的scale
的方法,但这种方法确实会导致图片清晰度变低,出现模糊的问题。
深究
本以为问题已经被完全解决了,直到换了一个高DPI的屏幕,又出现了新的问题。
这次没有出现锯齿问题,而是出现了模糊的问题,和Windows自带图片浏览器中的图片相比不够清晰。
关于在High DPI屏幕上的绘制问题,Qt官方文档其实提到过:
-
This is for example the case when drawing a QPixmap of 64x64 pixels size with a device pixel ratio of 2 onto a high DPI screen which also has a device pixel ratio of 2. Note that the pixmap is then effectively 32x32 pixels in user space.
-
Drawing High Resolution Versions of Pixmaps and Images
High resolution versions of pixmaps have a device pixel ratio value larger than 1 (see QImageReader, QPixmap::devicePixelRatio()). Should it match the value of the underlying QPaintDevice, it is drawn directly onto the device with no additional transformation applied.
This is for example the case when drawing a QPixmap of 64x64 pixels size with a device pixel ratio of 2 onto a high DPI screen which also has a device pixel ratio of 2. Note that the pixmap is then effectively 32x32 pixels in user space.
个人理解官方的意思是Qt会自适应高DPI的屏幕,不应该出现图片模糊的情况,没能找出具体问题出在哪里。
看到开源项目chatterino2也遇到过类似的[问题](https://github.com/Chatterino/chatterino2/issues/4552),他们的临时解决方案是在main.cpp
中添加下面代码:
#if defined(Q_OS_WINDOWS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
qputenv("QT_ENABLE_HIGHDPI_SCALING", "0");
#endif
但这个方案官方文档中提到:
This variable is intended for testing purposes only, and we do not recommend setting it on a permanent basis.
所以这种方案有可能有副作用,chatterino2后续的解决方案是调用Windows的一些原生接口,作为测试应用暂时就没必要考虑这么深了,暂时就用这个方案解决了。