Разработка графического пользовательского интерфейса в соответствии с паттерном Model-View-Viewmodel на платформе Windows Presentation Foundation. Осн
.pdfCommands и ViewEvents. Класс ViewCommands предназначен для пере-
дачи сигналов и сообщений из GUI в модель через контроллер. Роль класса ViewEvents заключается не столько во взаимодействии с моделью, сколько в обслуживании сложного поведения элементов GUI, недоступного для реализации чисто декларативными средствам. Как правило, инициатором обращения к ViewEvents является ViewCommands, при этом к элементам GUI подключаются обработчики событий (event handlers), которые в дальнейшем вызываются «автоматически», по мере возникновения событий в системе. Отключается реакция на события по мере завершения соответствующих сценариев действий пользователя, например по окончании прямого манипулирования.
Функции программы в рассматриваемом примере — загрузка изображений с диска, плавное попиксельное панорамирование с помощью мыши, а также простейшая модификация (в данном случае, получение негатива). В этом примере значительную смысловую нагрузку имеет класс Model, реализующий растровые операции с высокой производительностью. Кроме того, показана эффективность управления приложением на основе системы высокоуровневых команд, инициируемых разными способами (как с помощью устройств ввода, так и элементами
GUI).
Для лаконичной иллюстрации роли ViewEvents выбран несколько искусственный прием: реализована дополнительная функциональность панорамирования, отсутствующая в стандартном компоненте Image. Напомним, что в эргономичных просмотрщиках нередко применяется «перетаскивание» видимого в окне участка большого изображения мышью. Несложно реализовать такое поведение, обрабатывая низкоуровневые события, генерируемые мышью, и вызывая методы стандартного компонента WPF ScrollViewer, в контейнер которого помещен компонент Image.
Основные составляющие программного проекта — файлы MainWindow.xaml (декларативная часть на языке XAML), Model.cs, ViewMod-
41
el.cs, ViewController.cs, ViewCommands.cs и ViewEvents.cs (императивная часть на языке C#).
// начало файла Model.cs using System;
using System.Windows; using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace ImageViewer
{
class Model
{
//-------------------------------------------
public ImageSource Picture
{
get
{
if(exposeInverted)
{
return invertedPicture;
}
else
{
return picture;
}
}
}
//-------------------------------------------
public int PixelWidth
{
get
{
return picture.PixelWidth;
}
}
//-------------------------------------------
public int PixelHeight
{
get
42
{
return picture.PixelHeight;
}
}
//-------------------------------------------
public void LoadImage(string path)
{
BitmapImage bm = new BitmapImage
(
new Uri
(
path,
UriKind.Absolute
)
);
picture = bm;
invertedPicture = null; exposeInverted = false;
}
//-------------------------------------------
public void InvertImage()
{
if(picture == null)
{
return;
}
exposeInverted = !exposeInverted;
if (exposeInverted &&
(invertedPicture == null))
{
MakeInverted();
}
}
//-------------------------------------------
private void MakeInverted()
{
invertedPicture =
new WriteableBitmap(picture); invertedPicture.Lock();
43
int width = invertedPicture.PixelWidth; int height = invertedPicture.PixelHeight;
unsafe
{
UInt32* buffer = (UInt32*)
invertedPicture.BackBuffer;
int stride =
invertedPicture. BackBufferStride / 4;
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
buffer[x] =
GetInverted(buffer[x]);
}
buffer += stride;
}
}
invertedPicture.AddDirtyRect
(
new Int32Rect(0, 0, width, height) );
invertedPicture.Unlock();
} |
|
//------------------------------------------- |
|
private |
UInt32 GetInverted(UInt32 pixel) |
{ |
|
UInt32 |
r = (pixel & 0x00FF0000) >> 16; |
UInt32 |
g = (pixel & 0x0000FF00) >> 8; |
UInt32 |
b = (pixel & 0x000000FF); |
r = 255 - r; g = 255 - g; b = 255 - b;
return 0xFF000000 |
44
(r << 16) | (g << 8) | b;
}
//-------------------------------------------
private BitmapImage picture;
private WriteableBitmap invertedPicture;
private bool exposeInverted = false; //-------------------------------------------
}
}
// конец файла Model.cs
Особенностью модели является хранение двух версий изображения: исходного и инвертированного (негатива). При первом обращении за негативной версией она создается из исходного изображения. Применяются быстродействующие операции над пикселями «на месте», без дополнительного копирования растровых данных. Критичные действия с прямыми указателями в памяти заключены в unsafe-блоке программного кода, поэтому в свойствах проекта требуются специальные разре-
шения (Project Properties -> Build -> Allow unsafe code).
Модель определяет три интерфейсных свойства: Picture, PixelWidth
и PixelHeight, и два интерфейсных метода: LoadImage и InvertImage. Тип свойства Picture — абстрактный ImageSource, который является удобным для визуального отображения стандартными средствами WPF, например компонент Image, но в то же время в нужной степени закрывает внутреннюю реализацию (на самом деле изображения хранятся в переменных типов BitmapImage и WritableBitmap). Однако ImageSource
не определяет интерфейсных свойств для точных размеров изображения в пикселах (вместо этого используются приведенные экранные размеры, с учетом разрешения дисплея). Поэтому в модель дополнительно введены пиксельные размеры. Метод LoadImage принимает в качестве аргумента абсолютный путь к изображению. Обычно такого рода пути формируются с помощью стандартного системного диалога
45
открытия файлов (но модель, естественно, не осведомлена об этих способах). После вызова метода LoadImage свойство Picture должно содержать ссылку на загруженные растровые данные. После первого обращения к InvertImage свойство Picture будет содержать инвертированную версию изображения, а дальнейшие вызовы InvertImage действуют подобно переключателю, но уже без каких-либо операций над пикселями.
// начало файла ViewController.cs using System;
using System.Windows; using System.Windows.Media;
namespace ImageViewer
{
class ViewController
{
//-------------------------------------------
private Model model = new Model(); //-------------------------------------------
public ImageSource LoadImage(string path)
{
model.LoadImage(path);
return model.Picture;
}
//-------------------------------------------
public ImageSource InvertImage()
{
model.InvertImage();
return model.Picture;
}
//-------------------------------------------
public double PixelWidth
{
get
{
return model.PixelWidth;
}
}
//-------------------------------------------
public double PixelHeight
46
{
get
{
return model.PixelHeight;
}
}
//-------------------------------------------
}
}
// конец файла ViewController.cs
Особых пояснений код контроллера в этом простом примере не требует. Очевидно, что он является интерфейсным адаптером. Объект класса Model агрегирован в объект класса ViewController, поэтому контроллер управляет и временем жизни модели.
// начало файла ViewModel.cs using System;
using System.Windows; using System.Windows.Media; using System.Windows.Input;
namespace ImageViewer
{
class ViewModel : DependencyObject
{
//-------------------------------------------
public ViewController controller = new ViewController();
//-------------------------------------------
public static readonly DependencyProperty PictureProperty = DependencyProperty.Register
(
"Picture",
typeof(ImageSource),
typeof(ViewModel)
);
public ImageSource Picture
{
get
47
{
return
(ImageSource)
GetValue(PictureProperty);
}
set
{
SetValue(PictureProperty, value);
}
}
//-------------------------------------------
public static readonly
DependencyProperty PicturePathProperty = DependencyProperty.Register
(
"PicturePath",
typeof(string),
typeof(ViewModel)
);
public string PicturePath
{
get
{
return
(string)
GetValue(PicturePathProperty);
}
set
{
SetValue(PicturePathProperty, value);
}
}
//-------------------------------------------
public static readonly
DependencyProperty PictureWidthProperty = DependencyProperty.Register
(
"PictureWidth",
typeof(double),
typeof(ViewModel)
);
public double PictureWidth
{
48
get
{
return
(double)
GetValue(PictureWidthProperty);
}
set
{
SetValue
(
PictureWidthProperty, value
);
}
}
//-------------------------------------------
public static readonly DependencyProperty
PictureHeightProperty = DependencyProperty.Register
(
"PictureHeight",
typeof(double),
typeof(ViewModel)
);
public double PictureHeight
{
get
{
return (double) GetValue(PictureHeightProperty);
}
set
{
SetValue
(
PictureHeightProperty, value
);
}
}
//-------------------------------------------
public static RoutedCommand LoadImage =
new RoutedCommand();
49
public static ExecutedRoutedEventHandler ExecutedLoadImageHandler =
ViewCommands.ExecuteLoadImage; //-------------------------------------------
public static RoutedCommand InvertImage = new RoutedCommand();
public static ExecutedRoutedEventHandler ExecutedInvertImageHandler =
ViewCommands.ExecuteInvertImage; //-------------------------------------------
public static RoutedCommand ClickImage =
new RoutedCommand();
public static ExecutedRoutedEventHandler ExecutedClickImageHandler =
ViewCommands.ExecuteClickImage; //-------------------------------------------
}
}
// конец файла ViewModel.cs
Структура класса ViewModel определяется тем, что именно он взаимодействует с декларативными средствами на XAML. Несколько громоздкий синтаксис скрывает «слоты» (связываемые свойства) Picture, PicturePath, PictureWidth и PictureHeight. Механизмы связывания были подробно описаны ранее, здесь они не имеют каких-либо особенностей.
Кроме связываемых свойств, определены также статические поля трех команд: LoadImage, InvertImage и ClickImage. Команды связывают-
ся с обработчиками в декларативном коде, а в классе ViewModel для этого определены статические поля (соответственно, ExecutedLoadImageHandler, ExecutedInvertImageHandler и ExecutedClickImageHandler). Реализация этих делегатов содержится в классе ViewCommands.
Объект класса ViewController агрегирован в объект класса ViewModel. Эта переменная для сокращения программного текста объявлена как общедоступная (public) и не оформлена в виде свойства (property).
50
