C++使用Uniscribe进行文字自动换行的计算和渲染(一)

2014-11-24 09:13:54 · 作者: · 浏览: 8

在使用Uniscribe之前,我们先看看利用Uniscribe我们可以做到什么样的效果:

image

image

通过Uniscribe,我们可以获得把各种不同大小的字符串混合在一起渲染的时候所需要的所有数据,甚至可以再漂亮的地方换行,譬如说这里:

image

当然,渲染的部分不包含在Uniscribe里面,只不过Uniscribe告诉我们的信息可以让我们直接计算出渲染每一小段字符串的位置。当然,这也就足够了。下面我来介绍一下Uniscribe的几个函数的作用。

首先,我们需要注意的是,Uniscribe一次只处理一行字符串。我们固然可以把多行字符串一次性丢给Uniscribe进行计算,但是得到的结果处理起来要困难得多。所以我们一次只给Uniscribe一行的字符串。现在我们需要渲染一个带有多种格式的一行字符串。首先我们需要知道这些字符串可以被分为多少段。这在那些从右到左阅读的文字(譬如说阿拉伯文)特别重要,而且这也是一个特别复杂的话题,在这里我就不讲了,我们假设我们只处理从左到右的字符串。

于是我们第一个遇到的函数就是ScriptItemize。
HRESULT ScriptItemize(
_In_ const WCHAR *pwcInChars,
_In_ int cInChars,
_In_ int cMaxItems,
_In_opt_ const SCRIPT_CONTROL *psControl,
_In_opt_ const SCRIPT_STATE *psState,
_Out_ SCRIPT_ITEM *pItems,
_Out_ int *pcItems
);

由于我们不处理从右到左的字符串渲染,也不处理把数字变成乱七八糟格式的效果(譬如在某些邪恶的帝国主义国家12345被表达成12,345),因此这个函数的psControl和psState参数我们都可以给NULL。这个时候我们需要首先为SCRIPT_ITEM数组分配空间。由于一个字符串的item最多就是字符数量那么多个,所以我们要先创建一个cInChars+1那么长的SCRIPT_ITEM数组。在调用了这个函数之后,*pcItems+1的结果就是pItems里面的有效长度了。为什么pItems的长度总是要+1呢?因为SCRIPT_ITEM里面有一个很有用的成员叫做iCharPos,这个成员告诉我们这个item是从字符串的什么地方开始的。那长度呢?自然是用下一个SCRIPT_ITEM的cCharPos去剪了。那么最后一个item怎么办呢?所以ScriptItemize给了我们额外的一个结尾item,让我们总是可以方便的这么减……特别的蛋疼……

好了,现在我们把一行字符串分成了各个item。现在第一个问题就来了,一行字符串里面可能有各种不同的字体的样式,接下来怎么办呢?我们要同时用item的边界和样式的边界来切割这个字符串,让每一个字符串的片段都完全被某个item包含,并且片段的所有字符都有一样的样式。这听起来好像很复杂,我来举个例子:

譬如我们有一个字符串长成下面这个样子:
This parameter (foo) is optional

然后ScriptItemize告诉我们这个字符串一共分为3个片段(这个划分当然是我胡扯的,我只是举个例子):
This parameter
(foo)
is optional

所以,字体的样式和ScriptItemize的结果就把这个字符串分成了下面的五段:
This
parameter
(foo)
is
optional

是不是听起来很直观呢?但是代码写起来还是比较麻烦的,不过其实说麻烦也不麻烦,只需要大约十行左右就可以搞定了。在MSDN里面,这五段的“段”叫做“run”或者是“range”。www.2cto.com

现在,我们拿起一个run,送进一个叫做ScriptShape的函数里面:
HRESULT ScriptShape(
_In_ HDC hdc,
_Inout_ SCRIPT_CACHE *psc,
_In_ const WCHAR *pwcChars,
_In_ int cChars,
_In_ int cMaxGlyphs,
_Inout_ SCRIPT_ANALYSIS *psa,
_Out_ WORD *pwOutGlyphs,
_Out_ WORD *pwLogClust,
_Out_ SCRIPT_VISATTR *psva,
_Out_ int *pcGlyphs
);

这个函数可以告诉我们,这一堆wchar_t可以被如何分割成glyph。这里我们要注意的是,glyph的数量和wchar_t的数量并不相同。所以在调用这个函数的时候,我们要先猜一个长度来分配空间。MSDN告诉我们,我们可以先让cMaxGlyphs = cChars*1.5 + 16。

在上面的参数里,SCRIPT_ANALYSIS其实就是SCRIPT_ITEM::a。由于一个run肯定是完整的属于一个item的,因此SCRIPT_ITEM就可以直接从上一个函数的结果获得了。然后这个函数告诉我们三个信息:
1、pwOutGlyphs:这个字符串一共有多少glyph组成。
2、psva:每一个glyph的属性是什么。
3、pwLogClust:wchar_t(术语叫unicode code point)是如何跟glyph对应起来的。

在这里解释一下glyph是什么意思。glyph其实就是字体里面的一个“图”。一个看起来像一个字符的东西,有可能由多个glyph组成,譬如说“á”,其实就占用了两个wchar_t,同时这两个wchar_t具有两个glyph(a和上面的小点)。而且这两个wchar_t在渲染的时候必须被渲染在一起,因此他们至少应该属于同一个range,鼠标在文本框选中的时候,这两个wchar_t必须作为一个整体(后面这些信息可以由ScriptBreak函数给出)。当然还有1个wchar_t对多个glyph的情况,但是我现在一下子找不到。

不仅如此,还有两个wchar_t对一个glyph的情况,譬如说这些字“ ”。虽然wchar_t的范围是0到65536,但这并不代表utf-16只有6万多个字符(实际上是60多万),所以wchar_t其实也是变长的。但是utf-16的编码设计的很好,当我们拿到一个wchar_t的时候,我们通过阅读他们的数字就可以知道这个wchar_t是只有一个code point的、还是那些两个code point的字的第一个或者是第二个,跟我们以前遇到的MBCS(char/ANSI)完全不同。

因此wchar_t和glyph的对应关系很复杂,可能是一对多、多对一、一对一或者多对多。所以pwLogClust这个数组就特别的重要。MSDN里面有一个例子:

譬如说我们的一个7个wchar_t的字符串被分成4组glyph,对应关系如下:
字符:| c1u1 | c2u1 | c3u1 c3u2 c3u3 | c4u1 c4u2 |
图案:| c1g1 | c2g1 c2g2 c2g3 | c3g1 | c4g1 c4g2 c4g3 |

上面的意思是,第二个字符c2u2被渲染成了3个glyph:c2g1、c2g2和c2g3,而c3u1、c3u2和c3u3三个字符责备合并成了一