Introduction
One of the most challenging aspects about being a developer for Windows Mobile is the fact that Windows Mobile is not a single platform. Over the past decade, Microsoft has layered multiple user interface shells over the core Windows CE Operating System, and today, two contenders still remain. The first, formerly named Pocket PC, and now, Windows Mobile Classic or Windows Mobile Professional, is a touch-screen based design where a stylus or finger is the primary input device. The second, formerly named SmartPhone and now Windows Mobile Standard, uses only the keypad and navigation buttons.
In recent years, Microsoft has worked hard to converge the two user interfaces. They have even blurred the marketing distinctions with the Windows Mobile Classic/Standard/Professional naming scheme (so much so that the rest of this article refers to the two species as touchscreen and non-touchscreen). However, for the developer, differences certainly still exist.
Foremost, Microsoft has different logo certification requirements for the two platforms. In fact, each has their own PDF document outlining specific behavior which must be implemented. Obtaining logo certification for an application is a good idea, because it lets customers know that the application is robust and that it follows standards that will ease deployment and training. Even more important, if an application works with restricted low level system calls, it must obtain a Microsoft Mobile2Market privileged signature. To get a privileged signature, logo certification is now a requirement, not an option! This article shows how to abstract some of the most common issues a developer will encounter when creating a native code application that must be logo certified for each platform. The routines demonstrated by the demo application are included in a single source file, should work for Windows Mobile 5.0 and higher, and can easily be added to any project.
Background
Creating source code that compiles on multiple different systems is called cross platform development. Most code, if it is generic enough, will compile on any system. But in some cases, code will need to be specifically targeted to a certain platform. Usually, this involves inserting the #if/#endif
preprocessor directives. For touchscreen device platform configurations, WIN32_PLATFORM_PSPC
is defined. For non-touchscreen device platform configurations, WIN32_PLATFORM_WFSP
is defined. The code in this article will key off of those two definitions to tell the compiler which code to use and which code to ignore. Because these are preprocessor directives, a separate binary needs to be generated for each platform.
Using the Code
The SIP (Or Lack Thereof)
Many touchscreen devices lack a keyboard, and only have the screen available as a way to enter text. To solve this problem, all touchscreen devices offer a Soft Input Panel (SIP). The user can then enter text using the SIP. The application user interface residing behind the SIP should be notified about the limited screen real estate now available, so that it can resize itself as best as it can. Implementing the above is one of the requirements for Windows Mobile Professional logo certification, as stated in the Designed for Windows Mobile 6 Professional Software Application Handbook:
Required: Applications Must Handle the Input Panel Appearing/Disappearing
From the UI perspective, the application must be designed to account for the fact that an 80-pixel tall Soft Input Panel (SIP) may be docked in the area immediately above the command bar at any time, requiring the application to be resized out of the way. The specific requirements are:
- All text entry fields must be accessible with the SIP up. This can be accomplished either by placing all edit fields high enough on the screen so that they will not be obscured by an 80 pixel tall SIP, or by having a scroll bar when the SIP is displayed (or both).
- Applications must resize in response to docked input methods. This includes resizing and/or shifting things like dialog tabs, status bars, and other elements as needed, and/or resizing or drawing scroll bars to ensure all UI is accessible while an 80-pixel tall SIP is up. Note that a docked input method may be as tall as 140 pixels, but applications are not expected to be optimized for an SIP height greater than 80 pixels.
Handling the SIP and resizing any dialog affected by it involves just a few system calls. All of them revolve around the SHACTIVATEINFO
structure, which keeps state information for the SIP and how it is affecting screen real estate. This structure needs to be initialized during WM_INITDIALOG
, and updated during WM_ACTIVATE
and WM_SETTINGCHANGE
.
void
LH_SIPCreate(HWND hwnd,SHACTIVATEINFO *psai)
{
#if WIN32_PLATFORM_PSPC
SIPINFO si;
int cx,cy;
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 (!(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 }
In WM_INITDIALOG
, the LH_SIPCreate()
function is called. Here, the SHACTIVATEINFO
structure is initialized. The dialog is then sized properly, given the initial status of the SIP and the dimensions of the menu bar. That's a lot of work, but luckily, it only has to be done once.
void
LH_SIPActivate(HWND hwnd,WPARAM wParam,LPARAM lParam,SHACTIVATEINFO *psai)
{
#if WIN32_PLATFORM_PSPC
SHHandleWMActivate(hwnd, wParam, lParam, psai, FALSE);
#endif }
void
LH_SIPSettingChange(HWND hwnd,WPARAM wParam,LPARAM lParam,SHACTIVATEINFO *psai)
{
#if WIN32_PLATFORM_PSPC
SHHandleWMSettingChange(hwnd, wParam, lParam, psai);
#endif }
After that, on WM_ACTIVATE
, LH_SIPActivate()
delegates SHHandleWMActivate()
to update SHACTIVATEINFO
and do the work of sending WM_SIZE
events to the dialog. Similarly, for WM_SETTINGCHANGE
, LH_SIPSettingChange()
uses SHHandleWMSettingChange()
to handle the task.
Of course, non-touchscreen devices don't have SIPs--there is no way to tap on the panel. Instead, these devices have full keyboards, or at least a number pad that can enter alphanumeric characters. The question, then, is how to write clean code that will deal with both SIP and non-SIP devices. This is done by bracketing platform specific code with #if WIN32_PLATFORM_PSPC/#endif
. The LH_SIPCreate()
, LH_SIPActivate()
, and LH_SIPSettingChange()
functions can be called by both platforms, but on non-touchscreen devices, they are no-ops.
|
|
SIP not handled
|
SIP handled
|
The image on the left shows what happens when the SIP is not handled correctly. When docked, the SIP overlays the application's user interface, obscuring the edit control. This looks ugly, appears unprofessional, and would not satisfy the SIP logo requirement. On the other hand, the image on the right shows what happens when the application responds correctly. Here, the sample application processes the SHACTIVATEINFO
structure in WM_INITDIALOG
, WM_ACTIVATE
, and WM_SETTINGCHANGE
. As a result, the application is correctly informed about the available screen space via WM_SIZE
, and can resize itself to best take advantage of the new dimensions. In this case, the edit control stretches to fill all the available space. Even if there is a large amount of text, if the SIP appears and the edit control must shrink, text can still be viewed using the edit control's scrollbar. The logo requirement is thus accomplished.
The Mysterious Back Button
The back button performs very different operations depending on which platform you are using. For touchscreen devices, the button sends the application to the rear. For non-touchscreen devices, the back button is actually a delete key in some cases. The proper use of the back button is a requirement for Windows Mobile 6 Standard logo certification. Thus, it is important for a developer to handle this correctly, as stated in the Designed for Windows Mobile 6 Standard Software Application Handbook:
Required: Back Button Performs Backspace in an Edit Control
On a screen where text can be edited (for example, in a mail message), the back button must enable the user to backspace on the entire screen, but not exit.
Ironically, in order to achieve this standard functionality for non-touchscreen devices, we will actually have to override the way the back button works! If the dialog contains an edit control, we need the button to work as a delete key. If it doesn't have an edit control, the back button should behave normally, and either send the application to the rear or close a sub-dialog.
The function LH_BackKeyBehavior()
will toggle how the back button works. Depending on the parameters, sending a SHCMBM_OVERRIDEKEY
message specifying the back button (VK_TBACK
) will either redirect presses to WM_HOTKEY
or force it back to normal default behavior. This call should be made any time edit controls appear or completely disappear from a dialog.
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 }
In WM_HOTKEY
, LH_BackKeyHotKey()
is called. If VK_TBACK
is the hotkey in question, the back key has been overridden and deletion needs to be supported. In that case, the information in the notification is relayed to SHSendBackToFocusWindow()
in order to send delete events to the proper edit control.
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 }
Touchscreen devices, of course, do not honor this deletion behavior. The back key is always used to close dialogs or send applications to the rear. While maintaining the LH_BackKeyBehavior()
and LH_BackKeyHotKey()
calls, this code is eliminated in that platform by bracketing with #if WIN32_PLATFORM_WFSP/#endif
, just as was done in the SIP case described previously.
|
|
Back deletes
|
Back minimizes
|
In the image on the left, there is an edit control visible. According to the logo certification guidelines, the back button should be able to delete in this case. For the image on the right, the edit control has been disabled and hidden. Without any edit controls, the back button should perform its default behavior. For the main screen of an application, that behavior is to force the application to the back.
Spinners or Combo Boxes?
Complicating life for the Windows Mobile developer is the fact that not all common controls are acceptable for logo certification on both platforms. One of the biggest differences is regarding combo boxes. On touchscreen devices, combo boxes are A-OK. Clicking on one brings up a dropdown list and the user can select an item. However, on non-touchscreen devices, spinners are preferred. Instead of a combo box, the user is presented with a single line containing the selected entry. Alongside are left and right arrows which allow the user to scroll through alternate selections. If the user clicks on the entry field, a full screen listbox is displayed, allowing the user to see all the choices at once.
Non-touchscreen devices do support combo boxes. However, the Windows Mobile 6 Standard logo certification guidelines do not allow their use, as stated in the Designed for Windows Mobile 6 Standard Software Application Handbook:
Required: Spinner Controls
If an application requires radio-button or drop-down list behavior, spinner controls (left and right arrows) must be used.
If desktop code is being ported to Windows Mobile non-touchscreen devices, it most likely depends on combo boxes. The same is true for code being brought over from Windows Mobile touchscreen devices. Rewriting that code to use spinners instead would be a lot of work, and might risk breaking the existing implementation. How best to abstract this behavior? Subclassing!
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);
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 }
Instead of worrying about combo boxes on non-touchscreen devices, a spinner control can be created that will sit on top of the hidden combo box and intercept all of its communication. In WM_INITDIALOG
, _InitSpinCombo()
subclasses the existing combo box, creates a new spinner control, sizes it to the same dimensions as the combo box, puts it in the correct tab order, sets the font to be the same, and finally, hides and disables the old combo box. Since this code is only needed for non-touchscreen devices, the contents of LH_InitSpinCombo()
are bracketed by #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()
does the work of translating combo box messages to something the new spinner control can use. WM_SIZE
reports any movement or resizing of the combo box control so the spinner control can shadow it. WM_DESTROY
cleans up subclassing and memory allocation used to initialize the spinner control. The other messages populate the spinner or report on the selections. It certainly helps here that both the combo box and the spinner control appear to be based on the listbox and seem to be direct cousins--not a lot of work needs to be done here.
|
|
Combo box
|
Spinner
|
The image on the left shows a non-touchscreen device running with code using a combo box. This implementation does not follow the spinner rule from the Windows Mobile 6 Standard Software Application Handbook, and would not pass logo certification. The image on the right is the same code, only with the LH_InitSpinCombo()
call added. The combo box is there, it is just hidden and disabled. All of the combo box messages generated by the application are still being sent. However, now they are redirected to the new spinner control instead. The spinner logo requirement is solved with a single function call.
Putting it All Together
This demo shows how to use all of the above code to write an application that satisfies the logo certification requirements for either platform. On touchscreen devices, the SIP properly resizes the application window. On non-touchscreen devices, the back button is correctly enabled when the edit dialog is displayed. Finally, spinner controls appear on non-touchscreen devices, without any changes to the underlying combo box based code.
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); 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: EndDialog(hWnd, TRUE);
break;
case IDCANCEL: SHNavigateBack();
break;
}
break;
}
}
return FALSE;
}
In WM_INITDIALOG
, LH_InitSpinCombo()
is called in order to subclass the combo box on non-touchscreen devices and turn it into a spinner control. Afterwards, the combo box is initialized with the choices for the state of the edit control, with enabled being the default state. Note that these messages are redirected to the new spinner control for non-touchscreen devices via subclassing. LH_SIPCreate()
is then called to initialize SIP handling for touchscreen devices. Finally, LH_BackKeyBehavior()
configures the back key on non-touchscreen devices to allow deletion inside the enabled edit control. In the case where the the back key must provide deletion on non-touchscreen devices, we forward the WM_HOTKEY
message to the helper function.
Whenever there is a change in the combo box/spinner selection, the edit control needs to be updated with its new status, and the back key must be properly configured depending on that status. Inside WM_COMMAND
, the CBN_SELCHANGE
notification is processed. If the edit control is available, the back key should do deletion. If there is no edit control, pressing the back key should minimize the dialog, or, in the case of a sub-dialog, close the dialog. On non-touchscreen devices, if the back button is not overridden, a IDCANCEL
command will be sent. Since this dialog is the entire application, it is minimized by calling SHNavigateBack()
. If the dialog were a sub-dialog, the correct response here would be EndDialog(hWnd,FALSE)
.
Conclusion
Adding the six helper functions outlined here to an application will allow it to satisfy all of the logo certification issues discussed in this document. Because these functions abstract the platform specific operations, the rest of the hosting application's code does not need to worry about platform idiosyncrasies. Best of all, the hosting application should be able to pass logo certification on both platforms with a minimum of modifications.
References
History
- November 19, 2008 - Initial version (WJB).