xRemindService

一个可以自定义提醒标题、内容、时间的项目。

因为想每个小时提醒自己喝水就写了一个项目来搞这个。

开发

需求

  • 可以在windows界面弹窗
  • 自定义提醒时间、内容和标题
  • 重复打开时提示

难点

没有调用windowsAPI的经验

实现

弹窗及隐藏控制台

因为没有调用windowsAPI的经验,所以开始bing查询如何调用 windowsAPI才能弹出窗口。

  • 弹窗
    • 通过kernel32.dllWTSGetActiveConsoleSessionId来获取当前显示的桌面所在的SessionID
    • 通过wtsapi32.dllWTSSendMessage来让 Session 弹出对话框
  • 隐藏控制台
    • 通过kernel32.dllGetConsoleWindow来检索与调用进程关联的控制台使用的窗口句柄
    • 通过user32.dllShowWindow来控制窗口打状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public static class Interop
{
public static IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;

public static void ShowMessageBox(string message, string title)
{
int resp = 0;
WTSSendMessage(
WTS_CURRENT_SERVER_HANDLE,
WTSGetActiveConsoleSessionId(),
title, title.Length,
message, message.Length,
0, 0, out resp, false);
}

[DllImport("kernel32.dll", SetLastError = true)]
public static extern int WTSGetActiveConsoleSessionId();

[DllImport("wtsapi32.dll", SetLastError = true)]
public static extern bool WTSSendMessage(
IntPtr hServer,
int SessionId,
String pTitle,
int TitleLength,
String pMessage,
int MessageLength,
int Style,
int Timeout,
out int pResponse, bool bWait);

public static void Hide()
{
ShowWindow(GetConsoleWindow(), SW_HIDE);
}

[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();

[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

const int SW_HIDE = 0;
const int SW_SHOW = 5;
}

定时 Job

使用了以前用过的Quartz,简单说一下就是新建 Job,定义触发器,放入调度器,等待任务执行就好了。需要注意的点是在定义 Job 的时候给需要执行的 Job 传参。使用了 JobDataMap.Put 来实现。具体配置在下面。

读取配置文件

因为可能配置多个 Job 所以在appsettings.json中定义了一个CronJob的属性,下面包含多个子 Job。子 Job 中包含三个属性TitleCronSchedulerRemindString这样就能实现我的要求了。
通过使用IConfiguration来获取appsettings.json的属性和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var jobs = _configuration.GetSection(Constants.CronJob).GetChildren();
foreach (var cronJob in jobs)
{
var jobName = cronJob.Key;
var triggerName = $"{jobName}Trigger";
var groupName = $"{jobName}Group";
var title = _configuration.GetSection($"{Constants.CronJob}:{jobName}:{Constants.Title}").Value;
var cronScheduler = _configuration.GetSection($"{Constants.CronJob}:{jobName}:{Constants.CronScheduler}").Value;
var remindString = _configuration.GetSection($"{Constants.CronJob}:{jobName}:{Constants.RemindString}").Value;

var factory = new StdSchedulerFactory();
var scheduler = await factory.GetScheduler();

await scheduler.Start();

var job = JobBuilder.Create<RemindJob>()
.WithIdentity(jobName, groupName)
.Build();

job.JobDataMap.Put(Constants.RemindString, remindString);
job.JobDataMap.Put(Constants.Title, title);

var trigger = TriggerBuilder.Create()
.WithIdentity(triggerName, groupName)
.StartNow()
.WithCronSchedule(cronScheduler)
.Build();

await scheduler.ScheduleJob(job, trigger);
}

防多开

使用 System.Threading.Mutex 来实现。实现代码如下

Mutex(Boolean, String, Boolean)
使用可指示调用线程是否应具有互斥体的初始所有权以及字符串是否为互斥体的名称的 Boolean 值和当线程返回时可指示调用线程是否已赋予互斥体的初始所有权的 Boolean 值初始化 Mutex 类的新实例。
官网

参数

initiallyOwned Boolean

如果为 true,则给予调用线程已命名的系统互斥体的初始所属权(如果已命名的系统互斥体是通过此调用创建的);否则为 false。

name String

如果要与其他进程共享同步对象,则为名称;否则为 null 或空字符串。 该名称区分大小写。

createdNew Boolean

在此方法返回时,如果创建了局部互斥体(即,如果 name 为 null 或空字符串)或指定的命名系统互斥体,则包含布尔值 true;如果指定的命名系统互斥体已存在,则为 false。 此参数未经初始化即被传递。

1
2
3
4
5
6
7
8
9
10
public static void Main(string[] args)
{
new Mutex(true, System.Diagnostics.Process.GetCurrentProcess().ProcessName, out bool isAppRunning);
if (!isAppRunning)
{
Interop.ShowMessageBox("重复开启", "提示");
Environment.Exit(1);
}
CreateHostBuilder(args).Build().Run();
}

使用说明

appsettings.json中含有默认的提醒,可以随意添加

1
2
3
4
5
6
7
8
9
10
11
12
"CronJob": {
"RelaxJob": {
"Title": "休息提醒",
"CronScheduler": "0 0 * * * ?",
"RemindString": "整点了休息一下吧!!!"
},
"GoodJob": {
"Title": "工作提醒",
"CronScheduler": "0 2 * * * ?",
"RemindString": "该努力了!!!"
}
}
  • RelaxJob是 Job 名称可随意定义,包含三个属性TitleCronSchedulerRemindString
    • Title是提醒标题 必填
    • CronScheduler是提醒时间 Cron 表达式 必填
    • RemindString是提醒内容 必填

安装

可以安装成服务这样不用每天都启动项目了

  1. 项目发布
  2. 我选择的是
    • 配置:Release|Any CPU
    • 目标框架:net5.0
    • 部署模式:独立
    • 目标运行时:win-x64
    • 文件发布选项:勾选 生成单个文件
  3. 本人提供一个简单的安装方式
    • 在发布目录下新建两个 bat 文件,一个setup.bat 一个unsetup.bat内容如下:
      • setup.bat
        1
        2
        3
        4
        5
        6
        7
        @echo off
        set path=%~dp0
        echo %path%
        sc.exe create xRemindService binPath=%path%xRemindService.exe displayname= xRemindService
        net start xRemindService
        sc.exe config xRemindService start= AUTO
        pause
      • unsetup.bat
        1
        2
        3
        4
        @echo off
        sc.exe stop xRemindService
        sc.exe delete xRemindService
        pause
    • 通过管理员运行setup.bat进行安装服务,unsetup.bat进行卸载服务
    • 如果修改配置文件需要重启服务

项目地址

https://github.com/KanekiQAQ/xRemindService

TO DO

  • 新增 Web 添加 Job (替代原有appsettings.json的配置) 不一定做不做