События изменения размера и ориентации в WinRT

184

Много лет назад, когда система Windows еще только появилась, найти информацию о программировании для Windows было нелегко. Только в декабре 1986 года в журнале Microsoft Systems Journal (предшественник MSDN Magazine) появилась первая статья о программировании для Windows. В этой статье была описана программа с именем WHATSIZE (прописными буквами, конечно), которая не делала ничего, кроме определения текущих размеров окна программы. Но при изменении размеров окна новые значения автоматически отражались в программе.

Разумеется, исходная версия программы WHATSIZE была написана для Windows API того времени, поэтому она перерисовывала свое окно в ответ на сообщение WM_PAINT. В исходном варианте Windows API это сообщение генерировалось каждый раз, когда содержимое части окна программы становилось «недействительным» и нуждалось в перерисовке. Программа могла определить свое окно так, что при изменении размеров все содержимое окна становилось недействительным.

В Windows Runtime нет аналога сообщения WM_PAINT, а вся графическая парадигма работает совершенно иначе. В предыдущих версиях Windows была реализована графическая система «непосредственного режима», в которой приложения записывали данные в видеопамять. Конечно, запись осуществлялась через программную прослойку GDI (Graphics Device Interface) и драйвер устройства, но при вызове функций графического вывода в какой-то момент код осуществлял запись в видеопамять.

В Windows Runtime все происходит не так. В открытом интерфейсе программирования нет даже концепции перерисовки. Вместо этого приложение Windows 8 создает элементы (то есть объекты классов, производных от FrameworkElement) и добавляет их в визуальное дерево приложения. Эти элементы отвечают за перерисовку самих себя. Когда приложение Windows 8 хочет вывести текст, оно вместо вызова функции вывода текста создает элемент TextBlock. Когда приложение хочет отобразить растровое изображение, оно создает элемент Image. Вместо рисования линий, кривых Безье и эллипсов программа создает элементы Polyline и Path.

Windows Runtime реализует графическую систему «абстрактного режима». Между приложением и экраном монитора существует композиционный уровень, на котором собирается весь вывод перед его представлением пользователю. Одним из важнейших преимуществ графики абстрактного режима является отсутствие мерцания при анимации.

И хотя графическая система Windows Runtime сильно отличается от предыдущих версий Windows, в каком-то смысле приложения Windows 8 напоминают своих «предков». После того как программа загружается в память и начинает работать, она проводит большую часть времени, ожидая, пока произойдет что-то «интересное». Оповещения представляются в форме событий и функций обратного вызова. Часто события передают информацию о пользовательском вводе, но существуют и другие интересные события - как, например, метод OnNavigatedTo(), который в простой одностраничной программе вызывается сразу же после возврата управления конструктором.

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

Другое событие, которое тоже может представлять интерес для приложения Windows 8 (особенно делающего то же, что делала старая программа WHATSIZE), называется SizeChanged. Ниже приведен файл XAML из программы WhatSize для Windows 8. Обратите внимание: корневой элемент определяет обработчик для события SizeChanged:

<Page ... 
    FontSize="40"
    SizeChanged="Page_SizeChanged"
    Foreground="LimeGreen">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Top">
            &#x21a4; <Run x:Name="widthText" /> пикселей &#x21a6;
        </TextBlock>
        <TextBlock VerticalAlignment="Center"
                   HorizontalAlignment="Center"
                   TextAlignment="Center">
            &#x21a5;
            <LineBreak />
            <Run x:Name="heightText" /> пикселей
            <LineBreak />
            &#x21a7;
        </TextBlock>
    </Grid>
</Page>

В оставшейся части файла XAML определяются два элемента TextBlock с объектами Run, заключенными между стрелками (на следующем рисунке показано, как они выглядят). На первый взгляд задание трем свойствам второго элемента TextBlock значения Center кажется избыточным, но все они необходимы. Первые два свойства размещают TextBlock по центру страницы, а присваивание TextAlignment приводит к центровке двух стрелок относительно текста. Двум элементам Run задаются атрибуты x:Name, чтобы свойства Text можно было задавать в коде. Это происходит в обработчике события SizeChanged:

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

    private void Page_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        widthText.Text = e.NewSize.Width.ToString();
        heightText.Text = e.NewSize.Height.ToString();
    }
}

В аргументах события новый размер задается в виде структуры Size, а обработчик просто преобразует свойства Width и Height в строки и задает их свойствам Text двух элементов Run.

Размеры экрана в приложении Windows Runtime

Если программа выполняется на устройстве, реагирующем на изменения ориентации, попробуйте повернуть экран и проследите за изменением чисел. Также можно провести пальцем от левого края экрана, чтобы активизировать режим Snap View, разделить экран между этой и другой программой и понаблюдать за изменением ширины.

Назначать обработчик события SizeChanged не обязательно. Также можно назначить его в коде - например, в конструкторе Page:

this.SizeChanged += Page_SizeChanged;

Событие SizeChanged определяется классом FrameworkElement и наследуется всеми производными классами. Несмотря на тот факт, что класс SizeChangedEventArgs является производным от RoutedEventArgs, это событие не является маршрутизируемым. На это указывает целый ряд признаков: свойство OriginalSource в аргументах события всегда равно null; свойство SizeChangedEvent отсутствует; вы получаете размер того элемента, которому назначено это событие. Но обработчик SizeChanged можно назначить любому элементу. В общем случае порядок инициирования событий соответствует порядку перехода по визуальному дереву: сначала MainPage (для нашего примера), затем Grid и TextBlock.

Если выводимый размер элемента потребуется узнать в другом контексте (то есть вне обработчика SizeChanged), информацию можно получить из свойств ActualWidth и ActualHeight, определяемых классом FrameworkElement. Собственно, при использовании этих свойств даже обработчик SizeChanged становится немного короче:

private void Page_SizeChanged(object sender, SizeChangedEventArgs e)
{
    widthText.Text = this.ActualWidth.ToString();
    heightText.Text = this.ActualHeight.ToString();
}

Скорее всего, свойства Width и Height для определения размеров вам не понадобятся. Эти свойства тоже определяются классом FrameworkElement, но по умолчанию инициализируются значением NaN (Not a Number, то есть «не число»). Программа может задать Width и Height нужные значения, но чаще эти свойства сохраняют значения по умолчанию; для определения фактических размеров элемента они бесполезны. Класс FrameworkElement также определяет свойства MinWidth, MaxWidth, MinHeight и MaxHeight, по умолчанию инициализируемые значением NaN, но эти свойства используются относительно редко.

Впрочем, попытавшись обратиться к свойствам ActualWidth и ActualHeight в конструкторе страницы, вы увидите, что они равны нулю. Хотя визуальное дерево уже было создано вызовом InitializeComponent(), оно еще не прошло через процесс формирования макета. После завершения конструктора страница последовательно получает несколько событий:

При последующих изменениях размера страницы инициируются дополнительные события SizeChanged и LayoutUpdated. Событие LayoutUpdated также может инициироваться при добавлении или удалении элементов визуального дерева и при изменениях элементов, влияющих на макет.

Где ж удобнее выполнить инициализацию после формирования исходного макета, когда все элементы визуального дерева имеют ненулевые размеры? Используйте событие Loaded. Классы, производные от Page, очень часто присоединяют обработчик для события Loaded. Как правило, событие Loaded инициируется один раз за жизненный цикл объекта Page. Я говорю «как правило», потому что при отсоединении объекта Page от родителя (Frame) и его повторном присоединении событие Loaded инициируется снова. Но это произойдет только в том случае, если вы сделаете это намеренно. Кроме того, событие Unloaded сообщит об отсоединении страницы от визуального дерева.

Все классы, производные от FrameworkElement, содержат событие Loaded. При построении визуального дерева события Loaded инициируются последовательно вверх по визуальному дереву, заканчивая потомком Page. Когда этот объект Page получает событие Loaded, он может считать, что все его потомки уже инициировали свои события Loaded и все размеры были заданы правильно.

Обработка события Loaded в классе Page является настолько распространенной задачей, что некоторые программисты обрабатывают Loaded в конструкторе с использованием анонимного обработчика:

public MainPage()
{
    this.InitializeComponent();

    Loaded += (sender, e) =>
        {
            // ...
        };
}

Иногда в приложениях Windows 8 возникает необходимость в получении информации об изменении ориентации экрана. Ранее была представлена программа, которая нормально выглядит в альбомной ориентации, но при переключении в книжный режим, вероятно, произойдет искажение текста:

Нарушение компоновки при изменении ориентации в приложении Windows Runtime

Для решения этой проблемы файл отделенного кода программы в портретном режиме задает свойству FontSize страницы значение 24:

using Windows.UI.Xaml.Controls;
using Windows.Graphics.Display;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            SetFont();
            DisplayProperties.OrientationChanged += OnDisplayPropertiesOrientationChanged;
        }

        private void OnDisplayPropertiesOrientationChanged(object sender)
        {
            SetFont();
        }

        private void SetFont()
        {
            bool isLandscape =
                DisplayProperties.CurrentOrientation == DisplayOrientations.Landscape ||
                DisplayProperties.CurrentOrientation == DisplayOrientations.LandscapeFlipped;

            this.FontSize = isLandscape ? 40 : 24;
        }
    }
}

Класс DisplayProperties и перечисление DisplayOrientations определяются в пространстве имен Windows.Graphics.Display. DisplayProperties.OrientationChanged - статическое событие, а при его возникновении текущая ориентация устройства хранится в статическом свойстве DisplayProperties.CurrentOrientation.

Более подробная информация, включая состояние Snap View, содержится в событии ViewstateChanged класса AppicationView пространства имен Windows.UI.ViewManagement.

Настройка компоновки при смене ориентации в приложении Windows Runtime

Привязки к Run

Ранее мы кратко рассмотрели механизм привязки данных - такого связывания свойств двух элементов, при котором изменение свойства-источника приводит к изменению свойства-приемника. Привязки данных особенно удобны в тех ситуациях, в которых они позволяют обойтись без обработчиков событий. Возможно ли переписать приложение WhatSize так, чтобы оно использовало привязки данных вместо обработчика SizeChanged? Давайте попробуем.

Удалите обработчик OnPageSizeChanged из файла MainPage.xaml.cs (или просто закомментируйте его, если не хотите повредить файл). В корневом теге файла MainPage.xaml удалите атрибут SizeChanged и задайте элементу Page имя myPage. Затем создайте для двух объектов Run расширения разметки Binding, ссылающиеся на свойства ActualWidth и ActualHeight страницы:

<Page ...
    FontSize="40"
    Foreground="LimeGreen"
    x:Name="myPage">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Top">
            &#x21a4; 
            <Run Text="{Binding ElementName=myPage, Path=ActualWidth}"/> 
            пикселей &#x21a6;
        </TextBlock>
        <TextBlock VerticalAlignment="Center"
                   HorizontalAlignment="Center"
                   TextAlignment="Center">
            &#x21a5;
            <LineBreak />
            <Run Text="{Binding ElementName=myPage, Path=ActualHeight}" /> 
            пикселей <LineBreak />
            &#x21a7;
        </TextBlock>
    </Grid>
</Page>

Программа компилируется нормально и работает без выдачи исключений. Но есть одна проблема: там, где должны выводиться числа, почему-то выводится 0. Выгладит довольно странно, особенно если установить те же привязки для свойства Text элемента TextBlock вместо Run:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Top"
                   Text="{Binding ElementName=myPage, Path=ActualWidth}" />
        <TextBlock VerticalAlignment="Center"
                   HorizontalAlignment="Center"
                   TextAlignment="Center"
                   Text="{Binding ElementName=myPage, Path=ActualHeight}" />
</Grid>

В этом варианте все работает:

Привязка данных к свойству зависимости в приложении Windows Runtime

По крайней мере кажется, что работает. В версии Windows 8, которую я использую для написания этой статьи, числа не обновляются при обновлении ориентации или размера страницы, хотя и должны. Теоретически привязка данных оповещается об изменении свойства-источника, чтобы свойство-приемник тоже могло обновиться; при этом исходный код приложения не содержит ни обработчиков событий, ни лишнего кода. Собственно, именно этим так хороши привязки данных.

К сожалению, вместе с привязкой к Run мы также лишаемся удобных стрелок. Почему же привязки данных работают (или почти работают) со свойством Text элемента TextBlock и совсем не работают со свойством Text элемента Run?

Все очень просто. Приемником привязки данных должно быть свойство зависимости. Этот факт становится очевидным при определении привязки данных в коде методом SetBinding. В этом заключается различие: свойство Text элемента TextBlock поддерживается свойством зависимости TextProperty, а свойство Text элемента Run - нет. У элемента Run свойство Text является обычным свойством, которое не может использоваться в качестве приемника привязки данных. Вероятно, парсер XAML не должен разрешать установление привязки для свойства Text элемента Run, но он разрешает.

Позже я покажу, как использовать элемент StackPanel для возвращения стрелок в версию WhatSize, использующую привязки.

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