Главная » Разработка для Windows Phone 7 » Холст PhingerPaint

0

Компоненты, создаваемые PhingerPaint, а также некоторые другие необходимые данные хранятся как поля:

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент, демонстрирующий поля)

public class Game1 : Microsoft.Xna.Framework.Game {

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Texture2D canvas; Vector2 canvasSize; Vector2 canvasPosition; uint[] pixels;

List<float> xCollection = new List<float>();

Button clearButton, saveButton; string filename;

List<ColorBlock> colorBlocks = new List<ColorBlock>(); Color drawingColor = Color.Blue; int? touchIdToIgnore;

}

List сохраняет 12 компонентов ColorBlock; drawingColor (Цвет рисования) – выбранный в настоящий момент цвет. Роль главного холста исполняет, конечно же, объект Texture2D под именем canvas, значения пикселов этой текстуры хранятся в массиве pixels. Объект xCollection многократно используется при работе с классом RoundCappedLine, который был рассмотрен в главе 21.

В конструкторе для заднего буфера устанавливается портретный режим отображения, но высота задается равной 768, а не 800 пикселам. Таким образом, остается достаточно места для строки состояния, и задний буфер может отображаться в полном размере:

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

public Game1() {

graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content";

// Частота кадров по умолчанию для устройств Windows Phone – 30 кадров/с TargetElapsedTime = TimeSpan.FromTicks(333333);

// Задаем портретный режим, но оставляем достаточно места для строки состояния graphics.PreferredBackBufferWidth = 480; graphics.PreferredBackBufferHeight = 768;

}

Перегруженный метод Initialize отвечает за создание компонентов Button и ColorBlack, частично их инициализирует и добавляет в коллекцию Components класса Game. Это гарантирует вызов их собственных методов Initialize, LoadContent, Update и Draw.

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

protected override void Initialize() {

// Создаем компоненты Button clearButton = new Button(this, "clear"); clearButton.Click += OnClearButtonClick; this.Components.Add(clearButton);

saveButton = new Button(this, "save"); saveButton.Click += OnSaveButtonClick; this.Components.Add(saveButton);

// Создаем компоненты ColorBlock

Color[] colors = { Color.Red, Color.Green, Color.Blue,

Color.Cyan, Color.Magenta, Color.Yellow,

Color.Black, new Color(0.2f, 0.2f, 0.2f), new Color(0.4f, 0.4f, 0.4f), new Color (0.6f, 0.6f, 0.6f), new Color(0.8f, 0.8f, 0.8f), Color.White };

foreach (Color clr in colors) {

ColorBlock colorBlock = new ColorBlock(this); colorBlock.Color = clr; colorBlocks.Add(colorBlock); this.Components.Add(colorBlock);

}

base.Initialize();

}

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

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

protected override void LoadContent() {

spriteBatch = new SpriteBatch(GraphicsDevice);

Rectangle clientBounds = this.GraphicsDevice.Viewport.Bounds; SpriteFont segoe14 = this.Content.Load<SpriteFont>("Segoe14");

// Настраиваем компоненты Button clearButton.SpriteFont = segoe14; saveButton.SpriteFont = segoe14;

Vector2 textSize = segoe14.MeasureString(clearButton.Text); int buttonWidth = (int)(2 * textSize.X); int buttonHeight = (int)(1.5 * textSize.Y);

clearButton.Destination =

new Rectangle(clientBounds.Left + 20,

clientBounds.Bottom – 2 – buttonHeight, buttonWidth, buttonHeight);

saveButton.Destination =

new Rectangle(clientBounds.Right – 20 – buttonWidth, clientBounds.Bottom – 2 – buttonHeight, buttonWidth, buttonHeight);

int colorBlockSize = clientBounds.Width / (colorBlocks.Count / 2) – 2; int xColorBlock = 2; int yColorBlock = 2;

foreach (ColorBlock colorBlock in colorBlocks) {

colorBlock.Destination = new Rectangle(xColorBlock, yColorBlock,

colorBlockSize, colorBlockSize);

xColorBlock += colorBlockSize + 2;

if (xColorBlock + colorBlockSize > clientBounds.Width) {

xColorBlock = 2;

yColorBlock += colorBlockSize + 2;

canvasPosition = new Vector2(0, 2 * colorBlockSize + 6); canvasSize = new Vector2(clientBounds.Width,

clientBounds.Height – canvasPosition.Y

- buttonHeight – 4);

}

Метод LoadContent завершается вычислением местоположения и размера Texture2D, используемого в качестве холста. Но не делает последнего шага LoadContent к фактическому созданию этого Texture2D, потому что за методом LoadContent может последовать вызов перегруженного OnActivated, который свидетельствует о том, что приложение либо запускается впервые, либо возвращается из состояния захоронения.

Для приложения PhingerPaint важно реализовать захоронение, потому что пользователи не любят, когда их творческие порывы исчезают с экрана. Поэтому перегруженный OnDeactivated сохраняет изображение в PhoneApplicationService в формате PNG, и перегруженный OnActivated извлекает его оттуда. Я выбрал формат PNG, потому что это формат сжатия без потерь, и мне показалось, что изображение должно восстанавливаться точно в его исходное состояние.

Чтобы немного упростить процесс сохранения и загрузки объекта Texture2D, я использовал методы класса Texture2DExtensions из библиотеки Petzold.Phone.Xna, которые были рассмотрены в предыдущей главе. Метод OnActivated вызывает LoadFromPhoneService (Загрузить из сервиса приложения для телефона) для получения сохраненного Texture2D. И только если этот объект недоступен, создается и очищается новый Texture2D.

Для работы с классом PhoneApplicationService необходимо указать ссылки на сборки System.Windows и Microsoft.Phone, а также включить директиву using для Microosft.Phone.Shell.

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

protected override void OnActivated(object sender, EventArgs args) {

// Восстанавливаем после захоронения bool newlyCreated = false;

canvas = Texture2DExtensions.LoadFromPhoneServiceState(this.GraphicsDevice,

"canvas");

if (canvas == null) {

// В противном случае создаем новый Texture2D

canvas = new Texture2D(this.GraphicsDevice, (int)canvasSize.X,

(int)canvasSize.Y);

newlyCreated = true;

}

// Создаем массив pixels

pixels = new uint[canvas.Width * canvas.Height]; canvas.GetData<uint>(pixels);

if (newlyCreated)

ClearPixelArray();

// Из State получаем цвет рисунка, инициализируем выбранный ColorBlock if (PhoneApplicationService.Current.State.ContainsKey("color"))

drawingColor = (Color)PhoneApplicationService.Current.State["color"];

foreach (ColorBlock colorBlock in colorBlocks)

colorBlock.IsSelected = colorBlock.Color == drawingColor;

base.OnActivated(sender, args);

Перегруженный метод OnDeactivated сохраняет Texture2D с помощью метода расширения SaveToPhoneServiceState (Сохранить в состояние сервиса приложения для телефона):

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

protected override void OnDeactivated(object sender, EventArgs args) {

PhoneApplicationService.Current.State["color"] = drawingColor; canvas.SaveToPhoneServiceState("canvas"); base.OnDeactivated(sender, args);

}

Если происходит запуск приложения, OnActivated вызывает метод ClearPixelArray (Очистить массив Pixel):

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

void ClearPixelArray() {

for (int y = 0; y < canvas.Height; y++)

for (int x = 0; x < canvas.Width; x++) {

pixels[x + canvas.Width * y] = Color.GhostWhite.PackedValue;

}

canvas.SetData<uint>(pixels);

}

void OnClearButtonClick(object sender, EventArgs e) {

ClearPixelArray();

}

Вы заметите, что обработчик событий Click для «очистки» Button тоже вызывает этот метод. Как помните, класс Button формирует событие Click на основании сенсорного ввода, и Button получает сенсорный ввод, когда родительский класс Game вызывает метод ProcessTouch из собственного перегруженного Update. Это означает, что данный метод OnClearButtonClick (При щелчку кнопки очистить) фактически вызывается при вызове перегруженного Update этого класса.

Когда пользователь нажимает кнопку с надписью «save», приложение должно вывести на экран некоторое диалоговое окно для ввода имени файла. Приложение на XNA может принимать ввод с клавиатуры двумя способами: применяя низкоуровневый подход с использованием класса Keyboard (Клавиатура), и высокоуровневый подход посредством вызова метода Guide.BeginShowKeyboardInput (Начать отображение ввода с клавиатуры) из пространства имен Microsoft.Xna.Framework.GamerServices (Игровые сервисы). Я выбрал высокоуровневый подход. Метод Guide.BeginShowKeyboardInput требует некоторые исходные данные и функцию обратного вызова, что позволяет ему создавать уникальное имя файла на основании текущих даты и времени:

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

void OnSaveButtonClick(object sender, EventArgs e) {

DateTime dt = DateTime.Now; filename =

String.Format("PhingerPaint-{0:D2}-{1:D2}-{2:D2}-{3:D2}-{4:D2}-{5:D2}",

dt.Year % 100, dt.Month, dt.Day, dt.Hour, dt.Minute,

dt.Second);

Guide.BeginShowKeyboardInput(PlayerIndex.One, "phinger paint save file",

"enter filename:", filename, KeyboardCallback,

null); }

Вызов Guide.BeginShowKeyboardInput приводит к вызову метода OnDeactivated, после чего на экран выводится следующее:

В этом окне настроены могут быть только текстовые строки заголовков и исходный текст в строке ввода. Данный экран выглядит в портретном режиме намного лучше, чем в альбомном, поскольку в альбомном режиме все заголовки, строка ввода и экранная клавиатура меняют ориентацию, а две кнопки остаются ориентированными по-старому. В итоге окно выглядит очень странно. Одного взгляда на него будет достаточно, чтобы больше никогда не вызывать Guide.BeginShowKeyboardInput из приложения, отображающегося в альбомном режиме!

По нажатию кнопки «OK» или «Cancel» приложение повторно активируется, и в PhingerPaint вызывается функция обратного вызова:

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

void KeyboardCallback(IAsyncResult result) {

filename = Guide.EndShowKeyboardInput(result);

}

Приложение должно предполагать, что эта функция обратного вызова вызывается асинхронно (как подразумевает аргумент), поэтому здесь не следует делать ничего, кроме вызова Guide.EndShowKeyboardInput (Закончить отображение ввода с клавиатуры) и сохранения возвращенного значения в поле. Если пользователь нажимает кнопку «OK», возвращаемым значением является окончательный текст, введенный в строку ввода. Если

пользователь нажимает «Cancel» или кнопку Back, Guide.EndShowKeyboardInput возвращает null.

Самым подходящим местом обработать это возвращенное значение некоторым образом является следующий вызов перегруженного Update:

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

protected override void Update(GameTime gameTime) {

if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit ();

// Если диалоговое окно Save File возвращает значение, сохраняем изображение

if (!String.IsNullOrEmpty(filename)) {

canvas.SaveToPhotoLibrary(filename); filename = null;

}

}

Обратите внимание, в коде выполняется проверка того, что поле filename (имя файла) не пустое и его значение не null, но в конце этому полю опять присваивается значение null, что гарантирует однократное сохранение его значения.

SaveToPhotoLibrary (Сохранить в библиотеку фотографий), на самом деле, не метод класса Texture2D! Это еще один метод расширения класса Texture2DExtensions из библиотеки Petzold.Phone.Xna.

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

public static void SaveToPhotoLibrary(this Texture2D texture, string filename) {

MemoryStream memoryStream = new MemoryStream();

texture.SaveAsJpeg(memoryStream, texture.Width, texture.Height); memoryStream.Position = 0;

MediaLibrary mediaLibrary = new MediaLibrary(); mediaLibrary.SavePicture(filename, memoryStream); memoryStream.Close();

}

Это стандартный код для сохранения Texture2D в альбом Saved Pictures библиотеки фотографий телефона. Несмотря на то что PhingerPaint при захоронении сохраняет изображение в формате PNG, изображения, сохраняемые в библиотеке фотографий, должны быть в формате JPEG. Метод SaveAsJpeg сохраняет все изображение в MemoryStream. После этого положение курсора в MemoryStream сбрасывается, и он вместе с именем файла передается в метод SavePicture объекта MediaLibrary.

Если данное приложение будет развернуто на телефоне, при запуске настольного ПО Zune для обмена данными между Visual Studio и телефоном, этот код сформирует исключение. Zune требует эксклюзивного доступа к библиотеке мультимедиа телефона. Вам придется завершить выполнение приложения Zune и вместо него запустить инструмент WPDTPTConnect: WPDTPTConnect32.exe или WPDTPTConnect64.exe в зависимости от того 32- или 64-разрядная операционная система Windows используется.

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

холсте несколькими пальцами. Button в основном обрабатывает собственный сенсорный ввод с помощью интерфейса IProcessTouch, но ColorBlock обрабатывается иначе. Метод Update в самом классе игры обрабатывает компоненты ColorBlock и холст Texture2D.

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

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

protected override void Update(GameTime gameTime) {

TouchCollection touches = TouchPanel.GetState();

foreach (TouchLocation touch in touches) {

// Игнорируем дальнейшие действия касания ColorBlock if (touchIdToIgnore.HasValue && touch.Id == touchIdToIgnore.Value) continue;

// Обеспечиваем компонентам Button первоочередное право на касание bool touchHandled = false;

foreach (GameComponent component in this.Components) if (component is IProcessTouch &&

(component as IProcessTouch).ProcessTouch(touch))

{

touchHandled = true; break;

}

if (touchHandled) continue;

// Проверяем на наличие касания ColorBlock

if (touch.State == TouchLocationState.Pressed) {

Vector2 position = touch.Position; ColorBlock newSelectedColorBlock = null;

foreach (ColorBlock colorBlock in colorBlocks) {

Rectangle rect = colorBlock.Destination;

if (position.X >= rect.Left && position.X < rect.Right && position.Y >= rect.Top && position.Y < rect.Bottom)

{

drawingColor = colorBlock.Color; newSelectedColorBlock = colorBlock;

}

}

if (newSelectedColorBlock != null) {

foreach (ColorBlock colorBlock in colorBlocks)

colorBlock.IsSelected = colorBlock == newSelectedColorBlock;

touchIdToIgnore = touch.Id;

}

else {

touchIdToIgnore = null;

Дальнейшая обработка касания связана непосредственно с рисованием, и для этого требуется лишь проверять значение свойства State на равенство TouchLocationState.Moved. Это состояние позволяет вызывать метод TryGetPreviousLocation, после чего в конструктор класса RoundCappedLine из Petzold.Phone.Xna могут быть переданы две точки. Таким образом, мы получаем диапазоны пикселов для закрашивания в каждом небольшом фрагменте общего мазка кисти:

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

protected override void Update(GameTime gameTime) {

// Обрабатываем сенсорный ввод bool canvasNeedsUpdate = false;

TouchCollection touches = TouchPanel.GetState();

foreach (TouchLocation touch in touches) {

// Проверяем на наличие движения рисования

else if (touch.State == TouchLocationState.Moved) {

TouchLocation prevTouchLocation;

touch.TryGetPreviousLocation(out prevTouchLocation);

Vector2 point1 = prevTouchLocation.Position – canvasPosition; Vector2 point2 = touch.Position – canvasPosition;

// Безусловно, надеемся на возвращение touchLocation.Pressure! float radius = 12;

RoundCappedLine line = new RoundCappedLine(point1, point2, radius);

int yMin = (int)(Math.Min(point1.Y, point2.Y) – radius); int yMax = (int)(Math.Max(point1.Y, point2.Y) + radius);

yMin = Math.Max(0, Math.Min(canvas.Height, yMin)); yMax = Math.Max(0, Math.Min(canvas.Height, yMax));

for (int y = yMin; y < yMax; y++) {

xCollection.Clear(); line.GetAllX(y, xCollection);

if (xCollection.Count == 2) {

int xMin = (int)(Math.Min(xCollection[0], xCollection[1]) +

0.5f);

int xMax = (int)(Math.Max(xCollection[0], xCollection[1]) +

0.5f);

xMin = Math.Max(0, Math.Min(canvas.Width, xMin)); xMax = Math.Max(0, Math.Min(canvas.Width, xMax));

for (int x = xMin; x < xMax; x++) {

pixels[y * canvas.Width + x] = drawingColor.PackedValue;

}

canvasNeedsUpdate = true;

if (canvasNeedsUpdate)

canvas.SetData<uint>(pixels);

base.Update(gameTime);

}

Всегда радует, когда все подготовлено так, что перегруженному методу Draw практически ничего не надо делать. Компоненты ColorBlock и Button отрисовывают себя самостоятельно, поэтому метод Draw здесь лишь формирует визуальное представление canvas:

Проект XNA: PhingerPaint Файл: Game1.cs (фрагмент)

protected override void Draw(GameTime gameTime) {

this.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin();

spriteBatch.Draw(canvas, canvasPosition, Color.White); spriteBatch.End();

base.Draw(gameTime);

}

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

По теме:

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