• WPF 制作雷达扫描图


     

    实现一个雷达扫描图。

    源代码在TK_King/雷达 (gitee.com),自行下载就好了

     

     

    制作思路

    1. 绘制圆形(或者称之轮)
    2. 绘制分割线
    3. 绘制扫描范围
    4. 添加扫描点

    具体实现

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

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

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

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

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

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

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

    所以,我们构造一个集合依赖属性。关于集合依赖属性我们可以参加MSDN集合类型依赖属性 - WPF .NET | Microsoft Docs

    复制代码
            /// <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的参数

    复制代码
     <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。

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

    复制代码
            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;
    复制代码

     

    依赖属性

    复制代码
         /// <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代码

    复制代码
                 <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>
    复制代码

     

     

     

     

     下一步就是扇形扫描了。

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

    复制代码
         /// <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的依赖属性来播放动画。

    复制代码
         /// <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代码( 部分)

    复制代码
     <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的方法,是为了将点如果在雷达外侧时,按照同角度缩放到最外层的方法。就是通过半径重新计算一边极坐标。

    复制代码
         /// <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

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

     

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

    xaml代码

    复制代码
        <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

    复制代码
      /// <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)));
        }
    复制代码

     

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

     

  • 相关阅读:
    Npm——yalc本地库调试工具
    二叉搜索树
    【PHP代码审计】反序列化漏洞实战
    【Linux学习】基础开发工具
    内在不足外需存疑,是谁给了波司登继续涨价的“勇气”?
    【python】使用pysam读取sam文件时的常用属性
    算法分析与设计CH16:贪心算法——活动选择问题、背包问题(分数背包/0-1背包)、哈夫曼编码
    spring boot学习第十三篇:使用spring security控制权限
    java-Spring-入门学习-第二天(单例模式和多例模式)
    AI创作系统ChatGPT商业运营系统源码+支持GPT4/支持ai绘画
  • 原文地址:https://www.cnblogs.com/T-ARF/p/16253121.html