Wednesday, December 09, 2009

SDL & Object Pascal (Delphi) [2] 显示中文字符

一些基础的问题我不再赘述了,查阅帮助文档(Object Pascal SDL Doc.chm)显然比在这看我胡扯合适得多。那些画像素,获取像素信息,显示BMP文件多数时候只要把那些代码复制过来基本就能工作。下面说的是怎样显示中文。

其实SDL加上ttf支持时(需要将对应的dll文件添加到工程目录里)是完全可以显示中文的,简体和繁体都没问题(Object Pascal版本也不缺功能),但是只能用Unicode编码。这就是说,你必须选择一个Unicode编码完整的字体文件(或者说至少你需要的那些字符是完整的)。Windows下面这样的字体有很多,我推荐的有:

简体中文:宋体大字符集,微软雅黑;

繁体中文:细明体,微软正黑,标楷体。

这些基本是中文地区最重要的几种字体。一个缺憾就是用于显示简体中文的楷体(和其他字体)优秀的并不多。

输出中文时,可以先写这样的一个子程作为基础:

procedure DrawText(word: PUint16; x_pos, y_pos: integer; color: Uint32);
var
  text: PSDL_Surface; 
  dest: TSDL_Rect;
begin 
  text := TTF_RenderUNICODE_blended(font, word, TSDL_Color(Color));
  dest.x := x_pos;
  dest.y := y_pos;
  SDL_BlitSurface(text,nil, screen, @dest);
  SDL_FreeSurface(text);
end;

screen就是已经定义好的那个主屏幕的标记。font是一个指向字体文件的指针,可以这样定义和初始化:

全局变量:
font: PTTF_Font;

在初始化状态的子程中:
TTF_Init;
font:=TTF_OpenFont('kaiu.ttf', 20);

当然font的定义和初始化写在DrawText里面也一样,只是写在全局变量里面就可以只初始化一次,可能会效率高一些。同样把DrawText里面用的那个text写成全局变量也是一个选择。在程序结束时,还要释放这些资源。

kaiu就是标楷体的文件名字,20是字号,需要把字体文件也放在工程目录里面。

TTF_RenderUNICODE_blended这个函数是以blended效果输出文字,以TTF_RenderUNICODE开头的函数有3个,对应的是不同的效果。

SDL_BlitSurface大致可以理解为在“表面”上再贴一层。

调用DrawText的方法如下:

str: WideString;
......

str:=' 这是一个显示中文的测试';
DrawText(@str[1], 100, 100, $FFFFFF);
Sdl_UpdateRect(screen, 0, 0, screen.w, screen.h);

注意一定要用WideString(或PWideChar)类型!

str前面我添加了一个空格。这是因为我发现有些字体中的某些字符似乎包含自动缩进(原因不明),它们在作为首字符时整个字符串会右移,于是我干脆把首字符全都设成了空格(如果有人知道原因以及更好的解决方案,还请不吝赐教)。100,100,$FFFFFF就是位置和颜色了,这里我是随便写了一个颜色(应该是白的)。最后那句就是更新屏幕上的一个矩形区域了,不更新是不会有效果的。

如果用WideString类型的话,str的第0位保存的是字串长度,所以用的是str[1],由于DrawText的第一个参数是指针所以加了@。这个调用方法似乎有点别扭,当然如果在DrawText里多写几行代码也可以有其他的封装方案。

这样做基本上显示中文就可以了。但是有时你可能面对已经做好的资源文件,里面使用了其他的编码方案,比如繁体地区常用的Big5码。这时需要用到一个API函数 MultiByteToWideChar 。

这个API的参数还是有点麻烦的,可以写一个函数将它封装:

function Big5ToUnicode(str:PChar): widestring;
var
  len: integer;
begin
  len:=MultiByteToWideChar(950,0,PChar(str),-1,nil,0);
  setlength(result,len-1);
  MultiByteToWideChar(950, 0, PChar(str), -1, pwidechar(result), len+1);
  result:=' '+result;
end;

MultiByteToWideChar(950, 0, PChar(str), -1, pwidechar(result), len+1) 各个参数含义:

950:码表,950即Big5和Unicode的对应表;
0:这一位写0就可以了;
PChar(str):字符串的地址(这里又用PChar强行转换了一次,应该没必要);
-1:字符串长度,如果是-1就会自动计算长度并转换到字符串结束;
pwidechar(result):保存结果的地址;
len+1:缓冲区大小,如果为零函数就返回需要的缓冲区大小。

在Big5ToUnicode函数中MultiByteToWideChar使用了两次,第一次是计算所需缓冲区大小,第二次才是正式的转换。结果的长度设成多少需要实验一下,因为结尾或许有不想要的字符。但是不能不设或设成0,这样result并没有获得地址分配,转换会出错。至于后面在结果前又加了个空格,就看效果而定了。

在这里形参并没有用String而是PChar,是因为从资源文件中读出的数据不一定是读到String里面,甚至可能读到整数数组里,使用@算符传入地址就可以了。后面还可以利用这两个函数再写一个DrawBig5Text。

如果是GB2312,码表号是936。大部分的转码软件都是使用了这个API,GB2312与Big5的互相转换也是通过Unicode进行的。

同样还有一个WideCharToMultiByte的API,用法是类似的(Char和WideChar的位置当然相反),但是后面的参数多了两个,如果不知是干什么的可以都写成nil。

这个API是Windows提供的,所以这是在Windows下面比较简单的实现方法。但是如果作跨平台开发就不适合了。

最后是整个程序的代码。需要注意的是这份代码并未对打开设备失败进行处理。在你正式编写一个游戏时,判断一下经常是必要的。

unit myfunctions;

interface

uses
  sysutils,
  windows,
  sdl,
  sdl_ttf;

procedure Run;
procedure DrawText(word: PUint16; x_pos, y_pos: integer; color: Uint32);

implementation

var
  screen: PSDL_Surface;
  event: TSDL_event;
  font: PTTF_Font;

procedure Run;
var
  str: WideString;
begin
  SDL_Init(SDL_INIT_VIDEO);
  screen := SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE or SDL_DOUBLEBUF);
  TTF_Init;
  font := TTF_OpenFont('kaiu.ttf', 20);
  str := ' 这是一个显示中文的测试';
  DrawText(@str[1], 100, 100, $FFFFFF);
  Sdl_UpdateRect(screen, 0, 0, screen.w, screen.h);
  while SDL_PollEvent(@event) >= 0 do
  begin
    if event.type_ = SDL_QUITEV then break;
    Sdl_delay(10);
  end;
  TTF_Quit;
  SDL_Quit;
  exit;
end;

procedure DrawText(word: PUint16; x_pos, y_pos: integer; color: Uint32);
var
  text: PSDL_Surface;
  dest: TSDL_Rect;
begin
  text := TTF_RenderUNICODE_blended(font, word, TSDL_Color(Color));
  dest.x := x_pos;
  dest.y := y_pos;
  SDL_BlitSurface(text, nil, screen, @dest);
  SDL_FreeSurface(text);
end;

end.

No comments: