using System; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; using System.Windows.Forms.VisualStyles; namespace OpenNest { public enum ToolbarTheme { Toolbar, MediaToolbar, CommunicationsToolbar, BrowserTabBar, HelpBar } /// Renders a toolstrip using the UxTheme API via VisualStyleRenderer and a specific style. /// Perhaps surprisingly, this does not need to be disposable. public class ToolStripRenderer : ToolStripSystemRenderer { VisualStyleRenderer renderer; public ToolStripRenderer(ToolbarTheme theme) { Theme = theme; } /// /// It shouldn't be necessary to P/Invoke like this, however VisualStyleRenderer.GetMargins /// misses out a parameter in its own P/Invoke. /// 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 { e.Graphics.DrawLine(Pens.LightGray, e.Item.ContentRectangle.X, e.Item.ContentRectangle.Y, e.Item.ContentRectangle.X, e.Item.ContentRectangle.Y + e.Item.Height - 6); } } 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)); } } } }