0

I am working on a set of UI Elements currently and the ComboBox is giving me a hard time.

My goal is to paint a 1px border around the element which will change color when the mouse hovers over it.

I used the WndProc() Method to react to the WM_PAINT Message and draw the border:

protected override void WndProc(ref Message m)
{
        if (m.Msg == (int)WindowsMessages.Win32Messages.WM_PAINT)
        {
            base.WndProc(ref m);
            if (hovered || Focused)
            {
                //Paint frame with hovered color when being hovered over
                PaintHelper.PaintFrame(this, _frameColorHovered, _frameWidth);
            }
            else
            {
                //Paint frame with standart color
                PaintHelper.PaintFrame(this, _frameColor, _frameWidth);
            }

        }
        .
        .
        .
}

Paint helper method looks as follows:

public static void PaintFrame(Control target, Color color, int frameWidth)
{
        IntPtr dc = GetWindowDC(target.Handle);
        using (Graphics g = Graphics.FromHdc(dc))
        {
            using (Pen p = new Pen(color, frameWidth))
                g.DrawRectangle(p, frameWidth / 2, frameWidth / 2, target.Width - frameWidth, target.Height - frameWidth);
        }
 }

So far so good but the border keeps flickering when the mouse moves out or inside of the the Elements bounds! I did some research but everyone uses the UserPaint flag which is not an option for me.

So: Is there any way to remove the flickering without painting the whole control myself?

  • I don't know whether it is related, but you don't call ReleaseDC in PaintFrame function. Also, try to use WM_NCPAINT instead of WM_PAINT. – Alex F Jun 04 '18 at 13:04
  • NCPAINT does not work because I am not painting in non-client area. So WM_PAINT would just paint over my border ifI would paint it in NCPAINT. – Jan Hildebrandt Jun 04 '18 at 13:11
  • 1
    A more sensible way to do this is to draw a rectangle *around* the control instead of trying to paint on top of it. You'd use its Parent's Paint event. You can try [this trick](https://stackoverflow.com/a/89125/17034) to suppress the inevitable flicker you get now. – Hans Passant Jun 04 '18 at 13:54
  • I've posted something like this (the answer has not been accepted though): [Changing the border color of a Combobox on focus](https://stackoverflow.com/questions/49302435/changing-the-border-color-of-a-combobox-on-focus?answertab=active#tab-top). The custom control registers the `Enter` and `Leave` events, but you can change them to `MouseEnter` and `MouseLeave` without any modificaton to the code. You can drop the custom control onto a Form and try it out. – Jimi Jun 04 '18 at 15:18
  • Thank you for your suggestions but neither of them are working for me. @HansPassant turning that flag on seems to change nothing in my case. – Jan Hildebrandt Jun 05 '18 at 09:05
  • @Jimi I thought about that as well. The problem is you can still see parts of the rounded border because we are not painting on top of it. That looks weird and not solid. – Jan Hildebrandt Jun 05 '18 at 09:07

1 Answers1

0

The example below only sets the border when the cursor is actually within the control and only highlights it, but it should be easy enough to adjust the code to your needs.

The code does not cause any flickering at all for me.

In the past I tested the performance of WM_PAINT vs OnPaint several times and most often you get much less flickering and issues when using OnPaint. If it's possible to use OnPaint, use OnPaint... Before and after WM_PAINT a lot of stuff happens which you won't notice or know of so it's usually better to go with the managed OnPaint method instead.

internal class MyComboBox : ComboBox
{
    const int WM_PAINT = 0x000F;        
    
    private Pen _borderPen;

    private float _borderThickness = 1.0f;
    public float BorderThickness
    {
        get
        {
            return _borderThickness;
        }
        set
        {
            if (value != _borderThickness)
            {
                _borderThickness = value;
                _borderPen?.Dispose();
                _borderPen = new Pen(_borderColor, _borderThickness);
            }
        }
    }


    private Color _borderColor = Color.FromArgb(255, 0, 0);

    [DefaultValue(typeof(Color), "255, 0, 0")]
    [Browsable(true)]
    [EditorBrowsable(EditorBrowsableState.Always)]
    public Color BorderColor
    {
        get
        {
            return _borderColor;
        }
        set
        {
            if (value != _borderColor)
            {
                _borderColor = value;
                _borderPen?.Dispose();
                _borderPen = new Pen(_borderColor, _borderThickness);
            }
        }
    }


    // Signals if the mouse is currently within the controls bounds.
    private bool _mouseOver = false;
    
    protected override void OnMouseEnter(EventArgs e)
    {
        base.OnMouseEnter(e);
        _mouseOver = true;
    }

    protected override void OnMouseLeave(EventArgs e)
    {
        base.OnMouseLeave(e);
        _mouseOver = false;
    }


    private void HandleBorderPaint(IntPtr hWnd)
    {
        // Exit if mouse over flag is not set.
        if (!(_mouseOver)) {
            return;
        }

        // Adjust the border rectangle. 
        // You need to deflate the client rectangle so 
        // the border will be displayed correctly.
        Rectangle border = ClientRectangle;
        // If border thickness is below 0 take one. 
        // Make the value negative as we want to deflate, not inflate.
        int deflate = -(int)Math.Max(1f, _borderThickness);
        border.Inflate(deflate, deflate);

        using (var gr = Graphics.FromHwnd(hWnd))
        {
            gr.DrawRectangle(_borderPen, border);
        }
    }


    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case WM_PAINT:
                {
                    base.WndProc(ref m);

                    // Paint the custom border 
                    // after the system did its job.
                    HandleBorderPaint(m.HWnd);
                    
                    break;
                }
                
            default:
                {
                    base.WndProc(ref m);
                    
                    break;
                }                    
        }
    }


    protected override void Dispose(bool disposing)
    {
        _borderPen?.Dispose();

        base.Dispose(disposing);
    }


    public MyComboBox()
        : base()
    {
        _borderPen = new Pen(BorderColor, 1);
    }
}
peter
  • 21
  • 2