WPF TextBox ScrollToLine not updating if visible - c#

I have a Navigation-bar in my program that allows you to navigate the different sections in my TextBox, but the problem I have is that this doesn't work if the Text I am scrolling to is already visible on the screen.
Like in this example, if I try to jump from Section 1 to Section 3, it won't work as it's already visible.
But, in this example if I jump to Section 3, it works fine as it's not already visible.
The scrolling function I use is very simple:
if (nLine > 0 && nLine <= textBox.LineCount)
textBox.ScrollToLine(nLine - 1);
I hope that someone can shed some light on an alternative solution that allows me to scroll even if the text is already visible.
Edit: Added solution.
This is a code snippet from my project.
private static void ScrollToLineCallback(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
var textBox = (TextBox)target;
int newLineValue;
if (Int32.TryParse(e.NewValue.ToString(), out newLineValue))
{
if (newLineValue > 0 && newLineValue <= textBox.LineCount) // Validate
{
textBox.ScrollToLine(newLineValue - 1); // Scroll to Line
// Check and see if we are at the line we want.
if (textBox.GetFirstVisibleLineIndex() <= newLineValue && textBox.GetLastVisibleLineIndex() >= newLineValue)
{
// If not lets move to the desired location
int newLineCorrectionValue = newLineValue - textBox.GetFirstVisibleLineIndex() - 2; // How much further do we need to scroll down?
for (int i = 0; i < newLineCorrectionValue; i++)
{
textBox.LineDown(); // Scroll down
}
}
}
}
}

You could use GetCharacterIndexFromLineIndex to get the index of the beginning of the desired line and then set the CaretIndex to that value.
Because I don't really know, what you are trying to achieve, another possibility is to use LineUp and LineDown in conjunction with GetFirstVisibleLineIndex and GetLastVisibleLineIndex.

private void TextBoxGotoLine(
TextBox textbox1,
int linenum)
{
var target_cpos = textbox1.GetCharacterIndexFromLineIndex(linenum);
var target_char_rect = textbox1.GetRectFromCharacterIndex(target_cpos);
var first_char_rect = textbox1.GetRectFromCharacterIndex(0);
textbox1.ScrollToVerticalOffset(
target_char_rect.Top -
first_char_rect.Top
);
}
I found out if Wrapping is enabled its more complications:
private void TextBoxGotoLine(TextBox textbox1, int linenum)
{
// int Linenum is the Absolute Line, not including
// effect of Textbox Wrapping.
if (textbox1.TextWrapping == TextWrapping.Wrap)
{
// If textbox Wrapping is on, we need to
// Correct the Linenum for Wrapping which adds extra lines
int cidx = 0;
bool found = false;
int ln = 0;
char[] tmp = textbox1.Text.ToCharArray();
for (cidx = 0; cidx < tmp.Length; cidx++)
{
if (tmp[cidx] == '\n')
{
ln++;
}
if (ln == linenum)
{
found = true;
break;
}
}
if (!found)
return;
linenum = textbox1.GetLineIndexFromCharacterIndex(cidx+1);
}
// Non-Wrapping TextBox
var target_cpos = textbox1.GetCharacterIndexFromLineIndex(linenum);
var target_char_rect = textbox1.GetRectFromCharacterIndex(target_cpos);
var first_char_rect = textbox1.GetRectFromCharacterIndex(0);
textbox1.ScrollToVerticalOffset(target_char_rect.Top - first_char_rect.Top);
}

Related

Wpf multiline textbox style that affects the individual lines

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

C# - Peek Queue over time Without timer. Is it possible?

My current code is inside a timer to keep comparing if all drones actual position is near desired position but I dont think this is the best approach because I think this leads to slow processing.
Is there a way to check if the actual position is near desired position without using peek inside a timer?
private void timer_missao_Tick(object sender, EventArgs e)
{
string[] pontos_separados = null;
for (int k = 0; k < drone.Length; k++)
{
if (queue[k].Count > 0)
{
if (queue[k].Peek() == "levantar")
{
drone[k]._droneClient.FlatTrim();
drone[k]._droneClient.Takeoff();
drone[k].subir_ate_altura = true;
queue[k].Dequeue();
}
else if (queue[k].Peek().Split(null)[0] == "goto")
{
pontos_separados = queue[k].Peek().Split(null)[1].Split(',');
drone[k].posicao_desejada = new PointF(Convert.ToSingle(pontos_separados[0]), Convert.ToSingle(pontos_separados[1]));
int precisao = 5;
if (drone.All(d=> d.pos_atual().X > d.pos_desej().X - precisao && d.pos_atual().X <d.pos_desej().X + precisao &&
d.pos_atual().Y > d.pos_desej().Y - precisao && d.pos_atual().Y < d.pos_desej().Y + precisao))
{
for (int i = 0; i < drone.Length; i++)
{
queue[i].Dequeue();
}
}
}
else if (queue[k].Peek() == "aterrar")
{
drone[k]._droneClient.Land();
if (drone[k]._droneClient.NavigationData.State == NavigationState.Landed)
{
queue[k].Dequeue();
}
}
You could do the check only when the drone positions are updated, and not on every tick. That way you can also skip the Peek, by having the code that updates the drone position pass the updated Drone object to your check function.

Preserve word index on TextSelectionChanged

I've got a find next and previous function and edited it so that when the user selects text in a textbox and clicks on either Find Next or Find Previous button, the find feature will start it's index from the selected character and go through each search result (initially the feature wasn't there). To get the starting index of the selected text I created a function:
private int GetIntialCharPos(string Text)
{
int row = Variables._TextBox.GetLineIndexFromCharacterIndex(Variables._TextBox.CaretIndex);
int col = Variables._TextBox.CaretIndex - Variables._TextBox.GetCharacterIndexFromLineIndex(row);
return col;
}
The function which does the Find Next and Previous goes as follows:
private List<int> _matches;
private string _textToFind;
private bool _matchCase;
private int _matchIndex;
private void MoveToNextMatch(string textToFind, bool matchCase, bool forward)
{
if (_matches == null || _textToFind != textToFind || _matchCase != matchCase)
{
int startIndex = 0, matchIndex;
StringComparison mode = matchCase ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase;
_matches = new List<int>();
while (startIndex < Variables._TextBox.Text.Length && (matchIndex = Variables._TextBox.Text.IndexOf(textToFind, startIndex, mode)) >= 0)
{
_matches.Add(matchIndex);
startIndex = matchIndex + textToFind.Length;
}
_textToFind = textToFind;
_matchCase = matchCase;
_matchIndex = forward ? _matches.IndexOf(GetIntialCharPos(textToFind)) : _matches.IndexOf(GetIntialCharPos(textToFind)) - 1;
}
else
{
_matchIndex += forward ? 1 : -1;
if (_matchIndex < 0)
{
_matchIndex = _matches.Count - 1;
}
else if (_matchIndex >= _matches.Count)
{
_matchIndex = 0;
}
}
if (_matches.Count > 0)
{
Variables._TextBox.SelectionStart = _matches[_matchIndex];
Variables._TextBox.SelectionLength = textToFind.Length;
Variables._TextBox.Focus();
}
}
My issue is that once the user has selected the text he needs to search, and goes through the find next and previous buttons, and then he decides to select the text from a different index, rather than continuing the search from the selected index, it will maintain the default initial order which it goes by rather than starting from the selected index and going through each result from that. I created a small gif video here so you can take a better look at this problem.
How do I preserve the selected word index so every time the user selects from a different index it can start the search from the index in which the user selected rather than always starting from the start.
private int _matchIndex;
That's your problem variable. It retains the last match index but you don't know when the user changes it by himself. The TextBox class does not have a SelectionChanged event to tell you about it so there is no simple way to reset the variable.
Simply use a RichTextBox instead, it does have that event.
But it is much easier, this bug occurred because you added state unnecessarily by splitting of the searching operation into a separate class. State is in general a bad thing, it is a bug generator. You can trivially make the entire operation stateless by using the TextBox object directly:
private static void MoveToNextMatch(TextBoxBase box, bool forward) {
var needle = box.SelectedText;
var haystack = box.Text;
int index = box.SelectionStart;
if (forward) {
index = haystack.IndexOf(needle, index + 1);
if (index < 0) index = haystack.IndexOf(needle, 0);
}
else {
if (index == 0) index = -1;
else index = haystack.LastIndexOf(needle, index - 1);
if (index < 0) index = haystack.LastIndexOf(needle, haystack.Length - 1);
}
if (index >= 0) {
box.SelectionStart = index;
box.SelectionLength = needle.Length;
}
box.Focus();
}
Use the Last/IndexOf() method that takes a StringComparison to implement the matchCase argument.

Receiving XAML button name in

I have about 100 textboxes on a page, and each textbox has its own corresponding value in an array, each textbox has a method which is invoked when the value in the textbox is changed, and updates the corresponding elements of the array, to reflect that value it has been changed to. (In theory)
However, is there a way that you can adjust the below method, so that rather than writing it out 100 times, with a changing name "_8_8_TextChanged", and changing the values that it changes manually, and doing it so that 1 method is called by all textboxes, and the method recognises which textbox called it, and updates the relevant elements in the array?
The Method is defined below and features on the "Solver.xaml.cs" page.
private void _8_8_TextChanged(object sender, TextChangedEventArgs e)
{
int number = int.Parse(_8_8.Text);
if ((number >= 1) && (number <= 9))
{
for (int i = 0; i <= 8; i++)
{
if (i == (number - 1))
{
content[8, 8, i] = true;
}
else
{
content[8, 8, i] = false;
}
}
}
}
The XAML textbox itself is defined below and features on the "Solver.xaml" page, with its styling elements removed for simplicity.
<TextBox x:Name="_8_8" TextChanged="_8_8_TextChanged"/>
I really hope you have a good reason for using that many text boxes. In any case you can use the same event handler for all of your TextChange events, as follows.
All textboxes would be set up to use the same handler:
<TextBox x:Name="_8_8" TextChanged="_x_y_TextChanged"/>
<TextBox x:Name="_8_9" TextChanged="_x_y_TextChanged"/>
You can then update your array based on the sending text box:
private void _x_y_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox tb = (TextBox)sender;
// use the Name of the textbox to determine x, y value
string[] tmp_x_y = tb.Name.Split("_");
// you may have to adjust these indices based on how Split actually
// does its work.
int x = int.Parse(tmp_x_y[0]);
int y = int.Parse(tmp_x_y[1]);
int number = int.Parse(tb.Text);
if ((number >= 1) && (number <= 9))
{
for (int i = 0; i <= 8; i++)
{
if (i == (number - 1))
{
content[x, y, i] = true;
}
else
{
content[x, y, i] = false;
}
}
}
}
I didn't actually compile the code above, but it should give you a good starting point.

Autoexpand textbox while typing

I am needing an autoexpanding textbox like facebook has for it's status updates. I have code for it but for some reason it's not fully working correctly. It's updating the textbox and expanding it, but it is doing it way too soon. i am wanting it to expand when it gets to the end of the line. But it is doing it after 20 characters are entered! I have two different methods i have tried, they both do the same thing. Any suggestions on changing my code?
function sz(t) {
var therows = 0
var thetext = document.getElementById(t.id).value;
var newtext = thetext.split("\n");
therows += newtext.length
document.getElementById(t.id).rows = therows;
return false;
}
function sz(t)
{
a = t.value.split('\n');
b = 1;
for (x = 0; x < a.length; x++)
{
if (a[x].length >= t.cols)
{
b += Math.floor(a[x].length / t.cols);
}
}
b += a.length;
if (b > t.rows)
{
t.rows = b;
}
}
Check out this fiddle.
I think your problem was using txtbox.id rather than just passing the id.

Categories

Resources