代码改变世界

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

  王克伟  阅读(5701)  评论(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入口):

1
2
3
4
HWND APIENTRY InitializeCustomItem (
  TODAYLISTITEM *ptli,
  HWND hwndParent
);

我们在这个函数里面初始化资源,创建插件自己的窗口,并且显示窗口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*************************************************************************/
/* 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的对应的背景来刷插件背景):

1
2
3
4
5
6
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;
但是我这里使用自己的背景图片,所以你看到的是如下的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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主线程中等待,否则会导致用户界面被阻塞了。
看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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音量以及其它你感
兴趣的信息。
以下是注册表已经有的信息:

1
2
3
4
5
6
7
8
9
10
[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而非轮训(轮总是不好的^^):

1
2
3
4
RegistryNotifyApp
RegistryNotifyCallback
RegistryNotifyMsgQueue
RegistryNotifyWindow

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

下面的代码给个处理这些通知的演示(我使用了RegistryNotifyWindow,发送我自定义的消息WM_CHANGE_STATE):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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手机都可使用)。
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示