2009年9月28日 星期一

在WPF中關閉觸控指標(Disable Touch Pointer)

在Win Vista之後,微軟針對觸控做了不少改變
其中很明顯的改變就是增加了觸控指標(Touch Pointer)
還有輕拂手勢(Flicks Gesture)
但這兩個新增的東西對於程式開發人員常常造成很討厭的干擾
(也許這就是為什麼在Win7上面這個功能不再是預設屬性了)

在早期的Win32視窗以及.net Form的應用程式之中
有提供底層的API來將這些有關觸控的新增訊息給取消的機制
請參考Disable flicks and mouse pointer

但要把這個方法套用到WPF上面的時候會遇到幾個問題
首先是要使用到unmanagement的Win32 API
這會對WPF的封閉性及安全性造成一些顧慮
另外則是在WPF中已經取消handle這個概念了
所以最多只能針對一整個視窗取消這個功能
要對單一物件基本上是不可能的

幸好WPF中另外提供了簡易的方式
對於希望關掉該功能的控制項,更改對應的屬性

private void DisableTouchStylus(UIElement u)
{
/// disable flicks gesture
Stylus.SetIsFlicksEnabled(u, false);

/// disable press and hold gesture
Stylus.SetIsPressAndHoldEnabled(u, false);

/// disable tap gesture
Stylus.SetIsTapFeedbackEnabled(u, false);

/// disable touch pointer
Stylus.SetIsTouchFeedbackEnabled(u, false);
}

另外,也可以在該控制項的XAML宣告裡面直接更改
例如希望關掉Canvas的觸控效果的話

<Canvas Stylus.IsFlicksEnabled="False"
Stylus.IsPressAndHoldEnabled="False"
Stylus.IsTapFeedbackEnabled="False"
Stylus.IsTouchFeedbackEnabled="False" />

注意,在WPF裡面設定的效果都會套用到所有的子元件
所以如果需要整個視窗都關閉的話,只需要在最外層的視窗設定就好了

--
參考資料
Disable flicks and mouse pointer
Stylus.IsFlicksEnabled 附加屬性

2009年9月25日 星期五

在WPF中自訂事件(Event)

在WPF中自訂控制項很容易
但是如果自訂控制項的時候除了提供一些函式之外
有時候希望可以提供一些事件

自訂事件的概念基本上跟callback的概念是一樣的
也就是註冊一個事件處理函式給某個事件
當事件發生的時候可以觸發該事件處理函式(常見的像MouseDown, Click等)
.net2.0之後提供了很方便的EventHandler機制

參考在WPF中新增自訂控制項,我們先制定了一個新的控制項
這個控制項裡面只有一個Rectangle

<UserControl x:Class="test_wpf.NewButton"
...>
<Rectangle />
</UserControl>

因為Rectangle沒有Click事件,所以我們就幫這個控制項新增Click事件

首先,每個事件處理都具有兩個參數
sender代表是哪個物件被觸發了這個事件
而e則是代表該事件的一些特性(比如滑鼠事件都會攜帶滑鼠訊息)
所以如果不希望用已經被制定好的事件屬性的話
那就自訂一個繼承EventArgs的事件屬性

public class MsgEventArgs : EventArgs
{
/// the only string argument
private string _Msg;

/// constructor of the event argument
public MsgEventArgs(string s)
{
_Msg = s;
}

/// define a readonly string argument
public string Msg
{
get { return _Msg; }
}
}

這邊我們定義了一個只帶有一個字串的事件屬性
但如果沒有特別需要傳遞的資訊,那也可以直接用EventArgs類別
就不需要做上面這步驟

之後再新增的這個控制項中宣告一個使用這個事件屬性的事件

public partial class NewButton : UserControl
{
/// an event with our custom event argment
public event EventHandler<MsgEventArgs> Click;

/// original constructor
public NewButton()
{
InitializeComponent();
}
}

同樣的,如果不需要自訂自訂屬性的話
紅字的部分就可以省略

一般習慣來說,會為了每個事件作一個事件觸發函式
這函式可以宣告成virtual,以便之後如果有繼承的子類別可以直接修改

protected virtual void OnClick(MsgEventArgs e)
{
/// if there exists handler for the event, run it
if (Click != null)
{
Click(this, e);
}
}

接下來就是在適當的時間觸發這個事件就好了
因為是Click,所以我們在滑鼠按下又放開的時候觸發
(這邊只是示範,並不是一般嚴謹定義的Click)

/// flag to show if left button is down
private bool IsDown = false;

private void Rectangle_PreviewMouseLeftButtonDown(
object sender, MouseButtonEventArgs e)
{
/// left button is down
IsDown = true;
}

private void Rectangle_PreviewMouseLeftButtonUp(
object sender, MouseButtonEventArgs e)
{
if (IsDown)
{
/// left button is up after down, click
OnClick(new MsgEventArgs("Button Click"));
}

/// reset the flag
IsDown = false;
}

這樣就完成宣告了,接下來就是在使用這個控制項的時候
(參考在WPF的XAML裡面使用自訂的控制項)
定義相關的事件處理函式

<Window ...
xmlns:test_wpf="clr-namespace:test_wpf">
<test_wpf:NewButton Click="Btn_Click"/>
</Window>


private void Btn_Click(object sender, MsgEventArgs e)
{
Console.WriteLine(e.Msg);
}

因為我們只有一個message對應的事件處理
所以這邊的事件處理,我們只將他show到console上面
(可以參考在WPF中滑鼠事件(Event)傳遞中的設定)

--
參考資料
HOW TO:發行符合 .NET Framework 方針的事件 (C# 程式設計手冊)
C#的事件處理和自定義事件(轉)
C# - Delgate(委派)和Event(事件)

2009年9月23日 星期三

在WPF中跨執行緒非同步的元件操作

透過在WPF中跨執行緒的元件操作所說的方式
可以很容易的在不同的執行緒(Thread)中更新使用者介面(UI)
不過如果當更新的動作太過頻繁,就會發現整個反應變慢
原因就是Dispatcher.Invoke()這個函式是同步的更新
也就是當呼叫的時候會等待更新完成才會回傳
這時候可以改採用非同步的方法,最簡單的方式就是改用BeginInvoke()
假設被用到的函式是

void Func(Type Param);

原本呼叫的地方(另一個執行緒中)本來應該是

Func(p);

直接改成(注意這邊的參數順序跟Invoke不太一樣)

Dispatcher.BeginInvoke(new Action<Type>(Func),
DispatcherPriority.Normal, p);

如此一來,呼叫端的程式就不會等到更新完成才接下去做事
這時候會發現被呼叫的頻率回覆了,但可能會延遲一段時間後一次更新

另外如果是一些特別的情形,還有幾種主動的非同步方式
例如使用這個函式的地方並不是固定的
那可以先檢查是否有處理的權限再作呼叫

if (!Dispatcher.CheckAccess())
{
Dispatcher.Invoke(DispatcherPriority.Normal,
new Action<Type>(Func), p);
}
else
{
Func(p);
}

這樣一來,就不是每一次都會把更新動作放進更新的序列中
而是將可以執行的部分直接動作,只做必要的Invoke
而這時要注意不要用BeginInvoke,因為執行順序可能會不符預期

另外一種就更特別,如果其實希望做的更新可以整合在一起一次做的話
比如原本是用迴圈呼叫Invoke來做更新
那就看看可不可以將迴圈放進Invoke的函式裡面
也就是一次更新多一些,而不是放一堆更新任務到序列中

--
參考資料
Dispatcher.BeginInvoke 方法
Invoke != Dispatcher.Invoke
Dispatcher.CheckAccess 方法

2009年9月18日 星期五

在WPF中滑鼠事件(Event)傳遞 - 列表框(ListBox)篇

在WPF中滑鼠事件(Event)傳遞中我們介紹了基本的滑鼠事件傳遞
按鈕篇則是在有按鈕的視窗中事件是如何傳遞
這邊我們再舉一個例子,列表框(ListBox),例如

<Window ... Name="Win">
<Grid Name="Grd">
<ListBox Name="Lsb">
<ListBoxItem>
<Rectangle Name="Rec" />
</ListBoxItem>
</ListBox>
</Grid>
</Window>

這邊為了簡化,所以列表框只有一個物件Rec
我們一樣將所有的物件的滑鼠事件都套用相同的處理函式
依照在WPF中滑鼠事件(Event)傳遞所作的設定
出來的結果是

Win : PreviewMouseLeftButtonDown
Win : PreviewMouseDown
Grd : PreviewMouseLeftButtonDown
Grd : PreviewMouseDown
Lsb : PreviewMouseLeftButtonDown
Lsb : PreviewMouseDown
Rec : PreviewMouseLeftButtonDown
Rec : PreviewMouseDown
Rec : MouseLeftButtonDown
Rec : MouseDown
Win : PreviewMouseLeftButtonUp
Win : PreviewMouseUp
Grd : PreviewMouseLeftButtonUp
Grd : PreviewMouseUp
Lsb : PreviewMouseLeftButtonUp
Lsb : PreviewMouseUp
Rec : PreviewMouseLeftButtonUp
Rec : PreviewMouseUp
Rec : MouseLeftButtonUp
Rec : MouseUp
Lsb : MouseLeftButtonUp
Lsb : MouseUp
Grd : MouseLeftButtonUp
Grd : MouseUp
Win : MouseLeftButtonUp
Win : MouseUp

比較在按鈕篇的事件傳遞,我們可以發現,在Down的流程是一模一樣的
可是Up卻全部都回傳了(包括preview和真實的事件)

這邊可以這樣解釋,由於滑鼠按下的時候,列表框處理了按下的動作
選取了某個物件,並將該事件標示成已處理
而在滑鼠起來的時候,則因為選取物件的動作已經處理完成
所以就不再作處理,而直接回傳,所以上層物件都會收到Up的事件

同樣的,我們也可以依照按鈕篇將事件強制處理,在這邊就不贅述了

在WPF中滑鼠事件(Event)傳遞 - 按鈕(Button)篇

在WPF中滑鼠事件(Event)傳遞中我們提到基本的滑鼠事件傳遞
但是如果這時候視窗中有一個按鈕(Button),例如

<Window ... Name="Win">
<Grid Name="Grd">
<Button Name="Btn">
<Rectangle Name="Rec" />
</Button>
</Grid>
</Window>

同樣將所有的滑鼠事件都套用到相同的事件處理函式
依照在WPF中滑鼠事件(Event)傳遞所作的設定
出來的結果會是

Win : PreviewMouseLeftButtonDown
Win : PreviewMouseDown
Grd : PreviewMouseLeftButtonDown
Grd : PreviewMouseDown
Btn : PreviewMouseLeftButtonDown
Btn : PreviewMouseDown
Rec : PreviewMouseLeftButtonDown
Rec : PreviewMouseDown
Rec : MouseLeftButtonDown
Rec : MouseDown
Win : PreviewMouseLeftButtonUp
Win : PreviewMouseUp
Grd : PreviewMouseLeftButtonUp
Grd : PreviewMouseUp
Btn : PreviewMouseLeftButtonUp
Btn : PreviewMouseUp
Btn : Click

我們這時候會觀察到一個奇怪的現像
就是在Down的過程時,preview的事件都還是出來了
不過在Rec回傳給Btn之後,MouseDown事件就不見了
而Up的地方更奇怪
Btn在preview完後,竟然直接出現了Click
而Rec甚至連preview都沒有收到

這邊的解釋是,在Down的時候,preview的事件順利的傳遞到了Rec
而在回傳的時候,因為Btn發現這是左鍵的Down
所以將這個事件標示為已處理(handled)再回傳
而之後Grd和Win再收到的就是已處理過的事件,所以他們就不會再做動作

而在Up的階段,preview的事件傳到了Btn的時候
他就已經發現現在應該是Click發生的時候了
所以他就處理Click的事件,然後一樣標示成已處理並直接回傳
所以Rec就什麼都收不到,而上層的Grd和Win則是收到已處理過的事件
所以它們也不再做動作了

如果仍然希望已處理的事件可以觸發事件處理
那就要用UIElement.AddHandler()來加入事件處理,例如

Btn.AddHandler(
/// the event we want to handle
Button.MouseLeftButtonDownEvent,
/// the event handler be set
new MouseButtonEventHandler(MouseEH),
/// handle the evnet even if it is handled
true);

這裡第三個參數是handledEventsToo,是否需要處理已處理事件
在一般的加入事件處理(不論XAML或C#)會採用預設值false
而我們因為希望即使是已處理事件也要觸發事件,所以設成true

如果將所有元件的MouseLeftButtonDown, MouseDown,
MouseLeftButtonUp, MouseUp都利用這種方式來加入事件處理
那輸出結果會變成

Win : PreviewMouseLeftButtonDown
Win : PreviewMouseDown
Grd : PreviewMouseLeftButtonDown
Grd : PreviewMouseDown
Btn : PreviewMouseLeftButtonDown
Btn : PreviewMouseDown
Rec : PreviewMouseLeftButtonDown
Rec : PreviewMouseDown
Rec : MouseLeftButtonDown
Rec : MouseDown
Btn : MouseLeftButtonDown
Btn : MouseDown
Grd : MouseLeftButtonDown
Grd : MouseDown
Win : MouseLeftButtonDown
Win : MouseDown
Win : PreviewMouseLeftButtonUp
Win : PreviewMouseUp
Grd : PreviewMouseLeftButtonUp
Grd : PreviewMouseUp
Btn : PreviewMouseLeftButtonUp
Btn : PreviewMouseUp
Btn : Click
Btn : MouseLeftButtonUp
Btn : MouseUp
Grd : MouseLeftButtonUp
Grd : MouseUp
Win : MouseLeftButtonUp
Win : MouseUp

記得,如果這時候原本的事件處理仍然有寫的話
那就會變成同時加入只處理未處理事件,以及處理所有事件的兩個事件處理
所以Rec的Down就會重複兩次(因為每次加入都會視為不同的delegate)

--
參考資料
wpf button 事件的觸發順序
UIElement.AddHandler 方法 (RoutedEvent, Delegate, Boolean)

2009年9月17日 星期四

在WPF中滑鼠事件(Mouse Event)傳遞

WPF和.net form的一個很大的差異就是
大部分的控制項都已經沒有Click(除了按鈕(Button)之外)
猜測大概是因為希望未來在觸控(Touch)的世界裡面可以順利接軌吧

另外,不像.net form裡面,只有最表面(可視)的部分才能接收到事件
針對幾乎所有的事件(Event)都有Preview的機制
也就是說在上層(Parent)先收到Preview,然後傳遞到下層(Child)
等到沒有下層,或是已經引發了事件發生的話再作回傳
然後由下層先引發真正的事件,再繼續回傳

舉例來說,如果有一個架構如下

<Window ... Name="Win">
<Grid Name="Grd">
<Rectangle Name="Rec" />
</Grid>
</Window>

而我們觀察的滑鼠事件如下
事件
PreviewMouseLeftButtonDown
PreviewMouseDown
PreviewMouseLeftButtonUp
PreviewMouseUp
MouseLeftButtonDown
MouseDown
MouseLeftButtonUp
MouseUp
Click

將想要觀察的滑鼠事件都加上相同的事件處理

private void MouseEH(object sender, RoutedEventArgs e)
{
Console.WriteLine(
(sender as FrameworkElement).Name + " : " +
e.RoutedEvent.Name);
}

因為這邊用到Console,所以要特別去設定為Console的模式
在工具列中找到專案->屬性

然後設定應用程式->輸出類型,選擇"主控台應用程式"

然後在按鈕中間的方塊按下去,會得到下面的結果

Win : PreviewMouseLeftButtonDown
Win : PreviewMouseDown
Grd : PreviewMouseLeftButtonDown
Grd : PreviewMouseDown
Rec : PreviewMouseLeftButtonDown
Rec : PreviewMouseDown
Rec : MouseLeftButtonDown
Rec : MouseDown
Grd : MouseLeftButtonDown
Grd : MouseDown
Win : MouseLeftButtonDown
Win : MouseDown
Win : PreviewMouseLeftButtonUp
Win : PreviewMouseUp
Grd : PreviewMouseLeftButtonUp
Grd : PreviewMouseUp
Rec : PreviewMouseLeftButtonUp
Rec : PreviewMouseUp
Rec : MouseLeftButtonUp
Rec : MouseUp
Grd : MouseLeftButtonUp
Grd : MouseUp
Win : MouseLeftButtonUp
Win : MouseUp

從這個例子可以看出最簡單的WPF滑鼠事件傳遞順序
更複雜的例子可以參考按鈕(Button)篇以及列表框(ListBox)篇

--
參考資料
WPF的Event

在WPF中加入主畫面(Key Frame)驅動的動畫

在WPF中加入動畫有簡單的動畫例子
不過如果使用者希望作一些比較複雜的動作,比如變紅之後變黃再變綠
那就沒辦法用單純的From和To來執行
這時候可以採用主畫面(Key Frame)的方法來設定動畫
依照主畫面轉換的效果一共可以分成三類
類別說明
discrete在設定的時間直接從前一個主畫面切換成此畫面,字串類別的動畫僅有此種主畫面
linear在設定的時間之前,利用線性內插(linear interpolation),從前一個主畫面漸進切換成此畫面
spline在設定的時間之前,利用樣條插值(splined interpolation),以指定Beizer曲線作為變化軌跡,從前一個主畫面漸進切換成此畫面

下面的例子會讓按鈕的背景顏色隨著時間而有不同變化

private void ApplyAnimation(Button b)
{
/// init a color animation using key frames
ColorAnimationUsingKeyFrames cak =
new ColorAnimationUsingKeyFrames();

/// set the total duration of each animation
cak.Duration = TimeSpan.FromSeconds(6);

/// Smooth change from current color to red in the
/// first 2 sec

cak.KeyFrames.Add(
new LinearColorKeyFrame(
Brushes.Red.Color,
TimeSpan.FromSeconds(2)));

/// Sudden jump to yellow at 2.5 sec
cak.KeyFrames.Add(
new DiscreteColorKeyFrame(
Brushes.Yellow.Color,
TimeSpan.FromSeconds(2.5)));

/// By the keyspline property, smooth change from
/// yellow to green in the next 2 sec

cak.KeyFrames.Add(
new SplineColorKeyFrame(
Brushes.Green.Color,
TimeSpan.FromSeconds(4.5),
new KeySpline(0.6,0,0.9,0)));

/// set the background of input button as a solid
/// color brush and apply the animation on it

b.Background = new SolidColorBrush();
(b.Background as SolidColorBrush).BeginAnimation(
SolidColorBrush.ColorProperty, cak);
}

--
參考資料
ColorAnimationUsingKeyFrames 類別
樣條插值- 維基百科,自由的百科全書

2009年9月16日 星期三

在WPF中解除動畫綁定的屬性

在WPF中對某個屬性使用動畫的時候
原本的值會被忽略而直接由動畫管理需要呈現的數值
除了下面幾種情形,常會發生數據被綁定的現象

一種是在FillBehavior被設定為FillBehavior.Stop時
在動畫完成後,變化的屬性會自動被設定回原本的值
在此之後再對該值作動作則不會被動畫影響到

第二種則是僅使用From
則該屬性原本的值會被設定為終點
所以在此之後(甚至動畫執行期間)對該值做動作仍會發生作用

第三種是在動畫執行過後重新指定綁定的動畫(設成null即解除動畫)
不過這種方法跟第一種方法一樣
重新綁定的時候,該值會被設定成原本的值
如果需要將值保留下來,則在重新綁定之前先存起來再重設回來

private void ReleaseAnimation(Button b)
{
/// save width of button and set it back
double w = b.Width;
b.Width = w;

/// apply null animation to button
b.BeginAnimation(Button.WidthProperty, null);
}

此時需注意,重新設值必須放在重新綁定動畫之前
因為在還沒有解除綁定前,設定會更改真實的值
所以在解除綁定後,就會直接變成該值
如果先解除綁定在設定新值,則可能會出現閃爍或跳躍的影響

--
參考資料
WPF/Silverlight:解鎖被Storyboard束縛的關聯屬性

在WPF中加入動畫

WPF整合了DirectX的動態處理,內建了動畫的控制項
不過對於原本沒有在使用動畫或是flash的程式設計師
其實還是很難以想像要怎麼使用動畫的元件

基本上WPF提供的動畫依照動作的素材可以分成四類
表示在時間軸上所對應屬性的變化
素材說明
double套用在數值類型的屬性上
color套用在筆刷(顏色)屬性上
point套用在位置(座標)屬性上
string套用在字串屬性上

另外,也可以依照動畫的軌跡類型做分類
軌跡說明
僅有起點與終點將對應的屬性由起點變化到終點,字串類形(string)無此此軌跡
設定主畫面(KeyFrame)設定需要經過的主畫面,可選擇性設定每張主畫面應該出現的時間點,並可以選擇每個主畫面的切換效果
給定路徑(Path)給定屬性應該變化的路徑,僅適用於數值類型(double)以及位置類型(point)

如果希望設定一個按鈕的寬度由100到200

private void ApplyAnimation(Button b)
{
/// init a double animation
DoubleAnimation da = new DoubleAnimation();

/// set the from and to value of button width
da.From = 100;
da.To = 200;

/// set the duration of each animation
da.Duration = TimeSpan.FromSeconds(3);

/// apply the double animation to width of button
b.BeginAnimation(Button.WidthProperty, da);
}

這是最簡單的動畫了,只有設定起終點以及花費的時間
在DoubleAnimation中設定起終點的方式還有好幾種
指定欄位說明
From和To從From的數值,一直變化到To的數值
From和By從From的數值,一直變化到From+By的數值
From從From的數值,一直變化到原有的值(原本必須要有值)
To從原有的值(原本必須要有值),一直變化到To的數值
By從原有的值(原本必須要有值),一直變化到該值+By的數值

另外還有一些常用的設定可以參考下表
屬性說明
AccelerationRatio加速度,從0加速到最大的變化比率,0~1
DecelerationRatio減速度,從最大速度減速到0的變化比率,0~1, AccelerationRatio與DecelerationRatio總和亦需介於0~1之間
AutoReverse動畫結束時,再將起始點互換播放一次
RepeatBehavior重複設定,設為RepeatBehavior.Forever即會不斷重複播放(包含AutoReverse的設定)
FillBehavior動畫完成後的動作(若RepeatBehavior被設定,因為動畫沒有完成的時候,所以不會作用),若設成FillBehavior.Stop則會讓關聯的屬性回覆到原有的值(並非From的值),若不設定則預設為FillBehavior.HoldEnd,也就是保持終點的值

另外,這邊提供的是最簡易的加入動畫的方法
如果想要知道怎麼使用主畫面來設定動畫,可以參考
在WPF中加入主畫面(Key Frame)驅動的動畫
在XAML中還需要加入腳本(StoryBoard)才可以使用動畫
可以參考在WPF的XAML裡面使用動畫

--
參考資料
動畫概觀
HOW TO:不使用腳本而建立屬性的動畫
DoubleAnimation 類別
DoubleAnimation 成員

2009年9月11日 星期五

在WPF中的畫面重排(Measure & Arrange)

WPF相較於前代的Form最大的變化,就是把展示的部分做了很大的改動
會多做一些自動化(如不需要重繪)或最佳化(如版面的非同步配置)
在性能提升的同時,也會造成一些麻煩,就是比較難以自我掌控狀況

在WPF裡面的UIElement提供了Measure和Arrange
讓程式開發者可以自主的控制版面配置

Measure提供的功能是讓系統知道這個控制項需要多大的空間
預設會遞迴的向他裡面所包含的子控制項問下去,之後再回報所需要的大小
Arrange提供的功能則是將Measure出來的位置擺到選定的位置上
這個位置是相對於父物件而言的,預設中同樣也會遞迴的將子控制項排好

而在使用這兩個函式的時候主要有兩種方式
一個是覆寫掉系統中預設的函式,讓這個元件依照你的設計來排版
這個方式主要用在寫自訂的控制項時,而且只能安排到你的下一層
而再向下的子控制項就只能依照預設的方式處理了(除非底層也被覆寫)

第二種方式則是直接呼叫,這種方法並不會真正的改變控制項的屬性
只是暫時的排版而已,常用在輸出圖檔的時候
可以參考在WPF中存圖檔的問題(RenderTargetBitmap)
呼叫Measure的時候要傳入一個Size的物件
傳入的大小如果比預設需求小,那之後WPF會自動發生一次重新測量及排列
所以在畫面上完全看不出來有被更改過
(但是如果在同一個函式中有輸出圖檔,該圖會依照改動的大小排列)
預設需求會包含物件的大小,以及設定的margin或是border thinkness等

呼叫Arrange的時候要傳入一個Rect,位置是相對於父物件的原點
將前一次Measuer的大小塞入這個Arrange的大小中
之後會依照該控制項原本就設定好的Margin等屬性自動排列

需要注意的有三點
第一,除了第一次呼叫Arrange之前須要先Measure之外(系統做的也算)
之後都可以只呼叫Arrange來重排而不需要重新測量大小,比如對齊
第二,只要呼叫了Measure就必須在後面接一個Arrange
否則系統也會自動加上一個預設的Arrange來依照此大小重排
第三,此兩個函式分別有各自的佇列(Queue)
但任何的Measure都會讓尚未處理的Arrange直接失效

--
參考資料
UIElement.Measure 方法
UIElement.Arrange 方法

2009年9月10日 星期四

在WPF中存圖檔的問題(RenderTargetBitmap)

在WPF裡面要把一個視覺化物件給存成圖檔
需要使用到RenderTargetBitmap,可以參考在WPF中存讀圖檔

但是有時候會發現存出來的圖檔被位移或是根本就沒有影像
這是因為RenderTargetBitmap(RTB)在處理視覺化物件的描繪(Render)時
會依照該物件和其父物件(Parent)的相關性做處理
也就是會因為該物件的Margin或其父物件的Padding或BorderThickness
如果完全不做處理,那就會以父物件的左上角為準,多出一個位移
雖然我覺得這應該算WPF的bug
但是依照MicroSoft的解釋RenderTargetBitmap layout offset influence
似乎覺得這是應該發生的,而且不會再改的感覺

基本上目前我知道的解決方法有三種
方法說明
加一層Border簡單,但是會改變視覺物件結構樹
用VisualBrush畫出不改動到原本架構,但需要多做一些繪製的處理
用Measure和Arrange暫時改變相對位置改動後需要手動改回,而且WPF有針對這兩個函式作最佳化處理,所以不容易瞭解要怎麼用

第一個方法最單純,因為RTB只針對父物件作位移
所以就直接在你需要描繪的物件外面直接包一層類似像Border的容器
而且把需要用的margin都移到這個容器
之後就可以直接依照RTB的使用方法直接用了

如果原本的物件是這樣放

<Grid>
<Canvas Margin="20" />
</Grid>

那就改成

<Grid>
<Border Margin="20">
<Canvas />
</Border>
</Grid>

這樣就可以用之前在在WPF中存讀圖檔提到的方法
將物件送進SaveTo()存起來

private void SaveTo(Visual v, string f)
{
/// get bound of the visual
Rect b = VisualTreeHelper.GetDescendantBounds(v);

/// new a RenderTargetBitmap with actual size of c
RenderTargetBitmap r = new RenderTargetBitmap(
(int)b.Width, (int)b.Height,
96, 96, PixelFormats.Pbgra32);

/// render visual
r.Render(v);

/// new a JpegBitmapEncoder and add r into it
JpegBitmapEncoder e = new JpegBitmapEncoder();
e.Frames.Add(BitmapFrame.Create(r));

/// new a FileStream to write the image file
FileStream s = new FileStream(f,
FileMode.OpenOrCreate, FileAccess.Write);
e.Save(s);
s.Close();
}

第二種方法則是先將該視覺物件畫出來
直接將想要繪製的物件傳入修改過的SaveTo()
只是在render之前先將視覺物件作一個轉換

private DrawingVisual ModifyToDrawingVisual(Visual v)
{
/// new a drawing visual and get its context
DrawingVisual dv = new DrawingVisual();
DrawingContext dc = dv.RenderOpen();

/// generate a visual brush by input, and paint
VisualBrush vb = new VisualBrush(v);
dc.DrawRectangle(vb, null, b);
dc.Close();

return dv;
}

這時候要注意,使用context的時候要記得做close()才會真正做動作
如果怕忘記的話可以改用using將使用的區塊包起來
而原本在SaveTo裡面的Render就改寫成

/// render visual
r.Render(ModifyToDrawingVisual(v));


第三種方法是用Measure和Arrange暫時將位置換一下
一樣在render之前將視覺物件做個轉換

private void ModifyPosition(FrameworkElement fe)
{
/// get the size of the visual with margin
Size fs = new Size(
fe.ActualWidth +
fe.Margin.Left + fe.Margin.Right,
fe.ActualHeight +
fe.Margin.Top + fe.Margin.Bottom);

/// measure the visual with new size
fe.Measure(fs);

/// arrange the visual to align parent with (0,0)
fe.Arrange(new Rect(
-fe.Margin.Left, -fe.Margin.Top,
fs.Width, fs.Height));
}

這邊要注意這種方法只能套用在UIElement上面
另外要記得要把位置arrange回來

private void ModifyPositionBack(FrameworkElement fe)
{
/// remeasure a size smaller than need, wpf will
/// rearrange it to the original position

fe.Measure(new Size());
}

因為這時候measure的大小較實際上的大小為小
所以wpf會自動重新做排版,將位置對齊回去
而原本的render則是改寫成

/// render visual
ModifyPosition(v as FrameworkElement);
r.Render(v);
ModifyPositionBack(v as FrameworkElement);

如果對measure和arrange有興趣的可以參考UIElement.Measure 方法

--
參考資料
RenderTargetBitmap layout offset influence
RenderTargetBitmap tips
RenderTargetBitmap and XamChart - Broken, Redux

2009年9月9日 星期三

在C#中排序自訂型別陣列(Sorting custom array)

在C#中要對一些基本型態的陣列作排序相當簡單

/// init an integer array
int[] a = { 10, 9, 1, 2, 6, 5, 4 };

/// sort the array
Array.Sort(a);

這樣的方法可以用在已經有實作IComparison的基本型態上
例如int, string, 或是 DateTime...等

如果要針對自訂型態的物件陣列作排序
基本概念就是要能夠比較兩個物件的順序
假設自訂類別如下

public class T1
{
public int index;
public string name;
}

實作有兩種方法,其一就是將該類別實作IComparable
所以需要將類別定義改為

public class T1 : IComparable
{
public int index;
public string name;

/// implement IComparable interface
public int CompareTo(object obj)
{
return this.index - (obj as T1).index;
}
}

實作IComparable實際上需要做的就是CompareTo這個函式
也就是自己與一個傳入的obj比較,然後回傳一個整數
回傳說明
0兩者相等,順序無所謂
正數現在的物件(this)較比較對象(obj)為大,順序在後
負數現在的物件(this)較比較對象(obj)為小,順序在前

這樣實做出來的類別,就可以直接像基本型態一樣直接排序了

private void SortT1(T1[] t)
{
Array.Sort(t);
}

上述的方法有個缺點,排序T1這個類別只能使用一種方法
也就是如果有時候想要依照index排序,而有時又希望可以用name排序的話
就無法用這個方法實現了,這時候我們就要另外實作比較的函式

private int CompareT1ByIndex(T1 t1, T1 t2)
{
return t1.index - t2.index;
}

這個函數和CompareTo的差別就是傳入是兩個T1物件
然後兩者的比較方式和CompareTo是一樣的
之後要做排序的函式就可以改寫成

private void SortT1(T1[] t)
{
Array.Sort(t, CompareT1ByIndex);
}

這樣就可以看出來,如果我們希望使用name來作排序
其實只要另外實作一個CompareT1ByName的函式就可以做到

另外,需要注意的是Array.Sort的實作排序是quick sort,屬於不穩定的排序
所以兩個物件如果相同的話,順序並不一定會依照原本的順序

--
參考資料
Array.Sort(T) 方法 (T[], Comparison(T))
Sorting Arrays [C#]
Array.Sort 泛型方法 (T[])

2009年9月4日 星期五

在WPF中計時

需要計時的時候可以使用Stopwatch這個類別來計算(精密度可以到ms)

private Stopwatch StartCount()
{
Stopwatch s = new Stopwatch();

/// reset the clock
s.Reset();

/// start to count the time
s.Start();

/// return stopwatch for closing
return s;
}

private double StopCount(Stopwatch s)
{
/// stop to count the time
s.Stop();

/// return the total time passed in ms
return s.Elapsed.TotalMilliseconds;
}

--
參考資料
想計算程式執行時間,結果卻是?

在WPF中加入提示畫面(Splash Screen)

Splash Screen算是相當常用的特效
比較常見於應用程式啟動時,會顯示一些版本或是著作權資訊
然後並沒有特別做什麼動作就自動漸出了
跟Android裡面的Toast還蠻像的
不過Splash Screen一般用來顯示圖片,而Toast則是文字訊息

如果是要在程式啟動時顯示Splash Screen
可以直接在想要顯示的圖檔的屬性視窗中選擇建置動作(Build Action)
選為SplashScreen即可

如果希望在使用者做出某個動作(如按下About按鈕)的時候顯示的話
那就將在該動作的事件處理函式中呼叫

private void ShowSplashScreen()
{
/// new a splash screen with resource name
SplashScreen s = new SplashScreen("img.png");

/// show the splash screen and close automatically
s.Show(true);
}

如果希望該畫面可以停留到另外動作(如使用者按下或移動滑鼠)發生時
那就將Show裡面的參數換成false
然後在停止動作的事件處理函式中呼叫

private SplashScreen ShowSplashScreen()
{
/// new a splash screen with resource name
SplashScreen s = new SplashScreen("img.png");

/// show and hold until close
s.Show(false);

/// return splash screen for stop
return s;
}

private void CloseSplashScreen(SplashScreen s, int t)
{
/// close with fade out time t in 100 ns
s.Close(new TimeSpan(t));
}

這裡的時間是漸出(fade out)持續的時間,以100ns為單位計算
所以如果需要讓漸出時間(從開始淡化到完全消失)為1秒的話
那t要傳入10,000,000才行

--
參考資料
Splash Screen in WPF 3.5 SP1

在C# Form中引用被封裝的資源檔(影像)

要把影像檔封裝到C# form的執行檔或函式庫裡面很容易
請參考在C#中的封裝資源檔
並請記得將建置動作(Build Action)設成內嵌資源(EmbeddedResource)
同時,依照放置位置的不同,引用方法也不同
我們將四張圖檔依照上面的方法分別放於四個位置
檔名位置
img.png直接置於該應用程式專案裡面
img_folder.png放在應用程式專案底下的folder內
img_dll.png直接置於參考的函式庫test_dll裡面
img_folder_dll.png放在參考的函式庫test_dll裡面的folder內

在C#裡面引用的話(i1-i4是PictureBox控制項物件)

/// get application assembly by file name
Assembly app = Assembly.LoadFrom("test_cs.exe");
Assembly dll = Assembly.LoadFrom("test_dll.dll");

/// image at project
i1.Image=Image.FromStream(
app.GetManifestResourceStream(
"test_cs.img.png"));

/// image in folder at project
i2.Image=Image.FromStream(
app.GetManifestResourceStream(
"test_cs.folder.img_folder.png"));

/// image at dll
i3.Image=Image.FromStream(
dll.GetManifestResourceStream(
"test_dll.img_dll.png"));

/// image in folder at dll
i4.Image=Image.FromStream(
dll.GetManifestResourceStream(
"test_dll.folder.img_folder_dll.png"));

注意,GetManifestResourceStream的參數字串
第一個部分不是檔名,而是命名空間(namespace)
命名空間.資料夾.檔名.附檔名

--
參考資料
Image Resources in WinForms and WPF
圖檔資源載入與不規則 Form 的製作

2009年9月3日 星期四

在WPF中引用被封裝的資源檔(影像)

要把影像檔封裝到WPF的執行檔或函式庫裡面很容易
請參考在C#中的封裝資源檔
並請記得將建置動作(Build Action)設成Resource
同時,依照放置位置的不同,引用方法也不同
我們將四張圖檔依照上面的方法分別放於四個位置
檔名位置
img.png直接置於該應用程式專案裡面
img_folder.png放在應用程式專案底下的folder內
img_dll.png直接置於參考的函式庫test_dll裡面
img_folder_dll.png放在參考的函式庫test_dll裡面的folder內

在XAML裡面引用的方法

<!--image at project-->
<Image Name="i1" Source=
"img.png" />

<!--image in folder at project-->
<Image Name="i2" Source=
"folder/img_folder.png" />

<!--image at dll-->
<Image Name="i3" Source=
"/test_dll;component/img_dll.png" />

<!--image in folder at dll-->
<Image Name="i4" Source=
"/test_dll;component/folder/img_folder_dll.png" />

而如果是在C#裡面引用的話(i1-i4是Image控制項物件)

/// image at project
i1.Source = new BitmapImage(new Uri(
"img.png",
UriKind.Relative));
/// image in folder at project
i2.Source = new BitmapImage(new Uri(
"folder/img_folder.png",
UriKind.Relative));
/// image at dll
i3.Source = new BitmapImage(new Uri(
"/test_dll;component/img_dll.png",
UriKind.Relative));
/// image in folder at dll
i4.Source = new BitmapImage(new Uri(
"/test_dll;component/folder/img_folder_dll.png",
UriKind.Relative));

注意使用函式庫的時候不需要加.dll
(如上面的函式庫檔名為test_dll.dll,但在使用時僅需用test_dll即可)
在這邊都採用相對路徑的寫法,如果想用絕對路徑的話

/// image at project
i1.Source = new BitmapImage(new Uri(
"pack://application:,,,/img.png"));

在這邊application的意思是本機端的路徑
因為Uri也可以使用在網路上的路徑
而且這時候就不需要特別註明是絕對路徑
預設Uri的建構會判斷為絕對路徑

另外,需要注意的地方是
即使你是在函式庫中使用被封裝在自己函式庫中的資源
還是必須採用放在函式庫中的寫法
這樣真正執行那段程式碼的應用程式才有辦法識別

還有,如果是設定ImageBrush時,可能因為ImageBrush的ImageSource會在使用到的時候才會去解析Uri
所以需要寫絕對路徑,相對路徑會解析錯誤

--
參考資料
Windows Presentation Foundation 中的 Pack URI
WPF 封裝 圖片resource的問題 c#
Resources in WPF – I (Binary Resources)
Image Resources in WinForms and WPF

在C#中的封裝資源檔

在程式裡面有時候會用到圖檔或是影片之類的資源檔
但是由於安全或是方便的考量
不希望把這些檔案放在執行檔(.exe)或程式庫(.dll)之外
C#提供了很方便的方式讓使用者可以輕易的將這些檔案加入專案

選取想要加入的檔案即可
當然,如果不喜歡一堆檔案放在一起,也是可以用資料夾整理起來的

這時候只要在方案總管(Solution Explorer)裡面選取該檔案
然後在屬性視窗(Properties Window)選擇建置動作(Build Action)

這時候就需要依照檔案的種類以及讀取的方式來決定要怎麼選擇
一般來說,如果是圖檔的話,建議在WPF專案就選擇Resource
而C# form專案則是選內嵌資源(Embedded Resource)
選項說明
無(None)檔案未包含在專案輸出群組中,而且在建置過程中不加以編譯,例如讀我檔案(.txt)
編譯(Compile)將檔案編譯到建置輸出,例如程式碼檔(.cs)
內容(Content)不編譯檔案,但會將其加入內容輸出群組,例如網頁檔案(.htm)
嵌入資源
(Embedded Resource)
將檔案以 DLL 或可執行檔嵌入主要專案建置輸出,例如影像檔案(.bmp)
ApplicationDefinition定義應用程式相關屬性所用,轉換後編譯,例如tag為Application的xaml檔案(.xaml)
Page定義視窗或控制項等頁面相關屬性所用,轉換後編譯,例如tag為Window的xaml檔案(.xaml)
SplashScreen在程式載入時先顯示的畫面,將檔案嵌入主要專案建置輸出,例如影像檔案(.bmp)
Resource將檔案以分享的方式嵌入主要專案建置輸出,例如影像檔案(.bmp)
EntityDeployEntity Framework的建置工作,可以內嵌為資源或是寫入檔案中,例如內嵌檔案(.edmx)

--
參考資料
檔案屬性
What are the various 「Build action」 settings in
VS.NET project properties and what do they do?

EntityDeploy 類別
圖檔資源載入與不規則 Form 的製作

2009年9月2日 星期三

在WPF中自訂強制回應對話框(Dialog Box)

就像在WPF中的對話框(Dialog Box)中說的
新增一個對話框其實跟新增視窗沒有差別

從視窗的設定來說,強制回應對話框至少要有可以跟使用者互動(對話)的功能,而且除了簡單的通知功能之外(MessageBox),通常會希望使用者能夠表達出他的選擇,所以一般來說,對話框至少需要正反兩個選項。


<Window x:Class="test_wpf.NewDialog"
...>
<Grid>
<!--set an event handler as button clicked-->
<Button Width="100" HorizontalAlignment="Left"
Margin="10,10,0,10" Click="Ok_Click">
OK
</Button>
<!--a button with IsCancel property is just
like the close button in title bar, so there
is no need to add a click event handler-->

<Button Width="100" HorizontalAlignment="Right"
Margin="0,10,10,10" IsCancel="True">
Cancel
</Button>
</Grid>
</Window>

注意,這時候因為第二個按鈕設成IsCancel,所以按下它相當於按下視窗預設的關閉鍵,而此時視窗關閉,並將DialogResult設成false(英文版和中文版的msdn寫的不同,但經測試回傳為false),所以不需要額外寫事件處理,而第一個按鈕的事件處理如下

private void Ok_Click(object sender, RoutedEventArgs e)
{
/// set DialogResult and the window will close
/// automatically and no need to call Close()

DialogResult = true;
}

此時因為設定DialogResult,視窗自動關閉,不需要額外呼叫Close()

使用對話框其實就是使用視窗,只是Show出來的方法不同

NewDialog d = new NewDialog();
if ((bool)d.ShowDialog())
{
/// process when DialogResult is true
MessageBox.Show("OK");
}
else
{
/// process when DialogResult is false
MessageBox.Show("Cancel");
}

--
參考資料
對話方塊概觀

2009年9月1日 星期二

在WPF中的對話框(Dialog Box)

在WPF中,對話方塊跟視窗已經合併在一起
所以自訂對話方塊實際上也是定義一個視窗

一般來說,Dialog分為兩種,強制和非強制回應
區別強制回應非強制回應
呼叫方法ShowDialog()Show()
原視窗在對話框執行期間無法操作仍然可以繼續操作
回傳時機在DialogResult被設定後直接回傳,回傳後對話框自動關閉(無需另作Close)呼叫後直接回傳,對話框繼續運作,直到對話框被關閉
範例存檔對話框尋找對話框

一般常用的是強制回應的對話框,像是內建的存讀檔對話框
所謂的強制回應就是當對話框出來之後
使用者必須先針對對話框的訊息做出回應(確定或取消)
然後才會將主控權交回給原視窗
請參考在WPF中自訂強制回應對話框(Dialog Box)

而非強制回應的對話方塊其實就是視窗
唯一的差別只是這個視窗需要跟原本呼叫他的視窗作互動
所以可能需要宣告一個事件處理來達成這個互動

--
參考資料
對話方塊概觀