[UWP]不怎麼實用的Shape指南:自定義Shape

来源:http://www.cnblogs.com/dino623/archive/2017/05/17/UWP_Custom_Shape.html
-Advertisement-
Play Games

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ASP.NET mvc的razor視圖引擎是一個非常好的.NET MVC框架內置的視圖引擎。一般情況我們使用.NET MVC框架為我們提供的這個Razor視圖引擎就足夠了。但是有時我們想在我們的項目支持多模板&skins機制,比如我們可能會有多套的模板,也就是多個View風格,而我們只需要改一下配置 ...
  • 請求一個ASP.NET mvc的網站和以前的web form是有區別的,ASP.NET MVC框架內部給我們提供了路由機制,當IIS接受到一個請求時,會先看是否請求了一個靜態資源(.html,css,js,圖片等),這一步是web form和mvc都是一樣的,如果不是則說明是請求的是一個動態頁面,就 ...
  • namespace ConsoleApp1 { class Program { static void Main(string[] args) { People tom = new People() ; tom.Name =" Tom"; tom.Age = 18; tom.MyPlace = Co ...
  • 本系列文章將複習和重新理解C#語言基礎知識 (一) C#基礎 (二)對象和類型 (三)繼承 (四)數組 ...
  • 這篇博客真是乾貨,幹得估計還有點“磕牙”,所以還提供視頻和代碼。但基礎稍弱的同學,怕還是得自行補充一些基礎知識——就一篇文章,確實沒辦法面面俱到。 視頻和代碼下載:Demo - 百度雲盤 · 一起幫 參考原文:Automatic ModelState validation in ASP.NET MV ...
  • 最近公司項目進行架構調整,由原來的三層架構改進升級到微服務架構(準確的說是服務化,還沒完全做到微的程度,顆粒度沒那麼細),遵循RESTFull規範,使前後端完全分離,實現大前端思想。由於是初次嘗試,中途也遇到了不少問題。今天就來討論一下其中之一的問題,WebAPI與前端Ajax 進行跨域數據交互時,... ...
  • Visual Studio啟動winform項目時提示:由於缺少調試目標,Visual Studio 無法開始調試。 ...
  • 關於這三個關鍵字之前可以研究一下原本的一些操作 觀察運行結果發現 值並沒有被改變,也就是說此時的操作的原理可能也是跟以前C語言的函數操作是一樣的 本文主要討論params關鍵字,ref關鍵字,out關鍵字。 1)params關鍵字,官方給出的解釋為用於方法參數長度不定的情況。有時候不能確定一個方法的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...