Custom panel layout doesn't work as expected when animating (WPF) - wpf

I've got a custom (and getting complex) TabControl. It's a gathering of many sources, plus my own wanted features. In it is a custom Panel to show the headers of the TabControl. Its features are to compress the size of the TabItems until they reached their minimum, and then activates scrolling features (in the Panel, again). There is also another custom panel to hold a single button, that renders on the right of the TabItems (it's a "new tab" button).
It all works great, until I try to animate the scrolling.
Here are some relevant snippets :
In the CustomTabPanel (C#, overriding Panel and implementing IScrollInfo):
private readonly TranslateTransform _translateTransform = new TranslateTransform();
public void LineLeft()
{
FirstVisibleIndex++;
var offset = HorizontalOffset + _childRects[0].Width;
if (offset < 0 || _viewPort.Width >= _extent.Width)
offset = 0;
else
{
if (offset + _viewPort.Width > _extent.Width)
offset = _extent.Width - _viewPort.Width;
}
_offset.X = offset;
if (_scrollOwner != null)
_scrollOwner.InvalidateScrollInfo();
//Animate the new offset
var aScrollAnimation = new DoubleAnimation(_translateTransform.X, -offset,
new Duration(this.AnimationTimeSpan), FillBehavior.HoldEnd) { AccelerationRatio = 0.5, DecelerationRatio = 0.5 };
aScrollAnimation.Completed += ScrollAnimationCompleted;
_translateTransform.BeginAnimation(TranslateTransform.XProperty, aScrollAnimation , HandoffBehavior.SnapshotAndReplace);
//End of animation
// These lines are the only ones needed if we remove the animation
//_translateTransform.X = -offset;
//InvalidateMeasure();
}
void ScrollAnimationCompleted(object sender, EventArgs e)
{
InvalidateMeasure();
}
the _translateTransform is initialized in the constructor :
base.RenderTransform = _translateTransform;
Again, everything is fine if I remove the animation part and just replace it with the commented out lines at the end.
I must also point out that the problem is NOT with the animation itself. That part works out well. The problem is about when I remove some tab items : all the layout then screws up. The TranslateTransformation seems to hold on some wrong value, or something.
Thanks in advance.

Well. As it's often the case, I kept working on the thing, and... answered myself.
Could still be useful for other people, so here was the catch. In the line :
var aScrollAnimation = new DoubleAnimation(_translateTransform.X, -offset, new Duration(this.AnimationTimeSpan), FillBehavior.HoldEnd)
{ AccelerationRatio = 0.5, DecelerationRatio = 0.5 };
the FillBehavior should have been FillBehavior.Stop.
As easy as that!

Related

Snapping a SurfaceListBox

I'm looking to create a scrolling surfacelistbox which automatically snaps into a position after a drag is finished so that the center item on the screen is centered itself in the viewport.
I've gotten the center item, but now as usual the way that WPF deals with sizes, screen positions, and offsets has me perplexed.
At the moment I've chosen to subscribe to the SurfaceScrollViewer's ManipulationCompleted event, as that seems to consistently fire after I've finished a scroll gesture (whereas the ScrollChanged event tends to fire early).
void ManipCompleted(object sender, ManipulationCompletedEventArgs e)
{
FocusTaker.Focus(); //reset focus to a dummy element
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < List.Items.Count; i++)
{
SurfaceListBoxItem item = List.ItemContainerGenerator.ContainerFromIndex(i) as SurfaceListBoxItem;
if (ViewportHelper.IsInViewport(item) && (List.Items[i] as string != "Dummy"))
{
FrameworkElement el = item as FrameworkElement;
visibleElements.Add(el);
}
}
int centerItemIdx = visibleElements.Count / 2;
FrameworkElement centerItem = visibleElements[centerItemIdx];
double center = ss.ViewportWidth / 2;
//ss is the SurfaceScrollViewer
Point itemPosition = centerItem.TransformToAncestor(ss).Transform(new Point(0, 0));
double desiredOffset = ss.HorizontalOffset + (center - itemPosition.X);
ss.ScrollToHorizontalOffset(desiredOffset);
centerItem.Focus(); //this also doesn't seem to work, but whatever.
}
The list snaps, but where it snaps seems to be somewhat chaotic. I have a line down the center of the screen, and sometimes it looks right down the middle of the item, but other times it's off to the side or even between items. Can't quite nail it down, but it seems that the first and fourth quartile of the list work well, but the second and third are progressively more off toward the center.
Just looking for some help on how to use positioning in WPF. All of the relativity and the difference between percentage-based coordinates and 'screen-unit' coordinates has me somewhat confused at this point.
After a lot of trial and error I ended up with this:
void ManipCompleted(object sender, ManipulationCompletedEventArgs e)
{
FocusTaker.Focus(); //reset focus
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < List.Items.Count; i++)
{
SurfaceListBoxItem item = List.ItemContainerGenerator.ContainerFromIndex(i) as SurfaceListBoxItem;
if (ViewportHelper.IsInViewport(item))
{
FrameworkElement el = item as FrameworkElement;
visibleElements.Add(el);
}
}
Window window = Window.GetWindow(this);
double center = ss.ViewportWidth / 2;
double closestCenterOffset = double.MaxValue;
FrameworkElement centerItem = visibleElements[0];
foreach (FrameworkElement el in visibleElements)
{
double centerOffset = Math.Abs(el.TransformToAncestor(window).Transform(new Point(0, 0)).X + (el.ActualWidth / 2) - center);
if (centerOffset < closestCenterOffset)
{
closestCenterOffset = centerOffset;
centerItem = el;
}
}
Point itemPosition = centerItem.TransformToAncestor(window).Transform(new Point(0, 0));
double desiredOffset = ss.HorizontalOffset - (center - itemPosition.X) + (centerItem.ActualWidth / 2);
ss.ScrollToHorizontalOffset(desiredOffset);
centerItem.Focus();
}
This block of code effectively determines which visible list element is overlapping the center line of the list and snaps that element to the exact center position. The snapping is a little abrupt, so I'll have to look into some kind of animation, but otherwise I'm fairly happy with it! I'll probably use something from here for animations: http://blogs.msdn.com/b/delay/archive/2009/08/04/scrolling-so-smooth-like-the-butter-on-a-muffin-how-to-animate-the-horizontal-verticaloffset-properties-of-a-scrollviewer.aspx
Edit: Well that didn't take long. I expanded the ScrollViewerOffsetMediator to include HorizontalOffset and then simply created the animation as suggested in the above post. Works like a charm. Hope this helps someone eventually.
Edit2: Here's the full code for SnapList:
SnapList.xaml
SnapList.xaml.cs
Note that I got pretty lazy as this project went on an hard-coded some of it. Some discretion will be needed to determine what you do and don't want from this code. Still, I think this should work pretty well as a starting point for anyone who wants this functionality.
The code has also changed from what I pasted above; I found that using Windows.GetWindow gave bad results when the list was housed in a control that could move. I made it so you can assign a control for your movement to be relative to (recommended that be the control just above your list in the hierarchy). I think a few other things changed as well; I've added a lot of customization options including being able to define a custom focal point for the list.

How to force vertical scrollbar always be visible from AutoScroll in WinForms?

Using VS2010 and .NET 4.0 with C# and WinForms:
I always want a Vertical Scrollbar to show for my panel as a disabled scrollbar (when it's not needed, and a enabled one when it can be used.
So it's like a hybrid AutoScroll. I've tried using VScrollBars but I can't figure out where to place them to make this work.
Essentially I've got a user control that acts as a "Document" of controls, its size changes so when using auto-scroll it works perfectly. The scrollbar appears when the usercontrol doesn't fit and the user can move it updown.
It's like a web browser essentially. However, redrawing controls takes a long time (it's forms with many fields and buttons etc within groups in a grid within a panel :P
So anyhow, when autoscroll enables the vertical scrollbar, it takes a while to redraw the window. I'd like to ALWAYS show the vertical scrollbar as indicated above (with the enable/disable functionality).
If anyone has some help, i've read many posts on the subject of autoscroll, but noone has asked what I'm asking and I can't come up with a solution.
C# Version of competent_Tech's answer
using System.Runtime.InteropServices;
public class MyUserControl : UserControl
{
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ShowScrollBar(IntPtr hWnd, int wBar, bool bShow);
private enum ScrollBarDirection
{
SB_HORZ = 0,
SB_VERT = 1,
SB_CTL = 2,
SB_BOTH = 3
}
public MyUserControl()
{
InitializeComponent();
ShowScrollBar(this.Handle, (int) ScrollBarDirection.SB_VERT, true);
}
}
You can use the auto-scroll functionality of the panel, you just need to send it a windows message to show the vertical scrollbar:
<DllImport("user32.dll")> _
Public Shared Function ShowScrollBar(ByVal hWnd As System.IntPtr, ByVal wBar As Integer, ByVal bShow As Boolean) As Boolean
End Function
Private Const SB_VERT As Integer = 1
Public Sub New()
' This call is required by the designer.
InitializeComponent()
ShowScrollBar(Panel1.Handle, SB_VERT, True)
End Sub
The scrollbar will be displayed and appear as though it can be scrolled, but it won't do anything until it is actually ready to scroll. If you disable it, it won't be automatically re-enabled, so this is probably the best approach.
Also, to improve the performance while resizing, you can call SuspendLayout on the panel before updating and ResumeLayout when done.
What worked for me was overriding the CreateParams call and enabling the WS_VSCROLL style.
public class VerticalFlowPanel : FlowLayoutPanel
{
protected override CreateParams CreateParams
{
get
{
var cp = base.CreateParams;
cp.Style |= 0x00200000; // WS_VSCROLL
return cp;
}
}
}
The AutoScroll logic will now adjust the scrolling bounds without ever hiding the scrollbar.
Here is what solved this for me. My case is that I have a panel sandwiched between another three panels with no degree of liberty in any direction. I needed this panel to be so big that the whole structure would go out of my 1920x1080 screen.
The solution is actually very simple.
For the panel that needs scroll bars set the AutoScroll property to true. Then, add on it another control in the far right far down position (right-bottom position). The control I choose is a label which I made invisible.... And that is all.
Now my panel occupies its restricted area, but I can scroll to the size that I needed and use it for the size I need.
If you only need horizontal scroll bars add the invisible control outside left, for vertical only far down bottom.
The actual size of the panel is the one you restrict it to when display it, but the virtual size is dictated by the invisible control.
This code will draw a disabled vertical scrollbar whenever the built in scrollbar of the Panel is invisible. The codes assumes that
AutoScroll = true;
AutoSize = false;
The following code is speed-optimized. It does as few as possible in OnPaint().
Derive a class from Panel.
Add these member variables:
// NOTE: static variables are not thread safe.
// But as we have only one GUI thread this does not matter.
static IntPtr mh_ScrollTheme = IntPtr.Zero;
static int ms32_ScrollWidth = SystemInformation.VerticalScrollBarWidth;
Win32.RECT mk_ScrollTop;
Win32.RECT mk_ScrollBot; // coordinates of top scrollbar button
Win32.RECT mk_ScrollShaft; // coordinates of bottom scrollbar button
Then override OnSizeChanged:
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
Win32.RECT k_ScrollBar = new Win32.RECT(ClientRectangle);
k_ScrollBar.Left = k_ScrollBar.Right - ms32_ScrollWidth;
mk_ScrollTop = new Win32.RECT(k_ScrollBar);
mk_ScrollBot = new Win32.RECT(k_ScrollBar);
mk_ScrollShaft = new Win32.RECT(k_ScrollBar);
int s32_Upper = k_ScrollBar.Top + ms32_ScrollWidth;
int s32_Lower = k_ScrollBar.Bottom - ms32_ScrollWidth;
mk_ScrollTop .Bottom = s32_Upper;
mk_ScrollBot .Top = s32_Lower;
mk_ScrollShaft.Top = s32_Upper;
mk_ScrollShaft.Bottom = s32_Lower;
}
And paint the scrollbar when required:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (VScroll)
return; // The 'real' scrollbar is visible
if (mh_ScrollTheme == IntPtr.Zero)
mh_ScrollTheme = Win32.OpenThemeData(Handle, "SCROLLBAR");
if (mh_ScrollTheme == IntPtr.Zero)
return; // The user has disabled themes
// Draw the disabled vertical scrollbar.
IntPtr h_DC = e.Graphics.GetHdc();
// Draw shaft
const int SBP_UPPERTRACKVERT = 7;
const int SCRBS_DISABLED = 4;
Win32.DrawThemeBackground(mh_ScrollTheme, h_DC, SBP_UPPERTRACKVERT, SCRBS_DISABLED, ref mk_ScrollShaft, IntPtr.Zero);
// Draw top button
const int SBP_ARROWBTN = 1;
const int ABS_UPDISABLED = 4;
Win32.DrawThemeBackground(mh_ScrollTheme, h_DC, SBP_ARROWBTN, ABS_UPDISABLED, ref mk_ScrollTop, IntPtr.Zero);
// Draw lower button
const int ABS_DOWNDISABLED = 8;
Win32.DrawThemeBackground(mh_ScrollTheme, h_DC, SBP_ARROWBTN, ABS_DOWNDISABLED, ref mk_ScrollBot, IntPtr.Zero);
e.Graphics.ReleaseHdc(h_DC);
}
For some years, the answer of BradJ and fiat worked for me. Now I needed to show the disabled scrollbar look. But I failed to find the correct way… So here is my workaround.
The code bellow just draw the disabled scrollbar at the position of the real scrollbar.
THE CODE
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;
public class VerticalFlowPanel : FlowLayoutPanel
{
public VerticalFlowPanel()
{
AutoScroll = true;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var width = Width;
var height = Height;
var vsWidth = SystemInformation.VerticalScrollBarWidth;
var vsHeight = SystemInformation.VerticalScrollBarArrowHeight;
var left = width - vsWidth;
var sbUpper = new Rectangle(left, 0, vsWidth, height / 2);
var sbLower = new Rectangle(left, sbUpper.Height, vsWidth, height - sbUpper.Height);
var arUp = new Rectangle(left, 0, vsWidth, vsHeight);
var arDown = new Rectangle(left, height - vsHeight, vsWidth, vsHeight);
ScrollBarRenderer.DrawUpperVerticalTrack(e.Graphics, sbUpper, ScrollBarState.Disabled);
ScrollBarRenderer.DrawLowerVerticalTrack(e.Graphics, sbLower, ScrollBarState.Disabled);
ScrollBarRenderer.DrawArrowButton(e.Graphics, arUp, ScrollBarArrowButtonState.UpDisabled);
ScrollBarRenderer.DrawArrowButton(e.Graphics, arDown, ScrollBarArrowButtonState.DownDisabled);
}
// Necessary to avoid visual artifacts
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
var width = Width;
var height = Height;
var vsWidth = SystemInformation.VerticalScrollBarWidth;
var scrollBounds = new Rectangle(width - vsWidth, 0, vsWidth, height);
Invalidate(scrollBounds);
}
}
NOTE
This is not the best solution. But it was easier than migrate my hole solution to WPF…

How to change the direction of wpf marquee dynamically?

I want to change the direction of my marquee on changeDirection button click.
My code for changing direction is :
private void changeDirection_click(object sender, RoutedEventArgs e)
{
if (_marqueeType == MarqueeType.RightToLeft)
{
_marqueeType = MarqueeType.LeftToRight;
StartMarqueeing(_marqueeType);
}
else if (_marqueeType == MarqueeType.LeftToRight)
{
_marqueeType = MarqueeType.RightToLeft;
StartMarqueeing(_marqueeType);
}
}
And code for start marquee is :
public void StartMarqueeing(MarqueeType marqueeType)
{
double height = canMain.ActualHeight - marqueeList.ActualHeight;
marqueeList.Margin = new Thickness(0, 0, 0, 0);
doubleAnimation.From = -marqueeList.ActualWidth;
doubleAnimation.To = canMain.ActualWidth;
doubleAnimation.RepeatBehavior = RepeatBehavior.Forever;
doubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(_marqueeTimeInSeconds));
if (marqueeType == MarqueeType.RightToLeft)
{
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("(Canvas.Right)"));
_storyBoard.Children.Add(doubleAnimation);
_storyBoard.Begin(marqueeList, true);
}
else if (marqueeType == MarqueeType.LeftToRight)
{
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("(Canvas.Left)"));
_storyBoard.Children.Add(doubleAnimation);
_storyBoard.Begin(marqueeList, true);
}
}
Now here I am able to change the direction from Right to Left only first time.
But when I am change it from Left to Right it’s not changing the marquee position Left to Right.
It looks like you left out a _storyboard = new Storyboard(), perhaps at the top of the StartMarqueeing method.
From what I see it appears that every call to StartMarqueeing will add an additional DoubleAnimation to the storyboard, then start it again. So all the old DoubleAnimations will be recreated, and it looks like they take precedence.
Try creating a new Storyboard object each time, not just re-using it and adding to its children collection.
Update
Oh, now I see the problem. You should not be setting both (Canvas.Left) and (Canvas.Right). Use only one of the two: That's all you need anyway, and using both will give the Canvas conflicting instructions. Traditionally people use (Canvas.Left). I think that's what Canvas selects, which is what is causing your asymmetry.
You may wonder why I say you are using both when you don't think your two animations every run at the same time. Actually they do: The first animation runs then holds the value on the animated property until it is removed or bumped off by another animation. If the second animation then runs and modifies a different property it doesn't bump off the first animation so the first animation's value is still present.
The bottom line is, using (Canvas.Left) on both animations should fix it as long as you are using the default HandoffBehavior.SnapshotAndReplace.

Selecting an object on a WPF Canvas?

I have a WPF Canvas with some Ellipse objects on it (displayed as circles). Each circle is from a collection class instance which is actually a custom hole pattern class. Each pattern has a certain number of circles, and each circle then gets added to the canvas using an iteration over the collection using the code below.
So, the canvas is populated with a bunch of circles and each circle belongs to a certain pattern instance. You can see a screenshot here: http://twitpic.com/1f2ci/full
Now I want to add the ability to click on a circle on the canvas, and be able to determine the collection it belongs to, so that I can then do some more work on the selected pattern to which that circle belongs.
public void DrawHoles()
{
// Iterate over each HolePattern in the HolePatterns collection...
foreach (HolePattern HolePattern in HolePatterns)
{
// Now iterate over each Hole in the HoleList of the current HolePattern...
// This code adds the HoleEntity, HoleDecorator, and HoleLabel to the canvas
foreach (Hole Hole in HolePattern.HoleList)
{
Hole.CanvasX = SketchX0 + (Hole.AbsX * _ZoomScale);
Hole.CanvasY = SketchY0 - (Hole.AbsY * _ZoomScale);
canvas1.Children.Add(Hole.HoleEntity);
}
}
}
All FrameworkElements have a Tag property which is of type object that can be used to hold arbitrary information. You could assign the HolePattern to the Tag property and easily use that later to get the associated collection.
i.e.:
...
Hole.HoleEntity.Tag = HolePattern as object;
canvas1.Children.Add(Hole.HoleEntity);
later on in the click event:
event(object sender,....)
{
Ellipse e = sender as Ellipse;
HolePattern hp = e.Tag as HolePattern;
...
}
So you probably already read my reply where I said I had it working. And it does work perfectly, (except that it requires great precision with the mouse), but I want to ask this: is it really smart to add an event handler to EVERY ellipse that gets added to a canvas? Now I don't know what kind of memory bog that could be, or maybe it is a piece of cake for WPF and Windows to handle.
In a practical case, I guess there would be not more that 30-50 holes even on a screen that had multiple patterns, but still; FIFTY event handlers? It just seems scary. And actually, each "Hole" is visually represented by two concentric circles and a text label (see the screenshow here: http://twitpic.com/1f2ci/full ), and I know the user would expect to be able to click on any one of those elements to select a hole. That means an event handler on 3 elements for every hole. Now we could be talking about 100 or more event handlers.
It seems like there should be a solution where you could have just one event handler on the Canvas and read the element reference under the mouse, then work off of that to get the .Tag property of that elment, and so on.
I thought I'd post my final and more refined solution in case it helps anyone else.
void canvas1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
int ClickMargin = 2;// Adjust here as desired. Span is in both directions of selected point.
var ClickMarginPointList = new Collection<Point>();
Point ClickedPoint = e.GetPosition(canvas1);
Point ClickMarginPoint=new Point();
for (int x = -1 * ClickMargin; x <= ClickMargin; x++)
{
for (int y = -1 * ClickMargin; y <= ClickMargin; y++)
{
ClickMarginPoint.X = ClickedPoint.X + x;
ClickMarginPoint.Y = ClickedPoint.Y + y;
ClickMarginPointList.Add(ClickMarginPoint);
}
}
foreach (Point p in ClickMarginPointList)
{
HitTestResult SelectedCanvasItem = System.Windows.Media.VisualTreeHelper.HitTest(canvas1, p);
if (SelectedCanvasItem.VisualHit.GetType().BaseType == typeof(Shape))
{
var SelectedShapeTag = SelectedCanvasItem.VisualHit.GetValue(Shape.TagProperty);
if (SelectedShapeTag!=null && SelectedShapeTag.GetType().BaseType == typeof(Hole))
{
Hole SelectedHole = (Hole)SelectedShapeTag;
SetActivePattern(SelectedHole.ParentPattern);
SelectedHole.ParentPattern.CurrentHole = SelectedHole;
return; //Get out, we're done.
}
}
}
}

How can I show scrollbars on a System.Windows.Forms.TextBox only when the text doesn't fit?

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

Resources