source: trunk/eraser/Eraser.Util/Theming.cs @ 2242

Revision 2242, 14.9 KB checked in by lowjoel, 4 years ago (diff)

Use the VisualStylesRenderer? class to draw the theme parts instead of P/Invoking everything directly.

  • As part of the changes, the algorithms used to compute the sizes of theme components e.g. gutter width, margin with, check mark rectangle, check mark background rectangle have been rewritten. These new algorithms work across DPI settings. Addresses #303: Verify Eraser's UI in high DPI mode
  • Remove unnecessary UxTheme?.cs P/Invoke declarations.
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1/*
2 * $Id$
3 * Copyright 2008-2010 The Eraser Project
4 * Original Author: Joel Low <lowjoel@users.sourceforge.net>
5 * Modified By:
6 *
7 * This file is part of Eraser.
8 *
9 * Eraser is free software: you can redistribute it and/or modify it under the
10 * terms of the GNU General Public License as published by the Free Software
11 * Foundation, either version 3 of the License, or (at your option) any later
12 * version.
13 *
14 * Eraser is distributed in the hope that it will be useful, but WITHOUT ANY
15 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 *
18 * A copy of the GNU General Public License can be found at
19 * <http://www.gnu.org/licenses/>.
20 */
21
22using System;
23using System.Collections.Generic;
24using System.Text;
25using System.Runtime.InteropServices;
26using System.Windows.Forms;
27using System.Drawing;
28using System.IO;
29using System.Windows.Forms.VisualStyles;
30using Microsoft.Win32.SafeHandles;
31
32namespace Eraser.Util
33{
34    public static class Theming
35    {
36        /// <summary>
37        /// Verifies whether themeing is active.
38        /// </summary>
39        public static bool Active
40        {
41            get
42            {
43                try
44                {
45                    return NativeMethods.IsThemeActive();
46                }
47                catch (FileLoadException)
48                {
49                    return false;
50                }
51            }
52        }
53
54        /// <summary>
55        /// Updates the control's theme to fit in with the latest Windows visuals.
56        /// </summary>
57        /// <remarks>This function will also set the volume on all child controls.</remarks>
58        public static void ApplyTheme(Control control)
59        {
60            ContainerControl container = control as ContainerControl;
61            ButtonBase button = control as ButtonBase;
62            ListView listview = control as ListView;
63            ToolStrip toolstrip = control as ToolStrip;
64
65            if (container != null)
66                container.Font = SystemFonts.MessageBoxFont;
67            else if (control.Font != SystemFonts.MessageBoxFont)
68                control.Font = new Font(SystemFonts.MessageBoxFont.FontFamily,
69                    control.Font.Size, control.Font.Style);
70
71            if (button != null)
72                ApplyTheme(button);
73            else if (listview != null)
74                ApplyTheme(listview);
75            else if (toolstrip != null)
76                ApplyTheme(toolstrip);
77
78            if (control.ContextMenuStrip != null)
79                ApplyTheme(control.ContextMenuStrip);
80           
81            foreach (Control child in control.Controls)
82                ApplyTheme(child);
83        }
84
85        /// <summary>
86        /// Updates the control's theme to fit in with the latest Windows visuals.
87        /// </summary>
88        /// <param name="button">The ButtonBase control to set the theme on.</param>
89        public static void ApplyTheme(ButtonBase button)
90        {
91            if (button.FlatStyle == FlatStyle.Standard)
92                button.FlatStyle = FlatStyle.System;
93        }
94
95        /// <summary>
96        /// Updates the control's theme to fit in with the latest Windows visuals.
97        /// </summary>
98        /// <param name="lv">The List View control to set the theme on.</param>
99        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
100        public static void ApplyTheme(ListView lv)
101        {
102            try
103            {
104                NativeMethods.SetWindowTheme(lv.Handle, "EXPLORER", null);
105                NativeMethods.SendMessage(lv.Handle, NativeMethods.LVM_SETEXTENDEDLISTVIEWSTYLE,
106                    (UIntPtr)NativeMethods.LVS_EX_DOUBLEBUFFER,
107                    (IntPtr)NativeMethods.LVS_EX_DOUBLEBUFFER);
108            }
109            catch (DllNotFoundException)
110            {
111            }
112        }
113
114        /// <summary>
115        /// Updates the control's theme to fit in with the latest Windows visuals.
116        /// </summary>
117        /// <param name="menu">The tool strip control to set the theme on.</param>
118        public static void ApplyTheme(ToolStrip menu)
119        {
120            //Register for Theme changed messages
121            if (ThemeMessageFilter.Instance == null)
122            {
123                ThemeMessageFilter filter = new ThemeMessageFilter();
124                filter.ThemeChanged += OnThemeChanged;
125            }
126
127            if (Environment.OSVersion.Version.Major >= 6)
128            {
129                //Assign our themed renderer for non-custom renderers
130                UXThemeMenuRenderer renderer = new UXThemeMenuRenderer();
131                if (menu.Renderer is ToolStripProfessionalRenderer)
132                {
133                    menu.Disposed += OnThemedMenuDisposed;
134                    ThemedMenus.Add(menu, renderer);
135                    if (Active)
136                        menu.Renderer = renderer;
137                }
138            }
139
140            foreach (ToolStripItem item in menu.Items)
141            {
142                ToolStripMenuItem toolStripItem = item as ToolStripMenuItem;
143                if (toolStripItem != null)
144                    ApplyTheme(toolStripItem);
145            }
146        }
147
148        /// <summary>
149        /// Updates the control's theme to fit in with the latest Windows visuals.
150        /// </summary>
151        /// <param name="menu">The List View control to set the theme on.</param>
152        public static void ApplyTheme(ToolStripDropDownItem menuItem)
153        {
154            if (menuItem.Font != SystemFonts.MenuFont)
155                menuItem.Font = new Font(SystemFonts.MenuFont, menuItem.Font.Style);
156
157            ApplyTheme(menuItem.DropDown);
158        }
159
160        /// <summary>
161        /// Handles the theme changed event - reassigning the renderers to managed
162        /// context menus.
163        /// </summary>
164        private static void OnThemeChanged(object sender, EventArgs e)
165        {
166            bool themesActive = Active;
167            foreach (KeyValuePair<ToolStrip, UXThemeMenuRenderer> value in ThemedMenus)
168            {
169                if (themesActive)
170                    value.Key.Renderer = value.Value;
171                else
172                    value.Key.RenderMode = ToolStripRenderMode.ManagerRenderMode;
173            }
174        }
175
176        /// <summary>
177        /// Clean up the reference to the menu when the menu is disposed so we no
178        /// longer track the menu for theme changes.
179        /// </summary>
180        private static void OnThemedMenuDisposed(object sender, EventArgs e)
181        {
182            ThemedMenus.Remove(sender as ToolStrip);
183        }
184
185        /// <summary>
186        /// The private list of menus which has their render changed to the UxTheme renderer.
187        /// This allows us to revert the renderer back to the default Professional
188        /// renderer when we get a theme changed message.
189        /// </summary>
190        private static Dictionary<ToolStrip, UXThemeMenuRenderer> ThemedMenus =
191            new Dictionary<ToolStrip, UXThemeMenuRenderer>();
192
193        /// <summary>
194        /// Filters the Application message loop for WM_THEMECHANGED messages
195        /// and broadcasts them to the event handlers.
196        /// </summary>
197        private class ThemeMessageFilter : IMessageFilter
198        {
199            public ThemeMessageFilter()
200            {
201                if (Instance != null)
202                    throw new InvalidOperationException("Only one instance of the " +
203                        "theme-change message filter can exist at any one time.");
204
205                Instance = this;
206                ThemesActive = Theming.Active;
207                Application.AddMessageFilter(this);
208            }
209
210            #region IMessageFilter Members
211            public bool PreFilterMessage(ref Message m)
212            {
213                if (m.Msg == NativeMethods.WM_THEMECHANGED)
214                {
215                    ThemesActive = Theming.Active;
216                    ThemeChanged(null, EventArgs.Empty);
217                }
218                else if (m.Msg == NativeMethods.WM_DWMCOMPOSITIONCHANGED)
219                {
220                    if (ThemesActive != Theming.Active)
221                    {
222                        ThemesActive = Theming.Active;
223                        ThemeChanged(null, EventArgs.Empty);
224                    }
225                }
226
227                return false;           
228            }
229            #endregion
230
231            /// <summary>
232            /// The global ThemeMessageFilter instance.
233            /// </summary>
234            public static ThemeMessageFilter Instance
235            {
236                get;
237                private set;
238            }
239
240            /// <summary>
241            /// Called when a WM_THEMECHANGED message is sent.
242            /// </summary>
243            public EventHandler<EventArgs> ThemeChanged
244            {
245                get;
246                set;
247            }
248
249            private bool ThemesActive;
250        }
251    }
252
253    public class UXThemeMenuRenderer : ToolStripRenderer
254    {
255        protected override void Initialize(ToolStrip toolStrip)
256        {
257            base.Initialize(toolStrip);
258            ToolStrip = toolStrip;
259            Renderer = new VisualStyleRenderer(VisualStyleElement.Button.PushButton.Default);
260
261            //Hook the item added event to inflate the height of every item by 2px.
262            ToolStrip.ItemAdded += new ToolStripItemEventHandler(OnToolStripItemAdded);
263            foreach (ToolStripItem item in toolStrip.Items)
264                item.Height += 2;
265        }
266
267        void OnToolStripItemAdded(object sender, ToolStripItemEventArgs e)
268        {
269            //Inflate the height of every item by 2px.
270            e.Item.Height += 2;
271        }
272
273        protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e)
274        {
275            Rectangle rect = e.AffectedBounds;
276           
277            Renderer.SetParameters(MenuPopupBackground);
278            if (Renderer.IsBackgroundPartiallyTransparent())
279                Renderer.DrawParentBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.ToolStrip);
280           
281            Renderer.DrawBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.AffectedBounds);
282        }
283
284        protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e)
285        {
286            //Strange, borders are drawn after the content. So, clip to only the borders
287            //so that the internals will be retained.
288            Region oldClip = e.Graphics.Clip;
289            Rectangle insideRect = e.ToolStrip.ClientRectangle;
290
291            //The correct (Windows) size is actually 3px, but that will cut into our items.
292            insideRect.Inflate(-2, -2);
293            e.Graphics.ExcludeClip(insideRect);
294
295            Renderer.SetParameters(MenuPopupBorders);
296            Renderer.DrawBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.AffectedBounds);
297
298            //Restore the old clipping.
299            e.Graphics.IntersectClip(insideRect);
300        }
301
302        protected override void OnRenderImageMargin(ToolStripRenderEventArgs e)
303        {
304            //Compute the rectangle to draw the gutter.
305            Rectangle rect = e.AffectedBounds;
306            rect.X = 0;
307            Size gutterImageSize = Renderer.GetPartSize(e.Graphics, ThemeSizeType.True);
308            rect.Width = GutterWidth - gutterImageSize.Width + 1;
309
310            Renderer.SetParameters(MenuPopupGutter);
311            Renderer.DrawBackground(e.Graphics, rect);
312        }
313
314        protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e)
315        {
316            //Compute the rectangle of the background.
317            Rectangle rect = e.Item.ContentRectangle;
318            rect.Inflate(0, 1);
319
320            Renderer.SetParameters(GetItemElement(e.Item));
321            Renderer.DrawBackground(e.Graphics, rect, rect);
322        }
323
324        protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e)
325        {
326            //Get the size of the gutter image
327            Renderer.SetParameters(MenuPopupGutter);
328            Size gutterImageSize = Renderer.GetPartSize(e.Graphics, ThemeSizeType.True);
329
330            Renderer.SetParameters(MenuSeparator);
331            Renderer.DrawBackground(e.Graphics, new Rectangle(
332                GutterWidth - gutterImageSize.Width, 0, e.ToolStrip.DisplayRectangle.Width,
333                e.Item.Height));
334        }
335
336        protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e)
337        {
338            if (!(e.Item is ToolStripMenuItem))
339            {
340                base.OnRenderItemCheck(e);
341                return;
342            }
343
344            //Get the menu item.
345            ToolStripMenuItem item = (ToolStripMenuItem)e.Item;
346
347            //Compute the rectangle for the background.
348            Rectangle rect = e.Item.ContentRectangle;
349            rect.Y = 0;
350            rect.Size = new Size(item.Height, item.Height);
351
352            //Draw the background
353            Renderer.SetParameters(GetCheckBackgroundElement(item));
354            Renderer.DrawBackground(e.Graphics, rect);
355
356            //Compute the size of the checkmark
357            rect.Inflate(-3, -3);
358           
359            //Draw the checkmark
360            Renderer.SetParameters(GetCheckElement(item));
361            Renderer.DrawBackground(e.Graphics, rect);
362        }
363
364        protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
365        {
366            Renderer.SetParameters(GetItemElement(e.Item));
367            if (e.Item.Owner.IsDropDown || e.Item.Owner is MenuStrip)
368                e.TextColor = Renderer.GetColor(ColorProperty.TextColor);
369
370            base.OnRenderItemText(e);
371        }
372
373        protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e)
374        {
375            Renderer.SetParameters(GetSubmenuElement(e.Item));
376            Renderer.DrawBackground(e.Graphics, e.ArrowRectangle);
377        }
378
379        private VisualStyleElement GetItemElement(ToolStripItem item)
380        {
381            return item.Selected ?
382                (item.Enabled ?
383                    MenuPopupItemHot :
384                    MenuPopupItemDisabledHot) :
385                (item.Enabled ?
386                    MenuPopupItem :
387                    MenuPopupItemDisabled);
388        }
389
390        private VisualStyleElement GetCheckBackgroundElement(ToolStripItem item)
391        {
392            return item.Enabled ? MenuPopupCheckBackground : MenuPopupCheckBackgroundDisabled;
393        }
394
395        private VisualStyleElement GetCheckElement(ToolStripMenuItem item)
396        {
397            return item.Checked ?
398                (item.Enabled ? MenuPopupCheck : MenuPopupCheckDisabled) :
399                MenuPopupBitmap;
400        }
401
402        private VisualStyleElement GetSubmenuElement(ToolStripItem item)
403        {
404            return item.Enabled ? MenuSubmenu : MenuSubmenuDisabled;
405        }
406
407        /// <summary>
408        /// Gets the width of the gutter for images.
409        /// </summary>
410        private int GutterWidth
411        {
412            get
413            {
414                return ToolStrip.DisplayRectangle.Left;
415            }
416        }
417
418        private ToolStrip ToolStrip;
419        private VisualStyleRenderer Renderer;
420
421        private static readonly string MenuClass = "MENU";
422
423        private static VisualStyleElement MenuPopupBackground =
424            VisualStyleElement.CreateElement(
425                MenuClass, (int)NativeMethods.MENUPARTS.MENU_POPUPBACKGROUND, 0);
426        private static VisualStyleElement MenuPopupBorders =
427            VisualStyleElement.CreateElement(
428                MenuClass, (int)NativeMethods.MENUPARTS.MENU_POPUPBORDERS, 0);
429
430        private static VisualStyleElement MenuPopupItem =
431            VisualStyleElement.CreateElement(MenuClass,
432                (int)NativeMethods.MENUPARTS.MENU_POPUPITEM,
433                (int)NativeMethods.POPUPITEMSTATES.MPI_NORMAL);
434        private static VisualStyleElement MenuPopupItemHot =
435            VisualStyleElement.CreateElement(MenuClass,
436                (int)NativeMethods.MENUPARTS.MENU_POPUPITEM,
437                (int)NativeMethods.POPUPITEMSTATES.MPI_HOT);
438        private static VisualStyleElement MenuPopupItemDisabled =
439            VisualStyleElement.CreateElement(MenuClass,
440                (int)NativeMethods.MENUPARTS.MENU_POPUPITEM,
441                (int)NativeMethods.POPUPITEMSTATES.MPI_DISABLED);
442        private static VisualStyleElement MenuPopupItemDisabledHot =
443            VisualStyleElement.CreateElement(MenuClass,
444                (int)NativeMethods.MENUPARTS.MENU_POPUPITEM,
445                (int)NativeMethods.POPUPITEMSTATES.MPI_DISABLEDHOT);
446
447        private VisualStyleElement MenuPopupCheckBackground =
448            VisualStyleElement.CreateElement(MenuClass,
449            (int)NativeMethods.MENUPARTS.MENU_POPUPCHECKBACKGROUND,
450            (int)NativeMethods.POPUPCHECKBACKGROUNDSTATES.MCB_NORMAL);
451        private VisualStyleElement MenuPopupCheckBackgroundDisabled =
452            VisualStyleElement.CreateElement(MenuClass,
453            (int)NativeMethods.MENUPARTS.MENU_POPUPCHECKBACKGROUND,
454            (int)NativeMethods.POPUPCHECKBACKGROUNDSTATES.MCB_NORMAL);
455
456        private VisualStyleElement MenuPopupBitmap =
457            VisualStyleElement.CreateElement(MenuClass,
458            (int)NativeMethods.MENUPARTS.MENU_POPUPCHECK, 0);
459        private VisualStyleElement MenuPopupCheck =
460            VisualStyleElement.CreateElement(MenuClass,
461            (int)NativeMethods.MENUPARTS.MENU_POPUPCHECK,
462            (int)NativeMethods.POPUPCHECKSTATES.MC_CHECKMARKNORMAL);
463        private VisualStyleElement MenuPopupCheckDisabled =
464            VisualStyleElement.CreateElement(MenuClass,
465            (int)NativeMethods.MENUPARTS.MENU_POPUPCHECK,
466            (int)NativeMethods.POPUPCHECKSTATES.MC_CHECKMARKDISABLED);
467       
468
469        private static VisualStyleElement MenuPopupGutter =
470            VisualStyleElement.CreateElement(MenuClass,
471                (int)NativeMethods.MENUPARTS.MENU_POPUPGUTTER, 0);
472
473        private VisualStyleElement MenuSeparator =
474            VisualStyleElement.CreateElement(MenuClass,
475                (int)NativeMethods.MENUPARTS.MENU_POPUPSEPARATOR, 0);
476
477        private VisualStyleElement MenuSubmenu =
478            VisualStyleElement.CreateElement(MenuClass,
479            (int)NativeMethods.MENUPARTS.MENU_POPUPSUBMENU,
480            (int)NativeMethods.POPUPSUBMENUSTATES.MSM_NORMAL);
481        private VisualStyleElement MenuSubmenuDisabled =
482            VisualStyleElement.CreateElement(MenuClass,
483            (int)NativeMethods.MENUPARTS.MENU_POPUPSUBMENU,
484            (int)NativeMethods.POPUPSUBMENUSTATES.MSM_DISABLED);
485    }
486}
Note: See TracBrowser for help on using the repository browser.