65.9K
CodeProject 正在变化。 阅读更多。
Home

如何在不安装的情况下使用字体

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (54投票s)

2016年4月12日

CPOL

7分钟阅读

viewsIcon

213032

downloadIcon

7658

如何在不先在用户系统上安装的情况下使用字体

目录

Sample Font

引言

很多时候,由于内部图形设计师的选择,某个特定字体需要在应用程序中使用。为了让应用程序可以使用这些字体,就需要使用安装程序来安装字体。用户机器上过多的字体可能会严重拖慢系统速度。

实际上,你可以在不安装字体的情况下完成这项工作:GDI 和 GDI+ 各提供了两种方法,让你作为程序员,能够让应用程序在不安装字体的情况下使用字体。我将在本文中向你展示如何做到这一点!

GDI 的 AddFontResourceEx

首先,我将介绍 GDI 中用于向应用程序添加字体的两种函数。然后,我将介绍 GDI+ 自有的函数。你可以使用 AddFontResourceEx 来添加一个物理字体文件供应用程序使用。

int AddFontResourceEx(
  LPCTSTR lpszFilename, 	// font file name
  DWORD fl,             	// font characteristics
  PVOID pdv             	// reserved
);

以下是使用 AddFontResourceEx 的示例:

CString szFontFile = "D:\\SkiCargo.ttf";

int nResults = AddFontResourceEx(
    m_szFontFile, 		// font file name
    FR_PRIVATE,    	// font characteristics
    NULL);

要使用你添加的字体,只需在 CreateFontCreateFontIndirect 函数中像指定任何其他已安装字体一样指定其名称即可。要了解字体的名称,只需在 Windows 资源管理器中右键单击 TTF 扩展名文件并选择“打开”,你就会看到其真实名称。或者,你可以使用我编写的 TTFTTC 类来获取字体名称。

注意:本文中的字体文件名(“SkiCargo.ttf”)实际上是它的字体名称“SkiCargo”;这通常不是一般情况!为了稳妥起见,请使用 Windows 资源管理器右键单击方法,或者我刚才提到的 TTFTTC 类来找出字体名称!

CClientDC dc(this);

dc.SetBkMode(TRANSPARENT);

LOGFONT lf;
memset(&lf, 0, sizeof(lf));
lf.lfHeight = -MulDiv(24, pDC->GetDeviceCaps(LOGPIXELSY), 72);
lf.lfWeight = FW_NORMAL;
lf.lfOutPrecision = OUT_TT_ONLY_PRECIS;
wcscpy_s(lf.lfFaceName, L"SkiCargo");

// create and select it
CFont newFont;
if (!newFont.CreateFontIndirect(&lf))
    return;
CFont* pOldFont = dc.SelectObject(&newFont);

// use a path to record how the text was drawn
wchar_t buf[] = _T("The quick brown fox jumps over the lazy dog!");
dc.TextOut( 10, 10, buf, wcslen(buf));

// Put back the old font
dc.SelectObject(pOldFont);

你必须记住在应用程序退出前调用 RemoveFontResourceEx。你应该注意到,传递给 RemoveFontResourceEx 的参数必须与你传递给 AddFontResourceEx 的参数相同!

BOOL RemoveFontResourceEx(
  LPCTSTR lpFileName,  	// name of font file
  DWORD fl,            	// font characteristics
  PVOID pdv            	// Reserved.
);

CString szFontFile = "D:\\SkiCargo.ttf";

BOOL b = RemoveFontResourceEx(
    m_szFontFile, 		// name of font file
    FR_PRIVATE,   		// font characteristics
    NULL         		// Reserved.
    );

GDI 的 AddFontMemResourceEx

如果我们的字体位于资源 DLL、CAB 文件或存档压缩文件中,你可以将其提取到内存中,然后使用 AddFontMemResourceEx 从内存中读取它。

HANDLE AddFontMemResourceEx(
  PVOID pbFont,       	// font resource
  DWORD cbFont,       	// number of bytes in font resource 
  PVOID pdv,          	// Reserved. Must be 0.
  DWORD *pcFonts      	// number of fonts installed
);

以下是在嵌入到资源中的字体文件上使用 AddFontMemResourceEx 的示例。注意:要了解如何将字体文件添加到资源,请参阅本文后面的 本节

HINSTANCE hResInstance = AfxGetResourceHandle( );

HRSRC res = FindResource(hResInstance,
    MAKEINTRESOURCE(IDR_MYFONT),L"BINARY");
if (res) 
{
    HGLOBAL mem = LoadResource(hResInstance, res);
    void *data = LockResource(mem);
    size_t len = SizeofResource(hResInstance, res);

    DWORD nFonts;
    m_fonthandle = AddFontMemResourceEx(
        data,       	// font resource
        len,       	// number of bytes in font resource 
        NULL,          	// Reserved. Must be 0.
        &nFonts      	// number of fonts installed
        );

    if(m_fonthandle==0)
    {
        MessageBox(L"Font add fails", L"Error");
    }
}

要使用你添加的字体,请参考前面的 AddFontResourceEx 示例。它们是相同的。只需将其作为任何已安装字体一样使用即可。在应用程序退出前,你应该调用 RemoveFontMemResourceEx。当进程结束时,即使你不调用 RemoveFontMemResourceEx,系统也会卸载这些字体。注意:传递给 RemoveFontMemResourceEx 的参数必须与你传递给 AddFontResourceEx 的参数相同!

BOOL RemoveFontMemResourceEx(
  HANDLE fh   // handle to the font resource
);

if(m_fonthandle)
{
    BOOL b = RemoveFontMemResourceEx(m_fonthandle);
    if(b==0)
    {
        MessageBox(L"Font remove fails", L"Error");
    }
}

GDI+ PrivateFontCollection 的 AddFontFile

对于 GDI+,你可以使用其 PrivateFontCollection 类成员 AddFontFile 来添加一个物理字体文件。

Status AddFontFile(const WCHAR* filename);

以下是使用 AddFontFile 添加字体文件的方法:

Gdiplus::PrivateFontCollection m_fontcollection;
//...
CString szFontFile = szExePath + L"SkiCargo.ttf";

Gdiplus::Status nResults = m_fontcollection.AddFontFile(szFontFile);

以下是如何使用我们刚刚添加到 PrivateFontCollection 对象 m_fontcollection 的字体:

// When painting the text
FontFamily fontFamily;
int nNumFound=0;
m_fontcollection.GetFamilies(1,&fontFamily,&nNumFound);

if(nNumFound>0)
{
    Font font(&fontFamily,28,FontStyleRegular,UnitPixel);

    StringFormat strformat;
    wchar_t buf[] = L"The quick brown fox jumps over the lazy dog!";
    graphics.DrawString(buf,wcslen(buf),&font, 
             PointF(10.0f,10.0f),&strformat,&brush);
}

注意:与 GDI 的 AddFontResourceExAddFontMemResourceEx 不同,AddFontFile 没有对应的 RemoveFontFile。所有添加的字体都将由 PrivateFontCollection 的析构函数移除。

GDI+ PrivateFontCollection 的 AddMemoryFont

对于 GDI+,你可以使用其 PrivateFontCollection 类成员 AddMemoryFont 来添加内存中的字体。

Status AddMemoryFont(const VOID *memory, INT length);

以下是在嵌入到资源中的字体文件上使用 AddMemoryFont 的方法。与 AddFontFile 类似,没有 RemoveMemoryFont 需要调用。一切都将由 PrivateFontCollection 的析构函数处理。注意:要了解如何将字体文件添加到资源,请参阅本文后面的 本节

HINSTANCE hResInstance = AfxGetResourceHandle( );

HRSRC res = FindResource(hResInstance,
    MAKEINTRESOURCE(IDR_MYFONT),L"BINARY");
if (res) 
{
    HGLOBAL mem = LoadResource(hResInstance, res);
    void *data = LockResource(mem);
    size_t len = SizeofResource(hResInstance, res);

    Gdiplus::Status nResults = m_fontcollection.AddMemoryFont(data,len);

    if(nResults!=Gdiplus::Ok)
    {
        MessageBox(L"Font add fails", L"Error");
    }
}

至于如何使用你刚刚添加到 PrivateFontCollection 对象 m_fontcollection 的字体,请参考前面的 AddFontFile 示例,它们是相同的。

获取 TTF 和 TTC 字体名称

我编写了两个类,分别是 TTFTTC,用于分别读取 TTF/OTF 和 TTC 字体文件中的字体名称。为了支持 Matroska (mkv) 文件字体读取或嵌入式字体资源读取,我的 TTFTTC 类支持解析内存中的字体文件。供你参考,这些 Matroska 文件通常包含视频通道、多语言音频通道、字幕以及视频中使用的字幕字体。我的类非常易于使用。下面是一个读取 TTF 文件(物理或内存中)并显示其信息的示例。

void TestReadTtfFromFile(const std::wstring& szFile)
{
    TTF ttf;
    ttf.Parse(szFile);
    Display(ttf);
}

void TestReadTtfFromMemory(const std::wstring& szFile)
{
    struct _stat bufferStat;
    int nRet = _wstat(szFile.c_str(), &bufferStat);
    FILE* pFile = _wfopen(szFile.c_str(), L"rb");
    if(pFile == NULL)
    {
        std::wcout<<L"Failed to create file"<<std::endl;
        return;
    }
    BYTE* buf = new BYTE[bufferStat.st_size];
    fread(buf,bufferStat.st_size,1,pFile);
    fclose(pFile);
    TTF ttf;
    ttf.Parse(buf, bufferStat.st_size);

    delete [] buf;

    Display(ttf);
}

void Display(TTF& ttf)
{
    std::wcout<<L"FontName : "<<ttf.GetFontName()<<std::endl;
    std::wcout<<L"Copyright : "<<ttf.GetCopyright()<<std::endl;
    std::wcout<<L"FontFamilyName : "<<ttf.GetFontFamilyName()<<std::endl;
    std::wcout<<L"FontSubFamilyName : "<<ttf.GetFontSubFamilyName()<<std::endl;
    std::wcout<<L"FontID : "<<ttf.GetFontID()<<std::endl;
    std::wcout<<L"Version : "<<ttf.GetVersion()<<std::endl;
    std::wcout<<L"PostScriptName : "<<ttf.GetPostScriptName()<<std::endl;
    std::wcout<<L"Trademark : "<<ttf.GetTrademark()<<std::endl;

    std::wstring szBold = ttf.IsBold() ? L"true" : L"false"; 
    std::wstring szItalic = ttf.IsItalic() ? L"true" : L"false"; 
    std::wstring szRegular = ttf.IsRegular() ? L"true" : L"false"; 

    std::wcout<<L"Bold : "<<szBold<<std::endl;
    std::wcout<<L"Italic : "<<szItalic<<std::endl;
    std::wcout<<L"Regular : "<<szRegular<<std::endl;

    std::wcout<<std::endl;
}

TTC 是一种字体文件,其中包含一组 TTF 字体。下面是一个读取 TTC 文件(物理或内存中)并显示其信息的示例。

void TestReadTtcFromFile(const std::wstring& szFile)
{
    TTC ttc;
    ttc.Parse(szFile);
    Display(ttc);
}

void TestReadTtcFromMemory(const std::wstring& szFile)
{
    struct _stat bufferStat;
    int nRet = _wstat(szFile.c_str(), &bufferStat);
    FILE* pFile = _wfopen(szFile.c_str(), L"rb");
    if(pFile == NULL)
    {
        std::wcout<<L"Failed to create file"<<std::endl;
        return;
    }
    BYTE* buf = new BYTE[bufferStat.st_size];
    fread(buf,bufferStat.st_size,1,pFile);
    fclose(pFile);
    TTC ttc;
    ttc.Parse(buf, bufferStat.st_size);

    delete [] buf;

    Display(ttc);
}

void Display(TTC& ttc)
{
    for(size_t i=0; i<ttc.Size(); ++i )
    {
        std::wcout<<L"FontName : "<<ttc.GetFontName(i)<<std::endl;
        std::wcout<<L"Copyright : "<<ttc.GetCopyright(i)<<std::endl;
        std::wcout<<L"FontFamilyName : "<<ttc.GetFontFamilyName(i)<<std::endl;
        std::wcout<<L"FontSubFamilyName : "<<ttc.GetFontSubFamilyName(i)<<std::endl;
        std::wcout<<L"FontID : "<<ttc.GetFontID(i)<<std::endl;
        std::wcout<<L"Version : "<<ttc.GetVersion(i)<<std::endl;
        std::wcout<<L"PostScriptName : "<<ttc.GetPostScriptName(i)<<std::endl;
        std::wcout<<L"Trademark : "<<ttc.GetTrademark(i)<<std::endl;

        std::wstring szBold = ttc.IsBold(i) ? L"true" : L"false"; 
        std::wstring szItalic = ttc.IsItalic(i) ? L"true" : L"false"; 
        std::wstring szRegular = ttc.IsRegular(i) ? L"true" : L"false"; 

        std::wcout<<L"Bold : "<<szBold<<std::endl;
        std::wcout<<L"Italic : "<<szItalic<<std::endl;
        std::wcout<<L"Regular : "<<szRegular<<std::endl;

        std::wcout<<std::endl;
    }
}

注意:你应该始终调用 GetFontFamilyName 方法来获取字体族名称,而不是 GetFontName 方法。大多数字体都属于一个字体族。例如,在 Arial 字体族下,有几种 Arial 字体,它们的字体名称是“Arial Bold”、“Arial Bold Italic”等。下面是关于如何将 TTFGetFontFamilyName 方法与 AddFontResourceEx 函数一起使用的示例。

TTF m_Ttf;

//... During Initialization
CString szFontFile = "D:\\SkiCargo.ttf";

int nResults = AddFontResourceEx(
    m_szFontFile, 		// font file name
    FR_PRIVATE,           	// font characteristics
    NULL);

m_Ttf.Parse((LPCWSTR)(m_szFontFile));    
    
//... In the OnPaint method    
    
CClientDC dc(this);

dc.SetBkMode(TRANSPARENT);

LOGFONT lf;
memset(&lf, 0, sizeof(lf));
lf.lfHeight = -MulDiv(24, pDC->GetDeviceCaps(LOGPIXELSY), 72);
lf.lfWeight = FW_NORMAL;
lf.lfOutPrecision = OUT_TT_ONLY_PRECIS;
//wcscpy_s(lf.lfFaceName, L"SkiCargo");
wcscpy_s(lf.lfFaceName, m_Ttf.GetFontFamilyName().c_str());

// create and select it
CFont newFont;
if (!newFont.CreateFontIndirect(&lf))
    return;
CFont* pOldFont = dc.SelectObject(&newFont);

// use a path to record how the text was drawn
wchar_t buf[] = _T("The quick brown fox jumps over the lazy dog!");
dc.TextOut( 10, 10, buf, wcslen(buf));

// Put back the old font
dc.SelectObject(pOldFont);

注意:我在网上找不到足够的信息来解析 fon 文件,这是一种扩展名为“fon”的字体文件。我尝试了逆向工程来获取文件名,但失败了。不过,我会继续尝试。

将字体文件添加到资源

要将字体文件添加到资源节,请遵循我的分步示例。请注意,我的方法是直接编辑资源文件,而不是通过 IDE 的资源编辑器添加,因为根据我的经验,资源编辑器倾向于弄乱资源的 rc 文件,导致 WYSIWYG 对话框编辑器无法使用。注意:最新的资源编辑器现在可能更健壮和稳定。要添加字体文件,我们必须分配一个资源 ID 来引用该字体。为此,请关闭你关心的解决方案或项目(如果它们已打开)。要分配资源 ID,请打开 Resource.h

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by TestGDI_AddFontMem.RC
//
#define IDR_MAINFRAME			128
#define IDD_TESTGDI_ADDFONTMEM_DIALOG	102

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS

#define _APS_NEXT_RESOURCE_VALUE		129
#define _APS_NEXT_CONTROL_VALUE		1000
#define _APS_NEXT_SYMED_VALUE		101
#define _APS_NEXT_COMMAND_VALUE		32771
#endif
#endif

我相信你的资源 ID 比我这个简单的项目多得多。让我们将定义的 ID 命名为“IDR_MYFONT”。当然,你可以根据自己的需要进行命名。我们将 IDR_MYFONT 赋值为 _APS_NEXT_RESOURCE_VALUE 的当前值,即 129。然后我们将 _APS_NEXT_RESOURCE_VALUE 加一;这一点很重要,否则下一个资源将与你的字体共享相同的数值 ID。下面是手动编辑后的 Resource.h 的样子。

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by TestGDI_AddFontMem.RC
//
#define IDR_MAINFRAME			128
#define IDD_TESTGDI_ADDFONTMEM_DIALOG	102
#define IDR_MYFONT				129

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS

#define _APS_NEXT_RESOURCE_VALUE		130
#define _APS_NEXT_CONTROL_VALUE		1000
#define _APS_NEXT_SYMED_VALUE		101
#define _APS_NEXT_COMMAND_VALUE		32771
#endif
#endif

接下来,我们将编辑 rc 扩展文件。在您喜欢的文本编辑器中打开该文件。注意:您的 Visual Studio 不应打开包含此 rc 文件的项目。搜索下面列出的部分。

/////////////////////////////////////////////////////////////////////////////
//
// BINARY
//

如果找不到此部分,可以自行添加。然后添加字体 ID 及其字体文件。你的二进制节可能看起来像这样。

/////////////////////////////////////////////////////////////////////////////
//
// BINARY
//
IDR_MYFONT              BINARY                  "res\\SkiCargo.ttf"

正如 RC 代码所示,IDR_MYFONT 是一个二进制资源,它引用项目文件夹的“res”子文件夹中的 SkiCargo.ttf 文件。

如果你觉得将字体添加到资源很麻烦,可以重命名字体文件名及其扩展名,这样就没有人会知道该文件是字体并对其进行更改。你甚至可以加密或压缩它。在内存中读取文件之前,只需将其解密或解压缩即可。

结论

你已经看到了 GDI 和 GDI+ 各提供的两种方法,可以将字体文件从物理文件或内存中加载并使用。我希望这可以消除程序员在用户机器上安装字体才能使用字体的需求。我介绍了两个类来读取 TTF 和 TTC 字体文件以获取它们的字体名称。如果你对本文有任何喜欢或不喜欢的地方,请告诉我,以便我改进本文。我希望你喜欢阅读我的文章!

参考

历史

  • 2009 年 9 月 14 日 - 添加了关于如何将字体添加到资源的部分
  • 2009 年 8 月 31 日 - 首次在 CodeProject 发布
© . All rights reserved.