Главная » Разработка для Windows Phone 7 » Изображения и захоронение Windows Phone 7

0

В 1890-х годах американский головоломщик Сэм Ллойд популяризировал головоломку, которая была изобретена пару десятилетий до этого и с тех пор известна как «пятнашки» (или по-французски «JeuDeTaquin»). В классическом виде этот пазл состоит из 15 фрагментов, каждый из которых обозначен цифрами от 1 до 15, случайным образом расположенных в сетке размером 4×4 (т.е. одна ячейка остается свободной). Цель – перемещая фрагменты, расставить их в правильном числовом порядке.

Эта головоломка была положена в основу первых игровых приложений, созданных для Apple Macintosh, где ее назвали PUZZLE. Вариант для Windows появилась в ранних версиях Microsoft Windows Software Development Kit (SDK) под именем MUZZLE. Это был единственный пример в SDK, написанный на Microsoft Pascal, а не на С.

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

Область содержимого приложения включает Grid под именем playGrid (Игровое поле) для размещения фрагментов и две кнопки:

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

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

StrokeStartLineCap = PenLineCap.Round, StrokeEndLineCap = PenLineCap.Round, StrokeLineJoin = PenLineJoin.Round,

Теперь растровое изображение выглядит абсолютно правильно:

<RowDefinition Height="*" />

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

<Grid.ColumnDefinitions>

<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions>

<Grid Name="playGrid"

Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Center" VerticalAlignment="Center" />

<Button Content="load"

Grid.Row="1" Grid.Column="0" Click="OnLoadClick" />

<Button Name="scrambleButton" Content="scramble" Grid.Row="2" Grid.Column="1" IsEnabled="False" Click="OnScrambleClick" />

</Grid>

XAML-файл также включает две кнопки в ApplicationBar, которые также подписаны «load» (загрузить) и «scramble» (перемешать), что кажется совершенно излишним:

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

<phone:PhoneApplicationPage.ApplicationBar> <shell:ApplicationBar IsVisible="False">

<shell:ApplicationBarIconButton x:Name="appbarLoadButton"

IconUri="/Images/appbar.folder.rest.png" Text="load"

Click="OnLoadClick" />

<shell:ApplicationBarIconButton x:Name="appbarScrambleButton"

IconUri="/Images/appbar.refresh.rest.png" Text="scramble" IsEnabled="False" Click="OnScrambleClick" />

</shell:ApplicationBar> </phone:PhoneApplicationPage.ApplicationBar>

У меня не получилось реализовать функциональность рандомизации из ApplicationBar, но я оставил этот элемент в разметке (и коде) и задал его свойству IsVisible значение false. Может быть, однажды ApplicationBar обретет более регулярное поведение.

Класс MainPage в коде начинается с объявления некоторых констант. Приложение настроено на размещение 4 фрагментов изображения по вертикали и 4 по горизонтали, но это можно изменить. Очевидно, что в портретном режиме будет лучше, если VERT_TILES (Фрагментов по вертикали) будет больше HORZ_TILES (Фрагментов по горизонтали). Другие поля предусмотрены для сохранения данных состояния в объекте PhoneApplicationService при захоронении и использования PhotoChooserTask для выбора фотографий.

Исключительно важное значение имеет массив tileImages (Фрагменты изображения). В нем хранятся все элементы Image фрагментов изображения. В любой момент времени один из членов этого массива будет иметь значение null, представляя свободную ячейку. Эта свободная ячейка также обозначается индексами emptyRow (Пустая строка) и emptyCol (Пустой столбец).

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

public partial class MainPage : PhoneApplicationPage {

const int HORZ_TILES = 4; const int VERT_TILES = 4; const int MARGIN = 2;

PhoneApplicationService appService = PhoneApplicationService.Current; PhotoChooserTask photoChooser = new PhotoChooserTask(); Random rand = new Random();

Image[,] tileImages = new Image[VERT TILES, HORZ TILES]; bool haveValidTileImages; int emptyRow, emptyCol; int scrambleCountdown;

public MainPage() {

InitializeComponent();

for (int col = 0; col < HORZ TILES; col++) {

ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = new GridLength(1, GridUnitType.Star); playGrid.ColumnDefinitions.Add(coldef);

}

for (int row = 0; row < VERT_TILES; row++) {

RowDefinition rowdef = new RowDefinition(); rowdef.Height = new GridLength(1, GridUnitType.Star); playGrid.RowDefinitions.Add(rowdef);

}

appbarScrambleButton = this.ApplicationBar.Buttons[1] as ApplicationBarIconButton;

photoChooser.Completed += OnPhotoChooserCompleted;

}

}

В конструкторе приложение инициализирует коллекции ColumnDefinition и RowDefinition объекта Grid, в котором располагаются фрагменты, и (как обычно) задается обработчик события Completed, формируемого PhotoChooserTask.

Когда пользователь щелкает кнопку «load», приложение определяет размеры каждого фрагмента на основании заданных ширины и высоты области содержимого, количества фрагментов и размера поля. Полученные значения сохраняются в свойствах PixelWidth и PixelHeight объекта PhotoChooserTask:

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

void OnLoadClick(object sender, EventArgs args) {

int tileSize = (int)Math.Min(ContentPanel.ActualWidth / HORZ_TILES,

ContentPanel.ActualHeight / VERT TILES)

- 2 * MARGIN;

photoChooser.PixelWidth = tileSize * HORZ TILES; photoChooser.PixelHeight = tileSize * VERT_TILES; photoChooser.Show();

По завершении PhotoChooserTask обработчик события Completed разделяет растровое изображение на небольшие квадратные фрагменты и создает элемент Image для каждого из них. В рассматриваемом ранее в данной главе приложении SubdivideBitmap была продемонстрирована реализация разделения растрового изображения на квадратные фрагменты с использованием метода Render объекта WriteableBitmap. В данном приложении сделано наоборот: объекты WriteableBitmap создаются размером с фрагмент изображения, и затем в их массивы Pixels копируются соответствующие пикселы полного растрового изображения:

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

void OnPhotoChooserCompleted(object sender, PhotoResult args) {

if (args.Error == null && args.ChosenPhoto != null) {

BitmapImage bitmapImage = new BitmapImage(); bitmapImage.SetSource(args.ChosenPhoto);

WriteableBitmap writeableBitmap = new WriteableBitmap(bitmapImage); int tileSize = writeableBitmap.PixelWidth / HORZ TILES;

emptyCol = HORZ_TILES           – 1;

emptyRow = VERT TILES           – 1;

for (int row = 0; row < VERT_TILES; row++)

for (int col = 0;               col < HORZ TILES; col++)

if (row != emptyRow || col != emptyCol) {

WriteableBitmap tile = new WriteableBitmap(tileSize, tileSize);

for (int y = 0; y < tileSize; y++)

for (int x =                            0; x < tileSize; x++) {

int yBit                                    = row * tileSize + y;

int xBit                                    = col * tileSize + x;

tile.Pixels[y * tileSize + x] = writeableBitmap.Pixels[yBit *

writeableBitmap.PixelWidth + xBit];

}

GenerateImageTile(tile, row, col);

}

haveValidTileImages = true; scrambleButton.IsEnabled = true; appbarScrambleButton.IsEnabled = true;

}

}

void GenerateImageTile(BitmapSource tile, int row, int col) {

Image img = new Image(); img.Stretch = Stretch.None; img.Source = tile;

img.Margin = new Thickness(MARGIN); tileImages[row, col] = img;

Grid.SetRow(img, row); Grid.SetColumn(img, col); playGrid.Children.Add(img);

На этом этапе фрагменты еще расположены в правильном порядке, но уже есть возможность перемещать их по полю. Когда мы обратимся к реализации перемещений фрагментов, обнаружится, чтобы алгоритмически это намного проще, чем можно было себе представить. Рассмотрим пустой квадрат. Какие фрагменты могут быть перемещены в этот квадрат? Только те, которые находятся слева, сверху, справа и снизу пустого квадрата, и эти фрагменты могут перемещаться только в одном направлении. Это означает, что пользовательский интерфейс должен реагировать только на касания и не обращать внимания на перемещения других фрагментов.

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

Рассмотрим полную реализацию логики перемещения: Проект Silverlight: JeuDeTaquin Файл: MainPage.xaml.cs (фрагмент)

protected override void OnManipulationStarted(ManipulationStartedEventArgs args) {

if (args.OriginalSource is Image) {

Image img = args.OriginalSource as Image;

MoveTile(img);

args.Complete();

args.Handled = true;

}

base.OnManipulationStarted(args);

}

void MoveTile(Image img) {

За фактическое создание элементов Image и их добавление в Grid отвечает метод GenerateImageTile (Создать фрагмент изображения). Этот метод также сохраняет элементы Image в массив tileImages.

int touchedRow = -1, touchedCol = -1; for (int y = 0; y < VERT_TILES; y++)

for (int x = 0; x < HORZ_TILES; x++)

if (tileImages[y, x] == img) {

touchedRow = y; touchedCol = x;

}

if (touchedRow == emptyRow) {

int sign = Math.Sign(touchedCol – emptyCol);

for (int x = emptyCol; x != touchedCol; x += sign) {

tileImages[touchedRow, x] = tileImages[touchedRow, x + sign]; Grid.SetColumn(tileImages[touchedRow, x], x);

}

tileImages[touchedRow, touchedCol] = null; emptyCol = touchedCol;

}

else if (touchedCol == emptyCol) {

int sign = Math.Sign(touchedRow – emptyRow);

for (int y = emptyRow; y != touchedRow; y += sign) {

tileImages[y, touchedCol] = tileImages[y + sign, touchedCol]; Grid.SetRow(tileImages[y, touchedCol], y);

}

tileImages[touchedRow, touchedCol] = null; emptyRow = touchedRow;

}

}

Метод MoveTile (Переместить фрагмент) сначала определяет строку и столбец фрагмента, которого коснулся пользователь. Чтобы перемещение состоялось, эта строка должна быть строкой или столбцом с пустым квадратом (она не может быть одновременно и столбцом, и строкой с пустым квадратом). Довольно универсальные циклы for обеспечивают перемещение нескольких фрагментов вверх, вниз, влево или вправо.

Логика рандомизации является дополнением к логике перемещения. По нажатию кнопки «scramble» приложение подключает обработчик события CompositionTarget.Rendering:

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

void OnScrambleClick(object sender, EventArgs args) {

scrambleCountdown = 10 * VERT TILES * HORZ TILES; scrambleButton.IsEnabled = false; appbarScrambleButton.IsEnabled = false;

CompositionTarget.Rendering += OnCompositionTargetRendering;

}

void OnCompositionTargetRendering(object sender, EventArgs args) {

MoveTile(tileImages[emptyRow, rand.Next(HORZ_TILES)]); MoveTile(tileImages[rand.Next(VERT TILES), emptyCol]);

if (–scrambleCountdown == 0) {

CompositionTarget.Rendering -= OnCompositionTargetRendering; scrambleButton.IsEnabled = true; appbarScrambleButton.IsEnabled = true;

Данное приложение также обрабатывает захоронение, т.е. оно полностью сохраняет состояние игры при переходе пользователя на другую страницу и восстанавливает это состояние при повторной активации игры.

Мне удалось реализовать сохранение состояние игры с помощью всего нескольких полей. Поле haveValidTileImages (Имеются действительные фрагменты) получает значение true, если массив tileImages содержит действительные элементы Image; в противном случае с игрой ничего не происходит. Поля emptyRow и emptyCol также имеют больше значение. Но важнее всего, конечно же, растровые изображения, которые образуют фрагменты. Вместо того чтобы сохранять весь массив Pixels каждого WriteableBitmap целиком, я решил сэкономить память, сохраняя эти изображения в формате JPEG со сжатием:

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

protected override void OnNavigatedFrom(NavigationEventArgs args) {

appService.State["haveValidTileImages"] = haveValidTileImages;

if (haveValidTileImages) {

appService.State["emptyRow"] = emptyRow; appService.State["emptyCol"] = emptyCol;

for (int row = 0; row < VERT TILES; row++)

for (int col = 0; col < HORZ_TILES; col++)

if (col != emptyCol || row != emptyRow) {

WriteableBitmap tile = tileImages[row, colj.Source as

WriteableBitmap;

MemoryStream stream = new MemoryStream();

tile.SaveJpeg(stream, tile.PixelWidth, tile.PixelHeight, 0, 75); appService.State[TileKey(row, col)] = stream.GetBuffer();

}

}

Обработчик события вызывает MoveTile дважды: один раз для перемещения фрагмента из строки с пустым квадратом и второй раз для перемещения фрагмента из столбца с пустым квадратом.

base.OnNavigatedFrom(args);

string TileKey(int row, int col) {

return String.Format("tile {0} {1}", row, col);

}

Для каждого элемента Image в массиве tileImages приложение получает соответствующий WriteableBitmap и создает новый MemoryStream. Метод расширения SaveJpeg позволяет сохранять WriteableBitmap в формате JPEG в этот поток. Метод GetBuffer (Получить буфер) объекта MemoryStream получает массив byte, который просто сохраняется с остальными данными состояния.

Когда приложение возвращается из состояния захоронения, этот процесс выполняется в обратной последовательности:

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

protected override void OnNavigatedTo(NavigationEventArgs args) {

object objHaveValidTileImages;

if (appService.State.TryGetValue("haveValidTileImages", out objHaveValidTileImages) &&

(bool)objHaveValidTileImages)

{

emptyRow = (int)appService.State["emptyRow"]; emptyCol = (int)appService.State["emptyCol"];

for (int row = 0; row < VERT TILES; row++)

for (int col = 0; col < HORZ_TILES; col++)

if (col != emptyCol || row != emptyRow) {

byte[] buffer = (byte[])appService.State[TileKey(row, col)]; MemoryStream stream = new MemoryStream(buffer); BitmapImage bitmapImage = new BitmapImage(); bitmapImage.SetSource(stream);

WriteableBitmap tile = new WriteableBitmap(bitmapImage); GenerateImageTile(tile, row, col);

}

haveValidTileImages = true; appbarScrambleButton.IsEnabled = true;

}

base.OnNavigatedTo(args);

}

Этот метод читает буфер byte и преобразовывает его в MemoryStream, из которого создается BitmapImage и затем WriteableBitmap. После этого с помощью метода GenerateImageTile все элементы Image создаются и добавляются в Grid.

Важно помнить, что этот массив byte, используемый для сохранения и восстановления растрового изображения, очень отличается от массива int, доступного из свойства Pixels объекта WriteableBitmap. В массиве Pixels хранятся значения каждого пиксела растрового изображения, а массив byte – это растровое изображение в формате JPEG со сжатием, включающий все данные и заголовки файла JPEG и прочие подобные данные.

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

По теме:

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