Главная » Разработка для Windows Phone 7 » Приложение расширений для обработки фотографий Windows Phone 7

0

Архитектурно и функционально приложение Posterizer (Постеризатор), завершающее эту главу, аналогично приложению Monochromize. Оно позволяет пользователю выбирать фотографию из библиотеки изображений и опять сохранять ее в альбом Saved Pictures. Но также с помощью этого приложения пользователь может сокращать битовое разрешение каждого цвета в отдельности (создание эффекта плаката). Для этого в нем предусмотрена строка элементов RadioButton. Данное приложение также должно сохранять исходный массив пикселов, чтобы обеспечить возможность восстанавливать изображение в полном цветовом разрешении.

Кроме того, Posterizer регистрирует себя как приложений «расширений для обработки фотографий». Это означает, что пользователь может вызвать его прямо из библиотеки изображений.

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

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

Наложение реализовано с помощью производного от UserControl класса BitSelectDialog (Диалоговое окно выбора битового разрешения). Начнем обсуждение с этого элемента управления. Дерево визуальных элементов включает лишь Grid с тремя столбцами и девятью строками:

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

<Grid x:Name="LayoutRoot" Background="Transparent">

<Grid.ColumnDefinitions>

<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />

</Grid.ColumnDefinitions>

<Grid.RowDefinitions>

<RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" />

</Grid.RowDefinitions> </Grid>

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

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

public event EventHandler ColorBitsChanged; public int[] ColorBits { protected set; get; }

В этом уже можно заметить небольшой недостаток. Несмотря на то что метод доступа для задания значения массива ColorBits (Биты цвета) является защищенным, и вносить изменения в массив из внешнего класса невозможно, отдельные члены этого массива могут быть заданы. При этом нет никакой возможности проинформировать класс об этом, кроме как сформировать событие ColorBitsChanged (Биты цвета изменены). Но я решил не усложнять код класса и не устранять этот недостаток.

Все элементы TextBlock создаются в конструкторе класса. Обратите внимание, что изначально в массиве ColorBits хранится три числа 2.

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

public partial class BitSelectDialog : UserControl {

Brush selectedBrush;

Brush normalBrush;

TextBlock[,] txtblks = new TextBlock[3, 9];

public BitSelectDialog() {

InitializeComponent();

ColorBits = new int[3]; ColorBits[0] = 2; ColorBits[1] = 2; ColorBits[2] = 2;

selectedBrush = this.Resources["PhoneAccentBrush"] as Brush; normalBrush = this.Resources["PhoneForegroundBrush"] as Brush; string[] colors = { "red", "green", "blue" };

for (int col = 0; col < 3; col++) {

TextBlock txtblk = new TextBlock {

Text = colors[col], FontWeight = FontWeights.Bold, TextAlignment = TextAlignment.Center, Margin = new Thickness(8, 2, 8, 2)

Grid.SetRow(txtblk, 0); Grid.SetColumn(txtblk, col); LayoutRoot.Children.Add(txtblk);

for (int bit = 0; bit < 9; bit++) {

txtblk = new TextBlock {

Text = bit.ToString(),

Foreground = bit == ColorBits[col] ? selectedBrush :

normalBrush,

TextAlignment = TextAlignment.Center, Padding = new Thickness(2), Tag = col.ToString() + bit

Grid.SetRow(txtblk, bit + 1); Grid.SetColumn(txtblk, col); LayoutRoot.Children.Add(txtblk);

txtblks[col, bit] = txtblk;

}

}

}

}

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

Я также описал открытый метод, с помощью которого приложение инициирует три значения ColorBits. При этом цвета TextBlock меняются, но события ColorBitsChanged не формируются. Это пригодится при повторной активации приложения после захоронения.

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

public void Initialize(int[] colorBits) {

for (int clr = 0; clr < 3; clr++) {

txtblks[clr, ColorBits[clr]].Foreground = normalBrush; ColorBits[clr] = colorBits[clr];

txtblks[clr, ColorBits[clr]].Foreground = selectedBrush;

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

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

protected override void OnManipulationStarted(ManipulationStartedEventArgs args) {

if (args.OriginalSource is TextBlock) {

TextBlock txtblk = args.OriginalSource as TextBlock; string tag = txtblk.Tag as string;

if (tag != null && tag.Length == 2) {

int clr = Int32.Parse(tag[0].ToString()); int bits = Int32.Parse(tag[1].ToString());

if (ColorBits[clr] != bits) {

txtblks[clr, ColorBits[clr]].Foreground = normalBrush; ColorBits[clr] = bits;

txtblks[clr, ColorBits[clr]].Foreground = selectedBrush;

if (ColorBitsChanged != null)

ColorBitsChanged(this, EventArgs.Empty);

}

args.Complete(); args.Handled = true;

}

}

base.OnManipulationStarted(args);

}

На основании декодированных данных свойства Tag элемента управления TextBlock, которого коснулся пользователь, метод может изменить цвет этого TextBlock (при этом выбор с этого элемента снимается), сохранить новое значение в массиве ColorBits и сформировать событие ColorBitsChanged.

Область содержимого класса MainPage включает (уже традиционно) пустой элемент Image и вот такой элемент управления BitSelectDialog:

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

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

<local:BitSelectDialog x:Name="bitSelectDialog"

Visibility="Collapsed"

FontSize="{StaticResource PhoneFontSizeExtraLarge}"

HorizontalAlignment="Center"

VerticalAlignment="Center"

ColorBitsChanged="OnBitSelectDialogColorBitsChanged" />

</Grid>

Обратите внимание, что элемент управления BitSelectDialog имеет собственное свойство Visibility, которому задано значение Collapsed.

ApplicationBar в XAML-файле включает три кнопки: Проект Silverlight: Posterizer Файл: MainPage.xaml (фрагмент)

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

<shell:ApplicationBarIconButton x:Name="appbarLoadButton"

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

Click="OnAppbarLoadClick" />

<shell:ApplicationBarIconButton x:Name="appbarSetBitsButton"

IconUri="/Images/appbar.feature.settings.rest.png"

Text="set bits" IsEnabled="False" Click="OnAppbarSetBitsClick" />

<shell:ApplicationBarIconButton x:Name="appbarSaveButton"

IconUri="/Images/appbar.save.rest.png" Text="save" IsEnabled="False" Click="OnAppbarSaveClick" />

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

В файле выделенного кода поля класса MainPage включают три переменные, представляющие растровые изображения: переменную типа WriteableBitmap, массив byte и массив int.

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

public partial class MainPage : PhoneApplicationPage, ISaveFileDialogCompleted {

PhoneApplicationService appService = PhoneApplicationService.Current; PhotoChooserTask photoChooser = new PhotoChooserTask(); WriteableBitmap writeableBitmap; byte[] jpegBits; int[] pixels;

public MainPage() {

InitializeComponent();

appbarLoadButton = this.ApplicationBar.Buttons[0] as ApplicationBarIconButton;

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

appbarSaveButton = this.ApplicationBar.Buttons[2] as ApplicationBarIconButton;

photoChooser.Completed += OnPhotoChooserCompleted;

}

}

Поле WriteableBitmap – это растровое изображение, заданное как значение элемента Image и отображаемое на экране. Этот то растровое изображение, пикселы которого были изменены для понижения цветового разрешения. Массив jpegBits – это исходный файл, загружаемый пользователем из библиотеки изображений. Массив jpegBits удобно сохранять в целях

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

Когда пользователь нажимает кнопку «load», происходит инициация класса PhotoChooserTask. В ходе обработки события Completed приложение задает jpegBits из потока ChosenPhoto и затем вызывает LoadBitmap (Загрузить растровое изображение).

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

void OnAppbarLoadClick(object sender, EventArgs args) {

bitSelectDialog.Visibility = Visibility.Collapsed; appbarSetBitsButton.IsEnabled = false; appbarSaveButton.IsEnabled = false;

photoChooser.Show();

}

void OnPhotoChooserCompleted(object sender, PhotoResult args) {

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

jpegBits = new byte[args.ChosenPhoto.Length]; args.ChosenPhoto.Read(jpegBits, 0, jpegBits.Length); LoadBitmap(jpegBits);

}

}

void LoadBitmap(byte[] jpegBits) {

// Создаем WriteableBitmap из массива jpegBits MemoryStream memoryStream = new MemoryStream(jpegBits); BitmapImage bitmapImage = new BitmapImage(); bitmapImage.SetSource(memoryStream);

writeableBitmap = new WriteableBitmap(bitmapImage); img.Source = writeableBitmap;

// Копируем пикселы в массив pixels

pixels = new int[writeableBitmap.PixelWidth * writeableBitmap.PixelHeight];

for (int i = 0; i < pixels.Length; i++)

pixels[i] = writeableBitmap.Pixels[i];

appbarSetBitsButton.IsEnabled = true; appbarSaveButton.IsEnabled = true; ApplyBitSettingsToBitmap();

}

После этого метод LoadBitmap преобразует этот массив byte назад в MemoryStream для создания BitmapImage и WriteableBitmap. Может показаться, что так мы создаем растровое изображение окольными путями, но это имеет смысл с точки зрения захоронения.

После этого метод LoadBitmap создает копию массива Pixels объекта WriteableBitmap в виде поля pixels. Это поле pixels будет оставаться неизменным, в то время как массив Pixels объекта WriteableBitmap меняется, исходя из выбираемого пользователем битового разрешения. Наша задача – отсутствие необратимых операций. Пользователь всегда должен иметь возможность выбрать более высокое цветовое разрешение после применения более низкого.

Метод ApplyBitSettingsToBitmap (Применить настройки битового разрешения к растровому изображению), вызываемый в конце выполнения LoadBitmap, также вызывается каждый раз, когда BitSelectDialog формирует событие ColorBitsChanged. Видимость этого диалогового окна переключается по нажатию средней кнопки ApplicationBar:

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

void OnAppbarSetBitsClick(object sender, EventArgs args) {

bitSelectDialog.Visibility =

bitSelectDialog.Visibility == Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed;

}

void OnBitSelectDialogColorBitsChanged(object sender, EventArgs args) {

ApplyBitSettingsToBitmap();

}

void ApplyBitSettingsToBitmap() {

if (pixels == null || writeableBitmap == null) return;

int mask = -16777216; // например, FF000000

for (int clr = 0; clr < 3; clr++)

mask |= (byte)(0xFF << (8 – bitSelectDialog.ColorBits[clr]))

<< (16 – 8 * clr);

for (int i = 0; i < pixels.Length; i++)

writeableBitmap.Pixels[i] = mask & pixels[i];

writeableBitmap.Invalidate();

}

Переменная mask (маска) образуется из трех значений битового разрешения и затем применяется ко всем значениям поля pixels, что обеспечивает задание всех значений массива Pixels объекта WriteableBitmap.

Когда пользователь нажимает кнопку, чтобы сохранить файл в библиотеку изображений, приложение Posterizer предлагает пользователю имя файла, образованное именем данного приложения (Posterizer), за которым следует трехзначное число, превышающее номера всех имеющихся в библиотеке изображений. Для этого приложение выполняет доступ к альбому сохраненных изображений посредством свойства SavedPictures объекта MediaLibrary и выполняет поиск соответствующих имен файлов:

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

void OnAppbarSaveClick(object sender, EventArgs args) {

int fileNameNumber = 0;

MediaLibrary mediaLib = new MediaLibrary(); PictureCollection savedPictures = mediaLib.SavedPictures;

foreach (Picture picture in savedPictures) {

string filename = Path.GetFileNameWithoutExtension(picture.Name); int num;

if (filename.StartsWith("Posterizer"))

if (Int32.TryParse(filename.Substring(10), out num)) fileNameNumber = Math.Max(fileNameNumber, num);

}

}

string saveFileName = String.Format("Posterizer{0:D3}", fileNameNumber + 1);

string uri = "/Petzold.Phone.Silverlight;component/SaveFileDialog.xaml" + "?FileName=" + saveFileName;

this.NavigationService.Navigate(new Uri(uri, UriKind.Relative));

}

public void SaveFileDialogCompleted(bool okPressed, string filename) {

if (okPressed) {

MemoryStream memoryStream = new MemoryStream();

writeableBitmap.SaveJpeg(memoryStream, writeableBitmap.PixelWidth,

writeableBitmap.PixelHeight, 0, 75);

memoryStream.Position = 0;

MediaLibrary mediaLib = new MediaLibrary(); mediaLib.SavePicture(filename, memoryStream);

}

}

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

К файлу проекта приложения следует добавить еще такой файл: Проект Silverlight: Posterizer Файл: Extras.xml

<Extras>

<PhotosExtrasApplication>

<Enabled>true</Enabled> </PhotosExtrasApplication> </Extras>

В разделе Properties этого файла для Build Action должно быть выбрано Content и для Copy to Output Directory (Копировать в выходной каталог) – Copy Always (Копировать всегда).

Приложение также должно быть готово обрабатывать специальный вызов OnNavigatedTo для отображения выбранной фотографии.

Рассмотрим оба перегруженных метода навигации. Метод OnNavigatedFrom определяет, выполняется ли захоронение приложения или переход к объекту SaveFileDialog. В случае захоронения приложение должно сохранить и выбранное в настоящий момент цветовое разрешение, и растровое изображение (если таковое существует). При переходе к SaveFileDialog метод задает заголовок страницы.

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

protected override void OnNavigatedFrom(NavigationEventArgs args) {

appService.State["colorBits"] = bitSelectDialog.ColorBits;

if (jpegBits != null) {

appService.State["jpegBits"] = jpegBits;

}

if (args.Content is SaveFileDialog) {

SaveFileDialog page = args.Content as SaveFileDialog; page.SetTitle(ApplicationTitle.Text);

}

base.OnNavigatedFrom(args);

}

protected override void OnNavigatedTo(NavigationEventArgs args) {

if (this.NavigationContext.QueryString.ContainsKey("token")) {

string token = this.NavigationContext.QueryString["token"];

MediaLibrary mediaLib = new MediaLibrary();

Picture picture = mediaLib.GetPictureFromToken(token);

Stream stream = picture.GetImage();

jpegBits = new byte[stream.Length];

stream.Read(jpegBits, 0, jpegBits.Length);

LoadBitmap(jpegBits);

else if (appService.State.ContainsKey("colorBits"))

int[] colorBits = (int[])appService.State["colorBits"]; bitSelectDialog.Initialize(colorBits);

if (appService.State.ContainsKey("jpegBits"))

jpegBits = (byte[])appService.State["jpegBits"]; LoadBitmap(jpegBits);

base.OnNavigatedTo(args);

}

Метод OnNavigatedTo может показать, что приложение вызвано из библиотеки изображений. В этом случае словарь QueryString объекта NavigationContext будет содержать специальный строковый ключ «token». Элемент, соответствующий этой строке, передается в специальный метод GetPictureFromToken (Получить изображение по маркеру) объекта MediaLibrary для получения потока в памяти, из которого можно осуществить доступ к JPEG- файлу.

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

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

По теме:

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