Элемент Canvas в WinRT

192

Последний производный от Panel класс, который будет рассмотрен - Canvas, который является самой «традиционной» разновидностью панелей, потому что он позволяет позиционировать элементы в конкретных точках.

Но какое свойство элемента-потомка следует задать, чтобы обозначить позицию элемента относительно Canvas? Если вы попробуете найти в списке свойств, определяемых UIElement и FrameworkElement, имена вида Location, Position, X или Y, вы их там не найдете. Свойства, позволяющие задавать координаты, поддерживаются для векторной графики, но не для других элементов. В Windows Runtime они не имеют смысла, потому что они неприменимы при использовании Grid, StackPanel или WrapPanel. Нам до сих пор удавалось обходиться без указания конкретных пикселов при позиционировании элементов; такая необходимость может возникнуть только в том случае, если элемент является потомком Canvas.

По этой причине класс Canvas сам определяет свойства, используемые для позиционирования элементов относительно самого себя. Это особая категория свойств - так называемые вложенные свойства (attached properties) - составляет подмножество свойств зависимости. Вложенные свойства, определяемые одним классом (Canvas в нашем примере), в действительности задаются для экземпляров других классов (потомков Canvas в нашем случае). Объекты, для которых задаются вложенные свойства, не обязаны знать, что делает свойство и откуда было получено значение.

Давайте посмотрим, как работает эта схема. Ниже показан файл XAML, который содержит элемент Canvas в стандартном элементе Grid (также можно заменить Grid на Canvas). Элемент Canvas содержит трех потомков TextBlock:

<Page ...
    FontSize="40"
    Foreground="{StaticResource ApplicationForegroundThemeBrush}">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Canvas>
            <TextBlock Canvas.Left="0" Canvas.Top="0">
                Текст в элементе Canvas - <Run Foreground="Coral">позиция (0,0)</Run>
            </TextBlock>
            <TextBlock Canvas.Left="200" Canvas.Top="100">
                Текст в элементе Canvas - <Run Foreground="Coral">позиция (200,100)</Run>
            </TextBlock>
            <TextBlock Canvas.Left="400" Canvas.Top="200">
                Текст в элементе Canvas - <Run Foreground="Coral">позиция (400,200)</Run>
            </TextBlock>
        </Canvas>
    </Grid>
</Page>

Результат (не особо впечатляющий):

Позиционирование элементов на Canvas

Еще раз присмотритесь к разметке и обратите внимание на странный синтаксис:

<TextBlock Canvas.Left="400" Canvas.Top="200">

Судя по именам, атрибуты Canvasx.Left и Canvas.Top определяются классом Canvas, однако задаются они потомками класса Canvas для обозначения своих позиций. Имена атрибутов XAML, в которых указываются имена класса и свойства, всегда относятся к вложенным свойствам.

Как ни странно, сам класс Canvas не определяет свойства с именами Left и Top! Он определяет свойства и методы с похожими, но не полностью совпадающими именами.

Природа вложенных свойств станет чуть более понятной, если посмотреть, как они задаются в коде. Следующий файл XAML был упрощен и содержит только элемент Canvas в стандартной панели Grid:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
     <Canvas x:Name="canvas" />
</Grid>

За все остальное отвечает файл отделенного кода. Он переопределяет метод OnTapped() для создания точки (а на самом деле - элемента Ellipse) и элемента TextBlock; оба элемента добавляются в Canvas в точке касания:

using System;
using Windows.Foundation;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Shapes;

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

        protected override void OnTapped(TappedRoutedEventArgs args)
        {
            Point pt = args.GetPosition(this);

            // Создать точку
            Ellipse ellipse = new Ellipse
            {
                Width = 3,
                Height = 3,
                Fill = this.Foreground
            };

            Canvas.SetLeft(ellipse, pt.X);
            Canvas.SetTop(ellipse, pt.Y);
            canvas.Children.Add(ellipse);

            // Создать текст
            TextBlock txtblk = new TextBlock
            {
                Text = String.Format("({0})", pt),
                FontSize = 24,
            };

            Canvas.SetLeft(txtblk, pt.X);
            Canvas.SetTop(txtblk, pt.Y);
            canvas.Children.Add(txtblk);

            args.Handled = true;
            base.OnTapped(args);
        }
    }
}

Если прикоснуться к экрану, в месте касания появляется точка и текст:

Добавление точек касания в элемент Canvas

А вот как позиция точки задается в коде при добавлении в коллекцию Children элемента Canvas:

Canvas.SetLeft(txtblk, pt.X);
Canvas.SetTop(txtblk, pt.Y);
canvas.Children.Add(txtblk);

Порядок неважен: можно сначала добавить элемент в Canvas, а потом задать его позицию. Статические методы Canvas.SetLeft() и Canvas.SetTop() здесь играют ту же роль, что и атрибуты Canvas.Left и Canvas.Top в XAML. Они задают координатную точку, в которой позиционируется некоторый элемент. (У моего способа позиционирования точки имеется небольшой недостаток, который становится очевидным, если немного увеличить эллипс. Программа должна размещать в точке касания центр эллипса, тогда как использованные мной вызовы Canvas.SetLeft и Canvas.SetTop помещают там левый верхний угол эллипса. Если вы хотите, чтобы центр эллипса находился в точке pt, вычтите из pt.X половину его ширины, а из pt.Y половину его высоты.)

Как вы поняли, элемент Canvas удобен для размещения растровой и векторной графики, а также создания видео. Он позволяет гибко размещать видео и фотофайлы используя систему координат для позиционирования этих элементов.

Ранее я упоминал о том, что Canvas не определяет свойства Left и Top. Вместо них Canvas определяет статические методы SetLeft() и SetTop(), а также статические свойства типа DependencyProperty. Вот как выглядели бы определения двух объектов DependencyProperty, если бы класс Canvas был написан на C#:

public static DependencyProperty LeftProperty { get; set; }
public static DependencyProperty TopProperty { get; set; }

Как вы увидите позднее, это особые типы свойств зависимости, которые отличаются тем, что они могут задаваться для элементов, отличных от Canvas.

А сейчас я покажу нечто интересное. Наша программа вызывает статические методы Canvas.SetLeft() и Canvas.SetTop() следующим образом:

Canvas.SetLeft(ellipse, pt.X);
Canvas.SetTop(ellipse, pt.Y);

Альтернативное решение - настолько же законное, настолько же корректное и на 100% эквивалентное - основано на вызове SetValue() для элемента-потомка и обращении к статическим объектам DependencyProperty, определенным Canvas:

ellipse.SetValue(Canvas.LeftProperty, pt.X);
ellipse.SetValue(Canvas.TopProperty, pt.Y);

Эти команды полностью эквивалентны вызовам Canvas.SetLeft() и Canvas.SetTop(). Вы можете использовать любую из двух форм.

Метод SetValue() уже встречался нам ранее. Он определяется классом DependencyObject и наследуется очень многими классами в Windows Runtime. Свойство (такое, как FontSize) в действительности определяется в виде статического свойства зависимости, которое становится аргументом для того же метода SetValue():

public double FontSize
{
    get { return (double)GetValue(FontSizeProperty); }
    set { SetValue(FontSizeProperty, value); }
}

И хотя я никогда не видел внутреннего исходного кода класса Canvas, я практически уверен в том, что статические методы SetLeft() и SetTop() в Canvas определяются в коде, эквивалентном следующему синтаксису C#:

public static void SetLeft(DependencyObject element, double value)
{
    element.SetValue(LeftProperty, value);
}

public static void SetTop(DependencyObject element, double value)
{
    element.SetValue(TopProperty, value);
}

Эти методы очень четко показывают, что свойство зависимости на самом деле задается для элемента-потомка, а не для Canvas. Canvas также определяет методы GetLeft() и GetTop() в коде, эквивалентном следующему:

public static void GetLeft(DependencyObject element)
{
    return (double)element.GetValue(LeftProperty);
}

public static void GetTop(DependencyObject element)
{
    return (double)element.GetValue(TopProperty);
}

Класс Canvas использует эти методы во внутренней реализации для получения координат левого верхнего узла каждого из своих потомков, чтобы потом позиционировать их в процессе формирования макета.

Статические методы SetLeft, SetTop, GetLeft и GetTop предполагают, что система свойств зависимости основана на некой разновидности словаря. Метод SetValue позволяет сохранить вложенное свойство (такое, как Canvas.LeftProperty) в элементе которому ничего не известно ни о самом свойстве, ни о его предназначении. Canvas может позднее прочитать это свойство, чтобы определить, где внутри него должен находиться потомок.

Z-индекс

У Canvas имеется третье вложенное свойство, которое можно задать в XAML с использованием атрибута Canvas.ZIndex. «Z» в имени свойства относится к оси трехмерной системы координат, проходящей из экрана в направлении пользователя.

Если соседние элементы перекрываются, они обычно выводятся в порядке их следования в визуальном дереве. Это означает, что элементы, находящиеся в начале коллекции Children панели, могут быть накрыты элементами, находящимися в конце. Возьмем следующий пример:

<Grid>
    <TextBlock Text="Простой текст" Foreground="Blue" />
    <TextBlock Text="Простой текст" Foreground="Red" />
</Grid>

Красный текст закрывает часть синего текста. Такое поведение можно переопределить с помощью вложенного свойства Canvas.ZIndex. Как ни странно, оно работает для всех панелей, не только для Canvas. Чтобы синий текст располагался над красным текстом, присвойте ему большее значение z-индекса:

<Grid>
    <TextBlock Text="Простой текст" Foreground="Blue" Canvas.ZIndex="1" />
    <TextBlock Text="Простой текст" Foreground="Red" Canvas.ZIndex="0" />
</Grid>

Странности Canvas

Многое из того, что говорилось ранее о формировании макета приложения (компоновки), не относится к Canvas. Макет Canvas всегда определяется потомками: Canvas всегда предоставляет своим потомкам бесконечную ширину, то есть каждый потомок задает для себя естественный размер и занимает только это место.

Настройки HorizontalAlignment и VerticalAlignment не влияют на потомков Canvas. Свойство Stretch класса Image тоже не работает, если Image является потомком Canvas - Image всегда выводит изображение в его естественном размере. Элементы Rectangle и Ellipse в Canvas исчезают, если явно не задать им ширину и высоту.

Хотя HorizontalAlignment и VerticalAlignment не влияют на потомков Canvas, они действуют при задании их для самого элемента Canvas. С другими панелями при задании свойствам выравнивания значения, отличного от Stretch, панель принимает наименьший возможный размер, включающий всех потомков. Canvas ведет себя иначе. Стоит задать свойствам HorizontalAlignment и VerticalAlignment значения, отличные от Stretch, как Canvas уменьшается до исчезновения независимо от потомков.

Даже когда элемент Canvas уменьшается до нулевого размера, это никак не влияет на отображение его потомков. Концептуально Canvas представляет собой скорее базовую точку, нежели контейнер, а размер потомков Canvas в макете игнорируется.

Эту особенность Canvas можно использовать в своих интересах. Допустим, вы пытаетесь вывести элемент TextBlock в панели Grid, размеры которой явно недостаточны для него:

<Grid Width="200" Height="200">
    <TextBlock Text="Большой текст в элементе Grid" FontSize="140" />
</Grid>

TextBlock обрезается по размерам Grid. Конечно, можно увеличить Grid, но размер панели может быть фиксированным - например, из-за других дочерних элементов. При этом элемент TextBlock должен оставаться выровненным по отношению к этим элементам без усечения по границам Grid.

У проблемы есть в высшей степени простое решение: в Grid помещается элемент Canvas, a TextBlock помещается в Canvas:

<Grid Width="200" Height="200">
    <Canvas>
        <TextBlock Text="Большой текст в элементе Grid" FontSize="140" />
    </Canvas>
</Grid>

И хотя теперь Canvas усекается по размерам Grid, с TextBlock это не происходит. Элемент TextBlock остается там, где он должен находиться (выровненным по левому верхнему углу Grid), но выводится без усечения. Фактически TextBlock существует за пределами обычного макета. При всей простоте этот прием может оказаться чрезвычайно полезным.

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