2009/12/10

親 手 打 造 C++Builder 的 TRACE Window

出處:Borland Taiwan
親 手 打 造 C++Builder 的 TRACE Window


曾 經 用 過 Visual C++ 1.5 的 讀 者 們 都 知 道 , 你 可 以 使 用 TRACE 這 個 巨 集 將 需 要 Debug 的 訊 息 送 到 一 個 名 為 DBWIN 的 視窗 上 , 而 咱 們 可 以 一 邊 執 行 程 式 , 一 邊 觀 看 DBWIN 中 由 程式 所 輸 出 過 來 的 TRACE 訊 息 。 但 不 幸 的 事 ,Visual C++ 1.5 的下 一 個 版 本Visual C++ 4.X 竟 然 開 始 不 包 含 了DBWIN , 為 什 麼 呢? 原 來 是 原 先DBWIN 裡 頭 所 包 含 的 所 有 功 能 都 被 整 合 到 了Visual C++ 的 整 合 環 境 裡 頭 , 且 功 能 更 被 發 揚 光 大 , 但TRACE 巨 集從 現 在 起 只 存 在 於 除 錯 模 式 , 並 且 還 被 硬 性 規 定 只 許 應用 程 式 在 整 合 環 境 內 執 行 時 其TRACE 功 能 才 能 夠 使 用 。 這對 於 只 不 過 想 要 像 使 用 printf 一 樣 簡 單 的 輸 出 一 點 訊 息 的咱 們 , 卻 必 須 動 用 到 這 樣 龐 大 的 除 錯 器 似 乎 有 點!@#$%^ 。


幸 好 在 Microsoft System Journal (MSJ) 上 Paul DiLascia 生 生 的 兩篇 文 章 為 我 們 打 造 了 一 個 叫 簡 單DBWIN 稱 為TraceWin , 稍 微 補足 了 這 項 小 小 的 不 快 。 對 於 使 用 Visual C++ 4.x 的 使 用 者 來說 , 有 如 久 旱 逢 甘 霖 一 般 。 在 當 時 筆 者 也 跟 著 依 樣 畫 葫蘆 地 自 己 打 造 了 一 個TraceWin ; 但 自 從1997 的 白 色 情 人 節 過後 筆 者 與C++Builder 結 下 了 不 解 之 緣 , 用C++Builder 開 發 了 許多 專 案 , 但 總 覺 得 缺 少 了 一 點 什 麼 。 對 ! 就 是C++Builder 專用 的TraceWin 。C++Builder 的 除 錯 環 境 很 難 使 用 嗎 ? 一 點 也 不! 功 能 強 大 的 令 人 瞠 目 結 舌 ; 但 我 只 是 希 望 能 夠 簡 單 的檢 查 程 式 的 流 程 、 資 料 的 內 容 , 並 不 需 要 動 用 到 這 麼 龐大 的 除 錯 環 境 。 我 要 的 只 是 希 望 能 把Debug 的 動 作 弄 得 簡單 點 。
以 往 在DOS 的 文 字 模 式 下 ,printf 是 個 簡 單 又 好 用 的 除 錯幫 手 , 可 以 在 需 要 檢 查 的 位 置 適 時 地 加 入printf 將 除 錯 資訊 輸 出 到 螢 幕 上 。 而 在Windows Programming 中 ,printf 或 許 還 能夠 在Consol Mode 裡 佔 據 從 前 保 有 的 江 山 , 但 在 圖 形 界 面 裡printf 已 毫 無 用 武 之 地 , 簡 單 的Debug 動 作 可 以 由 訊 息 對 話 盒 (Message Box ) 來 代 替 龐 大 的 除 錯 環 境 , 但 使 用Message Box 會 打 斷 原有 程 式 的 執 行 並 且 將 原 始 視 窗 被 遮 蓋 住 , 最 討 厭 的 是 還得 去 按 個OK 才 能 讓 程 式 繼 續 執 行 , 並 且 在 按 下OK 後 原 始 視窗 還 收 到 一 個 不 速 之 客 : 不 必 要 的WM_PAINT 訊 息 , 這 讓 我們 的 程 式 多 做 了 一 個 重 繪 的 動 作 , 訊 息 對 話 盒 似 乎 是 個很 不 好 的 選 擇 , 但 在 沒 有 TraceWin 之 前 , 這 是 唯 一 也 較 簡單 的 選 擇 了 , 有 多 簡 單 呢 ? 在C++Builder 下 只 要 使 用 ShowMessage() 這 個 函 示 , 就 可 產 生 一 個 文 字 訊 息 視 窗 。
    不 同 行 程 間 的 資 料 傳 送
先 想 一 想 , 咱 們 得 把 我 們 所 要 檢 查 的 資 料 給 送 到 另 一 個行 程 中 的 『 除 錯 視 窗 』 裡 , 那 們 我 們 何 法 子 可 以 辦 得 到呢 ? 對 ! 就 是 行 程 間 的 通 訊 (Interprocess Communication ,IPC ) 。 但 行 程 通 訊 的 方 法 種 類 繁 多 , 光 是 『 行 程 通 訊 』 這個 主 題 就 夠 筆 者 寫 上 數 萬 字 , 該 選 擇 哪 一 種 來 用 都 是 個難 題 , 所 幸 在 眾 多Windows Programming 書 籍 裡 都 找 得 到 討 論 行程 通 訊 這 主 題 的 章 節1 , 在 此 筆 者 就 不 多 嘴 , 咱 們 就 挑 個背 後 技 術 不 難 且 實 作 起 來 較 容 易 的 方 法 來 用 用 , 對 ! 就是 使 用 視 窗 訊 息 來 傳 送 資 料 : 由 以 往 程 式 寫 作 經 驗 告 訴 我 們 : 要 將 較 大 量 的 資 料 在程 式 內 部 間 傳 遞 , 最 方 便 的 方 法 就 是 使 用 指 標 的 傳 遞 來達 成 ; 但 在Win32 平 台 上 不 同 行 程 間 的 定 址 空 間 各 自 獨 立的 , 因 此 要 藉 由 指 標 把 資 料 傳 送 給 另 外 一 個 行 程 在Win32 平 台 上 是 不 被 允 許 但 又 是 不 可 或 缺 的 , 想 當 然 耳Microsoft 當 然 會 注 意 到 這 個 問 題 , 因 此Microsoft 提 供 了 一 個WM_COPYDATA 這 個 視 窗 訊 息 來 替 個 各 行 程 間 透 過 指 標 來 將 傳 遞 資 料 ,使 用WM_COPYDATA 這 個 視 窗 訊 息 時 系 統 會 自 動 幫 我 們 把 指 標所 指 到 的 資 料 由 發 出 端 的 行 程 定 址 空 間 內 複 製 到 接 收 端的 行 程 定 址 空 間 裡 頭 , 並 將 複 製 後 的 資 料 指 標 通 知 給 接收 端 知 道 。 因 此 使 用 這 個 方 法 就 沒 有 必 要 去 考 慮 到 各 個行 程 間 的 定 址 空 間 的 問 題 。 此 外 使 用WM_COPYDATA 是 在 不 同執 行 緒 ( 不 論 這 兩 個 執 行 緒 是 否 為 同 一 行 程 內 ) 之 間 搬移 資 料 最 簡 單 的 一 種 做 法 了 , 不 過 要 接 收 這 個 訊 息 的 目的 地 必 須 擁 有 一 個 視 窗 代 碼 (Window Handle) , 也 就 是 必 須是 個 視 窗 , 為 什 麼 呢 ? 沒 為 什 麼 , 因 為 你 必 須 傳 遞 訊 息的 函 式SendMessage 的 目 的 地 必 須 是 個 視 窗 ,SendMessage 的 第 一個 參 數 就 是 接 收 訊 息 端 的 視 窗 代 碼 。
WM_COPYDATA 訊 息 的 使 用 如 下 ( 發 出 訊 息 端 ) :
SendMessage(
hwndReceiver,
WM_COPYDATA,
(WPARAM)hwndSender,
(LPARAM)&cds
);
hwndReceiver : 接 收 訊 息 端 的 視 窗 代 碼
hwndSender : 發 出 訊 息 端 的 視 窗 代 碼
cds : 需 要 傳 送 的 資 料 的 指 標
其 中LPARAM 參 數 中 的 cds 指 向 一 個 特 定 的Windows 資 料 結 構:COPYDATASTRUCT , 其 結 構 如 下 :
typedef struct tagCOPYDATASTRUCT { // cds DWORD dwData;
DWORD cbData;
PVOID lpData;
}
COPYDATASTRUCT,*PCOPYDATASTRUCT; dwData : 為 使 用 者 的 自 定 值, 使 用 者 可 以 透 過 此 值 來 通 知 接 收 端 其 傳 送 的 資 料 類 別以 及 傳 送 的 資 料 用 途 。
cbData : lpData 所 指 到 的 資 料 大 小 , 以 byte 為 單 位 。
LpData : 所 要 傳 送 資 料 的 起 始 指 標 。 資 料 的 內 容 可 以 是任 何 的 資 料 。
    取 得 接 收 端 的 視 窗 代 碼
大 約 知 道 了 怎 麼 把WM_COPYDATA 訊 息 送 出 後 , 咱 們 再 來 要 考慮 的 就 是 接 收 端 如 何 收 到 訊 息 的 問 題 。 不 過 在 此 之 前 ,在 訊 息 的 發 出 端 咱 們 還 得 知 道 接 收 端 的 視 窗 代 碼 , 因 為若 沒 有 接 收 端 的 視 窗 代 碼 就 好 比 郵 差 送 信 不 知 道 收 件 人地 址 一 般 , 當 然 是 送 不 出 去 啦 。 筆 者 最 常 用 來 取 得 接 收 端 的 視 窗 代 碼 的 方 法 有 兩 種 :
1. 使 用FindWindow
FindWindow 是 Windows API ,FindWindow 的 原 始 定 義 為 :
HWND FindWindow(
LPCTSTR lpClassName, // pointer to class name
LPCTSTR lpWindowName // pointer to window name
);
lpClassName : 為 視 窗 在 建 立 時 所 註 冊 的 視 窗 類 別
lpWindowName : 為 視 窗 的 名 稱 , 通 常 是 視 窗 的 標 題( Caption )
由 這 個 API 的 定 義 就 不 難 看 出 , 只 要 我 們 知 道 接 收 端 的視 窗 標 題 或 是 知 道 接 收 端 註 冊 的 視 窗 類 別 , 我 們 就 有 辦法 把 訊 息 給 傳 送 出 去 。 但 若 有 相 同 的 兩 個 視 窗 先 後 被 執行 時 怎 麼 辦 呢 ? FindWindow 會 找 到 先 執 行 的 那 個 視 窗 , 並取 回 其 視 窗 代 碼 。
所 以 , 只 要 接 收 端 的 視 窗 標 題 不 會 改 變 , 那 麼 我 們 就可 以 使 用FindWindow 來 取 得 接 收 端 的 視 窗 代 碼 。
2. 註 冊 視 窗 訊 息 與 廣 播 訊 息 (Register Window Message and Broadcast Message )
使 用 『 註 冊 視 窗 訊 息 與 廣 播 訊 息 』 是 在 於 你 不 想 用FindWindow 的 狀 況 下 使 用 的 。 為 什 麼 會 不 想 要 用FindWindow 呢 ? 就 是 先前 才 提 到 的 : 接 收 端 的 視 窗 標 題 會 改 變 , 若 你 寫 的 程 式須 常 常 變 換 視 窗 標 題 , 那 麼 在 使 用FindWindow 時 不 就 很 難 處理 嗎 ? 還 必 須 知 道 接 收 端 的 視 窗 標 題 改 變 成 什 麼 。
因 此 可 換 成 使 用 註 冊 視 窗 訊 息 與 廣 播 訊 息 的 方 式 來 取得 接 收 端 的 視 窗 代 碼 : 在 程 式 第 一 次 執 行 時 , 發 出 端 透過SendMessage 將 一 個 已 經 註 冊 好 的 視 窗 訊 息 給 廣 播 出 去2 ,當 接 收 端 收 到 這 個 已 經 註 冊 過 的 特 定 訊 息 時 , 會 回 傳 另一 個 自 訂 的 訊 息 給 廣 播 訊 息 的 發 出 端 , 而 且 這 個 回 傳 的訊 息 裡 還 帶 有 接 收 端 的 視 窗 代 碼 , 如 此 一 來 就 發 出 端 就可 以 掌 握 住 接 收 端 的 視 窗 代 碼 , 這 樣 一 來 , 即 使 接 收 端的 視 窗 標 題 常 常 在 變 換 , 對 發 出 端 來 說 要 將 訊 息 傳 送 給指 定 的 接 收 端 也 不 是 什 麼 難 事 了 。 但 使 用 這 種 方 法 的 前提 是 在 於 在 發 出 訊 息 端 程 式 執 行 前 接 收 訊 息 端 就 必 須 得先 執 行 了 , 不 然 沒 有 辦 法 傳 回 接 收 端 的 視 窗 代 碼 。 當 然了 , 若 你 要 每 次 發 送 訊 息 前 來 做 視 窗 代 碼 的 傳 遞 也 無 不可 , 只 是 這 樣 一 來 一 往 會 浪 費 掉 許 多 訊 息 傳 送 的 時 間 。
上 述 這 兩 個 方 法 都 很 不 錯 , 但 使 用 註 冊 視 窗 訊 息 與 廣播 訊 息 一 來 一 往 傳 遞 訊 息 次 數 過 於 頻 繁 , 且 所 撰 寫 需 程式 碼 也 較 為 多 些 , 筆 者 暫 時 不 考 慮 使 用 。
    接 收 端 接 收 訊 息
接 收 端 在 接 收 訊 息 這 個 部 份 處 理 流 程 較 為 簡 單 , 就 是 確認 收 到WM_COPYDATA 訊 息 , 並 且 將 這 個 訊 息 依 照 內 容 做 其 該做 的 處 理 。 這 個 主 題 曾 在53 期 周 先 生 的 『BCB 與Win32 訊 息 傳遞 』 一 文 裡 已 被 討 論 到 , 若 對 這 個 主 題 尚 有 不 了 解 的 ,筆 者 建 議 讀 者 們 先 把53 期 的 那 篇 文 章 給 先 看 過 。 但 筆 者在 此 將 會 對 這 個 主 題 做 更 深 入 的 探 討 , 雖 說 接 收 訊 的 流程 乍 看 之 下 很 簡 單 , 但 在C++Builder 的VCL 龐 大 架 構 下 , 視 窗訊 息 的 處 理 程 序 已 經 經 過VCL 架 構 的 層 層 包 裝 , 在VCL 的 背後 隱 藏 了 許 多 高 超 的 技 巧 , 但 對 一 般 使 用 者 來 說 , 只 不過 是 看 到 了 其 神 祕 面 紗 之 外 的 形 象 , 就 讓 筆 者 揭 開 這 神祕 面 紗 一 窺 究 竟 。 在 C++Builder 下 , 處 理 視 窗 訊 息 的 方 式 有 很 多 種 , 我 們要 選 擇 哪 一 種 呢 ? 都 可 以 ! 你 只 要 確 定 你 能 夠 收 到 這 個訊 息 並 且 正 確 的 處 理 他 就 可 以 了 , 你 可 以 送 給 這 個 應 用程 式 的 主 控 制 視 窗 , 也 可 以 送 到 應 用 程 式 的 主 視 窗 。 主控 制 視 窗 ? 主 視 窗 ? 都 快 搞 糊 塗 了 , 這 到 底 怎 麼 說 呢 ?說 來 話 長 , 這 得 從C++Builder 所 開 發 出 來 的 應 用 程 式 的 啟 動流 程 談 起 。
    C++Builder 應 用 程 式 啟 動 流 程
先 讓 咱 們 用C++Builder 建 立 一 個 新 的 應 用 程 式 ( 使 用 選 單 上【File 】\ 【New Application 】 ) ,Project 與Form 的 名 稱 都 保 持 預設 值 :Project 名 稱 為Project1 , 而 那 個 上 面 什 麼 物 件 都 沒 有的Form 為Form1 。 在 編 譯 並 執 行 後 讓 人 直 覺 性 的 認 為 這 個Form1 應 該 就 是 應 用 程 式 的 主 視 窗 , 並 且 應 該 也 掌 控 所 有 發 出給 此 應 用 程 式 的 訊 息 。 其 實 不 然 , 在Form1 背 後 的 黑 手 令有 其 人 , 那 到 底 是 誰 呢 ? 讓 我 們 先 用C++Builder 所 附 上 的WinSight ─ 這 個 好 用 的 工 具 來 觀 察 我 們 所 執 行 的Project1.exe ( 圖 一) , 由 圖 一 可 以 發 現 同 一 個Project.exe 裡 竟 然 會 有 兩 個Window Control 的 出 現 , 一 個 叫 做 TApplication , 而 另 一 個 叫 做TForm1 。 那 到 底 哪 個 才 是 這 個 應 用 程 式 的 主 要 幕 後 黑 手 呢 ? 是TApplication 還 是TForm1 ? 再 利 用WinSight 先 來 看 看TApplication 的 更 深 入 的 內 容 ( 圖二 ) , 由 圖 中 可 以 看 到TApplication 的 視 窗 大 小 是 零 , 而 其 Windows text ( 視 窗 標 題 ) 是 Project1 , 其Class name ( 視 窗 類別 ) 是 TApplication ; 視 窗 大 小 是 『 零 』 , 也 就 是 說 這 個Window 是 看 不 見 的 ,TApplication 的 隱 形 頗 有 躲 在 背 後 的 意 義 。 看起 來 似 乎 這TApplication 似 乎 就 是 幕 後 的 黑 手 , 先 聲 明 只 是看 起 來 而 已 , 稍 後 會 想 辦 法 驗 證 的 。 而 這 裡 所 提 到 的 『視 窗 標 題 』 與 『 視 窗 類 別 』 其 實 就 是 先 前 在 提 到FindWindow 這 個API 所 使 用 的lpWindowName 與lpClassName 了 ! 但 若 使 用FindWindow 來 傳 送 訊 息 給 現 在 我 們 觀 察 的 這 個 應 用 程 式 , 我 們 到 底應 該 把 訊 息 送 給 誰 呢 ? 是 送 給TApplication 還 是 送 給TForm1 呢? 容 後 在 述 。
先 看 看 是 否 真 的 如 筆 者 所 說TForm1 真 的 是 由TApplication 所掌 管 呢 ? 可 惜C++Builder 所 附 上 的WinSight 並 沒 有 辦 法 查 出TForm1 的 管 理 者 也 就 是 擁 有 者 是 那 個 視 窗 , 所 以 我 們 無 法 由WinSight 看 出 任 何 的 端 倪 , 不 過 幸 好 使 用 筆 者 使 用Visual C++ 所 附 上與WinSight 功 能 相 似 的Spy++ 倒 是 可 以 讓 我 們 瞧 見 所 要 的 證 據( 圖 三 ) :
由 圖 三 很 清 楚 的 可 以 看 出 ,TForm1 的Owner Window 就 是 隱 藏起 來 的TApplication 。 由 以 上 所 蒐 集 的 資 料 來 看TApplication 類別 , 似 乎TApplication 類 別 是 由C++Builder 所 建 立 的 應 用 程 式 中的 大 總 管 , 這 一 切 也 在Project1 的Project1.cpp 裡 透 露 出 這 祕密 :
程 式 列 表 一: 、Project1.cpp :
    #include #pragma hdrstop USERES("Project1.res"); USEFORM("Unit1.cpp", Form1); //-------------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { Application->Initialize(); Application->CreateForm(__classid(TForm1), &Form1); Application->Run(); } catch (Exception &exception) { Application->ShowException(&exception); } return 0; } //--------------------------------------------------------------
此 處 看 到 的Application 就 是TApplication 類 別 的 實 體 物 件 , 並且 是 個 全 域 物 件 。 但 由 這 幾 行 程 式 碼 來 看 為 何C++Builder 所建 立 的 應 用 程 式 如 此 神 奇 , 只 需 由0011 這 列 到0013 這 列 就可 以 讓 程 式 持 續 的 執 行 了 ? 其 實 在 這 三 列 程 式 的 背 後 可是 有 數 千 列 的 幕 後 黑 手 在 撐 著 , 且 讓 筆 者 先 把 這 三 行 的動 作 內 容 給 解 釋 一 下 :
    Application->Initialize(); 進 行Application 物 件 的 初 始 化 , 如 COM 與 OLE Automation 的 初始 化 Application->CreateForm(__classid(TForm1), &Form1); 開 始 建 立 主 視 窗 , 也 就 是Form1 , 並 顯 示Form1 Application->Run();
進 入 訊 息 分 派 迴 圈 裡 , 一 直 到 應 用 程 式 收 到WM_QUIT 訊 息 後才 跳 出 訊 息 分 派 迴 圈 並 結 束 應 用 程 式 。
喔 ! 原 來 整 個 由C++Builder 所 建 立 的 應 用 程 式 所 收 到 的 視窗 訊 息 , 都 會 由Application 這 全 域 物 件 來 接 收 , 並 且 透 過訊 息 在VCL 架 構 裡 的 流 動 把 訊 息 分 配 到 該 分 配 的 地 方 去 ,但 僅 止 於 他 認 得 的 訊 息 。 為 什 麼 說 僅 止 於 他 認 得 的 呢 ?這 得 討 論 到VCL 的 內 部 架 構 , 且 聽 筆 者 慢 慢 說 下 去 。
    VCL 對 訊 息 的 處 理
暫 且 回 到 先 前 的 問 題 , 要 把 訊 息 從 發 出 訊 息 端 送 給 誰 呢? 是 送 給Application 還 是 送 給Form1 呢 ? 先 前 已 經 將 答 案 給 透露 了 , 都 可 以 啦 ! 但 是 處 理 的 方 法 有 所 不 同 罷 了 。 在 討論 這 個 問 題 之 前 , 先 看 看 這 一 張 簡 化 的VCL 類 別 繼 承 架 構圖 ( 圖 四 ) , 讓 我 們 稍 稍 的 了 解 一 下VCL 類 別 之 間 的 關 係。 稍 後 會 使 用 到 這 些 關 係 : 接 下 來 就 讓 我 們 討 論 討 論 在C++Builder 下 接 收 視 窗 訊 息 的做 法 。 在C++Builder 下 有 很 多 做 法 , 咱 們 一 一 來 討 論 :
1 、 覆 載Dispatch 虛 擬 函 式 : ( 使 用Message Map )
在53 期 的 『BCB 與Win32 訊 息 傳 遞 』 一 文 內 曾 用 到 以 下 的 做法 來 接 收 並 處 理 訊 息 :
BEGIN_MESSAGE_MAP
VCL_MESSAGE_HANDLER( … …)
END_MESSAGE_MAP( …)
但 這 個 機 制 到 底 是 怎 麼 做 成 的 呢 ? 咱 們 把 他 從sysdef.h 裡 給 挖 出 來 瞧 一 瞧 :
程 式 列 表 二 、sysdefs.h :
    #define BEGIN_MESSAGE_MAP virtual void __fastcall Dispatch(void *Message) \ { \ switch (((PMessage)Message)->Msg) \ { #define VCL_MESSAGE_HANDLER(msg,type,meth) \ case msg: \ meth(*((type *)Message)); \ break; // NOTE: ATL defines a MESSAGE_HANDLER macro which conflicts with VCL's macro. The // VCL macro has been renamed to VCL_MESSAGE_HANDLER. If you are not using ATL, // MESSAGE_HANDLER is defined as in previous versions of BCB. // #if !defined(USING_ATL) && !defined(USING_ATLVCL) && !defined(INC_ATL_HEADERS) #define MESSAGE_HANDLER VCL_MESSAGE_HANDLER #endif // ATL_COMPAT #define END_MESSAGE_MAP(base) default: \ base::Dispatch(Message); \ break; \ } \ }
在sysdefs.h 裡 定 義 了 這 三 個 巨 集 , 讓 我 們 來 看 一 看 這 三 個巨 集 到 底 在 定 義 些 什 麼 ? A 、BEGIN_MESSAGE_MAP : (601 行 至603 行 )
這 個 巨 集 在 類 別 的 定 義 中 以 虛 擬 的 方 式 重 新 覆 載 了Dispatch 函 式 , 而Dispatch 的 原 始 定 義 到 底 是 什 麼 呢 ?Dispatch 原 以 虛擬 的 方 式 定 義 在TObject 裡 , 而 其 目 地 是 在 於 視 窗 訊 息 的 分配 。 而 緊 接 著 在 這 個 虛 擬 函 示 宣 告 之 後 的 是 一 個switch 敘述 , 用 來 分 派 視 窗 訊 息 交 給 之 後 的 VCL_MESSAGE_HANDLER 巨 集來 處 理 。
B 、VCL_MESSAGE_HANDLER : (606 行 至617 行 )
先 看 到615 至617 這 幾 行 程 式 , 你 可 以 發 現 , 若 沒 有 使 用ATL (Active Template Libray ) ,MESSAGE_HANDLER 就 自 動 被 定 義 成VCL_MESSAGE_HANDLER , 但 筆 者 在 這 裡 還 是 建 議 你 , 多 打 幾 個 字 會 比 較 保 險 些, 以 免 以 後 在 移 植 程 式 時 因 為 這 小 小 的 疏 忽 而 造 成 程 式的 錯 誤 , 而 一 時 無 法 察 覺 。
再 看 到607 至609 行 , 可 以 發 現 是 直 接 在BEGIN_MESSAGE_MAP 的switch 敘 述 之 後 加 入case 進 入 點 來 處 理 指 定 視 窗 訊 息 的 。 舉 個 例子 來 說 , 若 我 們 使 用VCL_MESSAGE_HANDLER 這 個 巨 集 如 下 :
VCL_MESSAGE_HANDLER(WM_COPYDATA,TMessage,OnCopyData)
則 會 會 被 展 開 成 :
case WM_COPYDATA:
OnCopyData(*((TMessage *) Message));
break;
C 、END_MESSAGE_MAP : (619 行 至623 行 )
想 當 然 耳 ,END_MESSAGE_MAP 巨 集 的 用 途 應 該 就 是 來 終 結switch 敘 述 , 但 你 可 以 發 現620 行 上 頭 的base::Dispatch(Message) 是 用來 將 不 認 得 的 訊 息 交 給 父 類 別 的Dispatch 虛 擬 函 式 做 處 理。
因 此 所 以 若 我 們 加 入 以 下 的 範 例 : BEGIN_MESSAGE_MAP
VCL_MESSAGE_HANDLER(WM_COPYDATA,TMessage,OnCopyData)
END_MESSAGE_MAP(TForm);
則 會 被 編 譯 成 : virtual void __fastcall Dispatch(void *Message)
{
switch (((PMessage)Message)->Msg)
{
case WM_COPYDATA:
OnCopyData(*((TMessage *) Message)); // 處 理WM_COPYDATA 的 函 式
break;
default:
TForm::Dispatch(Message);
break;
}
}
Dispatch 的 原 始 定 義 是 在 於TObject 這 個 基 礎 類 別 上 , 只要 是TObject 的 衍 生 類 別 都 可 以 覆 載 這 個 虛 擬 函 式 來 處 理 自己 特 定 的 訊 息 , 但 若Dispatch 不 認 得 這 個 送 來 的 訊 息 最 後會 把 處 理 程 序 丟 改TObject::DefaultHandler 這 個 函 式 處 理 , 不過TObject::DefaultHandler 並 未 實 作 任 何 的 動 作 , 真 正 的 功 能還 必 須 透 過 以 後 的 覆 載 來 達 成 。 挖VCL 的 原 始 碼 很 有 趣 吧! 若 還 覺 得 光 聽 筆 者 這 樣 說 說 還 不 夠 有 趣 的 話 , 可 以 自行 挖 掘 這 段VCL 原 始 碼 ; 在 此 要 先 說 明 一 下 , 若 讀 者 們 購買 的 是C++Builder 專 業 版 或 更 高 級 的 版 本 , 光 碟 片 裡 頭 都 會含 有VCL 的 原 始 碼 , 但 若 是 標 準 版 就 沒 有 附 上 原 始 始 碼 了。 有 志 挖 掘VCL 架 構 的 內 部 奧 祕 者 都 可 以 自 己 來 探 求VCL 背後 的 真 面 目 。OK !TObject::Dispatch 的 原 始 碼 你 可 以 在 :source\vcl\system.pas 裡 頭 找 到 , 有 興 趣 的 讀 者 可 以 把 這 段 程 式 給 拿 出 來 看 ,不 過 先 透 露 一 下 , 為 了 加 速 訊 息 的 傳 遞 ,Borland 的VCL 開 發小 組 把TObject::Dispatch 函 式 都 採 用 組 合 語 言 來 撰 寫 , 已 達加 速 處 理 的 目 的 。
但 僅 只 有 覆 載Dispatch 這 一 種 方 法 嗎 ? 不 還 有 很 多 呢 !接 著 看 下 去 。
2 、 覆 載WndProc 函 式
剛 才 談 過TObject::Dispatch 的 做 法 , 再 來 談 談TControl 裡 頭 對於 訊 息 的 處 理 , 在TControl 裡 頭 處 理 訊 息 有 兩 個 一 個 是 由TObject 衍 生 而 來 的Dispatch , 但 在TControl 裡 頭 並 沒 有 複 寫Dispatch 函式 , 依 舊 採 用TObject 裡 頭 的Dispatch 函 式 , 而 另 一 個 方 式 就是WndProc 函 式 , 先 來 瞧 瞧TControl 類 別 裡 頭 的WndProc 做 些 什 麼:
程 式 列 表 三 、controls.pas :
    procedure TControl.WndProc(var Message: TMessage); var Form: TCustomForm; begin if (csDesigning in ComponentState) then begin Form := GetParentForm(Self); if (Form <> nil) and (Form.Designer <> nil) and Form.Designer.IsDesignMsg(Self, Message) then Exit; end; if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) then begin Form := GetParentForm(Self); if (Form <> nil) and Form.WantChildKey(Self, Message) then Exit; end; if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then begin if not (csDoubleClicks in ControlStyle) then case Message.Msg of WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK: Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN); end; case Message.Msg of WM_MOUSEMOVE: Application.HintMouseMessage(Self, Message); WM_LBUTTONDOWN, WM_LBUTTONDBLCLK: begin if FDragMode = dmAutomatic then begin BeginDrag(True); Exit; end; Include(FControlState, csLButtonDown); end; WM_LBUTTONUP: Exclude(FControlState, csLButtonDown); end; end; Dispatch(Message); end;
由2429 至2451 行 程 式 中 可 以 看 得 到 ,TControl 類 別 僅 處 理 的 與滑 鼠 動 作 相 關 的 視 窗 訊 息 , 如 :WM_MOUSEMOVE 、WM_LBUTTONDOWN 這 一 類 的 訊 息 , 而 其 他 的 訊 息 就 丟 給Dispatch() 處 理 。 從VCL 的 原 始 碼 可 以 得 知TControl 類 別 是 沒 有 包 含 視 窗 代 碼 的 。怪 怪 !TControl 類 別 不 是 沒 有 視 窗 代 碼 嗎 ? 怎 麼 可 以 收 得這 些 必 須 有 視 窗 代 碼 才 收 得 到 的 到 滑 鼠 訊 息 呢 ? 別 急 ,先 接 著 看 到TControl 的 衍 生 類 別 :TWinControl 類 別 , 打 從TWinControl 類 別 開 始 才 包 含 了 視 窗 代 碼 , 先 來 看 看TWinControl 類 別 中對 於WndProc 函 式 的 覆 載 是 怎 麼 寫 的 :
    procedure TWinControl.WndProc(var Message: TMessage); var Form: TCustomForm; begin case Message.Msg of WM_SETFOCUS: begin Form := GetParentForm(Self); if (Form <> nil) and not Form.SetFocusedControl(Self) then Exit; end; WM_KILLFOCUS: if csFocusing in ControlState then Exit; WM_NCHITTEST: begin inherited WndProc(Message); if (Message.Result = HTTRANSPARENT) and (ControlAtPos(ScreenToClient(SmallPointToPoint( TWMNCHitTest(Message).Pos)), False) <> nil) then Message.Result := HTCLIENT; Exit; end; WM_MOUSEFIRST..WM_MOUSELAST: if IsControlMouseMsg(TWMMouse(Message)) then Exit; WM_KEYFIRST..WM_KEYLAST: if Dragging then Exit; WM_CANCELMODE: if (GetCapture = Handle) and (CaptureControl <> nil) and (CaptureControl.Parent = Self) then CaptureControl.Perform(WM_CANCELMODE, 0, 0); end; inherited WndProc(Message); end;
可 以 由3392 至3393 行 上 看 到 : 滑 鼠 訊 息 的 處 理 部 份 交 由TWinControl::IsControlMouseMsg() 函 式 處 理 , 而 在3401 行 中TWinControl 類 別 最 後 把 不 認 得 的 訊息 交 給 父 類 別TControl 的WndProc 做 處 理 ,TControl::WndProc 先 前已 經 瞧 過 了 , 但 這 不 是 我 們 想 要 知 道 的 重 點 , 現 在 我 們想 知 道 的 是 為 什 麼TControl 類 別 可 以 收 到 由TWinControl 才 收 得到 的 滑 鼠 訊 息 呢 ? 讓 我 們 追 下 去 看TWinControl::IsControlMouseMsg() :
    function TWinControl.IsControlMouseMsg(var Message: TWMMouse): Boolean; var Control: TControl; P: TPoint; begin if GetCapture = Handle then begin Control := nil; if (CaptureControl <> nil) and (CaptureControl.Parent = Self) then Control := CaptureControl; end else Control := ControlAtPos(SmallPointToPoint(Message.Pos), False); Result := False; if Control <> nil then begin P.X := Message.XPos - Control.Left; P.Y := Message.YPos - Control.Top; Control.Perform(Message.Msg,Message.Keys,Longint( PointToSmallPoint(P))); Result := True; end; end;
哦 ! 原 來TControl::WndProc 所 收 到 的 滑 鼠 訊 息 是 由TWinControl::WndProc 裡 頭 的IsControlMouseMsg() 函 式 所 傳 遞 出 來 的 。 但 是 由 圖 四 來看 ,TWinControl 不 是TControl 的 衍 生 類 別 嗎 ? 怎 麼 可 以 … ? 可以 的 , 在TControl 類 別 上 有 一 個 很 重 要 且 必 要 的 屬 性 , 就是TControl 類 別 必 須 是 由TWinControl 管 理 , 正 確 的 說 :TControl 類 別 以 或TControl 的 衍 生 類 別 的Owner 必 須 是TWinControl 或TWinControl 的 衍 生 類 別 ; 什 麼 是Owner ?Owner 可 以 稱 為 『 物 件 擁 有 者 』, 是 打 從TComponent 類 別 開 始 有 的 , 物 件 的Owner 負 責 物 件 的管 理 以 及 訊 息 的 分 派 。 就 好 比TGraphicControl 的 衍 生 類 別 :TImage 類 別 , 當 我 們 把TImage 元 件 被 我 們 置 放 到TWinControl 的 衍 生 類 別TForm1 上 時 ,TForm1 就 是TImage 的Owner , 而Owner 把 該 送 給 其 所 擁 有 的 子 物 件 的 所有 訊 息 給 發 送 出 去 , 如 此 一 來 這 個TImage 就 能 夠 擁 有 處 理滑 鼠 訊 息 的 能 力 了 。
由 以 上 可 知 , 若 要 在 接 收 端 接 收 訊 息 , 除 了 覆 載Dispatch 函 式 外 , 也 可 以 在 接 收 端 重 新 覆 載WndProc 這 個 函 式 來 達 到訊 息 的 處 理 目 的 。 方 法 如 下 :
    void __fastcall TForm1::WndProc(TMessage &Message) { switch(Message.Msg) { case WM_COPYDATA: OnCopyData(Message);// 處 理WM_COPYDATA 的 函 式 break; } TForm::WndProc(Message); }
這 樣 的 做 法 與 覆 載Dispatch() 方 法 相 似 , 但 實 際 上 還 是 有 所不 同 , 哪 裡 不 同 呢 ? 由 程 式 列 表 三 的2451 行 上 可 以 看 到 ,在 處 理 的 程 序 上 是 先 先 由WndProc 來 作 訊 息 的 處 理 , 若 不 認得 訊 息 最 後 才 把 訊 息 丟 給Dispatch 處 理 , 因 此 若 使 用 覆 載WndProc 函 式 的 方 式 來 處 理 訊 息 會 比 覆 載Dispatch 函 式 早 一 點 點 收到 並 處 理 訊 息 。 還 有 沒 有 比 覆 載WndProc 函 式 方 法 更 早 攔 截得 到 訊 息 的 方 法 呢 ? 有 的 ! 接 下 來 咱 們 來 看 看VCL 架 構 裡的 訊 息 接 收 先 鋒 。 3 、Application OnMessage
先 前 說 過 ,Application 物 件 是 整 個 應 用 程 式 的 幕 後 黑 手, 由 程 式 列 表 一 的13 行 開 始 進 入 整 個 應 用 程 式 的 訊 息 迴圈 , 一 直 到 收 到WM_QUIT 這 個 訊 息 才 跳 出 迴 圈 並 結 束 程 式 的執 行 。
先 來 看 看TApplication::Run 到 底 是 怎 麼 讓 程 式 進 入 訊 息 迴圈 :
程 式 列 表 四 、forms.pas
    procedure TApplication.Run; begin FRunning := True; try AddExitProc(DoneApplication); if FMainForm <> nil then begin case CmdShow of SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized; SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized; end; if FShowMainForm then if FMainForm.FWindowState = wsMinimized then Minimize else FMainForm.Visible := True; repeat HandleMessage until Terminated; end; finally FRunning := False; end; end;
我 們 可 以 看 出TApplication::Run 所 作 的 事 情 就 是 先 將 主 視 窗(FMainForm) 的 視 窗 形 態 (WindowsState , 如: 放 到 最 大 、 縮 到 最 小 與 正 常大 小) 給 設 定 好 (4910 至4916 行 ) , 並 設 定 主 視 窗 的 顯 示 與否(Visible ,4917 行 ) 。 而4918 至4920 行 是 個 迴 圈 , 由 程 式 碼上 可 以 看 出 這 個 迴 圈 會 一 直 執 行 直 到Terminated 這 個 旗 標 被設 定 為true 時 才 中 斷 , 這 個 近 乎 無 窮 迴 圈 也 就 是 我 們 所 謂的 訊 息 處 理 迴 圈 。 所 有 由 外 部 進 到 應 用 程 式 的 訊 息 都 得透 過 這 個 迴 圈 做 分 派 處 理 。 咱 們 繼 續 追 下 去 看 :
    procedure TApplication.HandleMessage; begin if not ProcessMessage then Idle; end;
由TApplication::Run 追 下 來 到 了TApplication::HandleMessage , 可 以發 現 會 先 交 給TApplication::ProcessMessage 處 理 , 若 沒 有 訊 息 需要 處 理 就 交 給TApplication::Idle 做 處 理 , 也 就 是 應 用 程 式 閒置 時 的 處 理 函 式 。 先 看 看TApplication::ProcessMessage 到 底 做 的是 什 麼 :
    function TApplication.ProcessMessage: Boolean; var Handled: Boolean; Msg: TMsg; begin Result := False; if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then begin Result := True; if Msg.Message <> WM_QUIT then begin Handled := False; if Assigned(FOnMessage) then FOnMessage(Msg, Handled); if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then begin TranslateMessage(Msg); DispatchMessage(Msg); end; end else FTerminate := True; end; end;
由4808 行 可 以 看 到TApplication 類 別 使 用PeekMessage 函 式 從 訊 息佇 列 裡 取 得 訊 息 , 之 後 先 檢 查 訊 息 是 否 為WM_QUIT , 若 是 則把Terminated 旗 標 設 定 為true , 讓 應 用 程 式 給 中 斷 。 但 若 訊息 並 非WM_QUIT , 會 先 檢 查 是 否 有 指 定OnMessage 函 式 , 若 有 則優 先 把 訊 息 交 給 所 指 定 的OnMessage 函 式 處 理 , 若 無 指 定 則開 始 將 訊 息 分 配 。 因 此 若 由 所 指 定 的OnMessage 函 式 來 接 收 並 訊 息 , 那 麼 我們 所 收 到 的 訊 息 應 該 就 是 第 一 手 訊 息 , 那 麼 我 們 的 處 理函 式 也 就 是 最 快 速 處 理 的 囉 ! 先 看 看OnMessage 的 原 始 定 義:
void __fastcall ApplicationOnMessage(tagMSG &Msg, bool &Handled)
若 你 不 希 望VCL 繼 續 處 理 這 個 訊 息 的 話 , 只 需 把Handled 這個 旗 標 給 設 定 成true 即 可 。 就 讓 筆 者 舉 個 簡 單 例 子 :
    void __fastcall TForm1::FormCreate(TObject *Sender) { Application->OnMessage = ApplicationOnMessage; } //-------------------------------------------------------------- void __fastcall TForm1::ApplicationOnMessage(tagMSG &Msg, bool &Handled) { switch(Msg.message) { case WM_COPYDATA: TMessage Message; Message.Msg = Msg.message; Message.WParam = Msg.wParam; Message.LParam = Msg.lParam; OnCopyData(Message);// 處 理WM_COPYDATA 的 函 式 Handled = true; break; } } //--------------------------------------------------------------
這 樣 一 來 這 個 範 例 的 功 能 就 與 先 前 兩 個 方 法 『 覆 載TObject::Dispatch 』 與 『 覆 載TControl::WndProc 』 所 得 到 的 結 果 相 同 。 但 是 有一 點 要 注 意 的 是 , 指 定OnMessage 的 方 法 是 直 接 由TApplication 類 別 來 處 理 , 所 以 若 使 用 指 定OnMessage 函 式 來 接 收 處 理 特定 的 視 窗 訊 息 , 則 發 出 訊 息 端 必 須 發 給Application 這 個 全域 物 件 的 視 窗 代 碼 , 為Application->Handle , 而 若 使 用 其 餘 兩個 方 法 則 要 發 給 所 要 處 理 的 視 窗 的 視 窗 代 碼 , 如 :Form1->Handle 。 以 先 前 建 立 的 那 個 什 麼 都 沒 做 的 應 用 程 式 為 例 , 使 用FindWindow 方 式 來 取 得 視 窗 代 碼 有 兩 種 方 法 : A 、 用FindWindow("TApplication","Project1");
來 取 得Application->Handle 。
B 、 用FindWindow("TForm1","Form1");
來 取 得Form1->Handle 。
    VCL 視 窗 訊 息 的 流 動 方 向
由 以 上 三 種 訊 息 的 接 收 方 法 以 及VCL 的 原 始 碼 追 蹤 經 驗 ,我 們 可 以 勾 勒 出 在VCL 所 架 構 下 的 應 用 程 式 中 視 窗 訊 息 流動 的 程 序 ( 圖 五 ) 。 當 程 式 執 行 到Application->Run() 時 就 進 入 到 訊 息 處 理 迴 圈裡 , 我 們 的 應 用 程 式 透 過PeekMessage 這 個API 函 式 由 訊 息 佇列 裡 取 得 發 給 本 應 用 程 式 的 訊 息 , 並 將 此 訊 息 從 訊 息 佇列 裡 移 除 , 之 後 把 訊 息 交 給TApplication::ProcessMessage() 來 將訊 息 分 派 給 該 收 到 的 物 件 上 。 但 若 指 定 了TApplication 類 別裡 的OnMessage 事 件 時 , 會 先 將 訊 息 引 導 到 指 定 的OnMessage 函式 上 , 經 過OnMessage 函 式 處 理 過 後 的 訊 息 才 流 到 訊 息 迴 圈裡 。 訊 息 經 過TApplication::ProcessMessage() 的 分 派 後 , 訊 息 會先 流 到StdWndProc , 而StdWndProc 的 工 作 就 是 將 訊 息 推 進 到WndProc 函 式 裡 做 處 理 , 進 入 到WndProc 函 式 後 被WndProc 所 認 得 的 就優 先 被 處 理 , 最 後 不 認 得 再 交 由 覆 載 的Dispatch 函 式 處 理, 若 仍 無 法 處 理 則 交 給 父 類 別 的Dispatch 做 處 理 , 而Dispatch 函 式 最 後 會 把 訊 息 推 動 到DefaultHandler 函 式 裡 去 處 理 。 嗯! 又 學 到 了 一 點 , 其 實 也 可 以 重 新 覆 載DefaultHandler 來 處理 訊 息 , 但 這 屬 於 訊 息 接 收 最 後 的 底 限 , 要 從 這 裡 才 接收 嗎 ? 隨 便 你 囉 !
根 據 這 張 訊 息 流 通 圖 來 看 , 這 三 種 方 法 所 接 收 到 的 訊息 先 後 為 :
    TApplication::OnMessage 覆 載 的WinProc 覆 載 的Dispatch
那 讀 者 們 要 用 哪 一 張 網 子 來 網 住 訊 息 呢 ? 就 看 讀 者 們 的喜 好 為 何 囉 !
    親 手 打 造TraceWin
現 在 我 們 終 於 懂 得 原 來VCL 裡 有 這 般 的 程 序 將 訊 息 傳 遞 到他 應 該 到 達 的 地 方 。 了 解 了 這 些 之 後 , 應 該 是 該 始 動 手來 實 作 咱 們 所 要 的TraceWin 。 1 、 訊 息 發 出 端
先 來 設 計 一 個 簡 單 板 的 訊 息 發 出 端 , 放 置 兩 個Button 元件 以 及 一 個Edit 元 件 到Form1 上 面 , 其 擺 設 如 圖 六 。
為Clear 按 鈕 加 上OnClick 事 件 , 用 來 清 除Edit1 元 件 上 的 文字 :
    void __fastcall TForm1::ClearBtnClick(TObject *Sender) { Edit1->Text = “ ”;// 清 除Edit1->Text 的 內 容 }
為Send 按 鈕 加 上OnClick 事 件 , 將 訊 息 送 出 :
    void __fastcall TForm1::SendBtnClick(TObject *Sender) { char Msg[255]; sprintf(Msg,"%s",Edit1->Text.c_str()); COPYDATASTRUCT *pcp = new COPYDATASTRUCT; pcp->dwData = 0; pcp->cbData = sizeof(Msg); pcp->lpData = &Msg; SendMessage(FindWindow("TForm1","TRACE Window"), WM_COPYDATA, (WPARAM)NULL, (LPARAM)pcp); delete pcp; }
其 中9 至10 行 的SendMessage 特 別 指 明 要 將 訊 息 送 給 視 窗 標 題為Trace Window 且 登 錄 的 視 窗 類 別 為TForm1 的 視 窗 , 則 表 示 訊息 接 收 端 必 須 使 用 『 覆 載TObject::Dispatch 函 式 』 或 『 覆 載TControl::WndProc 函 式 』 才 能 夠 收 得 到 訊 息 。 若 要 改 成 由 『 指 定Application::OnMessage 函 式 』 的 方 法 則 必 須 將9 至10 行 的SendMessage 改 為 :
    SendMessage(FindWindow("TApplication","TRACE Window"), WM_COPYDATA, (WPARAM)NULL, (LPARAM)pcp);
還 有 一 個 地 方 值 得 注 意 的 是 : 先 前 說 過 使 用WM_COPYDATA 時,SendMessage 的 第 三 個 參 數 必 須 是 發 出 端 的 視 窗 代 碼 , 但由 於 我 們 在 將 所 需 檢 查 的 訊 息 丟 給TraceWin 時 , 並 不 需 要TraceWin 對 發 出 端 有 所 回 應 , 所 以 在 此 並 不 一 定 要 將 發 出 端 的 視窗 代 碼 處 給TraceWin 那 一 端 收 到 。 2 、 訊 息 接 收 端 ─ TraceWin :
OK! 剛 剛 已 經 把 簡 單 版 本 的 訊 息 發 出 端 給 設 置 好 , 接 下來 就 是 要 把 接 收 端 給 搞 定 了 。 先 做 一 個 簡 單 的 接 收 端 來接 收 由 發 出 端 所 發 出 的 文 字 訊 息 並 秀 出 來 , 所 以 在Form1 上 放 置 一 個Memo ( 如 圖 七 ) , 且 設 定Memo 的Align 屬 性 為alClient , 並 把 其 中Lines 的 內 容 給 刪 除 。 但 是 要 記 住 , 我 們 先 得把Form1 的Caption 給 設 定 成 『TRACE Window 』 讓 發 出 端 能 夠 正 確使 用FindWindow 來 取 得 接 收 端 的 視 窗 代 碼 。 另 外 為 了 順 便 是一 下 這 三 種 接 收 訊 息 的 方 法 , 也 要 記 得 把 到Project Option 裡 把Application 的Title 給 改 成 『TRACE Window 』 ( 如 圖 八 ) , 或是 在Project1.cpp 裡 頭 加 上 指 定Application->Title 的 程 式 碼 , 如下 :
    Application->Initialize(); Application->Title = “TRACE Window ”; Application->CreateForm(__classid(TForm1), &Form1);
先 為Form1 建 立OnCopyData 函 式 以 供 處 理 訊 息 用 : 在Unit1.h 裡 加 入 :
private: // User declarations
void __fastcall OnCopyData(TMessage &Msg);
在Unit1.cpp 裡 加 入 :
    void __fastcall TForm1::OnCopyData(TMessage &Msg) { PCOPYDATASTRUCT pcp; pcp = (PCOPYDATASTRUCT)Msg.LParam; char* TraceMsg; TraceMsg = (char*)pcp->lpData; Memo1->Lines->Add(TraceMsg);// 將 接 收 到 的 陣 列 一 行 一 行加 到 給 到Memo1 裡 頭 }
再 來 就 是 選 擇 咱 們 要 的 訊 息 接 收 方 式 了 , 為 了 複 習 上 頭所 說 過 的 三 種 方 法 三 種 方 式 都 試 試 看 好 了 : A 、 『 覆 載Dispatch 函 式 』
在unit1.h 裡 加 入 :
    public: // User declarations BEGIN_MESSAGE_MAP VCL_MESSAGE_HANDLER(WM_COPYDATA,TMessage,OnCopyData) END_MESSAGE_MAP(TForm)
這 樣 即 可 配 合 前 頭 的 那 個OnCopyData 函 式 來 處 理 訊 息 了 。 B 、 『 覆 載WndProc 函 式 』
在unit1.h 裡 加 入 :
    protected: // User declarations void __fastcall WndProc(TMessage &Message);
在unit1.cpp 裡 加 入 :
    void __fastcall TForm1::WndProc(TMessage &Message) { switch(Message.Msg) { case WM_COPYDATA: OnCopyData(Message);// 處 理WM_COPYDATA 的 函 式 break; } TForm::WndProc(Message); }
這 也 一 樣 可 配 合 前 頭 的 那 個OnCopyData 函 式 來 處 理 訊 息 了 。 C 、 『 指 定OnMessage 函 式 』
在unit1.h 裡 加 入 :
    protected: // User declarations void __fastcall ApplicationOnMessage(tagMSG &Msg, bool &Handled);
在unit1.cpp 裡 加 入 :
    void __fastcall TForm1::FormCreate(TObject *Sender) { Application->OnMessage = ApplicationOnMessage; } //-------------------------------------------------------------- void __fastcall TForm1::ApplicationOnMessage(tagMSG &Msg, bool &Handled) { switch(Msg.message) { case WM_COPYDATA:
    TMessage Message; Message.Msg = Msg.message; Message.WParam = Msg.wParam; Message.LParam = Msg.lParam;
    OnCopyData(Message);// 處 理WM_COPYDATA 的 函 式 Handled = true; break; } } //--------------------------------------------------------------
這 也 一 樣 可 配 合 前 頭 的 那 個OnCopyData 函 式 來 處 理 訊 息 了 。 不 過 誠 如 先 前 所 提 到 的 , 採 用 這 種 做 法 相 對 的 在 訊 息發 出 端 就 得 把 訊 息 送 給Application 物 件 而 不 是 送 給Form1 。 發出 端 的FindWindow 應 該 是 ;FindWindow("TApplication","TRACE Window") , 這 樣 才 能 夠 取 得Application->Handle 。 否 則 若 把 訊 息 直 接 發給Application 而 你 在Form1 上 頭 攔 截 訊 息 是 絕 對 攔 截 不 到 的 ,反 之 亦 然 。
3 、 訊 息 收 發 測 試 : OK ! 已 經 完 成 了 大 半 , 先 來 測 試一 下 吧 ( 圖 九 ) :
可 以 清 楚 的 看 到 , 我 們 可 已 經Edit1 元 件 裡 頭 的 文 字 完整 的 傳 送 到TRACE Window 裡 頭 去 , 但 這 樣 就 完 成 了 嗎 ? 那 可不 ! 還 有 很 多 地 方 應 該 加 強 或 修 正 的 :
    TRACE Window 的 視 窗 類 別 修 改
先 前 我 們 使 用FindWindow 來 取 得 視 窗 代 碼 時 使 用FindWindow( “TForm1 ”, ”TRACE Window ”) 。 但C++Builder 所 預 設 的 視 窗 註 冊 類 別 為TForm1 , 我 們 一 樣 使 用TForm1 似 乎 有 點 怪 怪 的 ! 筆 者 的 習 慣 是 改為TMainFrom , 但 怎 麼 改 呢 ? 從 物 件 檢 視 器 (Object Inspector ) 上 頭 改 就 可 以 了 , 找 到原 先TForm1 的Name 屬 性 欄 把 他 由Form1 改 為MainForm 則 繼 承 至TForm 類 別 的TForm1 類 別 則 會 變 為TMainForm 類 別 ( 圖 十 ) 。
也 因 此 原 先 所 使 用 的FindWindow 要 改 成FindWindow( “TMainForm ”, ”TRACE Window ”) 。
B 、TRACE Window 的 功 能 加 強
嗯 , 改 好 了 視 窗 類 別 後 還 缺 什 麼 呢 ? 缺 的 可 多 了 ! 如把Memo1 元 件 上 的 文 字 給 存 檔 起 來 以 便 日 後 觀 看 、 清 除Memo1 元 件 上 的 內 容 或 是 增 加 進 制 轉 換 等 等 的 功 能 , 說 都 說 不完 。 這 些 都 是 應 該 且 必 須 的 , 該 怎 麼 做 呢 ? 這 些 功 能 筆者 在 此 僅 舉 幾 個 簡 單 的 說 明 , 不 再 詳 述 :
a 、Memo1 元 件 上 的 文 字 存 檔 : 在VCL 架 構 中 , 所 有 的VCL 元件 都 被 包 裝 的 非 常 完 整 , 所 以 做 起 例 行 工 作 來 非 常 的 容易 , 要 將Memo1 元 件 上 的 資 料 存 檔 只 需 如 下 :
Memo1->Lines->SaveToFile("D:\\trace.txt");
這 樣 就 會 把 資 料 存 到D:\trace.txt 裡 頭 了 。
不 過 這 樣 似 乎 有 點 太 過 簡 單 , 咱 們 再 加 上 一 個 存 檔 對話 盒 吧 :
    TSaveDialog *SD = new TSaveDialog(this); SD->Options <<> SD->Filter = "Text files (*.txt)|*.TXT"; if (SD->Execute()) { Memo1->Lines->SaveToFile(SD->FileName); } delete SD;
這 樣 一 來 , 存 檔 時 就 會 出 現 存 檔 對 話 盒 讓 你 選 擇 要 儲 存的 檔 案 位 置 了 。
b 、 清 除Memo1 上 的 文 字 : 清 除Memo1 上 的 文 字 筆 存 檔 更 容 易 , 只 需 使 用Clear() 這 個成 員 函 式 , 如 下 : Memo1->Clear();
這 樣 就 可 以 輕 輕 鬆 鬆 的 將Memo1 元 件 上 頭 的 文 字 給 清 除了 。
C 、 訊 息 發 出 端 的 指 令 簡 化
根 據 先 前 的 訊 息 發 出 端 範 例 來 看 , 我 們 使 用 特 定 的 型別 才 能 夠 轉 換 , 可 用 性 頗 低 , 且 還 要 打 那 麼 多 的 字 , 若每 次 要 使 用 時 還 得 打 那 麼 多 字 , 豈 不 累 人 , 因 此 咱 們 可以 把 他 編 成 通 用 函 式 , 等 需 要 用 到 時 只 需include 我 們 所 編寫 的 函 式 檔 案 即 可 , 我 把 函 式 名 稱 命 名 為dout 有Debug Out 的意 思 , 並 且 讓dout 能 夠 支 援Unicode string 、signal byte string 以及VCL 裡 頭 特 有 的AnsiString , 如 此 一 來 這 個 表 頭 檔 在Visual C++ 、Borland C++ 以 及Borland C++Builder 等Win32 平 台 上 的C++ 編 譯 器 都一 樣 可 以 使 用 , 當 然 了 對 於 一 些 特 定 的 類 別 如AnsiString 會用#define 這 些 前 置 指 令 將 他 隔 離 , 讓 不 認 得 的 編 譯 器 不 會編 譯 到 這 些 個 不 認 得 的 類 別 。
程 式 列 表 五 、dout.h
    ////////////////////////////////////////////////////////////////////// // // Easy Bug Tracer v 1.0.6 // @(#) dout.h 1.0.6, last edit: 09/15/98 09:45 // @(#) Copyright (C) 1998 Martin Hsiao (martins1@ms3.hinet.net) // @(#) Martin's WorkShop (http://insidebcb.copystar.com.tw) // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // ////////////////////////////////////////////////////////////////////// #ifndef __DOUT_H_ #define __DOUT_H_ #include #ifndef __STDIO_H #include #endif /* __STDIO_H */ #ifndef __WCHAR_H #include #endif /* __WCHAR_H */ #define SMSG(X) SendMessage(FindWindow("TMainForm","TRACE Window") ,\ WM_COPYDATA, (WPARAM)NULL,(LPARAM)X); #define DebugOut(X) \ { \ COPYDATASTRUCT *pcp = new COPYDATASTRUCT;\ pcp->dwData = 0;\ pcp->cbData = sizeof(X);\ pcp->lpData = &X;\ SMSG(pcp);\ delete pcp;\ } void douth(long Data)//for Tracing Hex Value { char TM[255]; sprintf(TM,"0x%x",Data); DebugOut(TM); } void dout(char* Data) { char TM[255]; sprintf(TM,"%s",Data); DebugOut(TM); } void dout(WCHAR* Data) //for Tracing Unicode string { char TM[255]; #ifdef DSTRING_H AnsiString AnsiData = AnsiString(Data); sprintf(TM,"%s",AnsiData.c_str()); #else sprintf(TM,"%ls",Data); #endif /* DSTRING_H */ DebugOut(TM); } #ifdef DSTRING_H void dout(AnsiString* Data)//for Tracing AnsiString BCB only { char TM[255]; sprintf(TM,"%s",Data->c_str()); DebugOut(TM); } void dout(AnsiString Data) //for Tracing AnsiString BCB only { char TM[255]; sprintf(TM,"%s",Data.c_str()); DebugOut(TM); } #endif /* DSTRING_H */ #endif
既 然 在C++Builder 下 可 以 使 用 , 那 們 我 們 就 一 同 造 福C++Builder 的 哥 兒 們Delphi 吧 ! 讓 這 個TRACE Window 一 樣 可 以 收 到 由Delphi 所 傳 送 出 來 的 訊 息 。 以 下 是 在Delphi 下 使 用 時 的 程 式 碼 :
程 式 列 表 六 、dout.pas
    ////////////////////////////////////////////////////////////////////// // // Easy Bug Tracer v 1.0.6 // @(#) dout.pas 1.0.6, last edit: 09/15/98 09:45 // @(#) Copyright (C) 1998 Martin Hsiao (martins1@ms3.hinet.net)
    // @(#) Martin's WorkShop (http://insidebcb.copystar.com.tw)
    // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // ////////////////////////////////////////////////////////////////////// unit douts; interface uses Windows, Messages; procedure dout(Data : pchar);overload; procedure dout(Data : String);overload; implementation procedure charout(Data : pchar);overload; var sSend : AnsiString; cdsData : TCopyDataStruct; begin sSend := Data; with cdsData do begin dwData := 0; cbData := Length(sSend) + 1; lpData := pchar(sSend); end; SendMessage(FindWindow('TMainFrame','TRACE Window'), WM_COPYDATA,WPARAM(nil),LPARAM(@cdsData)); end; procedure dout(Data : pchar);overload; begin charout(Data); end; procedure dout(Data : String);overload; begin charout(pchar(Data)); end; end.
對 於 使 用Win32 平 台 下C++ 編 譯 器 的 讀 者 們 , 可 以 把dout.h 給放 置 到 各 編 譯 器 的\include 目 錄 裡 頭 , 屆 時 要 用 到dout 指 令時 只 需 加 入dout.h 這 個 標 頭 檔 就 可 以 了 。 而 對 於Delphi 的 使用 者 來 說 , 把dout.pas 編 譯 後 , 把dout.dcu 給 放 置 到\lib 目 錄下 即 可 , 此 外 使 用 時 記 得use dout 喔 。 若 讀 者 們 懶 得 親 手打 這 麼 長 的 程 式 碼 的 話 ( 長 嗎 ?J) , 可 以 到 筆 者 的 網 站上 頭 抓 , 網 址 是http://insidebcb.copystar.com.tw 。 此 外 筆 者 所寫 的 多 功 能 版 本TRACE Window 也 放 在 網 站 讓 讀 者 們 下 載 。
    結 語
呼 ! 似 乎 做 了 一 個VCL 裡 頭 的 深 度 歷 險 , 夠 深 嗎 ? 一 點 也不 ! 其 實 在VCL 的 領 域 裡 , 這 只 挖 到 一 半 罷 了 , 更 深 入 的祕 密 , 更 高 階 的 技 巧 都 還 等 著 我 們 去 挖 掘 呢 ! 為 了 挖 掘出VCL 架 構 裡 的 奧 祕 , 文 章 內 列 出 了 不 少 使 用Object Pascal 所撰 寫 的VCL 原 始 碼 , 希 望 沒 有 把 讀 者 們 搞 得 暈 頭 轉 向 。 以目 前 的 情 況 來 看 , 下 一 個 版 本 的VCL 應 該 還 是 由Object Pascal 為 主 體 撰 寫 而 成 , 也 因 此 不 少 的C/C++ 好 手 們 都 因 為Object Pascal 而 裹 足 不 前 去 挖 掘 這 深 奧 的 祕 密 , 更 對VCL 的Object Pascal 出 身 血 統 而 打 抱 不 平 。 在 筆 者 使 用C++Builder 之 前 , 對 於Object Pascal 是 一 蹺 不 通 , 因 此 對 於Object Pascal 的 學 習 一 直 抱 著能 不 費 時 間 學 就 不 學 的 態 度 。 但 不 久 後 , 因 為 工 作 的 需要 得 以 開 始 鑽 研VCL 的 內 部 構 造 , 這 才 發 現VCL 的 內 部 是 如此 的 精 妙 , 對 於 慣 用C/C++ 的 我 來 說 何 嘗 不 是 一 個 學 習Object Pascal 的 機 會 呢 ? 因 此 , 在 此 呼 籲 各 位 讀 者 , 若 你 真 正 想要 把C++Builder 與VCL 架 構 給 學 好 , 該 是 自 己 開 始 研 讀VCL 的 原始 碼 並 學 習Object Pascal 的 好 時 候 了 。 註 一 、 與IPC 主 題 相 關 的 書 籍 有 :
Advanced Windows, Microsoft Press, Jeffrey Richter, Chapter 10.
Multithreading Application in Win32, Addison Wesley, Jim Beveridge & Robert Wiener, Chapter13.
Programming Windows 95, Microsoft Press, Charles Petzold, Chapert 17.
註 二 、 需 要 用 到 廣 播 的 訊 息 必 須 先 使 用RegisterWindowMessage 函 式 來 向 系 統 註 冊 。

沒有留言:

張貼留言