WideString 还是 AnsiString ?谈谈字符编码。(0分)

  • 主题发起人 小雨哥
  • 开始时间

小雨哥

Unregistered / Unconfirmed
GUEST, unregistred user!
这篇这次不给分了,我发现我的分开始只降不升了,长此以往,岂不穷死。
本来这一篇是讲关于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 - 1do
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.begin
Update;
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的接口在编程中的应用吧。这一篇就到这里了。
 
懒啊,你每天登录一把不就有分了?[:D]
好文,顶!
 
哎呀!咋不早说!你发贴的前一天我就让XML的字符编码差点没搞崩溃了
幸亏后来解决了
不过还是收藏了,谢谢楼主
 
小雨哥
请教一下:
可不可以用文件流直接读入文本文件
然后用上述函数转换成ansiString?
 
你这里指的“文件流”是什么概念?磁盘文件?还是某块内存?还是 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 的文件操作)操作也一样。
 
to dirk
你好像也是超级潜水员啊,论坛潜水还不够,QQ 也潜水,不过弄不懂你潜水怎么还涨分,我就不涨呢。
 
to dirk
那么直接用FileStream打开的TXT文件,前两个字节是数据还是编码类型?
 
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;

这样就直接可以通过一个简单的结构保证读取指针的正常。是不是很有趣。呵呵。
 
在delphi里如Label的Caption中的汉字在自动长度状态下会看不到后面部分是不是和这个有关
 
to 小雨哥
好,我试试,谢谢
 
To andyzhouap98111:
你说的情况和 AnsiString、WideString 没有关系。那是字符集的关系。Label 中计算文字
需要多少宽度由系统来进行,系统则根据 HDC 的字符集属性来计算正确的尺度。你只要为
Label 在显示中文的时候,正确地选择 GB2312 字符集就可以了。
 
嗯。好像再没什么人感兴趣了。结束吧,没分。
 
555咋没分
 
对于没 有 efbbbf 的 utf-8编码 识别你可没有提到阿。
 
顶部