Иногда разработчики программ начинают изобретать велосипед. Но часто достаточно просто сложить вместе несколько кусков головоломки. Сегодняшний пост - это один из последних случаев.
Если вам дан описатель (дескриптор) окна (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 комментарий(ев):
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.
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.