By Michael Dunn - Translated by Gengis Dave
Una guida passo-passo per scrivere shell extensions
Scarica il demo - 11 Kb
Una shell extension è un oggetto COM che aggiunge qualche tipo di funzionalità alla shell di Windows (Explorer). Ci sono molti tipi di extension, ma poche documentazioni su cosa sono e su come costruirle. Raccomando il libro di Esposito Visual C++ Windows Shell Programming se si vuole avere una visione completa su molti aspetti della shell, ma per coloro che non hanno il libro o ai quali interessano solo le shell extension, ho scritto questo tutorial che potrà stupire, oppure far solamente capire come scrivere le proprie extension. Questa guida assume che si abbia familiarità con le basi di COM and ATL.
Comunque, cosa diamine è una shell extension?
Ci sono due termini da spiegare, shell e extension. Shell si riferisce a Explorer, e extension si riferisce al codice che viene scritto ed eseguito da Explorer in determinati eventi (per esempio cliccando col tasto destro del mouse). Quindi una shell extension è un oggetto COM che aggiunge delle caratteristiche a Explorer.
Una shell extension è un processo interno che implementa un'interfaccia che comunica con Explorer. ATL è (IMO) il modo più felice per avere una extension, senza doversi bloccare nello scrivere il codice di QueryInterface() e AddRef() . Inoltre è molto facile debuggare le estensioni in Windows NT e 2000, come spiegato più avanti.
Ci sono molti tipi di shell extensions, ognuno dei quali viene invocato in un ogni diverso evento. Qui c'è un piccolo elenco e le situazioni in cui vengono invocati:
Tipo | Quando viene chiamata | Cosa fa |
Context menu handler | L'utente clicca col destro su un file o su una cartella. Dalla versione 4.71 della shell, funziona anche sullo sfondo o nelle finestre. | Aggiunge elementi al menù. |
Property sheet handler | Mostra le proprietà di un file. | Aggiunge pagine alla finestra. |
Drag and drop handler | L'utente seleziona degli oggetti e li trascina col tasto destro in una finestra o sul desktop. | Aggiunge elementi al menù. |
Drop handler | L'utente seleziona degli oggetti e li trascina su un file | Qualsiasi azione. |
QueryInfo handler (dalla shell 4.71 in poi) | L'utente posiziona il mouse su un file o su un'icona. | Mostra una stringa in un tooltip. |
Qualcuno potrebbe chiedersi come sembra un extension in Explorer. Se si ha WinZip (e chi non ce l'ha?), si notano molti tipi di extension, una delle quali è un menù contestuale. Così WinZip 8 aggiunge gli elementi al menù per i file compressi:
WinZip contiene il codice che aggiunge gli elementi al menù e compie delle azioni quando l'utente sceglie uno dei comandi di WinZip.
WinZip contiene anche una funzione per il drag and drop. Questo tipo è molto simile al menù contestuale, ma viene chiamata quando l'utente seleziona un file col tasto destro. Questi sono i comandi aggiunti da WinZip:
Di tipi ce ne sono sono molti (e Microsoft ne aggiunge sempre di nuovi ad ogni versione di Windows!). Per adesso, ci limiteremo alle extension del menù contestuale, poiché sono facili da fare e il risultato è semplice da usare. Prima di iniziare a codare, ci sono dei suggerimenti che renderanno il lavoro ancora più facile Quando una extension viene caricata da Explorer, resta in memoria per un momento, bloccandone l'accesso. Per fare in modo che Explorer scarichi le extension più spesso, bisogna creare questa chiave nel registro:
HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL
e impostarla a "1". Sul 9x, è la migliore cosa da fare. In NT/2000, bisogna andare a questa chiave:
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer
e creare una DWORD chiamata DesktopProcess con valore 1. Questo fa sì che il desktop e la Taskbar girino in un unico processo, e conseguentemente, Explorer giri come processo a sé. Questo significa che si può fare il debug del singolo Explorer, e quando verrà chiuso, le DLL verranno automaticamente scaricate, eliminando ogni problema di accesso ai file. Per attuare queste modifiche, ci sarà bisogno di effettuare un logout.
Iniziare un extension del menù che si fa?
Iniziamo a fare una semplice extension che mostrerà una message box per fa vedere che funziona. Verrà agganciata ai file .TXT, in questo modo l'estensione verrà chiamata cliccando col destro su un file di testo.
Uso dell'AppWizard
Trovo che sia molto facile accompagnare con esempi i concetti che vengono spiegati, immediatamente seguiti dal codice.
Scegliere dall'AppWizard una nuova applicazione ATL COM, che verrà chiamata SimpleExt . Mantenere tutti i settaggi di default. Ora si ha un progetto ATL vuoto che produrrà una DLL, ma bisogna aggiungere un oggetto COM alla shell extension. Nella finestra ClassView, cliccare col destro su SimpleExt classes , e scegliere New ATL Object . Nel menù ATL Object di default è selezionato Simple Object , cliccare su Next. Mettere SimpleShlExt in Short Name e cliccare su OK (tutti gli altri campi verranno riempiti automaticamente). Verrà creata una classe chiamata CSimpleShlExt che contiene il codice base per implementare l'oggetto COM. Il nostro codice verrà aggiunto a questa classe.
L'inizializzazione dell'interfaccia
Quando la nostra extension viene chiamata, Explorer esegue la funzione QueryInterface() per avere un puntatore all'interfaccia IShellExtInit . Questa interfaccia ha solamente un metodo, Initialize() , il cui prototipo è:
HRESULT IShellExtInit::Initialize (LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID);
Explorer usa questo metodo per dare varie informazioni. pidlFolder è il PIDL della cartella contenente il file su cui si sta lavorando (PIDL [pointer to an ID list] è una struttura che identifica solamente qualsiasi oggetto nella shell). pataObj è un puntatore all'interfaccia IDataObject attraverso la quale si ottiene il nome del file. hProgID è un HKEY con il quale si accede alla chiave di registro che contiene i dati sulla nostra DLL. Per questo esempio, si userà solamente il parametro pDataObj . Per aggiungerlo al nostro oggetto COM, aprire il file SimpleShlExt.h , e aggiungere le seguenti righe (quelle in rosso):
#include <shlobj.h>
#include <comdef.h>
class ATL_NO_VTABLE CSimpleShlExt :
public CcomObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
public IDispatchImpl<ISimpleShlExt, &IID_ISimpleShlExt, &LIBID_SIMPLEEXTLib>,
public IshellExtInit
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()
Questa COM_MAP è come ATL implementa QueryInterface() . Poi dentro la dichiarazione della classe, aggiungere il prototipo di Initialize() . Ci sarà bisogno anche di un buffer in cui salvare il nome del file:
protected:
TCHAR m_szFile [MAX_PATH];
public:
// IshellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
Poi, in SimpleShlExt.cpp , aggiungere la seguente definizione:
HRESULT CSimpleShlExt::Initialize (LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID )
L'intento è ottenere il nome del file selezionato, e mostrarlo nella message box. Se c'è più di un file, si può accedere a tutti tramite pDataObj , ma per rendere facili le cose, si considererà solamente il primo file. Il nome del file viene salvato con lo stesso metodo di quando trascini un file in una finestra con WS_EX_ACCEPTFILES . Ciò significa che si ottiene il nome del file con l'API: DragQueryFile() . Inizieremo la funzione ottenendo l'handle a IdataObject :
{
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HDROP hDrop;
// Ricerca di CF_HDROP
if ( FAILED( pDataObj->GetData ( &fmt, &stg )))
{
// No! Ritorna "invalid argument" a Explorer.
return E_INVALIDARG;
}
// ottiene il puntatore
hDrop = (HDROP) GlobalLock ( stg.hGlobal );
// controlla che funzioni
if ( NULL == hDrop )
{
return E_INVALIDARG;
}
Notare che è importanza vitale controllare tutto da eventuali errori, specialmente i puntatori. Poiché la nostra extension gira nello spazio di Explorer, se dovesse andare in crash, succederebbe anche a Explorer. Sul 9x ciò necessiterebbe di fare un reboot della macchina. Quindi adesso abbiamo il puntatore HDROP , possiamo ottenere il nome del file che ci serve.
// controlla che ci sia almeno un file
UINT uNumFiles = DragQueryFile ( hDrop, 0xFFFFFFFF, NULL, 0 );
if ( 0 == uNumFiles )
{
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return E_INVALIDARG;
}
HRESULT hr = S_OK;
// Prende il nome del primo file e lo mette in m_szFile
if ( 0 == DragQueryFile ( hDrop, 0, m_szFile, MAX_PATH ))
{
hr = E_INVALIDARG;
}
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return hr;
}
Se ritorniamo E_INVALIDARG , Explorer non richiamerà di nuovo la nostra extension. Se ritorniamo S_OK , allora Explorer eseguirà QueryInterface() e otterrà il puntatore all'interfaccia che aggiungeremo: IcontextMenu .
L'interfaccia per interagire col menù contestuale
Dopo che Explorer ha inizializzato l'extension, eseguirà il metodo IContextMenu per aggiungere un elemento al menù. Aggiungere IContextMenu alla shell extension è come aggiungere IShellExtInit . Aprire SimpleShlExt.h e aggiungere le linee in rosso:
class ATL_NO_VTABLE CSimpleShlExt :
public CcomObjectRootEx <CComSingleThreadModel>,
public CComCoClass <CSimpleShlExt, &CLSID_SimpleShlExt>,
public IDispatchImpl <ISimpleShlExt, &IID_ISimpleShlExt, &LIBID_SIMPLEEXTLib>,
public IshellExtInit,
public IcontextMenu
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
E aggiungere il prototipo per il metodo IcontextMenu :
public:
// IcontextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);
Modificare il menù contestuale
IContextMenu ha tre metodi. Il primo, QueryContextMenu() , ci permette di modificare il menù. Il prototipo è:
HRESULT IContextMenu::QueryContextMenu (HMENU hmenu, UINT uMenuIndex,
UINT uidFirstCmd, UINT uidLastCmd,
UINT uFlags );
hmenu è un puntatore al menù contestuale. uMenuIndex è la posizione nella quale aggiungere i nostri elementi. uidFirstCmd e uidLastCmd sono il range dei valori dei comandi che useremo per i nostri elementi del menù. uFlags indica perché Explorer esegue QueryContextMenu() , e verrà visto più tardi. Il valore di ritorno dipende da chi lo chiedi. Il libro di Dino Esposito dice che è il numero di elementi aggiunti al menù da QueryContextMenu() . Le MSDN contenute in VC 6 dicono che è l'ID dell'ultimo elemento aggiunto al menù, più 1. Le ultime MSDN online dicono:
"Ritorna il valore del più grande identificatore assegnato, più uno. Per esempio, se idCmdFirst è settato a 5 e vengono aggiunti 3 elementi con identificatori 5, 7 e 8. Il valore di ritorno sarà MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1). Ci atterremo alla spiegazione di Dino. Il suo metodo è uguale a quello delle MSDN online, numerando gli elementi con uidFirstCmd e incrementando di uno per ogni elemento. La nostra shell avrà solamente un elemento, quindi QueryContextMenu() è molto facile:
HRESULT CSimpleShlExt::QueryContextMenu (HMENU hmenu, UINT uMenuIndex,
UINT uidFirstCmd, UINT uidLastCmd,
UINT uFlags )
{
// se la flag contiene CMF_DEFAULTONLY non possiamo fare niente
if ( uFlags & CMF_DEFAULTONLY )
{
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
}
InsertMenu (hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, _T("SimpleShExt"));
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}
La prima cosa da fare è controllare uFlags . Si può vedere la lista completa sulle MSDN, ma per la shell extension è importante un solo valore: CMF_DEFAULTONLY . Questa flag dice di aggiungere solamente un elemento di default. Le shell extension non aggiungerebbero elementi se questa flag fosse presente. E per questo che la funzione ritorna immediatamente 0 se è presente CMF_DEFAULTONLY . Se questa flag non è presente, modifichiamo il menù (usando hmenu ), e ritorniamo 1 per dire alla shell che abbiamo aggiunto un elemento.
Mostrare l'help nella barra di stato
Il prossimo IContextMenu da chiamare è GetCommandString() . Se l'utente clicca col destro su un file di testo in una finestra di Explorer (il frame di destra, se la finestra ne ha due), oppure seleziona un file di testo e dopo clicca sul menù File , la barra di stato mostrerà l'help. La funzione GetCommandString() ritorna la stringa che Explorer mostrerà. Il prototipo è:
HRESULT IContextMenu::GetCommandString (UINT idCmd, UINT uFlags, UINT *pwReserved,
LPSTR pszName, UINT cchMax );
idCmd è un contatore che indica quale elemento è selezionato. Poiché c'è solamente un elemento, idCmd sarà sempre zero. Ma se per esempio, gli elementi fossero stati tre, idCmd sarebbe stato 0, 1 o 2. uFlags è un altro gruppo di flag che saranno descritte più avanti. Ignorando pwReserved , pszName è il puntatore ad un buffer della shell nel quale mettere la stringa da mostrare. cchMax è la misura del buffer. Il valore di ritorno è una costante HRESULT, tipo S_OK o E_FAIL .
GetCommandString() può anche essere usata per ottenere la "parola" per un elemento del menù. Una "parola" è una stringa indipendente dal linguaggio che identifica un azione da compiere sul file. La documentazione su ShellExecute() ha molto da dire, e l'argomento è buono per un altro articolo, ma il succo è che le "parole" possono essere sia conservate nel registro, sia create dinamicamente da una extension. Questo fa sì che un'azione implementata in una shell extension venga eseguita da ShellExecute() .
Comunque, il motivo di tutto questo discorso è per stabilire perché GetCommandString() venga chiamata. Se Explorer vuole una stringa al volo, la stabiliamo. Se Explorer si aspetta una "parola", ignoreremo la richiesta. E qui entra in gioco il parametro uFlags . Se uFlags ha il bit GCS_HELPTEXT settato, allora Explorer si aspetta una variabile al volo. Inoltre, se il bit GCS_UNICODE è settato, bisogna ritornare una stringa Unicode. Il codice di GetCommandString() è:
#include <atlconv.h> // macro per la conversione di stringhe ATL
HRESULT CSimpleShlExt::GetCommandString (UINT idCmd, UINT uFlags, UINT* pwReserved,
LPSTR pszName, UINT cchMax)
{
USES_CONVERSION;
// controlla idCmd, deve essere 0 poiché abbiamo solo un elemento nel menù
if ( 0 != idCmd )
return E_INVALIDARG;
// Se Explorer richiede una stringa, la copia nel buffer
if ( uFlags & GCS_HELPTEXT )
{
LPCTSTR szText = _T("This is the simple shell extension's help");
if ( uFlags & GCS_UNICODE )
{
// Bisogna portare pszName in formato Unicode, e poi copiarlo
lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );
}
else
{
// usa l'API per ritornare la stringa
lstrcpynA ( pszName, T2CA(szText), cchMax );
}
return S_OK;
}
return E_INVALIDARG;
}
Niente di strano. Si prende la stringa e la si converte nel giusto character set. Se non si ha mai usato le macro ATL, sarebbe meglio farlo, poiché rendono la vita molto semplice nel passare stringhe Unicode a metodi COM e funzioni OLE. Nel codice si usano T2CW e T2CA per convertire il formato TCHAR rispettivamente in Unicode e ANSI. La macro USES_CONVERSION all'inizio della funzione dichiara una variabile locale usata dalle macro.
Una cosa importante da notare è che l'API lstrcpyn() garantisce che la stringa di destinazione sia chiusa. Questo è differente dalla funzione CRT strncpy() , che non termina la stringa se la stringa sorgente è più grande o uguale a cchMax . Consiglio di usare lstrcpyn() , in modo da non dover sempre controllare che le stringhe siano terminate.
Eseguire la scelta dell'utente
L'ultimo metodo in IContextMenu è InvokeCommand() . Questo metodo viene eseguito se l'utente clicca su un elemento che abbiamo aggiunto. Il prototipo di InvokeCommand() è:
HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );
La struttura CMINVOKECOMMANDINFO contiene un sacco di informazioni, ma per i nostri scopi, ci occuperemo solamente di lpVerb e hwnd . lpVerb svolge un doppio compito - è sia il nome della "parola" chiamata, oppure un indice dell'elemento selezionato. hwnd è il puntatore alla finestra di Explorer nella quale viene chiamata l'extension.
Poiché c'è solo un elemento, controlleremo lpVerb , se è zero, il nostro elemento è stato cliccato. La cosa più semplice è mostrare una message box, ed è ciò che fa il codice. La message box mostra il nome del file selezionato, per mostrare che funziona.
HRESULT CSimpleShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo )
{
// se lpVerb punta ad una stringa, ignorare questa funzione
if ( 0 != HIWORD( pCmdInfo->lpVerb ))
return E_INVALIDARG;
switch ( LOWORD( pCmdInfo->lpVerb ))
{
case 0:
{
TCHAR szMsg [MAX_PATH + 32];
wsprintf ( szMsg, _T("The selected file was:\n\n%s"), m_szFile );
MessageBox ( pCmdInfo->hwnd, szMsg, _T("SimpleSh"), MB_ICONINFORMATION );
return S_OK;
}
break;
default:
return E_INVALIDARG;
break;
}
}
Registrare la shell extension
Adesso abbiamo implementato tutte le interfacce COM. Ma come far capire a Explorer di usare la nostra extension? ATL automaticamente genera il codice che registra la nostra DLL con un server COM, ma questo permette solamente alle altre applicazioni di usare la nostra DLL. Per dire a Explorer che esiste la nostra extension, bisogna registrarla nella chiave che contiene le informazione sui file di testo:
HKEY_CLASSES_ROOT\txtfile
In questa chiave, la sottochiave ShellEx contiene una lista di shell extensions che vengono invocate sui file di testo. In ShellEx , la sottochiave ContextMenuHandlers contiene una lista delle extension del menù contestuale. Ogni extension crea una sottochiave in ContextMenuHandlers e imposta il valore di questa chiave con il suo GUID. Nel nostro caso, creeremo questa chiave:
HKEY_CLASSES_ROOT\txtfile\ShellEx\ContextMenuHandlers\SimpleShlExt
e imposteremo il valore di default a "{5E2121EE-0300-11D4-8D3B-444553540000}".
Non ci sarà bisogno di scrivere alcun codice per fare ciò. Nella lista dei file c'è un file chiamato SimpleShlExt.rgs. Questo file è analizzato da ATL, e gli dice che chiavi aggiungere al registro e quali cancellare. Ecco come specifichiamo le chiavi da aggiungere:
HKCR
{
NoRemove txtfile
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove SimpleShlExt = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
}
}
}
}
Ogni riga è il nome di una chiave del registro, dove "HKCR" è l'abbreviazione di HKEY_CLASSES_ROOT . La parola NoRemove indica che la chiave non deve essere cancellata quando il servizio viene disinstallato. L'ultima linea è un po' più complicata. La parola ForceRemove significa che se la chiave è già esistente, verrà cancellata prima di scrivere quella nuova. Il resto della linea specifica una stringa (indicata da "s") scritta nel valore di default della sottochiave SimpleShlExt .
Adesso c'è bisogno di fare il punto della situazione. Noi registriamo l'extension sotto HKCR\txtfile . Però questa potrebbe non essere la chiave predefinita per i file di testo. Se si va a vedere in HKCR\.txt , si trova il vero nome della chiave che contiene le informazioni sui file di testo. Ci sono due effetti collaterali:
- "txtfile" potrebbe non essere la chiave giusta.
- Altri editor di testo potrebbero modificare i valori in
HKCR\.txt , che se modificati bloccheranno tutte le shell extension esistenti.
Personalmente mi sembra un difetto. Anche per la Microsoft, visto che ultimamente ha creato extension, tipo QueryInfo, che si possono registrare in .txt .
C'è un ultimo dettaglio. In NT/2000, bisogna mettere la nostra estensione in una lista "approved", altrimenti non verrà caricata senza i privilegi di administrator. La lista si trova in:
HKLM\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved
In questa chiave verrà creata una stringa contenente la nostra GUID. Il nome può essere qualsiasi cosa. Il codice per tutto ciò va messo nelle funzioni DllRegisterServer() e DllUnregisterServer() . Non mostrerò il codice poiché sono semplicemente accessi al registro, ma saranno presenti negli esempi.
Debuggare la shell extension
In seguito, si potrebbero scrivere delle extension non molto semplici, e ci sarà bisogno di farne il debug. Nei settaggi del progetto, in Debug, mettere la path di Explorer in "Executable for debug session", per esempio "C:\windows\explorer.exe". In NT o 2000, se si è aggiunta la chiave DesktopProcess precedentemente descritta, alla pressione di F5, si aprirà una nuova finestra di Explorer per il debug. Fino a che si lavora in questa finestra, non ci sono problemi per fare il rebuild della DLL in seguito, poiché quando si chiuderà la finestra, l'extension verrà terminata.
In Windows 9x, si dovrà terminare la shell prima di eseguire il debug. Cliccando su Start, poi Shut Down. Premendo Ctrl+Alt+Shift e poi Cancel. Questo chiuderà Explorer, e la barra di stato scomparirà. Adesso in MSVC basterà premere F5 per iniziare il debug. Per fermare il debug, premere Shift+F5 per chiudere. Fatto questo, basterà eseguire Explorer dal prompt per far ripartire la shell.
Come è il risultato?
Dopo l'aggiunta dei nostri elementi, il menù contestuale si presenta così:
E vediamo il nostro elemento per il menù. E qui è come Explorer visualizza il programma sulla barra di stato:
E questa è la message box, che mostra il nome del file selezionato:
|