[UWP]不怎么实用的Shape指南:自定义Shape
原文:[UWP]不怎么实用的Shape指南:自定义Shape

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 hereData = 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

posted on 2018-11-06 14:26 NET未来之路 阅读(...) 评论(...) 编辑 收藏

转载于:https://www.cnblogs.com/lonelyxmas/p/9915075.html

[UWP]不怎么实用的Shape指南:自定义Shape相关推荐

  1. android shape 自定义,Android自定义shape的使用

    MainActivity如下: package cn.testshape; import android.os.Bundle; import android.app.Activity; /** * D ...

  2. android shape 绘制气泡图,气泡图-自定义 shape

    源码复制成功复制失败全屏 复制 运行 气泡图-自定义 shape // 自定义 shape, 支持图片形式的气泡 var Shape = G2.Shape; Shape.registerShape(' ...

  3. ValueError: Shape mismatch: The shape of labels (received (768,)) should equal the shape of logits e

    问题 运行tensorflow 的resnet网络,预测结果,出现报错 ValueError: Shape mismatch: The shape of labels (received (768,) ...

  4. 【干货】如何打造一流创业团队-创业者最实用的管理指南.pdf(附下载链接)...

    大家好,我是文文(微信:sscbg2020),今天给大家分享一份干货文档<如何打造一流创业团队-创业者最实用的管理指南.pdf>,在创业或有创业想法的伙伴们可以重点看看哦,干货满满. 本文 ...

  5. tensorflow 基础: static shape VS Dynamic shape, get_shape VS tf.shape() , reshape VS set_shape

    ######################################################################################### 1) 概念:stat ...

  6. numpy的Y.shape和Y.shape[0]的区别

    numpy数组的shape属性返回数组的维度.如果Y有n行和m列,那么Y.shape是(n,m).所以Y.shape[0]是n. 所以 假定 import numpy as np a=np.array ...

  7. Python中的shape[0]、shape[1]和shape[-1]含义

    使用shape[0]读取矩阵的行数(长度) shape[1]的读取矩阵列数 直接用.shape可以快速读取矩阵的形状. 使用方法 import numpy as np x=np.array([[1,2 ...

  8. python中shape[0]与shape[1]

    import numpy as np k = np.matrix([[1, 2, 3, 4],[5, 6, 7, 8],[9, 10, 11, 12]]) print(np.shape(k)) # 输 ...

  9. AcrMap通过几何计算添加shape.length和shape.area字段

    为了进行几何计算(给图层添加面积.周长字段),矢量图层必须要进行投影变换,针对现有的图层(WKID:4326),有两种处理方式: 方法一:源数据转换为投影坐标系 1.打开工具箱 2.广州地区一般投影成 ...

  10. Android自定义Shape

    1.圆角控件 首先,定义形状: drawable/roundctrl.xml <?xml version="1.0" encoding="UTF-8"?& ...

最新文章

  1. 复习计算机网络基础 day3---什么是计算机网络:
  2. socket模拟http的登陆_Python网络爬虫之模拟登陆 !
  3. 2019年最好的前端进阶课,合同保障不过20w年薪全额退款!
  4. 导致出现404的原因以及解决方案
  5. php str_replace替换特殊字符
  6. mysql case默认_MySQL数据库架构和同步复制流程
  7. python实现决策树ID3算法
  8. mysql安装包说明
  9. 25个最好免费下载电子书(Ebooks)的网站
  10. 威纶通触摸屏与西门子PLC200之间的无线通讯
  11. 基于ffmpeg+opencv的h264解码显示功能的实现
  12. python学习 - 52周存钱挑战
  13. 链路追踪-SkyWalking
  14. 关于 continue 用法
  15. 写一个方法把字符串大小写进行切换【前端每日一题-2】
  16. 基于FPGA的DDS 信号发生器(三)
  17. B-树叶子个数和关键字个数间关系推导
  18. 手机网络抓包 转载记录http://blog.csdn.net/skylin19840101/article/details/43485911
  19. arranged by JerryC
  20. 关于3Dmax出现错误奔溃和中毒解决问题方法

热门文章

  1. php 有request,php实现httpRequest的方法
  2. mysql odbc 驱动程序不支持所需的属性_ODBC 驱动程序不支持所需的属性。
  3. php 会话 写入浏览器,创建PHP会话变量会挂起我的浏览器
  4. wordpress插件WP Rest API接口文档说明
  5. 使用bat命令批量命名图片名称的方法及解决bat格式中文乱码的问题(如:图片.jpg)
  6. L2-007 家庭房产 (25分)
  7. motion blur matlab,Motion Blur app
  8. python3.7怎么安装的_怎么安装python3.7:python 3.7入门教程
  9. centos6.4-x86-64系统更新系统自带Apache Http Server
  10. apropos linux