2009年9月23日 星期三

C# - Delgate(委派)和Event(事件)

首先, 先來釐清DelgateEvent的不同之處:

Delegate : 類似C語言中的函數指標(function pointer), 它包含了一個函数的原型(參數,返回值,呼叫方式)和該函数的内存位址. delegate是一種型別(class), 要宣告後才能使用.

例如, 我們在C語言中定義一個 callback函式, 可寫成:
typedef void (_stdcall* TouchFunc) (tOTM_Touch Touch);

等同於下面C#的寫法:
delegate void TouchEventFunc (tOTM_Touch Touch); 

Event: event是C#是關鍵字, 這個關鍵字是一個修飾詞, 類似const,static, 而event是用來修飾delegate. Event的實現需要透過delegate.

一個delegate被宣告為event(事件)後,除了在宣告事件的類別(Publisher)外,在其他的地方,只能利用+=-=操作(即subscrib或unsubscribe), 而delegate則沒有這樣的限制

若要訂閱某事件, 可利用“+=", 訂閱或註冊某事件指定的delegate(所代理的函数).

publisher.RaiseCustomEvent += new CustomEventHandler(HandleCustomEvent);


publisher.RaiseCustomEvent += HandleCustomEvent; //C# 2.0 內新增的語法


若要取消訂閱事件, 使用減法指派運算子 (-=) :
publisher.RaiseCustomEvent -= HandleCustomEvent;

event為物件提供一種方式, 可在某些事情發生時,通知其他類別或物件.

傳送或引發事件的類別稱為 Publisher(發行者) ,而接收或處理事件的類別則稱為Subscriber(訂閱者)

Method(方法)不一樣的地方:

Event是主動, 有事件發生就作通知,你可以訂閱或不訂閱
; 而Method是被動, 只能等待來呼叫,你不呼叫它就不會執行


下面為在C#中使用自訂Event的步驟:

在Publisher Class(傳送事件的類別)
  • 宣告事件的Delegate型別
public delegate void ChangedEventHandler(object sender, CustomEventArgs e);

  • 宣告事件
public event ChangedEventHandler Changed;

  • 當事件發生時,觸發此事件
if (Changed != null)
Changed(this, e);


在Subscriber Class(接收或處理事件的類別)
  • 訂閱Publisher Class的事件
DelegateEvent關聯, 並且註冊這個event的EventHandler函式

List.Changed += new ChangedEventHandler(ListChanged);

  • 自訂事件發生時要作的動作
public void ListChanged(object sender, CustomEventArgs e)
{
// Do something....
}


下面的sample code 為時間變更事件的範例, 出處為 Delegates and Events in C# / .NET

using System;
using System.Threading;

namespace EventSample
{
/* ======== Event Publisher ============ */
// Our subject -- it is this class that other classes
// will observe. This class publishes one event:
// SecondChange. The observers subscribe to that event.
public class Clock
{
// Private Fields holding the hour, minute and second
private int _hour;
private int _minute;
private int _second;

// The delegate named SecondChangeHandler,
// which will encapsulate
// any method that takes a clock object
// and a TimeInfoEventArgs
// object as the parameter and returns no value. It's the
// delegate the subscribers must implement.
public delegate void SecondChangeHandler(
object clock,
TimeInfoEventArgs timeInformation
);

// The event we publish
public event SecondChangeHandler SecondChange;

// The method which fires the Event
protected void OnSecondChange(
object clock,
TimeInfoEventArgs timeInformation
)
{
// Check if there are any Subscribers
if (SecondChange != null)
{
// Call the Event
SecondChange(clock, timeInformation);
}
}

// Set the clock running, it will raise an
// event for each new second
public void Run()
{
for (; ; )
{
// Sleep 1 Second
Thread.Sleep(1000);

// Get the current time
System.DateTime dt = System.DateTime.Now;

// If the second has changed
// notify the subscribers
if (dt.Second != _second)
{
// Create the TimeInfoEventArgs object
// to pass to the subscribers
TimeInfoEventArgs timeInformation =
new TimeInfoEventArgs(
dt.Hour, dt.Minute, dt.Second);

// If anyone has subscribed, notify them
OnSecondChange(this, timeInformation);
}

// update the state
_second = dt.Second;
_minute = dt.Minute;
_hour = dt.Hour;

}
}
}

// The class to hold the information about the event
// in this case it will hold only information
// available in the clock class, but could hold
// additional state information
public class TimeInfoEventArgs : EventArgs
{
public TimeInfoEventArgs(int hour, int minute, int second)
{
this.hour = hour;
this.minute = minute;
this.second = second;
}
public readonly int hour;
public readonly int minute;
public readonly int second;
}

/* ==== Event Subscribers ======= */

// An observer. DisplayClock subscribes to the
// clock's events. The job of DisplayClock is
// to display the current time
public class DisplayClock
{
// Given a clock, subscribe to
// its SecondChangeHandler event
public void Subscribe(Clock theClock)
{
theClock.SecondChange +=
new Clock.SecondChangeHandler(TimeHasChanged);
}

// The method that implements the
// delegated functionality
public void TimeHasChanged(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Current Time: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
/* ======= Test Application ========== */

// Test Application which implements the
// Clock Notifier - Subscriber Sample
public class Test
{
public static void Main()
{
// Create a new clock
Clock theClock = new Clock();

// Create the display and tell it to
// subscribe to the clock just created
DisplayClock dc = new DisplayClock();
dc.Subscribe(theClock);

// Get the clock started
theClock.Run();
}
}
}

下面圖是為執行結果:


參考文章:
Delegates and events
MSDN - Events Tutorial

2009年9月2日 星期三

WPF + Windows7 Multi-touch(Part 1)

Win32的程式中, 可以利用WndProc來接收WM_TOUCH / WM_GESTURE 訊息, 來得到觸控點和手勢的資訊(可參考這兩篇文章Get Touch Point , Windows 7 Gesture Sample), 可是在WPF中沒有WndProc函式可以呼叫, 因此, 下面的文章將介紹如何在WPF Window上使用Windows 7 Multi-Touch function.

在WPF Window使用Windows Touch SDK的步驟如下:

Step 1: 利用PInvoke的機制呼叫user32.dll中定義Touch / Gesture的資料型態, 結構和方法
此方法可參考這篇文章
: 如何在C#中使用Unmanaged dll

例如;我們要使用Winuser.hRegisterTouchWindow函式, 其轉換如下:

C
BOOL WINAPI RegisterTouchWindow(
__in  HWND hWnd,
__in  ULONG ulFlags
);


C#
[Flags, Serializable]
public enum RegisterTouchFlags
{
TWF_NONE = 0x00000000,

TWF_FINETOUCH = 0x00000001
}
[DllImport("user32.dll", SetLastError = true)]
public static extern bool RegisterTouchWindow(IntPtr hwnd,
[MarshalAs(UnmanagedType.U4)] RegisterTouchFlags flags);



Step 2: 在WPF Window中利用HwndSource物件來取得的Windows訊息


由於WPF Window不能像Win32 Window, 可利用WndProc函式處理WM_TOUCH / WM_GESTURE訊息,但WPF Window可以用HwndSource來裝載最上層HWND的所有內容.


其方法如下;


(1) 利用 System.Windows.Interop.WindowInteropHelper類別來取得任何WPF Window的HWND


example:

using System.Windows.Interop;

IntPtr _hwnd = new WindowInteropHelper(this).Handle;


(2) 利用HWND取得相關的HwndSource物件(用HwndSource.FromHwnd)

example:
HwndSource src = HwndSource.FromHwnd(_hwnd);

(3) 利用HwndSourceAddHook方法, 即可攔截視窗訊息


example:

src.AddHook(WndProc);
private IntPtr WndProc( IntPtr hwnd,
int msg,
IntPtr wParam,
IntPtr lParam,
ref bool handled)
{
switch (msg)
{
case TouchMessage.WM_GESTURE:
OnGesture(lParam);
// 記得將handle設為true,告訴系統你已經處理該訊息
handled = true;
break;

default:
break;
}
return IntPtr.Zero;
}



看完上面的介紹, 我們就來寫一個 WPF + Win7 Gesture 的應用程式, 我們會在WPF Window上繪製一個rectangle, 並透過一些手勢操作來對此rectangle作放大,縮小,平移,旋轉和改變顏色(twp finger tap)

其步驟如下:


  • 首先,我們會寫一個Win7TouchInterop.cs檔, 目的要將Winuser.h中的 touch和gesture資料作包裝
  • 在WPF的程式碼中, 使用Win7TouchInterop的命名空間 using Win7TouchInterop;
  • 利用HwndSource物件來取得的Windows訊息
  • 處理手勢訊息

程式碼如下所示;


XAML檔
:
<Window x:Class="WPFGestureMessage.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Touch SDK+WPF" Height="600" Width="800">
<Grid>

<Grid.Resources>
<Storyboard x:Key="_twoFingerTap">
<ColorAnimation
Storyboard.TargetName="rect"
Storyboard.TargetProperty=
"(Rectangle.Fill).(SolidColorBrush.Color)"
From="SkyBlue"
To="YellowGreen"
Duration="00:00:00.5"
AutoReverse="true"
FillBehavior="HoldEnd" />
</Storyboard>
</Grid.Resources>

<Rectangle  x:Name="rect"
RadiusX="5"
RadiusY="5"
Width="300"
Height="200"
Stroke="DarkBlue"
StrokeThickness="5"
Fill="YellowGreen"
RenderTransformOrigin="0.5,0.5">
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform
x:Name="_scaleTransform"
ScaleX="1"
ScaleY="1" />
<RotateTransform
x:Name="_rotateTransform"
Angle="0" />
<TranslateTransform
x:Name="_translateTransform"
X="0"
Y="0" />
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>

</Grid>
</Window>


C#檔
:
using System;
using System.Windows;

using Win7TouchInterop;
using System.Windows.Interop;
using System.Runtime.InteropServices;
using System.Windows.Media.Animation;

namespace WPFGestureMessage
{
/// 
/// Interaction logic for Window1.xaml
/// 
public partial class Window1 : Window
{
private IntPtr _hwnd;
const double scaleSensitivityMagicFactor = 200.0;
const double rotateSensitivityMagicFactor = 25.0;

private long _beginScale;

private bool captureAngle;
private double _beginAngle;

private Point firstPoint;

Storyboard _twoFingerTap;

public Window1()
{
InitializeComponent();
Loaded += new RoutedEventHandler(Window1_Loaded);
_twoFingerTap = rect.FindResource("_twoFingerTap") as Storyboard;

}

void Window1_Loaded(object sender, RoutedEventArgs e)
{
_hwnd = new WindowInteropHelper(this).Handle;
HwndSource src = HwndSource.FromHwnd(_hwnd);
if (src != null)
src.AddHook(WndProc);

// Setup the gestures
var gConfig = new[]
{
new GESTURECONFIG
{
dwID = GestureID.GID_ALL,

dwWant = WantGestures.GC_ANY
}   
};

Win7TouchMethod.SetGestureConfig(_hwnd,
0,
gConfig.Length,
gConfig,
Marshal.SizeOf(gConfig[0]));
}


private Point ConvertPoint(POINTS pts)
{
var pt = new POINT { X = pts.X, Y = pts.Y };
Win7TouchMethod.ScreenToClient(_hwnd, ref pt);
return new Point(pt.X, pt.Y);
}


private IntPtr WndProc(IntPtr hwnd,
int msg,
IntPtr wParam,
IntPtr lParam,
ref bool handled)
{

switch (msg)
{
case Win7TouchMessages.WM_GESTURE:
OnGesture(lParam);
handled = true;
break;
default:
break;
}


return IntPtr.Zero;
}

private void OnGesture(IntPtr gestureMsg)
{
GESTUREINFO gesture = new GESTUREINFO();
gesture.cbSize = Marshal.SizeOf(gesture);
if (Win7TouchMethod.GetGestureInfo(gestureMsg, out gesture))
{
switch (gesture.dwID)
{
case GestureID.GID_PAN:
OnPan(gesture.dwInstanceID,
ConvertPoint(gesture.ptsLocation),
gesture.dwFlags);
break;
case GestureID.GID_ROTATE:
OnRotate(gesture.ullArguments,
gesture.dwFlags);
break;
case GestureID.GID_TWOFINGERTAP:
_twoFingerTap.Stop();
_twoFingerTap.Begin();
break;
case GestureID.GID_ZOOM:
OnZoom(ConvertPoint(gesture.ptsLocation),
gesture.ullArguments,
gesture.dwFlags);
break;
default:
break;
}
}
Win7TouchMethod.CloseGestureInfoHandle(gestureMsg);
}



private void OnZoom(Point ptCenter,
long scale,
GestureFlags gestureFlags)
{

if ((gestureFlags & GestureFlags.GF_BEGIN) > 0)
{
_beginScale = scale;
}
else
{
double delta = scale - _beginScale;
_beginScale = scale;

double scaleX = (delta / scaleSensitivityMagicFactor);
double scaleY = (delta / scaleSensitivityMagicFactor);

_scaleTransform.ScaleX += scaleX;
_scaleTransform.ScaleY += scaleY;

}
}


private void OnPan(uint id,
Point point,
GestureFlags gestureFlags)
{

if ((gestureFlags & GestureFlags.GF_BEGIN) > 0)
{
firstPoint = point;
}
else
{
Point curPoint = point;
double xDelta = curPoint.X - firstPoint.X;
double yDelta = curPoint.Y - firstPoint.Y;

_translateTransform.X += xDelta;
_translateTransform.Y += yDelta;

firstPoint = curPoint;
}
}

private void OnRotate(double radians,
GestureFlags gestureFlags)
{

double angle = Win7TouchMethod.ROTATE_ANGLE_FROM_ARGUMENT(radians) * (180.0 / Math.PI);
if ((gestureFlags & GestureFlags.GF_BEGIN) > 0)
{
captureAngle = true;
}
else
{
if (captureAngle)
{
_beginAngle = angle;
captureAngle = false;
}
else
{
double delta = angle - _beginAngle;

_rotateTransform.Angle += 0 - (delta / rotateSensitivityMagicFactor);

if ((gestureFlags & GestureFlags.GF_END) > 0)
{
captureAngle = true;
_beginAngle = 0;
}
}
}//end else
}//end
}
}


成果如下圖所示:



[圖 1] 初始畫面


[圖 2] 縮小



[圖 3] 放大


[圖 3] 平移+inertia (因為有開啟GC_PAN_INERIA)


[圖 5] 旋轉


[圖 6] Two finger tap -->改變顏色

參考文章:
Windows 7 Multi-touch Using WPF
Windows 7: Experimenting with Multi-Touch on Windows 7
MSDN -WindowInteropHelper 類別