Files
IOT_SIM/web/src/designer/document.ts
T
xuyueandClaude Opus 5.5 8feda289de 多窗体:新建 / 删除窗体,项目 1 登录示例;标识符支持中文
- 新建窗体:生成与 VS 新建窗体相同的 X.cs 和 X.Designer.cs,沿用工程命名空间;
  删除窗体(启动窗体不能删)
- 标签页按窗体分组,启动窗体排在最前并默认打开
- 运行时点哪个窗口哪个到最前;消息框始终在所有窗口之上
- 示例:项目 1 任务 1 登录与增加用户,两个窗体的 Designer.cs 由设计器引擎生成;
  修正书上未声明的 change 变量、关闭后重复 Show 已释放窗体的问题
- 设计器的正则支持中文控件名、类名、命名空间
- 修正:切换窗体时属性窗口的名称没有刷新
- 测试:多窗体端到端测试;新窗体模板测试

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:23:37 +08:00

317 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Designer.cs 的读写:把 InitializeComponent 解析成一条条语句,每条保留原文。
// 没修改过的语句原样输出,修改过的按 VS 的格式重新生成,所以 VS 生成的文件读入后再写出,内容逐字节不变。
// 只认 VS 生成的标准结构(和 VS 一样);结构不对时抛出 DesignerParseError,设计器提示无法打开。
export class DesignerParseError extends Error {}
export type Stmt =
/** this.x = new T(...) */
| { kind: 'create'; name: string; type: string; args: string; text?: string }
/** this[.x].Path = value;target 为 '' 表示窗体本身 */
| { kind: 'prop'; target: string; path: string; value: string; text?: string }
/** this[.x].Event += new Delegate(this.handler) */
| { kind: 'event'; target: string; event: string; delegate: string; handler: string; text?: string }
/** this[.x].Controls.Add(this.child) */
| { kind: 'add'; target: string; child: string; text?: string }
/** this.x.Items.AddRange(new object[] { ... }),items 是每一项的 C# 字面量原文 */
| { kind: 'items'; target: string; items: string[]; text?: string }
/** SuspendLayout / ResumeLayout(false) / PerformLayout / BeginInit / EndInit */
| { kind: 'layout'; target: string; op: LayoutOp; text?: string }
/** 其他语句(局部变量、图表的 Series 设置等),原样保留 */
| { kind: 'raw'; text: string }
export type LayoutOp = 'SuspendLayout' | 'ResumeLayout' | 'PerformLayout' | 'BeginInit' | 'EndInit'
export interface Block {
/** 控件名;窗体的块用窗体类名 */
name: string
stmts: Stmt[]
}
export interface DesignerDocument {
/** 窗体的 Name(窗体属性块的名字,即最后一个块);通常与类名相同,但也可能不同(如教材的 AddUser 窗体叫 frmExam_5_82) */
formName: string
/** partial class 后面的窗体类名 */
className: string
/** InitializeComponent 方法体之前的全部原文(含方法体的 "{" 和换行) */
head: string
/** 第一个注释块之前的语句:components、创建控件、SuspendLayout、BeginInit 等 */
prologue: Stmt[]
/** 各控件的属性块(含窗体),顺序与文件一致 */
blocks: Block[]
/** 窗体块之后的 ResumeLayout、PerformLayout、EndInit */
epilogue: Stmt[]
/** 最后一条语句之后到文件末尾的原文(含方法的 "}"、#endregion、字段声明) */
tail: string
eol: string
indent: string
}
// ---------- 语句切分 ----------
interface Piece {
kind: 'stmt' | 'comment'
text: string
}
/** 把方法体切成语句和注释行;语句以顶层的 ";" 结束,跳过字符串、字符和注释里的符号 */
function splitBody(body: string): Piece[] {
const pieces: Piece[] = []
let i = 0
while (i < body.length) {
const ch = body[i]
if (/\s/.test(ch)) {
i++
continue
}
if (body.startsWith('//', i)) {
const end = body.indexOf('\n', i)
const stop = end < 0 ? body.length : end
pieces.push({ kind: 'comment', text: body.slice(i, stop).replace(/\r$/, '') })
i = stop
continue
}
const start = i
let depth = 0
while (i < body.length) {
const c = body[i]
if (c === '"' || c === "'") {
i = skipLiteral(body, i)
continue
}
if (c === '@' && body[i + 1] === '"') {
i = skipVerbatim(body, i + 1)
continue
}
if (c === '(' || c === '{' || c === '[') depth++
else if (c === ')' || c === '}' || c === ']') depth--
else if (c === ';' && depth === 0) {
pieces.push({ kind: 'stmt', text: body.slice(start, i + 1) })
i++
break
}
i++
}
if (i >= body.length && !body.slice(start).trim().endsWith(';') && body.slice(start).trim()) {
throw new DesignerParseError(`语句没有以分号结束:${body.slice(start, start + 40)}`)
}
}
return pieces
}
function skipLiteral(text: string, i: number) {
const quote = text[i]
i++
while (i < text.length && text[i] !== quote) {
if (text[i] === '\\') i++
if (text[i] === '\n') throw new DesignerParseError('字符串没有结束')
i++
}
return i + 1
}
function skipVerbatim(text: string, i: number) {
i++
while (i < text.length) {
if (text[i] === '"') {
if (text[i + 1] === '"') i += 2
else return i + 1
} else i++
}
throw new DesignerParseError('字符串没有结束')
}
// ---------- 解析 ----------
const CREATE = /^this\.([\p{L}\p{N}_]+) = new ([\p{L}\p{N}_.]+)\(([^()]*)\);$/u
const LAYOUT = /^this\.(?:([\p{L}\p{N}_]+)\.)?(SuspendLayout|ResumeLayout|PerformLayout)\((?:false)?\);$/u
const INIT = /^\(\(System\.ComponentModel\.ISupportInitialize\)\(this\.([\p{L}\p{N}_]+)\)\)\.(BeginInit|EndInit)\(\);$/u
const EVENT = /^this\.(?:([\p{L}\p{N}_]+)\.)?([\p{L}\p{N}_]+) \+= new ([\p{L}\p{N}_.]+)\(this\.([\p{L}\p{N}_]+)\);$/u
const ADD = /^this\.(?:([\p{L}\p{N}_]+)\.)?Controls\.Add\(this\.([\p{L}\p{N}_]+)\);$/u
const ITEMS = /^this\.([\p{L}\p{N}_]+)\.Items\.AddRange\(new object\[\] \{([\s\S]*)\}\);$/u
const PROP = /^this\.([\p{L}\p{N}_.]+) = ([\s\S]+);$/u
/** 一个 Designer.cs 文件里窗体类的名字:partial class 后面的标识符 */
function findFormName(text: string) {
const match = /partial\s+class\s+([\p{L}\p{N}_]+)/u.exec(text)
if (!match) throw new DesignerParseError('找不到 partial class 声明')
return match[1]
}
export function parseDesigner(text: string): DesignerDocument {
const eol = text.includes('\r\n') ? '\r\n' : '\n'
const className = findFormName(text)
const method = /private\s+void\s+InitializeComponent\s*\(\s*\)\s*\{[^\S\n]*\r?\n/.exec(text)
if (!method) throw new DesignerParseError('找不到 InitializeComponent 方法')
const bodyStart = method.index + method[0].length
const bodyEnd = findMethodEnd(text, bodyStart)
const body = text.slice(bodyStart, bodyEnd)
const pieces = splitBody(body)
// 找到最后一条语句在 body 里的结束位置,之后的原文(空行 + "}")归入 tail
const lastStmt = [...pieces].reverse().find((p) => p.kind === 'stmt')
const lastEnd = lastStmt ? body.lastIndexOf(lastStmt.text) + lastStmt.text.length : 0
const tail = body.slice(lastEnd).replace(/^[^\S\n]*\r?\n/, '') + text.slice(bodyEnd)
const indentMatch = /^([ \t]*)\S/m.exec(body)
const indent = indentMatch ? indentMatch[1] : ' '
// 第一遍:找出所有创建的控件。只有声明了字段的才算控件——
// 窗体属性也可能写成 this.ClientSize = new System.Drawing.Size(...),形式和创建控件一样
const fields = new Set([...text.matchAll(/^\s*private\s+[\p{L}\p{N}_.]+\s+([\p{L}\p{N}_]+)\s*;/gmu)].map((m) => m[1]))
const controls = new Set<string>()
for (const piece of pieces) {
const m = piece.kind === 'stmt' ? CREATE.exec(piece.text) : null
if (m && m[1] !== 'components' && fields.has(m[1])) controls.add(m[1])
}
const doc: DesignerDocument = { formName: className, className, head: text.slice(0, bodyStart), prologue: [], blocks: [], epilogue: [], tail, eol, indent }
let block: Block | undefined
let headerLines: string[] = []
for (const piece of pieces) {
if (piece.kind === 'comment') {
headerLines.push(piece.text)
// VS 的块头是三行注释:"// "、"// 名字"、"// "
if (headerLines.length === 3) {
const name = /^\/\/ ([\p{L}\p{N}_]+)$/u.exec(headerLines[1])?.[1]
if (!name || headerLines[0].trim() !== '//' || headerLines[2].trim() !== '//') {
throw new DesignerParseError(`无法识别的注释:${headerLines.join(' ')}`)
}
block = { name, stmts: [] }
doc.blocks.push(block)
headerLines = []
}
continue
}
if (headerLines.length) throw new DesignerParseError(`无法识别的注释:${headerLines.join(' ')}`)
const stmt = classify(piece.text, controls)
if (block) block.stmts.push(stmt)
else doc.prologue.push(stmt)
}
// 最后一个块是窗体本身;块末尾的 ResumeLayout / PerformLayout / EndInit 归入 epilogue
const formBlock = doc.blocks.at(-1)
if (formBlock) {
doc.formName = formBlock.name
while (formBlock.stmts.length && formBlock.stmts.at(-1)!.kind === 'layout') {
doc.epilogue.unshift(formBlock.stmts.pop()!)
}
}
return doc
}
function classify(text: string, controls: Set<string>): Stmt {
let m: RegExpExecArray | null
if ((m = CREATE.exec(text)) && controls.has(m[1])) return { kind: 'create', name: m[1], type: m[2], args: m[3], text }
if ((m = LAYOUT.exec(text))) return { kind: 'layout', target: m[1] ?? '', op: m[2] as LayoutOp, text }
if ((m = INIT.exec(text))) return { kind: 'layout', target: m[1], op: m[2] as LayoutOp, text }
if ((m = EVENT.exec(text))) return { kind: 'event', target: targetOf(m[1], controls), event: m[2], delegate: m[3], handler: m[4], text }
if ((m = ADD.exec(text))) return { kind: 'add', target: targetOf(m[1], controls), child: m[2], text }
if ((m = ITEMS.exec(text)) && controls.has(m[1])) return { kind: 'items', target: m[1], items: splitItems(m[2]), text }
if ((m = PROP.exec(text))) {
const [first, ...rest] = m[1].split('.')
if (controls.has(first) && rest.length) return { kind: 'prop', target: first, path: rest.join('.'), value: m[2], text }
return { kind: 'prop', target: '', path: m[1], value: m[2], text }
}
return { kind: 'raw', text }
}
/** this.x.Event:x 是控件名时是控件的事件;否则(如 this.Load)是窗体的事件 */
function targetOf(name: string | undefined, controls: Set<string>) {
if (!name) return ''
if (controls.has(name)) return name
throw new DesignerParseError(`代码引用了不存在的控件 ${name}`)
}
/** 拆分 new object[] { "a", "b" } 里的各项(逗号分隔,跳过字符串里的逗号) */
function splitItems(inner: string): string[] {
const items: string[] = []
let i = 0
let start = 0
while (i < inner.length) {
const c = inner[i]
if (c === '"' || c === "'") {
i = skipLiteral(inner, i)
continue
}
if (c === ',') {
items.push(inner.slice(start, i).trim())
start = i + 1
}
i++
}
const last = inner.slice(start).trim()
if (last) items.push(last)
return items
}
/** 从方法体开始位置找到与之匹配的 "}"(跳过字符串和注释) */
function findMethodEnd(text: string, start: number) {
let depth = 1
let i = start
while (i < text.length) {
const c = text[i]
if (text.startsWith('//', i)) {
const end = text.indexOf('\n', i)
i = end < 0 ? text.length : end
continue
}
if (c === '"' || c === "'") {
i = skipLiteral(text, i)
continue
}
if (c === '{') depth++
else if (c === '}') {
depth--
if (depth === 0) {
// 回到这一行的行首,"}" 前的缩进归入 tail
return text.lastIndexOf('\n', i) + 1
}
}
i++
}
throw new DesignerParseError('InitializeComponent 方法没有结束')
}
// ---------- 生成 ----------
export function renderStmt(stmt: Stmt, indent = ' ', eol = '\n'): string {
if (stmt.text !== undefined) return stmt.text
const self = (target: string) => (target ? `this.${target}` : 'this')
switch (stmt.kind) {
case 'create':
return `this.${stmt.name} = new ${stmt.type}(${stmt.args});`
case 'prop':
return `${self(stmt.target)}.${stmt.path} = ${stmt.value};`
case 'event':
return `${self(stmt.target)}.${stmt.event} += new ${stmt.delegate}(this.${stmt.handler});`
case 'add':
return `${self(stmt.target)}.Controls.Add(this.${stmt.child});`
case 'items': {
// VS 的格式:每项单独一行,与语句同样缩进,最后一项后紧跟 "});"
const lines = stmt.items.map((item, i) => indent + item + (i < stmt.items.length - 1 ? ',' : '});'))
return `this.${stmt.target}.Items.AddRange(new object[] {${eol}${lines.join(eol)}`
}
case 'layout':
if (stmt.op === 'BeginInit' || stmt.op === 'EndInit') {
return `((System.ComponentModel.ISupportInitialize)(this.${stmt.target})).${stmt.op}();`
}
return `${self(stmt.target)}.${stmt.op}(${stmt.op === 'ResumeLayout' ? 'false' : ''});`
case 'raw':
return stmt.text
}
}
export function generateDesigner(doc: DesignerDocument): string {
const { indent, eol } = doc
const lines: string[] = []
const emit = (stmt: Stmt) => lines.push(indent + renderStmt(stmt, indent, eol))
doc.prologue.forEach(emit)
for (const block of doc.blocks) {
lines.push(`${indent}// `, `${indent}// ${block.name}`, `${indent}// `)
block.stmts.forEach(emit)
}
doc.epilogue.forEach(emit)
return doc.head + lines.join(eol) + eol + doc.tail
}