1. 前言:
本篇内容基于上一篇文章【WPF实用教程2】(点此跳转)创建的解决方案进行演示。
这一篇章通过自定义一个具有清空功能的输入框,带大家进入WPF自定义控件开发的内容,重在演示创建方法,其中涉及到的一些理论知识会简单提及,不会重点解释,后续会在进阶篇中讲解。
工程最后效果图:
2. 工程相关内容清理
在进行演示前,先把工程内的内容清理一下,如下图
将MainWindow.xaml中的第4行、第9行到12删掉,删掉后如下图
将MicroUI.Wpf.Toolkit项目中的CustomControl.cs删除
将Themes\Generic.xaml文件中的第5到17行删掉
清理后的MicroUI.Wpf.Toolkit项目内容如下:
好了,这下清理的很干净了,可以重新生成解决方案并运行一下,看看此时的解决方案有没有问题:
我的没有问题,下面可以进行演示了。
3. 自定义控件工程
3.1 新建自定义控件
鼠标右击MicroUI.Wpf.Toolkit项目,依次选择“添加”\“新建项”
按照箭头顺序依次选择“WPF”\“自定义控件(WPF)”,名字为ClearTextBox.cs,然后点击“添加”按钮。
此时,在项目中增加了ClearTextBox.cs和 Generic.xaml中增加了其默认的样式:
为了方便管理控件,我以后把每个控件的内容都放在各自的文件夹里,所以再新建一个ClearTextBox文件夹:
然后把ClearTextBox.cs拖拽到新建的ClearTextBox文件夹:
然后,在ClearTextBox文件夹内新建一个资源文件:
资源文件命名为Themes.xaml
添加后如下:
将ClearTextBox\Themes.xaml内容修改如下:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MicroUI.Wpf.Toolkit">
<Style TargetType="{x:Type local:ClearTextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ClearTextBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
将Themes\Generic.xaml代码修改如下:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MicroUI.Wpf.Toolkit">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/MicroUI.Wpf.Toolkit;component/ClearTextBox/Themes.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
然后修改MicroUI.Wpf.Samples项目中的MainWindow.xaml内容如下:
<Window x:Class="MicroUI.Wpf.Samples.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:microui="clr-namespace:MicroUI.Wpf.Toolkit;assembly=MicroUI.Wpf.Toolkit"
Title="MainWindow" Height="350" Width="525">
<Grid>
<microui:ClearTextBox Width="100" Height="30"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
</Window>
运行:
啥都没有。下面开始修改控件了。
3.2 修改自定义控件
将ClearTextBox继承的Control换成TextBox,如下图:
然后修改ClearTextBox\Themes.xaml内容如下:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MicroUI.Wpf.Toolkit">
<Style TargetType="{x:Type local:ClearTextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ClearTextBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<!--文本内容区域-->
<ScrollViewer x:Name="PART_ContentHost"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
可以看到实际上只是加入了以下内容:
然后修改MainWindow.xaml内容如下:
由于ClearTextBox继承了TextBox,所以可以设置Text属性了,运行:
可以看到运行有效果。注意我在ClearBox\Themes.xaml中加的这句话:
这句话看似很简单,其实其中的PART_***这个名字就有很多的信息量,但是我这里一笔带过,即:
ScrollViewer的“PART_ContentHost”名称,是WPF内置的特殊名称,在本控件中,表示这个ScrollViewer是用于装载TextBox的文本内容的。PART_ContentHost这个名称只能用于ScrollViewer或者Adorner、Decorator控件。
接下来,我们要加一个清除的按钮:
3.3 添加清除按钮
重新修改一下ClearBox\Themes.xaml文件内容如下:
然后生成解决方案,重新运行:
可以看到已经有了一个按钮。样式还是比较丑的,先不管样式,实现功能再说。
3.4 给按钮添加清除功能
可以看到我已经在ClearBox\Themes.xaml中给清除按钮赋了一个名字PART_ClearButtonHost
接下来我就再修改一下ClearTextBox.cs,修改的内容如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MicroUI.Wpf.Toolkit
{
[TemplatePart(Name = "PART_ClearButtonHost", Type = typeof(ButtonBase))]
public class ClearTextBox : TextBox
{
private ButtonBase clearButtonHost;
static ClearTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ClearTextBox), new FrameworkPropertyMetadata(typeof(ClearTextBox)));
}
#region 继承事件
//模板化控件在加载ControlTemplate后会调用OnApplyTemplate
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (clearButtonHost != null)
{
clearButtonHost.Click -= HandleClearButtonClick;
}
clearButtonHost = GetTemplateChild("PART_ClearButtonHost") as ButtonBase;
if (clearButtonHost != null)
{
clearButtonHost.Click += HandleClearButtonClick;
}
}
#endregion
#region 方法
//清除事件
private void HandleClearButtonClick(object sender, RoutedEventArgs e)
{
Reset();
}
public void Reset()
{
Text = String.Empty;
}
#endregion
}
}
注意:不要在Loaded事件中尝试调用GetTemplateChild,因为Loaded在OnApplyTemplate前调用,而且Loaded更容易被多次触发。由于Template可能多次加载,或者不能正确获取TemplatePart,所以使用TemplatePart前应该先判断是否为空;如果要订阅事件,应该先取消订阅。
完整的GetTemplateChild的步骤如下:
-
取消订阅TemplatePart事件
-
将TemplatePart存储到私有字段
-
订阅TemplatePart事件
更详细的关于TemplatePart的意义暂时不解释,先会用,然后再去深究意义。
重新生成解决方案,运行程序:
可以看到,已经运行了。
到这里还没有完事,我们希望如果有数据了就显示清除按钮,没有数据就隐藏清除按钮,接下来我们就实现这个需求。
3.5 判断数据是否为空
接下来,我们再次修改ClearTextBox.cs,给其增加一个IsHasText的属性。
然后重载OnTextChanged()事件,
此时完整的代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MicroUI.Wpf.Toolkit
{
[TemplatePart(Name = "PART_ClearButtonHost", Type = typeof(ButtonBase))]
public class ClearTextBox : TextBox
{
private ButtonBase clearButtonHost;
static ClearTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ClearTextBox), new FrameworkPropertyMetadata(typeof(ClearTextBox)));
}
#region 输入框内是否有数据
[Browsable(false)]
public bool IsHasText
{
get { return (bool)GetValue(IsHasTextProperty); }
private set { SetValue(IsHasTextPropertyKey, value); }
}
private static readonly DependencyPropertyKey IsHasTextPropertyKey =
DependencyProperty.RegisterReadOnly(
"IsHasText",
typeof(Boolean),
typeof(ClearTextBox),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty IsHasTextProperty =
IsHasTextPropertyKey.DependencyProperty;
#endregion
#region 继承事件
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
IsHasText = !String.IsNullOrEmpty(Text);
}
//模板化控件在加载ControlTemplate后会调用OnApplyTemplate
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (clearButtonHost != null)
{
clearButtonHost.Click -= HandleClearButtonClick;
}
clearButtonHost = GetTemplateChild("PART_ClearButtonHost") as ButtonBase;
if (clearButtonHost != null)
{
clearButtonHost.Click += HandleClearButtonClick;
}
}
#endregion
#region 方法
//清除事件
private void HandleClearButtonClick(object sender, RoutedEventArgs e)
{
Reset();
}
public void Reset()
{
Text = String.Empty;
}
#endregion
}
}
然后我们要根据IsHasText属性,使用样式的触发来完成清除按钮的显示与否。修改ClearTextBox\Themes.xaml内容如下:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MicroUI.Wpf.Toolkit">
<Style TargetType="{x:Type local:ClearTextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ClearTextBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!--文本内容区域-->
<ScrollViewer x:Name="PART_ContentHost" Grid.Column="1" VerticalAlignment="Center"/>
<!--清除按钮-->
<Button x:Name="PART_ClearButtonHost" Grid.Column="2"
Padding="2,0" IsHitTestVisible="False" Visibility="Collapsed"
Content="x"
/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="UIElement.IsEnabled" Value="True"/>
<Condition Property="IsHasText" Value="True"/>
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter TargetName="PART_ClearButtonHost" Property="IsHitTestVisible" Value="True"/>
<Setter TargetName="PART_ClearButtonHost" Property="Visibility" Value="Visible"/>
</MultiTrigger.Setters>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
可以看到,我首先设置PART_ClearButtonHost初始状态不可见:
然后给模板添加了一个多条件的触发:
触发条件为:控件使能且IsHastText为True时,清除按钮显示且可点击。
然后我们重新生成解决方案,运行:
基本清除功能已经完成了,但是这个样式呢,略微丑一点,接下来我们美化一下清除按钮的样式。
3.6 使用iconfont美化按钮样式
这一节内容涉及到文章【WPF实用教程1-使用Iconfont图标字体】,建议先去看下这篇内容。
下载项目文件,提取出iconfont.ttf到Res目录
MicroUI.Wpf.Toolkit项目新增Fonts目录,如下图:
将iconfont.ttf导入到Fonts目录下:
MicroUI.Wpf.Toolkit项目新增Style目录,如下图:
在Style目录下新增资源字典,文件名为MicroIcon.xaml:
修改MicroIcon.xaml内容如下:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="MicroBtnIcon" TargetType="{x:Type Button}">
<Setter Property="FontFamily" Value="/MicroUI.Wpf.Toolkit;component/Fonts/#iconfont"></Setter>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="3" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border BorderBrush="{TemplateBinding Control.BorderBrush}" BorderThickness="0">
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Opacity" Value="0.8" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter Property="Opacity" Value="0.7" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="MicroBtnIcon_Close" TargetType="{x:Type Button}" BasedOn="{StaticResource MicroBtnIcon}" >
<Setter Property="Content" Value="" />
</Style>
</ResourceDictionary>
然后在ClearTextBox\Themes.xaml中添加如下三句话:
同时修改清除按钮内容如下:
重新生成解决方案,再次运行:
可以看到此时样式已经不错了。
3.7 添加水印
接下来,我准备再加上一个水印。编辑ClearTextBox.cs,在其中添加如下几句话:
#region 水印
[Category("Extend Properties")]
public String WaterMark
{
get { return (String)GetValue(WaterMarkProperty); }
set { SetValue(WaterMarkProperty, value); }
}
public static readonly DependencyProperty WaterMarkProperty =
DependencyProperty.Register("WaterMark", typeof(String), typeof(ClearTextBox), new PropertyMetadata(String.Empty));
#endregion
在ClearTextBox\Themes.xaml中添加如下几句话:
<!--水印区域-->
<Label x:Name="watermark" Grid.Column="1"
Content="{TemplateBinding WaterMark}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
IsHitTestVisible="False"
Foreground="#CCCCCC"
Margin="{TemplateBinding Padding}"
Visibility="Collapsed" />
以及添加如下几句话:
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="UIElement.IsEnabled" Value="True"/>
<Condition Property="UIElement.IsFocused" Value="False"/>
<Condition Property="TextBox.Text" Value=""/>
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter TargetName="watermark" Property="Visibility" Value="Visible"/>
</MultiTrigger.Setters>
</MultiTrigger>
修改MicroUI.Wpf.Samples项目的MainWindow.xaml内容如下:
<Window x:Class="MicroUI.Wpf.Samples.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:microui="clr-namespace:MicroUI.Wpf.Toolkit;assembly=MicroUI.Wpf.Toolkit"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<microui:ClearTextBox Width="200" Height="30"
VerticalAlignment="Center"
HorizontalAlignment="Center"
BorderBrush="#dddddd"
BorderThickness="1"
WaterMark="这里是水印"
Text="公众号:BigBearIT"/>
<TextBox Text="这里是普通的TextBox"
HorizontalAlignment="Center"
VerticalContentAlignment="Center"
Width="200" Height="30" Margin="0,10" />
</StackPanel>
</Grid>
</Window>
重新生成解决方案,运行程序:
3.8 小节
本篇通过创建一个带水印支持清除功能的输入框,演示了自定义控件的创建流程,建议大家先自己了解一下依赖属性、TemplatePart,我也会在后续合适的时机重点讲述一下这些。
/////////////////////////////////////////////////////////////////////////////////////////
** 原创文章,转载请附该部分声明
** 来源:https://blog.csdn.net/mybelief321
** 作者:玖零大壮
/////////////////////////////////////////////////////////////////////////////////////////