Come associare un TextBlock a una risorsa contenente testo formattato?

Ho un TextBlock nella mia finestra WPF.

 Some formatted text.  

Quando viene visualizzato, appare come questo,

Qualche testo formattato .

La mia domanda è: posso bind questo “contenuto” in linea a una risorsa nella mia applicazione?

Sono arrivato fino a:

Creare una stringa di risorse dell’applicazione,

 myText="Some formatted text." 

e il seguente xaml (Qualche codice omesso per brevità)

           

Try1 esegue il rendering con i tag in posizione e non influisce sulla formattazione.

Alcuni formattato testo.

Try2 non verrà compilato o renderizzato perché la risorsa “myText” non è di tipo Inline ma una stringa.

Questo compito apparentemente semplice è ansible e se sì, come?

Ecco il mio codice modificato per il formato ricorsivo del testo. Gestisce Grassetto, Corsivo, Sottolineato e LineBreak ma può essere facilmente esteso per supportare di più (modifica l’istruzione switch ).

 public static class MyBehavior { public static string GetFormattedText(DependencyObject obj) { return (string)obj.GetValue(FormattedTextProperty); } public static void SetFormattedText(DependencyObject obj, string value) { obj.SetValue(FormattedTextProperty, value); } public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached("FormattedText", typeof(string), typeof(MyBehavior), new UIPropertyMetadata("", FormattedTextChanged)); static Inline Traverse(string value) { // Get the sections/inlines string[] sections = SplitIntoSections(value); // Check for grouping if (sections.Length.Equals(1)) { string section = sections[0]; string token; // Eg  int tokenStart, tokenEnd; // Where the token/section starts and ends. // Check for token if (GetTokenInfo(section, out token, out tokenStart, out tokenEnd)) { // Get the content to further examination string content = token.Length.Equals(tokenEnd - tokenStart) ? null : section.Substring(token.Length, section.Length - 1 - token.Length * 2); switch (token) { case "": return new Bold(Traverse(content)); case "": return new Italic(Traverse(content)); case "": return new Underline(Traverse(content)); case "": return new LineBreak(); default: return new Run(section); } } else return new Run(section); } else // Group together { Span span = new Span(); foreach (string section in sections) span.Inlines.Add(Traverse(section)); return span; } } ///  /// Examines the passed string and find the first token, where it begins and where it ends. ///  /// The string to examine. /// The found token. /// Where the token begins. /// Where the end-token ends. /// True if a token was found. static bool GetTokenInfo(string value, out string token, out int startIndex, out int endIndex) { token = null; endIndex = -1; startIndex = value.IndexOf("<"); int startTokenEndIndex = value.IndexOf(">"); // No token here if (startIndex < 0) return false; // No token here if (startTokenEndIndex < 0) return false; token = value.Substring(startIndex, startTokenEndIndex - startIndex + 1); // Check for closed token. Eg  if (token.EndsWith("/>")) { endIndex = startIndex + token.Length; return true; } string endToken = token.Insert(1, "/"); // Detect nesting; int nesting = 0; int temp_startTokenIndex = -1; int temp_endTokenIndex = -1; int pos = 0; do { temp_startTokenIndex = value.IndexOf(token, pos); temp_endTokenIndex = value.IndexOf(endToken, pos); if (temp_startTokenIndex >= 0 && temp_startTokenIndex < temp_endTokenIndex) { nesting++; pos = temp_startTokenIndex + token.Length; } else if (temp_endTokenIndex >= 0 && nesting > 0) { nesting--; pos = temp_endTokenIndex + endToken.Length; } else // Invalid tokenized string return false; } while (nesting > 0); endIndex = pos; return true; } ///  /// Splits the string into sections of tokens and regular text. ///  /// The string to split. /// An array with the sections. static string[] SplitIntoSections(string value) { List sections = new List(); while (!string.IsNullOrEmpty(value)) { string token; int tokenStartIndex, tokenEndIndex; // Check if this is a token section if (GetTokenInfo(value, out token, out tokenStartIndex, out tokenEndIndex)) { // Add pretext if the token isn't from the start if (tokenStartIndex > 0) sections.Add(value.Substring(0, tokenStartIndex)); sections.Add(value.Substring(tokenStartIndex, tokenEndIndex - tokenStartIndex)); value = value.Substring(tokenEndIndex); // Trim away } else { // No tokens, just add the text sections.Add(value); value = null; } } return sections.ToArray(); } private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { string value = e.NewValue as string; TextBlock textBlock = sender as TextBlock; if (textBlock != null) textBlock.Inlines.Add(Traverse(value)); } } 

Modifica: (proposto da Spook)

Una versione più breve, ma richiede che il testo sia valido per XML:

 using System.Xml; // (...) public static class TextBlockHelper { #region FormattedText Attached dependency property public static string GetFormattedText(DependencyObject obj) { return (string)obj.GetValue(FormattedTextProperty); } public static void SetFormattedText(DependencyObject obj, string value) { obj.SetValue(FormattedTextProperty, value); } public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached("FormattedText", typeof(string), typeof(TextBlockHelper), new UIPropertyMetadata("", FormattedTextChanged)); private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { string value = e.NewValue as string; TextBlock textBlock = sender as TextBlock; if (textBlock != null) { textBlock.Inlines.Clear(); textBlock.Inlines.Add(Process(value)); } } #endregion static Inline Process(string value) { XmlDocument doc = new XmlDocument(); doc.LoadXml(value); Span span = new Span(); InternalProcess(span, doc.ChildNodes[0]); return span; } private static void InternalProcess(Span span, XmlNode xmlNode) { foreach (XmlNode child in xmlNode) { if (child is XmlText) { span.Inlines.Add(new Run(child.InnerText)); } else if (child is XmlElement) { switch (child.Name.ToUpper()) { case "B": case "BOLD": { Span boldSpan = new Span(); InternalProcess(boldSpan, child); Bold bold = new Bold(boldSpan); span.Inlines.Add(bold); break; } case "I": case "ITALIC": { Span italicSpan = new Span(); InternalProcess(italicSpan, child); Italic italic = new Italic(italicSpan); span.Inlines.Add(italic); break; } case "U": case "UNDERLINE": { Span underlineSpan = new Span(); InternalProcess(underlineSpan, child); Underline underline = new Underline(underlineSpan); span.Inlines.Add(underline); break; } } } } } } 

E un esempio di utilizzo:

    

Che ne dici di usare un comportamento allegato? Sotto il codice si occupano solo i tag in grassetto. Ogni parola che dovrebbe essere grassetto deve essere racchiusa in tag in grassetto. Probabilmente vorrai che la class accetti anche altri formati. Anche gli spazi devono essere gestiti meglio, la class elimina spazi consecutivi e ne aggiunge uno in più alla fine. Quindi considera di seguito solo la class come codice dimostrativo che avrà bisogno di ulteriore lavoro per essere utile ma dovrebbe iniziare.

XAML:

    

Codice dietro:

 using System; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; namespace FormatTest { public partial class Window1 : Window { public Window1() { InitializeComponent(); DataContext = this; } public string Text { get { return "Some formatted text."; } } } public static class FormattedTextBehavior { public static string GetFormattedText(DependencyObject obj) { return (string)obj.GetValue(FormattedTextProperty); } public static void SetFormattedText(DependencyObject obj, string value) { obj.SetValue(FormattedTextProperty, value); } public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached("FormattedText", typeof(string), typeof(FormattedTextBehavior), new UIPropertyMetadata("", FormattedTextChanged)); private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { TextBlock textBlock = sender as TextBlock; string value = e.NewValue as string; string[] tokens = value.Split(' '); foreach (string token in tokens) { if (token.StartsWith("") && token.EndsWith("")) { textBlock.Inlines.Add(new Bold(new Run(token.Replace("", "").Replace("", "") + " "))); } else { textBlock.Inlines.Add(new Run(token + " ")); } } } } } 

MODIFICARE:

Questa linea,

è un cattivo approccio all’accesso allo spazio dei nomi Project.Properties.Resources. Provoca problemi goffi durante la ricompilazione.

Molto meglio usare x:Static per fare qualcosa del genere,

Text="{x:Static props:Resources.SomeText}"

nella tua rilegatura. Grazie a Ben


Ok, è così che l’ho fatto. Non è perfetto ma funziona.

Ricorda, c’è una risorsa di progetto chiamata FormattedText.

cs:

 // TextBlock with a bindable InlineCollection property. // Type is List(Inline) not InlineCollection becuase // InlineCollection makes the IDE xaml parser complain // presumably this is caused by an inherited attribute. public class BindableTextBlock : TextBlock { public static readonly DependencyProperty InlineCollectionProperty = DependencyProperty.Register( "InlineCollection", typeof(List), typeof(BindableTextBlock), new UIPropertyMetadata(OnInlineCollectionChanged)); private static void OnInlineCollectionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { BinableTextBlock instance = sender as BindableTextBlock; if (instance != null) { List newText = e.NewValue as List; if (newText != null) { // Clear the underlying Inlines property instance.Inlines.Clear(); // Add the passed List to the real Inlines instance.Inlines.AddRange(newText.ToList()); } } } public List InlineCollection { get { return (List)GetValue(InlineCollectionProperty); } set { SetValue(InlineCollectionProperty, value); } } } // Convertor between a string of xaml with implied run elements // and a generic list of inlines [ValueConversion(typeof(string), typeof(List))] public class StringInlineCollectionConvertor : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { string text = value as String; // a surrogate TextBlock to host an InlineCollection TextBlock results = new TextBlock(); if (!String.IsNullOrEmpty(text)) { //Arbritary literal acting as a replace token, //must not exist in the empty xaml definition. const string Replace = "xxx"; // add a dummy run element and replace it with the text results.Inlines.Add(new Run(Replace)); string resultsXaml = XamlWriter.Save(results); string resultsXamlWithText = resultsXaml.Replace(Replace, text); // deserialise the xaml back into our TextBlock results = XamlReader.Parse(resultsXamlWithText) as TextBlock; } return results.Inlines.ToList(); } // Not clear when this will be called but included for completeness public object ConvertBack( object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { String results = String.Empty; InlineCollection inlines = value as InlineCollection; if (inlines != null) { //read the xaml as xml and return the "content" var reader = XElement.Parse(XamlWriter.Save(inlines)).CreateReader(); reader.MoveToContent(); results = reader.ReadInnerXml(); } return results; } } 

xaml:

        

Ho fatto 2 lezioni. Un TextBlock sottoclass con InlineCollection “associabile” e un convertitore di valore IValue per convertire la raccolta da e verso una stringa.

Utilizzando InlineCollection direttamente come il tipo di proprietà creato da VS2010 si lamenta, sebbene il codice continui a funzionare correttamente. Sono passato a un elenco generico di Inline. Presumo che ci sia un attributo ereditato che dice a VS che InlineCollection non ha costruttore.

Ho provato a rendere la proprietà InlineCollection proprietà ContentBlock di BindableTextBlock ma ha incontrato problemi e fuori dal tempo. Sentiti libero di fare il passo successivo e di parlarmene.

Mi scuso per qualsiasi errore, ma questo codice doveva essere trascritto e disinfettato.

Se c’è un modo migliore per farlo, sicuramente ci deve essere, per favore dillo anche a me. Non sarebbe bello se questa funzionalità fosse integrata o, ho perso qualcosa?

Ho finito per doverlo fare nella mia applicazione e ho dovuto supportare molti dei markup possibili normalmente in linea di TextBlock, quindi ho preso la risposta del programmatore di Wallstreet sopra (che funziona magnificamente ed è molto meno complicata della maggior parte delle altre risposte che ho trovato su questo argomento) e ampliato su di esso. Immagino che qualcun altro potrebbe trovare questo utile.

Non ho ancora provato a fondo questo con TUTTI i tag, ma tutti quelli che ho provato hanno funzionato come un fascino. Sospetto anche che non sia il codice più veloce al mondo, ma i miei test con migliaia di messaggi formattati in un ListView sono sembrati sorprendentemente rapidi. YMMV. Il codice è di seguito:

XAML:

    

C #

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; namespace FormatTest { public static class FormattedTextBehavior { public class TextPart { public String mType = String.Empty; public Inline mInline = null; public InlineCollection mChildren = null; public TextPart() {} public TextPart(String t, Inline inline, InlineCollection col) { mType = t; mInline = inline; mChildren = col; } } private static Regex mRegex = new Regex(@"<(?/?[^>]*)>", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static Regex mSpanRegex = new Regex("(?[^\\s=]+)=\"(?[^\\s\"]*)\"", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static string GetFormattedText(DependencyObject obj) { return (string)obj.GetValue(FormattedTextProperty); } public static void SetFormattedText(DependencyObject obj, string value) { obj.SetValue(FormattedTextProperty, value); } public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached("FormattedText", typeof(string), typeof(FormattedTextBehavior), new UIPropertyMetadata("", FormattedTextChanged)); private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { TextBlock textBlock = sender as TextBlock; FormatText(e.NewValue as string, new TextPart("TextBlock", null, textBlock.Inlines)); } public static void FormatText(String s, TextPart root) { int len = s.Length; int lastIdx = 0; List parts = new List(); parts.Add(root); Match m = mRegex.Match(s); while (m.Success) { String tag = m.Result("${Span}"); if (tag.StartsWith("/")) { String prevStr = s.Substring(lastIdx, m.Index - lastIdx); TextPart part = parts.Last(); if (!String.IsNullOrEmpty(prevStr)) { if (part.mChildren != null) { part.mChildren.Add(new Run(prevStr)); } else if (part.mInline is Run) { (part.mInline as Run).Text = prevStr; } } if (!tag.Substring(1).Equals(part.mType, StringComparison.InvariantCultureIgnoreCase)) { Logger.LogD("Mismatched End Tag '" + tag.Substring(1) + "' (expected ) at position " + m.Index.ToString() + " in String '" + s + "'"); } if (parts.Count > 1) { parts.RemoveAt(parts.Count - 1); TextPart parentPart = parts.Last(); if (parentPart.mChildren != null) { parentPart.mChildren.Add(part.mInline); } } } else { TextPart prevPart = parts.Last(); String prevStr = s.Substring(lastIdx, m.Index - lastIdx); if (!String.IsNullOrEmpty(prevStr)) { if (prevPart.mChildren != null) { prevPart.mChildren.Add(new Run(prevStr)); } else if (prevPart.mInline is Run) { (prevPart.mInline as Run).Text = prevStr; } } bool hasAttributes = false; TextPart part = new TextPart(); if (tag.StartsWith("bold", StringComparison.InvariantCultureIgnoreCase)) { part.mType = "BOLD"; part.mInline = new Bold(); part.mChildren = (part.mInline as Bold).Inlines; } else if (tag.StartsWith("underline", StringComparison.InvariantCultureIgnoreCase)) { part.mType = "UNDERLINE"; part.mInline = new Underline(); part.mChildren = (part.mInline as Underline).Inlines; } else if (tag.StartsWith("italic", StringComparison.InvariantCultureIgnoreCase)) { part.mType = "ITALIC"; part.mInline = new Italic(); part.mChildren = (part.mInline as Italic).Inlines; } else if (tag.StartsWith("linebreak", StringComparison.InvariantCultureIgnoreCase)) { part.mType = "LINEBREAK"; part.mInline = new LineBreak(); } else if (tag.StartsWith("span", StringComparison.InvariantCultureIgnoreCase)) { hasAttributes = true; part.mType = "SPAN"; part.mInline = new Span(); part.mChildren = (part.mInline as Span).Inlines; } else if (tag.StartsWith("run", StringComparison.InvariantCultureIgnoreCase)) { hasAttributes = true; part.mType = "RUN"; part.mInline = new Run(); } else if (tag.StartsWith("hyperlink", StringComparison.InvariantCultureIgnoreCase)) { hasAttributes = true; part.mType = "HYPERLINK"; part.mInline = new Hyperlink(); part.mChildren = (part.mInline as Hyperlink).Inlines; } if (hasAttributes && part.mInline != null) { Match m2 = mSpanRegex.Match(tag); while (m2.Success) { String key = m2.Result("${Key}"); String val = m2.Result("${Val}"); if (key.Equals("FontWeight", StringComparison.InvariantCultureIgnoreCase)) { FontWeight fw = FontWeights.Normal; try { fw = (FontWeight)new FontWeightConverter().ConvertFromString(val); } catch (Exception) { fw = FontWeights.Normal; } part.mInline.FontWeight = fw; } else if (key.Equals("FontSize", StringComparison.InvariantCultureIgnoreCase)) { double fs = part.mInline.FontSize; if (Double.TryParse(val, out fs)) { part.mInline.FontSize = fs; } } else if (key.Equals("FontStretch", StringComparison.InvariantCultureIgnoreCase)) { FontStretch fs = FontStretches.Normal; try { fs = (FontStretch)new FontStretchConverter().ConvertFromString(val); } catch (Exception) { fs = FontStretches.Normal; } part.mInline.FontStretch = fs; } else if (key.Equals("FontStyle", StringComparison.InvariantCultureIgnoreCase)) { FontStyle fs = FontStyles.Normal; try { fs = (FontStyle)new FontStyleConverter().ConvertFromString(val); } catch (Exception) { fs = FontStyles.Normal; } part.mInline.FontStyle = fs; } else if (key.Equals("FontFamily", StringComparison.InvariantCultureIgnoreCase)) { if (!String.IsNullOrEmpty(val)) { FontFamily ff = new FontFamily(val); if (Fonts.SystemFontFamilies.Contains(ff)) { part.mInline.FontFamily = ff; } } } else if (key.Equals("Background", StringComparison.InvariantCultureIgnoreCase)) { Brush b = part.mInline.Background; try { b = (Brush)new BrushConverter().ConvertFromString(val); } catch (Exception) { b = part.mInline.Background; } part.mInline.Background = b; } else if (key.Equals("Foreground", StringComparison.InvariantCultureIgnoreCase)) { Brush b = part.mInline.Foreground; try { b = (Brush)new BrushConverter().ConvertFromString(val); } catch (Exception) { b = part.mInline.Foreground; } part.mInline.Foreground = b; } else if (key.Equals("ToolTip", StringComparison.InvariantCultureIgnoreCase)) { part.mInline.ToolTip = val; } else if (key.Equals("Text", StringComparison.InvariantCultureIgnoreCase) && part.mInline is Run) { (part.mInline as Run).Text = val; } else if (key.Equals("NavigateUri", StringComparison.InvariantCultureIgnoreCase) && part.mInline is Hyperlink) { (part.mInline as Hyperlink).NavigateUri = new Uri(val); } m2 = m2.NextMatch(); } } if (part.mInline != null) { if (tag.TrimEnd().EndsWith("/")) { if (prevPart.mChildren != null) { prevPart.mChildren.Add(part.mInline); } } else { parts.Add(part); } } } lastIdx = m.Index + m.Length; m = m.NextMatch(); } if (lastIdx < (len - 1)) { root.mChildren.Add(new Run(s.Substring(lastIdx))); } } } } 

Lo stesso ho implementato usando il comportamento. Codice di seguito:

 public class FormatTextBlock : Behavior { public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.Register( "FormattedText", typeof(string), typeof(FormatTextBlock), new PropertyMetadata(string.Empty, OnFormattedTextChanged)); public string FormattedText { get { return (string)AssociatedObject.GetValue(FormattedTextProperty); } set { AssociatedObject.SetValue(FormattedTextProperty, value); } } private static void OnFormattedTextChanged(DependencyObject textBlock, DependencyPropertyChangedEventArgs eventArgs) { System.Windows.Controls.TextBlock currentTxtBlock = (textBlock as FormatTextBlock).AssociatedObject; string text = eventArgs.NewValue as string; if (currentTxtBlock != null) { currentTxtBlock.Inlines.Clear(); string[] strs = text.Split(new string[] { "", "" }, StringSplitOptions.None); for (int i = 0; i < strs.Length; i++) { currentTxtBlock.Inlines.Add(new Run { Text = strs[i], FontWeight = i % 2 == 1 ? FontWeights.Bold : FontWeights.Normal }); } } } } 

XAML: importazione spazio dei nomi

  

Quindi utilizzare il comportamento come:

       

Questo lavoro per me:

XAML:

  

e il tuo TextBlock XAML:

  

CODICE:

 public static class TextBlockHelper { public static string GetFormattedText(DependencyObject textBlock) { return (string)textBlock.GetValue(FormattedTextProperty); } public static void SetFormattedText(DependencyObject textBlock, string value) { textBlock.SetValue(FormattedTextProperty, value); } public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached("FormattedText", typeof(string), typeof(TextBlock), new PropertyMetadata(string.Empty, (sender, e) => { string text = e.NewValue as string; var textB1 = sender as TextBlock; if (textB1 != null) { textB1.Inlines.Clear(); var str = text.Split(new string[] { "", "" }, StringSplitOptions.None); for (int i = 0; i < str.Length; i++) textB1.Inlines.Add(new Run { Text = str[i], FontWeight = i % 2 == 1 ? FontWeights.Bold : FontWeights.Normal }); } })); } 

USE nel tuo binding di stringhe:

 String Text = Text Bold;