写得非常好,很实用。分析到位,原帖地址:http://www.ctsys.cn/files/show_FILES.aspx?id=33
GDI+绘制文字的位置偏移分析
2010-07-19 作者:李海彬
GDI+是Windows操作系统的图形设备接口,在Windows操作系统下,绝大多数具备图形界面的应用程序都离不开GDI,而GDI+是Windows XP和在其后推出的操作系统的一个子系统,是GDI的继承者,出于兼容性考虑,Windows XP仍然支持GDI,但相比于GDI,GDI+对于Windows版本中的GDI进行了优化,增加了许多新的功能,例如支持更多的图片格式、提供了渐变的画刷等等,但在使用GDI+的过程中,发现在(0,0)处绘制文字时,并不是从X轴的0点开始绘制,起绘点右移了,随着文字变大,起绘点离0点越远,这就给我们的文字定位造成很大麻烦,影响了文字排版的美观。
我们使用GDI或GDI+绘制文字时,许多时候必须知道我们要绘制的字符串的宽度和高度(例如一个在屏幕底行走字的程序),而GDI和GDI+都提供了相应的方法来获得指字这符串的宽度和高度,在此我先给出这些代码:
GDI获取字符串宽度和高度代码:
var rc:trect;
st:string;
begin
st:='中国人';
Canvas.Font.Size :=75;
Canvas.Font.Name:= '宋体';
//这个函数可以计算出字符串的RECT;
drawtext(Canvas.Handle ,pchar(st),length(st),rc,DT_CALCRECT);
end
GDI+获取字符串宽度和高度代码:
Uses GDIPAPI,GDIPOBJ, GDIPUTIl;
var g: TGpGraphics;
font: TGpFont;
strFormat: TGpStringFormat;
rc_in,rc_out:TGpRectF;
st:string;
begin
st:='中国人';
g := TGPGraphics.Create(Canvas.Handle );
font := TGpFont.Create('宋体',75 );
strFormat := TGpStringFormat.Create();
//rc_in是绘制窗体的区域,rc_out返回绘制字符串的区域
rc_in.X := Top;rc_in.Y := Left;
rc_in.Width := Width ;rc_in.Height := Height;
//这个函数可以计算出字符串的RECT;
g.MeasureString(widestring(st),length(widestring(st)),font,
rc_in,strFormat,rc_out);
strFormat.Free;
font.Free;
sb.Free;
g.Free;
end;
使用GDI绘制文字时,文字的定位是非常准确的,所以我决定用GDI绘制的文字作为基准,来比较GDI+的偏差,为了方便作比较,我写了一个程序来完成这个功能,程序用Delphi编写,分别在两个IMAGE控件上用GDI和GDI+绘制文字,这两个IMAGE重叠在一起,大小为400*151,其中IMAGE1用绿色来绘制GDI文字;IMAGE2用红色来绘制GDI文字,IMAGE1位于IMAGE2上层,背景透明使下层IMAGE2用红色绘制的GDI+文字可以显示出来, 字号为75,绘制的位置从(0,0)开始,在这里要注意的是,字号为75的单个汉字,其占用的文字宽高度并不是75px,一般是75*1.33333….约等于100px,其算法=字号*(屏幕DPI/72 DPI),一般情况下,我们屏幕的DPI选用96,96/72=1.33333….如果你选120 DPI,就是1.66666….字就会更大些。
以下是效果图(1)
从上图可以看到,相同字体和字号的文字,在相同位置绘制时,它们的位置是有偏差的,GDI绘制的偏左些,GDI+的偏右些,而且选择的字号越大,偏移就越大。从计算的结果可以知道,GDI绘制的文字的大小是300*100,而GDI+是342*127,显然GDI+所占的区域更多些,这是为什么呢?后来我在LorenLiu的专栏(http://blog.csdn.net/LorenLiu/archive/2008/09/07/2894170.aspx)找到答案: 在默认情况下,在测量字符串的长度时,GDI+会添加额外的1/6em长度,为悬垂的字形留出空间,例如斜体字符f。对于em,文中提到如果使用12点的字体,em单位的长度就是12点,根据这个算法,可以计算出,75大小的文字,宽度为100px,1/6em=17px,试着将Image1控件右移17px,两者重合了。
即 偏移=字号*(屏幕DPI/72 DPI)/6
以下是效果图(2)
从上面两个图可以看出,GDI和GDI+绘制的文字,相同字体字号的情况下,绘制文字的大小是相同的,但GDI+在绘制的时候,并不是真的从(0,0)位置开始绘制,而是要留出1/6em的空间(这里是17px),两者的高度差=127-100=27,在高度上,GDI+绘制比GDI多占27px,但从实例图上可以看得出它们重合了,即对于Y轴来说,两者的起绘点都是Y轴的0点,GDI+多出的部分只是空白区域,对排版影响不大。再来看看宽度,前面说了,GDI+起点留了1/6em的空间,而从两者的宽度差来看,342-300=42,减去17px=25px,这25px是不是无用的空白区域呢?
试着只绘制一个“中”字,发现GDI的绘制大小=100*100,而GDI+的=136*127,136-100-17=19,并<>25,说明右边的留白区域并不是25,但也不是17。再换宋体试试,这回发现它们会制的文字不再重合了。
以下是效果图(3)
再将IMAGE1控件右移使“人”字重合。
以下是效果图(4)
第三个字重合时,偏移=23,342-300-23=19,从上面这两个图可以看出,GDI 与GDI+绘制的文字并不一定会重合,虽然文字的大小是一样的,但字间距可能会根据字体的不同有差别,唯一可以确定的是,GDI+在绘制文字时,左右两边至少各留出了1/6em的空间。
最后再试一下,在文字所占区域大于实现绘制区域时,返回的绘字大小是否正确。将两个IMAGE控件调成200*80,运行程序试试。
测试结果是,GDI返回的绘字大小与原来一样,并没有因为IMAGE绘制区域变小而改变,而GDI+返回的是136*80,宽度和仅有一个“中”字时一样,高度是IMAGE2控件的高度,在这种情况下,我们无法准确的计算出GDI+字符串绘制区域的大小,这可能会对我们的计算定位造成影响,折衷的方法是用GDI的方法计算,在计算结果上给宽度加2*1/6em。
通过程序将GDI与GDI+的绘字进行比较,可以总结出以下几点:
(1) GDI+绘字所占区域比GDI大,但两者绘制的实际文字大小是一样的。
(2) 两者的Y轴起绘点相同,但GDI+ X轴的起绘点向右偏移了1/6em。
(3) 当绘制区域 < 所属区域时,GDI+无法正确计算出所属区域的大小。