Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2018年12月19日] [更新 2020年03月20日] Delphi 一般・その他 [未分類
モニタの電源制御とシステムのスリープ制御
動作確認等 Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32 (一部 Windows 10.0.17134)
[整理番号 - 1060]







モニタの電源制御とシステムのスリープ制御


ディスプレイ (モニタ) の電源を切るまでの時間と、システムをスリープ状態にするまでの時間は以下のコマンドで表示される電源オプションのダイアログで設定できます。
本ページはこのタイムアウト時間の制御と設定に関する記事です。
電源関係として、シャットダウン関係のサンプルも掲載しています。


%SystemRoot%\System32\control.exe /name Microsoft.PowerOptions /page pagePlanSettings


図1
電源オプション


[備考 1]
本ページにおけるサンプルコードの説明は、
システムがスリープ状態になるまでの時間 > モニタの電源が切れるまでの時間
モニタの電源が切れるまでの時間 > スクリーンセーバが起動するまでの時間
であることを前提としています。


[備考 2]
サンプルコードで使用している SetThreadExecutionState 関数は、Delphi 10.2.3 には実装されています。使用する定数名の定義も必要ありません。





01_モニタの電源 OFF とスクリーンセーバ起動の抑止


Windows API の SetThreadExecutionState 関数を使用すると、モニタの電源が切れるのを抑止することができます。
そのためには、引数を ES_DISPLAY_REQUIRED or ES_CONTINUOUS にして SetThreadExecutionState 関数を実行します。抑止を中止するには ES_CONTINUOUS を引数にして関数を実行します。

このモニタの電源 OFF を抑止する方法は、このプログラムが起動している間だけ有効となります。プログラムを終了すると、関数の引数に ES_CONTINUOUS を指定したのと同じ動作になります。

モニタの電源 OFF を抑止しても、マウスやキー操作をしない状態が続くと、システムのスリープまでの時間が経過するとシステムはスリープ状態になります。



図2
モニタの電源 OFF を抑止するサンプル

リスト1
SetThreadExecutionState 関数を使用して、モニタの電源 OFF を抑止するサンプル
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

function SetThreadExecutionState(esFlags: DWORD): DWORD; stdcall external 'kernel32.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  ES_SYSTEM_REQUIRED  = $00000001;
  ES_DISPLAY_REQUIRED = $00000002;
  ES_CONTINUOUS       = $80000000;


//=============================================================================
//  モニタの電源OFFの機能を抑止する
//  この設定は本プログラムが起動中のみ有効となる
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  SetThreadExecutionState(ES_DISPLAY_REQUIRED or ES_CONTINUOUS);
end;

//=============================================================================
//  モニタの電源OFFの機能を抑止しない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  SetThreadExecutionState(ES_CONTINUOUS);
end;

end.


SetThreadExecutionState 関数の引数を ES_DISPLAY_REQUIRED だけにして、一定時間間隔で関数を実行することでもモニタの電源がきれるのを抑止することができます。
TTimer の OnTimer イベントを発生させる時間間隔は、モニタの電源が切れるまでのタイムアウト時間よりも短くします。この時間間隔をスクリーンセーバの起動開始までの時間よりも短くすると、スクリーンセーバの起動も抑止することができます。



図3
設計時の画面
  • TTimer を使用して、一定時間間隔でモニタの電源 OFF を抑止する
  • 時間間隔をスクリーンセーバ起動までの時間よりも短くすれば、スクリーンセーバの起動も抑止となる

リスト2
SetThreadExecutionState 関数を使用して、一定時間間隔でモニタの電源 OFF を抑止する例
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Label1: TLabel;
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

function SetThreadExecutionState(esFlags: DWORD): DWORD; stdcall external 'kernel32.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  ES_SYSTEM_REQUIRED  = $00000001;
  ES_DISPLAY_REQUIRED = $00000002;
  ES_CONTINUOUS       = $80000000;


//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Timer1.Enabled := False;
end;

//=============================================================================
//  タイマーのイベントを使用してモニタの電源OFFの機能を抑止する
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Timer1.Interval := 30 * 1000;
  Timer1.Enabled := True;
end;

//=============================================================================
//  モニタの電源OFFの機能を抑止しない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  Timer1.Enabled := False;
end;

//=============================================================================
//  Timer1のOnTimerイベント処理
//  一定の時間間隔でモニタの電源OFFの機能を抑止する
//  時間間隔はモニタOFFまでのタイムアウトの時間よりも短くする
//=============================================================================
procedure TForm1.Timer1Timer(Sender: TObject);
begin
  SetThreadExecutionState(ES_DISPLAY_REQUIRED);
end;

end.


スクリーンセーバが起動するまでの時間は、以下のコマンドで設定ダイアログを表示して確認できます。スクリーンセーバが起動するまでの時間は、例えばその下のコードで取得できます。


%SystemRoot%\System32\control.exe desk.cpl,,1

procedure TForm1.Button1Click(Sender: TObject);
var
  LWaitTime : Integer;
  LText     : string;
begin
  SystemParametersInfo(SPI_GETSCREENSAVETIMEOUT,
                       0,
                       @LWaitTime,
                       0);

  LText := IntToStr(LWaitTime) + ' 秒';
  MessageBox(Handle, PChar(LText), '結果', MB_ICONINFORMATION);
end;




02_システムのスリープ機能の抑止


Windows API の SetThreadExecutionState 関数を使用すると、システムがスリープ状態 (スタンバイ状態) になるのを抑止することができます。
そのためには、引数を ES_SYSTEM_REQUIRED or ES_CONTINUOUS にして SetThreadExecutionState 関数を実行します。抑止を中止するには ES_CONTINUOUS を引数にして関数を実行します。

このシステムのスリープ状態を抑止する方法は、このプログラムが起動している間だけ有効となります。プログラムを終了すると、関数の引数に ES_CONTINUOUS を指定したのと同じ動作になります。

システムのスリープ状態を抑止しても、マウスやキー操作をしない状態が続くと、モニタの電源が貼れるまでのタイムアウトの時間が経過するとモニタの電源は OFF になります。



図4
システムのスリープ状態を抑止するサンプル


リスト3
SetThreadExecutionState 関数を使用して、システムがスリープ状態になるのを抑止するサンプル
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

function SetThreadExecutionState(esFlags: DWORD): DWORD; stdcall external 'kernel32.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  ES_SYSTEM_REQUIRED  = $00000001;
  ES_DISPLAY_REQUIRED = $00000002;
  ES_CONTINUOUS       = $80000000;

//=============================================================================
//  システムのスリープ機能を抑止する
//  この設定は本プログラムが起動中のみ有効となる
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  SetThreadExecutionState(ES_SYSTEM_REQUIRED or ES_CONTINUOUS);
end;

//=============================================================================
//  システムのスリープ機能を抑止しない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  SetThreadExecutionState(ES_CONTINUOUS);
end;

end.


SetThreadExecutionState 関数の引数を ES_SYSTEM_REQUIRED だけにして、一定時間間隔で関数を実行することでもシステムがスリープ状態になるのを抑止することができます。
この時間間隔は、システムがスリープ状態になるまでのタイムアウト時間よりも短くします。



図5
設計時の画面
  • TTimer を使用して、一定時間間隔でシステムがスリープ状態になるのを抑止する

リスト4
SetThreadExecutionState 関数を使用して、一定時間間隔でシステムのスリープ状態を抑止する例
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Label1: TLabel;
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

function SetThreadExecutionState(esFlags: DWORD): DWORD; stdcall external 'kernel32.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  ES_SYSTEM_REQUIRED  = $00000001;
  ES_DISPLAY_REQUIRED = $00000002;
  ES_CONTINUOUS       = $80000000;


//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Timer1.Enabled := False;
end;

//=============================================================================
//  タイマーのイベントを使用してシステムのスリープ機能を抑止する
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Timer1.Interval := 30 * 1000;
  Timer1.Enabled := True;
end;

//=============================================================================
//  システムのスリープ機能を抑止しない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  Timer1.Enabled := False;
end;

//=============================================================================
//  Timer1のOnTimerイベント処理
//  一定の時間間隔でモニタの電源OFFの機能を抑止する
//  時間間隔はコンピュータのスリープまでのタイムアウトの時間よりも短くする
//=============================================================================
procedure TForm1.Timer1Timer(Sender: TObject);
begin
  SetThreadExecutionState(ES_SYSTEM_REQUIRED);
end;

end.




03_モニタの電源 OFF とシステムのスリープ機能の両方を抑止


SetThreadExecutionState 関数を使用して、モニタの電源 OFF とシステムがスリープ状態になるのを抑止するには、関数の引数を ES_DISPLAY_REQUIRED or ES_SYSTEM_REQUIRED or ES_CONTINUOUS にして関数を実行します。
この場合も、抑止効果は、このプログラムが起動している間だけ有効となります。プログラムを終了すると、関数の引数に ES_CONTINUOUS を指定したのと同じ動作になります



図6
モニタの電源 OFF とシステムのスリープ状態を抑止するサンプル

リスト5
モニタの電源 OFF とシステムのスリープ機能の両方を抑止する例
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Label1: TLabel;
    Label2: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

function SetThreadExecutionState(esFlags: DWORD): DWORD; stdcall external 'kernel32.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  ES_SYSTEM_REQUIRED  = $00000001;
  ES_DISPLAY_REQUIRED = $00000002;
  ES_CONTINUOUS       = $80000000;


//=============================================================================
//  モニタの電源OFFの機能とシステムのスリープ機能を抑止する
//  この設定は本プログラムが起動中のみ有効となる
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  SetThreadExecutionState(ES_DISPLAY_REQUIRED or ES_SYSTEM_REQUIRED or ES_CONTINUOUS);
end;

//=============================================================================
//  モニタの電源OFFの機能とシステムのスリープ機能を抑止しない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  SetThreadExecutionState(ES_CONTINUOUS);
end;

end.


SetThreadExecutionState 関数の引数を ES_DISPLAY_REQUIRED or ES_SYSTEM_REQUIRED にして、一定時間間隔で関数を実行することでも抑止することができます。
この時間間隔は、モニタの電源が切れるタイムアウト時間とシステムがスリープ状態になるまでの時間の短い方の時間よりも短くします。

この時間間隔をスクリーンセーバの起動開始までの時間よりも短くすると、スクリーンセーバの起動も抑止することができます。



図7
設計時の画面
  • TTimer を使用して、一定時間間隔でモニタの電源 OFF とシステムのスリープ機能の両方を抑止する
  • 時間間隔をスクリーンセーバ起動までの時間よりも短くすれば、スクリーンセーバの起動も抑止となる

リスト6
一定時間間隔でシスモニタの電源 OFF とシステムのスリープ機能の両方を抑止する例
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Label1: TLabel;
    Timer1: TTimer;
    Label2: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

function SetThreadExecutionState(esFlags: DWORD): DWORD; stdcall external 'kernel32.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  ES_SYSTEM_REQUIRED  = $00000001;
  ES_DISPLAY_REQUIRED = $00000002;
  ES_CONTINUOUS       = $80000000;


//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Timer1.Enabled := False;
end;

//=============================================================================
//  モニタの電源OFFの機能とシステムのスリープ機能を抑止する
//  タイマーのイベントを使用
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Timer1.Interval := 30 * 1000;
  Timer1.Enabled := True;
end;

//=============================================================================
//  モニタの電源OFFの機能とシステムのスリープ機能を抑止しない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  Timer1.Enabled := False;
end;

//=============================================================================
//  Timer1のOnTimerイベント処理
//  一定の時間間隔でモニタの電源OFFの機能とシステムのスリープ機能を抑止
//  時間間隔はタイムアウトの時間よりも短くする
//=============================================================================
procedure TForm1.Timer1Timer(Sender: TObject);
begin
  SetThreadExecutionState(ES_DISPLAY_REQUIRED or ES_SYSTEM_REQUIRED);
end;

end.




04_モニタの電源の ON と OFF


WM_SYSCOMMAND メッセージの第 1 引数 (wParam) の値を SC_MONITORPOWER にして、HWND_BROADCAST にメッセージを送信するとモニタの電源を切ったり入れたりする操作がプログラムから実行可能です。
本サンプルを実行すると、30 秒間隔でモニタの電源の ON, OFF を繰り返します。

モニタの電源が OFF の状態でも、スクリーンセーバ起動までの時間が経過すればスクリーンセーバは起動しますが、画面では見えません。スクリーンセーバが起動した状態でモニタの電源が ON になるとスクリーンセーバが起動していることが確認できます。ただし、マウスの操作やキー操作でモニタの電源を ON にした場合は、スクリーンセーバは停止します。




図8
実行時の画面
  • モニタの電源の ON, OFF を自動で繰り返す

リスト7
WM_SYSCOMMAND メッセージを使用したモニタの電源操作
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private 宣言 }
    FIsMonitorPower : Boolean;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//  オブジェクト・インスペクタで Timer1 の Enabled を False にしておく
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  FIsMonitorPower := True;

  Timer1.Interval := 30 * 1000;
  Timer1.Enabled := True;
end;

//=============================================================================
//  Timer1のOnTimerイベント処理
//  一定の時間間隔でモニタの電源をON,OFF する
//=============================================================================
procedure TForm1.Timer1Timer(Sender: TObject);
begin
  if FIsMonitorPower then begin
    // モニタの電源をOFF
    PostMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, 2);
    FIsMonitorPower := False;
  end else begin
    // モニタの電源をON
    PostMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, -1);
    FIsMonitorPower := True;
  end;
end;

end.


Windows 10 では、ビルドバージョンによってはモニタの電源 ON の機能が正常に機能しないことがあります。その場合、モニタの電源が ON になっても、しばらくすると OFF になる現象が発生します。この現象ょ回避するには、以下のように、マウスカーソル移動、またはキー操作をミュレートするしかなさそうです。


    // マウスカーソルの移動をエミュレート
    mouse_event(MOUSEEVENTF_MOVE, 1, 1, 0, 0);

    // キー操作のエミュレート
    keybd_event(VK_ESCAPE, MapVirtualKey(VK_ESCAPE, 0), 0, 0);
    keybd_event(VK_ESCAPE, MapVirtualKey(VK_ESCAPE, 0), KEYEVENTF_KEYUP, 0);


モニタの電源が ON 状態であるか OFF 状態であるかは、TPowerBroadcastSetting レコード型の Data メンバの値で知ることができます。
下図はモニタの電源を切るまでの時間を 1 分 (60 秒) にしてテストした結果です。ボタンをクリックすると現在のモニタの電源状態を表示します。一定の時間が経過すると、またモニタの電源状態をチェックします。




図9
モニタの電源が ON か OFF かを検出
  • 最初の値はボタンをクリックした時刻
  • OFF の右の時刻はモニタの電源が OFF になった時刻ではない

リスト8
TPowerBroadcastSetting レコード型の Data メンバの値でモニタの電源が ON か OFF かを判定
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Lines.Clear;
end;

//=============================================================================
//  モニタの電源が ON であるか OFF であるかを調べてタイマーを起動
//
//  TPowerBroadcastSetting レコード型の Data メンバの値で判定
//  TPowerBroadcastSetting レコード型は POWERBROADCAST_SETTING 構造体に相当
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LPwrSetting : TPowerBroadcastSetting;
  LPwrState   : Byte;
begin
  LPwrState := LPwrSetting.Data[0];

  if LPwrState = 0 then begin
    Memo1.Lines.Add('OFF :  ' + FormatDateTime('hh:nn:ss', Now));
  end else begin
    Memo1.Lines.Add('ON  :  ' + FormatDateTime('hh:nn:ss', Now));
  end;

  if Sender = nil then begin
    Timer1.Enabled := False;
  end else begin
    Timer1.Interval := 70 * 1000;
    Timer1.Enabled := True;
  end;
end;

//=============================================================================
//  Timer1 の OnTimer イベント
//  一定の時間経過後,モニタの電源が ON であるか OFF であるかを調べる
//=============================================================================
procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Button1Click(nil);
end;

end.


モニタの電源が OFF になった時、あるいは ON になった時を検出するには、メッセージの処理を行います。RegisterPowerSettingNotification 関数で検出するメッセージを指定して登録します。このサンプルでは GUID_CONSOLE_DISPLAY_STATE を指定して自分自身のウィンドウで受信するようにしています。受信用のメッセージとして WM_POWERBROADCAST を使用しています。

このサンプルで、ボタンをクリックすると現在のモニタの電源の ON, OFF 状態を検出し、モニタの電源状態の検出を開始します。下図はモニタの電源を切るまでの時間を 1 分 (60 秒) にしてテストした結果です。




図10
モニタの電源の ON と OFF を検出
  • 最初の値はボタンをクリックした時刻
  • モニタの電源を切るまでの時間を 1 分 (60 秒) にしてテスト
  • 時刻はモニタの電源が ON または OFF になった時刻の値

リスト9
WM_POWERBROADCAST メッセージでモニタの電源の ON と OFF を検出
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    FhPowerNotify : THandle;
  protected
    procedure WMPowerbroadcast(var Message: TMessage);
      message WM_POWERBROADCAST;
  public
    { Public 宣言 }
  end;

  function RegisterPowerSettingNotification(hRecipient : THandle;
                                            PowerSettingGuid: PGuid;
                                            Flags: byte): THandle;
                                            stdcall;external 'user32.dll';

  function UnregisterPowerSettingNotification(AHandle: THandle): BOOL;
                                              stdcall; external 'user32.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  // MSDN に掲載の値は一部間違いあり
  GUID_ACDC_POWER_SOURCE             : TGUID = '{5d3e9a59-e9D5-4b00-a6bd-ff34ff516548}';
  GUID_BATTERY_PERCENTAGE_REMAININGE : TGUID = '{a7ad8041-b45a-4cae-87a3-eecbb468a9e1}';
  GUID_CONSOLE_DISPLAY_STATE         : TGUID = '{6fe69556-704a-47a0-8f24-c28d936fda47}';
  GUID_GLOBAL_USER_PRESENCE          : TGUID = '{786E8A1D-B427-4344-9207-09E70BDCBEA9}';
  GUID_IDLE_BACKGROUND_TASK          : TGUID = '{515c31d8-f734-163d-a0fd-11a08c91e8f1}';
  GUID_MONITOR_POWER_ON              : TGUID = '{02731015-4510-4526-99e6-e5a17ebd1aea}';
  GUID_POWER_SAVING_STATUS           : TGUID = '{E00958C0-C213-4ACE-AC77-FECCED2EEEA5}';
  GUID_POWERSCHEME_PERSONALITY       : TGUID = '{245d8541-3943-4422-b025-13A784F679B7}';
  GUID_SESSION_DISPLAY_STATUS        : TGUID = '{2B84C20E-AD23-4ddf-93DB-05FFBD7EFCA5}';
  GUID_SESSION_USER_PRESENCE         : TGUID = '{3C0F4548-C03F-4c4d-B9F2-237EDE686376}';
  GUID_SYSTEM_AWAYMODE               : TGUID = '{98a7f580-01f7-48aa-9c0f-44352c29e5C0}';


//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  if FhPowerNotify <> 0 then begin
    UnregisterPowerSettingNotification(FhPowerNotify);
  end;
end;

//=============================================================================
//  モニタの電源の ON, OFF の監視を開始
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.Clear;
  // モニタの電源状態変更メッセージの受信設定
  // このフォームで受信する場合  
  FhPowerNotify := RegisterPowerSettingNotification(Handle,
                                                    @GUID_CONSOLE_DISPLAY_STATE,
                                                    DEVICE_NOTIFY_WINDOW_HANDLE);
end;

//=============================================================================
//  WM_POWERBROADCAST メッセージの処理
//
//  TPowerBroadcastSetting レコード型の Data メンバの値で判定
//  TPowerBroadcastSetting レコード型は POWERBROADCAST_SETTING 構造体に相当
//=============================================================================
procedure TForm1.WMPowerbroadcast(var Message: TMessage);
var
  LpPwrSetting : PPowerBroadcastSetting;
begin
  LpPwrSetting := PPowerBroadcastSetting(Message.LParam);

  if LpPwrSetting^.PowerSetting = GUID_CONSOLE_DISPLAY_STATE then begin
    if LpPwrSetting^.Data[0] = 0 then begin
      Memo1.Lines.Add('OFF :  ' + FormatDateTime('hh:nn:ss', Now));
    end else begin
      Memo1.Lines.Add('ON  :  ' + FormatDateTime('hh:nn:ss', Now));
    end;
  end;
end;

end.




05_システムをスリープ状態にする


Windows API の SetSuspendState 関数を使用して、システム (コンピュータ) をスリープ状態、つまりスタンバイの状態にするサンプルです。



[備考]
SetSystemPowerState 関数を使用しても、システムをスリープ状態にできますが、自動復帰ができなくなるようです。そのため、タスクスケジューラでスリープからの復帰を設定をしても無効になってしまうようです。
SetSuspendState 関数でも自動復帰を無効にすることができます。3 番目の引数 (最後の引数) の値を True にすれは自動復帰のイベントを無視します。




図11
EXE を実行してボタンをクリックすると、システムがスリープ状態になる

リスト10
システムをスリープ状態にするサンプル
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

function SetSuspendState(bHibernate: Boolean;
                         bForce: Boolean;
                         bWakeupEventsDisabled: Boolean): Boolean;
                         stdcall external 'PowrProf.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  コンピュータをスリープ状態にする
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  SetSuspendState(False, False, False);
end;

//=============================================================================
//  コンピュータを休止状態のスリープにする
//  休止状態が無効の場合は上のスリープと同じ
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  SetSuspendState(True, False, False);
end;

end.


休止状態が有効な場合、SetSuspendState 関数の第 1 引数の値を True にすれば休止状態 (ハイバーネーション状態) のスリープになります。休止 (ハイバーネーション) 状態が無効の場合は True を指定しても通常のスリープとなります。
コマンドラインツール powercfg.exe の hibernate オプション を使用すると、休止状態のスリープを有効または無効にできます。このオプションの実行には管理者権限が必要です。



コマンドライン (省略形) 機能
powercfg /hibernate on ( powercfg /h on ) 休止 (ハイバーネーション) の機能を有効にする
powercfg /hibernate off ( powercfg /h off ) 休止 (ハイバーネーション) の機能を無効にする


[休止状態] の機能が有効な場合、下図のように、[スタート] の [シャットダウン] メニューに [休止状態] の選択項目が表示されます。




図12
[休止状態] の機能が無効な時の [スタート] の [シャットダウン] メニュー

図13
[休止状態] の機能が有効な時の [スタート] の [シャットダウン] メニュー




06_スリープの開始とスリープからの復帰を検出


システムがスリープ (スタンバイ) あるいは休止状態になる時と復帰は、WM_POWERBROADCAST メッセージで受信できます。このメッセージは全てのウィンドウに送られてくるメッセージです。したがって、自分自身がアクティブでなくても受信できます。




図14
WM_POWERBROADCAST メッセージの受信例

リスト11
スリープの開始とスリープからの復帰を WM_POWERBROADCAST メッセージで検出
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, 
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
  protected
    procedure WMPowerbroadcast(var Message: TMessage);
      message WM_POWERBROADCAST;
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Lines.Clear;
end;

//=============================================================================
//  WM_POWERBROADCAST メッセージ処理
//=============================================================================
procedure TForm1.WMPowerbroadcast(var Message: TMessage);
var
  LTypeText: string;
begin
  case Message.WParam of
    PBT_APMPOWERSTATUSCHANGE  : LTypeText := 'PBT_APMPOWERSTATUSCHANGE';
    PBT_APMRESUMEAUTOMATIC    : LTypeText := 'PBT_APMRESUMEAUTOMATIC';
    PBT_APMRESUMESUSPEND      : LTypeText := 'PBT_APMRESUMESUSPEND';
    PBT_APMSUSPEND            : LTypeText := 'PBT_APMSUSPEND';
    PBT_POWERSETTINGCHANGE    : LTypeText := 'PBT_POWERSETTINGCHANGE';
  end;
  Message.Result := Ord(True);

  Memo1.Lines.Add(LTypeText);
end;

end.




07_スリープの開始とスリープからの復帰を検出 - Windows 8 以降


Windows 8 の Connected Standby, Windows 8.1 の InstantGo, Windows 10 の Modern Standby (モダンスタンバイ) の機能が有効な状態の場合、前項のコードではスリープ等の開始とスリープ等からの復帰のメッセージが受信できないことがあるようです。

この現象を回避するには、マイクロソフトの資料には、PowerRegisterSuspendResumeNotification 等の関数で、メッセージを受信する関数を登録して受信する方法が説明してあります。
以下はその PowerRegisterSuspendResumeNotification 関数を使用する例です。このサンプルではコールバック関数でメッセージを受信するようにしています。
下図は、自分自身からスリープ状態にして復帰した時の画面です。テストしたシステムにはモダンスタンバイの機能は実装されていません。



[備考 1]
本項のサンプルは Windows 10.0.17134 でテストしています。
PowerRegisterSuspendResumeNotification 関数と RegisterSuspendResumeNotification 関数を使用するサンプルは Windows 8 以降専用です。Windows 7 にはこれらの関数は実装されていません。
Windows 7 でも実行するコードがある場合、DLL を動的にロードして利用する必要があります。


[備考 2]
Windows 10.0.1507 以降で、休止状態のスタンバイから復帰した場合、PBT_APMRESUMEAUTOMATIC は送信されません。したがって PBT_APMRESUMEAUTOMATIC は検出できません。




図15
PowerRegisterSuspendResumeNotification 関数でコールバック関数を指定して受信

リスト12
PowerRegisterSuspendResumeNotification 関数でコールバック関数を指定してメッセージを受信
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, 
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    FPowerNotify : HPOWERNOTIFY;
  end;


  function SetSuspendState(bHibernate: Boolean;
                           bForce: Boolean;
                           bWakeupEventsDisabled: Boolean): Boolean;
                           stdcall; external 'PowrProf.dll';

  function PowerRegisterSuspendResumeNotification(Flags : DWORD;
                           Recipient : PHandle;
                           RegistrationHandle : PHPOWERNOTIFY): DWORD; 
                           stdcall; external 'Powrprof.dll';

  function PowerUnregisterSuspendResumeNotification(
                           RegistrationHandle : HPOWERNOTIFY): DWORD; 
                           stdcall; external 'Powrprof.dll';

  function DeviceNotifyCallbackRoutine(Context: PVOID; AType: ULONG;
    Setting: PVOID): ULONG; stdcall;

var
  Form1: TForm1;

const
  DEVICE_NOTIFY_CALLBACK = 2;


implementation

{$R *.dfm}

type
  //--------------------------------------------------------
  //  DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS レコード型の定義
  //--------------------------------------------------------
  DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS = record
    ACallback : Pointer;
    AContext  : PVOID;
  end;
  PDEVICE_NOTIFY_SUBSCRIBE_PARAMETERS = ^DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS;

//=============================================================================
//  フォーム生成時の処理
//
//  本サンプルは Windows 8 以降用
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  LParameters : DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS;
begin
  // 関数の第 1 引数のパラメータを設定
  // コールバックに渡す値はないので nil としている
  LParameters.ACallback := @DeviceNotifyCallbackRoutine;
  LParameters.AContext := nil;

  // システムのスタンバイ・休止関係の状態変更メッセージの受信設定
  // コールバック関数で受信する場合
  PowerRegisterSuspendResumeNotification(DEVICE_NOTIFY_CALLBACK,
                                         @LParameters,
                                         @FPowerNotify);
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  // システムのスタンバイ・休止関係の状態変更メッセージの受信設定を解除
  PowerUnregisterSuspendResumeNotification(FPowerNotify);
end;

//=============================================================================
//  コンピュータをスリープ状態にする
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.Clear;
  SetSuspendState(False, False, False);
end;

//----------------------------------------------------------------------------
//  PowerRegisterSuspendResumeNotification 関数のコールバック関数
//----------------------------------------------------------------------------
function DeviceNotifyCallbackRoutine(Context: PVOID; AType: ULONG;
  Setting: PVOID): ULONG; stdcall;
var
  LTypeText: string;
begin
  case AType of
    PBT_APMPOWERSTATUSCHANGE  : LTypeText := 'PBT_APMPOWERSTATUSCHANGE';
    PBT_APMRESUMEAUTOMATIC    : LTypeText := 'PBT_APMRESUMEAUTOMATIC';
    PBT_APMRESUMESUSPEND      : LTypeText := 'PBT_APMRESUMESUSPEND';
    PBT_APMSUSPEND            : LTypeText := 'PBT_APMSUSPEND';
    PBT_POWERSETTINGCHANGE    : LTypeText := 'PBT_POWERSETTINGCHANGE';
  end;

  Form1.Memo1.Lines.Add(LTypeText);

  Result := 0;
end;

end.


次は RegisterSuspendResumeNotification 関数を使用する例です。このサンプルもコールバック関数でメッセージを受信するようにしています。
下図は、自分自身からスリープ状態にして復帰した時の画面です。テストしたシステムにはモダンスタンバイの機能は実装されていません。




図16
RegisterSuspendResumeNotification 関数でコールバック関数を指定して受信

リスト13
RegisterSuspendResumeNotification 関数でコールバック関数を指定してメッセージを受信
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, 
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    FPowerNotify : HPOWERNOTIFY;
  end;

  function SetSuspendState(bHibernate: Boolean;
                           bForce: Boolean;
                           bWakeupEventsDisabled: Boolean): Boolean;
                           stdcall; external 'PowrProf.dll';

  function RegisterSuspendResumeNotification(hRecipient : PHandle;
                           Flags : DWORD): HPOWERNOTIFY;
                           stdcall; external 'User32.dll';

  function UnregisterSuspendResumeNotification(
                           RegistrationHandle : HPOWERNOTIFY): BOOL;

                           stdcall; external 'User32.dll';
  function DeviceNotifyCallbackRoutine(Context: PVOID; AType: ULONG;
    Setting: PVOID): ULONG; stdcall;

var
  Form1: TForm1;

const
  DEVICE_NOTIFY_CALLBACK = 2;

implementation

{$R *.dfm}

type
  //--------------------------------------------------------
  //  DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS レコード型の定義
  //--------------------------------------------------------
  DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS = record
    ACallback : Pointer;
    AContext  : PVOID;
  end;
  PDEVICE_NOTIFY_SUBSCRIBE_PARAMETERS = ^DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS;

//=============================================================================
//  フォーム生成時の処理
//
//  本サンプルは Windows 8 以降用
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  LParameters : DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS;
begin
  // 関数の第 1 引数のパラメータを設定
  // コールバックに渡す値はないので nil としている 
  LParameters.ACallback := @DeviceNotifyCallbackRoutine;
  LParameters.AContext  := nil;

  // システムのスタンバイ・休止関係の状態変更メッセージの受信設定
  // コールバック関数で受信する場合
  FPowerNotify := RegisterSuspendResumeNotification(@LParameters,
                                                    DEVICE_NOTIFY_CALLBACK);
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  // システムのスタンバイ・休止関係の状態変更メッセージの受信設定を解除
  UnregisterSuspendResumeNotification(FPowerNotify);
end;

//=============================================================================
//  コンピュータをスリープ状態にする
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.Clear;
  SetSuspendState(False, False, False);
end;

//----------------------------------------------------------------------------
//  RegisterSuspendResumeNotification 関数のコールバック関数
//----------------------------------------------------------------------------
function DeviceNotifyCallbackRoutine(Context: PVOID; AType: ULONG;
  Setting: PVOID): ULONG; stdcall;
var
  LTypeText: string;
begin
  case AType of
    PBT_APMPOWERSTATUSCHANGE  : LTypeText := 'PBT_APMPOWERSTATUSCHANGE';
    PBT_APMRESUMEAUTOMATIC    : LTypeText := 'PBT_APMRESUMEAUTOMATIC';
    PBT_APMRESUMESUSPEND      : LTypeText := 'PBT_APMRESUMESUSPEND';
    PBT_APMSUSPEND            : LTypeText := 'PBT_APMSUSPEND';
    PBT_POWERSETTINGCHANGE    : LTypeText := 'PBT_POWERSETTINGCHANGE';
  end;

  Form1.Memo1.Lines.Add(LTypeText);

  Result := 0;
end;

end.


以下も RegisterSuspendResumeNotification 関数を使用する例ですが、メッセージを受信するウィンドウを指定する場合です。本サンプルでは自分のフォームで受信するようにしています。メッセージの受信には WM_POWERBROADCAST を使用しています。
下の最初の図は、自分自身からスリープ状態にして復帰した時の画面です。スリープ状態になる時のメッセージが受信できていません。その下の図は、タイムアウト時間が経過してスリープ状態になり、その後復帰した時の結果です。スリープ状態になった時のメッセージも受信できています。




図17
メッセージ受信用のウィンドウを指定したメッセージの受信例
  • 自分自身から強制的にスリープした場合

図18
メッセージ受信用のウィンドウを指定したメッセージの受信例
  • システム設定のタイムアウト時間経過後にスリープして復帰した場合

リスト14
メッセージを受信するウィンドウを指定してメッセージを受信
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, 
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  protected
    procedure WMPowerbroadcast(var Message: TMessage);
      message WM_POWERBROADCAST;
  private
    { Private 宣言 }
    FPowerNotify : HPOWERNOTIFY;
  public
    { Public 宣言 }
  end;

  function SetSuspendState(bHibernate: Boolean;
                           bForce: Boolean;
                           bWakeupEventsDisabled: Boolean): Boolean;
                           stdcall; external 'PowrProf.dll';

  function RegisterSuspendResumeNotification(hRecipient : PHandle;
                           Flags : DWORD): HPOWERNOTIFY;
                           stdcall; external 'User32.dll';

  function UnregisterSuspendResumeNotification(
                           RegistrationHandle : HPOWERNOTIFY): BOOL;
                           stdcall; external 'User32.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//
//  本サンプルは Windows 8 以降用
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  // システムのスタンバイ・休止関係の状態変更メッセージの受信設定
  // このフォームで受信する場合
  FPowerNotify := RegisterSuspendResumeNotification(PHandle(Self.Handle),
                                                    DEVICE_NOTIFY_WINDOW_HANDLE);
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  // システムのスタンバイ・休止関係の状態変更メッセージの受信設定を解除
  UnregisterSuspendResumeNotification(FPowerNotify);
end;

//=============================================================================
//  コンピュータをスリープ状態にする
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.Clear;
  SetSuspendState(False, False, False);
end;

//=============================================================================
//  WM_POWERBROADCAST メッセージ処理
//=============================================================================
procedure TForm1.WMPowerbroadcast(var Message: TMessage);
var
  LTypeText: string;
begin
  case Message.WParam of
    PBT_APMPOWERSTATUSCHANGE  : LTypeText := 'PBT_APMPOWERSTATUSCHANGE';
    PBT_APMRESUMEAUTOMATIC    : LTypeText := 'PBT_APMRESUMEAUTOMATIC';
    PBT_APMRESUMESUSPEND      : LTypeText := 'PBT_APMRESUMESUSPEND';
    PBT_APMSUSPEND            : LTypeText := 'PBT_APMSUSPEND';
    PBT_POWERSETTINGCHANGE    : LTypeText := 'PBT_POWERSETTINGCHANGE';
  end;

  Memo1.Lines.Add(LTypeText);
end;

end.




08_スリープの開始日時とスリープからの復帰日時の取得


最後にサスペンド状態になった時間と、サスペンドから復帰した時間は、 CallNtPowerInformation 関数を施用して取得できます。
CallNtPowerInformation 関数の第 1 引数に、LastSleepTime または LastWakeTimeを指定します。

取得した値は、システムが起動してからの時間です。この時間の単位は 100 ns です。
日時に換算するには、システムが起動した日時、あるいは現在の日時を基準にして計算できます。以下のサンプルは現在の日時を基準して計算する例です。



[備考 1]
本サンプルは JEDI プロジェクの JEDI API Library のユニットを使用しています。


[備考 2]
MSDN の CallNtPowerInformation 関数の記事よると、第 1 引数の説明が「is not supported」の機能は利用できません。それ以外にも Windows XP で使用できても、Windows 7 では使用できないパラメータがあります。
「The lpInBuffer parameter must be NULL」の記述があるパラメータは取得専用です。
MSDN の記述では、パラメータが無効の時のエラーが ERROR_INVALID_PARAMETER ( = 87) となっていますが、実際には NTSTATUS のエラーである STATUS_INVALID_PARAMETER ( = $C000000D ) です。
定数名 STATUS_BUFFER_TOO_SMALL の値は $C0000023 で、STATUS_SUCCESS の値は 0 です。




図19
スリープの開始と復帰の日時

リスト15
CallNtPowerInformation 関数を使用して、スリープの開始と復帰の日時を取得
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  JwaPowrProf,
  JwaWinNT,
  Diagnostics,
  DateUtils;

{$R *.dfm}

//=============================================================================
//  最後にスリープになった日時と最後にスリープから復帰した日時を表示
//
//  uses に JwaPowrProf, JwaWinNT, Diagnostics,DateUtils が必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LSleepTime : Int64;
  LWakeTime  : Int64;
  LNowTime   : Int64;
  LDateTime  : TDateTime;
  LFormatStr : string;
begin
  Memo1.Lines.Clear;

  // 第 1 引数に取得する情報のタイプを指定して CallNtPowerInformation 関数を実行
  CallNtPowerInformation(LastSleepTime, nil, 0, @LSleepTime, SizeOf(LSleepTime));
  CallNtPowerInformation(LastWakeTime, nil, 0, @LWakeTime, SizeOf(LWakeTime));

  // 取得した値はシステムが起動してからの 100 ns 単位なので秒単位の値にする
  LSleepTime := LSleepTime div 10000000;
  LWakeTime  := LWakeTime div 10000000;
  LNowTime   := TStopWatch.GetTimeStamp div 10000000;

  LFormatStr := 'yyyy/mm/dd hh:nn:ss';

  // 最後にスリープ状態にした日時に換算
  LDateTime := IncSecond(Now, LSleepTime - LNowTime);
  Memo1.Lines.Add('LastSleepTime : ' + FormatDateTime(LFormatStr, LDateTime));

  // 最後にスリープから復帰した日時に換算
  LDateTime := IncSecond(Now, LWakeTime - LNowTime);
  Memo1.Lines.Add('LastWakeTime  : ' + FormatDateTime(LFormatStr, LDateTime));
end;

end.


WMI の Win32_PowerManagementEvent クラスを使用すると、スリープ開始とスリープからの復帰を監視して、イベントが発生した時の時間も取得できます。






09_システムのログオフと再起動とシャットダウン
plPrivilegeUnit.zip [1.3 KB] 2016年10月19日版


コンピュータの再起動やシャットダウン関係のサンプルです。ロック操作以外は ExitWindowsEx 関数を使用しています。
シャットダウンの操作には SE_SHUTDOWN_NAME の特権を有効にする必要があります。この特権の設定用に、DEKO さんの「ちっぷす」のサンプルコードを別ユニットにして利用しています。上のダウンロードのリンクからこのユニットファイルがダウンロードできます。




図20
システム再起動やシャットダウン関係のサンプル

リスト16
システムのログオフと再起動とシャットダウンのサンプル
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    Button5: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure Button5Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  plPrivilegeUnit;

{$R *.dfm}

//=============================================================================
//  コンピュータのロック
//  ログイン画面になる
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  LockWorkStation;
end;

//=============================================================================
//  ログオフする
//  EWX_FORCE は強制実行のフラグ
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  ExitWindowsEx(EWX_LOGOFF or EWX_FORCE, 0);
end;

//=============================================================================
//  再起動する
//  EWX_FORCE は強制実行のフラグ
//  実行には SE_SHUTDOWN_NAME の特権が必要
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
begin
  plPrivilegeUnit.SetPrivilege(SE_SHUTDOWN_NAME, True);
  try
    ExitWindowsEx(EWX_REBOOT or EWX_FORCE, 0);
  finally
    plPrivilegeUnit.SetPrivilege(SE_SHUTDOWN_NAME, False);
  end;
end;

//=============================================================================
//  シャットダウン
//  実行には SE_SHUTDOWN_NAME の特権が必要
//=============================================================================
procedure TForm1.Button4Click(Sender: TObject);
begin
  plPrivilegeUnit.SetPrivilege(SE_SHUTDOWN_NAME, True);
  try
    ExitWindowsEx(EWX_SHUTDOWN, 0);
  finally
    plPrivilegeUnit.SetPrivilege(SE_SHUTDOWN_NAME, False);
  end;
end;

//=============================================================================
//  強制シャットダウン
//  実行には SE_SHUTDOWN_NAME の特権が必要
//=============================================================================
procedure TForm1.Button5Click(Sender: TObject);
begin
  plPrivilegeUnit.SetPrivilege(SE_SHUTDOWN_NAME, True);
  try
    ExitWindowsEx(EWX_SHUTDOWN or EWX_FORCE, 0);
  finally
    plPrivilegeUnit.SetPrivilege(SE_SHUTDOWN_NAME, False);
  end;
end;

end.


シャットダウン関係の関数に InitiateSystemShutdown があります。この関数でシャットダウンを行うと下図のようなダイアログを表示してから指定時間後にシャットダウンします。
設定したシャットダウンまでの時間内に AbortSystemShutdown 関数を実行するとシャットダウンのキャンセルとなります。他の方法でシャットダウンする時は、この AbortSystemShutdown 関数でシャットダウンをキャンセルすることはできません。
ダイアログの [閉じる] ボタンは、関数の実行を中止してシャットダウンをキャンセルするわけではありません。単にダイアログを閉じて非表示にするだけです。

シャットダウン開始までの時間内であれば、他のアプリケーションの操作が可能です、他の方法によるシャットダウン操作も可能です。




図21
シャットダウンダイアログ
  • [閉じる] はシャットダウンのキャンセルではない

リスト17
InitiateSystemShutdown 関数を使用したシャットダウン
implementation

uses
  plPrivilegeUnit;

{$R *.dfm}

//=============================================================================
//  シャットダウンのダイアログを表示して指定時間(秒)後 にシャットダウン
//  uses に plPrivilegeUnit が必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  plPrivilegeUnit.SetPrivilege(SE_SHUTDOWN_NAME, True);
  InitiateSystemShutdown(nil, nil, 30, True, False);
end;

//=============================================================================
//  シャットダウンの中止
//
//  AbortSystemShutdown は InitiateSystemShutdown,InitiateSystemShutdownEx 
//  で開始したシャットダウンを中止する関数
//  他の方法で開始したシャットダウンはキャンセルできない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  AbortSystemShutdown(nil);
end;


RestartDialog 関数を使用すると再起動またはシャットダウンの確認ダイアログを表示できます。この関数の戻り値が IDNO の時は再起動もシャットダウンもしなかったことになります。




図22
再起動の確認ダイアログ

図23
シャットダウンの確認ダイアログ

リスト18
RestartDialog 関数を使用して再起動またはシャットダウンの確認ダイアログ表示
  function RestartDialog(AhWnd: HWND;
                         pszPromt: PChar;
                         dwReturn: DWORD): Integer; stdcall; external 'shell32';

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  再起動を確認するダイアログを表示
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LRet : Integer;
begin
  LRet := RestartDialog(Handle, '', EWX_REBOOT);
  if LRet = IDNO then begin
    // 後で再起動する場合
  end;
end;

//=============================================================================
//  シャットダウンを確認するダイアログを表示
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LRet : Integer;
begin
  LRet := RestartDialog(Handle, '', EWX_SHUTDOWN);
  if LRet = IDNO then begin
    // キャンセルした場合
  end;
end;




10_システムのログオフと再起動とシャットダウンの検出と阻止


システムのログオフ、再起動やシャットダウンが開始されると、システムは WM_QUERYENDSESSION メッセージを送出します。このメッセージを捕捉することでシャットダウン等を検出できます。
ネット上には WM_ENDSESSION メッセージで処理する例がありますが、Delphi には OnCloseQuery という便利なイベントがあります。シャットダウンということはアプリケーションが終了するわけですから、メインフォームの OnCloseQuery イベントでシャットダウン時の処理が可能です。

下のサンプルコードはシャットダウン時に何かの処理をする例です。作成した EXE を実行して、シャットダウンするとしばらくしてシャットダウンの画面になります。下図はシャットダウンの画面を [キャンセル] ボタンで閉じた結果です。シャットダウンの画面が表示されている間も待機中の原因である処理は実行されています。
システムのシャットダウンが開始されてから、本サンプルのシャットダウン時の処理が実行されるまではタイムラグがあります。この時間は Windows 7 では 5 秒です。

このサンプルは、シャットダウンの操作を検出した時だけ処理していますが、シャットダウン検出のフラグを使用しなければ、アプリケーションが終了する時に実行したい処理の例と言えます。もちろんシャットダウンの時にも実行される処理となります。




図24
シャットダウンを検出すると処理を開始

WM_QUERYENDSESSION メッセージの戻り値を 0 にした場合、Windows XP ではシャットダウンを中止することができました。戻り値を 1 にした場合は直ちにシャットダウンとなりました。これをサイレントシャットダウンと言います。

Windows Vista 以降では、シャットダウンの操作をした場合、実行中の未処理の処理があると、待機中のシャットダウン画面となります。これが Windows Vista 以降におけるシャットダウン阻止の状態ということになります。Windows XP 以前のサイレントシャットダウンは Windows Vista で廃止となりました。下図は Windows 7 の場合です。

強制シャットダウンの場合は、Windows XP 以前でも Windows Vista 以降であってもシャットダウンを阻止することはできません。

Windows Vista / 7 の場合、このシャットダウン画面では、処理中の処理が終了すれば自動的にシャットダウンとなります。[キャンセル] をクリックするとシャットダウン中止となります。



[備考 1]
Windows 8.1 と Windows 10 の場合、このサンプルは、シャットダウン画面で待機時間が 1 分経過すると自動的にデスクトップ画面に戻ります。デスクトップに戻った後にシャットダウンすることはありません。つまりシャットダウン画面で [キャンセル] ボタンをクリックした時と同じ結果となります。




図25
シャットダウン阻止

リスト19
システムのログオフと再起動とシャットダウンを検出してシャットダウンの待機
終了処理のコードがなければ直ちにシャットダウンとなる
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
  protected
    procedure WMQueryEndSession(var Message: TWMQueryEndSession);
      message WM_QUERYENDSESSION;
  private
    { Private 宣言 }
    FFlagEndSession : Boolean;
    FEndSession     : Cardinal;
  public
    { Public 宣言 }
  end;

  function ShutdownBlockReasonCreate(AhWnd: HWND; pwszReason: LPCWSTR): BOOL;
    stdcall; external 'user32';
  function ShutdownBlockReasonDestroy(AhWnd: HWND): BOOL; stdcall; external 'user32';

var
  Form1: TForm1;

implementation

uses
  MMSystem;

{$R *.dfm}

//=============================================================================
//  WM_QUERYENDSESSION メッセージの処理
//
//  ここには数秒以上の処理コードを書かないこと(マイクロソフトの記事による)
//  Delphi には OnCloseQuery という便利なイベントがある
//=============================================================================
procedure TForm1.WMQueryEndSession(var Message: TWMQueryEndSession);
begin
  // ログオフやシャットダウンの種類
  // 変数の型はCardinal(32 ビット値) とする
  // Cardinal 型の変数でないと ENDSESSION_XXX との比較が正しくできない
  FEndSession := Message.Unused;

  if (FEndSession = 0) then begin
    Message.Result := LRESULT(False);
  end;

  // このメッセージ受信のフラグ
  FFlagEndSession := True;

  // この inherited で OnCloseQuery イベントが実行される
  inherited;
end;

//=============================================================================
//  フォームの OnCloseQuery イベントの処理
//  シャットダウンとログオフの検出
//=============================================================================
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
var
  LTimeStart : Cardinal;
  LOldValue  : Integer;
  LNewValue  : Integer;
begin
  if FFlagEndSession then begin
    FFlagEndSession := false;
    // 0 の時はシャットダウンか再起動
    // シャットダウンと再起動の区別はできない
    if FEndSession = 0 then begin
      //
    end else
    // 強制シャットダウン
    if FEndSession = ENDSESSION_CRITICAL then begin
      //
    end else
    // ログオフ
    if FEndSession = ENDSESSION_LOGOFF then begin
      //
    end;

    //
    // ここか上のどこかに Windows が終了する時の処理を書く

    LTimeStart := timeGetTime;
    //指定時間経過したら終了
    LOldValue := 0;
    while True do begin
      LNewValue := (timeGetTime - LTimeStart) div 1000;
      if LOldValue < LNewValue then begin
        LOldValue := LNewValue;
        Memo1.Lines.Add(IntToStr(LNewValue));
      end;
      if LNewValue >= 90 then Break;

      Application.ProcessMessages;
      Sleep(1);
    end;
    //
    //
  end;

  if CanClose then begin
    //
    // フォームを閉じる時の処理コード
    //
  end;
end;

end.


この上の図のシャットダウン画面における待機中のプログラムの説明は「このプログラムにより、シャットダウンできません」となっています。これがデフォルトの表示です。
このテキストは ShutdownBlockReasonCreate 関数で変更できます。複数行の表示も可能です。
ShutdownBlockReasonDestroy 関数でデフォルトのテキストに戻ります。




図26
シャットダウン画面の待機中のプログラムの説明文を変更

    ShutdownBlockReasonCreate(Handle, 'Delphi 処理中');

    LTimeStart := timeGetTime;
    //指定時間経過したら終了
    LOldValue := 0;
    while True do begin
      LNewValue := (timeGetTime - LTimeStart) div 1000;
      if LOldValue < LNewValue then begin
        LOldValue := LNewValue;
        Memo1.Lines.Add(IntToStr(LNewValue));
      end;
      if LNewValue >= 90 then Break;

      Application.ProcessMessages;
      Sleep(1);
    end;

    ShutdownBlockReasonDestroy(Handle);


処理の途中でシャットダウン画面に表示するテキストを変更すれば、下図のように、処理の進捗状況を表示することもできます。
ただし、Windows 10 では表示の更新でチラつきが発生することがあります。




図27
シャットダウン画面に進捗状況を表示

    LTimeStart := timeGetTime;
    //指定時間経過したら終了
    LOldValue := 0;
    while True do begin
      LNewValue := (timeGetTime - LTimeStart) div 1000;
      if LOldValue < LNewValue then begin
        LOldValue := LNewValue;
        Memo1.Lines.Add(IntToStr(LNewValue));

        LText := '処理中 : ' + IntToStr(LNewValue) + ' / 90 秒経過';
        ShutdownBlockReasonCreate(Handle, PChar(LText));
      end;
      if LNewValue >= 90 then Break;

      Application.ProcessMessages;
      Sleep(1);
    end;

    ShutdownBlockReasonDestroy(Handle);


Windows Vista 以降では WM_QUERYENDSESSION メッセージの戻り値の設定に関係なく、シャットダウンの操作をした時に未処理の処理があればシャットダウン画面で待機中になります。
シャットダウン時には OnCloseQuery イベントが実行されますから、このイベントだけでシャットダウン時の処理を操作できることになります。上のサンプルはシャットダウン時に何かの処理を行う例ですが、以下のサンプルはシャットトダウン時に未処理の処理コードがある場合の例です。

[処理開始] ボタンで処理を開始します。処理が終了する前にシャットダウンの操作をするとシャットダウン画面で待機中になります。
そのまま放置しておくと、Windows 7 では、処理が終了すると自動的にシャットダウンします。
一方、Windows 8.1 / 10 では 1 分経過すると自動的にデスクトップ画面に戻り、処理が終了すればアプリが終了しますが、シャットダウンはしません。Windows 8.1 / 10 でも、処理が 1 分以内で終了すればシャットダウンします。

いずれにしても、シャットダウン画面で [強制的にシャットダウン] をクリックすれば処理の途中でもシャットダウンとなります。シャットダウン画面で [キャンセル] をクリックすれば、テスクトップ画面に戻りシャットダウンすることはありません。

Windows XP 以前用のアプリケーションでシャットダウン時の処理コードがある場合、そのアプリケーションを Windows Vista 以降で使用するには、処理コードの見直しが必要になるかも知れません。



[備考 2]
OnCloseQuery イベントの CanClose の値が False のままだと、強制的にシャットダウンするか、タスクマネージャーを使用しないとフォームを閉じることができなくなってしまいます。




図28
シャットダウン時に未処理の処理がある例
  • [処理開始] ボタンをクリック
  • 処理終了前にシャットダウンの操作をする
  • シャットダウン画面で待機中になる
  • シャットダウン画面表示後の動作は Windows 7 と Windows 8.1 / 10 で異なることがある
    (未処理の程度による)


リスト20
シャットダウン時に未処理の処理を実行する例
単なるアプリ終了とシャットダウンを区別するには WM_QUERYENDSESSION メッセージの処理が必要
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    Button2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
    FCancelFlag : Boolean;
    FCloseEvent : Boolean;
  public
    { Public 宣言 }
  end;

  function ShutdownBlockReasonCreate(AhWnd: HWND; pwszReason: LPCWSTR): BOOL;
    stdcall; external 'user32';
  function ShutdownBlockReasonDestroy(AhWnd: HWND): BOOL; stdcall; external 'user32';

var
  Form1: TForm1;

implementation

uses
  MMSystem;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  // 初期状態ではフォームを閉じる設定
  FCancelFlag := True;
  // フォームを閉じる時の処理の有無(初期状態ではなし)
  // 時間のかかる処理を実行する前にこのフラグを True にしておく
  FCloseEvent := False;
end;

//=============================================================================
//  フォームの OnCloseQuery イベントの処理
//  システムのシャットダウン時にはメインフォームが閉じる
//  フォームが閉じる時はこのイベントが実行される
//=============================================================================
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  // このイベント通過フラグ
  FCloseEvent := True;
  // 時間のかかる処理が実行中の時は FCloseFlag は Falseになっている
  CanClose := FCancelFlag;

  if CanClose then begin
    // フォームを閉じる時のその他の処理
  end;
end;

//=============================================================================
//  [処理開始] ボタン
//  時間のかかる処理の例としてのコード
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LTimeStart : Cardinal;
  LOldValue  : Integer;
  LNewValue  : Integer;
begin
  Memo1.Lines.Clear;

  // フォームを閉じないようにフラグを False にする
  FCancelFlag := False;

  // シャットダウン画面に表示するテキスト
  ShutdownBlockReasonCreate(Handle, '処理中です。お待ちください');

  LTimeStart := timeGetTime;
  //指定時間経過したら終了
  LOldValue := 0;
  while True do begin
    LNewValue := (timeGetTime - LTimeStart) div 1000;
    if LOldValue < LNewValue then begin
      LOldValue := LNewValue;
      Memo1.Lines.Add(IntToStr(LNewValue));
    end;

    if LNewValue >= 90 then Break;
    // [処理中止] ボタンをクリックした時の処理
    if FCancelFlag then Break;

    Application.ProcessMessages;
    Sleep(1);
  end;

  // シャットダウン画面に表示するテキストをデフォルトに戻す
  ShutdownBlockReasonDestroy(Handle);

  // フォームを閉じても OK のフラグをセット
  FCancelFlag := True;
  // 上のコードを実行中に OnCloseQuery イベントが発生した場合
  // FCancelFlag を True にして再度 Close すればフォームが閉じる
  // 処理終了後またはキャンセル時にフォームを閉じる必要がなければこのコードは不要
  if FCloseEvent then Close;
end;

//=============================================================================
//  [処理中止] ボタン
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  // 処理のキャンセルフラグをセット
  FCancelFlag := True;
end;

end.


[備考 3]
未処理の処理がある場合、シャットダウンの画面が表示されるまでの時間、あるいはシャットダウン画面の待機時間等は、レジストリの操作で変更が可能かも知れません。
WaitToKillServiceTimeout, HungAppTimeout, WaitToKillAppTimeout 等をキーワードにして調べてみてください。それらはグループポリシーの設定からも変更できるようです。
組織内のコンピュータの運用では、電源操作関係の設定変更が必要なことがあります。そのような場合に利用できるかも知れません。


[備考 4]
何かの処理中に間違ってシャットダウンの操作租をしてしまった場合は当然、シャットダウン画面で [キャンセル] をクリックすることになると思います。

シャットダウン時に何かの処理を実行したい場合、シャットダウンのメッセージを検出してから処理を開始するのではなく、必要な処理が終了したら自アプリからシャットダウンを実行するという動作も選択の余地があると思われます。どうせシャットダウンするのですから。
これは、本当にシャットダウンを検知してから実行しなければならない処理なのか、という課題ということになります。





11_モニタの電源 OFF までの時間とスリープ開始時間の変更


コマンドラインツールの powercfg.exe で CHANGE (省略形は /X ) オプションを使用すると、モニタの電源を切るまでの時間やシステムがスリープ状態になるまでの時間を変更できます。また、設定時間を 0 にすると、それらを無効にすることもできます。
CHANGE オプションには下表のパラメータがあります。これらを使用したサンプルです。

ノート型の PC 等では、メーカ独自の電源管理用のソフトウェアが実装されていて、独自に管理している場合があります。そのような場合は以下のコマンドでの設定が無効となることがあります。



CHANGE オプションのパラメータ 機能
monitor-timeout-ac 数値 モニタの電源を切るまでの時間を分単位で指定
AC 電源動作時用
monitor-timeout-dc 数値 モニタの電源を切るまでの時間を分単位で指定
DC 電源動作時用
disk-timeout-ac 数値 ハードディスクの電源を切るまでの時間を分単位で指定
AC 電源動作時用
disk-timeout-dc 数値 ハードディスクの電源を切るまでの時間を分単位で指定
DC 電源動作時用
standby-timeout-ac 数値 スタンバイ状態にするまでの時間を分単位で指定
AC 電源動作時用
standby-timeout-dc 数値 スタンバイ状態にするまでの時間を分単位で指定
DC 電源動作時用
hibernate-timeout-ac 数値 休止 (ハイバーネーション) 状態にするまでの時間を分単位で指定
AC 電源動作時用
hibernate-timeout-dc 数値 休止 (ハイバーネーション) 状態にするまでの時間を分単位で指定
DC 電源動作時用



図29
モニタの電源 OFF までの時間とシステがスリープするまでの時間の設定

リスト21
モニタの電源 OFF までの時間とシステがスリープするまでの時間設定のサンプル
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button5: TButton;
    Button3: TButton;
    Button4: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure Button5Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  ShellAPI;

{$R *.dfm}

//=============================================================================
//  モニタ(ディスプレイ)の電源を切るまでの時間を設定
//
//  uses にShellAPI が必要 
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LCmdExe : string;
  LOption : string;
begin
  LCmdExe := 'powercfg.exe';
  LOption := '/CHANGE monitor-timeout-ac 5';
  ShellExecute(Handle, '', PChar(LCmdExe), PChar(LOption), nil, SW_HIDE);
end;

//=============================================================================
//  システムがスリープ(スタンバイ)状態になるまでの時間を設定
//
//  uses にShellAPI が必要 
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LCmdExe : string;
  LOption : string;
begin
  LCmdExe := 'powercfg.exe';
  LOption := '/CHANGE standby-timeout-ac 20';
  ShellExecute(Handle, '', PChar(LCmdExe), PChar(LOption), nil, SW_HIDE);
end;

//=============================================================================
//  モニタ(ディスプレイ)の電源を切らない設定
//  タイムアウト時間を 0 にすれば無効となる
//  電源オプションの設定ダイアログでは表示が [なし] になる
//
//  uses にShellAPI が必要 
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LCmdExe : string;
  LOption : string;
begin
  LCmdExe := 'powercfg.exe';
  LOption := '/CHANGE monitor-timeout-ac 0';
  ShellExecute(Handle, '', PChar(LCmdExe), PChar(LOption), nil, SW_HIDE);
end;

//=============================================================================
//  モニタの電源を切るまでの時間とシステムがスリープ状態になるまでの時間を設定
//
//  uses にShellAPI が必要 
//=============================================================================
procedure TForm1.Button4Click(Sender: TObject);
var
  LCmdExe : string;
  LOption : string;
begin
  LCmdExe := 'powercfg.exe';
  LOption := '/CHANGE monitor-timeout-ac 20';
  ShellExecute(Handle, '', PChar(LCmdExe), PChar(LOption), nil, SW_HIDE);

  LOption := '/CHANGE standby-timeout-ac 60';
  ShellExecute(Handle, '', PChar(LCmdExe), PChar(LOption), nil, SW_HIDE);
end;

//=============================================================================
//  電源オプションの設定ダイアログを表示
//  設定した内容の確認用
//
//  uses にShellAPI が必要 
//=============================================================================
procedure TForm1.Button5Click(Sender: TObject);
var
  LCmdExe : string;
  LOption : string;
begin
  LCmdExe := 'control.exe';
  LOption := '/name Microsoft.PowerOptions /page pagePlanSettings';
  ShellExecute(Handle, '', PChar(LCmdExe), PChar(LOption), nil, SW_SHOW);
end;


end.




12_電源管理用の関数を使用した設定値の取得と変更


電源管理関係の Windows API 関数を使用すると、モニタの電源 OFF までの時間やシステムがスリープ状態になるまでの時間の取得や変更ができます。

下図は powercfg.exe の /L オプションで電源設定を取得した結果です。いわゆる電源プランと呼ばれるものです。下図では 3 つだけですが、これ以外にもオリジナルの電源プランを作成するとその電源ンプランも表示されます。
各々の電源プランには GUID 値が割り当てられています。Windows API の関数で電源管理関係の設定内容を取得したり変更するにはこの GUID の値を使用します。




図30
電源プラン

上の結果で、現在アクティブな電源プランの GUID 値を指定して powercfg.exe の /Q オプションで設定内容を取得した結果の一部が下の図です。
各々の電源プラン対して、その階層下にサブグループの GUID があり、そのサブグループの階層下に具体的な設定項目の GUID がある階層構造になっています。各々の電源プラン対して、このサブグループの GUID とその階層下の GUID が存在します。このリスト内の「現在の XXX のインデックス」が具体的な設定値です。

電源管理関係の関数を使用する時は、この GUID 値を使用します。
例えば、システムがスリープするまでの時間は、下図の赤い下線の GUID 値を使用して、赤い枠線の中の値を取得します。赤い枠線の中の値は $E10 ですから 3600 秒 ( 1 時間) です。この値を 0 にすると、スリープの機能を無効にすることになります。




図31

以下は PowerReadACValueIndex 関数と PowerWriteACValueIndex 関数を使用したサンプルです。ディスプレイの電源を切断するまでのタイムアウトの値と、電源をスリープ状態にするまでのタイムアウトの値を取得または変更します。
このサンプルで使用している関数以外にも、GUID の説明文や「利用可能な設定...」の値の取得と変更用の関数もあります。

ノート型の PC 等では、メーカ独自の電源管理用のソフトウェア、あるいはファームウェアとして実装されていて、独自に管理している場合があります。そのような PC では、このサンプルでの取得値や設定値が意味のない値となることがあります。

このサンプルコードで使用している関数の一部は Windows XP 以前には実装されていません。




図32
モニタの電源 OFF までの時間とシステムがスリープするまての時間の取得と変更

リスト22
モニタの電源 OFF までの時間とシステムがスリープするまての時間の取得と変更
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
    procedure GetTimeOutValue(var AMonitorTime: DWORD; var ASystemTime: DWORD);
    procedure SetTimeOutValue(AMonitorTime: DWORD; ASystemTime: DWORD);
  public
    { Public 宣言 }
  end;

  function PowerGetActiveScheme(UserRootPowerKey : HKEY;
                                var ActivePolicyGuid : PGUID): DWORD;
                                stdcall; external 'PowrProf.dll';

  function PowerReadACValueIndex(RootPowerKey : HKEY;
                                 const SchemeGuid : PGUID;
                                 const SubGroupOfPowerSettingsGuid : PGUID;
                                 const PowerSettingGuid : PGUID;
                                 AcValueIndex : LPDWORD): DWORD;
                                 stdcall; external 'powrprof.dll';

  function PowerWriteACValueIndex(RootPowerKey : HKEY;
                                  const SchemeGuid : PGUID;
                                  const SubGroupOfPowerSettingsGuid : PGUID;
                                  const PowerSettingGuid : PGUID;
                                  AcValueIndex : DWORD): DWORD;
                                  stdcall; external 'powrprof.dll'

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  // 操作対象の GUID 値に定数名を設定
  // 設定する名前は任意(MSDNの記事には定数名の記述がある)
  GUID_SUB_DISPLAY       : TGUID = '{7516b95f-f776-4464-8c53-06167f40cc99}';
  GUID_SUB_DISPLAY_POWER : TGUID = '{3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e}';
  GUID_SUB_SLEEP         : TGUID = '{238c9fa8-0aad-41ed-83f4-97be242c8f20}';
  GUID_SUB_SLEEP_TIMEOUT : TGUID = '{29f6c1db-86da-48c5-9fdb-f2b67b1f44da}';

//=============================================================================
//  現在の電源関係の設定値を取得
//  ここではモニタの電源 OFF とシステムのスリープ開始までのタイムアウトの時間
//  時間の単位は秒
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LTime1 : DWORD;
  LTime2 : DWORD;
begin
  GetTimeOutValue(LTime1, LTime2);

  Memo1.Lines.Clear;
  Memo1.Lines.Add(IntToStr(LTime1) + ' sec ($' + IntToHex(LTime1, 8) + ')');
  Memo1.Lines.Add(IntToStr(LTime2) + ' sec ($' + IntToHex(LTime2, 8) + ')');
end;

//=============================================================================
//  電源関係の値を設定
//  ここではモニタの電源 OFF とシステムのスリープ開始までのタイムアウトの時間
//  時間の単位は秒
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  SetTimeOutValue(10 * 60, 120 * 60);

  // 確認のため設定値を取得して表示
  Button1Click(nil);
end;

//-----------------------------------------------------------------------------
//  モニタの電源 OFF とシステムのスリープ開始までの時間を取得
//
//  PowerGetActiveScheme 関数の引数 (AC は AC 電源用)
//  第 1 引数 : 常に 0
//  第 2 引数 : 取得対象の電源設定の GUID 値
//  第 3 引数 : 電源設定 GUID のサブグループの GUID 値
//  第 4 引数 : サブグループ階層下の取得対象の GUID 値
//  第 5 引数 : 取得するインデックスの値を格納する変数
//              4 バイトの符号なし整数変数のポインタ
//-----------------------------------------------------------------------------
procedure TForm1.GetTimeOutValue(var AMonitorTime: DWORD; var ASystemTime: DWORD);
var
  LpActiveGuid : PGUID;
  LTimeOut     : DWORD;
begin
  AMonitorTime := 0;
  ASystemTime  := 0;

  LpActiveGuid := nil;
  try
    // 現在アクティブな電源設定の GUID 値を取得
    // 別の電源プランの設定値を取得する場合はこの GUID の値を変える
    if PowerGetActiveScheme(0, LpActiveGuid) <> 0 then Exit;

    // ディスプレイの電源を切るまでの時間を取得(単位は秒)
    if PowerReadACValueIndex(0,
                             LpActiveGuid,
                             @GUID_SUB_DISPLAY,
                             @GUID_SUB_DISPLAY_POWER,
                             @LTimeOut) = ERROR_SUCCESS then begin
      AMonitorTime := LTimeOut;
    end else begin
      AMonitorTime := 0;
    end;

    // システムをスリープ状態にするまでの時間を取得(単位は秒)
    if PowerReadACValueIndex(0,
                             LpActiveGuid,
                             @GUID_SUB_SLEEP,
                             @GUID_SUB_SLEEP_TIMEOUT,
                             @LTimeOut) = ERROR_SUCCESS then begin
      ASystemTime := LTimeOut;
    end else begin
      ASystemTime := 0;
    end;
  finally
    LocalFree(HLOCAL(LpActiveGuid));
  end;
end;

//-----------------------------------------------------------------------------
//  モニタの電源 OFF とシステムのスリープ開始までの時間を設定
//
//  PowerWriteACValueIndex 関数の引数 (AC は AC 電源用)
//  第 1 引数 : 常に 0
//  第 2 引数 : 設定対象の電源設定の GUID 値
//  第 3 引数 : 電源設定 GUID のサブグループの GUID 値
//  第 4 引数 : サブグループ階層下の設定対象の GUID 値
//  第 5 引数 : 設定するインデックスの値
//              4 バイトの符号なし整数変数
//-----------------------------------------------------------------------------
procedure TForm1.SetTimeOutValue(AMonitorTime, ASystemTime: DWORD);
var
  LpActiveGuid : PGUID;
begin
  LpActiveGuid := nil;
  try
    // 現在アクティブな電源設定の GUID 値を取得
    // 別の電源プランの設定値を変更する場合はこの GUID の値を変える
    if PowerGetActiveScheme(0, LpActiveGuid) <> 0 then Exit;

    // ディスプレイの電源を切るまでの時間を設定(単位は秒)
    PowerWriteACValueIndex(0,
                           LpActiveGuid,
                           @GUID_SUB_DISPLAY,
                           @GUID_SUB_DISPLAY_POWER,
                           AMonitorTime);

    // システムをスリープ状態にするまでの時間を設定(単位は秒)
    PowerWriteACValueIndex(0,
                           LpActiveGuid,
                           @GUID_SUB_SLEEP,
                           @GUID_SUB_SLEEP_TIMEOUT,
                           ASystemTime);
  finally
    LocalFree(HLOCAL(LpActiveGuid));
  end;
end;

end.


上のコードで使用している関数名には「AC」という文字があります。これは AC 電源で動作している時の設定値の取得と変更用の関数です。DC 電源動作時の設定値の取得と変更には、関数名に「DC」がある関数を使用します。
システムが AC 電源で動作しているか、DC 電源で動作しているかは、GetSystemPowerStatus 関数を使用すると判定できます。この関数の引数は SYSTEM_POWER_STATUS 構造体です。この構造体は、Delphi では TSystemPowerStatus というレコード型として定義されています。



procedure TForm1.Button1Click(Sender: TObject);
var
  LPwrStatus   : TSystemPowerStatus;
  LAcLinStatus : Byte;
begin
  GetSystemPowerStatus(LPwrStatus);
  LAcLinStatus := LPwrStatus.ACLineStatus;

  if LAcLinStatus = 0 then begin
    // AC 電源オフライン(バッテリー動作)
  end else
  if LAcLinStatus = 1 then begin
    // AC 電源オンライン
  end else begin
    // AC 電源の状態は不明
  end;
end;


以下のコマンドを実行すると下図の [電源オプション] [詳細設定] のダイアログが表示されます。休止状態等、機能が無効になっている項目は、このダイアログに該当項目は表示されません。

上のサンプルで使用した関数を使用すると、この [電源オプション] [詳細設定] のダイアログの設定項目のほとんど全ての設定値を取得または変更可能でず。これらの設定値には、AC 電源での動作時用と DC 電源での動作時用の両方の値があります。設定項目によっては、管理者権限でないと設定できない内容があります。
図の下のリストは powercfg.exe /Q コマンドで取得した内容です。参考として掲載しておきます。


%SystemRoot%\System32\control.exe powercfg.cpl,,3


図33
電源オプションの詳細設定
  • これらの項目は、電源管理用の Windows API の関数を使用して取得または変更可能

リスト23
powercfg.exe /Q 電源プランの GUID で設定内容をリストした結果 (全て)
これらの多くは上の図の [電源オプション] のダイアログで設定可能
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

d:\>powercfg.exe /Q 381b4222-f694-41f0-9685-ff5bb260df2e
電源設定の GUID: 381b4222-f694-41f0-9685-ff5bb260df2e  (バランス)
  サブグループの GUID: fea3413e-7e05-4911-9a71-700331f1c294  (どのサブグループに
も属さない設定)
    電源設定の GUID: 0e796bdb-100d-47d6-a2d5-f7d2daa51f51  (復帰時のパスワードを
必要とする)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: いいえ
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: はい
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000001

  サブグループの GUID: 0012ee47-9041-4b5d-9b77-535fba8b1442  (ハード ディスク)
    電源設定の GUID: 6738e2c4-e8a5-4a42-b16a-e040e769756e  (次の時間が経過後ハー
ド ディスクの電源を切る)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0xffffffff
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: 秒
    現在の AC 電源設定のインデックス: 0x000004b0
    現在の DC 電源設定のインデックス: 0x00000258

  サブグループの GUID: 02f815b5-a5cf-4c84-bf20-649d1f75d3d8  (Internet Explorer)

    電源設定の GUID: 4c793e7d-a264-42e1-87d3-7a0d2f523ccd  (JavaScript タイマー
の間隔)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 最大限の省電力
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: 最大パフォーマンス
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000000

  サブグループの GUID: 0d7dbae2-4294-402a-ba8e-26777e8488cd  (デスクトップの背景
の設定)
    電源設定の GUID: 309dce9b-bef4-4119-9921-a851fb12f0f4  (スライド ショー)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 有効
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: 一時停止
    現在の AC 電源設定のインデックス: 0x00000000
    現在の DC 電源設定のインデックス: 0x00000001

  サブグループの GUID: 19cbb8fa-5279-450e-9fac-8a3d5fedd0c1  (ワイヤレス アダプ
ターの設定)
    電源設定の GUID: 12bbebe6-58d6-4636-95bb-3217ef867c1a  (省電力モード)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 最大パフォーマンス
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: 省電力 (低)
      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: 省電力 (中)
      利用可能な設定のインデックス: 003
      利用可能な設定のフレンドリ名: 省電力 (高)
    現在の AC 電源設定のインデックス: 0x00000000
    現在の DC 電源設定のインデックス: 0x00000002

  サブグループの GUID: 238c9fa8-0aad-41ed-83f4-97be242c8f20  (スリープ)
    電源設定の GUID: 29f6c1db-86da-48c5-9fdb-f2b67b1f44da  (次の時間が経過後スリ
ープする)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0xffffffff
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: 秒
    現在の AC 電源設定のインデックス: 0x00000e10
    現在の DC 電源設定のインデックス: 0x00000384

    電源設定の GUID: 94ac6d29-73ce-41a6-809f-6363ba21b47e  (ハイブリッド スリー
プを許可する)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: オフ
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: オン
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000001

    電源設定の GUID: 9d7815a6-7ee4-497e-8888-515a05f02364  (次の時間が経過後休止
状態にする)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0xffffffff
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: 秒
    現在の AC 電源設定のインデックス: 0x00000000
    現在の DC 電源設定のインデックス: 0x00000000

    電源設定の GUID: bd3b718a-0680-4d9d-8ab2-e1d2b4ac806d  (スリープ解除タイマー
の許可)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 無効
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: 有効
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000001

  サブグループの GUID: 2a737441-1930-4402-8d77-b2bebba308a3  (USB 設定)
    電源設定の GUID: 48e6b7a6-50f5-4782-a5d4-53bb8f07e226  (USB のセレクティブ
サスペンドの設定)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 無効
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: 有効
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000001

  サブグループの GUID: 4f971e89-eebd-4455-a8de-9e59040e7347  (電源ボタンとカバー
)
    電源設定の GUID: 5ca83367-6e45-459f-a27b-476b1d01c936  (カバーを閉じたときの
操作)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 何もしない
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: スリープ
      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: 休止状態
      利用可能な設定のインデックス: 003
      利用可能な設定のフレンドリ名: シャットダウン
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000001

    電源設定の GUID: 7648efa3-dd9c-4e3e-b566-50f929386280  (電源ボタンの操作)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 何もしない
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: スリープ
      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: 休止状態
      利用可能な設定のインデックス: 003
      利用可能な設定のフレンドリ名: シャットダウン
    現在の AC 電源設定のインデックス: 0x00000003
    現在の DC 電源設定のインデックス: 0x00000003

    電源設定の GUID: 96996bc0-ad50-47ec-923b-6f41874dd9eb  (スリープ ボタンの操
作)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 何もしない
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: スリープ
      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: 休止状態
      利用可能な設定のインデックス: 003
      利用可能な設定のフレンドリ名: シャットダウン
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000001

    電源設定の GUID: a7066653-8d6c-40a8-910e-a1f54b84c7e5  ([スタート] メニュー
の電源ボタンの操作)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: スリープ
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: 休止状態
      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: シャットダウン
    現在の AC 電源設定のインデックス: 0x00000000
    現在の DC 電源設定のインデックス: 0x00000000

  サブグループの GUID: 501a4d13-42af-4429-9fd1-a8218c268e20  (PCI Express)
    電源設定の GUID: ee12f906-d277-404b-b6da-e5fa1a576df5  (リンク状態の電源管理
)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: オフ
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: 適切な省電力
      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: 最大限の省電力
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000002

  サブグループの GUID: 528259f7-7bae-4f30-8321-8afa6e155c4c  (GPU 設定)
  サブグループの GUID: 54533251-82be-4824-96c1-47b60b740d00  (プロセッサの電源管
理)
    電源設定の GUID: 893dee8e-2bef-41e0-89c6-b55d0929964c  (最小のプロセッサの状
態)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0x00000064
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: %
    現在の AC 電源設定のインデックス: 0x00000005
    現在の DC 電源設定のインデックス: 0x00000005

    電源設定の GUID: 94d3a615-a899-4ac5-ae2b-e4d8f634367f  (システムの冷却ポリシ
ー)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: パッシブ
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: アクティブ
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000000

    電源設定の GUID: bc5038f7-23e0-4960-96da-33abaf5935ec  (最大のプロセッサの状
態)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0x00000064
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: %
    現在の AC 電源設定のインデックス: 0x00000064
    現在の DC 電源設定のインデックス: 0x00000064

  サブグループの GUID: 7516b95f-f776-4464-8c53-06167f40cc99  (ディスプレイ)
    電源設定の GUID: 17aaa29b-8b43-4b94-aafe-35f64daaf1ee  (次の時間の経過後、デ
ィスプレイを暗くする)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0xffffffff
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: 秒
    現在の AC 電源設定のインデックス: 0x0000012c
    現在の DC 電源設定のインデックス: 0x00000078

    電源設定の GUID: 3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e  (次の時間が経過後ディ
スプレイの電源を切る)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0xffffffff
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: 秒
    現在の AC 電源設定のインデックス: 0x000004b0
    現在の DC 電源設定のインデックス: 0x0000012c

    電源設定の GUID: aded5e82-b909-4619-9949-f5d71dac0bcb  (ディスプレイの明るさ
)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0x00000064
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: %
    現在の AC 電源設定のインデックス: 0x00000064
    現在の DC 電源設定のインデックス: 0x00000028

    電源設定の GUID: f1fbfde2-a960-4165-9f88-50667911ce96  (ディスプレイ暗転時の
明るさ)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0x00000064
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: %
    現在の AC 電源設定のインデックス: 0x0000001e
    現在の DC 電源設定のインデックス: 0x0000001e

  サブグループの GUID: 9596fb26-9850-41fd-ac3e-f7c3c00afd4b  (マルチメディアの設
定)
    電源設定の GUID: 03680956-93bc-4294-bba6-4e0f09bb717f  (メディアを共有するタ
イミング)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: コンピューターのスリープを許可する
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: アイドリングがスリープ状態になるのを回避する

      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: コンピューターが退席中モードになるのを許可す

    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000000

    電源設定の GUID: 34c7b99f-9a6d-4b3c-8dc7-b6693b78cef4  (ビデオの再生時)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: ビデオ品質の最適化
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: バランス
      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: 省電力の最適化
    現在の AC 電源設定のインデックス: 0x00000000
    現在の DC 電源設定のインデックス: 0x00000001

  サブグループの GUID: e73a048d-bf27-4f12-9731-8b2076e8891f  (バッテリ)
    電源設定の GUID: 637ea02f-bbcb-4015-8e2c-a1c7b9c0b546  (バッテリ切れの操作)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 何もしない
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: スリープ
      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: 休止状態
      利用可能な設定のインデックス: 003
      利用可能な設定のフレンドリ名: シャットダウン
    現在の AC 電源設定のインデックス: 0x00000000
    現在の DC 電源設定のインデックス: 0x00000002

    電源設定の GUID: 8183ba9a-e910-48da-8769-14ae6dc1170a  (バッテリ低下のレベル
)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0x00000064
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: %
    現在の AC 電源設定のインデックス: 0x0000000a
    現在の DC 電源設定のインデックス: 0x0000000a

    電源設定の GUID: 9a66d8d7-4ff7-4ef9-b5a2-5a326ca2a469  (バッテリ切れのレベル
)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0x00000064
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: %
    現在の AC 電源設定のインデックス: 0x00000005
    現在の DC 電源設定のインデックス: 0x00000005

    電源設定の GUID: bcded951-187b-4d05-bccc-f7e51960c258  (バッテリ低下の通知)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: オフ
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: オン
    現在の AC 電源設定のインデックス: 0x00000001
    現在の DC 電源設定のインデックス: 0x00000001

    電源設定の GUID: d8742dcb-3e6a-4b3c-b3fe-374623cdcf06  (バッテリ低下の操作)
      利用可能な設定のインデックス: 000
      利用可能な設定のフレンドリ名: 何もしない
      利用可能な設定のインデックス: 001
      利用可能な設定のフレンドリ名: スリープ
      利用可能な設定のインデックス: 002
      利用可能な設定のフレンドリ名: 休止状態
      利用可能な設定のインデックス: 003
      利用可能な設定のフレンドリ名: シャットダウン
    現在の AC 電源設定のインデックス: 0x00000000
    現在の DC 電源設定のインデックス: 0x00000000

    電源設定の GUID: f3c5027d-cd16-4930-aa6b-90db844a8f00  (省電源移行バッテリ
レベル)
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0x00000064
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: %
    現在の AC 電源設定のインデックス: 0x00000007
    現在の DC 電源設定のインデックス: 0x00000007


d:\>




13_スリープ状態から指定時刻に自動復帰する ( レジューム ) 機能


待機可能タイマー関係の関数、CreateWaitableTimer, SetWaitableTimer を使用して、スリープ状態のコンピュータを自動で復帰させる、いわゆるレジューム機能のサンプルです。
この自動復帰の概要と処理の流れについては以下の記事を参考にしてください。



以下が実際のサンプルコードと実行結果です。
このサンプルでは、待機タイマーを起動してから 3 分後に自動復帰するようにしています。[タイマーセット] ボタンをクリックしたらコンピュータをスリープ状態にします。このサンプルプログラムは起動しておく必要がありますが、アクティブにしておく必要はありません。

自動復帰をした後は、システムがスリープ状態になるまでのタイムアウト時間が経過してもシステムはスリープ状態になりません。本サンプルを終了すれば、スリープ開始までのタイムアウトの時間設定が有効になます。必要であれば、自動復帰後に何かの作業を実行して、その作業終了後に強制的にスリープ状態にすることも考えられます。
モニタの電源 OFF までの時間は有効のままにしています。

下の最初の図は、本サンプルの [スリープ開始] でスリープして自動復帰した場合です。待機タイマーで設定した復帰の時刻と実際に復帰した時刻に少しタイムラグがあります。その下の図は [スタート] [シャットダウン] のメニュー [スリープ] でスリープして自動復帰した場合です。待機タイマーで設定した復帰時刻と実際の復帰時刻にほとんど差がありません。
休止状態から自動復帰した場合は 30 秒以上のタイムラグがあります。



[備考]
[電源オプション] [詳細設定] の [スリープ] 階層下の [スリープ解除タイマーの使用] が無効になっていると自動復帰はできません。
この設定値は、前項のサンプルコードの手順で取得、変更できます。




図34
本サンプルの [スリープ開始] でスリープして自動復帰した場合
  • 最初は設定した自動復帰の時刻
  • 次は実際に自動復帰した時刻
  • 設定した復帰時刻と実際の復帰時刻に十数秒のタイムラグがある

図35
[スタート] [シャットダウン] の [スリープ] でスリープして自動復帰した場合
  • 設定した復帰時刻と実際の復帰時刻にタイムラグはほとんどない

図36
[スタート] [シャットダウン] の [休止状態] でスリープして自動復帰した場合
  • 実際にシステムが稼働状態になるまでは時間がかかる

リスト24
スリープ状態から指定時刻に自動復帰させるサンプル
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

const
  WM_RESUME_NOTIFY = WM_APP + 10;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Button2: TButton;
    Button3: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  protected
    procedure WMResumeNotiry(var Message: TMessage); message WM_RESUME_NOTIFY;
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

  procedure SetSystemWakeUpTimer(AHandle: THandle; ADateTime: TDateTime);
  procedure CancelSystemWakeUpTimer;
  function TDateTimeToUTCFileTime(ADateTime: TDateTime): Int64;

  function SetThreadExecutionState(esFlags: DWORD): DWORD; stdcall external 'kernel32.dll';

  function SetSuspendState(bHibernate: Boolean;
                         bForce: Boolean;
                         bWakeupEventsDisabled: Boolean): Boolean;
                         stdcall external 'PowrProf.dll';

var
  Form1: TForm1;

implementation

uses
  DateUtils;

{$R *.dfm}

const
  ES_SYSTEM_REQUIRED  = $00000001;
  ES_DISPLAY_REQUIRED = $00000002;
  ES_CONTINUOUS       = $80000000;

var
  FhWaitTimer  : THandle;
  FTimerThread : TThread;
  FFlagCancel  : Boolean;

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  // 待機可能タイマー関係変数の初期化
  // 実際にはグローバル変数なので以下の値が初期値となっている
  FhWaitTimer  := 0;
  FTimerThread := nil;
  FFlagCancel  := False;
end;

//=============================================================================
//  フォーム破棄時の処理
//=============================================================================
procedure TForm1.FormDestroy(Sender: TObject);
begin
  // 待機タイマーを無効にする
  CancelSystemWakeUpTimer;
end;

//=============================================================================
//  待機タイマーのセット
//  スリープ状態からの自動復帰の日時を設定
//
//  テストで日時を指定する場合,以下のようなコードでも指定可能
//  LDateTime := StrToDateTime('2019/01/08 07:00:00');
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LDateTime : TDateTime;
begin
  Memo1.Lines.Clear;

  // スリープからの自動復帰(レジューム)予定日時
  // 今の日時から指定時間後(単位は分)とする例
  LDateTime := IncMinute(Now, 3);

  // スリープからの自動復帰(レジューム)の時刻を表示
  Memo1.Lines.Add(FormatDateTime('yyyy/mm/dd hh:nn:ss', LDateTime));

  // 待機タイマーを起動
  SetSystemWakeUpTimer(Self.Handle, LDateTime);
end;

//=============================================================================
//  待機タイマーの処理を無効にする
//  タイマーをセットしたが中止したい場合
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  CancelSystemWakeUpTimer;
end;

//=============================================================================
//  スリープ開始
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
begin
  SetSuspendState(False, False, False);
end;

//-----------------------------------------------------------------------------
//  待機タイマーのスレッドが終了するとこのメッセージが送信されてくる
//  (システムがスリープ状態であるかとは関係なく)
//
//  スリープから復帰してからこのメッセージが処理されるまでは少しタイムラグがある
//  本サンプルでスリープ状態にした場合は 10 ~ 15 秒程度
//  [シャットダウン],他のプログラムからスリープ状態にした場合は 0 ~ 2 秒
//-----------------------------------------------------------------------------
procedure TForm1.WMResumeNotiry(var Message: TMessage);
begin
  if Message.LParam = 1 then begin

    // ここにスリープから自動復帰した時の処理を書く
    // 本サンプルでは以下の 2 行だけ

    // モニタの電源をON (モニタの電源を ON にする必要がなければ不要)
    PostMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, -1);

    Memo1.Lines.Add(FormatDateTime('yyyy/mm/dd hh:nn:ss', Now));
  end else begin
    Memo1.Lines.Add('Cancel');
  end;
end;

//-----------------------------------------------------------------------------
//  待機タイマーの生成とスリープからの自動復帰の日時のセット
// 
//  AHandle   : 指定した日時にメッセージを送信するウィンドウのハンドル
//  ADateTime : 自動復帰の日時.この日時になるとメッセージを送信する
//
//  以下のグローバル変数を使用
//  FhWaitTimer  : 待機タイマーのハンドル
//  FTimerThread : タイマー用のスレッドオブジェクト
//  FFlagCancel  : 待機タイマーを無効(リセット)する時のフラグ
//-----------------------------------------------------------------------------
procedure SetSystemWakeUpTimer(AHandle: THandle; ADateTime: TDateTime);
var
  LTimeValue : Int64;
begin
  // 現在の待機タイマー用のスレッドを終了させる
  if Assigned(FTimerThread) then begin
    FTimerThread.Terminate;
    FreeAndNil(FTimerThread);
  end;

  // 待機タイマー用のスレッドを生成
  FTimerThread := TThread.CreateAnonymousThread(
    procedure()
    begin
      // TDateTime の時刻を UTC 換算のシステムファイル時刻に変換
      LTimeValue := TDateTimeToUTCFileTime(ADateTime);

      // 待機タイマーのオブジェクトを作成
      FhWaitTimer:= CreateWaitableTimer(nil, False ,nil);

      // 待機可能タイマーの日時を設定
      SetWaitableTimer(FhWaitTimer, LTimeValue, 0, nil, nil, True);

      // 待機タイマーがシグナル状態になるのを待つ
      WaitForSingleObject(FhWaitTimer, INFINITE);

      //上の処理が終了したら...
      // スリープからの復帰(レジューム)完了またはキャンセル
      TThread.Synchronize(TThread.CurrentThread,
        procedure
        begin
          // 待機タイマーのハンドルを閉じる
          CloseHandle(FhWaitTimer);
          FhWaitTimer := 0;
          // スリープの抑止
          // これがないと再スリープしてしまう
          SetThreadExecutionState(ES_SYSTEM_REQUIRED or ES_CONTINUOUS);
          // 指定したウィンドウに終了のメッセージを送信
          // 処理をキャンセルした場合も送信
          PostMessage(AHandle, WM_RESUME_NOTIFY, 0, Ord(not FFlagCancel));
        end
      );
    end
  );
  FTimerThread.FreeOnTerminate := False;
  FTimerThread.Start;
end;

//-----------------------------------------------------------------------------
//  待機タイマーの処理を中止
//  スリープからの自動復帰(レジューム)のキャンセル用メソッド
//
//  SetWaitableTimer 関数でコールバック関数を指定しても処理できるが
//  本サンプルでは以下のコードで処理
//
//  以下のグローバル変数を使用
//  FhWaitTimer  : 待機タイマーのハンドル
//  FTimerThread : タイマー用のスレッドオブジェクト
//  FFlagCancel  : 待機タイマーを無効(リセット)する時のフラグ
//-----------------------------------------------------------------------------
procedure CancelSystemWakeUpTimer;
var
  LTimeValue : Int64;
begin
  if FhWaitTimer <> 0 then begin
    // キャンセル用のフラグを True にする
    FFlagCancel := True;
    // 待機タイマーの日時を今現在にする
    // 待機タイマーの処理は直ちに終了してシグナル状態になる(リセットと同じ効果)
    LTimeValue := TDateTimeToUTCFileTime(Now);
    SetWaitableTimer(FhWaitTimer, LTimeValue, 0, nil, nil, True);
  end;

  // 現在の待機タイマー用のスレッドを終了させる
  if Assigned(FTimerThread) then begin
    FTimerThread.Terminate;
    FreeAndNil(FTimerThread);
  end;

  FFlagCancel := False;
end;

//-----------------------------------------------------------------------------
//  TDateTime 型の時刻を UTC 換算のシステムファイル時刻に変換する関数
//  取得する値は 1970/1/1 9:0:0 から(日本の場合)の経過時間(単位は 100 ns)
//
//  uses に DateUtils が必要
//-----------------------------------------------------------------------------
function TDateTimeToUTCFileTime(ADateTime: TDateTime): Int64;
var
  LSystemTime    : TSystemTime;
  LLocalFileTime : TFileTime;
  LUTCFileTime   : TFileTime;
begin
  // TDateTime の時刻をシステム時刻に変換
  DateTimeToSystemTime(ADateTime, LSystemTime);

  // システム時刻をローカルなファイル時刻に変換
  SystemTimeToFileTime(LSystemTime, LLocalFileTime);

  // ローカルのファイル時刻を世界協定時刻 (UTC) ファイル時刻へ変換
  LocalFileTimeToFileTime(LLocalFileTime, LUTCFileTime);

  Result := Int64(LUTCFileTime);
end;

end.


待機可能タイマー関係の関数に CancelWaitableTimer 関数があります。この関数は、現在待機中のタイマーを非アクティブにしますが、待機タイマーを強制終了させる (シグナル状態にする) わけではありません。
待機タイマーがアクティブな時、つまり待機タイマーのスレッドが動作中に CancelWaitableTimer 関数を実行すると待機タイマーの処理が止まってしまい、スレッドが終了できなくなってしまいます。

上の待機タイマーのスレッド処理では、待機タイマーがシグナル状態 (設定した時刻) になるまでは待機タイマー用のスレッドを終了させることができません。そこで、キャンセルのコードでは、待機タイマーがシグナルになる時刻を今現在にして、強制的にシグナル状態にしています。そうすることで待機タイマー用のスレッドが終了できるようになります。
したがって、キャンセル用の FFlagCancel 変数は実際には必要ありませんが、キャンセルしたかどうかを判定するために使用しています。