2009-09-21 14 views
6

Esta es una continuación de
WinForms RichTextBox: how to perform a formatting on TextChanged?WinForms RichTextBox: Cómo volver a formatear de forma asíncrona, sin disparar TextChanged caso

Tengo una aplicación Windows Forms con un RichTextBox, las aplicaciones de auto-aspectos más destacados del contenido de dicha caja. Debido a que el formateo puede llevar mucho tiempo para un documento grande, de 10 segundos o más, he configurado un BackgroundWorker para hacer el formateado de un RichTextBox. Camina a través del texto y realiza una serie de estos:

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

A pesar de que está haciendo esto, la interfaz de usuario sigue siendo sensible.

BackgroundWorker se inició desde el evento TextChanged. de esta manera:

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

El método de obrero de fondo se ve así:

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

Pero, cada asignación a SelectionColor hace que el evento TextChanged al fuego: un bucle sin fin.

¿Cómo distingo entre los cambios de texto que se originan externamente de los cambios de texto que se originan desde el BackgroundWorker que realiza el formateo?

También podría resolver esto si pudiera detectar un cambio de contenido de texto independientemente de un cambio de formato de texto.

Respuesta

6

El enfoque que tomé fue para ejecutar la lógica del formateador en una BackgroundWorker. Elegí esto porque el formato tomaría un tiempo "largo", más de 1 segundo o dos, por lo que no podría hacerlo en el hilo de la interfaz de usuario.

Solo para volver a plantear el problema: cada llamada realizada por BackgroundWorker al colocador en RichTextBox.SelectionColor desencadenó el evento TextChanged nuevamente, lo que iniciaría nuevamente el hilo BG. Dentro del evento TextChanged, no pude encontrar ninguna manera de distinguir un evento "el usuario ha escrito algo" de un evento "el programa ha formateado el texto". Entonces puedes ver que sería una progresión infinita de cambios.

el enfoque simple no funciona

Un enfoque común (as suggested by Eric) es de manejar mientras se ejecuta dentro del controlador de cambio de texto del evento "desactivar" el cambio de texto.Pero, por supuesto, esto no funcionará para mi caso, porque el texto cambia (cambios de SelectionColor) están siendo generados por un hilo de fondo. No se están realizando dentro del alcance de un controlador de cambio de texto. Entonces, el enfoque simple para filtrar eventos iniciados por el usuario no funcionará en mi caso, donde un hilo de fondo está haciendo cambios.

Otros intentos para detectar los cambios iniciados por el usuario

He intentado utilizar el RichTextBox.Text.Length como una forma de distinguir los cambios en el RichTextBox procedentes de mi hilo formateador de los cambios en el RichTextBox hechas por el usuario. Si la longitud no hubiera cambiado, razoné, entonces el cambio fue un cambio de formato realizado por mi código, y no una edición del usuario. Pero recuperar la propiedad RichTextBox.Text es costoso, y hacer eso para cada evento TextChange hizo que toda la UI fuera inaceptablemente lenta. Incluso si esto fue lo suficientemente rápido, no funciona en el caso general, porque los usuarios también realizan cambios de formato. Y, una edición de usuario podría producir el mismo texto de longitud, si se tratara de un tipo de operación de conversión.

Tenía la esperanza de atrapar y manejar el evento TextChange SOLAMENTE para detectar los cambios que se originan en el usuario. Como no podía hacer eso, cambié la aplicación para usar el evento KeyPress y el evento Pegar. Como resultado, ahora no obtengo eventos esporádicos de cambio de texto debido a cambios de formato (como RichTextBox.SelectionColor = Color.Blue).

señalización del subproceso de trabajo para hacer su trabajo

OK, tengo un hilo en ejecución que se pueden hacer cambios de formato. Conceptualmente, hace esto:

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

¿Cómo puedo decirle al hilo BG que comience a formatear?

He usado un ManualResetEvent. Cuando se detecta KeyPress, el manejador de pulsaciones establece ese evento (lo enciende). El trabajador de segundo plano está esperando el mismo evento. Cuando está encendido, el hilo BG lo apaga y comienza a formatear.

¿Pero y si el trabajador BG es ya formato? En ese caso, una nueva pulsación de tecla puede haber cambiado el contenido del cuadro de texto, y cualquier formateado hasta ahora puede no ser válido, por lo que debe reiniciarse el formato. Lo que realmente quiero para el hilo formateador es algo como esto:

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 

Con esta lógica, cuando se establece el ManualResetEvent (encendido), el hilo formateador detecta que, y restablece ella (lo apaga) y comienza a formatear. Recorre el texto y decide cómo formatearlo. Periódicamente, el hilo del formateador comprueba nuevamente el ManualResetEvent. Si se produce otro evento de pulsación de tecla durante el formateo, el evento pasa nuevamente a un estado señalizado. Cuando el formateador ve que se vuelve a señalizar, el formateador se desconecta y comienza a formatear nuevamente desde el comienzo del texto, como Sísifo. Un mecanismo más inteligente reiniciaría el formateo desde el punto en el documento donde ocurrió el cambio.

inicio retrasado Formateo

Otra peculiaridad: no quiero que el formateador para comenzar sus trabajos inmediatamente formato con cada pulsación de tecla. Como tipos humanos, la pausa normal entre pulsaciones de teclas es inferior a 600-700 ms. Si el formateador comienza a formatear sin demora, intentará comenzar a formatear entre las pulsaciones de teclas. Bastante sin sentido.

De modo que la lógica del formateador solo comienza a hacer su trabajo de formateo si detecta una pausa en pulsaciones de teclas de más de 600 ms. Después de recibir la señal, espera 600 ms, y si no ha habido pulsaciones de tecla intermedias, entonces el tipeo se ha detenido y el formateo debería comenzar. Si ha habido un cambio intermedio, entonces el formateador no hace nada, y concluye que el usuario aún está escribiendo. En código:

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

El evento de pulsación de tecla:

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

En el método colorizer, que corre en el fondo de rosca:

.... 
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; 

La experiencia del usuario es que no se produce el formato antes de que se están escribiendo. Una vez que escribe pausas, comienza el formateo. Si el tipeo comienza de nuevo, el formateo se detiene y espera nuevamente.

Desactivación de desplazamiento durante el formateo cambia

Hubo un último problema: formatear el texto en un RichTextBox requiere una llamada a RichTextBox.Select(), lo que hace que el RichTextBox to automatically scroll al texto seleccionado, cuando el RichTextBox tiene el foco. Debido a que el formateo está sucediendo al mismo tiempo que el usuario se enfoca en el control, lectura y tal vez editar el texto, necesitaba una forma de suprimir el desplazamiento. No pude encontrar una manera de evitar el desplazamiento utilizando la interfaz pública de RTB, aunque encontré muchas personas en los intertubos preguntando al respecto. Después de algunos experimentos, descubrí que al utilizar la llamada Win32 SendMessage() (desde user32.dll), enviando WM_SETREDRAW antes y después de Select(), puede evitar el desplazamiento en RichTextBox al llamar a Select().

Debido a que estaba recurrir a PInvoke para evitar el desplazamiento, que también se utiliza PInvoke en SendMessage para obtener o establecer la selección o símbolo de intercalación en el cuadro de texto (EM_GETSEL o EM_SETSEL), y para establecer el formato de la selección (EM_SETCHARFORMAT). El enfoque de pinvoke terminó siendo un poco más rápido que usar la interfaz administrada.

actualizaciones por lotes para la capacidad de respuesta

y debido a la prevención de desplazamiento incurrido en alguna sobrecarga de cómputo, me decidieron por lotes los cambios realizados en el documento. En lugar de resaltar una sección o palabra contigua, la lógica mantiene una lista de resaltados o cambios de formato. De vez en cuando, aplica quizás 30 cambios al documento. Luego borra la lista y vuelve a analizar y poner en cola qué cambios de formato deben realizarse. Es lo suficientemente rápido para que no se interrumpa la escritura en el documento al aplicar estos lotes de cambios.

El resultado es que el documento se formatea automáticamente y se colorea en trozos discretos, cuando no hay tipeo. Si transcurre suficiente tiempo entre las pulsaciones de teclas del usuario, finalmente se formateará todo el documento. Esto es menos de 200 ms para un doc XML de 1 k, tal vez 2s para un doc de 30k, o 10s para un doc de 100k. Si el usuario edita el documento, se anula el formato que estaba en progreso y el formato comienza de nuevo.


¡Uf!

Me sorprende que algo tan aparentemente simple como formatear un richtextbox mientras el usuario lo escribe esté tan involucrado.Pero no pude encontrar nada más simple que no bloqueara el cuadro de texto, sin embargo, evité el extraño comportamiento de desplazamiento.


Puede view the code por lo que he descrito anteriormente.

+0

Me alegro de que mi solución y mis comentarios puedan ofrecer algo de inspiración para su solución final. –

+0

Muchas gracias, Eric. – Cheeso

+0

¿Tal vez un-down-vote mi respuesta? –

3

Normalmente cuando reacciono en un controlador de eventos de una manera que podría provocar el mismo evento de nuevo, establezco un indicador que indica que ya estoy procesando el controlador de eventos, marque la bandera en la parte superior del controlador de eventos , e inmediatamente volver si el indicador se establece:

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

Si es inaceptable para bloquear el control en el desempeño de su procesamiento, se puede comprobar si el evento KeyDown en su control y borrar el indicador de procesamiento cuando lo reciba (probablemente también finaliza su procesamiento actual TextChanged si es potencialmente largo).

EDIT:

completa, código de trabajo

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

    } 
} 
+0

Esto no funcionará si el formateo se realiza de forma asíncrona. – Cheeso

+0

Sí lo hará. Debe bloquear el control de entrada durante el formateo (de lo contrario, los cambios conflictivos pueden provenir del hilo de la cola de mensajes y del hilo de formateo). Puede bloquearlo haciendo que el control sea de solo lectura brevemente, o conectando eventos que pueden causar cambios (los eventos relacionados con la clave, algunos eventos del mouse [ya que pueden seleccionar una región] y arrastrar/soltar eventos si permite ese). –

+0

Actualicé mi respuesta con un código de trabajo completo usando mi método. –

Cuestiones relacionadas