WPF 制作雷达扫描图

前言

实现一个雷达扫描图。

源代码在TK_King/雷达 (gitee.com/TK_King/rad...%25EF%25BC%258C%25E8%2587%25AA%25E8%25A1%258C%25E4%25B8%258B%25E8%25BD%25BD%25E5%25B0%25B1%25E5%25A5%25BD%25E4%25BA%2586 "https://gitee.com/TK_King/radar)%EF%BC%8C%E8%87%AA%E8%A1%8C%E4%B8%8B%E8%BD%BD%E5%B0%B1%E5%A5%BD%E4%BA%86")

制作思路

1、绘制圆形(或者称之轮)

2、绘制分割线

3、绘制扫描范围

4、添加扫描点

具体实现

首先我们使用自定义的控件。你可以使用vs自动添加,也可以手动创建类。注意手动创建时要创建Themes/Generic.xaml的文件路径哦。

控件继承自itemscontrol,取名叫做Radar。

我们第一步思考如何实现圆形或者轮,特别是等距的轮。

我们可以使用简单的itemscontrol 的WPF控件,通过自定义ItemTemplate就可以简单的创建了。

因为要显示圆,所以使用Ellipse是最简单的事情。

又因为要在同一个区域内,显示同心圆,我们将面板改为Grid,利用叠加的特性去构造同心圆。

既然我们用了itemscontrol 来承载圈轮,直接让这个圈可自定义呢?

所以,我们构造一个集合依赖属性。

关于集合依赖属性我们可以参加MSDN集合类型依赖属性 - WPF .NET | Microsoft Docs

c# 复制代码
/// <summary>
/// 每圈的大小
/// </summary>
public FreezableCollection<RadarSize> RadarCircle
{
    get { return (FreezableCollection<RadarSize>)GetValue(RadarCircleProperty); }
    set { SetValue(RadarCircleProperty, value); }
}

/// <summary>
/// 每圈的大小
/// </summary>
public static readonly DependencyProperty RadarCircleProperty =
            DependencyProperty.Register("RadarCircle", typeof(FreezableCollection<RadarSize>), typeof(Radar), new PropertyMetadata(new PropertyChangedCallback(OnRadarCircelValueChanged)));

对应泛型类可以参考源代码,基本元素就是绑定ellipse的参数

xml 复制代码
 <ItemsControl Grid.ColumnSpan="2" Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="ic"  ItemsSource="{TemplateBinding RadarCircle }">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <Grid IsItemsHost="True"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Ellipse Width="{Binding  Width}" Height="{Binding Height}"  Stroke="{Binding Color}"/>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
</ItemsControl>

哇啦,图像就出来了。

同理,我们创建分割线也是同样的过程。

对于分割线的切割算法,我们使用圆上点的坐标可以通过( rcos,rsin)=》(x,y) ,也就是极坐标。

关于此部分代码是放在布局块内ArrangeOverride,也可以放置在OnReader。

下面是局部代码,完整可以参考源代码

c# 复制代码
var angle = 180.0 / 6;
circlesize = size.Height > size.Width ? size.Width : size.Height;
RadarFillWidth = circlesize;
var midx = circlesize / 2.0;
var midy = circlesize / 2.0;
circlesize = circlesize / 2;
RadarRadius = circlesize;
//默认为6个
for (int i = 0; i < 6; i++)
{
    var baseangel = angle * i;
    var l1 = new Point(midx + circlesize * Math.Cos(Rad(baseangel)), midy - circlesize * Math.Sin(Rad(baseangel)));
    var half = baseangel + 180;
    var l2 = new Point(midx + circlesize * Math.Cos(Rad(half)), midy - circlesize * Math.Sin(Rad(half)));
    RadarLineSize radarLine = new RadarLineSize();
    radarLine.Start = l1;
    radarLine.End = l2;
    radarLine.Color = RadarLineColor;
    RadarLine.Add(radarLine);
}
return size;

依赖属性

cs 复制代码
/// <summary>
/// 雷达图的分割线,目前固定为6,可以自行修改
/// </summary>
public FreezableCollection<RadarLineSize> RadarLine
{
    get { return (FreezableCollection<RadarLineSize>)GetValue(RadarLineProperty); }
    set { SetValue(RadarLineProperty, value); }
}

/// <summary>
/// 雷达图的分割线,目前固定为6,可以自行修改
/// </summary>
public static readonly DependencyProperty RadarLineProperty =
            DependencyProperty.Register("RadarLine", typeof(FreezableCollection<RadarLineSize>), typeof(Radar));

xaml代码

xml 复制代码
<ItemsControl Grid.ColumnSpan="2" Grid.RowSpan="2"  VerticalAlignment="Center" HorizontalAlignment="Center"  x:Name="ic2"   ItemsSource="{TemplateBinding RadarLine }">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Line X1="{Binding Start.X}" Y1="{Binding Start.Y}" X2="{Binding End.X}" Y2="{Binding End.Y}"  Stroke="{Binding Color}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

下一步就是扇形扫描了。

我们使用一个完整的圆,将其内部颜色填充为线性刷就可以得到一个效果不错的扫描了。

c# 复制代码
/// <summary>
/// 雷达扫描的颜色
/// </summary>
public Brush RadarColor
{
    get { return (Brush)GetValue(RadarColorProperty); }
    set { SetValue(RadarColorProperty, value); }
}

/// <summary>
/// 雷达扫描的颜色
/// </summary>
public static readonly DependencyProperty RadarColorProperty =
            DependencyProperty.Register("RadarColor", typeof(Brush), typeof(Radar));

为了更好的定义这个圆,我们将radar的template使用grid面板等距分成四个区域(其实没啥用,主要是为了扇形扫描时做圆心选择的line,也可以不分成四个)。

在考虑动画,只需要做圆形360的选择就可以了。为了更好应用,我们创一个paly的依赖属性来播放动画。

c# 复制代码
/// <summary>
/// 是否播放动画
/// </summary>
public bool Play
{
     get { return (bool)GetValue(PlayProperty); }
     set { SetValue(PlayProperty, value); }
}

/// <summary>
/// 是否播放动画
/// </summary>
public static readonly DependencyProperty PlayProperty =
     DependencyProperty.Register("Play", typeof(bool), typeof(Radar), new PropertyMetadata(false));

xaml代码( 部分)

xml 复制代码
 <Style.Resources>
           <LinearGradientBrush x:Key="radarcolor" StartPoint="0,0" EndPoint="0,1">
               <GradientStop Offset="0" Color="Lime" />
               <GradientStop Offset="0.5" Color="Transparent" />
           </LinearGradientBrush>
       </Style.Resources>
 <Setter Property="Template">
<Setter.Value>
      <ControlTemplate TargetType="{x:Type local:Radar}">
        <Grid x:Name="grid"   >
            <Grid.RowDefinitions>
                <RowDefinition Height="2*"/>
                <RowDefinition Height="2*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>
            <ItemsControl Grid.ColumnSpan="2" Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="ic"  ItemsSource="{TemplateBinding RadarCircle }">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Grid IsItemsHost="True"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Ellipse Width="{Binding  Width}" Height="{Binding Height}"  Stroke="{Binding Color}"/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
            <ItemsControl Grid.ColumnSpan="2" Grid.RowSpan="2"  VerticalAlignment="Center" HorizontalAlignment="Center"  x:Name="ic2"   ItemsSource="{TemplateBinding RadarLine }">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Grid IsItemsHost="True"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Line X1="{Binding Start.X}" Y1="{Binding Start.Y}" X2="{Binding End.X}" Y2="{Binding End.Y}"  Stroke="{Binding Color}"/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
            <Ellipse Fill="{TemplateBinding RadarColor}"   Grid.ColumnSpan="2" Grid.RowSpan="2"  x:Name="ep" RenderTransformOrigin="0.5,0.5" Width="{TemplateBinding RadarFillWidth}" Height="{TemplateBinding RadarFillWidth}">
                <Ellipse.RenderTransform>
                    <RotateTransform x:Name="rtf" />
                </Ellipse.RenderTransform>
            </Ellipse>
        </Grid>
        <ControlTemplate.Triggers>
            <Trigger Property="Play" Value="True">
                <Trigger.EnterActions>
                    <BeginStoryboard  x:Name="bs" >
                        <Storyboard >
                            <DoubleAnimation Storyboard.TargetName="rtf" Storyboard.TargetProperty="Angle"   From="0" To="360" Duration="0:0:2" RepeatBehavior="Forever"/>
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.EnterActions>
            </Trigger>
            <Trigger Property="Play" Value="False">
                <Trigger.EnterActions>
                    <RemoveStoryboard BeginStoryboardName="bs"/>
                </Trigger.EnterActions>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</Setter.Value>

效果

那么剩下就是扫描点的操作。

因为我们的控件是继承ItemsControl ,我们到现在还没有利用ItemsSource这个属性。

所以我们要制作一个子控件来呈现扫描点。

由于子控件较为简单,只不过是一个圆而已。我们就让子控件继承Control就好了。

一切从简,我们不弄布局这一套了,直接在父控件中使用Canvas面板,子控件增加属性Left,Top这两个依赖属性。

重点说一下,子控件中存在一个linscar的方法,是为了将点如果在雷达外侧时,按照同角度缩放到最外层的方法。就是通过半径重新计算一边极坐标。

c# 复制代码
/// <summary>
/// 线性缩放
/// </summary>
/// <param name="size">半径</param>
internal void LineScar(double size)
{
    var midpoint = new Vector(size, size);
    var vp = new Vector(Left, Top);
    var sub = vp - midpoint;
    var angle = Vector.AngleBetween(sub, new Vector(size, 1));
    angle = angle > 0 ? angle : angle + 360;
    //距离大于半径,根据半径重新绘制
    if (sub.Length >= size)
    {
        Top = size - size * Math.Sin(Rad(angle)) - Width / 2;
        Left = size + size * Math.Cos(Rad(angle)) - Width / 2;
    }
}

那么在父项中如何摆放呢?

我们刚才说父项使用canvas绘图,所以我们在radar中修改itempanel的面板属性,下面代码存在于父项xaml

xml 复制代码
<Setter Property="ItemsPanel">
    <Setter.Value>
        <ItemsPanelTemplate>
            <Canvas IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </Setter.Value>
</Setter>

子项代码如下,比较少就贴了

xaml代码

xml 复制代码
<Style TargetType="local:RadarItem">
    <Setter Property="VerticalAlignment" Value="Top" />
    <Setter Property="HorizontalAlignment" Value="Left" />
    <Setter Property="Padding" Value="0" />
    <Setter Property="Margin" Value="0" />
    <Setter Property="Canvas.Top" Value="{Binding RelativeSource={RelativeSource Mode=Self},Path=Top}" />
    <Setter Property="Canvas.Left" Value="{Binding RelativeSource={RelativeSource Mode=Self},Path=Left}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:RadarItem">
                <Border  >
                    <Ellipse Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Fill="{TemplateBinding Color}" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

radarItem

c# 复制代码
/// <summary>
/// 雷达子项 
/// </summary>
public class RadarItem : Control
{

    static RadarItem()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(RadarItem), new FrameworkPropertyMetadata(typeof(RadarItem)));
    }
    public RadarItem()
    {

    }

    /// <summary>
    /// 转弧度
    /// </summary>
    /// <param name="val">角度</param>
    /// <returns>弧度制</returns>
    double Rad(double val)
    {
        return val * Math.PI / 180;
    }
    /// <summary>
    /// 线性缩放
    /// </summary>
    /// <param name="size">半径</param>
    internal void LineScar(double size)
    {
        var midpoint = new Vector(size, size);
        var vp = new Vector(Left, Top);
        var sub = vp - midpoint;
        var angle = Vector.AngleBetween(sub, new Vector(size, 1));
        angle = angle > 0 ? angle : angle + 360;
        //距离大于半径,根据半径重新绘制
        if (sub.Length >= size)
        {
            Top = size - size * Math.Sin(Rad(angle)) - Width / 2;
            Left = size + size * Math.Cos(Rad(angle)) - Width / 2;
        }
    }

    /// <summary>
    /// 顶部距离,用canvas.top绘制
    /// </summary>
    public double Top
    {
        get { return (double)GetValue(TopProperty); }
        set { SetValue(TopProperty, value); }
    }

    /// <summary>
    /// 顶部距离,用canvas.top绘制
    /// </summary>
    public static readonly DependencyProperty TopProperty =
        DependencyProperty.Register("Top", typeof(double), typeof(RadarItem), new PropertyMetadata(0.0));


    /// <summary>
    /// 左侧距离,用于canvas.left绘制
    /// </summary>
    public double Left
    {
        get { return (double)GetValue(LeftProperty); }
        set { SetValue(LeftProperty, value); }
    }

    /// <summary>
    /// 左侧距离,用于canvas.left绘制
    /// </summary>
    public static readonly DependencyProperty LeftProperty =
        DependencyProperty.Register("Left", typeof(double), typeof(RadarItem), new PropertyMetadata(0.0));


    /// <summary>
    /// 填充颜色
    /// </summary>
    public Brush Color
    {
        get { return (Brush)GetValue(ColorProperty); }
        set { SetValue(ColorProperty, value); }
    }

    /// <summary>
    /// 填充颜色
    /// </summary>
    public static readonly DependencyProperty ColorProperty =
        DependencyProperty.Register("Color", typeof(Brush), typeof(RadarItem), new PropertyMetadata(new SolidColorBrush(Colors.Red)));
}

于是乎我们就得到了一个雷达扫描图

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:ARM830

出处:cnblogs.com/T-ARF/p/16253121.html

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!

相关推荐
CV大法好1 小时前
刘铁猛p3 C# 控制台程序引用System.Windows.Forms报错,无法引用程序集 解决方法
开发语言·c#
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS加油站管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·maven
VinciYan2 小时前
C#中通过ObjectPool重用对象提高程序性能
c#·asp.net·.net·.netcore
陌上笙清净2 小时前
flask内存马的真谛!!!
后端·python·网络安全·flask
m0_748256563 小时前
Rust环境安装配置
开发语言·后端·rust
梅洪3 小时前
ASP.NET Core API 前后端分离跨域
后端·bootstrap·asp.net
IT界的奇葩3 小时前
基于springboot使用Caffeine
java·spring boot·后端·caffeine
rookiesx3 小时前
springboot jenkins job error console log
spring boot·后端·jenkins
凡人的AI工具箱3 小时前
40分钟学 Go 语言高并发教程目录
开发语言·后端·微服务·性能优化·golang