超级屌的动态测距交互功能/耗时几万个脑细胞/自研原创地图控件

一、前言说明

之前做地图控件的时候,暂时只是把基础功能做好,最基础的当然是自动按照经纬度坐标和缩放级别,加载对应的瓦片地图图片文件并绘制,其次是自定义绘制图层,可以在上面绘制折线、多边形、圆形、标注点等,然后这些图形可以支持编辑状态调整位置和范围大小等。近期客户希望能增加测距的功能,之前就已经内置了测距的函数接口,需要用户自己传入两个经纬度坐标,自动计算距离,但是其实用户很多时候,希望直接在地图界面上直接画线测距,所以花费了点时间把这个功能搞定。

测距其实就是在画折线基础上,对每一条线段,计算出对应的距离,每一条线段的距离数值显示在对应线段的结束点的位置,第一个点显示起点,末尾一个点显示所有的总距离,参考了目前市面上的百度地图、高德地图、腾讯地图等,都是按照这个模式进行的。思路有了立即开干,在原有的折线图形类增加distancemode变量,标记当前折线是否开启测距,开启了,则绘制的时候,默认要绘制每个点的手柄,可以是方形也可以是圆形,这个句柄可以参照线条的粗细自动变化。在每个点的位置的右侧绘制文本,自动累加距离,最后一个点显示总距离文本,打完收工超级完美,还可以处于编辑状态,拉伸调整并拖动整体位置,实时测距。

二、效果图

Snipaste_2026-06-15_12-53-21

三、相关代码

#include "overlaypolyline.h"
#include "overlaybasehelper.h"
#include "maputil.h"

OverlayPolyline::OverlayPolyline(const QVector<PointWorldCoord> &points, int zoom_min, int zoom_max)
    : OverlayBase(OverlayType_Polyline, zoom_min, zoom_max)
{
    this->m_arrow_size = 15;
    this->m_arrow_angle = 30;
    this->m_arrow_position = 0.5;
    this->m_arrow_center = true;
    this->m_arrow_visible = false;
    this->m_distance_mode = false;

    this->m_pen_width = 3;
    this->setPoints(points);
}

OverlayPolyline::~OverlayPolyline()
{

}

QVector<PointWorldCoord> OverlayPolyline::points() const
{
    return m_points;
}

void OverlayPolyline::setPoints(const QVector<PointWorldCoord> &points, bool redraw)
{
    m_points = points;

    //每次更新完坐标立即更新多边形区域
    m_polygon.clear();
    foreach (PointWorldCoord point, m_points) {
        m_polygon.append(point.rawPoint());
    }

    m_rect = m_polygon.boundingRect();
    if (redraw) {
        emit requestRedraw();
    }
}

void OverlayPolyline::addPoint(const PointWorldCoord &point, bool redraw)
{
    m_points << point;
    this->setPoints(m_points, redraw);
}

void OverlayPolyline::updatePoint(const PointWorldCoord &point, int index, bool redraw)
{
    if (index < points().count()) {
        m_points[index] = point;
        this->setPoints(m_points, redraw);
    }
}

int OverlayPolyline::arrowSize() const
{
    return m_arrow_size;
}

void OverlayPolyline::setArrowSize(int size, bool redraw)
{
    if (m_arrow_size != size) {
        m_arrow_size = size;
        if (redraw) {
            emit requestRedraw();
        }
    }
}

int OverlayPolyline::arrowAngle() const
{
    return m_arrow_angle;
}

void OverlayPolyline::setArrowAngle(int angle, bool redraw)
{
    if (m_arrow_angle != angle) {
        m_arrow_angle = angle;
        if (redraw) {
            emit requestRedraw();
        }
    }
}

qreal OverlayPolyline::arrowPosition() const
{
    return m_arrow_position;
}

void OverlayPolyline::setArrowPosition(qreal position, bool redraw)
{
    if (m_arrow_position != position) {
        m_arrow_position = position;
        if (redraw) {
            emit requestRedraw();
        }
    }
}

bool OverlayPolyline::arrowCenter() const
{
    return m_arrow_center;
}

void OverlayPolyline::setArrowCenter(bool center, bool redraw)
{
    if (m_arrow_center != center) {
        m_arrow_center = center;
        if (redraw) {
            emit requestRedraw();
        }
    }
}

bool OverlayPolyline::arrowVisible() const
{
    return m_arrow_visible;
}

void OverlayPolyline::setArrowVisible(bool visible, bool redraw)
{
    if (m_arrow_visible != visible) {
        m_arrow_visible = visible;
        if (redraw) {
            emit requestRedraw();
        }
    }
}

bool OverlayPolyline::distanceMode() const
{
    return m_distance_mode;
}

void OverlayPolyline::setDistanceMode(bool distance, bool redraw)
{
    if (m_distance_mode != distance) {
        m_distance_mode = distance;
        if (redraw) {
            emit requestRedraw();
        }
    }
}

int OverlayPolyline::handIndex() const
{
    return m_hand_index;
}

qreal OverlayPolyline::distance() const
{
    return MapUtil::getDistance(points());
}

QRectF OverlayPolyline::closeRect() const
{
    return m_close_rect;
}

QRectF OverlayPolyline::toQRectF() const
{
    return m_rect;
}

QPolygonF OverlayPolyline::toQPolygonF() const
{
    return m_polygon;
}

RectWorldCoord OverlayPolyline::boundingBox(int /*cur_zoom*/) const
{
    return RectWorldCoord::fromQRectF(toQRectF());
}

bool OverlayPolyline::touches(const QPointF &mouse_point, int cur_zoom)
{
    if (m_points.size() <= 1) {
        return false;
    }

    //优先判断编辑状态下按下的区域
    if (this->isEdit()) {
        m_hand_index = -1;
        for (int i = 0; i < m_hand_rects.size(); ++i) {
            if (m_hand_rects.at(i).contains(mouse_point)) {
                m_hand_index = i;
                return true;
            }
        }
    }

    //将点坐标转成路径
    QPainterPath path;
    path.moveTo(m_points.at(0).rawPoint());
    for (int i = 1; i < m_points.size(); ++i) {
        path.lineTo(m_points.at(i).rawPoint());
    }

    //当前缩放级别下/每个像素表示的纬度距离
    qreal degress = MapUtil::getDegreesPerPixel(cur_zoom);
    qreal width = m_pen_width * 2 * degress;

    //放大路径/方便生成轮廓/用于判断点是否在里面
    QPainterPathStroker stroker;
    stroker.setWidth(width);
    QPainterPath strokePath = stroker.createStroke(path);
    QList<QPolygonF> polygons = strokePath.toSubpathPolygons();
    bool exist = polygons.first().containsPoint(mouse_point, Qt::OddEvenFill);
    return exist;
}

void OverlayPolyline::draw(QPainter *painter, const RectWorldCoord &backbuffer_rect, int cur_zoom)
{
    //当前多边形区域在缓存区域内则绘制/转换成世界坐标再绘制
    if (backbuffer_rect.rawRect().intersects(toQRectF())) {
        QPolygonF points;
        foreach (PointWorldCoord point, m_points) {
            points << coordToPoint(point, cur_zoom);
        }

        int count = points.size();
        m_pen_width = pen().widthF();
        painter->setPen(pen());
        painter->drawPolyline(points);

        //绘制编辑状态
        if (this->isEdit() || this->distanceMode()) {
            m_hand_rects.clear();
            int size = this->pen().width() + 2;
            int style = this->distanceMode() ? 1 : 0;
            foreach (QPointF point, points) {
                m_hand_rects << this->drawHand2(painter, point, cur_zoom, size, style, Qt::black);
            }

#if 0
            //绘制两点中间点/用于动态增加点
            for (int i = 0; i < count - 1; ++i) {
                QPointF point = (points.at(i) + points.at(i + 1)) / 2.0;
                this->drawHand(painter, point, cur_zoom);
            }
#endif
        }

        //绘制箭头
        if (this->arrowVisible()) {
            for (int i = 0; i < count - 1; ++i) {
                QPolygonF arrow = OverlayBaseHelper::calculateArrowPoints(points.at(i), points.at(i + 1), m_arrow_size, m_arrow_angle, m_arrow_position, m_arrow_center);
                painter->drawPolyline(arrow);
            }
        }

        //绘制距离
        if (this->distanceMode()) {
            int offset = 8;
            qreal distance = 0;
            for (int i = 0; i < count; ++i) {
                QString text;
                QColor border = Qt::black;
                if (i == 0) {
                    text = "起点";
                } else if (i == count - 1) {
                    //末尾点突出颜色显示
                    border = Qt::red;
                    distance += MapUtil::getDistance(m_points.at(i - 1), m_points.at(i), true);
                    text = QString("总长: %1 公里  X").arg(distance);
                } else {
                    //计算当前点和上一个点的距离
                    distance += MapUtil::getDistance(m_points.at(i - 1), m_points.at(i), true);
                    text = QString("%1 公里").arg(distance);
                }

                //计算文本的尺寸和区域并绘制
                QSize size = this->initText(painter, text) + QSize(offset, 3);
                QRectF rect(points.at(i) + QPointF(offset, -offset), size);
                OverlayBaseHelper::drawText(painter, rect, text, border, Qt::white, Qt::black);

                //计算关闭区域/文字区域右侧部分
                if (i == count - 1) {
                    m_close_rect = rect.adjusted(rect.width() - 15, 0, 0, 0);
                    //painter->fillRect(m_close_rect, Qt::red);
                    m_close_rect = MapProjection::pointRectToCoordRect(m_close_rect, cur_zoom);
                }
            }
        }
    }
}

四、相关地址

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