项目 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:
xuyueandClaude Opus 5.5 committed 2026-10-23 14:11:05 +08:00
1 parent a9af26c2d6
commit eedf1aa2fb
66 files changed
+3538 -43

No files matched your search

+36 -1
View File
@@ -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