Reputation: 121
My questions is very specific to ThemeResources in a Windows 10 Store App. Unfortunately several things available in "classic" WPF are different or not available here.
What I am trying to achieve for lots of ui elements:
{ThemeResource SystemAccentColor}
as value.)SystemAccentColor
key in the resourcedictionary)But I have not found a good solution to achieve all of this. If I have my own resource dictionary with the custom color, I won't get rid of it when the user would like to switch back to the system's accent color.
And using a property I am binding against has the drawback that I do not realize if the user changes the accent color in the system settings while the app is running - using the {ThemeResource}
markup it does.
Any ideas how to get this done properly?
If it would be possible to set the ThemeResource
from code I could write some behavior for this, but it seems not to be available.
Upvotes: 12
Views: 13707
Reputation: 66
I have a solution based on a couple of 'helper' classes. First up is just a container object with a DependencyProperty
Value
that can be bound or set to {ThemeResource …}
:
public class DependencyObjectReference<T> : DependencyObject where T : DependencyObject
{
#region Properties
public T Value
{
get { return (T)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
#endregion
#region Static Data
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value),
typeof(T),
typeof(DependencyObjectReference<T>),
new PropertyMetadata(default(T)));
#endregion
}
Next is the meat of the solution: a 'selector' that contains a bunch of references and selects from them based on an index:
[ContentProperty(Name = nameof(References))]
public class DependencyObjectSelector<T> : DependencyObject where T : DependencyObject
{
#region Constructors
public DependencyObjectSelector()
{
References = new DependencyObjectCollection();
}
#endregion
#region Properties
public DependencyObjectCollection References
{
get { return (DependencyObjectCollection)GetValue(ReferencesProperty); }
set { SetValue(ReferencesProperty, value); }
}
public Int32 SelectedIndex
{
get { return (Int32)GetValue(SelectedIndexProperty); }
set { SetValue(SelectedIndexProperty, value); }
}
public T SelectedObject
{
get { return (T)GetValue(SelectedObjectProperty); }
set { SetValue(SelectedObjectProperty, value); }
}
#endregion
#region Event Handlers
private void Evt_OnVectorChangedReferences(IObservableVector<DependencyObject> sender, IVectorChangedEventArgs args)
{
UpdateSelectedObject();
}
#endregion
#region Private Implementation Methods
private void UpdateSelectedObject()
{
if (
References != null
&&
SelectedIndex >= 0
&&
SelectedIndex < References.Count
&&
References[SelectedIndex] is DependencyObjectReference<T>
)
{
BindingOperations.SetBinding
(
this,
SelectedObjectProperty,
new Binding
{
Source = References[SelectedIndex],
Path = new PropertyPath(nameof(DependencyObjectReference<T>.Value))
}
);
}
else
{
ClearValue(SelectedObjectProperty);
}
}
private void OnReferencesPropertyChanged(DependencyObjectCollection oldValue, DependencyObjectCollection newValue)
{
if (oldValue != null)
oldValue.VectorChanged -= Evt_OnVectorChangedReferences;
if (newValue != null)
newValue.VectorChanged += Evt_OnVectorChangedReferences;
}
private static void ReferencesPropertyChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs args)
{
DependencyObjectSelector<T> _this = (DependencyObjectSelector<T>)dobj;
_this.OnReferencesPropertyChanged(args.OldValue as DependencyObjectCollection, args.NewValue as DependencyObjectCollection);
}
private static void SelectedIndexPropertyChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs args)
{
DependencyObjectSelector<T> _this = (DependencyObjectSelector<T>)dobj;
_this.UpdateSelectedObject();
}
#endregion
#region Static Data
public static readonly DependencyProperty ReferencesProperty =
DependencyProperty.Register(nameof(References),
typeof(DependencyObjectCollection),
typeof(DependencyObjectSelector<T>),
new PropertyMetadata(null, ReferencesPropertyChanged));
public static readonly DependencyProperty SelectedIndexProperty =
DependencyProperty.Register(nameof(SelectedIndex),
typeof(Int32),
typeof(DependencyObjectSelector<T>),
new PropertyMetadata(-1, SelectedIndexPropertyChanged));
public static readonly DependencyProperty SelectedObjectProperty =
DependencyProperty.Register(nameof(SelectedObject),
typeof(T),
typeof(DependencyObjectSelector<T>),
new PropertyMetadata(default(T)));
#endregion
}
As you can see, this class holds a collection of references and binds its SelectedObject
property to the Value
of the appropriate reference.
This binding is updated when SelectedIndex
changes, and when the reference collection itself changes.
These classes obviously can't be used in XAML as they are parameterized by type T
(which must derive from DependencyObject
). However, it's
a simple matter to subclass them:
public sealed class BrushReference : DependencyObjectReference<Brush>
{
}
public sealed class BrushSelector : DependencyObjectSelector<Brush>
{
}
The trick now is to place a BrushSelector
in some accessible ResourceDictionary
(such as your Page
's Resources
) and then bind
to its SelectedObject
property:
<Page.Resources>
<mynamespace:BrushSelector x:Key="MyBrushSelector" SelectedIndex="{x:Bind Path=MyViewModel.MyBrushIndex, Mode=OneWay}">
<mynamespace:BrushReference Value="{ThemeResource SystemControlForegroundAccentColor}"/>
<mynamespace:BrushReference Value="{ThemeResource SystemControlForegroundBaseHighBrush}"/>
<mynamespace:BrushReference Value="Red"/>
<mynamespace:BrushReference Value="Wheat"/>
</mynamespace:BrushSelector>
</Page.Resources>
<!-- ... -->
<TextBlock
Text="..."
Foreground="{Binding Source={StaticResource MyBrushSelector}, Path=SelectedObject}"
/>
Note that it's not necessary to specify a <DependencyObjectCollection>
when defining the BrushSelector
in XAML because of
the [ContentProperty]
attribute on the selector class.
A few other comments -- first, I'd prefer to have the SelectedObject
be a read-only DependencyProperty
, as it should never
be set by markup or code outside the selector, but UWP doesn't yet support that. Second, the References
property must be
of type DependencyObjectCollection
and the property itself must be a DependencyProperty
or the theme changes don't propagate
correctly. Finally, you can even use your own theme resources, and if your app doesn't specify an explicit theme, when you change
the theme in Windows Control Panel (e.g., Light -> Dark or vice-versa), those colors will update as well.
Upvotes: 1
Reputation: 362
I'm using this to set newAccentColor, until I can find a way to do it without toggling the theme. This updates all the derived brushes based on accent color:
Application.Current.Resources["SystemAccentColor"] = newAccentColor;
if (Window.Current.Content is FrameworkElement fe)
{
var requestedTheme = fe.RequestedTheme;
fe.RequestedTheme = fe.RequestedTheme == ElementTheme.Light ? ElementTheme.Dark : ElementTheme.Light;
fe.RequestedTheme = requestedTheme;
}
Upvotes: 3
Reputation: 3492
There is a way how to kinda set ThemeResource
in code... I've tested it only on W10 Creators Update so it may not work on older versions, but you can create your own resource that is referencing the original ThemeResource
you want to use and then use this resource:
XAML:
<SolidColorBrush x:Key="MyBorderBrush" Color="{ThemeResource SystemAccentColor}"/>
C#:
element.BorderBrush = (SolidColorBrush)Resources["MyBorderBrush"];
The border color of element
will be the same as the Accent Color selected in Windows Settings and it will change even when your app is running and user changes it.
Upvotes: 7
Reputation: 29792
Once I've also faced the same problem and I also haven't found a way to programatically change ThemeResource so that it will change along with phone's theme. Nevertheless there is a way to achieve what you want, but it's cumbersome and may need a lot of work when you want to implement this to many controls.
The basic idea is to use VisualStates to change from/to ThemeResource - the states are defined in xaml so this will work with ThemeResources. Then in code you can invoke the change back to phone's theme value. Here below is the sample button changing to theme's/user's color.
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button Name="ColorBtn" Content="Change users color to green rom red"/>
<local:ExtendedButton x:Name="UserBtn" Content="Change to user's theme" UserBackground="Red">
<local:ExtendedButton.Style>
<Style TargetType="local:ExtendedButton">
<!--default style's setters-->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ExtendedButton">
<Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="ThemeColor">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="RootGrid">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemColorControlAccentColor}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="UserColor"/>
</VisualStateGroup>
<!--rest of default visual states-->
</VisualStateManager.VisualStateGroups>
<ContentPresenter x:Name="ContentPresenter" AutomationProperties.AccessibilityView="Raw" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTransitions="{TemplateBinding ContentTransitions}" Content="{TemplateBinding Content}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</local:ExtendedButton.Style>
</local:ExtendedButton>
</StackPanel>
and code behind:
public class ExtendedButton : Button
{
public SolidColorBrush UserBackground
{
get { return (SolidColorBrush)GetValue(UserBackgroundProperty); }
set { SetValue(UserBackgroundProperty, value); }
}
public static readonly DependencyProperty UserBackgroundProperty =
DependencyProperty.Register("UserBackground", typeof(SolidColorBrush), typeof(ExtendedButton),
new PropertyMetadata(new SolidColorBrush(Colors.Red), (s, e) =>
{ if ((s as ExtendedButton).IsUserTheme) (s as ExtendedButton).Background = e.NewValue as SolidColorBrush; }));
// we need some property to indicate if to use user's theme or phone's
public bool IsUserTheme
{
get { return (bool)GetValue(IsUserThemeProperty); }
set { SetValue(IsUserThemeProperty, value); }
}
public static readonly DependencyProperty IsUserThemeProperty =
DependencyProperty.Register("IsUserTheme", typeof(bool), typeof(ExtendedButton), new PropertyMetadata(false, (s, e) =>
{
if ((bool)e.NewValue)
{
VisualStateManager.GoToState((s as ExtendedButton), "UserColor", false);
(s as ExtendedButton).Background = (s as ExtendedButton).UserBackground;
}
else VisualStateManager.GoToState((s as ExtendedButton), "ThemeColor", false);
}));
}
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
Random random = new Random();
UserBtn.Click += (s, e) => UserBtn.IsUserTheme = !UserBtn.IsUserTheme; ;
ColorBtn.Click += (s, e) => UserBtn.UserBackground = new SolidColorBrush(Color.FromArgb(0xFF, (byte)random.Next(255), (byte)random.Next(255), (byte)random.Next(255)));
}
}
It's a long way above just to change one color, but should work and maybe will give you an idea. Those are also DependencyProperties so you can use binding if needed.
Upvotes: 1
Reputation: 15255
In Windows 10, the name "Accent Color" is changed to "SystemControlHighlightAccentBrush", and it's a ThemeResource
Example using it
<TextBlock Foreground="{ThemeResource SystemControlHighlightAccentBrush}"
Text="This is a sample text" />
To override it, simply change value of it in App.xaml
<Application.Resources>
<SolidColorBrush x:Key="SystemControlHighlightAccentBrush" Color="Orange" />
</Application.Resources>
To switch, it's a little bit more difficult First, you need to set all the color for each theme in App.xaml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="SystemControlHighlightAccentBrush" Color="Orange" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="SystemControlHighlightAccentBrush" Color="Green" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="SystemControlHighlightAccentBrush" Color="Blue" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
Then, in the page or in code behind, you set the corresponding theme
<TextBlock x:Name="TestTextBlock"
Foreground="{ThemeResource SystemControlHighlightAccentBrush}"
RequestedTheme="Dark"
Text="This is a sample text" />
or in C#
TestTextBlock.RequestedTheme = ElementTheme.Dark;
Upvotes: 6