项目 7 情景控制:虚拟时钟、项目资源、Windows Media Player、TrackBar

- 虚拟时钟:实训台时钟面板可拨时间、1×/10×/60× 倍速、恢复真实时间;编译时把
  DateTime.Now / Today / UtcNow 改写成 SimClock;评分规则加 clock 步骤
- 项目资源(Properties.Resources):平台素材库(自绘开关图片、合成门铃声),"项目资源"对话框勾选,
  平台生成 Properties/Resources.Designer.cs;设计器图片框的"图片"属性可选资源;程序目录下的 Resources
  指向素材库;未添加资源时给中文提示
- Windows Media Player(AxWMPLib):设置 URL 自动播放、Ctlcontrols.play/stop,浏览器里播放声音,
  实训台加广播喇叭;编译时给窗体嵌空资源,OcxState 取到 null 不报错;图片和声音统一按内容缓存(blob)
- TrackBar:拖动、点轨道、方向键,先 ValueChanged 再 Scroll;设计器加"滑动条""媒体播放器"
- 评分规则加 value、media 检查;示例项目 7 任务 1–3(修正书上开关灯判断写反、漏填串口列表)
- 修正:Properties/ 下的资源类不当作窗体;带命名空间的 System.AppDomain.CurrentDomain.BaseDirectory 放行
- 测试:虚拟时钟、TrackBar、项目 7 示例评分、作业起点带资源类

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-23 14:11:05 +08:00
1 parent a9af26c2d6
commit eedf1aa2fb
66 files changed
+3538 -43

No files matched your search

@@ -0,0 +1,166 @@
using System;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json.Nodes;
using System.Windows.Forms;
using Sim.Host;
namespace System.Windows.Forms
{
/// <summary>
/// ActiveX 控件的基类。VS 把 COM 控件拖到窗体上时生成 OcxState = resources.GetObject("…OcxState"),
/// 平台编译时给每个窗体嵌了空的资源,这里取到的是 null,不影响使用。
/// </summary>
public abstract class AxHost : Control, ISupportInitialize
{
protected AxHost(string clsid) { }
public State? OcxState { get; set; }
public void BeginInit() { }
public void EndInit() { }
public class State { }
}
}
namespace WMPLib
{
public enum WMPPlayState
{
wmppsUndefined = 0, wmppsStopped = 1, wmppsPaused = 2, wmppsPlaying = 3, wmppsScanForward = 4, wmppsScanReverse = 5,
wmppsBuffering = 6, wmppsWaiting = 7, wmppsMediaEnded = 8, wmppsTransitioning = 9, wmppsReady = 10, wmppsReconnecting = 11,
}
/// <summary>播放控制:axWindowsMediaPlayer1.Ctlcontrols.play()</summary>
public interface IWMPControls
{
void play();
void pause();
void stop();
double currentPosition { get; set; }
}
/// <summary>播放设置:axWindowsMediaPlayer1.settings.volume</summary>
public interface IWMPSettings
{
bool autoStart { get; set; }
int volume { get; set; }
bool mute { get; set; }
void setMode(string bstrMode, bool varfMode);
}
}
namespace AxWMPLib
{
using WMPLib;
/// <summary>
/// Windows Media Player 控件(教材项目 7 的"广播"播放 ring.mp3)。和真的一样:设置 URL 后自动开始播放(settings.autoStart),
/// 也可以调用 Ctlcontrols.play()。声音在浏览器里播放;实训台上的广播喇叭随之亮起。只支持本机文件。
/// </summary>
[ClientType("AxWindowsMediaPlayer")]
public class AxWindowsMediaPlayer : AxHost
{
private string _url = "";
private WMPPlayState _playState = WMPPlayState.wmppsUndefined;
public AxWindowsMediaPlayer() : base("6bf52a52-394a-11d3-b153-00c04f79faa6")
{
Ctlcontrols = new Controls(this);
settings = new Settings();
}
protected override Size DefaultSize => new(245, 240);
public string URL
{
get => _url;
set
{
_url = value ?? "";
if (settings.autoStart && _url.Length > 0) Play();
}
}
public IWMPControls Ctlcontrols { get; }
// 属性名和 COM 接口一样是小写
public IWMPSettings settings { get; }
public WMPPlayState playState => _playState;
public string uiMode { get; set; } = "full";
public bool stretchToFit { get; set; }
public bool enableContextMenu { get; set; } = true;
public string status => _playState == WMPPlayState.wmppsPlaying ? "正在播放" : "已停止";
public event EventHandler? PlayStateChange;
private void Play()
{
LegacyThreads.CheckPoint();
// 教材写 BaseDirectory + "//Resources//ring.mp3":路径里多余的斜杠在 Linux 上也能用
var path = _url.StartsWith("file:///", StringComparison.OrdinalIgnoreCase) ? new Uri(_url).LocalPath : _url;
if (!File.Exists(path))
{
SimHost.SendConsole($"Windows Media Player:找不到要播放的文件 {_url}\n");
SetState(WMPPlayState.wmppsStopped);
return;
}
var data = File.ReadAllBytes(path);
var mime = Path.GetExtension(path).ToLowerInvariant() switch
{
".wav" => "audio/wav",
".ogg" => "audio/ogg",
".m4a" or ".aac" => "audio/aac",
_ => "audio/mpeg",
};
var key = SimHost.DataKey(Convert.ToHexString(SHA1.HashData(data)).ToLowerInvariant(), data, mime);
SimHost.Send(new JsonObject { ["type"] = "media", ["id"] = SimId, ["action"] = "play", ["key"] = key, ["name"] = Path.GetFileName(path) });
SetState(WMPPlayState.wmppsPlaying);
}
private void Stop()
{
if (_playState != WMPPlayState.wmppsPlaying) return;
SimHost.Send(new JsonObject { ["type"] = "media", ["id"] = SimId, ["action"] = "stop" });
SetState(WMPPlayState.wmppsStopped);
}
private void SetState(WMPPlayState state)
{
if (_playState == state) return;
_playState = state;
PlayStateChange?.Invoke(this, EventArgs.Empty);
}
protected override void WriteProps(JsonObject props)
{
base.WriteProps(props);
props["URL"] = _url;
}
private sealed class Controls(AxWindowsMediaPlayer owner) : IWMPControls
{
public void play()
{
if (owner._playState != WMPPlayState.wmppsPlaying && owner._url.Length > 0) owner.Play();
}
public void pause() => owner.Stop();
public void stop() => owner.Stop();
public double currentPosition { get; set; }
}
private sealed class Settings : IWMPSettings
{
public bool autoStart { get; set; } = true;
public int volume { get; set; } = 50;
public bool mute { get; set; }
public void setMode(string bstrMode, bool varfMode) { }
}
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ public class DateTimePicker : Control
public static readonly DateTime MinimumDateTime = new(1753, 1, 1);
public static readonly DateTime MaximumDateTime = new(9998, 12, 31);
private DateTime _value = DateTime.Now;
private DateTime _value = SimClock.Now;
private DateTimePickerFormat _format = DateTimePickerFormat.Long;
private string? _customFormat;
private bool _showUpDown;
+123
View File
@@ -0,0 +1,123 @@
using System.ComponentModel;
using System.Drawing;
using System.Text.Json.Nodes;
using Sim.Host;
namespace System.Windows.Forms;
public enum TickStyle { None, TopLeft, BottomRight, Both }
public enum Orientation { Horizontal, Vertical }
/// <summary>滑动条(教材项目 7 模拟空调温度、窗帘开合)</summary>
public class TrackBar : Control, ISupportInitialize
{
private int _minimum;
private int _maximum = 10;
private int _value;
private int _tickFrequency = 1;
private TickStyle _tickStyle = TickStyle.BottomRight;
private Orientation _orientation = Orientation.Horizontal;
private bool _initializing;
public TrackBar() => AutoSize = true;
protected override Size DefaultSize => new(104, 45);
public int Minimum
{
get => _minimum;
set => SetRange(value, Math.Max(value, _maximum));
}
public int Maximum
{
get => _maximum;
set => SetRange(Math.Min(value, _minimum), value);
}
public int Value
{
get => _value;
set
{
if (!_initializing && (value < _minimum || value > _maximum))
throw new ArgumentOutOfRangeException(nameof(value), $"“{value}”的值对于“Value”无效。“Value”应介于 'Minimum' 和 'Maximum' 之间。");
if (SetProp(ref _value, value, nameof(Value))) OnValueChanged(EventArgs.Empty);
}
}
public int TickFrequency
{
get => _tickFrequency;
set => SetProp(ref _tickFrequency, value, nameof(TickFrequency));
}
public TickStyle TickStyle
{
get => _tickStyle;
set => SetProp(ref _tickStyle, value, nameof(TickStyle));
}
public Orientation Orientation
{
get => _orientation;
set
{
if (value == _orientation) return;
SetProp(ref _orientation, value, nameof(Orientation));
Size = new Size(Height, Width);
}
}
public int LargeChange { get; set; } = 5;
public int SmallChange { get; set; } = 1;
public event EventHandler? Scroll;
public event EventHandler? ValueChanged;
protected virtual void OnScroll(EventArgs e) => Scroll?.Invoke(this, e);
protected virtual void OnValueChanged(EventArgs e) => ValueChanged?.Invoke(this, e);
public void SetRange(int minValue, int maxValue)
{
SetProp(ref _minimum, minValue, nameof(Minimum));
SetProp(ref _maximum, Math.Max(minValue, maxValue), nameof(Maximum));
if (_value < _minimum) Value = _minimum;
else if (_value > _maximum) Value = _maximum;
}
// Designer.cs 里 VS 生成 ((ISupportInitialize)trackBar1).BeginInit() / EndInit():期间先设 Value 再设范围也不报错
public void BeginInit() => _initializing = true;
public void EndInit()
{
_initializing = false;
if (_value < _minimum || _value > _maximum) Value = Math.Clamp(_value, _minimum, _maximum);
}
/// <summary>用户拖动滑块、点轨道或按方向键:和 WinForms 一样先改 Value(ValueChanged),再触发 Scroll</summary>
protected override void OnUserAction(UserAction action)
{
if (action.Action != "scroll" || !int.TryParse(action.Value, out var value))
{
base.OnUserAction(action);
return;
}
Value = Math.Clamp(value, _minimum, _maximum);
OnScroll(EventArgs.Empty);
}
protected override void WriteProps(JsonObject props)
{
base.WriteProps(props);
props["Minimum"] = _minimum;
props["Maximum"] = _maximum;
props["Value"] = _value;
props["TickFrequency"] = _tickFrequency;
props["TickStyle"] = _tickStyle.ToString();
props["Orientation"] = _orientation.ToString();
props["LargeChange"] = LargeChange;
props["SmallChange"] = SmallChange;
}
}