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

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

相关推荐
zopple6 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001118 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
唐青枫8 小时前
C#.NET ReaderWriterLockSlim 深入解析:读写锁原理、升级锁与使用边界
c#·.net
小江的记录本8 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji34169 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan9 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer10 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor35610 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor35610 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer11 小时前
Spring BOOT 启动参数
java·spring boot·后端