Search code examples
windowsdelphishortcut

SHChangeNotify not updating URL= change in my .url shortcut file


I have a simple Delphi application that creates a desktop shortcut for a URL. It makes a two-line text file with a .url filename extension in the user's Desktop folder:

[InternetShortcut]
URL=http://127.0.0.1/admin

That works fine. When I need to update the file with a new URL, I overwrite the old file. But Windows will not recognize the change until I restart Explorer or reboot. So I learned about SHChangeNotify() and called it after overwriting the file:

SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATH or SHCNF_FLUSH, PChar(Path), nil);

But it has no effect:

  • I tried with and without the SHCNF_FLUSH flag;
  • also the SHCNF_FLUSHNOWAIT flag makes no difference.
  • I also tried deleting the file first and then using the SHCNE_DELETE event and then re-creating the file. That doesn't work either, it just keeps using the old URL.

How do I force Explorer to reload the URL from the file without a restart?


Solution

  • While the file's content can be treated like any INI file I yet have not found a direct way to control manipulations to it:

    • When creating a file its content is read as expected: the system's default application for the URL='s protocol is started (i.e. for http it is most likely the internet browser).
    • Modifying the file per file systems has no effect - either MSIE itself maintains a cache or the COM's magic.

    Indirectly manipulation is possible in the following way:

    1. Empty the file's existing content. Why? Because the later step will just add the same INI section with an URL= value again, but the first section's URL= value remains the one that is taken into account.
    2. Access the file per COM and change its properties. Sadly this writes more into the file - in my case the outcome/file's content was:
      [{000214A0-0000-0000-C000-000000000046}]
      Prop3=19,2
      [InternetShortcut]
      URL=http://127.0.0.1/index.php
      IDList=
      

    However, it "works" as in: the change (speak: a different URL) is recognized. Putting it all together my following code for Delphi 7 on Windows 7 should also work for you - just call the function:

    uses
      ShlObj, ActiveX, ComObj;
    
    const
      SID_IUniformResourceLocatorA= '{FBF23B80-E3F0-101B-8488-00AA003E56F8}';
      SID_IUniformResourceLocatorW= '{CABB0DA0-DA57-11CF-9974-0020AFD79762}';
      SID_InternetShortcut= '{FBF23B40-E3F0-101B-8488-00AA003E56F8}';
    
    type
      PUrlInvokeCommandInfoA= ^TUrlInvokeCommandInfoA;
      TUrlInvokeCommandInfoA= record
        dwcbSize,
        dwFlags: DWORD;  // Bit field of IURL_INVOKECOMMAND_FLAGS
        hwndParent: HWND;  // Parent window. Valid only if IURL_INVOKECOMMAND_FL_ALLOW_UI is set.
        pcszVerb: LPCSTR;  // Verb to invoke. Ignored if IURL_INVOKECOMMAND_FL_USE_DEFAULT_VERB is set.
      end;
    
      PUrlInvokeCommandInfoW= ^TUrlInvokeCommandInfoW;
      TUrlInvokeCommandInfoW= record
        dwcbSize,
        dwFlags: DWORD;
        hwndParent: HWND;
        pcszVerb: LPCWSTR;
      end;
    
      IUniformResourceLocatorA= interface( IUnknown )
        [SID_IUniformResourceLocatorA]
        function SetURL( pcszURL: LPCSTR; dwInFlags: DWORD ): HRESULT; stdcall;
        function GetURL( ppszURL: LPSTR ): HRESULT; stdcall;
        function InvokeCommand( purlici: PUrlInvokeCommandInfoA ): HRESULT; stdcall;
    
      end;
    
      IUniformResourceLocatorW= interface( IUnknown )
        [SID_IUniformResourceLocatorW]
        function SetURL( pcszURL: LPCWSTR; dwInFlags: DWORD ): HRESULT; stdcall;
        function GetURL( ppszURL: LPWSTR ): HRESULT; stdcall;
        function InvokeCommand(purlici: PUrlInvokeCommandInfoW ): HRESULT; stdcall;
      end;
    
    function SetURL( sFile, sUrl: Widestring ): Integer;
    const
      CLSID_InternetShortCut: TGUID= SID_InternetShortcut;
    var
      oUrl: IUniformResourceLocatorW;
      oFile: IPersistFile;
      hFile: THandle;
    begin
      // First, the existing file's content should be emptied
      hFile:= CreateFileW( PWideChar(sFile), GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0 );
      if hFile= INVALID_HANDLE_VALUE then begin
        result:= 1;  // File might not exist, sharing violation, etc.
        exit;
      end;
    
      // Initial file pointer is at position 0
      if not SetEndOfFile( hFile ) then begin
        result:= 2;  // Missing permissions, etc.
        CloseHandle( hFile );
        exit;
      end;
    
      // Gracefully end accessing the file
      if not CloseHandle( hFile ) then begin
        result:= 3;  // File system crashed, etc.
        exit;
      end;
    
      // Using COM to access properties
      result:= 0;
      try
        oUrl:= CreateComObject( CLSID_InternetShortCut ) as IUniformResourceLocatorW;
      except
        result:= 4;  // CLSID unsupported, COM not available, etc.
      end;
      if result<> 0 then exit;
    
      // Opening the file again
      oFile:= oUrl as IPersistFile;
      if oFile.Load( PWideChar(sFile), STGM_READWRITE )<> S_OK then begin
        result:= 5;  // Sharing violations, access permissions, etc.
        exit;
      end;
    
      // Set the property as per interface - only saving the file is not enough
      if oUrl.SetURL( PWideChar(sUrl), 0 )<> S_OK then begin
        result:= 6;
        exit;
      end;
    
      // Storing the file's new content - setting only the property is not enough
      if oFile.Save( PWideChar(sFile), TRUE )<> S_OK then begin
        result:= 7;
        exit;
      end;
    
      // Success!
      result:= 0;
    end;
    

    As per my desktop firewall the executing process modifies the memory of explorer.exe upon IPersistFile.Save() - after that executing the URL file should reflect its new content, while any attempt before that should still act upon the old file's content.