用Windows 8.1的童鞋应该知道OneNote里面有一个RadialMenu。如下图,下图是WIn10应用Drawboard PDF的RadialMenu,Win8.1的机器不好找了。哈哈,由于整个文章比较长,大家可以放《给我一首歌的时间》 边听边看。<滑稽>

从设计到开发包括修复一些bug,大概用了不连续的2个月,想看源代码的童鞋可以先到 RadialMenu 查看效果和代码。

先放上项目里面的最终效果

下面说下整个的过程

1.布局

首先,可以看到这个控件一个圆盘形状的东东,在点击子菜单的时候,菜单变化,并且带有duangduangduang的特效。

先来看看RadialMenu的ControlTemplate

<ControlTemplate TargetType="local:RadialMenu">
<!--<Popup x:Name="Popup" IsLightDismissEnabled="False" IsOpen="{TemplateBinding IsOpen}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">-->
<Grid x:Name="Root" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
<Grid x:Name="ContentGrid">
<Ellipse Fill="{TemplateBinding Background}" StrokeThickness="{TemplateBinding ExpandAreaThickness}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
<local:RadialMenuItemsPresenter x:Name="CurrentItemPresenter" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding CurrentItem.Items,RelativeSource={RelativeSource TemplatedParent}}"
>
<local:RadialMenuItemsPresenter.ItemsPanel>
<ItemsPanelTemplate>
<local:RadialMenuPanel/>
</ItemsPanelTemplate>
</local:RadialMenuItemsPresenter.ItemsPanel>
</local:RadialMenuItemsPresenter>
</Grid>
<local:RadialMenuNavigationButton x:Name="NavigationButton" Width="{TemplateBinding RadialMenuNavigationButtonSize}" Height="{TemplateBinding RadialMenuNavigationButtonSize}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Grid>
<!--</Popup>-->
</ControlTemplate>

一共三个东西,

1)Ellipse -整个RadialMenu的外形以及背景

2)RadialMenuItemsPresenter-用来展示各个菜单

3)RadialMenuNavigationButton-中间那个导航的按钮

1,3应该比较好理解,大家看看代码就能明白。

我着重讲下2,这个也是开发一个自定义控件比较重要的操作,就是知道怎么按自己的想法去布局。

RadialMenuItemsPresenter 继承ItemsControl,它的ItemsPanel是RadialMenuPanel。

  <local:RadialMenuItemsPresenter x:Name="CurrentItemPresenter"
ItemsSource="{Binding CurrentItem.Items,RelativeSource={RelativeSource TemplatedParent}}"
>
<local:RadialMenuItemsPresenter.ItemsPanel>
<ItemsPanelTemplate>
<local:RadialMenuPanel/>
</ItemsPanelTemplate>
</local:RadialMenuItemsPresenter.ItemsPanel>
</local:RadialMenuItemsPresenter>

RadialMenuPanel 就是我们最重要控制怎么展示RadialMenuItemsPresenter 的Items。

重写过的Panel 同学一定知道MeasureOverride ,ArrangeOverride 这2个东西。如果不清楚的一定要去看看葡萄城控件技术团队的2篇文章 Measure,Arrange。看过之后你将对这2个方法有新的了解,对控件的布局更加清晰。

下面请看我优(丑)美(陋)的手画图

A菜单将它Arrange到图中位置,B菜单也放到同样的位置,但是给它做一定的旋转。按做这种原理,把全部的菜单都Arrange到控件的中上位置,并且给他们都做一定的旋转,这样就组成了整个的圆弧,这个递增的角度就是 360°除以菜单的个数。

具体的代码,请看ArrangeChildren方法。

2.每个菜单的设计

RadialMenuItem -最基础的显示文字,图片等等

RadialColorMenuItem-用于显示颜色

RadialNumericMenuItem-用于显示数字,它的子集是RadialNumericMenuChildrenItem,是一个内部的类。

下来还是从我优(丑)美(陋)的手画图开始介绍结构吧

蓝色的线是整个item的最外边,黄色是展开子菜单的按钮的中线。

从蓝色到红色部分就是整个展开子菜单按钮。 就是图中的蓝色的那个部分。

那么我们如何来做出这种效果呢。这里我们要用到Path。

可以看到我们只需要把Path的StorkeThickness设置为展开按钮的宽,而Path的位置为我们手绘图中的黄色那条线。宽为蓝色到红色线之间。就可以了。代码中具体在RadialMenuPanel.cs 中的169行。就是整个圆的半径减去展开区域的宽度的一半。

 var expandAreaRadius = radius - Menu.ExpandAreaThickness / 2.0;

这个半径就是ArcSegment的Size,那么StartPoint和ArcSegment的Point怎么计算呢。

再来上我的手绘图,这次真是手绘的了。。

如上图,知道了半径,可以看到里面有个直角三角形,一个边是r,一个边是x,一个边是y,由于我们可以算出每个item占的角度,那么这个直角三角形的角就等于item 角度的一半,使用勾股定理(话说最近项目里面还用到很多几何知识,还好没有把知识还老师)可以算出x,y。

那么StartPoint= (item的宽度-x,item的高度-y)。因为另一个点是对称的。。不能算出Point=(item的宽度+x,item的高度-y)

那么我们就可以画出这个圆弧了。。

同理RadialColorMenuItem ,那个色块跟扩展子菜单按钮部分一样。在代码中具体在RadialMenuPanel.cs 中的175-177行

  var colorElementStrokeThickness = radius - Menu.ExpandAreaThickness - Menu.SelectedElementThickness - navigationButtonSize * 0.5;

  var colorElementRadius = radius - Menu.ExpandAreaThickness - Menu.SelectedElementThickness - colorElementStrokeThickness / 2.0;

这里说明下,色块跟扩展子菜单按钮部分之间还有一个选中效果的色块。

RadialNumericMenuItem

它的子集是RadialNumericMenuChildrenItem

RadialNumericMenuChildrenItem  的选中效果和鼠标悬停效果是2条Line,代码中具体在RadialMenuPanel.cs 中的180-197行

3.动画效果

这篇是Composition API,当然要用Composition API来做动画呢。。哈哈。

先来说展开/收缩的动画

                _contentGridVisual = ElementCompositionPreview.GetElementVisual(_contentGrid);
_compositor = _contentGridVisual.Compositor; rotationAnimation = _compositor.CreateScalarKeyFrameAnimation();
scaleAnimation = _compositor.CreateVector3KeyFrameAnimation(); var easing = _compositor.CreateLinearEasingFunction(); _contentGrid.SizeChanged += (s, e) =>
{
_contentGridVisual.CenterPoint = new Vector3((float)_contentGrid.ActualWidth / 2.0f, (float)_contentGrid.ActualHeight / 2.0f, );
}; scaleAnimation.InsertKeyFrame(0.0f, new Vector3() { X = 0.0f, Y = 0.0f, Z = 0.0f });
scaleAnimation.InsertKeyFrame(1.0f, new Vector3() { X = 1.0f, Y = 1.0f, Z = 0.0f }, easing); rotationAnimation.InsertKeyFrame(0.0f, -90.0f);
rotationAnimation.InsertKeyFrame(1.0f, 0.0f, easing);

可以看到我准备2个动画,一个是旋转一个缩小放大,而他们的作用题是RadialMenu的中ContentGrid(不包括中心的那个导航按钮)

在10586版本之上,动画有Direction这个属性,可以反转动画效果,也就是说,我这里写2个动画,就可以做到展开/收缩

有没有觉得很简单。。为啥这里特别提10586呢。因为后面有坑。

再说说切换菜单效果,就是点了某个子菜单的展开按钮,切到它对应的菜单去的效果。其实更简单,直接用上面的scaleAnimation就可以。不一样的是我们使用了下面的代码来让菜单先收,再开。

                var batch = _compositor.GetCommitBatch(CompositionBatchTypes.Animation);
batch.Completed += (s, e) =>
{
SetCurrentItemIn(currentItem); scaleAnimation.Duration = TimeSpan.FromSeconds(0.1);
if (!lowerThan14393)
{
scaleAnimation.Direction = AnimationDirection.Normal;
}
_contentGridVisual.StartAnimation(nameof(_contentGridVisual.Scale), scaleAnimation);
}; scaleAnimation.Duration = TimeSpan.FromSeconds(0.07);
scaleAnimation.Direction = AnimationDirection.Reverse;
_contentGridVisual.StartAnimation(nameof(_contentGridVisual.Scale), scaleAnimation);

CompositionCommitBatch 有一个completed事件,这个就是动画结束时候触发的事件。

另外发现CompositionScopedBatch 也completed事件。

先看看官方介绍CompositionCommitBatch, CompositionScopedBatch

public void BatchAnimations()
{
// Create a Scoped batch to capture animation completion events
_batch = _compositor.CreateScopedBatch(CompositionBatchTypes.Animation); // Executing the Offset animation and aggregating completion event
ApplyOffsetAnimation(_greenSquare); // Suspending to exclude the following Rotation animation from the batch
_batch.Suspend(); // Executing the Rotation animation
ApplyRotationAnimation(_greenSquare); // Resuming the batch to collect additional animations
_batch.Resume(); // Executing the Opacity animation and aggregating completion event
ApplyOpacityAnimation(_greenSquare); // Batch is ended and no objects can be added
_batch.End(); // Method triggered when batch completion event fires
_batch.Completed += OnBatchCompleted;
}

可以看到CompositionScopedBatch 多了3个方法Resume,Suspend,End,也就是说你可以暂停动画,并且在这个时间段加入新的动画到这个group里面,然后启动,最后每个动画完成的时候都会触发complete。

最后是整个RadialMenu的交互。就是移动动画。代码太简单了。

 _radialMenuVisual = ElementCompositionPreview.GetElementVisual(this);
_radialMenuVisual.Offset = Offset;

移动的时候处理_radialMenuVisual的Offset就可以了。。哈哈。(注意我没有把RadialIMenu的元素放在一个Popup里面,原因是需要满足,点击其他地方的时候不要关闭RadialMenu。如果Popup的IsLightDismissEnabled 的属性设置为flase的话,其他地方点击又不会有事件触发。现在的方案是把这个控件总是放在最高的level 层)

因为设置了控件的ManipulationMode

ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY | ManipulationModes.TranslateInertia;

那么我们可以在OnManipulationDelta 方法中去改变Offset。达到移动整个控件的目的

        protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
{
UpdateOffset(e.Delta.Translation.X, e.Delta.Translation.Y, e.IsInertial);
}

这里当有惯性的时候我做了反弹效果,具体的代码在RadilaMenu的BounceOffset 方法中。

不要以为这样控件就搞定了,坑坑坑。在等着你。。

第一个地方:

之前讲了在10586上面。没有反转动画这个属性,而且我发现10586上面动画简直是垃圾。各种掉帧,各种失效,但是项目里面要支持10586,那我们怎么办呢,嗯。别忘记了我们还有Storyboard。我们需要4个动画

        //use animation for lower than 14393
Storyboard expand;
Storyboard collapse;
Storyboard open;
Storyboard close;

准备动画代码如下,说实话,确实有点麻烦难懂,这就是我为啥一直推荐使用Composition API的其中一个原因吧。

                _contentGrid.RenderTransformOrigin = new Point(0.5, 0.5);
_contentGrid.RenderTransform = new CompositeTransform();
#region expand
expand = new Storyboard();
var duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleX)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = , EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
expand.Children.Add(duk); duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleY)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = , EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
expand.Children.Add(duk); duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.Rotation)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = - });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = , EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
expand.Children.Add(duk);
#endregion #region collapse
collapse = new Storyboard();
duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleX)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = , EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
collapse.Children.Add(duk); duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleY)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = , EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
collapse.Children.Add(duk); duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.Rotation)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = -, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
collapse.Children.Add(duk); #endregion #region Open
open = new Storyboard();
duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleX)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.1)), Value = , EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
open.Children.Add(duk); duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleY)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.1)), Value = , EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
open.Children.Add(duk);
#endregion #region Close
close = new Storyboard();
duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleX)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.07)), Value = , EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
close.Children.Add(duk); duk = new DoubleAnimationUsingKeyFrames();
Storyboard.SetTarget(duk, _contentGrid);
Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleY)");
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds()), Value = });
duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.07)), Value = , EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
close.Children.Add(duk);
#endregion

第二个地方:

为了实现RadialNumericMenuChildrenItem 第一个和最后一个Item缺掉一个口的效果,如下图

我为这个Path做了一个Clip。问题在于15063版本上面Path的Clip是针对它实际图形位置,而不是Path实际的大小(从之前手绘图看错,实际大小是那个外面的矩形,而且它绘画的图像是小于它的)。所以做了下面的特殊处理

var Build16229 = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", );
//
var Build15063 = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", );
//var Build14393 = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 3);
//var Build10586 = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 2);
bool is15063 = Build15063 && !Build16229;
if (!is15063)
{
if (k == )
{
radialNumericMenuChildrenItem.ColorElement.Clip = new RectangleGeometry() { Rect = new Rect(sectorRect.Width / 2.0, , sectorRect.Width / 2.0, sectorRect.Height) }; }
else
{
radialNumericMenuChildrenItem.ColorElement.Clip = new RectangleGeometry() { Rect = new Rect(, , sectorRect.Width / 2.0, sectorRect.Height) };
}
}
else
{
if (k == )
{
radialNumericMenuChildrenItem.ColorElement.Clip = new RectangleGeometry() { Rect = new Rect((colorElement.EndPoint.X - colorElement.StartPoint.X) / 2.0, , colorElement.EndPoint.X - colorElement.StartPoint.X, colorElement.Size.Height) };
}
else
{
radialNumericMenuChildrenItem.ColorElement.Clip = new RectangleGeometry() { Rect = new Rect(, , (colorElement.EndPoint.X - colorElement.StartPoint.X) / 2.0, colorElement.Size.Height) };
}
}

第三个地方:

RadialNumericMenuItem 的Items之前设计是使用New关键字直接重写了子类的Items(RadialMenuItemCollection),为ObservableCollection<double>

在10586  release 模式上面RadialMenuPanel 的Children.Count会为一个奇怪的负数,而不是0.

修改方案之后 增加了一个ObservableCollection<double> NumericItems 属性来设置数

并且使用 [ContentProperty(Name = "NumericItems")] 属性标签覆盖子类,

但是哇在最新的SDK上面又有问题了。。debug模式下编译能过。但是运行起来ContentProperty 没有生效不通过。

最后也没啥办法了。。那就是在xaml里面老实实 写NumericItems等于啥吧。。( ╯□╰ )

第四个地方

居然是virtual 的Offset 在Page的 NavigationCacheMode 等于enable 或者request的时候。有大大大的问题啊。微软来背锅啊。

问题是这样的。Radilamenu放在一个Page NavigationCacheMode =Enable的页面上。。然后跳转到其他页面再跳转回来的时候。

Virtual.Offset 的还是原来的值。而且整个RadialMenu看起来也像在原来的位置。。但是其实你用鼠标或者触摸的时候。发现RadialMenu触发事件的位置居然是在左上角。就是说是Offset为0的时候那个区域。。不管用了什么办法。还是无解。

最后只能放弃使用Virtual.Offset ,然后改为控制控件Popup的HorizontalOffset和VerticalOffset来移动。

第五个地方:

之前Popup放ControlTemplate里面,这会导致RadilamMenu只是在你放那个父容器那一层是最上层。而一般都需要RadialMenu在整个应用的最上层。

办法只有一个不要将Popup放进去virtual tree,而且是在代码中new 一个出来,这样微软会把这个Popup的Child放进PopupRoot,这个东东是整个应用的最上层,如下图。

PopupRoot为应用的最上层

RootScrollViewer-ScrollContentPresenter-Border-Frame-ContentPresenter 这里就是平时大家熟悉的MainPage

不知道咋哪里查看这个的。请打开VS,运行UWP程序,看到中间那坨黑色的地方吗,点第一个VS的左边就会出现Live Vistual Tree,通过这个你能查看到结构,某个控件的当前属性,或者设置它的属性,反正很叼了。学习控件的孩子一定要搞懂哈。

注意不要将RadilaMenu直接放进Virtual tree,因为RadialMenu是准备放在new 的Popup里面的,大家都知道一个UIElement不能赋值给2个Parent的。

那么告诉大家一个技巧。可以用AttachedProperty将RadialMenu加入到Xaml但是不放进Virtual tree。这样我们就能在Xaml中开心的写了。

如Sample 中RadialMenuBase 类的AttachedMenu属性。

  <radialMenu:RadialMenuBase.AttachedMenu>
<radialMenu:RadialMenu x:Name="radialMenu" Offset="100,100" SectorCount="" IsExpanded="False">
<radialMenu:RadialMenuItem Content="Color" ToolTip="Color">
<radialMenu:RadialColorMenuItem Color="Red"/>
</radialMenu:RadialMenuItem>
<radialMenu:RadialNumericMenuItem x:Name="radialNumericMenuItem" Value="">
<radialMenu:RadialNumericMenuItem.Content>
<TextBlock>
<Run Text="Fontsize"/>
<Run Text="("/>
<Run Text="{Binding Value,ElementName=radialNumericMenuItem}"/>
<Run Text=")"/>
</TextBlock>
</radialMenu:RadialNumericMenuItem.Content>
<radialMenu:RadialNumericMenuItem.NumericItems>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
<x:Double></x:Double>
</radialMenu:RadialNumericMenuItem.NumericItems>
</radialMenu:RadialNumericMenuItem>
<radialMenu:RadialMenuItem Content="Disabled" IsEnabled="True">
<radialMenu:RadialMenuItem Content="test"/>
</radialMenu:RadialMenuItem>
</radialMenu:RadialMenu>
</radialMenu:RadialMenuBase.AttachedMenu>

最后最后,这个控件里面用到一些属性标签 比如

[ContentProperty(Name = "NumericItems")]

[EditorBrowsable(EditorBrowsableState.Never)]
[Browsable(false)]

这些东西跟Design Time有很大大的关系,就是你平时写在Xaml里面的东东。要是有不清楚的。可以留言或者私信我,欢迎投鸡蛋。

最后想说。要做好一个控件这些是远远不够的,还会涉及控件的 designtime,各种逻辑,各种版本适配,各种扩展,一个好的控件绝对等同一个复杂的项目。我喜欢做控件。希望给大家使用降低学习成本。

谢谢坚持看完的童鞋,整个控件研发的过程远远不止这篇文章,欢迎提问。

老规矩 开源有益:RadialMenu

想在10240上面使用这个控件的童鞋,可以把关于Composition API的代码都注释掉。使用StoryBoard来做动画就ok了。(是不是感觉我强行为Composition API 打Call,哈哈哈)

UWP Composition API - RadialMenu的更多相关文章

  1. UWP Composition API - 锁定列的FlexGrid

    需求是第一列锁定,那么怎么让锁定列不跟着滚动条向做移动呢? 其实很简单,让锁定列跟scrollviewer的滚动做反方向移动. 先看一下这个控件的模板,嗯,其实很简单,就是ListView的模板,不同 ...

  2. UWP Composition API - GroupListView(二)

    还是先上效果图: 看完了上一篇UWP Composition API - GroupListView(一)的童鞋会问,这不是跟上一篇一样的吗??? 骗点击的?? No,No,其实相对上一个有更简单粗暴 ...

  3. UWP Composition API - GroupListView(一)

    需求: 光看标题大家肯定不知道是什么东西,先上效果图: 这不就是ListView的Group效果吗?? 看上去是的.但是请听完需求.1.Group中的集合需要支持增量加载ISupportIncreme ...

  4. UWP Composition API - New FlexGrid 锁定行列

    如果之前看了 UWP Jenkins + NuGet + MSBuild 手把手教你做自动UWP Build 和 App store包 这篇的童鞋,针对VS2017,需要对应更新一下配置,需要的童鞋点 ...

  5. UWP Composition API - PullToRefresh

    背景: 之前用ScrollViewer 来做过 PullToRefresh的控件,在项目一些特殊的条件下总有一些问题,比如ScrollViewer不会及时到达指定位置.于是便有了使用Compositi ...

  6. UWP中使用Composition API实现吸顶(1)

    前几天需要在UWP中实现吸顶,就在网上找了一些文章: 吸顶大法 -- UWP中的工具栏吸顶的实现方式之一 在UWP中页面滑动导航栏置顶 发现前人的实现方式大多是控制ListViewBase的Heade ...

  7. win10 UWP 等级控件Building a UWP Rating Control using XAML and the Composition API | XAML Brewer, by Diederik Krols

    原文:Building a UWP Rating Control using XAML and the Composition API | XAML Brewer, by Diederik Krols ...

  8. [UWP小白日记-12]使用新的Composition API来实现控件的阴影

    前言 看了好久官方的Windows UI Dev Labs示例好久才有点心得,真是头大.(其实是英语幼儿园水平(⊙﹏⊙)b) 真的网上关于这个API的资料可以说几乎没有. 正文 首先用这东西的添加WI ...

  9. Windows Composition API 指南 - 认识 Composition API

    微软在 Windows 10中 面向通用 Windows 应用 (Universal Windows Apps, UWA) 新引入了一套用于用户界面合成的 API:Composition API.Co ...

随机推荐

  1. Coin Change (IV) (dfs)

    Coin Change (IV) Time Limit: 1000MS   Memory Limit: 32768KB   64bit IO Format: %lld & %llu [Subm ...

  2. S2_SQL_第一章

    第一章:数据库的设计 1.1:为什么需要规范数据库的设计 1.1.1:什么是数据库设计 数据库设计就是将数据中的数据实体及这些数据实体之间的关系,进行规范和结构的过程. 1.1.2:数据库设计非常重要 ...

  3. Centos7搭建swarm集群

    1. 准备 两台虚拟机,IP分别为: 192.168.1.104 192.168.1.105 保证能互相 ping 通 2. 修改虚拟机的 host,分别任 c1.c2 在 192.168.1.105 ...

  4. 简单Elixir游戏服务器-安装Elixir

    用WebInstaller 安装半天也没下载成功文件. 改成直接下载erlang 和 elixir 预编译包了. 安装很简单,最后设置好环境变量. cmd 执行 elixir -v 最后顺便下载了个g ...

  5. Vue 爬坑之路(六)—— 使用 Vuex + axios 发送请求

    Vue 原本有一个官方推荐的 ajax 插件 vue-resource,但是自从 Vue 更新到 2.0 之后,官方就不再更新 vue-resource 目前主流的 Vue 项目,都选择 axios ...

  6. 简单的CSS颜色查看工具

    可以通过输入ARGB(A代表透明度)格式或者HEX格式查看颜色,也可以进行ARGB格式和者HEX格式转换,如下图 使用C#编写,我已将源代码压缩上传 下载地址:http://files.cnblogs ...

  7. Django内置的通用类视图

    1.ListView 表示对象列表的一个页面. 执行这个视图的时候,self.object_list将包含视图正在操作的对象列表(通常是一个查询集,但不是必须). 属性: model: 指定模型 te ...

  8. javaScript基础的基础

    JavaScript是一个脚本语言,需要有宿主文件,他的宿主文件是HTML文件. 与JAVA没有直接关系 一般写在 1.head里面 2.body里面 3.</html>后面 一般写在&l ...

  9. 服务器cpu100%问题分析

    ecs 130 : slb:

  10. vue-cli如何引入bootstrap工具

    以下操作以正常安装node环境为前提. 1.引入jq: 在npm控制台中,进入项目目录,然后输入指令npm install jquery --save-dev(npm换成cnpm更好,国内环境下使用c ...