十年網(wǎng)站開發(fā)經(jīng)驗 + 多家企業(yè)客戶 + 靠譜的建站團隊
量身定制 + 運營維護+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
這篇文章介紹了繼承并自定義Shape的方法,不過,恐怕,事實上,100個xaml的程序員99個都不會用到。寫出來是因為反正都學(xué)了,當作寫個筆記。

通過這篇文章,你可以學(xué)到如下知識點:
自定義Shape。
DeferRefresh模式。
InvalidateArrange的應(yīng)用。
UWP中的Shape大部分都是密封類--除了Path。所以要自定義Shape只能從Path派生。Template10給出了這個例子:RingSegment 。
從這個類中可以看到,自定義Shape只需要簡單地在每個自定義屬性的屬性值改變時或SizeChanged時調(diào)用private void UpdatePath()為Path.Data賦值就完成了,很簡單吧。

RingSegment.StartAngle = 30; RingSegment.EndAngle = 330; RingSegment.Radius = 50; RingSegment.InnerRadius = 30;
這段代碼會產(chǎn)生一個問題:每更改一個屬性的值后都會調(diào)用UpdatePath(),那不就會重復(fù)調(diào)用四次?
事實上真的會,顯然這個類的作者也考慮過這個問題,所以提供了public void BeginUpdate()和public void EndUpdate()函數(shù)。
////// Suspends path updates until EndUpdate is called;/// public void BeginUpdate(){ _isUpdating = true; }////// Resumes immediate path updates every time a component property value changes. Updates the path./// public void EndUpdate(){ _isUpdating = false; UpdatePath(); }
使用這兩個方法重新寫上面那段代碼,就是這樣:
try{
RingSegment.BeginUpdate();
RingSegment.StartAngle = 30;
RingSegment.EndAngle = 330;
RingSegment.Radius = 100;
RingSegment.InnerRadius = 80;
}finally{
RingSegment.EndUpdate();
}這樣就保證了只有在調(diào)用EndUpdate()時才執(zhí)行UpdatePath(),而且只執(zhí)行一次。
在WPF中,DeferRefresh是一種更成熟的方案。相信很多開發(fā)者在用DataGrid時多多少少有用過(主要是通過CollectionView或CollectionViewSource)。典型的實現(xiàn)方式可以參考DataSourceProvider。在UWPCommunityToolkit中也通過AdvancedCollectionView實現(xiàn)了這種方式。
在RingSegment中添加實現(xiàn)如下:
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模式有兩個好處:
調(diào)用代碼比較簡單
通過_deferLevel判斷是否需要UpdatePath(),這樣即使多次調(diào)用DeferRefresh()也只會執(zhí)行一次UpdatePath()。譬如以下的調(diào)用方式:
using (RingSegment.DeferRefresh())
{
RingSegment.StartAngle = 30;
RingSegment.EndAngle = 330;
RingSegment.Radius = 50;
RingSegment.InnerRadius = 30; using (RingSegment.DeferRefresh())
{
RingSegment.Radius = 51;
RingSegment.InnerRadius = 31;
}
}也許你會覺得一般人不會寫得這么復(fù)雜,但在復(fù)雜的場景DeferRefresh模式是有存在意義的。假設(shè)現(xiàn)在要更新一個復(fù)雜的UI,這個UI由很多個代碼模塊驅(qū)動,但不清楚其它地方有沒有對需要更新的UI調(diào)用過DeferRefresh(),而創(chuàng)建一個DeferHelper 的消耗比起更新一次復(fù)雜UI的消耗低太多,所以執(zhí)行一次DeferRefresh()是個很合理的選擇。
看到
++_deferLevel這句代碼條件反射就會考慮到線程安全問題,但其實是過慮了。UWP要求操作UI的代碼都只能在UI線程中執(zhí)行,所以理論上來說所有UIElement及它的所有操作都是線程安全的。
每次更改屬性都要調(diào)用DeferRefresh顯然不是一個聰明的做法,而且在XAML中也不可能做到。另一種延遲執(zhí)行的機制是利用CoreDispatcher的public IAsyncAction RunAsync(CoreDispatcherPriority priority, DispatchedHandler agileCallback)函數(shù)異步地執(zhí)行工作項。要詳細解釋RunAsync可能需要一整篇文章的篇幅,簡單來說RunAsync的作用就是將工作項發(fā)送到一個隊列,UI線程有空的時候會從這個隊列獲取工作項并執(zhí)行。InvalidateArrange就是利用這種機制的典型例子。MSDN上對InvalidateArrange的解釋是:
使 UIElement 的排列狀態(tài)(布局)無效。失效后,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
}
});
}調(diào)用InvalidateArrange后將ArrangeDirty標記為True,然后異步執(zhí)行Measure及Arrange代碼進行布局。多次調(diào)用InvalidateArrange會檢查ArrangeDirty的狀態(tài)以免重復(fù)執(zhí)行。利用InvalidateArrange,我們可以在RingSegment的自定義屬性值改變事件中調(diào)用InvalidateArrange,異步地觸發(fā)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的很多代碼及經(jīng)驗更有參考價值,遇到難題不妨找些Silverlight代碼來作參考。

InvalidateArrange屬于比較核心的API,文檔中也充斥著“通常不建議“、”通常是不必要的”、“慎重地使用它”等字句,所以平時使用最好要謹慎。如果不是性能十分敏感的場合還是建議使用Template10的方式實現(xiàn)。
除了從Path派生,自定義Shape的功能也可以用TemplatedControl實現(xiàn),一般來說這種方式應(yīng)該是最簡單最通用的方式。下面的代碼使用TemplatedControl實現(xiàn)了一個三角形:
[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;
}
///
/// 標識 Direction 依賴屬性。
///
public static readonly DependencyProperty DirectionProperty =
DependencyProperty.Register("Direction", typeof(Direction), typeof(TriangleControl), new PropertyMetadata(Direction.Up, OnDirectionChanged)); ///
/// 獲取或設(shè)置Direction的值
///
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();
} ///
/// 獲取或設(shè)置PathElementStyle的值
///
public Style PathElementStyle
{ get { return (Style)GetValue(PathElementStyleProperty); } set { SetValue(PathElementStyleProperty, value); }
} ///
/// 標識 PathElementStyle 依賴屬性。
///
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;
}
}
這種方式的好處是容易實現(xiàn),而且兼容WPF和UWP。缺點是只能通過PathElementStyle修改Path的外觀,畢竟它不是Shape,而且增加了VisualTree的層次,不適合于性能敏感的場合。
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機、免備案服務(wù)器”等云主機租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價比高”等特點與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場景需求。