MFC 透過巢狀類別而非多重繼承實作 COM 介面,利用介面對映機制將介面與實作該介面的巢狀類別關聯。MFC 透過 CCmdTarget 類別實作了 IUnknown 介面。本文以建立 TimeLogServer 元件為例,示範 COM 伺服器與用戶端關鍵程式碼。
建立 GUIDs.h,使用 guidgen.exe 產生唯一識別碼:
// 元件 CLSID
static const IID CLSID_TimeLogServer =
{0xa433e701, 0xe45e, 0x11d3, {0x97, 0xb5, 0x52, 0x54, 0x4c, 0xba, 0x7f, 0x28}};
// 介面 IID
static const IID IID_ITimeLog =
{0xa433e702, 0xe45e, 0x11d3, {0x97, 0xb5, 0x52, 0x54, 0x4c, 0xba, 0x7f, 0x28}};
建立 ITimeLogServer.h:
#include "GUIDs.h"
DECLARE_INTERFACE_(ITimeLog, IUnknown)
{
STDMETHOD(OutputLog)(BSTR* varLogText) PURE;
};
說明:DECLARE_INTERFACE_ 宣告介面,STDMETHOD 宣告回傳 HRESULT 的純虛擬函數。
使用 ClassWizard 新增 CTimeLogServer(基類別 CCmdTarget),修改標頭檔:
#include "ITimeLogServer.h"
// 宣告實作 ITimeLog 的巢狀類別
BEGIN_INTERFACE_PART(TimeLog, ITimeLog)
STDMETHOD(OutputLog)(BSTR* varLogText);
END_INTERFACE_PART(TimeLog)
// 宣告介面對映與類別工廠
DECLARE_INTERFACE_MAP()
DECLARE_OLECREATE(CTimeLogServer)
在實作檔中:
// 實作類別工廠
IMPLEMENT_OLECREATE(CTimeLogServer, "TimeLogServer",
0xa433e701, 0xe45e, 0x11d3, 0x97, 0xb5, 0x52, 0x54, 0x4c, 0xba, 0x7f, 0x28)
// 介面對映到巢狀類別
BEGIN_INTERFACE_MAP(CTimeLogServer, CCmdTarget)
INTERFACE_PART(CTimeLogServer, IID_ITimeLog, TimeLog)
END_INTERFACE_MAP()
CTimeLogServer::CTimeLogServer() { ::AfxOleLockApp(); }
CTimeLogServer::~CTimeLogServer() { ::AfxOleUnlockApp(); }
雖 CCmdTarget 已實作 IUnknown,仍需將巢狀類別的 IUnknown 對映至外部實作:
STDMETHODIMP_(ULONG) CTimeLogServer::XTimeLog::AddRef() {
METHOD_PROLOGUE(CTimeLogServer, TimeLog)
return pThis->ExternalAddRef();
}
STDMETHODIMP_(ULONG) CTimeLogServer::XTimeLog::Release() {
METHOD_PROLOGUE(CTimeLogServer, TimeLog)
return pThis->ExternalRelease();
}
STDMETHODIMP CTimeLogServer::XTimeLog::QueryInterface(REFIID riid, void** ppvObj) {
METHOD_PROLOGUE(CTimeLogServer, TimeLog)
return pThis->ExternalQueryInterface(&riid, ppvObj);
}
元件功能:將日誌文字寫入以日期命名的日誌檔。
// 類別屬性中加入檔案指標
FILE* m_logfile;
// 建構函數初始化
CTimeLogServer::CTimeLogServer() {
::AfxOleLockApp();
CTime TimeStamp = CTime::GetCurrentTime();
CString FileName;
FileName.Format(_T("%s.log"), TimeStamp.Format(_T("%Y%m%d")));
m_logfile = fopen(FileName, _T("a"));
if (m_logfile) {
fprintf(m_logfile, _T("########## 開始於:%s "), (LPCTSTR)TimeStamp.Format(_T("%Y年%m月%d日%H:%M:%S")));
}
}
// 解構函數關閉檔案
CTimeLogServer::~CTimeLogServer() {
::AfxOleUnlockApp();
if (m_logfile) {
CTime TimeStamp = CTime::GetCurrentTime();
fprintf(m_logfile, _T(" 結束於:%s ##########"), (LPCTSTR)TimeStamp.Format(_T("%Y年%m月%d日%H:%M:%S")));
fclose(m_logfile);
}
}
// 實作 OutputLog
STDMETHODIMP CTimeLogServer::XTimeLog::OutputLog(BSTR* varLogText) {
METHOD_PROLOGUE(CTimeLogServer, TimeLog)
if (pThis->m_logfile) {
CTime TimeStamp = CTime::GetCurrentTime();
CString NowTime = TimeStamp.Format(_T("%Y年%m月%d日%H:%M:%S"));
CString LogText((LPCWSTR)*varLogText);
fprintf(pThis->m_logfile, " %s %s ", NowTime, LogText);
return NOERROR;
}
AfxMessageBox(_T("沒有日誌檔案!"));
return S_FALSE;
}
BOOL CTimeLogServerApp::InitInstance() {
::AfxOleLockApp();
COleObjectFactory::RegisterAll(); // 註冊所有類別工廠
return TRUE;
}
int CTimeLogServerApp::ExitInstance() {
::AfxOleUnlockApp();
return CWinApp::ExitInstance();
}
用戶端步驟:初始化 COM 庫 → 建立元件物件取得 IUnknown → 查詢介面使用 → 釋放資源。
#include "ITimeLogServer.h"
HRESULT hResult;
IUnknown* pIUnknown = NULL;
hResult = ::CoInitialize(NULL);
if (FAILED(hResult)) { AfxMessageBox(_T("無法初始化 COM 庫!")); return FALSE; }
hResult = ::CoCreateInstance(CLSID_TimeLogServer, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pIUnknown);
if (FAILED(hResult)) { AfxMessageBox(_T("無法建立 TimeLog 物件!")); return FALSE; }
if (pIUnknown) {
ITimeLog* pITimeLog = NULL;
hResult = pIUnknown->QueryInterface(IID_ITimeLog, (void**)&pITimeLog);
if (FAILED(hResult)) { pIUnknown->Release(); return FALSE; }
BSTR bstrLogText = m_logtext.AllocSysString();
if (FAILED(pITimeLog->OutputLog(&bstrLogText))) {
AfxMessageBox(_T("日誌輸出錯誤!"));
} else {
AfxMessageBox(_T("日誌已寫入!"));
}
pITimeLog->Release();
}
pIUnknown->Release();
::CoUninitialize();
MFC 簡化了 COM 開發,透過巢狀類別、介面對映、CCmdTarget 內建 IUnknown 實作等機制,讓開發者專注於業務邏輯。本文完整展示從 GUID 定義、介面宣告、元件實作到用戶端呼叫的全流程,可作為 MFC COM 開發的範本參考。