Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
895 views
in Technique[技术] by (71.8m points)

c# - Hiding the Scrollbar while allowing scrolling with the Mouse Wheel in a FlowLayoutPanel

I'm attempting to create a dynamic panel where I can add controls to and scroll when the controls are off the panel's height, while also hiding the scrollbars.

I'm using a FlowLayoutPanel and I'm constantly adding custom panels to it, with their Width set to the parent Container's Width.
I've also set its AutoScroll property to true.

However, one problem remains. How do I hide the darn Scrollbars? Both of them.

I've tried the following:

this.lobbiesPanel.AutoScroll = false;
this.lobbiesPanel.HorizontalScroll.Visible = false;
this.lobbiesPanel.VerticalScroll.Visible = false;
this.lobbiesPanel.AutoScroll = true;

To my disappointment, it didn't work as intended. The scrollbars were still visible. How can I hide the Scrollbars, while still maintaining the ability to scroll with a mouse wheel?

Demonstration of my issue

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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:

FlowLayoutPanel No ScrollBars


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...