Dynamiczna instalacja fontów podczas rozruchu programu zawiesza cały proces

0

Mój program podczas rozruchu dynamicznie instaluje fonty jako zasoby dla własnego użytku. Z racji tej, że program jest przenośny, wszystkie wymagane fonty posiada w swoich podkatalogach. Instalowane one są za pomocą funkcji AddFontResourceW z modułu Windows, a informacje o nich dodawane są do mapy – te dane są używane podczas deinstalacji fontów za pomocą RemoveFontResourceW, przy zamykaniu programu. Po instalacji i deinstalacji fontów, wołany jest SendMessage z komunikatem WM_FONTCHANGE, tak jak dokumentacja wskazuje.

Kod instalujący i deinstalujący wygląda tak:

type
  TFontsMap = class(specialize TFPGMap<WideString, LongInt>)
  public
    procedure LoadFromDirectory(const APath: String);
    procedure Unload();
  end;

{..}

procedure TFontsMap.LoadFromDirectory(const APath: String);
var
  FoundItem: TSearchRec;
var
  FontName: WideString;
  FontsCount: LongInt;
begin
  if FindFirst(APath + '*.?tf', faAnyFile, FoundItem) = 0 then
  try
    repeat
      FontName := UTF8ToUTF16(APath + FoundItem.Name);
      FontsCount := Windows.AddFontResourceW(PWideChar(FontName));

      Add(FontName, FontsCount);
    until FindNext(FoundItem) <> 0;
  finally
    FindClose(FoundItem);
  end;

  if FindFirst(APath + '*', faAnyFile, FoundItem) = 0 then
  try
    repeat
      if (FoundItem.Name = '.') or (FoundItem.Name = '..') then Continue;

      if FoundItem.Attr and faDirectory = faDirectory then
        LoadFromDirectory(APath + FoundItem.Name + '\');
    until FindNext(FoundItem) <> 0;
  finally
    FindClose(FoundItem);
  end;
end;

procedure TFontsMap.Unload();
var
  FontIndex: Integer;
  FontName: WideString;
begin
  for FontIndex := 0 to Count - 1 do
    if Data[FontIndex] <> 0 then
    begin
      FontName := Keys[FontIndex];
      Windows.RemoveFontResourceW(PWideChar(FontName));
    end;
end;

Powyższe metody wołane są z głównego obiektu zarządzającego, a po nich SendMessage:

type
  TFonts = class(TObject)
  private
    FFonts: TFontsMap;
  {..}
  public
    procedure LoadFromFiles(const APath: String);
    procedure UnloadFiles();
  end;

{..}

procedure TFonts.LoadFromFiles(const APath: String);
begin
  FFonts.LoadFromDirectory(APath);
  Windows.SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
end;

procedure TFonts.UnloadFiles();
begin
  FFonts.Unload();
  Windows.SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
end;

Problem polega na tym, że fonty zawsze instalują się i deinstalują prawidłowo, ale czasem podczas instalacji program zatrzymuje się na SendMessage i tak wisi w nieskończoność. Okazuje się, że program zawiesza się tylko wtedy, gdy uruchomiony jest Inkscape, GIMP lub Photoshop – gdy są wyłączone to wstaje za każdym razem, bez zająknięcia.

Ma ktoś pojęcie dlaczego tak się dzieje? :/


Sam nie rozumiem co mają ww. programy graficzne do tego, ale sprawdziłem dokładnie i ten błąd zreprodukowałem nawet pod WinXP (u klienta ten błąd występował pod Win10). Linijka po linijce pod debuggerem sprawdziłem i instalacja fontów przebiega bez problemu, a jak dochodzę do SendMessage to wykonanie tej linijki trwa w nieskończoność.

2

SendMessageA sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.
PostMessageA posts a message in the message queue associated with the thread that created the specified window and returns without waiting for the thread to process the message.

Jeśli nie musisz czekać na odpowiedź, to najprościej w świecie użyj PostMessage. Wiadomość powędruje w świat, a czy zostanie na którymkolwiek z programów (obstawiam że klasa "Windows" w Twoim kodzie, wcale nie odnosi się do Form, przynależących do Twojej aplikacji) prawidłowo odczytana, to już inna bajka.

1

Ja w Inkscape (a może też i GIMPie) miałem tak, że nie mogłem używać fontów zainstalowanych dla bieżącego użytkownika, tylko musiałem jako administrator zainstalować je dla wszystkich użytkowników... Inaczej te apki nie widziały fontów.

Najlepsze programy czytają fonty prosto z plików, bez zabawy w instalację/rejestrowanie itp. :] (Blender, Unity 3D)

0
MexikanoS napisał(a):

Jeśli nie musisz czekać na odpowiedź, to najprościej w świecie użyj PostMessage.

Dokumentacja nie opisuje użycia PostMessage, dlatego z niego nie korzystam, bo nie wiem jakie będą skutki uboczne. Co mi z tego, że program uruchomi się normalnie, skoro np. fonty nie będą widoczne?

Muszę sprawdzić czy to zadziała w praktyce, ale nie dziś, bo potrzebuję peceta nieposiadającego fontów, które program instaluje. Nie wiem czy muszę czy nie muszę – dokumentacja twierdzi, że muszę.

[…] (obstawiam że klasa "Windows" w Twoim kodzie, wcale nie odnosi się do Form, przynależących do Twojej aplikacji) […]

Nie no Windows to nazwa modułu, w którym znajdują się nagłówki funkcji itd.

Prefiks w postaci nazwy tego modułu w tym przypadku nie jest potrzebny, bo kolizja nazw nie zachodzi, ale w ten sposób oznaczam sobie wywołania instrukcji zależnych od platformy. Łatwiej mi czytać i rozumieć taki kod.

2

Spróbuj użyć AddFontResourceEx (AddFontResourceExW).

Wtedy będziesz mógł ustawić dodatkowe flagi, w tym taką, która oznacza, że tylko Twój proces może korzystać z tak zainstalowanych krojów (dla innych procesów tak zainstalowane kroje są niewidoczne).
I nie będziesz musiał powiadamiać systemu.

1

Miałem kiedyś podobny problem i przeglądam źródła jednego z moich starych programów gdzie też w końcu zrobiłem jak sugeruje @Stefan_3N

var
  cResHandle, cResData, cResSize, cFondAdded: Cardinal;
  pResFont: Pointer;
//....
      cResHandle:= FindResource(hInstance, MAKEINTRESOURCE(1103), 'RT_FONT'); //oczywiscie użyj swojego identyfikatoru zasobu
      cResSize:= SizeOfResource(hInstance, cResHandle);
      cResData:= LoadResource(hInstance, cResHandle);
      pResFont:= LockResource(cResData);
      if Assigned(pResFont) then
      begin
        fFontHandle:= AddFontMemResourceEx(pResFont, cResSize, nil, @cFondAdded);
        if fFontHandle > 0 then
        begin
          edName.Font.Name:= 'MAC C Times';
        end;
      end;

Jak widać trochę streszczałem się z obsługą ewentualnych błędów prawdopodobnie na potrzeby testu i tak zostało...

Co ciekawe w OnDestroy teraz zauważyłem nadal mam SendMessage i tu nie powoduje problemu.

      RemoveFontMemResourceEx(fFontHandle);
      SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);

W pliku zasobów RC

/* rosyjska czcionka MAC C Times */
1103 RT_FONT "fonts\MCTIME.TTF"
0

@kAzek: tak, tyle że ty używasz AddFontMemResourceEx, czyli wersji ładującej z zasobów. ;)

U mnie fonty są w osobnych plikach, wrzuconych do katalogu fonts\ (bezpośrednio lub w kolejnych podkatalogach), a program instaluje wszystkie jakie znajdzie wewnątrz, dlatego wykorzystywana jest rekurencyjna metoda wyszukująca. No ale to nie problem – AddFontResource(Ex) do tego właśnie służy.


Teoretycznie mógłbym zostawić kod w takiej postaci w jakiej jest obecnie i po prostu usunąć SendMessage, bo instalowane fonty mają być widoczne głównie dla samego programu – i bez rozgłaszania komunikatu będą one dla niego dostępne. No ale to raczej wchodzi w grę – lepiej jest skorzystać z wersji Ex i mieć problem z głowy. Tylko muszę sobie nagłówek dociągnąć, bo oczywiście go nie ma w module Windows… :/

0

@kAzek: mam prośbę. Możesz sprawdzić w Delphi jakie wartości mają flagi FR_PRIVATE i FR_NOT_ENUM? W dokumentacji wartości tych flag nie są podane. Co prawda znalazłem o nich informacje tutaj:

Value of nLoadFlag can be either: FR_PRIVATE (0x10) or FR_NOT_ENUM (0x20) or the sum of them.

ale nie mam pewności co do ich poprawności.

1

Szesnastkowo
FR_PRIVATE = 0x10
FR_NOT_ENUM = 0x20
czyli tak samo.

1

Ostatecznie skorzystałem z rady @Stefan_3N i użyłem funkcji AddFontResourceEx i pozbyłem się SendMessage. Testy jeszcze przede mną, więc w razie problemów lub nowych dziwnych bugów dam znać. No ale póki co kod działa perfekcyjnie, więc chwilowo problem rozwiązany.

W razie gdyby ktoś potrzebował sobie zaimplementować tego typu dynamiczne instalowanie fontów na wyłączny użytek swojego programu, to niżej podaję kod, którego sam używam. Tutaj brakujące nagłówki i stałe:

uses
  Windows;

const
  FR_PRIVATE  = $00000010;
  FR_NOT_ENUM = $00000020;

  function AddFontResourceExW(
    name: LPCWSTR;
    fl: DWORD;
    res: PVOID
  ): LongInt; stdcall external 'gdi32.dll';

  function RemoveFontResourceExW(
    name: LPCWSTR;
    fl: DWORD;
    pdv: PVOID
  ): BOOL; stdcall external 'gdi32.dll';

a tutaj kod metod – ładującej i deinstalującej fonty ze wszystkich plików .ttf i .otf znalezionych w danej lokalizacji (choć pasuje przerobić filtrowanie, bo np. rozszerzenie .wtf też pasuje, ale takie pliki fontami nie są):

procedure TFontsMap.LoadFromDirectory(const APath: String);
var
  FoundItem: TSearchRec;
var
  FontName: WideString;
  FontsCount: LongInt;
begin
  if FindFirst(APath + '*.?tf', faAnyFile, FoundItem) = 0 then
  try
    repeat
      FontName := UTF8ToUTF16(APath + FoundItem.Name);
      FontsCount := AddFontResourceExW(PWideChar(FontName), FR_PRIVATE, nil);

      Add(FontName, FontsCount);
    until FindNext(FoundItem) <> 0;
  finally
    FindClose(FoundItem);
  end;

  if FindFirst(APath + '*', faAnyFile, FoundItem) = 0 then
  try
    repeat
      if (FoundItem.Name = '.') or (FoundItem.Name = '..') then Continue;

      if FoundItem.Attr and faDirectory = faDirectory then
        LoadFromDirectory(APath + FoundItem.Name + '\');
    until FindNext(FoundItem) <> 0;
  finally
    FindClose(FoundItem);
  end;
end;

procedure TFontsMap.Unload();
var
  FontIndex: Integer;
  FontName: WideString;
begin
  for FontIndex := 0 to Count - 1 do
    if Data[FontIndex] <> 0 then
    begin
      FontName := Keys[FontIndex];
      RemoveFontResourceExW(PWideChar(FontName), FR_PRIVATE, nil);
    end;
end;

Reszta pozostaje bez zmian, czyli jest zgodna z kodem podanym w pierwszym poście tego wątku (mowa przede wszystkim o mapie do przechowywania informacji o instalowanych fontach). Zabezpieczenia i obsługę błędów dodam sobie później.

Dziękuję wszystkim za aktywność i pomoc. ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1