В этой статье будет описан метод реализации интерфейса IShellExecuteHook с целью перехвата вызовов функции ShellExecuteEx используемой оболочкой Windows. Это позволит написать простейшей расширение оболочки Windows (подробнее о расширения оболочки и написании расширений с помощью Visual C++ и библиотеки ATL смотрите статьи Michael Dunn).
Для того, чтобы создать COM компонент программисту не нужно прилагать лишних усилий и писать какой бы то ни было дополнительный код, достаточно в свойствах сборки (Build) проекта в Visual Studio .NET установить значение Register for COM Interop в True.
Интерфейс IShellExecuteHook описывает
функции ShellExecute и
ShellExecuteEx используемые оболочкой Windows.
Например, когда пользователь вводит команду используя пункт меню
Run (Выполнить) в меню
Start (Пуск), либо при двойном щелчке по файлу.
Таким образом, перехватив вызов функции мы можем получить имя файла,
директории, либо параметры командной строки и выполнить какое-либо действие,
либо заставить оболочку не выполнять никаких действий.
Интерфейс описан в заголовочном файле shlobj.h, входящим в
Platform SDK.
DECLARE_INTERFACE_(IShellExecuteHookW, IUnknown) // shexhk { // *** IUnknown methods *** STDMETHOD(QueryInterface) (THIS_ REFIID riid, void **ppv) PURE; STDMETHOD_(ULONG, AddRef) (THIS) PURE; STDMETHOD_(ULONG, Release) (THIS) PURE; // *** IShellExecuteHookW methods *** STDMETHOD(Execute)(THIS_ LPSHELLEXECUTEINFOW pei) PURE; };
Рассмотрим функцию Execute:
HRESULT Execute( LPSHELLEXECUTEINFO pei );
Функция возвращает S_OK, если
оболочка не должна передавать объект в дальнейшую обработку (это значение можно
использовать для того, чтобы информировать оболочку о том, что расширение
обработало объект, либо в случае, когда дальнейшая обработка нежелательна)
либо S_FALSE в противном случае.
Аргумент pei представляет собой
структуру SHELLEXECUTEINFO, содержащую информацию о
вызываемом обекте. Заглянув в заголовочный файл shellapi.h найдем описание
этой структуры:
typedef struct _SHELLEXECUTEINFO { DWORD cbSize; ULONG fMask; HWND hwnd; LPCTSTR lpVerb; LPCTSTR lpFile; LPCTSTR lpParameters; LPCTSTR lpDirectory; int nShow; HINSTANCE hInstApp; LPVOID lpIDList; LPCTSTR lpClass; HKEY hkeyClass; DWORD dwHotKey; union { HANDLE hIcon; HANDLE hMonitor; } DUMMYUNIONNAME; HANDLE hProcess; } SHELLEXECUTEINFO, *LPSHELLEXECUTEINFO;
Для полноты изложения приведу описание аргументов:
cbSize - размер структуры в байтах
fMask - комбинация флагов указывающая на содержание других членов структуры, описание флагов можно посмотреть в MSDN, для данной статьи оно не важно
hwnd - описатель окна любого сообщения, которое система может показать в результате обработки этого объекта
lpVerb - описывает действие, которое нужно выполнить, например: "open", "edit", "explore"
lpFile - указатель на имя файла или объекта
lpParameters - указатель на список параметров
lpDirectory - указатель на имя рабочей директории, если поле не заполнено, это означает, что текущая директория является рабочей
nShow - комбинация флагов указывающая как должно отобразиться окно приложения, обрабатывающего данный тип объектов (файлов), за списком желающие могут обратиться в MSDN
hInstApp - по окончании обработки, расширение может установить одно из допустимых значений, информирующее оболочку о результате обработки. Например, SE_ERR_FNF - файл не найден, SE_ERR_PNF- путь на найден и т.п.
Остальные параметры зависят от флагов, установленных в fMask. Опять же, интересующиеся могут посмотреть описание в MSDN. Данная статья лишь демонстрирует пример реализации интерфейса и не рассматривает все возможные варианты.
Надеюсь, что Вы не успели заскучать
рассматривая описание структур C++. Вы-то ждали
.NET. Вот и дождались. Сейчас мы "переведем"
приведенные выше описания C++ в описания структур и
методов .NET. Также, как это описано далее, Вы можете
реализовать любой интерфейс.
Для начала нам понадобится GUID
интерфейса, который мы хотим реализовать. За ним мы залезем в заголовочный файл
ShlGuid.h, входящий в Platform SDK
и найдем строку:
DEFINE_SHLGUID(IID_IShellExecuteHookW, 0x000214FBL, 0, 0);
В более привычном и знакомом виде: {000214FB-0000-0000-C000-000000000046}. Это значение нам понадобиться для импорта COM интерфейса в C#:
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214FB-0000-0000-C000-000000000046")] public interface IShellExecuteHook{ [PreserveSig()] int Execute(SHELLEXECUTEINFO pei); }
Поскольку IShellExecuteHook наследует IUnknown, мы указываем тип COM интерфейса InterfaceIsIUnknown. Аттрибут PreserveSig() указывается для того, чтобы возвращаемый параметр рассматривался именно как параметр стандартного типа HRESULT и не претерпевал каких-либо преобразований.
Затем опишем наш COM класс, для этого нам понадобиться уникальный GUID, который можно получить воспользовавшись стандартной утилитой, входящей в поставку Visual Studio .NET - Create GUID.
Получив GUID опишем класс, который будет доступен как COM:
[Guid("706CAD5F-0DD4-4477-B8DB-1FE4AF1C44B2"), ComVisible(true)] public class SimpleExt : IShellExecuteHook { }
Данный класс должен содержать только один метод Execute, в реализации которого есть только одна сложность - необходимо перевести структуру SHELLEXECUTEINFO из C++ в C# и привести неуправляемые типы к управляемым. Для того, чтобы члены структуры размещались в памяти в указанном порядке, необходимо указать атрибут [StructLayout(LayoutKind.Sequential)]. Приведем типы и опишем структуру:
[StructLayout(LayoutKind.Sequential)] public class SHELLEXECUTEINFO { public int cbSize; public int fMask; public int hwnd; [MarshalAs(UnmanagedType.LPWStr)] public string lpVerb; [MarshalAs(UnmanagedType.LPWStr)] public string lpFile; [MarshalAs(UnmanagedType.LPWStr)] public string lpParameters; [MarshalAs(UnmanagedType.LPWStr)] public string lpDirectory; public int nShow; public int hInstApp; public int lpIDList; [MarshalAs(UnmanagedType.LPWStr)] public string lpClass; public int hkeyClass; public int dwHotKey; public int hIcon; public int hProcess; }
Учтите, что для соответствия управляемого варианта структуры неуправляемому необходимо явно указать, что строки должны передаваться как неуправляемые, для чего используется атрибут MarshalAs(UnmanagedType.LPWStr).
Остается самая малость - определить типы возвращаемых значений, чтобы код реализации функции еще больше напоминал стандартный код COM класса. Типов возвращаемых значений в нашем случае всего два S_OK и S_FALSE, опишем их непосредственно в классе:
private int S_OK= 0; private int S_FALSE= 1;
Напишем код для функции Execute:
public int Execute(SHELLEXECUTEINFO pei) { try { switch(pei.lpFile) { case "msgbox": MessageBox.Show(pei.lpParameters); return S_OK; case "time": MessageBox.Show(DateTime.Now.ToString()); return S_OK; default: if(pei.lpFile.IndexOf (".txt") != -1) { MessageBox.Show( "You can not open text files! Acces to file: " + pei.lpFile + " denied!"); return S_OK; } } } catch(FormatException) { return S_FALSE; } catch(Exception ex) { MessageBox.Show(ex.Message); } return S_FALSE; }
Главное, при реализации данной функции, не забыть возвращать значение S_FALSE в случаях, когда обработка не нужна. В качестве небольшого эксперимента можете попробовать в начале функции возвращать S_OK. После этого Вы не сможете заставить оболочку запустить какое-либо приложение или какую-либо команду без удаления обработчика. Будьте осторожны!
Для того, чтобы установить наш COM компонент, перехватывающий функцию ShellExecuteEx, необходимо зарегистрировать сам компонент (как обычно), а также указать его GUID в ветке реестра, предназначенной для регистрации компонентов:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellExecuteHooks
Для этого создадим простейшую утилиту регистрации, которая будет регистрировать и удалять регистрационные данные из реестра. Вот два используемых метода, реализующих эту функциональность:
private void btnReg_Click(object sender, System.EventArgs e) { try { Assembly mAsm = Assembly.LoadFrom (txtPath.Text); RegistrationServices mReg = new RegistrationServices(); mReg.RegisterAssembly(mAsm, 0); RegistryKey key; key = Registry.LocalMachine.OpenSubKey( @"Software\Microsoft\Windows\CurrentVersion\Explorer\ShellExecuteHooks", true); key.SetValue(clsid, "SimpleExtension"); key.Close(); } catch(Exception ex) { MessageBox.Show (ex.Message); } } private void btnUnreg_Click(object sender, System.EventArgs e) { try { Assembly mAsm = Assembly.LoadFile (txtPath.Text); RegistrationServices mReg = new RegistrationServices(); mReg.UnregisterAssembly(mAsm); RegistryKey key; key = Registry.LocalMachine.OpenSubKey( @"Software\Microsoft\Windows\CurrentVersion\Explorer\ShellExecuteHooks", true); key.DeleteValue (clsid); key.Close(); } catch(Exception ex) { MessageBox.Show (ex.Message); } }
Для начала откройте исходный код прилагаемый к статье, затем соберите решение и используя утилиту регистрации зарегистрируйте наше простейшее расширения оболочки. Наслаждайтесь! Вот как выглядит утилита:
А вот так работает наше расширение:
Попробуйте просто ввести команду time до установки расширения и после установке расширения. Разница видна сама собой.
В данной статье был рассмотрен способ
перехвата функции ShellExecuteEx, а также попутно
рассказано о том, как перевести описание методов и структур из С++ в
C#, а также реализовать стандартный интерфейс в собственном
COM компоненте.
Если у Вас возникнут какие-то вопросы, Вы можете написать мне
письмо
gaidar@vbstreets.ru или задать вопрос в форуме
bbs.vbstreets.ru.