WP7模擬器土砲鏡頭DIY
9 |
截至Windows Phone Developer Tools Beta版為止,WP7模擬器還很陽春,功能有限。而我一直有個想法,模擬器何不利用PC Webcam來模擬拍照動作? 這樣不是更逼真有趣嗎?
前陣子,在MSDN CODING4FUN上讀到一篇介紹WP7照片後製程式的有趣文章(PicFx),其中展示了利用照相功能取得影像的程式寫法,勾起了貪玩念頭--反正手邊有Webcam,不如就為WP7模擬器安裝一個土砲鏡頭吧!
要幫WP7模擬器搞個土砲攝影鏡頭並不難,首先我們需要一個WinForm程式(姑且稱它WebCam Gateway好了),執行簡單的HTTP Server功能(先前提過用100行C#寫的MicroHttpSever應可勝任),它負責接受特定Port傳來的HTTP Request,並將當下Webcam擷取的影像轉成圖檔傳回去。而在WP7程式端,透過WebClient物件便可發出HTTP Request取回圖檔,跟CameraCaptureTask.Completed 事件的運作模式差不多,如此就能將兩邊整合在一起。
WebCam Gateway 程式
WebCam Gateway程式只有兩大使命: 從Webcam擷取影像並擔任HTTP伺服器! 提到Webcam程式開發,網路上查到較簡便的做法是利用WIA元件,只可惜Windows Vista起移除了WIA的影像支援功能,建議改用WPD API。我對Webcam一無所知,WPD API看起來又頗複雜,索性偷懶,在DirectShow.NET程式庫中,找了一個現成範例--DxSnap,正好示範了由Webcam抓圖片的程式寫法。打開DxSnap專案,加入MicroHttpServer再稍做修改,WebCam Gateway就大功告成了。
圖: 用IE輸入特定URL,測試從WebCam Gateway取回照片 (點圖放大)
CameraProxy類別
到目前為止,我們已可透過瀏覽器取得Webcam影像檔,餘下來的工作,就是在WP7程式裡用WebClient模擬瀏覽器行為即可。我寫了一個CameraProxy類別將連線外部HTTP伺服器、取回圖檔等細節都封裝起來,並設法讓它的行為愈像CameraCaptureTask愈好。因此它也具有一個Show()方法,一個Completed(object sender, PhotoResult e)事件,介面規格幾乎跟CameraCaptureTask一模一樣,甚至我們可直接共用原本CameraCaptureTask.Completed的事件處理程式來接收WebCam Gateway傳回的圖檔,減少程式配合修改的幅度。但很不幸地,PhotoResult.ChosenPhoto屬性被設成唯讀,我們無法直接用它傳回Webcam影像檔! 我的解法是另外宣告一個PhotoResultHacking類別繼承PhotoResult,並在其中宣告一個CameraPhotoStream屬性,在CameraCaptureTask.Completed事件中,便可透過偵測參數型別與轉型取出圖檔資料。
以下是CameraProxy程式碼:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.Phone.Tasks;
using System.IO;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace Darkthread
{
public class CameraProxy
{
private string gatewayUrl = null;
private IApplicationBar appBar = null;
private Border mask = new Border()
{
Background = new SolidColorBrush(Colors.White),
Opacity = 0.7,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Visibility = Visibility.Collapsed
};
//Constructor, url for WebCam Gateway, page for UI blocking
public CameraProxy(string url, PhoneApplicationPage page)
{
gatewayUrl = url;
if (page != null)
{
appBar = page.ApplicationBar;
//Try to get LayoutRoot element
Panel pnl =
VisualTreeHelper.GetChild(page, 0) as Panel;
//If the LayoutRoot is a container
if (pnl != null) //Add a mask element
pnl.Children.Add(mask);
else
mask = null;
}
}
//Completed event compatible with CameraCaptureTask
public event Action<object, PhotoResult> Completed;
public void Show()
{
WebClient wc = new WebClient();
wc.OpenReadCompleted += (sender, e) =>
{
try
{
//e.Result is the stream that contains image we need
PhotoResultHacking pr = new PhotoResultHacking(
Microsoft.Phone.Tasks.TaskResult.OK, e.Result);
if (Completed != null)
Completed(this, pr);
}
catch
{
//If any exception, return TaskResult.Cancel
PhotoResult epr = new PhotoResult(TaskResult.Cancel);
if (Completed != null)
Completed(this, epr);
}
finally
{
//Remove the blocking mask
if (mask != null)
mask.Visibility = Visibility.Collapsed;
//Show the ApplicationBar
if (appBar != null)
appBar.IsVisible = true;
}
};
//Add a mask to block the UI
if (mask != null)
mask.Visibility = Visibility.Visible;
//Hide ApplicationBar to block the UI
if (appBar != null)
appBar.IsVisible = false;
wc.OpenReadAsync(new Uri(gatewayUrl));
}
}
//A **compatible** PhotoResult providing writable property
public class PhotoResultHacking : PhotoResult
{
public Stream CameraPhotoStream;
public PhotoResultHacking(TaskResult result, Stream stream) :
base(result)
{
CameraPhotoStream = stream;
}
}
}
使用說明
現在來稍微調整一下PicFx專案,示範如何使用CameraProxy。
首先,在Initialize()建立CameraProxy物件,並偷CameraCaptureTask原本的Completed的事件處理函數來用。
//**CameraProxy**
Darkthread.CameraProxy cameraProxy;
private void Initialize()
{
// Init tasks
cameraCaptureTask = new CameraCaptureTask();
cameraCaptureTask.Completed += PhotoProviderTaskCompleted;
//**CameraProxy**
//Create CameraProxy object and shared the same event handler
//with cameraCaptureTask.Completed
//PS: 192.168.1.136 is the local IP address of host
cameraProxy = new Darkthread.CameraProxy(
"http://192.168.1.136:1688/", this);
cameraProxy.Completed += PhotoProviderTaskCompleted;
在原本專案裡,照相鈕在模擬器模式下是被停用的,我們修改程式解開封印。
if (btn != null)
{
//**CameraProxy**
//btn.IsEnabled = Microsoft.Devices.Environment.DeviceType
// != DeviceType.Emulator;
btn.IsEnabled = true;
}
當照相鈕被按下時,程式會判斷是否處於模擬器狀態,決定要呼叫cameraProxy.Show()還是cameraCaptureTask.Show()。而cameraProxy, cameraCaptureTask, 及photoChooserTask在選完圖檔或照完相後都交由同一個事件函數PhotoProviderTaskCompleted處理,因此在其中我們需加入額外邏輯: 當圖片由cameraProxy傳回時,要將result參數轉型成PhotoResultHacking,再透過CameraPhotoStream屬性取出影像檔。
還有最後一部分要調,改得有點醜但也莫可奈何。原因是CameraCaptureTask及PhotoChooser是由OS掌管的作業,當挑選圖檔或照相時,我們的應用程式會被中斷,在取得影像資料後,程式才會再次啟動,並觸發 Initialize()及LayoutUpdated()等初始化事件。然而,要從WP7程式內部去中止自己再重新啟動看來是不可能的任務,因此我們無法模擬出跟CameraCaptureTask及PhotoChooser完全一樣的行為,原本安排在初始事件中的邏輯就漏跑了。解決之道是另外將原本預期在Initialize()及LayoutUpdated()裡要執行的邏輯抽取出來,接在取得CameraProxy影像檔後執行。
//**CameraProxy** Add a flag to identify if it's in emulator mode
private bool EmulatorMode
{
get
{
return Microsoft.Devices.Environment.DeviceType
== DeviceType.Emulator;
}
}
private void ApplicationBarIconCameraButton_Click(object sender, EventArgs e)
{
//Trigger CameraCaptureTask or CameraProxy depends on EmulatorMode flag
if (EmulatorMode)
cameraProxy.Show();
else
cameraCaptureTask.Show();
}
private void PhotoProviderTaskCompleted(object sender, PhotoResult e)
{
// Load the photo from the task result
if (e != null && e.TaskResult == TaskResult.OK)
{
var bmpi = new BitmapImage();
//**CameraProxy** add branch code for CameraProxy
bool extCamera = (EmulatorMode &&
e is Darkthread.PhotoResultHacking);
if (extCamera)
bmpi.SetSource(
((Darkthread.PhotoResultHacking)e).CameraPhotoStream);
else
bmpi.SetSource(e.ChosenPhoto);
original = new WriteableBitmap(bmpi);
//**CameraProxy**
//Real CameraCaptureTask will terminate the application
//and cause Loaded event, but CameraProxy won't,
//so we have to add the logic after camera capture here.
//Ugly, but seems no better way
if (extCamera)
{
ShowImage(original);
ResizeAndShowImage(original);
}
}
}
好了,現在我們的WP7模擬器可以真的拿來照相了,很酷吧!
WebCam Gateway的原始碼、CameraProxy.cs及PicFx專案中被修改過的MainPage.xaml.cs,可以由這裡下載,有興趣的朋友可以拿回去玩,並歡迎提供回饋意見。
Comments
# by 小中中
光土砲兩個字就該按個讚了~~~!!! XD
# by Andy
不知道 要怎麼連絡你? 有MSN之類的東西嗎? 有些問題想請問一下
# by kobis
想請問黑大 目前wp7有開放鏡頭的相關api嗎? 想試著把黑大的這組程式碼轉成使用HD7本身鏡頭
# by Jeffrey
to kobis, 用Silverlight開發的話,我目前知道只能透過CameraCaptureTask.Show()觸發照相功能,然後在Completed事件中收照片,似乎沒有其他進一步操控鏡頭的做法。
# by DARK
黑大妳好,我執行你寫的程式結果出現"Init Failed:找不到中介篩選器的組合,所以無法連接",小弟不才,請大大指教~^^"
# by Jeffrey
to DARK, 這種訊息看起來較像Webcam的相關程式沒有安裝好,重新安裝看看,並使用其他軟體(例如: MSN, Live Messenger)驗證看看Webcam的功能是否正常。
# by DARK
感謝黑大的指教,目前問題好像出在於需要用虛擬WEBCAM才能使用,想請問黑大用的是哪款大廠的WEBCAM
# by Jeffrey
to DARK, 我用的WebCam是Microsoft LifeCam VX-7000。( 影片中WebCam Gateway程式的標題列也有顯示出來,可以當成佐證,:P )
# by Amos
感謝版主 非常有幫助的文章!!