Since you just need to hide the ScrollBars of a FlowLayoutPanel, not replace the ScrollBars with your own Controls, you can build a Custom Control derived from FlowLayoutPanel.
The Custom Control needs some features that the ancestor doesn't have:
- It needs to be selectable
- Must receive Mouse input
- It should be able to Scroll if the Mouse Wheel is rotated when the Mouse Pointer is hovering a child Control, otherwise it won't scroll when filled.
To make it selectable and receive Mouse Input, you can add to its Constructor:
SetStyle(ControlStyles.UserMouse | ControlStyles.Selectable, true);
To make it scroll no matter where the Mouse Pointer is located, it needs to pre-filter WM_MOUSEWHEEL messages and possibly WM_LBUTTONDOWN messages.
You can use the IMessageFilter Interface to pre-filter messages before they're dispatched and act on it (it can be tricky, you must not be greedy and learn when you need to let go or keep the message for yourself).
When the WM_MOUSEWHEEL
message is received and it appears it's directed to your Control, you can send it to the FlowLayoutPanel.
Now, there's the hack-ish part: a ScrollableControl tries very hard to show its Scrollbars and you (kind of) need them, because this Control has a very weird way to calculate its PreferredSize
(the overall area of the Control occupied by child Controls) and it changes based on the FlowDirection
, plus there's no real way to manage the standard Scrollbars: you get rid of them or you hide them.
Or you replace them with your own designed Controls, but this is all another matter.
To hide the Scrollbars, the common way is to call the ShowScrollBar function.
The int wBar
parameter specifies which Scrollbar to hide/show.
The bool bShow
parameter specifies whether to show (true
) or hide (false
) these Scrollbars.
- The FlowLayoutPanel tries to show its ScrollBars in specific conditions, so you need to trap some specific messages and call
ShowScrollBar
each time (you can't just call this function once and forget about it).
Here's a test Custom Control which implements all this stuff:
(it's working code, but not exactly production-grade: you'll have to work on it a bit, I suppose, to make it behave as you prefer in specific conditions / use-cases)
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);
}
This is how it works: