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

如何将任意内容嵌入 WPF 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (19投票s)

2010 年 5 月 19 日

Apache

5分钟阅读

viewsIcon

103313

downloadIcon

1727

许多 WPF 控件都可以在内部显示任意 XAML。我如何才能为我的自定义控件实现这一点?

引言

许多 WPF 控件(例如 Button、Border 等)都可以在内部显示任意 XAML

<Button>any XAML here...</Button>

如何创建具有此功能的自定义控件?也就是说,如果我这样写:

<MyControl><TextBox Text="edit me" /></MyControl>

然后我的控件中就会出现一个文本框?本文将演示如何正确地做到这一点,以及几种错误的方法及其原因。

粗略尝试:用户控件

我的第一次尝试是创建一个用户控件并尝试向其中添加一个 ContentPresenter。不幸的是,这不起作用,原因如下。

我向项目中添加了一个新的用户控件,并在其 XAML 文件中指定了内容

<UserControl x:Class="MyNamespace.MyControl" ... >
    <StackPanel Orientation="Vertical">
        <Label>Foo</Label>        
        <Label>Bar</Label>
    </StackPanel>
</UserControl> 

然后我尝试像这样在我的窗口中使用该控件:

<Window xmlns:local="clr-namespace:MyNamespace" ...>
    <local:MyControl />
</Window> 

到目前为止一切看起来都很好。现在有人告诉我 ContentPresenter 类可以在自定义控件中显示用户内容,所以我将其放入我的用户控件的 XAML 定义中:

<!-- don't do this at home --> 
<!-- MyControl.xaml --> 
<UserControl x:Class="MyNamespace.MyControl" ... >
    <StackPanel Orientation="Vertical">
        <Label>Foo</Label>        
        <ContentPresenter />
        <Label>Bar</Label>
    </StackPanel>
</UserControl>
...
<!-- MyWindow.xaml --> 
<Window xmlns:local="clr-namespace:MyNamespace" ...>
    <local:MyControl>User supplied content here</local:MyControl>
</Window> 

这样做不起作用,因为我们最终会在用户控件 XAML 内部和 Window XAML 内部两次定义用户控件的 Content 属性。Window XAML 在这场争夺中获胜,你唯一能看到的是文本User supplied content hereFooBar 都不会显示,ContentPresenter 也没有起到任何作用。

粗制滥造的尝试

事实证明 ContentPresenter 只能在 <ControlTemplate> 中使用。所以,我创建了一个名为 QuickAndDirtyAttempt 的项目,其中包含一个 Decorator 控件(是的,我知道,WPF 已经有一个 Decorator 类型,但我实在想不出更好的名字)。为了以一种强硬的方式达到我们的目标,我只是为用户控件分配了一个控件模板。与此 MSDN 文章相反,将控件模板应用于用户控件是可能的。

<UserControl x:Class="QuickAndDirtyAttempt.Decorator" ...=""
  <UserControl.Template>
    <ControlTemplate TargetType="{x:Type local:Decorator}">
      <StackPanel Orientation="Vertical">
        <Label>Foo</Label>
        <ContentPresenter />
        <Label>Bar</Label>
      </StackPanel>
    </ControlTemplate>
  </UserControl.Template>
</UserControl>
注意模板上的 TargetType 属性:没有它,项目将愉快地编译,但 ContentPresenter 将不起作用。
<Window ... >
    <StackPanel Orientation="Vertical">
        <local:Decorator>
            <Label Background="Wheat">User supplied content here</Label>
        </local:Decorator>
    </StackPanel>
</Window> 
Screenshot of the quick and dirty attempt

成功了!但我们可以做得更好。

为什么粗制滥造的解决方案是粗制的

首先,我们在这里并不真正需要自定义控件。我们可以直接使用一个现有控件,比如 ContentControl,并为其提供一个自定义控件模板。在 80% 的情况下,这是正确的解决方案。

WPF 的核心在于将“外观和感觉”与呈现逻辑分离。WPF 控件被称为“无外观”。例如,复选框控件只处理复选框的状态(开、关、未定义)以及这些状态之间的转换。它定义复选框的外观。系统为您提供了一个默认的标准外观,但如果您想要一个带有圆形按钮和其下方文本的复选框,您只需要提供一个备用控件模板。

如果您需要处理额外的逻辑,例如用户输入、可绑定属性等,那么您将需要一个自定义控件。

下面我将演示两种解决方案:一种是使用 ContentControl 的控件模板,另一种是使用功能齐全的自定义控件。

使用 ContentControl

我们在应用程序资源中定义我们的自定义模板:

<Application ...>
    <Application.Resources>
        <ControlTemplate x:Key="Decorator" TargetType="ContentControl">
            <StackPanel Orientation="Vertical" >
                <Label>Foo</Label>
                <ContentPresenter />
                <Label>Bar</Label>
            </StackPanel>
        </ControlTemplate>
    </Application.Resources>
</Application> 

然后我们在窗口中使用 ContentControl,如下所示指定模板:

<Window ... >
    <StackPanel Orientation="Vertical">
        <ContentControl Template="{StaticResource Decorator}">
            <Label Background="Yellow">User supplied content here</Label>
        </ContentControl>
    </StackPanel>
</Window> 

我确实希望模板名称的语法不那么冗长,但我认为这里无法简化。如果我们想同时指定控件的其他属性以及模板,我们可以定义一个自定义控件样式,而不是直接设置模板。然后该样式会有一个模板属性的 setter。但是,我认为在这种特定情况下,这将是不必要的复杂化。

Screenshot of using ContentControl

使用自定义控件

自定义控件是自定义 WPF 最重量级的方式,并且应该仅在必要时使用。如果您认为需要自定义控件,请先尝试找到一个满足您需求的现有控件。请记住,WPF 控件是纯逻辑加上默认的外观和感觉模板(在 WPF 中称为 <ControlTempalte>)。如Charles Petzold 的这篇文章所示,此模板可以进行大幅度的修改。

.

如果您的自定义超出了外观和感觉的范畴,并且涉及一些呈现逻辑,那么自定义控件可能是个不错的选择。但即使那样,派生您的控件自现有的控件(如 ContentControl)也可能是个好主意。通常,您的自定义控件将驻留在它们自己的程序集中,并且该程序集将被用户应用程序引用。

我们将创建一个名为 HeaderFooterControl 的控件,它定义了两个自定义属性:Header 和 Footer。您可以使用 Decorator 如下:

<my:HeaderFooterControl>
    <my:HeaderFooterControl.Header>
        header content here
    </my:HeaderFooterControl.Header>
    <my:HeaderFooterControl.Footer>
        footer content here
    </my:HeaderFooterControl.Footer>
    main control content here
</my:HeaderFooterControl> 

右键单击您的项目,然后选择“添加”?“添加新项”?WPF?“自定义控件(WPF)” Visual Studio 会为您的控件生成样板代码。这包括骨架 .cs 文件和 generic.xaml 中的默认控件模板。

Adding custom WPF control in Visual Studio

默认情况下,该控件继承自 WPF Control 类,但我们可以做得更好,继承自 HeaderedContentControl。它拥有我们所需的一切,除了页脚。通常,您应该尝试从最适合的基本 WPF 类派生您的控件(此图来自此处

WPF custom control hierarchy

下一步是为 Footer 定义一个依赖属性:快捷方式是右键单击您的 .cs 文件,然后选择 Insert snippet ? NetFX3d ? Dependency Property。该属性的类型为 object,因为页脚可以包含任何用户定义的内容。

public static readonly DependencyProperty FooterProperty =
   DependencyProperty.Register("Footer", typeof(object), 
	typeof(HeaderFooterControl), new UIPropertyMetadata(null));

然后我们在 Generic.xaml 中定义我们的控件模板。我们使用 ContentPresenter 来显示来自页眉、页脚和主控件体的任意用户内容。

<Style TargetType="{x:Type local:HeaderFooterControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:HeaderFooterControl}">
                <StackPanel Orientation="Vertical">
                    <ContentPresenter ContentSource="Header" />
                    <ContentPresenter />
                    <ContentPresenter ContentSource="Footer" />
                </StackPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style> 

我们还可以通过将以下属性应用于控件程序集来将 C# 命名空间映射到 XML 命名空间:

[assembly:XmlnsDefinition
	(http://ikriv.com/xaml/samples/DisplayingContent/CustomControl/MyControls, 
	"MyControls")]

现在,在用户应用程序中,我们可以引用我们的控件库并在 Window1.xaml 中这样说:

<Window xmlns:my=
"http://ikriv.com/xaml/samples/DisplayingContent/CustomControl/MyControls" ...>
    <my:HeaderFooterControl>...&lgt;/my:HeaderFooterControl>

这是我们使用自定义控件的应用程序的屏幕截图:

Screenshot of using custom control

选择哪个选项

我描述了三种在控件中嵌入用户定义内容的不同选项:

  • 带有 UserControl 和 ControlTempalte 的粗制滥造选项
  • 使用 ContentControl 和 ControlTemplate 的更好选项
  • 带有自定义 WPF 控件的重量级选项

您很少会使用粗制滥造的选项 - 也许只作为临时措施,但请记住,今天的临时措施往往会成为明天的遗留代码。如果所有您需要的是显示一些东西,请使用 ContentControl。如果您有自己的呈现逻辑,请使用自定义控件。例如,您可能希望通过设置 HeaderVisible 和 FooterVisible 属性来显示/隐藏页眉或页脚,并使用这些属性在某些触发器中。

源代码

可以使用此处下载包含所有相关源代码的 Visual Studio 2008 解决方案。

本文最初发布在我的网站上。

历史

  • 2010 年 5 月 19 日:初始帖子
© . All rights reserved.