项目 4 任务 2 网络摄像机:PictureBox、Image / Bitmap、照片列表
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

- 摄像头:自绘东围墙场景(Camera/scene.svg),按云台位置切成 640×480 帧(build-frames.ts);
  Pan 每次转一格,红外对射被遮挡时画面出现翻墙的人;类名、委托、事件参数与教材 dll 一致,
  ImageRead 每 250 ms 在后台线程触发
- Image / Bitmap 保存真实图片数据:Save、FromFile、FromStream,文件夹不存在时报 GDI+ 一般性错误;
  图片每张只发给浏览器一次(按内容哈希缓存)
- 程序所在目录(AppDomain.CurrentDomain.BaseDirectory,编译检查单独放行)和当前目录指向可写的 bin/Debug,
  沙箱里在沙箱的临时目录
- PictureBox(SizeMode、BorderStyle);设计器工具箱加"图片框";运行区显示程序保存的图片,点开看大图;
  实训台摄像头图标显示开启状态
- 评分规则加图片框显示图片、摄像头开关与转向、保存图片三种检查;示例 p4-t2-网络摄像机
- 测试:摄像头端到端、图片读写与程序目录、AppDomain 编译检查、评分出错情况

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 20:58:09 +08:00
1 parent 4ed9450c02
commit 6b088240da
76 files changed
+1290 -14

No files matched your search

+31 -1
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
// 按控件类型渲染一个 WinForms 控件及其子控件。
// 用户操作只通过 act() 上报,由运行器按真实 WinForms 的顺序触发事件。
import { computed, ref } from 'vue'
import { computed, inject, ref, type CSSProperties } from 'vue'
import type { ControlNode, UserAction } from '@iot/shared'
import { boxCss, displayText } from './format'
import { RUN_IMAGES, type ImageValue } from './images'
import LedControl from './LedControl.vue'
const props = defineProps<{
@@ -49,6 +50,24 @@ function selectItem(index: number) {
act({ action: 'select', index })
}
// 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 imageStyle = computed((): CSSProperties => {
const natural = { width: `${image.value?.width ?? 0}px`, height: `${image.value?.height ?? 0}px` }
switch (p.value.SizeMode) {
case 'StretchImage':
return { width: '100%', height: '100%', objectFit: 'fill' }
case 'Zoom':
return { width: '100%', height: '100%', objectFit: 'contain' }
case 'CenterImage':
return { ...natural, left: '50%', top: '50%', transform: 'translate(-50%, -50%)' }
default:
return natural
}
})
// ListBox
const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined) ?? 12}px`)
</script>
@@ -132,6 +151,17 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
</div>
</div>
<!-- PictureBox -->
<div
v-else-if="node.type === 'PictureBox'"
class="wf-control wf-picturebox"
:class="`wf-picturebox-border-${p.BorderStyle ?? 'None'}`"
:style="style"
@click="act({ action: 'click' })"
>
<img v-if="imageUrl" :src="imageUrl" :style="imageStyle" alt="" draggable="false" />
</div>
<!-- CheckBox / RadioButton:自绘勾选框,状态完全由运行器决定(点击只上报,不在浏览器里自行切换) -->
<div
v-else-if="node.type === 'CheckBox' || node.type === 'RadioButton'"
+12
View File
@@ -0,0 +1,12 @@
// 运行时图片:运行器发来的图片数据由 RunSession 保存成 blob 网址,控件属性里只带图片编号。
// 运行区 provide,ControlView inject;设计器预览没有运行时图片,取到空表。
import type { InjectionKey } from 'vue'
export const RUN_IMAGES: InjectionKey<ReadonlyMap<string, string>> = Symbol('runImages')
/** 控件 Image 属性的值(见 runtime/Sim.WinForms/Host/PropValue.cs) */
export interface ImageValue {
key: string
width: number
height: number
}
+20
View File
@@ -151,6 +151,26 @@
color: #fff;
}
.wf-picturebox {
overflow: hidden;
}
.wf-picturebox img {
position: absolute;
left: 0;
top: 0;
display: block;
user-select: none;
}
.wf-picturebox-border-FixedSingle {
border: 1px solid #646464;
}
.wf-picturebox-border-Fixed3D {
border: 2px inset #a0a0a0;
}
.wf-listbox {
overflow-y: auto;
overflow-x: hidden;