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

WPF 应用程序的结构

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (7投票s)

2010 年 8 月 10 日

CPOL

11分钟阅读

viewsIcon

31757

本文同时解释了 WPF 的“如何”和“为何”。

前言

有很多关于 WPF 的高级文章,它们的桌面应用程序体现了很多经验。但有时,学习曲线会很陡峭,因为高级水平和初级水平之间可能存在很大的差距。本文旨在逐步介绍构建过程。在任何形式的编程中,学习编程的最佳方法就是阅读写得好的程序。本文将深入探讨一个看似复杂的 WPF 应用程序。XAML 代码将显得冗长,而代码隐藏则相对简单。因此,我们将首先使用 Expression Blend 或 Visual Studio 2008(或 2010)来创建和开发此应用程序。完成后,我们将检查它是如何工作的,更重要的是,为什么它能工作。所以,我们开始创建一个新的 C# WPF 应用程序,并将其命名为默认的 WPFApp4。然后,我们修改 MainWindow.xaml 中的代码。

<Window
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          x:Class="WpfApp4.MainWindow"
          Title="Color Spinner" Height="370" 
          Width="270">
   <Window.Resources>
    <Storyboard x:Key="Spin">
      <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
     Storyboard.TargetName="ellipse1"
     Storyboard.TargetProperty=
         "(UIElement.RenderTransform).(
          TransformGroup.Children)[0].(RotateTransform.Angle)"
         RepeatBehavior="Forever">

       <SplineDoubleKeyFrame KeyTime="00:00:10" Value="360"/>
       </DoubleAnimationUsingKeyFrames>
           <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                 Storyboard.TargetName="ellipse2"
                                   Storyboard.TargetProperty=
                                  "(UIElement.RenderTransform).(TransformGroup.Children)[0]
                                  .(RotateTransform.Angle)"
           RepeatBehavior="Forever">

      <SplineDoubleKeyFrame KeyTime="00:00:10" Value="-360"/>
       </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                   Storyboard.TargetName="ellipse3"
                   Storyboard.TargetProperty=
                   "(UIElement.RenderTransform).(TransformGroup.Children)[0]
                   .(RotateTransform.Angle)"
             RepeatBehavior="Forever">

           <SplineDoubleKeyFrame KeyTime="00:00:05"
                     Value="360"/>
            </DoubleAnimationUsingKeyFrames>
             <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
             Storyboard.TargetName="ellipse4"
               Storyboard.TargetProperty=
               "(UIElement.RenderTransform).(TransformGroup.Children)[0]
                .(RotateTransform.Angle)"
                 RepeatBehavior="Forever">

            <SplineDoubleKeyFrame KeyTime="00:00:05"
                       Value="-360"/>
              </DoubleAnimationUsingKeyFrames>
              </Storyboard>

         <Storyboard x:Key="ellipse1"/>
     </Window.Resources>
     <Window.Triggers>
      <EventTrigger RoutedEvent="FrameworkElement.Loaded">
       <BeginStoryboard Storyboard="{StaticResource Spin}"
            x:Name="Spin_BeginStoryboard"/>
           <BeginStoryboard Storyboard="{StaticResource ellipse1}"/>
           </EventTrigger>

        <EventTrigger RoutedEvent="ButtonBase.Click"
        SourceName="goButton">
        <ResumeStoryboard BeginStoryboardName="Spin_BeginStoryboard"/>
         </EventTrigger>
       <EventTrigger RoutedEvent="ButtonBase.Click"
             SourceName="stopButton">
           <PauseStoryboard
             BeginStoryboardName="Spin_BeginStoryboard"/>
         </EventTrigger>
      </Window.Triggers>
     <Window.Background>
     <LinearGradientBrush EndPoint="0.5,1"
              StartPoint="0.5,0">
             <GradientStop Color="#FFFFFFFF" Offset="0"/>
              <GradientStop Color="#FFFFC45A" Offset="1"/>
     </LinearGradientBrush>
   </Window.Background>
   <Grid>
  <Ellipse Margin="50,50,0,0" Name="ellipse5"
      Stroke="Black" Height="150"
      HorizontalAlignment="Left"
      VerticalAlignment="Top" Width="150">
      <Ellipse.Effect>
       <BlurEffect Radius="10"/>
   </Ellipse.Effect>
 <Ellipse.Fill>
        <RadialGradientBrush>
         <GradientStop Color="#FF000000"
          Offset="1"/>
          <GradientStop Color="#FFFFFFFF" 
           Offset="0.306"/>
   </RadialGradientBrush>

  </Ellipse.Fill>
  </Ellipse>
     <Ellipse Margin="15,85,0,0" Name="ellipse1"
                     Stroke="{x:Null}"
                     Height="80" HorizontalAlignment="Left"
                     VerticalAlignment="Top"
                     Width="120" Fill="Red" Opacity="0.5"
                     RenderTransformOrigin="0.92,0.5" >
    <Ellipse.Effect>
   <BlurEffect/>
   </Ellipse.Effect>
    <Ellipse.RenderTransform>
     <TransformGroup>
      <RotateTransform Angle="0"/>
      </TransformGroup>
    </Ellipse.RenderTransform>
</Ellipse>
<Ellipse Margin="85,15,0,0" Name="ellipse2"
           Stroke="{x:Null}"
           Height="120" HorizontalAlignment="Left"
           VerticalAlignment="Top"
           Width="80" Fill="Blue" Opacity="0.5"
           RenderTransformOrigin="0.5,0.92" >
<Ellipse.Effect>
     <BlurEffect/>
 </Ellipse.Effect>
 <Ellipse.RenderTransform>
      <TransformGroup>
        <RotateTransform Angle="0"/>
      </TransformGroup>
    </Ellipse.RenderTransform>
</Ellipse>
<Ellipse Margin="115,85,0,0" Name="ellipse3"
          Stroke="{x:Null}"
           Height="80" HorizontalAlignment="Left"
           VerticalAlignment="Top"
           Width="120" Opacity="0.5" Fill="Yellow"
           RenderTransformOrigin="0.08,0.5" >
 <Ellipse.Effect>
    <BlurEffect/>
     </Ellipse.Effect>
   <Ellipse.RenderTransform>
     <TransformGroup>
     <RotateTransform Angle="0"/>
     </TransformGroup>
      </Ellipse.RenderTransform>
   </Ellipse>
     <Ellipse Margin="85,115,0,0" Name="ellipse4"
      Stroke="{x:Null}"
       Height="120" HorizontalAlignment="Left"
        VerticalAlignment="Top"
         Width="80" Opacity="0.5" Fill="Green"
         RenderTransformOrigin="0.5,0.08" >
      <Ellipse.Effect>
        <BlurEffect/>
     </Ellipse.Effect>
     <Ellipse.RenderTransform>
     <TransformGroup>
     <RotateTransform Angle="0"/>
    </TransformGroup>
   </Ellipse.RenderTransform>
</Ellipse>

   <Button Height="23" HorizontalAlignment="Left"
    Margin="20,0,0,56"
     Name="goButton" VerticalAlignment="Bottom"
     Width="75" Content="Go"/>

     <Button Height="23" HorizontalAlignment="Left"
       Margin="152,0,0,56"
        Name="stopButton" VerticalAlignment="Bottom" Width="75"
        Content="Stop"/>

      <Button Height="23" HorizontalAlignment="Left"
           Margin="85,0,86,16"
            Name="toggleButton" VerticalAlignment="Bottom"
            Width="75"
         Content="Toggle"/>
         </Grid>
</Window>

在设计视图中双击“Toggle”按钮,并按如下方式修改 MainWindow.xaml.cs 中的代码(当您双击按钮时添加的 using 语句和 toggleButton_Click() 事件处理程序中的新代码)

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace WpfApplication4
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
        }
        private void toggleButton_Click(object sender, RoutedEventArgs e)
        { 
            Storyboard spinStoryboard = Resources["Spin"] as Storyboard;
            if (spinStoryboard != null)
            {
                if (spinStoryboard.GetIsPaused(this))
                {
                    spinStoryboard.Resume(this);
                }
                else
                {
                    spinStoryboard.Pause(this);
                } 
            }
        }
    }
}

现在按 F5 运行应用程序。以下是它应该显示的样子

Capture.JPG

首先,我们来看一下桌面应用程序 MainWindow.xaml 的 XAML 代码,以及这段代码的顶层元素

<Window 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    x:Class="WpfApp4.MainWindow" 
    Title="Color Spinner" Height="370" 
    Width="270"> 
... 
</Window>

<Window> 元素顾名思义,用于定义一个窗口。一个应用程序可能由多个窗口组成,每个窗口都包含在一个单独的 XAML 文件中。但这并不意味着 XAML 文件总是定义一个窗口;XAML 文件可以包含用户控件、画笔和其他资源、网页等。在 WpfApp4 项目中甚至还有一个 XAML 文件定义了应用程序本身——App.xaml。回到 MainWindow.xaml,请注意 <Window> 元素包含一些非常自explanatory 的属性。有两个命名空间声明,一个用于 XML 的全局命名空间,另一个用于 x 命名空间。两者对于 WPF 功能至关重要,并定义了 XAML 语法的词汇。接下来是取自 x 命名空间的 Class 属性。此属性将 XAML <Window> 元素链接到代码隐藏中的部分类定义,在本例中是 WpfApp4.Window。这与 ASP.NET 中的工作方式类似,页面使用一个类,并使代码隐藏能够与 XAML 文件共享相同的代码模型,包括由 XAML 元素定义的控件等。请注意,x:Class 属性只能在 XAML 文件的根元素上使用。

另外三个属性,TitleHeightWidth,分别指定了要在窗口标题中显示的文本以及用于窗口的尺寸(以像素为单位)。这些属性映射到 System.Windows.Window 类的属性,WpfApp4.Window 类是从该类派生的。System.Windows.Window 类的几个其他属性使您能够定义其他功能。其中许多属性比 <Window> 元素上使用的三个属性更复杂——也就是说,它们不是简单的字符串或数字。XAML 语法允许您使用嵌套元素来指定这些属性的值。例如,Background 属性在此代码中使用嵌套的 <window.background> 元素定义如下

<Window.Background> 
  <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> 
  <GradientStop Color="#FFFFFFFF" Offset="0"/> 
  <GradientStop Color="#FFFFC45A" Offset="1"/> 
  </LinearGradientBrush> 
</Window.Background> 

此代码将 Background 属性设置为 LinearGradientBruch 类的实例。在这种情况下,画笔定义了一个从顶部到底部从白色变为桃红色的渐变。此代码中的嵌套元素还定义了另外两个“复杂”属性:“ <Window.Resources>”,它定义了动画,以及“ <Window.Triggers>”,它定义了控制动画的触发器。在查看这些属性的实现之前,值得跳到 <Grid> 元素。<Grid> 元素定义了 System.Windows.Controls.Grid 控件的一个实例。这是您可以在 WPF 应用程序中用于布局的几种控件之一。它允许您使用相对于矩形四个边缘的坐标来定位嵌套控件。

<Grid> 元素包含五个 <Ellipse> 元素(System.Windows.Shapes.Ellipse 控件)和三个 <Button> 元素(System.Windows.Controls.Button 控件)。这些元素分别定义了用于显示应用程序中的旋转图形的椭圆和用于控制应用程序的按钮。第一个 <Ellipse> 元素如下所示

<Ellipse Margin="50,50,0,0" Name="ellipse5" 
       Stroke="Black" Height="150" 
       HorizontalAlignment="Left" 
       VerticalAlignment="Top" Width="150"> 
<Ellipse.Effect> 
<BlurEffect Radius="10"/> 
</Ellipse.Effect> 
<Ellipse.Fill> 
<RadialGradientBrush> 
<GradientStop Color="#FF000000" Offset="1"/> 
<GradientStop Color="#FFFFFFFF" Offset="0.306"/> 
</RadialGradientBrush> 
</Ellipse.Fill> 
</Ellipse>

此元素定义了 System.Windows.Shapes.Ellipse 类的一个实例,该类用于显示椭圆形状,并按如下方式设置该实例的几个属性

  • Name:用于控件的标识符。
  • Margin:通过指定形状周围的边距,指示包含它的网格中由 Ellipse 控件定义的形状的位置。在此代码中,这些测量值以像素为单位。此属性如何映射到形状的实际位置取决于 HorizontalAlignmentVerticalAlignment 属性。
  • HorizontalAlignmentVerticalAlignment:用于指定 Grid 定义的矩形的哪些边缘用于布局形状。例如,设置为 LeftBottom 的值会导致形状相对于网格的左下角进行定位。
  • HeightWidth:形状的尺寸。
  • Stroke:由 Ellipse 控件定义的形状轮廓使用的画笔。
  • Fill:由 Ellipse 控件定义的形状内部使用的画笔。
  • Effect:显示 Ellipse 控件时要使用的特殊效果。

代码中的另外四个 <Ellipse> 元素非常相似。这些元素中的每一个都定义了四个彩色椭圆中的一个,这些椭圆被动画化。其中第一个元素如下所示

<Ellipse Margin="15,85,0,0" Name="ellipse1" 
        Stroke="{x:Null}" Height="80" HorizontalAlignment="Left" 
        VerticalAlignment="Top" Width="120" 
        Fill="Red" Opacity="0.5" 
        RenderTransformOrigin="0.92,0.5" > 
<Ellipse.Effect> 
<BlurEffect/> 
</Ellipse.Effect> 
<Ellipse.RenderTransform> 
<TransformGroup> 
<RotateTransform Angle="0"/> 
</TransformGroup> 
</Ellipse.RenderTransform> 
</Ellipse> 

此代码与前面的椭圆的代码非常相似,但有以下区别

  • Stroke 属性设置为 {x:null}。在 XAML 中,像这样用花括号括起来的值称为标记扩展,用于为在 XAML 语法中无法简化为简单字符串的属性提供值。在这种情况下,{x:null} 为属性指定了一个 null 值,这意味着 Stroke 不使用任何画笔。
  • 指定了 Opacity 属性,其值为 0.5。这表示椭圆是半透明的。
  • Effect 属性使用 BlurEffect,但没有 Radius 属性;在这种情况下,Radius 使用默认值 5。
  • 指定了 RenderTransform 属性。此属性设置为 TransformGroup 对象,其中包含一个转换:RotateTransform。当椭圆被动画化时,将使用此转换。它有一个指定的属性,Angle。这是椭圆旋转的角度(以度为单位),初始设置为 0。
  • RenderTransformOrigin 用于设置椭圆将由 RotateTransform 转换围绕其旋转的中心点。

最后这两个属性与 XAML 中定义的动画有关,该动画由 System.Windows.Media.Animation.Storyboard 对象定义。此对象定义在 <window.resources> 元素中,这意味着 Storyboard 对象可以通过窗口的 Resources 集合进行访问。代码还定义了一个 x:Key 属性,该属性允许通过 Resources 使用键引用 Storyboard 对象。

<Window.Resources> 
<Storyboard x:Key="Spin"> 
 ... 
</Storyboard> 
</Window.Resources> 

Storyboard 对象包含四个 DoubleAnimationUsingKeyFrames 对象。这些对象允许您指定一个包含双精度值的属性随时间变化,以及用于进一步定义此行为的详细信息。此代码中的每个元素都定义了一个彩色椭圆使用的动画。例如,前面讨论的 ellipse1 椭圆的动画如下所示

<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
     Storyboard.TargetName="ellipse1" 
     Storyboard.TargetProperty= 
       "(UIElement.RenderTransform).(TransformGroup.
           Children)[0].(RotateTransform.Angle)" 
     RepeatBehavior="Forever"> 
<SplineDoubleKeyFrame KeyTime="00:00:10" Value="360"/> 
</DoubleAnimationUsingKeyFrames> 

此时不深入探讨此元素,它指定了前面描述的 RotateTransform 转换的 Angle 属性应从其初始值更改为 360,时间周期为 10 秒,并且此更改完成后将重复一次。

在椭圆定义之后,有三个 <Button> 元素定义了按钮(请注意,示例代码中未显示 Click 属性;当您双击按钮时,IDE 会添加它)。

<Button Height="23" HorizontalAlignment="Left" 
     Margin="20,0,0,56" Name="goButton" 
     VerticalAlignment="Bottom" Width="75" 
     Content="Go"/> 
<Button Height="23" HorizontalAlignment="Left" 
       Margin="152,0,0,56" Name="stopButton" 
       VerticalAlignment="Bottom" Width="75" 
       Content="Stop"/> 
<Button Height="23" HorizontalAlignment="Left" 
     Margin="85,0,86,16" Name="toggleButton" 
     VerticalAlignment="Bottom" Width="75" 
     Content="Toggle" Click="toggleButton_Click"/> 

这些元素中的每一个都使用与前面所示的 <Ellipse> 元素相同的属性来指定 Button 对象的名称、位置和尺寸。它们还具有 Content 属性,用于确定按钮内容中显示的内容——在本例中,是按钮上显示的文本字符串。但是,按钮不限于以这种方式显示简单的字符串;如果您愿意,可以使用嵌入的形状或其他图形内容。您将在本章后面的“控件样式”部分中更详细地介绍这一点。toggleButton 按钮上的 Click 属性定义了 Click 事件的事件处理程序方法。此方法 toggleButton_Click() 实际上是一个路由事件处理程序。目前,您需要知道当您单击按钮时会引发此事件,然后会调用事件处理程序。在事件处理程序代码中,我们首先获取定义动画的 Storyboard 对象的引用。前面,我们看到该对象包含在包含它的 Window 对象的 Resources 属性中,并且它使用了 Spin 键。因此,检索故事板的代码应该不足为奇。

private void toggleButton_Click(object sender, RoutedEventArgs e)
{
    Storyboard spinStoryboard = Resources["Spin"] as Storyboard;

一旦获取,如果前面的代码没有获取 null 值,则使用 Storyboard.GetIsPaused() 方法来确定动画当前是暂停还是未暂停。如果是,则调用 Resume();否则,调用 Pause()。这些方法分别恢复或暂停动画。

if (spinStoryboard != null)
{
    if (spinStoryboard.GetIsPaused(this))
    {
        spinStoryboard.Resume(this);
    }
    else
    {
        spinStoryboard.Pause(this);
    }
  }
}

请注意,所有这些方法都需要一个指向包含故事板的对象的引用。这是因为故事板本身不跟踪时间。包含故事板的窗口有自己的时钟,故事板会使用它。通过传递窗口的引用(使用 this),故事板能够访问此。另外两个按钮 goButtonstopButton 没有在代码隐藏中链接到任何事件处理程序方法。相反,它们的功能由触发器决定。在此示例中,定义了三个触发器,如下所示。

<Window.Triggers> 
  <EventTrigger RoutedEvent="FrameworkElement.Loaded"> 
    <BeginStoryboard Storyboard="{StaticResource Spin}" 
       x:Name="Spin_BeginStoryboard"/> 
   </EventTrigger> 
  <EventTrigger RoutedEvent="ButtonBase.Click" 
        SourceName="goButton"> 
      <ResumeStoryboard BeginStoryboardName="Spin_BeginStoryboard"/> 
  </EventTrigger> 
  <EventTrigger RoutedEvent="ButtonBase.Click" 
     SourceName="stopButton"> 
        <PauseStoryboard BeginStoryboardName="Spin_BeginStoryboard"/> 
  </EventTrigger> 
</Window.Triggers> 

第一个触发器将 FrameworkElement.Loaded 事件(当应用程序加载时触发)与 BeginStoryboard 操作链接起来。此操作启动 Spin 动画。请注意,Spin 动画是如何使用 {StaticResource Spin} 代码的标记扩展语法引用的。BeginStoryboard 操作被命名为 Spin_BeginStoryboard,并在其他两个触发器中引用,这两个触发器分别连接了 goButtonstopButtonClick 事件。这些触发器使用 ResumeStoryboardPauseStoryboard 操作,这些操作正如其名称所示。

关于此 XAML 的解释在哪里?

XAML 文件的基本结构使用对象元素语法来描述对象的层次结构,其中有一个单一的根对象包含所有其他内容。对象元素语法顾名思义,描述了一个由 XML 元素表示的对象(或结构)。例如,在示例中,您看到如何使用 <Button> 元素来表示一个 System.Windows.Controls.Button 对象。XAML 文件的根元素始终使用对象元素语法,尽管如您在前面的示例中看到的,用于根对象的类不是由元素名称(<Window><Page>)定义的,而是由 x:Class 属性定义的。此语法仅在根元素中使用。对于桌面应用程序,根元素必须继承自 System.Windows.Window;对于 Web 应用程序,则必须继承自 System.Windows.Controls.Page

您已经看到,在许多情况下,当元素用于表示对象(使用对象元素语法)时,属性用于指定属性和事件。例如,前面显示的 <Button> 元素使用属性如下。

<Button Height="23" HorizontalAlignment="Left" 
   Margin="85,0,86,16" Name="toggleButton" 
   VerticalAlignment="Bottom" Width="75" 
   Content="Toggle" Click="toggleButton_Click"/> 

在此,除了 Click(它将一个路由事件处理程序分配给 toggleButtonClick 事件)和 Name 之外,每个属性都设置了 toggleButton 对象的属性值。这些都是属性语法的示例。此处使用的 Name 属性是一个特殊情况:它定义了控件的标识符,以便您可以从代码隐藏和其他 XAML 代码中引用它。属性可以用它们引用的基类限定,方法是使用句点。例如,Button 控件从 ButtonBase 继承其 Click 事件,因此您可以将前面的代码重写如下。

<Button Height="23" HorizontalAlignment="Left" 
    Margin="85,0,86,16" Name="toggleButton" 
    VerticalAlignment="Bottom" Width="75" 
    Content="Toggle" ButtonBase.Click="toggleButton_Click"/> 

通常,我们需要比简单的字符串更复杂的东西来初始化属性的值。在我们示例应用程序中,情况就是如此,例如我们使用的 Fill 属性,您将其设置为各种画笔对象。

<Ellipse ...> 
... 
<Ellipse.Fill> 
<RadialGradientBrush> 
<GradientStop Color="#FF000000" Offset="1"/> 
<GradientStop Color="#FFFFFFFF" Offset="0.306"/> 
</RadialGradientBrush> 
</Ellipse.Fill> 
</Ellipse>

在此,属性由一个子元素设置,该子元素的名称遵循以下约定:[父元素名称].[属性名称]。这被称为属性元素语法。

© . All rights reserved.