Unicode

 DelphiでUnicodeを扱うにあたって、まずはUnicodeそのものの概要を知っておく必要があります。最低限、ここに書いてある知識がないと、後で落とし穴にハマる事になります。

Unicodeって?
 簡単に言えば、65536文字にすべての国の文字を収めようとしたのがUnicode1.0でした。その後とても16bitの範囲に収まりきれなくなって、現在では21bitになっています。

 Unicodeは文字集合であり、その文字が格納されている場所をコードポイントと呼びます。コードポイントは16bit以下の領域の物をU+xxxx(4桁固定)で表わし、それ以上のものをU+10000..U+10FFFF(桁可変)で表 わします。"U+"で表わされる値の事をスカラー値と呼びます。Unicodeの先頭128文字(7bitの範囲)はAnsi文字をアップスケールしたものと同等です。

UCSって?
 簡単に言えば、Unicodeと互換性のある文字集合です。UCS2は16bitの文字集合で、Unicode1.0と互換性があります。UCS4は31bitの文字集合ですが、コードポイント の範囲はUnicodeに倣って21bitとなります。UCSに続く数値はコードポイントを表すために必要なオクテットの数です。

UTFって?
 簡単に言えば、Unicode/UCSをバイト列/ワード列/DWORD列に変形して扱いやすくするための符号化の種類です。UTFに続く数値は符号化後の"文字を構成する要素"が必要なbit数です。

"簡単に言えば"ってどういう事?
 簡単に書かないと、理解が進まないからです。歴史的背景は可能な限り端折ってあります。

UCS4とは?
 1文字を4octet(32bit)で表わす文字集合です。文字構成要素(Element Size)は4octet(32bit)ですが、文字集合としては31bitで、コードポイントはUnicodeに倣って21bitまでしか割り当てられない事になりました。

UCS2とは?
 1文字を2octet(16bit)で表わす文字集合です。Unicode1.0と互換性があります。

BMPとは?
 Unicode1.0/UCS2の範囲、UnicodeのコードポイントでU+0000..U+FFFFの範囲にある文字を格納したプレーンの事です。U+10000..U+10FFFFは16個のプレーンに分割され、その16個のプレーンにも名前が付いています。



UTF-32とは?
 Unicode/UCS4を32bitで表現するための符号化の一種です。欠点としては、文字構成要素(Element Size)が32bitなため、エンディアンの影響を受けます。符号化された文字はUnicode/UCS4と1対1で対応し、他のUTFへロスレス変換が可能です。

特徴:
UTF-32の構造は?


 Unicode/UCS4をそのまま32bitの器に押し込んだだけです。

UTF-16とは?
 元々はUnicode1.0/UCS2 を16bitで表現するための符号化方式でした。現在では仕様が拡張され、サロゲートという構造を用いて21bitまでのコードポイントを保持できます。サロゲートを2つ合わせてコードポイントを表します。これがサロゲートペアで、UTF-16には1word/2wordの文字があります。欠点としては、文字構成要素(Element Size)が16bitなため、エンディアンの影響を受けます。符号化された文字はUnicode/UCS4(21bitの範囲)と1対1で対応し、他のUTFへロスレス変換が可能です。

特徴:
UTF-16の構造は?



[UTF-16 サロゲートペア (1ワード目)]
16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
サロゲート #1 (110110) プレーン番号(Unicodeの上位 5bit)-1 Unicodeの上位6bit目から 6bit

[UTF-16  サロゲートペア (2ワード目)]
16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
サロゲート #2 (110111) Unicodeの下位10bit

 U+0000..U+FFFFまでの文字(BMP)は素直に1wordで表わします。それ以外の文字はサロゲート領域を使った符号化を用いて2wordで表わします。この2wordで表わされる文字をサロゲートペアと呼びます。

UTF-8とは?
 Unicode/UCS4を8bitのバイト列で表現するための符号化の一種です。文字構成要素(Element Size)は8bitです。Unicodeは最大で4バイト文字、UCS4は最大で6バイト文字が存在します。但し、UCS4はコードポイントを21bitの範囲で抑える事になったので、5-6バイト文字は実際に使われる事はありません。 また、1バイト文字はAnsi文字の7bitの範囲で互換性があります。符号化された文字はUnicode/UCS4(21bitの範囲)と1対1で対応し、他のUTFへロスレス変換が可能です。

 一般に、"UTF-8はUTF-16よりも効率がいい"とされますが、そうとばかりも言えません。多くの日本語の文字はUTF-8では3バイト文字(8*3=24bits)ですが、UTF-16では1ワード文字(16*1=16bits)です。 UTF-16のサロゲートペアは確かに2ワード文字(16*2=32bits)なのですが、サロゲートペアの領域の文字はUnicodeのコードポイントで"U+10000以上"です。これはUTF-8では4バイト文字(8*4=32bits)となり、サイズはどちらも変わらない事になります。 "日本語を多く記述するのであればUTF-16の方が効率がいい事もある"という事です。

特徴:
UTF-8の構造は?

[1バイト文字]
8 7 6 5 4 3 2 1
0 U+0000~U+007F(BMP)


[2バイト文字 (1バイト目)]
8 7 6 5 4 3 2 1
1 1
0 U+0080~U+07FF(BMP) の上位5ビット分
[2バイト文字 (2バイト目)]
8 7 6 5 4 3 2 1
1 0
U+0080~U+07FF(BMP) の下位6ビット分


[3バイト文字 (1バイト目)]
8 7 6 5 4 3 2 1
1 1
1 0
U+0800~U+FFFF(BMP) の上位4ビット分
[3バイト文字 (2バイト目)]
8 7 6 5 4 3 2 1
1 0
U+0800~U+FFFF(BMP) の上位5ビットから6ビット分
[3バイト文字 (3バイト目)]
8 7 6 5 4 3 2 1
1 0
U+0800~U+FFFF(BMP) の下位6ビット分


[4バイト文字 (1バイト目)]
8 7 6 5 4 3 2 1
1 1
1 1
0
U+10000~U+10FFFFの上位3ビット分
[4バイト文字 (2バイト目)]
8 7 6 5 4 3 2 1
1 0
U+10000~U+10FFFFの上位4ビット目から6ビット分
[4バイト文字 (3バイト目)]
8 7 6 5 4 3 2 1
1 0
U+10000~U+10FFFFの上位10ビット目から6ビット分
[4バイト文字 (4バイト目)]
8 7 6 5 4 3 2 1
1 0
U+10000~U+10FFFFの下位6ビット分

 こうなります。
 2 バイト文字は11bit(5+6)、3バイト文字は16bit(4+6+6)、4バイト文字は21bit(3+6+6+6)表せます。つまり、BMPは3 バイトで収まり、Unicode全体でも4バイトで収まる事になります。故に、UTF-8での4バイト文字はBMP以外のUnicode、 UTF-16で言うサロゲートペアと同じコードポイントの文字を扱います。

 あと2ビット分符号化できますので、これを使えば5バイト文 字/6バイト文字が表現できます(最初のバイトをすべて符号化に使うのであれば7バイト文字も可能ではありますが)。6バイト文字の場合に使えるビット数 は"1+6+6+6+6+6=31bit"となります。

ここまでのまとめ。
 Unicode/UCS/UTFの関係を表にすると以下のようになります。

Charset/Encoding
Range of 16bit (Legacy Unicode) Range of 32bit
Unicode (21bit集合) U+0000~U+FFFF (BMP/Plane #0) U+10000~U+10FFFF
(Plane #1~Plane #16)
UCS-4 (31bit集合) 0x00000000~0x0000FFFF
(Group #0 Plane #0)
0x00010000~0x7FFFFFFF
(Group #0 Plane #1~Group #127 Plane #255)
UCS-2 (16bit集合) 0x0000~0xFFFF N/A
UTF-32
(32bitを表現可能)
Unicode 1DWORD文字
1DWORD文字
UCS-4
UCS-2 N/A
UTF-16
(21bitを表現可能)
Unicode 1WORD文字
2WORD文字 (サロゲートペア)
※UCS-4のすべての範囲を表わす事はできない。
UCS-4
UCS-2 N/A
UTF-8
(31bitを表現可能)
Unicode 1byte文字~3byte文字 4byte文字
UCS-4 4byte文字~6byte文字
UCS-2 N/A

BOMとは?
 エンディアンの影響を受ける符号化文字列を扱うファイルに於いて、エンディアンを識別するためにファイル先頭に付加される"見えない文字"の事です。バイト・オーダ・マークの略で、具体的には"ZERO WIDTH NO-BREAK SPACE(U+FEFF)という見えない文字"の事です。

 処理系によってはハイバイトとローバイトが入れ替わる場合があります。"0x1234"というWORDのデータを読む場合、"0x12 0x34"と表現されるのがビッグエンディアン、逆に"0x34 0x12"となるのがリトルエンディアンです。このエンディアンを識別する為にUTF-16では0xFEFF(ビッグエンディアン)/0xFFFE(リト ルエンディアン)をファイルの先頭に付加します。

 UTF-8は先に述べたように"Byte単位処理"なので、エンディアンは関係ありません。しかし、BOMが付加されたUTF-8のファイルが存在します...何故UTF-8にBOMがあるのでしょうか?それは"ASCII と区別が付かない事があるから"で す。"ASCII圏の人には最小限の変更で済むアップグレードパス"ではありますが、ASCIIの範囲で書かれたテキストファイルは、それがASCIIなのかUTF-8なのか区別が付きません。 例えASCIIの範囲以外の文字があっても容易に判断できるものではなく、文字化けの原因となります。これを回避するためにUTF-8にBOM(正確には"U+FEFF"をUTF-8で符号化したもの)を付加する必要があったのです。しかしながら、このBOM付きUTF-8は「本来は規格外」であるため、処理系によっては異常をきたす場合があります。

 UTF-8のBOMは"エンコーディングを判定するためにある"のであり、"バイトオーダを調べるためのもの"ではありません。ですから、本来はBOMと呼ぶべきではないのですが、UTF16/UTF-32に於いて"U+FEFF"がBOMと呼ばれていることから、UTF-8でもBOMと呼ばれています。

Unicodeと全角/半角
 Unicodeには全角/半角という概念はありません。

昔々はテキストを表示するのにVRAMを使っていました。VRAMと言うのはビデオメモリの事ですが、現在のビットマップなビデオメモリではなく、テキストVRAMというものの事です。テキストVRAMはそのアドレスに値を書き込むと画面に文字が表示されました。

H e l l o , w o r l d .                                                                                                                                        
                                                                                                                                                   
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               
                                                                                                                                                               

 こんな感じです。テキストVRAMの先頭に0x48を書き込むと"H"が表示される訳です。これが単色のテキストVRAMで、色の出せるテキストVRAMの場合にはRGBのテキストVRAMが3プレーン用意されていて、それぞれのプレーンに同じ文字を書き込んで色を表現するものや、1文字分を2バイトで表現し、半分をアトリビュートエリアに使うものがありました。テキストVRAMは固定されたサイズですから、フォントもANK文字で16x8、漢字で16x16と固定されていました。大きな文字やプロポーショナルな文字をテキストVRAMに書く事はできず、グラフィックVRAMと呼ばれるビットマッ プなメモリに書く必要がありました。余談ですが、グラフィックVRAMにも水平ビットマップ/垂直ビットマップ等の種類がありました。

 昔々は"文字=メモリ"で、"文字幅=バイトサイズ"だった訳ですね。スクロールや文字列コピーはメモリを操作する事で実現しました。フォントはROMとして用意されていたので、テキストVRAMで表示されるフォントを変更する方法は基本的にありません。表示位置を1ドットずらす事も基本的には不可能でし た。テキストVRAMに書き込む文字コードもPC-98ではJIS-CODE固定でした(PC-98用のMS-DOSはSHIFT-JISですが、テキストVRAM自体はJIS-CODEです)。

  これが巷で言われる全角/半角です。SHIFT-JISでは基本的には2バイト文字は全角と考えて差し支えありませんでした(例外的に2バイト半角カナがあります)。しかし、Unicodeのスカラー値と文字の幅には関係はありません。例えば、ギリシャ文字は日本語環境では全角サイズ、英語環境では半角サイズで描画されます。Unicodeとしては同じコードポイントなのに、です。

 この問題を解決するには、Unicodeコンソシアムで策定されているEastAsianWidth属性を利用します。詳しくはWikipediaの"東アジアの文字幅"を参考にして下さい。なお、やってみれば解る事ですが、GetStringTypeW()/GetStringTypeEx()では半角サイズ(HALF_WIDTH)/全角サイズ(FULL_WIDTH)をマトモに処理できません。

Unicodeに存在する見えない文字
 Unicode でコードポイント"U+200B"の文字は"ZERO WIDTH SPACE"です。UTF-8で言えば"文字幅0の3バイト文字"になります。BOMも見えない文字の一つです(U+FEFF: "ZERO WIDTH NO-BREAK SPACE")。Windowsではファイル名に Unicodeが使えますので、これらの文字を使って"ファイル名がないように見えるファイル"を作る事ができます...くれぐれも悪用しないで下さい。

結合文字
 ここに「が」という文字があります。この文字はUTF-8で何バイトでしょうか?そう...日本語の普通の全角文字だから3バイトですよね?

違います。

 正解は"6バイト"です。「Unicodeは21bitでUTF-8なら4バイト文字が最大じゃなかったのか?」と仰る事でしょう。はい、その通りです。この文字は"3バイト文字+3バイト文字"で表現されているのですから、何も間違ってはいません。

 実はこの文字、「か(U+304B)」と「゙(U+3099)」の2つが組み合わさっています。これが「結合文字列(Combining Characters / Combining Character Sequence)」という奴です。「が」をメモ帳にコピペして、文字の右側にカーソルを置き、バックスペースキーを押すと「か」になります...面白いですね。

 結合文字列はアクセント記号付きの文字を使う国でも使われます。「か(U+304B)」と「゙(U+3099)」の結合文字列の他に「が(U+304C)」という3バイト文字(合成文字)もあります。結合文字列を単一の文字に変換する事を「合成(Composition)」、逆に合成文字(Composite Character)から複数の結合文字へ変換する事を「分解(Decomposition)」と呼びます。Mac OS Xでは結合文字列と合成文字が存在する場合にはデフォルトで結合文字列が使われるようです。

 結合文字列のマズい点は2つあります。一つは"1コードポイント=1文字の前提が崩れる"点で、もう一つは"結合文字列と合成文字が見た目で判断できない"という点です。前者は"Unicodeの仕様としてそうなっている"のでどうしようもありません。後者は、例えばテキスト中に合成文字の「が」と結合文字列の「が」が含まれる場合に、"完全に同じ字形なのに検索にヒットしない"という現象が起きてしまいます。これを回避するには"標準化(正規化)"で対処する必要があります。

Ansi->Unicode->Ansi変換をやっちゃ駄目ってどういう事?
 文字集合としてはShift-JISよりもUnicodeの方が大きいので、問題はないように思えます。ですが、Shift-JIS(CP932)をUnicodeに変換し、再度Shift-JIS(CP932)へ戻すとコードポイントの変更が発生する文字があります。

 Shift-JIS(CP932)には同じ字形の文字が複数登録されており、Unicodeへ変換する時点で一つのコードポイントにまとめられてしまいます。これを再度Shift-JIS(CP932)へ変換すると、元のコードポイントとは異なるコードポイントになってしまいます。以下のコードを見て下さい。

// Delphi2007(またはそれ以前)用 
procedure TForm1.Button1Click(Sender: TObject);
var
  A: String;
  S: String;
  i: Integer;
  function ThroughFunc(S: WideString): WideString;
  begin
    result := S;
  end;
begin
  A := '';
  A := A + #$81#$E6;
  A := A + #$87#$9A;
  A := A + #$FA#$5B;

  // ThroughFuncを通してみる
  A := ThroughFunc(A);

  S := '';
  for i:=1 to Length(A) do
    S := S + Format('#$%.2x',[Ord(A[i])]);
  ShowMessage(Format('%s : %s',[A, S]));
end;

// Delphi2009用
procedure TForm1.Button1Click(Sender: TObject);
type
  SJISString = type AnsiString(932);
var
  A: SJISString;
  S: String;
  i: Integer;
  function ThroughFunc(S: String): String;
  begin
    result := S;
  end;
begin
  A := '';
  A := A + #$81#$E6;
  A := A + #$87#$9A;
  A := A + #$FA#$5B;

  // ThroughFuncを通してみる
  A := ThroughFunc(A);

  S := '';
  for i:=1 to Length(A) do
    S := S + Format('#$%.2x',[Ord(A[i])]);
  ShowMessage(Format('%s : %s',[A, S]));
end;

 期待される結果は

#$81#$E6 #$87#$9A #$FA#$5B

 のハズです。ですが、実際には

#$81#$E6 #$81#$E6 #$81#$E6

 このように、コードポイントがまとめられてしまっています。"波ダッシュ問題"の事もありますし、"AnsiStringは原則AnsiStringのまま扱う"という事に異論はないと思います。 この現象に関する情報を詳しく知りたい方は"Windows-31J の文字セット"を参照して下さい。

 UnicodeからShift-JIS(CP932)への変換だけであれば問題はないのですが、Ansi文字列をUnicode用のAPI/関数へキャストして使ったりするのは極力避けるべきです。「字形が同じならそれでいい」場合とそうでない場合があるからです。

WindowsとUnicode
 Win9xのUnicodeはUnicode1.0...つまりUCS2です。NT系のUnicodeはUTF-16です。APIはW系のものを使う事になります。UTF-16のサロゲートペアはWindows2000から使えます。

Windows2000:
WindowsXP:
Windows Vista:
 なお、Unicodeに対応したフォントで表示可能な文字が一番多いのは、メイリオ...ではなく、"MingLiU"です(文字コード表で確認できます)。メイリオは日本語用のUnicode対応フォントですが、日本語で使われない文字は収録されていないものがあります。VistaとUnicodeに関する資料は"JIS X 0213:2004 / Unicode 実装ガイド"としてまとめられています(日本語:PDF)ので、御一読下さい。

  テキストコントロールに於けるUnicodeの扱いはXPとVistaとで異なっています。XPではサロゲートペアをBackSpaceで削除するとワード単位で削除され、結合文字も濁点のみ選択する事が可能です。VistaではサロゲートペアをBackSpaceで削除すると文字単位で削除され、結合文字で濁点のみの選択はできません。但し、BackSpaceキーで結合文字の「」を「」にする事が可能です。

 テキストコントロールで文字数制限を行う際に指定するのは"文字数"ではなく、"文字構成要素数"になります。最大入力可能サイズを1に設定するとサロゲートペアや結合文字を1文字入力できなくなります。

Microsoft Layer for Unicode とは?
 Microsoft Layer for Unicode (MSLU)とはW系APIを利用したWindowsアプリケーションをWin9xで動作させるためのものです。基本的にC++からの利用が前提で、Delphiでは使えないと思った方がいいです。

Unicode化とWin9x
 DelphiのUnicode化を推し進めるにあたって最大の弊害はWin9xの切り捨てです。

 「Win9xにもW系のAPIがあったハズだろ?Win9x切り捨てるなんてどうにかしている」という声があるかもしれませんが、それはWin9xでW系APIを使った事のない人間の言う事です。Win9xでUnicodeを扱える...つまり、A系とW系のAPI両方が正しく動作するAPIというのはたったこれだけしかありません。

 経験則で言えば、Win9xでのW系APIには3種類あるような気がします。
  1. ちゃんとW系を実装してある。
  2. ANSI変換を伴う(つまり、A系のラッパ)。
  3. エラーになる(つまり、単なるスタブ)。
 この3種類のどれに該当するのかは、実際にすべてのW系APIを調べた訳ではないので不明です(膨大すぎて調べる気にもなりませんが)。そもそも、Win9xのUnicodeはUCS-2なので、BMP以外の文字は使う事ができません。"Microsoft Layer for Unicode(MSLU)"を使うにしたって、上記2.の動作にしかなりません。エラーが出ないだけ多少はマシというだけです。

 Win9xの切り捨てを行えば、QC#58386のような問題は簡単に回避できます。他の多くの文字コードの問題も回避できます。古くからWindowsを触ってきたヒトはWindows98/Me等で「IEの追加サポート」で「~言語」とかいうのをインストールした事があるかもしれません。 またはXP等で「追加の言語」をインストールした事があるかもしれませんね。端的に言えば、あれらは「コードページエンコーダ/デコーダとフォントの詰め合わせ」です。適切に組み込まれていれば、MutiByteToWideChar()/WideCharToMultiByte()APIを使い、Unicode(UTF-16)を介して相互にコンバートする事も可能です(日本語<->Unicode<->日本語すらも...日本語のコードページはCP932だけじゃありませんし)。


 BACK