Un moyen de rendre un textblock WPF sélectionnable?
Je veux rendre le texte affiché dans le Witty, un client Twitter open source, sélectionnable. Il est actuellement affiché à l'aide d'un textblock personnalisé. J'ai besoin d'utiliser un TextBlock parce que je travaille avec les inlines du textblock pour afficher et formater le @username et les liens sous forme d'hyperliens. Une demande fréquente est de pouvoir copier-coller le texte. Pour ce faire, je dois rendre le TextBlock sélectionnable.
J'ai essayé de le faire fonctionner en affichant le texte en utilisant une lecture seule TextBox styled pour ressembler à un textblock mais cela ne fonctionnera pas dans mon cas car une zone de texte n'a pas de lignes. En d'autres termes, Je ne peux pas styliser ou formater le texte dans une zone de texte individuellement comme je peux le faire avec un TextBlock.
Des idées?
13 réponses
<TextBox Background="Transparent"
BorderThickness="0"
Text="{Binding Text, Mode=OneWay}"
IsReadOnly="True"
TextWrapping="Wrap" />
Toutes les réponses ici sont simplement en utilisant un TextBox
ou en essayant d'implémenter manuellement la sélection de texte, ce qui conduit à de mauvaises performances ou à un comportement non natif (caret clignotant dans TextBox
, Pas de support de clavier dans les implémentations manuelles, etc.)
Après des heures de fouille et de lecture du code source WPF , j'ai plutôt découvert un moyen d'activer la sélection de texte WPF native pour les contrôles TextBlock
(ou vraiment tout autre contrôle). La plupart des fonctionnalités autour de la sélection de texte est implémenté dans la classe système System.Windows.Documents.TextEditor
.
Pour activer la sélection de texte pour votre contrôle, vous devez faire deux choses:
-
Appelez
TextEditor.RegisterCommandHandlers()
Une fois pour enregistrer la classe gestionnaire d'événements Créer une instance de
TextEditor
pour chaque instance de votre classe et de passer à l'instance sous-jacente de votreSystem.Windows.Documents.ITextContainer
pour elle
Il est également nécessaire que la propriété Focusable
de votre contrôle soit définie sur True
.
C'est ça! Cela semble facile, mais malheureusement TextEditor
la classe est marquée comme interne. J'ai donc dû écrire un wrapper de réflexion autour de lui:
class TextEditorWrapper
{
private static readonly Type TextEditorType = Type.GetType("System.Windows.Documents.TextEditor, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
private static readonly PropertyInfo IsReadOnlyProp = TextEditorType.GetProperty("IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly PropertyInfo TextViewProp = TextEditorType.GetProperty("TextView", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly MethodInfo RegisterMethod = TextEditorType.GetMethod("RegisterCommandHandlers",
BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(Type), typeof(bool), typeof(bool), typeof(bool) }, null);
private static readonly Type TextContainerType = Type.GetType("System.Windows.Documents.ITextContainer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
private static readonly PropertyInfo TextContainerTextViewProp = TextContainerType.GetProperty("TextView");
private static readonly PropertyInfo TextContainerProp = typeof(TextBlock).GetProperty("TextContainer", BindingFlags.Instance | BindingFlags.NonPublic);
public static void RegisterCommandHandlers(Type controlType, bool acceptsRichContent, bool readOnly, bool registerEventListeners)
{
RegisterMethod.Invoke(null, new object[] { controlType, acceptsRichContent, readOnly, registerEventListeners });
}
public static TextEditorWrapper CreateFor(TextBlock tb)
{
var textContainer = TextContainerProp.GetValue(tb);
var editor = new TextEditorWrapper(textContainer, tb, false);
IsReadOnlyProp.SetValue(editor._editor, true);
TextViewProp.SetValue(editor._editor, TextContainerTextViewProp.GetValue(textContainer));
return editor;
}
private readonly object _editor;
public TextEditorWrapper(object textContainer, FrameworkElement uiScope, bool isUndoEnabled)
{
_editor = Activator.CreateInstance(TextEditorType, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance,
null, new[] { textContainer, uiScope, isUndoEnabled }, null);
}
}
J'ai également créé un SelectableTextBlock
dérivé de TextBlock
qui suit les étapes indiquées ci-dessus:
public class SelectableTextBlock : TextBlock
{
static SelectableTextBlock()
{
FocusableProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata(true));
TextEditorWrapper.RegisterCommandHandlers(typeof(SelectableTextBlock), true, true, true);
// remove the focus rectangle around the control
FocusVisualStyleProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata((object)null));
}
private readonly TextEditorWrapper _editor;
public SelectableTextBlock()
{
_editor = TextEditorWrapper.CreateFor(this);
}
}
Une Autre option serait de créer une propriété attachée pour TextBlock
pour activer la sélection du texte à la demande. Dans ce cas, pour désactiver à nouveau la sélection, il faut détacher un TextEditor
en utilisant l'équivalent de réflexion de ce code:
_editor.TextContainer.TextView = null;
_editor.OnDetach();
_editor = null;
J'ai été incapable de trouver un exemple de vraiment répondre à la question. Toutes les réponses ont utilisé une zone de texte ou RichTextbox. J'avais besoin d'une solution qui me permettait d'utiliser un TextBlock, et c'est la solution que j'ai créée.
Je crois que la bonne façon de le faire est d'étendre la classe TextBlock. C'est le code que j'ai utilisé pour étendre la classe TextBlock pour me permettre de sélectionner le texte et de le copier dans le presse-papiers. "sdo" est la référence d'espace de noms que j'ai utilisée dans le WPF.
WPF en utilisant Classe Étendue:
xmlns:sdo="clr-namespace:iFaceCaseMain"
<sdo:TextBlockMoo x:Name="txtResults" Background="Black" Margin="5,5,5,5"
Foreground="GreenYellow" FontSize="14" FontFamily="Courier New"></TextBlockMoo>
Code derrière pour la classe étendue:
public partial class TextBlockMoo : TextBlock
{
TextPointer StartSelectPosition;
TextPointer EndSelectPosition;
public String SelectedText = "";
public delegate void TextSelectedHandler(string SelectedText);
public event TextSelectedHandler TextSelected;
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
Point mouseDownPoint = e.GetPosition(this);
StartSelectPosition = this.GetPositionFromPoint(mouseDownPoint, true);
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
Point mouseUpPoint = e.GetPosition(this);
EndSelectPosition = this.GetPositionFromPoint(mouseUpPoint, true);
TextRange otr = new TextRange(this.ContentStart, this.ContentEnd);
otr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.GreenYellow));
TextRange ntr = new TextRange(StartSelectPosition, EndSelectPosition);
ntr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.White));
SelectedText = ntr.Text;
if (!(TextSelected == null))
{
TextSelected(SelectedText);
}
}
}
Exemple De Code De Fenêtre:
public ucExample(IInstanceHost host, ref String WindowTitle, String ApplicationID, String Parameters)
{
InitializeComponent();
/*Used to add selected text to clipboard*/
this.txtResults.TextSelected += txtResults_TextSelected;
}
void txtResults_TextSelected(string SelectedText)
{
Clipboard.SetText(SelectedText);
}
Créez ControlTemplate pour le TextBlock et placez une zone de texte à l'intérieur avec le jeu de propriétés readonly. Ou utilisez simplement TextBox et faites-le en lecture seule, alors vous pouvez changer la zone de texte.Style pour le faire ressembler à TextBlock.
Appliquer ce style à votre zone de texte et c'est tout (inspiré de cet article):
<Style x:Key="SelectableTextBlockLikeStyle" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="-2,0,0,0"/>
<!-- The Padding -2,0,0,0 is required because the TextBox
seems to have an inherent "Padding" of about 2 pixels.
Without the Padding property,
the text seems to be 2 pixels to the left
compared to a TextBlock
-->
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="False" />
<Condition Property="IsFocused" Value="False" />
</MultiTrigger.Conditions>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<TextBlock Text="{TemplateBinding Text}"
FontSize="{TemplateBinding FontSize}"
FontStyle="{TemplateBinding FontStyle}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
TextWrapping="{TemplateBinding TextWrapping}"
Foreground="{DynamicResource NormalText}"
Padding="0,0,0,0"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</MultiTrigger>
</Style.Triggers>
</Style>
Je ne suis pas sûr si vous pouvez sélectionner un TextBlock, mais une autre option serait d'utiliser un RichTextBox - c'est comme une zone de texte comme vous l'avez suggéré, mais supporte le formatage que vous voulez.
Selon Centre de développement Windows:
TextBlock.Propriété IsTextSelectionEnabled
[mise à jour pour les applications UWP sur Windows 10. Pour Windows 8.x articles, voir l'archive ]
Obtient ou définit une valeur qui indique si la sélection de texte est activée dans le TextBlock , soit par action de l'utilisateur, soit par appel API liée à la sélection.
TextBlock n'a pas de modèle. Donc, pour y parvenir, nous devons utiliser une zone de texte dont le style est modifié pour se comporter comme un textBlock.
<Style x:Key="TextBlockUsingTextBoxStyle" BasedOn="{x:Null}" TargetType="{x:Type TextBox}">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="1"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<TextBox BorderThickness="{TemplateBinding BorderThickness}" IsReadOnly="True" Text="{TemplateBinding Text}" Background="{x:Null}" BorderBrush="{x:Null}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Alors que la question dit "sélectionnable", je crois que les résultats intentionnels sont d'obtenir le texte dans le presse-papiers. Cela peut facilement et élégamment être réalisé en ajoutant un Menu contextuel et un élément de menu appelé copy qui met la valeur de la propriété Textblock Text dans le presse-papiers. Juste une idée de toute façon.
new TextBox
{
Text = text,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap,
IsReadOnly = true,
Background = Brushes.Transparent,
BorderThickness = new Thickness()
{
Top = 0,
Bottom = 0,
Left = 0,
Right = 0
}
};
J'ai implémenté SelectableTextBlock dans ma bibliothèque de contrôles opensource. Vous pouvez l'utiliser comme ceci:
<jc:SelectableTextBlock Text="Some text" />
Really nice and easy solution, exactly what I wanted !
J'apporte quelques petites modifications
public class TextBlockMoo : TextBlock
{
public String SelectedText = "";
public delegate void TextSelectedHandler(string SelectedText);
public event TextSelectedHandler OnTextSelected;
protected void RaiseEvent()
{
if (OnTextSelected != null){OnTextSelected(SelectedText);}
}
TextPointer StartSelectPosition;
TextPointer EndSelectPosition;
Brush _saveForeGroundBrush;
Brush _saveBackGroundBrush;
TextRange _ntr = null;
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
if (_ntr!=null) {
_ntr.ApplyPropertyValue(TextElement.ForegroundProperty, _saveForeGroundBrush);
_ntr.ApplyPropertyValue(TextElement.BackgroundProperty, _saveBackGroundBrush);
}
Point mouseDownPoint = e.GetPosition(this);
StartSelectPosition = this.GetPositionFromPoint(mouseDownPoint, true);
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
Point mouseUpPoint = e.GetPosition(this);
EndSelectPosition = this.GetPositionFromPoint(mouseUpPoint, true);
_ntr = new TextRange(StartSelectPosition, EndSelectPosition);
// keep saved
_saveForeGroundBrush = (Brush)_ntr.GetPropertyValue(TextElement.ForegroundProperty);
_saveBackGroundBrush = (Brush)_ntr.GetPropertyValue(TextElement.BackgroundProperty);
// change style
_ntr.ApplyPropertyValue(TextElement.BackgroundProperty, new SolidColorBrush(Colors.Yellow));
_ntr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.DarkBlue));
SelectedText = _ntr.Text;
}
}