检查 windows 资源管理器是否已在给定路径上打开

Check if windows explorer already opened on given path

如何查看 windows 资源管理器是否已使用特定路径打开?我不希望我的应用程序打开许多重复的 windows。我无法用这种方式做到这一点:

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;

IShellWindows::FindWindowSW 方法

有一个很好的方法 FindWindowSW 可以找到现有的 Shell window,其中还包括 Windows Explorer windows,我会说。所以,希望我能轻松找到现有的 window 我写了这段代码:

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;

但它从未找到具有给定文件夹路径的 Windows 资源管理器 window(它总是 returns 0)。我通过包含 SWFO_NEEDDISPATCH 选项来使用 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(方法总是返回 nil 引用)。所以我放弃了这个方法(没有找到任何例子)。

IShellWindows枚举

以下代码的灵感来自this article and this example。这是一个方案:

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

和一些描述:

  1. 首先获取 IShellWindows 接口引用并迭代其项。

  2. 对于每个项目,IShellWindows接口returnswindow的IDispatch 接口,然后查询 IWebBrowserApp 接口参考。

  3. 获得的IWebBrowserApp接口(文档参考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接口(这是获取给定服务接口的访问器)。

  4. 现在从最顶层的浏览器实现服务查询它的IShellBrowser接口。对这个接口的引用对于我们的目标来说仍然没有意义。

  5. 获得的IShellBrowser查询显示的Shell视图对象。

  6. 现在我们终于可以说,如果迭代 Shell window 不是 Internet Explorer window。到目前为止,他们已经实现了通用接口。现在如果我们查询得到的IShellView for the IFolderView界面成功,说明不是Internet Explorer,我们可以继续。

  7. 查询获取的IFolderView reference for the IPersistFolder2接口为当前显示的文件夹对象

  8. 如果我们成功了并且我们得到了当前文件夹对象的 IPersistFolder2 reference, let's get the ITEMIDLIST

  9. 如果我们在最后一步成功了,我们就有 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 等于我们为输入路径解析的那个。如果是,则将 window 置于前台,如果不是,则继续下一次迭代。

这里是 Delphi 代码。我不知道你的 Delphi 版本需要多少钱;这是 D2009 所需的最低限度(从 Windows SDK 10.0.15063.0 手动翻译)。这不是最好的例子;在实际代码中,您可能更喜欢将其包装到 class 中并具有更灵活的界面,但这取决于您的设计偏好。最后,如果你有 Delphi 比 2009 年更新的,你可能不需要导入的原型,如果旧的,你可能会遗漏一些:

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 = [=12=]400000;
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}

可能的用法:

BrowseInExplorer('C:\MyFolder');