背景介绍
嘿,各位小伙伴!今天想跟大家唠唠我为啥要搞这么个防沉迷小工具。
咱都清楚,现在这游戏啊,玩起来那叫一个带劲,但时间一长,不仅眼睛累,心也跟着累。有些游戏,规则定得挺有意思:要是玩超过 15 分钟,你就可以秒退了,系统不会给你什么惩罚。这不,我就寻思着,要是有个东西能帮咱盯着游戏开始的时间,到时候提醒咱一声,那该多好啊!我可以根据当前局势判断要不要退出,要是碰到开挂的、花钱的,那果断推出,不能苦了自己舒服了别人。
于是,我就琢磨着做这么个防沉迷小工具。它的功能说白了就是:在后台悄悄看着游戏进程,一旦快到那个关键的 15 分钟(或者你可以自己定别的时长),就给你来个提醒,比如播放个声音,或者用其他方式。这样,咱就能根据当时的游戏情况,决定是赶紧退出 "保全自己",还是再坚持一会儿。
而且啊,这小工具还很灵活,支持各种配置。你可以根据自己的喜好,调整提醒的时间、方式啥的,想怎么来就怎么来。不管你是想避开那些开挂的,还是不想被诱导花冤枉钱,有了它,都能帮你更果断地做出选择,不至于让自己玩得太累,让别人占了便宜。
总之,这防沉迷小助手就是咱在游戏世界里的一位贴心 "小管家",帮咱把握好分寸,让咱玩得开心又不 "上头"。
一、需求设计目标
在游戏场景中,许多防沉迷系统采用强制退出机制。本工具旨在实现柔性提醒机制,通过声音、弹窗、窗口抖动等方式提醒用户,同时满足以下工程目标:
- 可配置化:支持配置进程匹配规则、提醒策略等参数
- 扩展性:可灵活添加新的提醒方式
- 健壮性:避免资源泄漏,处理进程访问异常
- 低侵入性:不修改目标进程内存或行为
效果如下:
二、架构设计
采用分层架构:
- 配置层:处理XML配置加载
- 监控层:实现进程状态检测
- 策略层:多种提醒策略实现
- UI层:提供配置界面和状态显示
三、关键模块实现
3.1 配置管理模块
使用.NET ConfigurationSection实现自定义配置:
csharp
public class ProcessMonitorSection : ConfigurationSection
{
[ConfigurationProperty("ProcessItems")]
public ProcessItemCollection ProcessItems =>
(ProcessItemCollection)this["ProcessItems"];
}
优势:
- 配置热加载能力
- 强类型配置访问
- 配置验证机制
3.2 进程监控引擎
核心监控流程:
是 是 启动定时器 遍历配置规则 枚举系统进程 匹配进程名? 计算运行时间 超时且需提醒? 执行提醒策略
关键技术点:
- 通配符转正则表达式
csharp
private Regex ConvertWildcardToRegex(string pattern)
{
return new Regex("^" +
Regex.Escape(pattern)
.Replace("\\*", ".*")
.Replace("\\?", ".") + "$",
RegexOptions.IgnoreCase);
}
- 进程生命周期管理
- 异常处理机制
3.3 策略模式实现提醒
策略接口定义:
csharp
public interface IAlertStrategy
{
Task ExecuteAsync(AlertContext context);
}
具体策略示例(窗口抖动):
csharp
public class WindowShakeStrategy : IAlertStrategy
{
public async Task ExecuteAsync(AlertContext context)
{
await Task.Run(() =>
{
for (int i = 0; i < 3; i++) {
NativeMethods.ShakeWindow(_windowHandle);
Thread.Sleep(200);
}
});
}
}
策略组合配置示例:
xml
<add processNamePattern="notepad++"
addictionTime="300"
soundAlert="Alarm01.wav"
messageBoxText="该休息了!"
showMessageBox="true"/>
四、工程实践亮点
4.1 资源管理
- 实现IDisposable接口
- 使用using语句确保资源释放
- 定时器生命周期控制
4.2 并发控制
- 锁机制保护共享资源
csharp
lock (_lock)
{
// 访问_monitorItems
}
- 异步策略执行避免UI阻塞
4.3 可观测性
- 日志跟踪系统状态
- 异常捕获与处理
csharp
catch (Exception ex) when (ex is Win32Exception || ex is InvalidOperationException)
{
// 处理进程访问异常
}
五、使用与扩展
5.1 配置示例
xml
<ProcessItems>
<add processNamePattern="game*.exe"
addictionTime="900"
soundAlert="alert.wav"
messageBoxText="游戏时间已达15分钟"
showMessageBox="true"/>
</ProcessItems>
5.2 扩展新策略
- 实现IAlertStrategy接口
- 在配置中添加新策略参数
- 在工厂方法中创建策略实例
示例(邮件提醒策略):
csharp
public class EmailAlertStrategy : IAlertStrategy
{
public Task ExecuteAsync(AlertContext context)
{
return Task.Run(() =>
{
// 发送邮件逻辑
});
}
}
六、完整代码
- AppForm.cs
C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AntiAddictionAides
{
public partial class AppForm : Form
{
#region 函数
public AppForm()
{
InitializeComponent();
#region 初始化
Inst = this;
var headers = new string[] { "序号", "进程信息", "防沉迷时间(秒)", "声音提示", "弹框提示", "窗口抖动" };
foreach(var v in headers)
{
this.listViewProcess.Columns.Add(v).TextAlign = HorizontalAlignment.Center;
}
this.listViewProcess.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
this.listViewProcess.Columns[1].Width = 150;
this.listViewProcess.Columns[3].Width = 150;
this.listViewProcess.Columns[4].Width = 400;
this.listViewProcess.Columns[5].Width = 96;
// 读取配置项
_processMonitor = new ProcessMonitor();
var configItems = LoadProcessConfig();
foreach(var v in configItems)
{
var lv = this.listViewProcess.Items.Add($"{this.listViewProcess.Items.Count + 1}");
lv.SubItems.AddRange(new string[] {
v.ProcessNamePattern,
v.AddictionTime.ToString(),
v.SoundAlert,
v.MessageBoxText,
(v.ShowMessageBox ? "是" : "否")
});
var monitor = _processMonitor.SetMonitor(v.ProcessNamePattern, TimeSpan.FromSeconds(v.AddictionTime));
if(!string.IsNullOrWhiteSpace(v.SoundAlert))
{
monitor.AddAlert(new SoundAlertStrategy(v.SoundAlert));
}
if (!string.IsNullOrWhiteSpace(v.MessageBoxText))
{
monitor.AddAlert(new MessageBoxAlertStrategy(v.MessageBoxText));
}
if(v.ShowMessageBox)
{
monitor.AddAlert(new WindowShakeStrategy(this.Handle));
}
}
#endregion
}
public static void Log(string info)
{
lock(Inst)
{
Console.WriteLine($"[{DateTime.Now}]{info}");
}
}
#endregion
#region 内部函数
static List<ProcessItem> LoadProcessConfig()
{
var config = new List<ProcessItem>();
try
{
var section = ConfigurationManager.GetSection("ProcessMonitor") as ProcessMonitorSection;
foreach (ProcessItemElement element in section.ProcessItems)
{
config.Add(new ProcessItem
{
ProcessNamePattern = element.ProcessNamePattern,
AddictionTime = element.AddictionTime,
SoundAlert = element.SoundAlert,
MessageBoxText = element.MessageBoxText,
ShowMessageBox = element.ShowMessageBox
});
}
}
catch (Exception ex)
{
AppForm.Log($"配置加载失败: {ex.Message}");
}
return config;
}
class ProcessItem
{
public string ProcessNamePattern { get; set; }
public int AddictionTime { get; set; }
public string SoundAlert { get; set; }
public string MessageBoxText { get; set; }
public bool ShowMessageBox { get; set; }
}
#endregion
#region 属性变量
ProcessMonitor _processMonitor;
public static AppForm Inst { get; set; }
#endregion
#region 事件
private void buttonStart_Click(object sender, EventArgs e)
{
if(buttonStart.Text == "启动")
{
_processMonitor.Start();
buttonStart.Text = "停止";
}
else
{
_processMonitor.Stop();
buttonStart.Text = "启动";
}
}
#endregion
}
#region 配置
public class ProcessMonitorSection : ConfigurationSection
{
[ConfigurationProperty("ProcessItems")]
public ProcessItemCollection ProcessItems =>
(ProcessItemCollection)this["ProcessItems"];
}
public class ProcessItemCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement() =>
new ProcessItemElement();
protected override object GetElementKey(ConfigurationElement element) =>
((ProcessItemElement)element).ProcessNamePattern;
}
public class ProcessItemElement : ConfigurationElement
{
[ConfigurationProperty("processNamePattern", IsRequired = true)]
public string ProcessNamePattern =>
(string)this["processNamePattern"];
[ConfigurationProperty("addictionTime", IsRequired = true)]
public int AddictionTime =>
(int)this["addictionTime"];
[ConfigurationProperty("soundAlert", IsRequired = false)]
public string SoundAlert =>
(string)this["soundAlert"];
[ConfigurationProperty("messageBoxText", IsRequired = false)]
public string MessageBoxText =>
(string)this["messageBoxText"];
[ConfigurationProperty("showMessageBox", IsRequired = true)]
public bool ShowMessageBox =>
(bool)this["showMessageBox"];
}
#endregion
}
- ProcessMonitor.cs
C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Media;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace AntiAddictionAides
{
public class ProcessMonitor : IDisposable
{
private readonly Timer _timer;
private readonly Dictionary<string, MonitorItem> _monitorItems = new Dictionary<string, MonitorItem>();
private readonly object _lock = new object();
private bool _disposed;
public ProcessMonitor()
{
_timer = new Timer(CheckProcesses, null, Timeout.Infinite, Timeout.Infinite);
}
public void Start()
{
// 每30秒检查一次
_timer.Change(0, 5000);
}
public void Stop()
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
public MonitorItem SetMonitor(string processNamePattern, TimeSpan threshold)
{
var regex = ConvertWildcardToRegex(processNamePattern);
var monitor = new MonitorItem(regex, threshold);
lock (_lock)
{
_monitorItems[processNamePattern] = monitor;
}
AppForm.Log($"添加监控,进程:{processNamePattern} 沉迷时间:{threshold.TotalSeconds} 秒");
return monitor;
}
public void RemoveMonitor(string processNamePattern)
{
lock (_lock)
{
if (_monitorItems.TryGetValue(processNamePattern, out var item))
{
item.Dispose();
_monitorItems.Remove(processNamePattern);
}
}
AppForm.Log($"删除监控,进程:{processNamePattern}");
}
private void CheckProcesses(object state)
{
lock (_lock)
{
foreach (var item in _monitorItems.Values)
{
AppForm.Log($"任务检查,进程:{item.ProcessNameRegex}");
foreach (var process in Process.GetProcesses())
{
try
{
if (!item.ProcessNameRegex.IsMatch(process.ProcessName))
{
continue;
}
item.Alert(process);
}
catch (Exception ex) when (ex is Win32Exception || ex is InvalidOperationException)
{
// 处理进程访问异常
}
finally
{
process.Dispose();
}
}
}
}
}
private Regex ConvertWildcardToRegex(string pattern)
{
return new Regex("^" +
Regex.Escape(pattern).Replace("\\*", ".*").Replace("\\?", ".") +
"$", RegexOptions.IgnoreCase);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
Stop();
foreach (var item in _monitorItems.Values)
{
item.Dispose();
}
_timer.Dispose();
}
_disposed = true;
}
}
~ProcessMonitor()
{
Dispose(false);
}
public class MonitorItem
{
public Dictionary<int, AlertContext> ProcessAlert { get; set; }
public Regex ProcessNameRegex { get; }
public TimeSpan Threshold { get; }
public List<IAlertStrategy> Alerts { get; private set; }
public MonitorItem(Regex regex, TimeSpan threshold, IAlertStrategy[] alerts = null)
{
ProcessNameRegex = regex;
Threshold = threshold;
Alerts = new List<IAlertStrategy>();
if (alerts != null)
{
Alerts.AddRange(alerts);
}
ProcessAlert = new Dictionary<int, AlertContext>();
}
AlertContext GetAlertContext(Process process, int pid=0, string pname=null)
{
AlertContext context;
lock (ProcessAlert)
{
if (!ProcessAlert.TryGetValue(process.Id, out context) || process.ProcessName != context.ProcessName)
{
if(pid > 0 && !string.IsNullOrWhiteSpace(pname))
{
context = new AlertContext()
{
ProcessId = pid,
ProcessName = pname,
LastPlayed = DateTime.Now
};
ProcessAlert[pid] = context;
}
}
}
return context;
}
public void Alert(Process process)
{
var pinfo = $"[{process.Id}-{process.ProcessName}]";
AlertContext context = GetAlertContext(process, process.Id, process.ProcessName);
if (context == null)
{
AppForm.Log($"获取告警上下文失败:{pinfo}");
return;
}
AppForm.Log($"获取{pinfo}上下文信息,告警数:{context.AlertNum} 最近时间:{context.LastPlayed}");
if (context.AlertNum > 3)
{
AppForm.Log($"进程{pinfo}告警数{context.AlertNum}超限,已忽略");
return;
}
context.RunTime = DateTime.Now - process.StartTime;
if (context.RunTime < Threshold)
{
AppForm.Log($"进程{pinfo}未达沉迷时限,已忽略");
return;
}
if (DateTime.Now - context.LastPlayed < TimeSpan.FromSeconds(10))
{
AppForm.Log($"进程{pinfo}仍在提醒期限内,已忽略");
return;
}
context.AlertNum++;
context.LastPlayed = DateTime.Now;
AppForm.Log($"进程{pinfo}在{context.LastPlayed}触发提醒{context.AlertNum}");
// 默认提示音
if (Alerts.Count == 0)
{
new SoundAlertStrategy("Alarm01.wav").ExecuteAsync(context);
}
else
{
foreach(var v in Alerts)
{
v.ExecuteAsync(context);
}
}
}
public int AddAlert(IAlertStrategy alert)
{
int idx = 0;
lock(Alerts)
{
Alerts.Add(alert);
idx = Alerts.Count;
}
return idx;
}
public void RemoveAlert(int idx)
{
if(idx < 0 || idx >= Alerts.Count)
{
return;
}
lock (Alerts)
{
Alerts.RemoveAt(idx);
}
}
public void Dispose()
{
foreach(var v in Alerts)
{
if(v is SoundAlertStrategy sa)
{
sa.Dispose();
}
}
}
}
}
public class WinMediaWav
{
public static readonly string MediaPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Media");
public static string[] Wavs
{
get
{
return null;
}
}
public static string Wav(string name)
{
var file = Path.Combine(MediaPath, name);
var files = new string[] { name, file, file + ".wav" };
foreach(var v in files)
{
if (File.Exists(v))
{
return v;
}
}
return null;
}
}
public interface IAlertStrategy
{
Task ExecuteAsync(AlertContext context);
}
public class SoundAlertStrategy : IAlertStrategy
{
private readonly SoundPlayer _player;
public SoundAlertStrategy(string soundFilePath)
{
_player = new SoundPlayer(WinMediaWav.Wav(soundFilePath));
}
public Task ExecuteAsync(AlertContext context)
{
AppForm.Log($"执行提示音{context.RunTime} {context.ProcessId}-{context.ProcessName}");
return Task.Run(() => _player.PlaySync());
}
public void Dispose()
{
_player.Dispose();
}
}
public class MessageBoxAlertStrategy : IAlertStrategy
{
private string Text { get; set; }
public MessageBoxAlertStrategy(string text)
{
Text = text;
}
public Task ExecuteAsync(AlertContext context)
{
return Task.Run(() =>
{
AppForm.Log($"执行弹框提醒{context.RunTime} {context.ProcessId}-{context.ProcessName}");
var result = System.Windows.Forms.MessageBox.Show(
Text ?? $"老铁,醒醒,该休息啦!",
$"防沉迷提醒-{context.ProcessName} 沉迷时间:{context.RunTime}",
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Information);
});
}
}
public class WindowShakeStrategy : IAlertStrategy
{
private readonly IntPtr _windowHandle;
public WindowShakeStrategy(IntPtr hWnd)
{
_windowHandle = hWnd;
}
public async Task ExecuteAsync(AlertContext context)
{
await Task.Run(() =>
{
AppForm.Log($"执行窗口抖动提醒{context.RunTime} {context.ProcessId}-{context.ProcessName}");
for (int i = 0; i < 3; i++)
{
NativeMethods.ShakeWindow(_windowHandle);
Thread.Sleep(200);
}
});
}
}
public class AlertContext
{
public int ProcessId { get; set; }
public string ProcessName { get; set; }
public TimeSpan RunTime { get; set; }
public DateTime LastPlayed { get; set; } = DateTime.MinValue;
public int AlertNum { get; set; }
}
internal static class NativeMethods
{
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
public static void ShakeWindow(IntPtr hWnd)
{
const uint SWP_NOSIZE = 0x0001;
const uint SWP_NOZORDER = 0x0004;
const int SHAKE_DISTANCE = 10; // 抖动幅度
const int SHAKE_SPEED = 20; // 抖动速度(毫秒)
// 获取原始窗口位置
if (!GetWindowRect(hWnd, out RECT originalRect))
return;
int originalX = originalRect.Left;
int originalY = originalRect.Top;
// 抖动动画
for (int i = 0; i < 3; i++)
{
// 右移
SetWindowPos(hWnd, IntPtr.Zero,
originalX + SHAKE_DISTANCE, originalY,
0, 0, SWP_NOSIZE | SWP_NOZORDER);
System.Threading.Thread.Sleep(SHAKE_SPEED);
// 左移
SetWindowPos(hWnd, IntPtr.Zero,
originalX - SHAKE_DISTANCE, originalY,
0, 0, SWP_NOSIZE | SWP_NOZORDER);
System.Threading.Thread.Sleep(SHAKE_SPEED);
}
// 恢复原始位置
SetWindowPos(hWnd, IntPtr.Zero,
originalX, originalY,
0, 0, SWP_NOSIZE | SWP_NOZORDER);
}
}
}
- App.Config
xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- 必须放在最前面 -->
<configSections>
<section name="ProcessMonitor" type="AntiAddictionAides.ProcessMonitorSection, AntiAddictionAides" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<ProcessMonitor>
<ProcessItems>
<add processNamePattern="notepad++" addictionTime="300" soundAlert="Alarm01.wav" messageBoxText="该休息了!" showMessageBox="true" />
<add processNamePattern="chrome*" addictionTime="3600" soundAlert="Alarm02.wav" messageBoxText="注意用眼健康" showMessageBox="false" />
</ProcessItems>
</ProcessMonitor>
</configuration>