Delphi - A Windows Shell titkai

WinShell 1. rész

forráskód letöltése
Most induló cikksorozatunkban a Windows Shell titkait, nem vagy csak kevésbé dokumentált lehetőségeit kutatjuk.

A sorozatból így például megtudhatjuk majd, hogy miként rendelhetünk különböző ikonokat ugyanolyan kiterjesztéssel rendelkező állományokhoz. Fény derül arra is, hogy miként jeleníthetünk meg egyedi információkat egy-egy állományról, ha arra rámutat a felhasználó egy Windows Intézőben. Az első részben arra keresünk megoldást, hogy miként egészíthetjük ki azt a menürendszert, mely akkor jelenik meg, ha a felhasználó a Windows Intézőjében egy állományon jobb gombbal kattint. A tetszőlegesen megjelenő menüpontokhoz persze egyedi funkciókat is rendelhetünk. Gondoljunk csak például a WinZip hasonló módszerű szolgáltatásaira. A mellékelt példa kipróbálásához az alábbi lépésekre van szükség:
1. Nyissa meg a ShellMenuExt.dpr és a Project - Build menüponttal fordítsa le. Ekkor létrejön a ShellMenuExt.dll.
2. Nyissa meg szerkesztésre a mellékelt ShellMenuExt.reg állományt. Például egy Windows Intézővel keresse meg. Jobb gombbal kattintson rá, majd válassza az Edit menüpontot.
3. Ebben az állományban a @="D:\\Dso\\0245\\ShellSecret01\\ShellMenuExt.dll" sorban található elérési útvonalat javítsa ki arra, ahová mellékelt példaprogramot helyezte. Lényeg az, hogy a ShellMenuExt.dll-nek itt korrektül meg legyen adva a helye. Ügyeljen arra is, hogy az elérési útvonal megadásánál a \ jelet duplázva kell használnia!
4. Mentse és zárja a ShellMenuExt.reg állományt
5. Kattintson rá a ShellMenuExt.reg állományra. Ekkor a Windows megkérdezi, hogy szeretné-e tartalmát a regisztrációs adatbázishoz hozzáfűzni. Erre igennel válaszoljon.
6. Ezek után kattintson jobb gombbal a mellékelt a.sec állományon. A megjelenő menü első menüpontját már a saját ShellMenuExt.dll programunk szolgáltatja.

Fontos tudnivaló, hogy ha ezek után szeretnénk a DLL-ünket újra fordítani, akkor előtte be kell zárnunk az összes olyan alkalmazást, mely használja a ShellMenuExt.dll-t, mint például a Windows Intézője. E lépés nélkül a DLL-t nem lehet felülírni, így a Delphi-ben a fordításkor a Could not create output file hibaüzenettel találjuk szembe magunkat.


Nézzük most miként is készült ez a DLL, hogyan írhatunk tetszőleges, saját funkcióinkkal felszerelt alkalmazást.

A megvalósításhoz egy speciális DLL-re van szükségünk, mely COM objektumot tartalmaz. Ettől nem kell megijednie annak sem, aki esetleg ilyet még nem használt. A cikk végére belátható lesz, hogy ennek elkészítése sem ütközik különösebb nehézségekbe.

Hozzuk tehát létre a szükséges projectet. Ehhez válasszuk a File - New menüpontot. Majd a megjelenő ablakból az ActiveX lapon az ActiveX Library elemet. Ekkor létrejön a DLL kerete.

Most adjunk hozzá egy COM objektumot. Szintén File - New, majd az ActiveX lapon a COM Objects elem kiválasztása után megjelenik a COM objektum varázsló. Itt tudunk nevet adni az új objektumunknak a Class Name mezőben. A mellékelt példában a ShellMenu szöveget írtuk ide.

A példány és szálkezelés módját hagyjuk az alapértelmezett értékeken. A negyedik mezőben adhatjuk meg, hogy milyen osztályokból, vagyis interfészekből származzon az új objektumunk. A mellékelt példához az IShellExtInit, IContextMenu interfészeket kell használnunk, így ezeket vesszővel elválasztva felsoroljuk. Delphi 5, vagy későbbi változat esetén az Options-nál az Include Type Library-t kapcsoljuk ki.

Az utolsó, Description mezőbe egy egyedi leírást adhatunk meg, hogy mit is tud majdan e programunk. Ez persze elhagyható, nem kötelező.

Ezek után az Ok gomb lenyomásával létrejön az alábbi forráskód.
unit Unit1;
interface

uses Windows, ActiveX, Classes, ComObj;

type
  TShellMenu = class(TComObject, IShellExtInit, IContextMenu)
  protected
    {Declare IShellExtInit methods here}
    {Declare IContextMenu methods here}
  end;

const Class_ShellMenu: TGUID = 
           '{51E5E6D3-929B-418C-A72A-E079B182048C}';

implementation
uses ComServ;

initialization
TComObjectFactory.Create(ComServer, TShellMenu,
 Class_ShellMenu, 'ShellMenu', '', ciMultiInstance, tmApartment);
end.
Mint látható a ShellMenu néven megadott osztály automatikusan megkapta a T előtagot. Az osztály az általunk megadott két interfészből valamint a TComObject-ből származik. A helyes működés érdekében meg kell adnunk az IShellExtInit, IContextMenu interfészek metódusait:
  TShellMenu = class(TComObject, IShellExtInit, IContextMenu)
  protected
    {Declare IShellExtInit methods here}
    function IShellExtInit.Initialize=ShellInit;
    function ShellInit(pidlFolder: PItemIDList; lpdobj: 
       IDataObject; hKeyProgID: HKEY): HResult; stdcall;

    {Declare IContextMenu methods here}
    function QueryContextMenu(Menu: HMENU; indexMenu, 
         idCmdFirst, idCmdLast, uFlags: UINT): HResult; stdcall;
    function InvokeCommand(var lpici: TCMInvokeCommandInfo): 
         HResult; stdcall;
    function GetCommandString(idCmd, uType: UINT;
        pwReserved: PUINT; pszName: LPSTR; cchMax: UINT): 
        HResult; stdcall;
  end;
Mivel az IShellExtInit interfész tartalmaz egy Initialize metódust, valamint a TComObject osztály is, így az IShellExtInit Initialize metódusának új nevet kell adnunk, ami most ShellInit lesz.
   
function IShellExtInit.Initialize=ShellInit;
function ShellInit(pidlFolder: PItemIDList; lpdobj: 
   IDataObject; hKeyProgID: HKEY): HResult; stdcall;
Miután deklaráltuk az interfészek metódusait, most már csak a megfelelő kódot kell hozzárendelnünk.

Amint az a két interfész nevéből már sejthető, az IShellExtInit egyetlen Initialize nevű függvénye a Shell kiterjesztés inicializálásához kell. A másik IContextMenu interfész pedig a jobb gomb lenyomásakor megjelenő menürendszer kezelésére szolgál.

Nézzük az IShellExtInit Initialize nevű függvényét, melyet átneveztünk ShellInit-re.

Amikor ez a függvény meghívásra kerül, akkor tudjuk meghatározni, hogy a Windows Intézőjében mely állományok lettek kijelölve. Mellékelt példában csak egyetlen kijelölt állomány kezelését oldottuk meg. Ez persze kiterjeszthető könnyedén tetszőleges számú állomány kezelésére is. Ennek az egy állománynak a nevét a FFileName globális változóba tároljuk el. Ezt a lekérdezést az alábbi forráskóddal valósítjuk meg.
function TShellMenu.ShellInit(pidlFolder: PItemIDList; 
      lpdobj: IDataObject; hKeyProgID: HKEY): HResult;
var
  sm: TStgMedium;
  fe: TFormatEtc;
  count: integer;
  a: array[0..MAX_PATH-1] of char;
begin
  result:=E_FAIL;
  if lpdobj<>nil then begin
    with fe do begin
      cfFormat:=CF_HDROP;
      ptd:=nil;
      dwAspect:=DVASPECT_CONTENT;
      lindex:=-1;
      tymed:=TYMED_HGLOBAL;
    end;
    if lpdobj.GetData(fe, sm)=S_OK then begin
      try
        count:=DragQueryFile(sm.hGlobal, $FFFFFFFF, nil, 0);
        if count=1 then begin
          DragQueryFile(sm.hGlobal, 0, a, MAX_PATH);
          FFileName:=a;
          result:=NOERROR;
        end;
      finally
        ReleaseStgMedium(sm);
      end;
    end;
  end;
end;
A count:=DragQueryFile(sm.hGlobal, $FFFFFFFF, nil, 0); sor a count változóba kérdezi le, hogy hány kijelölt állományról van szó.

Ezek után a DragQueryFile(sm.hGlobal, 0, a, MAX_PATH); hívásával kérdezhetjük le, hogy mi az állomány neve teljes elérési útvonallal. Itt a második paraméterben adhatjuk meg, hogy hányadik kijelölt állomány nevet szeretnénk lekérdezni. Ha tehát az összes névre kíváncsiak vagyunk, akkor írhatunk egy ciklust, mely nullától az előbbi count-1 értékig mehet. A ciklusváltozót a második paraméterben megadva a ciklus végéig az összes nevet megkapjuk.

Mivel a mi példánkban csak egy állomány nevet kezelünk, így nincs szükségünk ciklusra.

Tehát amikor a felhasználó jobb gombbal kattint egy állományon, akkor kerül meghívásra a DLL-ünk ShellInit függvénye, ahol meghatározható, hogy melyik is ez az állomány.

Nézzük most, hogy miként adhatunk hozzá menüpontot a megjelenő menürendszerhez.

Az IContextMenu interfész QueryContextMenu függvénye kerül meghívásra elsőként. Itt kell rendelkeznünk arról, hogy létrehozunk-e menüpontokat vagy sem. Mellékelt példánkban egy új menüpontot szúrunk be a menürendszerbe méghozzá úgy, hogy a miénk legyen az első.
function TShellMenu.QueryContextMenu(Menu: HMENU;
      indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult;
begin
  InsertMenu(Menu, indexMenu, MF_STRING+
      MF_BYPOSITION, idCmdFirst, PChar('1. '+FFileName));
  result:=1;
end;
Ehhez az InsertMenu Windows függvényt használjuk. Első paraméterként a menü azonosítóját kell megadnunk. Ezt szerencsére a QueryContextMenu függvény első paraméterében megkapjuk, így csak tovább kell adnunk. A második paraméterben határozhatjuk meg az új menüpont helyét. Ezután azt határozzuk meg a konstansokkal, hogy sztring típusú menüpontot hozunk létre az általunk megadott helyen. Negyedik paraméterként a menühöz rendelt azonosító számot kell megadnunk. A későbbiekben e szám alapján azonosíthatjuk majd, hogy melyik menüpontunk lett kiválasztva. Az itt megadott érték a paraméterként kapott idCmdFirst, idCmdLast érték között kell hogy legyen. Végső paraméterként a menüpontunk szövegét kell megadnunk. Ez itt az 1. sztring és az állomány neve összevonva lesz.
A QueryContextMenu függvény visszatérési értékének azt a számot kell adnunk, ahány menüpontot felvettünk a menürendszerbe. Ez jelen esetben egy.


Amikor a felhasználó megjelenítette az Intézőben a jobb gombbal ezt a menürendszert és az egérrel az egyes menüpontok felett mozog, akkor az Intéző alsó státusz sorába egy tetszőleges súgó szöveget jeleníthetünk meg. Amikor a mi menüpontunk fölé áll a felhasználó az egérrel, akkor kerül meghívásra a GetCommandString függvény.
function TShellMenu.GetCommandString(idCmd, uType: UINT; 
    pwReserved: PUINT; pszName: LPSTR; cchMax: UINT): HResult;
begin
  result:=NOERROR;
  if (idCmd=0) and (uType=GCS_HELPTEXT) then begin
    StrCopy(pszName, 'Animare Software példaprogram 
      © 2000 (http://www.animare.hu)');
  end;
end;
Itt az idCmd paraméterben kapjuk meg, hogy mi a menüpontunk azonosító kódja (hiszen több menüpont is lehet a sajátunk) és a pszName változóba kell másolnunk a súgó szövegünket, mely maximum cchMax karaktert tartalmazhat.


A végső kérdés pedig az lesz, hogy mi történik akkor, ha a felhasználó nem csak nézegeti, hanem ki is választja a menüpontunkat. Nos ekkor kerül meghívásra az InvokeCommand.
function TShellMenu.InvokeCommand( 
     var lpici: TCMInvokeCommandInfo): HResult;
var
  i: integer;
begin
  result:=E_FAIL;
  if HiWord(integer(lpici.lpVerb))=0 then begin
    i:=LOWORD(lpici.lpVerb);
    if i=0 then begin
      ShowMessage(TimeToStr(Now));
      result:=NOERROR;
    end;
  end;
end;
A mellékelt példa egyszerűségének kedvéért itt most csak megjelenítünk egy kis üzenet ablakot benne az aktuális időponttal. Ha több menüpontunk lenne, akkor a programunkat az i változó értékétől függően ágaztathatjuk szét.


Ezzel a programunk el is készült, a DLL-t lefordíthatjuk. De honnan fogja tudni a Windows Intézője, hogy mikor kell meghívni a DLL-ünket és az hol található? Ehhez van szükségünk arra, hogy a Windows regisztrációs állományába elhelyezzük a szükséges bejegyzéseket.

Ezek a bejegyzések a ShellMenuExt.reg állományban láthatók. Ezt az állományt egyszerűen hozzáadhatjuk a regisztrációs adatbázishoz, ahogyan ezt a cikk elején le is írtuk. Természetesen lehetőség van arra, hogy ezeket a bejegyzéseket programból is beírhassuk a regisztrációs adatbázisba. Ehhez használhatjuk a TRegistry osztályt.

Nézzük milyen értékekre van szükség.

Általában egy ilyen lehetőséget csak egy adott állomány típus esetén használunk. Mellékelt példában ez az állomány SEC kiterjesztéssel rendelkezik. Vagyis a DLL-ünk csak akkor kap szerepet, ha a felhasználó SEC kiterjesztésű állományon kattint jobb gombbal.

A regisztrációs adatbázis HKEY_CLASSES_ROOT főkulcsa alá létre kell hoznunk egy .SEC kulcsot. Itt az alapértelmezett értéknek a SECFile sztringet adjuk. A későbbiekben ezzel a sztringgel hivatkozunk majd a bejegyzésre.
[HKEY_CLASSES_ROOT\.sec]
   @="SECFile"
Szintén a HKEY_CLASSES_ROOT alá be kell jegyeznünk az imént megadott sztringet a SECFile-t. Itt az alapértelmezett értéknek egy tetszőleges sztringet adhatunk, mely leírja, hogy mi ez az állomány.
[HKEY_CLASSES_ROOT\SECFile]
   @="DSO Sample file" 
Ehhez a kulcshoz kell bejegyeznünk egy olyan alkulcsot, melynek neve Shellex\ContextMenuHandlers. Ebből fogja tudni a Windows, hogy a SEC állományhoz tartozik Shell kiegészítés, mégpedig egy menükezelő alkalmazás. Az alapértelmezett értéknek itt is egy tetszőleges sztringet adhatunk, melyet most SECMenu-re választottunk.
[HKEY_CLASSES_ROOT\SECFile\Shellex\ContextMenuHandlers]
   @="SECMenu"
Minden COM objektum rendelkezik egyedi azonosítóval, melyet GUID-nak neveznek. Amikor létrehoztuk a programban a COM objektumunkat, akkor ezt a számot a Delphi automatikusan generálta és egy konstans formájában elhelyezte a forráskódunkban is.
const
  Class_ShellMenu: TGUID =
     '{8D67BEE0-F82A-4B7D-B430-2D3472331A45}';
Ezt az azonosító számot is meg kell adnunk a regisztrációs adatbázisban a következő kulcs alá:
[HKEY_CLASSES_ROOT\SECFile\shellex\ContextMenuHandlers\SECMenu]
   @="{8D67BEE0-F82A-4B7D-B430-2D3472331A45}"
Amint az látható is az új kulcs neve egyezik az előbb szabadon megválasztott sztringünkkel, a SECMenu-vel. Itt alapértelmezett értékként a forráskódban lévő GUID értékét kell megadnunk. Legegyszerűbb, ha ezt átmásoljuk onnan.

A továbbiakban meg kell adnunk, hogy ehhez a GUID-hoz melyik alkalmazás tartozik. Ehhez a HKEY_CLASSES_ROOT főkulcs alá a CLSID kulcshoz be kell jegyeznünk a mi alkalmazásunkat is. A CLSID alatt található az összes COM objektum, mely regisztrált az adott számítógépen. Itt hozzunk létre egy új kulcsot, melynek neve egyezik a GUID értékével. Alapértelmezett értékként egy egyedi sztringet adhatunk, mely most a ShellMenuExt Sample szöveget kapta.
[HKEY_CLASSES_ROOT\CLSID\
        {8D67BEE0-F82A-4B7D-B430-2D3472331A45}]
   @="ShellMenuExt Sample"
E kulcs alá kell létrehoznunk egy InProcServer32 kulcsot, ahol megadható a DLL neve, elérési útvonala.
[HKEY_CLASSES_ROOT\CLSID\
      {8D67BEE0-F82A-4B7D-B430-2D3472331A45}\InProcServer32]
   @="D:\\Dso\\0245\\ShellSecret01\\ShellMenuExt.dll"
   "ThreadingModel"="Apartment"
Amint az látható, a DLL-t elérési útvonallal az alapértelmezett értékhez kell megadni. Ha .REG állományt használunk, akkor itt a \ jelet duplán kell használnunk. Ha például Delphi-s programból végeznénk a regisztrációt, akkor a TRegistry használatával egy egyszerű sztringként írhatjuk ide az értéket. Ekkor nem kell duplázni e karaktert.
Továbbá meg kell adnunk még a ThreadingModel értéket is, mely a szálkezelés módját írja le, ami Apartment kell hogy legyen.


Végül már csak egyetlen bejegyzés maradt: a HKEY_LOCAL_MACHINE főkulcs alá a Microsoft\Windows\CurrentVersion\Shell Extensions\Approved kulcsra be kell jegyeznünk alkalmazásunkat. Itt létre kell hoznunk egy értéket, mely egyezik a GUID értékével és ennek azt a sztringet kell értékül adnunk, mely egyezik az imént még szabadon választott sztringünkkel.
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\
        Windows\CurrentVersion\Shell Extensions\Approved]
   "{8D67BEE0-F82A-4B7D-B430-2D3472331A45}"=
       "ShellMenuExt Sample"
A regisztrációs adatbázis változtatása után az alkalmazásunkat már megtalálja a Windows, így ha egy .SEC kiterjesztésű állományra kattintunk, akkor az elkészített ShellMenuExt.dll kerül meghívásra.

WinShell cikksorozat