我想,自從Tech-Days後,不管是程式設計師或是IT人員,對小光都已經不陌生,這個可愛的小女生也正是Silverlight 4技術的代言人,據說,
她還有個巒生妹妹叫月澤光的,不過傳說畢竟只是傳說。
可以拖移的小光
文/黃忠成
關於藍澤光
我想,自從Tech-Days後,不管是程式設計師或是IT人員,對小光都已經不陌生,這個可愛的小女生也正是Silverlight 4技術的代言人,據說,
她還有個巒生妹妹叫月澤光的,不過傳說畢竟只是傳說。
實作可以用滑鼠拖移的小光
在這篇文章中,筆者將帶領讀者實作一個簡單的Silverlight應用程式,我們將使用小光的SD版,讓使用者可以用滑鼠拖著小光跑。
開始前,請準備好你的開發環境,本文將使用Visual Studio 2010+Expression Blend4來實作,請注意,小光是四女,Visual Studio 2010內建的是三女,
完成安裝後,請開啟Visual Studio 2010,選擇新增專案。
圖001
著透過Expression Blend 4來開啟MainPage.xaml。
圖002
於此工具中,於方案上點選右鍵,加入新項目,此處我選擇的是小光的SD版圖形。
圖003
圖004
接著點選001.png,然後拖移到畫布上,此時就會出現圖005的畫面。
圖005
為了讓小光有更多的空間能自由移動,請點選左下角的LayoutRoot,然後將其先轉為Canvas。
圖006
接著點選UserControl,再放大UserControl的大小。
圖007
於SD小光對於我們的螢幕來說,還是大隻了點,我們將其縮小一點,最後替這個圖形控制項取個名字(Hikaru)。
圖008
到此,我們已經完成了UI部份的設計,現在要加入拖移小光的程式碼,其實邏輯很簡單,當使用者在小光上面按下滑鼠左鍵時,程式必須進入拖移狀態,
此時必須捕捉全域的滑鼠訊息,這可以透過UIElement.CaptureMouse函式完成,當使用者按著滑鼠左鍵移動滑鼠時,就視為拖移動作,此時小光必須跟著
滑鼠指標移動,也就是必須要在MouseMove事件中依據滑鼠的位置來設定小光的位置,當使用者放開左鍵時,就視為拖移結束,此時要呼叫
ReelaseMouseCapture來取消捕捉全域滑鼠訊息。
程式1
MainPage.xaml |
<UserControl x:Class="MoveHikaru.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Height="498" Width="851"> <Canvas x:Name="LayoutRoot" Background="White"> <Image Name="Hikaru" HorizontalAlignment="Left" Source="001.png" Stretch="Fill" Width="50" Height="59" VerticalAlignment="Top" Canvas.Left="73" Canvas.Top="61" /> </Canvas> </UserControl> |
MainPage.xaml.cs |
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; namespace MoveHikaru { public partial class MainPage : UserControl { private Point _startPoint = default(Point); private bool _isMouseCaptured = false; public MainPage() { InitializeComponent(); Hikaru.MouseLeftButtonDown += new MouseButtonEventHandler(Hikaru_MouseLeftButtonDown); Hikaru.MouseLeftButtonUp += new MouseButtonEventHandler(Hikaru_MouseLeftButtonUp); Hikaru.MouseMove += new MouseEventHandler(Hikaru_MouseMove); } void Hikaru_MouseMove(object sender, MouseEventArgs e) { if (_isMouseCaptured) { UIElement element = sender as UIElement; Canvas.SetLeft(element, (e.GetPosition(this).X - _startPoint.X)); Canvas.SetTop(element, (e.GetPosition(this).Y - _startPoint.Y)); } } void Hikaru_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { UIElement element = sender as UIElement; element.ReleaseMouseCapture(); _isMouseCaptured = false; } void Hikaru_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { UIElement element = sender as UIElement; _startPoint = e.GetPosition(element); _isMouseCaptured = element.CaptureMouse(); } } } |
效果影片請見:
我們下次再見了。
Online 版,可拖移的小光
呃?,你還在呀?看來你應該跟我一樣,是那種在電影院看完電影後,會等到演員、製作班底字幕跑完的人,因為我們知道,
常常在最後會有一小段劇情,本文也是如此。
如果你先前有寫過Silverlight應用程式,應該知道,有個Behavior叫MouseDragElementBehavior,使用這個Behavior的話,我們
完全不用寫程式就能做出前例的效果,那為何我還要這麼做呢?其實我的目的是要做出Online版的拖移小光程式,效果如以下影片:
Http Duplex通訊
要完成影片中的效果,必然得使用WCF Service,當使用者A拖移小光時,呼叫WCF Service將小光的位置傳回Host,然後再由
Host透過WCF Duplex Push到使用者B,這樣才能讓兩個視窗同步,下面的程式是WCF Service的Server端介面定義。
IInteractiveService.cs |
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace MoveHikaru.Web { [ServiceContract(CallbackContract = typeof(IMoveCallback))] public interface IInteractiveService { [OperationContract] Guid Register(); [OperationContract] void Move(Guid id, double top, double left); } [ServiceContract] public interface IMoveCallback { [OperationContract(IsOneWay = true)] void MoveCallback(double top, double left); } } |
此處使用了WCF Duplex機制,定義當使用呼叫IInteractiveService時,可以順便送上一個實作IMoveCallback的物件,當Server端有需要時,
可以透過這個物件呼叫客戶端,以本例來說,當使用者A啟動時,會呼叫Register函式,此時Server會記錄其送上來的IMoveCallback物件,
然後回傳一個GUID做為識別碼,當使用者B啟動時也會重複相同的動作,當使用者A移動小光時,使用者A端會呼叫Move函式,
此時送上來的是X、Y值,還有其識別GUID,Server端會依據GUID來判別,只會呼叫使用者B的IMoveCallback物件的MoveCallback,
而不需要呼叫使用者A的IMoveCallback物件。
下面是實作碼:
InteractiveService.svc.cs |
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; using System.Threading; namespace MoveHikaru.Web { public class InteractiveService : IInteractiveService { private static Dictionary<Guid, IMoveCallback> _callbacks = new Dictionary<Guid, IMoveCallback>(); static InteractiveService() { } public Guid Register() { Guid id = Guid.NewGuid(); _callbacks.Add(id, OperationContext.Current.GetCallbackChannel<IMoveCallback>()); return id; } public void Move(Guid id,double top, double left) { ThreadPool.QueueUserWorkItem((state) => { foreach (var c in _callbacks) { if (!c.Key.Equals(id)) c.Value.MoveCallback(top, left); } }); } } } |
迴圈呼叫IMoveCallback物件,否則會有效能低落的情況發生,因此這裡直接把呼叫IMoveCallback物件的程式碼放到
Thread Pool中執行。
由於使用到了WCF Duplex機制,我們還需要添加System.ServiceModel.PollingDuplex為Reference,此檔案在
Program Files\Microsoft SDKs\Silverlight\v4.0\Libraries\Server目錄下。
圖009
完成後編譯整個方案,接著在Silverlight 專案上按右鍵,選擇Add Service Reference。
圖010
圖011
點一下探索即可找到Service,按下確定,此時會出現錯誤。
圖012
意思是預設的WCF Service是使用BasicHttpBinding,而這個Binding不支援Duplex機制,打開web.config來修正此問題。
web.config |
<?xmlversion="1.0"encoding="utf-8"?> <configuration> <system.web> <compilationdebug="true"targetFramework="4.0" /> </system.web> <system.diagnostics> <sources> <sourcename="System.ServiceModel“¡±"switchValue="Information, ActivityTracing" propagateActivity="true"> <listeners> <addname="traceListener" type="System.Diagnostics.XmlWriterTraceListener" initializeData= "c:\log\Traces.svclog" /> </listeners> </source> </sources> </system.diagnostics> <system.serviceModel> <extensions> <bindingExtensions> <addname="pollingDuplexHttpBinding" type="System.ServiceModel.Configuration.PollingDuplexHttpBindingCollectionElement,System.ServiceModel.PollingDuplex, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </bindingExtensions> </extensions> <bindings> <pollingDuplexHttpBinding> <bindingname="SingleMessagePerPollPollingDuplexHttpBinding" duplexMode="SingleMessagePerPoll" > </binding> </pollingDuplexHttpBinding> </bindings> <behaviors> <serviceBehaviors> <behaviorname=""> <serviceMetadatahttpGetEnabled="true" /> <serviceDebugincludeExceptionDetailInFaults="true" /> </behavior> </serviceBehaviors> </behaviors> <services> <servicename="MoveHikaru.Web.InteractiveService"> <endpointaddress=""binding="pollingDuplexHttpBinding"bindingConfiguration="SingleMessagePerPollPollingDuplexHttpBinding" contract="MoveHikaru.Web.IInteractiveService"/> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> <host> <baseAddresses> <addbaseAddress="http://localhost:1843/InteractiveService.svc"/> </baseAddresses> </host> </service> </services> <serviceHostingEnvironmentmultipleSiteBindingsEnabled="true" /> </system.serviceModel> </configuration> |
再試一次應該就能成功了(注意service區段的Namespace.ClassName要對好)。
客戶端的程式碼就比較簡單了,只是註冊,取得GUID,然後在拖移小光時將位置回報回Host而已。
MainPage.xaml.cs |
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; namespace MoveHikaru { public partial class MainPage : UserControl { private Point _startPoint = default(Point); private bool _isMouseCaptured = false; private ServiceReference1.InteractiveServiceClient _client = null; private Guid _clientID = Guid.Empty; public MainPage() { InitializeComponent(); EndpointAddress address = new EndpointAddress("http://localhost:1843/InteractiveService.svc"); PollingDuplexHttpBinding binding = new PollingDuplexHttpBinding(PollingDuplexMode.SingleMessagePerPoll); _client = new ServiceReference1.InteractiveServiceClient(binding, address); _client.MoveCallbackReceived += (s, args) => { Canvas.SetLeft(Hikaru, args.left); Canvas.SetTop(Hikaru, args.top); }; Hikaru.MouseLeftButtonDown += new MouseButtonEventHandler(Hikaru_MouseLeftButtonDown); Hikaru.MouseLeftButtonUp += new MouseButtonEventHandler(Hikaru_MouseLeftButtonUp); Hikaru.MouseMove += new MouseEventHandler(Hikaru_MouseMove); _client.RegisterCompleted += (s, args) => { _clientID = args.Result; }; _client.RegisterAsync(); } void Hikaru_MouseMove(object sender, MouseEventArgs e) { if (_isMouseCaptured) { UIElement element = sender as UIElement; Canvas.SetLeft(element, (e.GetPosition(this).X - _startPoint.X)); Canvas.SetTop(element, (e.GetPosition(this).Y - _startPoint.Y)); _client.MoveAsync(_clientID, Canvas.GetTop(Hikaru), Canvas.GetLeft(Hikaru)); } } void Hikaru_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { UIElement element = sender as UIElement; element.ReleaseMouseCapture(); _isMouseCaptured = false; _client.MoveAsync(_clientID, Canvas.GetTop(Hikaru), Canvas.GetLeft(Hikaru)); } void Hikaru_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { UIElement element = sender as UIElement; _startPoint = e.GetPosition(element); _isMouseCaptured = element.CaptureMouse(); } } } |
完成後編譯執行,你可以直接拖移小光,不過會有很大機率出現下圖的錯誤訊息。
圖013
為何會出現錯誤?
為什麼會出現這種錯誤呢?這還只是一個使用者而已耶?原因出在Client端傳送小光位置給Host,而Host將小光位置廣播給其它使用者這個流程上。
可以想見,整個滑鼠拖移期間可能會產生數以百計的X、Y值送往Host,其結果不是Server端爆,就是Client端因同時多次開啟Connection而爆掉。
其原因歸根究底,就是我們把WCF Service呼叫的速度想得太快了,完成一個Service呼叫所耗的時間遠超過滑鼠的拖移速度。
加入Buffer機制
解決辦法有很多種,有路徑預測及慣性預測,但此處的拖移是不可預期的情況,所以不適用路徑預測,而慣性預測則過於複雜(其實是我不懂 >_<” ),
於此我選擇了最簡單的解法,就是加入Buffer,也就是緩衝區機制,當使用者A開始拖移時,會發出第一個X、Y值給Host,在此動作未完成前,其餘的移動
都會保留在一個緩衝區中,當第一個X、Y值遞送動作完成後,呼叫另一個函式,將期間紀錄的數個X、Y值一次遞送給Host。
IInteraceiveService.cs |
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace MoveHikaru.Web { [ServiceContract(CallbackContract = typeof(IMoveCallback))] public interface IInteractiveService { [OperationContract] Guid Register(); [OperationContract] void Move(Guid id, double top, double left); [OperationContract] void MoveBatch(Guid id, List<CPoint> points); } [ServiceContract] public interface IMoveCallback { [OperationContract(IsOneWay = true)] void MoveCallback(double top, double left); [OperationContract(IsOneWay = true)] void MoveBatchCallback(List<CPoint> points); } public struct CPoint { public double X; public double Y; } public struct DPoint { public Guid ID; public double X; public double Y; } } |
InteractiveService.cs |
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MoveHikaru.Web { public class InteractiveService : IInteractiveService { private static Dictionary<Guid, IMoveCallback> _callbacks = new Dictionary<Guid, IMoveCallback>(); static InteractiveService() { } public Guid Register() { Guid id = Guid.NewGuid(); _callbacks.Add(id, OperationContext.Current.GetCallbackChannel<IMoveCallback>()); return id; } public void Move(Guid id, double top, double left) { ThreadPool.QueueUserWorkItem((state) => { foreach (var c in _callbacks) { if (!c.Key.Equals(id)) c.Value.MoveCallback(top, left); } }); } public void MoveBatch(Guid id, List<CPoint> points) { ThreadPool.QueueUserWorkItem((state) => { foreach (var c in _callbacks) { if (!c.Key.Equals(id)) c.Value.MoveBatchCallback(points); } }); } } } |
MainPage.xaml.cs |
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; using System.Collections.ObjectModel; namespace MoveHikaru { public partial class MainPage : UserControl { private ServiceReference1.InteractiveServiceClient _client = null; private ObservableCollection<ServiceReference1.CPoint> _points = new ObservableCollection<ServiceReference1.CPoint>(); private bool _isMoving = false, _isBatchMoving = false; private Guid _clientID = Guid.Empty; private static object _lock = new object(); public MainPage() { InitializeComponent(); EndpointAddress address = new EndpointAddress("http://localhost:1843/InteractiveService.svc"); PollingDuplexHttpBinding binding = new PollingDuplexHttpBinding(PollingDuplexMode.SingleMessagePerPoll); _client = new ServiceReference1.InteractiveServiceClient(binding, address); _client.MoveCallbackReceived += (s, args) => { Canvas.SetLeft(Hikaru, args.left); Canvas.SetTop(Hikaru, args.top); }; _client.MoveBatchCallbackReceived += (s, args) => { foreach (var item in args.points) { Canvas.SetLeft(Hikaru, item.X); Canvas.SetTop(Hikaru, item.Y); System.Threading.Thread.Sleep(3); } }; _client.MoveCompleted += (s, args) => { lock (_lock) { _isMoving = false; if (_points.Count > 0) { _isBatchMoving = true; _client.MoveBatchAsync(_clientID, _points); } } }; _client.MoveBatchCompleted += (s, args) => { lock (_lock) { _points.Clear(); _isBatchMoving = false; } }; Hikaru.MouseLeftButtonDown += new MouseButtonEventHandler(Hikaru_MouseLeftButtonDown); Hikaru.MouseLeftButtonUp += new MouseButtonEventHandler(Hikaru_MouseLeftButtonUp); Hikaru.MouseMove += new MouseEventHandler(Hikaru_MouseMove); _client.RegisterCompleted += (s, args) => { _clientID = args.Result; }; _client.RegisterAsync(); } private Point _StartPoint = default(Point); private bool _IsMouseCaptured = false; private void Hikaru_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { UIElement uiElement = sender as UIElement; _StartPoint = e.GetPosition(uiElement); this._IsMouseCaptured = uiElement.CaptureMouse(); } private void Hikaru_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { UIElement uiElement = sender as UIElement; uiElement.ReleaseMouseCapture(); this._IsMouseCaptured = false; if (_isMoving) { lock (_lock) _points.Add(new ServiceReference1.CPoint() { X = Canvas.GetLeft(uiElement), Y = Canvas.GetTop(uiElement) }); return; } _client.MoveAsync(_clientID, Canvas.GetTop(Hikaru), Canvas.GetLeft(Hikaru)); } private void Hikaru_MouseMove(object sender, MouseEventArgs e) { if (this._IsMouseCaptured && !_isBatchMoving) { UIElement uiElement = sender as UIElement; Canvas.SetLeft(uiElement, (e.GetPosition(this).X - this._StartPoint.X)); Canvas.SetTop(uiElement, (e.GetPosition(this).Y - this._StartPoint.Y)); if (_isMoving) { lock (_lock) _points.Add(new ServiceReference1.CPoint() { X = Canvas.GetLeft(uiElement), Y = Canvas.GetTop(uiElement) }); return; } _isMoving = true; _client.MoveAsync(_clientID, Canvas.GetTop(Hikaru), Canvas.GetLeft(Hikaru)); } } private void ball_LayoutUpdated(object sender, EventArgs e) { } } } |
完成後記得點選ServiceReference1來更新Reference,然後重新編譯即可。
PS: 我沒做UnRegister,所以每次重新啟動應用程式前,你得手動關掉Web Development Server。
PS2: 這只是一個示範,其中還有很多細節要注意及微調,要想取得最高效能,得回到TCP模式(Socket)。
隱藏版的小光
另外,這篇文章的後半段文字中藏了一個隱藏版的小光照片連結,這應該是日本同人的作品 ^_^ 。
有興趣可以找找 ^_^!