zoukankan      html  css  js  c++  java
  • 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 方法添加消息过滤了。

  • 相关阅读:
    iOS中Zbar二维码扫描的使用
    SOJ 1135. 飞跃原野
    SOJ 1048.Inverso
    SOJ 1219. 新红黑树
    SOJ 1171. The Game of Efil
    SOJ 1180. Pasting Strings
    1215. 脱离地牢
    1317. Sudoku
    SOJ 1119. Factstone Benchmark
    soj 1099. Packing Passengers
  • 原文地址:https://www.cnblogs.com/h46incon/p/3299138.html
Copyright © 2011-2022 走看看