雖然MFC已經落伍好多年,而且用來做界面非常的不好用。。。但是我既不會C#也不會QT,又需要使用OpenGL,就只能將就用了。。。
一、首先介紹Windows圖像程序設計中幾個重要的概念:
GDI(Graphics Device Interface,圖形設備接口):這是Windows API的一個庫。當Windows應用程序需要顯示點、線、圖像、文字等內容,在顯示器或打印輸入這些內容時,就需要用到GDI。Windows應用程序不能直接操作系統的硬件(比如顯卡),GDI就為應用程序提供了相關的接口。
其相關的函數接口、數據類型等都在WinGDI.h中聲明(已經由Windows.h引入),在程序開發時,需要鏈接到Gdi32.lib。
DC(Device Contexts,設備上下文):是GDI庫中最基本也是最重要的概念。DC是一個對象,設定了圖形輸出的特性和屬性。
系統中可以有多個DC,每一個DC都必須關聯到一個特定的圖像輸出設備。這些設備可以是真實存在的物理設備(顯示器、打印機、繪圖儀等),也可以使虛擬設備。這些反應在DC的類型上,
DC具有4種類型:“顯示”、“打印機”、“內存”、“信息”。
其中顯示類型DC是最常用的,它被關聯到了顯示設備上,所有的圖像輸出操作將直接反映在顯示器上。
注:DC也可以只是設備全部輸出范圍的一部分。比如界面上某個窗口的客戶區也可以有DC與之對應,對這樣的DC進行操作只會影響到窗口客戶區。
如果要將圖像輸出到特定的設備只需要創建相應類型的DC即可(注:對不同類型DC的操作是統一的),我們只關注獲取顯示器相關DC的操作:(以下的函數都是GDI庫中的接口函數)
1.
獲取DC - GetDC(HWND hWnd):
調用該函數會返回hWnd參數所指定的窗口的客戶區所對應的DC的句柄。
如果hWnd參數設置為NULL,那么函數會返回整個桌面的DC。
2.
另一種獲取DC的方法 - CreateDC
該函數也是用來獲取DC句柄,與GetDC不同的是,
CreateDC可以獲取非顯示器輸出DC。只需指定不同的參數即可。
獲取顯示器的DC:HDC hScreenDC = CreateDC("DISPLAY", NULL, NULL, NULL);
若想獲取打印機的DC,一般是將第一個參數改為"WINSPOOL"。
3.
釋放DC - ReleaseDC(HWND hWnd, HDC hDC)
該函數的作用是釋放DC,使其他應用程序可以使用。
4.
釋放DC的相關系統資源 - DeleteDC(HDC hDC)
這個函數並不常用,一般做圖結束后,我們只需要調用ReleaseDC釋放DC即可。
也許看到這里,對DC具體是個什么東西還沒有什么清楚的概念。下面大概講一下。。。
DC實際上是包含了一系列的圖形對象,比如位圖(Bitmap)、畫刷(Brushe)、畫筆(Pen)、字體(Font)、邏輯調色板(Logical palette)等等,GDI庫中還定義了一系列接口函數,
應用程序通過調用這些接口函數來操作當前DC中的圖形對象,完成期望的繪圖操作,最終影響放映到對應設備的輸出上。
比如先創建一個窗口,然后得到該窗口對應的DC,這時候系統會為DC創建默認的圖形對象(位圖和路徑除外),此時如果不進行任何操作,那么顯示的窗口是一片白(也就是說剛開始是一塊白色畫布)。然后我們可以調用GDI庫中的接口函數進行畫圖操作,或者載入位圖(相當於在白色畫布上作畫)。
DC中就是提供了對畫布進行作畫的工具。
注:DC對應設備的顯示信息都存儲在位圖對象中。
為了更清楚DC的作用,下面舉個很簡單的例子:在屏幕上畫一條線
首先,獲得整個顯示器的DC:
HDC hdC = GetDC(NULL);
創建新的畫筆對象:
COLORREF cPen = RGB(0,0,0); //指定畫筆顏色為黑色
HPEN hpen = CreatePen(PS_SOLID, 10, cPen); //創建新的畫筆,返回新畫筆的句柄
將新創建的畫筆指定為DC的當前畫筆:(對同一種類型的圖形對象,DC中只能有一個當前對象)
HPEN hpenOld = SelectObject(hdc, hpen);
畫線:
LineTO(hdc, 500, 500);
畫圖操作結束,還原畫筆:
SelectObject(hdc, hpenOld);
釋放畫筆資源:
DeleteObject(hpen);
釋放DC:
ReleaseDC(NULL, hdc);
二、下面來討論使用OpenGL來繪圖的相關知識:
使用OpenGL繪圖與使用GDI庫繪圖是不同的,主要體現在:OpenGL采用的是
RC(Render Context,渲染上下文)繪圖。
DC和RC的區別和聯系:
1.
在Windows中使用GDI繪圖時必須指定在哪個DC中繪制,同樣地,
在使用OpenGL函數時也必須指定一個所謂的RC。正如設備上下文DC要存儲GDI的繪制環境信息如筆、刷和字體等,RC也必須存儲OpenGL所需的渲染信息如像素格式等。
2. Windows下的窗口和DC支持的位圖格式(PIXELFORMAT)屬性,和RC有位圖結構上的一致。
只要在創建RC時與一個DC建立聯系,OpenGL函數就可以通過與RC對應的DC繪制到相應設備上。而實際上,
RC只能通過建立了位圖格式的DC來創建。
3. 一個DC對應的是一個圖像輸出設備,而一個RC對應的則是一個線程。
一個線程只能擁有一個RC,而一個RC也只能屬於一個線程,不能在線程中共有。若一個線程想要在不同的設備上繪圖,只能通過更改與RC對應的DC來實現,而RC在線程總保持不變(當然也可以刪除舊的RC,再利用其它設備的DC創建新的RC)。
下面來講一下RC的創建。正如前面所說的,RC只能通過建立了位圖格式的DC來創建。
1. 首先,我們需要創建一個窗口,然后使用GetDC函數得到這個窗口的DC。我們這里討論在MFC使用OpenGL,因此窗口類是由工程自動創建的COpenglDemoView類,
由於我們做的是繪圖前的窗口初始化工作,因此我們將之后所有的操作都在OnCreate函數中完成。(
至於一些必要的成員數據初始化工作,應放在View類的構造函數中)
HWND hWnd = this->GetSafeHwnd();
HDC hDC = ::GetDC(hWnd);
2. 然后,我們需要指定DC中的位圖像素格式。這個工作是由GDI庫中名為
PIXELFORMATDESCRIPTOR的類來實現的。
第一步是填充這個數據結構:
PIXELFORMATDESCRIPTOR pixelDesc={
sizeof(PIXELFORMATDESCRIPTOR),
//nsize:像素格式描述子結構的大小
1,
//nVersion:PIXELFORMATDESCRIPTOR結構的版本,一般設為1
//dwFlags:一組表明像素緩沖特性的標志位
PFD_DRAW_TO_WINDOW |
//使之能在窗口或者其他設備窗口畫圖
PFD_SUPPORT_OPENGL,// |
//使之能使用OpenGL函數
//PFD_DOUBLEBUFFER,
//指明使用了雙緩沖
PFD_TYPE_RGBA,
//PixelType:定義了顯示顏色的方法
24,
//cColorBits:指定了一個顏色的位數
0,0,
//cRedBits, cRedShift:每個RGBA顏色緩沖區中紅色位平面的數目和偏移數
0,0,
//cGreenBits, cGreenShift:每個RGBA顏色緩沖區中綠色位平面的數目和偏移數
0,0,
//cBlueBits, cBlutShift:每個RGBA顏色緩沖區中藍色位平面的數目和偏移數
0,0,
//cAlphaBits, cAlphaShift:每個RGBA顏色緩沖區中Alpah位平面的數目和偏移數
0,
//cAccumBits:累加緩沖區中全部位平面的數目
0,0,0,0,
//cAccumRedBits, cAccumGreenBits, cAccumBlueBits, cAccumAlphaBits
32,
//cDepthBits:Z(深度)緩沖區的深度
0,
//cStencilBits:模板緩沖區的深度
0,
//cAuxBuffers:軸向緩沖區的數量(一般1.0版本不支持)
PFD_MAIN_PLANE,
//iLayerType:忽略,為了一致性而包含的
0,
//bReserved:表層和底層平面的數量
0,0,0
//dwLayerMask, dwVisibleMask, dwDamageMask
};
這個類中的大部分成員變量我們並不關心,最重要的是第三個參數
dwFlags,其中指定的
PFD_SUPPORT_OPENGL使得我們
可以在這個窗口中使用OpenGL函數。
而指定的
PFD_DOUBLEBUFFER則使得我們可以使用OpenGL中的雙緩存機制(這個機制在制作動畫時非常重要)。但奇怪的是當我選擇了這個標志位時,生成的窗口沒有圖像(一片空白),而將這個標志位注釋掉,就能顯示我所繪制的圖了。非常詭異,我也不明白為什么。。。
第二步是將這個像素格式選為當前DC的使用像素格式:
this->m_GLPixelIndex = ChoosePixelFormat(hDC, &pixelDesc);
if(this->m_GLPixelIndex==0){
this->m_GLPixelIndex = 1;
if(DescribePixelFormat(hDC,
this->m_GLPixelIndex,
sizeof(PIXELFORMATDESCRIPTOR),
&pixelDesc) == 0)
{
return false;
}
}
if(SetPixelFormat(hDC, this->m_GLPixelIndex, &pixelDesc) == false){
return false;
}
這段代碼首先調用了
ChoosePixelFormat函數來
尋找OpenGL所支持的像素格式中,最接近所設置的像素格式,如果沒有找到,就調用
DescribePixelFormat函數來
選擇索引值為1的像素格式來填充設置的像素格式。這些操作完成后,保證了所設置的像素格式是OpenGL所支持的。
最后就可以調用
SetPixelFormat函數來為
指定當前DC的像素格式。
注:代碼中的this->m_GLPixelIndex是我們自己添加的View類的protected型的成員變量,類型為int,用於保存所設置的像素格式在OpenGL所支持的像素格式列表中的索引值。
3. 設置像素格式完成后,就可以創建RC了。這里面要用到兩個GDI庫中的函數
wglCreateContext 和
wglMakeCurrent。
首先創建RC:
this->m_hGLContext = wglCreateContext(hDC);
然后選擇新創建的RC成為當前DC對應的RC
wglMakeCurrent(hDC, this->m_hGLContext);
注:代碼中的this->m_hGLContext是我們自己添加的View類的protected型的成員變量,類型為
HGLRC,用於保存當前RC的句柄。
這樣,RC就創建成功了。
RC創建成功后,我們就可以在View窗口中執行繪圖操作了。其中,
視窗、投影方式等設置放在
OnSize函數中,而
繪制操作則放在
OnPaint函數中。
這里需要注意的是,MFC中沒有提供類似GLUT中glutIdleFunc()的函數,而
OnPaint函數只會在窗口創建或者窗口需要重繪的時候才會被調用。因此OnPaint函數中的繪圖操作一般都是靜態的,如果想繪制動畫,則需要程序員自己寫定時函數來控制窗口的重繪。
最后列一下
參考的資料:
如果想快速實現在MFC添加OpenGL窗口的功能,參考:
基於MFC的OpenGL繪圖
如果想比較詳細地了解其中的原理,參考:
OpenGL在MFC下的編程原理 和
OpenGL與MFC編程思想(后一篇還提供了
解決重繪時屏幕閃爍問題的方法)
如果想了解其中用到的函數,參考:
OpenGL RC與像素格式