MFC對話框:逃跑按鈕、屬性表單、嚮導創建

一、基於對話框的程序

新建一個基於對話框的項目,項目名稱:DlgTest。

生成的項目結構爲:
解決方案結構:

類視圖下有三個類:

  • CAboutDlg
    派生於CDialog類,這個類與SDI應用程序中相應的類:CAboutDlg作用相同,用於顯示一個關於對話框。
  • CDlgTestApp
    這是MFC應用程序中必不可少的一個類,派生於CWinApp類,它的對象代表了應用程序本身。
  • CDlgTestDlg
    派生於CDialog類,基於對話框的MFC應用程序的主界面。

基於對話框的應用程序中沒有從CView類派生出來的視類,也沒有從CFrameWnd類派生出來的框架類,以及從CDocument類派生的文檔類,它只有從CDialog派生出來的一個對話框類:CTestDlg,這類應用程序的窗口就是一個對話框界面。
資源視圖:

運行該程序生成的界面:

二、“逃跑”的按鈕

實現功能:
  DlgTest程序的對話框主界面上增加一個按鈕,當用鼠標單擊這個按鈕時,該按鈕會自動移動到另一個位置,就像一個“逃跑”的按鈕。

✨ 1)首先刪除MFC AppWizard自動創建的對話框資源: IDD_DLGTEST_DIALOG上的所有控件。

✨✨2)然後添加一個按鈕控件,利用其屬性對話框,將其Caption修改爲:你能抓住我嗎?

打開IDD_DLGTEST_DIALOG對話框的屬性對話框,可以看到在其字體選項卡上,有一個Font按鈕:
✨✨✨3)爲了實現這種“逃跑”按鈕,可以通過捕獲鼠標移動的消息,並在此消息響應函數中讓這個按鈕的位置發生移動來實現。

   介紹一種巧妙的實現方法:在IDD_DLGTEST_DIALOG對話框資源窗口中,複製剛纔添加的那個按鈕,並在其下方進行粘貼操作,這樣IDD_DLGTEST_DIALOG對話框資源中就有了兩個外觀相同的按鈕。在程序實現時,首先讓其中的一個按鈕隱藏,另一個按鈕顯示;當隨後把鼠標移動到顯示的按鈕上時,將該按鈕隱藏,把另一個顯示出來。因爲這兩個按鈕的外觀是完全一樣的,因此這樣的效果給用戶的感覺好像按鈕是自動跑到新位置處的。

🔵🔵 3.1)在IDD_DLGTEST_DIALOG對話框資源窗口中,複製剛纔添加的那個按鈕。

爲了實現上述所述功能,程序首先就要捕獲鼠標移動消息,那麼由誰來捕獲這個消息比較合適呢?
  如果讓對話框窗口(CDlgTestDlg類)來捕獲,一旦鼠標在對話框窗口中祕動,程序就會讓按鈕上下移動,這當然不是想實現的功能。想要的功能是當鼠標移動到按鈕上時,按鈕才上下移動。鼠標移動的消息應該由按鈕窗口來捕獲。所以在MFC應用程序中,可以創建一個從CButton類派生的新類,然後將按鈕控件上這種新類型的成員變量相關聯,從而就把按鈕控件與一個自定義的按鈕窗口類關聯起來

🟢🟢 3.2)爲應用程序增加一個從CButton派生的新類,設置新類的名稱(Name)爲: CNewButton,基類爲: CButton。

接下來把對話框中的兩個按鈕分別關聯一個成員變量,關聯的變量類型爲CNewButton類型,即將變量的類型設置爲上面添加的新類。兩個Button按鈕的ID分別爲:

  • IDC_BUTTON1,設置變量名稱爲: m_btn1。
  • IDC_BUTTON2,設置變量名稱爲: m_btn2。


發現DlgTestDlg中有兩處代碼變化:

  • 第一處:頭文件DlgTestDlg.h中生成代碼:

    發現CNewButton處飄紅,是由於爲對話框的一個子控件關聯了一個成員變量,而這個成員變量的類型是CNewButton類,這個類是剛剛創建的新類。如果在CDlgTestDlg類中想要識別這種類型的話,就必須在CDlgTestDlg類中包含這個新類的頭文件

    點擊飄紅的CNewButton類,會給出解決方案。點擊給出解決方案就可解決問題。

    在CNewButton源文件中添加頭文件#include "pch.h",否則會報錯:錯誤C1010:在查找預編譯頭時遇到意外的文件結尾。是否忘記了向源中添加“#include “pch.h””?

  • 第二處:源文件DlgTestDlg.cpp中生成代碼:

    void CDlgTestDlg::DoDataExchange(CDataExchange* pDX)
    {
    	CDialogEx::DoDataExchange(pDX);
    
    	DDX_Control(pDX, IDC_BUTTON1, m_btn1);
    	DDX_Control(pDX, IDC_BUTTON2, m_btn2);
    }
    

🟣🟣3.3)CNewButton類捕獲鼠標移動消息。

打開類視圖的選項卡,在CNewButton上單擊鼠標右鍵,點擊屬性,在消息列表中找WM_MOUSEMOVE。點擊 OnMouseMove按鈕。

CNewButton的源文件的中BEGIN_MESSAGE_MAPEND_MESSAGE_MAP宏之間即添加了消息響應函數,並添加了消息響應函數的實現:

#include "CNewButton.h"
BEGIN_MESSAGE_MAP(CNewButton, CButton)
	ON_WM_MOUSEMOVE()
END_MESSAGE_MAP()
void CNewButton::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息處理程序代碼和/或調用默認值

	CButton::OnMouseMove(nFlags, point);
}

🟡🟡3.4)實現按鈕的隱藏功能。因爲當鼠標移動到該按鈕上時,就會由這個按鈕的鼠標移動消息的響應函數 OnMouseMove來響應,在此函數中以SW_HIDE參數去調用這個按鈕的ShowWindow函數,即可將其隱藏。

這時爲了讓另一個按鈕顯示出來,必須要知道另一個按鈕所關聯的那個對象的內存地址,然後才能調用該對象的ShowWindow函數,將其顯示出來
  爲了在一個按鈕對象中獲取另一個按鈕控件對象的地址,最簡單的方式就是在CNewButton類中定義一個成員變量,讓其指向另一個按鈕對象的地址

💦💦💦 3.4.1)因此爲CNewButton類再添加一個公開CNewButton* 類型的成員變量: m_pBtn

當用CNewButton類去實例化CDlgTestDlg類的成員變量 m_btn1m_btn2時,這兩個對象內部就都有了一個 m_pBtn成員變量,讓這兩個對象內部的 m_pBtn變量分別保存對方的首地址,相當於這兩個對象互相交換了自己的首地址。於是當m_btn1按鈕隱藏時,就可以利用它的成員變量 m_pBtn去調用ShowWinolw函數,將m_btn2按鈕顯示出來;同樣地,當m_btn2按鈕隱藏時,可以利用它的成員變量 m_pBtn去調用ShowWindow函數,將m_btn1按鈕顯示出來。

💦💦💦 3.4.2)在CDlgTestDlg類中把m_btn1m_btn2這兩個對象的首地址交換一下,這一工作可以放在OnInitDialog函數中實現。根據前面的知識,OnInitDialog函數就是 WM_INITDIALOG消息的響應函數,該消息是在對話框要顯示之前發送的。在CDlgTestDlg類的OnInitDialog函數的最後,但要在return語句之前添加:

m_btn1.m_pBtn = &m_btn2;
m btn2.m_pBtn = &m_btnl;

💦💦💦 3.4.3)然後在CNewButton類的OnMouseMove函數中,先讓對象自己隱藏起來,然後調用成員m_pBtn的ShowWindow函數將對方顯示出來:

void CNewButton::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息處理程序代碼和/或調用默認值
	//隱藏鼠標處的窗口
	ShowWindow(SW_HIDE);
	//顯示m_pBtn所指向的窗口
	m_pBtn->ShowWindow(SW_SHOW);

	CButton::OnMouseMove(nFlags, point);
}

當鼠標移動到第一個按鈕對象(m_btn1)上時,程序就會調用該對象的OnMouseMove函數。在這個函數中,首先調用ShowWindow函數將自身隱藏。因爲第一個按鈕對象的成員m_pBtn保存的是第二個按鈕對象m_btn2的地址,所以接下來的m_pBtn->ShowWindow (SW_SHOW)的調用就將第二個按鈕顯示了出來。當隨後鼠標移動到第二個按鈕對象上時,實現原理相同,是對m_bnt2對象來說,它的m_pBtn成員變量保存的是m_btn1按鈕的地址。

但是這個程序還有一個缺陷:初始顯示時,兩個按鈕都是顯示狀態,這很容易讓用戶看出程序的實現方式。
📋📋 解決:在初始時應該隱藏一個按鈕。利用按鈕屬性對話框,把第一個按鈕的Visible屬性去掉。這時只有一個按鈕處於顯示狀態,然後把鼠標移到這個按鈕上,這個按鈕就隱藏了,並顯示出另一個按鈕。再把鼠標移到這個顯示的按鈕上時,它又消失了,另一個又顯示出來。

✨✨✨✨ 4)利用SetWindowPos函數來設置按鈕在屏幕上移動的新位置

三、屬性表單和嚮導的創建

點擊VS的開發窗口中菜單項中的工具菜單命令,在選擇此菜單項下的子菜單選項。打開的對話框就是一個屬性表單,它的每一個選項卡就是一個屬性頁。一個屬性表單由一個或多個屬性頁組成。它有效解決了大量信息無法在一個對話框上顯示的問題,並提供了對信息的分類和組織管理功能。在程序設計中,可以將相關的選項放到一個屬性頁中。

現在重新建一個叫Prop的應用程序。項目樣式:MFC standard、應用程序類型:單個文檔。
生成的界面:

3.1 創建屬性頁

爲了創建屬性表單,首先需要創建屬性頁,MFC中屬性頁對應的類是CPropertyPage,該類生成的對象代表了屬性表單中一個單獨的屬性頁。該類的繼承層次結構:

  可以看出,CPropertyPage類是從CDialog派生而來的。因此,一個屬性頁窗口其實就是一個對話框窗口。

✨ 1)創建一個對話框窗口,首先需要創建對話框資源。點擊 資源視圖 –>右鍵Dialog–> 添加資源–> 資源視圖 ,在彈出的資源類型對話框中點擊Dialog菜單項左側的 +,即可看到三種屬性頁資源:

  • IDD_PROPPAGE_LARGE、
  • IDD_PROPPAGE_MEDIUM、
  • IDD_PROPPAGE_SMALL。


創建3個IDD_PROPPAGE_LARGE類型的屬性頁資源。

修改3個屬性頁資源的ID與標題:

  • 第一個:IDD_PROP1+Page1
  • 第二個:IDD_PROP2+Page2
  • 第三個:IDD_PROP3+Page3

屬性頁資源和通常插入的對話框資源之間的區別:
1. 對比外觀選項卡屬性:

可以看出,二者區別:

2. 對比其他屬性:

📢📢📢 知道了這兩種資源之間的區別後,可以在程序中先增加一個普通對話框資源,然後修改其屬性,使其符合屬性頁資源的要求,然後把它當作屬性頁資源來使用。

✨✨ 2)首先刪除Prop程序中各個屬性頁資源上已有的靜態文本控件,然後在每一個屬性頁中增加一些控件。

  1. 在第一個屬性頁:
    1) 放置一個組框(Group Box)。組框可以用來起一個分組的作用,可以把相關的一些選項放置在一個組框中
      將新添加的這個組框的標題修改爲:請選擇你的職業
      1.1)然後在這個組框內放置三個單選按鈕( Radio Button)。更改屬性名依次爲:程序員、老師、老闆。

    2)再放置一個列表框控件(List Box),這種類型的控件提供了信息的一種簡單的組織方式,可以排列一些字符串提供給用戶進行選擇
      2.1)在該列表框上放置一個靜態文本控件(Static Text),這種控件主要起標示作用。
      將其文本屬性修改爲:請選擇你的工作地點

    最後應該調整一下各個控件的Caption,以及相對位置,使其美觀些。

  2. 在第二個屬性頁:
    1)首先放置一個組框(Group Box)。
      將其標題修改爲: 請選擇你的興趣愛好
    2)組框內添加四個複選框(Check Box)。
      把它們的標題分別修改爲:“足球”、“籃球”、“排球”、“游泳”

  3. 在第三個屬性頁:
    1)首先增加一個組合框(Combo Box)。
      增加組合框時應注意:拖放時要將它的範圍拉得大些,否則在程序運行時單擊它右邊的下拉箭頭時,顯示的下拉空間很小。無法將其下拉框中的內容顯示出來。
      Type選擇Drop List類型。

    調整該組合框下拉列表部分的範圍,方法是在對話框資源處於編輯狀態時,把鼠標移動到該組合框控件右邊向下的箭頭上,當鼠標變成雙向箭頭形狀時,按下鼠標左鍵,把組合框的下拉列表範圍拖動到合適大小。
      組合框提供了編輯框加列表框的功能。VC++提供了三種類型的組合框,打開組合框控件的屬性對話框,並單擊Type選項頁:
    🔳 簡易式(Simple)
    這種類型的組合框包含一個編輯框和一個總是顯示的列表
    🔳下拉式(Dropdown)
    類似於簡易式組合框,二者的區別在於下拉式組合框僅當單擊下拉箭頭後,列表框纔會彈出。
    🔳下拉列表式(Drop List)
    下拉列表式組合框也有一個下拉的列表框,但它的編輯框是隻讀的,不能輸入字符。也就是說,這種類型的組合框只能從其下拉列表中選擇內容。

    2)在第三個屬性頁上,在添加的組合框控件上方擺放一個靜態文本,其標題設置爲: 請選擇你的薪資水平

✨✨✨ 3)爲屬性頁對話框資源添加3個屬性頁類。
  設定新類的名稱分別爲CProp1、CProp2、CProp3,並把它的基類選擇爲: CPropertyPage。
    
  根據下面的方法添加三個新類:解決用類嚮導添加MFC類,基類列表沒有CPropertyPage類。

在實際編程過程中,有時利用上述方法添加新類後,可能會出現這樣的現象:系統會提示無法打開新類的源文件和頭文件。這是VC++自身的問題。實際上,這時程序已經完成了新類的添加,只不過這個類的信息沒有記錄在類嚮導中。在類嚮導對話框的類名下拉列表中找不到這個新添加的類名,但這個類確實是一個完整的類,它有源文件和頭文件。爲了解決這個問題,即如何讓類嚮導找到新添加的類,可以按以下步驟:
① 保存工程。
② 利用文件中的關閉解決方案關閉當前工作區。
③ 在Windows資源瀏覽器中找到該工程所在的目錄,並找到.clw文件,該文件存儲的就是類嚮導的一些相關信息。
④ 回到VC++開發環境,打開剛剛關閉的工程,打開類嚮導。會彈出一個話話框,該對話框提示類嚮導的數據庫(即.clw文件) 不存在,詢問用戶是否願意從工程的源文件中創建這個數據庫。
⑤ 單擊【是】按鈕,就會彈出一個對話框,通常不需要對此對話框進行任何修改,直接單擊【OK】按鈕即可,完成.clw文件的創建。這時,就會發現在類嚮導對話框的類名下拉列表中就可以看到先前添加的新類了。

3.2 創建屬性表單

  爲了創建一個屬性表單,首先需要創建一個CPropertySheet對象,接下來,在此對象中爲每一個屬性頁創建一個對象(CPropertyPage類型),並調用AddPage函數添加每一個屬性頁,然後調用DoModal函數顯示一個模態屬性表單,或者調用Create函數創建一個非模態屬性表單
  因此,可以通過以下幾個步驟實現屬性表單創建的功能:

✨1)爲Prop程序創建一個屬性表單對象。
  通過類嚮導添加MFC類,新類命名爲:CPropSheet,並選擇其基類爲:CPropertySheet

✨✨2)在屬性表單對象CPropSheet中添加屬性頁。
需要調用CPropertySheet類的成員函數:AddPage。其申明如下:

void AddPage(CPropertyPage *pPage);

可以看出此函數有一個CPropertyPage類型指針的參數,它指向的就是需要添加到屬性表單中的屬性頁對象。也就是說,通過此函數可以將屬性頁對象添加到屬性表單中。

首先在屬性表單對象(CPropSheet)的頭文件中爲先前創建的三個屬性頁分別定義一個成員對象:

CProp1 m_prop1;
CProp2 m_prop2;
CProp3 m_prop3;

通常都是在屬性表單對象的構造函數中添加屬性頁對象。但是對CPropSheet對象來說,此時它還不知道CProp1、CProp2和CProp3這三種類型的定義,所以還必須在CPropSheet類的頭文件中分別把這三個屬性頁類的頭文件包含進來:

#include "CProp1.h"
#include "CProp2.h"
#include "CProp3.h"

接下來就可以在CPropSheet類的構造函數中添加這三個屬性頁對象,但是發現CPropSheet有兩個構造函數:

CPropSheet::CPropSheet(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage)
	:CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
{

}

CPropSheet::CPropSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
	:CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{

}

其中一個函數是用ID號(nlDCaption),另一個函數是用標題字符串(pszCaption)來構造屬性表單對象。對應的基類: CPropertySheet的兩個構造函數的聲明原型:

CPropertysheet( UINT nIDCaption, cwnd *pParentWnd = NULL, UINT iSelectPage =0 );
CPropertySheet( LPCTSTR pszCapion, CWnd *pParentWnd NULL, UINTiselectPage = o);

這兩個構造函數的後兩個參數都是相同的。

  • 第二個參數pParentWnd,即父窗口指針都有默認值:NULL,此時的屬性表單的父窗口就是應用程序的主窗口。對於SDI應用程序來說,就是應用程序的主框架窗口。
  • 第三個參數iSelectPage指定的是屬性表單初始選擇的屬性頁,可以通過這個參數指定屬性表單初始顯示時顯示的屬性頁,默認是第一個頁面。

因爲屬性表單類有兩個構造函數,在構造屬性表單對象時,可以任選其中一個構造函數。在這兩個構造函數都調用AddPage函數添加屬性頁對象:

CPropSheet::CPropSheet(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage)
	:CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
{
	AddPage(&m_prop1);
	AddPage(&m_prop2);
	AddPage(&m_prop3);
}

CPropSheet::CPropSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
	:CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{
	AddPage(&m_prop1);
	AddPage(&m_prop2);
	AddPage(&m_prop3);
}

✨✨✨3)顯示屬性表單。
CPropertySheet類的繼承關係結構圖:

CPropertySheet類從CWnd類派生而來。而不是派生於CDialog類。但是CPropertySheet對象和CDialog對象的操縱方式是類似的。屬性表單對象的創建也需要兩個步驟:第一步調用構造函數定義一個屬性表單對象,然後調用DoModal成員函數創建一個模態屬性表單或者調用Create成員函數創建一個非模態屬性表單

🟡🟡 1)在主菜單上添加一個菜單項,當用戶單擊這個菜單項後,程序顯示CPropertyPage屬性表單對象。
  在幫助菜單項後添加一個屬性表單菜單項。其屬性對話框中設置PopUp選項爲False、Caption設置:屬性表單、ID設置爲IDM_PROPERTYSHEET。

🔵🔵 2)爲此新菜單項添加命令響應函數。
  讓CPropView類捕獲此菜單命令,並接受系統自動賦予的響應函數名稱OnPropertysheet。

🟣🟣 3)在此函數中創建屬性表單。
1、先在CPropView類的源文件中包含CPropSheet類的頭文件。

#include "pch.h"
#include "framework.h"
// SHARED_HANDLERS 可以在實現預覽、縮略圖和搜索篩選器句柄的
// ATL 項目中進行定義,並允許與該項目共享文檔代碼。
#ifndef SHARED_HANDLERS
#include "Prop.h"
#endif

#include "PropDoc.h"
#include "PropView.h"
#include "CPropSheet.h"

2、添加創建屬性表單的代碼:

void CPropView::OnPropertysheet()
{
	// TODO: 在此添加命令處理程序代碼
	CPropSheet propSheet(_T("屬性表單"));
	propSheet.DoModal();
}

運行代碼:

3.3 嚮導的創建

創建一個嚮導類型的對話框,應該遵循創建一個標準屬性表單的步驟來實現。但在調用屬性表單對象的DoModal函數之前,應該先調用SetWizardMode這一函數。因此,在Prop工程的CPropView類的OnPropertysheet函數中修改代碼:

void CPropView::OnPropertysheet()
{
	// TODO: 在此添加命令處理程序代碼
	CPropSheet propSheet(_T("屬性表單"));
	propSheet.SetWizardMode();
	propSheet.DoModal();
}

運行Prop程序,單擊屬性表單菜單命令,發現該對話框已經變成了一種嚮導的模式,它底部的按鈕變成了:上一步下一步

🔳🔳 問題:但是,上述這個嚮導對話框仍存在一些問題:在第一個頁面上,不應該有上一步這個按鈕;在最後一個頁面上,不應該是下一步按鈕,而應該是完成按鈕。在前面定義屬性頁資源時,並沒有增加這些按鈕,可見這些按鈕是屬於屬性表單的,那麼就需要調用屬性表單的相關函數來修改它的按鈕。

CPropertySheet類提供了一個 SetWizardButtons成員函數,可以用來設置嚮導對話框上的按鈕。該函數聲明:

void SetWizardButtons(DWORD dwFlags);

dwFlags參數可以是下表中所列各值的組合:

一般來說,應該在屬性頁的OnSetActive函數中調用SetWizardButtons這個函數。當屬性頁被選中,從而成爲一個活動的頁面時,應用程序框架就會調用OnSetActive這個函數。OnSetActive函數是一個虛函數,因此,在屬性頁子類中重寫這個函數,然後根據需要設置該屬性頁上的按鈕。

✨1)首先在每個屬性頁資源關聯的類CProp1、CProp2、CProp3中重寫OnSetActive函數。

CProp2和CProp3操作同CProp1。

✨✨2)在OnSetActive函數中調用屬性表單對象的SetWizardButtons函數,設置每個屬性頁上的按鈕。

  由於屬性頁是被添加到屬性表單中的,所以屬性表單是屬性頁的父窗口,可通過GetParent函數獲取屬性頁父窗口的指針,即屬性表單的指針。但該函數返回的是CWnd類型的指針,需要進行強制轉換,將CWnd類型的指針轉化成CPropertySheet類型的指針。然後利用此指針,調用SetWizardButtons函數。

  • 第一個屬性頁應該只有一個下一頁按鈕。
    因此SetWizardButtons函數的參數應該爲PSWIZB_NEXT,所以CProp1類的OnSetActive函數的具體實現:
    BOOL CProp1::OnSetActive()
    {
    	// TODO: 在此添加專用代碼和/或調用基類
    	CPropertySheet* pCPS = (CPropertySheet*)GetParent();
    	pCPS->SetWizardButtons(PSWIZB_NEXT);
    
    	return CPropertyPage::OnSetActive();
    }
    
  • 第二個屬性頁應該有一個上一頁按鈕和下一頁按鈕。
    BOOL CProp2::OnSetActive()
    {
    	// TODO: 在此添加專用代碼和/或調用基類
    	((CPropertySheet*)GetParent())->SetWizardButtons(PSWIZB_BACK|PSWIZB_NEXT);
    
    	return CPropertyPage::OnSetActive();
    }
    
  • 第三個屬性頁應該有一個上一頁按鈕和完成按鈕。
    BOOL CProp3::OnSetActive()
    {
    	// TODO: 在此添加專用代碼和/或調用基類
    	((CPropertySheet*)GetParent())->SetWizardButtons(PSWIZB_BACK|PSWIZB_FINISH);
    
    	return CPropertyPage::OnSetActive();
    }
    

最終:

對於嚮導來說,通常是希望用戶在每個屬性頁中進行一些選擇。接下來,我們就對每個頁面進行一個判斷,檢查用戶是否做出選擇,如果沒有,就禁止程序進入下一個頁面。也就是說,用戶必須進行了一項選擇之後,才能進入下一個頁面。

3.3.1 處理第一個頁面

✨ 1)首先處理第一個頁面,爲這個頁面上的單選按鈕關聯一個成員變量。
  在第一個單選按鈕(其ID爲IDC_RADIO1)上單擊鼠標右鍵,從彈出的快捷菜單中選擇類嚮導,打開成員變量選項,發現成員變量的列表中並無IDC_RADIO1、IDC_RADIO2、IDC_RADIO3這三個ID。

📋📋 原因:因爲對一組單選按鈕來說,需要設置該組中第一個單選按鈕的Group屬性。
🟢🟢 解決:打開單選按鈕的屬性對話框,設置Group選項爲True。

再次打開類嚮導對話框,成員變量列表中出現了單選按鈕的ID號。爲此ID添加一個值類型的成員變量:m_occupation,數據類型選擇:int。

當爲第一個單選按鈕設置了Group選項後,隨後的兩個單選按鈕就和這個按鈕屬於同一組了,直到遇到下一個(按照Tab順序)具有Group屬性的控件爲止。

◼◻◼ 通過判斷成員變量的值判斷當前選中的是哪個單選按鈕控件。
  在Prop程序運行時,當選中第一個單選按鈕後,它所關聯的成員變量m_occupation的值就是0;當選中第二個單選按鈕後,m_occupation變量的值就是1;當選中第三個單選按鈕後,m_occupation變量的值就是2。於是在程序中,通過判斷這個成員變量的值就可以知道當前選中的是哪個單選按鈕控件。

程序中變量在構造函數中自動被初始化爲0,可以更改爲-1,表明初始顯示時,三個單選按鈕一個也沒有選中。因此在程序中就可以對這個變量進行判斷,如果其值爲-1,就說明用戶沒有選擇單選按鈕選項。

另外,在CProp1的DoDataExchange函數中,可以看到添加了一條DDX_Radio函數的調用,用來在單選按鈕控件與成員變量之間交換數據

void CProp1::DoDataExchange(CDataExchange* pDX)
{
	CPropertyPage::DoDataExchange(pDX);
	DDX_Radio(pDX, IDC_RADIO1, m_occupation);
}

✨✨ 2)當用戶單擊第一個屬性頁上的 下一步按鈕後,應該判斷用戶是否選擇了某個職業,只有當用戶選擇了某個職業時,程序才能進入下一個屬性頁。
📋📋 依據:當用戶單擊屬性頁上的下一步按鈕後,程序將調用OnWizardNext這個虛函數。如果這個函數返回0,那麼程序自動進入當前嚮導的下一個屬性頁;如果返回-1,將禁止屬性頁發生變更。
  爲CProp1類添加OnWizardNext這個虛函數的處理,來完成對該屬性頁上下一步按鈕的命令響應。該虛函數的添加方法與前面OnsetActive虛函數的添加方法相同。


LRESULT CProp1::OnWizardNext()
{
	// TODO: 在此添加專用代碼和/或調用基類

	return CPropertyPage::OnWizardNext();
}

添加之後,可以在這個虛函數中判斷m_occupation變量的值,如果是-1,說明用戶沒有選擇任何一個職業,則會彈出一個對話框,提示用戶應選擇一個職業,然後讓這個虛函數返回-1,禁止進入下一個屬性頁。

LRESULT CProp1::OnWizardNext()
{
	// TODO: 在此添加專用代碼和/或調用基類
	//未選中
	if (m_occupation == -1) {
		MessageBox(_T("請選擇您的職業!"));
		return -1;
	}
	
	return CPropertyPage::OnWizardNext();
}


📋📋 出現此問題原因:控件與成員變量的數據交換是通過DoDataExchange函數來完成的,而程序中並不直接調用這個函數,而是通過調用UpdateData函數來調用它。對UpdateData來說,當它的參數爲TRUE時,是從控件得到成員變量的值;當參數值爲FALSE時,是用成員變量的值初始化控件

🟢🟢 解決:在CProp1類的OnWizardNext函數中要從控件得到相關聯的變量的值,就應該以TRUE爲參數來調用UpdateData函數,由於這個參數的默認值就是TRUE,因此可以以不帶參數的形式直接調用UpdateData函數:

LRESULT CProp1::OnWizardNext()
{
	// TODO: 在此添加專用代碼和/或調用基類
	//未選中
	UpdateData();
	if (m_occupation == -1) {
		MessageBox(_T("請選擇您的職業!"));
		return -1;
	}
	return CPropertyPage::OnWizardNext();
}


✨✨✨ 3)爲第一個屬性頁添加對工作地點的選擇進行判斷的代碼。
  首先需要在工作地點列表框中增加一些工作地點。應該在響應這個屬性頁的WM_INITDIALOG消息的函數中完成這一任務,也就是在這個屬性頁顯示之前向列表框中增加一些工作地點。因此首先爲CPropl類添加WM_INITDIALOG消息的響應函數(OnInitDialog)。對話框“消息”中找不到WM_INITDIALOG

BOOL CProp1::OnInitDialog()
{
	CPropertyPage::OnInitDialog();
	// TODO:  在此添加額外的初始化

	return TRUE;  // return TRUE unless you set the focus to a control
				  // 異常: OCX 屬性頁應返回 FALSE
}

前面已經介紹過,在MFC編程中,對控件的操作都是通過相關的MFC類來完成的。對於列表框,也有一個與之對應的MFC類:CListBox。該類提供了一個成員函數AddString,用於向列表框添加字符串。因此在OnInitDialog函數中,首先需要獲得這個列表框控件對象,此列表框的ID:IDC_LIST2。然後調用該對象的AddString函數完成工作地點的添加。

BOOL CProp1::OnInitDialog()
{
	CPropertyPage::OnInitDialog();

	// TODO:  在此添加額外的初始化
	//獲取列表框控件對象
	CListBox* pCLB = (CListBox*)GetDlgItem(IDC_LIST2);
	pCLB->AddString(TEXT("北京"));
	pCLB->AddString(TEXT("信陽"));
	pCLB->AddString(TEXT("上海"));
	((CListBox*)GetDlgItem(IDC_LIST2))->AddString(TEXT("天津"));
	((CListBox*)GetDlgItem(IDC_LIST2))->AddString(TEXT("鄭州"));
	((CListBox*)GetDlgItem(IDC_LIST2))->AddString(TEXT("南京"));


	return TRUE;  // return TRUE unless you set the focus to a control
				  // 異常: OCX 屬性頁應返回 FALSE
}

✨✨✨✨ 4)對工作列表框控件進行判斷,讓用戶必須選擇一個工作地點;否則,不能進入下一個屬性頁面。
  同前面的單選按鈕一樣,首先需要給這個列表框控件關聯一個成員變量,方法同上。添加成員變量對話框中,設置這個成員變量的名稱爲: m_workAddr、選擇值類型、變量類型選擇CString。同前面的m-occupation變量一樣, CProp1類在其構造函數中對m_workAddr變量也需要進行初始化。

其初始化如下:

CProp1::CProp1()
	: CPropertyPage(IDD_PROP1)
	, m_occupation(-1)
	, m_workAddr(_T(""))
{

}

並在DoDataExchange函數中添加:

void CProp1::DoDataExchange(CDataExchange* pDX)
{
	CPropertyPage::DoDataExchange(pDX);
	DDX_Radio(pDX, IDC_RADIO1, m_occupation);
	DDX_LBString(pDX, IDC_LIST2, m_workAddr);
}

可以在CProp1類的OnWizardNext函數中對列表框控件相關聯的成員變量進行判斷,檢查用戶是否選擇了一個工作地點。如果m_workAddr變量爲空,那麼說明用戶沒有選擇工作地點,OnWizardNext函數就返回一個-1值,禁止進入下一個屬性頁。

LRESULT CProp1::OnWizardNext()
{
	// TODO: 在此添加專用代碼和/或調用基類
	//未選中
	UpdateData();
	if (m_occupation == -1) {
		MessageBox(_T("請選擇您的職業!"));
		return -1;
	}
	if (m_workAddr == "") {
		MessageBox(_T("請選擇你的工作地點!"));
		return -1;
	}
	return CPropertyPage::OnWizardNext();
}

3.3.2 處理第二個頁面

✨1)爲四個複選框分別關聯一個值類型的成員變量。



對於複選框控件來說,當選中時,它所關聯的成員變量的值應該爲TRUE,否則爲FALSE。在Prop2類的構造函數中,可以看到它將新添加的四個成員變量都初始化爲FALSE。

CProp2::CProp2()
	: CPropertyPage(IDD_PROP2)
	, m_football(FALSE)
	, m_basketball(FALSE)
	, m_volleyball(FALSE)
	, m_swim(FALSE)
{

}

數據交換情況:

void CProp2::DoDataExchange(CDataExchange* pDX)
{
	CPropertyPage::DoDataExchange(pDX);
	DDX_Check(pDX, IDC_CHECK1, m_football);
	DDX_Check(pDX, IDC_CHECK2, m_basketball);
	DDX_Check(pDX, IDC_CHECK3, m_volleyball);
	DDX_Check(pDX, IDC_CHECK4, m_swim);
}

✨✨2)如果用戶沒有選擇任何一個興趣愛好,就不讓程序進入下一個屬性頁面。因此同前面的第一個屬性頁一樣,**首先爲CProp2類添加OnWizardNext虛函數的重寫。

LRESULT CProp2::OnWizardNext()
{
	// TODO: 在此添加專用代碼和/或調用基類


	return CPropertyPage::OnWizardNext();
}

然後在此函數中,對用戶是否做出選擇進行判斷。實際上,對這四個成員變量,如果有任有一個變量爲TRUE,就可以進入下一個屬性頁面;否則顯示一個對話框,提示用戶必須先選擇一個興趣愛好,然後該虛函數返回-1,禁止程序進入下一個屬性頁。

需要注意一點,根據前面對第一個屬性頁面的處理,有了這樣的經驗,就是在對與控件相關聯的變量進行判斷之前,需要調用UpdateData函數,以實現控件與成員變量的數據交換。

LRESULT CProp2::OnWizardNext()
{
	// TODO: 在此添加專用代碼和/或調用基類
	UpdateData();
	if (m_basketball || m_football || m_volleyball || m_swim) {
		return CPropertyPage::OnWizardNext();
	}
	else
	{
		//未選中任何複選框
		MessageBox(TEXT("請選擇你的興趣愛好!"));
		return -1;
	}
}


3.3.3 處理第三個頁面

第三個屬性頁中擺放的是一個組合框控件,這時,要向這個組合框中添加一些關於薪資的選項,以便用戶進行選擇。

組合框控件由一個編輯框和一個列表框組成,其相對應的MFC類是CComboBox,該類也有一個成員函數:AddString,用來向組合框控件的列表框中添加字符串選項

✨1)首先爲CProp3類添加WM_INITDIALOG消息的響應函數(即重寫OnInitDialog函數)。

✨✨ 2)在此函數中對這個屬性頁對話框進行初始化,即在此函數中調用組合框對象的AddString函數,向組合框控件的列表框中添加一些薪資選項:


BOOL CProp3::OnInitDialog()
{
	CPropertyPage::OnInitDialog();

	// TODO:  在此添加額外的初始化
	CComboBox* pCCB = (CComboBox*)GetDlgItem(IDC_COMBO2);
	pCCB->AddString(_T("10000元以下"));
	pCCB->AddString(_T("10000~20000元"));
	pCCB->AddString(_T("20000~30000元"));

	((CComboBox*)GetDlgItem(IDC_COMBO2))->AddString(_T("30000~50000元"));
	((CComboBox*)GetDlgItem(IDC_COMBO2))->AddString(_T("50000元以上"));




	return TRUE;  // return TRUE unless you set the focus to a control
				  // 異常: OCX 屬性頁應返回 FALSE
}


發現這四個選項的顯示順序與代碼中添加的順序不一樣。這主要是因爲組合框默認情況下具有排序的功能,若希望組合框的列表框中的字符串按照代碼中添加的順序顯示的話,可以打開這個組合框控件的屬性對話框,設置Sort選項爲False。


✨✨✨ 3)在第三個屬性頁對話框初始顯示時,這個組合框在其編輯框中有一個初始選擇的項。
  通過組合框的一個成員函數: SetCursel來完成,該函數的功能是選擇組合框的列表框中的一個字符串,並將其顯示在該組合框的編輯框中。SetCurSel函數的聲明:

int SetCurSel(int nSelect);
  • nSelect是一個基於0的索引,指定選擇項的索引位置。如果其值爲-1,那麼將移除該組合框的當前選擇,並清空該組合框的編輯框中的內容。

因此在CProp3類的OnInitDialog函數中實現組合框初始顯示時選中第一個選項。


BOOL CProp3::OnInitDialog()
{
	CPropertyPage::OnInitDialog();

	// TODO:  在此添加額外的初始化
	CComboBox* pCCB = (CComboBox*)GetDlgItem(IDC_COMBO2);
	pCCB->AddString(_T("10000元以下"));
	pCCB->AddString(_T("10000~20000元"));
	pCCB->AddString(_T("20000~30000元"));

	((CComboBox*)GetDlgItem(IDC_COMBO2))->AddString(_T("30000~50000元"));
	((CComboBox*)GetDlgItem(IDC_COMBO2))->AddString(_T("50000元以上"));


	((CComboBox*)GetDlgItem(IDC_COMBO2))->SetCurSel(0);

	return TRUE;  // return TRUE unless you set the focus to a control
				  // 異常: OCX 屬性頁應返回 FALSE
}

3.3.4 接收用戶在嚮導中所作的選擇

實現的功能:
   Prop程序要將嚮導中用戶的選擇輸出到視類的窗口中。

爲了在視類中得到用戶在這三個頁面中所進行的選擇:
1)首先爲第三個頁面添加一個CString類型的成員變量: m_strSalary,用來接收用戶的選擇。

2)給CProp3類添加OnWizardFinish虛函數。
  程序應該在用戶單擊向導的完成按鈕時,將用戶所作的薪資水平選擇保存到這個變量中,所以應該給CProp3類添加一個虛函數:OnWizardFinish,以處理完成按鈕的單擊消息。

3)獲取用戶選擇的薪資選項。
  ◼◻◼ 爲了獲取用戶選擇的薪資選項,首先需要得到該選項的索引值,利用CComboBox類的GetCurSel成員函數實現。該函數的返回值是一個基於0的索引,表明組合框的列表框中當前選中項的位置。
  ◻◼◻ 獲得用戶選擇的薪資選項索引之後,再利用CComboBox類的另一個成員函數:GetLBText從組合框的列表框中指定位置處得到一個字符串,該函數有兩種聲明原型,其中一種:

void GetLBText(int nIndex,cstring& rstring ) const;
  • 第一個參數指定列表框中將被複制的字符串的索引位置。本例就可以將它設置爲GetCurSel函數的返回值,即得到當前選中項的字符串。
  • 第二個參數就是指定用來接收復制字符串的緩存。
BOOL CProp3::OnWizardFinish()
{
	// TODO: 在此添加專用代碼和/或調用基類
	int pos=((CComboBox*)GetDlgItem(IDC_COMBO2))->GetCurSel();
	((CComboBox*)GetDlgItem(IDC_COMBO2))->GetLBText(pos, m_strSalary);
	MessageBox(m_strSalary);
	return CPropertyPage::OnWizardFinish();
}


4)爲了接收用戶在嚮導中做出的選擇,在視類中需要定義一些變量來保存它們,下面列出了爲視類添加的成員變量,並且將它們的訪問權限都設置爲私有的。

◻◼◻ 在視類的構造函數中初始化這些添加的變量。

CPropView::CPropView() noexcept
{
	// TODO: 在此處添加構造代碼
	m_iOccupation = -1;
	m_strWorkAddr = "";
	memset(m_bLike, 0, sizeof(m_bLike));
	m_strSalary = "";
}

使用C語言的memset函數對m_bLike數組進行快速初始化,該函數的聲明:

void* memset(void *dest, int c, size_t count );

該函數的功能是把dest參數指定的內存中前count個字節設置爲字符: c。

  • dest
    指向將被賦值的目標內存。
  • c
    設置的字符值。
  • count
    設置的字節數。

在C/C++語言中,非0值即爲真(TRUE), 0值即爲假(FALSE),並且對數組來說,數組名就是它的首地址,數組大小可以利用sizeof函數來獲取。

因此可以用0值設置數組mbLike指向的內存緩存,從而將它的元素都設置爲FALSE。

5)接下來就要在視類窗口把用戶在嚮導中的選擇輸出到窗口中。
  但有一點需要注意:只有用戶單擊完成按鈕關閉嚮導後,才輸入用戶的選擇;如果用戶單擊的是取消按鈕,即放棄當前所作的選擇,程序就不應該輸出用戶的選擇。一般情況下,CPropertySheet類的DoModal函數的返回值是IDOK或IDCANCEL,但是如果屬性表單已經被創建爲嚮導了,那麼該函數的返回值將是ID_WIZFINISH或IDCANCEL。因此在程序中應該對屬性表單對象的DoModal函數的返回值進行判斷,如果返回的是完成按鈕的ID: ID_WIZFINISH,那麼才進行輸出處理。

  這裏有一點需要注意,當DoModal函數返回後,屬性表單窗口就被銷燬了,但propSheet這個屬性表單對象的生命週期並沒有結束。因此,仍然可以利用這個對象去訪問它的內部成員。這裏又一次提到窗口和對象的關係,它們並不是同一個事物

改錯:前面CProp1、CProp2、CProp3中添加的成員變量類型設置爲public類型的。因爲後續取值需要訪問這些變量的值。CPropSheet中的成員變量也設置爲public類型。

void CPropView::OnPropertysheet()
{
	// TODO: 在此添加命令處理程序代碼
	CPropSheet propSheet(_T("屬性表單"));
	//設置成嚮導模式
	propSheet.SetWizardMode();
	//判斷DoModal函數的返回值是否爲完成
	if (ID_WIZFINISH==propSheet.DoModal())
	{
	    //等號右側成員變量必須是公開的,否則此處無法訪問
		m_iOccupation = propSheet.m_prop1.m_occupation;
		m_strWorkAddr = propSheet.m_prop1.m_workAddr;
		m_bLike[0] = propSheet.m_prop2.m_football;
		m_bLike[1] = propSheet.m_prop2.m_basketball;
		m_bLike[2] = propSheet.m_prop2.m_volleyball;
		m_bLike[3] = propSheet.m_prop2.m_swim;
		m_strSalary = propSheet.m_prop3.m_strSalary;
		Invalidate();
	}	
}

調用Invalidate函數,讓視類窗口無效,從而引起重繪操作。然後,就可以在視類的OnDraw函數中完成這些消息的輸出:

void CPropView::OnDraw(CDC* pDC)
{
	CPropDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO: 在此處爲本機數據添加繪製代碼
	//設置畫筆的格式
	CFont font;
	font.CreatePointFont(300, _T("華文行楷"));
	//保存舊的畫筆
	CFont* pOldFont;
	pOldFont = pDC->SelectObject(&font);

	//臨時變量
	CString strTemp;
	
	//職業:
	strTemp = _T("職業:");
	switch (m_iOccupation)
	{
	case 0:
		strTemp += "程序員";
		break;
	case 1:
		strTemp += "老師";
		break;
	case 2:
		strTemp += "老闆";
		break;
	default:
		break;
	}
	//(0,0)處顯示職業
	pDC->TextOutW(0, 0, strTemp);


	//定義文本信息結構體變量
	TEXTMETRIC tm;
	//得到設備描述表中當前字體的度量信息:字體高度
	pDC->GetTextMetrics(&tm);


	//工作地點
	strTemp = _T("工作地點:");
	strTemp += m_strWorkAddr;
	//在(0,字體高度)處顯示工作地點
	pDC->TextOutW(0, tm.tmHeight, strTemp);


	//興趣愛好
	//選中,則在字符串末尾追加興趣
	strTemp = _T("興趣愛好:");
	if (m_bLike[0]) {
		strTemp += "足球 ";
	}
	if (m_bLike[1]) {
		strTemp += "籃球 ";
	}
	if (m_bLike[2]) {
		strTemp += "排球 ";
	}
	if (m_bLike[3]) {
		strTemp += "游泳 ";
	}
	//在(0,2倍字體高度)處顯示興趣愛好
	pDC->TextOutW(0, tm.tmHeight * 2, strTemp);

	//薪資
	strTemp= _T("薪資:");
	strTemp += m_strSalary;
	//在(0,3倍字體高度)處開始顯示薪資
	pDC->TextOutW(0, tm.tmHeight * 3, strTemp);

	//恢復原來畫筆
	pDC->SelectObject(pOldFont);


}

運行程序,輸出如下內容:
在這裏插入圖片描述
點擊屬性列表,並進行選擇:

最終窗口顯示如下:

這是由於調用Invalidate後,引起了窗口重繪,重新調用了視類的OnCreate函數,此時獲取到各個屬性頁的內容,並顯示出來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章