How to: Subclass a Button by Using Native Callbacks
[This documentation is for preview only, and is subject to change in later releases. Blank topics are included as placeholders.]
This topic is an exercise for demonstrating how to subclass a Windows Forms control to receive callbacks from native code using a managed Windows procedure (WndProc). This example program is described in detail in Subclassing Controls with a Managed Window Procedure.
This example program shows how to display a gradient fill with a control subclassed from Button. It requires handling Windows messages. An easier way to display a gradient fill in a button is to create a custom control derived from Control. For an example, see How to: Display a Gradient Fill.
To subclass a Button control to display a gradient fill
In Microsoft Visual Studio 2005, create a Smart Device Pocket PC project.
Add the Gradientfill class to your project.
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 } }
Add the GradientFilledButton class to your project.
// 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(); } }
Add the Win32 helper class to your project. This code is available in How to: Use a Helper Class for Platform Invokes.
Add the WinProcHooker class to our project. This code is available in How to: Use a Class for Hooking Windows Procedures.
Declare a form variable named buttonGF of type GradientFilledButton.
private GradientFilledButton buttonGF;
Add the following code, which initializes the subclassed button control, to the constructor of the Form1 class. This code should follow the call to the InitializeComponent method. You can specify the start and ending colors of the gradient fill and either a TopToBottom or LeftToRight fill direction.
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);
Add the event-handling code for the button's Click event to the Form1 class.
// 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"); }
Optionally override the OnPaint method to paint the background of the form with the gradient fill pattern.
// 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(); }
Compile and run the application.
See Also
Tasks
How to: Use a Class for Hooking Windows Procedures
How to: Use a Helper Class for Platform Invokes
How to: Subclass a TreeView by Using Native Callbacks
Concepts
.NET Compact Framework How-to Topics