在 阻止系統自動睡眠的小軟件,附C#制作過程 ,弄了一個防止系統睡眠的工具。然后馬上發現,新的需求來了:為了保護環境(省錢),在系統設置中,合上蓋子時會自動睡眠。那因下載之類的原因,需要臨時禁止睡眠的話,又懶得去改設置,而且下次還得改回來。所以沒事也是折騰,就研究了怎么用軟件實現了。
最開始的思路就是進行Hook,以截斷睡眠消息。但是木有找到方法。
然后發現當系統進行睡眠時,會廣播一個消息,然后每個軟件會有兩秒鍾(xp和03可以長達20秒)的時間進行善后(PBT_APMSUSPEND event)。雖然可以喚醒睡眠的電腦(System Wake-up Events),但是還沒找到方法取消這次睡眠。
最后,我的解決方法時,臨時修改電源設置,即將合上蓋子的動作設置為啥事不干,然后在需要的時候恢復原來的設置。
Windows下電源管理,及配置工具powercfg
Windows下電源管理方案是這樣的。最大的維度是電源配置方案,每套方案包含着一組電源設置。可以更改當前激活的方案,也可以修改每個電源設置的值。
使用系統自帶工具powercfg進行電源配置的查看及更改:其中GUID值會在后面用到。
注意到這里:
電源方案 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。
PowerReadACValueIndex (還有一個DC相關的API未列出,下同)
大致流程很簡單,首先獲取當前的設置,保存下來。然后對系統進行設置,使其合上蓋子時不采取任何操作。最后在需要的時候將原來的設置寫回。需要注意的一點是,在對當前激活的方案的設置進行修改時,需要調用 PowerSetActiveScheme 一次才能生效。
下面的問題,就變成了如何在C#里使用API了。
WinAPI基本只提供了C的接口,很多在C#中都沒有封裝,所以需要自己對相應的函數進行聲明。一個簡單的例子是下面這樣。
using System.Runtime.InteropServices; [DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);
其中,最蛋疼的一點就是得自己進行參數的類型轉換。最最蛋疼的一點是,使用有些API得往參數里傳二級指針的時候根本就不知道該怎么辦。
基本數據類型參考這個表格就好了(網上抄的,而且需要注意的是,真的是僅供參考):
對於指針,參考這個博文(他也是轉的,沒去找原始出處了): 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));
世界稍微有點復雜,但還是能接受的。
直到……
一個一個手工轉這也太不是個事了。
無意間看到這個網站,相見恨晚: http://www.pinvoke.net/ 別的碼農們干完上面的活后,把成果分享在這上面,造福后人。呃,這東西在VS上還弄了個插件……
只要輕按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 方法添加消息過濾了。