1. 前言 這篇文章介紹了繼承並自定義Shape的方法,不過,恐怕,事實上,100個xaml的程式員99個都不會用到。寫出來是因為反正都學了,當作寫個筆記。 通過這篇文章,你可以學到如下知識點: 自定義Shape。 DeferRefresh模式。 InvalidateArrange的應用。 2. 從 ...
1. 前言
這篇文章介紹了繼承並自定義Shape的方法,不過,恐怕,事實上,100個xaml的程式員99個都不會用到。寫出來是因為反正都學了,當作寫個筆記。
通過這篇文章,你可以學到如下知識點:
- 自定義Shape。
- DeferRefresh模式。
- InvalidateArrange的應用。
2. 從Path派生
UWP中的Shape大部分都是密封類--除了Path。所以要自定義Shape只能從Path派生。Template10給出了這個例子:RingSegment 。
從這個類中可以看到,自定義Shape只需要簡單地在每個自定義屬性的屬性值改變時或SizeChanged時調用private void UpdatePath()
為Path.Data賦值就完成了,很簡單吧。
RingSegment.StartAngle = 30;
RingSegment.EndAngle = 330;
RingSegment.Radius = 50;
RingSegment.InnerRadius = 30;
3. BeginUpdate、EndUpdate與DeferRefresh
這段代碼會產生一個問題:每更改一個屬性的值後都會調用UpdatePath(),那不就會重覆調用四次?
事實上真的會,顯然這個類的作者也考慮過這個問題,所以提供了public void BeginUpdate()
和public void EndUpdate()
函數。
/// <summary>
/// Suspends path updates until EndUpdate is called;
/// </summary>
public void BeginUpdate()
{
_isUpdating = true;
}
/// <summary>
/// Resumes immediate path updates every time a component property value changes. Updates the path.
/// </summary>
public void EndUpdate()
{
_isUpdating = false;
UpdatePath();
}
使用這兩個方法重新寫上面那段代碼,就是這樣:
try
{
RingSegment.BeginUpdate();
RingSegment.StartAngle = 30;
RingSegment.EndAngle = 330;
RingSegment.Radius = 100;
RingSegment.InnerRadius = 80;
}
finally
{
RingSegment.EndUpdate();
}
這樣就保證了只有在調用EndUpdate()時才執行UpdatePath(),而且只執行一次。
在WPF中,DeferRefresh是一種更成熟的方案。相信很多開發者在用DataGrid時多多少少有用過(主要是通過CollectionView或CollectionViewSource)。典型的實現方式可以參考DataSourceProvider。在UWPCommunityToolkit中也通過AdvancedCollectionView實現了這種方式。
在RingSegment中添加實現如下:
private int _deferLevel;
public virtual IDisposable DeferRefresh()
{
++_deferLevel;
return new DeferHelper(this);
}
private void EndDefer()
{
Debug.Assert(_deferLevel > 0);
--_deferLevel;
if (_deferLevel == 0)
{
UpdatePath();
}
}
private class DeferHelper : IDisposable
{
public DeferHelper(RingSegment source)
{
_source = source;
}
private RingSegment _source;
public void Dispose()
{
GC.SuppressFinalize(this);
if (_source != null)
{
_source.EndDefer();
_source = null;
}
}
}
使用如下:
using (RingSegment.DeferRefresh())
{
RingSegment.StartAngle = 30;
RingSegment.EndAngle = 330;
RingSegment.Radius = 100;
RingSegment.InnerRadius = 80;
}
使用DeferRefresh模式有兩個好處:
- 調用代碼比較簡單
- 通過
_deferLevel
判斷是否需要UpdatePath()
,這樣即使多次調用DeferRefresh()
也只會執行一次UpdatePath()
。譬如以下的調用方式:
using (RingSegment.DeferRefresh())
{
RingSegment.StartAngle = 30;
RingSegment.EndAngle = 330;
RingSegment.Radius = 50;
RingSegment.InnerRadius = 30;
using (RingSegment.DeferRefresh())
{
RingSegment.Radius = 51;
RingSegment.InnerRadius = 31;
}
}
也許你會覺得一般人不會寫得這麼複雜,但在複雜的場景DeferRefresh模式是有存在意義的。假設現在要更新一個複雜的UI,這個UI由很多個代碼模塊驅動,但不清楚其它地方有沒有對需要更新的UI調用過DeferRefresh()
,而創建一個DeferHelper 的消耗比起更新一次複雜UI的消耗低太多,所以執行一次DeferRefresh()
是個很合理的選擇。
看到
++_deferLevel
這句代碼條件反射就會考慮到線程安全問題,但其實是過慮了。UWP要求操作UI的代碼都只能在UI線程中執行,所以理論上來說所有UIElement及它的所有操作都是線程安全的。
4. InvalidateArrange
每次更改屬性都要調用DeferRefresh顯然不是一個聰明的做法,而且在XAML中也不可能做到。另一種延遲執行的機制是利用CoreDispatcher的public IAsyncAction RunAsync(CoreDispatcherPriority priority, DispatchedHandler agileCallback)
函數非同步地執行工作項。要詳細解釋RunAsync可能需要一整篇文章的篇幅,簡單來說RunAsync的作用就是將工作項發送到一個隊列,UI線程有空的時候會從這個隊列獲取工作項並執行。InvalidateArrange就是利用這種機制的典型例子。MSDN上對InvalidateArrange的解釋是:
使 UIElement 的排列狀態(佈局)無效。失效後,UIElement 將以非同步方式更新其佈局。
將InvalidateArrange的邏輯簡化後大概如下:
protected bool ArrangeDirty { get; set; }
public void InvalidateArrange()
{
if (ArrangeDirty == true)
return;
ArrangeDirty = true;
Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
ArrangeDirty = false;
lock (this)
{
//Measure
//Arrange
}
});
}
調用InvalidateArrange後將ArrangeDirty標記為True,然後非同步執行Measure及Arrange代碼進行佈局。多次調用InvalidateArrange會檢查ArrangeDirty的狀態以免重覆執行。利用InvalidateArrange,我們可以在RingSegment的自定義屬性值改變事件中調用InvalidateArrange,非同步地觸發LayoutUpdated併在其中改變Path.Data。
修改後的代碼如下:
private bool _realizeGeometryScheduled;
private Size _orginalSize;
private Direction _orginalDirection;
private void OnStartAngleChanged(double oldStartAngle, double newStartAngle)
{
InvalidateGeometry();
}
private void OnEndAngleChanged(double oldEndAngle, double newEndAngle)
{
InvalidateGeometry();
}
private void OnRadiusChanged(double oldRadius, double newRadius)
{
this.Width = this.Height = 2 * Radius;
InvalidateGeometry();
}
private void OnInnerRadiusChanged(double oldInnerRadius, double newInnerRadius)
{
if (newInnerRadius < 0)
{
throw new ArgumentException("InnerRadius can't be a negative value.", "InnerRadius");
}
InvalidateGeometry();
}
private void OnCenterChanged(Point? oldCenter, Point? newCenter)
{
InvalidateGeometry();
}
protected override Size ArrangeOverride(Size finalSize)
{
if (_realizeGeometryScheduled == false && _orginalSize != finalSize)
{
_realizeGeometryScheduled = true;
LayoutUpdated += OnTriangleLayoutUpdated;
_orginalSize = finalSize;
}
base.ArrangeOverride(finalSize);
return finalSize;
}
protected override Size MeasureOverride(Size availableSize)
{
return new Size(base.StrokeThickness, base.StrokeThickness);
}
public void InvalidateGeometry()
{
InvalidateArrange();
if (_realizeGeometryScheduled == false )
{
_realizeGeometryScheduled = true;
LayoutUpdated += OnTriangleLayoutUpdated;
}
}
private void OnTriangleLayoutUpdated(object sender, object e)
{
_realizeGeometryScheduled = false;
LayoutUpdated -= OnTriangleLayoutUpdated;
RealizeGeometry();
}
private void RealizeGeometry()
{
//other code here
Data = pathGeometry;
}
這些代碼參考了ExpressionSDK的Silverlight版本。ExpressionSDK提供了一些Shape可以用作參考。(安裝Blend後通常可以在這個位置找到它:C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\Silverlight\v5.0\Libraries\Microsoft.Expression.Drawing.dll)由於比起WPF,Silverlight更接近UWP,所以Silverlight的很多代碼及經驗更有參考價值,遇到難題不妨找些Silverlight代碼來作參考。
InvalidateArrange屬於比較核心的API,文檔中也充斥著“通常不建議“、”通常是不必要的”、“慎重地使用它”等字句,所以平時使用最好要謹慎。如果不是性能十分敏感的場合還是建議使用Template10的方式實現。
5. 使用TemplatedControl實現
除了從Path派生,自定義Shape的功能也可以用TemplatedControl實現,一般來說這種方式應該是最簡單最通用的方式。下麵的代碼使用TemplatedControl實現了一個三角形:
[TemplatePart(Name = PathElementName,Type =typeof(Path))]
[StyleTypedProperty(Property = nameof(PathElementStyle), StyleTargetType =typeof(Path))]
public class TriangleControl : Control
{
private const string PathElementName = "PathElement";
public TriangleControl()
{
this.DefaultStyleKey = typeof(TriangleControl);
this.SizeChanged += OnTriangleControlSizeChanged;
}
/// <summary>
/// 標識 Direction 依賴屬性。
/// </summary>
public static readonly DependencyProperty DirectionProperty =
DependencyProperty.Register("Direction", typeof(Direction), typeof(TriangleControl), new PropertyMetadata(Direction.Up, OnDirectionChanged));
/// <summary>
/// 獲取或設置Direction的值
/// </summary>
public Direction Direction
{
get { return (Direction)GetValue(DirectionProperty); }
set { SetValue(DirectionProperty, value); }
}
private static void OnDirectionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var target = obj as TriangleControl;
var oldValue = (Direction)args.OldValue;
var newValue = (Direction)args.NewValue;
if (oldValue != newValue)
target.OnDirectionChanged(oldValue, newValue);
}
protected virtual void OnDirectionChanged(Direction oldValue, Direction newValue)
{
UpdateShape();
}
/// <summary>
/// 獲取或設置PathElementStyle的值
/// </summary>
public Style PathElementStyle
{
get { return (Style)GetValue(PathElementStyleProperty); }
set { SetValue(PathElementStyleProperty, value); }
}
/// <summary>
/// 標識 PathElementStyle 依賴屬性。
/// </summary>
public static readonly DependencyProperty PathElementStyleProperty =
DependencyProperty.Register("PathElementStyle", typeof(Style), typeof(TriangleControl), new PropertyMetadata(null));
private Path _pathElement;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_pathElement = GetTemplateChild("PathElement") as Path;
}
private void OnTriangleControlSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateShape();
}
private void UpdateShape()
{
var geometry = new PathGeometry();
var figure = new PathFigure { IsClosed = true };
geometry.Figures.Add(figure);
switch (Direction)
{
case Direction.Left:
figure.StartPoint = new Point(ActualWidth, 0);
var segment = new LineSegment { Point = new Point(ActualWidth, ActualHeight) };
figure.Segments.Add(segment);
segment = new LineSegment { Point = new Point(0, ActualHeight / 2) };
figure.Segments.Add(segment);
break;
case Direction.Up:
figure.StartPoint = new Point(0, ActualHeight);
segment = new LineSegment { Point = new Point(ActualWidth / 2, 0) };
figure.Segments.Add(segment);
segment = new LineSegment { Point = new Point(ActualWidth, ActualHeight) };
figure.Segments.Add(segment);
break;
case Direction.Right:
figure.StartPoint = new Point(0, 0);
segment = new LineSegment { Point = new Point(ActualWidth, ActualHeight / 2) };
figure.Segments.Add(segment);
segment = new LineSegment { Point = new Point(0, ActualHeight) };
figure.Segments.Add(segment);
break;
case Direction.Down:
figure.StartPoint = new Point(0, 0);
segment = new LineSegment { Point = new Point(ActualWidth, 0) };
figure.Segments.Add(segment);
segment = new LineSegment { Point = new Point(ActualWidth / 2, ActualHeight) };
figure.Segments.Add(segment);
break;
}
_pathElement.Data = geometry;
}
}
<Style TargetType="Path"
x:Key="PathElementStyle">
<Setter Property="Stroke"
Value="RoyalBlue" />
<Setter Property="StrokeThickness"
Value="10" />
<Setter Property="Stretch"
Value="Fill" />
</Style>
<Style TargetType="local:TriangleControl">
<Setter Property="PathElementStyle"
Value="{StaticResource PathElementStyle}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TriangleControl">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Path x:Name="PathElement"
Style="{TemplateBinding PathElementStyle}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
這種方式的好處是容易實現,而且相容WPF和UWP。缺點是只能通過PathElementStyle修改Path的外觀,畢竟它不是Shape,而且增加了VisualTree的層次,不適合於性能敏感的場合。
6. 結語
自定義Shape真的很少用到,網上也沒有多少這方面的資料,如果你真的用到的話希望這篇文章對你有幫助。
其次,希望其它的知識點,例如DeferRefresh模式、InvalidateArrange的應用等也對你有幫助。
7. 參考
UIElement.InvalidateArrange Method
Template10.Controls.RingSegment