Windows mobile开发总结--文本控件篇 - 行者无疆 始于足下 - 行走,思考,在路上
Windows mobile开发总结--文本控件篇
这个项目是在自己已有的代码基础上,在windows mobile 6.x平台下开发出一套商旅个人助理软件,软件主要内容是世博相关的机票、酒店、火车票、行程安排等,界面要求是仿照Iphone风格。因为客户公司有了Iphone和Android平台上的成品,但是可能时间紧急,又缺乏mobile平台上的控件代码基础,所以找到了我们这边。
原来的代码基础是一套GUI的控件引擎。利用GAPI,从最底层写起,包括最基本的画点画线函数、显存操作函数、窗口管理函数、控件类继承体系等等。这套体系的第一个产品是宁波公安的移动警务系统。去年的时候我也曾经有所了解,但是终究因为自己课业繁忙没有真正参与进来。这次是个机会。
开始的时候我对这套东西很是抵触。一来是我本身不太欣赏windows的哲学。在进入实验室之前的半年都在linux下生活;二是我觉得重新写这么一套东西是"reinvent the wheels"的愚蠢做法。我还像导师建议Qt。坦白的说,虽然我从来没有用Qt写过一个像样的东西,但是Qt的信号与槽机制,感觉要优雅的多;三是除了Qt,MiniGUI据说也不错,为什么要自己大费周章地写这么一个东西呢?而且我虽然很菜,但是写这么一坨代码,我还是能看出很多问题的——虽然让我去写我肯定写不出特别好的东西。
事实上:
- 我很讨厌某些宏。大量的宏让程序显得莫名其妙。Win32 API中有大量的宏,什么handle, hwnd, COLORREF等等,虽然这些宏归根到底不过是个int,但是我觉得很多时候还是KISS原则最重要。直接用int好了。干嘛这么麻烦。
- 我很讨厌某些typedef。明明有了标准的true和false,为什么还要自己去定义个GTrue、GFalse和GOK呢?还有GRESULT?这大概是受windows编程风格的影响吧。
- 我很讨厌注释不全、命名风格糟糕的代码。
当然,GUI框架的设计是一件很复杂的事情。具体来说可以分为三层:
- 底层引擎层:包括基本的显存操作,画点画线,alpha渲染,图片压缩载入,图形学算法、消息事件处理、字体引擎、布局管理器、窗口管理器等等。
- 控件层:控件继承体系、文本类控件(标签,单行文本编辑,多行文本编辑域),按钮类(button、list等)、滚动条类、光标类、复杂控件类(菜单、表格等等)
- 应用层:这个算最简单的了。只要前两层足够稳定,应用层是手到擒来的事情。其余的事情如数据库连接、xml解析、网络连接等等,属于额外附加功能。
这里有份GUI系统需求描述,可以参考一下。嵌入式的GUI框架还是要简单一些。当然,这也与嵌入式系统的硬件条件是息息相关的。
实验室的这套框架主要是仿照Windows消息机制写的。很多东西保留了Win32 API的痕迹。事实上底层引擎是不可能脱离具体的操作系统而存在的。跨平台的原理就是在统一的接口下面提供不同的内部系统实现。
最开始接触这个项目的时候我心中很是没底。一来我从来没有接触过4W+行代码的项目,二来我对windows消息机制那一套也不是很了解。所以我就花了将近200大洋买了经典的《windows程序设计》,自己在那里面吭哧吭哧啃了好几天,看了200多页。终于算对windows消息循环机制有了初步的了解。
3月28号开会,3月29号熟悉整个系统体系。开始做控件。我被安排的任务是做文本类的控件:
- ILabel:静态文本标签。
- ITextField:单行文本编辑框。
- ITextArea:多行文本编辑框。
三个文本控件统一要求:
- 支持png图片载入
- 支持字体类型和大小,
- 字体颜色,
- 背景颜色,
- 圆角透明。
- 支持编辑锁定和光标定位。
说起来容易做起来难。底层控件的开发往往比上层应用的开发要难的多。写到现在,代码总量写了180k,估摸着6000行应该有的(虽然有很多框架性copy&paste的东西)。总共写了3个文本空间和8个窗口界面。3:8,由此可见一个小小的文本编辑框并不是我们想象中的那么容易。具体来讲,在画点画线画矩形这写仅有的GDI函数的基础上,你需要实现:
- 如何支持不同的风格(字体颜色,大小,背景颜色,alpha渲染风格);
- 如何支持png图片的载入(我设计的单行文本框支持一张mainPng和两张leftPng、rightPng的载入,并根据鼠标是否点击在leftPng和rightPng图片上进行不同的处理,如清空文本,确定查询等等,载入png事件简单的事情,但是由此引发的光标定位问题却让我无比头疼);
- 如何支持光标定位(这是文本框设计中非常复杂的一个问题。可能你不相信,等宽字体(monospace)的发明就是因为早期的电脑画面显示、打字机,由于技术的局限,无法进行字母宽度的比例调整,因此将每个字元都制作成一样的宽度)。说到这里应该明白了吧。当你点击光标的时候,需要根据你的光标的坐标,判断在整个字符串中最合适的位置。总不能让光标在一个字体的中间闪烁吧……原理也是很简单的,无非就是建立个链表,每个node的结构如下:
struct char_node { wchar_t * ch; int ch_width; int ch_height; }
然后根据光标的位置从头开始遍历链表,最终确定光标位置。但是,当你面对一个1k行的代码基,里面按照原来的机器写死了各种可恶的绝对坐标的时候,你想对 它进行改进,就不是说说那么简单的事情了。
- 如何支持字体的滚动(当文本输入到边缘时——拿单行编辑框为例,正常情况下文本应该向左滚动,光标始终在最右端的边缘闪烁)。老的平台控件是支持这个功能的,这让我很开心,但是开心了没多久,我发现了一个很严重的bug,就是在整个文本字符串在向左滚动的时候,文本字符会画到整个文本控件的左边……这显然是一个不可饶恕的bug,但是到现在我仍然没有搞定。因此现在我的ITextField,当你的文字字符串输到文本框右边的时候,光标仍然继续向右移动——于是就会消失不见了。
- 如何支持不同的字体大小(我发现在原来的代码基础上改造而来的ITextField对光标的定位简直是一塌糊涂。经过我一番实验,发现当用29号字体的时候光标的定位效果是最好的,于是我统一把我的文本控件的字体改成了默认的29号大小。陈老师告诉我们说“软件软件,越软越好……”,我当然明白这个道理,但是,写“软”是需要时间的。某些时候,为了赶进度,我们不得不牺牲某些扩展性来追求暂时的能用性。所以我的东西到现在,光标定位依然是很大的问题)
- 如何支持不同字体载入?据我所知,字体有点阵字体和矢量字体,矢量字体又有OpenType和TrueType等,这些字体的内在原理是什么?什么叫衬线字体?什么叫非衬线字体?为什么一般文章排版正文都要用宋体?等等,这是非常有研究的一个话题。
- 如何支持文字选块?
- 如何支持多行文本域的文字折行?
- 进一步,如何支持标点压缩和头尾压缩(这是一般字处理软件的事情了……)。
- 再进一步,我们只知道英语和汉语,陈老师说阿拉伯文的文字连着写和分着写也是不一样的,有特殊的规则,我们又该如何支持?
由此可以看出,文本编辑框虽然看似简单,实现起来却要涉及到很多很多的知识和细心斟酌。
当然,以我的水平是不可能在这么短的时间内写出一个完备的文本编辑框的。可取的方法就是模仿、修改。老的单行文本编辑框叫做GTextField,GTextField的邻居如下所示:
我呢,则丝毫没有客气,仿照主要的函数接口,框架代码,对GTextField做起了外科手术。一个好的医生应该是内外兼修。做这么一个东西自然需要对底层的东西有比较深入的了解。可是一来这一套东西是我们实验室自己yy出来的,很多尚达不到工业标准,也没有现成的教程指南,代码注释又不是特别完备,所以自己理解起来颇有些困难;二来很多东西急于求成,所以有非常多莫名其妙的1、2、3、4、5,只有通过自己的实验看效果,将这些12345变成const int default_xx_margin = 3……
经过我的改造,除了光标定位和字体大小,其余功能基本实现,只是代码写的比较恶心,自己都不忍去看了。
无怪乎,Knuth大人一个TeX系统写了十年时间,经过别人改造成LaTeX、ConTeXt、Omega、LuaTeX等等至今尚未完善;无怪乎求伯君大神当年十万行汇编代码的WPS1.0使他成为了全中国程序员的偶像。
当我们费了九牛二虎之力做出来一个可以用的东西时,却发现那个东东是如此的丑陋,以至于连自己都不敢去看它。有了这样的经历,再去使用Windows 7,Compiz Fusion,会多一份敬重之情。简洁优美的背后,隐藏着多少心思和功力。
这是我现在写出来的最终效果:
样式还不错,点击右边小按钮的时候还能清空文本。当然,更灵活的设计时发送个消息,让用户自己处理决定该做什么。
这是头文件,单行文本编辑框的类定义:
/************************************************** ITextField: 单行文本编辑框 功能描述:至此png图片载入。支持单行文本编辑。支持锁定操作。但是光标定位和字体大小还存在问题。 作者:Xiao Hanyu <xiaohanyu1988@gmail.com> 参考:GTextEdit **************************************************/ #pragma once #include "SimpleCtrl.h" #include "TwoWayLinkList.h" #include "DataTypeDef.h" //------------- #include "string" #include "ThTimer.h" #include "IStyle.h" #include "TextArea.h" #include "MCaret.h" #include "WndContainer.h" #include "GDIFactory.h" #include "DrawDevice.h" #include "GDIPen.h" #include "GDIBrush.h" #include "CombinedCtrl.h" #include "AllCtrlManager.h" #include "BaseWnd.h" #define ITF_TXT_CHANGED (MD_USER_BEGIN + 1) // 只要文字输入改变,就发送该消息 #define ITF_LEFT_PNG_CLICKED (MD_USER_BEGIN+2) // 只要设置mainPng和leftPng,如果点击leftPng,就发送该消息 #define ITF_RIGHT_PNG_CLICKED (MD_USER_BEGIN+3) // 只要设置mainPng和rightPng,如果点击rightPng,就发送该消息 const int ITEXTFIELD_MAX_LEN = 300; //设置边距,即绘制光标和文字时,距离边缘的最小值 const int ITF_EDGE_BORDER = 2; class MCaret; class ITextField : public SimpleCtrl { public: ITextField(GisHWND pBWnd, ControlID id); virtual ~ITextField(); Boolean ShowCaret(); Boolean HideCaret(); public: //父类继承需要重写函数 virtual GRESULT Init(int cx, int cy, int nWidth, int nHeight); virtual GRESULT UnInit(); virtual void Redraw(); virtual Boolean setDisabled(Boolean b); //Mouse Msg Action virtual GRESULT OnMouseDown(MouseButtons mbtn,int x,int y); virtual GRESULT OnMouseUp(MouseButtons mbtn,int x,int y); virtual GRESULT OnMouseDBClick(MouseButtons mbtn,int x,int y); virtual void SetFocus(Boolean bFocus);//设置本文本框为鼠标焦点 virtual void SetVisable(Boolean bVisable); //重载 当不可见时,失去焦点 //Msg proc virtual GRESULT ProcCharMsg(WPARAM wParam, LPARAM lParam); //控件文本内容操作 //获得当前文本 wchar_t* GetText(); //设置文本内容 void SetText(const wchar_t *content); //清空文本内容 Boolean ResetText(); static void CALLBACK DoTimer(UINT uTimerID,DWORD dwUser,DWORD dw); public: bool getCharVisable(); //输出文本框字符是否可见 bool setCharVisable(bool p);//设置文本框字符是否可见 void SetAutoOpenSip(bool p);//设置是否自动打开键盘 protected: //根据传入的x,y判断在字符串中的具体位置 //x,y,在控件中的相对坐标 int GetPosInString(int x,int y); //通过字符序号活得光标偏移位置长度 int GetCaretFromCharIndex(int nCharIndex); //获取当前整个字符串的长度 int GetStringLength(); //pos:在m_szText中的位置 Boolean InsertCharInString(wchar_t cdata,int pos); Boolean DeleteCharInString(int pos, wchar_t* pCharOut); ThTimer m_Timer; //光标显示定时器 public: IStyle GetStyle(); void SetStyle(wchar_t* fontType, COLORREF fontColor, int iMode, int fontSize, UINT textAlignment, COLORREF bgColor, int alpha, bool round); Boolean ResetStyle(); virtual GRESULT SetPngImage(wchar_t* main_png, wchar_t* left_png = NULL, wchar_t * right_png=NULL); GRESULT SetEditable(bool editable); private: bool isCharVisable; //决定文本框字符是否可见的变量,值为true,则输入的字符都可见, //值为false,则输入的字符均显示为*号,一般用来输入密码使用 MCaret* m_pCaret; //保存文本框字符内容的数组,容量有限,依据ITEXTFIELD_MAX_LEN的值来决定容量 wchar_t m_szText[ITEXTFIELD_MAX_LEN]; //根据输入的字符串长度来填充适量的*号,这个变量就是一串*号的首地址 wchar_t s_szText[ITEXTFIELD_MAX_LEN]; //字符串显示开始位置 GRect m_rcText; //字符串链表,因为不仅要保存字符本身,还要保存字符的显示宽度等信息,因此需要用到链表存储 TwoWayLinkList m_CharList; //当前光标对应的字符串中的字符位置 int m_nCurCharPos; //当前字符串的字符总数 int m_nCharNum; bool m_bAutoOpenSip; bool i_tfEditable; // 是否可编辑 protected: IStyle i_tfStyle; // 控制textfield的文字风格 enum { i_mainPng = 0, i_leftPng, i_rightPng, StateCount }; GImage i_tfArrPng[StateCount]; // textfield目前支持三张png图片 Boolean i_tfSetMainImg; // 是否设置mainPng? Boolean i_tfSetLeftImg; // 是否设置leftPng? Boolean i_tfSetRightImg; // 是否设置rightPng? //HFONT i_tfFont; // 保存控件的字体信息,用于光标定位和字体大小 };
多行文本域的设计还要复杂一点。所以这个控件至今有很多的bug,有非常多的改进之处。
除了控件层开发,到现在为止我还写了行程业务的8个窗口页面。在写的过程中思考了很久“如何把软件写软的问题”。但是由于没有系统了解过设计模式,对c++强大的继承用的又不熟,因此现在无法做出自我满意的总结。
还打算总结下这一个月来用VS2008及相关软件的一些小技巧。毕竟磨刀不误砍柴工。
水平有限、敬请批评指正。