Misiu
Misiu

Reputation: 4919

ScrollableControl draw border around entire control

I'm building custom user control that is based on ScrollableControl.
Right now I'm trying to add border around my control (similar to border that DataGridView has)

I'm able to draw border using:

e.Graphics.TranslateTransform(AutoScrollPosition.X*-1, AutoScrollPosition.Y*-1);
ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Color.DarkBlue, ButtonBorderStyle.Dashed);

but this draws border around ClientRectangle, not around whole control: enter image description here

As you can see in the above picture, border isn't surrounding scrollbars as it does in DataGridView.

Can I draw border around entire control so that scrollbars get included in area surrounded by border?

EDIT:
Based on Textbox custom onPaint I am able to draw custom border, by overriding WndProc but I get this weird looking border flickering:

enter image description here

Here is full code I have so far:

internal class TestControl : ScrollableControl
{
    private int _tileWidth = 100;
    private int _tileHeight = 100;
    private int _tilesX = 20;
    private int _tilesY = 20;

    public TestControl()
    {
        SetStyle(ControlStyles.ResizeRedraw, true);
        SetStyle(ControlStyles.UserPaint, true);
        SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        SetStyle(ControlStyles.Opaque, true);
        SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
        UpdateStyles();
        ResizeRedraw = true;
        AutoScrollMinSize = new Size(_tilesX*_tileWidth, _tilesY*_tileHeight);
    }

    private bool _test = true;
    [DefaultValue(true)]
    public bool Test
    {
        get { return _test; }
        set
        {
            if(_test==value) return;
            _test = value;
            Update();
        }
    }

    [DllImport("user32")]
    private static extern IntPtr GetWindowDC(IntPtr hwnd);
    struct RECT
    {
        public int left, top, right, bottom;
    }
    struct NCCALSIZE_PARAMS
    {
        public RECT newWindow;
        public RECT oldWindow;
        public RECT clientWindow;
        IntPtr windowPos;
    }
    int clientPadding = 1;
    int actualBorderWidth = 1;
    Color borderColor = Color.Black;

    protected override void WndProc(ref Message m)
    {
        //We have to change the clientsize to make room for borders
        //if not, the border is limited in how thick it is.
        if (m.Msg == 0x83 && _test) //WM_NCCALCSIZE   
        {
            if (m.WParam == IntPtr.Zero)
            {
                RECT rect = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));
                rect.left += clientPadding;
                rect.right -= clientPadding;
                rect.top += clientPadding;
                rect.bottom -= clientPadding;
                Marshal.StructureToPtr(rect, m.LParam, false);
            }
            else
            {
                NCCALSIZE_PARAMS rects = (NCCALSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALSIZE_PARAMS));
                rects.newWindow.left += clientPadding;
                rects.newWindow.right -= clientPadding;
                rects.newWindow.top += clientPadding;
                rects.newWindow.bottom -= clientPadding;
                Marshal.StructureToPtr(rects, m.LParam, false);
            }
        }
        if (m.Msg == 0x85 && _test) //WM_NCPAINT    
        {
            base.WndProc(ref m);

            IntPtr wDC = GetWindowDC(Handle);
            using (Graphics g = Graphics.FromHdc(wDC))
            {
                ControlPaint.DrawBorder(g, new Rectangle(0, 0, Size.Width, Size.Height), borderColor, actualBorderWidth, ButtonBorderStyle.Solid,
                    borderColor, actualBorderWidth, ButtonBorderStyle.Solid, borderColor, actualBorderWidth, ButtonBorderStyle.Solid,
                    borderColor, actualBorderWidth, ButtonBorderStyle.Solid);
            }
            return;
        }
        base.WndProc(ref m);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        e.Graphics.FillRectangle(new SolidBrush(BackColor), ClientRectangle);
        e.Graphics.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y);

        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

        var offsetX = (AutoScrollPosition.X*-1)/_tileWidth;
        var offsetY = (AutoScrollPosition.Y*-1)/_tileHeight;

        var visibleX = Width/_tileWidth + 2;
        var visibleY = Height/_tileHeight + 2;

        var x = Math.Min(visibleX + offsetX, _tilesX);
        var y = Math.Min(visibleY + offsetY, _tilesY);

        for (var i = offsetX; i < x; i++)
        {
            for (var j = offsetY; j < y; j++)
            {
                e.Graphics.FillRectangle(Brushes.Beige, new Rectangle(i*_tileWidth, j*_tileHeight, _tileWidth, _tileHeight));
                e.Graphics.DrawString(string.Format("{0}:{1}", i, j), Font, Brushes.Black, new Rectangle(i*_tileWidth, j*_tileHeight, _tileWidth, _tileHeight));
            }
        }

        using (var p = new Pen(Color.Black))
        {
            for (var i = offsetX + 1; i < x; i++)
            {
                e.Graphics.DrawLine(p, i*_tileWidth, 0, i*_tileWidth, y*_tileHeight);
            }

            for (var i = offsetY + 1; i < y; i++)
            {
                e.Graphics.DrawLine(p, 0, i*_tileHeight, x*_tileWidth, i*_tileHeight);
            }
        }

        e.Graphics.FillRectangle(Brushes.White, AutoScrollPosition.X*-1 + 10, AutoScrollPosition.Y*-1 + 10, 35, 14);
        e.Graphics.DrawString("TEST", DefaultFont, new SolidBrush(Color.Red), AutoScrollPosition.X*-1 + 10, AutoScrollPosition.Y*-1 + 10);

        e.Graphics.TranslateTransform(AutoScrollPosition.X*-1, AutoScrollPosition.Y*-1);

        ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Color.Red, actualBorderWidth, ButtonBorderStyle.None,
                    Color.Red, actualBorderWidth, ButtonBorderStyle.None, Color.Red, actualBorderWidth, ButtonBorderStyle.Solid,
                    Color.Red, actualBorderWidth, ButtonBorderStyle.Solid);
    }

    protected override void OnScroll(ScrollEventArgs e)
    {
        if (DesignMode)
        {
            base.OnScroll(e);
            return;
        }

        if (e.Type == ScrollEventType.First)
        {
            LockWindowUpdate(Handle);
        }
        else
        {
            LockWindowUpdate(IntPtr.Zero);
            Update();
            if (e.Type != ScrollEventType.Last) LockWindowUpdate(Handle);
        }
    }

    protected override void OnMouseWheel(MouseEventArgs e)
    {
        if (VScroll && (ModifierKeys & Keys.Shift) == Keys.Shift)
        {
            VScroll = false;
            LockWindowUpdate(Handle);
            base.OnMouseWheel(e);
            LockWindowUpdate(IntPtr.Zero);
            Update();
            VScroll = true;
        }
        else
        {
            LockWindowUpdate(Handle);
            base.OnMouseWheel(e);
            LockWindowUpdate(IntPtr.Zero);
            Update();
        }
    }

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool LockWindowUpdate(IntPtr hWnd);
}

Can this flickering be fixed?

Upvotes: 2

Views: 938

Answers (2)

Reza Aghaei
Reza Aghaei

Reputation: 125227

You can simply derive from UserControl. It has built-in support for showing scrollbars and also has built-in support for showing borders.

All of built-in features of UserControl can be added to your control which is derived from ScrollableControl but with some additional try and error effort or taking look at source code of UserControl. But using UserControl class you can simply have those features.

Show Border

To show border, it's enough to set its BorderStyle to FixedSingle to get desired feature:

enter image description here

Show Scrollbars

To gain scroll feature, it's enough to set its AutoScroll to true and also set a suitable AutoScrollMinSize for control. Then when the width or height of the control is less than width or height of given size, the suitable scrollbar will be shown.

Custom Border Color

I also suppose you want to have different border color for the control, then it's enough to override WndProc and handle WM_NCPAINT and draw custom border for the control like this:

enter image description here

In above example, I've used the same technique which I used for Changing BorderColor of TextBox with a small change, here I checked if the BorderStyle equals to FixedSingle the I drew the border with desired color.

Enable the designer to act like a Parent Control at design time

If you want to enable it's designer to be able to drop some controls onto your UserControl, it's enough to decorate it with [Designer(typeof(ParentControlDesigner))]. This way, when you drop your UserControl on form, it can host other controls like a panel control. If you don't like this feature, just don't decorate it with that attribute and it will use Control designer by default which doesn't act like a parent control.

Upvotes: 1

Misiu
Misiu

Reputation: 4919

I was able to solve my problem by overriding CreateParams:

protected override CreateParams CreateParams
{
    [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    get
    {
        CreateParams cp = base.CreateParams;
        cp.ExStyle |= NativeMethods.WS_EX_CONTROLPARENT;

        cp.ExStyle &= (~NativeMethods.WS_EX_CLIENTEDGE);
        cp.Style &= (~NativeMethods.WS_BORDER);

                cp.Style |= NativeMethods.WS_BORDER;

        return cp;
    }
}

and here is required NativeMethods class:

internal static class NativeMethods
{
    public const int WS_EX_CONTROLPARENT = 0x00010000;
    public const int WS_EX_CLIENTEDGE = 0x00000200;
    public const int WS_BORDER = 0x00800000;
}

below is the result:

enter image description here

Upvotes: 2

Related Questions