вторник, 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 всегда вычисляются слева направо. Это не всегда верно для других операторов.

7 комментариев:

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

    ОтветитьУдалить
  2. Вот спасибо за отличный пример :)

    ОтветитьУдалить
  3. Анонимный18 июня 2009 г., 21:41

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

    ОтветитьУдалить
  4. Анонимный26 июня 2009 г., 15:47

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

    ОтветитьУдалить
  5. 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

    ОтветитьУдалить
  6. 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.

    ОтветитьУдалить
  7. try
    try
    try
    ...
    ...
    ...
    finally
    finally
    finally

    Это будет даже чуть пострашнее моего парсера по циклу лабораторных работ "Создание транслятора паскалеподобного ЯП" )))

    ОтветитьУдалить

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

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

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

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

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