Windows Phone – 裁剪图片 (Crop Image)

原文:Windows Phone – 裁剪图片 (Crop Image)

最近在处理图像的功能,对於图像的比例我也不是非常的清楚,因此,在编辑图片上花了不少时间。

该篇文章主要说明的是:如何对图片选择需要的范围进行裁剪(Crop Image)

如果App里想要用到图像,通常我们会使用PhotoChooserTask来取得图像并加上一个Image Control显示内容,

程式码如下:

void AddImageBtn_Click(object sender, RoutedEventArgs e)

{

    // 呼叫PhotoTask取得指定的图示

    PhotoChooserTask tTask = new PhotoChooserTask();

    tTask.Completed += PhotoTask_Completed;

    tTask.Show();

}

?

void PhotoTask_Completed(object sender, PhotoResult e)

{

    if (e.Error != null)

        MessageBox.Show(e.Error.Message, "Error", MessageBoxButton.OK);

    else

    {

        if (e.ChosenPhoto == null) return;

        BitmapImage tBitMap = new BitmapImage();

        tBitMap.SetSource(e.ChosenPhoto);

        image1.Source = tBitMap;

    }

}

但是这样的内容就跟原来选到的内容是一样的,那如果我只想要某一块的内容呢?就需要加点程式来处理了。

?

参考<Custom image cropping in Windows Phone 7 - Part 1 of 2>与<Custom image cropping in Windows Phone 7 - Part 2 of 2>

这两篇的内容说明如何透过手指要裁剪的指定范围,其主要的概念分成几个部分:

?

(1) 采用整个LayoutRoot为底图,上方加入一个Image控件;并且加入4个按钮

???? 最重要的是该Image控件,定义了要裁剪的对象;透过PhotoChooserTask取得要裁剪的图像;

???? 4个按钮,其任务为了裁剪图像。

?

?

(2) 绘制一个方框,搭配手指移动调整要裁剪的范围

???? 增加透过手指的Touch输入,调整Rectangle的大小,搭配其中一个Accept的按钮执行透过该Rectangle裁剪图像。

???? 首先,根据参考文件的Part 1介绍了透过手指的滑动,调整Rectangle的大小范围,如下:

void SetPicture()

{

    Rectangle rect = new Rectangle();

    rect.Opacity = .5;

    rect.Fill = new SolidColorBrush(Colors.White);

    rect.Height = image1.Height;

    rect.MaxHeight = image1.Height;

    rect.MaxWidth = image1.Width;

    rect.Width = image1.Width;

    rect.Stroke = new SolidColorBrush(Colors.Red);

    rect.StrokeThickness = 2;

    rect.Margin = image1.Margin;

    rect.ManipulationDelta += new EventHandler<ManipulationDeltaEventArgs>(rect_ManipulationDelta);

?

    LayoutRoot.Children.Add(rect);

}

?

void rect_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)

{

    // 实现移动时,手指往外移动范围加大;往内移动范围缩小的效果。

    Rectangle r = (Rectangle)sender;

    // 利用 -= 的方式来调整

    r.Width -= e.DeltaManipulation.Translation.X;

    r.Height -= e.DeltaManipulation.Translation.Y;            

}

?????? 里面滑动的事件主要注册了ManipulationDelta,该事件为主要操作的Trigger,往下先补充有关Rectangle的操作说明。

???

载入图片不是最重要的部分,最重要是怎麽操作Rectangle与最後根据Rectangle的大小对应至图片中的范围进行裁剪,

往下先针对Rectangle的操作加以说明:

?

Rectangle

??? 绘制一个矩形的类别,可以具有Stoke(笔触)与Fill(填充)。

类型 名称 说明
Properties Opacity 设定或取得对象的透明程度。
Properties Fill 设定或取得如何绘制内部ShapeBrush。 (Inherited from Shape.)
Properties Stroke 设定或取得如何绘制指定Shape轮廓的Brush。 (Inherited from Shape.)
Properties StrokeThickness 设定或取得如何绘制指定Shape笔划轮廓的宽度。 (Inherited from Shape.)
Event ManipulationDelta 发生於当输入设备(例如:手指)开始操作UIElement时。(Inherited from UIElement.)

??? 透过ManipulationDelta负责处理当input device(输入设备)开始操作的UIElement开始,去修改目前Rectangle的大小,

进一步去调整要裁剪的范围。对於如何处理ManipulationDelta的事件,参考<How to: Handle Manipulation Events>来加以说明:

?

透过控制Manipulation事件对於touch与multitouch输入产生回应,进而对物件进行move、scale的调整。该事件由UIElement所提供。

在Windows Phone里manipulation events支援三种类型:

?ManipulationCompleted

??? 该事件发生於manipulation与inertia(惯性)完成时。

?

?ManipulationStarted

??? 该事件发生於当有input device (例如:touch)开始在UIElement进行manipulation。

?

?ManipulationDelta

??? 该事件发生於当input device在操作UIElment过程中改变了位置。例如:touch该UIElement时,由A这个位置移动到另一个位置。

??? 该事件在操作期间会发生多次,主要是因为touch deivce未离开UIElement之前该事件的触法会一直存在。

??? 那麽在操作时程式要怎麽处理呢,如下范例:

public Page2()

{

    InitializeComponent();

    // 初始化

    Initialization();

}

?

private TransformGroup transformGroup;

private TranslateTransform translation;

private ScaleTransform scale;

?

private void Initialization()

{

    this.transformGroup = new TransformGroup();

    this.translation = new TranslateTransform();

    this.scale = new ScaleTransform();

    // 注册要处理的效果 transliation与scale

    this.transformGroup.Children.Add(this.scale);

    this.transformGroup.Children.Add(this.translation);

    this.rectangle.RenderTransform = this.transformGroup;

    // 注册manipulation的事件

    this.ManipulationDelta += this.PhoneApplicationPage_ManipulationDelta;

}

?

private void PhoneApplicationPage_ManipulationDelta(object sender, 

    System.Windows.Input.ManipulationDeltaEventArgs e)

{

    // Scale the rectangle.

    //this.scale.ScaleX *= e.DeltaManipulation.Scale.X;

    //this.scale.ScaleY *= e.DeltaManipulation.Scale.Y;

?

    // Move the rectangle.

    this.translation.X += e.DeltaManipulation.Translation.X;

    this.translation.Y += e.DeltaManipulation.Translation.Y;

}

????? 在ManipulationDelta事件中,透过参数ManipluationDeltaEventArgs取得操作的效果值,配合公式完成UIElement的移动与Scale调整。

????? 更多有关手势操作的内容可以参考<Windows Phone 7 - 浅谈手势(Gestures)运作>的说明。

?

?

(3) 执行裁剪的重要逻辑,保持移动与缩放的值,重新计算实际要剪下的范围

????? 在这个部分,根据原文的说明将Rectangle的移动与大小的计算分开,增加了许多变数来加以协助,往下便依步骤说明如何调整:

?

3-1. 增加变数协助逻辑计算

// 标记目前是否为移动的状态

private bool isMove = false;

// 暂存Translation的X与Y值

private double trX = 0;

private double trY = 0;

// 独立Rectangle用於保存目前要裁剪的范围

private Rectangle r;

?

3-2. 调整拥有Image的LayoutRoot.width/height与Image相同

void SetPicture()

{

    Rectangle rect = new Rectangle();

    rect.Opacity = .5;

    rect.Fill = new SolidColorBrush(Colors.White);

    rect.Height = image1.Height;

    rect.MaxHeight = image1.Height;

    rect.MaxWidth = image1.Width;

    rect.Width = image1.Width;

    rect.Stroke = new SolidColorBrush(Colors.Red);

    rect.StrokeThickness = 2;

    rect.Margin = image1.Margin;

    rect.ManipulationDelta += new EventHandler<ManipulationDeltaEventArgs>(rect_ManipulationDelta);

?

    LayoutRoot.Children.Add(rect);

?

    // 增加整个LayoutRoot的width/height与image相同,

    // 这样Rectangle就可以使用与image相同的coordinate。

    LayoutRoot.Width = image1.Width;

    LayoutRoot.Height = image1.Height;

}

??? 在原有的SetPicture()加入两行指令,让LayoutRoot的width、height与Image相同,这样LayoutRoot才可以与Image的coordinate对齐。

??? 进行裁剪时才能对在相同的Point上。

?

3-3. 调整ManipulationDelta的事件处理逻辑

void rect_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)

{

    // 利用Rectangle的建立通用的Transform

    GeneralTransform gt = ((Rectangle)sender).TransformToVisual(LayoutRoot);

    Point p = gt.Transform(new Point(0, 0));

?

    // 计算LayoutRoot与Rectangle的间距:X、Y

    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;

?

        // 识别移动後的Y是否小於间距范围

        // True:给予最小参数

        // False:大於间距范围,给予最大间距

        if (trY < (-intermediateValueY / 2))

        {

            trY = (-intermediateValueY / 2);

        }

        else if (trY > (intermediateValueY / 2))

        {

            trY = (intermediateValueY / 2);

        }

?

        // 识别移动後的X是否小於间距范围

        // True:给予最小参数

        // False:大於间距范围,给予最大间距

        if (trX < (-intermediateValueX / 2))

        {

            trX = (-intermediateValueX / 2);

        }

        else if (trX > (intermediateValueX / 2))

        {

            trX = (intermediateValueX / 2);

        }

?

        // 修改为新的X,Y

        tr.X = trX;

        tr.Y = trY;

        croppingRectangle.RenderTransform = tr;

    }

    else

    {

        // 负责大小缩放的逻辑,>=0 代表手指往右移动,相反的往左移动,修改Width

        if (p.X >= 0)

        {

            // 识别此次移动的X, 是否小於等於区间范围

            if (p.X <= intermediateValueX)

            {

                croppingRectangle.Width -= (int)e.DeltaManipulation.Translation.X;

            }

            else

            {

                croppingRectangle.Width -= (p.X - intermediateValueX);

            }

        }

        else

        {

            croppingRectangle.Width -= Math.Abs(p.X);

        }

?

        // 负责大小缩放的逻辑,>=0 代表手指往下移动,相反的往上移动,修改Height

        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);

        }

    }

    //

}

??? 此段的逻辑相对复杂许多,主要是搭配Rectangle透过TransformToVisual(LayoutRoot)将LayoutRoot的座标转换为指定的视觉物件。

??? 再透过GeneralTransform重新取得目前移动後的Point;

??? 接着计算此次移动所造成LayoutRoot与Rectangle在X、Y的间距,接着识别目前是为移动模式还是缩放模式,进一步依手指移动的范围

??? 进行X、Y(则建立一个新的TranslateTransform根据计算移动的范围进行调整)或Width、Height(直接调整Rectangle物件)。

?

3-4. 利用Rectangle的范围进行裁剪图片

/// <summary>

/// 利用Rectangle取得Image要调整至新大小的范围。

/// </summary>

void ClipImage()

{            

    // 取得画面上的Rectangle

    r = (Rectangle)(from c in LayoutRoot.Children where c.Opacity == .5 select c).First();

?

    // 利用Rectangle建立RectangleGeometry指定要用来裁剪的大小

    RectangleGeometry geo = new RectangleGeometry();

    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);

    

    // 对image进行裁剪

    image1.Clip = geo;

    

    r.Visibility = System.Windows.Visibility.Collapsed;

?

    // 将image移动到裁剪的座标

    TranslateTransform t = new TranslateTransform();

    t.X = -p.X;

    t.Y = -p.Y;

    image1.RenderTransform = t;

}

??? 先取得Rectangle物件,再一次使用GeneralTransform取得LayoutRoot的座标转换为指定的视觉物件後,建立一个RectangleGeometry

??? 将取得的座标指定给该矩形类别,并且指定它的维度。接着指定image.Clip的值,让image进行裁剪形成我们选择的范围

??? 最後搭配TrasnslateTransform移动裁剪好的图示至预设位置。

??? 裁剪好的图示要怎麽储存呢,请参考下方的程式内容:

/// <summary>

/// 将画面撷取下来产生档案。

/// </summary>

/// <param name="element"></param>

void WriteBitmap(FrameworkElement element, string filename)

{

    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(filename, 

            FileMode.Create, IsolatedStorageFile.GetUserStoreForApplication()))

        {

            local.Write(stream.GetBuffer(), 0, stream.GetBuffer().Length);

        }

    }

}

?

GeneralTransform

??? 提供物件的通用转换支援,例如点和矩形。这是个抽象类别。本篇用到Transform()方法转换指定的点,然後传回结果。

?

TranslateTransform

??? 平移(移动)物件2D x-y座标系统。

?

RectangleGeometry

??? 描述二维矩型的类别。Rect属性用於设定/取得矩形的维度。

?

Image

??? 负责处理图像的显示、调整与效果。针对本篇用到的项目加以说明:

类型 名称 说明
Properties Clip Gets or sets the Geometry used to define the outline of the contents of a UIElement. (Inherited from UIElement.)
Properties RenderTransform Gets or sets transform information that affects the rendering position of a UIElement. (Inherited from UIElement.)

?

=====

以上为说明如何实作Crop Image的方式,但是如果参考原文的程式码,它是限制在固定的image物件width/height,

对於实际图像大小有些差异,所以我做了一个简单的调整:

?

(1) 调整放入图片时将原始图片加上萤幕比例的Size调整

void task_Completed(object sender, PhotoResult e)

{

    BitmapImage image = new BitmapImage();

    image.SetSource(e.ChosenPhoto);

    image1.Source = image;

    // 增加图像适用萤幕比例

    Fit(image.PixelWidth, image.PixelHeight);

    SetPicture();

}

?

/// <summary>

/// 依画面大小进行图像的比例缩放。

/// </summary>

/// <param name="width">图像width</param>

/// <param name="height">图像height</param>

private void Fit(double width, double height)

{

    double tScreenWidth = Application.Current.Host.Content.ActualWidth; 

    double tScreenHeight = Application.Current.Host.Content.ActualHeight;

    // 图像比例换算

    image1.Height = (tScreenWidth / width) * height;

    image1.Width = tScreenWidth;

}

记得要将xaml中指定image的width/height给去掉喔。

?

(2) 修改储存图片的方式,改为直接储存图像而不是全画面

/// <summary>

/// 储存实际图片的方法。

/// </summary>

/// <param name="element"></param>

void WriteBitmap(Image element)

{

    int tWidth = (int)element.Clip.Bounds.Width;

    int tHeight = (int)element.Clip.Bounds.Height;

    // 重新绘制一个新的WriteableBitmap

    WriteableBitmap wBitmap = new WriteableBitmap(tWidth, tHeight);

    // 加上图像来源与使用的RenderTransform

    wBitmap.Render(element, element.RenderTransform);

    wBitmap.Invalidate();

?

    using (MemoryStream stream = new MemoryStream())

    {

        wBitmap.SaveJpeg(stream, tWidth, tHeight, 0, 100);

?

        using (var local = new IsolatedStorageFileStream("myImage1.jpg",

            FileMode.Create, IsolatedStorageFile.GetUserStoreForApplication()))

        {

            local.Write(stream.GetBuffer(), 0, stream.GetBuffer().Length);

        }

    }

}

?

[范例程式]

Windows Phone – 裁剪图片 (Crop Image)Windows Phone – 裁剪图片 (Crop Image)Windows Phone – 裁剪图片 (Crop Image)

Page3.xaml为实际的范例程式。

?

[补充]

1. Rotate transforms are not supported on Windows Phone.

2. Manipulation events are supported by default on Windows Phone, so the IsManipulationEnabled property is not supported.

3. Inertia events are not supported in this release of Silverlight for Windows Phone.

4. 在Windows Phone里gesture events支援三种类型:

?? ?Tap:发生於当在该UIElement执行了Tag gesture。

?? ?DoubleTap:发生於当在该UIElement执行了DoubleTap gesture。

?? ?Hold:发生於当在该UIElement执行了Hold gesture。

======

本篇主要介绍在图像处理的撷取与编辑,不过里面有些内容可能不是解释的非常好,尤其是相关图像处理的部分,请大家多多包涵。

如果有写错的地方也请大家加以指导,谢谢。

?

References

?Custom image cropping in Windows Phone 7 - Part 1 of 2

?Custom image cropping in Windows Phone 7 - Part 2 of 2

?How to Crop an Image using the WriteableBitmap class?

?Silverlight Control to Crop Images

?Cropping an image in Silverlight 3

?How to cut a part of image in C#

?Cropping Images in Windows Phone 7 & Custom Image Cropping - Windows Phone 7

?Cropping an image stored locally

?Cropping and Scaling Images

?Gesture Support for Windows Phone & Windows Phone 7 Gestures

?WPF: How to apply a GeneralTransform to a Geometry data and return the new geometry?

?[Silverlight入门系列]用TransformToVisual和Transform取得元素绝对位置(Location)

?Silverlight - How to load and clip an image in code

?Cropping Images in Windows Phone 7 (重要重新绘制writeablebitmap)

?

Dotblogs 的标签: Windows Phone

enjoy developing application and learning new technology time.

Windows Phone – 裁剪图片 (Crop Image)

DotBlogs Tags:

Windows Phone

posted on
2013/6/24 22:15
|

我要推荐

|
阅读数 : 1575

| 文章分类 [

Windows Phone

]

|
订阅

上一篇:基于 Qt的聊天工具


下一篇:js 计算两个时间差