原文 3 w- H1 j; g2 ~2 T
http://blog.chinaunix.net/u/26313/showart_1663543.html $ @+ g6 [7 ?6 N. ~" g( j; Z6 A
+ S) A9 w2 V1 j' ?, Y内容超多的一课!不过我想精彩的程度也一定不会让大家失望。大家不妨先浏览一下课程里的图片:)。
8 G _4 K3 P. @3 d+ L" D+ d3 s( Z6 C' C
本课我们来谈谈如何显示文字。 3 v; F( X! Y q8 Y. G
OpenGL并没有直接提供显示文字的功能,并且,OpenGL也没有自带专门的字库。因此,要显示文字,就必须依赖操作系统所提供的功能了。
# E& N9 L, V6 R1 k6 A) l& Y各种流行的图形操作系统,例如Windows系统和Linux系统,都提供了一些功能,以便能够在OpenGL程序中方便的显示文字。 B7 Y) g- |& S; |8 A7 f
最常见的方法就是,我们给出一个字符,给出一个显示列表编号,然后操作系统由把绘制这个字符的OpenGL命令装到指定的显示列表中。当需要绘制字符的时候,我们只需要调用这个显示列表即可。 ' k5 n: G* W& [' V' z7 ]6 |
不过,Windows系统和Linux系统,产生这个显示列表的方法是不同的(虽然大同小异)。作为我个人,只在Windows系统中编程,没有使用Linux系统的相关经验,所以本课我们仅针对Windows系统。
4 i( r2 N/ H# M+ x' j5 E" T8 z- s9 z& O$ b! e: O! ^# ]
OpenGL版的“Hello, World!”
& o5 W. X3 T' y" F写完了本课,我的感受是:显示文字很简单,显示文字很复杂。看似简单的功能,背后却隐藏了深不可测的玄机。 ! F* Z2 x7 \! l, ?1 a! h8 H
呵呵,别一开始就被吓住了,让我们先从“Hello, World!”开始。 ' p1 C- o( @3 g6 ]& S! f( _
前面已经说过了,要显示字符,就需要通过操作系统,把绘制字符的动作装到显示列表中,然后我们调用显示列表即可绘制字符。
9 |) o: I. t6 u) b$ V$ I, w( u/ A1 E* n假如我们要显示的文字全部是ASCII字符,则总共只有0到127这128种可能,因此可以预先把所有的字符分别装到对应的显示列表中,然后在需要时调用这些显示列表。
! M9 ? K" q& u6 o4 xWindows系统中,可以使用wglUseFontBitmaps函数来批量的产生显示字符用的显示列表。函数有四个参数:
( K6 _" t6 p: v7 X" }第一个参数是HDC,学过Windows GDI的朋友应该会熟悉这个。如果没有学过,那也没关系,只要知道调用wglGetCurrentDC函数,就可以得到一个HDC了。具体的情况可以看下面的代码。
6 k/ r" |& b/ v( O* t. O第二个参数表示第一个要产生的字符,因为我们要产生0到127的字符的显示列表,所以这里填0。 ) u* v7 z3 y4 t3 E( b3 ]4 ?" }9 o
第三个参数表示要产生字符的总个数,因为我们要产生0到127的字符的显示列表,总共有128个字符,所以这里填128。
6 s; \0 K3 y U J6 S! @ [! h% i第四个参数表示第一个字符所对应显示列表的编号。假如这里填1000,则第一个字符的绘制命令将被装到第1000号显示列表,第二个字符的绘制命令将被装到第1001号显示列表,依次类推。我们可以先用glGenLists申请128个连续的显示列表编号,然后把第一个显示列表编号填在这里。 , }7 W" U" S0 y2 C6 a" |
还要说明一下,因为wglUseFontBitmaps是Windows系统特有的函数,所以在使用前需要加入头文件:#include <windows.h>。
# h$ \/ F4 T2 j' P现在让我们来看具体的代码:
1 M5 m# l; o0 Q+ [4 E9 v% u" s0 p
* a% U( p5 M& f! u#include <windows.h> ( i* z$ b+ |. U
9 S# [% w, x7 d5 L/ A# ^" O! L4 z// ASCII字符总共只有0到127,一共128种字符
" l# T; q0 T9 i* X" ?" u#define MAX_CHAR 128 $ |1 ?4 m8 V1 q$ | G+ D
. F' p, ~4 x1 j1 n
void drawString(const char* str) { * t% r; f3 ]: p8 r9 w" Q; N
static int isFirstCall = 1;
3 I5 e8 _% Z& Y6 t static GLuint lists;
1 h% U& ]4 l2 E1 ?& `+ Y3 C* |9 v/ Y6 i! k$ I7 C
if( isFirstCall ) { // 如果是第一次调用,执行初始化 5 e" d; i0 b% v& _& }
// 为每一个ASCII字符产生一个显示列表 ' W2 k+ N! v( F
isFirstCall = 0;
& P7 O1 c5 R, `' Y+ t; p. ?6 Z% t( Z" g
// 申请MAX_CHAR个连续的显示列表编号
( q E4 H& X8 E3 r0 [# p! g lists = glGenLists(MAX_CHAR);
3 r( b3 b- \' B) `: a$ f2 J5 g: ]" `# K; q. P: M" T
// 把每个字符的绘制命令都装到对应的显示列表中 6 f. C2 G0 s& F# U. p* g' e
wglUseFontBitmaps(wglGetCurrentDC(), 0, MAX_CHAR, lists);
7 M9 [) X. Z0 m: Q, O( Y } & o) Y9 \6 ?% U. |
// 调用每个字符对应的显示列表,绘制每个字符 ' ?' k2 W: ?$ o" a3 g; L2 `
for(; *str!='\0'; ++str) 8 Q2 ~2 s/ `2 d8 H+ r5 m
glCallList(lists + *str);
7 |. p# B/ m% r7 k}
: ?" n1 z8 W0 r9 i1 b; o" b! k* q9 G7 w* B' c
6 `# o6 D |; [# D. E5 r: s/ K# T* i
显示列表一旦产生就一直存在(除非调用glDeleteLists销毁),所以我们只需要在第一次调用的时候初始化,以后就可以很方便的调用这些显示列表来绘制字符了。
1 ^& }. \1 D5 x绘制字符的时候,可以先用glColor*等指定颜色,然后用glRasterPos*指定位置,最后调用显示列表来绘制。 # C4 s1 p# F3 f, {6 z; t/ x5 }
* s) @1 k& T$ ?. t* b, mvoid display(void) { " [% h' Q2 k( c4 E2 t! p
glClear(GL_COLOR_BUFFER_BIT);
% [# i. N. k6 ^; Z# e
7 l$ `( U4 [9 p9 o% Q glColor3f(1.0f, 0.0f, 0.0f);
" i$ K; z, m3 a glRasterPos2f(0.0f, 0.0f);
* L5 ^6 v7 Y4 P* r) @6 O" \ drawString("Hello, World!");
& O5 m3 j( m0 I& N
7 }3 A# ?; O! H) u9 ~% P) a$ j glutSwapBuffers(); 9 F1 b9 o! F' Z% Q2 ` k* q
}
5 W% e& Y6 S4 Q- g3 u3 X% I }8 J* Y& @2 @
# e3 Y" [) _8 o0 p n+ k5 O+ A) @; }6 K, W
效果如图:
' n3 k: G. G+ |4 f7 q4 [
) _" V4 j( A( D! v6 F: }, @, B
. C# d0 G" } v* |3 s( b7 _指定字体
0 k+ i. p+ g4 F在产生显示列表前,Windows允许选择字体。 # K( S0 s) g: g
我做了一个selectFont函数来实现它,大家可以看看代码。
( h6 ^; D* C4 c2 @6 t7 m" m7 m; J5 `" f$ q& k; \1 J) V! j% F! [1 F
void selectFont(int size, int charset, const char* face) { + Q3 g. `3 p/ i0 P+ O( q w
HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0,
* s/ M$ u t1 Y$ H6 m* T+ l charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
9 d( e' ~8 V* u& C- Z) m; ] DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face); ; U$ o6 H: P' P0 A
HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont);
8 Y8 v4 W4 Q& r; {& t% R" x DeleteObject(hOldFont);
0 Q0 c+ |$ h& e: K4 P5 X4 |: d}
' Q+ _( O3 y+ f8 j$ Z! C* a. U3 Y4 H: e
void display(void) {
7 `+ S6 B1 Q2 C& G( r selectFont(48, ANSI_CHARSET, "Comic Sans MS"); % x3 |# S+ P" v# t+ X5 }' t
( N3 |. n& r, Z% n- P/ I
glClear(GL_COLOR_BUFFER_BIT); " ?7 k( F9 e: r# L3 {7 K
8 F$ R, Y% r8 R" J glColor3f(1.0f, 0.0f, 0.0f);
+ F* w* S+ R; T' O4 [( G7 Z9 E glRasterPos2f(0.0f, 0.0f); " t7 S; m, N+ Z( ~1 Y ~1 }
drawString("Hello, World!"); ) S0 E' P9 i( n- r% E
- A% E$ y, R6 l: _) O9 j glutSwapBuffers();
8 v8 J: n& k- F7 a} - s H( X; O3 G! R; C
4 G! J4 k7 R3 @& m) R& h! T
0 J- O2 m6 p$ X& ~
最主要的部分就在于那个参数超多的CreateFont函数,学过Windows GDI的朋友应该不会陌生。没有学过GDI的朋友,有兴趣的话可以自己翻翻MSDN文档。这里我并不准备仔细讲这些参数了,下面的内容还多着呢:( . G$ V! \. s' r `
如果需要在自己的程序中选择字体的话,把selectFont函数抄下来,在调用glutCreateWindow之后、在调用wglUseFontBitmaps之前使用selectFont函数即可指定字体。函数的三个参数分别表示了字体大小、字符集(英文字体可以用ANSI_CHARSET,简体中文字体可以用GB2312_CHARSET,繁体中文字体可以用CHINESEBIG5_CHARSET,对于中文的Windows系统,也可以直接用DEFAULT_CHARSET表示默认字符集)、字体名称。
/ j, R# [8 v1 l7 ~) v效果如图:
. f9 z1 L' V2 c" p# L$ ~9 H9 C+ C: E
4 d. ?$ Q' D. @/ C4 v! W* ]8 C9 w2 ]- u0 r5 B% ^7 Y& U, w
显示中文 * ^$ d3 [- ` x& n4 G
原则上,显示中文和显示英文并无不同,同样是把要显示的字符做成显示列表,然后进行调用。 2 C/ E. i: o3 x, J& N
但是有一个问题,英文字母很少,最多只有几百个,为每个字母创建一个显示列表,没有问题。但是汉字有非常多个,如果每个汉字都产生一个显示列表,这是不切实际的。 $ x; L2 J1 v$ V( d* X' _4 `
我们不能在初始化时就为每个字符建立一个显示列表,那就只有在每次绘制字符时创建它了。当我们需要绘制一个字符时,创建对应的显示列表,等绘制完毕后,再将它销毁。 ! N R+ g* b9 g. P' f
这里还经常涉及到中文乱码的问题,我对这个问题也不甚了解,但是网上流传的版本中,使用了MultiByteToWideChar这个函数的,基本上都没有出现乱码,所以我也准备用这个函数:) / ~$ A9 d7 l$ k2 ]
通常我们在C语言里面使用的字符串,如果中英文混合的话,例如“this is 中文字符.”,则英文字符只占用一个字节,而中文字符则占用两个字节。用MultiByteToWideChar函数,可以转化为所有的字符都占两个字节(同时解决了前面所说的乱码问题:))。 9 h8 S. r% ]/ r# W4 r
转化的代码如下:
8 P0 T; @! q, N6 [6 q9 v* _$ f; U! Q; O8 \/ S& `
// 计算字符的个数 & b6 R; Q2 W3 U8 P. e& o0 x* F
// 如果是双字节字符的(比如中文字符),两个字节才算一个字符 ) k3 ~/ E# ~$ W
// 否则一个字节算一个字符
R7 J `2 \& p1 Q, ulen = 0;
/ Y, u# n- f( e0 j6 Kfor(i=0; str!='\0'; ++i) ' U0 E3 q* z2 I, z
{ - X) M Q, R1 [2 [, {1 F6 d, u, I
if( IsDBCSLeadByte(str) )
( }: e( T& O( P' P ++i;
) F1 U. J. ]: e% ?! Q0 O ++len;
4 c% | `5 g4 h% j& w$ u8 \} 3 {# p" n. U# Z2 y
! |. V [4 l/ D
// 将混合字符转化为宽字符
# h2 }+ y6 b2 i5 o0 b2 `/ q4 H, y% rwstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t)); 5 k+ b9 S$ D; e5 @6 O) x! Q
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len); A, G$ J# `- K+ n5 j
wstring[len] = L'\0';
5 i6 s# D; R% ] B
7 c5 K( I1 N, l8 m- J& r// 用完后记得释放内存 $ i+ k! N' k" }1 N) p0 I2 W, ]
free(wstring); 5 j, T) @" f+ u" v' _
5 g4 H9 z/ |0 j
: z; W, Z' P( @6 x4 S% g" V! Z
! m) g) t9 e3 Q- p$ Y; a! b
加上前面所讲到的wglUseFontBitmaps函数,即可显示中文字符了。 - P8 R+ @9 |+ }# p. s P
" c; ~% L; t5 U0 G. G/ o0 x- j% b
void drawCNString(const char* str) { , S/ `( }! a7 _2 @7 V
int len, i; " T3 s6 j/ ]/ T5 e6 @
wchar_t* wstring;
8 q& P* b2 B( e0 G0 K, ]- e' v HDC hDC = wglGetCurrentDC(); : \! R/ m x# @2 Y) G! F3 b3 H
GLuint list = glGenLists(1); 0 f$ C2 `. R: {1 r. P1 ^( K6 o
8 s4 [/ k, k" p; E$ U
// 计算字符的个数 4 k% G: p7 A. w3 [; h+ T7 P
// 如果是双字节字符的(比如中文字符),两个字节才算一个字符 1 _' c+ C9 o5 @# q) [" I
// 否则一个字节算一个字符
1 _& C6 }, z7 D1 X* e len = 0;
: m6 x. B( X8 k for(i=0; str!='\0'; ++i) / D' B# C. m1 a6 @/ o0 R" D3 C
{
2 d# \$ _$ G/ L, W( `* r' Y2 E I; c if( IsDBCSLeadByte(str) )
$ l' o8 l* G% _$ R2 h ++i; 5 d5 c: n2 Q7 Y% H5 x
++len;
0 W8 r" R' l0 Q# y4 r } 5 D T& |+ O( `. J5 ^
2 F( h# |6 z+ V, J4 q, W# z // 将混合字符转化为宽字符
6 K6 D, z( {' s5 x wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
/ W8 X% M& D B( G) \& k. o MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len); - R1 J' i: c4 G+ b8 p; s
wstring[len] = L'\0'; ( Y6 e# U4 x' X- `4 |
/ j" r3 u4 x# b$ S0 O& }6 [
// 逐个输出字符
/ m! g2 U0 ~( R& n5 ?$ T for(i=0; i<len; ++i)
3 p5 A+ Y, b) q" I" f* e% a {
6 N/ E% W- P8 T0 E; X4 l wglUseFontBitmapsW(hDC, wstring, 1, list);
* \( G2 l: y; h/ Z9 v7 N glCallList(list);
6 J% B6 ?7 ]" o. V3 f" B! h } . P7 D. [; h# S6 [
. s% ~; O5 a$ N" t) X/ ?8 F2 @
// 回收所有临时资源
2 K7 B5 W4 _* ^! t7 e: u( c free(wstring);
: o7 {, @$ K& b* N8 W: g glDeleteLists(list, 1); 0 X% A; n" k/ r1 A3 F
} 3 ^2 {5 V. V8 P( _' Z
" J. ^. v: V& Z
; w. q$ E) } M' Z
, x& `0 ^0 P2 J% Y" G5 m/ L注意我用了wglUseFontBitmapsW函数,而不是wglUseFontBitmaps。wglUseFontBitmapsW是wglUseFontBitmaps函数的宽字符版本,它认为字符都占两个字节。因为这里使用了MultiByteToWideChar,每个字符其实是占两个字节的,所以应该用wglUseFontBitmapsW。 0 y- m, n* G4 [+ n8 O" h3 z/ E; m7 Q
1 j/ g% q2 K) C* N* e- |5 Y: v
void display(void) {
% @1 q% _) R0 U2 V* ]8 E8 g- K2 i glClear(GL_COLOR_BUFFER_BIT);
- F9 p7 T" h* ]0 {; Q/ u
6 f# ~4 T' m; `) O( R selectFont(48, ANSI_CHARSET, "Comic Sans MS"); 8 m# E1 g+ c y, H3 s$ y5 W
glColor3f(1.0f, 0.0f, 0.0f); , o$ A3 I8 N( t9 m, l
glRasterPos2f(-0.7f, 0.4f); , T, j1 s/ K" `. }* q& T
drawString("Hello, World!");
8 U, t1 X9 O! O7 l W* ^ V/ r9 k8 x
selectFont(48, GB2312_CHARSET, "楷体_GB2312"); 2 a9 g( A) x3 _3 Y8 U
glColor3f(1.0f, 1.0f, 0.0f);
3 x* S% T4 C& @& ?# V. D' }& K glRasterPos2f(-0.7f, -0.1f); % Q- G$ G+ |6 C4 Z2 Z4 {1 s
drawCNString("当代的中国汉字");
! r6 V y7 L% m2 C3 S0 u7 k: g
selectFont(48, DEFAULT_CHARSET, "华文仿宋");
7 ?7 I) L+ b( k1 t, t1 { glColor3f(0.0f, 1.0f, 0.0f); , K! f, ?; \! m* h0 ?* t" V' C$ ~* Q
glRasterPos2f(-0.7f, -0.6f); ' [" z! L4 c3 _8 {* ~/ G- [5 [
drawCNString("傳統的中國漢字");
) [# j' _* o5 Q% j! a" }4 u" |+ P- N U) Y" Q. C9 e, e3 F
glutSwapBuffers();
* v# X* {5 ?! ~ X- B6 r} ' I5 h$ L. m8 Z: G/ m* e
* L- n( D" n; O ?: w9 e% u# l0 D
, F7 _* s8 H6 Q( I6 F/ A! g+ Q$ k' g效果如图:
& b3 D7 H! p p. r7 h |