C#使用WinAPI 修改電源設置,臨時禁止筆記本合上蓋子時睡眠


阻止系統自動睡眠的小軟件,附C#制作過程 ,弄了一個防止系統睡眠的工具。然后馬上發現,新的需求來了:為了保護環境(省錢),在系統設置中,合上蓋子時會自動睡眠。那因下載之類的原因,需要臨時禁止睡眠的話,又懶得去改設置,而且下次還得改回來。所以沒事也是折騰,就研究了怎么用軟件實現了。

 

最開始的思路就是進行Hook,以截斷睡眠消息。但是木有找到方法。

然后發現當系統進行睡眠時,會廣播一個消息,然后每個軟件會有兩秒鍾(xp和03可以長達20秒)的時間進行善后(PBT_APMSUSPEND event)。雖然可以喚醒睡眠的電腦(System Wake-up Events),但是還沒找到方法取消這次睡眠。

最后,我的解決方法時,臨時修改電源設置,即將合上蓋子的動作設置為啥事不干,然后在需要的時候恢復原來的設置。


 

Windows下電源管理,及配置工具powercfg

Windows下電源管理方案是這樣的。最大的維度是電源配置方案,每套方案包含着一組電源設置。可以更改當前激活的方案,也可以修改每個電源設置的值。

使用系統自帶工具powercfg進行電源配置的查看及更改:其中GUID值會在后面用到。

image

image

注意到這里:
  

電源方案 GUID: a1841308-3541-4fab-bc81-f71556f20b4a  (節能)
  子組 GUID: 4f971e89-eebd-4455-a8de-9e59040e7347  (電源按鈕和蓋子)
    電源設置 GUID: 5ca83367-6e45-459f-a27b-476b1d01c936  (合上蓋子操作)

電源方案GUID可能會因激活的方案不同而不同,而子組GUID和電源設置GUID在每個方案下都是一樣的。后面用這兩個ID進行設置就好。對了,每個設置都有直流和交流兩項,分別表示使用筆記本電源和外置電源的設置。

至此,省事的話差不多可以收工了:使用powercfg這個工具對電源方案進行設置就好了。


但是,為了折騰,我還是選擇了使用API對電源方案進行配置

祭出要用到的API。

PowerGetActiveScheme

PowerSetActiveScheme

PowerReadACValueIndex    (還有一個DC相關的API未列出,下同)

PowerWriteACValueIndex

大致流程很簡單,首先獲取當前的設置,保存下來。然后對系統進行設置,使其合上蓋子時不采取任何操作。最后在需要的時候將原來的設置寫回。需要注意的一點是,在對當前激活的方案的設置進行修改時,需要調用 PowerSetActiveScheme 一次才能生效。

 

下面的問題,就變成了如何在C#里使用API了

WinAPI基本只提供了C的接口,很多在C#中都沒有封裝,所以需要自己對相應的函數進行聲明。一個簡單的例子是下面這樣。

using System.Runtime.InteropServices;
[DllImport("kernel32.dll")]
public static extern uint SetThreadExecutionState(uint esFlags);

其中,最蛋疼的一點就是得自己進行參數的類型轉換。最最蛋疼的一點是,使用有些API得往參數里傳二級指針的時候根本就不知道該怎么辦。

 

基本數據類型參考這個表格就好了(網上抄的,而且需要注意的是,真的是僅供參考):

image

對於指針,參考這個博文(他也是轉的,沒去找原始出處了):  C#調用Win32 API如何處理指針類型的參數

 

 

下面來兩個用到的具體例子。

DWORD WINAPI PowerReadACValueIndex(
  _In_opt_  HKEY RootPowerKey,
  _In_opt_  const GUID *SchemeGuid,
  _In_opt_  const GUID *SubGroupOfPowerSettingsGuid,
  _In_opt_  const GUID *PowerSettingGuid,
  _Out_     LPDWORD AcValueIndex
);

在C#里聲明的時候長這樣了:

//返回值DWORD轉為uint。
uint PowerReadACValueIndex(
	//第一個參數類型HKEY,不知道他是一個干啥用的指針,而且這個API里只能是NULL值,就簡單聲明為IntPtr類型,使用時傳IntPtr.Zero就好了。
	IntPtr RootPowerKey,

	//GUID在C#里有這個Guid類型與之對應。至於一級指針,得看這個指針是干啥用的。如果這個指針只是指向一個變量的話,就用ref修飾,實際傳遞的就是指針了。如果這個指針指向的是一個數組的首地址,那就先得在C#里分配一段內存,然后把這個內存的地址傳進去。參考前面轉的博文。
	ref Guid SchemeGuid,
	ref Guid SubGroupOfPowerSettingsGuid,
	ref Guid PowerSettingGuid,

	//最后一個參數類型LPDWORD。LP指的是long pointer,好像現在的系統不分長短指針了,就簡單把他理解為一個指針吧。那LPDWORD就是一個指向DWORD的指針。對應到C#里就是ref uint了。
	ref uint AcValueIndex
);

 

世界還是很簡單的,直到碰上了一個二級指針

DWORD WINAPI PowerGetActiveScheme(
  _In_opt_  HKEY UserRootPowerKey,
  _Out_     GUID **ActivePolicyGuid
);

這東西目的是把一個指向GUID* 的變量p_GUID,的地址傳進去,然后他會new一個GUID作為結果,再然后會把p_GUID的值設為這個結果的地址。使用完畢之后,需調用LocalFree釋放這段內存。 這下不能用ref 來省事了,所以就老老實實傳個IntPtr進去吧:

uint PowerGetActiveScheme(IntPtr UserRootPowerKey, ref IntPtr p_ActivePolicyGuid);

調用之后,p_ActivePolicyGuid就是一個指向GUID變量的指針了。由於使用了ref修飾,所以他本身是個一級指針。要怎么樣對他指向的內容進行解釋呢?C#里有個Marshal

Guid guid = (Guid)Marshal.PtrToStructure(p_ActivePolicyGuid, typeof(Guid));

世界稍微有點復雜,但還是能接受的。


直到……

image

一個一個手工轉這也太不是個事了。

無意間看到這個網站,相見恨晚:   http://www.pinvoke.net/ 別的碼農們干完上面的活后,把成果分享在這上面,造福后人。呃,這東西在VS上還弄了個插件……

image

只要輕按Insert……不過對API的實際用法不一樣,也會導致聲明的類型有所不同,自己了解一下轉換方法總是有好處的。

 


當運行軟件后,用戶又去系統里對電源設置進行更改,比如又把合上蓋子的動作改成睡眠的話,那就不好了。更可能發生的情況是,系統更改了當前激活的電源方案,比如從“節能”改成“高性能”,那合上蓋子的動作就很有可能改變了。所以我們需要對這個動作進行監控。

這有個API(就是上面截圖里的那個)可以在修改制定選項時進行通知:

[DllImport(@"User32", SetLastError=true, EntryPoint = "RegisterPowerSettingNotification",
 	CallingConvention = CallingConvention.StdCall)]
public static extern
	IntPtr RegisterPowerSettingNotification(
		IntPtr hRecipient,
		ref Guid PowerSettingGuid,
		uint Flags
	);
public const uint DEVICE_NOTIFY_WINDOW_HANDLE = 0;
public const uint DEVICE_NOTIFY_SERVICE_HANDLE = 1;

需要往第一個參數里傳入一個句柄。這個句柄可以有兩種類型,一是窗口句柄,另一種比較復雜,涉及到服務,覺得很麻煩,還不知道有沒比較簡便的方法。

這個時候就比較坑爹了,因為剛開始寫這個軟件的時候,主線程里只跑了一個NotifyIcon控件,這東西的handle是私有的,而且就算通過下面的hack拿到句柄,並注冊成功后,這個線程也收不到消息。hack代碼如下(抄這的: 來個更BT的NotifyIcon支持BalloonTip,還沒搞懂):

private IntPtr GetWindowHandle(NotifyIcon notifyIcon)
{
    if ( notifyIcon == null )
    {
        return IntPtr.Zero;
    }

    Type type = notifyIcon.GetType();
    BindingFlags bf = BindingFlags.Instance | BindingFlags.NonPublic;
    FieldInfo fiWindow = type.GetField("window", bf);
    object objWindow = fiWindow.GetValue(this.m_NotifyIcon);

    type = objWindow.GetType().BaseType;
    FieldInfo fiHandle = type.GetField("handle", bf);
    IntPtr handle = (IntPtr)fiHandle.GetValue(objWindow);
    return handle;
}

所以最后還是乖乖地弄了一個Form控件。這有一個問題:一個線程已經有消息隊列了,我能不能在需要注冊窗體handle的地方,注冊線程的handle?

注冊之后怎么用呢?

一是重載窗體的消息處理函數:

protected override void WndProc(ref Message m)
{
	if (m.Msg == Win32API.WM_POWERBROADCAST) 
	{
		MessageBox.Show("Power mode Changed! wndproc");
		return;
 
	}
			
	base.WndProc(ref m);
}

二是使用消息過濾: IMessageFilter 

實現了這個接口后,就可以使用 Application.AddMessageFilter 方法添加消息過濾了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM