2012年4月4日 星期三

在C#中使用RSA加解密

RSA是非對稱式加密,金鑰分為公鑰和私鑰兩種,詳情可參考RSA加密演算法。一般來說,公鑰會存成.crt,並且撒出去讓大家知道,而私鑰則是存成.pfx,由自己保管,同時加上密碼來保護

在使用這兩把鑰匙的時候,需先用X509Certificate2物件將其讀出

using System.Security.Cryptography.X509Certificates;

RSACryptoServiceProvider LoadPublicKeyFromFile(
string publicKeyFile)
{
///load public key from .crt
X509Certificate2 pubKey =
new X509Certificate2(publicKeyFile);

return (RSACryptoServiceProvider)pubKey.PublicKey.Key;
}

RSACryptoServiceProvider LoadPrivateKeyFromFile(
string privateKeyFile, string password)
{
///load private key from .pfx with password
X509Certificate2 priKey =
new X509Certificate2(privateKeyFile, password);

return (RSACryptoServiceProvider)priKey.PrivateKey;
}

有時會需要將鑰匙存入資料庫,可以採用XML的格式,RSACryptoServiceProvider有提供很方便的存讀XML字串的函式。

string ToXmlString(RSACryptoServiceProvider key)
{
/// true for private key, false for public key
return key.ToXmlString(true);
}

RSACryptoServiceProvider FromXmlString(string xmlString)
{
RSACryptoServiceProvider key =
new RSACryptoServiceProvider();
key.FromXmlString(xmlString);

return key;
}

注意,不論是公鑰或私鑰皆可以取得一個RSACryptoServiceProvider的物件。實際上在使用RSA加解密演算法時,有時會需要用到私鑰加密,公鑰解密(數位簽章,由自己發給別人,證實該資訊是由我發出),有時則是公鑰加密,私鑰解密(確認只有自己才可以看的到的文件),所以這兩把鑰匙的用法是相同的。

byte[] Encrypt(byte[] rawData, RSACryptoServiceProvider key)
{
/// encrypt without OAEP, but PKCS#1 v1.5 padding
return key.Encrypt(rawData, false);
}

byte[] Decrypt(byte[] encData, RSACryptoServiceProvider key)
{
/// decrypt without OAEP, but PKCS#1 v1.5 padding
return key.Decrypt(rawData, false);
}

在這邊使用哪種padding演算法都可以,但是記得加解密的時候需要用同樣的演算法即可。另外,因為加解密時都是採用byte陣列做為原始資料,所以如果原本是string物件的話,可以透過Convert.FromBase64String及Encoding.UTF8.GetString互為轉換。

--
參考資料
RSACryptoServiceProvider Class
Optimal asymmetric encryption padding

2012年2月28日 星期二

報告服務(Sql Server Reporting Service, SSRS)的高可用性(High Availability, HA)

微軟的資料庫伺服器(Sql Server)提供了報告服務(Reporting Service),讓客戶可以很容易的產生有關資料庫狀態的報告,像是登入的列表或是某個事件發生的頻率等等。只要有相對應的記錄,都可以用查詢的方式,定期或是依照需求產生報告。有關報告服務的架構可以參考剖析SQL Server 2005中的報告服務架構。

在提供這樣的服務的同時,系統的高可用性也必須涵蓋這部分。報告服務的高可用性可以分成三個方面來考慮,服務,報告服務資料庫,以及查詢資料庫。

服務的部分比較單純,為了提供高可用性,就多建立幾台報告服務伺服器即可。而查詢資料庫的部分則如同一般資料庫伺服器的高可用性,可以採用叢集(Cluster)或是鏡像(Mirroring)等方式。值得注意的是,由於報告服務的查詢字串有提供故障轉移(Failover Partner)的選項,所以如果採用鏡像的方式時,只需要將備份的伺服器網址填入該欄位,在故障發生時,報告服務就會自動查詢備份的資料庫伺服器,而不會造成服務的中斷。

最困難的部分在於報告服務資料庫的高可用性。由於在報告服務伺服器端僅能設定一台報告服務伺服器(至少在2008之前尚未提供,可以參考ReportServer (TempDB) Mirror capability),所以如果採用鏡像的方式,在主伺服器發生故障時,無法自動將設定更新到備份伺服器。

在這樣的狀況下,第一種選擇是直接將報告服務的資料庫切開,每組報告服務都有自己的資料庫,所以在某一台報告服務+資料庫的伺服器故障時,其他台仍然可以正常運作。但問題是這些資料庫之間沒有同步,所以必須對每個資料庫匯入相同的範本及資料源。同時,報告服務的歷史資料也會散落在各地。

第二種選擇是純粹的鏡像,將所有的報告服務設定相同的資料庫,如何設定可以參考How to: Configure a Report Server Scale-Out Deployment (Reporting Services Configuration)。此方法在主要資料庫伺服器故障時,需要手動將報告服務的伺服器設定到備份的伺服器。如果不希望手動,也可以透過代理服務(Agent Service),在故障發生時,用rsconfig設定報告服務的資料庫。此方法的細節可以參考Reporting Services Disaster Recovery。

第三種是微軟官方的建議,就是使用叢集的方式,設定可以參考安裝 SQL Server 2008 容錯轉移叢集心得筆記。不過在叢集的資料庫伺服器上,無法同時安裝報告服務,也就是說,需要額外的數台伺服器來提供高可用性的報告服務。細節可以參考Planning a Deployment Topology。

--
參考資料
SQL Server Reporting Services Disaster Recovery Case Study
How to: Configure SharePoint Integration on Multiple Servers

2012年1月31日 星期二

在虛擬機器(Vitual Machine)中擴充硬碟

使用虛擬機器最重要的好處之一,就是可以彈性的擴充硬體
硬體的擴充動作相較於實體機器來說,就少掉了更新硬體這一步驟
(當然要在開虛擬機器的實體機器的硬體限制下)
而Windows在更新CPU或記憶體都是自動完成的
所以在虛擬機器上擴充這兩者都僅僅需要關機,設定,開機三個步驟

如果是一般的硬碟擴充,只要先在虛擬機器的設定中延伸(Expand)硬碟

再到作業系統裡面使用內建的diskpart將同一個硬碟擴充即可

C:\Documents and Settings\Administrator>diskpart

Microsoft DiskPart version 5.2.3790.3959
Copyright (C) 1999-2001 Microsoft Corporation.
On computer: WIN2K3X86

DISKPART> list volume

Volume ### Ltr Label Fs Type Size Status Info
---------- --- ----------- ----- ---------- ------- --------- --------
Volume 0 C NTFS Partition 15 GB Healthy System
Volume 1 D CD-ROM 0 B Healthy
Volume 2 E New Volume NTFS Partition 20 GB Healthy

DISKPART> select volume 2

Volume 2 is the selected volume.

DISKPART> extend

DiskPart successfully extended the volume.

DISKPART> list volume

Volume ### Ltr Label Fs Type Size Status Info
---------- --- ----------- ----- ---------- ------- --------- --------
Volume 0 C NTFS Partition 15 GB Healthy System
Volume 1 D CD-ROM 0 B Healthy
* Volume 2 E New Volume NTFS Partition 21 GB Healthy

DISKPART>


另外,在Windows 2003中
因為保護作業系統的檔案,所以無法變更系統磁碟的大小
這時候就需要一點小技巧,在把系統碟延伸後
先將系統碟掛到另外一台機器當作一般磁碟並擴充
此時用原本的虛擬機器開機,就會發現系統磁碟大小已經更改成功了

--
參考資料
How to Extend Windows Boot Volumes in VMware
如何擴充 VMware 虛擬磁碟及作業系統容量

2011年1月7日 星期五

常見開源協議(Open Source License)的比較

開放原始碼這樣的想法非常崇高,藉由自由分享各自的程式碼
讓軟體的發展可以更加的快速而且安全
(透明度可以讓全世界一起除錯,而且沒辦法藏後門)

但是對於"自由",大家的想法都不太一樣
比如說,如果有人拿你寫的程式去賣錢呢?
這種顧慮會讓很多開發者不願意分享,所以開源協議就出現了
目的就是,讓作者可以在被保護的狀況下
依照期待(選擇的開源協議)的讓全世界的人一起分享

目前常見的開源協議大都經過Open Source Initiative批准
協議書的全文都可以在Open Source Licenses找到
(從協議的數量就可以看出,這方面也是相當的"自由")
最常見的有GPL, GNU General Public License, LGPL, The GNU Lesser General Public License, BSD License等
這些協議的一個共同點,就是再發佈的時候需要伴隨原有的協議

其中最嚴厲(崇尚自由,並要求所有人一起崇尚自由)的是GPL
強硬的要求(傳染)所有"引用/修改/衍生"GPL授權的程式碼的軟體
也必須採用GPL授權,並提供未來開發者同樣的自由,包括
1. 以任何目的執行此程式的自由
2. 再發行複製件的自由
3. 改進此程式,並公開發布改進的自由
基於這樣的規定,所以商業軟體或是不打算公開程式碼的軟體
都不得修改甚至引用GPL授權的程式碼
著名的GPL自由軟體包括Linux核心和GCC

LGPL則是將GPL條件稍微放寬,
如果僅是"引用(Link)"LGPL授權的函式庫
則不需要公開該軟體的程式碼
所以對於商業軟體來說,是可以引用LGPL的函式庫的
但是,對於"修改/衍生"LGPL授權軟體的部分,則同樣必須採用LGPL的授權
也就是一樣需要公開,且提供未來開發者同等的自由
著名的LGPL自由軟體有Mozilla跟OpenOffice.Org

BSD相對來說就寬鬆的多了
主要差異是在"修改/衍生"BSD授權的軟體時
除了版權聲明中仍需包含原來代碼中的BSD協議以外
開發者有權選擇是否要開放原始碼,或是做為商業用途

由此可以看出,如果是公司企業打算要使用開放原始碼的授權軟體的時候
BSD授權軟體可以直接使用甚至修改衍生
LGPL授權軟體則是僅能引用(Link)
GPL授權軟體則是儘量不使用,或是規避GPL的授權範圍
例如避免GPL風險的另類方法所提的方法

--
參考資料
五種開源授權規範的比較 (BSD, Apache, GPL, LGPL, MIT)
GPL 常見問題
自由軟體專案授權方式的轉換(上):不得撤銷原授權條款
自由軟體專案授權方式的轉換(下):新版本號另以更改後的授權方式釋出
在原始碼之外:編譯與安裝資訊的提供
散佈 GPL 衍生程式所須提供的原始碼範圍

2010年12月28日 星期二

在C#中讀取Excel的試算表

在取出試算表的名稱後,就可以針對單一試算表進行讀取

public List LoadExcelSheet(
string excelFile, string sheetName)
{
List dataList = new List();

/// load excel file as an ole database
OleDbConnection excelDatabase =
new OleDbConnection(string.Format(
"Provider = Microsoft.Jet.OLEDB.4.0;" +
"Data Source = {0};" +
"Extended Properties='Excel 8.0;HDR=No'",
excelFile));
excelDatabase.Open();

/// query sheet from excel file
OleDbDataAdapter sheetAdapter =
new OleDbDataAdapter(string.Format(
"SELECT * FROM [{0}]", sheetName),
excelDatabase);
DataTable fileData = new DataTable();
sheetAdapter.Fill(fileData);

/// scan all cells in the sheet
foreach (DataRow row in fileData.Rows)
{
foreach (object item in row.ItemArray)
{
if (!(item is DBNull))
{
dataList.Add(item.ToString());
}
}
}

/// close and release database connection
excelDatabase.Close();

return dataList;
}

這裡值得注意的是,如果試算表中有些欄位是空的
讀出來的資料會是DBNull的型別,其餘則會是string的型別

另外,如果是手動輸入試算表名稱的話
要記得在試算表名稱後面加一個$,例如預設的"Sheet1$"

--
參考資料
轉錄 C# 讀取 Excel
C# 讀取 Excel
DataTable 類別

在C#中讀取Excel的試算表名稱

在將Excel的檔案當做資料庫的存取的時候
可以先取得檔案中每個試算表的名稱,再做處理

public List LoadExcelSheetNameList(
string excelFile)
{
List sheetNameList = new List();

/// load excel file as an ole database
OleDbConnection excelDatabase =
new OleDbConnection(string.Format(
"Provider = Microsoft.Jet.OLEDB.4.0;" +
"Data Source = {0};" +
"Extended Properties='Excel 8.0;HDR=No'",
excelFile));
excelDatabase.Open();

/// load excel sheet as DataRow
DataRow[] sheetList =
excelDatabase.GetSchema("Tables").Select();
foreach (DataRow sheet in sheetList)
{
/// query each sheet name from excel file
sheetNameList.Add(
sheet["TABLE_NAME"] as string);
}

/// close and release database connection
excelDatabase.Close();

return sheetNameList;
}

在得到名稱後,就可以針對單一的試算表進行讀取

--
參考資料
[工作] 取得Excel頁籤名稱
如何取得 Excel 中的 Sheet Name

2010年12月27日 星期一

在C#中使用SQLite資料庫

自己開發程式的時候,想用資料庫又不希望花一大筆錢
常常會使用SQLite,一個輕量級的嵌入式資料庫系統(僅單一檔案)
由於.net原生並不支援SQLite
可以去System.Data.SQLite找到在公開的函式庫
注意如果是x64的程式,則需要使用x64版本的函式庫,無法混用

在每次使用資料庫的時候,都需要建立與相關資料庫的連結

private SQLiteConnection ConnectDatabase()
{
string path = "Test.db";
string password = "Password";
SQLiteConnection connection;

if (!File.Exists(_databasePath))
{
/// for non-exist database, create it
SQLiteConnection.CreateFile(path);
connection = new SQLiteConnection(
"Data Source = " + path);
connection.Open();
connection.ChangePassword(password);
}
else
{
/// for exist database, connect it
connection = new SQLiteConnection(
"Data Source = " + path);
connection.SetPassword(password);
connection.Open();
}

return connection;
}

這邊要注意的是,由於SQLite本身並沒有支援資料庫加密
有關Password的功能(紅字部分)是System.Data.SQLite實做的
所以製作出來的加密資料庫並不能被其他的SQLite應用讀取
(例如Firefox的附加元件SQLite Manager)
所以如果不打算加密,建議將紅字部分拿掉

在連結之後,就可以透過該連結,執行SQL的命令

private void RunCommand(
SQLiteConnection connection, string commandText)
{
SQLiteCommand command =
new SQLiteCommand(connection);
command.CommandText = commandText;
command.ExecuteNonQuery();
}

除了不需要回傳值的命令(例如CREATE TABLE)以外
SQL最重要的當然是Query的指令

private List RunQueryCommand(
SQLiteConnection connection, string commandText)
{
List<string> result = new List<string>();

SQLiteCommand command =
new SQLiteCommand(connection);
command.CommandText = commandText;

using (SQLiteDataReader queryResult =
command.ExecuteReader())
{
/// read one row one time
while (queryResult.Read())
{
/// change index to get different column
result.Add(queryResult.GetValue(0)
.ToString());
}
}

return result;
}

當然,在使用完畢後一定要記得將連結中止,否則資料庫檔案會被鎖住

private void DisconnectDatabase(
SQLiteConnection connection)
{
connection.Close();
}

另外,讀取資料庫的時候,如果不能肯定資料表是否存在
則需要先做一個檢查,否則會跳出例外狀況
(當然也是可以直接用try catch將Command包住,端看習慣)

private bool IsTableExist(
SQLiteConnection connection, string name)
{
return connection.GetSchema("Tables")
.Select("Table_Name = '" + name + "'")
.Length > 0;
}

--
參考資料
SQLite 簡介
System.Data.SQLite
C sharp or .Net 使用sqlite 設定
SQLite Manager
SQL語法教學
Check if table exists

Windows的顯示/隱藏桌面

在Windows XP預設的快速啟動列中會有一個捷徑,可以顯示/隱藏桌面
(隱藏/顯示所有已經開啟的視窗)
在XP的時候有時候會不小心砍掉(Win7中已經被放在視窗的右下角,反正也移除不掉,就不會有這個困擾),這時候可以自己重新做一個捷徑

先開一個純文字檔,內容如下

[Shell]
Command=2
IconFile=explorer.exe,3
[Taskbar]
Command=ToggleDesktop

並將檔案附檔名改為.scf(檔名倒是無所謂,一般習慣設成"ShowDesktop.scf"或"顯示桌面.scf")就可以了

如果想要在程式中做出同樣的功能,則可以呼叫Shell.Application去呼叫ToggleDesktop這個命令

void ShowDesktop()
{
Type shell =
Type.GetTypeFromProgID("Shell.Application");
shell.InvokeMember("ToggleDesktop",
BindingFlags.InvokeMethod, null,
Activator.CreateInstance(shell), null);
}

--
參考資料
How to re-create the Show desktop icon on the Quick Launch toolbar in Windows XP
C#使用系統的「顯示桌面」功能(Shell.Application)

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年9月3日 星期五

在C#中確認視窗是否是全螢幕

要確認一個視窗是否是全螢幕
可以直接比對視窗的大小是否與螢幕大小相同
因為如果不是全螢幕的視窗,那至少會有一部分被工具列佔走

public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}

[DllImport("user32.dll")]
private static extern int GetWindowRect(
IntPtr hWnd, out RECT lpRect);

bool IsFullScreen(IntPtr h)
{
/// get window bound from handle
RECT b;
GetWindowRect(h, out b);

/// get screen bound from handle
Rectangle r = Screen.FromHandle(h).Bounds;

/// check if the two size is the same
if ((b.bottom - b.top) == r.Height &&
(b.right - b.left) == r.Width)
{
return true;
}

return false;
}

注意Screen這個類別是宣告在System.Windows.Forms裡面
還有Rectangle是宣告在System.Drawing裡面
所以如果是WPF的程式,則需要另外新增參考

--
參考資料
GetWindowRect Function
RECT Structure
Screen.FromHandle 方法
Detect whether active window is full screen

在C#中取得前景視窗(ForegroundWindow)的應用程式名稱

如果在寫工具類的程式,有時會需要知道目前前景的視窗
(Foreground,也就是持有Focus的視窗)
這時候可以透過Win32的API來取得相關的資訊

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

[DllImport("user32.dll")]
public static extern int GetWindowThreadProcessId(
IntPtr hWnd, out int lpdwProcessId);

string GetForegroundApplicationName()
{
/// get foreground window handle
IntPtr h = GetForegroundWindow();

/// get foreground process from handle
int pId;
GetWindowThreadProcessId(h, out pId);
Process p = Process.GetProcessById(pId);

return p.ProcessName;
}

--
參考資料
GetForegroundWindow Function
GetWindowThreadProcessId Function
Process.GetProcessById 方法 (Int32)

2010年7月16日 星期五

查找註冊表的變動(WinDiff)

在寫Windows程式時,如果遇到比較冷門的功能
常常會遇到怎麼找都找不到API的狀況(甚至上msdn也沒人回應)
如果這時候你發現其實有手動的方式可以達到同樣的目的
(例如之前提過的DPI Virtualization)
那就可以考慮一下去比對註冊表的變動了

在Windows系統中幾乎所有的變動都會改變註冊表
在執行列打入regedit就可以看到一堆機碼
基本上真的無從找起

這時候就要用到WinDiff工具了
可以參考如何使用 Windiff.exe 公用程式
這個工具可以讓你比對兩個文字檔案,找到並瀏覽差異

1. 在做想要達到目的的操作前,先將註冊表匯出成文字檔
2. 做手動的操作
3. 重開註冊表,並匯出成另一個文字檔
4. 開啟WinDiff,從表單中選取File->CompareFiles...
並分別選取兩個文字檔
5. 如果兩個檔案相同,會顯示identical的訊息
6. 如果不同,會顯示紅字,雙擊紅字就會展開整份檔案
7. 首先會到第一筆差異處,利用下一筆(F8)或上一筆(F7)瀏覽

接下來就是用眼睛比對了
通常你會發現一些關鍵字出現,就是你要找的了
這裡有幾點需要注意的
1. 整份註冊表還蠻大的(100MB+是正常的)
可以考慮針對有關的註冊表查找,或是分成很多個表匯出
這樣可以節省比對的時間
2. 使用WinDiff比對完一對檔案後,最好是重開後再比對下一份
因為似乎還是會占用記憶體,所以會變慢很多
3. 一個動作可能會更動到註冊表的很多地方
就算已經找到,還是建議繼續搜索完其他的部分

在找到差異後,就可以透過編輯註冊表的API來做同樣的調整了
例如DPI Virtualization中的方法
不過這邊要強調,這種做法其實有一定的風險
(當然,如果只是檢查倒是無所謂,但是有修改就很難說了)
你有可能有漏掉沒有改到的地方(造成註冊表的關聯錯誤)
建議是先在測試的機器(可隨時重新安裝的電腦)上,手動做幾次
如果確定穩定而且達到目的的話再用程式去改

--
參考資料
如何使用 Windiff.exe 公用程式

2010年6月24日 星期四

在C#中啟動服務(Service)

在Windows系統裡面有非常多的服務
但是有些時後服務並不會自動開啟
例如快速插拔硬體後,有些相關的服務會沒有被啟動(例如手寫板)
所以這時候就需要另外啟動這些服務了

如果要手動啟動這些服務,可以到
控制台->系統管理工具->服務
找到相關的服務並按滑鼠右鍵啟動

但是有時候在程式進行中一樣會發現服務沒被啟動
就需要用ServiceController去操作服務的狀態了

private void StartService(string s)
{
try
{
ServiceController sc = new ServiceController(s);

/// if service is stopped, start it
if (sc.Status ==
ServiceControllerStatus.Stopped)
{
/// start the service
sc.Start();
sc.WaitForStatus(
ServiceControllerStatus.Running);
}
}
catch (Exception e)
{
Console.WriteLine(
"Exception : ", e.Message);
}
}

因為服務名稱可能傳入錯誤,或是在該裝置上沒有該服務
(例如一般XP上面沒有觸控筆的服務)
所以在使用的時候建議使用try-catch

傳入的參數就是服務的名稱,例如觸控筆的服務

StartService("TabletInputService");

--
參考資料
ServiceController 類別
StartService Function

2010年5月28日 星期五

在C#中啟用及關閉多點觸控

在.net 4還沒有完全成熟之前,要開發win7的多點觸控程式
可以透過Windows 7 Multitouch .NET Interop Sample Library
請參考建立多點觸控應用程式

但是有時候並不希望另外使用其他的函式庫
這時候可以透過簡單的Win32 API達到同樣的效果

[DllImport("User32.dll", SetLastError = true)]
public static extern bool SetProp(
IntPtr hWnd, string lpString, IntPtr hData);

[DllImport("User32.dll", SetLastError = true)]
public static extern IntPtr GetProp(
IntPtr hWnd, string lpString);

const int TABLET_ENABLE_MULTITOUCHDATA = 0x01000000;
const string TPS = "MicrosoftTabletPenServiceProperty";

bool EnableMultiStylus()
{
/// get current window handle
Window w = Application.Current.MainWindow;
IntPtr h = new WindowInteropHelper(w).Handle;

/// get current tablet property
IntPtr tp = GetProp(h, TPS);

/// set with enabled multi-touch flag
return SetProp(h, TPS,
new IntPtr(tp.ToInt32() |
TABLET_ENABLE_MULTITOUCHDATA));
}

在這之後就可以直接使用stylus的事件來接收多個觸控點的事件
不過這時候如果註冊了滑鼠事件,就會發現滑鼠事件也被影響了
第二點下去及動作同時也會觸發滑鼠事件,進而造成困擾
(因為滑鼠事件沒辦法判斷哪個事件是由哪個點引發的)
所以記得在不需要使用的時候把多點觸控事件給關掉

bool DisableMultiStylus()
{
/// get current window handle
Window w = Application.Current.MainWindow;
IntPtr h = new WindowInteropHelper(w).Handle;

/// get current tablet property
IntPtr tp = GetProp(h, TPS);

/// set with disabled multi-touch flag
return SetProp(h, TPS,
new IntPtr(tp.ToInt32() &
~TABLET_ENABLE_MULTITOUCHDATA));
}

--
參考資料
建立多點觸控應用程式
SetProp Function
GetProp Function
IRealTimeStylus3::MultiTouchEnabled Property

2010年5月27日 星期四

在C#中做字串分析

C#中的字串本身就有一些簡單的分析,例如Contains或EndsWith
但是如果是比較複雜的判斷,例如是否全部由字母構成
就需要使用正規表式(Regular Expression)來判斷了

bool IsAlpha(string s)
{
return !Regex.IsMatch(s, "[^a-zA-Z]");
}

用正規表式來確認字串規則時,會在字串中進行搜索
所以如果要判斷一個字串是否滿足"全部都由字母構成"
實際上得去判斷是否沒有"其餘字符存在"

另外,正規表式也可以做成執行個體

bool IsAlpha(string s)
{
Regex r = new Regex("[^a-zA-Z]");

return !r.IsMatch(s);
}

注意這裏的執行個體中的規則是不可變的
所以如果有大量的運算都需要用到同樣的規則再這樣使用
否則用靜態的方式會比較有彈性

--
參考資料
Regular Expressions Usage in C#
正規表式
Regex 類別
Regex.IsMatch 方法

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日 星期五

在C#中將數值轉字串

在C#中要將一個數值表示成字串可以直接使用ToString的方法
不過在不同的地方可能需要不同的表示法
這時候就要使用ToString(String format)
透過format來定義字串的格式
比較常用的如下
格式說明
0固定位數數值字串使用,若該位數不存在則填0
#非固定位數數值字串使用,若該位數不存在則不會出現
E或e科學記號(指數)表示法
X或x16進位表示法,僅用於整數數值


void ToStringSample()
{
double n1 = 12.12;
Console.WriteLine(n1.ToString("000.000"));
/// 012.120
Console.WriteLine(n1.ToString("###.###"));
/// 12.12
Console.WriteLine(n1.ToString("E"));
/// 1.212000E+001

double n2 = 1234.1234;
Console.WriteLine(n2.ToString("000.000"));
/// 1234.123
Console.WriteLine(n2.ToString("###.###"));
/// 1234.123
Console.WriteLine(n2.ToString("E"));
/// 1.234123E+003

double n3 = 0.05;
Console.WriteLine(n3.ToString("000.000"));
/// 000.050
Console.WriteLine(n3.ToString("###.###"));
/// .05
Console.WriteLine(n3.ToString("E"));
/// 5.000000E-002

int n4 = 123456;
Console.WriteLine(n4.ToString("000.000"));
/// 123456.000
Console.WriteLine(n4.ToString("E"));
/// 123456
Console.WriteLine(n4.ToString("###.###"));
/// 1.234560E+005
Console.WriteLine(n4.ToString("X"));
/// 1E240
}

數值的表示法真的很多,真的需要的話可以直接從MSDN裡面找

--
參考資料
Double.ToString 方法 (String)
標準數值格式字串
自訂數值格式字串
Double.ToString如何指定小數點後的精度

在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 屬性