窗体设计器:工具箱、拖放、属性与事件窗口,读写 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

+174
View File
@@ -0,0 +1,174 @@
// 属性值的 C# 表达式 ⇄ 结构化值。读入时尽量识别;识别不了的保留原文(属性窗口里只读显示)。
// 写出时按 VS 设计器的格式,例如 new System.Drawing.Point(12, 12)、"文字"、System.Drawing.Color.White。
export type Value =
| { kind: 'string'; value: string }
| { kind: 'char'; value: string }
| { kind: 'bool'; value: boolean }
| { kind: 'int'; value: number }
| { kind: 'float'; value: number }
| { kind: 'point'; x: number; y: number }
| { kind: 'size'; width: number; height: number }
| { kind: 'color'; css: string; name?: string; r: number; g: number; b: number; a: number }
| { kind: 'font'; family: string; size: number; bold: boolean; italic: boolean; underline: boolean; strikeout: boolean; charset?: number }
| { kind: 'enum'; type: string; member: string }
| { kind: 'raw'; text: string }
// ---------- 读 ----------
export function parseValue(text: string): Value {
const t = text.trim()
let m: RegExpExecArray | null
if (/^"(?:[^"\\]|\\.)*"$/.test(t)) return { kind: 'string', value: unescapeString(t.slice(1, -1)) }
if (/^@"(?:[^"]|"")*"$/.test(t)) return { kind: 'string', value: t.slice(2, -1).replace(/""/g, '"') }
if (/^'(?:[^'\\]|\\.)'$/.test(t)) return { kind: 'char', value: unescapeString(t.slice(1, -1)) }
if (t === 'true' || t === 'false') return { kind: 'bool', value: t === 'true' }
if (/^-?\d+$/.test(t)) return { kind: 'int', value: Number(t) }
if ((m = /^(-?\d+(?:\.\d+)?)[Ff]$/.exec(t))) return { kind: 'float', value: Number(m[1]) }
if ((m = /^new System\.Drawing\.Point\((-?\d+), (-?\d+)\)$/.exec(t))) return { kind: 'point', x: +m[1], y: +m[2] }
if ((m = /^new System\.Drawing\.Size\((-?\d+), (-?\d+)\)$/.exec(t))) return { kind: 'size', width: +m[1], height: +m[2] }
if ((m = /^System\.Drawing\.(?:Color|SystemColors)\.(\w+)$/.exec(t))) return namedColor(m[1])
if ((m = /^System\.Drawing\.Color\.FromArgb\(([\s\S]+)\)$/.exec(t))) {
// VS 的写法是 ((int)(((byte)(53)))),手写的可能是 53
const parts = m[1].includes('(byte)') ? byteParts(m[1]) : m[1].split(',').map((n) => Number(n.trim()))
if (parts.length === 3) return rgb(parts[0], parts[1], parts[2])
if (parts.length === 4) return rgb(parts[1], parts[2], parts[3], parts[0])
}
if ((m = /^new System\.Drawing\.Font\("((?:[^"\\]|\\.)*)", (\d+(?:\.\d+)?)F(.*)\)$/.exec(t))) {
const rest = m[3]
const charset = /\(\(byte\)\((\d+)\)\)/.exec(rest)
return {
kind: 'font',
family: unescapeString(m[1]),
size: Number(m[2]),
bold: rest.includes('FontStyle.Bold'),
italic: rest.includes('FontStyle.Italic'),
underline: rest.includes('FontStyle.Underline'),
strikeout: rest.includes('FontStyle.Strikeout'),
charset: charset ? Number(charset[1]) : undefined,
}
}
if ((m = /^((?:System|WinFormsDeviceControlLibrary)(?:\.\w+)+)\.(\w+)$/.exec(t))) return { kind: 'enum', type: m[1], member: m[2] }
return { kind: 'raw', text: t }
}
/** 从 ((int)(((byte)(53)))) 这样的写法中逐个分量取出数值 */
function byteParts(args: string) {
return [...args.matchAll(/\(byte\)\((\d+)\)/g)].map((x) => Number(x[1]))
}
function unescapeString(s: string) {
return s.replace(/\\(u[0-9a-fA-F]{4}|.)/g, (_, c: string) => {
if (c.length === 5) return String.fromCharCode(parseInt(c.slice(1), 16))
return ({ n: '\n', r: '\r', t: '\t', '0': '\0', '\\': '\\', '"': '"', "'": "'" } as Record<string, string>)[c] ?? c
})
}
// ---------- 写 ----------
export function renderValue(value: Value): string {
switch (value.kind) {
case 'string':
return `"${escapeString(value.value)}"`
case 'char':
return `'${value.value === "'" ? "\\'" : escapeString(value.value)}'`
case 'bool':
return String(value.value)
case 'int':
return String(Math.round(value.value))
case 'float':
return `${value.value}F`
case 'point':
return `new System.Drawing.Point(${value.x}, ${value.y})`
case 'size':
return `new System.Drawing.Size(${value.width}, ${value.height})`
case 'color':
if (value.name) return `System.Drawing.${SYSTEM_COLORS[value.name] ? 'SystemColors' : 'Color'}.${value.name}`
return value.a === 255
? `System.Drawing.Color.FromArgb(${[value.r, value.g, value.b].map(byteArg).join(', ')})`
: `System.Drawing.Color.FromArgb(${[value.a, value.r, value.g, value.b].map(byteArg).join(', ')})`
case 'font': {
const styles = [
value.bold && 'Bold',
value.italic && 'Italic',
value.underline && 'Underline',
value.strikeout && 'Strikeout',
].filter(Boolean) as string[]
const style = styles.length ? styles.map((s) => `System.Drawing.FontStyle.${s}`).join(' | ') : 'System.Drawing.FontStyle.Regular'
// 与中文 Windows 上的 VS 一致:带 GB2312 字符集 134
return `new System.Drawing.Font("${escapeString(value.family)}", ${value.size}F, ${style}, System.Drawing.GraphicsUnit.Point, ((byte)(${value.charset ?? 134})))`
}
case 'enum':
return `${value.type}.${value.member}`
case 'raw':
return value.text
}
}
const byteArg = (n: number) => `((int)(((byte)(${n}))))`
function escapeString(s: string) {
return s.replace(/[\\"\r\n\t\0]/g, (c) => ({ '\\': '\\\\', '"': '\\"', '\r': '\\r', '\n': '\\n', '\t': '\\t', '\0': '\\0' })[c]!)
}
// ---------- 颜色 ----------
/** 常用的命名颜色(System.Drawing.Color),属性窗口里提供这些选项 */
export const NAMED_COLORS: Record<string, string> = {
Transparent: 'transparent',
Black: '#000000',
White: '#ffffff',
Red: '#ff0000',
Green: '#008000',
Blue: '#0000ff',
Yellow: '#ffff00',
Orange: '#ffa500',
Gray: '#808080',
DimGray: '#696969',
Silver: '#c0c0c0',
LightGray: '#d3d3d3',
DarkBlue: '#00008b',
SkyBlue: '#87ceeb',
LightGreen: '#90ee90',
Lime: '#00ff00',
Purple: '#800080',
Brown: '#a52a2a',
Pink: '#ffc0cb',
Cyan: '#00ffff',
}
/** 系统颜色(System.Drawing.SystemColors),按 Windows 10 默认主题取值 */
const SYSTEM_COLORS: Record<string, string> = {
Control: '#f0f0f0',
ControlText: '#000000',
Window: '#ffffff',
WindowText: '#000000',
ControlDark: '#a0a0a0',
ControlLight: '#e3e3e3',
Highlight: '#0078d7',
HighlightText: '#ffffff',
GrayText: '#6d6d6d',
InactiveCaption: '#bfcddb',
}
function namedColor(name: string): Value {
const css = NAMED_COLORS[name] ?? SYSTEM_COLORS[name] ?? cssNamed(name)
if (css === 'transparent') return { kind: 'color', css, name, r: 255, g: 255, b: 255, a: 0 }
const [r, g, b] = hexToRgb(css)
return { kind: 'color', css, name, r, g, b, a: 255 }
}
/** 不在列表里的颜色名(如 Color.Gold)交给浏览器:CSS 颜色名和 .NET 的颜色名基本一致 */
function cssNamed(name: string) {
return name.toLowerCase()
}
export function rgb(r: number, g: number, b: number, a = 255): Value {
const css = a === 255 ? `#${[r, g, b].map((n) => n.toString(16).padStart(2, '0')).join('')}` : `rgba(${r},${g},${b},${(a / 255).toFixed(3)})`
return { kind: 'color', css, r, g, b, a }
}
export function hexToRgb(hex: string): [number, number, number] {
const n = Number.parseInt(hex.replace('#', ''), 16)
return Number.isNaN(n) ? [0, 0, 0] : [(n >> 16) & 255, (n >> 8) & 255, n & 255]
}