2009年8月27日 星期四

如何在C#中使用Unmanaged dll

在C#中可以透過PInvoke(Platform Invocation Services), 呼叫 DLL 內實作的Unmanaged函式,
其流程可以參考 MSDN - PInvok的流程圖

在Managed的程式中使用Unmanaged dll的方法有兩種:
  • 呼叫DLL匯出(dllexport)的函式
  • 呼叫COM元件上的介面
因為, 使用COM Interop的方式有點複雜, 所以本文章只介紹第一種方法, 若要使用匯出的 DLL 函式(C-Style function), 可分成下面幾個步驟:
  • 引用DllImport所在的名子空間: using System.Runtime.InteropServices;
  • 定義類別(例如class PlatformInvoke)包裝要被呼叫的DLL介面(也就是C/C++中的header檔)
  • 使用 DllImport來引用DLL的函式, 並用 static 和 extern 來標記函式. 此目的在告訴編譯器該方法與實體物件無關.
DllImport的格式語法如下:
//將user32.dll換成要使用的DLL檔 
[DllImport("user32.dll"), SetLastError = true]

//換成所需的方法及參數(需要將Unmanaged類型轉成Managed)
public static extern ReturnType FunctionName(type arg);
如下面範例所示:
using System;
using System.Runtime.InteropServices;

class WrapperTest
{
    [DllImport("msvcrt.dll")]
    public static extern int puts(string c);

    [DllImport("msvcrt.dll")] 
    publlic static extern int _flushall();

    public static void Main() 
    {
        puts("Test");
        _flushall();
    }
}


下面表格列出DllImport的屬性欄位

名稱說明
BestFitMapping將 Unicode 字元轉換成 ANSI 字元時, 設定true/false可啟用或停用自動對應行為
CallingConventionCallingConvention 列舉成員, 此欄位的預設值為 WinAPI
CharSet指示用在入口處中的字串參數, 如果未指定 CharSet, 則預設為 CharSet.Auto
EntryPoint指示要呼叫的 DLL 進入點(Entry Point) 的名稱或序數
ExactSpelling控制 DllImportAttribute CharSet 欄位是否會導致 Common Language Runtime 搜尋 Unmanaged DLL 以取得不是指定名稱的進入點名稱.
PreserveSig指定簽章是否為 Unmanaged 程式碼進入點的直接轉譯, 預設此欄位為 true
SetLasrError指示自屬性方法傳回之前, 被呼叫端是否呼叫 SetLastError Win32 API 函式。
ThrowOnUnmappableChar在無法對應的 Unicode 字元轉換為 ANSI "?" 字元時, 啟用或停用例外狀況的擲回

  • 以 Managed 資料型別代替 Unmanaged 資料型別
Managed需要封裝Unmanaged的資料, 下面表格列出Unmanaged和Managed的資料型態轉換:
Wtypes.h 中的 Unmanaged 型別Unmanaged C 語言型別Managed 類別名稱
HANALEvoid*System.IntPtr
BYTEunsigned charSystem.Byte
SHORTshortSystem.Int16
WORDunsigned shortSystem.UInt16
INTintSystem.Int32
UINTunsigned intSystem.UInt32
LONGlongSystem.Int32
BOOLlongSystem.Int32
DWORDunsigned longSystem.UInt32
ULONGunsigned longSystem.UInt32
CHARcharSystem.Char
LPSTRchar*System.String 或
System.Text.StringBuilder
LPCSTRConst char*System.String 或
System.Text.StringBuilder
LPWSTRwchar_t*System.String 或
System.Text.StringBuilder
LPCWSTRConst wchar_t*System.String 或
System.Text.StringBuilder
FLOATFloatSystem.Single
DOUBLEDoubleSystem.Double


如果要包裝一個資料結構,
在 C# 中可以使用 StructLayout屬性來表示的結構, 如下面範例說明:

CC#

struct tOTM_Point
{
int X;

int Y;

int R;

};


[Serializable, StructLayout(LayoutKind.Sequential)]
public struct tOTM_Point
{

public int X;

public int Y;

public int R;

}

如果在Struct中有Array的型態, 可以利用MarshalAs 屬性來表示, 如下所示

CC#

struct tOTM_Touch
{
int TouchType;

int TouchState;

tOTM_Point PointArray[2];

};


[Serializable, StructLayout(LayoutKind.Sequential)]
public struct tOTM_Touch
{

public tOTM_TouchType TouchType;

public tOTM_TouchState TouchState;


[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
public tOTM_Point[] PointArray;

};



下面我以一個用C寫的OTMUT.dll為例子, 說明如何將Unmanaged dll轉成Managed程式碼:


C
#ifdef  OTMUT_EXPORTS
#define OTMUT_API __declspec(dllexport)
#else
#define OTMUT_API __declspec(dllimport)
#endif


struct tOTM_Point
{
int X;

int Y;

int R;
};

struct tOTM_Touch
{
int TouchType;

int TouchState;

tOTM_Point PointArray[2];
};

typedef void (_stdcall* TouchFunc) (tOTM_Touch Touch);

OTMUT_API void RegisterTouchFunc(TouchFunc Func);


C#
using System;
using System.Runtime.InteropServices;


namespace OTMUTInterop
{
[Serializable,
StructLayout(LayoutKind.Sequential)]
public struct tOTM_Point
{   
public int X;

public int Y;

public int R;

};

[Serializable,
StructLayout(LayoutKind.Sequential)]
public struct tOTM_Touch
{    
public tOTM_TouchType TouchType;

public tOTM_TouchState TouchState;

[MarshalAs(UnmanagedType.ByValArray,
SizeConst = 2)]
public tOTM_Point[] PointArray;

};

delegate void
TouchFunc(tOTM_Touch touch);

class OTMUT
{       
[DllImport("OTMUT.dll",
CallingConvention =
CallingConvention.StdCall)]
public static extern void
RegisterTouchFunc(TouchFunc func);

}
}



參考文章 :
MSDN -
使用 Unmanaged DLL 函式
MSDN - 使用平台叫用封送處理資料

2009年8月23日 星期日

C#型別轉換(Casting) - as運算子

當你在C#中作型別轉換時, 可以利用強制轉換的方法, 例如:

double dx = 7.3;
int i = (int) dx;


下面的範例中, 我們在一個控制項(ListBox)的選擇項目事件變更時, 將object(sender)轉型為 ListView.


void ONListBoxSelectinChange(object sender, SelectionChangedEventArgs args)
{
// 將object轉型為ListView
ListView lstView = (ListView)sender;
int TotalCount = lstView.Items.Count;
}
不過當轉型失敗時, 會造成程式當掉, 因此你必須使用try和catch來處理例外錯誤(系統會丟出InvalidCastException例外狀況)

void ONListBoxSelectinChange(object sender, SelectionChangedEventArgs args)
{
try
{
ListView lstView = (ListView)sender;
int TotalCount = lstView.Items.Count;
}
catch (InvalidCastException e)
{
MessageBox.Show(e.Message);
}
}


[圖 1] 使用try catch丟出的例外錯誤


而在C#中提供一個比較簡單的方法, 就是使用as運算子, 這樣作會更有效率也很安全, 如果轉型失敗的話, 它會返回null, 因此, 你可以很容易的來作一些判斷, 如下面的範例所示:



void ONListBoxSelectinChange(object sender, SelectionChangedEventArgs args)
{
ListView lstView = sender as ListView;

if (lstView == null)
{
MessageBox.Show("Could not convert object to ListView");
}
else
{
int TotalCount = lstView.Items.Count;
}
}

參考資料:
HOW TO:使用 as 和 is 運算子進行安全轉型 (C# 程式設計手冊)
Effective c# Item3:運算子is或as優於强制轉型

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.

下面的範例說明, 如何利用ItemSource去Binding一個物件:
首先, 我們先建立一個
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