隐藏滚动条,同时允许在 FlowLayoutPanel 中使用鼠标滚轮滚动
Hiding the Scrollbar while allowing scrolling with the Mouse Wheel in a FlowLayoutPanel
我正在尝试创建一个动态面板,我可以在其中添加控件并在控件超出面板高度时滚动,同时还隐藏滚动条。
我正在使用 FlowLayoutPanel 并不断向其添加自定义面板,并将它们的 Width
设置为父容器的 Width
。
我还将其 AutoScroll
属性 设置为 true
.
但是,还有一个问题。如何隐藏该死的滚动条?他们俩。
我试过以下方法:
this.lobbiesPanel.AutoScroll = false;
this.lobbiesPanel.HorizontalScroll.Visible = false;
this.lobbiesPanel.VerticalScroll.Visible = false;
this.lobbiesPanel.AutoScroll = true;
令我失望的是,它没有按预期工作。滚动条仍然可见。
如何隐藏滚动条,同时仍保持使用鼠标滚轮滚动的能力?
由于您只需要隐藏 FlowLayoutPanel 的 ScrollBars,而不是将 ScrollBars 替换为您自己的控件,您可以构建一个派生自 FlowLayoutPanel 的自定义控件。
自定义控件需要一些祖先没有的特性:
- 需要可选
- 必须接收鼠标输入
- 当鼠标指针悬停在子控件上时,如果鼠标滚轮旋转,应该可以滚动,否则填充时不会滚动。
要使其可选择并接收鼠标输入,您可以在其构造函数中添加:
SetStyle(ControlStyles.UserMouse | ControlStyles.Selectable, true);
要使其无论鼠标指针位于何处都可以滚动,需要预先过滤 WM_MOUSEWHEEL messages and possibly WM_LBUTTONDOWN 条消息。
您可以使用 IMessageFilter 接口在消息发送之前对其进行预过滤并对其进行操作(这可能很棘手,您不能贪婪并了解何时需要放手或自己保留消息) .
当收到 WM_MOUSEWHEEL
消息并且它似乎被定向到您的控件时,您可以将它发送到 FlowLayoutPanel。
现在,这是 hack-ish 部分:ScrollableControl 非常努力地显示它的滚动条,而你(有点)需要它们,因为这个控件有一种非常奇怪的方法来计算它的 PreferredSize
(子控件占用的整个控件区域),它会根据 FlowDirection
发生变化,而且没有真正的方法来管理标准滚动条:您可以摆脱它们或隐藏它们。
或者你用自己设计的控件替换它们,但这是另一回事。
要隐藏滚动条,常用的方法是调用ShowScrollBar函数。
int wBar
参数指定哪个Scrollbar到hide/show。
bool bShow
参数指定是显示(true
)还是隐藏(false
)这些滚动条。
- FlowLayoutPanel 尝试在特定条件下显示其 ScrollBars,因此您需要捕获一些特定消息并每次调用
ShowScrollBar
(您不能只调用一次此函数而忘记它)。
这是一个实现所有这些东西的测试自定义控件:
(它是工作代码,但不完全是 生产级 :我想,您必须对其进行一些工作,以使其在特定情况下按照您的喜好运行条件/用例)
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[DesignerCategory("code")]
public class FlowLayoutPanelNoScrollbars : FlowLayoutPanel, IMessageFilter
{
public FlowLayoutPanelNoScrollbars() {
SetStyle(ControlStyles.UserMouse | ControlStyles.Selectable, true);
}
protected override void OnHandleCreated(EventArgs e) {
base.OnHandleCreated(e);
Application.AddMessageFilter(this);
VerticalScroll.LargeChange = 60;
VerticalScroll.SmallChange = 20;
HorizontalScroll.LargeChange = 60;
HorizontalScroll.SmallChange = 20;
}
protected override void OnHandleDestroyed(EventArgs e)
{
Application.RemoveMessageFilter(this);
base.OnHandleDestroyed(e);
}
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
switch (m.Msg) {
case WM_PAINT:
case WM_ERASEBKGND:
case WM_NCCALCSIZE:
if (DesignMode || !AutoScroll) break;
ShowScrollBar(this.Handle, SB_SHOW_BOTH, false);
break;
case WM_MOUSEWHEEL:
// Handle Mouse Wheel for other specific cases
int delta = (int)(m.WParam.ToInt64() >> 16);
int direction = Math.Sign(delta);
ShowScrollBar(this.Handle, SB_SHOW_BOTH, false);
break;
}
}
public bool PreFilterMessage(ref Message m)
{
switch (m.Msg) {
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL:
if (DesignMode || !AutoScroll) return false;
if (VerticalScroll.Maximum <= ClientSize.Height) return false;
// Should also check whether the ForegroundWindow matches the parent Form.
if (RectangleToScreen(ClientRectangle).Contains(MousePosition)) {
SendMessage(this.Handle, WM_MOUSEWHEEL, m.WParam, m.LParam);
return true;
}
break;
case WM_LBUTTONDOWN:
// Pre-handle Left Mouse clicks for all child Controls
//Console.WriteLine($"WM_LBUTTONDOWN");
if (RectangleToScreen(ClientRectangle).Contains(MousePosition)) {
var mousePos = MousePosition;
if (GetForegroundWindow() != TopLevelControl.Handle) return false;
// The hosted Control that contains the mouse pointer
var ctrl = FromHandle(ChildWindowFromPoint(this.Handle, PointToClient(mousePos)));
// A child Control of the hosted Control that will be clicked
// If no child Controls at that position the Parent's handle
var child = FromHandle(WindowFromPoint(mousePos));
}
return false;
// Eventually, if you don't want the message to reach the child Control
// return true;
}
return false;
}
private const int WM_PAINT = 0x000F;
private const int WM_ERASEBKGND = 0x0014;
private const int WM_NCCALCSIZE = 0x0083;
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_MOUSEWHEEL = 0x020A;
private const int WM_MOUSEHWHEEL = 0x020E;
private const int SB_SHOW_VERT = 0x1;
private const int SB_SHOW_BOTH = 0x3;
[DllImport("user32.dll", SetLastError = true)]
private static extern bool ShowScrollBar(IntPtr hWnd, int wBar, bool bShow);
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int SendMessage(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
internal static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
internal static extern IntPtr WindowFromPoint(Point point);
[DllImport("user32.dll")]
internal static extern IntPtr ChildWindowFromPoint(IntPtr hWndParent, Point point);
}
这是它的工作原理:
我正在尝试创建一个动态面板,我可以在其中添加控件并在控件超出面板高度时滚动,同时还隐藏滚动条。
我正在使用 FlowLayoutPanel 并不断向其添加自定义面板,并将它们的 Width
设置为父容器的 Width
。
我还将其 AutoScroll
属性 设置为 true
.
但是,还有一个问题。如何隐藏该死的滚动条?他们俩。
我试过以下方法:
this.lobbiesPanel.AutoScroll = false;
this.lobbiesPanel.HorizontalScroll.Visible = false;
this.lobbiesPanel.VerticalScroll.Visible = false;
this.lobbiesPanel.AutoScroll = true;
令我失望的是,它没有按预期工作。滚动条仍然可见。 如何隐藏滚动条,同时仍保持使用鼠标滚轮滚动的能力?
由于您只需要隐藏 FlowLayoutPanel 的 ScrollBars,而不是将 ScrollBars 替换为您自己的控件,您可以构建一个派生自 FlowLayoutPanel 的自定义控件。
自定义控件需要一些祖先没有的特性:
- 需要可选
- 必须接收鼠标输入
- 当鼠标指针悬停在子控件上时,如果鼠标滚轮旋转,应该可以滚动,否则填充时不会滚动。
要使其可选择并接收鼠标输入,您可以在其构造函数中添加:
SetStyle(ControlStyles.UserMouse | ControlStyles.Selectable, true);
要使其无论鼠标指针位于何处都可以滚动,需要预先过滤 WM_MOUSEWHEEL messages and possibly WM_LBUTTONDOWN 条消息。
您可以使用 IMessageFilter 接口在消息发送之前对其进行预过滤并对其进行操作(这可能很棘手,您不能贪婪并了解何时需要放手或自己保留消息) .
当收到 WM_MOUSEWHEEL
消息并且它似乎被定向到您的控件时,您可以将它发送到 FlowLayoutPanel。
现在,这是 hack-ish 部分:ScrollableControl 非常努力地显示它的滚动条,而你(有点)需要它们,因为这个控件有一种非常奇怪的方法来计算它的 PreferredSize
(子控件占用的整个控件区域),它会根据 FlowDirection
发生变化,而且没有真正的方法来管理标准滚动条:您可以摆脱它们或隐藏它们。
或者你用自己设计的控件替换它们,但这是另一回事。
要隐藏滚动条,常用的方法是调用ShowScrollBar函数。
int wBar
参数指定哪个Scrollbar到hide/show。
bool bShow
参数指定是显示(true
)还是隐藏(false
)这些滚动条。
- FlowLayoutPanel 尝试在特定条件下显示其 ScrollBars,因此您需要捕获一些特定消息并每次调用
ShowScrollBar
(您不能只调用一次此函数而忘记它)。
这是一个实现所有这些东西的测试自定义控件:
(它是工作代码,但不完全是 生产级 :我想,您必须对其进行一些工作,以使其在特定情况下按照您的喜好运行条件/用例)
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[DesignerCategory("code")]
public class FlowLayoutPanelNoScrollbars : FlowLayoutPanel, IMessageFilter
{
public FlowLayoutPanelNoScrollbars() {
SetStyle(ControlStyles.UserMouse | ControlStyles.Selectable, true);
}
protected override void OnHandleCreated(EventArgs e) {
base.OnHandleCreated(e);
Application.AddMessageFilter(this);
VerticalScroll.LargeChange = 60;
VerticalScroll.SmallChange = 20;
HorizontalScroll.LargeChange = 60;
HorizontalScroll.SmallChange = 20;
}
protected override void OnHandleDestroyed(EventArgs e)
{
Application.RemoveMessageFilter(this);
base.OnHandleDestroyed(e);
}
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
switch (m.Msg) {
case WM_PAINT:
case WM_ERASEBKGND:
case WM_NCCALCSIZE:
if (DesignMode || !AutoScroll) break;
ShowScrollBar(this.Handle, SB_SHOW_BOTH, false);
break;
case WM_MOUSEWHEEL:
// Handle Mouse Wheel for other specific cases
int delta = (int)(m.WParam.ToInt64() >> 16);
int direction = Math.Sign(delta);
ShowScrollBar(this.Handle, SB_SHOW_BOTH, false);
break;
}
}
public bool PreFilterMessage(ref Message m)
{
switch (m.Msg) {
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL:
if (DesignMode || !AutoScroll) return false;
if (VerticalScroll.Maximum <= ClientSize.Height) return false;
// Should also check whether the ForegroundWindow matches the parent Form.
if (RectangleToScreen(ClientRectangle).Contains(MousePosition)) {
SendMessage(this.Handle, WM_MOUSEWHEEL, m.WParam, m.LParam);
return true;
}
break;
case WM_LBUTTONDOWN:
// Pre-handle Left Mouse clicks for all child Controls
//Console.WriteLine($"WM_LBUTTONDOWN");
if (RectangleToScreen(ClientRectangle).Contains(MousePosition)) {
var mousePos = MousePosition;
if (GetForegroundWindow() != TopLevelControl.Handle) return false;
// The hosted Control that contains the mouse pointer
var ctrl = FromHandle(ChildWindowFromPoint(this.Handle, PointToClient(mousePos)));
// A child Control of the hosted Control that will be clicked
// If no child Controls at that position the Parent's handle
var child = FromHandle(WindowFromPoint(mousePos));
}
return false;
// Eventually, if you don't want the message to reach the child Control
// return true;
}
return false;
}
private const int WM_PAINT = 0x000F;
private const int WM_ERASEBKGND = 0x0014;
private const int WM_NCCALCSIZE = 0x0083;
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_MOUSEWHEEL = 0x020A;
private const int WM_MOUSEHWHEEL = 0x020E;
private const int SB_SHOW_VERT = 0x1;
private const int SB_SHOW_BOTH = 0x3;
[DllImport("user32.dll", SetLastError = true)]
private static extern bool ShowScrollBar(IntPtr hWnd, int wBar, bool bShow);
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int SendMessage(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
internal static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
internal static extern IntPtr WindowFromPoint(Point point);
[DllImport("user32.dll")]
internal static extern IntPtr ChildWindowFromPoint(IntPtr hWndParent, Point point);
}
这是它的工作原理: