Implementing your own colour themes in a C# Windows Forms application

I now present one way of implementing your own colour themes in an application. (With apologies to Americans… I spell colour in English, not American, except for properties in source code, where I consider color to be correct by convention.) I don’t know if there is a standard way of doing colour themes, and I didn’t bother to search online to see if there is one. I had a very clear idea in my head as to how I’d like colour themes implemented, so I just went ahead and did that.

Again, the source, for this and a whole bunch of other stuff, is here: RomyView.zip

Today I’ll do this backwards. I’ll show some screenshots of the end result, and then explain how I got there.  The application has three colour themes:

  1. Light
  2. Dark
  3. Monochrome

Actually, monochrome is not a serious theme. I added it to make fun of the Visual Studio 2012 UI. My monochrome theme is designed to intentionally blur the colours together by virtue of their poor contrast. It also sports an ALL CAPS menu.

Here is the main form, the Browser (as in file browser) in light, dark and monochrome theme. (You can click each one to view the full-size image.) Notice the tool strip button that resembles a monitor? Clicking it opens a drop-down menu where you can select a different theme. Double-clicking it toggles between the themes.

BrowserFormLightTheme BrowserFormDarkTheme BrowserFormMonoTheme

Here is the video converter dialog, as it appears for the three different themes:

ConvertDialogLight

ConvertDialogDark

ConvertDialogMono

And here is an example of what most of my dialogs look like. In this case, it is a conversion progress dialog. All my progress dialogs are modeless and can be minimized, and you can do as many concurrent video conversions as you want. If you toggle the theme, the new colours are applied to all of them immediately.

ProgressDialogLight ProgressDialogDark

ProgressDialogMono

And finally, here’s a close-up of my thumbnail  control. In each case, we see a selected thumb, a normal (unselected) thumb, and a hot-tracked thumb.

ThumbsLight

 ThumbsDark

ThumbsMono

Note that although the light theme thumbnail is designed to look similar to a Windows thumbnail, it isn’t one. It’s my own control derived from ContainerControl. Thus the thumb control contains a fair amount of code to do its painting, using different gradients depending on the theme and its current state (selected; hot-tracked; grayed). It also measures the text and decides on how much space to allow for it, and it, and it fetches its own image, either from my custom cache, or forces the cache to get the image. It’s by far the most complex control in the solution, and there could be many of them on the container at once. I had to take this into account in my theme implementation, but more on that in due course.

The themes implemerntation

The idea is that for a predefined set of types, each has one or more colour properties that change when the theme changes. The simplest way to achieve this is to define an interface for each type that can be themed. Here are my theme interfaces:

  1. /// <summary>A control that changes colours according to the active theme, implementing one or more of the
  2. /// other interfaces defined below.</summary>
  3. /// <remarks><para>
  4. /// The <b>ThemeChanged</b> event is implemented by the base form class. Derived forms need not implement this
  5. /// event. Instead, they override <see cref=”BaseForm.LoadTheme()”/> in response to the theme being changed.</para>
  6. /// <para>
  7. /// Besides the properties defined here, forms also change their ToolStrip (and related) background images and their
  8. /// ToolStrip item images. See the <seealso cref=”BaseForm.OnThemeChanged(ThemeChangedEventArgs)”/> handler for details.</para>
  9. /// <para>
  10. /// The <b>BaseForm</b> implements the themes by association of instances of three types, each of which implements all of the theme
  11. /// interfaces: <see cref=”DarkThemeColors”/>, <see cref=”LightThemeColors”/> and <see cref=”MonoThemeColors”/>/</para></remarks>
  12. public interface IThemeableControl
  13. {
  14.     /// <summary>The themes are <see cref=”System.Drawing.Color”/> properties
  15.     /// defined by several interfaces for different types.</summary>
  16.     Theme Theme { get; }
  17.  
  18.     /// <summary>Indicates that the theme has changed.</summary>
  19.     event EventHandler<ThemeChangedEventArgs> ThemeChanged;
  20. }
  21.  
  22. /// <summary>Themeable properties for forms and dialogs.</summary>
  23. public interface IFormThemeColors
  24. {
  25.     /// <summary>Background colour for dialogs.</summary>
  26.     Color DialogBackColor { get; set; }
  27.  
  28.     /// <summary>Background colour for forms.</summary>
  29.     Color FormBackColor { get; set; }
  30.  
  31.     /// <summary>Background colour for Panels and any controls where a single custom colour sufficiently themes the control.</summary>
  32.     /// <remarks>For any control that needs more complex theming, a control-specific interface is added.</remarks>
  33.     Color ThemeableControlBackColor { get; set; }
  34. }
  35.  
  36. /// <summary>Themeable properties for the image viewer.</summary>
  37. public interface IImageViewerThemeColors
  38. {
  39.     /// <summary>Background colour of the image viewer.</summary>
  40.     Color ImageBackColor { get; set; }
  41. }
  42.  
  43. /// <summary>Themeable properties for property grids.</summary>
  44. public interface IPropertyGridThemeColors
  45. {
  46.     Color PropertyGridBackColor { get; set; }
  47.  
  48.     Color PropertyGridCategorySplitterColor { get; set; }
  49.  
  50.     Color PropertyGridHelpBackColor { get; set; }
  51.  
  52.     Color PropertyGridHelpBorderColor { get; set; }
  53.  
  54.     Color PropertyGridLineColor { get; set; }
  55.  
  56.     Color PropertyGridSelectedItemWithFocusBackColor { get; set; }
  57.  
  58.     Color PropertyGridViewBackColor { get; set; }
  59.  
  60.     Color PropertyGridViewBorderColor { get; set; }
  61. }
  62.  
  63. /// <summary>Themeable properties for the FolderTreeView. (This TreeView is <b>not owner-drawn</b>.
  64. /// It uses styles introduced in Windows Vista, hence we don’t really customize it much.) We only
  65. /// change its text colour (according to its background, since it is drawn transparently).</summary>
  66. public interface IFolderTreeViewThemeColors
  67. {
  68.     Color TreeTextColor { get; set; }
  69. }
  70.  
  71. /// <summary>Themeable properties for the Shell ListView. We only need set its text colour, depending
  72. /// on its background, like the TreeView.</summary>
  73. public interface IShellListViewThemeColors
  74. {
  75.     Color ShellListViewTextColor { get; set; }
  76. }
  77.  
  78. /// <summary>Themeable properties for thumbnails.</summary>
  79. /// <remarks><para>
  80. /// Thumbnails are instances of my own control where <b>everything</b> is drawn in custom code, where
  81. /// the normal (Light) theme is designed to closely resemble thumbnails as drawn by Windows explorer.
  82. /// </para><para>
  83. /// Since the thumbnails use gradients for selection and hot-tracking, different colours are required
  84. /// for various properties, to match the surrounding ambient colours when changing the theme.</para>
  85. /// <para>
  86. /// Since there are several colours that change on Thumbnails, and the Thumbnail code is already complex
  87. /// enough without adding the theme code, the theme-related properties are implemented as a static instance
  88. /// of the <see cref=”ThumbnailColorTable”/> helper type. This interface is implemented by another helper,
  89. /// the <see cref=”ThumbnailThemer”/>.</para></remarks>
  90. public interface IThumbnailThemeColors
  91. {
  92.     Color ThumbnailHottrackBorderColor { get; set; }
  93.  
  94.     Color ThumbnailHottrackGradientBottomColor { get; set; }
  95.  
  96.     Color ThumbnailHottrackGradientTopColor { get; set; }
  97.  
  98.     Color ThumbnailPlaceHolderGradientBottomColor { get; set; }
  99.  
  100.     Color ThumbnailPlaceHolderGradientTopColor { get; set; }
  101.  
  102.     Color ThumbnailSelectedBorderColor { get; set; }
  103.  
  104.     Color ThumbnailSelectedGradientBottomColor { get; set; }
  105.  
  106.     Color ThumbnailSelectedGradientTopColor { get; set; }
  107.  
  108.     Color ThumbnailTextColor { get; set; }
  109. }

 

I then created three types, each of which implements all of these interfaces, called LightThemeColors, DarkThemeColors and MonoThemeColors.

All forms in the application derive from one ancestor, which is Romy.Controls.BaseForm. This base form then contains private fields of the three above types (declared as static and readonly because they do not change). The base form itself implements the IThemeableControl interface, thus it raises the ThemeChanged event whenever the theme is changed. Since it is the common base for all forms in the application, none of the other forms need to implement any of the themes interfaces. Instead, it calls a virtual method, called LoadTheme, from the event handler.

The work done to change the colours by the base form class, is done here. It applies new theme colours for the form itself, as well as any thumbnails, then calls a private method, FindAndRecolorControls, that searches for all controls of the types defined by the theme interfaces, and applies the new colours there too. Thus derived forms inherit this theme implementation, and don’t need to do anything at all if the default theme implementation works for them.

  1. public virtual void LoadTheme()
  2. {
  3.     if (!DesignMode)
  4.     {
  5.         this.BackColor = FormThemeColors.FormBackColor;
  6.         this.ThemeableControlBackColor = FormThemeColors.ThemeableControlBackColor;
  7.  
  8.         ThumbnailThemer.SetColors(ThumbnailColors);
  9.  
  10.         // Check for any controls that must be assigned new colours.
  11.         FindAndRecolorControls(this);
  12.     }
  13. }
  14.  
  15. /// <summary>Search recursively for themeable controls and apply the theme colours to any found.</summary>
  16. protected virtual void FindAndRecolorControls(Control control)
  17. {
  18.     foreach (var c in control.Controls.ToArray<Control>().Where(cc =>
  19.         (cc is Panel ||                                       // Anything that derives from Panel.
  20.         (cc is Thumbnail && ((Thumbnail)cc).IsCustomSize) ||  // A “custom size” thumbnail is painted with a border, like a panel.
  21.         cc is PropertyGrid ||                                 // A PropertyGrid.
  22.         cc is FolderTreeView ||                               // Folder TreeView.
  23.         cc is ShellListView ||                                // Shell ListView.
  24.         cc.HasChildren)))                                     // Recurse into containers.
  25.     {
  26.         var panel = c as Panel;
  27.  
  28.         if ((panel != null && !(c is SplitterPanel) && !(panel is BlackPanel) &&
  29.             !(panel is ToolStripContentPanel) && !(panel is FlowLayoutPanel))
  30.             || c is Thumbnail)
  31.         {
  32.             if (c.BackgroundImage == null)
  33.                 c.BackColor = ThemeableControlBackColor;
  34.         }
  35.         else
  36.         {
  37.             var p = c as PropertyGrid;
  38.  
  39.             if (p != null)
  40.             {
  41.                 p.BackColor = this.PropertyGridThemeColors.PropertyGridBackColor;
  42.                 p.CategorySplitterColor = this.PropertyGridThemeColors.PropertyGridCategorySplitterColor;
  43.                 p.HelpBackColor = this.PropertyGridThemeColors.PropertyGridHelpBackColor;
  44.                 p.LineColor = this.PropertyGridThemeColors.PropertyGridLineColor;
  45.                 p.SelectedItemWithFocusBackColor = this.PropertyGridThemeColors.PropertyGridSelectedItemWithFocusBackColor;
  46.                 p.ViewBackColor = this.PropertyGridThemeColors.PropertyGridViewBackColor;
  47.                 p.ViewBorderColor = this.PropertyGridThemeColors.PropertyGridViewBorderColor;
  48.                 p.HelpBorderColor = this.PropertyGridThemeColors.PropertyGridHelpBorderColor;
  49.             }
  50.             else
  51.             {
  52.                 var tree = c as FolderTreeView;
  53.  
  54.                 if (tree != null)
  55.                 {
  56.                     tree.ForeColor = FolderTreeViewThemeColors.TreeTextColor;
  57.                 }
  58.                 else
  59.                 {
  60.                     var l = c as ShellListView;
  61.  
  62.                     if (l != null)
  63.                     {
  64.                         l.ForeColor = ShellListViewThemeColors.ShellListViewTextColor;
  65.                     }
  66.                 }
  67.             }
  68.         }
  69.  
  70.         if (c.HasChildren)
  71.             FindAndRecolorControls(c);
  72.     }
  73. }

Thanks to inheritance, all that any other form in the application needs to do to be able to respond to the theme changing (if it needs to customize the way it handles themes), is override the LoadTheme method. The base form also exposes a dynamic typed property, called ThemeColors, which simply maps to one of the LightThemeColors, DarkThemeColors or MonoThemeColors fields, whichever is appropriate for the theme. This works out to be very convenient and easy to use from any of the derived forms. To get the right colours for any of the types for which theme colours are defined, since the ThemeColors property maps to a type that implements all of the theme interfaces, the code that needs to know the colours when the theme changes can simply type-cast it to whatever interface they need. (In fact, in the code above, in LoadTheme, when ThumbnailColors is referenced, ThumbnailColors is just a property that has already type-cast ThemeColors to IThumbnailThemeColors.)

I mentioned that I needed to do something different for the thumbnails… Rather than having to manipulate properties on potentially thousands of thumbnails at runtime, I created a helper class, called the ThumbnailThemer. This type exists as a singleton. The actual colour properties are implemented as properties of a static type owned by the thumbnail type. When the theme changes, the ThumbnailThemer sets them just once; then every thumbnail instance picks up the new colours.

And that is the gist of my themes design. There is a little more to it. I was already using a custom-derived ToolStripProfessionalRenderer, for a number of reasons (mostly that I detest the way ToolStrips, StatusStrips and so on are rendered by default), so when I wrote the mono theme, I hacked some code into it, to make the menus ALL CAPS, and to make menus as hideous as possible when this theme is active.

As an example of derived forms customizing this behavior when loading a theme… I was also already using the AForge.Net image manipulation library elsewhere in the application, so now it is also used by derived forms, which override LoadTheme, to modify certain control’s images on the fly when the theme changes (as well as in the video player for dynamic mouse-over affects). It is very easy to use that library to change image’s hue, saturation, contrast etc. And I have found it very effective to modify images this way on the fly rather than storing unnecessary images in resources. (There are loads of images stored in resources already, without creating different versions of the same images for mouse-over affects, and so on. I see that as wasteful.)

Enough! It’s Friday afternoon. I’m tired. For more details, read the source code.

Advertisements

About Jerome

I am a senior C# developer in Johannesburg, South Africa. I am also a recovering addict, who spent nearly eight years using methamphetamine. I write on my recovery blog about my lessons learned and sometimes give advice to others who have made similar mistakes, often from my viewpoint as an atheist, and I also write some C# programming articles on my programming blog.
This entry was posted in Programming and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s