WPF 世界入门






4.12/5 (13投票s)
涵盖从XAML布局到C#与XAML通信及数据绑定的基本概念。
引言
我是CodeProject文章提交方面的新手。老实说,我不太懂,花了几个小时才弄出那糟糕的格式。我没找到WPF的格式,我的C#格式也搞得一团糟。如果有人知道关于文章提交的好文章,或者好的文章工具(也许是一个离线WinForm之类的),我将不胜感激。
首先声明,我绝对不是WPF方面的专家(目前还不是)。然而,我是一个WPF的狂热用户,怀有深厚的热情和渴望来扩展我的知识。我最近(几周前)刚从WinForms的世界转到WPF。对于一个没有任何标记语言经验(没有XML、HTML等)的人来说,从WinForms到WPF的转变并非易事。仍有许多概念我尚未试图掌握。然而,在过去的几周里,我每天花费无数小时沉浸在WPF的文章和教程中。我有幸(以及不那么愉快地)浏览了像WPF绑定女王Bea、WPF大师Josh Smith和Sacha Barber等用户的一些精彩文章。我关注Josh和Sacha(CodeProject社区的早期WPF用户)已经有一段时间了,所以无疑非常喜欢他们的文章。
说了这么多,我写这篇文章的原因仅仅是因为我不得不花大量时间挖掘来理解许多博客博客上未能提及的WPF基本概念。甚至MSDN也让他们的示例难以理解,因为他们“假设”你已经理解了底层概念。
我希望这篇文章(以及Josh的:精彩WPF教程)能成为提及WPF基本概念的文章之一,这些基本概念应该足以让开发者们深入到他们新发现的爱好[WPF :) ]之中。
背景
(从Josh Smith的文章中摘录于此(精彩WPF教程),因为它包含了WPF开发所需的基础内容。我本人正在使用Visual C# 2008 Express与Vista。)
“要运行WPF应用程序,您需要.NET Framework 3.0或更高版本。Windows Vista默认安装了.NET Framework v3.0,所以如果您只有Windows XP SP2,则需要安装它。” - Josh Smith。
简单的XAML布局
我们开始吧!
我将从最基础的开始……如果您觉得开头太简单,请跳过到您可能感兴趣的主题。
我创建了一个名为“JumpStartWPF”的新WPF应用程序。创建后IDE看起来是这样的(直到目前为止我什么都没做)。
看看这里的默认XAML代码
<Window x:Class="JumpStartWPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
</Grid>
逐行分解
它开始声明一个新的Window
并设置一些属性。在这种情况下,它创建了对一些默认Microsoft命名空间(URL)的引用,设置了Window
的Title
文本,以及Height
和Width
。但请注意它做的第一个引用
x:Class="JumpStartWPF.Window1"
这段标记是默认为您创建的,它指向您主要XAML Window
和代码隐藏文件的命名空间/类。在这种情况下,JumpStartWPF
是我们创建的命名空间,而Window1
是WPF应用程序的默认类名。如果您在此标记中更改命名空间或类名,那么我们的主要XAML文件(Window1.xaml)将无法找到其代码隐藏文件(Window1.xaml.cs),并在代码隐藏文件的构造函数中引发错误。
您可能想知道代码隐藏是什么意思。在WPF世界中,代码隐藏被虔诚地用作每个XAML对象后面的实际代码文件。在这种情况下,我们主要的Window
的XAML文件是Window1.xaml。每个XAML文件都需要一个代码隐藏文件才能正常工作,所以我们的文件简单地命名为Window1.xaml,并在代码隐藏文件名后附加.cs(因为我正在使用C#)。
接下来,您会看到Grid
的开始/结束标签。它基本上是其他控件(包括其他Panel
)的容器,就像在WinForms世界中,GroupBox
/DockPanel
/等通常用作Container
一样。
XAML有很多不同的方式来布局您的UI。您可能会花费几天甚至几周来决定应用程序的基础Panel
(Container
)是什么。我个人已经确定使用Grid
。原因是,我认为我对Grid
的控制力最强,因为使用Grid
,您可以为整个窗口定义行/列,然后将其他面板适当地放置在这些新定义的行/列中。
让我们做一个简单但有效的例子。
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
</Grid>
好的,让我们一步步来。
首先,我将Grid的“ShowGridLines
”属性设置为“True
”。我认为这仅在调试/布局目的时有用……它所做的只是在 said [Grid
] 面板的每个列和行分隔处绘制虚线。
接下来,我开始定义窗口的一些简单布局。正如您已经看到的,许多WPF概念都很容易理解(当然,也有很多不容易理解的,请不要误会),但是语法会让一些人望而却步。在这种情况下,您可以清楚地看到我正在定义父级[Grid
]面板中的两个新Column
。这从声明Grid
.ColumnDefinitions
(并关闭它)开始,然后在ColumnDefinition
之间。我可以开始声明我希望窗口拥有的所有列。稍后您将看到这些如何发挥作用。
如果您现在运行此应用程序,您将看到一个带有中间虚线的窗口。很酷,嗯?是的,我知道……其实不然 :) 这只是开始!
更复杂的XAML布局
让我们以定义列所学到的知识为基础,扩展它以创建一个类似于美国橄榄球场的布局(感谢ShowGridLines
=“True
”)。为了节省时间/简化,我将不绘制每一码的标记行。我将只绘制端区行和50码线的行。在您看完这个快速示例后,您可以轻松地添加更多行来获取码标记。
现在您可能已经猜到,我们需要在Grid
布局中使用一个新概念,那就是Row
。
现在,如果您知道美国橄榄球场的样子,您会知道有一条水平线将球场一分为二(50码线)。好吧,我从来没见过有垂直线的,而这正是我们现在有的,所以让我们调整我们当前的布局来显示一条水平线!
为了让您不至于迷失,在继续之前我做了一件事。我将窗口高度设置为500,宽度保持为300。美国橄榄球场是长方形的,而不是正方形的,所以这能给我们带来更理想的外观。
现在,继续布局。
所以,您实际上可以将“Column
”替换为“Row
”在之前的定义中,就能得到想要的效果 :)
<Grid ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
</Grid>
现在运行它,您将看到我们想要的效果!一条水平线将球场一分为二。
现在,让我们再折腾一下,添加一些端区的水平线。
您可能会问自己,我们如何告诉行占据多少空间?端区线只需要占用一定比例的空间。如果我们像之前一样添加另一行,它会根据窗口的高度将每行平均分配。好吧,幸运的是,每个RowDefinition
都有一个Height
属性可以设置。
设置每行高度有几种方法。您可以按像素值、百分比值或自动填充值设置每行的高度。
像素值不言自明,您可以这样做,比如
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
像这样写宽(Column
)或高(Row
)的整数值将被解析为像素格式。这里使用的数字给每一行提供了这么多空间。我们刚刚声明了3行,并给它们至少50像素的空间。
现在,您可以花几分钟时间玩弄一下,大概就能找出给每行精确像素数以得到两个端区……但我们还有另一个杀手锏。
我之前提到过我们可以设置每行或每列的百分比值。那么,我不知道您怎么想,但我宁愿使用百分比来定义行高,而不是硬编码的像素值。声明百分比值的标记有两种写法。就像您习惯的数字一样,50%写成“50%”或“0.5”。这里也是一样,只是您可以将“%”符号替换为“*”。
回到我们的例子,让我们将其应用于我们的美国橄榄球场。
当前,以下内容给我们两行,每行获得相同的空间(代表50码线)。
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
现在,让我们创建端区,给每行一个百分比。您可能已经猜到,对于端区,我们至少需要三行。每个端区本身是一行(2行),我们需要另一行来容纳剩余的球场空间(所有100码)。
有两种方法可以做到这一点
旧方法(3行,每行空间相等)
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
新方法(3行,也每行空间相等)
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
请注意,在这种情况下,我们将每行的高度设置为“*”。这意味着我们希望Row
自动获得一个Height
,根据其他行/窗口高度来平均分配。这与不设置任何高度相同,但它引出了我们的下一个要求,即设置百分比值。
如果您仔细想想,我们可以绕过为端区行(第1/3行)设置一些棘手的值,而只需告诉中心行(100码部分)占据多少空间。
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
现在,您应该开始看到定义窗口布局的真正力量了。我们刚刚告诉中心行占据的空间是其他行的3倍,这取决于窗口的高度。
我们刚刚告诉Grid
创建三行,中心行占据的空间是其他行的3倍,这取决于窗口的高度。
如果您现在运行应用程序,您将看到我们有一个中心球场和两个端区。但是,我们的50码线在哪里?嗯,我们需要将中心球场分成两行才能达到想要的效果。
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
<RowDefinition Height="3*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
所以在这里,我告诉我的Grid
它有4行。中心的两行各占据比外围的两行多3倍的空间。这基本上是将我们的球场一分为二,然后将剩余的空间分配给外围的两行(因为它们没有设置宽度)。
既然您已经掌握了窍门,让我们快速添加一些列来设置边线。
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
<RowDefinition Height="3*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="10*" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
我只是告诉我的Grid
创建三列,让中心列占据的空间比外围列多10倍。
就这样!一个美国橄榄球场。
现在您已经有了带有行和列的Grid
,您如何使用它?Grid
面板允许其子对象设置它们在网格中出现的行/列。默认情况下,网格的所有子元素都出现在第0行第0列,但我们可以设置它。
下面的示例不适用于美国橄榄球主题。我们只是用我们现有的布局来添加一些新控件。我们当前的Grid
面板实际上比大多数基本应用程序有更多的行和列,所以这将是一个很好的学习经验。
没有菜单栏的应用程序是什么样的?让我们添加一个!
菜单系统的标记非常简单
<Menu>
</Menu>
如果您运行它,您可能会注意到它几乎看不见。嗯,菜单系统的关键词是“系统” :) 我们还没有为这个Menu
对象添加任何子元素。这同样容易,就像声明Menu
本身一样。
<Menu>
<MenuItem>
<MenuItem />
</MenuItem>
</Menu>
如果您运行它,您可能会注意到所有东西都挤在左上角。还记得我说过,默认情况下,Grid
面板容器的所有子元素都设置在第0行和第0列吗?我们现在正在实际看到这一点。幸运的是,Grid
的每个子元素都有机会明确设置它们想在网格中出现的位置。在我们的例子中,Menu
通常横跨Window
的顶部。但这包含多个列……我们如何实现呢?
同样,幸运的是,Grid
面板的子元素还有一个属性可以设置,它告诉Grid
这个子元素将跨越‘x’列(或行)。
让我们看看这个实际应用。
首先,我删除了父级Grid
面板的ShowGridLines
属性,因为它会弄乱UI :)
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem>
<MenuItem />
</MenuItem>
</Menu>
所以,您可以看到我在这里设置了Menu
对象的两个属性。首先,我告诉菜单要贴紧父容器的顶部。在这种情况下,我们的父级是Grid
,我们的默认行是“0”,所以幸运的是,这也恰好是Window
的顶部。
接下来,我告诉Menu
要跨越多少列。我们的橄榄球场有三列,所以我将Grid.ColumnSpan
属性设置为“3”。
让我们向这些菜单项添加一些文本。我们可以为此使用Header
内容属性。
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem Header="File">
<MenuItem Header="Exit"/>
</MenuItem>
</Menu>
现在运行它,您将看到一个真实应用程序的开始!很快,我们将让那个退出按钮在被Click
ed时做一些事情,但在此期间,让我们向应用程序添加其他控件,并充分利用所有那些列和行。
首先,让我们为窗口的边框列(左/右列)添加一些颜色。有很多控件可供您使用……我将使用Border
控件。
所以基本概念是将一个Border
放置在第0列,跨越我们球场的全部四行,并将其复制到第3列,沿着球场的另一侧。
<Border Grid.Column="0" Grid.RowSpan="4" />
<Border Grid.Column="3" Grid.RowSpan="4" />
Border
控件非常强大,它的功能远超本文的范围。只要知道我纯粹是使用它来在我需要添加颜色以放置控件的地方进行放置,而Border
控件在我脑海中脱颖而出。
所以,就像我们大多数其他控件一样,我们可以通过设置其属性来与Border
进行交互。您可能可以猜到它有一个Background
属性。所以,让我们设置它。
<Border Grid.Column="0" Grid.RowSpan="4" Background="DarkBlue" />
<Border Grid.Column="3" Grid.RowSpan="4" Background="DarkBlue" />
请注意,Border
的顶部与Menu
重叠?如果您有任何图形背景知识,您会知道最后渲染的对象会绘制在顶部。在我们的UI中,我们已经告诉Border
在声明Menu
之后进行绘制。
为了解决这个问题,我们只需要在Border
之后渲染Menu
。
<Border Grid.Column="0" Grid.RowSpan="4" Background="DarkBlue" />
<Border Grid.Column="3" Grid.RowSpan="4" Background="DarkBlue" />
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem Header="File">
<MenuItem Header="Exit"/>
</MenuItem>
</Menu>
太酷了!没用,但很酷! :) 再次声明,本文是为了通过学习语法和对象的实际用法来掌握WPF的概念。
总之,让我们向这个UI添加一些交互控件。
为了让您知道我们的目标……我最终希望这个UI的退出按钮能够工作。另外,我希望用户能够选择/输入颜色,然后Border
背景会变为该颜色。
让我们先添加用户选择颜色的控件。有几种方法可以做到这一点。我们可以提供一个ListBox
/ComboBox
,里面充满了预设颜色,或者我们可以让用户输入颜色。输入颜色对于本文来说有点太复杂了。
所以,让我们开始添加一个Label
来告诉用户这个ComboBox
将做什么,然后添加ComboBox
。
<Label Content="Choose a color" Grid.Row="2"
Grid.Column="1" VerticalAlignment="Center"
HorizontalAlignment="Center" Margin="0,-50,0,0"/>
让我们来看看这段丑陋的硬编码代码。
首先,我声明了一个Label
。我给它设置了Content
(Text
)为“Choose a color”。然后,hacky-ness就开始了。首先,我想让它在UI的中间,所以我把它设置在第三行(0基索引的第二行)和第二列。默认情况下,它位于单元格的左上角,所以我随后将VerticalAlignment
和HorizontalAlignment
设置为Center
。然后我决定实际上想让我的ComboBox
正好放在该单元格的中间,所以我应该把这个Label
往上移一点,让它在我颜色ComboBox
的上方。为了做到这一点,我引入了一个几乎所有控件对象都有的新属性,称为Margin
。
处理Margin
有几种方法。实际上是三种:)
Margin
表示当前控件与其父容器边缘之间的空间量。所以,在我的例子中,我将Label
放在Cell
的中心,然后告诉它我希望它的Margin
向左移动0,向上移动-50,向右移动0,向下移动0。
正如您可能猜到的,当您使用所有四个值作为Margin
值时,它们分别是左、上、右、下。我希望我的Label
出现在ComboBox
的上方,所以我将Top
坐标移动了-50像素(或者我也可以这样做:“0,0,0,50”)。
Margin
可以使用两种方式,只设置一个值。即,如果我这样做
Margin="50"
那么,它将在控件的四个边上应用50像素的Margin
。
使用Margin
的最后一种方法是只设置两个值。
Margin="0,50,0,0" <!--Applies a margin of 50 pixels on the top side of the control-->
Margin="50,0" <!--Applies a margin of 50 pixels on the left and right sides of the control-->
Margin="0,50" <!--Applies a margin of 50 pixels on the top and bottom sides of the control-->
Margin="50" <!--Applies a margin of 50 pixels on all sides of the control-->
现在,让我们添加我们的ComboBox
。
<ComboBox Grid.Row="2" Grid.Column="1"
Width="150" Height="25" SelectedIndex="0">
<ComboBoxItem Content="DarkRed" />
<ComboBoxItem Content="DarkGreen" />
<ComboBoxItem Content="DarkBlue" />
</ComboBox>
所以,首先,我们将ComboBox
设置在与Label
相同的单元格中。我们给它设置了150的Width
和25的Height
(如果我们不这样做,那么ComboBox
默认将占据单元格中的所有空间)。我们还设置了SelectedIndex
为0,所以我们总是向用户显示一些内容,而不是迫使他们点击它才能看到东西。此外,当我们需要将Border
绑定到ComboBox
中选定的颜色时,这将发挥作用。然后,我们给它添加了子元素。就像我们主要的Menu
对象一样,我们可以给ItemsControl
类型的对象添加子元素,而ComboBox
和ListBox
都是它的子类。
现在,看看我们一直在向其添加控件的那个单元格。我认为我们编写的用于定位它们的XAML相当丑陋。这就是我喜欢发挥创意的地方。到目前为止,您只看到了Grid
面板容器并使用了它。嗯,我个人只在窗口的初始布局中使用Grid
。我不喜欢像我们到目前为止那样在Grid
中放置控件。我更喜欢向Grid
添加子Panel
,然后向它们添加子元素。
我们现在将谈论我最喜欢的面板,那就是StackPanel
。StackPanel
非常简单,但功能强大且有用。
考虑一下纸牌接龙游戏。视觉上,它充满了布局(我只是为了押韵这么说的)。考虑一下我们到目前为止学到的。如果您想用Grid
来布局卡片集合,您要么疯了,要么很疯狂。
您需要硬编码的边距来分隔每个卡片集合以及一堆其他耗时的工作。
介绍,StackPanel
。当我看到纸牌接龙的图片时,我看到一个Grid
包含三行(第三行用于最后的胜利动画)和两列。牌堆在Row[0]Column[0]。花色牌堆在Row[0]Column[1]。行堆在Row[1]并跨越两列。胜利动画在Row[2]。
那么,纸牌接龙中是如何使用StackPanel
的呢?嗯,我之前提到的每个堆栈都将是一个StackPanel
。StackPanel
就是一个堆叠其子元素(或并排堆叠,取决于您希望如何显示)的面板。
与网格相反,StackPanel
中的元素紧密相连。在Grid
中,所有子元素都会相互重叠,这就是为什么我们需要设置边距使其看起来比较友好。
让我们将一个StackPanel
放在我们Label
和ComboBox
目前所在的单元格中;然后,让我们将Label
和ComboBox
移到StackPanel
里面。
<StackPanel Grid.Row="2" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Content="Choose a color" />
<ComboBox SelectedIndex="0">
<ComboBoxItem Content="DarkRed" />
<ComboBoxItem Content="DarkGreen" />
<ComboBoxItem Content="DarkBlue" />
</ComboBox>
</StackPanel>
哇!比设置每个控件的所有边距和对齐方式要干净得多。StackPanel
中的每个元素都继承了Grid.Row
、Grid.Column
和Vertical/Horizontal Alignment属性。所以,我们可以立即移除它们,让它们隐式设置。您仍然可能想做的一件事是在每个元素的顶部或底部放置一个Margin
以使其具有一些间距,但我在此示例中将其省略。
现在,我将把它们向上移动一行网格,使其更靠近窗口顶部。
<StackPanel Grid.Row="1" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Center">
看,这多么容易!我们只需更改一个属性就将整个面板集合向上移动了一个单元格。以前,当我们依赖Grid
面板时,我们需要为我们想移动的每个控件进行更改。想象一下,如果我们有一整套按钮和组合框的UI流程,如果它在Grid
中,管理起来该有多么烦人。
我想要介绍的最后一个关于布局部分的主题是StackPanel
的Orientation
属性。默认情况下,它设置为Vertical
。这仅仅意味着StackPanel
中的所有元素都堆叠在一起。如果我们设置Orientation
为Horizontal
,那么每个元素将并排堆叠。一个常见的用法可能是显示用户输入表单。当您需要收集用户的姓名/电子邮件/城市/州时,您通常会有一个垂直StackPanel
,里面包含水平StackPanel
。每个水平StackPanel
将是一个Label
,如“姓名:”,以及它右侧的TextBox
,用户可以在其中输入信息。然后,垂直StackPanel
将包含所有这些。
简单的XAML到XAML元素绑定
转到一个完全不同的主题,也就是我最喜欢的,数据绑定。对我来说,这就是驱动WPF的核心和灵魂。在WinForms中,对我来说唯一真正的绑定就是将集合控件绑定到后端数据源等。那么,在WPF中,从内容控件上的文本,到面板的背景,到整个菜单系统,到每个集合中的数据,几乎所有东西都可以被绑定。那么,什么是绑定?我给您一个真实的例子。
回顾我们当前应用程序(由于其含义不明,将保持匿名),我们知道我们希望Window
的Border
s根据ComboBox
中当前选中的内容改变颜色。对于您WinForms程序员来说,您可能会注册监听ComboBox
的SelectedIndexChanged
事件,找到SelectedIndex
的文本属性,将其转换为Color
,然后更改Border
的Color
。好吧,我们实际上可以在WPF中做到这一点,而无需接触任何C#代码,我们将通过绑定来实现。
所以,在这里我将介绍元素绑定的概念。同样,有很多种绑定。我们可以全部在C#中完成,我们可以部分在XAML中完成,另一部分在C#中完成,但我将全部在XAML中完成。基本思想是,我们希望Border
的Background
能够监视我们ComboBox
的SelectedItem.Content
属性。无论ComboBox
说什么,我们都希望Border
的背景成为该颜色。
首先,为了能够引用任何元素,我们需要给它一个名称。我使用元素的x:Name
属性来给它命名。
<ComboBox x:Name="ComboBoxColors" SelectedIndex="0" IsSynchronizedWithCurrentItem="True">
这里,我给我们的ComboBox
命名为ComboBoxColors
。这样其他元素就可以通过唯一名称引用这个ComboBox
。您可以随意命名……这与在C#中设置Name
属性相同。
您还会注意到我设置了一个名为IsSynchronizedWithCurrentItem
的属性为“True”。这是为了确保我们的ComboBox
始终与当前在视觉上显示的项同步。我在线上看到的所有内容都将此设置为True……我假设它默认是False,这没什么意义,但是……
现在,我们将进行我们的第一次绑定标记!
转到Border
元素,并完全删除Background
属性。
<Border Grid.Column="0" Grid.RowSpan="4" />
<Border Grid.Column="3" Grid.RowSpan="4" />
这是元素绑定的基本布局。我们需要声明Binding
的标记,它总是被{}
(花括号)包围。在里面,我们需要告诉我们要绑定什么。有很多种绑定,例如绑定到StaticResource
、DynamicResource
和元素。幸运的是,元素是最简单的绑定目标。要绑定到一个元素,我们需要给Binding
标记提供一个ElementName
和一个Path
。ElementName
是我们希望绑定的元素的唯一名称(或x:Name
)。Path
是该元素在运行时我们希望绑定的属性。它在运行时使用反射来绑定到传入的ElementName
,然后查看该对象的Path
的值并进行绑定。
请小心!如果您提供了错误的Path
,您可能会很难找到错误,因为它不会抛出异常。相反,您会在Visual Studio的输出窗口中看到一条消息,例如
“System.Windows.Data Error: 35 : BindingExpression path error: 'sdf' property not found on 'object' ''ComboBoxItem' (Name='')'. BindingExpression:Path=SelectedItem.sdf; DataItem='ComboBox' (Name='ComboBoxColors'); target element is 'Border' (Name=''); target property is 'Background' (type 'Brush')”
显然,您可以看到“sdf
”不是ComboBoxItem
的属性。
那么,让我们来做Binding
,好吗?
<Border Grid.Column="0" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=SelectedItem.Content}" />
<Border Grid.Column="3" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=SelectedItem.Content}" />
瞧!我们将每个Border
的Background
属性设置为我们ComboBox
的SelectedItem
的当前Content
,在我们的例子中就是我们的ComboBox
。
现在,如果您运行应用程序并更改ComboBox
,您将看到Border
s发生了变化。请注意,我们仍然没有在代码隐藏中编写任何代码……这是一个真正强大的语言,它将业务逻辑与用户界面分离开来。理想情况下,您会有一个Color
的集合(或者绑定到System
命名空间mscorelib
的预定义颜色枚举)。
这就完成了我们简单的XAML到XAML元素数据绑定示例。
将XAML定义的控件绑定到代码隐藏
这个例子会很短,但很简单。我们将把我们的ComboBoxColors
元素改为从C#获取数据,而不是在XAML中定义其子元素。
这实际上很简单。让我们一步一步来。首先,我们可以从ComboBoxColors
中移除三个ComboBoxItem
,因为我们将在Window
的代码隐藏中定义它们。
现在,我们应该有
<ComboBox x:Name="ComboBoxColors" SelectedIndex="0"
IsSynchronizedWithCurrentItem="True"> </ComboBox>
或
<ComboBox x:Name="ComboBoxColors" SelectedIndex="0"
IsSynchronizedWithCurrentItem="True" />
现在,让我们创建一个包含我们颜色的enum
。
namespace
{
JumpStartWPFpublic enum Colors
{
DarkRed,
DarkGreen,
DarkBlue
}
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary> public partial
class Window1 : Window
{
{
InitializeComponent();
}
}
}
public Window1()
我花了好长时间才弄明白,您必须将enum
创建在类外面,并且是public
的。照做就是了,您以后会感谢我的。
现在,我们将把我们的ComboBox
的ItemsSource
设置为Colors
的所有值。
namespace
{
JumpStartWPFpublic enum Colors
{
DarkRed,
DarkGreen,
DarkBlue
}
/// <summary> /// Interaction logic for Window1.xaml
/// </summary> public partial class Window1 : Window
{
{
InitializeComponent();
ComboBoxColors.ItemsSource =
}
}
}
public Window1()Enum.GetNames(typeof(Colors));
有几种方法可以将WPF元素绑定到C#代码隐藏,对我来说这是最简单的方法。这只是将Item
集合ItemSource
设置为目标枚举的所有值。在这种情况下,它将在运行时填充我们的ComboBox
,包含DarkRed
、DarkGreen
和DarkBlue
。请注意,我将此代码放在InitializeComponent()
之后。如果您放在之前,会发生糟糕的事情。同样,您以后会感谢我的。
现在,如果您运行应用程序,您会注意到我们的ComboBox
已正确填充了Colors
(我们的枚举)类型的Item
。但是等等……我们的Border
的Background
没有显示!嗯,这是因为我们的ComboBox
不再包含ContentControl
的列表。现在,我们实际上是将ComboBox
的ItemsSource
绑定到一个String[]
(从Enum.GetNames()
返回)。那么,要绑定到String
,您只需要将目标Path
指向“Text”。
所以,进行这个更改,然后瞧,您应该看到工作的Border
s,它们被绑定到ComboBox
的SelectedItem
,类型为String
。
<Border Grid.Column="0" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=Text}" />
<Border Grid.Column="3" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=Text}" />
这完成了我们将XAML定义的元素绑定到C#代码隐藏的示例。我们成功地用代码隐藏定义的枚举填充了一个ComboBox
,并将Border
的Background
属性绑定到ComboBox
中当前选中的内容。
在XAML中显示自定义内容(业务对象C#类型)
我们的下一个目标是添加一个用户输入表单,要求输入名字、姓氏和年龄。它将每个对象存储为Person
类型,存储在People
类型的Person
集合中。然后,我们将有一个自定义列表来显示所有这些信息。
本节将涵盖的主题包括
- XAML 资源
- 在XAML
ItemsControl
中显示用户创建的自定义类(例如:在单个ListBox
中显示Person
类型的所有信息) ObservableCollection
以及WPF如何使用它- StaticResource绑定
我看到的许多教程都未能深入探讨这些类的创建细节。WPF中自定义类的设计非常重要,我将在此记录每一步,以免您错过任何内容。
首先,为此,我将创建一个名为People
的类,该类是Person
类型的ObservableCollection
,它将包含名字、姓氏和年龄的属性。
所以,创建一个名为Person
的新类。
为FirstName
、LastName
和Age
(Int32
)添加公共属性(我也使所有将在XAML中访问的类成为public
)。
namespace JumpStartWPF
{
public class Person
{
public Person(String firstName, String lastName, Int32 age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
public Int32 Age
{
get;
set;
}
public String LastName
{
get;
set;
}
public String FirstName
{
get;
set;
}
}
}
很简单……现在,让我们创建一个将是Person
类型的List
的类。我们称之为People
。
请记住,我们的目标是让用户在输入信息并点击提交按钮时,我们创建一个新的Person
并将其添加到我们的Person
列表(我们称之为People
)中。
那么,我们如何做到这一点呢?有一个非常巧妙的基类叫做ObservableCollection<Type>
。
基本上,任何派生自ObservableCollection<Type>
的类都会自动获得作为您在ObservableCollection
中声明的类型的List
的功能。因此,WPF只需要一个对People
的引用,并且当People
ObservableCollection
收到一个新类型为Person
的对象添加到其中时,它会自动更新。
ObservableCollection
驻留在命名空间
using System.Collections.ObjectModel;
namespace JumpStartWPF
{
public class People : ObservableCollection<Person>
{
public People() { }
}
}
由于People
继承自ObservableCollection
,我们自动获得了它包含的List
。
对于我们示例的第一部分,我将手动添加一些Person
,因为这比较容易。
public class People : ObservableCollection<Person>
{
public People()
{
}
}
我们调用父方法Add
,它是ObservableCollection
的一部分。它基本上只是添加一个类型为Person
的新对象(这是我们的ObservableCollection
所期望的),到父级ObservableCollection
内部声明的List
中。
好了,在我继续之前……我们需要重新组织窗口中的元素,为我们的下一个任务腾出空间。所以,我做了一些美化更改,没什么大不了的。
我将在这里发布新的代码……
<Window x:Class="JumpStartWPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="500" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
<RowDefinition Height="3*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="10*" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border x:Name="BorderLeft" Grid.Column="0" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=Text}" />
<Border Grid.Column="3" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=Text}" />
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem Header="File">
<MenuItem Header="Exit"/>
</MenuItem>
</Menu>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Top"
HorizontalAlignment="Center">
<Label Content="Choose a color" />
<ComboBox x:Name="ComboBoxColors" SelectedIndex="0"
IsSynchronizedWithCurrentItem="True">
</ComboBox>
</StackPanel>
<Border Grid.Row="1" Grid.Column="1" Margin="0,30"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
BorderBrush="{Binding ElementName=BorderLeft, Path=Background}"
BorderThickness="1"
CornerRadius="10">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="First Name: " Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center" Margin="0,0,15,0"/>
<TextBlock Text="Last Name: " Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center" Margin="0,0,15,0"/>
<TextBlock Text="Age: " Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center" Margin="0,0,15,0"/>
<TextBox Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left"
VerticalAlignment="Center" Width="90"/>
<TextBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left"
VerticalAlignment="Center" Width="90" />
<TextBox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left"
VerticalAlignment="Center" Width="50"/>
</Grid>
</Border>
<Border Grid.Row="1" Grid.Column="1"
BorderBrush="{Binding ElementName=BorderLeft,
Path=Background}"
BorderThickness="1" CornerRadius="5" Margin="60,5"
HorizontalAlignment="Center"
VerticalAlignment="Bottom" Width="100">
<Button Margin="1" Click="Button_Click">
<Button.ContentTemplate>
<DataTemplate>
<ContentControl Content="Submit"/>
</DataTemplate>
</Button.ContentTemplate>
</Button>
</Border>
</Grid>
</Window>
这里引入了一些新概念的标记,我将在下面进行解释。
首先,虽然我不想这样做,但我最终为用户信息表单的布局使用了一个Grid
。今天StackPanel
不太配合我,所以我设置了几列和三行。我还将所有内容移到了整个窗口的第二行。这将为我们在其他两行中显示信息腾出空间。
进入新概念。
首先,您会注意到一个Button
元素。我显示Button
的方式利用了WPF的另一个强大功能。所有ContentControl
和ItemsControl
都是可定制的。正如您在这里看到的,这个Button
唯一的Button
方面是声明Button
。从边框到内容的所有其他内容都是自定义的。
我将Button
包围在一个Border
中,仅仅是因为我喜欢Border
的外观,我把它当作我Button
(它的子元素)的一个面板。我还将这个Border
的BorderBrush
绑定到我Window
左右两侧Border
元素的Background
。这只是一个我想在整个应用程序中保持一致的酷功能。然后,我声明一个按钮,但没有直接在Button
声明中设置Content
,而是告诉Button
元素我想要更详细地说明。而不是设置Content
,我打开了Button.ContentTemplate
标签。在里面,您几乎只能定义DataTemplate
。DataTemplate
是WPF的一个重要主题,我可以花几个小时来讨论它。我只会说,将DataTemplate
视为元素的化妆品。当您定义DataTemplate
时,您实际上是在重新教WPF如何绘制这个特定元素。这也是开发人员保持应用程序主题的方式。例如,而不是不断地将我所有Border
的BorderBrush
属性设置为绑定到目标元素,我通常会在我的Window Resource中为所有Border
声明一个DataTemplate
,并说,每当我的Window
遇到一个Border
时,将其DataTemplate
设置为这个Resource,它将处理所有我想要显示的化妆品更改。
所以,对于这个按钮的例子,我所做的只是教按钮做它已经知道如何做的事情。我只是添加了一个ContentControl
(这是所有单项内容控件的基础控件类……例如TextBlock
、Button
和Label
)。我只扩展了DataTemplate
以便解释一些关于其用途的信息。我稍后将在Window Resource中定义一个DataTemplate
供使用。
另外请注意,我为Button
创建了一个Click
事件。当您这样做时,如果您已经连接了Intellisense,它将自动为刚刚注册的事件创建事件处理程序。在这种情况下,我的代码隐藏被编辑成了这样
private void Button_Click(object sender, RoutedEventArgs e)
{
}
这些是我进行大型美化更改中唯一的新概念。现在,我们可以回到目标。如果您忘记了,我们想要实现的目标是,每当您点击Submit
时,它都会向我们的People
ObservableCollection
添加一个新的Person
,然后我们在Window
中会有某种ItemsControl
,它被绑定到People
集合,所以它会自动拾取新添加的Person
并显示它们。
让我们一步一步来。首先,我们知道如果我们想添加一个新的Person
,我们需要某种方式来收集输入到TextBox
es中的信息。为了获取这些TextBox
es的内容,我们需要给它们起唯一的名称(就像我们很久以前给第一个ComboBox
起名一样)。我们现在就来做。
<TextBox x:Name="TextBoxFirstName" Grid.Row="0" Grid.Column="1"
HorizontalAlignment="Left" VerticalAlignment="Center" Width="90"/>
<TextBox x:Name="TextBoxLastName" Grid.Row="1" Grid.Column="1"
HorizontalAlignment="Left" VerticalAlignment="Center" Width="90" />
<TextBox x:Name="TextBoxAge" Grid.Row="2" Grid.Column="1"
HorizontalAlignment="Left" VerticalAlignment="Center" Width="50"/>
很简单。我只是给它们起了显眼的名字,这样我们就可以在添加新Person
时在C#端查找它们。
现在,让我们将我们的点击事件连接起来,以向People
集合添加一个新的Person
。我认为我们的People
集合应该有一个静态访问修饰符,这样我们就不会在每次从不同来源向People
集合添加新Person
时创建新的People
集合。为了做到这一点,我只是创建了People
类的一个静态实例,并在构造函数中将其设置为“this
”。
public class People : ObservableCollection<Person>
{
public static People Instance;
public People()
{
Instance = this;
}
}
现在,我们可以从我们的Button
点击中调用这段代码
private void Button_Click(object sender, RoutedEventArgs e)
{
// For UI Simplicity Sake
if (People.Instance.Count >= 5)
{
People.Instance.RemoveAt(0);
}
People.Instance.Add(new Person(TextBoxFirstName.Text, TextBoxLastName.Text,
Int32.Parse(TextBoxAge.Text)));
}
所以,我在这里做的是限制我们People
集合维护的Person
数量。如果超过5个,我们就会清除最旧的人,并将次旧的人移上来。这是一个先进先出集合(FIFO)。然后,我通过访问静态访问器来访问当前的People
实例,并调用ObservableCollection
基类的一部分Add
方法,传入我们的名字、姓氏和年龄(转换为int
)。
WPF通过使用另一个可以实现的接口IDataErrorInfo
来提供元素错误验证。这超出了本文的范围,但理想情况下,您应该对用户从TextBox
es输入的输入进行某种检查。甚至可能使用一些Converter
s,可能性是无限的。
好的,现在我们需要创建一个这些People
的可视化集合。
<StackPanel Grid.Row="2" Grid.Column="1" HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border BorderBrush="{Binding ElementName=BorderLeft, Path=Background}"
BorderThickness="1" CornerRadius="2">
<ItemsControl x:Name="PeopleItemsControl"
ItemsSource="{StaticResource PeopleCollection}"
ItemTemplate="{StaticResource PersonItemTemplate}" />
</Border>
</StackPanel>
所以,在这里,我创建了一个垂直(默认)的StackPanel
。这将是每个Person
的Container
。在其中,我放了一个Border
来围绕整个ItemsControl
。然后我放了一个通用的ItemsControl
。请记住,ItemsControl
只是一个Item
集合的基类。我通常在想要真正定义我的集合外观时使用它。在这种情况下,我不想让用户能够选择一个项目,或者在项目上有任何高亮/操作,所以我不希望使用ListBox
或ComboBox
。我只是想显示这个项目。
现在您应该注意到这里有几个新概念。首先是ItemsSource
。还记得我们上一个ComboBox
是如何在代码隐藏中设置的吗?嗯,我在这里做的事情基本上一样,但这次,我将其设置为XAML文件中的一个StaticResource
。什么是资源?嗯,它基本上是在C#或XAML中创建的对象,但它是在XAML本身中实例化的。(注意:可以在代码隐藏中声明一个资源,并将其作为Static或Dynamic Resource在XAML中使用,但为了简单起见,我们的Resource是在XAML中定义的)。
现在,关于语法。让我困惑了一段时间的是何时使用和何时不使用Binding
标记。我仍然不是100%确定,但我知道当我将集合控件绑定到Resource时,我不会使用Binding
标记。那么,我调用的Resource PeopleCollection
在哪里?嗯……它在Resources部分。
<Window.Resources>
<local:People x:Key="PeopleCollection" />
</Window.Resources>
如果您只是添加这个,它将无法编译。local
默认不存在。您必须创建它。在这种情况下,local
是我在People.cs中给我的类起的名字。还记得那个类通过代理是一个ObservableCollection
吗,所以如果我创建一个该类的实例,实际上我也得到了它附带的整个集合。这是添加Resource的方法。
窗口的顶部包含很多命名空间声明。那么,如果您想访问您自己的类型,您也需要在此处声明您的命名空间。
语法是,“xmlns
”后跟一个冒号“:”,然后是一个可选的命名空间键,然后是“clr-namespace
”,然后是一个冒号“:”,然后是您的命名空间。从“clr-namespace
”的开头到您命名空间的结尾,都用引号括起来。
您可以为命名空间提供的可选键非常重要……它是您在为Window
声明Resource时引用该命名空间中的类的依据。我通常将我的主要XAML窗口所在的命名空间命名为“local
”。
xmlns:local="clr-namespace:JumpStartWPF"
当您回到Window.Resources
时,您会注意到People
现在应该存在了。所有Resource都必须有一个相关的键或其他类型的相关键(例如TargetType
等)。您可以将创建类Resource视为创建类的新实例。当调用它时,它实际上将创建一个People
类的实例,并激活我们的静态实例以供使用。
现在,回到我们的ItemsControl
。有两种Resource绑定类型:StaticResource
和DynamicResource
。我从未用过DynamicResource
,但它的工作原理是,如果Resource在运行时发生更改,所有引用该Resource的元素也会发生更改。StaticResource
是一次性绑定。DynamicResource
有点慢,因为它可能不断创建新的引用,而静态Resource在应用程序生命周期中永远存在。
现在到我们最后的主题之一……我们的ItemsControl
的ItemTemplate
属性。还记得我们之前设置了Submit按钮的DataTemplate
吗?DataTemplate
告诉我们的Button
如何显示自己。我们所做的只是将Button
的Content
设置为Submit,但这包含了很多关于WPF的信息,一次性吸收起来有点多。
我们现在将扩展这一点,并实际完全自定义我们的ItemsControl
,并教它如何显示一个Person
。
您已经可以看到我绑定到了PersonItemTemplate
的StaticResource
。ItemTemplates
/ContentTemplates
指向DataTemplate
s。所以,您应该能够推断出,在我们的Resources部分,我们声明了一个键为PersonItemTemplate
的DataTemplate
。
然后,每当我们的ItemsControl
需要绘制一个Person
时,它就会引用DataTemplate
的StaticResource
来知道如何绘制一个Person
。
<DataTemplate x:Key="PersonItemTemplate">
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="10">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Margin="5,0,0,0" Text="{Binding LastName}" HorizontalAlignment="Center" />
<TextBlock Text=", " HorizontalAlignment="Center" />
<TextBlock Margin="0,0,5,0" Text="{Binding FirstName}" HorizontalAlignment="Center" />
</StackPanel>
<TextBlock Margin="0,0,0,5" Text="{Binding Age}" HorizontalAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
还记得,我们之前放了一个Border
来围绕整个ItemsControl
吗?嗯,现在,我想要一个更圆的Border
(CornerRadius
设置为10)来围绕每个单独的Person
。在其中,我有一个垂直StackPanel
,其中包含一个水平StackPanel
,显示姓氏,后面跟着逗号,然后是一个空格和名字。然后,在该StackPanel
下方是一个显示Person
年龄的TextBlock
。请注意每个TextBlock
的{Binding
...}标记。在编译时,我们的DataTemplate
绝对不知道Person
是什么。那么,它怎么知道LastName
、FirstName
和Age
是什么呢?答案是反射。在运行时,当需要使用这个DataTemplate
时,DataTemplate
将继承调用控件的所有属性。在这种情况下,我们的调用控件是ItemsTemplate
,其ItemsSource
设置为People
类型,这是一个Person
类型的集合,它具有FirstName
、LastName
和Age
属性。看到了吗,一切都奏效了?我们正在利用继承的优点。
好吧,我们现在漏掉了一件事,不是吗?添加主菜单中Exit MenuItem
的Click
事件。您现在应该能够根据您对上述主题的知识轻松地做到这一点。
代码如下
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem Header="File">
<MenuItem Header="Exit" Click="MenuItem_Click"/>
</MenuItem>
</Menu>
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
if (People.Instance != null)
{
People.Instance.Clear();
People.Instance = null;
}
this.Close();
}
谢谢阅读!再次声明,我才刚开始学习WPF一周左右。我的一些信息可能用词不当,或者可能存在更好的方法来实现我所做的事情。总而言之,我认为这是一个很好的WPF入门指南,它提供了一些我认为自己很难弄清楚的信息。希望对您有所帮助!