1、確定控件應該繼承的基類
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
xmlns:ec="http://schemas.microsoft.com/expression/2010/controls"
②、具體用法
<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
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" />
其中關鍵屬性描述如下:

4、正式構建控件
4.1、刻度部分
4.1.1、長刻度部分
<!-- 刻度盤完整圓弧 -->
<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());
}

<!-- 刻度盤完整圓弧 -->
<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" />
效果如下:

#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個依賴屬性,這個就自己去弄吧,這里就不貼出代碼了。
代碼下載:https://github.com/zhidanfeng/WPF.UI

