How to customize ToolStripDropDown scroll buttons
Не так давно я нашел способ как нарисовать свои кнопки для прокрутки ToolStripDropDownMenu
, которое не влезает в экран. В .NET-е все как всегда - все до того настраиваемо, что настроить толком ничего не получается. К примеру, есть класс ToolStripRenderer
, который позволяет менять внешний вид некоторых элементов меню. Практически всех. Кнопки прокрутки в этот список не попали. И что делать, если на красивом меню торчат эти убогие кнопки? Решение далее.
Сразу хочу сказать, что можно было поступить иначе - сделать скроллирование меню вручную. Но этот вариант был отвергнут как слишком сложный. Поддержка скроллирования есть даже в самом базовом классе ToolStrip
, но там нужные методы - internal или даже virtual internal! Охуенный объектно-ориентированный подход!
Итак, решение состоит в следующем: каждая кнопка прокрутки - это элемент управления, который создает внутренний (странно, да?) класс internal class ToolStripScrollButton : ToolStripControlHost
:
private static Control CreateControlInstance(bool up) { StickyLabel label = new StickyLabel(); label.ImageAlign = ContentAlignment.MiddleCenter; label.Image = up ? UpImage : DownImage; return label; }
StickyLabel
- это у нас internal class StickyLabel : Label
. На полотно ToolStripDropDownMenu
добавляются два таких экземпляра - сверху и снизу. Нужно найти эти экземпляры, сделать им subclass, а также найти способ поменять высоту (высота StickyLabel считается из Image
).
Следующий кусок кода находит экземпляры и выполняет две перечисленные операции:
#region Scroll buttons paint HACK private StickyLabelSubclass _topLabelSubclass; private StickyLabelSubclass _bottomLabelSubclass; protected override void OnOpened(EventArgs e) { // WARNING: When menu is opened - search for StickyLabels and apply our HACK. foreach (Control control in Controls) { Type type = control.GetType(); if (type.Name != "StickyLabel" || !control.Visible) { continue; } Label label = control as Label; if (label == null) { continue; } // This information is from .NET reflector: // ============================================================================== // public override Size GetPreferredSize(Size constrainingSize); // Declaring Type: System.Windows.Forms.ToolStripScrollButton // Assembly: System.Windows.Forms, Version=2.0.0.0 // ============================================================================== // empty.Height = (this.Label.Image != null) ? (this.Label.Image.Height + 4) : 0; // ============================================================================== // WARNING: We create bulk bitmap to set the height of the StickyLabel. // 16 - button height, 3 - menu border + menu shadow + button shadow const Int32 BUTTON_HEIGHT = 16; const Int32 BORDERS_AND_SHADOWS_DELTA = 3; const Int32 HACK_DELTA = -4; Bitmap bitmap = new Bitmap(1, BUTTON_HEIGHT + BORDERS_AND_SHADOWS_DELTA + HACK_DELTA); label.Image = bitmap; if (_topLabelSubclass == null) { _topLabelSubclass = new StickyLabelSubclass(label, true); } else if (_bottomLabelSubclass == null) { _bottomLabelSubclass = new StickyLabelSubclass(label, false); } } base.OnOpened(e); } #endregion
Для установки нужной высоты я подкладываю в Image
рисунок нужного размера, а две StickyLabel
ищу в коллекции Controls
.
Вот шаблон для класса StickyLabelSubclass
:
using System; using System.Drawing; using System.Windows.Forms; using CQG.Framework.UI.Controls.Utility; namespace CQG.Framework.UI.Controls.Menu { /// <summary> /// This is the subclass for an .NET Framework internal class. /// /// Reflector's class description: /// ============================================================ /// internal class StickyLabel : Label /// Name: System.Windows.Forms.ToolStripScrollButton+StickyLabel /// Assembly: System.Windows.Forms, Version=2.0.0.0 /// ============================================================ /// </summary> /// <remarks> /// This class is repsonsible for painting scroll buttons. /// </remarks> internal sealed class StickyLabelSubclass : NativeWindow { #region Private fields /// <summary> /// System.Windows.Forms.ToolStripScrollButton+StickyLabel instance. /// </summary> private readonly Label _target; /// <summary> /// Scroll up/down. /// </summary> private readonly bool _toScrollUp; #endregion Private fields #region Construction /// <summary> /// Constructor. /// </summary> /// <param name="target">System.Windows.Forms.ToolStripScrollButton+StickyLabel instance.</param> /// <param name="toScrollUp">Scroll up/down.</param> public StickyLabelSubclass(Label target, bool toScrollUp) { _target = target; _toScrollUp = toScrollUp; if (_target.IsHandleCreated) { targetOnHandleCreated(_target, EventArgs.Empty); } _target.HandleCreated += targetOnHandleCreated; _target.HandleDestroyed += targetOnHandleDestroyed; _target.EnabledChanged += targetOnStateChanged; _target.MouseDown += targetOnStateChanged; _target.MouseUp += targetOnStateChanged; } #endregion Construction #region Overrides /// <summary> /// <see cref="NativeWindow.WndProc"/> /// </summary> /// <param name="m">Message.</param> protected override void WndProc(ref Message m) { if (m.Msg == Win32.WM_PAINT) { onPaint(ref m); return; } base.WndProc(ref m); } #endregion Overrides #region Private methods private void targetOnHandleCreated(object sender, EventArgs args) { AssignHandle(_target.Handle); } private void targetOnHandleDestroyed(object sender, EventArgs args) { ReleaseHandle(); } private void targetOnStateChanged(object sender, EventArgs args) { _target.Invalidate(); } private void onPaint(ref Message m) { Win32.PAINTSTRUCT paint; IntPtr hDC = Win32.BeginPaint(Handle, out paint); if (hDC == IntPtr.Zero) { throw new InvalidOperationException("BeginPaint failed."); } Graphics g = null; try { g = Graphics.FromHdc(hDC); Rectangle clientRect = _target.ClientRectangle; // Lets paint our "buttons" here... } finally { if (g != null) { g.Dispose(); } Win32.EndPaint(Handle, ref paint); } } #endregion Private methods } }
Ну, и утилитарные мелочи для полноты картины:
/// <summary> /// Defines the coordinates of the upper-left and lower-right corners of a rectangle /// </summary> [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left; public int top; public int right; public int bottom; public int Width { get { return right - left; } } public int Height { get { return bottom - top; } } public static explicit operator Rectangle(RECT rect) { return new Rectangle(rect.left, rect.top, rect.Width, rect.Height); } public override string ToString() { return ((Rectangle)this).ToString(); } public void MarshalToIntPtr(IntPtr ptr) { Marshal.StructureToPtr(this, ptr, false); } public static RECT CreateFromIntPtr(IntPtr ptr) { return (RECT)Marshal.PtrToStructure(ptr, typeof(RECT)); } public Rectangle ToRectangle() { return new Rectangle(left, top, right - left, bottom - top); } public RECT(Rectangle rect) { left = rect.Left; right = rect.Right; top = rect.Top; bottom = rect.Bottom; } } /// <summary> /// The PAINTSTRUCT structure contains information for an application. This information can be used to paint /// the client area of a window owned by that application. /// </summary> [StructLayout(LayoutKind.Sequential, Pack = 4)] public struct PAINTSTRUCT { /// <summary> /// A handle to the display DC to be used for painting. /// </summary> public IntPtr hdc; /// <summary> /// Indicates whether the background must be erased. This value is nonzero if the application should erase /// the background. The application is responsible for erasing the background if a window class is created /// without a background brush. For more information, see the description of the hbrBackground member of /// the WNDCLASS structure. /// </summary> public bool fErase; /// <summary> /// A RECT structure that specifies the upper left and lower right corners of the rectangle in which the /// painting is requested, in device units relative to the upper-left corner of the client area. /// </summary> public RECT rcPaint; /// <summary> /// Reserved; used internally by the system. /// </summary> public bool fRestore; /// <summary> /// Reserved; used internally by the system. /// </summary> public bool fIncUpdate; /// <summary> /// Reserved; used internally by the system. /// </summary> [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public Byte[] rgbReserved; } /// <summary> /// The BeginPaint function prepares the specified window for painting and fills a PAINTSTRUCT structure with information /// about the painting. /// </summary> /// <param name="hWnd">Handle to the window to be repainted.</param> /// <param name="lpPaint">Pointer to the <see cref="PAINTSTRUCT"/> structure that will receive painting information.</param> /// <returns> /// If the function succeeds, the return value is the handle to a display device context for the specified window. /// If the function fails, the return value is NULL, indicating that no display device context is available. /// </returns> [DllImport(User32_DLL_NAME)] public static extern IntPtr BeginPaint(IntPtr hWnd, out PAINTSTRUCT lpPaint); /// <summary> /// The EndPaint function marks the end of painting in the specified window. This function is required for each call to the BeginPaint function, but only after painting is complete. /// </summary> /// <param name="hWnd">Handle to the window that has been repainted.</param> /// <param name="lpPaint">Pointer to a <see cref="PAINTSTRUCT"/> structure that contains the painting information retrieved by BeginPaint.</param> /// <returns>The return value is always nonzero.</returns> [DllImport(User32_DLL_NAME)] public static extern bool EndPaint(IntPtr hWnd, ref PAINTSTRUCT lpPaint);