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作用等价。

最后放出最终效果图,喜欢的可以下载源码看一下:

源码下载

GitHub

版权说明:本文版权归博客园和hystar所有,转载请保留本文地址。文章代码可以在项目随意使用,如果以文章出版物形式出现请表明来源

时间: 2024-10-11 20:52:59

WinRT自定义控件第一 - 转盘按钮控件的相关文章

WPF自定义控件第一 - 进度条控件

本文主要针对WPF新手,高手可以直接忽略,更希望高手们能给出一些更好的实现思路. 前期一个小任务需要实现一个类似含步骤进度条的控件.虽然对于XAML的了解还不是足够深入,还是摸索着做了一个.这篇文章介绍下实现这个控件的步骤,最后会放出代码.还请高手们给出更好的思路.同时也希望这里的思路能给同道中人一些帮助.话不多说,开始正题. 实现中的一些代码采用了网上现有的方案,代码中通过注释标记了来源,再次对代码作者一并表示感谢. 首先放一张最终效果图. 节点可以被点击 控件会根据绑定的集合数据生成一系列节

android自定义控件(五) 自定义组合控件

转自http://www.cnblogs.com/hdjjun/archive/2011/10/12/2209467.html 代码为自己编写 目标:实现textview和ImageButton组合,可以通过Xml设置自定义控件的属性. 通过代码或者通过xml设置自定义控件的属性 1.控件布局:以Linearlayout为根布局,一个TextView,一个ImageButton.  Xml代码 [html] view plaincopy < ?xml version="1.0" 

(转载)VS2010/MFC编程入门之二十二(常用控件:按钮控件Button、Radio Button和Check Box)

因为私人问题,鸡啄米暂停更新了几天,首先向关注鸡啄米动态的朋友说一声抱歉. 言归正传,鸡啄米上一节中讲了编辑框的用法,本节继续讲解常用控件--按钮控件的使用. 按钮控件简介 按钮控件包括命令按钮(Button).单选按钮(Radio Button)和复选框(Check Box)等.命令按钮就是我们前面多次提到的狭义的按钮控件,用来响应用户的鼠标单击操作,进行相应的处理,它可以显示文本也可以嵌入位图.单选按钮使用时,一般是多个组成一组,组中每个单选按钮的选中状态具有互斥关系,即同组的单选按钮只能有

第三章 按钮控件的创建

一.前言 不知不觉一晃两个月过去,说来惭愧,在此期间alterto一直没有再研究DuiEngine.主要是因为DuiEngine的作者现在构建一个新的界面库soui,而笔者也一直处于观望状态,因为DuiEngine的作者说了以后可能就不维护DuiEngine了,要把主要的经历放在SOUI上.alterto顿时犹豫了,既然这样我还为DuiEngine做什么教程啊.于是这两个月的时间里一直都很犹豫,也没有再出DuiEngine相关的教程.现在看来这种焦躁对于一个优秀的程序猿来说着实不应该,这也正暴露

MFC中按钮控件的用法笔记(转)

VC学习笔记1:按钮的使能与禁止 用ClassWizard的Member Variables为按钮定义变量,如:m_Button1:则m_Button1.EnableWindow(true); 使按钮处于允许状态m_Button1.EnableWindow(false); 使按钮被禁止,并变灰显示 VC学习笔记2:控件的隐藏与显示 用CWnd类的函数BOOL ShowWindow(int nCmdShow)可以隐藏或显示一个控件. 例1:CWnd *pWnd;pWnd = GetDlgItem(

VS2010-MFC(常用控件:按钮控件Button、Radio Button和Check Box)

转自:http://www.jizhuomi.com/software/182.html 按钮控件简介 按钮控件包括命令按钮(Button).单选按钮(Radio Button)和复选框(Check Box)等.命令按钮就是我们前面多次提到的狭义的按钮控件,用来响应用户的鼠标单击操作,进行相应的处理,它可以显示文本也可以嵌入位图.单选按钮使用时,一般是多个组成一组,组中每个单选按钮的选中状态具有互斥关系,即同组的单选按钮只能有一个被选中. 命令按钮是我们最熟悉也是最常用的一种按钮控件,而单选按钮

自定义水晶按钮控件

namespace 自定义水晶按钮控件 { partial class Form1 { /// <summary> /// 必需的设计器变量. /// </summary> private System.ComponentModel.IContainer components = null; /// <summary> /// 清理所有正在使用的资源. /// </summary> /// <param name="disposing&quo

安卓开发_复选按钮控件(CheckBox)的简单使用

复选按钮 即可以选择若干个选项,与单选按钮不同的是,复选按钮的图标是方块,单选按钮是圆圈 复选按钮用CheckBox表示,CheckBox是Button的子类,支持使用Button的所有属性 一.由于复选框可以选中多项,所有为了确定用户是否选择了某一项,还需要为每一个选项添加setOnCheckedChangeListener事件监听 例如: 为id为like1的复选按钮添加状态改变事件监听,代码如下 1 final CheckBox like1 = (CheckBox)findViewById

ToggleButton开关状态按钮控件

ToggleButton开关状态按钮控件 一.简介 1. 2.ToggleButton类结构 父类是CompoundButton,引包的时候注意下 二.ToggleButton开关状态按钮控件使用方法 1.新建ToggleButton控件及对象 private ToggleButton toggleButton1; toggleButton1=(ToggleButton) findViewById(R.id.toggleButton1); 2.设置setOnCheckedChangeListen