Статьи

Пользовательская обрезка изображения в Windows Phone 7 — часть 2 из 2

В моей предыдущей статье я заложил основу для собственного механизма обрезки. Чтобы быть точным, я установил основной макет пользовательского интерфейса и показал, как можно открыть изображение для обрезки. Но это еще не все — теперь вам действительно нужно выполнить обрезку. И этот процесс сопровождается конкретными интересными аспектами, которые следует учитывать.

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

bool isMove = false;

Теперь, когда перевод выполнен, значения перевода по осям X и Y должны быть временно сохранены для манипуляции. Поэтому в заголовок класса я добавляю два поля:

int trX = 0;
int trY = 0;

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

Rectangle r;

Поэтому, прежде чем идти дальше, мне нужно отрегулировать размер LayoutRoot — основной сетки, в которой размещено изображение, чтобы я мог ссылаться на прямоугольник внутри сетки, поэтому используя те же координаты в верхней части изображения. Для этого в существующий метод SetPicture добавьте две строки в конце:

LayoutRoot.Height = image1.Height;
LayoutRoot.Width = image1.Width;

Теперь перейдем к самой простой части — переключению кнопок. Очевидно, что когда пользователь нажимает кнопку «Переместить», режим изменения размера отключается, и наоборот. Вот что у меня есть для этого:

private void btn_Click(object sender, EventArgs e)
{
IApplicationBarIconButton button;
if (isMove)
{
button = (IApplicationBarIconButton)ApplicationBar.Buttons[1];
button.IsEnabled = true;
button = (IApplicationBarIconButton)ApplicationBar.Buttons[0];
button.IsEnabled = false;
isMove = false;
}
else
{
button = (IApplicationBarIconButton)ApplicationBar.Buttons[1];
button.IsEnabled = false;
button = (IApplicationBarIconButton)ApplicationBar.Buttons[0];
button.IsEnabled = true;
isMove = true;
}
}

Посмотрите на XAML, который у меня есть для кнопок приложений (поскольку именно это я использую в качестве «панели переключателей»):

<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
<shell:ApplicationBarIconButton IconUri="/Images/appbar.download.rest.png" Text="Resize" x:Name="btnResize" Click="btn_Click" IsEnabled="False"/>
<shell:ApplicationBarIconButton IconUri="/Images/appbar.upload.rest.png" Text="Move" x:Name="btnMove" Click="btn_Click" IsEnabled="True"/>
<shell:ApplicationBarIconButton IconUri="/Images/appbar.check.rest.png" Text="Accept" Click="Accept_Click" />
<shell:ApplicationBarIconButton IconUri="/Images/appbar.close.rest.png" Text="Cancel" />
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

Обратите внимание на один интересный факт — я делю обработчик события Click между двумя кнопками режима. В Windows Phone 7 кнопки приложений не указываются по имени, а по индексу. Поэтому в самом обработчике событий я должен определить, какая кнопка была нажата, проверив поле isMove, которое устанавливается только этими кнопками. Смущает немного? Не беспокойся об этом — ты к этому привыкнешь.

В самом обработчике событий нет ничего особенного — все, что здесь делается, — это кнопки включения и отключения, одновременно устанавливающие значение isMove (как вы помните, оно показывает, включен ли режим Move или Resize).

Обработчик события ManipulationDelta прошел небольшой пересмотр. Вот:

void rect_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
    GeneralTransform gt = ((Rectangle)sender).TransformToVisual(LayoutRoot);
    Point p = gt.Transform(new Point(0, 0));

    int intermediateValueY = (int)((LayoutRoot.Height - ((Rectangle)sender).Height));
    int intermediateValueX = (int)((LayoutRoot.Width - ((Rectangle)sender).Width));
    Rectangle croppingRectangle = (Rectangle)sender;

    if (isMove)
    {
        TranslateTransform tr = new TranslateTransform();
        trX += (int)e.DeltaManipulation.Translation.X;
        trY += (int)e.DeltaManipulation.Translation.Y;

        if (trY < (-intermediateValueY /2))
        {
            trY = (-intermediateValueY /2);
        }
        else if (trY > (intermediateValueY/2))
        {
            trY = (intermediateValueY/2);
        }

        if (trX < (-intermediateValueX/2))
        {
            trX = (-intermediateValueX/2);
        }
        else if (trX > (intermediateValueX/2))
        {
            trX = (intermediateValueX/2);
        }

        tr.X = trX;
        tr.Y = trY;

        croppingRectangle.RenderTransform = tr;
    }
    else
    {
        if (p.X >= 0)
        {
            if (p.X <= intermediateValueX)
            {
                croppingRectangle.Width -= (int)e.DeltaManipulation.Translation.X;
            }
            else
            {
                croppingRectangle.Width -= (p.X - intermediateValueX);
            }
        }
        else
        {
            croppingRectangle.Width -= Math.Abs(p.X);
        }

        if (p.Y >= 0)
        {
            if (p.Y <= intermediateValueY)
            {
                croppingRectangle.Height -= (int)e.DeltaManipulation.Translation.Y;
            }
            else
            {
                croppingRectangle.Height -= (p.Y - intermediateValueY);
            }
        }
        else
        {
            croppingRectangle.Height -= Math.Abs(p.Y);
        }
    }
}

Это много кода для начала. Но позвольте мне разобрать это, поскольку части действительно довольно просты.

Прежде всего, мне нужно найти положение прямоугольника обрезки внутри сетки (в моем случае, LayoutRoot). В отличие от приложения WinForms, приложение WP7 не позволяет найти координаты элемента управления внутри другого элемента управления. Или, по крайней мере, для этого требуется немного больше усилий. И этот фрагмент делает это:

GeneralTransform gt = ((Rectangle)sender).TransformToVisual(LayoutRoot);
Point p = gt.Transform(new Point(0, 0));

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

int intermediateValueY = (int)((LayoutRoot.Height - ((Rectangle)sender).Height));
int intermediateValueX = (int)((LayoutRoot.Width - ((Rectangle)sender).Width));

Как для высоты (промежуточного значения) и ширины (промежуточного значения) промежуточное значение представляет собой разницу между высотой / шириной основной сетки (и, следовательно, изображения) и высотой / шириной прямоугольника обрезки.

В зависимости от текущего режима я использую перевод и изменение размера прямоугольника.

if (isMove)
{
TranslateTransform tr = new TranslateTransform();
trX += (int)e.DeltaManipulation.Translation.X;
trY += (int)e.DeltaManipulation.Translation.Y;

if (trY < (-intermediateValueY /2))
{
trY = (-intermediateValueY /2);
}
else if (trY > (intermediateValueY/2))
{
trY = (intermediateValueY/2);
}

if (trX < (-intermediateValueX/2))
{
trX = (-intermediateValueX/2);
}
else if (trX > (intermediateValueX/2))
{
trX = (intermediateValueX/2);
}

tr.X = trX;
tr.Y = trY;

croppingRectangle.RenderTransform = tr;
}
else
{
if (p.X >= 0)
{
if (p.X <= intermediateValueX)
{
croppingRectangle.Width -= (int)e.DeltaManipulation.Translation.X;
}
else
{
croppingRectangle.Width -= (p.X - intermediateValueX);
}
}
else
{
croppingRectangle.Width -= Math.Abs(p.X);
}

if (p.Y >= 0)
{
if (p.Y <= intermediateValueY)
{
croppingRectangle.Height -= (int)e.DeltaManipulation.Translation.Y;
}
else
{
croppingRectangle.Height -= (p.Y - intermediateValueY);
}
}
else
{
croppingRectangle.Height -= Math.Abs(p.Y);
}
}

Когда я перемещаю его, все, что мне нужно сделать, это убедиться, что прямоугольник все еще находится в пределах сетки. Говоря об этом, еще один способ перемещения прямоугольника — использование матриц (как это было сделано в одном из примеров Чарльза Петцольда), но TranslateTransform делает почти то же самое, поэтому нет необходимости в прямом преобразовании слоя.

Это немного сложнее, когда дело доходит до изменения размера прямоугольника. Его максимальный размер задан равным размеру изображения, но границы не определены, поэтому я должен проверять координаты X и Y при изменении размера прямоугольника. Они могут быть отрицательными (автоматически означает, что оно выходит за пределы), больше, чем промежуточное значение (также означает, что оно выходит за пределы) или между (означает, что прямоугольник находится в «хорошем положении»).

Как только прямоугольник будет правильно расположен и имеет правильный размер, вам придется его обрезать. Для этого я использую кнопку «Принять». Связанный с ним обработчик событий выглядит так:

private void Accept_Click(object sender, EventArgs e)
{
ClipImage();

WriteBitmap(LayoutRoot);

var image = new BitmapImage();
using (var local = new IsolatedStorageFileStream("myImage.jpg", FileMode.Open, IsolatedStorageFile.GetUserStoreForApplication()))
{
image.SetSource(local);
}

WriteDummyImage(image);
}

Здесь ClipImage фактически выберет нужную область (определенную прямоугольником) и преобразует изображение так, чтобы выбранная часть была перемещена в левый верхний угол сетки:

void ClipImage()
{
RectangleGeometry geo = new RectangleGeometry();

r = (Rectangle)(from c in LayoutRoot.Children where c.Opacity == .5 select c).First();
GeneralTransform gt = r.TransformToVisual(LayoutRoot);
Point p = gt.Transform(new Point(0, 0));
geo.Rect = new Rect(p.X, p.Y, r.Width, r.Height);
image1.Clip = geo;
r.Visibility = System.Windows.Visibility.Collapsed;

TranslateTransform t = new TranslateTransform();
t.X = -p.X;
t.Y = -p.Y;
image1.RenderTransform = t;
}

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

Кроме того, из-за ограничений безопасности я не могу напрямую извлечь изображение из элемента управления Image, поэтому мне нужно это обойти.

WriteBitmap сохранит «скриншот» в изолированном хранилище для дальнейшей обработки:

void WriteBitmap(FrameworkElement element)
{
WriteableBitmap wBitmap = new WriteableBitmap(element, null);

using (MemoryStream stream = new MemoryStream())
{
wBitmap.SaveJpeg(stream, (int)element.Width, (int)element.Height, 0, 100);

using (var local = new IsolatedStorageFileStream("myImage.jpg", FileMode.Create, IsolatedStorageFile.GetUserStoreForApplication()))
{
local.Write(stream.GetBuffer(), 0, stream.GetBuffer().Length);
}
}
}

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

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

private void WriteDummyImage(BitmapImage image)
{
Image imageC = new Image();
imageC.Stretch = Stretch.None;
imageC.Source = image;
imageC.Height = r.Height;
imageC.Width = r.Width;

WriteBitmap(imageC);
}

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

Обрезанное изображение теперь там, и вы можете получить к нему доступ в любое время!

Кстати, я не налагал ограничений на минимальный размер прямоугольника обрезки из-за того, что в некоторых регионах требуется выделение очень маленьких сечений, поэтому вам нужно установить их самостоятельно, если у вас есть минимальный предел размера области обрезки , Это можно сделать в ManipulationDelta. Кроме того, обрезанное изображение зависит от фактического размера элемента управления изображением, а не от самого изображения. Поиграйте с режимом Stretch, если вам нужны разные размеры обрезки.

Вы можете скачать пример проекта, над которым я работал, здесь .