WPF 教程(译文)(第一部分)

感觉一年多时间以来,状态遭透了,荒芜了这片自留地,不过还好,这只是自留地,不会有太多人时常关注。这次为了避免被开心开除,先发几篇译文。

这篇译文是我为了学习 WPF 找到的,我认为是我见到最好的 WPF 入门教程。我不知道是否有其他人翻译过,我简单的 Google 了一圈,似乎是没有,因此贴出来给大家共享。另外,从这篇文章开始,我的文章原则上不再发布到博客堂首页了,最近很喜欢清静。

原文地址: http://dotnetslackers.com/articles/silverlight/WPFTutorial.aspx  因为原文很长,我可能将它分两三次来发。另外,如果你发现已经有人译过的话,也告诉我,那我就不用献丑了,呵呵。

WPF Tutorial

Published: 16 May 2007
By: Rob Eisenberg  (作者的博客)

微软发布新技术的速度总是快得难以追赶上,WPF就是这样的例子。WPF和以往构建用户界面的任何框架都不一样。本文尝试着给您一幅WPF的全景鸟瞰图,向您介绍WPF技术中最关键也最革新的思维方式,以期使您充分地了解它的威力,并能够顺利地展开进一步的学习。

简介

Windows Presentation Foundation (通常被我们简称做WPF),是Windows平台上创建用户界面的全新框架。它是WinForm窗体技术的换代品,它能够充分发挥现代PC在多媒体支持方面的潜力。WinForm通过包装底层的经典Win32 GDI+接口实现用户界面,WPF则抛弃了这一路线,使用.NET技术自下而上构建了全新的API框架。它的强大威力在于基于矢量的绘图能力,支持硬件加速,以及不依赖于设备分辨率。

或许你在WPF中找到了那些你在WinForm开发中常用的控件:Button, ComboBox, ListBox等。但在WPF开发中,你的UI设计器完全不同于以往,不再有设计器生成的构建画面控件及布局的代码文件,不再有设计器生成的资源文件。WPF使用XML,也就是XAML。千万别被这个新特点吓跑,你将发现它并不如想象中的困难,它很符合直觉,或许你还会认为充满乐趣。WPF拥有许多全新的功能,下图展示了其中最重要的一部分:

图 1: WPF中重要的功能

Important WPF Features

表的右列中,列出了一些WPF之前的技术,你或许会觉得它们有相似点。希望会对你认识左列中的新术语有所帮助,但请不要把它们想得太像,差别还是很大的。

除了上面介绍到的功能,WPF引入了一种新的程序运行方式。你仍然可以做成标准的Windows应用程序,新的选择是XBAP: the XAML Browser Application。你将XBAP程序放置到Web服务器上,把地址链接告诉你的使用者,使用者通过浏览器访问,XBAP程序就可以在浏览器的沙盒中运行了。这种体验有点类似于Flash,但仅限于IE用户(版本6以上)和其他一些支持WebControl的浏览器(如Firefox可以通过它的IE-tab)。你可以设想到,这很适合于为企业、公司创建应用程序,如果他们的职员主要工作在Windows XP或Windows Vista上的话。但是,影响选择XBAP的一个问题或许是,需要在客户端安装.NET Framework 3.0(译注:或3.5)。微软正在研发中的Silverlight(之前称为WPF/E),将是一个跨平台的、跨浏览器的新选择,它将包含.NET Framework的一个子集和WPF技术的子集。

你准备开始了吗?你需要准备好开发工具。WPF是.NET Framework 3.0(或3.5)的一部分,首先,你得知道,.NET 3.0应安装在Windows XP Service Pack 2以上、Windows Server 2003 Service Pack 1以上,或者Windows Vista操作系统上,你可以从这里下载最新的.NET Framework(注:3.5版)。安装3.0的同时,.NET 2.0也会被安装,如果你的机器上没有2.0的话,这是因为3.0是2.0的超集。我猜你更希望使用Visual Studio的集成开发环境来学习,你可以下载一个试用版的Visual Studio 2005。(注意那文件可不小,得花点时间的。),和对.NET 3.0的扩展支持包。(译注:建议直接用Visual Studio 2008)另外,我还建议你去WPF的官方站点逛逛。

打开开发环境,WPF的画面设计器和WinForm的很相像,但它们还是差别很大。你可以通过标签切换画面效果和XAML代码。而更为专业的工具是Expression Blend(之前称为Expression Interactive Designer),你可以从这里下载它的试用版。如果你希望界面有特别的外观效果,你应该去试试它。它有些像Adobe Illustrator,不过它用于XAML的编辑,并且和VS集成得很好。设计师可以以他们熟悉的方式工作,并且产生的XAML代码直接用于产品,设计师的参与度大大加强了。以往的工作模式中,设计师将设计出的效果图转交给开发队伍后,开发人员需要用编程的方式将画面重新实现出来,并且还会因为难以实现而放弃某些设计效果。你还可以考虑第三方的Mobiform's Aurora,我强烈建议你试一下。除此以外,还有很多插件用于将其他流行软件制作的2D/3D图形转化为XAML。或许你希望我能推荐一些书籍,目前我所知的最棒的两本书是Charles Petzold著的Applications = Code + Markup 和Adam Nathan著的Windows Presentation Foundation Unleashed

XAML

XAML是eXtensible Application Markup Language的缩写。你可以将它想成用类似HTML的方式来定义Windows应用程序的界面,不过XAML的功能更强大一些。或许代码狂们会认为XAML不过是一堆CLR对象被序列化后的结果,不过XAML除了可以被逆序列化之外,还比较适合阅读,程序员们可以直接编辑它,图形化的设计器可以方便生成它。包含XAML文件的项目,编译后对应的是BAML,这是二进制表示的XAML,它被当作资源存储在程序exe或dll中,当运行需要时被快速加载到程序中来,BAML比XAML的加载效率要高一些。

学习XAML最好的方式就是通过一些例子,不再絮叨,首先来比较两段代码:

HTML 代码片段

<div style="border: solid 5px black; margin: 10px; padding: 5px">

 <input type="button" value="Click Me!" />

</div>

XAML 代码片段

<Border BorderBrush="Black" BorderThickness="5" Margin="10" Padding="5">

 <Button>Click Me!</Button>

</Border>

图 2: 带边框的按钮

A Button with Border.

这两段代码将各自实现相似的效果:一个按钮以及环绕其一圈的边框。(注意:将样式直接声明在控件上,多数时候并不太好。此处只是为了演示目的。)这个例子大家可以看到XAML和HTML的写法有很像的地方。或许你也会认可,XAML的写法比HTML更直观些。(译注:如果你的英语很poor,或许你不会这么认为。)多解释一些XAML的基本写法:标签名(Border, Button)直接表示WPF类库中的类(主要在System.Windows.Controls命名空间中),标签的属性(BorderBrush, Margin等)直接对应该类的同名属性。因为继承关系,很多控件都有一些相同的属性,这将便于学习。

使用XAML,你可以标记出大量和界面相关的设定,除了控件,还有面板布局,基本图形(圆形、方形等),3D图形,甚至还包括动画。来看下一个例子,我们画一个椭圆:

<Ellipse Width="200" Height="150">

  <Ellipse.Fill>

      <LinearGradientBrush>

         <GradientStop Offset="0" Color="Teal"/>

         <GradientStop Offset="1" Color="Aqua"/>

      </LinearGradientBrush>

   </Ellipse.Fill>

</Ellipse>

图 3: 椭圆

An Ellipse

我们来简单分析一下:第一行代码很简单,设置了椭圆的高度和宽度。第二行Ellipse的子节点,有些跟刚才看到的不一样,从图3的效果看,我们是打算为这个椭圆设置一个渐变的填充颜色,也就是打算给Fill属性赋值。如果我们只是给Fill属性设置一种单一的颜色,就可以仿照Width和Height那样子声明即可,可现在要声明一个渐变,是不可能以刚才那样的简单方式的。这是一个很常见的场景,从示例代码中你已经知道解决方法了,我们可以称之为Property Element Syntax(元素属性语法),在该标签的内部,创建一个“类型名.属性名”的子节点,然后再在此子节点内部定义我们希望的东西。此例中就是Ellipse.Fill节点。再往下看第三行、第四行,LinearGradientBrush类型属于System.Windows.Media命名空间,你可以去查看这个类的GradientStops属性,该属性用于设置渐变的起止颜色。如果你足够仔细,你会发现我们关于其GradientStops属性的设置不符合刚刚的“元素属性语法”,它并没有使用LinearGradientBrush.GradientStops的子节点,而在LinearGradientBrush内直接写了两个GradientStop标签。你以后会发现类似的例外很普遍。XAML规定,每个类型可以指定一个属性作为它的默认内容属性(default content property),该默认内容属性的子节点可以直接写在类型节点的内部。此例中,GradientStops属性是LinearGradientBrush类的默认内容属性。其实,刚才的第一个例子已经出现了这种情况:

<Button>Click Me!</Button>

这里,Button类型的默认内容属性是Content,“Click Me!”将作为Content的值。下一个例子,我们为Button的Content属性指定稍微“复杂”一些的内容:

<Button>

   <Ellipse Width="200" Height="150">

      <Ellipse.Fill>

         <LinearGradientBrush>

            <GradientStop Offset="0" Color="Teal"/>

            <GradientStop Offset="1" Color="Aqua" />

         </LinearGradientBrush>

      </Ellipse.Fill>

   </Ellipse>

</Button>

图 4: 内部包含一个椭圆的按钮

A Button with Ellipse content.

看明白了吗?Button(还有其他一些控件)继承自ContentControl类,该类定义了一个Content属性,你可以将任意的CLR对象放进Content中。不过对于大部分不可显示的对象,WPF将直接的调用其ToString()方法,将所得的字符串作为实际的Content。但是除此之外,还有很多可以显示的对象,这意味着Button中可能嵌套有另外的控件,或者图片、图形,甚至3D、动画、视频等。这个有些奇妙的特性其实不是XAML带来的,而是WPF的设计理念之一,有时被称为Content Model(内容模型)。这个开放性的架构允许了很多想得到或想不到的组合与嵌套,蕴藏着无限的可能性。

如果你还没有想通这个道理,不用着急,我们将要看到更多的示例,保证你越看越着迷。你应该将这些示例运行起来,这样效果更好,我推荐你使用XamlPad,可以直接显示XAML效果的小编辑器,它属于SDK的一部分。当然你也可以放到Visual Studio里面运行。

UI 布局

创建用户界面时,或许第一件事就是如何布局,如何将各种各样的界面元素合理的安排到它们应在的位置。在WPF之前的MS技术里,我们只有有限的支持,.NET 2.0提供的布局模型或许是多数人等待已久的,然后它仍然有很多不便之处。但在WPF,布局成为了一等公民,提供了一系列的布局控件,满足各种不同场合的需要。这里,我只能介绍其中的很少一部分,因此建议你读完本文后,去查看SDK中关于其他布局控件的介绍。来看这个例子:

<StackPanel>

   <TextBlock>My UI</TextBlock>

   <ListBox>

      <ListBoxItem>Item 1</ListBoxItem>

      <ListBoxItem>Item 2</ListBoxItem>

   </ListBox>

   <RichTextBox/>

</StackPanel>

图 5: StackPanel 布局

A StackPanel layout.

StackPanel是最简单的布局选择,就像它的名字那样,它将放在其中的控件叠放在一起,从上到下或者从左到右。下面是DockPanel:

<DockPanel>

   <TextBlock DockPanel.Dock="Top">My UI</TextBlock>

   <ListBox DockPanel.Dock="Right">

      <ListBoxItem>Item 1</ListBoxItem>

      <ListBoxItem>Item 2</ListBoxItem>

   </ListBox>

   <RichTextBox/>

</DockPanel>

图 6: DockPanel 布局

A DockPanel layout.

DockPanel有些类似WinForm中最初提供的Dock布局模式,你可以将元素紧贴上边缘、下边缘、左边缘或右边缘进行布局,最后一个元素将充满剩余的空间。关于这些特点,想必大家都不陌生。此处重点看一下示例XAML中的新语法。注意看TextBlock和ListBox标签中的DockPanel.Dock属性,而实际上,这两个类型是不包含这样的属性的。这是一个新语法,“DependencyProperty”:DockPanel类型中声明了一个名为DockProperty的DependencyProperty。对这个特例来说,这个DependencyProperty是一个AttachedProperty,即允许父级控件将设定信息存储在子级控件上。(我们目前没有时间来讨论DependencyProperty的细节,如果你有兴趣,请阅读上面提到过的Charles Petzold的Applications = Code + Markup。)在这个示例中,DockPanel将它所需的Dock设定存储到了它的子级控件上,语法为“父级标签名.AttachedProperty名”。OK,我们看一个Grid布局的示例:

<Grid>

   <Grid.RowDefinitions>

      <RowDefinition Height="*"/>

      <RowDefinition Height="3*"/>

   </Grid.RowDefinitions>

   <Grid.ColumnDefinitions>

      <ColumnDefinition/>

      <ColumnDefinition/>

   </Grid.ColumnDefinitions>

   <TextBlock Grid.ColumnSpan="2">My UI</TextBlock>

   <ListBox Grid.Row="1" Grid.Column="0">

      <ListBoxItem>Item 1</ListBoxItem>

      <ListBoxItem>Item 2</ListBoxItem>

   </ListBox>

   <RichTextBox Grid.Row="1" Grid.Column="1"/>

</Grid>

图 7: Grid 布局

A Grid layout.

Grid布局或许是最经常使用的,它有些类似HTML中的TABLE布局模式。它的语法或许最复杂。示例代码中的开始部分,定义了行、列。你或许注意到此处行的高度定义写法,它的意思是第二行是第一行高度的3倍,也就是第一行占Grid总高度的1/4,第二行占3/4。我们没有定义列宽,因此两列各占一半宽度。这个示例中,你仍然看得到AttachedProperty,诸如“Grid.Column”“Grid.ColumnSpan”。你注意到第一行第一列的TextBlock没有明确指定Grid.Column和Grid.Row,这两个属性默认为零,因此显示是正常的。

在实际的应用程序中,界面或许使用多种不同的布局控件相互嵌套在一起,元素的宽度、高度最好避免使用绝对的数字,而让它们自己协调,以充分的利用屏幕空间。经过一段时间练习,相信你很快会熟悉WPF的布局模型并感受到它的灵活性。除了上面介绍的三种外,还有很多布局选择,以下是一些常用的,本文没有时间来介绍了:

  1. WrapPanel: 类似于StackPanel,但当一行不够显示时会转到下一行。
  2. GroupBox: 类似于WinForm的GroupBox。
  3. Viewport: 可以自动放大或缩小,以适应空间的大小。
  4. Border: 经常用于表示边框、标题条等。
  5. Canvas: 使用坐标位置布局的Panel,坐标相对于Canvas。

当然,你还可以创建自己的Panel,实现特殊的功能。网上有一些自定义Panel的示例,比如Kevin's Bag-O-Tricks,你可以跟他学习很多自定义Panel的方法,当然还包括自定义其他控件的示例。另外,也建议你在适当的时候使用Margin、Padding的方式来布局,以避免过多的嵌套。其他一些可能用到的属性,如VerticalAlignment和HorizontalAlignment,都很有用,帮助你实现实际程序中的界面布局。

数据绑定

数据绑定(Databinding)和数据模板(DataTemplates)或许是WPF中最具威力的特性了。无论你以往的经历是MS系的技术,还是其他技术,WPF的数据绑定都会让你印象深刻。数据绑定功能在WPF底层的DependencyObject类型里实现,WPF里绝大多数类型都从此类型衍生,因而都具有该基类实现的数据绑定、样式、动画等功能。本文不再此展开讨论,简单说,DependencyObject的子类型都支持DependencyProperty,而大多数属性,像Text, Content, Width, Height 等都是DependencyProperty,任何一个DependencyProperty都具有实现动画、样式、数据绑定的能力。WPF在数据绑定方面有着丰富的支持,本文只能选择其中最基本的示例来演示,你应该去看看Beatriz Costa的博客,那里有更深入的探讨,我建议你从他的第一篇文章开始,一直看到最近的文章,我认为在那里花些时间是值得的。

第一个例子通常都很简单,也很普遍的场景。假定我们有一些对象,是Person, Customer, Employee等类型,下面这个XAML用于显示这些对象的数据,用于编辑模式:

<Grid>

   <Grid.RowDefinitions>

      <RowDefinition/>

      <RowDefinition/>

      <RowDefinition/>

      <RowDefinition/>

      <RowDefinition/>

   </Grid.RowDefinitions>

   <Grid.ColumnDefinitions>

      <ColumnDefinition Width="75"/>

      <ColumnDefinition/>

   </Grid.ColumnDefinitions>

   <Label>First Name:</Label>

   <TextBox Grid.Column="1" Text="{Binding Path=FirstName}"/>

   <Label Grid.Row="1">Last Name:</Label>

   <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding LastName}"/>

  <Label Grid.Row="2">Street:</Label>

   <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Address.Street}"/>

   <Label Grid.Row="3">City:</Label>

   <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Address.City}"/>

   <Label Grid.Row="4">State:</Label>

   <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Address.State}"/>

</Grid>

图 8: 简单的数据绑定

A basic databinding example.

截图很直观。我得说明一下对象的成员组成:Person类有字符串类型的FirstName和LastName属性,此外还有一个Address类型的Address属性。Address类包含Street, City和State三个字符串属性。上例中,我在后台代码中把Person对象赋值给Window的DataContext属性(每个FrameworkElement的子类型都有这个属性)。数据绑定后,WPF沿着控件树,从末端向根部逐级查询,直到找到一个它可以绑定的DataContext属性。注意绑定的语法,使用了大括号,并且以“Binding”开头。注意到FirstName和LastName的绑定写法稍有不同,后者多了“Path”,但两者效果相同。另外,还要注意关于Address的绑定写法。如果运行这个程序,你会发现在界面上改变Person对象的值,会影响到后台的Person对象;而相反的,后台Person对象的改变不会自动反映到界面上。你可以选择实现INotifyPropertyChanged接口来实现这一过程,一项较为琐碎的工作。

接下来我们考虑一个普通的场景,我们想在不同的画面表示相同的信息,比如类似一张“联系人卡片”那样子。在WinForm中,你或许考虑创建一个自定义控件或者用户控件来实现。在WPF当然也可以如此去做,不过你最好看看数据模板功能:

<Grid>

 <Grid.Resources>

    <DataTemplate DataType="{x:Type local:Person}">

      <Grid Margin="3">

        <Grid.BitmapEffect>

          <DropShadowBitmapEffect />

        </Grid.BitmapEffect>

        <Rectangle Opacity="1" RadiusX="9" RadiusY="9" Fill="Blue" StrokeThickness="0.35">

          <Rectangle.Stroke>

            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">

              <GradientStop Color="White" Offset="0" />

              <GradientStop Color="#666666" Offset="1" />

            </LinearGradientBrush>

          </Rectangle.Stroke>

        </Rectangle>

      <Rectangle Margin="2,2,2,0"

        VerticalAlignment="Top"

        RadiusX="6"

        RadiusY="6"

        Stroke="Transparent"

        Height="15px">

        <Rectangle.Fill>

          <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">

            <GradientStop Color="#ccffffff" Offset="0" />

            <GradientStop Color="transparent" Offset="1" />

          </LinearGradientBrush>

        </Rectangle.Fill>

      </Rectangle>

      <Grid Margin="5">

        <Grid.RowDefinitions>

          <RowDefinition Height="auto"/>

          <RowDefinition Height="auto"/>

          <RowDefinition Height="auto"/>

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>

          <ColumnDefinition />

        </Grid.ColumnDefinitions>

        <StackPanel Grid.Row="0" Orientation="Horizontal">

          <TextBlock FontSize="16" Foreground="White" Text="{Binding LastName}" />

          <TextBlock FontSize="16" Foreground="White" xml:space="preserve">, </TextBlock>

          <TextBlock FontSize="16" Foreground="White" Text="{Binding FirstName}" />

        </StackPanel>

        <TextBlock Grid.Row="1" FontSize="16" Foreground="White"

          Text="{Binding Address.Street}" />

        <StackPanel Orientation="Horizontal" Grid.Row="2">

          <TextBlock FontSize="16" Foreground="White" Text="{Binding Address.City}" />

          <TextBlock FontSize="16" Foreground="White" xml:space="preserve">, </TextBlock>

          <TextBlock FontSize="16" Foreground="White" Text="{Binding Address.State}" />

        </StackPanel>

      </Grid>

    </Grid>

 </DataTemplate>

 </Grid.Resources>

 <ListBox Name="personList" />

</Grid>

图 9: 数据模板示例:联系人卡片

A contact card DataTemplate sample.

数据模板是让原本不可视的对象变得可视化的模板。此例中,Person类本身是不可视的,它应该显示成什么样子?例子中,我们创建了一个DataTemplate,并指定它的DataType为Person,现在WPF知道如何来显示一个Person类的对象了,在数据绑定发生时,它就那么去显示了。你也可以给模板指定一个key,避免自动的模板套用,只在你指定的地方让它套用模板。这跟HTML中CSS有些类似,当然数据模板要更强大。数据模板写在资源中,你可以把它放置到不同级别节点的资源中,它的作用范围也就不同了,此例中放在Grid.Resources中,向上还可以放在Window.Resources或Page.Resources,或者顶级的Application.Resources。另外,类型的继承关系也会影响数据模板,比如Person的子类型Employee, Manager等也会套用这个模板,不过如果单独的Manager模板也存在的话,WPF会套用Manager的模板也不用Person的模板。神奇吧?

本文中关于数据绑定和数据模板的介绍只能到此了,我很惭愧没有能力覆盖更多的特性,但我的文章必须得进入下一个特性了。不过,留下如下的思考题给你: You can have conditional binding based on business rule validation, conditional template selection based on business rules, converters that shape data when binding to and from the UI, DataTriggers that change the appearance of the template based on values in the data, binding between different UI elements based on name or relative position in the UI hierarchy, a choice of four different binding modes and a choice of what type of event triggers the binding, and bindings to data sources that pass in other bindings as parameters, etc. (译注:此段保留未翻译,暂时理解还不够深刻。)此外还有很多可能的思路。再一次地,向你推荐Beatriz Costa的博客,等你有了更深的理解后,再来读这些文字。

。。。(第二部分

posted on 2008-03-14 12:43  破宝  阅读(515)  评论(0编辑  收藏  举报

导航