5

I have a function below which installs a font (.ttf) into Windows by copying it into the Windows font folder and then triggering the WM_FONTCHANGE message. However, that font does not immediately become visible across Windows Explorer.

After running this, when I open Fonts through the Control Panel, my font does not show there. And when I open C:\Windows\Fonts\ it does not show there either.

However I can confirm that my .ttf file is really there. Navigating here with the Command Prompt, I can see my font file. When I open the Character Map utility, my font is listed here. And the font is usable in my application. I have to restart explorer.exe to get it to show within the Windows Explorer views. I've even tried running my app as administrator (elevated), and still no luck.

I thought the WM_FONTCHANGE message was supposed to take care of this but apparently this is not doing the trick.

What am I missing in this Font Installation to make sure Windows is aware of it?

uses
  SysUtils, ShlObj, ComObj, ActiveX;

function SystemDir(Handle: THandle; Folder: Integer): String;
var
  R: HRESULT;
  PIDL: PItemIDList;
  Path: array[0..MAX_PATH] of Char;
begin
  Result:= '';
  R:= SHGetSpecialFolderLocation(Handle, Folder, PIDL);
  if R = S_OK then begin
    if SHGetPathFromIDList(PIDL, Path) then
      Result:= StrPas(Path);
  end;
end;

function InstallFont(Handle: THandle; const Filename: String): Boolean;
var
  Dir, FN: String;
begin
  Result:= False;
  FN:= ExtractFileName(Filename);
  Dir:= IncludeTrailingPathDelimiter(SystemDir(Handle, CSIDL_FONTS));
  Result:= FileExists(Filename);
  if Result then begin
    Result:= CopyFile(PChar(Filename), PChar(Dir + FN), False);
  end;
  SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
end;

Usage:

Result:= InstallFont(Application.Handle, 'C:\MyTestFont.ttf');

UPDATE

It was suggested in the comments of an answer below to install the font via the shell instead of Windows API. So, I wrote this function to essentially accomplish the same:

function InstallFont2(Handle: THandle; const Filename: String): Boolean;
var
  R: HINST;
begin
  Result:= False;
  R:= ShellExecuteW(Handle, 'install', PWideChar(Filename), nil, nil, SW_HIDE);
  Result:= R > 32;
end;

However this too is problematic. The return value is 31 (indicating an error) and when I call GetLastError it tells me 1155 ("No application is associated with the specified file for this operation.")

I also tried the particular resolution in the answer below, but to no avail. I both used AddFontResource and written the appropriate registry key - while trying combinations of uninstalling/restarting/retrying with this font installation.

2
  • Make this the last time you ever call ShellExecute. Its error handling is useless. Why did you call GetLastError? Where does it say to do that in the documentation. ShellExecute only exists for compat with old programs. You are expected to call ShellExecuteEx. Anyway, don't you need to be elevated to do this? Commented Feb 24, 2015 at 9:55
  • ShellExecute can execute verbs defined on file classes. It fails in this case because the file class for true-type fonts (ttffile) doesn't define verbs directly on itself. It does implement IContextMenu and inject additional items into the right-click menu, and one of those items is verb "install", but that can only be invoked via IContextMenu::InvokeCommand. ShellExecute doesn't know how to do this. This probably isn't what you want anyway, though, as there is no way to shut off the user interaction, including prompting to delete the font if it already exists etc. Commented Nov 30, 2019 at 1:04

2 Answers 2

7

WM_FONTCHANGE only notifies applications of a new font in the system, but it doesn't actually tell the system what the new font is.

Before sending WM_FONTCHANGE you need to call AddFontResource to add the font to the system font table. If you want the font to remain after a reboot, you also need to add an entry to the registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts (see the documentation for AddFontResource for more information).

Sign up to request clarification or add additional context in comments.

5 Comments

o0o... I thought AddFontResource was only per-process instance.
@JerryDodge: you are thinking of AddFontMemResourceEx() instead: "A font that is added by AddFontMemResourceEx is always private to the process that made the call and is not enumerable." Or maybe AddFontResourceEx() with the FR_PRIVATE flag: "Specifies that only the process that called the AddFontResourceEx function can use this font."
Haven't accepted yet because I'm still not having any luck with it... Still trying to figure out where I'm going wrong. I've both called AddFontResource successfully and written this registry entry successfully before calling this message, and Windows Explorer still doesn't want to show it. Not to mention having to detect what name to register it under... When I install it via other software (including the file itself) it installs fine. I guess there's still some missing pieces here.
An even easier method is to just use the shell to install it, using ShellExecuteEx with the "install" verb.
Using ShellExecuteW(Handle, 'install', PWideChar(Filename), nil, nil, SW_HIDE) is returning 31 (indicating an error) and GetLastError is telling me 1155 ("No application is associated with the specified file for this operation."). Hmmm this doesn't seem right... Does it have to be via ShellExecuteEx?
2

I have just traced through exactly what Windows 7 does when it installs a font. Here is a summary:

  • If the font is a true-type font and its name doesn't already end with " (TrueType)" then it appends this.
  • If the font already exists, it can uninstall it in order to reinstall it:
    • It calls RemoveFontResourceW.
    • The registry entry describing the font, if any, is deleted out of SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts.
  • It takes the filename you are trying to install, and if that already exists in the Fonts directory, then it scans for a unique filename by repeatedly adding 1 to a counter and then formatting "basename_X.ttf" where X is in hexadecimal. So e.g. if "myfont_1.ttf" through "myfont_9.ttf" already exist, then it will try "myfont_A.ttf" next.
  • It copies the file you supplied to this free filename it has identified.
  • It calls AddFontResourceW on the target path.
  • It writes an entry to SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts based on the "(TrueType)"-qualified name of your font whose value is the target filename without path.
  • It does a thing I couldn't quite figure out, creating a PropertyStore and putting a bunch of values into it. I'm not sure what exactly it does with the resulting property store, but it calls it a FID.
  • It waits for 2 seconds by calling Sleep.
  • It calls PostMessageW(HWND_BROADCAST, WM_SETTINGSCHANGE, NULL, L"fonts")
  • It calls PostMessageW(HWND_BROADCAST, WM_FONTCHANGE, NULL, NULL)
  • It calls SHGetSpecialFolderLocation(CSIDL_FONTS) and then passes the resulting IDLIST into SHChangeNotify(SHCNE_UPDATEDIR, SHCNF_IDLIST, idlist, NULL).

I suspect it is these latter three that are crucial in getting the system to recognize the new font in other applications and in the Fonts folder.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.