Иногда разработчики программ начинают изобретать велосипед. Но часто достаточно просто сложить вместе несколько кусков головоломки. Сегодняшний пост - это один из последних случаев.
Если вам дан описатель (дескриптор) окна (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
Это будет даже чуть пострашнее моего парсера по циклу лабораторных работ "Создание транслятора паскалеподобного ЯП" )))