Introduction
Most PocketPC programming is done in MFC using wizards. If instead you like to program in plain Win32/C++, you'll have found there to be not enough documentation or examples. This article and the accompanying source code provide a complete minimal example - a Notepad clone. It covers:
- The
DocList
control (i.e. front page list of files, like in Pocket Word)
- The "Card" architecture (that's what PocketPC calls an opened document window)
- The Soft Input Panel (SIP, the keyboard/transcriber)
I haven't tried to wrap up the Windows functionality into my own API. That's because I think people want to learn the Windows API itself, not my own API. And also, the Windows API for PocketPC is rather clean and simple already. PocketPC is actually the only native implementation of Win32: it doesn't rest upon an internal layer like NT/Win2K/XP, and it doesn't rest upon 16bit like Win95/98/ME.
Program structure
PocketPC C++ programs are written using Microsoft eMbedded Visual Tools 3.0 (free). You should also download a free STL port for eVC++, by Giuseppe Govi. Although this article doesn't use it.
To write clean programs for PocketPC, you must (as Yoda says) unlearn what you have learnt:
- Keep a small memory footprint.
- There will never be more than one instance of your app running at a time; nor will there be more than one document open at a time.
- Therefore, global variables are good. Per-app variables can be global. Per-document variables can be global.
- Because the variables are declared statically, not created dynamically, it means that memory-allocation need not be done at runtime. This is a good thing.
Incidentally, the development environment often fails to build unless the "WCE Configuration" toolbar is visible. (This is the toolbar with drop-downs to select target OS, target device, target processor).
Preliminaries
These are the header files and app-specific global variables. Also, under Project > Settings > Linker > Input, you must link against doclist.lib, aygshell.lib and note_prj.lib.
#include <windows.h>
#include <aygshell.h> // for SIP
#include <doclist.h> // for DocList control
#include <projects.h> // for Folder drop-down
#include <newmenu.h> // for New menu
#include <commctrl.h>
#include <shellapi.h>
#ifndef IDM_NEWMENUMAX #include <newmenu.h> // included in PPC2000
#endif
HINSTANCE hInstance;
HWND hmain=0; HWND hmbar=0; HWND hdlc=0; HWND hcard=0; HWND hed=0; wchar_t dlcfolder[MAX_PATH]={0}; SHACTIVATEINFO shai={sizeof(SHACTIVATEINFO),0,0,0,0,0};
bool close_on_ok=false;
The window layout: there is a main window hmain
. It owns a menu-bar hmbar
which sits underneath. In the client area is a DocList
control hdlc
and a document-window hcard
occupying the same area; only one will be visible at a time. Our app is a Notepad-clone, so we have an edit-window hed
inside.
The variable dlcfolder
holds the currently-selected folder by the DocList
. That's because, when it comes to saving a new document, this will tell us which folder to put it in.
The SHACTIVATEINFO
shai
is used by the system to maintain the SIP's state: there are standard calls we must make in WM_ACTIVATE
and WM_SETTINGCHANGED
, involving this structure, for the SIP to behave properly.
And close_on_ok
is for the following reason. If the user clicked on an associated document within File Explorer and so opened our application, then the OK button should close us and return to File Explorer. But if the user opened us from the Start menu, then closing the document should return the user to the main DocList
control. This flag records which mode of closure was most recently indicated.
hmain = FindWindow(L"LuMainClass",L"My App");
if (hmain!=0)
{ SetForegroundWindow((HWND)((ULONG)hmain|1));
if (arg!=0 && wcslen(arg)!=0)
{ COPYDATASTRUCT cs; cs.dwData=0;
cs.cbData=2+2*wcslen(arg); cs.lpData=arg;
SendMessage(hmain,WM_COPYDATA,NULL,(LPARAM)&cs);
}
else SendMessage(hmain,WM_APP,0,0);
return 0;
}
To save on memory, only one instance of our app should ever be loaded. When we're loaded, we check if an instance already exists. If we're loaded with a filename argument to open, we have to communicate this filename to the other instance: hence WM_COPYDATA
. We use WM_APP
to signal a general wakeup, whereupon some refreshes are performed.
SHInitExtraControls();
hmain = CreateWindow(L"LuMainClass", L"My App", WS_VISIBLE,
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
ShowWindow(hmain,nCmdShow);
UpdateWindow(hmain);
if (arg!=0 && wcslen(arg)!=0)
{ COPYDATASTRUCT cs; cs.dwData=0;
cs.cbData=2+2*wcslen(arg); cs.lpData=arg;
SendMessage(hmain,WM_COPYDATA,NULL,(LPARAM)&cs);
}
Using SHInitExtraControls()
allows us to use CAPEDIT
(like a normal EDIT
but it capitalizes the first letter) and also WC_SIPPREF
(automatic management of the SIP).
Managing the windows
We start with creation and destruction of the application window. Note that most of our child windows need not be destroyed, since that happens automatically. The exception is the menu-bar, which must be destroyed explicitly.
case WM_CREATE:
{ SHMENUBARINFO mbi; ZeroMemory(&mbi,sizeof(mbi));
mbi.cbSize=sizeof(mbi);
mbi.hwndParent = hwnd;
mbi.nToolBarId = 101;
mbi.hInstRes = hInstance;
SHCreateMenuBar(&mbi);
hmbar = mbi.hwndMB;
SHGetSpecialFolderPath(hwnd,dlcfolder,CSIDL_PERSONAL,TRUE);
DOCLISTCREATE dlc; ZeroMemory(&dlc,sizeof(dlc));
dlc.dwStructSize=sizeof(dlc);
dlc.hwndParent=hwnd;
dlc.pstrFilter = L"Text\0*.txt\0";
dlc.wId=102;
hdlc = DocList_Create(&dlc);
hcard = CreateWindow(L"LuCardClass",L"",WS_CHILD,0,0,0,0,
hwnd,(HMENU)103,hInstance,0);
hed = CreateWindow(L"CAPEDIT",L"",WS_CHILD|WS_VISIBLE
|WS_HSCROLL|WS_VSCROLL|ES_AUTOHSCROLL
|ES_AUTOVSCROLL|ES_MULTILINE|WS_TABSTOP,0,0,0,0,
hcard,(HMENU)104,hInstance,0);
CreateWindow(WC_SIPPREF,L"",WS_CHILD,0,0,0,0,
hcard,(HMENU)105,hInstance,0);
MakeLayout(mlShowHide|mlRefreshDocList|mlResize,0);
int nitems=DocList_GetItemCount(hdlc);
if (nitems==0)
{ SendMessage(hwnd,WM_COMMAND,IDM_NEWMENUMAX+1,0);
}
} return 0;
case WM_HIBERNATE:
{ if (!doc.open) SetWindowText(hed,L"");
} return 0;
case WM_DESTROY:
{ if (doc.open && doc.changed) doc.Save();
CommandBar_Destroy(hmbar);
PostQuitMessage(0);
} return 0;
The WM_DESTROY
message is an odd case. It is never called during the normal operation of a program. That's because, if the user clicks the Close button, it doesn't close but merely minimizes the app (so that, subsequently, the app can be opened up instantly). If the system is running low on memory, it sends a WM_HIBERNATE
message asking applications to clean themselves up. If it's running really low, it sends a WM_CLOSE
which (in DefWindowProc
) calls DestroyWindow
, so finally triggering our WM_DESTROY
.
The resources for the menubar are as follows. I'm writing it here because it doesn't seem to be documented elsewhere.
#include <windows.h>
#include <commctrl.h>
#include <aygshell.h>
101 RCDATA MOVEABLE
BEGIN
101, 3,
I_IMAGENONE, IDM_SHAREDNEW, TBSTATE_ENABLED,
TBSTYLE_DROPDOWN | TBSTYLE_AUTOSIZE, IDS_SHNEW, 0, 0,
I_IMAGENONE, 202, TBSTATE_ENABLED,
TBSTYLE_DROPDOWN | TBSTYLE_AUTOSIZE, 202, 0, 1,
I_IMAGENONE, 203, TBSTATE_ENABLED,
TBSTYLE_AUTOSIZE, 203, 0, NOMENU,
END
This RCDATA
is for a menubar. The first row (101,3) says to look up MENU#101, and that there will be three items in this menubar. In each row, the second item (IDM_SHAREDNEW
, 203, 203) is the WM_COMMAND
id that will be sent upon clickage. Here, "Shared New" means that it's a menu that the system will fill up with all the standard items (Task, Note, ...) and then will offer to us to fill up the rest.
STRINGTABLE DISCARDABLE
BEGIN
201 "DummyNew"
202 "Tools"
203 "Edit"
END
Within each row of RCDATA
, the third-last item (IDS_SHNEW
, 202, 203) is an index into the STRINGTABLE
for the screen-names of each item. IDS_SHNEW
is a system-defined string.
101 MENU DISCARDABLE
BEGIN
POPUP ""
BEGIN
MENUITEM "DummNewEntry" -1
END
POPUP ""
BEGIN
MENUITEM "Exit" 301
END
END
The final item in each row of RCDATA
says which menu popup to choose from the MENU#101 resource. The first item (0) is actually ignored, since it is overridden by IDM_SHAREDNEW
. The second item (1) points to popup index 1. And the third item (NOMENU
) says that there is no popup here.
The interaction between IDM_SHAREDNEW
and the popup number is a bit idiosyncratic:
IDM_SHAREDNEW
, popup#0
-- uses the global menu, but without an arrow
IDM_SHAREDNEW
, NOMENU
-- global menu, with uparrow, and 'new' acts like a button not a menu.
cmd#201
, popup#0
-- just our own menu, exactly like the others
cmd#201
, NOMENU
-- has the uparrow, and a button, but neither does anything
MakeLayout
The function MakeLayout
is as follows. It's job is to rearrange and refresh windows as necessary according to the current state.
const DWORD mlShowHide=0, mlRefreshDocList=1, mlResize=2;
void MakeLayout(DWORD flags, LPARAM wmsc_lparam=0)
{ ShowWindow(hcard, doc.open?SW_SHOW:SW_HIDE);
ShowWindow(hdlc, doc.open?SW_HIDE:SW_SHOW);
TBBUTTONINFO tbbi; ZeroMemory(&tbbi,sizeof(tbbi));
tbbi.cbSize=sizeof(tbbi); tbbi.dwMask=TBIF_STATE;
tbbi.fsState= (doc.open&&doc.readonly)?TBSTATE_ENABLED
:TBSTATE_HIDDEN;
SendMessage(hmbar,TB_SETBUTTONINFO,203,(LPARAM)&tbbi);
SendMessage(hed,EM_SETREADONLY,doc.readonly?TRUE:FALSE,0);
if (doc.open && !doc.readonly) SetFocus(hed);
Like most PocketPC apps, by default we open documents in 'readonly' mode (indicated by our edit control having ES_READONLY
). The user can click the "Edit" button in the menubar to switch to editing mode.
SHDoneButton(hmain, doc.open?SHDB_SHOW:SHDB_HIDE);
Windows can have either an X, or an OK, or nothing at all in their top right. Notionally the OK is "stronger" than the X: thus, if both are enabled, only the OK will be visible. It's normally to show OK when you're editing a document, and X when you're in DocList
mode.
if (flags&mlResize)
{ SIPINFO si;
SHSipInfo(SPI_GETSIPINFO,wmsc_lparam,&si,0);
int x=si.rcVisibleDesktop.left;
int y=si.rcVisibleDesktop.top;
int w=si.rcVisibleDesktop.right-x;
int h=si.rcVisibleDesktop.bottom-y;
RECT rc; GetWindowRect(hmbar,&rc);
int mh=rc.bottom-rc.top;
if ((si.fdwFlags&SIPF_ON)) h+=1; else h-=mh-1;
MoveWindow(hmain,x,y,w,h,FALSE);
GetClientRect(hmain,&rc);
MoveWindow(hcard,0,0,rc.right,rc.bottom,FALSE);
MoveWindow(hdlc,0,0,rc.right,rc.bottom,TRUE);
MoveWindow(hed,0,0,rc.right,rc.bottom,FALSE);
}
if (!doc.open && (flags&mlRefreshDocList))
{ DocList_Refresh(hdlc);
}
}
It is costly to get the SIP info and also to refresh the DocList
. That's why we control both by flags, to be done only if necessary.
Activation and commands
The responses to WM_ACTIVATE
and WM_SETTINGCHANGE
are standard, required by the system to handle the SIP. Also, we call MakeLayout
, to accommodate the new size of the SIP:
case WM_ACTIVATE:
{ SHHandleWMActivate(hwnd, wParam, lParam, &shai, FALSE);
} return 0;
case WM_SETTINGCHANGE:
{ SHHandleWMSettingChange(hwnd, wParam, lParam, &shai);
MakeLayout(mlResize,lParam);
} return 0;
Also, when our app was running in the background but another instance was launched, then we have to wake up the old instance: either WM_APP
if we just need to activate it, or WM_COPYDATA
if we need to get it to open a document.
case WM_APP:
{ close_on_ok=false;
if (!doc.open) MakeLayout(mlRefreshDocList);
} return 0;
case WM_COPYDATA:
{ if (doc.open && doc.changed) doc.Save();
close_on_ok=true;
COPYDATASTRUCT *cs = (COPYDATASTRUCT*)lParam;
doc.Open((wchar_t*)cs->lpData);
MakeLayout(mlShowHide);
} return 0;
And this is the code to respond to clicks on the menubar and on the OK button:
case WM_COMMAND:
{ int id=LOWORD(wParam);
if (id==IDOK && doc.open) { if (doc.open && doc.changed) doc.Save();
SHSipPreference(hmain,SIP_FORCEDOWN);
doc.open=false; MakeLayout(mlShowHide|mlRefreshDocList);
if (close_on_ok) ShowWindow(hwnd,SW_MINIMIZE);
}
The OK button means that the user has finished editing the current document. That's why we save it. We also force down the SIP. That's because we know the next thing that the user sees will be the DocList
control, and we know that this doesn't need the SIP: without the force-down, the SIP would wait two seconds before hiding.
Note also the close_on_ok
. If the user had opened up most recently from an outside source (e.g. within File Explorer, clicked on an app associated with our app), then a subsequent click on OK should return straight to File Explorer.
In the following, we respond to a menu item Tools > Exit, by terminating. I include this for debugging purposes. The menu item should be removed in the release version of the code.
else if (id==301) { DestroyWindow(hwnd);
}
else if (id==203) { if (doc.open && doc.readonly)
{ doc.readonly=false; MakeLayout(mlShowHide);
}
}
else if (id==IDM_NEWMENUMAX+1) { if (doc.open && doc.changed) doc.Save();
doc.New(); MakeLayout(mlShowHide);
}
} return 0;
case WM_NOTIFY:
{ NMHDR *nmhdr = (NMHDR*)lParam;
if (nmhdr->code==NMN_GETAPPREGKEY)
{ NMNEWMENU *nmnew = (NMNEWMENU*)lParam;
AppendMenu(nmnew->hMenu, MF_ENABLED,
IDM_NEWMENUMAX+1, L"Text");
AppendMenu(nmnew->hMenu, MF_SEPARATOR, 0, 0);
}
else if (nmhdr->code==DLN_ITEMACTIVATED)
{ DLNHDR *dln = (DLNHDR*)lParam;
doc.Open(dln->pszPath);
MakeLayout(mlShowHide);
}
else if (nmhdr->code==DLN_FOLDER)
{ DLNHDR *dln = (DLNHDR*)lParam;
GetPathForFolder(dln->pszPath,dlcfolder);
}
} return 0;
The response to DLN_FOLDER
means that the user has clicked on a folder from the DocList
's drop-down folder list. We call our own function (below) to retrieve this folder's full path. That's so that, when we subsequently save a new document, we can save it into the place the user was looking at. We need a separate function because dln->pszPath
merely gives the folder name, not its full path.
The are bugs in the DocList
control. It's meant to display folders from My Documents and every storage card, and it does, but it only allows the user to select folders from the first storage card or My Documents - secondary storage cards don't work. This is problematic on some devices which have a permanent built-in primary storage card, and external SDs count as secondary! Another problem is that, if a folder "Name" exists in two different locations, it collapses them into just one entry -- unless they happen to have different capitalizations, in which case it collapses them for some purposes (selection) but not for other purposes (display).
struct EnumProjectsInfo
{ const wchar_t *folder; wchar_t *path;
};
BOOL CALLBACK EnumCallback(PAstruct *pa, LPARAM lp)
{ wchar_t *fn;
if (pa->m_IDtype!=FILE_ID_TYPE_OID) fn=pa->m_szPathname;
else
{ CEOIDINFO cinf;
CeOidGetInfo(pa->m_fileOID,&cinf);
fn = cinf.infDirectory.szDirName;
}
const wchar_t *c=fn, *lastslash=c;
while (*c!=0) {if (*c=='\\') lastslash=c+1; c++;}
EnumProjectsInfo *epi = (EnumProjectsInfo*)lp;
if (wcsicmp(epi->folder,lastslash)!=0) return TRUE;
wcscpy(epi->path,fn); return FALSE;
}
void GetPathForFolder(const wchar_t *folder, wchar_t *path)
{ *path=0;
EnumProjectsInfo epi; epi.folder=folder; epi.path=path;
EnumProjectsEx(EnumCallback,0,PRJ_ENUM_ALL_DEVICES,
(LPARAM)&epi);
if (*path==0) SHGetSpecialFolderPath(NULL,epi.path,
CSIDL_PERSONAL,FALSE);
}
}
The document
Our application is a version of Notepad. So, our document window is basically an EDIT
control. (Actually, a CAPEDIT
, which is unique to PocketPC and which capitalizes the first character). Here's how we represent our document:
struct TDocument
{ wchar_t fn[MAX_PATH];
bool open, changed, readonly;
bool unicode;
TDocument() : open(false) {*fn=0;}
void New();
bool Open(const wchar_t *ofn);
void Save();
} doc;
Note at the bottom: we declare a global static variable doc
, for our document. Global variables make PocketPC applications much easier and cleaner. Most of the fields are obvious:
open
indicates whether there is an open document (and hence whether the rest of the flags are meaningful)
readonly
is because existing documents are normally opened in read-only mode; the user has to click Edit to start altering them
changed
says whether any changes have been made (and hence have to be saved)
fn
is the name of the file, if we opened an existing one, empty otherwise.
unicode
says whether this file should be saved to disk in Unicode format. Unicode is native for PocketPC. But if we had opened an ASCII document, it would be rude to convert it.
LRESULT CALLBACK CardWndProc(HWND hwnd, UINT msg,
WPARAM wParam, LPARAM lParam)
{ if (msg==WM_COMMAND)
{ int id=LOWORD(wParam), code=HIWORD(wParam);
if (id==104 && code==EN_CHANGE) doc.changed=true;
}
return DefWindowProc(hwnd,msg,wParam,lParam);
}
Most of the fields are set by the New
/Open
/Save
methods, but changed
is also set when there have been any changes to the edit control, above.
void TDocument::New()
{ *fn=0; SetWindowText(hed,L"");
open=true; changed=false; readonly=false; unicode=true;
}
bool TDocument::Open(const wchar_t *ofn)
{ HANDLE hf=CreateFile(ofn,GENERIC_READ,0,
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if (hf==INVALID_HANDLE_VALUE) return false;
DWORD size=GetFileSize(hf,0); char *buf=new char[size+2];
DWORD red; ReadFile(hf,buf,size,&red,NULL);
buf[size]=0; buf[size+1]=0;
CloseHandle(hf);
The interesting functionality in Open
is to detect whether the file is Unicode or not. If it starts with the Unicode Byte-Order-Mark (BOM), we know it's Unicode. Otherwise, we'll guess, based on whether the first 256 bytes look ASCII-ish or not. That's because many systems save Unicode files without the BOM.
wchar_t *unc=0;
if (buf[0]==-1 && buf[1]==-2) unc=(wchar_t*)(buf+2);
else
{ for (unsigned int i=3; i<256 && i<size && unc==0; i+=2)
{ if (buf[i]==0) unc=(wchar_t*)buf;
}
}
unicode = (unc!=0);
if (unicode) SetWindowText(hed,unc);
else
{ unc = new wchar_t[size];
MultiByteToWideChar(CP_ACP,0,buf,-1,unc,size);
SetWindowText(hed,unc);
delete[] unc;
}
wcscpy(fn,ofn);
open=true; changed=false; readonly=true;
return true;
}
As for Save()
, its complication is that it might have to auto-generate a filename for a newly created document. That's the way of things in the PocketPC: trouble the user as little as possible with pesky things like FileOpen or FileSave. We make use of a trivial function isalphanum()
, which is defined below:
void TDocument::Save()
{ if (!open || !changed) return;
int len=GetWindowTextLength(hed); if (len==0) return;
wchar_t *unc=new wchar_t[len+1];
GetWindowText(hed,unc,len+1);
if (*fn==0)
{ wcscpy(fn,dlcfolder); wchar_t *d=fn+wcslen(fn);
*d='\\'; d++; bool any=false;
int i=0; wchar_t *c=unc;
for (; !isalphanum(*c) && *c!=0 && i<32; i++) {}
for (; isalphanum(c[i]) && c[i]!=0 && i<32; i++)
{ *d=c[i]; d++; any=true;
}
if (!any) {wcscpy(d,L"unnamed"); d+=wcslen(d);}
wcscpy(d,L".txt");
for (i=1; GetFileAttributes(fn)!=0xFFFFFFFF; i++)
{ wsprintf(d,L" (%i).txt",i);
}
}
There's a bug in PocketPC's handling of SD cards. Every now and again, like one time in 400, it simply fails to save a file onto the card -- even though all the functions report success. I don't know what to do about it. One idea is to use FILE_FLAG_WRITE_THROUGH
to avoid lazy write caching. Another is to re-open the file after saving it (maybe even 30 seconds after saving it) to check whether it worked.
HANDLE hf = CreateFile(fn,GENERIC_WRITE,0,NULL,
CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
if (unicode)
{ const char byteorder[2]={-1,-2};
DWORD writ; WriteFile(hf,byteorder,2,&writ,NULL);
WriteFile(hf,unc,sizeof(wchar_t)*len,&writ,NULL);
}
else
{ int size = WideCharToMultiByte(CP_ACP,WC_DEFAULTCHAR,
unc,-1,0,0,NULL,NULL);
char *buf = new char[size];
WideCharToMultiByte(CP_ACP,WC_DEFAULTCHAR,
unc,-1,buf,size,NULL,NULL);
DWORD writ; WriteFile(hf,buf,size,&writ,NULL);
delete[] buf;
}
CloseHandle(hf);
delete[] unc;
changed=false;
}
bool isalphanum(const wchar_t c)
{ if (c>='a' && c<='z') return true;
if (c>='A' && c<='Z') return true;
if (c>='0' && c<='9') return true;
if (c==' ' || c=='-') return true;
return false;
}
The function isalphanum()
assists in auto-constructing save filenames. Our idea: considering up to the first 32 characters, use the first span of alphanumeric characters as the filename.
Postscript
If anyone posts a complaint that the code doesn't compile, with a stdafx.h error, they had better post anonymously -- otherwise I will track them down and bite their heads off. To avoid decapitation, read your compiler documentation on Project Settings > Compiler > Precompiled Headers.
History
- 10 June 2003 - Now, also compiles under PPC2000 (as per Mike Dimmick's suggestion) and under PPC2003 (with eVC4).