Главная » Разработка для Windows Phone 7 » Небольшой обзор SpinPaint

0

Приложение SpinPaint имеет необычную историю появления. Первую его версию я написал однажды утром, будучи слушателем двухдневных курсов по разработке ПО для Microsoft Surface (это такие компьютеры размером с журнальный столик, разработанные специально для общественных мест). Та версия была написана для Windows Presentation Foundation и могла использоваться одновременно несколькими пользователями, сидящими вокруг устройства.

Сначала я хотел представить Silverlight-версию SpinPaint в главе 14 данной книги для демонстрации WriteableBitmap, но производительность приложения была просто ужасной. Первую XNA-версию для Zune HD я написал до того, как у меня появился Windows Phone, и уже ту версию впоследствии я трансформировал в приложение, которое будет рассмотрено в этой главе.

SpinPaint начинает выполнение с отображения белого диска, который вращается с частотой 12 оборотов в минуту. Также можно заметить, что цвет названия приложения циклически меняется каждые 10 секунд:

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

Продолжая рисовать, можно получить довольно фантазийные изображения:

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

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

Код SpinPaint

Приложение SpinPaint должно обрабатывать касание очень особым способом. Не только пальцы перемещаются по экрану, но и диск вращается под пальцами, поэтому даже если палец остается неподвижным, он продолжает рисовать. В отличие от PhingerPaint это приложение должно отслеживать каждое касание. Поэтому в нем определен Dictionary с целочисленным ключом (им является идентификатор касания), в котором хранятся объекты типа TouchInfo. TouchInfo – это небольшой внутренний класс Game1, сохраняющий два местоположения касания:

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

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

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

// Поля, участвующие в описании текстуры вращающегося диска

Texture2D diskTexture;

uint[] pixels;

Vector2 displayCenter;

Vector2 textureCenter;

int radius;

Color currentColor;

// Данные касания и поля, используемые при отрисовке линий

class TouchInfo {

public Vector2 PreviousPosition;

public Vector2 CurrentPosition;

Dictionary<int, TouchInfo> touchDictionary = new Dictionary<int, TouchInfo>(); float currentAngle; float previousAngle;

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

// Кнопки и заголовки

Button clearButton, saveButton;

SpriteFont segoe14;

SpriteFont segoe48;

string titleText = "spin paint";

Vector2 titlePosition;

string filename;

}

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

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

public Game1() {

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

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

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

}

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

Два компонента Button создаются в ходе выполнения метода Initialize. Для них задаются свойства Text и обработчики события Click, и это пока что все:

Проект XNA: SpinPaint Файл: 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);

base.Initialize();

}

Обратите внимание на крайне важный шаг по добавлению компонентов в коллекцию Components класса Game. Если не сделать этого, компоненты вообще не появятся на экране, что, вероятно, приведет вас в замешательство. (Я говорю, исходя из собственного опыта.)

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

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

protected override void LoadContent() {

spriteBatch = new SpriteBatch(GraphicsDevice); // Получаем данные экрана

Rectangle clientBounds = this.GraphicsDevice.Viewport.Bounds; displayCenter = new Vector2(clientBounds.Center.X, clientBounds.Center.Y);

// Загружаем шрифты и вычисляем местоположение заголовка segoe14 = this.Content.Load<SpriteFont>("Segoe14"); segoe48 = this.Content.Load<SpriteFont>("Segoe4 8"); titlePosition = new Vector2((int)((clientBounds.Width -

segoe4 8.MeasureString(titleText).X) / 2), 20);

// Задаем шрифты и местоположение кнопок 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 – 20 – buttonHeight, buttonWidth, buttonHeight); saveButton.Destination =

new Rectangle(clientBounds.Right – 20 – buttonWidth,

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

}

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

Как и в PhingerPaint, перегруженный OnDeactivated сохраняет изображение в формате PNG, и перегруженный OnActivated восстанавливает его. Оба метода вызывают методы класса TextureExtensions (Расширения текстуры) библиотеки Petzold.Phone.Xna. Если извлекать нечего, значит, выполняется запуск приложения, и необходимо создать новый Texture2D.

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

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

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

diskTexture = Texture2DExtensions.LoadFromPhoneServiceState(this.GraphicsDevice,

"disk");

// Или создаем новый Texture2D

if (diskTexture == null) {

Rectangle clientBounds = this.GraphicsDevice.Viewport.Bounds; int textureDimension = Math.Min(clientBounds.Width, clientBounds.Height); diskTexture = new Texture2D(this.GraphicsDevice, textureDimension,

textureDimension);

newlyCreated = true;

pixels = new uint[diskTexture.Width * diskTexture.Height]; radius = diskTexture.Width / 2; textureCenter = new Vector2(radius, radius);

if (newlyCreated) {

ClearPixelArray();

}

else {

diskTexture.GetData<uint>(pixels);

}

base.OnActivated(sender, args);

}

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

diskTexture.SaveToPhoneServiceState("disk"); base.OnDeactivated(sender, args);

}

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

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

void ClearPixelArray() {

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

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

if (IsWithinCircle(x, y)) {

Color clr = Color.White;

// Линии, разделяющие диск на четверти

if (x == diskTexture.Width / 2 || y == diskTexture.Height / 2) clr = Color.LightGray;

pixels[y * diskTexture.Width + x] = clr.PackedValue;

}

diskTexture.SetData<uint>(pixels);

}

bool IsWithinCircle(int x, int y) {

x -= diskTexture.Width / 2; y -= diskTexture.Height / 2;

return x * x + y * y < radius * radius;

}

void OnClearButtonClick(object sender, EventArgs args) {

ClearPixelArray();

}

Метод ClearPixelArray также вызывается, когда пользователь нажимает кнопку «clear».

Логика обработки кнопки «save» практически идентична применяемой в приложении PhingerPaint:

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

void OnSaveButtonClick(object sender, EventArgs args) {

DateTime dt = DateTime.Now; string filename =

String.Format("spinpaint-{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, "spin paint save file",

"enter filename:", filename, KeyboardCallback,

null); }

void KeyboardCallback(IAsyncResult result) {

filename = Guide.EndShowKeyboardInput(result);

}

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

Проект XNA: SpinPaint Файл: 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)) {

diskTexture.SaveToPhotoLibrary(filename); filename = null;

}

}

Процесс рисования

Оставшаяся часть перегруженного Update выполняет на самом деле очень сложную работу: рисование на диске на основании сенсорного ввода и вращения диска.

Обработка Update начинается с вычисления текущего угла вращающегося диска и текущего цвета рисования:

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

protected override void Update(GameTime gameTime) {

// Диск поворачивается каждые 5 секунд

double seconds = gameTime.TotalGameTime.TotalSeconds;

currentAngle = (float)(2 * Math.PI * seconds / 5);

// Цвета меняются каждые 10 секунд

float fraction = (float)(6 * (seconds % 10) / 10);

if (fraction < 1)

currentColor = new Color(1, fraction, 0); else if (fraction < 2)

currentColor = new Color(2 – fraction, 1, 0); else if (fraction < 3)

currentColor = new Color(0, 1, fraction – 2); else if (fraction < 4)

currentColor = new Color(0, 4 – fraction, 1); else if (fraction < 5)

currentColor = new Color(fraction – 4, 0, 1);

else

currentColor = new Color(1, 0, 6 – fraction);

// Сначала предполагаем, что палец неподвижен foreach (TouchInfo touchInfo in touchDictionary.Values)

touchInfo.CurrentPosition = touchInfo.PreviousPosition;

}

Для любого касания экрана приложение сохраняет объект TouchInfo с полями CurrentPosition (Текущее местоположение) и PreviousPosition (Предыдущее местоположение). Эти координаты всегда отсчитываются относительно холста Texture2D без учета вращения. Поэтому данный раздел перегруженного метода Update завершается присвоением полю CurrentPosition значения поля PreviousPosition на основании предположения, что пальцы остаются неподвижными.

После этого Update готов заняться точкой касания, сначала вызывая метод ProcessTouch каждой из кнопок и затем находя новые координаты текущих касаний или новые касания. За преобразование координат касания относительно экрана в координаты относительно Texture2D отвечает небольшой метод TranslateToTexture (Перенести на текстуру), который показан здесь после Update.

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

protected override void Update(GameTime gameTime) {

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

TouchCollection touches = TouchPanel.GetState();

foreach (TouchLocation touch in touches) {

// Предоставляем компонентам 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;

// Задаем элементы TouchInfo на основании данных касания int id = touch.Id;

switch (touch.State) {

case TouchLocationState.Pressed:

if (ItouchDictionary.ContainsKey(id))

touchDictionary.Add(id, new TouchInfo());

touchDictionary[id].PreviousPosition = TranslateToTexture(touch.Position);

touchDictionary[id].CurrentPosition = TranslateToTexture(touch.Position); break;

case TouchLocationState.Moved:

if (touchDictionary.ContainsKey(id))

touchDictionary[id].CurrentPosition =

TranslateToTexture(touch.Position);

break;

case TouchLocationState.Released:

if (touchDictionary.ContainsKey(id))

touchDictionary.Remove(id); break;

}

}

}

Vector2 TranslateToTexture(Vector2 point) {

return point – displayCenter + textureCenter;

}

Чтобы учесть вращение диска, предусмотрены поля previousAngle (Предыдущий угол) и currentAngle (Текущий угол). Теперь Update на основании значений этих полей вычисляет две матрицы: previousRotation (Предыдущий разворот) и currentRotation (Текущий разворот). Обратите внимание, что эти матрицы вычисляются посредством вызовов Matrix.CreateRotationZ, но при этом умножаются на трансформации переноса, что обеспечивает расчет вращения относительно центра Texture2D:

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

protected override void Update(GameTime gameTime) {

// Вычисляем трансформации для вращения

Matrix translate1 = Matrix.CreateTranslation(-textureCenter.X, -textureCenter.Y,

0);

Matrix translate2 = Matrix.CreateTranslation(textureCenter.X, textureCenter.Y,

0);

Matrix previousRotation = translate1 *

Matrix.CreateRotationZ(-previousAngle) * translate2;

Matrix currentRotation = translate1 *

Matrix.CreateRotationZ(-currentAngle) * translate2;

}

Когда трансформации вычислены, они могут применяться к полям PreviousPosition и CurrentPosition объекта TouchInfo посредством статического метода Vector2.Transform и затем передаваться в RoundCappedLine для получения данных, необходимых для отрисовки линии на Texture2D:

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

protected override void Update(GameTime gameTime) {

bool textureNeedsUpdate = false;

foreach (TouchInfo touchInfo in touchDictionary.Values) {

// Выполняем рисование из предыдущей в текущую точку Vector2 point1 = Vector2.Transform(touchInfo.PreviousPosition, previousRotation);

Vector2 point2 = Vector2.Transform(touchInfo.CurrentPosition, currentRotation);

float radius = 6;

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(diskTexture.Height, yMin)); yMax = Math.Max(0, Math.Min(diskTexture.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(diskTexture.Width, xMin)); xMax = Math.Max(0, Math.Min(diskTexture.Width, xMax));

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

if (IsWithinCircle(x, y)) {

// Отрисовываем точку во всех четвертях int xFlip = diskTexture.Width – x; int yFlip = diskTexture.Height – y;

pixels[y * diskTexture.Width + x] = currentColor.PackedValue;

pixels[y * diskTexture.Width + xFlip] = currentColor.PackedValue;

pixels[yFlip * diskTexture.Width + x] = currentColor.PackedValue;

pixels[yFlip * diskTexture.Width + xFlip] =

currentColor.PackedValue;

}

}

textureNeedsUpdate = true;

}

}

}

if (textureNeedsUpdate) {

// Обновляем текстуру значениями массива pixels this.GraphicsDevice.Textures[0] = null; diskTexture.SetData<uint>(pixels);

}

// Подготовка к следующему проходу

foreach (TouchInfo touchInfo in touchDictionary.Values)

touchInfo.PreviousPosition = touchInfo.CurrentPosition;

previousAngle = currentAngle; base.Update(gameTime);

Сам перегруженный метод Draw чрезвычайно мал. Он лишь формирует визуальное представление вращающегося diskTexture (Текстура диска) и имени приложения, которое постоянно меняет цвет и отображается вверху экрана:

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

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.Navy); spriteBatch.Begin();

spriteBatch.Draw(diskTexture, displayCenter, null, Color.White,

currentAngle, textureCenter, 1, SpriteEffects.None, 0); spriteBatch.DrawString(segoe4 8, titleText, titlePosition, currentColor); spriteBatch.End();

base.Draw(gameTime);

}

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

По теме:

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