Greatest

All about 非主流脑残技术

导航

Win32编程点滴2

前段时间我一直在研究一个问题:在一个DC中选择一个英文字体,为什么还是能够正确的绘制出中文?然后,我就找到了这篇文章,觉得还蛮好看的,所以翻译了一下。虽然这篇2004年写的文章,即使放到当年来看技术上也是很老了。从windows2000开始,类似TextOut等gdi函数的实现就使用了比本文提及的mlang库更加高级的uniscribe库。现在的TextOut,无论font linking,还是什么从右到左显示的阿拉伯文字,还有各式各样的unicode显示问题,都能应付。所以,这篇文章看点并不在于技术有多高级,而是涉及的技术的有趣性,还有作为windows作者,写出的函数的严谨性。好了,不多说了,正文开始。(可能文章比较老,根据我查msdn,原文好像有几个错误,不过我都保留了原来的意思,没有修正。)

How to display a string without those ugly boxes


当你尝试绘制一个字符串,而当前字体不包含它的所有字符时,你会看见一些难看的方块。那些字体中不包含的字符将显示为一个难看的方块。
我们从这个示例程序开始,把下面代码加入到PaintContent函数中:

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
    TextOutW(pps->hdc, 0, 0,
            L"ABC\x0410\x0411\x0412\x0E01\x0E02\x0E03", 9);

}

该字符串包含了三个不同的字母表里的前三个字母:罗马字母“ABC”,西里尔字母“АБВ”和泰国字母“กขฃ”。

如果你运行这个程序,对那些非罗马字母你会看到一堆难看的方块,因为SYSTEM字体支持的字符集非常有限。

但如何选择合适的字体?如果该字符串包含韩语或日语的字符呢?没有一个单独的字体,包含了Unicode定义的全部字符。(至少,常见的字体中没有。)你会怎么做?

这就引入了font linking技术。

Font linking技术允许你将一个字符串分成几块,每一块用一个合适的字体来绘制。

IMLangFontLink2接口对这样的字符串分块提供了必要的方法。GetStrCodePages将字符串分割成块,每一块中的每个字符能用同一种字体来绘制,而MapFont创建这个字体。

好了,让我们来写带有font linking技术的TextOut函数。我们将一步一步完成这个函数,首先看核心的部分。

#include <mlang.h>

HRESULT TextOutFL(HDC hdc, int x, int y, LPCWSTR psz, int cch)
{
  ...
  while (cch > 0) {
    DWORD dwActualCodePages;
    long cchActual;
    pfl->GetStrCodePages(psz, cch, 0, &dwActualCodePages, &cchActual);
    HFONT hfLinked;
    pfl->MapFont(hdc, dwActualCodePages, 0, &hfLinked);
    HFONT hfOrig = SelectFont(hdc, hfLinked);
    TextOut(hdc, ?, ?, psz, cchActual);
    SelectFont(hdc, hfOrig);
    pfl->ReleaseFont(hfLinked);
    psz += cchActual;
    cch -= cchActual;
  }
  ...
}

在计算出当前默认字体所支持的代码页之后,我们通过GetStrCodePages以块为单位来遍历字符串。对每一块,我们创建一个字体,在“正确的地方”绘制出这块的字符。重复如此,直至字符串结束。

剩下的就是完善和文书工作。

首先,什么是“正确的地方”?我们希望下一块能接着上一块的地方显示。为此,我们利用TA_UPDATECP文本对齐模式,此模式要求GDI在当前位置显示文本,并将当前位置更新为文本结尾处(也就是下一块的位置)。

因此,文书工作的一部分是设置DC的当前位置并设置文本模式为TA_UPDATECP:

  SetTextAlign(hdc, GetTextAlign(hdc) | TA_UPDATECP);
MoveToEx(hdc, x, y, NULL);

然后,我们将坐标“0,0”传给TextOut,因为如果文本对齐模式是TA_UPDATECP的话,TextOut忽略坐标参数,而在当前位置绘制文本。

当然,我们不能这样乱改DC的设置。如果调用者并没有设置TA_UPDATECP,那么调用者也不会希望我们改变当前位置。因此,我们保存下当前位置并回复它(以及原来的文本对齐模式)。

  POINT ptOrig;
DWORD dwAlignOrig = GetTextAlign(hdc);
SetTextAlign(hdc, dwAlignOrig | TA_UPDATECP);
MoveToEx(hdc, x, y, &ptOrig);
while (cch > 0) { ... TextOut(hdc, 0, 0, psz, cchActual);
...
}
// if caller did not want CP updated, then restore it
// and restore the text alignment mode too
if (!(dwAlignOrig & TA_UPDATECP)) {
SetTextAlign(hdc, dwAlignOrig);
MoveToEx(hdc, ptOrig.x, ptOrig.y, NULL);
}


下一步是完善:利用GetStrCodePages的第二个参数,它指定了优先使用的代码页。显然我们应该优先使用我们想要的那个字体,那样如果用这个字体能直接显示,那就不必用另外的字体了。

  HFONT hfOrig = (HFONT)GetCurrentObject(hdc, OBJ_FONT);
DWORD dwFontCodePages = 0;
pfl->GetFontCodePages(hdc, hfOrig, &dwFontCodePages);
... while (cch > 0) { pfl->GetStrCodePages(psz, cch, dwFontCodePages, &dwActualCodePages, &cchActual); if (dwActualCodePages & dwFontCodePages) {
// our font can handle it - draw directly using our font
TextOut(hdc, 0, 0, psz, cchActual);
} else {
... MapFont etc ... } } ...


当然,你可能想知道这个神奇的pfl哪来的。它来自于mlang的Multilanguage对象。

  IMLangFontLink2 *pfl;
CoCreateInstance(CLSID_CMultiLanguage, NULL,
CLSCTX_ALL, IID_IMLangFontLink2, (void**)&pfl);
...
pfl->Release();

当然,我们忽略了所有的错误处理。当遍历了字符串的几块后,产生了一个错误,这将是个问题。怎么办?

当产生错误的时候,我使用原来的字体显示字符串。我们不能擦除已经绘制的字符,也不能只绘制一半的字符串(调用者显然不知道如何继续)因此,我们使用原来的字体绘制,并希望它能工作的很好。毕竟,这样也不会比原来没有font linking技术跟糟糕吧。

完善后,我们得到最终的函数:

HRESULT TextOutFL(HDC hdc, int x, int y, LPCWSTR psz, int cch)
{
HRESULT hr;
IMLangFontLink2 *pfl;
if (SUCCEEDED(hr = CoCreateInstance(CLSID_CMultiLanguage, NULL,
CLSCTX_ALL, IID_IMLangFontLink2, (void**)&pfl))) {
HFONT hfOrig = (HFONT)GetCurrentObject(hdc, OBJ_FONT);
POINT ptOrig;
DWORD dwAlignOrig = GetTextAlign(hdc);
if (!(dwAlignOrig & TA_UPDATECP)) {
SetTextAlign(hdc, dwAlignOrig | TA_UPDATECP);
}
MoveToEx(hdc, x, y, &ptOrig);
DWORD dwFontCodePages = 0;
hr = pfl->GetFontCodePages(hdc, hfOrig, &dwFontCodePages);
if (SUCCEEDED(hr)) {
while (cch > 0) {
DWORD dwActualCodePages;
long cchActual;
hr = pfl->GetStrCodePages(psz, cch, dwFontCodePages, &dwActualCodePages, &cchActual);
if (FAILED(hr)) {
break;
}

if (dwActualCodePages & dwFontCodePages) {
TextOut(hdc, 0, 0, psz, cchActual);
} else {
HFONT hfLinked;
if (FAILED(hr = pfl->MapFont(hdc, dwActualCodePages, 0, &hfLinked))) {
break;
}
SelectFont(hdc, hfLinked);
TextOut(hdc, 0, 0, psz, cchActual);
SelectFont(hdc, hfOrig);
pfl->ReleaseFont(hfLinked);
}
psz += cchActual;
cch -= cchActual;
}
if (FAILED(hr)) {
// We started outputting characters so we have to finish.
// Do the rest without font linking since we have no choice.
TextOut(hdc, 0, 0, psz, cch);
hr = S_FALSE;
}
}

pfl->Release();

if (!(dwAlignOrig & TA_UPDATECP)) {
SetTextAlign(hdc, dwAlignOrig);
MoveToEx(hdc, ptOrig.x, ptOrig.y, NULL);
}
}

return hr;
}


最后,我们将整个操作放进一个辅助函数:首先尝试font linking技术,如果失败,则用原来的方式绘制文本。

void TextOutTryFL(HDC hdc, int x, int y, LPCWSTR psz, int cch)
{
if (FAILED(TextOutFL(hdc, x, y, psz, cch)) {
TextOut(hdc, x, y, psz, cch);
}
}

好了,我们完成了可靠的,带有font linking技术的TextOut了,我们回到原来的PaintContent函数来使用它。

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
TextOutTryFL(pps->hdc, 0, 0,
TEXT("ABC\x0410\x0411\x0412\x0E01\x0E02\x0E03"), 9);
}


可以看到,现在字符串不再显示出那些黑色的方块了吧。
一个我没有优化的地方是我每次绘制文本时都创建了一个IMlangFontLink2指针。在一个“真正的程序”中,你可能在整个绘制上下文范围中创建一个multilanguage对象,重用它,避免每次绘制字符串都创建一次。

posted on 2009-08-31 16:50  Greatest  阅读(2895)  评论(3编辑  收藏  举报