这篇文章继续设计并编写一个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?
第1个问题:如何编写the Legacy Today Screen 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的窗口消息(点击窗口空白地方时产生的):
第2个问题:如何与Media Player Plugin通信
我们知道在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做对应操作(暂停、开始等)。
第3个问题:如何调试你编写的Media Player Today Plugin
一种是通过附加到进程(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手机都可使用)。