Система Orphus

вторник, 2 июня 2009 г.

Запрашиваем информацию из окна Проводника

Это перевод Querying information from an Explorer window. Автор: Реймонд Чен.

Иногда разработчики программ начинают изобретать велосипед. Но часто достаточно просто сложить вместе несколько кусков головоломки. Сегодняшний пост - это один из последних случаев.

Если вам дан описатель (дескриптор) окна (window handle), то вы можете определить: (1) является ли окно окном Проводника (Explorer), и если да, то (2) какую папку оно показывает и (3) какой элемент в ней выделен.

Это вовсе не сверх-сложная задача. Вы просто должны сложить вместе кучу маленьких кусочков информации.

Начнём с объекта ShellWindows, который представляет все открытые окна оболочки (shell). Вы можете пройтись по ним (enumerate) с помощью свойства Item. Это выглядит несколько неуклюже в native-языках, т.к. объект ShellWindows был спроектирован для использования в скриптовых языках типа JScript или Visual Basic. Хорошо ещё, что Delphi нам чуть-чуть помогает с авто-приведением типов.

var
  psw: IShellWindows;
  pdisp: IDispatch;
begin
  if SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, nil, CLSCTX_ALL, ID_IShellWindows, psw)) then
  try
    for X := 0 to psw.Count - 1 do
    begin
      pdisp := psw.Item(X);
      try
        ...
      finally
        pdisp := nil;
      end;
    end;  
  finally
    psw := nil;
  end;
end;
Для каждого элемента мы запрашиваем его описатель окна и сравниваем с данным нам: наше ли это окно?
var
  pwba: IWebBrowserApp;
...
  if SUCCEEDED(pdisp.QueryInterface(IID_IWebBrowserApp, pwba)) then
  try
    if pwba.get_HWND(hwndWBA)= hwndFind then
    begin
      fFound := True;
      ...
    end;
  finally
    pwba := nil;
  end;
Окей, теперь, когда мы нашли папку через интерфейс IWebBrowserApp, нам нужно получить браузер оболочки верхнего уровня (top shell browser). Это делается запросом к службе SID_STopLevelBrowser для интерфейса IShellBrowser.
var
  psp: IServiceProvider;
  psb: IShellBrowser;
...
  if SUCCEEDED(pwba.QueryInterface(IID_IServiceProvider, psp)) then
  try
    if SUCCEEDED(psp.QueryService(SID_STopLevelBrowser, IID_IShellBrowser, psb)) then
    try
      ...
    finally
      psb := nil;
    end;
  finally
    psp := nil;
  end;
Теперь, из IShellBrowser, мы можем запросить текущий вид Shell (shell view) методом QueryActiveShellView.
var
  psv: IShellView;
...
  if SUCCEEDED(psb.QueryActiveShellView(&psv)) then
  try
    ...
  finally
    psv := nil;
  end;
Конечно же, нам на самом деле нужен интерфейс IFolderView, в котором и содержатся все вкусности.
var
  pfv: IFolderView;
...
  if SUCCEEDED(psv.QueryInterface(IID_IFolderView, pfv)) then
  try
    ...
  finally
    pfv := nil;
  end;
Окей, теперь у нас всё есть. Что мы хотим получить от вида (view)? Как насчёт папки, которую мы просматриваем? Для этого нам надо использовать IPersistFolder2.GetCurFolder. Метод GetFolder даст нам доступ к папке оболочки (shell folder), из которого мы запрашиваем IPersistFolder2 (хотя чаще всего вам нужен будет интерфейс IShellFolder).
var
  ppf2: IPersistFolder2;
  pidlFolder: PITEMIDLIST;
...
  if SUCCEEDED(pfv.GetFolder(IID_IPersistFolder2, ppf2)) then
  try
    if SUCCEEDED(ppf2.GetCurFolder(pidlFolder)) then
    try
      ...
    finally
      CoTaskMemFree(pidlFolder);
    end;
  finally
    ppf2 := nil;
  end;
Далее, давайте сконвертируем этот pidl в путь - для отображения на форме.
if not SHGetPathFromIDList(pidlFolder, g_szPath) then
  StrCopy(g_szPath, '<Не каталог>');
Что бы ещё сделать с тем, что у нас уже есть? Ах, да - давайте посмотрим, что же у нас выбранно в папке.
var
  iFocus: Integer;
...
  if SUCCEEDED(pfv.GetFocusedItem(iFocus)) then
  begin
    ...
  end;
Давайте покажем имя сфокусированного элемента. Для этого нам нужен его pidl и экземпляр IShellFolder (видите, я же вам говорил, что в IShellFolder лежат всякие полезняшки). Элемент (item) получается из метода Item (удивил, да?).
var
  pidlItem: PITEMIDLIST;
...
  if SUCCEEDED(pfv.Item(iFocus, pidlItem)) then
  try
    ...
  finally
    CoTaskMemFree(pidlItem);
  end;
(если бы мы хотели получить все выделенные элементы - мы бы использовали метод Items, передавая туда SVGIO_SELECTION).

После того, как у нас есть pidl элемента, нам также нужен IShellFolder:
var
  psf: IShellFolder;
...
  if SUCCEEDED(ppf2.QueryInterface(IID_IShellFolder, psf)) then
  try
    ...
  finally
    psf := nil;
  end;
Потом мы используем их обоих для получения отображаемого имени элемента с помощью метода GetDisplayNameOf.
var
  Str: TStrRet;
...
  if SUCCEEDED(psf.GetDisplayNameOf(pidlItem, SHGDN_INFOLDER, Str)) then
  begin
    ...
  end;
Мы можем использовать вспомогательную функцию StrRetToBuf для перевода хитрой структуры TStrRet в обычный строковый буфер (история этой структуры подождёт до другого раза).
StrRetToBuf(@Str, pidlItem, g_szItem, MAX_PATH);
Окей, теперь давайте соединим это всё вместе. Результат будет выглядеть не очень, т.к. я просто свалю всё в одну кучу, вместо того, чтобы разбить на подфункции. В реальной жизни я бы разбил всё по вспомогательным функциям, которые могли бы сделать код более управляемым. Создаёте пустое VCL приложение и добавьте в него такой метод:
type
  TForm1 = class(TForm)
  ...
  private
    { Private declarations }
    g_szPath: array[0..MAX_PATH] of Char;
    g_szItem: array[0..MAX_PATH] of Char;
    procedure RecalcText;
  ...
  end;

...

uses
  ShLwAPI, ShDocVw, ShlObj, ActiveX, JwaShlObj, JwaShlDisp;

...

procedure TForm1.RecalcText;
var
  hwndFind: HWND;
  psw: IShellWindows;
  pdisp: IDispatch;
  X: Integer;
  pwba: IWebBrowserApp;
  psp: IServiceProvider;
  psb: IShellBrowser;
  psv: IShellView;
  pfv: IFolderView;
  ppf2: IPersistFolder2;
  pidlFolder: PItemIDList;
  iFocus: Integer;
  pidlItem: PItemIDList;
  psf: IShellFolder;
  Str: TStrRet;
begin
  hwndFind := GetForegroundWindow;
  g_szPath[0] := #0;
  g_szItem[0] := #0;

  if SUCCEEDED(CoCreateInstance(CLASS_ShellWindows, nil, CLSCTX_ALL, IID_IShellWindows, psw)) then
  try
    for X := 0 to psw.Count - 1 do
    begin
      pdisp := psw.Item(X);
      try
        if SUCCEEDED(pdisp.QueryInterface(IID_IWebBrowserApp, pwba)) then
        try
          if pwba.get_HWND = hwndFind then
          begin
            if SUCCEEDED(pwba.QueryInterface(IServiceProvider, psp)) then
            try
              if SUCCEEDED(psp.QueryService(SID_STopLevelBrowser, IID_IShellBrowser, psb)) then
              try
                if SUCCEEDED(psb.QueryActiveShellView(psv)) then
                try
                  if SUCCEEDED(psv.QueryInterface(IID_IFolderView, pfv)) then
                  try
                    if SUCCEEDED(pfv.GetFolder(IPersistFolder2, ppf2)) then
                    try
                      if SUCCEEDED(ppf2.GetCurFolder(pidlFolder)) then
                      try
                        if not SHGetPathFromIDList(pidlFolder, g_szPath) then
                          StrCopy(g_szPath, '<Не каталог>');
                        if SUCCEEDED(pfv.GetFocusedItem(iFocus)) then
                        begin
                          if SUCCEEDED(pfv.Item(iFocus, pidlItem)) then
                          try
                            if SUCCEEDED(ppf2.QueryInterface(IID_IShellFolder, psf)) then
                            try
                              if SUCCEEDED(psf.GetDisplayNameOf(pidlItem, SHGDN_INFOLDER, Str)) then
                                StrRetToBuf(@Str, Pointer(pidlItem), g_szItem, MAX_PATH);
                            finally
                              psf := nil;
                            end;
                          finally
                            CoTaskMemFree(pidlItem);
                          end;
                        end;
                      finally
                        CoTaskMemFree(pidlFolder);
                      end;
                    finally
                      ppf2 := nil;
                    end;
                  finally
                    pfv := nil;
                  end;
                finally
                  psv := nil;
                end;
              finally
                psb := nil;
              end;
            finally
              psp := nil;
            end;
            Break;
          end;
        finally
          pwba := nil;
        end;
      finally
        pdisp := nil;
      end;
    end;
  finally
    psw := nil;
  end;
end;
Теперь, всё что нам надо сделать - это вызывать периодически эту функцию и выводить её результаты (*).
procedure TForm1.Timer1Timer(Sender: TObject);
begin
  RecalcText;
  Label1.Caption := 'Path: ' + g_szPath;
  Label2.Caption := 'Item: ' + g_szItem;
end;
Теперь мы готовы. Запустите программу и пусть она висит сбоку (на втором мониторе - для богатых ребят). Потом запустите Проводник (Explorer) и наблюдайте, как ваша программа будет менять свой вывод в зависимости от текущего окна и сфокусированного в нём элемента. Попробуйте открыть диск C: или Панель управления.

Окей, я надеюсь, что вы меня поняли: часто, все нужные вам кусочки уже есть; вам просто нужно сообразить, как сложить их вместе. Заметьте, что в нашем случае каждый кусочек весьма мал. Вам нужно просто увидеть, что вы можете сложить их вместе для получения интересного результата.

Упражнение: изменить эту программу так, чтобы она получала на вход окно и переключала его в подробный вид ("таблица").

Примечания переводчика:
(*) На самом деле этот код - перевод исходного кода на C++ один-к-одному. С учётом того, что интерфейсы на Delphi финализируются автоматически, мы можем переписать нашу основную функцию гораздо короче:
procedure TForm1.RecalcText;

  function SupportsEx(const Instance: IUnknown; const Intf: TGUID; out Inst): Boolean;
  begin
    Result := (Instance <> nil) and (Succeeded(Instance.QueryInterface(Intf, Inst))) and (Pointer(Inst) <> nil);
  end;

var
  hwndFind: HWND;
  psw: IShellWindows;
  pdisp: IDispatch;
  X: Integer;
  pwba: IWebBrowserApp;
  psp: IServiceProvider;
  psb: IShellBrowser;
  psv: IShellView;
  pfv: IFolderView;
  ppf2: IPersistFolder2;
  pidlFolder: PItemIDList;
  iFocus: Integer;
  pidlItem: PItemIDList;
  psf: IShellFolder;
  Str: TStrRet;
begin
  hwndFind := GetForegroundWindow;
  g_szPath[0] := #0;
  g_szItem[0] := #0;

  if SUCCEEDED(CoCreateInstance(CLASS_ShellWindows, nil, CLSCTX_ALL, IID_IShellWindows, psw)) then
    for X := 0 to psw.Count - 1 do
    begin
      pdisp := psw.Item(X);
      if SupportsEx(pdisp, IID_IWebBrowserApp, pwba) and
         (pwba.get_HWND = hwndFind) and
         SupportsEx(pwba, IServiceProvider, psp) and
         SUCCEEDED(psp.QueryService(SID_STopLevelBrowser, IID_IShellBrowser, psb)) and
         SUCCEEDED(psb.QueryActiveShellView(psv)) and
         SupportsEx(psv, IID_IFolderView, pfv) and
         SUCCEEDED(pfv.GetFolder(IPersistFolder2, ppf2)) and
         SUCCEEDED(ppf2.GetCurFolder(pidlFolder)) then
      try
        if not SHGetPathFromIDList(pidlFolder, g_szPath) then
          StrCopy(g_szPath, '<Не каталог>');
        if SUCCEEDED(pfv.GetFocusedItem(iFocus)) and
           SUCCEEDED(pfv.Item(iFocus, pidlItem)) then
        try
          if SupportsEx(ppf2, IID_IShellFolder, psf) and
             SUCCEEDED(psf.GetDisplayNameOf(pidlItem, SHGDN_INFOLDER, Str)) then
            StrRetToBuf(@Str, Pointer(pidlItem), g_szItem, MAX_PATH);
        finally
          CoTaskMemFree(pidlItem);
        end;
      finally
        CoTaskMemFree(pidlFolder);
      end;
    end;
end;
Заметьте, что мы пользуемся тем, что выражения в операторе and всегда вычисляются слева направо. Это не всегда верно для других операторов.

6 комментарий(ев):

GunSmoker комментирует...

P.S. Для тех, у кого проблемы с поиском заголовочников - качать тут.
Вам нужна JEDI Windows API - последняя версия от 2008-09-15.

Yams комментирует...

Вот спасибо за отличный пример :)

Анонимный комментирует...

Пример действительно очень интересный.
Вот только я так и не смог найти ShLwAPI, пришлось заменить аналогичным из джедайского набора. Ну и вызов StrRetToBuf соответственно подкоректировал.
Torbins.

Анонимный комментирует...

Можно инициализировать переменную psw так:
uses ShDocVw;
psw := CoShellWindows.Create;

Анонимный комментирует...

Can we minimize the code to get only the path and filename. Using a Jedi is a big source.
Can you post another code that will get only the path and filename anywhere in windows.

I can't understand

Thank you
rocarob

GunSmoker комментирует...

This blog contains translations only. It's not a place to solve your problems.

I think that you need to ask that question on any forum for programmers.
For example.


Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.