Введение
В этой статье будет описан метод реализации интерфейса
IShellExecuteHook с целью перехвата вызовов функции ShellExecuteEx
используемой оболочкой Windows. Это позволит написать простейшей
расширение оболочки Windows (подробнее о расширения оболочки и написании
расширений с помощью Visual C++ и библиотеки ATL смотрите статьи
Michael Dunn).
Создание COM компонента
Для того, чтобы создать COM компонент программисту не нужно прилагать
лишних усилий и писать какой бы то ни было дополнительный код,
достаточно в свойствах сборки (Build) проекта в Visual Studio .NET
установить значение Register for COM Interop в True.
Интерфейс IShellExecuteHook
Интерфейс 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#
Надеюсь, что Вы не успели заскучать рассматривая описание структур
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.