I have a .NET UWP TextBox with a good deal of text, and I want to search for a word in it. When I click on the button to start my search, it will find the first occurrence of this word. When I click again, it will find the second, like ctrl+f in Notepad).
I want to get focus on the found world, but when is text is long enough that there is a scrollbar in, it will not bring the found word into view.
This is a screengrab of the screen in this state, showing how I must resize the window to see the found word.
Here is my code for searching (textarea is of type TextBox):
private void Find(string text)
{
textarea.Focus(FocusState.Programmatic);
var start = textarea.SelectionStart + textarea.SelectionLength;
var found = (bool)checkboxFindCaseSensitive.IsChecked ? textarea.Text.IndexOf(text, start) : textarea.Text.IndexOf(text, start, StringComparison.CurrentCultureIgnoreCase);
if (found == -1)
{
textarea.SelectionStart = 0;
found = (bool)checkboxFindCaseSensitive.IsChecked ? textarea.Text.IndexOf(text, start) : textarea.Text.IndexOf(text, start, StringComparison.CurrentCultureIgnoreCase);
if (found == -1) return;
}
textarea.SelectionStart = found;
textarea.SelectionLength = text.Length;
}
I have already tried to put textarea.Focus(FocusState.Programmatic); at the end of method as well as textarea.Focus(FocusState.Pointer);, but neither helped.
UPDATE:
I've found that it's focusing correctly, but to the last found word (to position, where is the cursor before find next word), not to the currently found word.
So I need to update focus to current SelectionStart, not to the last. Any ideas? I have already tried to change SelectionStart again, replace text and update layout - nothing helps.
What you can do is to measure the height of your text until the index, and resize the textbox accordingly.
private static float GetTextHeightUntilIndex(TextBox textBox, int index)
{
var height = 0;
var textBuffer = textBox.Text;
// Remove everything after `index` in order to measure its size
textBox.Text = textBuffer.Substring(0, index);
textBox.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
var height = textBox.DesiredSize().Height;
// Put the full text back
textBox.Text = textBuffer;
return height;
}
private void Find(string text)
{
textarea.Focus(FocusState.Programmatic);
var start = textarea.SelectionStart + textarea.SelectionLength;
var found = (bool)checkboxFindCaseSensitive.IsChecked ? textarea.Text.IndexOf(text, start) : textarea.Text.IndexOf(text, start, StringComparison.CurrentCultureIgnoreCase);
if (found == -1)
{
textarea.SelectionStart = 0;
found = (bool)checkboxFindCaseSensitive.IsChecked ? textarea.Text.IndexOf(text, start) : textarea.Text.IndexOf(text, start, StringComparison.CurrentCultureIgnoreCase);
if (found == -1) return;
}
textarea.SelectionStart = found;
textarea.SelectionLength = text.Length;
// -------------------
var cursorPosInPx = GetTextHeightUntilIndex(textarea, found);
// First method: resize your textbox to the selected word
textarea.Height = cursorPosInPx;
// Second method: scroll the textbox
var grid = (Grid)VisualTreeHelper.GetChild(textarea, 0);
for (var i = 0; i <= VisualTreeHelper.GetChildrenCount(grid) - 1; i++)
{
object obj = VisualTreeHelper.GetChild(grid, i);
if (obj is ScrollViewer)
((ScrollViewer)obj).ChangeView(null, cursorPosInPx, null, true);
}
}
Be careful however, for the first method, depending on whatlayout your textbox is, resizing the control may have an unwanted effect or no effect at all.
Related
I'm using ScintillaNET in VisualStudio/C#.
When the user clicks (LMB or RMB) a specific word inside the text, I need to get the surrounding symbols. For example:
This is <a test> to show my <problem>
In this case, if the user clicks over the word "test", I want to retrieve the entire block between "<" and ">", so I need to get <a test>.
If the user clicks over "problem" I need to get <problem>.
I know that I can get the caret position then "navigate" (for loop) before the position (going left) to find the first occurence of "<", then "navigate" after the caret position (going right) to find the first occurrence of ">".
But is there any other better way to achieve this? Does Scintilla supply some methods to find them?
Thank you for your help!
I used this code to find a workaround, but frankly speaking I wish to find a better solution:
const char CHAR_START_BLOCK = '[';
const char CHAR_END_BLOCK = ']';
if(e.Button == MouseButtons.Right) {
int leftPos = -1;
int rightPos = -1;
//
// Search for CHAR_START_BLOCK
//
for(int i = scintilla1.CurrentPosition; i >= 1; i--) {
if(scintilla1.GetCharAt(i) == CHAR_START_BLOCK) {
leftPos = i;
break;
}
if(scintilla1.GetCharAt(i) == '\r') {
break;
}
if( (scintilla1.GetCharAt(i) == CHAR_END_BLOCK) && (i != scintilla1.CurrentPosition)) {
break;
}
}
if(leftPos != -1) {
//
// Search for CHAR_END_BLOCK
//
string currentLine = scintilla1.Lines[scintilla1.CurrentLine].Text;
for(int i = scintilla1.CurrentPosition; i <= (scintilla1.CurrentPosition + currentLine.len()); i++) {
if(scintilla1.GetCharAt(i) == CHAR_END_BLOCK) {
rightPos = i;
break;
}
}
LogManager.addLog("LEFT/RIGHT: " + scintilla1.GetTextRange(leftPos, (rightPos + 1 - leftPos)));
}
}
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.
I have this code, which turns the word or phrase sought red:
private void rtb_TextChanged(object sender, EventArgs e) {
String textToFind = textBoxWordOrPhraseToFind.Text;
String richText = rtb.Text;
if ((textToFind == "") || (richText == "") || (!(richText.Contains(textToFind)))) {
return;
}
tOut.Select(richText.IndexOf(textToFind), textToFind.Length);
tOut.SelectionColor = Color.Red;
}
...but then it stops - it only reddens the first word or phrase. I want it to give the entire (matching) contents of the RichTextBox the Sammy Hagar treatment.
How do I do so?
RichTextBox doesn't support multiple selections.
Searching other occurrences of the same text could be done, but you can't keep more than one selection. However the SelectionBackColor property could be changed to simulate the multiple selection behavior.
The searching could be done in this way
int pos = 0;
pos = richText.IndexOf(textToFind, 0);
while(pos != -1)
{
tOut.Select(pos, textToFind.Length);
tOut.SelectionBackColor = Color.Red;
pos = richText.IndexOf(textToFind, pos + 1);
}
I've got a gantt chart (RangeBar) I've made with the MS Chart control; for some shorter series, the label gets displayed outside the bar; I'd prefer to set it so the label gets stays inside the bar and gets truncated (with ellipsis would be nice). Is there a way of doing this? I've been poking around in the properties of the chart and series for ages now with no success.
I think the property you need to set is BarLabelStyle
eg.
chart.Series["mySeries"]["BarLabelStyle"] = "Center";
See this Dundas page that explains that custom property which should be similar or same for the MS Chart control.
In the end I rolled my own using this (yes it's messy, it'll get tidied up when I have time):
private static void Chart_PostPaint(object sender, ChartPaintEventArgs e)
{
Chart c = ((Chart)sender);
foreach (Series s in c.Series)
{
string sVt = s.GetCustomProperty("PixelPointWidth");
IGanttable ig = (IGanttable)s.Tag;
double dblPixelWidth = c.ChartAreas[0].AxisY.ValueToPixelPosition(s.Points[0].YValues[1]) - c.ChartAreas[0].AxisY.ValueToPixelPosition(s.Points[0].YValues[0]);
s.Label = ig.Text.AutoEllipsis(s.Font, Convert.ToInt32(dblPixelWidth)-dblSeriesPaddingGuess);
}
}
public static string AutoEllipsis(this String s, Font f, int intPixelWidth)
{
if (s.Length == 0 || intPixelWidth == 0) return "";
var result = Regex.Split(s, "\r\n|\r|\n");
List<string> l = new List<string>();
foreach(string str in result)
{
int vt = TextRenderer.MeasureText(str, f).Width;
if (vt < intPixelWidth)
{ l.Add(str); }
else
{
string strTemp = str;
int i = str.Length;
while (TextRenderer.MeasureText(strTemp + "…", f).Width > intPixelWidth)
{
strTemp = str.Substring(0, --i);
if (i == 0) break;
}
l.Add(strTemp + "…");
}
}
return String.Join("\r\n", l);
}
This seems to work quite happily as long as it is the Post_Paint event (If you use the Paint event it will stop ToolTips from showing)
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);
}