为 Windows Mobile 平台编写 Logo 认证代码






4.44/5 (23投票s)
本文介绍如何抽象开发人员在创建需要为每个平台进行 Logo 认证的原生代码应用程序时会遇到的一些最常见问题。
引言
Windows Mobile 开发人员面临的最具挑战性的方面之一是 Windows Mobile 并非单一平台。在过去十年中,微软在核心 Windows CE 操作系统之上添加了多个用户界面外壳,如今仍有两个主要平台。第一个,以前称为 Pocket PC,现在称为 Windows Mobile Classic 或 Windows Mobile Professional,是一种基于触摸屏的设计,手写笔或手指是主要的输入设备。第二个,以前称为 SmartPhone,现在称为 Windows Mobile Standard,仅使用键盘和导航按钮。
近年来,微软一直在努力整合这两个用户界面。他们甚至模糊了 Windows Mobile Classic/Standard/Professional 命名方案的市场区分(以至于本文其余部分将这两种类型称为触摸屏和非触摸屏)。然而,对于开发人员来说,差异确实依然存在。
最重要的是,微软对这两个平台有不同的 Logo 认证要求。事实上,每个平台都有自己的 PDF 文档,概述了必须实现的具体行为。为应用程序获得 Logo 认证是一个好主意,因为它让客户知道该应用程序很健壮,并且符合将简化部署和培训的标准。更重要的是,如果应用程序需要使用受限制的低级系统调用,则必须获得 Microsoft Mobile2Market 特权签名。要获得特权签名,Logo 认证现在是必需项,而不是可选项!本文介绍如何抽象开发人员在创建需要为每个平台进行 Logo 认证的原生代码应用程序时会遇到的一些最常见问题。演示应用程序中提供的例程包含在单个源文件中,应适用于 Windows Mobile 5.0 及更高版本,并且可以轻松添加到任何项目中。
背景
创建可以在多个不同系统上编译的源代码称为跨平台开发。大多数代码,如果足够通用,可以在任何系统上编译。但在某些情况下,代码需要针对特定平台进行专门编写。这通常涉及插入 #if/#endif
预处理器指令。对于触摸屏设备平台配置,定义了 WIN32_PLATFORM_PSPC
。对于非触摸屏设备平台配置,定义了 WIN32_PLATFORM_WFSP
。本文中的代码将基于这两个定义来告诉编译器使用哪些代码以及忽略哪些代码。由于这些是预处理器指令,因此每个平台都需要生成一个单独的二进制文件。
Using the Code
SIP(或缺失的 SIP)
许多触摸屏设备缺少键盘,只有屏幕可用于输入文本。为了解决这个问题,所有触摸屏设备都提供了一个软输入面板(SIP)。然后,用户可以使用 SIP 输入文本。位于 SIP 后面的应用程序用户界面应该被告知可用的屏幕空间有限,以便它可以尽可能地调整自身大小。实现以上功能是 Windows Mobile Professional Logo 认证的要求之一,如《面向 Windows Mobile 6 Professional 的软件应用程序手册》中所述。
必需:应用程序必须处理输入面板的出现/消失
从 UI 的角度来看,应用程序必须考虑到一个 80 像素高的软输入面板(SIP)可能随时停靠在命令栏正上方区域,从而需要应用程序调整大小以避开。具体要求是:
- 所有文本输入字段在 SIP 出现时都必须可访问。这可以通过将所有编辑字段放置在屏幕足够高的位置,使其不会被 80 像素高的 SIP 遮挡,或者在显示 SIP 时具有滚动条(或两者兼有)来实现。
- 应用程序必须响应停靠的输入方法进行大小调整。这包括根据需要调整和/或移动对话框标签、状态栏和其他元素的大小,以及/或调整滚动条的大小或绘制滚动条,以确保在 80 像素高的 SIP 出现时所有 UI 都可访问。请注意,停靠的输入方法可能高达 140 像素,但应用程序不需要针对大于 80 像素的 SIP 高度进行优化。
处理 SIP 和调整受其影响的任何对话框的大小只需进行几次系统调用。所有这些都围绕着 SHACTIVATEINFO
结构,该结构保存 SIP 的状态信息以及它如何影响屏幕空间。此结构需要在 WM_INITDIALOG
期间初始化,并在 WM_ACTIVATE
和 WM_SETTINGCHANGE
期间更新。
void
LH_SIPCreate(HWND hwnd,SHACTIVATEINFO *psai)
{
#if WIN32_PLATFORM_PSPC
SIPINFO si;
int cx,cy;
// Initialize the shell activate info structure.
memset(psai, 0, sizeof (SHACTIVATEINFO));
psai->cbSize = sizeof (SHACTIVATEINFO);
memset(&si, 0, sizeof(si));
si.cbSize = sizeof(si);
SHSipInfo(SPI_GETSIPINFO, 0, (PVOID) &si, FALSE);
cx = si.rcVisibleDesktop.right - si.rcVisibleDesktop.left;
cy = si.rcVisibleDesktop.bottom - si.rcVisibleDesktop.top;
// If the SIP is not shown, or it is showing but not docked, the
// desktop rect does not include the height of the menu bar.
if (!(si.fdwFlags & SIPF_ON) ||
((si.fdwFlags & SIPF_ON) && !(si.fdwFlags & SIPF_DOCKED)))
{
RECT rcMenu;
HWND hwndMenuBar;
hwndMenuBar = SHFindMenuBar(hwnd);
if(hwndMenuBar!=NULL)
{
GetWindowRect(hwndMenuBar,&rcMenu);
cy -= (rcMenu.bottom-rcMenu.top);
}
}
SetWindowPos(hwnd, NULL, 0, 0, cx, cy, SWP_NOMOVE | SWP_NOZORDER);
#endif // WIN32_PLATFORM_PSPC
}
在 WM_INITDIALOG
中,会调用 LH_SIPCreate()
函数。在这里,初始化了 SHACTIVATEINFO
结构。然后,根据 SIP 的初始状态和菜单栏的尺寸,对话框会被正确调整大小。这是一项繁重的工作,但幸运的是,只需要执行一次。
void
LH_SIPActivate(HWND hwnd,WPARAM wParam,LPARAM lParam,SHACTIVATEINFO *psai)
{
#if WIN32_PLATFORM_PSPC
SHHandleWMActivate(hwnd, wParam, lParam, psai, FALSE);
#endif // WIN32_PLATFORM_PSPC
}
void
LH_SIPSettingChange(HWND hwnd,WPARAM wParam,LPARAM lParam,SHACTIVATEINFO *psai)
{
#if WIN32_PLATFORM_PSPC
SHHandleWMSettingChange(hwnd, wParam, lParam, psai);
#endif // WIN32_PLATFORM_PSPC
}
之后,在 WM_ACTIVATE
上,LH_SIPActivate()
调用 SHHandleWMActivate()
来更新 SHACTIVATEINFO
并执行发送 WM_SIZE
事件到对话框的工作。同样,对于 WM_SETTINGCHANGE
,LH_SIPSettingChange()
使用 SHHandleWMSettingChange()
来处理任务。
当然,非触摸屏设备没有 SIP——无法点击面板。相反,这些设备具有全键盘,或者至少有一个可以输入字母数字字符的数字键盘。那么问题来了,如何编写清晰的代码来处理 SIP 和非 SIP 设备?这是通过将特定于平台的代码括在 #if WIN32_PLATFORM_PSPC/#endif
中来完成的。LH_SIPCreate()
、LH_SIPActivate()
和 LH_SIPSettingChange()
函数可以被两个平台调用,但在非触摸屏设备上,它们是空操作(no-ops)。
|
|
未处理 SIP
|
已处理 SIP
|
左边的图像显示了未正确处理 SIP 时发生的情况。停靠时,SIP 会覆盖应用程序的用户界面,遮挡编辑控件。这看起来很难看,显得不专业,并且无法满足 SIP Logo 要求。另一方面,右边的图像显示了应用程序正确响应时发生的情况。在这里,示例应用程序在 WM_INITDIALOG
、WM_ACTIVATE
和 WM_SETTINGCHANGE
中处理 SHACTIVATEINFO
结构。结果,应用程序通过 WM_SIZE
正确获知可用屏幕空间,并可以调整自身大小以充分利用新尺寸。在此示例中,编辑控件会拉伸以填充所有可用空间。即使有大量文本,如果 SIP 出现且编辑控件必须收缩,仍可以通过编辑控件的滚动条查看文本。这样就满足了 Logo 要求。
神秘的返回键
返回键在不同平台上的操作方式截然不同。对于触摸屏设备,返回键将应用程序退到后台。对于非触摸屏设备,返回键在某些情况下实际上是删除键。返回键的正确使用是 Windows Mobile 6 Standard Logo 认证的要求。因此,正如《面向 Windows Mobile 6 Standard 的软件应用程序手册》中所述,开发人员正确处理这一点非常重要。
必需:返回键在编辑控件中执行退格操作
在可以编辑文本的屏幕上(例如,在邮件消息中),返回键必须使用户能够在整个屏幕上退格,而不是退出。
讽刺的是,为了实现非触摸屏设备的这种标准功能,我们实际上必须覆盖返回键的工作方式!如果对话框包含编辑控件,我们需要该按钮充当删除键。如果没有编辑控件,返回键应表现正常,要么将应用程序退到后台,要么关闭子对话框。
LH_BackKeyBehavior()
函数将切换返回键的工作方式。根据参数,发送一个 SHCMBM_OVERRIDEKEY
消息并指定返回键(VK_TBACK
)会将其重定向到 WM_HOTKEY
或强制其恢复正常的默认行为。只要编辑控件出现在对话框中或完全从对话框中消失,就应该调用此调用。
void
LH_BackKeyBehavior(HWND hwnd,BOOL bHasEditControl)
{
#if WIN32_PLATFORM_WFSP
LPARAM lparam;
HWND hwndMenuBar;
hwndMenuBar = SHFindMenuBar(hwnd);
if(hwndMenuBar!=NULL)
{
if(bHasEditControl)
lparam = MAKELPARAM(SHMBOF_NODEFAULT | SHMBOF_NOTIFY,
SHMBOF_NODEFAULT | SHMBOF_NOTIFY);
else
lparam = MAKELPARAM(SHMBOF_NODEFAULT | SHMBOF_NOTIFY, 0);
SendMessage(hwndMenuBar, SHCMBM_OVERRIDEKEY, VK_TBACK, lparam);
}
#endif // WIN32_PLATFORM_WFSP
}
在 WM_HOTKEY
中,会调用 LH_BackKeyHotKey()
。如果 VK_TBACK
是热键,则返回键已被覆盖,需要支持删除。在这种情况下,通知中的信息会传递给 SHSendBackToFocusWindow()
,以便将删除事件发送到正确的编辑控件。
void
LH_BackKeyHotKey(HWND hwnd,UINT uMessage,WPARAM wParam,LPARAM lParam)
{
#if WIN32_PLATFORM_WFSP
if(HIWORD(lParam) == VK_TBACK)
SHSendBackToFocusWindow(uMessage, wParam, lParam);
#endif // WIN32_PLATFORM_WFSP
}
当然,触摸屏设备不遵循这种删除行为。返回键始终用于关闭对话框或将应用程序退到后台。在保持 LH_BackKeyBehavior()
和 LH_BackKeyHotKey()
调用不变的情况下,可以通过使用 #if WIN32_PLATFORM_WFSP/#endif
进行括起来来从该平台中删除此代码,就像之前在 SIP 案例中所做的那样。
|
|
返回键删除
|
返回键最小化
|
在左边的图像中,有一个可见的编辑控件。根据 Logo 认证指南,在这种情况下,返回键应该能够执行删除操作。对于右边的图像,编辑控件已被禁用并隐藏。在没有编辑控件的情况下,返回键应执行其默认行为。对于应用程序的主屏幕,该行为是强制最小化应用程序。
旋转控件还是组合框?
让 Windows Mobile 开发人员感到复杂的是,并非所有常用控件在两个平台上都能被 Logo 认证接受。最大的区别之一是组合框。在触摸屏设备上,组合框是完全可以接受的。点击其中一个会弹出一个下拉列表,用户可以选择一个项目。然而,在非触摸屏设备上,更倾向于使用旋转控件。与组合框不同,用户会看到一个包含所选条目的单行。旁边有左右箭头,允许用户滚动浏览其他选项。如果用户点击条目字段,将显示一个全屏列表框,允许用户一次性查看所有选项。
非触摸屏设备确实支持组合框。但是,《面向 Windows Mobile 6 Standard 的软件应用程序手册》中的 Windows Mobile 6 Standard Logo 认证指南不允许使用它们。
必需:旋转控件
如果应用程序需要单选按钮或下拉列表行为,则必须使用旋转控件(左右箭头)。
如果桌面代码要移植到 Windows Mobile 非触摸屏设备,它最有可能依赖于组合框。从 Windows Mobile 触摸屏设备移植过来的代码也是如此。将这些代码重写为使用旋转控件将是一项艰巨的任务,并且可能会破坏现有实现。如何最好地抽象这种行为?子类化!
void
LH_InitSpinCombo(HWND hWnd)
{
#if WIN32_PLATFORM_WFSP
PSPINNERCOMBO psc;
RECT rc;
psc=(PSPINNERCOMBO)malloc(sizeof(SPINNERCOMBO));
if(psc!=NULL)
{
memset(psc,0x00,sizeof(SPINNERCOMBO));
SetProp(hWnd, TEXT("SpinComboData"), psc);
psc->origCombo = (WNDPROC)SetWindowLong (hWnd, GWL_WNDPROC,
(LONG_PTR)SubclassComboProc);
GetWindowRect(hWnd,&rc);
MapWindowPoints (NULL, GetParent(hWnd), (LPPOINT)&rc, 2);
psc->hwndSpin = CreateWindow (TEXT("listbox"), NULL,
WS_VISIBLE | WS_TABSTOP | LBS_NOTIFY,
rc.left, rc.top, rc.right-rc.left, rc.bottom-rc.top,
GetParent(hWnd), (HMENU)GetDlgCtrlID(hWnd), NULL, NULL);
// Put spinner in the proper tab order after the original combobox
SetWindowPos(psc->hwndSpin,hWnd,0,0,0,0,
SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE);
psc->hwndUpDown = CreateWindow (UPDOWN_CLASS, NULL,
WS_VISIBLE | UDS_HORZ | UDS_ALIGNRIGHT | UDS_ARROWKEYS |
UDS_SETBUDDYINT | UDS_WRAP | UDS_EXPANDABLE,
0, 0, 0, 0, GetParent(hWnd), NULL, NULL, NULL);
SendMessage (psc->hwndUpDown, UDM_SETBUDDY, (WPARAM)psc->hwndSpin, 0);
psc->hFont = (HFONT)SendMessage(hWnd, WM_GETFONT, 0, 0);
SendMessage(psc->hwndSpin,WM_SETFONT,(WPARAM)psc->hFont,0);
EnableWindow(hWnd,FALSE);
ShowWindow(hWnd,SW_HIDE);
}
#endif // WIN32_PLATFORM_WFSP
}
而不是担心非触摸屏设备上的组合框,可以创建一个旋转控件,该控件会覆盖在隐藏的组合框之上并拦截其所有通信。在 WM_INITDIALOG
中,_InitSpinCombo()
对现有的组合框进行子类化,创建一个新的旋转控件,将其尺寸设置为与组合框相同,将其放置在正确的制表符顺序中,设置相同的字体,最后隐藏并禁用旧的组合框。由于此代码仅适用于非触摸屏设备,因此 LH_InitSpinCombo()
的内容将用 #if WIN32_PLATFORM_WFSP/#endif
括起来。
LRESULT CALLBACK
SubclassComboProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PSPINNERCOMBO psc;
WNDPROC origCombo;
psc = (PSPINNERCOMBO)GetProp(hWnd, TEXT("SpinComboData"));
origCombo=psc->origCombo;
switch (message)
{
case CB_ADDSTRING:
return SendMessage(psc->hwndSpin,LB_ADDSTRING,wParam,lParam);
case CB_SETITEMDATA:
return SendMessage(psc->hwndSpin,LB_SETITEMDATA,wParam,lParam);
case CB_GETITEMDATA:
return SendMessage(psc->hwndSpin,LB_GETITEMDATA,wParam,lParam);
case CB_SETCURSEL:
return SendMessage(psc->hwndSpin,LB_SETCURSEL,wParam,lParam);
case CB_GETCURSEL:
return SendMessage(psc->hwndSpin,LB_GETCURSEL,wParam,lParam);
case WM_SIZE:
{
RECT rc;
GetWindowRect(hWnd,&rc);
MapWindowPoints (NULL, GetParent(hWnd), (LPPOINT)&rc, 2);
MoveWindow(psc->hwndSpin,rc.left,rc.top,
rc.right-rc.left,rc.bottom-rc.top,TRUE);
SendMessage (psc->hwndUpDown, UDM_SETBUDDY, (WPARAM)psc->hwndSpin, 0);
break;
}
case WM_DESTROY:
{
SetWindowLong (hWnd, GWL_WNDPROC, (LONG_PTR)psc->origCombo);
free(psc);
break;
}
}
return CallWindowProc (origCombo, hWnd, message, wParam, lParam);
}
SubclassComboProc()
负责将组合框消息转换为新旋转控件可以使用的内容。WM_SIZE
会报告组合框控件的任何移动或大小调整,以便旋转控件可以对其进行跟踪。WM_DESTROY
会清理用于初始化旋转控件的子类化和内存分配。其他消息用于填充旋转控件或报告选择。这里确实有帮助的是,组合框和旋转控件似乎都基于列表框,并且似乎是直接的“表亲”——这里不需要做很多工作。
|
|
组合框
|
Spinner
|
左边的图像显示了一个运行组合框代码的非触摸屏设备。此实现不符合《Windows Mobile 6 Standard 软件应用程序手册》中的旋转控件规则,因此无法通过 Logo 认证。右边的图像是相同的代码,只是添加了 LH_InitSpinCombo()
调用。组合框存在,只是被隐藏和禁用了。应用程序生成的所有组合框消息仍然会发送。然而,现在它们被重定向到了新的旋转控件。通过一个函数调用即可解决旋转控件的 Logo 要求。
整合
此演示展示了如何使用上述所有代码编写一个满足任一平台 Logo 认证要求的应用程序。在触摸屏设备上,SIP 会正确调整应用程序窗口的大小。在非触摸屏设备上,当显示编辑对话框时,会正确启用返回键。最后,在非触摸屏设备上会出现旋转控件,而不会对基础的基于组合框的代码进行任何更改。
BOOL CALLBACK
DlgProc (HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam)
{
LOGODATA *ld;
ld=(LOGODATA *)GetWindowLong (hWnd, GWL_USERDATA);
switch (wMsg)
{
case WM_INITDIALOG:
{
SHMENUBARINFO mbi;
SHINITDLGINFO shidi;
SetWindowLong(hWnd,GWL_USERDATA,lParam);
ld=(LOGODATA *)lParam;
ld->hwndEdit=GetDlgItem(hWnd,IDC_EDIT1);
ld->hwndCombo=GetDlgItem(hWnd,IDC_COMBO1);
...
LH_InitSpinCombo(ld->hwndCombo);
SendMessage (ld->hwndCombo, CB_ADDSTRING, 0,
(LPARAM)TEXT("Edit Control Disabled"));
ld->iEnable=SendMessage (ld->hwndCombo, CB_ADDSTRING, 0,
(LPARAM)TEXT("Edit Control Enabled"));
SendMessage (ld->hwndCombo, CB_SETCURSEL,
(WPARAM)ld->iEnable, (LPARAM)0);
LH_SIPCreate(hWnd,&(ld->sai));
LH_BackKeyBehavior(hWnd,TRUE); // There is an edit control
return TRUE;
}
case WM_ACTIVATE:
LH_SIPActivate(hWnd, wParam, lParam, &(ld->sai));
break;
case WM_SETTINGCHANGE:
LH_SIPSettingChange(hWnd, wParam, lParam, &(ld->sai));
break;
case WM_HOTKEY:
LH_BackKeyHotKey(hWnd,wMsg,wParam,lParam);
break;
case WM_SIZE:
OnSize(hWnd,ld);
break;
case WM_COMMAND:
{
switch (LOWORD (wParam))
{
case IDC_COMBO1:
{
if(HIWORD(wParam)==CBN_SELCHANGE)
{
BOOL bShowEdit;
bShowEdit=(SendMessage(ld->hwndCombo,CB_GETCURSEL,0,0)==ld->iEnable);
EnableWindow(ld->hwndEdit,bShowEdit?TRUE:FALSE);
ShowWindow(ld->hwndEdit,bShowEdit?SW_SHOW:SW_HIDE);
LH_BackKeyBehavior(hWnd,bShowEdit);
}
break;
}
case IDC_EXIT: // From our menubar
EndDialog(hWnd, TRUE);
break;
case IDCANCEL: // Via Smartphone Back Button
SHNavigateBack();
break;
}
break;
}
}
return FALSE;
}
在 WM_INITDIALOG
中,调用 LH_InitSpinCombo()
以在非触摸屏设备上对组合框进行子类化并将其转换为旋转控件。之后,使用编辑控件的状态选项初始化组合框,其中启用是默认状态。请注意,通过子类化,这些消息会被重定向到非触摸屏设备的新旋转控件。然后调用 LH_SIPCreate()
来初始化触摸屏设备的 SIP 处理。最后,调用 LH_BackKeyBehavior()
将非触摸屏设备的返回键配置为允许在启用的编辑控件中进行删除。在返回键必须为非触摸屏设备提供删除功能的情况下,我们将 WM_HOTKEY
消息转发给辅助函数。
每当组合框/旋转控件选择发生变化时,都需要使用其新状态更新编辑控件,并且必须根据该状态正确配置返回键。在 WM_COMMAND
中,会处理 CBN_SELCHANGE
通知。如果编辑控件可用,则返回键应执行删除操作。如果不存在编辑控件,按下返回键应最小化对话框,或者在子对话框的情况下,关闭对话框。在非触摸屏设备上,如果返回键未被覆盖,将发送 IDCANCEL
命令。由于此对话框是整个应用程序,因此通过调用 SHNavigateBack()
来最小化它。如果对话框是子对话框,则正确的响应将是 EndDialog(hWnd,FALSE)
。
结论
将此处概述的六个辅助函数添加到应用程序将使其能够满足本文档中讨论的所有 Logo 认证问题。由于这些函数抽象了特定于平台的 operations,因此托管应用程序的其余代码无需担心平台特性。最重要的是,托管应用程序应能以最少的修改通过两个平台的 Logo 认证。
参考文献
- 面向 Windows Mobile 6 Standard 的软件应用程序手册
- 面向 Windows Mobile 6 Professional 的软件应用程序手册
- 返回键和其他有趣按钮
- Pocket PC 应用程序中的差异
历史
- 2008 年 11 月 19 日 - 初始版本 (WJB)。