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

Use ATL string convertion properly

В ATL 7 появилось новое семейство СX2Y макросов для конвертации строк из текущей кодовой страницы в UNICODE и обратно. И я заметил, что их часто используют абсолютно неправильно! Например:

const std::wstring some = L"Some";
AfxMessageBox(CW2T(some.c_str()));

Код выглядит, как не содержащий ошибок. Но на самом деле это не так. Взгляните на макрос CW2T когда определен _UNICODE (atlconv.h):

#ifdef _UNICODE

    #define CW2T CW2W

А теперь самое интересное:

template< int t_nBufferLength = 128 >
class CW2WEX
{
public:
    CW2WEX( _In_opt_ LPCWSTR psz ) throw(...) :
        m_psz( m_szBuffer )
    {
        Init( psz );
    }
    CW2WEX( _In_opt_ LPCWSTR psz, UINT nCodePage ) throw(...) :
        m_psz( m_szBuffer )
    {
        (void)nCodePage;  // Code page doesn't matter

        Init( psz );
    }
    ~CW2WEX() throw()
    {
        AtlConvFreeMemory(m_psz,m_szBuffer,t_nBufferLength);
    }

    operator LPWSTR() const throw()
    {
        return( m_psz );
    }

private:
    void Init( _In_opt_ LPCWSTR psz ) throw(...)
    {
        if (psz == NULL)
        {
            m_psz = NULL;
            return;
        }
        int nLength = lstrlenW( psz )+1;
        AtlConvAllocMemory(&m_psz,nLength,m_szBuffer,t_nBufferLength);
        ATLASSUME(m_psz != NULL);
        Checked::memcpy_s( m_psz, nLength*sizeof( wchar_t ), psz, nLength*sizeof( wchar_t ));
    }

public:
    LPWSTR m_psz;
    wchar_t m_szBuffer[t_nBufferLength];

private:
    CW2WEX( const CW2WEX& ) throw();
    CW2WEX& operator=( const CW2WEX& ) throw();
};
typedef CW2WEX<> CW2W;

CW2WEX выполняет копирование строки даже в случае, когда этого можно избежать, как в примере выше!

Используйте макросы CX2CY чтобы избежать лишнего копирования строк:

#ifdef _UNICODE

    #define CW2T CW2W
    #define CW2TEX CW2WEX
    #define CW2CT CW2CW
    #define CW2CTEX CW2CWEX
    #define CT2W CW2W
    #define CT2WEX CW2WEX
    #define CT2CW CW2CW
    #define CT2CWEX CW2CWEX

template< int t_nBufferLength = 128 >
class CW2CWEX
{
public:
    CW2CWEX( _In_ LPCWSTR psz ) throw(...) :
        m_psz( psz )
    {
    }
    CW2CWEX( _In_ LPCWSTR psz, UINT nCodePage ) throw(...) :
        m_psz( psz )
    {
        (void)nCodePage;
    }
    ~CW2CWEX() throw()
    {
    }

    operator LPCWSTR() const throw()
    {
        return( m_psz );
    }

public:
    LPCWSTR m_psz;

private:
    CW2CWEX( const CW2CWEX& ) throw();
    CW2CWEX& operator=( const CW2CWEX& ) throw();
};
typedef CW2CWEX<> CW2CW;

Pretty print XML using MSXML6

Задача простая - отформатировать красиво XML.

MSXML6 я обычно использую следующим образом:

#import <msxml6.dll> rename_namespace("MSXML6")

Заголовочный файл с классом XMLUtility.h:

#pragma once

struct XMLUtility
{

    static _bstr_t PrettyPrint(const MSXML6::IXMLDOMNodePtr &pSrc);

};

cpp файл с классом XMLUtility.cpp:

#include "stdafx.h"
#include "XMLUtility.h"

namespace {

    using namespace MSXML6;

    class SAXContentHandlerFilter : public ISAXContentHandler
    {

    public:
        SAXContentHandlerFilter(const MSXML6::ISAXContentHandlerPtr &pTarget) : _pTarget(pTarget), _charactersSkipThreshold(INT_MAX) {
            ASSERT(pTarget);
        }
        virtual ~SAXContentHandlerFilter() {}

    public:
        // IUnknown implementation
        HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppvObj) {
            if (!ppvObj)
                return(E_POINTER);

            if (riid == IID_IUnknown || riid == __uuidof(ISAXContentHandler)) {
                *ppvObj = this;
            }
            else {
                *ppvObj = NULL;
                return(E_NOINTERFACE);
            }

            AddRef();
            return(S_OK);
        }

        ULONG STDMETHODCALLTYPE AddRef() {
            return(InterlockedIncrement(&_refCount));
        }

        ULONG STDMETHODCALLTYPE Release() {
            const ULONG count = InterlockedDecrement(&_refCount);
            if (count == 0)
                delete this;
            return(count);
        }


        // ISAXContentHandler implementation
    public:
        STDMETHODIMP raw_putDocumentLocator(struct ISAXLocator *pLocator) {
            return(_pTarget->raw_putDocumentLocator(pLocator));
        }

        STDMETHODIMP raw_startDocument() {
            return(_pTarget->raw_startDocument());
        }

        STDMETHODIMP raw_endDocument() {
            return(_pTarget->raw_endDocument());
        }

        STDMETHODIMP raw_startPrefixMapping(unsigned short *pwchPrefix, int cchPrefix, unsigned short *pwchUri, int cchUri) {
            return(_pTarget->raw_startPrefixMapping(pwchPrefix, cchPrefix, pwchUri, cchUri));
        }

        STDMETHODIMP raw_endPrefixMapping(unsigned short *pwchPrefix, int cchPrefix) {
            return(_pTarget->raw_endPrefixMapping(pwchPrefix, cchPrefix));
        }

        STDMETHODIMP raw_startElement(unsigned short *pwchNamespaceUri, int cchNamespaceUri, unsigned short *pwchLocalName, int cchLocalName, unsigned short *pwchQName, int cchQName, struct ISAXAttributes *pAttributes) {
            return(_pTarget->raw_startElement(pwchNamespaceUri, cchNamespaceUri, pwchLocalName, cchLocalName, pwchQName, cchQName, pAttributes));
        }

        STDMETHODIMP raw_endElement(unsigned short *pwchNamespaceUri, int cchNamespaceUri, unsigned short *pwchLocalName, int cchLocalName, unsigned short *pwchQName, int cchQName) {
            return(_pTarget->raw_endElement(pwchNamespaceUri, cchNamespaceUri, pwchLocalName, cchLocalName, pwchQName, cchQName));
        }

        STDMETHODIMP raw_characters(unsigned short *pwchChars, int cchChars) {
            const bool skip = _charactersSkipThreshold < cchChars && _pSAXLexicalHandler;
            if (!skip) {
                return(_pTarget->raw_characters(pwchChars, cchChars));
            }
            else {
                CStringW skipped;
                skipped.Format(L" %d characters have been skipped ", cchChars);
                return(_pSAXLexicalHandler->comment(reinterpret_cast<unsigned short *>(skipped.LockBuffer()), skipped.GetLength()));
            }
        }

        STDMETHODIMP raw_ignorableWhitespace(unsigned short *pwchChars, int cchChars) {
            return(_pTarget->raw_ignorableWhitespace(pwchChars, cchChars));
        }

        STDMETHODIMP raw_processingInstruction(unsigned short *pwchTarget, int cchTarget, unsigned short *pwchData, int cchData) {
            return(_pTarget->raw_processingInstruction(pwchTarget, cchTarget, pwchData, cchData));
        }

        STDMETHODIMP raw_skippedEntity(unsigned short *pwchName, int cchName) {
            return(_pTarget->raw_skippedEntity(pwchName, cchName));
        }

    public:
        void SetLexicalHandler(const MSXML6::ISAXLexicalHandlerPtr &pSAXLexicalHandler) {
            ASSERT(pSAXLexicalHandler);

            _pSAXLexicalHandler = pSAXLexicalHandler;
        }

        void SetCharactersSkipThreshold(int charactersSkipThreshold) {
            ASSERT(_pSAXLexicalHandler);

            _charactersSkipThreshold = charactersSkipThreshold;
        }

    private:
        const MSXML6::ISAXContentHandlerPtr _pTarget;
        LONG _refCount;
        // Filter options
        ISAXLexicalHandlerPtr _pSAXLexicalHandler;
        int _charactersSkipThreshold;

    };

}

_bstr_t XMLUtility::PrettyPrint(const MSXML6::IXMLDOMNodePtr &pSrc) {
    ASSERT(pSrc);
    if (!pSrc) {
        return(L"<NULL/>");
    }

    using namespace MSXML6;

    HRESULT hr = S_OK;

    IMXWriterPtr pMXWriter;
    if (FAILED(hr = pMXWriter.CreateInstance(__uuidof(MXXMLWriter60)))) {
        return(pSrc->xml);
    }

    pMXWriter->indent = true;
    pMXWriter->omitXMLDeclaration = true;

    const ISAXContentHandlerPtr pSAXContentHandler = pMXWriter;
    const ISAXErrorHandlerPtr pSAXErrorHandler = pMXWriter;
    const ISAXDTDHandlerPtr pSAXDTDHandler = pMXWriter;
    const ISAXLexicalHandlerPtr pSAXLexicalHandler = pMXWriter;
    const ISAXDeclHandlerPtr pSAXDeclHandler = pMXWriter;
    if (!pSAXContentHandler || !pSAXErrorHandler || !pSAXDTDHandler || !pSAXLexicalHandler || !pSAXDeclHandler) {
        return(pSrc->xml);
    }

    ISAXXMLReaderPtr pSAXReader;
    if (FAILED(hr = pSAXReader.CreateInstance(__uuidof(SAXXMLReader60)))) {
        return(pSrc->xml);
    }

    SAXContentHandlerFilter * const pFilter = new SAXContentHandlerFilter(pSAXContentHandler);
    const ISAXContentHandlerPtr pContentHandlerProxy(pFilter);
    pFilter->SetLexicalHandler(pSAXLexicalHandler);
    pFilter->SetCharactersSkipThreshold(200);

    if    (FAILED(hr = pSAXReader->putContentHandler(pContentHandlerProxy))
            || FAILED(hr = pSAXReader->putDTDHandler(pSAXDTDHandler))
            || FAILED(hr = pSAXReader->putErrorHandler(pSAXErrorHandler))
            || FAILED(hr = pSAXReader->putProperty(reinterpret_cast<unsigned short *>(L"http://xml.org/sax/properties/lexical-handler"), _variant_t(pSAXLexicalHandler.GetInterfacePtr())))
            || FAILED(hr = pSAXReader->putProperty(reinterpret_cast<unsigned short *>(L"http://xml.org/sax/properties/declaration-handler"), _variant_t(pSAXDeclHandler.GetInterfacePtr())))) {
        return(pSrc->xml);
    }

    if    (FAILED(hr = pSAXReader->parse(_variant_t(pSrc.GetInterfacePtr())))) {
        return(pSrc->xml);
    }

    return(pMXWriter->output);
}

Кроме форматирования я делаю еще некоторую фильтрацию - выбрасываю слишком большие текстовые фрагменты, заменяя их комментариями.

One Mercedes selling story

Кризис, мать вашу!

SystemParametersInfo with SPI_GETNONCLIENTMETRICS may fail

Оказывается, на версиях всеми любимой OS Windows ниже 6 (XP/2003) вызов функции SystemParametersInfo проваливается, если в поле cbSize структуры NONCLIENTMETRICS указан размер больше, чем нужно. Посмотрим на структуру NONCLIENTMETRICS:

typedef struct tagNONCLIENTMETRICSW
{
    UINT    cbSize;
    int     iBorderWidth;
    int     iScrollWidth;
    int     iScrollHeight;
    int     iCaptionWidth;
    int     iCaptionHeight;
    LOGFONTW lfCaptionFont;
    int     iSmCaptionWidth;
    int     iSmCaptionHeight;
    LOGFONTW lfSmCaptionFont;
    int     iMenuWidth;
    int     iMenuHeight;
    LOGFONTW lfMenuFont;
    LOGFONTW lfStatusFont;
    LOGFONTW lfMessageFont;
#if(WINVER >= 0x0600)
    int     iPaddedBorderWidth;
#endif /* WINVER >= 0x0600 */
}   NONCLIENTMETRICSW, *PNONCLIENTMETRICSW, FAR* LPNONCLIENTMETRICSW;

Правильный код:

NONCLIENTMETRICS ncm = { sizeof(NONCLIENTMETRICS), 0 };
#if (WINVER >= 0x0600)
OSVERSIONINFO osvi = { sizeof(OSVERSIONINFO), 0 };
VERIFY(GetVersionEx(&osvi));
if (osvi.dwMajorVersion < 6) {
    ncm.cbSize -= sizeof(int);
}
#endif
if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &ncm, 0)) {
    _defaultLogFont = ncm.lfMessageFont;
}

How differential works

Copyright 2007-2011 Chabster