前言
本文将深入分析一个使用 Delphi 11.3 编写的定时关机控制台程序。通过这个实例,我们将学习如何处理 Windows 系统权限、时间计算、用户交互以及控制台中文显示等关键技术。
“C:\delphisource\autoshutdown\temp_delphi_source\AutoShutdown.dproj”
程序概述
这是一个简洁实用的控制台应用程序,允许用户设置具体的关机时间(如 23:30),程序将显示倒计时并在指定时间自动关闭计算机。
核心功能
- 用户输入目标关机时间(HH:MM 格式)
- 智能日期判断(过期时间自动顺延至次日)
- 实时倒计时显示
- 获取系统关机权限
- 执行强制关机
源码详细解析
1. 程序头部与引用单元
program AutoShutdown;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
System.DateUtils,
Winapi.Windows;
关键点:
{$APPTYPE CONSOLE}:指定为控制台应用程序System.SysUtils:提供基础系统工具函数System.DateUtils:提供日期时间计算函数Winapi.Windows:提供 Windows API 访问
2. 控制台编码设置
procedure SetConsoleUTF8;
begin
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
end;
功能说明:
这个函数解决了 Windows 控制台中文乱码问题。Windows 控制台默认使用 GBK 编码(代码页 936),而 Delphi 现代版本默认使用 UTF-8 编码。
SetConsoleOutputCP(CP_UTF8):设置控制台输出编码为 UTF-8SetConsoleCP(CP_UTF8):设置控制台输入编码为 UTF-8CP_UTF8是 Windows 定义的常量,值为 65001
为什么需要这个?
如果不设置,程序中的中文字符会显示为乱码,因为编码不匹配。
3. Windows 关机常量定义
const
EWX_SHUTDOWN = $00000001;
EWX_FORCE = $00000004;
EWX_FORCEIFHUNG = $00000010;
这些是 Windows API 中 ExitWindowsEx 函数使用的标志位:
- EWX_SHUTDOWN ($01):关闭计算机
- EWX_FORCE ($04):强制关闭所有应用程序
- EWX_FORCEIFHUNG ($10):强制关闭无响应的应用程序
位运算组合:
EWX_SHUTDOWN or EWX_FORCE
使用 or 运算符组合多个标志,实现"强制关机"效果。
4. 获取关机权限
function EnableShutdownPrivilege: Boolean;
var
hToken: THandle;
tkp: TTokenPrivileges;
ReturnLength: Cardinal;
begin
Result := False;
if OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, hToken) then
begin
try
if LookupPrivilegeValue(nil, 'SeShutdownPrivilege', tkp.Privileges[0].Luid) then
begin
tkp.PrivilegeCount := 1;
tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
Result := AdjustTokenPrivileges(hToken, False, tkp, 0, nil, ReturnLength);
end;
finally
CloseHandle(hToken);
end;
end;
end;
深度解析:
这是程序最核心的部分之一。Windows 系统关机需要特殊权限,即使是管理员账户也需要显式启用。
执行流程:
-
打开进程令牌
OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, hToken)GetCurrentProcess:获取当前进程句柄TOKEN_ADJUST_PRIVILEGES:请求调整权限的权限TOKEN_QUERY:请求查询权限的权限hToken:返回的令牌句柄
-
查找关机权限
LookupPrivilegeValue(nil, 'SeShutdownPrivilege', tkp.Privileges[0].Luid)'SeShutdownPrivilege':Windows 系统的关机权限名称Luid:本地唯一标识符(Locally Unique Identifier)
-
启用权限
tkp.PrivilegeCount := 1; tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, False, tkp, 0, nil, ReturnLength);- 设置要调整的权限数量为 1
- 设置权限属性为"启用"
- 调用 API 应用更改
-
资源清理
finally CloseHandle(hToken); end;使用
try-finally确保令牌句柄被正确关闭,防止资源泄漏。
为什么需要这个?
Windows 安全模型要求即使是管理员也必须显式请求某些敏感操作的权限。这是纵深防御策略的一部分。
5. 执行关机操作
procedure PerformShutdown;
begin
if EnableShutdownPrivilege then
begin
Writeln('正在关机...');
ExitWindowsEx(EWX_SHUTDOWN or EWX_FORCE, 0);
end
else
Writeln('错误: 无法获取关机权限!');
end;
执行逻辑:
- 先尝试获取关机权限
- 如果成功,调用
ExitWindowsExAPI 执行关机 - 如果失败,显示错误信息
参数说明:
- 第一个参数:
EWX_SHUTDOWN or EWX_FORCE(强制关机) - 第二个参数:
0(关机原因,0 表示其他原因)
6. 时间解析函数
function ParseTimeInput(const Input: string; out ShutdownTime: TDateTime): Boolean;
var
Hour, Minute: Integer;
Parts: TArray<string>;
begin
Result := False;
Parts := Input.Split([':']);
if Length(Parts) <> 2 then
Exit;
if not TryStrToInt(Parts[0], Hour) or not TryStrToInt(Parts[1], Minute) then
Exit;
if (Hour < 0) or (Hour > 23) or (Minute < 0) or (Minute > 59) then
Exit;
ShutdownTime := EncodeTime(Hour, Minute, 0, 0);
ShutdownTime := Date + ShutdownTime;
// 如果设定时间已过,则设为明天
if ShutdownTime <= Now then
ShutdownTime := IncDay(ShutdownTime, 1);
Result := True;
end;
功能详解:
这个函数负责将用户输入的字符串(如"23:30")转换为实际的日期时间值。
处理步骤:
-
字符串分割
Parts := Input.Split([':']);使用冒号分割输入,得到小时和分钟部分。
-
格式验证
if Length(Parts) <> 2 then Exit;确保分割后正好有两部分。
-
数值转换
if not TryStrToInt(Parts[0], Hour) or not TryStrToInt(Parts[1], Minute) then Exit;使用
TryStrToInt安全地转换字符串为整数,失败返回 False。 -
范围检查
if (Hour < 0) or (Hour > 23) or (Minute < 0) or (Minute > 59) then Exit;验证小时(0-23)和分钟(0-59)的合法性。
-
构建时间
ShutdownTime := EncodeTime(Hour, Minute, 0, 0); ShutdownTime := Date + ShutdownTime;EncodeTime:创建时间部分(时、分、秒、毫秒)Date:获取今天的日期- 相加得到完整的日期时间
-
智能日期处理
if ShutdownTime <= Now then ShutdownTime := IncDay(ShutdownTime, 1);如果设定时间已经过去,自动顺延到明天同一时间。
设计亮点:
- 使用
out参数返回解析结果 - 返回布尔值表示解析是否成功
- 多重验证确保数据有效性
- 智能处理跨日期情况
7. 倒计时显示函数
procedure ShowCountdown(TargetTime: TDateTime);
var
Remaining: Int64;
Hours, Minutes, Seconds: Integer;
begin
Remaining := SecondsBetween(TargetTime, Now);
if Remaining > 0 then
begin
Hours := Remaining div 3600;
Minutes := (Remaining mod 3600) div 60;
Seconds := Remaining mod 60;
Write(Format(#13'距离下次关机: %2.2d:%2.2d:%2.2d', [Hours, Minutes, Seconds]));
end;
end;
技术要点:
-
时间差计算
Remaining := SecondsBetween(TargetTime, Now);SecondsBetween返回两个时间点之间的秒数差。 -
时分秒转换
Hours := Remaining div 3600; // 总秒数除以3600得到小时 Minutes := (Remaining mod 3600) div 60; // 余数除以60得到分钟 Seconds := Remaining mod 60; // 再取余得到秒数经典的时间单位转换算法。
-
原地刷新显示
Write(Format(#13'距离下次关机: %2.2d:%2.2d:%2.2d', [Hours, Minutes, Seconds]));#13:回车符(CR),使光标返回行首Write(而非Writeln):不换行输出%2.2d:格式化为两位数字,不足补零- 效果:倒计时在同一行不断刷新
显示效果:
距离下次关机: 01:30:45
数字会每秒更新,但不产生新行。
8. 主程序逻辑
var
ShutdownTime: TDateTime;
UserInput: string;
begin
try
SetConsoleUTF8;
Writeln('========================================');
Writeln(' 定时关机程序 v1.0');
Writeln('========================================');
Writeln;
// 获取用户输入的关机时间
Write('请输入关机时间(格式 HH:MM,如 23:30): ');
Readln(UserInput);
if not ParseTimeInput(UserInput, ShutdownTime) then
begin
Writeln('错误: 请输入有效的时间格式(HH:MM)!');
Writeln('按回车键退出...');
Readln;
Exit;
end;
Writeln;
Writeln(Format('已设置关机时间: %s', [FormatDateTime('yyyy-mm-dd hh:nn:ss', ShutdownTime)]));
Writeln('按 Ctrl+C 可随时取消');
Writeln('========================================');
Writeln;
// 倒计时循环
while Now < ShutdownTime do
begin
ShowCountdown(ShutdownTime);
Sleep(1000); // 每秒更新一次
end;
Writeln;
Writeln;
// 执行关机
PerformShutdown;
except
on E: Exception do
begin
Writeln('发生错误: ', E.Message);
Writeln('按回车键退出...');
Readln;
end;
end;
end.
程序流程分析:
-
初始化
- 设置 UTF-8 编码
- 显示程序标题
-
用户输入
- 提示输入格式
- 读取用户输入
- 解析并验证
-
错误处理
if not ParseTimeInput(UserInput, ShutdownTime) then begin Writeln('错误: 请输入有效的时间格式(HH:MM)!'); Writeln('按回车键退出...'); Readln; Exit; end;输入无效时给出清晰提示并等待用户确认退出。
-
倒计时循环
while Now < ShutdownTime do begin ShowCountdown(ShutdownTime); Sleep(1000); end;- 每秒检查一次时间
- 调用显示函数更新倒计时
Sleep(1000):暂停1秒,避免CPU满负荷
-
执行关机
时间到达后调用PerformShutdown执行关机。 -
全局异常处理
except on E: Exception do begin Writeln('发生错误: ', E.Message); Writeln('按回车键退出...'); Readln; end; end;捕获所有未处理的异常,显示错误信息并优雅退出。
技术亮点总结
1. 安全性设计
- 权限检查和获取机制
- 多重输入验证
- 完善的错误处理
2. 用户体验
- 清晰的界面提示
- 实时倒计时反馈
- 智能的日期处理
3. 代码质量
- 结构清晰,职责分明
- 资源管理规范(try-finally)
- 异常处理完善
4. Windows API 集成
- 进程令牌操作
- 权限管理
- 系统关机控制
使用注意事项
必须以管理员权限运行
即使代码中获取了关机权限,程序本身也必须以管理员身份运行才能成功。
原因:
OpenProcessToken需要进程有足够权限- 普通用户权限无法调整令牌权限
如何运行:
- 右键点击 exe 文件
- 选择"以管理员身份运行"
- 或在项目属性中设置需要管理员权限
时间格式严格
- 必须使用
HH:MM格式 - 小时:00-23(24小时制)
- 分钟:00-59
- 必须有冒号分隔
强制关机说明
程序使用 EWX_FORCE 标志,会:
- 强制关闭所有程序
- 不保存未保存的工作
- 不给用户确认机会
建议:
测试时可以将关机代码注释,用输出语句代替。
可能的改进方向
1. 功能增强
- 添加取消关机的热键(非 Ctrl+C)
- 支持休眠、重启等其他操作
- 添加日志记录
- 支持配置文件
2. 用户界面
- 添加倒计时结束前的警告提示
- 支持多种时间输入格式
- 显示更详细的状态信息
3. 安全改进
- 添加密码保护
- 记录操作日志
- 支持远程取消
4. 代码优化
// 可以添加取消机制
function CheckCancelKey: Boolean;
var
InputRec: TInputRecord;
NumRead: Cardinal;
begin
Result := False;
if PeekConsoleInput(GetStdHandle(STD_INPUT_HANDLE), InputRec, 1, NumRead) then
begin
if (NumRead > 0) and (InputRec.EventType = KEY_EVENT) then
Result := InputRec.Event.KeyEvent.wVirtualKeyCode = VK_ESCAPE;
end;
end;
学习价值
通过这个项目,我们学到了:
-
Windows API 编程
- 进程和令牌管理
- 权限操作
- 系统控制
-
Delphi 编程技巧
- 控制台应用开发
- 字符串处理
- 日期时间操作
-
编程最佳实践
- 错误处理
- 资源管理
- 用户体验设计
-
系统编程知识
- Windows 安全模型
- 权限提升
- 系统调用
运行结果

1741

被折叠的 条评论
为什么被折叠?



