¿Cómo vincular un TextBlock a un recurso que contiene texto formateado?

Tengo un TextBlock en mi ventana de WPF.

 Some formatted text.  

Cuando se representa, se ve así,

Algún texto formateado .

Mi pregunta es, ¿puedo vincular este “contenido” en línea a un recurso en mi aplicación?

Llegué tan lejos como:

Hacer una cadena de recursos de la aplicación

 myText="Some formatted text." 

y el siguiente xaml (se omite algún código por brevedad)

           

Try1 se representa con las tags en su lugar y no afecta al formateo.

Algo del texto formateado .

Try2 no comstackrá ni representará porque el recurso “myText” no es de tipo Inline sino una cadena.

¿Es posible esta tarea aparentemente simple y, en caso afirmativo, cómo?

Aquí está mi código modificado para texto de formato recursivo. Maneja negrita, cursiva, subrayado y LineBreak, pero puede ampliarse fácilmente para admitir más (modifique la instrucción de cambio ).

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

Editar: (propuesto por Spook)

Una versión más corta, pero requiere que el texto sea XML-válido:

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

Y un ejemplo de uso:

    

¿Qué hay de usar el comportamiento adjunto? El siguiente código solo maneja tags en negrita. Cada palabra que debe ser negrita debe estar envuelta en tags en negrita. Es probable que desee hacer que la clase acepte otros formatos también. También se deben manejar mejor los espacios, la clase elimina espacios consecutivos y agrega uno adicional al final. Por lo tanto, considere la siguiente clase como código de demostración solo, que necesitará más trabajo para ser útil, pero debería ayudarlo a comenzar.

XAML:

    

Código detrás:

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

EDITAR:

Esta línea,

es un mal enfoque para acceder al espacio de nombres Project.Properties.Resources. Causa problemas técnicos incómodos al recomstackr.

Es mucho mejor usar x:Static para hacer algo así,

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

en tu encuadernación Thx a Ben


Bien, así es como lo hice. No es perfecto, pero funciona.

Recuerde, hay un recurso del proyecto llamado FormattedText.

cs:

 // TextBlock with a bindable InlineCollection property. // Type is List(Inline) not InlineCollection becuase // InlineCollection makes the IDE xaml parser complain // presumbly 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:

        

Hice 2 clases. Un TextBlock subcategorizado con InlineCollection “vinculable” y un IValueConverter para convertir la colección de y a una Cadena.

El uso de InlineCollection directamente como el tipo de propiedad hizo quejarse a VS2010, aunque el código aún funcionaba bien. Cambié a una lista genérica de Inlines. Supongo que hay un atributo heredado que le dice a VS que InlineCollection no tiene constructor.

Intenté hacer que la propiedad InlineCollection fuera ContentProperty de BindableTextBlock, pero me encontré con problemas y sin tiempo. Por favor, siéntanse libres de dar el siguiente paso y contarme al respecto.

Me disculpo por cualquier errata, pero este código tuvo que ser transcrito y desinfectado.

Si hay una mejor manera de hacerlo, seguramente debe haber, por favor dímelo también. ¿No sería bueno si esta funcionalidad estuviera incorporada o, me he perdido algo?

Terminé necesitando hacer esto en mi aplicación y tuve que admitir muchas de las marcas posibles normalmente en las inserciones de TextBlock, así que tomé la respuesta de Wallstreet Programmer anterior (que funciona muy bien y es mucho menos complicado que la mayoría de las respuestas que encontré sobre este tema) y ampliado en él. Me imagino que alguien más puede encontrar esto útil.

Aún no lo he probado exhaustivamente con TODAS las tags, pero cada uno que he probado funciona como un hechizo. También sospecho que no es el código más rápido del mundo, pero mi propia prueba con varios miles de mensajes formateados en un ListView parecía sorprendentemente rápida. YMMV. El código está abajo:

XAML:

    

DO#

 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 mismo que he implementado usando Comportamiento. Código que se proporciona a continuación:

 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: espacio de nombres de importación

  

Luego, use el comportamiento como:

       

Este trabajo para mí:

XAML:

  

y su TextBlock XAML:

  

CÓDIGO:

 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 en su encuadernación de cuerdas:

 String Text = Text Bold;