WPF使得在你的应用程序中使用图形很容易,以及更容易开发你的显卡的能力。这有很多图形构架的方面来达到这个目标。其中最重要的是综合。
7.1.1综合
图形化元素可以组合到你的用户界面的任何一部分中。很多GUI技术易于将图形分离到一个独立的世界。这就需要一个“操纵杆”——当移动一个世界的按钮、文本框和其它框架到另一个世界的Shape和图像中,由于在很多系统中,这两个世界有不同的编程模型。
例如,Windows Forms和Mac OS的Cocoa都提供了在窗体中排列控件的能力,以及建立一个与这些控件交互的程序。它们还提供了一些API,这些API提供了高级的、完全的可伸缩的2维空间绘图工具。(在Windows Forms中为GDI+,在OS X中为Quartz 2D。)但是这些绘图API是不同于控件API的。绘图基础是非常不同于这些系统中的控件的,你不可以自由的混合这两种。
WPF,另一方面,把Shape作为UI树中的元素,就像其他元素一样。因此,我们可以自由的混和它们在任意类型的元素中。示例7-1显示了各种各样的例子。
示例7-1
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<TextBlock>Mix</TextBlock>
<Rectangle Fill="Red" Width="20" />
<TextBlock>text</TextBlock>
<Ellipse Fill="Blue" Width="40" />
<TextBlock>and</TextBlock>
<Button>Controls</Button>
</StackPanel>
<Ellipse DockPanel.Dock="Left" Fill="Green" Width="100" />
<Button DockPanel.Dock="Left">Foo</Button>
<TextBlock FontSize="24" TextWrap="Wrap">
And of course you can put graphics into
your text: <Ellipse Fill="Cyan" Width="50" Height="20" />
</TextBlock>
</DockPanel>
正如你看到的,图形化元素可以被无缝隙的添加到标记中。外观和图形一起工作——正如它和任何其它元素一起工作一样,结果如图7-1所示。
图7-1
虽然这个例子是在xaml中,你还可以使用代码创建元素。本章的大多数示例都使用xaml,因为标记的结构直接反射了被创建对象的结构。然而,究竟使用代码还是标记依赖于你正在做什么。如果你是在创建绘图,你多数会使用一个xaml来为这些绘图创建xaml,但是如果你从数据中创建图形,从代码中做些事情就是有意义的。本章显示的大多数技术可以使用在代码或标记中。参见附录A获取更多XAML和代码间关系的信息。
不仅图形和其它内容可以在标记中并排放置在一起,它们还可以混合在一起。注意到图7-1,右手边的椭圆,被放置在TextBlock中。如果你想达到在WindowsForms中的排列效果,只使用Label控件是不够的,你将不得不写一个新的控件,从头开始绘制这些文本和椭圆。这种混合有两种方式,不仅你可以把你的控件混合到你的图形中,还可以使用图形化元素来自定义你的控件外观——如第五章所描述的。
这种混合不只是像文本块这样的简单元素,同时也适用于控件。例如,图7-2显示了一个按钮,其中混合了文本和图形作为标题。
图7-2
传统的Windows中,你可以得到这种效果,以来于按钮可以被显示为一张图片。但是图片是相当没有弹性的,它们只是一个混合了图片的块,因此你不能容易的使图片的一部分产生交互或使选中的部分响应用户输入以产生动画。因此,在WPF中,把图形放入按钮,这种方式工作的有点不一样。按钮的标记显示在示例7-2中。
示例7-2
<StackPanel Orientation="Horizontal">
<Canvas Width="20" Height="18" VerticalAlignment="Center">
<Ellipse Canvas.Left="1" Canvas.Top="1" Width="16" Height="16"
Fill="Yellow" Stroke="Black" />
<Ellipse Canvas.Left="4.5" Canvas.Top="5" Width="2.5" Height="3"
Fill="Black" />
<Ellipse Canvas.Left="11" Canvas.Top="5" Width="2.5" Height="3"
Fill="Black" />
<Path Data="M 5,10 A 3,3 0 0 0 13,10" Stroke="Black" />
</Canvas>
<TextBlock VerticalAlignment="Center">Click!</TextBlock>
</StackPanel>
</Button>
当然,带有图像的按钮并不是一个新事物,但是传统上,按钮允许设置一张图片来支持于此。例如,Windows Forms中,按钮有一个Image属性,以及在Cocoa中,NSButton有一个setImage方法。这种实现使相当没有弹性的——控件允许设置一个单独的标题和一张单独的图片。比较示例7-2,这里使用了StackPanel在按钮内部布局以及只增加它需要的内容。你可以使用任意的外观面板在Button中,带有任意类型的内容。示例7-3使用了一个Grid来排列文本和一些椭圆在Button中。结果如图7-3所示。
示例7-3
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Fill="Blue" Width="10" Height="10" />
<Ellipse Grid.Column="2" Grid.Row="0" Fill="Blue" Width="10" Height="10" />
<Ellipse Grid.Column="0" Grid.Row="2" Fill="Blue" Width="10" Height="10" />
<Ellipse Grid.Column="2" Grid.Row="2" Fill="Blue" Width="10" Height="10" />
<Ellipse Grid.ColumnSpan="3" Grid.RowSpan="3" Stroke="LightGreen"
StrokeThickness="3" />
<TextBlock Grid.Column="1" Grid.Row="1" VerticalAlignment="Center">Click!</
TextBlock>
</Grid>
</Button>
图7-3
在WPF中,很少需要提供非弹性属性如Text或Image的元素。如果一个表示内嵌内容的元素是有意义的,那么它将会表示无论什么你选择提供的混合型元素。
如果你熟悉二维绘图技术如Quartz 2D,GDI+或者旧式的GDI32,另一种高级的绘图方式会使你震惊。我们不再需要写一个函数来响应重画的请求。WPF可以为我们保持屏幕重画。这是因为WPF使我们表示将图形表示为对象。
7.1.2绘图对象模型
有很多GUO技术,想自定义外观的应用程序,需要能够在一开始就创建它们的外观。通常显示一个自定义的外观的技术是写代码来表现一系列绘图操作,从而创建显示。这些代码将会在相应的图形第一次需要显示的时候运行。在一些系统中,OS不会保留应用程序绘图的副本,因此改方法会以运行时每次需要重画某个区域而告终,例如,如果一个窗体是不透明而且没有被盖住的。
在使用这个方法时,在这个生成函数构建整张图片时,更新独立的元素通常是有问题的。即使是OS获得绘图的一份副本的地方,通常是获取到一个图像。这意味这一旦你想要改变绘图的一部分,你通常需要重画改变区域的每一个事物。
WPF提供了一种不同的方式:你可以添加表示图形化形状的对象到用户元素树上。形状元素是UI树中和其它元素一样的对象,因此你的代码可以在任何时刻修改它们。如果你改变某个有可视化效果的属性,如Size,Location,或Color,WPF将会自动更新显示。
为了说明这个技术,示例7-4显示了一个简单的包含一些椭圆的窗体。其中每个椭圆都由一个Ellipse对象表示,我们将在后台代码文件中更新显示。
示例7-4
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
Text="Change Item"
>
<Canvas>
<Ellipse Canvas.Left="10" Canvas.Top="30" Fill="Indigo"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
<Ellipse Canvas.Left="20" Canvas.Top="40" Fill="Blue"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
<Ellipse Canvas.Left="30" Canvas.Top="50" Fill="Cyan"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
<Ellipse Canvas.Left="40" Canvas.Top="60" Fill="LightGreen"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
<Ellipse Canvas.Left="50" Canvas.Top="70" Fill="Yellow"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
</Canvas>
</Window>
每个椭圆的MouseLeftButtonDown事件都由定义在该窗体对应后台文件的OnClick方法处理。如示例7-5所示,OnClick方法简单的增加了Width属性——当Ellipse激发这个事件的时候。点击的结果使这个椭圆更加宽了。
示例7-5
using System.Windows.Shapes;
namespace ChangeItem
{
public partial class MainWindow : Window
{
public MainWindow( ) : base( )
{
InitializeComponent( );
}
private void OnClick(object sender, RoutedEventArgs e)
{
Ellipse r = (Ellipse) sender;
r.Width += 10;
}
}
}
如果我们使用旧的方法在一个单独的生成函数中绘制每一个事物,这样的代码对于更新显示不充分地。正规地,这将告诉OS,屏幕不再是有效地,导致激发一个重画请求。但是在WPF中,这是不必要的——当你在Ellipse对象上设置一个属性时,WPF保证了屏幕会相应地更新。进一步说,WPF知道所有这些交叠项,正如图7-4所示,因此,为了获得正确的结果,它将会根据需要上上下下地重绘这些项。所有你必须去做的时调整这个对象的属性。
图7-4
由于GUI第一次开始呈现,尽管计算机内存容量已经成数量级的增长,仍然有一些情形,使得绘制这个对象模型的方法可能会非常昂贵。尤其是,因为应用程序处理的是巨大的数据集,如maps,在UI树中有一组完全的对象,镜像映射了底层数据的机构,可能使用太多的内存。用于,为了某中类型的图形或数据,它可能更便利的使用旧有的生成代码的样式。为此,WPF还支持一些轻量级的操作模式。这将在本章的后面进行介绍——在“可视化层次编程”一节。
你可能已经注意到,到目前为止,我们已经做的所有的图画都使用形状而不是图片,WPF当然支持图片,但存在一个好的使用形状的原因。几何形状可以被伸缩和旋转而不损失图片质量。高质量的变型是WPF绘图中的一个重要特征。
7.1.3分辨率的独立性
当GUI第一次显示时,不仅显示卡可以显著的提高性能,显示器也可以。长期以来,唯一的主流显示技术是CRT。CRT的颜色只提供了有限的分辨率。它们努力显示清晰度高于100px/I的图片。然而,纯平显示器,现在卖的比CRT多,可以在很大的余地胜过它。
本书的作者之一有一台使用两年的膝上型电脑,分辨率为150px/I。写作本书的时候,显示器可利用超过了200px/I。创建更高象素密度是可能的。尽管如此,在当前操作系统使用这些显示器仍然存在一个问题。每个事物都会以很小以至于不可用而告终。这是因为一种基于象素的开发文化,大多数应用程序使用象素按量配给它们的用户界面。
这并不是完全的技术限制的结果。从Wndows NT使用到现在,以一种独立于分辨率的方式绘图是可能的,因为绘图API和GDI32运行你应用变型到所有你的绘图中。在2001引进的GDI+,提供了同样的机制。但是正因为有效的样式并不意味着应用程序就可以使用它。大多数应用程序并不暴露它的可伸缩性。
不幸的是,在Win32中,将图形和其他UI元素分隔开意味着,即使应用程序没有暴露绘图API中的可伸缩性,其他UI部分不会自动地遵循。图7-5显示了一个Windows Forms应用程序,使用了GDI+来绘制图形,可以任意的大小伸缩。
图7-5
注意到在图7-5中,虽然星星和“Hello World”文本已经被伸缩了,Trackbar和Label控件却并没有这样。这是因为绘图变型仅仅影响你使用GDI+绘制的那些图形。它们并不影讯整个UI。尽管Windows Forms提供了一些伸缩其余UI元素的样式,但这不是自动的。你不得不采用深思熟虑的非平凡的措施来建立一个独立于分辨率的UI在Windows Forms中。
7.1.3.1伸缩和旋转
通过在底层支持变形,WPF解决了这样一个问题。取代以只在二维绘图级别提供伸缩性,WPF将其生成在底层的合成的引擎中。结果是UI中的每一个事物都可以被变形,不仅仅是用户绘制的图形。回到示例7-2我们简单的带有笑脸的按钮,我们可以通过只改变第一行来使用这个伸缩性:
LayoutTransform属性在WPF所有的用户接口元素中是有效的,因此你可以伸缩整个窗体如同一个按钮一样简单。多种类型的变形是可以利用的,这将在后面更详细的讨论。目前,我们简单地要求扩大一个按钮——通过x和y方向上的因数3。图7-6显示了扩大后地按钮。
图7-6
比较原始的图7-2和图7-6,后者比前者更明显,正如你所希望的。更加意味深长地,细节变得更零碎了。按钮的圆角边缘比它们在一个较小的版本中更加容易看到。后者的形状定义得更好。而且我们的图形更加清晰。我们获取这种清晰度是因为WPF生成的按钮看上去和详细指明大小的按钮一样好。比较图7-6和图7-7中的示例。
图7-7
图7-7显示了发生了什么——如果你简单的扩大原始的小按钮的图像。有很多种扩大图像的不同方式。左边的示例使用了简单的算法,称为“”,有时又称为“”。为了是这张图片更大一些,象素被简单的重复了。这导致了对这张图像一种很方正的感觉。右边的示例使用了一种比较高级的插值算法。这使得圆角看起来是圆的,同时不会遭受矮矮胖胖的象素效果,而这会导致看起来很模糊。清晰,两个图像中没有一个可以达到图7-6的效果。
7.1.2分辨率,坐标系统,和“象素”
对伸缩图形的支持,意味着在你的应用程序中使用的坐标系统和屏幕使用的坐标系统间没有固定的关联。如果运行在一台高DPI的显示器上,即使你不用伸缩变形,变形会自动应用到你整个的应用程序上——这是真实的。
在WPF中,如果不是物理性象素,那么默认的度量单位又是什么呢?答案是,有点混淆,象素。为了更加精确,真实的答案是,独立于装置的象素。
独立于装置的象素,WPF官方的定义是1/96每象素。如果你详细指出一个形状的宽度为96px,就意味着这是1英寸宽。WPF会使用与所需同样多的物理象素来填充一英寸,例如,高分辨率的膝上型显示器,分辨率达到150px/I。因此如果你绘制一个宽为96px的象素,WPF将生成150px/I的宽度。
WPF依赖于系统级显示器设置,来决定物理象素的大小。你可以通过Windows Display Properties程序来调整它们。右击“桌面”,选择“属性”来显示这个程序,进下来进入到“设置”条。点击“高级”按钮,在打开的对话框中,选择“通用”条,这将告诉Windows你的显示器分辨率。
你可能想知道为什么WPF使用有点奇怪的选择1/96英寸,以及为什么叫“pixel”。原因是,96dpi是Windows中默认的DPI——当运行时使用正常的字体,因此长期考虑pixel正常的大小。这意味着在带有正常象素密度的显示器上,WPF会伸缩你的绘图为了它们保持正确的物理大小。
WPF为每一个伸缩优化图形化样式生成的能力,意味着它能理想的被用于利用增长的显示器分辨率。作为第一次,屏幕上的文字和图形会竞争这些清楚的碎片——我们已经希望来自打印机的。当然,为了所有实际中的工作,我们需要一组全面的绘图基础。
7.1.4形状,笔刷和钢笔
WPF绘图工具中的大多数类被归类为三种类型:形状,笔刷和钢笔。在这些主题上有很多变体,我们将会在后面详细检查它们。然而,为了完全获取任何地方的图形,对这些类一个基本的了解是强制的,
形状是用户界面树中的对象,提供了用于绘图的基本的创建块。我们已经看到的Ellipse、Path和Rectangle元素都是形状对象的例子。同样支持线条——基本线条和多段线条,分别使用Line和PolyLine。还可以创建任意的填充形状。Polygon支持所有边都是直的形状,而一旦你需要弧形的边,Path类支持填充如同弧形一样的形状。图7-8显示了使用中的每一个形状。
图7-8
不管你选择的是什么图形,你需要决定如何对其进行着色。为此,可以使用Brush。有很多可利用的Brush类型,最简单的是单颜色的SolidColorBrush。你可以使用LinearGradientBrush或RadialGradientBrush达到更有趣的可视化效果。这些允许在界面的形状上改变颜色,这是一种很好的办法——提供了很深的印象。你也可以基于图像创建笔刷,ImageBrush使用了一个图片,DrawingBrush使用了一个可伸缩的绘图。最后VisualBrush使你在任何可视化树、任何用户界面中你喜欢和使用的大块上绘制其他的形状。这使得达到想反射你的用户界面的全部的效果很简单。
最后,钢笔用于绘制形状的轮廓。钢笔其实只是一个增强的笔刷。当你创建一个Pen对象的时候,你给了它一个Brush来告诉它应该怎么在屏幕上画画。Pen类只是增加了一些信息如线条的厚度,阴影样式。图7-9显示了一些可用的效果,使用了笔刷和钢笔。
图7-9
7.1.5合成
图形构架的最后一个关键样式是合成。在计算机图形中,名词“合成”是指将多个形状和图片联合在一起形成最后输出的过程。WPF的合成模型是不同于传统Windows工作模型的,对于创建支持高质量可视化,这是至关紧要的。
在经典的Win32模型中,每个用户界面元素都独占它所拥有的应用程序窗体中的某个区域。在每一个顶级窗体中,任何在窗体中给定的象素都完全由那一个确切的元素控制。这就防止了元素是局部透明的。它同样排除了在元素的边缘使用抗锯齿处理,当联合非矩形元素的时候,这个技术是尤其重要的。虽然各种各样的hack已经被分成提供幻想在Win32中的透明度,它们有局限以及可以有点便利的与之工作。
WPF的组合模型支持任何形状的元素以及允许它们交叠。它还允许元素有任意局部或完全的透明化区域的混合。这意味着任何给定的屏幕上的象素都可能有多个有作用的可视化元素。进一步,WPF使用锯齿化处理在所有形状的边缘。这样减少了参差不齐的外观,更简单的绘图技术可以在屏幕上生成,导致一个平滑外观的图像。最后,组合引擎允许任何元素在组合前,将变形应用上去。
WPF的组合引擎使用了现代显卡来加速绘图的过程。在内,这可以在Direct3D模型的顶层实现。这看起来是多余的,因为多半WPF的绘图功能都是二维的,但是大多数面向3维的功能,在现代显卡上,可以还被用于绘制二维形状。例如,象素阴影可以被用于实现高级的ClearType机制,当组合文本到UI中。WPF开发了同样超快的多边形绘图机制——由SD游戏用来生成基本形状。进一步,WPF在显卡上缓存了可视化树的一部分,显著的促进了重绘的性能——当在屏幕上改动小的细节时。
现在让我们看一下支撑WPF图形系统的核心概念,让我们仔细的看一下细节。