7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-01-24

もくじ

類似記事

やりたいこと

Windowsの省電力の設定画面で、設定した時間が経過したらディスプレイの電源をOFFしているときに、ディスプレイの電源が切れたことをC#のプログラムで知りたい。
image.png

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

やり方

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

手順

  • まずは、こちらのやり方で、ウインドウメッセージハンドラをフックするメソッドを用意する。(下のC#サンプル中のWndProc()がそれにあたる。)

  • RegisterPowerSettingNotification()で、自分のアプリ(ウインドウ)に電源設定変更イベントが来るように設定(登録)する。→参照

    • RegisterPowerSettingNotification()User32.dllの中に含まれているWin32APIなので、C#から呼べるようにP/Invoke登録してやる。(一緒に使うUnregisterPowerSettingNotification()関数も一緒に行う)
  • PBT_POWERSETTINGCHANGEがきたときのWM_POWERBROADCASTlParamは電源設定を格納したPOWERBROADCAST_SETTINGなので、その中身を見る。→参照

    • POWERBROADCAST_SETTINGWinUser.hに定義されている定数なので、C#で使う場合は同じ構成でstructしなおしておいてやる。
      image.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ならバッテリののこり容量)
      参照
  • 今回は、ディスプレイの状態(ON/OFF)を取りたいので、GUID_CONSOLE_DISPLAY_STATEで判定する。

  • 各GUIDによって、下記のようにlParamをキャストする(下記はPOWERBROADCAST_SETTINGの場合)

var pbs = (POWERBROADCAST_SETTING)Marshal.PtrToStructure(lParam,typeof(POWERBROADCAST_SETTING));
  • その中のData[1]が、欲しいデータ。
    image.png
    そのData[1]の中に何が入っているかは、MSのPower Setting GUIDsのページに書いてある。
    今回はGUID_CONSOLE_DISPLAY_STATEを使うので、下記のデータがとれる。
    image.png
    →上記3つがとれるので、ディスプレイが消えたかどうか、は、値が0かどうか、で判定できそう。

実験コード(C++)

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

#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()だけだが、そのための準備がたくさん書かれている)

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でサスペンド⇔レジュームも取れる

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

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

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?