我可真是受够了这群半夜踩键盘把电脑屏幕踩亮的臭猫咪了!
——忍无可忍·无能狂怒·我
为什么要写这玩意
想必你也一定有过这样的烦恼,当你躺在床上社会性睡眠的时候,忽然猫咪跳到了桌子上,把熄屏的电脑屏幕踩亮
而你恰好:
- 晚上睡觉没有关闭电脑的习惯
- 上床之前没有手动关闭显示器电源的习惯
- 显示器的自动休眠时间非常长
- 浑身懒癌发作不愿意爬起来去关屏幕
- 并且你养的懒猫也不愿意抬抬爪子帮你关一下显示器
……好吧,我相信大多数人应该不会有这样的烦恼,但是总之,在半夜被显示器最大亮度糊脸失眠几十次以后,我终于忍无可忍,写出了这么个小玩意。
实现方式
虽然家里用的是米家的智能家居,但是很不幸,米家并没有提供PC的可自定义操作客户端,所以用小爱同学关显示屏这条路是走不通了,拿智能插座控制显示器的话,等于是直接给显示器拔电源,伤显示器不说,再想用的时候还得让小爱同学重新给显示器通电。
最好的方法还是让操作系统自己熄屏,这样再使用的时候,只要晃动鼠标就能自然唤醒。想要通过代码关闭显示屏并不难,主要的问题就是如何触发了。好在IOS的快捷指令现在足够强大,可以执行HTTP请求,并且可以被Siri语音调用,甚至可以用Apple Watch脱离手机执行。正好本来我在每天晚上睡觉时就需要佩戴Apple Watch进行睡眠监测,那么实现起来就很简单了。
这里通过.net自带的HttpListener在本地30001端口开启监听,接到特定HTTP请求后发送消息关闭电脑的显示器。
首先新建一个HTTPServer.cs实现监听部分功能
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Windows;
namespace ScreenOff
{
public delegate void ReceivedRequestEventHandler(string payload);
public delegate void OnExceptionEventHandler(Exception ex);
class HttpServer
{
private Thread _serverThread;
private HttpListener _listener;
public int Port { get; private set; }
public event ReceivedRequestEventHandler ReceivedCommandRequest;
public event OnExceptionEventHandler OnException;
/// <summary>
/// 在指定端口启动监听
/// </summary>
/// <param name="port">要启动的端口</param>
public HttpServer(int port)
{
Initialize(port);
}
/// <summary>
/// 停止监听并释放资源
/// </summary>
public void Stop()
{
//_serverThread.Abort();
_listener.Stop();
}
private void Listen()
{
try
{
_listener = new HttpListener();
_listener.Prefixes.Add("http://*:" + Port + "/");
_listener.Start();
}
catch (Exception ex)
{
OnException?.Invoke(ex);
return;
}
ThreadPool.QueueUserWorkItem(o => {
try
{
while (_listener.IsListening)
ThreadPool.QueueUserWorkItem(c =>
{
if (c is not HttpListenerContext context)
throw new ArgumentNullException(nameof(context));
try
{
DoAction(ref context);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
finally
{
context.Response.OutputStream.Close();
}
}, _listener.GetContext());
}
catch
{
// ignored
}
});
}
private void DoAction(ref HttpListenerContext context)
{
var command = new StreamReader(context.Request.InputStream, Encoding.UTF8).ReadToEnd();
ReceivedCommandRequest?.Invoke(command);
var buf = Encoding.UTF8.GetBytes(command);
context.Response.ContentLength64 = buf.Length;
context.Response.OutputStream.Write(buf, 0, buf.Length);
context.Response.StatusCode = (int)HttpStatusCode.OK;
context.Response.OutputStream.Flush();
}
private void Initialize(int port)
{
Port = port;
_serverThread = new Thread(Listen);
_serverThread.Start();
}
}
}
这个模块在鲶鱼精邮差里已经用过了。虽然其实用Nancy之类的包也能方便地实现需要的功能,不过这里就不大炮打蚊子了。
在MainWindow.cs中初始化HttpServer,并将接收到指令时进行的操作和错误处理事件委托给ReceivedCommandRequest和OnException
private void ServerStart(object sender = null, EventArgs e = null)
{
try
{
_httpServer = new HttpServer(30001);
_httpServer.ReceivedCommandRequest += DoTextCommand;
_httpServer.OnException += OnException;
}
catch (Exception ex)
{
OnException(ex);
}
}
private void DoTextCommand(string command)
{
switch (command.ToLower(CultureInfo.CurrentCulture))
{
case "screen=off":
_ = PostMessage(-1, 0x0112, 0xF170, 2);//hwnd,WM_SYSCOMMAND, (IntPtr)SC_MONITORPOWER, (IntPtr)2);
return;
default:
return;
}
}
private void OnException(Exception ex)
{
string errorMessage = $"无法在{_httpServer.Port}端口启动监听\n{ex.Message}";
MessageBox.Show(errorMessage);
}
这就几乎是全部的功能性代码了,完整代码详见Natsukage/ScreenOff: 通过IOS快捷指令联动关闭显示器 (github.com)
这玩意能干什么
配合IOS的捷径功能,就可以实现对着Apple Watch或者手机 “Hi Siri,关闭显示器” 隔空语音关闭显示器了
当然关闭显示器这个动作实际上只需要一条PostMessage指令就可以实现。这里完全可以自由扩展,根据指令的不同来执行不同的操作,只是我目前还完全没有想到其他需求。
毕竟社畜除了睡觉以外,其他时间都是坐在电脑前的。
在IOS的快捷指令中按照如图所示进行设置,通过HTTP请求将screen=off参数Post至PC的30001端口即可触发软件中写好的指令,通过发送消息关闭显示器。
重要注意事项
这里注意,由于IOS的捷径似乎并不能随心所欲的设定Post的内容,必须是按照标准格式?A=a&B=b&C=c这样进行提交。因此其实用Get方法可能更自由一些。Post方法的话,指令的内容就只能是A=B的这种样式。例如这里就设定为了screen=off。
这里增加了一条判断,即只有当当前wifi是自家wifi时候才会执行指令,实际是没必要的。而且如果这么判断了,后面还需要加一条判断,即如果设备是apple watch的话 那么就不考虑wifi名执行快捷指令。因为手表在和手机连接时,自己是没有连接wifi的,所以获取wifi名称对比的这步判断也会直接判非。
实际真正重要的只有第四行的获取文本内容这条,只要这一句指令就足以控制显示器关闭了。
此外,这里的监听注册了http://*:30001/,而按照windows默认的安全规则,注册localhost以外的监听是需要管理员权限的。因此要么每次都使用管理员权限启动程序,要么用管理员权限打开CMD,输入netsh http add urlacl url=http://*:30001/ user=Everyone
授权所有用户即可一劳永逸
此外,如果测试时快捷指令卡住,记得关掉Windows防火墙再试。windows自带防火墙默认是会阻止这样的请求的,而且阻止时不会有提示。我也没有在程序里自动添加防火墙规则,你需要手动放行TCP 30001端口才可以。
本文源码链接: