项目 5 补齐:旧版线程 API(Suspend / Resume / Abort)与 ListBox
- 编译时把 Thread 的 Suspend / Resume / Abort、ThreadState、Sleep、new Thread 改写成平台的 协作式实现:被挂起或终止的线程在 Sleep、后台改控件、调设备类库、弹提示框时停下或结束; ThreadStateException 等行为与 .NET Framework 一致;编辑器不再提示"已过时" - ListBox:列表项、单选、SelectedIndexChanged 先于 Click、删除选中项后取消选中; ComboBox 与 ListBox 共用 ListControl 和列表项集合;设计器工具箱加"列表框" - 评分规则的"包含"对列表框检查所有列表项 - 示例:项目 5 任务 2(烟雾,线程)、任务 3(推送灾情,二维数组、两个窗体、ListBox), 参考答案修正书上直接 Abort 挂起线程、烟雾记成"发现火情"的问题 - 一直不换行的 Console 输出攒满 4096 字符先发出,避免占满内存 - 测试:旧版线程 API、ListBox 事件顺序、两个示例的端到端和评分测试 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8a92354fe4
commit
91b39603a2
36 files changed
+1648
-100
No files matched your search
@@ -66,14 +66,15 @@ Docker Compose,HTTPS 由服务器上的 Nginx Proxy Manager 负责。推送到
|
||||
已完成:
|
||||
|
||||
- **虚拟实训台**:按教材实训台布局图排列(红外对射、警示灯、LED 屏、照明灯、人体红外、温湿度、光照、烟雾、火焰、ADAM 模块)。每个浏览器会话一台,状态由服务器统一保存:传感器读数由网页上的滑块和开关设定,经服务器推给学生程序;灯、继电器、LED 屏由学生程序控制。
|
||||
- 新控件:CheckBox、RadioButton(事件顺序与 WinForms 一致),教材控件库的 Lamp、Alarm。
|
||||
- 示例工程(按书上代码整理):项目 1 任务 1–2、项目 2 任务 1–3、项目 3 任务 1–2、项目 4 任务 1 和 3、项目 5 任务 1,共 10 个。网址带 `?sample=示例编号` 可直接打开指定示例。
|
||||
- 新控件:CheckBox、RadioButton(事件顺序与 WinForms 一致)、ListBox,教材控件库的 Lamp、Alarm。
|
||||
- 示例工程(按书上代码整理):项目 1 任务 1–2、项目 2 任务 1–3、项目 3 任务 1–2、项目 4 任务 1 和 3、项目 5 任务 1–3,共 12 个。网址带 `?sample=示例编号` 可直接打开指定示例。
|
||||
- 代码编辑器(Monaco):C# 高亮、编译错误波浪线、点错误跳到对应行、F5 运行 / Shift+F5 停止、草稿自动保存在浏览器本地。
|
||||
- 窗体设计器(仿 VS,界面中文):工具箱、拖放与改大小、属性窗口、事件窗口、双击生成事件处理方法、撤销 / 重做;读写 VS 格式的 Designer.cs。
|
||||
- 多窗体:新建、删除窗体;运行时多个窗口可切换前后。
|
||||
- 部署:bubblewrap 沙箱(断网、只读、看不到其他进程,限制内存、CPU 时间和运行时长)、编译时禁止网络 / 反射 / 启动进程等 API、Docker Compose、GitHub Actions 自动部署。
|
||||
- 第 2 阶段(进行中):账号登录(本机开发时老师账号为 admin / admin)、班级管理与 CSV 名单导入、学生作品保存在服务器上;自动评分(每个示例的 `checks.yaml`);布置作业、学生提交后自动评分;老师评阅(看代码和界面截图、运行学生的程序、老师评分项打分、评语)、AI 评阅建议(DeepSeek,老师采纳后生效)、全班成绩表与 CSV 导出。
|
||||
- 第 3 阶段(进行中):教材项目 5 用到的旧版线程 API(`Thread.Suspend` / `Resume` / `Abort`)编译时改写成平台的协作式实现(`runtime/Sim.Runner/LegacyThreadRewriter.cs`、`runtime/Sim.WinForms/Host/LegacyThreads.cs`)。
|
||||
|
||||
尚未完成:
|
||||
|
||||
- 教材项目 4–8 的其余任务(需要摄像头、线程 API、ListBox、数据库、TrackBar、PictureBox、图表)、账号和班级、自动评测。
|
||||
- 教材项目 4 任务 2 和项目 6–8(需要摄像头、数据库、TrackBar、PictureBox、图表)。
|
||||
+8
-4
@@ -166,7 +166,7 @@ IOT/
|
||||
| 后端 | 登录 | 计划:Cookie 会话 + `Bun.password` |
|
||||
| 后端 | 参数校验 | 计划:Zod |
|
||||
| C# 运行时 | 平台 | .NET 10 LTS |
|
||||
| C# 运行时 | 编译 | Roslyn,常驻编译服务,内存编译,中文报错;计划:编译时改写旧版线程 API |
|
||||
| C# 运行时 | 编译 | Roslyn,常驻编译服务,内存编译,中文报错;编译时改写旧版线程 API(Suspend / Resume / Abort) |
|
||||
| C# 运行时 | 运行器 | .NET 控制台程序,模拟界面线程和消息循环 |
|
||||
| C# 运行时 | 仿真类库 | 自研:`System.Windows.Forms` 子集 + 教材设备类库 + `Font` 等少量 System.Drawing 类型 |
|
||||
| C# 运行时 | 数据库仿真 | 计划:仿冒 `System.Data.SqlClient`,底层 Microsoft.Data.Sqlite;T-SQL 用 ScriptDom 解析后转写 |
|
||||
@@ -211,7 +211,7 @@ IOT/
|
||||
| 2 环境监测 | LED 屏推送文字;4017 采集温湿度和光照;采集后推送 LED | LED 控件、ledHelper、adam4017Helper、GroupBox、虚拟 COM 口 | 已完成 |
|
||||
| 3 智慧路灯 | ZigBee 开关路灯;人体红外每 5 秒检测并联动路灯 | Lamp 控件、zigBeeHelper、adam4150Helper、CheckBox、RadioButton、Timer | 已完成 |
|
||||
| 4 入侵监测 | 红外对射检测;网络摄像机画面与云台;报警灯 | PictureBox、摄像头仿真、Alarm 控件、图片保存 | 任务 1、3 已完成;任务 2 需摄像头 |
|
||||
| 5 灾情监测 | 火焰(Timer)、烟雾(Thread)、灾情记录与推送 | Thread(含 Suspend / Resume / Abort)、跨线程改界面、ListBox、FormClosing | 任务 1 已完成;任务 2 需线程 API,任务 3 需 ListBox |
|
||||
| 5 灾情监测 | 火焰(Timer)、烟雾(Thread)、灾情记录与推送 | Thread(含 Suspend / Resume / Abort)、跨线程改界面、ListBox、FormClosing | 已完成 |
|
||||
| 6 数据存储 | 建库建表、采集入库与增删改、按时间段查询 | SQL 窗口(代替 SSMS)、SqlClient 仿冒(SQLite)、DataGridView、DateTimePicker | |
|
||||
| 7 情景控制 | 智慧教室、寝室、会议室三种情景联动 | TrackBar、资源图片、PictureBox 当按钮、mp3 播放、虚拟时钟 | |
|
||||
| 8 管理平台 | 主界面、校园环境(自动控制、7 天温度图表)、校园安防(抓拍入库、历史图片) | Panel Dock、无边框窗体、透明背景、Chart、ShowDialog、图片存数据库 | |
|
||||
@@ -236,7 +236,7 @@ flowchart TB
|
||||
P0["阶段 0 · 技术验证(已完成 2026-10-22)<br/>教材项目 2 任务 1 原样运行;实测编译、延迟、内存"]
|
||||
P1["阶段 1 · MVP(进行中,约 3 个月)<br/>虚拟实训台 ✔、代码编辑器 ✔、窗体设计器 ✔、多窗体 ✔<br/>教材示例:项目 1–3 全部 ✔,项目 4–5 中平台已支持的 3 个任务 ✔<br/>部署(沙箱、Docker Compose、GitHub Actions)✔"]
|
||||
P2["阶段 2 · 教学闭环(进行中,约 2 个月)<br/>账号与班级 ✔、作品保存在服务器 ✔、自动评分 ✔、布置与提交作业 ✔<br/>评阅(看代码、运行、老师评分、评语)✔、成绩表与导出 ✔;一个班试用"]
|
||||
P3["阶段 3 · 扩展(下学期)<br/>教材项目 4–8:摄像头、线程、SQLite 模拟数据库、图表、虚拟时钟"]
|
||||
P3["阶段 3 · 扩展(进行中)<br/>教材项目 4–8:摄像头、线程 ✔、SQLite 模拟数据库、图表、虚拟时钟"]
|
||||
P4["阶段 4 · 进阶(之后)<br/>校园街景场景、故障注入题库、导出 VS 工程"]
|
||||
P0 -->|"关口:链路跑通,延迟和资源占用可接受"| P1
|
||||
P1 -->|"关口:教材项目 1–3 能在平台上完整做完"| P2
|
||||
@@ -276,6 +276,10 @@ flowchart TB
|
||||
- 2026-10-22:布置与提交作业:老师选实验、起点(界面已做好 / 空白工程,起点工程由设计器引擎从参考答案生成)、截止时间和每项分值(自动评分项 + 老师评分项);布置时保存起点和评分规则的快照。学生在"我的作业"里打开作业,提交后排队评分(全站同时最多评 3 份、跑 6 个检查项),结果立即可见;截止后不能再交。老师看全班的提交情况和自动得分。
|
||||
- 2026-10-22:评阅与成绩:评阅页左边是全班学生(提交状态、得分、是否已评阅),右边是选中学生的代码(只读)、自动评分结果、规范提示(代码里用到却还是默认名字的控件、注释行数)和老师评分项打分、评语,可以"保存并看下一个";"运行他的程序"在新标签页的实验页里打开学生的提交(可以操作实训台,改动不保存)。学生能看到老师的各项得分和评语。班级管理页加"成绩"标签页:学生 × 作业的总分(自动评分 + 老师评分),点分数进入评阅;单个作业和全班成绩都能导出 CSV(Excel 打开不乱码)。
|
||||
|
||||
**第 3 阶段进展**:
|
||||
|
||||
- 2026-10-22:项目 5 补齐(线程、ListBox)。旧版线程 API:编译时把 `thd.Suspend()` / `Resume()` / `Abort()`、`thd.ThreadState`、`Thread.Sleep`、`new Thread(...)` 改写成平台实现(只改语义上确实是 `System.Threading.Thread` 的调用,不换行,报错和异常的行号不变;编辑器里看到的仍是学生的原始代码,也不再提示"已过时")。新版 .NET 不能从外部停住线程,平台的实现是协作式的:被挂起或终止的线程在下一个检查点停下或结束(`Thread.Sleep`、从后台线程改控件属性、调用设备类库、弹出提示框),教材的线程方法每次循环都经过这些地方;休眠中被终止的线程立即醒来结束。行为与 .NET Framework 一致的地方:线程可以挂起自己、对没启动的线程 Abort 不报错、`Resume` 没挂起的线程和直接 `Abort` 挂起的线程抛出 ThreadStateException(书上任务 2 关闭窗体时直接 `thd.Abort()`,在真实环境中也会出这个错,参考答案改为先 `Resume` 再 `Abort`,报错信息里写明这样改)。ListBox:列表项、单选、SelectedIndexChanged 先于 Click、Text 是选中项、删除选中项后取消选中;设计器工具箱加"列表框";评分规则的"包含"对列表框检查所有列表项。新增示例:项目 5 任务 2(烟雾,线程)、任务 3(推送灾情,二维数组、两个窗体、ListBox),各有评分规则和端到端测试,共 12 个示例。
|
||||
|
||||
**其他改进**:
|
||||
|
||||
- 2026-10-22:代码编辑器的语言功能:代码补全(输入 `btn_Open.` 列出按钮的属性、方法、事件,只列出平台仿真了的成员)、悬停提示、参数提示(`MessageBox.Show(` 列出重载,标出正在填的参数)、停止输入 1 秒后自动检查编译错误(提示和点"运行"时一样)。语言服务是一个常驻进程,每个浏览器标签页一份工程,只更新有变化的文件。实测(限制 2 核):50 个标签页同时检查 1.2 秒内全部完成,同时补全 0.7 秒;内存约 260 MB。
|
||||
@@ -291,7 +295,7 @@ flowchart TB
|
||||
| 事件顺序保真 | 书上代码依赖真实 WinForms 的事件顺序:单选按钮先触发"取消选中"再触发"选中",ComboBox 的 Click 先于展开,CheckBox 的 Click 时 Checked 已切换 | 逐项对照真实行为实现,并用书上代码做回归测试(单选、复选、下拉已实现并有测试) |
|
||||
| 书与配套不一致 | 控件名、步骤在书、表格和配套工程之间经常不同,书上代码有编译错误 | 以书为准逐任务整理模板和修正版答案 |
|
||||
| T-SQL 转 SQLite | 教材用 SQL Server 写法(`TOP`、`CONVERT`、DataAdapter 回写、日期字符串) | 用 ScriptDom 解析后转写;连接串一律指向学生自己的 SQLite 库;日期统一存为标准格式;以教材全部 SQL 语句为测试集 |
|
||||
| 旧版线程 API | `Thread.Suspend` / `Resume` / `Abort` 在新版 .NET 上会报错 | 编译时替换为平台实现的可暂停线程 |
|
||||
| 旧版线程 API | `Thread.Suspend` / `Resume` / `Abort` 在新版 .NET 上会报错 | 已实现:编译时改写成平台的协作式实现,被挂起或终止的线程在 Sleep、改控件、调设备类库时停下 |
|
||||
| 图片处理 | System.Drawing 在 Linux 上不受支持 | 自实现 `Image` / `Bitmap` 等少量类型,只传递和保存图片 |
|
||||
| 时间相关逻辑 | 项目 3 起就有按时刻开关灯的练习,部分定时器间隔长达 2 小时 | 提供可调、可加速的虚拟时钟 |
|
||||
| 事件延迟 | 浏览器和服务器之间来回传递事件,点击是否有卡顿感 | 已实测:单次约 15 ms,100 个程序同时操作中位数约 60 ms |
|
||||
|
||||
@@ -79,6 +79,7 @@ public class ADAM4017Helper
|
||||
/// </summary>
|
||||
public ADAM4017ResModel SetData()
|
||||
{
|
||||
LegacyThreads.CheckPoint();
|
||||
var model = new ADAM4017ResModel();
|
||||
if (string.IsNullOrWhiteSpace(_portName)) return model;
|
||||
model.temperatureValue = Read(0);
|
||||
@@ -110,6 +111,7 @@ public class ADAM4150Helper
|
||||
/// <summary>读取人体红外、火焰、烟雾、红外对射;串口未打开时全部为 false</summary>
|
||||
public ADAM4150ResModel SetData()
|
||||
{
|
||||
LegacyThreads.CheckPoint();
|
||||
var model = new ADAM4150ResModel();
|
||||
if (string.IsNullOrWhiteSpace(_portName)) return model;
|
||||
model.bodyInfraredValue = Read(0);
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace Sim.Runner;
|
||||
/// <summary>把学生程序的 Console 输出按行转发给浏览器(标准输出本身留给协议消息使用)</summary>
|
||||
internal sealed class ConsoleForwarder : TextWriter
|
||||
{
|
||||
/// <summary>一直不换行的输出(如教材的 while (true) Console.Write("A"))攒到这么多字符就先发出去</summary>
|
||||
private const int MaxBuffered = 4096;
|
||||
|
||||
private readonly StringBuilder _buffer = new();
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
@@ -16,7 +19,7 @@ internal sealed class ConsoleForwarder : TextWriter
|
||||
lock (_buffer)
|
||||
{
|
||||
_buffer.Append(value);
|
||||
if (value == '\n') FlushBuffer();
|
||||
if (value == '\n' || _buffer.Length >= MaxBuffered) FlushBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +29,7 @@ internal sealed class ConsoleForwarder : TextWriter
|
||||
lock (_buffer)
|
||||
{
|
||||
_buffer.Append(value);
|
||||
if (value.Contains('\n')) FlushBuffer();
|
||||
if (value.Contains('\n') || _buffer.Length >= MaxBuffered) FlushBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@ internal static class LanguageServer
|
||||
foreach (var diagnostic in compilation.GetDiagnostics()
|
||||
.Concat(ApiPolicy.Check(compilation))
|
||||
.Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning && d.Location.IsInSource)
|
||||
.Where(d => !LegacyThreadRewriter.IsSupportedObsoleteWarning(d))
|
||||
.OrderBy(d => d.Severity == DiagnosticSeverity.Error ? 0 : 1))
|
||||
{
|
||||
diagnostics.Add(StudentCompiler.ToJson(diagnostic));
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Sim.Runner;
|
||||
|
||||
/// <summary>
|
||||
/// 编译前改写学生代码里的旧版线程 API,让教材项目 5 的 Suspend / Resume / Abort 能在新版 .NET 上运行:
|
||||
/// <code>
|
||||
/// new Thread(GetData) → new Thread(LegacyThreads.Wrap(new ThreadStart(GetData)))
|
||||
/// thd.Suspend() / Resume() / Abort() → LegacyThreads.Suspend(thd) …
|
||||
/// thd.ThreadState → LegacyThreads.GetThreadState(thd)
|
||||
/// Thread.Sleep(2000) → LegacyThreads.Sleep(2000)
|
||||
/// </code>
|
||||
/// 只改写语义上确实是 System.Threading.Thread 成员的调用;改写不换行,报错和异常堆栈里的行号不变。
|
||||
/// 学生在编辑器里看到的仍是原来的代码。
|
||||
/// </summary>
|
||||
internal static class LegacyThreadRewriter
|
||||
{
|
||||
private const string Target = "global::Sim.Host.LegacyThreads";
|
||||
|
||||
/// <summary>改写整个编译;没有需要改写的地方时原样返回</summary>
|
||||
public static CSharpCompilation Rewrite(CSharpCompilation compilation)
|
||||
{
|
||||
var result = compilation;
|
||||
foreach (var tree in compilation.SyntaxTrees)
|
||||
{
|
||||
// 用到线程的文件一定会出现 Thread 这个词,其他文件不用做语义分析
|
||||
if (!tree.GetText().ToString().Contains("Thread")) continue;
|
||||
var rewriter = new Rewriter(compilation.GetSemanticModel(tree));
|
||||
var root = rewriter.Visit(tree.GetRoot());
|
||||
if (!rewriter.Changed) continue;
|
||||
var text = SourceText.From(root.ToFullString(), Encoding.UTF8);
|
||||
result = result.ReplaceSyntaxTree(tree, CSharpSyntaxTree.ParseText(text, (CSharpParseOptions)tree.Options, tree.FilePath));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>平台支持的过时线程 API 不再提示"已过时"(边写边检查时用)</summary>
|
||||
public static bool IsSupportedObsoleteWarning(Diagnostic diagnostic)
|
||||
{
|
||||
if (diagnostic.Id is not ("CS0618" or "SYSLIB0006")) return false;
|
||||
// 警告标在整个调用上,如 thd.Suspend()
|
||||
var source = diagnostic.Location.SourceTree?.GetText().ToString(diagnostic.Location.SourceSpan) ?? "";
|
||||
return Regex.IsMatch(source, @"\b(Suspend|Resume|Abort)\b");
|
||||
}
|
||||
|
||||
private static bool IsThread(ITypeSymbol? type) =>
|
||||
type is { Name: "Thread", ContainingNamespace: { Name: "Threading", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } } };
|
||||
|
||||
private sealed class Rewriter(SemanticModel model) : CSharpSyntaxRewriter
|
||||
{
|
||||
public bool Changed { get; private set; }
|
||||
|
||||
public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node)
|
||||
{
|
||||
var method = model.GetSymbolInfo(node).Symbol as IMethodSymbol;
|
||||
var visited = (InvocationExpressionSyntax)base.VisitInvocationExpression(node)!;
|
||||
if (method == null || !IsThread(method.ContainingType)) return visited;
|
||||
|
||||
switch (method.Name)
|
||||
{
|
||||
case "Suspend" or "Resume" or "Abort" when !method.IsStatic && visited.Expression is MemberAccessExpressionSyntax access:
|
||||
// thd.Abort(x) → LegacyThreads.Abort(thd, x)
|
||||
var arguments = visited.ArgumentList.Arguments.Insert(0, SyntaxFactory.Argument(access.Expression.WithoutTrivia()));
|
||||
Changed = true;
|
||||
return SyntaxFactory.InvocationExpression(Member(method.Name), SyntaxFactory.ArgumentList(arguments))
|
||||
.WithTriviaFrom(visited);
|
||||
case "Sleep" when method.IsStatic:
|
||||
Changed = true;
|
||||
return visited.WithExpression(Member("Sleep").WithTriviaFrom(visited.Expression));
|
||||
default:
|
||||
return visited;
|
||||
}
|
||||
}
|
||||
|
||||
public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
|
||||
{
|
||||
var symbol = model.GetSymbolInfo(node).Symbol;
|
||||
var visited = (MemberAccessExpressionSyntax)base.VisitMemberAccessExpression(node)!;
|
||||
if (symbol is not IPropertySymbol { Name: "ThreadState", IsStatic: false } property || !IsThread(property.ContainingType)) return visited;
|
||||
Changed = true;
|
||||
return SyntaxFactory.InvocationExpression(
|
||||
Member("GetThreadState"),
|
||||
SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(visited.Expression.WithoutTrivia()))))
|
||||
.WithTriviaFrom(visited);
|
||||
}
|
||||
|
||||
public override SyntaxNode? VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) =>
|
||||
WrapThreadStart(node, base.VisitObjectCreationExpression(node));
|
||||
|
||||
public override SyntaxNode? VisitImplicitObjectCreationExpression(ImplicitObjectCreationExpressionSyntax node) =>
|
||||
WrapThreadStart(node, base.VisitImplicitObjectCreationExpression(node));
|
||||
|
||||
/// <summary>new Thread(start) 的第一个参数包一层 LegacyThreads.Wrap,线程被 Abort 时安静地结束</summary>
|
||||
private SyntaxNode? WrapThreadStart(BaseObjectCreationExpressionSyntax original, SyntaxNode? visitedNode)
|
||||
{
|
||||
if (visitedNode is not BaseObjectCreationExpressionSyntax visited
|
||||
|| model.GetSymbolInfo(original).Symbol is not IMethodSymbol { MethodKind: MethodKind.Constructor } constructor
|
||||
|| !IsThread(constructor.ContainingType)
|
||||
|| constructor.Parameters.Length == 0
|
||||
|| visited.ArgumentList is not { Arguments.Count: > 0 } argumentList)
|
||||
{
|
||||
return visitedNode;
|
||||
}
|
||||
var first = argumentList.Arguments[0];
|
||||
if (first.Expression.IsKind(SyntaxKind.NullLiteralExpression)) return visited;
|
||||
var delegateType = constructor.Parameters[0].Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
// new ThreadStart(方法名或 lambda):明确委托类型,Wrap 的重载就不会有歧义
|
||||
var wrapped = SyntaxFactory.ParseExpression($"{Target}.Wrap(new {delegateType}({first.Expression.WithoutTrivia().ToFullString()}))");
|
||||
Changed = true;
|
||||
return visited.WithArgumentList(argumentList.WithArguments(
|
||||
argumentList.Arguments.Replace(first, first.WithExpression(wrapped.WithTriviaFrom(first.Expression)))));
|
||||
}
|
||||
|
||||
private static ExpressionSyntax Member(string name) => SyntaxFactory.ParseExpression($"{Target}.{name}");
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,8 @@ internal static class StudentCompiler
|
||||
|
||||
using var peStream = new MemoryStream();
|
||||
using var pdbStream = new MemoryStream();
|
||||
var emit = compilation.Emit(peStream, pdbStream,
|
||||
// 编译改写后的代码(旧版线程 API 换成平台实现),API 检查仍针对学生写的原始代码
|
||||
var emit = LegacyThreadRewriter.Rewrite(compilation).Emit(peStream, pdbStream,
|
||||
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
|
||||
var denied = ApiPolicy.Check(compilation).ToList();
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public class ComboBox : Control
|
||||
public class ComboBox : ListControl
|
||||
{
|
||||
private int _selectedIndex = -1;
|
||||
private ComboBoxStyle _dropDownStyle = ComboBoxStyle.DropDown;
|
||||
|
||||
public ComboBox() => Items = new ObjectCollection(this);
|
||||
@@ -31,15 +27,15 @@ public class ComboBox : Control
|
||||
set => SetProp(ref _dropDownStyle, value, nameof(DropDownStyle));
|
||||
}
|
||||
|
||||
public int SelectedIndex
|
||||
public override int SelectedIndex
|
||||
{
|
||||
get => _selectedIndex;
|
||||
get => SelectedIndexValue;
|
||||
set
|
||||
{
|
||||
if (value < -1 || value >= Items.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(value), $"“{value}”的值对于“SelectedIndex”无效。");
|
||||
if (value == _selectedIndex) return;
|
||||
SetProp(ref _selectedIndex, value, nameof(SelectedIndex));
|
||||
if (value == SelectedIndexValue) return;
|
||||
SetProp(ref SelectedIndexValue, value, nameof(SelectedIndex));
|
||||
// 选中项变化时 Text 随之变化(先 TextChanged,再 SelectedIndexChanged,与 WinForms 一致)
|
||||
Text = value >= 0 ? Items[value]?.ToString() ?? "" : "";
|
||||
OnSelectedIndexChanged(EventArgs.Empty);
|
||||
@@ -49,19 +45,17 @@ public class ComboBox : Control
|
||||
|
||||
public object? SelectedItem
|
||||
{
|
||||
get => _selectedIndex >= 0 ? Items[_selectedIndex] : null;
|
||||
get => SelectedIndexValue >= 0 ? Items[SelectedIndexValue] : null;
|
||||
set => SelectedIndex = value == null ? -1 : Items.IndexOf(value);
|
||||
}
|
||||
|
||||
public string SelectedText { get; set; } = "";
|
||||
|
||||
public event EventHandler? SelectedIndexChanged;
|
||||
public event EventHandler? SelectedValueChanged;
|
||||
public event EventHandler? DropDown;
|
||||
public event EventHandler? DropDownClosed;
|
||||
|
||||
protected virtual void OnSelectedIndexChanged(EventArgs e) => SelectedIndexChanged?.Invoke(this, e);
|
||||
protected virtual void OnSelectedValueChanged(EventArgs e) => SelectedValueChanged?.Invoke(this, e);
|
||||
protected virtual void OnDropDown(EventArgs e) => DropDown?.Invoke(this, e);
|
||||
protected virtual void OnDropDownClosed(EventArgs e) => DropDownClosed?.Invoke(this, e);
|
||||
|
||||
@@ -84,7 +78,7 @@ public class ComboBox : Control
|
||||
case "text" when _dropDownStyle != ComboBoxStyle.DropDownList:
|
||||
SetTextFromUser(action.Value ?? "");
|
||||
var match = Items.IndexOf(Text);
|
||||
if (match != _selectedIndex) _selectedIndex = match;
|
||||
if (match != SelectedIndexValue) SelectedIndexValue = match;
|
||||
break;
|
||||
default:
|
||||
base.OnUserAction(action);
|
||||
@@ -92,85 +86,28 @@ public class ComboBox : Control
|
||||
}
|
||||
}
|
||||
|
||||
internal void ItemsChanged()
|
||||
internal override void ItemsChanged()
|
||||
{
|
||||
if (_selectedIndex >= Items.Count) _selectedIndex = -1;
|
||||
NotifyChanged(nameof(Items), Items.Select(i => i?.ToString() ?? "").ToArray());
|
||||
NotifyChanged(nameof(Items), ItemTexts(Items));
|
||||
NotifyChanged(nameof(SelectedIndex), SelectedIndexValue);
|
||||
}
|
||||
|
||||
internal override void ItemsCleared()
|
||||
{
|
||||
SelectedIndex = -1;
|
||||
ItemsChanged();
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["Items"] = PropValue.From(Items.Select(i => i?.ToString() ?? "").ToArray());
|
||||
props["SelectedIndex"] = _selectedIndex;
|
||||
props["Items"] = PropValue.From(ItemTexts(Items));
|
||||
props["SelectedIndex"] = SelectedIndexValue;
|
||||
props["DropDownStyle"] = _dropDownStyle.ToString();
|
||||
}
|
||||
|
||||
public class ObjectCollection : IList, IEnumerable<object?>
|
||||
public class ObjectCollection : ListItemCollection
|
||||
{
|
||||
private readonly List<object?> _items = new();
|
||||
private readonly ComboBox _owner;
|
||||
|
||||
public ObjectCollection(ComboBox owner) => _owner = owner;
|
||||
|
||||
public int Count => _items.Count;
|
||||
public bool IsReadOnly => false;
|
||||
bool IList.IsFixedSize => false;
|
||||
bool ICollection.IsSynchronized => false;
|
||||
object ICollection.SyncRoot => this;
|
||||
|
||||
public object? this[int index]
|
||||
{
|
||||
get => _items[index];
|
||||
set
|
||||
{
|
||||
_items[index] = value;
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int Add(object? item)
|
||||
{
|
||||
if (item == null) throw new ArgumentNullException(nameof(item), "值不能为 null。");
|
||||
_items.Add(item);
|
||||
_owner.ItemsChanged();
|
||||
return _items.Count - 1;
|
||||
}
|
||||
|
||||
public void AddRange(object[] items)
|
||||
{
|
||||
_items.AddRange(items);
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public void Insert(int index, object? item)
|
||||
{
|
||||
_items.Insert(index, item);
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public void Remove(object? item)
|
||||
{
|
||||
if (_items.Remove(item)) _owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
_items.RemoveAt(index);
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_items.Clear();
|
||||
_owner.SelectedIndex = -1;
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public bool Contains(object? item) => _items.Contains(item);
|
||||
public int IndexOf(object? item) => _items.IndexOf(item);
|
||||
public void CopyTo(Array array, int index) => ((ICollection)_items).CopyTo(array, index);
|
||||
public IEnumerator<object?> GetEnumerator() => _items.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
public ObjectCollection(ComboBox owner) : base(owner) { }
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,7 @@ public class Control : Component, IWin32Window
|
||||
get => _text;
|
||||
set
|
||||
{
|
||||
LegacyThreads.CheckPoint();
|
||||
value ??= "";
|
||||
if (_text == value) return;
|
||||
EnsureThreadAccess();
|
||||
@@ -279,6 +280,9 @@ public class Control : Component, IWin32Window
|
||||
OnClick(args);
|
||||
OnMouseClick(args);
|
||||
break;
|
||||
case "doubleClick":
|
||||
OnDoubleClick(EventArgs.Empty);
|
||||
break;
|
||||
case "mouseDown":
|
||||
OnMouseDown(new MouseEventArgs(ToMouseButtons(action.Button), 1, action.X, action.Y, 0));
|
||||
break;
|
||||
@@ -354,6 +358,8 @@ public class Control : Component, IWin32Window
|
||||
/// <summary>修改字段并在需要时通知浏览器;返回值表示是否真的改变</summary>
|
||||
protected bool SetProp<T>(ref T field, T value, string prop)
|
||||
{
|
||||
// 后台线程改界面时响应 Suspend / Abort(教材的线程方法每次循环都会改控件)
|
||||
LegacyThreads.CheckPoint();
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
|
||||
EnsureThreadAccess();
|
||||
field = value;
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Nodes;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public enum SelectionMode { None, One, MultiSimple, MultiExtended }
|
||||
|
||||
/// <summary>列表框(教材项目 5 任务 3 显示历史灾情记录)。只支持单选。</summary>
|
||||
public class ListBox : ListControl
|
||||
{
|
||||
private int _itemHeight = 12;
|
||||
private BorderStyle _borderStyle = BorderStyle.Fixed3D;
|
||||
|
||||
public ListBox() => Items = new ObjectCollection(this);
|
||||
|
||||
protected override Size DefaultSize => new(120, 96);
|
||||
|
||||
public ObjectCollection Items { get; }
|
||||
|
||||
public bool FormattingEnabled { get; set; }
|
||||
public bool IntegralHeight { get; set; } = true;
|
||||
public bool HorizontalScrollbar { get; set; }
|
||||
public bool ScrollAlwaysVisible { get; set; }
|
||||
public bool MultiColumn { get; set; }
|
||||
public bool Sorted { get; set; }
|
||||
public SelectionMode SelectionMode { get; set; } = SelectionMode.One;
|
||||
public int TopIndex { get; set; }
|
||||
|
||||
public int ItemHeight
|
||||
{
|
||||
get => _itemHeight;
|
||||
set => SetProp(ref _itemHeight, value, nameof(ItemHeight));
|
||||
}
|
||||
|
||||
public BorderStyle BorderStyle
|
||||
{
|
||||
get => _borderStyle;
|
||||
set => SetProp(ref _borderStyle, value, nameof(BorderStyle));
|
||||
}
|
||||
|
||||
public override int SelectedIndex
|
||||
{
|
||||
get => SelectedIndexValue;
|
||||
set
|
||||
{
|
||||
if (value < -1 || value >= Items.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(value), $"“{value}”的值对于“SelectedIndex”无效。");
|
||||
if (!SetProp(ref SelectedIndexValue, value, nameof(SelectedIndex))) return;
|
||||
OnSelectedIndexChanged(EventArgs.Empty);
|
||||
OnSelectedValueChanged(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public object? SelectedItem
|
||||
{
|
||||
get => SelectedIndexValue >= 0 ? Items[SelectedIndexValue] : null;
|
||||
set => SelectedIndex = value == null ? -1 : Items.IndexOf(value);
|
||||
}
|
||||
|
||||
/// <summary>和 WinForms 一样,列表框的 Text 是选中项的文字;赋值时选中文字相同的项</summary>
|
||||
public override string Text
|
||||
{
|
||||
get => GetItemText(SelectedItem);
|
||||
set
|
||||
{
|
||||
var index = FindStringExact(value ?? "");
|
||||
if (index >= 0) SelectedIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler? SelectedIndexChanged;
|
||||
|
||||
protected virtual void OnSelectedIndexChanged(EventArgs e) => SelectedIndexChanged?.Invoke(this, e);
|
||||
|
||||
public void ClearSelected() => SelectedIndex = -1;
|
||||
|
||||
public void SetSelected(int index, bool value)
|
||||
{
|
||||
if (value) SelectedIndex = index;
|
||||
else if (index == SelectedIndexValue) SelectedIndex = -1;
|
||||
}
|
||||
|
||||
public bool GetSelected(int index) => index == SelectedIndexValue;
|
||||
|
||||
/// <summary>第一个以 s 开头的项(不区分大小写),没有时返回 -1</summary>
|
||||
public int FindString(string s)
|
||||
{
|
||||
for (var i = 0; i < Items.Count; i++)
|
||||
if (GetItemText(Items[i]).StartsWith(s, StringComparison.OrdinalIgnoreCase)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int FindStringExact(string s)
|
||||
{
|
||||
for (var i = 0; i < Items.Count; i++)
|
||||
if (string.Equals(GetItemText(Items[i]), s, StringComparison.OrdinalIgnoreCase)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void BeginUpdate() { }
|
||||
public void EndUpdate() { }
|
||||
|
||||
protected override void OnUserAction(UserAction action)
|
||||
{
|
||||
switch (action.Action)
|
||||
{
|
||||
case "select":
|
||||
// 和 WinForms 一样,按下鼠标时先改变选中项,松开后才触发 Click
|
||||
if (action.Index >= 0 && action.Index < Items.Count) SelectedIndex = action.Index;
|
||||
base.OnUserAction(new UserAction { Action = "click", X = action.X, Y = action.Y });
|
||||
break;
|
||||
default:
|
||||
base.OnUserAction(action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal override void ItemsChanged()
|
||||
{
|
||||
NotifyChanged(nameof(Items), ItemTexts(Items));
|
||||
NotifyChanged(nameof(SelectedIndex), SelectedIndexValue);
|
||||
}
|
||||
|
||||
internal override void ItemsCleared()
|
||||
{
|
||||
SelectedIndex = -1;
|
||||
ItemsChanged();
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["Items"] = PropValue.From(ItemTexts(Items));
|
||||
props["SelectedIndex"] = SelectedIndexValue;
|
||||
props["ItemHeight"] = _itemHeight;
|
||||
props["BorderStyle"] = _borderStyle.ToString();
|
||||
}
|
||||
|
||||
public class ObjectCollection : ListItemCollection
|
||||
{
|
||||
public ObjectCollection(ListBox owner) : base(owner) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
/// <summary>ComboBox 和 ListBox 的共同基类:列表项、选中项</summary>
|
||||
public abstract class ListControl : Control
|
||||
{
|
||||
/// <summary>选中项的序号。列表项增删时直接调整(和 WinForms 一样不触发 SelectedIndexChanged)</summary>
|
||||
private protected int SelectedIndexValue = -1;
|
||||
|
||||
public abstract int SelectedIndex { get; set; }
|
||||
|
||||
public string DisplayMember { get; set; } = "";
|
||||
public string ValueMember { get; set; } = "";
|
||||
|
||||
public event EventHandler? SelectedValueChanged;
|
||||
|
||||
protected virtual void OnSelectedValueChanged(EventArgs e) => SelectedValueChanged?.Invoke(this, e);
|
||||
|
||||
public string GetItemText(object? item) => item?.ToString() ?? "";
|
||||
|
||||
/// <summary>列表项变化后调用:把新的列表和选中项发给浏览器</summary>
|
||||
internal abstract void ItemsChanged();
|
||||
|
||||
/// <summary>Items.Clear() 时调用:先取消选中,再刷新列表</summary>
|
||||
internal abstract void ItemsCleared();
|
||||
|
||||
internal void ItemInserted(int index)
|
||||
{
|
||||
if (SelectedIndexValue >= index) SelectedIndexValue++;
|
||||
ItemsChanged();
|
||||
}
|
||||
|
||||
/// <summary>删掉的正是选中项时取消选中,删掉前面的项时选中项序号前移</summary>
|
||||
internal void ItemRemoved(int index)
|
||||
{
|
||||
if (SelectedIndexValue == index) SelectedIndexValue = -1;
|
||||
else if (SelectedIndexValue > index) SelectedIndexValue--;
|
||||
ItemsChanged();
|
||||
}
|
||||
|
||||
protected string[] ItemTexts(IEnumerable<object?> items) => items.Select(GetItemText).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>ComboBox.ObjectCollection 和 ListBox.ObjectCollection 共用的实现</summary>
|
||||
public abstract class ListItemCollection : IList, IEnumerable<object?>
|
||||
{
|
||||
private readonly List<object?> _items = new();
|
||||
private readonly ListControl _owner;
|
||||
|
||||
private protected ListItemCollection(ListControl owner) => _owner = owner;
|
||||
|
||||
public int Count => _items.Count;
|
||||
public bool IsReadOnly => false;
|
||||
bool IList.IsFixedSize => false;
|
||||
bool ICollection.IsSynchronized => false;
|
||||
object ICollection.SyncRoot => this;
|
||||
|
||||
public object? this[int index]
|
||||
{
|
||||
get => _items[index];
|
||||
set
|
||||
{
|
||||
_items[index] = value;
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int Add(object? item)
|
||||
{
|
||||
if (item == null) throw new ArgumentNullException(nameof(item), "值不能为 null。");
|
||||
_items.Add(item);
|
||||
_owner.ItemsChanged();
|
||||
return _items.Count - 1;
|
||||
}
|
||||
|
||||
public void AddRange(object[] items)
|
||||
{
|
||||
_items.AddRange(items);
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public void Insert(int index, object? item)
|
||||
{
|
||||
_items.Insert(index, item);
|
||||
_owner.ItemInserted(index);
|
||||
}
|
||||
|
||||
public void Remove(object? item)
|
||||
{
|
||||
var index = _items.IndexOf(item);
|
||||
if (index >= 0) RemoveAt(index);
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
_items.RemoveAt(index);
|
||||
_owner.ItemRemoved(index);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_items.Clear();
|
||||
_owner.ItemsCleared();
|
||||
}
|
||||
|
||||
public bool Contains(object? item) => _items.Contains(item);
|
||||
public int IndexOf(object? item) => _items.IndexOf(item);
|
||||
public void CopyTo(Array array, int index) => ((ICollection)_items).CopyTo(array, index);
|
||||
public IEnumerator<object?> GetEnumerator() => _items.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Sim.Host;
|
||||
|
||||
/// <summary>
|
||||
/// 教材项目 5 用到的旧版线程 API:Thread.Suspend / Resume / Abort 在新版 .NET 上会抛出 PlatformNotSupportedException。
|
||||
/// 编译时把学生代码里对它们(以及 new Thread、Thread.Sleep、ThreadState)的调用改写成这里的方法(见 Sim.Runner/LegacyThreadRewriter.cs)。
|
||||
/// <para>
|
||||
/// 新版 .NET 不能从外部强行停住或中止一个线程,所以这里是"协作式"的:被挂起或中止的线程在下一个检查点停下或结束。
|
||||
/// 检查点是 Thread.Sleep、从后台线程修改控件属性、调用设备类库、弹出提示框,
|
||||
/// 教材里的线程方法每次循环都会经过它们,看到的效果和 .NET Framework 一致。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class LegacyThreads
|
||||
{
|
||||
private sealed class State
|
||||
{
|
||||
public volatile bool Suspended;
|
||||
public volatile bool AbortRequested;
|
||||
}
|
||||
|
||||
// 线程少、操作不频繁,用一把锁保护所有线程的状态;挂起和休眠都在这把锁上等待
|
||||
private static readonly object Gate = new();
|
||||
private static readonly ConditionalWeakTable<Thread, State> States = new();
|
||||
|
||||
/// <summary>当前线程是学生用 new Thread 创建的线程时,指向它的状态</summary>
|
||||
[ThreadStatic] private static State? _current;
|
||||
|
||||
private static State StateOf(Thread thread) => States.GetValue(thread, static _ => new State());
|
||||
|
||||
// ---------- 改写后的调用 ----------
|
||||
|
||||
/// <summary>new Thread(GetData) → new Thread(Wrap(new ThreadStart(GetData))):线程被中止时安静地结束</summary>
|
||||
public static ThreadStart Wrap(ThreadStart start) => () => RunThread(() => start());
|
||||
|
||||
public static ParameterizedThreadStart Wrap(ParameterizedThreadStart start) => obj => RunThread(() => start(obj));
|
||||
|
||||
private static void RunThread(Action body)
|
||||
{
|
||||
var state = StateOf(Thread.CurrentThread);
|
||||
_current = state;
|
||||
try
|
||||
{
|
||||
// 启动前就被 Abort 的线程(.NET Framework 里启动后立即结束)
|
||||
CheckPoint(state);
|
||||
body();
|
||||
}
|
||||
catch (ThreadAbortedException)
|
||||
{
|
||||
// 和 .NET Framework 一样,被中止的线程结束时不算未处理的异常
|
||||
}
|
||||
}
|
||||
|
||||
public static void Suspend(Thread thread)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if ((thread.ThreadState & (ThreadState.Unstarted | ThreadState.Stopped)) != 0)
|
||||
throw new ThreadStateException("线程没有在运行,无法挂起。");
|
||||
StateOf(thread).Suspended = true;
|
||||
}
|
||||
// 线程挂起自己:立即停下,直到别的线程调用 Resume
|
||||
if (thread == Thread.CurrentThread) CheckPoint(StateOf(thread));
|
||||
}
|
||||
|
||||
public static void Resume(Thread thread)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
var state = StateOf(thread);
|
||||
if (!state.Suspended) throw new ThreadStateException("线程没有被挂起,无法继续(Resume 只能用在调用过 Suspend 的线程上)。");
|
||||
state.Suspended = false;
|
||||
Monitor.PulseAll(Gate);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Abort(Thread thread) => Abort(thread, null);
|
||||
|
||||
public static void Abort(Thread thread, object? stateInfo)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if ((thread.ThreadState & ThreadState.Stopped) != 0) return;
|
||||
var state = StateOf(thread);
|
||||
state.AbortRequested = true;
|
||||
// 正在 Thread.Sleep 的线程立即醒来结束
|
||||
Monitor.PulseAll(Gate);
|
||||
// .NET Framework 不能直接中止挂起的线程:抛出异常,等它被 Resume 后才结束
|
||||
if (state.Suspended) throw new ThreadStateException("线程已挂起,不能直接终止。先调用 Resume() 让线程继续,再调用 Abort()。");
|
||||
}
|
||||
if (thread == Thread.CurrentThread) throw new ThreadAbortedException();
|
||||
}
|
||||
|
||||
/// <summary>thd.ThreadState:在真实状态上加上挂起、请求中止</summary>
|
||||
public static ThreadState GetThreadState(Thread thread)
|
||||
{
|
||||
var actual = thread.ThreadState;
|
||||
if (!States.TryGetValue(thread, out var state) || (actual & ThreadState.Stopped) != 0) return actual;
|
||||
var result = actual & (ThreadState.Background | ThreadState.Unstarted);
|
||||
if (state.Suspended) result |= ThreadState.Suspended;
|
||||
else if ((actual & ThreadState.Unstarted) == 0) result |= actual & ThreadState.WaitSleepJoin;
|
||||
if (state.AbortRequested) result |= ThreadState.AbortRequested;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void Sleep(int millisecondsTimeout)
|
||||
{
|
||||
var state = _current;
|
||||
if (state == null)
|
||||
{
|
||||
Thread.Sleep(millisecondsTimeout);
|
||||
return;
|
||||
}
|
||||
if (millisecondsTimeout < Timeout.Infinite) throw new ArgumentOutOfRangeException(nameof(millisecondsTimeout), "休眠时间不能是负数(-1 表示一直休眠)。");
|
||||
CheckPoint(state);
|
||||
var deadline = Environment.TickCount64 + millisecondsTimeout;
|
||||
lock (Gate)
|
||||
{
|
||||
// 休眠中被 Abort 时提前醒来;被 Suspend 的线程睡完后在检查点停下
|
||||
while (!state.AbortRequested)
|
||||
{
|
||||
if (millisecondsTimeout == Timeout.Infinite)
|
||||
{
|
||||
Monitor.Wait(Gate);
|
||||
continue;
|
||||
}
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (remaining <= 0) break;
|
||||
Monitor.Wait(Gate, (int)remaining);
|
||||
}
|
||||
}
|
||||
CheckPoint(state);
|
||||
}
|
||||
|
||||
public static void Sleep(TimeSpan timeout) => Sleep((int)timeout.TotalMilliseconds);
|
||||
|
||||
// ---------- 检查点 ----------
|
||||
|
||||
/// <summary>平台内部使用:学生创建的线程在这里响应 Suspend 和 Abort(其他线程直接返回)</summary>
|
||||
public static void CheckPoint()
|
||||
{
|
||||
var state = _current;
|
||||
if (state != null) CheckPoint(state);
|
||||
}
|
||||
|
||||
private static void CheckPoint(State state)
|
||||
{
|
||||
if (!state.Suspended && !state.AbortRequested) return;
|
||||
lock (Gate)
|
||||
{
|
||||
while (state.Suspended) Monitor.Wait(Gate);
|
||||
if (state.AbortRequested) throw new ThreadAbortedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>线程被 Abort 时在线程里抛出(对应 .NET Framework 的 ThreadAbortException)</summary>
|
||||
public sealed class ThreadAbortedException : Exception
|
||||
{
|
||||
public ThreadAbortedException() : base("正在中止线程。") { }
|
||||
}
|
||||
@@ -77,13 +77,17 @@ public static class SimHost
|
||||
|
||||
public static void SendConsole(string text) => Send(new JsonObject { ["type"] = "console", ["text"] = text });
|
||||
|
||||
public static void SendDevice(string device, string action, object? data) => Send(new JsonObject
|
||||
public static void SendDevice(string device, string action, object? data)
|
||||
{
|
||||
LegacyThreads.CheckPoint();
|
||||
Send(new JsonObject
|
||||
{
|
||||
["type"] = "device",
|
||||
["device"] = device,
|
||||
["action"] = action,
|
||||
["data"] = PropValue.From(data),
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 异常 ----------
|
||||
|
||||
@@ -121,6 +125,7 @@ public static class SimHost
|
||||
/// </summary>
|
||||
public static JsonNode? Request(JsonObject message)
|
||||
{
|
||||
LegacyThreads.CheckPoint();
|
||||
var id = Interlocked.Increment(ref _nextRequestId);
|
||||
var pending = new PendingRequest();
|
||||
Requests[id] = pending;
|
||||
|
||||
Generated
+146
@@ -0,0 +1,146 @@
|
||||
namespace 烟雾
|
||||
{
|
||||
partial class Form1
|
||||
{
|
||||
/// <summary>
|
||||
/// 必需的设计器变量。
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有正在使用的资源。
|
||||
/// </summary>
|
||||
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows 窗体设计器生成的代码
|
||||
|
||||
/// <summary>
|
||||
/// 设计器支持所需的方法 - 不要
|
||||
/// 使用代码编辑器修改此方法的内容。
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.lbl_port = new System.Windows.Forms.Label();
|
||||
this.com_port = new System.Windows.Forms.ComboBox();
|
||||
this.btn_open = new System.Windows.Forms.Button();
|
||||
this.groupBox1 = new System.Windows.Forms.GroupBox();
|
||||
this.txt_even = new System.Windows.Forms.TextBox();
|
||||
this.lbl_even = new System.Windows.Forms.Label();
|
||||
this.txt_time = new System.Windows.Forms.TextBox();
|
||||
this.lbl_time = new System.Windows.Forms.Label();
|
||||
this.groupBox1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// lbl_port
|
||||
//
|
||||
this.lbl_port.AutoSize = true;
|
||||
this.lbl_port.Location = new System.Drawing.Point(53, 21);
|
||||
this.lbl_port.Name = "lbl_port";
|
||||
this.lbl_port.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_port.TabIndex = 0;
|
||||
this.lbl_port.Text = "串口";
|
||||
//
|
||||
// com_port
|
||||
//
|
||||
this.com_port.FormattingEnabled = true;
|
||||
this.com_port.Location = new System.Drawing.Point(100, 18);
|
||||
this.com_port.Name = "com_port";
|
||||
this.com_port.Size = new System.Drawing.Size(121, 20);
|
||||
this.com_port.TabIndex = 1;
|
||||
//
|
||||
// btn_open
|
||||
//
|
||||
this.btn_open.Location = new System.Drawing.Point(227, 16);
|
||||
this.btn_open.Name = "btn_open";
|
||||
this.btn_open.Size = new System.Drawing.Size(75, 23);
|
||||
this.btn_open.TabIndex = 2;
|
||||
this.btn_open.Text = "开启";
|
||||
this.btn_open.UseVisualStyleBackColor = true;
|
||||
this.btn_open.Click += new System.EventHandler(this.btn_open_Click);
|
||||
//
|
||||
// groupBox1
|
||||
//
|
||||
this.groupBox1.Controls.Add(this.txt_even);
|
||||
this.groupBox1.Controls.Add(this.lbl_even);
|
||||
this.groupBox1.Controls.Add(this.txt_time);
|
||||
this.groupBox1.Controls.Add(this.lbl_time);
|
||||
this.groupBox1.Location = new System.Drawing.Point(30, 63);
|
||||
this.groupBox1.Name = "groupBox1";
|
||||
this.groupBox1.Size = new System.Drawing.Size(286, 110);
|
||||
this.groupBox1.TabIndex = 3;
|
||||
this.groupBox1.TabStop = false;
|
||||
this.groupBox1.Text = "数据显示";
|
||||
//
|
||||
// txt_even
|
||||
//
|
||||
this.txt_even.Location = new System.Drawing.Point(70, 70);
|
||||
this.txt_even.Name = "txt_even";
|
||||
this.txt_even.Size = new System.Drawing.Size(133, 21);
|
||||
this.txt_even.TabIndex = 3;
|
||||
//
|
||||
// lbl_even
|
||||
//
|
||||
this.lbl_even.AutoSize = true;
|
||||
this.lbl_even.Location = new System.Drawing.Point(23, 73);
|
||||
this.lbl_even.Name = "lbl_even";
|
||||
this.lbl_even.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_even.TabIndex = 2;
|
||||
this.lbl_even.Text = "事件";
|
||||
//
|
||||
// txt_time
|
||||
//
|
||||
this.txt_time.Location = new System.Drawing.Point(70, 34);
|
||||
this.txt_time.Name = "txt_time";
|
||||
this.txt_time.Size = new System.Drawing.Size(133, 21);
|
||||
this.txt_time.TabIndex = 1;
|
||||
//
|
||||
// lbl_time
|
||||
//
|
||||
this.lbl_time.AutoSize = true;
|
||||
this.lbl_time.Location = new System.Drawing.Point(23, 37);
|
||||
this.lbl_time.Name = "lbl_time";
|
||||
this.lbl_time.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_time.TabIndex = 0;
|
||||
this.lbl_time.Text = "时间";
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(342, 193);
|
||||
this.Controls.Add(this.groupBox1);
|
||||
this.Controls.Add(this.btn_open);
|
||||
this.Controls.Add(this.com_port);
|
||||
this.Controls.Add(this.lbl_port);
|
||||
this.Name = "Form1";
|
||||
this.Text = "烟雾";
|
||||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
|
||||
this.Load += new System.EventHandler(this.Form1_Load);
|
||||
this.groupBox1.ResumeLayout(false);
|
||||
this.groupBox1.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label lbl_port;
|
||||
private System.Windows.Forms.ComboBox com_port;
|
||||
private System.Windows.Forms.Button btn_open;
|
||||
private System.Windows.Forms.GroupBox groupBox1;
|
||||
private System.Windows.Forms.TextBox txt_even;
|
||||
private System.Windows.Forms.Label lbl_even;
|
||||
private System.Windows.Forms.TextBox txt_time;
|
||||
private System.Windows.Forms.Label lbl_time;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.IO.Ports;
|
||||
using DeviceCtrlLibrary;
|
||||
using System.Threading;//引用线程命名空间
|
||||
|
||||
namespace 烟雾
|
||||
{
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
public Form1()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
bool IsOpen = false;
|
||||
bool IsFirst = true;//记录线程是否第一次打开
|
||||
Thread thd;//定义Thread线程
|
||||
|
||||
private void Form1_Load(object sender, EventArgs e)
|
||||
{
|
||||
//用 foreach 循环把所有可用的串口号加入列表
|
||||
foreach (string item in SerialPort.GetPortNames())
|
||||
{
|
||||
com_port.Items.Add(item);
|
||||
}
|
||||
thd = new Thread(GetData);//实例化线程,绑定线程运行方法
|
||||
Control.CheckForIllegalCrossThreadCalls = false;//允许跨线程调用
|
||||
}
|
||||
|
||||
private void GetData()
|
||||
{
|
||||
//线程运行方法,不断循环运行
|
||||
while (true)
|
||||
{
|
||||
if (DeviceCtrlHelper.adam4150Helper.SetData().smokeValue)//判断烟雾状态,true为发现烟雾灾情,false为正常状态
|
||||
{
|
||||
txt_time.Text = DateTime.Now.ToString();
|
||||
txt_even.Text = "发现烟情";
|
||||
}
|
||||
else
|
||||
{
|
||||
txt_time.Text = DateTime.Now.ToString();
|
||||
txt_even.Text = "正常";
|
||||
}
|
||||
Thread.Sleep(2000);//线程休眠2s
|
||||
}
|
||||
}
|
||||
|
||||
private void btn_open_Click(object sender, EventArgs e)
|
||||
{
|
||||
IsOpen = !IsOpen;
|
||||
if (IsOpen)
|
||||
{
|
||||
DeviceCtrlHelper.adam4150Helper.Open(com_port.Text);
|
||||
btn_open.Text = "关闭";
|
||||
if (IsFirst)
|
||||
{
|
||||
IsFirst = false;
|
||||
thd.Start();//第一次打开时启动线程
|
||||
}
|
||||
else
|
||||
{
|
||||
thd.Resume();//继续已挂起的线程
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DeviceCtrlHelper.adam4150Helper.Close();
|
||||
btn_open.Text = "开启";
|
||||
thd.Suspend();//挂起线程
|
||||
}
|
||||
}
|
||||
|
||||
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
//挂起的线程不能直接终止,要先让它继续
|
||||
if (thd.ThreadState == ThreadState.Suspended)
|
||||
{
|
||||
thd.Resume();
|
||||
}
|
||||
thd.Abort();//终止线程
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace 烟雾
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用程序的主入口点。
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# 评分规则:书上评价表的功能项。每项单独启动一次程序,按步骤操作,核对结果。
|
||||
# 格式说明见 server/src/grading/checks.ts
|
||||
- title: 按钮能打开串口、开启线程,更改按钮文字
|
||||
steps:
|
||||
- select: { control: com_port, item: COM3 }
|
||||
- click: btn_open
|
||||
- expect: { control: btn_open, text: 关闭, desc: 按钮文字变成"关闭" }
|
||||
- expect: { control: txt_time, matches: '\d{1,2}:\d{2}', within: 5s, desc: 显示当前时间 }
|
||||
|
||||
- title: 能利用线程实时显示烟雾传感器数据
|
||||
steps:
|
||||
- select: { control: com_port, item: COM3 }
|
||||
- click: btn_open
|
||||
- board: { smoke: true }
|
||||
- expect: { control: txt_even, contains: 发现烟情, within: 5s }
|
||||
- board: { smoke: false }
|
||||
- expect: { control: txt_even, contains: 正常, within: 5s }
|
||||
|
||||
- title: 再点按钮能关闭串口、挂起线程,再次打开时线程继续
|
||||
steps:
|
||||
- select: { control: com_port, item: COM3 }
|
||||
- click: btn_open
|
||||
- expect: { control: txt_time, matches: '\d{1,2}:\d{2}', within: 5s, desc: 显示当前时间 }
|
||||
- click: btn_open
|
||||
- expect: { control: btn_open, text: 开启, desc: 按钮文字变回"开启" }
|
||||
- expect: { control: txt_time, unchanged: 5s, desc: 不再更新时间(线程应已挂起) }
|
||||
- click: btn_open
|
||||
- expect: { control: btn_open, text: 关闭, desc: 按钮文字变成"关闭" }
|
||||
- board: { smoke: true }
|
||||
- expect: { control: txt_even, contains: 发现烟情, within: 5s, desc: 继续显示烟雾数据(线程应已继续) }
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "项目5 任务2 采集烟雾传感器数据",
|
||||
"source": "教材书 p98–101,界面取自配套参考工程,控件名按书上表格修改;关闭窗体时先让挂起的线程继续再终止(书上直接 Abort 挂起的线程会出错)"
|
||||
}
|
||||
Generated
+241
@@ -0,0 +1,241 @@
|
||||
namespace 火焰烟雾
|
||||
{
|
||||
partial class Form1
|
||||
{
|
||||
/// <summary>
|
||||
/// 必需的设计器变量。
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有正在使用的资源。
|
||||
/// </summary>
|
||||
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows 窗体设计器生成的代码
|
||||
|
||||
/// <summary>
|
||||
/// 设计器支持所需的方法 - 不要
|
||||
/// 使用代码编辑器修改此方法的内容。
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.led1 = new WinFormsDeviceControlLibrary.LED();
|
||||
this.lbl_time = new System.Windows.Forms.Label();
|
||||
this.txt_time = new System.Windows.Forms.TextBox();
|
||||
this.txt_fire = new System.Windows.Forms.TextBox();
|
||||
this.lbl_fire = new System.Windows.Forms.Label();
|
||||
this.txt_smoke = new System.Windows.Forms.TextBox();
|
||||
this.lbl_smoke = new System.Windows.Forms.Label();
|
||||
this.lbl_Ledport = new System.Windows.Forms.Label();
|
||||
this.com_ledPort = new System.Windows.Forms.ComboBox();
|
||||
this.com_environmentPort = new System.Windows.Forms.ComboBox();
|
||||
this.lbl_4150port = new System.Windows.Forms.Label();
|
||||
this.btn_Open = new System.Windows.Forms.Button();
|
||||
this.btn_get = new System.Windows.Forms.Button();
|
||||
this.groupBox1 = new System.Windows.Forms.GroupBox();
|
||||
this.OpenRecord = new System.Windows.Forms.Button();
|
||||
this.groupBox2 = new System.Windows.Forms.GroupBox();
|
||||
this.groupBox1.SuspendLayout();
|
||||
this.groupBox2.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// led1
|
||||
//
|
||||
this.led1.BackColor = System.Drawing.Color.Black;
|
||||
this.led1.Location = new System.Drawing.Point(19, 0);
|
||||
this.led1.Name = "led1";
|
||||
this.led1.Size = new System.Drawing.Size(800, 130);
|
||||
this.led1.TabIndex = 0;
|
||||
//
|
||||
// lbl_time
|
||||
//
|
||||
this.lbl_time.AutoSize = true;
|
||||
this.lbl_time.Location = new System.Drawing.Point(62, 163);
|
||||
this.lbl_time.Name = "lbl_time";
|
||||
this.lbl_time.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_time.TabIndex = 1;
|
||||
this.lbl_time.Text = "时间";
|
||||
//
|
||||
// txt_time
|
||||
//
|
||||
this.txt_time.Location = new System.Drawing.Point(109, 160);
|
||||
this.txt_time.Name = "txt_time";
|
||||
this.txt_time.Size = new System.Drawing.Size(174, 21);
|
||||
this.txt_time.TabIndex = 2;
|
||||
//
|
||||
// txt_fire
|
||||
//
|
||||
this.txt_fire.Location = new System.Drawing.Point(109, 205);
|
||||
this.txt_fire.Name = "txt_fire";
|
||||
this.txt_fire.Size = new System.Drawing.Size(174, 21);
|
||||
this.txt_fire.TabIndex = 4;
|
||||
//
|
||||
// lbl_fire
|
||||
//
|
||||
this.lbl_fire.AutoSize = true;
|
||||
this.lbl_fire.Location = new System.Drawing.Point(62, 208);
|
||||
this.lbl_fire.Name = "lbl_fire";
|
||||
this.lbl_fire.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_fire.TabIndex = 3;
|
||||
this.lbl_fire.Text = "火焰";
|
||||
//
|
||||
// txt_smoke
|
||||
//
|
||||
this.txt_smoke.Location = new System.Drawing.Point(109, 256);
|
||||
this.txt_smoke.Name = "txt_smoke";
|
||||
this.txt_smoke.Size = new System.Drawing.Size(174, 21);
|
||||
this.txt_smoke.TabIndex = 6;
|
||||
//
|
||||
// lbl_smoke
|
||||
//
|
||||
this.lbl_smoke.AutoSize = true;
|
||||
this.lbl_smoke.Location = new System.Drawing.Point(62, 259);
|
||||
this.lbl_smoke.Name = "lbl_smoke";
|
||||
this.lbl_smoke.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_smoke.TabIndex = 5;
|
||||
this.lbl_smoke.Text = "烟雾";
|
||||
//
|
||||
// lbl_Ledport
|
||||
//
|
||||
this.lbl_Ledport.AutoSize = true;
|
||||
this.lbl_Ledport.Location = new System.Drawing.Point(29, 26);
|
||||
this.lbl_Ledport.Name = "lbl_Ledport";
|
||||
this.lbl_Ledport.Size = new System.Drawing.Size(47, 12);
|
||||
this.lbl_Ledport.TabIndex = 7;
|
||||
this.lbl_Ledport.Text = "L E D";
|
||||
//
|
||||
// com_ledPort
|
||||
//
|
||||
this.com_ledPort.FormattingEnabled = true;
|
||||
this.com_ledPort.Location = new System.Drawing.Point(88, 23);
|
||||
this.com_ledPort.Name = "com_ledPort";
|
||||
this.com_ledPort.Size = new System.Drawing.Size(121, 20);
|
||||
this.com_ledPort.TabIndex = 8;
|
||||
//
|
||||
// com_environmentPort
|
||||
//
|
||||
this.com_environmentPort.FormattingEnabled = true;
|
||||
this.com_environmentPort.Location = new System.Drawing.Point(88, 49);
|
||||
this.com_environmentPort.Name = "com_environmentPort";
|
||||
this.com_environmentPort.Size = new System.Drawing.Size(121, 20);
|
||||
this.com_environmentPort.TabIndex = 10;
|
||||
//
|
||||
// lbl_4150port
|
||||
//
|
||||
this.lbl_4150port.AutoSize = true;
|
||||
this.lbl_4150port.Location = new System.Drawing.Point(29, 52);
|
||||
this.lbl_4150port.Name = "lbl_4150port";
|
||||
this.lbl_4150port.Size = new System.Drawing.Size(53, 12);
|
||||
this.lbl_4150port.TabIndex = 9;
|
||||
this.lbl_4150port.Text = "环境采集";
|
||||
//
|
||||
// btn_Open
|
||||
//
|
||||
this.btn_Open.Location = new System.Drawing.Point(134, 85);
|
||||
this.btn_Open.Name = "btn_Open";
|
||||
this.btn_Open.Size = new System.Drawing.Size(75, 23);
|
||||
this.btn_Open.TabIndex = 11;
|
||||
this.btn_Open.Text = "开启";
|
||||
this.btn_Open.UseVisualStyleBackColor = true;
|
||||
this.btn_Open.Click += new System.EventHandler(this.btn_Open_Click);
|
||||
//
|
||||
// btn_get
|
||||
//
|
||||
this.btn_get.Location = new System.Drawing.Point(270, 20);
|
||||
this.btn_get.Name = "btn_get";
|
||||
this.btn_get.Size = new System.Drawing.Size(49, 117);
|
||||
this.btn_get.TabIndex = 12;
|
||||
this.btn_get.Text = "获取";
|
||||
this.btn_get.UseVisualStyleBackColor = true;
|
||||
this.btn_get.Click += new System.EventHandler(this.btn_get_Click);
|
||||
//
|
||||
// groupBox1
|
||||
//
|
||||
this.groupBox1.Controls.Add(this.OpenRecord);
|
||||
this.groupBox1.Controls.Add(this.lbl_Ledport);
|
||||
this.groupBox1.Controls.Add(this.btn_Open);
|
||||
this.groupBox1.Controls.Add(this.com_ledPort);
|
||||
this.groupBox1.Controls.Add(this.com_environmentPort);
|
||||
this.groupBox1.Controls.Add(this.lbl_4150port);
|
||||
this.groupBox1.Location = new System.Drawing.Point(447, 143);
|
||||
this.groupBox1.Name = "groupBox1";
|
||||
this.groupBox1.Size = new System.Drawing.Size(375, 150);
|
||||
this.groupBox1.TabIndex = 13;
|
||||
this.groupBox1.TabStop = false;
|
||||
this.groupBox1.Text = "配置";
|
||||
//
|
||||
// OpenRecord
|
||||
//
|
||||
this.OpenRecord.Location = new System.Drawing.Point(256, 23);
|
||||
this.OpenRecord.Name = "OpenRecord";
|
||||
this.OpenRecord.Size = new System.Drawing.Size(99, 25);
|
||||
this.OpenRecord.TabIndex = 13;
|
||||
this.OpenRecord.Text = "历史灾情信息";
|
||||
this.OpenRecord.UseVisualStyleBackColor = true;
|
||||
this.OpenRecord.Click += new System.EventHandler(this.OpenRecord_Click);
|
||||
//
|
||||
// groupBox2
|
||||
//
|
||||
this.groupBox2.Controls.Add(this.btn_get);
|
||||
this.groupBox2.Location = new System.Drawing.Point(19, 136);
|
||||
this.groupBox2.Name = "groupBox2";
|
||||
this.groupBox2.Size = new System.Drawing.Size(402, 157);
|
||||
this.groupBox2.TabIndex = 14;
|
||||
this.groupBox2.TabStop = false;
|
||||
this.groupBox2.Text = "数据采集";
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(834, 315);
|
||||
this.Controls.Add(this.txt_smoke);
|
||||
this.Controls.Add(this.lbl_smoke);
|
||||
this.Controls.Add(this.txt_fire);
|
||||
this.Controls.Add(this.lbl_fire);
|
||||
this.Controls.Add(this.txt_time);
|
||||
this.Controls.Add(this.lbl_time);
|
||||
this.Controls.Add(this.led1);
|
||||
this.Controls.Add(this.groupBox1);
|
||||
this.Controls.Add(this.groupBox2);
|
||||
this.Name = "Form1";
|
||||
this.Text = "火焰烟雾";
|
||||
this.Load += new System.EventHandler(this.Form1_Load);
|
||||
this.groupBox1.ResumeLayout(false);
|
||||
this.groupBox1.PerformLayout();
|
||||
this.groupBox2.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private WinFormsDeviceControlLibrary.LED led1;
|
||||
private System.Windows.Forms.Label lbl_time;
|
||||
private System.Windows.Forms.TextBox txt_time;
|
||||
private System.Windows.Forms.TextBox txt_fire;
|
||||
private System.Windows.Forms.Label lbl_fire;
|
||||
private System.Windows.Forms.TextBox txt_smoke;
|
||||
private System.Windows.Forms.Label lbl_smoke;
|
||||
private System.Windows.Forms.Label lbl_Ledport;
|
||||
private System.Windows.Forms.ComboBox com_ledPort;
|
||||
private System.Windows.Forms.ComboBox com_environmentPort;
|
||||
private System.Windows.Forms.Label lbl_4150port;
|
||||
private System.Windows.Forms.Button btn_Open;
|
||||
private System.Windows.Forms.Button btn_get;
|
||||
private System.Windows.Forms.GroupBox groupBox1;
|
||||
private System.Windows.Forms.GroupBox groupBox2;
|
||||
private System.Windows.Forms.Button OpenRecord;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.IO.Ports;
|
||||
using DeviceCtrlLibrary;
|
||||
using DeviceCtrlLibrary.Model;//引用设备控制库下的Model命名空间
|
||||
|
||||
namespace 火焰烟雾
|
||||
{
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
public Form1()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
ADAM4150ResModel ResModel;
|
||||
bool IsOpen = false;
|
||||
string LED_MSG = "";//存储LED显示屏所需发送的文字信息
|
||||
|
||||
private void Form1_Load(object sender, EventArgs e)
|
||||
{
|
||||
com_environmentPort.Items.Clear();
|
||||
com_ledPort.Items.Clear();
|
||||
//将系统串口名依次加入下拉列表框中,由于本题使用串口服务器,所以COM1计算机串口可不加入串口列表
|
||||
for (int i = 0; i < SerialPort.GetPortNames().Length; i++)
|
||||
{
|
||||
//判断计算机的默认串口,将其排除
|
||||
if (SerialPort.GetPortNames()[i].ToUpper() == "COM1")
|
||||
{
|
||||
continue;//跳出此次循环
|
||||
}
|
||||
com_environmentPort.Items.Add(SerialPort.GetPortNames()[i].ToString());
|
||||
com_ledPort.Items.Add(SerialPort.GetPortNames()[i].ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void btn_Open_Click(object sender, EventArgs e)
|
||||
{
|
||||
IsOpen = !IsOpen;
|
||||
if (IsOpen)
|
||||
{
|
||||
DeviceCtrlHelper.adam4150Helper.Open(com_environmentPort.Text);
|
||||
DeviceCtrlHelper.ledHelper.OpenLedCom(com_ledPort.Text);
|
||||
btn_Open.Text = "关闭";
|
||||
}
|
||||
else
|
||||
{
|
||||
DeviceCtrlHelper.adam4150Helper.Close();
|
||||
btn_Open.Text = "开启";
|
||||
}
|
||||
}
|
||||
|
||||
private void btn_get_Click(object sender, EventArgs e)
|
||||
{
|
||||
ResModel = DeviceCtrlHelper.adam4150Helper.SetData();//获取数字量采集器ADAM 4150的最新数据
|
||||
LED_MSG = "";
|
||||
txt_time.Text = DateTime.Now.ToString();
|
||||
txt_fire.Text = "正常";
|
||||
txt_smoke.Text = "正常";
|
||||
if (ResModel.fireValue)
|
||||
{
|
||||
txt_fire.Text = "发现火情";
|
||||
LED_MSG += "发现火情";
|
||||
RecordClass.record[RecordClass.recoudeindex, 0] = txt_time.Text;//记录灾情时间至数组
|
||||
RecordClass.record[RecordClass.recoudeindex, 1] = " 发现火情";//记录灾情内容至数组
|
||||
RecordClass.recoudeindex++;
|
||||
}
|
||||
if (ResModel.smokeValue)
|
||||
{
|
||||
txt_smoke.Text = "发现烟情";
|
||||
LED_MSG += "发现烟情";
|
||||
RecordClass.record[RecordClass.recoudeindex, 0] = txt_time.Text;//记录灾情时间至数组
|
||||
RecordClass.record[RecordClass.recoudeindex, 1] = " 发现烟情";//记录灾情内容至数组
|
||||
RecordClass.recoudeindex++;
|
||||
}
|
||||
string ResStr = DeviceCtrlHelper.ledHelper.SendLedString(LED_MSG);
|
||||
if (!(ResStr.IndexOf("Error") >= 0))
|
||||
led1.setLedText(ResStr);//推送灾情信息至LED显示屏
|
||||
}
|
||||
|
||||
private void OpenRecord_Click(object sender, EventArgs e)
|
||||
{
|
||||
Form2 fr2 = new Form2();
|
||||
fr2.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+60
@@ -0,0 +1,60 @@
|
||||
namespace 火焰烟雾
|
||||
{
|
||||
partial class Form2
|
||||
{
|
||||
/// <summary>
|
||||
/// 必需的设计器变量。
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有正在使用的资源。
|
||||
/// </summary>
|
||||
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows 窗体设计器生成的代码
|
||||
|
||||
/// <summary>
|
||||
/// 设计器支持所需的方法 - 不要
|
||||
/// 使用代码编辑器修改此方法的内容。
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.lstbRecored = new System.Windows.Forms.ListBox();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// lstbRecored
|
||||
//
|
||||
this.lstbRecored.FormattingEnabled = true;
|
||||
this.lstbRecored.ItemHeight = 12;
|
||||
this.lstbRecored.Location = new System.Drawing.Point(12, 26);
|
||||
this.lstbRecored.Name = "lstbRecored";
|
||||
this.lstbRecored.Size = new System.Drawing.Size(260, 148);
|
||||
this.lstbRecored.TabIndex = 0;
|
||||
//
|
||||
// Form2
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(284, 185);
|
||||
this.Controls.Add(this.lstbRecored);
|
||||
this.Name = "Form2";
|
||||
this.Text = "历史灾情记录";
|
||||
this.Load += new System.EventHandler(this.Form2_Load);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.ListBox lstbRecored;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace 火焰烟雾
|
||||
{
|
||||
public partial class Form2 : Form
|
||||
{
|
||||
public Form2()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Form2_Load(object sender, EventArgs e)
|
||||
{
|
||||
//把数组里记录的灾情信息逐条加入列表框
|
||||
for (int i = 0; i < RecordClass.recoudeindex; i++)
|
||||
{
|
||||
lstbRecored.Items.Add(RecordClass.record[i, 0] + " " + RecordClass.record[i, 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace 火焰烟雾
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用程序的主入口点。
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace 火焰烟雾
|
||||
{
|
||||
class RecordClass
|
||||
{
|
||||
public static string[,] record = new string[50, 2];//定义一个二维数组,用于存储灾情信息的时间和内容
|
||||
public static int recoudeindex = 0;//用于记录灾情信息的索引位置
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# 评分规则:书上评价表的功能项。每项单独启动一次程序,按步骤操作,核对结果。
|
||||
# 格式说明见 server/src/grading/checks.ts
|
||||
- title: 能实时显示火焰、烟雾传感器数据
|
||||
steps:
|
||||
- select: { control: com_environmentPort, item: COM3 }
|
||||
- select: { control: com_ledPort, item: COM5 }
|
||||
- click: btn_Open
|
||||
- expect: { control: btn_Open, text: 关闭, desc: 按钮文字变成"关闭" }
|
||||
- board: { fire: true }
|
||||
- click: btn_get
|
||||
- expect: { control: txt_fire, contains: 发现火情 }
|
||||
- expect: { control: txt_smoke, contains: 正常 }
|
||||
- expect: { control: txt_time, matches: '\d{1,2}:\d{2}', desc: 显示当前时间 }
|
||||
- board: { fire: false, smoke: true }
|
||||
- click: btn_get
|
||||
- expect: { control: txt_fire, contains: 正常 }
|
||||
- expect: { control: txt_smoke, contains: 发现烟情 }
|
||||
|
||||
- title: 能正确推送灾情信息到 LED 屏
|
||||
steps:
|
||||
- select: { control: com_environmentPort, item: COM3 }
|
||||
- select: { control: com_ledPort, item: COM5 }
|
||||
- click: btn_Open
|
||||
- board: { fire: true }
|
||||
- click: btn_get
|
||||
- expect: { led: 发现火情 }
|
||||
- expect: { control: led1, ledText: 发现火情, desc: 窗体上的 LED 控件显示"发现火情" }
|
||||
|
||||
- title: 能利用数组存储灾情信息,在历史灾情窗体里显示
|
||||
steps:
|
||||
- select: { control: com_environmentPort, item: COM3 }
|
||||
- select: { control: com_ledPort, item: COM5 }
|
||||
- click: btn_Open
|
||||
- board: { fire: true }
|
||||
- click: btn_get
|
||||
- board: { fire: false, smoke: true }
|
||||
- click: btn_get
|
||||
- click: OpenRecord
|
||||
- expect: { control: lstbRecored, contains: 发现火情, desc: 列表里有"发现火情"的记录 }
|
||||
- expect: { control: lstbRecored, contains: 发现烟情, desc: 列表里有"发现烟情"的记录 }
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "项目5 任务3 实时推送灾情信息",
|
||||
"source": "教材书 p104–107,界面取自配套参考工程,控件名按书上表格修改;修正烟雾记成“发现火情”的错误,列表框按表5-7 命名为 lstbRecored"
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export type Step =
|
||||
export type Expectation =
|
||||
/** 控件属性等于某个值:text → Text,ledText → LedText(LED 控件),enabled → Enabled,checked → Checked,on → IsOn(灯泡、报警灯) */
|
||||
| { kind: 'prop'; control: string; prop: string; value: unknown }
|
||||
/** 控件文字包含某段文字;列表框是任意一个列表项包含 */
|
||||
| { kind: 'contains'; control: string; text: string }
|
||||
| { kind: 'matches'; control: string; pattern: RegExp; source: string }
|
||||
/** 一段时间内控件文字不变(比如定时器已经停止) */
|
||||
|
||||
@@ -134,13 +134,20 @@ async function expectStep(driver: ProgramDriver, step: Extract<Step, { kind: 'ex
|
||||
if (!ok) throw new Failure(describeFailure(driver, e, step.desc, wait))
|
||||
}
|
||||
|
||||
/** 列表框的 Text 只是选中的那一项,检查"包含"时看所有列表项 */
|
||||
function listItems(node: ControlNode | undefined): string[] | undefined {
|
||||
return node?.type === 'ListBox' ? ((node.props.Items as unknown[] | undefined) ?? []).map(String) : undefined
|
||||
}
|
||||
|
||||
function satisfied(driver: ProgramDriver, e: Exclude<Expectation, { kind: 'unchanged' }>): boolean {
|
||||
const props = (name: string) => driver.find(name)?.props
|
||||
switch (e.kind) {
|
||||
case 'prop':
|
||||
return props(e.control)?.[e.prop] === e.value
|
||||
case 'contains':
|
||||
return String(props(e.control)?.Text ?? '').includes(e.text)
|
||||
case 'contains': {
|
||||
const items = listItems(driver.find(e.control))
|
||||
return items ? items.some((item) => item.includes(e.text)) : String(props(e.control)?.Text ?? '').includes(e.text)
|
||||
}
|
||||
case 'matches':
|
||||
return e.pattern.test(String(props(e.control)?.Text ?? ''))
|
||||
case 'led':
|
||||
@@ -168,8 +175,12 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
if (e.prop === 'IsOn') return `期望${prefix}窗体上的 ${e.control} ${desc ?? (e.value ? '亮' : '灭')},实际是${actual ? '亮' : '灭'}的`
|
||||
return `期望${prefix} ${e.control} 的 ${e.prop} 是 ${String(e.value)},实际是 ${String(actual)}`
|
||||
}
|
||||
case 'contains':
|
||||
return `期望${prefix} ${e.control} ${desc ?? `的文字包含"${e.text}"`},实际是${quote(need(driver, e.control).props.Text)}`
|
||||
case 'contains': {
|
||||
const node = need(driver, e.control)
|
||||
const items = listItems(node)
|
||||
if (items) return `期望${prefix} ${e.control} ${desc ?? `的列表项包含"${e.text}"`},实际${items.length ? `列表里是 ${items.map((i) => `"${i}"`).join('、')}` : '列表是空的'}`
|
||||
return `期望${prefix} ${e.control} ${desc ?? `的文字包含"${e.text}"`},实际是${quote(node.props.Text)}`
|
||||
}
|
||||
case 'matches':
|
||||
return `期望${prefix} ${e.control} ${desc ?? `的文字符合 ${e.pattern}`},实际是${quote(need(driver, e.control).props.Text)}`
|
||||
case 'led':
|
||||
|
||||
@@ -425,6 +425,13 @@ describe('语言服务', () => {
|
||||
expect((await t.post('/api/language', { type: 'run', session: 'tab1' })).status).toBe(400)
|
||||
expect((await new Browser().post('/api/language', { type: 'complete', session: 'tab1', files })).status).toBe(401)
|
||||
}, 60000)
|
||||
|
||||
test('平台支持的旧版线程 API(Suspend / Resume / Abort)不提示"已过时"', async () => {
|
||||
const t = await teacher()
|
||||
const files = [{ path: 'Program.cs', content: 'using System.Threading;\nstatic class P\n{\n static void Main()\n {\n var t = new Thread(() => Thread.Sleep(10));\n t.Start();\n t.Suspend();\n t.Resume();\n t.Abort();\n }\n}\n' }]
|
||||
const check = await t.post('/api/language', { type: 'diagnostics', session: 'tab2', files })
|
||||
expect(check.body.diagnostics).toEqual([])
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
describe('AI 评阅', () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ async function prepare(id: string, name: string, edit?: { file: string; from: st
|
||||
|
||||
describe('参考答案全部通过', async () => {
|
||||
const ids = (await Array.fromAsync(new Bun.Glob('*/checks.yaml').scan({ cwd: SAMPLES }))).map((p) => p.split('/')[0]).sort()
|
||||
test('每个示例都有评分规则', () => expect(ids.length).toBe(10))
|
||||
test('每个示例都有评分规则', () => expect(ids.length).toBe(12))
|
||||
for (const id of ids) {
|
||||
test(id, async () => {
|
||||
const result = await grade(await prepare(id, id), await checksOf(id))
|
||||
@@ -80,6 +80,22 @@ describe('改坏的代码', () => {
|
||||
expect(stop.message).toStartWith('期望 txt_time 不再更新时间(定时器应已停止),实际从')
|
||||
}, 60000)
|
||||
|
||||
test('关闭时忘了挂起线程', async () => {
|
||||
const id = 'p5-t2-烟雾'
|
||||
const dir = await prepare(id, 'thread', { file: 'Form1.cs', from: 'thd.Suspend();//挂起线程', to: '' })
|
||||
const result = await grade(dir, await checksOf(id))
|
||||
const stop = result.results.find((r) => r.title === '再点按钮能关闭串口、挂起线程,再次打开时线程继续')!
|
||||
expect(stop.passed).toBe(false)
|
||||
expect(stop.message).toStartWith('期望 txt_time 不再更新时间(线程应已挂起),实际从')
|
||||
}, 60000)
|
||||
|
||||
test('历史灾情窗体没有把数组里的记录加入列表框', async () => {
|
||||
const id = 'p5-t3-推送灾情'
|
||||
const dir = await prepare(id, 'listbox', { file: 'Form2.cs', from: 'lstbRecored.Items.Add(', to: 'Console.WriteLine(' })
|
||||
const result = await grade(dir, await checksOf(id))
|
||||
expect(result.results[2]).toMatchObject({ passed: false, message: '期望 lstbRecored 列表里有"发现火情"的记录,实际列表是空的' })
|
||||
}, 60000)
|
||||
|
||||
test('报警灯接错了 DO 口', async () => {
|
||||
const id = 'p4-t3-报警灯'
|
||||
const dir = await prepare(id, 'relay', { file: 'Form1.cs', from: 'DeviceCtrlHelper.adam4150Helper.OnOff(0, IsOpen);', to: 'DeviceCtrlHelper.adam4150Helper.OnOff(1, IsOpen);' })
|
||||
|
||||
@@ -335,6 +335,52 @@ test('项目5 任务1:有火焰时显示"发现火情!"', async () => {
|
||||
await runner.next('patch', (m) => m.changes.some((c) => c.id === id('txt_even') && c.value === '发现火情!'), 5000)
|
||||
}, 30000)
|
||||
|
||||
test('项目5 任务2:线程实时显示烟雾数据;关闭时线程挂起、再打开时继续;关闭窗体后程序正常结束', async () => {
|
||||
const { runner, id, form } = await startSample('p5-t2-烟雾')
|
||||
// 改写后的代码不再用过时的 API,不应出现"已过时"警告
|
||||
expect((await runner.waitFor('compiled')).diagnostics).toEqual([])
|
||||
const changed = (name: string, match: (value: unknown) => boolean = () => true, timeout = 5000) =>
|
||||
runner.next('patch', (m) => m.changes.some((c) => c.id === id(name) && match(c.value)), timeout)
|
||||
const normal = changed('txt_even', (v) => v === '正常')
|
||||
await openAdam4150(runner, id)
|
||||
await normal
|
||||
runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: [false, false, true, false] })
|
||||
await changed('txt_even', (v) => v === '发现烟情')
|
||||
|
||||
// 关闭:线程挂起,时间不再更新
|
||||
runner.send({ type: 'action', id: id('btn_open'), action: 'click' })
|
||||
await changed('btn_open', (v) => v === '开启')
|
||||
await expect(changed('txt_time', undefined, 3000)).rejects.toThrow('超时')
|
||||
|
||||
// 再打开:线程继续
|
||||
runner.send({ type: 'action', id: id('btn_open'), action: 'click' })
|
||||
await changed('txt_time', undefined, 3000)
|
||||
|
||||
// 挂起后关闭窗体:先 Resume 再 Abort,程序正常结束
|
||||
runner.send({ type: 'action', id: id('btn_open'), action: 'click' })
|
||||
await changed('btn_open', (v) => v === '开启')
|
||||
runner.send({ type: 'action', id: form.id, action: 'close' })
|
||||
expect((await runner.finished()).code).toBe(0)
|
||||
expect(runner.messages.filter((m) => m.type === 'exception')).toEqual([])
|
||||
}, 30000)
|
||||
|
||||
test('项目5 任务2:照书上直接 Abort 挂起的线程,和 .NET Framework 一样报错,并提示先 Resume', async () => {
|
||||
const dir = await prepareSample('p5-t2-烟雾')
|
||||
const file = `${dir}/Form1.cs`
|
||||
const code = await Bun.file(file).text()
|
||||
await Bun.write(file, code.replace(/if \(thd\.ThreadState == ThreadState\.Suspended\)\s*\{\s*thd\.Resume\(\);\s*\}/, ''))
|
||||
runner = await Runner.start(dir)
|
||||
const { form } = await runner.waitFor('formShown')
|
||||
const id = (name: string) => findByName(form, name).id
|
||||
await openAdam4150(runner, id)
|
||||
runner.send({ type: 'action', id: id('btn_open'), action: 'click' })
|
||||
await runner.waitFor('patch', (m) => m.changes.some((c) => c.id === id('btn_open') && c.value === '开启'))
|
||||
runner.send({ type: 'action', id: form.id, action: 'close' })
|
||||
const error = await runner.next('exception')
|
||||
expect(error.message).toContain('ThreadStateException')
|
||||
expect(error.message).toContain('先调用 Resume()')
|
||||
}, 30000)
|
||||
|
||||
/** 把一段 Main 方法体写成工程并运行,收集它的控制台输出 */
|
||||
async function runMain(name: string, body: string, usings = '') {
|
||||
const dir = resolve(ROOT, '.runs', `test-${name}`)
|
||||
@@ -347,6 +393,96 @@ async function runMain(name: string, body: string, usings = '') {
|
||||
return { runner, dir }
|
||||
}
|
||||
|
||||
test('旧版线程 API:Suspend / Resume / Abort、线程自己挂起、休眠中被终止、ThreadState', async () => {
|
||||
const { runner } = await runMain(
|
||||
'legacy-threads',
|
||||
`
|
||||
int count = 0;
|
||||
Thread worker = new Thread(() => { while (true) { count++; Thread.Sleep(20); } });
|
||||
Console.WriteLine("state0=" + worker.ThreadState);
|
||||
worker.Start();
|
||||
Thread.Sleep(200);
|
||||
worker.Suspend();
|
||||
Thread.Sleep(100);
|
||||
int frozen = count;
|
||||
Console.WriteLine("state1=" + worker.ThreadState);
|
||||
Thread.Sleep(200);
|
||||
Console.WriteLine("frozen=" + (count == frozen));
|
||||
try { worker.Abort(); } catch (ThreadStateException) { Console.WriteLine("abortSuspended=error"); }
|
||||
worker.Resume();
|
||||
Console.WriteLine("joined=" + worker.Join(1000) + "," + worker.ThreadState);
|
||||
|
||||
Thread self = null;
|
||||
self = new Thread(() => { self.Suspend(); Console.WriteLine("self=resumed"); });
|
||||
self.Start();
|
||||
Thread.Sleep(100);
|
||||
Console.WriteLine("selfState=" + self.ThreadState);
|
||||
self.Resume();
|
||||
self.Join(1000);
|
||||
|
||||
Thread sleeper = new Thread(new ThreadStart(Sleepy));
|
||||
sleeper.Start();
|
||||
Thread.Sleep(100);
|
||||
try { sleeper.Resume(); } catch (ThreadStateException) { Console.WriteLine("resumeRunning=error"); }
|
||||
sleeper.Abort();
|
||||
Console.WriteLine("sleeperJoined=" + sleeper.Join(1000));
|
||||
|
||||
Thread unstarted = new Thread(Sleepy);
|
||||
unstarted.Abort();
|
||||
Console.WriteLine("unstarted=" + unstarted.ThreadState);
|
||||
|
||||
var param = new Thread(o => Console.WriteLine("param=" + o));
|
||||
param.Start(42);
|
||||
param.Join();
|
||||
}
|
||||
|
||||
static void Sleepy()
|
||||
{
|
||||
Thread.Sleep(100000);
|
||||
Console.WriteLine("sleeper=woke");`,
|
||||
'using System.Threading;',
|
||||
)
|
||||
expect((await runner.waitFor('compiled')).ok).toBe(true)
|
||||
await runner.finished()
|
||||
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
|
||||
expect(runner.messages.filter((m) => m.type === 'exception')).toEqual([])
|
||||
for (const line of [
|
||||
'state0=Unstarted', 'state1=Suspended', 'frozen=True', 'abortSuspended=error', 'joined=True,Stopped',
|
||||
'selfState=Suspended', 'self=resumed', 'resumeRunning=error', 'sleeperJoined=True', 'unstarted=Unstarted, AbortRequested', 'param=42',
|
||||
]) {
|
||||
expect(output).toContain(line)
|
||||
}
|
||||
expect(output).not.toContain('sleeper=woke')
|
||||
}, 30000)
|
||||
|
||||
test('ListBox:点某一项先选中(SelectedIndexChanged)再触发 Click,Text 是选中项;双击触发 DoubleClick', async () => {
|
||||
const { runner } = await runMain(
|
||||
'listbox',
|
||||
`
|
||||
var form = new Form();
|
||||
var list = new ListBox { Name = "list" };
|
||||
list.Items.AddRange(new object[] { "甲", "乙", "丙" });
|
||||
list.SelectedIndexChanged += (s, e) => Console.WriteLine("changed=" + list.SelectedIndex + "," + list.Text);
|
||||
list.Click += (s, e) => Console.WriteLine("click");
|
||||
list.DoubleClick += (s, e) => { Console.WriteLine("double=" + list.SelectedItem); list.Items.RemoveAt(1); };
|
||||
form.Controls.Add(list);
|
||||
Application.Run(form);`,
|
||||
'using System.Windows.Forms;',
|
||||
)
|
||||
const { form } = await runner.waitFor('formShown')
|
||||
const list = findByName(form, 'list')
|
||||
expect(list.props.Items).toEqual(['甲', '乙', '丙'])
|
||||
runner.send({ type: 'action', id: list.id, action: 'select', index: 1 })
|
||||
await runner.waitFor('console', (m) => m.text === 'click\n')
|
||||
runner.send({ type: 'action', id: list.id, action: 'doubleClick' })
|
||||
// 删掉选中的项后取消选中,浏览器收到新的列表
|
||||
const patch = await runner.next('patch', (m) => m.changes.some((c) => c.prop === 'Items'))
|
||||
expect(patch.changes).toContainEqual({ id: list.id, prop: 'SelectedIndex', value: -1 })
|
||||
expect(patch.changes).toContainEqual({ id: list.id, prop: 'Items', value: ['甲', '丙'] })
|
||||
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
|
||||
expect(output).toBe('changed=1,乙\nclick\ndouble=乙\n')
|
||||
}, 30000)
|
||||
|
||||
test.skipIf(config.sandbox === 'none')('沙箱:看不到服务器文件、别的运行目录、环境变量和其他进程,工程目录只读', async () => {
|
||||
process.env.SIM_TEST_SECRET = '不该被看到'
|
||||
const { runner } = await runMain(
|
||||
|
||||
@@ -107,6 +107,7 @@ export type UserAction =
|
||||
| { action: 'text'; value: string }
|
||||
| { action: 'dropDown' }
|
||||
| { action: 'select'; index: number }
|
||||
| { action: 'doubleClick' }
|
||||
| { action: 'mouseDown'; button: 'left' | 'right'; x: number; y: number }
|
||||
| { action: 'mouseUp'; button: 'left' | 'right'; x: number; y: number }
|
||||
| { action: 'close' }
|
||||
|
||||
@@ -159,6 +159,26 @@ export const CONTROLS: ControlDef[] = [
|
||||
events: [...COMMON_EVENTS, { name: 'SelectedIndexChanged', label: '选中项改变', ...EVENT }, { name: 'DropDown', label: '展开列表', ...EVENT }],
|
||||
initial: () => [['FormattingEnabled', bool(true)], ['Size', size(121, 20)]],
|
||||
},
|
||||
{
|
||||
type: `${FORMS}.ListBox`,
|
||||
label: '列表框',
|
||||
group: '常用控件',
|
||||
namePrefix: 'listBox',
|
||||
defaultEvent: 'SelectedIndexChanged',
|
||||
props: [
|
||||
...COMMON_PROPS.filter((p) => p.name !== 'Text'),
|
||||
{ name: 'Items', label: '列表项', description: '列表里的项,每行一项', type: 'items', category: '数据' },
|
||||
{
|
||||
name: 'BorderStyle', label: '边框', description: '边框', type: 'enum', enumType: `${FORMS}.BorderStyle`,
|
||||
options: ['None', 'FixedSingle', 'Fixed3D'], optionLabels: { None: '无', FixedSingle: '单线', Fixed3D: '立体' }, default: enumValue(`${FORMS}.BorderStyle`, 'Fixed3D'), category: '外观',
|
||||
},
|
||||
],
|
||||
events: [
|
||||
...COMMON_EVENTS.filter((e) => e.name !== 'TextChanged'),
|
||||
{ name: 'SelectedIndexChanged', label: '选中项改变', ...EVENT },
|
||||
],
|
||||
initial: () => [['FormattingEnabled', bool(true)], ['ItemHeight', int(12)], ['Size', size(120, 88)]],
|
||||
},
|
||||
{
|
||||
type: `${FORMS}.CheckBox`,
|
||||
label: '复选框',
|
||||
|
||||
@@ -63,7 +63,7 @@ export function previewTree(doc: DesignerDocument): ControlNode {
|
||||
const props: Record<string, unknown> = { ...DEVICE_DEFAULTS[type], ...propsOf(doc, name) }
|
||||
// 设计时即使 Visible = false 也显示出来(和 VS 一样),方便选中和编辑
|
||||
props.Visible = true
|
||||
if (type === 'System.Windows.Forms.ComboBox') props.Items = getItems(doc, name).map((item) => {
|
||||
if (type === 'System.Windows.Forms.ComboBox' || type === 'System.Windows.Forms.ListBox') props.Items = getItems(doc, name).map((item) => {
|
||||
const v = parseValue(item)
|
||||
return v.kind === 'string' ? v.value : item
|
||||
})
|
||||
|
||||
@@ -48,6 +48,9 @@ function selectItem(index: number) {
|
||||
dropDownOpen.value = false
|
||||
act({ action: 'select', index })
|
||||
}
|
||||
|
||||
// ListBox
|
||||
const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined) ?? 12}px`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -108,6 +111,27 @@ function selectItem(index: number) {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ListBox:点空白处只触发 Click,点某一项先选中再触发 Click -->
|
||||
<div
|
||||
v-else-if="node.type === 'ListBox'"
|
||||
class="wf-control wf-listbox"
|
||||
:class="[`wf-listbox-border-${p.BorderStyle ?? 'Fixed3D'}`, { 'wf-disabled': !enabled }]"
|
||||
:style="style"
|
||||
@click.self="act({ action: 'click' })"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="wf-listbox-item"
|
||||
:class="{ 'wf-listbox-selected': index === p.SelectedIndex }"
|
||||
:style="{ height: itemHeight, lineHeight: itemHeight }"
|
||||
@click="act({ action: 'select', index })"
|
||||
@dblclick="act({ action: 'doubleClick' })"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CheckBox / RadioButton:自绘勾选框,状态完全由运行器决定(点击只上报,不在浏览器里自行切换) -->
|
||||
<div
|
||||
v-else-if="node.type === 'CheckBox' || node.type === 'RadioButton'"
|
||||
|
||||
@@ -151,6 +151,36 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wf-listbox {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: #fff;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.wf-listbox-border-Fixed3D {
|
||||
border: 1px solid #828790;
|
||||
}
|
||||
|
||||
.wf-listbox-border-FixedSingle {
|
||||
border: 1px solid #646464;
|
||||
}
|
||||
|
||||
.wf-listbox-item {
|
||||
padding: 0 2px;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wf-listbox-selected {
|
||||
background: #0078d7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wf-disabled .wf-listbox-selected {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
|
||||
.wf-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in new issue
Block a user