通常WPF中通过继承UserControl的来快速创建自定义控件,最近项目上需要设计一个卫星星图显示控件,最终效果如下图所示。完成过程中遇到了自定义集合依赖属性无法触发更新通知的问题,在此记录一下,方便有相同问题的朋友们可以快速解决,也希望有人能发现更好的解决办法。
为了完成目的,我写了下面一个SateChart自定义控件类,
XAML代码如下:
<UserControl x:Class="WpfApplication1.SateChart" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" > <DockPanel Margin="5" LastChildFill="True"> <StackPanel Margin="1" DockPanel.Dock="Top" Orientation="Horizontal" HorizontalAlignment="Center"> <Rectangle Fill="Orange" Width="10" Height="10"/> <CheckBox Name="bdView" Content="BD-2" Margin="5,0,5,0" IsChecked="True" Click="bdView_Click"/> <Rectangle Fill="Red" Width="10" Height="10"/> <CheckBox Name="gpsView" Content="GPS" Margin="5,0,5,0" IsChecked="True" Click="gpsView_Click"/> <Rectangle Fill="Blue" Width="10" Height="10"/> <CheckBox Name="glnssView" Content="GLNS" Margin="5,0,5,0" IsChecked="True" Click="glnssView_Click"/> </StackPanel> <Viewbox DockPanel.Dock="Bottom" MaxHeight="300" MaxWidth="300"> <Canvas Name="myCanvas" Width="90" Height="90" > <Ellipse Name="e1" Width="90" Height="90" Fill="Black" Stroke="Black" HorizontalAlignment="Center"/> <Ellipse Name="e2" Width="90" Height="90" Fill="Black" Stroke="White" HorizontalAlignment="Center"> <Ellipse.RenderTransform> <ScaleTransform ScaleX="0.8333" ScaleY="0.8333" CenterX="45" CenterY="45"/> </Ellipse.RenderTransform> </Ellipse> <Ellipse Name="e3" Width="90" Height="90" Fill="Black" Stroke="White" HorizontalAlignment="Center"> <Ellipse.RenderTransform> <ScaleTransform ScaleX="0.6666" ScaleY="0.6666" CenterX="45" CenterY="45"/> </Ellipse.RenderTransform> </Ellipse> <Ellipse Name="e4" Width="90" Height="90" Fill="Black" Stroke="White" HorizontalAlignment="Center"> <Ellipse.RenderTransform> <ScaleTransform ScaleX="0.5" ScaleY="0.5" CenterX="45" CenterY="45"/> </Ellipse.RenderTransform> </Ellipse> <Ellipse Name="e5" Width="90" Height="90" Fill="Black" Stroke="White" HorizontalAlignment="Center"> <Ellipse.RenderTransform> <ScaleTransform ScaleX="0.333" ScaleY="0.333" CenterX="45" CenterY="45"/> </Ellipse.RenderTransform> </Ellipse> <Ellipse Name="e6" Width="90" Height="90" Fill="Black" Stroke="White" HorizontalAlignment="Center"> <Ellipse.RenderTransform> <ScaleTransform ScaleX="0.1666" ScaleY="0.1666" CenterX="45" CenterY="45"/> </Ellipse.RenderTransform> </Ellipse> <Line Name="line0" Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" /> <Line Name="line1" Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" > <Line.RenderTransform> <RotateTransform Angle="30" CenterX="45" CenterY="45"/> </Line.RenderTransform> </Line> <Line Name="line2" Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" > <Line.RenderTransform> <RotateTransform Angle="60" CenterX="45" CenterY="45"/> </Line.RenderTransform> </Line> <Line Name="line3" Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" > <Line.RenderTransform> <RotateTransform Angle="90" CenterX="45" CenterY="45"/> </Line.RenderTransform> </Line> <Line Name="line4" Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" > <Line.RenderTransform> <RotateTransform Angle="120" CenterX="45" CenterY="45"/> </Line.RenderTransform> </Line> <Line Name="line5" Stroke="White" StrokeDashArray="1 2" StrokeThickness="0.3" X1="45" Y1="90" X2="45" Y2="0" > <Line.RenderTransform> <RotateTransform Angle="150" CenterX="45" CenterY="45"/> </Line.RenderTransform> </Line> <TextBlock Name="tb1" Text="360°" Canvas.Left="43" Canvas.Top="0" Foreground="White" FontSize="2"/> <TextBlock Name="tb2" Text="30°" Canvas.Left="66" Canvas.Top="7" Foreground="White" FontSize="2" /> <TextBlock Name="tb3" Text="60°" Canvas.Left="80" Canvas.Top="22" Foreground="White" FontSize="2" /> <TextBlock Name="tb4" Text="90°" Canvas.Left="86" Canvas.Top="45" Foreground="White" FontSize="2" /> <TextBlock Name="tb5" Text="120°" Canvas.Left="80" Canvas.Top="64" Foreground="White" FontSize="2" /> <TextBlock Name="tb6" Text="150°" Canvas.Left="65" Canvas.Top="80" Foreground="White" FontSize="2" /> <TextBlock Name="tb7" Text="180°" Canvas.Left="43" Canvas.Top="86" Foreground="White" FontSize="2" /> <TextBlock Name="tb8" Text="210°" Canvas.Left="25" Canvas.Top="82" Foreground="White" FontSize="2" /> <TextBlock Name="tb9" Text="240°" Canvas.Left="8" Canvas.Top="67" Foreground="White" FontSize="2" /> <TextBlock Name="tb10" Text="270°" Canvas.Left="1" Canvas.Top="45" Foreground="White" FontSize="2" /> <TextBlock Name="tb11" Text="300°" Canvas.Left="8" Canvas.Top="21" Foreground="White" FontSize="2" /> <TextBlock Name="tb12" Text="330°" Canvas.Left="25" Canvas.Top="6" Foreground="White" FontSize="2" /> <TextBlock Name="tb13" Text="15°" Canvas.Left="52" Canvas.Top="6" Foreground="White" FontSize="2" /> <TextBlock Name="tb14" Text="30°" Canvas.Left="50" Canvas.Top="14" Foreground="White" FontSize="2" /> <TextBlock Name="tb15" Text="45°" Canvas.Left="48" Canvas.Top="21" Foreground="White" FontSize="2" /> <TextBlock Name="tb16" Text="60°" Canvas.Left="47" Canvas.Top="29" Foreground="White" FontSize="2" /> <TextBlock Name="tb17" Text="75°" Canvas.Left="46" Canvas.Top="35" Foreground="White" FontSize="2" /> </Canvas> </Viewbox> </DockPanel> </UserControl>
后台代码如下:
1 public partial class SateChart : UserControl 2 { 3 static bool isShowBD = true; 4 static bool isShowGPS = true; 5 static bool isShowGLONASS = true; 6 //卫星图标半径 7 static int r = 45; 8 public static DependencyProperty SateSourceProperty; 9 10 public IEnumerable<Sate> SateSource 11 { 12 get { return (IEnumerable<Sate>)GetValue(SateSourceProperty); } 13 set { SetValue(SateSourceProperty, value); } 14 } 15 static SateChart() 16 { 17 SateSourceProperty = DependencyProperty.Register( 18 "SateSource", typeof(IEnumerable<Sate>), typeof(SateChart), new FrameworkPropertyMetadata( 19 null, new PropertyChangedCallback(OnSateSourceChanged))); 20 } 21 public SateChart() 22 { 23 InitializeComponent(); 24 } 25 private static void OnSateSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) 26 { 27 if (e.NewValue != null) 28 { 29 SateChart sateChart = (SateChart)sender; 30 clearSates(sateChart); 31 drawSates(sateChart, (IEnumerable<Sate>)e.NewValue); 32 } 33 } 34 35 private void bdView_Click(object sender, RoutedEventArgs e) 36 { 37 isShowBD = this.bdView.IsChecked == true ? true : false; 38 } 39 40 private void gpsView_Click(object sender, RoutedEventArgs e) 41 { 42 isShowGPS = this.gpsView.IsChecked == true ? true : false; 43 } 44 45 private void glnssView_Click(object sender, RoutedEventArgs e) 46 { 47 isShowGLONASS = this.glnssView.IsChecked == true ? true : false; 48 } 49 //画卫星图 50 private static void drawSates(SateChart sateChart, IEnumerable<Sate> staes) 51 { 52 // clearSates(); 53 54 foreach (Sate item in staes) 55 { 56 switch (item.SateType) 57 { 58 case SateTypes.BD: if (isShowBD) addSate(sateChart, item); break; 59 case SateTypes.GPS: if (isShowGPS) addSate(sateChart, item); break; 60 case SateTypes.GLONASS: if (isShowGLONASS) addSate(sateChart, item); break; 61 default: break; 62 } 63 } 64 } 65 //在卫星图上添加卫星 66 private static void addSate(SateChart sateChart, Sate sate) 67 { 68 double azimuth = double.Parse(sate.Azimuth); 69 double elevation = double.Parse(sate.Elevation); 70 double cosLen = getCosLen(r, elevation); 71 //卫星图片显示 72 //Image image = new Image(); 73 //image.Source = new BitmapImage(new Uri("images/satellite2.png", UriKind.Relative)); 74 //image.Width = imageWidth; 75 //image.Height = imageHeight; 76 //Canvas.SetTop(image, getY(cosLen, azimuth) - imageHeight/2); 77 //Canvas.SetLeft(image, getX(cosLen, azimuth) - imageWidth/2); 78 //myCanvas.Children.Add(image); 79 Ellipse el = new Ellipse(); 80 el.Width = 3; 81 el.Height = 3; 82 Canvas.SetTop(el, getY(cosLen, azimuth) - 1.5); 83 Canvas.SetLeft(el, getX(cosLen, azimuth) - 1.5); 84 SolidColorBrush sb; 85 switch (sate.SateType) 86 { 87 case SateTypes.BD: sb = new SolidColorBrush(Colors.Orange); break; 88 case SateTypes.GPS: sb = new SolidColorBrush(Colors.Red); break; 89 case SateTypes.GLONASS: sb = new SolidColorBrush(Colors.Blue); break; 90 default: sb = new SolidColorBrush(Colors.Orange); break; 91 } 92 el.Stroke = sb; 93 el.Fill = sb; 94 95 TextBlock tb = new TextBlock(); 96 tb.Text = sate.PRN; 97 tb.Foreground = new SolidColorBrush(Colors.White); 98 tb.FontSize = 2; 99 Canvas.SetTop(tb, getY(cosLen, azimuth) + 1); 100 Canvas.SetLeft(tb, getX(cosLen, azimuth) + 1); 101 102 sateChart.myCanvas.Children.Add(el); 103 sateChart.myCanvas.Children.Add(tb); 104 } 105 //清空图上的卫星 106 private static void clearSates(SateChart sateChart) 107 { 108 int count = sateChart.myCanvas.Children.Count; 109 string[] ellipseNames=new string[]{"e1","e2","e3","e4","e5","e6"}; 110 string[] tbNames = new string[] {"tb1","tb2","tb3","tb4","tb5","tb6","tb7","tb8","tb9","tb10","tb11" 111 ,"tb12","tb13","tb14","tb15","tb16","tb17"}; 112 for (int i = count - 1; i >= 0; i--) 113 { 114 Ellipse ep = sateChart.myCanvas.Children[i] as Ellipse; 115 if (ep != null) 116 { 117 if (!ellipseNames.Contains(ep.Name)) 118 sateChart.myCanvas.Children.RemoveAt(i); 119 }else 120 { 121 TextBlock tb = sateChart.myCanvas.Children[i] as TextBlock; 122 if (tb != null) 123 { 124 if(!tbNames.Contains(tb.Name)) 125 sateChart.myCanvas.Children.RemoveAt(i); 126 } 127 } 128 129 } 130 } 131 //求此点到圆心的距离 132 private static double getCosLen(double r, double elevation) 133 { 134 double x = (1 - elevation / 90) * r; 135 return x; 136 } 137 //求X坐标 138 private static double getX(double cosLen, double azimuth) 139 { 140 double x = Math.Sin(azimuth * Math.PI / 180) * cosLen; 141 return x + r; 142 } 143 //求Y坐标 144 private static double getY(double cosLen, double azimuth) 145 { 146 double y = Math.Cos(azimuth * Math.PI / 180) * cosLen; 147 return r - y; 148 } 149 }
然后就在XAML中绑定数据源,后台写好模拟数据源,想着一切ok了
<local:SateChart x:Name="sateChart" SateSource="{Binding Path=Sates,Mode=OneWay}" />
List<Sate> _sates = new List<Sate>(); public List<Sate> Sates { get { if (_sates == null) _sates = new List<Sate>(); return _sates; } set { _sates = value; } } public MainWindow() { InitializeComponent(); this.DataContext = this; } private void Window_Loaded(object sender, RoutedEventArgs e) { _sates.Clear(); // _sates = new List<Sate>(); Sate sate1 = new Sate() {SateType=SateTypes.BD,Azimuth="50",CNO=21,Elevation="60",PRN="166" }; Sate sate2 = new Sate() { SateType = SateTypes.BD, Azimuth = "60", CNO = 21, Elevation = "60", PRN = "169" }; Sate sate3 = new Sate() { SateType = SateTypes.BD, Azimuth = "70", CNO = 21, Elevation = "70", PRN = "180" }; Sate sate4 = new Sate() { SateType = SateTypes.BD, Azimuth = "80", CNO = 21, Elevation = "80", PRN = "111" }; Sate sate5 = new Sate() { SateType = SateTypes.BD, Azimuth = "90", CNO = 21, Elevation = "90", PRN = "123" }; _sates.Add(sate1); _sates.Add(sate2); _sates.Add(sate3); _sates.Add(sate4); _sates.Add(sate5); OnPropertyChanged("Sates"); } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this,new PropertyChangedEventArgs(propertyName)); }
经过调试发现执行OnPropertyChanged("Sates")的时候,却没有触发自定义控件中的自定义依赖属性的OnSateSourceChanged(..)方法,这是为什么呢?自己一番百度没有解决,后来还是把问题发布到最近新加的一个Blend设计群里面,热心的群友果然有经验很多,有人提示说要使用ObservaleCollection,可是我早已试过此方法,并不能解决问题,因为绑定的是我们自定义依赖集合属性,不是ItemsControl的ItemsSource属性。还有人直接道出了有效解决办法,要触发自定义依赖集合属性的更改通知需要先new一下集合,然后再OnPropertyChanged方法,但是为什么会出现这样的问题至今他也没有找到相关资料,只是知道这样能够成功解决问题。
我本来一开始是考虑继承ItemsControl的,可是没有找到相关的资料,网上大多基本都是继承UserControl的。。。,微软的ItemsControl的ItemsSource属性就可以触发更新通知,我想肯定是有原因的,希望后面有时间好好研究一下。
问题得以解决,虽然不是最佳的解决办法。下面附上完整的测试用例代码,有兴趣的朋友们可以试验一下我说的情况。有高人知道更好的解决办法,希望不吝赐教。