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

简介: 本文介绍如何在Win32中通过DirectWrite动态加载嵌入式字体资源(RT_RCDATA),构建自定义字体集合与多语言文本格式映射,实现跨平台一致的文本渲染,无需分发字体文件。使用DraftDepot一键发布。

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

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

生成自定义字体集合

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集合

// 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

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资源中的字体文件

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中了

iconfong RCDATA "res/iconfont.ttf"

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

程序最终运行结果如下:

目录
相关文章
|
7天前
|
云安全 人工智能 运维
当攻击全面Agent化,你的安全防御能否跟上AI Speed?
多起客户侧AK泄露事件应急的真实观察
1309 6
|
5天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
|
10天前
|
存储 人工智能 安全
千问办公官网入口网页版登录:qwenwork.cn 注册送2000积分、登录送100积分
阿里云千问办公(QwenWork)是AI智能工作平台,支持一句话生成PPT、表格、网页、视频及数据分析。个人免费版送2000积分+每日登录赠100分,含1GB存储、5个发布页;企业版198元/席/月起。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
|
1月前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
9963 33
|
5天前
|
人工智能 开发者
Qoder CN 大使计划正式开启|无限创造,等你一起
全年免费积分、优先内测、活动支持与更多共创机会,邀请用 Qoder 把想法变成现实的你。
523 6
|
16天前
|
存储 人工智能 并行计算
大模型本地部署终端选型方法论:以 Qwen3.8-27B 为例的四档分层完整流程
本文提出一套大模型本地部署终端选型方法论:定约束、定档位、定框架、定参数四步决策法,配合入门、主力、质量、无损四档分层模型。以 Qwen3.8-27B 实测数据为例,逐环节解读显存、带宽、存储、散热、系统、预算等要素,给出面向不同预算的优选方案、决策自查清单与市场观察框架。文末前瞻 AI 笔记本的 CPU+GPU 与统一内存两条路线,论证四步决策法在新品类上的延续性。
|
7天前
|
Web App开发 人工智能 JavaScript
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA
REA(Reverse Engineer Anything)是近期爆火的开源AI逆向工具,4月发布,1个月内斩获15万+ Star。它让AI Agent无需源码即可分析二进制、APK、JS/Electron、HAR包等17类目标,自动反编译、生成可运行代码,并强制返回带证据与局限的结论。全本地执行,MIT协议,支持Hopper/Ghidra/IDA等引擎,一行命令即可上手。
747 2
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA
|
15天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型深度拆解:原理解读、实战测评与保姆级落地教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
1069 1