Qt-图片查看器-改进3型

“改进2型”中,我们给图片查看器添加了一个缩放的功能。

这里我们添加了一个多页连续展示图片的功能,经常看网络小说的读者都知道,小说的每一页都可以连续的往下翻,好像页和页之间是连起来的那样。

“改进3型”就提供了“连续”展示多页的效果,并且还可以缩放。

本文参考自Qt中的QPdfView的实现思路。如果你读过QPdfView的源码,你就会发现本篇文章中的代码有雷同的点。

源码传送门:Aderversa/MultiImageViewer (github.com)

在这里插入图片描述

实现思路解析

在这里我提炼一下关于“改进2型”的实现思路:

假设我们要展示的图片的左上角是它的原点,以此为原点得出一个坐标系,下图的绿色部分即是图片,而绿色矩形左上角就是图片坐标系的原点:
在这里插入图片描述
我们规定好viewport的移动返回,让viewport只在绿色区域中移动,保证它不出界。这样就得到一个可以移动观察的图片查看器。

当缩放操作发生的时候,我们根据缩放重新计算viewport的位置,这样就得到一个可以移动+缩放的图片查看器。

合理使用image.translate()我们就可以达成让viewport移动的效果,具体原理请看Qt-图片查看器-改进1型-CSDN博客

现在,我们试图让多个图片在同一个viewport中展示出来,并且还可以连续滚动,如何实现呢?

参考QPdfView的实现,我得出了这样一种抽象:我们要展示的多个图片构成的整体,其实也是一张图片,只不过这个“图片”由多个图片构,我称之为虚拟图片。

通过自定义一个虚拟图片坐标系,将图片安排到指定的坐标位置上,同时制定好这个虚拟图片的大小,我们最终就可以得出这样一张虚拟图片:

在这里插入图片描述
绿色框即是我们的虚拟图片的范围,而蓝色框是viewport的范围。当我们的需要绘制viewport的内容时,我们就会查看哪个image对应的坐标rect和viewport对应的rect相交,发现相交的image,我们就把它画出来。你当然也可以全部都画,毕竟不与viewport相交的都展示不出来,但这样会浪费性能,特别是要图片有几百张的时候,无效绘制会很多。

个人感觉难的是对于缩放和虚拟坐标的运算,还有viewport的定位。我一开始对这些东西不熟悉,不知道从哪开始,完成了“改进1型”和“改进2型”之后才逐渐找到完成这个多页连续展示的整体思路。

核心代码实现

在本篇文章中,我使用QAbstractScrollArea提供的水平和垂直滚动条的值来定位viewport左上点。滚动条的移动返回即viewport的坐标范围,通过简单的数学运算,我们就可以得出水平和垂直的移动范围:

void MultiImageViewer::updateScrollBars()
{
    const QSize p = viewport()->size();
    const QSize v = m_documentLayout.documentSize; // 虚拟图片的大小
    horizontalScrollBar()->setRange(0, v.width() - p.width());
    horizontalScrollBar()->setPageStep(p.width());
    verticalScrollBar()->setRange(0, v.height() - p.height());
    verticalScrollBar()->setPageStep(p.height());
}

如何设定viewport呢?其实也非常简单,QAbstractScrollArea::viewport()->size()提供了我们的viewport的大小(这里你可以直接把viewport()看做一个画布,不过它的大小会随着窗口的大小而变化)。而我们可以通过滚动条获得左上点的坐标,那viewport的rect不就出来了吗?

void MultiImageViewer::calculateViewport()
{
    const int x = horizontalScrollBar()->value();
    const int y = verticalScrollBar()->value();
    const int width = viewport()->width();
    const int height = viewport()->height();

    setViewport(QRect(x, y, width, height));
}

void MultiImageViewer::setViewport(QRect viewport)
{
    if (m_viewport == viewport)
        return;
    const QSize oldSize = m_viewport.size();

    m_viewport = viewport;
    if (oldSize != m_viewport.size()) { // viewport大小发生变化,调整虚拟布局
        updateDocumentLayout();
    }
}

这里viewport大小变化之后需要调整虚拟图片的坐标布局,是因为:当viewport的大小太大时,我们的margins可能不够让图片居中,所以需要利用viewport本身的大小来计算新的margins;当viewport太小时,我们又必须保持margins。

并且由于,虚拟布局的大小和viewport大小决定了滚动条的范围,所以这里还会更新滚动条。

这里的重点在于,我们如何构造这样一个虚拟布局,QPdfView中功能很多,所以搭建虚拟布局的代码很长,我们这里取其精华,得到了一个简化版的虚拟布局构建算法:

MultiImageViewer::DocumentLayout MultiImageViewer::calculateDocumentLayout() const
{
    DocumentLayout documentLayout;

	// qreal是缩放因子,后续渲染的时候需要根据这个值来缩放原图
    QHash<int, QPair<QRect, qreal>> pageGeometryAndScale;

    const int pageCount = m_document.size();
    int totalWidth = 0;
    const int startPage = 0;
    const int endPage = pageCount;
    // 只是计算页面本身的大小
    for (int page = startPage; page < endPage; ++page) {
        QSize pageSize;
        qreal pageScale = m_zoomFactor;
        // 计算出缩放后的大小
        pageSize = m_document[page].size() * m_zoomFactor;
        // 考虑到不同页面的宽度可能不一,这里取大的页面的宽度
        totalWidth = qMax(totalWidth, pageSize.width());
        pageGeometryAndScale[page] = {QRect(QPoint(0,0), pageSize), pageScale};
    }
    // margins也会占用一定的viewport宽度
    totalWidth += m_documentMargins.left() + m_documentMargins.right();
    // 第一个页面的top的起始y坐标
    int pageY = m_documentMargins.left();
    // 根据页面的大小,为每个页面定义一个自定义坐标系中的位置
    for (int page = startPage; page < endPage; ++page) {
        const QSize pageSize = pageGeometryAndScale[page].first.size();
        // 如果viewpor.width > totalWidth,说明margins.left不能让页面刚好居中,应该取(viewport.width() - pageSize.width() )/ 2
        // 这样相当于调整了margins.left,让页面居中
        // 这样看来,在viewport太大的时候,totalWidth只是作为一层参考?
        const int pageX = (qMax(totalWidth, m_viewport.width()) - pageSize.width()) / 2;
        pageGeometryAndScale[page].first.moveTopLeft(QPoint(pageX, pageY));
        pageY += pageSize.height() + m_pageSpacing;
        // m_pageSpacing是垂直层面上,页面之间的距离。
    }
    pageY += m_documentMargins.bottom();

    documentLayout.pageGeometryAndScale = pageGeometryAndScale;
    documentLayout.documentSize = QSize(totalWidth, pageY);

    return documentLayout;
}

简单理解就是:先通过缩放因子计算组成虚拟图片的image的size,然后让这些image竖直排队,中间要有距离。

那么有了这个布局之后,我们的绘图操作就简化了非常多了,因为,我们不再需要将复杂的坐标计算放到paintEvent中进行:

void MultiImageViewer::paintEvent(QPaintEvent * event)
{
    QPainter painter(viewport());
    // 将背景涂黑,区分页面和背景
    painter.fillRect(event->rect(), palette().brush(QPalette::Dark));
    // 定位viewport
    painter.translate(-m_viewport.x(), -m_viewport.y());
    for (auto it = m_documentLayout.pageGeometryAndScale.cbegin();
         it != m_documentLayout.pageGeometryAndScale.cend(); ++it) {
        const QRect pageGeometry = it.value().first; // 前面计算得出:图片在虚拟图片的rect坐标
        const qreal scale = it.value().second;       // 该图片的缩放因子,上面的rect坐标正是通过这个原图和该因子得出来的
        if (pageGeometry.intersects(m_viewport)) {
            painter.fillRect(pageGeometry, Qt::white);
            const int page = it.key();
            // pageGeometry是缩放后的图片的大小,这里匹配缩放
            QImage img = m_document[page].scaled(m_document[page].size() * scale);
            painter.drawImage(pageGeometry, img);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值