2009-12-23 104 views
7

我有必要建立一個波浪捲髮尋找文本對象在我的WPF應用程序一個好看的波浪,而我其實是假設會有類型的選項是「沿着路徑彎曲」,但在Blend我沒有看到任何一個。WPF如何創建字母

我發現了一個教程,建議你需要給你的信的文本轉換爲路徑字母,然後圍繞其旋轉,但在我看來是完全可怕的,要很大的錯誤,並且沒有足夠的靈活性。

我想基本上是一個句子有一個動畫波浪效果,我怎麼能做到這一點?

感謝所有 馬克

回答

8

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

wavy text

我發現這篇文章非常有用,他還提供了他使用動畫的樣本。

+0

感謝您的鏈接,我不能相信這篇文章沒有出現在我的谷歌搜索... – Mark 2010-01-18 11:15:53

+0

不用擔心 - 很高興我可以幫助:-) – 2010-01-19 00:58:30

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從中

它會是這個樣子:

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 /> 
       ... 

這樣做是顯示一個使用DisplayMesh的位置,並使用控件的內容作爲刷材料的3D模型。

請注意,您可能需要在Viewport3DVisual和VisualBrush上設置其他屬性以使佈局以您想要的方式工作,並使內容視覺得到適當拉伸。

所有剩下的是「ComputeDisplayMesh」功能。如果您希望內容的頂部(您顯示的字詞)與路徑相距一定距離,則這是一個簡單的映射。當然,也可以選擇其他算法,例如創建並行路徑並沿每個路徑使用百分比距離。

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

  1. 轉換爲PathGeometry使用PathGeometry.CreateFromGeometry
  2. 在網格中選擇矩形適當數量的「N」,用你選擇的啓發。也許從硬編碼n = 50開始。
  3. 計算矩形的所有角落的Positions值。頂部有n + 1個角落,底部有n + 1個角落。請致電PathGeometry.GetPointAtFractionOfLength找到每個底角。這也返回一個切線,所以很容易找到頂角。
  4. 計算您的TriangleIndices。這是微不足道的。每個矩形都是兩個三角形,所以每個矩形會有六個索引。
  5. 計算您的TextureCoordinates。這更加微不足道,因爲它們都是0,1或i/n(其中i是矩形索引)。

請注意,如果您使用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'值,你可以得到一些真正令人敬畏的效果。

更新

馬克實現的代碼,這和遇到的三個問題。下面是他們的問題和解決方案:

  1. 我在三角形#1的TriangleIndices順序中犯了一個錯誤。它在上面得到糾正。我原本有這些指標左上角 - 左下角 - 右上角。通過逆時針繞過三角形,我們實際上看到了三角形的背面,所以沒有畫任何東西。通過簡單地改變索引的順序,我們順時針旋轉,因此三角形是可見的。

  2. GeometryModel3D上的綁定最初是TemplateBinding。這不起作用,因爲TemplateBinding不以相同的方式處理更新。將其更改爲常規綁定可解決問題。

  3. 3D的座標系是+ Y向上,而對於2D + Y向下,所以路徑出現顛倒。這可以通過在代碼中否定Y或在ViewPort3DVisual上添加RenderTransform來解決,如您所願。我個人更喜歡RenderTransform,因爲它使ComputeDisplayMesh代碼更具可讀性。

這裏是馬克的代碼動畫情緒的快照我想大家都分享到:

Snapshot of animating text "StackOverflowIsFun" http://rayburnsresume.com/StackOverflowImages/WavyLetters.png

+6

+1爲你付出了巨大的努力,你把你的答案。獎勵! – 2009-12-24 01:12:30

+4

+1。我不明白你的答案的一半,但爲了這樣一個徹底的解釋,你應得的;) – 2009-12-24 01:17:19

+1

這是一個很棒的回覆,我需要一些時間消化這個並進入它...我會盡快回復你我有機會玩。真的,非常感謝你的努力,如果我能夠按照我的想法做到這一點,那麼這對我的項目來說將是一個巨大的勝利,你不知道這會讓人感到興奮:) – Mark 2009-12-24 01:23:06

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/

其中有2分主要的問題,因爲它是:

  1. 三角形都是尖尖的,所以我覺得矩形沒有被正確定義
  2. 它倒過來了!但我認爲這與切線有關
+1

你應該編輯你的原始問題,而不是添加一個答案(因爲這不是一個答案) – 2010-01-16 10:57:35

相關問題