原文
6 N* R/ M N- ghttp://blog.chinaunix.net/u/26313/showart_1663543.html
6 P0 \9 ]9 L0 y1 N: `# J$ e9 C0 X' E% Y3 x! y" a
内容超多的一课!不过我想精彩的程度也一定不会让大家失望。大家不妨先浏览一下课程里的图片:)。 - k/ w) w! P+ W! A2 h( y5 [
! B; `6 a+ l X4 o$ y+ ]; H; A本课我们来谈谈如何显示文字。
9 v8 k! n7 F; c7 I4 }2 l+ [/ AOpenGL并没有直接提供显示文字的功能,并且,OpenGL也没有自带专门的字库。因此,要显示文字,就必须依赖操作系统所提供的功能了。
/ ~; X% ~/ i; n2 U0 R各种流行的图形操作系统,例如Windows系统和Linux系统,都提供了一些功能,以便能够在OpenGL程序中方便的显示文字。
# r. L3 y+ K2 k7 U# o8 i最常见的方法就是,我们给出一个字符,给出一个显示列表编号,然后操作系统由把绘制这个字符的OpenGL命令装到指定的显示列表中。当需要绘制字符的时候,我们只需要调用这个显示列表即可。
( R# j9 s$ h. T# j, x不过,Windows系统和Linux系统,产生这个显示列表的方法是不同的(虽然大同小异)。作为我个人,只在Windows系统中编程,没有使用Linux系统的相关经验,所以本课我们仅针对Windows系统。
1 O( V7 F# d! T' ?3 x6 n' {$ T, \8 Z( @
* R, i# W: C' K1 z; H4 rOpenGL版的“Hello, World!” 9 [- P& P+ t) M$ k7 f! a
写完了本课,我的感受是:显示文字很简单,显示文字很复杂。看似简单的功能,背后却隐藏了深不可测的玄机。 / b$ E- M# N6 x H- z% x6 {4 W
呵呵,别一开始就被吓住了,让我们先从“Hello, World!”开始。
) i' t( \9 I6 @: q8 r. Y8 w前面已经说过了,要显示字符,就需要通过操作系统,把绘制字符的动作装到显示列表中,然后我们调用显示列表即可绘制字符。
2 I* [0 z& e) _+ D假如我们要显示的文字全部是ASCII字符,则总共只有0到127这128种可能,因此可以预先把所有的字符分别装到对应的显示列表中,然后在需要时调用这些显示列表。
% L! o- B. b. |# y8 @; e/ vWindows系统中,可以使用wglUseFontBitmaps函数来批量的产生显示字符用的显示列表。函数有四个参数: # ?0 }- M' L$ U$ a! M
第一个参数是HDC,学过Windows GDI的朋友应该会熟悉这个。如果没有学过,那也没关系,只要知道调用wglGetCurrentDC函数,就可以得到一个HDC了。具体的情况可以看下面的代码。
% z# [ v: d1 L1 l% m第二个参数表示第一个要产生的字符,因为我们要产生0到127的字符的显示列表,所以这里填0。
$ [4 X" n; F1 S3 d6 M( K第三个参数表示要产生字符的总个数,因为我们要产生0到127的字符的显示列表,总共有128个字符,所以这里填128。 ) O9 G( ?8 W/ O! Z6 B; _7 b3 c U
第四个参数表示第一个字符所对应显示列表的编号。假如这里填1000,则第一个字符的绘制命令将被装到第1000号显示列表,第二个字符的绘制命令将被装到第1001号显示列表,依次类推。我们可以先用glGenLists申请128个连续的显示列表编号,然后把第一个显示列表编号填在这里。
U! u/ j: K3 C2 u9 b; v% @还要说明一下,因为wglUseFontBitmaps是Windows系统特有的函数,所以在使用前需要加入头文件:#include <windows.h>。
' b ^/ J. M9 u- r% K现在让我们来看具体的代码: 8 W' Z8 M1 f$ l2 j! E) c* l
5 a5 Y; ^1 H3 ]# X; p#include <windows.h>
/ L2 g; x. V3 M. Z' V: B Q }: D
// ASCII字符总共只有0到127,一共128种字符
$ N$ z0 A! x. i$ [! `#define MAX_CHAR 128 ( g: c ~- ~& i8 i5 c8 T) V# a
5 k8 d: H4 L* v+ p; D% h1 ?
void drawString(const char* str) { / l: m2 Y" J& `, J2 I* X+ l/ |" S2 P
static int isFirstCall = 1; 7 J; f: E! L$ F' k, Q" q
static GLuint lists;
- T. j# K% K8 f3 J
+ d* W* G" y1 }: ~/ ?7 i' | if( isFirstCall ) { // 如果是第一次调用,执行初始化
O: Y4 c; {4 |) @. T // 为每一个ASCII字符产生一个显示列表 ( v4 S% a# M7 f
isFirstCall = 0; / g0 T1 Q" B0 I8 Q: {1 L
2 B5 Q, e* d. ~! D- q- [0 B$ p. B' a // 申请MAX_CHAR个连续的显示列表编号
7 b5 x% q# \; `6 ~' X7 j) w lists = glGenLists(MAX_CHAR);
$ l; I1 y8 t% S# P1 F
5 f# I- k4 z- T' c% i // 把每个字符的绘制命令都装到对应的显示列表中 ; s* q+ y8 ~$ |
wglUseFontBitmaps(wglGetCurrentDC(), 0, MAX_CHAR, lists);
) y/ I' T/ P6 k. z; v2 K% r( k } 4 i& S1 D$ q$ ^( J+ I# L
// 调用每个字符对应的显示列表,绘制每个字符
8 o6 b5 }, ^# j: _+ ~8 \2 ^0 A for(; *str!='\0'; ++str) 6 L) n5 w* P0 L
glCallList(lists + *str); 5 G& G# ?6 t: Q Q) i
} 8 c) U, b( j4 ^$ C+ C
5 x- `7 S& h! ?- U& s' V+ b+ f
6 A0 b) g( U9 H. \4 \1 Z( ], p' S, B- g
显示列表一旦产生就一直存在(除非调用glDeleteLists销毁),所以我们只需要在第一次调用的时候初始化,以后就可以很方便的调用这些显示列表来绘制字符了。
0 U9 }4 A* ^! S! o绘制字符的时候,可以先用glColor*等指定颜色,然后用glRasterPos*指定位置,最后调用显示列表来绘制。 9 u0 I% I2 c! C5 T
9 \" ?6 H( b# }; ?! @
void display(void) {
( S3 k, ^/ z$ ^3 V2 h' h- j9 Q glClear(GL_COLOR_BUFFER_BIT);
+ }8 u8 B& }- K* A- {; c
4 x% o9 K. A5 U" ? O8 Y! Y) L glColor3f(1.0f, 0.0f, 0.0f);
1 f# }/ ~" Q3 v @ glRasterPos2f(0.0f, 0.0f); , w' L. m) q- S2 W7 c5 \
drawString("Hello, World!"); " k* c7 E7 h% a2 B) D
* ]2 b; r X# w: G5 f+ |) r glutSwapBuffers(); 7 u- \( Y+ f5 q7 ` _
}
) w* I: A& w8 _8 N$ T1 Q$ \# u- R, m
- M& T9 V5 U# a- R# ]# l5 Y! [3 p- m# M! f
效果如图: & n6 S9 O$ ^5 E+ z4 V) [; c$ q# I4 h
# p1 \ j2 \! F, L9 z$ |/ x3 R4 f9 |
\) F) m8 Y/ J, Y& `( Y指定字体
$ g0 ]" v" V4 G t在产生显示列表前,Windows允许选择字体。
F6 S2 y N. }4 x/ }8 q我做了一个selectFont函数来实现它,大家可以看看代码。
[/ c1 U) ?* ]# g4 Y; m+ n1 P, }- O* i& Q5 n) x1 p4 R2 u
void selectFont(int size, int charset, const char* face) { 1 c x' D: } D: R- J7 q. e
HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0, # f" Y# l1 u* [4 z& { u2 h/ k
charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, 4 e3 R h: B: n3 O; ^
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face); ( `. ^, N0 ?" g. B: R# f
HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont);
3 o5 u, c- H/ ?7 s! V, z DeleteObject(hOldFont);
& _0 ^- j: o) e+ G. W} 0 _0 W- n& Q. P4 u
8 D8 ?6 c/ ~ n2 Zvoid display(void) {
" u2 P0 H/ w5 @1 J6 D selectFont(48, ANSI_CHARSET, "Comic Sans MS"); c. B$ ]- |) h [
9 h/ U; C$ Y' J$ d7 \, g' G8 O glClear(GL_COLOR_BUFFER_BIT);
& j- @5 a ?: f$ `6 w1 b2 Z o. X) H" F$ f6 ~
glColor3f(1.0f, 0.0f, 0.0f);
: M& l- c2 s! L5 E2 `! v glRasterPos2f(0.0f, 0.0f); 3 R. X) C% }$ i
drawString("Hello, World!"); $ G- I* Z# a' ?1 H6 F, i
0 q; i4 y- z! g
glutSwapBuffers(); 1 Q" j/ E' L# b$ v. y g( [! l
}
5 E; I8 [/ H+ }) _
; n& O9 c& Z- i5 o& [" c0 n+ X3 G, a& y6 l
最主要的部分就在于那个参数超多的CreateFont函数,学过Windows GDI的朋友应该不会陌生。没有学过GDI的朋友,有兴趣的话可以自己翻翻MSDN文档。这里我并不准备仔细讲这些参数了,下面的内容还多着呢:(
* |, R, C5 V" ~' i6 g如果需要在自己的程序中选择字体的话,把selectFont函数抄下来,在调用glutCreateWindow之后、在调用wglUseFontBitmaps之前使用selectFont函数即可指定字体。函数的三个参数分别表示了字体大小、字符集(英文字体可以用ANSI_CHARSET,简体中文字体可以用GB2312_CHARSET,繁体中文字体可以用CHINESEBIG5_CHARSET,对于中文的Windows系统,也可以直接用DEFAULT_CHARSET表示默认字符集)、字体名称。 9 ]# k! c+ r8 A8 ]
效果如图:
! h3 i8 D2 {. d3 u) i
3 E4 h5 D: P6 H. s. ^
8 {) O; Z" _6 }( f# Q6 w2 D7 j3 d6 ]显示中文
6 b6 G7 p# @, Q: _原则上,显示中文和显示英文并无不同,同样是把要显示的字符做成显示列表,然后进行调用。 - \( D5 n' j' y3 n1 D& _
但是有一个问题,英文字母很少,最多只有几百个,为每个字母创建一个显示列表,没有问题。但是汉字有非常多个,如果每个汉字都产生一个显示列表,这是不切实际的。 3 c E" u1 R2 X/ {8 e
我们不能在初始化时就为每个字符建立一个显示列表,那就只有在每次绘制字符时创建它了。当我们需要绘制一个字符时,创建对应的显示列表,等绘制完毕后,再将它销毁。
' H$ a" h; b- }( B) `这里还经常涉及到中文乱码的问题,我对这个问题也不甚了解,但是网上流传的版本中,使用了MultiByteToWideChar这个函数的,基本上都没有出现乱码,所以我也准备用这个函数:) 5 q+ r- X# H0 p1 z7 ?8 F9 [
通常我们在C语言里面使用的字符串,如果中英文混合的话,例如“this is 中文字符.”,则英文字符只占用一个字节,而中文字符则占用两个字节。用MultiByteToWideChar函数,可以转化为所有的字符都占两个字节(同时解决了前面所说的乱码问题:))。 2 |4 ~4 h+ U1 F1 _, t, n2 V; N/ L5 K
转化的代码如下:
4 `9 A- h5 Q* n: k& d! i% {$ E9 v* Q- Q2 y
// 计算字符的个数 2 G- d {$ h# R& j: F
// 如果是双字节字符的(比如中文字符),两个字节才算一个字符 ' j7 ^, L# s4 P' Q ^
// 否则一个字节算一个字符
: O& `: x# M* N2 B! u( Z( hlen = 0; 0 h F% \& `* S L( @
for(i=0; str!='\0'; ++i) 2 U0 Q) Q3 B. S( d+ r
{ 7 K5 A* w& T6 z3 a
if( IsDBCSLeadByte(str) ) r5 d' v# q" Y g$ |9 \' y
++i; 6 j$ n. {. E3 ~& T2 A
++len; 8 W* w: {) H0 w* M7 ^4 b* J5 d
} 9 }0 V, N: s q) i8 W
- Z4 ]7 A: K$ U' b) }
// 将混合字符转化为宽字符
4 ^8 s$ c1 u7 ~; l [4 ywstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
9 p% O0 x5 F9 ~) G6 CMultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
) h4 i' [0 F0 ~% g% _1 s8 p: A! A3 `. dwstring[len] = L'\0'; $ P, x* t9 }; @7 z& w3 n( ~
X- s f; c, Q# e7 S
// 用完后记得释放内存 : i% x, M V6 b" W& j' `5 S6 u
free(wstring); - U) v; n$ j6 O0 a4 I% s3 ` u
5 H/ T+ h* F) l6 `: V: k. P. e
: K: ^% p5 p& @' D9 i加上前面所讲到的wglUseFontBitmaps函数,即可显示中文字符了。 $ e4 ?- X6 m. S2 x# ~
5 P, K( f5 Q5 u' o% k7 M4 ]- svoid drawCNString(const char* str) { % A, Y" T$ M* Z
int len, i;
2 E+ S( ?# [- O7 u# f9 k+ d, p wchar_t* wstring;
2 x2 z+ r. J1 H2 V9 P7 G HDC hDC = wglGetCurrentDC(); % D0 n/ i) j' |( `
GLuint list = glGenLists(1);
' j) h) f, p. X' w" S( j
% N- z2 b7 t3 G; m // 计算字符的个数
" k- w( i: U, h1 q7 h. a. C // 如果是双字节字符的(比如中文字符),两个字节才算一个字符
8 k! N1 G) F+ k `& X // 否则一个字节算一个字符 6 L w8 \% ~( ^8 J; I
len = 0; # N' D! P7 x2 U* U; F ~
for(i=0; str!='\0'; ++i) # [: r. `% Q7 M6 _
{
) H7 v; ~5 N6 x. a4 S2 d; @ if( IsDBCSLeadByte(str) ) , w+ Y/ d! b- Q' l
++i;
5 n- ~' _& a \7 J2 O3 q% S" ? ++len;
6 I+ x6 ?" i% F, r) k3 D9 _9 f } , a" r# \! k, [9 b9 d- g) y9 s
& c M7 x+ G! R# i* M. i% W \ // 将混合字符转化为宽字符
6 T( `$ a+ J: d2 ~" `6 U1 v wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
9 Q) `3 q- @4 M5 Y8 p6 U MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len); 9 b! P1 J9 E. ^ J- z& K
wstring[len] = L'\0';
4 N% R: p" W, w. f3 ]( S& b) b3 W; u, N- M
// 逐个输出字符 9 w/ q% L& [/ B9 i9 Q( A
for(i=0; i<len; ++i) ; j$ T- D8 b5 \6 a
{ ; e/ c/ _9 B! k0 `" U, K- S
wglUseFontBitmapsW(hDC, wstring, 1, list);
. e6 q# \) u9 ~# a/ h8 J glCallList(list);
" S) Q: W2 T, R2 Y3 C, x } ( N& ?9 _+ g. f$ L! k7 U5 w9 _+ ]
6 n: X) C$ H, [; c2 W3 q
// 回收所有临时资源 $ S! O3 p# x7 v1 b( X
free(wstring); 1 H# D8 m2 }: E( T8 ~0 m' k! o
glDeleteLists(list, 1); - C" w6 C" w# F* A' L
} ' _6 d5 f. ?5 |6 R1 K0 }- h
9 Z7 N% s) m! U9 I( ]8 ^) Q# w- g; j; v: Z6 M: R r+ I% e
) |: z6 N: u3 _. b5 U' D' H7 j
注意我用了wglUseFontBitmapsW函数,而不是wglUseFontBitmaps。wglUseFontBitmapsW是wglUseFontBitmaps函数的宽字符版本,它认为字符都占两个字节。因为这里使用了MultiByteToWideChar,每个字符其实是占两个字节的,所以应该用wglUseFontBitmapsW。
! F+ D/ ~' ?8 v$ m" A# R$ @2 y8 u9 n# D! \& c( Y
void display(void) { - [, S# B2 z! Z: q, B. H3 ]" A
glClear(GL_COLOR_BUFFER_BIT);
! g: `/ e. M( g& H
9 w: {* W! E# I5 \: F1 [1 Z: g selectFont(48, ANSI_CHARSET, "Comic Sans MS"); * ^2 `# b7 _0 G# q& L& o% y
glColor3f(1.0f, 0.0f, 0.0f);
6 `/ H% ]$ Z* \$ C9 F% O glRasterPos2f(-0.7f, 0.4f);
4 X( t# X2 |1 g/ C drawString("Hello, World!"); ; a9 m2 m( @7 @% U+ ]& }0 c
; V5 n) I7 _* {! k$ n( }2 C4 l selectFont(48, GB2312_CHARSET, "楷体_GB2312"); 9 W: Q' f. ~( f9 r% h Y
glColor3f(1.0f, 1.0f, 0.0f);
I2 s/ @, @. t( s! F: W/ B% {. | glRasterPos2f(-0.7f, -0.1f); ( Q% h# ~; }6 F3 F* \8 U
drawCNString("当代的中国汉字");
- y- r2 z, ]: j- W+ t. Z: R
7 d: X" \( ~9 _" x" g1 a selectFont(48, DEFAULT_CHARSET, "华文仿宋");
z# v& s* ]4 n* x glColor3f(0.0f, 1.0f, 0.0f); ( L, [# ~& g) q+ d9 P1 f) D
glRasterPos2f(-0.7f, -0.6f); % J7 o, F1 E/ C# L9 N
drawCNString("傳統的中國漢字"); 2 D5 ^# N2 _& p, H
/ d" h& b8 r/ a5 | m glutSwapBuffers(); : p0 g: V+ N2 i H
} 6 d, R% k3 G) m* T0 U6 v' \, T
/ t) @( r: y% W0 Y3 j3 X9 E6 c+ F& v } {0 X* g% V' n
效果如图:8 h0 S! S8 c+ Y- r5 G: x, S) |
|