【问题标题】:Converting UnicodeString to AnsiString将 UnicodeString 转换为 AnsiString
【发布时间】:2015-01-09 15:04:58
【问题描述】:

在过去,我有一个函数可以将指定代码页的 WideString 转换为 AnsiString

function WideStringToString(const Source: WideString; CodePage: UINT): AnsiString;
...
begin
   ...
    // Convert source UTF-16 string (WideString) to the destination using the code-page
    strLen := WideCharToMultiByte(CodePage, 0,
        PWideChar(Source), Length(Source), //Source
        PAnsiChar(cpStr), strLen, //Destination
        nil, nil);
    ...
end;

一切正常。我向函数传递了一个 unicode 字符串(即 UTF-16 编码数据)并将其转换为 AnsiString,并理解 AnsiString 中的字节表示来自指定代码页的字符.

例如:

TUnicodeHelper.WideStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 1252);

将返回Windows-1252 编码字符串:

The qùíçk brown fôx jumped ovêr the lázÿ dog

注意:在从完整的 Unicode 字符集到 Windows-1252 代码页的有限范围的转换过程中,信息当然会丢失:

  • Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ (之前)
  • The qùíçk brown fôx jumped ovêr the lázÿ dog (之后)

但是 Windows WideChartoMultiByte 在最佳匹配映射方面做得非常好;就像它设计的那样。

现在是以后

现在我们处于后期。 WideString 现在是贱民,UnicodeString 是善良的。这是一个无关紧要的变化;因为 Windows 函数只需要一个指向一系列WideChar指针UnicodeString 也是)。所以我们将声明改为使用UnicodeString

funtion WideStringToString(const Source: UnicodeString; CodePage: UINT): AnsiString;
begin
   ...
end;

现在我们来看看返回值。我有一个包含字节的AnsiString

54 68 65 20 71 F9 ED E7  The qùíç
6B 20 62 72 6F 77 6E 20  k brown 
66 F4 78 20 6A 75 6D 70  fôx jump
65 64 20 6F 76 EA 72 20  ed ovêr 
74 68 65 20 6C E1 7A FF  the lázÿ
20 64 6F 67               dog

在过去这很好。我记录了AnsiString 实际包含的代码页;我必须记住返回的AnsiString 不是使用计算机的区域设置(例如Windows 1258)编码的,而是使用另一个代码页(CodePage 代码页)编码的。 p>

但在 Delphi XE6 中,AnsiString 也秘密包含代码页:

  • 代码页: 1258
  • 长度: 44
  • 值: The qùíçk brown fôx jumped ovêr the lázÿ dog

此代码页错误。 Delphi 正在指定我的计算机的代码页,而不是字符串所在的代码页。从技术上讲,这不是问题,我一直都知道AnsiString 在特定的代码页中,我只需要确保传递该信息。

所以当我想解码字符串时,我必须传递代码页:

s := TUnicodeHeper.StringToWideString(s, 1252);

function StringToWideString(s: AnsiString; CodePage: UINT): UnicodeString;
begin
   ...
   MultiByteToWideChar(...);
   ...
end;

然后一个人搞砸了一切

问题是在过去我声明了一个名为Utf8String的类型:

type
   Utf8String = type AnsiString;

因为它很常见:

function TUnicodeHelper.WideStringToUtf8(const s: UnicodeString): Utf8String;
begin
   Result := WideStringToString(s, CP_UTF8);
end;

反之:

function TUnicodeHelper.Utf8ToWideString(const s: Utf8String): UnicodeString;
begin
   Result := StringToWideString(s, CP_UTF8);
end;

现在在 XE6 中,我有一个 采用 Utf8String 的函数。如果某处的某些现有代码采用 UTF-8 编码的 AnsiString,并尝试使用 Utf8ToWideString 将其转换为 UnicodeString,它将失败:

s: AnsiString;
s := UnicodeStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', CP_UTF8);

...

 ws: UnicodeString;
 ws := Utf8ToWideString(s); //Delphi will treat s an CP1252, and convert it to UTF8

或者更糟的是,现有代码的广度:

s: Utf8String;
s := UnicodeStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', CP_UTF8);

返回的字符串将完全被破坏:

  • 函数返回AnsiString(1252)AnsiString标记为使用当前代码页编码)
  • 返回结果存储在 AnsiString(65001) 字符串 (Utf8String) 中
  • Delphi 将 UTF-8 编码的字符串转换为 UTF-8,就好像它是 1252。

如何前进

理想情况下,我的UnicodeStringToString(string, codePage) 函数(返回AnsiString)可以使用SetCodePage 之类的东西在字符串中设置CodePage 以匹配实际代码页:

function UnicodeStringToString(s: UnicodeString; CodePage: UINT): AnsiString;
begin
   ...
   WideCharToMultiByte(...);
   ...

   //Adjust the codepage contained in the AnsiString to match reality
   //SetCodePage(Result, CodePage, False); SetCodePage only works on RawByteString
   if Length(Result) > 0 then
      PStrRec(PByte(Result) - SizeOf(StrRec)).codePage := CodePage;
end;

除了手动修改 AnsiString 的内部结构非常危险。

那么返回RawByteString呢?

很多不是我的人都说过,RawByteString 应该是普遍接受者;它不应该作为返回参数:

function UnicodeStringToString(s: UnicodeString; CodePage: UINT): RawByteString;
begin
   ...
   WideCharToMultiByte(...);
   ...

   //Adjust the codepage contained in the AnsiString to match reality
   SetCodePage(Result, CodePage, False); SetCodePage only works on RawByteString
end;

这具有能够使用受支持和记录的SetCodePage 的优点。

但是如果我们要越界,然后开始返回RawByteString,那么Delphi 肯定已经有一个函数可以将UnicodeString 转换为RawByteString 字符串,反之亦然:

function WideStringToString(const s: UnicodeString; CodePage: UINT): RawByteString;
begin
   Result := SysUtils.Something(s, CodePage);
end;

function StringToWideString(const s: RawByteString; CodePage: UINT): UnicodeString;
begin
   Result := SysUtils.SomethingElse(s, CodePage);       
end;

但它是什么?

或者我还应该怎么做?

这是一个琐碎问题的冗长背景。 真正的问题当然是我应该怎么做呢?有很多代码依赖于UnicodeStringToString 和反之。

tl;博士:

我可以通过以下方式将 UnicodeString 转换为 UTF:

Utf8Encode('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ');

我可以使用以下方法将UnicodeString 转换为当前代码页:

AnsiString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ');

但我如何将UnicodeString 转换为任意(未指定)代码页?

我的感觉是,既然一切都是AnsiString

Utf8String = AnsiString(65001);
RawByteString = AnsiString(65535);

我应该咬紧牙关,打开AnsiString 结构,然后将正确的代码页插入其中:

function StringToAnsi(const s: UnicodeString; CodePage: UINT): AnsiString;
begin
   LocaleCharsFromUnicode(CodePage, ..., s, ...);

   ...

   if Length(Result) > 0 then
      PStrRec(PByte(Result) - SizeOf(StrRec)).codePage := CodePage;
end;

那么 VCL 的其余部分就会排成一行。

【问题讨论】:

标签: delphi unicode internationalization delphi-xe6


【解决方案1】:

我认为返回RawByteString 可能与您得到的一样好。您可以使用AnsiString 来完成此操作,但RawByteString 可以更好地捕捉意图。在这种情况下,RawByteString 在道德上被视为 Embarcadero 官方建议意义上的参数。它只是一个输出而不是一个输入。真正的关键是不要将其用作变量。

你可以这样编码:

function MBCSString(const s: UnicodeString; CodePage: Word): RawByteString;
var
  enc: TEncoding;
  bytes: TBytes;
begin
  enc := TEncoding.GetEncoding(CodePage);
  try
    bytes := enc.GetBytes(s);
    SetLength(Result, Length(bytes));
    Move(Pointer(bytes)^, Pointer(Result)^, Length(bytes));
    SetCodePage(Result, CodePage, False);
  finally
    enc.Free;
  end;
end;

然后

var
  s: AnsiString;
....
s := MBCSString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 1252);
Writeln(StringCodePage(s));
s := MBCSString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 1251);
Writeln(StringCodePage(s));
s := MBCSString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 65001);
Writeln(StringCodePage(s));

按照您的预期输出 1252、1251 和 65001。

如果您愿意,可以使用LocaleCharsFromUnicode。当然,您需要对its documentation 加一点盐:LocaleCharsFromUnicode 是 WideCharToMultiByte 函数的包装器。令人惊讶的是,自从LocaleCharsFromUnicode 以来一直编写文本肯定只是为了跨平台而存在。


但是,我想知道您在尝试将 ANSI 编码的文本保留在程序中的 AnsiString 变量中是否会出错。通常,您会尽可能晚地(在互操作边界处)编码为 ANSI,并尽可能早地解码。

如果您只需要这样做,那么也许有更好的解决方案可以完全避免可怕的AnsiString。不要将文本存储在AnsiString 中,而是将其存储在TBytes 中。您已经拥有跟踪编码的数据结构,所以为什么不保留它们。将包含代码页和AnsiString 的记录替换为包含代码页和TBytes 的记录。这样你就不用担心在你背后重新编码你的文本了。您的代码将可以在移动编译器上使用。

【讨论】:

  • 当时使用AnsiString 是为了方便。它的特点是易于申报、易于操作、写时复制、易于返回。 (参见声明的参数array of Byte,以及声明的返回类型TByteDynArray)。
  • 虽然TEncoding 的使用比使用LocaleCharsFromUnicode()(或WideCharToMultiByte())要干净一些,但它确实需要使用两倍的内存(一个用于TBytes,然后再次用于Result),即使是暂时的。顺便说一句,您可以使用SetString() 而不是SetLength()/Move()
  • 至于LocaleCharsFromUnicode() 文档,它可能是在LocaleCharsFromUnicode() 在添加新平台之前实际上只是WideCharToMultiByte() 本身的一个包装器时编写的,并且没有相应地更新。
  • @Remy 如果在 Win32 是唯一编译器时添加了 LocaleCharsFromUnicode,那么为什么要添加它?当然它的存在是为了支持跨平台。即使它是在 bcc32 是唯一的编译器时添加的,他们肯定也可以预见到未来。无论如何,我在猜测。不是最有成效的任务。
  • @DavidHeffernn: 是的,它们是为跨平台添加的,每个 cmets 在 System 单元中:"LocaleCharsFromUnicode is a cross-platform wrapper for WideCharToMultiByte with an emulated implemention on non-Windows platforms.""UnicodeFromLocaleChars is a cross-platform wrapper for MultiByteToWideChar with an emulated implemention on non-Windows platforms." 这两个函数是在 XE 中添加的,但 FireMonkey 和 OSX 支持在 XE2 中引入。
【解决方案2】:

在这种特殊情况下,使用RawByteString 是一个合适的解决方案:

function WideStringToString(const Source: UnicodeString; CodePage: UINT): RawByteString;
var
  strLen: Integer;
begin
  strLen := LocaleCharsFromUnicode(CodePage, 0, PWideChar(Source), Length(Source), nil, 0, nil, nil));
  if strLen > 0 then
  begin
    SetLength(Result, strLen);
    LocaleCharsFromUnicode(CodePage, 0, PWideChar(Source), Length(Source), PAnsiChar(Result), strLen, nil, nil));
    SetCodePage(Result, CodePage, False);
  end;
end;

这样,RawByteString 保存代码页,并将RawByteString 分配给任何其他字符串类型,无论是AnsiStringUTF8String 还是其他,将允许RTL 自动转换RawByteString数据从其当前代码页到目标字符串的代码页(包括到 UnicodeString 的转换)。

如果您绝对必须返回 AnsiString(我不推荐),您仍然可以通过类型转换使用 SetCodePage()

function WideStringToString(const Source: UnicodeString; CodePage: UINT): AnsiString;
var
  strLen: Integer;
begin
  strLen := LocaleCharsFromUnicode(CodePage, 0, PWideChar(Source), Length(Source), nil, 0, nil, nil));
  if strLen > 0 then
  begin
    SetLength(Result, strLen);
    LocaleCharsFromUnicode(CodePage, 0, PWideChar(Source), Length(Source), PAnsiChar(Result), strLen, nil, nil));
    SetCodePage(PRawByteString(@Result)^, CodePage, False);
  end;
end;

反过来要容易得多,只需使用已存储在 (Ansi|RawByte)String 中的代码页(只需确保这些代码页始终准确),因为 RTL 已经知道如何为您检索和使用代码页:

function StringToWideString(const Source: AnsiString): UnicodeString;
begin
  Result := UnicodeString(Source);
end;

function StringToWideString(const Source: RawByteString): UnicodeString;
begin
  Result := UnicodeString(Source);
end;

话虽如此,我建议完全放弃辅助函数,而只使用类型化字符串。让 RTL 为您处理转换:

type
  Win1252String = type AnsiString(1252);

var
  s: UnicodeString;
  a: Win1252String;
begin
  s := 'Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ';
  a := Win1252String(s);
  s := UnicodeString(a);
end;

var
  s: UnicodeString;
  u: UTF8String;
begin
  s := 'Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ';
  u := UTF8String(s);
  s := UnicodeString(u);
end;

【讨论】:

  • 关于最后一节,在运行时确定代码页时无济于事
  • @DavidHeffernan:没错,这就是RawByteString 发挥作用的地方。最好不要弄乱与普通 AnsiString 关联的代码页。在将其分配给UnicodeString之前,将UnicodeString转换为具有适当代码页的RawByteString,并将Ansi数据存储在具有适当代码页的RawByteString中,是这种情况下的最佳选择。
  • 我不打算返回AnsiString;我只需要非常小心地更改所有现有代码(在每个项目中、每个共享库中、每个 dll 中等)。在已内置到二进制文件中的代码(例如 dll/bpl)中尤其麻烦。
  • 在这种情况下,提示指令(如 deprecated)会派上用场。将您受影响的函数/类/变量标记为deprecated,让编译器向您展示它们被使用的任何地方,然后您可以根据需要调整该代码。
  • 跨 DLL 边界传递 Delphi 字符串总是错误的。你确实需要停止这样做。如果您不跨越 DLL 边界,那么代码驻留在 DLL 中的事实既不存在也不存在。
【解决方案3】:

通过System.pas 的抱怨,我发现了内置函数SetAnsiString 可以满足我的需求:

procedure SetAnsiString(Dest: _PAnsiStr; Source: PWideChar; Length: Integer; CodePage: Word);

还需要注意的是,此函数确实将 CodePage 推送到我的内部 StrRec 结构中:

PStrRec(PByte(Dest) - SizeOf(StrRec)).codePage := CodePage;

这让我可以写如下内容:

function WideStringToString(const s: UnicodeString; DestinationCodePage: Word): AnsiString;
var
   strLen: Integer;
begin
   strLen := Length(Source);

   if strLen = 0 then
   begin
      Result := '';
      Exit;
   end;

   //Delphi XE6 has a function to convert a unicode string to a tagged AnsiString
   SetAnsiString(@Result, @Source[1], strLen, DestinationCodePage);
end;

所以当我打电话时:

actual := WideStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 850);

我得到了结果AnsiString

codePage: $0352 (850)
elemSize: $0001 (1)
refCnt:   $00000001 (1)
length:   $0000002C (44)
contents: 'The qùíçk brown fôx jumped ovêr the láZÿ dog' 

一个 AnsiString,其相应的代码页已填充到秘密 codePage 成员中。

另一种方式

class function TUnicodeHelper.ByteStringToUnicode(const Source: RawByteString; CodePage: UINT): UnicodeString;
var
    wideLen: Integer;
    dw: DWORD;
begin
{
    See http://msdn.microsoft.com/en-us/library/dd317756.aspx
    Code Page Identifiers
    for a list of code pages supported in Windows.

    Some common code pages are:
        CP_UTF8 (65001) utf-8               "Unicode (UTF-8)"
        CP_ACP  (0)                         The system default Windows ANSI code page.
        CP_OEMCP    (1)                         The current system OEM code page.
        1252                    Windows-1252    "ANSI Latin 1; Western European (Windows)", this is what most of us in north america use in Windows
        437                 IBM437          "OEM United States", this is your "DOS fonts"
        850                 ibm850          "OEM Multilingual Latin 1; Western European (DOS)", the format accepted by Fincen for LCTR/STR
        28591                   iso-8859-1      "ISO 8859-1 Latin 1; Western European (ISO)", Windows-1252 is a super-set of iso-8859-1, adding things like euro symbol, bullet and ellipses
        20127                   us-ascii            "US-ASCII (7-bit)"
}
    if Length(Source) = 0 then
    begin
        Result := '';
        Exit;
    end;

    // Determine real size of final, string in symbols
//  wideLen := MultiByteToWideChar(CodePage, 0, PAnsiChar(Source), Length(Source), nil, 0);
    wideLen := UnicodeFromLocaleChars(CodePage, 0, PAnsiChar(Source), Length(Source), nil, 0);
    if wideLen = 0 then
    begin
        dw := GetLastError;
        raise EConvertError.Create('[StringToWideString] Could not get wide length of UTF-16 string. Error '+IntToStr(dw)+' ('+SysErrorMessage(dw)+')');
    end;

    // Allocate memory for UTF-16 string
    SetLength(Result, wideLen);

    // Convert source string to UTF-16 (WideString)
//  wideLen := MultiByteToWideChar(CodePage, 0, PAnsiChar(Source), Length(Source), PWChar(wideStr), wideLen);
    wideLen := UnicodeFromLocaleChars(CodePage, 0, PAnsiChar(Source), Length(Source), PWChar(Result), wideLen);
    if wideLen = 0 then
    begin
        dw := GetLastError;
        raise EConvertError.Create('[StringToWideString] Could not convert string to UTF-16. Error '+IntToStr(dw)+' ('+SysErrorMessage(dw)+')');
    end;
end;

注意:任何发布到公共领域的代码。无需署名。

【讨论】:

  • 最好/正确的方法是如何做到这一点?转换 AnsiString 例如从具有不同代码页的文件然后操作系统到 Unicode 字符串?示例:系统 CP 为 1252,文件代码页为 1250(在某些 meta-header-info 中描述)
  • 对 Ansistring 执行 "SetCodePage(MyCP1250AnsiString, 1250, false)" 然后只执行 MyUnicodeString := MyCP1250AnsiString 将其转换为 unicode 字符串是否足够?还是我错过了什么?
  • @Hugie 对于这种情况,我使用 Windows 函数 MultiByteToWideChar。你给它一个 AnsiString 的字节和它当前所在的代码页,Windows 可以为你将它转换为 UTF-16。
  • 这似乎类似于我最终发现的通过 Delphi API 的方法:UniStr := String( TEncoding.GetEncoding(SourceCP).GetChars( SourceBytes ) ) ; 谢谢你的额外工作。
猜你喜欢
  • 2014-01-19
  • 1970-01-01
  • 1970-01-01
  • 2012-07-02
  • 1970-01-01
  • 1970-01-01
  • 2011-05-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多