То, что вы ищете, по сути является нелинейным преобразованием. Свойство Transform в Visual может выполнять только линейные преобразования. К счастью, 3D-функции WPF вам в помощь. Вы можете легко выполнить то, что вы ищете, создав простой пользовательский элемент управления, который будет использоваться следующим образом:
<local:DisplayOnPath Path="{Binding ...}" Content="Text to display" />
Вот как это сделать:
Сначала создайте пользовательский элемент управления «DisplayOnPath».
- Создайте его, используя пользовательский шаблон элемента управления Visual Studio (убедитесь, что ваша сборка: атрибут ThemeInfo установлен правильно и все такое)
- Добавить свойство зависимости «Путь» типа
Geometry
(используйте фрагмент wpfdp)
- Добавить свойство зависимостей «DisplayMesh» только для чтения типа
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 />
...
Для этого отображается трехмерная модель, которая использует DisplayMesh для определения местоположения и использует содержимое элемента управления в качестве материала кисти.
Обратите внимание, что вам может потребоваться установить другие свойства в Viewport3DVisual и VisualBrush, чтобы макет работал так, как вы хотите, и для визуального растяжения контента соответствующим образом.
Все, что осталось, это функция "ComputeDisplayMesh". Это тривиальное отображение, если вы хотите, чтобы верхняя часть содержимого (отображаемые слова) была перпендикулярна определенному расстоянию от пути. Конечно, есть и другие алгоритмы, которые вы можете выбрать вместо этого, например, создать параллельный путь и использовать процентное расстояние вдоль каждого.
В любом случае основной алгоритм такой же:
- Преобразовать в
PathGeometry
, используя PathGeometry.CreateFromGeometry
- Выберите подходящее количество прямоугольников в вашей сетке 'n', используя эвристику по вашему выбору. Возможно, начнем с жесткого кодирования n = 50.
- Вычислите ваши
Positions
значения для всех углов прямоугольников. Есть n + 1 углов в верхней части и n + 1 углов в нижней части. Каждый нижний угол можно найти, позвонив по номеру PathGeometry.GetPointAtFractionOfLength
. Это также возвращает касательную, поэтому легко найти и верхний угол.
- Вычислите ваш
TriangleIndices
. Это тривиально. Каждый прямоугольник будет состоять из двух треугольников, поэтому в каждом прямоугольнике будет шесть индексов.
- Вычислите ваш
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', вы можете получить действительно потрясающие эффекты.
Update
Марк реализовал код для этого и столкнулся с тремя проблемами. Вот проблемы и решения для них:
Я допустил ошибку в своем порядке TriangleIndices для треугольника # 1. Это исправлено выше. Первоначально у меня были эти индексы, идущие вверху слева - внизу слева - вверху справа. Обойдя треугольник против часовой стрелки, мы фактически увидели заднюю часть треугольника, поэтому ничего не было нарисовано. Просто изменяя порядок индексов, мы вращаемся по часовой стрелке, чтобы треугольник был виден.
Привязка к GeometryModel3D изначально была TemplateBinding
. Это не сработало, потому что TemplateBinding не обрабатывает обновления таким же образом. Замена на обычную привязку устранила проблему.
Система координат для 3D - это + Y вверх, тогда как для 2D + Y - вниз, поэтому путь оказался перевернутым. Это может быть решено либо отрицанием Y в коде, либо добавлением RenderTransform
к ViewPort3DVisual
, как вы предпочитаете. Я лично предпочитаю RenderTransform, потому что он делает код ComputeDisplayMesh более читабельным.
Вот снимок кода Марка, оживляющего настроение, которое, я думаю, мы все разделяем:
(источник: rayburnsresume.com )