Closed watfordjc closed 4 years ago
The first control I have decided to style appears to also be the hardest to get a template for: the tray icon context menu.
The reasons are rather simple:
That last reason is rather important, in terms of Handle OS settings for high contrast (or another TODO item that might mention dark/light mode). In Windows 10, the default user preference setting for Windows colours is Dark.
Under Personalisation in Windows 10, under Colours, there is the Choose your colour dropdown menu.
At the time of this comment, the following Custom setting is the default (because default app mode is a recent customisation option):
So, all is good? Other than removing custom colours when High Contrast is enabled, dark mode is just something that can be added later. The windows use the Windows default colours of Light Mode, and if Microsoft were to change the default you would hope they would make it a lot easier to change the default colours in WPF apps. A system-wide dark mode theme would be the obvious option.
Not quite. Point 3 above. What exactly does Windows mean by "default Windows mode"?
Hitting F1 did nothing, let's try Get help and see what Microsoft's robot assistant says...
Personalize your PC by enabling the dark theme
You can change the appearance of your apps (like Mail and Calendar) and Windows interface (which includes the taskbar, Start menu, Action Center, touch keyboard, and more) instantly from light mode to dark mode – great for low-light conditions like working at night or for saving battery power. When you switch your apps or Windows interface to dark mode, they appear with a black background. When in light mode, the background appears white or gray.
To return Windows 10 to default settings:
- Open Settings, then select Personalization. Select Colors, and under "Choose your color", select Custom from the dropdown options. Set "Choose your default Windows mode" to Dark, and "Choose your default app mode" to Light.
Note: The dark theme was added in Windows 10 version 1607. If you don't see these options in Settings, make sure you've installed the latest Windows Updates.
Default Windows mode includes the taskbar. If you look at the context menus for the clock, sound, battery, and network, they are all in dark mode when using the default settings. If I'm going to be looking at styles, it would make sense to start by looking at the one part of my app that doesn't follow the default Personalisation Colour settings.
I opened Visual Studio Blend for the first time and... you can't easily create a custom ContextMenu
control because, well, it disappears from the Live Visual Tree as soon as you try to click it.
This StackOverflow answer was really helpful here as it gave some code that would allow extracting any template:
var str = new StringBuilder();
using (var writer = new StringWriter(str))
XamlWriter.Save(ContextMenu.Template, writer);
Debug.Write(str);
Or the version I ended up having due to me not (yet?) seeing the point of var
, my tab-complete choices, and the fact I'm using C# 8 (simplified using
statements):
StringBuilder stringBuilder = new StringBuilder();
using StringWriter stringWriter = new StringWriter(stringBuilder);
XamlWriter.Save(contextMenu1.Template, stringWriter);
Debug.Write(stringBuilder.ToString());
contextMenu1
contained the following, as I wasn't sure if I needed an instance of ContextMenu
in order to generate a template that includes a ScrollViewer
- something I'd seen mentioned elsewhere whilst searching for a template.
<Grid>
<Button Content="Click Me!">
<Button.ContextMenu>
<ContextMenu x:Name="contextMenu1">
<MenuItem Header="Test" />
<MenuItem Header="Test2">
<MenuItem Header="Test4" />
<MenuItem Header="Test5" IsChecked="True" />
</MenuItem>
</ContextMenu>
</Button.ContextMenu>
</Button>
</Grid>
After a bit of copying and pasting in order to turn that single line from the Output window into something readable, and some newline insertions to reduce horizontal scroll and de-tabbing to remove superfluous indenting for a Github code block, I had this:
<ControlTemplate TargetType="ContextMenu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:s="clr-namespace:System;assembly=System.Private.CoreLib"
xmlns:mwt="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero2"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<mwt:SystemDropShadowChrome Color="#00FFFFFF" Name="Shdw" SnapsToDevicePixels="True">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}"
BorderBrush="{TemplateBinding Border.BorderBrush}"
Background="{TemplateBinding Panel.Background}"
Name="ContextMenuBorder">
<ScrollViewer Style="{DynamicResource {ComponentResourceKey TypeInTargetAssembly=FrameworkElement, ResourceId=MenuScrollViewer}}"
Name="ContextMenuScrollViewer"
Margin="1,0,1,0"
Grid.ColumnSpan="2">
<Grid RenderOptions.ClearTypeHint="Enabled">
<Canvas Width="0" Height="0" HorizontalAlignment="Left" VerticalAlignment="Top">
<Rectangle Fill="{x:Null}" Name="OpaqueRect" Width="Auto" Height="Auto" />
</Canvas>
<Rectangle RadiusX="2" RadiusY="2"
Fill="#FFF1F1F1" Width="28" Margin="1,2,1,2" HorizontalAlignment="Left" />
<Rectangle Fill="#FFE2E3E3" Width="1" Margin="29,2,0,2" HorizontalAlignment="Left" />
<Rectangle Fill="#FFFFFFFF" Width="1" Margin="30,2,0,2" HorizontalAlignment="Left" />
<ItemsPresenter Name="ItemsPresenter" Margin="{TemplateBinding Control.Padding}"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"
KeyboardNavigation.DirectionalNavigation="Cycle" />
</Grid>
</ScrollViewer>
</Border>
</mwt:SystemDropShadowChrome>
<ControlTemplate.Triggers>
<Trigger Property="ContextMenuService.HasDropShadow">
<Setter Property="FrameworkElement.Margin" TargetName="Shdw">
<Setter.Value>
<Thickness>0,0,5,5</Thickness>
</Setter.Value>
</Setter>
<Setter Property="mwt:SystemDropShadowChrome.Color" TargetName="Shdw">
<Setter.Value>
<Color>#71000000</Color>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
<Trigger Property="ScrollViewer.CanContentScroll" SourceName="ContextMenuScrollViewer">
<Setter Property="Canvas.Top" TargetName="OpaqueRect">
<Setter.Value>
<Binding Path="VerticalOffset" ElementName="ContextMenuScrollViewer" />
</Setter.Value>
</Setter>
<Setter Property="Canvas.Left" TargetName="OpaqueRect">
<Setter.Value>
<Binding Path="HorizontalOffset" ElementName="ContextMenuScrollViewer" />
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>False</s:Boolean>
</Trigger.Value>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
That has a few errors when pasted into a ResourceDictionary
in my program though:
Each dictionary entry must have an associated key.
Assembly
System.Private.CoreLib
was not found. Verify that you are not missing an assembly reference. Also, verify that your project and all referenced assemblies have been built.
The name "Boolean" does not exist in the namespace "clr-namespace:System;assembly=System.Private.CoreLib".
The type 's:Boolean' was not found. Verify that you are not missing an assembly reference and that all referenced assemblies have been built.
For the first error, <ControlTemplate TargetType="ContextMenu"
becomes:
<ControlTemplate x:Key="ContextMenuTemplateLight" TargetType="ContextMenu"
For the second group of related errors, xmlns:s="clr-namespace:System;assembly=System.Private.CoreLib"
becomes:
xmlns:s="clr-namespace:System;assembly=System.Runtime"
Finally, all the XML namespaces get merged into the XML document root:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:uk.JohnCook.dotnet.StreamController.Properties"
xmlns:s="clr-namespace:System;assembly=System.Runtime"
xmlns:mwt="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero2">
I believe the base ControlTemplate
for ContextMenu
is now complete, and just needs some customisation.
ContextMenu
is just the outer layer of what needs templating. The ContextMenu
in question contains MenuItem
(with sub-menus) and Separator
children, and the /Themes/Aero2.NormalColor.xaml theme suggests styling MenuItem
could be complex.
As expected, MenuItem
is going to be rather more difficult.
MenuItem
First, there are four different types of MenuItem
:
All four of these, whilst being MenuItem
, act differently.
So, there are four types of MenuItem
. How difficult are they to style?
This is a ContextMenu
. It contains several MenuItem
.
With the changes already made to ContextMenu
it no longer has hard-coded colours. That means it can be styled without those hard-coded colours making it look ugly:
With ContextMenu
being able to be styled, there is the rather more involved process of styling its MenuItem
s.
With a lot of guesswork and trial-and-error, I have managed to get the context menu and its sub-menus to look somewhat OK for a dark mode theme.
Am I done? Nowhere close.
Remember those four types of MenuItem
I mentioned? Well, there is something I didn't say about them...
All thirteen colours used by Menu
and MenuItem
are hard-coded. Although some of them can be overridden for each individual menu item, they can't be overridden with dynamic styles/brushes/colours.
These are some of the colours I am currently overriding (application-wide) between the loading of my menu template and the loading of my menu item template:
<SolidColorBrush x:Key="Menu.Static.Background" Color="#434343" />
<SolidColorBrush x:Key="Menu.Static.Border" Color="#FF999999" />
<SolidColorBrush x:Key="Menu.Static.Foreground" Color="LightGray" />
<SolidColorBrush x:Key="MenuItem.Highlight.Background" Color="#545454" />
<SolidColorBrush x:Key="MenuItem.Highlight.Border" Color="LightGoldenrodYellow" />
I can either have a dark theme for all menus, or I can have a light theme for all menus. The only way I can currently have both is if I duplicate everything and point every single MenuItem
at the relevant ControlTemplate
.
<MenuItem Template="LightSubMenuHeaderTemplate" Header="File">
<MenuItem Template="LightSubMenuItemTemplate" Header="Exit"/>
</MenuItem>
I would, of course, also need two different sets of brushes/colours, and I will also need to update all four (now eight) templates to use the new brush names.
After doing all that I might need a third set for High Contrast mode, and I still won't be able to dynamically adjust the colours.
One of the things I came across a lot whilst modifying the context menu's item's colours was the word TemplateBinding
. My menu item templates file does have the Style
for x:Type MenuItem
, and I'm assuming TemplateBinding
refers to the properties defined in this Style
or somewhere else (auto-complete doesn't offer the full list of MenuItem
properties to choose from for TemplateBinding
).
That means I can change some things from a StaticResource Menu.Static.Background
brush to a TemplateBinding Background
brush.
What I therefore think I need to work out is how to extend MenuItem
so that additional properties can be added. If I'm on the right path, I would then extend the default Style
, and then modify the four MenuItem
ControlTemplate
s to refer to the new properties via TemplateBinding
.
I think the thing I'm after is DependencyProperty
:
The purpose of dependency properties is to provide a way to compute the value of a property based on the value of other inputs. These other inputs might include system properties such as themes and user preference, just-in-time property determination mechanisms such as data binding and animations/storyboards, multiple-use templates such as resources and styles, or values known through parent-child relationships with other elements in the element tree. —Dependency properties and CLR properties, Dependency properties overview
Styles and templates are two of the chief motivating scenarios for using dependency properties. Styles are particularly useful for setting properties that define application user interface (UI). Styles are typically defined as resources in XAML. Styles interact with the property system because they typically contain "setters" for particular properties, as well as "triggers" that change a property value based on the real-time value for another property. —Styles, Property functionality provided by a dependency property, Dependency properties overview
DependencyProperty
and InheritanceThere are three dependency properties that have so far appeared almost everywhere:
Foreground
Background
BorderBrush
ContextMenu
is based on MenuBase
which is based on ItemsControl
which is based on Control
which has the dependency properties ForegroundProperty
, BackgroundProperty
, and BorderBrushProperty
.
Menu
is also based on MenuBase
.
MenuItem
is based on HeaderedItemsControl
which is based on ItemsControl
which is based on Control
.
Ideally, I would add the new dependency properties to the relevant control so all controls inheriting from it can use the property. For example, Menu.Static.Separator
could become SeparatorBrushProperty
in MenuBase
. I know I could replace the default ControlTemplate
for something, but what about replacing a default Control
?
After 12+ hours I have no idea if I'm anywhere close.
A style intended for type '
StyledMenuItem
' cannot be applied to type 'MenuItem
'
The problem, I think, is with the inheritance. StyledMenuItem
appears to be working fine for the third type of menu item, but not the fourth type.
- A menu item in a sub-menu that doesn't have any children. Think Close or Undo.
- A menu item in a sub-menu that has a sub-menu itself. Think Recent Files or Bookmarks.
I created a new solution when I had trouble working out how to bind a sub-menu list to collection, and with the rest of my code out of the way it was easier to get to the code I needed to use. I need to break this problem down...
A sub-menu is a Popup
that contains an ItemsPresenter
. A Popup
can only have one Child
, so in the case of a MenuItem
, ItemsPresenter
is "insert ItemsPanel
here", and as far as I can tell ItemsPanel
in this case is a vertical StackPanel
.
After trying to think of a simpler control I could derive from, I have decided to go with a CheckBox
. It isn't a HeaderedContentControl
, but it is a ContentControl
and in terms of part of what I'm trying to do (adding DependencyProperty
to existing control type), is it a suitable place to start:
<SolidColorBrush x:Key="OptionMark.Static.Glyph" Color="#FF212121" />
A hard-coded Aero2.NormalColor colour for the tick/check/glyph. I think I'll standardise on glyph so my British English doesn't get in the way. The first Google result for wpf change checkbox color is StackOverflow post How to change the color of the CheckMark in WPF CheckBox control? Not the answer, but at least the question is Googleable.
CheckBox
Following on from the previous comment, I need to break the problem down. A CheckBox
is probably the simplest place to start.
ColorCheckBox
SetupLet's start from scratch. Note that I am only focusing on Windows 10 here, so I'm using Aero2.NormalColor styles. I will probably move them from Generic.xaml once I am done, into the correct theme file.
CustomControl1
to ColorCheckBox
.ColorCheckBox
's inheritance from Control
to Checkbox
<Grid>
…</Grid>
, making the text big and coloured to highlight a couple of UI problems we can try to fix:
<StackPanel Orientation="Vertical" Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<local:ColorCheckBox Content="Test ColorCheckBox" VerticalContentAlignment="Center"
Foreground="DarkGreen"
FontFamily="Open Sans"
FontSize="48" />
</StackPanel>
<Style>
…</Style>
with the Aero2 style for CheckBox
from CheckBox.xaml.x:Type CheckBox
with x:Type local:ColorCheckBox
.ColorCheckBox
style, add the Aero2 styles for FocusVisual
and OptionMarkFocusVisual
from FocusVisual.xamlIncluding all the code changes in this comment is going to get tiring. CustomControls is now its own GitHub repository so I can refer to code changes by linking to diffs instead. It might eventually end up as a library StreamController depends on.
There are a total of 12 hard-coded colours for CheckBox
, one for each of four states (Static, MouseOver, Pressed, Disabled) for each of three things (Background, Border, Glyph).
Following the naming convention used in SytemColors
, I will likely call the brushes StateItemBrush, omitting State when it is Static. OptionMark.Static.Glyph
will therefore become GlyphBrush
, and OptionMark.MouseOver.Glyph
will become MouseOverGlyphBrush
.
I am also going to define the default brushes in C# using the Aero2.NormalColor colours and use those brushes as the default metadata for the dependency properties. That will both remove the hard-coded colours from the templates and allow templates to define their own defaults through setters.
As all the hard coded colours are in hex, I am just going to manually convert them to ARGB [diff] by copying and pasting and then typing the0x
s. For example,
<SolidColorBrush x:Key="OptionMark.Static.Glyph" Color="#FF212121" />
will become:
private static readonly Brush GlyphBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0x21, 0x21, 0x21));
That leaves the issue of BorderBrush
, which MenuItem
inherits from Control
. I'm going to call that DefaultBorderBrush
.
After defining four Brush
dependency properties for glyph state colours, switching the template to use the dependency properties, and adding the colours to the window's XAML [diff], I have the following:
As with MenuItem
, it works. The problem isn't in this area, it is in the area of the wrong type of child object being created by a container.
Well, I do now have a ColorCheckBox
, surely there must be a ListItem
of some sort that uses CheckBox
?
The only thing I can find is RibbonCheckBox
. Back to MenuItem
then…
MenuItem
Given how complex MenuItem
is, it looks like I am going to have to duplicate the control just to add some dependent properties and modify the control templates.
MenuItem
ClassMenuItem.cs
starts with a enum MenuItemRole
. This enum defines the four different types of MenuItem
.
Then comes the MenuItem
class. It has some attributes, inherits from HeaderedItemsControl, and uses interface ICommandSource:
[DefaultEvent("Click")]
[Localizability(LocalizationCategory.Menu)]
[TemplatePart(Name = "PART_Popup", Type = typeof(Popup))]
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(MenuItem))]
public class MenuItem : HeaderedItemsControl, ICommandSource
DefaultEventAttribute
Specifies the default event for a component.
Click
is the default event for a MenuItem
.
LocalizabilityAttribute
Specifies the localization attributes for a binary XAML (BAML) class or class member.
There are three types of attributes:
- Category. This specifies whether a value should be modifiable from a localizer tool. See Category.
- Readability. This specifies whether a localizer tool should read (and display) a value. See Readability.
- Modifiability. This specifies whether a localizer tool allows a value to be modified. See Modifiability.
There is a LocalizationCategory Enum that defines LocalizationCategory.Menu
as:
TemplatePartAttribute
Represents an attribute that is applied to the class definition to identify the types of the named parts that are used for templating.
Remarks
Control authors apply this attribute to the class definition to inform template authors the types of the parts to use for styling the class. These parts are usually required in the template and have a specific predefined name. There can only be one element with a given name in any template.
Templates for MenuItem
should contain a Popup
with Name="PART_Popup"
.
StyleTypedPropertyAttribute
Represents an attribute that is applied to the class definition and determines the TargetTypes of the properties that are of type Style.
Remarks
Control authors apply this attribute to the class definition to specify the TargetTypes of the properties that are of type Style. For example, if you look at the declaration of the ListBox class, this attribute is used to specify that the TargetType of the ItemContainerStyle property is ListBoxItem.
Subclasses inherit this definition but can redefine the TargetType of the property by using this attribute on its own class definition.
Properties
Property: Gets or sets the name of the property that is of type Style. StyleTargetType: Gets or sets the TargetType of the Property this attribute is specifying. TypeId: When implemented in a derived class, gets a unique identifier for this Attribute. (Inherited from Attribute)
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
</Style>
</MenuItem.ItemContainerStyle>
I'd need to change that attribute to StyledMenuItem
.
public class MenuItem : HeaderedItemsControl, ICommandSource
We're in the right place as we're wanting to know about MenuItem
.
HeaderedItemsControl
Represents a control that contains multiple items and has a header.
ICommandSource
InterfaceDefines an object that knows how to invoke a command.
StyledMenuItem
There is no need to redefine MenuItemRole
as we can reuse it.
Other than that, the only thing that needs to change are:
StyleTypedProperty
attribute.MenuItem
).ICommandSource
as we can probably inherit MenuItem
's implementation.[DefaultEvent("Click")]
[Localizability(LocalizationCategory.Menu)]
[TemplatePart(Name = "PART_Popup", Type = typeof(Popup))]
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(StyledMenuItem))]
public class StyledMenuItem : MenuItem
{
}
MenuItem
Types and their Style Keys// ----------------------------------------------------------------------------
// Defines the names of the resources to be consumed by the MenuItem style.
// Used to restyle several roles of MenuItem without having to restyle
// all of the control.
// ----------------------------------------------------------------------------
There is a public static ResourceKey
getter for each type/style of MenuItem
, each of which is named in PascalCase with a name ending in TemplateKey.
There is then a private static ComponentResourceKey
for each type/style, each of which is named in _camelCase with a name ending in TemplateKey.
The public getter sets the private variable by instantiating a new ComponentResourceKey
if it is not null, otherwise returns the value of the private variable.
_topLevelItemTemplateKey = new ComponentResourceKey(typeof(MenuItem), "TopLevelItemTemplateKey");
ComponentResourceKey(Type, Object)
Initializes a new instance of a ComponentResourceKey, specifying the Type that defines the key, and an object to use as an additional resource identifier.
ComponentResourceKey
Defines or references resource keys based on class names in external assemblies, as well as an additional identifier.
Remarks
This class implements an object type that is useful for creating keys that are based on types in assemblies plus an identifier. Because you define or reference the type, you do not need to request a specific ResourceDictionary, and more than one set of resources can exist in the assembly, each differentiated by the type of their ComponentResourceKey.
If you want an easily accessible key, you can define a static property on your control class code that returns a ComponentResourceKey, constructed with a TypeInTargetAssembly that exists in the external resource assembly, and a ResourceId. The key can be used for defining alternate default styles for controls in an assembly, by swapping the original external resource assembly for a custom one. You can also define a named resource part within a larger control style or template to expose a customization entry point. This is particularly useful if you are defining a theme resource dictionary for your control.
These won't need renaming, can just change their types to StyledMenuItem
.
MenuItem
ConstructorsNext up are the constructors. The default public
constructor just calls base()
.
static MenuItem()
calls OverrideMetaData
on some inherited dependency properties, makes two calls to EventManager.RegisterClassHandler()
, and there is also this:
_dType = DependencyObjectType.FromSystemTypeInternal(typeof(MenuItem));
At the end of MenuItem.cs
_dType is defined.
// Returns the DependencyObjectType for the registered ThemeStyleKey's default
// value. Controls will override this method to return approriate types.
internal override DependencyObjectType DTypeThemeStyleKey
{
get { return _dType; }
}
private static DependencyObjectType _dType;
DependencyObjectType represents a specific underlying system (CLR) Type of a DependencyObject. DependencyObjectType is essentially a wrapper for the (CLR) Type so that it can extend some of its capabilities.
This class has no public constructor. Instances of this class can only be obtained through properties on other objects (such as DependencyObject.DependencyObjectType), or through the static method
FromSystemType
. —DependencyObjectType Class
_dType
and DTypeThemeStyleKey
aren't used anywhere else in the file, so I'll ignore them for now.
OverrideMetadata
MenuItem
calls OverrideMetadata()
on the following dependency properties:
HeaderProperty
ForegroundProperty
FontFamilyProperty
FontSizeProperty
FontStyleProperty
FontWeightProperty
ToolTipService.IsEnabledProperty
OLD_AUTOMATION
is set: AutomationProvider.AcceleratorKeyProperty
DefaultStyleKeyProperty
KeyboardNavigation.DirectionalNavigationProperty
FocusVisualStyleProperty
InputMethod.IsInputMethodSuspendedProperty
AutomationProperties.IsOffscreenBehaviorProperty
If my understanding is correct, I only need to override dependency properties that I am going to change the default values for.
DefaultStyleKey
is used for the Separators
s. FocusVisualStyle
is for keyboard focus. Foreground
is set to SystemColors.MenuTextBrush
and Font*
are set to SystemFonts.Message*
.
I'll come back to OverrideMetadata
.
MenuItem
MethodsThe primary thing wrong with my StyledMenuItem
is with MenuItemRole.SubmenuHeader
. I think what I'll do is make some notes on every method in MenuItem
, breaking it down by #region
.
For these, they are all registered with typeof(MenuItem)
and use RoutingStrategy.Bubble
.
I don't know if I can just let the base MenuItem
class deal with them.
This section of the file is way too long. Instead of going through it all, I am going to instead copy and paste MenuItem.cs
into CustomControls and see what breaks.
After trying to find some way of creating a class that inherits from MenuItem
without copying huge chunks of code from MS.Internal
and other methods/properties that are internal
only, I have decided to backtrack.
After playing around with attached properties for a bit, I think I have started working out how I'm going to do things.
First, I created a class in CustomControls to contain an attached property. I decided to go with a Brush
type as I'm trying to do styling. For a default SolidColorBrush
, I went with Brushes.Blue
. I set the attached property on the ColorCheckBox going with Brushes.Gold
, and then in the template I set the value of Background
to the value of the attached property. Here is a diff of the changes that produced the following:
As I'd want to change a lot of colours in a lot of places, I then added a second checkbox and attempted to change them simultaneously using a timer. That didn't work, as I'd need to iterate over every object I want to change the attached property's value of.
After mulling it over for a bit, I realised they were both inside a StackPanel
. I changed the timer so it changed the attached property on the StackPanel
to Brushes.DarkGoldenRod
, and I changed the template for ColorCheckBox
to add a DataTrigger
so that if it has an ancestor of x:Type StackPanel
whose BackgroundColor
(attached property) value of the becomes Brushes.DarkGoldenRod
, the checkbox background colour becomes dark golden rod. A diff for that can be found here, and this is the result:
Basing a trigger on a fixed colour is a rather limited exercise, but this is where everything comes together. The BackgroundColor
attached property was set on a StackPanel
but the colour was only applied to its descendant ColorCheckBox
es.
I already have two of the things I need to implement dark mode: an enum and some untested methods for the current mode (as well as separation of Windows mode from App mode), and I have a way of getting windows of a certain type.
As the system tray ContextMenu
is a descendant of a Window
, I should be able to set an attached property of type WindowsTheme
to a window, and then have the ContextMenu
and its MenuItem
s switch colour palette.
WindowsTheme
I have the following options in my enum (Github doesn't like 0-based numbered lists):
In order to determine the theme to use, a window can call CurrentWindowsTheme(bool applicationTheme)
where applicationTheme
is true
if wanting the "app mode" preference, or false
if wanting the "Windows mode" preference.
If the answer is HighContrast
, that is all that is needed to know. Use the colours from the Windows theme. If the answer is Default
, calling DefaultTheme(bool applicationTheme)
will give an answer if the the default for app/Windows mode is Light
or Dark
.
As I'm only focusing on the system tray context menu for now, I need that list of hard-coded colours I'm going to be triplicating:
<SolidColorBrush x:Key="Menu.Static.Background" Color="#FFF0F0F0" />
<SolidColorBrush x:Key="Menu.Static.Border" Color="#FF999999" />
<SolidColorBrush x:Key="Menu.Static.Foreground" Color="#FF212121" />
<SolidColorBrush x:Key="Menu.Static.Separator" Color="#FFD7D7D7" />
<SolidColorBrush x:Key="Menu.Disabled.Background" Color="#3DDADADA" />
<SolidColorBrush x:Key="Menu.Disabled.Border" Color="#FFDADADA" />
<SolidColorBrush x:Key="Menu.Disabled.Foreground" Color="#FF707070" />
<SolidColorBrush x:Key="MenuItem.Selected.Background" Color="#3D26A0DA" />
<SolidColorBrush x:Key="MenuItem.Selected.Border" Color="#FF26A0DA" />
<SolidColorBrush x:Key="MenuItem.Highlight.Background" Color="#3D26A0DA" />
<SolidColorBrush x:Key="MenuItem.Highlight.Border" Color="#FF26A0DA" />
<SolidColorBrush x:Key="MenuItem.Highlight.Disabled.Background" Color="#0A000000" />
<SolidColorBrush x:Key="MenuItem.Highlight.Disabled.Border" Color="#21000000" />
I might also want to change the FocusVisualStyle
, so here's the Aero2.NormalColor
defaults extracted from FocusVisual.xaml
:
<!-- [[AeroLite.NormalColor, Aero2.NormalColor]] -->
<!-- Focus Visual -->
<LinearGradientBrush x:Key="MenuItemSelectionFill"
StartPoint="0,0"
EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#34C5EBFF"
Offset="0"/>
<GradientStop Color="#3481D8FF"
Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="MenuItemPressedFill"
StartPoint="0,0"
EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#28717070"
Offset="0"/>
<GradientStop Color="#50717070"
Offset="0.75"/>
<GradientStop Color="#90717070"
Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
Does Github markdown do coloured tables? No. Does Github allow HTML? Some, but styles are stripped out.
Not to worry, I still have Visual Studio Blend open from when I was trying to work out the vertical line in menu items. The vertical line doesn't translate well, but I'm just after a way of visually representing colour schemes:
WindowsTheme.Light
The "menu" and "sub-menu" use the following colour for their borders:
The first and third Menu options (in both menus) use the following colours:
The second Menu.Disabled option (in both menus) uses the following colours:
The fourth MenuItem.Selected option in the first menu uses the following colours:
The fourth MenuItem.Highlight option in the first menu uses the following colours:
WindowsTheme.Dark
I already have my colour palette from my system tray work, so after a bit of tweaking I have this palette:
<SolidColorBrush x:Key="Menu.Static.Background" Color="#FF434343" />
<SolidColorBrush x:Key="Menu.Static.Border" Color="#FF999999" />
<SolidColorBrush x:Key="Menu.Static.Foreground" Color="LightGray" />
<SolidColorBrush x:Key="Menu.Static.Separator" Color="#FFD7D7D7" />
<SolidColorBrush x:Key="Menu.Disabled.Background" Color="#3DDADADA" />
<SolidColorBrush x:Key="Menu.Disabled.Border" Color="#FFDADADA" />
<SolidColorBrush x:Key="Menu.Disabled.Foreground" Color="#FF707070" />
<SolidColorBrush x:Key="MenuItem.Selected.Background" Color="#D0434343" />
<SolidColorBrush x:Key="MenuItem.Selected.Border" Color="LightGoldenrodYellow" />
<SolidColorBrush x:Key="MenuItem.Highlight.Background" Color="#D0434343" />
<SolidColorBrush x:Key="MenuItem.Highlight.Border" Color="LightGoldenrodYellow" />
<SolidColorBrush x:Key="MenuItem.Highlight.Disabled.Background" Color="#0A000000" />
<SolidColorBrush x:Key="MenuItem.Highlight.Disabled.Border" Color="#21000000"
Which creates this colour scheme:
There is a problem here: menu items use the same foreground colour as menu bar items. To improve contrast ratio, I will need two additional brushes: MenuItem.Selected.Foreground
and MenuItem.Highlight.Foreground
.
The lighter grey (via transparency) used for the highlighted item needs a white text foreground for contrast reasons. Although the selected item that is the parent of the sub-menu doesn't use a different background colour, using white text there might also improve contrast.
<SolidColorBrush x:Key="MenuItem.Selected.Foreground" Color="White" />
<SolidColorBrush x:Key="MenuItem.Highlight.Foreground" Color="White" />
That change has the following effect:
As I'm only looking at the menu items in the context menu for now, I think I have the colour scheme and now just need to write the Style
, ControlTemplate
and the theme switching stuff.
Out of curiosity, I fed in my Web site's usual colour scheme to have MenuItem.Selected use its background colour.
I don't think that is making it into the light theme, even though that pastel pink background colour is used for my (non-Christmas) branding on Twitter, YouTube, and Twitch. It is already in the app, as with my secondary pastel purple colour, in the system tray and window status bar icons for obs-websocket heartbeats.
The problem with using attached properties is that a context menu is outside of the visual tree of a window.
After a bit of thinking I think I have settled on the following…
GetStyledResourceDictionary
GetStyledResourceDictionary(WindowsTheme windowsTheme)
is a new method in WindowUtilityLibrary that converts a WindowsTheme to a resource dictionary for that theme. It adds two dictionaries, one for colours, and one for theme. It then returns a Collection<ResourceDictionary>
equal to the merged dictionaries.
Using WindowsTheme.Dark
as an example, the two dictionaries added are DarkColours.xaml
and DarkTheme.xaml
. The colours dictionary just contains a load of brushes.
The theme contains the following in its resource dictionary:
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="DarkColours.xaml" />
<ResourceDictionary Source="..\Templates\ContextMenuTemplate.xaml" />
<ResourceDictionary Source="..\Templates\MenuItemTemplate.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="ContextMenuStyle" TargetType="{x:Type ContextMenu}" BasedOn="{StaticResource ResourceKey=StyledContextMenu}">
<Style.Resources>
<ResourceDictionary Source="DarkColours.xaml" />
</Style.Resources>
</Style>
<Style x:Key="MenuItemStyle" TargetType="{x:Type MenuItem}" BasedOn="{StaticResource ResourceKey=StyledMenuItem}">
<Style.Resources>
<ResourceDictionary Source="DarkColours.xaml" />
</Style.Resources>
</Style>
The merged dictionary includes everything needed for the styles, and the styles are based on named styles (e.g. StyledMenuItem
from MenuItemTemplate.xaml
) with the relevant colour scheme in the style's resource dictionary. There is probably some unnecessary XAML loading, but I'm not going to try changing it until I finally make some commits (it has been a while).
There is very little change to the light theme. The most noticeable one from the Aero2.NormalColor default is that of checkboxes: their background colour is usually the highlighted colour. The border of a highlighted menu item also covers the left side border of a checkbox, which just looked bad.
So, minimally changed light theme for context menu and menu items:
The dark theme is, essentially, what I was going for in the last post. There is something missing which is part of material design: each sub-menu should have a slightly lighter background than its parent menu/sub-menu to simulate "depth". That isn't an accessibility issue, so I'll come back to it later.
I haven't dealt with high contrast yet, but the only thing that should need doing for that is fixing all of the brushes to the limited set of system colours for high contrast mode.
The vertical bar could probably do with some further work: I just had to sleep on the issue of it being off a pixel or two on mouse over. The sub-menu popup doesn't have an equivalent of Rectangle 3 which is why, in light mode, there is still a slight visual discrepancy between the two menu types.
Other than that, the two menu types are rather close in style/look now. The popup sub-menu now has a drop shadow if its parent ContextMenu
has a drop shadow, which is itself determined by Windows preferences. I don't think sub-menus from menu bars have drop shadows.
In order to reference the styles in XAML, the window's constructor creates the merged dictionary before InitializeComponent()
is called. I'm not sure if that is a problem.
Hot swapping of themes in response to a user changing their Windows preferences is something that still needs working out. I am hoping that swapping one theme with another by clearing the merged dictionary doesn't cause a crash due to the styles temporarily not existing.
There is still an issue with an inconsistent <Separator />
height, but I'll come back to that later. I think I have previously worked out how to fix it, but it looks like I've misplaced that change.
Feature Branch
Feature branch has been merged into master.
Progress
<DataGrid />
?ContextMenu
that replaces hard-coded colours with ones derived from the menu's foreground colour.Menu
(currently unused) andMenuItem
and commit 24ea5b2 made changes to theMenuItem
template. Commit 9aade0e moved the colours forMenu
andMenuItem
, and commit 357ede9 reverted commit 0868f15.GetStyledResourceDictionary
method so windows can get a collection ofResourceDictionary
suitable for the system Windows/App mode that applies to them.GetStyledResourceDictionary
on startup and apply the relevant light/dark theme to the system tray icon's context menu.Style
ofStyledWindow
exactly the same as that ofWindow
and removed theTemplate
/ControlTemplate
. Commit 7647a36 migrated all windows fromWindow
inheritance toStyledWindow
inheritance for future styling work.WindowUtilityLibrary.AccentColor
, and commit f0b18e1 replaced that with the better named methodWindowUtilityLibrary.WindowAccentColor
that takes into account there shouldn't be accents in High Contrast mode.[x] Moved: Creating an accessibility options tab/page in the preferences window has been moved to issue #27.
Background
Accessibility. Don't need more background than that.
This needs fixing and continuous reassessment.
Accessibility in Windows 10
Tools
Accessibility Insights for Windows
UIA (User Interface Automation)
WinAppDriver (Windows Application Driver)
AccEvent (Accessible Event Watcher)Inspect Tool (UI Inspect)UIA Verify (UI Automation Verify)