假设我将一行可变宽度字体的文本写入带有 TextOut 的窗口,并允许用户单击任何字母。那我怎么知道他点击了文字的哪一部分呢?换句话说,我如何将他点击的光标坐标转换为字符串偏移量?
我想这可以通过在各种字符串截断上调用 GetTextExtentPoint32 来完成,直到我找到正确的,但肯定有一种更有效的方法。微软的记事本程序确切地知道当我向右箭头越过一条线时要移动多少像素 - 但是如何?
这个答案是在翻阅微软的神秘文档后通过反复试验编译的。
MSDN C 库提供以下函数来显示文本:
* TextOut, which does not kern
* ExtTextOut, which can kern if its final parameter is non-null
* DrawText, which always kerns
如果需要字距调整(经过反思,我认为这是可取的),那么它是 ExtTextOut 和 DrawText 之间的选择。
DrawText 按照 Groo 的建议提供了一个解决方案。它需要在文本区域周围绘制一个框,如下所示:
void textOut(HDC hdc, int x, int y, char *s, int l)
{
RECT r = {0};
r.left = x;
r.right = Ewidth;
r.top = y;
r.bottom = y+LineHt;
DrawText(hdc,s,l,&r,DT_NOPREFIX);
}
当设置或更改字体大小时,必须构建字符宽度查找表“CharW”:
ABC CharW[256]; // char-width including leading/trailing space
GetCharABCWidths(hdc, 0, 0xff, CharW);
更改字体时,必须构建字距调整表:
KERNINGPAIR *Kern; // pairs of chars and the (usually negative) additional gap between them
int KernCnt; // number of same
KERNINGPAIR *CharK[256];// ptr to first kerning-pair for each char
if(!Kern)
free(Kern);
KernCnt = GetKerningPairs(hdc, -1, 0);
Kern = malloc(KernCnt * sizeof(*Kern));
GetKerningPairs(hdc, KernCnt, Kern);
{
int i;
for(i = 0; i < KernCnt; ++i) {
KERNINGPAIR *k = Kern+i;
if(k->wFirst < 0x100) {
KERNINGPAIR **k2 = CharK + k->wFirst;
if(!*k2)
*k2 = k;
}
}
}
为了安全起见,字距调整表“Kern”应该按 (wFirst, wSecond) 排序,但它似乎是由 wFirst 聚集的,因此我的代码在没有 qsort 的情况下工作。
因此,我们可以计算任何子串的像素宽度,如下所示:
int pixelWidth(char *s, int l)
{
int x = 0;
int i;
for(i = 0; i < l; ++i) {
char c = s[i];
ABC *w = CharW+c;
int wk = 0;
if(i > 0) {
char b = s[i-1];
KERNINGPAIR *k = CharK[b];
if(k)
for(; k < Kern+KernCnt && k->wFirst == b; ++k)
if(k->wSecond == c)
{wk = k->iKernAmount; break;}
}
x += wk + w->abcA + (w->abcB) + w->abcC;
}
return x;
}
这已经过测试,并且在设置维护当前坐标标志时与 DrawText 返回的 x 坐标一致:
SetTextAlign(hdc,TA_UPDATECP)
因此,很容易找到与给定像素宽度匹配的子字符串长度。
但是,ExtTextOut 提供了一个更简单的解决方案:
INT W[512]; // maximum string-length
void textOut(HDC hdc, int x, int y, char *s, int l)
{
GCP_RESULTS g={0};
g.lStructSize = sizeof(g);
g.lpDx = W;
g.nGlyphs = sizeof(W)/sizeof(*W);
GetCharacterPlacement(hdc, s, l, sizeof(W), &g, GCP_USEKERNING);
ExtTextOut(hdc, x, y, 0, 0, s, l, g.lpDx);
}
MSDN 函数 GetCharacterPlacement() 返回一个数组,其中包含字符串 s 中每个字符的实际像素宽度。它取代了我上面的查表 CharW、Kern、CharK。根据微软的说法,它已被 Uniscribe 功能取代,尽管它仍然适用于像英语这样的欧洲语言。