每次要学点新东西时,我都会发现使用教程比阅读文档要简单、省事得多。假定大多数人都有同感,难道您真的会在尝试使用某个东西之前先阅读说明吗?我肯定不会。为此,我已经决定直接付诸行动,演练如何构建一个 Windows Presentation Foundation (WPF) 应用程序。由于这是 Coding4Fun,而且世界上有足够多的企业-Web 2.0-数据-门户-小玩意,那么就让我们面对这个现实,来准备做一个游戏吧!遗憾的是,我认为在此直接制作 Halo 3 游戏并不现实,因此,我认为更小的游戏可能更合适,也就是说在这些教程结束时能够完成的游戏,比如说数独游戏。嗨,好处是您可以在工作中玩数独游戏而不会受惩罚!好吧,那么我们要怎么开始呢?按以下顺序安装:
• | |
• | |
• | |
• |
还需要运行 Windows XP SP2、Windows Server 2003 SP1 或 Vista February CTP。
当然,我的工作站上碰巧安装了所有这些软件(太好了)。不过既然一切都已经安装好了,就让我们开始吧。激活 VC#,新建一个项目,然后选择"WinFX Windows Application"。(如果看不到这一项,您应该确保安装了 Visual Studio Extensions,因为该项是一项新增的内容。)。我输入"SudokuFX"作为项目名称,您将在屏幕快照中看到该项目名,但这取决于您给项目起了什么名字。您马上就会看到以下内容:
<Window x:Class="SudokuFX.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="SudokuFX" Height="300" Width="300" > <Grid> </Grid> </Window>
这是 XAML,Microsoft 新的声明性编程语言。XAML 中的类可以同时具有代码(C# 或 VB.NET)组件和声明性 (XAML) 组件。Window 标记的 x:Class 属性指定了我们正在定义的类的 C# 组件名。C# 部分应当如下所示:
namespace SudokuFX { public partial class Window1 : Window { public Window1() { InitializeComponent(); } } }
编译并运行该程序后,这两部分结合在一起从而创建一个完整的 Window1 类,该类从 WinFX 提供的 System.Windows.Window 类派生。在这里,您唯一需要了解的还不明确的事情是,用 XAML 声明的所有对象均可从代码访问,反之亦然。事实上,根本不需要使用 XAML,您也可以使用相同的 API 编写一个类!嵌套元素放在一个集合中,该集合存储为这些元素父级的 Children 属性,或者存储为分配给 Content 属性的一个对象。换言之,这段 XAML 代码创建了一个 Grid 实例,并将其放在窗口的 Content 属性中。
那么,我们如何开始着手设计应用程序的布局呢?基本上,理想的布局是:顶端有一个标题,左边是游戏菜单(类似于 Windows 资源管理器中的"任务"窗格),右边是游戏计时信息,底部是移动历史记录,中间是板。当然,这都应该与分辨率无关,而且能够动态调整大小和流动。(如果您以前使用过 Windows 窗体,那么当您偏头痛发作时,额头可能已经开始冒汗了,不过不用担心,动态调整大小和流是 WinFX 的默认设置)。最适合该任务的容器是明显命名的 DockPanel。DockPanel 是一个容器控件,它根据子级要放置到哪一边来安排它们;然后,添加的最后一个元素用于填充剩余空间。例如,用以下内容替代 Grid 标记:
<DockPanel> <TextBlock Background="Red" DockPanel.Dock ="Top" FontSize ="36"> Sudoku </TextBlock> <StackPanel Background="Green" DockPanel.Dock ="Left"> <Button>A</Button> </StackPanel> <StackPanel Background="Blue" DockPanel.Dock ="Right"> <Button>B</Button> </StackPanel> <ListBox Background ="Gray" DockPanel.Dock="Bottom"/> <StackPanel Background ="Yellow"/> </DockPanel>
DockPanel.Dock 语法用于引用每个子级,但要特定于容器的属性。另一个可以说明这种情况的示例是 Canvas.Left 属性,它与 Canvas 中的元素一起使用,Canvas 是一个允许显式确定元素位置的容器控件。StackPanel 容器在垂直堆栈或水平堆栈中排列自己的子级:向上、向下、向左,或向右。在该示例中,我添加了一些按钮来填补面板,这样面板就不会缩小到没有了。我还指定了一些过分鲜艳刺眼的背景色(稍后会删除),以便准确显示所有内容的摆放位置。如果编译并运行该程序,会得到以下奇怪的结果:
好吧,现在该努力编写一些代码来运行基本的用户界面 (UI) 了。首先,我们来布置左面板。其中将包括主菜单和新的游戏设置。我们应该将它们放在扩展器控件中,以防任何使用我们程序的人以 800x600 的分辨率运行(我知道这很恐怖,但我见过)。
<Expander IsExpanded ="True" Header ="Main Menu"> <StackPanel> <Button>New Game</Button> <Button>Load Game</Button> <Button>Save Game</Button> <Button>Quit</Button> </StackPanel> </Expander> <Expander IsExpanded ="True" Header ="New Game Settings"> <StackPanel> <TextBlock>Board Size:</TextBlock> <ComboBox IsEditable ="False"> <ComboBoxItem IsSelected ="True">9x9</ComboBoxItem> <ComboBoxItem>16x16</ComboBoxItem> <ComboBoxItem>25x25</ComboBoxItem> <ComboBoxItem>36x36</ComboBoxItem> </ComboBox> </StackPanel> </Expander> </StackPanel>
这会生成以下界面:
好吧,现在可能看起来不太可怕,但等一下:很快就会有其他内容了!以类似方式充实该 UI 的其余部分,并为板(在以后的教材中,我们将制作板)添加虚拟图像,我们将获得以下界面:
现在,我们应该如何改进该 UI 的外观呢?一种方法是单独调整控件的所有属性,使其外观看起来更好;另一种方法是使用某种皮肤设置。遗憾的是,这两种方法向您的代码中添加了一堆乱七八糟的东西,且难以维护。相反,我们可以使用 WPF 的样式概念。通过定义一组新的默认属性值,WPF 使得在给定范围内(例如,作为一个整体的应用程序、一个特殊的对话框,或者一个容器)自定义控件变得十分简单。可以通过将样式添加到其他对象的 Resources 集合中来实现该任务。在 XAML 中,如果要将一个更复杂的对象或对象集合分配给一个属性,可以将该属性的语法从 Property="Value" 扩展为语法:
<Object.Property> <ValueObject/> </Object.Property>
例如,如果没有显式设置为其他颜色,下面的代码将蓝色指定为应用程序中所有按钮的背景色:
<Application.Resources> <Style TargetType ="{x:Type Button}"> <Setter Property ="Background" Value ="Blue"/> </Style> </Application.Resources>
也可以使用 x:Key 属性来命名样式。之所以使用 x:Key 而不使用 x:Name,是因为 <Application.Resources> 实际上是一个键值对列表。例如,我们可以定义:
<Application.Resources> <Style x:Key ="BlueButton" TargetType ="{x:Type Button}"> <Setter Property ="Background" Value ="Blue"/> </Style> </Application.Resources>
这段代码创建了一种仅适用于某些按钮的样式,这些按钮通过其 Style 属性引用该样式。例如:
<Button Style="{StaticResource BlueButton}"/>
也可以定义窗口、面板或其他具有 Resources 属性的对象中的资源。当代码引用某个资源时,进行应用程序级别的搜索。这样,<Window.Resources> 中定义的样式或其他资源就只能在该窗口内应用。
到目前为止,我尚未解释过一直使用的括号符号。本质上说,括号用于声明对其他对象的引用,而不是对其进行实例化。例如,{x:Type Button} 引用 Button 类,而 {StaticResource BlueButton} 搜索具有"BlueButton"键的项目的资源层次。StaticResource 指令指出,该搜索仅在首次创建对象时执行;相反,DynamicResource 指令指定该值应不断更新。
现在,我们可以开始添加一种样式以改善扩展器的外观。我将以下内容添加到 <Application.Resources> 部分中:
<Style TargetType ="{x:Type Expander}"> <Setter Property ="Background"> <Setter.Value> <LinearGradientBrush StartPoint ="0,0" EndPoint ="1,0"> <LinearGradientBrush.GradientStops> <GradientStop Color ="LightGray" Offset ="0"/> <GradientStop Color ="Gray" Offset ="1"/> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Setter.Value> </Setter> <Setter Property ="BorderBrush" Value ="DimGray"/> <Setter Property ="BorderThickness" Value ="1"/> <Setter Property ="Margin" Value ="5"/> <Setter Property ="HorizontalContentAlignment" Value ="Stretch"/> <Setter Property ="Foreground" Value ="White"/> <Setter Property ="VerticalContentAlignment" Value ="Stretch"/> </Style>
这是一种比较简单的样式,它很好地演示了双属性分配语法。背景设置为两种灰度之间的水平渐变,其余属性设定为与背景很好融合的合理的默认值。通过在扩展器内容中添加一个 Border 标记,我还在控件内部的周围添加了一个额外的边框,如下所示:
<Expander IsExpanded ="True" Header="Main Menu"> <Border Margin ="5" Padding ="10" Background ="#77FFFFFF" BorderBrush ="DimGray" BorderThickness ="1"> <StackPanel> <Button>New Game</Button> <Button>Load Game</Button> <Button>Save Game</Button> <Button>Quit</Button> </StackPanel> </Border> </Expander>
Margin 属性指定扩展器控件外部的边框间距,Padding 属性指定内部的额外间距。请注意,只有容器控件才有内部间距。这两个值由四个逗号分隔的值(分别针对左、上、右、下)指定,如果四个值相同(如此处所示),则用一个数字指定。
如果您觉得这有点儿像杂牌军,那就对了。事实上,我本来可以使用自定义样式在控件自身内部包括额外边框。为此,您可以修改所谓的控件模板,但那是另一个话题,我不会在此进行讨论。不要担心,我们稍后会回来修正这个问题。制定了其他一些样式并为窗口背景添加了一个很好的渐变之后,应用程序看上去好多了……嗯,至少色彩更丰富了!如果下载该代码,您可以查看这些样式。
最后,为了完成教程的这一部分,我们添加一些简单的事件处理以使 Quit 按钮工作。实际上非常简单。只需定义一个符合 Window1 类中 RoutedEventHandler 委托的方法。例如:
void QuitClicked(object sender, RoutedEventArgs e) { this.Close(); }
然后,只需将按钮的 Clicked 属性设置为处理程序的名称,如下所示:
<Button Click ="QuitClicked">Quit</Button>
现在,如果您单击 Quit 按钮,程序将退出。这实际上是处理简单事件所需的一切操作!