第 0 阶段:教材项目 2 任务 1 在网页中原样运行
- runtime:仿真 WinForms 子集(Form、Button、Label、TextBox、ComboBox、 GroupBox、Timer、MessageBox、界面线程消息循环、跨线程检查)、 教材设备类库(DeviceCtrlHelper、LED 控件)、运行器(常驻编译服务 + 运行模式) - server:Bun + Hono,WebSocket 会话,每次运行一个进程,消息转发 - web:Vue + Naive UI,WinForms 风格窗体渲染、MessageBox、实训台 LED 屏 - 端到端测试与 100 个程序并发的压力测试 - 教材逐任务摘要(docs/教材摘要) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
98be8331bc
71 files changed
+7919
No files matched your search
@@ -0,0 +1,56 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public static class Application
|
||||
{
|
||||
internal static readonly List<Form> OpenFormsList = new();
|
||||
private static bool _exitRequested;
|
||||
|
||||
public static FormCollection OpenForms { get; } = new();
|
||||
|
||||
public static string StartupPath => AppContext.BaseDirectory.TrimEnd('/', '\\');
|
||||
public static string ExecutablePath => System.IO.Path.Combine(StartupPath, "app.exe");
|
||||
public static string ProductName => "WindowsFormsApplication";
|
||||
|
||||
public static void EnableVisualStyles() { }
|
||||
|
||||
public static void SetCompatibleTextRenderingDefault(bool defaultValue) { }
|
||||
|
||||
public static void SetHighDpiMode(object mode) { }
|
||||
|
||||
/// <summary>显示主窗体并运行消息循环,直到主窗体关闭或调用 Application.Exit()</summary>
|
||||
public static void Run(Form mainForm)
|
||||
{
|
||||
mainForm.Show();
|
||||
SimHost.Ui.RunUntil(() => _exitRequested || mainForm.IsClosed);
|
||||
Exit();
|
||||
}
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
SimHost.Ui.RunUntil(() => _exitRequested);
|
||||
}
|
||||
|
||||
public static void Exit()
|
||||
{
|
||||
_exitRequested = true;
|
||||
foreach (var form in OpenFormsList.ToArray()) form.CloseCore(CloseReason.ApplicationExitCall);
|
||||
SimHost.Ui.Wake();
|
||||
}
|
||||
|
||||
public static void ExitThread() => Exit();
|
||||
|
||||
public static void DoEvents() => SimHost.Ui.DoEvents();
|
||||
|
||||
public sealed class FormCollection : IEnumerable<Form>
|
||||
{
|
||||
public int Count => OpenFormsList.Count;
|
||||
public Form this[int index] => OpenFormsList[index];
|
||||
public Form? this[string name] => OpenFormsList.Find(f => f.Name == name);
|
||||
public IEnumerator<Form> GetEnumerator() => OpenFormsList.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Nodes;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public abstract class ButtonBase : Control
|
||||
{
|
||||
private FlatStyle _flatStyle = FlatStyle.Standard;
|
||||
|
||||
public bool UseVisualStyleBackColor { get; set; } = true;
|
||||
public bool UseMnemonic { get; set; } = true;
|
||||
public ContentAlignment TextAlign { get; set; } = ContentAlignment.MiddleCenter;
|
||||
|
||||
public FlatStyle FlatStyle
|
||||
{
|
||||
get => _flatStyle;
|
||||
set => SetProp(ref _flatStyle, value, nameof(FlatStyle));
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["FlatStyle"] = _flatStyle.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class Button : ButtonBase, IButtonControl
|
||||
{
|
||||
protected override Size DefaultSize => new(75, 23);
|
||||
|
||||
public DialogResult DialogResult { get; set; }
|
||||
|
||||
public void NotifyDefault(bool value) { }
|
||||
|
||||
public void PerformClick()
|
||||
{
|
||||
if (Enabled && Visible) OnClick(EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected override void OnClick(EventArgs e)
|
||||
{
|
||||
// 按钮设置了 DialogResult 时,点击会把结果交给所在的模态窗体并关闭它
|
||||
if (DialogResult != DialogResult.None && FindForm() is { } form) form.DialogResult = DialogResult;
|
||||
base.OnClick(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public class ComboBox : Control
|
||||
{
|
||||
private int _selectedIndex = -1;
|
||||
private ComboBoxStyle _dropDownStyle = ComboBoxStyle.DropDown;
|
||||
|
||||
public ComboBox() => Items = new ObjectCollection(this);
|
||||
|
||||
protected override Size DefaultSize => new(121, 20);
|
||||
|
||||
public ObjectCollection Items { get; }
|
||||
|
||||
public bool FormattingEnabled { get; set; }
|
||||
public int MaxDropDownItems { get; set; } = 8;
|
||||
public int DropDownWidth { get; set; } = 121;
|
||||
public int DropDownHeight { get; set; } = 106;
|
||||
public bool Sorted { get; set; }
|
||||
public bool DroppedDown { get; set; }
|
||||
|
||||
public ComboBoxStyle DropDownStyle
|
||||
{
|
||||
get => _dropDownStyle;
|
||||
set => SetProp(ref _dropDownStyle, value, nameof(DropDownStyle));
|
||||
}
|
||||
|
||||
public int SelectedIndex
|
||||
{
|
||||
get => _selectedIndex;
|
||||
set
|
||||
{
|
||||
if (value < -1 || value >= Items.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(value), $"“{value}”的值对于“SelectedIndex”无效。");
|
||||
if (value == _selectedIndex) return;
|
||||
SetProp(ref _selectedIndex, value, nameof(SelectedIndex));
|
||||
// 选中项变化时 Text 随之变化(先 TextChanged,再 SelectedIndexChanged,与 WinForms 一致)
|
||||
Text = value >= 0 ? Items[value]?.ToString() ?? "" : "";
|
||||
OnSelectedIndexChanged(EventArgs.Empty);
|
||||
OnSelectedValueChanged(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public object? SelectedItem
|
||||
{
|
||||
get => _selectedIndex >= 0 ? Items[_selectedIndex] : null;
|
||||
set => SelectedIndex = value == null ? -1 : Items.IndexOf(value);
|
||||
}
|
||||
|
||||
public string SelectedText { get; set; } = "";
|
||||
|
||||
public event EventHandler? SelectedIndexChanged;
|
||||
public event EventHandler? SelectedValueChanged;
|
||||
public event EventHandler? DropDown;
|
||||
public event EventHandler? DropDownClosed;
|
||||
|
||||
protected virtual void OnSelectedIndexChanged(EventArgs e) => SelectedIndexChanged?.Invoke(this, e);
|
||||
protected virtual void OnSelectedValueChanged(EventArgs e) => SelectedValueChanged?.Invoke(this, e);
|
||||
protected virtual void OnDropDown(EventArgs e) => DropDown?.Invoke(this, e);
|
||||
protected virtual void OnDropDownClosed(EventArgs e) => DropDownClosed?.Invoke(this, e);
|
||||
|
||||
public void BeginUpdate() { }
|
||||
public void EndUpdate() { }
|
||||
|
||||
protected override void OnUserAction(UserAction action)
|
||||
{
|
||||
switch (action.Action)
|
||||
{
|
||||
case "dropDown":
|
||||
// 教材在 Click 事件里刷新串口列表,所以 Click 必须在列表展开之前触发
|
||||
OnClick(EventArgs.Empty);
|
||||
OnDropDown(EventArgs.Empty);
|
||||
break;
|
||||
case "select":
|
||||
if (action.Index >= 0 && action.Index < Items.Count) SelectedIndex = action.Index;
|
||||
OnDropDownClosed(EventArgs.Empty);
|
||||
break;
|
||||
case "text" when _dropDownStyle != ComboBoxStyle.DropDownList:
|
||||
SetTextFromUser(action.Value ?? "");
|
||||
var match = Items.IndexOf(Text);
|
||||
if (match != _selectedIndex) _selectedIndex = match;
|
||||
break;
|
||||
default:
|
||||
base.OnUserAction(action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal void ItemsChanged()
|
||||
{
|
||||
if (_selectedIndex >= Items.Count) _selectedIndex = -1;
|
||||
NotifyChanged(nameof(Items), Items.Select(i => i?.ToString() ?? "").ToArray());
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["Items"] = PropValue.From(Items.Select(i => i?.ToString() ?? "").ToArray());
|
||||
props["SelectedIndex"] = _selectedIndex;
|
||||
props["DropDownStyle"] = _dropDownStyle.ToString();
|
||||
}
|
||||
|
||||
public class ObjectCollection : IList, IEnumerable<object?>
|
||||
{
|
||||
private readonly List<object?> _items = new();
|
||||
private readonly ComboBox _owner;
|
||||
|
||||
public ObjectCollection(ComboBox owner) => _owner = owner;
|
||||
|
||||
public int Count => _items.Count;
|
||||
public bool IsReadOnly => false;
|
||||
bool IList.IsFixedSize => false;
|
||||
bool ICollection.IsSynchronized => false;
|
||||
object ICollection.SyncRoot => this;
|
||||
|
||||
public object? this[int index]
|
||||
{
|
||||
get => _items[index];
|
||||
set
|
||||
{
|
||||
_items[index] = value;
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int Add(object? item)
|
||||
{
|
||||
if (item == null) throw new ArgumentNullException(nameof(item), "值不能为 null。");
|
||||
_items.Add(item);
|
||||
_owner.ItemsChanged();
|
||||
return _items.Count - 1;
|
||||
}
|
||||
|
||||
public void AddRange(object[] items)
|
||||
{
|
||||
_items.AddRange(items);
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public void Insert(int index, object? item)
|
||||
{
|
||||
_items.Insert(index, item);
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public void Remove(object? item)
|
||||
{
|
||||
if (_items.Remove(item)) _owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
_items.RemoveAt(index);
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_items.Clear();
|
||||
_owner.SelectedIndex = -1;
|
||||
_owner.ItemsChanged();
|
||||
}
|
||||
|
||||
public bool Contains(object? item) => _items.Contains(item);
|
||||
public int IndexOf(object? item) => _items.IndexOf(item);
|
||||
public void CopyTo(Array array, int index) => ((ICollection)_items).CopyTo(array, index);
|
||||
public IEnumerator<object?> GetEnumerator() => _items.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public class ScrollableControl : Control
|
||||
{
|
||||
public bool AutoScroll { get; set; }
|
||||
}
|
||||
|
||||
public class ContainerControl : ScrollableControl
|
||||
{
|
||||
public SizeF AutoScaleDimensions { get; set; }
|
||||
public AutoScaleMode AutoScaleMode { get; set; } = AutoScaleMode.Inherit;
|
||||
public Control? ActiveControl { get; set; }
|
||||
}
|
||||
|
||||
public class UserControl : ContainerControl
|
||||
{
|
||||
protected override Size DefaultSize => new(150, 150);
|
||||
|
||||
public event EventHandler? Load;
|
||||
|
||||
protected virtual void OnLoad(EventArgs e) => Load?.Invoke(this, e);
|
||||
|
||||
private bool _loaded;
|
||||
|
||||
/// <summary>UserControl 的 Load 在第一次随窗体显示时触发</summary>
|
||||
internal void RaiseLoadOnce()
|
||||
{
|
||||
if (_loaded) return;
|
||||
_loaded = true;
|
||||
OnLoad(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public class Panel : ScrollableControl
|
||||
{
|
||||
private BorderStyle _borderStyle;
|
||||
|
||||
protected override Size DefaultSize => new(200, 100);
|
||||
|
||||
public BorderStyle BorderStyle
|
||||
{
|
||||
get => _borderStyle;
|
||||
set => SetProp(ref _borderStyle, value, nameof(BorderStyle));
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["BorderStyle"] = _borderStyle.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupBox : Control
|
||||
{
|
||||
protected override Size DefaultSize => new(200, 100);
|
||||
|
||||
public FlatStyle FlatStyle { get; set; } = FlatStyle.Standard;
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Nodes;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// 仿真控件的基类。属性保存在本进程里;控件所在窗体显示后,属性变化以 patch 消息发给浏览器渲染。
|
||||
/// Font、BackColor、ForeColor 是"环境属性":没有显式设置时沿用父控件的值,只把显式设置的值发给浏览器。
|
||||
/// </summary>
|
||||
public class Control : Component, IWin32Window
|
||||
{
|
||||
private static readonly Font DefaultFontValue = new("宋体", 9F);
|
||||
|
||||
private string _name = "";
|
||||
private string _text = "";
|
||||
private Point _location;
|
||||
private Size _size;
|
||||
private bool _visible = true;
|
||||
private bool _enabled = true;
|
||||
private Color _backColor;
|
||||
private Color _foreColor;
|
||||
private Font? _font;
|
||||
private bool _autoSize;
|
||||
private DockStyle _dock;
|
||||
private AnchorStyles _anchor = AnchorStyles.Top | AnchorStyles.Left;
|
||||
private Control? _parent;
|
||||
|
||||
public Control()
|
||||
{
|
||||
SimId = SimHost.NewControlId();
|
||||
SimHost.Register(this);
|
||||
_size = DefaultSize;
|
||||
Controls = CreateControlsInstance();
|
||||
}
|
||||
|
||||
public Control(string text) : this() => _text = text ?? "";
|
||||
|
||||
/// <summary>平台内部使用:控件在浏览器端的编号</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public string SimId { get; }
|
||||
|
||||
/// <summary>为 true 时,从非界面线程修改已显示控件的属性会抛出异常(和在 VS 里按 F5 调试时一致)</summary>
|
||||
public static bool CheckForIllegalCrossThreadCalls { get; set; } = true;
|
||||
|
||||
public static Color DefaultBackColor => Color.FromArgb(240, 240, 240);
|
||||
public static Color DefaultForeColor => Color.Black;
|
||||
public static Font DefaultFont => DefaultFontValue;
|
||||
|
||||
protected virtual Size DefaultSize => Size.Empty;
|
||||
|
||||
// ---------- 层级 ----------
|
||||
|
||||
public ControlCollection Controls { get; }
|
||||
|
||||
protected virtual ControlCollection CreateControlsInstance() => new(this);
|
||||
|
||||
public Control? Parent
|
||||
{
|
||||
get => _parent;
|
||||
set
|
||||
{
|
||||
if (value == _parent) return;
|
||||
_parent?.Controls.Remove(this);
|
||||
value?.Controls.Add(this);
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetParentInternal(Control? parent) => _parent = parent;
|
||||
|
||||
public Form? FindForm()
|
||||
{
|
||||
for (var c = _parent; c != null; c = c._parent)
|
||||
if (c is Form form) return form;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>控件已经显示在浏览器里(所在窗体已打开)时,属性变化才需要发送</summary>
|
||||
internal virtual bool IsAttached
|
||||
{
|
||||
get
|
||||
{
|
||||
var root = this;
|
||||
while (root._parent != null) root = root._parent;
|
||||
return root is Form { IsOpenInBrowser: true };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 常用属性 ----------
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => _name = value ?? "";
|
||||
}
|
||||
|
||||
public virtual string Text
|
||||
{
|
||||
get => _text;
|
||||
set
|
||||
{
|
||||
value ??= "";
|
||||
if (_text == value) return;
|
||||
EnsureThreadAccess();
|
||||
_text = value;
|
||||
NotifyChanged(nameof(Text), value);
|
||||
OnTextChanged(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>来自浏览器输入的文字:只修改值并触发事件,不再回传给浏览器(避免打字时光标跳动)</summary>
|
||||
internal void SetTextFromUser(string value)
|
||||
{
|
||||
if (_text == value) return;
|
||||
_text = value;
|
||||
OnTextChanged(EventArgs.Empty);
|
||||
}
|
||||
|
||||
public Point Location
|
||||
{
|
||||
get => _location;
|
||||
set => SetProp(ref _location, value, nameof(Location));
|
||||
}
|
||||
|
||||
public Size Size
|
||||
{
|
||||
get => _size;
|
||||
set => SetProp(ref _size, value, nameof(Size));
|
||||
}
|
||||
|
||||
public int Left { get => _location.X; set => Location = new Point(value, _location.Y); }
|
||||
public int Top { get => _location.Y; set => Location = new Point(_location.X, value); }
|
||||
public int Width { get => _size.Width; set => Size = new Size(value, _size.Height); }
|
||||
public int Height { get => _size.Height; set => Size = new Size(_size.Width, value); }
|
||||
public int Right => Left + Width;
|
||||
public int Bottom => Top + Height;
|
||||
|
||||
public Rectangle Bounds
|
||||
{
|
||||
get => new(_location, _size);
|
||||
set
|
||||
{
|
||||
Location = value.Location;
|
||||
Size = value.Size;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Size ClientSize
|
||||
{
|
||||
get => Size;
|
||||
set => Size = value;
|
||||
}
|
||||
|
||||
public bool Visible
|
||||
{
|
||||
get => _visible;
|
||||
set
|
||||
{
|
||||
if (SetProp(ref _visible, value, nameof(Visible))) OnVisibleChanged(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
if (SetProp(ref _enabled, value, nameof(Enabled))) OnEnabledChanged(EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Color BackColor
|
||||
{
|
||||
get => !_backColor.IsEmpty ? _backColor : _parent?.BackColor ?? DefaultBackColor;
|
||||
set => SetProp(ref _backColor, value, nameof(BackColor));
|
||||
}
|
||||
|
||||
public virtual Color ForeColor
|
||||
{
|
||||
get => !_foreColor.IsEmpty ? _foreColor : _parent?.ForeColor ?? DefaultForeColor;
|
||||
set => SetProp(ref _foreColor, value, nameof(ForeColor));
|
||||
}
|
||||
|
||||
public virtual Font Font
|
||||
{
|
||||
get => _font ?? _parent?.Font ?? DefaultFont;
|
||||
set => SetProp(ref _font, value, nameof(Font));
|
||||
}
|
||||
|
||||
public virtual bool AutoSize
|
||||
{
|
||||
get => _autoSize;
|
||||
set => SetProp(ref _autoSize, value, nameof(AutoSize));
|
||||
}
|
||||
|
||||
public DockStyle Dock
|
||||
{
|
||||
get => _dock;
|
||||
set => SetProp(ref _dock, value, nameof(Dock));
|
||||
}
|
||||
|
||||
public AnchorStyles Anchor
|
||||
{
|
||||
get => _anchor;
|
||||
set => SetProp(ref _anchor, value, nameof(Anchor));
|
||||
}
|
||||
|
||||
// 以下属性只保存值,暂不影响浏览器显示
|
||||
public int TabIndex { get; set; }
|
||||
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; }
|
||||
public bool CausesValidation { get; set; } = true;
|
||||
|
||||
public bool Focused { get; private set; }
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public IntPtr Handle => new(int.Parse(SimId));
|
||||
|
||||
public bool IsHandleCreated => IsAttached;
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
// ---------- 事件 ----------
|
||||
|
||||
public event EventHandler? Click;
|
||||
public event EventHandler? DoubleClick;
|
||||
public event MouseEventHandler? MouseClick;
|
||||
public event MouseEventHandler? MouseDown;
|
||||
public event MouseEventHandler? MouseUp;
|
||||
public event EventHandler? MouseEnter;
|
||||
public event EventHandler? MouseLeave;
|
||||
public event EventHandler? TextChanged;
|
||||
public event EventHandler? VisibleChanged;
|
||||
public event EventHandler? EnabledChanged;
|
||||
public event EventHandler? GotFocus;
|
||||
public event EventHandler? LostFocus;
|
||||
public event EventHandler? Enter;
|
||||
public event EventHandler? Leave;
|
||||
|
||||
protected virtual void OnClick(EventArgs e) => Click?.Invoke(this, e);
|
||||
protected virtual void OnDoubleClick(EventArgs e) => DoubleClick?.Invoke(this, e);
|
||||
protected virtual void OnMouseClick(MouseEventArgs e) => MouseClick?.Invoke(this, e);
|
||||
protected virtual void OnMouseDown(MouseEventArgs e) => MouseDown?.Invoke(this, e);
|
||||
protected virtual void OnMouseUp(MouseEventArgs e) => MouseUp?.Invoke(this, e);
|
||||
protected virtual void OnMouseEnter(EventArgs e) => MouseEnter?.Invoke(this, e);
|
||||
protected virtual void OnMouseLeave(EventArgs e) => MouseLeave?.Invoke(this, e);
|
||||
protected virtual void OnTextChanged(EventArgs e) => TextChanged?.Invoke(this, e);
|
||||
protected virtual void OnVisibleChanged(EventArgs e) => VisibleChanged?.Invoke(this, e);
|
||||
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 OnEnter(EventArgs e) => Enter?.Invoke(this, e);
|
||||
protected virtual void OnLeave(EventArgs e) => Leave?.Invoke(this, e);
|
||||
|
||||
/// <summary>平台内部使用:在界面线程上处理浏览器上报的用户操作</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public void PerformUserAction(UserAction action)
|
||||
{
|
||||
if (!Enabled || !Visible) return;
|
||||
OnUserAction(action);
|
||||
}
|
||||
|
||||
/// <summary>按真实 WinForms 的顺序,把一个用户操作翻译成事件</summary>
|
||||
protected virtual void OnUserAction(UserAction action)
|
||||
{
|
||||
switch (action.Action)
|
||||
{
|
||||
case "click":
|
||||
var args = new MouseEventArgs(MouseButtons.Left, 1, action.X, action.Y, 0);
|
||||
OnClick(args);
|
||||
OnMouseClick(args);
|
||||
break;
|
||||
case "mouseDown":
|
||||
OnMouseDown(new MouseEventArgs(ToMouseButtons(action.Button), 1, action.X, action.Y, 0));
|
||||
break;
|
||||
case "mouseUp":
|
||||
OnMouseUp(new MouseEventArgs(ToMouseButtons(action.Button), 1, action.X, action.Y, 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static MouseButtons ToMouseButtons(string button) => button == "right" ? MouseButtons.Right : MouseButtons.Left;
|
||||
|
||||
// ---------- 方法 ----------
|
||||
|
||||
public void Show() => Visible = true;
|
||||
public void Hide() => Visible = false;
|
||||
public bool Focus() => true;
|
||||
public void Select() { }
|
||||
public void BringToFront() { }
|
||||
public void SendToBack() { }
|
||||
public void Refresh() { }
|
||||
public void Invalidate() { }
|
||||
public void Update() { }
|
||||
public void SuspendLayout() { }
|
||||
public void ResumeLayout() { }
|
||||
public void ResumeLayout(bool performLayout) { }
|
||||
public void PerformLayout() { }
|
||||
|
||||
public bool InvokeRequired => !SimHost.Ui.IsUiThread;
|
||||
|
||||
public object? Invoke(Delegate method) => Invoke(method, Array.Empty<object>());
|
||||
|
||||
public object? Invoke(Delegate method, params object?[]? args)
|
||||
{
|
||||
object? result = null;
|
||||
SimHost.Ui.Send(() => result = method.DynamicInvoke(args));
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Invoke(Action method) => SimHost.Ui.Send(method);
|
||||
|
||||
public IAsyncResult BeginInvoke(Delegate method) => BeginInvoke(method, Array.Empty<object>());
|
||||
|
||||
public IAsyncResult BeginInvoke(Delegate method, params object?[]? args)
|
||||
{
|
||||
var result = new SimpleAsyncResult();
|
||||
SimHost.Ui.Post(() =>
|
||||
{
|
||||
try { result.Value = method.DynamicInvoke(args); }
|
||||
finally { result.Complete(); }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public object? EndInvoke(IAsyncResult asyncResult)
|
||||
{
|
||||
var result = (SimpleAsyncResult)asyncResult;
|
||||
result.AsyncWaitHandle.WaitOne();
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && !IsDisposed)
|
||||
{
|
||||
IsDisposed = true;
|
||||
_parent?.Controls.Remove(this);
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
// ---------- 属性变化与快照 ----------
|
||||
|
||||
/// <summary>修改字段并在需要时通知浏览器;返回值表示是否真的改变</summary>
|
||||
protected bool SetProp<T>(ref T field, T value, string prop)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
|
||||
EnsureThreadAccess();
|
||||
field = value;
|
||||
NotifyChanged(prop, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void NotifyChanged(string prop, object? value)
|
||||
{
|
||||
if (IsAttached) SimHost.QueueChange(this, prop, value);
|
||||
}
|
||||
|
||||
protected void EnsureThreadAccess()
|
||||
{
|
||||
if (CheckForIllegalCrossThreadCalls && IsAttached && !SimHost.Ui.IsUiThread)
|
||||
throw new InvalidOperationException($"线程间操作无效: 从不是创建控件“{Name}”的线程访问它。");
|
||||
}
|
||||
|
||||
/// <summary>平台内部使用:生成发给浏览器的控件树</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public JsonObject Snapshot()
|
||||
{
|
||||
var props = new JsonObject();
|
||||
WriteProps(props);
|
||||
var children = new JsonArray();
|
||||
foreach (var child in Controls) children.Add(child.Snapshot());
|
||||
return new JsonObject
|
||||
{
|
||||
["id"] = SimId,
|
||||
["type"] = ClientType,
|
||||
["name"] = Name,
|
||||
["props"] = props,
|
||||
["children"] = children,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>写入浏览器渲染需要的属性;子类追加自己的属性</summary>
|
||||
protected virtual void WriteProps(JsonObject props)
|
||||
{
|
||||
props["Text"] = Text;
|
||||
props["Location"] = PropValue.From(_location);
|
||||
props["Size"] = PropValue.From(_size);
|
||||
props["Visible"] = _visible;
|
||||
props["Enabled"] = _enabled;
|
||||
props["BackColor"] = PropValue.ColorCss(_backColor);
|
||||
props["ForeColor"] = PropValue.ColorCss(_foreColor);
|
||||
props["Font"] = PropValue.From(_font);
|
||||
props["AutoSize"] = _autoSize;
|
||||
props["Dock"] = _dock.ToString();
|
||||
}
|
||||
|
||||
/// <summary>浏览器按这个类型名选择渲染方式。学生自己继承的控件按最近的仿真库基类渲染。</summary>
|
||||
private string ClientType
|
||||
{
|
||||
get
|
||||
{
|
||||
for (var type = GetType(); type != null; type = type.BaseType)
|
||||
{
|
||||
var attribute = type.GetCustomAttribute<ClientTypeAttribute>(inherit: false);
|
||||
if (attribute != null) return attribute.Name;
|
||||
if (type.Assembly == typeof(Control).Assembly) return type.Name;
|
||||
}
|
||||
return nameof(Control);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SimpleAsyncResult : IAsyncResult
|
||||
{
|
||||
private readonly System.Threading.ManualResetEvent _done = new(false);
|
||||
public object? Value { get; set; }
|
||||
public object? AsyncState => null;
|
||||
public System.Threading.WaitHandle AsyncWaitHandle => _done;
|
||||
public bool CompletedSynchronously => false;
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
IsCompleted = true;
|
||||
_done.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public class ControlCollection : IEnumerable<Control>
|
||||
{
|
||||
private readonly List<Control> _items = new();
|
||||
|
||||
public ControlCollection(Control owner) => Owner = owner;
|
||||
|
||||
public Control Owner { get; }
|
||||
public int Count => _items.Count;
|
||||
public Control this[int index] => _items[index];
|
||||
public Control? this[string key] => _items.Find(c => string.Equals(c.Name, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public virtual void Add(Control? value)
|
||||
{
|
||||
if (value == null || value.Parent == Owner) return;
|
||||
value.Parent?.Controls.Remove(value);
|
||||
_items.Add(value);
|
||||
value.SetParentInternal(Owner);
|
||||
if (Owner.IsAttached)
|
||||
{
|
||||
SimHost.Send(new JsonObject
|
||||
{
|
||||
["type"] = "controlAdded",
|
||||
["parentId"] = Owner.SimId,
|
||||
["index"] = _items.Count - 1,
|
||||
["node"] = value.Snapshot(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void AddRange(Control[] controls)
|
||||
{
|
||||
foreach (var control in controls) Add(control);
|
||||
}
|
||||
|
||||
public void Remove(Control? value)
|
||||
{
|
||||
if (value == null || !_items.Remove(value)) return;
|
||||
var wasAttached = Owner.IsAttached;
|
||||
value.SetParentInternal(null);
|
||||
if (wasAttached) SimHost.Send(new JsonObject { ["type"] = "controlRemoved", ["id"] = value.SimId });
|
||||
}
|
||||
|
||||
public void RemoveAt(int index) => Remove(_items[index]);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var control in _items.ToArray()) Remove(control);
|
||||
}
|
||||
|
||||
public bool Contains(Control? control) => control != null && _items.Contains(control);
|
||||
public int IndexOf(Control? control) => control == null ? -1 : _items.IndexOf(control);
|
||||
|
||||
public IEnumerator<Control> GetEnumerator() => _items.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>仿真库内部使用:指定浏览器端的渲染类型名(教材控件库的控件用完整类名)</summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class ClientTypeAttribute : Attribute
|
||||
{
|
||||
public ClientTypeAttribute(string name) => Name = name;
|
||||
|
||||
public string Name { get; }
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// WinForms 中设计器代码和教材代码会用到的枚举与结构体,取值与 .NET Framework 一致。
|
||||
using System;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public enum AutoScaleMode { None, Font, Dpi, Inherit }
|
||||
|
||||
public enum FormBorderStyle { None, FixedSingle, Fixed3D, FixedDialog, Sizable, FixedToolWindow, SizableToolWindow }
|
||||
|
||||
public enum FormStartPosition { Manual, CenterScreen, WindowsDefaultLocation, WindowsDefaultBounds, CenterParent }
|
||||
|
||||
public enum FormWindowState { Normal, Minimized, Maximized }
|
||||
|
||||
public enum DialogResult { None, OK, Cancel, Abort, Retry, Ignore, Yes, No, TryAgain = 10, Continue = 11 }
|
||||
|
||||
public enum MessageBoxButtons { OK, OKCancel, AbortRetryIgnore, YesNoCancel, YesNo, RetryCancel, CancelTryContinue }
|
||||
|
||||
public enum MessageBoxIcon
|
||||
{
|
||||
None = 0,
|
||||
Hand = 16,
|
||||
Stop = 16,
|
||||
Error = 16,
|
||||
Question = 32,
|
||||
Exclamation = 48,
|
||||
Warning = 48,
|
||||
Asterisk = 64,
|
||||
Information = 64,
|
||||
}
|
||||
|
||||
public enum MessageBoxDefaultButton { Button1 = 0, Button2 = 256, Button3 = 512, Button4 = 768 }
|
||||
|
||||
public enum FlatStyle { Flat, Popup, Standard, System }
|
||||
|
||||
public enum BorderStyle { None, FixedSingle, Fixed3D }
|
||||
|
||||
public enum ScrollBars { None, Horizontal, Vertical, Both }
|
||||
|
||||
public enum ComboBoxStyle { Simple, DropDown, DropDownList }
|
||||
|
||||
public enum DockStyle { None, Top, Bottom, Left, Right, Fill }
|
||||
|
||||
[Flags]
|
||||
public enum AnchorStyles { None = 0, Top = 1, Bottom = 2, Left = 4, Right = 8 }
|
||||
|
||||
public enum ImeMode { Inherit = -1, NoControl = 0, On = 1, Off = 2, Disable = 3 }
|
||||
|
||||
public enum RightToLeft { No, Yes, Inherit }
|
||||
|
||||
[Flags]
|
||||
public enum MouseButtons { None = 0, Left = 0x100000, Right = 0x200000, Middle = 0x400000, XButton1 = 0x800000, XButton2 = 0x1000000 }
|
||||
|
||||
public enum CloseReason { None, WindowsShutDown, MdiFormClosing, UserClosing, TaskManagerClosing, FormOwnerClosing, ApplicationExitCall }
|
||||
|
||||
public struct Padding : IEquatable<Padding>
|
||||
{
|
||||
public static readonly Padding Empty = new(0);
|
||||
|
||||
public Padding(int all) : this(all, all, all, all) { }
|
||||
|
||||
public Padding(int left, int top, int right, int bottom)
|
||||
{
|
||||
Left = left;
|
||||
Top = top;
|
||||
Right = right;
|
||||
Bottom = bottom;
|
||||
}
|
||||
|
||||
public int Left { get; set; }
|
||||
public int Top { get; set; }
|
||||
public int Right { get; set; }
|
||||
public int Bottom { get; set; }
|
||||
public int All
|
||||
{
|
||||
get => Left == Top && Top == Right && Right == Bottom ? Left : -1;
|
||||
set => Left = Top = Right = Bottom = value;
|
||||
}
|
||||
public int Horizontal => Left + Right;
|
||||
public int Vertical => Top + Bottom;
|
||||
|
||||
public bool Equals(Padding other) => Left == other.Left && Top == other.Top && Right == other.Right && Bottom == other.Bottom;
|
||||
public override bool Equals(object? obj) => obj is Padding other && Equals(other);
|
||||
public override int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom);
|
||||
public static bool operator ==(Padding a, Padding b) => a.Equals(b);
|
||||
public static bool operator !=(Padding a, Padding b) => !a.Equals(b);
|
||||
public override string ToString() => $"{{Left={Left},Top={Top},Right={Right},Bottom={Bottom}}}";
|
||||
}
|
||||
|
||||
/// <summary>窗口句柄接口;MessageBox.Show(owner, ...) 的 owner 参数类型</summary>
|
||||
public interface IWin32Window
|
||||
{
|
||||
IntPtr Handle { get; }
|
||||
}
|
||||
|
||||
public interface IButtonControl
|
||||
{
|
||||
DialogResult DialogResult { get; set; }
|
||||
void NotifyDefault(bool value);
|
||||
void PerformClick();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public class MouseEventArgs : EventArgs
|
||||
{
|
||||
public MouseEventArgs(MouseButtons button, int clicks, int x, int y, int delta)
|
||||
{
|
||||
Button = button;
|
||||
Clicks = clicks;
|
||||
X = x;
|
||||
Y = y;
|
||||
Delta = delta;
|
||||
}
|
||||
|
||||
public MouseButtons Button { get; }
|
||||
public int Clicks { get; }
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public int Delta { get; }
|
||||
public System.Drawing.Point Location => new(X, Y);
|
||||
}
|
||||
|
||||
public delegate void MouseEventHandler(object? sender, MouseEventArgs e);
|
||||
|
||||
public class FormClosingEventArgs : CancelEventArgs
|
||||
{
|
||||
public FormClosingEventArgs(CloseReason closeReason, bool cancel) : base(cancel) => CloseReason = closeReason;
|
||||
|
||||
public CloseReason CloseReason { get; }
|
||||
}
|
||||
|
||||
public delegate void FormClosingEventHandler(object? sender, FormClosingEventArgs e);
|
||||
|
||||
public class FormClosedEventArgs : EventArgs
|
||||
{
|
||||
public FormClosedEventArgs(CloseReason closeReason) => CloseReason = closeReason;
|
||||
|
||||
public CloseReason CloseReason { get; }
|
||||
}
|
||||
|
||||
public delegate void FormClosedEventHandler(object? sender, FormClosedEventArgs e);
|
||||
@@ -0,0 +1,208 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Nodes;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// 窗体。Show() 时先触发 Load,再把整个控件树发给浏览器;Close() 依次触发 FormClosing、FormClosed。
|
||||
/// </summary>
|
||||
public class Form : ContainerControl
|
||||
{
|
||||
// Windows 10 下可调边框窗体的标题栏和边框尺寸,Size = ClientSize + 边框
|
||||
private static readonly Size NonClientSize = new(16, 39);
|
||||
|
||||
private Size _clientSize = new(284, 261);
|
||||
private FormBorderStyle _formBorderStyle = FormBorderStyle.Sizable;
|
||||
private bool _maximizeBox = true;
|
||||
private bool _minimizeBox = true;
|
||||
private bool _controlBox = true;
|
||||
private bool _loaded;
|
||||
private bool _closed;
|
||||
private bool _modal;
|
||||
private DialogResult _dialogResult;
|
||||
|
||||
public Form()
|
||||
{
|
||||
base.Visible = false;
|
||||
}
|
||||
|
||||
internal bool IsOpenInBrowser { get; private set; }
|
||||
|
||||
public override Size ClientSize
|
||||
{
|
||||
get => _clientSize;
|
||||
set => SetProp(ref _clientSize, value, nameof(ClientSize));
|
||||
}
|
||||
|
||||
public new Size Size
|
||||
{
|
||||
get => _clientSize + NonClientSize;
|
||||
set => ClientSize = value - NonClientSize;
|
||||
}
|
||||
|
||||
public FormBorderStyle FormBorderStyle
|
||||
{
|
||||
get => _formBorderStyle;
|
||||
set => SetProp(ref _formBorderStyle, value, nameof(FormBorderStyle));
|
||||
}
|
||||
|
||||
public bool MaximizeBox
|
||||
{
|
||||
get => _maximizeBox;
|
||||
set => SetProp(ref _maximizeBox, value, nameof(MaximizeBox));
|
||||
}
|
||||
|
||||
public bool MinimizeBox
|
||||
{
|
||||
get => _minimizeBox;
|
||||
set => SetProp(ref _minimizeBox, value, nameof(MinimizeBox));
|
||||
}
|
||||
|
||||
public bool ControlBox
|
||||
{
|
||||
get => _controlBox;
|
||||
set => SetProp(ref _controlBox, value, nameof(ControlBox));
|
||||
}
|
||||
|
||||
public FormStartPosition StartPosition { get; set; } = FormStartPosition.WindowsDefaultLocation;
|
||||
public FormWindowState WindowState { get; set; }
|
||||
public bool ShowInTaskbar { get; set; } = true;
|
||||
public bool TopMost { get; set; }
|
||||
public bool KeyPreview { get; set; }
|
||||
public double Opacity { get; set; } = 1;
|
||||
public IButtonControl? AcceptButton { get; set; }
|
||||
public IButtonControl? CancelButton { get; set; }
|
||||
public Form? Owner { get; set; }
|
||||
public bool Modal => _modal;
|
||||
|
||||
public DialogResult DialogResult
|
||||
{
|
||||
get => _dialogResult;
|
||||
set
|
||||
{
|
||||
_dialogResult = value;
|
||||
// 模态窗体设置 DialogResult 后自动关闭
|
||||
if (_modal && value != DialogResult.None) Close();
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler? Load;
|
||||
public event EventHandler? Shown;
|
||||
public event EventHandler? Activated;
|
||||
public event FormClosingEventHandler? FormClosing;
|
||||
public event FormClosedEventHandler? FormClosed;
|
||||
|
||||
protected virtual void OnLoad(EventArgs e) => Load?.Invoke(this, e);
|
||||
protected virtual void OnShown(EventArgs e) => Shown?.Invoke(this, e);
|
||||
protected virtual void OnActivated(EventArgs e) => Activated?.Invoke(this, e);
|
||||
protected virtual void OnFormClosing(FormClosingEventArgs e) => FormClosing?.Invoke(this, e);
|
||||
protected virtual void OnFormClosed(FormClosedEventArgs e) => FormClosed?.Invoke(this, e);
|
||||
|
||||
public new void Show()
|
||||
{
|
||||
if (_closed) throw new ObjectDisposedException(Name, "无法访问已释放的对象。");
|
||||
if (IsOpenInBrowser)
|
||||
{
|
||||
Visible = true;
|
||||
return;
|
||||
}
|
||||
SimHost.Ui.Send(() => Open(modal: false));
|
||||
}
|
||||
|
||||
public void Show(IWin32Window? owner) => Show();
|
||||
|
||||
public DialogResult ShowDialog() => ShowDialog(null);
|
||||
|
||||
public DialogResult ShowDialog(IWin32Window? owner)
|
||||
{
|
||||
if (IsOpenInBrowser && Visible) throw new InvalidOperationException("窗体已可见,不能显示为模式对话框。");
|
||||
// 模态窗体关闭后不释放,可以再次 ShowDialog(Load 只在第一次触发)
|
||||
_closed = false;
|
||||
_dialogResult = DialogResult.None;
|
||||
_modal = true;
|
||||
Open(modal: true);
|
||||
// 嵌套消息循环:等到窗体关闭(或被隐藏)后 ShowDialog 才返回
|
||||
SimHost.Ui.RunUntil(() => _closed || !Visible);
|
||||
_modal = false;
|
||||
return _dialogResult == DialogResult.None ? DialogResult.Cancel : _dialogResult;
|
||||
}
|
||||
|
||||
private void Open(bool modal)
|
||||
{
|
||||
if (!_loaded)
|
||||
{
|
||||
_loaded = true;
|
||||
OnLoad(EventArgs.Empty);
|
||||
if (_closed) return; // Load 里调用了 Close()
|
||||
RaiseUserControlLoad(this);
|
||||
}
|
||||
base.Visible = true;
|
||||
IsOpenInBrowser = true;
|
||||
Application.OpenFormsList.Add(this);
|
||||
SimHost.Send(new JsonObject { ["type"] = "formShown", ["form"] = Snapshot(), ["modal"] = modal });
|
||||
OnShown(EventArgs.Empty);
|
||||
OnActivated(EventArgs.Empty);
|
||||
}
|
||||
|
||||
private static void RaiseUserControlLoad(Control parent)
|
||||
{
|
||||
foreach (var child in parent.Controls)
|
||||
{
|
||||
if (child is UserControl userControl) userControl.RaiseLoadOnce();
|
||||
RaiseUserControlLoad(child);
|
||||
}
|
||||
}
|
||||
|
||||
public void Close() => CloseCore(CloseReason.UserClosing);
|
||||
|
||||
internal void CloseCore(CloseReason reason)
|
||||
{
|
||||
if (_closed) return;
|
||||
if (!IsOpenInBrowser)
|
||||
{
|
||||
_closed = true;
|
||||
return;
|
||||
}
|
||||
var closing = new FormClosingEventArgs(reason, false);
|
||||
OnFormClosing(closing);
|
||||
if (closing.Cancel) return;
|
||||
_closed = true;
|
||||
if (_modal && _dialogResult == DialogResult.None) _dialogResult = DialogResult.Cancel;
|
||||
OnFormClosed(new FormClosedEventArgs(reason));
|
||||
IsOpenInBrowser = false;
|
||||
Application.OpenFormsList.Remove(this);
|
||||
SimHost.Send(new JsonObject { ["type"] = "formClosed", ["formId"] = SimId });
|
||||
SimHost.Ui.Wake();
|
||||
if (!_modal) Dispose();
|
||||
}
|
||||
|
||||
internal bool IsClosed => _closed;
|
||||
|
||||
public void Activate() { }
|
||||
|
||||
public void CenterToScreen() { }
|
||||
|
||||
protected override void OnUserAction(UserAction action)
|
||||
{
|
||||
if (action.Action == "close")
|
||||
{
|
||||
CloseCore(CloseReason.UserClosing);
|
||||
return;
|
||||
}
|
||||
base.OnUserAction(action);
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["Size"] = PropValue.From(Size);
|
||||
props["ClientSize"] = PropValue.From(_clientSize);
|
||||
props["FormBorderStyle"] = _formBorderStyle.ToString();
|
||||
props["MaximizeBox"] = _maximizeBox;
|
||||
props["MinimizeBox"] = _minimizeBox;
|
||||
props["ControlBox"] = _controlBox;
|
||||
props["StartPosition"] = StartPosition.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public class Label : Control
|
||||
{
|
||||
private BorderStyle _borderStyle;
|
||||
private ContentAlignment _textAlign = ContentAlignment.TopLeft;
|
||||
|
||||
protected override Size DefaultSize => new(100, 23);
|
||||
|
||||
public BorderStyle BorderStyle
|
||||
{
|
||||
get => _borderStyle;
|
||||
set => SetProp(ref _borderStyle, value, nameof(BorderStyle));
|
||||
}
|
||||
|
||||
public ContentAlignment TextAlign
|
||||
{
|
||||
get => _textAlign;
|
||||
set => SetProp(ref _textAlign, value, nameof(TextAlign));
|
||||
}
|
||||
|
||||
public bool UseMnemonic { get; set; } = true;
|
||||
public FlatStyle FlatStyle { get; set; } = FlatStyle.Standard;
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["BorderStyle"] = _borderStyle.ToString();
|
||||
props["TextAlign"] = _textAlign.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
/// <summary>在浏览器里弹出对话框,阻塞到用户点击按钮后返回结果(期间界面线程照常处理 Timer 等消息)</summary>
|
||||
public static class MessageBox
|
||||
{
|
||||
public static DialogResult Show(string? text) => Show(text, "");
|
||||
|
||||
public static DialogResult Show(string? text, string? caption) =>
|
||||
Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.None);
|
||||
|
||||
public static DialogResult Show(string? text, string? caption, MessageBoxButtons buttons) =>
|
||||
Show(text, caption, buttons, MessageBoxIcon.None);
|
||||
|
||||
public static DialogResult Show(string? text, string? caption, MessageBoxButtons buttons, MessageBoxIcon icon) =>
|
||||
ShowCore(text, caption, buttons, icon);
|
||||
|
||||
public static DialogResult Show(string? text, string? caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) =>
|
||||
ShowCore(text, caption, buttons, icon);
|
||||
|
||||
public static DialogResult Show(IWin32Window? owner, string? text) => Show(text);
|
||||
|
||||
public static DialogResult Show(IWin32Window? owner, string? text, string? caption) => Show(text, caption);
|
||||
|
||||
public static DialogResult Show(IWin32Window? owner, string? text, string? caption, MessageBoxButtons buttons) =>
|
||||
Show(text, caption, buttons);
|
||||
|
||||
public static DialogResult Show(IWin32Window? owner, string? text, string? caption, MessageBoxButtons buttons, MessageBoxIcon icon) =>
|
||||
Show(text, caption, buttons, icon);
|
||||
|
||||
private static DialogResult ShowCore(string? text, string? caption, MessageBoxButtons buttons, MessageBoxIcon icon)
|
||||
{
|
||||
var reply = SimHost.Request(new JsonObject
|
||||
{
|
||||
["type"] = "messageBox",
|
||||
["text"] = text ?? "",
|
||||
["caption"] = caption ?? "",
|
||||
["buttons"] = buttons.ToString(),
|
||||
["icon"] = icon.ToString(),
|
||||
});
|
||||
return Enum.TryParse<DialogResult>((string?)reply, out var result) ? result : DialogResult.Cancel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Nodes;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public abstract class TextBoxBase : Control
|
||||
{
|
||||
private bool _multiline;
|
||||
private bool _readOnly;
|
||||
private BorderStyle _borderStyle = BorderStyle.Fixed3D;
|
||||
|
||||
public bool Multiline
|
||||
{
|
||||
get => _multiline;
|
||||
set => SetProp(ref _multiline, value, nameof(Multiline));
|
||||
}
|
||||
|
||||
public bool ReadOnly
|
||||
{
|
||||
get => _readOnly;
|
||||
set => SetProp(ref _readOnly, value, nameof(ReadOnly));
|
||||
}
|
||||
|
||||
public BorderStyle BorderStyle
|
||||
{
|
||||
get => _borderStyle;
|
||||
set => SetProp(ref _borderStyle, value, nameof(BorderStyle));
|
||||
}
|
||||
|
||||
public int MaxLength { get; set; } = 32767;
|
||||
public bool WordWrap { get; set; } = true;
|
||||
public bool HideSelection { get; set; } = true;
|
||||
public bool Modified { get; set; }
|
||||
public int SelectionStart { get; set; }
|
||||
public int SelectionLength { get; set; }
|
||||
public int TextLength => Text.Length;
|
||||
|
||||
public string[] Lines
|
||||
{
|
||||
get => Text.Replace("\r\n", "\n").Split('\n');
|
||||
set => Text = string.Join("\r\n", value);
|
||||
}
|
||||
|
||||
public void AppendText(string text) => Text += text;
|
||||
public void Clear() => Text = "";
|
||||
public void SelectAll() { }
|
||||
public void Select(int start, int length) { }
|
||||
public void ScrollToCaret() { }
|
||||
|
||||
protected override void OnUserAction(UserAction action)
|
||||
{
|
||||
if (action.Action == "text" && !ReadOnly)
|
||||
{
|
||||
Modified = true;
|
||||
SetTextFromUser(action.Value ?? "");
|
||||
return;
|
||||
}
|
||||
base.OnUserAction(action);
|
||||
}
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["Multiline"] = _multiline;
|
||||
props["ReadOnly"] = _readOnly;
|
||||
props["BorderStyle"] = _borderStyle.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class TextBox : TextBoxBase
|
||||
{
|
||||
private char _passwordChar;
|
||||
private bool _useSystemPasswordChar;
|
||||
private ScrollBars _scrollBars;
|
||||
|
||||
protected override Size DefaultSize => new(100, 21);
|
||||
|
||||
public char PasswordChar
|
||||
{
|
||||
get => _passwordChar;
|
||||
set => SetProp(ref _passwordChar, value, nameof(PasswordChar));
|
||||
}
|
||||
|
||||
public bool UseSystemPasswordChar
|
||||
{
|
||||
get => _useSystemPasswordChar;
|
||||
set => SetProp(ref _useSystemPasswordChar, value, nameof(UseSystemPasswordChar));
|
||||
}
|
||||
|
||||
public ScrollBars ScrollBars
|
||||
{
|
||||
get => _scrollBars;
|
||||
set => SetProp(ref _scrollBars, value, nameof(ScrollBars));
|
||||
}
|
||||
|
||||
public HorizontalAlignment TextAlign { get; set; }
|
||||
|
||||
protected override void WriteProps(JsonObject props)
|
||||
{
|
||||
base.WriteProps(props);
|
||||
props["PasswordChar"] = PropValue.From(_passwordChar);
|
||||
props["UseSystemPasswordChar"] = _useSystemPasswordChar;
|
||||
props["ScrollBars"] = _scrollBars.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public enum HorizontalAlignment { Left, Right, Center }
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Threading;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
public delegate void MethodInvoker();
|
||||
|
||||
/// <summary>界面线程的同步上下文:Send 同步执行,Post 排队执行(教材项目 8 用它从后台线程更新界面)</summary>
|
||||
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext
|
||||
{
|
||||
public override void Send(SendOrPostCallback d, object? state) => SimHost.Ui.Send(() => d(state));
|
||||
|
||||
public override void Post(SendOrPostCallback d, object? state) => SimHost.Ui.Post(() => d(state));
|
||||
|
||||
public override SynchronizationContext CreateCopy() => new WindowsFormsSynchronizationContext();
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.ComponentModel;
|
||||
using Sim.Host;
|
||||
|
||||
namespace System.Windows.Forms;
|
||||
|
||||
/// <summary>窗体定时器:Tick 事件总是在界面线程上触发,上一次 Tick 处理完之前不会重复排队</summary>
|
||||
public class Timer : Component
|
||||
{
|
||||
private System.Threading.Timer? _timer;
|
||||
private int _interval = 100;
|
||||
private bool _enabled;
|
||||
private int _tickQueued;
|
||||
|
||||
public Timer() { }
|
||||
|
||||
public Timer(IContainer container) : this() => container.Add(this);
|
||||
|
||||
public int Interval
|
||||
{
|
||||
get => _interval;
|
||||
set
|
||||
{
|
||||
if (value < 1) throw new ArgumentOutOfRangeException(nameof(value), $"值“{value}”对于 Interval 无效。Interval 必须大于 0。");
|
||||
_interval = value;
|
||||
if (_enabled) _timer?.Change(value, value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
if (_enabled == value) return;
|
||||
_enabled = value;
|
||||
if (value)
|
||||
{
|
||||
_timer ??= new System.Threading.Timer(_ => QueueTick());
|
||||
_timer.Change(_interval, _interval);
|
||||
}
|
||||
else
|
||||
{
|
||||
_timer?.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object? Tag { get; set; }
|
||||
|
||||
public event EventHandler? Tick;
|
||||
|
||||
public void Start() => Enabled = true;
|
||||
|
||||
public void Stop() => Enabled = false;
|
||||
|
||||
protected virtual void OnTick(EventArgs e) => Tick?.Invoke(this, e);
|
||||
|
||||
private void QueueTick()
|
||||
{
|
||||
if (System.Threading.Interlocked.Exchange(ref _tickQueued, 1) == 1) return;
|
||||
SimHost.Ui.Post(() =>
|
||||
{
|
||||
_tickQueued = 0;
|
||||
if (_enabled) OnTick(EventArgs.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_enabled = false;
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user