WPF如何创建一个漂亮的字母波

时间:2009-12-23 23:10:15

标签: wpf text layout animation

我需要在我的WPF应用程序中创建一个看起来很浪漫的文本对象,我实际上假设会有一个“沿路径弯曲”类型的选项,但我在Blend中根本看不到一个。 / p>

我找到了一个教程,建议您需要将文本转换为逐字母的路径然后旋转它,但在我看来,这是非常可怕的,错误的空间和灵活性不足。

我基本上想要一个具有动画波浪效果的句子,我该如何实现呢?

谢谢大家 标记

3 个答案:

答案 0 :(得分:36)

您正在寻找的是有效的非线性变换。 Visual上的Transform属性只能进行线性变换。幸运的是,WPF的3D功能可以帮助你解决问题。您可以通过创建一个简单的自定义控件来轻松完成您要查找的内容:

<local:DisplayOnPath Path="{Binding ...}" Content="Text to display" />

以下是如何操作:

首先创建“DisplayOnPath”自定义控件。

  1. 使用Visual Studio的自定义控件模板创建它(确保您的程序集:ThemeInfo属性设置正确且所有这些)
  2. 添加Geometry类型的依赖项属性“路径”(使用wpfdp片段)
  3. 添加Geometry3D类型的只读依赖项属性“DisplayMesh”(使用wpfdpro片段)
  4. 为路径添加PropertyChangedCallback以调用“ComputeDisplayMesh”方法将路径转换为Geometry3D,然后从中设置DisplayMesh
  5. 看起来像这样:

    public class DisplayOnPath : ContentControl
    {
      static DisplayOnPath()
      {
        DefaultStyleKeyProperty.OverrideMetadata ...
      }
    
      public Geometry Path { get { return (Geometry)GetValue(PathProperty) ...
      public static DependencyProperty PathProperty = ...  new UIElementMetadata
      {
        PropertyChangedCallback = (obj, e) =>
        {
          var displayOnPath = obj as DisplayOnPath;
          displayOnPath.DisplayMesh = ComputeDisplayMesh(displayOnPath.Path);
      }));
    
      public Geometry3D DisplayMesh { get { ... } private set { ... } }
      private static DependencyPropertyKey DisplayMeshPropertyKey = ...
      public static DependencyProperty DisplayMeshProperty = ...
    }
    

    接下来,在Themes/Generic.xaml(或其中包含的ResourceDictionary)中创建样式和控件模板,与任何自定义控件一样。模板将包含以下内容:

    <Style TargetType="{x:Type local:DisplayOnPath}">
    
      <Setter Property="Template">
        <Setter.Value>
    
          <ControlTemplate TargetType="{x:Type local:DisplayOnPath}">
    
            <Viewport3DVisual ...>
    
              <ModelVisual3D>
                <ModelVisual3D.Content>
    
                  <GeometryModel3D Geometry="{Binding DisplayMesh, RelativeSource={RelativeSource TemplatedParent}}">
                    <GeometryModel3D.Material>
    
                      <DiffuseMaterial>
                        <DiffuseMaterial.Brush>
    
                          <VisualBrush ...>
                            <VisualBrush.Visual>
    
                              <ContentPresenter />
                    ...
    

    这样做是显示一个3D模型,它使用DisplayMesh定位并使用您的控件的内容作为画笔材料。

    请注意,您可能需要在Viewport3DVisual和VisualBrush上设置其他属性,以使布局按照您希望的方式工作,并使内容视觉被适当地拉伸。

    剩下的就是“ComputeDisplayMesh”功能。如果您希望内容的顶部(您正在显示的单词)与路径垂直一定距离,则这是一个简单的映射。当然,您可以选择其他算法,例如创建并行路径并使用每个算法的百分比距离。

    在任何情况下,基本算法都是相同的:

    1. 使用PathGeometry
    2. 转换为PathGeometry.CreateFromGeometry
    3. 使用您选择的启发式选择网格中适当数量的矩形'n'。也许从硬编码n = 50开始。
    4. 计算矩形所有角的Positions值。顶部有n + 1个角,底部有n + 1个角。可以通过调用PathGeometry.GetPointAtFractionOfLength找到每个底角。这也会返回一个切线,因此也很容易找到顶角。
    5. 计算您的TriangleIndices。这是微不足道的。每个矩形将是两个三角形,因此每个矩形将有六个索引。
    6. 计算您的TextureCoordinates。这更加微不足道,因为它们都是0,1或i / n(其中i是矩形索引)。
    7. 请注意,如果使用固定值n,则路径更改时唯一需要重新计算的是Posisions数组。其他一切都是固定的。

      以下是此方法的主要部分:

      var pathGeometry = PathGeometry.CreateFromGeometry(path);
      int n=50;
      
      // Compute points in 2D
      var positions = new List<Point>();
      for(int i=0; i<=n; i++)
      {
        Point point, tangent;
        pathGeometry.GetPointAtFractionOfLength((double)i/n, out point, out tangent);
        var perpendicular = new Vector(tangent.Y, -tangent.X);
        perpendicular.Normalize();
      
      
        positions.Add(point + perpendicular * height); // Top corner
        positions.Add(point); // Bottom corner
      }
      // Convert to 3D by adding 0 'Z' value
      mesh.Positions = new Point3DCollection(from p in positions select new Point3D(p.X, p.Y, 0));
      
      // Now compute the triangle indices, same way
      for(int i=0; i<n; i++)
      {
        // First triangle
        mesh.TriangleIndices.Add(i*2+0);  // Upper left
        mesh.TriangleIndices.Add(i*2+2);  // Upper right
        mesh.TriangleIndices.Add(i*2+1);  // Lower left
      
        // Second triangle
        mesh.TriangleIndices.Add(i*2+1);  // Lower left
        mesh.TriangleIndices.Add(i*2+2);  // Upper right
        mesh.TriangleIndices.Add(i*2+3);  // Lower right
      }
      // Add code here to create the TextureCoordinates
      

      就是这样。大部分代码都是上面写的。我留给你填写其余部分。

      顺便说一句,请注意,通过创造“Z”值,您可以获得一些非常棒的效果。

      <强>更新

      Mark为此实现了代码并遇到了三个问题。以下是问题和解决方案:

      1. 我在三角形#1的TriangleIndices命令中犯了一个错误。它在上面得到纠正。我最初的那些指数在左上角 - 左下角 - 右上角。通过逆时针绕三角形,我们实际上看到了三角形的背面,所以没有画任何东西。通过简单地改变指数的顺序,我们顺时针转动,以便可以看到三角形。

      2. GeometryModel3D上的绑定最初是TemplateBinding。这不起作用,因为TemplateBinding不会以相同的方式处理更新。将其更改为常规绑定可以解决问题。

      3. 3D的坐标系是+ Y是向上,而对于2D + Y是向下,所以路径是颠倒的。这可以通过在代码中否定Y或在RenderTransform上添加ViewPort3DVisual来解决,如您所愿。我个人更喜欢RenderTransform,因为它使ComputeDisplayMesh代码更具可读性。

      4. 以下是马克代码的快照,其中包含了我认为我们共享的情绪:

        Snapshot of animating text "StackOverflowIsFun"
        (来源:rayburnsresume.com

答案 1 :(得分:8)

您可能想查看Charles Petzold的MSDN文章Render Text On A Path With WPFarchived version here)。

wavy text

我发现这篇文章非常有用,他还提供了一个使用动画的示例。

答案 2 :(得分:0)

我以为我会发布我的进度细节,以便我们可以退出评论(不要格式化为好):)

这是我的主窗口:

<Window.Resources>
        <Style TargetType="{x:Type local:DisplayOnPath}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:DisplayOnPath}">
                        <Viewport3D>
                            <Viewport3D.Camera>
                                <PerspectiveCamera FieldOfView="60" 
                                               FarPlaneDistance="1000" 
                                               NearPlaneDistance="10" 
                                               Position="0,0,300" 
                                               LookDirection="0,0,-1" 
                                               UpDirection="0,1,0"/>
                            </Viewport3D.Camera>
                            <ModelVisual3D>
                                <ModelVisual3D.Content>
                                    <Model3DGroup>
                                        <AmbientLight Color="#ffffff" />
                                        <GeometryModel3D Geometry="{TemplateBinding DisplayMesh}">
                                            <GeometryModel3D.Material>
                                                <DiffuseMaterial>
                                                    <DiffuseMaterial.Brush>
                                                        <SolidColorBrush Color="Red" />
                                                    </DiffuseMaterial.Brush>
                                                </DiffuseMaterial>
                                            </GeometryModel3D.Material>
                                        </GeometryModel3D>
                                    </Model3DGroup>
                                </ModelVisual3D.Content>
                            </ModelVisual3D>
                        </Viewport3D>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Storyboard x:Key="movepath">
            <PointAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="p1" Storyboard.TargetProperty="(Path.Data).(PathGeometry.Figures)[0].(PathFigure.Segments)[4].(LineSegment.Point)">
                <SplinePointKeyFrame KeyTime="00:00:01" Value="181.5,81.5"/>
            </PointAnimationUsingKeyFrames>
            <PointAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="p1" Storyboard.TargetProperty="(Path.Data).(PathGeometry.Figures)[0].(PathFigure.Segments)[3].(LineSegment.Point)">
                <SplinePointKeyFrame KeyTime="00:00:01" Value="141.5,69.5"/>
            </PointAnimationUsingKeyFrames>
            <PointAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="p1" Storyboard.TargetProperty="(Path.Data).(PathGeometry.Figures)[0].(PathFigure.Segments)[1].(LineSegment.Point)">
                <SplinePointKeyFrame KeyTime="00:00:01" Value="62.5,49.5"/>
            </PointAnimationUsingKeyFrames>
        </Storyboard>
    </Window.Resources>

    <Window.Triggers>
        <EventTrigger RoutedEvent="FrameworkElement.Loaded">
            <BeginStoryboard Storyboard="{StaticResource movepath}"/>
        </EventTrigger>
    </Window.Triggers>

  <Grid x:Name="grid1">
    <Path x:Name="p1" Stroke="Black" Margin="238.5,156.5,331.5,0" VerticalAlignment="Top" Height="82">
        <Path.Data>
            <PathGeometry>
                <PathFigure StartPoint="0.5,0.5">
                    <LineSegment Point="44.5,15.5"/>
                    <LineSegment Point="73.5,30.5"/>
                    <LineSegment Point="91.5,56.5"/>
                    <LineSegment Point="139.5,53.5"/>
                    <LineSegment Point="161,80"/>
                </PathFigure>
            </PathGeometry>
        </Path.Data>
    </Path>
    <local:DisplayOnPath x:Name="wave1" Path="{Binding Data, ElementName=p1, Mode=Default}" />
    </Grid>

然后我有实际的用户控件:

public partial class DisplayOnPath : UserControl
    {
        public MeshGeometry3D DisplayMesh
        {
            get { return (MeshGeometry3D)GetValue(DisplayMeshProperty); }
            set { SetValue(DisplayMeshProperty, value); }
        }

        public Geometry Path
        {
            get { return (Geometry)GetValue(PathProperty); }
            set { SetValue(PathProperty, value); }
        }

        public static readonly DependencyProperty DisplayMeshProperty = 
            DependencyProperty.Register("DisplayMesh", typeof(MeshGeometry3D), typeof(DisplayOnPath), new FrameworkPropertyMetadata(new MeshGeometry3D(), FrameworkPropertyMetadataOptions.AffectsRender));

        public static readonly DependencyProperty PathProperty =
        DependencyProperty.Register("Path", 
                                    typeof(Geometry), 
                                    typeof(DisplayOnPath), 
                                    new PropertyMetadata()
                                    {
                                        PropertyChangedCallback = (obj, e) =>
                                        {
                                            var displayOnPath = obj as DisplayOnPath;
                                            displayOnPath.DisplayMesh = ComputeDisplayMesh(displayOnPath.Path);
                                        }
                                    }
        );

        private static MeshGeometry3D ComputeDisplayMesh(Geometry path)
        {
            var mesh = new MeshGeometry3D();

            var pathGeometry = PathGeometry.CreateFromGeometry(path);
            int n = 50;
            int height = 10;

            // Compute points in 2D
            var positions = new List<Point>();
            for (int i = 0; i <= n; i++)
            {
                Point point, tangent;
                pathGeometry.GetPointAtFractionLength((double)i / n, out point, out tangent);
                var perpendicular = new Vector(tangent.Y, -tangent.X);
                perpendicular.Normalize();
                positions.Add(point + perpendicular * height); // Top corner
                positions.Add(point); // Bottom corner
            }
            // Convert to 3D by adding 0 'Z' value
            mesh.Positions = new Point3DCollection(from p in positions select new Point3D(p.X, p.Y, 0));

            // Now compute the triangle indices, same way
            for (int i = 0; i < n; i++)
            {
                // First triangle
                mesh.TriangleIndices.Add(i * 2 + 0);  // Upper left
                mesh.TriangleIndices.Add(i * 2 + 1);  // Lower left
                mesh.TriangleIndices.Add(i * 2 + 2);  // Upper right
                // Second triangle
                mesh.TriangleIndices.Add(i * 2 + 1);  // Lower left
                mesh.TriangleIndices.Add(i * 2 + 2);  // Upper right
                mesh.TriangleIndices.Add(i * 2 + 3);  // Lower right
            }

            for (int i = 0; i <= n; i++)
            {
                for (int j = 0; j < 2; j++)
                {
                    mesh.TextureCoordinates.Add(new Point((double) i/n, j));
                }
            }

            //Console.WriteLine("Positions=\"" + mesh.Positions + "\"\nTriangleIndices=\"" + mesh.TriangleIndices +
            //                  "\"\nTextureCoordinates=\"" + mesh.TextureCoordinates + "\"");
            return mesh;
        }

        static DisplayOnPath()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(DisplayOnPath), new FrameworkPropertyMetadata(typeof(DisplayOnPath)));
        }

        public DisplayOnPath()
        {
            InitializeComponent();
        }
    }

目前,除了路径之外,这不会呈现任何其他内容。

但是如果在窗口加载后得到wave1的网格细节,那么将绑定替换为硬编码值,你得到:http://img199.yfrog.com/i/path1.png/

其中有两个主要问题:

  1. 三角形都是尖的,所以我认为矩形没有正确定义
  2. 它逆转了!但我认为那与切线有关
相关问题