Показать сообщение отдельно

[СТАТЬЯ] Создание расширения для Windows Explorer при помощи C++ Builder
  #1  
Старый 28.03.2009, 15:35
Аватар для Dobby007
Dobby007
Познающий
Регистрация: 07.09.2008
Сообщений: 55
Провел на форуме:
163517

Репутация: 57
Отправить сообщение для Dobby007 с помощью ICQ
Lightbulb [СТАТЬЯ] Создание расширения для Windows Explorer при помощи C++ Builder

[ИНТРО]
Привет всем. Сегодня я расскажу вам об программном расширении оболочки Форточек при помощи C++ Builder’а. Сразу говорю, что это моя первая статья. Так что не обессудьте если что-то не совсем правильно будет написано.
А теперь немного истории. Решил я написать данную статью, так как сам просидел очень долгое время, собирая по кусочкам информацию, разбросанную по всем уголкам интернета. Может быть и сильно громко сказано, но это в действительности так и было. Итак, начнем-с, пожалуй…

[ЧТО НАМ ПОНАДОБИТСЯ]
Ну во-первых конечно же сама установленная среда C++ Builder. Во-вторых, прямые (желательно длинные) руки и здравомыслящая голова . А, в третьих, упорство и терпение. Вот пожалуй и весь набор начинающего программера…

[ЧЕГО МЫ ХОТИМ ДОБИТЬСЯ]
Именно в данной статье мы рассмотрим простой пример создания расширения для Эксплорера, дополняющий некоторые возможности к контекстному меню, появляющегося при клике на определенный файл, как это показано на рисунке:


[ПОДГОТОВКА]
Итак, запускаем С++ Builder. В-принципе не играет особой роли какой он версии (только будет некоторые различия в интерфейсе). Но я использую Codegear C++ Builder 2007.

Нажимаем File->New->Other->ActiveX->ActiveX Library. Мы увидим уже стандартный уже написанный за нас код, являющийся основой для любого ActiveX-контрола. Теперь снова заходим File->New->Other->ActiveX. Сейчас здесь уже появились дополнительные пункты. Но нам из них нужен только Com Object. Выбираем его из списка и видим следующее диалоговое окно:

Вводим в качестве CoClassName MyFirstContextMenu. Вы конечно можете другое имя, но, чтобы не было каких-либо дальнейших вопросов по написанию кода, рекомендую написать там все же именно это.
Теперь сохраняем проект в любую папку и идем дальше.

[Main.h (по другому не назовешь )]

Немного оглянувшись по файлам проекта, заходим Main.h и в самый нижний паблик добавляем следующий код:
Код:
public:
	STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
	STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
	STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
	STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);
Что же сие означает? Это обыкновенное объявление функций обязательных для создания расширения. Рассмотрим каждую из них по-подробней…
Initialize – инициализирует наше расширение. Из нее мы можем прочитать такие параметры имя файла, дополнительные флаги, переданные нам для дальнейшего их использования нами.
QueryContextMenu – возвращает проводнику наши созданные пункты меню.
InvokeCommand – задает команду для каждого пункта меню.
GetCommandString – возвращает провднику строку-подсказку, которая затем появляется в строке состояния при наведении курсора на какой-либо пункт меню.


Теперь поднимемся немного выше и под строчкой COM_INTERFACE_ENTRY(IMyFirstContextMenu) пишем:
Код:
  COM_INTERFACE_ENTRY(IShellExtInit)
  COM_INTERFACE_ENTRY(IContextMenu)
А в самом объявлении класса изменяем код на следующий:
Код:
class ATL_NO_VTABLE TMyFirstContextMenuImpl :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<TMyFirstContextMenuImpl, &CLSID_MyFirstContextMenu>,
  public IMyFirstContextMenu,
  public IShellExtInit,
  public IContextMenu

Эти строчки сообщают проводнику, что «эта DLL есть не что иное, как расширение контекстного меню». Также необходимо заиклудить shlobj.h:
#include <shlobj.h>
Во избежание лишних проблем и ошибок типа Multiple Declaration заходим в Project->Options и добавляем в Conditional Defines дефайн NO_WIN32_LEAN_AND_MEAN.

Ну с Main.h мы вроде как закончили пока что. Теперь переходим к редактированию не менее важного файла Main.cpp ^)…

[Main.cpp]
Ну как обычно не забываем сохранять проект… А то… мало ли что?... ?
Открываем Main.cpp и сначала инклудим файлы:
Код:
#include "stdio.h"
#include <windows.h>
#include <ComServ.hpp>
#include <ComObj.hpp>
#include <ActiveX.hpp>
#include <ShlObj.h>
#include <ShlObj.hpp>
#include <Menus.hpp>
#include <ShellAPI.hpp>
#include <SysUtils.hpp>
#include <registry.hpp>
#include <atlconv.h>
Все это стандартные файлы, которые описания не требуют, кроме atlconv.h. Данный файлик нужен нам для дальнейшей конвертации строк в юникод. Иначе «подсказка» в строке состояния будет отображаться криво и крякозябрами.
Теперь нужно прописать метод initialize. Начнем мы именно с нее, так как эта функция является первой, на которую ссылается эксплорер при загрузке DLL.


Код:
UINT uNumFiles=0; // количество переданных файлов
char szFile[MAX_PATH];
TStringList *sel_files = new TStringList; // «массив» содержащий пути к файлам
HResult _stdcall TMyFirstContextMenuImpl::Initialize(LPCITEMIDLIST pidlFolder,LPDATAOBJECT pDataObj,HKEY hProgID)
{
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HDROP     hDrop;
	if ( FAILED( pDataObj->GetData ( &fmt, &stg )))
        {
				return E_INVALIDARG;
        }
	hDrop = (HDROP) GlobalLock ( stg.hGlobal );
	if ( NULL == hDrop )
        {
        return E_INVALIDARG;
		}
uNumFiles = DragQueryFile ( hDrop, 0xFFFFFFFF, NULL, 0 );
       if ( 0 == uNumFiles )
        {
        GlobalUnlock ( stg.hGlobal );
        ReleaseStgMedium ( &stg );
        return E_INVALIDARG;
      }
…
В этой части кода идут проверки (на передачу неправильных аргументов, на нулевое количество выделенных файлов в проводнике и т.д.), а также «запоминание» файлов для дальнейшего их добавления в sel_files. Следует отметить, что переменная uNumFiles и содержит это самое количество выделенных файлов.
Код:
…
HRESULT hr = S_OK;
for (UINT i = 0; i < uNumFiles; i++) {
if ( 0 == DragQueryFile ( hDrop, i, szFile, MAX_PATH ))
        {
		hr = E_INVALIDARG;
		break;
        }
sel_files->Add(szFile);
}
    GlobalUnlock ( stg.hGlobal );
    ReleaseStgMedium ( &stg );
    return hr;
}
Здесь особо ничего, я так думаю, объяснять не надо. Просто цикл, который поочередно добавляет пути к файлам в наш «массив» из szFile, которая в свою очередь заполняется вызовом DragQueryFile. Перед добавлением в массив также можно реализовать проверку на «фшивость», так сказать. Ну т.е. действительно ли был выделен тот или иной файл, а уж затем и добавлять файл в список «правильных».
С инициализацией закончили. Теперь перейдем к построению нашего меню. Но для начала рассмотрим формат функции InsertMenu.
Вот как она объявлена в winuser.h:
Код:
InsertMenuW(
    __in HMENU hMenu,
    __in UINT uPosition,
    __in UINT uFlags,
    __in UINT_PTR uIDNewItem,
    __in_opt LPCWSTR lpNewItem);
Аргумент hMenu указывает на структуру HMENU, которая содержит наши «айтемы», их типы, флаги и т.п. uPosition указывает на позицию того или иного пункта, т.е. номер пункта в меню. Переменная uFlags содержит в себе флаги для определенного пункта меню. Флагов может быть несколько (отделяются он с помощью “|”), но обычно это MF_STRING | MF_BYPOSITION. Именно MF_BYPOSITION указывает обработчику , что пункт надо вставить под номером uPosition. uIDNewItem содержит идентификатор пункта меню который мы задаем сами для себя. Это необходимо, чтобы потом узнать куда кликнул юзверь. lpNewItem содержит название нашего добавляемого пункта.
Ну этого хватит для общего ознакомления с данной функцией. Если же есть желание «поизучать» и узнать более конкретные вещи о ней , то тебе прямая дорога на мсдн: [URL].
Теперь напишем долгожданную функцию QueryContextMenu.
Код:
HRESULT _stdcall TMyFirstContextMenuImpl::QueryContextMenu(HMENU Menu,UINT IndexMenu,UINT IDCmdFirst,UINT idCmdLast,UINT uFlags)
{
	UINT uCmdID = IDCmdFirst;
	UINT MenuIndex = IndexMenu;
	if ( uFlags & CMF_DEFAULTONLY )
		{
       		 return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
		}

	InsertMenu ( Menu, MenuIndex, MF_STRING | MF_BYPOSITION, uCmdID, " Test Menu Item 1") ;
uCmdID++; MenuIndex++;
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, /**/ 1 /**/ );
}
Данный код просто добавит один пункт с названием Test Menu Item 1. Если вы хотите добавить больше одного пункта то также не забываем изменить число в функции MAKE_HRESULT (оно сообщает проводнику о количестве добавленных пунктов). Иначе свои пункты ты просто не увидишь. Алгоритм добавления иконок к вашим пунктам и создания «подменю» мы рассмотрим чуть ниже…
Теперь напишем функции InvokeCommand и GetCommandString.
Код:
HResult _stdcall TMyFirstContextMenuImpl::InvokeCommand(LPCMINVOKECOMMANDINFO lpici)
{
             if ( 0 != HIWORD( lpici->lpVerb ))
		return E_INVALIDARG;
	switch ( LOWORD( lpici->lpVerb ))
		{
		case 0:
			{
			char *szMsg ="";
			szMsg=(char *)malloc(MAX_PATH*uNumFiles + 32); // мы не знаем количества выделенных файлов поэтому и размер переменной мы задаем с помощью malloc, иначе переполнения буфера нам не избежать…
			strcpy ( szMsg, "Selected file(s):\n" );
			for (int i = 0; i < uNumFiles; i++) {
			strcat ( szMsg, AnsiString("\n"+IntToStr(i+1)+": "+sel_files->Strings[i]).c_str() ); // копируем поочередно строки из sel_files 
			}
			ShowMessage(szMsg); //показываем сообщение
			break;
			}
		default: // если не то что мы хотели увидеть то…
        			sel_files->Clear();  // чистим sel_files
			return E_INVALIDARG; //возвращаем сообщение о некорректном параметре
		}
sel_files->Clear(); // чистим sel_files, а то переданные нам «файлы» будут в нем оставаться до закрытия окна проводника и выгрузки библиотеки из памяти
return S_OK; // все афигенски
}
Ну думаю в комментах ты разберешься. Единственное, что требует немного объяснения – это LOWORD( lpici->lpVerb ). lpici->lpVerb есть ни что иное как наш uCmdID, который мы задавали для создаваемого нами пункта.



С GetCommandString все еще проще. Только здесь необходимо переводить строчку в уникод-формат. Вот где нам и пригодился atlconv.h.
Код:
HRESULT _stdcall TMyFirstContextMenuImpl::GetCommandString (UINT  idCmd,UINT  uFlags,UINT* pwReserved,LPSTR pszName,UINT  cchMax )
{
	USES_CONVERSION; //ОБРАТИТЕ ВНИМАНИЕ НА НЕОБХОДИМОСТЬ ВЫЗОВА ДАННОЙ ФУНКЦИИ
	LPCTSTR szText;
	char temp[256]="";
	if ( uFlags & GCS_HELPTEXT )
		{
		switch(idCmd){
		case 0:	szText = _T("My Menu Item Number 0");break;
		default:sprintf(temp,"Unknown Menu Item Number %d",idCmd);szText = _T(temp);break;
		}

		if ( uFlags & GCS_UNICODE )
			{
			lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );
			}
        else
			{
			lstrcpynA ( pszName, T2CA(szText), cchMax );
            }

        return S_OK; 
        }

    return E_INVALIDARG;
}
Ну вот и все. С Main.cpp мы закончили.

[ПОДГОТОВКА К РЕГИСТРАЦИИ РАСШИРЕНИЯ В СИСТЕМЕ]
Прежде чем регистрировать наше расширение мы должны прописать определенный тип файла, соответствующий для него.
Откроем для ознакомления реестр форточек. Если ты уже знаешь, как регистрируются такие расширения, то можешь спокойно пропустить этот абзац. Итак, открываем реестр: Пуск->Выполнить->regedit. Заходим в HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers

Что мы здесь видим? А видим мы здесь все расширения, зарегистрированные для данного типа файла (* - говорит о том, что расширение нужно зарегистрировать для всех типов файлов). Таким образом, регистрируются расширения для антивирей (куда не нажмешь везде Сканировать файл и т.п.), архиваторов, асек и т.д. Теперь, если поискать (Правка->Поиск если кто в танке) данную ссылку в реестре, мы наткнемся на раздел, в котором будет храниться описание для данного расширение, а также путь для его загрузки.

Если с этим разобрались, то переходим непосредственно к нашему расширению.

Открываем файлик MyFirstContextMenu.cpp и ищем там функции DllRegisterServer и DllUnregisterServer. Эти функции вызываются соответственно при регистрации и удалении расширения. DllRegisterServer автоматически запишет в реестр путь до библиотеки, поэтому нам придется только сделать ссылку и зарегистрировать ее для конкретного типа файла. Изменяем код DllRegisterServer и DllUnregisterServer таким образом:
Код:
STDAPI __export DllRegisterServer(void)
{
	TRegistry *reg = new TRegistry(KEY_ALL_ACCESS);;
if (reg) // если всё ОК
	{
	reg->RootKey = HKEY_LOCAL_MACHINE;
	reg->OpenKey("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved", true);
	reg->WriteString ( "XXXXXXXXXXXXXXXXX","MyContextMenu extension" );
	reg->CloseKey();
	reg->RootKey = HKEY_CLASSES_ROOT;
	reg->OpenKey("*\\\ShellEx\\ContextMenuHandlers\\ MyContextMenu ", true);
	reg->WriteString ( "","XXXXXXXXXXXXXXX" );
	reg->CloseKey();
	}

delete reg;
	return _Module.RegisterServer(TRUE);
}

STDAPI __export DllUnregisterServer(void)
{
	  TRegistry *reg = new TRegistry(KEY_ALL_ACCESS);;
if (reg) // если всё ОК
	{
	reg->RootKey = HKEY_LOCAL_MACHINE;
	reg->OpenKey("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved", true);
	reg->DeleteValue ( "XXXXXXXXXXXXXXXXXXX" );
	reg->CloseKey();
	reg->RootKey = HKEY_CLASSES_ROOT;
	reg->DeleteKey("*\\\ShellEx\\ContextMenuHandlers\\SimpleShlExt");
	reg->CloseKey();


	}

delete reg;
    return _Module.UnregisterServer();
}
Ну думаю, если Вы разобрались, с предыдущим абзацем, то ничего объяснять не надо. Рассмотрим конкретно, что нужно написать вместо иксов.
А вместо иксов нужно написать нашу ссылку. Заходим в билдере MyFirstContextMenu.tlb, нажимаем на MyFirstContextMenu и копируем оттуда поле GUID. Это и есть идентификатор нашей библиотеки И ИМЕННО ЕГО НУЖНО ПРОПИСАТЬ В РЕЕСТР ДЛЯ ОТОБРАЖЕНИЯ НАШИХ ПУНКТОВ В МЕНЮ ЭКСПЛОРЕРА.
Теперь возвращаемся к нашим иксам. Вставляем везде просто вместо них только что скопированную строчку. Компилируем библиотеку по Ctrl+F9.

[РЕГИСТРАЦИЯ РАСШИРЕНИЯ]
Процесс этот невероятно прост. За нас все сделает волшебный экзишник с именем regsvr32.exe. Открываем командную строку. Переходим с помощью cd и cd ../ к нашей папке cо скомпилированной DLL. И пишем regsvr32 MyFirstContextMenu.dll. И СОБСТВЕННО ВСЕ!

Для удаления следует просто добавить аргумент –U, т.е. regsvr32 -U MyFirstContextMenu.dll

Теперь заходим в любую папку, тыкаем по любому файлу и видим наш Test Item!


[КАК ДОБАВИТЬ КАРТИНКИ К ПУНКТАМ]
Если разобраться, то это тоже просто Так-с… Начнем с начала…
А для этого начала необходимо сперва нарисовать иконки в формате BMP с размером 16x16 (желательно использовать как можно меньше бит при сохранении). Cрисовали ^) ?..
Тогда открываем Main.cpp и в глобальных переменных записываем:
HBITMAP bitmap1;
HBITMAP bitmap2;
Рядом с проектом создаем файлик res.rc, а в него копируем:
Код:
10000         BITMAP  DISCARDABLE     "yourbitmap1.bmp"
10001       BITMAP  DISCARDABLE     "yourbitmap2.bmp"
Что же сие означает? А означает это, что при компиляции DLL в следующие разы компилятор включит также файлы yourbitmap1.bmp и yourbitmap2.bmp (файлы должны лежать рядом с res.rc) для последующей их загрузки. Теперь добавляем в проект res.rc.
В очередной раз открываем Main.cpp и пишем в функции TMyFirstContextMenuImpl::Initialize после всех проверок следующее:
Код:
	
bitmap1 = LoadBitmap ( _Module.GetModuleInstance(),
							 MAKEINTRESOURCE(10000) );
	bitmap2 = LoadBitmap ( _Module.GetModuleInstance(),
							 MAKEINTRESOURCE(10001) );
Если ты подумал об этом, то ты прав =). Вот где нам пригодились эти таинственные цифры 10000 и 10001, о которых я умолчал =)

Теперь в TMyFirstContextMenuImpl::QueryContextMenu после вызова функции InsertMenu копируем:
Код:
if ( NULL != bitmap1 )
        {
		SetMenuItemBitmaps ( Menu, MenuIndex, MF_BYPOSITION, bitmap1, NULL );
	}
Компилируем проект и… о чудо… Напротив нашего пункта стоит наш bitmap1.
P.S. bitmap2 вставите в другой раз ?

[СОЗДАНИЕ ПОДМЕНЮ (SUBMENU)]
Вот над этим, честно говоря, просидел дня два. Но как всегда оказалось, что
«все гениальное - просто» ?.
Для создания подменю достаточно в QueryContextMenu добавить следующие строки:
Код:
	HMENU HSUBMENU = CreatePopupMenu ();
	InsertMenu (HSUBMENU, 0, MF_POPUP | MF_BYPOSITION | MF_STRING,uCmdID, "Test SubMenu Item 1");
	if ( NULL != bitmap2 )
        {
		SetMenuItemBitmaps ( Menu, MenuIndex, MF_BYPOSITION, bitmap2, NULL );
	}
	uCmdID++;
	InsertMenu (HSUBMENU, 1, MF_POPUP | MF_BYPOSITION | MF_STRING,uCmdID, "Test SubMenu Item 2");
	uCmdID++;
	InsertMenu (HSUBMENU, 2, MF_POPUP | MF_BYPOSITION | MF_STRING,uCmdID, "Test SubMenu Item 2");
	uCmdID++;
	InsertMenu (Menu, MenuIndex, MF_POPUP | MF_BYPOSITION | MF_STRING,(UINT)HSUBMENU, "Test SubMenu");
    if ( NULL != bitmap2 )
        {
		SetMenuItemBitmaps ( Menu, MenuIndex, MF_BYPOSITION, bitmap2, NULL );
	}
	uCmdID++; MenuIndex++;
То есть, если приглядеться в код, то за подменю отвечает один простой аргумент MF_POPUP. В InvokeCommand пункты подменю также узнаем с помощью LOWORD( lpici->lpVerb ):
Код:
….
case 1:
ShowMessage("Test Submenu Command 1");
break;
case 2:
ShowMessage("Test Submenu Command 2");
break;
case 3:
ShowMessage("Test Submenu Command 3");
break;
….
Правда, таким образом, я так и не смог понять как сделать подсказку для самого подменю, а не для его пунктов. Но это не суть как важно и большинство юзверей туда и не смотрят (по-моему даже в винде по-умолчанию строка состояния отключена).

[ЗАКЛЮЧЕНИЕ]
Ну что?... Научились мы изменять и дополнять меню эксплорера. В качестве дополнительной литературы очень советую прочитать: http://www.rsdn.ru/article/winshell/shlext1.xml. Так как некоторый код в данной статье взят именно оттуда + там же можно увидеть как создавать страницы свойств для файла, подсказки и многое другое. Только придется чуть переделать код для билдера. Так как мы пишем именно для него

Скачать пример проекта можно отсюда: http://depositfiles.com/ru/files/xtyae5rud
Пароль к архиву: by_dobby007_for_antichat.ru

Bye-bye…

Последний раз редактировалось Dobby007; 30.03.2009 в 06:20..
 
Ответить с цитированием