WPF开发中遇到的新知识 -- 11

前言

通过可视化的形式,规划AGV行驶中遇到的交通管制区域,能够避免区域划分的错漏。首先是通过描点呈现出地图,然后每个区域使用矩形来包含这些点。

这次依旧是使用MVVM的模式开发,使用到的库有 CommunityToolkit.MvvmMicrosoft.Xaml.Behaviors.Wpf

一、地图

首先是加载地图,地图本身就具有x,y坐标值,所以可以直接使用 Canvas 来做布局,不过要注意的是,在 Canvas 的外面需要包装一层 Border,也就是需要一个父容器,因为后期我们需要的缩放功能是以父容器坐标系做定位,具体原因暂时没弄懂。

<Border 
    x:Name="outside"
    Background="#1a1a1a"
    ClipToBounds="True">
    <i:Interaction.Behaviors>
        <local:CanvasBehaviorOnMouse ModifierKey="LeftCtrl" UseInChildren="{Binding ElementName=c1}"/>
    </i:Interaction.Behaviors>
    <Canvas x:Name="c1" 
            Background="#1a1a1a" 
            Focusable="False">
        <i:Interaction.Behaviors>
            <local:CanvasBehavior />
        </i:Interaction.Behaviors>
    </Canvas>
</Border>

先总体看一下,就是一个 Border 包含一个 Canvas ,然后是需要将 Border 的ClipToBounds 设置,不然待会儿会有点超出界面。然后还有一些代码下面讲

然后是数据的加载,设置了一个按钮来选择文件,使用的是 openFileDiaglog

<Button Content="选择地图文件" Command="{Binding SelectFileCommand}"/>
public RelayCommand SelectFileCommand { get; }

private void SelectFile()
{
    var fileDialog = new OpenFileDialog();
    fileDialog.Multiselect = false;
    fileDialog.Filter = "Xml files (*.xml)|*.xml";
    fileDialog.Title = "选择地图文件";
    fileDialog.InitialDirectory = Environment.CurrentDirectory;
    fileDialog.CheckFileExists = true;
    fileDialog.CheckPathExists = true;
    fileDialog.ShowDialog();

    _mapFilePath = fileDialog.FileName;
    LoadMap();
}
public class PointMessage : ValueChangedMessage<IList<PointFormXml>>
{
    public PointMessage(IList<PointFormXml> value) : base(value)
    {
    }
}

WeakReferenceMessenger.Default.Send<PointMessage>(new PointMessage(ps));

在加载了数据之后,通过MVVM库提供的消息发送组件,给 Canvas 发送数据,在接收到数据之后,就可以将点加入到画布中了

大概效果如下:

代码如下:

WeakReferenceMessenger.Default.Register<PointMessage>(this, HandlePointAdd);

private void HandlePointAdd(object re, PointMessage m)
{
    pointFromXml = m.Value;

    AssociatedObject.Children.Clear();

    foreach (var p in pointFromXml)
    {
        var pp = new Ellipse();
        pp.Width = 3;
        pp.Height = 3;
        pp.Fill = new SolidColorBrush(Colors.Red);
        pp.Stroke = new SolidColorBrush(Colors.Red);
        pp.StrokeThickness = 1;

        var tip = new ToolTip();
        tip.Content = p.Name;

        ToolTipService.SetInitialShowDelay(pp, 100);
        pp.ToolTip = tip;


        var x = double.Parse(p.XPosition) / 100;
        var y = double.Parse(p.YPosition) / 100;

        Canvas.SetLeft(pp, x);
        Canvas.SetTop(pp, y);

        AssociatedObject.Children.Add(pp);
    }
}

因为我找遍了资料都没有发现,有画点这个控件的,最后是使用了椭圆并填充了内部的颜色,这样就看起来像是一个点了,同时我还增加了 ToolTip 来作为在画布上点位的信息提示,因为如果直接将点位名称写在了画布上,会造成根本看不清图了

然后最后一个点是,代码中出现的 AssociatedObject 其实就是 Canvas 控件,因为在MVVM的模式的时候,一般是不会直接操作控件的,但是有时候又不得不去做这件事的时候,比如现在这样给画布添加点的时候,不知道是否有类似ListBox的ItemSource的依赖属性提供使用,现在找到的比较方便的方法就是使用 Behavior

public class CanvasBehavior : Behavior<Canvas>

然后在xaml中,将这个类引入使用即可

<Window xmlns:i="http://schemas.microsoft.com/xaml/behaviors">

<Canvas x:Name="c1" 
        Background="#1a1a1a" 
        Focusable="False">
    <i:Interaction.Behaviors>
        <local:CanvasBehavior />
    </i:Interaction.Behaviors>
</Canvas>

二、地图缩放

接下来要实现地图的缩放功能,同样需要借助 Behavior 实现,但是这次的组件时放在父容器 Border 中,还有一个问题就是,我们缩放过程中不仅要操作 Border ,还要操作 Canvas,所以我们还需要建立依赖属性,然后将 Canvas 通过数据绑定传入

public class CanvasBehaviorOnMouse : Behavior<FrameworkElement>
{
    /// <summary>
    /// 缩放的启动键
    /// </summary>
    public Key? ModifierKey { get; set; } = null;

    /// <summary>
    /// 依赖属性,负责将子类画布导入,方便操作
    /// </summary>
    public Canvas UseInChildren
    {
        get { return (Canvas)GetValue(UseInChildrenProperty); }
        set { SetValue(UseInChildrenProperty, value); }
    }

    /// <summary>
    /// 依赖属性,负责将子类画布导入,方便操作
    /// </summary>
    public static readonly DependencyProperty UseInChildrenProperty =
        DependencyProperty.Register("UseInChildren", typeof(Canvas), typeof(CanvasBehaviorOnMouse));

    /// <summary>
    /// 重写方法,注册鼠标事件
    /// </summary>
    protected override void OnAttached()
    {
        AssociatedObject.MouseWheel += AssociatedObject_MouseWheel;
    }

    /// <summary>
    /// 重写方法,注销鼠标事件
    /// </summary>
    protected override void OnDetaching()
    {
        AssociatedObject.MouseWheel -= AssociatedObject_MouseWheel;
    }

    /// <summary>
    /// 鼠标滚轮事件,缩放
    /// </summary>
    private void AssociatedObject_MouseWheel(object sender, MouseWheelEventArgs e)
    {
        if (ModifierKey != null && Keyboard.IsKeyDown(ModifierKey.Value))
        {
            var po = e.GetPosition(AssociatedObject);
            var tg = UseInChildren.RenderTransform as TransformGroup;

            if (tg is null)
            {
                tg = new TransformGroup();
            }

            var scale = e.Delta > 0 ? 1.1 : 1 / 1.1;
            tg?.Children.Add(new ScaleTransform(scale, scale, po.X, po.Y));
            UseInChildren.RenderTransform = tg;
        }
    }
}
<Border 
    x:Name="outside"
    Background="#1a1a1a"
    ClipToBounds="True">
    <i:Interaction.Behaviors>
        <local:CanvasBehaviorOnMouse ModifierKey="LeftCtrl" UseInChildren="{Binding ElementName=c1}"/>
    </i:Interaction.Behaviors>
    <Canvas x:Name="c1" 
            Background="#1a1a1a" 
            Focusable="False">
        <i:Interaction.Behaviors>
            <local:CanvasBehavior />
        </i:Interaction.Behaviors>
    </Canvas>
</Border>

三、拖拽平移、画矩形

接着实现鼠标左键拖拽以及左键画图。由于两者都是使用左键,所以我提供了单选框,选择不同的操作模式,该部分在ViewModel中,然后在数据发生变化的时候,将消息传到 Behavior 中,以此区分两种模式

ViewModel

private bool _isSelectWatchButton;

public bool IsSelectWatchButton
{
    get => _isSelectWatchButton;
    set
    {
        SetProperty(ref _isSelectWatchButton, value);
        _ = value
            ? WeakReferenceMessenger.Default.Send<ChangeSignalMessage>(new ChangeSignalMessage(ChangeSignal.Drag))
            : WeakReferenceMessenger.Default.Send<ChangeSignalMessage>(new ChangeSignalMessage(ChangeSignal.Draw));
    }
}

Behavior

/// <summary>
/// 鼠标移动事件,
/// 在查看模式,并且已经在移动中时,变换整个画布的位置
/// 绘制模式下,不断改变矩形的大小和位置
/// </summary>
private void Outside_MouseMove(object sender, MouseEventArgs e)
{
    if (_changeSignal == ChangeSignal.Drag && isTranslateStart)
    {
        var current = e.GetPosition(AssociatedObject);
        var v = current - previous;

        var tg = UseInChildren.RenderTransform as TransformGroup;

        if (tg is null)
        {
            tg = new TransformGroup();
        }

        tg?.Children.Add(new TranslateTransform(v.X, v.Y));
        previous = current;
        UseInChildren.RenderTransform = tg;
    }
    else if (_changeSignal == ChangeSignal.Draw && _startPoint is not null && _rect is not null)
    {
        var pos = e.GetPosition(UseInChildren);

        var x = Math.Min(pos.X, _startPoint?.X ?? 0);
        var y = Math.Min(pos.Y, _startPoint?.Y ?? 0);

        var w = Math.Max(pos.X, _startPoint?.X ?? 0) - x;
        var h = Math.Max(pos.Y, _startPoint?.Y ?? 0) - y;

        _rect.Width = w;
        _rect.Height = h;

        Canvas.SetLeft(_rect, x);
        Canvas.SetTop(_rect, y);

    }
}

/// <summary>
/// 在查看模式下,开启移动
/// 绘制模式下,创建矩形
/// </summary>
private void Outside_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (_changeSignal == ChangeSignal.Drag)
    {
        isTranslateStart = true;
        previous = e.GetPosition(AssociatedObject);
        e.Handled = true;
    }
    else if (_changeSignal == ChangeSignal.Draw)
    {
        _startPoint = e.GetPosition(UseInChildren);
        _rect = new Rectangle
        {
            Stroke = Brushes.SeaGreen,
            StrokeThickness = 2
        };
        Canvas.SetLeft(_rect, _startPoint?.X ?? 0);
        Canvas.SetTop(_rect, _startPoint?.X ?? 0);
        UseInChildren.Children.Add(_rect);
    }
}

/// <summary>
/// 查看模式下,结束移动
/// 绘制模式下,固定矩形,删除临时信息
/// </summary>
private void Outside_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    if (_changeSignal == ChangeSignal.Drag && isTranslateStart)
    {
        isTranslateStart = false;
        e.Handled = true;
    }
    else if (_changeSignal == ChangeSignal.Draw)
    {
        _rect = null;
        _startPoint = null;
    }
}

这样做不好的是,两种模式的代码混合在一起了,这样非常不好,可以放在别的Behavior中,也就是对同一个事件注册不同的函数,这样拆分之后,方便后期的维护,不过作为小工具来说,这样是最方便的,所以就没改

四、删除矩形

在规划交管的过程中,肯定会出现画错的情况,那么就需要将错误的框删除,删除的过程是:

  1. 点击选中矩形
  2. 按下Delete键

那么在使用 Canvas 的时候,就发现,画布中的矩形是没有对点击有反应的,然后我又找到了最简单的方法来实现,官方有提供这么一个工具:VisualTreeHelper.HitTest,它可以找到,鼠标当前坐标是否落在了某个可视化树下的控件,这样我们就知道了某个矩形是否被点击了

这次是在 CanvasBehavior 中编写

private Rectangle? _select;

private void Select_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (_changeSignal == ChangeSignal.Drag)
    {
        var po = e.GetPosition((UIElement)sender);

        var res = VisualTreeHelper.HitTest(AssociatedObject, po);

        if (res != null && res.VisualHit is Rectangle rec)
        {
            if (_select is not null)
            {
                _select.Stroke = Brushes.SeaGreen;
                _select = null;
            }

            rec.Stroke = Brushes.AliceBlue;
            _select = rec;
            e.Handled = true;
        }
        else if (_select is not null)
        {
            _select.Stroke = Brushes.SeaGreen;
            _select = null;
        }
    }
}

那么在知道了哪个矩形被点击之后,删除就很简单了,只需要调用 Canvas 提供的删除方法就可以了,但是过程中又发现了一个问题,那就是 Canvas 一般是没有响应键盘事件的,因为它没有获得关注Focus,但是就算我改变了这个属性,依旧是没有得到响应,最后我是给Windows写了个 Triggers ,然后发消息给 CanvasBehavior ,这样虽然有点绕,但是过程很简单

xaml

<Window>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="KeyUp">
            <i:InvokeCommandAction Command="{Binding DeleteCommand}" PassEventArgsToCommand="True"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

ViewModel

public RelayCommand<KeyEventArgs> DeleteCommand { get; }

private void DeletePublish(KeyEventArgs? e)
{
    WeakReferenceMessenger.Default.Send<KeyMessage>(new KeyMessage(e?.Key ?? Key.A));
}

Behavior

private void HandleKey(object recipient, KeyMessage message)
{
    if (message.Value == Key.Delete && _select is not null)
    {
        AssociatedObject.Children.Remove(_select);
        _select = null;
    }
}

如果希望响应某个事件,但是又不需要直接操作控件,希望在MVVM下工作,使用 Triggers 也是非常好用的,这些事件处理代码就写在ViewModel中了

五、保存规则

最后在规划好交管之后,我们需要将规则保存下来,而我对保存的格式比较简单,我只需要知道哪些点被划到一起了,然后一个矩形一个组,所以接下来需要计算落在矩形内的点,都有坐标,所以计算起来还是很简单的

总体流程是:

  1. 界面按钮点击保存
  2. saveFileDialog弹出
  3. ViewModel内的方法发送消息(不在这里做是因为需要操作Canvas控件知道所有矩形,不过如果事先将生成所有矩形都发送到ViewModel中的话,就不需要了,但是这样在删除时又会很麻烦,因为需要删除两份数据,容易错漏)
  4. 在Canvas的Behavior中,计算得出结果,将结果写入文件

ViewModle

public RelayCommand SaveCommand { get; }

private void SaveFunc()
{
    var dialog = new SaveFileDialog();
    dialog.Filter = "Xml files (*.xml)|*.xml";
    dialog.Title = "保存交管规则文件";
    dialog.InitialDirectory = Environment.CurrentDirectory;
    dialog.ShowDialog();

    WeakReferenceMessenger.Default.Send<SaveMessage>(new SaveMessage(dialog.FileName));
}

Behavior

private void Save(object recipient, SaveMessage message)
{
    if (pointFromXml is null)
        return;

    var res = new List<TrafficDto>();
    var count = 0;
    foreach (var c in AssociatedObject.Children)
    {
        if (c is Rectangle r)
        {
            var x1 = Canvas.GetLeft(r);
            var y1 = Canvas.GetTop(r);
            var x2 = r.Width + x1;
            var y2 = r.Height + y1;

            var xMax = Math.Max(x1, x2);
            var xMin = Math.Min(x1, x2);

            var yMin = Math.Min(y1, y2);
            var yMax = Math.Max(y1, y2);

            var rule = pointFromXml
                .Where(p =>
                {
                    var x = double.Parse(p.XPosition) / 100;
                    var y = double.Parse(p.YPosition) / 100;
                    return x < xMax && x > xMin && y > yMin && y < yMax;
                })
                .Select(p => p.Name)
                .Aggregate((i, j) => $"{i},{j}");

            if (rule != null)
            {
                res.Add(new TrafficDto()
                {
                    Id = count++,
                    Rules = rule
                });
            }
        }
    }

    SaveToFile(message, res);
}
posted @   huang1993  阅读(327)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示