• [UWP]合体姿势不对的HeaderedContentControl


    1. 前言

    HeaderedContentControl是WPF中就存在的控件,这个控件的功能很简单:提供Header和Content两个属性,在UI上创建两个ContentPresenter并分别绑定到Header和Content,让这两个ContentPresenter合体组成HeaderedContentControl。

    2. 以前的问题

    在WPF中,HeaderedContentControl是Expander、GroupBox、TabItem等诸多拥有Header属性的控件的基类,虽然很少直接用这个控件,它的存在也有一定价值。不过在WPF中它的价值也仅此而已,由开发者自己实现也极其容易,以至于后来在Silverlight中就没有提供这个控件(后来放到了Silverlight Toolkit这个扩展里)。

    UWP中几乎所有的表单控件都有Header属性,如TextBox、ComboBox等,这么看起来HeaderedContentControl更加重要了,但UWP反而没有提供HeaderedContentControl这个控件。每个有Header属性的控件都既没有继承HeaderedContentControl,也没有使用HeaderedContentControl作为外层容器包装自己的内容,而是全都单独实现这个属性。其实这也可以理解,毕竟不是所有控件都是ContentControl,而且使用HeaderedContentControl作为外层容器会导致VisualTree多了一层,变得复杂而且影响性能。其实现在很少会有一个页面出现十分多表单控件的情况,这点性能损失我是不介意的。

    UWP CommunityToolkit中也有一些控件包含Header属性,如HeaderedTextBlock和Expander,CommunityToolkit也没有为它们创建一个HeaderedContentControl,而且和TextBox等控件不同,UWP CommunityToolkit中的Header属性都是string类型,真是任性。

    GitHub上也有过添加HeaderedContentControl的意见,其实我是很支持这件事的,毕竟HeaderedContentControl可不只是多了一个Header属性而已。可是微软一直拖到 UWPCommunityToolkit Release v2.1.0 发布才终于肯提供这个控件。

    3. 现在的问题

    虽然终于~终于等到了HeaderedContentControl,但让人高兴不起来,而且现在连HeaderedTextBlock和Expander都不使用这个HeaderedContentControl。微软第一次在UWP提供了HeaderedContentControl,有了一个Object类型的Header属性,两件事本应该为开发者提供更多的方便,但是,为什么会变成这样呢。

    刚开始,HeaderedContentControl的Default Style是这样的:

    <Style TargetType="controls:HeaderedContentControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:HeaderedContentControl">
                    <StackPanel>
                        <ContentPresenter Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}"/>
                        <ContentPresenter/>
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    
    

    真是让人扫兴。

    毕竟这是照抄WPF的,也不能说它不对,但同样地这就把WPF的遗留问题完全保留下来了:因为使用了StackPanel,所以VerticalContentAlignment无论怎么设置都是无效的,Content都是直接趴在Header下面,两个ContentPresenter总是腻在一起:

    <Grid Background="#FF017DB3"
          Padding="10">
        <controls:HeaderedContentControl Header="Header"
                                         Foreground="White"
                                         Content="正确的垂直居中"
                                         VerticalContentAlignment="Center" />
    </Grid>
    <Grid Grid.Column="1"
          Padding="10"
          Background="#FFBB310A">
        <controls:HeaderedContentControl Header="Header"
                                         Foreground="White"
                                         Content="错误的垂直居中"
                                         VerticalContentAlignment="Center"
                                         Style="{StaticResource WPFStyle}" />
    </Grid>
    

    这样的合体姿势明显不对,事实上在WPF中继承HeaderedContentControl的控件(如Expander和GroupBox)都在ControlTempalte中使用了Grid或DockPanel,而不是StackPanel,HeaderedContentControl使用StackPanel本身就是个错误。好在UWP CommunityToolkit
    2.1正式添加HeaderedContentControl时Default Style修改为了使用Grid,总算解决了这个历史遗留问题:

    <Style TargetType="controls:HeaderedContentControl">
        <Setter Property="HorizontalContentAlignment" Value="Left"/>
        <Setter Property="VerticalContentAlignment" Value="Top"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:HeaderedContentControl">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <ContentPresenter Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}"/>
                        <ContentPresenter Grid.Row="1" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    
    

    另一个问题是Header与Content之间的Margin。仔细观察就会发现TextBox等控件的Header是有一个0,0,0,8的Margin,可是HeaderedContentControl并没有这样设置,结果HeaderedContentControl就会出现高度不匹配的问题:

    <StackPanel Width="200"
                Margin="10,0">
        <TextBox Header="TextBox" />
        …
    </StackPanel>
    <StackPanel Width="200"
                Margin="10,0"
                Grid.Column="1">
        <controls:HeaderedContentControl Header="TextBox"
                                         HorizontalContentAlignment="Stretch">
            <TextBox />
        </controls:HeaderedContentControl>
        …
    </StackPanel>
    
    

    不仅如此,TextBox在Disabled状态下Header会变成灰色,但HeaderedContentControl明显漏了这个VisualState,结果如下图所示,这个如果也要自己实现就很麻烦了。

    以前微软迟迟不肯提供HeaderedContentControl,现在一出手就是半成品,我很怀疑微软这样做是为了考验我们这些还在坚持UWP的纯真开发者。

    4. 自己实现有一个HeaderedContentControl

    与其留着这个半成品祸害自己的代码,还不如干脆动手实现一个HeaderedContentControl。在以前已写过一次实现HeaderedContentControl的文章,但那篇主要是为了讲解模板化控件,没有完整的功能。这次要做得完善些。

    4.1 基本外观

    <Style TargetType="local:HeaderedContentControl">
        <Setter Property="FontFamily"
                Value="{ThemeResource ContentControlThemeFontFamily}" />
        <Setter Property="FontSize"
                Value="{ThemeResource ControlContentThemeFontSize}" />
        <Setter Property="Foreground"
                Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
        <Setter Property="HorizontalContentAlignment"
                Value="Stretch" />
        <Setter Property="VerticalContentAlignment"
                Value="Stretch" />
        <Setter Property="IsTabStop"
                Value="False" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:HeaderedContentControl">
                    <Grid>
                        …
                        …
                        …          
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="*" />
                        </Grid.RowDefinitions>
                        <ContentPresenter x:Name="HeaderContentPresenter"
                                          x:DeferLoadStrategy="Lazy"
                                          Visibility="Collapsed"
                                          Margin="0,0,0,8"
                                          Foreground="{ThemeResource SystemControlForegroundBaseHighBrush}"
                                          Content="{TemplateBinding Header}"
                                          ContentTemplate="{TemplateBinding HeaderTemplate}"
                                          FontWeight="Normal" />
                        <ContentPresenter Grid.Row="1"
                                          Content="{TemplateBinding Content}"
                                          ContentTemplate="{TemplateBinding ContentTemplate}"
                                          Margin="{TemplateBinding Padding}"
                                          ContentTransitions="{TemplateBinding ContentTransitions}"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    
    

    包含Header和HeaderTemplate这两个属性和CommunityToolkit中的HeaderedContentControl一样,ControlTemplate中使用了Grid作为容器这点也一样,改变的主要有以下几点:

    • Margin、ContentTransitions等属性有按照标准做法好好做了绑定。
    • HorizontalContentAlignment和VerticalContentAlignment也从Left和Top改为Stretch,毕竟很多时候使用ContentPresenter 都要把这两个属性改为Stretch,还不如一开始就这样做。
    • 别忘了IsTabStop要设置为False,这点以前在UI指南里有介绍过原因,这里不再赘述。

    4.2 Disabled状态

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="CommonStates">
            <VisualState x:Name="Disabled">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter"
                                                   Storyboard.TargetProperty="Foreground">
                        <DiscreteObjectKeyFrame KeyTime="0"
                                                Value="{ThemeResource SystemControlDisabledBaseMediumLowBrush}" />
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
            <VisualState x:Name="Normal" />
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    
    protected virtual void UpdateVisualState(bool useTransitions)
    {
        VisualStateManager.GoToState(this, IsEnabled ? NormalName : DisabledName, useTransitions);
    }
    
    

    ControlTemplate中需要包办Disabled状态,HeaderedContentControl中订阅自身的IsEnabledChanged事件,根据IsEnabled的值转换状态。

    4.3 隐藏HeaderContentPresenter

    private void UpdateVisibility()
    {
        if (_headerContentPresenter != null)
            _headerContentPresenter.Visibility = _headerContentPresenter.Content == null ? Visibility.Collapsed : Visibility.Visible;
    }
    
    

    OnApplyTemplate()OnHeaderChanged(object oldValue, object newValue)函数中调用UpdateVisibility()以决定HeaderContentPresenter是否显示。这个功能,以及HeaderContentPresenter的Margin,HeaderedTextBlock都是有的,但偏偏就没做到隔壁的HeaderedContentControl,真是够了。

    4.4 处理HeaderContentPresenter的点击事件

    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _headerContentPresenter = GetTemplateChild(HeaderContentPresenterName) as ContentPresenter;
        UpdateVisibility();
        UpdateVisualState(false);
    
        if (_headerContentPresenter != null)
        {
            _headerContentPresenter.PointerReleased += OnHeaderContentPresenterPointerReleased;
            _headerContentPresenter.PointerPressed += OnHeaderContentPresenterPointerPressed1;
        }
    }
    
    
    private void OnHeaderContentPresenterPointerPressed1(object sender, PointerRoutedEventArgs e)
    {
        if (Content is Control control)
            control.Focus(FocusState.Programmatic);
    }
    
    private void OnHeaderContentPresenterPointerReleased(object sender, PointerRoutedEventArgs e)
    {
        e.Handled = true;
    }
    
    

    在TextBox上点击它的Header,输入框将会获得焦点,上述代码就是实现这个功能。

    这个功能我不是十分确定,至少目前看来这个行为是正确的。

    5. 结语

    HeaderedContentControl 明明只是个很简单的控件,明明只是个很简单的控件,明明只是个很简单的控件。

    附上完整的代码:

    [TemplateVisualState(Name = NormalName, GroupName = CommonStatesName)]
    [TemplateVisualState(Name = DisabledName, GroupName = CommonStatesName)]
    [TemplatePart(Name = HeaderContentPresenterName, Type = typeof(ContentPresenter))]
    public class HeaderedContentControl : ContentControl
    {
        private const string CommonStatesName = "CommonStates";
        private const string NormalName = "Normal";
        private const string DisabledName = "Disabled";
        private const string HeaderContentPresenterName = "HeaderContentPresenter";
    
        /// <summary>
        ///     标识 Header 依赖属性。
        /// </summary>
        public static readonly DependencyProperty HeaderProperty =
            DependencyProperty.Register("Header", typeof(object), typeof(HeaderedContentControl), new PropertyMetadata(null, OnHeaderChanged));
    
        /// <summary>
        ///     标识 HeaderTemplate 依赖属性。
        /// </summary>
        public static readonly DependencyProperty HeaderTemplateProperty =
            DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate), typeof(HeaderedContentControl), new PropertyMetadata(null, OnHeaderTemplateChanged));
    
    
        private ContentPresenter _headerContentPresenter;
    
        public HeaderedContentControl()
        {
            DefaultStyleKey = typeof(HeaderedContentControl);
            IsEnabledChanged += OnPickerIsEnabledChanged;
        }
    
        /// <summary>
        ///     获取或设置Header的值
        /// </summary>
        public object Header
        {
            get => GetValue(HeaderProperty);
            set => SetValue(HeaderProperty, value);
        }
    
        /// <summary>
        ///     获取或设置HeaderTemplate的值
        /// </summary>
        public DataTemplate HeaderTemplate
        {
            get => (DataTemplate) GetValue(HeaderTemplateProperty);
            set => SetValue(HeaderTemplateProperty, value);
        }
    
    
        private static void OnHeaderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            var target = obj as HeaderedContentControl;
            var oldValue = args.OldValue;
            var newValue = args.NewValue;
            if (oldValue != newValue)
                target.OnHeaderChanged(oldValue, newValue);
        }
    
        private static void OnHeaderTemplateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            var target = obj as HeaderedContentControl;
            var oldValue = (DataTemplate) args.OldValue;
            var newValue = (DataTemplate) args.NewValue;
            if (oldValue != newValue)
                target.OnHeaderTemplateChanged(oldValue, newValue);
        }
    
    
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _headerContentPresenter = GetTemplateChild(HeaderContentPresenterName) as ContentPresenter;
            UpdateVisibility();
            UpdateVisualState(false);
    
            if (_headerContentPresenter != null)
            {
                _headerContentPresenter.PointerReleased += OnHeaderContentPresenterPointerReleased;
                _headerContentPresenter.PointerPressed += OnHeaderContentPresenterPointerPressed1;
            }
        }
    
        protected virtual void OnHeaderChanged(object oldValue, object newValue)
        {
            UpdateVisibility();
        }
    
        protected virtual void OnHeaderTemplateChanged(DataTemplate oldValue, DataTemplate newValue)
        {
        }
    
    
        protected virtual void UpdateVisualState(bool useTransitions)
        {
            VisualStateManager.GoToState(this, IsEnabled ? NormalName : DisabledName, useTransitions);
        }
    
        private void UpdateVisibility()
        {
            if (_headerContentPresenter != null)
                _headerContentPresenter.Visibility = _headerContentPresenter.Content == null ? Visibility.Collapsed : Visibility.Visible;
        }
    
        private void OnPickerIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            UpdateVisualState(true);
        }
    
        private void OnHeaderContentPresenterPointerPressed1(object sender, PointerRoutedEventArgs e)
        {
            if (Content is Control control)
                control.Focus(FocusState.Programmatic);
        }
    
        private void OnHeaderContentPresenterPointerReleased(object sender, PointerRoutedEventArgs e)
        {
            e.Handled = true;
        }
    }
    
    

    6. 参考

    HeaderedContentControl
    HeaderedContentControl XAML Control

    7. 源码

    PickerTest

  • 相关阅读:
    批处理、WMI、wmic、守护进程
    链接
    给自己一个提醒——读了这篇文章感觉目前就略有体会,但是它又是一个长期的过程。
    如何设置一个日期型的TextView
    广播机制与短信拦截器
    开篇来个记录随笔 ,再感谢一下园子的管理们
    J2EE (四) Servlet 模板方法模式
    J2EE (五) SQL+PageClass实现真分页
    JMeter设置HTML报告中开始以及结束时间精确到秒
    Tomcat部署应用启动报错java.lang.ClassCastException: [XXX].filter.CharacterEncoding cannot be cast to jakarta.servlet.Filter
  • 原文地址:https://www.cnblogs.com/dino623/p/HeaderContentPresenter.html
Copyright © 2020-2023  润新知