Wolfgang Chien's Homepage | Delphi學習筆記 - 創作篇 |
前言
這兩天DelphiChat上有一些朋友在討論MDI父視窗與子視窗互相呼叫的問題,十分熱心, 內容也很精釆, 我簡要的整理一下這幾天對這個題目的討論,另外, 也花了很大的篇幅提出我的方案(SendMessage 訊息傳遞).
MDI 父子視窗的合作模式
對的,如同這幾天 favor 與 Jimmy 的方法,假如子視窗的類別是TfrmChild, 而 TryTrySee 是 TfrmChild public 出來的一個方法, 那主視窗要呼叫這個子視窗的方法時便可以用類似如下的作法:
if (ActiveMDIChild is TfrmChild) then TfrmChild(ActiveMDIChild).TryTrySee;
之所以必須轉型, 是因為 ActiveMDIChild 傳回的是 TForm, 一般的TForm並沒有 TryTrySee 方法, 不是嗎? 這個方法是 TfrmChild 繼承 TForm 後再才出現的, 但是, 回到現實來看 -- 畫面上目前作用中的就是TfrmChild型的子視窗啊, 也就是說當時的 ActiveMDIChild 所傳回的 TFrom 事實上就是參考到一個 TfrmChild 型的真實物件, 於是, 經過 if 判斷後, 程式寫明將 ActiveMDIChild 傳回值轉型成 TfrmChild, 就可以通過語法檢查.
好了, 如果是仿'記事本'將這支程式改成 MDI 的話, 以上的方法是很好用的, 因為子視窗的類別種類只有一種, 規格簡單單純; 然而,當專案中的子視窗類別不只一個(甚至很多個時), 該怎麼辦呢?
第一個念頭就是: 多加幾個 if 判斷, 例如:
uses ..., 子視窗的那幾個 Units; ... if (ActiveMDIChild is TfrmChild) then TfrmChild(ActiveMDIChild).TryTrySee; (* 繼續加其他判斷 *) if (ActiveMDIChild is TfrmAnotherChild) then TfrmAnotherChild(ActiveMDIChild).TryAnother; if ...
於是, 每併進來一個子視窗到主專案時, 主程式就要改一次 -- 是有點辛苦!聰明的人於是開始動腦筋了...
![]() |
- Jimmy 提出的, 應用 D2 form 繼承的觀念, 設個基底作為各個子視窗的祖先,在這個基底寫明各子視窗所需要的功能, 這樣, 子視窗只要從這個基底繼承, 就自然有了這些預先準備好的功能, 不足時,再 override 掉修改即可; 對主視窗而言呢? 只需判斷是不是這個基底即可, 簡便很多,呵呵! 用 D2 寫程式確實是舒服很多. |
![]() |
- Angus Lin剛提出來的 procedure point, 這個我還沒有上機試過, 之前也沒想過, 但 Jimmy jimmygan@mail.seeder.net.tw 試出來, 請大家直接參照該信. :) |
應用自定訊息搭起父子視窗的對話管道
應用上述的各種方法, 相信在規劃這類程式時就很有把握了, 呵呵! 我實在是太雞婆了, 再提出另一種作法: SendMessage
大綱是主視窗以 SendMessage() 這個 WinAPI 送出 wm_MainTellChid 這個事先定義好的使用者自訂訊息, 而子視窗就寫好一個訊息處理程序等著接收這個訊息, 根據伴隨訊息而來的 wParam, lParam 長短參數就可得知主視窗通知要作什麼事; 同樣的, 子視窗也以送出訊息(wm_ChildTellMain)的方式去通知主視窗一些事情.
整個過程中, 主視窗根本不需要知道子視窗的類別, 只管對目前作用中的子視窗發訊息即可, 子視窗如果懂得這個訊息, 自然會去呼叫自己相應的方法完成工作, 所以, 只要協定好, 程式可以獨立各管各的, 需要時再通知對方.
物件程式設計的一個觀念是摹擬現實世界, 想想看, 實際生活中, 我們是用訊息溝通的, 通常不會直接要求特定的方法, 在向某一家公司買東西時, 通常就是打電話過去, 說要訂什麼, 協議價格交運方式等, 有可能說: 「麻煩您啟動 T台灣寶藍.訂單系統 訂貸嗎? 」, 或者對另一家公司說: 「我要用T台灣微軟.特惠查詢」, 不會, 只會告訴接電話的人, 我想作什麼事? 要什麼? 至於他(她)會引用工作手冊第X章第X節的標準處理程序, 誰知道啊!
實例演練
這個想法可行嗎? 我有一個例子, 詳細解說起見, 步驟稍多, 但只要您願意,跟著演練一遍後, 一定會有心得. :)
以 Delphi 2 為例:
![]() |
1. File | New Project |
![]() |
2. 將 form1.FormStyle 設為 fsMDIForm |
![]() |
3. 在 form1 上放置 TPanel 一個, 設定其
Align = alBottom Alignment = taLeftJustify |
![]() |
4. 在 Panel1 中靠右邊一點放置一個 TButton, 其 OnClick 事件:
procedure TForm1.Button1Click(Sender: TObject); var Result: longint; begin (* 對目前作用中的子視窗發出訊息 *) Result := SendMessage(ActiveMDIChild.Handle, wm_MainTellChild, _ShowMeSomething, 0); end; |
![]() |
5. File | New Form 開一個全新空白的視窗, 預設應為 form2 並將 FormStyle 設為 fsMDIChild |
![]() |
6. 在 TForm2 的 private 節中加上procedure MainTellChild宣告:
TForm2 = class(TForm) ... private (* 預備接收 wm_MainTellChild 訊息 *) procedure MainTellChild(var Msg: TMessage); message wm_MainTellChild; ... |
![]() |
7. 在 Implement 節中加入 procedure MainTellChild 的實作:
implementation {$R *.DFM} procedure TForm2.MainTellChild(var Msg: TMessage); begin (* 根據短參數的值判斷服務項目 *) case Msg.wParam of _ShowMeSomething: ShowMessage('Ok! You got it.'); { ... 其他的服務項目 ... } end; Msg.Result := _OK; end; |
![]() |
8. 在 Form2 (子視窗) 放置 TEdit 與 TButton 各一, Button1 的 OnClick:
procedure TForm2.Button1Click(Sender: TObject); var sTellUser: string; szTellUser: array[0..254] of char; Result: longint; begin sTellUser := Edit1.Text; StrPCopy(szTellUser, sTellUser); (* 轉換成 Null-Term. 型的字串 *) (* 將字串的指標作為長參數的內容傳給父階視窗 *) Result := SendMessage(Application.MainForm.Handle, wm_ChildTellMain, _PassAString, longint(@szTellUser)); end; |
![]() |
9. 回到 Unit1 單元, 在 TForm1 的 private 節中加上 procedure ChildTellMain宣告:
type TForm1 = class(TForm) ... private procedure ChildTellMain(var Msg: TMessage); message wm_ChildTellMain; ... |
![]() |
10. 在 Implement 節中加入 procedure ChildTellMain 的實作, 如下:
procedure TForm1.ChildTellMain(var Msg: TMessage); var sChildSay: string; begin (* 根據短參數的值判斷服務項目, 長參數為額外資訊, 如本例指向字串的指標 *) case Msg.wParam of _PassAString: Panel1.Caption := StrPas(pchar(msg.lParam)); { ... 其他的服務項目 ... } end; Msg.Result := _OK; end; |
![]() |
11. File | New Unit, 用以下的程式取代原來的 unit3 程式:
unit Unit3; interface uses Messages; const (* 自訂訊息 *) wm_MainTellChild = wm_User + 1; wm_ChildTellMain = wm_user + 2; (* for wParam, 服務項目 *) _ShowMeSomething = 10; _PassAString = 20; (* 訊息反饋的值 *) _OK = 0; _FailDontRetry = -1; _NoAnswer = -2; implementation end. |
![]() |
12. 回到 unit1 與 unit2, 在 interface 的 uses 述句中都加入 unit3 |
![]() |
13. 專案存檔, 都用預設檔名存檔. |
![]() |
14. 執行. |
研討與應用
透過訊息的傳遞, 父子視窗的溝通管道就建立起來了, 以後, 只要針對短長參數的內容增加不同的服務項目與細節即可.
除了傳字串, 傳物件參考也是可以的喔:
(* -------------------------------------------------- *) (* 發送端 (* (* -------------------------------------------------- *) procedure TfrmSub.FormActivate(Sender: TObject); var lParam: longint; lResult: longint; begin lParam := longint(FControlTable); // 取得指向主控 Table 的參考(指標) lResult := SendMessage(Application.MainForm.Handle, wm_SubFormTellMainForm, id_TellDataSet, lParam); end; (* -------------------------------------------------- *) (* <接收端> (* (* -------------------------------------------------- *) procedure TfrmMain.MsgResponder(var Msg: TMessage); begin Msg.Result := id_Success; case Msg.wParam of // 子系統向主程式報告目前作用中的資料集 id_TellDataSet: begin dsrMain.DataSet := TDataSet(msg.lParam); // 接收端的處理 dnvMain.Enabled := True; if dsrMain.DataSet = nil then pnlDataSpeedBar.Visible := False else pnlDataSpeedBar.Visible := True; end; { ... } else Msg.Result := id_NoRespond; end; end; { TfrmMain.MsgResponder }
如果在D2, 情形就更好, 可以為子視窗類別前架一個基底, 將訊息的溝通處理內建在其中, 同時預留一些虛擬的方法給後代繼承改寫, 子視窗們就自然有了與主程式溝通的的能力, 後來發現要加上其他的'服務項目'時, 修改基底之後, 兒孫們就一同受惠了, 真是方便! 這也是我公司目前 AP Framwork的建構概念.
首頁 | 學習筆記 | 主題公園 | 軟體下載 | 關於本站 | 討論信群 | 相約下次 |