I have a RichTextBox (RTB) and next to it a DataGrid (DG). A user can paste a list of IDs into the RTB to perform some bulk actions on them. In order that users can be aware if an ID they passed into the RTB isn't in the DG, I want to change the colour of the text.
This is what I have at the moment:
private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
//TODO: Enforce 13 character line limit and replace any delimiters with new line
//Get all text
RichTextBox rtb = (RichTextBox)sender;
TextRange textRange = new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd);
//Split into lines
string[] rtbLines = textRange.Text.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in rtbLines)
{
//remove comms, but could some others
line.Replace(",", "");
//Get the index of the start of an ID
int startPos = textRange.Text.IndexOf(line);
//this should be unncessary?
if (startPos > -1)
{
//Get a pointer to text we found
TextPointer tp = textRange.Start.GetPositionAtOffset(startPos, LogicalDirection.Backward);
//this should be unnecessary?
if (tp != null)
{
//Get the line start position
TextPointer tpLine = tp.GetLineStartPosition(0); //This throws StackOverflowException sometimes?
//Get start of next line
TextPointer testLineEnd = tp.GetLineStartPosition(1);
//In case we're at the end, default to end of the document
TextPointer tpLineEnd = (testLineEnd ?? tp.DocumentEnd).GetInsertionPosition(LogicalDirection.Backward);
//Get text range between line start/end
TextRange lineText = new TextRange(tpLine, tpLineEnd);
//Cast the data and check if our text is in it so we can check it
if (UnassignedMPANsListView.ItemsSource != null && UnassignedMPANsListView.ItemsSource.Cast<CrmUnassignedMPAN>().ToList().Any(x => x.MPAN == line))
{
lineText.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Red);
}
else
{
lineText.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Black);
}
}
}
}
}
The issue is that often the line TextPointer tpLine = tp.GetLineStartPosition(0); causes a StackOverflowException. It seems to be only when the user is typing an ID; pasting seems to be OK.
My thought is that this is caused because the RichTextBox_TextChanged event is being called repeatedly, whereas pasting is a single hit, but I don't understand why that would cause this exception rather than just become very slow.
Also open to any other suggestions for how to implement this feature. My key requirements are that a user can paste a list, type them 1 by 1, and the rows can be programmatically coloured indepedently.
You have exception because you have recursive call at the place lineText.ApplyPropertyValue(TextElement.ForegroundProperty this call does modify your text. To avoid it you can simply add the flag:
bool changeIsRunning = false;
private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if(changeIsRunning)
{
return;
}
changeIsRunning = true;
try
{
//Put your logic here.
}
finally
{
changeIsRunning = false;
}
}
Be aware, that there are not only visible text, but also invisible element tags in the text(e.g. for the color).
I am using C# WinForm, and I have a RichTextBox that I am trying to make look like a C# script.
Means when using specific words, I want them to be colored. When they edit the word by changing it, I want it to go back to be black.
My approach works, but it really messy and cause bugs when the a scroll option is created and needed to be used to see the code below. (When typing, pretty much the richtextbox jumps up and down without stop)
private void ScriptRichTextBox_TextChanged(object sender, EventArgs e)
{
ScriptTextChange = ScriptRichTextBox.Text;
ScriptColorChange();
}
private void ScriptColorChange()
{
int index = ScriptRichTextBox.SelectionStart;
ScriptRichTextBox.Text = ScriptTextChange; //Only way I found to make the all current text black again, SelectAll() didn't work well.
ScriptRichTextBox.SelectionStart = index;
String[] coloredNames = {"Main", "ClickMouseDown", "ClickMouseUp", "PressKey", "StopMoving", "Delay", "GoRight", "GoLeft", "GoUp", "GoDown", "MousePosition", "LastHorizontalDirection", "LastVerticalDirections", "CurrentDirection", "Directions" };
String[] coloredNames2 = { "cm.", "if", "else", "while", "switch", "case", "break", "return", "new" };
String[] coloredNames3 = { "MyPosition", "MyHp", "MyMp", "OtherPeopleInMap", ".RIGHT", ".LEFT", ".UP", ".DOWN", ".STOP_MOVING" };
foreach (String s in coloredNames)
this.CheckKeyword(s, Color.LightSkyBlue, 0);
foreach (String s in coloredNames2)
this.CheckKeyword(s, Color.Blue, 0);
foreach (String s in coloredNames3)
this.CheckKeyword(s, Color.DarkGreen, 0);
}
private void CheckKeyword(string word, Color color, int startIndex)
{
if (this.ScriptRichTextBox.Text.Contains(word))
{
int index = 0;
int selectStart = this.ScriptRichTextBox.SelectionStart;
while ((index = this.ScriptRichTextBox.Text.IndexOf(word, (index + 1))) != -1)
{
this.ScriptRichTextBox.Select((index + startIndex), word.Length);
this.ScriptRichTextBox.SelectionColor = color;
this.ScriptRichTextBox.Select(selectStart, 0);
this.ScriptRichTextBox.SelectionColor = Color.Black;
}
}
}
I refactored your code a little to hopefully demonstrate a better approach to colouring the text. It is also not optimal to instantiate your string arrays every time you fire the TextChanged event.
Updated:The idea is to build up a word buffer that will be matched with your set of words when typing.
The buffer records each key and if it .IsLetterOrDigit it adds it to the StringBuilder buffer. The buffer has some additional bugs, with recording key press values and not removing recorded chars if you hit backspace etc..
Instead of the word buffer, use RegEx to match any of the words in your reserve word list. Build up the reserve word RegEx so you end up with something like \b(word|word2|word3....)\b This is done in the code in the BuildRegExPattern(..) method.
Once you hit any key other than a letter or number the buffer is checked for content and if the content matches a word then only the text right before the cursor in the ScriptRichTextBox.Text is checked and changed.
Remove the .(dots) from the reserve words as this just complicates the matching criteria. The RegEx in the built up patters will match the words exactly, so if you type something like FARRIGHT or cms the words will not partially change colour.
As an extra I also covered the paste process pressing Ctrl+V because it is a bit of a pain in WinForms and will probably happen quite often.
There are older questions eg. this one that cover the scrolling behaviour, where it shows how to interop by adding the [System.Runtime.InteropServices.DllImport("user32.dll")] attribute, but it can be done without it.
To prevent all the scroll jumping you can make use of the .DefWndProc(msg) method on the form. this question pointed me towards the WM_SETREDRAW property.
There is also this list of other properties that can be set.
The full implementation is this:
public partial class Form1 : Form
{
private readonly string[] _skyBlueStrings;
private readonly string[] _blueStrings;
private readonly string[] _greenStrings;
//for pasting
bool _IsCtrl;
bool _IsV;
//value to fix the colour not setting first character after return key pressed
int _returnIdxFix = 0;
//regex patterns to use
string _LightBlueRegX = "";
string _BlueRegX = "";
string _GreenRegX = "";
//match only words
Regex _rgxAnyWords = new Regex(#"(\w+)");
//colour setup
Color _LightBlueColour = Color.LightSkyBlue;
Color _BlueColour = Color.Blue;
Color _GreenColour = Color.DarkGreen;
Color _DefaultColour = Color.Black;
public Form1()
{
InitializeComponent();
_skyBlueStrings = new string[] { "Main", "ClickMouseDown", "ClickMouseUp", "PressKey", "StopMoving", "Delay", "GoRight", "GoLeft", "GoUp", "GoDown", "MousePosition", "LastHorizontalDirection", "LastVerticalDirections", "CurrentDirection", "Directions" };
_blueStrings = new string[] { "cm", "if", "else", "while", "switch", "case", "break", "return", "new" };
_greenStrings = new string[] { "MyPosition", "MyHp", "MyMp", "OtherPeopleInMap", "RIGHT", "LEFT", "UP", "DOWN", "STOP_MOVING" };
_LightBlueRegX = BuildRegExPattern(_skyBlueStrings);
_BlueRegX = BuildRegExPattern(_blueStrings);
_GreenRegX = BuildRegExPattern(_greenStrings);
}
string BuildRegExPattern(string[] keyworkArray)
{
StringBuilder _regExPatern = new StringBuilder();
_regExPatern.Append(#"\b(");//beginning of word
_regExPatern.Append(string.Join("|", keyworkArray));//all reserve words
_regExPatern.Append(#")\b");//end of word
return _regExPatern.ToString();
}
private void ProcessAllText()
{
BeginRtbUpdate();
FormatKeywords(_LightBlueRegX, _LightBlueColour);
FormatKeywords(_BlueRegX, _BlueColour);
FormatKeywords(_GreenRegX, _GreenColour);
//internal function to process words and set their colours
void FormatKeywords(string regExPattern, Color wordColour)
{
var matchStrings = Regex.Matches(ScriptRichTextBox.Text, regExPattern);
foreach (Match match in matchStrings)
{
FormatKeyword(keyword: match.Value, wordIndex: match.Index, wordColour: wordColour);
}
}
EndRtbUpdate();
ScriptRichTextBox.Select(ScriptRichTextBox.Text.Length, 0);
ScriptRichTextBox.Invalidate();
}
void ProcessWordAtIndex(string fullText, int cursorIdx)
{
MatchCollection anyWordMatches = _rgxAnyWords.Matches(fullText);
if (anyWordMatches.Count == 0)
{ return; } // no words found
var allWords = anyWordMatches.OfType<Match>().ToList();
//get the word just before cursor
var wordAtCursor = allWords.FirstOrDefault(w => (cursorIdx - _returnIdxFix) == (w.Index + w.Length));
if (wordAtCursor is null || string.IsNullOrWhiteSpace(wordAtCursor.Value))
{ return; }//no word at cursor or the match was blank
Color wordColour = CalculateWordColour(wordAtCursor.Value);
FormatKeyword(wordAtCursor.Value, wordAtCursor.Index, wordColour);
}
private Color CalculateWordColour(string word)
{
if (_skyBlueStrings.Contains(word))
{ return _LightBlueColour; }
if (_blueStrings.Contains(word))
{ return _BlueColour; }
if (_greenStrings.Contains(word))
{ return _GreenColour; }
return _DefaultColour;
}
private void FormatKeyword(string keyword, int wordIndex, Color wordColour)
{
ScriptRichTextBox.Select((wordIndex - _returnIdxFix), keyword.Length);
ScriptRichTextBox.SelectionColor = wordColour;
ScriptRichTextBox.Select(wordIndex + keyword.Length, 0);
ScriptRichTextBox.SelectionColor = _DefaultColour;
}
#region RichTextBox BeginUpdate and EndUpdate Methods
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
//wait until the rtb is visible, otherwise you get some weird behaviour.
if (ScriptRichTextBox.Visible && ScriptRichTextBox.IsHandleCreated)
{
if (m.LParam == ScriptRichTextBox.Handle)
{
rtBox_lParam = m.LParam;
rtBox_wParam = m.WParam;
}
}
}
IntPtr rtBox_wParam = IntPtr.Zero;
IntPtr rtBox_lParam = IntPtr.Zero;
const int WM_SETREDRAW = 0x0b;
const int EM_HIDESELECTION = 0x43f;
void BeginRtbUpdate()
{
Message msg_WM_SETREDRAW = Message.Create(ScriptRichTextBox.Handle, WM_SETREDRAW, (IntPtr)0, rtBox_lParam);
this.DefWndProc(ref msg_WM_SETREDRAW);
}
public void EndRtbUpdate()
{
Message msg_WM_SETREDRAW = Message.Create(ScriptRichTextBox.Handle, WM_SETREDRAW, rtBox_wParam, rtBox_lParam);
this.DefWndProc(ref msg_WM_SETREDRAW);
//redraw the RichTextBox
ScriptRichTextBox.Invalidate();
}
#endregion
private void ScriptRichTextBox_TextChanged(object sender, EventArgs e)
{
//only run all text if it was pasted NOT ON EVERY TEXT CHANGE!
if (_IsCtrl && _IsV)
{
_IsCtrl = false;
ProcessAllText();
}
}
protected void ScriptRichTextBox_KeyPress(object sender, KeyPressEventArgs e)
{
if (!char.IsLetterOrDigit(e.KeyChar))
{
//if the key was enter the cursor position is 1 position off
_returnIdxFix = (e.KeyChar == '\r') ? 1 : 0;
ProcessWordAtIndex(ScriptRichTextBox.Text, ScriptRichTextBox.SelectionStart);
}
}
private void ScriptRichTextBox_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)
{
if (e.KeyCode == Keys.ControlKey)
{
_IsCtrl = true;
}
if (e.KeyCode == Keys.V)
{
_IsV = true;
}
}
private void ScriptRichTextBox_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e)
{
if (e.KeyCode == Keys.ControlKey)
{
_IsCtrl = false;
}
if (e.KeyCode == Keys.V)
{
_IsV = false;
}
}
}
It looks like this when you paste some "code" with keywords:
and typing looks like this:
Ok after 2 days of not finding something that actually works good or has annoying bugs. I managed to find a solution myself after a big struggle of trying to make it work. The big idea is people try to edit all the RichTextBox words at once, which cause bugs. Why to edit all of the rich text box when you can do your checks on the current word only to get the same result. Which is what I did, I checked if any of my array strings is in the current word, and colored all of them.
private void ScriptRichTextBox_TextChanged(object sender, EventArgs e)
{
FindStringsInCurrentWord();
}
private void FindStringsInCurrentWord()
{
RichTextBox script = ScriptRichTextBox;
String finalWord, forwards, backwards;
int saveLastSelectionStart = script.SelectionStart;
int index = script.SelectionStart;
String[] coloredNames = { "Main", "ClickMouseDown", "ClickMouseUp", "PressKey", "StopMoving", "Delay", "GoRight", "GoLeft", "GoUp", "GoDown", "MousePosition", "LastHorizontalDirection", "LastVerticalDirections", "CurrentDirection", "Directions" };
String[] coloredNames2 = { "cm.", "if", "else", "while", "switch", "case", "break", "return", "new" };
String[] coloredNames3 = { "MyPosition", "MyHp", "MyMp", "OtherPeopleInMap", ".RIGHT", ".LEFT", ".UP", ".DOWN", ".STOP_MOVING" };
String[] arr2 = coloredNames.Union(coloredNames2).ToArray();
Array arrAll = arr2.Union(coloredNames3).ToArray(); //Gets all arrays together
Array[] wordsArray = { coloredNames, coloredNames2, coloredNames3 }; //All found strings in the word
List<String> wordsFoundList = new List<String>();
int foundChangedColor = 0;
int wordsFound = 0;
char current = (char)script.GetCharFromPosition(script.GetPositionFromCharIndex(index)); //Where the editor thingy is
//Check forward text where he uses space and save text
while (!System.Char.IsWhiteSpace(current) && index < script.Text.Length)
{
index++;
current = (char)script.GetCharFromPosition(script.GetPositionFromCharIndex(index));
}
int lengthForward = index - saveLastSelectionStart;
script.Select(script.SelectionStart, lengthForward);
forwards = script.SelectedText;
//Debug.WriteLine("Forwards: " + forwards);
script.SelectionStart = saveLastSelectionStart;
this.ScriptRichTextBox.Select(script.SelectionStart, 0);
index = script.SelectionStart;
current = (char)script.GetCharFromPosition(script.GetPositionFromCharIndex(index));
int length = 0;
//Check backwords where he uses space and save text
while ((!System.Char.IsWhiteSpace(current) || length == 0) && index > 0 && index <= script.Text.Length)
{
index--;
length++;
current = (char)script.GetCharFromPosition(script.GetPositionFromCharIndex(index));
}
script.SelectionStart -= length;
script.Select(script.SelectionStart + 1, length - 1);
backwards = script.SelectedText;
//Debug.WriteLine("Backwards: " + backwards);
script.SelectionStart = saveLastSelectionStart;
this.ScriptRichTextBox.Select(saveLastSelectionStart, 0);
this.ScriptRichTextBox.SelectionColor = Color.Black;
finalWord = backwards + forwards; //Our all word!
//Debug.WriteLine("WORD:" + finalWord);
//Setting all of the word black, after it coloring the right places
script.Select(index + 1, length + lengthForward);
script.SelectionColor = Color.Black;
foreach (string word in arrAll)
{
if (finalWord.IndexOf(word) != -1)
{
wordsFound++;
wordsFoundList.Add(word);
script.Select(index + 1 + finalWord.IndexOf(word), word.Length);
if (coloredNames.Any(word.Contains))
{
script.SelectionColor = Color.LightSkyBlue;
foundChangedColor++;
}
else if (coloredNames2.Any(word.Contains))
{
script.SelectionColor = Color.Blue;
foundChangedColor++;
}
else if (coloredNames3.Any(word.Contains))
{
script.SelectionColor = Color.DarkGreen;
foundChangedColor++;
}
//Debug.WriteLine("Word to edit: " + script.SelectedText);
this.ScriptRichTextBox.Select(saveLastSelectionStart, 0);
this.ScriptRichTextBox.SelectionColor = Color.Black;
}
}
//No strings found, color it black
if (wordsFound == 0)
{
script.Select(index + 1, length + lengthForward);
script.SelectionColor = Color.Black;
//Debug.WriteLine("WORD??: " + script.SelectedText);
this.ScriptRichTextBox.Select(saveLastSelectionStart, 0);
this.ScriptRichTextBox.SelectionColor = Color.Black;
}
}
I need to make a multi line textbox that changes the back ground for charaters after the line becomes longer than 80 characters. So that if someone types a sentence of 85 characters the last 5 characters will have a yellow background. I would like it to make this feature part of the style because currently we are tying to do this with logic in the code behind and it lags when someone types quickly.
current highlighting imlp
private void HighlightLines()
{
try
{
//// x is the distance in the row from the left side of the Textbox. e.g. Each character is one unit.
int x = 0;
//// y is the distance in the columns from the top of the Textbox. e.g. Each character is one unit.
int y = 0;
//// lines is the number of newline characters found in the Text.
int lines = 0;
//// Point 1 is the starting point of the range that needs to be highlighted.
TextPointer point1 = this.Document.ContentStart;
//// Point 2 is the end point of the range that needs to be highlighted.
TextPointer point2 = this.Document.ContentStart;
//// Range is the distance from Point 1 to Point 2 needed to apply the Yellow color to the area past 69 characters.
TextRange range;
//// Additional Ranges is the collection of all the ranges that need to be Yellow.
this.AdditionalRanges = new ObservableCollection<TextRange>();
//// Count the number of lines.
for (int i = 0; i < this.Text.Length; i++)
{
if (this.Text[i] == '\n')
{
lines++;
this.AdditionalRanges.Add(new TextRange(this.Document.ContentStart, this.Document.ContentEnd));
}
}
//// This map is used to differentiate which lines need to be colored. (True means the range is over 69 characters. False means the opposite).
bool[] map = new bool[lines];
//// Traverse the whole text.
for (int i = 0; i < this.Text.Length; i++)
{
var currentCharacter = this.Text[i];
var newLineCharacter = '\n';
if (currentCharacter == newLineCharacter)
{
var pointDifference = point1.GetOffsetToPosition(point2);
point1 = point1.GetPositionAtOffset(pointDifference);
x = 0;
y++;
}
else if (x > 69)
{
range = new TextRange(point1, point2);
this.AdditionalRanges[y] = range;
map[y] = true;
if (point2.GetNextInsertionPosition(LogicalDirection.Forward) != null)
{
point2 = point2.GetNextInsertionPosition(LogicalDirection.Forward);
}
}
else if (point1.GetNextInsertionPosition(LogicalDirection.Forward) != null && point2.GetNextInsertionPosition(LogicalDirection.Forward) != null)
{
point1 = point1.GetNextInsertionPosition(LogicalDirection.Forward);
point2 = point2.GetNextInsertionPosition(LogicalDirection.Forward);
x++;
}
}
//// Make everything white.
foreach (var item in this.AdditionalRanges)
{
item.ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.White);
}
//// Make the appropriate ranges Yellow.
for (int i = 0; i < this.AdditionalRanges.Count; i++)
{
if (map[i])
{
this.AdditionalRanges[i].ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Yellow);
}
}
}
catch (Exception e)
{
// drop exception. this has only broke once and we dont exactly know why.
}
}
It's probably lagging because your HighlightLines() method is scanning the entire document every time, and I assume it gets called after every keypress. Ideally you want to only re-scan the portions of the text that have changed. Fortunately, the TextChanged event provides the exact offset of the changes.
The example code below was written to work with a RichTextBox but you should be able to adapt it. Also, it looks like your code was checking for 69 characters instead of 80, so this does the same:
RichTextBox txt;
...
bool suppressChanges = false;
private void Txt_TextChanged(object sender, TextChangedEventArgs e)
{
if (!suppressChanges)
{
// suppress changes because changing highlights will trigger the event again
suppressChanges = true;
foreach (var change in e.Changes)
{
var changeStart = txt.Document.ContentStart.GetPositionAtOffset(change.Offset);
TextRange changedRange;
if (change.AddedLength > 0)
changedRange = new TextRange(changeStart, changeStart.GetPositionAtOffset(change.AddedLength));
else
changedRange = new TextRange(changeStart, changeStart);
SetRangeColors(changedRange);
}
//unsuppress changes
suppressChanges = false;
}
}
void SetRangeColors(TextRange range)
{
// Scan one line at a time starting with the beginning of the range
TextPointer current = range.Start.GetLineStartPosition(0);
while (current != null && current.CompareTo(range.End) < 0)
{
// find the next line or the end of the document
var nextLine = current.GetLineStartPosition(1, out int lines);
TextPointer lineEnd;
if (lines > 0)
lineEnd = nextLine.GetNextInsertionPosition(LogicalDirection.Backward);
else
lineEnd = txt.Document.ContentEnd;
var lineRange = new TextRange(current, lineEnd);
// clear properties first or the offsets won't match the characters
lineRange.ClearAllProperties();
var lineText = lineRange.Text;
if (lineText.Length > 69)
{
var highlight = new TextRange(current.GetPositionAtOffset(70), lineEnd);
highlight.ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Yellow);
}
// advance to the next line
current = lineEnd.GetLineStartPosition(1);
}
}
text box area that automatically cuts a long paragraph text into 30 characters sentences
i am trying run this code but occurs exception [exception of type 'System.StackOverflowException']
private void txtCutParagraph_TextChanged(object sender, EventArgs e)
{
int limitNum = 30;
string sentence = txtCutParagraph.Text;
string[] words = sentence.Split(' ');
string line = "";
foreach (string word in words)
{
if ((line + word).Length > limitNum)
{
newLine += line + "\r\n";
line = "";
}
line += word + " ";
}
if (line.Length > 0)
newLine += line + "\r\n";
txtCutParagraph.Text = newLine;
}
If the form is freezing is because that txtCutParagraph_TextChanged event is firing infinitely, because you are changing the text of textbox at the end of the event: txtCutParagraph.Text = newLine;, so it means that change text in the textbox, and the event will fire again and again.
To prevent this form from freezing please move your code to another event of textbox, named KeyPress as:
private void txtCutParagraph_KeyPress(object sender, KeyPressEventArgs e)
{
int limitNum = 30;
string sentence = txtCutParagraph.Text;
string[] words = sentence.Split(' ');
string line = "";
foreach (string word in words)
{
if ((line + word).Length > limitNum)
{
newLine += line + "\r\n";
line = "";
}
line += word + " ";
}
if (line.Length > 0)
newLine += line + "\r\n";
txtCutParagraph.Text = newLine;
}
What you are trying to do is called Word wrapping. TextBox class has Wordwrap option by default. unfortunately you cant limit number of characters per line.
You have to write an algorithm instead. I have noticed that your algorithm does not work correctly. so i decided to write one my self (as it was a good practice!). It is hard to handle all situations that can happen inside text formatting. I tried my best anyway you have to write one your self if you are not satisfied with results.
Before using this algorithm you have to disable Wordwrap feature of Textbox. So they will not Interfere each other. In InitializeComponent inside Form Designer add this line.
this.textBox1.WordWrap = false;
Now use this algorithm to do it for you! Note that textbox1 is a multi line text box.
private StringBuilder stringBuilder = new StringBuilder();
private bool _isInsideTextChanged = false;
private const int MaximumChars = 30; // Maximum characters
private StringBuilder WrapText(StringBuilder text, ref int position)
{
StringBuilder newStringBuilder = new StringBuilder(text.ToString());
int charsPerLine = 0;
int lastSpace = -1; // index of last space per line
for (int i = 0; i < newStringBuilder.Length; i++)
{
if (newStringBuilder[i] == ' ')
{
if (newStringBuilder.Length > i + 2 && newStringBuilder.ToString(i + 1, 2) == "\r\n")
{
if (newStringBuilder.Length > i + 3)
{
int next = newStringBuilder.ToString().IndexOf(' ', i + 3);
if (next != -1 && charsPerLine + next - i <= MaximumChars || charsPerLine + newStringBuilder.Length - i - 2 <= MaximumChars)
{
newStringBuilder.Remove(i + 1, 2);
if (i <= textBox1.SelectionStart)
{
position -= 2;
}
continue;
}
}
i++;
continue;
}
if (newStringBuilder.Length > i + 1 && newStringBuilder[i + 1] != ' ')
{
lastSpace = i;
}
}
if (newStringBuilder[i] == '\n' || newStringBuilder[i] == '\r')
{
lastSpace = -1;
charsPerLine = 0;
}
if (++charsPerLine > MaximumChars && lastSpace != -1)
{
newStringBuilder.Insert(lastSpace + 1, "\r\n");
if (lastSpace <= textBox1.SelectionStart)
{
position += 2;
}
charsPerLine = i - lastSpace;
lastSpace = -1;
i++;
}
}
return newStringBuilder;
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
if (_isInsideTextChanged) return;
_isInsideTextChanged = true;
stringBuilder.Clear();
stringBuilder.Append(textBox1.Text);
int position = textBox1.SelectionStart;
string newText = WrapText(stringBuilder, ref position).ToString();
textBox1.Text = newText;
textBox1.SelectionStart = position;
_isInsideTextChanged = false;
}
Here is the test that shows the results.
How this wroks?
This algorithm will count the number of characters from last line break index (default value is 0) up to last space character index per line.(default value is -1 means no space in that line). Then it will put line break after last space if the number of characters on that line is more than 30. How ever this algorithm test other things too to better handle text formatting.
This is done every time a textbox value is changed. StringBuilder is used instead of string to increase performance.
To prevent stack overflow exception as described by #KhaksarWeqar I used a boolean value _isInsideTextChanged with TextChanged event:
private bool _isInsideTextChanged = false;
private void textBox1_TextChanged(object sender, EventArgs e)
{
if (_isInsideTextChanged) return; // return if was inside TextChanged.
_isInsideTextChanged = true; // inside TextChanged
// Do stuff...
_isInsideTextChanged = false; // outside TextChanged
}
There is also a better way explained on wiki. you can create your own even better!. https://en.wikipedia.org/wiki/Line_wrap_and_word_wrap
Hey Im creating my own coding language and I already have the entire application set up with save, open, close, new, etc.
In order for the "Run" part to work, I need a way to scan and test every single line in richTextBox1.
Maybe for past Java users, something along the lines of the "java-util-scanner," but easier to use for testing each line.
Does anyone know a way to do this, where a string "scannedString" would be tested as so:
if(scannedString == "#load(moduleFiles)") {
//action here
}
string scannedStringNextLine = ???
if(scannedStringNextLine = "") {
//action here
}
Eventually it would look more like this:
if(scannedString == "code1" || scannedString == "code2" etc... ) {
if(scannedString == "code1") {
//action here
}
} else {
//error message
}
hope this is enough information...
To get lines of code of the RichTextBox you can split the content by a new line symbol:
var lines = this.richTextBox.Text.Split('\n').ToList();
The lines are in order of appearance. Then you can go through lines in a for or foreach loop:
foreach (var line in lines)
{
// access a line, evaluate it, etc.
}
One way to do it is to split the text on the newline charaters and then parse each line for the text you care about:
private void btnScan_Click(object sender, EventArgs e)
{
var code = richTextBox1.Text;
foreach (var line in code.Split(new []{'\n','\r'},
StringSplitOptions.RemoveEmptyEntries))
{
CheckForLoad(line);
}
}
void CheckForLoad(string line)
{
if (string.IsNullOrWhiteSpace(line)) return;
int i = line.IndexOf("#load");
if (i < 0) return;
int openParen = line.IndexOf("(", i + 1);
if (openParen < 0) return;
int closeParen = line.IndexOf(")", openParen + 1);
if (closeParen < 0) return;
string modules = line.Substring(openParen + 1, closeParen - openParen - 1);
MessageBox.Show(string.Format("Loading modules: {0}", modules));
}