第六篇:映射模式
在此篇之前我們已經學會了在窗口顯示圖形,更准確的說是在窗口指定位置顯示圖形或文字,我們使用的坐標單位是象素,稱之為設備坐標。看下面語句:
pDC->Rectangle(CRect(0,0,200,200));
畫一個高和寬均為200個象素的方塊,因為采用的是默認的MM_TEXT映射模式,所以在設備環境不一樣時,畫的方塊大小也不一樣,在1024*768的顯示器上看到的方塊會比640*480的顯示器上的小(在不同分辨率下的屏幕象素,在WINDOWS程序設計一書中有示例程序可以獲得,或者可以用GetClientRect函數獲得客戶區的矩形大小。在這裡就不說了,大家只要知道就行了),在輸出到打印機時也會有類似的情況發生。如何做才能保證在不同設備上得到大小一致的方塊或者圖形、文字呢?就需要我們進行選擇模式映射,來轉換設備坐標和邏輯坐標。
Windows提供了以下幾種映射模式:
MM_TEXT
MM_LOENGLISH
MM_HIENGLISH
MM_LOMETRIC
MM_HIMETRIC
MM_TWIPS
MM_ISOTROPIC
MM_ANISOTROPIC
下面分別講講這幾種映射模式:
MM_TEXT:
默認的映射模式,把設備坐標被映射到象素。x值向右方向遞增;y值向下方向遞增。坐標原點是屏幕左上角(0,0)。但我們可以通過調用CDC的SetViewprotOrg和SetWindowOrg函數來改變坐標原點的位置看下面兩個例子:
//************************************************
// 例子6-1
void CMyView::OnDraw(CDC * pDC)
{
pDC->Rectangle(CRect(0,0,200,200));//全部采用默認畫一個寬和高為200象素的方塊
}
//**************************************************
// 例子6-2
void CMyView::OnDraw(CDC * pDC)
{
pDC->SetMapMode(MM_TEXT);//設定映射模式為MM_TEXT
pDC->SetWindowOrg(CPoint(100,100));//設定邏輯坐標原點為(100,100)
pDC->Rectangle(CRect(100,100,300,300));//畫一個寬和高為200象素的方塊
}
這兩個例子顯示出來的圖形是一樣的,都是從屏幕左上角開始的寬和高為200象素的方塊,可以看出例子2將邏輯坐標(100,100)映射到了設備坐標(0,0)處,這樣做有什麼用?滾動窗口使用的就是這種變換。
固定比例映射模式:
MM_LOENGLISH、MM_HIENGLISH、MM_LOMETRIC、MM_HIMETRIC、MM_TWIPS這一組是Windows提供的重要的固定比例映射模式。
它們都是x值向右方向遞增,y值向下遞減,並且無法改變。它們之間的區別在於比例因子見下:(我想書上P53頁肯定是印錯了,因為通過程序實驗x值向右方向也是遞增的)
MM_LOENGLISH 0.01英寸
MM_HIENGLISH 0.001英寸
MM_LOMETRIC 0.1mm
MM_HIMETRIC 0.01mm
MM_TWIPS 1/1440英寸 //應用於打印機,一個twip相當於1/20磅,一磅又相當於1/72英寸。
看例3
//**************************************************
// 例子6-3
void CMyView::OnDraw(CDC * pDC)
{
pDC->SetMapMode(MM_HIMETRIC);//設定映射模式為MM_HIMETRIC
pDC->Rectangle(CRect(0,0,4000,-4000));//畫一個寬和高為4厘米的方塊
}
還有一種是可變比例映射模式,MM_ISOTROPIC、MM_ANISOTROPIC。用這種映射模式可以做到當窗口大小發生變化時圖形的大小也會相應的發生改變,同樣當翻轉某個軸的伸展方向時圖象也會以另外一個軸為軸心進行翻轉,並且我們還可以定義任意的比例因子,怎麼樣很有用吧。
MM_ISOTROPIC、MM_ANISOTROPIC兩種映射模式的區別在於MM_ISOTROPIC模式下無論比例因子如何變化縱橫比是1:1而M_ANISOTROPIC模式則可以縱橫比獨立變化。
讓我們看例子4
//**************************************************
// 例子6-4
void CMy002View::OnDraw(CDC* pDC)
{
CRect rectClient; //
GetClientRect(rectClient);//返回客戶區矩形的大小
pDC->SetMapMode(MM_ANISOTROPIC);//設定映射模式為MM_ANISOTROPIC
pDC->SetWindowExt(1000,1000);
pDC->SetViewportExt (rectClient.right ,-rectClient.bottom );
//用SetWindowExt和SetViewportExt函數設定窗口為1000邏輯單位高和1000邏輯單位寬
pDC->SetViewportOrg(rectClient.right/2,rectClient.bottom/2 );//設定邏輯坐標原點為窗口中心
pDC->Ellipse(CRect(-500,-500,500,500));//畫一個撐滿窗口的橢圓。
// TODO: add draw code for native data here
}
怎麼樣,屏幕上有一個能跟隨窗口大小改變而改變的橢圓。把 pDC->SetMapMode(MM_ANISOTROPIC);這句改為pDC->SetMapMode(MM_ISOTROPIC)會怎樣?大家可以試試。那還有一個問題就是上例的比例因子是多少呢?看下面公式(注意是以例子4為例的)
x比例因子=rectClient.right/1000 //視窗的寬除以窗口范圍
y比例因子=-rectClient.bottom/1000 //視窗的高除以窗口范圍
從Windows的鼠標消息可以獲得鼠標指針的當前坐標值(point.x和point.y)此坐標值是設備坐標。
很多MFC庫函數尤其是CRect的成員函數只能工作在設備坐標下。
還有我們有時需要利用物理坐標,物理坐標的概念就是現實世界的實際尺寸。
設備坐標-邏輯坐標-物理坐標之間如何進行轉換便成為我們要考慮的一個問題,物理坐標和邏輯坐標是完全要我們自己來做的,但WINDOWS提供了函數來幫助我們轉換邏輯坐標和設備坐標。
CDC的LPtoDP函數可以將邏輯坐標轉換成設備坐標
CDC的DPtoLP函數可以將設備坐標轉換成邏輯坐標
下面列出我們應該在什麼時候使用什麼樣的坐標系一定要記住:
◎CDC的所有成員函數都以邏輯坐標為參數
◎CWnd的所有成員函數都以設備坐標為參數
◎區域的定義采用設備坐標
◎所有的選中測試操作應考慮使用設備坐標。
◎需要長時間使用的值用邏輯坐標或物理坐標來保存。因設備坐標會因窗口的滾動變化而改變。
用書上的例子作為以前幾篇的復習,如果你能夠獨立完成它說明前面的內容已經掌握。另外有些東西是新的,我會比較詳細的做出說明,例如客戶區、滾動窗口等。
下面我們來一步步完成例子6-5:
■第一步:用AppWizard創建MyApp6。除了Setp 1 選擇單文檔視圖和Setp 6 選擇基類為CScrollView外其余均為確省。
■第二步:在CMyApp6View類中增加m_rectEllipse和m_nColor兩個私有數據成員。你可以手工在myapp6View.h添加,不過雷神建議這樣做,在ClassView中選中CMyApp6View類,擊右鍵選擇Add
Member Variable插入它們。
//**************************
// myapp6View.h
private:
int m_nColor; //存放橢圓顏色值
CRect m_rectEllipse; //存放橢圓外接矩形
//***************************************************
問題1:CRect是什麼?
CRect是類,是從RECT結構派生的,和它類似的還有從POINT結構派生的CPoint、從SIZE派生的CSize。因此它們繼承了結構中定義的公有整數數據成員,並且由於三個類的一些操作符被重載所以可以直接在三個類之間進行類的運算。
//重載operator +
CRect operator +( POINT point ) const;
CRect operator +( LPCRECT lpRect ) const;
CRect operator +( SIZE size ) const;
//重載operator -
CRect operator -( POINT point ) const;
CRect operator -( SIZE size ) const;
CRect operator -( LPCRECT lpRect ) const;
......
更多的請在MSDN中查看
■第三步:修改由AppWizard生成的OnIntitalUpdate函數
void CMyApp6View::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize sizeTotal(20000,30000);
CSize sizePage(sizeTotal.cx /2,sizeTotal.cy /2);
CSize sizeLine(sizeTotal.cx /50,sizeTotal.cy/50);
SetScrollSizes(MM_HIMETRIC,sizeTotal,sizePage,sizeLine);//設置滾動視圖的邏輯尺寸和映射模式
}
問題2:關於void CMyApp6View::OnInitialUpdate()
函數OnInitialUpdate()是一個非常重要的虛函數,在視圖窗口完全建立後框架用的第一個函數,框架在第一次調用OnDraw前會調用它。因此這個函數是設置滾動視圖的邏輯尺寸和映射模式的最佳地點。
■第四步:編輯CMyApp6View構造函數和OnDraw函數
//*********************************************
// CMyApp6View構造函數
//
CMyApp6View::CMyApp6View():m_rectEllipse(0,0,4000,-4000)//橢圓矩形為4*4厘米。
{
m_nColor=GRAY_BRUSH;//設定刷子顏色
}
//*********************************************
// CMyApp6View的OnDraw函數
//
void CMyApp6View::OnDraw(CDC* pDC)
{
pDC->SelectStockObject (m_nColor);
pDC->Ellipse(m_rectEllipse);
}
問題3:
CMyApp6View::CMyApp6View():m_rectEllipse(0,0,4000,-4000)為什麼不能這樣寫:
CMyApp6View::CMyApp6View()
{
m_rectEllipse(0,0,4000,-4000);
m_nColor=GRAY_BRUSH;
}
我從CSDN上得到的答案:兩者實際上沒有區別。有兩個原因使得我們選擇第一種語法,它被稱為成員初始化列表:
一個原因是必須的,另一個只是出於效率考慮。
讓我們先看一下第一個原因——必要性。設想你有一個類成員,它本身是一個類或者結構,而且只有一個帶一個參數的構造函數。
class CMember {
public:
CMember(int x) { ... }
};
因為Cmember有一個顯式聲明的構造函數, 編譯器不產生一個缺省構造函數(不帶參數),所以沒有一個整數就無法創建Cmember的一個實例。
CMember* pm = new CMember(2); // OK
如果Cmember是另一個類的成員, 你怎樣初始化它呢?你必須使用成員初始化列表。
class CMyClass {
CMember m_member;
public:
CMyClass();
};
//必須使用成員初始化列表
CMyClass::CMyClass() : m_member(2)
{
}
沒有其它辦法將參數傳遞給m_member,如果成員是一個常量對象或者引用也是一樣。根據C++的規則,常量對象和引用不能被賦值, 它們只能被初始化。
第二個原因是出於效率考慮,當成員類具有一個缺省的構造函數和一個賦值操作符時。MFC的Cstring提供了一個完美的例子。假定你有一個類CmyClass具有一個Cstring類型成員m_str,你想把它初始化為"yada
yada."。你有兩種選擇:
CMyClass::CMyClass() {
// 使用賦值操作符
// CString::operator=(LPCTSTR);
m_str = _T("yada yada");
}
//使用類成員列表
// and constructor CString::CString(LPCTSTR)
CMyClass::CMyClass() : m_str(_T("yada yada"))
{
}
在它們之間有什麼不同嗎?是的。編譯器總是確保所有成員對象在構造函數體執行之前初始化,因此在第一個例子中編譯的代碼將調用CString::Cstring來初始化m_str,這在控制到達賦值語句前完成。在第二個例子中編譯器產生一個對CString::
CString(LPCTSTR)的調用並將"yada yada" 傳遞給這個函數。結果是在第一個例子中調用了兩個Cstring函數(構造函數和賦值操作符),而在第二個例子中只調用了一個函數。在Cstring的例子裡這是無所謂的,因為缺省構造函數是內聯的,Cstring只是在需要時為字符串分配內存(即,當你實際賦值時)。但是,一般而言,重復的函數調用是浪費資源的,尤其是當構造函數和賦值操作符分配內存的時候。在一些大的類裡面,你可能擁有一個構造函數和一個賦值操作符都要調用同一個負責分配大量內存空間的Init函數。在這種情況下,你必須使用初始化列表,以避免不要的分配兩次內存。在內部類型如ints或者longs或者其它沒有構造函數的類型下,在初始化列表和在構造函數體內賦值這兩種方法沒有性能上的差別。不管用那一種方法,都只會有一次賦值發生。有些程序員說你應該總是用初始化列表以保持良好習慣,但我從沒有發現根據需要在這兩種方法之間轉換有什麼困難。在編程風格上,我傾向於在主體中使用賦值,因為有更多的空間用來格式化和添加注釋,你可以寫出這樣的語句:x=y=z=0;
或者memset(this,0,sizeof(this)); 注意第二個片斷絕對是非面向對象的。
當我考慮初始化列表的問題時,有一個奇怪的特性我應該警告你,它是關於 C++初始化類成員的,它們是按照聲明的順序初始化的,而不是按照出現在初始化列表中的順序。
class CMyClass {
CMyClass(int x, int y);
int m_x;
int m_y;
};
CMyClass::CMyClass(int i) : m_y(i), m_x(m_y)
{
}
你可能以為上面的代碼將會首先做m_y=I,然後做m_x=m_y,最後它們有相同的值。但是編譯器先初始化m_x,然後是m_y,因為它們是按這樣的順序聲明的。結果是m_x將有一個不可預測的值。
我的例子設計來說明這一點,然而這種bug會更加自然的出現。有兩種方法避免它, 一個是總是按照你希望它們被初始化的順序聲明成員,第二個是,如果你決定使用初始化列表,總是按照它們聲明的順序羅列這些成員。這將有助於消除混淆。
■第五步:映射WM_LBUTTONDOWN消息並編輯OnLButtonDown消息處理函數。在Class Wizard中選擇CMyApp6View類,在Message列表中選擇WM_LBUTTONDOWN雙擊,則此消息映射便完成了。用下面代碼替換Wizard生成的OnLButtonDown消息處理函數。
void CMyApp6View::OnLButtonDown(UINT nFlags, CPoint point)
{
CClientDC dc(this);
OnPrepareDC(&dc);
CRect rectDevice = m_rectEllipse;
dc.LPtoDP(rectDevice);
if (rectDevice.PtInRect(point)) {
if (m_nColor == GRAY_BRUSH) {
m_nColor = WHITE_BRUSH;
}
else{
m_nColor = GRAY_BRUSH;
}
InvalidateRect(rectDevice);
}
}
問題4:詳解此段代碼
第1行 CClientDC由CDC派生,它的對象dc是當前窗口的客戶區域
第2行 OnPrepareDC是在OnDraw函數前調用的。
第3行 將m_rectEllipse賦給rectDevice矩形區域
第4行 將矩形區域的邏輯坐標轉為設備坐標,LPtoDP是CDC類的成員函數,且是多態的,函數聲明如下:
void LPtoDP( LPPOINT lpPoints, int nCount = 1 ) const;
void LPtoDP( LPRECT lpRect ) const;
void LPtoDP( LPSIZE lpSize ) const;
第5-11行 CRect的成員函數PtInRect(point)用來判斷鼠標當前位置(point)是否在當前矩形(rectDevice)內
第12行 InvalidateRect函數可以觸發WM_PAINT消息,改消息又被映射,引起調用OnDraw調用。
■第六步:映射WM_KEYDOWN消息並編輯OnKeyDown消息處理函數。在Class Wizard中選擇CMyApp6View類,在Message列表中選擇WM_KEYDOWN雙擊。用下面代碼替換Wizard生成的OnKeyDown消息處理函數。
void CMyApp6View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
switch (nChar) {
case VK_HOME:
OnVScroll(SB_TOP, 0, NULL);
OnHScroll(SB_LEFT, 0, NULL);
break;
case VK_END:
OnVScroll(SB_BOTTOM, 0, NULL);
OnHScroll(SB_RIGHT, 0, NULL);
break;
case VK_UP:
OnVScroll(SB_LINEUP, 0, NULL);
break;
case VK_DOWN:
OnVScroll(SB_LINEDOWN, 0, NULL);
break;
case VK_PRIOR:
OnVScroll(SB_PAGEUP, 0, NULL);
break;
case VK_NEXT:
OnVScroll(SB_PAGEDOWN, 0, NULL);
break;
case VK_LEFT:
OnHScroll(SB_LINELEFT, 0, NULL);
break;
case VK_RIGHT:
OnHScroll(SB_LINERIGHT, 0, NULL);
break;
default:
break;
}
}
問題5:此段代碼詳解:
先看OnVScroll和OnHScroll的函數原型
afx_msg void OnVScroll( UINT nSBCode, UINT nPos, CScrollBar* pScrollBar
);
afx_msg void OnHScroll( UINT nSBCode, UINT nPos, CScrollBar* pScrollBar
);
主要參數 nSBCode是指滾動條移動方向。
再看OnKeyDown函數原型
afx_msg void OnKeyDown( UINT nChar, UINT nRepCnt, UINT nFlags );
主要參數nChar是指Virtual Keys code 虛擬鍵碼你可以在winuser.h文件中看到更多,這裡只列出很小一部分。
#define VK_ESCAPE 0x1B
#define VK_SPACE 0x20
#define VK_PRIOR 0x21
#define VK_NEXT 0x22
#define VK_END 0x23
#define VK_HOME 0x24
......
/* VK_0 thru VK_9 are the same as ASCII '0' thru '9' (0x30 - 0x39) */
/* VK_A thru VK_Z are the same as ASCII 'A' thru 'Z' (0x41 - 0x5A) */
#define VK_LWIN 0x5B
......
編譯運行它,怎麼樣成功了吧,
■第七步:做一個更復雜的程序,例如屏幕上有多個圓,然後點其中一個,則點中的變色,其他的不變。
未完待續