座標空間和轉換,圖形的保存和重繪,元文件

🔳🔳 繪製線條 、畫刷繪圖、繪製連續線條、繪製扇形效果的線條


🔳🔳 插入符【文本插入符|圖形插入符】、窗口重繪、路徑、字符輸入【設置字體|字幕變色】


🔳🔳 菜單命令響應函數、菜單命令的路由、基本菜單操作、動態菜單操作、電話本實例


🔳🔳 對話框的創建與顯示、動態創建按鈕、控件的訪問【控件調整|靜態文本控件|編輯框控件】、對話框伸縮功能、輸入焦點的傳遞、默認按鈕的說明


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


🔳🔳 在對話框程序中讓對話框捕獲WM_KEYDOWN消息


🔳🔳 修改應用程序窗口的外觀【窗口光標|圖標|背景】、模擬動畫圖標、工具欄編程、狀態欄編程、進度欄編程、在狀態欄上顯示鼠標當前位置、啓動畫面


🔳🔳 設置對話框、顏色對話框、字體對話框、示例對話框、改變對話框和控件的背景及文本顏色、位圖顯示


一、座標空間和轉換

Microsoft Windows下的程序運用座標空間和轉換來對圖形輸出進行縮放、旋轉、平移、斜切和反射。

1.1 座標空間

一個座標空間是一個平面空間,通過使用兩個相互垂直並且長度相等的軸來定位二維空間:

Win32應用程序編程接口(API)使用四種座標空間:世界座標系空間、頁面空間、設備空間和物理設備空間。應用程序運用世界座標系空間對圖形輸出進行旋轉、斜切或者反射。Win32 API把世界座標系空間和頁面空間稱爲邏輯空間。最後一種座標空間(物理設備空間)通常指應用程序窗口的客戶區,但是它也包括整個桌面、完整的窗口(包括框架、標題欄和菜單欄)或打印機的一頁或繪圖儀的一頁紙。物理設備的尺寸隨顯示器、打印記或者繪圖儀所設置的尺寸而改變。

1.2 轉換

如果要在物理設備上繪製輸出, Windows就把一個矩形區域從一個座標空間複製到(或映射到)另一個座標空間,直至最終完整的輸出呈現在物理設備(通常是屏幕或打印機)上。

如果應用程序調用了SetWorldTransform函數,那麼映射就從應用程序的世界座標系空間開始;否則,映射在頁面空間開始進行。

在Windows把矩形區域的每一點從一個空間複製到另一個空間時,它採用了一種被稱作轉換的算法,轉換是把對象從一個座標空間複製到另一個座標空間時改變(或轉變)這一對象的大小、方位和形態,儘管轉換把對象看成一個整體,但它也作用於對象中的每一點或每條線。

在實際繪圖時,世界座標系空間中的一個區域要先被映射到頁面空間,然後再由頁面空間映射到設備空間。對設備空間來說,通常它的左上角是座標點(0,0),向右是x增加的方向,向下是y增加的方向。然後再由設備空間映射到物理設備空間(通常就是屏幕),於是,圖形就在計算機的屏幕上顯示出來了。我們平時在開發程序時,如果需要把圖形的某個局部區域放大,就可以利用世界座標系和轉換來完成。

實際編程中,主要處理的是從頁面空間到設備空間的轉換。

  1. 頁面空間到設備空間的轉換
      頁面空間(也稱爲邏輯空間)到設備空間的轉換是原Windows接口的一部分。這種轉換確定與一個特定設備描述表相關的所有圖形輸出的映射方式。所謂映射方式是指確定用於繪圖操作的單位大小的一種量度轉換。也就是說,設定的映射方式主要是確定應該如何將頁面空間的一個座標點轉換爲設備空間中的一個設備座標點。

    映射方式是一種影響幾乎任何客戶區繪圖的設備環境屬性。另外還有四種設備環境屬性:窗口原點、視口原點、窗口範圍和視口範圍,這四種屬性與映射方式密切相關。

    • 窗口是基於邏輯座標的,邏輯座標可以是像素、毫米、英寸等單位,
    • 視口是基於設備座標(像素)的。通常,視口和客戶區相同。

    頁面空間到設備空間的轉換所用的是兩個矩形的寬與高的比率(通常將其稱爲轉換因子),其中頁面空間中的矩形被稱爲窗口,設備空間中的矩形被稱爲視口, Windows把窗口原點映射到視口原點,把窗口範圍映射到視口範圍,就完成了這種轉換。該轉換過程: 在這裏插入圖片描述

  2. 設備空間到物理空間的轉換
      設備空間到物理空間的轉換有幾個獨特之處:它只限於平移,並由Windows的窗口管理部分控制。這種轉換的惟一用途是確保設備空間的原點被映射到物理設備上的適當點上。沒有函數能設置這種轉換,也沒有函數可以獲取有關數據。也就是說從設備空間到物理空間的轉換是由Windows控制的,程序員是沒有辦法去設置這種轉換的。因此,通常不考慮這種轉換。

  3. 默認轉換
      一旦應用程序建立了設備描述表,並調用GDI繪圖或輸出函數,則運用默認頁面空間到設備空間的轉換和設備空間到客戶區的轉換(再次強調:在應用程序調用SetWorldTransform函數之前,不會出現世界座標空間到頁面空間的轉換)。

      默認頁面空間到設備空間的轉換結果是一對一的映射,即頁面空間上給出的一個點映射到設備空間的一個點。正如前文講到的,這種轉換沒有以矩陣指定,而是通過把視口寬除以窗口寬,把視口高除以窗口高而得出的轉換因子來完成。在默認的情況下,視口尺寸爲1x1個像素,窗口尺寸爲1x1頁單位。

      設備空間到物理設備(客戶區、桌面或打印機)的轉換結果總是一對一的,即設備空間的一個單位總是與客戶區、桌面或者打印機上的一個單位相對應。這一轉換的惟一用途是平移。無論窗口移到桌面的什麼位置,它永遠確保輸出能夠正確無誤地出現在窗口上。正因爲設備空間到物理設備的轉換結果總是一對一的,所以,通常也把設備空間看作是客戶區,而程序實現時主要考慮的還是頁面空間到設備空間的轉換。

      默認轉換的一個獨特之處是設備空間和應用程序窗口的y軸方向。在默認的狀態下,y軸正向朝下,負y方向朝上。

1.3 邏輯座標和設備座標

邏輯座標和設備座標幾乎在所有GDI函數中使用的座標值都採用是的邏輯單位。Windows必須將邏輯單位轉換爲“設備單位”,即像素。這種轉換是由映射方式、窗口和視口的原點,以及窗口和視口的範圍所控制的。

例如dc->TextOut(0,100," text ");該函數的參數值(0,100)採用的就是邏輯單位,當程序運行後,在窗口中真正顯示文本時,該參數值需要被轉換爲設備單位,而且轉換的結果由映射方式、窗口和視口的原點,以及窗口和視口的範圍控制。

Windows對所有的消息(如WM_SIZE,WM_MOUSEMOVE、WM_LBUTTONDOWN、WM_LBUTTONUP),所有的非GDI函數和一些GDI函數(例如GetDeviceCaps函數),永遠使用設備座標。

  1. 映射模式
    Windows提供的映射模式如下表所示。默認映射模式爲MM_TEXT,在此映射模式下,邏輯單位和設備單位相同,這時將邏輯值(0,100)轉換爲設備座標後,它的值仍是(0,100)。映射模式的改變可以通過SetMapMode函數來實現。

  2. 邏輯座標和設備座標的相互轉換
    對於邏輯座標和設備座標之間的相互轉換,可以利用下面的公式完成。
    💞💞 窗口(邏輯)座標轉換爲視口(設備)座標的兩個公式

    xViewport=(xWindow-xWinOrg)*xViewExt/xWinExt+xViewOrg
    yViewport=(yWindow-yWinOrg)*yViewExt/yWinExt+yViewOrg
    

    💞💞 視口(設備)座標轉換爲窗口(邏輯)座標的兩個公式

    xWindow=(xViewPort-xViewOrg)*xWinExt/xViewExt+xWinOrg
    yWindow=(yViewPort-yViewOrg)*yWinExt/yViewExt+yWinOrg
    

    💞💞**在MM_TEXT映射方式下邏輯座標和設備座標的相互轉換**
    因爲在MM_TEXT映射方式下,邏輯單位和設備單位是一樣的,而且它們的窗口和視口的範圍都是1x1的,相當於它們的轉換因子就是1,因此:
    ◻◾◻ 窗口(邏輯)座標轉換爲視口(設備)座標的兩個公式爲:

    xViewport= xWindow-xWinOrg+xViewOrg
    yViewport= yWindow-yWinOrg+yViewOrg
    

    ◻◾◻ 視口(設備)座標轉換爲窗口(邏輯)座標的兩個公式爲:

    xWindow = xViewport-xViewOrg+xWinOrg
    yWindow = yViewport-yViewOrg+yWinOrg
    

    而通過消息,例如鼠標左鍵單擊消息得到的座標點是以設備座標爲單位,即以像素爲單位的值。因爲默認映射模式是MM_TEXT,所以邏輯單位和設備單位是一樣的,因此,在前面章節的程序中我們沒有顯式地進行座標點的轉換,而是直接使用得到的設備座標調用GDI函數進行了圖形的繪製。

  3. 視口和窗口原點的改變
    CDC中提供了兩個成員函數: SetViewportOrgSetWindowOrg用來改變視口和窗口的原點。如果將視口原點設置爲(xViewOrg,yViewOrg),則邏輯點(0,0)就會被映射爲設備點(xViewOrg,yViewOrg)。如果將窗口原點改變爲(xWinOrg,yWinOrg),則邏輯點(xWinOrg,yWinOrg)將會被映射爲設備點(0,0),即設備客戶區的左上角。注意:不管對窗口和視口原點如何改變,設備點(0,0)始終是客戶區的左上角

二、圖形的保存和重繪

在已有程序(Graphic)的基礎上繼續添加圖形的保存和重繪功能,爲了使程序演示效果更好,首先將已有的Graphic程序的窗口恢復爲默認的白色背景,也就是將CGraphicView類的OnEraseBkgnd函數中顯示位圖的代碼註釋起來。

然後運行一下Graphic程序,並利用相應菜單命令在窗口中繪製一些圖形,當窗口尺寸發生變化時,將會發現窗口中繪製的圖形都消失了。這是因爲當窗口尺寸發生變化時,引起窗口重繪,會發送WM_PAINT消息,這時首先會擦除窗口的背景,然後再進行重繪操作,這樣就把窗口中先前繪製的圖形擦除掉了。如果希望所繪製的圖形始終在窗口中呈現出來,就需要將這些圖形保存起來,然後當窗口尺寸發生變化引起窗口重繪時,將這些圖形再次在窗口中輸出

當窗口重繪時總是會調用程序視類的OnDraw函數,因此可以在該函數中完成圖形的輸出。而保存圖形的方式有多種,對於本例所繪製的圖形來說,有三個要素:起點、終點和繪製的類型(點、線、矩形或橢圓)。也就是說,對本例所繪製的每一個圖形,只需要保存這三個要素就可以了。當窗口重繪時,在CGraphicView類的OnDraw函數中,根據每一個已保存的圖形的繪製類型,利用其起點和終點將該圖形在窗口中重新輸出。由於這三個要素的數據類型不同,而在C++中用結構體來保存不同類型的對象是比較合適的。在C++中,結構體就是一個類,因此本例也可以利用一個類來保存圖形的這三個要素,這比較符合面向對象的思想。

於是,爲Graphic程序添加一個新類:CGraph。新增類不是MFC類,是一般的類。
在這裏插入圖片描述
然後爲CGraph這個新類增加三個成員變量,三個變量的訪問權限都設置爲public類型,因爲在後面的程序中其他類需要訪問它們。


爲了能夠方便地對這三個新增加的變量進行賦值,再爲CGraph類提供一個帶參數的構造函數,允許用戶在構造CGraph類的對象時,直接通過參數給這三個成員變量賦值。

CGraph.h
#pragma once
class CGraph
{
public:
	// 繪製類型
	UINT m_nDrawType;
	// 起點
	CPoint m_ptOrigin;
	// 終點
	CPoint m_ptEnd;
	CGraph(UINT m_nDrawType, CPoint m_ptOrigin, CPoint m_ptEnd);

};

CGraph.cpp
#include "pch.h"
#include "CGraph.h"

CGraph::CGraph(UINT m_nDrawType, CPoint m_ptOrigin, CPoint m_ptEnd)
{
	this->m_nDrawType = m_nDrawType;
	this->m_ptOrigin = m_ptOrigin;
	this->m_ptEnd = m_ptEnd;
}

在程序中,通過CGraph類就可以構造相應的對象來保存圖形的三個要素。因爲在繪圖時可能會繪製多個圖形,所以必須爲每一個圖形創建一個相應的CGraph對象,以保存該圖形的三個要素。可以採用數組來保存這些創建的CGraph對象,但是這樣做將會非常不方便,因爲數組有一個缺點,一旦定義之後,就只能存儲一定容量的元素

用戶每次繪製的圖形個數是不定的,需要創建的CGraph對象的個數也是不定的,因此應該採用一種動態的存儲結構來保存這些CGraph對象。本例將使用MFC提供的一個集合類來完成這一任務。這裏可以使用鏈表來保存這些對象,但其實現比較複雜

2.1 集合類 CPtrArray

◾◾◾ MFC提供的一個集合類:CStringArray,它可以用來存儲CString類型的對象,而且它的容量是可以動態增加的。
◾◾◾ 再介紹一個集合類:CPtrArray。它支持void類型的指針數組,該類的成員函數與CObArray類的相應函數類似,只是CObArray類的成員函數中使用CObject指針作爲參數或返回值類型的地方,在CPtrArray類中都用void類型的指針代替。因此在程序中,可以利用CPtrArray對象存儲多個對象的地址:

  • 如果想要增加一個成員,可以調用其Add方法,來增加一個void指針所指向的對象;
  • 如果想取得這個集合類中的某個元素,可以調用其GetAt方法;
  • 如果想獲得這個集合類的元素數目,可以調用其GetSize方法。

1)下面首先爲Graphic程序的CGraphicView類增加一個CPtrArray類型的私有成員變量:m_ptrArray。

2)在GraphicView.cpp文件的前部將CGraph類的定義包含進來:

#include "CGraph.h"

3)然後在每次繪圖操作結束後,就構造一個相應的CGraph對象,並將該對象的地址保存到m_ptrArray對象中:

void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息處理程序代碼和/或調用默認值
	CClientDC dc(this);
	//改變畫筆顏色
	//CPen pen(PS_SOLID, 5, RGB(150, 140, 32));
	CPen pen(m_nLineStyle, m_nLineWidth, m_clr);
	dc.SelectObject(&pen);
	//設置畫刷透明
	CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
	dc.SelectObject(pBrush);
	switch (m_nDrawType)
	{
	case 1:
		dc.SetPixel(point,m_clr);
		break;
	case 2:
		dc.MoveTo(m_ptOrigin);
		dc.LineTo(point);
		break;
	case 3:
		dc.Rectangle(CRect(m_ptOrigin, point));
		break;
	case 4:
		dc.Ellipse(CRect(m_ptOrigin, point));
		break;
	}
	//m_nDrawType表示所畫的對象類型1:點,2:線,3:矩形,4:橢圓
	CGraph graph(m_nDrawType, m_ptOrigin, point);
	m_ptrArray.Add(&graph);
	CView::OnLButtonUp(nFlags, point);
}

4)在OnDraw函數中將保存的圖形元素再次顯示出來:


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

	// TODO: 在此處爲本機數據添加繪製代碼
	CFont* pOldFont = pDC->SelectObject(&m_font);
	pDC->TextOutW(0, 0, m_strFontName);
	pDC->SelectObject(pOldFont);
	/*
	//第一步:
	//首先構建位圖對象: bitmap
	CBitmap bitmap;
	//加載圖像
	bitmap.LoadBitmap(IDB_BITMAP1);

	BITMAP bmp;
	bitmap.GetBitmap(&bmp);


	//第二步:創建兼容的DC
	//創建與當前DC (pDC)兼容的DC: dcCompatible
	CDC dcCompatible;
	dcCompatible.CreateCompatibleDC(pDC);

	//第三步:將位圖選入兼容DC中
	//調用SelectObject函數將位圖選入兼容DC中,從而確定兼容DC顯示錶面的大小
	dcCompatible.SelectObject(&bitmap);

	//第四步:將兼容DC (dcCompatible)中的位圖複製到目的DC (pDC)中
	//因爲要指定複製目的矩形區域的寬度和高度,首先需要得到目的DC客戶區大小,所以就構造一個CRect對象
	CRect rect;
	//然後調用GetClientRect函數得到客戶區大小
	GetClientRect(&rect);

	//源DC就是先前創建的兼容DC,
	//複製模式選擇: SRCCOPY:就是將源位圖複製到目的矩形區域中
	//pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0, SRCCOPY);
	 pDC->StretchBlt(0, 0, rect.Width(), rect.Height(), &dcCompatible, 0, 0, bmp.bmWidth, bmp.bmHeight, SRCCOPY);
	 */

	for (int i = 0; i < m_ptrArray.GetSize(); i++)
	{
		CGraph* cGraph = (CGraph*)m_ptrArray.GetAt(i);
		switch (cGraph->m_nDrawType)
		{
		case 1:
			pDC->SetPixel(cGraph->m_ptEnd,RGB(0,0,0));
			break;
		case 2:
			pDC->MoveTo(cGraph->m_ptOrigin);
			pDC->LineTo(cGraph->m_ptEnd);
			break;
		case 3:
			pDC->Rectangle(CRect(cGraph->m_ptOrigin, cGraph->m_ptEnd));
			break;
		case 4:
			pDC->Ellipse(CRect(cGraph->m_ptOrigin, cGraph->m_ptEnd));
			break;
		}
	}
}

運行,當點擊程序界面的最小化按鈕後,再次點擊程序,程序再次出現時所畫圖形再次消失:

📋📋 原因:CGraphicView類的OnLButtonUp函數中,定義的CGraph類型的對象:graph是一個局部變量,當調用CPtrArray類的Add方法後,這個局部對象的地址就被保存到m_ptrArray集合類對象中,這一過程如圖:

當OnLButtonUp函數執行完成之後,graph這個局部對象就會發生析構,其所佔內存就被回收了,也就是說該graph對象在內存中就不存在了。雖然這時在m_ptrArray集合對象中仍保存了這個graph對象先前在內存中的地址,但是這個對象本身已經不存在了。在OnDraw函數中, GetAt函數實際上是從m_ptrArray集合對象中取出其所保存的地址,但是原先在這個地址處的對象已經不存在了,因此得到的並不是先前保存的那個graph對象。

🟢🟢 解決:爲了解決這一問題,應該在CGraphicView類OnLButtonUp函數中,把定義的CGraph類型的對象修改爲CGraph指針類型的變量:

void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息處理程序代碼和/或調用默認值
	CClientDC dc(this);
	//改變畫筆顏色
	//CPen pen(PS_SOLID, 5, RGB(150, 140, 32));
	CPen pen(m_nLineStyle, m_nLineWidth, m_clr);
	dc.SelectObject(&pen);
	//設置畫刷透明
	CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
	dc.SelectObject(pBrush);
	switch (m_nDrawType)
	{
	case 1:
		dc.SetPixel(point,m_clr);
		break;
	case 2:
		dc.MoveTo(m_ptOrigin);
		dc.LineTo(point);
		break;
	case 3:
		dc.Rectangle(CRect(m_ptOrigin, point));
		break;
	case 4:
		dc.Ellipse(CRect(m_ptOrigin, point));
		break;
	}
	//m_nDrawType表示所畫的對象類型1:點,2:線,3:矩形,4:橢圓
	CGraph *graph=new CGraph(m_nDrawType, m_ptOrigin, point);
	m_ptrArray.Add(graph);
	CView::OnLButtonUp(nFlags, point);
}

因爲pGraph變量是在OnLButtonUp函數中定義的,所以它也是一個局部變量,系統將在Graphic程序的棧中爲該變量分配一個內存。這時的內存示意圖如下:

圖中假定pGraph變量所在內存地址爲: 0x0012fbc4。然後利用new操作符構造一個CGraph對象,並將該對象賦給pGraph這個變量。凡是用new分配內存的對象均是在堆中定義的。也就是說現在pGraph這個變量就保存了CGraph對象在堆中內存的首地址:0x00346708,已經被保存到集合類的對象m_ptrArray中了,所以此時任然可以通過這個地址索引到相應的CGraph對象。


再次運行最小化後再重現圖形就不會消失。

2.2 OnPaint與OnDraw

前面已經提過OnDraw函數是一個虛函數,另外,在窗口重繪時會發送一個WM_PAINT消息,如果想讓圖形在窗口中始終都能顯示出來,就可以將圖形的繪製操作放置在該消息的響應函數(OnPaint)中

而OnDraw函數並不是WM_PAINT消息的響應函數,爲什麼它會在窗口重繪過程中被調用?
  OnPaint函數中調用OnDraw函數

三、窗口滾動功能的實現

3.1 CScrollView類

若爲程序增加窗口滾動的能力,可以將視類的基類選擇爲:CScrollView。視圖窗口就具有滾動功能,當圖形在窗口中不能完整顯示時,可以通過拖動滾動條來瀏覽整個窗口中的內容。

1、創建項目後添加滾動功能

Graphic程序已經生成了,如果要爲其增加窗口滾動的能力,可以手工將該程序的視類的基類由CView修改爲CScrollView,這需要修改源程序中幾處內容:

  1. 在CGraphicView類頭文件中只有一處需要修改,即該類的定義處,將Cview修改爲CScrollview即可,讓CGraphicView類從後者派生。
  2. 但在CGraphicView類的源文件中有多處需要修改爲了避免遺漏,可以利用查找替換菜單命令進行替換。將GraphView.cpp文件中所有出現CView的地方都替換爲CScrollView了。
    在這裏插入圖片描述

現在編譯通過,但是運行時出現非法操作錯誤:

📋📋 原因:這是因爲對滾動窗口來說,在初始創建時,需要進行一些設置,包括整個滾動窗口的大小,以及當單擊滾動條箭頭時滾動條滾動的數值和單擊滾動欄時滾動條滾動的數值。要進行這些設置,需要調用CScrollView類的成員函數: SetScrollSizes,該函數的聲明:

void SetScrollSizes(int nMapMode, SIZE sizeTotal, const SIZE& sizePage = sizeDefault, const SIZE& sizeLine = sizeDefault);

SetScrollSizes函數的作用是設置滾動窗口的大小,有四個參數,其中後面兩個參數都有默認值。
◼ nMapMode
指定映射模式,其取值可以是表11.1所列值之一。
◼ sizeTotal
設置滾動視圖窗口總的尺寸。
◼ sizePage
設置響應鼠標單擊滾動條的軸時水平和垂直方向滾動的量。
◼ sizeLine
設置響應鼠標單擊滾動箭頭時水平和垂直方向滾動的量。

1)因爲該函數的後兩個參數都有默認值,所以在調用時可以只爲其傳遞前兩個參數的值。在視類窗口創建之後再調用SetScrollSizes函數。爲CGraphicView類重載一個虛函數: OnInitialUpdate。該函數是在窗口完全創建完成之後第一個調用的函數,也就是說,該函數在第一次調用OnDraw函數之前調用

2)在函數中對窗口進行一些初始化工作,本例就是設置滾動窗口的初始尺寸。

void CGraphicView::OnInitialUpdate()
{
	CScrollView::OnInitialUpdate();
	SetScrollSizes(MM_TEXT, CSize(1400, 800));
	// TODO: 在此添加專用代碼和/或調用基類
}

運行程序:

3.2 圖形錯位現象

Graphic程序這時的窗口滾動功能還不完備。先把垂直滾動條拖動到窗口的最下端後,再在靠近窗口底部的位置繪製一個圖形,然後切換到其他程序窗口,再切換回來,這時會發現在程序窗口中剛纔在底端繪製的圖形出現在了原位置的上方。當窗口發生切換時,窗口會發生重繪,也就是在OnDraw函數中將重新繪製圖形。

先前CGraphicView類以CView類爲基類時,窗口中繪製的圖形無論窗口如何切換或者尺寸如何變化,都能正確顯示,但爲什麼增加滾動功能後窗口中的內容就不能正確地顯示呢?

根據前面的知識,調用GDI函數繪圖時使用的是邏輯座標,而Windows需要把其轉換爲設備座標,然後輸出圖形。

📝📝原因:

在調用OnDraw函數之前,OnPaint函數調用OnPrepareDC函數來調整顯示上下文的屬性。因此猜想可能就是在此函數中調整了顯示上下文的屬性,從而導致先前的現象,即圖形跑到原位置的上方顯示了。

🔳🔳 解決:
因爲每次窗口重繪時,都會調用OnPrepareDC函數,而OnPrepareDC會隨時根據滾動窗口的位置來調整視口的原點。也就是說,視口的原點不是一成不變的,它會隨着滾動條的位置不同而變化。

void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息處理程序代碼和/或調用默認值
	CClientDC dc(this);
	//改變畫筆顏色
	CPen pen(m_nLineStyle, m_nLineWidth, m_clr);
	dc.SelectObject(&pen);
	//設置畫刷透明
	CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
	dc.SelectObject(pBrush);
	switch (m_nDrawType)
	{
	case 1:
		dc.SetPixel(point,m_clr);
		break;
	case 2:
		dc.MoveTo(m_ptOrigin);
		dc.LineTo(point);
		break;
	case 3:
		dc.Rectangle(CRect(m_ptOrigin, point));
		break;
	case 4:
		dc.Ellipse(CRect(m_ptOrigin, point));
		break;
	}
	
	//OnPrepareDC會隨時根據滾動窗口的位置來調整視口的原點
	OnPrepareDC(&dc);
	dc.DPtoLP(&m_ptOrigin);
	dc.DPtoLP(&point);
    
    //m_nDrawType表示所畫的對象類型1:點,2:線,3:矩形,4:橢圓
	CGraph *graph=new CGraph(m_nDrawType, m_ptOrigin, point);
	m_ptrArray.Add(graph);
	CScrollView::OnLButtonUp(nFlags, point);
}

我們在繪圖時使用的都是邏輯座標,也就是說,我們都是在頁面空間,即邏輯空間中進行繪圖操作的。但是這些圖形實際上都要被映射到設備空間中,因此座標值必須要被轉換爲設備座標,而這種轉換不僅由映射模式,還由窗口原點和視口原點、窗口範圍和視口範圍來約束。在MM_TEXT映射模式下,因爲邏輯座標單位和設備座標單位都是像素,所以相對來說轉換比較簡單,但是這時的轉換仍會受到窗口原點和視口原點的影響。

四、元文件

利用元文件實現圖形的保存和重繪。

4.1 元文件的使用

1. 元文件的介紹

利用元文件設備上下文類:CMetaFileDC,該類的派生層次結構如下:

CMetaFileDC類是從CDC類派生的。一個Windows元文件DC包含了一系列圖形設備接口命令,在程序中可以重放這些命令,以便創建所需的圖形或文本。

爲了更好地理解元文件DC的作用,可以進行這樣的一個比喻:準備一塊畫布來繪製圖形,這塊畫布就相當於元文件DC,然後在這塊畫布上繪製各種各樣的圖形,但是這些圖形並沒有被別人所看到。畫好之後,如果有人想看,就可以隨時打開這塊畫布,讓他們參觀,也就是說可以多次重複地打開同一塊畫布以展示上面的內容。

元文件的工作原理與此類似,它包含的實際上是一系列圖形設備接口命令,例如繪製一條直線、繪製一個橢圓、輸出一行文本,但這時繪製的圖形是看不到的,它們存在於元文件中,實際上是在內存中繪製的。當在元文件中繪製完成之後,可以播放該元文件,這時就可以在窗口中看到先前在該文件中繪製的圖形了。

要注意的是,元文件並沒有包含所繪圖形的圖形數據,它包含的是圖形的繪製命令

我們可以通過以下步驟來使用Windows元文件:
✨✨ 1. 利用CMetaFileDC構造函數構造一個元文件DC對象,然後調用該類的Create成員函數創建一個Windows元文件設備上下文,並將其與已構造的CMetaFileDC對象關聯起來。

該Create函數的聲明如下所示:

BOOL create( LPCTSTR 1pszFilename = NULL );

lpszFilename:它是以null爲結尾的字符串,指定要創建的元文件的文件名。如果此參數爲NULL,創建的元文件就是一個內存元文件。

✨✨ 2. 給已創建的元文件DC對象發送一系列的GDI命令,例如MoveTo或LineTo等。

✨✨ 3. 在給元文件DC對象發送了需要的命令之後,調用Close成員函數關閉元文件設備上下文,返回元文件句柄(HMETAFILE類型)。CMetaFileDC類的Close成員函數的聲明如下所示:

HMETAFILE close();

✨✨ 4. 以得到的元文件句柄爲參數,利用CDC類的PlayMetaFile成員函數播放該元文件。PlayMetaFile函數的聲明如下所示:

BOOL PlayMetaFile( HMETAFILE hMF );

✨✨ 5. 播放完其中的圖形繪製命令之後,就不再需要該元文件了。因爲元文件也是一種資源,所以在使用結束後,也需要釋放,這可以通過調用DeleteMetaFile函數將其刪除。該函數的聲明形式如下所示:

BOOL DeleteMetaFile (HMETAFILE hmf);
2. 元文件的使用舉例

在Graphic程序中利用元文件來保存圖形,並在OnDraw函數中播放該元文件。

🔶🔸 1. 爲CGraphicView類添加一個CMetaFileDC類型的私有成員變量: m_dcMetaFile

然後在該類的構造函數中調用CMetaFileDC類的Create方法創建一個內存元文件DC。

m_dcMetaFile.Create();

🔶🔸 2. 在CGraphic View類的OnLButtonUp函數中,將已有的設備上下文對象都換成元文件設備上下文對象,即把該函數中的CDC類型的對象dc都替換爲CMetaFileDC類型的對象: m_dcMetaFile。並將該函數中先前編寫的將圖形對象保存到集合類對象中的代碼註釋起來。

void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息處理程序代碼和/或調用默認值
	CClientDC dc(this);
	//改變畫筆顏色
	CPen pen(m_nLineStyle, m_nLineWidth, m_clr);
	//dc.SelectObject(&pen);
	m_dcMetaFile.SelectObject(&pen);
	//設置畫刷透明
	CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
	//dc.SelectObject(pBrush);
	m_dcMetaFile.SelectObject(pBrush);
	switch (m_nDrawType)
	{
	case 1:
		//dc.SetPixel(point,m_clr);
		m_dcMetaFile.SetPixel(point, m_clr);
		break;
	case 2:
		//dc.MoveTo(m_ptOrigin);
		//dc.LineTo(point);
		m_dcMetaFile.MoveTo(m_ptOrigin);
		m_dcMetaFile.LineTo(point);
		break;
	case 3:
		//dc.Rectangle(CRect(m_ptOrigin, point));
		m_dcMetaFile.Rectangle(CRect(m_ptOrigin, point));
		break;
	case 4:
		//dc.Ellipse(CRect(m_ptOrigin, point));
		m_dcMetaFile.Ellipse(CRect(m_ptOrigin, point));
		break;
	}
	
	/*
	OnPrepareDC(&dc);
	//dc.DPtoLP(&m_ptOrigin);
	m_dcMetaFile.DPtoLP(&m_ptOrigin);
	//dc.DPtoLP(&point);
	m_dcMetaFile.DPtoLP(&point);

	//m_nDrawType表示所畫的對象類型1:點,2:線,3:矩形,4:橢圓
	CGraph *graph=new CGraph(m_nDrawType, m_ptOrigin, point);
	m_ptrArray.Add(graph);
	*/

	CScrollView::OnLButtonUp(nFlags, point);
}

🔶🔸 3. 在OnDraw函數中,播放元文件
按照上面元文件使用步驟的第三步,接下來應該在窗口重繪時,即在OnDraw函數中,播放元文件,以實現圖形的顯示。爲了效果更清晰,我們首先將OnDraw函數先前編寫的繪製圖形的代碼註釋起來或刪除,然後添加新代碼:

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

	// TODO: 在此處爲本機數據添加繪製代碼
	HMETAFILE hmetaFile;
	//調用Close函數關閉元文件設備上下文,獲得元文件句柄。
	hmetaFile = m_dcMetaFile.Close();
	//利用CDC類的成員函數: PlayMetaFile播放該元文件
	pDC->PlayMetaFile(hmetaFile);
	//因爲在窗口重繪之後,用戶可能希望能夠繼續繪製圖形。
	//而我們仍採用元文件的方式來實現圖形的繪製,所以在播放先前元文件中繪製圖形的命令之後,應該接着再創建一個元文件設備上下文對象,以便用戶再次繪製圖形使用
	m_dcMetaFile.Create();
	//最後調用DeleteMetaFile函數釋放元文件資源
	DeleteMetaFile(hmetaFile);
}

4.2 元文件的保存和打開

接下來,我們希望把保存圖形繪製命令的元文件保存爲磁盤文件,以便在以後需要時隨時可以打開該文件,並在程序窗口中顯示其中的圖形內容。於是爲Graphic程序的CGraphicView類分別添加【文件】子菜單下的【打開】和【保存】菜單命令的響應函數,然後在這兩個命令響應函數(分別爲OnFileOpen和OnFileSave)中分別實現元文件的打開和保存。


爲了保存元文件,可以使用CopyMetaFile函數,該函數的作用是把Windows元文件的內容複製到指定的文件。該函數的聲明形式如下所示:

HMETAFILE CopyMetaFile( 
    HMETAFILE hmfsrc,
    LPCTSTR IpszFile
);

CopyMetaFile函數有兩個參數,含義分別如下所述:
◼ hmfSrc
指定要複製的Windows元文件的柄;
◼ lpszFile
指定複製目標文件名稱。

在Graphic程序的OnFileSave函數中實現元文件的保存。

void CGraphicView::OnFileSave()
{
	// TODO: 在此添加命令處理程序代碼
	HMETAFILE hmetaFile;
	hmetaFile = m_dcMetaFile.Close();
	CopyMetaFile(hmetaFile, _T("meta.wmf"));
	m_dcMetaFile.Create();
	DeleteMetaFile(hmetaFile);
}

void CGraphicView::OnFileOpen()
{
	// TODO: 在此添加命令處理程序代碼
	HMETAFILE hmetaFile;
	hmetaFile = GetMetaFile(_T("meta.wmf"));
	m_dcMetaFile.PlayMetaFile(hmetaFile);
	DeleteMetaFile(hmetaFile);
	Invalidate();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章