1. 程式人生 > >使用MFC編寫繪圖程式的總結

使用MFC編寫繪圖程式的總結

之前學習了C++面向物件和STL(標準模板庫)的知識,苦於沒有實戰專案來加深理解。於是翻來Ivor Horton 編寫的《Visual C++ 2013 入門經典(第7版)》這本書,把書中繪圖的專案程式碼了一遍。這個專案是一個基於MFC多文件的桌面應用程式,完成這個專案除了增加了對C++面向物件程式設計的理解,同時也加深了對MFC的理解,在整個過程中我側重於把握前者。這是第二遍寫這本書的程式碼,第一次C++知識不怎麼完善,寫完整個專案的感覺像搭積木,而且對類中一些限定詞(如const、override、virtual、protected),指標引用和STL中的容器物件不怎麼理解,所以這一次重在把握程式設計的整體架構,加深理解面向物件繼承和成員函式過載等概念,同時加深了對MFC的瞭解。

對MFC的理解:

在windows平臺上開發桌面應用程式有兩種方式:一種是直接面向WindowsAPI,API函式比如WinMain(),WindowProc(),WinMain就是main函式,windowProc是處理各種訊息的函式;另一種是基於MFC,MFC是對WindowsAPI的進一步封裝,這種方式看不到WinMain(),windowProc()函式,它們在MFC內部處理好了。我們要做的就是對MFC提供的幾個類進行派生以及編寫自己的類。

MFC提供的基類關係圖:


所以簡單來說,按照MFC嚮導建立的專案,包括單文件和多文件型別都將會看到App類、Doc類、View類、Frame類。DocTemplate類是隱藏的用來連線他們的一個類。基於對話方塊的結構簡單些,其中的Dialog類,和View類、Frame類一樣是由CWnd派生的。

怎麼與Windows通訊呢?說白了就是,按滑鼠、點選選單欄、按鈕等產生的訊息,Windows怎麼處理?響應函式寫在什麼地方?在WindowsAPI中有WindowProc()函式,MFC封裝了看不到了之後呢?實際上上面你能看到的類都能處理windows訊息,類中DECLARE_MESSAGE_MAP()巨集表明這個類裡面有作為訊息處理程式的函式成員。

把訊息處理程式放置在什麼地方取決於訊息的類別。一、標準的Windows訊息,以WM_開頭(不包括WM_COMMAND),包括重畫WM_PAINT,釋放滑鼠左鍵WM_LBUTTONUP等,它們總是由最終派生於CWnD的類的物件處理,意思就是不能放在App類,Doc類中;二、對話方塊裡的控制元件發出的訊息,包括點選了按鈕,這樣的訊息順其自然可以由Dialog類物件處理;三、命令訊息,即WM_COMMAND訊息,包括點選了選單欄和工具欄,處理這樣的訊息比較靈活,可能放在上面可以看到的任一類物件中,比如在此程式就放在了CDoc類中。

對繪圖過程的理解:

開始迴歸到這個繪圖專案程式,程式是基於多文件。為了消除零碎的感覺,簡要畫了類之間的關係圖如下:



完成的目的就是用滑鼠在視窗中繪製各種元素,畫直線、矩形、圓等。按左鍵確定起點;左鍵不放移動滑鼠調整臨時元素;右鍵鬆手將元素新增進元素物件容器;可以隨意變換顏色、元素型別、鋼筆線寬等屬性。

假如沒有與滑鼠的互動,怎麼繪圖呢?CView類裡的OnDraw(CDC* pDC),我簡單的理解就是這個函式提供24小時隨時重新整理服務。具體地,百度了一下,是這麼說的:MFC呼叫OnDraw()函式是和所有會產生WM_PAINT訊息的函式有關,Invalidate()、ReDisplay() 呼叫時會產生WM_PAINT訊息,所以也會使用MFC呼叫OnDraw()函式。

比如畫直線的語句是這樣的:pDC->MoveTo();pDC->LineTo()。畫圖操作由不同的元素的型別分別實現,比如畫直線和畫矩形是不同的;定義元素基類CElement,將Draw()設為虛擬函式,由子類CLine,CRectangle實現不同的Draw()。所以程式在進行繪圖時是OnDraw()根據不同的元素型別呼叫了不同的Draw()函式。

所有元素相同的比如顏色、線寬、起點等屬性可以繼承自基類。

在CDoc類中設定和元素屬性相關的成員,比如m_Color,m_PenWidth等,可以由選單欄改變值。構造元素物件所需的引數可以從CDoc類物件中的這些成員的值中獲取。

線寬由對話方塊中單選按鈕設定,在CDialog類中設定成員m_PenWidth接收控制元件裡的值。然後CDialog類物件成員m_Penwidth的值傳入CDoc類物件裡的m_PenWidth。

處理滑鼠訊息,將滑鼠動作的響應函式寫在CView類裡。 在類中增加3個成員變數:m_FirstPoint,m_SecondPoint,m_pTempElement。根據程式的設計目標,左鍵按下的處理函式將當前的游標位置傳遞給m_FirstPoint;左鍵按下而且滑鼠移動的處理函式(繪直線的情況)將當前游標位置傳遞給m_SecondPoint,同時建立臨時元素物件m_pTempElement,並呼叫Draw()函式實時繪圖;左鍵鬆開的處理函式檢測是否存在臨時元素物件,若存在將其壓入元素容器m_Sketch,消除臨時物件,發出重繪命令訊息。

對程式設計技術的理解:

成員函式應該設定為public還是protected,我這樣理解:如果只允許派生類訪問,則設定為protected,如果存在在其他類中訪問的情況,則設定為public。

在派生類中重寫基類成員函式時,最好加上override,加上之後就代表聲稱了這個函式就是用來過載基類函式的,不然報錯。當然不寫也可以。

在成員函式的引數和返回值中經常出現引用型別,而且如果不用改變它的值會在前面加上限定const。

當然也有很多指標型別,比如CDC* pDC,CxxDoc* pDoc,CElement* pTempElement等。在list容器中儲存的也是元素物件的指標,比如這句:std::list<std::shared_ptr<CElement>> m_Sketch,使用了共享指標。

const限定的成員函式 ,也就是const加在引數列表的後面,表明這個函式是“只讀的”,不會改變類物件裡的任何成員變數(static型別修飾的成員變數除外,因為static型別成員是所有物件共同維護的,單個物件的成員函式的權力沒這麼大)。

最後:

下圖1是基於多文件的程式介面效果,下圖2是我又用單文件模板寫完之後的介面效果:

因為在單文件程式中一次只存在一個文件,其中的差別在OnDraw()函式中有所體現。 在多文件中就存在文件物件的遍歷,用for迴圈實現:
void CSketcherView::OnDraw(CDC* pDC)
{
	CSketcherDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO:  在此處為本機資料新增繪製程式碼
	for (auto it = pDoc->begin(); it != pDoc->end();++it)
	{
		for (const auto pElement : *pDoc)
		{
			if (pDC->RectVisible(pElement->GetEnclosingRect()))
			pElement->Draw(pDC);
		}
	}
}
而在單文件中程式改寫如下:
void CDrawView::OnDraw(CDC* pDC)
{
	CDrawDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;
	std::list<std::shared_ptr<CElement>> Sketch = pDoc->GetSketch();
	const auto& haha = *pDoc;
	for (auto it = Sketch.begin(); it != Sketch.end();++it)
	{
		if (pDC->RectVisible((*it)->GetEnclosingRect()))
		{
			(*it)->Draw(pDC);
		}
	}
	
	// TODO:  在此處為本機資料新增繪製程式碼
}
可以看到list儲存的就是CElement物件的指標,然後使用了list的迭代器,它是指向list儲存物件的指標,所以it要想呼叫CElement物件的成員函式,需要這麼寫(*it)->Draw(pDC)或者(**it).Draw(pDC)。也就是需要對指標多做一次解引用。