这篇文章的目的是对微软对Uniscribe绘制复杂文本的例子进行一些说明,同时主要是对ScriptItemize、ScriptShape、ScriptPlace、ScriptLayout、ScriptTextOut等函数以及一些绘制复杂文字过程中要使用的结构体如SCRIPT_ANALYSIS、SCRIPT_CACHE、SCRIPT_STATE、SCRIPT_CONTROL等进行说明:
对于利于ScriptString绘制复杂文字,它比较简单,所以在最后给出说明:
本文作者通过查阅相关资料给了自己的理解:
首先对于复杂文字,由于其有不同于一般文字的特性,所以要求绘制时要具体考虑绘制方法,以正确显示文字,至于其特征可参照微软给出的官方资料:
针对微软给出的例子:
例子中从资料中读取一个文件,文件的数组是Unicode文本,由于Unicode编码中一个具体的Unicode值对应一个文字,所以对于世界任何语言中一个字符Unicode编码是唯一的。
进行函数:
我们先定义一些变量用以绘制过程中使用:
typedef struct tag_SCRIPT_ANALYSIS
{
WORD eScript :10; //图形引擎
WORD fRTL :1; //绘制方向
WORD fLayoutRTL :1; // Set for GCP classes ARABIC/HEBREW and LOCALNUMBER
WORD fLinkBefore :1; // Implies there was a ZWJ before this item
WORD fLinkAfter :1; // Implies there is a ZWJ following this item.
WORD fLogicalOrder :1; // Set by client as input to ScriptShape/Place
WORD
fNoGlyphIndex :1; // Generated by ScriptShape/Place - this item does not use glyph indices
SCRIPT_STATE s;
} SCRIPT_ANALYSIS;
这是一个重要的结构体:
该结构体在绘制的过程中经常要使用,至于其成员的含义可查阅MSDN:
假设我们有一段文本。文本放在一个名称为m_wcBuf的WCHAR类型的数组内:
1、HRESULT WINAPI ScriptItemize(
const WCHAR *pwcInChars要分析的字符串指针 , //
int cInChars, //字符数量
int
cMaxItems要处理的SCRIPT_ITEM结构体的最大数量, //
const SCRIPT_CONTROL *psControl指针, //SCRIPT_CONTROL
const SCRIPT_STATE *psState指针, //SCRIPT_STATE
SCRIPT_ITEM *pItems接收SCRIPT_ITEM指针, //
int *pcItems接收处理的SCRIPT_ITEM数量 //
);
#define MAXITEM 200
SCRIPT_ITEM
Item[MAXITEM];
int
cItems;
ScriptItemize(&m_wcBuf , strlenw(m_wcBuf), MAXITEM , NULL , NULL , &Item ,&cItems);
那么这条语句执行完毕后,我们以m_wcBuf中的文本生成了一个结构体数组,保存在Item中.
typedef struct tag_SCRIPT_ITEM {
int iCharPos;
SCRIPT_ANALYSIS a;
} SCRIPT_ITEM;
SCRIPT_ITEM这个结构体中有一个成员
iCharPos;它代表了相对ScriptItemize()函数的pwcInChars字符数组起始位置的字符偏移量。一个SCRIPT_ITEM对应pwcInChars中的一段文字,ScriptItemize()函数调用完成后,把pwcInChars中的字符串划分成了cItems个SCRIPT_ITEM.每个SCRIPT_ITEM中都包含了对应于pwcInChars中的字符信息。如iCharPos,代表偏移量。
我们可以定义一个
RUN结构体:
struct tagRUN
{
struct tagRUN * pNext;
int iLen;
int iStyle; //风格样式表序号
(全局 'g_style')
SCRIPT_ANALYSIS analysis; //Uniscribe 解析
}
依据
ScriptItemize函数的返回的数组Item,我们以每个SCRIPT_ITEM结构体的信息来填充这个结构体。并以逻辑顺序,就是Item数组中的元素排列顺序形成一个节点类型为RUN的链表。
这时就可以说,我们已经把一段文本划分成了
RUN.
2、
ScriptShape与ScriptPlace.
CRect
rc;
假设我们有一个窗口,矩形大小为
rc.我们要在这个窗口中显示文本.假设我们这时获得了RUN链表指针为RUN *pFirstRun;显示文本要考虑窗口的宽度。因为我们要确保无论窗口如何改变大小,文字应依据窗口大小而重新排版以确保完整的显示。
先为
ScriptShape定义参数
HDC
hdc;
hr = ScriptShape(hdc,
&m_style[*piCurStyle].sc,
m_wcBuf + iPos,
iLen,
MAXGLYPHS,
psa,
pGlyphs,
pClusters,
pVisattrs,
pcGlyphs);
参数的解释按顺序给出
hdc
[in] 设备环境句柄.
psc
[in/out] 一个
SCRIPT_CACHE结构指针
pwcChars
[in] 一个包含run的Unicede字符串指针.
cChars
[in] 在Unicode run中的字符数量.
cMaxGlyphs
[in] Maximum number of glyphs to generate. 生成字形的最大数.
psa
[in/out] Pointer to the
SCRIPT_ANALYSISstructure for the run, containing the results from an earlier call to
ScriptItemize.
一个关于run的SCRIPT_ANALYSIS结构体指针,包含早期调用ScriptItemize而获得的结果.
pwOutGlyphs
[out] 接收字形的数组指针.
pwLogClust
[out]一个接收逻辑簇信息的数组指针。在*pwLogClust中的每个元素对应*pwcChars中的一个字符。每个元素的值是从在run中首个字形到包含相应字符的簇中第一个字形的偏移量。当*
psa.fRTL为TRUE时,当读取数组时在*pwLogClust中的元素递减。
psva
[out]
一个SCRIPT_VISATTR结构体数组的指针,用以接收可视属性信息。每个字形有一个可视属性,因此*
psva有
cMaxGlyphs个元素.
pcGlyphs
[out]
一个整数指针,用以接收一个写入
pwOutGlyphs中的字形编号(数量)计数
我们只关心后面的四个输出参数
:
pwOutGlyphs:字形数组
pwLogClus:逻辑
clusters数组。元素值为字形数组首字形到包含相应字符的簇中的第一个字形的偏移量。
至此我们得到了字形数组和逻辑
clusters数组。pwOutGlyphs中保存的是字形。可能一个字符由多个字形表示,也可能多个字符由一个字形表示。
pwLogClust是逻辑
cluster,它与宽字符数组pwOutGlyphs相对应.每个元素值代表该字符所对应的字形在字形数组中的偏移量。簇实际上最小的显示单位,它由一个或多个符组成。而这一个或多个字符对应一个或多个形。在同一个簇中如果该簇对应于字符数组中的多个字符,则该簇的大小与字符数组中这多个字符的数量是相等的,也就是说,逻辑簇数组中的每个元素与字符数组中元素一一对应。那么如何划分簇呢,是由字形划分的。如果多个字形来表示一个簇,(这里这个簇可以对应多个符,也可以对应一个字符),如果这个簇对应多个字符,则这个簇中的每个元素对应字符数组中同样数量的字符,这个簇中的每个元素的值是相同的,就是表示这个簇的字形在字形数组中偏移量。由于簇是由字形来划分的。也可能一个字形来表示一个簇,也可能多个字形来表示一个簇。在这里,如果多个字形来表示一个簇,则这个簇中元素的值是这个字形组合的首字形相对于字形数组首字形的偏移量。
这们我们获得了逻辑
clusters和字形数组。以及字形数组的大小和一个对应于每个字形信息的SCRIPT_VISATTR结构数组。
下面我们调用
ScriptPlace();
hr = ScriptPlace(hdc,
&m_style[*piCurStyle].sc,
pGlyphs,
*pcGlyphs,
pVisattrs,
psa,
piAdvances,
pGoffsets,
&abc);
HRESULT WINAPI ScriptPlace(
HDC hdc,
SCRIPT_CACHE *psc,
const WORD *pwGlyphs,
int cGlyphs,
const SCRIPT_VISATTR *psva,
SCRIPT_ANALYSIS *psa,
int *piAdvance,
GOFFSET *pGoffset,
ABC *pABC
);
参数
hdc
[in] 设备环境句柄
psc
[in/out]一个SCRIPT_CACHE结构指针。
pwGlyphs
[in]
从先前调用ScriptShape函数而获取的一个字形缓冲区指针。
cGlyphs
[in]在字形缓冲区中的字形数量。
psva
[in]一个SCRIPT_VISATTR结构数组指针。
psa
[in/out]从先前调用ScriptItemize函数而获得的一个SCRIPT_ANALYSIS结构数组指针。
piAdvance
[out]一个数组指针用于接收前进宽度信息。可能是每个字形的宽度,即每个字形的abcA+abcB+abcC.
pGoffset
[out]
一个GOFFSET结构指针用于接收组合字形的x和y偏移量.
pABC
[out]
一个ABC结构指针用于接收整个run的ABC宽度。
我们获得了每个字形的宽度数组
piAdvances,以及这个RUN所占用的总宽度abc。
piVdvances对应于每个字符,它取得了每个字形所占用宽度。
如果我们以行为单位来绘制文字,我们可以以一个循环来取得每个
RUN的宽度信息,并一直相加,假设我们己取得了一些run的宽度和,假设这个宽度和为LineWidth.当我们再取得链表中下一个run的宽度(假设为lo)与这个宽度相加后超过一个行的宽度。我们假设这个run的索引为n即在链表中的节点顺序由头节点向后遍历的顺序为第n个。
我们可以中断这个
run即把这个run分为两个run,所谓划分实际上也就是把这个run的字符分为两部分。
可以这样实现:
hr = ScriptGetLogicalWidths(&pRun->analysis,
pRun->iLen,
cGlyphs,
piAdvances,
pClusters,
pVisattrs,
logwidths);
ScriptGetLogicalWidhts函数转换一个指定字体的字形向前宽度为逻辑宽度。
HRESULT WINAPI ScriptGetLogicalWidths(
const SCRIPT_ANALYSIS *psa,
int cChars,
int cGlyphs,
const int *piGlyphWidth,
const WORD *pwLogClust,
const SCRIPT_VISATTR *psva,
int *piDx,
);
psa
[in]一个
SCRIPT_ANALYSIS 结构指针.
cChars
[in] 在RUN中的逻辑编码点数量
cGlyphs
[in] 在一个RUN中的字形数量
piGlyphWidth
[in] 字形向前宽度的数组指针
pwLogClust
[in] 逻辑Cluster的数组指针
psva
[in]
SCRIPT_VISATTR 结构指针
piDx
[out] 逻辑宽度的数组指针
可以看出这个函数的最后一个参数为输出参数,它代表逻辑宽度的数组。即每个字形所占用的宽度数组。
然后我们可以以一个循环来判断具体到哪个字符时满足不超过窗口宽度的条件。
iChars = 0;
iWidth = 0;
while(iChars < pRun->iLen && iWidth + logwidths[iChars] < iMaxWidth)
{
iWidth += logwidths[iChars];
iChars++;
}
pRun代表我们当前的
run。iWidth代表当前run中某个字符的逻辑宽度,iMaxWidth代表窗口剩余的宽度即剩余了iMaxWidth的宽度,但这个宽度不足以显示这个run.
通过上面这个循环,我们找到了这个字符的位置。
if(iChars < pRun->iLen )
{
pNewRun = new RUN;
*pNewRun = *pRun;
pRun->pNext = pNewRun;
pRun->iLen = iChars;
pNewRun->iLen -= iChars;
}
我们已经划分了这个
RUN。
第二步已经完成,总结第二步,主要目的是确定一行
(窗口宽度)要显示多少个RUN,并进行相应的划分。
3、ScriptLayout
BYTE
BidiLevel[MAXRUN];
int iLogicalToVisual[MAXRUN];
int iVisualToLogical[MAXRUN];
int iRun;
RUN *pRun;
int iPos;
//对行析取双向级别到一个数组中
iRun = 0;
pRun = pFirstLogicalRun;
while(pRun != NULL && iRun < cRuns && iRun < MAXRUN)
{
BidiLevel[iRun] = (BYTE)pRun->analysis.s.uBidiLevel;
iRun++;
pRun = pRun->pNext;
}
//获得逻辑顺序到可视顺序的
run索引映射表
/* 1、在输出上
,iVisualToLogical[0]是显示在最左边的run的逻辑序号。
* 以后的条目应当被显示从左到右进行。
* 是否可以这样理解:这个数组的元素是以显示位置排序的即数组的下标代码代表显示位置,
* 而该下标所引用的元素值是应当在此下标位置上显示的逻辑
run的索引。
*
* 2、
iLogicalToVisual[0]参数相对于第一个逻辑run应当被显示的可视位置,
* 最左边的显示位置是
0。可以这样理解,所谓位置指的是从左到右,既然最左边的是0,
* 那么它的下一个位置即右边的一个就是
1,依此类推。
* 那么这个数组中元素的下标就是逻辑
run的索引,
* 而元素的值则是索引所对应的逻辑
run应当被显示的位置。
*/
ScriptLayout(cRuns , BidiLevel , iVisualToLogical , iLogicalToVisual);
while(iRun < cRuns)
{
//以显示位置为
ppVisualOrder的下标,值为该位置上要显示的run指针
ppVisualOrder[iLogicalToVisual[iRun]] = pRun;
piPos[iLogicalToVisual[iRun]] = iPos;
iPos += pRun->iLen;
pRun = pRun->pNext;
iRun++;
}
}
在此作些解释
:iRun循环递增,pRun同时也以pRun->pNext的方法迭代.那么可以把iRun理解
为当前
pRun的索引,即索引iRun对应的RUN指针是pRun.那么通过iLogicalToVisual[iRun]
获得的这个值是逻辑顺序上的
run应当被显示的可视位置.简单的说就是一个可视位置.
而这个逻辑顺序
iRun对应于pRun,所以通过ppVisualOrder[iLogicalToVisual[iRun]] = pRun;
我们获得了在这个可视位置上应该显示的
run指针。
我们可以把上个语句中的
pRun 换为 iRun 即:
ppVisualOrder[iLogicalToVisual[iRun]] = iRun;
那么这时就是以可视位置为下标以逻辑位置为该下标所引用的值
.相当于iVisualTological.
那么再将
iRun换回来即又换为pRun;
这时则是以可视位置为下标
,以可视位置上应当显示的RUN指针为该下标所引用的值。
到此我们获得了一个
RUN指针pVisualOrder.以可视顺序排序的run指针。以及一个iPos数组。元素是以可视顺序排序的每个RUN的相对于该组run头的字符偏移量。
4、
ScriptTextOut.
再次调用
ScriptShape与ScriptPlace
//使用想使用的字体产生字形
hr = ScriptShape(hdc,
&m_style[*piCurStyle].sc,
m_wcBuf + iPos,
iLen,
MAXGLYPHS,
psa,
pGlyphs,
pClusters,
pVisattrs,
pcGlyphs);
hr = ScriptShape(hdc ,
&m_style[*piCurStyle].sc,
m_wcBuf + iPos,
iLen,
MAXGLYPHS,
psa,
pGlyphs,
pClusters,
pVisattrs,
pcGlyphs);
//绘制
hr = ScriptTextOut(hdc,
&m_style[*piCurStyle].sc,
*piX,
iY,
0,
NULL,
psa,
NULL,
0,
glyphs,
cGlyphs,
advances,
NULL,
goffsets);