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

在 3D 空间中旋转 WPF 内容

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (69投票s)

2009年3月22日

CPOL

9分钟阅读

viewsIcon

353646

downloadIcon

13894

介绍 ContentControl3D:一个使在任何 WPF 用户界面中加入 3D 翻转变得容易的控件。

Intro.png

引言

本文介绍并探讨了 ContentControl3D;一个 Windows Presentation Foundation (WPF) 控件,它可以轻松地将动画 3 维 (3D) 旋转添加到任何用户界面。ContentControl3D 可以在其正面和背面承载任何 UI 内容,并提供多种方式来配置两面之间的动画旋转。除了回顾如何使用该控件之外,我们还将了解其某些方面是如何实现的。ContentControl3D 是 CodePlex.com 上的 Thriple 项目的一部分。

优点、缺点和实用性

WPF 为创建 3D 用户界面提供了强大的支持。如果您对 3D 编程概念、矢量数学知识渊博,并且已经掌握了复杂的 WPF 3D 对象模型,那么一切皆有可能。这是个好消息。

然而,坏消息是,大多数开发人员不是 3D 专家,也没有时间、倾向等来攀登相当艰巨的学习曲线。我们大多数人都忙于按时完成任务,无法尝试 WPF 为在 3D 空间中创建和操作实体抽象而暴露的相对低级的编程模型。

这是一个相当不幸的局面。WPF 平台为将 3D 融入用户界面提供了巨大的潜力,但编程模型过于复杂和低级,以至于大多数开发人员无法实际利用它。这正是我决定创建 ContentControl3D 的原因。

ContentControl3D 降低了 WPF 3D 编程的入门门槛。您可以在您的应用程序中使用此控件,即使纯粹从 XAML,也几乎与 WPF 附带的标准 ContentControl 一样容易。所有围绕使用 3D 编程模型的复杂性都封装在 ContentControl3D 中,因此您无需了解任何 3D 编程即可在应用程序中使用它。

为什么要使用 3D?

此时,您可能想知道为什么要考虑在应用程序中使用 3D。这当然不是必需的。如果使用不当,它可能导致令人困惑和俗气的用户界面。但是,您可以通过一些方法巧妙地将 3D 融入用户界面,从而改善用户体验。

使用 ContentControl3D 最常见和最令人信服的原因是节省空间。许多应用程序有大量数据可视化和输入屏幕需要显示给用户。应用程序用户界面的物理大小受限于用户显示器的大小,或应用程序窗口的任意大小。这通常会在 UI 的各个部分之间造成屏幕空间的争用。

除了布局问题,还存在一次性向用户呈现过多信息的问题。如果您试图将尽可能多的“东西”塞进用户界面,用户在尝试查找所需内容时可能会感到困惑和沮丧。通常最好始终只呈现必要的最少信息,并根据请求披露更详细的信息。

屏幕空间争用和信息过载造成的认知失调问题可以通过将用户界面的某些部分放置在 ContentControl3D 的背面来缓解。当用户需要查看某项详细信息或进入编辑模式等时,ContentControl3D 可以平滑地翻转以向用户显示。当用户不再需要查看用户界面的该部分时,控件可以翻转到正面并隐藏不必要的详细信息。下图包含来自“绑定到 ViewModel”演示应用程序的两个屏幕截图,展示了这种思想的应用。

Impetus.png

Thriple 项目

ContentControl3D 是我的 CodePlex 项目 Thriple 的一部分。您可以从该站点下载最新的源代码和演示应用程序。以下是 Thriple 项目页面的链接:

  • 主页 – 包含介绍材料的登陆页面
  • 源代码 – 下载 Thriple 库和示例项目的最新源代码
  • 讨论 – 提问或阅读其他人关于 Thriple 的提问
  • 发布 – 阅读有关系统要求和已知问题的信息

ContentControl3D API

ContentControl3D 中有趣的公共成员以及一些相关的枚举如下图所示
 
ClassDiagram.png

ContentControl3D 暴露的公共成员数量并不多。总体设计原则是使此控件非常易于理解和使用,同时仍足够强大和灵活,足以支持我假定是最常见的用例。控件公共接口的每个成员的实现都旨在满足一个或多个演示应用程序的需求;所有这些都包含在 CodePlex 上的源代码包中。您可以通过查看示例及其 XAML 或 C# 文件来了解有关这些功能以及如何使用它们的更多信息。因此,本文不回顾控件的每个公共成员。

简单用法 - 直接内容

有几种方法可以将内容放置到 ContentControl3D 的两侧。最简单的技术是直接将 UI 元素添加到 ContentBackContent 属性。由于这两个属性都是 Object 类型,您可以将数据对象而不是 UI 元素分配给它们,但让我们首先从最简单的用例开始。这是一个示例:

<thriple:ContentControl3D
  xmlns:thriple="http://thriple.codeplex.com/"
  Background="LightBlue"
  BorderBrush="Black"
  BorderThickness="2"
  MaxWidth="200" MaxHeight="200"
  >
  <thriple:ContentControl3D.Content>
    <Button 
      Content="Front Side"
      Command="thriple:ContentControl3D.RotateCommand"
      Width="100" Height="100"
      />
  </thriple:ContentControl3D.Content>
  <thriple:ContentControl3D.BackContent>
    <Button 
      Content="Back Side"
      Command="thriple:ContentControl3D.RotateCommand"
      Width="100" Height="100"
      />
  </thriple:ContentControl3D.BackContent>
</thriple:ContentControl3D>

运行这个简单的例子会产生一个用户界面,其中包含一个“正面”按钮,点击后会翻转显示“背面”按钮。以下系列截图展示了这一点。
 
DirectContentSequence.png

在上面的 XAML 中,需要注意的是,Button 控件的 Command 属性设置为引用 ContentControl3D 类暴露的静态 RotateCommand 对象。这是您应该从 XAML 使用的命令,以使控件翻转。您用于执行该命令的 UI 元素以及它在 UI 中的位置,由您决定。例如,"内容模板"示例使用 Hyperlink 元素来执行 RotateCommand,如下所示:

HyperlinkCommandSource.png

高级用法 - 内容模板

如前一节末尾所述,有一个名为“内容模板”的示例应用程序。这是一个示例,说明如何通过使用 DataTemplateContentControl3D 提供要在正面和背面显示的 UI 元素。由于 WPF 中的标准 ContentControl 暴露 ContentContentTemplate 属性,ContentControl3D 也依样画葫芦,暴露 BackContentBackContentTemplate 属性。这两个属性的使用方式与您使用 ContentContentTemplate 的方式完全相同。如果您想知道,我没有添加 BackContentTemplateSelector 属性(它相当于 ContentControlContentTemplateSelector 属性),因为我在编写示例应用程序时没有需要它。

以下 XAML 展示了如何创建我们上一节中看到的相同 UI,只不过这次使用了 DataTemplate 来解释控件两侧要显示的内容。

<thriple:ContentControl3D
  xmlns:thriple="http://thriple.codeplex.com/"
  Background="LightBlue"
  BorderBrush="Black"
  BorderThickness="2"
  MaxWidth="200" MaxHeight="200"
  
  Content="Front Side"
  BackContent="Back Side"
  ContentTemplate="{DynamicResource ButtonTemplate}"
  BackContentTemplate="{DynamicResource ButtonTemplate}"
  >
  <thriple:ContentControl3D.Resources>
    <DataTemplate x:Key="ButtonTemplate">
      <Button 
        Content="{Binding}"
        Command="thriple:ContentControl3D.RotateCommand"
        Width="100" Height="100"
        />
    </DataTemplate>
  </thriple:ContentControl3D.Resources>
</thriple:ContentControl3D>

在此示例中,两侧都由相同的 DataTemplate 渲染。如果需要,您可以自由地为每侧分配不同的模板。通过对 IsOnFrontSide 附加属性进行触发,还可以使用一个 DataTemplate 以不同方式渲染每侧。这种技术在几个示例应用程序中都有使用,例如“内容模板”示例。以下 XAML 片段来自该示例中的一个 DataTemplate

<!-- 
Show a different spaceship on the back side of the surface. 
-->
<Trigger Property="thriple:ContentControl3D.IsOnFrontSide" Value="False">
  <Setter TargetName="grid" Property="Background">
    <Setter.Value>
      <ImageBrush 
        ImageSource="Images/Spaceship2.jpg" 
        Stretch="Uniform" 
        Opacity="0.5"
        />
    </Setter.Value>
  </Setter>
</Trigger>

旋转缓动模式

如本文前面所述,ContentControl3D 可用于创建一种简单直观的方式来节省屏幕空间并防止用户看到过多信息。这本身就是使用该控件的一个令人信服的理由。然而,它提供的不仅仅是 3D 空间中旋转的能力。它可以旋转出风格

Thriple 库包含一个名为 EasingDoubleAnimation 的类。该类派生自标准的 DoubleAnimation 类,并增加了使用 Robert Penner 的缓动方程之一的能力,以在两个 double 值之间创建更“自然”的动画。如果您不熟悉 Penner 方程,我建议您在此查看。

ContentControl3D 使用 EasingDoubleAnimation 来旋转 3D 表面。如果您将 EasingMode 属性设置为除默认值“None”之外的其他值,则将使用缓动方程在动画期间“引导”表面。RotationEasingMode 枚举值具有我自定义的友好名称,例如“Slap”和“RoundhouseKick”。这些值随后分别映射到 Penner 作品中更晦涩的缓动方程名称,例如“QuadEaseOut”和“QuartEaseInOut”。

为旋转使用缓动模式可以为用户界面增添一些额外的特色和活力。像所有时髦的东西一样,它应该谨慎使用,而不是滥用。您可以通过打开 ContentControl3D_Demo 项目中的“缓动模式”示例或“属性资源管理器”示例来测试各种缓动模式。

ContentControl3D 内部结构

本节讨论 ContentControl3D 的某些方面是如何工作的,因此如果您对实现细节不感兴趣,请随意跳过本节。

ContentControl3D 类派生自 ContentControl。它有一个控件模板,当展开成活动的 UI 元素时,在 Mole 中看起来像这样

ElementTree.png

两个 ContentPresenter 的后代元素是您碰巧分配给 ContentBackContent 依赖属性的任何元素,或者通过 DataTemplate 生成的任何元素。正如您所看到的,视觉树不是太大,主要由 WPF 3D 元素组成。显示在 ContentControl3D 两侧的对象都托管在 Viewport2DVisual3D 元素中,这是 WPF 在 3D 空间中托管交互式 2D 元素的方式。让我们看一下控件模板中包含背面内容的 Viewport2DVisual3D 的声明:

<Viewport2DVisual3D>
  <Viewport2DVisual3D.Geometry>
    <MeshGeometry3D
     TriangleIndices="0,1,2 2,3,0"
     TextureCoordinates="0,1 1,1 1,0 0,0"
     Positions="-1,-1,0 1,-1,0 1,1,0 -1,1,0" 
     />
  </Viewport2DVisual3D.Geometry>

  <Viewport2DVisual3D.Transform>
    <RotateTransform3D>
      <RotateTransform3D.Rotation>
        <AxisAngleRotation3D Angle="180" />
      </RotateTransform3D.Rotation>
    </RotateTransform3D>
  </Viewport2DVisual3D.Transform>

  <Viewport2DVisual3D.Material>
    <DiffuseMaterial 
      Viewport2DVisual3D.IsVisualHostMaterial="True"
      Brush="White" 
      />
  </Viewport2DVisual3D.Material>

  <Viewport2DVisual3D.Visual>
    <Border BorderBrush="Transparent" BorderThickness="1">
      <ContentPresenter 
        x:Name="PART_BackContentPresenter"
        Content="{TemplateBinding BackContent, 
          Converter={StaticResource ContentConv}, 
          ConverterParameter=BACK}" 
        ContentTemplate="{TemplateBinding BackContentTemplate}" 
        />
    </Border>
  </Viewport2DVisual3D.Visual>
</Viewport2DVisual3D>

需要注意的一个重要点是,由于 AxisAngleRotation3D 的存在,背面最初绕 Y 轴旋转了 180 度。正面的 Viewport2DVisual3D 也有一个 AxisAngleRotation3D,但其 Angle 初始化为 0。这意味着 3D 表面的两面最初是“背对背”的。随后两面的所有旋转都会导致其 AxisAngleRotation3DAngle 改变 180 度。这是 ContentControl3D 中旋转表面的方法的简化版本:

public void Rotate()
{
 if (this.IsRotating)
  return;

 // Avoid trying to animate a null or frozen instance.
 if (_viewport.Camera == null || _viewport.Camera.IsFrozen)
  _viewport.Camera = this.CreateCamera();

 PerspectiveCamera camera = _viewport.Camera as PerspectiveCamera;

 // Create the animations.
 DoubleAnimation frontAnimation, backAnimation;
 this.PrepareForRotation(out frontAnimation, out backAnimation);
 Point3DAnimation cameraZoomAnim = this.CreateCameraAnimation();

 // Start the animations.
 _frontRotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, frontAnimation);
 _backRotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, backAnimation);
 camera.BeginAnimation(PerspectiveCamera.PositionProperty, cameraZoomAnim);

 this.IsRotating = true;
}

当旋转动画完成时,会执行此方法:

void OnRotationCompleted(object sender, EventArgs e)
{
 AnimationClock clock = sender as AnimationClock;
 clock.Completed -= this.OnRotationCompleted;

 this.IsRotating = false;
 this.IsFrontInView = !this.IsFrontInView;

 if (_isRotationPending)
 {
  // The BringFrontSideIntoView/BringBackSideIntoView
  // method was called during a rotation, and the
  // appropriate side is not in view, so rotate again.
  _isRotationPending = false;
  this.Rotate();
 }
 else
 {
  CommandManager.InvalidateRequerySuggested();
 }
}

请注意,当旋转开始时,IsRotating 依赖属性设置为 true,当旋转结束时设置为 false(这很合理!),但 IsFrontInView 依赖属性仅在动画完成才改变状态。在编写针对这些属性的触发器时,了解这些信息很有用。

总结

ContentControl3D 让您轻松地将 3D 功能添加到 WPF 应用程序中。如果使用得当,它可以使原本拥挤、混乱、沉闷的用户界面变得更加简洁和引人入胜。请务必探索 Thriple 中包含的示例项目,以了解该控件中所有可用的功能以及如何使用它们。

您可以从此处下载最新的 Thriple 源代码。

修订历史

  • 2009 年 3 月 22 日 – 发布文章
© . All rights reserved.