Cómo: Crear subclases de un botón mediante devoluciones de llamada nativas

Actualización: noviembre 2007

Este tema es un ejercicio para mostrar como crear subclases de un control de formularios Windows Forms para recibir devoluciones de llamada del código nativo mediante un procedimiento de ventana (WndProc) administrado. Este programa de ejemplo se describe con detalle en Crear subclases de controles con un procedimiento de ventana administrado.

En este programa de ejemplo se muestra cómo mostrar un relleno de degradado con un control al que se ha creado una subclase a partir de Button. Requiere el control de mensajes de Windows. Una manera más fácil de mostrar un relleno de degradado en un botón es crear un control personalizado derivado de Control. Para obtener un ejemplo, vea Cómo: Mostrar un relleno de degradado.

Para crear subclases de un control de botón para mostrar un relleno de degradado

  1. En Microsoft Visual Studio 2005, cree un proyecto de Smart Device para Pocket PC.

  2. Agregue la clase Gradientfill al proyecto.

    public sealed class GradientFill
    {
        // This method wraps the PInvoke to GradientFill.
        // Parameters:
        //  gr - The Graphics object we are filling
        //  rc - The rectangle to fill
        //  startColor - The starting color for the fill
        //  endColor - The ending color for the fill
        //  fillDir - The direction to fill
        //
        // Returns true if the call to GradientFill succeeded; false
        // otherwise.
        public static bool Fill(
            Graphics gr,
            Rectangle rc,
            Color startColor, Color endColor,
            FillDirection fillDir)
        {
    
            // Initialize the data to be used in the call to GradientFill.
            Win32.TRIVERTEX[] tva = new Win32.TRIVERTEX[2];
            tva[0] = new Win32.TRIVERTEX(rc.X, rc.Y, startColor);
            tva[1] = new Win32.TRIVERTEX(rc.Right, rc.Bottom, endColor);
            Win32.GRADIENT_RECT[] gra = new Win32.GRADIENT_RECT[] {
    new Win32.GRADIENT_RECT(0, 1)};
    
            // Get the hDC from the Graphics object.
            IntPtr hdc = gr.GetHdc();
    
            // PInvoke to GradientFill.
            bool b;
    
            b = Win32.GradientFill(
                    hdc,
                    tva,
                    (uint)tva.Length,
                    gra,
                    (uint)gra.Length,
                    (uint)fillDir);
            System.Diagnostics.Debug.Assert(b, string.Format(
                "GradientFill failed: {0}",
                System.Runtime.InteropServices.Marshal.GetLastWin32Error()));
    
            // Release the hDC from the Graphics object.
            gr.ReleaseHdc(hdc);
    
            return b;
        }
    
        // The direction to the GradientFill will follow
        public enum FillDirection
        {
            //
            // The fill goes horizontally
            //
            LeftToRight = Win32.GRADIENT_FILL_RECT_H,
            //
            // The fill goes vertically
            //
            TopToBottom = Win32.GRADIENT_FILL_RECT_V
        }
    }
    
  3. Agregue la clase GradientFilledButton al proyecto.

    // Extends the standard button control and does some custom
    // drawing with a GradientFill background
    public class GradientFilledButton : Button
    {
        // Creates a new instance of the object
        public GradientFilledButton()
        {
            // Messages required to override
            // in this control's window procedure.
            WndProcHooker.HookWndProc(this,
                new WndProcHooker.WndProcCallback(this.WM_Paint_Handler),
                Win32.WM_PAINT);
            WndProcHooker.HookWndProc(this,
                new WndProcHooker.WndProcCallback(this.WM_LButtonDown_Handler),
                Win32.WM_LBUTTONDOWN);
            WndProcHooker.HookWndProc(this,
                new WndProcHooker.WndProcCallback(this.WM_LButtonUp_Handler),
                Win32.WM_LBUTTONUP);
            WndProcHooker.HookWndProc(this,
                new WndProcHooker.WndProcCallback(this.WM_MouseMove_Handler),
                Win32.WM_MOUSEMOVE);
            WndProcHooker.HookWndProc(this,
                new WndProcHooker.WndProcCallback(this.WM_KeyDown_Handler),
                Win32.WM_KEYDOWN);
            WndProcHooker.HookWndProc(this,
                new WndProcHooker.WndProcCallback(this.WM_KeyUp_Handler),
                Win32.WM_KEYUP);
        }
    
        // Controls the direction in which the button is filled
        public GradientFill.FillDirection FillDirection
        {
            get
            {
                return fillDirectionValue;
            }
            set
            {
                fillDirectionValue = value;
                Invalidate();
            }
        }
        private GradientFill.FillDirection fillDirectionValue;
    
        // The start color for the GradientFill. This is the color
        // at the left or top of the control depending on the value
        // of the FillDirection property.
        public Color StartColor
        {
            get { return startColorValue; }
            set
            {
                startColorValue = value;
                Invalidate();
            }
        }
        private Color startColorValue = Color.Red;
    
        // The end color for the GradientFill. This is the color
        // at the right or bottom of the control depending on the
        // value of the FillDirection property
        public Color EndColor
        {
            get { return endColorValue; }
            set
            {
                endColorValue = value;
                Invalidate();
            }
        }
        private Color endColorValue = Color.Blue;
    
        // This is the offset from the left or top edge of the button
        // to start the gradient fill.
        public int StartOffset
        {
            get { return startOffsetValue; }
            set
            {
                startOffsetValue = value;
                Invalidate();
            }
        }
        private int startOffsetValue;
    
        // This is the offset from the right or bottom edge
        // of the button to end the gradient fill.
        public int EndOffset
        {
            get { return endOffsetValue; }
            set
            {
                endOffsetValue = value;
                Invalidate();
            }
        }
        private int endOffsetValue;
    
        // Used to double-buffer our drawing to avoid flicker between
        // painting the background, border, focus-rect and the
        // text of the control.
        private Bitmap DoubleBufferImage
        {
            get
            {
                if (bmDoubleBuffer == null)
                    bmDoubleBuffer = new Bitmap(
                        this.ClientSize.Width,
                        this.ClientSize.Height);
                return bmDoubleBuffer;
            }
            set
            {
                if (bmDoubleBuffer != null)
                    bmDoubleBuffer.Dispose();
                bmDoubleBuffer = value;
            }
        }
        private Bitmap bmDoubleBuffer;
    
        // Called when the control is resized. When that happens, we need to
        // recreate the bitmap we use for double-buffering.
        // e - The arguments for this event
        protected override void OnResize(EventArgs e)
        {
            DoubleBufferImage = new Bitmap(
                this.ClientSize.Width,
                this.ClientSize.Height);
            base.OnResize(e);
        }
    
        // Called when the control gets focus. We need to repaint the control
        // to ensure the focus rectangle is drawn correctly.
        // e - The arguments for this control
        protected override void OnGotFocus(EventArgs e)
        {
            base.OnGotFocus(e);
            this.Invalidate();
        }
    
        // Called when the control loses focus. We need to repaint the control
        // to ensure the focus rectangle is removed.
        // e - The arguments for this control
        protected override void OnLostFocus(EventArgs e)
        {
            base.OnLostFocus(e);
            this.Invalidate();
        }
    
        // This is set to true when we get a MouseDown event. It is used
        // to determine if we should fire the Click event when we get
        // a MouseUp
        bool gotMouseDown = false;
        bool gotKeyDown = false;
    
        // Called when a mouse button is pressed while the cursor is
        // in the control.
        // e - The arguments for this event.
        protected override void OnMouseDown(MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
                gotMouseDown = true;
            base.OnMouseDown(e);
        }
    
        // Called when a mouse button is released while the cursor is
        // in the control
        // e - The arguments for this event
        protected override void OnMouseUp(MouseEventArgs e)
        {
            base.OnMouseUp(e);
            // If the MouseDown event was fired before this event then
            // that constitutes a Click.
            if ((e.Button == MouseButtons.Left) && gotMouseDown)
            {
                base.OnClick(EventArgs.Empty);
                gotMouseDown = false;
            }
        }
    
        // The callback called when the window receives a WM_MOUSEMOVE
        // message. If we have the mouse captured (the user had previously
        // clicked down on the button), we redraw the button.
        // hwnd - The handle to the window that received the
        // message.
        // wParam - Indicates whether various virtual keys are
        // down.
        // lParam - The coordinates of the cursor
        // handled - Set to true if we don't want to pass this
        // message on to the original window procedure.
        // Returns zero if we process this message.
        int WM_MouseMove_Handler(
            IntPtr hwnd, uint msg, uint wParam, int lParam,
            ref bool handled)
        {
            if (this.Capture)
            {
                Point coord = Win32.LParamToPoint(lParam);
                if (this.ClientRectangle.Contains(coord) !=
                    this.ClientRectangle.Contains(lastCursorCoordinates))
                {
                    DrawButton(hwnd,
                        this.ClientRectangle.Contains(coord));
                }
                lastCursorCoordinates = coord;
            }
            return -1;
        }
        // The coordinates of the cursor the last time we saw a WM_MOUSEMOVE,
        // WM_LBUTTONDOWN or WM_LBUTTONUP message.
        Point lastCursorCoordinates;
    
        // The callback called when the window receives a WM_LBUTTONDOWN
        // message. We capture the mouse and draw the button in the "pushed"
        // state.
        // hwnd - The handle to the window that received the
        // message.
        // wParam - Indicates whether various virtual keys are
        // down.
        // lParam - The coordinates of the cursor.
        // handled - Set to true if we don't want to pass this
        // message on to the original window procedure.
        // Returns zero if we process this message.
        int WM_LButtonDown_Handler(
            IntPtr hwnd, uint msg, uint wParam, int lParam,
            ref bool handled)
        {
            // Start capturing the mouse input.
            this.Capture = true;
            // someone clicked on us so grab the focus
            this.Focus();
    
            // draw the button
            DrawButton(hwnd, true);
    
            // Fire the MouseDown event
            lastCursorCoordinates = Win32.LParamToPoint(lParam);
            OnMouseDown(new MouseEventArgs(MouseButtons.Left, 1,
                lastCursorCoordinates.X, lastCursorCoordinates.Y, 0));
    
            // We have handled this windows message and we don't want the
            // sub-classed window to do anything else.
            handled = true;
            return 0;
        }
    
        // The callback called when the window receives a WM_KEYDOWN message.
        // If the key was the spacebar, We draw the button in the "pushed"
        // state.
        // hwnd - The handle to the window that received the
        // message
        // wParam - Specifies the virtual-key code of the
        // non system key.
        // lParam - Specifies various attributes about the key
        // that is down.
        // handled - Set to true if we don't want to pass this
        // message on to the original window procedure
        // Returns>Zero if we process this message.
        int WM_KeyDown_Handler(
            IntPtr hwnd, uint msg, uint wParam, int lPAram,
            ref bool handled)
        {
            if ((wParam == Win32.VK_SPACE) ||
                (wParam == Win32.VK_RETURN))
            {
                DrawButton(hwnd, true);
                handled = true;
                gotKeyDown = true;
            }
            return handled ? 0 : -1;
        }
    
        // The callback called when the window receives a WM_KEYUP message.
        // If the key was the spacebar, We draw the button in the "un-pushed"
        // state and fire the Click event.
        // hwnd - The handle to the window that received the
        // message
        // wParam - Specifies the virtual-key code of the non-
        // system key.
        // lParam - Specifies various attributes about the key
        // that is down.
        // handled - Set to true if we don't want to pass this
        // message
        // on to the original window procedure
        // Returns zero if we process this message.
        int WM_KeyUp_Handler(
            IntPtr hwnd, uint msg, uint wParam, int lParam,
            ref bool handled)
        {
            if (gotKeyDown &&
                ((wParam == Win32.VK_SPACE) ||
                (wParam == Win32.VK_RETURN)))
            {
                DrawButton(hwnd, false);
                OnClick(EventArgs.Empty);
                handled = true;
                gotKeyDown = false;
            }
            return handled ? 0 : -1;
        }
    
        // The callback called when the window receives a WM_LBUTTONUP
        // message. We release capture on the mouse, draw the button in the
        // "un-pushed" state and fire the  OnMouseUp event if the cursor was
        // let go of inside our client area.
        // hwnd - The handle to the window that received the
        // message
        // wParam - Indicates whether various virtual keys are
        // down.
        // lParam - The coordinates of the cursor
        // handled - Set to true if we don't want to pass this
        // message
        // on to the original window procedure
        // Returns zero if we process this message.
        int WM_LButtonUp_Handler(
            IntPtr hwnd, uint msg, uint wParam, int lParam,
            ref bool handled)
        {
            this.Capture = false;
    
            DrawButton(hwnd, false);
    
            lastCursorCoordinates = Win32.LParamToPoint(lParam);
            if (this.ClientRectangle.Contains(lastCursorCoordinates))
                OnMouseUp(new MouseEventArgs(MouseButtons.Left, 1,
                    lastCursorCoordinates.X, lastCursorCoordinates.Y, 0));
            handled = true;
            return 0;
        }
    
    
        // The callback called when the window receives a WM_PAINT message.
        // We draw the button in the appropriate state.
        // hwnd - The handle to the window that received the
        // message
        // wParam - Indicates whether various virtual keys are
        // down.
        // lParam - The coordinates of the cursor
        // handled - Set to true if we don't want to pass this
        // message on to the original window procedure
        // Returns zero if we process this message.
        int WM_Paint_Handler(
            IntPtr hwnd, uint msg, uint wParam, int lParam,
            ref bool handled)
        {
            Win32.PAINTSTRUCT ps = new Win32.PAINTSTRUCT();
    
            Graphics gr = Graphics.FromHdc(Win32.BeginPaint(hwnd, ref ps));
            DrawButton(gr, this.Capture &&
                (this.ClientRectangle.Contains(lastCursorCoordinates)));
            gr.Dispose();
            Win32.EndPaint(hwnd, ref ps);
            handled = true;
            return 0;
        }
    
        // Gets a Graphics object for the provided window handle and then
        // calls DrawButton(Graphics, bool).
        // hwnd - The handle to the window to draw as a
        // button
        // pressed - If true, the button is draw in the
        // depressed state
        void DrawButton(IntPtr hwnd, bool pressed)
        {
            IntPtr hdc = Win32.GetDC(hwnd);
            Graphics gr = Graphics.FromHdc(hdc);
            DrawButton(gr, pressed);
            gr.Dispose();
            Win32.ReleaseDC(hwnd, hdc);
        }
    
        // Draws the button on the specified Graphics in the specified
        // state.
        // gr - The Graphics object on which to draw the
        // button
        // pressed - If true, the button is draw in the
        // depressed state
        void DrawButton(Graphics gr, bool pressed)
        {
            // get a Graphics object from our background image
            Graphics gr2 = Graphics.FromImage(DoubleBufferImage);
    
            // fill solid up until where the gradient fill starts
            if (startOffsetValue > 0)
            {
                if (fillDirectionValue ==
                    GradientFill.FillDirection.LeftToRight)
                {
                    gr2.FillRectangle(
                        new SolidBrush(pressed ? EndColor : StartColor),
                        0, 0, startOffsetValue, Height);
                }
                else
                {
                    gr2.FillRectangle(
                        new SolidBrush(pressed ? EndColor : StartColor),
                        0, 0, Width, startOffsetValue);
                }
            }
    
            // draw the gradient fill
            Rectangle rc = this.ClientRectangle;
            if (fillDirectionValue == GradientFill.FillDirection.LeftToRight)
            {
                rc.X = startOffsetValue;
                rc.Width = rc.Width - startOffsetValue - endOffsetValue;
            }
            else
            {
                rc.Y = startOffsetValue;
                rc.Height = rc.Height - startOffsetValue - endOffsetValue;
            }
            GradientFill.Fill(
                gr2,
                rc,
                pressed ? endColorValue : startColorValue,
                pressed ? startColorValue : endColorValue,
                fillDirectionValue);
    
            // fill solid from the end of the gradient fill to the edge of the
            // button
            if (endOffsetValue > 0)
            {
                if (fillDirectionValue ==
                    GradientFill.FillDirection.LeftToRight)
                {
                    gr2.FillRectangle(
                        new SolidBrush(pressed ? StartColor : EndColor),
                        rc.X + rc.Width, 0, endOffsetValue, Height);
                }
                else
                {
                    gr2.FillRectangle(
                        new SolidBrush(pressed ? StartColor : EndColor),
                        0, rc.Y + rc.Height, Width, endOffsetValue);
                }
            }
    
            // draw the text
            StringFormat sf = new StringFormat();
            sf.Alignment = StringAlignment.Center;
            sf.LineAlignment = StringAlignment.Center;
            gr2.DrawString(this.Text, this.Font,
                new SolidBrush(this.ForeColor),
                this.ClientRectangle, sf);
    
            // draw the border.
            // we need to shrink the width and height by 1 otherwise we
            // won't get any border on the right or bottom.
            rc = this.ClientRectangle;
            rc.Width--;
            rc.Height--;
            Pen pen = new Pen(SystemColors.WindowFrame);
            // focused buttons have a thicker border on device
            if (this.Focused)
                pen = new Pen(SystemColors.WindowFrame, 3f);
            gr2.DrawRectangle(pen, rc);
    
            // draw from the background image onto the screen
            gr.DrawImage(DoubleBufferImage, 0, 0);
            gr2.Dispose();
        }
    }
    
  4. Agregue la clase auxiliar Win32 al proyecto. Este código está disponible en Cómo: Usar una clase auxiliar para las invocaciones de la plataforma.

  5. Agregue la clase WinProcHooker al proyecto. Este código está disponible en Cómo: Utilizar una clase para enlazar procedimientos de Windows.

  6. Declare una variable de formulario denominada buttonGF de tipo GradientFilledButton.

    private GradientFilledButton buttonGF;
    
  7. Agregue el código siguiente, que inicializa el control personalizado subclasificado, al constructor de la clase Form1. Este código debería seguir la llamada al método InitializeComponent. Puede especificar los colores iniciales y finales de relleno de degradado y la dirección de relleno TopToBottom o LeftToRight.

    InitializeComponent();
    this.buttonGF = new GradientFilledButton();
    this.buttonGF.EndColor = System.Drawing.Color.White;
    this.buttonGF.Location = new System.Drawing.Point(71, 24);
    this.buttonGF.Name = "button1";
    this.buttonGF.Size = new System.Drawing.Size(100, 23);
    this.buttonGF.StartColor = System.Drawing.Color.Turquoise;
    this.buttonGF.TabIndex = 1;
    this.buttonGF.Text = "button1";
    this.buttonGF.Click += new System.EventHandler(this.button_Click);
    this.Controls.Add(buttonGF);
    
    
  8. Agregue el código de control de eventos para el evento Click del botón a la clase Form1.

    // The event handler called when a button is clicked
    // sender - The object that raised this event.
    // e - The arguments for this event.
    void button_Click(object sender, System.EventArgs e)
    {
        MessageBox.Show("Clicked", "Click event handler");
    }
    
    
  9. Si lo desea, reemplace el método OnPaint para pintar el fondo del formulario con el modelo de relleno de degradado.

    // Paints the background of the form with a GradientFill pattern.
    protected override void OnPaintBackground(PaintEventArgs e)
    {
        // On Windows Mobile Pocket PC 2003, the call to GradientFill
        // fails with GetLastError() returning 87 (ERROR_INVALID_PARAMETER)
        // when e.Graphics is used.
        // Instead, fill into a bitmap and then draw that onto e.Graphics.
        Bitmap bm = new Bitmap(Width, Height);
        Graphics gr = System.Drawing.Graphics.FromImage(bm);
    
        GradientFill.Fill(
            gr,
            this.ClientRectangle,
            Color.LightCyan, Color.SlateBlue,
            GradientFill.FillDirection.TopToBottom);
        e.Graphics.DrawImage(bm, 0, 0);
        gr.Dispose();
        bm.Dispose();
    }
    
  10. Compile y ejecute la aplicación.

Vea también

Tareas

Cómo: Utilizar una clase para enlazar procedimientos de Windows

Cómo: Usar una clase auxiliar para las invocaciones de la plataforma

Cómo: Crear subclases de un control TreeView mediante devoluciones de llamada nativas

Conceptos

.Temas "Cómo..." de .NET Compact Framework

Otros recursos

Interoperabilidad en .NET Compact Framework