Qt OpenGL 點陣圖字型
這次教程中,我們將建立一些基於2D影象的字型,它們可以縮放平移,但不能旋轉,並且總是面向前方,但作為基本的顯示來說,我想已經足夠了。
或者對於這次教程,你會覺得“在螢幕上顯示文字沒什麼難的”,但是你真正嘗試過就會知道,它確實沒那麼容易。你當然可以把文字寫在一個圖片上,再把這幅圖片載入你的OpenGL程式中,開啟混合選項,從而在螢幕上顯示出文字。但這種做法非常耗時,而且經常影象會顯得模糊。另外,除非你的影象包含一個Alpha通道,否則一旦繪製在螢幕上,那些文字就會不透明(與螢幕中的其他物體混合)。
使用點陣圖字型比起使用圖形字型(貼圖)看起來不止強100倍,你可以隨時改變顯示在螢幕上的文字,而且用不著為它們逐個製作貼圖。只需要將文字定位,再呼叫我們即將構建的glPrint()函式就可以在螢幕上顯示文字了。
程式執行時效果如下:
下面進入教程:
我們這次將在第01課的基礎上修改程式碼,我會對新增程式碼一一解釋,希望大家能掌握,首先開啟myglwidget.h檔案,將類宣告更改如下:
1 #ifndef MYGLWIDGET_H
2 #define MYGLWIDGET_H
3
4 #include <QWidget>
5 #include <QGLWidget>
6
7 class MyGLWidget : public QGLWidget
8 {
9 Q_OBJECT
10 public:
11 explicit MyGLWidget(QWidget *parent = 0 );
12 ~MyGLWidget();
13
14 protected:
15 //對3個純虛擬函式的重定義
16 void initializeGL();
17 void resizeGL(int w, int h);
18 void paintGL();
19
20 void keyPressEvent(QKeyEvent *event); //處理鍵盤按下事件
21
22 private:
23 void buildFont(); //建立字型
24 void killFont(); //刪除顯示列表
25 void glPrint(const char *fmt, ...); //輸出字串
26
27 private:
28 bool fullscreen; //是否全屏顯示
29 HDC m_HDC; //儲存當前裝置的指標
30
31 int m_FontSize; //控制字型的大小
32 GLuint m_Base; //儲存繪製字型的顯示列表的開始位置
33 GLfloat m_Cnt1; //字型移動計數器1
34 GLfloat m_Cnt2; //字型移動計數器2
35 };
36
37 #endif // MYGLWIDGET_H
我們新增了幾個變數,第一個變數m_HDC是用來儲存當前裝置繪製資訊的一種windows資料結構,我們將會把我們自己建立的字型繫結到m_HDC上去,這樣我們繪製文字時就自動採用繫結的字型了。後面幾個變數的作用依次是控制字型大小、儲存繪製字型的顯示列表的開始位置、字型移動計數,具體的會在後面講。
另外我們增加了三個函式,分別用於建立字型、刪除顯示列表、輸出特定的字串,當然最後一個glPrint()函式在前面已經提到,是個很重要的函式。
接下來,我們需要開啟myglwidget.cpp,加上宣告#include <QTimer>、#include <QtMath>,將建構函式和解構函式修改一下,具體程式碼如下:
1 MyGLWidget::MyGLWidget(QWidget *parent) :
2 QGLWidget(parent)
3 {
4 fullscreen = false;
5 m_FontSize = -18;
6 m_Cnt1 = 0.0f;
7 m_Cnt2 = 0.0f;
8
9 HWND hWND = (HWND)winId(); //獲取當前視窗控制代碼
10 m_HDC = GetDC(hWND); //通過視窗控制代碼獲得HDC
11
12 QTimer *timer = new QTimer(this); //建立一個定時器
13 //將定時器的計時訊號與updateGL()繫結
14 connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
15 timer->start(10); //以10ms為一個計時週期
16 }
1 MyGLWidget::~MyGLWidget()
2 {
3 killFont(); //刪除顯示列表
4 }
幾個普通變數的初始化我不作解釋了,我們重點看m_HDC的初始化。我們要如何獲得當前視窗的HDC呢?方法是我們先得到當前視窗的控制代碼(HWND),通過呼叫函式GetCD(HWND)可以獲得HDC。那如何獲得HWND呢?Qt中有一個winId()函式可以返回當前視窗的Id(型別為WId),我們把它強制轉換為HWND型別就可以了,這樣我們就可以初始化關鍵的m_HDC。
注意一下解構函式,在退出程式之前,我們應該確保我們分配的用於存放顯示列表的空間被釋放,所以我們在解構函式處呼叫killFont()函式刪除顯示列表(具體實現看下面)。
繼續,我們要來定義我們新增的三個函數了,這可是重頭戲,具體程式碼如下:
1 void MyGLWidget::buildFont() //建立點陣圖字型
2 {
3 HFONT font; //字型控制代碼
4 m_Base = glGenLists(96); //建立96個顯示列表
5
6 font = CreateFont(m_FontSize, //字型高度
7 0, //字型寬度
8 0, //字型的旋轉角度
9 0, //字型底線的旋轉角度
10 FW_BOLD, //字型的重量
11 FALSE, //是否斜體
12 FALSE, //是否使用下劃線
13 FALSE, //是否使用刪除線
14 ANSI_CHARSET, //設定字符集
15 OUT_TT_PRECIS, //輸出精度
16 CLIP_DEFAULT_PRECIS, //剪裁精度
17 ANTIALIASED_QUALITY, //輸出質量
18 FF_DONTCARE | DEFAULT_PITCH, //Family and Pitch的設定
19 LPCWSTR("Courier New")); //字型名稱(電腦中已裝的)
20
21 wglUseFontBitmaps(m_HDC, 32, 96, m_Base); //建立96個顯示列表,繪製ASCII碼為32-128的字元
22 SelectObject(m_HDC, font); //選擇字型
23 }
1 void MyGLWidget::killFont() //刪除顯示列表
2 {
3 glDeleteLists(m_Base, 96); //刪除96個顯示列表
4 }
1 void MyGLWidget::glPrint(const char *fmt, ...) //自定義輸出文字函式
2 {
3 char text[256]; //儲存字串
4 va_list ap; //指向一個變數列表的指標
5
6 if (fmt == NULL) //如果無輸入則返回
7 {
8 return;
9 }
10
11 va_start(ap, fmt); //分析可變引數
12 vsprintf(text, fmt, ap); //把引數值寫入字串
13 va_end(ap); //結束分析
14
15 glPushAttrib(GL_LIST_BIT); //把顯示列表屬性壓入屬性堆疊
16 glListBase(m_Base - 32); //設定顯示列表的基礎值
17 glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); //呼叫顯示列表繪製字串
18 glPopAttrib(); //彈出屬性堆疊
19 }
首先是buildFont()函式。我們先定義了字型控制代碼變數(HFONT),用來存放我們將要建立和使用的字型。接著我們在定義m_Base的同時使用glGenLists(96)建立了一組共96個顯示列表。然後我們呼叫Windows的API函式CreateFont()來建立我們自己的字型,前13個引數的意義大家請參考註釋,我覺得沒必要一個個解釋了(有興趣瞭解CreateFont各個引數請點選此處),最後一個引數是字型型別,我們可以使用我們電腦已安裝的任何字型,在Windows\Fonts目錄可檢視電腦已安裝的字型。
然後我們從ASCII碼第32個字元(空格)開始建立96個顯示列表。如果你願意,也可以建立所有256個字元,只要確保使用glGenLists建立256個顯示列表就可以了。最後我們將font物件指標選入HDC,如此就完成了字型的建立及繫結。
然後是killFont()函式。它很簡單,就是呼叫glDeleteLists()函式從m_Base開始刪除96個顯示列表。
最後是glPrint()函式。首先第一行我們建立一個大小為256個字元的字元陣列,將用來儲存我們要輸出的字串。第二行我們建立了一個指向一個變數列表的指標,我們在傳遞字串的同時也傳遞了這個變數列表。然後是排除字串為空的情況。接著的三行程式碼將文字中的所有符號轉換為它們的字元編號,最終文字和轉換的符號被儲存在字串text中。然後我們將GL_LIST_BIT壓入屬性堆疊,它會防止glListBase影響到我們的程式中的其它顯示列表。
glListBase(m_Base-32)是告訴OpenGL去哪找對應字元的顯示列表,由於每個字元對應一個顯示列表,通過m_Base設定一個起點,OpenGL就知道到哪去找到正確的顯示列表。減去32是因為我們沒有構造前32個顯示列表,那麼久跳過它們就好了。於是,我們不得不通過從m_Base的值減去32來讓OpenGL知道這一點。
現在OpenGL知道字母的存放位置了,我們就可以讓它在螢幕上顯示文字了。glCallLists()函式能同時將多個顯示列表的內容顯示在螢幕上,第一個引數是要顯示在螢幕上的字串長度,第二個引數告訴OpenGL將字串當作一個無符號陣列處理,它們的值都介於0到255之間,第三個引數通過傳遞text來告訴OpenGL顯示的具體內容。最後,我們將GL_LIST_BIT屬性彈出堆疊,恢復到我們使用glListBase(m_Base-32)之前的狀態。
也許你想知道為什麼字元不會彼此重疊堆積在一起。那是因為每個字元的顯示列表都知道字元的右邊緣在哪裡,在寫完一個字元後,OpenGL自動移動到剛剛寫過的字元的右邊,再寫下一個字或畫下一個物體時就會從最後的位置開始,也就是最後一個字元的右邊。
然後我們修改一下initializeGL()函式,不作解釋,程式碼如下:
1 void MyGLWidget::initializeGL() //此處開始對OpenGL進行所以設定
2 {
3 glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
4 glShadeModel(GL_SMOOTH); //啟用陰影平滑
5 glClearDepth(1.0); //設定深度快取
6 glEnable(GL_DEPTH_TEST); //啟用深度測試
7 glDepthFunc(GL_LEQUAL); //所作深度測試的型別
8 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告訴系統對透視進行修正
9
10 buildFont(); //建立字型
11 }
還有,我們該進入paintGL()函數了,很簡單,難的都過去了,具體程式碼如下:
1 void MyGLWidget::paintGL() //從這裡開始進行所以的繪製
2 {
3 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除螢幕和深度快取
4 glLoadIdentity(); //重置當前的模型觀察矩陣
5
6 glTranslatef(0.0f, 0.0f, -10.0f); //移入螢幕10.0單位
7 //根據字型位置設定顏色
8 glColor3f(1.0f*float(cos(m_Cnt1)), 1.0f*float(sin(m_Cnt2)),
9 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)));
10 //設定光柵化位置,即字型位置
11 glRasterPos2f(-4.5f+0.5f*float(cos(m_Cnt1)), 1.92f*float(sin(m_Cnt2)));
12 //輸出文字到螢幕上
13 glPrint("Active OpenGL Text With NeHe - %7.2f", m_Cnt1);
14 m_Cnt1 += 0.051f; //增加兩個計數器的值
15 m_Cnt2 += 0.005f;
16 }
值得注意的是,深入螢幕並不能縮小字型,只會給字型變化移動範圍(這一點大家自己改改資料就知道了)。然後字型顏色設定和位置設定我覺得沒必要解釋了,都是數學的東西,我們主要是為了得到一個變化的效果,並不在乎它是怎麼實現的。然後就是呼叫glPrint()函式輸出文字,最後增加兩個計數器的值就OK了。
最後就是鍵盤控制的程式碼了,大家自己看吧,很簡單,具體程式碼如下:
1 void MyGLWidget::keyPressEvent(QKeyEvent *event)
2 {
3 switch (event->key())
4 {
5 case Qt::Key_F1: //F1為全屏和普通屏的切換鍵
6 fullscreen = !fullscreen;
7 if (fullscreen)
8 {
9 showFullScreen();
10 }
11 else
12 {
13 showNormal();
14 }
15 updateGL();
16 break;
17 case Qt::Key_Escape: //ESC為退出鍵
18 close();
19 break;
20 case Qt::Key_PageUp: //PageUp按下字型縮小
21 m_FontSize -= 1;
22 if (m_FontSize < -75)
23 {
24 m_FontSize = -75;
25 }
26 buildFont();
27 break;
28 case Qt::Key_PageDown: //PageDown按下字型放大
29 m_FontSize += 1;
30 if (m_FontSize > -5)
31 {
32 m_FontSize = -5;
33 }
34 buildFont();
35 break;
36 }
37 }
現在就可以執行程式檢視效果了!