第一卷 圖形繪製和輸出
序
自從Windows操作係統誕生以來,憑借其漂亮的圖形界麵和友好的用戶接口(當然不僅僅是憑借這兩點),很快就擊敗了采用冷冰冰的字符界麵的DOS操作係統,雖然後者也曾經是Microsoft公司的成功產品。Windows的圖形界麵也給程序員豐富的想象力提供了發揮的空間,程序員可以使用各種樣式的圖形和文本來完成程序,即便同樣是使用文本,Windows也是"畫"上去的。
當然,程序員的發揮是受到一定限製的。在Windows應用程序中,一般都是在窗口的客戶區中使用圖形和文本,以顯示應用程序的數據。當然,使用對話框和控件同樣也能夠將程序的數據顯示給用戶。但是對於一個具有"窗口"的應用程序來說,不在窗口中顯示點什麼似乎總是一種缺憾。因此,在窗口客戶區中按特定的方式繪製圖形或顯示文本就成為了開發Windows應用程序時的一項重要工作。
在MFC應用程序中,廣泛采用了文檔/視圖結構。文檔中保存了應用程序的數據,視圖則負責將文檔的數據顯示出來,因此,在MFC應用程序中,大多數的顯示工作都是在應用程序的視圖類中完成的。本章中將向用戶介紹如何在應用程序中使用圖形或文本顯示數據。
第一章 5.1 在文檔窗口中繪圖
對Windows程序而言,將程序運行的結果在文檔窗口上顯示出來是最自然的一種選擇,本節中就將通過一個例子程序向用戶介紹如何在MFC應用程序中實現繪圖。用戶將會看到,在文檔窗口上繪圖並不困難,基本上就像使用畫筆在畫布上作畫那樣簡單。但在開始建立例子程序之前,必須對一些基本的概念進行一些解釋。
5.1.1 理解設備環境
如果稍微考慮一下實際作畫的過程就可以發現,有兩樣東西是畫家所必須的:畫布與畫筆。在Windows程序中"繪圖"也是類似的,用戶必須有一塊電子畫布,一支電子畫筆,然後才能開始繪圖。
在Windows程序中,所謂的"設備環境(DC:Device Context)"就是這樣的一塊電子畫布。值得注意的是,設備環境並不僅僅隻是用戶的顯示器屏幕,也有可能是用戶的打印機,或者是其他的輸出設備。用戶在繪圖的時候,不需要考慮麵對的是顯示器設備環境或者打印機設備環境,隻要保證了在設備環境上的正確繪圖,Windows係統會通過具體物理設備的驅動程序,將用戶需要的圖形顯現出來,這就是Windows係統的設備無關性。
開始繪圖的另外一個條件就是用戶需要有一支電子畫筆。在Windows係統中,電子畫筆被稱為"GDI對象(GDI Object)"。除了畫筆之外,GDI對象還包括畫刷、字體、位圖和調色板等。因此,前麵簡單地說電子畫筆可能是不準確的,應該說,GDI對象就是那些可以用來在設備環境上繪圖的工具。
在Windows程序中,一個設備環境隻能擁有一種畫筆、一種畫刷和一種字體,也就是說,一個設備環境不能同時擁有同一類型的多個GDI對象。因此,如果用戶需要使用一支粗一點的畫筆,就需要先創建這樣的一支畫筆,然後將它選進設備環境代替原來的畫筆,然後使用這支新的畫筆。這就是在Windows應用程序中使用GDI對象的一般過程。
1.CDC類
在MFC中,CDC類實現了對設備環境的封裝。所有的繪圖操作都必須通過一個CDC類(或其派生類)的對象來完成。CDC類提供了許多函數,可以完成各種與設備環境有關的操作,如選擇GDI對象、使用顏色與調色板、繪製圖形、顯示字體、設置設備環境的屬性和坐標轉換等等。
在MFC基礎類庫中,CDC類還有幾個派生類,包括CPaintDC類、CClientDC類、CWindowDC類和CMetaFileDC類。
在CDC類的派生類中,CPaintDC類隻在響應WM_PAINT消息的函數中使用,大多數情況下都是OnPaint()函數。當應用程序的窗口出於某種原因需要更新時,係統就會向應用程序發送WM_PAINT消息,從而調用OnPaint()函數。但是用戶不要試圖去截獲視圖類的WM_PAINT消息或重載OnPaint()函數,而應該在OnDraw()函數中完成繪圖任務。這是因為CView類已經完成了對WM_PAINT消息的捕獲,並在OnPaint()函數中完成了特定了工作。
OnPaint()函數在最後調用了OnDraw()函數,並且向OnDraw()傳遞了一個CDC類對象的指針,用戶可以通過該指針完成各種繪圖操作。這裏需要提醒用戶的是,並不是一定要在OnDraw()函數完成繪圖操作,在其他函數中同樣是可以的,本節的例子程序就繞過了OnDraw()函數,在其他的消息處理函數中完成了繪圖操作。
CClientDC類可能是使用最多的CDC的派生類,正如其類名所指出,CClientDC類代表了應用程序窗口的客戶區。因此,所有使用CClientDC類對象完成的繪圖操作都位於窗口的客戶區內。
CWindowDC類封裝了與整個窗口有關的設備環境,包括窗口的客戶區和非客戶區;CMetaFileDC類封裝了與Windows元文件有關的繪圖操作。用戶可以通過聯機手冊獲得這兩個派生類的詳細信息。
前麵已經指出,在使用設備環境之前,必須構造一個CDC類(或其派生類)的對象。在MFC應用程序中,大部分的繪圖操作是針對應用程序窗口客戶區的,因此,大多數情況下都使用了CClientDC類的對象。
有兩種方法可以建立一個CClientDC對象,最簡單的是這樣:
CClientDC dc(this)
這樣的一條語句在程序的堆棧上建立了一個CClientDC對象,該對象與當前使用的窗口的客戶區相關聯。在本節的例子程序中,都是使用這種方式建立CClientDC對象的。
另外一種建立CClientDC對象是調用GetDC()函數,該函數的返回值雖然是一個CDC類對象的指針,但是實際該對象所代表的是窗口的客戶區。需要注意的是,在CDC類對象使用結束後,需要調用ReleaseDC()函數釋放設備環境。
2.CGdiObject類
在MFC中,CGdiObject類封裝了GDI對象。但是,用戶幾乎從來不需要直接使用CGdiObject類,而是使用該類的派生類。CGdiOjbect類的派生類包括:CPen類、CBrush類、CFont類、CBitmap類、CPalette類和CRgn類。
CPen類封裝了GDI畫筆對象,在Windows中,畫筆用來繪製各種線條,包括直線、曲線以及各種封閉圖形的邊框;CBrush類封裝了GDI畫刷對象,畫刷用來填充矩形、橢圓等各種封閉圖形的內部區域;CFont類封裝了GDI字體對象,字體用來顯示文本。
CBitmap類封裝了GDI位圖對象,並提供了相應的成員函數對位圖進行操作,在本書的第四章的"NewCtrl"程序中已經使用過了CBitmap類;CPalette類封裝了Windows調色板,調色板用來在用戶需要的顏色和係統能夠提供的顏色之間進行協調;CRgn類封裝了GDI區域,區域是窗口中特定的一塊,通常用來指定操作的範圍,以免幹擾其他不需要修改的區域。
在本節的例子程序中,將介紹畫筆對象和畫刷對象的使用方法。在下一章的例子程序中,介紹了一些使用字體對象進行文本輸出的知識。
在上麵的敘述中,簡要介紹了在MFC應用程序中實現屏幕輸出的CDC類和CGdiObject類(及其派生類)的一些基礎知識。在本節的例子程序中,將向用戶演示使用CDC類和CGdiObject類對象的具體方法。
5.1.2 建立新的項目
在本節中使用的"Draw"例子程序,將向用戶介紹的內容包括:如何建立和使用設備環境對象、如何創建和使用不同的畫筆和畫刷、如何響應鼠標消息等等。在"Draw"程序中,OnDraw()函數沒有承擔任何繪圖工作,所有的繪圖工作都是在相應的消息響應函數中完成的。用戶隻要理解了設備環境的工作過程,自然也就能夠在OnDraw()函數中進行繪圖工作。
在"Draw"程序中,用戶可以作出相當多的繪圖方式選擇:
* 選擇繪圖功能:繪直線、矩形或者是隨手畫;
* 選擇畫筆的線寬:1個像素寬、2個像素寬或者5個像素寬;
* 選擇畫筆的線型:實線、虛線或者點線;
* 選擇畫刷的類型:空畫刷、白色畫刷、垂直條紋畫刷或者是斜條紋的畫刷;
* 選擇畫筆和畫刷使用的顏色。
此外,"Draw"程序還提供了圖形拉伸的功能。本節將向用戶展示如何逐步實現這些功能。
建立"Draw"程序的工作與前麵的例子程序沒有什麼不同。在"File"菜單下選擇"New"命令,建立新的MFC應用程序項目"Draw"。在AppWizard中,在步驟一中選擇"單文檔界麵",選擇"英語(美國)"為應用程序資源的語言類型;在步驟二中接受缺省設置;在步驟三中清除"ActiveX Controls"選項;在步驟四中清除"Print and print preview"選項;在步驟五、六中保持缺省設置。最後,單擊"Finish"按鈕完成應用程序的設置,並生成"Draw"程序的框架代碼。
5.1.3 實現繪圖功能
對於"Draw"程序,首要的任務是實現計劃中的三種繪圖功能:畫直線、畫矩形和隨手畫。用戶或許期待著能夠繪製更多類型的圖形,如橢圓、多邊形等等,但是作為一個例子程序而言,實現這三種繪圖功能已經足夠了。用戶如果真正理解了實現這些功能的過程,其他的繪圖功能也不是什麼困難的事情,隻要調用合適的函數就可以了。
1.選擇繪圖功能
在"Draw"程序中,每次隻能選擇和使用一種繪圖功能,因此必須提供給用戶在繪圖功能中進行切換的方法。"Draw"程序通過菜單提供了選擇繪圖功能的方法。為了讓程序能夠識別當前使用的功能,"Draw"程序采用了一種比較"笨"的方法,就是對每一種繪圖功能相應地設置了一個標誌變量,在程序運行的過程中檢測相應的變量是否為"TRUE"就可以了。這有些類似於本書第三章"AlignMode"程序中選擇水平對齊方式的處理方法,雖有重複累贅之嫌,對於這種小的例子程序還是適用的。
"Draw"程序需要在"Edit"菜單和"View"菜單之間增加一個新的下拉菜單"Draw",在該下拉菜單中提供了菜單項供用戶選擇當前的繪圖功能,最後程序運行時的"Draw"菜單如圖5-1所示。
圖5-1 "Draw"菜單
表5-1中列出了各菜單項的需要設置的屬性。
表5-1 "Draw"菜單中的菜單項
命令ID
標題文本
提示信息
ID_DRAW_LINE
Line
Draw a line.
ID_DRAW_RECT
Rectangle
Draw a rectangle.
ID_DRAW_SKETCH
Sketch
Sketch as you want.
相應地,在CDrawView類中需要添加三個BOOL類型的成員變量,如下所示:
protected:
BOOL m_bLine; // 繪直線功能標誌
BOOL m_bRect; // 繪矩形功能標誌
BOOL m_bSketch; // 隨手畫功能標誌
用戶需要使用ClassWizard對"Draw"菜單下各菜單項的COMMAND消息和UPDATE_COMMAND_UI消息生成消息處理函數,在這些消息處理函數中,需要對上述標誌變量進行相應的設置。程序清單5-1中列出了這些函數的代碼,這其中還包括CDrawView類的構造函數,在構造函數中對上述標誌變量進行了初始化。
程序清單5-1 設置標誌變量
CDrawView::CDrawView()
{
// TODO: add construction code here
m_bLine=TRUE;
m_bRect=FALSE;
m_bSketch=FALSE;
}
void CDrawView::OnDrawLine()
{
// TODO: Add your command handler code here
m_bLine=TRUE;
m_bRect=FALSE;
m_bSketch=FALSE;
}
void CDrawView::OnDrawRect()
{
// TODO: Add your command handler code here
m_bLine=FALSE;
m_bRect=TRUE;
m_bSketch=FALSE;
}
void CDrawView::OnDrawSketch()
{
// TODO: Add your command handler code her
m_bLine=FALSE;
m_bRect=FALSE;
m_bSketch=TRUE;
}
void CDrawView::OnUpdateDrawLine(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
pCmdUI->SetCheck(m_bLine);
}
void CDrawView::OnUpdateDrawRect(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
pCmdUI->SetCheck(m_bRect);
}
void CDrawView::OnUpdateDrawSketch(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
pCmdUI->SetCheck(m_bSketch);
}
在程序清單5-1中的代碼都很簡單,因此沒有添加任何注釋,用戶應該可以看懂。這些函數隻是完成了設置各標誌變量的任務,真正的繪圖功能的實現還需要更多的努力。
2.實現繪直線與繪矩形功能
在很多繪圖程序中,最簡單的如Windows係統附帶的"PaintBrush"程序,都是使用鼠標進行操作的。在這些程序中,隻要選擇了相應的功能,通過鼠標的"拖曳"就可以完成任務。"Draw"程序也將使用鼠標完成繪圖操作。
用戶已經知道,一條直線需要兩點才能確定,在MFC應用程序中也不例外。設想一下具體的操作就可以發現,按下鼠標左鍵的位置應該是直線的起點,鬆開鼠標左鍵的位置則是直線的終點。因此,需要響應鼠標的WM_LBUTTONDOWN和WM_LBUTTONUP消息,並在相應的消息處理函數中記錄直線的起點和終點,完成畫線任務。
為了記錄直線的起點和終點,還需要向CDrawView類添加兩個成員變量,如下所示:
protected:
CPoint m_LastPoint; //上一點的位置
CPoint m_CurPoint; //當前點的位置
用戶可能已經注意到這兩個成員變量的名稱和注釋,並沒有使用"起點"、"終點"之類的名稱,這是因為這兩個變量還將被其他的繪圖功能所使用,而其他的功能中不一定有"起點"和"終點"這樣的說法。CPoint類的兩個成員cx和cy保存了點在x方向和y方向上的坐標。
用戶可以使用ClassWizard或者WizardBar生成對鼠標消息的處理函數,由CDrawView類接收鼠標消息,並接受缺省的函數名:OnLButtonDown()和OnLButtonUp()。程序清單5-2中列出了這兩個函數的代碼。
程序清單5-2 OnLButtonDown()和OnLButtonUp()
void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
// 記錄上一點的位置
m_LastPoint=point;
CView::OnLButtonDown(nFlags, point);
}
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
// 建立客戶區設備環境對象
CClientDC dc(this);
// 記錄當前點的位置
m_CurPoint=point;
// 繪直線
if(m_bLine)
{
dc.MoveTo(m_LastPoint);
dc.LineTo(m_CurPoint);
}
// 繪矩形
if(m_bRect)
{
dc.Rectangle(m_LastPoint.x,m_LastPoint.y,m_CurPoint.x,m_CurPoint.y);
}
CView::OnLButtonUp(nFlags, point);
}
OnLButtonDown()的參數有兩個,nFlags參數中包含了一些當前輸入設備的信息,這裏不需要;point參數中保存了WM_LBUTTONDOWN事件發生時鼠標的位置,這也正是需要保存的直線的"起點"。
OnLButtonUp()函數中,首先建立了一個客戶區設備環境對象,注意傳遞給CClientDC對象構造函數的參數"this"。在C++語言中,"this"關鍵字隻能在類、結構或聯合中使用,代表的就是調用該成員函數的對象。在這裏,OnLButtonUp()函數是CDrawView類的一個成員函數,因此,"this"代表的是一個CDrawView類對象,即應用程序的視圖。因此,最終建立的設備環境就是視圖的客戶區的設備環境,所有的繪圖操作都顯示在視圖的客戶區中。
在OnLButtonUp()函數中,接著保存了直線的"終點"。在對當前選中的繪圖功能進行判斷後,調用了CClientDC類的MoveTo()函數將設備環境的當前位置移動到直線的"起點",然後調用LineTo()函數從"起點"向"終點"畫了一條直線。這樣就實現了繪直線的功能。
繪矩形的代碼是類似的,隻不過Rectangle()函數可以直接接受兩個點的坐標作為參數,因此不需要進行當前點的移動而已。
3.實現隨手畫功能
與實現繪直線和繪矩形相比,實現隨手畫功能要複雜一點。實際上,隨手畫過程就是用一小段一小段的直線跟蹤鼠標的移動過程。試設想一下具體的操作過程:按下鼠標左鍵,隨手畫過程開始;在鼠標的移動過程中始終按下鼠標左鍵保持隨手畫過程;鬆開鼠標左鍵,結束隨手畫過程。