Руководство полного идиота по написанию расширений оболочки - Часть VI

Расширение оболочки, которое может быть использовано в меню Send To.

Автор: Michael Dunn
Перевод: Инна Кирюшкина
Алексей Кирюшкин

Источник: The Code Project
Опубликовано: 21.09.2001
Версия текста: 1.0

Введение
Обработчик сбрасывания
Использование AppWizard
Интерфейс инициализации
Участвуем в операции drag and drop
DragEnter()
DragLeave()
Drop()
Регистрация расширения
Продолжение следует..?

Демонстрационный проект

Введение

В части VI я познакомлю вас с типом расширения оболочки, который используется достаточно редко - обработчиком сбрасывания. Этот тип расширения может быть использован для добавления функции "перетащить и сбросить" к проводнику. При этом файл, на который осуществляется сбрасывание, определяет используемое расширение.

Эта статья предполагает, что вы имеете основные знания по расширениям оболочки и знакомы с классами MFC, используемыми для взаимодействия с оболочкой. Если вам необходимо освежить знания по MFC, вам нужно прочитать Часть IV, т.к. та же технология используется в этой статье. Код использует несколько функций из shlwapi.dll, так что Вам нужен будет IE4 или выше (Active Desktop устанавливать не обязательно).

Обработчик сбрасывания

В Части IV я говорил об обработчике перетаскивания, который загружается во время операции перетаскивания с помощью правой кнопки мыши. Проводник также позволяет нам написать расширение, которое будет загружаться при перетаскивании файлов левой кнопкой мыши. В этой операции расширение оперирует файлом, на который осуществляется сбрасывание. Например, WinZip содержит обработчик сбрасывания, который позволяет добавить файлы к архиву просто сбрасывая их на архив. Когда вы проносите файл над zip-архивом, проводник показывает, что этот архив является целью для сбрасывания, подсвечивая zip-файл и добавляя к курсору знак «+»:


Если бы обработчика сбрасывания не было, при перемещении файла над архивом ничего бы не произошло:


Обработчики сбрасывания действительно полезны только тогда, когда у вас есть свой тип файлов, как у WinZip. Намного интереснее с помощью обработчика сбрасывания добавить пункты к меню Send To. Меню Send To показывает содержимое меню \Windows\SendTo (в NT путь отличается для каждого пользователя). Обычно каталог Send To содержит ярлычки, но Microsoft Power Toys добавляет несколько других пунктов, как показано ниже:


Если вы не понимаете, причем здесь обработчики, взгляните на листинг каталога Send To:

12-02-98   0:27   129  3? Floppy (A).lnk
11-26-98  10:27     0  Any Folder....OtherFolder
11-26-98  10:27     0  Clipboard as Contents.ContentsOnClipboard
11-26-98  10:27     0  Clipboard as Name.NameOnClipboard
11-26-98  10:27     0  Command Line.CommandLine
03-26-99   8:42     0  Desktop (create shortcut).DeskLink
04-22-99  23:30     0  Norton Wipe Slack Space.WipeSlack
04-22-99  23:30     0  Norton WipeInfo.WipeInfo
11-26-98  10:26   285  Notepad.lnk
01-07-00   9:01   212  xfer directory on zip drive.lnk

Обратите внимание на странные расширения типа ".ContentsOnClipboard". Эти пустые файлы появятся как пункты в меню Send To, а их расширения прописаны в реестре. Это не обычные ассоциации, т.к. этим файлам не поставлены в соответствие действия типа «Открыть» или «Печатать». Их работа - обработка сбрасываний. Когда вы выбираете один из этих пунктов в меню Send To, проводник загружает соответствующий обработчик сбрасывания. Вот другой вид меню с выделенными обработчиками сбрасывания:


Демонстрационный проект этой статьи будет аналогом Send To Any Folder Powertoy - он скопирует или переместит файлы в любой каталог вашей системы.

Использование AppWizard

Запустите AppWizard и создайте новый ATL COM проект. Назовем его SendToClone. Пометьте переключатель Support MFC и щелкните Finish. Чтобы добавить COM объект к DLL, перейдите в дерево просмотра классов, ClassView, щелкните правой кнопкой на пункте SendToClone classes и укажите New ATL Object.

В мастере ATL объектов, на первой панели уже указан Simple Object, поэтому просто щелкните Next. Во второй панели, в поле редактирования Short Name введите краткое имя SendToShlExt и щелкните OK. (Остальные поля заполняются автоматически.) Мы создали класс CSendToShlExt, который содержит основной код для реализации объекта COM. Добавим свой код к этому классу.

Интерфейс инициализации

Т.к. обработчик сбрасывания загружается на цели сбрасывания (файле, на который осуществляется сбрасывание), инициализация осуществляется посредством интерфейса IPersistFile. Вспомните, что этот интерфейс используется для расширений, оперирующих только одиночными файлами. IPersistFile имеет много методов, но расширению необходимо реализовать только один метод - Load().

Нам необходимо добавить IPersistFile в список интерфейсов, которые реализует CSendToShlExt. Откройте SendToShlExt.h и добавьте выделенные строки:

#include <comdef.h>
#include <shlobj.h>

  class ATL_NO_VTABLE CSendToShlExt : 
      public CComObjectRootEx<CComSingleThreadModel>,
      public CComCoClass<CSendToShlExt, &CLSID_SendToShlExt>,
      public IDispatchImpl<ISendToShlExt, &IID_ISendToShlExt, &LIBID_SENDTOCLONELib>,
      public IPersistFile
  {
  BEGIN_COM_MAP(CSendToShlExt)
      COM_INTERFACE_ENTRY(ISendToShlExt)
      COM_INTERFACE_ENTRY(IDispatch)
      COM_INTERFACE_ENTRY(IPersistFile)
  END_COM_MAP()

  public:
      // IPersistFile
      STDMETHOD(GetClassID)(LPCLSID)      { return E_NOTIMPL; }
      STDMETHOD(IsDirty)()                { return E_NOTIMPL; }
      STDMETHOD(Load)(LPCOLESTR, DWORD)   { return S_OK;      }
      STDMETHOD(Save)(LPCOLESTR, BOOL)    { return E_NOTIMPL; }
      STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; }
      STDMETHOD(GetCurFile)(LPOLESTR*)    { return E_NOTIMPL; }

Заметьте, что Load() не делает ничего, кроме возврата S_OK. Load() получает имя файла, который является целью для сбрасывания, но для нашего расширения нет необходимости заботиться об этом имени, поэтому мы его игнорируем.

Участвуем в операции drag and drop

Чтобы выполнить свою работу, наше расширение должно связываться с источником сбрасывания, которым является проводник. Наше расширение получает список перетаскиваемых файлов, и проводник сообщает его, независимо от того, будут ли приняты эти файлы, если пользователь надумает их сбросить. Эта связь осуществляется, естественно, через интерфейс: IDropTarget. Методы IDropTarget:

Чтобы добавить IDropTarget к CSendToShlExt, откройте SendToShlExt.h и добавьте выделенные строки:

class ATL_NO_VTABLE CSendToShlExt : 
      public CComObjectRootEx<CComSingleThreadModel>,
      public CComCoClass<CSendToShlExt, &CLSID_SendToShlExt>,
      public IDispatchImpl<ISendToShlExt, &IID_ISendToShlExt, &LIBID_SENDTOCLONELib>,
      public IPersistFile,
      public IDropTarget
  {
  BEGIN_COM_MAP(CSendToShlExt)
      COM_INTERFACE_ENTRY(ISendToShlExt)
      COM_INTERFACE_ENTRY(IDispatch)
      COM_INTERFACE_ENTRY(IPersistFile)
      COM_INTERFACE_ENTRY(IDropTarget)
  END_COM_MAP()

  protected:
      // ISendToShlExt
      CStringList m_lsDroppedFiles;

  public:
      // IDropTarget
      STDMETHOD(DragEnter)(IDataObject* pDataObj, DWORD grfKeyState,
                           POINTL pt, DWORD* pdwEffect);

      STDMETHOD(DragOver)(DWORD grfKeyState, POINTL pt, DWORD* pdwEffect)
          { return E_NOTIMPL; }

      STDMETHOD(DragLeave)();

      STDMETHOD(Drop)(IDataObject* pDataObj, DWORD grfKeyState,
                      POINTL pt, DWORD* pdwEffect);
  }

Как и в предыдущих расширениях, нам понадобится строковый список, чтобы хранить имена перетаскиваемых файлов. Метод DragOver() не нуждается в реализации, т.к. он не вызывается. Далее я приведу оставшиеся три метода.

DragEnter()

Прототип функции:

HRESULT IDropTarget::DragEnter (
      IDataObject* pDataObj,
      DWORD        grfKeyState,
      POINTL       pt,
      DWORD*       pdwEffect );

pDataObj - указатель на интерфейс IDataObject, с помощью которого мы перечисляем имена перетаскиваемых файлов. grfKeyState - установка флагов, сигнализирующих, какие shift-клавиши и кнопки мыши были нажаты. (Я не знаю, что означает префикс "gr", может имеется ввиду "group of flags"?) pt - это структура POINTL (бывает и POINT), которая хранит координаты мыши. pdwEffect - указатель на DWORD, в котором мы возвратим значение, сообщающее проводнику, примем ли мы брошенные файлы, и если так, какой вид будет иметь иконка, которая перекроет курсор мыши.

Как я упоминал раньше, DragEnter() вообще-то вызывается, когда пользователь заносит файл над целью. Однако этот метод также вызывается, когда пользователь щелкает на пункте меню Send To, поэтому мы все еще можем сделать всю нашу работу в DragEnter(), хотя технически это не случай drag and drop.

Наша реализация DragEnter() заполнит строковый список именами перетаскиваемых файлов. Это расширение примет все файлы или каталоги, т.к. любые объекты файловой системы могут быть скопированы или перемещены.

DragEnter() начинается со знакомых вам действий - мы подключаем COleDataObject к интерфейсу IDataObject, и перечисляем перетаскиваемые файлы.

HRESULT CSendToShlExt::DragEnter (
      IDataObject* pDataObj,
      DWORD        grfKeyState,
      POINTL       pt,
      DWORD*       pdwEffect )
  {
      AFX_MANAGE_STATE(AfxGetStaticModuleState());    // init MFC

  COleDataObject dataobj;
  TCHAR          szItem [MAX_PATH];
  UINT           uNumFiles;
  HGLOBAL        hglobal;
  HDROP          hdrop;

      dataobj.Attach ( pDataObj, FALSE ); // attach to the IDataObject, don't auto-release it

      // Read the list of items from the data object.  They're stored in HDROP
      // form, so just get the HDROP handle and then use the drag 'n' drop APIs
      // on it.

      hglobal = dataobj.GetGlobalData ( CF_HDROP );

      if ( NULL != hglobal )
          {
          hdrop = (HDROP) GlobalLock ( hglobal );

          uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );

          for ( UINT uFile = 0; uFile < uNumFiles; uFile++ )
              {
              if ( 0 != DragQueryFile ( hdrop, uFile, szItem, MAX_PATH ))
                  {
                  m_lsDroppedFiles.AddTail ( szItem );
                  }
              }

          GlobalUnlock ( hglobal );
          }

Настало время возвратить значение через pdwEffect. Возвращаемое значение может быть таким:

Мы будем возвращать только DROPEFFECT_COPY. Мы не можем вернуть DROPEFFECT_MOVE, т.к. проводник бы удалил перетаскиваемые файлы. Мы могли бы вернуть DROPEFFECT_LINK, но тогда курсор имел бы маленькую стрелочку, что обычно обозначает ярлычки, и это вводило бы в заблуждение пользователя. Если список файлов пуст, что бывает, когда мы не можем прочитать буфер обмена, мы возвращаем DROPEFFECT_NONE, чтобы сказать проводнику, что мы не примем переброску.

if ( m_lsDroppedFiles.GetCount() > 0 ) 
          {
          *pdwEffect = DROPEFFECT_COPY;
          return S_OK;
          }
      else
          {
          *pdwEffect = DROPEFFECT_NONE;
          return E_INVALIDARG;
          }
  }

DragLeave()

DragLeave() вызывается, если пользователь утаскивает объект прочь от нашего целевого файла, не сбрасывая его. Этот метод не используется для меню Send To, но он вызывается, если у вас открыто окно проводника на каталоге Send To и вы перетаскиваете файлы в этот каталог. У нас нет никакой очистки, которую нужно было бы сделать (деструктор CString сам позаботиться об этом), поэтому все, что нам нужно сделать - это вернуть S_OK:

HRESULT CSendToShlExt::DragLeave()
  {
      return S_OK;
  }

Drop()

Если пользователь выберет наш пункт из меню Send To, проводник вызывает метод Drop(), прототип которого:

HRESULT IDropTarget::Drop (
      IDataObject* pDataObj,
      DWORD        grfKeyState,
      POINTL       pt,
      DWORD*       pdwEffect );

Первые три параметра такие же, как у DragEnter(). Drop() должен вернуть результат всей операции через параметр pdwEffect. Наша реализация Drop() создает диалог и передает ему список имен файлов. Диалог делает все работу, и, когда происходит возврат из DoModal(), мы устанавливаем результат сбрасывания:

HRESULT CSendToShlExt::Drop (
      IDataObject* pDataObj,
      DWORD        grfKeyState,
      POINTL       pt,
      DWORD*       pdwEffect )
  {
      AFX_MANAGE_STATE(AfxGetStaticModuleState());    // init MFC

  CSendToCloneDlg dlg ( &m_lsDroppedFiles );

      dlg.DoModal();

      *pdwEffect = DROPEFFECT_COPY;
      return S_OK;
  }

Диалог выглядит так:


Это достаточно прямолинейный MFC диалог, и вы можете найти исходники в файле SendToCloneDlg.cpp. Я делал копирование и перемещение используя класс CShellFileOp из моей статьи "CShellFileOp - Wrapper for SHFileOperation."

Подождите! А как мы сообщим проводнику о нашем обработчике сбрасывания? И как нам получить пункт в меню Send To? Я объясню, как это делается, в следующем разделе.

Регистрация расширения

Регистрация обработчика сбрасывания немного отличается от регистрации других типов расширений, т.к. требует создания новой ассоциации под ключом HKEY_CLASSES_ROOT. AppWizard генерирует RGS сценарий, показанный ниже. Часть, которую вы должны добавить или изменить выделена.

HKCR
  {
      .SendToClone = s 'CLSID\{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'
      SendToClone.SendToShlExt.1 = s 'SendToShlExt Class'
      {
          CLSID = s '{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'
      }
      SendToClone.SendToShlExt = s 'SendToShlExt Class'
      {
          CLSID = s '{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'
          CurVer = s 'SendToClone.SendToShlExt.1'
      }
      NoRemove CLSID
      {
          ForceRemove {B7F3240E-0E29-11D4-8D3B-80CD3621FB09} = s 'Send To Any Folder Clone'
          {
              ProgID = s 'SendToClone.SendToShlExt.1'
              VersionIndependentProgID = s 'SendToClone.SendToShlExt'
              ForceRemove 'Programmable'
              InprocServer32 = s '%MODULE%'
              {
                  val ThreadingModel = s 'Apartment'
              }
              'TypeLib' = s '{B7F32400-0E29-11D4-8D3B-80CD3621FB09}'
              val NeverShowExt = s ''
              DefaultIcon = s '%MODULE%,0'
              shellex
              {
                  DropHandler = s '{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'
              }
          }
      }
  }

Первая строка как раз и выполняет ассоциацию. Она создает новое расширение, .SendToClone, которое и будет использоваться нашей целью для сбрасывания. Заметим, что у значения по умолчанию ключа .SendToClone есть приставка "CLSID\". Это говорит проводнику, что данные, которые описывает ассоциация находятся в ключе под HKCR\CLSID, рядом с более стандартными ассоциациями, данные которых сохранены в ключе прямо под HKEY_CLASSES_ROOT (например, ключ .txt указывает на ключ txtfile), но это только кажется стандартным - сохранение ассоциации обработчика сбрасывания под своим CLSID ключом, чтобы держать все данные в одном месте.

Строка "Send To Any Folder Clone" - это описание типа файла, которая появляется в проводнике, если вы просматриваете каталог Send To. Значение NeverShowExt создано, чтобы сообщить проводнику, что не нужно показывать расширение ".SendToClone". DefaultIcon - ключ, указывающий размещение иконки, ассоциированной с файлами .SendToClone. Наконец, у нас есть знакомый ключ shellex с подключом DropHandler. Т.к. здесь может быть только один обработчик для данного типа файлов, GUID обработчика сохранен прямо в ключе DropHandler, вместо подключа под DropHandler.

Оставшаяся деталь - создать файл в каталоге Send To, чтобы там появился наш пункт в меню. Мы можем сделать это в DllRegisterServer() и удалить файл в DllUnregisterServer(). Вот код, создающий файл:

LPITEMIDLIST pidl;
  TCHAR        szSendtoPath [MAX_PATH];
  HANDLE       hFile;
  LPMALLOC     pMalloc;

      if ( SUCCEEDED( SHGetSpecialFolderLocation ( NULL, CSIDL_SENDTO, &pidl )))
          {
          if ( SHGetPathFromIDList ( pidl, szSendtoPath ))
              {
              PathAppend ( szSendtoPath, _T("Some other folder.SendToClone") );

              hFile = CreateFile ( szSendtoPath, GENERIC_WRITE, FILE_SHARE_READ,
                                   NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );

              CloseHandle ( hFile );
              }

          if ( SUCCEEDED( SHGetMalloc ( &pMalloc )))
              {
              pMalloc->Free ( pidl );
              pMalloc->Release();
              }
          }

Вот как выглядит новое меню Send To с нашим пунктом меню:


DllUnregisterServer() содержит простой код, который удаляет файл. Код, показанный выше, работает на всех версиях Windows (ну хорошо, на версиях 4 и выше). Если вы знаете, что ваш код будет запущен только в оболочке версии 4.71 и выше, вы можете использовать SHGetSpecialFolderPath() вместо SHGetSpecialFolderLocation().

Как всегда, в Windows NT и Windows 2000 нам необходимо добавить наше расширение в список "одобренных" расширений. Код, выполняющий эту операцию, находится в функциях DllRegisterServer() и DllUnregisterServer() в демонстрационном проекте.

Продолжение следует..?

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


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.