代码改变世界

设计并编写一个Windows Mobile 6.5今日界面 之播放器今日插件

2009-10-18 21:56  王克伟  阅读(5690)  评论(15编辑  收藏  举报

这篇文章继续设计并编写一个Windows Mobile 6.5今日界面,介绍the Legacy Today Screen Plugin。

在文章Windows Mobile多媒体开发总结之Media Player PluginsWindows 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才搞出你看到的这个界面:

 CEZoom0  CEZoom1
CEZoom2

这里有可以使用现成的一些设计资源:
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消息来得的高亮状态,在这里不再详细说明,需要的时候你可以查看文档。

clip_image002[7]

另外你可以在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 //这里才是插件的窗口

clip_image002[9]


另外通过该工具的Messages功能能很方便的监测到每个窗口收到的消息,便于窗口消息的调整,进而优化插件性能:

clip_image002[11]

上面看到的WM_USER+243消息就是WM_TODAYCUSTOM_QUERYREFRESHCACHE消息,每隔4s左右你就会收到。除了这个消息,其它消息是我点击播放按钮时产生的。这里有个不好的地方是我使用了6个按钮,用户小小的一个动作,好家伙,一大堆消息要处理。这也就是为什么少使用控件的原因之一了(在要求运行效率的时候)。

.Net CF下能够开发Today Plugin的原因是因为它封装了上面介绍的东西,上面这些东西是更底层的。所以你使用C#开发时同样要注意上面提到的优化建议。

下面就是在.Net CF下创建的一个默认Application的窗口消息(点击窗口空白地方时产生的):

clip_image002[13]

 

第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的句柄。

clip_image002[12]

我们知道这样的线程大部分时间因为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:

clip_image002[14]

我仍然嫌这么做麻烦了点,所以我用了更简单的通过注册表和自定义的窗口消息进行通信。下面主要说明这个思路。

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 

下图给出更多的与注册表有关的API介绍:
clip_image002clip_image002[5]
clip_image002[8]clip_image002[10]

下面的代码给个处理这些通知的演示(我使用了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文件的原因)。

clip_image002[3]

 

我这里是在Win32下编写的,所以选择本地代码。Today Plugin的DLL文件是被shell32.exe加载的,所以附加到这个进程中:

image002

 

很不幸,这次就没搞成功,既不是上面说的Symbol的问题,也不是Debug/Release版本的问题,我把责任推到VS头上,因为使用VS调试C++程序(C#程序那就方便多了)有时就是不方便。

image004

 

当你不想调试时,应该选择全部分离,而不是其它(比如全部终止),想知道为什么的话查一下MSDN吧:

clip_image002[5]

 

所以有时得依靠另一种方法——Debug Zone来查看程序运行时的Trace信息:

clip_image002[1]

 

如果你不会使用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手机都可使用)。