Pull to refresh

Windows Phone 8: Создаем приложение. Матрица. Часть 1

Reading time 12 min
Views 31K
Windows Phone 8: Создаем приложение. Матрица. Часть 1
Windows Phone 8: Создаем приложение. Матрица. Часть 2
Windows Phone 8: Создаем приложение. Матрица. Часть 3. MVVM

Создавая приложение на конкурс появилась так же идея поделиться процессом его создания, так как сам столкнулся с трудностями нахождения информации по созданию приложений для Windows Phone 8. Почему «Матрица»? Потому, что еще с выхода фильма она меня завораживала. Потом нашел заставку на экран. Мог часами смотреть на нее. А теперь решил уже перенести ее на телефон, что б всегда была под рукой. И так начнем.


Принт скрин с экрана Lumia 520

Года 2 назад я уже создавал нечто похожее, но на JQuery. Однако тогда все получилось, кроме полноценности: максимальный размер матрицы, при котором она не «тормозила» составлял приблизительно 15 Х 20 клеточек, что явно мало для современных FULL HD мониторов. При выяснении причин такого поведения выяснил, что оно все грузило только 1 ядро и как я тогда понял, только в 1 потоке (могу ошибаться, но больше на эту тему разбора полетов не устраивал). Да и создавал только с целью освоиться в этом JS фреймворке. C# лишен этих недостатков, да и мощности современных смартфонов должно хватить для обработки большого количество элементов и множества случайных чисел на каждый такой элемент. Забегая наперед, скажу что при тестировании на моей слабенькой Lumia 520 все мои надежды оправдались.

Для начала определим, что именно хотим создать


  1. Сетка ячеек, в которой случайным образом выбираются координаты начала змейки
  2. Змейки будут разной случайной длины
  3. В каждой ячейке змейки элементы меняются случайным образом случайное количество раз
  4. Присутствует эффект затухания цвета. Яркость каждой составляющей змейки зависит от ее длины


Что использовал для создания приложения


  • Windows 8 Pro x64 (RU)
  • VS 2012 SP1 (RU)
  • Windows Phone SDK 8.0 (RU)
  • Только стандартные элементы, входящие в VS
  • Тестировал на эмуляторе с настройками WVGA 512 MB (RU)

Пару слов про тестирование. На эмуляторе результаты не сильно правдоподобны. При запуске большого количества змеек он показывал больше FPS чем на реальном телефоне. При отладке на телефоне — телефон тормозит ужасно. Однако после остановки отладки и простого запуска приложения производительность была адекватная.

Перейдем к работе в Visual Studio


Создаем проект: Шаблоны — Visual C# — Windows Phone — Приложение Windows Phone.
Выбираем .NET Framework 4.5.
Имя проекта у меня SE_Matrix_2d_v_1. Оно же будет и namespace.
Выбираем Windows Phone OS 8.0.

MainPage.xaml

Теперь отредактируем XAML код. Нам нужен будет только Grid с именем LayoutRoot и событие нажатия(Tap) Event_Grid_Tap_LayoutRoot, в который динамически, в зависимости от расширения экрана будем вписывать нужное количество ячеек TextBlock. Смотрим:

<phone:PhoneApplicationPage
    x:Class="SE_Matrix_2d_v_1.MainPage"
    x:Name="SE_Matrix_2d_v_1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True">

    <!--LayoutRoot представляет корневую сетку, где размещается все содержимое страницы-->
    <Grid x:Name="LayoutRoot" Background="Transparent" Tap="Event_Grid_Tap_LayoutRoot">
    </Grid>
</phone:PhoneApplicationPage>

Запускаем эмулятор. Должен быть просто черный экран.

MainPage.xaml.cs

Сразу подключаем необходимые библиотеки:
using System.Windows.Media; // Для работы с цветами
using System.Threading.Tasks; // Для асинхронных вызовов
using System.Diagnostics; // Для отладки. Debug.WriteLine( SomethingYouNeedToSee);

В конструкторе вызовем метод CreateElement, который с создаст матрицу элементов, в которой и будут проходить все дальнейшие действия:
        // Конструктор
        public MainPage()
        {
            InitializeComponent();
            CreateElement();
        }

Для того, что б создать матрицу, нужно определить расширение экрана. Добавим в класс MainPage свойства ScreenWidth и ScreenHeight:
    public partial class MainPage : PhoneApplicationPage
    {
        // Получаем расширение экрана
        double ScreenWidth = System.Windows.Application.Current.Host.Content.ActualWidth;
        double ScreenHeight = System.Windows.Application.Current.Host.Content.ActualHeight;
         ...

Приступим к созданию метода CreateElement.
В первую очередь нужно определить количество строк и столбцов в матрице, которую будем создавать:
            // Количество строк и столбцов
            countWidth = (int)Math.Round(ScreenWidth / 50);
            countHeight = (int)Math.Round(ScreenHeight / 50);

Если Вы начали изучение C# после освоения других языков, то может так будет понятней:
            // Количество строк и столбцов
            countWidth = (int)Math.Round(this.ScreenWidth / 50);
            countHeight = (int)Math.Round(this.ScreenHeight / 50);

Так (используя this) Вы однозначно указываете, что обращаться нужно именно к свойству класса.
Если this не использовать, то сначала ищется переменная с таким именем внутри метода, а если не находится, то поиск продолжается по свойствам класса.

Теперь зная количество строк и столбцов, создаем с помощью обычного перебора (2 цикла for ) нужное количество элементов TextBlock с необходимыми начальными настройками:
                // Создаем сетку ячеек
                for (i = 0; i < countWidth; i++)
                {
                    for (j = 0; j < countHeight; j++)
                    {
                        // Создаем TextBlock
                        TextBlock element = new TextBlock();

                        // Задаем имя элемента TextBlock
                        element.Name = "TB_" + i + "_" + j;

                        // Инициализируем начальный символ
                        //element.Text = char.ConvertFromUtf32(random.Next(0x4E00, 0x4FFF)); // Случайный символ из заданного диапазона
                        element.Text = ""; // Пустота

                        // Задаем смещение каждого нового элемента TextBlock
                        int wx = i * 50;
                        int wy = j * 50;
                        element.Margin = new Thickness(wx, wy, 0, 0);

                        // Задаем цвет символа
                        element.Foreground = new SolidColorBrush(Colors.Green);

                        // Задаем размер шрифта
                        element.FontSize = 36;

                        // Добавляем созданный элемент в Grid
                        LayoutRoot.Children.Add(element);
                    }
                }           

Margin задается для того, что б они не были все друг над другом.
Имя каждого TextBlock формируется с участием координаты Х и У для облегчения последующего доступа к ним и возможности случайно задавать координаты начала змейки.

Если добавить в этот метод элемент Border, то можно визуально оценить размеры ячеек и их расположение. Смотрим:

        // Создание сетки элементов, в которой будет сыпаться матрица
        public void CreateElement()
        {
            int i, j, countWidth, countHeight;

            // Количество строк и столбцов
            countWidth = (int)Math.Round(this.ScreenWidth / 50);
            countHeight = (int)Math.Round(this.ScreenHeight / 50);

                // Создаем сетку ячеек
                for (i = 0; i < countWidth; i++)
                {
                    for (j = 0; j < countHeight; j++)
                    {
                        // Создаем TextBlock
                        TextBlock element = new TextBlock();

                        // Создаем Border
                        Border elementBorder = new Border();

                        // Задаем имя элемента TextBlock
                        elementBorder.Name = "B_" + i + "_" + j;

                        // Задаем имя элемента TextBlock
                        element.Name = "TB_" + i + "_" + j;

                        // Инициализируем начальный символ
                        //element.Text = char.ConvertFromUtf32(random.Next(0x4E00, 0x4FFF)); // Случайный символ из заданного диапазона
                        element.Text = ""; // Пустота

                        // Задаем смещение каждого нового элемента TextBlock
                        int wx = i * 50;
                        int wy = j * 50;
                        element.Margin = new Thickness(wx, wy, 0, 0);

                        // Задаем цвет символа
                        element.Foreground = new SolidColorBrush(Colors.Green);

                        // Задаем размер шрифта
                        element.FontSize = 36;

                        // Задаем смещение каждого нового элемента Border
                        elementBorder.Margin = new Thickness(wx, wy, 0, 0);

                        // Задаем толщину линии обводки
                        elementBorder.BorderThickness = new Thickness(1);

                        // Задаем цвет обводки
                        elementBorder.BorderBrush = new SolidColorBrush(Colors.Green);

                        // Добваляем TextBlock в Border
                        elementBorder.Child = element;                     

                        // Добавляем созданный элемент в Grid
                        LayoutRoot.Children.Add(elementBorder);
                    }
                }           
        }

В таком случае в Grid добавляем уже элемент Border (LayoutRoot.Children.Add(elementBorder);), так как что б получить видимые границы нужного элемента необходимо его обернуть в тег Border:
<Border BorderThickness="1" BorderBrush="Black" Background="Green" CornerRadius="5">
    <TextBlock Text="Description"/>
</Border>

Далее продолжим работу с первым вариантом, без границ.

Теперь, когда матрица есть, можно приступить к созданию змеек, которые будут сыпаться. Для начала создадим обработчик события нажатия на на элемент Grid, который просто будет вызывать метод Start:
        // Событие при нажатии на элемент Grid (на экран)
        private void Event_Grid_Tap_LayoutRoot(object sender, System.Windows.Input.GestureEventArgs e)
        {
            Start();
        }

Если в этом обработчике вызвать метод Start в цикле, то получим несколько одновременных змеек. Но это чуть позже.

Метод Start определяет начальные настройки змейки, такие как координаты Х и У начала змейки, длину змейки, скорость смены символов в каждой ячейке TextBlock.случайным образом, количество змеек после нажатия на экран в очереди:
        // Метод запуска змейки
        public async void Start()
        {
            int count, iteration;

            // Количество змеек после нажатия на экран в очереди
            iteration = 1;

            count = 0;

            // Количество змеек после нажатия на экран в очереди
            while (count < iteration)
            {
                // Начало змейки по горизонтали случайным образом
                int ranX    = random.Next(0, 10);

                // Начало змейки по вертикали случайным образом
                int ranY    = random.Next(0, 20);

                // Длина змейки случайным образом
                int length  = random.Next(3, 7);

                // Скорость смены символов в змейке случайным образом в мс
                int time    = random.Next(30, 70);

                await Task.Delay(1);

                // Обработка змейки
                await RandomElementQ_Async(ranX, ranY, length, time);

                count++;
            }
        }

Вот мы и подобрались к «сердцу» нашего приложения, методу RandomElementQ_Async. Он отвечает за захват нужного элемента, определению его цвета, создание эффекта падения и защиты от выползания змейки за границы матрицы. А теперь более подробно:

        // Определяю элемент, в котором нужно менять символы
        public async Task RandomElementQ_Async(int x, int y, int length, int timeOut)
        {
            // Словарь для хранения идентификаторов ячеек, которые вызывались на предыдущем этапе.
            Dictionary<int, TextBlock> dicElem = new Dictionary<int, TextBlock>();

            // Счетчик, нужен для обработки случаев, когда не выполняется условие if ((y + i) < countHeight && (y + i) >= 0). Смотри на 4 строчки вниз.
            int count = 0;

            // Цикл формирует змейку заданной длины length
            for (int i = 0; i < length; i++)
            {
                // Нужно для обработки случаев, когда змейка растет за пределы области вниз 
                // Просто ничего не делаем
                int countHeight = (int)Math.Round(ScreenHeight / 50);

                // Проверяем, что б змейка отображалась только в координатах, которые существуют в нашей сетке
                if ((y + i) < countHeight)
                {
                    // Формируем имя элемента, в котором будут меняться символы
                    string elementName = "TB_" + x + "_" + (y + i);

                    // Получаем элемент по его имени
                    object wantedNode = LayoutRoot.FindName(elementName);
                    TextBlock element = (TextBlock)wantedNode;

                    // Отправляем элемент в словарь, из которого он будет извлекаться для эффекта "падения" и "затухания" змейки
                    dicElem[count] = (element);

                    // Определяем коеффициент для подсчета яркости. Первый элемент(который падает) -  всега самый яркий, последний - самый темный.
                    // Отнимаем 1, потому, что последний элемент в итоге получается больше 255 и становится ярким.
                    int rf = (int)Math.Round(255 / (double)(i + 1)) - 1;

                    // Вызываем на прорисовку первый, самый яркий падающий элемент. Асинхронно.
                    await Change(element, timeOut, 255);

                    // Перебираем все элементы, составляющие змейку на данном этапе. С каждым циклом она увеличивается, пока не достигнет своей длины.
                    for (int k = 0; k <= i; k++)
                    {
                        // Если змейка начинается "выше" начальных координат (например, если y = -5)
                        if (dicElem.ContainsKey(k))
                        {
                            //Извлекаем элементы, которые должны следовать за самым ярким. Создаем эффект "затухания" цвета
                            TextBlock previousElement = dicElem[k];

                            // Вызываем извлеченные элементы
                            // (rf * (k + 1)) - 20 Высчитываем яркость так, что б разница между первым и последним была на всех змейках одинаковая
                            // и равномерно распределялась независимо от ее длины(количества элементов)
                            Task dsvv = Change(previousElement, timeOut, (rf * (k + 1)) - 20);
                        }
                    }
                    count++;
                }
            }
        }

И последний простенький метод, меняющий символы и их цвет в нужной ячейке нужное количество раз:
        // Метод изменения символов в заданном элеменете
        public async Task Change(TextBlock txt, int timeOut, int Opacity)
        {
            // Формируем нужный цвет с заданной яркостью
            SolidColorBrush NewColor = new SolidColorBrush(new Color()
            {
                A = (byte)(255) /*Opacity*/,
                R = (byte)(0) /*Red*/,
                G = (byte)(Opacity) /*Green*/,
                B = (byte)(0) /*Blue*/
            });

            // При каждом "падении" на 1 клеточку равномерно "затухает"
            txt.Foreground = NewColor;

            // Количество смены символов в каждой ячейке          
            for (int i = 0; i < 5; i++)
            {              
                // Каждый раз разный символ
                txt.Text = char.ConvertFromUtf32(random.Next(0x4E00, 0x4FFF));

                // Скорость смены символов в ячейке
                await Task.Delay(timeOut);
            }            
        }


Работающий код файла MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using System.Threading.Tasks;
using System.Diagnostics;

namespace SE_Matrix_2d_v_1
{
    public partial class MainPage : PhoneApplicationPage
    {
        // Случайное число
        Random random = new Random();

        // Получаем расширение экрана
        double ScreenWidth = System.Windows.Application.Current.Host.Content.ActualWidth;
        double ScreenHeight = System.Windows.Application.Current.Host.Content.ActualHeight;

        // Конструктор
        public MainPage()
        {
            InitializeComponent();
            CreateElement();
        }
        // Создание сетки элементов, в которой будет сыпаться матрица
        public void CreateElement()
        {
            int i, j, countWidth, countHeight;

            // Количество строк и столбцов
            countWidth = (int)Math.Round(this.ScreenWidth / 50);
            countHeight = (int)Math.Round(this.ScreenHeight / 50);

                // Создаем сетку ячеек
                for (i = 0; i < countWidth; i++)
                {
                    for (j = 0; j < countHeight; j++)
                    {
                        // Создаем TextBlock
                        TextBlock element = new TextBlock();

                        // Задаем имя элемента TextBlock
                        element.Name = "TB_" + i + "_" + j;

                        // Инициализируем начальный символ
                        //element.Text = char.ConvertFromUtf32(random.Next(0x4E00, 0x4FFF)); // Случайный символ из заданного диапазона
                        element.Text = ""; // Пустота

                        // Задаем смещение каждого нового элемента TextBlock
                        int wx = i * 50;
                        int wy = j * 50;
                        element.Margin = new Thickness(wx, wy, 0, 0);

                        // Задаем цвет символа
                        element.Foreground = new SolidColorBrush(Colors.Green);

                        // Задаем размер шрифта
                        element.FontSize = 36;                     

                        // Добавляем созданный элемент в Grid
                        LayoutRoot.Children.Add(element);
                    }
                }           
        }

        // Событие при нажатии на элемент Grid (на экран)
        private void Event_Grid_Tap_LayoutRoot(object sender, System.Windows.Input.GestureEventArgs e)
        {
            Start();
        }

        // Метод запуска змейки
        public async void Start()
        {
            int count, iteration;

            // Количество змеек после нажатия на экран в очереди
            iteration = 1;

            count = 0;

            // Количество змеек после нажатия на экран в очереди
            while (count < iteration)
            {
                // Начало змейки по горизонтали случайным образом
                int ranX    = random.Next(0, 10);

                // Начало змейки по вертикали случайным образом
                int ranY    = random.Next(0, 20);

                // Длина змейки случайным образом
                int length  = random.Next(3, 7);

                // Скорость смены символов в змейке случайным образом в мс
                int time    = random.Next(30, 70);

                await Task.Delay(1);

                // Обработка змейки
                await RandomElementQ_Async(ranX, ranY, length, time);

                count++;
            }
        }

        // Определяю элемент, в котором нужно менять символы
        public async Task RandomElementQ_Async(int x, int y, int length, int timeOut)
        {
            // Словарь для хранения идентификаторов ячеек, которые вызывались на предыдущем этапе.
            Dictionary<int, TextBlock> dicElem = new Dictionary<int, TextBlock>();

            // Счетчик, нужен для обработки случаев, когда не выполняется условие if ((y + i) < countHeight && (y + i) >= 0). Смотри на 4 строчки вниз.
            int count = 0;

            // Цикл формирует змейку заданной длины length
            for (int i = 0; i < length; i++)
            {
                // Нужно для обработки случаев, когда змейка растет за пределы области вниз 
                // Просто ничего не делаем
                int countHeight = (int)Math.Round(ScreenHeight / 50);

                // Проверяем, что б змейка отображалась только в координатах, которые существуют в нашей сетке
                if ((y + i) < countHeight)
                {
                    // Формируем имя элемента, в котором будут меняться символы
                    string elementName = "TB_" + x + "_" + (y + i);

                    // Получаем элемент по его имени
                    object wantedNode = LayoutRoot.FindName(elementName);
                    TextBlock element = (TextBlock)wantedNode;

                    // Отправляем элемент в словарь, из которого он будет извлекаться для эффекта "падения" и "затухания" змейки
                    dicElem[count] = (element);

                    // Определяем коеффициент для подсчета яркости. Первый элемент(который падает) -  всега самый яркий, последний - самый темный.
                    // Отнимаем 1, потому, что последний элемент в итоге получается больше 255 и становится ярким.
                    int rf = (int)Math.Round(255 / (double)(i + 1)) - 1;

                    // Вызываем на прорисовку первый, самый яркий падающий элемент. Асинхронно.
                    await Change(element, timeOut, 255);

                    // Перебираем все элементы, составляющие змейку на данном этапе. С каждым циклом она увеличивается, пока не достигнет своей длины.
                    for (int k = 0; k <= i; k++)
                    {
                        // Если змейка начинается "выше" начальных координат (например, если y = -5)
                        if (dicElem.ContainsKey(k))
                        {
                            //Извлекаем элементы, которые должны следовать за самым ярким. Создаем эффект "затухания" цвета
                            TextBlock previousElement = dicElem[k];

                            // Вызываем извлеченные элементы
                            // (rf * (k + 1)) - 20 Высчитываем яркость так, что б разница между первым и последним была на всех змейках одинаковая
                            // и равномерно распределялась независимо от ее длины(количества элементов)
                            Task dsvv = Change(previousElement, timeOut, (rf * (k + 1)) - 20);
                        }
                    }
                    count++;
                }
            }
        }

        // Метод изменения символов в заданном элеменете
        public async Task Change(TextBlock element, int timeOut, int Opacity)
        {
            // Формируем нужный цвет с заданной яркостью
            SolidColorBrush NewColor = new SolidColorBrush(new Color()
            {
                A = (byte)(255) /*Opacity*/,
                R = (byte)(0) /*Red*/,
                G = (byte)(Opacity) /*Green*/,
                B = (byte)(0) /*Blue*/
            });

            // При каждом "падении" на 1 клеточку равномерно "затухает"
            element.Foreground = NewColor;

            // Количество смены символов в каждой ячейке          
            for (int i = 0; i < 5; i++)
            {              
                // Каждый раз разный символ
                element.Text = char.ConvertFromUtf32(random.Next(0x4E00, 0x4FFF));

                // Скорость смены символов в ячейке
                await Task.Delay(timeOut);
            }            
        }
        
        // Пример кода для построения локализованной панели ApplicationBar
        //private void BuildLocalizedApplicationBar()
        //{
        //    // Установка в качестве ApplicationBar страницы нового экземпляра ApplicationBar.
        //    ApplicationBar = new ApplicationBar();

        //    // Создание новой кнопки и установка текстового значения равным локализованной строке из AppResources.
        //    ApplicationBarIconButton appBarButton = new ApplicationBarIconButton(new Uri("/Assets/AppBar/appbar.add.rest.png", UriKind.Relative));
        //    appBarButton.Text = AppResources.AppBarButtonText;
        //    ApplicationBar.Buttons.Add(appBarButton);

        //    // Создание нового пункта меню с локализованной строкой из AppResources.
        //    ApplicationBarMenuItem appBarMenuItem = new ApplicationBarMenuItem(AppResources.AppBarMenuItemText);
        //    ApplicationBar.MenuItems.Add(appBarMenuItem);
        //}
    }
}


Ну вот и готова первая часть приложения. Костяк.
В следующей части мы переведем приложение на шаблон «Приложение Windows Phone с панорамой», расширим функционал приложения добавлением различных настроек матрицы, изменение которых будут сразу же отображаться на экране вашего смартфона.

П.С. Старался максимально передать последовательность своих мыслей, что б для людей начавших изучение WP8 приложение не казалось черным ящиком. А так же максимально упростить код и постепенно наращивать его сложность в следующих частях. Если есть непонятные моменты — спрашивайте.
Tags:
Hubs:
+16
Comments 19
Comments Comments 19

Articles