一杯清酒邀明月
天下本无事,庸人扰之而烦耳。

这次教程中,我们将创建一些基于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 }

现在就可以运行程序查看效果了!

 

posted on 2020-11-27 15:32  一杯清酒邀明月  阅读(1088)  评论(0编辑  收藏  举报