[C#/C++] WPFアプリでディスプレイのON/OFFを取得する（そのほか、WM_POWERBROADCASTでサスペンド⇔レジューム等も取れる）

# もくじ

https://tera1707.com/entry/2022/02/06/144447

# 類似記事

- [[C#/C++] WPFアプリでディスプレイのON/OFFを取得する（そのほか、WM_POWERBROADCASTでサスペンド⇔レジューム等も取れる）](https://qiita.com/tera1707/items/dc4d261fd0d64b8fc725)
- [[C#/WPF] ユーザーがログイン/ログアウト、PCをシャットダウン等したときになにかしたい](https://qiita.com/tera1707/items/e7b4df031b750d0663c6)

# やりたいこと

Windowsの省電力の設定画面で、設定した時間が経過したらディスプレイの電源をOFFしているときに、ディスプレイの電源が切れたことをC#のプログラムで知りたい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/efecc1cd-0e1d-9044-8333-78f8df52c233.png)

下記のページを参考に、やってみる。
https://www.366service.com/jp/qa/8cddf991f384dadaa31692fc612bd4e0

# やり方

「電源設定の変化イベント」をWindowsから受け取って、その中に含まれているディスプレイのONOFFの情報を読み取る形で実現する。

## 手順

- まずは、[こちらのやり方](https://qiita.com/tera1707/items/fc6b4bed1b2709d21a03)で、ウインドウメッセージハンドラをフックするメソッドを用意する。(下のC#サンプル中の`WndProc()`がそれにあたる。)

- `RegisterPowerSettingNotification()`で、自分のアプリ(ウインドウ)に電源設定変更イベントが来るように設定(登録)する。→[参照](https://docs.microsoft.com/en-us/windows/win32/power/registering-for-power-events)
    - `RegisterPowerSettingNotification()`は`User32.dll`の中に含まれているWin32APIなので、C#から呼べるようにP/Invoke登録してやる。(一緒に使う`UnregisterPowerSettingNotification()`関数も一緒に行う)

- `PBT_POWERSETTINGCHANGE`がきたときの`WM_POWERBROADCAST` の `lParam`は電源設定を格納した`POWERBROADCAST_SETTING`なので、その中身を見る。→[参照](https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-powerbroadcast_setting)

    - `POWERBROADCAST_SETTING`は`WinUser.h`に定義されている定数なので、C#で使う場合は同じ構成でstructしなおしておいてやる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/db7099ea-c7be-ae79-97d1-ad7cbe56a09e.png)

    - また、下記の定数も同じように、C#側で定義しなおしてやる。
        - `WinUser.h`より
            - `WM_POWERBROADCAST`
            - `PBT_POWERSETTINGCHANGE`
            - `DEVICE_NOTIFY_WINDOW_HANDLE`
        - `winnt.h`より
            - `GUID_CONSOLE_DISPLAY_STATE`


- メッセージハンドラの中で、`WM_POWERBROADCAST`を拾って処理する。やり方は、
    - メッセージが`WM_POWERBROADCAST`で、WParamが`PBT_POWERSETTINGCHANGE`のものの、lParamに`POWERBROADCAST_SETTING`のデータがのっかっているので、それを拾う。
    - `POWERBROADCAST_SETTING`のメンバ`PowerSetting`にはGUIDが入っている。それをみれば、電源設定の中でも何の設定なのかがわかる。(例：`GUID_CONSOLE_DISPLAY_STATE`ならディスプレイの状態、`GUID_BATTERY_PERCENTAGE_REMAINING`ならバッテリののこり容量)
→[参照](https://docs.microsoft.com/en-us/windows/win32/power/power-setting-guids)

- 今回は、ディスプレイの状態(ON/OFF)を取りたいので、`GUID_CONSOLE_DISPLAY_STATE`で判定する。
- 各GUIDによって、下記のようにlParamをキャストする（下記はPOWERBROADCAST_SETTINGの場合） 
 
```cs
var pbs = (POWERBROADCAST_SETTING)Marshal.PtrToStructure(lParam,typeof(POWERBROADCAST_SETTING));
```

- その中の`Data[1]`が、欲しいデータ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/d8ed4d0b-9708-ba59-2bdc-0b652cbd4034.png)  
そのData[1]の中に何が入っているかは、MSの[Power Setting GUIDs](https://docs.microsoft.com/en-us/windows/win32/power/power-setting-guids)のページに書いてある。
今回は`GUID_CONSOLE_DISPLAY_STATE`を使うので、下記のデータがとれる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/5fe57339-aaa6-c42c-eac9-ad1b5725e051.png)  
→上記3つがとれるので、ディスプレイが消えたかどうか、は、値が0かどうか、で判定できそう。

## 実験コード(C++)

C#でどうやるか、を知りたいのだが、まずはC++のダイアログベースアプリでディスプレイのON/OFFを取ってみる。

```cpp
#include "framework.h"
#include "WindowsProject1.h"
#include "resource.h"

HINSTANCE hInst;
BOOL CALLBACK MyDlgProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
    hInst = hInstance;
    DialogBox(hInst, L"MyTestDlgBase_Main", NULL, (DLGPROC)MyDlgProc);
    return (int)0;
}

BOOL CALLBACK MyDlgProc(HWND hDlg, UINT msg, WPARAM wp, LPARAM lp)
{
    GUID a = GUID_CONSOLE_DISPLAY_STATE;
    switch (msg) {
        case WM_INITDIALOG:
            RegisterPowerSettingNotification(hDlg, &a, DEVICE_NOTIFY_WINDOW_HANDLE);
            break;

        case WM_POWERBROADCAST:
            if (wp == PBT_POWERSETTINGCHANGE)
            {
                auto lppbc = (POWERBROADCAST_SETTING*)lp;
                if (lppbc->PowerSetting == GUID_CONSOLE_DISPLAY_STATE) {
                    if (lppbc->Data[0] == 0)   OutputDebugString(L"OFF"); // ディスプレイがOFF
                    else                       OutputDebugString(L"ON");  // ディスプレイON
                }
            }
            break;
    }
    return FALSE;
}

```


## 実験コード(C#)

C++でやったことと同じことを、C#でやる。
やってることは同じだが、P/Invokeの設定、定数定義だけが増えている。
(やりたいことが書いてあるのは`WndProc()`だけだが、そのための準備がたくさん書かれている)

```cs
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

namespace WpfApp61
{
    public partial class MainWindow : Window
    {
        #region RegisterPowerSettingNotificationのための準備部分
        private const int WM_POWERBROADCAST = 0x0218;
        private const int PBT_POWERSETTINGCHANGE = 0x8013;
        private static Guid GUID_CONSOLE_DISPLAY_STATE = new Guid(0x6fe69556, 0x704a, 0x47a0, 0x8f, 0x24, 0xc2, 0x8d, 0x93, 0x6f, 0xda, 0x47);
        const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000;

        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        private struct POWERBROADCAST_SETTING
        {
            public Guid PowerSetting;
            public uint DataLength;
            public byte Data;
        }

        [DllImport(@"User32.dll", SetLastError = true, EntryPoint = "RegisterPowerSettingNotification", CallingConvention = CallingConvention.StdCall)]
        static extern IntPtr RegisterPowerSettingNotification(IntPtr hRecipient, ref Guid PowerSettingGuid, uint Flags);

        [DllImport(@"User32.dll", EntryPoint = "UnregisterPowerSettingNotification", CallingConvention = CallingConvention.StdCall)]
        static extern bool UnregisterPowerSettingNotification(IntPtr RegistrationHandle);

        #endregion

        private IntPtr registerConsoleDisplayHandle = IntPtr.Zero;

        public MainWindow()
        {
            InitializeComponent();

            // フックの設定
            var hWnd = new WindowInteropHelper(Application.Current.MainWindow).EnsureHandle();
            HwndSource source = HwndSource.FromHwnd(hWnd);
            source.AddHook(new HwndSourceHook(WndProc));

            // WM_POWERBROADCAST > PBT_POWERSETTINGCHANGE > GUID_CONSOLE_DISPLAY_STATE が取れるように登録
            registerConsoleDisplayHandle = RegisterPowerSettingNotification(hWnd, ref GUID_CONSOLE_DISPLAY_STATE, DEVICE_NOTIFY_WINDOW_HANDLE);
        }

        private void Window_Closed(object sender, EventArgs e)
        {
            if (registerConsoleDisplayHandle != IntPtr.Zero)
                UnregisterPowerSettingNotification(registerConsoleDisplayHandle);
        }

        // メッセージループを記述したメソッド
        private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg == WM_POWERBROADCAST)
            {
                switch (wParam.ToInt32())
                {
                    case PBT_POWERSETTINGCHANGE:
                        var pbs = (POWERBROADCAST_SETTING)Marshal.PtrToStructure(lParam, typeof(POWERBROADCAST_SETTING));
                        if (pbs.PowerSetting == GUID_CONSOLE_DISPLAY_STATE)
                        {
                            if (pbs.Data == 0) Debug.WriteLine("--Display OFF");
                            else                        Debug.WriteLine("--Display ON");
                        }
                        break;
                }
            }
            return IntPtr.Zero;
        }
    }
}
```

# 思ったこと

ウインドウメッセージハンドラをC#からフックして何かするときは、C++でウインドウメッセージハンドラを書いてみてから、C#のコードを書いてみたらわかりやすい気がした。

例えば`WM_POWERBROADCAST`って、何番だったっけ？となったときに、C++版だとF12を押せば定数定義に飛べる、とか。
(何番か？を調べるときに、パッとMSの公式ページから定数定義を見つけられなかったので...)

# ギモン

似たデータで、`GUID_MONITOR_POWER_ON`がある。
こっちは、1分経ってディスプレイがOFFしたときに0、何か操作してディスプレイがONしたときに1になってるが、今回やった`GUID_CONSOLE_DISPLAY_STATE`は、ディスプレイがOFFする数秒前に2になって、実際OFFしたときに0になるっぽい。
どう違う？？

# 参考：WM_POWERBROADCASTでサスペンド⇔レジュームも取れる

下記のようなコードで、取れる。

```cs
private void root_Loaded(object sender, RoutedEventArgs e)
{
    SystemEvents.SessionSwitch += ((sender, e) => { AddLog("SessionSwitch       :" + e.Reason.ToString()); });
    SystemEvents.SessionEnding += ((sender, e) => { AddLog("SessionEnding       :" + e.Reason.ToString()); });
    SystemEvents.SessionEnded += ((sender, e) => { AddLog("SessionEnded        :" + e.Reason.ToString()); });
    SystemEvents.PowerModeChanged += ((sender, e) => { AddLog("PowerModeChanged    :" + e.Mode.ToString()); });
    SystemEvents.EventsThreadShutdown += ((sender, e) => { AddLog("EventsThreadShutdown:" + e.ToString()); });

    var hWnd = new WindowInteropHelper(this).EnsureHandle();
    HwndSource source = HwndSource.FromHwnd(hWnd);
    source.AddHook(new HwndSourceHook(WndProc));
}

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    if (msg == WM_POWERBROADCAST)
    {
        var wpStr = wParam.ToInt32() switch
        {
            PBT_APMPOWERSTATUSCHANGE => "PBT_APMPOWERSTATUSCHANGE",
            PBT_APMRESUMEAUTOMATIC => "PBT_APMRESUMEAUTOMATIC",
            PBT_APMRESUMESUSPEND => "PBT_APMRESUMESUSPEND",
            PBT_APMSUSPEND => "PBT_APMSUSPEND",
            PBT_POWERSETTINGCHANGE => "PBT_POWERSETTINGCHANGE", // ←win11 22H2で試した限り、RegisterPowerSettingNotificationをよばないとこれは来なかった。
                                                                //   サスペンド、レジューム系は、呼ばなくても来た。
            _ => "不明なイベント",
        };
    }
    return IntPtr.Zero;
}

```



# 参考

実験コードの元にしたページ
ディスプレイパワーがオン/オフに切り替わったときに発生したイベント
https://www.366service.com/jp/qa/8cddf991f384dadaa31692fc612bd4e0

[WPF] ウインドウメッセージハンドラをフックする
https://qiita.com/tera1707/items/fc6b4bed1b2709d21a03

Registering for Power Events
パワーイベントが来てくれるように登録するやり方
https://docs.microsoft.com/en-us/windows/win32/power/registering-for-power-events

PBT_POWERSETTINGCHANGE event
https://docs.microsoft.com/en-us/windows/win32/power/pbt-powersettingchange

WM_POWERBROADCAST message
https://docs.microsoft.com/en-us/windows/win32/power/wm-powerbroadcast

POWERBROADCAST_SETTING structure (winuser.h)
https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-powerbroadcast_setting

Power Setting GUIDs
**同じ`PBT_POWERSETTINGCHANGE`で、ここにある種類の情報が取れる**
https://docs.microsoft.com/en-us/windows/win32/power/power-setting-guids
バッテリーの残量変化(`GUID_BATTERY_PERCENTAGE_REMAINING`)とかもとれる。

POWERBROADCAST_SETTING の Data[1] の中身について
https://www.codeproject.com/Articles/1193099/Determining-the-Monitors-On-Off-sleep-Status
~~※Data[1]の中身が、MSの公式から見つけられなかった..~~
→あった。下記に、取れるデータの種類(GUID)と、その時の値が何か(Data)が書いてある。
https://docs.microsoft.com/en-us/windows/win32/power/power-setting-guids