2016-07-08 26 views
5

Sono stato incaricato di creare un RichTextBox parzialmente modificabile. Ho visto suggerimenti in Xaml aggiungendo gli elementi TextBlock per le sezioni ReadOnly, tuttavia questo ha l'indesiderabile effetto visivo di non essere avvolto piacevolmente. (Dovrebbe apparire come un unico blocco di testo continuo.)Come impedire a RichTextBox Inline di essere eliminato in TextChanged?

ho patchato insieme un prototipo funzionante utilizzando alcuni reverse string formatting per limitare/consentire modifiche ed accoppiati che, con la creazione dinamica di inline Run elements per scopi di visualizzazione. Utilizzando un dizionario per memorizzare i valori correnti delle sezioni di testo modificabili, aggiorno gli elementi Run di conseguenza su qualsiasi evento trigger TextChanged - con l'idea che se il testo di una sezione modificabile viene completamente eliminato, verrà sostituito nuovamente al suo valore predefinito.

Nella stringa: "Ciao NOME, benvenuto nel campo SPORT"., solo NAME e SPORT sono modificabili.

   ╔═══════╦════════╗     ╔═══════╦════════╗ 
Default values: ║ Key ║ Value ║ Edited values: ║ Key ║ Value ║ 
       ╠═══════╬════════╣     ╠═══════╬════════╣ 
       ║ NAME ║ NAME ║     ║ NAME ║ John ║ 
       ║ SPORT ║ SPORT ║     ║ SPORT ║ Tennis ║ 
       ╚═══════╩════════╝     ╚═══════╩════════╝ 

"Hi NAME, welcome to SPORT camp." "Hi John, welcome to Tennis camp." 

Problema:

Cancellare l'intero valore di testo in un particolare periodo di rimuovere quella corsa (e la successiva corsa) dal RichTextBox Document. Anche se li aggiungo tutti, non vengono più visualizzati correttamente sullo schermo. Ad esempio, utilizzando la stringa modificata dal setup sopra:

  • utente mette in evidenza il testo "John" e fa clic Elimina, invece di salvare il valore vuoto, deve essere sostituito con il testo predefinito di "NAME". Internamente questo succede Il dizionario ottiene il valore corretto, lo Run.Text ha il valore, lo Document contiene tutti gli elementi Run corretti. Ma lo schermo visualizza:

    • Previsto: "Ciao NOME, benvenuto nel campo di tennis".
    • Effettivo: "Ciao campo NAMETennis."

Actual vs Expected screenshot

Nota a margine: Questo comportamento perdita-di-Run-elemento può anche essere duplicati quando si incolla. Evidenziare "SPORT" e incollare "Tennis" e la sequenza contenente il campo "". è perso.

Domanda:

Come faccio a tenere ogni elemento Run visibile anche attraverso azioni distruttive, una volta che sono stati sostituiti?

Codice:

ho cercato di smontare il codice a un esempio minimo, così ho rimosso:

  • Ogni DependencyProperty ed associato vincolante nel xaml
  • Logic ricalcolo posizione di inserimento (scusa)
  • Rifattorizzato i metodi di estensione della formattazione della stringa collegata dal primo collegamento a un singolo metodo conteneva all'interno della classe. (Nota: questo metodo funzionerà con semplici formati di stringa di esempio.È stato escluso il mio codice per una formattazione più solida. Quindi, attenersi all'esempio fornito per questi scopi di test.)
  • Rende le sezioni modificabili chiaramente visibili, non importa la combinazione di colori .

Per eseguire il test, rilasciare la classe nella cartella delle risorse del progetto WPF, correggere lo spazio dei nomi e aggiungere il controllo a una vista.

using System.Collections.Generic; 
using System.Linq; 
using System.Text.RegularExpressions; 
using System.Windows.Controls; 
using System.Windows.Documents; 
using System.Windows.Media; 

namespace WPFTest.Resources 
{ 
    public class MyRichTextBox : RichTextBox 
    { 
    public MyRichTextBox() 
    { 
     this.TextChanged += MyRichTextBox_TextChanged; 
     this.Background = Brushes.LightGray; 

     this.Parameters = new Dictionary<string, string>(); 
     this.Parameters.Add("NAME", "NAME"); 
     this.Parameters.Add("SPORT", "SPORT"); 

     this.Format = "Hi {0}, welcome to {1} camp."; 
     this.Text = string.Format(this.Format, this.Parameters.Values.ToArray<string>()); 

     this.Runs = new List<Run>() 
     { 
     new Run() { Background = Brushes.LightGray, Tag = "Hi " }, 
     new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "NAME" }, 
     new Run() { Background = Brushes.LightGray, Tag = ", welcome to " }, 
     new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "SPORT" }, 
     new Run() { Background = Brushes.LightGray, Tag = " camp." }, 
     }; 

     this.UpdateRuns(); 
    } 

    public Dictionary<string, string> Parameters { get; set; } 
    public List<Run> Runs { get; set; } 
    public string Text { get; set; } 
    public string Format { get; set; } 

    private void MyRichTextBox_TextChanged(object sender, TextChangedEventArgs e) 
    { 
     string richText = new TextRange(this.Document.Blocks.FirstBlock.ContentStart, this.Document.Blocks.FirstBlock.ContentEnd).Text; 
     string[] oldValues = this.Parameters.Values.ToArray<string>(); 
     string[] newValues = null; 

     bool extracted = this.TryParseExact(richText, this.Format, out newValues); 

     if (extracted) 
     { 
     var changed = newValues.Select((x, i) => new { NewVal = x, Index = i }).Where(x => x.NewVal != oldValues[x.Index]).FirstOrDefault(); 
     string key = this.Parameters.Keys.ElementAt(changed.Index); 
     this.Parameters[key] = string.IsNullOrWhiteSpace(newValues[changed.Index]) ? key : newValues[changed.Index]; 

     this.Text = richText; 
     } 
     else 
     { 
     e.Handled = true; 
     } 

     this.UpdateRuns(); 
    } 

    private void UpdateRuns() 
    { 
     this.TextChanged -= this.MyRichTextBox_TextChanged; 

     foreach (Run run in this.Runs) 
     { 
     string value = run.Tag.ToString(); 

     if (this.Parameters.ContainsKey(value)) 
     { 
      run.Text = this.Parameters[value]; 
     } 
     else 
     { 
      run.Text = value; 
     } 
     } 

     Paragraph p = this.Document.Blocks.FirstBlock as Paragraph; 
     p.Inlines.Clear(); 
     p.Inlines.AddRange(this.Runs); 

     this.TextChanged += this.MyRichTextBox_TextChanged; 
    } 

    public bool TryParseExact(string data, string format, out string[] values) 
    { 
     int tokenCount = 0; 
     format = Regex.Escape(format).Replace("\\{", "{"); 
     format = string.Format("^{0}$", format); 

     while (true) 
     { 
     string token = string.Format("{{{0}}}", tokenCount); 

     if (!format.Contains(token)) 
     { 
      break; 
     } 

     format = format.Replace(token, string.Format("(?'group{0}'.*)", tokenCount++)); 
     } 

     RegexOptions options = RegexOptions.None; 

     Match match = new Regex(format, options).Match(data); 

     if (tokenCount != (match.Groups.Count - 1)) 
     { 
     values = new string[] { }; 
     return false; 
     } 
     else 
     { 
     values = new string[tokenCount]; 

     for (int index = 0; index < tokenCount; index++) 
     { 
      values[index] = match.Groups[string.Format("group{0}", index)].Value; 
     } 

     return true; 
     } 
    } 
    } 
} 
+0

'RichTextBox' fornisce molto più di un semplice editor di testo tramite la classe' FlowDocument'. Questo lo rende un comodo controllo per la visualizzazione di testo formattato, immagini, tabelle e persino 'UIElements'. Tuttavia tutti questi vengono con il costo della complessità. Pertanto, trattare con 'RichTextBox' può essere piuttosto complicato. C'è qualche possibilità che tu possa passare a un controllo più conveniente come 'AvalonEdit' che è più adatto per l'elaborazione del testo? –

+0

Da quello che posso vedere il problema è che 'TryParseExact (richText' non riesce. È ragionevole iniziare a risolverlo? –

+0

@ qqww2 Non ho familiarità con' AvalonEdit' oi suoi requisiti di licenza, ma lo prenderò in considerazione – OhBeWise

risposta

2

Il problema con il vostro codice è che quando si modifica il testo tramite interfaccia utente, interni Run oggetti vengono modificati, creato, cancellato e tutte le cose pazze si verificano dietro le quinte. La struttura interna è molto complessa. Per esempio qui è un metodo che si chiama in profondità all'interno del innocenti singola linea p.Inlines.Clear();:

private int DeleteContentFromSiblingTree(SplayTreeNode containingNode, TextPointer startPosition, TextPointer endPosition, bool newFirstIMEVisibleNode, out int charCount) 
{ 
    SplayTreeNode leftSubTree; 
    SplayTreeNode middleSubTree; 
    SplayTreeNode rightSubTree; 
    SplayTreeNode rootNode; 
    TextTreeNode previousNode; 
    ElementEdge previousEdge; 
    TextTreeNode nextNode; 
    ElementEdge nextEdge; 
    int symbolCount; 
    int symbolOffset; 

    // Early out in the no-op case. CutContent can't handle an empty content span. 
    if (startPosition.CompareTo(endPosition) == 0) 
    { 
     if (newFirstIMEVisibleNode) 
     { 
      UpdateContainerSymbolCount(containingNode, /* symbolCount */ 0, /* charCount */ -1); 
     } 
     charCount = 0; 
     return 0; 
    } 

    // Get the symbol offset now before the CutContent call invalidates startPosition. 
    symbolOffset = startPosition.GetSymbolOffset(); 

    // Do the cut. middleSubTree is what we want to remove. 
    symbolCount = CutContent(startPosition, endPosition, out charCount, out leftSubTree, out middleSubTree, out rightSubTree); 

    // We need to remember the original previous/next node for the span 
    // we're about to drop, so any orphaned positions can find their way 
    // back. 
    if (middleSubTree != null) 
    { 
     if (leftSubTree != null) 
     { 
      previousNode = (TextTreeNode)leftSubTree.GetMaxSibling(); 
      previousEdge = ElementEdge.AfterEnd; 
     } 
     else 
     { 
      previousNode = (TextTreeNode)containingNode; 
      previousEdge = ElementEdge.AfterStart; 
     } 
     if (rightSubTree != null) 
     { 
      nextNode = (TextTreeNode)rightSubTree.GetMinSibling(); 
      nextEdge = ElementEdge.BeforeStart; 
     } 
     else 
     { 
      nextNode = (TextTreeNode)containingNode; 
      nextEdge = ElementEdge.BeforeEnd; 
     } 

     // Increment previous/nextNode reference counts. This may involve 
     // splitting a text node, so we use refs. 
     AdjustRefCountsForContentDelete(ref previousNode, previousEdge, ref nextNode, nextEdge, (TextTreeNode)middleSubTree); 

     // Make sure left/rightSubTree stay local roots, we might 
     // have inserted new elements in the AdjustRefCountsForContentDelete call. 
     if (leftSubTree != null) 
     { 
      leftSubTree.Splay(); 
     } 
     if (rightSubTree != null) 
     { 
      rightSubTree.Splay(); 
     } 
     // Similarly, middleSubtree might not be a local root any more, 
     // so splay it too. 
     middleSubTree.Splay(); 

     // Note TextContainer now has no references to middleSubTree, if there are 
     // no orphaned positions this allocation won't be kept around. 
     Invariant.Assert(middleSubTree.ParentNode == null, "Assigning fixup node to parented child!"); 
     middleSubTree.ParentNode = new TextTreeFixupNode(previousNode, previousEdge, nextNode, nextEdge); 
    } 

    // Put left/right sub trees back into the TextContainer. 
    rootNode = TextTreeNode.Join(leftSubTree, rightSubTree); 
    containingNode.ContainedNode = rootNode; 
    if (rootNode != null) 
    { 
     rootNode.ParentNode = containingNode; 
    } 

    if (symbolCount > 0) 
    { 
     int nextNodeCharDelta = 0; 
     if (newFirstIMEVisibleNode) 
     { 
      // The following node is the new first ime visible sibling. 
      // It just moved, and loses an edge character. 
      nextNodeCharDelta = -1; 
     } 

     UpdateContainerSymbolCount(containingNode, -symbolCount, -charCount + nextNodeCharDelta); 
     TextTreeText.RemoveText(_rootNode.RootTextBlock, symbolOffset, symbolCount); 
     NextGeneration(true /* deletedContent */); 

     // Notify the TextElement of a content change. Note that any full TextElements 
     // between startPosition and endPosition will be handled by CutTopLevelLogicalNodes, 
     // which will move them from this tree to their own private trees without changing 
     // their contents. 
     Invariant.Assert(startPosition.Parent == endPosition.Parent); 
     TextElement textElement = startPosition.Parent as TextElement; 
     if (textElement != null) 
     {    
      textElement.OnTextUpdated();      
     } 
    } 

    return symbolCount; 
} 

Può consultare il codice sorgente da here, se siete interessati.

Una soluzione è non utilizzare gli oggetti Run creati per scopi di confronto direttamente nello FlowDocument. Effettua sempre una copia prima di aggiungerli:

private void UpdateRuns() 
{ 
    TextChanged -= MyRichTextBox_TextChanged; 

    List<Run> runs = new List<Run>(); 
    foreach (Run run in Runs) 
    { 
     Run newRun; 
     string value = run.Tag.ToString(); 

     if (Parameters.ContainsKey(value)) 
     { 
      newRun = new Run(Parameters[value]); 
     } 
     else 
     { 
      newRun = new Run(value); 
     } 

     newRun.Background = run.Background; 
     newRun.Foreground = run.Foreground; 

     runs.Add(newRun); 
    } 

    Paragraph p = Document.Blocks.FirstBlock as Paragraph; 
    p.Inlines.Clear(); 
    p.Inlines.AddRange(runs); 

    TextChanged += MyRichTextBox_TextChanged; 
} 
+0

Questo è quasi esattamente quello che ho fatto per risolvere questo problema, ma non ho veramente capito * perché * era una soluzione, ma sospettavo che qualcosa sotto il cofano fosse * ricordando * che la nuova versione di Run fosse stata cancellata. Non potrei essere più d'accordo con "* cose folli accadono dietro le quinte *". Cercando di aggiustare il posizionamento del cursore per esempio ... l'incubo algoritmico che sto cercando di semplificare - così tanti casi limite. Grazie per il link alla fonte. – OhBeWise

1

vorrei suggerire di spostare il codice per creare le piste nelle UpdateRuns

private void UpdateRuns() 
    { 
     this.TextChanged -= this.MyRichTextBox_TextChanged; 

     this.Runs = new List<Run>() 
    { 
    new Run() { Background = Brushes.LightGray, Tag = "Hi " }, 
    new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "NAME" }, 
    new Run() { Background = Brushes.LightGray, Tag = ", welcome to " }, 
    new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "SPORT" }, 
    new Run() { Background = Brushes.LightGray, Tag = " camp." }, 
    }; 

     foreach (Run run in this.Runs) 
+0

Apprezzo la tua risposta. Era simile a quello che avevo fatto e qqww2 aveva riaffermato per m e.L'altro approccio era più adatto al di fuori di questo semplice esempio quando i collegamenti e 'DependencyProperty' erano le origini dati e questo ha aiutato a fornire più * del perché * di cui avevo bisogno. Comunque, prendi un +1 per i tuoi problemi e i miei ringraziamenti! – OhBeWise