项目 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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user