WideString 还是 AnsiString ?谈谈字符编码

http://www.delphibbs.com/delphibbs/dispq.asp?lid=3349991

问题: WideString 还是 AnsiString ?谈谈字符编码。 ( 积分:0, 回复:12, 阅读:242 )
分类: 编程手记 ( 版主:DNChen, cAkk )

来自: 小雨哥 , 时间: 2006-2-10 17:12:00, ID: 3349991

 

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

本 来这一篇是讲关于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的中文字符集。这个设置与具体使用的字体无关,只要强制把这个属
性设置好 了,字体是否支持这个集合由系统自动转换。

如果要了解更多这方面的情况,建议使用虚拟电脑模拟语言环境来观察。暂时就先到这里。  

 

来 自: satanmonkey , 时间: 2006-2-10 20:09:45, ID: 3350123
好文章,收藏  

 

来 自: 小雨哥 , 时间: 2006-2-11 11:24:21, ID: 3350814
呵呵,没人拍砖,象是感兴趣 的人不多啊。那就收尾吧。

因为 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的接口在编程中的应用吧。这一篇就到这里 了。  

 

来 自: dirk , 时间: 2006-2-11 11:38:39, ID: 3350833
懒啊,你每天登录一把不就有 分了?[:D]

好文,顶!  

 

来 自: anyway , 时间: 2006-2-11 21:01:15, ID: 3351261
哎呀!咋不早说!你发贴的前 一天我就让XML的字符编码差点没搞崩溃了
幸亏后来解决了
不过还是收藏了,谢谢楼主  

 

来 自: anyway , 时间: 2006-2-11 21:07:10, ID: 3351264
小雨哥
请教一下:
可 不可以用文件流直接读入文本文件
然后用上述函数转换成ansiString?  

 

来 自: 小雨哥 , 时间: 2006-2-12 10:31:27, ID: 3351314
你这里指的“文件流”是什么 概念?磁盘文件?还是某块内存?还是 Delphi 的 TStream?

我想应该不是指 TStream 。那么好,假设它是磁盘文件好了。当使用 FileOpen 后,你可
以直接用 CreateFileMapping 把这个文件建成映像,如果这是一个已经存在的物理文件,
那么 GetFileSize 可以知道这个文件的真实大小。CreateFileMapping 后就可以使用文件
映像的一系列操作获得这个文件了。

使用 MapViewOfFile 返回的指针就是文件的最开头。当你已经能够确切地知道这个文件是
某个类型、某种编码的文件,并且很肯定的话,那么我 下面说的步骤你可以酌情省略。

读取文件头,检查是否是你要处理的文件,比如 Unicode ,你可以定义一个 Word 变量:

var
 Unicode:Word;
begin
 Move(MapViewOfFile^,@Unicode,SizeOf(Word));
end;

这 样就取到了 2 个字节的 Unicode 编码标志,检查它是否符合 Unicode 标志,不是就放
弃,是的话就继续。

获知 它确实是你要的 Unicode 文件后,继续以前,你要确定 1 件事,就是你是否打算分段
读取。文件映像你不读它,它是不会占用你的内存的, 如果你只需要一点一点地显示,那么
可以采用分段读取。这里假设你采用分段读取,只读其中的一部分:

function UnicodeFileStreamToAnsiString(ptr:Pointer;nLen:DWord):AnsiString;
var
 StrW:WideString;
begin
 SetLength(StrW,nLen div SizeOf(WideChar));
 Move(ptr^,@StrW,Length(StrW)*SizeOf(WideChar));
 Result:=StrW;
end;

好 了。现在你已经读到了需要的长度的 Unicode 文本了,值得注意的是:这个 ptr 是减掉
Unicode 文件头后的任意 Word 对齐的位置,并且这个函数正好利用了 Delphi 自动转换
WideString 到 AnsiString 的特性。这时输出的结果已经是你需要的 AnsiString 了。

假如只是一个小文件,你可能采用一次性全部读取,在 Unicode 一次读取中,你最好在建
映像文件的时候就考虑到要这样做,这样的话,你可以提前在建文件映像的时候把文件映像
建得比实际尺寸大一个 Word 字节,并设置可写属性,当确定是一个想要的 Unicode 文件
后,你只要在增加的那个 Word 字节里填入两个 #0,就可以把这个文件映像直接当成内存
中的 PWideChar 来使用,也就意味着连转换代码都不用写,全部由 Delphi 帮你解决。

如 果你正好很不巧地要读的是 UTF-8 文本,那么你不能分割了,你只能使用我上面代码中
提供的示范,不过为了读取磁盘文件流,代码将改成下面的 样子:

function UTF8ToWideString(ptr:Pointer;FileSize:DWord): WideString;
var
  nLen: Cardinal;
begin
    SetLength(Result, FileSize div SizeOf(WideChar) * 3);
    nLen := Utf8ToUnicode(@Result[1], Length(Result),
      Pointer(DWord(ptr) + SizeOf(TUTF8Falg)),
      FileSize - SizeOf(TUTF8Falg));
    SetLength(Result, nLen);
end;

注意:这个函数里的 ptr 是从文件的最开头算起。

这 个函数返回一个 WideString ,你爱转换也好,你不爱转换也好,我上面帖子已经说了,
Delphi 自动会把 WideString 转换为 AnsiString ,你只要这样:

var
 s:AnsiString
begin
 s:=UTF8ToWideString(...);
end;

如 果希望 UTF-8 也要分段读取,就要自己完成自己的解码算法。我上面帖子里提到的几个
著名的开放源项目中都有代码例子可供参考。

好 了,就这些,其实比 TStream 还简单。这里介绍的是文件映像,其实直接使用 ReadFile
(注意不是 Delphi 中的文件操作,是指 API 的文件操作)操作也一样。  

 

来 自: 小雨哥 , 时间: 2006-2-12 0:21:54, ID: 3351315
to dirk
 你 好像也是超级潜水员啊,论坛潜水还不够,QQ 也潜水,不过弄不懂你潜水怎么还涨分,我就不涨呢。
  

 

来 自: anyway , 时间: 2006-2-12 13:57:03, ID: 3351427
to dirk
那么 直接用FileStream打开的TXT文件,前两个字节是数据还是编码类型?
  

 

来 自: 小雨哥 , 时间: 2006-2-12 16:33:07, ID: 3351434
anyway,你可能是问我 吧?

如果你说的“FileStream”,是Delphi 中的 TFileStream 的话,那么直接可以用我上面例
子代码 中的片段,如果你是指文件映像这样的“流”的话,那么应该知道文件映象创建起来
后,需要通过MapViewOfFile才能得到读取的具体内存位 置。这个函数声明:

function MapViewOfFile(
    hFileMappingObject: THandle; // file-mapping object to map into address space  
    dwDesiredAccess: DWORD; // access mode
    dwFileOffsetHigh, // high-order 32 bits of file offset
    dwFileOffsetLow, // low-order 32 bits of file offset
    dwNumberOfBytesToMap: // number of bytes to map
    DWORD): Pointer; stdcall;

其中 3、4 的参数设置指针位置,如果你设置为 0,0 ,就是文件的最开头。其中应该就包
括编码类型指示的那 2 个字节。ReadFile 也是这样,可以看一下函数声明。ReadFile 函
数你还可以自定义一个类型给它读,比如:

 TImNeedRead = packed record
    Falg:Word;
    Text:array[0..0]of WideChar;
  end;

这样就直接可以通 过一个简单的结构保证读取指针的正常。是不是很有趣。呵呵。  

 

来 自: andyzhouap98111 , 时间: 2006-2-12 14:19:47, ID: 3351441
在delphi里如 Label的Caption中的汉字在自动长度状态下会看不到后面部分是不是和这个有关  

 

来 自: anyway , 时间: 2006-2-12 15:19:07, ID: 3351453
to 小雨哥
好,我 试试,谢谢  

 

来 自: 小雨哥 , 时间: 2006-2-17 23:12:24, ID: 3356675
To andyzhouap98111:

你说的情况和 AnsiString、WideString 没有关系。那是字符集的关系。Label 中计算文字
需要多少宽度由系统来进行,系统则根据 HDC 的字符集属性来计算正确的尺度。你只要为
Label 在显示中文的时候,正确地选择 GB2312 字符集就可以了。 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值