使用EnumFontFamiliesEx函数枚举时出现太多字体

28

我想创建一个字体列表,让用户可以选择。我是通过使用EnumFontFamiliesEx函数来实现的,但不幸的是返回的字体列表太长了。这其中有很多看起来都是无用的、重复的或者是用于其他语言的字体,或是其他不需要展示给用户的字体。我的屏幕截图最好地说明了我要过滤掉的垃圾内容。

我的调用EnumFontFamiliesEx的代码如下:

LOGFONT lf;
memset(&lf, 0, sizeof(lf));
lf.lfCharSet = DEFAULT_CHARSET;
// screenDC is result of CreateCompatibleDC(NULL)
EnumFontFamiliesEx(screenDC, &lf, GetFontsCallback, NULL, 0);

经过按字母顺序排序和删除重复字体名称后,结果列表如下所示:

enter image description here

可以看到,ChooseFont 字体公共对话框显示了一个非常合理的、用户友好并且有意义的字体列表。另一方面,我的代码显示了一个很长的额外字体列表:以 "@" 开头的字体(为什么?它们究竟是什么用途?),Arial 字体的三个额外变体,以及其他几种未知用途的字体,例如 Aheroni、Andalus、Angsana New、AngsanaUPC 等等。这太疯狂了。

如何过滤 EnumFontFamiliesEx 返回的字体列表,使其完全匹配 ChooseFont 对话框中显示的列表?


3
另一种看待这个问题的方式是,西方用户不太关心东亚字体。列表中有295种字体。我在记事本“选择字体”列表中数了148种字体。不知何时,“选择字体”对话框变得聪明起来并移除了一些字体。出于一致性考虑,我希望我的应用程序与该对话框相匹配。当然,如果在东亚使用Windows 7,则可以期望侧重于东亚字体,并且我希望我的应用程序也这样做。如果你认为这是“相当严重的文化偏见” - 微软在Windows 7 WordPad和Paint、MS Word 2010、ChooseFont 对话框等方面也是这样做的。 - James Johnston
1
@ildjarn:Script下拉菜单可能对应于ENUMLOGFONTEX结构的elfScript成员(http://msdn.microsoft.com/en-us/library/dd162627(VS.85).aspx)-我已经检查过了,大多数有问题的字体都有“西方”脚本。(即使需要对此进行某些过滤,也不清楚我应该从哪里获取要比较的字符串,因为我想知道用户当前的文化)。 - James Johnston
5
经过一番搜索,我找到了这个网站(http://vb.mvps.org/samples/FontFilter/),它谈到了这个问题,虽然使用的是VB语言,但你可能会从中获得一些帮助。 - Jesse Good
1
另一种看待这个问题的方式是,西方用户并不在意东亚字体。但事实并非如此。许多海报都是西方人,他们寻求帮助是因为他们确实关心正确显示他们客户的语言。 - Windows programmer
1
“像所有前面带有@符号的字体一样。支持竖排文字的字体。”-- 确实如此,但这些字体比没有@符号的字体使用频率要低。除非你在制作大众市场杂志(即非技术期刊)、报纸、广告等,否则你会水平书写。把那些@字体放在列表的末尾会更友好。 - Windows programmer
显示剩余7条评论
4个回答

29

感谢Jesse Good的分享,我现在了解到Windows 7团队做出了一些糟糕的设计决策。我暂时不接受自己的答案,因为如果其他人想到了一种在Windows 7中使用这个隐藏字体功能的方法,即使注册表键不存在(例如通过使用未记录的API或其他诡计),并且他们的答案有效,我将接受它。

这种过滤是通过在Windows 7控制面板中实际“隐藏”字体来完成的。默认情况下,隐藏了其他语言环境的字体,但用户可以将它们显示出来。至少,这是这个想法。以下是讨论此功能的MSDN页面:国际字体管理

以下是来自此页面和MSDN附近页面的一些关键摘录(也请参见Windows 7兼容性手册中的http://msdn.microsoft.com/en-us/library/windows/desktop/dd371704(v=vs.85).aspx):

从Windows 7开始,字体管理基础设施支持隐藏不适合用户字体选择列表的字体。......这个功能意味着用户不再需要面对不合适的字体列表。

在Windows 7中,没有直接查询隐藏字体或设置字体隐藏的API [我的强调]。如果您今天使用Windows ChooseFont API(字体常规对话框)启用字体选择,则会免费获得新行为。 Windows Scenic Ribbon(字体控件)在Windows 7中引入,也支持此行为,并提供了另一个理由“Ribbonize”您的应用程序。

当字体被选入设备上下文中时,字体隐藏不会影响绘图。 EnumFontFamiliesEx函数继续枚举设置为隐藏的字体。 [突出显示是我的; 显然无法使用EnumFontFamiliesEx区分隐藏和可见的字体]

请注意,字符集是与Unicode字符集之前相对应的传统概念。 [突出显示是我的]

ChooseFont仅列出显示的字体并过滤掉隐藏的字体,同时在列表框中显示字体。 CHOOSEFONT结构的标志成员中的附加标志(CF_INACTIVEFONTS)被添加以允许您在字体列表中显示所有已安装的字体,与Windows 7之前的ChooseFont行为相同。

换句话说,除非您使用ChooseFont公共对话框或官方的Windows功能区控件(仅适用于Windows Vista/7),否则您根本没有支持的方式可以过滤隐藏的字体。任何想隐藏Windows 7控制面板中的字体的用户都感到惊讶或疑惑吗?(我之前错误地发布了MS Word 2010过滤隐藏字体的消息。看起来它并没有,因为他们使用自己的自定义功能区控件而不是Windows内置的功能区。有趣的是,Windows 7字体控制面板不兼容Microsoft的旗舰产品之一,如果不删除Office中更强大的功能区,则无法使其兼容。)

根据Jesse Good发布的链接,我了解到隐藏的字体存储在一个不公开的注册表键中。通过这个链接,以及使用Process Monitor进行的一些实验和分析(查看栈跟踪和注册表访问),我了解到以下内容:

  • 功能区控件调用FMS.DLL(字体管理服务)中的一个未公开函数FmsGetFilteredFontList。它的目的显而易见。真遗憾他们没能公开记录和维护它。
  • 设置存储在一个不公开的注册表键中,FMS.DLL可以访问该键。
  • 如果注册表键被删除,则由FmsGetFilteredFontList重新创建带有默认设置的注册表键,这些设置是隐藏与当前输入语言无关的字体。
  • 在干净的Windows安装中创建的全新用户配置文件不包含任何与隐藏字体有关的注册表键。

因此,Jesse Good发布的链接可能适用于许多/大多数情况,但不是100%可靠。如果这些注册表键不存在,您需要一种可靠地重新创建这些键(或至少假定默认值)的方法。即使注册表键消失(例如在新用户配置文件上),默认行为仍然是隐藏某些字体。


5
赞成这项研究。这真是一个荒谬的混乱。微软最近在实施50%的解决方案方面变得非常擅长。 - Carey Gregory

5
考虑到FmsGetFilteredFontList没有文档说明,你获取与Windows 7+ ChooseFont对话框中用户看到的完全相同的字体列表的选项可能会受限制。然而,通过仅使用已记录的API可能会得到默认字体列表的很好近似。我曾经采取类似的方法,以减少算法自动选择适当字体时的可能性数量。我的方法是使用FONTSIGNATURE中的Unicode子范围位掩码,在枚举字体时可以检查它。如果您知道需要哪些Unicode子范围,字体签名将告诉您当前字体是否覆盖它。如果是,请将其包含在列表中。如果不是,则跳过它。我怀疑这可能与FmsGetFilteredFontList构建其默认列表的方式类似。关键是要弄清楚用户需要哪个子范围。在我的情况下,这相对容易,因为我知道我将要渲染的确切文本。我基于文档构建了子范围到FONTSIGNATURE样式位掩码值的映射。我扫描要渲染的文本中的代码点,在映射中查找它们,并构建一个目标位掩码。对于每个枚举字体,我将此目标位掩码与字体签名中的位掩码进行按位与操作。每当结果与目标位掩码匹配时,我就知道该字体可以(很可能)支持该文本。对于我的应用程序,我需要在字体中包含所有目标位。对于您的应用程序,我认为您想要任何目标位。字体签名位掩码是提供字体字符的一个很好的第一步。您可以使用GetFontUnicodeRanges来确保,但我发现这不是必要的,而且比仅检查字体签名慢。在您的情况下,也许用户的语言中有一些代表性的文本字符串。例如,从他们正在编辑的文档中或从已翻译的UI资源中。您可以扫描该示例文本以获取目标字体签名。例如,如果您扫描一些英文文本,您会发现所有必要的字符都在拉丁子范围内。如果您在Windows 7的字体控制面板小程序中查看英语用户(并切换到详细信息视图),您将看到“显示/隐藏”列与拉丁语是否在“设计用于”列中列出密切相关,后者似乎是字体签名的Unicode子范围位掩码的文本表示。 更新: 我刚刚尝试使用DirectWrite枚举字体,以为这个较新的API可以处理字体隐藏功能。可惜,它返回了所有字体,并没有过滤隐藏字体的选项(至少我找不到)。

2

说实话,微软没有记录这个功能真是令人不齿,但越来越多的情况表明我们已经对他们有了这样的期望。

筛选自己字体列表的另一种方法是利用 shell 枚举字体文件夹。如果您使用资源管理器查看,您会发现隐藏的字体显示为虚化图标 - 我们可以使用该属性来判断字体是否被隐藏。

例如(不完整):

LPITEMIDLIST pidlFonts;
if (SUCCEEDED(SHGetKnownFolderIDList(FOLDERID_Fonts, 0, nullptr, &pidlFonts)))
{
    CComPtr<IShellFolder> psf;
    if (SUCCEEDED(SHBindToObject(nullptr, pidlFonts, nullptr, IID_IShellFolder, reinterpret_cast<void**>(&psf))))
    {
        CComPtr<IEnumIDList> pEnum;
        if (SUCCEEDED(psf->EnumObjects(hWnd, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN | SHCONTF_INIT_ON_FIRST_NEXT, &pEnum)))
        {
            LPITEMIDLIST pidl;
            ULONG celt = 0;
            while (pEnum->Next(1, &pidl, &celt) == S_OK)
            {
                SFGAOF hidden = SFGAO_GHOSTED;
                if (SUCCEEDED(psf->GetAttributesOf(1, const_cast<LPCITEMIDLIST*>(&pidl), &hidden)) && (hidden & SFGAO_GHOSTED) == SFGAO_GHOSTED)
                {
                    // this font should be hidden
                    // get its name via IShellFolder::GetDisplayNameOf
                }
                CoTaskMemFree(pidl);
            }
        }
    }
    CoTaskMemFree(pidlFonts);
}

你可以使用这种方法来构建一组隐藏字体,然后使用它来过滤EnumFontFamiliesEx的结果。

3
什么意思?由于API没有文档,因此没有“正确”的操作方式。请问您需要什么进一步的信息吗? - Jonathan Potter

-2

我认为这里的整个讨论都是误导性的。

当我向用户提供字体选择器时,我为什么要关心Microsoft隐藏了哪些字体?为什么我要隐藏Microsoft认为应该默认隐藏的所有字体?

如果我的用户想使用Microsoft隐藏的其中一种字体怎么办?我会让用户去控制面板取消隐藏这种字体吗?

如果有一天,一个中国用户想在英文Windows上写中文文本,而中文字体被隐藏了怎么办?

我认为有一种更好的方法来限制EnumFontFamiliesEx()返回的大量字体。

我编写了自己的字体选择器,它具有字体过滤器,允许用户选择他想要使用的字体组。这样,我不会隐藏任何东西,而是将所有权力交给用户,而不是交给Microsoft!

用户可能希望查看所有字体! 有时候,人们只需要Arial Black或Arial Narrow,尽管Microsoft认为它应该被隐藏。

int CALLBACK EnumFontFamExProc(const LOGFONT* pk_Font, 
                               const TEXTMETRIC* pk_Metric, 
                               DWORD e_FontType, 
                               LPARAM lParam)
{
    if (e_FontType & TRUETYPE_FONTTYPE)
    {   
        // u32_Flags128 = DWORD[4] = 4 * 32 bit = 128 bit
        DWORD* u32_Flags128 = ((NEWTEXTMETRICEX*)pk_Metric)->ntmFontSig.fsUsb;

        if (u32_Flags128[13 / 32] & (1 << (13 % 32)))
        {
            // the font contains arabic characters (bit 13)
        }
        if (u32_Flags128[38 / 32] & (1 << (38 % 32)))
        {
            // the font contains mathematical symbols (bit 38)
        }
        if (u32_Flags128[70 / 32] & (1 << (70 % 32)))
        {
            // the font contains tibetan characters (bit 70)
        }
    }

在回调函数中,您会得到一个128位标志,它精确地定义了字体支持的Unicode区域。
请参见http://msdn.microsoft.com/en-us/library/dd374090%28v=vs.85%29.aspx 您可以使用这128位来过滤和减少在字体列表中显示的字体数量:

enter image description here


4
你误解了重点。字体并不是“被 Microsoft 隐藏的”,而是被用户隐藏的。Microsoft 默认会隐藏一些字体,但用户有最终控制权。 - Jonathan Potter
我完全理解。我不想让用户承担负担,他们必须去控制面板启用Arial Black,而这个字体被Microsoft默认隐藏了。我希望允许用户从任何已安装的字体中选择,而不会使它变得更加复杂。 - Elmue
我添加了一些代码。现在你明白了吗?你必须去上面的MSDN链接了解这些值。 - Elmue

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接