WPF 自定义表单控件项:Label + TextBox

▪ 前言

WPF 内置了丰富的 UI 控件,但是他们都是独立。在实际的开发中,我们经常需要将几个 UI 控件组合起来用。

比如下面 效果图 中的 “账号输入项”:背景白色,左侧 Label控件,右侧 TextBox控件。当然它还有一些变种:背景是透明的,下面有条白线。

为了和 “UI 控件” 名字区分,我们约定将几个 “UI 控件” 组合的统称为 “表单控件项

▪ 效果图

在这里插入图片描述

▪ 原始实现

白色背景的 表单控件项(账号输入项)实现代码:
<DockPanel Width="360" Height="35" Background="#FFFFFFFF">
    <Label Width="70" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" DockPanel.Dock="Left">账号:</Label>
    <TextBox x:Name="uiUsername" VerticalContentAlignment="Center" BorderThickness="0"></TextBox>
</DockPanel>
透明背景的 表单控件项(账号输入项)实现代码:
<DockPanel Width="360" Height="35" Background="{x:Null}">
    <Label Width="70" BorderBrush="White" BorderThickness="0,0,0,1" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" DockPanel.Dock="Left" Foreground="#FFFFFFFF">账号:</Label>
    <TextBox x:Name="uiUsername" BorderBrush="White" BorderThickness="0,0,0,1" VerticalContentAlignment="Center" Background="{x:Null}" Foreground="#FFFFFFFF" CaretBrush="#FFFFFFFF"></TextBox>
</DockPanel>

上次代码使用了 DockPanel 容器控件,并设置其内的 Label 控件属性 DockPanel.Dock="Left",这样做的主要好处就是 TextBox 空间宽度可以自适应填满整个 DockPanel 的剩余宽度空间

▪ 优化实现

原始实现,一个 表单控件项 的实现需要4行代码(每个控件里还有大量的属性设置)。当软件里只有一两个 表单控件项 的时候还可以,但是如果有大量的这些 表单控件项,那么将非常不利于维护。OK,接下来我们将利用 StyleControlTemplate 来优化上述的代码,看看优化后实现上述两个效果的代码:

白色背景的 表单控件项(账号输入项)实现代码:
<TextBox x:Name="uiUsername" Tag="账号:" Width="360" Height="35" Style="{StaticResource styleFormcItemLTB}" Background="White"></TextBox>
透明背景的 表单控件项(账号输入项)实现代码:
<TextBox x:Name="uiUsername" Tag="账号:" Text="{Binding username}" Style="{StaticResource styleFormcItemLTB}" Width="365" Height="35" BorderBrush="White" CaretBrush="White" BorderThickness="0,0,0,1" Foreground="#FFFFFFFF"></TextBox>

只用一行代码我们就实现了上述功能,相比 原始实现 简单了很多。那我们是如何做到的呢?主要功劳还是 Style="{StaticResource styleFormcItemLTB}" 这一句,下面我们最为核心的 styleFormcItemLTB 样式资源的实现代码:

<!-- 表单控件项:Label + TextBox -->
<Style x:Key="styleFormcItemLTB" TargetType="{x:Type TextBox}">
    <Setter Property="BorderThickness" Value="0"></Setter>
    <Setter Property="VerticalContentAlignment" Value="Center"></Setter>

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBox}">
                <DockPanel Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Background="{TemplateBinding Background}">
                    <Label Width="70" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{x:Null}" Foreground="{TemplateBinding Foreground}" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Content="{TemplateBinding Tag}" DockPanel.Dock="Left"></Label>
                    <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{x:Null}">
                        <ScrollViewer x:Name="PART_ContentHost" Foreground="{TemplateBinding Foreground}"></ScrollViewer>
                    </Border>
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

其实原理很简单,就是把 原始实现 的代码稍作一些修改然后移植到 StyleControlTemplate 中,然后通过 {TemplateBinding ...} 继承 TextBox 的一些属性。

以样式 styleFormcItemLTB 作为基础,通过继承 TextBox 控件里的自定义样式属性值自动设置 表单控件项 内各个控件属性,快速的构建出 表单控件项

这里有个知识点需要注意,代码:

<ScrollViewer x:Name="PART_ContentHost" Foreground="{TemplateBinding Foreground}"></ScrollViewer>

ControlTemplate 中我们使用了上述代码,这段代码初学者这可能一下子理解不了。但是如果你把它移除了,你会发现运行软件是输入框没有了,那说明这段代码负责呈现一个输入框的,那能不能直接把它替换为:

<TextBox Foreground="{TemplateBinding Foreground}"><TextBox>

尝试了一下,替换以后可以正常显示输入框,但是在 cs 文件中我们发现无法通过 uiUsername.Text 获取到输入框中的值;很明显,这个代码只实现了展示效果,没有实现一个控件的逻辑,<TextBox Foreground="{TemplateBinding Foreground}"><TextBox> 生成了一个新的 TextBox 控件,没有和 <TextBox x:Name="uiUsername" ... 关联。

通过上面的尝试,我们基本可以确定 <ScrollViewer x:Name="PART_ContentHost" ... 是生成了一个和 <TextBox x:Name="uiUsername" 关联 TextBox,因为你可以在 cs 文件中通过 uiUsername.Text 设置和获取控件的值。

▪ 原理说明

为什么<ScrollViewer x:Name="PART_ContentHost" ... 是生成了一个和 <TextBox x:Name="uiUsername" 关联呢?这其中最为核心的就是 x:Name="PART_ContentHost" 这句话。

我们知道WPF控件是不用有固定形状的,我们可以通过 Style 来任意改变它的具体表现。
但是,控件本身具有特定的逻辑和作用:比如一个按钮,应该是可以点击的;一个输入框控件,应该是可以输入的。
这里就存在了一个矛盾:如果可以任意改变控件的具体表现,那么如何保证它特有的逻辑和作用呢?

答案就是 WPF 控件的 “部件” 概念。简单的说,就是你可以任意改变我,但是要提供我期待的部件,否则我的逻辑和作用就不能得到保证。如果阅读 TextBoxBase 的 MSDN 参考(TextBox 继承于 TextBoxBase),你会看到如下的特性:

[TemplatePartAttribute(Name = "PART_ContentHost", Type = typeof(FrameworkElement))]
[LocalizabilityAttribute(LocalizationCategory.Text)]
public abstract class TextBoxBase : Control

上述代码引用自 https://msdn.microsoft.com/zh-cn/library/system.windows.controls.primitives.textboxbase

该特性表明 TextBoxBase 控件期待你提供一个名字叫 PART_ContentHost 的部件,该部件必须是 FrameworkElement。而 TextBoxBase 将会把具体的 TextView 和 TextEditor 放到 PART_ContentHost 里面。

而能设置 x:Name="PART_ContentHost" 的只有 DecoratorScrollViewer 元素,所以下面的代码就会出错:

// 提示错误:只有 Decorator 或 ScrollViewer 元素可以用作 PART_ContentHost
<Label x:Name="PART_ContentHost" Foreground="{TemplateBinding Foreground}"></Label>

不使用 因为这个该元素不支持 Foreground 属性值

经过测试 也支持 x:Name="PART_ContentHost",不过其不支持 Foreground 属性值

▪ 潜在问题

上述的代码设计中,我们采用了 TextBox 关联 styleFormcLTB 样式,然后在 TextBox 里设置样式属性值,styleFormcLTB 通过集成这些属性值快速构建出 表单控件项

后来在使用过程中这种方式会导致 TextBox 禁用状态下时样式无法改变(默认变灰),就算在 styleFormcLTB 设置了 Trigger IsEnabled 也无法改变样式。就感觉 TextBox 控件里的样式属性值优先级高于 styleFormcLTB 里的属性值,优先级低的无法改变优先高级的属性值,没有办法只能基于 styleFormcLTB 样式类在按需做出其他样式类了,重构的代码如下:

白色背景的 表单控件项(账号输入项)实现代码:
<TextBox x:Name="uiUsername" Tag="账号:" Width="360" Height="35" Style="{StaticResource styleFormcItemLTB10}"></TextBox>
透明背景的 表单控件项(账号输入项)实现代码:
<TextBox x:Name="uiUsername" Tag="账号:" Text="{Binding username}" Style="{StaticResource styleFormcItemLTB01}"></TextBox>
<Style x:Key="styleFormcItemLTB01" BasedOn="{StaticResource styleFormcItemLTB}" TargetType="{x:Type TextBox}">
    <Setter Property="Background" Value="{x:Null}"></Setter>
    <Setter Property="CaretBrush" Value="White"></Setter>
    <Setter Property="Foreground" Value="White"></Setter>
    <Setter Property="BorderBrush" Value="White"></Setter>
    <Setter Property="BorderThickness" Value="0,0,0,1"></Setter>
</Style>

<Style x:Key="styleFormcItemLTB10" BasedOn="{StaticResource styleFormcItemLTB}" TargetType="{x:Type TextBox}">
    <Setter Property="Background" Value="White"></Setter>
    <Style.Triggers>
        <Trigger Property="IsEnabled" Value="False">
            <Setter Property="Background" Value="#FFF0F0F0"></Setter>
            <Setter Property="Foreground" Value="#FF666666"></Setter>
        </Trigger>
    </Style.Triggers>
</Style>

styleFormcItemLTB01 中的首位 0 表示没有背景,末尾 1 表示没有背景情况下一种样式(下划线),当然也可以自定义 00、02、03、04 …

styleFormcItemLTB10 中的首位 1 表示白色背景,末尾 0 表示白色背景下的一种样式,当然也可以自定义 01、02、03、04 …

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值