顯示具有 WPF 標籤的文章。 顯示所有文章
顯示具有 WPF 標籤的文章。 顯示所有文章

2010年12月31日 星期五

WPF - Style使用

Style(樣式)
    Style主要就是透過屬性(Property)去自訂控制項的外觀, 尤其當我們需要相同的屬性套用在多個控制項時, Style會很方便.

比方在UI設計時, 常會讓UI具有某些相同的Style,例如:背景顏色,字型,控制項的顏色…等等.

使用Style的好處如下:
  • 可以方便使用者將一組屬性值(顏色,尺寸,動畫, 觸發方式等)套用到多個控制項的方式,這樣一來, 程式碼將變得簡潔.
  • 可集中在同一個地方作管理(ex: 資源 Resource), 好處如果以後想改變這些屬性, 只要在同一個地方更新即可
例如下面範例中, 有一個影音播放程式,如圖1所示, 其中UI上有三個外觀相同的按鈕, 在這程式碼當中, 如圖2所示, 如果不套用Style的話, 我們必需要將每一個控制項都設定一次相同的屬性, 這樣, 程式碼將會有很多重覆的部分, 而且, 萬一以後要變更其中一個屬性, 將要修改到很多地方

                                                       [圖1]具有一些相同屬性的Button



                                                          [圖2] 沒有套用Style


下面的範例中, 我們利用Style將上述程式碼中相同屬性抽離出來(StyleSetter組成的集合來設定目標屬性, 而每個Setter是由依存屬性和設定值組成), 並集中在Resource中. 程式碼將變得簡潔, 管理上也變的更方便, 如圖3所示.
                                                 [圖3] 套用Style


Style的共享與限定
你也可以將Style套用在不同的控制項上,如下所示:
<Style x:Key="controlStyle">
  <Setter Property="Control.Height" Value="30"/>
  <Setter Property="Control.Width" Value="75"/>
  <Setter Property="Control.Margin" Value="10"/>
  <Setter Property="Control.Background" 
                    Value="{StaticResource btnBackground}"/>
</Style>

<!-- Style套用在不同的控制項上-->
<Button x:Name="btnPlay" Grid.Column="0"
           Style="{StaticResource controlStyle}"
           Content="Play" Click="btnPlay_Click">
</Button>

<Label Grid.Column="1"
          Style="{StaticResource controlStyle}"
          Content="Text Block"/>
            
<Border Grid.Column="2"
           Style="{StaticResource controlStyle}"/>


或限定此Style只能給特定的控制項使用, 可用TargetType屬性指定, 例如你希望只有Button可套用此Style, 可寫成:
<Style x:Key="btnStyle" TargetType="{x:Type Button}">
     <Setter Property="Height" Value="30"/>
     <Setter Property="Width" Value="75"/>
     <Setter Property="Margin" Value="10"/>
     <Setter Property="Background" 
                Value="{StaticResource btnBackground}"/>
</Style>

若此Syle套用在非Button的控制項上, 將會產生編譯錯誤

Style繼承
Style之間可以互相繼承, 如果要這樣做, 您可以使用一個樣式做為基礎來建立新樣式, 並利用BasedOn屬性來繼承
<Style x:Key="btnStyleWithBold" 
          BasedOn="{StaticResource btnStyle}"
   <Setter Property="Button.FontWeight" Value= "Bold"/>
</Style>

另外, Style除了可用來設定控制項的屬性(Property)之外, 還可以運用在下列幾個地方
  • 設定觸發方式(Trigger)
  • 設定動畫(Animation)
  • 設定樣板(Template)
相關範例, 可參考: MSDN-設定樣式與範本


2010年11月11日 星期四

Background Process in WPF

在程式執行中, 有時後需要處理一些比較耗時的程序, 例如讀取或計算大量的資料, 因為這些動作會花比較多的時間, 而且會讓畫面停留不動. 所以, 通常需要一個Loading/Processing的畫面來提醒使用者.

Loading過程中, 如果要更新UI內容的話, 需要在背景工作, 否則會等Loading處理完畢後, 才會更新UI.

下面的範例中, 程式會顯示Loading過程中的次數和完成的百分比.
private void UpdateProcess()
 {
     for (int i = 1; i <= _numberOfTasks; i++)
     {
         labelNumber.Content = i +"/" + _numberOfTasks;
         float percentageDone = (i / (float)_numberOfTasks) * 100f;
         labelPercent.Content = (int)percentageDone+"%";
         Thread.Sleep(1);
     }
}


從執行結果可看出, 因為不是在背景中(background)更新UI內容, 所以畫面會停住不動, Loading完才會更新UI內容

 
[影片1] 不在背景工作中更新UI 


WPF中提供多個背景工作的方法, 下面逐一介紹

1. 使用Dispatcher

此方法最為簡單, 我們可將Dispatcher Priority 設成 DispatcherPriority.Background,即可在背景執行,而Dispatcher的相關用法可參考這篇文章WPF - Thread and Dispatcher
因此我們可將上面的程式碼, 改成為下面的程式:

private delegate void UpdateProcessDelegate(int index, int percent);
private void UpdateProcessAction(int index, int percent)
{
    labelNumber.Content = index + "/" + _numberOfTasks;
    labelPercent.Content = percent + "%";
 }

 private void UpdateProcess()
 {
     for (int i = 1; i <= _numberOfTasks; i++)
     {
         //labelNumber.Content = i +"/" + _numberOfTasks;
         float percentageDone = (i / (float)_numberOfTasks) * 100f;
         //labelPercent.Content = (int)percentageDone+"%";

         Dispatcher.Invoke(DispatcherPriority.Background,
                           new UpdateProcessDelegate(UpdateProcessAction),
                           i, 
                           (int)percentageDone);
         
         Thread.Sleep(1);
      }
 }

 
[影片2] 使用Dispatcher更新UI內容

 
2. 使用DispatcherFrame       
此作法類似Windows Form的DoEvents.  
DispatcherFrame 表示處理暫停之工作項目的迴圈.發送器會處理迴圈中的工作項目佇列. 此迴圈即稱為框架。通常是由應用程式以呼叫 Run 的方式來初始化初始迴圈. PushFrame 進入以 frame 參數表示的迴圈。對該迴圈的每次查看, Dispatcher 都會檢查 DispatcherFrame 類別上的 Continue 屬性,以判斷迴圈是否應繼續或停止.

程式碼如下:
public void DoEvents()
{
   DispatcherFrame frame = new DispatcherFrame();
   Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                                            new DispatcherOperationCallback(ExitFrame),
                                            frame);
   Dispatcher.PushFrame(frame);
}

private object ExitFrame(object f)
{
   ((DispatcherFrame)f).Continue = false;
   return null;
}

private void UpdateProcess()
{
   for (int i = 1; i <= _numberOfTasks; i++)
   {
       labelNumber.Content = i +"/" + _numberOfTasks;
       float percentageDone = (i / (float)_numberOfTasks) * 100f;
       labelPercent.Content = (int)percentageDone+"%";
       DoEvents();
       Thread.Sleep(1);
    }  
 }

BackgroundWorker可以在個別的執行緒上執行. BackgroundWoker常用的function如下:
程式碼如下:
private void btnShowText_Click(object sender, RoutedEventArgs e)
{
    BackgroundWorker worker = new BackgroundWorker();
    worker.WorkerReportsProgress = true;
    worker.DoWork += new DoWorkEventHandler(DoWork);
    worker.ProgressChanged += new ProgressChangedEventHandler(DuringWork);
    
    // start the background work
    worker.RunWorkerAsync();
 }

 void DoWork(object sender, DoWorkEventArgs e)
 {
     BackgroundWorker worker = sender as BackgroundWorker;
     for (int i = 1; i <= _numberOfTasks; i++)
     {
         if (worker.CancellationPending)
         {
            e.Cancel = true;
            return;
          }
          Thread.Sleep(1);
          float percentageDone = (i / (float)_numberOfTasks) * 100f;
          worker.ReportProgress((int)percentageDone, i);
      }
  }
       
 void DuringWork(object sender, ProgressChangedEventArgs e)
 {
    labelNumber.Content = e.UserState + "/" + _numberOfTasks;
    labelPercent.Content = e.ProgressPercentage.ToString() + "%";
 }


參考文章:

2010年11月10日 星期三

WPF - Thread and Dispatcher

WPF 執行緒:
WPF應用程式中主要有兩個執行緒(Thread):Rendering ThreadUI Thread
  • Rendering Thread: 在背景中執行並協助UI Thread完成工作
  • UI Thread: 主要負責處理InputEvent與程式碼的部分
WPF中, 所有UI工作和事件都是在UI Thread上完成. 每個WPF程式至少有一個Dispatcher物件和一個UI Thread. 而大部分WPF物件的操作都是binding在UI Thread上,其他的Thread如果想要直接操作這些UI物件, 是不被允許的, 因為只有建立 DispatcherObject 的Thread才可以存取UI物件。例如, 要讓其他的Thread存取 LabelContent 屬性, 將會發生錯誤, 此範例如下所示:
設計一個WPF視窗, 當按下Button後, 讓Label顯示按Button的次數.

C#程式碼如下所示:
private static int _clickCount = 0;
private void btnShowText_Click(object sender, RoutedEventArgs e)
{
      _clickCount++;
       // Update Label Conten in another thread
       Thread updateThread = new Thread(UpdateText);
       updateThread.Start();
 }
 private void UpdateText()
 {
      labelClickCount.Content = "Click Count= " + _clickCount.ToString();
 }

[圖1]無法再另一個Thread中更新UI


Dispatcher用法:

Dispatcher可讓其他的ThreadWPF中的 UI Thread上執行程式碼. Dispatcher又被稱為WPF中的Message Pump,提供一個機制去發送工作項目給UI Thread進行處理.  
Dispatcher將工作項目轉給UI Thread時, 會讓UI Thread進入封鎖狀態(畫面鎖定, 因為要處理工作項目). 其他的Thread必須將工作委派給與 UI Thread相關聯的 Dispatcher. 這可以使用 Invoke BeginInvoke 完成.
  • Invoke 是同步呼叫,也就是說,除非 UI 執行緒實際完成委派的執行,否則不會返回. 
Dispatcher 會依優先權排列其佇列中項目的順序. 在將項目加入至 Dispatcher 佇列.

下面的程式碼, 將說明其解決方法:
C#程式碼如下所示:

private static int _clickCount = 0;

 // Declare a delegate type for updating UI
 private delegate void UpdateDelegate(int number, string str);
 private void btnShowText_Click(object sender, RoutedEventArgs e)
 {
     _clickCount++;
     Thread updateThread = new Thread(UpdateText);
      updateThread.Start();
 }

 private void UpdateText()
 {
      //labelClickCount.Content = "Click Count= " + _clickCount.ToString();
      // Places the delegate onto the UI Thread's Dispatcher
      Dispatcher.Invoke(new UpdateDelegate(UpdateClickCountAction),
                                          _clickCount, 
                                          "Click Count = ");
 }

  private void UpdateClickCountAction(int number, string str)
  {
       labelClickCount.Content = str + number.ToString();
  }

[圖2] 利用Dispatcher執行另一個Thread的function

參考文章:

2010年4月15日 星期四

Game Loop Compare- DispacterTimer, Storyboard, Composition.Rendering

Game Loop 對寫遊戲來說是一個很重要的步驟, 通常我們會將一些不需長時間且不用大量計算的事情, 放在Game Loop裡面去作, 例如:碰撞偵測, 動畫撥放, 更新繪圖座標, 檢查滑鼠或鍵盤的狀態..等.

WPF/Silverlight中, 可以透過下列方式來實作Game Loop,分別是:

  • DispatcherTimer - 可以在指定的時間間隔內, 處理佇列中的執行緒.
  • Storyboard - 以時間軸控制動畫, 和DispatcherTimer的作法類似, 透過Duration屬性決定時間長短, 然後在Completed事件中再次呼叫StoryboardBegin方法, 就可以達到類似計時器的迴圈.
  • CompositionTarget.Rendering - 每當程式要顯示一個畫面(frame)時, 就會引發Rendering事件

下列程式碼將示範如何使用上述的方法, 我們會讓一個圓球在視窗內作移動, 當碰到邊界時會作反彈, 而圓球的座標是在Game Loop中作更新, 並顯示對應的FPS值, 如下圖所示:

[圖 1]利用Game Loop作動畫

xmal檔如下:

<Window x:Class="GameLoopTesting.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" SizeToContent="WidthAndHeight">
<Canvas x:Name="LayoutRoot" Background="Black" Width="800" Height="600">
<TextBlock x:Name="txtFPS"
Foreground="White"
FontFamily="Arial"
FontSize="24"
Text="FPS:"
Panel.ZIndex="99"/>
<Ellipse x:Name="ball"
Width="50"
Height="50"
Fill="LightBlue"
Stroke="Gray"
Canvas.Left="100"
Canvas.Top="100">
</Ellipse>
</Canvas>
</Window>



cs檔如下:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using System.Windows.Media.Animation;

namespace GameLoopTesting
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public enum GameLoopType
{
DISPATCHERTIMER,
STORYBOARD,
RENDERING
}

/// <summary>
/// For object parameters
/// </summary>
private float radius;
private double dx = 1;
private double dy = 1;
private double x = 0;
private double y = 0;


/// <summary>
/// For FPS property
/// </summary>
private int _frameCounter = 0;
private double _timeSinceLastUpdate = 0.0;
private DateTime _lastUpdateTime = DateTime.MinValue;
private double _fpsValue = 0.0;

private Storyboard _sbTrigger;


public Window1()
{
InitializeComponent();

radius = (float)ball.Width/2;
_lastUpdateTime = DateTime.Now;


///Switch game loop type
SwitchGameLoop(GameLoopType.DISPATCHERTIMER);
}


void SwitchGameLoop(GameLoopType gameLoopType)
{
switch (gameLoopType)
{
case GameLoopType.DISPATCHERTIMER:
{
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(10);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
break;

case GameLoopType.STORYBOARD:
{
_sbTrigger = new Storyboard();
_sbTrigger.Duration = TimeSpan.FromMilliseconds(10);
_sbTrigger.Completed += new EventHandler(sbTrigger_Completed);
_sbTrigger.Begin();

}
break;


case GameLoopType.RENDERING:
{
CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);

}
break;
}
}

void timer_Tick(object sender, EventArgs e)
{
/// Calculate FPS value
/// -----------------------------------------------------------
_frameCounter++;

if ((DateTime.Now - _lastUpdateTime).Seconds >= 1)
{
txtFPS.Text = "FPS : " + _frameCounter;
_frameCounter = 0;
_lastUpdateTime = DateTime.Now;
}
//-----------------------------------------------------------

UpdateObjectPosition();
}

void sbTrigger_Completed(object sender, EventArgs e)
{
UpdateObjectPosition();
_sbTrigger.Begin();
}

void CompositionTarget_Rendering(object sender, EventArgs e)
{
/// Calculate FPS value
/// -----------------------------------------------------------
TimeSpan elapsedTime = DateTime.Now - _lastUpdateTime;

_frameCounter++;
_timeSinceLastUpdate += elapsedTime.TotalSeconds;

if (_frameCounter >= 100)
{
_fpsValue = (int)(_frameCounter / _timeSinceLastUpdate);
txtFPS.Text = "FPS : " + _fpsValue;

_frameCounter = 0;
_timeSinceLastUpdate = 0;
}
_lastUpdateTime = DateTime.Now;
//----------------------------------------------------------------

UpdateObjectPosition();
}


/// <summary>
/// Update the position of object
/// </summary>
private void UpdateObjectPosition()
{
x = ((double)Canvas.GetLeft(ball));
y = ((double)Canvas.GetTop(ball));

x += dx;
y += dy;

Canvas.SetLeft(ball, x);
Canvas.SetTop(ball, y);

if (x > LayoutRoot.Width - 2 * radius)
{
dx = -Math.Abs(dx);
}
if (x < 0)
{
dx = Math.Abs(dx);
}
if (y > LayoutRoot.Height - 2 * radius)
{
dy = -Math.Abs(dy);
}
if (y < 0)
{
dy = Math.Abs(dy);
}
}
}
}


總 結:
* 在WPF中使用Storyboard作Game Loop時, 會發生畫面更新到一半時, 卻停止更新,但Silverlight上卻不會, 原因目前還不知道.
* 透過上述的3種方式, 都可以作為遊戲的迴圈, 不過穩定度和解析度上有所差別.可參考這篇Game Loop部落格的詳細比較.
就穩定度來說 :
CompositionTarget.Rendering > Storyboard > DispatcherTimer


參考文章:
Creating a Game Loop
Application (Game) Loop, FPS and Sprite like Animation
Game Loop

2009年11月18日 星期三

WPF + Windows7 Multi-touch (Part 3)

目前在WPF中使用Multi-touch功能,必須透過P/Invoke的方式呼叫Win32 Touch API的功能(可參考這篇文章 : WPF + Windows7 Multi-touch (Part 2) ), 或是可以直接使用微軟提供的Windows 7 Multitouch .NET Interop Sample Library, 它提供 WPF WinForms 3.5 SP1 要開發Multi-Touch 程式所需的功能, 不過利用上述這些方法開發觸控功能時, 還是有稍嫌複雜, 也不是很直覺.

因此在WPF 4.0 已經將一些觸控功能放到UIElement, UIElement3DContentElement中,
Beta 1中, 將支援高階操作的觸控功能(Manipulation), 如下所示:
而在Beta 2 中, 除了原本高階操作的觸控功能(Manipulation)外, 還提供的Touch相關的event,如下所示:

下面我們將利用WPF 4.0 Beta2 Touch功能, 撰寫一個顯示目前觸控點的應用程式, 其步驟如下:

軟體和硬體需求
程式碼如下:

XAML檔的部份:


<Window x:Class="WPFTouchPoint.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="800" Width="800"
TouchDown="Window_TouchDown"
TouchMove="Window_TouchMove"
TouchUp="Window_TouchUp">
<Canvas Background="Black">
<Ellipse
Canvas.Left="0"
Canvas.Top="0"
Name="Touch1"
Stroke="Black"
Height="60"
Width="60"
Fill="LightGreen"
Visibility="Hidden">
<Ellipse.BitmapEffect>
<DropShadowBitmapEffect ShadowDepth="10"
Direction="270"
Color="White"
Opacity="0.5"
Softness="0.25"/>
</Ellipse.BitmapEffect>
</Ellipse>

<Ellipse
Canvas.Left="0"
Canvas.Top="0"
Name="Touch2"
Stroke="Black"
Height="60"
Width="60"
Fill="LightBlue"
Visibility="Hidden">
<Ellipse.BitmapEffect>
<dropshadowbitmapeffect shadowdepth="10" direction="270" color="White"
opacity="0.5" softness="0.25">
</dropshadowbitmapeffect>
</Ellipse>
</Canvas>
</Window>


C#檔的部分:


namespace WPFTouchPoint
{
///
/// Interaction logic for MainWindow.xaml
///

public partial class MainWindow : Window
{

private int Touch1ID = 0; // id for first touch contact
private int Touch2ID = 0; // id for second touch contact

public MainWindow()
{
InitializeComponent();
}


private void Window_TouchDown(object sender, TouchEventArgs e)
{
TouchPoint p = e.GetTouchPoint(this);

if (Touch1ID == 0)
{
// Show the touch point
Touch1.Visibility = Visibility.Visible;

Touch1ID = e.TouchDevice.Id;

// move the ellipse to the given location
Touch1.SetValue(Canvas.LeftProperty, p.Position.X - Touch1.Width / 2);
Touch1.SetValue(Canvas.TopProperty, p.Position.Y - Touch1.Height / 2);
}
else if (Touch2ID == 0)
{
Touch2.Visibility = Visibility.Visible;

Touch2ID = e.TouchDevice.Id;
// move the ellipse to the given location
Touch2.SetValue(Canvas.LeftProperty, p.Position.X - Touch2.Width / 2);
Touch2.SetValue(Canvas.TopProperty, p.Position.Y - Touch2.Height / 2);
}

}

private void Window_TouchMove(object sender, TouchEventArgs e)
{
TouchPoint p = e.GetTouchPoint(this);
// determine which contact this belongs to
if (Touch1ID == e.TouchDevice.Id)
{
Touch1.SetValue(Canvas.LeftProperty, p.Position.X - Touch1.Width / 2);
Touch1.SetValue(Canvas.TopProperty, p.Position.Y - Touch1.Height / 2);
}
else if (Touch2ID == e.TouchDevice.Id)
{
Touch2.SetValue(Canvas.LeftProperty, p.Position.X - Touch2.Width / 2);
Touch2.SetValue(Canvas.TopProperty, p.Position.Y - Touch2.Height / 2);
}
}

private void Window_TouchUp(object sender, TouchEventArgs e)
{
if (e.TouchDevice.Id == Touch1ID)
{
Touch1.Visibility = Visibility.Hidden;

Touch1ID = 0;
}
else if (e.TouchDevice.Id == Touch2ID)
{
Touch2.Visibility = Visibility.Hidden;

Touch2ID = 0;
}

}
}
}

顯示結果如下所示:


[圖 1] Multi-touch points in WPF4.0 Beta 2


不過目前這些觸控功能在Beta版還是有很多bug, 可參考這些文章測試出來的結果:
Multi-touch in WPF4.0 Beta2

WPF 4, Beta 2 expands multi-touch API but is buggy


參考文章:
Windows 7: Experimenting with Multi-Touch on Windows 7 ( Part 5 )

Windows using Multi-touch using WPF

Multi-touch in WPF4.0 and VS2010


Introduction to WPF 4 Multitouch


Walkthrough: Creating Your First Touch Application

What's New in WPF Version 4

2009年10月18日 星期日

WPF + Windows7 Multi-touch (Part 2)

WPF + Windows7 Multi-touch (Part 1) 這篇文章中, 我們可以透過 P/ Invoke 的方式來取得Windows 7 的手勢訊息, 並對物體作手勢操作.

不過當你用這個方法來取得觸控點的資訊時, 你會發現收不到WM_TOUCH 的訊息, 這個是因為WPF團隊還沒把這部分加進去(這篇
WPF Windows 7 Multi-touch.文章中, WPF團隊有回答此問題)

因此, 如果要在WPF程式中取得觸控點的資訊的話, 就必須採用下面步驟:

  • 新增一個"MicrosoftTabletPenServiceProperty"屬性到視窗上
             IntPtr hWnd = new WindowInteropHelper(this).Handle;
HwndSource src = HwndSource.FromHwnd(hWnd);
Win7TouchMethod.SetProp(src.Handle,
"MicrosoftTabletPenServiceProperty",
new IntPtr(0x01000000));

  • 新增StylusDown/StylusUp/StylusMove的事件, 來處理觸控的相關事件.

this.StylusDown += new StylusDownEventHandler(Window1_StylusDown);
this.StylusMove += new StylusEventHandler(Window1_StylusMove);
this.StylusUp += new StylusEventHandler(Window1_StylusUp);

下面的範例中, 我們會寫一個WPF的觸控應用程式, 當有觸控發生時, 畫面上會出現圓點(用來表示觸控點的位置)

source code如下:

<Window x:Class="WPFTouch.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="600" Width="800">
<Canvas>
<Ellipse
Canvas.Left="0"
Canvas.Top="0"
Name="Touch1"
Stroke="Black"
Height="30"
Width="30"
Fill="Blue"
Visibility="Hidden"/>

<Ellipse
Canvas.Left="0"
Canvas.Top="0"
Name="Touch2"
Stroke="Black"
Height="30"
Width="30"
Fill="Green"
Visibility="Hidden"/>
</Canvas>

</Window>


using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using Win7TouchInterop;

namespace WPFTouch
{
public partial class Window1 : Window
{

private int Touch1ID = 0; // id for first touch contact
private int Touch2ID = 0; // id for second touch contact

public Window1()
{
InitializeComponent();

Loaded += new RoutedEventHandler(Window1_Loaded);

// Add StylusDown/StylusMove/StylusUp events to handle the touch related events
this.StylusDown += new StylusDownEventHandler(Window1_StylusDown);
this.StylusMove += new StylusEventHandler(Window1_StylusMove);
this.StylusUp += new StylusEventHandler(Window1_StylusUp);
}

void Window1_Loaded(object sender, RoutedEventArgs e)
{
IntPtr hWnd = new WindowInteropHelper(this).Handle;
HwndSource src = HwndSource.FromHwnd(hWnd);

Win7TouchMethod.SetProp(src.Handle,
"MicrosoftTabletPenServiceProperty",
new IntPtr(0x01000000));

}


void Window1_StylusDown(object sender, StylusDownEventArgs e)
{
Point p = e.GetPosition(this); // get the location for this contact


if (Touch1ID == 0)
{
// Show the touch point
Touch1.Visibility = Visibility.Visible;

Touch1ID = e.StylusDevice.Id;

// move the ellipse to the given location
Touch1.SetValue(Canvas.LeftProperty, p.X - Touch1.Width / 2);
Touch1.SetValue(Canvas.TopProperty, p.Y - Touch1.Height / 2);
}
else if (Touch2ID == 0)
{
Touch2.Visibility = Visibility.Visible;

Touch2ID = e.StylusDevice.Id;
// move the ellipse to the given location
Touch2.SetValue(Canvas.LeftProperty, p.X - Touch2.Width / 2);
Touch2.SetValue(Canvas.TopProperty, p.Y - Touch2.Height / 2);
}
}

void Window1_StylusMove(object sender, StylusEventArgs e)
{
Point p = e.GetPosition(this);
// determine which contact this belongs to
if (Touch1ID == e.StylusDevice.Id)
{
Touch1.SetValue(Canvas.LeftProperty, p.X - Touch1.Width / 2);
Touch1.SetValue(Canvas.TopProperty, p.Y - Touch1.Height / 2);
}
else if (Touch2ID == e.StylusDevice.Id)
{
Touch2.SetValue(Canvas.LeftProperty, p.X - Touch2.Width / 2);
Touch2.SetValue(Canvas.TopProperty, p.Y - Touch2.Height / 2);
}
}

void Window1_StylusUp(object sender, StylusEventArgs e)
{
if (e.StylusDevice.Id == Touch1ID)
{
Touch1.Visibility = Visibility.Hidden;

Touch1ID = 0;
}
else if (e.StylusDevice.Id == Touch2ID)
{
Touch2.Visibility = Visibility.Hidden;

Touch2ID = 0;
}
}
}
}


執行畫面如下:


[圖 1] WPF Touch Application

參考文章:
Windows 7 Multi-touch using WPF

2009年10月13日 星期二

如何讓WPF控制項產生陰影效果

WPF中可以利用BitmapEffect(點陣圖效果), 使控制項呈現一些影像特效;如: Blur(模糊效果), Shadow(陰影效果), Bevel(斜面), Emboss(凹凸)和色彩光暈等....

在這篇文章中, 我們先來介紹會使控制像看起來比較有立體感的陰影效果.

由於BitmapEffect Visual 物件上的屬性, 因此, 我們可將BitmapEffect套用至任何視覺物件(例如 Button,Image, DrawingVisual 或 UIElement)上. 例如我們可以利用DropShadowBitmapEffect 物件建立 WPF 物件的各種下拉式陰影效果.

它提供下列屬性來讓你自訂陰影效果:
  • Color - 設定陰影的顏色
  • Opacity - 設定陰影的透明度, 設定範圍為0 至1, 預設值是1
  • ShadowDepth - 控制陰影的寬,有效值的範圍為 0 至 300。預設值為 5
  • Direction - 控制陰影的方向。請將這個屬性的方向值設定為介於 0 與 360 之間的度數. 0表示陰影會顯示在物件的正右方, 隨著Direction的增加, 陰影會逆時針方向移動
  • Softness - 控制陰影的柔和度或模糊,值 0.0 表示沒有模糊,而值 1.0 則表示全部模糊

下面的範例中, 我們將圖1透過DropShadowBitmapEffect 的效果, 產生出如圖片2的陰影效果:

XAML檔如下所示:

<Image Source="tulips.jpg"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="80, 50, 80, 50"
Name="image1"
Stretch="Fill" >
<Image.BitmapEffect>
<DropShadowBitmapEffect ShadowDepth="20"
Direction="300"
Color="Black"
Opacity="0.5"
Softness="0.25" />
</Image.BitmapEffect>
</Image>



[圖1]沒有陰影效果



[圖2]加上陰影效果



需要注意的地方:
WPF 點陣圖效果是在軟體模式中呈現. 而且是由UI執行緒執行, 而不是呈現的執行緒, 因此使用點陣圖效果會對效能有很大的影響(效能和元素的數量成正比), 所以最好在少量而且是靜態的內容才使用這些效果. 尤其在大型視覺物件上使用點陣圖效果, 或者以點陣圖效果的屬性建立動畫時, 效能會降低最多.


參考文章 :
MSDN - 點陣圖效果概觀
MSDN - 建立含陰影的文字

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 類別

2009年8月19日 星期三

WPF ListBox - Beginning


ListBox
可以讓使用者能夠從清單選取某個項目, 它是一個項目的控制項, 可以包含物件(object)的集合.

ListBox繼承自ItemControl, 它可以透過Items(型別為object collection)和 ItemsSource(有實踐IEnumerable的物件)這兩個屬性 , 來將任何物件放入ListBox, 如果放入的項目不是UIElement(例如:數值或string), 則會呼叫物件的ToString()函式, 然後將資料放入TextBlock中.

將物件放入
ListBox有兩種方法:
  • 利用Items屬性加入項目(Add)
下面的程式碼說明利用Items將任一物件放到ListBox中:

XAML檔如下:

<Window
x:Class="ListBoxTemplate.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="ListBox" Height="300" Width="300">
<Grid>
<ListBox Margin="10" Name="listBox1">

<ListBoxItem Foreground="Aqua">Text</ListBoxItem>

<ListBoxItem>
<sys:DateTime>8/20/2009</sys:DateTime>
</ListBoxItem>

<ListBoxItem>
<Button Content="Button"
Background="LavenderBlush"
Foreground="Green"
Width="60"
Height="30"/>
</ListBoxItem>

<ListBoxItem>
<StackPanel>
<Rectangle Width="50"
Height="50"
Fill="Chartreuse"
Stroke="LightSlateGray"
StrokeThickness="3"/>
<TextBlock Text="Multiple objects in Panel"
FontSize="16"
Foreground="Red"/>
</StackPanel>
</ListBoxItem>
</ListBox>
</Grid>
</Window>

C#檔如下:

Grid grid = new Grid();
ListBox listBox1 = new ListBox();
listBox1.Margin = new Thickness(10);

grid.Children.Add(listBox1);

// Add a string object to a ListBox.
listBox1.Items.Add("Text");

// Add a DateTime object to a ListBox
DateTime dateTime = new DateTime(2009, 8, 20);
listBox1.Items.Add(dateTime);

// Add a button object to a ListBox
Button button1 = new Button();
button1.Content = "Button";
button1.Background = Brushes.LavenderBlush;
button1.Foreground = Brushes.Green;
button1.Width = 60;
button1.Height = 30;
listBox1.Items.Add(button1);

// Add a stackpanel that contains multpile objects to the ListBox.
StackPanel panel = new StackPanel();

Rectangle rect = new Rectangle();
rect.Width = 50;
rect.Height = 50;
rect.Fill = Brushes.Chartreuse;
rect.Stroke = Brushes.LightSlateGray;
rect.StrokeThickness = 3;

TextBlock txtBlock = new TextBlock();
txtBlock.Text = "Multiple objects in Panel";
txtBlock.FontSize = 16;
txtBlock.Foreground = Brushes.Red;

panel.Children.Add(rect);
panel.Children.Add(txtBlock);
listBox1.Items.Add(panel);

this.AddChild(grid);


[圖 1] 利用Items加物件

  • ItemsSource 繫結到一個物件的集合
你可以ItemsSource等於一個物件的集合, 或是將ItemsSource繫結(Binding)至集合物件,
注意: 當設定ItemsSource 屬性設定時, 會將 Items 集合設為唯讀和固定大小, 因此不能再對Items作新增或移除. 若將ItemsSource 屬性設為 null, 則會移除集合, 並將用法還原為 Items.

下面的範例說明, 如何利用ItemSourceBinding一個物件:
首先, 我們先建立一個
WeekData 的物件,如下:

 public class WeekData : ObservableCollection<string>
{
public WeekData()
{
Add("Sun");
Add("Mon");
Add("Tue");
Add("Wed");
Add("Thu");
Add("Fri");
Add("Sat");
}
}

ItemsSource 繫結至 WeekData:

Grid grid = new Grid();
ListBox listBox = new ListBox();
WeekData weekData = new WeekData();
Binding binding = new Binding();

binding.Source = weekData;
listBox.SetBinding(ListBox.ItemsSourceProperty, binding);
grid.Children.Add(listBox);
this.AddChild(grid);

如果您想要從 XAML中 繫結的CLR物件, 可以將物件定義為資源, 並指定x:Key,如下所示:

<Window.Resources>
<c:WeekData x:Key="weekData"/>
</Window.Resources>

<ListBox ItemsSource="{Binding Source={StaticResource weekData}}"/>


[圖 2] 利用ItemSource加入物件

參考文件:
MSDN-
Controls 內容模型概觀
MSDN- ListBox Class

2009年8月14日 星期五

WPF 面板 - Grid

Grid是很常見的容器(container), 它可以用來作UI layout, 它的功能類似HTML中的Table,可定義表格的直行與橫列.

如果你想要製定一個3x3的表格(三直行+三橫列), 寫法如下面程式碼(XAML檔和C#)所示:


XAML:
<Grid ShowGridLines="True" Name="grid">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>

<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
</Grid>

C#
public GridWindow()
{
InitializeComponent();

Grid grid1 = new Grid();
grid1.ShowGridLines = true;

for (int i = 0; i < 3; i++)
{
ColumnDefinition colDef = new ColumnDefinition();
RowDefinition rowDef = new RowDefinition();

grid1.ColumnDefinitions.Add(colDef);
grid1.RowDefinitions.Add(rowDef);
}

this.AddChild(grid1);
}

在此範例中, 我們可以讓ShowGridLines = true以顯示格子之間的線, 方便作Debug用.


[圖 1] 3X3 Grid


GridUnitType可用來設定尺寸單位的類型, 你可以搭配GridLength()來指定指定行和列的大小,其成員如下:


  • Pixel - 固定大小
example:
RowDefinition rowDef = new RowDefinition();
rowDef.Height = new GridLength(50, GridUnitType.Pixel)


  • Auto - 根據Content(內容)來設定大小
example:
RowDefinition rowDef = new RowDefinition();
rowDef.Height =
GridLength.Auto;


  • Star - 分攤剩下的空間
example:
RowDefinition rowDef = new RowDefinition();
rowDef.Height = new GridLength(33, GridUnitType.Star) ;



如果你想要把控制項塞到Grid裡面, 可以使用SetRowSetColumn方法, 指定row和column的index(從0開始), 來表式控制項擺放的位置.

example:
Grid.SetRow(control, 1);
Grid.SetColumn(control, 2);

你也可以使用SetRowSpanSetColumn的方法, 指定要跨列和行的個數.

example:
Grid.SetRowSpan(control, 3);
Grid.SetColumnSpan(control, 3);


下面的範例, 我們將示範如何將控制項放入
3X3的Grid 中, 然後適當調整Grid行和列的大小, 畫面如下所示:


[圖 2] Number Grid

Grid的第一列中, 我們會放一個內容為"Number Grid"TextBox,
TextBox會橫跨3行, 列的高度會設為Auto.

然後, 我們在Grid其他的cell中各填入一個數字Button, 其程式碼如下所示:


XAML
<Grid Name="grid1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>

<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>

<TextBox Name="txbBox"
FontSize="25"
Background="BurlyWood"
Foreground="DarkGreen"
Text="Number Grid"

HorizontalContentAlignment
="Center"

Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3">
</TextBox>

<Button
FontSize="30"
Name="button1"
Grid.Row="1"
Grid.Column="0">1
</Button>

<Button
FontSize="30"
Name="button2"
Grid.Row="1"
Grid.Column="1">2
</Button>

<Button
FontSize="30"
Name="button3"
Grid.Row="1"
Grid.Column="2">3
</Button>

<Button
FontSize="30"
Name="button4"
Grid.Row="2"
Grid.Column="0">4
</Button>

<Button
FontSize="30"
Name="button5"
Grid.Row="2"
Grid.Column="1">5
</Button>

<Button
FontSize="30"
Name="button6"
Grid.Row="2"
Grid.Column="2">6
</Button>
</Grid>

C#
public GridWindow()
{
InitializeComponent();

Grid grid1 = new Grid();

for (int i = 0; i < 3; i++)
{                 
RowDefinition rowDef =
new RowDefinition();

ColumnDefinition colDef =
new ColumnDefinition();

if( i == 0)
rowDef.Height = GridLength.Auto;
else
rowDef.Height =
new GridLength(1, GridUnitType.Star);

grid1.ColumnDefinitions.Add(colDef);
grid1.RowDefinitions.Add(rowDef);
}

this.AddChild(grid1);


// Add a TexBox
TextBox txbBox = new TextBox();
txbBox.FontSize = 25;
txbBox.Background = Brushes.BurlyWood;
txbBox.Foreground = Brushes.DarkGreen;
txbBox.Text = "Number Grid";

txbBox.HorizontalContentAlignment =
HorizontalAlignment.Center;

grid1.Children.Add(txbBox);
Grid.SetRow(txbBox, 0);
Grid.SetColumn(txbBox , 0);
Grid.SetColumnSpan(txbBox, 3);

// Add button1
Button button1 = new Button();
button1.FontSize = 30;
button1.Content = "1";
grid1.Children.Add(button1);
Grid.SetRow(button1, 1);
Grid.SetColumn(button1, 0);

// Add button2
Button button2 = new Button();
button2.FontSize = 30;
button2.Content = "2";
grid1.Children.Add(button2);
Grid.SetRow(button2, 1);
Grid.SetColumn(button2, 1);

// Add button3
Button button3 = new Button();
button3.FontSize = 30;
button3.Content = "3";
grid1.Children.Add(button3);
Grid.SetRow(button3, 1);
Grid.SetColumn(button3, 2);


// Add button4
Button button4 = new Button();
button4.FontSize = 30;
button4.Content = "4";
grid1.Children.Add(button4);
Grid.SetRow(button4, 2);
Grid.SetColumn(button4, 0);

// Add button5
Button button5 = new Button();
button5.FontSize = 30;
button5.Content = "5";
grid1.Children.Add(button5);
Grid.SetRow(button5, 2);
Grid.SetColumn(button5, 1);

// Add button6
Button button6 = new Button();
button6.FontSize = 30;
button6.Content = "6";
grid1.Children.Add(button6);
Grid.SetRow(button6, 2);
Grid.SetColumn(button6, 2);
}



參考資料:
MSDN- Grid 類別
WPF Tutorial - Grid

2009年8月10日 星期一

Multi-Touch Application - Photo Viewer

之前的文章有提到 WPF提供2D Transform的功能, 而這樣的功能很適合發展應用程式的手勢(Gesture)操控, 下面, 我就舉一個簡單的圖片瀏覽應用程式, 透過一些手勢操作, 來對圖片進行放大, 縮小, 旋轉, 翻轉和平移.

環境設定:
以下是我的環境設定:

  • 觸控裝置 - Quanta Optical Touch Monitor
  • Touch API - OTMUT(Optical Touch Monitor Utility Toolkit), 這隻Touch API是我們自行撰寫的dll, 主要用來取得觸控點和手勢的資料, 功能類似Windows 7 Touch SDK
當然, 你也可以自行使用一台具有觸控功能的電腦或螢幕, 然後搭配Windows 7 Touch SDK 來開發此程式.

視窗和控制項的設定:

開啟一個WPF專案, 然後從Toolbox中將一個Image的控制項拉到視窗中, 如下圖所示:


[圖 1] 新增Image control

在專案中新增一個Image的資料夾,並把要顯示圖片放進去, 然後在XAML檔中設定圖片的路徑, 如下所示:


[圖2] 設定圖片路徑
<Grid Background="Beige">
<Image Margin="150"
Name="image1"
Stretch="Fill"
Source="Image\Koala.jpg">

</Image>
</Grid>


設定Image 2D Transform:

我們在XAML檔中, 先設定ImageRotaeTransform, ScaleTransformTranslateTransform, 程式碼如下所示:

<Grid Background="Beige">
<Image Margin="150" Name="image1"
Stretch="Fill"
Source="Image\Koala.jpg"
RenderTransformOrigin=".5,.5">
<Image.RenderTransform>
<TransformGroup>
<RotateTransform x:Name="_rotate" Angle="0">
</RotateTransform>

<ScaleTransform x:Name="_scale"
ScaleX="1"
ScaleY="1">    
</ScaleTransform>

<TranslateTransform x:Name="_translate"
X="0"
Y="0">                        
</TranslateTransform>
</TransformGroup>
</Image.RenderTransform>
</Image>
</Grid>



Gesture Function:
我們在.cs檔中加入OTMUT 的 callback function, 當有偵測到Zoom In/Out, Rotate和 Pan手勢作用時, 則對圖片作transform, 如下所示:
void GestureFunction(tOTM_Gesture gesture)
{
double k;

switch (gesture.GestureType)
{
// Detect Pan gesture
case (int)tOTM_GestureType.OTM_GT_PAN:
{
// Display gesture type in console mode
System.Console.WriteLine("Pan");


if (gesture.GestureState == (int)tOTM_GestureState.OTM_GS_BEGIN)
{
g_FirstX = gesture.ActivePoint.X;
g_FirstY = gesture.ActivePoint.Y;
}
else
{
g_SecondX = gesture.ActivePoint.X;
g_SecondY = gesture.ActivePoint.Y;


// translate the image
_translate.X += g_SecondX - g_FirstX;
_translate.Y += g_SecondY - g_FirstY;


// We have to copy second point into first one to prepare
// for next pan message.
g_FirstX = g_SecondX;
g_FirstY = g_SecondY;
}

};
break;

// Detect zoom gesture
case (int)tOTM_GestureType.OTM_GT_ZOOM:
{
// Display gesture type in console mode
System.Console.WriteLine("Zoom");

if (gesture.GestureState == (int)tOTM_GestureState.OTM_GS_BEGIN)
{
g_bBeginZoom = true;
}
else if (gesture.GestureState == (int)tOTM_GestureState.OTM_GS_INERTIA)
{
if (g_bBeginZoom)
{
g_fGEArguments = gesture.Argument1;
g_bBeginZoom = false;
}
else
{
k = (double)gesture.Argument1 / g_fGEArguments;

// Zoom in/out the image
_scale.ScaleX *= (float)k;
_scale.ScaleY *= (float)k;
}
g_fGEArguments = gesture.Argument1;
}

};
break;

// Detect rotate gesture
case (int)tOTM_GestureType.OTM_GT_ROTATE:
{

// Display gesture type in console mode
System.Console.WriteLine("Rotate");

if (gesture.GestureState == (int)tOTM_GestureState.OTM_GS_BEGIN)
{
g_bBeginRotate = true;
}
else
{
if (g_bBeginRotate)
{
_rotate.Angle = gesture.Argument1;
}
else
{
k = (float)(gesture.Argument1 - g_fGEArguments);

// Rotate the image
_rotate.Angle += k;

}
g_fGEArguments = gesture.Argument1;
}
};
break;

// Detect the Press and Tap
case (int)tOTM_GestureType.OTM_GT_PRESS_TAP:
{
// Display gesture type in console mode
System.Console.WriteLine("Press and Tap");

// Flip the image
_scale.ScaleX *= -1;
};
break;

default:
break;
}
}

下面為程式的demo結果


[圖3] 初始畫面


[圖4] 縮小



[圖5] 放大



[圖6] 旋轉


[圖7] 平移



[圖7] 翻轉