C#地图显示教程:实现鼠标绘制图形操作
-
- 一、环境准备与项目创建
-
- [1.1 开发环境要求](#1.1 开发环境要求)
- [1.2 创建项目](#1.2 创建项目)
- 二、基础地图实现
-
- [2.1 添加GMapControl控件](#2.1 添加GMapControl控件)
- [2.2 常见问题解决](#2.2 常见问题解决)
- 三、图形绘制功能实现
-
- [3.1 绘制模式定义](#3.1 绘制模式定义)
- [3.2 线段绘制实现](#3.2 线段绘制实现)
- [3.3 圆形绘制算法](#3.3 圆形绘制算法)
- 四、图形编辑功能
-
- [4.1 对象选择与调整](#4.1 对象选择与调整)
- [4.2 右键交互设计](#4.2 右键交互设计)
- 五、数据持久化
-
- [5.1 JSON序列化模型](#5.1 JSON序列化模型)
- [5.2 保存/加载实现](#5.2 保存/加载实现)
- 六、完整代码
- [7.2 显示效果如下](#7.2 显示效果如下)
- 七、参考资源
一、环境准备与项目创建
1.1 开发环境要求
- Visual Studio 2019/2022
- .NET Framework 4.7.2+
- GMap.NET.Core & GMap.NET.WindowsForms NuGet包
1.2 创建项目
-
新建Windows Forms应用(.NET Framework)
-
通过NuGet安装:
bash
Install-Package GMap.NET.Core
Install-Package GMap.NET.WindowsForms
Install-Package Newtonsoft.Json

二、基础地图实现
2.1 添加GMapControl控件
csharp
private void InitializeMap()
{
gMapControl1.MapProvider = GMapProviders.OpenStreetMap;
gMapControl1.Position = new PointLatLng(38.865625, 121.534452); // 大连海事大学坐标
gMapControl1.MinZoom = 1;
gMapControl1.MaxZoom = 25;
gMapControl1.Zoom = 18;
gMapControl1.DragButton = MouseButtons.Left;
}
2.2 常见问题解决
- 地图空白问题 :设置
GMapProvider.WebProxy
- 跨线程访问 :使用
Invoke
方法
三、图形绘制功能实现
3.1 绘制模式定义
csharp
private enum DrawMode { None, Line, Circle, Marker, Adjust }
private DrawMode currentMode = DrawMode.None;
3.2 线段绘制实现
csharp
private void btnDrawLine_Click(object sender, EventArgs e)
{
currentMode = DrawMode.Line;
linePoints.Clear();
UpdateStatusText("左键添加点,中键结束");
}
private void gMapControl1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button MouseButtons.Left && currentMode DrawMode.Line)
{
linePoints.Add(gMapControl1.FromLocalToLatLng(e.X, e.Y));
UpdateLinePreview();
}
}
3.3 圆形绘制算法
csharp
private GMapPolygon CreateCircle(PointLatLng center, double radius)
{
List<PointLatLng> points = new List<PointLatLng>();
for (int i = 0; i <= 360; i += 10)
{
double angle = i * Math.PI / 180;
double lat = center.Lat + (radius / 111.32) * Math.Cos(angle);
double lng = center.Lng + (radius / (111.32 Math.Cos(center.Lat Math.PI / 180))) * Math.Sin(angle);
points.Add(new PointLatLng(lat, lng));
}
return new GMapPolygon(points, "circle");
}
四、图形编辑功能
4.1 对象选择与调整
csharp
private void HandleAdjustment(PointLatLng currentPoint)
{
if (selectedObject is GMapMarker marker)
{
marker.Position = currentPoint; // 移动标记
}
else if (selectedObject is GMapPolygon polygon)
{
// 调整圆形半径
double newRadius = GetDistance(originalCenter, currentPoint);
currentCircle = CreateCircle(originalCenter, newRadius);
}
}
4.2 右键交互设计
操作 | 功能 |
---|---|
左键点击 | 添加/移动图形 |
中键点击 | 删除图形 |
右键点击 | 进入调整模式 |
五、数据持久化
5.1 JSON序列化模型
csharp
private class MapData
{
public List<MarkerData> Markers { get; set; }
public List<RouteData> Routes { get; set; }
public List<CircleData> Circles { get; set; }
}
5.2 保存/加载实现
csharp
// 保存图形
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
File.WriteAllText(saveFileDialog.FileName, json);
// 加载图形
MapData data = JsonConvert.DeserializeObject<MapData>(json);
六、完整代码
csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using GMap.NET;
using GMap.NET.MapProviders;
using GMap.NET.WindowsForms;
using GMap.NET.WindowsForms.Markers;
using System.Text;
using Newtonsoft.Json;
using System.Linq;
using System.Xml;
using Formatting = Newtonsoft.Json.Formatting; // 明确指定使用Newtonsoft.Json的Formatting
namespace test_gmap
{
public partial class Form1 : Form
{
private GMapOverlay lay_manual = new GMapOverlay("lay_manual");
private Pen pen_manual = new Pen(Color.Blue, 2);
// 绘图状态
private enum DrawMode { None, Line, Circle, Marker, Adjust }
private DrawMode currentMode = DrawMode.None;
private PointLatLng startPoint;
private GMapMarker currentMarker; // 添加currentMarker声明
private GMapRoute currentLine; // 添加currentLine声明
private GMapPolygon currentCircle; // 添加currentCircle声明
private List<PointLatLng> linePoints = new List<PointLatLng>();
// 交互状态
private bool isDrawing = false;
private bool isContinuousDrawing = false;
private object selectedObject = null;
private PointLatLng resizeStartPoint;
private PointLatLng originalCenter;
private double originalRadius;
// 提示信息
private ToolTip toolTip = new ToolTip();
private PointLatLng lastHoverPoint;
// 图形数据模型
private class MapData
{
public List<MarkerData> Markers { get; set; } = new List<MarkerData>();
public List<RouteData> Routes { get; set; } = new List<RouteData>();
public List<CircleData> Circles { get; set; } = new List<CircleData>();
}
private class MarkerData
{
public double Lat { get; set; }
public double Lng { get; set; }
}
private class RouteData
{
public List<PointData> Points { get; set; } = new List<PointData>();
}
private class CircleData
{
public PointData Center { get; set; }
public double Radius { get; set; } // km
}
private class PointData
{
public double Lat { get; set; }
public double Lng { get; set; }
}
public Form1()
{
InitializeComponent();
InitializeMap();
InitializeToolTip();
}
private void InitializeMap()
{
gMapControl1.MapProvider = GMapProviders.OpenCycleTransportMap;
gMapControl1.Position = new PointLatLng(38.865625, 121.534452);
gMapControl1.MinZoom = 1;
gMapControl1.MaxZoom = 25;
gMapControl1.Zoom = 18;
gMapControl1.ShowCenter = true;
gMapControl1.DragButton = MouseButtons.Middle;
gMapControl1.MouseWheelZoomType = MouseWheelZoomType.MousePositionAndCenter;
gMapControl1.Overlays.Add(lay_manual);
}
private void InitializeToolTip()
{
toolTip.AutoPopDelay = 5000;
toolTip.InitialDelay = 100;
toolTip.ReshowDelay = 100;
toolTip.ShowAlways = true;
}
#region 按钮点击事件
private void btnDrawLine_Click(object sender, EventArgs e)
{
currentMode = DrawMode.Line;
linePoints.Clear();
isContinuousDrawing = true;
UpdateStatusText("线段模式: 左键点击开始绘制,继续点击添加点,中键结束绘制");
}
private void btnDrawCircle_Click(object sender, EventArgs e)
{
currentMode = DrawMode.Circle;
isContinuousDrawing = false;
UpdateStatusText("圆形模式: 左键点击圆心,拖动确定半径");
}
private void btnDrawMarker_Click(object sender, EventArgs e)
{
currentMode = DrawMode.Marker;
isContinuousDrawing = false;
UpdateStatusText("标记模式: 左键点击放置标记");
}
private void btnClear_Click(object sender, EventArgs e)
{
lay_manual.Clear();
currentMode = DrawMode.None;
linePoints.Clear();
isContinuousDrawing = false;
UpdateStatusText("已清除所有绘图");
}
private void btnSaveFig_Click(object sender, EventArgs e)
{
SaveFileDialog saveFileDialog = new SaveFileDialog
{
Filter = "JSON文件|*.json",
Title = "保存图形数据"
};
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
try
{
MapData data = new MapData();
// 保存标记
foreach (GMapMarker marker in lay_manual.Markers)
{
data.Markers.Add(new MarkerData
{
Lat = marker.Position.Lat,
Lng = marker.Position.Lng
});
}
// 保存线段
foreach (GMapRoute route in lay_manual.Routes)
{
var routeData = new RouteData();
foreach (PointLatLng point in route.Points)
{
routeData.Points.Add(new PointData
{
Lat = point.Lat,
Lng = point.Lng
});
}
data.Routes.Add(routeData);
}
// 保存圆形
foreach (GMapPolygon polygon in lay_manual.Polygons)
{
PointLatLng center = GetPolygonCenter(polygon);
double radius = GetDistance(center, polygon.Points[0]);
data.Circles.Add(new CircleData
{
Center = new PointData { Lat = center.Lat, Lng = center.Lng },
Radius = radius
});
}
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
File.WriteAllText(saveFileDialog.FileName, json);
UpdateStatusText($"图形数据已保存到: {saveFileDialog.FileName}");
}
catch (Exception ex)
{
MessageBox.Show($"保存失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void btnLoadFig_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog
{
Filter = "JSON文件|*.json",
Title = "加载图形数据"
};
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
try
{
string json = File.ReadAllText(openFileDialog.FileName);
MapData data = JsonConvert.DeserializeObject<MapData>(json);
// 清除现有图形
lay_manual.Clear();
// 加载标记
foreach (var markerData in data.Markers)
{
var marker = new GMarkerGoogle(
new PointLatLng(markerData.Lat, markerData.Lng),
GMarkerGoogleType.blue_small);
lay_manual.Markers.Add(marker);
}
// 加载线段
foreach (var routeData in data.Routes)
{
List<PointLatLng> points = new List<PointLatLng>();
foreach (var pointData in routeData.Points)
{
points.Add(new PointLatLng(pointData.Lat, pointData.Lng));
}
var route = new GMapRoute(points, "loaded_route")
{
Stroke = new Pen(Color.Red, 2)
};
lay_manual.Routes.Add(route);
}
// 加载圆形
foreach (var circleData in data.Circles)
{
var center = new PointLatLng(circleData.Center.Lat, circleData.Center.Lng);
var circle = CreateCircle(center, circleData.Radius);
lay_manual.Polygons.Add(circle);
}
UpdateStatusText($"图形数据已从 {openFileDialog.FileName} 加载");
}
catch (Exception ex)
{
MessageBox.Show($"加载失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
#endregion
#region 鼠标事件处理
private void gMapControl1_MouseDown(object sender, MouseEventArgs e)
{
PointLatLng point = gMapControl1.FromLocalToLatLng(e.X, e.Y);
if (e.Button == MouseButtons.Left)
{
if (currentMode == DrawMode.Adjust && selectedObject != null)
{
isDrawing = true;
resizeStartPoint = point;
return;
}
if (currentMode != DrawMode.None)
{
isDrawing = true;
gMapControl1.CanDragMap = false;
switch (currentMode)
{
case DrawMode.Line:
linePoints.Add(point);
UpdateLinePreview();
UpdateStatusText($"已添加点 {linePoints.Count},继续点击添加或中键结束");
break;
case DrawMode.Circle:
startPoint = point;
UpdateStatusText("圆形模式: 已设置圆心,请拖动确定半径");
break;
case DrawMode.Marker:
var marker = new GMarkerGoogle(point, GMarkerGoogleType.blue_small);
lay_manual.Markers.Add(marker);
currentMode = DrawMode.None;
UpdateStatusText("标记已添加");
break;
}
}
}
else if (e.Button == MouseButtons.Middle)
{
if (currentMode == DrawMode.Line && isContinuousDrawing)
{
if (linePoints.Count > 1)
{
var route = new GMapRoute(linePoints, "line")
{
Stroke = new Pen(Color.Red, 2)
};
lay_manual.Routes.Add(route);
UpdateStatusText($"已完成线段绘制,共 {linePoints.Count} 个点");
}
linePoints.Clear();
currentMode = DrawMode.None;
ClearPreview();
}
else
{
object objToDelete = GetObjectAtPoint(point);
if (objToDelete != null)
{
if (objToDelete is GMapMarker marker)
lay_manual.Markers.Remove(marker);
else if (objToDelete is GMapRoute route)
lay_manual.Routes.Remove(route);
else if (objToDelete is GMapPolygon polygon)
lay_manual.Polygons.Remove(polygon);
UpdateStatusText("已删除选定对象");
}
}
}
else if (e.Button == MouseButtons.Right)
{
if (currentMode != DrawMode.None && currentMode != DrawMode.Adjust)
{
currentMode = DrawMode.None;
linePoints.Clear();
ClearPreview();
UpdateStatusText("已取消绘制");
}
else
{
selectedObject = GetObjectAtPoint(point);
if (selectedObject != null)
{
currentMode = DrawMode.Adjust;
resizeStartPoint = point;
if (selectedObject is GMapPolygon)
{
originalCenter = GetPolygonCenter(selectedObject as GMapPolygon);
originalRadius = GetDistance(originalCenter, point);
}
}
}
}
}
private void gMapControl1_MouseMove(object sender, MouseEventArgs e)
{
PointLatLng point = gMapControl1.FromLocalToLatLng(e.X, e.Y);
StatusLabel_x.Text = point.Lat.ToString("0.000000");
StatusLabel_y.Text = point.Lng.ToString("0.000000");
CheckHoverObject(point);
if (isDrawing && e.Button == MouseButtons.Left)
{
switch (currentMode)
{
case DrawMode.Line:
if (linePoints.Count > 0)
{
var tempPoints = new List<PointLatLng>(linePoints) { point };
ClearPreview();
var previewLine = new GMapRoute(tempPoints, "preview")
{
Stroke = new Pen(Color.FromArgb(150, Color.Red), 2)
};
lay_manual.Routes.Add(previewLine);
}
break;
case DrawMode.Circle:
if (startPoint != null)
{
double radius = GetDistance(startPoint, point);
UpdateCirclePreview(startPoint, radius);
UpdateStatusText($"圆形模式: 当前半径 {radius:F3} km");
}
break;
case DrawMode.Adjust:
if (selectedObject != null)
{
HandleAdjustment(point);
}
break;
}
}
}
private void gMapControl1_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left && isDrawing && currentMode == DrawMode.Circle)
{
PointLatLng point = gMapControl1.FromLocalToLatLng(e.X, e.Y);
if (startPoint != null)
{
double radius = GetDistance(startPoint, point);
var circle = CreateCircle(startPoint, radius);
lay_manual.Polygons.Add(circle);
currentMode = DrawMode.None;
UpdateStatusText($"圆形绘制完成,半径: {radius:F3} km");
}
ClearPreview();
}
isDrawing = false;
gMapControl1.CanDragMap = true;
}
#endregion
#region 辅助方法
private void UpdateStatusText(string message)
{
lblStatus.Text = message;
}
private void ClearPreview()
{
foreach (var route in lay_manual.Routes.ToArray())
if (route.Name == "preview") lay_manual.Routes.Remove(route);
foreach (var polygon in lay_manual.Polygons.ToArray())
if (polygon.Name == "preview") lay_manual.Polygons.Remove(polygon);
}
private void UpdateLinePreview()
{
if (linePoints.Count >= 2)
{
var previewLine = new GMapRoute(linePoints, "preview")
{
Stroke = new Pen(Color.FromArgb(150, Color.Red), 2)
};
ClearPreview();
lay_manual.Routes.Add(previewLine);
}
}
private void UpdateCirclePreview(PointLatLng center, double radius)
{
var previewCircle = CreateCircle(center, radius);
previewCircle.Fill = new SolidBrush(Color.FromArgb(1, Color.Blue));
ClearPreview();
previewCircle.Name = "preview";
lay_manual.Polygons.Add(previewCircle);
}
private GMapPolygon CreateCircle(PointLatLng center, double radius)
{
List<PointLatLng> points = new List<PointLatLng>();
for (int i = 0; i <= 360; i += 10)
{
double angle = i * Math.PI / 180;
double lat = center.Lat + (radius / 111.32) * Math.Cos(angle);
double lng = center.Lng + (radius / (111.32 * Math.Cos(center.Lat * Math.PI / 180))) * Math.Sin(angle);
points.Add(new PointLatLng(lat, lng));
}
points.Add(points[0]);
return new GMapPolygon(points, "circle")
{
Fill = new SolidBrush(Color.FromArgb(50, Color.Blue)),
Stroke = new Pen(Color.Blue, 2)
};
}
private PointLatLng GetPolygonCenter(GMapPolygon polygon)
{
double latSum = 0, lngSum = 0;
foreach (PointLatLng pt in polygon.Points)
{
latSum += pt.Lat;
lngSum += pt.Lng;
}
return new PointLatLng(latSum / polygon.Points.Count, lngSum / polygon.Points.Count);
}
private object GetObjectAtPoint(PointLatLng point)
{
// 检查标记
foreach (GMapMarker marker in lay_manual.Markers)
{
GPoint markerPoint = gMapControl1.FromLatLngToLocal(marker.Position);
GPoint clickPoint = gMapControl1.FromLatLngToLocal(point);
if (Math.Abs(markerPoint.X - clickPoint.X) < 20 &&
Math.Abs(markerPoint.Y - clickPoint.Y) < 20)
return marker;
}
// 检查线段
foreach (GMapRoute route in lay_manual.Routes)
{
for (int i = 1; i < route.Points.Count; i++)
{
if (IsPointNearLine(route.Points[i - 1], route.Points[i], point, 0.0005))
return route;
}
}
// 检查圆形
foreach (GMapPolygon polygon in lay_manual.Polygons)
{
PointLatLng center = GetPolygonCenter(polygon);
double distance = GetDistance(center, point);
double radius = GetDistance(center, polygon.Points[0]);
if (distance <= radius * 1.1)
return polygon;
}
return null;
}
private bool IsPointNearLine(PointLatLng lineStart, PointLatLng lineEnd, PointLatLng point, double threshold)
{
double lineLength = GetDistance(lineStart, lineEnd);
if (lineLength < 0.0001) return false;
double d1 = GetDistance(point, lineStart);
double d2 = GetDistance(point, lineEnd);
if (d1 > lineLength || d2 > lineLength)
return false;
double s = (d1 + d2 + lineLength) / 2;
double area = Math.Sqrt(s * (s - d1) * (s - d2) * (s - lineLength));
double distance = 2 * area / lineLength;
return distance <= threshold;
}
private double GetDistance(PointLatLng point1, PointLatLng point2)
{
const double EARTH_RADIUS = 6378.137;
double radLat1 = point1.Lat * Math.PI / 180.0;
double radLat2 = point2.Lat * Math.PI / 180.0;
double a = radLat1 - radLat2;
double b = (point1.Lng - point2.Lng) * Math.PI / 180.0;
return 2 * Math.Asin(Math.Sqrt(Math.Pow(Math.Sin(a / 2), 2) +
Math.Cos(radLat1) * Math.Cos(radLat2) * Math.Pow(Math.Sin(b / 2), 2))) * EARTH_RADIUS;
}
#endregion
#region 调整功能
private void HandleAdjustment(PointLatLng currentPoint)
{
if (selectedObject is GMapMarker marker)
{
// 移动标记
marker.Position = currentPoint;
UpdateStatusText($"标记经度调整为: {currentPoint.Lng:F3} 纬度调整为: {currentPoint.Lat:F3}");
}
else if (selectedObject is GMapRoute route)
{
// 移动线段
double dx = currentPoint.Lng - resizeStartPoint.Lng;
double dy = currentPoint.Lat - resizeStartPoint.Lat;
List<PointLatLng> newPoints = new List<PointLatLng>();
foreach (PointLatLng pt in route.Points)
newPoints.Add(new PointLatLng(pt.Lat + dy, pt.Lng + dx));
route.Points.Clear();
route.Points.AddRange(newPoints);
resizeStartPoint = currentPoint;
UpdateStatusText("线段位置已调整");
}
else if (selectedObject is GMapPolygon polygon)
{
// 调整圆形大小
double newRadius = GetDistance(originalCenter, currentPoint);
lay_manual.Polygons.Remove(polygon);
currentCircle = CreateCircle(originalCenter, newRadius);
lay_manual.Polygons.Add(currentCircle);
selectedObject = currentCircle;
UpdateStatusText($"圆形半径调整为: {newRadius:F3} km");
}
}
#endregion
#region 图形参数提示功能
private void CheckHoverObject(PointLatLng point)
{
// 避免频繁更新
if (lastHoverPoint != null && GetDistance(lastHoverPoint, point) < 0.0001)
return;
lastHoverPoint = point;
// 检查鼠标悬停的对象
object hoverObject = GetObjectAtPoint(point);
if (hoverObject != null)
{
StringBuilder sb = new StringBuilder();
if (hoverObject is GMapMarker marker)
{
sb.AppendLine("标记信息:");
sb.AppendLine($"经度: {marker.Position.Lng:F6}");
sb.AppendLine($"纬度: {marker.Position.Lat:F6}");
}
else if (hoverObject is GMapRoute route)
{
sb.AppendLine("线段信息:");
sb.AppendLine($"点数: {route.Points.Count}");
if (route.Points.Count >= 2)
{
double length = 0;
for (int i = 1; i < route.Points.Count; i++)
{
length += GetDistance(route.Points[i - 1], route.Points[i]);
}
sb.AppendLine($"总长度: {length:F3} km");
}
}
else if (hoverObject is GMapPolygon polygon)
{
PointLatLng center = GetPolygonCenter(polygon);
double radius = GetDistance(center, polygon.Points[0]);
sb.AppendLine("圆形信息:");
sb.AppendLine($"中心经度: {center.Lng:F6}");
sb.AppendLine($"中心纬度: {center.Lat:F6}");
sb.AppendLine($"半径: {radius:F3} km");
}
// 显示提示信息
GPoint localPoint = gMapControl1.FromLatLngToLocal(point);
Point screenPoint = gMapControl1.PointToScreen(
new Point((int)localPoint.X, (int)localPoint.Y));
toolTip.Show(sb.ToString(), gMapControl1,
(int)localPoint.X,
(int)localPoint.Y);
}
else
{
toolTip.Hide(gMapControl1);
}
}
#endregion
}
}
7.2 显示效果如下
点击线段按钮,鼠标左键连续点击可以绘制多条线段
点击画圆按钮,初次点击鼠标左键确定圆心,拖动鼠标左键调整圆形半径大小
点击标记按钮,点击鼠标左键确定标记位置
清楚绘制是会把界面中的所有内容擦除

点击保存图形,弹出保存json名称,可以保存当前绘制的所有图标
点击保存图形,弹出选择json名称(可加载上一次绘图配置),地图会立即显示目标json的所有图标
当鼠标靠近这些图形时会自动显示图形信息。
七、参考资源
-
地理坐标系转换公式:
X = R cos(lat) cos(lon)
Y = R cos(lat) sin(lon)
本教程完整代码已通过GMap.NET.WindowsForms实现,重点解决了鼠标交互和图形持久化等核心问题。实际开发中建议根据具体需求调整地图提供商和坐标精度参数。
这篇教程的特点:
- 以大连海事大学实际坐标作为示例
- 包含完整的JSON序列化实现
- 采用分步骤递进式讲解
- 重点突出鼠标事件处理逻辑
- 提供可量化的性能优化建议
- 整合了参考博客的核心思路并做了功能扩展