Главная » Разработка для Windows Phone 7 » Картотека Windows Phone 7

0

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

На этапе подготовки к реализации этой задачи, я добавил в библиотеку Petzold.Phone.Silverlight новую панель. Отчасти эта панель аналогична панели UniformStack, обсуждаемой в главе 9. Как и UniformStack, эта новая панель выделяет всем своим дочерним элементам равное пространство. Но в отличие от UniformStack дочерние элементы этой панели перекрывают друг друга, если пространства панели не достаточно для отображения их полностью. Поэтому я назвал эту панель OverlapPanel (Панель с перекрытием).

OverlapPanel определяет свойство Orientation и расставляет свои дочерние элементы по вертикали или по горизонтали. Если OverlapPanel расставляет дочерние элементы по горизонтали, каждый последующий дочерний элемент размещается поверх и несколько правее предыдущего, оставляя видимым лишь узкую полоску с левого края. Для вертикальной ориентации видимой остается верхушка каждого дочернего элемента.

Если дочерних элементов очень много, видимая полоска становится очень тонкой. Чтобы сделать OverlapPanel более полезной, необходимо обеспечить возможность задания минимальной высоты или ширины видимой части перекрываемого элемента, даже если это будет приводить к выходу панели за границы выделенного для нее пространства. В случае выхода за допустимые границы поведение OverlapPanel во многом аналогично обычному StackPanel:для просмотра всех элементов понадобится ScrollViewer.

OverlapPanel определяет два свойства: Orientation и MinimumOverlap (Минимальное наложение):

Проект Silverlight: Petzold.Phone.Silverlight Файл: OverlapPanel.cs (фрагмент)

public class OverlapPanel : Panel {

Size maxChildSize = new Size();

public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(OverlapPanel),

new PropertyMetadata(Orientation.Horizontal, OnAffectsMeasure));

public static readonly DependencyProperty MinimumOverlapProperty = DependencyProperty.Register("MinimumOverlap", typeof(double), typeof(OverlapPanel),

new PropertyMetadata(0.0, OnAffectsMeasure));

public Orientation Orientation {

set { SetValue(OrientationProperty, value); }

get { return (Orientation)GetValue(OrientationProperty); }

}

public double MinimumOverlap {

set { SetValue(MinimumOverlapProperty, value); }

get { return (double)GetValue(MinimumOverlapProperty); }

static void OnAffectsMeasure(DependencyObject obj,

DependencyPropertyChangedEventArgs args)

{

(obj as OverlapPanel).InvalidateMeasure();

}

}

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

Метод MeasureOverride сначала проходит по всем дочерним элементам для получения их максимального размера. Для OverlapPanel с ItemsControl или ListBox все дочерние элементы будут, вероятнее всего, одного размера.

Проект Silverlight: Petzold.Phone.Silverlight Файл: OverlapPanel.cs (фрагмент)

protected override Size MeasureOverride(Size availableSize) {

if (Children.Count == 0)

return new Size(0, 0);

maxChildSize = new Size();

foreach (UIElement child in Children) {

if (Orientation == Orientation.Horizontal)

child.Measure(new Size(Double.PositiveInfinity, availableSize.Height));

else

child.Measure(new Size(availableSize.Width, Double.PositiveInfinity));

maxChildSize.Width = Math.Max(maxChildSize.Width,

child.DesiredSize.Width);

maxChildSize.Height = Math.Max(maxChildSize.Height,

child.DesiredSize.Height);

}

if (Orientation == Orientation.Horizontal) {

double maxTotalWidth = maxChildSize.Width * Children.Count; double minTotalWidth = maxChildSize.Width +

MinimumOverlap * (Children.Count – 1);

if (Double.IsPositiveInfinity(availableSize.Width))

return new Size(minTotalWidth, maxChildSize.Height);

if (maxTotalWidth < availableSize.Width)

return new Size(maxTotalWidth, maxChildSize.Height);

else if (minTotalWidth < availableSize.Width)

return new Size(availableSize.Width, maxChildSize.Height);

return new Size(minTotalWidth, maxChildSize.Height);

}

// Orientation = Vertical

double maxTotalHeight = maxChildSize.Height * Children.Count; double minTotalHeight = maxChildSize.Height +

MinimumOverlap * (Children.Count – 1);

if (Double.IsPositiveInfinity(availableSize.Height))

return new Size(maxChildSize.Width, minTotalHeight);

if (maxTotalHeight < availableSize.Height)

return new Size(maxChildSize.Width, maxTotalHeight);

else if (minTotalHeight < availableSize.Height)

return new Size(maxChildSize.Width, availableSize.Height);

return new Size(maxChildSize.Width, minTotalHeight);

}

После этого метод разделяется на две ветви в зависимости от значения свойства Orientation. Например, для вертикальной ориентации (которая будет использоваться в примере ниже) метод вычисляет maxTotalHeight (Максимальная общая высота), которая соответствует сумме высот всех дочерних элементов без наложения, и minTotalHeight (Минимальная общая высота), которая соответствует суммарной высоте всех элементов при максимально допустимом наложении. Если доступная высота не бесконечна (эта возможность обрабатывается отдельно), она либо больше maxTotalHeight, либо находится в диапазоне между minTotalHeight и maxTotalHeight, либо меньше minTotalHeight. Если все дочерние элементы могут поместиться в предоставляемом пространстве, располагаясь вплотную друг к другу без наложения, запрашивается такой размер. Но метод никогда не запрашивает высоту, меньшую, чем требуется для отображения всех дочерних элементов.

Метод ArrangeOverride несколько проще. Значение приращения соответствует минимально допустимой ширине или высоте видимой части дочернего элемента:

Проект Silverlight: Petzold.Phone.Silverlight Файл: OverlapPanel.cs (фрагмент)

protected override Size ArrangeOverride(Size finalSize) {

if (Children.Count == 0) return finalSize;

double increment = 0;

if (Orientation == Orientation.Horizontal) increment = Math.Max(MinimumOverlap,

(finalSize.Width – maxChildSize.Width) / (Children.Count – 1));

else

increment = Math.Max(MinimumOverlap,

(finalSize.Height – maxChildSize.Height) / (Children.Count – 1));

Point ptChild = new Point();

foreach (UIElement child in Children) {

child.Arrange(new Rect(ptChild, maxChildSize));

if (Orientation == Orientation.Horizontal) ptChild.X += increment;

else

ptChild.Y += increment;

}

return finalSize;

}

Проект StudentCardFile (Картотека учащихся) включает ссылки на библиотеки Petzold.Phone.Silverlight и ElPasoHighSchool. Файл MainPage.xaml включает класс StudentBodyPresenter в коллекции Resources:

<phone:PhoneApplicationPage.Resources>

<elpaso:StudentBodyPresenter x:Key="studentBodyPresenter" /> </phone:PhoneApplicationPage.Resources>

Область содержимого довольно проста и включает только ScrollViewer и ItemsControl. Свойство ItemsPanel класса ItemsControl ссылается на OverlapPanel с двумя заданными свойствами:

Проект Silverlight: StudentCardFile Файл: MainPage.xaml (фрагмент)

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"

DataContext="{Binding Source={StaticResource studentBodyPresenter}, Path=StudentBody}">

<ScrollViewer>

<ItemsControl ItemsSource="{Binding Students}"> <ItemsControl.ItemTemplate> <DataTemplate>

<local:StudentCard /> </DataTemplate> </ItemsControl.ItemTemplate>

<ItemsControl.ItemsPanel> <ItemsPanelTemplate>

<petzold:OverlapPanel Orientation="Vertical" MinimumOverlap="24" />

</ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </ScrollViewer> </Grid>

Простота разметки здесь является, главным образом, результатом задания элемента управления StudentCard в качестве значения свойства DataTemplate класса ItemsControl.

StudentCard наследуется от UserControl. Наследование от UserControl является традиционной методикой создания элемента управления, который будет использоваться как DataTemplate. Если в приведенном ниже фрагменте не обращать внимания на многоточия (…), мы имеем здесь довольно простой набор элементов TextBlock и Image со свернутым Rectangle, который используется как разделительная линия:

Проект Silverlight: StudentCardFile Файл: StudentCard.xaml (фрагмент)

<UserControl x:Class="StudentCardFile.StudentCard"

xmlns="http://schemas.microsoft.com/winfx/2 0 0 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2 0 0 6/xaml" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" Width="2 4 0" Height="2 4 0">

<Border BorderBrush="{StaticResource PhoneAccentBrush}" BorderThickness="1"

Background="{StaticResource PhoneChromeBrush}"

CornerRadius="12"

Padding="6 0">

<Grid>

<Grid.RowDefinitions>

<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>

<TextBlock Grid.Row="0"

Text="{Binding FullName}" />

<Rectangle Grid.Row="1"

Fill="{StaticResource PhoneAccentBrush}"

Height="1"

Margin="0 0 0 4" />

<Image Grid.Row="2"

Source="{Binding PhotoFilename}" />

<StackPanel Grid.Row="3"

Orientation="Horizontal" HorizontalAlignment="Center"> <TextBlock Text="GPA = " />

<TextBlock Text="{Binding GradePointAverage}" /> </StackPanel> </Grid> </Border> </UserControl>

Я задал для MinimumOverlap значение, обеспечивающее видимость этого TextBlock. Если прокрутить весь список вниз, мы увидим, что самая нижняя карточка видна полностью:

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

Это было бы замечательно, если бы требовалось просматривать только последнюю карточку. Нам же необходимо выводить на экран любую карточку, выбираемую пользователем. Одним из подходов может быть изменение присоединенного свойства Canvas.ZIndex выбранной карточки. Или можно изменять порядок всех карточек в колоде, обеспечивая перемещение выбранной карточки и ее расположение поверх остальных.

Я решил, что выбранная карточка при касании будет выезжать из колоды и затем возвращаться на место при повторном касании или при выборе другой карточки.

При интеграции ScrollViewer с остальным кодом обнаруживается, что ScrollViewer перехватывает события Manipulation. Безусловно, ScrollViewer использует эти события Manipulation для собственной логики прокрутки, но это мешает дочерним визуальным элементам ScrollViewer (таким как элементы StudentCard) обрабатывать события Manipulation для реализации собственного перемещения при выборе.

Поэтому я решил добавить в StudentCard обработчик низкоуровневого события Touch.FrameReported и использовать его для переключения значения свойства-зависимости IsOpen. Рассмотрим это свойство в файле выделенного кода StudentCard:

Проект Silverlight: StudentCardFile Файл: StudentCard.xaml.cs (фрагмент)

public partial class StudentCard : UserControl {

public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register("IsOpen", typeof(bool), typeof(StudentCard),

new PropertyMetadata(false, OnIsOpenChanged));

bool IsOpen {

set { SetValue(IsOpenProperty, value); }

get { return (bool)GetValue(IsOpenProperty); }

Чуть ниже я покажу обработчик события изменения значения свойства для IsOpen.

При касании одного из экземпляров StudentCard он должен выдвигаться из колоды карточек. Но если в этот момент раскрыта другая карточка, она должна быть задвинута назад в колоду. Если класс CardFile должен обрабатывать эту логику самостоятельно, каждому экземпляру CardFile необходим доступ ко всем остальным экземплярам. Поэтому для работы с этими экземплярами я определил статическое поле типа List:

Проект Silverlight: StudentCardFile Файл: StudentCard.xaml.cs (фрагмент)

public partial class StudentCard : UserControl {

static List<StudentCard> studentCards = new List<StudentCard>();

public StudentCard() {

InitializeComponent(); studentCards.Add(this);

}

}

Каждый новый экземпляр просто добавляет себя в коллекцию.

Я понял, что каждый отдельный экземпляр StudentCard не нуждается в собственном обработчике событий Touch.FrameReported. Все экземпляры могут использовать совместно один статический обработчик, заданный в статическом конструкторе и ссылающийся на статические поля:

Проект Silverlight: StudentCardFile Файл: StudentCard.xaml.cs (фрагмент)

public partial class StudentCard : UserControl {

static int contactTime; static Point contactPoint;

static StudentCard() {

Touch.FrameReported += OnTouchFrameReported;

}

static void OnTouchFrameReported(object sender, TouchFrameEventArgs args) {

TouchPoint touchPoint = args.GetPrimaryTouchPoint(null);

if (touchPoint != null && touchPoint.Action == TouchAction.Down) {

contactPoint = touchPoint.Position; contactTime = args.Timestamp;

}

else if (touchPoint != null && touchPoint.Action == TouchAction.Up) {

// Проверяем, приходится ли касание непосредственно // на StudentCard или дочерний элемент

DependencyObject element = touchPoint.TouchDevice.DirectlyOver;

while (element != null && !(element is StudentCard)) element = VisualTreeHelper.GetParent(element);

if (element == null) return;

// Получаем точку снятия касания и вычисляем разницу Point liftPoint = touchPoint.Position;

double distance = Math.Sqrt(Math.Pow(contactPoint.X – liftPoint.X, 2) +

Math.Pow(contactPoint.Y – liftPoint.Y, 2));

// Распознаем прикосновение как Tap, если // пройденное расстояние < 12 пикселов за 1/4 секунды

if (distance < 12 && args.Timestamp – contactTime < 250) {

// Выполняем перечисление объектов StudentCard и задаем свойство

IsOpen

foreach (StudentCard studentCard in studentCards) studentCard.IsOpen =

(element == studentCard && !studentCard.IsOpen);

}

}

}

}

Немного поэкспериментировав, я определился с тем, что буду квалифицировать как касание только прикосновение, длящееся не более гЛ секунды, при котором перемещение точки касания составляет не более 12 пикселов. Это позволит распознавать касания и отличать их от прокручивания.

В конце кода этого метода цикл foreach обеспечивает перечисление всех объектов StudentCard и задание значения свойства IsOpen для каждого из них. IsOpen всегда имеет значение false, если StudentCard не является выбранным элементом; и IsOpen всегда задается значение false, если в настоящее время это свойство имеет значение true. В противном случае, если объект StudentCard является выбранным элементом, текущее значение IsOpen false меняется на true. Конечно же, как для любого свойства-зависимости, обработчики событий изменения свойства IsOpen будут вызываться, только если значение свойства действительно изменилось.

Мы еще не рассматривали обработчик событий изменения свойства для свойства IsOpen. Как обычно, статическая версия этого метода вызывает версию экземпляра:

Проект Silverlight: StudentCardFile Файл: StudentCard.xaml.cs (фрагмент)

public partial class StudentCard : UserControl {

static void OnIsOpenChanged(DependencyObject obj,

DependencyPropertyChangedEventArgs args)

{

(obj as StudentCard).OnIsOpenChanged(args);

}

void OnIsOpenChanged(DependencyPropertyChangedEventArgs args) {

VisualStateManager.GoToState(this, IsOpen ? "Open" : "Normal", false);

}

}

Версия экземпляра вызывает метод VisualStateManager.GoToState. Несмотря на то что Visual State Manger чаще всего используется в связи с элементами управления и шаблоном элементов управления, он может также использоваться с производными от UserControl, такими как StudentCard. Вызов GoToState обеспечивает переключение состояния из кода.

В XAML-файле разметка Visual State Manager должна располагаться сразу после самого верхнего элемента дерева визуальных элементов. В StudentCard.xaml это элемент Border. Рассмотрим остальной код StudentCard.xaml (с некоторым повтором предыдущего фрагмента), демонстрирующий разметку Visual State Manager для TranslateTransform, заданного для самого элемента управления:

Проект Silverlight: StudentCardFile Файл: StudentCard.xaml (фрагмент)

<UserControl x:Class="StudentCardFile.StudentCard"

xmlns="http://schemas.microsoft.com/winfx/2 0 0 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2 0 0 6/xaml" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" Width="2 4 0" Height="2 4 0">

<UserControl.RenderTransform>

<TranslateTransform x:Name="translate" /> </UserControl.RenderTransform>

<Border BorderBrush="{StaticResource PhoneAccentBrush}" BorderThickness="1"

Background="{StaticResource PhoneChromeBrush}"

CornerRadius="12"

Padding="6 0">

<VisualStateManager.VisualStateGroups>

<VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Open"> <Storyboard>

<DoubleAnimation Storyboard.TargetName="translate" Storyboard.TargetProperty="X" To="22 0" Duration="0:0:1" /> </Storyboard> </VisualState>

<VisualState x:Name="Normal"> <Storyboard>

<DoubleAnimation Storyboard.TargetName="translate" Storyboard.TargetProperty="X" Duration="0:0:1" />

</Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>

</Border> </UserControl>

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

В ходе данной главы я попытался реализовать полностью в XAML несколько совершенно разных типов операций. Это не всегда возможно; очень часто код просто необходим, в частности, для обработки сенсорного ввода.

Но код не может заменить XAML, код только поддерживает разметку. Эти классы принимают форму конвертеров привязок и пользовательских панелей, на которые ссылается XAML-файл. В общем, создавайте код для XAML, а не вместо XAML, как это делают все хорошие разработчики на Silverlight и Windows Phone 7.

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии