Jump to content
FPiette

How to register a shell property sheet for a single file type?

Recommended Posts

21 minutes ago, FPiette said:

Bingo! it works.

That's great!

However, as far as I can tell, there's no documentation to explain this behavior. A bit unfortunate, to say the least. Or maybe I'm just not understanding how association arrays work.

 

I have already updated my code to support registration under HKCU\Software\Classes instead of HKCR. I will update it to also add the SystemFileAssociation keys.

  • Like 2

Share this post


Link to post
21 hours ago, Anders Melander said:

Or maybe I'm just not understanding how association arrays work.

They work even easier than I thought: When registering a property sheet, it is enough to register it in the SystemFileAssociations. No need to register the class for the file extension nor the file class.

 

To register a property sheet for .foobar files, under de key HKLM\SOFTWARE\Classes\SystemFileAssociations\.foobar\ShellEx\PropertySheetHandlers (May also have to be create) create an entry with the name registered for the COM object implementing the shell extension and set default value to the CLSID.

This is an important feature for me to avoid interfering with an existing file type already having a program defined.

Tested the behavior with both Win10 and Win11.

 

Share this post


Link to post
On 3/12/2023 at 2:28 PM, FPiette said:

They work even easier than I thought: When registering a property sheet, it is enough to register it in the SystemFileAssociations. No need to register the class for the file extension nor the file class.

  

To register a property sheet for .foobar files, under de key HKLM\SOFTWARE\Classes\SystemFileAssociations\.foobar\ShellEx\PropertySheetHandlers (May also have to be create) create an entry with the name registered for the COM object implementing the shell extension and set default value to the CLSID.

This is an important feature for me to avoid interfering with an existing file type already having a program defined.

Tested the behavior with both Win10 and Win11.

@Anders Melander I had the opportunity to test on Win7. The behavior is the same as on Win 10 and 11: it is enough to register it in the SystemFileAssociations. No need to register the class for the file extension nor the file class.

Share this post


Link to post
5 hours ago, FPiette said:

I had the opportunity to test on Win7. The behavior is the same as on Win 10 and 11: it is enough to register it in the SystemFileAssociations. No need to register the class for the file extension nor the file class.

Yes, that's my conclusion too. I'm on Win7, 10 and 11 here.

 

This is the relevant code I use for registration:

 

TPropertySheetHandlerFactory class

const
  // CLSID for this shell extension.
  // Modify this for your own shell extensions (press [Ctrl]+[Shift]+G in
  // the IDE editor to gererate a new CLSID).
  CLSID_PropertySheetHandler: TGUID = '{1067C264-8B1F-4B22-919F-DB5191C359CB}';
  sFileClass = 'pasfile';
  sFileExtension = '.pas';
  sClassName = 'DelphiPropSheetShellExt';

resourcestring
  // Description of our shell extension.
  sDescription = 'Drag and Drop Component Suite property sheet demo';

...

////////////////////////////////////////////////////////////////////////////////

type
  TPropertySheetHandlerFactory = class(TShellExtFactory)
  protected
    function HandlerRegSubKey: string; override;
  end;

function TPropertySheetHandlerFactory.HandlerRegSubKey: string;
begin
  Result := 'PropertySheetHandlers';
end;

////////////////////////////////////////////////////////////////////////////////

initialization
  TPropertySheetHandlerFactory.Create(ComServer, TDataModulePropertySheetHandler,
    CLSID_PropertySheetHandler, sClassName, sDescription, sFileClass,
    sFileExtension, ciMultiInstance);
end.

TShellExtFactory class

////////////////////////////////////////////////////////////////////////////////
//
//              TShellExtFactory
//
////////////////////////////////////////////////////////////////////////////////
// Class factory for component based COM classes.
// Specialized for Shell Extensions.
////////////////////////////////////////////////////////////////////////////////
type
  TShellExtFactory = class(TVCLComObjectFactory)
  private
    FFileExtension: string;
    FFileClass: string;
  protected
    function GetProgID: string; override;
    function HandlerRegSubKey: string; virtual; abstract;
    function UseSystemFileAssociations: boolean; virtual;
    function OwnsFileExtension: boolean; virtual;
  public
    constructor Create(ComServer: TComServerObject; ComponentClass: TComponentClass;
      const ClassID: TGUID; const ClassName, Description, AFileClass,
      AFileExtension: string; Instancing: TClassInstancing);

    procedure UpdateRegistry(ARegister: Boolean); override;

    property FileClass: string read FFileClass write FFileClass;
    property FileExtension: string read FFileExtension write FFileExtension;
  end;

.....

////////////////////////////////////////////////////////////////////////////////
//
//              TShellExtFactory
//
////////////////////////////////////////////////////////////////////////////////
constructor TShellExtFactory.Create(ComServer: TComServerObject;
  ComponentClass: TComponentClass; const ClassID: TGUID; const ClassName,
  Description, AFileClass, AFileExtension: string; Instancing: TClassInstancing);
begin
  inherited Create(ComServer, ComponentClass, ClassID, ClassName,
    Description, Instancing);
  FFileClass := AFileClass;
  FFileExtension := AFileExtension;
end;

function TShellExtFactory.GetProgID: string;
begin
  Result := '';
end;

function TShellExtFactory.OwnsFileExtension: boolean;
begin
  // Return True if it's safe to delete the file association upon unregistration.
  // Be careful that we don't delete file associations used by other applications
  Result := False;
end;

procedure TShellExtFactory.UpdateRegistry(ARegister: Boolean);
var
  RegPrefix: string;
  RootKey: HKEY;
  ClassIDStr: string;
  Registry: TRegistry;
begin
  ComServer.GetRegRootAndPrefix(RootKey, RegPrefix);

  ClassIDStr := GUIDToString(ClassID);

  if ARegister then
  begin
    inherited UpdateRegistry(ARegister);

    if UseSystemFileAssociations and (FileExtension <> '') then
    begin
      CreateRegKey(RegPrefix+'SystemFileAssociations\'+FileExtension+'\shellex\'+HandlerRegSubKey+'\'+ClassName, '', ClassIDStr, RootKey);
    end else
    if (FileClass <> '') then
    begin
      if (FileExtension <> '') and (GetRegStringValue(RegPrefix+FileExtension, '', RootKey) = '') then
        CreateRegKey(RegPrefix+FileExtension, '', FileClass, RootKey);
      CreateRegKey(RegPrefix+FileClass+'\shellex\'+HandlerRegSubKey+'\'+ClassName, '', ClassIDStr, RootKey);
    end;

    if (Win32Platform = VER_PLATFORM_WIN32_NT) then
    begin
      Registry := TRegistry.Create;
      try
        if (ComServer.PerUserRegistration) then
          Registry.RootKey := HKEY_CURRENT_USER
        else
          Registry.RootKey := HKEY_LOCAL_MACHINE;

        if Registry.OpenKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved', False) then
          Registry.WriteString(ClassIDStr, Description);
      finally
        Registry.Free;
      end;
    end;
  end else
  begin
    if (Win32Platform = VER_PLATFORM_WIN32_NT) then
    begin
      Registry := TRegistry.Create;
      try
        if (ComServer.PerUserRegistration) then
          Registry.RootKey := HKEY_CURRENT_USER
        else
          Registry.RootKey := HKEY_LOCAL_MACHINE;

        if Registry.OpenKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved', False) then
          Registry.DeleteKey(ClassIDStr);
      finally
        Registry.Free;
      end;
    end;

    if UseSystemFileAssociations and (FileExtension <> '') then
    begin
      DeleteDefaultRegValue(RegPrefix+'SystemFileAssociations\'+FileExtension+'\shellex\'+HandlerRegSubKey+'\'+ClassName, RootKey);
      DeleteEmptyRegKey(RegPrefix+'SystemFileAssociations\'+FileExtension+'\shellex\'+HandlerRegSubKey+'\'+ClassName, True, RootKey);
    end else
    if (FileClass <> '') then
    begin
      if (FileExtension <> '') and (OwnsFileExtension) and (GetRegStringValue(RegPrefix+FileExtension, '', RootKey) = FileClass) then
        DeleteDefaultRegValue(RegPrefix + FileExtension, RootKey);
      DeleteDefaultRegValue(RegPrefix+FileClass+'\shellex\'+HandlerRegSubKey+'\'+ClassName, RootKey);
      DeleteEmptyRegKey(RegPrefix+FileClass+'\shellex\'+HandlerRegSubKey+'\'+ClassName, True, RootKey);
    end;

    inherited UpdateRegistry(ARegister);
  end;

  SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nil, nil);
end;

function TShellExtFactory.UseSystemFileAssociations: boolean;
begin
  Result := True;
end;

TVCLComObjectFactory class

////////////////////////////////////////////////////////////////////////////////
//
//              TVCLComObjectFactory
//
////////////////////////////////////////////////////////////////////////////////
// Class factory for component based COM classes.
// Does not require a type library.
// Based on TComponentFactory and TComObjectFactory.
////////////////////////////////////////////////////////////////////////////////
type
  TVCLComObjectFactory = class(TComObjectFactory, IClassFactory)
  private
  protected
    function CreateInstance(const UnkOuter: IUnknown; const IID: TGUID;
      out Obj): HResult; stdcall;
  public
    constructor Create(ComServer: TComServerObject; ComponentClass: TComponentClass;
      const ClassID: TGUID; const ClassName, Description: string;
      Instancing: TClassInstancing);
    function CreateComObject(const Controller: IUnknown): TComObject; override;
    procedure UpdateRegistry(Register: Boolean); override;
  end;

...

////////////////////////////////////////////////////////////////////////////////
//
//              TVCLComObjectFactory
//
////////////////////////////////////////////////////////////////////////////////
constructor TVCLComObjectFactory.Create(ComServer: TComServerObject;
  ComponentClass: TComponentClass; const ClassID: TGUID; const ClassName,
  Description: string; Instancing: TClassInstancing);
begin
  inherited Create(ComServer, TComClass(ComponentClass), ClassID, ClassName,
    Description, Instancing, tmApartment);
end;

function TVCLComObjectFactory.CreateComObject(const Controller: IUnknown): TComObject;
begin
  Result := TVCLComObject.CreateFromFactory(Self, Controller);
end;

function TVCLComObjectFactory.CreateInstance(const UnkOuter: IUnknown;
  const IID: TGUID; out Obj): HResult;
begin
  if not IsLibrary then
  begin
    LockServer(True);
    try
      with TApartmentThread.Create(Self, UnkOuter, IID) do
      begin
        if WaitForSingleObject(Semaphore, INFINITE) = WAIT_OBJECT_0 then
        begin
          Result := CreateResult;
          if Result <> S_OK then Exit;
          Result := CoGetInterfaceAndReleaseStream(IStream(ObjStream), IID, Obj);
        end else
          Result := E_FAIL
      end;
    finally
      LockServer(False);
    end;
  end else
    Result := inherited CreateInstance(UnkOuter, IID, Obj);
end;

type
  TComponentProtectedAccess = class(TComponent);
  TComponentProtectedAccessClass = class of TComponentProtectedAccess;

procedure TVCLComObjectFactory.UpdateRegistry(Register: Boolean);
begin
  if Register then
    inherited UpdateRegistry(Register);
  TComponentProtectedAccessClass(ComClass).UpdateRegistry(Register,
    GUIDToString(ClassID), ProgID);
  if not Register then
    inherited UpdateRegistry(Register);
end;

 

Share this post


Link to post
10 hours ago, FPiette said:

@Anders Melander I had the opportunity to test on Win7. The behavior is the same as on Win 10 and 11: it is enough to register it in the SystemFileAssociations. No need to register the class for the file extension nor the file class.

Just FYI, I have just now looked back at an old project of mine, which was first written back in the XP days before SystemFileAssociations was introduced.  It still registers a custom PropertySheetHandler for a specific file extension under HKCR\<FileExtProgID>\ShellEx\PropertySheetHandlers and HKLM\SOFTWARE\Windows\CurrentVersion\Shell Extensions\Approved, and it still works fine on Win7 and Win10 (I didn't test on Win11).  If SystemFileAssociations also works, good for it.

Share this post


Link to post
2 hours ago, Remy Lebeau said:

It still registers a custom PropertySheetHandler for a specific file extension under HKCR\<FileExtProgID>\ShellEx\PropertySheetHandlers and HKLM\SOFTWARE\Windows\CurrentVersion\Shell Extensions\Approved, and it still works fine on Win7 and Win10 (I didn't test on Win11).

Yes, that was the initial observation but if you reread the thread you will see that the original problem was that for some file types, it didn't work. Probably those that already have stuff registered under SystemFileAssociations. FWIW I think this is a bug in Windows.

Regardless, unless one has to support XP, SystemFileAssociations should be used since it's the only way to safely extend file types that you don't "own".

Share this post


Link to post
13 hours ago, Anders Melander said:

Regardless, unless one has to support XP, SystemFileAssociations should be used since it's the only way to safely extend file types that you don't "own".

That's what I do in my property sheet and so far (win11) I don't see any adverse effect.

Share this post


Link to post
15 hours ago, Remy Lebeau said:

Just FYI, I have just now looked back at an old project of mine, which was first written back in the XP days before SystemFileAssociations was introduced.  It still registers a custom PropertySheetHandler for a specific file extension under HKCR\<FileExtProgID>\ShellEx\PropertySheetHandlers and HKLM\SOFTWARE\Windows\CurrentVersion\Shell Extensions\Approved, and it still works fine on Win7 and Win10 (I didn't test on Win11).  If SystemFileAssociations also works, good for it.

My experience (Win11) shows that it DOESN'T WORK for some file extension, specifically the one that was my target (.arw).

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×