M5StickCのテキスト表示機能を読み解く(2) ~ 色指定と二派のAPIについて


M5StickCの(というかベースとなっているTFT_eSPIの)テキスト表示機能は謎が多い。M5StickC非公式日本語リファレンスなど親切な情報サイトもあるのだが、私はソースコードを直接見て確認する方が早いし安心できる。ここに調査結果をメモしておくことにする。(→その1)

setTextColorの謎

setTextColorの仕様はわかりにくい。文字色だけを指定する版(A)と、文字色と背景色を指定する版(B)がオーバーロードされているのだが、(A)を呼ぶと背景が透明になるというものだ。
https://github.com/m5stack/M5StickC/blob/8ca47b408f6d58759c7d45aed51a3e12ecf25bd9/src/utility/In_eSPI.h#L723-L724

setTextColor(uint16_t color), // (A)
setTextColor(uint16_t fgcolor, uint16_t bgcolor), // (B)

できるプログラマなら「(A)相当を実現する(B)の呼び出し方があるのが普通ではないか、例えば透明色のコードを背景色に渡すとか」と思うわけだが、ソースコードの該当箇所を見ると意外な実装になっていた。
https://github.com/m5stack/M5StickC/blob/8ca47b408f6d58759c7d45aed51a3e12ecf25bd9/src/utility/In_eSPI.cpp#L2196-L2201

void TFT_eSPI::setTextColor(uint16_t c)
{
  // For 'transparent' background, we'll set the bg
  // to the same as fg instead of using a flag
  textcolor = textbgcolor = c;
}

なんと、「背景色に文字色と同じものを渡すと背景が透明になる」である。まぁ工夫したなとは思うが "This API design smells" である。

さらに、デフォルトが「文字色が白・背景色が黒」であることがわかりにくさを増している。
https://github.com/m5stack/M5StickC/blob/8ca47b408f6d58759c7d45aed51a3e12ecf25bd9/src/utility/In_eSPI.cpp#L209-L210

例えば入門者がカウンタや時計などを作り始めたとする。デフォルト状態だと新しい文字を上書きすれば前の文字は黒で塗りつぶされて正常に見える。その後「じゃあ色を変えてみよう」と setTextColor(TFT_BLUE) などと(常識的に)呼んでみると、急に背景が透明になり、文字がオーバーラップしてぐちゃぐちゃになる。入門者にとってその原因はまったく自明ではない。使用者のエクスペリエンスを考えたら、やはりトリッキーなAPI設計である。

setTextDatumの謎と二派のAPI

文字列の左/右/中央寄せなどのアラインメントを指定できる setTextDatum という関数があるが、これが効く場合と効かない場合がある。まぁ見るからに素性の異なるテキスト表示機能がごちゃっと存在しているのでさもありなんだが、この機会に整理してみた。(ソースの読み間違いがあったらスミマセン。)

ざっくり言うと、下図において緑で囲んだ部分とオレンジで囲んだ部分とが異なるAPI派閥なのだ。一文字を描画する低レベル関数 drawChar は共通だが、その上に趣の異なる二種類のAPIが独立に生えている。

緑の派閥は座標を指定して文字列を描画するスタイルである。この派閥の筆頭者は下記の drawString で、こいつが textdatum を見てアラインメントを意識した上で文字列の描画を行なう。
https://github.com/m5stack/M5StickC/blob/8ca47b408f6d58759c7d45aed51a3e12ecf25bd9/src/utility/In_eSPI.cpp#L4554-L4717

int16_t TFT_eSPI::drawString(const char *string, int32_t poX, int32_t poY, uint8_t font)

drawNumberdrawFloat はこの上に乗っかってフォーマット処理をしているだけである。

一方、オレンジの派閥は setCursor でカーソル座標を指定し、print系の関数でカーソルを進めながらストリーミング描画を行なう。実はこれらのAPIはM5StickCライブラリではなく、ESP32ツールキット側のPrintクラスで定義されている。
https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/Print.h

この中にあるwrite関数が純粋仮想関数(所謂テンプレートメソッド)になっており、すべてのprint系の呼び出しはここに流れ着く。

virtual size_t write(uint8_t) = 0;

そしてwrite関数の実装がライブラリ側で提供されている↓といった塩梅である。つまり実質的にはこいつが本派閥の筆頭者である。
https://github.com/m5stack/M5StickC/blob/8ca47b408f6d58759c7d45aed51a3e12ecf25bd9/src/utility/In_eSPI.cpp#L4106-L4245

図からわかる通り、オレンジの派閥はtextdatumを参照しない。これがsetTextDatumが効かない場合がある所以である(ストリーミングという性質上、上下はともかく少なくとも左右寄せが効かないのは道理と言えば道理である)。また、図からは省略したが、setTextWrap はオレンジ派閥であり、write関数が画面端での折り返しの面倒を見る。緑の派閥には折り返しは実装されていない。

また、文字コードの扱いにも若干の違いが見られた。デフォルトのフォント(setTextFont(1))は文字コード0x80~0xffにもグリフが存在するのだが、オレンジの派閥では正しくUTF-8エンコードされたバイト列を渡さないと文字が表示されない。一方緑の派閥はISO-8859-1的なバイト列をそのまま放り込んでもなんとなく動いてしまう。おかげで私は最初非ASCII文字はdrawString系関数しかサポートしてないと勘違いしてしまった。

このように、独立な二種類のAPI派閥が存在するにもかかわらず、普通の人間には(特に入門者には)数多くのごちゃごちゃしたテキスト描画関数としてしか説明されないので非常にわかりにくい。このような本質的な構造が見抜ければ理解するのは楽になる。

ちなみに歴史的な話をすると、もともとAdafruit GFX Libraryから派生してTFT_eSPIが作られ、それをM5StickCが取り込んでいるのだが、オレンジの派閥はもとのAdafruit GFX Libraryにあったもので、緑の派閥はTFT_eSPIで追加されたものだそう。(lovyan03さん、情報提供ありがとうございました。)

以上です(・∀・)