Skip to content

Instantly share code, notes, and snippets.

@Sigmanor
Created August 1, 2015 17:51
Show Gist options
  • Select an option

  • Save Sigmanor/3ed700a4f255d9ca33fd to your computer and use it in GitHub Desktop.

Select an option

Save Sigmanor/3ed700a4f255d9ca33fd to your computer and use it in GitHub Desktop.
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;
// Thanks for fixes:
// * Marco Minerva, jachymko - http://www.codeplex.com/windowsformsaero
// * Ben Ryves - http://www.benryves.com/
//
// ** Note for anyone considering using this: **
//
// A better alternative to using this class is to use the MainMenu and ContextMenu
// controls instead of MenuStrip and ContextMenuStrip, as they provide true native
// rendering. If you require icons, try this:
//
// http://wyday.com/blog/2009/making-the-menus-in-your-net-app-look-professional/
namespace Szotar.WindowsForms {
 public enum ToolbarTheme {
  Toolbar,
  MediaToolbar,
  CommunicationsToolbar,
  BrowserTabBar,
  HelpBar
 }
 /// <summary>Renders a toolstrip using the UxTheme API via VisualStyleRenderer and a specific style.</summary>
 /// <remarks>Perhaps surprisingly, this does not need to be disposable.</remarks>
 public class ToolStripAeroRenderer : ToolStripSystemRenderer {
  VisualStyleRenderer renderer;
  public ToolStripAeroRenderer(ToolbarTheme theme) {
   Theme = theme;
  }
  /// <summary>
  /// It shouldn't be necessary to P/Invoke like this, however VisualStyleRenderer.GetMargins
  /// misses out a parameter in its own P/Invoke.
  /// </summary>
  static internal class NativeMethods {
   [StructLayout(LayoutKind.Sequential)]
   public struct MARGINS {
    public int cxLeftWidth;
    public int cxRightWidth;
    public int cyTopHeight;
    public int cyBottomHeight;
   }
   [DllImport("uxtheme.dll")]
   public extern static int GetThemeMargins(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, int iPropId, IntPtr rect, out MARGINS pMargins);
  }
  // See http://msdn2.microsoft.com/en-us/library/bb773210.aspx - "Parts and States"
  // Only menu-related parts/states are needed here, VisualStyleRenderer handles most of the rest.
  enum MenuParts : int {
   ItemTMSchema = 1,
   DropDownTMSchema = 2,
   BarItemTMSchema = 3,
   BarDropDownTMSchema = 4,
   ChevronTMSchema = 5,
   SeparatorTMSchema = 6,
   BarBackground = 7,
   BarItem = 8,
   PopupBackground = 9,
   PopupBorders = 10,
   PopupCheck = 11,
   PopupCheckBackground = 12,
   PopupGutter = 13,
   PopupItem = 14,
   PopupSeparator = 15,
   PopupSubmenu = 16,
   SystemClose = 17,
   SystemMaximize = 18,
   SystemMinimize = 19,
   SystemRestore = 20
  }
  enum MenuBarStates : int {
   Active = 1,
   Inactive = 2
  }
  enum MenuBarItemStates : int {
   Normal = 1,
   Hover = 2,
   Pushed = 3,
   Disabled = 4,
   DisabledHover = 5,
   DisabledPushed = 6
  }
  enum MenuPopupItemStates : int {
   Normal = 1,
   Hover = 2,
   Disabled = 3,
   DisabledHover = 4
  }
  enum MenuPopupCheckStates : int {
   CheckmarkNormal = 1,
   CheckmarkDisabled = 2,
   BulletNormal = 3,
   BulletDisabled = 4
  }
  enum MenuPopupCheckBackgroundStates : int {
   Disabled = 1,
   Normal = 2,
   Bitmap = 3
  }
  enum MenuPopupSubMenuStates : int {
   Normal = 1,
   Disabled = 2
  }
  enum MarginTypes : int {
   Sizing = 3601,
   Content = 3602,
   Caption = 3603
  }
  static readonly int RebarBackground = 6;
  Padding GetThemeMargins(IDeviceContext dc, MarginTypes marginType) {
   NativeMethods.MARGINS margins;
   try {
    IntPtr hDC = dc.GetHdc();
    if (0 == NativeMethods.GetThemeMargins(renderer.Handle, hDC, renderer.Part, renderer.State, (int)marginType, IntPtr.Zero, out margins))
     return new Padding(margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, margins.cyBottomHeight);
    return new Padding(0);
   } finally {
    dc.ReleaseHdc();
   }
  }
  private static int GetItemState(ToolStripItem item) {
   bool hot = item.Selected;
   if (item.IsOnDropDown) {
    if (item.Enabled)
     return hot ? (int)MenuPopupItemStates.Hover : (int)MenuPopupItemStates.Normal;
    return hot ? (int)MenuPopupItemStates.DisabledHover : (int)MenuPopupItemStates.Disabled;
   } else {
    if (item.Pressed)
     return item.Enabled ? (int)MenuBarItemStates.Pushed : (int)MenuBarItemStates.DisabledPushed;
    if (item.Enabled)
     return hot ? (int)MenuBarItemStates.Hover : (int)MenuBarItemStates.Normal;
    return hot ? (int)MenuBarItemStates.DisabledHover : (int)MenuBarItemStates.Disabled;
   }
  }
  public ToolbarTheme Theme {
   get;
   set;
  }
  private string RebarClass {
   get {
    return SubclassPrefix + "Rebar";
   }
  }
  private string ToolbarClass {
   get {
    return SubclassPrefix + "ToolBar";
   }
  }
  private string MenuClass {
   get {
    return SubclassPrefix + "Menu";
   }
  }
  private string SubclassPrefix {
   get {
    switch (Theme) {
     case ToolbarTheme.MediaToolbar: return "Media::";
     case ToolbarTheme.CommunicationsToolbar: return "Communications::";
     case ToolbarTheme.BrowserTabBar: return "BrowserTabBar::";
     case ToolbarTheme.HelpBar: return "Help::";
     default: return string.Empty;
    }
   }
  }
  private VisualStyleElement Subclass(VisualStyleElement element) {
   return VisualStyleElement.CreateElement(SubclassPrefix + element.ClassName,
    element.Part, element.State);
  }
  private bool EnsureRenderer() {
   if (!IsSupported)
    return false;
   if (renderer == null)
    renderer = new VisualStyleRenderer(VisualStyleElement.Button.PushButton.Normal);
   return true;
  }
  // Gives parented ToolStrips a transparent background.
  protected override void Initialize(ToolStrip toolStrip) {
   if (toolStrip.Parent is ToolStripPanel)
    toolStrip.BackColor = Color.Transparent;
   base.Initialize(toolStrip);
  }
  // Using just ToolStripManager.Renderer without setting the Renderer individually per ToolStrip means
  // that the ToolStrip is not passed to the Initialize method. ToolStripPanels, however, are. So we can
  // simply initialize it here too, and this should guarantee that the ToolStrip is initialized at least
  // once. Hopefully it isn't any more complicated than this.
  protected override void InitializePanel(ToolStripPanel toolStripPanel) {
   foreach (Control control in toolStripPanel.Controls)
    if (control is ToolStrip)
     Initialize((ToolStrip)control);
   base.InitializePanel(toolStripPanel);
  }
  protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) {
   if (EnsureRenderer()) {
    renderer.SetParameters(MenuClass, (int)MenuParts.PopupBorders, 0);
    if (e.ToolStrip.IsDropDown) {
     Region oldClip = e.Graphics.Clip;
     // Tool strip borders are rendered *after* the content, for some reason.
     // So we have to exclude the inside of the popup otherwise we'll draw over it.
     Rectangle insideRect = e.ToolStrip.ClientRectangle;
     insideRect.Inflate(-1, -1);
     e.Graphics.ExcludeClip(insideRect);
     renderer.DrawBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.AffectedBounds);
     // Restore the old clip in case the Graphics is used again (does that ever happen?)
     e.Graphics.Clip = oldClip;
    }
   } else {
    base.OnRenderToolStripBorder(e);
   }
  }
  Rectangle GetBackgroundRectangle(ToolStripItem item) {
   if (!item.IsOnDropDown)
    return new Rectangle(new Point(), item.Bounds.Size);
   // For a drop-down menu item, the background rectangles of the items should be touching vertically.
   // This ensures that's the case.
   Rectangle rect = item.Bounds;
   // The background rectangle should be inset two pixels horizontally (on both sides), but we have
   // to take into account the border.
   rect.X = item.ContentRectangle.X + 1;
   rect.Width = item.ContentRectangle.Width - 1;
   // Make sure we're using all of the vertical space, so that the edges touch.
   rect.Y = 0;
   return rect;
  }
  protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) {
   if (EnsureRenderer()) {
    int partID = e.Item.IsOnDropDown ? (int)MenuParts.PopupItem : (int)MenuParts.BarItem;
    renderer.SetParameters(MenuClass, partID, GetItemState(e.Item));
    Rectangle bgRect = GetBackgroundRectangle(e.Item);
    renderer.DrawBackground(e.Graphics, bgRect, bgRect);
   } else {
    base.OnRenderMenuItemBackground(e);
   }
  }
  protected override void OnRenderToolStripPanelBackground(ToolStripPanelRenderEventArgs e) {
   if (EnsureRenderer()) {
    // Draw the background using Rebar & RP_BACKGROUND (or, if that is not available, fall back to
    // Rebar.Band.Normal)
    if (VisualStyleRenderer.IsElementDefined(VisualStyleElement.CreateElement(RebarClass, RebarBackground, 0))) {
     renderer.SetParameters(RebarClass, RebarBackground, 0);
    } else {
     renderer.SetParameters(RebarClass, 0, 0);
    }
    if (renderer.IsBackgroundPartiallyTransparent())
     renderer.DrawParentBackground(e.Graphics, e.ToolStripPanel.ClientRectangle, e.ToolStripPanel);
    renderer.DrawBackground(e.Graphics, e.ToolStripPanel.ClientRectangle);
    e.Handled = true;
   } else {
    base.OnRenderToolStripPanelBackground(e);
   }
  }
  // Render the background of an actual menu bar, dropdown menu or toolbar.
  protected override void OnRenderToolStripBackground(System.Windows.Forms.ToolStripRenderEventArgs e) {
   if (EnsureRenderer()) {
    if (e.ToolStrip.IsDropDown) {
     renderer.SetParameters(MenuClass, (int)MenuParts.PopupBackground, 0);
    } else {
     // It's a MenuStrip or a ToolStrip. If it's contained inside a larger panel, it should have a
     // transparent background, showing the panel's background.
     if (e.ToolStrip.Parent is ToolStripPanel) {
      // The background should be transparent, because the ToolStripPanel's background will be visible.
      // (Of course, we assume the ToolStripPanel is drawn using the same theme, but it's not my fault
      // if someone does that.)
      return;
     } else {
      // A lone toolbar/menubar should act like it's inside a toolbox, I guess.
      // Maybe I should use the MenuClass in the case of a MenuStrip, although that would break
      // the other themes...
      if (VisualStyleRenderer.IsElementDefined(VisualStyleElement.CreateElement(RebarClass, RebarBackground, 0)))
       renderer.SetParameters(RebarClass, RebarBackground, 0);
      else
       renderer.SetParameters(RebarClass, 0, 0);
     }
    }
    if (renderer.IsBackgroundPartiallyTransparent())
     renderer.DrawParentBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.ToolStrip);
    renderer.DrawBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.AffectedBounds);
   } else {
    base.OnRenderToolStripBackground(e);
   }
  }
  // The only purpose of this override is to change the arrow colour.
  // It's OK to just draw over the default arrow since we also pass down arrow drawing to the system renderer.
  protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventArgs e) {
   if (EnsureRenderer()) {
    ToolStripSplitButton sb = (ToolStripSplitButton)e.Item;
    base.OnRenderSplitButtonBackground(e);
    // It doesn't matter what colour of arrow we tell it to draw. OnRenderArrow will compute it from the item anyway.
    OnRenderArrow(new ToolStripArrowRenderEventArgs(e.Graphics, sb, sb.DropDownButtonBounds, Color.Red, ArrowDirection.Down));
   } else {
    base.OnRenderSplitButtonBackground(e);
   }
  }
  Color GetItemTextColor(ToolStripItem item) {
   int partId = item.IsOnDropDown ? (int)MenuParts.PopupItem : (int)MenuParts.BarItem;
   renderer.SetParameters(MenuClass, partId, GetItemState(item));
   return renderer.GetColor(ColorProperty.TextColor);
  }
  protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) {
   if (EnsureRenderer())
    e.TextColor = GetItemTextColor(e.Item);
   base.OnRenderItemText(e);
  }
  protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) {
   if (EnsureRenderer()) {
    if (e.ToolStrip.IsDropDown) {
     renderer.SetParameters(MenuClass, (int)MenuParts.PopupGutter, 0);
     // The AffectedBounds is usually too small, way too small to look right. Instead of using that,
     // use the AffectedBounds but with the right width. Then narrow the rectangle to the correct edge
     // based on whether or not it's RTL. (It doesn't need to be narrowed to an edge in LTR mode, but let's
     // do that anyway.)
     // Using the DisplayRectangle gets roughly the right size so that the separator is closer to the text.
     Padding margins = GetThemeMargins(e.Graphics, MarginTypes.Sizing);
     int extraWidth = (e.ToolStrip.Width - e.ToolStrip.DisplayRectangle.Width - margins.Left - margins.Right - 1) - e.AffectedBounds.Width;
     Rectangle rect = e.AffectedBounds;
     rect.Y += 2;
     rect.Height -= 4;
     int sepWidth = renderer.GetPartSize(e.Graphics, ThemeSizeType.True).Width;
     if (e.ToolStrip.RightToLeft == RightToLeft.Yes) {
      rect = new Rectangle(rect.X - extraWidth, rect.Y, sepWidth, rect.Height);
      rect.X += sepWidth;
     } else {
      rect = new Rectangle(rect.Width + extraWidth - sepWidth, rect.Y, sepWidth, rect.Height);
     }
     renderer.DrawBackground(e.Graphics, rect);
    }
   } else {
    base.OnRenderImageMargin(e);
   }
  }
  protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) {
   if (e.ToolStrip.IsDropDown && EnsureRenderer()) {
    renderer.SetParameters(MenuClass, (int)MenuParts.PopupSeparator, 0);
    Rectangle rect = new Rectangle(e.ToolStrip.DisplayRectangle.Left, 0, e.ToolStrip.DisplayRectangle.Width, e.Item.Height);
    renderer.DrawBackground(e.Graphics, rect, rect);
   } else {
    base.OnRenderSeparator(e);
   }
  }
  protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) {
   if (EnsureRenderer()) {
    Rectangle bgRect = GetBackgroundRectangle(e.Item);
    bgRect.Width = bgRect.Height;
    // Now, mirror its position if the menu item is RTL.
    if (e.Item.RightToLeft == RightToLeft.Yes)
     bgRect = new Rectangle(e.ToolStrip.ClientSize.Width - bgRect.X - bgRect.Width, bgRect.Y, bgRect.Width, bgRect.Height);
    renderer.SetParameters(MenuClass, (int)MenuParts.PopupCheckBackground, e.Item.Enabled ? (int)MenuPopupCheckBackgroundStates.Normal : (int)MenuPopupCheckBackgroundStates.Disabled);
    renderer.DrawBackground(e.Graphics, bgRect);
    Rectangle checkRect = e.ImageRectangle;
    checkRect.X = bgRect.X + bgRect.Width / 2 - checkRect.Width / 2;
    checkRect.Y = bgRect.Y + bgRect.Height / 2 - checkRect.Height / 2;
    // I don't think ToolStrip even supports radio box items, so no need to render them.
    renderer.SetParameters(MenuClass, (int)MenuParts.PopupCheck, e.Item.Enabled ? (int)MenuPopupCheckStates.CheckmarkNormal : (int)MenuPopupCheckStates.CheckmarkDisabled);
    renderer.DrawBackground(e.Graphics, checkRect);
   } else {
    base.OnRenderItemCheck(e);
   }
  }
  protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) {
   // The default renderer will draw an arrow for us (the UXTheme API seems not to have one for all directions),
   // but it will get the colour wrong in many cases. The text colour is probably the best colour to use.
   if (EnsureRenderer())
    e.ArrowColor = GetItemTextColor(e.Item);
   base.OnRenderArrow(e);
  }
  protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEventArgs e) {
   if (EnsureRenderer()) {
    // BrowserTabBar::Rebar draws the chevron using the default background. Odd.
    string rebarClass = RebarClass;
    if (Theme == ToolbarTheme.BrowserTabBar)
     rebarClass = "Rebar";
    int state = VisualStyleElement.Rebar.Chevron.Normal.State;
    if (e.Item.Pressed)
     state = VisualStyleElement.Rebar.Chevron.Pressed.State;
    else if (e.Item.Selected)
     state = VisualStyleElement.Rebar.Chevron.Hot.State;
    renderer.SetParameters(rebarClass, VisualStyleElement.Rebar.Chevron.Normal.Part, state);
    renderer.DrawBackground(e.Graphics, new Rectangle(Point.Empty, e.Item.Size));
   } else {
    base.OnRenderOverflowButtonBackground(e);
   }
  }
  public bool IsSupported {
   get {
    if (!VisualStyleRenderer.IsSupported)
     return false;
    // Needs a more robust check. It seems mono supports very different style sets.
    return
     VisualStyleRenderer.IsElementDefined(
      VisualStyleElement.CreateElement("Menu",
       (int)MenuParts.BarBackground,
       (int)MenuBarStates.Active));
   }
  }
 }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment