项目 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>
This commit is contained in:
1 parent
a9af26c2d6
commit
eedf1aa2fb
66 files changed
+3538
-43
No files matched your search
@@ -67,8 +67,8 @@ Docker Compose,HTTPS 由服务器上的 Nginx Proxy Manager 负责。推送到
|
||||
已完成:
|
||||
|
||||
- **虚拟实训台**:按教材实训台布局图排列(红外对射、警示灯、LED 屏、照明灯、人体红外、温湿度、光照、烟雾、火焰、ADAM 模块)。每个浏览器会话一台,状态由服务器统一保存:传感器读数由网页上的滑块和开关设定,经服务器推给学生程序;灯、继电器、LED 屏由学生程序控制。
|
||||
- 新控件:CheckBox、RadioButton(事件顺序与 WinForms 一致)、ListBox、PictureBox,教材控件库的 Lamp、Alarm。
|
||||
- 示例工程(按书上代码整理):项目 1 任务 1–2、项目 2 任务 1–3、项目 3 任务 1–2、项目 4 任务 1–3、项目 5 任务 1–3、项目 6 任务 2–3,共 15 个(项目 6 任务 1 在数据库页面上完成)。网址带 `?sample=示例编号` 可直接打开指定示例。
|
||||
- 新控件:CheckBox、RadioButton(事件顺序与 WinForms 一致)、ListBox、PictureBox、DataGridView、DateTimePicker、TrackBar、Windows Media Player,教材控件库的 Lamp、Alarm。
|
||||
- 示例工程(按书上代码整理):项目 1 任务 1–2、项目 2 任务 1–3、项目 3 任务 1–2、项目 4 任务 1–3、项目 5 任务 1–3、项目 6 任务 2–3、项目 7 任务 1–3,共 18 个(项目 6 任务 1 在数据库页面上完成)。网址带 `?sample=示例编号` 可直接打开指定示例。
|
||||
- 代码编辑器(Monaco):C# 高亮、编译错误波浪线、点错误跳到对应行、F5 运行 / Shift+F5 停止、草稿自动保存在浏览器本地。
|
||||
- 窗体设计器(仿 VS,界面中文):工具箱、拖放与改大小、属性窗口、事件窗口、双击生成事件处理方法、撤销 / 重做;读写 VS 格式的 Designer.cs。
|
||||
- 多窗体:新建、删除窗体;运行时多个窗口可切换前后。
|
||||
@@ -78,4 +78,4 @@ Docker Compose,HTTPS 由服务器上的 Nginx Proxy Manager 负责。推送到
|
||||
|
||||
尚未完成:
|
||||
|
||||
- 教材项目 7–8(需要 TrackBar、资源图片、图表、虚拟时钟、无边框窗体等)。
|
||||
- 教材项目 8(需要图表、无边框窗体、Panel 停靠、背景图片等)。
|
||||
+10
-3
@@ -214,7 +214,7 @@ IOT/
|
||||
| 4 入侵监测 | 红外对射检测;网络摄像机画面与云台;报警灯 | PictureBox、摄像头仿真、Alarm 控件、图片保存 | 已完成 |
|
||||
| 5 灾情监测 | 火焰(Timer)、烟雾(Thread)、灾情记录与推送 | Thread(含 Suspend / Resume / Abort)、跨线程改界面、ListBox、FormClosing | 已完成 |
|
||||
| 6 数据存储 | 建库建表、采集入库与增删改、按时间段查询 | SQL 窗口(代替 SSMS)、SqlClient 仿冒(SQLite)、DataGridView、DateTimePicker | 已完成 |
|
||||
| 7 情景控制 | 智慧教室、寝室、会议室三种情景联动 | TrackBar、资源图片、PictureBox 当按钮、mp3 播放、虚拟时钟 | |
|
||||
| 7 情景控制 | 智慧教室、寝室、会议室三种情景联动 | TrackBar、资源图片、PictureBox 当按钮、mp3 播放、虚拟时钟 | 已完成 |
|
||||
| 8 管理平台 | 主界面、校园环境(自动控制、7 天温度图表)、校园安防(抓拍入库、历史图片) | Panel Dock、无边框窗体、透明背景、Chart、ShowDialog、图片存数据库 | |
|
||||
|
||||
每个实验以数据形式存放(目前在 `samples/`,以后迁到 `content/`),由以下部分组成:
|
||||
@@ -237,7 +237,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
|
||||
@@ -291,6 +291,13 @@ flowchart TB
|
||||
- **评分**:每个实验可以带 `database.sql`,评分时每个检查项用一份新建的实验库,不碰学生自己的数据;评分规则加"点 / 修改表格单元格""在日期时间选择器里选日期""表格里有 / 没有某行"。新增示例项目 6 任务 2、3(任务 1 在数据库页面上完成),共 15 个。
|
||||
- 实测:用到数据库的学生程序比不用的多约 11 MB 内存(PSS 17 → 28 MB)。另外修正了平台上的时间格式:Linux 的中文区域数据是 09:05:03,改成和中文 Windows 一样的 9:05:03。
|
||||
|
||||
- 2026-10-23:项目 7 情景控制(3 个任务),共 18 个示例。
|
||||
- **虚拟时钟**:实训台加时钟面板,默认真实时间,可以拨到任意日期时刻、选 1×/10×/60× 倍速、一键恢复真实时间;时钟状态由服务器保存(`BoardState.clock`),运行器按它算当前时间。编译时把学生代码里的 `DateTime.Now / Today / UtcNow` 改写成 `SimClock`(和线程 API 的改写一样不换行、编辑器里看到的是原代码)。任务 2 按 6:45 起床、7:30 上课、21:00 回寝、22:00 熄灯切换,任务 3 按预约时刻开启,都靠它来测试。评分规则加 `clock` 步骤(拨到今天的某个时刻,可设倍速)。
|
||||
- **项目资源**(`Properties.Resources`):只用平台素材库(自绘的开关图片、ffmpeg 合成的门铃声,名字和教材一致,`runtime/Sim.WinForms/Resources/gallery`)。实验页"项目资源"对话框勾选素材(图片看缩略图、声音可试听),平台生成 `Properties/Resources.Designer.cs`(图片是 Bitmap、声音是 byte[],用法和 VS 的强类型资源类一样,不在标签页里显示);设计器里图片框的"图片"属性可以从项目资源里选,写进 Designer.cs 的是 `global::命名空间.Properties.Resources.名字`,预览直接显示素材。学生程序的程序目录下有 Resources 文件夹指向素材库(书上用 `BaseDirectory + "//Resources//ring.mp3"`)。没添加资源就用时给中文提示。
|
||||
- **Windows Media Player**(`AxWMPLib.AxWindowsMediaPlayer`):设置 URL 后和真的一样自动播放,`Ctlcontrols.play() / stop()`;声音在浏览器里播放,实训台上加"广播"喇叭,播放时亮起。VS 为它生成的 `ComponentResourceManager(...).GetObject("…OcxState")`:编译时给每个窗体嵌一份空资源,取到 null 不报错。图片、声音数据统一按内容哈希缓存(`blob` 消息),每份只传一次。
|
||||
- **TrackBar**:拖动、点轨道(LargeChange)、方向键(SmallChange),先 ValueChanged 再 Scroll,竖向时最小值在下面;设计器工具箱加"滑动条""媒体播放器"。评分规则加 `value`(滑动条的值)、`media`(播放了某个文件)检查。
|
||||
- 示例:书上任务 1 手动开关灯的判断写反(参考工程是对的),任务 3 漏了填充串口列表,示例里都已改正;任务 3 的"每 5 分钟整点拍照"依赖定时器正好在 0 秒触发,自动评分不检查这一项,由老师评阅。编译检查修正:`System.AppDomain.CurrentDomain.BaseDirectory`(带命名空间)也放行。
|
||||
|
||||
**其他改进**:
|
||||
|
||||
- 2026-10-22:代码编辑器的语言功能:代码补全(输入 `btn_Open.` 列出按钮的属性、方法、事件,只列出平台仿真了的成员)、悬停提示、参数提示(`MessageBox.Show(` 列出重载,标出正在填的参数)、停止输入 1 秒后自动检查编译错误(提示和点"运行"时一样)。语言服务是一个常驻进程,每个浏览器标签页一份工程,只更新有变化的文件。实测(限制 2 核):50 个标签页同时检查 1.2 秒内全部完成,同时补全 0.7 秒;内存约 260 MB。
|
||||
@@ -308,7 +315,7 @@ flowchart TB
|
||||
| T-SQL 转 SQLite | 教材用 SQL Server 写法(`TOP`、`CONVERT`、DataAdapter 回写、日期字符串) | 已实现:ScriptDom 解析后按白名单转写;连接串只按库名打开学生自己的库;日期统一存为标准格式;STRICT 表和 CHECK 约束让类型、长度错误和 SQL Server 一样报出 |
|
||||
| 旧版线程 API | `Thread.Suspend` / `Resume` / `Abort` 在新版 .NET 上会报错 | 已实现:编译时改写成平台的协作式实现,被挂起或终止的线程在 Sleep、改控件、调设备类库时停下 |
|
||||
| 图片处理 | System.Drawing 在 Linux 上不受支持 | 已实现:自己的 `Image` / `Bitmap` 只保存图片文件数据和尺寸,能显示、保存、读回,不能逐像素绘制 |
|
||||
| 时间相关逻辑 | 项目 3 起就有按时刻开关灯的练习,部分定时器间隔长达 2 小时 | 提供可调、可加速的虚拟时钟 |
|
||||
| 时间相关逻辑 | 项目 3 起就有按时刻开关灯的练习,部分定时器间隔长达 2 小时 | 已实现:实训台上的虚拟时钟可拨时间、1×/10×/60× 倍速,编译时把 DateTime.Now / Today / UtcNow 改写成虚拟时钟 |
|
||||
| 事件延迟 | 浏览器和服务器之间来回传递事件,点击是否有卡顿感 | 已实测:单次约 15 ms,100 个程序同时操作中位数约 60 ms |
|
||||
| 并发资源 | 100 个程序同时运行时的内存和 CPU | 已压测:100 个程序合计约 1.1 GB;2 核下 100 人同时启动约 8 秒;CPU 时间上限防死循环,运行超时回收 |
|
||||
| 公网安全 | 恶意或无意的危险代码 | bubblewrap 沙箱 + 编译检查 + 账号登录(已完成) |
|
||||
|
||||
@@ -22,9 +22,10 @@ public static class TrainingBoard
|
||||
if (!string.IsNullOrEmpty(initial) && JsonNode.Parse(initial) is JsonObject json) Update(json);
|
||||
}
|
||||
|
||||
/// <summary>平台内部使用:应用服务器推送的 {"analog":[…],"digital":[…]}</summary>
|
||||
/// <summary>平台内部使用:应用服务器推送的 {"analog":[…],"digital":[…],"clock":…}(clock 不带时表示不变)</summary>
|
||||
public static void Update(JsonObject json)
|
||||
{
|
||||
if (json.ContainsKey("clock")) Sim.Host.SimClock.Update(json["clock"]);
|
||||
lock (Lock)
|
||||
{
|
||||
if (json["analog"] is JsonArray analog)
|
||||
|
||||
@@ -64,7 +64,9 @@ internal static class ApiPolicy
|
||||
if (name.Parent is QualifiedNameSyntax q && q.Left == name) continue;
|
||||
// var 推断出的类型在初始化表达式里已经检查过
|
||||
if (name is IdentifierNameSyntax { IsVar: true }) continue;
|
||||
if (name.Parent is MemberAccessExpressionSyntax m && m.Expression == name && model.GetSymbolInfo(name).Symbol is INamespaceOrTypeSymbol) continue;
|
||||
// 类型名只是用来限定成员(AppDomain.CurrentDomain、System.AppDomain.CurrentDomain):检查后面的成员就够了
|
||||
ExpressionSyntax qualifier = name.Parent is MemberAccessExpressionSyntax inner && inner.Name == name ? inner : name;
|
||||
if (qualifier.Parent is MemberAccessExpressionSyntax m && m.Expression == qualifier && model.GetSymbolInfo(name).Symbol is INamespaceOrTypeSymbol) continue;
|
||||
|
||||
var symbol = model.GetSymbolInfo(name).Symbol;
|
||||
if (symbol == null) continue;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Sim.Runner;
|
||||
|
||||
/// <summary>
|
||||
/// 编译前把学生代码里的 DateTime.Now / Today / UtcNow 改写成虚拟时钟 Sim.Host.SimClock(实训台上可以拨时间、调倍速,
|
||||
/// 教材项目 7 按 6:45 起床、22:00 熄灯等时刻切换情景模式)。只改写语义上确实是 System.DateTime 这三个属性的地方;
|
||||
/// 改写不换行,报错和异常里的行号不变。学生在编辑器里看到的仍是原来的代码。
|
||||
/// </summary>
|
||||
internal static class ClockRewriter
|
||||
{
|
||||
private const string Target = "global::Sim.Host.SimClock";
|
||||
|
||||
public static CSharpCompilation Rewrite(CSharpCompilation compilation)
|
||||
{
|
||||
var result = compilation;
|
||||
foreach (var tree in compilation.SyntaxTrees)
|
||||
{
|
||||
var text = tree.GetText().ToString();
|
||||
if (!text.Contains("Now") && !text.Contains("Today")) continue;
|
||||
var rewriter = new Rewriter(compilation.GetSemanticModel(tree));
|
||||
var root = rewriter.Visit(tree.GetRoot());
|
||||
if (!rewriter.Changed) continue;
|
||||
result = result.ReplaceSyntaxTree(tree,
|
||||
CSharpSyntaxTree.ParseText(SourceText.From(root.ToFullString(), Encoding.UTF8), (CSharpParseOptions)tree.Options, tree.FilePath));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? Replacement(ISymbol? symbol) =>
|
||||
symbol is IPropertySymbol { IsStatic: true, Name: "Now" or "Today" or "UtcNow" } property
|
||||
&& property.ContainingType.SpecialType == SpecialType.System_DateTime
|
||||
? $"{Target}.{property.Name}"
|
||||
: null;
|
||||
|
||||
private sealed class Rewriter(SemanticModel model) : CSharpSyntaxRewriter
|
||||
{
|
||||
public bool Changed { get; private set; }
|
||||
|
||||
public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
|
||||
{
|
||||
// DateTime.Now、System.DateTime.Now
|
||||
if (Replacement(model.GetSymbolInfo(node).Symbol) is { } replacement)
|
||||
{
|
||||
Changed = true;
|
||||
return SyntaxFactory.ParseExpression(replacement).WithTriviaFrom(node);
|
||||
}
|
||||
return base.VisitMemberAccessExpression(node);
|
||||
}
|
||||
|
||||
public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
|
||||
{
|
||||
// using static System.DateTime; 之后直接写 Now
|
||||
if (node.Parent is not MemberAccessExpressionSyntax access || access.Expression == node)
|
||||
{
|
||||
if (Replacement(model.GetSymbolInfo(node).Symbol) is { } replacement)
|
||||
{
|
||||
Changed = true;
|
||||
return SyntaxFactory.ParseExpression(replacement).WithTriviaFrom(node);
|
||||
}
|
||||
}
|
||||
return base.VisitIdentifierName(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ internal static class CompileHints
|
||||
("CS0103", "Catch") or ("CS0246", "Catch") => "catch 要小写,括号里写异常类型,例如 catch (Exception ex)。",
|
||||
("CS0246", var name) when name.StartsWith("bool") || name.StartsWith("private") || name.StartsWith("public") || name.StartsWith("string") || name.StartsWith("int") =>
|
||||
$"“{name}”像是类型和名字之间少了一个空格。",
|
||||
("CS0234", var s) when s.Contains("Properties") => "要用 Properties.Resources,先点代码上方的“项目资源”,添加需要的图片或声音。",
|
||||
("CS0117", var s) when s.Contains("Resources.") => $"项目资源里还没有“{s[(s.LastIndexOf('.') + 1)..]}”:点代码上方的“项目资源”添加(名字要和代码里一致)。",
|
||||
("CS0103", _) => $"找不到“{source}”。检查拼写和大小写(C# 区分大小写),以及控件的 Name 是否和代码里一致。",
|
||||
("CS1002", _) => "C# 每条语句末尾要有英文分号 ;",
|
||||
("CS1056", _) or ("CS1525", _) when source.Length == 1 && source[0] > 127 => "这里出现了中文标点,C# 代码里的符号要用英文半角。",
|
||||
|
||||
@@ -56,9 +56,10 @@ internal static class StudentCompiler
|
||||
|
||||
using var peStream = new MemoryStream();
|
||||
using var pdbStream = new MemoryStream();
|
||||
// 编译改写后的代码(旧版线程 API 换成平台实现),API 检查仍针对学生写的原始代码
|
||||
var emit = LegacyThreadRewriter.Rewrite(compilation).Emit(peStream, pdbStream,
|
||||
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
|
||||
// 编译改写后的代码(旧版线程 API 换成平台实现,DateTime.Now 换成虚拟时钟),API 检查仍针对学生写的原始代码
|
||||
var emit = ClockRewriter.Rewrite(LegacyThreadRewriter.Rewrite(compilation)).Emit(peStream, pdbStream,
|
||||
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb),
|
||||
manifestResources: EmptyControlResources(compilation));
|
||||
var denied = ApiPolicy.Check(compilation).ToList();
|
||||
|
||||
var diagnostics = new JsonArray();
|
||||
@@ -71,6 +72,39 @@ internal static class StudentCompiler
|
||||
return new Result(emit.Success && denied.Count == 0, peStream.ToArray(), pdbStream.ToArray(), diagnostics);
|
||||
}
|
||||
|
||||
private static readonly Lazy<byte[]> EmptyResources = new(() =>
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
using (var writer = new System.Resources.ResourceWriter(stream)) writer.Generate();
|
||||
return stream.ToArray();
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// 给学生工程里的每个窗体、控件类嵌一份空的 .resources:VS 生成的 Designer.cs 会用
|
||||
/// ComponentResourceManager(typeof(窗体)).GetObject("…OcxState") 读窗体的 .resx(如 Windows Media Player 控件),
|
||||
/// 平台没有 .resx,有了空资源时取到 null,而不是报"找不到资源"。
|
||||
/// </summary>
|
||||
private static List<ResourceDescription> EmptyControlResources(CSharpCompilation compilation)
|
||||
{
|
||||
var control = compilation.GetTypeByMetadataName("System.Windows.Forms.Control");
|
||||
var resources = new List<ResourceDescription>();
|
||||
if (control == null) return resources;
|
||||
var names = new HashSet<string>();
|
||||
foreach (var tree in compilation.SyntaxTrees)
|
||||
{
|
||||
var model = compilation.GetSemanticModel(tree);
|
||||
foreach (var declaration in tree.GetRoot().DescendantNodes().OfType<Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax>())
|
||||
{
|
||||
if (model.GetDeclaredSymbol(declaration) is not { } type) continue;
|
||||
var derives = false;
|
||||
for (var t = type.BaseType; t != null && !derives; t = t.BaseType) derives = SymbolEqualityComparer.Default.Equals(t, control);
|
||||
var name = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "") + ".resources";
|
||||
if (derives && names.Add(name)) resources.Add(new ResourceDescription(name, () => new MemoryStream(EmptyResources.Value), isPublic: true));
|
||||
}
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
internal static JsonObject ToJson(Diagnostic diagnostic)
|
||||
{
|
||||
var span = diagnostic.Location.GetMappedLineSpan();
|
||||
|
||||
@@ -104,6 +104,9 @@ internal static class StudentProgramHost
|
||||
baseDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "StudentApp", "bin", "Debug")).FullName;
|
||||
}
|
||||
baseDir = Path.TrimEndingDirectorySeparator(baseDir) + Path.DirectorySeparatorChar;
|
||||
// 程序目录下的 Resources 文件夹指向素材库:教材用 BaseDirectory + "//Resources//ring.mp3" 播放铃声
|
||||
var resources = Path.Combine(baseDir, "Resources");
|
||||
if (!Path.Exists(resources)) Directory.CreateSymbolicLink(resources, ProjectResources.Directory);
|
||||
AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", baseDir);
|
||||
Directory.SetCurrentDirectory(baseDir);
|
||||
SimHost.AppBaseDirectory = baseDir;
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Windows.Forms;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// ActiveX 控件的基类。VS 把 COM 控件拖到窗体上时生成 OcxState = resources.GetObject("…OcxState"),
|
||||
/// 平台编译时给每个窗体嵌了空的资源,这里取到的是 null,不影响使用。
|
||||
/// </summary>
|
||||
public abstract class AxHost : Control, ISupportInitialize
|
||||
{
|
||||
protected AxHost(string clsid) { }
|
||||
|
||||
public State? OcxState { get; set; }
|
||||
|
||||
public void BeginInit() { }
|
||||
public void EndInit() { }
|
||||
|
||||
public class State { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace WMPLib
|
||||
{
|
||||
public enum WMPPlayState
|
||||
{
|
||||
wmppsUndefined = 0, wmppsStopped = 1, wmppsPaused = 2, wmppsPlaying = 3, wmppsScanForward = 4, wmppsScanReverse = 5,
|
||||
wmppsBuffering = 6, wmppsWaiting = 7, wmppsMediaEnded = 8, wmppsTransitioning = 9, wmppsReady = 10, wmppsReconnecting = 11,
|
||||
}
|
||||
|
||||
/// <summary>播放控制:axWindowsMediaPlayer1.Ctlcontrols.play()</summary>
|
||||
public interface IWMPControls
|
||||
{
|
||||
void play();
|
||||
void pause();
|
||||
void stop();
|
||||
double currentPosition { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>播放设置:axWindowsMediaPlayer1.settings.volume</summary>
|
||||
public interface IWMPSettings
|
||||
{
|
||||
bool autoStart { get; set; }
|
||||
int volume { get; set; }
|
||||
bool mute { get; set; }
|
||||
void setMode(string bstrMode, bool varfMode);
|
||||
}
|
||||
}
|
||||
|
||||
namespace AxWMPLib
|
||||
{
|
||||
using WMPLib;
|
||||
|
||||
/// <summary>
|
||||
/// Windows Media Player 控件(教材项目 7 的"广播"播放 ring.mp3)。和真的一样:设置 URL 后自动开始播放(settings.autoStart),
|
||||
/// 也可以调用 Ctlcontrols.play()。声音在浏览器里播放;实训台上的广播喇叭随之亮起。只支持本机文件。
|
||||
/// </summary>
|
||||
[ClientType("AxWindowsMediaPlayer")]
|
||||
public class AxWindowsMediaPlayer : AxHost
|
||||
{
|
||||
private string _url = "";
|
||||
private WMPPlayState _playState = WMPPlayState.wmppsUndefined;
|
||||
|
||||
public AxWindowsMediaPlayer() : base("6bf52a52-394a-11d3-b153-00c04f79faa6")
|
||||
{
|
||||
Ctlcontrols = new Controls(this);
|
||||
settings = new Settings();
|
||||
}
|
||||
|
||||
protected override Size DefaultSize => new(245, 240);
|
||||
|
||||
public string URL
|
||||
{
|
||||
get => _url;
|
||||
set
|
||||
{
|
||||
_url = value ?? "";
|
||||
if (settings.autoStart && _url.Length > 0) Play();
|
||||
}
|
||||
}
|
||||
|
||||
public IWMPControls Ctlcontrols { get; }
|
||||
|
||||
// 属性名和 COM 接口一样是小写
|
||||
public IWMPSettings settings { get; }
|
||||
|
||||
public WMPPlayState playState => _playState;
|
||||
|
||||
public string uiMode { get; set; } = "full";
|
||||
public bool stretchToFit { get; set; }
|
||||
public bool enableContextMenu { get; set; } = true;
|
||||
public string status => _playState == WMPPlayState.wmppsPlaying ? "正在播放" : "已停止";
|
||||
|
||||
public event EventHandler? PlayStateChange;
|
||||
|
||||
private void Play()
|
||||
{
|
||||
LegacyThreads.CheckPoint();
|
||||
// 教材写 BaseDirectory + "//Resources//ring.mp3":路径里多余的斜杠在 Linux 上也能用
|
||||
var path = _url.StartsWith("file:///", StringComparison.OrdinalIgnoreCase) ? new Uri(_url).LocalPath : _url;
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
SimHost.SendConsole($"Windows Media Player:找不到要播放的文件 {_url}\n");
|
||||
SetState(WMPPlayState.wmppsStopped);
|
||||
return;
|
||||
}
|
||||
var data = File.ReadAllBytes(path);
|
||||
var mime = Path.GetExtension(path).ToLowerInvariant() switch
|
||||
{
|
||||
".wav" => "audio/wav",
|
||||
".ogg" => "audio/ogg",
|
||||
".m4a" or ".aac" => "audio/aac",
|
||||
_ => "audio/mpeg",
|
||||
};
|
||||
var key = SimHost.DataKey(Convert.ToHexString(SHA1.HashData(data)).ToLowerInvariant(), data, mime);
|
||||
SimHost.Send(new JsonObject { ["type"] = "media", ["id"] = SimId, ["action"] = "play", ["key"] = key, ["name"] = Path.GetFileName(path) });
|
||||
SetState(WMPPlayState.wmppsPlaying);
|
||||
}
|
||||
|
||||
private void Stop()
|
||||
{
|
||||
if (_playState != WMPPlayState.wmppsPlaying) return;
|
||||
SimHost.Send(new JsonObject { ["type"] = "media", ["id"] = SimId, ["action"] = "stop" });
|
||||
SetState(WMPPlayState.wmppsStopped);
|
||||
}
|
||||
|
||||
private void SetState(WMPPlayState state)
|
||||
{
|
||||
if (_playState == state) return;
|
||||
_playState = state;
|
||||
PlayStateChange?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["URL"] = _url;
|
||||
}
|
||||
|
||||
private sealed class Controls(AxWindowsMediaPlayer owner) : IWMPControls
|
||||
{
|
||||
public void play()
|
||||
{
|
||||
if (owner._playState != WMPPlayState.wmppsPlaying && owner._url.Length > 0) owner.Play();
|
||||
}
|
||||
|
||||
public void pause() => owner.Stop();
|
||||
public void stop() => owner.Stop();
|
||||
public double currentPosition { get; set; }
|
||||
}
|
||||
|
||||
private sealed class Settings : IWMPSettings
|
||||
{
|
||||
public bool autoStart { get; set; } = true;
|
||||
public int volume { get; set; } = 50;
|
||||
public bool mute { get; set; }
|
||||
public void setMode(string bstrMode, bool varfMode) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public class DateTimePicker : Control
|
||||
public static readonly DateTime MinimumDateTime = new(1753, 1, 1);
|
||||
public static readonly DateTime MaximumDateTime = new(9998, 12, 31);
|
||||
|
||||
private DateTime _value = DateTime.Now;
|
||||
private DateTime _value = SimClock.Now;
|
||||
private DateTimePickerFormat _format = DateTimePickerFormat.Long;
|
||||
private string? _customFormat;
|
||||
private bool _showUpDown;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Nodes;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public enum TickStyle { None, TopLeft, BottomRight, Both }
|
||||
|
||||
public enum Orientation { Horizontal, Vertical }
|
||||
|
||||
/// <summary>滑动条(教材项目 7 模拟空调温度、窗帘开合)</summary>
|
||||
public class TrackBar : Control, ISupportInitialize
|
||||
{
|
||||
private int _minimum;
|
||||
private int _maximum = 10;
|
||||
private int _value;
|
||||
private int _tickFrequency = 1;
|
||||
private TickStyle _tickStyle = TickStyle.BottomRight;
|
||||
private Orientation _orientation = Orientation.Horizontal;
|
||||
private bool _initializing;
|
||||
|
||||
public TrackBar() => AutoSize = true;
|
||||
|
||||
protected override Size DefaultSize => new(104, 45);
|
||||
|
||||
public int Minimum
|
||||
{
|
||||
get => _minimum;
|
||||
set => SetRange(value, Math.Max(value, _maximum));
|
||||
}
|
||||
|
||||
public int Maximum
|
||||
{
|
||||
get => _maximum;
|
||||
set => SetRange(Math.Min(value, _minimum), value);
|
||||
}
|
||||
|
||||
public int Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (!_initializing && (value < _minimum || value > _maximum))
|
||||
throw new ArgumentOutOfRangeException(nameof(value), $"“{value}”的值对于“Value”无效。“Value”应介于 'Minimum' 和 'Maximum' 之间。");
|
||||
if (SetProp(ref _value, value, nameof(Value))) OnValueChanged(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public int TickFrequency
|
||||
{
|
||||
get => _tickFrequency;
|
||||
set => SetProp(ref _tickFrequency, value, nameof(TickFrequency));
|
||||
}
|
||||
|
||||
public TickStyle TickStyle
|
||||
{
|
||||
get => _tickStyle;
|
||||
set => SetProp(ref _tickStyle, value, nameof(TickStyle));
|
||||
}
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => _orientation;
|
||||
set
|
||||
{
|
||||
if (value == _orientation) return;
|
||||
SetProp(ref _orientation, value, nameof(Orientation));
|
||||
Size = new Size(Height, Width);
|
||||
}
|
||||
}
|
||||
|
||||
public int LargeChange { get; set; } = 5;
|
||||
public int SmallChange { get; set; } = 1;
|
||||
|
||||
public event EventHandler? Scroll;
|
||||
public event EventHandler? ValueChanged;
|
||||
|
||||
protected virtual void OnScroll(EventArgs e) => Scroll?.Invoke(this, e);
|
||||
protected virtual void OnValueChanged(EventArgs e) => ValueChanged?.Invoke(this, e);
|
||||
|
||||
public void SetRange(int minValue, int maxValue)
|
||||
{
|
||||
SetProp(ref _minimum, minValue, nameof(Minimum));
|
||||
SetProp(ref _maximum, Math.Max(minValue, maxValue), nameof(Maximum));
|
||||
if (_value < _minimum) Value = _minimum;
|
||||
else if (_value > _maximum) Value = _maximum;
|
||||
}
|
||||
|
||||
// Designer.cs 里 VS 生成 ((ISupportInitialize)trackBar1).BeginInit() / EndInit():期间先设 Value 再设范围也不报错
|
||||
public void BeginInit() => _initializing = true;
|
||||
|
||||
public void EndInit()
|
||||
{
|
||||
_initializing = false;
|
||||
if (_value < _minimum || _value > _maximum) Value = Math.Clamp(_value, _minimum, _maximum);
|
||||
}
|
||||
|
||||
/// <summary>用户拖动滑块、点轨道或按方向键:和 WinForms 一样先改 Value(ValueChanged),再触发 Scroll</summary>
|
||||
protected override void OnUserAction(UserAction action)
|
||||
{
|
||||
if (action.Action != "scroll" || !int.TryParse(action.Value, out var value))
|
||||
{
|
||||
base.OnUserAction(action);
|
||||
return;
|
||||
}
|
||||
Value = Math.Clamp(value, _minimum, _maximum);
|
||||
OnScroll(EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["Minimum"] = _minimum;
|
||||
props["Maximum"] = _maximum;
|
||||
props["Value"] = _value;
|
||||
props["TickFrequency"] = _tickFrequency;
|
||||
props["TickStyle"] = _tickStyle.ToString();
|
||||
props["Orientation"] = _orientation.ToString();
|
||||
props["LargeChange"] = LargeChange;
|
||||
props["SmallChange"] = SmallChange;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
|
||||
namespace Sim.Host;
|
||||
|
||||
/// <summary>
|
||||
/// 项目资源(Properties.Resources)的素材库。平台为学生工程生成的 Properties/Resources.Designer.cs 通过这里读取素材,
|
||||
/// 和 VS 一样每次取图片都得到一个新的 Bitmap。
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class ProjectResources
|
||||
{
|
||||
/// <summary>素材文件所在目录(运行器目录下的 Resources/gallery)</summary>
|
||||
public static string Directory { get; } = Path.Combine(Path.GetDirectoryName(typeof(ProjectResources).Assembly.Location)!, "Resources", "gallery");
|
||||
|
||||
public static Bitmap Image(string file) => (Bitmap)System.Drawing.Image.FromStream(new MemoryStream(Bytes(file)));
|
||||
|
||||
public static byte[] Bytes(string file) => File.ReadAllBytes(Path.Combine(Directory, Path.GetFileName(file)));
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Sim.Host;
|
||||
|
||||
/// <summary>
|
||||
/// 虚拟时钟(实训台上可以拨时间、调倍速;教材项目 7 按时刻切换情景模式)。
|
||||
/// 编译时把学生代码里的 DateTime.Now / Today / UtcNow 改写成这里的属性(见 Sim.Runner/ClockRewriter.cs)。
|
||||
/// 没有拨过时就是真实时间。
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class SimClock
|
||||
{
|
||||
private sealed record Setting(DateTime Wall, long RealAt, double Speed);
|
||||
|
||||
private static volatile Setting? _setting;
|
||||
|
||||
public static DateTime Now
|
||||
{
|
||||
get
|
||||
{
|
||||
var setting = _setting;
|
||||
if (setting == null) return DateTime.Now;
|
||||
var elapsed = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - setting.RealAt;
|
||||
return setting.Wall.AddMilliseconds(elapsed * setting.Speed);
|
||||
}
|
||||
}
|
||||
|
||||
public static DateTime Today => Now.Date;
|
||||
|
||||
public static DateTime UtcNow => Now.ToUniversalTime();
|
||||
|
||||
/// <summary>平台内部使用:应用服务器推送的 {"wall":"2024-01-16T06:40:00","realAt":…,"speed":60},null 恢复真实时间</summary>
|
||||
public static void Update(JsonNode? clock)
|
||||
{
|
||||
if (clock is not JsonObject json
|
||||
|| !DateTime.TryParseExact((string?)json["wall"], "yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var wall))
|
||||
{
|
||||
_setting = null;
|
||||
return;
|
||||
}
|
||||
_setting = new Setting(DateTime.SpecifyKind(wall, DateTimeKind.Local), (long?)json["realAt"] ?? 0, (double?)json["speed"] ?? 1);
|
||||
}
|
||||
}
|
||||
@@ -86,19 +86,23 @@ public static class SimHost
|
||||
public static string AppBaseDirectory { get; set; } = AppContext.BaseDirectory;
|
||||
|
||||
/// <summary>图片在浏览器端的编号;第一次用到时先把图片数据发过去,浏览器缓存后同一张图不再重发</summary>
|
||||
public static string ImageKey(System.Drawing.Image image)
|
||||
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)
|
||||
{
|
||||
var key = image.SimKey;
|
||||
bool isNew;
|
||||
lock (SentImages) isNew = SentImages.Add(key);
|
||||
if (isNew)
|
||||
{
|
||||
Send(new JsonObject
|
||||
{
|
||||
["type"] = "image",
|
||||
["type"] = "blob",
|
||||
["key"] = key,
|
||||
["mime"] = image.MimeType,
|
||||
["data"] = Convert.ToBase64String(image.Data),
|
||||
["mime"] = mime,
|
||||
["data"] = Convert.ToBase64String(data),
|
||||
});
|
||||
}
|
||||
return key;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 项目资源素材库
|
||||
|
||||
学生在"项目资源"里添加的图片和声音(`Properties.Resources.xxx`),名字和教材一致,全部由平台自绘或合成(不使用教材配套资料里的素材)。
|
||||
|
||||
- `gallery/`:发布给运行器和网页的文件,`index.json` 是清单(名字、文件、中文说明、类型)。
|
||||
- `*.svg`:图片的源文件,改了以后用 `rsvg-convert xxx.svg -o gallery/xxx.png` 重新生成。
|
||||
- `gallery/ring.mp3`:用 ffmpeg 合成的门铃声(叮—咚),命令见提交记录。
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "name": "switch_on", "file": "switch_on.png", "label": "开关:开", "kind": "image" },
|
||||
{ "name": "switch_off", "file": "switch_off.png", "label": "开关:关", "kind": "image" },
|
||||
{ "name": "ring", "file": "ring.mp3", "label": "铃声", "kind": "audio" }
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="24" viewBox="0 0 48 24">
|
||||
<rect x="1" y="2" width="46" height="20" rx="10" fill="#c7cbd1" stroke="#a9aeb6" stroke-width="1"/>
|
||||
<text x="33" y="16.5" font-family="Arial, Helvetica, sans-serif" font-size="10" font-weight="bold" fill="#ffffff" text-anchor="middle">OFF</text>
|
||||
<circle cx="12" cy="12" r="8" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 390 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="24" viewBox="0 0 48 24">
|
||||
<rect x="1" y="2" width="46" height="20" rx="10" fill="#34c759" stroke="#24a148" stroke-width="1"/>
|
||||
<text x="15" y="16.5" font-family="Arial, Helvetica, sans-serif" font-size="10" font-weight="bold" fill="#ffffff" text-anchor="middle">ON</text>
|
||||
<circle cx="36" cy="12" r="8" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 389 B |
@@ -7,4 +7,8 @@
|
||||
<!-- 仿真类型的公开 API 要和真实 WinForms 一致,很多成员没有 XML 注释 -->
|
||||
<NoWarn>$(NoWarn);CS1591;CS8618</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- 项目资源素材库(Properties.Resources 的图片、声音),随运行器一起输出 -->
|
||||
<None Include="Resources/gallery/*" CopyToOutputDirectory="PreserveNewest" LinkBase="Resources/gallery" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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 frm_control());
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 项目资源(平台素材库)。在"项目资源"里添加或移除,不要手动修改这个文件。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace 教室情景模式.Properties {
|
||||
|
||||
/// <summary>
|
||||
/// 项目资源:Properties.Resources.资源名
|
||||
/// </summary>
|
||||
internal class Resources {
|
||||
|
||||
/// <summary>开关:开</summary>
|
||||
internal static System.Drawing.Bitmap switch_on {
|
||||
get { return global::Sim.Host.ProjectResources.Image("switch_on.png"); }
|
||||
}
|
||||
|
||||
/// <summary>开关:关</summary>
|
||||
internal static System.Drawing.Bitmap switch_off {
|
||||
get { return global::Sim.Host.ProjectResources.Image("switch_off.png"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# 评分规则:书上评价表的功能项。每项单独启动一次程序,按步骤操作,核对结果。
|
||||
# 格式说明见 server/src/grading/checks.ts
|
||||
- title: 能实时显示环境数据
|
||||
steps:
|
||||
- select: { control: com_import, item: COM2 }
|
||||
- click: btn_apply
|
||||
- expect: { control: btn_apply, text: 关闭, desc: 按钮文字变成"关闭" }
|
||||
- board: { light: 600, temperature: 22, humidity: 48 }
|
||||
- expect: { control: txt_light, text: '600', within: 5s }
|
||||
- expect: { control: txt_temperature, text: '22', within: 5s }
|
||||
- expect: { control: txt_humidity, text: '48', within: 5s }
|
||||
|
||||
- title: 能根据环境数据的变化实时控制智能设备
|
||||
steps:
|
||||
- select: { control: com_import, item: COM2 }
|
||||
- click: btn_apply
|
||||
- board: { light: 300, temperature: 22 }
|
||||
- expect: { relay: 1, on: true, within: 5s, desc: 光照低于 400 时打开照明灯 }
|
||||
- expect: { control: lamp, on: true, desc: 窗体上的灯亮 }
|
||||
- board: { light: 900 }
|
||||
- expect: { relay: 1, on: false, within: 5s, desc: 光照高于 800 时关闭照明灯 }
|
||||
- board: { temperature: 33 }
|
||||
- expect: { control: trb_airconditione, value: 26, within: 5s, desc: 温度高于 30℃ 时空调设为 26℃ }
|
||||
- board: { temperature: 3 }
|
||||
- expect: { control: trb_airconditione, value: 25, within: 5s, desc: 温度低于 5℃ 时空调设为 25℃ }
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "项目7 任务1 实现智慧教室",
|
||||
"source": "教材书 p133–137,界面和代码取自配套参考工程(手动开关灯的判断书上写反了,参考工程是对的);开关图片来自平台素材库"
|
||||
}
|
||||
Generated
+350
@@ -0,0 +1,350 @@
|
||||
namespace 教室情景模式
|
||||
{
|
||||
partial class frm_control
|
||||
{
|
||||
/// <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.components = new System.ComponentModel.Container();
|
||||
this.lbl_mode = new System.Windows.Forms.Label();
|
||||
this.btn_apply = new System.Windows.Forms.Button();
|
||||
this.grp_mode = new System.Windows.Forms.GroupBox();
|
||||
this.grp_input = new System.Windows.Forms.GroupBox();
|
||||
this.txt_temperature = new System.Windows.Forms.TextBox();
|
||||
this.txt_humidity = new System.Windows.Forms.TextBox();
|
||||
this.com_import = new System.Windows.Forms.ComboBox();
|
||||
this.lbl_temperature = new System.Windows.Forms.Label();
|
||||
this.txt_light = new System.Windows.Forms.TextBox();
|
||||
this.lbl_import = new System.Windows.Forms.Label();
|
||||
this.lbl_humidity = new System.Windows.Forms.Label();
|
||||
this.lbl_light = new System.Windows.Forms.Label();
|
||||
this.grp_status = new System.Windows.Forms.GroupBox();
|
||||
this.pic_airconditioneStatus = new System.Windows.Forms.PictureBox();
|
||||
this.lbl_airconditionmin = new System.Windows.Forms.Label();
|
||||
this.lbl_airconditionmax = new System.Windows.Forms.Label();
|
||||
this.trb_airconditione = new System.Windows.Forms.TrackBar();
|
||||
this.lamp = new WinFormsDeviceControlLibrary.Lamp();
|
||||
this.lbl_airconditione = new System.Windows.Forms.Label();
|
||||
this.pic_lightsStatus = new System.Windows.Forms.PictureBox();
|
||||
this.lbl_lights = new System.Windows.Forms.Label();
|
||||
this.timer = new System.Windows.Forms.Timer(this.components);
|
||||
this.grp_mode.SuspendLayout();
|
||||
this.grp_input.SuspendLayout();
|
||||
this.grp_status.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_airconditioneStatus)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trb_airconditione)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_lightsStatus)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// lbl_mode
|
||||
//
|
||||
this.lbl_mode.AutoSize = true;
|
||||
this.lbl_mode.Location = new System.Drawing.Point(109, 30);
|
||||
this.lbl_mode.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_mode.Name = "lbl_mode";
|
||||
this.lbl_mode.Size = new System.Drawing.Size(89, 12);
|
||||
this.lbl_mode.TabIndex = 1;
|
||||
this.lbl_mode.Text = "智慧教室将开启";
|
||||
//
|
||||
// btn_apply
|
||||
//
|
||||
this.btn_apply.Location = new System.Drawing.Point(285, 22);
|
||||
this.btn_apply.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.btn_apply.Name = "btn_apply";
|
||||
this.btn_apply.Size = new System.Drawing.Size(55, 28);
|
||||
this.btn_apply.TabIndex = 2;
|
||||
this.btn_apply.Text = "启用";
|
||||
this.btn_apply.UseVisualStyleBackColor = true;
|
||||
this.btn_apply.Click += new System.EventHandler(this.btn_apply_Click);
|
||||
//
|
||||
// grp_mode
|
||||
//
|
||||
this.grp_mode.Controls.Add(this.lbl_mode);
|
||||
this.grp_mode.Controls.Add(this.btn_apply);
|
||||
this.grp_mode.Location = new System.Drawing.Point(8, 8);
|
||||
this.grp_mode.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.grp_mode.Name = "grp_mode";
|
||||
this.grp_mode.Padding = new System.Windows.Forms.Padding(2);
|
||||
this.grp_mode.Size = new System.Drawing.Size(396, 59);
|
||||
this.grp_mode.TabIndex = 3;
|
||||
this.grp_mode.TabStop = false;
|
||||
this.grp_mode.Text = "模式选择";
|
||||
//
|
||||
// grp_input
|
||||
//
|
||||
this.grp_input.Controls.Add(this.txt_temperature);
|
||||
this.grp_input.Controls.Add(this.txt_humidity);
|
||||
this.grp_input.Controls.Add(this.com_import);
|
||||
this.grp_input.Controls.Add(this.lbl_temperature);
|
||||
this.grp_input.Controls.Add(this.txt_light);
|
||||
this.grp_input.Controls.Add(this.lbl_import);
|
||||
this.grp_input.Controls.Add(this.lbl_humidity);
|
||||
this.grp_input.Controls.Add(this.lbl_light);
|
||||
this.grp_input.Location = new System.Drawing.Point(8, 71);
|
||||
this.grp_input.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.grp_input.Name = "grp_input";
|
||||
this.grp_input.Padding = new System.Windows.Forms.Padding(2);
|
||||
this.grp_input.Size = new System.Drawing.Size(133, 236);
|
||||
this.grp_input.TabIndex = 4;
|
||||
this.grp_input.TabStop = false;
|
||||
this.grp_input.Text = "采集量";
|
||||
//
|
||||
// txt_temperature
|
||||
//
|
||||
this.txt_temperature.Location = new System.Drawing.Point(56, 113);
|
||||
this.txt_temperature.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.txt_temperature.Name = "txt_temperature";
|
||||
this.txt_temperature.Size = new System.Drawing.Size(65, 21);
|
||||
this.txt_temperature.TabIndex = 3;
|
||||
//
|
||||
// txt_humidity
|
||||
//
|
||||
this.txt_humidity.Location = new System.Drawing.Point(57, 160);
|
||||
this.txt_humidity.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.txt_humidity.Name = "txt_humidity";
|
||||
this.txt_humidity.Size = new System.Drawing.Size(65, 21);
|
||||
this.txt_humidity.TabIndex = 3;
|
||||
//
|
||||
// com_import
|
||||
//
|
||||
this.com_import.FormattingEnabled = true;
|
||||
this.com_import.Location = new System.Drawing.Point(57, 20);
|
||||
this.com_import.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.com_import.Name = "com_import";
|
||||
this.com_import.Size = new System.Drawing.Size(65, 20);
|
||||
this.com_import.TabIndex = 6;
|
||||
//
|
||||
// lbl_temperature
|
||||
//
|
||||
this.lbl_temperature.AutoSize = true;
|
||||
this.lbl_temperature.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_temperature.Location = new System.Drawing.Point(22, 117);
|
||||
this.lbl_temperature.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_temperature.Name = "lbl_temperature";
|
||||
this.lbl_temperature.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_temperature.TabIndex = 1;
|
||||
this.lbl_temperature.Text = "温度";
|
||||
//
|
||||
// txt_light
|
||||
//
|
||||
this.txt_light.Location = new System.Drawing.Point(57, 67);
|
||||
this.txt_light.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.txt_light.Name = "txt_light";
|
||||
this.txt_light.Size = new System.Drawing.Size(65, 21);
|
||||
this.txt_light.TabIndex = 2;
|
||||
//
|
||||
// lbl_import
|
||||
//
|
||||
this.lbl_import.AutoSize = true;
|
||||
this.lbl_import.Location = new System.Drawing.Point(23, 23);
|
||||
this.lbl_import.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_import.Name = "lbl_import";
|
||||
this.lbl_import.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_import.TabIndex = 1;
|
||||
this.lbl_import.Text = "串口";
|
||||
//
|
||||
// lbl_humidity
|
||||
//
|
||||
this.lbl_humidity.AutoSize = true;
|
||||
this.lbl_humidity.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_humidity.Location = new System.Drawing.Point(23, 163);
|
||||
this.lbl_humidity.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_humidity.Name = "lbl_humidity";
|
||||
this.lbl_humidity.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_humidity.TabIndex = 1;
|
||||
this.lbl_humidity.Text = "湿度";
|
||||
//
|
||||
// lbl_light
|
||||
//
|
||||
this.lbl_light.AutoSize = true;
|
||||
this.lbl_light.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_light.Location = new System.Drawing.Point(11, 70);
|
||||
this.lbl_light.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_light.Name = "lbl_light";
|
||||
this.lbl_light.Size = new System.Drawing.Size(44, 12);
|
||||
this.lbl_light.TabIndex = 0;
|
||||
this.lbl_light.Text = "光照度";
|
||||
//
|
||||
// grp_status
|
||||
//
|
||||
this.grp_status.Controls.Add(this.pic_airconditioneStatus);
|
||||
this.grp_status.Controls.Add(this.lbl_airconditionmin);
|
||||
this.grp_status.Controls.Add(this.lbl_airconditionmax);
|
||||
this.grp_status.Controls.Add(this.trb_airconditione);
|
||||
this.grp_status.Controls.Add(this.lamp);
|
||||
this.grp_status.Controls.Add(this.lbl_airconditione);
|
||||
this.grp_status.Controls.Add(this.pic_lightsStatus);
|
||||
this.grp_status.Controls.Add(this.lbl_lights);
|
||||
this.grp_status.Location = new System.Drawing.Point(145, 71);
|
||||
this.grp_status.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.grp_status.Name = "grp_status";
|
||||
this.grp_status.Padding = new System.Windows.Forms.Padding(2);
|
||||
this.grp_status.Size = new System.Drawing.Size(259, 236);
|
||||
this.grp_status.TabIndex = 5;
|
||||
this.grp_status.TabStop = false;
|
||||
this.grp_status.Text = "状态栏";
|
||||
//
|
||||
// pic_airconditioneStatus
|
||||
//
|
||||
this.pic_airconditioneStatus.Image = global::教室情景模式.Properties.Resources.switch_off;
|
||||
this.pic_airconditioneStatus.Location = new System.Drawing.Point(58, 27);
|
||||
this.pic_airconditioneStatus.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.pic_airconditioneStatus.Name = "pic_airconditioneStatus";
|
||||
this.pic_airconditioneStatus.Size = new System.Drawing.Size(33, 17);
|
||||
this.pic_airconditioneStatus.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
|
||||
this.pic_airconditioneStatus.TabIndex = 6;
|
||||
this.pic_airconditioneStatus.TabStop = false;
|
||||
//
|
||||
// lbl_airconditionmin
|
||||
//
|
||||
this.lbl_airconditionmin.AutoSize = true;
|
||||
this.lbl_airconditionmin.Location = new System.Drawing.Point(10, 87);
|
||||
this.lbl_airconditionmin.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_airconditionmin.Name = "lbl_airconditionmin";
|
||||
this.lbl_airconditionmin.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_airconditionmin.TabIndex = 5;
|
||||
this.lbl_airconditionmin.Text = "18℃";
|
||||
//
|
||||
// lbl_airconditionmax
|
||||
//
|
||||
this.lbl_airconditionmax.AutoSize = true;
|
||||
this.lbl_airconditionmax.Location = new System.Drawing.Point(95, 87);
|
||||
this.lbl_airconditionmax.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_airconditionmax.Name = "lbl_airconditionmax";
|
||||
this.lbl_airconditionmax.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_airconditionmax.TabIndex = 5;
|
||||
this.lbl_airconditionmax.Text = "32℃";
|
||||
//
|
||||
// trb_airconditione
|
||||
//
|
||||
this.trb_airconditione.Location = new System.Drawing.Point(12, 53);
|
||||
this.trb_airconditione.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.trb_airconditione.Maximum = 32;
|
||||
this.trb_airconditione.Minimum = 18;
|
||||
this.trb_airconditione.Name = "trb_airconditione";
|
||||
this.trb_airconditione.Size = new System.Drawing.Size(107, 45);
|
||||
this.trb_airconditione.TabIndex = 4;
|
||||
this.trb_airconditione.Value = 18;
|
||||
//
|
||||
// lamp
|
||||
//
|
||||
this.lamp.BackColor = System.Drawing.Color.Transparent;
|
||||
this.lamp.Location = new System.Drawing.Point(139, 40);
|
||||
this.lamp.Name = "lamp";
|
||||
this.lamp.Size = new System.Drawing.Size(105, 180);
|
||||
this.lamp.TabIndex = 3;
|
||||
//
|
||||
// lbl_airconditione
|
||||
//
|
||||
this.lbl_airconditione.AutoSize = true;
|
||||
this.lbl_airconditione.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_airconditione.Location = new System.Drawing.Point(10, 27);
|
||||
this.lbl_airconditione.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_airconditione.Name = "lbl_airconditione";
|
||||
this.lbl_airconditione.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_airconditione.TabIndex = 2;
|
||||
this.lbl_airconditione.Text = "空调";
|
||||
//
|
||||
// pic_lightsStatus
|
||||
//
|
||||
this.pic_lightsStatus.Image = global::教室情景模式.Properties.Resources.switch_off;
|
||||
this.pic_lightsStatus.Location = new System.Drawing.Point(204, 19);
|
||||
this.pic_lightsStatus.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.pic_lightsStatus.Name = "pic_lightsStatus";
|
||||
this.pic_lightsStatus.Size = new System.Drawing.Size(33, 17);
|
||||
this.pic_lightsStatus.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
|
||||
this.pic_lightsStatus.TabIndex = 1;
|
||||
this.pic_lightsStatus.TabStop = false;
|
||||
this.pic_lightsStatus.Click += new System.EventHandler(this.pic_lightsStatus_Click);
|
||||
//
|
||||
// lbl_lights
|
||||
//
|
||||
this.lbl_lights.AutoSize = true;
|
||||
this.lbl_lights.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_lights.Location = new System.Drawing.Point(161, 19);
|
||||
this.lbl_lights.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_lights.Name = "lbl_lights";
|
||||
this.lbl_lights.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_lights.TabIndex = 0;
|
||||
this.lbl_lights.Text = "灯光";
|
||||
//
|
||||
// timer
|
||||
//
|
||||
this.timer.Interval = 2000;
|
||||
this.timer.Tick += new System.EventHandler(this.timer_Tick);
|
||||
//
|
||||
// frm_control
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(417, 315);
|
||||
this.Controls.Add(this.grp_status);
|
||||
this.Controls.Add(this.grp_input);
|
||||
this.Controls.Add(this.grp_mode);
|
||||
this.Margin = new System.Windows.Forms.Padding(2);
|
||||
this.Name = "frm_control";
|
||||
this.Text = "智慧教室情景控制";
|
||||
this.Load += new System.EventHandler(this.frm_control_Load);
|
||||
this.grp_mode.ResumeLayout(false);
|
||||
this.grp_mode.PerformLayout();
|
||||
this.grp_input.ResumeLayout(false);
|
||||
this.grp_input.PerformLayout();
|
||||
this.grp_status.ResumeLayout(false);
|
||||
this.grp_status.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_airconditioneStatus)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trb_airconditione)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_lightsStatus)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label lbl_mode;
|
||||
private System.Windows.Forms.Button btn_apply;
|
||||
private System.Windows.Forms.GroupBox grp_mode;
|
||||
private System.Windows.Forms.GroupBox grp_input;
|
||||
private System.Windows.Forms.TextBox txt_humidity;
|
||||
private System.Windows.Forms.TextBox txt_light;
|
||||
private System.Windows.Forms.Label lbl_humidity;
|
||||
private System.Windows.Forms.Label lbl_light;
|
||||
private System.Windows.Forms.GroupBox grp_status;
|
||||
private System.Windows.Forms.Label lbl_lights;
|
||||
private System.Windows.Forms.TextBox txt_temperature;
|
||||
private System.Windows.Forms.Label lbl_temperature;
|
||||
private System.Windows.Forms.ComboBox com_import;
|
||||
private System.Windows.Forms.Label lbl_import;
|
||||
private System.Windows.Forms.Timer timer;
|
||||
private System.Windows.Forms.PictureBox pic_lightsStatus;
|
||||
private WinFormsDeviceControlLibrary.Lamp lamp;
|
||||
private System.Windows.Forms.Label lbl_airconditionmin;
|
||||
private System.Windows.Forms.Label lbl_airconditionmax;
|
||||
private System.Windows.Forms.TrackBar trb_airconditione;
|
||||
private System.Windows.Forms.Label lbl_airconditione;
|
||||
private System.Windows.Forms.PictureBox pic_airconditioneStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
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;//引用系统IO端口库
|
||||
using DeviceCtrlLibrary;//引用自定义设备控制库
|
||||
using DeviceCtrlLibrary.Model;//引用自定义设备控制模型库
|
||||
|
||||
namespace 教室情景模式
|
||||
{
|
||||
public partial class frm_control : Form
|
||||
{
|
||||
bool IsOpen = false;//定义布尔变量记录串口打开情况,默认为false
|
||||
bool IsLampOpen = false;//定义布尔变量记录电灯打开情况,默认为false
|
||||
bool IsairconditioneOpen = false;//定义布尔变量记录空调打开情况,默认为false
|
||||
ADAM4017ResModel ResModel;//创建4017接收数据模型
|
||||
|
||||
public frm_control()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void frm_control_Load(object sender, EventArgs e)
|
||||
{
|
||||
//将系统串口名依次加入下拉列表框中
|
||||
foreach (string item in SerialPort.GetPortNames())
|
||||
{
|
||||
com_import.Items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void btn_apply_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!IsOpen)//判断串口是否打开
|
||||
{
|
||||
DeviceCtrlHelper.adam4017Helper.Open(com_import.Text);//打开串口
|
||||
DeviceCtrlHelper.adam4150Helper.Open(com_import.Text);//打开串口
|
||||
btn_apply.Text = "关闭";//按钮btn_apply显示“关闭”
|
||||
lbl_mode.Text = "智慧教室已开启";//标签lbl_mode显示“智慧教室已开启”
|
||||
IsOpen = true;
|
||||
timer.Start();//通过timer_Tick实时把采集到的数据显示出来,并判断是否需要开灯、关窗帘
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
DeviceCtrlHelper.adam4017Helper.Close();//关闭采集
|
||||
DeviceCtrlHelper.adam4150Helper.Close();//关闭4150
|
||||
timer.Stop();//停止数据采集与自动控制
|
||||
btn_apply.Text = "启用";
|
||||
lbl_mode.Text = "智慧教室将开启";
|
||||
IsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void timer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
ResModel = DeviceCtrlHelper.adam4017Helper.SetData();//采集数据
|
||||
//显示采集到的数据
|
||||
txt_light.Text = ResModel.lightValue;
|
||||
txt_temperature.Text = ResModel.temperatureValue;
|
||||
txt_humidity.Text = ResModel.humidityValue;
|
||||
|
||||
if (Convert.ToInt16(ResModel.lightValue) < 400)//光照度过低则打开灯
|
||||
{
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_on;
|
||||
IsLampOpen = true;
|
||||
lamp.OnOff(true);//打开界面灯
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1,true);//打开1通道灯
|
||||
}
|
||||
else if (Convert.ToInt16(ResModel.lightValue) > 800 )
|
||||
{
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_off;
|
||||
IsLampOpen = false;
|
||||
lamp.OnOff(false);//关闭界面灯
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, false);//关闭1通道灯
|
||||
}
|
||||
|
||||
if (Convert.ToInt16(ResModel.temperatureValue) > 30 )
|
||||
{
|
||||
pic_airconditioneStatus.Image = Properties.Resources.switch_on;
|
||||
IsairconditioneOpen = true;
|
||||
trb_airconditione.Value = 26;
|
||||
|
||||
}
|
||||
else if (Convert.ToInt16(ResModel.temperatureValue) < 5)
|
||||
{
|
||||
pic_airconditioneStatus.Image = Properties.Resources.switch_on;
|
||||
IsairconditioneOpen = true;
|
||||
trb_airconditione.Value = 25;
|
||||
}
|
||||
else
|
||||
{
|
||||
pic_airconditioneStatus.Image = Properties.Resources.switch_off;
|
||||
IsairconditioneOpen = false;
|
||||
trb_airconditione.Value = 18;
|
||||
}
|
||||
}
|
||||
|
||||
private void pic_lightsStatus_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (IsLampOpen)
|
||||
{
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_off;
|
||||
IsLampOpen = false;
|
||||
lamp.OnOff(false);
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, false);//关闭1通道灯
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_on;
|
||||
IsLampOpen = true;
|
||||
lamp.OnOff(true);
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, true);//打开1通道灯
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Generated
+388
@@ -0,0 +1,388 @@
|
||||
namespace 寝室情景模式
|
||||
{
|
||||
partial class frm_control
|
||||
{
|
||||
/// <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.components = new System.ComponentModel.Container();
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frm_control));
|
||||
this.grp_status = new System.Windows.Forms.GroupBox();
|
||||
this.alarm1 = new WinFormsDeviceControlLibrary.Alarm();
|
||||
this.lamp = new WinFormsDeviceControlLibrary.Lamp();
|
||||
this.lbl_windowopen = new System.Windows.Forms.Label();
|
||||
this.lbl_windowclose = new System.Windows.Forms.Label();
|
||||
this.trb_curtain = new System.Windows.Forms.TrackBar();
|
||||
this.lbl_boardcast = new System.Windows.Forms.Label();
|
||||
this.lbl_curtain = new System.Windows.Forms.Label();
|
||||
this.pic_boardcastStatus = new System.Windows.Forms.PictureBox();
|
||||
this.lbl_lights = new System.Windows.Forms.Label();
|
||||
this.grp_input = new System.Windows.Forms.GroupBox();
|
||||
this.txt_temperature = new System.Windows.Forms.TextBox();
|
||||
this.txt_humidity = new System.Windows.Forms.TextBox();
|
||||
this.com_import = new System.Windows.Forms.ComboBox();
|
||||
this.lbl_temperature = new System.Windows.Forms.Label();
|
||||
this.txt_light = new System.Windows.Forms.TextBox();
|
||||
this.lbl_import = new System.Windows.Forms.Label();
|
||||
this.lbl_humidity = new System.Windows.Forms.Label();
|
||||
this.lbl_light = new System.Windows.Forms.Label();
|
||||
this.grp_mode = new System.Windows.Forms.GroupBox();
|
||||
this.lbl_time = new System.Windows.Forms.Label();
|
||||
this.txt_time = new System.Windows.Forms.TextBox();
|
||||
this.lbl_mode = new System.Windows.Forms.Label();
|
||||
this.btn_apply = new System.Windows.Forms.Button();
|
||||
this.timer = new System.Windows.Forms.Timer(this.components);
|
||||
this.axWindowsMediaPlayer1 = new AxWMPLib.AxWindowsMediaPlayer();
|
||||
this.grp_status.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trb_curtain)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_boardcastStatus)).BeginInit();
|
||||
this.grp_input.SuspendLayout();
|
||||
this.grp_mode.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.axWindowsMediaPlayer1)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// grp_status
|
||||
//
|
||||
this.grp_status.Controls.Add(this.alarm1);
|
||||
this.grp_status.Controls.Add(this.lamp);
|
||||
this.grp_status.Controls.Add(this.lbl_windowopen);
|
||||
this.grp_status.Controls.Add(this.lbl_windowclose);
|
||||
this.grp_status.Controls.Add(this.trb_curtain);
|
||||
this.grp_status.Controls.Add(this.lbl_boardcast);
|
||||
this.grp_status.Controls.Add(this.lbl_curtain);
|
||||
this.grp_status.Controls.Add(this.pic_boardcastStatus);
|
||||
this.grp_status.Controls.Add(this.lbl_lights);
|
||||
this.grp_status.Location = new System.Drawing.Point(193, 97);
|
||||
this.grp_status.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.grp_status.Name = "grp_status";
|
||||
this.grp_status.Padding = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.grp_status.Size = new System.Drawing.Size(328, 286);
|
||||
this.grp_status.TabIndex = 8;
|
||||
this.grp_status.TabStop = false;
|
||||
this.grp_status.Text = "状态栏";
|
||||
//
|
||||
// alarm1
|
||||
//
|
||||
this.alarm1.BackColor = System.Drawing.Color.White;
|
||||
this.alarm1.Location = new System.Drawing.Point(37, 130);
|
||||
this.alarm1.Margin = new System.Windows.Forms.Padding(5);
|
||||
this.alarm1.Name = "alarm1";
|
||||
this.alarm1.Size = new System.Drawing.Size(7, 6);
|
||||
this.alarm1.TabIndex = 8;
|
||||
//
|
||||
// lamp
|
||||
//
|
||||
this.lamp.BackColor = System.Drawing.Color.White;
|
||||
this.lamp.Location = new System.Drawing.Point(172, 48);
|
||||
this.lamp.Margin = new System.Windows.Forms.Padding(5);
|
||||
this.lamp.Name = "lamp";
|
||||
this.lamp.Size = new System.Drawing.Size(148, 232);
|
||||
this.lamp.TabIndex = 6;
|
||||
//
|
||||
// lbl_windowopen
|
||||
//
|
||||
this.lbl_windowopen.AutoSize = true;
|
||||
this.lbl_windowopen.Location = new System.Drawing.Point(13, 101);
|
||||
this.lbl_windowopen.Name = "lbl_windowopen";
|
||||
this.lbl_windowopen.Size = new System.Drawing.Size(37, 15);
|
||||
this.lbl_windowopen.TabIndex = 5;
|
||||
this.lbl_windowopen.Text = "全开";
|
||||
//
|
||||
// lbl_windowclose
|
||||
//
|
||||
this.lbl_windowclose.AutoSize = true;
|
||||
this.lbl_windowclose.Location = new System.Drawing.Point(127, 101);
|
||||
this.lbl_windowclose.Name = "lbl_windowclose";
|
||||
this.lbl_windowclose.Size = new System.Drawing.Size(37, 15);
|
||||
this.lbl_windowclose.TabIndex = 5;
|
||||
this.lbl_windowclose.Text = "全关";
|
||||
//
|
||||
// trb_curtain
|
||||
//
|
||||
this.trb_curtain.Location = new System.Drawing.Point(16, 59);
|
||||
this.trb_curtain.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.trb_curtain.Name = "trb_curtain";
|
||||
this.trb_curtain.Size = new System.Drawing.Size(143, 56);
|
||||
this.trb_curtain.TabIndex = 4;
|
||||
this.trb_curtain.Value = 10;
|
||||
//
|
||||
// lbl_boardcast
|
||||
//
|
||||
this.lbl_boardcast.AutoSize = true;
|
||||
this.lbl_boardcast.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_boardcast.Location = new System.Drawing.Point(23, 186);
|
||||
this.lbl_boardcast.Name = "lbl_boardcast";
|
||||
this.lbl_boardcast.Size = new System.Drawing.Size(39, 15);
|
||||
this.lbl_boardcast.TabIndex = 2;
|
||||
this.lbl_boardcast.Text = "广播";
|
||||
//
|
||||
// lbl_curtain
|
||||
//
|
||||
this.lbl_curtain.AutoSize = true;
|
||||
this.lbl_curtain.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_curtain.Location = new System.Drawing.Point(13, 34);
|
||||
this.lbl_curtain.Name = "lbl_curtain";
|
||||
this.lbl_curtain.Size = new System.Drawing.Size(39, 15);
|
||||
this.lbl_curtain.TabIndex = 2;
|
||||
this.lbl_curtain.Text = "窗帘";
|
||||
//
|
||||
// pic_boardcastStatus
|
||||
//
|
||||
this.pic_boardcastStatus.Enabled = false;
|
||||
this.pic_boardcastStatus.Image = global::寝室情景模式.Properties.Resources.switch_off;
|
||||
this.pic_boardcastStatus.Location = new System.Drawing.Point(105, 182);
|
||||
this.pic_boardcastStatus.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.pic_boardcastStatus.Name = "pic_boardcastStatus";
|
||||
this.pic_boardcastStatus.Size = new System.Drawing.Size(44, 21);
|
||||
this.pic_boardcastStatus.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
|
||||
this.pic_boardcastStatus.TabIndex = 1;
|
||||
this.pic_boardcastStatus.TabStop = false;
|
||||
//
|
||||
// lbl_lights
|
||||
//
|
||||
this.lbl_lights.AutoSize = true;
|
||||
this.lbl_lights.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_lights.Location = new System.Drawing.Point(235, 25);
|
||||
this.lbl_lights.Name = "lbl_lights";
|
||||
this.lbl_lights.Size = new System.Drawing.Size(39, 15);
|
||||
this.lbl_lights.TabIndex = 0;
|
||||
this.lbl_lights.Text = "灯光";
|
||||
//
|
||||
// grp_input
|
||||
//
|
||||
this.grp_input.Controls.Add(this.txt_temperature);
|
||||
this.grp_input.Controls.Add(this.txt_humidity);
|
||||
this.grp_input.Controls.Add(this.com_import);
|
||||
this.grp_input.Controls.Add(this.lbl_temperature);
|
||||
this.grp_input.Controls.Add(this.txt_light);
|
||||
this.grp_input.Controls.Add(this.lbl_import);
|
||||
this.grp_input.Controls.Add(this.lbl_humidity);
|
||||
this.grp_input.Controls.Add(this.lbl_light);
|
||||
this.grp_input.Location = new System.Drawing.Point(10, 97);
|
||||
this.grp_input.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.grp_input.Name = "grp_input";
|
||||
this.grp_input.Padding = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.grp_input.Size = new System.Drawing.Size(177, 238);
|
||||
this.grp_input.TabIndex = 7;
|
||||
this.grp_input.TabStop = false;
|
||||
this.grp_input.Text = "采集量";
|
||||
//
|
||||
// txt_temperature
|
||||
//
|
||||
this.txt_temperature.Location = new System.Drawing.Point(75, 141);
|
||||
this.txt_temperature.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.txt_temperature.Name = "txt_temperature";
|
||||
this.txt_temperature.Size = new System.Drawing.Size(85, 25);
|
||||
this.txt_temperature.TabIndex = 3;
|
||||
//
|
||||
// txt_humidity
|
||||
//
|
||||
this.txt_humidity.Location = new System.Drawing.Point(76, 200);
|
||||
this.txt_humidity.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.txt_humidity.Name = "txt_humidity";
|
||||
this.txt_humidity.Size = new System.Drawing.Size(85, 25);
|
||||
this.txt_humidity.TabIndex = 3;
|
||||
//
|
||||
// com_import
|
||||
//
|
||||
this.com_import.FormattingEnabled = true;
|
||||
this.com_import.Location = new System.Drawing.Point(76, 25);
|
||||
this.com_import.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.com_import.Name = "com_import";
|
||||
this.com_import.Size = new System.Drawing.Size(85, 23);
|
||||
this.com_import.TabIndex = 6;
|
||||
//
|
||||
// lbl_temperature
|
||||
//
|
||||
this.lbl_temperature.AutoSize = true;
|
||||
this.lbl_temperature.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_temperature.Location = new System.Drawing.Point(29, 146);
|
||||
this.lbl_temperature.Name = "lbl_temperature";
|
||||
this.lbl_temperature.Size = new System.Drawing.Size(39, 15);
|
||||
this.lbl_temperature.TabIndex = 1;
|
||||
this.lbl_temperature.Text = "温度";
|
||||
//
|
||||
// txt_light
|
||||
//
|
||||
this.txt_light.Location = new System.Drawing.Point(76, 84);
|
||||
this.txt_light.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.txt_light.Name = "txt_light";
|
||||
this.txt_light.Size = new System.Drawing.Size(85, 25);
|
||||
this.txt_light.TabIndex = 2;
|
||||
//
|
||||
// lbl_import
|
||||
//
|
||||
this.lbl_import.AutoSize = true;
|
||||
this.lbl_import.Location = new System.Drawing.Point(31, 29);
|
||||
this.lbl_import.Name = "lbl_import";
|
||||
this.lbl_import.Size = new System.Drawing.Size(37, 15);
|
||||
this.lbl_import.TabIndex = 1;
|
||||
this.lbl_import.Text = "串口";
|
||||
//
|
||||
// lbl_humidity
|
||||
//
|
||||
this.lbl_humidity.AutoSize = true;
|
||||
this.lbl_humidity.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_humidity.Location = new System.Drawing.Point(31, 204);
|
||||
this.lbl_humidity.Name = "lbl_humidity";
|
||||
this.lbl_humidity.Size = new System.Drawing.Size(39, 15);
|
||||
this.lbl_humidity.TabIndex = 1;
|
||||
this.lbl_humidity.Text = "湿度";
|
||||
//
|
||||
// lbl_light
|
||||
//
|
||||
this.lbl_light.AutoSize = true;
|
||||
this.lbl_light.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_light.Location = new System.Drawing.Point(15, 88);
|
||||
this.lbl_light.Name = "lbl_light";
|
||||
this.lbl_light.Size = new System.Drawing.Size(55, 15);
|
||||
this.lbl_light.TabIndex = 0;
|
||||
this.lbl_light.Text = "光照度";
|
||||
//
|
||||
// grp_mode
|
||||
//
|
||||
this.grp_mode.Controls.Add(this.lbl_time);
|
||||
this.grp_mode.Controls.Add(this.txt_time);
|
||||
this.grp_mode.Controls.Add(this.lbl_mode);
|
||||
this.grp_mode.Controls.Add(this.btn_apply);
|
||||
this.grp_mode.Location = new System.Drawing.Point(11, 10);
|
||||
this.grp_mode.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.grp_mode.Name = "grp_mode";
|
||||
this.grp_mode.Padding = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.grp_mode.Size = new System.Drawing.Size(511, 83);
|
||||
this.grp_mode.TabIndex = 6;
|
||||
this.grp_mode.TabStop = false;
|
||||
this.grp_mode.Text = "模式选择";
|
||||
//
|
||||
// lbl_time
|
||||
//
|
||||
this.lbl_time.AutoSize = true;
|
||||
this.lbl_time.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_time.Location = new System.Drawing.Point(68, 20);
|
||||
this.lbl_time.Name = "lbl_time";
|
||||
this.lbl_time.Size = new System.Drawing.Size(71, 15);
|
||||
this.lbl_time.TabIndex = 9;
|
||||
this.lbl_time.Text = "当前时间";
|
||||
//
|
||||
// txt_time
|
||||
//
|
||||
this.txt_time.Location = new System.Drawing.Point(145, 16);
|
||||
this.txt_time.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.txt_time.Name = "txt_time";
|
||||
this.txt_time.Size = new System.Drawing.Size(127, 25);
|
||||
this.txt_time.TabIndex = 10;
|
||||
//
|
||||
// lbl_mode
|
||||
//
|
||||
this.lbl_mode.AutoSize = true;
|
||||
this.lbl_mode.Location = new System.Drawing.Point(82, 54);
|
||||
this.lbl_mode.Name = "lbl_mode";
|
||||
this.lbl_mode.Size = new System.Drawing.Size(0, 15);
|
||||
this.lbl_mode.TabIndex = 1;
|
||||
//
|
||||
// btn_apply
|
||||
//
|
||||
this.btn_apply.Location = new System.Drawing.Point(300, 16);
|
||||
this.btn_apply.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.btn_apply.Name = "btn_apply";
|
||||
this.btn_apply.Size = new System.Drawing.Size(129, 25);
|
||||
this.btn_apply.TabIndex = 2;
|
||||
this.btn_apply.Text = "开启情景模式";
|
||||
this.btn_apply.UseVisualStyleBackColor = true;
|
||||
this.btn_apply.Click += new System.EventHandler(this.btn_apply_Click);
|
||||
//
|
||||
// timer
|
||||
//
|
||||
this.timer.Interval = 1000;
|
||||
this.timer.Tick += new System.EventHandler(this.timer_Tick);
|
||||
//
|
||||
// axWindowsMediaPlayer1
|
||||
//
|
||||
this.axWindowsMediaPlayer1.Enabled = true;
|
||||
this.axWindowsMediaPlayer1.Location = new System.Drawing.Point(12, 345);
|
||||
this.axWindowsMediaPlayer1.Name = "axWindowsMediaPlayer1";
|
||||
this.axWindowsMediaPlayer1.OcxState = ((System.Windows.Forms.AxHost.State)(resources.GetObject("axWindowsMediaPlayer1.OcxState")));
|
||||
this.axWindowsMediaPlayer1.Size = new System.Drawing.Size(175, 38);
|
||||
this.axWindowsMediaPlayer1.TabIndex = 9;
|
||||
this.axWindowsMediaPlayer1.Visible = false;
|
||||
//
|
||||
// frm_control
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(539, 396);
|
||||
this.Controls.Add(this.axWindowsMediaPlayer1);
|
||||
this.Controls.Add(this.grp_status);
|
||||
this.Controls.Add(this.grp_input);
|
||||
this.Controls.Add(this.grp_mode);
|
||||
this.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.Name = "frm_control";
|
||||
this.Text = "智慧寝室情景控制";
|
||||
this.Load += new System.EventHandler(this.frm_control_Load);
|
||||
this.grp_status.ResumeLayout(false);
|
||||
this.grp_status.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trb_curtain)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_boardcastStatus)).EndInit();
|
||||
this.grp_input.ResumeLayout(false);
|
||||
this.grp_input.PerformLayout();
|
||||
this.grp_mode.ResumeLayout(false);
|
||||
this.grp_mode.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.axWindowsMediaPlayer1)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.GroupBox grp_status;
|
||||
private WinFormsDeviceControlLibrary.Lamp lamp;
|
||||
private System.Windows.Forms.Label lbl_windowopen;
|
||||
private System.Windows.Forms.Label lbl_windowclose;
|
||||
private System.Windows.Forms.TrackBar trb_curtain;
|
||||
private System.Windows.Forms.Label lbl_boardcast;
|
||||
private System.Windows.Forms.Label lbl_curtain;
|
||||
private System.Windows.Forms.PictureBox pic_boardcastStatus;
|
||||
private System.Windows.Forms.Label lbl_lights;
|
||||
private System.Windows.Forms.GroupBox grp_input;
|
||||
private System.Windows.Forms.TextBox txt_temperature;
|
||||
private System.Windows.Forms.TextBox txt_humidity;
|
||||
private System.Windows.Forms.ComboBox com_import;
|
||||
private System.Windows.Forms.Label lbl_temperature;
|
||||
private System.Windows.Forms.TextBox txt_light;
|
||||
private System.Windows.Forms.Label lbl_import;
|
||||
private System.Windows.Forms.Label lbl_humidity;
|
||||
private System.Windows.Forms.Label lbl_light;
|
||||
private System.Windows.Forms.GroupBox grp_mode;
|
||||
private System.Windows.Forms.Label lbl_mode;
|
||||
private System.Windows.Forms.Button btn_apply;
|
||||
private System.Windows.Forms.Label lbl_time;
|
||||
private System.Windows.Forms.TextBox txt_time;
|
||||
private System.Windows.Forms.Timer timer;
|
||||
private WinFormsDeviceControlLibrary.Alarm alarm1;
|
||||
private AxWMPLib.AxWindowsMediaPlayer axWindowsMediaPlayer1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
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;//引用系统IO端口库
|
||||
using DeviceCtrlLibrary;//引用自定义设备控制库
|
||||
using DeviceCtrlLibrary.Model;//引用自定义设备控制模型库
|
||||
using System.Threading;//引用线程库
|
||||
|
||||
namespace 寝室情景模式
|
||||
{
|
||||
public partial class frm_control : Form
|
||||
{
|
||||
bool IsOpen = false;//定义布尔变量记录串口打开情况,默认为false
|
||||
bool IsLampOopen = false;//定义布尔变量记录灯光打开情况,默认为false
|
||||
bool IsBoardcastOpen = false;//定义布尔变量记录广播打开情况,默认为false
|
||||
ADAM4017ResModel ResModel;//创建4017接收数据模型
|
||||
DateTime now;
|
||||
DateTime GetUp = DateTime.Today.AddHours(6.75);//6:45起床时间
|
||||
DateTime ClassBegin = DateTime.Today.AddHours(7.5);//7:30上课开始时间
|
||||
DateTime ClassEnd = DateTime.Today.AddHours(21.0);//21:00上课结束时间
|
||||
DateTime Sleep = DateTime.Today.AddHours(22.0);//22:00睡觉时间
|
||||
public frm_control()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
private void frm_control_Load(object sender, EventArgs e)
|
||||
{
|
||||
com_import.Items.Clear();
|
||||
//将系统串口名依次加入下拉列表框中
|
||||
foreach (string item in SerialPort.GetPortNames())
|
||||
{
|
||||
com_import.Items.Add(item);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void btn_apply_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!IsOpen)
|
||||
{
|
||||
IsOpen = true;
|
||||
btn_apply.Text = "停止情景模式";
|
||||
DeviceCtrlHelper.adam4017Helper.Open(com_import.Text);
|
||||
DeviceCtrlHelper.adam4150Helper.Open(com_import.Text);
|
||||
timer.Start();
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
btn_apply.Text = "开启情景模式";
|
||||
timer.Stop();
|
||||
IsOpen = false;
|
||||
DeviceCtrlHelper.adam4017Helper.Close();
|
||||
DeviceCtrlHelper.adam4150Helper.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckMode()//切换各个模式
|
||||
{
|
||||
if (now >= GetUp && now < ClassBegin)
|
||||
{
|
||||
lbl_mode.Text = "智慧寝室正处于起床模式";
|
||||
if (IsLampOopen == false)
|
||||
{
|
||||
IsLampOopen = true;
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, true);
|
||||
lamp.OnOff(true);
|
||||
}
|
||||
if (IsBoardcastOpen == false)
|
||||
{
|
||||
IsBoardcastOpen = true;
|
||||
Thread playthd = new Thread(PlayMp3);
|
||||
playthd.Start();
|
||||
}
|
||||
trb_curtain.Value = 5;
|
||||
|
||||
}
|
||||
else if (now >= ClassBegin && now < ClassEnd)
|
||||
{
|
||||
lbl_mode.Text = "智慧寝室正处于上课模式";
|
||||
if (IsLampOopen == true)
|
||||
{
|
||||
IsLampOopen = false;
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, false);
|
||||
lamp.OnOff(false);
|
||||
}
|
||||
if (IsBoardcastOpen == true)
|
||||
{
|
||||
IsBoardcastOpen = false;
|
||||
pic_boardcastStatus.Image = Properties.Resources.switch_off;
|
||||
}
|
||||
trb_curtain.Value = 0;
|
||||
|
||||
}
|
||||
else if (now >= ClassEnd && now < Sleep)
|
||||
{
|
||||
lbl_mode.Text = "智慧寝室正处于回寝模式";
|
||||
if (IsLampOopen == false)
|
||||
{
|
||||
IsLampOopen = true;
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, true);
|
||||
lamp.OnOff(true);
|
||||
}
|
||||
if (IsBoardcastOpen == false)
|
||||
{
|
||||
IsBoardcastOpen = true;
|
||||
Thread playthd = new Thread(PlayMp3);
|
||||
playthd.Start();
|
||||
}
|
||||
|
||||
trb_curtain.Value = 10;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
lbl_mode.Text = "智慧寝室正处于熄灯模式";
|
||||
if (IsLampOopen == true)
|
||||
{
|
||||
IsLampOopen = false;
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, false);
|
||||
lamp.OnOff(false);
|
||||
}
|
||||
if (IsBoardcastOpen == true)
|
||||
{
|
||||
IsBoardcastOpen = false;
|
||||
pic_boardcastStatus.Image = Properties.Resources.switch_off;
|
||||
}
|
||||
trb_curtain.Value = 10;
|
||||
}
|
||||
|
||||
}
|
||||
private void timer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
now = DateTime.Now;
|
||||
txt_time.Text = now.ToString("HH:mm:ss");//显示时间
|
||||
ResModel = DeviceCtrlHelper.adam4017Helper.SetData();//采集数据
|
||||
//显示采集到的数据
|
||||
txt_light.Text = ResModel.lightValue;//ResModel.airPressureValue
|
||||
txt_temperature.Text = ResModel.temperatureValue;//ResModel.airQualityValue
|
||||
txt_humidity.Text = ResModel.humidityValue;//ResModel.CO2Value
|
||||
CheckMode();
|
||||
}
|
||||
private void PlayMp3()//播放mp3音乐
|
||||
{
|
||||
|
||||
pic_boardcastStatus.Image = Properties.Resources.switch_on;
|
||||
axWindowsMediaPlayer1.URL = System.AppDomain.CurrentDomain.BaseDirectory + "//Resources//ring.mp3";
|
||||
axWindowsMediaPlayer1.Ctlcontrols.play();
|
||||
Thread.Sleep(4000);
|
||||
pic_boardcastStatus.Image = Properties.Resources.switch_off;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 frm_control());
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 项目资源(平台素材库)。在"项目资源"里添加或移除,不要手动修改这个文件。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace 寝室情景模式.Properties {
|
||||
|
||||
/// <summary>
|
||||
/// 项目资源:Properties.Resources.资源名
|
||||
/// </summary>
|
||||
internal class Resources {
|
||||
|
||||
/// <summary>开关:开</summary>
|
||||
internal static System.Drawing.Bitmap switch_on {
|
||||
get { return global::Sim.Host.ProjectResources.Image("switch_on.png"); }
|
||||
}
|
||||
|
||||
/// <summary>开关:关</summary>
|
||||
internal static System.Drawing.Bitmap switch_off {
|
||||
get { return global::Sim.Host.ProjectResources.Image("switch_off.png"); }
|
||||
}
|
||||
|
||||
/// <summary>铃声</summary>
|
||||
internal static byte[] ring {
|
||||
get { return global::Sim.Host.ProjectResources.Bytes("ring.mp3"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# 评分规则:书上评价表的功能项。每项单独启动一次程序,按步骤操作,核对结果。
|
||||
# 情景模式按时刻切换:用 clock 拨实训台的虚拟时钟(今天的某个时刻)。格式说明见 server/src/grading/checks.ts
|
||||
- title: 能实时显示环境数据
|
||||
steps:
|
||||
- select: { control: com_import, item: COM2 }
|
||||
- click: btn_apply
|
||||
- expect: { control: btn_apply, text: 停止情景模式, desc: 按钮文字变成"停止情景模式" }
|
||||
- board: { light: 650, temperature: 23, humidity: 51 }
|
||||
- expect: { control: txt_light, text: '650', within: 5s }
|
||||
- expect: { control: txt_temperature, text: '23', within: 5s }
|
||||
- expect: { control: txt_time, matches: '^\d{2}:\d{2}:\d{2}$', desc: 显示当前时间 }
|
||||
|
||||
- title: 能根据时间段的变化判断寝室情景实时控制智能设备
|
||||
steps:
|
||||
- clock: '06:50:00'
|
||||
- select: { control: com_import, item: COM2 }
|
||||
- click: btn_apply
|
||||
- expect: { control: lbl_mode, contains: 起床模式, within: 5s, desc: 6:50 是起床模式 }
|
||||
- expect: { relay: 1, on: true, desc: 起床模式打开灯光 }
|
||||
- expect: { control: trb_curtain, value: 5, desc: 起床模式窗帘半开 }
|
||||
- expect: { media: ring.mp3, desc: 起床模式广播播放铃声 }
|
||||
- clock: '08:00:00'
|
||||
- expect: { control: lbl_mode, contains: 上课模式, within: 5s, desc: 8:00 是上课模式 }
|
||||
- expect: { relay: 1, on: false, desc: 上课模式关闭灯光 }
|
||||
- expect: { control: trb_curtain, value: 0, desc: 上课模式窗帘全开 }
|
||||
- clock: '21:30:00'
|
||||
- expect: { control: lbl_mode, contains: 回寝模式, within: 5s, desc: 21:30 是回寝模式 }
|
||||
- expect: { control: trb_curtain, value: 10, desc: 回寝模式关窗帘 }
|
||||
- clock: '23:00:00'
|
||||
- expect: { control: lbl_mode, contains: 熄灯模式, within: 5s, desc: 23:00 是熄灯模式 }
|
||||
- expect: { relay: 1, on: false, desc: 熄灯模式关闭灯光 }
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "项目7 任务2 实现智慧寝室",
|
||||
"source": "教材书 p141–145,界面和代码取自配套参考工程;开关图片和铃声来自平台素材库。按时刻切换情景模式:在实训台的时钟上拨时间"
|
||||
}
|
||||
Generated
+611
@@ -0,0 +1,611 @@
|
||||
namespace 会议室情景模式
|
||||
{
|
||||
partial class frm_control
|
||||
{
|
||||
/// <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.components = new System.ComponentModel.Container();
|
||||
this.grp_status = new System.Windows.Forms.GroupBox();
|
||||
this.lbl_subMicrophone = new System.Windows.Forms.Label();
|
||||
this.lbl_mainMicrophone = new System.Windows.Forms.Label();
|
||||
this.lamp = new WinFormsDeviceControlLibrary.Lamp();
|
||||
this.lbl_curtainopen = new System.Windows.Forms.Label();
|
||||
this.lbl_curtainclose = new System.Windows.Forms.Label();
|
||||
this.trb_curtain = new System.Windows.Forms.TrackBar();
|
||||
this.lbl_microphone = new System.Windows.Forms.Label();
|
||||
this.lbl_curtain = new System.Windows.Forms.Label();
|
||||
this.pic_lightsStatus = new System.Windows.Forms.PictureBox();
|
||||
this.pic_subMicrophone = new System.Windows.Forms.PictureBox();
|
||||
this.pic_mainMicrophone = new System.Windows.Forms.PictureBox();
|
||||
this.lbl_lights = new System.Windows.Forms.Label();
|
||||
this.grp_input = new System.Windows.Forms.GroupBox();
|
||||
this.lbl_camera = new System.Windows.Forms.Label();
|
||||
this.txt_camera = new System.Windows.Forms.TextBox();
|
||||
this.txt_temperature = new System.Windows.Forms.TextBox();
|
||||
this.txt_airQuality = new System.Windows.Forms.TextBox();
|
||||
this.txt_humidity = new System.Windows.Forms.TextBox();
|
||||
this.com_import = new System.Windows.Forms.ComboBox();
|
||||
this.lbl_temperature = new System.Windows.Forms.Label();
|
||||
this.txt_airPressure = new System.Windows.Forms.TextBox();
|
||||
this.txt_light = new System.Windows.Forms.TextBox();
|
||||
this.lbl_import = new System.Windows.Forms.Label();
|
||||
this.lbl_airQuality = new System.Windows.Forms.Label();
|
||||
this.lbl_airPressure = new System.Windows.Forms.Label();
|
||||
this.lbl_humidity = new System.Windows.Forms.Label();
|
||||
this.lbl_light = new System.Windows.Forms.Label();
|
||||
this.grp_mode = new System.Windows.Forms.GroupBox();
|
||||
this.com_mode = new System.Windows.Forms.ComboBox();
|
||||
this.lbl_second = new System.Windows.Forms.Label();
|
||||
this.lbl_minute = new System.Windows.Forms.Label();
|
||||
this.lbl_hour = new System.Windows.Forms.Label();
|
||||
this.lbl_time = new System.Windows.Forms.Label();
|
||||
this.txt_second = new System.Windows.Forms.TextBox();
|
||||
this.txt_minute = new System.Windows.Forms.TextBox();
|
||||
this.txt_hour = new System.Windows.Forms.TextBox();
|
||||
this.chk_record = new System.Windows.Forms.CheckBox();
|
||||
this.lbl_mode = new System.Windows.Forms.Label();
|
||||
this.btn_exit = new System.Windows.Forms.Button();
|
||||
this.btn_reserve = new System.Windows.Forms.Button();
|
||||
this.timer = new System.Windows.Forms.Timer(this.components);
|
||||
this.grp_status.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trb_curtain)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_lightsStatus)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_subMicrophone)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_mainMicrophone)).BeginInit();
|
||||
this.grp_input.SuspendLayout();
|
||||
this.grp_mode.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// grp_status
|
||||
//
|
||||
this.grp_status.Controls.Add(this.lbl_subMicrophone);
|
||||
this.grp_status.Controls.Add(this.lbl_mainMicrophone);
|
||||
this.grp_status.Controls.Add(this.lamp);
|
||||
this.grp_status.Controls.Add(this.lbl_curtainopen);
|
||||
this.grp_status.Controls.Add(this.lbl_curtainclose);
|
||||
this.grp_status.Controls.Add(this.trb_curtain);
|
||||
this.grp_status.Controls.Add(this.lbl_microphone);
|
||||
this.grp_status.Controls.Add(this.lbl_curtain);
|
||||
this.grp_status.Controls.Add(this.pic_lightsStatus);
|
||||
this.grp_status.Controls.Add(this.pic_subMicrophone);
|
||||
this.grp_status.Controls.Add(this.pic_mainMicrophone);
|
||||
this.grp_status.Controls.Add(this.lbl_lights);
|
||||
this.grp_status.Location = new System.Drawing.Point(145, 100);
|
||||
this.grp_status.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.grp_status.Name = "grp_status";
|
||||
this.grp_status.Padding = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.grp_status.Size = new System.Drawing.Size(294, 270);
|
||||
this.grp_status.TabIndex = 11;
|
||||
this.grp_status.TabStop = false;
|
||||
this.grp_status.Text = "状态栏";
|
||||
//
|
||||
// lbl_subMicrophone
|
||||
//
|
||||
this.lbl_subMicrophone.AutoSize = true;
|
||||
this.lbl_subMicrophone.Location = new System.Drawing.Point(113, 140);
|
||||
this.lbl_subMicrophone.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_subMicrophone.Name = "lbl_subMicrophone";
|
||||
this.lbl_subMicrophone.Size = new System.Drawing.Size(17, 12);
|
||||
this.lbl_subMicrophone.TabIndex = 7;
|
||||
this.lbl_subMicrophone.Text = "客";
|
||||
//
|
||||
// lbl_mainMicrophone
|
||||
//
|
||||
this.lbl_mainMicrophone.AutoSize = true;
|
||||
this.lbl_mainMicrophone.Location = new System.Drawing.Point(64, 140);
|
||||
this.lbl_mainMicrophone.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_mainMicrophone.Name = "lbl_mainMicrophone";
|
||||
this.lbl_mainMicrophone.Size = new System.Drawing.Size(17, 12);
|
||||
this.lbl_mainMicrophone.TabIndex = 7;
|
||||
this.lbl_mainMicrophone.Text = "主";
|
||||
//
|
||||
// lamp
|
||||
//
|
||||
this.lamp.BackColor = System.Drawing.Color.White;
|
||||
this.lamp.Location = new System.Drawing.Point(175, 65);
|
||||
this.lamp.Name = "lamp";
|
||||
this.lamp.Size = new System.Drawing.Size(104, 177);
|
||||
this.lamp.TabIndex = 6;
|
||||
//
|
||||
// lbl_curtainopen
|
||||
//
|
||||
this.lbl_curtainopen.AutoSize = true;
|
||||
this.lbl_curtainopen.Location = new System.Drawing.Point(10, 74);
|
||||
this.lbl_curtainopen.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_curtainopen.Name = "lbl_curtainopen";
|
||||
this.lbl_curtainopen.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_curtainopen.TabIndex = 5;
|
||||
this.lbl_curtainopen.Text = "全开";
|
||||
//
|
||||
// lbl_curtainclose
|
||||
//
|
||||
this.lbl_curtainclose.AutoSize = true;
|
||||
this.lbl_curtainclose.Location = new System.Drawing.Point(95, 74);
|
||||
this.lbl_curtainclose.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_curtainclose.Name = "lbl_curtainclose";
|
||||
this.lbl_curtainclose.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_curtainclose.TabIndex = 5;
|
||||
this.lbl_curtainclose.Text = "全关";
|
||||
//
|
||||
// trb_curtain
|
||||
//
|
||||
this.trb_curtain.Location = new System.Drawing.Point(12, 47);
|
||||
this.trb_curtain.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.trb_curtain.Name = "trb_curtain";
|
||||
this.trb_curtain.Size = new System.Drawing.Size(107, 45);
|
||||
this.trb_curtain.TabIndex = 4;
|
||||
this.trb_curtain.Value = 10;
|
||||
//
|
||||
// lbl_microphone
|
||||
//
|
||||
this.lbl_microphone.AutoSize = true;
|
||||
this.lbl_microphone.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_microphone.Location = new System.Drawing.Point(10, 121);
|
||||
this.lbl_microphone.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_microphone.Name = "lbl_microphone";
|
||||
this.lbl_microphone.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_microphone.TabIndex = 2;
|
||||
this.lbl_microphone.Text = "话筒";
|
||||
//
|
||||
// lbl_curtain
|
||||
//
|
||||
this.lbl_curtain.AutoSize = true;
|
||||
this.lbl_curtain.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_curtain.Location = new System.Drawing.Point(10, 35);
|
||||
this.lbl_curtain.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_curtain.Name = "lbl_curtain";
|
||||
this.lbl_curtain.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_curtain.TabIndex = 2;
|
||||
this.lbl_curtain.Text = "窗帘";
|
||||
//
|
||||
// pic_lightsStatus
|
||||
//
|
||||
this.pic_lightsStatus.Image = global::会议室情景模式.Properties.Resources.switch_off;
|
||||
this.pic_lightsStatus.Location = new System.Drawing.Point(211, 35);
|
||||
this.pic_lightsStatus.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.pic_lightsStatus.Name = "pic_lightsStatus";
|
||||
this.pic_lightsStatus.Size = new System.Drawing.Size(33, 17);
|
||||
this.pic_lightsStatus.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
|
||||
this.pic_lightsStatus.TabIndex = 1;
|
||||
this.pic_lightsStatus.TabStop = false;
|
||||
this.pic_lightsStatus.Click += new System.EventHandler(this.pic_lightsStatus_Click);
|
||||
//
|
||||
// pic_subMicrophone
|
||||
//
|
||||
this.pic_subMicrophone.Image = global::会议室情景模式.Properties.Resources.switch_off;
|
||||
this.pic_subMicrophone.Location = new System.Drawing.Point(105, 118);
|
||||
this.pic_subMicrophone.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.pic_subMicrophone.Name = "pic_subMicrophone";
|
||||
this.pic_subMicrophone.Size = new System.Drawing.Size(33, 17);
|
||||
this.pic_subMicrophone.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
|
||||
this.pic_subMicrophone.TabIndex = 1;
|
||||
this.pic_subMicrophone.TabStop = false;
|
||||
this.pic_subMicrophone.Click += new System.EventHandler(this.pic_subMicrophone_Click);
|
||||
//
|
||||
// pic_mainMicrophone
|
||||
//
|
||||
this.pic_mainMicrophone.Image = global::会议室情景模式.Properties.Resources.switch_off;
|
||||
this.pic_mainMicrophone.Location = new System.Drawing.Point(57, 118);
|
||||
this.pic_mainMicrophone.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.pic_mainMicrophone.Name = "pic_mainMicrophone";
|
||||
this.pic_mainMicrophone.Size = new System.Drawing.Size(33, 17);
|
||||
this.pic_mainMicrophone.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
|
||||
this.pic_mainMicrophone.TabIndex = 1;
|
||||
this.pic_mainMicrophone.TabStop = false;
|
||||
this.pic_mainMicrophone.Click += new System.EventHandler(this.pic_mainMicrophone_Click);
|
||||
//
|
||||
// lbl_lights
|
||||
//
|
||||
this.lbl_lights.AutoSize = true;
|
||||
this.lbl_lights.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_lights.Location = new System.Drawing.Point(213, 21);
|
||||
this.lbl_lights.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_lights.Name = "lbl_lights";
|
||||
this.lbl_lights.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_lights.TabIndex = 0;
|
||||
this.lbl_lights.Text = "灯光";
|
||||
//
|
||||
// grp_input
|
||||
//
|
||||
this.grp_input.Controls.Add(this.lbl_camera);
|
||||
this.grp_input.Controls.Add(this.txt_camera);
|
||||
this.grp_input.Controls.Add(this.txt_temperature);
|
||||
this.grp_input.Controls.Add(this.txt_airQuality);
|
||||
this.grp_input.Controls.Add(this.txt_humidity);
|
||||
this.grp_input.Controls.Add(this.com_import);
|
||||
this.grp_input.Controls.Add(this.lbl_temperature);
|
||||
this.grp_input.Controls.Add(this.txt_airPressure);
|
||||
this.grp_input.Controls.Add(this.txt_light);
|
||||
this.grp_input.Controls.Add(this.lbl_import);
|
||||
this.grp_input.Controls.Add(this.lbl_airQuality);
|
||||
this.grp_input.Controls.Add(this.lbl_airPressure);
|
||||
this.grp_input.Controls.Add(this.lbl_humidity);
|
||||
this.grp_input.Controls.Add(this.lbl_light);
|
||||
this.grp_input.Location = new System.Drawing.Point(8, 100);
|
||||
this.grp_input.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.grp_input.Name = "grp_input";
|
||||
this.grp_input.Padding = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.grp_input.Size = new System.Drawing.Size(133, 270);
|
||||
this.grp_input.TabIndex = 10;
|
||||
this.grp_input.TabStop = false;
|
||||
this.grp_input.Text = "采集量";
|
||||
//
|
||||
// lbl_camera
|
||||
//
|
||||
this.lbl_camera.AutoSize = true;
|
||||
this.lbl_camera.Location = new System.Drawing.Point(26, 222);
|
||||
this.lbl_camera.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_camera.Name = "lbl_camera";
|
||||
this.lbl_camera.Size = new System.Drawing.Size(77, 12);
|
||||
this.lbl_camera.TabIndex = 8;
|
||||
this.lbl_camera.Text = "摄像头IP地址";
|
||||
//
|
||||
// txt_camera
|
||||
//
|
||||
this.txt_camera.Location = new System.Drawing.Point(19, 244);
|
||||
this.txt_camera.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.txt_camera.Name = "txt_camera";
|
||||
this.txt_camera.Size = new System.Drawing.Size(95, 21);
|
||||
this.txt_camera.TabIndex = 9;
|
||||
this.txt_camera.Text = "192.168.14.131";
|
||||
//
|
||||
// txt_temperature
|
||||
//
|
||||
this.txt_temperature.Location = new System.Drawing.Point(62, 80);
|
||||
this.txt_temperature.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.txt_temperature.Name = "txt_temperature";
|
||||
this.txt_temperature.Size = new System.Drawing.Size(65, 21);
|
||||
this.txt_temperature.TabIndex = 3;
|
||||
//
|
||||
// txt_airQuality
|
||||
//
|
||||
this.txt_airQuality.Location = new System.Drawing.Point(62, 190);
|
||||
this.txt_airQuality.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.txt_airQuality.Name = "txt_airQuality";
|
||||
this.txt_airQuality.Size = new System.Drawing.Size(65, 21);
|
||||
this.txt_airQuality.TabIndex = 3;
|
||||
//
|
||||
// txt_humidity
|
||||
//
|
||||
this.txt_humidity.Location = new System.Drawing.Point(62, 117);
|
||||
this.txt_humidity.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.txt_humidity.Name = "txt_humidity";
|
||||
this.txt_humidity.Size = new System.Drawing.Size(65, 21);
|
||||
this.txt_humidity.TabIndex = 3;
|
||||
//
|
||||
// com_import
|
||||
//
|
||||
this.com_import.FormattingEnabled = true;
|
||||
this.com_import.Location = new System.Drawing.Point(57, 13);
|
||||
this.com_import.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.com_import.Name = "com_import";
|
||||
this.com_import.Size = new System.Drawing.Size(65, 20);
|
||||
this.com_import.TabIndex = 6;
|
||||
//
|
||||
// lbl_temperature
|
||||
//
|
||||
this.lbl_temperature.AutoSize = true;
|
||||
this.lbl_temperature.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_temperature.Location = new System.Drawing.Point(30, 83);
|
||||
this.lbl_temperature.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_temperature.Name = "lbl_temperature";
|
||||
this.lbl_temperature.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_temperature.TabIndex = 1;
|
||||
this.lbl_temperature.Text = "温度";
|
||||
//
|
||||
// txt_airPressure
|
||||
//
|
||||
this.txt_airPressure.Location = new System.Drawing.Point(62, 153);
|
||||
this.txt_airPressure.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.txt_airPressure.Name = "txt_airPressure";
|
||||
this.txt_airPressure.Size = new System.Drawing.Size(65, 21);
|
||||
this.txt_airPressure.TabIndex = 2;
|
||||
//
|
||||
// txt_light
|
||||
//
|
||||
this.txt_light.Location = new System.Drawing.Point(62, 43);
|
||||
this.txt_light.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.txt_light.Name = "txt_light";
|
||||
this.txt_light.Size = new System.Drawing.Size(65, 21);
|
||||
this.txt_light.TabIndex = 2;
|
||||
//
|
||||
// lbl_import
|
||||
//
|
||||
this.lbl_import.AutoSize = true;
|
||||
this.lbl_import.Location = new System.Drawing.Point(23, 17);
|
||||
this.lbl_import.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_import.Name = "lbl_import";
|
||||
this.lbl_import.Size = new System.Drawing.Size(29, 12);
|
||||
this.lbl_import.TabIndex = 1;
|
||||
this.lbl_import.Text = "串口";
|
||||
//
|
||||
// lbl_airQuality
|
||||
//
|
||||
this.lbl_airQuality.AutoSize = true;
|
||||
this.lbl_airQuality.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_airQuality.Location = new System.Drawing.Point(5, 193);
|
||||
this.lbl_airQuality.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_airQuality.Name = "lbl_airQuality";
|
||||
this.lbl_airQuality.Size = new System.Drawing.Size(57, 12);
|
||||
this.lbl_airQuality.TabIndex = 1;
|
||||
this.lbl_airQuality.Text = "空气质量";
|
||||
//
|
||||
// lbl_airPressure
|
||||
//
|
||||
this.lbl_airPressure.AutoSize = true;
|
||||
this.lbl_airPressure.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_airPressure.Location = new System.Drawing.Point(30, 157);
|
||||
this.lbl_airPressure.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_airPressure.Name = "lbl_airPressure";
|
||||
this.lbl_airPressure.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_airPressure.TabIndex = 0;
|
||||
this.lbl_airPressure.Text = "气压";
|
||||
//
|
||||
// lbl_humidity
|
||||
//
|
||||
this.lbl_humidity.AutoSize = true;
|
||||
this.lbl_humidity.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_humidity.Location = new System.Drawing.Point(30, 120);
|
||||
this.lbl_humidity.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_humidity.Name = "lbl_humidity";
|
||||
this.lbl_humidity.Size = new System.Drawing.Size(31, 12);
|
||||
this.lbl_humidity.TabIndex = 1;
|
||||
this.lbl_humidity.Text = "湿度";
|
||||
//
|
||||
// lbl_light
|
||||
//
|
||||
this.lbl_light.AutoSize = true;
|
||||
this.lbl_light.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_light.Location = new System.Drawing.Point(17, 47);
|
||||
this.lbl_light.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_light.Name = "lbl_light";
|
||||
this.lbl_light.Size = new System.Drawing.Size(44, 12);
|
||||
this.lbl_light.TabIndex = 0;
|
||||
this.lbl_light.Text = "光照度";
|
||||
//
|
||||
// grp_mode
|
||||
//
|
||||
this.grp_mode.Controls.Add(this.com_mode);
|
||||
this.grp_mode.Controls.Add(this.lbl_second);
|
||||
this.grp_mode.Controls.Add(this.lbl_minute);
|
||||
this.grp_mode.Controls.Add(this.lbl_hour);
|
||||
this.grp_mode.Controls.Add(this.lbl_time);
|
||||
this.grp_mode.Controls.Add(this.txt_second);
|
||||
this.grp_mode.Controls.Add(this.txt_minute);
|
||||
this.grp_mode.Controls.Add(this.txt_hour);
|
||||
this.grp_mode.Controls.Add(this.chk_record);
|
||||
this.grp_mode.Controls.Add(this.lbl_mode);
|
||||
this.grp_mode.Controls.Add(this.btn_exit);
|
||||
this.grp_mode.Controls.Add(this.btn_reserve);
|
||||
this.grp_mode.Location = new System.Drawing.Point(8, 8);
|
||||
this.grp_mode.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.grp_mode.Name = "grp_mode";
|
||||
this.grp_mode.Padding = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.grp_mode.Size = new System.Drawing.Size(431, 88);
|
||||
this.grp_mode.TabIndex = 9;
|
||||
this.grp_mode.TabStop = false;
|
||||
this.grp_mode.Text = "模式选择";
|
||||
//
|
||||
// com_mode
|
||||
//
|
||||
this.com_mode.FormattingEnabled = true;
|
||||
this.com_mode.Location = new System.Drawing.Point(191, 53);
|
||||
this.com_mode.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.com_mode.Name = "com_mode";
|
||||
this.com_mode.Size = new System.Drawing.Size(61, 20);
|
||||
this.com_mode.TabIndex = 12;
|
||||
this.com_mode.Text = "会议";
|
||||
//
|
||||
// lbl_second
|
||||
//
|
||||
this.lbl_second.AutoSize = true;
|
||||
this.lbl_second.Location = new System.Drawing.Point(127, 57);
|
||||
this.lbl_second.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_second.Name = "lbl_second";
|
||||
this.lbl_second.Size = new System.Drawing.Size(65, 12);
|
||||
this.lbl_second.TabIndex = 11;
|
||||
this.lbl_second.Text = "秒将会有场";
|
||||
//
|
||||
// lbl_minute
|
||||
//
|
||||
this.lbl_minute.AutoSize = true;
|
||||
this.lbl_minute.Location = new System.Drawing.Point(91, 57);
|
||||
this.lbl_minute.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_minute.Name = "lbl_minute";
|
||||
this.lbl_minute.Size = new System.Drawing.Size(17, 12);
|
||||
this.lbl_minute.TabIndex = 11;
|
||||
this.lbl_minute.Text = "分";
|
||||
//
|
||||
// lbl_hour
|
||||
//
|
||||
this.lbl_hour.AutoSize = true;
|
||||
this.lbl_hour.Location = new System.Drawing.Point(56, 57);
|
||||
this.lbl_hour.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_hour.Name = "lbl_hour";
|
||||
this.lbl_hour.Size = new System.Drawing.Size(17, 12);
|
||||
this.lbl_hour.TabIndex = 11;
|
||||
this.lbl_hour.Text = "时";
|
||||
//
|
||||
// lbl_time
|
||||
//
|
||||
this.lbl_time.AutoSize = true;
|
||||
this.lbl_time.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||
this.lbl_time.Location = new System.Drawing.Point(17, 27);
|
||||
this.lbl_time.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_time.Name = "lbl_time";
|
||||
this.lbl_time.Size = new System.Drawing.Size(126, 12);
|
||||
this.lbl_time.TabIndex = 9;
|
||||
this.lbl_time.Text = "当前时间:00:00:00";
|
||||
//
|
||||
// txt_second
|
||||
//
|
||||
this.txt_second.Location = new System.Drawing.Point(109, 53);
|
||||
this.txt_second.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.txt_second.Name = "txt_second";
|
||||
this.txt_second.Size = new System.Drawing.Size(19, 21);
|
||||
this.txt_second.TabIndex = 10;
|
||||
this.txt_second.Text = "00";
|
||||
//
|
||||
// txt_minute
|
||||
//
|
||||
this.txt_minute.Location = new System.Drawing.Point(73, 53);
|
||||
this.txt_minute.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.txt_minute.Name = "txt_minute";
|
||||
this.txt_minute.Size = new System.Drawing.Size(19, 21);
|
||||
this.txt_minute.TabIndex = 10;
|
||||
this.txt_minute.Text = "20";
|
||||
//
|
||||
// txt_hour
|
||||
//
|
||||
this.txt_hour.Location = new System.Drawing.Point(38, 53);
|
||||
this.txt_hour.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.txt_hour.Name = "txt_hour";
|
||||
this.txt_hour.Size = new System.Drawing.Size(19, 21);
|
||||
this.txt_hour.TabIndex = 10;
|
||||
this.txt_hour.Text = "13";
|
||||
//
|
||||
// chk_record
|
||||
//
|
||||
this.chk_record.AutoSize = true;
|
||||
this.chk_record.Location = new System.Drawing.Point(344, 26);
|
||||
this.chk_record.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.chk_record.Name = "chk_record";
|
||||
this.chk_record.Size = new System.Drawing.Size(72, 16);
|
||||
this.chk_record.TabIndex = 3;
|
||||
this.chk_record.Text = "拍照记录";
|
||||
this.chk_record.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// lbl_mode
|
||||
//
|
||||
this.lbl_mode.AutoSize = true;
|
||||
this.lbl_mode.Location = new System.Drawing.Point(162, 27);
|
||||
this.lbl_mode.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
|
||||
this.lbl_mode.Name = "lbl_mode";
|
||||
this.lbl_mode.Size = new System.Drawing.Size(149, 12);
|
||||
this.lbl_mode.TabIndex = 1;
|
||||
this.lbl_mode.Text = "智慧会议室未开启情景模式";
|
||||
//
|
||||
// btn_exit
|
||||
//
|
||||
this.btn_exit.Location = new System.Drawing.Point(345, 49);
|
||||
this.btn_exit.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.btn_exit.Name = "btn_exit";
|
||||
this.btn_exit.Size = new System.Drawing.Size(51, 27);
|
||||
this.btn_exit.TabIndex = 2;
|
||||
this.btn_exit.Text = "退出";
|
||||
this.btn_exit.UseVisualStyleBackColor = true;
|
||||
this.btn_exit.Click += new System.EventHandler(this.btn_exit_Click);
|
||||
//
|
||||
// btn_reserve
|
||||
//
|
||||
this.btn_reserve.Location = new System.Drawing.Point(277, 49);
|
||||
this.btn_reserve.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.btn_reserve.Name = "btn_reserve";
|
||||
this.btn_reserve.Size = new System.Drawing.Size(51, 27);
|
||||
this.btn_reserve.TabIndex = 2;
|
||||
this.btn_reserve.Text = "预约";
|
||||
this.btn_reserve.UseVisualStyleBackColor = true;
|
||||
this.btn_reserve.Click += new System.EventHandler(this.btn_reserve_Click);
|
||||
//
|
||||
// timer
|
||||
//
|
||||
this.timer.Interval = 1000;
|
||||
this.timer.Tick += new System.EventHandler(this.timer_Tick);
|
||||
//
|
||||
// frm_control
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(447, 376);
|
||||
this.Controls.Add(this.grp_status);
|
||||
this.Controls.Add(this.grp_input);
|
||||
this.Controls.Add(this.grp_mode);
|
||||
this.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
|
||||
this.Name = "frm_control";
|
||||
this.Text = "智慧会议室情景模式";
|
||||
this.Load += new System.EventHandler(this.frm_control_Load);
|
||||
this.grp_status.ResumeLayout(false);
|
||||
this.grp_status.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trb_curtain)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_lightsStatus)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_subMicrophone)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pic_mainMicrophone)).EndInit();
|
||||
this.grp_input.ResumeLayout(false);
|
||||
this.grp_input.PerformLayout();
|
||||
this.grp_mode.ResumeLayout(false);
|
||||
this.grp_mode.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.GroupBox grp_status;
|
||||
private WinFormsDeviceControlLibrary.Lamp lamp;
|
||||
private System.Windows.Forms.Label lbl_curtainopen;
|
||||
private System.Windows.Forms.Label lbl_curtainclose;
|
||||
private System.Windows.Forms.TrackBar trb_curtain;
|
||||
private System.Windows.Forms.Label lbl_microphone;
|
||||
private System.Windows.Forms.Label lbl_curtain;
|
||||
private System.Windows.Forms.PictureBox pic_lightsStatus;
|
||||
private System.Windows.Forms.PictureBox pic_mainMicrophone;
|
||||
private System.Windows.Forms.Label lbl_lights;
|
||||
private System.Windows.Forms.GroupBox grp_input;
|
||||
private System.Windows.Forms.TextBox txt_temperature;
|
||||
private System.Windows.Forms.TextBox txt_humidity;
|
||||
private System.Windows.Forms.ComboBox com_import;
|
||||
private System.Windows.Forms.Label lbl_temperature;
|
||||
private System.Windows.Forms.TextBox txt_light;
|
||||
private System.Windows.Forms.Label lbl_import;
|
||||
private System.Windows.Forms.Label lbl_humidity;
|
||||
private System.Windows.Forms.Label lbl_light;
|
||||
private System.Windows.Forms.GroupBox grp_mode;
|
||||
private System.Windows.Forms.Label lbl_time;
|
||||
private System.Windows.Forms.TextBox txt_hour;
|
||||
private System.Windows.Forms.CheckBox chk_record;
|
||||
private System.Windows.Forms.Label lbl_mode;
|
||||
private System.Windows.Forms.Button btn_reserve;
|
||||
private System.Windows.Forms.TextBox txt_airQuality;
|
||||
private System.Windows.Forms.TextBox txt_airPressure;
|
||||
private System.Windows.Forms.Label lbl_airQuality;
|
||||
private System.Windows.Forms.Label lbl_airPressure;
|
||||
private System.Windows.Forms.ComboBox com_mode;
|
||||
private System.Windows.Forms.Label lbl_second;
|
||||
private System.Windows.Forms.Label lbl_minute;
|
||||
private System.Windows.Forms.Label lbl_hour;
|
||||
private System.Windows.Forms.TextBox txt_second;
|
||||
private System.Windows.Forms.TextBox txt_minute;
|
||||
private System.Windows.Forms.Timer timer;
|
||||
private System.Windows.Forms.Button btn_exit;
|
||||
private System.Windows.Forms.Label lbl_subMicrophone;
|
||||
private System.Windows.Forms.Label lbl_mainMicrophone;
|
||||
private System.Windows.Forms.PictureBox pic_subMicrophone;
|
||||
private System.Windows.Forms.Label lbl_camera;
|
||||
private System.Windows.Forms.TextBox txt_camera;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
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;//引用系统IO库
|
||||
using System.IO.Ports;//引用系统IO端口库
|
||||
using DeviceCtrlLibrary;//引用自定义设备控制库
|
||||
using DeviceCtrlLibrary.Model;//引用自定义设备控制模型库
|
||||
|
||||
namespace 会议室情景模式
|
||||
{
|
||||
public partial class frm_control : Form
|
||||
{
|
||||
bool IsLightsOpen = false;//定义布尔变量记录电灯打开情况,默认为false
|
||||
bool IsmainMicrophoneOpen = false;//定义布尔变量记录主麦克风打开情况,默认为false
|
||||
bool IssubMicrophoneOpen = false;//定义布尔变量记录客麦克风打开情况,默认为false
|
||||
bool IsInitialized = false;//定义布尔变量记录情景模式是否初始化,默认为false
|
||||
ADAM4017ResModel ResModel;//创建4017接收数据模型
|
||||
DateTime now;//定义时间变量记录现在的时间
|
||||
DateTime reserve=DateTime.Now.AddDays(1) ;//定义时间变量记录预约的时间,默认为明天,防止初始值小于当前时间的假预约情况
|
||||
Image ReadImage = null;//定义图片变量记录拍摄的图片,默认为空
|
||||
|
||||
public frm_control()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void frm_control_Load(object sender, EventArgs e)
|
||||
{
|
||||
com_import.Items.Clear();
|
||||
com_mode.Items.Clear();
|
||||
//将系统串口名依次加入下拉列表框中
|
||||
foreach (string item in SerialPort.GetPortNames())
|
||||
{
|
||||
com_import.Items.Add(item);
|
||||
}
|
||||
com_mode.Items.Add("会议");
|
||||
com_mode.Items.Add("讨论");
|
||||
com_mode.Items.Add("展示");
|
||||
timer.Start();
|
||||
//判断拍照保存文件夹是否存在,若无则先创建
|
||||
if (!Directory.Exists(AppDomain.CurrentDomain.BaseDirectory + "Camera/"))
|
||||
Directory.CreateDirectory(AppDomain.CurrentDomain.BaseDirectory + "Camera/");
|
||||
|
||||
DeviceCtrlHelper.ipCameraHelper.ImageRead += ipCameraHelper_ImageRead;
|
||||
}
|
||||
|
||||
private void btn_reserve_Click(object sender, EventArgs e)
|
||||
{
|
||||
reserve = DateTime.Today.AddHours(Convert.ToInt16(txt_hour.Text));//设定预约时间
|
||||
reserve = reserve.AddMinutes(Convert.ToInt16(txt_minute.Text));
|
||||
reserve = reserve.AddSeconds(Convert.ToInt16(txt_second.Text));
|
||||
}
|
||||
|
||||
void ipCameraHelper_ImageRead(ImageReadEvenArgs e)
|
||||
{
|
||||
ReadImage = e.img;
|
||||
}
|
||||
|
||||
private void ChooseMode()
|
||||
{
|
||||
lbl_mode.Text = "智慧会议室已开启" + com_mode.Text + "模式";
|
||||
switch (com_mode.Text)
|
||||
{
|
||||
case "会议":
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_on;//转换灯光图片
|
||||
lamp.OnOff(true);//开启界面灯光
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1,true);//打开1通道灯
|
||||
trb_curtain.Value = 0;//打开窗帘
|
||||
pic_mainMicrophone.Image = Properties.Resources.switch_on;//打开主麦
|
||||
pic_subMicrophone.Image = Properties.Resources.switch_off;//关闭客麦
|
||||
break;
|
||||
case "讨论":
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_on;//转换灯光图片
|
||||
lamp.OnOff(true);//开启界面灯光
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1,true);//打开1通道灯
|
||||
trb_curtain.Value = 0;//打开窗帘
|
||||
pic_mainMicrophone.Image = Properties.Resources.switch_on;//打开主麦
|
||||
pic_subMicrophone.Image = Properties.Resources.switch_on;//打开客麦
|
||||
break;
|
||||
case "展示":
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_on;//转换灯光图片
|
||||
lamp.OnOff(true);//开启界面灯光
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, true);//打开1通道灯
|
||||
trb_curtain.Value = 10;//关闭窗帘
|
||||
pic_mainMicrophone.Image = Properties.Resources.switch_on;//打开主麦
|
||||
pic_subMicrophone.Image = Properties.Resources.switch_off;//关闭主麦
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void timer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
DeviceCtrlHelper.adam4017Helper.Open(com_import.Text);
|
||||
DeviceCtrlHelper.adam4150Helper.Open(com_import.Text);
|
||||
now = DateTime.Now;
|
||||
lbl_time.Text = now.ToString("HH:mm:ss");//显示时间
|
||||
ResModel = DeviceCtrlHelper.adam4017Helper.SetData();//采集数据
|
||||
//显示采集到的数据
|
||||
txt_light.Text = ResModel.lightValue;
|
||||
txt_temperature.Text = ResModel.temperatureValue;
|
||||
txt_humidity.Text = ResModel.humidityValue;
|
||||
txt_airPressure.Text = ResModel.airPressureValue;
|
||||
txt_airQuality.Text = ResModel.airQualityValue;
|
||||
//模式判断
|
||||
if (now >= reserve)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
{
|
||||
ChooseMode();//进入选择的模式
|
||||
if (chk_record.Checked)
|
||||
{
|
||||
DeviceCtrlHelper.ipCameraHelper.Open(txt_camera.Text, "admin", "");//初始化视频监控参数
|
||||
DeviceCtrlHelper.ipCameraHelper.Start();//开启视频监控
|
||||
}
|
||||
IsInitialized = true;
|
||||
}
|
||||
if (now.Second == 0 && now.Minute % 5 == 0)//每次分钟数被5整除,秒钟数为00拍照,实现每隔5分钟拍照存储
|
||||
{
|
||||
if (ReadImage != null)
|
||||
{
|
||||
ReadImage.Save(AppDomain.CurrentDomain.BaseDirectory + "Camera/" + DateTime.Now.ToString("yyyyMMdd_HH mm ss") + ".jpg");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void pic_lightsStatus_Click(object sender, EventArgs e)
|
||||
{//电灯可以手动开关
|
||||
if (IsLightsOpen)
|
||||
{
|
||||
lamp.OnOff(false);
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_off;
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, false);//关闭1通道灯
|
||||
IsLightsOpen = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
lamp.OnOff(true);
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_on;
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, true);//关闭1通道灯
|
||||
IsLightsOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void pic_subMicrophone_Click(object sender, EventArgs e)
|
||||
{//客麦可以手动开关
|
||||
if (IssubMicrophoneOpen)
|
||||
{
|
||||
pic_subMicrophone.Image = Properties.Resources.switch_off;
|
||||
IssubMicrophoneOpen = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
pic_subMicrophone.Image = Properties.Resources.switch_on;
|
||||
IssubMicrophoneOpen = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void btn_exit_Click(object sender, EventArgs e)
|
||||
{
|
||||
timer.Stop();
|
||||
reserve = DateTime.Today.AddDays(1);
|
||||
chk_record.Enabled = false;
|
||||
pic_lightsStatus.Image = Properties.Resources.switch_off;//转换灯光图片
|
||||
lamp.OnOff(false);//关闭界面灯光
|
||||
DeviceCtrlHelper.adam4150Helper.OnOff(1, false);//关闭1通道灯
|
||||
trb_curtain.Value = 0;//关闭窗帘
|
||||
pic_mainMicrophone.Image = Properties.Resources.switch_off;//关闭主麦
|
||||
pic_subMicrophone.Image = Properties.Resources.switch_off;//关闭客麦
|
||||
IsInitialized = false;//初始化状态
|
||||
DeviceCtrlHelper.adam4150Helper.Close();
|
||||
DeviceCtrlHelper.adam4017Helper.Close();//关闭串口
|
||||
DeviceCtrlHelper.ipCameraHelper.Stop();//关闭视频监控
|
||||
|
||||
}
|
||||
|
||||
private void pic_mainMicrophone_Click(object sender, EventArgs e)
|
||||
{//主麦可以手动开关
|
||||
if (IsmainMicrophoneOpen)
|
||||
{
|
||||
pic_mainMicrophone.Image = Properties.Resources.switch_off;
|
||||
IsmainMicrophoneOpen = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
pic_mainMicrophone.Image = Properties.Resources.switch_on;
|
||||
IsmainMicrophoneOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 frm_control());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 项目资源(平台素材库)。在"项目资源"里添加或移除,不要手动修改这个文件。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace 会议室情景模式.Properties {
|
||||
|
||||
/// <summary>
|
||||
/// 项目资源:Properties.Resources.资源名
|
||||
/// </summary>
|
||||
internal class Resources {
|
||||
|
||||
/// <summary>开关:开</summary>
|
||||
internal static System.Drawing.Bitmap switch_on {
|
||||
get { return global::Sim.Host.ProjectResources.Image("switch_on.png"); }
|
||||
}
|
||||
|
||||
/// <summary>开关:关</summary>
|
||||
internal static System.Drawing.Bitmap switch_off {
|
||||
get { return global::Sim.Host.ProjectResources.Image("switch_off.png"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# 评分规则:书上评价表的功能项。每项单独启动一次程序,按步骤操作,核对结果。
|
||||
# 预约时刻用 clock 拨实训台的虚拟时钟。格式说明见 server/src/grading/checks.ts
|
||||
- title: 能实时显示环境数据
|
||||
steps:
|
||||
- select: { control: com_import, item: COM2 }
|
||||
- board: { light: 640, temperature: 24, airQuality: 88 }
|
||||
- expect: { control: txt_light, text: '640', within: 5s }
|
||||
- expect: { control: txt_temperature, text: '24', within: 5s }
|
||||
- expect: { control: txt_airQuality, text: '88', within: 5s }
|
||||
|
||||
- title: 能根据预约时间控制智能设备
|
||||
steps:
|
||||
- clock: '13:19:54'
|
||||
- select: { control: com_import, item: COM2 }
|
||||
- select: { control: com_mode, item: 讨论 }
|
||||
- text: { control: txt_hour, value: '13' }
|
||||
- text: { control: txt_minute, value: '20' }
|
||||
- text: { control: txt_second, value: '00' }
|
||||
- click: btn_reserve
|
||||
- expect: { relay: 1, on: false, desc: 预约时间(13:20:00)没到时不开灯 }
|
||||
- expect: { control: lbl_mode, contains: 讨论模式, within: 15s, desc: 到了预约时间开启讨论模式 }
|
||||
- expect: { relay: 1, on: true, desc: 讨论模式打开灯光 }
|
||||
- expect: { control: trb_curtain, value: 0, desc: 讨论模式打开窗帘 }
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "项目7 任务3 实现智慧会议室",
|
||||
"source": "教材书 p148–153,界面和代码取自配套参考工程;补上书上漏写的填充串口列表。预约时刻:在实训台的时钟上拨时间"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// HTTP 接口。除登录外都要先登录;/api/teacher/* 只有老师能用。
|
||||
import type { Database } from 'bun:sqlite'
|
||||
import { dirname } from 'node:path'
|
||||
import { Hono, type Context } from 'hono'
|
||||
import { bearerToken, login, logout, refresh, tokenUser, tooManyFailures, type Tokens, type User } from './auth'
|
||||
import { createClass, deleteClass, deleteStudent, importStudents, listClasses, listStudents, renameClass, resetPassword, type ImportRow } from './classes'
|
||||
@@ -71,6 +72,15 @@ export function createApp(db: Database, gradingQueue: GradingQueue, aiQueue: AiQ
|
||||
return c.json({})
|
||||
})
|
||||
|
||||
// 项目资源素材库(Properties.Resources 的图片、声音):设计器预览用 <img> 直接显示,不需要登录
|
||||
app.get('/resources/:file', async (c) => {
|
||||
const name = c.req.param('file')
|
||||
if (!/^[\w.-]+$/.test(name)) return c.notFound()
|
||||
const file = Bun.file(`${dirname(config.runnerDll)}/Resources/gallery/${name}`)
|
||||
if (!(await file.exists())) return c.notFound()
|
||||
return new Response(file, { headers: { 'Cache-Control': 'public, max-age=3600' } })
|
||||
})
|
||||
|
||||
// 其余接口都要先登录(请求头 Authorization: Bearer <令牌>)
|
||||
app.use('/api/*', async (c, next) => {
|
||||
const user = await tokenUser(db, bearerToken(c.req.header('authorization')))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
clockWall,
|
||||
ANALOG_CHANNELS,
|
||||
ANALOG_INFO,
|
||||
type BoardState,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
type RunnerInput,
|
||||
type RunnerMessage,
|
||||
} from '@iot/shared'
|
||||
import { config } from './config'
|
||||
|
||||
export function defaultBoardState(): BoardState {
|
||||
return {
|
||||
@@ -16,6 +18,7 @@ export function defaultBoardState(): BoardState {
|
||||
zigbee: [false, false],
|
||||
ledText: '',
|
||||
camera: { on: false, x: 3, y: 1 },
|
||||
clock: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +32,7 @@ export class Board {
|
||||
type: 'board',
|
||||
analog: ANALOG_CHANNELS.map((key) => this.state.analog[key]),
|
||||
digital: DIGITAL_CHANNELS.map((key) => this.state.digital[key]),
|
||||
clock: this.state.clock,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +53,31 @@ export class Board {
|
||||
this.state.digital[name] = value
|
||||
changed = true
|
||||
}
|
||||
if (message.clock === null) {
|
||||
changed ||= this.state.clock !== null
|
||||
this.state.clock = null
|
||||
} else if (message.clock && typeof message.clock === 'object') {
|
||||
changed = this.setClock(message.clock.time, message.clock.speed) || changed
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
/**
|
||||
* 拨虚拟时钟:time 是墙上时间(yyyy-MM-ddTHH:mm:ss,不给就从现在的时刻接着走),speed 是倍速(不给就保持原来的)。
|
||||
* 恢复真实时间是 clock = null。
|
||||
*/
|
||||
setClock(time: string | undefined, speed: number | undefined) {
|
||||
if (time !== undefined && (typeof time !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(time) || Number.isNaN(Date.parse(`${time}Z`)))) return false
|
||||
if (speed !== undefined && (typeof speed !== 'number' || !Number.isFinite(speed) || speed < 1 || speed > 3600)) return false
|
||||
const now = Date.now()
|
||||
this.state.clock = {
|
||||
wall: time ?? (this.state.clock ? clockWall(this.state.clock, now) : wallNow()),
|
||||
realAt: now,
|
||||
speed: speed ?? this.state.clock?.speed ?? 1,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 学生程序控制了输出设备;返回 true 表示实训台状态有变化 */
|
||||
applyDevice(message: Extract<RunnerMessage, { type: 'device' }>) {
|
||||
switch (`${message.device}/${message.action}`) {
|
||||
@@ -93,3 +119,11 @@ export class Board {
|
||||
this.state = defaultBoardState()
|
||||
}
|
||||
}
|
||||
|
||||
/** 服务器所在时区(config.timeZone)现在的墙上时间 yyyy-MM-ddTHH:mm:ss */
|
||||
export function wallNow(now = new Date()) {
|
||||
return new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: config.timeZone,
|
||||
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
|
||||
}).format(now).replace(' ', 'T')
|
||||
}
|
||||
@@ -17,13 +17,15 @@ export type Step =
|
||||
| { kind: 'edit'; control: string; row: number; column: number | string; value: string }
|
||||
/** 在日期时间选择器里选日期(和时间):2024-01-15 或 2024-01-15 08:30:00 */
|
||||
| { kind: 'pick'; control: string; value: string }
|
||||
/** 拨虚拟时钟到今天的某个时刻(HH:mm:ss),可以同时设倍速 */
|
||||
| { kind: 'clock'; time: string; speed?: number }
|
||||
| { kind: 'board'; analog: Partial<Record<AnalogKey, number>>; digital: Partial<Record<DigitalKey, boolean>> }
|
||||
| { kind: 'wait'; ms: number }
|
||||
/** desc:给学生看的期望说明,如"显示当前时间",不写就按期望自动生成 */
|
||||
| { kind: 'expect'; expect: Expectation; within: number; desc?: string }
|
||||
|
||||
export type Expectation =
|
||||
/** 控件属性等于某个值:text → Text,ledText → LedText(LED 控件),enabled → Enabled,checked → Checked,on → IsOn(灯泡、报警灯) */
|
||||
/** 控件属性等于某个值:text → Text,ledText → LedText(LED 控件),enabled → Enabled,checked → Checked,on → IsOn(灯泡、报警灯),value → Value(滑动条) */
|
||||
| { kind: 'prop'; control: string; prop: string; value: unknown }
|
||||
/** 控件文字包含某段文字;列表框是任意一个列表项包含 */
|
||||
| { kind: 'contains'; control: string; text: string }
|
||||
@@ -38,6 +40,8 @@ export type Expectation =
|
||||
| { kind: 'image'; control: string }
|
||||
/** 摄像头在传画面(on)/ 已关闭(off),或云台比开始时朝某个方向转过 */
|
||||
| { kind: 'camera'; state: 'on' | 'off' | 'up' | 'down' | 'left' | 'right' }
|
||||
/** 程序用媒体播放器播放了这个文件(如 ring.mp3) */
|
||||
| { kind: 'media'; name: string }
|
||||
/** 程序用 Image.Save 保存了图片,路径包含这段文字(如 Camera/) */
|
||||
| { kind: 'savedImage'; path: string }
|
||||
/** 数据表格里有(present)/ 没有一行同时满足这些"列标题=值" */
|
||||
@@ -47,8 +51,9 @@ export type Expectation =
|
||||
/** 期望默认最多等多久(毫秒);依赖定时器的检查在规则里写 within */
|
||||
const DEFAULT_WITHIN = 2000
|
||||
|
||||
const PROP_KEYS: Record<string, string> = { text: 'Text', ledText: 'LedText', enabled: 'Enabled', checked: 'Checked', on: 'IsOn' }
|
||||
const PROP_KEYS: Record<string, string> = { text: 'Text', ledText: 'LedText', enabled: 'Enabled', checked: 'Checked', on: 'IsOn', value: 'Value' }
|
||||
const STRING_PROPS = new Set(['Text', 'LedText'])
|
||||
const NUMBER_PROPS = new Set(['Value'])
|
||||
|
||||
/** "6s"、"500ms" 或毫秒数 */
|
||||
function duration(value: unknown, where: string): number {
|
||||
@@ -78,6 +83,7 @@ function parseExpectation(raw: Record<string, unknown>, where: string): Expectat
|
||||
if (!['on', 'off', 'up', 'down', 'left', 'right'].includes(state)) throw new Error(`${where}:camera 要写 on、off、up、down、left 或 right`)
|
||||
return { kind: 'camera', state: state as Extract<Expectation, { kind: 'camera' }>['state'] }
|
||||
}
|
||||
if ('media' in raw) return { kind: 'media', name: str(raw.media, where, '播放的文件名') }
|
||||
if ('savedImage' in raw) return { kind: 'savedImage', path: str(raw.savedImage, where, '保存的图片路径应包含的文字') }
|
||||
if ('relay' in raw) return { kind: 'relay', index: Number(raw.relay), on: bool(raw.on, where, 'on') }
|
||||
if ('zigbee' in raw) return { kind: 'zigbee', index: Number(raw.zigbee), on: bool(raw.on, where, 'on') }
|
||||
@@ -102,7 +108,10 @@ function parseExpectation(raw: Record<string, unknown>, where: string): Expectat
|
||||
}
|
||||
if ('image' in raw) return { kind: 'image', control }
|
||||
for (const [key, prop] of Object.entries(PROP_KEYS)) {
|
||||
if (key in raw) return { kind: 'prop', control, prop, value: STRING_PROPS.has(prop) ? String(raw[key]) : bool(raw[key], where, key) }
|
||||
if (key in raw) {
|
||||
if (NUMBER_PROPS.has(prop) && typeof raw[key] !== 'number') throw new Error(`${where}:${key} 要写数字`)
|
||||
return { kind: 'prop', control, prop, value: STRING_PROPS.has(prop) ? String(raw[key]) : NUMBER_PROPS.has(prop) ? raw[key] : bool(raw[key], where, key) }
|
||||
}
|
||||
}
|
||||
throw new Error(`${where}:不认识的期望 ${JSON.stringify(raw)}`)
|
||||
}
|
||||
@@ -140,6 +149,14 @@ function parseStep(raw: unknown, where: string): Step {
|
||||
return { kind, control: str(obj.control, where, '控件名 control'), value: String(obj.value ?? '') }
|
||||
case 'select':
|
||||
return { kind, control: str(obj.control, where, '控件名 control'), item: str(obj.item, where, '要选的项 item') }
|
||||
case 'clock': {
|
||||
// clock: 06:44:50 或 clock: { time: 06:44:50, speed: 60 }
|
||||
const time = typeof value === 'string' ? value : str(obj.time, where, '时刻 time')
|
||||
if (!/^\d{2}:\d{2}:\d{2}$/.test(time)) throw new Error(`${where}:时刻要写成 06:44:50(加引号)`)
|
||||
const speed = typeof value === 'object' && obj.speed !== undefined ? Number(obj.speed) : undefined
|
||||
if (speed !== undefined && !(speed >= 1 && speed <= 3600)) throw new Error(`${where}:倍速 speed 要在 1 到 3600 之间`)
|
||||
return { kind, time, ...(speed === undefined ? {} : { speed }) }
|
||||
}
|
||||
case 'pick': {
|
||||
const value = str(obj.value, where, '日期 value')
|
||||
if (!/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$/.test(value)) throw new Error(`${where}:日期要写成 2024-01-15 或 2024-01-15 08:30:00`)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 提示框自动点第一个按钮("确定"/"是"),免得程序一直等在 MessageBox.Show 上。
|
||||
import type { Subprocess } from 'bun'
|
||||
import type { ControlNode, RunnerInput, RunnerMessage, UserAction } from '@iot/shared'
|
||||
import { Board } from '../board'
|
||||
import { Board, wallNow } from '../board'
|
||||
import { readLines } from '../runner-process'
|
||||
import { runCommand } from '../sandbox'
|
||||
import type { Step } from './checks'
|
||||
@@ -18,6 +18,8 @@ export class ProgramDriver {
|
||||
readonly messageBoxes: { text: string; caption: string }[] = []
|
||||
/** 程序用 Image.Save 保存的图片路径 */
|
||||
readonly savedImages: string[] = []
|
||||
/** 媒体播放器播放过的文件名 */
|
||||
readonly played: string[] = []
|
||||
readonly exceptions: string[] = []
|
||||
console = ''
|
||||
exited = false
|
||||
@@ -111,6 +113,9 @@ export class ProgramDriver {
|
||||
case 'imageSaved':
|
||||
this.savedImages.push(message.path)
|
||||
break
|
||||
case 'media':
|
||||
if (message.action === 'play' && message.name) this.played.push(message.name)
|
||||
break
|
||||
case 'console':
|
||||
this.console += message.text
|
||||
break
|
||||
@@ -139,6 +144,12 @@ export class ProgramDriver {
|
||||
this.send({ type: 'action', id: node.id, ...action })
|
||||
}
|
||||
|
||||
/** 拨虚拟时钟到今天的某个时刻(服务器所在时区) */
|
||||
setClock(time: string, speed?: number) {
|
||||
this.board.setClock(`${wallNow().slice(0, 10)}T${time}`, speed)
|
||||
this.send(this.board.runnerInputs())
|
||||
}
|
||||
|
||||
setBoard(step: BoardStep) {
|
||||
this.board.applyInput({ type: 'boardInput', analog: step.analog, digital: step.digital })
|
||||
this.send(this.board.runnerInputs())
|
||||
|
||||
@@ -141,6 +141,10 @@ async function runStep(driver: ProgramDriver, step: Step) {
|
||||
driver.setBoard(step)
|
||||
await driver.settle()
|
||||
break
|
||||
case 'clock':
|
||||
driver.setClock(step.time, step.speed)
|
||||
await driver.settle()
|
||||
break
|
||||
case 'wait':
|
||||
await Bun.sleep(step.ms)
|
||||
break
|
||||
@@ -227,6 +231,8 @@ function satisfied(driver: ProgramDriver, e: Exclude<Expectation, { kind: 'uncha
|
||||
}
|
||||
case 'savedImage':
|
||||
return driver.savedImages.some((path) => path.includes(e.path))
|
||||
case 'media':
|
||||
return driver.played.includes(e.name)
|
||||
case 'row': {
|
||||
const node = driver.find(e.control)
|
||||
return node !== undefined && rowMatches(node, e.cells) === e.present
|
||||
@@ -244,6 +250,7 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
if (e.prop === 'Enabled') return `期望${prefix} ${e.control} ${desc ?? (e.value ? '可用' : '变成灰色不可用')},实际${actual === false ? '不可用' : '可用'}`
|
||||
if (e.prop === 'Checked') return `期望${prefix} ${e.control} ${desc ?? (e.value ? '被选中' : '没有被选中')},实际${actual ? '被选中' : '没有被选中'}`
|
||||
if (e.prop === 'IsOn') return `期望${prefix}窗体上的 ${e.control} ${desc ?? (e.value ? '亮' : '灭')},实际是${actual ? '亮' : '灭'}的`
|
||||
if (e.prop === 'Value') return `期望${prefix} ${e.control} ${desc ?? `的值是 ${String(e.value)}`},实际是 ${String(actual)}`
|
||||
return `期望${prefix} ${e.control} 的 ${e.prop} 是 ${String(e.value)},实际是 ${String(actual)}`
|
||||
}
|
||||
case 'contains': {
|
||||
@@ -274,6 +281,8 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
const saved = driver.savedImages.map((p) => `"${p}"`).join('、')
|
||||
return `期望${prefix}${desc ?? `保存图片到"${e.path}"`},${saved ? `实际保存的是 ${saved}` : '实际没有保存图片'}`
|
||||
}
|
||||
case 'media':
|
||||
return `期望${prefix}${desc ?? `播放 ${e.name}`},${driver.played.length ? `实际播放的是 ${driver.played.join('、')}` : '实际没有播放声音'}`
|
||||
case 'row': {
|
||||
const node = need(driver, e.control)
|
||||
const wanted = Object.entries(e.cells).map(([k, v]) => `${k}=${v}`).join(',')
|
||||
|
||||
@@ -35,7 +35,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(15))
|
||||
test('每个示例都有评分规则', () => expect(ids.length).toBe(18))
|
||||
for (const id of ids) {
|
||||
test(id, async () => {
|
||||
const result = await grade(await prepare(id, id), await checksOf(id), id)
|
||||
@@ -110,6 +110,22 @@ describe('改坏的代码', () => {
|
||||
expect(result.results[0].message).toStartWith('程序出错:System.ArgumentNullException')
|
||||
}, 60000)
|
||||
|
||||
test('情景控制:照书上把手动开关灯的判断写反,点了灯不亮', async () => {
|
||||
const id = 'p7-t1-智慧教室'
|
||||
const dir = await prepare(id, 'lamp-reversed', { file: '教室情景模式.cs', from: ' if (IsLampOpen)\n', to: ' if (!IsLampOpen)\n' })
|
||||
const checks = [
|
||||
...(await checksOf(id)),
|
||||
...parseChecks(`
|
||||
- title: 能手动开关灯
|
||||
steps:
|
||||
- click: pic_lightsStatus
|
||||
- expect: { control: lamp, on: true, desc: 点开关后灯亮 }
|
||||
`),
|
||||
]
|
||||
const result = await grade(dir, checks, id)
|
||||
expect(result.results.at(-1)).toMatchObject({ passed: false, message: '期望窗体上的 lamp 点开关后灯亮,实际是灭的' })
|
||||
}, 60000)
|
||||
|
||||
test('关闭时忘了挂起线程', async () => {
|
||||
const id = 'p5-t2-烟雾'
|
||||
const dir = await prepare(id, 'thread', { file: 'Form1.cs', from: 'thd.Suspend();//挂起线程', to: '' })
|
||||
|
||||
@@ -4,6 +4,7 @@ import { afterAll, afterEach, describe, expect, test } from 'bun:test'
|
||||
import { cp, mkdir, rm } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import type { ControlNode, RunnerInput, RunnerMessage } from '@iot/shared'
|
||||
import { wallNow } from '../src/board'
|
||||
import { compileService } from '../src/compile-service'
|
||||
import { config, ROOT } from '../src/config'
|
||||
import { cpuLimitMessage, runCommand } from '../src/sandbox'
|
||||
@@ -332,7 +333,7 @@ test('项目4 任务2:打开摄像机显示画面,转动后画面变化,
|
||||
const center = imageOf(await first)
|
||||
expect(center).toMatchObject({ width: 640, height: 480 })
|
||||
// 图片数据先于(或随)属性变化发给浏览器,同一张图只发一次
|
||||
const sent = runner.messages.filter((m) => m.type === 'image' && m.key === center.key)
|
||||
const sent = runner.messages.filter((m) => m.type === 'blob' && m.key === center.key)
|
||||
expect(sent.length).toBe(1)
|
||||
expect(sent[0]).toMatchObject({ mime: 'image/jpeg' })
|
||||
|
||||
@@ -630,6 +631,56 @@ test('数据库:照书上的写法建库建表、插入、查询、SqlCommandB
|
||||
}
|
||||
}, 30000)
|
||||
|
||||
test('虚拟时钟:拨时间、倍速后 DateTime.Now / Today 跟着走,恢复后是真实时间', async () => {
|
||||
const { runner } = await runMain(
|
||||
'clock',
|
||||
`
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
Console.WriteLine("now=" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " today=" + DateTime.Today.ToString("yyyy-MM-dd"));
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}`,
|
||||
)
|
||||
expect((await runner.waitFor('compiled')).ok).toBe(true)
|
||||
await runner.waitFor('console')
|
||||
runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: DIGITAL_OFF, clock: { wall: '2024-01-16T06:44:58', realAt: Date.now(), speed: 60 } })
|
||||
// 60 倍速:真实的 1 秒是虚拟的 1 分钟
|
||||
await runner.next('console', (m) => /now=2024-01-16 06:4[6-9]/.test(m.text), 5000)
|
||||
runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: DIGITAL_OFF, clock: null })
|
||||
const real = await runner.next('console', (m) => !m.text.includes('2024-01-16'), 3000)
|
||||
// 学生程序按服务器配置的时区(config.timeZone)计时,CI 机器本身是 UTC
|
||||
expect(real.text).toContain(`today=${wallNow().slice(0, 10)}`)
|
||||
expect(runner.messages.some((m) => m.type === 'console' && m.text.includes('today=2024-01-16'))).toBe(true)
|
||||
}, 30000)
|
||||
|
||||
test('TrackBar:拖动时先 ValueChanged 再 Scroll,超出范围的值截到两端;代码设超出范围的 Value 报错', async () => {
|
||||
const { runner } = await runMain(
|
||||
'trackbar',
|
||||
`
|
||||
var form = new Form();
|
||||
var bar = new TrackBar { Name = "bar", Minimum = 18, Maximum = 32 };
|
||||
bar.ValueChanged += (s, e) => Console.WriteLine("changed=" + bar.Value);
|
||||
bar.Scroll += (s, e) => Console.WriteLine("scroll=" + bar.Value);
|
||||
form.Load += (s, e) =>
|
||||
{
|
||||
try { bar.Value = 40; } catch (ArgumentOutOfRangeException ex) { Console.WriteLine("error=" + ex.Message); }
|
||||
};
|
||||
form.Controls.Add(bar);
|
||||
Application.Run(form);`,
|
||||
'using System.Windows.Forms;',
|
||||
)
|
||||
const { form } = await runner.waitFor('formShown')
|
||||
const bar = findByName(form, 'bar')
|
||||
expect(bar.props).toMatchObject({ Minimum: 18, Maximum: 32, Value: 18 })
|
||||
runner.send({ type: 'action', id: bar.id, action: 'scroll', value: '26' })
|
||||
await runner.waitFor('console', (m) => m.text === 'scroll=26\n')
|
||||
runner.send({ type: 'action', id: bar.id, action: 'scroll', value: '99' })
|
||||
await runner.waitFor('console', (m) => m.text === 'scroll=32\n')
|
||||
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
|
||||
expect(output).toContain("error=“40”的值对于“Value”无效。")
|
||||
expect(output).toContain('changed=26\nscroll=26\nchanged=32\nscroll=32\n')
|
||||
}, 30000)
|
||||
|
||||
test.skipIf(config.sandbox === 'none')('沙箱:看不到服务器文件、别的运行目录、环境变量和其他进程,工程目录只读', async () => {
|
||||
process.env.SIM_TEST_SECRET = '不该被看到'
|
||||
const { runner } = await runMain(
|
||||
|
||||
+27
-5
@@ -64,6 +64,23 @@ export interface BoardState {
|
||||
ledText: string
|
||||
/** 网络摄像机:是否在传画面,云台位置(左右 0–6、上下 0–2,开始时在正中) */
|
||||
camera: { on: boolean; x: number; y: number }
|
||||
/** 虚拟时钟;null 表示真实时间 */
|
||||
clock: VirtualClock | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟时钟:从真实时刻 realAt(毫秒时间戳)起,墙上时间从 wall(本地时间 yyyy-MM-ddTHH:mm:ss)开始按 speed 倍速走。
|
||||
* 学生程序的 DateTime.Now 跟着它(教材项目 7 按时刻切换情景模式)。
|
||||
*/
|
||||
export interface VirtualClock {
|
||||
wall: string
|
||||
realAt: number
|
||||
speed: number
|
||||
}
|
||||
|
||||
/** 虚拟时钟现在的墙上时间(yyyy-MM-ddTHH:mm:ss);按 UTC 做加减,只用来算时刻,不涉及时区 */
|
||||
export function clockWall(clock: VirtualClock, now = Date.now()) {
|
||||
return new Date(Date.parse(`${clock.wall}Z`) + (now - clock.realAt) * clock.speed).toISOString().slice(0, 19)
|
||||
}
|
||||
|
||||
export type AnalogKey = keyof BoardState['analog']
|
||||
@@ -94,8 +111,10 @@ export type RunnerMessage =
|
||||
| { type: 'controlAdded'; parentId: string; index: number; node: ControlNode }
|
||||
| { type: 'controlRemoved'; id: string }
|
||||
| { type: 'messageBox'; requestId: number; text: string; caption: string; buttons: string; icon: string }
|
||||
/** 图片数据(base64),同一张图只发一次;控件属性和 imageSaved 里用 key 引用 */
|
||||
| { type: 'image'; key: string; mime: string; data: string }
|
||||
/** 图片、声音数据(base64),同一份数据只发一次;控件属性、imageSaved、media 里用 key 引用 */
|
||||
| { type: 'blob'; key: string; mime: string; data: string }
|
||||
/** 媒体播放器(教材项目 7 的广播):开始 / 停止播放;name 是文件名,id 是播放器控件 */
|
||||
| { type: 'media'; id: string; action: 'play' | 'stop'; key?: string; name?: string }
|
||||
/** 学生程序用 Image.Save 保存了图片;path 相对程序所在目录,如 Camera/20241022_10 30 00.jpg */
|
||||
| { type: 'imageSaved'; path: string; image: string }
|
||||
/** 学生程序操作了实训台输出:led/text、adam4150/do、zigbee/relay、camera/on、camera/pan,data 见 DeviceCtrlHelper.cs、IPCameraHelper.cs */
|
||||
@@ -120,6 +139,8 @@ export type UserAction =
|
||||
| { action: 'cellEdit'; row: number; column: number; value: string }
|
||||
/** DateTimePicker 选了日期或时间:value 是本地时间 yyyy-MM-ddTHH:mm:ss */
|
||||
| { action: 'pick'; value: string }
|
||||
/** TrackBar 拖动滑块、点轨道或按方向键:value 是新值 */
|
||||
| { action: 'scroll'; value: string }
|
||||
| { action: 'mouseDown'; button: 'left' | 'right'; x: number; y: number }
|
||||
| { action: 'mouseUp'; button: 'left' | 'right'; x: number; y: number }
|
||||
| { action: 'close' }
|
||||
@@ -128,15 +149,16 @@ export type UserAction =
|
||||
export type RunnerInput =
|
||||
| ({ type: 'action'; id: string } & UserAction)
|
||||
| { type: 'reply'; requestId: number; result: unknown }
|
||||
/** 实训台传感器读数,按通道号排列(见 ANALOG_CHANNELS、DIGITAL_CHANNELS) */
|
||||
| { type: 'board'; analog: number[]; digital: boolean[] }
|
||||
/** 实训台传感器读数,按通道号排列(见 ANALOG_CHANNELS、DIGITAL_CHANNELS),以及虚拟时钟(不带时表示不变) */
|
||||
| { type: 'board'; analog: number[]; digital: boolean[]; clock?: VirtualClock | null }
|
||||
| { type: 'stop' }
|
||||
|
||||
/** 浏览器发给服务器的消息 */
|
||||
export type ClientMessage =
|
||||
| { type: 'run'; files: SourceFile[] }
|
||||
/** 调节实训台环境:拖动滑块、触发传感器 */
|
||||
| { type: 'boardInput'; analog?: Partial<BoardState['analog']>; digital?: Partial<BoardState['digital']> }
|
||||
/** clock:拨到某个时刻(time:yyyy-MM-ddTHH:mm:ss)、改倍速,或 null 恢复真实时间 */
|
||||
| { type: 'boardInput'; analog?: Partial<BoardState['analog']>; digital?: Partial<BoardState['digital']>; clock?: { time?: string; speed?: number } | null }
|
||||
/** 实训台复位:关掉所有灯和继电器、清空 LED 屏,传感器恢复默认 */
|
||||
| { type: 'boardReset' }
|
||||
| Exclude<RunnerInput, { type: 'board' }>
|
||||
|
||||
@@ -31,6 +31,8 @@ const props = defineProps<{
|
||||
codeText: string
|
||||
canUndo: boolean
|
||||
canRedo: boolean
|
||||
/** 项目资源(图片属性的选项) */
|
||||
resources?: { names: string[]; prefix: string }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -42,6 +44,8 @@ const emit = defineEmits<{
|
||||
openDesignerFile: []
|
||||
undo: []
|
||||
redo: []
|
||||
/** 属性窗口里要添加项目资源 */
|
||||
addResource: []
|
||||
}>()
|
||||
|
||||
/** Ctrl+Z 撤销,Ctrl+Y / Ctrl+Shift+Z 重做;在属性窗口的输入框里打字时不拦截(输入框有自己的撤销) */
|
||||
@@ -492,11 +496,13 @@ const noop = () => {}
|
||||
class="properties"
|
||||
:doc="doc"
|
||||
:selected="selected"
|
||||
:resources="resources"
|
||||
@set-prop="onSetProp"
|
||||
@set-items="onSetItems"
|
||||
@rename="onRename"
|
||||
@open-event="openEvent"
|
||||
@clear-event="clearEvent"
|
||||
@add-resource="emit('addResource')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,12 +4,15 @@ import { computed, ref, watch } from 'vue'
|
||||
import { type ControlDef, FORM_DEF, type PropDef, controlDef } from './catalog'
|
||||
import type { DesignerDocument } from './document'
|
||||
import { checkName, getEventHandler, getItems, getPropText, typeOf } from './edit'
|
||||
import { resourceNameOf } from './resources'
|
||||
import { NAMED_COLORS, hexToRgb, parseValue, rgb, type Value } from './values'
|
||||
|
||||
const props = defineProps<{
|
||||
doc: DesignerDocument
|
||||
/** '' 表示窗体本身 */
|
||||
selected: string
|
||||
/** 项目资源(图片属性从这里选)和引用资源的前缀 global::命名空间.Properties.Resources. */
|
||||
resources?: { names: string[]; prefix: string }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -19,6 +22,8 @@ const emit = defineEmits<{
|
||||
/** 双击事件或点"生成":绑定(必要时生成)事件处理方法并跳到代码 */
|
||||
openEvent: [target: string, event: string, handler?: string]
|
||||
clearEvent: [target: string, event: string]
|
||||
/** 图片属性里选了"添加资源…":打开项目资源对话框 */
|
||||
addResource: []
|
||||
}>()
|
||||
|
||||
const tab = ref<'props' | 'events'>('props')
|
||||
@@ -37,6 +42,22 @@ function set(prop: PropDef, value: Value) {
|
||||
emit('setProp', props.selected, prop.name, isDefault ? null : value)
|
||||
}
|
||||
|
||||
/** 图片属性:项目资源里的名字,没有设置时为空 */
|
||||
function imageOf(prop: PropDef) {
|
||||
const text = getPropText(props.doc, props.selected, prop.name)
|
||||
return text === undefined ? '' : resourceNameOf(text) ?? text
|
||||
}
|
||||
|
||||
function setImage(prop: PropDef, choice: string, select: HTMLSelectElement) {
|
||||
if (choice === '+') {
|
||||
select.value = imageOf(prop)
|
||||
emit('addResource')
|
||||
return
|
||||
}
|
||||
if (choice === '') reset(prop)
|
||||
else emit('setProp', props.selected, prop.name, { kind: 'raw', text: `${props.resources?.prefix ?? ''}${choice}` })
|
||||
}
|
||||
|
||||
function reset(prop: PropDef) {
|
||||
emit('setProp', props.selected, prop.name, null)
|
||||
}
|
||||
@@ -152,7 +173,7 @@ function renameHandler(event: string, value: string) {
|
||||
<div v-for="prop in def.props" :key="prop.name" class="row">
|
||||
<label :title="`${prop.name}:${prop.description}`">{{ prop.label }}</label>
|
||||
<div class="editor">
|
||||
<code v-if="raw(prop) !== undefined" class="raw" :title="raw(prop)">{{ raw(prop) }}</code>
|
||||
<code v-if="prop.type !== 'image' && raw(prop) !== undefined" class="raw" :title="raw(prop)">{{ raw(prop) }}</code>
|
||||
|
||||
<input v-else-if="prop.type === 'string'" :value="str(prop)" @change="set(prop, { kind: 'string', value: ($event.target as HTMLInputElement).value })" />
|
||||
|
||||
@@ -213,6 +234,14 @@ function renameHandler(event: string, value: string) {
|
||||
<textarea v-else-if="prop.type === 'items'" rows="4" :value="itemsText" placeholder="每行一项"
|
||||
@change="setItemsFromText(($event.target as HTMLTextAreaElement).value)" />
|
||||
|
||||
<select v-else-if="prop.type === 'image'" :value="imageOf(prop)"
|
||||
@change="setImage(prop, ($event.target as HTMLSelectElement).value, $event.target as HTMLSelectElement)">
|
||||
<option value="">(无)</option>
|
||||
<option v-for="name in resources?.names ?? []" :key="name" :value="name">Resources.{{ name }}</option>
|
||||
<option v-if="imageOf(prop) && !resources?.names.includes(imageOf(prop))" :value="imageOf(prop)">{{ imageOf(prop) }}(不在项目资源里)</option>
|
||||
<option value="+">添加资源…</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
// 项目资源(相当于 VS 的"项目属性 → 资源 → 添加资源"):从平台素材库里勾选图片和声音,
|
||||
// 代码里用 Properties.Resources.名字,设计器里图片框的"图片"属性也可以选它们。
|
||||
import { NButton, NCheckbox, NModal } from 'naive-ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { gallery, loadGallery } from './resources'
|
||||
|
||||
const props = defineProps<{ show: boolean; selected: string[] }>()
|
||||
const emit = defineEmits<{ 'update:show': [show: boolean]; save: [names: string[]] }>()
|
||||
|
||||
const chosen = ref(new Set<string>())
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (!show) return
|
||||
void loadGallery()
|
||||
chosen.value = new Set(props.selected)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function toggle(name: string, on: boolean) {
|
||||
const next = new Set(chosen.value)
|
||||
if (on) next.add(name)
|
||||
else next.delete(name)
|
||||
chosen.value = next
|
||||
}
|
||||
|
||||
let audio: HTMLAudioElement | undefined
|
||||
function listen(file: string) {
|
||||
audio?.pause()
|
||||
audio = new Audio(`/resources/${file}`)
|
||||
void audio.play()
|
||||
}
|
||||
|
||||
function save() {
|
||||
// 按素材库的顺序保存,生成的代码稳定
|
||||
emit('save', gallery.value.map((g) => g.name).filter((name) => chosen.value.has(name)))
|
||||
emit('update:show', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="show" preset="card" title="项目资源" style="width: 560px; max-width: calc(100vw - 32px)" @update:show="emit('update:show', $event)">
|
||||
<p class="note">勾选要加入项目的图片和声音(平台素材库,名字和教材一致)。代码里写 <code>Properties.Resources.名字</code>,图片框的"图片"属性也可以直接选。</p>
|
||||
<div class="items">
|
||||
<label v-for="item in gallery" :key="item.name" class="item" :class="{ on: chosen.has(item.name) }">
|
||||
<NCheckbox :checked="chosen.has(item.name)" @update:checked="toggle(item.name, $event)" />
|
||||
<img v-if="item.kind === 'image'" :src="`/resources/${item.file}`" alt="" class="thumb" />
|
||||
<NButton v-else size="tiny" class="audio" @click.prevent="listen(item.file)">▶ 试听</NButton>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
</label>
|
||||
<div v-if="gallery.length === 0" class="note">正在加载素材库…</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="buttons">
|
||||
<NButton type="primary" @click="save">确定</NButton>
|
||||
<NButton @click="emit('update:show', false)">取消</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.note {
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 64px 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item.on {
|
||||
border-color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.item > :first-child {
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
.thumb,
|
||||
.audio {
|
||||
grid-row: span 2;
|
||||
max-width: 64px;
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.audio {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
// 只列出仿真运行时已经实现的控件和属性;默认值与 WinForms 一致(等于默认值的属性不写进 Designer.cs)。
|
||||
import type { Value } from './values'
|
||||
|
||||
export type PropType = 'string' | 'multiline' | 'bool' | 'int' | 'char' | 'point' | 'size' | 'color' | 'font' | 'enum' | 'items'
|
||||
export type PropType = 'string' | 'multiline' | 'bool' | 'int' | 'char' | 'point' | 'size' | 'color' | 'font' | 'enum' | 'items' | 'image'
|
||||
|
||||
export interface PropDef {
|
||||
/** WinForms 的属性名,写进 Designer.cs,代码里也用它 */
|
||||
@@ -187,6 +187,7 @@ export const CONTROLS: ControlDef[] = [
|
||||
defaultEvent: 'Click',
|
||||
props: [
|
||||
...COMMON_PROPS.filter((p) => p.name !== 'Text' && p.name !== 'ForeColor' && p.name !== 'Font'),
|
||||
{ name: 'Image', label: '图片', description: '显示的图片:从项目资源里选(Properties.Resources)', type: 'image', category: '外观' },
|
||||
{
|
||||
name: 'SizeMode', label: '显示方式', description: '图片在图片框里怎样显示', type: 'enum', enumType: `${FORMS}.PictureBoxSizeMode`,
|
||||
options: ['Normal', 'StretchImage', 'AutoSize', 'CenterImage', 'Zoom'],
|
||||
@@ -254,6 +255,38 @@ export const CONTROLS: ControlDef[] = [
|
||||
events: [...COMMON_EVENTS.filter((e) => e.name !== 'TextChanged'), { name: 'ValueChanged', label: '值改变', ...EVENT }],
|
||||
initial: () => [['Size', size(200, 21)]],
|
||||
},
|
||||
{
|
||||
type: `${FORMS}.TrackBar`,
|
||||
label: '滑动条',
|
||||
group: '常用控件',
|
||||
namePrefix: 'trackBar',
|
||||
defaultEvent: 'Scroll',
|
||||
props: [
|
||||
...COMMON_PROPS.filter((p) => p.name !== 'Text' && p.name !== 'ForeColor' && p.name !== 'Font'),
|
||||
{ name: 'Minimum', label: '最小值', description: '滑块在最小端时的值', type: 'int', default: int(0), category: '行为' },
|
||||
{ name: 'Maximum', label: '最大值', description: '滑块在最大端时的值', type: 'int', default: int(10), category: '行为' },
|
||||
{ name: 'Value', label: '值', description: '滑块现在的值', type: 'int', default: int(0), category: '行为' },
|
||||
{ name: 'TickFrequency', label: '刻度间隔', description: '每隔多少画一个刻度', type: 'int', default: int(1), category: '外观' },
|
||||
{ name: 'LargeChange', label: '大步长', description: '点击轨道时每次改变多少', type: 'int', default: int(5), category: '行为' },
|
||||
{ name: 'SmallChange', label: '小步长', description: '按方向键时每次改变多少', type: 'int', default: int(1), category: '行为' },
|
||||
{
|
||||
name: 'TickStyle', label: '刻度位置', description: '刻度画在哪一边', type: 'enum', enumType: `${FORMS}.TickStyle`,
|
||||
options: ['None', 'TopLeft', 'BottomRight', 'Both'], optionLabels: { None: '不画刻度', TopLeft: '上方(左边)', BottomRight: '下方(右边)', Both: '两边' },
|
||||
default: enumValue(`${FORMS}.TickStyle`, 'BottomRight'), category: '外观',
|
||||
},
|
||||
{
|
||||
name: 'Orientation', label: '方向', description: '横向或竖向', type: 'enum', enumType: `${FORMS}.Orientation`,
|
||||
options: ['Horizontal', 'Vertical'], optionLabels: { Horizontal: '横向', Vertical: '竖向' },
|
||||
default: enumValue(`${FORMS}.Orientation`, 'Horizontal'), category: '外观',
|
||||
},
|
||||
],
|
||||
events: [
|
||||
...COMMON_EVENTS.filter((e) => e.name !== 'TextChanged'),
|
||||
{ name: 'Scroll', label: '拖动滑块', ...EVENT },
|
||||
{ name: 'ValueChanged', label: '值改变', ...EVENT },
|
||||
],
|
||||
initial: () => [['Size', size(104, 45)]],
|
||||
},
|
||||
{
|
||||
type: `${FORMS}.CheckBox`,
|
||||
label: '复选框',
|
||||
@@ -312,6 +345,16 @@ export const CONTROLS: ControlDef[] = [
|
||||
events: [{ name: 'Tick', label: '计时到', ...EVENT }],
|
||||
initial: () => [],
|
||||
},
|
||||
{
|
||||
type: 'AxWMPLib.AxWindowsMediaPlayer',
|
||||
label: '媒体播放器',
|
||||
group: '物联网设备',
|
||||
namePrefix: 'axWindowsMediaPlayer',
|
||||
defaultEvent: 'Enter',
|
||||
props: COMMON_PROPS.filter((p) => ['Location', 'Size', 'Enabled', 'Visible', 'TabIndex'].includes(p.name)),
|
||||
events: [{ name: 'Enter', label: '获得焦点', ...EVENT }],
|
||||
initial: () => [['Enabled', bool(true)], ['Size', size(245, 240)]],
|
||||
},
|
||||
{
|
||||
type: 'WinFormsDeviceControlLibrary.LED',
|
||||
label: 'LED 显示屏',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ControlNode } from '@iot/shared'
|
||||
import { controlDef } from './catalog'
|
||||
import type { DesignerDocument } from './document'
|
||||
import { childrenOf, findBlock, getItems, listControls } from './edit'
|
||||
import { resourceNameOf, resourceUrl } from './resources'
|
||||
import { parseValue, type Value } from './values'
|
||||
|
||||
/** 窗体在预览树里的 id;控件的 id 就是控件名 */
|
||||
@@ -11,6 +12,7 @@ export const FORM_ID = '__form'
|
||||
|
||||
/** 运行时控件树里的类型名:System.Windows.Forms.Button → Button,教材控件库保留完整类名 */
|
||||
function clientType(type: string) {
|
||||
if (type === 'AxWMPLib.AxWindowsMediaPlayer') return 'AxWindowsMediaPlayer'
|
||||
return type.startsWith('System.Windows.Forms.') ? type.slice('System.Windows.Forms.'.length) : type
|
||||
}
|
||||
|
||||
@@ -43,6 +45,12 @@ function propsOf(doc: DesignerDocument, target: string): Record<string, unknown>
|
||||
const props: Record<string, unknown> = {}
|
||||
for (const stmt of findBlock(doc, target)?.stmts ?? []) {
|
||||
if (stmt.kind !== 'prop' || stmt.path.includes('.')) continue
|
||||
// 项目资源里的图片:预览时直接显示素材文件
|
||||
const resource = resourceNameOf(stmt.value)
|
||||
if (resource) {
|
||||
props[stmt.path] = { key: '', url: resourceUrl(resource), width: 0, height: 0 }
|
||||
continue
|
||||
}
|
||||
const value = runtimeValue(parseValue(stmt.value))
|
||||
if (value !== undefined) props[stmt.path] = value
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// 项目资源(Properties.Resources):学生从平台素材库里选图片和声音,平台生成 Properties/Resources.Designer.cs。
|
||||
// 生成的类和 VS 的强类型资源类用法一样(Properties.Resources.switch_on 是 Bitmap,声音是 byte[]),
|
||||
// 取值时调用运行器的素材库。这个文件由平台生成,不在标签页里显示。
|
||||
import { ref } from 'vue'
|
||||
import type { SourceFile } from '@iot/shared'
|
||||
|
||||
export interface GalleryItem {
|
||||
/** 资源名,也是 Properties.Resources 的属性名,和教材一致(如 switch_on) */
|
||||
name: string
|
||||
file: string
|
||||
/** 中文说明 */
|
||||
label: string
|
||||
kind: 'image' | 'audio'
|
||||
}
|
||||
|
||||
export const RESOURCES_FILE = 'Properties/Resources.Designer.cs'
|
||||
|
||||
/** 素材清单(服务器上 /resources/index.json),第一次用到时加载 */
|
||||
export const gallery = ref<GalleryItem[]>([])
|
||||
let loading: Promise<void> | undefined
|
||||
|
||||
export function loadGallery() {
|
||||
loading ??= fetch('/resources/index.json')
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((items: GalleryItem[]) => {
|
||||
gallery.value = items
|
||||
})
|
||||
.catch(() => {
|
||||
loading = undefined
|
||||
})
|
||||
return loading
|
||||
}
|
||||
|
||||
/** 素材文件的网址(设计器预览、资源对话框里显示) */
|
||||
export function resourceUrl(name: string) {
|
||||
const item = gallery.value.find((g) => g.name === name)
|
||||
return `/resources/${item?.file ?? `${name}.png`}`
|
||||
}
|
||||
|
||||
/** 工程里已添加的资源名(从生成的 Resources.Designer.cs 里读出) */
|
||||
export function projectResources(files: SourceFile[]): string[] {
|
||||
const text = files.find((f) => f.path === RESOURCES_FILE)?.content ?? ''
|
||||
return [...text.matchAll(/internal static \S+ ([\p{L}\p{N}_]+) \{/gu)].map((m) => m[1])
|
||||
}
|
||||
|
||||
/** 设计器里引用资源的写法:global::命名空间.Properties.Resources.名字 */
|
||||
export const resourcePrefix = (namespace: string) => `global::${namespace}.Properties.Resources.`
|
||||
|
||||
/** 从属性值里认出资源名:global::X.Properties.Resources.switch_off → switch_off */
|
||||
export function resourceNameOf(text: string) {
|
||||
return /\.Properties\.Resources\.([\p{L}\p{N}_]+)$/u.exec(text.trim())?.[1]
|
||||
}
|
||||
|
||||
/** 生成 Properties/Resources.Designer.cs;没有资源时返回 null(删掉这个文件) */
|
||||
export function resourcesFile(namespace: string, names: string[]): SourceFile | null {
|
||||
const items = names.map((name) => gallery.value.find((g) => g.name === name)).filter((g): g is GalleryItem => g !== undefined)
|
||||
if (items.length === 0) return null
|
||||
const members = items.map((item) =>
|
||||
item.kind === 'image'
|
||||
? ` /// <summary>${item.label}</summary>\n internal static System.Drawing.Bitmap ${item.name} {\n get { return global::Sim.Host.ProjectResources.Image("${item.file}"); }\n }`
|
||||
: ` /// <summary>${item.label}</summary>\n internal static byte[] ${item.name} {\n get { return global::Sim.Host.ProjectResources.Bytes("${item.file}"); }\n }`,
|
||||
)
|
||||
const content = `//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 项目资源(平台素材库)。在"项目资源"里添加或移除,不要手动修改这个文件。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ${namespace}.Properties {
|
||||
|
||||
/// <summary>
|
||||
/// 项目资源:Properties.Resources.资源名
|
||||
/// </summary>
|
||||
internal class Resources {
|
||||
|
||||
${members.join('\n\n')}
|
||||
}
|
||||
}
|
||||
`
|
||||
return { path: RESOURCES_FILE, content }
|
||||
}
|
||||
@@ -31,7 +31,7 @@ test('新窗体的 Designer.cs 能被设计器打开,添加控件后格式正
|
||||
/** 读取 samples/ 下的示例工程 */
|
||||
async function sample(id: string) {
|
||||
const dir = `${import.meta.dir}/../../../samples/${id}`
|
||||
const paths = await Array.fromAsync(new Bun.Glob('*.cs').scan({ cwd: dir }))
|
||||
const paths = await Array.fromAsync(new Bun.Glob('**/*.cs').scan({ cwd: dir }))
|
||||
return Promise.all(paths.sort().map(async (path) => ({ path, content: await Bun.file(`${dir}/${path}`).text() })))
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ test('作业起点"界面已做好":多个窗体都保留,其他类(Public
|
||||
expect(files.find((f) => f.path === 'AddUser.cs')!.content).toContain('public partial class AddUser : Form')
|
||||
})
|
||||
|
||||
test('作业起点"界面已做好":图片引用了项目资源时带上资源类,资源类不当作窗体', async () => {
|
||||
const files = assignmentTemplate(await sample('p7-t1-智慧教室'), 'ui')
|
||||
expect(files.map((f) => f.path).sort()).toEqual(['Program.cs', 'Properties/Resources.Designer.cs', '教室情景模式.Designer.cs', '教室情景模式.cs'])
|
||||
expect(files.find((f) => f.path === '教室情景模式.Designer.cs')!.content).toContain('global::教室情景模式.Properties.Resources.switch_off')
|
||||
})
|
||||
|
||||
test('作业起点"空白工程":Program.cs 启动空的 Form1', async () => {
|
||||
const files = assignmentTemplate(await sample('p2-t2-模拟量采集'), 'blank')
|
||||
expect(files.map((f) => f.path).sort()).toEqual(['Form1.Designer.cs', 'Form1.cs', 'Program.cs'])
|
||||
|
||||
@@ -123,7 +123,10 @@ export function assignmentTemplate(files: SourceFile[], mode: StartMode): Source
|
||||
return [...(programText ? [{ path: 'Program.cs', content: programText }] : []), ...newFormFiles(namespace, 'Form1')]
|
||||
}
|
||||
const result: SourceFile[] = program ? [{ ...program }] : []
|
||||
for (const designer of files.filter((f) => f.path.endsWith('.Designer.cs'))) {
|
||||
// 界面里的图片引用了项目资源,资源类一起带上(空白工程由学生自己按书上的步骤添加)
|
||||
const resources = files.find((f) => f.path.startsWith('Properties/') && f.path.endsWith('.Designer.cs'))
|
||||
if (resources) result.push({ ...resources })
|
||||
for (const designer of files.filter((f) => isFormDesigner(f.path))) {
|
||||
const doc = parseDesigner(designer.content)
|
||||
for (const block of doc.blocks) block.stmts = block.stmts.filter((s) => s.kind !== 'event')
|
||||
const [code] = newFormFiles(namespace, doc.className)
|
||||
@@ -131,3 +134,6 @@ export function assignmentTemplate(files: SourceFile[], mode: StartMode): Source
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** 窗体的 Designer.cs(Properties/ 下平台生成的 Resources.Designer.cs 不是窗体) */
|
||||
export const isFormDesigner = (path: string) => path.endsWith('.Designer.cs') && !path.startsWith('Properties/')
|
||||
@@ -1,5 +1,6 @@
|
||||
// 提交作业时给每个窗体截一张图:用设计器的预览树和运行时同一套 FormWindow 在屏幕外渲染,再转成 PNG。
|
||||
// 截图随提交保存,老师在评阅页直接看界面,AI 评阅也据此评"窗体界面"。截图失败不影响提交。
|
||||
import { isFormDesigner } from '../designer/templates'
|
||||
import { createApp, h, nextTick } from 'vue'
|
||||
import { domToPng } from 'modern-screenshot'
|
||||
import type { Screenshot, SourceFile } from '@iot/shared'
|
||||
@@ -12,7 +13,7 @@ const MAX_FORMS = 6
|
||||
const MAX_PNG_LENGTH = 700_000
|
||||
|
||||
export async function captureForms(files: SourceFile[]): Promise<Screenshot[]> {
|
||||
const designers = files.filter((f) => f.path.endsWith('.Designer.cs')).slice(0, MAX_FORMS)
|
||||
const designers = files.filter((f) => isFormDesigner(f.path)).slice(0, MAX_FORMS)
|
||||
if (designers.length === 0) return []
|
||||
const host = document.createElement('div')
|
||||
host.style.cssText = 'position: fixed; left: -10000px; top: 0; width: 2000px; height: 2000px; pointer-events: none;'
|
||||
|
||||
@@ -8,7 +8,9 @@ import { NButton, NInput, NModal, NPopconfirm, NPopover, NSelect, NTab, NTabs, N
|
||||
import type { Assignment, Review, SourceFile, Submission } from '@iot/shared'
|
||||
import FormDesigner from '../designer/FormDesigner.vue'
|
||||
import { createDesignerHistory } from '../designer/history'
|
||||
import { formNameTaken, newFormFiles, nextFormName, projectNamespace, startupForm } from '../designer/templates'
|
||||
import { gallery, loadGallery, projectResources, resourcePrefix, resourcesFile, RESOURCES_FILE } from '../designer/resources'
|
||||
import ResourcesDialog from '../designer/ResourcesDialog.vue'
|
||||
import { formNameTaken, isFormDesigner, newFormFiles, nextFormName, projectNamespace, startupForm } from '../designer/templates'
|
||||
import CodeEditor from '../editor/CodeEditor.vue'
|
||||
import { api } from '../api'
|
||||
import { currentUser, logout } from '../auth'
|
||||
@@ -34,7 +36,7 @@ const DESIGN = '设计:'
|
||||
const forms = computed(() => {
|
||||
const startup = startupForm(files.value)
|
||||
return files.value
|
||||
.filter((f) => f.path.endsWith('.Designer.cs'))
|
||||
.filter((f) => isFormDesigner(f.path))
|
||||
.map((f) => ({ tab: DESIGN + f.path, designer: f.path, code: f.path.replace(/\.Designer\.cs$/, '.cs') }))
|
||||
// 启动窗体排在最前
|
||||
.sort((a, b) => Number(b.code === `${startup}.cs`) - Number(a.code === `${startup}.cs`))
|
||||
@@ -43,6 +45,7 @@ const forms = computed(() => {
|
||||
* 标签页按窗体分组:每个窗体依次是 [设计]、代码;其他文件(Program.cs 等)放最后。
|
||||
* Designer.cs 由设计器生成,平时不显示(和 VS 里藏在窗体文件下面一样),
|
||||
* 在设计器里点"查看生成的代码"或点到其中的编译错误时才临时出现,切走后收起。
|
||||
* 项目资源生成的 Properties/Resources.Designer.cs 也一样,在"项目资源"里管理。
|
||||
*/
|
||||
const tabs = computed(() => {
|
||||
const grouped = new Set(forms.value.flatMap((f) => [f.code, f.designer]))
|
||||
@@ -52,7 +55,9 @@ const tabs = computed(() => {
|
||||
{ name: f.code, label: f.code },
|
||||
...(activeTab.value === f.designer ? [{ name: f.designer, label: f.designer }] : []),
|
||||
]),
|
||||
...files.value.filter((f) => !grouped.has(f.path)).map((f) => ({ name: f.path, label: f.path })),
|
||||
...files.value
|
||||
.filter((f) => !grouped.has(f.path) && (f.path !== RESOURCES_FILE || activeTab.value === RESOURCES_FILE))
|
||||
.map((f) => ({ name: f.path, label: f.path })),
|
||||
]
|
||||
})
|
||||
const activeForm = computed(() => forms.value.find((f) => f.tab === activeTab.value))
|
||||
@@ -270,6 +275,24 @@ function filesChanged() {
|
||||
if (workKey.value) scheduleSave()
|
||||
}
|
||||
|
||||
// ---------- 项目资源 ----------
|
||||
void loadGallery()
|
||||
const resourcesOpen = ref(false)
|
||||
const resourceNames = computed(() => projectResources(files.value))
|
||||
/** 设计器里图片属性的选项:项目资源里的图片(声音不能当图片用) */
|
||||
const designerResources = computed(() => ({
|
||||
names: resourceNames.value.filter((name) => gallery.value.find((g) => g.name === name)?.kind !== 'audio'),
|
||||
prefix: resourcePrefix(projectNamespace(files.value)),
|
||||
}))
|
||||
|
||||
/** 保存勾选的项目资源:重新生成 Properties/Resources.Designer.cs(一个都没有时删掉) */
|
||||
function saveResources(names: string[]) {
|
||||
const file = resourcesFile(projectNamespace(files.value), names)
|
||||
const rest = files.value.filter((f) => f.path !== RESOURCES_FILE)
|
||||
files.value = file ? [...rest, file] : rest
|
||||
filesChanged()
|
||||
}
|
||||
|
||||
// ---------- 新建、删除窗体 ----------
|
||||
const newFormOpen = ref(false)
|
||||
const newFormName = ref('')
|
||||
@@ -444,6 +467,9 @@ function run() {
|
||||
<NButton size="small" type="primary" :disabled="!!newFormError" @click="createForm">创建</NButton>
|
||||
</div>
|
||||
</NPopover>
|
||||
<NButton size="small" quaternary title="相当于 VS 的“项目属性 → 资源”:添加图片和声音,代码里用 Properties.Resources.名字" @click="resourcesOpen = true">
|
||||
项目资源{{ resourceNames.length ? `(${resourceNames.length})` : '' }}
|
||||
</NButton>
|
||||
<NPopconfirm v-if="deletableForm" positive-text="删除" negative-text="取消" @positive-click="deleteForm">
|
||||
<template #trigger>
|
||||
<NButton size="small" quaternary>删除窗体</NButton>
|
||||
@@ -463,7 +489,10 @@ function run() {
|
||||
@redo="designerHistory.redo(activeForm.designer, activeForm.code)"
|
||||
@open-code="openCode"
|
||||
@open-designer-file="activeTab = activeForm.designer"
|
||||
:resources="designerResources"
|
||||
@add-resource="resourcesOpen = true"
|
||||
/>
|
||||
<ResourcesDialog v-model:show="resourcesOpen" :selected="resourceNames" @save="saveResources" />
|
||||
<CodeEditor
|
||||
v-show="!activeForm"
|
||||
ref="editorRef"
|
||||
|
||||
@@ -37,7 +37,7 @@ const viewing = ref<{ path: string; url: string } | null>(null)
|
||||
/>
|
||||
<div v-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
|
||||
</div>
|
||||
<TrainingBoard :board="state.board" @set="session.setBoard" @reset="session.resetBoard" />
|
||||
<TrainingBoard :board="state.board" :playing="state.playing" @set="session.setBoard" @reset="session.resetBoard" />
|
||||
<div v-if="state.savedImages.length" class="photos">
|
||||
<div class="photos-title">程序保存的图片({{ state.savedImages.length }})<span>只在这次运行中保留</span></div>
|
||||
<div class="photos-list">
|
||||
|
||||
+36
-1
@@ -42,6 +42,8 @@ export function createRunSession() {
|
||||
board: null as BoardState | null,
|
||||
/** 本次运行保存的图片,显示在运行区的照片列表里;下次运行时清空 */
|
||||
savedImages: [] as SavedImage[],
|
||||
/** 正在播放的声音(媒体播放器控件,实训台上的广播喇叭随之亮起) */
|
||||
playing: null as { name: string } | null,
|
||||
metrics: { compileMs: 0, startMs: 0, lastRoundTripMs: 0 },
|
||||
})
|
||||
/** id → 控件节点,patch 按 id 修改节点属性 */
|
||||
@@ -52,6 +54,34 @@ export function createRunSession() {
|
||||
/** 正在建立或已建立的连接;同时多处调用 connect() 时共用同一个 */
|
||||
let connecting: Promise<void> | undefined
|
||||
let actionSentAt = 0
|
||||
/** 每个媒体播放器控件一个 <audio> */
|
||||
const players = new Map<string, HTMLAudioElement>()
|
||||
|
||||
function stopAudio() {
|
||||
for (const audio of players.values()) audio.pause()
|
||||
players.clear()
|
||||
state.playing = null
|
||||
}
|
||||
|
||||
function media(message: Extract<ServerMessage, { type: 'media' }>) {
|
||||
players.get(message.id)?.pause()
|
||||
players.delete(message.id)
|
||||
if (message.action === 'stop' || !message.key) {
|
||||
if (players.size === 0) state.playing = null
|
||||
return
|
||||
}
|
||||
const url = images.get(message.key)
|
||||
if (!url) return
|
||||
const audio = new Audio(url)
|
||||
players.set(message.id, audio)
|
||||
state.playing = { name: message.name ?? '' }
|
||||
audio.onended = () => {
|
||||
players.delete(message.id)
|
||||
if (players.size === 0) state.playing = null
|
||||
}
|
||||
// 浏览器可能因为页面还没被点过而拒绝自动播放:喇叭照样亮一会儿
|
||||
audio.play().catch(() => setTimeout(() => audio.onended?.(new Event('ended')), 2000))
|
||||
}
|
||||
|
||||
function connect() {
|
||||
connecting ??= validAccessToken().then(
|
||||
@@ -87,6 +117,7 @@ export function createRunSession() {
|
||||
state.forms = []
|
||||
state.messageBoxes = []
|
||||
state.savedImages = []
|
||||
stopAudio()
|
||||
nodes.clear()
|
||||
for (const url of images.values()) URL.revokeObjectURL(url)
|
||||
images.clear()
|
||||
@@ -186,11 +217,14 @@ export function createRunSession() {
|
||||
case 'messageBox':
|
||||
state.messageBoxes.push(message)
|
||||
break
|
||||
case 'image': {
|
||||
case 'blob': {
|
||||
const bytes = Uint8Array.from(atob(message.data), (c) => c.charCodeAt(0))
|
||||
images.set(message.key, URL.createObjectURL(new Blob([bytes], { type: message.mime })))
|
||||
break
|
||||
}
|
||||
case 'media':
|
||||
media(message)
|
||||
break
|
||||
case 'imageSaved':
|
||||
state.savedImages.push({ path: message.path, image: message.image })
|
||||
break
|
||||
@@ -207,6 +241,7 @@ export function createRunSession() {
|
||||
state.errors.push(message.message)
|
||||
break
|
||||
case 'exited':
|
||||
stopAudio()
|
||||
state.forms = []
|
||||
state.messageBoxes = []
|
||||
break
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
// 虚拟实训台:按教材实训台布局图(顶棚 → LED 屏 → 传感器 → 采集模块)排列。
|
||||
// 传感器卡片上的滑块和开关调节环境(经服务器推给学生程序),灯和继电器的状态由学生程序控制。
|
||||
import { computed, ref } from 'vue'
|
||||
import { ANALOG_INFO, type AnalogKey, type BoardState, type ClientMessage, type DigitalKey } from '@iot/shared'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { ANALOG_INFO, clockWall, type AnalogKey, type BoardState, type ClientMessage, type DigitalKey } from '@iot/shared'
|
||||
|
||||
type BoardInput = Omit<Extract<ClientMessage, { type: 'boardInput' }>, 'type'>
|
||||
|
||||
const props = defineProps<{ board: BoardState | null }>()
|
||||
/** playing:学生程序正在播放的声音(媒体播放器),广播喇叭亮起 */
|
||||
const props = defineProps<{ board: BoardState | null; playing?: { name: string } | null }>()
|
||||
const emit = defineEmits<{ set: [input: BoardInput]; reset: [] }>()
|
||||
|
||||
const MORE_SENSORS: AnalogKey[] = ['airPressure', 'airQuality', 'co2', 'wind']
|
||||
@@ -78,6 +79,38 @@ const streetLampOn = computed(() => props.board?.zigbee[1] === true)
|
||||
const alarmOn = computed(() => relays.value[0] === true)
|
||||
const ledText = computed(() => props.board?.ledText ?? '')
|
||||
const camera = computed(() => props.board?.camera ?? { on: false, x: 3, y: 1 })
|
||||
// ---------- 虚拟时钟:显示学生程序看到的时间,可以拨时间、调倍速 ----------
|
||||
const SPEEDS = [1, 10, 60]
|
||||
const tick = ref(Date.now())
|
||||
const ticker = setInterval(() => (tick.value = Date.now()), 250)
|
||||
onBeforeUnmount(() => clearInterval(ticker))
|
||||
|
||||
const clock = computed(() => props.board?.clock ?? null)
|
||||
/** 现在的墙上时间 yyyy-MM-ddTHH:mm:ss:拨过时间时按虚拟时钟算,否则是本机时间 */
|
||||
const wall = computed(() => {
|
||||
if (clock.value) return clockWall(clock.value, tick.value)
|
||||
const d = new Date(tick.value)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
})
|
||||
const clockDate = ref('')
|
||||
const clockTime = ref('')
|
||||
|
||||
function openClockEditor() {
|
||||
clockDate.value = wall.value.slice(0, 10)
|
||||
clockTime.value = wall.value.slice(11, 19)
|
||||
}
|
||||
|
||||
function setClock() {
|
||||
if (!clockDate.value || !clockTime.value) return
|
||||
const time = clockTime.value.length === 5 ? `${clockTime.value}:00` : clockTime.value
|
||||
emit('set', { clock: { time: `${clockDate.value}T${time}` } })
|
||||
}
|
||||
|
||||
function setSpeed(speed: number) {
|
||||
emit('set', { clock: { speed } })
|
||||
}
|
||||
|
||||
const cameraTitle = computed(() =>
|
||||
camera.value.on ? `网络摄像头:正在传送画面(云台左右 ${camera.value.x + 1}/7,上下 ${camera.value.y + 1}/3)` : '网络摄像头(项目 4 使用):未开启',
|
||||
)
|
||||
@@ -106,6 +139,7 @@ const cameraTitle = computed(() =>
|
||||
<i />{{ input.label }}
|
||||
</button>
|
||||
<span v-if="ledText" class="summary-led" :title="ledText">LED:{{ ledText }}</span>
|
||||
<span v-if="clock" class="summary-clock" title="虚拟时钟:学生程序里的 DateTime.Now">🕒 {{ wall.slice(11) }}({{ clock.speed }}×)</span>
|
||||
<button class="link expand" @click="setCollapsed(false)">展开 ▾</button>
|
||||
</div>
|
||||
|
||||
@@ -190,6 +224,15 @@ const cameraTitle = computed(() =>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card" :title="playing ? `广播正在播放 ${playing.name}` : '广播(教材项目 7 用 Windows Media Player 播放铃声)'">
|
||||
<svg viewBox="0 0 24 24" width="30" height="30" class="icon speaker" :class="{ on: playing }">
|
||||
<path fill="currentColor" d="M4 9h4l5-4v14l-5-4H4z" />
|
||||
<path v-if="playing" d="M16 8.5a5 5 0 0 1 0 7M18.5 6a8.5 8.5 0 0 1 0 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
</svg>
|
||||
<div class="card-title">广播</div>
|
||||
<div class="card-note">{{ playing ? `播放 ${playing.name}` : '静音' }}</div>
|
||||
</div>
|
||||
|
||||
<button class="card toggle" :class="{ active: digital.smoke }" title="点击切换:有烟雾 / 正常" @click="toggle('smoke')">
|
||||
<div class="smoke" :class="{ on: digital.smoke }"><i /><i /><i /></div>
|
||||
<div class="card-title">烟雾传感器</div>
|
||||
@@ -254,6 +297,31 @@ const cameraTitle = computed(() =>
|
||||
<button class="reset" :disabled="!board" @click="emit('reset')">实训台复位</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 虚拟时钟:学生程序里的 DateTime.Now 跟着它走 -->
|
||||
<div class="clock-strip" :class="{ virtual: clock }">
|
||||
<span class="module-title">时钟</span>
|
||||
<b class="clock-time">{{ wall.slice(11) }}</b>
|
||||
<span class="clock-date">{{ wall.slice(0, 10) }}</span>
|
||||
<span class="clock-tag">{{ clock ? `虚拟时间 · ${clock.speed}×` : '真实时间' }}</span>
|
||||
<span class="spacer" />
|
||||
<input v-model="clockDate" type="date" :disabled="!board" @focus="!clockDate && openClockEditor()" />
|
||||
<input v-model="clockTime" type="time" step="1" :disabled="!board" @focus="!clockTime && openClockEditor()" />
|
||||
<button class="chip-button" :disabled="!board || !clockDate || !clockTime" title="把学生程序看到的时间拨到这个时刻" @click="setClock">拨到</button>
|
||||
<span class="speeds" title="倍速:60× 时真实的 1 秒是 1 分钟">
|
||||
<button
|
||||
v-for="speed in SPEEDS"
|
||||
:key="speed"
|
||||
class="chip-button"
|
||||
:class="{ active: (clock?.speed ?? 1) === speed && (clock !== null || speed === 1) }"
|
||||
:disabled="!board"
|
||||
@click="setSpeed(speed)"
|
||||
>
|
||||
{{ speed }}×
|
||||
</button>
|
||||
</span>
|
||||
<button v-if="clock" class="chip-button" :disabled="!board" @click="emit('set', { clock: null })">恢复真实时间</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -580,6 +648,91 @@ button {
|
||||
}
|
||||
|
||||
/* 模块 */
|
||||
.clock-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px 8px;
|
||||
padding: 6px 8px;
|
||||
background: #3a4556;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.clock-strip.virtual {
|
||||
box-shadow: inset 0 0 0 1px #f5a623;
|
||||
}
|
||||
|
||||
.clock-time {
|
||||
font-size: 16px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.clock-date {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.clock-tag {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.virtual .clock-tag {
|
||||
color: #f5a623;
|
||||
}
|
||||
|
||||
.clock-strip .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.clock-strip input {
|
||||
background: #2b3340;
|
||||
color: var(--text);
|
||||
border: 1px solid #556175;
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
font: inherit;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.chip-button {
|
||||
background: #2b3340;
|
||||
color: var(--text);
|
||||
border: 1px solid #556175;
|
||||
border-radius: 3px;
|
||||
padding: 1px 8px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chip-button.active {
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
background: #2f5f99;
|
||||
}
|
||||
|
||||
.chip-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.speeds {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.speaker {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.speaker.on {
|
||||
color: #4cd964;
|
||||
animation: blink 0.8s steps(2) infinite;
|
||||
}
|
||||
|
||||
.summary-clock {
|
||||
color: #f5a623;
|
||||
}
|
||||
|
||||
.modules {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1.5fr 1fr;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RUN_IMAGES, type ImageValue } from './images'
|
||||
import DataGridViewControl from './DataGridViewControl.vue'
|
||||
import DateTimePickerControl from './DateTimePickerControl.vue'
|
||||
import LedControl from './LedControl.vue'
|
||||
import TrackBarControl from './TrackBarControl.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
node: ControlNode
|
||||
@@ -55,9 +56,11 @@ function selectItem(index: number) {
|
||||
// PictureBox:按 SizeMode 摆放图片
|
||||
const runImages = inject(RUN_IMAGES, new Map<string, string>())
|
||||
const image = computed(() => p.value.Image as ImageValue | null | undefined)
|
||||
const imageUrl = computed(() => (image.value ? runImages.get(image.value.key) : undefined))
|
||||
const imageUrl = computed(() => (image.value ? image.value.url ?? runImages.get(image.value.key) : undefined))
|
||||
// 设计器预览里不知道素材的尺寸,原样大小显示时让浏览器按图片本身的大小
|
||||
const naturalSize = computed(() => (image.value?.width ? { width: `${image.value.width}px`, height: `${image.value.height}px` } : {}))
|
||||
const imageStyle = computed((): CSSProperties => {
|
||||
const natural = { width: `${image.value?.width ?? 0}px`, height: `${image.value?.height ?? 0}px` }
|
||||
const natural = naturalSize.value
|
||||
switch (p.value.SizeMode) {
|
||||
case 'StretchImage':
|
||||
return { width: '100%', height: '100%', objectFit: 'fill' }
|
||||
@@ -168,6 +171,14 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
|
||||
|
||||
<DateTimePickerControl v-else-if="node.type === 'DateTimePicker'" :node="node" :style="style" :enabled="enabled" :act="act" />
|
||||
|
||||
<TrackBarControl v-else-if="node.type === 'TrackBar'" :node="node" :style="style" :enabled="enabled" :act="act" />
|
||||
|
||||
<!-- Windows Media Player(教材里通常设为不可见,只用来播放声音) -->
|
||||
<div v-else-if="node.type === 'AxWindowsMediaPlayer'" class="wf-control wf-wmp" :style="style">
|
||||
<div class="wf-wmp-screen">Windows Media Player</div>
|
||||
<div class="wf-wmp-bar"><span>▶</span><span>■</span><span class="wf-wmp-progress" /></div>
|
||||
</div>
|
||||
|
||||
<!-- CheckBox / RadioButton:自绘勾选框,状态完全由运行器决定(点击只上报,不在浏览器里自行切换) -->
|
||||
<div
|
||||
v-else-if="node.type === 'CheckBox' || node.type === 'RadioButton'"
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
// 滑动条(仿 Windows 10 的 TrackBar):拖动滑块、点轨道(按 LargeChange 靠近)、方向键(按 SmallChange)改变值,
|
||||
// 新值发给运行器,由运行器决定最终的 Value(并触发 ValueChanged、Scroll)。竖向时最小值在下面,和 WinForms 一样。
|
||||
import { computed, ref, type CSSProperties } from 'vue'
|
||||
import type { ControlNode, UserAction } from '@iot/shared'
|
||||
|
||||
const props = defineProps<{
|
||||
node: ControlNode
|
||||
style: CSSProperties
|
||||
enabled: boolean
|
||||
act: (action: UserAction) => void
|
||||
}>()
|
||||
|
||||
/** 轨道两端留出的距离(滑块半宽 + 边距),和 WinForms 差不多 */
|
||||
const PAD = 13
|
||||
|
||||
const p = computed(() => props.node.props)
|
||||
const min = computed(() => Number(p.value.Minimum ?? 0))
|
||||
const max = computed(() => Number(p.value.Maximum ?? 10))
|
||||
const value = computed(() => Number(p.value.Value ?? 0))
|
||||
const vertical = computed(() => p.value.Orientation === 'Vertical')
|
||||
const tickStyle = computed(() => String(p.value.TickStyle ?? 'BottomRight'))
|
||||
const size = computed(() => p.value.Size as { width: number; height: number } | undefined)
|
||||
/** 轨道方向上的长度 */
|
||||
const length = computed(() => (vertical.value ? size.value?.height ?? 104 : size.value?.width ?? 104))
|
||||
|
||||
/** 值 → 沿轨道的位置(像素);竖向时最小值在下面 */
|
||||
function position(v: number) {
|
||||
const ratio = max.value === min.value ? 0 : (v - min.value) / (max.value - min.value)
|
||||
const along = PAD + ratio * (length.value - 2 * PAD)
|
||||
return vertical.value ? length.value - along : along
|
||||
}
|
||||
|
||||
const ticks = computed(() => {
|
||||
const frequency = Math.max(1, Number(p.value.TickFrequency ?? 1))
|
||||
const list: number[] = []
|
||||
for (let v = min.value; v <= max.value && list.length < 200; v += frequency) list.push(v)
|
||||
if (list.at(-1) !== max.value) list.push(max.value)
|
||||
return list
|
||||
})
|
||||
|
||||
const root = ref<HTMLDivElement>()
|
||||
let dragging = false
|
||||
|
||||
function valueAt(event: PointerEvent) {
|
||||
const rect = root.value!.getBoundingClientRect()
|
||||
const along = vertical.value ? rect.bottom - event.clientY : event.clientX - rect.left
|
||||
const ratio = Math.min(1, Math.max(0, (along - PAD) / (length.value - 2 * PAD)))
|
||||
return Math.round(min.value + ratio * (max.value - min.value))
|
||||
}
|
||||
|
||||
function send(next: number) {
|
||||
const clamped = Math.min(max.value, Math.max(min.value, next))
|
||||
if (clamped !== value.value) props.act({ action: 'scroll', value: String(clamped) })
|
||||
}
|
||||
|
||||
function down(event: PointerEvent) {
|
||||
if (!props.enabled) return
|
||||
root.value?.focus()
|
||||
const target = event.target as HTMLElement
|
||||
if (target.classList.contains('wf-trackbar-thumb')) {
|
||||
dragging = true
|
||||
root.value!.setPointerCapture(event.pointerId)
|
||||
return
|
||||
}
|
||||
// 点轨道:向点击的位置移动一个 LargeChange
|
||||
const large = Number(p.value.LargeChange ?? 5)
|
||||
send(value.value + (valueAt(event) > value.value ? large : -large))
|
||||
}
|
||||
|
||||
function move(event: PointerEvent) {
|
||||
if (dragging) send(valueAt(event))
|
||||
}
|
||||
|
||||
function up() {
|
||||
dragging = false
|
||||
}
|
||||
|
||||
function key(event: KeyboardEvent) {
|
||||
if (!props.enabled) return
|
||||
const small = Number(p.value.SmallChange ?? 1)
|
||||
const large = Number(p.value.LargeChange ?? 5)
|
||||
const delta: Record<string, number> = { ArrowRight: small, ArrowUp: small, ArrowLeft: -small, ArrowDown: -small, PageUp: large, PageDown: -large }
|
||||
if (event.key in delta) send(value.value + delta[event.key])
|
||||
else if (event.key === 'Home') send(min.value)
|
||||
else if (event.key === 'End') send(max.value)
|
||||
else return
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const thumbStyle = computed((): CSSProperties => (vertical.value ? { top: `${position(value.value) - 5}px` } : { left: `${position(value.value) - 5}px` }))
|
||||
/** 刻度:滑块下方(BottomRight)或上方(TopLeft),两端的刻度长一点 */
|
||||
const tickStyleOf = (v: number, side: 'a' | 'b'): CSSProperties => {
|
||||
const extent = v === min.value || v === max.value ? '5px' : '3px'
|
||||
const offset = side === 'a' ? '0px' : '28px'
|
||||
return vertical.value ? { top: `${position(v)}px`, left: offset, width: extent } : { left: `${position(v)}px`, top: offset, height: extent }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
class="wf-control wf-trackbar"
|
||||
:class="{ 'wf-trackbar-vertical': vertical, 'wf-disabled': !enabled }"
|
||||
:style="style"
|
||||
tabindex="0"
|
||||
@pointerdown="down"
|
||||
@pointermove="move"
|
||||
@pointerup="up"
|
||||
@keydown="key"
|
||||
>
|
||||
<div class="wf-trackbar-track" :style="vertical ? { top: `${PAD}px`, bottom: `${PAD}px` } : { left: `${PAD}px`, right: `${PAD}px` }" />
|
||||
<template v-if="tickStyle === 'TopLeft' || tickStyle === 'Both'">
|
||||
<span v-for="v in ticks" :key="`a${v}`" class="wf-trackbar-tick" :style="tickStyleOf(v, 'a')" />
|
||||
</template>
|
||||
<template v-if="tickStyle === 'BottomRight' || tickStyle === 'Both'">
|
||||
<span v-for="v in ticks" :key="`b${v}`" class="wf-trackbar-tick" :style="tickStyleOf(v, 'b')" />
|
||||
</template>
|
||||
<span class="wf-trackbar-thumb" :style="thumbStyle" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,4 +9,6 @@ export interface ImageValue {
|
||||
key: string
|
||||
width: number
|
||||
height: number
|
||||
/** 设计器预览里的项目资源:直接给出素材网址(运行时用 key) */
|
||||
url?: string
|
||||
}
|
||||
@@ -301,6 +301,96 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* TrackBar:灰色轨道、刻度、蓝色滑块(Windows 10 样式) */
|
||||
.wf-trackbar {
|
||||
outline: none;
|
||||
touch-action: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.wf-trackbar-track {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
height: 4px;
|
||||
background: #e7e7e7;
|
||||
border: 1px solid #d6d6d6;
|
||||
}
|
||||
|
||||
.wf-trackbar-vertical .wf-trackbar-track {
|
||||
top: auto;
|
||||
left: 12px;
|
||||
width: 4px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.wf-trackbar-tick {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
background: #c4c4c4;
|
||||
}
|
||||
|
||||
.wf-trackbar-vertical .wf-trackbar-tick {
|
||||
width: auto;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.wf-trackbar-thumb {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
width: 11px;
|
||||
height: 20px;
|
||||
background: #007ad9;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.wf-trackbar-vertical .wf-trackbar-thumb {
|
||||
top: auto;
|
||||
left: 4px;
|
||||
width: 20px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.wf-trackbar:focus-visible .wf-trackbar-thumb {
|
||||
outline: 1px dotted #000;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.wf-disabled .wf-trackbar-thumb {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.wf-wmp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #000;
|
||||
color: #9aa4b2;
|
||||
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wf-wmp-screen {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wf-wmp-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
background: linear-gradient(#3a3f47, #1c1f24);
|
||||
}
|
||||
|
||||
.wf-wmp-progress {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.wf-listbox {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3000',
|
||||
'/resources': 'http://localhost:3000',
|
||||
'/ws': { target: 'ws://localhost:3000', ws: true },
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user