UIComposition在Win32窗口中绘画字体图标
前面,介绍了如何使用UIComposition技术在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个名字建`格式`
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"zhCN", format.GetAddressOf()))) continue;
// 各本地化名字都注册一份(同一个`格式`),调用者写哪个名字都能命中
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,普通字重).
在创建好基础的格式后,代码会遍历该字体的所有本地化名,按小写统一转换它们,并全部按键(键)指向同一个格式对象(值)保存字典.
后续渲染文本时,调用者无论传入字体的英文,中文还是任意大小写组合,都能精准命中对应的文本格式.
这段代码为后续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);
}
这段代码从WindowsPE资源段中读取二进制数据的常见工具函数.
它是标准的Win32资源读取方法(FindResource->LoadResource->LockResource)定位并锁定指定名的RT_RCDATA资源,最终返回一个包含内存指针和数据大小的元组.
只要以形式设置应用的Res.rc文件即可在exe中嵌入一个字体文件资源了.
cpp
iconfong RCDATA "res/iconfont.ttf"
这样分发应用时就不用附加分发字体文件了.