DDX/DDV工作內幕 (轉)

worldblog發表於2007-12-06
DDX/DDV工作內幕 (轉)[@more@]

 

DDX/DDV工作內幕   DDX(動態資料)和DDV(動態資料驗證)看起來好象是在對話方塊中某和 某成員變數之間建立連線,自動實現控制元件和變數之間的資料轉移.但這只是一個幻 覺.它的實際工作方式是這樣的:當你用ClassWizard把某變數和控制元件連線起來時 (透過Member Variables選項卡),它在資料對映中建立一個入口.實際上也就是 在對話方塊的DoData中新增一個入口函式(DoDataExchange函式是 Class Wizard產生和維護的函式).當你UpdateData(FALSE)時,MFC呼叫 DoDataExchange 函式,Class Wizard放於DoDataExchange中的實現程式碼將把 來自變數的資料複製到對應的控制元件.如果呼叫UpdateData(TRUE),MFC反過來把 資料複製回變數(並且可能同時進行資料驗證)   應該注意到,CDialog經常在OnInitDialog函式中呼叫UpdateData(FALSE), 這樣當對話方塊顯示時你的成員變數就會神秘的出現在對話方塊中.同樣OnOK函式也 呼叫UpdateData,但引數是TRUE.這樣模態對話方塊看起來自己處理自己了.你可以 編寫類似下面的程式碼: CNameDlg dlg; dlg.m_name=&quot;New Name&quot;; if ( dlg.odal() == IDOK ) MessageBox(dlg.m_name,&quot;Greetings&quot;); 來實現模態對話方塊的自動處理.   下面考慮一下使用非模態對話方塊的情況吧.對話方塊仍處理OnInitDialog訊息, 因此資料初始傳輸正常.但是非模態對話方塊一般不等到按OK按鈕來處理它們的數 據,這就意味這我們必須自己處理資料傳輸,具體請看『DDX』.   好了,現在來看看DDV動態資料驗證.你在使用DDX動態資料交換的同時,也可 以使用資料驗證.典型的,驗證可以保證一個字串的字元數小於給定的數目,或 者數字在一定範圍之內.   不過資料驗證通常並不能滿足我們的期望,這是因為驗證只在控制元件到變數的 資料傳輸時才發生.這通常意味著在輸入了所有資料,單擊OK,然後就收到 一個錯誤訊息. 不過我們可以改進一下,使用『現場資料驗證』. 改進DDX/DDV 快速DDX   有幾種情況我們需要使用快速DDX,比如你編寫一個電子,使用者在對 話框中輸入名稱和地址,你需要一個按鈕使應用程式可以在使用者輸入完之後得到 郵件的名稱和地址.或者考慮一下模態對話方塊的&quot;應用&quot;按鈕的實現吧.   當然要實現上面的任務,我們可以直接呼叫GetDlgItemText獲取編輯框資料, 但為什麼不使用DDX呢?這樣至少可以使對話方塊看起來有點自動化.可以呼叫 UpdateData(TRUE)把資料傳送到變數,反過來填充地址時可以呼叫UpdateData(FALSE).   要想得到每個控制元件的狀態以確定何時需要進行資料交換,可以過載CDialog類 的OnCommand函式,因為一般的傳統控制元件都用WM_COMMAND訊息來提示狀態的改變, 當然對於使用WM_NOTIFY訊息的新型控制元件可以一樣的處理OnNotify函式.下面是 使用該技術的一個簡單例子,當你在對話方塊中輸入資料的同時主視窗中資料也進 行相應的改變. 程式清單:快速DDX // LiveDialog.cpp : implementation file // #include &quot;stdafx.h&quot; #include &quot;Custom.h&quot; #include &quot;LiveDialog.h&quot; #ifdef _DE #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CLiveDialog dialog CLiveDialog::CLiveDialog(CWnd* pParent /*=NULL*/) : CDialog(CLiveDialog::IDD, pParent) { //{{AFX_DATA_INIT(CLiveDialog) m_e = _T(&quot;&quot;); m_name = _T(&quot;&quot;); //}}AFX_DATA_INIT m_pView=NULL; // 應用程式視窗視的指標 } void CLiveDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CLiveDialog) DDX_Text(pDX, IDC_, m_email); DDX_Text(pDX, IDC_NAME, m_name); //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CLiveDialog, CDialog) //{{AFX_MSG_MAP(CLiveDialog) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CLiveDialog message handlers BOOL CLiveDialog::OnCommand(WPARAM wParam, LPARAM lParam) { BOOL fOk= CDialog::OnCommand(wParam, lParam); // Don't do if this command destroyed us or we are initializing if ( ::IsWindow(m_hWnd) &amp;&amp; !in_init ) { UpdateData(); // Update on any change ASSERT(m_pView !=NULL ); m_pView-&gt;GetDocument()-&gt;UpdateAllViews(NULL); } return fOk; } BOOL CLiveDialog::OnInitDialog() { in_init=TRUE; // 正在初始化 CDialog::OnInitDialog(); in_init=FALSE; // 初始化結束 return TRUE; } 在視窗視類中用下面的方法呼叫對話方塊: void CLiveView::OnGo() { m_dlg.m_pView=this; // 設定m_dlg.m_pView指向當前視 m_dlg.DoModal(); } 有關該程式還有幾個細節要注意: 1.WM_COMMAND訊息有破壞對話方塊,並使之無效的機會,那就是為什麼在 ::IsWindow(m_hWnd)返回FALSE時不呼叫UpdateData的原因. 2.如果你在該段程式碼的某部分呼叫UpDateData(FALSE)時,控制元件可能在那時啟用 命令訊息,這樣在你遞迴呼叫UpDateDate(FALSE)時將產生一個斷言.也就是說, 在你呼叫UpDateDate之前必須確定你不是正在資料,這就是為什麼在 OnInitDialog函式中設定in_ini標誌的原因(OnInitDialog中呼叫了 UpDateDate(FALSE)),同樣在其它任何呼叫UpDateDate的地方都要這樣處理. 3.主視必須知道何時需要更新,在這個例子中先儲存指向主視的指標,然後在某種 事情發生時呼叫主視的文件的UpdateAllView實現視的更新.你可能說我們可以找 到對話方塊的父視窗而不需要事先儲存主視指標,但這是無法實現的,因為對話方塊的 父視窗永遠不可能是視(對話方塊的父視窗必須是頂級視窗). 現場資料驗證   實現DDV資料驗證的函式其實就是DoDataExchange中以DDV_開頭的函式, 如DDV_MinMaxInt()用來驗證整數的範圍、DDV_MaxChars用來驗證字串的字 符個數,呼叫UpdateData()函式就可以引起這些函式的了。   要想改進資料驗證,比如你想在資料輸入框中資料的改變時驗證大小是否 合適而不是等到按OK之後得到一個錯誤對話方塊(就象DDV_MaxChars一樣在資料 改變的同時進行字串個數的驗證),我們可以過載對話方塊的OnCommand函式以 截獲所有的命令訊息,從中篩選(提取wParam的高位字)符合要求的訊息(比如 EN_CHANGE就意味著編輯框中的內容發生了改變),這裡也就是我們驗證域中值 的好時機。   還有一個問題是我們並不想驗證所有的東西,這有幾種方法可以解決, 比如手工更改資料對映: 第一步,新增一個成員變數UINT m_vid,在建構函式中把它置為0(當為0時執行 常規的延遲資料驗證),在OnCommand中儲存當前要驗證的控制元件ID(提取wParam的 底位字). BOOL CAboutDlg::OnCommand(WPARAM wParam,LPARAM lParam) { if ( HI(wParam) == EN_CHANGE &amp;&amp; !m_fIsUpdating ) // 檢驗標誌,是否正在進行資料驗證或更新 { UpdateData(); m_vid=0; } return CDialog::OnCommand(wParam,lParam); } 第二步,修改資料對映。把DoDataExchange中的所有資料對映移出Class Wizard 的特別註釋,並按下面的方法修改程式碼: void CLiveDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CAboutDlg) //}}AFX_DATA_MAP m_fIsUpdating=TRUE; // 是否正在進行資料傳遞或驗證的標誌,設定它 if ( !m_vid ||m_vid==IDC_LOG ) { DDX_Text(pDX, IDC_LOG, m_log); DDV_MaxChars(pDX, m_log, 10); } if ( !m_vid || m_vid ==IDC_NUM ) { DDX_Text(pDX, IDC_NUM, m_num); DDV_MinMaxInt(pDX, m_num, -10, 10); } m_fIsUpdating=FALSE; // 清除標誌 }   這裡我們還會碰到前一個例子中的第2個問題,也就是你在驗證一個特殊的 域時,必須確信你沒有正在驗證,不然你會得到一個斷言。當然可以採用和上面 的例子相似的辦法,即在進行資料驗證之前先設定m_fIsUpdating=TRUE;(正在進 行資料傳遞或驗證),驗證完之後再設為FALSE,在呼叫UpdateData之前判斷該標誌.   不過這裡會有一個小小的問題,如果資料驗證失敗,MFC會發出一個異常,以放 棄DoDataExchange,這樣你設定的標誌就不靈了.最簡單的解決辦法是在 UpdateData呼叫之後重新把m_fIsUpdating標誌設為FALSE(必須是所有的 UpdateData的呼叫之後,當然包括MFC內部程式碼中的呼叫,如 CDialog::OnInitdialog中),另一種方法是捕獲該異常,然後清除標誌,具體請 看下面的程式碼: void CLiveDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CAboutDlg) //}}AFX_DATA_MAP m_fIsUpdating=TRUE; // 是否正在進行資料傳遞或驗證的標誌,設定它 try { if ( !m_vid ||m_vid==IDC_LOG ) { DDX_Text(pDX, IDC_LOG, m_log); DDV_MaxChars(pDX, m_log, 10); } if ( !m_vid || m_vid ==IDC_NUM ) { DDX_Text(pDX, IDC_NUM, m_num); DDV_MinMaxInt(pDX, m_num, -10, 10); } m_fIsUpdating=FALSE; // 清除標誌 } catch (...) // 捕獲異常 { m_fIsUpdating=FALSE; // 清除標誌 throw; // 丟擲異常 } } 還有一種方法是參考MFC原碼寫的,在OnCommand中做如下修改: BOOL CAboutDlg::OnCommand(WPARAM wParam,LPARAM lParam) { if ( HIWORD(wParam) == EN_CHANGE &amp;&amp; !m_fIsUpdating ) // prevent control notifications from being dispatched during UpdateData { m_vid=LOWORD(wParam); _AFX_THREAD_STATE* pThreadState = AfxGetThreadState(); HWND hWndOldLockout = pThreadState-&gt;m_hLockoutNotifyWindow; if (hWndOldLockout != m_hWnd) // must not recurse UpdateData(); m_vid=0; } return CDialog::OnCommand(wParam,lParam); } 關於資料對映 應該認識到,所謂的資料對映只是一個函式,它展現了許多可能性(也就是可擴 展性),另外你還可以定製自己的資料驗證,比如你想定製一個郵政編碼的資料驗 證.具體實現方法請看下面的定製DDX/DDV部分. 還有一點要注意到的,一定要在對應的DDX呼叫之後立即新增驗證程式碼,否則,當 驗證失敗時,你的程式或許不會正確識別哪個域有問題. 定製DDX/DDV   現在你可以嘗試編寫自己的資料交換和資料驗證過程了.你要知道交換和 驗證函式只是一些知道如何處理CDataExchange的全域性函式而已,沒有什 麼特殊的.   下面來看看具體做法:   對於資料交換,需要編寫一個帶有引數CDataExchange指標、一個控制元件ID和對 某變數引用的全域性函式,儘管可以不在函式前面新增DDX_字首,但是為了可以和 Class Wizard整合,最好忍住你的這種念頭(後面你會看到為什麼了).   在交換函式中,可以檢查CDataExchange指標,以瞭解你所須的細節.下面就 來看看CDataExchange類的成員 成員 描述 m_bSaveAndValidate 對應於你提供給UpdateData的引數,當為TRUE時資料從控制元件傳遞到變數 m_pDlgWnd 控制視窗或對話方塊的控制程式碼 PrepareCtrl(int nIDC) 呼叫該函式,以標識當前控制元件(如不是編輯框) PrepareEditCtrl(int nIDC) 呼叫該函式,以標識當前控制元件(如是編輯框) Fail() 產生一個對控制元件的驗證失敗(你可以在DDX或者DDV中呼叫該函式),丟擲一個異 常,破壞DoDataExchange函式的執行 一般的,編寫交換函式,,你首先要檢查m_bSaveAndValidate的值以確定資料 的傳遞方向,如果傳遞失敗,你有必要呼叫PrepareEditCtrl(適於編輯控制元件)或者 PrepareCtrl(適於所有控制元件),在做此呼叫之後,任何對Fail的呼叫將導致把焦 點交回給該控制元件,即使其它過程(比如一起的驗證過程)釋出同樣的失敗,也是這樣 編寫一個驗證函式,跟編寫一個交換函式差不多,差別只是引數的不同.函式 以DDV_為字首,引數可以接受一個CDataExchange指標、合適型別的一個只值、 一個或兩個引數.   它的工作很簡單,如果m_bSaveAndValidate為TRUE時,一定要保證值是合法 的(通常認為從程式傳遞到控制元件的值是正確的).如果資料正常,則從該函式返回, 如果資料不正常,則呼叫Fail函式.前一個資料交換函式已經標識了當前操作的 是哪個控制元件(這也就是為什麼必須要在對應的DDX呼叫之後立即新增DDV的驗證代 碼的原因了).   下面的例子演示瞭如何定製自己的資料驗證: 程式清單:使用定製的DDX/DDV // validView.cpp : implementation of the CValidView class // #include &quot;stdafx.h&quot; #include &quot;valid.h&quot; typedef float Currency; // used for DDV #include &quot;validDoc.h&quot; #include &quot;validView.h&quot; #include &quot;customdd.h&quot; #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CValidView IMPLEMENT_DYNCREATE(CValidView, CFormView) // Class Wizard won't put this here because it thinks // Dialog boxes handle OnOK. They do, but this is a // foview, not a dialog box BEGIN_MESSAGE_MAP(CValidView, CFormView) //{{AFX_MSG_MAP(CValidView) ON_COMMAND(IDOK,OnOK) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CValidView construction/destruction CValidView::CValidView() : CFormView(CValidView::IDD) { validating=FALSE; vid=0; //{{AFX_DATA_INIT(CValidView) m_age = 18; m_name = _T(&quot;&quot;); m_wager = 1.0; m_btnenable = TRUE; //}}AFX_DATA_INIT // TODO: add construction code here } CValidView::~CValidView() { } void CValidView::DoDataExchange(CDataExchange* pDX) { CFormView::DoDataExchange(pDX); //{{AFX_DATA_MAP(CValidView) DDX_Text(pDX, IDC_AGE, m_age); DDV_MinMaxInt(pDX, m_age, 18, 150); DDX_Text(pDX, IDC_NAME, m_name); DDV_MaxChars(pDX, m_name, 64); DDX_Text(pDX, IDC_WAGER, m_wager); DDV_MinMaxCurrency(pDX, m_wager, 1.f, 100.f); DDX_EnableWindow(pDX, IDOK, m_btnenable); //}}AFX_DATA_MAP } BOOL CValidView::PreCreateWindow(CREATESTRUCT&amp; cs) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs return CFormView::PreCreateWindow(cs); } ///////////////////////////////////////////////////////////////////////////// // CValidView diagnostics #ifdef _DEBUG void CValidView::AssertValid() const { CFormView::AssertValid(); } void CValidView::Dump(CDumpContext&amp; dc) const { CFormView::Dump(dc); } CValidDoc* CValidView::GetDocument() // non-debug version is inline { ASSERT(m_pDocument-&gt;IsKindOf(RUNTIME_CLASS(CValidDoc))); return (CValidDoc*)m_pDocument; } #endif //_DEBUG ///////////////////////////////////////////////////////////////////////////// // CValidView message handlers void CValidView::OnOK() { if (UpdateData(TRUE)) { MessageBox(&quot;Wager placed&quot;); m_btnenable=FALSE; UpdateData(FALSE); } } 程式清單:定製的DDX/DDV過程 #include <stdafx> #include &quot;customdd.h&quot; // Custom Exchange void DDX_EnableWindow(CDataExchange *pDX, int id, BOOL &amp;flag) { CWnd *ctl=pDX-&gt;m_pDlgWnd-&gt;GetDlgItem(id); if (pDX-&gt;m_bSaveAndValidate) flag=ctl-&gt;IsWindowEnabled(); else ctl-&gt;EnableWindow(flag); } // Custom validator void DDV_MinMaxCurrency(CDataExchange *pDX, float val, float min, float max) { CWnd *editctl=CWnd::FromHandle(pDX-&gt;m_hWndLastControl); CString s; int n; if (pDX-&gt;m_bSaveAndValidate) { // Using math to dec if anything is left over is bad because of rounding // errors, so use a string method instead editctl-&gt;GetWindowText(s); n=s.Find('.'); if (n!=-1 &amp;&amp; n+3<s pdx-="">Fail(); } DDV_MinMaxFloat(pDX,val,min,max); // let the existing one do the job } } 與Class Wizard整合   如果你只想把某定製過程應用於一個專案的話,可以把它新增到該專案的 CLW.你也可以在包含mfcclwz.dll檔案的BIN目錄(我的是 ... CommonMSDev98Bin)下建立一個DDX.CLW檔案, 然後你的DDX過程就可以應用到所有專案了.   下面來看一看怎樣寫CLW檔案: 首先新增一個名為[ExtraDDX]的區段,看起來象INI檔案的區段,但是這裡的名 稱是區分大小寫的. [ExtraDDX] ExtraDDXCount=2 ExtraDDX1=E;;Value;Currency;0.0;Text;Floating Point Currency;MinMaxCurrency;Mi&amp;nimum;f;Ma&amp;ximum;f ExtraDDX2=bBECcRLIMNn;;Enable State;BOOL;TRUE;EnableWindow;Window Enabled Status 這些程式碼是什麼意思呢? 第二行 ExtraDDXCount=x 的x表示專案的數目(這裡是2),然後接下來的一行以ExtraDDX1=開頭,再下來是 ExtraDDX2=、ExtraDDX3=等等。等號右邊的程式碼被分成7個、10個或者12個域, 具體倚賴於你所想實現的目標,每個域以分號分隔,下面的表格列出了這些域的意思: 域 描述 1 DDX應用的空間型別(比如E=編輯框) 2 未使用 3 屬性型別(經常是值,對應著Class Wizard的第一個組合框) 4 變數的資料型別 5 初始值 6 沒有DDV_字首的DDV過程名 7 註釋 8 沒有DDV_字首的DDV過程名 9 第一個DDV引數的名稱(可選) 10 第一個DDV引數的型別(比如,f=float;可選) 11 第二個DDV引數的名稱(可選) 12 第二個DDV引數的型別(比如,f=float;可選)   你不必指定任何DDV過程,你可以把一個新的驗證過程與標準的交換函式混合 在一起(也就象上面ExtraDDX1那一行所做的一樣).注意,這些DDX和DDV函式名並 沒有以DDX_和DDV_開頭,但是Class Wizard確實往它產生的程式碼中新增了這些前 綴.這也就是用這些字首命名函式的原因了。 </s></stdafx>

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-988973/,如需轉載,請註明出處,否則將追究法律責任。

相關文章