COM 組件讓 C# 代碼可被 VB6、VBA、C++、Python、PowerShell 等非 .NET 環境呼叫。本文涵蓋介面定義屬性、類別實現、註冊部署、版本控管、跨語言呼叫範例。
| 屬性 | 用途 | 必要性 |
|---|---|---|
[ComVisible(true)] | 讓組件/類型對 COM 可見 | 組件層級必須 |
[Guid("...")] | 指定穩定 GUID (介面/類別/事件) | 全部必須,勿自動產生 |
[InterfaceType(ComInterfaceType.InterfaceIsDual)] | 支援早期/晚期綁定 | 介面建議 |
[ClassInterface(ClassInterfaceType.None)] | 不自動產生類別介面,強制走自定介面 | 類別建議 |
[DispId(n)] | 指定成員 DISPID,確保版本穩定 | 介面成員必須 |
[ProgId("MyLib.MyClass")] | 易讀程式識別碼,供 CreateObject 使用 | 類別建議 |
using System;
using System.Runtime.InteropServices;
namespace MyCompany.ComInterop
{
// 介面 GUID:固定不變,版本間相容關鍵
[Guid("1EA4DBF0-3C3B-11CF-810C-00AA00389B71")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)] // 支援 IDispatch (晚期綁定) + vtable (早期綁定)
[ComVisible(true)]
public interface IMessageLogger
{
[DispId(1)]
void LogSend(string senderId, string targetId, string fileName);
[DispId(2)]
void LogReceive(string receiverId, string senderId, string fileName);
[DispId(3)]
void LogSendSuccess(string senderId, string targetId, string fileName);
[DispId(4)]
void LogSendCancel(string cancelId, string targetId, string fileName);
[DispId(5)]
int GetLogCount(string userId); // 有回傳值
[DispId(6)]
string GetLastLog(string userId); // 字串回傳
}
}
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MyCompany.ComInterop
{
// 類別 GUID:固定
[Guid("A1B2C3D4-E5F6-7890-ABCD-EF1234567890")]
// 不自動產生 _MessageLogger 介面,強制使用 IMessageLogger
[ClassInterface(ClassInterfaceType.None)]
// ProgId:供 VBA/Python CreateObject("MyCompany.MessageLogger") 使用
[ProgId("MyCompany.MessageLogger")]
[ComVisible(true)]
public class MessageLogger : IMessageLogger
{
private readonly string _logDirectory;
private readonly object _lock = new();
public MessageLogger()
{
// 預設記錄至 CommonApplicationData
_logDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"MyCompany", "Logs");
Directory.CreateDirectory(_logDirectory);
}
// 允許自訂路徑 (給進階用戶)
public MessageLogger(string customLogPath) : this()
{
if (Directory.Exists(customLogPath))
_logDirectory = customLogPath;
}
private void WriteLog(string fileName, string content)
{
string path = Path.Combine(_logDirectory, fileName);
lock (_lock)
{
File.AppendAllText(path, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {content}{Environment.NewLine}");
}
}
public void LogSend(string senderId, string targetId, string fileName)
{
WriteLog("Send.log", $"{senderId} -> {targetId} : {fileName}");
}
public void LogReceive(string receiverId, string senderId, string fileName)
{
WriteLog("Receive.log", $"{receiverId} <- {senderId} : {fileName}");
}
public void LogSendSuccess(string senderId, string targetId, string fileName)
{
WriteLog("Success.log", $"{senderId} => {targetId} : {fileName} [OK]");
}
public void LogSendCancel(string cancelId, string targetId, string fileName)
{
WriteLog("Cancel.log", $"{cancelId} -X- {targetId} : {fileName} [CANCEL]");
}
public int GetLogCount(string userId)
{
string path = Path.Combine(_logDirectory, "Send.log");
if (!File.Exists(path)) return 0;
return File.ReadLines(path).Count(line => line.Contains(userId));
}
public string GetLastLog(string userId)
{
string path = Path.Combine(_logDirectory, "Send.log");
if (!File.Exists(path)) return string.Empty;
foreach (string line in File.ReadLines(path).Reverse())
{
if (line.Contains(userId)) return line;
}
return string.Empty;
}
}
}
using System.Runtime.InteropServices;
[assembly: ComVisible(true)] // 整個組件對 COM 可見
[assembly: Guid("ORGANIZATION-GUID-FOR-TYPELIB")] // TypeLib GUID
net48
true
true
true
專案屬性 → Build → 勾選 "Register for COM interop" (需管理員權限)
# 註冊並產生 .tlb
RegAsm.exe MyCompany.ComInterop.dll /tlb:MyCompany.ComInterop.tlb /codebase
# 僅註冊 (不產生 tlb)
RegAsm.exe MyCompany.ComInterop.dll /codebase
# 解除註冊
RegAsm.exe MyCompany.ComInterop.dll /unregister
# 64 位系統註冊到 32 位 (給 32 位呼叫者如 VBA)
C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe MyCompany.ComInterop.dll /tlb /codebase
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe MyCompany.ComInterop.dll /tlb /codebase
Sub TestComLogger()
Dim logger As Object
Set logger = CreateObject("MyCompany.MessageLogger")
logger.LogSend "USER001", "USER002", "Report.pdf"
logger.LogReceive "USER002", "USER001", "Report.pdf"
logger.LogSendSuccess "USER001", "USER002", "Report.pdf"
Dim count As Long
count = logger.GetLogCount("USER001")
MsgBox "USER001 發送記錄數: " & count
Dim lastLog As String
lastLog = logger.GetLastLog("USER001")
MsgBox "最後記錄: " & lastLog
End Sub
import win32com.client
logger = win32com.client.Dispatch("MyCompany.MessageLogger")
logger.LogSend("PYTHON01", "PYTHON02", "data.csv")
count = logger.GetLogCount("PYTHON01")
print(f"Log count: {count}")
#import "MyCompany.ComInterop.tlb" raw_interfaces_only, named_guids
using namespace MyCompany_ComInterop;
int main()
{
CoInitialize(NULL);
{
IMessageLoggerPtr logger(__uuidof(MessageLogger));
logger->LogSend(_bstr_t("CPP01"), _bstr_t("CPP02"), _bstr_t("file.txt"));
long count = logger->GetLogCount(_bstr_t("CPP01"));
printf("Count: %d\n", count);
}
CoUninitialize();
return 0;
}
$logger = New-Object -ComObject MyCompany.MessageLogger
$logger.LogSend("PS01", "PS02", "script.ps1")
$count = $logger.GetLogCount("PS01")
Write-Host "Count: $count"
// 晚期綁定 (無需參考組件)
dynamic logger = Activator.CreateInstance(Type.GetTypeFromProgID("MyCompany.MessageLogger"));
logger.LogSend("CS01", "CS02", "test.txt");
// 早期綁定 (專案參考組件)
using MyCompany.ComInterop;
IMessageLogger logger = new MessageLogger();
logger.LogSend("CS01", "CS02", "test.txt");
IMessageLogger2[assembly: TypeLibVersion(1, 0)],重大變更升主版本| 錯誤 | 原因 | 解決 |
|---|---|---|
| Class not registered (0x80040154) | 未註冊、位數不符 (32/64位) | 雙位數註冊、檢查呼叫者位數 |
| Type mismatch | 參數型別不符 (如傳 Integer 給 String) | VBA 用 CStr/CLng 轉型、Python 確保 str/int |
| Access denied | 寫入受保護目錄 (Program Files) | 改寫 CommonApplicationData、或提權 |
| Catastrophic failure (0x8000FFFF) | COM 物件內部例外未處理 | 所有 public 方法包 try-catch、回傳 HRESULT |
COM 互操作核心在於:穩定 GUID、明確 DispId、雙介面支援早晚綁定、正確註冊部署。現代 .NET 專案若需跨語言,優先考慮 gRPC、REST API、Named Pipes、MessagePack 等技術;維護舊系統整合時,本文模式可確保穩定相容。