在Windows系统中检测屏幕保护程序是否处于活动状态和/或用户是否锁定屏幕

9
我正在编写一个应用程序,有时会通过烤面包机消息向用户发送通知(toaster messages)
如果用户不在场,他就看不到通知。因此,我想要做的是能够检查用户是否锁定了屏幕或者屏幕保护程序是否已激活。
任何在用户无法看到的情况下触发的通知都将被延迟,并在用户登录并恢复会话时显示。
我自己使用的是Windows 7,但我更喜欢适用于Windows XP及以上版本的解决方案。

6个回答

28

目前没有记录的方法可以查找工作站是否已被锁定。但是,您可以在其解锁/锁定时获取通知。订阅SystemEvents.SessionSwitch事件,您将收到SessionSwitchReason.SessionLock和Unlock。

屏幕保护程序也很麻烦。当屏幕保护程序打开时,您的主窗口会收到WM_SYSCOMMAND消息,SC_SCREENSAVE。您可以调用SystemParametersInfo来检查它是否正在运行。在此线程中,您将找到示例代码。

目前没有好的方法可以确定用户是否已经睡着。


6
+1 对于判断用户是否睡着,没有好的方法。 :) - zeFree

11

我最近从一篇先前的博客文章中 (链接) 再次检查了这段代码,以确保它适用于 Windows XP 到 7 版本,包括 x86 和 x64,并对其进行了简化。

以下是最新的、检查工作站是否锁定并且屏幕保护程序是否正在运行的简洁代码,封装在两个易于使用的静态方法中:

using System;
using System.Runtime.InteropServices;

namespace BrutalDev.Helpers
{
  public static class NativeMethods
  {
    // Used to check if the screen saver is running
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool SystemParametersInfo(uint uAction, 
                                                   uint uParam, 
                                                   ref bool lpvParam,
                                                   int fWinIni);

    // Used to check if the workstation is locked
    [DllImport("user32", SetLastError = true)]
    private static extern IntPtr OpenDesktop(string lpszDesktop,
                                             uint dwFlags,
                                             bool fInherit,
                                             uint dwDesiredAccess);

    [DllImport("user32", SetLastError = true)]
    private static extern IntPtr OpenInputDesktop(uint dwFlags,
                                                  bool fInherit,
                                                  uint dwDesiredAccess);

    [DllImport("user32", SetLastError = true)]
    private static extern IntPtr CloseDesktop(IntPtr hDesktop);

    [DllImport("user32", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SwitchDesktop(IntPtr hDesktop);

    // Check if the workstation has been locked.
    public static bool IsWorkstationLocked()
    {
      const int DESKTOP_SWITCHDESKTOP = 256;
      IntPtr hwnd = OpenInputDesktop(0, false, DESKTOP_SWITCHDESKTOP);

      if (hwnd == IntPtr.Zero)
      {
        // Could not get the input desktop, might be locked already?
        hwnd = OpenDesktop("Default", 0, false, DESKTOP_SWITCHDESKTOP);
      }

      // Can we switch the desktop?
      if (hwnd != IntPtr.Zero)
      {
        if (SwitchDesktop(hwnd))
        {
          // Workstation is NOT LOCKED.
          CloseDesktop(hwnd);
        }
        else
        {
          CloseDesktop(hwnd);
          // Workstation is LOCKED.
          return true;
        }
      }

      return false;
    }

    // Check if the screensaver is busy running.
    public static bool IsScreensaverRunning()
    {
      const int SPI_GETSCREENSAVERRUNNING = 114;
      bool isRunning = false;

      if (!SystemParametersInfo(SPI_GETSCREENSAVERRUNNING, 0, ref isRunning, 0))
      {
        // Could not detect screen saver status...
        return false;
      }

      if (isRunning)
      {
        // Screen saver is ON.
        return true;
      }

      // Screen saver is OFF.
      return false;
    }
  }
}

更新:根据评论中的建议更新了代码。

当工作站被锁定时,OpenInputDesktop方法不会返回句柄,因此我们可以回退到OpenDesktop以获取一个句柄,并尝试切换来确保它被锁定。如果没有被锁定,则由于OpenInputDesktop将返回您正在查看的桌面的有效句柄,因此不会激活默认桌面。


2
除非我漏掉了什么,否则如果在我使用的应用程序上以定时器运行,这段代码会让我发疯。 我很确定这将始终强制默认桌面处于活动状态,而不被锁定时。 我使用多个桌面,在所有桌面上都实际工作。 我不使用它,但微软有一个desktops.exe应用程序,可以让您创建多个桌面并在它们之间切换,不确定是否有很多人使用它,如果是这样,这段代码不适合生产。 我认为有一种方法可以获取当前输入桌面,可能是更好的检查方式。 - eselk
1
请在 MSDN 上查找 OpenInputDesktop & GetUserObjectInformation,以获取活动桌面名称。 - eselk
@eselk:感谢您的有效评论。如果您有不同命名的桌面(非常罕见),那么您是正确的,这将在计时器循环中将您切换到默认桌面。我将使用您的建议使用OpenInputDesktop,以便切换是静默的。如果使用OpenInputDesktop,则GetUserObjectInformation将无用,因为我不需要桌面名称,只需要句柄即可进行切换。等我有机会测试它时,将对代码进行基本编辑 :) - BrutalDev
1
我已经在Windows 10上尝试了这段代码,但IsWorkstationLocked()始终返回false。 - Christopher Painter
这在 Windows 11 上可用。谢谢! - Billy Long
显示剩余4条评论

3
使用SystemParametersInfo函数,调用SPI_GETSCREENSAVERRUNNING参数可以检测屏幕保护是否开启,此功能支持Win2000及以上版本。
在StackOverflow上有@dan_g的代码在这里,可以检查工作站是否已锁定。

2
SystemEvents.SessionSwitch += new SessionSwitchEventHandler((sender, e) =>
        {
            switch (e.Reason)
            {
                //If Reason is Lock, Turn off the monitor.
                case SessionSwitchReason.SessionLock:
                    //SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, MONITOR_OFF);
                    MessageBox.Show("locked");
                    break;

                case SessionSwitchReason.SessionUnlock:
                    MessageBox.Show("unlocked");
                    break;
            }
        });

这将指示会话何时被锁定和解锁。


如果有人想要更多信息,请参考上面代码的来源:Anuraj Parameswaran,2012年,https://www.codeproject.com/articles/472795/how-to-turn-off-your-monitor-when-you-lock-your-ma - SunsetQuest

1

在研究这个问题时,我发现了几种技术可以支持检测工作站是否已锁定。其中一种非常简单:

bool locked = Process.GetProcessesByName("logonui").Any();

这能够生效是因为只有当桌面被锁定时,logonui进程才会运行。
另一种更为复杂的方法是向后遍历系统事件日志,查找事件ID 4800和4801。它们表示计算机何时被锁定和解锁。
更多详细信息可在以下链接中找到:

https://superuser.com/questions/1170918/determine-remote-windows-screen-locked-or-unlocked-remotely

http://mctexpert.blogspot.com/2012/10/how-to-determine-if-client-on-your.html

后者需要设置审计策略,这通常是默认的。我在企业IT组织中工作,所以这对我来说不是一个问题,因为我确定这些设置已经应用了。

感谢这个简单的解决方案! - ecif
我发现这并不是100%可靠的。最终,我创建了一个计划任务,在锁定和解锁时运行以更新注册表值。那更可靠一些,但仍然不完美。 - Christopher Painter
是的,当我把它放在Windows服务中时,实际上它不起作用... - ecif
1
在远程桌面下,即使有远程用户登录,当本地屏幕被锁定时,登录UI进程仍然存在。 - tomsv

1

有很多原因会导致用户无法看到您的通知,例如全屏视频播放或用户不在场。

我建议您不要仅检查是否可以显示通知,而是检查用户是否在场,您可以通过监视键盘和鼠标来实现。


这可能有效,但只有在用户不玩全屏游戏的情况下。 - Pieter
@Pieter,你需要检查屏幕是否处于全屏独占模式。 - Shay Erlichmen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接