[Delphi] Menu kontekstowe pliku - problem z 64bit

Problem dość nietypowy, mam stworzoną bibliotekę DLL która realizuje w mojej aplikacji menu kontekstowe dla każdego pliku. Od razu zaznaczę iż nie jest to zrobione na zasadzie dopisania się do obsługi typu pliku w rejestrze pod *, tylko tak jak robi to większość porządnych aplikacji - dll + [HKEY_CLASSES_ROOT*\shellex\ContextMenuHandlers]. Kod który to realizuje udało mi się znaleźć dopiero na jakiejś niemieckiej stronie. Przystosowałem go oczywiście do swojego przypadku (nie ma to jak edycja kodu z niemieckimi komentarzami).

Używałem tego przez spory kawałek czasu, jednak problem pojawił się niedawno, właściwie to został odkryty. Nie wiedzieć czemu menu kontekstowe pliku nie dodaje się na systemach 64-bitowych (Vista, 7 RC). Rejestrowanie biblioteki realizowane jest przez instalator InnoSetup. Doszedłem już do wniosku iż sam fakt iż aplikacji jest 32-bitowa nie jest problemem, gdyż wiele aplikacji stricte 32-bitowych mających również menu kontekstowe pliku działa bez problemu (np: Spik, KIS, Unlocker). Sama główna aplikacja działa bez problemu, jedynie menu kontekstowe się nie dodaje, tak jakby w ogóle nie była wywoływana komenda regsvr32 (zarówno gdy robi to instalator jak i gdy ręcznie próbuję zarejestrować DLL’a).

Poniżej załączam kod biblioteki DLL:

unit ShellExtMenu;


interface


// -----------------------------------------------------------------------------

uses

  Windows, SysUtils, Classes, ComServ, ShellAPI, Registry, ActiveX, ComObj,

  ShlObj, Graphics, Dialogs,

  CTZPTypes;


// -----------------------------------------------------------------------------

type

  TShellExtMenu = class(TComObject, IShellExtInit, IContextMenu)

  protected

    // Metody

    function IShellExtInit.Initialize = SEInitialize;

    function SEInitialize(pidlFolder: PItemIDList; lpdobj: IDataObject; hKeyProgID: HKEY): HResult; stdcall;

    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;


  TShellExtFactory = class(TComObjectFactory)

  public

    // Metody

    procedure UpdateRegistry(Register: Boolean); override;

  end;


implementation


// -----------------------------------------------------------------------------

var

  ShellExtFactory: TShellExtFactory;

  S: String;

  GUID: TGUID;

  InstallLocation: String;

  Icon: TBitmap;

  FileName: String;


// -----------------------------------------------------------------------------

function TShellExtMenu.SEInitialize(pidlFolder: PItemIDList; lpdobj: IDataObject; hKeyProgID: HKEY): HResult;

var

  StgMedium: TStgMedium;

  FormatEtc: TFormatEtc;

  fn: array[0..MAX_PATH] of Char;

begin

  if (lpdobj = nil) then

  begin

    Result := E_INVALIDARG;

    Exit;

  end;


  with FormatEtc do

  begin

    cfFormat := CF_HDROP;

    ptd := nil;

    dwAspect := DVASPECT_CONTENT;

    lindex := -1;

    tymed := TYMED_HGLOBAL;

  end;


  Result := lpdobj.GetData(FormatEtc, StgMedium);

  if Failed(Result) then

    Exit;


  if DragQueryFile(StgMedium.hGlobal, $FFFFFFFF, nil, 0) = 1 then

    begin

    DragQueryFile(StgMedium.hGlobal, 0, fn, SizeOf(fn));

    FileName := fn;

    end

  else

    FileName := '';


  ReleaseStgMedium(StgMedium);

  Result := NOERROR;

end;


// -----------------------------------------------------------------------------

function TShellExtMenu.QueryContextMenu(Menu: HMENU; indexMenu,

  idCmdFirst, idCmdLast, uflags: UINT): HResult;

var

  info: TMenuItemInfo;

begin


if (FileName <> '') and

   (

     ((uFlags and $0000000F) = CMF_NORMAL) or

     ((uFlags and CMF_EXPLORE) <> 0) or

     ((uFlags and CMF_VERBSONLY) <> 0)

   ) then

  begin


  info.cbSize := SizeOf(TMenuItemInfo);

  info.fMask := MIIM_STRING or MIIM_ID;

  info.wID := idCmdFirst;

  info.dwTypeData := PAnsiChar(SHELLEXT_CAPTION);


  InsertMenuItem(Menu, indexMenu, True, info);


  if Icon.Handle <> 0 then

    SetMenuItemBitmaps(Menu, indexMenu, MF_BYPOSITION, Icon.Handle, Icon.Handle);


  Result := 4


  end

else

  Result := 0;


end;


// -----------------------------------------------------------------------------

function TShellExtMenu.InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult;

begin


if HiWord(Integer(lpici.lpVerb)) = 0 then

  begin


  if LoWord(lpici.lpVerb) = 0 then // Numer komendy

    begin


    // Uruchomienie aplikacji

    ShellExecute(

      0,

      '',

      PAnsiChar(InstallLocation + MAIN_EXE_FILENAME),

      PAnsiChar('"' + FileName + '"'),

      PAnsiChar(InstallLocation),

      SW_SHOWDEFAULT

      );


    Result := NOERROR;


    end

  else

    Result := E_INVALIDARG;


  end

else

  Result := E_FAIL;


end;


// -----------------------------------------------------------------------------

function TShellExtMenu.GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;

  pszName: LPSTR; cchMax: UINT): HResult;

begin


//Result := S_OK;


try


  if idCmd = 0 then

    begin


    if uType = GCS_HELPTEXT then

      StrCopy(pszName, SHELLEXT_CAPTION);


    Result := NOERROR;


    end

  else

    Result := E_INVALIDARG;


except

  Result := E_UNEXPECTED;

end;


end;




// -----------------------------------------------------------------------------

procedure TShellExtFactory.UpdateRegistry(Register: Boolean);

var

  classid: string;

begin


if Register then

  begin


  inherited UpdateRegistry(Register);


  classid := GUIDToString(GUID);


  CreateRegKey('*\shellex', '', '');

  CreateRegKey('*\shellex\ContextMenuHandlers', '', '');

  CreateRegKey('*\shellex\ContextMenuHandlers\' + SHELLEXT_NAME, '', classid);


  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, SHELLEXT_NAME);

      finally

        Free;

      end;


  end

else

  begin


  DeleteRegKey('*\shellex\ContextMenuHandlers\' + SHELLEXT_NAME);

  DeleteRegKey('*\shellex\ContextMenuHandlers');

  DeleteRegKey('*\shellex');


  inherited UpdateRegistry(Register);


  end;


end;




// -----------------------------------------------------------------------------

initialization


  // GUID

  S := GetRegStringValue('*\shellex\ContextMenuHandlers\' + SHELLEXT_NAME, '');

  if S = '' then

    CoCreateGUID(GUID)

  else

    GUID := StringToGUID(S);


  // Sciezka aplikacji

  InstallLocation := ParsePath(GetRegStringValue('Software\' + REG_NAME, 'InstallLocation', HKEY_CURRENT_USER));


  // ???

  ShellExtFactory := TShellExtFactory.Create(

    ComServer, TShellExtMenu, GUID,

    '', SHELLEXT_NAME,

    ciMultiInstance, tmApartment

    );


  // Ikona

  Icon := TBitmap.Create;

  Icon.LoadFromResourceName(hInstance, 'ICON');


// -----------------------------------------------------------------------------

finalization


  // Zwalnianie ikony

  Icon.Free;


  // Zwalnianie ???

  ShellExtFactory.Free;


end.

Gdzie składujesz swoją DLLkę? WOW64 powoduje, że widok na niektóre standardowe katalogi jest inny niż z poziomu natywnej, 64-bitowej aplikacji. Także część rejestru jest przekierowywana (i to jest pewnie Twoim problemem).

http://msdn.microsoft.com/en-us/library/aa384253(VS.85).aspx

Składuję w folderze z aplikacją. Z tego co pamiętam to wpis do rejestru się dodawał poprawnie.

To, że gałąź rejestru istnieje nie znaczy, że działa tak jak oczekujesz. Na 64-bitowym systemie część gałęzi ma 32-bitowe odpowiedniki. Jeśli chcesz, żeby z 64-bitowym shellem wchodziła w interakcję 32-bitowa DLLka, musisz ją zarejestrować w innym miejscu w rejestrze. Przeczytaj podany link.

Ok, ok, dzięki za trop, już się za to biorę.

– Dodane 13.09.2009 (N) 12:59 –

Nie czaje tego, tzn. rozumiem że rejestr ma jakby hardlinki porobione. Sprawdzałem to teraz, moje wpisy są tam gdzie powinny, obok innych aplikacji z menu, z tą różnicą że mojego nie widać a ich tak :.

Rejestrujesz swoją aplikację pod jakimś GUIDem i zakładasz uchwyt na pliki “*”, kierując akcję do tego GUIDu (DLLki), tak? Na wskazanej stronie klucz SOFTWARE\Classes\CLSID (w którym zapewne rejestruje się obecnie Twoja DLLka) oznaczony jest jako Redirected. Z tego co rozumiem oznacza to, że Twój wpis musi się znajdować w SOFTWARE\Classes\Wow6432Node\CLSID.

Po instalacji, na jaki CLSID wskazuje Twój ContextMenuHandler? Gdzie w rejestrze jest to CLSID zdefiniowane? (Classes\CLSID czy Clsses\Wow6432Node\CLSID czy nie możesz go znaleźć w ogóle)

Swoją drogą wolę się upewnić: czy GUID któy stosujesz w CLSID jest jednakowy na każdym systemie, czy generujesz go w programie? Nie czytałem całości Twojego kodu, ale nie widzę żadnego GUIDa na twardo a jest niepokojący CoCreateGUID. Twoja aplikacja powinna zawsze używać tego samego GUIDu, w przeciwnym razie diagnozowanie problemów stanie się piekłem.

Tak swoją drogą - dlaczego nie zbudujesz 64-bitowej wersji swojej DLLki? Nie wiem co Twój handler ma robić, ale komunikacja między procesami o różnej “bitowości” nie jest najszybsza i trochę szkoda, żeby Explorer się krztusił na 64-bitowych systemach.

Tak, jest, sprawdziłem, zgadza się.

Jest w Classes\Wow6432Node\CLSID. Nie jestem w stanie wsadzić go do Classes\CLSID (bo tak jest w kodzie), gdyż to mnie przekierowuje do Wiw6432Node i tak (chyba że DLL był by 64bitowy).

Tutaj się mi przypomina początek pracy z tym kodem, właśnie pierwotnie był tam sztywny GUID wpisany. To ja zmieniłem na tworzenie dynamiczne. Tzn. zrobiłem to z jednego powodu, co jeśli założony GUID już by istniał?

Cała idea GUIDów jest taka, że statystycznie nie powinny się powtarzać (pod warunkiem, że są poprawnie generowane). Stwórz sobie jeden przy pomocy CoCreateGUID i używaj go w tej aplikacji. GUID generowany w ten sposób jest hashem czasu, MAC karty sieciowej i szumu z systemu. Hash jest mocny, więc szansa kolizji jest praktycznie zerowa (nie spotkałem się z kolizją GUIDów, które wyszły z CoCreateGUID, mimo iż szereg komponentów Windowsa na których się opierałem jak i mój komponent z nich korzystały).

Co do Twojego problemu - pogrzebię jeszcze. Pisanie pod WOW64 jest obwarowane masą ograniczeń - między innymi dlatego nasz komponent nie wspierał w ogóle mieszanego środowiska. Gdyby to nie był Pascal, poleciłbym Ci podpięcie kernel debugera i ustawienie leniwego breakpointa w Twojej DLLce. Możliwe, że Shell woła Twoją DLLkę, ale coś nie rynksztosuje. Niestety ntsd nie działa z Delphi. :confused:

Co do tej kolizji, wystarczy że 2 programistów skorzystało z pierwotnej wersji tego kodu gdzie był sztywny GUID i już jest ;). Ale, ok, rozumiem o co chodzi, poprawie to tak jak mówisz.

Tak jeszcze dla pewności sprawdziłem Spik’a, mają ten sam problem, też nie działa shell menu na 64bitach. Zaczynam się obawiać czy faktycznie nie trzeba będzie 64bitowego DLL’a :\ Moja aplikacja opiera się na tym menu kontekstowym, bez niego jej używanie mija się z celem (tak jakbyś musiał odpalić inny program i wybrać w nim plik do usunięcia a nie bezpośrednio z menu pliku).

Ani losowy, ani sztywny GUID Cię nie zabezpieczy przed głupotą kogoś innego. Zasada jest taka, że jeśli identyfikujesz coś w systemie, to masz mieć jeden GUID między maszynami. Jeśli dwie wersje tej samej aplikacji mogą istnieć SxS to musisz zmienić GUID co wydanie, w przeciwnym wypadku nie powinieneś (choć w uzasadnionych przypadkach jest ti dopuszczalne).

Dodane niedziela, 14:51

Wygrzebałem. Shell - podobnie jak mój komponent - zrezygnował ze wsparcia dla mieszanych binarek. Na 64-bitowym systemie shell extension musi być 64-bitowe. Aż do RC Windowsa Visty możliwe było odpalenie 32-bitowego explorera w 64-bitowym systemie (%windir%\syswow64\explorer.exe /separate) i w nim rozszerzenia działały. Obecnie jednak nie jest to już możliwe.

Ok. Dzięki.

To teraz pytanie numer dwa, jest dostępny 64-bitowy kompilator Object Pascala :wink: ? Czytałem już wiele razy że ma w końcu wyjść ale czy udało im się go wydać już?

Jest 64-bitowy FPC. O innych implementacjach Pascala nie wiem nic.

Ściągłem sobie Lazarusa który korzysta z FPC, ale powiem szczerze że mnie to przerasta. Chociażby już 1 problem, że nie ma w Lazarusie czegoś takiego jak IShellExtInit…

Nie wiem czy nie zrealizować tego na 64-bitach jako zwykłe podpięcie się pod wszystkie typy plików. Nie będzie ikony wprawdzie, ale innej drogi chyba nie widzę :frowning:

Jeszcze jedna sprawa, czy takie coś jest dobre do GUID’ów? -> http://www.guidgenerator.com/online-guid-generator.aspx

Czy lepiej użyć jednak funkcji CoCreateGUID()?