Files
IOT_SIM/runtime/Sim.WinForms/Host/SimHost.cs
T
xuyueandClaude Opus 5.5 eedf1aa2fb 项目 7 情景控制:虚拟时钟、项目资源、Windows Media Player、TrackBar
- 虚拟时钟:实训台时钟面板可拨时间、1×/10×/60× 倍速、恢复真实时间;编译时把
  DateTime.Now / Today / UtcNow 改写成 SimClock;评分规则加 clock 步骤
- 项目资源(Properties.Resources):平台素材库(自绘开关图片、合成门铃声),"项目资源"对话框勾选,
  平台生成 Properties/Resources.Designer.cs;设计器图片框的"图片"属性可选资源;程序目录下的 Resources
  指向素材库;未添加资源时给中文提示
- Windows Media Player(AxWMPLib):设置 URL 自动播放、Ctlcontrols.play/stop,浏览器里播放声音,
  实训台加广播喇叭;编译时给窗体嵌空资源,OcxState 取到 null 不报错;图片和声音统一按内容缓存(blob)
- TrackBar:拖动、点轨道、方向键,先 ValueChanged 再 Scroll;设计器加"滑动条""媒体播放器"
- 评分规则加 value、media 检查;示例项目 7 任务 1–3(修正书上开关灯判断写反、漏填串口列表)
- 修正:Properties/ 下的资源类不当作窗体;带命名空间的 System.AppDomain.CurrentDomain.BaseDirectory 放行
- 测试:虚拟时钟、TrackBar、项目 7 示例评分、作业起点带资源类

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:11:05 +08:00

194 lines
7.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Windows.Forms;
namespace Sim.Host;
/// <summary>
/// 运行器的全局状态:向服务器发消息、收集控件属性变化、登记控件、等待浏览器的应答。
/// </summary>
public static class SimHost
{
// 中文原样输出,不转义成 \uXXXX
private static readonly JsonSerializerOptions JsonOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
private static readonly object WriteLock = new();
private static readonly List<JsonObject> PendingChanges = new();
private static readonly ConcurrentDictionary<string, Control> ControlsById = new();
private static readonly ConcurrentDictionary<int, PendingRequest> Requests = new();
private static TextWriter _output = TextWriter.Null;
private static int _nextControlId;
private static int _nextRequestId;
public static UiDispatcher Ui { get; } = new();
/// <summary>学生程序所在的程序集,用于筛选异常堆栈中属于学生代码的部分</summary>
public static Assembly? StudentAssembly { get; set; }
/// <summary>运行器启动时调用:协议消息写到真正的标准输出(学生的 Console 输出另行转发)</summary>
public static void Initialize(Stream stdout)
{
_output = new StreamWriter(stdout, new UTF8Encoding(false)) { AutoFlush = true };
}
// ---------- 发送消息 ----------
public static void Send(JsonObject message)
{
lock (WriteLock)
{
FlushChangesLocked();
_output.WriteLine(message.ToJsonString(JsonOptions));
}
}
/// <summary>记录一个已显示控件的属性变化;在界面线程上会合并到本轮消息处理结束时一起发送</summary>
public static void QueueChange(Control control, string prop, object? value)
{
var change = new JsonObject { ["id"] = control.SimId, ["prop"] = prop, ["value"] = PropValue.From(value) };
lock (WriteLock)
{
PendingChanges.Add(change);
if (!Ui.IsUiThread) FlushChangesLocked();
}
}
public static void FlushChanges()
{
lock (WriteLock) FlushChangesLocked();
}
private static void FlushChangesLocked()
{
if (PendingChanges.Count == 0) return;
var changes = new JsonArray();
foreach (var change in PendingChanges) changes.Add(change);
PendingChanges.Clear();
_output.WriteLine(new JsonObject { ["type"] = "patch", ["changes"] = changes }.ToJsonString(JsonOptions));
}
// ---------- 图片 ----------
private static readonly HashSet<string> SentImages = new();
/// <summary>
/// 学生程序的"程序所在目录"(AppDomain.CurrentDomain.BaseDirectory),可写,程序结束后清掉。
/// 运行器启动时设定;保存的图片按相对这里的路径显示在网页的照片列表里。
/// </summary>
public static string AppBaseDirectory { get; set; } = AppContext.BaseDirectory;
/// <summary>图片在浏览器端的编号;第一次用到时先把图片数据发过去,浏览器缓存后同一张图不再重发</summary>
public static string ImageKey(System.Drawing.Image image) => DataKey(image.SimKey, image.Data, image.MimeType);
/// <summary>
/// 图片、声音等数据在浏览器端的编号(key 通常是内容的哈希):第一次用到时先把数据发过去,之后只发编号。
/// </summary>
public static string DataKey(string key, byte[] data, string mime)
{
bool isNew;
lock (SentImages) isNew = SentImages.Add(key);
if (isNew)
{
Send(new JsonObject
{
["type"] = "blob",
["key"] = key,
["mime"] = mime,
["data"] = Convert.ToBase64String(data),
});
}
return key;
}
/// <summary>学生程序用 Image.Save 保存了图片:网页的照片列表里显示出来</summary>
public static void ImageSaved(string fullPath, System.Drawing.Image image)
{
var path = fullPath.StartsWith(AppBaseDirectory, StringComparison.Ordinal) ? fullPath[AppBaseDirectory.Length..] : fullPath;
Send(new JsonObject { ["type"] = "imageSaved", ["path"] = path, ["image"] = ImageKey(image) });
}
public static void SendConsole(string text) => Send(new JsonObject { ["type"] = "console", ["text"] = text });
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),
});
}
// ---------- 异常 ----------
public static void ReportException(Exception ex)
{
while (ex is TargetInvocationException { InnerException: not null } wrapper) ex = wrapper.InnerException!;
Send(new JsonObject
{
["type"] = "exception",
["message"] = $"{ex.GetType().FullName}: {ex.Message}",
["detail"] = StudentStackTrace(ex),
});
}
/// <summary>只保留学生代码的堆栈行,去掉仿真库内部的调用</summary>
private static string StudentStackTrace(Exception ex)
{
var lines = (ex.StackTrace ?? "").Split('\n').Select(l => l.TrimEnd());
var studentLines = lines.Where(l => l.Contains(".cs:") && !l.Contains("/runtime/Sim.")).ToList();
return string.Join("\n", studentLines.Count > 0 ? studentLines : lines);
}
// ---------- 控件登记 ----------
public static string NewControlId() => Interlocked.Increment(ref _nextControlId).ToString();
public static void Register(Control control) => ControlsById[control.SimId] = control;
public static Control? FindControl(string id) => ControlsById.TryGetValue(id, out var control) ? control : null;
// ---------- 请求与应答(如 MessageBox 等待用户点按钮) ----------
/// <summary>
/// 向浏览器发出请求并阻塞等待应答。在界面线程上调用时会继续处理消息队列(和真实的模态对话框一样,Timer 照常触发)。
/// </summary>
public static JsonNode? Request(JsonObject message)
{
LegacyThreads.CheckPoint();
var id = Interlocked.Increment(ref _nextRequestId);
var pending = new PendingRequest();
Requests[id] = pending;
message["requestId"] = id;
Send(message);
if (Ui.IsUiThread) Ui.RunUntil(() => pending.Done.IsSet);
else pending.Done.Wait();
Requests.TryRemove(id, out _);
return pending.Result;
}
public static void CompleteRequest(int id, JsonNode? result)
{
if (!Requests.TryGetValue(id, out var pending)) return;
pending.Result = result;
pending.Done.Set();
Ui.Wake();
}
private sealed class PendingRequest
{
public ManualResetEventSlim Done { get; } = new();
public JsonNode? Result { get; set; }
}
}