WideString 还是 AnsiString ?谈谈字符编码

这篇这次不给分了,我发现我的分开始只降不升了,长此以往,岂不穷死。

本来这一篇是讲关于XML字符编码的,我觉得写着写着好像与XML的关系不大了,就改了标题
。所以,看的时候如果感觉到摸不到头脑,那就对了,如果感觉和你的认识不一样,欢迎批
评指正。

这里还有一个字符编码的问题。字符编码在Delphi7中已经得到了很大提高。
Delphi7自己的IDE虽然不能读取Unicode编码的源代码文件,但编译器已经支持
AnsiString和WideString的转换。也就是说,只要定义的时候定义WideString,
那么在后面直接给他赋值时,AnsiString自动转换为WideString,反之亦然。
这样有好处也有坏处,好处是在快速开发中,不需要考虑更多的字符转换问题,
能够比较平顺地从Win98向NT字符集转换,坏处是混淆了字符界限,深入看下
去,有时候搞不清我的内存里究竟是Ansi还是Wide,特别是希望仅仅使用宽字
符的情况下,更要留意字符格式的定义。

WideString保存为文本文件时,常用的有UTF-8、Unicode、Ansi、Unicode Big Endian,
其中 UTF-8 的格式,从文件读取的时候,需要利用 Delphi7 提供的 Utf8ToUnicode
转换一下全部编码,其他几种编码本身都不需要转换(BigEndian编码是摩托罗拉规范,是
intel 规范的 Unicode (即我们现在说的 WideString)编码的字符按字节反转,这符合摩
托罗拉生产的计算机芯片的构造特点,所以读取后要按 WORD 反转),但保存为相应格式的
文本文件时,必须按要求在文件头部写入一个编码识别记号,他们分别为:

Ansi:不需要
Unicode:$FEFF (十六进制编辑器看到的是高位在前显示$FFFE,以下同)
BigEndian:$FFFE (正好是上面 Unicode 的反转)
UTF-8:$BBEF $BF (三字节,十六进制编辑器里显示 $EFBB BF)

这样,其他编辑器读取时就可以识别出保存者把文本翻译成了什么编码。
Unicode(即WideString)只要写好文件头,后面的就按照保存Ansi文本一样把
文本写入文件,保存为Big Endian,则按WORD逐字节反转写入,保存为UTF-8
要利用UnicodeToUtf8转换后写入。

在XML解析中,如果带有非ASCII编码的文字,MS默认使用UTF-16编码,如果
原始文本是Ansi编码,这时将获得乱码的字符。这个编码不是Delphi造成的,是
MS的XML库所致,所以在使用非ASCII字符前,建议转换成UTF-8编码,上面例
子中我没有使用WideString,所以没有实现编码转换。

编码转换有很多现成的开源代码可以利用,其中影响最深远的就是JEDI的Unicoee.pas,
但这个文件很庞大,大约有250K大小,它还带有一个转换表的资源文件,如果
处理一些小型的字符转换就显得杀鸡用牛刀了。当然我们可以直接利用Delphi7
提供给我们的函数,比如:

function PUCS4Chars(const S: UCS4String): PUCS4Char;

function WideStringToUCS4String(const S: WideString): UCS4String;
function UCS4StringToWidestring(const S: UCS4String): WideString;

function UnicodeToUtf8(Dest: PChar; Source: PWideChar; MaxBytes: Integer): Integer;
function UnicodeToUtf8(Dest: PChar; MaxDestBytes: Cardinal; Source: PWideChar; SourceChars: Cardinal): Cardinal;

function Utf8ToUnicode(Dest: PWideChar; Source: PChar; MaxChars: Integer): Integer;
function Utf8ToUnicode(Dest: PWideChar; MaxDestChars: Cardinal; Source: PChar; SourceBytes: Cardinal): Cardinal;

function Utf8Encode(const WS: WideString): UTF8String;
function Utf8Decode(const S: UTF8String): WideString;

function AnsiToUtf8(const S: string): UTF8string;
function Utf8ToAnsi(const S: UTF8string): string;

等等。这些已经足够使用了。轻量级的代码是OmniXML中的TGpTextStream,
不过这个代码有不少BUG,并且不支持BigEndian的写入(读取部分也因忘了使
用临时变量而错误)。这些都可以利用。

在Delphi7中,Edit等控件不支持WideString,但有一组TnTWare的开源控件可
以直接支持WideString。

所以,了解了这些内容后,就可以明确这么多编码在读入内存后变成了什么。
读入内存中的字符其实已经只剩下二种格式了:
要么是 AnsiString,
要么是WideString。
因此,对于认识字符编码的关键就是理解读取和理解保存,只有这二个地方需
要对编码有了解才能正确地完成工作。

哦,对了,还要补充一下Delphi中比较特殊的一个事情:本来我们全程使用了
WideString后,在NT系统下应该可以不考虑处于哪种语言环境的,但是Delphi
的全部控件都是基于Ansi的,因此,除非使用了象Tnt控件一样的显示控件,
否则都要注意字符集的定义。象Edit,如果要显示WideString,Edit的Line.Text
会自动转换为AnsiString,这个转换的依据是活动文档的键盘定义或者活动文档
的字符集定义(字符集定义优先),因此一定不要忘记把Edit字符集设置为与
文本相适应的标志,比如中文,就设置为GB2313_CHARSET,这样,转换时会
使用936的中文字符集。这个设置与具体使用的字体无关,只要强制把这个属
性设置好了,字体是否支持这个集合由系统自动转换。 
 因为 WideString 中最需要转换的编码就是 UTF-8,所以演示了 UTF-8 就可以应用到所有
WideString 编码。

下面的演示代码是把 UTF-8 格式的文本装入只能显示 AnsiString 的 Delphi 自带的 Memo
中,并且可以再将这个 Memo 中的 AnsiString 取出来保存为 UTF-8 格式文本,并且支持
在任何语种的 Windows NT 操作系统上显示中文。

unit frmUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Forms, Dialogs, ComCtrls, Menus, StdCtrls;

type
  TEncodeFlags = (efUnknown, efAnsi, efUnicode, efUncodeBigEn, efUTF8);
  TUniEditFrm = class(TForm)
    MainMenu1: TMainMenu;
    mnuFileItem: TMenuItem;
    mnuOpen: TMenuItem;
    mnuSpace1: TMenuItem;
    mnuSaveAs: TMenuItem;
    mnuSpace2: TMenuItem;
    mnuExit: TMenuItem;
    StatusBar: TStatusBar;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    FStream: TStream;
    OpenDlg: TOpenDialog;
    SaveDlg: TSaveDialog;
    UnicoMemo: TMemo;
    procedure SetMemoCharset;
    procedure LoadFromFile(fName: string);
    procedure SaveToFile(fName: string);
    procedure SetStatusMessage(Msg: string);
    procedure MenuItemOnClick(Sender: TObject);
    function ChWideToAnsi(const StrW: WideString): AnsiString;
    function ChAnsiToWide(const StrA: AnsiString): WideString;
    function UTF8ToWideString(const Stream: TStream): WideString;
    procedure TextToUTF8Stream(const Text: string; var Stream: TStream);
    function GetEncodeFromStream(const Stream: TStream): TEncodeFlags;
  public
    { Public declarations }
  end;

var
  UniEditFrm: TUniEditFrm;

implementation

{$R *.dfm}
type
  TUTF8Falg = packed record
    EF, BB, BF: Byte;
  end;

const
  Encode: TUTF8Falg = (EF: $EF; BB: $BB; BF: $BF);
  MenuActSpace = 0;
  MenuActOpen = 1;
  MenuActSaveAs = 2;
  MenuActExit = 3;

{ TUniEditFrm }

procedure TUniEditFrm.FormCreate(Sender: TObject);
var
  n: integer;
begin
  mnuOpen.Tag := MenuActOpen;
  mnuSaveAs.Tag := MenuActSaveAs;
  mnuExit.Tag := MenuActExit;
  for n := 0 to mnuFileItem.Count - 1 do
    if mnuFileItem.Items[n].Caption <> '-' then
      mnuFileItem.Items[n].OnClick := MenuItemOnClick;
  OpenDlg := TOpenDialog.Create(Self);
  OpenDlg.Filter := 'UTF8 Text File|*.txt';
  SaveDlg := TSaveDialog.Create(Self);
  SaveDlg.Filter := 'UTF8 Text File|*.txt';
  SaveDlg.DefaultExt := '.txt';
  UnicoMemo := TMemo.Create(Self);
  UnicoMemo.Parent := Self;
  UnicoMemo.Align := alClient;
  UnicoMemo.ScrollBars := ssVertical;
  SetMemoCharset;
end;

procedure TUniEditFrm.FormDestroy(Sender: TObject);
begin
  OpenDlg.Free; SaveDlg.Free; UnicoMemo.Free;
  if Assigned(FStream) then FStream.Free;
end;

procedure TUniEditFrm.MenuItemOnClick(Sender: TObject);
begin
  case TComponent(Sender).tag of
    MenuActOpen: if OpenDlg.Execute then LoadFromFile(OpenDlg.FileName);
    MenuActSaveAs: if SaveDlg.Execute then SaveToFile(SaveDlg.FileName);
    MenuActExit: Close;
  end;
end;

procedure TUniEditFrm.SetMemoCharset;
begin
  UnicoMemo.Font.Charset := GB2312_CHARSET;
  UnicoMemo.Font.Size := 12;
end;

procedure TUniEditFrm.SetStatusMessage(Msg: string);
begin
  SendMessage(StatusBar.Handle, WM_USER + 1, 0, DWord(PChar(Msg)));
end;

procedure TUniEditFrm.LoadFromFile(fName: string);
begin
  if not Assigned(FStream) then FStream := TMemoryStream.Create;
  TMemoryStream(FStream).LoadFromFile(fName);
  if GetEncodeFromStream(FStream) = efUTF8 then
  begin
    SetStatusMessage(Format('File: %s ,Size:%d Byte', [fName, FStream.Size]));
    UnicoMemo.Lines.BeginUpdate;
    UnicoMemo.Clear;
    try
      UnicoMemo.Lines.Add(ChWideToAnsi(UTF8ToWideString(FStream)));
    finally
      UnicoMemo.Lines.EndUpdate;
    end;
  end
  else SetStatusMessage(Format('File: %s ,Unknown Encode', [fName]));
  FStream.Size := 0;
end;

procedure TUniEditFrm.SaveToFile(fName: string);
begin
  try
    if not Assigned(FStream) then FStream := TMemoryStream.Create;
    TextToUTF8Stream(UnicoMemo.Lines.Text, FStream);
    TMemoryStream(FStream).SaveToFile(fName);
    SetStatusMessage(Format('Save File: %s ,Size:%d Byte', [fName, FStream.Size]));
  finally
    FStream.Size := 0;
  end;
end;

function TUniEditFrm.ChWideToAnsi(const StrW: WideString): AnsiString;
var
  nLen: integer;
begin
  Result := StrW;
  if Result <> '' then
  begin
    nLen := WideCharToMultiByte(936, 624, @StrW[1], -1, nil, 0, nil, nil);
    SetLength(Result, nLen - 1);
    if nLen > 1 then
      WideCharToMultiByte(936, 624, @StrW[1], -1, @Result[1], nLen - 1, nil, nil);
  end;
end;

function TUniEditFrm.ChAnsiToWide(const StrA: AnsiString): WideString;
var
  nLen: integer;
begin
  Result := StrA;
  if Result <> '' then
  begin
    nLen := MultiByteToWideChar(936, 1, PChar(@StrA[1]), -1, nil, 0);
    SetLength(Result, nLen - 1);
    if nLen > 1 then
      MultiByteToWideChar(936, 1, PChar(@StrA[1]), -1, PWideChar(@Result[1]), nLen - 1);
  end;
end;

function TUniEditFrm.UTF8ToWideString(const Stream: TStream): WideString;
var
  nLen: Cardinal;
begin
  try
    SetLength(Result, Stream.Size div SizeOf(WideChar) * 3);
    nLen := Utf8ToUnicode(@Result[1], Length(Result),
      Pointer(DWord(TMemoryStream(Stream).Memory) + Stream.Position),
      Stream.Size - Stream.Position);
    SetLength(Result, nLen);
  except
    SetLength(Result, 0);
  end;
end;

procedure TUniEditFrm.TextToUTF8Stream(const Text: string; var Stream: TStream);
var
  StringW, StrW: WideString;
  nLen: Cardinal;
begin
  try
    if Text <> '' then
    begin
      StrW := ChAnsiToWide(Text);
      nLen := Length(StrW) * 3;
      SetLength(StringW, nLen);
      nLen := UnicodeToUtf8(@StringW[1], nLen, @StrW[1], Length(StrW));
      SetLength(StringW, nLen);
      Stream.Write(Encode, SizeOf(Encode));
      Stream.Write(StringW[1], Length(StringW));
    end
    else
      Stream.Write(Encode, SizeOf(Encode));
  except
    SetLength(StrW, 0);
    SetLength(StringW, 0);
  end;
end;

function TUniEditFrm.GetEncodeFromStream(const Stream: TStream): TEncodeFlags;
var
  FEncode: TUTF8Falg;
begin
  Result := efUnknown;
  Stream.Read(FEncode, SizeOf(FEncode));
  if (FEncode.EF = Encode.EF) and (FEncode.BB = Encode.BB)
    and (FEncode.BF = Encode.BF) then Result := efUTF8;
end;

end.

代码中有几个控件是动态创建的,其中有对话框和 Memo。这是为了随时改变使用不同控件
库进行测试观察的需要,如果你使用支持宽字符的控件,不用改界面,直接把创建实例改一
改就可以测试观察了。这种写法不是最好的,如果使用接口来描述就会更随意些。如果有时
间,下一次再乱谈Delphi的接口在编程中的应用吧。这一篇就到这里了。

你可能感兴趣的:(WideString 还是 AnsiString ?谈谈字符编码)