Qt代码实现地图瓦片电子放大/无极缩放/可将层级拓展到22级别/精确到0.03米

一、前言说明

想实现这个功能由来已久,对标奥维地图或者qgc等地图控件,都具备这个功能,也就是当瓦片不存在的时候,先查找上级瓦片,然后裁切对应区域,最后拉伸填充到当前瓦片区域,看起来会模糊一些,但总归是效果满足人眼的需要,不然就是一个空白区域很突兀。理论上来说,地图放大的时候,上一级的瓦片之前是肯定加载过的,那就可以利用这个机制做文章,有标准公式来计算,比如当前瓦片区域对应x、y、z,他的上级瓦片是x/2、y/2、z-1,然后再根据xy的奇数偶数,来判断需要裁切四个区域的哪个区域,按照这个机制吭哧吭哧一顿输出,搞完收工非常完美,先从内存瓦片中找到对应上级的瓦片,裁切对应区域拉伸填充占位,再去联网下载瓦片,下载好了再覆盖对应区域,整体上看下来就是先一个模糊的瓦片,下载好了替换后就是一个清晰的图片。

光有这个查找上级的功能还是不够的,顺藤摸瓜能不能实现无极缩放呢?或者最少两到三个级别,观察发现市面上的其他地图软件,几乎都是2-3级别,后面发现有通用的公式,传入xy和层级,直接裁切哪个区域的图片返回,2级别是图片的八分之一,3层级就是16分之一,层级越大切割的数量越多,图片月模糊,又是一顿吭哧吭哧搞,实现了无极缩放,用户非常满意,理论上来说,只要提供一张最初的世界地图瓦片,可以无极缩放到所有层级,后面验证下来,我还是太年轻了,超过8个层级,居然用的原始图片裁切的,原来最大只能往上找8个层级,因为第八层级就已经是256等分了,如果是256的瓦片,就是一个像素要填充一个区域,全是马赛克,再小就是0,导致裁切失败返回的原图。

二、效果图

Snipaste_2026-08-16_11-24-15

Snipaste_2026-08-16_11-25-26

Snipaste_2026-08-16_11-45-23

三、相关代码

void LayerMap::draw(QPainter *painter, const RectWorldPx &backbuffer_rect, int cur_zoom) const
{
    QReadLocker locker(&m_mutex);

    //没有设置或者不可见则不用继续
    if (!m_map_adapter || !m_map_image || !isVisible(cur_zoom)) {
        return;
    }

    //没有设置瓦片地址也不用继续
    if (m_map_adapter->url().isEmpty()) {
        return;
    }

    //绘制背景颜色/形成样式风格
    if (m_bgColor != Qt::transparent) {
        painter->fillRect(backbuffer_rect.rawRect(), m_bgColor);
    }

    int z = cur_zoom;
    int size = m_tile_size;
    bool offline = m_map_adapter->isOffline();

    //计算瓦片绘制区域
    int left = qFloor(backbuffer_rect.leftPx() / size);
    int top = qFloor(backbuffer_rect.topPx() / size);
    int right = qFloor(backbuffer_rect.rightPx() / size);
    int bottom = qFloor(backbuffer_rect.bottomPx() / size);

    //绘制网格背景/绘制完不用继续
    if (ImageHelper::drawTileGrid) {
        this->drawGrid(painter, z, size, left, top, right, bottom);
        return;
    }

    //1. 先从内存中查找/找不到则记住XY/意味着需要获取图片
    //2. 内存中找不到/逐个上级瓦片电子放大/临时填充区域
    //3. 再对XY列表获取图片/本地只查找本地文件/远程只联网获取
    //4. 地址队列和标识队列只是为了方便直接传入/不用再计算
    //5. scale队列用来存放电子放大的层级/方便下面判断是否找到了电子放大的
    //6. image队列用来存放电子放大的图片/下面绘制需要用到/找到了原图则用原图/否则用电子放大的
    QList<int> listX;
    QList<int> listY;
    QList<QUrl> listUrl;
    QList<QString> listFlag;
    QList<int> listScale;
    QList<QPixmap> listImage;

#if 1
    //先从内存中查找/从中间往四周扩散/靠近地图焦点的瓦片优先查找和下载
    int centerX = (left + right) / 2;
    int centerY = (top + bottom) / 2;

    //按到中心点的距离分桶/距离近的瓦片优先处理/QMap数据会自动排序
    QMap<int, QList<QPoint> > mapTile;
    for (int x = left; x <= right; ++x) {
        for (int y = top; y <= bottom; ++y) {
            int distance = (x - centerX) * (x - centerX) + (y - centerY) * (y - centerY);
            mapTile[distance] << QPoint(x, y);
        }
    }

    QMapIterator<int, QList<QPoint> > itTile(mapTile);
    while (itTile.hasNext()) {
        itTile.next();
        foreach (QPoint point, itTile.value()) {
            int x = point.x();
            int y = point.y();
#else
    //先从内存中查找/从左往右从上到下依次查找
    for (int x = left; x <= right; ++x) {
        for (int y = top; y <= bottom; ++y) {
#endif
            if (!m_map_adapter->isTileValid(x, y, z)) {
                continue;
            }

            QUrl url;
            QString flag = this->getUrlFlag(url, x, y, z, 0);
            QPixmap pixmap = ImageHelper::findPixmap(flag);
            if (pixmap.isNull()) {
                //循环从内存中的上级瓦片中查找/找到则裁切对应区域/返回放大层级
                int scale = this->findParentPixmap(pixmap, x, y, z, size, offline);
                //如果还没找到/往下级找四张拼起来/拼接成功则把scale改为255区分上下级
                this->findChildPixmap(pixmap, scale, x, y, z, size, offline);

                listX << x;
                listY << y;
                listUrl << url;
                listFlag << flag;
                listScale << scale;
                listImage << pixmap;
            } else {
                this->drawPixmap(painter, pixmap, size, x, y, 0, offline, false);
            }
        }
    }

    //超过最大查找层级不用继续
    int level = z - m_tile_zoom;
    if (level > ImageHelper::zoomInFindLevel) {
        return;
    }

    //内存中不存在的则主动获取
    int count = listX.size();
    for (int i = 0; i < count; ++i) {
        int x = listX.at(i);
        int y = listY.at(i);
        QUrl url = listUrl.at(i);
        QString flag = listFlag.at(i);
        int scale = listScale.at(i);
        QPixmap image = listImage.at(i);

        //获取绘制用的瓦片图片并绘制/没有原图则取电子放大或者下级拼接的
        this->getAndDrawPixmap(painter, url, flag, x, y, z, size, level, scale, image, offline);
    }

    //预加载瓦片区域往四周扩散部分/方便拖动快速绘制/不用再去临时加载
    if (!offline && z <= m_tile_zoom) {
        this->prefetchImage(z, left, top, right, bottom, offline);
    }

    //绘制网格文字
    if (ImageHelper::drawTileInfo) {
        this->drawInfo(painter, z, size, left, top, right, bottom);
    }
}

QString LayerMap::getUrlFlag(QUrl &url, int x, int y, int z, int level) const
{
    url = m_map_adapter->tileQuery(x >> level, y >> level, z - level);
    return TileHelper::getUrlFlag(url.toString(), m_tile_size, m_epsg_type);
}

int LayerMap::findParentPixmap(QPixmap &pixmap, int x, int y, int z, int size, bool offline) const
{
    //要过滤下最小级别不能小于1
    int scale = 0;
    int maxLevel = qMin(ImageHelper::zoomInFindLevel, z - 1);
    for (int level = 1; level <= maxLevel; ++level) {
        QUrl url;
        QString flag = this->getUrlFlag(url, x, y, z, level);
        pixmap = ImageHelper::findPixmap(flag);

        //离线状态还需要从文件获取
        if (offline && pixmap.isNull()) {
            pixmap = m_map_image->getImage(url, flag, offline, false);
        }

        if (!pixmap.isNull()) {
            scale = level;
            MapHelper::copyPixmapGrid(pixmap, size, x, y, level);
            break;
        }
    }

    return scale;
}

void LayerMap::findChildPixmap(QPixmap &pixmap, int &scale, int x, int y, int z, int size, bool offline) const
{
    //如果还没找到/往下级找四张拼起来/当前级一张瓦片对应下一级四张瓦片
    if (!offline && ImageHelper::zoomOutFindLevel > 0 && pixmap.isNull()) {
        int nextX = x * 2;
        int nextY = y * 2;
        int nextZ = z + 1;
        QPixmap pixs[2][2];
        for (int i = 0; i < 2; ++i) {
            for (int j = 0; j < 2; ++j) {
                QUrl url;
                QString flag = this->getUrlFlag(url, nextX + i, nextY + j, nextZ, 0);
                QPixmap pix = ImageHelper::findPixmap(flag);
                if (!pix.isNull()) {
                    pixs[i][j] = pix;
                } else {
                    //只要有一张没找到则不用继续
                    return;
                }
            }
        }

        //四张瓦片都找到则拼接成一张大图/正好是当前瓦片的完整区域
        //pixs[i][j]中i是x偏移对应列/j是y偏移对应行/按坐标顺序绘制
        scale = 255;
        MapHelper::mergePixmapGrid(pixmap, size, pixs[0][0], pixs[1][0], pixs[0][1], pixs[1][1]);
    }
}

void LayerMap::getAndDrawPixmap(QPainter *painter, QUrl url, QString flag, int x, int y, int z, int size, int level, int scale, const QPixmap &image, bool offline) const
{
    //查找上一个层级的瓦片
    if (level > 0) {
        flag = this->getUrlFlag(url, x, y, z, level);
    }

    //获取到的上一级瓦片/还需要裁切对应区域
    QPixmap pixmap = m_map_image->getImage(url, flag, offline, false);
    if (level > 0 && !pixmap.isNull()) {
        MapHelper::copyPixmapGrid(pixmap, size, x, y, level);
    }

    //找到了原图则用原图/没有则取电子放大或者下级拼接的
    int level2 = level;
    if (scale > 0 && pixmap.isNull()) {
        level2 = scale;
        pixmap = image;
    }

    //绘制瓦片到对应网格
    this->drawPixmap(painter, pixmap, size, x, y, level2, offline, scale == 0);
}

void LayerMap::drawPixmap(QPainter *painter, QPixmap &pixmap, int size, int x, int y, int level, bool offline, bool fill) const
{
    bool isNull = pixmap.isNull();
    if (fill && isNull) {
        pixmap = offline ? ImageHelper::getNotFoundPixmap(size) : ImageHelper::getLoadingPixmap(size);
    }

    //绘制瓦片到对应网格/可能是电子放大的图片/需要填充缩放
    if (!pixmap.isNull()) {
        MapHelper::scalePixmap(pixmap, size);
        QPoint point(x * size, y * size);
        painter->drawPixmap(point, pixmap);

        //绘制查找到的瓦片层级/裁切的图片
        if (!isNull && ImageHelper::drawTileInfo) {
            QString text = QString::number(level > 0 ? level : 0);
            OverlayBaseHelper::drawText(painter, point, text, Qt::green, size, Qt::AlignRight);
        }
    }
}

void LayerMap::drawGrid(QPainter *painter, int z, int size, int left, int top, int right, int bottom) const
{
    static QPixmap pixmap = ImageHelper::getNotFoundPixmap(size);
    for (int x = left; x <= right; ++x) {
        for (int y = top; y <= bottom; ++y) {
            if (m_map_adapter->isTileValid(x, y, z)) {
                this->drawPixmap(painter, pixmap, size, x, y);
            }
        }
    }
}

void LayerMap::drawInfo(QPainter *painter, const QPointF &point, int x, int y, int z, int size) const
{
    QString text = QString("z=%3\nx=%1\ny=%2").arg(x).arg(y).arg(z);
    OverlayBaseHelper::drawText(painter, point, text, Qt::red, size, Qt::AlignLeft | Qt::AlignVCenter);
}

void LayerMap::drawInfo(QPainter *painter, int z, int size, int left, int top, int right, int bottom) const
{
    for (int x = left; x <= right; ++x) {
        for (int y = top; y <= bottom; ++y) {
            if (m_map_adapter->isTileValid(x, y, z)) {
                QPoint point(x * size, y * size);
                this->drawInfo(painter, point, x, y, z, size);
            }
        }
    }
}

四、相关地址

  1. 国内站点:https://gitee.com/feiyangqingyun
  2. 国际站点:https://github.com/feiyangqingyun
  3. 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 文件地址:https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A 提取码:o05q 文件名:bin_mapwidget.zip

五、功能特点

  1. 支持各种地图源,包括天地图、高德地图、腾讯地图、谷歌地图、微软地图、电子海图、ArcGis、OpenStreet、360地图等。
  2. 同时支持WGS-84地球坐标系和GCJ-02火星坐标系,如果是天地图、谷歌地图,自动应用WGS-84坐标系,如果是高德地图、腾讯地图,自动应用GCJ-02坐标系。也可以主动设置在WGS-84坐标系加载高德地图,内部会自动处理坐标偏移。
  3. 同时支持默卡托投影和经纬度投影,比如天地图的地图源可以是默卡托投影或者经纬度投影。
  4. 每一种地图源都支持在线和离线两种场景需求,可以自定义在线瓦片地址格式和离线瓦片地址格式。
  5. 多线程下载和加载瓦片图片文件,多线程绘制,自动缓存瓦片文件。
  6. 在线模式下,可以开启是否缓存文件,指定缓存路径,将下载的瓦片文件存放到本地,默认优先从缓存文件查找,如果存在缓存文件则加载缓存文件,不存在则联网下载。
  7. 可以任意拖曳地图,鼠标滚轮放大和缩小地图,以鼠标所在位置作为缩放中心点,提供缩放控件手动单击进行操作,支持鼠标左键双击放大和右键双击缩小。
  8. 多图层机制,支持多个瓦片叠加图层和图形绘制图层,全部采用双缓冲技术,所有的图形和瓦片全部绘制到一个图片文件上,最终再将图片文件绘制到地图控件。不可见区域的图层包括覆盖物不会触发绘制,最大程度降低CPU占用。
  9. 动态图层机制,瓦片图层和图形图层分离,图形图层不更新的情况下,动态更新底图瓦片图层,任意切换更新多个瓦片图层。无需重新加载整个地图。
  10. 预加载机制,默认绘制的图层大小以当前区域往四周放大一倍,这样在鼠标拖动和缩放的时候,不会看到明显的加载过程,因为已经提前预加载好了,体验友好。
  11. 内置了多种图形覆盖物,包括坐标点、纯文本、标注点、折线条、多边形、矩形、圆形、雷达、点聚合、热力图等,可以设置边框颜色粗细、填充颜色和透明度等众多参数。
  12. 标注点支持旋转角度和提示文本,其中提示文本可以设置在标注点的相对位置,标注点图片支持gif动图,可以动态切换静态图和动图。
  13. 标注点和提示文本可以设置相对位置,位置包括左侧、右侧、上侧、下侧、中间、左上角、右上角、左下角、右下角。标注点默认按照底部居中对齐,一般圆形图标可以设置中心点对齐。
  14. 标注点提示文本可设置背景颜色,透明度、颜色边框和粗细,支持换行和多行文字。
  15. 所有的图形可以动态更新前景色、颜色粗细、背景颜色、颜色透明度等。
  16. 支持删除单个图形、删除一种类型的图形、删除所有图形、隐藏单个或者所有图形等。
  17. 支持动态绘制各种图形,开启后直接在地图上鼠标按下绘制,鼠标右键结束绘制,非常方便快捷。
  18. 在对应图形区域鼠标按下,发出图形单击信号,精准识别单击区域,比如折线以鼠标在折线条上作为判断依据,多边形区域以鼠标在整个多边形区域内为准,而不是以矩形区域,包括圆形也是以圆形内部为准。
  19. 可以动态启动禁用比例尺、十字线、缩放控件、地图拖曳、键盘操作、滚轮缩放、双击缩放、鼠标追踪等特性。
  20. 可以任意指定经纬度区域进行瓦片拼接保存成图片文件,也可以直接对整个可视区域或者缓存区域的地图图片文件保存。支持任意多边形轮廓保存成图片,比如某个行政区的瓦片保存。
  21. 图形可以动态设置zindex层叠顺序,值越大,越显示在前面,内部维护着一个zindex表,默认按照添加的先后顺序增加,后面添加的显示在前面,主动设置后,按照设置的zindex来绘制。
  22. 支持将QWidget对象作为覆盖物添加到地图控件中,跟随地图移动位置,极大提高灵活性,比如可以将自定义控件直接作为地图控件的子对象加入进去。
  23. 支持测距和测面积,动态绘制多边形区域,实时动态调整位置和点坐标自动重新计算,可以关闭单个测距测面的图形。
  24. 内置MarkerMove轨迹移动类,支持历史轨迹数据回放和实时轨迹移动,可设置图标、轨迹线的颜色和粗细、移动速度、移动间隔、平滑移动、自动中心等,支持多条轨迹线条同时移动。
  25. 内置MarkerLine航迹规划类,支持动态添加航迹点,显示对应箭头,可以动态拖曳调整航迹点的位置,选中点高亮显示。
  26. 大量使用按需绘制机制,包括内部提供合理的默认值来触发绘制,也可以手动传入参数指定是否需要立即绘制,比如删除了某个覆盖物,有些频繁的操作可以不指定立即绘制,等操作完成后再统一一起绘制,效率更高。
  27. 默认开启内存瓦片机制,所有加载过的瓦片文件都存储在内存中,下次再次绘制直接从内存取出来绘制,既不需要从联网获取,也不需要从缓存文件获取,直接内存取出来绘制,响应迅速效率最高体验最佳。
  28. 支持批量添加覆盖物,比如几万个标注点和圆形,都是瞬间完成绘制,相比web网页的方式,性能提升百倍以上。
  29. 支持街道图、卫星图、混合图、路网图等各种图层,可以任意叠加N个图层,甚至杂交不同地图厂家的瓦片文件。
  30. 内置缩放动画效果,在缩放的时候先截取上一个地图画面作为缩放动画基准图片,逐步过渡到下一个层级。
  31. 内置电子放大机制,瓦片图片不存在的时候,优先获取该区域上一图层的瓦片,按照规则裁切图片缩放填充,支持无极缩放。在线和离线模式都支持电子放大,在线情况下主动获取上一级的瓦片,离线情况下主动本地文件查找。
  32. 电子放大除了支持找上级瓦片,还支持找下级瓦片,比如地图缩小的时候,先找4张下级瓦片按照规则合并到对应区域瓦片,防止缩小的时候看到空白区域。
  33. 支持双指捏合缩放,在移动端或者任何可以触摸的地方,双指拉大放大地图,双指聚拢缩小地图,平滑动画过渡缩放。
  34. 提供离线地图下载模块,多线程高速并发下载,可框选指定区域下载瓦片,可指定任意级别下载。
  35. 提供高程海拔显示模块,离线识别高程海拔数据,传入经纬度即可,无需引入任何第三方模块。
  36. 纯QWidget绘制,非qml也非web,不依赖qml或者浏览器控件,没有任何第三方库依赖,支持极低性能的嵌入式环境。
  37. 原创轻量级,核心代码5000行,整体代码10000行,架构漂亮,注释详细,拓展方便,容易学习,适合各种初学者和进阶者,方便二次开发。
  38. 支持任意Qt版本、任意系统、任意编译器,包括嵌入式linux和各种国产电脑环境。
  39. 全部原创自研,古法编程,不含任何AI代码,持续迭代更新,商业级别、品质保证。
posted @ 2026-08-20 13:15  飞扬青云  阅读(24)  评论(0)    收藏  举报