У меня есть потребность создать wavey смотрящий текстовый объект в моем приложении WPF, и я на самом деле предполагал, что будет "изгиб вдоль пути" тип опций, но я не вижу один вообще в Смешении.
Я нашел учебное руководство, которые предлагают, чтобы Вы преобразовали Вас, текст к пути, побуквенному затем, поворачивает его вокруг, но это полностью ужасно, по-моему, в большое количество места для ошибки и недостаточно гибкости.
Я по существу хочу, чтобы предложение имело анимированный волновой эффект, как я могу достигнуть этого?
Спасибо весь Mark
Вы, возможно, захотите ознакомиться со статьей Чарльза Петцольда по MSDN Render Text On A Path With WPF (архивная версия здесь).
Я нашел эту статью очень полезной, и он также приводит пример, где он использует анимацию.
То, что вы ищете, по сути, является нелинейным преобразованием. Свойство Transform в Visual может выполнять только линейные преобразования. К счастью, вам на помощь приходят 3D-функции WPF. Вы можете легко выполнить то, что ищете, создав простой настраиваемый элемент управления, который можно было бы использовать следующим образом:
<local:DisplayOnPath Path="{Binding ...}" Content="Text to display" />
Вот как это сделать:
Сначала создайте настраиваемый элемент управления «DisplayOnPath».
Geometry
(используйте фрагмент wpfdp) Geometry3D
(используйте фрагмент wpfdpro) PropertyChangedCallback
для Path для вызова метода ComputeDisplayMesh для преобразования Path в 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 />
...
Он отображает 3D-модель, которая использует DisplayMesh для определения местоположения и использует Контент вашего элемента управления в качестве материала кисти.
Обратите внимание, что вам может потребоваться установить другие свойства в Viewport3DVisual и VisualBrush, чтобы макет работал так, как вы хотите, и чтобы визуальный элемент содержимого растягивался соответствующим образом.
Все, что осталось, это " GetPointAtFractionOfLength . Это также возвращает касательную, поэтому также легко найти верхний угол.
TriangleIndices
. Это банально. Каждый прямоугольник будет состоять из двух треугольников, поэтому на каждый прямоугольник будет шесть индексов. координаты текстуры
. Это еще более тривиально, потому что все они будут равны 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", вы можете получить действительно потрясающие эффекты.
Обновление
Марк реализовал код для этого и столкнулся с тремя проблемами. Вот проблемы и решения для них:
Я сделал ошибку в моем заказе TriangleIndices для треугольника №1. Это исправлено выше. Изначально у меня были эти индексы: верхний левый - нижний левый - верхний правый. Обойдя треугольник против часовой стрелки, мы фактически увидели заднюю часть треугольника, поэтому ничего не было нарисовано. Просто изменив порядок индексов, мы обходим по часовой стрелке, чтобы треугольник был виден.
Привязка к GeometryModel3D изначально была TemplateBinding
. Это не сработало, потому что TemplateBinding не обрабатывает обновления таким же образом. Изменение его на обычную привязку устранило проблему.
Система координат для 3D: + Y - вверху, тогда как для 2D + Y - вниз, так путь оказался перевернутым. Это можно решить либо отрицанием Y в коде, либо добавлением RenderTransform
в ViewPort3DVisual
, как вы предпочитаете. Лично я предпочитаю RenderTransform, потому что он делает код ComputeDisplayMesh более читабельным.
Вот снимок кода Марка, оживляющий настроение, которое, я думаю, все мы разделяем:
(источник: rayburnsresume.com )
Я подумал, что на самом деле выложу подробности моего прогресса, чтобы мы могли выбраться из комментариев (которые не так хорошо форматированы :))
Вот мое главное окно:
<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 основные проблемы: