65.9K
CodeProject 正在变化。 阅读更多。
Home

创建和使用自定义 WPF 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (39投票s)

2007年3月1日

8分钟阅读

viewsIcon

246882

downloadIcon

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\OBJRELEASE\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:初始发布
© . All rights reserved.