一个典型的 PocketPC 程序
一个用纯 Win32/C++ 编写的完整 PocketPC 应用程序。
引言
大多数 PocketPC 编程都是使用向导在 MFC 中完成的。如果你喜欢用纯 Win32/C++ 编程,你会发现文档和示例不够多。本文和随附的源代码提供了一个完整的最小示例——一个记事本克隆。它涵盖了:
DocList
控件(即首页文件列表,就像 Pocket Word 中一样)- “卡片”架构(PocketPC 将打开的文档窗口称为“卡片”)
- 软输入面板(SIP,键盘/笔迹识别器)
我没有尝试将 Windows 功能封装到我自己的 API 中。那是因为我认为人们想学习 Windows API 本身,而不是我自己的 API。而且,PocketPC 的 Windows API 已经相当简洁了。PocketPC 实际上是 Win32 的唯一原生实现:它不依赖于像 NT/Win2K/XP 这样的内部层,也不依赖于像 Win95/98/ME 这样的 16 位。
程序结构
PocketPC C++ 程序使用 Microsoft eMbedded Visual Tools 3.0(免费)编写。你还应该下载 Giuseppe Govi 为 eVC++ 提供的免费 STL 移植。尽管本文没有使用它。
要为 PocketPC 编写简洁的程序,你必须(正如尤达所说)忘掉你所学到的东西
- 保持较小的内存占用。
- 一次永远不会有多个你的应用程序实例运行;一次也不会打开多个文档。
- 因此,全局变量很好。每个应用程序的变量可以是全局的。每个文档的变量可以是全局的。
- 由于变量是静态声明的,而不是动态创建的,这意味着运行时无需进行内存分配。这是一件好事。
顺便说一下,如果“WCE 配置”工具栏不可见,开发环境通常无法构建。(这是带有下拉菜单用于选择目标操作系统、目标设备、目标处理器的工具栏)。
准备工作
这些是头文件和应用程序特定的全局变量。此外,在 Project > Settings > Linker > Input 下,你必须链接到 doclist.lib、aygshell.lib 和 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 // newmenu.h was already #include <newmenu.h> // included in PPC2000 #endif HINSTANCE hInstance; HWND hmain=0; // Main window HWND hmbar=0; // Menu-bar HWND hdlc=0; // DocList control HWND hcard=0; // Document window HWND hed=0; // Edit window inside Doc-window wchar_t dlcfolder[MAX_PATH]={0}; // current DLC folder SHACTIVATEINFO shai={sizeof(SHACTIVATEINFO),0,0,0,0,0}; bool close_on_ok=false;
窗口布局:有一个主窗口 hmain
。它拥有一个菜单栏 hmbar
,菜单栏位于其下方。在客户区中,有一个 DocList
控件 hdlc
和一个文档窗口 hcard
占用相同的区域;一次只有一个可见。我们的应用程序是一个记事本克隆,所以我们在里面有一个编辑窗口 hed
。
变量 dlcfolder
保存了 DocList
当前选定的文件夹。这是因为,当保存新文档时,它会告诉我们将文档放在哪个文件夹中。
SHACTIVATEINFO
shai
由系统用于维护 SIP 的状态:我们必须在 WM_ACTIVATE
和 WM_SETTINGCHANGED
中进行标准调用,涉及此结构,以使 SIP 正常运行。
而 close_on_ok
是出于以下原因。如果用户在文件资源管理器中点击了关联的文档并打开了我们的应用程序,那么“确定”按钮应该关闭我们并返回到文件资源管理器。但是,如果用户从“开始”菜单打开我们,那么关闭文档应该将用户返回到主 DocList
控件。此标志记录了最近指示的关闭模式。
// At the start of WinMain: hmain = FindWindow(L"LuMainClass",L"My App"); if (hmain!=0) { SetForegroundWindow((HWND)((ULONG)hmain|1)); // the |1 will activate any owned windows 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; }
为了节省内存,我们的应用程序应该只加载一个实例。当我们加载时,我们检查实例是否已存在。如果我们加载时带有要打开的文件名参数,我们必须将此文件名传达给另一个实例:因此使用 WM_COPYDATA
。我们使用 WM_APP
来发出通用唤醒信号,然后执行一些刷新。
// within WinMain, after registering classes: 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); }
使用 SHInitExtraControls()
允许我们使用 CAPEDIT
(类似于普通的 EDIT
,但它会将第一个字母大写)和 WC_SIPPREF
(SIP 的自动管理)。
管理窗口
我们从应用程序窗口的创建和销毁开始。请注意,我们的大多数子窗口不需要销毁,因为它们会自动销毁。例外是菜单栏,必须显式销毁。
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;
WM_DESTROY
消息是一个奇怪的案例。在程序的正常运行过程中,它永远不会被调用。那是因为,如果用户点击“关闭”按钮,它不会关闭,而只是最小化应用程序(这样,随后,应用程序可以立即打开)。如果系统内存不足,它会发送 WM_HIBERNATE
消息,要求应用程序清理自身。如果内存真的不足,它会发送 WM_CLOSE
,这(在 DefWindowProc
中)会调用 DestroyWindow
,从而最终触发我们的 WM_DESTROY
。
菜单栏的资源如下。我在这里写出来是因为它似乎没有在其他地方记录。
// A resource file for a menubar #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
此 RCDATA
用于菜单栏。第一行(101,3)表示查找 MENU#101,并且此菜单栏中有三个项目。在每一行中,第二个项目 (IDM_SHAREDNEW
, 203, 203) 是点击时将发送的 WM_COMMAND
ID。这里,“Shared New”表示这是一个系统将填充所有标准项目(任务、备注等),然后提供给我们填充其余部分的菜单。
STRINGTABLE DISCARDABLE BEGIN 201 "DummyNew" 202 "Tools" 203 "Edit" END
在 RCDATA
的每一行中,倒数第三项 (IDS_SHNEW
, 202, 203) 是 STRINGTABLE
中每个项目屏幕名称的索引。IDS_SHNEW
是一个系统定义的字符串。
101 MENU DISCARDABLE BEGIN POPUP "" BEGIN MENUITEM "DummNewEntry" -1 END POPUP "" BEGIN MENUITEM "Exit" 301 END END
RCDATA
每行的最后一项说明从 MENU#101 资源中选择哪个菜单弹出窗口。第一项 (0) 实际上被忽略了,因为它被 IDM_SHAREDNEW
覆盖了。第二项 (1) 指向弹出索引 1。第三项 (NOMENU
) 表示这里没有弹出窗口。
IDM_SHAREDNEW
和弹出窗口编号之间的交互有点特殊
IDM_SHAREDNEW
,popup#0
-- 使用全局菜单,但没有箭头IDM_SHAREDNEW
,NOMENU
-- 全局菜单,带上箭头,“新建”功能像按钮而非菜单。cmd#201
,popup#0
-- 只是我们自己的菜单,与其他菜单完全相同cmd#201
,NOMENU
-- 有上箭头和按钮,但两者都无任何作用
MakeLayout
MakeLayout
函数如下。它的工作是根据当前状态,根据需要重新排列和刷新窗口。
// MakeLayout - this shows/hides the edit/doclist as approp- // riate, resizes according to SIP, refreshes the DocList. // 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); // the 'edit' button 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); // readonly in the card itself SendMessage(hed,EM_SETREADONLY,doc.readonly?TRUE:FALSE,0); if (doc.open && !doc.readonly) SetFocus(hed);
像大多数 PocketPC 应用程序一样,默认情况下我们以“只读”模式打开文档(由我们的编辑控件具有 ES_READONLY
指示)。用户可以点击菜单栏中的“编辑”按钮切换到编辑模式。
// OK/X. If OK is hidden, then X will be visible SHDoneButton(hmain, doc.open?SHDB_SHOW:SHDB_HIDE);
窗口的右上角可以显示 X、OK 或什么都没有。从概念上讲,OK 比 X“更强”:因此,如果两者都启用,则只会显示 OK。通常在编辑文档时显示 OK,而在 DocList
模式下显示 X。
// Now resize the windows to accommodate SIP. If this // is called from WM_SETTINGSCHANGE, then use the same // lParam for efficency; otherwise set wmsc_lparam=0 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); } // We need to refresh after files have been changed, // to reshow their time/size if (!doc.open && (flags&mlRefreshDocList)) { DocList_Refresh(hdlc); } }
获取 SIP 信息和刷新 DocList
的成本都很高。这就是为什么我们通过标志来控制它们,只有在必要时才执行。
激活和命令
WM_ACTIVATE
和 WM_SETTINGCHANGE
的响应是标准的,系统要求它们处理 SIP。此外,我们调用 MakeLayout
,以适应 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;
此外,当我们的应用程序在后台运行但启动了另一个实例时,我们必须唤醒旧实例:如果我们只需要激活它,则使用 WM_APP
;如果我们需要让它打开文档,则使用 WM_COPYDATA
。
case WM_APP: { // we are a background instance being reactivated close_on_ok=false; // maybe have to refresh the doclist. if (!doc.open) MakeLayout(mlRefreshDocList); } return 0; case WM_COPYDATA: { // we are a background instance being told to open a doc. 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;
这就是响应菜单栏和“确定”按钮点击的代码。
case WM_COMMAND: { int id=LOWORD(wParam); if (id==IDOK && doc.open) // OK button { 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); }
“确定”按钮表示用户已完成当前文档的编辑。这就是我们保存它的原因。我们还强制关闭 SIP。那是因为我们知道用户接下来看到的是 DocList
控件,而且我们知道它不需要 SIP:如果不强制关闭,SIP 将等待两秒钟才会隐藏。
另请注意 close_on_ok
。如果用户最近从外部来源(例如,在文件资源管理器中,点击了一个与我们的应用程序关联的应用程序)打开,那么随后点击“确定”应该直接返回到文件资源管理器。
在以下内容中,我们通过终止来响应菜单项 工具 > 退出。我将其包含用于调试目的。此菜单项应在代码的发布版本中删除。
// ... continued from WM_COMMAND else if (id==301) // tools.exit { DestroyWindow(hwnd); } else if (id==203) // tools.edit { if (doc.open && doc.readonly) { doc.readonly=false; MakeLayout(mlShowHide); } } else if (id==IDM_NEWMENUMAX+1) // new.text { 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) { // Called by the system for us to fill in our New menu. 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;
对 DLN_FOLDER
的响应意味着用户点击了 DocList
下拉文件夹列表中的一个文件夹。我们调用我们自己的函数(如下)来检索此文件夹的完整路径。这样,当我们随后保存新文档时,我们可以将其保存到用户正在查看的位置。我们需要一个单独的函数,因为 dln->pszPath
仅提供文件夹的名称,而不是其完整路径。
DocList
控件存在错误。它旨在显示“我的文档”和所有存储卡中的文件夹,并且它确实如此,但它只允许用户从第一个存储卡或“我的文档”中选择文件夹——辅助存储卡不起作用。这在某些设备上存在问题,这些设备具有永久内置的主存储卡,而外部 SD 卡被视为辅助存储卡!另一个问题是,如果文件夹“名称”存在于两个不同的位置,它会将其折叠成一个条目——除非它们恰好有不同的字母大小写,在这种情况下,它会出于某些目的(选择)折叠它们,但不会出于其他目的(显示)。
// GetPathForFolder(const wchar_t *folder, wchar_t *path): // Given a folder name we return its path // All Folders --> \My Documents // Business --> \My Documents\Business // MySdFolder --> \Storage Card\MySdFolder // Assume path>=MAX_PATH. 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; // TRUE means keep enumerating wcscpy(epi->path,fn); return FALSE; } void GetPathForFolder(const wchar_t *folder, wchar_t *path) { // assume 'path' is at least MAX_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); } }
文档
我们的应用程序是记事本的一个版本。因此,我们的文档窗口基本上是一个 EDIT
控件。(实际上是 CAPEDIT
,它在 PocketPC 中独有,并且会将第一个字符大写)。以下是我们表示文档的方式
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;
注意底部:我们为文档声明了一个全局静态变量 doc
。全局变量使 PocketPC 应用程序更容易、更简洁。大多数字段都很明显。
open
表示是否有打开的文档(以及其余标志是否有效)readonly
是因为现有文档通常以只读模式打开;用户必须点击“编辑”才能开始修改它们changed
表示是否进行了任何更改(因此需要保存)fn
是文件的名称,如果我们打开现有文件,否则为空。unicode
表示该文件是否应以 Unicode 格式保存到磁盘。Unicode 是 PocketPC 的原生格式。但是,如果我们打开了一个 ASCII 文档,将其转换为 Unicode 会很粗鲁。
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); }
大多数字段由 New
/Open
/Save
方法设置,但 changed
也在编辑控件有任何更改时设置,如上所述。
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);
Open
中有趣的功能是检测文件是否为 Unicode。如果它以 Unicode 字节顺序标记 (BOM) 开头,我们就知道它是 Unicode。否则,我们将根据前 256 个字节是否看起来像 ASCII 来猜测。那是因为许多系统保存 Unicode 文件时不带 BOM。
// Unicode detector: 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; }
至于 Save()
,它的复杂之处在于它可能需要为新创建的文档自动生成文件名。这是 PocketPC 的方式:尽可能少地用诸如“文件打开”或“文件保存”之类的烦人事情来打扰用户。我们使用一个简单的函数 isalphanum()
,它定义在下面。
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); // To construct the filename from the doc's content... 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); } }
PocketPC 对 SD 卡的处理存在一个 bug。每隔一段时间,大约 400 次中有一次,它就是无法将文件保存到卡上——即使所有函数都报告成功。我不知道该怎么办。一个想法是使用 FILE_FLAG_WRITE_THROUGH
来避免懒惰的写入缓存。另一个想法是在保存文件后(甚至在保存 30 秒后)重新打开文件以检查是否成功。
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; }
函数 isalphanum()
有助于自动构建保存文件名。我们的想法是:考虑最多前 32 个字符,使用第一个字母数字字符序列作为文件名。
Postscript
如果有人抱怨代码无法编译,并出现 stdafx.h 错误,他们最好匿名发布——否则我将找到他们并拧下他们的头。为了避免斩首,请阅读您的编译器文档中关于 项目设置 > 编译器 > 预编译头 的内容。
历史
- 2003 年 6 月 10 日 - 现在,也可以在 PPC2000(根据 Mike Dimmick 的建议)和 PPC2003(使用 eVC4)下编译。