4

How can I see if windows explorer is already opened with certain path ? I don't want my application opens many duplicated windows. I was unable to do it with this way :

var
  H: hwnd;
begin
  if FileExists(edt8.Text) then
  begin
    H := FindWindow(0, PChar(ExtractFilePath(edt8.Text)));
    if H <> 0 then
      ShowMessage('explorer already opened')//explorer bring to front
    else
      ShellExecute(Application.Handle, nil, 'explorer.exe',
        PChar(ExtractFilePath(edt8.Text)), nil, SW_NORMAL);
  end;
end;
Bianca
  • 973
  • 2
  • 14
  • 33
  • 5
    Enumerate `IShellWindows`, search for `SWC_EXPLORER` item and if you find some, ask if its service provider `IShellView`'s `IFolderView`'s `IPersistFolder2.GetCurFolder` returns the folder of your interest. You'll be even able to get its window handle to bring the window to front if necessary. See e.g. [this article](https://blogs.msdn.microsoft.com/oldnewthing/20040720-00/?p=38393) for inspiration. – Victoria Aug 01 '17 at 11:26
  • Thankyou @Victoria, will find about it. Sounds new to me :) – Bianca Aug 01 '17 at 11:33
  • You're welcome! If you'll need help with a code example, I could try to craft some for you. Just let me know ;) – Victoria Aug 01 '17 at 12:01
  • After reading a while, yes, i need an example. The whole reading were very overkill for this task. Thank you. – Bianca Aug 01 '17 at 13:22
  • 3
    @Victoria that should be posted as an answer instead of a comment. – Remy Lebeau Aug 01 '17 at 16:43

1 Answers1

7

IShellWindows::FindWindowSW method

There is a nice method FindWindowSW that should find an existing Shell window, which includes Windows Explorer windows as well, I'd say. So, in a hope I'll find an existing window easily I wrote this code:

uses
  ActiveX, ShlObj, SHDocVw, ComObj;

function IDListFromPath(const Path: WideString): PItemIDList;
var
  Count: ULONG;
  Attributes: ULONG;
  ShellFolder: IShellFolder;
begin
  OleCheck(SHGetDesktopFolder(ShellFolder));
  OleCheck(ShellFolder.ParseDisplayName(0, nil, PWideChar(Path), Count, Result, Attributes));
end;

function GetExplorerHandle(const Path: WideString): HWND;
var
  IDList: PItemIDList;
  Unused: OleVariant;
  Location: OleVariant;
  ShellWindows: IShellWindows;
begin
  OleCheck(CoCreateInstance(CLASS_ShellWindows, nil, CLSCTX_LOCAL_SERVER, IID_IShellWindows, ShellWindows));
  Unused := Unassigned;
  IDList := IDListFromPath(Path);
  PVariantArg(@Location).vt := VT_VARIANT or VT_BYREF;
  PVariantArg(@Location).pvarVal := PVariant(IDList);
  ShellWindows.FindWindowSW(Location, Unused, SWC_EXPLORER, Integer(Result), SWFO_INCLUDEPENDING);
end;

But it never finds the Windows Explorer window with the given folder path (it always returns 0). I've used SWC_EXPLORER class to search only for Windows Explorer windows, build the absolute ID list, used a proper VT_VARIANT | VT_BYREF variant for location (at least I hope so, if not, please let me know). And I also tried to return IDispatch by including the SWFO_NEEDDISPATCH option (method always returned nil reference). So I gave up on this method (haven't found any example).

IShellWindows enumeration

The following code was inspired by this article and this example. Here is a scheme:

1. IShellWindows.Item(n)
2. ⤷ IDispatch.QueryInterface(IWebBrowserApp)
3.   ⤷ IWebBrowserApp.QueryInterface(IServiceProvider)
4.     ⤷ IServiceProvider.QueryService(STopLevelBrowser, IShellBrowser)
5.       ⤷ IShellBrowser.QueryActiveShellView
6.         ⤷ IShellView.QueryInterface(IFolderView)
7.           ⤷ IFolderView.GetFolder(IPersistFolder2)
8.             ⤷ IPersistFolder2.GetCurFolder
9.               ⤷ ITEMIDLIST

And some description:

  1. As first you obtain the IShellWindows interface reference and iterate its items.

  2. For each item, the IShellWindows interface returns window's IDispatch interface which you then query for an IWebBrowserApp interface reference.

  3. The obtained IWebBrowserApp interface (for documentation refer to IWebBrowser2, as it's their implementation) provides except others also the information about the host window, like handle which can be later used for bringing the window to foreground. We need to go deeper though. So let's query this interface reference for the IServiceProvider interface (which is an accessor for getting interfaces for the given service).

  4. Now from the top-most browser implementation service query its IShellBrowser interface. A reference of this interface is still not interesting for our aim.

  5. The obtained IShellBrowser query for the displayed Shell view object.

  6. Now we can finally say, if the iterated Shell window is not an Internet Explorer window. So far they were having common interfaces implemented. Now if we query the obtained IShellView for the IFolderView interface and it succeeds, it is not Internet Explorer and we can continue.

  7. Query the obtained IFolderView reference for the IPersistFolder2 interface for the currently displayed folder object.

  8. If we succeeded even there and we got IPersistFolder2 reference, let's get the ITEMIDLIST for the current folder object.

  9. And if we succeeded even with this last step, we have ITEMIDLIST of the currently displayed folder of a Windows Explorer instance (or the same interface implementor) and we can finally check if the obtained ITEMIDLIST equals to the one we parsed for the input path. If so, bring that window to foreground, if not, continue to the next iteration.

And here is a Delphi code. I don't know how much do you need for your Delphi version; this was a bare minimum I've needed for D2009 (manually translated from Windows SDK 10.0.15063.0). It's not a best example; in real code you may prefer wrapping this into a class and have more flexible interface, but that's upon your design preference. And finally, if you have Delphi newer than 2009, you may not need the imported prototypes, if older, you might be missing some:

uses
  ActiveX, ShlObj, SHDocVw, ComObj;

{ because of Win32Check }
{$WARN SYMBOL_PLATFORM OFF}
const
  IID_IFolderView: TGUID = '{CDE725B0-CCC9-4519-917E-325D72FAB4CE}';
  IID_IPersistFolder2: TGUID = '{1AC3D9F0-175C-11D1-95BE-00609797EA4F}';
  IID_IServiceProvider: TGUID = '{6D5140C1-7436-11CE-8034-00AA006009FA}';
  SID_STopLevelBrowser: TGUID = '{4C96BE40-915C-11CF-99D3-00AA004AE837}';

type
  IFolderView = interface(IUnknown)
  ['{CDE725B0-CCC9-4519-917E-325D72FAB4CE}']
    function GetCurrentViewMode(out pViewMode: UINT): HRESULT; stdcall;
    function SetCurrentViewMode(ViewMode: UINT): HRESULT; stdcall;
    function GetFolder(const riid: TIID; out ppv): HRESULT; stdcall;
    function Item(iItemIndex: Integer; out ppidl: PItemIDList): HRESULT; stdcall;
    function ItemCount(uFlags: UINT; out pcItems: Integer): HRESULT; stdcall;
    function Items(uFlags: UINT; const riid: TIID; out ppv): HRESULT; stdcall;
    function GetSelectionMarkedItem(out piItem: Integer): HRESULT; stdcall;
    function GetFocusedItem(out piItem: Integer): HRESULT; stdcall;
    function GetItemPosition(pidl: PItemIDList; out ppt: TPoint): HRESULT; stdcall;
    function GetSpacing(var ppt: TPoint): HRESULT; stdcall;
    function GetDefaultSpacing(out ppt: TPoint): HRESULT; stdcall;
    function GetAutoArrange: HRESULT; stdcall;
    function SelectItem(iItem: Integer; dwFlags: DWORD): HRESULT; stdcall;
    function SelectAndPositionItems(cidl: UINT; var apidl: PItemIDList; var apt: TPoint; dwFlags: DWORD): HRESULT; stdcall;
  end;

  EShObjectNotFolder = class(Exception);

function ILGetSize(pidl: PItemIDList): UINT; stdcall;
  external 'shell32.dll' name 'ILGetSize';
function ILIsEqual(pidl1: PItemIDList; pidl2: PItemIDList): BOOL; stdcall;
  external 'shell32.dll' name 'ILIsEqual';
function InitVariantFromBuffer(pv: Pointer; cb: UINT; out pvar: OleVariant): HRESULT; stdcall;
  external 'propsys.dll' name 'InitVariantFromBuffer';
function CoAllowSetForegroundWindow(pUnk: IUnknown; lpvReserved: Pointer): HRESULT; stdcall;
  external 'ole32.dll' name 'CoAllowSetForegroundWindow';

resourcestring
  rsObjectNotFolder = 'Object "%s" is not a folder.';

{ this parses the input folder path and creates ITEMIDLIST structure if the given
  folder path is a valid absolute path to an existing folder }
function GetFolderIDList(const Folder: string): PItemIDList;
const
  SFGAO_STREAM = $00400000;
var
  Count: ULONG;
  Attributes: ULONG;
  ShellFolder: IShellFolder;
begin
  OleCheck(SHGetDesktopFolder(ShellFolder));
  Attributes := SFGAO_FOLDER or SFGAO_STREAM;
  OleCheck(ShellFolder.ParseDisplayName(0, nil, PWideChar(WideString(Folder)), Count, Result, Attributes));
  if not ((Attributes and SFGAO_FOLDER = SFGAO_FOLDER) and (Attributes and SFGAO_STREAM <> SFGAO_STREAM)) then
  begin
    CoTaskMemFree(Result);
    raise EShObjectNotFolder.CreateFmt(rsObjectNotFolder, [Folder]);
  end;
end;

{ translated from the link mentioned in this comment; D2009 does not allow me to
  create an OleVariant of type VT_ARRAY|VT_UI1 which is needed for the Navigate2
  method so I've imported and used the InitVariantFromBuffer function here
  https://msdn.microsoft.com/en-us/library/windows/desktop/gg314982(v=vs.85).aspx }
procedure OpenNewExplorer(IDList: PItemIDList);
var
  Location: OleVariant;
  WebBrowser: IWebBrowser2;
begin
  OleCheck(CoCreateInstance(CLASS_ShellBrowserWindow, nil, CLSCTX_LOCAL_SERVER, IID_IWebBrowser2, WebBrowser));
  OleCheck(CoAllowSetForegroundWindow(WebBrowser, nil));
  OleCheck(InitVariantFromBuffer(IDList, ILGetSize(IDList), Location));
  try
    WebBrowser.Navigate2(Location, Unassigned, Unassigned, Unassigned, Unassigned);
  finally
    VariantClear(Location);
  end;
  WebBrowser.Visible := True;
end;

{ translated from the link mentioned in this comment
  https://blogs.msdn.microsoft.com/oldnewthing/20040720-00/?p=38393 }
procedure BrowseInExplorer(const Folder: string);
var
  I: Integer;
  WndIface: IDispatch;
  ShellView: IShellView;
  FolderView: IFolderView;
  SrcFolderID: PItemIDList;
  CurFolderID: PItemIDList;
  ShellBrowser: IShellBrowser;
  ShellWindows: IShellWindows;
  WebBrowserApp: IWebBrowserApp;
  PersistFolder: IPersistFolder2;
  ServiceProvider: IServiceProvider;
begin
  SrcFolderID := GetFolderIDList(Folder);
  try
    OleCheck(CoCreateInstance(CLASS_ShellWindows, nil, CLSCTX_LOCAL_SERVER, IID_IShellWindows, ShellWindows));
    { iterate all Shell windows }
    for I := 0 to ShellWindows.Count - 1 do
    begin
      WndIface := ShellWindows.Item(VarAsType(I, VT_I4));
      { do not use OleCheck here; windows like Internet Explorer do not implement
        all the interfaces; it is the way to distinguish Windows Explorer windows
        actually; so let's get all the references and if we succeed, check if the
        obtained folder equals to the passed one; if so, bring that window to top
        and exit this procedure }
      if Assigned(WndIface) and
        Succeeded(WndIface.QueryInterface(IID_IWebBrowserApp, WebBrowserApp)) and
        Succeeded(WebBrowserApp.QueryInterface(IID_IServiceProvider, ServiceProvider)) and
        Succeeded(ServiceProvider.QueryService(SID_STopLevelBrowser, IID_IShellBrowser, ShellBrowser)) and
        Succeeded(ShellBrowser.QueryActiveShellView(ShellView)) and
        Succeeded(ShellView.QueryInterface(IID_IFolderView, FolderView)) and
        Succeeded(FolderView.GetFolder(IID_IPersistFolder2, PersistFolder)) and
        Succeeded(PersistFolder.GetCurFolder(CurFolderID)) and
        ILIsEqual(SrcFolderID, CurFolderID) then
      begin
        { restore the window if minimized, try to bring it to front and exit this
          procedure }
        if IsIconic(WebBrowserApp.HWnd) then
          Win32Check(ShowWindow(WebBrowserApp.HWnd, SW_RESTORE));
        {$IFNDEF IBelieveThatIWebBrowserAppVisiblePropertyBringsWindowToFront}
        Win32Check(SetForegroundWindow(WebBrowserApp.HWnd));
        {$ELSE}
        OleCheck(CoAllowSetForegroundWindow(WebBrowserApp, nil));
        WebBrowserApp.Visible := True;
        {$ENDIF}
        Exit;
      end;
    end;
    { the procedure was not exited, hence an existing window was not found, so go
      and open the new one }
    OpenNewExplorer(SrcFolderID);
  finally
    CoTaskMemFree(SrcFolderID);
  end;
end;
{$WARN SYMBOL_PLATFORM ON}

Possible usage:

BrowseInExplorer('C:\MyFolder');
Victoria
  • 7,822
  • 2
  • 21
  • 44
  • You could pass `Folder` to `OpenNewExplorer` instead of the `PItemIDList` argument and avoid the `InitVariantFromBuffer` - `IWebBrowser2.Navigate2` accepts `BSTR` URL as argument. +1 nevertheless! BTW, not sure I entirely got that `IBelieveThatIWebBrowserAppVisiblePropertyBringsWindowToFront` directive... Why is it there and what is it good for? `CoAllowSetForegroundWindow` seems to work just fine. – kobik Aug 03 '17 at 23:15
  • @kobik, thanks again! That directive says it all :) The implementor of the `IWebBrowserApp` seems to be `IWebBrowser2` (not sure about it, it's used in that linked official example) and for that no one explicitly said that enabling `Visible` property will bring the host window to front as well (it does for me on Win 7). And `CoAllowSetForegroundWindow` is used most probably because the `Visible` property calls just `SetForegroundWindow` in its setter and needs privileges for that. That's from [this example](https://msdn.microsoft.com/en-us/library/windows/desktop/gg314982(v=vs.85).aspx). – Victoria Aug 04 '17 at 02:56
  • @kobik, and you're right of course about the URL (I was thinking about it), but parsing of that string will be more expensive than passing already parsed ID list (which costs one extra import and function call; maybe is newer Delphi ready to create such variant type, I can't say). But still, it's a pity that the `FindWindowSW` method doesn't work as I was expecting (it returns `S_OK`, but nothing else, returned window dispatch is `nil` and output handle 0). I was trying to pass there even ID list right from the iteration, but no change. When I crippled that variant, call failed. – Victoria Aug 04 '17 at 03:05
  • OMG, this is way too advance for me. The example code is working as expected. GREAT ! You are awesome @Victoria. And by the way, I just need to add `SysUtils` , `System.Variants`, and `Windows` on uses clause. I use XE. Thank you so much and have a good week end. – Bianca Aug 04 '17 at 04:39
  • 1
    Thank you, so to you! About the units I know. I've tried to show which ones need to be added to the ones that are automatically used when you create a form unit. And maybe there is a stuff that is duplicated. You may try to comment out defined prototypes and see if the project still compiles. I mean, EMBT could add e.g. `IID_IPersistFolder2` constant, `IFolderView` interface, `ILGetSize` function etc. into Delphi XE. – Victoria Aug 04 '17 at 04:46
  • 1
    I comment out the 3 items as your direction, it is still compiled, and working. Thanks again. I gained new lesson :) – Bianca Aug 04 '17 at 05:13