第二章 2D 图形学简介——基于WPF
2.1 引言
选择具体的图形学平台:微软的 Window Presentation Foundation(WPF) WPF 是同时支持 2D 和 3D应用的现代图形学平台
2.2 2D图形流水线概述
- 2D图形平台是应用程序和显示硬件的中介,提供的功能与输入和输出相关联。
- 应用通常是将某些数据(称为 应用模型 (Application Modle), AM) 转化为图像
- 应用程序开设客户区域有两个目的:
- 一部分用于应用程序的用户界面(UI)控制
- 其余部分为 视图,用来显示 场景 绘制的结果。显示内容由 应用程序的 场景生成器 模块从 AM 中提取或导出
2.3 2D图形平台的演变
- 从整数坐标到浮点数坐标
- 早期的2D光栅图形平台采用整数坐标
- 应用程序并非对单个像素进行着色,而是通过调用绘制 基元 的程序来绘制场景。基于可以是 几何形状 或 预先读入的矩形图像(通常叫做位图)。每一个几何基元的外观由具体的属性参数控制。
- 应用程序采用整数坐标,可一对一地直接映射屏幕像素。
- 原点 (0, 0) 置于画布的左上角,x 值按从左至右的方向,y值按从上到下的方向
-
- 应用程序通过函数对每个基元进行设置
- 采用整数坐标系,显示大小取决于输出设备的分辨率。同样的图形,如果输出设备的分辨率更大,则输出的图形会更小
- 早期的2D光栅图形平台采用整数坐标
-
- 之后图形平台采用 浮点数坐标系将详细的几何数据与具体设备的特性分离开。 整数坐标系对应 物理坐标系统 浮点数坐标系对应 抽象坐标系统
- 即时模式与保留模式
- 基于整数的数据 和 基于浮点数数据这两种表示 最终形成了 两种不同目标和功能的架构:即时模式(IM) 和 保留模式(RM)
- 即时模式当绘制函数被调用时,立即执行任务,将几何基元的坐标映射为设备坐标,并在显示缓冲器中对相关像素进行着色,然后将控制权返回给应用程序。
- 在即时模式下,程序员的工作就是:对绘制图像做任何修改时,让场景生成器遍历 AM,重新生成表示场景的基元集合
- 保留模式在一个专用数据库中保留了需绘制或观看的场景的表示,我们称之场景图。
- 应用程序的 UI 和 场景生成器 使用 RM平台的 API 构建场景图。可通过编辑场景图对场景进行增量式修改。任何增量式修改都会导致 RM平台的同步显示器自动更新客户区域的绘制结果。
- RM平台除了承担显示任务外,还可以承担许多与用户交互相关的任务
- 即时模式当绘制函数被调用时,立即执行任务,将几何基元的坐标映射为设备坐标,并在显示缓冲器中对相关像素进行着色,然后将控制权返回给应用程序。
- 过程语言 与 描述语言
- 为了给出对用户接口和场景的详细描述,图形平台提供以下两种技术:
- 面向过程的代码:采用命令式编程语言编写(通常是面向对象的,但并非必须如此),可通过大量的图形 API 与显示设备进行交互。例如,Java Swing、WPF、DirectX 等
- 描述性语言:通过标记语言表达,例如 SVG 或 XAML
- 最底层:面向对象的 API
- 核心层是一组提供所有 WPF 功能的类,程序员可以在这一层使用 .Net 语言来定义应用的对外接口和行为。仅通过这一层就可以创建一个 WPF 应用程序。但另外两层可以提高开发效率。
- 中间层:XAML
- 中间层 提供了定义 API 大部分功能的另一种途径,它使用描述语言 XAML,其语法与 HTML 和 XML 类似。可通过对描述性语言的解释性执行程序支持应用的快速原型实现。
- 最高层:工具
- WPF 的最高层集成了设计师和工程师用于生成 XAML 的实用程序包括 绘图工具、3D建模工具、创建复杂用户界面的工具
2.4 使用 WPF 定义 2D场景
在 2.4 节中,我们将构造一个简单的 XAML 应用程序,来显示一个模拟时钟
- 像 HTML 一样,XAML 也给出元素的层次化结构,但其元素类型不同,它包含:
- 布局面板(例如,负责对紧密安放在一起的控件或菜单进行空间布局的 Stack Panel)
- 用户界面控件(例如,按钮 和 文本输入框)
- 用于绘制场景的 Canvas(画布)
- 第一个例子,用 XAML 创建一个Canvas(画布):
<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
Cx,lipToBounds="True"
>
</Canvas>
- 通常都将 ClipToBoudns 设为 True,它将保证画布是有界的,即不会再指定的矩形区域之外显示任何数据
- 我们没有指定画布大小
- XAML 具有某些语法特性(例如上面提到的 Canvas 标签中奇特的 xmlns 性质),但它们并不会掩盖标签和属性的语义。
- 采用抽象坐标系定义场景
- 在图纸上建立 抽象坐标系,在抽象坐标系中我们不需要关心其一个单位的长度的具体物理单位(是 米 还是 厘米 还是 毫米)
- 原点为 (0, 0) x 从左 到 右 y 从上 到 下
- 绘制顺序:按从后(离观察者最远处) 到 近 的顺序绘制。即最开始绘制时钟的钟面
- 在图纸上建立 抽象坐标系,在抽象坐标系中我们不需要关心其一个单位的长度的具体物理单位(是 米 还是 厘米 还是 毫米)
XAML:
<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
ClipToBounds="True">
<Ellipse
Canvas.Left="-10.0" Canvas.Top="-10.0"
Width="20.0" Height="20.0"
Fill="LightGray"/>
</Canvas>
- 此时会发现抽象坐标系带来的问题
- 抽象坐标映射到显示设备上后,显示出的灰色圆太小
- 只能看到 四分之一的圆
- 这是由于我们的抽象坐标系 并不 适应WPF画布坐标系统。我们希望钟面的半径为 1英寸,而对应 WPF坐标系统,96个单位 = 1英寸。因此,我们需要定义一个 抽象坐标系 到 WPF坐标系 的 显示变换
- 我们抽象坐标系 20个单位 映射到 WPF坐标系上 希望是其96个单位,那么就是x、y轴都乘上 96/20 即 4.8。可以通过 RenderTransform 实现比例变换
- 抽象坐标映射到显示设备上后,显示出的灰色圆太小
<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
ClipToBounds="True">
<Canvas>
<Ellipse
Canvas.Left="-10.0" Canvas.Top="-10.0"
Width="20.0" Height="20.0"
Fill="LightGray" >
</Ellipse>
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="4.8" ScaleY="4.8" CenterX="0" CenterY="0"/>
</TransformGroup>
</Canvas.RenderTransform>
</Canvas>
</Canvas>
- 坐标系/基原则:始终选择你工作作为方便的坐标系或基,并通过变换使它和不同的坐标系或基关联起来
- 此时,我们仍是只能看到四分之一圆,我们再添加一条 平移变换:
<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
ClipToBounds="True">
<Canvas>
<Ellipse
Canvas.Left="-10.0" Canvas.Top="-10.0"
Width="20.0" Height="20.0"
Fill="LightGray" >
</Ellipse>
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="4.8" ScaleY="4.8" CenterX="0" CenterY="0"/>
<TranslateTransform X="48" Y="48"/>
</TransformGroup>
</Canvas.RenderTransform>
</Canvas>
</Canvas>
- 比例变换 和 位移变换的执行顺序不同,结果也不同。即它们是相关联的
- 构造并使用模块化模板
- WPF 中 有重用模板(控制模板 Control),对于我们指针的时针和分针,它们的形状是相同的,只是缩放不同,因此我们可以先构造 模板,然后通过 模板 来实例化出具体的元素
<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
ClipToBounds="True">
<Canvas>
<Canvas.Resources>
<ControlTemplate x:Key="ClockHandTemplate">
<Polygon
Points="-0.3,-1 -0.2,8 0,9 0.2,8 0.3,-1 "
Fill="Navy"/>
</ControlTemplate>
</Canvas.Resources>
<Ellipse
Canvas.Left="-10.0" Canvas.Top="-10.0"
Width="20.0" Height="20.0"
Fill="LightGray"
/>
<Ellipse
Canvas.Left="-2" Canvas.Top="-10.0"
Width="4" Height="4"
Fill="Blue"
/>
<Control Name="MinuteHand"
Template="{StaticResource ClockHandTemplate}"
/>
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="4.8" ScaleY="4.8" CenterX="0" CenterY="0"/>
<TranslateTransform X="60" Y="60"/>
</TransformGroup>
</Canvas.RenderTransform>
</Canvas>
</Canvas>
- - 在 Canvas.Resources 内通过 ControlTemplate 定义模板,每个模板必须赋予一个唯一的名称(x:Key属性)
- 之后通过 Control 使用 Name 属性调用之前定义的模板进行实例化
- 我们再实例化出一个时针,并为该时针 即 Control 元素,添加 缩放 和 旋转变换
<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
ClipToBounds="True">
<Canvas>
<Canvas.Resources>
<ControlTemplate x:Key="ClockHandTemplate">
<Polygon
Points="-0.3,-1 -0.2,8 0,9 0.2,8 0.3,-1 "
Fill="Navy"/>
</ControlTemplate>
</Canvas.Resources>
<Ellipse
Canvas.Left="-10.0" Canvas.Top="-10.0"
Width="20.0" Height="20.0"
Fill="LightGray"
/>
<Ellipse
Canvas.Left="-2" Canvas.Top="-10.0"
Width="4" Height="4"
Fill="Blue"
/>
<Control Name="MinuteHand"
Template="{StaticResource ClockHandTemplate}"
/>
<Control Name="HourHand"
Template="{StaticResource ClockHandTemplate}"
>
<Control.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1.7" ScaleY="0.7" CenterX="0.0" CenterY="0.0"/>
<RotateTransform Angle="45" CenterX="0.0" CenterY="0.0"/>
</TransformGroup>
</Control.RenderTransform>
</Control>
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="4.8" ScaleY="4.8" CenterX="0" CenterY="0"/>
<TranslateTransform X="60" Y="60"/>
</TransformGroup>
</Canvas.RenderTransform>
</Canvas>
</Canvas>
- 之前我们对整块画布做的变换,是为了控制场景在显示设备上的尺寸和位置,因此 称为 显示变换
- 而对时针的变换,只是作用于一部分场景,称为 建模变换
- 上述时针模板是一个非常基本的、单层次的 层次化建模
2.5 用 WPF实现 2D 图形动态显示
- 这一节介绍 WPF 应用程序中两种可行的动态显示
- 自动、非交互的动态显示,此时 2D 形状由 XAML 定义的动画对象操纵
- 传统的用户界面动态显示,此时用户通过操纵 GUI 控件,例如 按钮、列表框、文本输入等,来激活动态过程的代码
- 基于描述性动画的动态显示
- 可以用 XAML 动画元素来实现动画效果。通过插值使对象的动态属性随时间而变化。
- 每一种 XAML 元素的属性都可以成为动画的对象,例如:
- 形状的局部原点(例如,椭圆左上角) 可由动画元素操控,从而使动画振动
- 形状的基本填充色,边界色 和 边界粗细等属性均可由动画元素操控,以实现反馈式动画、例如发光和脉动
- 动画元素也可以操控旋转变换的角度属性,从而使指定物体产生旋转
- 回到我们上面构建的时钟。我们想要时针能够被动画程序控制。
- 我们增加一个 RotateTransform 并设置标签 ActualTimeHour 使其可被动画程序控制
<Control.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1.7" ScaleY="0.7" CenterX="0.0" CenterY="0.0"/>
<RotateTransform Angle="180" CenterX="0.0" CenterY="0.0"/>
<RotateTransform x:Name="ActualTimeHour" Angle="0"/>
</TransformGroup>
</Control.RenderTransform>
- 接下来是 动画程序的 XAML 代码。创建一个 EventTrigger,在定义触发器时,必须指定启动它的事件类型(本例为画布内容全部载入时) 和 它将执行什么内容(本例为时针的动画元素,其被封装在 Storyboard中) (为了更好观察,这里将动画时间从 1h 该为 1s
<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
ClipToBounds="True">
<Canvas>
<Canvas.Resources>
<ControlTemplate x:Key="ClockHandTemplate">
<Polygon
Points="-0.3,-1 -0.2,8 0,9 0.2,8 0.3,-1 "
Fill="Navy"/>
</ControlTemplate>
</Canvas.Resources>
<Ellipse
Canvas.Left="-10.0" Canvas.Top="-10.0"
Width="20.0" Height="20.0"
Fill="LightGray"
/>
<Ellipse
Canvas.Left="-2" Canvas.Top="-10.0"
Width="4" Height="4"
Fill="Blue"
/>
<Control Name="MinuteHand"
Template="{StaticResource ClockHandTemplate}"
/>
<Control Name="HourHand"
Template="{StaticResource ClockHandTemplate}"
>
<Control.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1.7" ScaleY="0.7" CenterX="0.0" CenterY="0.0"/>
<RotateTransform Angle="180" CenterX="0.0" CenterY="0.0"/>
<RotateTransform x:Name="ActualTimeHour" Angle="0"/>
</TransformGroup>
</Control.RenderTransform>
</Control>
<Canvas.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ActualTimeHour"
Storyboard.TargetProperty="Angle"
From="0.0" To="360.0"
Duration="00:00:01.00" RepeatBehavior="Forever"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="4.8" ScaleY="4.8" CenterX="0" CenterY="0"/>
<TranslateTransform X="60" Y="60"/>
</TransformGroup>
</Canvas.RenderTransform>
</Canvas>
</Canvas>
- 再来看里面的 DoubleAnimation 其就是一个动画元素,并且是通过双精度数来控制属性的变换
- TargetName 和 TargetProperty 可将时针设置成动画 它们被关联在 RotateTransform 元素的 Angle 属性上
- From 和 To 属性决定旋转的区间和方向
- Duration 则控制旋转角度跨越这个区间所需时间。即单次动画时间
- Duration 的格式为:Hours : Minutes : Secounds . FractionalSecond
- RepeatBehavior 设为 Forever 即为循环动画,当动画到达 To 条件时 就会回到 From 条件,重新向 To 步进
- 完整的时针 XAML:
<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
ClipToBounds="True">
<Canvas>
<Canvas.Resources>
<ControlTemplate x:Key="ClockHandTemplate">
<Polygon
Points="-0.3,-1 -0.2,8 0,9 0.2,8 0.3,-1 "
Fill="Navy"/>
</ControlTemplate>
<ControlTemplate x:Key="ClockSecoundHandTemplate">
<Polygon
Points="-0.3,-1 -0.2,8 0,9 0.2,8 0.3,-1 "
Fill="Pink"/>
</ControlTemplate>
</Canvas.Resources>
<Ellipse
Canvas.Left="-10.0" Canvas.Top="-10.0"
Width="20.0" Height="20.0"
Fill="LightGray"
/>
<Ellipse
Canvas.Left="-2" Canvas.Top="-10.0"
Width="4" Height="4"
Fill="Blue"
/>
<Control Name="MinuteHand"
Template="{StaticResource ClockHandTemplate}"
>
<Control.RenderTransform>
<TransformGroup>
<RotateTransform Angle="180" CenterX="0.0" CenterY="0.0"/>
<RotateTransform x:Name="ActualTimeMinute" Angle="0"/>
</TransformGroup>
</Control.RenderTransform>
</Control>
<Control Name="HourHand"
Template="{StaticResource ClockHandTemplate}"
>
<Control.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1.7" ScaleY="0.7" CenterX="0.0" CenterY="0.0"/>
<RotateTransform Angle="180" CenterX="0.0" CenterY="0.0"/>
<RotateTransform x:Name="ActualTimeHour" Angle="0"/>
</TransformGroup>
</Control.RenderTransform>
</Control>
<Control Name="SecoundHand" Template="{StaticResource ClockSecoundHandTemplate}">
<Control.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1.2" CenterX="0.0" CenterY="0.0"/>
<RotateTransform Angle="180" CenterX="0.0" CenterY="0.0"/>
<RotateTransform x:Name="ActualTimeSecound" Angle="0"/>
</TransformGroup>
</Control.RenderTransform>
</Control>
<Canvas.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ActualTimeHour"
Storyboard.TargetProperty="Angle"
From="0.0" To="360.0"
Duration="12:00:00.00" RepeatBehavior="Forever"
/>
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ActualTimeMinute"
Storyboard.TargetProperty="Angle"
From="0" To="360"
Duration="01:00:00.00" RepeatBehavior="Forever"
/>
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ActualTimeSecound"
Storyboard.TargetProperty="Angle"
From="0" To="360"
Duration="00:01:00.00" RepeatBehavior="Forever"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="4.8" ScaleY="4.8" CenterX="0" CenterY="0"/>
<TranslateTransform X="60" Y="60"/>
</TransformGroup>
</Canvas.RenderTransform>
</Canvas>
</Canvas>
- 基于过程代码的动态显示
- 这里书中只是做了简短的介绍。在展示一个真实时针时,可采用过程代码给出正确的本地时间、提供闹铃、对用户交互进行反馈等。
2.6 支持各种形状系数
- 这一小节对 用户界面(UI) 和 屏幕显示区域 的自适应策略进行了探讨
- 用户界面(UI)
- 当 UI 控件的尺寸变小、屏幕区域有限时。可以采用 省略策略(例如 隐藏不常用控件)
- 例如窗口宽度明显变短时,可以隐藏掉一些不常用控件,并提供一个 扩展按钮,用来对已隐藏的菜单进行访问
- 当 UI 控件的尺寸变小、屏幕区域有限时。可以采用 省略策略(例如 隐藏不常用控件)
-
- 屏幕显示区域
- 缩小绘制图形以便视窗中容纳更多内容,并提供可拖动控件
- 屏幕显示区域
- 用户界面(UI)