WinForms RichTextBox: как переформатировать асинхронно, без запуска события TextChanged - PullRequest
6 голосов
/ 22 сентября 2009

Это продолжение к
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 ]

6 голосов
/ 23 сентября 2009

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

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

Простой подход не работает

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

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

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

Я надеялся поймать и обработать событие TextChange ТОЛЬКО для обнаружения изменений, происходящих от пользователя. Так как я не мог этого сделать, я изменил приложение, чтобы использовать событие KeyPress и событие Paste. В результате теперь я не получаю ложных событий TextChange из-за изменений форматирования (например, RichTextBox.SelectionColor = Color.Blue).

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

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

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

Форматирование с отложенным началом

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

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

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;

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

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

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

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

Пакетные обновления для отзывчивости

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

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


Уф!

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


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

3 голосов
/ 22 сентября 2009

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

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

    }
}
...