Векторная графика в WinRT

77

Как вы уже видели, для вывода текста и графики в приложениях Windows 8 программист создает объекты типа TextBlock и Image и присоединяет их к визуальному дереву страницы. Здесь не существует концепции пользовательской прорисовки, по крайней мере на уровне приложения. Элементы TextBlock и Image прорисовывают себя во внутренних механизмах Windows Runtime.

А если вам потребуется вывести векторную графику - линии, кривые, заполненные фигуры, для этого не следует вызывать такие методы, как DrawLine и DrawBezier. В Windows Runtime таких методов нет! Методы с такими именами существуют в DirectX, и они могут использоваться в приложениях Windows 8, но при использовании Windows Runtime программист вместо этого создает элементы типа Line, Polyline, Polygon и Path. Эти классы являются производными от класса Shape (который, в свою очередь, является производным от FrameworkElement); все упомянутые классы находятся в пространстве имен Windows.UI.Xaml.Shapes, которое иногда называют «библиотекой Shapes».

Из всех компонентов библиотеки Shapes самыми широкими возможностями обладают Polyline и Path. Класс Polyline выводит набор соединенных прямых отрезков, но в действительности он предназначен для рисования сложных кривых. От программиста потребуется лишь задать множество достаточно коротких отрезков. Не сомневайтесь и передавайте классу Polyline тысячи отрезков - для этого он и нужен.

Давайте используем Polyline для рисования архимедовой спирали. Файл XAML нашей программы создает экземпляр Polyline, но не включает в него точки, определяющие фигуру:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Polyline x:Name="polyline"
                  Stroke="{StaticResource ApplicationForegroundThemeBrush}"
                  StrokeThickness="3"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center" />
</Grid>

Свойство Stroke (унаследованное от Shape) содержит кисть, используемую для рисования звеньев. Как правило, используется кисть SolidColorBrush, но как вы вскоре увидите, это не обязательно. Я использовал StaticResource с предопределенным идентификатором, который определяет белую кисть с темной темой или черную кисть со светлой темой. Класс StrokeThickness (также унаследованный от Shape) содержит толщину отрезков в пикселах, а свойства HorizontalAlignment и VerticalAlignment уже встречались нам ранее.

На первый взгляд задание свойств HorizontalAlignment и VerticalAlignment для векторной графики выглядит немного странно. Пожалуй, здесь нужны более подробные объяснения. При работе с двумерной векторной графикой используются точки (X, Y) в декартовой системе координат, где X - координата по горизонтальной оси, а Y - координата по вертикальной оси. В векторной графике Windows Runtime используется системе координат, часто применяемая в оконных средах: значения X возрастают слева направо (как обычно), а значения Y возрастают сверху вниз (то есть в направлении, обратном традиционному).

Если используются только положительные значения X и Y, начало координат - точка (0,0) - находится в левом верхнем углу графической фигуры.

Отрицательные координаты могут использоваться для обозначения точек, находящихся левее и выше начала координат. Но когда Windows Runtime вычисляет размеры векторного графического объекта для целей формирования макета, отрицательные координаты игнорируются. Допустим, вы рисуете ломаную линию с точками, у которых координаты X лежат в диапазоне от -100 до 300, а координаты Y - в диапазоне от -200 до 400. Подразумевается, что ломаная имеет ширину 400 пикселов и высоту 600 пикселов. Однако в контексте формирования макета и выравнивания ломаная рассматривается так, словно ее ширина равна 300 пикселам, а высота - 400 пикселам.

Чтобы векторная графика нормально интерпретировалась системой формирования макета Windows Runtime, достаточно выполнения одного условия: точка (0,0) должна находиться в левом верхнем углу. Максимальная положительная координата X становится шириной, а максимальная положительная координата Y - высотой элемента.

Для указания координат в пространстве имен Windows.Foundation имеется структура Point, содержащая два свойства X и Y типа double. Кроме того, пространство имен Windows.UI.Xaml.Media определяет класс PointCollection для представления набора объектов Point.

Непосредственно в Polyline определяется всего одно свойство Points типа PointCollection. Набор точек может быть назначен свойству Points в разметке XAML, но чаще точки вычисляются по некоторому алгоритму; для таких случаев идеально подходит программный код. В конструкторе класса программы цикл for проходит углы от 0 до 3600°, фактически выполняя 10 полных оборотов:

public MainPage()
{
    this.InitializeComponent();

    for (int angle = 0; angle < 3600; angle++)
    {
        double radians = Math.PI * angle / 180;
        double radius = angle / 10;
        double x = 360 + radius * Math.Sin(radians);
        double y = 360 + radius * Math.Cos(radians);
        polyline.Points.Add(new Point(x, y));
    }
}

Переменная radians преобразует градусы в радианы для тригонометрических функций .NET, a radius вычисляется в диапазоне от 0 до 360 в зависимости от angle; таким образом, максимальный радиус составляет 360 пикселов. Значения, возвращаемые статическими методами Math.Sin и Math.Cos, умножаются на radius. Соответственно произведения будут лежать в диапазоне от -360 до 360 пикселов.

Чтобы все пикселы имели положительные значения относительно левого верхнего угла, к обоим произведениям прибавляется 360. Соответственно центр спирали находится в точке (360,360), а сама она распространяется не более чем на 360 пикселов в любом направлении.

Цикл завершается созданием экземпляра Point и его включением в коллекцию Points объекта Polyline. Результат представлен на следующем рисунке:

Использование векторной графики WinRT для построения спирали

Без настроек HorizontalAlignment и VerticalAlignment фигура была бы выровнена по левому верхнему углу страницы. Если при этом убрать из вычислений прибавление смещения для центра спирали, то центр будет находиться в левом верхнем углу страницы, а три четверти фигуры останутся невидимыми. А если оставить HorizontalAlignment и VerticalAlignment значение Center, но убрать смещение, фигура будет размещена таким образом, что правая нижняя четверть окажется выровненной по центру.

Спираль почти заполняет экран, но это объясняется только тем, что я выбрал экран с высотой 760 пикселов. А если нужно, чтобы спираль заполняла экран независимо от его размеров?

Одно из возможных решений - включение размера экрана в пикселах непосредственно в вычисление координат спирали. Есть и другое решение: в классе Shape определяется свойство с именем Stretch, которое используется точно так же, как и свойство Stretch класса Image. По умолчанию свойство Stretch для Polyline равно значению перечисляемого типа Stretch.None (растяжение отключено), но ему можно задать значение Uniform, чтобы фигура заполняла контейнер с сохранением пропорций изображения.

Эта возможность продемонстрирована в примере ниже, где также устанавливается увеличенная ширина кисти:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Polyline x:Name="polyline"
                  Stroke="{StaticResource ApplicationForegroundThemeBrush}"
                  StrokeThickness="6"
                  Stretch="Uniform" />
</Grid>

Файл фонового кода вычисляет координаты точек спирали в произвольной системе координат, которая в моем примере основана на величине радиуса 1000:

for (int angle = 0; angle < 3600; angle++)
{
    double radians = Math.PI * angle / 180;
    double radius = angle / 3.6;
    double x = 1000 + radius * Math.Sin(radians);
    double y = 1000 - radius * Math.Cos(radians);
    polyline.Points.Add(new Point(x, y));
}

Возможно, вы заметили, что при вычислении y знак "+" заменен на "-" чтобы спираль заканчивалась наверху, а не внизу. Переключение на светлую тему демонстрирует, как удобно использовать ApplicationForegroundThemeBrush для цвета Stroke:

Растягивание векторного изображения по ширине экрана

Попробуйте задать свойству Stretch значение Fill; вы увидите, как круговая спираль искажается и превращается в эллиптическую.

Вспомните, как кисть LinearGradientBrush адаптируется к размерам того элемента, к которому она применена. То же относится и к использованию кисти с векторной графикой. Давайте вместо нее применим ImageBrush - кисть, созданную на основе растрового изображения. Пример разметки XAML ниже существенно увеличивает ширину линии и создает экземпляр ImageBrush:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Polyline x:Name="polyline"
                  StrokeThickness="26"
                  Stretch="Uniform">
            <Polyline.Stroke>
                <ImageBrush Stretch="UniformToFill"
                            ImageSource="http://professorweb.ru/my/windows8/rt/level1/files/win8logo.png"
                            AlignmentY="Top" />
            </Polyline.Stroke>
        </Polyline>
</Grid>

Свойство ImageSource класса ImageBrush относится к типу ImageSource, как и свойство Source класса Image. В XAML его значением можно задать URL-адрес. ImageBrush имеет собственное свойство Stretch, которое по умолчанию равно Fill (изображение растягивается для заполнения области без соблюдения исходных пропорций). Чтобы не выглядеть толстым на картинке, я выбрал режим UniformToFill, при котором область заполняется с соблюдением исходных пропорций (естественно, с отсечением части изображения). Свойства AlignmentX и AlignmentY определяют, каким образом изображение должно выравниваться по графической фигуре, и соответственно, какая часть изображения должна отсекаться. В данном случае я предпочитаю потерять нижнюю часть, чтобы сохранить верх:

Использование кисти ImageBrush вместе с векторной графикой

Обратите внимание: выравнивание изображения базируется на геометрической линии спирали, а не на линии, выведенной кистью с шириной 26 пикселов. Это приводит к отсечению областей у верхнего, левого и правого края. Проблему можно решить при помощи свойства Transform класса ImageBrush, но не будем забегать вперед.

Возможно, вы заметили, что ImageBrush наследует от TileBrush. Такое наследование наводит на мысль, что копии растрового изображения можно выстраивать по вертикали и горизонтали для мозаичного заполнения поверхности, но Windows Runtime не поддерживает такую возможность.

Polyline может использоваться для вывода любой кривой, определяемой параметрической формулой. Но при прорисовке сложных кривых - дуг (то есть кривых на эллиптических контурах), кубических кривых Безье (стандартная разновидность) и квадратичных кривых Безье (имеющих только одну контрольную точку), вместо Polyline лучше использовать элемент Path.

Path определяет всего одно свойство с именем Data и типом Geometry (класс, определяемый в пространстве имен Windows.UI.Xaml.Media). В Windows Runtime Geometry и сопутствующие классы представляют чистую аналитическую геометрию. Geometry определяет линии и кривые с использованием координатных точек, а прорисовывает их с конкретным типом и шириной кисти.

Самый мощный и универсальный класс, производный от Geometry, называется PathGeometry. Свойство содержимого PathGeometry называется Figures; оно содержит коллекцию объектов PathFigure. Каждый объект PathFigure представляет собой серию соединенных отрезков и кривых. Свойство содержимого PathFigure - Segments - содержит коллекцию объектов PathSegment. PathSegment является базовым классом для LineSegment, PolylineSegment, BezierSegment, PolyBezierSegment, QuadraticBezierSegment, PolyQuadraticBezierSegment и ArcSegment.

Выведем приветствие HELLO с использованием Path и PathGeometry:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Path Stroke="{StaticResource rainbowBrush}"
              StrokeThickness="12"
              StrokeLineJoin="Round"
              HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <Path.Data>
                <PathGeometry>
                    <!-- H -->
                    <PathFigure StartPoint="0 0">
                        <LineSegment Point="0 100" />
                    </PathFigure>
                    <PathFigure StartPoint="0 50">
                        <LineSegment Point="50 50" />
                    </PathFigure>
                    <PathFigure StartPoint="50 0">
                        <LineSegment Point="50 100" />
                    </PathFigure>

                    <!-- E -->
                    <PathFigure StartPoint="125 0">
                        <BezierSegment Point1="60 -10" Point2="60 60" Point3="125 50" />
                        <BezierSegment Point1="60 40" Point2="60 110" Point3="125 100" />
                    </PathFigure>

                    <!-- L -->
                    <PathFigure StartPoint="150 0">
                        <LineSegment Point="150 100" />
                        <LineSegment Point="200 100" />
                    </PathFigure>

                    <!-- L -->
                    <PathFigure StartPoint="225 0">
                        <LineSegment Point="225 100" />
                        <LineSegment Point="275 100" />
                    </PathFigure>

                    <!-- O -->
                    <PathFigure StartPoint="300 50">
                        <ArcSegment Size="25 50" Point="300 49.9" IsLargeArc="True" />
                    </PathFigure>
                </PathGeometry>
            </Path.Data>
        </Path>
</Grid>

Каждая буква определяется одним или несколькими объектами PathFigure, которые всегда задают начальную точку для нескольких соединенных линий. Классы, производные от PathSegment, продолжают рисование фигуры от этой точки. Например, для рисования буквы "E" BezierSegment задает две контрольные точки и одну конечную точку. Следующий объект BezierSegment продолжает рисование от конца предыдущего сегмента. (В ArcSegment конечная точка дуги не может совпадать с начальной, иначе ничего выведено не будет; по этой причине я разнес их на 1/10 пиксела. Правильнее было бы разделить ArcSegment на два сегмента, каждый из которых рисует половину окружности.)

Рисование векторного текста в приложении Windows Runtime

Результат наводит на мысль, что пара кривых Безье - не лучший способ рисования прописной буквы E. Попробуйте задать свойству Stretch объекта Path значение Fill, чтобы увеличить изображение:

Растягивание векторного текста по ширине

Конечно, объекты PathFigure и PathSegment можно построить и в коде, но я покажу более простой способ сделать это в разметке XAML. Синтаксис разметки Path состоит из букв, координат, периодически встречающихся размеров и пары логических значений, которые существенно сокращают объем разметки. В примере ниже показана разметка, выдающая тот же результат, но использующая мини-язык описания геометрий XAML:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Path Stroke="{StaticResource rainbowBrush}"
              StrokeThickness="12"
              StrokeLineJoin="Round"
              HorizontalAlignment="Center"
              VerticalAlignment="Center"
              Data="M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100
                    M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100
                    M 150 0 L 150 100, 200 100
                    M 225 0 L 225 100, 275 100
                    M 300 50 A 25 50 0 1 0 300 49.9" 
              />
</Grid>

Свойство Data представляет собой одну большую строку, но я разделил его на пять строк, соответствующих пяти командам. Команда M обозначает перемещение (Move); за ней следует пара координат X и Y, определяющих начало фигуры. Команда L обозначает линию (Line, хотя правильнее было бы назвать ее «ломаной», то есть polyline); за ней следует одна или несколько точек. Команда C обозначает кубическую кривую Безье (Cubic); за ней следуют контрольные точки и конечная точка. Команда A обозначает дугу (Arc). Определение дуги является самым сложным: первые два числа определяют горизонтальный и вертикальный радиусы эллипса, повернутого на угол в градусах, заданный следующим аргументом. Далее следуют два флага: свойства IsLargeArc и направления дуги, а за ними указывается конечная точка. В данном примере не используется часто полезная команда Z, которая замыкает фигуру, проводя отрезок к начальной точке.

Определение сложной геометрии в синтаксисе разметки Path - один из примеров того, что может быть сделано только средствами XAML. Класс, выполняющий это преобразование, закрыт для использования в Windows Runtime - он доступен только для парсера XAML. Для преобразования строки синтаксиса разметки Path в объект Geometry необходим механизм преобразования XAML в программный объект.

К счастью, некое подобие такого механизма все же существует. Статический метод XamlReader.Load() в пространстве имен Windows.UI.Xaml.Markup получает строку XAML и выдает экземпляр корневого элемента с полным набором созданных и собранных экземпляров остальных частей дерева. У XamlReader.Load() есть некоторые ограничения (например, разбираемая разметка XAML не может ссылаться на обработчики событий во внешнем коде), но в целом это очень мощный инструмент.

Вот как выглядит объект Path с синтаксисом разметки Path, созданным исключительно в программном коде:

using Windows.UI;                   // для перечисления Colors
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Markup;       // для XamlReader
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;       // для Path

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            Path path = new Path
            {
                Stroke = App.Current.Resources["rainbowBrush"] as LinearGradientBrush,
                StrokeThickness = 12,
                StrokeLineJoin = PenLineJoin.Round,
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center,
                Data = PathMarkupToGeometry(
                    "M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100 " +
                    "M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100 " +
                    "M 150 0 L 150 100, 200 100 " +
                    "M 225 0 L 225 100, 275 100 " +
                    "M 300 50 A 25 50 0 1 0 300 49.9")
            };

            (this.Content as Grid).Children.Add(path);
        }

        private Geometry PathMarkupToGeometry(string pathMarkup)
        {
            string xaml =
                "<Path " +
                "xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
                "<Path.Data>" + pathMarkup + "</Path.Data></Path>";

            Path path = XamlReader.Load(xaml) as Path;

            // Отделить PathGeometry от Path
            Geometry geometry = path.Data;
            path.Data = null;
            return geometry;
        }
    }
}

Будьте внимательны при работе с классом Path в коде: файл MainPage.xaml.cs, генерируемый Visual Studio, не содержит директивы using для пространства имен Windows.UI.Xaml.Shapes, в котором находится Path, но зато в нем присутствует директива using для пространства System.IO, содержащего совершенно другой класс Path для работы с файлами и каталогами.

«Волшебный» метод вызывается в конце. Он собирает из корректной разметки XAML маленькую структуру с корневым элементом Path и конструкцией элементов свойств, в которую заключена строка синтаксиса разметки Path. Не забудьте, что разметка XAML должна включать стандартное объявление пространства имен XML. Если метод XamlReader.Load() не выявит ошибок, он возвращает объект Path со свойством Data, которому задан объект PathGeometry. Но чтобы объект PathGeometry можно было использовать с другим объектом Path, его необходимо отсоединить от текущего объекта Path, для чего свойству Data возвращенного объекта Path задается значение null.

Выбор правильного презентабельного номера телефона важен для вашей компании или бизнеса. Вы можете выбрать красивые номера, которые легко запомнить. Такие номера телефонов обязательно привлекут к вам новых клиентов.

Пройди тесты
Лучший чат для C# программистов