2

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?

Community
  • 1
  • 1
Misiu
  • 4,738
  • 21
  • 94
  • 198
  • You write `ControlPaint.DrawBorder(e.Graphics, ClientRectangle,..` and then complain about the border being draw around the ClientRectangle ??? – TaW Apr 11 '17 at 13:52
  • @TaW I've tried this.Width instead of ClientRectangle.Width, but it looks like scrollbars are drawn on top – Misiu Apr 11 '17 at 13:56
  • Does the UC have an BorderStyle? I suggest Single.. – TaW Apr 11 '17 at 13:59
  • @TaW I'm using `ScrollableControl` as base so there is no `BorderStyle` property. I'm trying to draw in manually. – Misiu Apr 11 '17 at 14:06
  • Ah, that's right. borders only come in from UserControl. Which makes me curious: Why not go for a real UC? Also: You could set the ClientSize(!), but not the ClientRectangle (!?). Not sure if that would help, though.. – TaW Apr 11 '17 at 14:52
  • 1
    @TaW I've searched a bit because I couldn't decide what to pick as base class. I found this answer: http://stackoverflow.com/a/1322499/965722 I thought that `ScrollableControl` will be a good choice because I don't want any extra stuff, I'd like to get max performance as possible, but I see that I'm trying to recreate `UserControl` from `ScrollableControl` – Misiu Apr 11 '17 at 18:30
  • _Can this flickering be fixed?_ Did you turn on DoubleBuffered? – TaW Apr 11 '17 at 18:46
  • @TaW yes I have `SetStyle(ControlStyles.OptimizedDoubleBuffer, true);` flickering only affects border. – Misiu Apr 11 '17 at 18:48
  • See [here](http://stackoverflow.com/questions/1967228/controlstyles-doublebuffer-vs-controlstyles-optimizeddoublebuffer) on 'details' of the two types.. – TaW Apr 11 '17 at 19:35
  • @TaW I'm setting both `SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, true);` I've put my currect code in updated question. – Misiu Apr 11 '17 at 19:42
  • Hm, I usually prefer the direct DoubleBuffered proerty over setting styles but it ought to work the same (?).. - When does it flicker? When moving, on each redraw? When drawing child controls?...? – TaW Apr 12 '17 at 06:21
  • @TaW As You can see on second image in my question the flickering only happens when I resize my control, control is fine, only black border isnn't painted correctly. I don't have any child controls. I'm drawing content of my control in `OnPaint`. I'll try to set DoubleBuffered and compare results. – Misiu Apr 12 '17 at 06:33
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/141500/discussion-between-taw-and-misiu). – TaW Apr 12 '17 at 06:43

2 Answers2

2

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

Misiu
  • 4,738
  • 21
  • 94
  • 198
  • Both your question and your answer are useful, but it seems you are reinventing the wheel. Have you ever thought of deriving from `UserControl`? – Reza Aghaei Apr 14 '17 at 22:23
1

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.

Community
  • 1
  • 1
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398