Pergunta

Esta é uma continuação a
WinForms RichTextBox: como executar uma formatação em TextChanged

Eu tenho um aplicativo WinForms com um RichTextBox, o aplicativo auto-realça o conteúdo da referida caixa. Porque a formatação pode levar um longo tempo para um documento grande, 10 segundos ou mais, eu configurar um BackgroundWorker para fazer o re-formatação de um RichTextBox. Ele caminha através do texto e executa uma série de estes:

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

Enquanto ele está fazendo isso, a interface do usuário permanece responsivo.

O BackgroundWorker é expulso do evento TextChanged. assim:

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

O método de trabalho de fundo esta aparência:

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

Mas, cada atribuição para selectionColor faz com que o evento TextChanged ao fogo: um loop infinito.

Como posso distinguir entre alterações de texto que se originam externamente a partir de alterações de texto que se originam a partir do BackgroundWorker fazendo a formatação?

Eu também poderia resolver este se eu poderia detectar uma mudança de conteúdo de texto independentemente de uma mudança de formato de texto.

Foi útil?

Solução

A abordagem que eu tomei foi para executar a lógica formatador em um BackgroundWorker. Eu escolhi este porque o formato seria necessário um "longo" tempo, mais de 1 segundo ou dois, então eu não poderia fazê-lo no segmento.

Apenas para reafirmar o problema: todas as chamadas feitas pelo BackgroundWorker para o setter em RichTextBox.SelectionColor disparou o evento TextChanged de novo, que iria começar o fio BG tudo de novo. Dentro do evento TextChanged, eu poderia encontrar nenhuma maneira de distinguir um "usuário digitou alguma coisa" evento de um "programa formatado o texto" do evento. Assim você pode ver que seria uma progressão infinita de alterações.

A abordagem simples não funciona

Uma abordagem comum ( como sugerido por Eric ) é a manipulação durante a execução dentro do identificador de alteração do texto do evento "desativar" mudança de texto. Mas é claro que isso não vai funcionar para o meu caso, porque as alterações de texto (mudanças selectionColor) estão sendo gerado por um background fio. Eles não estão sendo realizadas no âmbito de um identificador de alteração de texto. Portanto, a abordagem simples de filtragem de eventos iniciados pelo usuário não vai funcionar para o meu caso, onde uma discussão de fundo é fazer alterações.

Outras tentativas para detectar mudanças iniciadas pelo usuário

Eu tentei usar o RichTextBox.Text.Length como uma forma de distinguir as mudanças no richtextbox originário do meu fio formatador das mudanças no richtextbox feitas pelo usuário. Se o comprimento não tinha mudado, eu raciocinei, então a mudança foi uma mudança de formato feito por meu código, e não um usuário editar. Mas recuperar a propriedade RichTextBox.Text é caro, e fazendo isso para cada evento TextChange fez toda a UI inaceitavelmente lento. Mesmo que isso foi rápido o suficiente, ele não funciona no caso geral, porque os usuários fazer alterações de formato, também. E, uma edição usuário pode produzir o mesmo texto de comprimento, se fosse uma espécie typeover de operação.

Eu estava esperando para pegar e manipular o evento TextChange SOMENTE para detectar alterações provenientes do usuário. Desde que eu não poderia fazer isso, eu mudei o aplicativo para usar o evento KeyPress eo evento Colar. Como resultado, eu agora não obter eventos TextChange espúrias devido a alterações de formatação (como RichTextBox.SelectionColor = Color.Blue).

Sinalização o segmento de trabalho para fazer o seu trabalho

OK, eu tenho um segmento em execução que podem fazer alterações de formatação. Conceitualmente, ele faz isso:

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

Como posso saber o histórico da BG para iniciar a formatação?

Eu usei um ManualResetEvent . Quando um KeyPress é detectado, os conjuntos de manipulador keypress esse evento (transforma-lo). O trabalho de fundo está esperando no mesmo evento. Quando ele está ligado, as voltas de rosca BG-lo fora, e começa a formatação.

Mas e se o trabalhador BG é formatação? Nesse caso, uma nova pressão de tecla pode ter alterado o conteúdo da caixa de texto, e qualquer formatação feito até agora pode agora ser inválida, de modo a formatação deve ser reiniciado. O que eu realmente quero para o segmento formatador é algo como isto:

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

Com esta lógica, quando o ManualResetEvent é definido (ligado), os detecta rosca formatador isso, e redefine TI (desliga), e começa a formatação. Ele caminha através do texto e decide como formatá-lo. Periodicamente, o formatador thread verifica o ManualResetEvent novamente. Se ocorrer outro evento keypress durante a formatação, em seguida, o evento novamente vai para um estado sinalizado. Quando o formatador vê que é re-sinalizado, os prendedores do formatador para fora e começa a formatação novamente a partir do início do texto, como Sísifo. Um mecanismo mais inteligente seria reiniciar formatting a partir do ponto no documento onde a mudança ocorreu.

Delayed Onset formatação

Outra diferença: eu não quero que o formatador para começar seu trabalho formatação imediatamente com cada tecla. Como uma tipos humanos, a pausa normais entre teclas é inferior a 600-700ms. Se o formatador começa formatação sem um atraso, em seguida, ele irá tentar iniciar a formatação entre teclas. inútil bonito.

Portanto, a lógica formatador só começa a fazer o seu trabalho formatação se detectar uma pausa em combinações de teclas de mais de 600ms. Depois de receber o sinal, ele espera 600ms, e se não houve keypresses intervenientes, em seguida, a digitação parou e a formatação deve começar. Se houve uma mudança de intervir, então o formatador não faz nada, concluindo que o usuário ainda está digitando. No código:

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

O evento keypress:

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

No método colorizer, que corre na discussão de fundo:

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

A experiência do usuário é que nenhuma formatação ocorre enquanto eles estão digitando. Uma vez pausas de digitação, formatação é iniciada. Se digitação começa novamente, a formatação pára e espera novamente.

A desativação Scrolling durante formato muda

Houve um problema final: a formatação do texto em um RichTextBox requer uma chamada para RichTextBox.Select () , o que faz com que o RichTextBox para automaticamente rolagem ao texto selecionado, quando o RichTextBox tem o foco. Porque a formatação está acontecendo ao mesmo tempo que o usuário está focada no controle, lendo e talvez editar o texto, eu precisava de uma maneira de suprimir a rolagem. Eu não poderia encontrar uma maneira de impedir a rolagem usando a interface pública de RTB, embora ENTENDI encontrar muitas pessoas nas intertubes perguntando sobre isso. Depois de algumas experiências, descobri que usando o Win32 SendMessage () chamada (a partir de user32. dll), o envio de WM_SETREDRAW antes e depois da Selecione (), pode impedir que o rolo na RichTextBox ao chamar Select ().

Porque eu estava recorrendo a pinvoke para evitar a rolagem, eu também usado pinvoke em SendMessage para obter ou definir a seleção ou acento circunflexo na caixa de texto ( EM_GETSEL ou EM_SETSEL ), e para definir a formatação da seleção ( EM_SETCHARFORMAT ). A abordagem pinvoke acabou sendo um pouco mais rápido do que usando a interface gerenciado.

Atualizações em lote para a capacidade de resposta

E porque a prevenção rolagem incorridos alguns computação em cima, eu decidi batch-se as alterações feitas ao documento. Em vez de destacar uma seção contígua ou palavra, a lógica mantém uma lista de destaque ou formato alterações a fazer. De vez em quando, aplica-se talvez 30 alterações de uma só vez para o documento. Em seguida, ele limpa a lista e volta para análise e filas que mudanças de formato precisam ser feitas. É o suficiente rápido que digitar o doc não é interrompido ao aplicar estes lotes de alterações.

O resultado é o documento fica auto-formatado e colorizado em pedaços discretos, quando nenhuma digitação está acontecendo. Se passa tempo suficiente entre keypresses usuário, o documento inteiro acabará por se formatado. Isto é menos de 200ms para um doc 1k XML, talvez 2s para um doc 30k, ou 10s para um doc 100k. Se o usuário edita o documento, em seguida, qualquer formatação que estava em andamento é abortado e é iniciada a formatação tudo de novo.


Ufa!

Estou espantado que algo aparentemente tão simples como a formatação de um richtextbox enquanto o usuário digita é tão envolvidos. Mas eu não conseguia pensar em nada mais simples que não bloquear a caixa de texto, mas evitou o comportamento de rolagem estranho.


Você pode ver o código para a coisa que eu descrevi acima .

Outras dicas

Normalmente quando eu reagir em um manipulador de eventos de uma forma que pode causar o mesmo evento a ser acionado novamente, eu definir um sinalizador indicando que eu já estou processando o manipulador de eventos, verifique a bandeira no topo do manipulador de eventos, e imediatamente retornar se o sinalizador está definido:

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

Se é inaceitável para bloquear o controle durante a execução de seu processamento, você pode verificar para o evento KeyDown em seu controle e limpar o sinalizador de processamento quando você recebê-lo (provavelmente também encerrar sua atual TextChanged processamento se é potencialmente longa).

EDIT:

, código de trabalho completa

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

    }
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top