第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 的事件处理与交互:

  1. 事件系统概述:V5 的统一指针事件模型
  2. Map.Info 事件:地图点击查询
  3. 指针事件处理:在 Widget 中处理各类指针事件
  4. 手势处理:平移、缩放、旋转手势
  5. 要素交互:选择和悬停效果
  6. 绘制工具:点、线、多边形绘制
  7. 测量工具:距离测量实现

在下一章中,我们将学习投影与坐标系统。

9.9 思考与练习

  1. 实现一个要素多选功能(按住 Ctrl 键多选)。
  2. 创建一个面积测量工具。
  3. 实现一个要素拖动编辑功能。
  4. 创建一个捕捉(Snap)功能,绘制时自动捕捉到现有要素。
  5. 实现一个撤销/重做功能用于绘制操作。
posted @ 2026-01-08 14:40  我才是银古  阅读(11)  评论(0)    收藏  举报