In my WinForm application I have a multiline TextBox control (uiResults) which is used for reporting progress while processing a large number of items. Using AppendText works great for automatically scrolling to the bottom at every update, but if the user scrolls back to read some older data I need to turn off the autoscroll. I would rather stay away from P/Invoke calls if possible.
Is it possible to detect if the user has scrolled back without using P/Invoke? For now, I just check SelectionStart which works but requires the user to move the caret from the end of the textbox to stop the autoscroll:
if(uiResults.SelectionStart == uiResults.Text.Length)
{
uiResults.AppendText(result + Environment.NewLine);
}
My main problem is that when appending a string using the Text property, the textbox is scrolled to the beginning. I tried to solve this by storing the caret position and resetting and scrolling to it after the update, but this causes the current line to move to the bottom (of course, since ScrollToCaret scrolls no more than the necessary distance to bring the caret into view).
[Continued from above]
else
{
int pos = uiResults.SelectionStart;
int len = uiResults.SelectionLength;
uiResults.Text += result + Environment.NewLine;
uiResults.SelectionStart = pos;
uiResults.SelectionLength = len;
uiResults.ScrollToCaret();
}
Auto-scrolling text box uses more memory than expected
The code in the question implements exactly what you are looking for. Text is added, but scrolling only occurs if the scroll bar is at the very bottom.
I have had the same problem.
And finally, I made an easy way.
(Sorry, I'm not good at English.)
key point is get the first displayed char index using GetCharIndexFromPosition method.
//Get current infomation
int selStart = textBox.SelectionStart;
int selLen = textBox.SelectionLength;
int firstDispIndex = textBox.GetCharIndexFromPosition(new Point(3, 3));
//Append Text
textBox.AppendText(...);
//Scroll to original displayed position
textBox.Select(firstDispIndex, 0);
text.ScrolltoCaret();
//Restore original Selection
textBox.Select(selStart, selLen);
And, if textbox is flicking, use this extention.
Call textBox.Suspend() before adding text, and call textBox.Resume() after adding text.
namespace System.Windows.Forms
{
public static class ControlExtensions
{
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern bool LockWindowUpdate(IntPtr hWndLock);
public static void Suspend(this Control control)
{
LockWindowUpdate(control.Handle);
}
public static void Resume(this Control control)
{
LockWindowUpdate(IntPtr.Zero);
}
}
}
Hope this will help you.
Thank you~
Are you open to another approach, because you are bound to get into trouble this way and the solutions will get complex (pinvoke etc. that you want to avoid). For eg. suppose you find a way to "detect if the user has scrolled back" and you stop scrolling to bottom. But after reading the line user might want the scroll to bottom feature to resume. So why not give user a way to control Auto-Scrolling. Here's how I would do it...
Use a RichTextBox to show data and a Checkbox to control AutoScrolling, then your code might be something like this:
richTextBox1.AppendText(result + Environment.NewLine);
if (checkBoxAutoScroll.Checked)
{
richTextBox1.SelectionStart = richTextBox1.Text.Length;
richTextBox1.ScrollToCaret();
}
RichTextBox by default will not automatically scroll to bottom on AppendText, so the first line would always be visible (and not the newly appended line). But if user checks this checkbox called AutoScroll, our code will then scroll the richtextbox to the bottom where new lines are shown. If user wants to manually scroll to read a line he will first need to uncheck the checkbox.
Related
As part of my GUI in C# (.Net 5.0) I'm using the RichTextBox control (part of System.Windows.Forms) to display text and clickable links. My problem is when I click any links included without the form first having focus the act of clicking a link also focuses the textbox which automatically places the caret at the start of the text, this has the effect of making the textbox jump (or scroll) to the very top of document, losing one's position.
For my particular problem, the RichTextBox classes are assigned to TabPages within a TabControl- and all three of these components are created in response to user interaction (I understood the creation of these at runtime might limit the types of event handling available to me).
I've defined the RichTextBox control with multiline and both horizontal and vertical scrollbars, and to enable clickable links I'm using the DetectUrls option. Here is the full list of properties being used.
var control = new RichTextBox();
control.DetectUrls = true;
control.Dock = DockStyle.Fill;
control.Multiline = true;
control.ReadOnly = true;
control.WordWrap = false;
control.Text = myContent; // large content filling multiple screens
control.ScrollBars = RichTextBoxScrollBars.Both;
control.LinkClicked += new LinkClickedEventHandler(RichTextBoxLinkClicked);
I've included the LinkClicked because I want to provide some interaction for the user, but whether this handler is assigned or not doesn't seem to influence the problem.
Edit1 I came up with a solution that works to some extent; it prevents the scrolling by placing the caret at the beginning of the link (It doesn't work well though if there are more than one of the same link)
private void RichTextBoxLinkClicked(object sender, LinkClickedEventArgs e) {
int caret = ((RichTextBox) sender).Text.IndexOf(e.LinkText);
SelectionStart = caret;
}
Edit2 A second solution that I found is to set the focus of the text-box on a MouseEnter event. I've come to think the key to the problem is to set the focus of the text boxes before any user interaction takes place, but I'm having problems finding a satisfactory way to achieve it.
I have added a TextChanged function to allow AutoScroll behavior for a TextBox,
it works fine and shortcuts working, except the UNDO (CTRL-Z) does not work when I have pasted text that is bigger than TextBox size.
It seems its because the ScrollBars appear and then for some reason the program does not have the past action (the PASTE) in the clipboard or something of this nature.
Is there something I need to change in this function? Or do I need to find a way to iterate thru events on ClipBoard (if that is even possible) I have posted the Function that I use to create the AutoScroll effect (found here on SO). Thanks for help
void TextBoxClass_TextChanged(object sender, EventArgs e)
{
System.Drawing.Size textBoxRect = System.Windows.Forms.TextRenderer.MeasureText(
this.Text, this.Font, new System.Drawing.Size(this.Width, int.MaxValue),
System.Windows.Forms.TextFormatFlags.WordBreak | System.Windows.Forms.TextFormatFlags.TextBoxControl);
try
{
if (textBoxRect.Height > this.Height)
{
this.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
// I ADDED below line to make it stay scrolled to bottom..
// ..when the scrollBars appear
this.ScrollToCaret();
}
else
this.ScrollBars = System.Windows.Forms.ScrollBars.None;
}
catch (System.ComponentModel.Win32Exception)
{
// this sometimes throws "failure to create window handle" error
// This might happen if the TextBox is unvisible and/or
// too small to display a toolbar.
}
}
I have a WPF application, that has a number TextBox elements. The user fills those out, and the application then prints them. The problem with TextBoxes is that if you keep typing in one, once you fill it out to the end, the text starts scrolling horizontally to make room for more letters, and you no longer see the first couple letters that were typed in.
It was decided that the solution is to prevent the user from entering more text that will fit inside the TextBox. What's the best way of going about it?
I looked at TextBox properties, and didn't see anything that would do what I want directly. The first idea, is to set wrapping to Wrap. Then subscribe to PreviewTextInput event, and if the number of lines is going to exceed 1, handle the event without adding newly typed in text. Obviously you'll still be able to get around it by pasting text, but the bigger issue is that it will only work for single line TextBoxes, and I need it to work with multiline TextBoxes as well.
Is there a better approach that I'm missing? Would calculating text width, and then making sure it's less than TextBox width/height (how?) be a better option? Or perhaps another solution?
Here's my final solution, that also works for multiline TextBoxes. It will even work when pasting-in text. The only weirdness is that I delete trailing characters when text overflows, and that may seem strange if you are typing in text in the middle of the text box. I tried working around that by removing characters at the point of CaretIndex, but it was getting too involved. But other than that it does what I need it to do. To improve performance you can cache results from GetLineHeight function, so that you only need to call it once per TextBox (EDIT - I added the code for that as well).
<TextBox Height="23" Width="120" TextWrapping="Wrap" TextChanged="TextBoxTextChanged" AcceptsReturn="True"/>
private void TextBoxTextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null)
return;
double textLineHeight = GetCachedTextLineHeight(textBox);
int maxTextBoxLines = (int)(textBox.ViewportHeight / textLineHeight);
while (textBox.LineCount > maxTextBoxLines) //if typed in text goes out of bounds
{
if (textBox.Text.Length > 0)
textBox.Text = textBox.Text.Remove(textBox.Text.Length - 1, 1); //remove last character
if (textBox.Text.Length > 0)
textBox.CaretIndex = textBox.Text.Length;
}
}
private double GetTextLineHeight(TextBox textBox)
{
FormattedText formattedText = new FormattedText(
"a",
CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
new Typeface(textBox.FontFamily, textBox.FontStyle, textBox.FontWeight, textBox.FontStretch),
textBox.FontSize,
Brushes.Black);
return formattedText.Height;
}
#region Caching
Dictionary<TextBox, double> _cachedLineHeights = new Dictionary<TextBox, double>();
private double GetCachedTextLineHeight(TextBox textBox)
{
if (!_cachedLineHeights.ContainsKey(textBox))
{
double lineHeight = GetTextLineHeight(textBox);
_cachedLineHeights.Add(textBox, lineHeight);
}
return _cachedLineHeights[textBox];
}
#endregion
You're looking for the MaxLength property. You'll have to experiment with the number since you need to remember that you can fit a lot of i's in the space you can fit one W in. Typically when I size TextBoxes, I size and set the max length based on W's since thats the widest character.
EDIT: just saw you want it to work with multi-line text boxes too... in that case, just set it to wrap and it won't scroll horizontally.
I am trying to create a form with a multi-line TextBox with the following requirements:
The text in the text box might be short or long.
There is not much space availble.
I want to be sure that the entirety of the text has been seen. I can't be sure that the user has actually read it, but I can at least require that all of it has been seen.
So I'm trying to make a "fully viewed" multi-line TextBox.
This picture should make it clear what I'm trying to do:
If they check the checkbox before they've scrolled through the whole thing, I'll know not to believe them.
I think I need to know:
When the form comes up, was the text that was put into the TextBox short (all visible without scrolling) or long (the vertical scroll bar was shown)?
If the vertical scroll bar is showing, has the user scrolled it all the way to the bottom?
Any ideas about how to achieve this?
The TextBox has no scrolling event but the RichTextBox has. Also it has a method that allows you to get the index of the character closest to a point position.
private readonly Point _lowerRightCorner;
public frmDetectTextBoxScroll()
{
InitializeComponent();
_lowerRightCorner = new Point(richTextBox1.ClientRectangle.Right,
richTextBox1.ClientRectangle.Bottom);
}
private void richTextBox1_VScroll(object sender, EventArgs e)
{
int index = richTextBox1.GetCharIndexFromPosition(_lowerRightCorner);
if (index == richTextBox1.TextLength - 1) {
// Enable your checkbox here
}
}
I have a multiline TextBox called Console. While running, this textbox is being filled up with some communications data. I use
TextBox.AppendText("txt\r\n");
to add a line to it and that allows me to have it autoscroll down. My problem is I want to be able to not have it autoscroll down. So I thought I would try
TextBox.Text += "text";
But that scrolls you to the beginning of the box. My latest attempt was to use TextBox.SelectionStart to save the position before I wrote and restore it back to that after, but that didn't seem to make a difference and still brings me back to the beginning of the text.
int txtPosition = Console.SelectionStart;
Console.Text += "TextToAdd";
Console.SelectionStart = txtPosition;
Ideally I want to just be able to have the box stay where ever it happens to be and not scroll to the beginning or end of the text.
I think you need to you a richtextbox instead of a generic textbox and this will provide you with the functionality you desire.
Enjoy!
For a WinForms textbox, you may should able to do this:
textBox.SelectionStart = 0;
textBox.ScrollToCaret(); // force current position back to top