[UWP]实现Picker控件

2018-02-07 11:33:05来源:cnblogs.com作者:dino.c人点击

分享

1. 前言

在WPF中,很多打开下拉框(Popup或Flyout)选择一个结果值的控件,除了ComboBox等少数例外,这种控件都以-Picker做名称后缀。因为要打开关闭下拉框和计算下拉框的弹出位置, 这类控件实现起来还挺麻烦的。Silverlight Toolkit中贴心地提供了一个Picker控件,可以作为这类控件的基类,省略了大量代码。

2. 现在的问题

由于UWP中有Flyout,-Picker控件的实现其实算是相当轻松的。如ColorPicker的官方文档就介绍了使用Flyout承载ColorPicker的实现代码。但是做起来还是有一些问题:

  1. 在有“确定/取消”按钮的Flyout中,即使选择了值,如果没有点击“确定”按钮也不更新结果值。
  2. 在Flyout打开的状态,还是希望它所属的按钮有某种已被按下的状态显示,典型的如ComboBox、Extended WPF Toolkit的ColorPicker、WinForm的DateTimePicker,或其它大部分Windows的控件那样:

上面第一点是硬性要求,所有-Picker类控件都会实现这点(偶尔也见到没做好的)。第二点就比较麻烦了,UWP几乎完全没有理会这点。其实WPF/Silverlight时代即已经开始忽略这点UI需求了,但我还是希望可以注意这些UI的细节,毕竟UWP就经常被诟病UI细节缺失。

3. 我的解决方案

于是我决定实现一个UWP的Picker类。

3.1 定义外观

SilverlightToolkit的Picker相当复杂,UI有三个VisualStateGroup,两个TemplatPart:

[TemplateVisualState(GroupName = "PopupStates", Name = "PopupClosed")][TemplatePart(Name = "DropDownToggle", Type = typeof (ToggleButton))][TemplateVisualState(GroupName = "PopupStates", Name = "PopupOpened")][TemplateVisualState(GroupName = "CommonStates", Name = "Normal")][TemplateVisualState(GroupName = "CommonStates", Name = "MouseOver")][TemplateVisualState(GroupName = "CommonStates", Name = "Pressed")][TemplateVisualState(GroupName = "CommonStates", Name = "Disabled")][TemplateVisualState(GroupName = "FocusStates", Name = "Focused")][TemplateVisualState(GroupName = "FocusStates", Name = "Unfocused")][TemplatePart(Name = "Popup", Type = typeof (Popup))]public abstract class Picker : Control, IUpdateVisualState

我不想做到这么复杂,只有一个名为PopupStatesName的VisualStateGroup(沿用Silverlight Toolkit的命名),一个Flyout,一组“确定/取消”按钮就够了。

[TemplateVisualState(Name = PopupClosedName, GroupName = PopupStatesName)][TemplateVisualState(Name = PopupOpenedName, GroupName = PopupStatesName)][TemplatePart(Name = AcceptButtonName, Type = typeof(Button))][TemplatePart(Name = DismissButtonName, Type = typeof(Button))][TemplatePart(Name = FlyoutName, Type = typeof(FlyoutBase))]public abstract class Picker : HeaderedContentControl{    private const string PopupClosedName = "PopupClosed";    private const string PopupOpenedName = "PopupOpened";    private const string PopupStatesName = "PopupStates";    private const string FlyoutName = "Flyout";    private const string AcceptButtonName = "AcceptButton";    private const string DismissButtonName = "DismissButton";

此为下文中MyDatePicker的运行效果。

3.2 IsOpen属性

Picker中提供一个bool IsDropDownOpen属性,用于控制下拉框是否打开。无论是AcceptButton或DismissButton的点击事件,或者Flyout的Closed事件都会调用这个属性。

protected virtual void OnIsDropDownOpenChanged(bool oldValue, bool newValue){    if (_flyout == null)        return;    if (newValue)        _flyout.ShowAt(this);    else        _flyout.Hide();}protected virtual void OnFlyoutClosed(object e){    IsDropDownOpen = false;}protected virtual void OnAccept(RoutedEventArgs e){    IsDropDownOpen = false;}protected virtual void OnDismiss(RoutedEventArgs e){    IsDropDownOpen = false;}protected override void UpdateVisualState(bool useTransitions){    base.UpdateVisualState(useTransitions);    VisualStateManager.GoToState(this, IsDropDownOpen ? PopupOpenedName : PopupClosedName, useTransitions);}

3.3 实际应用:实现一个MyDatePicker

需要实现一个MyDatePicker,可以继承Picker,Default Style如下,不少内容都是参考DatePicker:

<Style TargetType="local:MyDatePicker">    <Setter Property="IsTabStop"            Value="False" />    <Setter Property="FontFamily"            Value="{ThemeResource ContentControlThemeFontFamily}" />    <Setter Property="FontSize"            Value="{ThemeResource ControlContentThemeFontSize}" />    <Setter Property="Template">        <Setter.Value>            <ControlTemplate TargetType="local:MyDatePicker">                <StackPanel x:Name="LayoutRoot"                            Margin="{TemplateBinding Padding}">                    <local:HeaderedContentControl Header="{TemplateBinding Header}"                                                  HeaderTemplate="{TemplateBinding HeaderTemplate}">                        <ToggleButton x:Name="DateButton"                                      Content="{TemplateBinding DateTime}"                                      IsEnabled="{TemplateBinding IsEnabled}"                                      IsChecked="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsDropDownOpen,Mode=TwoWay}"                                      HorizontalAlignment="Stretch"                                      HorizontalContentAlignment="Stretch">                            <FlyoutBase.AttachedFlyout>                                <Flyout Placement="Bottom"                                        x:Name="Flyout">                                    <Flyout.FlyoutPresenterStyle>                                        <Style TargetType="FlyoutPresenter">                                            <Setter Property="Padding"                                                    Value="0" />                                            <Setter Property="BorderThickness"                                                    Value="0" />                                            <Setter Property="Template">                                                <Setter.Value>                                                    <ControlTemplate TargetType="FlyoutPresenter">                                                        <ContentPresenter Background="{TemplateBinding Background}"                                                                          BorderBrush="{TemplateBinding BorderBrush}"                                                                          BorderThickness="{TemplateBinding BorderThickness}"                                                                          Content="{TemplateBinding Content}"                                                                          ContentTemplate="{TemplateBinding ContentTemplate}"                                                                          ContentTransitions="{TemplateBinding ContentTransitions}"                                                                          Margin="{TemplateBinding Padding}"                                                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"                                                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />                                                    </ControlTemplate>                                                </Setter.Value>                                            </Setter>                                        </Style>                                    </Flyout.FlyoutPresenterStyle>                                    <Grid>                                        <Grid.RowDefinitions>                                            <RowDefinition />                                            <RowDefinition Height="Auto" />                                        </Grid.RowDefinitions>                                        <CalendarView x:Name="Calendar"                                                      Style="{TemplateBinding CalendarViewStyle}" />                                        <Grid Grid.Row="1"                                              Height="45"                                              x:Name="AcceptDismissHostGrid">                                            <Grid.ColumnDefinitions>                                                <ColumnDefinition Width="*" />                                                <ColumnDefinition Width="*" />                                            </Grid.ColumnDefinitions>                                            <Button x:Name="AcceptButton"                                                    Grid.Column="0"                                                    Content="&#xE8FB;"                                                    FontFamily="{ThemeResource SymbolThemeFontFamily}"                                                    FontSize="16"                                                    HorizontalAlignment="Stretch"                                                    VerticalAlignment="Stretch"                                                    Style="{StaticResource DateTimePickerFlyoutButtonStyle}"                                                    Margin="0,2,0,0" />                                            <Button x:Name="DismissButton"                                                    Grid.Column="1"                                                    Content="&#xE711;"                                                    FontFamily="{ThemeResource SymbolThemeFontFamily}"                                                    FontSize="16"                                                    HorizontalAlignment="Stretch"                                                    VerticalAlignment="Stretch"                                                    Style="{StaticResource DateTimePickerFlyoutButtonStyle}"                                                    Margin="0,2,0,0" />                                        </Grid>                                    </Grid>                                </Flyout>                            </FlyoutBase.AttachedFlyout>                        </ToggleButton>                    </local:HeaderedContentControl>                </StackPanel>            </ControlTemplate>        </Setter.Value>    </Setter></Style>

注意这里的ToggleButton使用TwoWay Binding将IsChecked绑定到Picker的IsDropDownOpen属性,通过IsChecked属性与Flyout的Show/Hide关联起来。

另外,Flyout里放了一个CalendarView,用于选择日期。

在MyDatePicker.cs里除了属性,主要的内容是这段代码:

protected override void OnAccept(RoutedEventArgs e){    base.OnAccept(e);    if (_calendar != null && _calendar.SelectedDates.Any())        DateTime = _calendar.SelectedDates.First().DateTime;}

重写OnAccept(即点击AcceptButton触发的事件)并为DateTime选择值。

3.4 实际应用:实现一个MyTimePicker

使用TemplatePart的一个重要原则是:即使ControlTemplate中缺少声明的TemplatePart,模板化控件也不会报错,只会缺少部分功能。

根据这个原则实现的MyTimePicker就缺少了AcceptButton和DismissButton,因为使用了TimePickerFlyout,这个控件本身就有AcceptButton和DismissButton按钮。

<Style TargetType="local:MyTimePicker">    <Setter Property="IsTabStop"            Value="False" />    <Setter Property="FontFamily"            Value="{ThemeResource ContentControlThemeFontFamily}" />    <Setter Property="FontSize"            Value="{ThemeResource ControlContentThemeFontSize}" />    <Setter Property="Template">        <Setter.Value>            <ControlTemplate TargetType="local:MyTimePicker">                <StackPanel x:Name="LayoutRoot"                            Margin="{TemplateBinding Padding}">                    <local:HeaderedContentControl Header="{TemplateBinding Header}"                                                  HeaderTemplate="{TemplateBinding HeaderTemplate}">                        <ToggleButton x:Name="DateButton"                                      Content="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=Time,Converter={StaticResource TimeSpanToStringConverter}}"                                      IsEnabled="{TemplateBinding IsEnabled}"                                      IsChecked="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsDropDownOpen,Mode=TwoWay}"                                      HorizontalAlignment="Stretch"                                      HorizontalContentAlignment="Stretch">                            <FlyoutBase.AttachedFlyout>                                <TimePickerFlyout x:Name="Flyout"  Placement="Bottom"/>                            </FlyoutBase.AttachedFlyout>                        </ToggleButton>                    </local:HeaderedContentControl>                </StackPanel>            </ControlTemplate>        </Setter.Value>    </Setter></Style>
protected override void OnApplyTemplate(){    base.OnApplyTemplate();    _timePickerFlyout = GetTemplateChild(FlyoutName) as TimePickerFlyout;    if (_timePickerFlyout != null)        _timePickerFlyout.TimePicked += OnTimePicked;    UpdateTimePicerFlyoutPickedTime();    UpdateVisualState(false);}protected virtual void OnTimeChanged(TimeSpan oldValue, TimeSpan newValue){    UpdateTimePicerFlyoutPickedTime();}protected override void OnIsDropDownOpenChanged(bool oldValue, bool newValue{    base.OnIsDropDownOpenChanged(oldValue, newValue);    if (newValue)        UpdateTimePicerFlyoutPickedTime();}private void UpdateTimePicerFlyoutPickedTime(){    if (_timePickerFlyout != null)        _timePickerFlyout.Time = Time;}private void OnTimePicked(TimePickerFlyout sender, TimePickedEventArgs args){    Time = args.NewTime;}

4. 结语

细心的话会发现Picker虽然定义了PopupStates这个VisualStateGroup,但从来没用到。其实这是为了将来可能会用到这个这组状态而预留的。值得一提的是Picker不止可以针对弹出Flyout的控件,将ToggleButton和它的Flyout换成Expander也一样适用。

有了Picker类后确实方便了很多。其实Silverlight虽然死了,但开源的Silverlight Toolkit中有不少内容都可用作参考。本来还想给出Silverlight Toolkit中Picker的源码地址作为参考,但最近CodePlex关闭服务了。有兴趣的人仍可直接下载Silverlight Toolkit的源码,毕竟还是挺有趣的。
Silverlight Toolkit - CodePlex Archive

5. 参考

Guidelines for date and time controls

6. 源码

PickerTest

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台