Skojarzenie pliku z naszym programem bez względu na rozszerzenie

reichel

1 Wstęp
2 Rozszerzenia w systemie Windows
3 Idea
4 Implementacja interfejsu IShellExecuteHook
5 Implementacja interfejsu IShellIconOverlayIdentifier
6 Uwagi i wnioski
7 Literatura, pomocne linki

Wstęp

Na wstępie pragnę od razu wspomnieć, że tekst ten to raczej rozważania ne temat
problemu stosowania rozszerzeń niż lekarstwo na wszelkie problemy związane z nimi.
Poruszę tu problem ukrywania rozszerzeń (i ten fragment będzie miał znamiona największej użyteczności).
Pozostały fragment tyczył się zaś będzie problemu a co by było gdyby nie było rozszerzeń.
Część ta jednak nie będzie całkowicie nieprzydatna, będzie z niej można się nauczyć obsługi takich interfejsów
jak:

IShellExecuteHook

IShellIconOverlayIdentifier

Rozszerzenia w systemie Windows

Temat jest bardzo dobrze opisany zarówno w samym MSDN [1] jak i na stronie 4programmers.net [2].
Zatem chciałbym tu tylko wspomnieć w jaki sposób można zawsze ukryć lub zawsze pokazywać rozszerzenie.
Służą do tego dwa wpisy (klucze) w rejestrze:

AlwaysShowExt - aby rozszerzenie zawsze było pokazywane

NeverShowExt - aby rozszerzenie nigdy nie było widoczne

są to klucze typu string (reg_sz) posiadające pustą wartość.
Umieszcza się je albo w gałęzi oznaczającej samo rozszerzenie np
HKEY_CLASSES_ROOT.moje_rozszezenie
jak w przypadku wszystkich plików (HKEY_CLASSES_ROOT*)
lub też w gałęzi opisującej typ pliku
HKEY_CLASSES_ROOT\moje_rozszezenie
jak np. skróty (HKEY_CLASSES_ROOT\lnkfile).

Idea

Chcemy, aby nasze pliki nie posiadały rozszerzenia (albo posiadały jakiekolwiek rozszerzenie),
a pomimo tego były rozpoznawane właściwie przez powłokę windows. Jedyną możliwością w tym przypadku jest rozpoznanie
pliku po jego zawartości i zakładamy, że nasz typ pliku to umożliwia.

Co zatem jest nam potrzebne? W pierwszej kolejności powinniśmy przechwycić moment uruchomienia (kliknięcia na ikonkę)
pliku przez powłokę. Taka akcja odbywa się najczęściej poprzez wywołanie funkcji ShellExecute (ShellExecuteEx) z parametrem open.
Istnieje możliwość albo przechwycenia API (co jednak nie jest najlepszą praktyką) albo zwrócenie uwagi na gotowy mechanizm pozwalający to uczynić
a opisywany poprzez interfejs rozszerzający powłokę windows: IShellExecuteHook.

Druga część związana z ikoną niestety nie jest już tak łatwa do zrealizowania (przy założeniu, że nie chcemy używać mechanizmów przechwytujących funkcje
czy też komunikaty pochodzące od okien). W systemie windows nie przewidziano żadnego mechanizmu pozwalającego na podstawienie swojej własnej ikony dla plików
nie posiadających rozszerzenia. Istotnie możemy podmienić taką ikonę globalnie, tak że wszystkie pliki bez rozszerzeń będą miały swoją własną.

Wydawało by się co prawda, że interfejs IExtractIcon jest doskonały do tego celu, ma on jednak poważny mankament - nie może być stosowany do klasy plików *
( w przeciwieństwie do zakładek IShellPropSheetExt, haków na operacje na pliku ICopyHook czy też menu podręcznego IContextMenu).
Czy oznacza to, że pozostają nam tylko haki ? Na szczęście nie, co prawda rozwiązania tego nie można określić jak doskonałego, pozwala ono
wstawić ikonę dla naszego pliku. Rozwiązanie to opiera się o dość rzadko stosowany interfejs IShellIconOverlayIdentifier
(poza implementacjami w samym windows - skrót, udostępnianie, nie spotkałem się z jego innym praktycznym zastosowaniem).

Ma on jednak pewne wady:

  • niestety nie wszystkie programy biorą go pod uwagę (chociażby Total Commander),
  • ikona nie zmienia koloru po zaznaczeniu (można udawać taki efekt),
  • w trybie thumbnails ikona nie jest wyświetlana centralnie,
  • może być tylko jedna ikona tego typu (więc jeśli stworzymy skrót do takiego pliku, stracimy ikonę),
  • instalacja interfejsu wymaga restartu,

to jednak mimo tak wielu wad na razie poprzestaniemy na tym rozwiązaniu.

Implementacja interfejsu IShellExecuteHook

Interfejs ten jest banalny jeśli chodzi o oprogramowanie. Wystarczy wypełnić tutaj metodę Execute.
Do niej podawana jest struktura TShellExecuteInfo zawierająca informacje o pliku, który został uruchomiony (w najprostszym przypadku
ikonka tego pliku została dwa razy kilknięta). teraz do akcji wkracza nasz kod. Funkcja CzyMojPlik sprawdza czy
plik spełnia kryteria naszego pliku, czyli czy znajduje się w nim na początku napis "To moj wypasiony plik". Tu należy zwrócić uwagę na
obsługę błędów. Powinniśmy starannie łapać wszelkie wyjątki, w przeciwnym razie możemy spowodować wysypanie się powłoki (czyli w 99,9% explorer.exe).
Nie obojętne jest też dbanie o rezerwowanie pamięci oraz jej zwalnianie. W rozszerzeniach powłoki windows powinno się to robić za
pomocą interfejsu IMalloc oraz funkcji SHGetMalloc.

Poniżej kod funkcji CzyMojPlik:

unit helper;

interface

uses Windows, SysUtils, ActiveX, ShlObj;

function CzyMojPlik(path:string):boolean;

implementation

function CzyMojPlik(path:string):boolean;
const
  checkstr = 'To moj wypasiony plik';//naglowek naszego pliku
var
  P:PChar;
  l:longint;
  F:File;
  SM:IMalloc;
begin
  Result := false;
  if Failed(SHGetMalloc(SM)) then Exit;
  try
  if not FileExists(path) then exit;
  AssignFile(F,path);
  {$I-}
  Reset(F,1);
  l := Length(checkstr)+1;
  if FileSize(F) < l-1 then
  begin
    CloseFile(F);
    exit;
  end;
  P := SM.Alloc(l);
  try
    FillChar(P^,l,0);
    BlockRead(F,P^,l-1);
    result := StrComp(checkstr,P) = 0;
  finally
    SM.Free(P);
    CloseFile(F);
  end;
  {$I+}
  finally
   SM := nil;//teoretycznie samo delphi zniszczy interfejs po wyjsciu z funkcji
  end;
end;

end.

Jeśli teraz już mamy pewność, że pracujemy z naszym plikiem powinniśmy podmienić domyślną akcje dla tego pliku (powiedzmy, że jest to plik tekstowy i domyślnie otwiera się w notatniku).
Robimy to za pomocą podmiany wartości w strukturze TShellExecuteInfo. W miejsce parametrów lpParameters wstawiamy
wartość z lpFile (ona tak naprawdę zawiera parametry, oczywiście mogą być odstępstaw od tej reguły). Zerujemy też parametr lpVerb
zawierający opcje co ma się stać z plikiem: open, edit, print. W tym najprostszym przypadku nie sprawdzamy czy przypadkiem nie jest on pusty na starcie, jeśli tak by było powinniśmy
sprawdzić czy przypadkiem w lpFile nie ma podanej aplikacji do uruchomienia (przed podaniem do lpParameters powinno się ją usunąć).
Na koniec uruchamiamy naszą aplikację za pomocą funkcji ShellExecuteEx (zakładamy, że nasza aplikacja jest w tym samym folderze co
plik dll rozszerzenia).

Implementacja interfejsu:

unit seh_unit;

interface

uses
  Windows, ActiveX, ComObj, ShlObj, ShellApi;

type
  TShellExecuteHook = class(TComObject, IShellExecuteHook)
  public
    function Execute(var ShellExecuteInfo: TShellExecuteInfo):HResult;stdcall;
  end;

const
  Class_ShellExecuteHook: TGUID = '{603BD06E-7102-408E-A46E-3465EA4551E2}';
  szShellExecuteHook = 'ShellExecuteHook: Moj wypasiony plik';
implementation

uses ComServ, SysUtils, Registry, helper;



function TShellExecuteHook.Execute(var ShellExecuteInfo: TShellExecuteInfo):HResult;
var
 mp:array[0..MAX_PATH] of char;
begin
  ShellExecuteInfo.hInstApp := 33; // Gdy blad to wartosc mniejsza rowna od 32

  if CzyMojPlik(ShellExecuteInfo.lpFile) then
  begin
    //wykorzystamy stara strukture
    ShellExecuteInfo.fMask := SEE_MASK_NOCLOSEPROCESS;
    ShellExecuteInfo.lpParameters :=  PChar('"'+ShellExecuteInfo.lpFile+'"');
    ShellExecuteInfo.lpVerb := nil;
    //tu podajemy sciezke do naszej aplikacji, zakladamy ze w tym samym folderze co dll'ka
    GetModuleFileName(Hinstance,mp,sizeof(mp));
    ShellExecuteInfo.lpFile := PChar(ExtractFIlePath(mp)+'Project1.exe');
    ShellExecuteEx(@ShellExecuteInfo);
    result:= S_OK;
  end
  else
    result:= S_FALSE;//jesli to nie nasz plik to nie robmy nic
end;

type
  TShellExecuteHookFactory = class(TComObjectFactory)
  public
    procedure UpdateRegistry(Register: Boolean); override;
  end;

procedure TShellExecuteHookFactory.UpdateRegistry(Register: Boolean);
var
  ClassID: string;
begin
  if Register then
    begin
      inherited UpdateRegistry(Register);
      ClassID := GUIDToString(Class_ShellExecuteHook);
      with TRegistry.Create do
        try
          RootKey := HKEY_LOCAL_MACHINE;
          OpenKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions', True);
          OpenKey('Approved', True);
          WriteString(ClassID, szShellExecuteHook);
          CloseKey;
          OpenKey('Software\Microsoft\Windows\CurrentVersion\Explorer\ShellExecuteHooks\', True);
          WriteString(ClassID, szShellExecuteHook);
        finally
          Free;
        end;
      end
  else
  begin
    with TRegistry.Create do
    try
      RootKey := HKEY_LOCAL_MACHINE;
      OpenKey('Software\Microsoft\Windows\CurrentVersion\Explorer\ShellExecuteHooks\', True);
      DeleteValue(ClassID);
      CloseKey();
      DeleteKey('Software\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\WypasPlik');
    finally
          Free;
    end;
    inherited UpdateRegistry(Register);
  end;
end;

initialization
  TShellExecuteHookFactory.Create(ComServer, TShellExecuteHook, Class_ShellExecuteHook, '',
    szShellExecuteHook, ciMultiInstance, tmApartment);
end.

Implementacja interfejsu IShellIconOverlayIdentifier

Niestety MS Windows nie posiada ogólnej możliwości podmiany dowolnej ikony. Jednak za pomocą interfesu IShellIconOverlayIdentifier
można odrobinę oszukać system. Interfejs ten odpowiada z tworzenie ikon w stylu skrótu, pliki/foldery udostępnione, my wykorzystamy go w niestandardowy sposób.
Najczęściej ikona ta jest bardzo mała (w rogu), my jednak użyjemy normalnej ikony (najlepiej z białym tłem). Dzięki temu przesłoni ona nam oryginalną ikonę
danego rozszerzenia. Jeśli chodzi o sam interfejs jest on na tyle prosty, że jedyne o czym można wspomnieć to parametr pIPriority
w funkcji GetPriority odpowiadający za ważność ikony, gdyż może ona być tylko jedna dla pliku (tu wada rozwiązania, musimy wybrać pomiędzy ikoną skrótu dla naszego plik a samą ikoną,
powinna się zatem jeszcze znaleźć funkcja sprawdzająca skrót. Można tą niedogodność obejść implementując interfejs IExtractIcon dla plików *.lnk).

UWAGA! Interfejs IShellIconOverlayIdentifier staje się aktywny dopiero po restarcie systemu.

//**********************************************************************
//**********************************************************************
//sioi_unit - ShellIconOverlayIdentifier
//[email protected]
//http://rudy.mif.pg.gda.pl/~reichel/
//http://reichel.pl
//2007.09.22
//**********************************************************************
//**********************************************************************
unit sioi_unit;

interface

uses
  Windows, ActiveX, ComObj, ShlObj, Classes, ShellAPI;


{$R w.RES} //ikona :)

type
  TShellIconOverlayIdentifier = class(TComObject,IShellIconOverlayIdentifier)
  private
    pMalloc: IMalloc;
  protected
    //IShellIconOverlayIdentifier
    function IsMemberOf(pwszPath: PWideChar; dwAttrib: DWORD): HResult; stdcall;
    function GetOverlayInfo(pwszIconFile: PWideChar; cchMax: Integer;
      var pIndex: Integer; var pdwFlags: DWORD): HResult; stdcall;
    function GetPriority(out pIPriority: Integer): HResult; stdcall;


 public
    procedure  Initialize; override;
    destructor Destroy; override;
  end;

const
  Class_ShellIconOverlayIdentifier: TGUID = '{88E26886-2121-4A93-9DD5-97902DFA725C}';
  szShellIconOverlayIdentifier = 'ShellIconOverlayIdentifier: Moj wypasiony plik';

implementation


uses ComServ, SysUtils, Registry, Math, helper;


function TShellIconOverlayIdentifier.IsMemberOf(pwszPath: PWideChar; dwAttrib: DWORD): HResult;
var
  Path:String;
begin
  Result := S_FALSE;//nie jesli nie nasza ikona
  Path := WideCharToString(pwszPath);
  if CzyMojPlik(Path) then
  begin
     Result := S_OK;
  end;

end;

function TShellIconOverlayIdentifier.GetOverlayInfo(pwszIconFile: PWideChar; cchMax: Integer;
      var pIndex: Integer; var pdwFlags: DWORD): HResult;
var
 iconfile:string;
 fn:array[0..Max_PATH] of Char;
begin


 GetModuleFilename(Hinstance,fn,sizeof(fn));
 iconfile := fn;

 StringToWideChar(iconfile,pwszIconFile,Length(iconfile)*SizeOf(WideChar) + 1);

 pdwFlags := ISIOI_ICONFILE;
 pIndex  := 0;
 result := S_OK;
end;

function TShellIconOverlayIdentifier.GetPriority(out pIPriority: Integer): HResult;
begin
   pIPriority := 0;
   result := S_OK;
end;

procedure TShellIconOverlayIdentifier.Initialize;
begin
  inherited;
  if Failed(ShGetMalloc(pMalloc)) then
    pMalloc := nil;
end;

destructor TShellIconOverlayIdentifier.Destroy;
begin
  inherited;
  pMalloc := nil;
end;


type
  TShellIconOverlayIdentifierFactory = class(TComObjectFactory)
  public
    procedure UpdateRegistry(Register: Boolean); override;
  end;

procedure TShellIconOverlayIdentifierFactory.UpdateRegistry(Register: Boolean);
var
  ClassID: string;
begin
  if Register then begin
    inherited UpdateRegistry(Register);

    ClassID := GUIDToString(Class_ShellIconOverlayIdentifier);
      with TRegistry.Create do
        try
          RootKey := HKEY_LOCAL_MACHINE;
          OpenKey('Software\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\WypasPlik',True);
          WriteString('',ClassID);
          CloseKey;
        finally
          Free;
        end;
    if (Win32Platform = VER_PLATFORM_WIN32_NT) then
      with TRegistry.Create do
        try
          RootKey := HKEY_LOCAL_MACHINE;
          OpenKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions', True);
          OpenKey('Approved', True);
          WriteString(ClassID, szShellIconOverlayIdentifier);
        finally
          Free;
        end;
  end
  else begin
    with TRegistry.Create do
    try
      RootKey := HKEY_LOCAL_MACHINE;
      DeleteKey('Software\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\WypasPlik');
    finally
          Free;
    end;
    inherited UpdateRegistry(Register);
  end;
end;

initialization
  TShellIconOverlayIdentifierFactory.Create(ComServer, TShellIconOverlayIdentifier, Class_ShellIconOverlayIdentifier, '', szShellIconOverlayIdentifier,
               ciMultiInstance, tmApartment);
end.

Uwagi i wnioski

*Oczywiście istnieje problem z plikami (ich ikonami) bez rozszerzeń, rozwiązaniem jest wspomniane białe tło.
*Należy być swiadomym zwiększania obciążenia system.

W załączniku znajduje sie kod bibliotek, przykładowa aplikacja obsługująca format pliku i zestaw plików testowych.
bez_rozszerzenia.rar

Literatura, pomocne linki

[1] http://msdn2.microsoft.com/en-us/library/aa969357.aspx [2] [[Delphi/FAQ/Jak_skojarzyć_moją_aplikację_z_rozszerzeniem_danego_typu]] [3] [[Delphi/FAQ/Jak_wyświetlić_ikonę_skojarzoną_z_danym_rozszerzeniem]]

0 komentarzy