is it possible to set number of characters per row for a multiline (AcceptsReturn="True") textbox in Silverlight? Like Cols attribute of Textarea in HTML.
Not really. Normally you just set the height and width to whatever you want. Is there a particular reason why you want a certain number of characters on each line?
[EDIT]
I found some code here that splits a string into equal chunks:
Splitting a string into chunks of a certain size
Using that, I came up with the following. It works ok but needs some tweaking.
private void TextBox_KeyUp(object sender, KeyEventArgs e)
{
var text = (sender as TextBox).Text.Replace(Environment.NewLine, "").Chunk(8).ToList();
(sender as TextBox).Text = String.Join(Environment.NewLine, text.ToArray());
(sender as TextBox).SelectionStart = (sender as TextBox).Text.Length;
}
And the extension method:
public static class Extensions
{
public static IEnumerable<string> Chunk(this string str, int chunkSize)
{
for (int i = 0; i < str.Length; i += chunkSize)
yield return str.Substring(i, Math.Min(chunkSize, str.Length - i));
}
}
Related
public partial class Form1 : Form
{
TextBox textBox;
public Form1()
{
InitializeComponent();
textBox = new TextBox() { Height = 30, Width = 200, Text = "Syncfusion Software", Font = new Font("Arial", 11) };
textBox.MouseMove += textBox_MouseMove;
this.Controls.Add(textBox);
}
void textBox_MouseMove(object sender, MouseEventArgs e)
{
var selectionStart = textBox.GetCharIndexFromPosition(e.Location);
textBox.SelectionStart = selectionStart;
textBox.SelectionLength = 0;
}
}
Here is my code, this is simple sample that i tried to get clear information about GetCharIndexFromPosition() method of TextBox.
In Mouse move, i get the char index position using the current mouse position of the textbox and set selection start of the textbox based on it. So that if i mouse move, then the selection start or caret position will be set based on the mouse move. But there was an issue with when mouse moved at the end of text, selection start is not set to last. it sets last but before.
For example, if a text box contains text "stack", then if mouse position is after the "k" then caret position should be at the end, but it displayed inbetween "c" and "k". GetCharIndexPosition() doesn't returns value properly for last character. Let me know solution for this
Thanks in Advance.
Regards,
Venkatesan R
This is a known documented behavior. The Remarks section of the GetCharIndexFromPosition method documentation contains the following Important note:
If the specified location is not within the client rectangle of the control, or is beyond the last character in the control, the return value is the index of the last character.
The workaround is to use the reverse method GetPositionFromCharIndex to adjust the returned index.
Something like this
void textBox_MouseMove(object sender, MouseEventArgs e)
{
var charIndex = textBox.GetCharIndexFromPosition(e.Location);
var charPosition = textBox.GetPositionFromCharIndex(charIndex);
if (e.Location.X > charPosition.X) charIndex++;
textBox.Select(charIndex, 0);
}
P.S. As a side note, I have no idea what this method is trying to achieve, but for sure it prevents the standard text selection by mouse behavior.
This ended up really bothering me, so I expanded Ivan Stoev's idea to a rather overengineered method that calculates the pixel width of the last character and divides it by two to accurately emulate the same behaviour as on the other characters.
The method was written for a drag/drop scenario, where the selection is adjusted to the drop position while hovering the mouse over.
// Cached, so it doesn't get recalculated on each moved pixel.
private Char _textBoxLastChar = '\0';
private Int32 _textBoxLastCharHalfWidth = 0;
private void TextBox_DragOver(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.UnicodeText))
return;
TextBox tb = sender as TextBox;
if (tb == null)
return;
Int32 textLen = tb.Text.Length;
if (textLen > 0 && _textBoxLastChar != tb.Text[textLen - 1])
{
_textBoxLastChar = tb.Text[textLen - 1];
_textBoxLastCharHalfWidth = (Int32)Math.Round(GetStringWidth(_textBoxLastChar.ToString(), tb.Font) / 2);
}
Point localPoint = tb.PointToClient(new Point(e.X, e.Y));
Int32 index = tb.GetCharIndexFromPosition(localPoint);
// fix for fact it returns the last char position when you go outside text bounds.
Int32 charPosition = tb.GetPositionFromCharIndex(index).X;
if (textLen != 0 && index == textLen - 1 && localPoint.X > charPosition + _textBoxLastCharHalfWidth)
index++;
if (!tb.Focused)
tb.Focus();
tb.SelectionStart = index;
tb.SelectionLength = 0;
}
public static Double GetStringWidth(String text, Font f)
{
//create a bmp / graphic to use MeasureString on
Single areaSize = f.Size * 20;
using (Bitmap b = new Bitmap(1, 1))
using (Graphics g = Graphics.FromImage(b))
{
SizeF sizeOfString = g.MeasureString(text, f, new SizeF(areaSize, areaSize), StringFormat.GenericTypographic);
return sizeOfString.Width;
}
}
Of course, if you ever change the font or font size of the text box, you'll have to reset _textBoxLastChar back to '\0'.
How to create a simple WPF Authentication for WPF application?
For example:
First time a user should registry then login.
Users login and password should be saved as txt file(encrypted).
If process of authentication is successful,then it should redirect to another existed window.
I'm a beginner in WPF.
I've searched about this question,but didn't find what I need.
I need a simple,step by step explanation of how to do it.
Thanks in advance! :)
I am also learning so in order to exercise a bit i have created a very simple example for you. It is probably unprofessional and unsafe but i think (hope) it is possible to extend it somehow :).
Firstly you need to create simple WPF windows (use txt/btn+Name naming convention):
For both windows add
using System.IO;
Then you need to add events for buttons and modify code for both windows:
public partial class LoginWindow : Window
{
public LoginWindow()
{
InitializeComponent();
}
// This is really bad/weak encryption method
String WeakDecryptMethod(String textIn)
{
Char[] temp = textIn.ToArray<Char>();
for (int i = 0; i < textIn.Length; i++)
{
temp[i] = (char)((int)temp[i] - 3);
}
return new String(temp);
}
private void btnRegister_Click(object sender, RoutedEventArgs e)
{
RegisterWindow newWindow = new RegisterWindow();
newWindow.ShowDialog();
}
private void btnOK_Click(object sender, RoutedEventArgs e)
{
// If file exist and login and password are "correct"
if (File.Exists("Users.txt")
&& txtLogin.Text.Length >= 4
&& txtPass.Text.Length >= 4)
{
using (StreamReader streamReader = new StreamReader("Users.txt"))
{
// While there is something in streamReader read it
while (streamReader.Peek() >= 0)
{
String decryptedLogin = WeakDecryptMethod(streamReader.ReadLine());
String decryptedPass = WeakDecryptMethod(streamReader.ReadLine());
if (decryptedLogin == txtLogin.Text && decryptedPass == txtPass.Text)
{
ProtectedWindow protectedWindow = new ProtectedWindow();
this.Close();
protectedWindow.Show();
break;
}
}
}
}
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
}
And code for Register window:
public partial class RegisterWindow : Window
{
public RegisterWindow()
{
InitializeComponent();
}
// This is really bad/weak method to encrypt files
String WeakEncryptMethod(String textIn)
{
Char[] temp = textIn.ToArray<Char>();
for (int i = 0; i < textIn.Length; i++)
{
temp[i] = (char)((int)temp[i] + 3);
}
return new String(temp);
}
private void btnRegister_Click(object sender, RoutedEventArgs e)
{
// If file exist and login and password are "correct"
if (File.Exists("Users.txt")
&& txtLogin.Text.Length >= 4
&& txtPass.Text.Length >= 4
&& txtPass.Text == txtPassCheck.Text)
{
StringBuilder stringBuilder = new StringBuilder();
using (StreamReader streamReader = new StreamReader("Users.txt"))
{
stringBuilder.Append(streamReader.ReadToEnd());
}
using (StreamWriter streamWriter = new StreamWriter("Users.txt"))
{
streamWriter.Write(stringBuilder.ToString());
streamWriter.WriteLine(WeakEncryptMethod(txtLogin.Text));
streamWriter.WriteLine(WeakEncryptMethod(txtPass.Text));
}
this.Close();
}
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
}
In order to work application need to have access to file "Users.txt" which needs to be placed in the same folder.
Notes:
It will be better if you will use some proper encryption functions and probably create separate class for it. Additionally i am almost sure that it will not work properly with login and password which contains the last 3 characters from the end of ASCII tables.
In my opinion it is a bad idea to store login and password data in *.txt file :).
As far i know C# code is very easily reverse engineered so probably it will be better to hide encryption/decryption part somehow. I do not know much about it, but u will be able to read more [here] 2 and probably uncle Google will be able to help.
Code is very simple and there is probably a lot of possibilities to extend it (more file handling stuff, TextBox validation for proper input and password strength calculations)
Standard WPF 4 Datagrid.
Let' say I have datagrid 200 pixels wide, and 2 columns. I would like the columns take always entire space, meaning if the user resizes the first column to 50 pixels, the last one would be 150.
Initially I set width 100 pixels for the 1st column, and * for the last one (in XAML).
I thought the problem is with removing the virtual, 3rd column, as explained here:
http://wpf.codeplex.com/Thread/View.aspx?ThreadId=58939
but there is no difference really -- still, when resizing the columns, I get some extra space on right -- with virtual column, it is a virtual column (white color by default), without it, it is empty space (gray by default).
QUESTION: how to enforce the constraint, that no matter how the user resizes the columns,
sum(columns width)==datagrid width
?
Edits
Yes, I use WPF 4.
WORKAROUND
I marked one of the answers as solution, but actually it is not a solution due to WPF design. It is simply what WPF can do at best, and it is not very good -- first of all the option CanUserResize for column means really IsResizeable and this option when turned on contradicts Width set to *. So without some really smart trick you end up with:
datagrid which last column in superficially resizable but in fact it is not, and little space on right is shown (i.e. the virtual column is not resizable) -- for last column: CanUserResize=true, Width=*
datagrid which last column cannot be resized by user and it is shown accordingly, initially no space on right is shown, but it can be shown when user resizes any element of datagrid -- for last column: CanUserResize=false, Width=*
So far I can see two problems with WPF datagrid:
misleading naming
contradiction of features
I am still all ears to how really solve this issue.
Set the width for the data grid to "Auto". You're allowing the columns to resize correctly within the grid itself, but you've hard-wired the width to 200.
UPDATE: Base on #micas's comment, I may have misread. If that's the case, try using 100 for the left column's width and 100* for the right column (note the asterisk). This will default the width for the right column to 100 but allow it to resize to fill the grid.
You can set a column width to star on code.
In your constructor, add:
Loaded += (s, e) => dataGrid1.Columns[3].Width =
new DataGridLength(1, DataGridLengthUnitType.Star);
I've just implemented this as an attached behavior. The problem is when you set the DataGrid's last column to *, it does resize to fit, but then all the auto-fitting of the other cells messes up. To resolve this, the attached behaviour does a manual auto-fit of other (non last) cells.
This also works when resizing the other columns - once loaded, you can resize and the last column will always fill. Note this behavior works once on the Loaded event
// Behavior usage: <DataGrid DataGridExtensions.LastColumnFill="True"/>
public class DataGridExtensions
{
public static readonly DependencyProperty LastColumnFillProperty = DependencyProperty.RegisterAttached("LastColumnFill", typeof(bool), typeof(DataGridExtensions), new PropertyMetadata(default(bool), OnLastColumnFillChanged));
public static void SetLastColumnFill(DataGrid element, bool value)
{
element.SetValue(LastColumnFillProperty, value);
}
public static bool GetLastColumnFill(DataGrid element)
{
return (bool)element.GetValue(LastColumnFillProperty);
}
private static void OnLastColumnFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dataGrid = d as DataGrid;
if (dataGrid == null) return;
dataGrid.Loaded -= OnDataGridLoaded;
dataGrid.Loaded += OnDataGridLoaded;
}
private static void OnDataGridLoaded(object sender, RoutedEventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
var lastColumn = dataGrid.Columns.LastOrDefault();
if(lastColumn != null)
lastColumn.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
// Autofit all other columns
foreach (var column in dataGrid.Columns)
{
if (column == lastColumn) break;
double beforeWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToCells);
double sizeCellsWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToHeader);
double sizeHeaderWidth = column.ActualWidth;
column.MinWidth = Math.Max(beforeWidth, Math.Max(sizeCellsWidth, sizeHeaderWidth));
}
}
}
Be forewarned: It's a hack....
I registered to the "AutoGeneratedColumns" event in the "OnLastColumnFillChanged" method of Dr. ABT's class and copied the Loaded method into it, and it works. I haven't really thoroughly tested it yet, so YMMV.
My change:
private static void OnLastColumnFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dataGrid = d as DataGrid;
if (dataGrid == null) return;
dataGrid.Loaded -= OnDataGridLoaded;
dataGrid.Loaded += OnDataGridLoaded;
dataGrid.AutoGeneratedColumns -= OnDataGrid_AutoGeneratedColumns;
dataGrid.AutoGeneratedColumns += OnDataGrid_AutoGeneratedColumns;
}
private static void OnDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
var lastColumn = dataGrid.Columns.LastOrDefault();
if (lastColumn != null)
lastColumn.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
// Autofit all other columns
foreach (var column in dataGrid.Columns)
{
if (column == lastColumn) break;
double beforeWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToCells);
double sizeCellsWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToHeader);
double sizeHeaderWidth = column.ActualWidth;
column.MinWidth = Math.Max(beforeWidth, Math.Max(sizeCellsWidth, sizeHeaderWidth));
}
}
Oh, and don't forget to add the namespace to the XAML declaration! :)
Up top:
xmlns:ext="clr-namespace:TestProject.Extensions"
And then in the DataGrid declaration:
ext:DataGridExtensions.LastColumnFill="True"
Update:
I said that the mileage would vary! Mine certainly did.
That whole "autofit columns" bit caused some of my columns in a DataGrid with a variable number of columns to only be as wide as the column header. I removed that portion, and now it seems to be working on all of the DataGrids in the application.
Now I have:
private static void OnDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
UpdateColumnWidths(dataGrid);
}
private static void OnDataGridLoaded(object sender, RoutedEventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
UpdateColumnWidths(dataGrid);
}
private static void UpdateColumnWidths(DataGrid dataGrid)
{
var lastColumn = dataGrid.Columns.LastOrDefault();
if (lastColumn == null) return;
lastColumn.Width = new DataGridLength(1.0d, DataGridLengthUnitType.Star);
}
Here's a very simple answer, all performed in code behind. :-)
All columns will be auto-sized; the final column will fill all remaining space.
// build your datasource, e.g. perhaps you have a:
List<Person> people = ...
// create your grid and set the datasource
var dataGrid = new DataGrid();
dataGrid.ItemsSource = people;
// set AutoGenerateColumns to false and manually define your columns
// this is the price for using my solution :-)
dataGrid.AutoGenerateColumns = false;
// example of creating the columns manually.
// there are obviously more clever ways to do this
var col0 = new DataGridTextColumn();
col0.Binding = new Binding("LastName");
var col1 = new DataGridTextColumn();
col1.Binding = new Binding("FirstName");
var col2 = new DataGridTextColumn();
col2.Binding = new Binding("MiddleName");
dataGrid.Columns.Add(col0);
dataGrid.Columns.Add(col1);
dataGrid.Columns.Add(col2);
// Set the width to * for the last column
col2.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
I might be a bit late, but you can try my code from this question. I extended original grid and added method for the last column stretching:
private void StretchLastColumnToTheBorder()
{
if (ViewPortWidth.HasValue)
{
var widthSum = 0d;
for (int i = 0; i < Columns.Count; i++)
{
if (i == Columns.Count - 1 && ViewPortWidth > widthSum + Columns[i].MinWidth)
{
var newWidth = Math.Floor(ViewPortWidth.Value - widthSum);
Columns[i].Width = new DataGridLength(newWidth, DataGridLengthUnitType.Pixel);
return;
}
widthSum += Columns[i].ActualWidth;
}
}
}
where ViewPortWidth is:
public double? ViewPortWidth
{
get
{
return FindChild<DataGridColumnHeadersPresenter>(this, "PART_ColumnHeadersPresenter")?.ActualWidth;
}
}
So, you have to find the visual child (answer from here) of type DataGridColumnHeadersPresenter, which has the width of the viewport and calculate the width of the last column. To do it automatically, you can fire this method on LayoutUpdated event. Additionally, you can add a DependencyProperty, indicating, whether automatical stretching of the last column should be performed.
Based on the update pennyrave gave to DR.ABT's answer I made a further update to get this to work better. It's still a hack, but it seems to work better than either of their answers when I'm constantly updating the DataGrid's ItemsSource property. If I try to use a star or auto width anywhere, WPF insists that all the columns are only 20 pixels wide, so I hardcode them all based off the auto values they were set to.
I've added an invoke to the AutoGeneratedColumns event to make it delay a bit. Without this delay, all the columns insist that they are only 20 pixels wide. They still sometimes do, but I've got a check for that and it seems to work, (but the columns are rendered wrong, then corrected a millisecond later.)
Ideally we'd apply the column sizes after WPF figures out what the automatic sizes would be, and before the DataGrid is rendered, but I can't find any way to get my code to run there. It's either too early or too late.
public class DataGridExtensions
{
public static readonly DependencyProperty LastColumnFillProperty = DependencyProperty.RegisterAttached("LastColumnFill", typeof(bool), typeof(DataGridExtensions), new PropertyMetadata(default(bool), OnLastColumnFillChanged));
public static void SetLastColumnFill(DataGrid element, bool value)
{
element.SetValue(LastColumnFillProperty, value);
}
public static bool GetLastColumnFill(DataGrid element)
{
return (bool)element.GetValue(LastColumnFillProperty);
}
private static void OnLastColumnFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dataGrid = d as DataGrid;
if (dataGrid == null) return;
dataGrid.Loaded -= OnDataGridLoaded;
dataGrid.Loaded += OnDataGridLoaded;
dataGrid.AutoGeneratedColumns -= OnDataGrid_AutoGeneratedColumns;
dataGrid.AutoGeneratedColumns += OnDataGrid_AutoGeneratedColumns;
}
private static void OnDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => afterInvoke(dataGrid)));
}
private static void afterInvoke(DataGrid dataGrid)
{
bool nonMin = false;
foreach (var col in dataGrid.Columns)
{
if (col.ActualWidth != col.MinWidth)
{
nonMin = true;
}
}
if(nonMin)
{
OnDataGridLoaded(dataGrid, null);
}
}
public static void OnDataGridLoaded(object sender, RoutedEventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
// set size of columns
double sizeSoFar = 0;
for(int i =0; i < dataGrid.Columns.Count; i++)
{
var column = dataGrid.Columns[i];
//if last column
if (i == dataGrid.Columns.Count-1)
{
sizeSoFar = dataGrid.ActualWidth - sizeSoFar - 2;//2 pixels of padding
if(column.ActualWidth != sizeSoFar)
{
column.MinWidth = sizeSoFar;
column.Width = new DataGridLength(sizeSoFar);
}
}
else //not last column
{
double beforeWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToCells);
double sizeCellsWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToHeader);
double sizeHeaderWidth = column.ActualWidth;
column.MinWidth = Math.Max(beforeWidth, Math.Max(sizeCellsWidth, sizeHeaderWidth));
sizeSoFar += column.MinWidth; //2 pixels of padding and 1 of border
}
}
}
}
Remember to add something like xmlns:Util="clr-namespace:MyProject.Util" to your window tag at the top of your xaml and then you can use Util:DataGridExtensions.LastColumnFill="True" in your DataGrid tag.
I have a WPF application on which the user can paste some data from Word inside a RichTextBox... but if that word data has an image, I need to remove it, how can I accomplish that?
Since the FlowDocument is xml, maybe doing some linq magic could do it, but I don't know how.
There is a tool called WordtoXAML Converter (http://wordtoxaml.codeplex.com). You can use that to convert your Word document contents to XAML, use regular expression matching to identify the images and then strip them out.
The following code will do what you want. While it can be a bit wasteful (it looks through the entire document instead of just the bit that was just pasted), it is the only way to do it, as sometimes the RichTextBox is inaccurate when it indicates the recently painted range:
public class MyTextBox : RichTextBox
{
public MyTextBox()
{
CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, Paste));
}
protected virtual void Paste(object sender, ExecutedRoutedEventArgs e)
{
Paste();
foreach (var image in FindImages())
{
if (image.SiblingInlines != null)
{
image.SiblingInlines.Remove(image);
}
}
}
IEnumerable<InlineUIContainer> FindImages()
{
var result = new List<InlineUIContainer>();
var blocks = Document.Blocks;
for (TextPointer position = blocks.FirstBlock.ElementStart; position != null && position.CompareTo(blocks.LastBlock.ElementEnd) != 1; position = position.GetNextContextPosition(LogicalDirection.Forward))
{
InlineUIContainer element = position.Parent as InlineUIContainer;
if (element != null && element.Child is Image)
{
result.Add(element);
}
}
return result;
}
}
For a System.Windows.Forms.TextBox with Multiline=True, I'd like to only show the scrollbars when the text doesn't fit.
This is a readonly textbox used only for display. It's a TextBox so that users can copy the text out. Is there anything built-in to support auto show of scrollbars? If not, should I be using a different control? Or do I need to hook TextChanged and manually check for overflow (if so, how to tell if the text fits?)
Not having any luck with various combinations of WordWrap and Scrollbars settings. I'd like to have no scrollbars initially and have each appear dynamically only if the text doesn't fit in the given direction.
#nobugz, thanks, that works when WordWrap is disabled. I'd prefer not to disable wordwrap, but it's the lesser of two evils.
#André Neves, good point, and I would go that way if it was user-editable. I agree that consistency is the cardinal rule for UI intuitiveness.
I came across this question when I wanted to solve the same problem.
The easiest way to do it is to change to System.Windows.Forms.RichTextBox. The ScrollBars property in this case can be left to the default value of RichTextBoxScrollBars.Both, which indicates "Display both a horizontal and a vertical scroll bar when needed." It would be nice if this functionality were provided on TextBox.
Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto your form. It's not quite perfect but ought to work for you.
using System;
using System.Drawing;
using System.Windows.Forms;
public class MyTextBox : TextBox {
private bool mScrollbars;
public MyTextBox() {
this.Multiline = true;
this.ReadOnly = true;
}
private void checkForScrollbars() {
bool scroll = false;
int cnt = this.Lines.Length;
if (cnt > 1) {
int pos0 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(0)).Y;
if (pos0 >= 32768) pos0 -= 65536;
int pos1 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(1)).Y;
if (pos1 >= 32768) pos1 -= 65536;
int h = pos1 - pos0;
scroll = cnt * h > (this.ClientSize.Height - 6); // 6 = padding
}
if (scroll != mScrollbars) {
mScrollbars = scroll;
this.ScrollBars = scroll ? ScrollBars.Vertical : ScrollBars.None;
}
}
protected override void OnTextChanged(EventArgs e) {
checkForScrollbars();
base.OnTextChanged(e);
}
protected override void OnClientSizeChanged(EventArgs e) {
checkForScrollbars();
base.OnClientSizeChanged(e);
}
}
I also made some experiments, and found that the vertical bar will always show if you enable it, and the horizontal bar always shows as long as it's enabled and WordWrap == false.
I think you're not going to get exactly what you want here. However, I believe that users would like better Windows' default behavior than the one you're trying to force. If I were using your app, I probably would be bothered if my textbox real-estate suddenly shrinked just because it needs to accomodate an unexpected scrollbar because I gave it too much text!
Perhaps it would be a good idea just to let your application follow Windows' look and feel.
There's an extremely subtle bug in nobugz's solution that results in a heap corruption, but only if you're using AppendText() to update the TextBox.
Setting the ScrollBars property from OnTextChanged will cause the Win32 window (handle) to be destroyed and recreated. But OnTextChanged is called from the bowels of the Win32 edit control (EditML_InsertText), which immediately thereafter expects the internal state of that Win32 edit control to be unchanged. Unfortunately, since the window is recreated, that internal state has been freed by the OS, resulting in an access violation.
So the moral of the story is: don't use AppendText() if you're going to use nobugz's solution.
I had some success with the code below.
public partial class MyTextBox : TextBox
{
private bool mShowScrollBar = false;
public MyTextBox()
{
InitializeComponent();
checkForScrollbars();
}
private void checkForScrollbars()
{
bool showScrollBar = false;
int padding = (this.BorderStyle == BorderStyle.Fixed3D) ? 14 : 10;
using (Graphics g = this.CreateGraphics())
{
// Calcualte the size of the text area.
SizeF textArea = g.MeasureString(this.Text,
this.Font,
this.Bounds.Width - padding);
if (this.Text.EndsWith(Environment.NewLine))
{
// Include the height of a trailing new line in the height calculation
textArea.Height += g.MeasureString("A", this.Font).Height;
}
// Show the vertical ScrollBar if the text area
// is taller than the control.
showScrollBar = (Math.Ceiling(textArea.Height) >= (this.Bounds.Height - padding));
if (showScrollBar != mShowScrollBar)
{
mShowScrollBar = showScrollBar;
this.ScrollBars = showScrollBar ? ScrollBars.Vertical : ScrollBars.None;
}
}
}
protected override void OnTextChanged(EventArgs e)
{
checkForScrollbars();
base.OnTextChanged(e);
}
protected override void OnResize(EventArgs e)
{
checkForScrollbars();
base.OnResize(e);
}
}
What Aidan describes is almost exactly the UI scenario I am facing. As the text box is read only, I don't need it to respond to TextChanged. And I'd prefer the auto-scroll recalculation to be delayed so it's not firing dozens of times per second while a window is being resized.
For most UIs, text boxes with both vertical and horizontal scroll bars are, well, evil, so I'm only interested in vertical scroll bars here.
I also found that MeasureString produced a height that was actually bigger than what was required. Using the text box's PreferredHeight with no border as the line height gives a better result.
The following seems to work pretty well, with or without a border, and it works with WordWrap on.
Simply call AutoScrollVertically() when you need it, and optionally specify recalculateOnResize.
public class TextBoxAutoScroll : TextBox
{
public void AutoScrollVertically(bool recalculateOnResize = false)
{
SuspendLayout();
if (recalculateOnResize)
{
Resize -= OnResize;
Resize += OnResize;
}
float linesHeight = 0;
var borderStyle = BorderStyle;
BorderStyle = BorderStyle.None;
int textHeight = PreferredHeight;
try
{
using (var graphics = CreateGraphics())
{
foreach (var text in Lines)
{
var textArea = graphics.MeasureString(text, Font);
if (textArea.Width < Width)
linesHeight += textHeight;
else
{
var numLines = (float)Math.Ceiling(textArea.Width / Width);
linesHeight += textHeight * numLines;
}
}
}
if (linesHeight > Height)
ScrollBars = ScrollBars.Vertical;
else
ScrollBars = ScrollBars.None;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
finally
{
BorderStyle = borderStyle;
ResumeLayout();
}
}
private void OnResize(object sender, EventArgs e)
{
m_timerResize.Stop();
m_timerResize.Tick -= OnDelayedResize;
m_timerResize.Tick += OnDelayedResize;
m_timerResize.Interval = 475;
m_timerResize.Start();
}
Timer m_timerResize = new Timer();
private void OnDelayedResize(object sender, EventArgs e)
{
m_timerResize.Stop();
Resize -= OnResize;
AutoScrollVertically();
Resize += OnResize;
}
}