Элементы управления в WinRT

98

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

Элемент управления списком цветов

Всего в программе используется 283 элемента StackPanel. Для каждого из 141 цвета создается пара: вертикальный элемент StackPanel является родителем двух элементов TextBlock, а горизонтальный - родителем элемента Rectangle и вертикального элемента StackPanel. Все горизонтальные элементы StackPanel становятся потомками главного вертикального элемента StackPanel в ScrollViewer. Файл XAML, отвечающий за выравнивание этого элемента StackPanel по центру, выглядит так:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <ScrollViewer>
            <StackPanel x:Name="stackPanel" HorizontalAlignment="Center" />
        </ScrollViewer>
</Grid>

Хотя элемент StackPanel выравнивается по центру ScrollViewer (а его ширина соответствует ширине самого широкого потомка), сам элемент ScrollViewer занимает всю ширину страницы. Все видимые индикаторы и полосы прокрутки отображаются у правого края страницы. Также можно задать элементу ScrollViewer значение HorizontalAlignment; в этом случае содержимое также будет выровнено по центру, но ширина ScrollViewer будет ограничена шириной StackPanel.

В процессе перебора статических свойств класса Colors конструктор в файле отделенного кода строит для каждого свойства вложенные элементы StackPanel:

using System;
using System.Collections.Generic;
using System.Reflection;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

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

            IEnumerable<PropertyInfo> properties = typeof(Colors).GetTypeInfo().DeclaredProperties;

            foreach (PropertyInfo property in properties)
            {
                Color clr = (Color)property.GetValue(null);

                StackPanel vertStackPanel = new StackPanel
                {
                    VerticalAlignment = VerticalAlignment.Center
                };

                TextBlock txb = new TextBlock
                {
                    Text = property.Name,
                    FontSize = 24
                };
                vertStackPanel.Children.Add(txb);

                TextBlock txbRgb = new TextBlock
                {
                    Text = String.Format("{0:X2}-{1:X2}-{2:X2}-{3:X2}",
                                         clr.A, clr.R, clr.G, clr.B),
                    FontSize = 18
                };
                vertStackPanel.Children.Add(txbRgb);

                StackPanel horzStackPanel = new StackPanel
                {
                    Orientation = Orientation.Horizontal
                };

                Rectangle rectangle = new Rectangle
                {
                    Width = 72,
                    Height = 72,
                    Fill = new SolidColorBrush(clr),
                    Margin = new Thickness(6)
                };

                horzStackPanel.Children.Add(rectangle);
                horzStackPanel.Children.Add(vertStackPanel);
                stackPanel.Children.Add(horzStackPanel);
            }
        }
    }
}

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

Наследование от UserControl

Ключом к усовершенствованию нашей программы будет выражение пунктов списка цветов - вложенных элементов StackPanel, TextBlock и Rectangle - в разметке XAML. На первый взгляд это кажется невозможным. Разместить XAML в файле MainPage.xaml нельзя, потому что мы не можем приказать XAML создать 141 экземпляр элемента... разве что вставить вручную 141 копию, а это, как вы наверняка согласитесь, самое худшее из возможных решений.

Мы можем использовать стандартный прием: щелкните правой кнопкой мыши на имени проекта в окне Solution Explorer и выберите команду Add --> New Item. В диалоговом окне Add New Item выберите пункт User Control и введите имя ColorItem.xaml. В результате будет создана пара файлов: ColorItem.xaml вместе с парным файлом отделенного кода ColorItem.xaml.cs.

Файл ColorItem.xaml.cs, созданный Visual Studio, определяет в базовом пространстве имен проекта класс ColorItem, производный от UserControl:

public sealed partial class ColorItem : UserControl
{
        public ColorItem(string name, Color color)
        {
            this.InitializeComponent();
        }
}

Файл ColorItem.xaml, созданный Visual Studio, выражает то же самое средствами XAML:

<UserControl
    x:Class="WinRTTestApp.ColorItem"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinRTTestApp">
    
    <Grid>

    </Grid>
</UserControl>

На самом деле класс UserControl вам уже знаком, потому что класс Page является производным от UserControl. И под «пользователем» (user) в его имени подразумевается не конечный пользователь приложения, а разработчик, то есть вы. Наследование от UserControl - самый простой механизм создания пользовательских элементов управления разработчиком, позволяющий определять визуальное оформление в файле XAML. UserControl определяет свойство с именем Content, которое также является свойством содержимого класса, поэтому все, что находится в тегах UserControl обновится содержимым свойства Content.

Не обращайте внимания на свойства d:DesignHeight и d:DesignWidth в файле ColorItem.xaml; они используются Microsoft Expression Blend. Фактический размер элемента управления зависит от его содержимого.

Следующий шаг - определение визуального оформления пункта списка цветов в файле ColorItem.xaml:

<UserControl ...>
    
    <Grid>
        <StackPanel Orientation="Horizontal">
            <Rectangle Name="rectangle"
                       Width="72"
                       Height="72"
                       Margin="6" />

            <StackPanel VerticalAlignment="Center">
                <TextBlock Name="txb"
                           FontSize="24" />

                <TextBlock Name="txbRgb"
                           FontSize="18" />
            </StackPanel>
        </StackPanel>
    </Grid>
</UserControl>

Иерархия элементов не отличается от той, что определялась раньше в коде, но читается она гораздо лучше. Элементу Rectangle и двум элементам TextBlock теперь присвоены имена, на которые можно ссылаться в файле отделенного кода:

public sealed partial class ColorItem : UserControl
{
        public ColorItem(string name, Color color)
        {
            this.InitializeComponent();

            rectangle.Fill = new SolidColorBrush(color);
            txb.Text = name;
            txbRgb.Text = String.Format("{0:X2}-{1:X2}-{2:X2}-{3:X2}",
                color.A, color.R, color.G, color.B);
        }
}

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

Предупреждаю, что определение параметризованного конструктора в классе, производном от UserControl - довольно необычное решение. Гораздо правильнее было бы определить свойства, но я не хочу сейчас этим заниматься, потому что свойства должны быть свойствами зависимости, а на данный момент это будет слишком сложно.

Без непараметризованного конструктора экземпляры класса ColorItem не могут создаваться в XAML. Но в нашей программе это вполне нормально, потому что я и не собираюсь создавать экземпляры в XAML. Файл MainPage.xaml измененного проекта не изменился. Отличается только простота файла отделенного кода:

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

            IEnumerable<PropertyInfo> properties = typeof(Colors).GetTypeInfo().DeclaredProperties;

            foreach (PropertyInfo property in properties)
            {
                Color color = (Color)property.GetValue(null);
                ColorItem item = new ColorItem(property.Name, color);
                stackPanel.Children.Add(item);
            }
     }
}

Экземпляр ColorItem инициализируется названием цвета и значением Color, после чего добавляется на панель StackPanel.

Создание библиотек Windows Runtime

В следующей версии этой программы класс ColorItem будет оформлен в виде библиотеки, которую можно будет использовать в других проектах.

Можно сразу создать решение Visual Studio, содержащее только проект библиотеки, но на практике проект библиотеки чаще добавляется в решение существующего проекта приложения. В ходе разработки библиотеки ее необходимо будет тестировать, и наличие проекта приложения в том же решении безусловно упростит эту процедуру. А когда библиотека будет разработана на пару с приложением, вы при желании сможете предоставить ее другим пользователям.

Итак, в окне Solution Explorer добавьте в решение проект библиотеки; для этого щелкните правой кнопкой мыши на имени решения и выберите команду Add --> New Project (или выберите команду Add New Project в меню File). В диалоговом окне Add New Project выберите слева категорию Visual C# и команду создания нового проекта Windows Store. В списке шаблонов выберите пункт Class Library.

В новой библиотеке Visual Studio автоматически создает файл с именем Class1.cs, но его можно удалить. Щелкните правой кнопкой мыши на имени проекта библиотеки, выберите команду Add --> New Item, затем в диалоговом окне Add New Item выберите категорию User Control и введите имя ColorItem. Я решил немного улучшить визуальное оформление ColorItem по сравнению с тем, что вы уже видели:

<UserControl ...>
    <Grid>
        <Border BorderBrush="{StaticResource ApplicationForegroundThemeBrush}"
                BorderThickness="1"
                Width="336"
                Margin="6">
            <StackPanel Orientation="Horizontal">
                <Rectangle Name="rectangle"
                           Width="72"
                           Height="72"
                           Margin="6" />

                <StackPanel VerticalAlignment="Center">

                    <TextBlock Name="txtblkName"
                               FontSize="24" />

                    <TextBlock Name="txtblkRgb"
                               FontSize="18" />
                </StackPanel>
            </StackPanel>
        </Border>
    </Grid>
</UserControl>

Обратите внимание на элемент Border с явно заданными свойствами Width и Margin. Ширина была выбрана эмпирически на основании самого длинного названия цвета (LightGoldenrodYellow). Также обратите внимание на то, что BorderBrush задается предопределенный идентификатор кисти - черный в светлой теме оформления, белый - в темной. Темы задаются на уровне приложений, а не библиотек (в самом деле, библиотека не содержит класса App для задания темы), поэтому эта кисть будет определяться темой приложения, использующего ColorItem.

В проект приложения необходимо включить ссылку на библиотеку (несмотря на то, что они находятся в одном решении); щелкните правой кнопкой мыши на строке References вашего проекта и выберите команду Add Reference. В диалоговом окне Reference Manager выберите слева категорию Solution (тем самым вы сообщаете, что сборка находится в том же решении), щелкните на ссылке на библиотеку, а затем на кнопке ОК.

Нахождение обоих проектов в одном решении имеет очевидное преимущество: при каждом построении главного проекта приложения Visual Studio также будет заново строить библиотеку классов, если библиотека устарела.

Файл MainPage.xaml в проекте не изменился. Файл отделенного кода должен содержать директиву using для пространства имен библиотеки, но в остальном он не отличается от предыдущей версии:

...
using WinRT.Controls;

...

Результат выглядит так:

Список цветов из библиотеки Windows Runtime

Описывая создание библиотеки классов, я указал, что вам следует выбрать в диалоговом окне Add New Project вариант Class Library. Также существует другой способ создания библиотек - команда Windows Runtime Component. В нашем конкретном примере неважно, какой из двух способов вы выберете. Более того, вы можете щелкнуть правой кнопкой мыши на имени проекта библиотеки, выбрать команду Properties и изменить в поле Output type значение Class Library на Windows Runtime Component. Программа будет работать так же, как и прежде.

Однако между этими двумя способами существует важное различие: библиотека, которая будет создана при выборе Class Library, будет доступна только из других приложений C# и Visual Basic. Библиотека, созданная при выборе Windows Runtime Component, также будет доступна из C++ и JavaScript. Именно вариант Windows Runtime Component обеспечивает языковую совместимость с приложениями Windows 8.

Соответственно для библиотек Windows Runtime Component устанавливаются некоторые ограничения, отсутствующие для обычных библиотек Class Library. Например, открытые классы в них должны быть запечатанными. Если удалить ключевое слово sealed из определения элемента управления ColorItem, такой класс не может быть частью библиотеки Windows Runtime Component. Другие важные правила относятся к структурам (которые не могут содержать открытые члены, не являющиеся полями) и ограничениям на передачу типов данных через API.

Использование панели VariableSizedWrapGrid

Давайте продемонстрируем отображение списка цветов на панели VariableSizedWrapGrid. Несмотря на имя класса, все образцы цвета в нашем примере должны иметь одинаковые размеры - именно поэтому я добавил явное определение свойства Width для элемента Border в ColorItem.

Как и StackPanel, элемент VariableSizedWrapGrid содержит свойство Orientation, которое по умолчанию равно Vertical. Первые элементы коллекции Children выводятся в столбец. Отличие в том, что содержимое VariableSizedWrapGrid выводится в несколько столбцов, как на начальном экране Windows 8. Это означает, что панель VariableSizedWrapGrid по умолчанию будет прокручиваться по горизонтали, поэтому свойства ScrollViewer должны быть настроены соответствующим образом. Файл XAML выглядит так:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <ScrollViewer HorizontalScrollBarVisibility="Visible"
                      VerticalScrollBarVisibility="Disabled">
            <VariableSizedWrapGrid x:Name="wrapPanel" />
        </ScrollViewer>
</Grid>

Файл отделенного кода аналогичен файлу из предыдущей программы, но на этот раз цветовые данные размещаются в wrapPanel:

using System.Collections.Generic;
using System.Reflection;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using WinRT.Controls;

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

            IEnumerable<PropertyInfo> properties = typeof(Colors)
                .GetTypeInfo().DeclaredProperties;

            foreach (PropertyInfo property in properties)
            {
                Color color = (Color)property.GetValue(null);
                ColorItem item = new ColorItem(property.Name, color);
                wrapPanel.Children.Add(item);
            }
        }
    }
}

Результат:

Список цветов в панели VariableSizedWrapGrid

Список прокручивается по горизонтали.

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