zoukankan      html  css  js  c++  java
  • 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各个参数 请点击此处 ),最后一个参数是字体类型,我们可以使用我们电脑已安装的任何字体,在WindowsFonts目录可查看电脑已安装的字体。

    然后我们从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 }

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

  • 相关阅读:
    第八章 1 元组简介
    第七章 5 字典生成式
    第七章 4 字典的视图操作、字典的遍历、字典的特点
    第七章 3 字典的常用操作(增删改查)
    第七章 2 字典的创建以及字典元素的获取
    第七章 1 字典介绍
    Java 中 Executors.newSingleThreadExecutor() 与Executors.newFixedThreadPool(1)有什么区别
    Java 使用线程池执行若干任务
    使用Jacoco获取 Java 程序的代码执行覆盖率
    基于 Golang 完整获取百度地图POI数据的方案
  • 原文地址:https://www.cnblogs.com/ybqjymy/p/14048279.html
Copyright © 2011-2022 走看看