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

2010年12月21日 星期二

在WPF中發出鍵盤事件

WPF沒有控制鍵盤的API
所以要發出鍵盤事件(例如按下"J")就必須要呼叫Win32的API了

private enum KeyEventFlag
{
KEYEVENTF_KEYDOWN = 0x0000,
KEYEVENTF_EXTENDEDKEY = 0x0001,
KEYEVENTF_KEYUP = 0x0002,
}

[DllImport("user32.dll", SetLastError = true)]
private static extern void keybd_event(
byte bVk, byte bScan, KeyEventFlag dwFlags,
IntPtr dwExtraInfo);

這時發生一個問題,bVk是以Win32中定義的Virtual-Key Codes做為對照
但是在WPF中所採用的是Key 列舉型別
需要使用KeyInterop做轉換,所以實際上發出按下鍵盤的函式如下

private void SendKeyDown(Key key)
{
keybd_event(
(byte)KeyInterop.VirtualKeyFromKey(key), 0,
KeyEventFlag.KEYEVENTF_KEYDOWN, IntPtr.Zero);
}

另外,如果要送出放開鍵盤的事件,那就把KEYEVENTF_KEYDOWN換成KEYEVENTF_KEYUP,而連續送出一個按下加上一個放開,就變成Click了
同理,要發出復合鍵則是再放開前按下所有鍵,再放開即可

--
參考資料
.NET - How can I convert 'System.Windows.Input.Key' to 'System.Windows.Forms.Keys'?

2010年5月25日 星期二

將WPF介面多國語言化(Localization)

要將WPF介面變成支援多國語言首先要注意一個問題
因為介面可能同時定義在XAML或C#檔案中
所以如果使用傳統的resx或是單純讀文字檔的方法是沒辦法直接做到的
(當然,如果把所有的介面文字都使用C#定義也是可行
不過這樣就不太符合WPF的精神了)

這時候需要加入新的資源字典(ResourceDictionary)

資源字典是一個XAML檔案,通常的命名會以CultureInfo的Name命名
(例如en-US.xaml或zh-TW.xaml)
這邊我們定義了一系列的字串,並賦予對應的索引(key)

<ResourceDictionary
...
xmlns:sys="clr-namespace:System;assembly=mscorlib">

<sys:String x:Key="OK">
OK
</sys:String>
<sys:String x:Key="Msg">
Msg
</sys:String>
</ResourceDictionary>

繁體中文的語系檔如下

<ResourceDictionary
...
xmlns:sys="clr-namespace:System;assembly=mscorlib">

<sys:String x:Key="OK">
確定
</sys:String>
<sys:String x:Key="Msg">
訊息
</sys:String>
</ResourceDictionary>

必須注意的是,此時此資源字典的BuildAction可以設定為Page或Content

設為Page的時候,此檔案會直接被包含到應用程式中
通常在預設語系會採用此種做法(確保至少有一種語言字典可以被找到)

而設成Content的時候,將會將此檔案獨立出來
此時,複製到輸出目錄(CopyToOutputDirectory)
需設成有更新時才複製(Copy if newer)或永遠複製(Copy always)

這種方式的好處是可以彈性的加入或移除某種語系的支援而不需要重新編譯
但是就可能會有很多檔案,在發佈的時候會造成一些麻煩
而且使用者可以任意的改動語系檔案,可能造成其他的錯誤

另外一種就是將非預設語系都包成另一個Dll(全部都設成page)
動態載入需要的語系,這樣一樣可以在不需要重新編譯程式的狀態下換語系檔
也可以保有語系檔的封閉性,只是在讀的時候可能會稍微麻煩一些
可以參考在WPF中引用被封裝的資源檔(影像)

在定義資源檔後,可以透過

private void LoadLanguage()
{
ResourceDictionary rd = null;

/// get current culture name
string lang = CultureInfo.CurrentCulture.Name;

try
{
rd = Application.LoadComponent(
new Uri(lang + " .xaml", UriKind.Relative))
as ResourceDictionary;
}
catch
{
}

if (rd != null)
{
/// find current lang xaml, replace default
if (Resources.MergedDictionaries.Count > 0)
{
Resources.MergedDictionaries.Clear();
}
Resources.MergedDictionaries.Add(rd);
}
}

將語系檔載入到應用程式的資源裡面

而在使用的地方則可以使用DynamicResource去讀取

<Button Content="{DynamicResource OK}"
Click="BtnClick" />

如果是在C#檔裡則是可以用FindResource去讀

private void BtnClick(object sender, RoutedEventArgs e)
{
MessageBox.Show(FindResources("Msg") as string);
}

此時會從物件本身開始向上查找,直到找到或Application層級為止

--
參考資料
關於讓WPF軟件界面支持全球化和本地化
WPF - Resource
FrameworkElement.FindResource 方法
資源概觀

2010年5月19日 星期三

在WPF中觸控事件的捕捉

在WPF中觸控事件(Stylus Event)傳遞
我們可以觀察到一般觸控事件的傳遞
但是如果我們在中間的矩形按下,卻移開到視窗之外再釋放
那Up的事件全部都不會發生了,這時候就需要用到捕捉了

捕捉有兩種,CaptureMouse以及CaptureStylus
經過實際測試,這兩個動作是完全一致的
(也許是為了讓寫觸控AP的RD不要更動太多)

先在PreviewMouseLeftButtonDown的地方做CaptureMouse

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

記得要在對應的地方(PreviewMouseLeftButtonUp)將滑鼠捕捉釋放掉

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

為了更清楚的看出捕捉的效果,我們將這兩個事件放在Grd上
得出的結果如下

Win : PreviewStylusDown
Grd : PreviewStylusDown
Rec : PreviewStylusDown
Rec : StylusDown
Grd : StylusDown
Win : StylusDown
Win : PreviewMouseLeftButtonDown
Grd : PreviewMouseLeftButtonDown
Rec : PreviewMouseLeftButtonDown
Grd : MouseLeftButtonDown
Win : MouseLeftButtonDown
Win : PreviewStylusUp
Grd : PreviewStylusUp
Grd : StylusUp
Win : StylusUp
Win : PreviewMouseLeftButtonUp
Grd : PreviewMouseLeftButtonUp
Rec : MouseLeftButtonUp
Grd : MouseLeftButtonUp
Win : MouseLeftButtonUp

而如果在PreviewStylus事件中捕捉Stylus的話
結果如下

Win : PreviewStylusDown
Grd : PreviewStylusDown
Rec : PreviewStylusDown
Grd : StylusDown
Win : StylusDown
Win : PreviewMouseLeftButtonDown
Grd : PreviewMouseLeftButtonDown
Grd : MouseLeftButtonDown
Win : MouseLeftButtonDown
Win : PreviewStylusUp
Grd : PreviewStylusUp
Rec : StylusUp
Grd : StylusUp
Win : StylusUp
Win : PreviewMouseLeftButtonUp
Grd : PreviewMouseLeftButtonUp
Rec : PreviewMouseLeftButtonUp
Rec : MouseLeftButtonUp
Grd : MouseLeftButtonUp
Win : MouseLeftButtonUp

從上面兩個結果中可以看出
不論是在滑鼠或是觸控事件中捕捉,都會將該事件繼續傳遞下去
但在該事件傳遞結束後到觸發釋放事件傳遞結束前
所有下層的物件都不再收到任何滑鼠或觸控的事件

在WPF中觸控事件(Stylus Event)傳遞

在單點觸控發生時,Windows會觸發Stylus及Mouse的事件
參考在WPF中滑鼠事件(Mouse Event)傳遞中使用的結構

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

因為我們這次觀察的重點是Mouse跟Stylus的交互作用
所以觀察的事件如下
事件
PreviewMouseLeftButtonDown
PreviewMouseLeftButtonUp
MouseLeftButtonDown
MouseLeftButtonUp
PreviewStylusDown
PreviewStylusUp
StylusDown
StylusUp

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

Win : PreviewStylusDown
Grd : PreviewStylusDown
Rec : PreviewStylusDown
Rec : StylusDown
Grd : StylusDown
Win : StylusDown
Win : PreviewMouseLeftButtonDown
Grd : PreviewMouseLeftButtonDown
Rec : PreviewMouseLeftButtonDown
Rec : MouseLeftButtonDown
Grd : MouseLeftButtonDown
Win : MouseLeftButtonDown
Win : PreviewStylusUp
Grd : PreviewStylusUp
Rec : PreviewStylusUp
Rec : StylusUp
Grd : StylusUp
Win : StylusUp
Win : PreviewMouseLeftButtonUp
Grd : PreviewMouseLeftButtonUp
Rec : PreviewMouseLeftButtonUp
Rec : MouseLeftButtonUp
Grd : MouseLeftButtonUp
Win : MouseLeftButtonUp

從這例子中可以看出Stylus的事件會全部發生完才發生Mouse事件
而Stylus事件上下層間的傳遞也如同Mouse事件一般

2010年5月14日 星期五

在WPF中宣告非公開類別(internal class)

在製作動態連結函式庫(DLL)的時候
有時候會用到一些非公開的類別(在內部使用,卻無法被外部的專案參考)
在C#中僅需在宣告的時候將類別修飾詞改為internal即可
(或是不要用修飾詞,預設即為internal)

internal class Class1
{
}

但如果使用WPF的時候在DLL裡面使用了
UserControl或是像Window等使用XAML宣告的類別時
預設的partial class會是public的
(此時即使沒有使用修飾詞也會預設為public)

如果將cs檔的partial class宣告為internal partial class
編譯時會出現錯誤
原因是XAML會在編譯時自動使用修飾詞public
所以兩個部分類別的修飾不同而造成錯誤
此時就要用x:ClassModifier去設定編譯時使用的修飾詞

<UserControl
x:Class="test_wpf.UserControl1"
x:ClassModifier="internal"
...>
...
</UserControl>

此時cs檔對應的類別就要改成

namespace test_wpf
{
internal partial class UserControl1 : UserControl
{
}
}

或是將修飾詞去掉,就會使用XAML設定的修飾辭為預設

--
參考資料
WPF's Use of Partial Classes' Access Modifiers
x:ClassModifier 屬性

2010年4月2日 星期五

在WPF中捕捉螢幕畫面(ScreenShot)

WPF中沒有直接可以捕捉螢幕畫面的物件或函式
不過可以使用.net Form的方法做到,先加入參考System.Drawing

Bitmap CaptureScreen()
{
/// new a bitmap with screen width and height
Bitmap b = new Bitmap(
(int)SystemParameters.PrimaryScreenWidth,
(int)SystemParameters.PrimaryScreenHeight);

/// copy screen through .net form api
using (Graphics g = Graphics.FromImage(b))
{
g.CopyFromScreen(0, 0, 0, 0,
b.Size, CopyPixelOperation.SourceCopy);
}

return b;
}

這時候得到的是Form使用的Bitmap物件
如果要存成圖檔的話就可以直接使用Bitmap物件的Save

void SaveBitmap(Bitmap b, string f)
{
b.Save(f);
}

或是再把它轉換成WPF使用的BitmapSource

[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeleteObject(IntPtr hObject);

private static BitmapSource ToBitmapSource(Bitmap b)
{
/// transform to hbitmap image
IntPtr hb = b.GetHbitmap();

/// convert hbitmap to bitmap source
BitmapSource bs =
Imaging.CreateBitmapSourceFromHBitmap(
hb, IntPtr.Zero, Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());

/// release temp hbitmap image
DeleteObject(hb);

return bs;
}

不過由於透明背景的視窗會被放在不同層的顯示記憶體中
這也造成了使用上面的捕捉畫面方法會抓不到透明的視窗
如果希望抓到的畫面包括這樣的視窗,那就要直接用Win32的GDI+函式了

const uint SRCCOPY = 0x00CC0020;
const uint CAPTUREBLT = 0x40000000;

[DllImport("user32.dll")]
static extern IntPtr GetDesktopWindow();

[DllImport("user32.dll")]
static extern IntPtr GetDC(IntPtr hWnd);

[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool BitBlt(IntPtr hdcDest, int nXDest,
int nYDest, int nWidth, int nHeight, IntPtr hdcSrc,
int nXSrc, int nYSrc, uint dwRop);

[DllImport("user32.dll")]
static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);

void CaptureScreen()
{
/// new a bitmap with screen width and height
Bitmap b = new Bitmap(
(int)SystemParameters.PrimaryScreenWidth,
(int)SystemParameters.PrimaryScreenHeight);

/// copy screen through gdi+ api
using (Graphics g = Graphics.FromImage(b))
{
IntPtr hdc = g.GetHdc();
IntPtr dh = GetDesktopWindow();
IntPtr dhdc = GetDC(dh);
BitBlt(hdc, 0, 0,
(int)SystemParameters.PrimaryScreenWidth,
(int)SystemParameters.PrimaryScreenHeight,
dhdc, 0, 0, SRCCOPY | CAPTUREBLT);

/// release and dispose unmanaged resources
ReleaseDC(dh, dhdc);
g.ReleaseHdc();
hdc = IntPtr.Zero;
dh = IntPtr.Zero;
}
}

這樣就可以抓到透明背景的視窗
但是如果仔細看的話會發現在抓圖的時候,滑鼠游標會有一個閃爍
(需要連續一直抓比較明顯,直接抓是看不太出來的)
這是因為透明圖層跟滑鼠游標是在同一層
但是BitBlt的原則是不要抓到滑鼠游標
所以會再抓取透明圖層的時候會先將滑鼠變不見

--
參考資料
ScreenShot Application in WPF using GD+
BitBlt (gdi32)
The Case of the Disappearing Cursor

2010年4月1日 星期四

在WPF中的形狀(Shape)增加虛線框

在Shape中要加上框線很簡單
只要設定框線的顏色和粗細就好

<Rectangle Stroke="Black" StrokeThickness="2" />

再設定框線變成虛線

<Rectangle Stroke="Black" StrokeThickness="2"
StrokeDashArray="3 5" />

這裡需要兩個數字,一個是虛線中的線段長度
另一個是虛線中的空間長度,單位都是框線的粗度
所以上面例子是粗度為2像素,而每6像素就會空白10像素的長方形外框

--
參考資料
Shape.StrokeDashArray 屬性

2010年3月22日 星期一

在WPF中使用部分影像筆刷

在WPF中使用影像筆刷(ImageBrush)可以很容易的將文字或是圖形使用想要用的圖片填滿,可以參考在WPF中使用影像筆刷(ImageBrush)

不過有時候我們不想要用整張圖填滿,這時候就需要用ViewBox了
直接設定一個Rect去限定圖片中的某個範圍
然後再依照Stretch的屬性去填滿

private void SetViewBox(ImageBrush ib, Rect r)
{
ib.ViewboxUnits = BrushMappingMode.Absolute;
ib.Viewbox = r;
}

這裡採用的是絕對座標
也可以使用相對的方法(BrushMappingMode.RelativeToBoundingBox)
那傳入的Rect就要變成相對於整張圖的座標
例如(0,0,0.5,0.5)就是左上角的四分之一張圖

--
參考資料
TileBrush.Viewbox 屬性

2010年3月15日 星期一

在WPF中取得像素顏色(Pick Color)

在寫調色盤介面的時候,需要在一張圖透過滑鼠去選取顏色

一個很簡單的做法就是使用一個Image控制項,加上MouseDown事件
另外加一個色塊去表示取出的顏色

<StackPanel>
<!-- an image to pick color from -->
<Image Name="img" Source="color.png"
MouseDown="Image_MouseDown" />
<!-- an rectangle to output picked color -->
<Rectangle Name="rec" Height="20" />
</StackPanel>

然後將MouseDown的事件處理定義如下

private void Image_MouseDown(
object sender, MouseButtonEventArgs e)
{
/// get a bitmap reference
BitmapSource bmp = img.Source as BitmapSource;

/// get mouse down position
Point pos = e.GetPosition(img);

/// map position to pixel base
int x = (int)((pos.X / img.ActualWidth) *
bmp.PixelWidth);
int y = (int)((pos.Y / img.ActualHeight) *
bmp.PixelHeight);

/// pick the color to a byte array
CroppedBitmap cb = new CroppedBitmap(
bmp, new Int32Rect(x, y, 1, 1));
byte[] pix = new byte[4];
cb.CopyPixels(pix, 4, 0);

/// show the picked color
rec.Fill = new SolidColorBrush(
Color.FromRgb(pix[2], pix[1], pix[0]));
}

這樣在程式中點那張圖的時候
下方的長方形圖快就會被填滿為點下選取到的顏色

注意這邊我們對應到圖片的位置的時候
要轉換成PixelWidth & PixelHeight
因為圖片本身的dpi可能不同
直接使用圖片的大小可能會對應錯誤或超出範圍

--
參考資料
WPF : A Simple Color Picker With Preview
BitmapSource.CopyPixels 方法 (Array, Int32, Int32)

2010年2月23日 星期二

在WPF中取得DPI的設定

要取得DPI的設定,需要一個針對一個視窗
在WPF中可以透過主視窗來取得形變的比率
再乘上預設的DPI(96)即可

void GetDpiSetting(
out double DpiX, out double DpiY)
{
const double DEFAULT_DPI = 96.0;

/// get transform matrix from current main window
Matrix m = PresentationSource
.FromVisual(Application.Current.MainWindow)
.CompositionTarget.TransformToDevice;

/// scale default dpi
DpiX = m.M11 * DEFAULT_DPI;
DpiY = m.M22 * DEFAULT_DPI;
}

另外習慣使用Win32的程式設計師
一樣可以使用GetDeviceCaps取得DPI的設定

const int LOGPIXELSX = 88;
const int LOGPIXELSY = 90;

[DllImport("gdi32.dll")]
static extern int GetDeviceCaps(IntPtr hdc, int Index);

[DllImport("user32.dll")]
static extern IntPtr GetDC(IntPtr Hwnd);

void GetDpiSetting(
out double DpiX, out double DpiY)
{
/// get desktop dc
IntPtr h = GetDC(IntPtr.Zero);

/// get dpi from dc
DpiX = GetDeviceCaps(h, LOGPIXELSX);
DpiY = GetDeviceCaps(h, LOGPIXELSY);
}

--
參考資料
Getting system DPI in WPF app
GetDeviceCaps Function

在WPF中取得螢幕的解析度

在WPF中有一個很簡單可以取得系統設定的類別
所以可以直接透過SystemParameters取得主螢幕的解析度

Size GetScreenResolution()
{
return new Size(
SystemParameters.PrimaryScreenWidth,
SystemParameters.PrimaryScreenHeight);
}

當然,如果是習慣使用Win32的程式設計師
也一樣可以用GetSystemMetrics來取得

const int SM_CXSCREEN = 0;
const int SM_CYSCREEN = 1;

[DllImport("user32.dll")]
static extern int GetSystemMetrics(int nIndex);

Size GetScreenResolution()
{
return new Size(
GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN));
}

此時要注意DPI的設定,在非預設的DPI(96)下
由於WPF的特性,這兩種方法取出的數值會不同
例如同樣在1920x1080的解析度下
DPI 設定SystemParametersGetSystemMetrics
100% (96)1920x10801920x1080
125% (120)1536x8641920x1080
150% (144)1280x7201920x1080

這樣彷彿使用GetSystemMetrics比較一致
但在WPF中,視窗相對位置會使用SystemParameters的解析度來算
也就是一個最大化的視窗,在滑鼠事件中取得右下角座標時
會取得SystemParameters的大小
所以還是需要依照使用的需求而選用
有關DPI的設定可以參考在WPF中取得DPI的設定DPI Virtualization

--
參考資料
SystemParameters 類別
GetSystemMetrics Function

2010年2月22日 星期一

在WPF中自訂Cursor

如果要使用WPF內建的游標符號(Cursor)可以直接透過

void SetCursor()
{
Cursor = Cursors.ArrowCD;
}

內建的游標符號種類可以參考Cursors 類別
但是有時候我們會希望要把游標設成一張圖或是一個UI元件的樣子
這時候就要透過Win32的幾個函式了

struct IconInfo
{
public bool fIcon;
public int xHotspot;
public int yHotspot;
public IntPtr hbmMask;
public IntPtr hbmColor;
}

[DllImport("user32.dll")]
static extern IntPtr CreateIconIndirect(
ref IconInfo piconinfo);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetIconInfo(
IntPtr hIcon,
ref IconInfo piconinfo);

然後將一張圖檔(System.Drawing.Bitmap)轉成WPF的Cursor

Cursor CreatCursor(Bitmap b, int x, int y)
{
/// get icon from input bitmap
IconInfo ico = new IconInfo();
GetIconInfo(b.GetHicon(), ref ico);

/// set the hotspot
ico.xHotspot = x;
ico.yHotspot = y;
ico.fIcon = false;

/// create a cursor from iconinfo
IntPtr cursor = CreateIconIndirect(ref ico);
return CursorInteropHelper.Create(
new SafeFileHandle(cursor, true));
}

這裡hotspot是指真正的游標在這張圖的哪個位置
如果是(0,0),整張圖會在游標的右下方
如果是圖的寬高的一半的話,那就會像是抓到整張圖的中心在動

另外,有時候會希望要把游標設成一個UIElement的樣子
那就先做一個轉換

Cursor CreatCursor(UIElement u, Point p)
{
Cursor c;

/// move to the orignal point of parent
u.Measure(new Size(double.PositiveInfinity,
double.PositiveInfinity));
u.Arrange(new Rect(0, 0,
u.DesiredSize.Width,
u.DesiredSize.Height));

/// render the source to a bitmap image
RenderTargetBitmap r =
new RenderTargetBitmap(
(int)u.DesiredSize.Width,
(int)u.DesiredSize.Height,
96, 96, PixelFormats.Pbgra32);
r.Render(u);

/// reset back to the orignal position
u.Measure(new Size(0, 0));

using (MemoryStream m = new MemoryStream())
{
/// use an encoder to transform to Bitmap
PngBitmapEncoder e = new PngBitmapEncoder();
e.Frames.Add(BitmapFrame.Create(r));
e.Save(m);
System.Drawing.Bitmap b =
new System.Drawing.Bitmap(m);

/// create cursor from Bitmap
c = CreatCursor(b,
(int)p.X, (int)p.Y);
}

return c;
}

有關measure及arrange可以參考在WPF中的畫面重排(Measure & Arrange)在WPF中存圖檔的問題(RenderTargetBitmap)

--
參考資料
FrameworkElement.Cursor 屬性
WPF Tutorial - How To Use Custom Cursors
ICONINFO Structure
CreateIconIndirect Function
GetIconInfo Function
SafeFileHandle 類別

2009年11月6日 星期五

在WPF中自訂形狀(Shape)

在WPF的UI設計中,有時候會需要特殊形狀的控制項
可以使用ImageBrush以及透明背景的一張圖
將控制項(如Border)繪製成不規則形狀
(參考在WPF中使用影像筆刷(ImageBrush))
但是這種做法有個缺點,就是實際上的控制項並不是不規則的
所以如果要做成鑲嵌在一起的一些控制項就會因為互相覆蓋而造成問題

第二種就是用現有的基本形狀(Shape),如方形、橢圓等組合成新的控制項
可以利用Union, Xor, Intersect, Exclude兩兩組合
從而做出許多變化,例如

<Path Fill="Black">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Xor">
<CombinedGeometry.Geometry1>
<EllipseGeometry Center="50,50"
RadiusX="30"
RadiusY="30" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center="100,50"
RadiusX="30"
RadiusY="30" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>

上面就是兩個圓直接做Xor,顯示出來的圖會是

這樣的做法雖然已經可以做出一些很特別的控制項了
但是畢竟彈性還是有限,所以同樣使用Path,還可以用更彈性的方法來做
將上述例子中的CombinedGeometry中換成PathGeometry

<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure IsClosed="True"
StartPoint="10,10">
<PathFigure.Segments>
<PathSegmentCollection>
<ArcSegment Point="50,50"
Size="50,50"
SweepDirection="Clockwise"/>
<LineSegment Point="50,10" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>

同樣的效果也可以用C#寫

private Path AddShape()
{
/// set path with stroke
Path p = new Path();
p.Stroke = Brushes.Black;
p.StrokeThickness = 1;

/// set path figure with start and IsClosed
PathFigure pf = new PathFigure();
pf.IsClosed = true;
pf.StartPoint = new Point(10,10);

/// add two segments
pf.Segments.Add(new ArcSegment(
new Point(50,50), new Size(50,50), 0,
false, SweepDirection.Clockwise, true));
pf.Segments.Add(new LineSegment(
new Point(50,10), true));

/// set the data as the new path figure
p.Data = new PathGeometry();
(p.Data as PathGeometry).Figures.Add(pf);

return p;
}

在這邊的路徑片段(Segment)都代表從現在的點到一個點
然後構成一整條路徑,再將其填滿,可分成
種類說明
LineSegment直線段,只需設定目的點(Point),即會從目前所在連成一條直線段
ArcSegment弧線段,除目的點(Point)外,還需要設定弧形大小(Size,橢圓的長短軸)、軸旋轉角度(RotationAngle,預設為0)、線段轉彎方向(SweepDirection),以及是否轉彎超過180度(IsLargeArc)
PolyLineSegment多線段,設定一組連續的點(Points),即會從目前所在依照順序連接起來
BezierSegment三次貝茲曲線,設定目的點(Point3)以及兩個控制點(Point1, Point2)
PolyBezierSegment多段三次貝茲曲線,設定一組連續點(Points),其中每三個為一小組,代表經過兩控制點到目的點的一段三次貝茲曲線
QuadraticBezierSegment二次貝茲曲線,設定目的點(Point2)以及一個控制點(Point1)
PolyQuadraticBezierSegment多段二次貝茲曲線,設定一組連續點(Points),其中每兩個為一小組,代表經過控制點到目的點的一段二次貝茲曲線

--
參考資料
Path 類別
HOW TO:建立複合圖案
HOW TO:使用 PathGeometry 建立圖案
PathSegment 類別

在WPF中使用漸層筆刷(GradientBrush)

在WPF中要達到漸層效果非常簡單
只要在想要展示漸層效果的地方使用漸層筆刷上色就可以了
筆刷的分類可見在WPF中的筆刷(Brush)

而漸層筆刷分為兩個實體類別
第一類是線性漸層(LinearGradientBrush)
也就是遵照一個線性的方向去漸層

<Rectangle>
<Rectangle.Fill>
<!--gradually change according a vector-->
<LinearGradientBrush StartPoint="0,0"
EndPoint="1,0"
>
<GradientStop Offset="0" Color="Black" />
<GradientStop Offset="1" Color="White" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>

結果就像

StartPoint和EndPoint都是以該筆刷作用範圍的左上為(0,0),右下為(1,1)
除非想要做接合效果(好幾個物件像是連在一起),否則不會超出1
另外GradientStop.Offset是0-1之間的一個小數
相對於StartPoint為0,EndPoint為1的向量相對位置
上面的例子裡只有設起始點的顏色,所以就是單純的漸進
那如果增加一個

<GradientStop Offset="0.5" Color="Red" />

那就會變成

另外一類則是放射漸層(RadialGradientBrush)
也就是從一個點放射向外的漸層筆刷

<Rectangle>
<Rectangle.Fill>
<RadialGradientBrush GradientOrigin="0.5,0.5"
Center="0.5,0.5"
RadiusX="0.5"
RadiusY="0.5"
>
<GradientStop Color="Black" Offset="0" />
<GradientStop Color="White" Offset="1" />
</RadialGradientBrush>
</Rectangle.Fill>
</Rectangle>

這裡面的座標跟線性漸層筆刷是一樣的
Center, RadiusX和RadiusY構成一個圓
然後從GradientOrigin為放射中心做出漸層

這邊的圓形會因為Brush作用的範圍而變形
另外,如果放射中心在圓的外面,那就會拉出一個椎形
比如將上面例子中的GradientOrigin改為"0,0"
那結果就會變成

--
參考資料
LinearGradientBrush 類別
RadialGradientBrush 類別

在WPF中使用影像筆刷(ImageBrush)

在WPF中可以透過筆刷的設定,將圖片直接畫在各種使用筆刷的物件上面
比如說在文字方塊設定背景

<TextBlock FontSize="50">
<TextBlock.Background>
<ImageBrush ImageSource="img.jpg" />
</TextBlock.Background>
</TextBlock>

那顯示出來的會是

這時候可以換成設定前景,也就是文字色彩

<TextBlock FontSize="50">
<TextBlock.Foreground>
<ImageBrush ImageSource="img.jpg" />
</TextBlock.Foreground>
</TextBlock>

那結果就會變成

透過這樣的動作就可以做出很多變化的介面
不過這邊要注意一個問題
就是使用影像筆刷的圖可以有透明的背景
畫出來的時候當然那些區域會是透明的,不過大小並不會改變
比如說當你將很多張透明背景的圖疊在一起的時候
下面的圖基本上是沒辦法接收到滑鼠事件的

--
參考資料
ImageBrush 類別

在WPF中的筆刷(Brush)

在WPF中的筆刷有很多種,常用的如下
筆刷說明
SolidColorBrush單色筆刷,可以用直接設定ARGB或是內建的Brushes
GradientBrush漸層筆刷,分為線性漸層(LinearGradientBrush)以及放射漸層(RadialGradientBrush)
ImageBrush影像筆刷,可以將一張圖片塗上某個物件的背景或前景
參考在WPF中使用影像筆刷(ImageBrush)
DrawingBrush可以包含圖案、影像、文字及媒體等繪圖物件
VisualBrush直接用視覺物件來繪製

筆刷可以用的地方其實很多,大部份的控制項都有背景(Background)
有些會有前景(Foreground, 如TextBlock)
還有些則是填滿(Fill, 如Retangle)
透過筆刷的交互作用,可以讓WPF的程式變得非常多變而彈性

--
參考資料
WPF 筆刷概觀

2009年10月14日 星期三

在C#中使用Mutex

在多執行緒(Multi-Thread)程式中
執行緒之間常常會有相依性,所以需要一些指標
讓搶到指標的人才能存取某個資源或是執行動作
這時候就會需要用到Mutex

Mutex的使用其實非常簡單

/// new a mutex with name
Mutex m = new Mutex(false, "m");
m.WaitOne();

如果要釋放這個資源讓別人使用的時候

m.ReleaseMutex();

不過這邊有很多需要注意的地方
首先,Mutex分為有名字及無名兩種
在自己程式中可以使用無名的Mutex
但是有命名的Mutex會在程式間也有效
所以可以做出讓程式無法重覆啟動的檢查
例如在wpf程式中增加應用程式的建構子(constructor)

/// declare mutex in class to last it until app end
Mutex m = new Mutex(false, "m");
App()
{
/// if the mutex exists, return false in 1 ms
if (!m.WaitOne(1))
{
MessageBox.Show("program is current running!");
Shutdown();
}
}

這時候這支程式就沒辦法重覆啟動了
但是這時候要注意到第二個重點了
因為C#會自動作回收(garbage collection)的動作
如果Mutex是宣告在建構子裡面,那在建構成功後就會被回收了
所以即使沒有釋放(ReleaseMutex)依舊會自動失效
也就是沒辦法阻止重覆啟動

第三個重點就是WaitOne的等待時間了
如果裡面沒有任何的參數,那就是直接等待到有空為止
這有可能會讓程式直接停住
所以最簡單的做法就是增加一個很短的等待時間
並在回傳失敗的地方先做一些處理(比如等待的警告之類的)
之後就放心大膽的等下去~~

另外,如果是有命名的Mutex
那會跟Win32的CreateMutex有相同的效果
也就是兩者使用相同命名的時候也會互相等待

--
參考資料
Threading Tutorial Example 4: Using the Mutex object
Mutex 類別

2009年10月5日 星期一

在WPF中使用背景工作(BackgroundWorker)

在程式中有時候會需要執行一些比較花時間的工作
比如連上網路下載或上傳東西
但在和網路溝通及傳輸的時候,畫面上的工作就會暫停
這時候會想要提示使用者程式還在工作,比如說在msn登入的時候撥放動畫

這時候就要用到背景工作了,實際上這就是一種多執行緒處理
只是.net提供了方便的包裝,讓主執行緒在背景工作執行中得知進度
BackgroundWorker主要有三個事件處理
事件說明
DoWork在背景執行的工作,為另外一個執行緒,不可以變更介面畫面(介面變更在後面兩個事件中處理)
RunWorkerCompleted背景工作完成後的處理,因為背景工作是非同步處理,所以在完成後會觸發這事件來啟動主執行緒接下來的動作,如登入完成後執行切換畫面
ProgressChanged如果動作費時太長,有時候會希望將進度反應在介面上,在這裡改變介面畫面

我們來做一個範例(參考HOW TO:使用幕後背景工作)
視窗中只有一個按鈕(Button)

<Window ...>
<Button Name="b" Click="Button_Click" />
</Window>

在按鈕按下的時候開始設定並執行背景工作

private void Button_Click(
object sender, RoutedEventArgs e)
{
/// new and allow cancel and report progress
BackgroundWorker bw = new BackgroundWorker();
bw.WorkerSupportsCancellation = true;
bw.WorkerReportsProgress = true;

/// add the event handler for each progress
bw.DoWork += new DoWorkEventHandler(DoWork);
bw.ProgressChanged +=
new ProgressChangedEventHandler(DuringWork);
bw.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(AfterWork);

/// start the background work
bw.RunWorkerAsync();
}

事件處理定義如下

void DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker w = sender as BackgroundWorker;

for (int i = 1; i <= 10; i++)
{
/// check if the work is cancelled
if (w.CancellationPending == true)
{
e.Cancel = true;
break;
}

/// perform a time consuming operation
Thread.Sleep(500);

/// report progress in percentage
w.ReportProgress(i * 10);
}
}

void DuringWork(
object sender, ProgressChangedEventArgs e)
{
/// reflect the change of progress in UI
b.Content =
e.ProgressPercentage.ToString() + "%";
}

void AfterWork(
object sender, RunWorkerCompletedEventArgs e)
{
/// reflect the result after background work
if (e.Cancelled == true)
{
b.Content = "Canceled!";
}
else if (!(e.Error == null))
{
b.Content = ("Error: " + e.Error.Message);
}
else
{
b.Content = "Done!";
}
}

這個程式執行後,按下按鈕的時候
就會開始執行DoWork裡面的動作(每半秒更新一下進度)
並在每次更新進度的時候更新Button上面顯示的文字

另外,有時候背景程式需要使用到介面輸入的資訊
(比如登入畫面,需要將帳號密碼傳上網路)
但是DoWork裡面不能使用介面的物件
這時候可以透過argument(object形態,可以傳任意型別)傳入
所以啟動背景工作的呼叫改寫成

private void Button_Click(
object sender, RoutedEventArgs e)
{
/// new and allow cancel and report progress
BackgroundWorker bw = new BackgroundWorker();
bw.WorkerSupportsCancellation = true;
bw.WorkerReportsProgress = true;

/// add the event handler for each progress
bw.DoWork += new DoWorkEventHandler(DoWork);
bw.ProgressChanged +=
new ProgressChangedEventHandler(DuringWork);
bw.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(AfterWork);

/// start the background work with argument
bw.RunWorkerAsync(b.Content);
}

而在DoWork裡面則是用e.Argument將傳入的參數讀出
如果有超過一個以上的參數,也可以利用ArrayList先打包起來再一起傳

--
參考資料
HOW TO:使用幕後背景工作
BackgroundWorker 類別

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(事件)