品牌网站建设

psd转xhtml/css 88元/页起

  • 符合W3C标准的XHTML/CSS编码
  • 多浏览器及操作平台支持
  • SEO(搜索引擎)语义代码标准
  • 经过优化的和切片图像
  • 结构良好的XHTML/CSS
  • 转换页面越多,折扣越多
更多优惠

手机13146413981qq393992480msnyibing98@hotmail.com

文本含义丰富,不仅仅局限于其字面意思。无论是打印计算 机屏幕上的内容还是在计算机屏幕上显示内容,字体的选择都可以增强或减低对文本的影响。文本分为赏心悦目型或恐怖型、舒缓型或激烈型,可以根据需要制定相 应类型的文本。计算机图形还可以在其行与行之间的交界处(一般都会存在)显示文本。通过将文本字符视为图形对象,编程人员可以使文本在屏幕上舞动起来。
一种理想的方法是沿曲线定位文本字符,如图 1 所示。在图形编程中,直线和曲线的集合称为“路径”,因此,此任务有时被称为“路径上的文本”,这就是我要在本专栏中介绍的内容。
 
图 1 曲线路径上的文本
针对 Windows Presentation Foundation (WPF) 进行编程时,路径上的文本编程有一个显著优点:您可以设置定义此路径的各个点的动画效果,并观看这些字符在响应时的弹跳效果。
在 WPF 中,通常有多种方法可用于执行此任务,我将介绍其中的几种方法。在 WPF 中,该任务中困难的部分通常是我们最初无法想到的问题。就将文本放在路径上而言,最大的问题其实并不是解决如何移动和旋转文本字符的问题,这个问题相对来 说比较简单。真正的难题在于如何将生成图形的正确大小准确通知到 WPF 布局系统。

将路径和文本相结合
图 形路径是直线和曲线的集合。其中的一些直线和曲线可能彼此之间相互连接,有些可能形成封闭区域。在 WPF 中,图形路径在 PathGeometry 类和 StreamGeometry 类中是封闭的。虽然 StreamGeometry 可以提供更好的性能,但是定义路径的各个点就会被固定,从而无法为其制作动画效果。
就 将文本放在路径上而言,必须将组成路径的直线和曲线端对端连接起来,这一点非常重要。您可能不希望处理跳过路径中的连接断开处的文本字符串。鉴于此原因, 我最欣赏 WPF 类中的 PathFigure,它是连接的直线和曲线的单一集合。PathGeometry 是一个或多个 PathFigure 对象的集合。
PathFigure 本身定义 StartPoint,并且包含段的集合。第一个段从 StartPoint 开始,然后每个后续段从上一个段断开的位置继续。这些段都是从抽象的 PathSegment 类派生而来:LineSegment、PolyLineSegment、BezierSegment、PolyBezierSegment、 QuadraticBezierSegment、PolyQuadraticBezierSegment 和 ArcSegment。换句话说,PathFigure 是连接在一起的一系列直线、贝赛尔曲线、二次贝赛尔曲线和弧线(椭圆圆周曲线上的曲线)。
PathFigure 具有几何长度。在本专栏附带的源代码中,我将此长度称为变量 pathLength。如果 PathFigure 仅由 LineSegment 和 PolyLineSegment 对象组成,则计算长度非常简单:只需利用勾股定理计算每条线的长度,然后相加。但 ArcSegment 却需要更复杂的计算,计算贝赛尔曲线的长度简直就是一项令人恐惧的任务。
要 简化 PathFigure 长度的计算,比较简便的方法是将路径转换为类似于曲线的多段相连的直线集合。这种方法称为拉平路径。然后,只需重复应用勾股定理即可轻松计算长度。使用 WPF,人们甚至不需要了解勾股定理:从 Vector 对象的其他结果中减去一个点,Vector 对象包含的 Length 属性可返回两点之间的长度。
PathFigure 类包含名为 GetFlattenedPathFigure 的方法,此方法可返回仅包含 LineSegment 和 PolyLineSegment 对象的另一个 PathFigure。这是计算整个 PathFigure 长度的理想方法。
以 特殊字体和字体大小呈现的文本字符串也具有几何大小。如果使用常用的 TextBlock 元素显示文本,则所显示的文本的宽度可从 ActualWidth 属性获得。在实际显示之前的布局过程中,可能尚未设置 ActualWidth,但 TextBlock 会将其 DesiredSize 属性设置为文本的大小。
TextBlock 使用 FormattedText 对象计算其文本的大小,而且应用程序编程人员也可以使用此类。FormattedText 构造函数需要使用文本字符串本身、字体大小和类型为 Typeface 的对象,该对象基于所需的 FontFamily、FontStyle、FontWeight 和 FontStretch 对象构建而成。
脑海中出现了几种结合 PathFigure 和文本字符串的不同方法。您可能希望为文本指定某一特定字体大小,并从路径的起始处显示文本。但是,文本可能并不适合整个路径,甚至可能会超过路径的长度。此时该怎么办呢?
为了避免出现这些问题,我采用了另一种方法:我决定调整文本的大小,使其从头到尾完全符合路径的长度。首次创建文本时,使用的是任意字体大小,在示例程序中为 100。文本的宽度存储在名为 textLength 的变量中。
我将 pathLength 与 textLength 的比值这一数值称为 scalingFactor。这个缩放比例必须作为一种转换应用到各个文本字符中,以便这些字符从头到尾都符合路径。
文本字符串中的各个字符不仅受这种缩放转换的限制,同时还受其他两种转换的限制:将字符移至路径上某一特定位置的平移转换;转动字符以使其基线与该路径相切的旋转转换。
PathGeometry 定义一个方法,称为 GetPointAtFractionLength,此方法对于定位路径上的文本极其有用。此方法需要一个介于 0 到 1 的参数来指示路径的分式长度。这种方法可以返回路径上的相应点。此分式长度很容易确定。每个字符占用路径的分式长度等于单个字符的宽度乘以缩放比例再除以 pathLength。利用此方法返回的路径上的点可用于将字符平移到路径上。另一个优点是,GetPointAtFractionLength 方法会返回另一点,表示路径在该点处的切线。将第二个点的 X 和 Y 属性传递给 Math.Atan2 方法就可以将文本字符旋转所需的角度。
虽 然 GetPointAtFractionLength 由 PathGeometry 而不是 PathFigure 定义,但是可以轻松地从单一的 PathFigure 生成 PathGeometry,以便使用 GetPointAtFractionLength。

端点和中点
虽 然已经确定了通用方法,但还有一些问题:每个字符的哪一部分应该与路径对齐?应该是字符的顶部、底部(下行字母的下面)还是基线?我选择基线,因为这是最 简单的方法,其实选择顶部或底部也很简单。FontFamily 中的 Baseline 属性可帮助您实现此目的。该属性基于单位为 1 的字体大小,因此只需利用 Baseline 属性、字体大小(在我的代码中为 100)和 scalingFactor 即可计算出从字符顶部到其基线的距离。
图 2 显示了两种将文本字符放在简单圆形路径上的不同方法。看看左侧图形中的 h、x 和 n 字符。您会发现每个字符的基线的左右两个端点都接触到了路径,而看看右侧图形中的 e 和 o 字符,您会发现每个字符的基线的中点都接触到了路径。
 
图 2 对齐路径上文本的两种方法

乍一看,这其中的差别似乎微不足道。如果圆圈本身不存在,您可能甚至看不出两者有何区别。不过,我认为使用左侧的方法呈现出的字符间的视觉连贯性效果稍微好一点。
但 当我了解到它比右侧的方法要困难得多(从算法上讲)时,我非常失望。在路径上定位字符基线的左端并不难,但是定位右端需要在路径上找到一个点,此点到第一 个点的直线距离应该等于该字符的缩放宽度。此外,定位完所有字符后,您会发现最后一个字符超过了路径的末端,其原因是,这些字符缩放时根据的是曲线路径, 而定位时根据的是路径的直线捷径。此方法需要多次传递缩放比例的连续优化值。
虽然我按照图 2 进行操作时获得的是此算法的简单版本,但所有可下载的代码使用的中点方法更简单。每个字符要求每个路径片段都等于该片段的文本总宽度。

利用 UserControl!
在 WPF 编程中,从 UserControl 派生是一项相当常用的技术,就像安装设备可以快速构建稳定的控件一样。UserControl 从 ContentControl 派生而来,因此定义此控件的可视树通常作为此控件的 Content 属性在 XAML 中进行定义。代码隐藏文件不仅可以定义更多个自定义属性,还可以处理事件和交互。
可 以使用 UserControl、Canvas 和 TextBlock 中的日常 WPF 构造块来实现路径上的文本。使用 UserControl 来执行这项特殊任务相当方便,因为它已经包含许多定义文本所需的属性:FontFamily、FontStyle、FontWeight、 FontStretch 和 Foreground。(将忽略 FontSize 属性,因为文本大小可以根据路径长度进行缩放。)如果 TextBlock 元素由 UserControl 派生而来,则 UserControl 中上述属性的设置将由 TextBlock 继承。将需要覆盖这些属性的元数据,以便提供其他属性更改事件处理程序,而这正是关键所在。所需的新属性只有 Text(类型字符串)和 PathFigure(类型 PathFigure)。
生成的类称为 TextOnPathControl,图 3 显示了其中一段摘录。当然,此文件的其余内容可在代码下载中找到。本专栏的源代码由名为 TextOnPath 的单一解决方案组成,该解决方案包含名为 Petzold.TextOnPath 的 DLL 项目,而此项目包含为安排路径上的文本而实现各种技术的所有类。TextOnPath 解决方案中的其他项目是演示程序。
static void OnTextPropertyChanged(DependencyObject obj,
  DependencyPropertyChangedEventArgs args) 
{

  TextOnPathControl ctrl = obj as TextOnPathControl;
  ctrl.mainPanel.Children.Clear();

  if (String.IsNullOrEmpty(ctrl.Text))
    return;

  foreach (Char ch in ctrl.Text) 
{
    TextBlock textBlock = new TextBlock();
    textBlock.Text = ch.ToString();
    textBlock.FontSize = FONTSIZE;
    ctrl.mainPanel.Children.Add(textBlock);
  }

  ctrl.OrientTextOnPath();
}

void OrientTextOnPath() 
{
  double pathLength = 
    TextOnPathBase.GetPathFigureLength(PathFigure);
  double textLength = 0;

  foreach (UIElement child in mainPanel.Children) 
{
    child.Measure(new Size(Double.PositiveInfinity,
      Double.PositiveInfinity));
    textLength += child.DesiredSize.Width;
  }

  if (pathLength == 0 || textLength == 0)
    return;

  double scalingFactor = pathLength / textLength;
  PathGeometry pathGeometry =
    new PathGeometry(new PathFigure[] { PathFigure });
  double baseline =
    scalingFactor * FONTSIZE * FontFamily.Baseline;
  double progress = 0;

  foreach (UIElement child in mainPanel.Children) 
{
    double width = scalingFactor * child.DesiredSize.Width;
    progress += width / 2 / pathLength;
    Point point, tangent;

    pathGeometry.GetPointAtFractionLength(progress, 
      out point, out tangent);

    TransformGroup transformGroup = new TransformGroup();

    transformGroup.Children.Add(
      new ScaleTransform(scalingFactor, scalingFactor));
    transformGroup.Children.Add(
      new RotateTransform(Math.Atan2(tangent.Y, tangent.X)
      * 180 / Math.PI, width / 2, baseline));
    transformGroup.Children.Add(
      new TranslateTransform(point.X - width / 2,
        point.Y - baseline));

    child.RenderTransform = transformGroup;
    progress += width / 2 / pathLength;
  }
}
OnTextPropertyChanged 方法是 Text 属性中属性更改处理程序,负责创建所有 TextBlock 子项。mainPanel 变量是 UserControl 的 Content 属性的 Canvas 对象集。OnTextPropertyChanged 最后调用 OrientTextOnPath,与字体相关的属性和 PathFigure 属性中的其他属性更改处理程序也是如此。
OrientTextOnPath 基本上会对所有 TextBlock 元素都应用转换。它可以获取路径的总长度(TextOnPathBase 是 DLL 中的另一个类)和 TextBlock 对象的总宽度,并计算非常重要的 scalingFactor。此方法最后广泛遍历 Canvas 的所有 TextBlock 子对象。请注意,传递给 GetPointAtFractionLength 方法的变量“progress”以此字符缩放宽度的一半为基础,因此,使用这种方法返回的点将对应于此字符的中点。
此 循环的结尾部分包含对三个转换的定义:第一个转换根据 scalingFactor 缩放 TextBlock 的大小。然后,根据 GetPointAtFractionLength 返回的切线数值来旋转 TextBlock。请注意,将以字符基线的中点为旋转中心。最后,TranslateTransform 将经过缩放和旋转的字符移到路径上相应的点。这三个转换就组成了 TextBlock 的 RenderTransform 属性。
图 4 中显示的 TextOnPathControlDemo1.xaml 文件负责创建图 1 中的图像。请注意,TextOnPathControl 类本身并不会创建用于定位文本的 PathFigure。如果希望创建 PathFigure,您必须亲自操作。然而,根据 TextOnPathControlDemo1.xaml 文件中显示的内容可知,您可以使用同一 PathGeometry 来执行这两个任务。
<!-- TextOnPathControlDemo1.xaml by Charles Petzold, September 2008 -->
<Page 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:textonpath=
    "clr-namespace:Petzold.TextOnPath;assembly=Petzold.TextOnPath"
  Title="TextOnPathControl Demo #1"
  WindowTitle="TextOnPathControl Demo #1"
  FontFamily="Times New Roman"
  Foreground="Blue">

  <Page.Resources>
    <PathGeometry x:Key="path" 
      Figures="M 100 100 C 200 150 300 0 400 100" />
  </Page.Resources>

  <Grid>
    <Path 
      Data="{StaticResource path}" 
      Stroke="Red" />

    <textonpath:TextOnPathControl 
      Text="Hello, Path!" 
      PathFigure="{Binding Source={StaticResource path}, 
      Path=Figures[0]}" />
  </Grid>
</Page>

大小是多少?
TextOnPathControl 类不仅显示了 UserControl 的多种用途,还显示了它的限制。该控件将 Canvas 用作所有 TextBlock 对象的父对象,但如果未指定 Canvas 的显式高度或宽度,则该 Canvas 没有具体的尺寸,不能用于布局。例如,如果您将此控件放到 StackPanel 中,它将是完全不可见的。
您 可以用单个单元格 Grid 替换 TextOnPathControl 中的 Canvas。这将为控件提供一个数值不为零的尺寸,以供布局之用,但此数值可能并不正确。计算控件大小时,会认为所有单个未经缩放的 TextBlock 子对象逐个垂直排列,与应用 RenderTransform 之前此控件的布局完全一样。如您所知,RenderTransform 对布局没有任何影响。
您可能会考虑使用 LayoutTransform 代替 RenderTransform,以便 WPF 布局系统可以识别转换后的 TextBlock 元素。但这样也不会奏效,因为 LayoutTransform 将忽略平移转换。
通 常,UserControl 使用包含其他控件和元素的可视树进行填充,其中这些控件和元素会向 WPF 布局系统报告其大小。然后,UserControl 就掌握了将其所有子对象计算在内的总大小。但是,如果您设置了这些子对象的 RenderTransform 属性,则无法保证它们都位于元素边界的内部。
简单来说:虽然 TextOnPathControl 类显示了 UserControl 的多种用途,但它并不是很合适的方法。为各个字符创建 TextBlock 显得有些多余,并且只有当您希望每个字符与它自己的鼠标操作或工具提示相关联时,此操作才有意义。

降低级别
让 我们降低几个级别并从 FrameworkElement 中派生一些类,其中 FrameworkElement 是 TextBlock 本身和许多其他元素(包括 Control)的相同基类。从 FrameworkElement 派生的类通常会在覆盖 OnRender 方法时绘制元素的可视内容。OnRender 方法使用 DrawingContext 类型的对象调用,此类可定义一整套绘制方法,包括 DrawText。这是您可以执行的最低级别的图形输出,但是您自己仍然可以称之为成熟的 WPF 应用程序。
遗 憾的是,FrameworkElement 并不具有您需要的 FontFamily、FontStyle、FontWeight、FontStretch 或 Foreground 属性中的任何一个,更不用说 Text 和 PathFigure 了。由于本专栏的其他类同样需要这些属性,因此我将这七个属性全都放在我称之为 TextOnPathBase 的类中了。此类还定义了四个抽象的属性更改方法,分别为 OnFontPropertyChanged、OnForegroundPropertyChanged、OnTextPropertyChanged 和 OnPathPropertyChanged;当实施测量和绘制代码时,派生类将覆盖这四种方法。TextOnPathElement 类从 TextOnPathBase 派生。图 5 显示了此类中的一段摘录。
protected override void OnTextPropertyChanged(
  DependencyPropertyChangedEventArgs args) 
{

  formattedChars.Clear();
  textLength = 0;

  foreach (char ch in Text) 
{
    FormattedText formattedText =
      new FormattedText(ch.ToString(),
      CultureInfo.CurrentCulture,
      FlowDirection.LeftToRight, typeface, 100,
      Foreground);

    formattedChars.Add(formattedText);
    textLength +=
      formattedText.WidthIncludingTrailingWhitespace;
  }

  InvalidateMeasure();
  InvalidateVisual();
}

protected override void OnPathPropertyChanged(
  DependencyPropertyChangedEventArgs args) 
{

  pathLength = GetPathFigureLength(PathFigure);

  InvalidateMeasure();
  InvalidateVisual(); 
}

protected override Size MeasureOverride(Size availableSize) 
{
  if (PathFigure == null)
    return MeasureOverride(availableSize);

  Rect rect = new PathGeometry(
    new PathFigure[] { PathFigure }).Bounds;
  return (Size)rect.BottomRight;
}

protected override void OnRender(DrawingContext dc) 
{
  if (pathLength == 0 || textLength == 0)
    return;

  double scalingFactor = pathLength / textLength;
  double progress = 0;
  PathGeometry pathGeometry = 
    new PathGeometry(new PathFigure[] { PathFigure });

  foreach (FormattedText formText in formattedChars) 
{
    double width = scalingFactor * 
      formText.WidthIncludingTrailingWhitespace;
    double baseline = scalingFactor * formText.Baseline;
    progress += width / 2 / pathLength;
    Point point, tangent;

    pathGeometry.GetPointAtFractionLength(progress, 
      out point, out tangent);
    dc.PushTransform(
      new TranslateTransform(point.X - width / 2, 
      point.Y - baseline));
    dc.PushTransform(
      new RotateTransform(Math.Atan2(tangent.Y, tangent.X)
      * 180 / Math.PI, width / 2, baseline));
    dc.PushTransform(
      new ScaleTransform(scalingFactor, scalingFactor));

    dc.DrawText(formText, new Point(0, 0));
    dc.Pop();
    dc.Pop();
    dc.Pop();

    progress += width / 2 / pathLength;
  }
}
只 要 Text 属性或其中的一个 Font 属性发生更改,此类就会为字符串中的每个字符创建 FormattedText 对象。这些字符的宽度在 textLength 字段中逐个相加起来。只要 PathFigure 属性发生更改,路径的长度就会存储到 pathLength 字段中。大多数实际操作都在 OnRender 覆盖中进行。
OnRender 会为文本字符串中的每个字符都调用一次 DrawingContext 的 DrawText 方法。但请注意,会调用 PushTransform 三次。虽然这与您在 TextOnPathControl 类中看到的转换相同,但必须按照应用这些转换的相反顺序将其传递到 DrawingContext 堆栈上。调用 DrawText 之后,如果调用 Pop,便会从 DrawingContext 堆栈中删除这三个转换。

测量难题
在 FrameworkElement 级别执行操作时,您也可以通过覆盖 MeasureOverride 方法来告知 WPF 您的元素的大小。您需要负责返回该元素所需的 Size 对象。MeasureOverride 提供一个可用于缩放图形的 availableSize 参数,您也可以完全忽略此参数。
现 在问题出现了:WPF 布局系统假定 OnRender 方法已在其左上角,即点 (0, 0) 处呈现了此元素。如果事实并非如此,则您将遇到一个小麻烦。例如,假定您要编写一个 FrameworkElement 的派生类,专用于绘制一条从点 (-50, 100) 到点 (100, 50) 的直线。OnRender 方法可能包含以下调用:
dc.DrawLine(pen, new Point(-50, 100), new Point(100, 50));
但是,MeasureOverride 会返回什么呢?标准的方法是根据呈现对象的 X 坐标和 Y 坐标上的最大值来返回 Size 对象,这意味着 MeasureOverride 的正文将仅包含以下内容:
return new Size(100, 100);
实际上,MeasureOverride 应该将绘制此线时使用的钢笔的宽度计算在内,但是我们在本次讨论中并不介绍如何改进这个问题。
如果现在您将该元素放入网格单元格中并以蓝绿色作为背景色,此单元格的列宽和行高都设置为 GridLength.Auto,则效果将如图 6 的第一个图形所示。
 
图 6 绘制直线和指定其大小的不同方法
虽然网格单元格允许将坐标轴的坐标垂直向下移动到零,但单元格还是无法容纳此直线,因为它超出了单元格的左边框。您可能会尝试通过从 MeasureOverride 返回一个等于此图形实际呈现的宽度和高度的值来弥补这个问题:
return new Size(150, 50);
但那样只能使事情变得更糟!该直线仍然在单元格外面,而且此单元格变得太大了,如图 6 的第二个图形所示。
此时,您可能决定学聪明点,从 MeasureOverride 返回一个大小为 (150, 150) 的值,然后使用 OnRender 方法调整坐标轴使其原点为 (0, 0)。接着只需将 X 坐标增加 50,Y 坐标减去 50 即可:
dc.DrawLine(pen, new Point(0, 50), new Point(150, 0));
现在,此图形非常适合网格单元格,如图 6 的第三个图形所示。然而,如果您希望将此图形与同一网格单元格或 Canvas 上的其他直线放在一起,则无法正确呈现此图形,因为 DrawLine 调用已经调整了坐标点。如果您使用路径上的文本类的其中一个来在路径上显示文本,则文本和路径将无法对齐。
虽然我们不情愿,但还是不得不承认,图 6 的第一个图形显示的方法是正确的。这就是常规 Line 元素如何运行的一个示例。
现在,我们将一些文本放在路径上,如图 6 的第四个图形所示。元素的大小如何更改才能适合文本?以下内容只适用于这个特殊的示例,并不是在所有情况下都适合。文本并没有超出直线的水平坐标的最大 值,也没有低于垂直坐标的最大值。虽然在其他路径中文本可能会不同,但通常都是大同小异。这就是为什么 TextOnPathElement 方法完全根据 PathFigure 尺寸来实现 MeasureOverride 方法的原因,如图 5 所示。我接下来要介绍的类可执行一些稍微复杂点的操作。

Visual 子对象的世界
从 FrameworkElement 中派生的所有类也可以从 Visual 中派生,因此可以维护可视子对象的集合。在实际情况中,这些子对象通常属于 DrawingVisual 类型。在一些测量中,这些 DrawingVisual 对象类似于元素,也就是说,它们具有可视的外观并可以用于点击测试,但作用却小得多,因为它们不能接收键盘、鼠标或笔针事件,而且也不直接参与布局。
首 次创建 DrawingVisual 类型的对象后,您通常会调用 RenderOpen 方法来获得 DrawingContext 对象。使用您在 OnRender 覆盖中使用的相同的绘制方法来处理此 DrawingContext 对象。最后调用 Close 方法。使用绘制命令呈现的图形现在存储在 DrawingVisual 中。
不 会在屏幕上直接呈现 DrawingVisual 对象。相反,创建和存储 DrawingVisual 对象的类必须覆盖 VisualChildrenCount 属性才能指示可视子对象的数量,还要覆盖 GetVisualChild 方法才能根据索引返回可视子对象。
使用可视子对象的类也将调用 AddVisualChild 和 RemoveVisualChild,以便可视子对象可以正确参与事件路由。如果您将可视子对象存储在 VisualCollection 对象中,系统会为您处理此开销。
对 于在路径上显示文本来说,为文本字符串的每个字符创建 DrawingVisual 对象非常有用。与使用 OnRender 方法进行绘制一样,您也可以调用 PushTransform 方法来对图形应用转换。但是,要将文本放到路径上,还有一个更好的方法:DrawingVisual 本身具有 Transform 属性,这意味着可以将创建 DrawingVisual 对象和应用 Transform 这两项任务分离开来,情况与较早版本的 TextOnPathControl 类中的情况类似。这种分离使得我们可以针对 PathFigure 制作动画效果,而不需要重新创建 DrawingVisual 对象。
图 7 中的代码显示了 TextOnPathVisuals 类的两个方法。只要 Text 属性或字体属性发生更改,就会调用 GenerateVisualChildren 方法,此方法最后会调用 TransformVisualChildren。只要 PathFigure 发生更改,就会调用 TransformVisualChildren 方法。请注意,GenerateVisualChildren 方法是如何通过使用三种转换创建 TransformGroup 方法并将其设置为 DrawingVisual 的 Transform 属性来准备 DrawingVisual 的。这些转换使用 TransformVisualChildren 方法中的适当值进行填充。
protected virtual void GenerateVisualChildren() {
  visualChildren.Clear();

  foreach (FormattedText formText in formattedChars) 
{
    DrawingVisual drawingVisual = new DrawingVisual();

    TransformGroup transformGroup = new TransformGroup();
    transformGroup.Children.Add(new ScaleTransform());
    transformGroup.Children.Add(new RotateTransform());
    transformGroup.Children.Add(new TranslateTransform());
    drawingVisual.Transform = transformGroup;

    DrawingContext dc = drawingVisual.RenderOpen();
    dc.DrawText(formText, new Point(0, 0));
    dc.Close();

    visualChildren.Add(drawingVisual);
  }

  TransformVisualChildren();
}

protected virtual void TransformVisualChildren() 
{
  boundingRect = new Rect();

  if (pathLength == 0 || textLength == 0)
    return;

  if (formattedChars.Count != visualChildren.Count)
    return;

  double scalingFactor = pathLength / textLength;
  PathGeometry pathGeometry = 
    new PathGeometry(new PathFigure[] { PathFigure });
  double progress = 0;
  boundingRect = new Rect();

  for (int index = 0; index < visualChildren.Count; index++) 
{
    FormattedText formText = formattedChars[index];
    double width = scalingFactor * 
      formText.WidthIncludingTrailingWhitespace;
    double baseline = scalingFactor * formText.Baseline;
    progress += width / 2 / pathLength;
    Point point, tangent;
    pathGeometry.GetPointAtFractionLength(progress, 
      out point, out tangent);

    DrawingVisual drawingVisual = 
      visualChildren[index] as DrawingVisual;
    TransformGroup transformGroup = 
      drawingVisual.Transform as TransformGroup;
    ScaleTransform scaleTransform = 
      transformGroup.Children[0] as ScaleTransform;
    RotateTransform rotateTransform = 
      transformGroup.Children[1] as RotateTransform;
    TranslateTransform translateTransform = 
      transformGroup.Children[2] as TranslateTransform;

    scaleTransform.ScaleX = scalingFactor;
    scaleTransform.ScaleY = scalingFactor;
    rotateTransform.Angle = 
      Math.Atan2(tangent.Y, tangent.X) * 180 / Math.PI;
    rotateTransform.CenterX = width / 2;
    rotateTransform.CenterY = baseline;
    translateTransform.X = point.X - width / 2;
    translateTransform.Y = point.Y - baseline;

    Rect rect = drawingVisual.ContentBounds;
    rect.Transform(transformGroup.Value);
    boundingRect.Union(rect);

    progress += width / 2 / pathLength;
  }
  InvalidateMeasure();
}
最 终的效果要比使用 TextOnPathElement 类稍好一些,同时还可以指示包含字符在内的布局大小。TransformVisualChildren 方法首先创建名为 boundingRect 的 Rect 对象。对每个可视子对象应用转换后,ContentBounds 属性(代表未转换的可视对象)将由复合转换进行修改,然后将此矩形与 boundingRect 结合起来:
Rect rect = drawingVisual.ContentBounds;
rect.Transform(transformGroup.Value);
boundingRect.Union(rect);
结果会生成一个矩形,其中包含所有转换后的字符。MeasureOverride 的正文仅包含以下内容:
return (Size)boundingRect.BottomRight;
图 8 显示的 TextOnPathVisualsDemo 程序上的文本沿山脉形状的动画正弦曲线显示。粉红色背景表示布局大小,在动画将不同字符移向右侧和底部时,此大小会随之变化。请注意,此矩形如何扩展以在右侧适合 y 的显示并在底部适合下行字母 g 的显示。
 
图 8 TextOnPathVisualsDemo 程序

 
 

扭曲文本
将文本放到路径上的一个截然不同的方法是使用 TextOnPathWarped 类。每个字符的基线实际上以曲线的形式显示在路径上;虽然每个字符的垂线仍然是直线,但在该点与路径垂直。如果路径是曲线,则字符在凸面将呈离散状态,但在凹面会变得比较拥挤。
此 方法并不需要将文本拆分为单个字符,但预备步骤则需要对整个字符串调用 FormattedText 的 BuildGeometry 方法,然后拉平几何体。虽然不用进行任何转换,但需要用到一些三角学知识。当将文本转换到路径时,文本通常会失去其呈现提示,并且如果文本尺寸很小,则很 难看清文本的内容。您可以制作路径的动画效果,如果文本的字符串很长,获得的效果将非常好。
TextOnPathWarped 也定义 ShiftToOrigin 属性。如果设置为 true,此类将偏移此弯曲几何体上的所有点,以使左上角为 (0, 0)。这样,此类可以报告它在 MeasureOverride 方法中的实际尺寸,但结果不能成功地与其他图形混合。在某些情况下,如图 9 中的屏幕快照,您实际上看到的是变形的字符。
 
图 9 TextOnPathWarpedDemo3 程序
虽然可以采用不同的解决方案解决同一问题通常是一件令人非常感兴趣的事情,但哪个才是最佳的解决方案呢?总的来说,我认为最好的解决方案应该包含 TextOnPathVisuals 类的可视子对象,尤其是您还要制作路径的动画效果时。

代码下载

Comments

Add comment


 

biuquote
  • Comment
  • Preview
微笑得意调皮害羞酷大笑惊讶发呆喜欢可怜尴尬闭嘴噘嘴皱眉伤心抓狂呕吐坏笑漫骂发怒
Loading



订阅新易网博客

  • 订阅到抓虾
  • 哪吒提醒
  • pageflakes
  • Add to My Yahoo!
  • Add to Google
  • 鲜果阅读器订阅图标
  • 订阅到有道阅读
  • 用QQ邮箱阅读空间订阅我的博客。
专业设计 量身定制 品牌网站建设 体验价只需999元
.me 我要我的域名 新网域名 260元/年 再送空间100M
印彩色名片,每盒仅5元
免费推广您的网站或产品 互换广告位、友情连接、软文

Recent comments