1. 程式人生 > 實用技巧 >Qt OpenGL 點陣圖字型

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 }

現在就可以執行程式檢視效果了!