一、需求
無論何時,當你在Explorer視窗中建立、刪除或重新命名一個檔案夾/檔案,或者插入拔除移動儲存空間時,Windows總是能非常快速地更新它所有的視圖。有時候我們的程式中也需要這樣的功能,以便當使用者在Shell中作出建立、刪除、重新命名或其他動作時,我們的應用程式也能快速地隨之更新。
二、原理
Windows內部有兩個未公開的函數(註:在最新的MSDN中,已經公開了這兩個函數),分別叫做SHChangeNotifyRegister和SHChangeNotifyDeregister,可以實現以上的功能。這兩個函數位於Shell32.dll中,是用序號方式匯出的。這就是為什麼我們用VC內建的Depends工具察看Shell32.dll時,找不到這兩個函數的原因。SHChangeNotifyRegister的匯出序號是2;而SHChangeNotifyDeregister的匯出序號是4。
SHChangeNotifyRegister可以把指定的視窗添加到系統的訊息監視鏈中,這樣視窗就能接收到來自檔案系統或者Shell的通知了。而對應的另一個函數,SHChangeNotifyDeregister,則用來取消監視鉤掛。SHChangeNotifyRegister的原型和相關參數如下:
ULONG SHChangeNotifyRegister
(
HWND hwnd,
int fSources,
LONG fEvents,
UINT wMsg,
Int cEntries,
SHChangeNotifyEntry *pfsne
);
其中:
hwnd
將要接收改變或通知訊息的視窗的控制代碼。
fSource
指示接收訊息的事件類型,將是下列值的一個或多個(註:這些標誌沒有被包括在任何標頭檔中,使用者須在自己的程式中加以定義或者直接使用其對應的數值)
SHCNRF_InterruptLevel
0x0001。接收來自檔案系統的中斷層級通知訊息。
SHCNRF_ShellLevel
0x0002。接收來自Shell的Shell層級通知訊息。
SHCNRF_RecursiveInterrupt
0x1000。接收目錄下所有子目錄的中斷事件。此標誌必須和SHCNRF_InterruptLevel 標誌合在一起使用。當使用該標誌時,必須同時設定對應的SHChangeNotifyEntry結構體中的fRecursive成員為TRUE(此結構體由函數的最後一個參數pfsne指向),這樣通知訊息在分類樹上是遞迴的。
SHCNRF_NewDelivery
0x8000。接收到的訊息使用共用記憶體。必須先調用SHChangeNotification_Lock,然後才能存取實際的資料,完成後調用SHChangeNotification_Unlock函數釋放記憶體。
fEvents
要捕捉的事件,其所有可能的值請參見MSDN中關於SHChangeNotify函數的註解。
wMsg
產生對應的事件後,發往視窗的訊息。
cEntries
pfsne指向的數組的成員的個數。
pfsne
SHChangeNotifyEntry結構體數組的起始指標。此結構體承載通知訊息,其成員個數必須設定成1,否則SHChangeNotifyRegister或者SHChangeNotifyDeregister將不能正常工作(但是據我實驗,如果cEntries設為大於1的值,依然可以註冊成功,不知何故)。
如果函數調用成功,則返回一個整型註冊標誌號,否則將返回0。同時系統就會將hwnd指定的視窗加入到操作監視鏈中,當有檔案操作發生時,系統會向hwnd標識的視窗發送wMsg指定的訊息,我們只要在程式中加入對該訊息的處理函數就可以實現對系統操作的監視了。
如果要退出程式監視,就要調用另外一個未公開得函數SHChangeNotifyDeregister來取消程式監視。該函數的原型如下:
BOOL SHChangeNotifyDeregister(ULONG ulID);
其中ulID指定了要登出的監視註冊標誌號,如果卸載成功,返回TRUE,否則返回FALSE。
三、執行個體
在前一文中,我們派生了一個支援檔案拖放的清單控制項CListCtrlEx,但它還有一個小小的缺憾,就是當使用者把一個檔案拖放進來之後,如果使用者在Shell中對該檔案進行移動、刪除、重新命名等操作時,CListCtrlEx將不能保證即時更新。經過上面的討論,現在就讓我們為CListCtrlEx加上即時檔案監控功能。
在使用這兩個函數之前,必須要先聲明它們的原型,同時還要添加一些宏和結構定義。我們在原工程中添加一個ShellDef.h標頭檔,然後加入如下聲明:
#define SHCNRF_InterruptLevel 0x0001 //Interrupt level notifications from the file system
#define SHCNRF_ShellLevel 0x0002 //Shell-level notifications from the shell
#define SHCNRF_RecursiveInterrupt 0x1000 //Interrupt events on the whole subtree
#define SHCNRF_NewDelivery 0x8000 //Messages received use shared memory
typedef struct
{
LPCITEMIDLIST pidl; //Pointer to an item identifier list (PIDL) for which to receive notifications
BOOL fRecursive; //Flag indicating whether to post notifications for children of this PIDL
}SHChangeNotifyEntry;
typedef struct
{
DWORD dwItem1; // dwItem1 contains the previous PIDL or name of the folder.
DWORD dwItem2; // dwItem2 contains the new PIDL or name of the folder.
}SHNotifyInfo;
typedef ULONG
(WINAPI* pfnSHChangeNotifyRegister)
(
HWND hWnd,
int fSource,
LONG fEvents,
UINT wMsg,
int cEntries,
SHChangeNotifyEntry* pfsne
);
typedef BOOL (WINAPI* pfnSHChangeNotifyDeregister)(ULONG ulID);
這些宏和函數的聲明,以及參數含義,如前所述。下面我們要在CListCtrlEx體內添加兩個函數指標和一個ULONG型的成員變數,以儲存函數地址和返回的註冊號。
接下來我們為CListCtrlEx類添加一個公有成員函數Initialize,在其中,我們首先進行載入Shell32.dll以及初始化函數指標動作,接著調用註冊函數向Shell註冊。使用者在使用我們提供的CListCtrlEx類時,應當在CListCtrlEx建立完畢後跟著調用Initialize函數以確保註冊了訊息監視鉤子。否則將失去即時監視功能。初始化函數如下(已略去涉及OLE初始化的部分):
BOOL CListCtrlEx::Initialize()
{
…………
//載入Shell32.dll
m_hShell32 = LoadLibrary("Shell32.dll");
if(m_hShell32 == NULL)
{
return FALSE;
}
//取函數地址
m_pfnDeregister = NULL;
m_pfnRegister = NULL;
m_pfnRegister = (pfnSHChangeNotifyRegister)GetProcAddress(m_hShell32,MAKEINTRESOURCE(2));
m_pfnDeregister = (pfnSHChangeNotifyDeregister)GetProcAddress(m_hShell32,MAKEINTRESOURCE(4));
if(m_pfnRegister==NULL || m_pfnDeregister==NULL)
{
return FALSE;
}
SHChangeNotifyEntry shEntry = {0};
shEntry.fRecursive = TRUE;
shEntry.pidl = 0;
m_ulNotifyId = 0;
//註冊Shell監視函數
m_ulNotifyId = m_pfnRegister(
GetSafeHwnd(),
SHCNRF_InterruptLevel|SHCNRF_ShellLevel,
SHCNE_ALLEVENTS,
WM_USERDEF_FILECHANGED, //自訂訊息
1,
&shEntry
);
if(m_ulNotifyId == 0)
{
MessageBox("Register failed!","ERROR",MB_OK|MB_ICONERROR);
return FALSE;
}
return TRUE;
}
在CListCtrlEx類中再添加如下函數。該函數的作用是從PIDL中解出實際字元路徑。
CString CListCtrlEx::GetPathFromPIDL(DWORD pidl)
{
char szPath[MAX_PATH];
CString strTemp = _T("");
if(SHGetPathFromIDList((struct _ITEMIDLIST *)pidl, szPath))
{
strTemp = szPath;
}
return strTemp;
}
現在我們的程式就可以接收到來自Shell或檔案系統的通知訊息了,只要編寫一個處理自訂訊息的函數,就可以對系統範圍內的更改動作作出我們希望的響應動作。這裡lParam參數中存放的是當前發生的事件(譬如SHCNE_CREATE),wParam參數中存放的是SHNotifyInfo結構。下面是一段架構代碼:
LRESULT CListCtrlEx::OnFileChanged(WPARAM wParam, LPARAM lParam)
{
CString strOriginal = _T("");
CString strCurrent = _T("");
SHNotifyInfo* pShellInfo = (SHNotifyInfo*)wParam;
strOriginal = GetPathFromPIDL(pShellInfo->dwItem1);
if(strOriginal.IsEmpty())
{
return NULL;
}
switch(lParam)
{
case SHCNE_CREATE:
break;
case SHCNE_DELETE:
break;
case SHCNE_RENAMEITEM:
break;
}
return NULL;
}
四、總結
至此我們完成了對CListCtrlEx的擴充工作,通過這兩個未公開的API函數,編寫一個檔案夾/檔案監視器不再是一件難事。當然同樣的功能也可以通過編寫裝置驅動程式來,但這種方法來實現,但是難度大,周期長,開發上也有不少困難。