How to append Text in xaml Textblock or rich textbox? - c#

I'm creating a chat app in Windows 8 Windows Metro Style App. I need to append the conversation in a richtextblock or textblock in XAML. Would somebody tell me the equivalent for this Code block?
public void AppendConversation(string str)
{
conversation.Append(str);
rtbConversation.Text = conversation.ToString();
rtbConversation.Focus();
rtbConversation.SelectionStart = rtbConversation.Text.Length - 1;
rtbConversation.ScrollToCaret();
rtbSendMessage.Focus();
}

Since WPF uses System.Windows.Controls instead of System.Windows.Forms, we must consider the following
1. System.Windows.Controls.RichTextBox does not have a property for Text to set its value, we may set its value creating a new class of TextRange since the control depends on TextPointer which can be defined using TextRange
string _Text = ""
new TextRange(
rtbConversation.Document.ContentStart,
rtbConversation.Document.ContentEnd).Text = _Text;
2. Selections in System.Windows.Controls.RichTextBox does not depend on int yet they are held by TextPointer. So, we can't say
rtbConversation.SelectionStart = rtbConversation.Text.Length - 1;
but we can say
int TextLength = new TextRange(
rtbConversation.Document.ContentStart,
rtbConversation.Document.ContentEnd).Text.Length;
TextPointer tr = rtbConversation.Document.ContentStart.GetPositionAtOffset(
TextLength - 1, LogicalDirection.Forward);
rtbConversation.Selection.Select(tr, tr);
which will do the same as rtbConversation.SelectionStart = rtbConversation.Text.Length - 1;
Remark: You can always retrieve the beginning of the selection in WPF using RichTextBox.Selection.Start
Notice: RichTextBox.Selection.Start outputs a class of name TextPointer but not a struct of name int
3. Finally, System.Windows.Controls.RichTextBox does not have a definition for ScrollToCaret();. In this case, we may use one of the following voids regarding your control rtbConversation
rtbConversation.ScrollToEnd();
rtbConversation.ScrollToHome();
rtbConversation.ScrollToHorizontalOffset(double offset);
rtbConversation.ScrollToVerticalOffset(double offset);
So, your void should look like this in WPF
Example
public void AppendConversation(string str)
{
conversation.Append(str) // Sorry, I was unable to detect the type of 'conversation'
new TextRange(rtbConversation.Document.ContentStart,
rtbConversation.Document.ContentEnd).Text =
conversation.ToString();
rtbConversation.Focus();
int TextLength = new TextRange(rtbConversation.Document.ContentStart,
rtbConversation.Document.ContentEnd).Text.Length;
TextPointer tr = rtbConversation.Document.ContentStart.GetPositionAtOffset(
TextLength - 1, LogicalDirection.Forward);
rtbConversation.Selection.Select(tr, tr);
rtbConversation.ScrollToEnd();
rtbSendMessage.Focus();
}
Thanks,
I hope you find this helpful :)

Related

Change TextRange font color, but not the text that will be written next to it

I am using TextRanges in a WPF RichTextBox and I want to change the font color of a TextRange, but that if then it is written text next to it, it has the original color.
For exmaple, I have a RichTextBox called richTextBox with font color black, and I use this code:
TextPointer start = richTextBox.Document.ContentStart;
TextPointer end = richTextBox.Document.ContentEnd;
new TextRange(start, end).ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Blue);
I want that if, after executing this code, someone adds text to the richTextBox, it is written in black, not in blue.
Is there any way to do this? Thank you!
I don't think you can set it directly so that the next caracter would be in a different color. (Someone please correct me if you know how).
But what you could do is subscribe to the TextChangedEvent of your RichTextBox and apply the new foreground color when the user add some text at the end.
The tricky part however is to detect how much was changed so you can apply the new foreground to this part only and to know if the changes made where done at the end of the text.
Where you execute the code for setting the foreground to blue:
TextPointer start = this.RTextBox.Document.ContentStart;
TextPointer end = this.RTextBox.Document.ContentEnd;
TextRange textRange = new TextRange(start, end);
textRange.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Blue);
//Storing the text to allow for comparison later
this.textSaved = textRange.Text;
this.wasTextColorInitialized = true;
Variables and new method added:
private bool wasTextColorInitialized = false;
private string textSaved;
/// <summary>
/// Remove some of the invisible caracter in a string to allowe for comparison
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
private string GetComparableString(string text)
{
return string.Join(string.Empty, Regex.Split(text, #"(?:\r\n|\n|\r)"));
}
Source for the very useful regex expression
https://stackoverflow.com/a/1982317/13448212
In the textChanged event, you have to check if the change is due to the user adding text, then determine if the changes were made at the end of the text:
private void RTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextPointer endOfDoc = this.RTextBox.Document.ContentEnd;
TextPointer startOfDoc = this.RTextBox.Document.ContentStart;
TextRange fullRange = new TextRange(startOfDoc, endOfDoc);
string newText = fullRange.Text;
//e.Changes.Count = 0 means that only a Property was changed (eg Foreground)
if (this.wasTextColorInitialized && e.Changes.Count != 0)
{
int lengthOfTextAdded = newText.Length - this.textSaved.Length;
//The text can end with "\r\n" and the text added will be before this hence the "GetComparableString" method
if (lengthOfTextAdded > 0 && newText.StartsWith(this.GetComparableString(this.textSaved)))
{
//in e.Changes, the changes are ordered, the latest being the furthest in the text
TextRange rangeOfTextAdded = new TextRange(endOfDoc.GetPositionAtOffset(-e.Changes.Last().AddedLength), endOfDoc);
rangeOfTextAdded.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Black);
}
else
{
//Nothing to do
}
}
else
{
//Nothing to do
}
this.textSaved = newText;
}
Documentation for e.Changes: https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.textchangedeventargs.changes?view=netcore-3.1#System_Windows_Controls_TextChangedEventArgs_Changes
I think Ostas's approach will work. I just would like to suggest a simple workaround.
It is to add a space at the end of RichTextBox when changing the color. After that, if someone adds a new text immediately after the existing text, the color will be blue. If someone adds a new text at the end (after the space), the color will be default.
TextPointer start = textBox.Document.ContentStart;
TextPointer end = textBox.Document.ContentEnd;
var range = new TextRange(start, end);
range.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Blue);
if (textBox.Document.Blocks.LastBlock is Paragraph paragraph)
paragraph.Inlines.Add(" ");

How do I get the start and end index from a selection inside RichTextBox?

I have a rich text box that I'm allowing user to highlight text. Text being loaded is coming from a simple plain text file. But I need to store the absolute start and end character position (relative to beginning of document) of the highlighted text so that when they save it, it can reload with highlights.
So far I can do this to apply the highlighting
private void textBox_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
HighlightWordInTextBox(this.textBox, this.textBox.Selection.ToString(), new SolidColorBrush(Colors.Yellow));
}
public void HighlightWordInTextBox(RichTextBox textbox, string word, SolidColorBrush color)
{
TextRange tr = new TextRange(this.textBox.Selection.Start, this.textBox.Selection.End);
tr.ApplyPropertyValue(TextElement.BackgroundProperty, color);
}
But I do not see anywhere inside the Start or End objects of Selection anything that provides the character position? Almost all the methods return another TextPointer - but how do you get the character position from a TextPointer?
Assuming all the text is loaded into a single
this.textBox.Document.Blocks.Add(new Paragraph(new Run(fullText)));
EDIT:
In the immediate window when debugging I can access something called CharOffset and Offset but cannot do so in the source code, it gives a compile error. Also those properties although present when inspecting the object at runtime, they are not in the documentation.
And yet...
You can find the index of the Selection's start and end with this...
var docStart = textBox.Document.ContentStart;
var selectionStart = textBox.Selection.Start;
var selectionEnd = textBox.Selection.End;
//these will give you the positions needed to apply highlighting
var indexStart = docStart.GetOffsetToPosition(selectionStart);
var indexEnd = docStart.GetOffsetToPosition(selectionEnd);
//these values will give you the absolute character positions relative to the very beginning of the text.
TextRange start = new TextRange(docStart, selectionStart);
TextRange end = new TextRange(docStart, selectionEnd);
int indexStart_abs = start.Text.Length;
int indexEnd_abs = end.Text.Length;

C# Load TextBlock content from a valriable (containing images)

I just wrote some nice functions, that allow me to add Emojis in a textbox with Inline.Add([Syste.Windows.Controls.Image]). It basically takes a TextBlock as an argument and appends the text/emojis that I want.
Icons is a Dictionary which maps from string to BitmapImage. (unable to use '<' and '>' here somehow)
private void AddTextToString(TextBlock block, string txt)
{
var textRun = new Run(txt);
textRun.BaselineAlignment = BaselineAlignment.Center;
block.Inlines.Add(textRun);
}
private void AddEmojiToString(TextBlock block, string txt)
{
if (!Icons.ContainsKey(txt))
return;
System.Windows.Controls.Image emo = new System.Windows.Controls.Image();
emo.Height = 15;
emo.Width = 15;
emo.VerticalAlignment = VerticalAlignment.Center;
emo.Source = Icons[txt];
block.Inlines.Add(emo);
}
Now I was wondering if there was an elegant way of saving this kind of text to a variable, so that I could bind the TextBlock Text dynamically to it.
Thank you very much in advance!
TextBlock.Inlines is property of type InlineCollection, you can use this type to keep the contents.

How do I "restore" the Caret Position in a Wpf RichTextBox?

After setting my RichTextBox's text to the string T, the Caret Position in the RichTextBox is "lost" (it goes to the start of it). Here's what I'm doing to try to "restore" it after it is "lost":
public static int GetCaretIndex(RichTextBox C)
{
return new TextRange(C.Document.ContentStart, C.CaretPosition).Text.Length;
}
...
int CaretIndex = GetCaretIndex(C); // Get the Caret position before setting the text of the RichTextBox
new TextRange(C.Document.ContentStart, C.Document.ContentEnd).Text = T; // Set the text of the RichTextBox
C.CaretPosition = C.Document.ContentStart.GetPositionAtOffset(CaretIndex, LogicalDirection.Forward); // Set the Caret Position based on the "Caret Index" variable
This code, however, does not work. The "restored" Caret is at a different position than the "original" one (always behind the "original" one for some reason).
"Saving" the RichTextBox's CaretPosition as a TextPointer doesn't seem to work either.
Can anyone provide me with an alternative way of "restoring" the Caret, or a way to fix the code above?
Seems to work (for me):
C.CaretPosition = C.Document.ContentStart;
C.CaretPosition = C.CaretPosition.GetPositionAtOffset(CaretIndex, LogicalDirection.Forward);
(I hate RichTextBox by the way.)
I was dealing with a similar issue recently and there is my solution. In my case, I'm creating a new RichTextBox.Document content and when I do this, I want to keep the caret position.
My idea was that caret offset functions are biased thanks to data structures used for text representation (Paragraphs, Runs, ...) which are also somehow calculated to offset position.
TextRange is a good approach to get exact caret position in the text. The problem lays in its restoration. But it gets easy when I know from which components my document is constructed. In my case, there are just Paragraphs and Runs.
What remains is to visit document structure, find an exact run where the caret should be and set the caret to correct position of found run.
Code:
// backup caret position in text
int backPosition =
new TextRange(RichTextBox.CaretPosition.DocumentStart, RichTextBox.CaretPosition).Text.Length;
// set new content (caret position is lost there)
RichTextBox.Document.Blocks.Clear();
SetNewDocumentContent(RichTextBox.Document);
// find position and run to which place caret
int pos = 0; Run caretRun = null;
foreach (var block in RichTextBox.Document.Blocks)
{
if (!(block is Paragraph para))
continue;
foreach (var inline in para.Inlines){
{
if (!(inline is Run run))
continue;
// find run to which place caret
if (caretRun == null && backPosition > 0)
{
pos += run.Text.Length;
if (pos >= backPosition){
caretRun = run;
break;
}
}
}
if (caretRun!=null)
break;
}
// restore caret position
if (caretRun != null)
RichTextBox.CaretPosition =
caretRun.ContentEnd.GetPositionAtOffset(backPosition - pos, LogicalDirection.Forward);
The code is not tested. I assembled it from various parts of my application. Let me know if you find any issue.
In my situation I have a RichTextBox with a single Paragraph that only allows entering text and line breaks. I change the structure of the RichTextBox ( by creating different coloured Run instances ) but not the text and restore after the change.
public static class CaretRestorer
{
public static void Restore(RichTextBox richTextBox, Action changer)
{
var caretPosition = GetCaretPosition(richTextBox);
changer();
Restore(richTextBox, caretPosition);
}
private static string GetFullText(RichTextBox richTextBox)
{
return new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd).Text;
}
private static int GetInlineTextLength(Inline inline)
{
if(inline is LineBreak)
{
return 2;
}
return new TextRange(inline.ContentStart, inline.ContentEnd).Text.Length;
}
private static void Restore(RichTextBox richTextBox,int caretPosition)
{
var inlines = GetInlines(richTextBox);
var accumulatedTextLength = 0;
foreach (var inline in inlines)
{
var inlineTextLength = GetInlineTextLength(inline);
var newAccumulatedTextLength = accumulatedTextLength + inlineTextLength;
if (newAccumulatedTextLength >= caretPosition)
{
TextPointer newCaretPosition = null;
if(inline is LineBreak)
{
newCaretPosition = inline.ContentEnd;
}
else
{
var diff = caretPosition - accumulatedTextLength;
newCaretPosition = inline.ContentStart.GetPositionAtOffset(diff);
}
richTextBox.CaretPosition = newCaretPosition;
break;
}
else
{
accumulatedTextLength = newAccumulatedTextLength;
}
}
}
private static int GetCaretPosition(RichTextBox richTextBox)
{
return new TextRange(richTextBox.Document.ContentStart, richTextBox.CaretPosition).Text.Length;
}
private static Paragraph GetParagraph(RichTextBox RichTextBox)
{
return RichTextBox.Document.Blocks.FirstBlock as Paragraph;
}
private static InlineCollection GetInlines(RichTextBox RichTextBox)
{
return GetParagraph(RichTextBox).Inlines;
}
}

How to set different forecolor to single label?

lblOperationType.Text = "Text";
Label l1 = new Label();
int len = lblOperationType.Text.Length - 1;
string b = Convert.ToString(lblOperationType.Text.ToCharArray()[0]);
string a = lblOperationType.Text.Substring(1, len);
l1.Text = (b);
l1.ForeColor = Color.Red;
lblOperationType.Text = l1.Text + a;
Is this correct code? I have to make a single label Text like 1st Letter should be red in color.
No, in general (i.e. without custom rendering yourself) a label only has a single foreground colour. Assuming this is Windows Forms, it sounds like you might want a RichTextBox instead - that allows multiple colours, fonts etc within a single control.
As an example:
using System;
using System.Drawing;
using System.Windows.Forms;
class Test
{
static void Main()
{
var rtb = new RichTextBox {
Text = "Test",
ReadOnly = true
};
rtb.Select(1, 3);
rtb.SelectionColor = Color.Red;
rtb.DeselectAll();
var form = new Form { Controls = { rtb } };
Application.Run(form);
}
}
That's not terribly nice code - it would be better to set the Rtf property directly, with the control codes necessary to set the colour, but I'm having a tricky time getting the exact format of the RTF right.

Categories