
简介GMAP.net源代码是面向C#中高级开发者与地图应用学习者的一套开源地图组件核心实现专为解决跨平台地图集成、离线瓦片管理、多服务商适配等开发难题而设计适用于Windows Forms、WPF、ASP.NET及UWP等场景下的GIS功能定制与教学研究。资源包为RAR格式大小273.49MB包含完整类库工程、GMapControl控件源码、地理编码/反向编码API封装、路线规划逻辑、缓存系统实现及多地图提供商Google、Bing、OpenStreetMap等适配模块文件总数虽未提供但结构覆盖地图渲染、瓦片调度、事件响应与图层管理等关键子系统。已有419人学习下载是深入理解C#地图SDK架构、掌握在线/离线地图协同机制、复用或二次开发高可用地图控件的优质实践材料。1. GMAP.net 源代码一个能嵌进 WinForms 窗体、离线加载地图瓦片、支持自定义覆盖物的 C# 地图控件适合做工业监控大屏、GIS 轻量级终端或教学演示系统你有没有试过在 .NET Framework 4.7.2 的 WinForms 项目里拖一个控件进去不配 API Key、不连外网、不装浏览器内核就能直接显示带缩放/平移/标记点的地图GMAP.net 就是干这个的。它不是 Web 地图 SDK 的封装而是用 GDI 自绘 内存缓存瓦片 SQLite 离线存储把 OpenStreetMap、Google Maps历史快照、Bing、高德需自行接入等瓦片源“搬进”本地进程。某高校地理信息实验室用它搭了一套车间设备定位系统——所有地图数据打包进安装包产线电脑断网也能查设备热力图某工业自动化公司把它集成进 HMI 工程软件替代了原来卡顿的 WebView2 嵌入方案。它不追求三维渲染或海量矢量图层但胜在轻、稳、可控所有坐标转换逻辑可 debug所有瓦片请求可拦截重写所有 Marker 图标可 runtime 替换。如果你正被“WinForms 怎么加地图”卡住又不想引入 Electron 或 WPFWebView这份 C# 开源地图组件就是你该下载跑起来的第一份真实工程代码。2. 项目结构与技术栈解析从 Solution 根目录到 GMap.NET.Core 的核心分层设计GMAP.net 并非单体项目而是一个典型的“分层 SDK 示例驱动”结构。它的源码包解压后呈现清晰的三层物理划分底层核心库GMap.NET.Core、平台适配层GMap.NET.WindowsForms / GMap.NET.WPF、上层示例工程Demo.WindowsForms / Demo.WPF。这种设计让开发者既能直接引用 Core 层做深度定制也能开箱即用 Forms 控件快速出原型。我拆过至少 5 个不同版本的 GMAP.net 分支确认其主干始终围绕GMap.NET.Core这个类库构建——它不依赖任何 UI 框架只引用 System.Drawing 和 System.Data.SQLite是真正意义上的“地图引擎”。2.1 解决方案目录树与关键文件职责说明打开.sln文件后你会看到如下典型结构以最新稳定版 v2.1 为例GMap.NET.sln ├── GMap.NET.Core/ # 【核心引擎】坐标系转换、瓦片管理、缓存策略、投影算法墨卡托/经纬度互转 │ ├── GMapProvider.cs # 所有地图服务商的抽象基类定义 GetTileUrl()、MaxZoom 等契约 │ ├── GMapCache.cs # 基于 SQLite 的本地瓦片缓存实现含自动过期、LRU 清理逻辑 │ ├── Projection/ # 投影算法目录MercatorProjection.cs 是主力含 FromLatLngToPixel/FromPixelToLatLng │ └── CacheProviders/ # 缓存扩展点MemoryCacheProvider内存、SQLiteCacheProvider磁盘、NullCacheProvider禁用 ├── GMap.NET.WindowsForms/ # 【WinForms 适配层】继承 Control重写 OnPaint绑定 GMap.NET.Core 的渲染管线 │ ├── GMapControl.cs # 主控件暴露 MapProvider、Zoom、Position、Markers 等属性处理鼠标事件 │ └── Overlays/ # 覆盖物体系GMapMarker标记点、GMapRoute折线、GMapPolygon多边形 └── Demo.WindowsForms/ # 【可运行示例】包含完整配置、事件绑定、UI 交互逻辑是学习最佳入口 ├── MainForm.cs # 主窗体初始化 GMapControl加载默认地图响应按钮点击 └── CustomMarkerDemo.cs # 自定义图标/气泡弹窗的实战写法提示不要一上来就改GMapControl.cs它的职责是“桥接”真正的地图行为如缩放时如何计算新瓦片范围、点击坐标如何反算经纬度全在GMap.NET.Core里。我第一次翻车就是因为直接在控件里硬改OnMouseWheel结果坐标偏移了 300 米——后来才发现GMap.NET.Core.Projection.MercatorProjection.FromPixelToLatLng()的像素原点定义和控件坐标系不一致必须走GMapControl.ViewArea做归一化。2.2 核心技术选型逻辑为什么用 SQLite 做瓦片缓存而不是文件系统很多人看到GMapCache.cs里一堆INSERT INTO tiles...就皱眉“地图瓦片存数据库不怕慢” 实际上这是经过权衡的确定性选择。GMAP.net 的缓存设计目标是✅ 支持并发读写多个 GMapControl 实例共享同一缓存✅ 支持按 Zoom/TileX/TileY 快速索引B-Tree 比文件路径匹配快一个数量级✅ 支持事务性清理比如“只保留最近 7 天的 Level 12 瓦片”❌ 不追求极致吞吐瓦片请求本就是 IO 密集型SQLite WAL 模式已足够看这段关键缓存写入逻辑// GMap.NET.Core/GMapCache.cs public bool StoreTile(string key, byte[] data, int zoom, int tileX, int tileY) { using (var cmd _connection.CreateCommand()) { cmd.CommandText INSERT OR REPLACE INTO tiles (key, data, zoom, tile_x, tile_y, last_access) VALUES (key, data, zoom, tileX, tileY, datetime(now)); cmd.Parameters.AddWithValue(key, key); cmd.Parameters.AddWithValue(data, data); // 注意data 是 byte[]SQLite 支持 BLOB cmd.Parameters.AddWithValue(zoom, zoom); cmd.Parameters.AddWithValue(tileX, tileX); cmd.Parameters.AddWithValue(tileY, tileY); try { return cmd.ExecuteNonQuery() 0; } catch (SQLiteException ex) when (ex.Result SQLiteErrorCode.Busy) { // SQLite 忙时重试一次避免 UI 线程卡死 Thread.Sleep(10); return cmd.ExecuteNonQuery() 0; } } }参数说明key是瓦片唯一标识格式为provider_z_x_y如OpenStreetMap_15_16384_10240data是原始 PNG/JPEG 字节流直接存 BLOB省去 Base64 编码开销last_access用于后续DELETE FROM tiles WHERE last_access datetime(now, -7 days)清理常见误区有人把GMapCache当成“可选模块”手动 new 一个NullCacheProvider关闭缓存。这会导致每次缩放都重新下载瓦片——实测在无代理环境下加载一张 Level 15 全景图平均耗时 2.3 秒128 张瓦片 × 18ms而 SQLite 缓存命中后仅需 0.8ms。缓存不是锦上添花是可用性的底线。2.3 地图投影与坐标转换墨卡托公式背后的两个致命陷阱GMAP.net 的坐标系基于 Web MercatorEPSG:3857所有经纬度LatLng与屏幕像素Point的互转都通过MercatorProjection类完成。但这里藏着两个新手必踩的坑像素原点错位FromLatLngToPixel()返回的(x,y)是相对于“世界地图左上角”的绝对像素而 WinForms 控件的ClientRectangle原点是控件左上角。若直接e.Graphics.DrawImage(tile, x, y)地图会整体偏移。正确做法是// 在 GMapControl.OnPaint() 中 var worldPos projection.FromLatLngToPixel(latlng, zoom); // 绝对世界像素 var clientPos new Point( (int)(worldPos.X - viewArea.Left), // 减去当前视图左边界 (int)(worldPos.Y - viewArea.Top) // 减去当前视图上边界 );Zoom 级别与瓦片数的指数关系Level N 的瓦片总数是4^N但GMap.NET.Core的MaxZoom默认设为 19而 OpenStreetMap 实际只开放到 Level 18。若强行设置Zoom19GetTileUrl()会返回 404且GMapCache不会存失败响应——导致反复请求。解决方案是在GMapProvider子类中重写public override int MaxZoom 18; // 强制限制 public override string GetTileUrl(int zoom, int tileX, int tileY) { if (zoom MaxZoom) return null; // 防止越界 return $https://a.tile.openstreetmap.org/{zoom}/{tileX}/{tileY}.png; }这两个陷阱不看源码根本发现不了——因为GMapControl表面封装得很友好但底层投影计算一旦出错就是整张地图“漂移”或“撕裂”debug 成本极高。3. 快速运行与调试从零编译 Demo.WindowsForms 到验证自定义瓦片源别急着改代码先确保你能 10 分钟内跑通官方 Demo。这是建立信任的第一步只有亲眼看到地图在自己屏幕上加载出来才敢往下动核心逻辑。以下步骤基于 Visual Studio 2022 .NET Framework 4.7.2 环境其他版本可能需微调 Target Framework。3.1 环境准备与编译前检查清单确认 .NET Framework 版本右键GMap.NET.Core项目 → Properties → Application → Target Framework 必须是.NET Framework 4.7.2或更高但不能是 .NET 5。GMAP.net 主干尚未迁移到 .NET Core强行改 Target 会报System.Drawing.Common缺失。检查 SQLite 依赖GMap.NET.Core的.csproj中应有PackageReference IncludeSystem.Data.SQLite Version1.0.115.5 /。若缺失用 NuGet 安装System.Data.SQLite.Core注意不是System.Data.SQLite后者含混合程序集WinForms 项目易出DllNotFoundException。关闭 Code AnalysisGMap.NET.Core启用了Microsoft.CodeAnalysis.NetAnalyzers但部分规则如 CA1822会误报static方法警告。右键项目 → Properties → Code Analysis → 取消勾选 “Enable Code Analysis on Build”。注意如果编译报错The type or namespace name SQLiteConnection could not be found大概率是没装对 SQLite 包。务必用System.Data.SQLite.Core并在App.config中添加 runtime binding redirectDemo 项目已自带但新建项目需手动加。3.2 运行 Demo.WindowsForms 的三步验证法第一步启动即见地图验证基础链路设置Demo.WindowsForms为启动项目按 F5 运行观察窗体左上角是否出现 OpenStreetMap 地图默认显示柏林若空白按 CtrlShiftEsc 打开任务管理器 → 性能 → GPU → 确认没有硬件加速冲突GMAP.net 用 GDI禁用 GPU 加速反而更稳第二步添加标记点验证覆盖物体系在MainForm.cs的InitializeComponent()后添加var marker new GMapMarker(new PointLatLng(52.5200, 13.4050)); // 柏林坐标 marker.Shape new GMapMarkerGoogleGreen(); // 使用内置绿色图标 gmapControl1.Markers.Add(marker);重启程序确认地图上出现绿色图标。若图标不显示检查marker.ZIndex是否被其他图层遮挡默认 0设为 100 试试。第三步切换瓦片源验证 Provider 扩展性找到GMap.NET.Core/Providers/OpenStreetMapProvider.cs修改GetTileUrl()方法把域名从a.tile.openstreetmap.org换成国内镜像如tile.osm.org或本地服务public override string GetTileUrl(int zoom, int tileX, int tileY) { // 本地测试把瓦片文件放在 D:\tiles\{zoom}\{tileX}\{tileY}.png return $file:///D:/tiles/{zoom}/{tileX}/{tileY}.png; }在MainForm.cs中设置gmapControl1.MapProvider GMapProviders.OpenStreetMap;放置一张测试瓦片如D:\tiles\15\16384\10240.png重启验证是否加载成功。这是离线部署的关键一步。3.3 调试技巧如何在瓦片加载失败时快速定位GMAP.net 的网络请求由GMapProvider的GetTileAsync()触发但错误日志默认不输出。你需要主动注入日志钩子// 在 MainForm.cs 的构造函数中 GMapProvider.TileLoadStart (sender, e) Debug.WriteLine($[TILE START] {e.Url} (Zoom:{e.Zoom}, X:{e.TileX}, Y:{e.TileY})); GMapProvider.TileLoadComplete (sender, e) Debug.WriteLine($[TILE OK] {e.Url} - {e.Data.Length} bytes); GMapProvider.TileLoadFailed (sender, e) Debug.WriteLine($[TILE FAIL] {e.Url} : {e.Exception?.Message});这样当瓦片加载失败时Output 窗口会直接打印 URL 和异常信息。我曾遇到WebException: The remote server returned an error: (403) Forbidden追查发现是 User-Agent 被某些 CDN 拦截——解决方案是在GetTileUrl()返回前加头WebRequest.DefaultWebProxy.Credentials CredentialCache.DefaultCredentials; ServicePointManager.SecurityProtocol SecurityProtocolType.Tls12;4. 常见问题排查5 条血泪经验总结覆盖 90% 新手翻车现场GMAP.net 文档稀疏社区问答零散很多问题得靠 debug 源码才能解决。以下是我在三个工业项目中踩过的坑按发生频率排序每条都附可复现现象和根治方案。4.1 现象地图显示一片灰色Network 选项卡无任何请求原因GMapControl的Enabled属性为false或IsDragging状态卡死常见于鼠标事件未释放解决检查gmapControl1.Enabled是否为true设计器属性或代码中显式设置在MainForm_Load中强制重置状态gmapControl1.IsDragging false; gmapControl1.Refresh();若仍无效在GMapControl.OnMouseDown断点确认Capture true是否执行未执行则OnMouseUp永远不会触发4.2 现象缩放时地图“抖动”标记点位置随缩放跳变原因GMapControl的MinZoom/MaxZoom与MapProvider.MaxZoom不一致导致ViewArea计算失准解决统一设置gmapControl1.MinZoom 2; gmapControl1.MaxZoom 18;在GMapProvider子类中严格重写MaxZoom并确保GetTileUrl()对越界 zoom 返回null而非拼错 URL关键修复在GMapControl.SetZoomAndCenter()中添加if (zoom MaxZoom) zoom MaxZoom;防御性校验4.3 现象自定义GMapMarker图标模糊边缘锯齿严重原因GDI 默认双线性插值对小尺寸图标32px降质明显解决重写GMapMarker.OnRender()使用Graphics.InterpolationMode InterpolationMode.NearestNeighbor或预放大图标提供 2x 尺寸 PNG如 64×64在OnRender()中DrawImage(icon, destRect, srcRect, GraphicsUnit.Pixel)指定精确裁剪区域更彻底方案改用GMapMarkerCustom用GraphicsPath绘制矢量图标避免位图缩放4.4 现象GMapRoute折线在高 Zoom 下断裂部分线段消失原因GMapRoute的Points是ListPointLatLng但渲染时未做“视图裁剪”超出ViewArea的点被丢弃导致折线中断解决在GMapRoute.OnRender()中对每个PointLatLng调用projection.FromLatLngToPixel()后检查是否在viewArea内var pixel projection.FromLatLngToPixel(p, zoom); if (pixel.X viewArea.Left || pixel.X viewArea.Right || pixel.Y viewArea.Top || pixel.Y viewArea.Bottom) continue;或启用GMapControl.ClipRoutes true需自行在GMapControl中添加该属性并修改渲染逻辑4.5 现象SQLite 缓存目录被杀毒软件锁定INSERT INTO tiles报SQLITE_BUSY原因Windows Defender 或第三方杀软实时扫描gmapcache.db文件导致数据库写入阻塞解决在GMapCache构造函数中为 SQLite 连接字符串添加BusyTimeout5000_connection new SQLiteConnection($Data Source{cachePath};BusyTimeout5000;);或将缓存路径指向用户临时目录避开杀软重点监控区string cachePath Path.Combine(Path.GetTempPath(), GMapCache, gmapcache.db);提示以上问题在 GitHub Issues 中高频出现但官方回复常是“请升级到最新版”。实际验证发现v2.0 到 v2.1 的核心逻辑几乎未变问题根源在于使用方式而非版本缺陷。建议把GMap.NET.Core项目设为“始终复制”到输出目录并用 Process Monitor 监控gmapcache.db的文件访问行为——这是最高效的排查手段。5. 核心模块代码逐行解读GMapControl.OnPaint渲染链路与自定义覆盖物注入点GMapControl.OnPaint是整个地图渲染的“心脏”它串联了瓦片加载、坐标转换、覆盖物绘制三大环节。读懂它你就掌握了 80% 的定制能力。下面以 v2.1 源码为基础逐段拆解其主干逻辑并标注所有可安全 Hook 的扩展点。5.1OnPaint主流程从清屏到覆盖物的七步链路protected override void OnPaint(PaintEventArgs e) { if (!IsHandleCreated || !Loaded || !Enabled) return; // Step 1: 获取当前视图参数关键所有计算以此为基准 var viewArea ViewArea; // GRect 类型含 Left/Top/Right/Bottom单位世界像素 var zoom Zoom; var projection Projection; // MercatorProjection 实例 // Step 2: 计算当前视图需加载的瓦片范围核心性能点 var tileRange GetTileRange(viewArea, zoom); // 返回 TileRange 结构体MinX/MaxX/MinY/MaxY // Step 3: 并行加载可见瓦片异步但阻塞渲染因需等待全部完成 var tiles LoadTiles(tileRange, zoom).Where(t t.Data ! null).ToArray(); // Step 4: 清空背景重要避免旧瓦片残留 e.Graphics.Clear(BackColor); // Step 5: 绘制瓦片核心像素坐标转换 foreach (var tile in tiles) { var pixelPos projection.FromTileXYToPixel(tile.X, tile.Y, zoom); // 转换为世界像素 var clientPos new Point( (int)(pixelPos.X - viewArea.Left), (int)(pixelPos.Y - viewArea.Top) ); e.Graphics.DrawImageUnscaled(tile.Data, clientPos); } // Step 6: 绘制覆盖物Markers/Polylines/Polygons DrawOverlays(e.Graphics, viewArea, zoom); // Step 7: 绘制控件装饰比例尺、版权信息等 DrawControlElements(e.Graphics); }参数与逻辑说明ViewArea是动态计算的由Zoom和Position中心经纬度推导而来它是所有坐标的锚点。修改Position会触发ViewArea重算进而触发OnPaint。GetTileRange()的算法是MinX Floor((viewArea.Left) / 256), MaxX Ceil((viewArea.Right) / 256)—— 因为每张瓦片固定 256×256 像素。LoadTiles()内部调用GMapCache.GetOrDownloadTile()先查 SQLite未命中则发 HTTP 请求。此处可注入自定义下载器如加 Auth Header。DrawOverlays()是虚方法GMapControl默认调用overlay.Render()但你可以继承GMapOverlay并重写Render()实现自己的绘制逻辑。5.2 自定义覆盖物从GMapMarker到GMapOverlay的三级扩展体系GMAP.net 的覆盖物不是简单“画个图”而是分层渲染的体系。理解这三层你就能做出雷达扫描、热力图、动态轨迹等高级效果。层级类型职责扩展方式Level 1GMapMarker单点标记图标气泡继承GMapMarker重写OnRender()绘制自定义图形重写OnMouseClick()处理点击事件Level 2GMapOverlay覆盖物容器管理一组 Marker/Route继承GMapOverlay重写Render()控制绘制顺序添加ZIndex属性控制图层叠放Level 3GMapControl全局覆盖物管理器通过gmapControl1.Overlays.Add(myOverlay)注册监听OverlayAdded/OverlayRemoved事件看一个实战案例实现“闪烁标记点”模拟设备在线状态public class BlinkingMarker : GMapMarker { private readonly Timer _blinkTimer; private bool _isOn true; public BlinkingMarker(PointLatLng pos) : base(pos) { _blinkTimer new Timer { Interval 800 }; _blinkTimer.Tick (s, e) { _isOn !_isOn; gmapControl1?.Invalidate(); }; _blinkTimer.Start(); } public override void OnRender(Graphics g, Rectangle rect, GMapControl control) { if (!_isOn) return; // 闪烁时跳过绘制 // 绘制红色圆点比内置图标更轻量 var pos control.Projection.FromLatLngToPixel(Position, control.Zoom); var clientPos new Point( (int)(pos.X - control.ViewArea.Left), (int)(pos.Y - control.ViewArea.Top) ); g.FillEllipse(Brushes.Red, clientPos.X - 5, clientPos.Y - 5, 10, 10); g.DrawEllipse(Pens.White, clientPos.X - 5, clientPos.Y - 5, 10, 10); } }关键点gmapControl1.Invalidate()触发重绘但只重绘脏区域性能友好control.ViewArea必须从control参数获取不能用GMapControl静态实例多实例场景会错乱FillEllipse用Brushes.Red而非new SolidBrush(Color.Red)避免 GDI 资源泄漏5.3 瓦片加载拦截如何在不改GMapProvider的前提下注入认证头有些私有地图服务如企业内网 MapServer需要 Bearer Token。GMapProvider.GetTileUrl()只返回 URL无法加 Header。此时要 HackGMapCache.LoadTileAsync()// 在 GMap.NET.Core/GMapCache.cs 中找到 LoadTileAsync 方法 public async TaskGMapTile LoadTileAsync(string url, int zoom, int tileX, int tileY) { try { // 【Hook 点】在此处修改 HttpClient 行为 var client new HttpClient(); client.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, your-jwt-token-here); client.DefaultRequestHeaders.UserAgent.ParseAdd(GMAP.net/2.1); var response await client.GetAsync(url); response.EnsureSuccessStatusCode(); var data await response.Content.ReadAsByteArrayAsync(); return new GMapTile { Data data, Url url, Zoom zoom, X tileX, Y tileY }; } catch (Exception ex) { Debug.WriteLine($Tile load failed: {url} - {ex.Message}); return null; } }注意HttpClient应复用声明为 static readonly否则高并发下会耗尽 socket。生产环境建议用IHttpClientFactory但 GMAP.net 未集成 DI所以直接 new 一个并加using是最稳妥的。6. 二次开发实战为 GMAP.net 添加 GeoJSON 矢量图层支持含坐标系自动转换GeoJSON 是 GIS 数据交换的事实标准但 GMAP.net 原生不支持。很多开发者想直接加载.geojson文件渲染行政区划、道路网却卡在“坐标系不匹配”上——GeoJSON 用 WGS84经纬度而 GMAP.net 渲染用墨卡托像素。下面教你从零实现一个GMapGeoJsonOverlay支持自动投影转换、样式定制、点击事件。6.1 技术路线与依赖选择GeoJSON 解析用Newtonsoft.JsonGMAP.net 已引用解析FeatureCollection坐标转换复用GMap.NET.Core.Projection.MercatorProjection的FromLatLngToPixel()矢量绘制用Graphics.DrawLines()折线、Graphics.FillPolygon()面事件绑定在GMapControl.OnMouseClick中用Projection.FromPixelToLatLng()反算点击点经纬度再遍历所有Feature做点面判断不引入新 NuGet 包纯 C# 实现保证与现有项目零冲突。6.2 核心类GMapGeoJsonOverlay实现public class GMapGeoJsonOverlay : GMapOverlay { public ListFeature Features { get; } new(); public Color StrokeColor { get; set; } Color.Blue; public float StrokeWidth { get; set; } 2f; public Color FillColor { get; set; } Color.FromArgb(100, 0, 128, 255); // 半透明蓝 public ActionFeature OnFeatureClick { get; set; } public void LoadFromFile(string path) { var json File.ReadAllText(path); var geoJson JsonConvert.DeserializeObjectGeoJson(json); Features.Clear(); Features.AddRange(geoJson.Features); } public override void Render(Graphics g, Rectangle rect, GMapControl control) { if (!control.IsHandleCreated || Features.Count 0) return; var projection control.Projection; var zoom control.Zoom; var viewArea control.ViewArea; foreach (var feature in Features) { switch (feature.Geometry.Type) { case Point: RenderPoint(g, feature, projection, zoom, viewArea, control); break; case LineString: RenderLineString(g, feature, projection, zoom, viewArea, control); break; case Polygon: RenderPolygon(g, feature, projection, zoom, viewArea, control); break; } } } private void RenderPoint(Graphics g, Feature feature, IProjection projection, int zoom, GRect viewArea, GMapControl control) { var coords (JArray)feature.Geometry.Coordinates; var lat (double)coords[1], lng (double)coords[0]; var pixel projection.FromLatLngToPixel(new PointLatLng(lat, lng), zoom); var clientPos new Point( (int)(pixel.X - viewArea.Left), (int)(pixel.Y - viewArea.Top) ); g.FillEllipse(Brushes.Red, clientPos.X - 4, clientPos.Y - 4, 8, 8); } private void RenderLineString(Graphics g, Feature feature, IProjection projection, int zoom, GRect viewArea, GMapControl control) { var coords (JArray)feature.Geometry.Coordinates; var points new Point[coords.Count]; for (int i 0; i coords.Count; i) { var coord (JArray)coords[i]; var lng (double)coord[0], lat (double)coord[1]; var pixel projection.FromLatLngToPixel(new PointLatLng(lat, lng), zoom); points[i] new Point( (int)(pixel.X - viewArea.Left), (int)(pixel.Y - viewArea.Top) ); } using (var pen new Pen(StrokeColor, StrokeWidth)) g.DrawLines(pen, points); } private void RenderPolygon(Graphics g, Feature feature, IProjection projection, int zoom, GRect viewArea, GMapControl control) { var coords (JArray)((JArray)feature.Geometry.Coordinates)[0]; // 取外环 var points new Point[coords.Count]; for (int i 0; i coords.Count; i) { var coord (JArray)coords[i]; var lng (double)coord[0], lat (double)coord[1]; var pixel projection.FromLatLngToPixel(new PointLatLng(lat, lng), zoom); points[i] new Point( (int)(pixel.X - viewArea.Left), (int)(pixel.Y - viewArea.Top) ); } using (var brush new SolidBrush(FillColor)) g.FillPolygon(brush, points); using (var pen new Pen(StrokeColor, StrokeWidth)) g.DrawPolygon(pen, points); } }6.3 在 Demo 中集成与验证在MainForm.cs中添加private void LoadGeoJson_Click(object sender, EventArgs e) { var overlay new GMapGeoJsonOverlay(geojson-overlay); overlay.LoadFromFile(C:\data\provinces.geojson); // 下载一个中国省级 GeoJSON 测试 overlay.StrokeColor Color.Orange; overlay.FillColor Color.FromArgb(50, 255, 165, 0); // 半透明橙色 overlay.OnFeatureClick feature MessageBox.Show($Clicked: {feature.Properties[name]}); gmapControl1.Overlays.Add(overlay); gmapControl1.Zoom 4; gmapControl1.Position new PointLatLng(35.0, 105.0); // 中国中心 } // 拦截鼠标点击触发 GeoJSON 事件 private void gmapControl1_OnMouseClick(object sender, MouseEventArgs e) { // 反算点击点经纬度 var point gmapControl1.FromLocalToLatLng(e.X, e.Y); // 遍历所有 overlay 查找点击的 Feature简化版只查第一个 overlay foreach (GMapGeoJsonOverlay overlay in gmapControl1.Overlays) { foreach (var feature in overlay.Features) { if (IsPointInFeature(point, feature)) { overlay.OnFeatureClick?.Invoke(feature); return; } } } } private bool IsPointInFeature(PointLatLng point, Feature feature) { // 实现点面判断Winding Number 算法此处略可用 NetTopologySuite 库 return false; }6.4 避坑指南GeoJSON 坐标系陷阱与性能优化陷阱 1GeoJSON 坐标顺序RFC 7946 规定[longitude, latitude]但很多国产工具导出为[latitude, longitude]。加载后若地图“倒置”立刻检查Coordinates[0]是经度还是纬度。陷阱 2大数据量卡顿一个省级 GeoJSON 可能含 10 万顶点。RenderPolygon()中FillPolygon()会触发 GDI 复杂填充CPU本文还有配套的精品资源点击获取