阿拉伯数字书写的数字转换成汉字大写的数字,高手们都来看看

gouhuafeng 2008-11-22 08:22:42
阿拉伯数字书写的数字转换成汉字大写的数字, 如下


1 一
10 十
11 十一
20 二十
21 二十一
100 一百
101 一百零一
要求:
1. 将可以将所有 阿拉伯数字书写的数字转换成汉字大写的数字
2. 在转换的同时要兼顾性能.

路过高人不妨谈谈你的算法或思路,有代码更好.....
...全文
186 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
unit CnDigits;{-------------------------------------------------------------------- 货币数字转为为中文大写的组件 Version 1.31 by SNAKEJIAO 2003.4.13简介: 可以转化整数,浮点数,及时间日期型数据 配合别名列表可以进行语音合成,主要用于 东进中继卡中的PlayPrompt提示语音的合成 用于电话系统中播放金额,日期及时间 还有输入的数字序列,同时更多的应用于显示 销售系统中的打印发票时的中文大写金额字段email: jiaoguoqing26@21cn.com来自:爱数星星的猫, 时间:2003-9-17 没有习惯,有规定。给你看看人民银行的规定吧(说不定你还得改改^_^)。正确填写票据和结算凭证的基本规定 银行、单位和个人填写的各种票据和结算凭证是办理支付结算和现金收付的重要依据,直接关系到支付结算的准确、及时和安全。票据和结算凭证是银行、单位和个人凭以记载账务的会计凭证,是记载经济业务和明确经济责任的一种书面证明。因此,填写票据和结算凭证,必须做到标准化、规范化,要要素齐全、数字正确、字迹清晰、不错漏、不潦草,防止涂改。 一、中文大写金额数字应用正楷或行书填写,如壹(壹)、贰(贰)、叁、肆(肆)、伍(伍)、陆(陆)、柒、捌、玖、拾、佰、仟、万(万)、亿、元、角、分、零、整(正)等字样。不得用一、二(两)、三、四、五、六、七、八、九、十、念、毛、另(或0)填写,不得自造简化字。如果金额数字书写中使用繁体字,如貳、陸、億、萬、圓的,也应受理。 二、中文大写金额数字到“元”为止的,在“元”之后,应写“整”(或“正”)字,在“角”之后可以不写“整”(或“正”)字。大写金额数字有“分”的,“分”后面不写“整”(或“正”)字。 三、中文大写金额数字前应标明“人民币”字样,大写金额数字应紧接“人民币”字样填写,不得留有空白。大写金额数字前未印“人民币”字样的,应加填“人民币”三字。在票据和结算凭证大写金额栏内不得预印固定的“仟、佰、拾、万、仟、伯、拾、元、角、分”字样。 四、阿拉伯小写金额数字中有“0”时,中文大写应按照汉语语言规律、金额数字构成和防止涂改的要求进行书写。举例如下: (一)阿拉伯数字中间有“O”时,中文大写金额要写“零”字。如 ¥1,409.50,应写成人民币壹仟肆佰零玖元伍角。 (二)阿拉伯数字中间连续有几个“0”时,中文大写金额中间可以只写 一个“零”字。如¥6,007.14,应写成人民币陆仟零柒元壹角肆分。 (三)阿拉伯金额数字万位或元位是“0”,或者数字中间连续有几个“0”, 万位、元位也是“0’,但千位、角位不是“0”时,中文大写金额中 可以只写一个零字,也可以不写“零”字。如¥1,680.32,应写成 人民币壹仟陆佰捌拾元零叁角贰分,或者写成人民币壹仟陆佰捌拾元 叁角贰分;又如¥107,000.53,应写成人民币壹拾万柒仟元零伍角 叁分,或者写成人民币壹拾万零柒仟元伍角叁分。 (四)阿拉伯金额数字角位是“0”,而分位不是“0”时,中文大写金额 “元”后面应写“零”字。如¥16,409.02,应写成人民币壹万陆 仟肆佰零玖元零贰分;又如¥325.04,应写成人民币叁佰贰拾伍元 零肆分。 五、阿拉伯小写金额数字前面,均应填写入民币符号“¥”(或草写:)。阿拉伯小写金额数字要认真填写,不得连写分辨不清。 六、票据的出票日期必须使用中文大写。为防止变造票据的出禀日期,在填写月、日时,月为壹、贰和壹拾的,日为壹至玖和壹拾、贰拾和叁抬的,应在其前加“零”;日为抬壹至拾玖的,应在其前加“壹”。如1月15日,应写成零壹月壹拾伍日。再如10月20日,应写成零壹拾月零贰拾日。 七、票据出票日期使用小写填写的,银行不予受理。大写日期未按要求规范填写的,银行可予受理,但由此造成损失的,由出票入自行承担。--------------------------------------------------------------------}{ ‘NOTICE: uncomment following line to discard unused code !‘}{ ‘注意: 去掉下面这一行,可以抛弃收集的其它代码‘}// { discard_other_code}interfaceuses Windows, Messages, SysUtils, Classes, Math;type //类型定义 TDate = TDateTime; TTime = TDateTime; { 中文数字大写组件 by lichaohui } TCnDigits = class(TComponent) private Fda: Boolean; procedure SetDA(const Value: Boolean); procedure SetCnUP(const Value: String); protected FCurrVa: Currency; FCnUpDigits: String; FAlias: TStrings; procedure SetCurrVa(const Value: Currency); procedure SetAlias(const Value: TStrings); public { discard_other_code} //下面是肖杨的商场管理源码中的代码 //返加人民币的中文数值 class function other_RMB(NN:real):string; //其它的两个方法 class function other_SmallTOBig(small: real): string; class function other_XD(xx: currency): string; {} //下面是lichaohui的工具函数 class function LzhFmtInt(IntValue: Int64; IsYear: Boolean = False; SoftTone: Boolean = False): String; class function LzhFmtDigits(Digits: String): String; class function LzhFmtFloat(FloatValue: Extended; RoundPos: Integer = 8): String; class function LzhFmtCurrency(AValue: Currency): String; overload; class function LzhFmtCurrency(AValue: Int64): String; overload; class function LzhFmtDate(AValue: TDate; DigitsYear: Boolean = False): String; class function StdFmtDate(AValue: TDate; DigitsYear: Boolean = False): String; class function LzhFmtTime(AValue: TTime; HasSecond: Boolean = False): String; class function LzhFmtDateTime(AValue: TDateTime; DigitsYear: Boolean = False): String; class function StdFmtDateTime(AValue: TDateTime; DigitsYear: Boolean = False): String; //常用方法调用接口 function ChineseUpper(Curr: Currency): String; function ChineseDate(ADate: TDate; DigitsYear: Boolean = False): String; function ChineseTime(ATime: TTime; HasSecond: Boolean = False): String; //等价别名转化 function StrToAlias(S: String): String; function StrToAlias2(S: String): String; //构造和析构函数 constructor Create(AOwner: TComponent); override; destructor Destroy; override; published property CurrencyValue: Currency read FCurrVa write SetCurrVa; property CnUpperDigits: String read FCnUpDigits write SetCnUP stored False; property Alias: TStrings read FAlias write SetAlias; property DefaultAlias: Boolean read Fda write SetDA; end;const DP: WideString = ‘零壹贰叁肆伍陆柒捌玖两负‘; DN: WideString = ‘点拾佰仟万拾佰仟亿拾佰仟万拾佰仟兆拾佰仟‘; CrNa: WideString = ‘元角分整‘; DtNa: WideString = ‘年月日时分秒‘;procedure Register;implementationprocedure Register;begin RegisterComponents(‘System‘, [TCnDigits]);end;{ TCnDigits }{ discard_other_code}class function TCnDigits.other_RMB(NN: real): string;var HZ, NS, NW, NA, N1, N2:string; LA, X, Nk:integer;begin //此行代码是为了修正小数位多于两位时生成的结果的错误 NN := Int(NN * 100) / 100; //下面的代码被lichaohui格式化整理 if NN > 9999999999999.99 then begin //MessageDlg(‘金额溢出。‘,mtError,[mbOk], 0); HZ := ‘‘; Result := HZ; exit; end; if NN = 0 then begin HZ := ‘零元‘; result := HZ; exit; end; NS := ‘零壹贰叁肆伍陆柒捌玖‘; NW := ‘分角元拾佰仟万拾佰仟亿拾佰仟万‘; NA := FloatToStr(NN * 100); LA := length(NA); X := 1; HZ := ‘‘; while X<=LA do begin NK := Ord(NA[x]) - Ord(‘0‘); N1 := Copy(NS, NK * 2 + 1, 2); N2 := Copy(NW, LA * 2 + 1 - X * 2, 2); if (NK = 0) AND ((N2 = ‘亿‘) OR( N2 = ‘万‘) OR( N2 = ‘元‘))then begin if copy(HZ,Length(HZ)-1,2) = ‘零‘ then HZ := copy(HZ, 1, length(HZ) - 2); if copy(HZ, Length(HZ) - 1, 2) = ‘亿‘ then if N2 = ‘元‘ then begin N1 := N2; N2 := ‘零‘; end else N2 := ‘‘ else begin N1 := N2; N2 := ‘零‘; end end else if NK=0 then begin if copy(HZ, length(HZ) - 1, 2)=‘零‘ then N1 := ‘‘; if N2=‘分‘ then begin if copy(HZ, length(HZ) - 1, 2)=‘零‘ then HZ := copy(HZ, 1, length(HZ) - 2) + ‘整‘ else HZ := HZ + ‘整‘; N1 := ‘‘; end; N2 := ‘‘; end; HZ := HZ + N1 + N2; X := X + 1 end; {end of while statement} Result := HZ;end;class function TCnDigits.other_SmallTOBig(small: real): string;var SmallMonth,BigMonth:string; wei1,qianwei1:string[2]; qianwei,dianweizhi,qian:integer;begin {------- 修改参数令值更精确 -------} {小数点后的位数,需要的话也可以改动该值} qianwei:=-2; {转换成货币形式,需要的话小数点后加多几个零} Smallmonth:=formatfloat(‘0.00‘,small); {---------------------------------} dianweizhi :=pos(‘.‘,Smallmonth);{小数点的位置} {循环小写货币的每一位,从小写的右边位置到左边} for qian:=length(Smallmonth) downto 1 do begin {如果读到的不是小数点就继续} if qiandianweizhi then begin {位置上的数转换成大写} case strtoint(copy(Smallmonth,qian,1)) of 1:wei1:=‘壹‘; 2:wei1:=‘贰‘; 3:wei1:=‘叁‘; 4:wei1:=‘肆‘; 5:wei1:=‘伍‘; 6:wei1:=‘陆‘; 7:wei1:=‘柒‘; 8:wei1:=‘捌‘; 9:wei1:=‘玖‘; 0:wei1:=‘零‘; end; {判断大写位置,可以继续增大到real类型的最大值,可是谁有那么多钱} case qianwei of -3:qianwei1:=‘厘‘; -2:qianwei1:=‘分‘; -1:qianwei1:=‘角‘; 0 :qianwei1:=‘元‘; 1 :qianwei1:=‘拾‘; 2 :qianwei1:=‘佰‘; 3 :qianwei1:=‘千‘; 4 :qianwei1:=‘万‘; 5 :qianwei1:=‘拾‘; 6 :qianwei1:=‘佰‘; 7 :qianwei1:=‘千‘; 8 :qianwei1:=‘亿‘; 9 :qianwei1:=‘拾‘; 10:qianwei1:=‘佰‘; 11:qianwei1:=‘千‘; end; inc(qianwei); BigMonth :=wei1+qianwei1+BigMonth;{组合成大写金额} end; end; Result := BigMonth;end;class function TCnDigits.other_XD(xx: currency): string;var dx,ws:string; i,cd:integer; int:currency;begin int:=trunc(abs(xx)+0.005*100); cd:=length(currtostr(int)); dx:=‘零壹贰叁肆伍陆柒捌玖‘; ws:=‘分角元拾佰仟万拾佰仟亿拾佰仟‘; result:=‘‘; i:=1; while i 0) and (FracPart >= 10) then rs := rs + DP[1]; if (IntPart = 0) and (FracPart 0) then rs := ‘‘; if FracPart >= 10 then begin if FracPart mod 10 = 0 then rs := rs + DP[Jiao + 1] + CrNa[2] + CrNa[4] else rs := rs + DP[Jiao + 1] + CrNa[2] + DP[Fen + 1] + CrNa[3]; end else if FracPart > 0 then begin if IntPart = 0 then rs := rs + DP[Fen + 1] + CrNa[3] else rs := rs + DP[1] + DP[Fen + 1] + CrNa[3]; end else begin rs := rs + CrNa[4]; end; if AValue 0) and (FracPart >= 10) then rs := rs + DP[1]; if (IntPart = 0) and (FracPart 0) then rs := ‘‘; if FracPart >= 10 then begin if FracPart mod 10 = 0 then rs := rs + DP[Jiao + 1] + CrNa[2] + CrNa[4] else rs := rs + DP[Jiao + 1] + CrNa[2] + DP[Fen + 1] + CrNa[3]; end else if FracPart > 0 then begin if IntPart = 0 then rs := rs + DP[Fen + 1] + CrNa[3] else rs := rs + DP[1] + DP[Fen + 1] + CrNa[3]; end else begin rs := rs + CrNa[4]; end; if AValue = ‘0‘) and (Digits[i] 1e15) or (FloatValue 16 then rps := 16; beit := IntPower(10, rps); FraPart := Round(Frac(FloatValue) * beit); rs := LzhFmtInt(IntPart); if FraPart > 0 then begin Digits := IntToStr(FraPart); len := Length(Digits); while (len > 0) and (Digits[len] = ‘0‘) do len := len - 1; Digits := Copy(Digits, 1, len); rs := rs + DN[1] + LzhFmtDigits(Digits); end; if FloatValue < 0 then rs := DP[12] + rs; Result := rs;end;class function TCnDigits.LzhFmtInt(IntValue: Int64; IsYear: Boolean; SoftTone: Boolean): String;var i, n, t: Integer; bi: WideString; LastBt: WideChar; rs: WideString; reach: Boolean; LastHit: Integer;begin //限制为18位数 if IntValue 8000000000000000000 then begin Result := ‘‘; Exit; end; bi := IntToStr(IntValue); //格式化为 bi := StringOfChar(‘0‘, 20 - Length(bi)) + bi; //从左侧开始格式化 LastBt := ‘ ‘; LastHit := 0; reach := False; for i := 1 to 20 do begin //如果非0 if (bi[i] >= ‘1‘) and (bi[i] <= ‘9‘) then begin //如果前一位是0,又不到分界位,补零 if reach and (LastBt = ‘0‘) and (i mod 4 1) then begin rs := rs + DP[1]; end; reach := True; //转换数字,如果是十位,且前一位为0 if SoftTone and (i mod 4 = 3) and (LastBt = ‘0‘) and ((not reach) or (bi[i] = ‘1‘)) then else begin rs := rs + DP[(Ord(bi[i]) - Ord(‘0‘)) mod 10 + 1]; end; //添加位名 n := (20 - i) mod 20 + 1; if n > 1 then begin rs := rs + DN[n]; end; LastHit := 20 - i; end; //分界名 if (bi[i] = ‘0‘) and (i mod 4 = 0) and reach then begin //特殊分界处的显示原则,8位前为空的不显示分界名 //不是8位处的,4位前为空不显示分界名 n := (20 - i) mod 20 + 1; t := n - 1; if (n > 1) and ( ((t mod 8 = 0) and (LastHit - t < 8)) or (((t - 12) mod 8 = 0) and (LastHit - t = 0 then if Result = ‘‘ then Result := FAlias.Values[Ws[i]] else Result := Result + ‘,‘ + FAlias.Values[Ws[i]] else if Result = ‘‘ then Result := Ws[i] else Result := Result + Ws[i]; end;end;function TCnDigits.StrToAlias2(S: String): String;var Ws: WideString; i: Integer; t: Integer;begin Ws := S; for i := 1 to Length(Ws) do begin t := TStringList(FAlias).IndexOfName(Ws[i]); if t >= 0 then Result := Result + FAlias.Values[Ws[i]] else Result := Result + Ws[i]; end;end;constructor TCnDigits.Create(AOwner: TComponent);begin inherited; FAlias := TStringList.Create; TStringList(FAlias).Sorted := True; TStringList(FAlias).Duplicates := dupIgnore;end;destructor TCnDigits.Destroy;begin FAlias.Free; inherited;end;procedure TCnDigits.SetDA(const Value: Boolean);var Ws: WideString; i: Integer; sv: String;begin if Value then begin Ws := DP + DN + CrNa + DtNa; for i := 1 to Length(Ws) do begin sv := Ws[i]; FAlias.Add(sv + ‘=‘ + FAlias.Values[Ws[i]]); end; end else begin for i := FAlias.Count - 1 downto 0 do begin sv := FAlias[i]; if Trim(sv) = ‘‘ then FAlias.Delete(i) else if (Length(sv) > 0) and (sv[Length(sv)] = ‘=‘) then FAlias.Delete(i); end; end; Fda := Value;end;procedure TCnDigits.SetCnUP(const Value: String);begin //FCnUpDigits := Value;end;class function TCnDigits.StdFmtDate(AValue: TDate; DigitsYear: Boolean): String;var yy, mm, dd: Word; ys: WideString; rs: WideString;begin DecodeDate(AValue, yy, mm, dd); if DigitsYear then ys := LzhFmtDigits(IntToStr(yy)) + DtNa[1] else ys := LzhFmtInt(yy, True) + DtNa[1]; rs := ys; if (mm <= 2) or (mm = 10) then rs := rs + DP[1]; rs := rs + LzhFmtInt(mm) + DtNa[2]; if (dd < 10) or (dd mod 10 = 0) then rs := rs + DP[1]; rs := rs + LzhFmtInt(dd) + DtNa[3]; Result := rs;end;class function TCnDigits.StdFmtDateTime(AValue: TDateTime; DigitsYear: Boolean): String;var dt: TDate; tm: TTime;begin dt := AValue; tm := AValue; Result := StdFmtDate(dt, DigitsYear) + LzhFmtTime(tm);end;end.附例子Demo
第一部分 程序员必读 第一章 对程序错误的处理 在我们开始介绍Microsoft Windows应该提供的许多特性之前,我们首先必须了解Windows的各个函数是如何进行错误处理的。 当你调用一个Windows函数时,它首先要检验你传递给它的的各个参数的有效性,然后再设法执行它的任务。如果你传递了一个无效参数,或者由于某种原因它无法执行这项操作,那么该函数就会返回一个值,指明该函数在某种程度上运行失败了。表1-1列出了大多数Windows函数使用的数据类型的返回值。 表1-1 Windows函数常用的返回值类型 数据类型 表示失败的值 VOID 该函数的运行不可能失败。Windows函数的返回值类型很少 是VOID。 BOLL 如果函数运行失败,那么返回值是0,否则返回的是非0值。最 好对返回值进行测试,以确定它是0还是非0。如果它是TRUE ,则不要测试返回值。 HANDLE 如果函数运行失败,则返回值通常是NULL,否则返回值为 HANDLE,,用于标识你可以操作的一个对象。对于这个返回 值,你应该小心处理,因为有些函数会返回一个句柄 值INVALID_HANDLE_VALUE,它被定义为-1。该函数的 Platform SDK资料将会清楚地说明该函数是返回NULL还 是INVALID_HANDLE_VALID,以便指明函数运行已经失败。 PVOID 如果函数运行失败,则返回值是NULL,否则返回PVOID,以 标识数据块的内存地址。 LONG/DWORD 这是个难以处理的值。返回数量的函数通常返回LONG 或DWORD。如果由于某种原因,函数无法对你想要进行计数 的对象进行计数,那么该函数通常返回0或-1(根据该函数而定) 。如果你调用的函数返回了LONG/DWORD,那么请认真阅 读Platform SDK资料,以确保你能正确检查潜在的错误。 当一个Windows函数返回一个错误代码时,它常常可以用来了解函数为什么会运行失败。Microsoft公司编译了一个所有可能的错误代码的列表,并且为每个错误代码分配了一个32位的号码。 从系统内部来讲,当一个Windows函数检测到一个错误时,它会使用一个称为线程本地存储器的机制,将相应的错误代码号码与调用的线程关联起来。(“线程本地存储器”将在第21章中介绍)。这将使线程能够互相独立地运行,而不会影响各自的错误代码。当函数返回给你时,它的返回值就能指明一个错误已经发生。若要确定这是个什么错误,请调用GetLastError函数: 见原书P4的程序(1) 该函数只返回线程的32位错误代码。 当你拥有32位错误代码的号码时,你必须将该号码转换成更有用的某种对象。WinError.h头文件包含了Microsoft公司定义的错误代码的列表。下面我显示了该列表的某些内容,使你能够看到它的大概样子: 见原书P4的程序(2)和P5的程序 你可以看到,每个错误都有3种表示法:即一个消息ID(这是你可以在源代码中使用的一个宏,以便与GetLastError的返回值进行比较),消息文本(对错误的英文描述)和一个号码(你应该避免使用这个号码,而应该使用消息ID)。请记住,我只选择了WinError.h头文件中的很少一部分内容来向你进行展示,整个文件的长度超过21000行。 当Windows函数运行失败时,你应该立即调用GetLastError函数,否则,如果你调用另一个Windows函数,它的值很可能被改写。 说明 GetLastError能返回线程产生的最后一个错误。如果该线程调用的Windows 函数运行成功,那么最后一个错误代码就不被改写,并且不指明运行成功。有少 数Windows函数并不遵循这一规则,并且它会更改最后的错误代码,但是Platform SDK资料通常指明,当函数运行成功时,该函数会更改最后的错误代码。 Windows 98 许多Windows 98的函数实际上是用Microsoft公司的16位Windows 3.1产 品产生的16位代码来实现的。这种比较老的代码并不通过GetLastError之类函 数来报告错误,而且Microsoft公司并没有在Windows 98中修改16位代码,以 支持这种错误处理方式。对于我们来说,这意味着Windows 98中的许多Win32 函数在运行失败时不能设置最后的错误代码。该函数将返回一个值,指明运行失 败,这样你就能够发现该函数确实已经运行失败。但是你无法确定运行失败的原 因。 有些Windows函数之所以能够成功运行,那是若干个原因产生的结果。例如,创建指明的事件内核对象之所以能够取得成功,原因是你实际上创建了该对象,或者是因为已经存在带有相同名字的事件内核对象。你的应用程序必须知道成功的原因。为了将该信息返回给你,Microsoft公司选择使用最后错误代码机制。这样,当某些函数运行成功时,你就能够通过调用GetLadtError函数来确定其他的一些信息。对于具有这种行为特性的函数来说,Platform SDK资料清楚地说明了GetLastError函数可以这样来使用。请参见该资料,以便找出CreateEvent函数的例子。 当你进行调试的时候,我发现监控线程的最后错误代码是非常有用的。在Microsoft Visual studio 6.0中,Microsoft的调试程序支持一个非常有用的特性,即你可以配置Watch窗口,以便始终都能向你显示线程的最后错误代码的号码和该错误的英文描述。通过选定Watch窗口中的一行,并键入“@err,hr",你就能够做到这一点。观察图1-1,你会看到我已经调用了CreateFile函数。该函数返回INVALID_HANDLE_VALUE(-1)的HANDLE,表示它未能打开指定的文件。但是Watch窗口向我们显示最后错误代码(即如果我调用GetLastErro函数,该函数返回的错误代码)是0x00000002。该Watch窗口又进一步指明错误代码2是指“系统不能找到指定的文件。”你会发现它与WinError.h头文件中的错误代码2所指的字符串是相同的。 图1-1 在Visual Studio 6.0的Watch窗口中键入 “@err,hr",你就可以查看当前线程的最后错误代码。 Visual studio还配有一个小的实用程序,称为Error Lookup。你可以使用Error Lookup将错误代码的号码转换成它的文本描述。 见P7的Error Lookup插图 如果我在我编写的应用程序中发现一个错误,我可能想要向用户显示该错误的文本描述。Windows提供了一个函数,可以将错误代码转换成它的文本描述。该函数称为FormatMessage。请看下面的代码: 见原书P8的程序(1) FormatMessage函数的功能实际上是非常丰富的,在创建向用户显示的字符串信息时,它是人们首选的函数。该函数之所以有这样大的作用,原因之一是它很容易用多种语言来进行操作。该函数能够检测出用户首选的语言(在Regional Settings Control Panel小应用程序中设定),并返回相应的文本。当然,你首先必须自己转换字符串,然后将已转换的消息表资源嵌入你的.exe文件或DLL模块,不过,这时该函数会选定正确的嵌入对象。ErrorShow示例应用程序(本章后面将加以介绍)展示了如何调用该函数,以便将Microsoft公司定义的错误代码转换成它的文本描述。 有些人常常问我,Microsoft公司是否建立了一个主控列表,以显示每个Windows函数可能返回的所有错误代码。可惜,答案是没有这样的列表,而且Microsoft公司将永远不会建立这样的一个列表。因为在创建系统的新版本时,建立和维护该列表实在太困难了。 建立这样一个列表时存在的问题是,你可以调用一个Windows函数,但是该函数能够在内部调用另一个函数,而这另一个函数又可以调用另一个函数,如此类推。由于各种不同的原因,这些函数中的任何一个函数都可能运行失败。有时,当一个函数运行失败时,较高级的函数对它进行恢复,并且仍然可以执行你想执行的操作。为了创建该主控列表,Microsoft公司必须跟踪每个函数的运行路径,并建立所有可能的错误代码的列表。这项工作很困难。当创建系统的新版本时,这些函数的运行路径就会改变。 1.1 你也能够定义自己的错误代码 好了,我已经说明Windows函数是如何向函数的调用者指明发生的错误。Microsoft公司也使你能够将该机制用于你自己的函数。比如说,你编写了一个你希望其他人调用的函数。你的函数可能因为这样或那样的原因而运行失败,你必须向函数的调用者说明它已经运行失败。 若要指明函数运行失败,你只需要设定线程的最后的错误代码,然后让你的函数返回FALSE,INVALID_HANDLE_VALUE,NULL,或者返回任何合适的信息。若要设定线程的最后错误代码,你只需要调用下面的代码: 见原书P8的程序(2) 请将你认为合适的任何32位号码传递给该函数。我设法使用WinError.h中已经存在的代码,只要该代码能够正确地指明我想要报告的错误即可。如果你认为WinError.h中的任何代码都不能正确地反映该错误的性质,那么你可以创建你自己的代码。错误代码是个32位的数字,它可以划分成下表所示的各个域。 位 31-30 29 28 27-16 15-0 内容 严重性 Microsoft/ 保留 设备代码 异常代码 客户 含义 0=成功 0=Microsoft 必须是0 由Microsoft 由Microsoft/ 1=供参考 公司定义的 公司定义 客户定义 2=警告 代码 3=错误 1=客户定义 的代码 这些域将在第24章中详细讲述。现在,你需要知道的重要域是第29位的信息。Microsoft公司规定,他们建立的所有错误代码的这个信息位均使用0。如果你创建自己的错误代码,你必须在这个信息位中输入1。这样,就可以确保你的错误代码与Microsoft公司目前或者将来定义的错误代码不会发生冲突, 1.2 ErrorShow示例应用程序 ErrorShow应用程序“01 ErrorShow.exe"(在图1-2中列出)展示了如何获取错误代码的文本描述的方法。该应用程序的源代码和资源文件位于本书所附光盘上的01-ErrorShow目录下。一般来说,该应用程序用于显示调试程序的Watch窗口和Error Lookup程序是如何运行的。当你启动该程序时,就会出现下面这个窗口。 见原书P9的插图 你可以将任何错误代码键入该编辑控件。当你单击Look Up按钮时,在底部的滚动窗口中就会显示该错误的文本描述。该应用程序唯一令人感兴趣的特性是如何调用FormatMessage函数。下面是我使用该函数的方法: 见原书P10的程序(1) 第一个代码行用于从编辑控件中检索错误代码的号码。然后,建立一个内存块的句柄并将它初始化为NULL。FormatMessage函数在内部对内存块进行分配,并将它的句柄返回给我们。 当调用FormatMessage函数时,我传递了FORMAT_MESSAGE_FROM_SYSTEM标志。该标志告诉FormatMessage函数,我们想要系统定义的错误代码的字符串。我还传递了FORMAT_MESSAGE_ALLOCATE_BUFFER标志,告诉该函数为错误代码的文本描述分配足够大的内存块。该内存块的句柄将在hlocal变量中返回。第三个参数指明我们想要查找的错误代码的号码,第四个参数指明我们想要文本描述使用什么语言。 如果FormatMessage函数运行成功,那么错误代码的文本描述就位于内存块中,我将它拷贝到对话框底部的滚动窗口中。如果FormatMesage函数运行失败,我设法查看NetMsg.dll模块中的消息代码,以了解该错误是否与网络有关。使用NetMsg.dll模块的句柄,我再次调用FormatMessage函数。你会看到,每个DLL(或.exe)都有它自己的一组错误代码,你可以使用Message Compiler(MC.exe)将这组错误代码添加给该模块,并将一个资源添加给该模块。这就是Visual Studio的Error Lookup工具允许你用Modules对话框进行的操作。 图1-2 ErrorShow示例应用程序 见原书P11—16 第2章 UNICODE 随着Microsoft公司的Windows操作系统在全世界日益广泛的流行,对于我们这些软件开发人员来说,将我们的目标瞄准国际上的各个不同市场,已经成为一个越来越重要的问题。美国的软件版本比国际版本提前6个月推向市场,这曾经是个司空见惯的现象。但是,由于各国对Windows操作系统提供了越来越多的支持,因此就更加容易为国际市场生产各种应用软件,从而缩短了软件的美国版本与国际版本推出的时间间隔。 Windows操作系统始终不逾地提供各种支持,以帮助软件开发人员进行应用程序的本地化工作。应用软件可以从各种不同的函数中获得特定国家的信息,并可观察控制面板的设置,以确定用户的首选项。Windows甚至支持不同的字体,以适应我们的应用的需要。 我之所以将这一章放在本书的开头,是因为我考虑到Unicode是开发任何应用程序时要采用的基本步骤。关于Unicode的问题,我在本书的每一章中几乎都要讲到,而且本书中给出的所有示例应用程序都是“用Unicode实现的”。如果你为Microsoft Windows 2000或Microsoft Windows CE开发应用程序,你应该使用Unicode进行开发。如果你为Microsoft Windows 98开发应用程序,你必须对某些问题作出决定。本章也要讲述Windows 98的有关问题。 2.1 字符集 软件的本地化要解决的真正问题,实际上就是如何来处理不同的字符集。多年来,我们许多人一直将文本串作为一系列单字节字符来进行编码,并在结尾处放上一个零。对于我们来说,这已经成了习惯。当我们调用strlen函数时,它在以0结尾的单字节字符数组中返回字符的数目。 问题是,有些文字和书写规则(比如日文中的汉字就是个典型的例子)的字符集中的符号太多了,因此单字节(它提供的符号最多不能超过256个)是根本不敷使用的。为此我们创建了双字节字符集(DBCS),以支持这些文字和书写规则。 2.1.1 单字节与双字节字符集 在双字节字符集中,字符串中的每个字符可以包含一个字节,也可以包含两个字节。例如,日文中的汉字,如果第一个字符在0x81与0x9F之间,或者在0xE0与0xFC之间,那么你就必须观察下一个字节,才能确定字符串中的这个完整的字符。如果要使用双字节字符集,对于程序员来说简直是个很大的难题,因为有些字符只有一个字节宽,而有些字符则是两个字节宽。 如果只是调用strlen函数,那么你无法真正了解字符串中究竟有多少字符,它只能告诉你到达结尾的0之前有多少个字节。ANSI的C运行期库中没有配备相应的函数,使你能够对双字节字符集进行操作。但是,Microsoft Visual C++的运行期库却包含许多函数,如_mbslen,它可以用来操作多字节(既包括单字节也包括双字节)字符串。 为了帮助你对DBCS字符串进行操作,Windows提供了下面的一组帮助函数。 函数 描述 PTSTR CharNext 返回字符串中的下一个字符的地址 (PCTSTR pszCurrentChar); PTSTR CharPrev 返回字符串中的上一个字符的地址 (PCTSTR pszStart, PCTSTR pszCurrentChar); BOOL IsDBCSLendByte 如果该字节是DBCS字符的第一个字节,则返 (BYTE bTestChar); 回TRUE 2.1.2 Unicode:宽字节字符集 Unicode是Apple和Xerox公司于1988年建立的一个技术标准。1991年,成立了一个集团机构负责Unicode的开发和推广应用。该集团由Apple、Compaq、HP、IBM、Microsoft、Oracle、Silicon Graphics、Sybase、Unisys和Xerox等公司组成。(若要了解该集团的全部成员,请通过网址www.Unicode.org查找。)该集团公司负责维护Unicode标准。Unicode的完整描述可以参阅AddisonWesley出版的《Unicode Standard》一书。(该书可以通过网址www.Unicode.org订购。) Unicode提供了一种简单而又一致的表示字符串的方法。Unicode字符串中的所有字符都是16位的字符(两个字节)。它没有专门的字节来指明下一个字节是属于同一个字符的组成部分,还是一个新字符。这意味着你只需要对指针进行递增或递减,就可以遍历字符串中的各个字符。你不再需要调用CharNext,CharPrev和IsDBCSLeadByte之类的函数。 由于Unicode用一个16位的值来表示每个字符,因此总共可以得到65000个字符,这样,它就能够对世界各国的书面文字中的所有字符进行编码。这远远超过了单字节字符集的256个字符的数目。 目前,已经为阿拉伯文、中文拼音、西里尔字母(俄文)、希腊文、西伯莱文、日文、韩文和拉丁文(英文)字母定义了Unicode代码点1。这些字符集中还包含了大量的标点符号、数学符号、技术符号、箭头、装饰标志、区分标志和其他许多字符。如果你将所有这些字母和符号加在一起,总计约达35000个不同的代码点,这样,总计的65000个代码点中,大约还有一半可供将来扩充时使用。 这65536个字符可以分成不同的区域。下面这个表显示了一部分这样的区域以及分配给这些区域的字符。 16位代码 字符 16位代码 字符 0000-007F ASCII 0300-036F 通用区分标志 0080-00FF 拉丁文1字符 0400-04FF 西里尔字母 0100-017F 欧洲拉丁文 0530-058F 亚美尼亚文 0180-01FF 扩充拉丁文 0590-05FF 西伯莱文 0250-02AF 标准拼音 0600-06FF 阿拉伯文 02B0-02FF 修改型字母 0900-097F 梵文 注1. 代码点是指字符集中的一个符号的位置 目前尚未分配的代码点大约还有29000个,不过它们是保留供将来使用的。另外,大约有6000个代码点是保留供你个人使用的。 2. 2 为何应该使用Unicode 当你开发应用程序时,你当然应该考虑利用Unicode的优点。即使现在你不打算对你的应用程序进行本地化,开发时将Unicode放在心上,肯定可以简化将来的代码转换工作。此外,Unicode还具备下列功能: * 可以很容易地在不同语言之间进行数据交换 * 使你能够分配支持所有语言的单个二进制.exe文件或DLL文件 * 提高你的应用程序的运行效率(本章后面还要详细介绍) 2.3 Windows 2000与Unicode Windows 2000是使用Unicode从头进行开发的,用于创建窗口、显示文本、进行字符串操作等的所有核心函数都需要Unicode字符串。如果你调用任何一个Windows函数并给它传递一个ANSI字符串,那么系统首先要将字符串转换成Unicode,然后将Unicode字符串传递给操作系统。如果你希望函数返回ANSI字符串,系统就会首先将Unicode字符串转换成ANSI字符串,然后将结果返回给你的应用程序。所有这些转换操作都是在你看不见的情况下发生的。当然,进行这些字符串的转换需要占用系统的时间和内存开销。 例如,如果你调用CreateWindowEx函数,并传递类名字和窗口标题文本的非Unicode字符串,那么CreateWindowEx必须分配内存块(在你的进程的默认堆中),将非Unicode字符串转换成Unicode字符串,并将结果存储在分配到的内存块中,然后调用Unicode版本的CreateWindowEx函数。 对于用字符串填入缓存的函数来说,系统必须首先将Unicode字符串转换成非Unicode字符串,然后你的应用程序才能处理该字符串。由于系统必须执行所有这些转换操作,因此你的应用程序需要更多的内存,并且运行的速度比较慢。通过从头开始用Unicode来开发应用程序,你就能够使你的应用程序更加有效地运行。 2. 4 Windows 98与Unicode Windows 98不是一种全新的操作系统。它继承了16位Windows操作系统的特性,它不是用来处理Unicode。如果要增加对Unicode的支持,其工作量非常大,因此在该产品的特性列表中没有包括这个支持项目。由于这个原因,Windows 98象它的前任产品一样,几乎都是使用ANSI字符串来进行所有的内部操作的。 你仍然可以编写用于处理Unicode字符和字符串的Windows应用程序,不过,使用Windows函数要难得多。例如,如果你想要调用CreateWindowEx函数并将ANSI字符串传递给它,这个调用的速度非常快,不需要从你进程的默认堆栈中分配缓存,也不需要进行字符串转换。但是,如果你想要调用CreateWindowEx函数并将Unicode字符串传递给它,你就必须明确分配缓存,并调用函数,以便执行从Unicode到ANSI字符串的转换操作。然后你可以调用CreateWindowEx,传递ANSI字符串。当CreateWindowEx函数返回时,你就能释放临时缓存。这比使用Windows 2000上的Unicode要麻烦得多。在本章的后面部分中,我要介绍如何在Windows 98下进行这些转换。 虽然Unicode函数的大多数代码在Windows 98中不起任何作用,但是有少数Unicode函数确实拥有非常有用的实现代码。这些函数是: 见原书的P21 可惜的是,这些函数中有许多函数在Windows 98中会出现各种各样的错误。有些函数无法使用某些字体,有些函数会破坏内存堆栈,有些函数会使打印机驱动程序崩溃,如此等等。如果你要使用这些函数,你必须对它们进行大量的测试。即使这样,你可能仍然无法解决问题。因此你必须向用户说明这些情况。 2. 5 Windows CE与Unicode Windows CE操作系统是为小型设备开发的,这些设备的内存很小,并且不带磁盘存储器。你可能认为,由于Microsoft公司的主要目标是建立一种尽可能小的操作系统,因此它会使用ANSI作为自己的字符集。但是Microsoft公司并不是鼠目寸光。他们懂得,采用Windows CE的设备要在世界各地销售,他们希望降低软件开发成本,这样就能更加容易地开发应用程序。为此,Windows CE本身就是使用Unicode的一种操作系统。 但是,为了使Windows CE尽量做得小一些,Microsoft公司决定完全不支持ANSI Windows函数。因此,如果你要为Windows CE开发应用程序,你必须懂得Unicode,并且在整个应用程序中使用Unicode。 2. 6 需要注意的问题 下面让我们进一步明确一下“Microsoft公司对Unicode支持的情况”: * Windows 2000既支持Unicode,也支持ANSI,因此你可以为它们当中的任何一种开发应用程序 * Windows 98 只支持ANSI,你只能为ANSI开发应用程序 * Windows CE只支持Unicode,你只能为Unicode开发应用程序 虽然Microsoft公司试图让软件开发人员能够非常容易地开发在这3种平台上运行是软件,但是Unicode与ANSI之间的差异使得事情变得困难起来,并且这种差异通常是我遇到的最大的问题之一。请不要误解,Microsoft公司坚定地支持Unicode,并且我也坚决鼓励你使用它。不过你应该懂得,你可能遇到一些问题,需要一定的时间来解决这些问题。我建议你尽可能使用Unicode。如果你运行Windows 98,那么只有在必要时才要转换到ANSI。 不过,还有另一个小问题你应该了解,那就是COM。 2.7 对COM的简单说明 当Microsoft公司将COM从16位Windows转换成Win32时,公司作出了一个决定,即,需要字符串的所有COM接口方法都只能接受Unicode字符串。这是个了不起的决定,因为COM通常用于使不同的组件能够互相之间进行通信,而Unicode则是传递字符串的最佳手段。 如果你为Windows 2000或Windows CE开发应用程序,并且也使用COM,那么你将会如虎添翼。在你的整个源代码中使用Unicode,将使与操作系统进行通信和与COM对象进行通信的操作变成一件轻而易举的事情。 如果你为Windows 98开发应用程序,并且也使用COM,那么你将会遇到一些问题。COM要求你使用Unicode字符串。操作系统的大多数函数要求你使用ANSI字符串。那是多么难办的事情啊!我曾经从事过若干个项目的开发,在这些项目中,我编写了许多代码,仅仅是为了来回进行字符串的转换。 2. 8 如何编写Unicode源代码 Microsoft公司为Unicode设计了Windows API,这样,它可以尽量减少对你的代码的影响。实际上,你可以编写单个源代码文件,以便使用或者不使用Unicode来对它进行编译。你只需要定义两个宏(UNICODE和_UNICODE),就可以修改然后重新编译该源文件。 2. 8.1 C运行期库对Unicode的支持 为了利用Unicode字符串,因此定义了一些数据类型。标准的C头文件String.h已经作了修改,以便定义一个名字为wchar_t的数据类型,它是一个Unicode字符的数据类型: 见原书P23的程序(1) 例如,如果你想要创建一个缓存,用于存放最多为99个字符的Unicode字符串和一个结尾为零的字符,你可以使用下面这个语句: 见原书P23的程序(2) 该语句创建了一个由100个16位值组成的数组。当然,标准的C运行期字符串函数,如strcpy、strchr和strcat等,只能对ANSI字符串进行操作,它们不能正确地处理Unicode字符串。因此,ANSI C也拥有一组补充函数。图2-1显示了一些标准的ANSI C字符串函数,后面是它们的等价Unicode函数。 图2-1 标准的ANSI C字符串函数和它们的等价Unicode函数 见原书P23的程序(3)和P24的程序 请注意,所有的Unicode函数均以wcs开头,wcs是宽字符串的英文缩写。若要调用Unicode函数,只需用前缀wcs来取代任何ANSI字符串函数的前缀str即可。 说明 大多数软件开发人员可能已经不记得这样一个非常重要的问题了,那就 是Microsoft公司提供的C运行期库与ANSI的标准C运行期库是一致的。 ANSI C规定,C运行期库支持Unicode字符和字符串。这意味着你始终都可 以调用C运行期函数,以便对Unicode字符和字符串进行操作,即使你是在 Windows 98上运行,也可以调用这些函数。换句话说,wcscat,wcslen和wcstok 等函数都能够在Windows 98上很好地运行,这些都是你必须关心的操作系统函数。 对于包含了对str函数或wcs函数进行显式调用的代码来说,你无法非常容易地同时为ANSI和Unicode对这些代码进行编译。在本章前面部分的内容中,我说过可以创建同时为ANSI和Unicode进行编译的单个源代码文件。若要建立这种双重功能,你必须包含Tchar.h文件,而不是包含String.h文件。 Tchar.h文件的唯一作用是帮助你创建ANSI/Unicode通用源代码文件。它包含你应该用在源代码中的一组宏,而不应该直接调用str函数或者wcs函数。如果你在编译源代码文件时定义了_UNICODE,这些宏就会引用wcs这组函数。如果你没有定义_UNICODE,那么这些宏将引用str这组宏。 例如,在Tchar.h中有一个宏称为_tcscpy。如果在你包含该头文件时没有定义_UNICODE,那么_tcscpy就会扩展为ANSI的strcpy函数。但是如果定义了_UNICODE,_tcscpy将扩展为Unicode的wcscpy函数。拥有字符串参数的所有C运行期函数都在Tchar.h文件中定义了一个通用宏。如果你使用通用宏,而不是ANSI/Unicode的特定函数名,你就能够顺利地创建可以为ANSI或Unicode进行编译的源代码。 但是,除了使用这些宏之外,还有一些操作你是必须进行的。Tchar.h文件包含了一些其他的宏。 若要定义一个ANSI/Unicode通用的字符串数组,请使用下面的TCHAR数据类型。如果定义了_UNICODE,TCHAR将声明为下面的形式: 见原书P25的程序(1) 如果没有定义_UNICODE,则TCHAR将声明为下面的形式: 见原书P25的程序(2) 使用该数据类型,你可以象下面这样分配一个字符串: 见原书P25的程序(3) 你也可以创建对字符串的指针: 见原书P25的程序(4) 不过上面这行代码存在一个问题。按照默认设置,Microsoft公司的C++编译器能够编译所有的字符串,就象它们是ANSI字符串,而不是Unicode字符串。因此,如果没有定义_UNICODE,该编译器将能正确地编译这一行代码。但是,如果定义了_UNICODE,就会产生一个错误。若要生成一个Unicode字符串而不是ANSI字符串,你必须将该代码行改写为下面的样子: 见原书P25的程序(5) 原义字符串前面的大写字母L,用于告诉编译器该字符串应该作为Unicode字符串来编译。当编译器将字符串置于程序的数据部分中时,它在每个字符之间分散插入零字节。这种变更带来的问题是,现在只有当定义了_UNICODE时,程序才能成功地进行编译。我们需要另一个宏,以便有选择地在原义字符串的前面加上大写字母L。这项工作由_TEXT宏来完成,_TEXT宏也在Tchar.h文件中做了定义。如果定义了_UNICODE,那么_TEXT定义为下面的形式: 见原书P25的程序(6)

111,130

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Creator Browser
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧