6

これは、
WinForms RichTextBoxのフォローアップです。TextChangedでフォーマットを実行する方法は?

RichTextBoxを備えたWinformsアプリがあります。このアプリは、そのボックスのコンテンツを自動ハイライトします。大きなドキュメントではフォーマットに10秒以上かかることがあるため、RichTextBoxの再フォーマットを行うBackgroundWorkerを設定しました。テキストをウォークスルーし、次の一連の処理を実行します。

rtb.Select(start, length);
rtb.SelectionColor = color;

これを実行している間、UIは応答性を維持します。

BackgroundWorkerは、TextChangedイベントからキックオフされます。このような:

private ManualResetEvent wantFormat = new ManualResetEvent(false);
private void richTextBox1_TextChanged(object sender, EventArgs e)
{
    xpathDoc = null;
    nav = null;
    _lastChangeInText = System.DateTime.Now;
    if (this.richTextBox1.Text.Length == 0) return;
    wantFormat.Set();
}

バックグラウンドワーカーメソッドは次のようになります。

private void DoBackgroundColorizing(object sender, DoWorkEventArgs e)
{
    do
    {
        wantFormat.WaitOne();
        wantFormat.Reset();

        while (moreToRead())
        {
            rtb.Invoke(new Action<int,int,Color>(this.SetTextColor,
                      new object[] { start, length, color} ) ;
        }                

    } while (true);
}

private void SetTextColor(int start, int length, System.Drawing.Color color)
{
   rtb.Select(start, length);
   rtb.SelectionColor= color;
}

ただし、SelectionColorに割り当てるたびに、TextChangedイベントが発生します。無限ループです。

外部で発生したテキストの変更と、フォーマットを実行しているBackgroundWorkerで発生したテキストの変更を区別するにはどうすればよいですか?

テキスト形式の変更とは関係なく、テキストコンテンツの変更を検出できれば、これを解決することもできます。

4

2 に答える 2

6

私が採用したアプローチは、BackgroundWorkerでフォーマッターロジックを実行することでした。これを選択したのは、フォーマットに1〜2秒以上の「長い」時間がかかるため、UIスレッドでは実行できなかったためです。

問題を言い換えると、BackgroundWorkerがRichTextBox.SelectionColorのセッターを呼び出すたびに、TextChangedイベントが再度発生し、BGスレッドが最初からやり直されます。TextChangedイベント内で、「ユーザーが何かを入力しました」イベントと「プログラムがテキストをフォーマットしました」イベントを区別する方法が見つかりませんでした。ですから、それは変化の無限の進行であることがわかります。

単純なアプローチは機能しません

一般的なアプローチ(Ericが提案)は、テキスト変更ハンドラー内で実行しているときに、テキスト変更イベントの処理を「無効」にすることです。しかしもちろん、これは私の場合は機能しません。テキストの変更(SelectionColorの変更)がバックグラウンドスレッドによって生成されているためです。これらは、テキスト変更ハンドラーのスコープ内では実行されていません。したがって、バックグラウンドスレッドが変更を行っている私の場合、ユーザーが開始したイベントをフィルタリングするための単純なアプローチは機能しません。

ユーザーが開始した変更を検出するその他の試み

フォーマッタースレッドに起因するリッチテキストボックスの変更と、ユーザーが行ったリッチテキストボックスの変更を区別する方法として、RichTextBox.Text.Lengthを使用してみました。長さが変更されていない場合、私は推論しました。変更は、ユーザーによる編集ではなく、コードによって行われた形式の変更でした。ただし、RichTextBox.Textプロパティの取得にはコストがかかり、TextChangeイベントごとに取得すると、UI全体が許容できないほど遅くなります。これが十分に高速であったとしても、ユーザーがフォーマットを変更するため、一般的なケースでは機能しません。また、タイプオーバーのような操作の場合、ユーザーが編集すると同じ長さのテキストが生成される可能性があります。

私は、TextChangeイベントをキャッチして処理し、ユーザーからの変更を検出することだけを望んでいました。それができなかったので、KeyPressイベントとPasteイベントを使用するようにアプリを変更しました。その結果、フォーマットの変更(RichTextBox.SelectionColor = Color.Blueなど)による誤ったTextChangeイベントが発生しなくなりました。

ワーカースレッドにその作業を行うように信号を送る

OK、フォーマットの変更を行うことができるスレッドを実行しています。概念的には、これを行います。

while (forever)
    wait for the signal to start formatting
    for each line in the richtextbox 
        format it
    next
next

BGスレッドにフォーマットを開始するように指示するにはどうすればよいですか?

ManualResetEventを使用しました。KeyPressが検出されると、keypressハンドラーがそのイベントを設定します(オンにします)。バックグラウンドワーカーが同じイベントを待っています。オンにすると、BGスレッドはオフになり、フォーマットを開始します。

しかし、BGワーカーがすでにフォーマットしている場合はどうなりますか?その場合、新しいキーを押すとテキストボックスの内容が変更され、これまでに行われたフォーマットが無効になる可能性があるため、フォーマットを再開する必要があります。フォーマッタースレッドに本当に必要なのは、次のようなものです。

while (forever)
    wait for the signal to start formatting
    for each line in the richtextbox 
        format it
        check if we should stop and restart formatting
    next
next

このロジックでは、ManualResetEventが設定(オン)されると、フォーマッタースレッドがそれを検出してリセット(オフ)し、フォーマットを開始します。テキストをウォークスルーし、フォーマット方法を決定します。フォーマッタスレッドは定期的にManualResetEventを再度チェックします。フォーマット中に別のキー押下イベントが発生すると、イベントは再びシグナル状態になります。フォーマッターが再シグナリングされたことを確認すると、フォーマッターはベイルアウトし、Sisyphusのようにテキストの先頭からフォーマットを再開します。よりインテリジェントなメカニズムは、変更が発生したドキュメント内のポイントからフォーマットを再開します。

遅発性筋肉痛

もう1つの工夫:フォーマッターがすべてのKeyPressですぐにフォーマット作業を開始することを望んでいません。人間のタイプとして、キーストローク間の通常の一時停止は600〜700ミリ秒未満です。フォーマッタが遅滞なくフォーマットを開始すると、キーストローク間でフォーマットを開始しようとします。かなり無意味です。

したがって、フォーマッタロジックは、600ミリ秒を超えるキーストロークの一時停止を検出した場合にのみ、フォーマット作業を開始します。信号を受信した後、600ms待機します。キーが押されていない場合は、入力が停止し、フォーマットが開始されます。間に変更があった場合、フォーマッターは何もせず、ユーザーはまだ入力中であると結論付けます。コード内:

private System.Threading.ManualResetEvent wantFormat = new System.Threading.ManualResetEvent(false);

キープレスイベント:

private void richTextBox1_KeyPress(object sender, KeyPressEventArgs e)
{
    _lastRtbKeyPress = System.DateTime.Now;
    wantFormat.Set();
}

バックグラウンドスレッドで実行されるcolorizerメソッドでは、次のようになります。

....
do
{
    try
    {
        wantFormat.WaitOne();
        wantFormat.Reset();

        // We want a re-format, but let's make sure 
        // the user is no longer typing...
        if (_lastRtbKeyPress != _originDateTime)
        {
            System.Threading.Thread.Sleep(DELAY_IN_MILLISECONDS);
            System.DateTime now = System.DateTime.Now;
            var _delta = now - _lastRtbKeyPress;
            if (_delta < new System.TimeSpan(0, 0, 0, 0, DELAY_IN_MILLISECONDS))
                continue;
        }

        ...analyze document and apply updates...

        // during analysis, periodically check for new keypress events:
        if (wantFormat.WaitOne(0, false))
            break;

ユーザーエクスペリエンスでは、入力中に書式設定は行われません。入力が一時停止すると、フォーマットが開始されます。タイピングが再開されると、フォーマットは停止し、再び待機します。

フォーマット変更中のスクロールの無効化

最後の問題が1つありました。RichTextBoxでテキストをフォーマットするには、 RichTextBox.Select()を呼び出す必要があります。これにより、 RichTextBoxにフォーカスがある場合、RichTextBoxは選択したテキストまで自動的にスクロールします。ユーザーがコントロール、テキストの読み取り、編集に集中すると同時にフォーマットが行われるため、スクロールを抑制する方法が必要でした。RTBのパブリックインターフェイスを使用してスクロールを防ぐ方法を見つけることができませんでしたが、インターチューブで多くの人がそれについて質問していました。いくつかの実験の結果、Win32 SendMessage()呼び出し(user32.dllから)を使用して、Select()の前後にWM_SETREDRAWを送信すると、Select()を呼び出すときにRichTextBoxのスクロールを防ぐことができることがわかりました。

スクロールを防ぐためにpinvokeを使用していたため、SendMessageでpinvokeを使用して、テキストボックス(EM_GETSELまたはEM_SETSEL )で選択範囲またはキャレットを取得または設定し、選択範囲( EM_SETCHARFORMAT )でフォーマットを設定しました。pinvokeアプローチは、マネージドインターフェイスを使用するよりもわずかに高速になりました。

応答性のためのバッチ更新

また、スクロールを防ぐと計算のオーバーヘッドが発生するため、ドキュメントに加えられた変更をまとめてまとめることにしました。連続する1つのセクションまたは単語を強調表示する代わりに、ロジックは強調表示またはフォーマット変更のリストを保持します。時々、ドキュメントに一度に30の変更が適用されます。次に、リストをクリアし、どのフォーマット変更を行う必要があるかを分析してキューに戻します。これらの変更のバッチを適用するときに、ドキュメントの入力が中断されないほど高速です。

結果として、タイピングが行われていないときに、ドキュメントが自動フォーマットされ、個別のチャンクに色付けされます。ユーザーがキーを押すまでに十分な時間が経過すると、ドキュメント全体が最終的にフォーマットされます。これは、1k XMLドキュメントの場合は200ミリ秒未満、30kドキュメントの場合は2秒未満、100kドキュメントの場合は10秒未満です。ユーザーがドキュメントを編集すると、進行中のフォーマットはすべて中止され、フォーマットが最初からやり直されます。


ふぅ!

ユーザーが入力するリッチテキストボックスをフォーマットするのと同じくらい単純に見えるものが非常に複雑であることに私は驚いています。しかし、テキストボックスをロックしない、それでも奇妙なスクロール動作を回避する、より単純なものを思い付くことができませんでした。


上で説明したもののコードを表示できます。

于 2009-09-22T21:10:56.627 に答える
3

通常、同じイベントが再度発生する可能性のある方法でイベントハンドラーに反応する場合は、イベントハンドラーを既に処理していることを示すフラグを設定し、イベントハンドラーの上部にあるフラグを確認して、すぐに戻ります。フラグが設定されている場合:

bool processing = false;

TextChanged(EventArgs e)
{
    if (processing) return;

    try
    {
        processing = true;
        // You probably need to lock the control here briefly in case the user makes a change
        // Do your processing
    }
    finally
    {
        processing = false;
    }
}

処理の実行中にコントロールをロックすることが受け入れられない場合は、コントロールのKeyDownイベントをチェックし、受信時に処理フラグをクリアできます(おそらく、現在のTextChanged処理が長くなる可能性がある場合は終了します)。

編集:

完全で機能するコード

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.ComponentModel;

namespace BgWorkerDemo
{
    public class FormatRichTextBox : RichTextBox
    {
        private bool processing = false;

        private BackgroundWorker worker = new BackgroundWorker();

        public FormatRichTextBox()
        {
            worker.DoWork += new DoWorkEventHandler(worker_DoWork);
        }

        delegate void SetTextCallback(string text);
        private void SetText(string text)
        {
            Text = text;
        }

        delegate string GetTextCallback();
        private string GetText()
        {
            return Text;
        }

        void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                GetTextCallback gtc = new GetTextCallback(GetText);
                string text = (string)this.Invoke(gtc, null);

                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < text.Length; i++)
                {
                    sb.Append(Char.ToUpper(text[i]));
                }

                SetTextCallback stc = new SetTextCallback(SetText);
                this.Invoke(stc, new object[]{ sb.ToString() });
            }
            finally
            {
                processing = false;
            }
        }

        protected override void OnTextChanged(EventArgs e)
        {
            base.OnTextChanged(e);

            if (processing) return;

            if (!worker.IsBusy)
            {
                processing = true;
                worker.RunWorkerAsync();
            }
        }

        protected override void OnKeyDown(KeyEventArgs e)
        {
            if (processing)
            {
                BeginInvoke(new MethodInvoker(delegate { this.OnKeyDown(e); }));
                return;
            }

            base.OnKeyDown(e);
        }

    }
}
于 2009-09-21T23:39:48.670 に答える