2007年2月26日 星期一

C# 剪貼簿測試

MSN SpaceGoogle DocGoogle Blog

Chui-Wen Chiu(Arick)

2007.02.27 備註兩點錯誤修正

2007.02.26 建立

.NET 中的剪貼簿由 Clipboard, DataFormats 和 IDataObject 所構成。這些類別定義在 System.Windows.Forms 名稱空間。
如果要檢測剪貼簿目前的資料型態可以透過 IDataObject.GetDataPresent 來檢測,如下
IDataObject d = Clipboard.GetDataObject();
Type t = typeof(DataFormats);
FieldInfo[] fis = t.GetFields();
foreach (FieldInfo fi in fis) {
Debug.Write(String.Format("{0} => ", fi.Name));
Debug.WriteLine(d.GetDataPresent(fi.Name) ? "YES" : "NO");
}

執行結果:
Text => YES
UnicodeText => YES
Dib => NO
Bitmap => NO
EnhancedMetafile => NO
MetafilePict => NO
SymbolicLink => NO
Dif => NO
Tiff => NO
OemText => YES
Palette => NO
PenData => NO
Riff => NO
WaveAudio => NO
FileDrop => NO
Locale => YES
Html => NO
Rtf => NO
CommaSeparatedValue => NO
StringFormat => NO
Serializable => NO

.NET2.0 對 Clipboard 新增 ContainsData,可以不用透過 IDataObject 來檢測料型態,修改如下:

Type t = typeof(DataFormats);
FieldInfo[] fis = t.GetFields();
foreach (FieldInfo fi in fis) {
Debug.Write(String.Format("{0} => ", fi.Name));
Debug.WriteLine(Clipboard.ContainsData(fi.Name) ? "YES" : "NO");
}

取得剪貼簿內容
IDataObject d = Clipboard.GetDataObject();

if (d.GetDataPresent(DataFormats.Text)) {
MessageBox.Show((String)d.GetData(DataFormats.Text));
}

if (d.GetDataPresent(DataFormats.Bitmap)) {
Bitmap b = (Bitmap)d.GetData(DataFormats.Bitmap);
this.BackgroundImage = b;
}


或是直接取用 .NET 2.0 新增的 Clipboard.GetData,如下:
Object obj;
if ( (obj = Clipboard.GetData(DataFormats.Text)) != null ){
MessageBox.Show((String)obj);
}

if ((obj = Clipboard.GetData(DataFormats.Bitmap)) != null) {
Bitmap b = (Bitmap)obj;
this.BackgroundImage = b;
}

將資料放置在剪貼簿內容
* 文字
Clipboard.SetData(DataFormats.Text, "Chui-Wen Chiu TEST!!");

* Bitmap
Clipboard.SetData(DataFormats.Bitmap, new Bitmap(@"c:snapshot20070213131306.bmp"));

備註:
1. 雖然 [1] 的範例在 Console 模式下展示,可是我實際在 .NET 2.0 的 Console 下使用 Clipboard.GetDataObject(),回傳值永遠為 null。 使用 Clipboard.ContainData 也無法正常檢測出資料型別。

依據[4]如下描述

"Clipboard 類別只能用在設定為單一執行緒 Apartment (STA) 模式的執行緒中。若要使用這個類別,請確定您的 Main 方法是STAThreadAttribute 屬性做為標記。"

所以,如果要在 Console 下使用剪貼簿必須使用 STAThreadAttribute,如下

using System;
using System.Diagnostics;
using System.Windows.Forms;
using System.Reflection;

namespace ConsoleApplication1 {

class Program {
[STAThread]
static void Main(string[] args) {
IDataObject d = Clipboard.GetDataObject();
Type t = typeof(DataFormats);
FieldInfo[] fis = t.GetFields();
foreach (FieldInfo fi in fis) {
Debug.Write(String.Format("{0} => ", fi.Name));
Debug.WriteLine(d.GetDataPresent(fi.Name) ? "YES" : "NO");
}
}
}
}

2. 雖然 Clipboard.SetData 和 Clipboard.GetData 的資料格式都以 String 來識別,可是卻無法以自訂資料型態來擴充,如下:

Clipboard.SetData("MyData", new MyData("Chui-Wen Chiu", 1));
Object obj;
if ( (obj = Clipboard.GetData("MyData")) != null ){
MessageBox.Show(((MyData)obj).ToString());
}

public class MyData {
public String Name;
public int Sex;
public MyData(String name, int sex) {
Name = name;
Sex = sex;
}
public override string ToString() {
return String.Format("{0}={1}", Name, Sex);
}
}


2007.02.27 Update
依據 MSDN 描述[4],
"物件必須可以序列化,才能放在剪貼簿上。如果您將不可序列化的物件傳遞至 Clipboard 方法,則該方法會失敗而不擲回例外狀況。"

依據上述說明,將程式修正如下即可放置在剪貼簿中。

Clipboard.SetDataObject(new MyData("Chui-Wen Chiu", 1), false);
MyData m2=(MyData)Clipboard.GetData(typeof(MyData).ToString() );
MessageBox.Show(m2.ToString());

[Serializable]
public class MyData {
public String Name;
public int Sex;
public MyData(String name, int sex) {
Name = name;
Sex = sex;
}
public override string ToString() {
return String.Format("{0}={1}", Name, Sex);
}
}

另外,[3]描述剪貼簿可以放入任何物件,但為了避免傳遞給 SetDataObject 的物件不是字串、Metafile 或 Bitmap,必須在 SetDataObject 的第二個參數傳入 false,或是直接忽略第二個參數。這個限制是因為剪貼簿不能在應用程式之間傳遞任意物件,只有將自訂物件放入剪貼簿的應用程式可以存取他。


參考資料:
[1] Using the clipboard to transfer data to and from your applications
[2] Clipboard.GetDataObject 方法(System.Windows.Forms)

[3] Charles Petzold, "Microsoft Windows Programming With C#"
[4] Clipboard 類別(System.Windows.Forms)

2007年2月25日 星期日

使用 IMessageFilter 建立通用型過濾器來處理系統事件

MSN SpaceGoogle DocGoogle Blog

Chui-Wen Chiu(Arick)

2007.02.26 建立

.NET 的 IMessageFilter 介面允許系統訊息分派給應用程式的表單和控制項之前進行攔截處理。IMessageFilter 介面只有 PreFilterMessage 這個方法需要實作。如果你想要阻斷這個訊息繼續流竄,在 PreFilterMessage 方法中回傳 false。


實作自己的訊息過濾器 MyMessageFilter,攔截 WM_MOUSEMOVE 訊息,並且將該訊息以事件方式通知:

public class MyMessageFilter : IMessageFilter {
public event EventHandler<EventArgs> MyMouseMove;
public void OnMyMouseMove(object sender, EventArgs e) {
if (MyMouseMove != null) {
MyMouseMove(sender, e);
}
}


public bool PreFilterMessage(ref Message objMessage) {
if (objMessage.Msg == 0x0200) { // WM_MOUSEMOVE
OnMyMouseMove(this, new EventArgs());
return true;
}
return false;
}
}

測試程式
1. 將自訂的過濾器新增到 Application
MyMessageFilter mmf;
mmf = new MyMessageFilter();
mmf.MyMouseMove += new EventHandler<EventArgs>(mmf_MyMouseMove);
Application.AddMessageFilter(mmf);

void mmf_MyMouseMove(object sender, EventArgs e) {
this.Text = DateTime.Now.ToString();
}

2. 移除自訂的過濾器
Application.RemoveMessageFilter(mmf);

參考資料
[1] Using IMessageFilter to create a generic filter for operating system events

[2] IMessageFilter 成員(System.Windows.Forms)

2007年2月20日 星期二

.NET 透過 IKVM 使用 Java CLASS

MSN SpaceGoogle DocGoogle Blog

Chui-Wen Chiu(Arick)

2007.02.21 建立

IKVM.NET 是在 .NET 上實作 JVM 的 Open Source Project[2]。他可以讓你在 .NET 中使用 Java CLASS,也可以在 Java 中使用 NET。IKVM 主要包含三個部份:
1. .NET 上的 JVM 實作
2. .NET 實作版本的 Java Class Library
3. 賦予 Java 和 .NET 互通的工具

本文將透過 IKVM 進行簡單的互通測試,本文的測試環境如下:
1. Windows 2000
2. .NET Framewrok 2.0
3. IKVM.NET 0.32.0.0
4. Java 5.0 SE
5. Editplus

首先撰寫一個簡單的 Java 程式如下:

JavaToNet.java

public class JavaToNet
{
public static void main(String[] args)
{
System.out.println("This is a demonstration Program whichn");
System.out.println("shows the conversion of Java class ton");
System.out.println("a .NET dlln");
}

public String getAuthor(){
return "Chui-Wen Chiu(Arick)";
}

public static double AddNumbers(double a,double b){
double c = 0;
c = a + b;
return c;
}

// 產生 Exception
public static double hasException(){
return 1/0;
}
}

使用 Java SE5 的編譯器產生 CLASS
javac JavaToNet.java

然後使用 IKVM 產生 .NET Assembly
ikvmc -target:library JavaToNet.class

此時,我們已經擁有一個 .NET Assembly,接著撰寫一個簡單的測試程式如下:

dotNetTest.cs

using System;
using System.ComponentModel;
using System.Windows.Forms;
using TimeZone = java.util.TimeZone;
namespace WindowsApplication2
{
public class Program{
static void Main()
{
// 使用 java 內建類別
Console.WriteLine(TimeZone.getDefault().getDisplayName());

// 使用自訂類別靜態方法
Console.WriteLine(JavaToNet.AddNumbers(999, 888));

// 使用自訂類別物件方法
JavaToNet jt = new JavaToNet();
Console.WriteLine(jt.getAuthor());

// 引發 Exception
try{
JavaToNet.hasException();
}catch(System.DivideByZeroException ){
Console.WriteLine("Has Exception");
}
}
}
}

編譯程式

csc /r:JavaToNet.dll /r:IKVM.GNU.Classpath.dll dotNetTest.cs

執行結果


參考資料
[1] http://www.codeproject.com/useritems/csharpikvm.asp
[2] http://sourceforge.net/project/showfiles.php?group_id=69637

[C#]檔案總管新增一個 MD5 欄位

MSN SpaceGoogle DocGoogle Blog
Chui-Wen Chiu(Arick)
2007.02.21 建立

Windows 95 時代就可以透過 COM 方式來擴充檔案總管功能,Windows 2000 時,又加入欄位擴充功能,本文將透過 C# 來說明此一技術。本文的測試環境如下:
1. Windows 2000
2. .NET Framework 2.0
3. EditPlus

要讓檔案總管新增一個自訂的欄位,需要在 COM 實作 IColumnProvider 介面,並將你的程式註冊在 HKEY_CLASSES_ROOTFolderShellExColumnHandlers。IColumnProvider 介面定義在 ShlObj.h,看起來如下:

DECLARE_INTERFACE_(IColumnProvider, IUnknown)
{
// IUnknown methods
STDMETHOD (QueryInterface)(THIS_ REFIID riid, void **ppv) PURE;
STDMETHOD_(ULONG, AddRef)(THIS) PURE;
STDMETHOD_(ULONG, Release)(THIS) PURE;

// IColumnProvider methods
STDMETHOD (Initialize)(THIS_ LPCSHCOLUMNINIT psci) PURE;
STDMETHOD (GetColumnInfo)(THIS_ DWORD dwIndex, SHCOLUMNINFO *psci) PURE;
STDMETHOD (GetItemData)(THIS_ LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
VARIANT *pvarData) PURE;
};

這個介面還有用到 4 個資料結構,其中3 個定義在 ShlObj.h 如下:

typedef struct {
ULONG dwFlags; // initialization flags
ULONG dwReserved; // reserved for future use.
WCHAR wszFolder[MAX_PATH]; // fully qualified folder path (or empty
// if multiple folders)
} SHCOLUMNINIT, *LPSHCOLUMNINIT;

typedef const SHCOLUMNINIT* LPCSHCOLUMNINIT;

typedef struct {
SHCOLUMNID scid; // OUT the unique identifier
// of this column
VARTYPE vt; // OUT the native type of the
// data returned
DWORD fmt; // OUT this listview format
// (LVCFMT_LEFT, usually)
UINT cChars; // OUT the default width of
// the column, in characters
DWORD csFlags; // OUT SHCOLSTATE flags
WCHAR wszTitle[MAX_COLUMN_NAME_LEN]; // OUT the title of the column
WCHAR wszDescription[MAX_COLUMN_DESC_LEN]; // OUT full description of
// this column
} SHCOLUMNINFO, *LPSHCOLUMNINFO;
typedef const SHCOLUMNINFO* LPCSHCOLUMNINFO;

typedef struct {
ULONG dwFlags; // combination of SHCDF_ flags.
DWORD dwFileAttributes; // file attributes.
ULONG dwReserved; // reserved for future use.
WCHAR* pwszExt; // address of file name extension
WCHAR wszFile[MAX_PATH]; // Absolute path of file.
} SHCOLUMNDATA, *LPSHCOLUMNDATA;
typedef const SHCOLUMNDATA* LPCSHCOLUMNDATA;

另外一個定義在 ShObjIdl.idl

typedef struct {
GUID fmtid;
DWORD pid;
} SHCOLUMNID, *LPSHCOLUMNID;
typedef const SHCOLUMNID* LPCSHCOLUMNID;

接著我們要將上述提到的介面和結構轉換到 .NET 環境相對應的定義,由於 .NET COM Interop 會自動建立 IUnknown 介面的三個標準方法,所以我們只需要定義 IColumnProvider 介面

[ComVisible(false), ComImport, Guid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IColumnProvider {
[PreserveSig()] int Initialize(LPCSHCOLUMNINIT psci);
[PreserveSig()] int GetColumnInfo(int dwIndex, out SHCOLUMNINFO psci);

/// Note: these objects must be threadsafe! GetItemData _will_ be called
/// simultaneously from multiple threads.
[PreserveSig()]
int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
out object /*VARIANT */ pvarData);
}


注意上述的 PreserveSig 特性(Attribute),這個是告訴 COM 不要將回傳值視為 out 參數,而要看作 COM HRESULT 的回傳值。
另外,使用到的四個結構重新改寫成如下:

[ComVisible(false),
StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public class LPCSHCOLUMNINIT {
public uint dwFlags; //ulong
public uint dwReserved; //ulong
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
public string wszFolder; //[MAX_PATH]; wchar
}

[ComVisible(false), StructLayout(LayoutKind.Sequential)]
public struct SHCOLUMNID {
public Guid fmtid; //GUID
public uint pid; //DWORD
}

[ComVisible(false), StructLayout(LayoutKind.Sequential)]
public class LPCSHCOLUMNID {
public Guid fmtid; //GUID
public uint pid; //DWORD
}

[ComVisible(false), StructLayout(LayoutKind.Sequential,
CharSet=CharSet.Unicode, Pack=1)]
public struct SHCOLUMNINFO {
public SHCOLUMNID scid; //SHCOLUMNID
public ushort vt; //VARTYPE
public LVCFMT fmt; //DWORD
public uint cChars; //UINT
public SHCOLSTATE csFlags; //DWORD
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=80)] //MAX_COLUMN_NAME_LEN
public string wszTitle; //WCHAR
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] //MAX_COLUMN_DESC_LEN
public string wszDescription; //WCHAR
}

[ComVisible(false), StructLayout(LayoutKind.Sequential,
CharSet=CharSet.Unicode)]
public class LPCSHCOLUMNDATA{
public uint dwFlags; //ulong
public uint dwFileAttributes; //dword
public uint dwReserved; //ulong
[MarshalAs(UnmanagedType.LPWStr)]
public string pwszExt; //wchar
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
public string wszFile; //[MAX_PATH]; wchar
}


實作 IColumnProvider 介面, Initialize 只需要簡單的回傳 S_OK 即可。檔案總管要求每一個註冊的自訂欄位必須提供這個欄位的相關描述。IColumnProvider介面一次可以實作多個自訂欄位,所以檔案總管會使用索引值呼叫 GetColumnInfo,假如回傳 S_FALSE,檔案總管將終止下一個索引值的呼叫,並可以從這裡得到自訂欄位的數量。接著,我們針對索引值 0 建立一個欄位,首先建立 SHCOLUMNINFO 結構提供自訂欄位的相關資訊,檔案總管會透過此結構來取得他所需要的資訊。

public override int GetColumnInfo(int dwIndex, out SHCOLUMNINFO psci) {
psci=new SHCOLUMNINFO();

// 只提供一個自訂欄位,所以如果索引值不為 0 的皆忽略

if(dwIndex!=0)
return S_FALSE;

try {
psci.scid.fmtid=GetType().GUID;
psci.scid.pid=0;

// Cast to a ushort, because a VARTYPE is ushort and a VARENUM is int
psci.vt=(ushort)VarEnum.VT_BSTR;
psci.fmt=LVCFMT.LEFT;
psci.cChars=40;

psci.csFlags=SHCOLSTATE.TYPE_STR;

psci.wszTitle = "MD5 Hash";
psci.wszDescription = "Provides an MD5 Hash of every file";

} catch(Exception e) {
MessageBox.Show(e.Message);
return S_FALSE;
}

return S_OK;
}

當檔案總管瀏覽某一個資料夾時,會在取得每一個檔案或目錄時,呼叫 IColumnProvider 介面的 GetItemData,因此,可以在這個方法中計算檔案的 MD5。如下:

public override int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
out object pvarData) {
pvarData=string.Empty;

// 忽略目錄
if(((FileAttributes)pscd.dwFileAttributes|FileAttributes.Directory)==
FileAttributes.Directory)
return S_FALSE;

// 只處理我們自己定義的欄位
if(pscid.fmtid!=GetType().GUID || pscid.pid!=0)
return S_FALSE;

try {
// 計算檔案的 MD5
MD5 md5 = new MD5CryptoServiceProvider();
byte[] result;

using(Stream stream=File.OpenRead(pscd.wszFile)) {
result = md5.ComputeHash(stream);
}

StringBuilder output=new StringBuilder(2+(result.Length*2));

foreach(byte b in result) {
output.Append(b.ToString("x2"));
}

// 顯示的資料
pvarData="0x" + output.ToString();

} catch(UnauthorizedAccessException) {
return S_FALSE;
}catch(Exception e) {
MessageBox.Show(e.Message);
return S_FALSE;
}

return S_OK;
}


測試程式
1. 將 .NET 如同 COM 元件將相關資料註冊到 Registry,regasm MD5ColumnHandler.dll

2. 將元件註冊到 GAC ,gacutil -i MD5ColumnHandler.dll
3. 重新啟動檔案總管,從"工作管理員(taskmgr.exe)"刪除 explorer.exe 程序。

執行結果

補充

1. 本文開頭說到元件除了要實作 IColumnProvider 介面之外,還需要在 HKEY_CLASSES_ROOTFolderShellExColumnHandlers 進行註冊,註冊這部份已經封裝在 COM 元件註冊時處理掉了,詳細可以參考 ShellLib.cs 原始碼的 ColumnProvider.Register 和 ColumnProvider.UnRegister。

2. 如果要移除註冊的元件,可以執行 gacutil -u MD5ColumnHandler。

3. 如果想要針對自訂欄位進行排序參考 http://www.codeproject.com/shell/shellextguide8.asp#xx724804xx


程式碼下載
[1] http://www.codeproject.com/csharp/ColumnHandler/ColumnHandler_src.zip

參考資料
[1] http://www.codeproject.com/csharp/columnhandler.asp

2007年2月12日 星期一

DOTNET Script Host

MSN SpaceGoogle DocGoogle Blog
Chui-Wen Chiu(Arick)
2007.02.13 建立

系統環境
1. Windows XP Professional CHT
2. .NET Framework 1.0+1.1+2.0+3.0
3. DSH 1.1

簡介
這是一套模擬 WSH(Window Script Host) 的 Script Host,這套可以直接使用 .NET CLR 功能。他的功能特色有:

1. 可以透過命令列或 double click 啟動

2. 可傳遞參數給 Script

3. 將多個 Script 封裝在 XML 結構中

4. 在單一Script 文件中可以使用不同的語言

5. 目前支援的語言有 Visual Basic .NET, C# 和 JScript .NET

6. 可以使用元件 (.NET Framework 類別庫或其他 .NET 組件)

7. 可遠端執行 Script

8. 可選擇將資訊紀錄在文字檔或 Windows 事件紀錄

9. 可將 Script 編譯成可執行檔


安裝
1. 下載 http://www.IT-Visions.de/download/getfile.aspx?/dsh/dsh1.1.zip
2. 解壓縮後,Installdsh.exe 放置在 System32 目錄
3. 執行 Installdsh.reg 安裝相關登錄資訊,使得能夠透過 Double Click 方式執行 Script
ps. 如果你的System32 路徑在 c:windowssystem32,可以直接執行 Installinstall_WXP_W2003.bat 取代步驟 2 和 3

測試
helloworld_cs.dsh
<?xml version="1.0" encoding="ISO-8859-2"?>
<scriptdoc process="1">
<comment>
Hello World
Homepage: http://chuiwenchiu.spaces.live.com
Version: 1.0.0.0
Date: 2007.02.13
Author: Chui-Wen Chiu
Comments: 第一個 DSH C# Hello World 程式
</comment>

<references>
<assembly>System.dll</assembly>
<assembly>System.Drawing.dll</assembly>
<assembly>System.Windows.Forms.dll</assembly>
</references>

<log error="YES" success="YES" start="YES">
<eventlog1 name="Scripting"/>
<logfile1 name="c:dsh-log.txt"/>
</log>

<script name="HelloWorld" language="CS" startClass="HelloWorld.Program">
<![CDATA[
using System;
using System.Drawing;
using System.Windows.Forms;

namespace HelloWorld
{
class Program
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine("Hello World");
MessageBox.Show("Hello World");
}
}
}
]]>
</script>
</scriptdoc>
<scriptdoc>: 根節點
<comment>: 註解
<references>: 參考的組件清單
<log>: log 紀錄
<script>: 程式碼區段


參考資料
[1] DOTNET Scripting Host Version 1.1 - Dokumentation

2007年2月1日 星期四

使用 VS2005 C# 開發 ActiveX 控制項
MSN SpaceGoogle DocGoogle Blog
Chui-Wen Chiu(Arick)
2007.01.31

1. 新增 "Windows Control Library" 專案,取名為 dotNetCOM

2. 產生 COM 介面
在 class 加入 [Guid("1247B983-4861-460c-8A1E-5DCF579B0A61")] 和 [ComVisible(true)] 兩個 Attribute,Guid 可透過 GuideGen 產生唯一的 CLASS ID,ComVisible 是告知要產生 COM 介面,因此可透過 COM 方式存取這個類別。

[Guid("1247B983-4861-460c-8A1E-5DCF579B0A61")]
[ComVisible(true)]
public partial class UserControl1 : UserControl {
public UserControl1() {
InitializeComponent();
}
}
註:guideGen: C:Program FilesMicrosoft Visual Studio 8Common7Toolsguidgen.exe


3. 加入一些測試碼
[Guid("1247B983-4861-460c-8A1E-5DCF579B0A61")]
[ComVisible(true)]
public partial class UserControl1 : UserControl {
public UserControl1() {
InitializeComponent();
}

public Int32 Add(Int32 a, Int32 b) {
return a + b;
}
}

4. 開啟專案設定,將 Build 中的 Register for COM interop 勾選


4. 編譯專案

5. 測試
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<script type = 'text/javascript'>
<!--//
function body_onload(){
alert( '123+456=' + dotNetCOM.Add(123, 456));
}
//-->
</script>
</head>
<body onload = 'body_onload();'>
<object id="dotNetCOM"
classid="clsid:1247B983-4861-460c-8A1E-5DCF579B0A61"
width='0' height='0'/>
</body>
</html>

6. 執行結果



總結:
雖然這個方法可以利用既有的 C# 寫法來開發 COM,但是有兩個缺點,用戶端必須安裝 .NET Framework,否則使用者還是沒有辦法使用上述的元件,這個問題可能需要等 .NET Framework 普及後才能獲得比較好的解決。另外一個問題是,.NET 開發出來的 COM,其實是在 .NET 元件上加了一層 COM Wrapper,所以執行效率上會稍差些。

搜尋此網誌