窗体设计器:工具箱、拖放、属性与事件窗口,读写 VS 格式的 Designer.cs

- Designer.cs 在浏览器端解析为语句列表,未改动的语句保留原文:
  教材 25 个参考窗体和全部示例读入再写出逐字节不变
- 编辑操作:添加 / 删除 / 移动 / 改名控件、属性、事件、列表项;
  布局语句增删规则与 VS 一致(对全部现有文件执行无变化)
- 双击控件或事件:绑定事件并在窗体代码末尾插入处理方法,跳到代码
- 预览复用运行时的 ControlView,设计时与运行时外观一致
- 修正:窗体属性 this.ClientSize = new Size(...) 被误认成创建控件;
  示例 Designer.cs 的块头注释改为 VS 的 "// " 格式
- 测试:73 个(读写往返、布局规则、编辑操作、插入事件处理方法)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 11:11:48 +08:00
1 parent 03e80ce2e2
commit ea5a25af06
20 files changed
+2767 -132

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
// 把设计器文档转换成与运行时相同格式的控件树,直接用运行时的 ControlView 渲染,
// 保证设计时看到的样子和运行时一致。
import type { ControlNode } from '@iot/shared'
import { controlDef } from './catalog'
import type { DesignerDocument } from './document'
import { childrenOf, findBlock, getItems, listControls } from './edit'
import { parseValue, type Value } from './values'
/** 窗体在预览树里的 id;控件的 id 就是控件名 */
export const FORM_ID = '__form'
/** 运行时控件树里的类型名:System.Windows.Forms.Button → Button,教材控件库保留完整类名 */
function clientType(type: string) {
return type.startsWith('System.Windows.Forms.') ? type.slice('System.Windows.Forms.'.length) : type
}
/** 把 Value 转换成运行时消息里的属性值格式(见 web/src/winforms/format.ts) */
export function runtimeValue(value: Value): unknown {
switch (value.kind) {
case 'string':
case 'char':
return value.value
case 'bool':
case 'int':
case 'float':
return value.value
case 'point':
return { x: value.x, y: value.y }
case 'size':
return { width: value.width, height: value.height }
case 'color':
return value.css
case 'font':
return { family: value.family, size: value.size, bold: value.bold, italic: value.italic, underline: value.underline, strikeout: value.strikeout }
case 'enum':
return value.member
case 'raw':
return undefined
}
}
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 value = runtimeValue(parseValue(stmt.value))
if (value !== undefined) props[stmt.path] = value
}
return props
}
/** 教材控件在 Designer.cs 里不写、但运行时构造函数会设置的外观 */
const DEVICE_DEFAULTS: Record<string, Record<string, unknown>> = {
'WinFormsDeviceControlLibrary.LED': { BackColor: '#000000', ForeColor: '#ff0000', Font: { family: '宋体', size: 70, bold: false, italic: false, underline: false, strikeout: false } },
'WinFormsDeviceControlLibrary.Lamp': { BackColor: '#ffffff' },
'WinFormsDeviceControlLibrary.Alarm': { BackColor: '#ffffff' },
}
export function previewTree(doc: DesignerDocument): ControlNode {
const types = new Map(listControls(doc).map((c) => [c.name, c.type]))
const build = (name: string): ControlNode => {
const type = types.get(name)!
const props: Record<string, unknown> = { ...DEVICE_DEFAULTS[type], ...propsOf(doc, name) }
// 设计时即使 Visible = false 也显示出来(和 VS 一样),方便选中和编辑
props.Visible = true
if (type === 'System.Windows.Forms.ComboBox') props.Items = getItems(doc, name).map((item) => {
const v = parseValue(item)
return v.kind === 'string' ? v.value : item
})
// 设计器里不需要滚动:LED 只显示黑底
if (type === 'WinFormsDeviceControlLibrary.LED') props.LedText = ''
return { id: name, type: clientType(type), name, props, children: childrenOf(doc, name).filter((c) => types.has(c)).map(build) }
}
const formProps = propsOf(doc, '')
return {
id: FORM_ID,
type: 'Form',
name: doc.formName,
props: { ClientSize: { width: 300, height: 300 }, ...formProps, Visible: true },
children: childrenOf(doc, '').filter((c) => types.has(c)).map(build),
}
}
/** 非可视组件(Timer 等),显示在窗体下方的组件栏 */
export function componentsOf(doc: DesignerDocument) {
return listControls(doc).filter((c) => c.parent === null && controlDef(c.type).component === true)
}