Search code examples
c#winformswinapipinvoke

Can't register for power notification settings in WinForms C#


Here is the code

    [DllImport(@"User32", SetLastError = true, EntryPoint = "RegisterPowerSettingNotification",
        CallingConvention = CallingConvention.StdCall)]
    private static extern IntPtr RegisterPowerSettingNotification(IntPtr hRecipient, ref Guid PowerSettingGuid, Int32 Flags);

    static Guid GUID_LIDSWITCH_STATE_CHANGE = new Guid(0xBA3E0F4D, 0xB817, 0x4094, 0xA2, 0xD1, 0xD5, 0x63, 0x79, 0xE6, 0xA0, 0xF3);
    private const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000;
    private const int WM_POWERBROADCAST = 0x0218;
    const int PBT_POWERSETTINGCHANGE = 0x8013;

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    internal struct POWERBROADCAST_SETTING
    {
        public Guid PowerSetting;
        public uint DataLength;
        public byte Data;
    }

    private bool? _previousLidState = null;


    public TrayIcon()
    {
        RegisterForPowerNotifications();

    }

    [SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case WM_POWERBROADCAST:
                OnPowerBroadcast(m.WParam, m.LParam);
                break;
            default:
                break;
        }
        base.WndProc(ref m);
    }


    private void RegisterForPowerNotifications()
    {
        IntPtr handle = this.Handle;
        Debug.WriteLine("Handle: " + handle.ToString()); //If this line is omitted, then lastError = 1008 which is ERROR_NO_TOKEN, otherwise, lastError = 0
        IntPtr hLIDSWITCHSTATECHANGE = RegisterPowerSettingNotification(handle,
             ref GUID_LIDSWITCH_STATE_CHANGE,
             DEVICE_NOTIFY_WINDOW_HANDLE);
        Debug.WriteLine("Registered: " + hLIDSWITCHSTATECHANGE.ToString());
        Debug.WriteLine("LastError:" + Marshal.GetLastWin32Error().ToString());
    }

    private void OnPowerBroadcast(IntPtr wParam, IntPtr lParam)
    {
        if ((int)wParam == PBT_POWERSETTINGCHANGE)
        {
            POWERBROADCAST_SETTING ps = (POWERBROADCAST_SETTING)Marshal.PtrToStructure(lParam, typeof(POWERBROADCAST_SETTING));
            IntPtr pData = (IntPtr)((int)lParam + Marshal.SizeOf(ps));
            Int32 iData = (Int32)Marshal.PtrToStructure(pData, typeof(Int32));
            if (ps.PowerSetting == GUID_LIDSWITCH_STATE_CHANGE)
            {
                bool isLidOpen = ps.Data != 0;

                if (!isLidOpen == _previousLidState)
                {
                    LidStatusChanged(isLidOpen);
                }

                _previousLidState = isLidOpen;
            }
        }
    }

    private void LidStatusChanged(bool isLidOpen)
    {
        if (isLidOpen)
        {
            //Do some action on lid open event
            MessageBox.Show("Lid is now open");
        }
        else
        {
            //Do some action on lid close event
            MessageBox.Show("Lid is now closed");
        }
    }
}
}

I have no idea what the problem is. I get calls to WndProc function, but nothing happens when the lid is closed or opened. LidStatusChanged is never called.

I have followed this post but that doesn't help as everything matches.

I have no idea what the heck I did wrong. All help is greatly appreciated.


Solution

  •    ShowInTaskbar = Visible = false;
    

    The bug is no longer visible in the snippet. It is the ShowInTaskbar property assignment that caused the problem. It is a "difficult" property, it can only be specified in the style flags passed to CreateWindowEx(). So that forces Winforms to destroy the current window and create a new one, it now gets a different Handle value. No more notifications.

    You probably got into this trouble by trying to keep the window invisible. Proper way to do that is:

    protected override void SetVisibleCore(bool value) {
        if (!IsHandleCreated) {
            this.CreateHandle();
            value = false;
        }
        base.SetVisibleCore(value);
    }
    

    Delete OnLoad(), no longer necessary and not called until the window actually becomes visible. And you want to make sure that, even if the Handle value changes for some reason (there are several "difficult" properties), you still get a notification. Which you do by deleting the code from the constructor and:

    protected override void OnHandleCreated(EventArgs e) {
        base.OnHandleCreated(e);
        RegisterForPowerNotifications();
    }