Kinnara / ModernWpf

Modern styles and controls for your WPF applications
MIT License
4.51k stars 450 forks source link

PowerShell toggle theme #516

Closed ClarkRSD closed 1 year ago

ClarkRSD commented 2 years ago

Is it possible to toggle the theme using PowerShell? I can't seem to get it to work.

My understanding is I just add the resource dictionaries to the window and pages, then use the class to change it within PowerShell, but calling the Current method shows that it's supposed to be dark but it's still light. Does it have something to do with PowerShell being single threaded? I'm sure I'm doing something wrong but can't seem to figure out what it is.

$btnPage1.Add_Click({
    [ModernWpf.ThemeManager]::Current.ApplicationTheme = [ModernWpf.ApplicationTheme]::Dark
})
PS > [ModernWpf.ThemeManager]::Current

ApplicationTheme              : Dark
ActualApplicationTheme        : Dark
AccentColor                   : #FFFF0000
ActualAccentColor             : #FFFF0000
DependencyObjectType          : System.Windows.DependencyObjectType
IsSealed                      : False
Dispatcher                    : System.Windows.Threading.Dispatcher
ActualApplicationThemeChanged : 
ActualAccentColorChanged      : 

Window XAML:

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ui="http://schemas.modernwpf.com/2019"
    Height="450"
    Width="800"
    ui:TitleBar.ExtendViewIntoTitleBar="True"
    ui:WindowHelper.UseModernWindowStyle="True">
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ui:ThemeResources />
                <ui:XamlControlsResources />
                <ui:ColorPaletteResources TargetTheme="Light"/>
                <ResourceDictionary Source="pack://application:,,,/ModernWpf;component/ThemeResources/Dark.xaml" />
                <ResourceDictionary Source="pack://application:,,,/ModernWpf;component/ThemeResources/Light.xaml" />
                <ResourceDictionary Source="pack://application:,,,/ModernWpf;component/ControlsResources.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <ui:NavigationView
        IsBackButtonVisible="Visible"
        IsTitleBarAutoPaddingEnabled="False"
        PaneTitle="PowerShell"
        IsBackEnabled="True"
        PaneDisplayMode="Left"
        Name="Navigation">
        <ui:NavigationView.MenuItems>
            <ui:NavigationViewItem Icon="Home" Content="Sample Item 1" Tag="Page1" IsSelected="True" />
            <ui:NavigationViewItem Icon="Keyboard" Tag="Page2" Content="Sample Item 2" />
        </ui:NavigationView.MenuItems>
        <ui:Frame Name="ContentFrame"/>
    </ui:NavigationView>
</Window>

Page 1 XAML:

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:ui="http://schemas.modernwpf.com/2019"
      Title="Page1">
    <Page.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ui:ThemeResources />
                <ui:XamlControlsResources />
                <ui:ColorPaletteResources TargetTheme="Light"/>
                <ResourceDictionary Source="pack://application:,,,/ModernWpf;component/ThemeResources/Dark.xaml" />
                <ResourceDictionary Source="pack://application:,,,/ModernWpf;component/ThemeResources/Light.xaml" />
                <ResourceDictionary Source="pack://application:,,,/ModernWpf;component/ControlsResources.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Page.Resources>

    <Grid>
        <Button Content="Page 1" Name="btnPage1"/>
    </Grid>
</Page>

Page 2 XAML:

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:ui="http://schemas.modernwpf.com/2019"
      Title="Page1">
    <Page.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ui:ThemeResources />
                <ui:XamlControlsResources />
                <ui:ColorPaletteResources TargetTheme="Light"/>
                <ResourceDictionary Source="pack://application:,,,/ModernWpf;component/ThemeResources/Dark.xaml" />
                <ResourceDictionary Source="pack://application:,,,/ModernWpf;component/ThemeResources/Light.xaml" />
                <ResourceDictionary Source="pack://application:,,,/ModernWpf;component/ControlsResources.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Page.Resources>

    <Grid>
        <Button Content="Page 2" Name="btnPage2"/>
    </Grid>
</Page>

Script:

# Add frameworks
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing, WindowsBase, PresentationCore
[reflection.Assembly]::LoadFrom("Assemblies\ModernWpf.dll") | Out-Null
[reflection.Assembly]::LoadFrom("Assemblies\ModernWpf.Controls.dll") | Out-Null

# XAML
[xml]$Main = Get-Content -Path "GUI.xaml"
[xml]$Page1xaml = Get-Content -Path "Page1.xaml"
[xml]$Page2xaml = Get-Content -Path "Page2.xaml"

# Create variables
$readerWindow=(New-Object System.Xml.XmlNodeReader $Main)
$windowMain=[Windows.Markup.XamlReader]::Load( $readerWindow )
$Main.SelectNodes("//*[@Name]") | ForEach-Object { Set-Variable -Name ($_.Name) -Value $windowMain.FindName($_.Name) }

$readerPage1=(New-Object System.Xml.XmlNodeReader $Page1xaml)
$Page1=[Windows.Markup.XamlReader]::Load( $readerPage1 )
$Page1xaml.SelectNodes("//*[@Name]") | ForEach-Object { Set-Variable -Name ($_.Name) -Value $Page1.FindName($_.Name) }

$readerPage2=(New-Object System.Xml.XmlNodeReader $Page2xaml)
$Page2=[Windows.Markup.XamlReader]::Load( $readerPage2 )
$Page2xaml.SelectNodes("//*[@Name]") | ForEach-Object { Set-Variable -Name ($_.Name) -Value $Page2.FindName($_.Name) }

# Test theme toggle button
$btnPage1.Add_Click({
    [ModernWpf.ThemeManager]::Current.ApplicationTheme = [ModernWpf.ApplicationTheme]::Dark
})

# Nav handler
$Navigation.Add_ItemInvoked({
    switch ($Navigation.SelectedItem.Tag){
        "Page1" {
            $ContentFrame.Navigate($Page1)
        }
        "Page2" {
            $ContentFrame.Navigate($Page2)
        }
    }
})

# Navigate to Page 1 on open
$ContentFrame.Navigate($Page1)

# Back requested handler
$Navigation.Add_BackRequested({
    if ($ContentFrame.CanGoBack){
        $ContentFrame.GoBack()
    }
})

$windowMain.ShowDialog() | Out-Null
ClarkRSD commented 1 year ago

I was able to fix this. I still cannot get accent colors to work without recompiling, but this satisfies my needs. You're able to change it by utilizing the [ModernWpf.ThemeManager]::SetRequestedTheme() method. Looking at the source code it just sets the value of ui:ThemeManager.RequestedTheme to either Light or Dark, which is good enough to be able to change in PowerShell without recompiling. I know that class is intended for changing themes on elements but that's the only solution. There isn't a public one for setting the accent color and every property I found to change the accent color requires that recompile to happen. I also tried changing the ResourceDictionary after it's been parsed already and it requires a recompile there as well.

Demo

Just make sure you specify the following ResourceDictionary with a color in-place as well as ui:ThemeManager.HasThemeResources="True", ui:ThemeManager.RequestedTheme="Light", and ui:ThemeManager.IsThemeAware="True".

<Window.Resources>
    <ResourceDictionary x:Key="ThemeResourceDictionary">
        <ResourceDictionary.MergedDictionaries>
            <ui:ColorPaletteResources Accent="Blue" />
            <ui:ThemeResources/>
            <ui:XamlControlsResources />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ui="http://schemas.modernwpf.com/2019"
    Height="450"
    Width="800"
    ui:TitleBar.ExtendViewIntoTitleBar="True"
    ui:WindowHelper.UseModernWindowStyle="True"
    ui:ThemeManager.IsThemeAware="True"
    ui:ThemeManager.RequestedTheme="Light"
    ui:ThemeManager.HasThemeResources="True">
    <Window.Resources>
        <ResourceDictionary x:Key="ThemeRD">
            <ResourceDictionary.MergedDictionaries>
                <ui:ColorPaletteResources Accent="Blue" />
                <ui:ThemeResources/>
                <ui:XamlControlsResources />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <ui:NavigationView
        IsBackButtonVisible="Visible"
        IsTitleBarAutoPaddingEnabled="False"
        PaneTitle="PowerShell"
        IsBackEnabled="True"
        PaneDisplayMode="LeftCompact"
        Name="Navigation"
        IsSettingsVisible="False"
        IsPaneOpen="False">
        <ui:NavigationView.MenuItems>
            <ui:NavigationViewItem Icon="Home" Content="Sample Item 1" Tag="Page1" IsSelected="True" />
            <ui:NavigationViewItem Tag="Page2" Content="Sample Item 2">
                <ui:NavigationViewItem.Icon>
                        <ui:FontIcon Glyph="&#xEDA5;"/>
                    </ui:NavigationViewItem.Icon>
            </ui:NavigationViewItem>
        </ui:NavigationView.MenuItems>
        <ui:Frame Name="ContentFrame"/>
    </ui:NavigationView>
</Window>

Here is the exact code that I ran to change the value in code-behind. Note that the $WindowObject variable is the variable that you use to run your .ShowDialog() method in your script.

$btnLight.Add_Click({
    [ModernWpf.ThemeManager]::SetRequestedTheme($WindowObject,[ModernWpf.ElementTheme]::Light)
})
$btnDark.Add_Click({
    [ModernWpf.ThemeManager]::SetRequestedTheme($WindowObject,[ModernWpf.ElementTheme]::Dark)
})

If you specify a key for your ResourceDictionary you can also dynamically change it using this helper function that I created. Make sure that you run this function BEFORE you parse the xml file, otherwise the changes won't get reflected. See script block at the bottom for where exactly it needs to go.

Function Set-AccentColor {
    param(
        [string]$AccentColor,
        [System.Xml.XmlNode]$Element
    )

    if ($AccentColor){
        $Color = $AccentColor
    }
    else {
        # Get decimal value for system accent color
        [Int64]$ColorDec = (Get-ItemProperty "HKCU:\Software\Microsoft\Windows\DWM\" -Name 'AccentColor').AccentColor

        # Convert to hex and turn to array because it's in ABGR and we need it in RGB
        $Hex = ([System.Convert]::ToString($ColorDec,16)).ToCharArray()

        # Grab each value and convert to RGB
        $R = ($Hex)[6,7] -join ''
        $G = ($Hex)[4,5] -join ''
        $B = ($Hex)[2,3] -join ''

        # Turn into a single string
        $Color = $R + $G + $B
    }

    # Create a Namespace object and add the x: namespace
    [System.Xml.XmlNamespaceManager]$NamespaceManager = $Element.NameTable
    $NamespaceManager.AddNamespace("x", "http://schemas.microsoft.com/winfx/2006/xaml")

    # Create a variable for the ResourceDictionary and modify the ResourceDictionary before it's parsed
    $Key = $Element.SelectNodes("//*[@x:Key]",$NamespaceManager)
    New-Variable -Name "AccentColorData" -Value $Key.'ResourceDictionary.MergedDictionaries'

    if ($Color -like "#*"){
        $AccentColorData.ColorPaletteResources.Accent = $Color
    }
    else {
        $AccentColorData.ColorPaletteResources.Accent = "#$($Color)"
    }

}
[xml]$Content = Get-Content -Path C:\Path\To\xaml\file.xaml
Set-AccentColor -Element $Content
$reader = (New-Object System.Xml.XmlNodeReader $Content)
$parser = [Windows.Markup.XamlReader]::Load($reader)