C# から使えるテキストエディタ部品

はじめに

この記事は、 私が個人的に開発しているテキストエディタ用に C# から使えるテキストエディタ GUI 部品を選定した際の記録を簡単にまとめたものです。

2009年8月1日追記。
結局、テキストエディタエンジンを自作しました。 まだまだ発展途上ですが、 SourceForge.JP でオープンソースプロジェクトをホストして開発しています。 ご興味があれば、ご参照ください。
Azuki - A Text Editor Engine for C#

背景

テキストエディタは極めて使用頻度の高いツールであるため好みが十人十色であり、 求められる機能はユーザにより異なります。 機能の量も通常の部品とは比較にならないほど多いため、 部品として利用できるようコンパクトにまとまった物がほとんどありません。 つまり、もしソースコードがすべて公開されているテキストエディタがあったとしても、 そのコア部分だけを取り出して他プロジェクトにそのまま流用するのは難しい場合がほとんどです。 部品として利用する事を強く意識しながらメンテナンスされていなければ、 エディタ ソフトウェアの最大のユーザが作者自身という事もあり、 度重なる機能追加により部品としての可搬性が失われやすいのでしょう。

さて、今回は私が個人的に開発しているテキストエディタ用に部品を選定したのですが、 それにあたって次のポイントを重要視しました。

以上のポイントを押さえた上で Web 上で探したところ、4 つの候補が見つかりました。 次にその候補を記します。

  1. Scintilla

    C++ で開発されている、世界的に有名なテキストエディタ部品です。 Windows 用 SubVersion クライアントの TortoiseSVN などでの採用実績があります。 相互運用性という観点からも非常に良くできており、 外部操作は Win32API のみで行えるよう設計されていますので C++ への言語依存もありません。 内部データ形式には UTF-8 が選択可能で、 日本語の入力や表示も問題ありません。 単語補完やコードの折り畳み機能、ブックマーク機能もあります。 ライセンスは非常に緩く、MIT など FreeBSD 系のものに近いライセンスになっています。

  2. ICSharpCode.TextEditor

    オープンソースな C# 用の統合開発環境 SharpDevelop のテキストエディタ部品です。 これは純粋な C# のプログラムで、 内部的には複数の箇所を選択できる(ようにソースは読めた) など非常に高機能です。 部品単体では配布されていませんが、 SharpDevelop そのもののライセンスが LGPL なのでエディタ部品だけ取り出して改造できます。 内部データ形式は UTF-16 (もしかするとフラットな UCS2?)であり、 日本語の入力も表示も問題ありません。 単語補完やコードの折り畳み機能、ブックマーク機能もあります。

  3. SynEdit (UniSynEdit の方)

    海外で人気の高い Delphi 用 VCL コンポーネントです。 私が日本語訳を行った PSPadConTEXT など、著名なプログラミング用テキストエディタでの採用実績があります。 VCL という規格はよく知らないのですが、 Win32API 的に操作できるらしいので C# からも利用可能だと思います。

  4. TEditor

    和製エディタコンポーネントです。 シェアウェアの Delphi 用 VCL コンポーネントであり、 K2Editor など非常に多くのエディタで利用されている実績があります。 和製ですので日本語の利用もまったく問題ありません。

Scintilla

第一候補の Scintilla を採用する場合の問題は、3 点です。

一つ目は例によって海外エディタは日本語の単語をうまく扱えないため、 単語検出ロジックを自前実装しなければ使いにくくて困る事です。 たとえば日本語だけの文章上で「次の単語へ移動」すると、 行末までカーソルが飛んでいってしまいます。 とはいえ、これは自前で単語処理を実装すれば良いだけの話なので大きな問題ではありません。

二つ目はテキストデータの内部表現が純粋なバイトストリームとなっている Scintilla では内部表現を UTF-16 にできず、 UTF-16 ベースで文字列を扱う C# との相性が悪い事です。 これが意外に厄介です。 まず UTF-16 の C# から利用する事を考えると Scintilla の内部エンコーディングは文字情報が欠損しないよう UTF-8 に決まりです。 しかし Scintilla の内部表現はあくまでバイトストリームであるため、 多バイト長の UTF-8 が格納されている場合、 Scintilla 内のインデックスはバイトインデックスであり文字インデックスではありません。 例として 「文字列を文字インデックス N の位置に挿入する」 という処理を考えましょう。 文字列挿入のインタフェースは、 C# プログラムから使いますから文字インデックスで指定するのが自然です。 しかし Scintilla の API に 「インデックス N」と直接指定するとバイトインデックスとして解釈されてしまうので、 格納されているテキストを一度 UTF-16 にデコードして N 文字目が何バイト目に相当するか計算しなくてはなりません。 これはかなり非効率に思えます。 一方、逆に C# 側のインタフェースをバイトインデックスにすれば再計算の量は減ると思いますが、 かなり扱いにくくなる事は間違いありません。

三つ目の問題点は .NET の Form 上にネイティブなウィンドウを配置する場合の問題です。 まず Win32API の世界におけるイベントは親ウィンドウへ投げられた通知のメッセージを処理する事で扱います。 .NET でネイティブなウィンドウを扱う場合、 親ウィンドウは .NET のウィンドウですから、 親ウィンドウの WndProc メソッドをオーバーライドしてその通知を解釈すればイベントは扱えます。 しかしこれは非常に .NET 的ではありません。 たとえば .NET において GUI 部品のイベントや操作はその部品クラスのオブジェクトだけで完結しますが、 通知の処理を考えるとネイティブなウィンドウをそのまま置くだけではこのような形にできません。 また Dock や Anchor など GUI デザイン上重要なプロパティも無視されるため、 あらゆる面で非常に扱いにくくなります。 そこで、子ウィンドウとしてネイティブなウィンドウを持つだけの Control 派生クラスを作り、 このクラスをそのネイティブなウィンドウと同等なものとして扱うようにします。 まずこのクラス自身は .NET のコントロールですから Dock も Anchor も受け付けます。 したがってこのクラスにサイズ変更などが起こるたびに子ウィンドウへサイズ変更命令をそのまま送信すれば、 子ウィンドウにも Dock や Anchor のプロパティが反映されるようになります。 そして WndProc をカスタムする事でネイティブウィンドウの通知を補足し、 独自定義の .NET イベントを適宜発行すれば .NET 的に使いやすくなります。 後はキーボードイベントを Form. ProcessDialogChar や Form. IsInputKey 等をオーバーライドする事で適切に処理すれば、 純粋な .NET のコントロールとまったく同じ使用感でそのネイティブウィンドウを操作できます。 ちなみにキーボードイベントの処理をオーバーライドしないと、 そのウィンドウにフォーカスを移動できなかったり、 同一ウィンドウ内のボタン等に設定されたニーモニックと同じキーを押すとそちらにフォーカスが奪われてしまう等の問題が起こります。 この問題を解決するのは大変ですが、不可能ではありません。

ICSharpCode.TextEditor

第二候補の ICSharpCode.TextEditor を採用する場合の問題は二点です。 一点目は Scintilla の場合と同様に日本語の単語検出ロジックを自前実装しないと、という話です。 二点目は非常に高機能であるためか部品としての機能を一手に公開するインタフェースが無い事です。 これは、要するに外部操作を一つのクラスで担当していないため、 機能ごとに異なるメンバオブジェクトへアクセスする必要があるという事です。 エディタの内部構造をかなり把握しないと目的の機能がどこで実装されているのか分からず、 とっつきにくく、そして操作が面倒というのが問題です。 たとえば textEditorControl という変数名で Form に配置した場合、現在選択しているテキストを取得するには次のように長い文を書く必要があります。

textEditorControl.ActiveTextAreaControl.TextArea.SelectionManager.SelectedText

あるいは現在入力されているテキストの長さを取得するには次のようになります。

textEditorControl.Document.TextBufferStrategy.Length

テキストの選択は本質的に編集対象の属性ではなく表示や一時的な状態であるため、 Document 側ではなく GUI 側の情報ですから TextArea オブジェクトにアクセスします。 入力されているテキストの長さは採用している内部構造によって計算方法が変わるため TextBufferStrategy、実際には GapTextBufferStrategy オブジェクトにアクセスします。 分かってしまえば何という事でもありませんが、 クラス階層のどこに何があるのかすら最初は分からないため苦労しました。

(しかしこの部品、カーソルの挙動が少し気持ち悪い。個人的には修正して使いたい。)

SynEdit

私にとって SynEdit を採用する場合の問題は、プログラミング言語の壁です。 まず私は Pascal の開発環境を持っていません。 Delphi を買わないといけないのかもしれませんし、 買わずに開発する方法があるかもしれない。 また VCL をどう Win32API 的に操作するのかも調べる必要があります。 これらを調べたところでその労力に見合うとは思えないため、 この時点でほぼ諦めが入っていました。

TEditor

基本的には SynEdit 同様、 プログラミング言語の壁があるため採用する気は最初からほとんどありません。 どうやらソースコードごと配布可能らしいですが、 そのソースコードを使って第三者が開発を行う場合は当然シェアウェア料金を払う必要があります。