TextBox drawn in WM_PAINT flickers on mouse enter/leave
Asked Answered
W

1

1

I have a custom TextBox in which I draw some place holder text when it's empty. It works pretty well, but it flickers when the mouse enters and leaves the TextBox. It seems related to the border becoming blue when the mouse hovers the control (I'm on Windows 8.1).

Any idea how I could fix this ?

I've tried various SetStyles flags without success.

class MyTextBox : TextBox
{
  public string PlaceHolder { get; set; }

  static readonly Brush sPlaceHolderBrush = new SolidBrush(Color.FromArgb(70, 70, 78));
  static readonly StringFormat sFormat = new StringFormat
  {
     Alignment = StringAlignment.Near,
     LineAlignment = StringAlignment.Center
  };

  private Font mPlaceHolderFont;

  [DllImport("user32")]
  private static extern IntPtr GetWindowDC(IntPtr hwnd);

  protected override void WndProc(ref Message m)
  {
     base.WndProc(ref m);

     if (m.Msg == 0x0F)   
     {
        if (string.IsNullOrEmpty(Text) && !Focused)
        {
           IntPtr dc = GetWindowDC(Handle);
           using (Graphics g = Graphics.FromHdc(dc))
           {
              if (mPlaceHolderFont == null)
                 mPlaceHolderFont = new Font(Font, FontStyle.Italic);

              var rect = new RectangleF(2, 2, Width - 4, Height - 4);
              g.FillRectangle(Brushes.White, rect);
              g.DrawString(PlaceHolder, mPlaceHolderFont, sPlaceHolderBrush, rect, sFormat);
           }
        }
     }
  }
}

I had other problems with overriding OnPaint. Here is the best solution I came up with :

class MyTextBox : TextBox
{
  public string PlaceHolder { get; set; }

  static readonly Brush sPlaceHolderBrush = new SolidBrush(Color.FromArgb(70, 70, 78));
  static readonly StringFormat sFormat = new StringFormat
  {
     Alignment = StringAlignment.Near,
     LineAlignment = StringAlignment.Near
  };

  private Font mPlaceHolderFont;
  private Brush mForegroundBrush;

  protected override void OnHandleCreated(EventArgs e)
  {
     base.OnHandleCreated(e);
     SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
  }

  protected override void OnPaint(PaintEventArgs e)
  {
     var bounds = new Rectangle(-2, -2, Width, Height);
     var rect = new RectangleF(1, 0, Width - 2, Height - 2);

     e.Graphics.FillRectangle(Brushes.White, rect);
     if (string.IsNullOrEmpty(Text) && !Focused)
     {
        if (mPlaceHolderFont == null)
           mPlaceHolderFont = new Font(Font, FontStyle.Italic);

        if (mForegroundBrush == null)
           mForegroundBrush = new SolidBrush(ForeColor);

        e.Graphics.DrawString(PlaceHolder, mPlaceHolderFont, sPlaceHolderBrush, rect, sFormat);
     }
     else
     {
        var flags = TextFormatFlags.Default | TextFormatFlags.TextBoxControl;
        if (!Multiline)
           flags |= TextFormatFlags.SingleLine | TextFormatFlags.NoPadding;

        TextBoxRenderer.DrawTextBox(e.Graphics, bounds, Text, Font, flags, TextBoxState.Selected);
     }
  }
}
Wilsey answered 1/9, 2015 at 11:14 Comment(1)
TextBox wraps the native EDIT control. The grand-daddy of controls on Windows, goes all the way back to 1985. To make it work acceptably on the very limited hardware available back then it had to commit a very grave crime. It paints parts of itself without use WM_PAINT. 30 years of appcompat, its been hacked in every way imaginable, stopped them from ever fixing this. You cannot make this reliable.Salvation
M
2

Is there a special reason for using WM_PAINT instead of OnPaint? In WM_PAINT you obtain a drawing context from the handle, which is always a direct access to the control. In OnPaint you already have a Graphics in the event args, which can be either a buffer or a direct context, depending on the styles.

You mentioned that you have tried a few styles with no success. Firstly I would say try these and move your paint logic into OnPaint:

SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);

If it does not work (a focused control may behave strangely in Windows) and you must stick to WM_PAINT, then create a buffer manually. Your original code draws a white rectangle first, then some text, which causes flickering. You can avoid this by using a buffer:

IntPtr dc = GetWindowDC(Handle);
using (Graphics g = Graphics.FromHdc(dc))
{
    // creating a buffered context
    using (BufferedGraphicsContext context = new BufferedGraphicsContext())
    {
        // creating a buffer for the original Graphics
        using (BufferedGraphics bg = context.Allocate(g, ClientRectangle))
        {
             if (mPlaceHolderFont == null)
                mPlaceHolderFont = new Font(Font, FontStyle.Italic);

             var gBuf = bg.Graphics;
             var rect = ClientRectangle;
             rect.Inflate(-1, -1);
             gBuf.FillRectangle(Brushes.White, rect);
             gBuf.DrawString(PlaceHolder, mPlaceHolderFont, sPlaceHolderBrush, rect, sFormat);

             // copying the buffer onto the original Graphics
             bg.Render(g);
        }
    }
}

ReleaseDC(Handle, dc); // also in user32
Marjoriemarjory answered 1/9, 2015 at 11:40 Comment(3)
It works ! I had read on some other websites that having OnPaint in TextBox was not possible. Thanks for proving him wrong !Wilsey
Putting SetStyle in OnHandleCreated rather than the constructor avoids this issue: #1222528Wilsey
Using the above approach still works well using WM_PAINT and the BufferedGraphicsContext . just fix the part "context.Allocate(e.Graphics , ClientRectangle)" to "context.Allocate(g , ClientRectangle)". and don't forget to call ReleaseDC after using GetWindowDCCreate

© 2022 - 2024 — McMap. All rights reserved.