中国語簡体字を中国語繁体字に変換するユーザー定義関数を作ってみた

中国本土の字って、さっぱり読めません。
たとえば、「请给我电话」。日本人的には、「清給我申活」に見えます。さっぱり意味がわかりません。
でも、これが実は「請給我電話」の字体違いだとわかると、もう少し何か意味がみえてくる気がします。

ま、最初から翻訳サイトにかけちゃえば一発なんですが、せめて、字の変換だけでも簡単にできないかなと、ExcelVBAでワークシート関数を作ってみたいと思います。

ちなみに、この投稿で訴えたい技術面でのポイントは、以下のことです。

1. VBAでワークシートで使える関数が作れる。
1. VBAでWindowsAPIを使える。
1. VBAでもUnicodeを使える。

まず、1点目、VBAでワークシートで使える関数が作れる点。
標準モジュールに、Public Functionを作ると、そのFunctionは、ワークシートから実行できるようになります。
まずは、最初の一歩として、与えられた引数をそのまま返す関数として、作ってみます。
Visual Basic Editorのプロジェクトツリーで　右クリック→「挿入」→「標準モジュール」として、標準モジュールを追加します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215956/1a53d9d2-239c-5875-b013-6545a82b5b29.png)

そして、この追加してできた 標準モジュール Module1 に以下のソースを記載します。

```vbnet
Public Function ToTraditional(ByVal s As String)
    ToTraditional = s
End Function
```

では、この関数がワークシート関数として動作することを確認してみます。
準備として、A1セルに適当な文字列を入れておきます。
「数式」→「関数の挿入」として表示される「関数の挿入」画面で「関数の分類」を「ユーザー定義」に切り替えると、見事に先に作ったToTraditional関数ができています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215956/3c900e28-cf14-6e38-eee9-385032b3edd8.png)


ToTraditional関数を選択し、「OK」を押します。
「関数の引数」画面がでてきました。引数名が ソースコードのまま、「S」とでてて、ダサいですが、いったんはこれで良しとします。　Sの値として、A1セルを参照して、「OK」を押してみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215956/18cd0896-2954-acbf-e50a-c7c2c6c1adaa.png)
意図したとおり、引数がそのまま結果となるワークシート関数ができました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215956/80418844-39d1-0d96-faaa-dd0a8b5d83e7.png)

では、ここから実際に簡体字を繁体字（台湾・香港などの字体。おおよそ日本の戦前の旧字体に類似）に変換する関数を作っていきます。
Windowsは簡体字と繁体字の変換機能をもっています。
残念ながら簡体字と日本語字体との変換機能はもっていません。それでも繁体字の方が簡体字よりは、まだわかりやすいので、繁体字への変換で我慢することにします。
WindowsがOSとして提供する機能をVBAからも利用することができます。こうした行為は一般的にAPI呼び出しとよばれたりします。

ちなみに、API呼び出しができるのが、私がExcelVBAが好きな理由だったりします。会社に存在するほとんどすべてのPCでAPI呼び出しを使ったプログラムを作ることができるという言語はExcelVBAをおいてほかにありません。他の言語は開発言語のインストールが必要です。VBAならExcelが入っていればいいので、多くのPCで追加インストールの必要がありません。

ではこの変換機能の呼び出しを実装していきます。
Windowsのこの変換機能は LCMapString APIによって呼び出すことができます。
LCMapStringでググるとでてくるMicrosoft社の[解説ページ](https://msdn.microsoft.com/ja-jp/library/cc448052.aspx)によると、LCMapStringは次のように説明されています。
```
int LCMapString(
  LCID Locale,       // ロケール識別子
  DWORD dwMapFlags,  // マップ変換の種類
  LPCTSTR lpSrcStr,  // マップ元文字列のアドレス
  int cchSrc,        // マップ元文字列の文字数
  LPTSTR lpDestStr,  // マップ先バッファのアドレス
  int cchDest        // マップ先バッファのサイズ
);
```
VBAでこれを使うには、declare文というものを書く必要があります。
declare文は上のAPI解説の内容をVBAに翻訳して、VBAに伝えるものとなります。

MicrosoftのAPIの解説は、以下の構造を持っています。
```
戻り値のC言語での型  API関数名(
 引数1のC言語での型 引数1の名前のサンプル, // 引数1の意味
 引数2のC言語での型 引数2の名前のサンプル, // 引数2の意味
 .... 
);
```
ざっくりいうと、引数の型名がLPで始まるものは VBAでいうところのLongPtr型、そうでないものはたいていVBAでいうところのLong型です。
ほんとは、そんなに簡単ではなくて、山のような対応規則がありますが、その説明はまた別の機会に譲りたいと思います。

説明を一気にはしょると、上のAPIの解説を Declare文にすると、こうなります。
```
Declare PtrSafe Function LCMapStringW Lib "kernel32" _
(ByVal Locale As Long, ByVal dwMapFlags As Long, _
 ByVal lpSrcStr As LongPtr, ByVal cchSrc As Long, _
 ByVal lpDesStr As LongPtr, ByVal cchDest As Long) As Long
```
LCMapStringWはLCMapStringに由来しますが、これは上記の解説ページに「Unicode：Windows NT/2000 は Unicode 版と ANSI 版を実装」とあることに起因します。
この記述があるAPIは Wで終わるUnicode版と Aで終わるANSI版という2バージョンがあります。中国語など日本語文字以外を扱う場合はUnicode版を使う必要があり、API名の後ろにWをつけます。
そして、「Lib "kernel32"」は、これまた上記の解説ページの
「インポートライブラリ：kernel32.lib を使用」に起因します。
どのdllファイルにそのAPIを存在するのかをExcelに伝えるために、declare文では利用API名のあとに、「lib [dllファイル名]」　と記載しますが、この部分にインポートライブラリの.libを取り払った文字列を記載します。

そして、このLCMapString APIは上記の解説ページによると、dwMapFlagsの使い方で、変換処理の内容が決まります。

```
LCMAP_SIMPLIFIED_CHINESE	中国語の簡体字を繁体字にマップします。
LCMAP_TRADITIONAL_CHINESE	中国語の繁体字を簡体字にマップします。
```

となってますが、たぶん誤訳で、本当は

```
LCMAP_SIMPLIFIED_CHINESE	中国語の簡体字に繁体字をマップします。
LCMAP_TRADITIONAL_CHINESE	中国語の繁体字に簡体字をマップします。
```

が正解で、我々が使うべきは、LCMAP_TRADITIONAL_CHINESE です。
このLCMAP_TRADITIONAL_CHINESEがなにかいうと別途定義されている定数なのですが、解説ページには定義内容がないので、こういうときは
「const [定数名]」でググります。
「const LCMAP_TRADITIONAL_CHINESE」でググると、0x04000000 という値がみつかると思います。 0x は VBAでの &h に相当するC言語の表現なので &H4000000 と考えます。

そして、また、いっきに説明をはしょると、できあがりのソースはこうなります。
先ほど、標準モジュール Module1 に書いたダミーのソースは一度全部消して、以下のソースを貼り付けてみてください。

```

Declare PtrSafe Function LCMapStringW Lib "kernel32" _
(ByVal Locale As Long, ByVal dwMapFlags As Long, _
 ByVal lpSrcStr As LongPtr, ByVal cchSrc As Long, _
 ByVal lpDesStr As LongPtr, ByVal cchDest As Long) As Long

Const LCMAP_TRADITIONAL_CHINESE = &H4000000
 
Public Function ToTraditional(ByVal Simplified As String)
    Dim nLengthToNeed As Long
    
    'まずは変換結果の格納に必要な文字数をLCMapStringWで調べる
    nLengthToNeed = LCMapStringW(0, LCMAP_TRADITIONAL_CHINESE, _
                    StrPtr(Simplified), Len(Simplified), _
                    0, 0)
    
    Dim sTemp As String
    '必要な文字数分のメモリを変数に確保する
    sTemp = String(nLengthToNeed, vbNullChar)
    
    '確保した変数sTempの内容を変換結果で上書きするようにLCMapStringWに指示
    LCMapStringW 0, LCMAP_TRADITIONAL_CHINESE, _
                 StrPtr(Simplified), Len(Simplified), _
                 StrPtr(sTemp), nLengthToNeed
    
    'sTempの内容を関数の戻り値とする
    ToTraditional = sTemp
    
End Function
```
ここでのポイントは、上記解説ページで「～のアドレス」と書いてある引数には 文字列変数の場合、StrPtrを使って、変数に紐づく文字列バッファのアドレスを渡すということです。(これは Unicode版を使うときに限定される話です。)

では、実行してみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215956/4d5b5781-3c5d-798c-e385-454963697522.png)

みごと、请给我电话　が　請給我電話　に変換できました。

どうでしょうか？ この投稿でVBAからのAPI利用に興味を持たれて、始められる方がいたら、うれしいです。