Практическое руководство. Реализация подкласса кнопки с помощью функций обратного вызова на машинном коде
Обновлен: Ноябрь 2007
В этом разделе приводится упражнение, в котором создается подкласс элемента управления Windows Forms для получения обратных вызов от машинного кода с помощью управляемой процедуры Windows (WndProc). Этот пример подробно описан в разделе Создание подклассов элементов управления при помощи управляемой процедуры окна.
В этом примере демонстрируется отображение градиентной заливки в элементе управления, созданном как подкласс Button. Для этого требуется обработка сообщений Windows. Более простой способ отображения градиентной заливки в кнопке состоит в том, что необходимо создать пользовательский элемент управления, унаследованный от класса Control. Пример см. в разделе Пошаговое руководство. Отображение градиентной заливки.
Чтобы создать подкласс кнопки для отображения градиентной заливки, выполните следующие действия.
В Microsoft Visual Studio 2005 создайте проект для интеллектуальных устройств.
Добавьте в проект класс Gradientfill.
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 } }
Добавьте в проект класс GradientFilledButton.
// 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(); } }
Добавьте в проект вспомогательный класс Win32. Этот код доступен в Практическое руководство. Использование вспомогательного класса для вызова неуправляемого кода.
Добавьте в проект класс WinProcHooker. Этот код доступен в Пошаговое руководство. Использование класса для подключения процедур Windows.
Объявите переменную формы с именем buttonGF типа GradientFilledButton
private GradientFilledButton buttonGF;
Добавьте следующий код, инициализирующий подкласс пользовательского элемента управления "кнопка", в конструктор класса Form1. Этот код должен следовать за вызовом метода InitializeComponent. Необходимо указать начальный и конечный цвета градиентной заливки и направление заливки TopToBottom или 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);
Добавьте код обработки события Click кнопки в класс 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"); }
При необходимости переопределите метод OnPaint, чтобы залить фон формы при помощи шаблона градиентной заливки.
// 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(); }
Скомпилируйте и запустите приложение.
См. также
Задачи
Пошаговое руководство. Использование класса для подключения процедур Windows
Практическое руководство. Использование вспомогательного класса для вызова неуправляемого кода
Основные понятия
Разделы руководства по платформе .NET Compact Framework