??????

一個插件就是一個位于屏幕上的窗口,每個插件負責自己的繪制和對用戶輸入的響應。插件可以通過給自己的父窗口(桌面窗口)發送TODAYM_DRAWWATERMARK 消息,委托父窗口為自己繪制背景,也就是通過把自己的HDC傳遞給shell來完成的,這樣插件就看起來好像是“透明”的效果。同時,shell 也負責在相鄰的插件之間繪制一條分割線。
通常,PPC 最多允許加載 12 個插件。最大插件數量是由 K_cTodayItemsMax 定義的。
對于自定義的插件,要求開發者提供一個DLL函數并注冊到注冊表: HKLM\Softeware\Microsoft\Today\Items;
如下圖,我們使用遠程注冊表查看工具打開一個插件的在注冊表中的位置:
??????

在下面包含了所有今日插件的鍵。每個插件將含有下列的值:?
◆Type (DWORD);
對自定義插件來說,等于4。它是 SDK 中的插件類型枚舉中的一個值( tlitCustom)。?
typedef enum _TODAYLISTITEMTYPE {?
tlitOwnerInfo = 0,?
tlitAppointments,?
tlitMail,
tlitTasks,
tlitCustom, //自定義插件 = 4
tlitNil
} TODAYLISTITEMTYPE;
◆Enabled;
插件是否啟用。用戶能夠在設置-今日-項目中進行啟用或禁用。
◆Options;
是否含有設置對話框。也就是設置-今日-項目中插件被選中時的 “選項按鈕” 的 Enabled 狀態。
◆DLL:
插件dll的路徑。
◆Selectability;
可選項,插件是否可以被選中(用戶在屏幕上按導航鍵時)。通常為1,表示允許被選中。當允許選中時,用戶按上下方向鍵,被選中的插件背景會高亮。如果不能選中,就會跳過該插件。
◆Order;
可選項,插件顯示循序的排序值。缺省時由系統自動設置。
下面我們再介紹插件的協議,也就是插件的DLL應當滿足以下要求。
(1)要求 dll 導出序號為 240 的以下函數,以初始化和創建插件窗口;
??????#define ORDINAL_INITIALIZEITEM????? 240
typedef HWND (*PFNCUSTOMINITIALIZEITEM)(TODAYLISTITEM *, HWND);
參數1:TODAYLISTITEM 結構的指針,包含了該插件在系統中注冊的相關信息。
參數2:桌面窗口的句柄,它將成為插件窗口的父窗口。
(2)如果插件具有設置對話框,則要求dll導出序號為 241 的以下函數,作為設置對話框的窗口過程;
??????#define ORDINAL_OPTIONSDIALOGPROC?? 241
typedef BOOL (*PFNCUSTOMOPTIONSDLGPROC)(HWND, UINT, UINT, LONG);
同時要求dll 提供資源ID為 500的一個對話框資源作為設置對話框的模板。(可以通過手工修改 resource.h 中的定義)
(3)shell 將向插件窗口發送以下信息,要求插件處理這些消息;
??????WM_TODAYCUSTOM_CLEARCACHE
告知插件正在被從顯示中卸載,要求插件清理自己所維護的數據緩存。
??????WM_TODAYCUSTOM_QUERYREFRESHCACHE
此消息在桌面顯示期間以每2秒鐘一次的頻率周期性對所有插件發送。詢問插件是否需要進行更新。
在插件首次加載時,還要求插件告知系統插件的高度以對插件窗口進行布局。由于插件窗口被上下垂直分布,所以寬度對于系統屬于已知的。如果返回TRUE,表示要求進行更新。如果不需要更新返回FALSE。
(4)同時插件還能夠向父窗口發送以下消息,以輔助繪制。
??????TODAYM_GETCOLOR
詢問系統當前使用的前景色,背景色,高亮前景色等信息。以便繪制時,和系統使用的主題風格保持一致。
??????TODAYM_DRAWWATERMARK
要求shell 為插件繪制背景。也就是把背景位圖復制到插件窗口的背景。
好了,關于插件開發規則我們就簡要介紹到此。在開發插件時,開發者的主要任務是編寫插件窗口\設置對話框的窗口過程,完成屬于自己的功能。這里要求具有的是windows開發的一些基礎。我們不細作介紹了。下面是我在這幾天制作的插件。
(1)根據 SDK 中 范例改編而成的 memWatcher 插件 和 “桌面便箋”。效果如下:
??????



在左圖是SDK中的 memWatcher 范例在模擬器中的顯示效果,右圖是經過我適當改寫后,在實際HTC S1中運行的效果。SDK范例顯示了程序和存儲的百分比,并且創建了兩個進度條窗口顯示。經過我的改寫,我把進度條去掉了,從而可以把信息壓縮到一行以內,這樣可以節省屏幕空間,并且增加了電池電量的顯示。
獲取這些信息的相關API函數是:
??????GlobalMemoryStatus,GetStoreInformation,GetSystemPowerStatusEx;
下面我們看下桌面便箋插件,這個插件發布在 pdafans 論壇后,很快就有網友向我反饋了備忘錄的內容在重啟后消失,這是因為我做的這個插件也僅僅是個范例來使用,測試了我的想法是可行的,所以并沒有考慮那么多。便箋的內容被放到了內存里,聲明周期和DLL一樣,這也一旦DLL被卸載,存儲在內存中的內容也就失去了。這也提醒了我們一點,我們的插件應該將數據持久化。所以我又修改了這個插件,把備忘信息和圖標索引存儲到了注冊表中,也就是插件注冊的鍵下面新增了兩個值。這樣我們就可以保證每次插件啟動時都會從注冊表中讀取出上次的用戶記錄的內容。
當用鼠標點擊桌面便箋時,就會彈出一個對話框用于設置新的備忘內容,如下圖所示:
??????

這個對話框中具有一點難度和技巧性的是上面的圖標選擇反饋,全部是通過鼠標點擊事件來完成的。我們在對話框的 WM_ONPAINT 消息處理中,在對話框上繪制了所有可選圖標,每個圖標實際上是16*16像素大小,所以我指定的網格是20*20像素,在每個網格中繪制一個圖標,并對被選中圖標繪制了一個藍色矩形框表示選中狀態。當鼠標點擊到其他圖標時,我們就要更新這個藍色矩形。同時我們也要根據鼠標位置在網格中正確的定位要當前位置選中的鼠標索引。這里的處理并不算非常難,但是需要少許的耐心。
顯示和隱藏輸入面板,在 .NET CF中,有一個inputPanel控件,我們 可以方便的設置它的Visible屬性去控制。而在EVC中,我們是通過下面的API函數去顯示或者隱藏SIP的。
??????SipShowIM(SIPF_ON) 和 SipShowIM(SIPF_OFF);
或者我們也可以使用:SHSipPreference(hDlg, SIP_UP) ,去要求 Shell 浮出輸入面板。
(2)桌面記單詞插件。
桌面記單詞插件的靈感是來自桌面上的類似工具,即有一個頂層窗口,以一個固定的頻率切換詞條顯示,以幫助用戶背單詞。我這里就是模擬這種軟件的效果做的一個今日插件。當然它不僅僅可以背單詞,也可以顯示其他字典內容,例如唐詩宋詞,名言名句等等。用戶可以自定義字典文件,本質上就是一個文本文件,并通過修改配置文件把字典添加進來。
為了降低讀文件的頻率,我在插件內維護了一個詞條緩存(緩存10個詞條),每次一次性嘗試從文件中加載10個詞條文件(每個詞條也就是文本文件中的一行)到內存中。當詞條正在滾動期間,文件保持打開狀態。當暫停滾動時,將會關閉文件。插件利用每2秒鐘接收到的消息去滾動詞條。效果如下圖所示:
??????


開發這個插件時,我忽然發現 Pocket PC 的操作系統是不支持讀寫 ini 文件的相關API函數的。我去網絡上找了下相關代碼,但是沒有看到特別滿意的。因此我自己用C語言寫了幾個和API函數功能相同的讀 ini 文件的函數。函數命名也是完全相同的,為了在 PC上進行測試,我在每個函數名前面加了 Ce ,以和系統的API函數區分開。我這里僅僅為了插件功能寫了有限的幾個函數,這里以 CeGetPrivateProfileString 為例給出代碼。
在PC上,這個函數負責讀取 ini 文件某個 section 中某個 key 的值。為了同時在 unicode 和 多字節字符串環境中適用,我又把相關的文件和字符串操作函數進行了宏定義,并且以這種方式命名:?
??????t_多字節版本函數名;
這是因為對多字節版本的函數我們通常更加熟悉它的命名。例如對于 t_strcpy 等等。下面是這個函數的代碼:


#ifdef??UNICODE???//?r_winnt
????#define?t_fopen?????????_wfopen
????#define?t_fgets????????????fgetws
????#define?t_sprintf????????swprintf????//格式化文本
????#define?t_strcpy????????wcscpy
????#define?t_strncpy????????wcsncpy????????//拷貝指定個數的字符
????#define?t_strcat????????wcscat????????//append?a?string
????#define?t_strtol????????wcstol
????#define?t_strlen????????wcslen
????#define?t_strcmp????????wcscmp
????#define?t_stricmp????????_wcsicmp????//忽略大小寫的字符串比較
????#define?t_strncmp????????wcsncmp????????//比較n個字符
????#define?t_strchr????????wcschr????????//find?a?character?in?a?string
????#define?t_strrchr????????wcsrchr????????//從結尾向前查找字符
#else??//ASCII?CODE
????#define?t_fopen?????????fopen
????#define?t_fgets????????????fgets????????//讀取一行文本
????#define?t_sprintf????????sprintf????????//格式化文本
????#define?t_strcpy????????strcpy
????#define?t_strncpy????????strncpy????????//拷貝指定個數的字符
????#define?t_strcat????????strcat????????//append?a?string
????#define?t_strtol????????strtol????????//把字符串轉換成long(int32)
????#define?t_strlen????????strlen
????#define?t_strcmp????????strcmp????????//比較字符串
????#define?t_stricmp????????_stricmp????//忽略大小寫的字符串比較
????#define?t_strncmp????????strncmp????????//比較n個字符
????#define?t_strchr????????strchr????????//查找字符
????#define?t_strrchr????????strrchr????????//從結尾向前查找字符
#endif
#define?LINESIZE????260????//行緩沖區大小
//從appname(section)中讀取string類型key
DWORD?CeGetPrivateProfileString(
????LPCTSTR?lpAppName,????????????????//section?name:?[lpAppName]
????LPCTSTR?lpKeyName,????????????????//lpKeyName=lpReturnedString
????LPCTSTR?lpDefault,????????????????//未找到時的默認值
????LPTSTR?lpReturnedString,????//[out]?查找到的結果
????DWORD?nSize,????????????????????????????//[in]lpReturnedString的字符數,注意單位不是字節!
????LPCTSTR?lpFileName
????)
{
????DWORD?ret?=?0;
????FILE?*stream;
????bool?bFindVal?=?false;
????bool?bFindSection?=?false;
????TCHAR?line[?LINESIZE?];
????size_t?sectionLength,?keyLength,?lineLength;
????
????stream?=?t_fopen(lpFileName,?_T("r"));
????if(stream?==?NULL)
????{
????????//設置默認值
????????t_strcpy(lpReturnedString,?lpDefault);
????????ret?=?t_strlen(lpReturnedString);?
????????return?ret;
????}
????
????sectionLength?=?t_strlen(lpAppName);
????
????while(t_fgets(line,?LINESIZE,?stream)?!=?NULL)
????{
????????//忽略注釋行和空行
????????if(line[0]?==?0?||?line[0]?==?';')?continue;
????????lineLength?=?t_strlen(line);
????????//注意:把LF(0xa)字符替換成0,這在UNICODE環境下可能出現結尾是LF)
????????if(line[?lineLength?-?1?]?==?0x0a)
????????{
????????????line[?lineLength?-?1?]?=?0;
????????????lineLength--;
????????????//注意此時可能會成為空字符串
????????????if(lineLength?==?0)?continue;
????????}
????????
????????//嘗試尋找到?section
????????if(!bFindSection)
????????{
????????????if(line[0]?!=?'[')?continue;?//本行是否是?[section]
????????????//這里是我們想要的Section嗎?
????????????//檢查這一行的寬度是否正好是section長度加2,?[lpAppName]
????????????if(line[sectionLength?+?1]?!=?']')?continue;
????????????if(t_strncmp(line+1,?lpAppName,?sectionLength)?!=?0)?continue;
????????????//Now?Section?will?appear?on?next?line

????????????//讀取section前求出?Key?的長度
????????????keyLength?=?t_strlen(lpKeyName);
????????????bFindSection?=?true;????????????
????????????continue;
????????}
????????
????????//查找Key,?Section?End?
????????if(line[0]=='[')?break;?//遇到了下一個
????????????
????????if(lineLength?<?keyLength+1?||?line[keyLength]?!=?'=')?continue;?//"KeyName=

????????if(t_strncmp(line,?lpKeyName,?keyLength)!=0)?continue;
????????//Now?We?Get?the?Key!?
????????t_strcpy(lpReturnedString,?line?+?keyLength?+?1);
????????//Now?It's?done.
????????bFindVal?=?true;
????????break;
????}
????
????fclose(stream);
????if(!bFindVal)
????{
????????//設置默認值
????????t_strcpy(lpReturnedString,?lpDefault);?
????}
????ret?=?t_strlen(lpReturnedString);?
????return?ret;
}
下面是我提供了一個演示程序,由于我們知道了插件的協議,所以我們也可以顯示出其他插件的選項對話框,為了更具可讀性,代碼經過了精簡。


TCHAR?path[256];
GetDlgItemText(hDlg,?IDC_DLLPATH,?path,?256);
//?load?dll
g_PluginModule?=?LoadLibrary(path);
//?get?dlgproc?address 窗口過程函數的導出序號是241
g_PluginProc?=?(DLGPROC)GetProcAddress(g_PluginModule,?(LPCTSTR)241);
//?create?options?dlg
g_PluginDlg?=?CreateDialog(g_PluginModule,?(LPCTSTR)MAKEINTRESOURCE(500),?NULL,?g_PluginProc);
ShowWindow(g_PluginDlg,?SW_SHOW);
SetWindowPos(g_PluginDlg,?NULL,?30,?80,?0,?0,?SWP_NOSIZE);
return?TRUE;
運行效果如圖所示:

最后我們開發好插件以后,可以利用SDK提供的打包工具,把插件制作成 cab 包,這樣復制到設備上即可自動安裝。打包是使用SDK提供的工具完成,但是我們首先需要自己為我們的軟件編寫一個 inf 文件,描述軟件的發裝過程。inf文件詳細描述了需要拷貝的文件清單,源目錄,目標目錄,要添加的注冊表信息等內容。這里可以參考 SDK中的范例,細節就不再描述了。這里我使用 mymemo 的 inf 文件做一個例子說明:為了更具可讀性,文件內容經過了精簡。


[Version]
Signature???=?"$Windows?NT$"
Provider????=?"Microsoft"
CESignature?=?"$Windows?CE$"
[CEStrings]
AppName?????=?"MyMemo"
InstallDir??=?%CE2%?????????????;?"\Windows"?
[CEDevice]
UnsupportedPlatforms????=?"HPC","Jupiter","Palm?PC2"
VersionMin?????????=?3.0
VersionMax????????=?6.0
[PPC2003_Device]
ProcessorType???????????=?2577??????;?ARM?CPU
[SourceDisksNames.PPC2003_Device]
1?=?,"ARM?Files",,ARMV4Rel
[SourceDisksFiles]
"mymemo.dll"????????????????????=?1?????;?the?Today?plugin?dll
[DestinationDirs]
Files.Windows???????=?0,%CE2%???????????;?"\Windows"?directory
[Files.Windows]
"mymemo.dll","mymemo.dll",,0x00000001
[Reg.Version1]
HKLM,Software\Microsoft\Today\Items\%AppName%,Enabled,0x00010001,0
HKLM,Software\Microsoft\Today\Items\%AppName%,Type,0x00010001,4
HKLM,Software\Microsoft\Today\Items\%AppName%,Options,0x00010001,0
HKLM,Software\Microsoft\Today\Items\%AppName%,Selectability,0x00010001,1
HKLM,Software\Microsoft\Today\Items\%AppName%,IconIndex,0x00010001,0
HKLM,Software\Microsoft\Today\Items\%AppName%,DLL,0x00000002,"%InstallDir%\mymemo.dll"
HKLM,Software\Microsoft\Today\Items\%AppName%,Memo,0x00000002,"Type?here?to?input?memo
"
HKLM,Software\Microsoft\Today\Items\%AppName%,Memo,0x00000002,"Type?here?to?input?memo

打包工具是一個命令行程序,我們執行以下命令:
cabwiz mymemo.inf? /err errinfo.txt? /cpu PPC2003_Device
??????其中,/err選項指定錯誤輸出文件,當打包失敗時,這是診斷問題的重要信息。
/cpu選項指定是inf文件中定義過的CPU類型,如果在inf文件中定義了多種CPU類型,可以同時為多種CPU打包,所以一個inf文件可以多用。
最后我們給出相關的下載連接:
(1)程序存儲電量百分比顯示和桌面便箋插件的CAB包下載鏈接:
??????http://files.cnblogs.com/hoodlum1980/PPCCAB_MyMemo_MemWatcher.rar
(2)桌面記單詞插件的CAB包下載鏈接:
??????http://files.cnblogs.com/hoodlum1980/Recite_CAB_ARMV4.rar
(3)然后在給出一個我以前寫的C語言的俄羅斯方塊(最早發表在編程論壇),移植到PPC上的版本:
??????http://files.cnblogs.com/hoodlum1980/Tetris.rar
運行效果截圖:
??????


(4)最后我們給出本文提及所有源代碼的合集下載連接,全部使用EVC4.0使用C++開發。每個插件包含了用于打包的 inf 文件。
??????http://files.cnblogs.com/hoodlum1980/TodayPlugins.rar