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; /// /// 仿真控件的基类。属性保存在本进程里;控件所在窗体显示后,属性变化以 patch 消息发给浏览器渲染。 /// Font、BackColor、ForeColor 是"环境属性":没有显式设置时沿用父控件的值,只把显式设置的值发给浏览器。 /// 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 Padding _padding; private int _layoutSuspendCount; private Image? _backgroundImage; private ImageLayout _backgroundImageLayout = ImageLayout.Tile; 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 ?? ""; /// 平台内部使用:控件在浏览器端的编号 [EditorBrowsable(EditorBrowsableState.Never)] public string SimId { get; } /// 为 true 时,从非界面线程修改已显示控件的属性会抛出异常(和在 VS 里按 F5 调试时一致) 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; } /// 控件已经显示在浏览器里(所在窗体已打开)时,属性变化才需要发送 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 { LegacyThreads.CheckPoint(); value ??= ""; if (_text == value) return; EnsureThreadAccess(); _text = value; NotifyChanged(nameof(Text), value); OnTextChanged(EventArgs.Empty); } } /// 来自浏览器输入的文字:只修改值并触发事件,不再回传给浏览器(避免打字时光标跳动) 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 { 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); } 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))) return; if (_dock != DockStyle.None) _parent?.PerformLayout(); 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 { if (SetProp(ref _dock, value, nameof(Dock))) _parent?.PerformLayout(); } } public Padding Padding { get => _padding; set { if (_padding.Equals(value)) return; _padding = value; PerformLayout(); } } /// 背景图片;BackgroundImageLayout 决定平铺(默认)、居中、拉伸或按比例缩放 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 { 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 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; /// 重绘。平台由浏览器绘制控件,不会触发这个事件(VS 双击 Panel 生成的空处理程序照常能编译) public event PaintEventHandler? Paint; 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 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); /// 平台内部使用:在界面线程上处理浏览器上报的用户操作 [EditorBrowsable(EditorBrowsableState.Never)] public void PerformUserAction(UserAction action) { if (!Enabled || !Visible) return; OnUserAction(action); } /// 按真实 WinForms 的顺序,把一个用户操作翻译成事件 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 "doubleClick": OnDoubleClick(EventArgs.Empty); 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() => _layoutSuspendCount++; public void ResumeLayout() => ResumeLayout(true); public void ResumeLayout(bool performLayout) { if (_layoutSuspendCount > 0) _layoutSuspendCount--; if (performLayout) PerformLayout(); } /// /// 停靠布局(和 WinForms 一样):按 Controls 集合从后往前,Top / Bottom / Left / Right 依次贴边并占掉那一条, /// Fill 占剩下的全部。VS 在 Designer.cs 里写的位置大小就是这样排出来的;代码里改停靠、大小、可见时重新排。 /// 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); } } /// 停靠布局给出的位置大小:不再反过来触发父容器的布局 private void SetBoundsFromLayout(Rectangle bounds) { SetProp(ref _location, bounds.Location, nameof(Location)); if (SetProp(ref _size, bounds.Size, nameof(Size))) PerformLayout(); } /// 容器里用来停靠子控件的区域:客户区去掉内边距(分组框、带边框的面板再去掉边框) 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; public object? Invoke(Delegate method) => Invoke(method, Array.Empty()); 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()); 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); } // ---------- 属性变化与快照 ---------- /// 修改字段并在需要时通知浏览器;返回值表示是否真的改变 protected bool SetProp(ref T field, T value, string prop) { // 后台线程改界面时响应 Suspend / Abort(教材的线程方法每次循环都会改控件) LegacyThreads.CheckPoint(); if (EqualityComparer.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}”的线程访问它。"); } /// 平台内部使用:生成发给浏览器的控件树 [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, }; } /// 写入浏览器渲染需要的属性;子类追加自己的属性 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(); props["BackgroundImage"] = PropValue.From(_backgroundImage); props["BackgroundImageLayout"] = _backgroundImageLayout.ToString(); } /// 浏览器按这个类型名选择渲染方式。学生自己继承的控件按最近的仿真库基类渲染。 private string ClientType { get { for (var type = GetType(); type != null; type = type.BaseType) { var attribute = type.GetCustomAttribute(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 { private readonly List _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 (value.Dock != DockStyle.None) Owner.PerformLayout(); 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 (value.Dock != DockStyle.None) Owner.PerformLayout(); 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 GetEnumerator() => _items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } /// 仿真库内部使用:指定浏览器端的渲染类型名(教材控件库的控件用完整类名) [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class ClientTypeAttribute : Attribute { public ClientTypeAttribute(string name) => Name = name; public string Name { get; } }