打断(而非防止启动)屏幕保护程序

43
我正在尝试通过移动鼠标来编程地中断屏幕保护程序,就像这样:
win32api.SetCursorPos((random.choice(range(100)),random.choice(range(100))))

并且它会显示错误信息:

pywintypes.error: (0, 'SetCursorPos', 'No error message is available')

只有当屏幕保护程序正在运行时才会出现此错误。

请求之所以如此,是因为计算机仅通过蓝牙设备(通过Python程序)输入数据。 当BT设备向计算机发送数据时,屏幕保护程序不会中断(这意味着我无法看到BT设备发送的数据)。 因此,当Python程序从BT设备接收到数据时,它也应该中断屏幕保护程序。

我已经看到了一些解决方法,可以防止屏幕保护程序启动(但在我的情况下不适用),但没有一种方法可以中断正在运行的屏幕保护程序。 我该如何在Windows 10和Python 3.10中实现这一点?


4
我有点困惑,不太明白为什么你要这么做。程序在什么情况下会打断屏保?我以为屏保的作用是在人离开电脑时降低能源消耗。为什么不让人来打断它,表明电脑又被使用了呢? - Karl Knechtel
4
这个请求的原因是电脑只用于通过蓝牙设备输入数据(通过Python程序)。当BT设备向电脑发送数据时,屏幕保护程序不会中断(这意味着我无法看到BT设备发送的数据)。因此,当Python程序从BT设备接收到数据时,它也应该打断屏幕保护程序。注意:以前我在Raspi上运行时,默认情况下屏幕保护程序会被打断。 - mortpiedra
3
所以......基本上你希望屏幕通常是关闭的,但在发送蓝牙数据时打开?而当数据可能被发送时,你会一直在周围......盯着屏幕保护程序,直到它实际发生?我仍然不太明白使用场景。 - Karl Knechtel
15
这似乎是一个XY问题。为什么你需要立即在屏幕上看到蓝牙设备发送的数据被接收?你想要做什么需要立即处理而又不能通过简单禁用屏保或使用声音提示来解决? - Braiam
8
这个问题正在 Meta 上进行讨论。 - TylerH
显示剩余26条评论
3个回答

52
Windows操作系统有一个对象层次结构。在层次结构的顶部是"Window Station"(窗口站点)。紧接着是"Desktop"(不要与桌面文件夹或桌面窗口混淆,后者显示该文件夹的图标)。您可以在文档中了解更多关于这个概念的信息。
我提到这一点是因为通常只有一个桌面可以接收和处理用户输入。当由于超时而被Windows激活的屏幕保护程序时,Windows会创建一个新的桌面来运行屏幕保护程序。
这意味着任何与任何其他桌面相关联的应用程序,包括你的Python脚本,都将无法向新的桌面发送输入,除非进行一些额外的工作。这项工作的性质取决于几个因素。假设最简单的情况是创建了没有“恢复时显示登录屏幕”的屏幕保护程序,并且没有通过远程连接或本地用户登录创建其他Window Station,则可以请求Windows获取活动桌面,将Python脚本附加到该桌面,移动鼠标,然后恢复以前的桌面,以便其余脚本按预期工作。
幸运的是,执行此操作的代码比解释要简单:
import win32con, win32api, win32service
import random
# Get a handle to the current active Desktop
hdesk = win32service.OpenInputDesktop(0, False, win32con.MAXIMUM_ALLOWED);
# Get a handle to the Desktop this process is associated with
hdeskOld = win32service.GetThreadDesktop(win32api.GetCurrentThreadId())
# Set this process to handle messages and input on the active Desktop
hdesk.SetThreadDesktop()
# Move the mouse some random amount, most Screen Savers will react to this,
# close the window, which in turn causes Windows to destroy this Desktop
# Also, move the mouse a few times to avoid the edge case of moving
# it randomly to the location it was already at.
for _ in range(4):
    win32api.SetCursorPos((random.randint(0, 100), random.randint(0, 100)))
# Revert back to the old desktop association so the rest of this script works
hdeskOld.SetThreadDesktop()

然而,如果屏幕保护程序在单独的窗口工作站上运行(因为选择了“在恢复时显示登录屏幕”),或者另一个用户通过物理控制台连接或远程连接,则连接和附加到活动桌面将需要提升Python脚本的权限,即使如此,根据其他因素,它可能需要特殊权限。
虽然这可能有助于您的特定情况,但我要补充一下,在一般情况下,核心问题也许更适当地定义为“如何通知用户某个状态,而不会被屏幕保护程序阻止?”这个问题的答案不是“结束屏幕保护程序”,而是“使用像 SetThreadExecutionState() 中的 ES_DISPLAY_REQUIRED 来防止屏幕保护程序运行。并显示一个全屏顶部窗口,显示当前状态,当您想要提醒用户时,闪烁一个引人注目的图形和/或播放声音来引起他们的注意”。
以下是使用tkinter显示窗口的示例:
from datetime import datetime, timedelta
import ctypes
import tkinter as tk

# Constants for calling SetThreadExecutionState
ES_CONTINUOUS = 0x80000000
ES_SYSTEM_REQUIRED = 0x00000001
ES_DISPLAY_REQUIRED= 0x00000002

# Example work, show nothing, but when the timer hits, "alert" the user
ALERT_AT = datetime.utcnow() + timedelta(minutes=2)

def timer(root):
    # Called every second until we alert the user
    # TODO: This is just alerting the user after a set time goes by,
    #       you could perform a custom check here, to see if the user
    #       should be alerted based off other conditions.
    if datetime.utcnow() >= ALERT_AT:
        # Just alert the user
        root.configure(bg='red')
    else:
        # Nothing to do, check again in a bit
        root.after(1000, timer, root)

# Create a full screen window
root = tk.Tk()
# Simple way to dismiss the window
root.bind("<Escape>", lambda e: e.widget.destroy())
root.wm_attributes("-fullscreen", 1)
root.wm_attributes("-topmost", 1)
root.configure(bg='black')
root.config(cursor="none")
root.after(1000, timer, root)
# Disable the screen saver while the main window is shown
ctypes.windll.kernel32.SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED)
root.mainloop()
# All done, let the screen saver run again
ctypes.windll.kernel32.SetThreadExecutionState(ES_CONTINUOUS)

虽然需要更多的工作,但这样做可以解决安全桌面设置为“恢复后显示登录屏幕”时出现的问题,还可以防止系统在配置为休眠时进入休眠状态。它通常允许应用程序更清晰地传达其意图。

21

SetCursorPos 失败的原因可能是在屏幕保护程序运行时光标被设置为空(NULL)了。

与其移动光标,不如尝试查找当前屏幕保护程序的可执行路径并结束该进程。我认为这将是一个不错的解决方案。

  1. 您可以检查Windows注册表记录以获取屏幕保护程序的文件名 (HKEY_USERS\.DEFAULT\Control Panel\Desktop\SCRNSAVE.EXE (msdn)

  2. 或者您可以检查当前正在运行的进程列表,以查找扩展名为.scr的进程

然后,只需使用 TerminateProcess 或者 os.system('taskkill /IM "' + ProcessName + '" /F') 结束该进程即可。


2
杀死进程会导致屏幕保护程序在另一个一段时间的不活动后无法恢复吗?如果是这样,那听起来似乎不符合OP的使用情况。 - Ken Williams
3
不,屏幕保护程序会重新开始。Windows在特定时刻启动一个屏幕保护进程,就像启动任何其他应用程序一样。当鼠标移动时,屏幕保护程序会自动终止(有些非默认的屏幕保护程序甚至可能不会在鼠标移动时退出,并提供一些交互式用户界面)。 - Pavel Shishmarev

2
这是一个经典的 XY 问题:假设您成功阻止了屏幕保护程序在您的机器/测试设置上启动。但是还有进一步的问题:
  • 如果您的程序在没有 UI 会话的终端服务器上运行会发生什么情况?
  • 如果电源节能设置按一定时间后使计算机进入睡眠状态,您的解决方案是否有效?
  • 它将与未来的 Windows 版本一起工作吗?与不同的子产品一起工作吗?(那种创造性的“查看此未记录的注册表键,然后杀死某些随机进程”的解决方案似乎注定要失败)

谁知道呢,而且肯定很难测试。

你真正需要的是一种告诉操作系统“嘿,我正在忙碌,请保持会话活动状态,即使您的正常启发式会告诉您用户已离开”的方法。这是视频播放器和演示软件经常面临的标准问题。

标准解决方案是在程序开始时使用SetThreadExecutionState,类似于ES_DISPLAY_REQUIRED | ES_CONTINUOUS(可能还有其他标志 - 文档在这方面相当合理)。 Raymond Chen曾经在过去写过这个问题(这并不奇怪)。
请注意,这不会停止已经激活的屏幕保护程序 - 这通常不是问题,因为您可以在启动时(或触发预期操作时)设置标志。它也不能阻止用户手动将计算机置于睡眠状态,但这通常是不应禁用的。

6
虽然非常有信息量,但我认为这并没有帮助原帖作者解决他们的问题。 - Super Retarded Dog

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