这篇文章继续设计并编写一个Windows Mobile 6.5今日界面,介绍the Legacy Today Screen Plugin。
在文章Windows Mobile多媒体开发总结之Media Player Plugins和Windows Mobile多媒体开发总结之Media Player Plugins(续)中提到过你可以实现一个Today插件(我们姑且叫做Media Player Today Plugin)来与Media Player Plugin通信,进而达到让用户在Today界面就可以获得Media Player信息和简单控制Media Player。该主意早已实现,你能够在网络上经常看到这样的插件。这篇文章就介绍该插件的设计和编写,知道设计思路后你也可以以其它方式实现,并不一定局限于Media Player Today Plugin。
比如可以实现一个服务用于从网上获得天气信息、最新新闻、游戏信息(比如网页游戏)等(使用C++编写与网络有关的应用难度较大,可以使用C#开发一个没界面的Application,或者使用widget),然后将数据传递给你的一个Today Plugin或者Today Application。
这篇文章仅仅带你实现最基本的功能,如果你想做的更好,我建议:添加更多的面板(用户使用向左或者向右的手势切换),增加面板切换Animation(面板滚动、渐变消失),支持换肤功能等等。
具体实现我们遇到3个问题:
1.如何编写the Legacy Today Screen Plugin,既能绚丽又能有很好的运行效率问题?这个问题在文章中有一些介绍。这篇文章就来次实践吧。我会介绍我的滚动字幕实现的思路以及解决闪烁的方法。为了优化效率,我们会稍微深入一下Today的窗口系统以及窗口消息。
2.如何与Media Player Plugin通信?
3.如何调试你编写的Media Player Today Plugin?
因为前面开发过插件,凭着记忆我自己重新设计了UI,因为只有一点平面设计基础,捣鼓了半天Adobe Photoshop和Adobe Illustator才搞出你看到的这个界面:
这里有可以使用现成的一些设计资源:
http://www.teehanlax.com/blog/?p=1628
http://320480.com/
http://graffletopia.com/stencils/413
插件必须导出的函数是这个函数(我们知道DLL还有DllMain入口):
HWND APIENTRY InitializeCustomItem ( TODAYLISTITEM *ptli, HWND hwndParent );
我们在这个函数里面初始化资源,创建插件自己的窗口,并且显示窗口:
/*************************************************************************/ /* Initialize the DLL by creating a new window */ /*************************************************************************/ HWND InitializeCustomItem(TODAYLISTITEM *ptli, HWND hwndParent) { long lNotifyIndx; LPCTSTR appName = (LPCTSTR)LoadString(g_hInst,IDS_WMPPLUGIN_APPNAME,0,0); LoadBitmapRes(); //create a new window g_hWnd = CreateWindow(appName,appName,WS_VISIBLE | WS_CHILD, CW_USEDEFAULT,CW_USEDEFAULT,0,0,hwndParent, NULL, g_hInst, NULL) ; //display the window ShowWindow (g_hWnd, SW_SHOWNORMAL); UpdateWindow (g_hWnd) ; // clear out our notification handles for (lNotifyIndx=0; lNotifyIndx < NOTIFY_CNT; lNotifyIndx++) { g_hNotify[lNotifyIndx] = NULL; } // register our State and Notification Broker notifications RegisterNotifications(); //initialize the g_WMPStarted value DWORD dwState = 3; if ( S_OK == RegistryGetDWORD(SN_MEDIAPLAYERSTATE_ROOT, SN_MEDIAPLAYERSTATE_PATH, SN_MEDIAPLAYERSTATE_VALUE, &dwState) ) { if (dwState != g_bWMPStarted) { g_bWMPStarted = dwState; } } else { g_bWMPStarted = FALSE; } return g_hWnd; }
这里有几个Today Plugin特有的消息:
WM_TODAYCUSTOM_QUERYREFRESHCACHE
这个消息发送给你,询问你的插件窗口是否要刷新,return TRUE表示需要,FALSE反之。这个消息发送的频率约为4s一次。Today用这样方式来维持界面处于最新状态。
还有其它消息,比如处理WM_TODAYCUSTOM_RECEIVEDSELECTION消息来得的高亮状态,在这里不再详细说明,需要的时候你可以查看文档。
另外你可以在WM_ERASEBKGND消息里面使用如下代码来实现透明的插件背景(其实是叫插件的父窗口使用Today的对应的背景来刷插件背景):
TODAYDRAWWATERMARKINFO dwi; dwi.hdc = (HDC)wParam; GetClientRect(hwnd, &dwi.rc);//你的插件所在Today界面上的位置 dwi.hwnd = hwnd; SendMessage(GetParent(hwnd), TODAYM_DRAWWATERMARK, 0,(LPARAM)&dwi);//叫Today窗口刷新指定的界面,也就是你插件所在的整个界面 return TRUE;
但是我这里使用自己的背景图片,所以你看到的是如下的代码:
// this fills in the background with defined image case WM_ERASEBKGND: { HDC hdc = (HDC)wParam; RECT rcClient = {0}; GetClientRect(hwnd, &rcClient); RECT rcMemDC = {0, 0, BKPIC_WIDTH, BKPIC_HEIGHT}; HDC hMemDC = CreateCompatibleDC(hdc); HBITMAP hBmp = CreateCompatibleBitmap(hdc, BKPIC_WIDTH, BKPIC_HEIGHT); HBITMAP hBmpOld = (HBITMAP)SelectObject(hMemDC, hBmp); DrawBackground(hMemDC, rcMemDC); BitBlt( hdc, rcClient.left, rcClient.top, rcClient.right-rcClient.left, rcClient.bottom-rcClient.top, hMemDC, rcMemDC.right-(rcClient.right-rcClient.left), 0, SRCCOPY ); SelectObject(hMemDC, hBmpOld); DeleteDC(hMemDC); DeleteObject(hBmp); } return TRUE;
前面我以为Today Plugin的窗口仅仅是桌面窗口的子窗口,之后发现自己错了。并且你也无法这样来获得插件的窗口句柄:
FindWindow( TEXT("WMPPlugin"), TEXT("WMPPlugin") );
使用Visual Studio自带的工具Windows CE Remote Spy帮你弄清真相。其实窗口结构是这样的:
0x00000000 WindowName:Desktop Window ClassName:None
0x7C073200 WindowName:Desktop ClassName:DesktopExplorerWindow GetDesktopWindow();
0x7C0736B0 WindowName:No name ClassName:Worker
0x7C073D60 WindowName:No name ClassName:Worker
0x7C077E30 WindowName:WMPPlugin ClassName:WMPPlugin //这里才是插件的窗口
另外通过该工具的Messages功能能很方便的监测到每个窗口收到的消息,便于窗口消息的调整,进而优化插件性能:
上面看到的WM_USER+243消息就是WM_TODAYCUSTOM_QUERYREFRESHCACHE消息,每隔4s左右你就会收到。除了这个消息,其它消息是我点击播放按钮时产生的。这里有个不好的地方是我使用了6个按钮,用户小小的一个动作,好家伙,一大堆消息要处理。这也就是为什么少使用控件的原因之一了(在要求运行效率的时候)。
.Net CF下能够开发Today Plugin的原因是因为它封装了上面介绍的东西,上面这些东西是更底层的。所以你使用C#开发时同样要注意上面提到的优化建议。
下面就是在.Net CF下创建的一个默认Application的窗口消息(点击窗口空白地方时产生的):
我们知道在Windows系统中进程间有很多通信方法:File Mapping, mailslot, pipe, DDE, COM, RPC, clipboard, socket, WM_COPYDATA,MsgQueue等等。
这里需要传输像歌曲名等信息,使用WM_COPYDATA比较适合,但是WM_COPYDATA要求进程是有窗口消息循环的,我们遇到的问题是找不到
Today Plugin的窗口句柄。所以最好的方法是使用命名的MsgQueue来通信。这时Media Player Today Plugin需要用单独的线程监测这个命名的MsgQueue,
怎么监测?使用WaitForMultipleObjects/WaitForSingleObject这样的API等待这个命名的MsgQueue的句柄。
我们知道这样的线程大部分时间因为MsgQueue无信号而被阻塞处于"Sleeping"状态,所以需要在另外一个线程而非UI主线程中等待,否则会导致用户界面被阻塞了。
看下代码:
DWORD ThreadProc() { HANDLE rgHandles[2]; // Set up our HANDLE array. rgHandles[0] = g_hMsgQueue; rgHandles[1] = g_hEventLifetime; // Loop endlessly. During each iteration of the loop, wait for one of // the two objects to become signaled. // // If g_hMsgQueue is signaled, then our Windows Media Player plugin // has a message for us regarding the status of Windows Media Player. // // If g_hEventLifetime is signaled, we're being asked to shut down, // so just return. for (;;) { DWORD dwObjSignaled; dwObjSignaled = WaitForMultipleObjects(2, rgHandles, /*fWaitAll=*/FALSE, INFINITE); if (dwObjSignaled == WAIT_OBJECT_0) { DWORD cbRead, dwFlags; MQMESSAGE msg; // We have a message from our Windows Media Player plugin. Copy the // information to our g_wmpinfo instance. if (ReadMsgQueue(g_hMsgQueue, &msg, sizeof(msg), &cbRead, INFINITE, &dwFlags) && cbRead == sizeof(msg)) { BOOL fStatusChanged, fTitleChanged; // Note that both SetStatus and SetTitle return TRUE if the value // we're passing actually changed. // // Therefore, if they both return FALSE, nothing really changed and // we can ignore this notification. // // Warning: don't "optimize" the code like this: // // if (g_wmpinfo.SetStatus(msg.status) || g_wmpinfo.SetTitle(msg.szMediaTitle)) // // to get rid of the two BOOL variables (fStatusChanged and fTitleChanged) // because BOTH methods need to be called. If you were to code it like that, // and if the SetStatus method returned TRUE, then the SetTitle method would // never be called due to the "short-circuit" behavior of the || operator. fStatusChanged = g_wmpinfo.SetStatus(msg.status); fTitleChanged = g_wmpinfo.SetTitle(msg.szMediaTitle); if (fStatusChanged || fTitleChanged) { // We tend to get a LOT of notifications from Media Player, so when we // get a notification, we actually set a short timer and don't invalidate // our plugin until the timer goes off. // // This helps to prevent 'flicker' in the display when Media Player gives // us notifications in a quick sequence like this: { playing, paused, playing, // paused, ... }. Those correspond to internal state changes in Media Player, // and we don't need to draw them all. // // Of course, we don't want to set a one-shot timer if we've already set // one, because then we'd get a slew of timer notifications, one for each // Media Player notification, which wouldn't solve the problem. if (!g_fTimerSet) { if (SUCCEEDED(g_pHpe->SetSingleShotTimer(g_hPlugin, CMSEC_INVALIDATE_TIMER))) { // Remember that we set the timer so we don't do it again until // AFTER it goes off. g_fTimerSet = TRUE; } else { // SetSingleShotTimer failed for some reason, so we're forced to just // invalidate here. g_pHpe->InvalidatePlugin(g_hPlugin, 0); } } } } } else { // This is probably our 'lifetime' event, telling us to shut down. It could // also be an error return from WaitForMultipleObjects, but in that case // we should just exit as well. return 0; } } }
题外话:WaitForMultipleObjects还有个不阻塞的版本MsgWaitForMultipleObjects/MsgWaitForMultipleObjectsEx:
我仍然嫌这么做麻烦了点,所以我用了更简单的通过注册表和自定义的窗口消息进行通信。下面主要说明这个思路。
Media Player Plugin -> Media Player Today Plugin
我们看到注册表中已经有记录当前Media Player所播放的歌曲的部分信息,只是这些信息是Media Player本身去维护的,而非Media Player Plugin,
但是我们可以让Media Player Plugin维护Media Player不负责的其它信息,比如Media Player当前状态、Media Player音量以及其它你感
兴趣的信息。
以下是注册表已经有的信息:
[HKEY_CURRENT_USER\System\State\MediaPlayer] "Elapsed"=dword:0002d9c7 //播放掉的时间 "TotalDuration"=dword:000316eb //总时间 "WM/TrackNumber"="0" "Bitrate"="128Kbps" "WM/Genre"="" "Title"="" //歌曲文件名 "WM/AlbumArtist"="" "WM/AlbumTitle"="" "WM/OriginalArtist"=""
让Media Player Today Plugin去监测这些键值,当变化时去做相应的处理,你会问怎么监测这些键值,Windows Mobile已经提供这样的API了, 建议你使用这些API而非轮训(轮总是不好的^^):
RegistryNotifyApp RegistryNotifyCallback RegistryNotifyMsgQueue RegistryNotifyWindow
下面的代码给个处理这些通知的演示(我使用了RegistryNotifyWindow,发送我自定义的消息WM_CHANGE_STATE):
case WM_CHANGE_STATE: { DWORD dwState = 3; if ( S_OK == RegistryGetDWORD(SN_MEDIAPLAYERSTATE_ROOT, SN_MEDIAPLAYERSTATE_PATH, SN_MEDIAPLAYERSTATE_VALUE, &dwState) ) { if (dwState != g_bWMPStarted) { g_bWMPStarted = dwState; HDC hButtonDC = GetDC(g_hPlayBt); HDC hMemDC = CreateCompatibleDC(hButtonDC); HBITMAP hBmpOld = (HBITMAP)SelectObject(hMemDC, g_bWMPStarted ? g_hPauseBmpI : g_hPlayBmpI); BitBlt( hButtonDC, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, hMemDC, 0, 0, SRCCOPY ); DeleteDC(hMemDC); ReleaseDC(g_hPlayBt, hButtonDC); } } else { g_bWMPStarted = FALSE; } } break;
Media Player Plugin怎么与Media Player通信就不是这篇文章介绍的内容了,请见这里:Windows Mobile多媒体开发总结之Media Player Plugins(续)。简单的说Media Player Plugin就是Media Player的
进程内COM服务器。
Media Player Today Plugin -> Media Player Plugin
这个问题很好解决,我们在Media Player Plugin里面创建一个隐藏的窗口(宽高为0),并且有自己的消息泵(GetMessage/DispatchMessage),
当Media Player Today Plugin想让Media Player Plugin做什么事时就SendMessage一个自定义的窗口消息,Media Player Plugin的窗口收到
对应消息后对Media Player做对应操作(暂停、开始等)。
一种是通过附加到进程(Shell32.exe)的方法调试代码,但是这个方法有时会失败,为什么会失败我也没搞明白(并不是没有Symbols文件的原因)。
我这里是在Win32下编写的,所以选择本地代码。Today Plugin的DLL文件是被shell32.exe加载的,所以附加到这个进程中:
很不幸,这次就没搞成功,既不是上面说的Symbol的问题,也不是Debug/Release版本的问题,我把责任推到VS头上,因为使用VS调试C++程序(C#程序那就方便多了)有时就是不方便。
当你不想调试时,应该选择全部分离,而不是其它(比如全部终止),想知道为什么的话查一下MSDN吧:
所以有时得依靠另一种方法——Debug Zone来查看程序运行时的Trace信息:
如果你不会使用Debug Zone,也可以这样自己封装一个函数来获得程序的Trace信息:
void DebugPrintString( const char *format, ... ) { va_list args; va_start(args, format); #ifdef _LOG_ FILE *fpLog; fpLog = fopen("DebugInfo.log", "a+"); // "a+" appends context to the end of the file. if (fpLog) { vfprintf(fpLog, format, args); fflush(fpLog); fclose(fpLog); } #else vwprintf(format, args); #endif va_end(args); }
最后你可以从这里下载我编写的这个插件的Windows Mobile安装包(屏幕的最大宽度/高度不要超过400像素的Windows Mobile Professional手机都可使用)。