Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

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);

0 коммент.:

Отправить комментарий

Copyright 2007-2011 Chabster