46

I (finally!) found a way of rendering Windows.Forms controls on glass that doesn't seem to have any major drawback nor any big implementation time. It's inspired by this article from Coded, which basically explains how to natively override the painting of controls to draw over them.

I used that approach to render the control to a bitmap and paint it back with GDI+ and the appropriate alpha channel over the NativeWindow's painting area. The implementation is simple but could be perfected for usability, but that's not the point of this question. The results are, however, quite satisfying:

Real textbox on glass

There are 2 areas that need to be fixed for this to be really usable, however.

  1. Double-buffering, because the flicker between this overlay image and the real control is frequent and horrible (test yourself with the code). Setting the basic control to be double buffered with SetStyles(this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true) doesn't work, but I suspect we can make it work with a little trial and error.
  2. Some controls don't work. I've been able to make the following work:

    • TextBox
    • MaskedComboBox
    • ComboBox (DropDownStyle == DropDownList)
    • ListBox
    • CheckedListBox
    • ListView
    • TreeView
    • DateTimePicker
    • MonthCalendar

    But I can't get these to work, although I don't see why not. My educated guess is that the actual NativeWindow handle I'm referencing the whole control, while I need to reference the "input" (textual) part of it, probably a child. Any help from WinAPI experts on how to get that input window handle is welcome.

    • ComboBox (DropDownStyle != DropDownList)
    • NumericUpDown
    • RichTextBox

But fixing the double buffering would be the main focus for usability.

Here's a sample usage:

new GlassControlRenderer(textBox1);

Here's the code:

public class GlassControlRenderer : NativeWindow
{
    private Control Control;
    private Bitmap Bitmap;
    private Graphics ControlGraphics;

    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case 0xF: // WM_PAINT
            case 0x85: // WM_NCPAINT
            case 0x100: // WM_KEYDOWN
            case 0x200: // WM_MOUSEMOVE
            case 0x201: // WM_LBUTTONDOWN
                this.Control.Invalidate();
                base.WndProc(ref m);
                this.CustomPaint();
                break;

            default:
                base.WndProc(ref m);
                break;
        }
    }

    public GlassControlRenderer(Control control)
    {
        this.Control = control;
        this.Bitmap = new Bitmap(this.Control.Width, this.Control.Height);
        this.ControlGraphics = Graphics.FromHwnd(this.Control.Handle);
        this.AssignHandle(this.Control.Handle);
    }

    public void CustomPaint()
    {
        this.Control.DrawToBitmap(this.Bitmap, new Rectangle(0, 0, this.Control.Width, this.Control.Height));
        this.ControlGraphics.DrawImageUnscaled(this.Bitmap, -1, -1); // -1, -1 for content controls (e.g. TextBox, ListBox)
    }
}

I'd be really glad to fix this, and once and for all have a real way of rendering on glass, for all .NET controls, without WPF.

EDIT: Possible paths for double-buffering/anti-flicker:

  • Removing the line this.Control.Invalidate() removes the flicker, but breaks the typing in a textbox.
  • I've tried the WM_SETREDRAW approach and the SuspendLayout method, with no luck:

    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);
    
    private const int WM_SETREDRAW = 11;
    
    public static void SuspendDrawing(Control parent)
    {
        SendMessage(parent.Handle, WM_SETREDRAW, false, 0);
    }
    
    public static void ResumeDrawing(Control parent)
    {
        SendMessage(parent.Handle, WM_SETREDRAW, true, 0);
        parent.Refresh();
    }
    
    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case 0xF: // WM_PAINT
            case 0x85: // WM_NCPAINT
            case 0x100: // WM_KEYDOWN
            case 0x200: // WM_MOUSEMOVE
            case 0x201: // WM_LBUTTONDOWN
                //this.Control.Parent.SuspendLayout();
                //GlassControlRenderer.SuspendDrawing(this.Control);
                //this.Control.Invalidate();
                base.WndProc(ref m);
                this.CustomPaint();
                //GlassControlRenderer.ResumeDrawing(this.Control);
                //this.Control.Parent.ResumeLayout();
                break;
    
            default:
                base.WndProc(ref m);
                break;
        }
    }
    
Lazlo
  • 7,888
  • 13
  • 69
  • 113
  • 15
    I am impressed by your progress, but also curious why you have taken this path (without doubt a good reason, but I'd like to be educated!). Why going through all these pains, when there is WPF? – Dennis Smit Aug 30 '11 at 23:11
  • 1
    Hey grate work would you mind shearing the full code with an example project once your done, by what you have said the flickering is caused by the `this.Control.Invalidate()` and your saying without that your text does not work have you tried fixing it with out the `this.Control.Invalidate()` you might have to create a keyDown handler that passes all the keyDown action's to the object currently in focus then calls your redraw? (it's just a though i know how easy it is to get lost in complexity) – Barkermn01 Sep 16 '11 at 08:15
  • 1
    When you set the double buffering flag, did you also set ControlStyles.AllPaintingInWmPaint like the documentation suggests? – Bryce Wagner Sep 17 '11 at 13:29
  • I, for one, would really like to see the rest of this code. – Mike Hofer Sep 28 '11 at 21:17
  • Can you explain what you are doing when you notice the flickering? Are you moving the window, resizing, scrolling? – TyCobb Sep 29 '11 at 00:34
  • 1
    @Dennis Smit: I hate Xaml. I feel like designing application controls should not be like designing website controls: stylable, standard-free, etc. I think the base of WPF rendering is good, but the flexibility is overrated. But I don't really want to get into that debate here. – Lazlo Sep 29 '11 at 23:07
  • @BryceWagner I tried that, yes. Didn't work. – Lazlo Sep 29 '11 at 23:07
  • @MikeHofer This is the whole code. – Lazlo Sep 29 '11 at 23:08
  • Just out of interest, as this code is now a year old, did you manage to perfect it? what was your final approach? – Matthew Layton May 03 '13 at 15:33
  • @series0ne No, the answer I wrote below is the best I've been able to do (it's still quite usable, though). – Lazlo Sep 18 '13 at 22:48

2 Answers2

6

Here is a version with much less flickering, still not perfect though.

public class GlassControlRenderer : NativeWindow
{
    private Control Control;
    private Bitmap Bitmap;
    private Graphics ControlGraphics;

    private object Lock = new object();

    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case 0x14: // WM_ERASEBKGND
                this.CustomPaint();
                break;

            case 0x0F: // WM_PAINT
            case 0x85: // WM_NCPAINT

            case 0x100: // WM_KEYDOWN
            case 0x101: // WM_KEYUP
            case 0x102: // WM_CHAR

            case 0x200: // WM_MOUSEMOVE
            case 0x2A1: // WM_MOUSEHOVER
            case 0x201: // WM_LBUTTONDOWN
            case 0x202: // WM_LBUTTONUP
            case 0x285: // WM_IME_SELECT

            case 0x300: // WM_CUT
            case 0x301: // WM_COPY
            case 0x302: // WM_PASTE
            case 0x303: // WM_CLEAR
            case 0x304: // WM_UNDO
                base.WndProc(ref m);
                this.CustomPaint();
                break;

            default:
                base.WndProc(ref m);
                break;
        }
    }

    private Point Offset { get; set; }

    public GlassControlRenderer(Control control, int xOffset, int yOffset)
    {
        this.Offset = new Point(xOffset, yOffset);
        this.Control = control;
        this.Bitmap = new Bitmap(this.Control.Width, this.Control.Height);
        this.ControlGraphics = Graphics.FromHwnd(this.Control.Handle);
        this.AssignHandle(this.Control.Handle);
    }

    public void CustomPaint()
    {
        this.Control.DrawToBitmap(this.Bitmap, new Rectangle(0, 0, this.Control.Width, this.Control.Height));
        this.ControlGraphics.DrawImageUnscaled(this.Bitmap, this.Offset); // -1, -1 for content controls (e.g. TextBox, ListBox)
    }
}
Lazlo
  • 7,888
  • 13
  • 69
  • 113
1

I had a problem with flickering before (lots of controls on the form, user controls). Tried almost everything. This is what worked for me:

Have you tried putting this in your form class?

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED
            cp.ExStyle |= 0x00080000; // WS_EX_LAYERED
            return cp;
        }
    }

And in your constructor you have to enable double buffering, otherwise it won't work:

this.DoubleBuffered = true;
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

It only works when aero is enabled, if not it can make flickering even worse.

and you can also add this

  protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;

                cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED
            return cp;
        }
    } 

to your UserControls class.

Arie
  • 4,869
  • 2
  • 28
  • 49
  • `WS_EX_COMPOSITED` helped me to solve a bit different problem - with RichTextBox used as frequently updated state display - I have enclosed it with panel with `WS_EX_COMPOSITED` ExStyle + `DoubleBuffered`, but it had a little side effect - **even caret stopped blinking** – firda Feb 12 '16 at 20:18