Silverlight 4 - 可拖移的小光

我想,自從Tech-Days後,不管是程式設計師或是IT人員,對小光都已經不陌生,這個可愛的小女生也正是Silverlight 4技術的代言人,據說,
她還有個巒生妹妹叫月澤光的,不過傳說畢竟只是傳說。

 

可以拖移的小光
 
 
/黃忠成
 
 
 
 
 
 
關於藍澤光
 
 
 
   我想,自從Tech-Days後,不管是程式設計師或是IT人員,對小光都已經不陌生,這個可愛的小女生也正是Silverlight 4技術的代言人,據說,
她還有個巒生妹妹叫月澤光的,不過傳說畢竟只是傳說。
 
 
實作可以用滑鼠拖移的小光
 
  在這篇文章中,筆者將帶領讀者實作一個簡單的Silverlight應用程式,我們將使用小光的SD版,讓使用者可以用滑鼠拖著小光跑。
  開始前,請準備好你的開發環境,本文將使用Visual Studio 2010+Expression Blend4來實作,請注意,小光是四女,Visual Studio 2010內建的是三女,
是小光的姐姐,所以你得到:http://www.silverlight.net/getstarted/silverlight-4/ 下載小光專用的SDK哦。 
完成安裝後,請開啟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應用程式,應該知道,有個BehaviorMouseDragElementBehavior,使用這個Behavior的話,我們
完全不用寫程式就能做出前例的效果,那為何我還要這麼做呢?其實我的目的是要做出Online版的拖移小光程式,效果如以下影片:
 
 
 
Http Duplex通訊
 
 
   要完成影片中的效果,必然得使用WCF Service,當使用者A拖移小光時,呼叫WCF Service將小光的位置傳回Host,然後再由
Host透過WCF Duplex Push到使用者B,這樣才能讓兩個視窗同步,下面的程式是WCF ServiceServer端介面定義。
 
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函式,
此時送上來的是XY值,還有其識別GUIDServer端會依據GUID來判別,只會呼叫使用者BIMoveCallback物件的MoveCallback
而不需要呼叫使用者AIMoveCallback物件。
下面是實作碼
 
 
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);
                }
            });
        }
    }
}
 
    特別注意,Move函式於拖移期間會不斷被呼叫,其也會不斷的呼叫非同一GuidIMoveCallback物件,此處不能夠直接
迴圈呼叫IMoveCallback物件,否則會有效能低落的情況發生,因此這裡直接把呼叫IMoveCallback物件的程式碼放到
Thread Pool中執行。
 
    由於使用到了WCF Duplex機制,我們還需要添加System.ServiceModel.PollingDuplexReference,此檔案在
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將小光位置廣播給其它使用者這個流程上。
可以想見,整個滑鼠拖移期間可能會產生數以百計的XY值送往Host,其結果不是Server端爆,就是Client端因同時多次開啟Connection而爆掉。
其原因歸根究底,就是我們把WCF Service呼叫的速度想得太快了,完成一個Service呼叫所耗的時間遠超過滑鼠的拖移速度。
  
 
加入Buffer機制
 
 
 
     解決辦法有很多種,有路徑預測及慣性預測,但此處的拖移是不可預期的情況,所以不適用路徑預測,而慣性預測則過於複雜(其實是我不懂 >_<” )
於此我選擇了最簡單的解法,就是加入Buffer,也就是緩衝區機制,當使用者A開始拖移時,會發出第一個XY值給Host,在此動作未完成前,其餘的移動
都會保留在一個緩衝區中,當第一個XY值遞送動作完成後,呼叫另一個函式,將期間紀錄的數個XY值一次遞送給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)
 
 
 
 
 
 
 
隱藏版的小光
 
   另外,這篇文章的後半段文字中藏了一個隱藏版的小光照片連結,這應該是日本同人的作品 ^_^
有興趣可以找找 ^_^!