1、確定控制項應該繼承的基類 從錶面上看,目前WPF自帶常用控制項中,沒有一個是接近這個表盤控制項的,但將該控制項拆分就能夠發現,該控制項的每個子部分都是在WPF中存在的,因此我們需要將各個子控制項組合才能形成這個表盤控制項,因此我們直接定義一個Dashboard類,繼承自Control類。 2、設置Dashbo ...
1、確定控制項應該繼承的基類
從錶面上看,目前WPF自帶常用控制項中,沒有一個是接近這個表盤控制項的,但將該控制項拆分就能夠發現,該控制項的每個子部分都是在WPF中存在的,因此我們需要將各個子控制項組合才能形成這個表盤控制項,因此我們直接定義一個Dashboard類,繼承自Control類。2、設置Dashboard的樣式
<Style TargetType="{x:Type local:Dashboard}"> <Setter Property="BorderBrush" Value="Black" /> <Setter Property="BorderThickness" Value="1" /> <Setter Property="Background" Value="Transparent" /> <Setter Property="SnapsToDevicePixels" Value="True" /> <Setter Property="UseLayoutRounding" Value="True" /> <Setter Property="HorizontalContentAlignment" Value="Left" /> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:Dashboard}"> <Grid> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
主要註意的是,因為我們還不知道Dashboard內部到底有哪些東西,因此這裡先放置了一個Grid,後面所有的代碼將在<Grid></Grid>中編寫
3、確定控制項的內部基本構造
該表盤控制項從錶面上看去,共由三個部分組成
- 有文字顯示的刻度
- 有進度展示的圓弧(紅色與灰色部分的圓弧)
- 中間偏下的內容展示區域
3.1、PathListBox
從控制項字面上描述來看Path指的是路徑,ListBox指的是一個列表。那麼PathListBox組合起來就是按照一定的路徑,來顯示列表中的各個Item,下麵使用具體代碼來認識一下這個控制項 ①、命名空間 在WPF預設的System.Windows.Controls下麵是找不到PathListBox的,需引入Microsoft.Expression.Controls.dll,然後在xaml中定義命名空間的別名xmlns:ec="http://schemas.microsoft.com/expression/2010/controls"
②、具體用法
既然是按照一定路徑排列各個Item,那肯定得先定義一個Path路徑,這裡先定義一個長500的直線Path<Path x:Name="path" Data="M0,0 500,0" Stroke="Black" StrokeThickness="1" />
然後放置PathListBox,在PathListBox的LayoutPath中去設置PathListBox應該按照哪個路徑去排列,在ItemsTemplate中設置每個Item子項應該呈現成什麼效果,最後在後臺設置PathListBox的ItemsSource,設置PathListBox一共有幾個Item子項。完整代碼如下:
<Grid VerticalAlignment="Center"> <Path x:Name="path" Data="M0,0 500,0" Stroke="Black" StrokeThickness="1" /> <ec:PathListBox x:Name="pathListBox"> <ec:PathListBox.ItemTemplate> <DataTemplate> <Border Width="3" Height="10" Background="Black" SnapsToDevicePixels="True" UseLayoutRounding="True" /> </DataTemplate> </ec:PathListBox.ItemTemplate> <ec:PathListBox.LayoutPaths> <ec:LayoutPath Distribution="Even" Orientation="OrientToPath" SourceElement="{Binding ElementName=path}" /> </ec:PathListBox.LayoutPaths> </ec:PathListBox> </Grid>
其中Distributeion與Orientation是關鍵屬性,SourceElement指向的就是PathListBox的排列路徑。最終效果如下圖所示:
3.2、Arc
Arc就是圓弧的意思,這個控制項比較簡單,直接貼出代碼 ①、命名控制項 需引入Microsoft.Expression.Drawing.dll,然後在xaml中定義命名空間的別名xmlns:ec="http://schemas.microsoft.com/expression/2010/controls"
②、具體用法
<ed:Arc x:Name="DoubleCircle" ArcThickness="8" ArcThicknessUnit="Pixel" EndAngle="120" StartAngle="-120" Width="200" Height="200" Fill="Red" Stretch="None" Stroke="Yellow" StrokeThickness="1" />
其中關鍵屬性描述如下:
學習了下PathListBox與Arc的用法後,下麵正式講解Dashboard是如何創建的。4、正式構建控制項
4.1、刻度部分
Dashboard表盤控制項共有2種刻度,一個是長一點的刻度,一個是短一點的刻度。如果我們只是用一個PathListBox是不能達到這種效果的,因此這裡使用了2個PathListBox,一個PathListBox放置短一點的刻度,一個PathListBox放置長一點的刻度,將他們2個疊加放在一起,就能達到這種效果。4.1.1、長刻度部分
這裡定義一個從-120°到120°的一個圓弧與一個PathListBox<!-- 刻度盤完整圓弧 --> <ed:Arc x:Name="LongTickPath" Margin="0" ArcThickness="0" ArcThicknessUnit="Pixel" EndAngle="120" StartAngle="-120" Stretch="None" Stroke="Black" StrokeThickness="1" /> <!-- 長刻度 --> <ec:PathListBox x:Name="LongTick" IsHitTestVisible="False"> <ec:PathListBox.ItemTemplate> <DataTemplate> <Border Width="1" Height="13" Background="Black" SnapsToDevicePixels="True" UseLayoutRounding="False" /> </DataTemplate> </ec:PathListBox.ItemTemplate> <ec:PathListBox.LayoutPaths> <ec:LayoutPath Distribution="Even" Orientation="OrientToPath" SourceElement="{Binding ElementName=LongTickPath}" /> </ec:PathListBox.LayoutPaths> </ec:PathListBox>
但是這樣只能看到圓弧,並沒有看到PathListBox的刻度效果,因為PathListBox沒有設置ItemsSource。而且由於我們是在自定義控制項,因此為了設置PathListBox的ItemsSource的值,我們需要在Dashboard定義一個依賴屬性LongTicksInternal,由於我們並不希望用戶能夠在外面能夠設置LongTicksInternal的值,因此在依賴屬性的set的時候,設置其訪問許可權,設置成private,這樣就只能在樣式裡面訪問該依賴屬性,用戶在外面使用的時候是看不到這個依賴屬性的。
#region LongTicksInternal 長刻度集合 public IList<object> LongTicksInternal { get { return (IList<object>)GetValue(LongTicksInternalProperty); } private set { SetValue(LongTicksInternalProperty, value); } } public static readonly DependencyProperty LongTicksInternalProperty = DependencyProperty.Register("LongTicksInternal", typeof(IList<object>), typeof(Dashboard)); #endregion
定義了該依賴屬性之後,將該依賴屬性給綁定到PathListBox的ItemsSource上面去
ItemsSource="{TemplateBinding ShortTicks}"
綁定了依賴屬性之後還是不能顯示,因為LongTicksInternal目前是空的一個集合,還需要給LongTicksInternal賦值。
public Dashboard() { this.LongTicksInternal = new List<object>(); for (int i = 0; i < 10; i++) { this.LongTicksInternal.Add(i); } }
效果如下:
#region LongTickCount 長刻度個數 public int LongTickCount { get { return (int)GetValue(LongTickCountProperty); } set { SetValue(LongTickCountProperty, value); } } public static readonly DependencyProperty LongTickCountProperty = DependencyProperty.Register("LongTickCount", typeof(int), typeof(Dashboard), new PropertyMetadata(5)); #endregion
改動下上面的for迴圈代碼,這樣就可以靈活的設置長刻度的個數了。
for (int i = 0; i < this.LongTickCount; i++) { this.LongTicksInternal.Add(i); }
4.1.2、短刻度部分
再次定義一個Path與一個PathListBox,並新增一個依賴屬性,用來設置PathListBox的ItemsSource
<!-- 刻度盤完整圓弧 --> <ed:Arc x:Name="ShortTickPath" Margin="0" ArcThickness="0" ArcThicknessUnit="Pixel" EndAngle="120" StartAngle="-120" Stretch="None" Stroke="Black" StrokeThickness="1" /> <!-- 長刻度 --> <ec:PathListBox x:Name="ShortTick" IsHitTestVisible="False" ItemsSource="{TemplateBinding ShortTicksInternal}"> <ec:PathListBox.ItemTemplate> <DataTemplate> <Border Width="1" Height="8" Background="Black" SnapsToDevicePixels="True" UseLayoutRounding="False" /> </DataTemplate> </ec:PathListBox.ItemTemplate> <ec:PathListBox.LayoutPaths> <ec:LayoutPath Distribution="Even" Orientation="OrientToPath" SourceElement="{Binding ElementName=ShortTickPath}" /> </ec:PathListBox.LayoutPaths> </ec:PathListBox>
短刻度個數的依賴屬性
#region ShortTicksInternal 短刻度集合 public IList<object> ShortTicksInternal { get { return (IList<object>)GetValue(ShortTicksInternalProperty); } set { SetValue(ShortTicksInternalProperty, value); } } public static readonly DependencyProperty ShortTicksInternalProperty = DependencyProperty.Register("ShortTicksInternal", typeof(IList<object>), typeof(Dashboard)); #endregion
但由於短刻度會有很多,不可能去細數表盤一共有多少個短刻度,而且如果手動設置所有的短刻度個數,會有一個問題就是短刻度和長刻度不會重合,導致寬的寬,窄的窄。我們不知道所有的短刻度個數,但是我們可以知道2個長刻度之間有多少個短刻度,因此定義一個ShortTickCount,用來設置2個長刻度間的短刻度的個數
#region ShortTickCount 短刻度個數 public int ShortTickCount { get { return (int)GetValue(ShortTickCountProperty); } set { SetValue(ShortTickCountProperty, value); } } public static readonly DependencyProperty ShortTickCountProperty = DependencyProperty.Register("ShortTickCount", typeof(int), typeof(Dashboard), new PropertyMetadata(5)); #endregion
根據LongTickCount與ShortTickCount,生成ShortTicksInternal
this.ShortTicksInternal = new List<object>(); for (int i = 0; i < (this.LongTickCount - 1) * (this.ShortTickCount + 1) + 1; i++) { this.ShortTicksInternal.Add(new object()); }
這裡簡單介紹一下這個演算法:LongTickCount有9個,ShortTickCount有5個,由圖示可以看出,我們可以將表盤刻度分成8份,每一份由1個長刻度和5個短刻度組成,因此每一份的表達式就是【ShortTickCount + 1】,然後總共分為8份,表達式就成了 (LongTickCount - 1) * (ShortTickCount + 1),最後我們發現第9份只有一個長刻度,這其實也是一個短刻度,那麼最終的表達式就是 (LongTickCount - 1) * (ShortTickCount + 1) + 1 微調下Arc的邊框,將其去掉,然後調整下短刻度的Arc的Margin,將其調整成和長刻度的底部水平
<!-- 刻度盤完整圓弧 --> <ed:Arc x:Name="LongTickPath" Margin="0" ArcThickness="0" ArcThicknessUnit="Pixel" EndAngle="120" StartAngle="-120" Stretch="None" Stroke="Black" StrokeThickness="0" /> <!-- 長刻度 --> <ec:PathListBox x:Name="LongTick" IsHitTestVisible="False" ItemsSource="{TemplateBinding LongTicksInternal}"> <ec:PathListBox.ItemTemplate> <DataTemplate> <Border Width="1" Height="13" Background="Black" VerticalAlignment="Bottom" SnapsToDevicePixels="True" UseLayoutRounding="False" /> </DataTemplate> </ec:PathListBox.ItemTemplate> <ec:PathListBox.LayoutPaths> <ec:LayoutPath Distribution="Even" Orientation="OrientToPath" SourceElement="{Binding ElementName=LongTickPath}" /> </ec:PathListBox.LayoutPaths> </ec:PathListBox> <!-- 刻度盤完整圓弧 --> <ed:Arc x:Name="ShortTickPath" Margin="5" ArcThickness="0" ArcThicknessUnit="Pixel" EndAngle="120" StartAngle="-120" Stretch="None" Stroke="Black" StrokeThickness="0" /> <!-- 長刻度 --> <ec:PathListBox x:Name="ShortTick" IsHitTestVisible="False" ItemsSource="{TemplateBinding ShortTicksInternal}"> <ec:PathListBox.ItemTemplate> <DataTemplate> <Border Width="1" Height="8" Background="Black" VerticalAlignment="Bottom" SnapsToDevicePixels="True" UseLayoutRounding="False" /> </DataTemplate> </ec:PathListBox.ItemTemplate> <ec:PathListBox.LayoutPaths> <ec:LayoutPath Distribution="Even" Orientation="OrientToPath" SourceElement="{Binding ElementName=ShortTickPath}" /> </ec:PathListBox.LayoutPaths> </ec:PathListBox>
終於,刻度的效果出來了
4.1.3、文字部分
上一節已經將刻度做出來了,還差一個文字部分。文字部分與刻度部分同理,只不過不顯示成刻度了,需將每個Item的樣式設置成TextBlock
<ed:Arc x:Name="NumberPath" Margin="20" ArcThickness="0" ArcThicknessUnit="Pixel" EndAngle="120" StartAngle="-120" Stretch="None" /> <!-- 刻度上顯示的數字 --> <ec:PathListBox x:Name="Number" IsHitTestVisible="False" ItemsSource="{TemplateBinding NumberListInternal}"> <ec:PathListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding}" /> </DataTemplate> </ec:PathListBox.ItemTemplate> <ec:PathListBox.LayoutPaths> <ec:LayoutPath Distribution="Even" Orientation="OrientToPath" SourceElement="{Binding ElementName=NumberPath}" /> </ec:PathListBox.LayoutPaths> </ec:PathListBox>
#region NumberListInternal 數字集合 public IList<object> NumberListInternal { get { return (IList<object>)GetValue(NumberListInternalProperty); } set { SetValue(NumberListInternalProperty, value); } } public static readonly DependencyProperty NumberListInternalProperty = DependencyProperty.Register("NumberListInternal", typeof(IList<object>), typeof(Dashboard)); #endregion
由於表盤上面顯示的數字會有不同,因此應該讓其可以設置,因此定義一個最大值與最小值的依賴屬性,表盤上面的文字應該根據這兩個屬性來自動生成
#region Minimum 最小值 /// <summary> /// 最小值依賴屬性,用於Binding /// </summary> public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register( "Minimum", typeof(double), typeof(Dashboard), new PropertyMetadata(0.0)); /// <summary> /// 獲取或設置最小值. /// </summary> /// <value>最小值.</value> public double Minimum { get { return (double)GetValue(MinimumProperty); } set { SetValue(MinimumProperty, value); } } #endregion #region Maximum 最大值 /// <summary> /// 最大值依賴屬性,用於Binding /// </summary> public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register( "Maximum", typeof(double), typeof(Dashboard), new PropertyMetadata(100.0)); /// <summary> /// 獲取或設置最大值. /// </summary> /// <value>最大值.</value> public double Maximum { get { return (double)GetValue(MaximumProperty); } set { SetValue(MaximumProperty, value); } } #endregion
由於文字只在長刻度下麵顯示,因此在設置Long的for迴圈中設置的值
this.NumberListInternal = new List<object>();
for (int i = 0; i < this.LongTickCount; i++)
{
this.NumberListInternal.Add(Math.Round(this.Minimum + (this.Maximum - this.Minimum) / (this.LongTickCount - 1) * i));
this.LongTicksInternal.Add(i);
}
演算法解析:上面已經說到,我們將表盤刻度分成了8份,那麼 (this.Maximum - this.Minimum) / (this.LongTickCount - 1) 可以得到每一份所代表的值,每一份乘以i,就表示接下來的每份的值,但是表盤不可能永遠都是從0開始的,我們會給它設置最小值,因此得加上Minimum,最後得出來的結果有可能會有小數點,為了省去這個小數點,使用了Math.Round()函數來取整。至此,刻度與數字部分完成了。
4.2、進度(當前值)部分
這段圓弧一共由兩個圓弧組成,紅色表示當前值,灰色只是作為底色展示用的,並無太大作用
<!-- 刻度盤完整圓弧 --> <ed:Arc x:Name="DoubleCircle" Margin="50" ArcThickness="1" ArcThicknessUnit="Pixel" EndAngle="120" SnapsToDevicePixels="True" StartAngle="-120" Stretch="None" Stroke="#746E7A" StrokeThickness="1" UseLayoutRounding="True" /> <!-- 刻度盤當前值對應的圓弧 --> <ed:Arc x:Name="PART_IncreaseCircle" Margin="50" ArcThickness="1" ArcThicknessUnit="Pixel" RenderTransformOrigin="0.5,0.5" StartAngle="-120" EndAngle="10" Stretch="None" Stroke="Yellow" StrokeThickness="1" />
效果如下:
至此,控制項的內部構造基本完成,接下來完成最後一部分,就是根據表盤當前值來設置黃色部分的角度,從而實現儀錶盤的效果。 上面我們已經定義了最大值與最小值,還差一個當前值Value,因此定義一個Value的依賴屬性
#region Value 當前值 /// <summary> /// 最大值依賴屬性,用於Binding /// </summary> public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof(double), typeof(Dashboard), new PropertyMetadata(0.0, new PropertyChangedCallback(OnValuePropertyChanged))); /// <summary> /// 獲取或設置當前值 /// </summary> public double Value { get { return (double)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { //Dashboard dashboard = d as Dashboard; //dashboard.OldAngle = dashboard.Angle; //dashboard.SetAngle(); //dashboard.TransformAngle(); } #endregion
之外為了設置圓弧的角度,還需要新增一個Angle依賴屬性
#region Angle public double Angle { get { return (double)GetValue(AngleProperty); } set { SetValue(AngleProperty, value); } } public static readonly DependencyProperty AngleProperty = DependencyProperty.Register("Angle", typeof(double), typeof(Dashboard), new PropertyMetadata(0d)); #endregion
在代碼中,根據Value的值,自動設置Angle
private void SetAngle() { var diff = this.Maximum - this.Minimum; var valueDiff = this.Value - this.Minimum; this.Angle = -120 + (120 - (-120)) / diff * valueDiff; }
演算法解析:結束角度-起始角度可以得出圓弧總共經過的角度值,除以最大值與最小值的差值,得到1°對應的數值,乘以當前值與最小值的差值就可以得到差值所對應的角度總和了。由於起始角度不固定,因此最終的角度值應該是:起始角度+差值角度和
最終,我們的效果實現了:這裡面有一個不足的地方就是起始角度和結束角度硬編碼成-120和120了,為了靈活性,可以將其設置為2個依賴屬性,這個就自己去弄吧,這裡就不貼出代碼了。