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的操作加以說明:
繪製一個矩形的類別,可以具有Stoke(筆觸)與Fill(填充)。
類型 | 名稱 | 說明 |
Properties | Opacity | 設定或取得對象的透明程度。 |
Properties | Fill | 設定或取得如何繪製內部Shape的Brush。 (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支援三種類型:
該事件發生於manipulation與inertia(慣性)完成時。
該事件發生於當有input device (例如:touch)開始在UIElement進行manipulation。
該事件發生於當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);
}
}
}
提供物件的通用轉換支援,例如點和矩形。這是個抽象類別。本篇用到Transform()方法轉換指定的點,然後傳回結果。
平移(移動)物件2D x-y座標系統。
描述二維矩型的類別。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);
}
}
}
[範例程式]
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
‧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)