TextBoxに関する覚書のすべて


自作のソフトでテキストの編集が出来るようにしたかったのだが、普通のテキストエディタのようにundo/redoが出来るようにしたり、範囲選択+tabだとか、自動インデントとか行番号表示だとかそういったものをサポートしようと思うと結構大変だ。


.NET FrameworkのTextBoxはとても非力だ。undo/redoすらサポートしていない。.NETの標準コントロールとして、もっと高機能なテキストボックスがあってもいいんじゃないのと思うのだが。

25年前のテキストエディタ


もうかれこれ25年ぐらい前になるが、当時のパソコンでもテキストエディタをきちんと作るのは大変だった。オールアセンブラで書かなければならないということに加えてターゲットがZ80 4MHzとかなので無駄なメモリコピーなどを限りなく省略しなければならない。おまけにメモリも16KBほどしか使えないのでメモリの無駄遣いもできない。富豪的プログラミングならぬ、大貧民的プログラミングだ。


これについて当時どうやっていたのかについてもう少し突っ込んだ解説をしておかなければならない。

25年前のテキストエディタの設計技法


テキストエディタなので行の挿入・削除などが頻繁に行われるわけだが、行の挿入や削除によって大きなメモリブロックのコピーをするわけにはいかない。ゆえに、各行を双方向linked listで持っておいて削除・挿入時にメモリブロックのコピーは極力発生しないようにする。


まあ、linked listでなくて、各行へのポインタだけ持ってるような実装でもいいのだが、その場合行削除するときにそのポインタ配列の要素のremoveに伴なうメモリブロックのコピーが発生するのでそれはそれでレスポンスの低下につながるのである。


次に、行を削除した場合だが、その削除した行のデータが存在していたメモリブロックを永久に使わないような実装だともったいないのでここを再利用しなければならない。これは、バックグラウンドで少しずつ前方に詰めていく作業を行う。(次図)



ちょっとこの図ではわかりにくいかも知れないがGCは、1行目から順番にlinked listを辿っていく。行の終端は'\0'で終わっていることは保証されているので、この図で4行目まできたときに、4行目の行頭の1番地手前のメモリが'\0'でなければここは削除された行であることがわかるのでそのさらに手前にある'\0'をスキャンして(これは2行目の最後の'\0'で見つかる)、4行目を中身を2行目の末尾の直後に移動させる。


いま風の言葉で言うと、incremental GCである。とても簡素な実装ではあるが。


25年も昔のプログラムですらこれくらいのことは最低限やっていたのだ。

現代のテキストボックスのあるべき姿とは?


こういう大貧民的プログラミングの視点で捉えると、.NET FrameworkのTextBoxは、WindowsのEditBoxの薄いwrapperで、メソッドが十分に用意されておらず(現在のキャレットの存在する行番号すら取得できない)、これでは十分なパフォーマンスが出るとはとても言い難い。


いまの時代、エンドユーザー(.NET Frameworkを使ってプログラムを書く人)は基本的に大富豪的なプログラミングで良いのだが、その制作時に必要となるコンポーネントフレームワークなんかは本当に大貧民的に作られていなければエンドユーザーが大富豪的になれないと思う。


はてブでコメントしていただいた「富豪はあくまで大貧民に支えられていると。」は本当にそうだと思う。マシンはリソースが限られているので、その限られたリソースをひとつのアプリを支えるそれぞれのコンポーネントが奪い合って共存している。ユーザーが幸せになるためにはコンポーネントに泣いてもらうしかない。これは、穀物の生産量が限られている状況下で、誰かが腹いっぱい食べようと思うと他のみんながまともに食にありつけないのにも似ている。

AlpTextBox


最初、AlpTextBoxという.NET Frameworkで使えるテキストボックスのコンポーネントを使ってみた。vectorで検索するとこれしか無かったのだ。


速度はそこそこ速い。WindowsAPIを直接呼び出して行番号を描画してある。


使ってみてわかったことは行番号を表示すると行の1文字目が左2ドットほど欠ける。これが致命的だ。ソースがついてないし、Dotfuscatorで難読化してある。


C#のreflectionを使ってprivateメンバの値を書き換えればこの2ドット欠けている問題自体は回避できることを発見したが、.NET Framework1.1で書かれているし作者はメンテナンスすらしてないと思われるので、このコンポーネントは絶対に使っては駄目だ。本当、時間を無駄にした。AlpTextBox爆発しろ!


AlpTextBoxのダウンロード先にリンクを貼るとSEO的に協力することになって犠牲者が増えるといけないのでリンクは貼らない。

ScintillaNET


ScintillaNET(→ http://scintillanet.codeplex.com/ )は、世界的に使われているScintillaを.NET用にwrapしたものだ。現在の最新のバージョンは2.2。


早速使ってみた。行番号表示、C#シンタックスハイライトともに問題ない。


しかし日本語を入力時に入力中の文字のフォントが小さくなる。またscintilla.Fontを変更しても表示されているフォントが反映されない。scintillaAPIを直接呼び出せば解決するのかも知れないが、そんなことはしたくない。きちんとwrapして欲しい。


あとscintilla.InsertTextを行なう場合、1行前に日本語文字が使ってあると挿入場所がおかしくなる。


たぶん内部的にはutf-16になっておらず、utf-8で扱われているのではないかと思う。文字列の検索・置換などでもこの問題がある。


私は、このコンポーネントは日本語環境で使い物になるとは思えなかった。ひとつかふたつぐらいのバグなら自力で直してパッチをcommitするつもりだったが、内部的にutf-8だったりするようなコンポーネントを修正しててもキリが無い。utf-16で作り直せと言いたくなる。


そんなわけで、私はScintillaNETは現段階で日本語環境で満足に使えるとは思えないのでお薦めしない。バージョンがあがればまたfixされるかも知れないがきちんと.NET FrameworkのTextBoxと互換性があるようにwrapしてくれないと非常に使いにくい。


例えばReadOnlyにするとTextプロパティに代入しても反映されない。ReadOnlyって普通ユーザーからのUIでの操作に対してread onlyにするのであって、誰が本当にTextまでread onlyにしろと…と言った感じで、使っているといろいろおかしいところに気づく。

ICSharpCode.TextEditor


SharpDevelopで内部的に使われているテキストボックス。試してみようかとSharpDevelopのソースをダウンロードしてきたのだが、サンプルがVisual Studioでビルドできない。参照設定とかきちんとすれば出来るんだろうけど、Visual Studioでビルドできないプロジェクトファイルがついてる時点で萎えたので調査終了。


ICSharpCode.TextEditorについて詳しいことはAzukiの作者によるレビュー記事(→ http://sgry.jp/pgarticles/cs_editor_component.html )とかご覧を。

Azuki テキストエディタエンジン


Azuki テキストエディタエンジン
http://azuki.sourceforge.jp/


日本人の書いたテキストエディタエンジン。C#から使える。行番号表示、シンタックスハイライトなどが出来る。現在のバージョンは1.55。



ざっと使ってみた感想だが、まずディフォルトのデザインに私には違和感がある。背景色が薄いピンクになっていて気持ち悪い。白でいいんだよ、白で。色を変更しようとフォームデザイナでBackColorにWhiteを指定したが反映されなかった。コンポーネント初期化のあとに自分でBackColorにWhiteを代入すれば反映された。なんでだろ…。


まあ、配色に関してはテキストボックスの代わりにソフトに埋めて使いたいわけで、自己主張の激しい配色は迷惑なのである。あとTabの記号が存在感を主張しすぎ。こんな横長のTabの記号見たことがない。


しかしよーく見ると、Tabの記号、わざとTab幅にして、スペースと混在させたときにどこまでがTabであるか見てわかるようにしてあるのだと気づく。なるほど。これならTabとスペースを混在させてもTabの直後にスペースがあれば見てわかるわけだ。この作者は頭いいな…。


日本語入力に関しては何の問題もない。シンタックスハイライトは反映されるのがワンテンポ遅い。作者によると1.6で改善するとのことだ。私はソースをビルドしなおしてシンタックスハイライトの反映時間を1/4の時間に変更した。


あと、AppendやらInsertやらDeleteやら普通あるだろ、と思うようなメソッドがまったく用意されていない。あるのは、Replaceだけだ。


しかしこのReplaceがすごく強力で、開始位置と終了位置を指定して文字置換できるので、Insertの代わりにReplace(string)を使ったり、先頭行から100行目まで削除したければこう書くとか?

int end = azukiControl1.Document.GetLineHeadIndex( 100 );
azukiControl1.Document.Replace("",0,end);

上のように行番号からそのポジションを取得できるのでReplaceと組み合わせれば何だって出来るだろうからReplaceだけ用意して、そのReplaceを限りなく高速化しとけばいいじゃんという設計思想なのだと思う。最初戸惑ったが、これはこれでいいや。


あと、CanCopy,CanCut,CanPasteが実装されていない。(CanUndoとCanRedoはある) CanCopy,CanCutは選択範囲があればtrueとして扱えばいいとして、CanPasteは実装しとかなきゃいけないと思う。

結論として


.NET Frameworkの標準のテキストボックスが高機能になるというのは将来的にもあまり期待できない。素直に誰かのコンポーネントを使うのが一番いいと思う。


私が今回調査した範囲においてAzuki以外には行番号が表示とシンタックスハイライトとが出来て、日本語がきちんと扱えるテキストボックスが無かったというのが実情である。Azukiにもいろいろ不満はあるのだが、テキストボックスの自前実装というのは想像する以上に大変なのでやはり誰かが作ったものをそのまま使いたい。


日本語まわりのバグさえなくなればAzukiよりはScintillaNETのほうがプロジェクト母体は大きそうなので良いような気もする。とりあえず現状はここで書いてきた通りだ。