第09章 - 事件处理与交互
第09章:事件处理与交互
9.1 事件系统概述
9.1.1 Mapsui V5 事件模型
Mapsui V5 引入了统一的指针事件模型,在所有平台上提供一致的交互体验:
// 指针事件类型
public enum PointerEventType
{
PointerPressed, // 指针按下
PointerMoved, // 指针移动
PointerReleased, // 指针释放
Tapped // 点击(包括单击、双击、长按)
}
9.1.2 事件流程
用户输入 (触摸/鼠标)
│
▼
┌─────────────────┐
│ MapControl │ ◄── 平台特定的输入处理
└────────┬────────┘
│
▼
┌─────────────────┐
│ ManipulationTracker │ ◄── 处理平移、缩放、旋转
└────────┬────────┘
│
▼
┌─────────────────┐
│ TapTracker │ ◄── 检测点击、双击、长按
└────────┬────────┘
│
▼
┌─────────────────┐
│ Widgets │ ◄── 小部件事件处理
└────────┬────────┘
│
▼
┌─────────────────┐
│ Map.Info │ ◄── 地图信息事件
└─────────────────┘
9.2 Map.Info 事件
9.2.1 基本使用
// 订阅地图信息事件
map.Info += OnMapInfo;
private void OnMapInfo(object? sender, MapInfoEventArgs e)
{
var mapInfo = e.MapInfo;
if (mapInfo == null) return;
// 屏幕位置
var screenPosition = mapInfo.ScreenPosition;
// 世界坐标
var worldPosition = mapInfo.WorldPosition;
// 点击的要素
var feature = mapInfo.Feature;
// 要素所在的图层
var layer = mapInfo.Layer;
if (feature != null)
{
Console.WriteLine($"点击了要素: {feature}");
Console.WriteLine($"图层: {layer?.Name}");
}
else
{
Console.WriteLine($"点击位置: {worldPosition}");
}
}
9.2.2 MapInfo 详解
public class MapInfo
{
// 屏幕坐标
public MPoint ScreenPosition { get; }
// 世界坐标(地图坐标系)
public MPoint? WorldPosition { get; }
// 点击的要素(如果有)
public IFeature? Feature { get; }
// 要素所在的图层
public ILayer? Layer { get; }
// 分辨率
public double Resolution { get; }
}
9.2.3 主动获取地图信息
// 不通过事件,直接获取地图信息
public MapInfo? GetInfoAtPoint(MPoint screenPosition)
{
return map.GetMapInfo(screenPosition, mapControl.Viewport);
}
// 带容差的查询
public MapInfo? GetInfoAtPointWithTolerance(MPoint screenPosition, int tolerance = 10)
{
return map.GetMapInfo(screenPosition, mapControl.Viewport, tolerance);
}
9.3 指针事件处理
9.3.1 在 Widget 中处理指针事件
public class InteractiveWidget : BaseWidget
{
public override bool OnPointerPressed(Navigator navigator, WidgetEventArgs e)
{
// 指针按下时调用
Console.WriteLine($"Pointer pressed at: {e.ScreenPosition}");
return true; // 返回 true 表示事件已处理
}
public override bool OnPointerMoved(Navigator navigator, WidgetEventArgs e)
{
// 指针移动时调用(包括悬停)
return false; // 返回 false 允许事件继续传播
}
public override bool OnPointerReleased(Navigator navigator, WidgetEventArgs e)
{
// 指针释放时调用
return true;
}
public override bool OnTapped(Navigator navigator, WidgetEventArgs e)
{
// 点击时调用
Console.WriteLine($"Tapped! NumTaps: {e.NumTaps}");
if (e.NumTaps == 2)
{
// 双击处理
HandleDoubleTap(e);
}
return true;
}
private void HandleDoubleTap(WidgetEventArgs e)
{
// 双击处理逻辑
}
}
9.3.2 WidgetEventArgs
public class WidgetEventArgs : EventArgs
{
// 屏幕位置
public MPoint ScreenPosition { get; }
// 点击次数(用于区分单击、双击)
public int NumTaps { get; }
// 是否为长按
public bool IsLongPress { get; }
// 是否已处理
public bool Handled { get; set; }
// 获取地图信息
public MapInfo? GetMapInfo(Map map, Viewport viewport)
{
return map.GetMapInfo(ScreenPosition, viewport);
}
}
9.4 手势处理
9.4.1 ManipulationTracker
Mapsui 使用 ManipulationTracker 处理平移、缩放和旋转手势:
// ManipulationTracker 内部处理:
// - 单指拖动 → 平移
// - 双指缩放 → 缩放
// - 双指旋转 → 旋转
// - 惯性滑动 → FlingTracker
// 这些手势默认启用,无需额外配置
9.4.2 控制手势行为
// 通过 Navigator 控制手势行为
public class GestureController
{
private readonly Navigator _navigator;
public GestureController(Navigator navigator)
{
_navigator = navigator;
}
// 禁用/启用旋转
public void SetRotationEnabled(bool enabled)
{
_navigator.RotationLock = !enabled;
}
// 设置缩放限制
public void SetZoomLimits(double minResolution, double maxResolution)
{
_navigator.ZoomLimits = new MMinMax(minResolution, maxResolution);
}
// 设置平移限制
public void SetPanLimits(MRect? extent)
{
_navigator.PanLimits = extent;
}
}
9.5 要素交互
9.5.1 要素选择
public class FeatureSelector
{
private readonly Map _map;
private IFeature? _selectedFeature;
private readonly WritableLayer _highlightLayer;
public event EventHandler<IFeature?>? SelectionChanged;
public FeatureSelector(Map map)
{
_map = map;
// 创建高亮图层
_highlightLayer = new WritableLayer
{
Name = "Selection",
Style = CreateHighlightStyle()
};
_map.Layers.Add(_highlightLayer);
// 订阅地图点击事件
_map.Info += OnMapInfo;
}
private void OnMapInfo(object? sender, MapInfoEventArgs e)
{
var feature = e.MapInfo?.Feature;
if (feature != _selectedFeature)
{
_selectedFeature = feature;
UpdateHighlight();
SelectionChanged?.Invoke(this, feature);
}
}
private void UpdateHighlight()
{
_highlightLayer.Clear();
if (_selectedFeature != null)
{
// 复制选中要素到高亮图层
var highlightFeature = _selectedFeature.Copy();
highlightFeature.Styles = new[] { CreateHighlightStyle() };
_highlightLayer.Add(highlightFeature);
}
_highlightLayer.DataHasChanged();
}
private IStyle CreateHighlightStyle()
{
return new VectorStyle
{
Fill = new Brush(new Color(255, 255, 0, 100)),
Outline = new Pen(Color.Yellow, 3)
};
}
public void ClearSelection()
{
_selectedFeature = null;
UpdateHighlight();
SelectionChanged?.Invoke(this, null);
}
}
9.5.2 要素悬停效果
public class FeatureHoverHandler
{
private readonly Map _map;
private readonly MapControl _mapControl;
private IFeature? _hoveredFeature;
private readonly WritableLayer _hoverLayer;
public FeatureHoverHandler(Map map, MapControl mapControl)
{
_map = map;
_mapControl = mapControl;
_hoverLayer = new WritableLayer
{
Name = "Hover",
Style = CreateHoverStyle()
};
_map.Layers.Add(_hoverLayer);
// 需要在 MapControl 级别处理鼠标移动
// 具体实现取决于平台
}
public void OnPointerMoved(MPoint screenPosition)
{
var mapInfo = _map.GetMapInfo(screenPosition, _mapControl.Viewport);
var feature = mapInfo?.Feature;
if (feature != _hoveredFeature)
{
_hoveredFeature = feature;
UpdateHover();
}
}
private void UpdateHover()
{
_hoverLayer.Clear();
if (_hoveredFeature != null)
{
var hoverFeature = _hoveredFeature.Copy();
_hoverLayer.Add(hoverFeature);
}
_hoverLayer.DataHasChanged();
}
private IStyle CreateHoverStyle()
{
return new VectorStyle
{
Fill = new Brush(new Color(0, 120, 255, 80)),
Outline = new Pen(new Color(0, 120, 255), 2)
};
}
}
9.6 绘制与编辑
9.6.1 点绘制
public class PointDrawingTool
{
private readonly Map _map;
private readonly WritableLayer _drawLayer;
private bool _isActive;
public event EventHandler<PointFeature>? PointDrawn;
public PointDrawingTool(Map map)
{
_map = map;
_drawLayer = new WritableLayer
{
Name = "Drawing",
Style = CreateDrawStyle()
};
_map.Layers.Add(_drawLayer);
}
public void Activate()
{
_isActive = true;
_map.Info += OnMapInfo;
}
public void Deactivate()
{
_isActive = false;
_map.Info -= OnMapInfo;
}
private void OnMapInfo(object? sender, MapInfoEventArgs e)
{
if (!_isActive) return;
var worldPosition = e.MapInfo?.WorldPosition;
if (worldPosition == null) return;
var feature = new PointFeature(worldPosition);
feature["created"] = DateTime.Now;
_drawLayer.Add(feature);
_drawLayer.DataHasChanged();
PointDrawn?.Invoke(this, feature);
}
public IEnumerable<PointFeature> GetDrawnPoints()
{
return _drawLayer.GetFeatures(null, 0).OfType<PointFeature>();
}
public void Clear()
{
_drawLayer.Clear();
_drawLayer.DataHasChanged();
}
private IStyle CreateDrawStyle()
{
return new SymbolStyle
{
SymbolScale = 0.5,
Fill = new Brush(Color.Orange),
Outline = new Pen(Color.White, 2)
};
}
}
9.6.2 线绘制
public class LineDrawingTool
{
private readonly Map _map;
private readonly WritableLayer _drawLayer;
private readonly List<MPoint> _currentPoints = new();
private bool _isActive;
public event EventHandler<GeometryFeature>? LineDrawn;
public LineDrawingTool(Map map)
{
_map = map;
_drawLayer = new WritableLayer { Name = "LineDrawing" };
_map.Layers.Add(_drawLayer);
}
public void Activate()
{
_isActive = true;
_currentPoints.Clear();
_map.Info += OnMapInfo;
}
public void Deactivate()
{
_isActive = false;
_map.Info -= OnMapInfo;
}
private void OnMapInfo(object? sender, MapInfoEventArgs e)
{
if (!_isActive) return;
var worldPosition = e.MapInfo?.WorldPosition;
if (worldPosition == null) return;
_currentPoints.Add(worldPosition);
UpdatePreview();
}
private void UpdatePreview()
{
_drawLayer.Clear();
if (_currentPoints.Count >= 2)
{
var factory = new GeometryFactory();
var coordinates = _currentPoints
.Select(p => new Coordinate(p.X, p.Y))
.ToArray();
var line = factory.CreateLineString(coordinates);
var feature = new GeometryFeature(line);
feature.Styles = new[] { CreateLineStyle() };
_drawLayer.Add(feature);
}
// 添加顶点
foreach (var point in _currentPoints)
{
var vertexFeature = new PointFeature(point);
vertexFeature.Styles = new[] { CreateVertexStyle() };
_drawLayer.Add(vertexFeature);
}
_drawLayer.DataHasChanged();
}
public void FinishLine()
{
if (_currentPoints.Count >= 2)
{
var factory = new GeometryFactory();
var coordinates = _currentPoints
.Select(p => new Coordinate(p.X, p.Y))
.ToArray();
var line = factory.CreateLineString(coordinates);
var feature = new GeometryFeature(line);
LineDrawn?.Invoke(this, feature);
}
_currentPoints.Clear();
UpdatePreview();
}
public void Cancel()
{
_currentPoints.Clear();
UpdatePreview();
}
private IStyle CreateLineStyle()
{
return new VectorStyle
{
Line = new Pen(Color.Blue, 3)
};
}
private IStyle CreateVertexStyle()
{
return new SymbolStyle
{
SymbolScale = 0.3,
Fill = new Brush(Color.White),
Outline = new Pen(Color.Blue, 2)
};
}
}
9.6.3 多边形绘制
public class PolygonDrawingTool
{
private readonly Map _map;
private readonly WritableLayer _drawLayer;
private readonly List<MPoint> _currentPoints = new();
private bool _isActive;
public event EventHandler<GeometryFeature>? PolygonDrawn;
public PolygonDrawingTool(Map map)
{
_map = map;
_drawLayer = new WritableLayer { Name = "PolygonDrawing" };
_map.Layers.Add(_drawLayer);
}
public void Activate()
{
_isActive = true;
_currentPoints.Clear();
_map.Info += OnMapInfo;
}
public void Deactivate()
{
_isActive = false;
_map.Info -= OnMapInfo;
}
private void OnMapInfo(object? sender, MapInfoEventArgs e)
{
if (!_isActive) return;
var worldPosition = e.MapInfo?.WorldPosition;
if (worldPosition == null) return;
_currentPoints.Add(worldPosition);
UpdatePreview();
}
private void UpdatePreview()
{
_drawLayer.Clear();
if (_currentPoints.Count >= 3)
{
// 绘制多边形预览
var factory = new GeometryFactory();
var coordinates = _currentPoints
.Select(p => new Coordinate(p.X, p.Y))
.Concat(new[] { new Coordinate(_currentPoints[0].X, _currentPoints[0].Y) })
.ToArray();
var polygon = factory.CreatePolygon(coordinates);
var feature = new GeometryFeature(polygon);
feature.Styles = new[] { CreatePolygonStyle() };
_drawLayer.Add(feature);
}
else if (_currentPoints.Count == 2)
{
// 绘制线预览
var factory = new GeometryFactory();
var coordinates = _currentPoints
.Select(p => new Coordinate(p.X, p.Y))
.ToArray();
var line = factory.CreateLineString(coordinates);
var feature = new GeometryFeature(line);
feature.Styles = new[] { CreateLineStyle() };
_drawLayer.Add(feature);
}
// 添加顶点
foreach (var point in _currentPoints)
{
var vertexFeature = new PointFeature(point);
vertexFeature.Styles = new[] { CreateVertexStyle() };
_drawLayer.Add(vertexFeature);
}
_drawLayer.DataHasChanged();
}
public void FinishPolygon()
{
if (_currentPoints.Count >= 3)
{
var factory = new GeometryFactory();
var coordinates = _currentPoints
.Select(p => new Coordinate(p.X, p.Y))
.Concat(new[] { new Coordinate(_currentPoints[0].X, _currentPoints[0].Y) })
.ToArray();
var polygon = factory.CreatePolygon(coordinates);
var feature = new GeometryFeature(polygon);
PolygonDrawn?.Invoke(this, feature);
}
_currentPoints.Clear();
UpdatePreview();
}
private IStyle CreatePolygonStyle()
{
return new VectorStyle
{
Fill = new Brush(new Color(0, 100, 255, 80)),
Outline = new Pen(new Color(0, 100, 255), 2)
};
}
private IStyle CreateLineStyle()
{
return new VectorStyle
{
Line = new Pen(new Color(0, 100, 255), 2)
};
}
private IStyle CreateVertexStyle()
{
return new SymbolStyle
{
SymbolScale = 0.3,
Fill = new Brush(Color.White),
Outline = new Pen(new Color(0, 100, 255), 2)
};
}
}
9.7 测量工具
9.7.1 距离测量
public class DistanceMeasureTool
{
private readonly Map _map;
private readonly WritableLayer _measureLayer;
private MPoint? _startPoint;
private bool _isActive;
public event EventHandler<double>? DistanceMeasured;
public DistanceMeasureTool(Map map)
{
_map = map;
_measureLayer = new WritableLayer { Name = "Measure" };
_map.Layers.Add(_measureLayer);
}
public void Activate()
{
_isActive = true;
_startPoint = null;
_map.Info += OnMapInfo;
}
public void Deactivate()
{
_isActive = false;
_map.Info -= OnMapInfo;
_measureLayer.Clear();
_measureLayer.DataHasChanged();
}
private void OnMapInfo(object? sender, MapInfoEventArgs e)
{
if (!_isActive) return;
var worldPosition = e.MapInfo?.WorldPosition;
if (worldPosition == null) return;
if (_startPoint == null)
{
_startPoint = worldPosition;
DrawStartPoint();
}
else
{
var distance = CalculateDistance(_startPoint, worldPosition);
DrawMeasureLine(_startPoint, worldPosition, distance);
DistanceMeasured?.Invoke(this, distance);
_startPoint = null;
}
}
private double CalculateDistance(MPoint p1, MPoint p2)
{
// 转换为经纬度后使用 Haversine 公式计算
var lonLat1 = SphericalMercator.ToLonLat(p1.X, p1.Y);
var lonLat2 = SphericalMercator.ToLonLat(p2.X, p2.Y);
return HaversineDistance(lonLat1.Y, lonLat1.X, lonLat2.Y, lonLat2.X);
}
private double HaversineDistance(double lat1, double lon1, double lat2, double lon2)
{
const double R = 6371000; // 地球半径(米)
var dLat = ToRadians(lat2 - lat1);
var dLon = ToRadians(lon2 - lon1);
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(ToRadians(lat1)) * Math.Cos(ToRadians(lat2)) *
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
return R * c;
}
private double ToRadians(double degrees) => degrees * Math.PI / 180;
private void DrawStartPoint()
{
_measureLayer.Clear();
var feature = new PointFeature(_startPoint!);
feature.Styles = new[] { CreatePointStyle() };
_measureLayer.Add(feature);
_measureLayer.DataHasChanged();
}
private void DrawMeasureLine(MPoint start, MPoint end, double distance)
{
_measureLayer.Clear();
// 绘制线
var factory = new GeometryFactory();
var line = factory.CreateLineString(new[]
{
new Coordinate(start.X, start.Y),
new Coordinate(end.X, end.Y)
});
var lineFeature = new GeometryFeature(line);
lineFeature.Styles = new[] { CreateLineStyle() };
_measureLayer.Add(lineFeature);
// 绘制端点
foreach (var point in new[] { start, end })
{
var pointFeature = new PointFeature(point);
pointFeature.Styles = new[] { CreatePointStyle() };
_measureLayer.Add(pointFeature);
}
// 添加距离标签
var midPoint = new MPoint((start.X + end.X) / 2, (start.Y + end.Y) / 2);
var labelFeature = new PointFeature(midPoint);
labelFeature.Styles = new[] { CreateLabelStyle(FormatDistance(distance)) };
_measureLayer.Add(labelFeature);
_measureLayer.DataHasChanged();
}
private string FormatDistance(double meters)
{
if (meters >= 1000)
return $"{meters / 1000:F2} km";
return $"{meters:F0} m";
}
private IStyle CreatePointStyle() => new SymbolStyle
{
SymbolScale = 0.3,
Fill = new Brush(Color.Red),
Outline = new Pen(Color.White, 2)
};
private IStyle CreateLineStyle() => new VectorStyle
{
Line = new Pen(Color.Red, 2) { PenStyle = PenStyle.Dash }
};
private IStyle CreateLabelStyle(string text) => new LabelStyle
{
Text = text,
ForeColor = Color.Black,
BackColor = new Brush(Color.White),
Offset = new Offset(0, -15)
};
}
9.8 本章小结
本章详细介绍了 Mapsui 的事件处理与交互:
- 事件系统概述:V5 的统一指针事件模型
- Map.Info 事件:地图点击查询
- 指针事件处理:在 Widget 中处理各类指针事件
- 手势处理:平移、缩放、旋转手势
- 要素交互:选择和悬停效果
- 绘制工具:点、线、多边形绘制
- 测量工具:距离测量实现
在下一章中,我们将学习投影与坐标系统。
9.9 思考与练习
- 实现一个要素多选功能(按住 Ctrl 键多选)。
- 创建一个面积测量工具。
- 实现一个要素拖动编辑功能。
- 创建一个捕捉(Snap)功能,绘制时自动捕捉到现有要素。
- 实现一个撤销/重做功能用于绘制操作。

浙公网安备 33010602011771号