这次教程中,我们将创建一些基于2D图像的字体,它们可以缩放平移,但不能旋转,并且总是面向前方,但作为基本的显示来说,我想已经足够了。
或者对于这次教程,你会觉得“在屏幕上显示文字没什么难的”,但是你真正尝试过就会知道,它确实没那么容易。你当然可以把文字写在一个图片上,再把这幅图片载入你的OpenGL程序中,打开混合选项,从而在屏幕上显示出文字。但这种做法非常耗时,而且经常图像会显得模糊。另外,除非你的图像包含一个Alpha通道,否则一旦绘制在屏幕上,那些文字就会不透明(与屏幕中的其他物体混合)。
使用位图字体比起使用图形字体(贴图)看起来不止强100倍,你可以随时改变显示在屏幕上的文字,而且用不着为它们逐个制作贴图。只需要将文字定位,再调用我们即将构建的glPrint()函数就可以在屏幕上显示文字了。
程序运行时效果如下:
下面进入教程:
我们这次将在第01课的基础上修改代码,我会对新增代码一一解释,希望大家能掌握,首先打开myglwidget.h文件,将类声明更改如下:
【领QT开发教程学习资料,点击下方链接莬费领取↓↓,先码住不迷路~】
点击→领取「链接」
1 #ifndef MYGLWIDGET_H 2 #define MYGLWIDGET_H 3 4 #include
我们新增了几个变量,第一个变量m_HDC是用来储存当前设备绘制信息的一种windows数据结构,我们将会把我们自己创建的字体绑定到m_HDC上去,这样我们绘制文字时就自动采用绑定的字体了。后面几个变量的作用依次是控制字体大小、储存绘制字体的显示列表的开始位置、字体移动计数,具体的 会在后面讲。
另外我们增加了三个函数,分别用于创建字体、删除显示列表、输出特定的字符串,当然最后一个glPrint()函数在前面已经提到,是个很重要的函数。
接下来,我们需要打开myglwidget.cpp,加上声明#include
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); //通过窗口句柄获得HDC11 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。
【领QT开发教程学习资料,点击下方链接莬费领取↓↓,先码住不迷路~】
点击→领取「链接」
注意一下析构函数,在退出程序之前,我们应该确保我们分配的用于存放显示列表的空间被释放,所以我们在析构函数处调用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 else12 {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 }
现在就可以运行程序查看效果了!
标签: 字体
②文章观点仅代表原作者本人不代表本站立场,并不完全代表本站赞同其观点和对其真实性负责。
③文章版权归原作者所有,部分转载文章仅为传播更多信息、受益服务用户之目的,如信息标记有误,请联系站长修正。
④本站一律禁止以任何方式发布或转载任何违法违规的相关信息,如发现本站上有涉嫌侵权/违规及任何不妥的内容,请第一时间反馈。发送邮件到 88667178@qq.com,经核实立即修正或删除。