尽管这是一个老问题,但它在 .NET 4 中仍然是一个问题。在阅读了尽可能多的关于该问题的内容后,我将解决方案的组合整合到一个帮助程序类中。
首先,这是我要拍摄的结果……我有一个包含各种控件的面板。子控件及其大小可以根据用户活动而改变。我希望面板水平调整大小,以便永远不会有水平滚动条,但如果垂直空间不足,我希望出现垂直滚动条。此外,垂直滚动条出现时无法覆盖我的任何子控件,并且我不想在不需要时为它留下空隙。
我的助手类试图修复的两个“错误”是第一个,从不显示水平滚动条,其次,当垂直滚动条出现时,面板的宽度会自动增加以适应它。
我的假设是面板设置为 AutoSize 和 AutoScroll,子控件也设置为 AutoSize。
解决方案
帮助类将自己附加到面板(通过处理 Paint 和 SizeChanged 事件)并做两件事。首先,它禁用水平滚动条。这并不像听起来那么容易,我在这里找到了解决这个问题的方法Horizontal scroll bar answer by Kbv Subrahmanyam。其次,响应 Paint 和 SizeChanged 事件以及后台计时器,它检查垂直滚动条的 Visible 属性是否已更改。如果是这样,帮助程序类会更改面板的 Right Padding 属性以添加或删除滚动条所需的额外空间。需要使用各种面板事件和计时器,因为 .NET 完全为滚动条公开 no 事件(恕我直言,这是一个很大的设计缺陷)。
最后一点是,在处理 SizeChanged 事件时,您不能做任何改变面板大小的事情。如果您这样做,就会发生坏事(tm)。因此,如果由于 SizeChanged 事件而需要更改填充,我会安排稍后进行更改。
不管怎样,这里是帮助类的代码。它假定您拥有所有适当的“使用”语句,包括用于 System.Threading 的语句...
/// <summary>
/// This class is intended to beat the AutoSize and AutoScroll features into submission!
///
/// Or, at least getting them to work the way I want them to (which may not be the way
/// others think they should work).
///
/// This class will force a panel that has AutoSize enabled to actually increase its
/// width as appropriate when the AutoScroll Vertical scroll bar becomes visible.
/// I like this better than attempting to 'reserve' space for the Vertical scroll bar,
/// which wastes space when the scroll bar is not needed, and leaves ugly gaps in
/// your user interface.
/// </summary>
public class AutoScrollFixer
{
/// <summary>
/// This is the panel we are 'fixing'
/// </summary>
private Panel _panel;
/// <summary>
/// This field keeps track of the original value for
/// the right padding property of the panel.
/// </summary>
private int _originalRightPadding = 0;
/// <summary>
/// We use this flag to prevent recursion problems.
/// </summary>
private bool _adjusting = false;
/// <summary>
/// This flag keeps track of the last known state of the scroll bar.
/// </summary>
private bool _lastScrollBarVisible = false;
/// <summary>
/// We use a timer to check the scroll bar state every so often.
/// This is necessary since .NET (in another stunning piece of
/// architecture from Microsoft) provides absolutely no events
/// attached to the scroll bars of a panel.
/// </summary>
private System.Windows.Forms.Timer _timer = new System.Windows.Forms.Timer();
/// <summary>
/// Construct an AutoScrollFixer and attach it to the provided panel.
/// Once created, there is no particular reason to keep a reference
/// to the AutoScrollFixer in your code. It will silently do its thing
/// in the background.
/// </summary>
/// <param name="panel"></param>
public AutoScrollFixer(Panel panel)
{
_panel = panel;
_originalRightPadding = panel.Padding.Right;
EnableVerticalAutoscroll(_panel);
_lastScrollBarVisible = _panel.VerticalScroll.Visible;
_panel.Paint += (s, a) =>
{
AdjustForVerticalScrollbar();
};
_panel.SizeChanged += (s, a) =>
{
//
// We can't do something that changes the size while handling
// a size change. So, if an adjustment is needed, we will
// schedule it for later.
//
if (_lastScrollBarVisible != _panel.VerticalScroll.Visible)
{
AdjustLater();
}
};
_timer.Tick += (s, a) =>
{
//
// Sadly, the combination of the Paint event and the SizeChanged event
// is NOT enough to guarantee that we will catch a change in the
// scroll bar status. So, as a last ditch effort, we will check
// for a status change every 500 mSecs. Yup, this is a hack!
//
AdjustForVerticalScrollbar();
};
_timer.Interval = 500;
_timer.Start();
}
/// <summary>
/// Enables AutoScroll, but without the Horizontal Scroll bar.
/// Only the Vertical Scroll bar will become visible when necessary
///
/// This method is based on this StackOverflow answer ...
/// https://stackoverflow.com/a/28583501/2175233
/// </summary>
/// <param name="panel"></param>
public static void EnableVerticalAutoscroll( Panel panel )
{
panel.AutoScroll = false;
panel.HorizontalScroll.Enabled = false;
panel.HorizontalScroll.Visible = false;
panel.HorizontalScroll.Maximum = 0;
panel.AutoScroll = true;
}
/// <summary>
/// Queue AdjustForVerticalScrollbar to run on the GUI thread after the current
/// event has been handled.
/// </summary>
private void AdjustLater()
{
ThreadPool.QueueUserWorkItem((t) =>
{
Thread.Sleep(200);
_panel.BeginInvoke((Action)(() =>
{
AdjustForVerticalScrollbar();
}));
});
}
/// <summary>
/// This is where the real work gets done. When this method is called, we will
/// simply set the right side padding on the panel to make room for the
/// scroll bar if it is being displayed, or reset the padding value to
/// its original value if not.
/// </summary>
private void AdjustForVerticalScrollbar()
{
if (!_adjusting)
{
try
{
_adjusting = true;
if (_lastScrollBarVisible != _panel.VerticalScroll.Visible)
{
_lastScrollBarVisible = _panel.VerticalScroll.Visible;
Padding p = _panel.Padding;
p.Right = _lastScrollBarVisible ? _originalRightPadding + System.Windows.Forms.SystemInformation.VerticalScrollBarWidth + 2 : _originalRightPadding;
_panel.Padding = p;
_panel.PerformLayout();
}
}
finally
{
_adjusting = false;
}
}
}
}