返回目录
-1:控件演示和源代码下载
文章将会创建一个Lookless控件:文件选择器(FileSelector)
使用起来非常简单,用控件的PredefinedExts属性指定支持的扩展名(第一个会作为默认值),用逗号分开多个扩展名。
注意下面XAML中loc是程序当前命名空间:
xmlns:loc="clr-namespace:Mgen_WPF"
接着类型定义:
<loc:FileSelector PredefinedExts="txt,doc,xml" />
然后程序会出现默认样式的文件选择控件:

点击“浏览”后打开文件对话框的扩展名设置都会自动完成:

当然既然是Lookless的,那么控件模板用户随意定义,比如这样定义:
<loc:FileSelector PredefinedExts="txt,doc,xml">
<loc:FileSelector.Template>
<ControlTemplate TargetType="{x:Type loc:FileSelector}">
<Border BorderBrush="Red"
BorderThickness="2"
CornerRadius="5"
Padding="10"
Margin="10">
<DockPanel>
<TextBox Name="PART_Text"
DockPanel.Dock="Bottom"/>
<Button Command="{x:Static loc:FileSelector.BrowseCommand}"
Foreground="White"
Background="Red"
Content="{Binding Path=Text,Source={x:Staticloc:FileSelector.BrowseCommand}}"/>
</DockPanel>
</Border>
</ControlTemplate>
</loc:FileSelector.Template>
</loc:FileSelector>
控件变成这样的样式:

控件内部是WPF依赖属性,路由命令,路由事件的执行,比如下面用另一个按钮进行路由命令的绑定:
<StackPanel>
<loc:FileSelector PredefinedExts="txt,doc,xml"
x:Name="fileSelector" />
<!--
绑定命令 -->
<Button Command="{x:Static loc:FileSelector.BrowseCommand}"
CommandTarget="{Binding ElementName=fileSelector}">这里也可以浏览(_B)</Button>
<TextBlock Text="{Binding Path,ElementName=fileSelector,StringFormat=这里是路径:\{0\}}"/>
</StackPanel>
下载
当前版本的演示工程和源代码下载
下载地址
注意:此为微软SkyDrive存档,请用浏览器直接下载,用某些下载工具可能无法下载
示例程序运行环境:.NET Framework 4.0 Client Profile
源代码环境:Visual Studio 2010
返回目录
0. 准备工作
下面代码均假定用户已加入如下命名空间:
using System;
using System.Linq; //程序用到LINQ
using System.Windows; //TemplatePartAttribute
using System.Windows.Controls; //Control
using System.Windows.Input; //RoutedUICommand
using Microsoft.Win32; //对话框OpenFileDialog
返回目录
1. 继承Control类型
我们的文件选择控件名称是FileSelector,那么定义一个类型继承System.Windows.Controls.Control类型。
接着在类型的静态构造函数中改写类型的默认样式类型,这样WPF进行样式匹配时会选用正确的自定义类型。上述方法需要改写FrameworkElement的DefaultStyleKeyProperty依赖属性,改写依赖属性的元数据则通过DependencyProperty.OverrideMetadata方法。
代码:
public class FileSelector : Control
{
static FileSelector()
{
//改写默认样式类型
DefaultStyleKeyProperty.OverrideMetadata(typeof(FileSelector), newFrameworkPropertyMetadata(typeof(FileSelector)));
}
}
返回目录
2. 添加依赖属性
程序的路径以Path依赖属性做存储。定义则基本上就是WPF依赖属性定义的一贯套路:通过DependencyProperty.Register方法注册依赖属性,该方法可以指定属性名称,默认值,验证回调方法(ValidationValueCallback),属性改变回调方法(PropertyChangedCallback),强制转换回调方法(CoerceValueCallback),和属性元数据(其实属性改变回调方法和强制转换回调方法是属于元数据内的)。(关于这三个回调方法的执行顺序,可以参考:WPF:关于依赖属性的ValidateValueCallback,PropertyChangedCallback和CoerceValueCallback的执行顺序)
注意依赖属性是静态的(static),所以值改变和强制转换回调方法也是静态的(在注册方法中定义它们)。而控件中还定义非静态的受保护的(protected virtual)值改变方法,这个方法被静态值改变方法调用(通过PropertyChangedCallback中的第一个参数:DependencyObject)。另外这个方法之后还要加入路由事件的调用。
#region Path依赖属性
//注册
public static readonly DependencyProperty PathProperty =
DependencyProperty.Register("Path", typeof(string),typeof(FileSelector),
new FrameworkPropertyMetadata((string)string.Empty,
new PropertyChangedCallback(OnPathChanged),
new CoerceValueCallback(CoercePath)));
//CLR属性包装
public string Path
{
get { return (string)GetValue(PathProperty);
}
set {
SetValue(PathProperty, value);
}
}
//静态值改变方法
private static void OnPathChanged(DependencyObject d,DependencyPropertyChangedEventArgs e)
{
FileSelector target = (FileSelector)d;
string oldPath = (string)e.OldValue;
string newPath = target.Path;
target.OnPathChanged(oldPath,
newPath);
}
//非静态受保护值改变方法
protected virtual void OnPathChanged(string oldPath, string newPath)
{
}
//强制转换方法
private static object CoercePath(DependencyObject d, object value)
{
FileSelector target = (FileSelector)d;
string desiredPath = (string)value;
if (desiredPath == null)
desiredPath = string.Empty;
else
{
desiredPath = desiredPath.Trim();
//试图去掉最外的引号(如果有的话)
while (desiredPath.Length >= 2 && desiredPath[0] == '"' &&desiredPath[desiredPath.Length - 1] == '"')
desiredPath = desiredPath.Substring(1,
desiredPath.Length - 2);
}
return desiredPath;
}
#endregion
返回目录
3. 添加路由事件
首先通过EventManager.RegisterRoutedEvent方法注册事件,然后添加CLR事件的包装。
注意对于属性值改变的路由事件,WPF有RoutedPropertyChangedEventHandler和RoutedPropertyChangedEventArgs泛型类型做辅助支持,因此定义此类路由事件变得非常方便。(关于这两个类型,可以参考:WPF:使用RoutedPropertyChangedEventArgs和RoutedPropertyChangedEventHandler类型)
#region PathChanged路由事件
//注册
public static readonly RoutedEvent PathChangedEvent =EventManager.RegisterRoutedEvent("PathChanged",
RoutingStrategy.Bubble,typeof(RoutedPropertyChangedEventHandler<string>), typeof(FileSelector));
//CLR事件包装
public event RoutedPropertyChangedEventHandler<string> PathChanged
{
add {
AddHandler(PathChangedEvent, value);
}
remove {
RemoveHandler(PathChangedEvent, value);
}
}
#endregion
接着别忘了在上面的依赖属性改变回调方法中添加调用路由事件的代码:
protected virtual void OnPathChanged(string oldPath, string newPath)
{
RaiseEvent(new RoutedPropertyChangedEventArgs<string>(oldPath,
newPath, PathChangedEvent));
}
返回目录
4. 控件模板中的已命名对象
当用户选择一个文件后,文件路径会被显示在TextBox中的,既然控件是Lookless的,那么不能强制定义一个模板(只能定义一个默认模板,用户可以改变模板)。控件中需要的类型通过“已命名对象”的概念来得到。也就是要求用户自定义模板的时候必须提供一个什么样的类型。已命名对象一般以PART_xxx命名。
我们的控件,需要一个TextBox,我们要求它在模板中命名为:PART_Text。那么添加[TemplatePart]特性在类型定义上:
[TemplatePart(Name = "PART_Text",
Type = typeof(TextBox))]
public class FileSelector : Control
接着改写FrameworkElement.OnApplyTemplate()方法将控件模板中要求的控件提取出来。注意不要改写成OnTemplateChanged方法,两种容易混淆。
#region 控件模板
TextBox tbx;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
tbx = Template.FindName("PART_Text", this) as TextBox;
}
#endregion
最后不要忘了在依赖属性改变的方法中加入更新内部TextBox控件的代码:
protected virtual void OnPathChanged(string oldPath, string newPath)
{
//调用路由事件
RaiseEvent(new RoutedPropertyChangedEventArgs<string>(oldPath,
newPath, PathChangedEvent));
//更新TextBox的显示值
if (tbx != null)
tbx.Text = newPath;
}
返回目录
5. 对话框处理
接下来定义对话框处理的辅助函数,为后面命令执行做准备。
定义三个成员。PredefinedExts定义支持显示的扩展名,格式是”a,b,c…”多个扩展名用逗号分开,第一个会是默认显示的扩展名。当然不管有没有定义扩展名,程序都会在后面加上“所有文件”这种类型。
其他两个成员分别从PredefinedExts属性提取OpenFileDialog.Filter属性值和打开并返回对话框的结果。
(这段代码执行读者可以跳过,理解意义就可以)
#region 对话框处理
public string PredefinedExts
{ get; set;
}
string GetFilterString(out string first)
{
first = null;
var exts = PredefinedExts.Split(new string[]
{ "," },StringSplitOptions.RemoveEmptyEntries);
string head = string.Empty;
if (exts.Length != 0)
{
var trim = exts.Where(s => s.Trim() != String.Empty);
first = trim.FirstOrDefault();
head = String.Join("|",
trim.Select(s => String.Format("{0}文件|*.{1}",
s.ToUpper(),
s.ToLower())));
}
if (head != String.Empty)
head += "|";
return head + "所有文件|*.*";
}
string OpenDialog()
{
var dlg = new OpenFileDialog();
string defExt;
dlg.Filter = GetFilterString(out defExt);
dlg.DefaultExt = defExt;
if (dlg.ShowDialog() == true)
return dlg.FileName;
return null;
}
#endregion
返回目录
6. 添加路由命令
定义好了上面的对话框辅助支持,下面的命令执行就方便多了。
注册路由命令,然后用CommandManager.RegisterClassCommandBinding方法注册类型级别的命令执行(通过命令绑定CommandBinding类型)。
关于CommandManager的RegisterClassCommandBinding和RegisterClassInputBinding方法可以参考:WPF:使用CommandManager.RegisterClassCommandBinding和RegisterClassInputBinding方法。
#region 命令
//定义路由命令
public static RoutedUICommand BrowseCommand
= new RoutedUICommand("浏览", "BrowseCommand", typeof(FileSelector));
//初始化命令逻辑
static void InitCommands()
{
CommandManager.RegisterClassCommandBinding(typeof(FileSelector),
new CommandBinding(BrowseCommand,
BrowseCommandExecuted));
}
//命令执行
static void BrowseCommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
var fs = sender as FileSelector;
if (fs != null)
fs.OnBrowse();
}
//类内非静态命令执行
protected virtual void OnBrowse()
{
Path = OpenDialog();
}
#endregion
接着需要把命令初始化方法InitCommands在类型静态构造方法中调用:
static FileSelector()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(FileSelector), newFrameworkPropertyMetadata(typeof(FileSelector)));
//初始化命令
InitCommands();
}
返回目录
7. 添加拖放支持
最后程序支持路径的拖放支持。
关于WPF拖放操作,这里就不多讲理论,读者可以参考:WPF IDataObject,拖放操作,剪切板操作这篇文章。
代码:
#region 拖放
//初始化拖放支持
void InitDragDrop()
{
AllowDrop = true;
Drop += new DragEventHandler(FileSelector_Drop);
DragEnter += new DragEventHandler(FileSelector_DragEnter);
}
void FileSelector_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effects = DragDropEffects.Copy;
else
e.Effects = DragDropEffects.None;
}
void FileSelector_Drop(object sender, DragEventArgs e)
{
Path = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
}
#endregion
然后在构造函数中调用初始化拖放操作的方法:
public FileSelector()
{
InitDragDrop();
}
返回目录
8. 定义默认控件模板
创建好了我们的类型FileSelector.cs,接着需要创建控件的默认控件模板。打开Themes文件夹中的Generic.xaml

加入默认模板:
<Style TargetType="{x:Type local:FileSelector}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:FileSelector}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel Margin="{TemplateBinding Padding}"
VerticalAlignment="Top">
<Button DockPanel.Dock="Right"
Command="{x:Staticlocal:FileSelector.BrowseCommand}"
Content="{Binding Path=Text,Source={x:Staticlocal:FileSelector.BrowseCommand}}"/>
<TextBox Name="PART_Text"/>
</DockPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
:D