WinRT自定义控件第一 - 转盘按钮控件
之前的文章中,介绍了用WPF做一个转盘按钮控件,后来需要把这个控件移植到WinRT时,遇到了很大的问题,主要原因在于WPF和WinRT还是有很大不同的。这篇文章介绍了这个移植过程,由于2次实现的控件功能完全一样,文章主要关注点放在WPF与WinRT的不同上。
定义控件模板的XAML文件
在WinRT上的实现和WPF中实现一个很大的不同是,这个实现的TemplatedControl没有从ItemsControl继承,而是由Control继承手动添加了一些对集合属性的支持。不从ItemsControl继承的原因是WinRT中无法由ItemsPresenter继承,从而不能继承一个自己的ItemsPresenter并添加属性供自定义的Panel绑定。所以在WinRT的实现中直接在空间里放了一个Panel来对集合属性的元素进行布局,Panel的属性可以直接绑定到控件的依赖属性。
首先来展示一下控件模板的基本结构:
基本上分为四部分:定义状态,圆形透明背景,显示一圈小按钮的Panel以及中间的大按钮。WinRT中ZIndex附加属性被定义在Canvas上,而不像WPF中被定义在Panel上,所以WinRT的实现中不是用ZIndex附加属性实现元素的前后关系,而是通过元素在XAML中出现的顺序来保证这个关系。这是一个不小的限制,但暂时没找到什么好方法。
大按钮和圆形透明背景依然很简单:
- <Ellipse Width="{TemplateBinding ShadowSize}" Height="{TemplateBinding ShadowSize}" Fill="#66559977"></Ellipse>
- <Border x:Name="PART_CenterBtn" VerticalAlignment="Center" HorizontalAlignment="Center"
- CornerRadius="15"
- Width="{TemplateBinding CenterSize}" Height="{TemplateBinding CenterSize}"
- Tag="Main"
- BorderThickness="{TemplateBinding BorderThickness}"
- BorderBrush="{TemplateBinding BorderBrush}"
- Background="{TemplateBinding Background}">
- <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center"
- Foreground="{TemplateBinding Foreground}"
- Text="{TemplateBinding CenterMenu}">
- </TextBlock>
- </Border>
这里一个很困惑的问题是中间按钮那个Border的CornerRadius始终无法绑定到控件的依赖属性。在网上找了半天,看了很多例子,都没发现有这个限制。但我这里用只要是绑定CornerRadius就变成被设置为0的效果,实在是诡异。这个问题导致目前的实现只能将CornerRadius设置为显式的固定值,从而不能通过控件的属性灵活改变中央按钮的大小。希望高手们能帮忙看看文章最后的源程序。
几个状态的定义和之前差不多,子菜单正是根据不同的状态来在收缩和展开模型来回切换。较之WPF中实现的改变就是其不再是通过ItemsPresenter间接控制Panel,而是直接控制控件中定义的Panel:
- <VisualStateManager.VisualStateGroups>
- <VisualStateGroup x:Name="CommonStates">
- <VisualState x:Name="Initial">
- <Storyboard >
- <ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_Panel"
- Storyboard.TargetProperty="PanelStatus">
- <DiscreteObjectKeyFrame KeyTime="0" Value="Initial" />
- </ObjectAnimationUsingKeyFrames>
- </Storyboard>
- </VisualState>
- <VisualState x:Name="Collapsed">
- <Storyboard >
- <ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_Panel"
- Storyboard.TargetProperty="PanelStatus">
- <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
- </ObjectAnimationUsingKeyFrames>
- </Storyboard>
- </VisualState>
- <VisualState x:Name="Expanded">
- <Storyboard >
- <ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_Panel"
- Storyboard.TargetProperty="PanelStatus">
- <DiscreteObjectKeyFrame KeyTime="0" Value="Expanded" />
- </ObjectAnimationUsingKeyFrames>
- </Storyboard>
- </VisualState>
- </VisualStateGroup>
- </VisualStateManager.VisualStateGroups>
上面代码中Storyboard控制的Panel也就是控件中用于容纳并展示子菜单的Panel定义如下,可以看到这个自定义的Panel中的很多依赖属性直接绑定到控件的依赖属性,还是很方便的。
- <circleMenuControl:CircleMenuPanel x:Name="PART_Panel" PanelStatus="Initial"
- AnimationDuration="{TemplateBinding CircleDuration}"
- AnimationDurationStep="{TemplateBinding CircleDurationStep}"
- Radius="{TemplateBinding ShadowRadius}" Angle="360" >
- </circleMenuControl:CircleMenuPanel>
除了Control Template,xaml文件中还定义了一些默认值和子菜单的Button控件默认的模板:
- <Setter Property="BorderThickness" Value="0" ></Setter>
- <Setter Property="Background" Value="CadetBlue"></Setter>
- <Setter Property="SubMenuStyle">
- <Setter.Value>
- <Style TargetType="Button">
- <Setter Property="Template">
- <Setter.Value>
- <ControlTemplate TargetType="Button">
- <!-- width&height: subRadius -->
- <Border CornerRadius="15" Background="Coral" Width="30" Height="30" >
- <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
- </Border>
- </ControlTemplate>
- </Setter.Value>
- </Setter>
- </Style>
- </Setter.Value>
- </Setter>
当然这些都是可以由控件使用者自行替换的。
自定义Panel
上面用到的Panel依然是控件的关键,这个Panel和WPF中的实现“大”同“小”异。说大同是因为其中的依赖属性,和所需要重写的方法与WPF中的实现几乎完全一样。说小异是因为由于WinRT对动画的支持有所改变,在重写的方法中用WinRT支持的方式重新实现了动画。这里着重说一下这个。在WPF中我定义好一个Transform,然后通过transform的BeginAnimation方法开启一段动画,而在WinRT中transform不再支持这种方式,实现动画只能通过Storyboard(不管实在XAML标记还是在C# Code,Storyboard成了控制xaml动画的不二选择)。所以在WinRT中我们只能先定义好Transform,然后让StoryBoard控制Transform的属性值来实现一段动画,下面给出Panel中一小段代码为例子:
- private void ArrangeExpandElement(int idx, UIElement element,
- double panelCenterX, double panelCenterY,
- double elementCenterX, double elementCenterY,
- double destX, double destY)
- {
- element.Arrange(new Rect(panelCenterX, panelCenterY, element.DesiredSize.Width, element.DesiredSize.Height));
- var transGroup = element.RenderTransform as TransformGroup;
- Transform translateTransform, rotateTransform;
- if (transGroup == null)
- {
- element.RenderTransform = transGroup = new TransformGroup();
- translateTransform = new TranslateTransform();
- rotateTransform = new RotateTransform() { CenterX = elementCenterX, CenterY = elementCenterY };
- transGroup.Children.Add(translateTransform);
- transGroup.Children.Add(rotateTransform);
- }
- else
- {
- translateTransform = transGroup.Children[0] as TranslateTransform;
- rotateTransform = transGroup.Children[1] as RotateTransform;
- }
- element.RenderTransformOrigin = new Point(0.5, 0.5);
- //if (i != 0) continue;
- var aniDuration = AnimationDuration + TimeSpan.FromSeconds(AnimationDurationStep * idx);
- var storySpin = new Storyboard();
- var translateXAnimation = new DoubleAnimation() { From = 0, To = destX - panelCenterX, Duration = aniDuration };
- var translateYAnimation = new DoubleAnimation() { From = 0, To = destY - panelCenterY, Duration = aniDuration };
- var transparentAnimation = new DoubleAnimation() { From = 0, To = 1, Duration = aniDuration };
- var rotateXAnimation = new DoubleAnimation() { From = 0, To = destX - panelCenterX, Duration = aniDuration };
- var rotateYAnimation = new DoubleAnimation() { From = 0, To = destY - panelCenterY, Duration = aniDuration };
- var rotateAngleAnimation = new DoubleAnimation() { From = 0, To = 720, Duration = aniDuration };
- storySpin.Children.Add(translateXAnimation);
- storySpin.Children.Add(translateYAnimation);
- storySpin.Children.Add(transparentAnimation);
- storySpin.Children.Add(rotateXAnimation);
- storySpin.Children.Add(rotateYAnimation);
- storySpin.Children.Add(rotateAngleAnimation);
- Storyboard.SetTargetProperty(translateXAnimation, "(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.X)");
- Storyboard.SetTargetProperty(translateYAnimation, "(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.Y)");
- Storyboard.SetTargetProperty(transparentAnimation, "UIElement.Opacity");
- Storyboard.SetTargetProperty(rotateXAnimation, "(UIElement.RenderTransform).(TransformGroup.Children)[1].(RotateTransform.CenterX)");
- Storyboard.SetTargetProperty(rotateYAnimation, "(UIElement.RenderTransform).(TransformGroup.Children)[1].(RotateTransform.CenterY)");
- Storyboard.SetTargetProperty(rotateAngleAnimation, "(UIElement.RenderTransform).(TransformGroup.Children)[1].(RotateTransform.Angle)");
- Storyboard.SetTarget(translateXAnimation, element);
- Storyboard.SetTarget(translateYAnimation, element);
- Storyboard.SetTarget(transparentAnimation, element);
- Storyboard.SetTarget(rotateXAnimation, element);
- Storyboard.SetTarget(rotateYAnimation, element);
- Storyboard.SetTarget(rotateAngleAnimation, element);
- storySpin.Begin();
- }
这里比较值得注意是StoryBoard所用做用到的用字符串表示的属性。这个也是博主尝试了很久才找到的正确写法。
控件代码
下面依次来看下WinRT中控件实现与WPF的不同之处。第一个比较明显的不同就是设置控件与XAML模板关联的代码,WinRT中变得很简洁:
- public CircleMenu()
- {
- DefaultStyleKey = typeof(CircleMenu);
- }
最主要的变化还在于WinRT中没有从ItemsControl继承,我们要自行完成集合元素的处理。首先一个依赖属性是必不可少的:
- public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
- "ItemsSource",
- typeof(IEnumerable),
- typeof(CircleMenu), new PropertyMetadata(null));
- public IEnumerable ItemsSource
- {
- get { return (IEnumerable)GetValue(ItemsSourceProperty); }
- set { SetValue(ItemsSourceProperty, value); }
- }
这个属性的声明完全仿照ItemsControl中声明的样子。下面这个重要的方法用于根据ItemsSource中的实体来创建Panel中的元素,ItemsControl控件很大程度上也就是完成了这个工作。
- private void SetSubMenu()
- {
- CircleMenuPanel.Children.Clear();
- foreach (var item in ItemsSource)
- {
- var menuItem = item as CircleMenuItem;
- if (menuItem != null)
- {
- var btn = new Button();
- btn.Opacity = 0;
- var bindTag = new Binding
- {
- Path = new PropertyPath("Id"),
- Source = menuItem,
- Mode = BindingMode.OneWay
- };
- btn.SetBinding(TagProperty, bindTag);//用Tag存储Id
- var textBlock = new TextBlock();
- var bindTitle = new Binding
- {
- Path = new PropertyPath("Title"),
- Source = menuItem,
- Mode = BindingMode.OneWay
- };
- textBlock.SetBinding(TextBlock.TextProperty,bindTitle);
- btn.Content = textBlock;
- var binding = new Binding()
- {
- Path = new PropertyPath("SubMenuStyle"),
- RelativeSource = new RelativeSource() { Mode = RelativeSourceMode.TemplatedParent },
- Source = this
- };
- btn.SetBinding(StyleProperty, binding);
- btn.Click += (s, e) =>
- {
- VisualStateManager.GoToState(this, VisualStateCollapsed, false);
- if (SubClickCommand != null)
- {
- var sbtn = s as Button;
- if (sbtn != null)
- SubClickCommand(Convert.ToInt32(sbtn.Tag));
- }
- SetSubMenu();
- VisualStateManager.GoToState(this, VisualStateExpanded, false);
- };
- CircleMenuPanel.Children.Add(btn);
- }
- }
- }
在OnApplyTemplate中调用上面这个方法,完成Panel中元素的添加。上面代码中通过Binding的方式将Button的依赖属性与ItemsControl中实体相关联。如果是通过XAML实现这个Button,一般也是这么做。这样ItemsSource中Item的内容变化会反映在Button上。另外这个Button子元素还支持使用者通过XAML来定义其Control Template。实现这个功能的几个关键点:
在控件上添加声明:
- [StyleTypedProperty(Property = "SubMenuStyle", StyleTargetType = typeof(Button))]
表示有一个目标为Button类型的Style类型的属性,这个依赖属性声明如下:
- public static readonly DependencyProperty SubMenuStyleProperty = DependencyProperty.Register(
- "SubMenuStyle", typeof(Style), typeof(CircleMenu), null);
- public Style SubMenuStyle
- {
- get { return (Style)GetValue(SubMenuStyleProperty); }
- set { SetValue(SubMenuStyleProperty, value); }
- }
然后就是将这个属性与Button相关联:
- var binding = new Binding()
- {
- Path = new PropertyPath("SubMenuStyle"),
- RelativeSource = new RelativeSource() { Mode = RelativeSourceMode.TemplatedParent },
- Source = this
- };
- btn.SetBinding(StyleProperty, binding);
这样就实现了像ItemsControl中通过ItemTemplate给Item设置模板那样可以灵活的通过控件给子元素指定模板的功能。
另外由于WinRT中不支持路由属性,实现点击子菜单触发一个自定义操作的方式也有变化。新的方式就是在控件上定义一个Action<T>类型的依赖属性来表示一个事件处理函数。然后在控件内部元素的事件处理函数中调用这个依赖属性表示的操作:
- btn.Click += (s, e) =>
- {
- VisualStateManager.GoToState(this, VisualStateCollapsed, false);
- if (SubClickCommand != null)
- {
- var sbtn = s as Button;
- if (sbtn != null)
- SubClickCommand(Convert.ToInt32(sbtn.Tag));//调用依赖属性表示的事件处理函数,这前后可以添加一些通用的处理操作。
- }
- SetSubMenu();
- VisualStateManager.GoToState(this, VisualStateExpanded, false);
- };
可以根据需要把这个依赖属性定义为Action<T>、Action<T,T>甚至是Action<T,T,T>以传入所需的参数。其使用可以在下文看到。
关于这个自定义事件的处理,暂时只找到了上述方法,如果您有更好的方法请不吝赐教。
控件使用
控件使用和ItemsControl下的方式稍有区别:
- <circleMenu:CircleMenu ItemsSource="{Binding SubMenuItems}" Foreground="FloralWhite" CenterMenu="济南"
- Width="100" Height="100" CenterSize="30" CenterRadius="15"
- ShadowSize="80" ShadowRadius="40" SubClickCommand="{Binding SubMenuClickCommand}">
- <circleMenu:CircleMenu.SubMenuStyle>
- <Style TargetType="Button">
- <Setter Property="Template">
- <Setter.Value>
- <ControlTemplate TargetType="Button">
- <Border CornerRadius="15" Background="Coral" Width="30" Height="30" >
- <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
- </Border>
- </ControlTemplate>
- </Setter.Value>
- </Setter>
- </Style>
- </circleMenu:CircleMenu.SubMenuStyle>
- </circleMenu:CircleMenu>
通过用内容属性的方式来定义子元素的样式(模板),而不是像ItemsControl中设置ItemTemplate。另外可以看到SubClickCommand这个Action<T>绑定到一个对象,其定义如下(位于ViewModel中):
- public Action<int> SubMenuClickCommand
- {
- get
- {
- return subMenuId =>
- {
- _dialogService.ShowMessage(subMenuId.ToString(), "子菜单编号");
- };
- }
- }
其基本与WPF版本中的RelayCommand作用等价。
最后放出最终效果图,喜欢的可以下载源码看一下:
源码下载
版权说明:本文版权归博客园和hystar所有,转载请保留本文地址。文章代码可以在项目随意使用,如果以文章出版物形式出现请表明来源
WinRT自定义控件第一 - 转盘按钮控件的更多相关文章
- WPF自定义控件第二 - 转盘按钮控件
继之前那个控件,又做了一个原理差不多的控件.这个控件主要模仿百度贴吧WP版帖子浏览界面左下角那个弹出的按钮盘.希望对大家有帮助. 这个控件和之前的也差不多,为了不让大家白看,文章最后发干货. 由于这个 ...
- Qt编写自定义控件9-导航按钮控件
前言 导航按钮控件,主要用于各种漂亮精美的导航条,我们经常在web中看到导航条都非常精美,都是html+css+js实现的,还自带动画过度效果,Qt提供的qss其实也是无敌的,支持基本上所有的CSS2 ...
- WPF自定义控件第一 - 进度条控件
本文主要针对WPF新手,高手可以直接忽略,更希望高手们能给出一些更好的实现思路. 前期一个小任务需要实现一个类似含步骤进度条的控件.虽然对于XAML的了解还不是足够深入,还是摸索着做了一个.这篇文章介 ...
- Qt编写自定义控件11-设备防区按钮控件
前言 在很多项目应用中,需要根据数据动态生成对象显示在地图上,比如地图标注,同时还需要可拖动对象到指定位置显示,能有多种状态指示,安防领域一般用来表示防区或者设备,可以直接显示防区号,有多种状态颜色指 ...
- Android自定义控件之自定义组合控件
前言: 前两篇介绍了自定义控件的基础原理Android自定义控件之基本原理(一).自定义属性Android自定义控件之自定义属性(二).今天重点介绍一下如何通过自定义组合控件来提高布局的复用,降低开发 ...
- 基于C#开发的扩展按钮控件
最近在准备一套自定义控件开发的课程,下面将第一个做的按钮控件分享给大家. 其实这个控件属于自定义控件中的扩展控件,与组合控件和GDI+开发的控件不同,这个控件是继承原生的Button, 这个控件的目的 ...
- C# 使用PictureBox实现图片按钮控件
引言 我们有时候会在程序的文件夹里看见一些图标,而这些图标恰好是作为按钮的背景图片来使用的.鼠标指针在处于不同状态时,有"进入按钮"."按下左键"," ...
- MFC编程入门之二十二(常用控件:按钮控件Button、Radio Button和Check Box)
本节继续讲解常用控件--按钮控件的使用. 按钮控件简介 按钮控件包括命令按钮(Button).单选按钮(Radio Button)和复选框(Check Box)等.命令按钮就是我们前面多次提到的侠义的 ...
- MFC动态创建对话框中的按钮控件并创建其响应消息
转自:http://www.cnblogs.com/huhu0013/p/4626686.html 动态按钮(多个)的创建: 1.在类中声明并定义按钮控件的ID #define IDC_D_BTN 1 ...
随机推荐
- MVVM设计模式和WPF中的实现(四)事件绑定
MVVM设计模式和在WPF中的实现(四) 事件绑定 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在WPF中 ...
- 谱聚类(spectral clustering)原理总结
谱聚类(spectral clustering)是广泛使用的聚类算法,比起传统的K-Means算法,谱聚类对数据分布的适应性更强,聚类效果也很优秀,同时聚类的计算量也小很多,更加难能可贵的是实现起来也 ...
- 来吧,HTML5之一些注意事项
1.说什么是HTML HTML是一种超文本标记语言(Hyper Text Markup Language), 标记语言是一套标记标签(markup tag),用来描述网页的非编程语言. 2.标签特性: ...
- 来吧,HTML5之基础标签(上)
什么是html5 HTML 5 是下一代的 HTML.HTML5 仍处于完善之中.然而,大部分现代浏览器已经具备了某些 HTML5 支持. 学习过程中标签的理解 <a>标签 定义超链接, ...
- PHP设计模式(七)适配器模式(Adapter For PHP)
适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作. 如下图(借图): // 设置书的接口 // 书接口 interface BookI ...
- PHP设计模式(五)建造者模式(Builder For PHP)
建造者模式:将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示的设计模式. 设计场景: 有一个用户的UserInfo类,创建这个类,需要创建用户的姓名,年龄,爱好等信息,才能获得用 ...
- “老坛泡新菜”:SOD MVVM框架,让WinForms焕发新春
火热的MVVM框架 最近几年最热门的技术之一就是前端技术了,各种前端框架,前端标准和前端设计风格层出不穷,而在众多前端框架中具有MVC,MVVM功能的框架成为耀眼新星,比如GitHub关注度很高的Vu ...
- iOS -- CocoaPods
CocoaPods 是什么? CocoaPods 是一个负责管理 iOS 项目中第三方开源库的工具.CocoaPods 的项目源码在 GitHub( https://github.com/CocoaP ...
- VisualStudio 2015 开启IIS Express可以调试X64项目
现在项目开发时总有时需要在X64下开发,这样我们就需要IIS Express中调试.不要总是放在IIS中,在Attach这样好慢. 如果不设置直接调试X64的程序,我们有可能会受到以下类似的错误 ...
- 递归实现n(经典的8皇后问题)皇后的问题
问题描述:八皇后问题是一个以国际象棋为背景的问题:如何能够在8×8的国际象棋棋盘上放置八个皇后, 使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行.纵行或斜线上 ...