纪念一个曾经的软件产品(六)——快捷方式,联系人,任务管理器

[回目录]

之前我曾经尝试过写系列文章,但一直没写成,而这次想把这一系列写完看起来还真不容易,看来我的恒心与行动力确实还是有所欠缺,难怪自己一直都不怎么“成功”。OK……继续吧,能把这一系列写完,对自己而言,一定会是一大进步。

九、功能模块介绍

是时候把各个功能模块的开发故事列一下了,我还是大致地按照开发的顺序来写。

9.1,快捷方式

快捷方式恐怕是最简单的模块了,但做起来比想像的要复杂一些,任何事情,当涉及到越来越多的细节的时候,都会变得复杂,在今天这个推崇“极简主义”(Minimalism)的时代,SoSoPi的代码看起来并不好。快捷方式的基本功能就是摆几个程序的大图标出来,让用户方便启动这些程序,我将快捷方式的数量限制为32个,原因是:摆放了太多的图标的话其实也不快捷了,所以鼓励用户把最常用的几个程序的图标摆出来。

如何获取系统的全部程序?我的方法是用“SHGetSpecialFolderPath(NULL, szStartMenuPath, CSIDL_PROGRAMS, 0);”来获取开始菜单的目录,遍历这个目录下的lnk文件,这些就是指向程序的快捷方式,也就是我们要获取的东西。这里面可能比较困难的是获取程序的图标,需要用到一些Shell函数,稍微使用不当就会导致资源泄漏,从而让程序莫名其妙地出现黑屏之类的问题(资源枯竭),我调试这个花了不少时间,Windows Mobile的API的一些行为并不像文档上所描述的那样,这个真是很为难开发者。

Windows Mobile的快捷方式的扩展名和Windows的快捷方式的扩展名是一样的,都是lnk,但格式却不同,Windows Mobile的lnk是个文本文件,上面直接有指向目标的完整地址,而Windows 的则是个二进制格式的文件,得通过一些接口来创建和读取,看来微软内部确实不怎么团结,每个部门都喜欢搞自己的一套,而Windows Mobile的lnk的格式,我记得是没有文档描述的,我是自己摸索出的。

另外还值得一提的就是快捷方式的的图标编辑的这个界面,确实是花了一些心思的:

当手指拽着一个图标往下拖的时候,拖到了底部,屏幕会自动向上滚,这个“理所当然”的功能是怎么实现的呢?——当然是用Timer,当程序检测到用户在拖拽图标,并且靠近了底部的时候,就启动滚动内容这个Timer,让内容向上滚,当用户拖拽图标离开了底部区域之后,就停止这个Timer,滚动也就停止了,手指松开,图标也就落在了松开的位置的两个图标之间的位置。向上拖拽图标也是一样的道理。大致就是这样……是的,大致。

这个界面还允许用户修改图标的图片,修改快捷方式的名称,和删除快捷方式。这是一个很不错的操作方式:

其实这是仿HTC Sense的:

SoSoPi别的地方也有类似的调整,做法也差不多。

9.2,联系人

联系人模块和快捷方式模块很类似,只是内容的获取方式有些不一样。

首先是联系人的获取方式,两种方式,一种是SIM卡中的联系人,一种是手机通讯录中的联系人,我还真有些不明白为什么还有用户(而且数量不少)喜欢把联系人存在SIM卡上,问原因无非就是方便换手机,可现在都已经是智能机时代了啊,通讯录的同步方法太多了,怎么还用这么原始的方式呢?可事实就是这样,还有许多人用SIM卡联系人的。SIM卡联系人的获取方式跟手机通讯录联系人的获取方式是不同的,前者通过一个Windows API来读取,而后者则通过Outlook的接口来获取。SIM卡联系人仅仅有一个名称和一个号码,而手机通讯录的联系人则可以带很多信息,手机、办公电话1、办公电话2、家庭电话、传真、家庭住址、公司地址……甚至还能自定义一些信息,我也是花了一番心思才了解上面那些字段究竟什么意思。(资料缺乏,Windows Mobile的开发文档真是一团糟啊)我现在就把用Outlook接口能获取到的联系人的信息列一下:

    pContact->get_MobileTelephoneNumber(&bstrMobileTelephoneNumber);       //手机号码
    pContact->get_HomeTelephoneNumber(&bstrHomeTelephoneNumber);           //家庭电话
    pContact->get_Home2TelephoneNumber(&bstrHomeTelephoneNumber2);         //家庭电话2
    pContact->get_HomeFaxNumber(&bstrHomeFaxNumber);                       //家庭传真
    pContact->get_BusinessTelephoneNumber(&bstrBusinessTelephoneNumber);   //商务电话
    pContact->get_Business2TelephoneNumber(&bstrBusinessTelephoneNumber2); //商务电话2
    pContact->get_BusinessFaxNumber(&bstrBusinessFaxNumber);               //商务传真号码
    pContact->get_CarTelephoneNumber(&bstrCarTelephoneNum);                //车辆电话
    pContact->get_AssistantTelephoneNumber(&bstrAssistantTelephoneNumber); //助理电话
    pContact->get_RadioTelephoneNumber(&bstrRadioTelephoneNumber);         //无线电话号码
    pContact->get_PagerNumber(&bstrPagerNumber);                           //寻呼机号码
    pContact->get_Email1Address(&bstrEmailAddress);                        //电子邮件
    pContact->get_Email2Address(&bstrEmailAddress2);                       //电子邮件2
    pContact->get_Email3Address(&bstrEmailAddress3);                       //电子邮件3

应该说这么个列表是很不人性化的,或者说很不符合“国情”,像车辆电话,助理电话,无线电话,寻呼机这种东西我们几乎没有一个人会用到,而多个手机号码却很寻常,可它又没有,另外Windows Mobile的默认的联系人面板是也很不人性化的,由于它“假定”你的联系人只有一个手机号码,所以它这么显示:

图中这位联系人的工作电话明显是一个手机号码,但我却无法给这个手机号码发短信,它就不能很方便地选择给哪个号码发短信。这是SoSoPi的改进:

先选择一个联系方式条目,然后再选择要执行的操作,我想这个更符合用户的需求,大多数时候,用户其实也就这些需求:拨打这个号码,拨打这个号码(加拨IP电话前缀),给这个号码发短信和给这个Email地址写信,我自己用下来感觉这样的联系人面板是非常实用的。当然了,到了今天这个移动互联网时代,就远远不止这些动作了,还有什么?发微信,微博“艾特”,还有些别的什么SNS工具啥的,总之很多了,所以这个联系人面板可能又不够用了,当然现在新版本的智能手机软件都能处理这些了;而由于3G号码的普及,IP电话现在已经没什么人用了。

实现方法嘛,电话拨号用的是TAPI(即Telephone API)的一个函数:

tapiRequestMakeCall(pStrToAct, NULL, NULL, NULL);

pStrToAct既是要拨打的号码,如果是要拨打IP电话的,就在要拨打的号码之前加上“17951”(或者别的)。而发短信和发邮件,最正规的做法则是需要用到MAPI,这是一套规则相当复杂的调用,标准的微软的COM技术(现在已经不怎么用,渐渐被.net的相关技术取代),当然也有比较简单的实现,我用的就是这种比较简单的实现,那就是利用“\windows\tmail.exe”这个程序,给它带上些适当的参数即可。

这是发送短信的命令行:

tmail.exe -service "SMS" -to "名字<号码>"

这是发送邮件的命令行:

tmail.exe -service "ActiveSync" -to "收件人名字<Email地址>"

发短信的这个"SMS"参数还好理解,发邮件的这个"ActiveSync"就有点难理解了,我估计这又是一个有历史原因的问题。

最后谈谈“全部联系人”的检索,这个是十分难做的UI,原因有三:

  1. Windows Mobile默认不能处理汉字拼音序;
  2. 汉字有些多音字难处理,例如“曾”字到底是ceng还是zeng?如果是前者,那么会排在前面,如果是后者那就排在后面;
  3. 把横竖屏这种问题考虑进去之后,UI设计就变得很困难。

这是马尼拉的“全部联系人”界面,(其实它也是仿iPhone的)应该说这个界面已经做得足够好了,虽然还是有些小毛病,比如多音字排序,但已经很好了,要实现这么一个界面还真不容易,我考虑了很多种设计之后,觉得自己水平确实还是比较有限的,于是在这里弄得就比较山寨了,或者说也算有自己的特色吧:

即便这样,我也不得不克服一个难题,那就是汉字的拼音序,如果是全英文,那好办啊,B比A大,C比B大,但汉字,如何确定李比陈大呢?还是得先获取它们的拼音对吧,但经过研究,我发现绝大多数汉字其实都是多音字,我总不能自己从头到尾弄一张汉字拼音表吧,常用汉字3000,次常用汉字5000,使用中的汉字大约有8000,但汉字总和,那就多了……总之没底,我不可能去弄这么一个程序。最后我想到了一种办法:能不能不去考虑汉字的拼音,我只需要一个排好序的汉字列表,我只需要知道哪个汉字在前,哪个在后不就可以了?于是我到网上去找了这么一个列表,一共20898个汉字,应该说已经是一个汉字大全了,里面的大多数字我们都见不着,由于这些字都是排好序的,我只需要知道第一个C开头的汉字和第一个D开头的汉字在这个列表中的位置,我就能判断任意一个汉字是否C开头(落在这两个位置之间就是了),初一看,要在一个两万多成员的列表中找出一个汉字的位置,会有点慢,后来我也试过了,速度嘛,也还能接受,也就在模块资源加载的时候去查找一番,所以也没做什么优化。这个长长的汉字列表是以一个文本文件的形式存放的,就在SoSoPi的程序目录下,叫Phonetic.dat。

后来我从事.net开发,也遇到了需要拼音的问题,但由于微软提供了一些比较出色的支持库,解决起这些问题来就比较轻松了。

9.3,任务管理器及开关项

9.3.1,Windows Mobile的“X”按钮

Windows的“X”按钮的作用当然是关闭一个窗口,关闭一个窗口有时也意味着关闭一个程序;而Windows Mobile的“X”按钮,在默认情况下,却不是用来关闭一个窗口,尽管你认为它“关闭”了,默认情况下,这个按钮的作用叫“智能隐藏”,当然它的行为是可以调整的,所以我前面一再强调“默认”,“智能隐藏”其实就是隐藏,并非真的关闭,但这样一个动作就相当于告诉系统,这个程序“可以被关闭”了,于是系统会在它认为的适当的时候把程序关闭掉。这样做有利于你“关闭”一个程序之后,又马上把他“启动”起来,其实系统做的只是把它隐藏掉,然后重新Show出来而已,开销很小,有利于省电。这种设计思想跟iPhone是很类似的,那就是一般情况下,你不需要“关闭”一个程序,iPhone甚至都没有关闭按钮。

但,习惯了Windows操作的人对于这个特性就有些不爽,总想着点了“X”就要关闭一个程序,否则这东西占内存,内存占用多,他们就会浑身不舒服,但从技术上来说,好的程序就是要尽量地多用内存来减少CPU的负担和提高用户体验,而不是看起来“很省内存”……好吧,这不是个技术问题,这是个使用习惯的问题,我且称之为“内存洁癖”,就好像现在许多用户在自己的电脑上装个360安全卫士,此卫士时不时会报“内存需要整理了”,于是点一下“整理”,卫士说释放了多少多少内存,于是就觉得很爽,实际上也不知道卫士到底干了些什么……好了,扯远了,还是回来说说SoSoPi吧。

9.3.2,列出所有进程

程序的一个运行实例被成为一个进程,要知道系统当前有哪些进程并不难,有专门的API可以做这个事情。这个API函数叫“CreateToolhelp32Snapshot”,看字面意思就知道,是做一个快照,因为进程时时刻刻可能在变,所以要先做一个快照,再对它进行一次遍历,就可以知道有哪些进程在跑。

快照里并不包含图标信息,但包含了进程的可执行文件的路径,我用了一个Shell函数去抓取这个可执行文件的图标,并把它绘出来,才有了上面这个截图的效果。

SoSoPi这个显示进程的界面也只是有显示的功能,并不具备别的功能,比如结束进程,这个功能我没有做,其实要做并不难,有API函数叫“TerminateProcess”,用来强制结束进程,但这样可能带来一些不可预知的严重后果,比如机器死机之类的,而且我试验下来很多时候这个方法并不奏效,不能有效地结束进程,所以我最后没把这个功能放进去。

另外也许你要问,为什么不把进程所占用的内存数也显示出来?在这里,我可以相当负责任地说,你所看到的告诉你一个程序占多少多少内存的软件,都在撒谎,因为一个进程究竟占多少内存,这个恐怕谁也说不准,这是一个复杂的问题,我也可以告诉你一个程序占用了多少工作集(Working Set),或者提交了多少虚拟内存,但这些都不能准确反映出这个进程占多少内存,不是一般用户所理解的那样,另外:“占多少内存”这个意义真的不大。具体可以看看我很早以前的一篇blog:《dll占的究竟是谁的空间?——浅谈Windows内存机制》。

9.3.3,任务及任务管理

这就是所谓的“任务管理器”:

什么是“任务”?Windows Mobile本身并没有这个概念,我认为,所谓“任务”就是一个有显示界面的,正在运行的非系统程序。我经过了许多的尝试,才写出一个这么个“任务管理器”来,我的做法是这样的:用EnumWindows这个API函数遍历所有的窗口,去除子窗口,去除带WS_POPUP属性的窗口(这类窗口有可能是系统设置或者消息框之类的),去除不可见的窗口,剩下的这些窗口里面再做这样的筛选:用GetWindowThreadProcessId获取窗口的进程ID,在之前用CreateToolhelp32Snapshot这个API做的进程快照里查找这个进程ID,如果能查找到,并且这个进程不是系统进程,那么就认定这是一个“任务”。简单地说,有可见的顶层窗口,并且不属于系统进程,那这是一个任务。

系统进程有哪些呢?我这里列一下:

  • connmgr.exe
  • device.exe
  • filesys.exe
  • gwes.exe
  • nk.exe
  • services.exe
  • shell32.exe
  • cprog.exe
  • repllog.exe

我怎么知道这些是系统进程?一部分是网上搜索,另一部分自己摸索。接下来:

  • 用SetForegroundWindow将任务的窗口切换至前台
  • 用PostMessage发送WM_CLOSE消息来尝试结束一个任务

当然,如果任务自己不肯结束的话,我发WM_CLOSE消息过去也是无用的。

9.3.4,状态显示

这是SoSoPi的系统状态显示:

Storage Card这个没什么好说,就是存储卡,利用FindFirstFlashCard/FindNextFlashCard这对API函数可以遍历系统所有的存储卡(当然,我还没见过有超过一张存储卡的机器),然后用GetDiskFreeSpaceEx这个API函数来获取存储卡的容量及剩余空间大小。

而“对象存储”这是什么意思?如果要详细了解,还真得花点时间,这里我就把它简单通俗点吧,就是你的机器上(非存储卡)的存储空间。其实这样解释并不准确,我没办法用一句话准确地把这个概念的来龙去脉说清楚,有兴趣的话请看我以前的一篇博客:《浅析Windows Mobile内存机制》,同样地,可以用GetDiskFreeSpaceEx这个API函数来获取其容量及剩余空间大小。

“程序内存”也同样是一个无法一句话说清楚的概念,一样地,可以参考我上面提到的这篇博客。如果你没兴趣看,那你也可以将“程序内存”理解为“内存”,当然这样肯定也不太准确。程序内存的值可以通过GlobalMemoryStatus来获取到。

而电池和信号强度则不是在这里获取的,具体获取方法可以看之前的关于任务栏系统通知的描述。

9.3.5,开关

SoSoPi准备了四种开关:蓝牙开关、WIFI开关、电话开关和断开电话数据连接。

不得不说,这些开关的实现很“琐碎”,并不直接,我甚至用了一些比较投机取巧的方法(比如直接获取并使用未公开的API等),所以不能确保这些方法在所有的手机上都能用,Windows Mobile并没有好好地把这些规范化好。以WIFI为例(当然蓝牙的开关也是够复杂的):

BOOL CUIyProcess::TurnWifi(BOOL bOn)
{
    //从注册表中获取WiFi设备名
    //位置: HKLM\Software\System\CurrentControlSet\Control\Power\State
    //键名(一种情况): \{98C5250D-C29A-4985-AE5F-AFE5367E5006}\CF8385PN1
    HKEY hKey = NULL;
    RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("System\\CurrentControlSet\\Control\\Power\\State"), 0, 0, &hKey);
    if(hKey!=NULL)
    {
        long error = 0;
        DWORD index = 0;
        TCHAR (&name)[COMMON_BUFF_LENGTH] = g_szCommBuff;
        DWORD namelength = COMMON_BUFF_LENGTH;
        BOOL bFound = FALSE;
        while(ERROR_SUCCESS==RegEnumValue(hKey, index, name, &namelength, NULL, NULL, NULL, NULL))
        {
            //{98C5250D-C29A-4985-AE5F-AFE5367E5006}
            if (namelength>38)
            {
                if (0==memcmp(name, TEXT("{98C5250D-C29A-4985-AE5F-AFE5367E5006}"), sizeof(TCHAR)*38))
                {
                    bFound = TRUE;
                    break;
                }
            }
            index++;
            namelength = COMMON_BUFF_LENGTH;
        }

        if (bFound)
        {
            if (bOn)
            {
                DevicePowerNotify(name, D0, 1);
                SetDevicePower(name, 1, D0);
            }
            else
            {
                DevicePowerNotify(name, D4, 1);
                SetDevicePower(name, 1, D4);    
            }
        }

        RegCloseKey(hKey);
        hKey = NULL;

        if(bFound)
            return TRUE;
    }
    return FALSE;
}

从代码上看出来,我是通过尝试对GUID为“{98C5250D-C29A-4985-AE5F-AFE5367E5006}”的设备进行操作来开关WIFI的,这种方法是肯定有问题的,难道Windows Mobile没有提供开关蓝牙的API?——还真的没有。究其原因,我想是因为WIFI出现相对较晚,在Pocket PC那个年代,还基本上没有,(我2007年买的Samsung i718就不带WIFI)所以WIFI看起来就像是后来“临时加”的功能。

最后“断开电话数据连接”这个功能如今看起来有些奇怪,其实在过去3G网络普及之前,这是个很有用的功能,因为那时候手机上网费贵,用户很在意自己的手机是否还连着网络,一旦连着,心就不安了,于是点一下这个,电话数据网络就断开了。

[回目录]

posted @ 2013-05-12 23:25  guogangj  阅读(818)  评论(0编辑  收藏  举报