DDX/DDV工作內幕

轉自:http://dev.csdn.net/article/6/6291.shtm   



    DDX(動態數據交換)和DDV(動態數據驗證)看起來好象是在對話框中某控件和某成員變量之間建立連接,自動實現控件和變量之間的數據轉移.但這只是一個幻覺.它的實際工作方式是這樣的:當你用ClassWizard把某變量和控件連接起來時 (通過Member Variables選項卡),它在數據映射中創建一個入口.實際上也就是在對話框的DoDataExchange函數中添加一個入口函數(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="New Name";
if ( dlg.DoModal() == IDOK )
{
    MessageBox(dlg.m_name,"Greetings");
}

來實現模態對話框的自動處理.

    下面考慮一下使用非模態對話框的情況吧.對話框仍處理OnInitDialog消息, 因此數據初始傳輸正常.但是非模態對話框一般不等到按OK按鈕來處理它們的數據,這就意味這我們必須自己處理數據傳輸,具體請看『快速DDX』.

    好了,現在來看看DDV動態數據驗證.你在使用DDX動態數據交換的同時,也可以使用數據驗證.典型的,驗證可以保證一個字符串的字符數小於給定的數目,或者數字在一定範圍之內.
    不過數據驗證通常並不能滿足我們的期望,這是因爲驗證只在控件到變量的數據傳輸時才發生.這通常意味着用戶在輸入了所有數據,單擊OK,然後就收到一個錯誤消息. 不過我們可以改進一下,使用『現場數據驗證』

改進DDX/DDV
快速DDX
    有幾種情況我們需要使用快速DDX,比如你編寫一個電子郵件程序,用戶在對話框中輸入名稱和地址,你需要一個按鈕使應用程序可以在用戶輸入完之後得到郵件的名稱和地址.或者考慮一下模態對話框的"應用"按鈕的實現吧.
    當然要實現上面的任務,我們可以直接調用GetDlgItemText獲取編輯框數據, 但爲什麼不使用DDX呢?這樣至少可以使對話框看起來有點自動化.可以調用 UpdateData(TRUE)把數據傳送到變量,反過來填充地址時可以調用UpdateData(FALSE).
    要想得到每個控件的狀態以確定何時需要進行數據交換,可以重載CDialog類的OnCommand函數,因爲一般的傳統控件都用WM_COMMAND消息來提示狀態的改變, 當然對於使用WM_NOTIFY消息的新型控件可以一樣的處理OnNotify函數.下面是使用該技術的一個簡單例子,當你在對話框中輸入數據的同時主窗口中數據也進行相應的改變.

// LiveDialog.cpp : implementation file

#include "stdafx.h"
#include "Custom.h"
#include "LiveDialog.h"

#ifdef _DEBUG
#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_email = _T("");
      m_name = _T("");
     //}}AFX_DATA_INIT
     m_pView=NULL; // 應用程序窗口視的指針 }
void CLiveDialog::DoDataExchange(CDataExchange* pDX)
{
     CDialog::DoDataExchange(pDX);
     //{{AFX_DATA_MAP(CLiveDialog)
     DDX_Text(pDX, IDC_EMAIL, 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) && !in_init )
     {
          UpdateData(); // Update on any change
          ASSERT(m_pView !=NULL );
          m_pView->GetDocument()->UpdateAllViews(NULL);
     }
     return fOk;
}

BOOL CLiveDialog::OnInitDialog()
{
     in_init=TRUE; // 正在初始化
     CDialog::OnInitDialog();
     in_init=FALSE; // 初始化結束
     return TRUE;
}
[/code]
在窗口視類中用下面的方法調用對話框:
[code="c++"]
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 ( HIWORD(wParam) == EN_CHANGE
         &&  !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
         && !m_fIsUpdating ) // prevent control notifications from being dispatched during UpdateData
    {
        m_vid=LOWORD(wParam);
        _AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
        HWND hWndOldLockout = pThreadState->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 "stdafx.h"
#include "valid.h"

typedef float Currency; // used for DDV

#include "validDoc.h"
#include "validView.h"
#include "customdd.h"
#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
// form view, 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("");
    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, <B style='color:white;background-color:#00aa00'>6</B>4);
    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& 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& dc) const
{
    CFormView::Dump(dc);
}
 
CValidDoc* CValidView::GetDocument() // non-debug version is inline
{
    ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CValidDoc)));
    return (CValidDoc*)m_pDocument;
}
#endif //_DEBUG

///////////////////////////////////////////////////////////////////////////// // CValidView message handlers
void CValidView::OnOK()
{
    if (UpdateData(TRUE))
    {
        MessageBox("Wager placed");
        m_btnenable=FALSE;
        UpdateData(FALSE);
    }
}

程序清單:定製的DDX/DDV過程

#include <stdafx.h>
#include "customdd.h"
// Custom Exchange
void DDX_EnableWindow(CDataExchange *pDX, int id, BOOL &flag)
{
    CWnd *ctl=pDX->m_pDlgWnd->GetDlgItem(id);
    if (pDX->m_bSaveAndValidate)
        flag=ctl->IsWindowEnabled();
    else
        ctl->EnableWindow(flag);
    }
// Custom validator
void DDV_MinMaxCurrency(CDataExchange *pDX, float val, float min, float max)
{
    CWnd *editctl=CWnd::FromHandle(pDX->m_hWndLastControl);
    CString s;
    int n;
    if (pDX->m_bSaveAndValidate)
    {
        // Using math to decide if anything is left over is bad because of rounding         // errors, so use a string method instead
        editctl->GetWindowText(s);
        n=s.Find('.');
        if (n!=-1 && n+3<s.GetLength())
        {
            AfxMessageBox("Please enter the data to the nearest penny!");
            pDX->Fail();
        }
        DDV_MinMaxFloat(pDX,val,min,max);
        // let the existing one do the job
    }
}

與Class Wizard集成
    如果你只想把某定製過程應用於一個項目的話,可以把它添加到該項目的 CLW文件.你也可以在包含mfcclwz.dll文件的BIN目錄(我的是 .../Microsoft Visual Studio/Common/MSDev98/Bin)下創建一個DDX.CLW文件, 然後你的DDX過程就可以應用到所有項目了.
    下面來看一看怎樣寫CLW文件: 首先添加一個名爲[ExtraDDX]的區段,看起來象INI文件的區段,但是這裏的名稱是區分大小寫的.

[ExtraDDX]
ExtraDDXCount=2
ExtraDDX1=E;;Value;Currency;0.0;Text;Floating Point Currency;MinMaxCurrency;Mi&nimum;f;Ma&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 初始值 <B style='color:white;background-color:#00aa00'>
6</B>  沒有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確實往它產生的代碼中添加了這些前綴.這也就是用這些前綴命名函數的原因了。

發佈了12 篇原創文章 · 獲贊 13 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章