もくじ
類似記事
- [C#/C++] WPFアプリでディスプレイのON/OFFを取得する(そのほか、WM_POWERBROADCASTでサスペンド⇔レジューム等も取れる)
- [C#/WPF] ユーザーがログイン/ログアウト、PCをシャットダウン等したときになにかしたい
やりたいこと
Windowsの省電力の設定画面で、設定した時間が経過したらディスプレイの電源をOFFしているときに、ディスプレイの電源が切れたことをC#のプログラムで知りたい。
下記のページを参考に、やってみる。
https://www.366service.com/jp/qa/8cddf991f384dadaa31692fc612bd4e0
やり方
「電源設定の変化イベント」をWindowsから受け取って、その中に含まれているディスプレイのONOFFの情報を読み取る形で実現する。
手順
-
まずは、こちらのやり方で、ウインドウメッセージハンドラをフックするメソッドを用意する。(下のC#サンプル中の
WndProc()
がそれにあたる。) -
RegisterPowerSettingNotification()
で、自分のアプリ(ウインドウ)に電源設定変更イベントが来るように設定(登録)する。→参照-
RegisterPowerSettingNotification()
はUser32.dll
の中に含まれているWin32APIなので、C#から呼べるようにP/Invoke登録してやる。(一緒に使うUnregisterPowerSettingNotification()
関数も一緒に行う)
-
-
PBT_POWERSETTINGCHANGE
がきたときのWM_POWERBROADCAST
のlParam
は電源設定を格納したPOWERBROADCAST_SETTING
なので、その中身を見る。→参照 -
メッセージハンドラの中で、
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]
が、欲しいデータ。
そのData[1]の中に何が入っているかは、MSのPower Setting GUIDsのページに書いてある。
今回はGUID_CONSOLE_DISPLAY_STATE
を使うので、下記のデータがとれる。
→上記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