WinForms RichTextBox: как переформатировать асинхронно, без запуска события TextChanged

Это продолжение
WinForms RichTextBox: как выполнить форматирование на TextChanged?

у меня есть приложение Winforms с RichTextBox, приложение автоматически выделяет содержимое указанного окна. Поскольку форматирование может занять много времени для большого документа, 10 секунд или более, я настроил BackgroundWorker для повторного форматирования RichTextBox. Он проходит по тексту и выполняет ряд следующих действий:

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

пока он делает это, пользовательский интерфейс остается отзывчивым.

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 делать форматирование?

Я также мог бы решить эту проблему, если бы я мог обнаружить изменение текстового содержимого независимо от изменения текстового формата.

2 ответов


подход я взял было для запуска форматирования логика в BackgroundWorker. Я выбрал это, потому что формат займет "долгое" время, более 1 секунды или двух, поэтому я не мог сделать это в потоке пользовательского интерфейса.

просто чтобы повторить проблему: каждый вызов, сделанный BackgroundWorker сеттеру на RichTextBox.SelectionColor снова запустил событие TextChanged, которое снова запустит поток BG. В событии TextChanged я не мог найти способ отличить " пользователь имеет набрал что-то "событие из программы" отформатировал текст " событие. Таким образом, вы можете видеть, что это будет бесконечная прогрессия изменений.

Простой Подход Не Работает

общий подход (как предложил Эрик) - "отключить" обработку событий изменения текста во время работы в обработчике изменения текста. Но, конечно, это не сработает для моего случая, потому что изменения текста (изменения SelectionColor) генерируются фон нить. Они не выполняются в рамках обработчика изменения текста. Поэтому простой подход к фильтрации инициированных пользователем событий не будет работать для моего случая, когда фоновый поток вносит изменения.

другие попытки обнаружить изменения, инициированные пользователем

Я попытался использовать RichTextBox.Текст.Длина как способ отличить изменения в richtextbox, происходящие из моего потока форматирования от изменений в richtextbox сделано пользователем. Если длина не изменилась, рассуждал я, то изменение было изменением формата, сделанным моим кодом, а не пользовательским редактированием. Но получение RichTextBox.Свойство Text дорого, и это для каждого события TextChange сделало весь пользовательский интерфейс недопустимо медленным. Даже если это было достаточно быстро, в общем случае это не работает, потому что пользователи также изменяют формат. И пользовательское редактирование может создавать текст той же длины, если это был вид опечатки операция.

Я надеялся поймать и обработать событие TextChange только для обнаружения изменений, происходящих от пользователя. Поскольку я не мог этого сделать, я изменил приложение, чтобы использовать событие нажатия клавиши и событие вставки. В результате теперь я не получаю ложных событий TextChange из-за изменений форматирования (например, RichTextBox.SelectionColor = Цвет.Синий.)

сигнализация рабочего потока для выполнения его работы

хорошо, у меня есть поток, который может сделать изменения форматирования. Концептуально он делает следующее:

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

как я могу сказать потоку BG, чтобы начать форматирование?

я использовал ManualResetEvent. При обнаружении нажатия клавиши обработчик нажатия клавиши устанавливает это событие (включает его). Фоновый работник ожидает того же события. Когда он включен, поток 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 установлен (включен), поток форматирования обнаруживает это, и сброс он (выключает его) и начинает форматирование. Он просматривает текст и решает, как его форматировать. Периодически formatter поток проверяет ManualResetEvent снова. Если во время форматирования происходит другое событие нажатия клавиши, событие снова переходит в сигнальное состояние. Когда форматер видит, что сигнал повторен, он выпрыгивает и снова начинает форматирование с начала текста, как Сизиф. Более интеллектуальный механизм перезапустит форматирование с точки в документе, где произошло изменение.

Форматирование Отложенного Начала

еще один поворот: я не хочу, чтобы форматер начал свою работу по форматированию тут С каждым нажатием клавиши. Как человеческие типы, нормальная пауза между нажатиями клавиш составляет менее 600-700ms. Если форматер начинает форматирование без задержки, то он попытается начать форматирование между нажатиями клавиш. Довольно бессмысленно.

таким образом, логика форматирования только начинает выполнять свою работу форматирования, если она обнаруживает паузу в нажатиях клавиш более 600 мс. После получать сигнал, он ждет 600ms, и если не было никаких промежуточных нажатий клавиш, то ввод текста остановился и форматирование должно начаться. Если произошло промежуточное изменение, то форматер ничего не делает, заключая, что пользователь все еще печатает. В коде:

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

событие keypress:

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;

пользовательский опыт заключается в том, что форматирование не происходит во время ввода. После ввода паузы, начинается форматирование. Если ввод текста начинается снова, форматирование останавливается и снова ждет.

отключение прокрутки во время изменения формата

была одна последняя проблема: форматирование текста в RichTextBox требует вызова управления richtextbox.Select (), который причиняет RichTextBox для автоматической прокрутки к выбранному тексту, когда RichTextBox имеет фокус. Поскольку форматирование происходит одновременно с пользователем сосредоточенный на управлении, чтении и, возможно, редактировании текста, мне нужен был способ подавить прокрутку. Я не мог найти способ предотвратить прокрутку с помощью публичного интерфейса RTB, хотя я нашел много людей в intertubes, спрашивающих об этом. После некоторых экспериментов, я обнаружил, что с помощью Win32 SendMessage () call (from user32.dll файлы), направив WM_SETREDRAW до и после Select (), может предотвратить прокрутку в RichTextBox при вызове Select ().

поскольку я прибегал к pinvoke для предотвращения прокрутки, я также использовал pinvoke на SendMessage, чтобы получить или установить выделение или курсор в текстовом поле (EM_GETSEL или EM_SETSEL), и установить форматирование на выбор (EM_SETCHARFORMAT). Подход pinvoke оказался немного быстрее, чем использование управляемого интерфейса.

пакетные обновления для отклика

и так предотвращение прокрутки повлекло за собой некоторые вычислительные накладные расходы, я решил пакетировать изменения, внесенные в документ. Вместо выделения одного смежного раздела или слова логика сохраняет список изменений выделения или формата. Время от времени он применяет, возможно, 30 изменений за раз к документу. Затем он очищает список и возвращается к анализу и очереди, какие изменения формата должны быть сделаны. Это достаточно быстро, что ввод в doc не прерывается при применении этих пакетов изменения.

в результате документ автоматически форматируется и раскрашивается в дискретные куски, когда не происходит набора текста. Если между нажатиями клавиш пользователя проходит достаточно времени, весь документ в конечном итоге будет отформатирован. Это под 200ms для 1K XML doc, возможно, 2s для 30K doc или 10s для 100k doc. Если пользователь редактирует документ, то любое выполняемое форматирование прерывается, и форматирование начинается заново.


Фух!

Я поражен, что что-то, казалось бы, простое, как форматирование richtextbox в то время как пользователь вводит в нем так вовлечен. Но я не мог придумать ничего проще, что не запирало бы текстовое поле, но избегало странного поведения прокрутки.


вы можете код для чего я описал выше.


обычно, когда я реагирую в обработчике событий способом, который может привести к повторному запуску того же события, я устанавливаю флаг, указывающий, что я уже обрабатываю обработчик событий, проверяю флаг в верхней части обработчика событий и немедленно возвращаю, если флаг установлен:

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, если она потенциально длинная).

EDIT:

полный, рабочий код

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);
        }

    }
}