29.10.2023, Vladimír Klaus, navštíveno 650x

Delphi
Systém

Všichni, kdo programují v Delphi, znají jeho moc a flexibilitu, ale také tu a tam narazíme na zádrhely, které nám způsobí vrásky na čele. Jedním z takových zapeklitých problémů je použití funkce ShellExecute pro otevření JPG souboru. Funkci používám už mnoho let a nikdy s ní nebyly žádné potíže.

Jak v Delphi řešit problém se ShellExecute, když vrací 31, tedy SE_ERR_NOASSOC, ale není k tomu důvod

Problém

Cílem bylo jednoduše otevřít JPG soubor ve výchozí aplikaci prostřednictvím funkce ShellExecute. Tento kód obvykle funguje na běžných desktopových verzích Windows. Nicméně, když jsem to zkoušel na Windows Serveru, funkce mi vracela chybový kód 31, což je SE_ERR_NOASSOC.

První krok bylo ověřit, zda soubor a cesta k němu jsou v pořádku. Ano, všechno bylo v pořádku. V dalším kroku jsem zkontroloval, jestli nemám problém s oprávněními. Jelikož jsem byl přihlášen jako Administrátor, toto také nebyl problém. Stejně tak poklepání na soubor způsobilo zcela korektní spuštění asociované aplikace a otevření daného souboru. Jelikož standardní postupy selhaly, zkoušel jsem různé způsoby, jak problém obejít. Například, pomocí explicitní cesty k mspaint.exe s parametrem cesty k souboru fungovala. Avšak, i po znovu nastavení výchozí aplikace pro otevření JPG souborů, problém přetrvával.

Řešení

Zkoušel jsem několik metod, až jsem narazil na diskuzi z roku 2003, kde se někdo potýkal s podobným problémem. Nakonec jsem problém vyřešil tím, že jsem postup rozdělil na dva kroky

  1. zjistit asociovanou aplikaci
  2. zavolat aplikaci s parametrem
var app:=ZjistiAsociovanouAplikaci(TempSoubor);
if FileExists(app) then begin
  SimpleRun(app, TempSoubor);
end else begin
  VKWarning('Pro příponu "%s" nemáte žádnou výchozí aplikaci!', [ExtractFileExt(TempSoubor)]);
end;

Jak vypadá zjišťování asociované aplikace? Třeba takto:

{*----------------------------------------------------------------------
  Vrátí název aplikace (plnou cestu) asociované se zadaným souborem
-----------------------------------------------------------------------}
function ZjistiAsociovanouAplikaci(aFileName: string): string;
var
  Aplikace: PChar;
begin
  GetMem(Aplikace,MAX_PATH);
  case FindExecutable(PChar(aFileName),PChar(GetCurrentDir),Aplikace) of
    0..30: result:=''; //chyba; soubor neexistuje?
    31: result:='' //soubor nemá žádnou asociovanou aplikaci
    else result:=Aplikace;
  end;
  FreeMem(Aplikace);
end;

Spuštění aplikace/souboru pak takto:

{*----------------------------------------------------------------------
  Jednoduché spuštění aplikace nebo zobrazení souboru pomocí
  výchozí aplikace. Slouží i k navigování na web atd.
-----------------------------------------------------------------------}
procedure SimpleRun (aFileName: string; aParam: string = ''; aShowCmd: integer = SW_SHOW);
var
  i: integer;

begin
  //pro jistotu se přepnu do adresáře spouštěné aplikace
  SetCurrentDirectory(PChar(ExtractFilePath(aFileName)));
  //podle toho, jestli to volám s parametrem
  if Trim(aParam)<>'' then begin
    i:=ShellExecute(0,'open',PChar(aFileName),PChar(aParam),nil,aShowCmd);
  end else begin
    i:=ShellExecute(0,'open',PChar(aFileName),nil,nil,aShowCmd);
  end;
  //když se vrátí nízké číslo, jde o nějaký problém - já z toho dělám výjimku
  if i<=32 then begin
    raise Exception.CreateFmt('ShellExecute result = %d',[i]);
  end;
end;

A voilà, funguje to! Tento problém ukazuje, že i staré problémy se občas vracejí a řešení nemusí být vždy zřejmé. Ale s trochou vytrvalosti a kreativity, i ty nejzapeklitější problémy mohou být vyřešeny. Doufám, že tento postup pomůže někomu, kdo se s něčím podobným setká.