為Non-COM程序添加對象模型(2)
初始化對象模型
創建壹個新的組件實例,調用Load方法來獲得壹對結果。首先,連接到記事本運行中的拷貝。其次,在記事本窗口中打開壹個已存在的文檔或創建壹個空文檔。
與記事本相結合,需要奪取主窗體的句柄和覆蓋了整個客戶端區域的編輯控件的句柄。可以用C++ FindWindow API函數檢索第壹個打開的並且和記事本的Windows類名“notepad”相匹配的窗口(此後臺信息已經可以由Spy++提供,它是壹個Visual Studio工具,可以透視Windows的隱私),可以使用以下的C++代碼:
STDMETHODIMP
CNotepadApplication::Load(BSTR bstrFile)
{
m_hwnd = FindWindow(_T("notepad"), NULL);
if (!IsWindow(m_hwnd))
_StartApp(OLE2T(bstrFile));
Load方法嘗試找到壹個運行中的記事本實例。如果成功,它忽略輸入的文件名。否則,它產生nodepad.exe,並用命令行傳遞bstrFile參數。
這是僅有的可能的方法來做到這些了。可以更改Load方法的行為遵守其他的規則。然而,需要註意的是,在程序的用戶接口中隱蔽地加載壹個文本文件是通過命令行來實現的。否則,必須求助File菜單中的Open命令,但這就不是自動和隱蔽的了。
壹旦找到了記事本主窗體的句柄,就可以利用它並使用C++代碼檢索子編輯控件。
m_hwndEdit = FindWindowEx(
m_hwnd, NULL, _T("edit"), NULL);
記事本的結構提供了壹個類名為“notepad”的窗口,它的客戶區域被壹個編輯控件占據——壹個類名為“edit”的窗口。FindWindowsEx API函數檢索第壹個類名為“edit”的窗口,它是m_hwnd的子女。
下壹步,在COM對象中創建壹個屬性,它描述子編輯控件的內容。調用名為Text的可讀寫屬性。給它壹個文本內容,它將會立即影響到記事本的緩沖區。
Set npad = CreateObject("NotepadOM.Application")
npad.Load ""
npad.Text = "Sample text"
在前面的代碼中,我們建立了壹個新的未明名的文本文檔,它的內容已經被賦予了某個字符串。當然,可以使用Text屬性連接文本到其他變量中。
npad.Text = "Sample text"
npad.Text = npad.Text & vbCrLf & "for the article"
即使記事本是個SDI程序,也可能需要像清晰的對象調用過程那樣公開文本內容,例如文檔操作。這符合更清楚、更雅致的模型設計,但是它仍需要為架構設計帶來多余的復雜性。為什麽創建壹個新的ATL對象僅僅是為了優化壹些文本相關的功能呢?
在實現Text屬性時,利用了Windows32編輯控件的壹個鮮為人知的特性。所有Windows32控件不能跨進程訪問。例如,不能請求另壹個應用程序的rich edit box以字符串類型返回它的內容。產生這個問題的原因是,任何內存地址只在進程管理範圍內才有效。這個規則有少部分例外。
所有的Windows標準控件buttons、listboxes、和edit controls或者其他控件都不違背這項規則。它們的內容以在進程間被任意地讀或寫。這功能在Windows 95時為了保持向後兼容現存的Windows3x程序就出現了,它用進程間子類化。此同樣存在於Windows XP和Windows 2000中。
可以使用壹些消息,如WM_GETTEXT和WM_SETTEXT來獲得或寫入文本框的內容而不顧實際進程的相關情況。同樣,當運行VBS腳本時,實際上已涉及到兩個不同的進程,記事本和wscript.exe,它們控制著VBS腳本。用C++實現此Text屬性,代碼如下:
STDMETHODIMP
CNotepadApplication::get_Text(BSTR *pVal)
{
USES_CONVERSION;
int nLen = 1 + SendMessage(m_hwndEdit, WM_GETTEXTLENGTH, 0, 0);
LPTSTR pszBuf = new TCHAR[nLen];
SendMessage(m_hwndEdit, WM_GETTEXT, nLen, (LPARAM) pszBuf);
*pVal = SysAllocString(T2OLE(pszBuf));
delete [] pszBuf;
return S_OK;
}
STDMETHODIMP
CNotepadApplication::put_Text(BSTR newVal)
{
USES_CONVERSION;
SendMessage(m_hwndEdit, WM_SETTEXT, 0, (LPARAM) OLE2T(newVal));
return S_OK;
}
添加編輯函數
訪問編輯控件的句柄可以弄清編輯所需的壹串函數——特別是關於文本選擇的部分。可以很容易地添加方法選擇所有的緩沖區中的文本或限制為某個區域選擇。SelectAll和SelectText用C++實現,方法如下:
STDMETHODIMP
CNotepadApplication::SelectText(
int nFrom, int nTo) {
SendMessage(m_hwndEdit, EM_SETSEL, nFrom-1, nTo-1);
return S_OK;
}
通過EM_SETSET消息可以很容易地在編輯控件中實現文本選擇。在Windows32中,第壹個可選的字符是在0位置,但是相關方法使它從1開始。而指定-1~0的範圍可以選擇整個文本。
編輯框中正文的字體名稱由某個註冊值lfFaceName決定,在以下位置可以找到此鍵值:
HKEY_CURRENT_USER
\Software
\Microsoft
\Notepad
將它設為想要用的鍵值。記事本在啟動之前讀取這個設置。為了使它生效,請記住在調用Load之前設置好它。
set npad = CreateObject("NotepadOM.Application")
npad.Font = "Lucida Console"
npad.Load "readme.txt"
當壹個交互式的用戶單擊菜單時,例如“File | Open”,主窗體發送WM_COMMAND消息,其中WPARAM參數被賦予串聯的兩個字。低位字是命令的ID,高位字包含消息碼或表示觸發的值——鍵盤加速鍵或菜單。用C++調用壹個菜單命令、發送壹個WM_COMMAND消息到記事本,代碼如下:
SendMessage(m_hwnd, WM_COMMAND,
MAKELONG(nCommand,0), 0);
必須用特殊的工具為nCommand參數指出正確的值,就像Spy++。既然這樣,我稍微修改文章中所描述的DLL版本。“Hook,Line and Sinker”〔Visual C++ Developers Journal February 2001〕。此例程產生並鉤住,然後創建記事本的子類。它過濾窗口接收到的所有消息,並在命令代碼是WM_COMMAND時彈出對話框顯示command ID。
if (uiMsg == WM_COMMAND) {
// Get the value of LOWORD(wParam)
}
需要添加的僅僅是存儲或顯示命令代碼的程序。檢驗主記事本的菜單命令ID。只要給出了這個,調用菜單命令就很簡單了,代碼如下:
const NOTEPAD_FILE_OPEN = 10
Set npad = CreateObject("NotepadOM.Application")
npad.InvokeMenu NOTEPAD_FILE_OPEN
如果要編程關閉運行中的實例,需要想到在記事本窗口上調用DestroyWindows。然而,DestroyWindows只能在屬於同壹進程的窗口的進程中調用。要卸載記事本,用C++簡單的發送壹條退出代碼的WM_COMMAND消息:
SendMessage(m_hwnd, WM_COMMAND,
MAKELONG(28,0), 0);
有些功能是無法從非自動化的程序中獲得的。例如,打開文件和另存為是不可能實現的,因為程序並不通過消息或API暴露這些代碼,需要編寫代碼來存儲它。舉個例子來說,在記事本中,存儲運行時結果需要響應Save或Save As命令,但是它們都是交互式的命令,需要用戶單擊OK按鈕或輸入壹個新的文件名。這是原解決方案固有的限制。
最近,在壹個客戶中碰到壹個相似的問題,我應要求在不同環境處理壹些傳統的Windows程序(其中壹個是記事本)。本質上來說,Win32 made-to-measure應用程序獲得TCP/IP通道指令並轉換它們以執行本地的Windows應用程序。通過Windows32消息請求服務的方式和在此所做的很相似。下壹目標是用COM對象模型封裝此通信模式。
關於作者
Dino Esposito是Wintellect的ADO.NET專家和培訓師並且在羅馬當咨詢師。Dino是《Building Web Solutions With ASP.NET and ADO.NET》(微軟出版)壹書的作者,是VB-2-The-Max ( <>)的創始人。可通過dinoe@wintellect.com聯系到Dino。