c# - Controles de renderizado en vidrio: solución encontrada, necesita doble buffering/perfeccionamiento
.net winapi (2)
Yo (¡finalmente!) Encontré una forma de renderizar los controles de Windows.Forms sobre el vidrio que no parece tener ningún inconveniente importante ni gran tiempo de implementación. Está inspirado en este artículo de Coded, que básicamente explica cómo anular de forma nativa la pintura de controles para dibujar sobre ellos.
Usé ese enfoque para renderizar el control en un mapa de bits y volver a pintarlo con GDI + y el canal alfa apropiado sobre el área de pintura de NativeWindow. La implementación es simple, pero podría perfeccionarse para la usabilidad, pero ese no es el punto de esta pregunta. Los resultados son, sin embargo, bastante satisfactorios:
Sin embargo, hay 2 áreas que necesitan ser arregladas para que esto sea realmente útil.
- Double-buffering , porque el parpadeo entre esta imagen superpuesta y el control real es frecuente y horrible (compruébalo con el código). Establecer el control básico como doble buffer con
SetStyles(this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true)
no funciona, pero sospecho que podemos hacerlo funcionar con un poco de prueba y error. Algunos controles no funcionan . He podido hacer el siguiente trabajo:
- Caja de texto
- MaskedComboBox
- ComboBox (DropDownStyle == DropDownList)
- Cuadro de lista
- CheckedListBox
- Vista de la lista
- Vista de árbol
- DateTimePicker
- MonthCalendar
Pero no puedo hacer que funcionen, aunque no veo por qué no. Mi conjetura es que el asa de NativeWindow real hace referencia al control completo, mientras que necesito hacer referencia a la parte de "entrada" (textual), probablemente un elemento secundario. Cualquier ayuda de los expertos de WinAPI sobre cómo obtener el control de la ventana de entrada es bienvenida.
- ComboBox (DropDownStyle! = DropDownList)
- NumericUpDown
- RichTextBox
Pero arreglar el doble buffering sería el enfoque principal para la usabilidad.
Aquí hay un uso de muestra:
new GlassControlRenderer(textBox1);
Aquí está el código:
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)
}
}
Estaría encantado de arreglar esto, y de una vez por todas tengo una manera real de renderizar en vidrio, para todos los controles .NET, sin WPF.
EDITAR: Posibles rutas para doble buffer / anti parpadeo:
- Al eliminar la línea
this.Control.Invalidate()
elimina el parpadeo, pero se interrumpe el tipeo en un cuadro de texto. Probé el método WM_SETREDRAW y el método SuspendLayout, sin suerte:
[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; } }
Aquí hay una versión con mucho menos parpadeo, sin embargo, no es perfecta.
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)
}
}
Tuve un problema con el parpadeo antes (muchos controles en el formulario, controles de usuario). Intenté casi todo. Esto es lo que funcionó para mí:
¿Has intentado poner esto en tu clase de forma?
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED
cp.ExStyle |= 0x00080000; // WS_EX_LAYERED
return cp;
}
}
Y en su constructor debe habilitar el doble almacenamiento en búfer; de lo contrario, no funcionará:
this.DoubleBuffered = true;
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
Solo funciona cuando Aero está habilitado, si no puede empeorar el parpadeo.
y también puedes agregar esto
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED
return cp;
}
}
a su clase UserControls.