Иногда разработчики программ начинают изобретать велосипед. Но часто достаточно просто сложить вместе несколько кусков головоломки. Сегодняшний пост - это один из последних случаев.
Если вам дан описатель (дескриптор) окна (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 всегда вычисляются слева направо. Это не всегда верно для других операторов.
P.S. Для тех, у кого проблемы с поиском заголовочников - качать тут.
ОтветитьУдалитьВам нужна JEDI Windows API - последняя версия от 2008-09-15.
Вот спасибо за отличный пример :)
ОтветитьУдалитьПример действительно очень интересный.
ОтветитьУдалитьВот только я так и не смог найти 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
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.
try
ОтветитьУдалитьtry
try
...
...
...
finally
finally
finally
Это будет даже чуть пострашнее моего парсера по циклу лабораторных работ "Создание транслятора паскалеподобного ЯП" )))