停靠布局、背景图片、无边框窗体;运行区按比例缩小、可放大
- 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
@@ -0,0 +1,14 @@
|
||||
namespace System.Drawing;
|
||||
|
||||
/// <summary>
|
||||
/// 绘图面。平台的控件由浏览器绘制,学生程序不能自己画(没有 DrawLine 等方法);
|
||||
/// 只为让 VS 生成的空 Paint 事件处理程序(panel1_Paint)能编译。
|
||||
/// </summary>
|
||||
public sealed class Graphics : IDisposable
|
||||
{
|
||||
private Graphics()
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose() => GC.SuppressFinalize(this);
|
||||
}
|
||||
@@ -43,7 +43,21 @@ public class Panel : ScrollableControl
|
||||
public BorderStyle BorderStyle
|
||||
{
|
||||
get => _borderStyle;
|
||||
set => SetProp(ref _borderStyle, value, nameof(BorderStyle));
|
||||
set
|
||||
{
|
||||
if (SetProp(ref _borderStyle, value, nameof(BorderStyle))) PerformLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>边框占掉的宽度:单线 1 像素,立体 2 像素</summary>
|
||||
public override Rectangle DisplayRectangle
|
||||
{
|
||||
get
|
||||
{
|
||||
var inset = _borderStyle switch { BorderStyle.FixedSingle => 1, BorderStyle.Fixed3D => 2, _ => 0 };
|
||||
var rect = base.DisplayRectangle;
|
||||
return new Rectangle(rect.X, rect.Y, Math.Max(0, rect.Width - 2 * inset), Math.Max(0, rect.Height - 2 * inset));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
@@ -58,4 +72,14 @@ public class GroupBox : Control
|
||||
protected override Size DefaultSize => new(200, 100);
|
||||
|
||||
public FlatStyle FlatStyle { get; set; } = FlatStyle.Standard;
|
||||
|
||||
/// <summary>分组框的边框和标题占掉的地方(宋体 9pt 时和 WinForms 一致)</summary>
|
||||
public override Rectangle DisplayRectangle
|
||||
{
|
||||
get
|
||||
{
|
||||
var rect = base.DisplayRectangle;
|
||||
return new Rectangle(rect.X + 3, rect.Y + 17, Math.Max(0, rect.Width - 6), Math.Max(0, rect.Height - 20));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,10 @@ public class Control : Component, IWin32Window
|
||||
private Font? _font;
|
||||
private bool _autoSize;
|
||||
private DockStyle _dock;
|
||||
private Padding _padding;
|
||||
private int _layoutSuspendCount;
|
||||
private Image? _backgroundImage;
|
||||
private ImageLayout _backgroundImageLayout = ImageLayout.Tile;
|
||||
private AnchorStyles _anchor = AnchorStyles.Top | AnchorStyles.Left;
|
||||
private Control? _parent;
|
||||
|
||||
@@ -131,7 +135,13 @@ public class Control : Component, IWin32Window
|
||||
public Size Size
|
||||
{
|
||||
get => _size;
|
||||
set => SetProp(ref _size, value, nameof(Size));
|
||||
set
|
||||
{
|
||||
if (!SetProp(ref _size, value, nameof(Size))) return;
|
||||
// 大小变了:自己的停靠子控件重新排;自己是停靠的,父容器也要重新排
|
||||
PerformLayout();
|
||||
if (_dock != DockStyle.None) _parent?.PerformLayout();
|
||||
}
|
||||
}
|
||||
|
||||
public int Left { get => _location.X; set => Location = new Point(value, _location.Y); }
|
||||
@@ -162,7 +172,9 @@ public class Control : Component, IWin32Window
|
||||
get => _visible;
|
||||
set
|
||||
{
|
||||
if (SetProp(ref _visible, value, nameof(Visible))) OnVisibleChanged(EventArgs.Empty);
|
||||
if (!SetProp(ref _visible, value, nameof(Visible))) return;
|
||||
if (_dock != DockStyle.None) _parent?.PerformLayout();
|
||||
OnVisibleChanged(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +214,40 @@ public class Control : Component, IWin32Window
|
||||
public DockStyle Dock
|
||||
{
|
||||
get => _dock;
|
||||
set => SetProp(ref _dock, value, nameof(Dock));
|
||||
set
|
||||
{
|
||||
if (SetProp(ref _dock, value, nameof(Dock))) _parent?.PerformLayout();
|
||||
}
|
||||
}
|
||||
|
||||
public Padding Padding
|
||||
{
|
||||
get => _padding;
|
||||
set
|
||||
{
|
||||
if (_padding.Equals(value)) return;
|
||||
_padding = value;
|
||||
PerformLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>背景图片;BackgroundImageLayout 决定平铺(默认)、居中、拉伸或按比例缩放</summary>
|
||||
public virtual Image? BackgroundImage
|
||||
{
|
||||
get => _backgroundImage;
|
||||
set
|
||||
{
|
||||
LegacyThreads.CheckPoint();
|
||||
if (ReferenceEquals(_backgroundImage, value)) return;
|
||||
_backgroundImage = value;
|
||||
NotifyChanged(nameof(BackgroundImage), value);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual ImageLayout BackgroundImageLayout
|
||||
{
|
||||
get => _backgroundImageLayout;
|
||||
set => SetProp(ref _backgroundImageLayout, value, nameof(BackgroundImageLayout));
|
||||
}
|
||||
|
||||
public AnchorStyles Anchor
|
||||
@@ -216,7 +261,6 @@ public class Control : Component, IWin32Window
|
||||
public bool TabStop { get; set; } = true;
|
||||
public object? Tag { get; set; }
|
||||
public Padding Margin { get; set; } = new(3);
|
||||
public Padding Padding { get; set; }
|
||||
public ImeMode ImeMode { get; set; } = ImeMode.NoControl;
|
||||
public virtual RightToLeft RightToLeft { get; set; } = RightToLeft.No;
|
||||
public virtual bool AllowDrop { get; set; }
|
||||
@@ -246,6 +290,8 @@ public class Control : Component, IWin32Window
|
||||
public event EventHandler? LostFocus;
|
||||
public event EventHandler? Enter;
|
||||
public event EventHandler? Leave;
|
||||
/// <summary>重绘。平台由浏览器绘制控件,不会触发这个事件(VS 双击 Panel 生成的空处理程序照常能编译)</summary>
|
||||
public event PaintEventHandler? Paint;
|
||||
|
||||
protected virtual void OnClick(EventArgs e) => Click?.Invoke(this, e);
|
||||
protected virtual void OnDoubleClick(EventArgs e) => DoubleClick?.Invoke(this, e);
|
||||
@@ -259,6 +305,7 @@ public class Control : Component, IWin32Window
|
||||
protected virtual void OnEnabledChanged(EventArgs e) => EnabledChanged?.Invoke(this, e);
|
||||
protected virtual void OnGotFocus(EventArgs e) => GotFocus?.Invoke(this, e);
|
||||
protected virtual void OnLostFocus(EventArgs e) => LostFocus?.Invoke(this, e);
|
||||
protected virtual void OnPaint(PaintEventArgs e) => Paint?.Invoke(this, e);
|
||||
protected virtual void OnEnter(EventArgs e) => Enter?.Invoke(this, e);
|
||||
protected virtual void OnLeave(EventArgs e) => Leave?.Invoke(this, e);
|
||||
|
||||
@@ -305,10 +352,65 @@ public class Control : Component, IWin32Window
|
||||
public void Refresh() { }
|
||||
public void Invalidate() { }
|
||||
public void Update() { }
|
||||
public void SuspendLayout() { }
|
||||
public void ResumeLayout() { }
|
||||
public void ResumeLayout(bool performLayout) { }
|
||||
public void PerformLayout() { }
|
||||
public void SuspendLayout() => _layoutSuspendCount++;
|
||||
public void ResumeLayout() => ResumeLayout(true);
|
||||
|
||||
public void ResumeLayout(bool performLayout)
|
||||
{
|
||||
if (_layoutSuspendCount > 0) _layoutSuspendCount--;
|
||||
if (performLayout) PerformLayout();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停靠布局(和 WinForms 一样):按 Controls 集合从后往前,Top / Bottom / Left / Right 依次贴边并占掉那一条,
|
||||
/// Fill 占剩下的全部。VS 在 Designer.cs 里写的位置大小就是这样排出来的;代码里改停靠、大小、可见时重新排。
|
||||
/// </summary>
|
||||
public void PerformLayout()
|
||||
{
|
||||
if (_layoutSuspendCount > 0) return;
|
||||
var rect = DisplayRectangle;
|
||||
for (var i = Controls.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var child = Controls[i];
|
||||
if (!child._visible || child._dock == DockStyle.None) continue;
|
||||
var size = child._size;
|
||||
Rectangle bounds;
|
||||
switch (child._dock)
|
||||
{
|
||||
case DockStyle.Top:
|
||||
bounds = new Rectangle(rect.X, rect.Y, rect.Width, size.Height);
|
||||
rect = new Rectangle(rect.X, rect.Y + size.Height, rect.Width, Math.Max(0, rect.Height - size.Height));
|
||||
break;
|
||||
case DockStyle.Bottom:
|
||||
bounds = new Rectangle(rect.X, rect.Bottom - size.Height, rect.Width, size.Height);
|
||||
rect = new Rectangle(rect.X, rect.Y, rect.Width, Math.Max(0, rect.Height - size.Height));
|
||||
break;
|
||||
case DockStyle.Left:
|
||||
bounds = new Rectangle(rect.X, rect.Y, size.Width, rect.Height);
|
||||
rect = new Rectangle(rect.X + size.Width, rect.Y, Math.Max(0, rect.Width - size.Width), rect.Height);
|
||||
break;
|
||||
case DockStyle.Right:
|
||||
bounds = new Rectangle(rect.Right - size.Width, rect.Y, size.Width, rect.Height);
|
||||
rect = new Rectangle(rect.X, rect.Y, Math.Max(0, rect.Width - size.Width), rect.Height);
|
||||
break;
|
||||
default:
|
||||
bounds = rect;
|
||||
break;
|
||||
}
|
||||
child.SetBoundsFromLayout(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>停靠布局给出的位置大小:不再反过来触发父容器的布局</summary>
|
||||
private void SetBoundsFromLayout(Rectangle bounds)
|
||||
{
|
||||
SetProp(ref _location, bounds.Location, nameof(Location));
|
||||
if (SetProp(ref _size, bounds.Size, nameof(Size))) PerformLayout();
|
||||
}
|
||||
|
||||
/// <summary>容器里用来停靠子控件的区域:客户区去掉内边距(分组框、带边框的面板再去掉边框)</summary>
|
||||
public virtual Rectangle DisplayRectangle =>
|
||||
new(_padding.Left, _padding.Top, Math.Max(0, ClientSize.Width - _padding.Horizontal), Math.Max(0, ClientSize.Height - _padding.Vertical));
|
||||
|
||||
public bool InvokeRequired => !SimHost.Ui.IsUiThread;
|
||||
|
||||
@@ -409,6 +511,8 @@ public class Control : Component, IWin32Window
|
||||
props["Font"] = PropValue.From(_font);
|
||||
props["AutoSize"] = _autoSize;
|
||||
props["Dock"] = _dock.ToString();
|
||||
props["BackgroundImage"] = PropValue.From(_backgroundImage);
|
||||
props["BackgroundImageLayout"] = _backgroundImageLayout.ToString();
|
||||
}
|
||||
|
||||
/// <summary>浏览器按这个类型名选择渲染方式。学生自己继承的控件按最近的仿真库基类渲染。</summary>
|
||||
@@ -459,6 +563,7 @@ public class Control : Component, IWin32Window
|
||||
value.Parent?.Controls.Remove(value);
|
||||
_items.Add(value);
|
||||
value.SetParentInternal(Owner);
|
||||
if (value.Dock != DockStyle.None) Owner.PerformLayout();
|
||||
if (Owner.IsAttached)
|
||||
{
|
||||
SimHost.Send(new JsonObject
|
||||
@@ -481,6 +586,7 @@ public class Control : Component, IWin32Window
|
||||
if (value == null || !_items.Remove(value)) return;
|
||||
var wasAttached = Owner.IsAttached;
|
||||
value.SetParentInternal(null);
|
||||
if (value.Dock != DockStyle.None) Owner.PerformLayout();
|
||||
if (wasAttached) SimHost.Send(new JsonObject { ["type"] = "controlRemoved", ["id"] = value.SimId });
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ public enum ComboBoxStyle { Simple, DropDown, DropDownList }
|
||||
|
||||
public enum DockStyle { None, Top, Bottom, Left, Right, Fill }
|
||||
|
||||
public enum ImageLayout { None, Tile, Center, Stretch, Zoom }
|
||||
|
||||
[Flags]
|
||||
public enum AnchorStyles { None = 0, Top = 1, Bottom = 2, Left = 4, Right = 8 }
|
||||
|
||||
|
||||
@@ -41,3 +41,20 @@ public class FormClosedEventArgs : EventArgs
|
||||
}
|
||||
|
||||
public delegate void FormClosedEventHandler(object? sender, FormClosedEventArgs e);
|
||||
|
||||
/// <summary>重绘事件的参数。平台由浏览器绘制控件,不支持在 Graphics 上自己画</summary>
|
||||
public class PaintEventArgs : EventArgs, IDisposable
|
||||
{
|
||||
public PaintEventArgs(System.Drawing.Graphics graphics, System.Drawing.Rectangle clipRect)
|
||||
{
|
||||
Graphics = graphics;
|
||||
ClipRectangle = clipRect;
|
||||
}
|
||||
|
||||
public System.Drawing.Graphics Graphics { get; }
|
||||
public System.Drawing.Rectangle ClipRectangle { get; }
|
||||
|
||||
public void Dispose() => GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public delegate void PaintEventHandler(object? sender, PaintEventArgs e);
|
||||
@@ -33,13 +33,16 @@ public class Form : ContainerControl
|
||||
public override Size ClientSize
|
||||
{
|
||||
get => _clientSize;
|
||||
set => SetProp(ref _clientSize, value, nameof(ClientSize));
|
||||
set
|
||||
{
|
||||
if (SetProp(ref _clientSize, value, nameof(ClientSize))) PerformLayout();
|
||||
}
|
||||
}
|
||||
|
||||
public new Size Size
|
||||
{
|
||||
get => _clientSize + NonClientSize;
|
||||
set => ClientSize = value - NonClientSize;
|
||||
get => _formBorderStyle == FormBorderStyle.None ? _clientSize : _clientSize + NonClientSize;
|
||||
set => ClientSize = _formBorderStyle == FormBorderStyle.None ? value : value - NonClientSize;
|
||||
}
|
||||
|
||||
public FormBorderStyle FormBorderStyle
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export async function captureForms(files: SourceFile[]): Promise<Screenshot[]> {
|
||||
}
|
||||
const holder = document.createElement('div')
|
||||
host.append(holder)
|
||||
const app = createApp({ render: () => h(FormWindow, { form, modal: false, offset: 0, act: () => {} }) })
|
||||
const app = createApp({ render: () => h(FormWindow, { form, modal: false, offset: 0, scale: 1, act: () => {} }) })
|
||||
app.mount(holder)
|
||||
await nextTick()
|
||||
const frame = holder.querySelector<HTMLElement>('.wf-window')
|
||||
|
||||
+124
-13
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// 运行区:学生程序的窗体(在一块"桌面"上)、提示框、虚拟实训台,以及编译错误和控制台输出。
|
||||
// 实验页和老师的评阅页共用。
|
||||
import { provide, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, provide, ref } from 'vue'
|
||||
import TrainingBoard from '../scene/TrainingBoard.vue'
|
||||
import FormWindow from '../winforms/FormWindow.vue'
|
||||
import type { SizeValue } from '../winforms/format'
|
||||
import FormWindow, { WINDOW_MARGIN, WINDOW_STEP } from '../winforms/FormWindow.vue'
|
||||
import { RUN_IMAGES } from '../winforms/images'
|
||||
import MessageBoxDialog from '../winforms/MessageBoxDialog.vue'
|
||||
import type { RunSession } from './session'
|
||||
@@ -16,19 +17,59 @@ provide(RUN_IMAGES, props.session.images)
|
||||
|
||||
/** 照片列表里点开看大图 */
|
||||
const viewing = ref<{ path: string; url: string } | null>(null)
|
||||
|
||||
// ---------- 窗体比桌面大时整体缩小(项目 8 的窗体有 1000 多像素宽),"放大"后运行区占满整个网页 ----------
|
||||
const maximized = ref(false)
|
||||
const desktop = ref<HTMLElement>()
|
||||
const desktopSize = ref({ width: 0, height: 0 })
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
desktopSize.value = { width: entry.contentRect.width, height: entry.contentRect.height }
|
||||
})
|
||||
onMounted(() => observer.observe(desktop.value!))
|
||||
onBeforeUnmount(() => observer.disconnect())
|
||||
|
||||
const TITLEBAR = 30
|
||||
/** 按窗体最初的层叠位置算出放下所有窗体需要多大,桌面不够时按比例缩小(不放大);拖动窗体不影响缩放 */
|
||||
const scale = computed(() => {
|
||||
let width = 0
|
||||
let height = 0
|
||||
state.forms.forEach((form, index) => {
|
||||
const p = form.root.props
|
||||
if (p.Visible === false) return
|
||||
const client = (p.ClientSize as SizeValue | undefined) ?? { width: 300, height: 300 }
|
||||
const start = WINDOW_MARGIN + index * WINDOW_STEP
|
||||
width = Math.max(width, start + client.width + 2 + WINDOW_MARGIN)
|
||||
height = Math.max(height, start + client.height + 2 + (p.FormBorderStyle === 'None' ? 0 : TITLEBAR) + WINDOW_MARGIN)
|
||||
})
|
||||
const { width: w, height: h } = desktopSize.value
|
||||
if (!width || !w || !h) return 1
|
||||
return Math.min(1, w / width, h / height)
|
||||
})
|
||||
|
||||
function onKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && maximized.value) maximized.value = false
|
||||
}
|
||||
onMounted(() => window.addEventListener('keydown', onKey))
|
||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="run-area">
|
||||
<div class="desktop">
|
||||
<FormWindow
|
||||
v-for="(form, index) in state.forms"
|
||||
:key="form.root.id"
|
||||
:form="form.root"
|
||||
:modal="form.modal"
|
||||
:offset="index"
|
||||
:act="session.act"
|
||||
/>
|
||||
<section class="run-area" :class="{ maximized }">
|
||||
<div ref="desktop" class="desktop">
|
||||
<div
|
||||
class="desktop-canvas"
|
||||
:style="scale < 1 ? { transform: `scale(${scale})`, width: `${100 / scale}%`, height: `${100 / scale}%` } : undefined"
|
||||
>
|
||||
<FormWindow
|
||||
v-for="(form, index) in state.forms"
|
||||
:key="form.root.id"
|
||||
:form="form.root"
|
||||
:modal="form.modal"
|
||||
:offset="index"
|
||||
:scale="scale"
|
||||
:act="session.act"
|
||||
/>
|
||||
</div>
|
||||
<MessageBoxDialog
|
||||
v-for="box in state.messageBoxes"
|
||||
:key="box.requestId"
|
||||
@@ -36,6 +77,14 @@ const viewing = ref<{ path: string; url: string } | null>(null)
|
||||
@reply="session.replyMessageBox(box.requestId, $event)"
|
||||
/>
|
||||
<div v-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
|
||||
<div class="desktop-tools">
|
||||
<span v-if="scale < 1" class="desktop-scale" title="窗体比运行区大,按比例缩小显示;点"放大"可以看得更清楚">
|
||||
缩放 {{ Math.round(scale * 100) }}%
|
||||
</span>
|
||||
<button type="button" :title="maximized ? '还原(Esc)' : '运行区占满整个网页'" @click="maximized = !maximized">
|
||||
{{ maximized ? '还原' : '放大' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<TrainingBoard :board="state.board" :playing="state.playing" @set="session.setBoard" @reset="session.resetBoard" />
|
||||
<div v-if="state.savedImages.length" class="photos">
|
||||
@@ -91,10 +140,72 @@ const viewing = ref<{ path: string; url: string } | null>(null)
|
||||
min-height: 320px;
|
||||
background: #2d6aa3;
|
||||
border-radius: 4px;
|
||||
/* 窗体比桌面区域大时可以滚动查看 */
|
||||
/* 窗体被拖到桌面外面时可以滚动查看 */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.desktop-canvas {
|
||||
position: absolute;
|
||||
inset: 0 auto auto 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.desktop-tools {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 100001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.desktop-scale {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: rgb(0 0 0 / 0.35);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.desktop-tools button {
|
||||
padding: 2px 10px;
|
||||
border: 1px solid rgb(255 255 255 / 0.6);
|
||||
border-radius: 3px;
|
||||
background: rgb(0 0 0 / 0.35);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.desktop-tools button:hover {
|
||||
background: rgb(0 0 0 / 0.55);
|
||||
}
|
||||
|
||||
/* 放大:桌面在左边占满高度,实训台、图片列表和输出在右边一栏 */
|
||||
.run-area.maximized {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 900;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 460px;
|
||||
grid-auto-rows: min-content;
|
||||
align-content: start;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.run-area.maximized .desktop {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / span 20;
|
||||
height: calc(100vh - 16px);
|
||||
}
|
||||
|
||||
.run-area.maximized > :not(.desktop) {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.desktop-hint {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// 按控件类型渲染一个 WinForms 控件及其子控件。
|
||||
// 用户操作只通过 act() 上报,由运行器按真实 WinForms 的顺序触发事件。
|
||||
import { computed, inject, ref, type CSSProperties } from 'vue'
|
||||
import { computed, inject, provide, ref, type CSSProperties } from 'vue'
|
||||
import type { ControlNode, UserAction } from '@iot/shared'
|
||||
import { boxCss, displayText } from './format'
|
||||
import { RUN_IMAGES, type ImageValue } from './images'
|
||||
import { AMBIENT_BACK_COLOR, AMBIENT_BACK_TYPES, boxCss, DEFAULT_BACK_COLOR, displayText } from './format'
|
||||
import { backgroundCss, RUN_IMAGES, type ImageValue } from './images'
|
||||
import DataGridViewControl from './DataGridViewControl.vue'
|
||||
import DateTimePickerControl from './DateTimePickerControl.vue'
|
||||
import LedControl from './LedControl.vue'
|
||||
@@ -19,12 +19,28 @@ const p = computed(() => props.node.props)
|
||||
// WinForms 中先加入 Controls 的控件在最上层,所以倒序渲染(后渲染的 DOM 元素在上面)
|
||||
const childrenTopLast = computed(() => [...props.node.children].reverse())
|
||||
const enabled = computed(() => p.value.Enabled !== false)
|
||||
const style = computed(() => boxCss(p.value, props.node.type === 'Label' && p.value.AutoSize === true))
|
||||
const runImages = inject(RUN_IMAGES, new Map<string, string>())
|
||||
const ambient = inject(AMBIENT_BACK_COLOR, ref(DEFAULT_BACK_COLOR))
|
||||
/** 这个控件实际的背景色,子控件没设置背景色时沿用 */
|
||||
const backColor = computed(() => (p.value.BackColor as string | null) ?? ambient.value)
|
||||
provide(AMBIENT_BACK_COLOR, backColor)
|
||||
const style = computed(() => ({
|
||||
...boxCss(p.value, props.node.type === 'Label' && p.value.AutoSize === true),
|
||||
...(AMBIENT_BACK_TYPES.has(props.node.type) ? { backgroundColor: backColor.value } : {}),
|
||||
...backgroundCss(p.value.BackgroundImage, p.value.BackgroundImageLayout, runImages),
|
||||
}))
|
||||
|
||||
function act(action: UserAction) {
|
||||
if (enabled.value) props.act(props.node.id, action)
|
||||
}
|
||||
|
||||
/** 鼠标按下、松开(教材项目 8 的方向键图片:按下转动摄像机、换成按下的图片) */
|
||||
function mouse(kind: 'mouseDown' | 'mouseUp', event: PointerEvent) {
|
||||
if (event.target !== event.currentTarget && !(event.target as HTMLElement).matches('img')) return
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
act({ action: kind, button: event.button === 2 ? 'right' : 'left', x: Math.round(event.clientX - rect.left), y: Math.round(event.clientY - rect.top) })
|
||||
}
|
||||
|
||||
// TextBox
|
||||
const passwordChar = computed(() => {
|
||||
if (p.value.UseSystemPasswordChar) return '●'
|
||||
@@ -54,7 +70,6 @@ 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 ? image.value.url ?? runImages.get(image.value.key) : undefined))
|
||||
// 设计器预览里不知道素材的尺寸,原样大小显示时让浏览器按图片本身的大小
|
||||
@@ -163,14 +178,18 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
|
||||
:class="`wf-picturebox-border-${p.BorderStyle ?? 'None'}`"
|
||||
:style="style"
|
||||
@click="act({ action: 'click' })"
|
||||
@pointerdown="mouse('mouseDown', $event)"
|
||||
@pointerup="mouse('mouseUp', $event)"
|
||||
>
|
||||
<img v-if="imageUrl" :src="imageUrl" :style="imageStyle" alt="" draggable="false" />
|
||||
<ControlView v-for="child in childrenTopLast" :key="child.id" :node="child" :act="props.act" />
|
||||
</div>
|
||||
|
||||
<DataGridViewControl v-else-if="node.type === 'DataGridView'" :node="node" :style="style" :enabled="enabled" :act="act" />
|
||||
|
||||
<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(教材里通常设为不可见,只用来播放声音) -->
|
||||
@@ -201,8 +220,10 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
|
||||
|
||||
<!-- GroupBox -->
|
||||
<div v-else-if="node.type === 'GroupBox'" class="wf-control wf-groupbox" :style="style">
|
||||
<div class="wf-groupbox-frame" />
|
||||
<span v-if="p.Text" class="wf-groupbox-caption">{{ displayText(p.Text) }}</span>
|
||||
<!-- 标题处的边框断开、透出底下的背景(教材项目 8 的分组框放在背景图上,白色标题) -->
|
||||
<fieldset class="wf-groupbox-frame">
|
||||
<legend v-if="p.Text" class="wf-groupbox-caption">{{ displayText(p.Text) }}</legend>
|
||||
</fieldset>
|
||||
<ControlView v-for="child in childrenTopLast" :key="child.id" :node="child" :act="props.act" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
<script lang="ts">
|
||||
let topZ = 1
|
||||
const nextZ = () => ++topZ
|
||||
|
||||
/** 第一个窗体离桌面左上角的距离,之后每个窗体错开的距离 */
|
||||
export const WINDOW_MARGIN = 24
|
||||
export const WINDOW_STEP = 32
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 一个 WinForms 窗体:标题栏 + 客户区。窗体可以拖动标题栏移动。
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, inject, provide, ref } from 'vue'
|
||||
import type { ControlNode, UserAction } from '@iot/shared'
|
||||
import ControlView from './ControlView.vue'
|
||||
import { displayText, fontCss, type FontValue, type SizeValue } from './format'
|
||||
import { AMBIENT_BACK_COLOR, DEFAULT_BACK_COLOR, displayText, fontCss, type FontValue, type SizeValue } from './format'
|
||||
import { backgroundCss, RUN_IMAGES } from './images'
|
||||
|
||||
const props = defineProps<{
|
||||
form: ControlNode
|
||||
modal: boolean
|
||||
offset: number
|
||||
/** 桌面的缩放比例(窗体太大时整体缩小),拖动时把鼠标移动的距离换算回去 */
|
||||
scale: number
|
||||
act: (id: string, action: UserAction) => void
|
||||
}>()
|
||||
|
||||
const p = computed(() => props.form.props)
|
||||
const runImages = inject(RUN_IMAGES, new Map<string, string>())
|
||||
provide(AMBIENT_BACK_COLOR, computed(() => (p.value.BackColor as string | null) ?? DEFAULT_BACK_COLOR))
|
||||
const borderless = computed(() => p.value.FormBorderStyle === 'None')
|
||||
// WinForms 中先加入 Controls 的控件在最上层,所以倒序渲染
|
||||
const childrenTopLast = computed(() => [...props.form.children].reverse())
|
||||
const clientSize = computed(() => (p.value.ClientSize as SizeValue | undefined) ?? { width: 300, height: 300 })
|
||||
|
||||
const position = ref({ x: 24 + props.offset * 32, y: 24 + props.offset * 32 })
|
||||
const position = ref({ x: WINDOW_MARGIN + props.offset * WINDOW_STEP, y: WINDOW_MARGIN + props.offset * WINDOW_STEP })
|
||||
|
||||
// 点哪个窗口,哪个窗口到最前面(所有窗体共用一个递增的层级)
|
||||
const zIndex = ref(nextZ())
|
||||
@@ -39,7 +48,7 @@ function startDrag(event: PointerEvent) {
|
||||
|
||||
function drag(event: PointerEvent) {
|
||||
if (!dragStart) return
|
||||
position.value = { x: dragStart.px + event.clientX - dragStart.x, y: dragStart.py + event.clientY - dragStart.y }
|
||||
position.value = { x: dragStart.px + (event.clientX - dragStart.x) / props.scale, y: dragStart.py + (event.clientY - dragStart.y) / props.scale }
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -69,6 +78,7 @@ function close() {
|
||||
color: (p.ForeColor as string | null) ?? undefined,
|
||||
backgroundColor: (p.BackColor as string | null) ?? undefined,
|
||||
...fontCss(p.Font as FontValue | null),
|
||||
...backgroundCss(p.BackgroundImage, p.BackgroundImageLayout, runImages),
|
||||
}"
|
||||
>
|
||||
<ControlView v-for="child in childrenTopLast" :key="child.id" :node="child" :act="act" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 把运行器发来的 WinForms 属性值转换成 CSS。
|
||||
// 运行器的序列化格式:Point → {x,y},Size → {width,height},
|
||||
// Color → CSS 颜色字符串(未设置时为 null,表示继承父控件),Font → FontValue。
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { CSSProperties, InjectionKey, Ref } from 'vue'
|
||||
|
||||
export interface PointValue {
|
||||
x: number
|
||||
@@ -45,6 +45,17 @@ export function fontCss(font: FontValue | null | undefined): CSSProperties {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 父控件的背景色(BackColor 是"环境属性":控件没设置时沿用父控件的)。
|
||||
* 父控件有背景图片时,没设置背景色的子控件在 WinForms 里画的是父控件的背景色而不是那张图,
|
||||
* 所以这些控件要自己涂上这个颜色;"transparent" 表示父控件是透明的(Color.Transparent),子控件也透明。
|
||||
*/
|
||||
export const AMBIENT_BACK_COLOR: InjectionKey<Readonly<Ref<string>>> = Symbol('ambientBackColor')
|
||||
export const DEFAULT_BACK_COLOR = '#f0f0f0'
|
||||
|
||||
/** 背景色沿用父控件的控件(按钮、文本框等有自己的默认底色) */
|
||||
export const AMBIENT_BACK_TYPES = new Set(['Label', 'Panel', 'GroupBox', 'CheckBox', 'RadioButton', 'PictureBox', 'TrackBar'])
|
||||
|
||||
/** 控件外框:绝对定位,坐标相对父控件的客户区 */
|
||||
export function boxCss(props: Record<string, unknown>, autoSize = false): CSSProperties {
|
||||
const location = (props.Location as PointValue | undefined) ?? { x: 0, y: 0 }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 运行时图片:运行器发来的图片数据由 RunSession 保存成 blob 网址,控件属性里只带图片编号。
|
||||
// 运行区 provide,ControlView inject;设计器预览没有运行时图片,取到空表。
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { CSSProperties, InjectionKey } from 'vue'
|
||||
|
||||
export const RUN_IMAGES: InjectionKey<ReadonlyMap<string, string>> = Symbol('runImages')
|
||||
|
||||
@@ -12,3 +12,23 @@ export interface ImageValue {
|
||||
/** 设计器预览里的项目资源:直接给出素材网址(运行时用 key) */
|
||||
url?: string
|
||||
}
|
||||
|
||||
/** 背景图片的 CSS:BackgroundImageLayout 为 Tile(默认,平铺)、None(左上角一张)、Center、Stretch、Zoom */
|
||||
export function backgroundCss(value: unknown, layout: unknown, images: ReadonlyMap<string, string>): CSSProperties {
|
||||
const image = value as ImageValue | null | undefined
|
||||
const url = image ? image.url ?? images.get(image.key) : undefined
|
||||
if (!url) return {}
|
||||
const base = { backgroundImage: `url("${url}")` }
|
||||
switch (layout) {
|
||||
case 'None':
|
||||
return { ...base, backgroundRepeat: 'no-repeat', backgroundPosition: '0 0' }
|
||||
case 'Center':
|
||||
return { ...base, backgroundRepeat: 'no-repeat', backgroundPosition: 'center' }
|
||||
case 'Stretch':
|
||||
return { ...base, backgroundRepeat: 'no-repeat', backgroundSize: '100% 100%' }
|
||||
case 'Zoom':
|
||||
return { ...base, backgroundRepeat: 'no-repeat', backgroundPosition: 'center', backgroundSize: 'contain' }
|
||||
default:
|
||||
return { ...base, backgroundRepeat: 'repeat', backgroundPosition: '0 0' }
|
||||
}
|
||||
}
|
||||
@@ -105,11 +105,20 @@
|
||||
border-color: #0078d7;
|
||||
}
|
||||
|
||||
.wf-textbox input[readonly],
|
||||
.wf-textbox textarea[readonly] {
|
||||
/* 文本框的背景色画在外层(控件的 BackColor 写在外层的 style 上),输入框本身透明 */
|
||||
.wf-textbox {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.wf-textbox:has(> [readonly]) {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.wf-textbox input,
|
||||
.wf-textbox textarea {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.wf-combobox input {
|
||||
padding-right: 18px;
|
||||
}
|
||||
@@ -391,6 +400,10 @@
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.wf-chart {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.wf-listbox {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
@@ -472,17 +485,21 @@
|
||||
|
||||
.wf-groupbox-frame {
|
||||
position: absolute;
|
||||
inset: 6px 0 0 0;
|
||||
inset: 0;
|
||||
margin: 0;
|
||||
padding: 0 6px;
|
||||
border: 1px solid #dcdcdc;
|
||||
border-radius: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 没有标题时边框的上沿和有标题时一样在 6px 处 */
|
||||
.wf-groupbox-frame:not(:has(legend)) {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.wf-groupbox-caption {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 0;
|
||||
padding: 0 2px;
|
||||
background: #f0f0f0;
|
||||
line-height: 12px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user