UI Composition 在 Win32 窗口中绘制字体图标

本文使用 DraftDepot(稿仓 https://github.com/xland/DraftDepot )编辑并发布,DraftDepot 可以把一篇文章一键发布到多个平台。

在上一篇文章中,我们介绍了如何使用 UI Composition 技术在 Win32 窗口中绘制文本

生成自定义字体集合

cpp 复制代码
ComPtr<IDWriteFontSetBuilder1> builder;
if (FAILED(dwriteFactory->CreateFontSetBuilder(&builder))) {
    _ASSERT_EXPR(FALSE, L"CreateFontSetBuilder, error");
    return;
}
// Microsoft::WRL::ComPtr<IDWriteInMemoryFontFileLoader> fontLoader;
if (!fontLoader) {
    dwriteFactory->CreateInMemoryFontFileLoader(fontLoader.GetAddressOf());
    dwriteFactory->RegisterFontFileLoader(fontLoader.Get());
}
// std::vector<std::wstring> fontNames;
for (const auto& resName : fontNames) {
    auto [pData, size] = Util::getRes(resName.c_str());  //工具方法,读取exe资源内的字体文件,见后文
    if (pData == nullptr || size == 0) {
        _ASSERT_EXPR(FALSE, L"load font res, error");
        return;
    }
    ComPtr<IDWriteFontFile> fontFile;
    auto hr = fontLoader->CreateInMemoryFontFileReference(dwriteFactory.Get(), pData, size, nullptr, fontFile.GetAddressOf());
    if (FAILED(hr)) {
        _ASSERT_EXPR(FALSE, L"font res decode, error");
        continue;
    }
    builder->AddFontFile(fontFile.Get());
}
ComPtr<IDWriteFontSet> customFontSet;
if (FAILED(builder->CreateFontSet(&customFontSet))) {
    _ASSERT_EXPR(FALSE, L"CreateFontSet, error");
    return;
}
// Microsoft::WRL::ComPtr<IDWriteFontCollection1> customFontCollection;
if (FAILED(dwriteFactory->CreateFontCollectionFromFontSet(customFontSet.Get(), customFontCollection.ReleaseAndGetAddressOf()))) {
    _ASSERT_EXPR(FALSE, L"CreateFontCollectionFromFontSet, error");
    return;
}

这段代码的作用是从应用程序资源中加载自定义字体,并构建一个独立的 DirectWrite 字体集合,

代码首先创建 IDWriteFontSetBuilder1 构建器,并以懒加载单例模式注册内存字体加载器 fontLoader,确保加载器只被初始化一次且不污染系统全局状态。

随后遍历字体资源列表,通过工具函数提取 exe 中的字体资源(二进制数据,稍后介绍此函数),

将字体二进制数据包装为 DWrite 可识别的字体文件引用并添加到构建器中 fontFile,单个字体加载失败不会影响整体流程,保证了程序的健壮性。

最终,构建器生成字体集合(customFontSet)并转换为 IDWriteFontCollection1 对象。

创建自定义字体TextFormat集合

cpp 复制代码
// std::unordered_map<std::wstring, Microsoft::WRL::ComPtr<IDWriteTextFormat>> customFormats;
customFormats.clear();
if (!customFontCollection) return;
auto familyCount = customFontCollection->GetFontFamilyCount();
for (UINT32 i = 0; i < familyCount; i++) {
    ComPtr<IDWriteFontFamily> family;
    if (FAILED(customFontCollection->GetFontFamily(i, family.GetAddressOf()))) continue;
    ComPtr<IDWriteLocalizedStrings> names;
    if (FAILED(family->GetFamilyNames(names.GetAddressOf()))) continue;
    auto nameCount = names->GetCount();
    if (nameCount == 0) continue;
    // 先取第 0 个名字建 format
    UINT32 len{ 0 };
    if (FAILED(names->GetStringLength(0, &len))) continue;
    std::wstring first(len, L'\0');
    if (FAILED(names->GetString(0, first.data(), len + 1))) continue;
    ComPtr<IDWriteTextFormat> format;
    if (FAILED(dwriteFactory->CreateTextFormat(first.data(), customFontCollection.Get(),
        DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
        12.f, L"zh-CN", format.GetAddressOf()))) continue;
    // 各本地化名字都登记一份(同一个 format),调用方写哪个名字都能命中
    for (UINT32 j = 0; j < nameCount; j++) {
        if (FAILED(names->GetStringLength(j, &len))) continue;
        std::wstring name(len, L'\0');
        if (FAILED(names->GetString(j, name.data(), len + 1))) continue;
        std::transform(name.begin(), name.end(), name.begin(), ::towlower);
        customFormats[name] = format;
    }
}

这段代码首先遍历上一步生成的 customFontCollection 中的所有字体族,获取每个字体族的本地化名称列表。

由于一个字体通常包含多个名称(如英文名、中文名或不同语言下的别名),代码会提取第一个名称作为主名称。

调用 CreateTextFormat 创建一个默认的 IDWriteTextFormat 对象(12pt、常规字重)。

在创建好基础的 format 后,代码会遍历该字体的所有本地化名称,将它们统一转换为小写,并全部作为键(Key)指向同一个 format 对象(Value)存入字典。

这样的话后续渲染文本时,调用方无论传入字体的英文、中文还是任意大小写组合,都能以精准命中对应的文本格式。

这段代码为后续 DrawTextLayout 提供了开箱即用的格式模板。

使用自定义字体的TextFormat

cpp 复制代码
IDWriteTextFormat* D2D::getTextFormat(const std::wstring& fontFamily)
{
    if (fontFamily.empty() || customFormats.empty()) return baseTextFormat.Get();
    std::wstring key{ fontFamily };
    std::transform(key.begin(), key.end(), key.begin(), ::towlower);
    auto it = customFormats.find(key);
    return it == customFormats.end() ? baseTextFormat.Get() : it->second.Get();
}

auto format = d2d->getTextFormat(fontFamily);
d2d->dwriteFactory->CreateTextLayout(text.data(), (UINT32)text.length(), format, FLT_MAX, FLT_MAX, textLayout.ReleaseAndGetAddressOf());

这段代码首先对传入的字体族名称进行小写标准化处理,以确保与之前构建的 customFormats 字典键值匹配;

随后在字典中查找对应的 IDWriteTextFormat,若未找到或输入为空,则自动返回初始化阶段创建的 baseTextFormat(微软雅黑)作为兜底。

获取到安全的文本格式后,代码立即调用 CreateTextLayout 生成文本布局对象。

到这里,就和上一节介绍的使用系统字体绘制文本一样了。

附:读取exe资源中的字体文件

cpp 复制代码
std::tuple<void*, DWORD> Util::getRes(const std::wstring& name)
{
    HRSRC hRes = FindResource(NULL, name.data(), RT_RCDATA);
    if (!hRes) {
        return std::make_tuple(nullptr, 0);
    }
    HGLOBAL hData = LoadResource(NULL, hRes);
    if (!hData) {
        return std::make_tuple(nullptr, 0);
    }
    void* pData = LockResource(hData);
    DWORD size = SizeofResource(NULL, hRes);
    return std::make_tuple(pData, size);
}

这段代码从 Windows PE 资源段中读取二进制数据的通用工具函数。

它通过标准的 Win32 资源读取方法(FindResource → LoadResource → LockResource)定位并锁定指定名称的 RT_RCDATA 资源,最终返回一个包含内存指针和数据大小的元组。

你只要通过这种形式设置应用程序的Res.rc文件即可把一个字体文件以资源的形式嵌入到exe中了

cpp 复制代码
iconfong RCDATA "res/iconfont.ttf"

这样分发应用的时候就不用附带分发字体文件了。

程序最终运行结果如下: