Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50946467f6 | ||
|
|
a2f03ee57f |
No files matched your search
@@ -11,7 +11,7 @@ namespace System.Windows.Forms;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 仿真控件的基类。属性保存在本进程里;控件所在窗体显示后,属性变化以 patch 消息发给浏览器渲染。
|
/// 仿真控件的基类。属性保存在本进程里;控件所在窗体显示后,属性变化以 patch 消息发给浏览器渲染。
|
||||||
/// Font、BackColor、ForeColor 是"环境属性":没有显式设置时沿用父控件的值,只把显式设置的值发给浏览器。
|
/// Font、BackColor、ForeColor 是"环境属性":没有显式设置时沿用父控件的值(文本框等的颜色除外,见 OwnBackColor),只把显式设置的值发给浏览器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Control : Component, IWin32Window
|
public class Control : Component, IWin32Window
|
||||||
{
|
{
|
||||||
@@ -178,24 +178,42 @@ public class Control : Component, IWin32Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>和 WinForms 一样:父控件(分组框、面板)禁用时子控件也是禁用的,读出 false、不响应操作;浏览器按同样的规则画成灰色</summary>
|
||||||
public bool Enabled
|
public bool Enabled
|
||||||
{
|
{
|
||||||
get => _enabled;
|
get => _enabled && (_parent?.Enabled ?? true);
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (SetProp(ref _enabled, value, nameof(Enabled))) OnEnabledChanged(EventArgs.Empty);
|
var old = Enabled;
|
||||||
|
SetProp(ref _enabled, value, nameof(Enabled));
|
||||||
|
if (Enabled != old) OnEnabledChanged(EventArgs.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 没有显式设置时控件自己的颜色;null 表示沿用父控件。文本框、列表框、下拉框、日期选择是白底黑字(SystemColors.Window / WindowText),
|
||||||
|
/// 不沿用父控件:分组框设成白字时,里面的文本框还是黑字。
|
||||||
|
/// </summary>
|
||||||
|
private protected virtual Color? OwnBackColor => null;
|
||||||
|
|
||||||
|
private protected virtual Color? OwnForeColor => null;
|
||||||
|
|
||||||
|
/// <summary>能不能把 BackColor 设成透明(含半透明):窗体、文本框、列表框、下拉框、日期选择、数据表格不行,和 WinForms 一样报错</summary>
|
||||||
|
private protected virtual bool SupportsTransparentBackColor => true;
|
||||||
|
|
||||||
public virtual Color BackColor
|
public virtual Color BackColor
|
||||||
{
|
{
|
||||||
get => !_backColor.IsEmpty ? _backColor : _parent?.BackColor ?? DefaultBackColor;
|
get => !_backColor.IsEmpty ? _backColor : OwnBackColor ?? _parent?.BackColor ?? DefaultBackColor;
|
||||||
set => SetProp(ref _backColor, value, nameof(BackColor));
|
set
|
||||||
|
{
|
||||||
|
if (!value.IsEmpty && value.A < 255 && !SupportsTransparentBackColor) throw new ArgumentException("控件不支持透明的背景色。");
|
||||||
|
SetProp(ref _backColor, value, nameof(BackColor));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual Color ForeColor
|
public virtual Color ForeColor
|
||||||
{
|
{
|
||||||
get => !_foreColor.IsEmpty ? _foreColor : _parent?.ForeColor ?? DefaultForeColor;
|
get => !_foreColor.IsEmpty ? _foreColor : OwnForeColor ?? _parent?.ForeColor ?? DefaultForeColor;
|
||||||
set => SetProp(ref _foreColor, value, nameof(ForeColor));
|
set => SetProp(ref _foreColor, value, nameof(ForeColor));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +320,14 @@ public class Control : Component, IWin32Window
|
|||||||
protected virtual void OnMouseLeave(EventArgs e) => MouseLeave?.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 OnTextChanged(EventArgs e) => TextChanged?.Invoke(this, e);
|
||||||
protected virtual void OnVisibleChanged(EventArgs e) => VisibleChanged?.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 OnEnabledChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
EnabledChanged?.Invoke(this, e);
|
||||||
|
// 自己没被禁用的子控件实际状态也跟着变,同样触发 EnabledChanged(WinForms 的 OnParentEnabledChanged)
|
||||||
|
for (var i = 0; i < Controls.Count; i++)
|
||||||
|
if (Controls[i]._enabled) Controls[i].OnEnabledChanged(e);
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void OnGotFocus(EventArgs e) => GotFocus?.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 OnLostFocus(EventArgs e) => LostFocus?.Invoke(this, e);
|
||||||
protected virtual void OnPaint(PaintEventArgs e) => Paint?.Invoke(this, e);
|
protected virtual void OnPaint(PaintEventArgs e) => Paint?.Invoke(this, e);
|
||||||
|
|||||||
@@ -468,6 +468,8 @@ public class DataGridView : Control, ISupportInitialize
|
|||||||
private bool _initializing;
|
private bool _initializing;
|
||||||
private bool _rowsDirty;
|
private bool _rowsDirty;
|
||||||
|
|
||||||
|
private protected override bool SupportsTransparentBackColor => false;
|
||||||
|
|
||||||
public DataGridView()
|
public DataGridView()
|
||||||
{
|
{
|
||||||
Columns = new DataGridViewColumnCollection(this);
|
Columns = new DataGridViewColumnCollection(this);
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ public class DateTimePicker : Control
|
|||||||
|
|
||||||
protected override Size DefaultSize => new(200, 21);
|
protected override Size DefaultSize => new(200, 21);
|
||||||
|
|
||||||
|
private protected override Color? OwnBackColor => SystemColors.Window;
|
||||||
|
private protected override Color? OwnForeColor => SystemColors.WindowText;
|
||||||
|
private protected override bool SupportsTransparentBackColor => false;
|
||||||
|
|
||||||
public DateTime Value
|
public DateTime Value
|
||||||
{
|
{
|
||||||
get => _value;
|
get => _value;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public class Form : ContainerControl
|
|||||||
private bool _modal;
|
private bool _modal;
|
||||||
private DialogResult _dialogResult;
|
private DialogResult _dialogResult;
|
||||||
|
|
||||||
|
private protected override bool SupportsTransparentBackColor => false;
|
||||||
|
|
||||||
public Form()
|
public Form()
|
||||||
{
|
{
|
||||||
base.Visible = false;
|
base.Visible = false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace System.Windows.Forms;
|
namespace System.Windows.Forms;
|
||||||
@@ -10,6 +11,10 @@ public abstract class ListControl : Control
|
|||||||
/// <summary>选中项的序号。列表项增删时直接调整(和 WinForms 一样不触发 SelectedIndexChanged)</summary>
|
/// <summary>选中项的序号。列表项增删时直接调整(和 WinForms 一样不触发 SelectedIndexChanged)</summary>
|
||||||
private protected int SelectedIndexValue = -1;
|
private protected int SelectedIndexValue = -1;
|
||||||
|
|
||||||
|
private protected override Color? OwnBackColor => SystemColors.Window;
|
||||||
|
private protected override Color? OwnForeColor => SystemColors.WindowText;
|
||||||
|
private protected override bool SupportsTransparentBackColor => false;
|
||||||
|
|
||||||
public abstract int SelectedIndex { get; set; }
|
public abstract int SelectedIndex { get; set; }
|
||||||
|
|
||||||
public string DisplayMember { get; set; } = "";
|
public string DisplayMember { get; set; } = "";
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ public abstract class TextBoxBase : Control
|
|||||||
private bool _readOnly;
|
private bool _readOnly;
|
||||||
private BorderStyle _borderStyle = BorderStyle.Fixed3D;
|
private BorderStyle _borderStyle = BorderStyle.Fixed3D;
|
||||||
|
|
||||||
|
private protected override Color? OwnBackColor => _readOnly ? SystemColors.Control : SystemColors.Window;
|
||||||
|
private protected override Color? OwnForeColor => SystemColors.WindowText;
|
||||||
|
private protected override bool SupportsTransparentBackColor => false;
|
||||||
|
|
||||||
public bool Multiline
|
public bool Multiline
|
||||||
{
|
{
|
||||||
get => _multiline;
|
get => _multiline;
|
||||||
|
|||||||
@@ -672,6 +672,87 @@ test('虚拟时钟:拨时间、倍速后 DateTime.Now / Today 跟着走,恢
|
|||||||
expect(runner.messages.some((m) => m.type === 'console' && m.text.includes('today=2024-01-16'))).toBe(true)
|
expect(runner.messages.some((m) => m.type === 'console' && m.text.includes('today=2024-01-16'))).toBe(true)
|
||||||
}, 30000)
|
}, 30000)
|
||||||
|
|
||||||
|
test('颜色:分组框设成白字时,里面的标签沿用白字,文本框、列表框、下拉框还是白底黑字;只发显式设置的颜色', async () => {
|
||||||
|
const { runner } = await runMain(
|
||||||
|
'colors',
|
||||||
|
`
|
||||||
|
var form = new Form();
|
||||||
|
var group = new GroupBox { ForeColor = Color.White, BackColor = Color.Navy };
|
||||||
|
var label = new Label();
|
||||||
|
var text = new TextBox { Name = "text" };
|
||||||
|
var readOnly = new TextBox { ReadOnly = true };
|
||||||
|
var list = new ListBox();
|
||||||
|
var combo = new ComboBox();
|
||||||
|
var red = new TextBox { Name = "red", ForeColor = Color.Red };
|
||||||
|
group.Controls.AddRange(new Control[] { label, text, readOnly, list, combo, red });
|
||||||
|
form.Controls.Add(group);
|
||||||
|
Console.WriteLine(string.Join(",", new Control[] { label, text, list, combo, red }.Select(c => c.ForeColor.Name)));
|
||||||
|
Console.WriteLine(string.Join(",", new Control[] { label, text, readOnly, list, combo }.Select(c => c.BackColor.Name)));
|
||||||
|
Application.Run(form);`,
|
||||||
|
'using System.Drawing;\nusing System.Windows.Forms;',
|
||||||
|
)
|
||||||
|
const { form } = await runner.waitFor('formShown')
|
||||||
|
expect(findByName(form, 'text').props.ForeColor).toBeNull()
|
||||||
|
expect(findByName(form, 'red').props.ForeColor).toBe('#ff0000')
|
||||||
|
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
|
||||||
|
expect(output).toBe('White,WindowText,WindowText,WindowText,Red\nNavy,Window,Control,Window,Window\n')
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
|
test('禁用:分组框禁用后里面的控件 Enabled 读出 false、触发 EnabledChanged、点击不响应;重新启用后恢复', async () => {
|
||||||
|
const { runner } = await runMain(
|
||||||
|
'enabled',
|
||||||
|
`
|
||||||
|
var form = new Form();
|
||||||
|
var group = new GroupBox { Name = "group" };
|
||||||
|
var button = new Button { Name = "button" };
|
||||||
|
var off = new Button { Enabled = false };
|
||||||
|
group.Controls.Add(button);
|
||||||
|
group.Controls.Add(off);
|
||||||
|
form.Controls.Add(group);
|
||||||
|
button.EnabledChanged += (s, e) => Console.WriteLine("button changed=" + button.Enabled);
|
||||||
|
off.EnabledChanged += (s, e) => Console.WriteLine("off changed");
|
||||||
|
button.Click += (s, e) => Console.WriteLine("click");
|
||||||
|
var toggle = new Button { Name = "toggle" };
|
||||||
|
toggle.Click += (s, e) => { group.Enabled = !group.Enabled; Console.WriteLine("group=" + group.Enabled + " button=" + button.Enabled + " off=" + off.Enabled); };
|
||||||
|
form.Controls.Add(toggle);
|
||||||
|
Application.Run(form);`,
|
||||||
|
'using System.Windows.Forms;',
|
||||||
|
)
|
||||||
|
const { form } = await runner.waitFor('formShown')
|
||||||
|
const click = (name: string) => runner.send({ type: 'action', id: findByName(form, name).id, action: 'click' })
|
||||||
|
click('toggle')
|
||||||
|
await runner.waitFor('console', (m) => m.text.startsWith('group=False'))
|
||||||
|
click('button')
|
||||||
|
click('toggle')
|
||||||
|
await runner.waitFor('console', (m) => m.text.startsWith('group=True'))
|
||||||
|
click('button')
|
||||||
|
await runner.waitFor('console', (m) => m.text === 'click\n')
|
||||||
|
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
|
||||||
|
// 浏览器只收到分组框自己的 Enabled,子控件按父控件画成禁用
|
||||||
|
expect(findByName(form, 'button').props.Enabled).toBe(true)
|
||||||
|
expect(output).toBe(
|
||||||
|
'button changed=False\ngroup=False button=False off=False\nbutton changed=True\ngroup=True button=True off=False\nclick\n',
|
||||||
|
)
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
|
test('透明背景:文本框、窗体等设透明背景色和 WinForms 一样报错,标签、面板可以', async () => {
|
||||||
|
const { runner } = await runMain(
|
||||||
|
'transparent',
|
||||||
|
`
|
||||||
|
foreach (var control in new Control[] { new TextBox(), new ComboBox(), new ListBox(), new Form(), new Label(), new Panel() })
|
||||||
|
{
|
||||||
|
try { control.BackColor = Color.Transparent; Console.WriteLine(control.GetType().Name + " ok"); }
|
||||||
|
catch (ArgumentException ex) { Console.WriteLine(control.GetType().Name + " " + ex.Message); }
|
||||||
|
}`,
|
||||||
|
'using System.Drawing;\nusing System.Windows.Forms;',
|
||||||
|
)
|
||||||
|
await runner.finished()
|
||||||
|
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
|
||||||
|
expect(output).toBe(
|
||||||
|
'TextBox 控件不支持透明的背景色。\nComboBox 控件不支持透明的背景色。\nListBox 控件不支持透明的背景色。\nForm 控件不支持透明的背景色。\nLabel ok\nPanel ok\n',
|
||||||
|
)
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
test('TrackBar:拖动时先 ValueChanged 再 Scroll,超出范围的值截到两端;代码设超出范围的 Value 报错', async () => {
|
test('TrackBar:拖动时先 ValueChanged 再 Scroll,超出范围的值截到两端;代码设超出范围的 Value 报错', async () => {
|
||||||
const { runner } = await runMain(
|
const { runner } = await runMain(
|
||||||
'trackbar',
|
'trackbar',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 属性窗口和事件窗口(仿 VS):显示选中控件的常用属性;改动立即写回 Designer.cs。
|
// 属性窗口和事件窗口(仿 VS):显示选中控件的常用属性;改动立即写回 Designer.cs。
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { type ControlDef, FORM_DEF, type PropDef, controlDef } from './catalog'
|
import { type ControlDef, FORM_DEF, NO_TRANSPARENT_BACK_COLOR, type PropDef, controlDef } from './catalog'
|
||||||
import { type ChartModel, readChart } from './chart'
|
import { type ChartModel, readChart } from './chart'
|
||||||
import ChartDialog from './ChartDialog.vue'
|
import ChartDialog from './ChartDialog.vue'
|
||||||
import type { DesignerDocument } from './document'
|
import type { DesignerDocument } from './document'
|
||||||
@@ -47,6 +47,13 @@ function current(prop: PropDef): Value | undefined {
|
|||||||
return text === undefined ? prop.default : parseValue(text)
|
return text === undefined ? prop.default : parseValue(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 颜色下拉框的选项:文本框等的背景色没有"透明"(已经设成透明的旧窗体照样显示出来) */
|
||||||
|
function colorChoices(prop: PropDef) {
|
||||||
|
const names = Object.keys(NAMED_COLORS)
|
||||||
|
if (prop.name !== 'BackColor' || !NO_TRANSPARENT_BACK_COLOR.has(typeName.value) || color(prop)?.name === 'Transparent') return names
|
||||||
|
return names.filter((name) => name !== 'Transparent')
|
||||||
|
}
|
||||||
|
|
||||||
/** 设成默认值时删除这条属性语句(VS 不写默认值) */
|
/** 设成默认值时删除这条属性语句(VS 不写默认值) */
|
||||||
function set(prop: PropDef, value: Value) {
|
function set(prop: PropDef, value: Value) {
|
||||||
const isDefault = prop.default && JSON.stringify(prop.default) === JSON.stringify(value)
|
const isDefault = prop.default && JSON.stringify(prop.default) === JSON.stringify(value)
|
||||||
@@ -289,7 +296,7 @@ function renameHandler(event: string, value: string) {
|
|||||||
<i class="swatch" :style="{ background: color(prop)?.css ?? 'transparent' }" />
|
<i class="swatch" :style="{ background: color(prop)?.css ?? 'transparent' }" />
|
||||||
<select :value="color(prop)?.name ?? (color(prop) ? 'custom' : '')" @change="setColor(prop, ($event.target as HTMLSelectElement).value)">
|
<select :value="color(prop)?.name ?? (color(prop) ? 'custom' : '')" @change="setColor(prop, ($event.target as HTMLSelectElement).value)">
|
||||||
<option value="">(默认)</option>
|
<option value="">(默认)</option>
|
||||||
<option v-for="(_, name) in NAMED_COLORS" :key="name" :value="name" :title="name">{{ COLOR_LABELS[name] ?? name }}</option>
|
<option v-for="name in colorChoices(prop)" :key="name" :value="name" :title="name">{{ COLOR_LABELS[name] ?? name }}</option>
|
||||||
<option v-if="color(prop) && !color(prop)!.name" value="custom" disabled>{{ color(prop)!.css }}</option>
|
<option v-if="color(prop) && !color(prop)!.name" value="custom" disabled>{{ color(prop)!.css }}</option>
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export interface PropDef {
|
|||||||
category: '外观' | '行为' | '布局' | '数据'
|
category: '外观' | '行为' | '布局' | '数据'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** BackColor 不能选透明的控件(WinForms 里设透明会报"控件不支持透明的背景色",VS 的属性窗口也不让选) */
|
||||||
|
export const NO_TRANSPARENT_BACK_COLOR = new Set(['Form', 'TextBox', 'ComboBox', 'ListBox', 'DateTimePicker', 'DataGridView'])
|
||||||
|
|
||||||
export interface EventDef {
|
export interface EventDef {
|
||||||
/** WinForms 的事件名,事件处理方法的名字也用它(如 button1_Click) */
|
/** WinForms 的事件名,事件处理方法的名字也用它(如 button1_Click) */
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ export function previewTree(doc: DesignerDocument): ControlNode {
|
|||||||
const build = (name: string): ControlNode => {
|
const build = (name: string): ControlNode => {
|
||||||
const type = types.get(name)!
|
const type = types.get(name)!
|
||||||
const props: Record<string, unknown> = { ...DEVICE_DEFAULTS[type], ...propsOf(doc, name) }
|
const props: Record<string, unknown> = { ...DEVICE_DEFAULTS[type], ...propsOf(doc, name) }
|
||||||
// 设计时即使 Visible = false 也显示出来(和 VS 一样),方便选中和编辑
|
// 设计时即使 Visible = false 也显示出来、Enabled = false 也不画成灰色(和 VS 一样,运行后才生效),方便选中和编辑
|
||||||
props.Visible = true
|
props.Visible = true
|
||||||
|
props.Enabled = true
|
||||||
if (type === 'System.Windows.Forms.ComboBox' || type === 'System.Windows.Forms.ListBox')
|
if (type === 'System.Windows.Forms.ComboBox' || type === 'System.Windows.Forms.ListBox')
|
||||||
props.Items = getItems(doc, name).map((item) => {
|
props.Items = getItems(doc, name).map((item) => {
|
||||||
const v = parseValue(item)
|
const v = parseValue(item)
|
||||||
|
|||||||
@@ -676,7 +676,7 @@ function run() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<button class="run-toggle" :title="runCollapsed ? '展开运行区' : '收起运行区(左边的代码和设计器占满宽度,运行时自动展开)'" @click="toggleRunArea()">
|
<button class="run-toggle" :title="runCollapsed ? '展开运行区' : '收起运行区(左边的代码和设计器占满宽度,运行时自动展开)'" @click="toggleRunArea()">
|
||||||
{{ runCollapsed ? '◂' : '▸' }}
|
{{ runCollapsed ? '展开运行区' : '收起运行区' }}
|
||||||
</button>
|
</button>
|
||||||
<RunArea v-show="!runCollapsed" :session="session" @diagnostic="showDiagnostic" />
|
<RunArea v-show="!runCollapsed" :session="session" @diagnostic="showDiagnostic" />
|
||||||
</main>
|
</main>
|
||||||
@@ -763,22 +763,25 @@ function run() {
|
|||||||
.workspace {
|
.workspace {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 3fr) 14px minmax(360px, 2fr);
|
grid-template-columns: minmax(0, 3fr) 22px minmax(360px, 2fr);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace.run-collapsed {
|
.workspace.run-collapsed {
|
||||||
grid-template-columns: minmax(0, 1fr) 14px;
|
grid-template-columns: minmax(0, 1fr) 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 编辑区和运行区之间的竖条:点一下收起、展开运行区 */
|
/* 编辑区和运行区之间的竖条:点一下收起、展开运行区(写着"收起运行区""展开运行区",比只有箭头好找) */
|
||||||
.run-toggle {
|
.run-toggle {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-right: 1px solid #e5e7eb;
|
border-right: 1px solid #e5e7eb;
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
color: #6b7280;
|
color: #4b5563;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
|
/* 竖排文字,整根竖条都能点 */
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
letter-spacing: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// 用户操作只通过 act() 上报,由运行器按真实 WinForms 的顺序触发事件。
|
// 用户操作只通过 act() 上报,由运行器按真实 WinForms 的顺序触发事件。
|
||||||
import { computed, inject, onBeforeUnmount, provide, ref, watch, type CSSProperties } from 'vue'
|
import { computed, inject, onBeforeUnmount, provide, ref, watch, type CSSProperties } from 'vue'
|
||||||
import type { ControlNode, UserAction } from '@iot/shared'
|
import type { ControlNode, UserAction } from '@iot/shared'
|
||||||
import { AMBIENT_BACK_COLOR, AMBIENT_BACK_TYPES, boxCss, DEFAULT_BACK_COLOR, displayText } from './format'
|
import { AMBIENT_BACK_COLOR, AMBIENT_BACK_TYPES, boxCss, contentAlignCss, DEFAULT_BACK_COLOR, displayText, PARENT_ENABLED } from './format'
|
||||||
import { backgroundCss, imageSources, RUN_IMAGES, srcset, type ImageValue } from './images'
|
import { backgroundCss, imageSources, RUN_IMAGES, srcset, type ImageValue } from './images'
|
||||||
import ChartControl from './ChartControl.vue'
|
import ChartControl from './ChartControl.vue'
|
||||||
import DataGridViewControl from './DataGridViewControl.vue'
|
import DataGridViewControl from './DataGridViewControl.vue'
|
||||||
@@ -19,7 +19,10 @@ const props = defineProps<{
|
|||||||
const p = computed(() => props.node.props)
|
const p = computed(() => props.node.props)
|
||||||
// WinForms 中先加入 Controls 的控件在最上层,所以倒序渲染(后渲染的 DOM 元素在上面)
|
// WinForms 中先加入 Controls 的控件在最上层,所以倒序渲染(后渲染的 DOM 元素在上面)
|
||||||
const childrenTopLast = computed(() => [...props.node.children].reverse())
|
const childrenTopLast = computed(() => [...props.node.children].reverse())
|
||||||
const enabled = computed(() => p.value.Enabled !== false)
|
const parentEnabled = inject(PARENT_ENABLED, ref(true))
|
||||||
|
/** 实际是否可用:自己和所有父控件都可用(和 WinForms 一样,运行器也按这个规则忽略禁用控件上的操作) */
|
||||||
|
const enabled = computed(() => p.value.Enabled !== false && parentEnabled.value)
|
||||||
|
provide(PARENT_ENABLED, enabled)
|
||||||
const runImages = inject(RUN_IMAGES, new Map<string, string>())
|
const runImages = inject(RUN_IMAGES, new Map<string, string>())
|
||||||
const ambient = inject(AMBIENT_BACK_COLOR, ref(DEFAULT_BACK_COLOR))
|
const ambient = inject(AMBIENT_BACK_COLOR, ref(DEFAULT_BACK_COLOR))
|
||||||
/** 这个控件实际的背景色,子控件没设置背景色时沿用 */
|
/** 这个控件实际的背景色,子控件没设置背景色时沿用 */
|
||||||
@@ -65,12 +68,16 @@ function openDropDown() {
|
|||||||
const el = combo.value
|
const el = combo.value
|
||||||
if (!el) return
|
if (!el) return
|
||||||
const rect = el.getBoundingClientRect()
|
const rect = el.getBoundingClientRect()
|
||||||
|
// 列表挂在页面最外层,字体、颜色从下拉框本身取(和 WinForms 一样跟着下拉框的 Font、ForeColor、BackColor)
|
||||||
|
const css = getComputedStyle(el)
|
||||||
listStyle.value = {
|
listStyle.value = {
|
||||||
left: `${rect.left}px`,
|
left: `${rect.left}px`,
|
||||||
top: `${rect.bottom}px`,
|
top: `${rect.bottom}px`,
|
||||||
width: `${el.offsetWidth}px`,
|
width: `${el.offsetWidth}px`,
|
||||||
transform: `scale(${rect.width / el.offsetWidth || 1})`,
|
transform: `scale(${rect.width / el.offsetWidth || 1})`,
|
||||||
font: getComputedStyle(el).font,
|
font: css.font,
|
||||||
|
color: css.color,
|
||||||
|
backgroundColor: css.backgroundColor,
|
||||||
}
|
}
|
||||||
// 先触发 Click(教材在 Click 里填充串口列表),列表随后按最新的 Items 显示
|
// 先触发 Click(教材在 Click 里填充串口列表),列表随后按最新的 Items 显示
|
||||||
act({ action: 'dropDown' })
|
act({ action: 'dropDown' })
|
||||||
@@ -131,8 +138,8 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
|
|||||||
<div
|
<div
|
||||||
v-else-if="node.type === 'Label'"
|
v-else-if="node.type === 'Label'"
|
||||||
class="wf-control wf-label"
|
class="wf-control wf-label"
|
||||||
:class="{ 'wf-autosize': p.AutoSize === true, 'wf-disabled': !enabled }"
|
:class="[`wf-border-${p.BorderStyle ?? 'None'}`, { 'wf-autosize': p.AutoSize === true, 'wf-disabled': !enabled }]"
|
||||||
:style="style"
|
:style="[style, p.AutoSize === true ? {} : contentAlignCss(p.TextAlign)]"
|
||||||
@click="act({ action: 'click' })"
|
@click="act({ action: 'click' })"
|
||||||
>
|
>
|
||||||
{{ displayText(p.Text) }}
|
{{ displayText(p.Text) }}
|
||||||
@@ -142,6 +149,7 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
|
|||||||
<div v-else-if="node.type === 'TextBox'" class="wf-control wf-textbox" :style="style">
|
<div v-else-if="node.type === 'TextBox'" class="wf-control wf-textbox" :style="style">
|
||||||
<textarea
|
<textarea
|
||||||
v-if="p.Multiline"
|
v-if="p.Multiline"
|
||||||
|
:class="`wf-scrollbars-${p.ScrollBars ?? 'None'}`"
|
||||||
:value="String(p.Text ?? '')"
|
:value="String(p.Text ?? '')"
|
||||||
:readonly="p.ReadOnly === true"
|
:readonly="p.ReadOnly === true"
|
||||||
:disabled="!enabled"
|
:disabled="!enabled"
|
||||||
@@ -232,7 +240,11 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
|
|||||||
:style="style"
|
:style="style"
|
||||||
@click="act({ action: 'click' })"
|
@click="act({ action: 'click' })"
|
||||||
>
|
>
|
||||||
<span :class="node.type === 'CheckBox' ? 'wf-checkbox-box' : 'wf-radio-dot'" :data-checked="p.Checked === true" />
|
<span
|
||||||
|
:class="node.type === 'CheckBox' ? 'wf-checkbox-box' : 'wf-radio-dot'"
|
||||||
|
:data-checked="p.Checked === true && p.CheckState !== 'Indeterminate'"
|
||||||
|
:data-indeterminate="p.CheckState === 'Indeterminate'"
|
||||||
|
/>
|
||||||
<span class="wf-check-text">{{ displayText(p.Text) }}</span>
|
<span class="wf-check-text">{{ displayText(p.Text) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -245,7 +257,7 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- GroupBox -->
|
<!-- GroupBox -->
|
||||||
<div v-else-if="node.type === 'GroupBox'" class="wf-control wf-groupbox" :style="style">
|
<div v-else-if="node.type === 'GroupBox'" class="wf-control wf-groupbox" :class="{ 'wf-disabled': !enabled }" :style="style">
|
||||||
<!-- 标题处的边框断开、透出底下的背景(教材项目 8 的分组框放在背景图上,白色标题) -->
|
<!-- 标题处的边框断开、透出底下的背景(教材项目 8 的分组框放在背景图上,白色标题) -->
|
||||||
<fieldset class="wf-groupbox-frame">
|
<fieldset class="wf-groupbox-frame">
|
||||||
<legend v-if="p.Text" class="wf-groupbox-caption">{{ displayText(p.Text) }}</legend>
|
<legend v-if="p.Text" class="wf-groupbox-caption">{{ displayText(p.Text) }}</legend>
|
||||||
|
|||||||
@@ -69,9 +69,12 @@ function close() {
|
|||||||
>
|
>
|
||||||
<div v-if="!borderless" class="wf-titlebar" @pointerdown="startDrag" @pointermove="drag" @pointerup="dragStart = undefined">
|
<div v-if="!borderless" class="wf-titlebar" @pointerdown="startDrag" @pointermove="drag" @pointerup="dragStart = undefined">
|
||||||
<span class="wf-title">{{ displayText(p.Text) }}</span>
|
<span class="wf-title">{{ displayText(p.Text) }}</span>
|
||||||
<span v-if="p.MinimizeBox !== false" class="wf-titlebar-button">─</span>
|
<!-- ControlBox = false 时标题栏右边的三个按钮都不显示(和 WinForms 一样) -->
|
||||||
<span v-if="p.MaximizeBox !== false" class="wf-titlebar-button">☐</span>
|
<template v-if="p.ControlBox !== false">
|
||||||
<span class="wf-titlebar-button wf-titlebar-close" @pointerdown.stop @click="close">✕</span>
|
<span v-if="p.MinimizeBox !== false" class="wf-titlebar-button">─</span>
|
||||||
|
<span v-if="p.MaximizeBox !== false" class="wf-titlebar-button">☐</span>
|
||||||
|
<span class="wf-titlebar-button wf-titlebar-close" @pointerdown.stop @click="close">✕</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="wf-client"
|
class="wf-client"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
import { contentAlignCss } from './format'
|
||||||
|
|
||||||
|
describe('Label 的 TextAlign', () => {
|
||||||
|
test('默认左上不加样式,其他位置用 flex 对齐', () => {
|
||||||
|
expect(contentAlignCss('TopLeft')).toEqual({})
|
||||||
|
expect(contentAlignCss(undefined)).toEqual({})
|
||||||
|
expect(contentAlignCss('MiddleCenter')).toEqual({ display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center' })
|
||||||
|
expect(contentAlignCss('BottomRight')).toEqual({ display: 'flex', alignItems: 'flex-end', justifyContent: 'flex-end', textAlign: 'right' })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -53,6 +53,18 @@ export function fontCss(font: FontValue | null | undefined): CSSProperties {
|
|||||||
export const AMBIENT_BACK_COLOR: InjectionKey<Readonly<Ref<string>>> = Symbol('ambientBackColor')
|
export const AMBIENT_BACK_COLOR: InjectionKey<Readonly<Ref<string>>> = Symbol('ambientBackColor')
|
||||||
export const DEFAULT_BACK_COLOR = '#f0f0f0'
|
export const DEFAULT_BACK_COLOR = '#f0f0f0'
|
||||||
|
|
||||||
|
/** 父控件实际是否可用:分组框、面板禁用时里面的控件也是禁用的(变灰、不响应操作),和 WinForms 一样 */
|
||||||
|
export const PARENT_ENABLED: InjectionKey<Readonly<Ref<boolean>>> = Symbol('parentEnabled')
|
||||||
|
|
||||||
|
/** Label 的 TextAlign(ContentAlignment,如 MiddleCenter)对应的 CSS:AutoSize 为 false 时文字在控件里的位置 */
|
||||||
|
export function contentAlignCss(align: unknown): CSSProperties {
|
||||||
|
const match = /^(Top|Middle|Bottom)(Left|Center|Right)$/.exec(String(align ?? ''))
|
||||||
|
if (!match || align === 'TopLeft') return {}
|
||||||
|
const flex = { Top: 'flex-start', Middle: 'center', Bottom: 'flex-end', Left: 'flex-start', Center: 'center', Right: 'flex-end' } as const
|
||||||
|
const [, vertical, horizontal] = match as unknown as [string, keyof typeof flex, keyof typeof flex]
|
||||||
|
return { display: 'flex', alignItems: flex[vertical], justifyContent: flex[horizontal], textAlign: horizontal.toLowerCase() as 'left' | 'center' | 'right' }
|
||||||
|
}
|
||||||
|
|
||||||
/** 背景色沿用父控件的控件(按钮、文本框等有自己的默认底色) */
|
/** 背景色沿用父控件的控件(按钮、文本框等有自己的默认底色) */
|
||||||
export const AMBIENT_BACK_TYPES = new Set(['Label', 'Panel', 'GroupBox', 'CheckBox', 'RadioButton', 'PictureBox', 'TrackBar'])
|
export const AMBIENT_BACK_TYPES = new Set(['Label', 'Panel', 'GroupBox', 'CheckBox', 'RadioButton', 'PictureBox', 'TrackBar'])
|
||||||
|
|
||||||
|
|||||||
@@ -74,9 +74,22 @@
|
|||||||
border-color: #005499;
|
border-color: #005499;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 文本框、下拉框、列表框、日期选择的文字默认是黑色(SystemColors.WindowText),不沿用父控件的 ForeColor:
|
||||||
|
* 教材里分组框设成白字时,里面白底的文本框还是黑字。控件自己设了 ForeColor 时写在 style 上,优先于这里。
|
||||||
|
* 数据表格不在这里:它的单元格文字和 WinForms 一样跟着 ForeColor 走(没设置时沿用父控件)。
|
||||||
|
*/
|
||||||
|
.wf-textbox,
|
||||||
|
.wf-combobox,
|
||||||
|
.wf-listbox,
|
||||||
|
.wf-dtp {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 禁用的控件不管设了什么 ForeColor 都画成灰字(WinForms 画禁用文字用系统灰色),所以要压过 style 上的颜色 */
|
||||||
.wf-button:disabled,
|
.wf-button:disabled,
|
||||||
.wf-disabled {
|
.wf-disabled {
|
||||||
color: #a0a0a0;
|
color: #a0a0a0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wf-label {
|
.wf-label {
|
||||||
@@ -84,6 +97,16 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Label 的 BorderStyle:单线是黑色细边框,立体是凹下去的边框 */
|
||||||
|
.wf-label.wf-border-FixedSingle {
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wf-label.wf-border-Fixed3D {
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #a0a0a0 #fff #fff #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.wf-textbox input,
|
.wf-textbox input,
|
||||||
.wf-textbox textarea,
|
.wf-textbox textarea,
|
||||||
.wf-combobox input {
|
.wf-combobox input {
|
||||||
@@ -105,8 +128,9 @@
|
|||||||
border-color: #0078d7;
|
border-color: #0078d7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文本框的背景色画在外层(控件的 BackColor 写在外层的 style 上),输入框本身透明 */
|
/* 文本框、下拉框的背景色画在外层(控件的 BackColor 写在外层的 style 上),输入框本身透明 */
|
||||||
.wf-textbox {
|
.wf-textbox,
|
||||||
|
.wf-combobox {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +138,28 @@
|
|||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 禁用时和 WinForms 一样灰字(设了 ForeColor 也是灰字);没设背景色时是灰底 */
|
||||||
|
.wf-textbox:has(> :disabled),
|
||||||
|
.wf-combobox:has(> :disabled) {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #6d6d6d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 多行文本框的滚动条和 WinForms 一样由 ScrollBars 决定:默认没有,Vertical / Both 一直显示竖直滚动条(自动换行时没有水平滚动条) */
|
||||||
|
.wf-textbox textarea.wf-scrollbars-None,
|
||||||
|
.wf-textbox textarea.wf-scrollbars-Horizontal {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wf-textbox textarea.wf-scrollbars-Vertical,
|
||||||
|
.wf-textbox textarea.wf-scrollbars-Both {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
.wf-textbox input,
|
.wf-textbox input,
|
||||||
.wf-textbox textarea {
|
.wf-textbox textarea,
|
||||||
|
.wf-combobox input {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,10 +177,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 挂在 body 下,位置、宽度、缩放和字体由 ControlView.vue 按下拉框算好 */
|
/* 挂在 body 下,位置、宽度、缩放、字体和颜色由 ControlView.vue 按下拉框算好 */
|
||||||
.wf-combobox-list {
|
.wf-combobox-list {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
@@ -464,6 +507,12 @@
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 不确定状态(CheckState.Indeterminate):框里一个实心小方块 */
|
||||||
|
.wf-checkbox-box[data-indeterminate='true'] {
|
||||||
|
background: #333;
|
||||||
|
box-shadow: inset 0 0 0 2px #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.wf-radio-dot[data-checked='true'] {
|
.wf-radio-dot[data-checked='true'] {
|
||||||
background: radial-gradient(circle, #333 0 3px, #fff 3.5px);
|
background: radial-gradient(circle, #333 0 3px, #fff 3.5px);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user