
简介GMap.NET源码实例V1.7.5是一套面向.NET开发者的地图应用开发资源基于C#编写的GMap.NET开源控件可在Windows Forms、WPF及Windows Mobile环境中集成Google、Bing、OpenStreetMap、ArcGIS等地图源实现路径查找、地理编码、城市标记与缩放等常用功能同时支持C/S与B/S两种模式。压缩包共1191个文件体积59.76MB其中751个cs源码文件构成主体另有XML配置、DLL依赖、XAML界面、CSS/JS前端脚本及PNG图标等覆盖桌面端与Web端多种工程形态。内含可编译的Demo项目、不同.NET版本构建脚本NET2/NET4/CF/MONO以及codeplex未合并补丁等便于读者快速定位关键代码并运行体验也可以借助源码研究地图缓存、离线存储与跨平台坐标转换的实现思路。目前已有561人学习下载适合希望掌握地图控件二次开发、或需要在自己系统中集成多源地图能力的初中级.NET工程师参考。1. 接手地图控件项目时的一个疑问老版 GMap.NET 源码值不值得花时间上个月我在某公司维护一个老旧的桌面地图调度系统地图加载慢、缓存路径混乱、标记位置还对不齐想重做又不敢轻易换框架因为几十个业务窗口全都挂在同一个地图控件上。手里这份 GMap.NET 源码实例 V1.7.5单看版本号像个古董可真正把它翻开之后我发现它比很多新封装控件更好读核心工程就两个 dll图源、瓦片、缓存、图层的边界画得清清楚楚。它不是最新版本却是把 GMap.NET 吃透的最短路径。这篇文章就把我复现这个源码实例的过程、参数和踩过的坑拆开来讲适合正在做 WinForms/WPF 桌面地图项目、需要离线地图或自定义瓦片源的开发者也适合想通过源码实例理解地图组件原理的人。2. 把 GMap.NET 源码实例拆开控件、Provider、Overlay 与缓存四件套2.1 核心对象先分清楚GMapControl、MapProvider、GMapOverlay 的关系在使用这份源码之前先把对象关系理清不然会陷入“地图不显示又不知道改哪里”的困境。GMapControl 是核心宿主控件负责把瓦片拼成完整地图处理鼠标拖拽、缩放和坐标换算MapProvider 是抽象地图源接口决定瓦片从哪里来、URL 怎么拼、坐标系是什么GMapOverlay 是图层标记 Marker、路线 Route、多边形 Polygon 都挂载在 Overlay 上每个 Overlay 可以独立控制显示或隐藏。再加上 CacheManager 负责把瓦片按地图源、层级、行列号存到本地这四件套几乎覆盖了桌面地图应用的所有常见需求。我在项目里把它们的配合关系概括成一句话Provider 管“数据怎么取”GMapControl 管“数据怎么画”Overlay 管“业务数据放哪层”Cache 管“数据怎么存”。在 V1.7.5 的 Demo 工程里这些对象都直接由 GMapControl 暴露出来在窗体上拖入控件后就能访问开发者不用自己构造复杂的容器对象。2.2 V1.7.5 源码实例包的工程结构与依赖搞清楚每个 dll 的职责排错时能少走一半弯路。源码实例包里一般包括 GMap.NET.Core 工程和 GMap.NET.WindowsForms 工程外加一个完整的 Demo 工程和可直接运行的示例程序。Core 负责网络下载、缓存、坐标系统、Provider 基础类WindowsForms 负责渲染、Overlay、Marker、Route、Polygon、预取等界面层功能。这份源码把“网络与业务”和“界面与渲染”分得很开所以定位问题非常简便缓存和网络相关去 Core 里找绘制和图层相关去 WindowsForms 里找。这个版本依赖很少环境中只要有 .NET Framework 4.0 或 4.5 就能编译。我一般直接复制两个 dll 引用到业务工程里而不是把源码工程整个合入避免命名空间冲突和多余依赖。注意 Demo 工程里默认的缓存目录可能不是你机器上存在的路径第一次运行前要把 CacheLocation 改成实际存在的绝对路径否则会出现“运行时表现一切正常但地图区域一片空白”的现象。2.3 初始化时最容易抄错的一段示例代码源码里初始化地图一般写在窗体的 Load 事件里。这里有一个常见的错误做法有人会在窗体设计器里直接设置地图源结果 IDE 不断发起瓦片请求编辑器直接卡死。正确的方式是在代码里显式初始化。我的最小初始化骨架如下// 在窗体的 Load 事件里完成初始化避免设计器状态下触发网络请求 private void MainForm_Load(object sender, EventArgs e) { mapControl.MapProvider GMapProviders.OpenStreetMapProvider; mapControl.MinZoom 2; mapControl.MaxZoom 18; mapControl.Zoom 13; mapControl.Position new PointLatLng(30.0, 120.0); mapControl.Manager.Mode AccessMode.ServerAndCache; mapControl.Manager.CacheLocation D:\MapCache; }这段代码看着简单但每个参数都影响后续行为。MapProvider 决定图源类型MinZoom 和 MaxZoom 限制缩放级别范围超出范围的缩放请求会被忽略Zoom 是当前缩放级别取值范围必须在 MinZoom 与 MaxZoom 之间Position 是地图中心点坐标Manager.Mode 决定网络与缓存的优先级CacheLocation 决定瓦片缓存写到哪个目录。位置初始化讲究顺序先设 MapProvider 和 Zoom再设 Position。有同事一上来就设置 Position结果第一次加载总是跳到默认区域就是因为没先指定缩放级别。缓存目录我坚持用绝对路径。旧版本对相对路径的解析逻辑在各个工程里不一致有的相对当前工作目录有的相对程序集所在目录后果就是缓存分散到多个位置离线数据拷到机器上却找不着这类问题最难排查。3. 在 WinForms 里跑通 V1.7.5图源切换、图层标注与离线缓存参数3.1 从源码实例包引用 dll 并配置地图源拿到源码实例包第一步是把 Demo 工程先跑起来。确认它能在当前机器上正常运行之后再复制到正式项目。正式工程中引用两个 dllGMap.NET.dll 和 GMap.NET.WindowsForms.dll然后在工具箱里选择 GMapControl 拖到窗体上。如果工具箱里看不到可以在“选择项”对话框的 .NET Framework 组件里找到它类名通常显示为 GMapControl命名空间是 GMap.NET.WindowsForms。图源选择决定了后续所有表现。V1.7.5 内置了多个 Provider包括不需要凭证的开放街道图源、ArcGIS 在线图源以及若干国内图源。我的建议是先用内置开放图源做通配测试因为它免费、无需注册、请求头简单适合用来验证控件本身是否工作正常。确认控件能显示地图后再切换成业务实际使用的图源。部分商业图源需要注册调用凭证凭证失效时表现为“缩放级别正确但瓦片区域全是深色占位符”这一点要在项目启动时预留图源切换开关。3.2 用 Overlay 组织 Marker 与 Route真正做业务时地图上不会只有一张底图还会有设备位置、历史轨迹、电子围栏等图层。V1.7.5 的做法是把这些业务数据分别放进不同的 GMapOverlay而不是直接往 GMapControl 上塞对象。下面这段代码我在接手项目时反复使用// 按业务类型拆分成两个图层便于统一控制显示与隐藏 GMapOverlay markerOverlay new GMapOverlay(markers); GMapOverlay routeOverlay new GMapOverlay(routes); // 添加一个标记点 PointLatLng taskPoint new PointLatLng(30.001, 120.002); GMarkerGoogle marker new GMarkerGoogle(taskPoint, GMarkerGoogleType.red_dot); marker.ToolTipMode MarkerTooltipMode.OnMouseOver; marker.ToolTipText 任务点A; markerOverlay.Markers.Add(marker); // 添加一条路线 ListPointLatLng routePoints new ListPointLatLng(); routePoints.Add(new PointLatLng(30.001, 120.002)); routePoints.Add(new PointLatLng(30.002, 120.005)); routePoints.Add(new PointLatLng(30.004, 120.008)); GMapRoute route new GMapRoute(routePoints, task_route); route.Stroke new Pen(Color.FromArgb(220, 220, 20, 20), 3); routeOverlay.Routes.Add(route); // 注意添加顺序先路线后标记标记才能显示在路线之上 mapControl.Overlays.Add(routeOverlay); mapControl.Overlays.Add(markerOverlay);这段代码背后的逻辑是Overlay 的添加顺序决定了渲染顺序后添加的 Overlay 绘制在上层。实际业务中轨迹路线应放在底层标记点放在上层避免路线把图标盖住。GMarkerGoogle 构造函数的第二个参数是标记样式V1.7.5 提供 arrow、red_dot、green_dot、blue_dot、yellow_dot 等常用样式需要自定义图标时可以改用 GMarkerCross 或自己继承 GMapMarker 重写 OnRender。这里还有一个容易忽略的点Marker 的 ToolTipMode 默认不显示提示需要显式设置为 OnMouseOver否则鼠标悬停看不到任何文字。3.3 缓存策略ServerOnly、CacheOnly 与 ServerAndCache 怎么选访问模式是 GMap.NET 里最影响运行表现的参数在 V1.7.5 里通过 mapControl.Manager.Mode 设置。三种模式的区别可以用下面这张参数表说明访问模式行为适合场景注意点ServerOnly每次都请求网络不读写本地缓存临时调试查看图源瓦片结构滚动地图会重复下载流量大CacheOnly只读本地缓存瓦片缺失显示空白内网隔离、离线作业缓存一旦缺失无法自动补齐ServerAndCache先读缓存缺失再走网络并写回缓存大多数正式项目需要保证缓存目录可写且空间充足在调度系统这种长期运行的桌面程序里我几乎都会选 ServerAndCache。它兼顾加载速度和数据完整性第一次浏览时比较慢之后滚动地图基本走缓存。CacheOnly 模式不要在调试阶段开启否则图源或缓存 key 不一致时地图会静默失败连个报错都没有。还有一个细节V1.7.5 的缓存是按地图源区分的切换 MapProvider 之后旧图源的缓存不会被复用磁盘上可能出现多份重复数据做离线部署时要提前规划好缓存目录。// 推荐组合网络与缓存并用缓存目录固定为程序目录下的 MapCache mapControl.Manager.Mode AccessMode.ServerAndCache; mapControl.Manager.CacheLocation Path.Combine(AppDomain.CurrentDomain.BaseDirectory, MapCache);这里把缓存目录放到程序集同一目录方便离线部署时整体打包。如果放在桌面或用户目录不同账号登录同一台机器会导致缓存目录不一致。3.4 控件生命周期缩放、中心点与刷新时机还有一类问题跟控件生命周期有关。GMapControl 的 Position 属性赋值之后并不会立刻触发重绘需要调用 Refresh 或 Invalidate。缩放级别变化时如果当前地图源不支持某个缩放级别控件会停在最近的有效级别上而不是报错。所以我在初始化时都会检查 MaxZoom 是否与图源能力匹配比如某些图源只到 18 级代码却设到 20缩放时就会有一段区域显示空白。窗体 Resize 导致地图重绘时如果业务系统里有大量 Marker要避免在 Resize 事件里重新创建 Overlay。正确做法是初始化时创建 Overlay 和 Marker在定时器里只更新 Marker.Position 或 ToolTipText。这样既能保持界面流畅也不会让内存里的对象数量随窗口缩放不断增长。简单说这一段行为稳定之后很多“地图越来越卡”的问题其实都出在重复创建对象上。4. GMap.NET 排坑实录黑屏、坐标偏移与瓦片缓存失效4.1 窗体上拖入控件后显示黑屏现象控件拖进窗体后运行整个区域是黑的没有地图也没有网格线。原因最常见的有三种一是窗体 Load 之前就访问了地图对象的属性二是 MapProvider 没有赋值三是 Manager.Mode 是 CacheOnly 但缓存目录为空。我在某个同事机器上还遇到过一种情况初始化代码写在了窗体的构造函数里构造函数执行时控件句柄尚未创建触发了渲染异常。解决把初始化代码统一放到 Load 或 Shown 事件中并先设置 MapProvider 再设置 Zoom。如果控件在加载完成后仍是黑的再检查是否开启了 DoubleBuffered某些老版本控件在开启系统双缓冲后反而会出现刷新问题关闭掉就能恢复。4.2 地图瓦片一直转圈缓存里全是 .tmp 文件现象地图显示正常但拖动到新区域后一直处于加载状态打开缓存目录发现大量 .tmp 临时文件没有正常的图片文件。原因瓦片下载后写入缓存时进程异常中断或者瓦片服务返回的不是图片而是错误页面。在公网不稳定的环境里这种问题尤其明显。源码里做的是“先临时文件后改扩展名”程序退出太暴力就会留下大量 .tmp。解决把访问模式切成 ServerOnly 观察是否复现如果仍然加载不出来基本可以确认是瓦片源返回内容有问题。可以抓一下 URL 拼接多数图源对请求头里的 Referer 和 UserAgent 有校验如果不需要凭证的图源也不行就清空缓存目录再试。清缓存时改掉 CacheLocation 最省事不用删老目录。4.3 用线程更新当前位置导致界面假死现象用一个后台线程接收定位数据然后在循环里直接给 mapControl.Position 赋值运行几分钟后界面开始假死移动地图时反应迟钝甚至直接无响应。原因GMapControl 是 WinForms 控件只能在创建它的线程上安全访问。后台线程直接修改 Position 或 Overlay 集合会造成跨线程访问冲突轻则界面卡顿重则抛出 InvalidOperationException。解决所有地图控件操作都通过 BeginInvoke 回投到 UI 线程。代码如下// 由设备线程传入坐标通过 BeginInvoke 回投到 UI 线程 private void UpdatePositionFromDevice(double lat, double lng) { if (mapControl.InvokeRequired) { mapControl.BeginInvoke( new Actiondouble, double(UpdatePositionFromDevice), lat, lng); return; } mapControl.Position new PointLatLng(lat, lng); }这段代码的关键在于 InvokeRequired 判断和 BeginInvoke 回投。InvokeRequired 检查当前是否在 UI 线程上如果不是则把参数原样传回 UI 线程再执行一次。这里不要用 Invoke 而用 BeginInvoke因为高频定位时 Invoke 会阻塞设备线程时间长了反而会造成数据堆积。4.4 不同图源的坐标偏移现象把 GPS 设备返回的经纬度直接画到某国内图源上标记点和实际道路位置差了大概一百米所有点都朝同一个方向偏移。原因地基坐标系不一致。GPS 设备通常输出 WGS-84 坐标而部分国内图源使用 GCJ-02 坐标两者之间存在非线性偏移。GMap.NET 不会自动做坐标转换它只负责把 Position 交给图源拼瓦片源是什么坐标系画出来就是什么坐标系。解决先确定整个项目统一使用哪套坐标系再决定路线图。如果底图是国内图源GPS 数据必须先用坐标转换逻辑转成 GCJ-02 再写入 Position如果底图是 WGS-84 图源则业务坐标直接使用原始经纬度。最怕的是底图切换但坐标转换逻辑没跟着切换几套坐标混在一起就会出现“标记对不上路”的玄学问题。4.5 缓存目录的玄学换机器后离线地图加载不了现象在一台机器上预先生成了大量瓦片缓存拷到另一台机器后CacheOnly 模式下地图仍然空白缓存目录里明明有文件。原因缓存路径不一致或者缓存的 key 与当前配置不一致。V1.7.5 的缓存文件路径往往包含 Provider 名称、缩放级别、行列号只要图源 Guid 或 Name 与预取时不一致缓存文件全部对不上。另外如果程序在其他目录下没有正确写入绝对缓存路径新机器会新建一个空目录。解决把 CacheLocation 固定为程序目录下的相对子目录并将地图源配置写入配置文件。离线部署时把整个 MapCache 目录随程序一起分发同时要保证首次运行时不会因为权限问题把这个目录重定向。从那以后我每次做地图项目都会强制走一遍流程确认坐标系、锁定图源、固定缓存路径、再跑离线预取。希望帮到你。5. 进阶技巧TilePrefetcher 离线预取与自定义 Provider 思路5.1 用 TilePrefetcher 预取指定区域部署内网环境前我一般会先用 TilePrefetcher 把目标区域的瓦片提前下载下来。V1.7.5 源码实例包的 Demo 工程里自带预取窗口核心用法是把西北角和东南角两个坐标点填进去再指定最小和最大缩放级别点击开始后它就会按行列号把瓦片逐级下载。// 常见写法按矩形区域和缩放范围预取瓦片 TilePrefetcher prefetcher new TilePrefetcher(); prefetcher.Overlay mapControl.Overlays[0]; prefetcher.Shuffle false; prefetcher.Start( new PointLatLng(30.9, 119.5), // 西北角坐标 new PointLatLng(30.5, 120.4), // 东南角坐标 10, // 最小缩放级别 14); // 最大缩放级别这里两个角点用矩形框定了下载范围缩放级别范围决定要下载的层级数量。级别越多数据量越大实际操作时不要一次性把 0 到 18 级全部勾选否则可能下载出好几个 GB 的缓存文件。我一般先按业务区域圈定范围只预取 10 到 14 级够用就行。5.2 替换瓦片地址复制一个 Provider 改 URL如果项目需要接公司内部的瓦片服务不要重新写整个 Provider那样工程量太大。源码实例包里所有现成图源都继承自同一个基础类我把其中结构最简单的图源复制一份出来改掉 Name、Id 和 URL 拼接逻辑做成自定义图源。这个思路适合 V1.7.5 源码实例包里的所有 Provider属于那种“一次改对后面全通”的基础操作。从那以后我每次接手地图类项目都强制自己先走一遍这套流程确认坐标标准、锁定图源、固定缓存路径、再跑离线预取。希望帮到你。本文还有配套的精品资源点击获取