This is very easy using two textboxes laid directly over one another. The rear one is a normal textbox with extra padding and transparent text. The front one has your extra characters but has its borders hidden and is IsHitTestVisible=False
and Focusable=False
so it doesn't interact with the user. The user interacts exclusively with the rear textbox but the front textbox is the one that displays the text. A binding with a value converter keeps the front textbox displaying exactly what the rear textbox displays, plus the extra characters.
This is how it would look:
<ControlTemplate x:Key="TextBoxWithExtraCharacters" TargetType="{x:Type TextBox}">
<ControlTemplate.Resources>
<!-- Remove the border from the inner textboxes -->
<ControlTemplate TargetType="{x:Type TextBox}">
<Decorator x:Name="PART_Content" />
</ControlTemplate>
</ControlTemplate.Resources>
<!-- Now add our own border -->
<Border
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="true">
<!-- Scrolling must happen at this level so both text boxes scroll simultaneously -->
<ScrollViewer>
<Grid>
<!-- Rear textbox provides editing and user interaction but the text is transparent -->
<TextBox
Margin="10,0,10,0"
Foreground="Transparent"
Text="{Binding Text, RelativeSource={RelativeSource TemplatedParent}"
Background="{x:Null}"
IsReadOnly="{TemplateBinding IsReadOnly}"
IsUndoEnabled="{TemplateBinding IsUndoEnabled}"
AcceptsReturn="{TemplateBinding AcceptsReturn}"
AcceptsTab="{TemplateBinding AcceptsTab}"
/>
<!-- Front textbox displays modified text but does not interact with user -->
<TextBox
IsHitTestVisible="false"
Focusable="false"
Text="{Binding Text, RelativeSource={RelativeSource TemplatedParent},
Converter={x:Static ExtraCharacterConverter.Instance}"
Background="{x:Null}"
IsReadOnly="{TemplateBinding IsReadOnly}"
IsUndoEnabled="{TemplateBinding IsUndoEnabled}"
AcceptsReturn="{TemplateBinding AcceptsReturn}"
AcceptsTab="{TemplateBinding AcceptsTab}"
/>
</Grid>
</ScrollViewer>
</Border>
</ControlTemplate>
ExtraCharacterConverter is a simple IValueConverter
class that implements the Convert
method by taking the given string, appending quotes or // or whatever to it, and returning the result.
Note that I hard-coded a left and right margin of 10 units on the rear textbox, which assumes a particular width for the quote characters. This should be exactly the width of the added characters to make the text line up correctly. You want to get this right, or your caret and text selection positioning will be wrong. Also note that the correct value will change as you vary your font size and your choice of extra characters.
An easy alternative to hard-coding the margin would be to set it to a multi-binding on FontSize, FontFamily, FontWeight
, etc, then use a IMultiValueConverter
to compute the proper margin given this these values.
Note: This solution is slighly unsatisfactory when it comes to the color scheme for text selection. This can be fixed, but it requires a more complex solution: The rear text box is the same but its text is not invisible. The front text box is replaced with a RichTextBox (or TextBlock) whose content is computed dynamically to be the text with extra characters, but the regular text transparent. Because it is a RichTextBox the extra characters can be visible while the others are transparent.