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