停靠布局、背景图片、无边框窗体;运行区按比例缩小、可放大
- Dock:和 WinForms 一样按 Controls 倒序依次停靠,扣掉 Panel 边框、GroupBox 标题占的地方; 改大小、显示隐藏、加入移出控件时重新布局;SuspendLayout / ResumeLayout / PerformLayout - 设计器里每次修改后按同样的规则排好停靠的控件(designer/layout.ts);属性窗口加"停靠" - BackgroundImage + BackgroundImageLayout(平铺、居中、拉伸、缩放):窗体、面板、图片框, 设计器里从项目资源选;无边框窗体没有标题栏,大小不含边框 - 背景色沿用父控件:没设背景色的标签、面板、分组框等画父控件的背景色(不是背景图片), 和 WinForms 一样;分组框标题处边框断开、透出背景;文本框的 BackColor 生效 - 图片框可以放子控件,把鼠标按下、松开发给程序 - 运行区:窗体比运行区大时整体按比例缩小并显示比例;"放大"占满整个网页(窗体在左、实训台在右),Esc 还原 - Paint 事件:VS 双击面板生成的空 panel1_Paint 能编译(平台由浏览器绘制,不触发) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42e258ddcd
commit
d60868d912
17 files changed
+600
-49
No files matched your search
@@ -22,6 +22,7 @@ import {
|
||||
setProp,
|
||||
typeOf,
|
||||
} from './edit'
|
||||
import { dockLayout } from './layout'
|
||||
import PropertyGrid from './PropertyGrid.vue'
|
||||
import { componentsOf, previewTree } from './preview'
|
||||
import { parseValue, type Value } from './values'
|
||||
@@ -84,9 +85,10 @@ watch(parsed, () => {
|
||||
if (selected.value && doc.value && !listControls(doc.value).some((c) => c.name === selected.value)) selected.value = ''
|
||||
})
|
||||
|
||||
/** 每次修改后像 VS 一样把停靠的控件排好(没有停靠控件时文档不变) */
|
||||
function commit(next: DesignerDocument, codeText?: string) {
|
||||
dragDoc.value = undefined
|
||||
emit('update', generateDesigner(next), codeText)
|
||||
emit('update', generateDesigner(dockLayout(next)), codeText)
|
||||
}
|
||||
|
||||
// ---------- 几何 ----------
|
||||
|
||||
@@ -59,6 +59,8 @@ const int = (value: number): Value => ({ kind: 'int', value })
|
||||
const size = (width: number, height: number): Value => ({ kind: 'size', width, height })
|
||||
const enumValue = (type: string, member: string): Value => ({ kind: 'enum', type, member })
|
||||
|
||||
const FORMS = 'System.Windows.Forms'
|
||||
|
||||
const EVENT = { delegate: 'System.EventHandler', args: 'EventArgs' }
|
||||
const MOUSE = { delegate: 'System.Windows.Forms.MouseEventHandler', args: 'MouseEventArgs' }
|
||||
|
||||
@@ -72,6 +74,21 @@ const COMMON_PROPS: PropDef[] = [
|
||||
{ name: 'Enabled', label: '可用', description: '是否可用', type: 'bool', default: bool(true), category: '行为' },
|
||||
{ name: 'Visible', label: '可见', description: '是否可见', type: 'bool', default: bool(true), category: '行为' },
|
||||
{ name: 'TabIndex', label: 'Tab 顺序', description: 'Tab 键顺序', type: 'int', category: '行为' },
|
||||
{
|
||||
name: 'Dock', label: '停靠', description: '贴在所在容器的哪一边(并随容器拉伸);填充是占满剩下的地方', type: 'enum', enumType: `${FORMS}.DockStyle`,
|
||||
options: ['None', 'Top', 'Bottom', 'Left', 'Right', 'Fill'], optionLabels: { None: '不停靠', Top: '顶部', Bottom: '底部', Left: '左边', Right: '右边', Fill: '填充' },
|
||||
default: enumValue(`${FORMS}.DockStyle`, 'None'), category: '布局',
|
||||
},
|
||||
]
|
||||
|
||||
/** 背景图片(面板、图片框、窗体):从项目资源里选,排列方式默认平铺 */
|
||||
const BACKGROUND_PROPS: PropDef[] = [
|
||||
{ name: 'BackgroundImage', label: '背景图片', description: '背景图片:从项目资源里选(Properties.Resources)', type: 'image', category: '外观' },
|
||||
{
|
||||
name: 'BackgroundImageLayout', label: '背景排列', description: '背景图片怎样摆放', type: 'enum', enumType: `${FORMS}.ImageLayout`,
|
||||
options: ['None', 'Tile', 'Center', 'Stretch', 'Zoom'], optionLabels: { None: '左上角', Tile: '平铺', Center: '居中', Stretch: '拉伸填满', Zoom: '按比例缩放' },
|
||||
default: enumValue(`${FORMS}.ImageLayout`, 'Tile'), category: '外观',
|
||||
},
|
||||
]
|
||||
|
||||
const COMMON_EVENTS: EventDef[] = [
|
||||
@@ -95,7 +112,6 @@ export function textWidth(text: string) {
|
||||
return width
|
||||
}
|
||||
|
||||
const FORMS = 'System.Windows.Forms'
|
||||
|
||||
export const CONTROLS: ControlDef[] = [
|
||||
{
|
||||
@@ -188,6 +204,7 @@ export const CONTROLS: ControlDef[] = [
|
||||
props: [
|
||||
...COMMON_PROPS.filter((p) => p.name !== 'Text' && p.name !== 'ForeColor' && p.name !== 'Font'),
|
||||
{ name: 'Image', label: '图片', description: '显示的图片:从项目资源里选(Properties.Resources)', type: 'image', category: '外观' },
|
||||
...BACKGROUND_PROPS,
|
||||
{
|
||||
name: 'SizeMode', label: '显示方式', description: '图片在图片框里怎样显示', type: 'enum', enumType: `${FORMS}.PictureBoxSizeMode`,
|
||||
options: ['Normal', 'StretchImage', 'AutoSize', 'CenterImage', 'Zoom'],
|
||||
@@ -327,7 +344,7 @@ export const CONTROLS: ControlDef[] = [
|
||||
namePrefix: 'panel',
|
||||
container: true,
|
||||
defaultEvent: 'Click',
|
||||
props: COMMON_PROPS.filter((p) => p.name !== 'Text'),
|
||||
props: [...COMMON_PROPS.filter((p) => p.name !== 'Text'), ...BACKGROUND_PROPS],
|
||||
events: COMMON_EVENTS.filter((e) => e.name !== 'TextChanged'),
|
||||
initial: () => [['Size', size(200, 100)]],
|
||||
},
|
||||
@@ -413,6 +430,7 @@ export const FORM_DEF: ControlDef = {
|
||||
optionLabels: { Manual: '手动指定', CenterScreen: '屏幕中央', WindowsDefaultLocation: '系统默认位置', WindowsDefaultBounds: '系统默认位置和大小', CenterParent: '父窗体中央' },
|
||||
default: enumValue(`${FORMS}.FormStartPosition`, 'WindowsDefaultLocation'), category: '布局',
|
||||
},
|
||||
...BACKGROUND_PROPS,
|
||||
{ name: 'MaximizeBox', label: '最大化按钮', description: '显示最大化按钮', type: 'bool', default: bool(true), category: '外观' },
|
||||
{ name: 'MinimizeBox', label: '最小化按钮', description: '显示最小化按钮', type: 'bool', default: bool(true), category: '外观' },
|
||||
],
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import { parseDesigner } from './document'
|
||||
import { getPropText } from './edit'
|
||||
import { dockLayout } from './layout'
|
||||
|
||||
/** 一个窗体:panel1(Top)、panel2(Fill,里面有 panel3 Right);后加入 Controls 的先停靠 */
|
||||
const DESIGNER = `namespace 智慧校园
|
||||
{
|
||||
partial class Main
|
||||
{
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.panel1 = new System.Windows.Forms.Panel();
|
||||
this.panel2 = new System.Windows.Forms.Panel();
|
||||
this.panel3 = new System.Windows.Forms.Panel();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// panel1
|
||||
//
|
||||
this.panel1.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.panel1.Location = new System.Drawing.Point(5, 5);
|
||||
this.panel1.Name = "panel1";
|
||||
this.panel1.Size = new System.Drawing.Size(100, 54);
|
||||
//
|
||||
// panel2
|
||||
//
|
||||
this.panel2.Controls.Add(this.panel3);
|
||||
this.panel2.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.panel2.Location = new System.Drawing.Point(0, 0);
|
||||
this.panel2.Name = "panel2";
|
||||
this.panel2.Size = new System.Drawing.Size(10, 10);
|
||||
//
|
||||
// panel3
|
||||
//
|
||||
this.panel3.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this.panel3.Location = new System.Drawing.Point(0, 0);
|
||||
this.panel3.Name = "panel3";
|
||||
this.panel3.Size = new System.Drawing.Size(120, 30);
|
||||
//
|
||||
// Main
|
||||
//
|
||||
this.ClientSize = new System.Drawing.Size(800, 600);
|
||||
this.Controls.Add(this.panel2);
|
||||
this.Controls.Add(this.panel1);
|
||||
this.Name = "Main";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
private System.Windows.Forms.Panel panel1;
|
||||
private System.Windows.Forms.Panel panel2;
|
||||
private System.Windows.Forms.Panel panel3;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
test('停靠布局:顶部贴边拉满宽度,填充占剩下的,嵌套的右停靠贴着父面板右边', () => {
|
||||
const doc = dockLayout(parseDesigner(DESIGNER))
|
||||
expect(getPropText(doc, 'panel1', 'Location')).toBe('new System.Drawing.Point(0, 0)')
|
||||
expect(getPropText(doc, 'panel1', 'Size')).toBe('new System.Drawing.Size(800, 54)')
|
||||
expect(getPropText(doc, 'panel2', 'Location')).toBe('new System.Drawing.Point(0, 54)')
|
||||
expect(getPropText(doc, 'panel2', 'Size')).toBe('new System.Drawing.Size(800, 546)')
|
||||
expect(getPropText(doc, 'panel3', 'Location')).toBe('new System.Drawing.Point(680, 0)')
|
||||
expect(getPropText(doc, 'panel3', 'Size')).toBe('new System.Drawing.Size(120, 546)')
|
||||
})
|
||||
|
||||
test('停靠布局:已经排好的窗体不变', () => {
|
||||
const doc = dockLayout(parseDesigner(DESIGNER))
|
||||
expect(dockLayout(doc)).toEqual(doc)
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
// 设计器里的停靠布局:和 VS 一样,改了停靠、大小等之后立即把停靠的控件排好,位置大小写进 Designer.cs。
|
||||
// 规则和运行时(runtime/Sim.WinForms/Forms/Control.cs 的 PerformLayout)相同:按 Controls 集合从后往前,
|
||||
// 上 / 下 / 左 / 右依次贴边并占掉那一条,填充占剩下的全部。设计时不可见的控件也照样占位(VS 设计器里看得见它们)。
|
||||
import type { DesignerDocument } from './document'
|
||||
import { childrenOf, getPropText, setProp, typeOf } from './edit'
|
||||
import { parseValue } from './values'
|
||||
|
||||
interface Rect {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
function sizeOf(doc: DesignerDocument, name: string, path = 'Size') {
|
||||
const text = getPropText(doc, name, path)
|
||||
const value = text === undefined ? undefined : parseValue(text)
|
||||
return value?.kind === 'size' ? { width: value.width, height: value.height } : undefined
|
||||
}
|
||||
|
||||
function pointOf(doc: DesignerDocument, name: string) {
|
||||
const text = getPropText(doc, name, 'Location')
|
||||
const value = text === undefined ? undefined : parseValue(text)
|
||||
return value?.kind === 'point' ? { x: value.x, y: value.y } : { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
function dockOf(doc: DesignerDocument, name: string) {
|
||||
const text = getPropText(doc, name, 'Dock')
|
||||
const value = text === undefined ? undefined : parseValue(text)
|
||||
return value?.kind === 'enum' ? value.member : 'None'
|
||||
}
|
||||
|
||||
/** new System.Windows.Forms.Padding(2) 或 (1, 2, 3, 4) */
|
||||
function paddingOf(doc: DesignerDocument, name: string) {
|
||||
const m = /Padding\((\d+)(?:,\s*(\d+),\s*(\d+),\s*(\d+))?\)/.exec(getPropText(doc, name, 'Padding') ?? '')
|
||||
if (!m) return { left: 0, top: 0, right: 0, bottom: 0 }
|
||||
return m[2] === undefined
|
||||
? { left: +m[1], top: +m[1], right: +m[1], bottom: +m[1] }
|
||||
: { left: +m[1], top: +m[2], right: +m[3], bottom: +m[4] }
|
||||
}
|
||||
|
||||
/** 容器里用来停靠的区域:客户区去掉内边距;带边框的面板去掉边框,分组框去掉边框和标题 */
|
||||
function displayRect(doc: DesignerDocument, container: string): Rect | undefined {
|
||||
const size = container === '' ? sizeOf(doc, '', 'ClientSize') : sizeOf(doc, container)
|
||||
if (!size) return undefined
|
||||
const pad = paddingOf(doc, container)
|
||||
let rect = { x: pad.left, y: pad.top, width: size.width - pad.left - pad.right, height: size.height - pad.top - pad.bottom }
|
||||
const type = container === '' ? '' : typeOf(doc, container)
|
||||
if (type === 'System.Windows.Forms.GroupBox') rect = { x: rect.x + 3, y: rect.y + 17, width: rect.width - 6, height: rect.height - 20 }
|
||||
if (type === 'System.Windows.Forms.Panel') {
|
||||
const border = (getPropText(doc, container, 'BorderStyle') ?? '').split('.').at(-1)
|
||||
const inset = border === 'FixedSingle' ? 1 : border === 'Fixed3D' ? 2 : 0
|
||||
rect = { ...rect, width: rect.width - 2 * inset, height: rect.height - 2 * inset }
|
||||
}
|
||||
return { ...rect, width: Math.max(0, rect.width), height: Math.max(0, rect.height) }
|
||||
}
|
||||
|
||||
/** 把整个窗体的停靠控件排好;没有停靠控件或位置已经对时原样返回 */
|
||||
export function dockLayout(doc: DesignerDocument): DesignerDocument {
|
||||
let next = doc
|
||||
|
||||
const place = (name: string, bounds: Rect) => {
|
||||
const point = pointOf(next, name)
|
||||
if (point.x !== bounds.x || point.y !== bounds.y) next = setProp(next, name, 'Location', { kind: 'point', x: bounds.x, y: bounds.y })
|
||||
const size = sizeOf(next, name)
|
||||
if (size?.width !== bounds.width || size?.height !== bounds.height) next = setProp(next, name, 'Size', { kind: 'size', width: bounds.width, height: bounds.height })
|
||||
}
|
||||
|
||||
const layout = (container: string) => {
|
||||
const children = childrenOf(next, container)
|
||||
let rect = displayRect(next, container)
|
||||
if (rect) {
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const child = children[i]
|
||||
const dock = dockOf(next, child)
|
||||
if (dock === 'None') continue
|
||||
const size = sizeOf(next, child) ?? { width: 0, height: 0 }
|
||||
switch (dock) {
|
||||
case 'Top':
|
||||
place(child, { x: rect.x, y: rect.y, width: rect.width, height: size.height })
|
||||
rect = { ...rect, y: rect.y + size.height, height: Math.max(0, rect.height - size.height) }
|
||||
break
|
||||
case 'Bottom':
|
||||
place(child, { x: rect.x, y: rect.y + rect.height - size.height, width: rect.width, height: size.height })
|
||||
rect = { ...rect, height: Math.max(0, rect.height - size.height) }
|
||||
break
|
||||
case 'Left':
|
||||
place(child, { x: rect.x, y: rect.y, width: size.width, height: rect.height })
|
||||
rect = { ...rect, x: rect.x + size.width, width: Math.max(0, rect.width - size.width) }
|
||||
break
|
||||
case 'Right':
|
||||
place(child, { x: rect.x + rect.width - size.width, y: rect.y, width: size.width, height: rect.height })
|
||||
rect = { ...rect, width: Math.max(0, rect.width - size.width) }
|
||||
break
|
||||
default:
|
||||
place(child, rect)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const child of children) layout(child)
|
||||
}
|
||||
|
||||
layout('')
|
||||
return next
|
||||
}
|
||||
Reference in new issue
Block a user