ライト/ダーク/ハイコントラストの変化時イベントはWinUI3で取れるか?

もくじ
https://tera1707.com/entry/2022/02/06/144447#WinUI3

やりたいこと

Windowsの設定上で、ライト/ダークモードとハイコントラストのONOFFが変化したときに、なにか処理をしたい。

前提

  • Windows10 Home 21H1 19043.1706
  • VisualStudio2022 Community 17.2.3
  • Windows App SDK 1.1.1
  • 2022年7月の時点の調査

結論

イベントを取って、処理できそう。

調べた結果

UWPでは、Microsoft.Toolkit.Uwp.UIの中に、「ThemeListener」という、ライト/ダーク/ハイコントラストの変化を検出できる、そのものズバリなクラスがある。

Microsoft.Toolkit.Uwp.UI
https://www.nuget.org/packages/Microsoft.Toolkit.Uwp.UI/7.1.2?_src=template

ThemeListenerクラスのページ
https://docs.microsoft.com/en-us/dotnet/api/microsoft.toolkit.uwp.ui.helpers.themelistener?view=win-comm-toolkit-dotnet-7.0

が、このnugetパッケージをWinUI3のプロジェクトで使おうとすると、「対応してません」と怒られる。

そこで、そのThemeListenerのコードがgithubに上がっているので、それを参考にWinUI3用のThemeListenerを自作できないかな?と思い、やってみた。

結果、できそう。

「できそう」、とちょっと不安げなのは、

  • 今回作ったコードの中で「UISettings」クラスの中のColorValuesChangedというイベントを、
  • ライト/ダーク/ハイコントラストの変化時のイベントとして使っていて、実際そのように動いてくれているが、
  • ColorValuesChangedが、本当にその変化時のイベントとして使えるという公式資料が見つけられなかったのでちょっと不安

という理由。

Microsoft.Toolkit.Uwp.UIの中のThemeListenerのコードでは、

  • ライト/ダークモードの変化イベントとして、UISettings クラスColorValuesChanged イベントを使っている。
  • ハイコントラストの変化イベントとして、AccessibilitySettings クラスHighContrastChanged イベントを使っている。

のだが、どうもWinUI3アプリでは、AccessibilitySettings クラスのHighContrastChangedは動いてくれない様子。

参考:
[Preview 4] HighContrastChanged crashes app
https://github.com/microsoft/microsoft-ui-xaml/issues/4163

なので、ダメもとでUISettings クラスColorValuesChanged イベントの中で、ライトダークとハイコントラスト両方を見るようにしてやると、ライト/ダークの変化時とハイコントラスト変化時の両方にうまくイベント発火してくれてるように見える。

ColorValuesChanged が本当にそういう使い方してもいいのか、を、MSのドキュメントの中をいろいろ探しては見たのだが、はっきりそうと書いてあるものは見つけられなかった。

一度試しに下記コードで実装して、様子を見ようと思う。

実験コード

Microsoft.Toolkit.Uwp.UI を参考に、今回作ったMyThemeListener

using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using System;
using Windows.UI.ViewManagement;

namespace ThemeListnerForWInUI3.Model
{
    public delegate void ThemeChangedEvent(MyThemeListener sender);

    public class MyThemeListener :IDisposable
    {
        private UISettings _settings = new();
        private AccessibilitySettings _accessible = new();
        private DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();

        public ApplicationTheme CurrentTheme { get; set; }
        public bool IsHighContrast { get; set; }

        public event ThemeChangedEvent ThemeChanged;

        public MyThemeListener()
        {
            CurrentTheme = Application.Current.RequestedTheme;
            IsHighContrast  = _accessible.HighContrast;

            _settings.ColorValuesChanged += _settings_ColorValuesChanged; ;
        }

        public void Dispose()
        {
            _settings.ColorValuesChanged -= _settings_ColorValuesChanged;
        }

        private void _settings_ColorValuesChanged(UISettings sender, object args)
        {
            dispatcherQueue.TryEnqueue(() =>
            {
                if (CurrentTheme != Application.Current.RequestedTheme
                    || IsHighContrast != _accessible.HighContrast)
                {
                    CurrentTheme = Application.Current.RequestedTheme;
                    IsHighContrast = _accessible.HighContrast;

                    ThemeChanged?.Invoke(this);
                }
            });
        }
    }
}

それを使った画面のコードビハインド

using Microsoft.UI.Xaml;
using System.Diagnostics;
using ThemeListnerForWInUI3.Model;

namespace ThemeListnerForWInUI3
{
    public sealed partial class MainWindow : Window
    {
        MyThemeListener mtl = new();

        public MainWindow()
        {
            this.InitializeComponent();

            // 自作テーマリスナーに登録
            mtl.ThemeChanged += Mtl_ThemeChanged;

            this.Closed += MainWindow_Closed;
        }

        private void MainWindow_Closed(object sender, WindowEventArgs args)
        {
            // ウインドウを閉じるときに、イベント解除して自作テーマリスナーをDispose
            mtl.ThemeChanged -= Mtl_ThemeChanged;
            mtl.Dispose();
        }

        private void Mtl_ThemeChanged(MyThemeListener sender)
        {
            // テーマとハイコントラストが変化したら、ココが呼ばれる
            Debug.WriteLine("テーマ:" + sender.CurrentTheme);
            Debug.WriteLine("ハイコントラスト:" + sender.IsHighContrast);
        }
    }
}

※画面のxamlは、デフォで配置されるボタン削除して、Gridだけ配置されてる何もしないxamlなので割愛。

気づいたこと

Windowsがハイコントラストモードに入っているときは、「ダーク/ライトモード」は存在しない設定というか概念のようで、通常下記のように見えている「色」の設定画面が、

ハイコントラスト中は下記のようになる。

で、今回使ったApplication.Current.RequestedTheme_accessible.HighContrastの値も、ハイコントラスト中は、

  • Application.Current.RequestedTheme → Light
  • _accessible.HighContrast → true

になっていた。

※もともと、ダークモードだった場合は、

  • まず RequestedTheme = Dark / HighContrast = true になったあと、
  • RequestedTheme = Light / HighContrast = true になる

という2段階の動きをする様子。2回通られると困るような処理をいれるのは避けた方がよいかも。

追記

ハイコントラスト時は

  • RequestedTheme = Light / HighContrast = true

になってると書いたが、

  • RequestedTheme = Dark / HighContrast = true

になっているケースもあった。ちょっとWindowsの中でどういう動きをしているかわからないので、あまり気にしない方がよいかも。

※ただ「RequestedTheme = Light / HighContrast = true」「RequestedTheme = Dark/ HighContrast = true」のどちらになるケースでも、元のモードによって2回通るケースがあるのは同じな様子。

追記2 SystemEvents.UserPreferenceChangedでハイコントラストの変化を取れないか?

WPFでは、下記のようにすると、ハイコントラストの変化を取ることができた。

using Microsoft.Win32;
using System.Diagnostics;
using System.Windows;
using System.Windows.Forms;

namespace WpfHighContrastChangedJikken
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;            
        }

        private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
        {
            if (e.Category == UserPreferenceCategory.Color)
            {
                Debug.WriteLine("ハイコントラストのONOFF変化しました");
                Debug.WriteLine("{0}    SystemParameters.HighContrast : {1}", e.Category, SystemInformation.HighContrast);
            }
        }
    }
}

WInUI3でも同じことができないか?全くおなじコードを書いてやってみたが、動作してくれなかった。
ハイコントラストをONOFFしたときに、WPFではSystemEvents_UserPreferenceChangedを複数Categoryで通っていたのに、WinUI3では一度も通ってくれなかった。

WInUI3では、機能しなくなっているのかも。

WM_THEMECHANGEDでハイコントラスト変化が取れる

コメントを頂いて知った内容。(とおりすがりさん、ありがとうございます)

アクセントカラー、透明効果の変更などは、WM_SETTINGCHANGEで、 ハイコントラストの変更はWM_THEMECHANGEDで、取れるとのこと。

pinvokeするのが面倒だったので、C++のコードで試してみた。

流れとしては、

  • WM_THEMECHANGEDが来たら、
  • SystemParametersInfo()関数を読んで、
  • 引数のHIGHCONTRAST構造体で受けたdwFlagsの
  • ビットの0番目を見ると、

ハイコントラストのONOFFが取れるっっぽい。

SystemParametersInfoA関数

https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfoa

HIGHCONTRASTA 構造体

https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-highcontrasta

case WM_THEMECHANGED:
{
    auto hc = HIGHCONTRAST();
    hc.cbSize = sizeof(HIGHCONTRAST);
    auto si = SystemParametersInfo(SPI_GETHIGHCONTRAST, sizeof(HIGHCONTRAST), &hc, 0);
    break;
}

ハイコントラストON時に取った情報

ハイコントラストOFFの時に取った情報

HIGHCONTRASTA 構造体のdwFlagsのハイコントラストONOFF情報

てな感じで、ハイコントラストONOFF変化時に、ハイコントラストONOFF情報が取れそう。

※結局、「Win32APIを使えば何でもできる」ということなのか...

参考

Microsoft.Toolkit.Uwp.UI の nugetパッケージ

https://www.nuget.org/packages/Microsoft.Toolkit.Uwp.UI/7.1.2?_src=template

Microsoft.Toolkit.Uwp.UIの中のThemeListenerのコード

https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp.UI/Helpers/ThemeListener.cs

[Preview 4] HighContrastChanged crashes app
WinUI3でHighContrastChanged を使うとアプリクラッシュするよ、というissue

https://github.com/microsoft/microsoft-ui-xaml/issues/4163

UISettings.ColorValuesChanged Event
今回、ライト/ダーク、ハイコントラスト変化時として使ったイベント

https://docs.microsoft.com/ja-jp/uwp/api/Windows.UI.ViewManagement.UISettings.ColorValuesChanged?view=winrt-22621

AccessibilitySettings.HighContrastChanged Event
WinUI3では動かなくなっている、ハイコントラスト変化時のイベント

https://docs.microsoft.com/ja-jp/uwp/api/windows.ui.viewmanagement.accessibilitysettings.highcontrastchanged?view=winrt-22621

参考書

WinUI3

WinUI3でアプリを作ろうと思ったときのとっかかりによかった。 msdocsに書いてある情報を、体系的に、順番に読みたいな、というときによいかも。(ただし英語)
この本で分からなかった、かゆいところに手が届かなかった部分を私は記事にしてる感じ。

C#①

表紙に書いてある通り、教科書として最適。 これからC#を勉強したいけど、ネットだけで勉強するのは効率が悪いから体系的に学べる本が欲しいときや、 ちょっとC#を勉強してコード書けるようになったけど、もう少し広く深く知りたいなというときによいと思う。
私は仕事で触れるコードを軸に、基本ネットで断片的にC#を学んだので、その知識の隙間を埋めて枝葉を広げるためにとても分かりやすかった。

C#②

C#の文法的に色々できるのは分かったが、いざ実装するときに、わかったことを使ってどう実装すればいいのか?と悩んだときに指針になりそうな本。
「プロパティ等の名前の付け方、どうすればいい?」「情報をクラス外部に見せるときに、プロパティにすべき?メソッドにすべき?」「異常だと判定したいとき、どんなときにどんな例外をスローすべき?」などなど、勉強になる部分が山ほどあった。
私のように「コードは書くけどこれであってるのか自信がない、レビューで指摘されるのが嫌だ、実装時の(心の)よりどころが欲しい」という人に最適。