今回は、Visual C# に含まれる RichEditBoxを使って、正規表現で指定したキーワードを引っかけて、
色を付けるという処理をいかに速く行うか?という話。
MarkDown#Editorで、実際にやってみたことを記事にしておきたいと思います。
前提として、僕は、卑下するわけではないですが、あまりたいしたプログラマーとは言えません。
ですので。最初からヒヨっているわけですよ。たとえば、
だから、ありあわせのRichEditBoxを使ってどうにかしたい。超楽したいのです。
メモリとCPUパワーが潤沢に使えれば、それでやってしまおうと。
つまりは、スマートなコーディングより、まあ動けば良い、的な、妥協点の方を見出したくなっちゃう。
それを踏まえた上で、
エディターのシンタックスハイライト処理が激重になってきてしまいました。
従来、シンタックスハイライターのパース処理は、エディター側でテキストの変更がかけられるたびに、
色定義した正規表現のリスト配列の数だけ、処理をブン回すという力技をやっていました。
つまりは、エディタのTextChangedイベントが起きるたびに、処理が走っていたということになります。
当然ですが、これがまた重い。
一文字一文字打つのが、まるで重しを付けられたかのような入力感。
最近、Markdown Extra にも対応して、さらにその正規表現リストが増大したため、そのアプリケーションの重さは耐えがたいものに。
これはなんとかしないといけない。
昔のファミコンじゃないですけど、再描画必要なキャラだけ処理を行うみたいに、
エディターで現在編集中のパラグラフを見て、そこだけ反映するとか。
でもこれが、けっこう難しい。Markdown Extraでは空行を含む範囲で変換する項目もあり、
どこからどこまでを、適切に、シンタックスハイライトするかという判定がとても面倒です。
試しに、ver.1.2.0.0に入れてみたのですが、超不安定なアプリケーションに変貌。。。
使用された方へ。本当に申し訳ありませんでした。。。
で、けっきょく設計を見直した末に辿りついた結論は、BackgroundWorker(別スレッド)を使って、
エディター内の文字列をパースし、シンタックスハイライターの該当箇所をあらかじめ取得して、リスト化しておくこと。
こうすることで、少しは軽くならないかな、と考えました。
[csharp]
private void richTextBox1_TextChanged(object sender, EventArgs e)
{
if (backgroundWorker2.IsBusy == false)
{
//バックグラウンドワーカーへパースを投げる
backgroundWorker2.RunWorkerAsync(richTextBox1.Text);
}
}
[/csharp]
バックグラウンドワーカー側では、
SyntaxColorSchemeという独自クラス(該当文字列の位置と、長さ、前景色、背景色のデータを格納する構造体)をつくって、
引っかかった結果を配列に突っ込んでいきます。
そして、バックグラウンドワーカーが仕事を返してくるのを待ちます。
[csharp]
private void backgroundWorker2_DoWork(object sender, DoWorkEventArgs e)
{ //動作設定オブジェクトのインスタンス
var obj = MarkDownSharpEditor.AppSettings.Instance;
RichTextBoxEx richTextBoxBackground = new RichTextBoxEx();
richTextBoxBackground.Clear();
richTextBoxBackground.Text = (string)e.Argument;
//エディターの前景色(文字色)
richTextBoxBackground.ForeColor = Color.FromArgb(obj.ForeColor_MainText);
//エディターの背景色
richTextBoxBackground.BackColor = Color.FromArgb(obj.BackColor_MainText);
_SyntaxArrayList.Clear();
foreach (MarkdownSyntaxKeyword mk in _MarkdownSyntaxKeywordAarray)
{
Regex r = new Regex(mk.RegText, RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.Compiled);
MatchCollection col = r.Matches(richTextBoxBackground.Text, 0);
if (col.Count > 0)
{
foreach (Match m in col)
{
SyntaxColorScheme sytx = new SyntaxColorScheme();
sytx.SelectionStartIndex = m.Groups[0].Index;
sytx.SelectionLength = m.Groups[0].Length;
sytx.ForeColor = mk.ForeColor;
sytx.BackColor = mk.BackColor;
_SyntaxArrayList.Add(sytx);
}
}
}
e.Result = richTextBoxBackground.Rtf;
}
[/csharp]
結果が返ってきたところで、RichTextBoxに反映します。
[csharp]
private void backgroundWorker2_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Font fc = richTextBox1.Font; //現在のフォント設定
bool fModify = richTextBox1.Modified; //現在の編集状況
//現在のカーソル位置
int selectStart = this.richTextBox1.SelectionStart;
int selectEnd = richTextBox1.SelectionLength;
Point CurrentOffset = richTextBox1.AutoScrollOffset;
int CurrentScrollPos = richTextBox1.VerticalPosition; //現在のスクロール位置
richTextBox1.BeginUpdate(); //描画停止
//RichTextBoxの書式をクリア
richTextBox1.ForeColor = Color.FromArgb(obj.ForeColor_MainText);
richTextBox1.BackColor = Color.FromArgb(obj.BackColor_MainText);
//裏でパースしていたシンタックスハイライトの結果を反映
for (i = 0; i < _SyntaxArrayList.Count; i++)
{
SyntaxColorScheme s = (SyntaxColorScheme)_SyntaxArrayList[i];
richTextBox1.Select(s.SelectionStartIndex, s.SelectionLength);
richTextBox1.SelectionColor = s.ForeColor;
richTextBox1.SelectionBackColor = s.BackColor;
}
//カーソル位置を戻す
richTextBox1.Select(selectStart, selectEnd);
richTextBox1.AutoScrollOffset = CurrentOffset;
richTextBox1.EndUpdate(); //描画再開
richTextBox1.Modified = fModify;
}
[/csharp]
この方法だと、常にパース処理がバックグラウンドで動いていることにはなりますが、
エディター側の動きに直接影響を与えることはなく、わりとキビキビとした動作へと改善しました。
これらのソースコードは、すべてGitHubに上がっています。興味のある方は、そちらからどうぞ。
まあ、ただ、こういうやり方はスマートではなく、ハードウェア性能頼り的で、あんまり参考にならないかもしれませんね。
手っ取り早く、スピードを上げたいという場合には、参考にどうぞ、という感じです。
さらに、踏み込んでいくならば、該当箇所のリストから、現在編集中の前後を絞って反映すれば、さらに高速化が図れるかもしれません。
これはMarkDown#Editorの今後の課題として、バージョンアップ予定の項目に入れておきます。
このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。
日々の開発作業で気づいたこと共有を。同じところで躓いている人が、 検索で辿り着けたら良いな、というスタンスで記事を書くので不定期更新になります。
コメントする