创建和使用自定义 WPF 控件






4.71/5 (39投票s)
2007年3月1日
8分钟阅读

246882

3823
创建和使用自定义 WPF 控件
引言
每当出现一项新技术时,我个人认为,掌握其功能的最佳方法是尝试用另一种语言重现你已经做过的事情。为此,本文将介绍如何在 WPF 中创建一个自定义控件,该控件将引发自定义事件。然后,该自定义控件将被放置在一个标准的 XAML 窗口中,并订阅该自定义控件的事件。简而言之就是这样。但在此过程中,我希望向您指出几个方面。
拟议的结构如下
- 关于 XAML / WPF 的说明
- 自定义控件本身
- 关于 .NET 3.0 中事件的说明
- 在 XAML 窗口中引用外部自定义控件
InitializeComponent()
方法到底在哪里?- 演示应用程序的屏幕截图
关于 XAML / WPF 的说明
从某种意义上说,WPF 应用程序与 ASP.NET 应用程序非常相似;可能(或不)有一个 XAML 文件,还有一个代码隐藏文件,其中 XAML 文件包含窗口/控件的渲染,而代码隐藏文件处理所有过程式代码。这是一种开发模式。但还有另一种方法。可以在 XAML 中完成的任何事情也可以完全在代码隐藏(C#/VB)中完成。为此,我创建的自定义控件完全是通过代码创建的。至于这个例子,这似乎更合理。
自定义控件本身
由于颜色选择器在 codeproject 上似乎非常受欢迎,我想那就做一个吧。这是一个在单独的 Visual Studio 2005 项目中创建的单个控件,并且是整个解决方案的一部分。我这样做是因为这是我们所有人使用第三方控件最常见的方式。我们得到一个 DLL 并对其进行引用。事实上,我选择了这条路,因为引用控件的 XAML 指令会根据它是内部类还是外部 DLL 而略有不同。最常见的情况是,我想它将是一个第三方外部 DLL 被引用。如果您不理解这一点,请不要担心。稍后会有更多关于它的内容。
那么,事不宜迟,让我们看看代码
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace ColorPicker
{
#region ColorPickerControl CLASS
/// <summary>
/// A simple color picker control, with a custom event that uses
/// the standard RoutedEventArgs.
/// <br/>
/// NOTE: I also tried to create a custom event with custom inherited
/// RoutedEventArgs, but this didn't seem to work,
/// so this event is commented out. But if anyone knows how to do this
/// please let me know, as far as I know
/// I am doing everything correctly
/// </summary>
public class ColorPickerControl : ListBox
{
#region InstanceFields
//A RoutedEvent using standard RoutedEventArgs, event declaration
//The actual event routing
public static readonly RoutedEvent NewColorEvent =
EventManager.RegisterRoutedEvent
("NewColor", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ColorPickerControl));
//A RoutedEvent using standard custom ColorRoutedEventArgs,
//event declaration
////the event handler delegate
public delegate void NewColorCustomEventHandler
(object sender, ColorRoutedEventArgs e);
////The actual event routing
public static readonly RoutedEvent NewColorCustomEvent =
EventManager.RegisterRoutedEvent
("NewColorCustom", RoutingStrategy.Bubble,
typeof(NewColorCustomEventHandler),
typeof(ColorPickerControl));
//******************************************************************
//string array or colors
private string[] _sColors =
{
"Black", "Brown", "DarkGreen", "MidnightBlue",
"Navy", "DarkBlue", "Indigo", "DimGray",
"DarkRed", "OrangeRed", "Olive", "Green",
"Teal", "Blue", "SlateGray", "Gray",
"Red", "Orange", "YellowGreen", "SeaGreen",
"Aqua", "LightBlue", "Violet", "DarkGray",
"Pink", "Gold", "Yellow", "Lime",
"Turquoise", "SkyBlue", "Plum", "LightGray",
"LightPink", "Tan", "LightYellow", "LightGreen",
"LightCyan", "LightSkyBlue", "Lavender", "White"
};
#endregion
#region Constructor
/// <summary>
/// Constructor for ColorPickerControl, which is a ListBox subclass
/// </summary>
public ColorPickerControl()
{
// Define a template for the Items,
// used the lazy FrameworkElementFactory method
FrameworkElementFactory fGrid = new
FrameworkElementFactory
(typeof(System.Windows.Controls.Primitives.UniformGrid));
fGrid.SetValue
(System.Windows.Controls.Primitives.UniformGrid.ColumnsProperty,10);
// update the ListBox ItemsPanel with the new
// ItemsPanelTemplate just created
ItemsPanel = new ItemsPanelTemplate(fGrid);
// Create individual items
foreach (string clr in _sColors)
{
// Creat bounding rectangle for items data
Rectangle rItem = new Rectangle();
rItem.Width = 10;
rItem.Height = 10;
rItem.Margin = new Thickness(1);
rItem.Fill =
(Brush)typeof(Brushes).GetProperty(clr).GetValue(null, null);
//add rectangle to ListBox Items
Items.Add(rItem);
//add a tooltip
ToolTip t = new ToolTip();
t.Content = clr;
rItem.ToolTip = t;
}
//Indicate that SelectedValue is Fill property of Rectangle item.
//Kind of like an XPath query,
//this is the string name of the property
//to use as the selected item value from the actual item data.
//The item data being a Rectangle in this case
SelectedValuePath = "Fill";
}
#endregion
#region Events
// Provide CLR accessors for the event
public event RoutedEventHandler NewColor
{
add { AddHandler(NewColorEvent, value); }
remove { RemoveHandler(NewColorEvent, value); }
}
// This method raises the NewColor event
private void RaiseNewColorEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(NewColorEvent);
RaiseEvent(newEventArgs);
}
// Provide CLR accessors for the event
public event NewColorCustomEventHandler NewColorCustom
{
add { AddHandler(NewColorCustomEvent, value); }
remove { RemoveHandler(NewColorCustomEvent, value); }
}
// This method raises the NewColorCustom event
private void RaiseNewColorCustomEvent()
{
ToolTip t = (ToolTip)(SelectedItem as Rectangle).ToolTip;
ColorRoutedEventArgs newEventArgs =
new ColorRoutedEventArgs(t.Content.ToString());
newEventArgs.RoutedEvent = ColorPickerControl.NewColorCustomEvent;
RaiseEvent(newEventArgs);
}
//*******************************************************************
#endregion
#region Overrides
/// <summary>
/// Overrides the OnSelectionChanged ListBox inherited method, and
/// raises the NewColorEvent
/// </summary>
/// <param name="e">the event args</param>
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
//raise the event with standard RoutedEventArgs event args
RaiseNewColorEvent();
//raise the event with the custom ColorRoutedEventArgs event args
RaiseNewColorCustomEvent();
//****************************************************************
}
#endregion
}
#endregion
#region ColorRoutedEventArgs CLASS
/// <summary>
/// ColorRoutedEventArgs : a custom event argument class
/// </summary>
public class ColorRoutedEventArgs : RoutedEventArgs
{
#region Instance fields
private string _ColorName = "";
#endregion
#region Constructor
/// <summary>
/// Constructs a new ColorRoutedEventArgs object
/// using the parameters provided
/// </summary>
/// <param name="clrName">the color name string</param>
public ColorRoutedEventArgs(string clrName)
{
this._ColorName = clrName;
}
#endregion
#region Public properties
/// <summary>
/// Gets the stored color name
/// </summary>
public string ColorName
{
get { return _ColorName; }
}
#endregion
}
#endregion
}
可以看出,这都是相当正常的 C# .NET 3.0 代码(也就是说,如果您熟悉 .NET 3.0 的东西,我只是在学习)。我想特别关注构造函数。让我们逐部分看看那一部分。
// Define a template for the Items, use the lazy FrameworkElementFactory
// method
FrameworkElementFactory fGrid = new FrameworkElementFactory
(typeof(System.Windows.Controls.Primitives.UniformGrid));
fGrid.SetValue
(System.Windows.Controls.Primitives.UniformGrid.ColumnsProperty, 10);
//update the ListBox ItemsPanel with the new ItemsPanelTemplate just created
ItemsPanel = new ItemsPanelTemplate(fGrid);
FrameworkElementFactory
类是一种以编程方式创建模板的方法,这些模板是 FrameworkTemplate
的子类,例如 ControlTemplate
或 DataTemplate
。这相当于在 XAML 标记中创建一个 <ControlTemplate>
标签。因此,我们在这里实际上要做的是说,内部继承的 Listbox.ItemsPanel
将应用一个模板,该模板将是一个具有 10 列的统一网格布局。
// Create individual items
foreach (string clr in _sColors)
{
// Create bounding rectangle for items data
Rectangle rItem = new Rectangle();
rItem.Width = 10;
rItem.Height = 10;
rItem.Margin = new Thickness(1);
rItem.Fill =
(Brush)typeof(Brushes).GetProperty(clr).GetValue(null, null);
//add rectangle to ListBox Items
Items.Add(rItem);
//add a tooltip
ToolTip t = new ToolTip();
t.Content = clr;
rItem.ToolTip = t;
}
代码的这一部分负责创建单个 ListItem
内容。那么,这是怎么回事?嗯,这些项被创建为 Rectangle
对象(是的,没错,矩形)。然后用画笔颜色填充矩形,然后为矩形应用 ToolTip
。
//Indicate that SelectedValue is Fill property of Rectangle item.
//Kind of like an XPath query, this is the string name of the property
//to use as the selected item value from the actual item data. The item
//data being a Rectangle in this case
SelectedValuePath = "Fill";
最后,SelectedValuePath
被告知应映射到 SelectedValue
的属性是“Fill
”。这意味着,无论何时我们获取 SelectedValue
,它所对应的对象都是一个Fill
”是 Brush
类型,除非将其转换为另一种对象类型。这不是很疯狂吗?WPF 真是令人惊叹,确实如此。
最敏锐的读者会注意到,控件的代码包含 2 个事件,其中一个被注释掉了。稍后将对此进行更多说明。
关于 .NET 3.0 中事件的说明
微软就是微软,不想让我们过于安逸,所以他们似乎彻底改革了所有东西。即使是像事件这样的小事,也与 .NET 2.0 中的情况不同了。
下面的代码片段代表了创建事件的新 .NET 3.0 方法。
我创建了一个名为 NewColorEvent
的自定义事件,所以让我们看看如何定义一个事件。
//A RoutedEvent using standard RoutedEventArgs, event declaration
//The actual event routing
public static readonly RoutedEvent NewColorEvent =
EventManager.RegisterRoutedEvent("NewColor", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ColorPickerControl));
我们还需要做什么?嗯,我们需要创建用于订阅/取消订阅事件的访问器。
// Provide CLR accessors for the event
public event RoutedEventHandler NewColor
{
add { AddHandler(NewColorEvent, value); }
remove { RemoveHandler(NewColorEvent, value); }
}
我们还需要一个引发事件的方法,例如
// This method raises the NewColor event
private void RaiseNewColorEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(NewColorEvent);
RaiseEvent(newEventArgs);
}
最后,我们需要在某个地方引发该事件。我选择在重写继承的 ListBox
OnSelectionChanged
方法中执行此操作,如下所示。
//raise the event with standard RoutedEventArgs event args
RaiseNewColorEvent();
这就是创建自定义控件中的自定义事件的所有内容。我们现在只需要将控件放置在某个地方并订阅这个可爱的新事件。
在 XAML 窗口中引用外部自定义控件
好的,您认为您知道如何引用包含自定义控件的 DLL。您只需在工具栏上创建一个新选项卡,然后将 DLL 和其中包含的任何控件浏览到工具栏。对吗?嗯,这似乎不起作用。那么您必须做什么?是添加项目引用(右键单击“引用”)并浏览到包含自定义控件的程序集(DLL),在本例中只有一个。
所以这是第一步。然后我们实际上想在 XAML 窗口中使用自定义控件。所以我们必须在 XAML 窗口的根元素中添加一个额外的指令。需要添加的重要部分如下。
如果使用您拥有源代码的代码文件
xmlns:src="clr-namespace:NAMESPACE_NEEDED"
如果使用外部 DLL
xmlns:src="clr-namespace:NAMESPACE_NEEDED;assembly=ASSEMBLYNAME_NEEDED"
因此,对于附加的示例,其中有一个外部 DLL 包含我们需要使用的控件,根元素将更改为以下内容。
<Window x:Class="ColorControlApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:ColorPicker;assembly=ColorPickerControl"
Title="ColorControlApp" Height="300" Width="300">
这是故事的一部分。我们已经成功地在 XAML 中引用了外部用户控件,但我们仍然没有标记中的控件实例。那么我们该怎么做呢?嗯,我们做类似这样的事情。
<src:ColorPickerControl HorizontalAlignment="Center"
VerticalAlignment="Center" Name="lstColorPicker"/>
好的,我们已经完成了 XAML 部分,但是代码隐藏文件呢?我们仍然需要完成那部分。那么它是怎么做的呢?嗯,幸运的是,那部分更容易。它只是一个普通的 using
语句,我们需要的。
using ColorPicker;
InitializeComponent() 方法到底在哪里?
现在我们确实拥有了一个完全引用的外部 DLL,其中包含一个用户控件,而我们现在在 XAML 窗口中有了它的实例,并且由于刚才执行的两个步骤,代码隐藏逻辑也知道了它。
但是,XAML 的代码隐藏文件是如何知道 XAML 文件中包含的用户控件的呢?答案是,当 Visual Studio 编译项目时,它会创建一个新的生成源文件,该文件放置在 DEBUG\OBJ 或 RELEASE\OBJ(取决于您如何编译项目)中。此文件的名称与当前 XAML 窗口文件的名称相同,但扩展名将是 c# 的 g.cs 或 vb 的 g.vb。
以下屏幕截图显示了附加项目的情况。
您可以看到那里有一个 Window1.g.cs(因为我使用 C#)文件。
那么这一切是怎么回事?嗯,让我们看看。
//---------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:2.0.50727.42
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//---------------------------------------------------------------------------
using ColorPicker;
using System;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Media.TextFormatting;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ColorControlApp
{
/// <summary>
/// Window1
/// </summary>
public partial class Window1 :
System.Windows.Window, System.Windows.Markup.IComponentConnector
{
internal System.Windows.Controls.StackPanel Stack;
internal System.Windows.Controls.Label lblColor;
internal ColorPicker.ColorPickerControl lstColorPicker;
private bool _contentLoaded;
/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponent()
{
if (_contentLoaded)
{
return;
}
_contentLoaded = true;
System.Uri resourceLocater =
new System.Uri("/ColorControlApp;component/window1.xaml",
System.UriKind.Relative);
System.Windows.Application.LoadComponent(this, resourceLocater);
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.ComponentModel.EditorBrowsableAttribute
(System.ComponentModel.EditorBrowsableState.Never)]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute
("Microsoft.Design",
"CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
void System.Windows.Markup.IComponentConnector.Connect
(int connectionId, object target)
{
switch (connectionId)
{
case 1:
this.Stack = ((System.Windows.Controls.StackPanel)(target));
return;
case 2:
this.lblColor = ((System.Windows.Controls.Label)(target));
return;
case 3:
this.lstColorPicker = ((ColorPicker.ColorPickerControl)(target));
return;
}
this._contentLoaded = true;
}
}
}
可以看出,此源文件提供了缺失的部分,最明显的是 InitializeComponent()
方法,并且还注意到有几个实例字段代表 XAML 文件中的组件。这就是代码隐藏和 XAML 文件如何编译以形成一个包含所有必需信息的程序集。
附加应用程序的演示
我最后应该展示的是运行的应用程序。那部分可能不那么重要,因为我真正想分享的是概念。
但为了完整起见,这里有一个屏幕截图。
请记住,ColorPicker
实际上只是一个专门的 ListBox
。相当令人印象深刻,不是吗?
就是这样
嗯,尽管我们只创建了一个简单的控件并在单个 XAML 页面中使用它,但我希望您能看到这里涵盖了不少核心概念。
那么你觉得呢?
我想问一下,如果您喜欢这篇文章,请投票支持它,因为它能让我知道文章的水平是否合适。
结论
我非常喜欢编写这篇文章。希望您喜欢它。我认为当您有时间进行 XAML / WPF 类型应用程序时,它将对您有很大帮助。我刚开始接触 XAML,我坚信它将彻底改变我们都将看到的应用程序类型。
历史
- v1.1 2007/03/01:修复了带有自定义事件参数的自定义事件问题。非常感谢 Steve Maier。
- v1.0 2007/03/01:初始发布