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

使用 WPF 3.5 创建带有语音和墨迹的 3D 书本式应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (63投票s)

2007年12月21日

CPOL

17分钟阅读

viewsIcon

227865

downloadIcon

13847

一个带有交互式 3D、语音和墨迹的文本编辑器,使用 WPF 3.5。

The WPF 3D Book Writer

目录

引言

Windows Presentation Foundation (WPF) 以其对动画、3D(现在包括交互式 3D)、复杂矢量图形、数据绑定、样式和模板等的支持,已被公认为一个非常强大且完整的用于高度交互式 Windows 界面的平台。

在本文中,我为文本编辑器和阅读器开发了一个 3D 书本式界面。在第一个迭代中,书本可以打开和关闭,您可以在左页输入文字,在右页涂鸦。左页在双击时会使用 Windows 语音合成引擎朗读,右页可以通过右键单击在墨迹和橡皮擦模式之间切换。文本编辑器还集成了英语拼写检查功能。

在您阅读本文并查看代码时,您将学习到 WPF 3.5 中的许多有趣概念,例如交互式 3D、基本的语音和墨迹支持,以及使用样式和资源字典重用和组织资源的一些技巧。

要求

要阅读本文,建议您对 WPF 和 XAML 有良好的理解。您还需要 .NET Framework 3.5 来运行示例应用程序,以及 Visual Studio 2008 来构建代码。

WPF 3D 基础知识

在开始之前,我们将回顾一些 WPF 3D 编程的关键概念。如果您想了解更多关于 WPF 3D 的信息,我强烈建议您阅读 Charles Petzold 的《Windows 3D 编程》作为附加参考书。

WPF 3D 的第一个关键概念是三维空间本身。三维通常由三个相互垂直的轴(X、Y 和 Z)表示。

3D axes as they appear in WPF

图 1: WPF 中的 3D 轴。摘自 MSDN 库.

在 WPF 中,3D 空间是通过 Viewport3D 对象创建的。我们通过创建其几何表示(称为模型)来在此空间中表示 3D 对象。模型可以代表物理几何体(网格)或光源。如果没有光源,您将只能看到一个空白画布。您还需要一个摄像头,以便可以从指定视角查看模型。

网格

网格是任何 3D 场景的主要组成部分:它们代表物理 3D 对象。在 WPF 中,网格由许多 3D 三角形组成,这些三角形组合在一起,给人一种平面或曲面 3D 形状的印象。几何网格最重要的属性是:

  • Positions:一个 Point3D 点的集合,表示三角形的顶点。
  • TriangleIndices:一个索引集合,更具体地说,是一组整数三元组,表示顶点如何连接以形成三角形。例如,三元组“0,1,2”表示应通过连接 Positions 集合中索引为 0、1 和 2 的点来创建一个三角形。此外,三角形可见的一面是索引顺序为逆时针的一面。
  • TextureCoordinates:3D 表面必须覆盖纹理,2D 元素将赋予模型外部外观。TextureCoordinates 属性是一个 Point 点的集合,表示纹理应如何应用于网格。对于每个位置,您将一个 2D 点映射到 3D 点,告诉您纹理的哪个部分应该覆盖该点,使用相对坐标(例如,“0.5 0.5”是图像的中心)。一张图片可能有助于解释这个概念。

The texture coordinates in the left 2D image are mapped to specific points in the 3D triangle.

图 2: 左侧 2D 图像中的纹理坐标映射到 3D 三角形中的特定点。图片摘自 Daniel Lehenbauer 的博客

材质

除了几何体,您还必须设置覆盖模型的材质。材质表示模型如何处理光照。有三种主要类型的材质:

  • DiffuseMaterial:一种在光线击中表面时会漫射光线的材质。
  • EmissiveMaterial:一种看起来会发光的材质。
  • SpecularMaterial:一种在光线击中表面时会反射光线的材质。它有一个 SpecularPower 属性,表示材质的反射度。

灯光

在 3D 编程世界中,灯光是允许您看到 3D 对象的模型。它们还可以通过使用阴影和定向灯来帮助您在场景中创建更真实的感觉。要创建 WPF 中的灯光,您需要了解其类型和颜色,并且根据类型,还需要了解其他属性。

  • Type:灯光的类型会影响光线如何应用于场景。WPF 中包含的灯光类型有:
    • AmbientLight:场景中的均匀光,没有特定方向。
    • DirectionalLight:一种灯光,允许您在特定方向创建阴影和光照区域。您可以将定向灯想象成来自房间“窗户”的光,具有平行光线。
    • PointLight:一种在空间中具有特定位置的灯光。它向所有方向发光,并且当您增加与光源的距离时,其强度会减小。您可以将其视为场景中的“灯泡”。
    • SpotLight:一种光源,在场景中的一个方向投射一个圆锥。您可以控制完全照明和部分照明的区域,以创建聚光灯效果。
  • Color:灯光的颜色。对象的显示方式在很大程度上取决于您为灯光选择的颜色:要小心,不要让房间太亮或产生意外的情绪。
  • Other attributes:根据灯光类型,您可以设置灯光的位置、方向、范围、衰减、圆锥角度以及许多其他特性。

构建灯光时要记住的另一件事是灯光和阴影对场景性能的影响。例如,AmbientLight 的性能比 SpotLight 好得多。根据 WPF 3D 性能指南(http://msdn2.microsoft.com/en-us/library/bb613553.aspx),性能从快到慢的顺序是:Ambient、Directional、Point 和 Spot。

摄像头

摄像头是您在 3D 场景中获得视角的工具。在 WPF 中,最常见的摄像头类型是:

  • PerspectiveCamera:工作方式类似于真实世界的相机;距离物体越远,物体看起来越小。
  • OrthographicCamera:表示物体的正交投影;物体在任何位置看起来都相同。

Difference between the orthographic and perspective projections.

图 3: 正交投影和透视投影之间的区别。图片摘自 MSDN 库

对于摄像头,您还可以设置许多有趣的属性,包括:

  • Position:表示摄像头位置的 Point3D。如果摄像头离对象越远,对象看起来会越小,而如果摄像头离对象越近,对象会看起来越大。
  • LookDirection:指向摄像头视角的 Vector3D。找到所需 LookDirection 的简单方法:从您想要位于中心的点的摄像头位置减去摄像头的位置。例如:如果您想看向空间原点 (0,0,0),而您的摄像头位于位置 (4,5,6),则应将 LookDirection 属性设置为 (0,0,0) - (4,5,6) = (-4,-5,-6)。请注意:如果 LookDirection 不正确,您可能只能看到模型的一部分,甚至什么也看不到。
  • UpDirection:指向摄像头认为“向上”方向的 Vector3D。通常,它是 (0,1,0) 向量,指向 Y 值的增加方向。
  • FieldOfView:一个以度为单位的角度,表示摄像头的视角。通常,30 度就足够了。

转换为 XAML

理解了所有这些概念后,您就可以开始编写 XAML 了。场景中的模型由 ModelVisual3D 表示,摄像头是显示内容的 Viewport3D 的属性。以下代码摘自 MSDN 库,代表了一个简单的 3D 场景。

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
  <DockPanel>
    <Viewbox>
      <Canvas Width="321" Height="201">

        <!-- The Viewport3D provides a rendering surface for 3-D visual content. -->
        <Viewport3D ClipToBounds="True" Width="150" 
            Height="150" Canvas.Left="0" Canvas.Top="10">

          <!-- Defines the camera used to view the 3D object. -->
          <Viewport3D.Camera>
            <PerspectiveCamera Position="0,0,2" 
              LookDirection="0,0,-1" FieldOfView="60" />
          </Viewport3D.Camera>

          <!-- The ModelVisual3D children contain the 3D models -->
          <Viewport3D.Children>

            <!-- This ModelVisual3D defines the light cast in the scene. -->
            <ModelVisual3D>
              <ModelVisual3D.Content>
                <DirectionalLight Color="#FFFFFF" 
                  Direction="-0.612372,-0.5,-0.612372" />
              </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
              <ModelVisual3D.Content>
                <GeometryModel3D>

                  <!-- The geometry specifes the shape of the 3D plane. 
                        In this sample, a flat sheet is created. -->
                  <GeometryModel3D.Geometry>
                    <MeshGeometry3D
                     TriangleIndices="0,1,2 3,4,5"
                     Normals="0,0,1 0,0,1 0,0,1 0,0,1 0,0,1 0,0,1 "
                     TextureCoordinates="0,0 1,0 1,1 1,1 0,1 0,0 "
                     Positions="-0.5,-0.5,0.5 0.5,-0.5,0.5 0.5,0.5,0.5 0.5, 
                                0.5,0.5 -0.5,0.5,0.5 -0.5,-0.5,0.5 " />
                  </GeometryModel3D.Geometry>

                  <!-- The material specifies the material applied 
                       to the 3D object. In this sample a linear gradient 
                       covers the surface of the 3D object.-->
                  <GeometryModel3D.Material>
                    <MaterialGroup>
                      <DiffuseMaterial>
                        <DiffuseMaterial.Brush>
                          <LinearGradientBrush StartPoint="0,0.5" 
                                         EndPoint="1,0.5">
                            <LinearGradientBrush.GradientStops>
                              <GradientStop Color="Yellow" Offset="0" />
                              <GradientStop Color="Red" Offset="0.25" />
                              <GradientStop Color="Blue" Offset="0.75" />
                              <GradientStop Color="LimeGreen" Offset="1" />
                            </LinearGradientBrush.GradientStops>
                          </LinearGradientBrush>
                        </DiffuseMaterial.Brush>
                      </DiffuseMaterial>
                    </MaterialGroup>
                  </GeometryModel3D.Material>

                  <!-- Apply a transform to the object. In this sample, 
                       a rotation transform is applied, rendering the 
                       3D object rotated. -->
                  <GeometryModel3D.Transform>
                    <RotateTransform3D>
                      <RotateTransform3D.Rotation>
                        <AxisAngleRotation3D Axis="0,3,0" Angle="40" />
                      </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                  </GeometryModel3D.Transform>
                </GeometryModel3D>
              </ModelVisual3D.Content>
            </ModelVisual3D>
          </Viewport3D.Children>

        </Viewport3D>
      </Canvas>
    </Viewbox>
  </DockPanel>
</Page>

交互式 3D

在 WPF 3.5 中,最值得注意的变化是添加了交互式 3D 类,这些类允许您创建具有焦点、事件的模型,甚至可用的 2D 控件,而无需手动进行 3D 场景的命中测试。其中一些类现在已集成到 WPF 中,是 Kurt Berglund 的“3D 上的交互式 2D”类的演进,该类随 Daniel Lehenbauer 的 3D Tools 一起提供(http://www.codeplex.com/3DTools)。

不幸的是,关于 WPF 交互式 3D 的资料不多,所以您必须自己摸索大部分内容。幸运的是,它并不复杂;在简短介绍之后,您可能会根据 WPF 3D 博客(http://blogs.msdn.com/wpf3d/)上的一篇博文自己弄清楚。

WPF 3.5 中启用交互式 3D 魔力的两个类是 UIElement3DViewport2DVisual3D

  • UIElement3D:UIElement3D 使得在 3D 可视元素中添加输入、焦点和事件成为可能。由于它是一个抽象类,WPF 3D 团队提供了两个类来直接使用这些功能:
    • ContainerUIElement3D:交互式 Visual3D 的容器。
    • ModelUIElement3D:有一个 Model 属性,它是视觉上表示 UI 元素的模型。

    通常,您会最终使用 Containers 来处理一组模型的事件,并使用 Models 来单独处理事件。

  • Viewport2DVisual3D:在创建 3D 界面方面提供了巨大的帮助,Viewport2DVisual3D 类似于 ModelVisual3D,但它还有一个 Visual 属性,您可以在其中放置 2D 控件。这些 2D 控件的工作方式与在普通 2D 界面中一样。例如,如果您将 Visual 设置为 Button,它将支持所有事件,例如 Click 和 Focus,同时显示在 3D 中。要使用 Viewport2DVisual3D,您还必须设置一个材质,并将 Viewport2DVisual3D.IsVisualHostMaterial 设置为 true,表示将显示 Visual 的材质。

为 3D 内容准备窗口

现在我们可以开始检查应用程序了。首先要做的是准备窗口以显示 3D 内容。在我的应用程序中,我从一个空白的 WPF Windows 应用程序开始,并更改了窗口尺寸和背景图像。在这个第一步,我已经创建了一个 ResourceDictionary 来存储应用程序中各种图像的 ImageSource,称为 *ImageResources.xaml*。正如您将看到的,我有点组织癖,即使是一个小项目,我也倾向于将所有内容组织在多个 ResourceDictionary 中。

然后,我创建了一个 Viewport3D,并在 *Other3DResources.xaml* 文件中设置了灯光和摄像头。在此步骤中,XAML 如下所示:

[MainWindow.xaml]
<Window x:Class="BookWriter3D.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="BookWriter3D"
        Height="768"
        Width="1024">
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source=".\Resources\ImageResources.xaml" />
                <ResourceDictionary Source=".\Resources\Other3DResources.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <Grid x:Name="_LayoutRoot">
        <Grid.Background>
            <ImageBrush ImageSource="{StaticResource Image_Background}" />
        </Grid.Background>

          <Viewport3D x:Name="_Main3D"
                      ClipToBounds="False"
                      Camera="{StaticResource Other3D_MainCamera}">

              <!-- ModelVisual3D containing the lights -->
              <StaticResource ResourceKey="Other3D_Lights" />

          </Viewport3D>
    </Grid>
</Window>
[ImageResources.xaml]
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <ImageSource x:Key="Image_Background">Images/darkaurora.png</ImageSource>

</ResourceDictionary>
[Other3DResources.xaml]
<ResourceDictionary 
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <PerspectiveCamera x:Key="Other3D_MainCamera"
                       Position="0 -2.5 6.5"
                       LookDirection="0 2.5 -6.5"
                       UpDirection="0 1 0"
                       FieldOfView="30" />

    <ModelVisual3D x:Key="Other3D_Lights">
        <ModelVisual3D.Content>
            <Model3DGroup>
                <AmbientLight Color="LightGray" />
                <DirectionalLight Color="Gray"
                                  Direction="2 -3 -1" />
            </Model3DGroup>
        </ModelVisual3D.Content>
    </ModelVisual3D>

</ResourceDictionary>

3D 几何体:平面、封面和边缘

在此项目中,我使用了三个简单的 3D 模型(或更准确地说,网格几何体),它们代表了书的各个部分。您可以浏览附件代码中的 *MeshGeometry3DResources.xaml* 文件,了解它们是如何制作的。

第一个模型是平面,它只是代表 3D 中的一个纸张形状的平面。第二个是封面模型,它是一个薄的长方形盒子,将作为 front、back 和 spine 封面的几何体。最后,还有边缘模型,形状像一个围绕长方形盒子的“条带”,将代表 3D 书本的左边缘和右边缘。

Model of a sheet of paper.

Model of a book cover.

Model of the edge of a book.

图 4: 应用程序中使用的 3D 模型(纸张、封面和边缘),已应用纹理和变换以便于识别。

组装书本

通常,在创建 3D WPF 应用程序时,我会尽可能使对象简单,然后使用 3D 变换来定位和旋转它们。这简化了稍后可能发生的许多操作,例如动画和移动,并且通过对不同对象应用相同的变换集,还可以帮助保持对象平行或一起移动。此外,我通常将几何体、材质和变换存储在单独的 ResourceDictionary 中,以便在需要时快速找到它们。

对于这个项目,我使用了七个模型:前封面、后封面、书脊、左右边缘以及左右页面。前五个包含在一个 Model3DGroup 中,该组代表 ModelUIElement3DModel 属性,因为我希望它们像一个对象一样工作,具有相同的事件;后两个是单独的 Viewport2DVisual3D,因为它们将包含功能性的 2D 控件。

为了显示和组合这些模型,我创建了一组材质和变换,并使用了上面提到的三种几何体。材质非常简单:静态元素(封面、书脊和边缘)使用带有 ImageBrushDiffuseMaterial,而交互式控件的材质则使用 IsVisualHostMaterial 属性设置为 trueDiffuseMaterial。所有图像都在项目文件夹的 *\\Resources\\Images* 文件夹中,并且还在 *ImageResources.xaml* 中引用,以便在需要时轻松修改。

对于变换,我使用了对左右“块”(封面+边缘+页面)的 Y 轴旋转,注意旋转中心以便在正确的位置组合页面。然后,我在 X 轴上对块进行了平移,并对封面进行了不同的平移(在 X 和 Z 轴上)。最后,我为书脊应用了三个变换:一个缩放变换以在 X 方向上缩放封面网格(使其变短),一个旋转以便能够关闭书本,以及一个平移以使该旋转平滑。我在单独的资源中完成了所有这些操作,以便能够“命名”最有用的变换,从而以后轻松地在代码中访问资源。

要创建对象,在创建资源并引用 ResourceDictionary 后,可以在 Viewport3D 中插入类似以下内容:

<!-- 
    Clickable 3D models (ModelUIElement3D): Cover, spine and edges
-->
<ModelUIElement3D>
    <ModelUIElement3D.Model>
        <Model3DGroup>
            <GeometryModel3D x:Name="_FrontCover"
                             Geometry="{StaticResource MeshGeometry3D_Cover}"
                             Material="{StaticResource Material_FrontCover}"
                             Transform="{StaticResource Transform3D_FrontCover}" />
            <GeometryModel3D x:Name="_BackCover"
                             Geometry="{StaticResource MeshGeometry3D_Cover}"
                             Material="{StaticResource Material_Cover}"
                             Transform="{StaticResource Transform3D_BackCover}" />
            <GeometryModel3D x:Name="_SpineCover"
                             Geometry="{StaticResource MeshGeometry3D_Cover}"
                             Material="{StaticResource Material_Cover}"
                             Transform="{StaticResource Transform3D_SpineCover}" />
            <GeometryModel3D x:Name="_LeftEdge"
                             Geometry="{StaticResource MeshGeometry3D_Edge}"
                             Material="{StaticResource Material_Edge}"
                             Transform="{StaticResource Transform3D_Left}" />
            <GeometryModel3D x:Name="_RightEdge"
                             Geometry="{StaticResource MeshGeometry3D_Edge}"
                             Material="{StaticResource Material_Edge}"
                             Transform="{StaticResource Transform3D_Right}" />
        </Model3DGroup>
    </ModelUIElement3D.Model>
</ModelUIElement3D>

<!-- Interactive 3D models: Pages -->

<Viewport2DVisual3D x:Name="_LeftPage"
                    Geometry="{StaticResource MeshGeometry3D_Plane}"
                    Transform="{StaticResource Transform3D_Left}">
    <Viewport2DVisual3D.Material>
        <DiffuseMaterial Viewport2DVisual3D.IsVisualHostMaterial="True" />
    </Viewport2DVisual3D.Material>
</Viewport2DVisual3D>

<Viewport2DVisual3D x:Name="_RightPage"
                    Geometry="{StaticResource MeshGeometry3D_Plane}"
                    Transform="{StaticResource Transform3D_Right}">
    <Viewport2DVisual3D.Material>
        <DiffuseMaterial Viewport2DVisual3D.IsVisualHostMaterial="True" />
    </Viewport2DVisual3D.Material>
</Viewport2DVisual3D>

正如您所看到的,Viewport2DVisual3D 没有为其 Visual 属性设置任何内容,因此,如果您现在构建项目,您将看到类似以下内容:

3D book without the content pages

图 5: 没有内容页面的 3D 书本。

添加内容

每页的内容将是一个简单的 TextBox 加上一些额外的功能。首先,您必须将一个 TextBox 添加为每个页面的 Visual,例如左页面:

<Viewport2DVisual3D x:Name="_LeftPage"
                    Geometry="{StaticResource MeshGeometry3D_Plane}"
                    Transform="{StaticResource Transform3D_Left}">
    <Viewport2DVisual3D.Material>
        <DiffuseMaterial Viewport2DVisual3D.IsVisualHostMaterial="True" />
    </Viewport2DVisual3D.Material>
    
    <Viewport2DVisual3D.Visual>
        <TextBox Padding="30,30,5,30" 
                 FontFamily="Segoe Script" 
                 Width="500" 
                 Height="700" 
                 IsTabStop="True" 
                 FontSize="30" 
                 AcceptsReturn="True" 
                 TextWrapping="Wrap"  />
    </Viewport2DVisual3D.Visual>
    
</Viewport2DVisual3D>

这个 TextBox 的一些有趣属性:它的 FontFamily 是 Segoe Script,因此具有更“手写”的外观。它必须是制表符停止,以便于键盘导航,并且必须接受回车键(Enter 键)以实现多行编辑器。启用换行也很重要,这样我们就不会有文本流出水平可见区域。

您可以看到,这两个 TextBoxes 将具有许多共同的属性,因此这是一个非常适合组织和重用代码的样式。事实上,在上面的示例中,唯一在 TextBoxes 之间发生变化的属性是 Padding

因此,要添加样式,最好创建另一个 ResourceDictionary 并使用类似以下的 Style

<Style x:Key="Control_PagesStyle"
       TargetType="{x:Type TextBox}">
    <Setter Property="Width"
            Value="500" />
    <Setter Property="Height"
            Value="720" />
    <Setter Property="IsTabStop"
            Value="True" />
    <Setter Property="FontFamily"
            Value="Segoe Script, script" />
    <Setter Property="FontSize"
            Value="30" />
    <Setter Property="AcceptsReturn"
            Value="True" />
    <Setter Property="TextWrapping"
            Value="Wrap" />
</Style>

正如资源键所示,我已将此资源保存在一个名为 *ControlResources.xaml* 的文件中。

拼写检查

WPF 提供的一个有趣功能是我们集成的拼写检查界面。启用它非常简单:只需在您的根标签(通常是 WindowPage)中添加一个 xml:lang="en-us" 属性,并将 TextBox 中的 SpellCheck.IsEnabled 属性设置为 true。我在上面定义的样式中已经这样做了:

<Style x:Key="Control_PagesStyle"
       TargetType="{x:Type TextBox}">
    ...
    <Setter Property="SpellCheck.IsEnabled"
            Value="True" />
</Style>

就是这样。现在您拥有一个完整的 3D 书本拼写检查写入器,如下所示:

Integrated spell checking with WPF

图 6: 使用 WPF 集成拼写检查。

有关拼写检查器的更多信息,以及有关如何改进拼写检查器用户界面的技巧,请参阅 Josh Smith 的文章,网址为 SmartTextBox.aspx

动画化书本

现在,是时候为我们的书本添加打开和关闭动画了。为了提高灵活性,我决定使用代码隐藏来创建动画。因此,我创建了两个方法,OpenBookCloseBook,它们使用临时变量来执行简单的动画以进行组织。有旋转动画、书脊平移动画以及用于将书本居中的摄像头动画。您可以在示例代码中看到完整的方法;在这里,我将从 OpenBook 方法中摘录左旋转部分:

RotateTransform3D rot = 
   (RotateTransform3D)TryFindResource("Transform3D_LeftRotation");
DoubleAnimation da = new DoubleAnimation(15, 
   new Duration(TimeSpan.FromSeconds(durationSeconds)));
da.DecelerationRatio = 1;
rot.Rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, da);

使用代码隐藏动画带来的一个有趣的补充是能够进一步自定义动画。在这种情况下,我使用了一个参数(double durationSeconds)来选择动画所需的时间。另一个有用的功能是使用 TryFindResource 方法,它允许我们动画化资源,并且也适用于外部(已合并)资源字典。

动画代码完成后,只需将事件连接起来以触发这些动画。因此,创建了一个名为 IsBookOpenbool 字段来存储书本的状态,并将“封面”模型(包含封面和边缘的 ModelUIElement3D)的 MouseDown 事件连接到一个处理程序,如下所示:

private void Cover_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (IsBookOpen) CloseBook(1.5);
    else OpenBook(1.5);
}

这样,整个封面就可以作为打开或关闭书本的可点击区域。我还添加了一个窗口 Loaded 事件中的“闪屏”动画,它会执行淡入效果并立即关闭书本(CloseBook(0))。

为书本添加语音

下一个功能非常有趣。通过使用 Windows 语音合成引擎,让您的应用程序朗读任何文本都非常简单;.NET Framework 3.0 中包含的 API 使它更加容易。

要启用应用程序中的语音合成,您必须添加对 System.Speech 命名空间的引用(Project > Add Reference... > .NET tab > System.Speech),并在 using 子句中添加 System.Speech.Synthesis。之后,您只需要调用 SpeechSynthesizerSpeakAsync 方法,Microsoft Anna 就会朗读您的文本(如果您使用的是 Windows XP,则是 Microsoft Sam)。

using System.Speech.Synthesis;
...
SpeechSynthesizer synth = new SpeechSynthesizer();
synth.SpeakAsync("Hello, speech!");

在此应用程序中,当您双击文本框时,它会使用 XAML 连接的事件处理程序来朗读其中的文本。

添加墨迹

在附带的示例中,我还为右侧页面添加了墨迹支持。这非常简单:只需将右侧的 Visual(一个 TextBox)替换为 InkCanvas,即可完成。Viewport2DVisual3D 将为您处理困难的部分。

这里的另一个值得关注的地方是 InkCanvasDefaultDrawingAttibutes 属性。此属性类型为 DrawingAttributes,允许您更改墨迹的许多视觉属性,例如颜色、宽度以及是否应将笔画拟合到曲线。在示例中,此属性在 InkCanvas 样式中定义,可以在 *ControlResources.xaml* 文件中找到。

最后,添加了一个事件处理程序,以便在右键单击时将 InkCanvas 的编辑模式从墨迹切换到橡皮擦。

private void InkCanvas_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    // Switch InkCanvas editing mode
    InkCanvas ic = sender as InkCanvas;
    ic.EditingMode = (ic.EditingMode == InkCanvasEditingMode.Ink) ? 
       InkCanvasEditingMode.EraseByPoint : InkCanvasEditingMode.Ink;
}

最后的润色:使用 3DTools 实现自动轨道球支持

为了完成这个应用程序,我借助前面提到的 WPF 的 3D Tools(http://www.codeplex.com/3DTools)添加了轨道球和缩放功能。下载 50KB 的 DLL 并添加引用后,您只需将 Viewport3D 包装在 TrackballDecorator 中即可。为此,您还必须在 XAML 文件中设置对自定义命名空间的引用。

<Window ...
        xmlns:tools="clr-namespace:_3DTools;assembly=3DTools"
        ...>
...
<tools:TrackballDecorator>
            <Viewport3D>
             ...
            </Viewport3D>
</tools:TrackballDecorator>

启用轨道球后,使用鼠标左键旋转场景,使用鼠标右键放大和缩小。3D Tools 的 TrackballDecorator 通过在您鼠标在场景上移动时更改摄像头的位置和方向来工作。如果您有兴趣,3D Tools 项目是开源的,因此您可以查看其实现方式。

未来展望

至此,我们的项目就完成了。当然,我在这里的重点是展示概念,所以这是一个非常简单的应用程序,还有很多工作要做。一些有趣的未来想法:

  • 重构为完整的自定义控件
  • 翻页,使用代表页面的 Visual 集合
  • 墨迹识别支持
  • 语音识别支持
  • 一个用于更改封面图像或“纸张类型”的界面

您有什么看法?

我希望本文和应用程序能教会您一些 WPF 的实用概念。您有什么看法?请评论、投票、提出修改建议、纠正我,并随时根据您的喜好扩展此代码。

其他链接和参考

历史

  • V1.0 (2007/12/21) - 初始发布。
  • V1.01 (2007/12/21) - 添加了目录。
  • V1.02 (2009/10/30) - 修正了无法正确显示的字符。
© . All rights reserved.