diff --git a/MainForm.cs b/MainForm.cs
new file mode 100644
index 0000000..be87601
--- /dev/null
+++ b/MainForm.cs
@@ -0,0 +1,994 @@
+/*
+ * MyCSharpProject - Shanxue-Education-Points-Coin-Converter
+ * Copyright (C) 2026 ZerkyLiu
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+using System;
+using System.Drawing;
+using System.Globalization;
+using System.Net;
+using System.Net.Sockets;
+using System.IO;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace 善学教育积分卡汇率计算器
+{
+ public class MainForm : Form
+ {
+ private static readonly decimal[] RatePoints = { 500m, 1000m, 1500m, 2000m, 2500m, 3000m, 3500m, 4000m };
+ private static readonly decimal[] RateRmb = { 10m, 20m, 35m, 45m, 60m, 75m, 85m, 100m };
+ private static readonly string[] Zodiac = { "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪" };
+ private static readonly string[] NtpServers = { "ntp.aliyun.com", "ntp.ntsc.ac.cn", "pool.ntp.org" };
+ private static readonly string RateTextHorizontal = BuildRateText(true);
+ private static readonly string RateTextVertical = BuildRateText(false);
+ private readonly Label _currencyLabel;
+ private readonly Label _timeLabel;
+ private readonly Label _timeSourceLabel;
+ private readonly CheckBox _topMostToggle;
+ private readonly Button _aboutButton;
+ private readonly Label _inputLabel;
+ private readonly Label _rmbLabel;
+ private readonly TextBox _pointsInput;
+ private readonly Label _rmbOutput;
+ private readonly Label _rateLabel;
+ private readonly Timer _clockTimer;
+ private readonly Timer _syncTimer;
+ private readonly Timer _fallbackTopMostTimer;
+ private DateTime _currentTime;
+ private string _timeSource;
+ private IntPtr _uiAccessDll;
+ private string? _uiAccessDllPath;
+ private IsUIAccessDelegate? _isUiAccess;
+ private bool _uiAccessEnabled;
+ private bool _usingFallbackTopMost;
+ private bool _usingUiAccessTopMost;
+ private bool _suppressPointsTextChange;
+ private readonly bool _forceFallbackTopMost;
+ private const string UiAccessRelaunchArg = "--uiaccess-relaunch";
+ private const string UiAccessResourceSuffix = ".dll.uiaccess.dll";
+ private const string IconResourceSuffix = ".ico.logo_256x256.ico";
+ private float _rmbOutputBaseFontSize;
+
+ public MainForm()
+ {
+ Text = "善学教育积分卡汇率计算器";
+ StartPosition = FormStartPosition.CenterScreen;
+ ClientSize = new Size(640, 730);
+ MinimumSize = SizeFromClientSize(ClientSize);
+ Font = new Font("Microsoft YaHei UI", 10F);
+ BackColor = Color.WhiteSmoke;
+ AutoScaleMode = AutoScaleMode.Dpi;
+ DoubleBuffered = true;
+ SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
+
+ var layout = new TableLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ ColumnCount = 1,
+ RowCount = 4,
+ Padding = new Padding(28),
+ };
+ layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
+ layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
+ layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+
+ _currencyLabel = new Label
+ {
+ AutoSize = true,
+ Font = new Font("Microsoft YaHei UI", 18F, FontStyle.Bold),
+ ForeColor = Color.FromArgb(38, 50, 56),
+ Dock = DockStyle.Fill,
+ TextAlign = ContentAlignment.MiddleLeft
+ };
+ layout.Controls.Add(_currencyLabel, 0, 0);
+
+ _timeLabel = new Label
+ {
+ AutoSize = true,
+ ForeColor = Color.FromArgb(55, 71, 79),
+ Dock = DockStyle.Fill,
+ TextAlign = ContentAlignment.MiddleLeft
+ };
+
+ _topMostToggle = new CheckBox
+ {
+ Text = "超级置顶",
+ AutoSize = true,
+ Dock = DockStyle.Fill,
+ TextAlign = ContentAlignment.MiddleRight
+ };
+ _topMostToggle.CheckedChanged += (_, __) => ApplyTopMost(_topMostToggle.Checked);
+
+ _aboutButton = new Button
+ {
+ Text = "关于",
+ AutoSize = true,
+ Dock = DockStyle.Fill
+ };
+ _aboutButton.Click += (_, __) => ShowAboutDialog();
+
+ _timeSourceLabel = new Label
+ {
+ AutoSize = true,
+ ForeColor = Color.FromArgb(96, 125, 139),
+ Dock = DockStyle.Fill,
+ TextAlign = ContentAlignment.MiddleLeft
+ };
+ var rightPanel = new TableLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ ColumnCount = 1,
+ RowCount = 2,
+ AutoSize = true
+ };
+ rightPanel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
+ rightPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ rightPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ rightPanel.Controls.Add(_aboutButton, 0, 0);
+ rightPanel.Controls.Add(_topMostToggle, 0, 1);
+ var timeRow = new TableLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ ColumnCount = 2,
+ RowCount = 1,
+ AutoSize = true
+ };
+ timeRow.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
+ timeRow.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
+ timeRow.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ timeRow.Controls.Add(_timeLabel, 0, 0);
+ timeRow.Controls.Add(rightPanel, 1, 0);
+ var timePanel = new TableLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ ColumnCount = 1,
+ RowCount = 2,
+ AutoSize = true
+ };
+ timePanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
+ timePanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ timePanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ timePanel.Controls.Add(timeRow, 0, 0);
+ timePanel.Controls.Add(_timeSourceLabel, 0, 1);
+ layout.Controls.Add(timePanel, 0, 1);
+
+ _inputLabel = new Label
+ {
+ Text = "输入积分币数量",
+ AutoSize = true,
+ Dock = DockStyle.Fill,
+ TextAlign = ContentAlignment.MiddleLeft
+ };
+
+ _pointsInput = new TextBox
+ {
+ Dock = DockStyle.Fill,
+ TextAlign = HorizontalAlignment.Left,
+ ImeMode = ImeMode.Disable
+ };
+ _pointsInput.TextChanged += PointsInputOnTextChanged;
+ _pointsInput.KeyPress += PointsInputOnKeyPress;
+ _rmbLabel = new Label
+ {
+ Text = "换算人民币(元)",
+ AutoSize = true,
+ Dock = DockStyle.Fill,
+ TextAlign = ContentAlignment.MiddleLeft
+ };
+
+ _rmbOutput = new Label
+ {
+ Text = "0.00",
+ AutoSize = true,
+ Font = new Font("Microsoft YaHei UI", 22F, FontStyle.Bold),
+ ForeColor = Color.FromArgb(0, 150, 136),
+ Dock = DockStyle.Fill,
+ TextAlign = ContentAlignment.MiddleCenter
+ };
+ var convLayout = new TableLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ ColumnCount = 2,
+ RowCount = 2,
+ AutoSize = false
+ };
+ convLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 40F));
+ convLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 60F));
+ convLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
+ convLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
+ convLayout.Controls.Add(_inputLabel, 0, 0);
+ convLayout.Controls.Add(_pointsInput, 1, 0);
+ convLayout.Controls.Add(_rmbLabel, 0, 1);
+ convLayout.Controls.Add(_rmbOutput, 1, 1);
+ layout.Controls.Add(convLayout, 0, 2);
+
+ _rateLabel = new Label
+ {
+ Text = GetRateText(true),
+ AutoSize = true,
+ ForeColor = Color.FromArgb(96, 125, 139),
+ Dock = DockStyle.Fill,
+ TextAlign = ContentAlignment.MiddleLeft
+ };
+ layout.Controls.Add(_rateLabel, 0, 3);
+ Controls.Add(layout);
+
+ _timeSource = "本地";
+ _currentTime = DateTime.Now;
+
+ _clockTimer = new Timer { Interval = 1000 };
+ _clockTimer.Tick += ClockTimerOnTick;
+
+ _syncTimer = new Timer { Interval = 10 * 60 * 1000 };
+ _syncTimer.Tick += async (_, __) => await SyncNetworkTimeAsync();
+
+ _fallbackTopMostTimer = new Timer { Interval = 1 };
+ _fallbackTopMostTimer.Tick += (_, __) => ApplyFallbackTopMostTick();
+ _forceFallbackTopMost = IsWin7OrLower();
+
+ ClientSizeChanged += (_, __) => { UpdateRateLayout(); UpdateResponsiveFonts(); UpdateConversion(); };
+ Shown += (_, __) => _ = InitializeAsync();
+ }
+
+ private Task InitializeAsync()
+ {
+ TryInitializeIcon();
+ TryInitializeUiAccess();
+ if (TryRelaunchWithUiAccess())
+ {
+ return Task.CompletedTask;
+ }
+ UpdateTimeLabels();
+ UpdateRateLayout();
+ UpdateResponsiveFonts();
+ UpdateConversion();
+ StartInputFocus();
+ _ = SyncNetworkTimeAsync();
+ _clockTimer.Start();
+ _syncTimer.Start();
+ return Task.CompletedTask;
+ }
+
+ private void UpdateRateLayout()
+ {
+ var horizontal = ClientSize.Width >= 720;
+ _rateLabel.MaximumSize = new Size(Math.Max(0, ClientSize.Width - 120), 0);
+ _rateLabel.Text = horizontal ? RateTextHorizontal : RateTextVertical;
+ }
+
+ private void UpdateResponsiveFonts()
+ {
+ var w = ClientSize.Width;
+ var h = ClientSize.Height;
+ var resultSize = (float)Math.Max(24, Math.Min(w * 0.06, h * 0.08));
+ var inputSize = (float)Math.Max(12, Math.Min(w * 0.03, h * 0.04));
+ _rmbOutputBaseFontSize = resultSize;
+ _rmbOutput.Font = new Font("Microsoft YaHei UI", resultSize, FontStyle.Bold);
+ _pointsInput.Font = new Font("Microsoft YaHei UI", inputSize, FontStyle.Regular);
+ }
+ private static string GetRateText(bool horizontal)
+ {
+ return horizontal ? RateTextHorizontal : RateTextVertical;
+ }
+
+ private static string BuildRateText(bool horizontal)
+ {
+ var header = "汇率:";
+ if (horizontal)
+ {
+ var text = header;
+ for (var i = 0; i < RatePoints.Length; i++)
+ {
+ text += $"{RatePoints[i]}币={RateRmb[i]}";
+ if (i < RatePoints.Length - 1)
+ {
+ text += ",";
+ }
+ }
+
+ return text;
+ }
+
+ var newline = Environment.NewLine;
+ var multi = header + newline;
+ for (var i = 0; i < RatePoints.Length; i++)
+ {
+ multi += $"{RatePoints[i]}币={RateRmb[i]}";
+ if (i < RatePoints.Length - 1)
+ {
+ multi += newline;
+ }
+ }
+
+ return multi;
+ }
+
+ private void UpdateTimeLabels()
+ {
+ var timeText = _currentTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
+ _timeLabel.Text = $"当前时间:{timeText}";
+ _timeSourceLabel.Text = $"时间来源:{_timeSource}";
+ _currencyLabel.Text = $"当前积分币:{GetZodiacCurrency(_currentTime)}";
+ }
+
+ private void ClockTimerOnTick(object? sender, EventArgs e)
+ {
+ _currentTime = _currentTime.AddSeconds(1);
+ UpdateTimeLabels();
+ }
+
+ private async Task SyncNetworkTimeAsync()
+ {
+ var networkTime = await Task.Run(TryGetNetworkTime);
+ if (networkTime.HasValue)
+ {
+ _currentTime = networkTime.Value.ToLocalTime();
+ _timeSource = "NTP时间服务器";
+ }
+ else
+ {
+ _currentTime = DateTime.Now;
+ _timeSource = "本地";
+ }
+
+ UpdateTimeLabels();
+ }
+
+ private static DateTime? TryGetNetworkTime()
+ {
+ foreach (var server in NtpServers)
+ {
+ try
+ {
+ var ntpData = new byte[48];
+ ntpData[0] = 0x1B;
+ var addresses = Dns.GetHostEntry(server).AddressList;
+ foreach (var address in addresses)
+ {
+ if (address.AddressFamily != AddressFamily.InterNetwork)
+ {
+ continue;
+ }
+
+ var ipEndPoint = new IPEndPoint(address, 123);
+ using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
+ {
+ socket.ReceiveTimeout = 1500;
+ socket.SendTimeout = 1500;
+ socket.Connect(ipEndPoint);
+ socket.Send(ntpData);
+ socket.Receive(ntpData);
+ }
+
+ const byte serverReplyTime = 40;
+ var intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);
+ var fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4);
+ intPart = SwapEndianness(intPart);
+ fractPart = SwapEndianness(fractPart);
+
+ var milliseconds = (intPart * 1000L) + ((fractPart * 1000L) / 0x100000000L);
+ var networkDateTime = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(milliseconds);
+ return networkDateTime;
+ }
+ }
+ catch
+ {
+ continue;
+ }
+ }
+
+ return null;
+ }
+
+ private static uint SwapEndianness(uint x)
+ {
+ return (x & 0x000000FFU) << 24 | (x & 0x0000FF00U) << 8 | (x & 0x00FF0000U) >> 8 | (x & 0xFF000000U) >> 24;
+ }
+
+ private static string GetZodiacCurrency(DateTime time)
+ {
+ var index = Mod(time.Year - 2020, 12);
+ return $"{Zodiac[index]}币";
+ }
+
+ private static int Mod(int x, int m)
+ {
+ var r = x % m;
+ return r < 0 ? r + m : r;
+ }
+
+ private void StartInputFocus()
+ {
+ if (!IsHandleCreated)
+ {
+ return;
+ }
+
+ BeginInvoke(new Action(() =>
+ {
+ ActiveControl = _pointsInput;
+ _pointsInput.Focus();
+ _pointsInput.SelectionStart = _pointsInput.TextLength;
+ }));
+ }
+
+ private static bool IsWin7OrLower()
+ {
+ var version = Environment.OSVersion.Version;
+ return version.Major < 6 || (version.Major == 6 && version.Minor <= 1);
+ }
+
+ private void PointsInputOnTextChanged(object? sender, EventArgs e)
+ {
+ if (_suppressPointsTextChange)
+ {
+ return;
+ }
+
+ var rawText = _pointsInput.Text;
+ var filteredText = new string(rawText.Where(char.IsDigit).ToArray());
+ if (!string.Equals(rawText, filteredText, StringComparison.Ordinal))
+ {
+ var selectionStart = _pointsInput.SelectionStart;
+ var removedCount = rawText.Length - filteredText.Length;
+ _suppressPointsTextChange = true;
+ _pointsInput.Text = filteredText;
+ _pointsInput.SelectionStart = Math.Max(0, Math.Min(filteredText.Length, selectionStart - removedCount));
+ _suppressPointsTextChange = false;
+ }
+
+ UpdateConversion();
+ }
+
+ private void PointsInputOnKeyPress(object? sender, KeyPressEventArgs e)
+ {
+ if (char.IsControl(e.KeyChar) || char.IsDigit(e.KeyChar))
+ {
+ return;
+ }
+
+ e.Handled = true;
+ }
+
+ private void UpdateConversion()
+ {
+ var rawText = _pointsInput.Text.Trim();
+ if (string.IsNullOrEmpty(rawText))
+ {
+ SetRmbOutputText("0.00", GetRmbOutputBaseSize());
+ return;
+ }
+
+ if (!decimal.TryParse(rawText, out var points))
+ {
+ SetRmbOutputText("TL", GetRmbOutputBaseSize());
+ return;
+ }
+
+ if (points <= 0)
+ {
+ SetRmbOutputText("0.00", GetRmbOutputBaseSize());
+ return;
+ }
+
+ try
+ {
+ var rmb = ConvertPointsToRmb(points);
+ if (rmb < 0.01m)
+ {
+ SetRmbOutputText("0.00", GetRmbOutputBaseSize());
+ return;
+ }
+
+ UpdateRmbDisplay(rmb);
+ }
+ catch
+ {
+ SetRmbOutputText("TL", GetRmbOutputBaseSize());
+ }
+ }
+
+ private static decimal ConvertPointsToRmb(decimal points)
+ {
+ var pointSteps = new[] { 0m, RatePoints[0], RatePoints[1], RatePoints[2], RatePoints[3], RatePoints[4], RatePoints[5], RatePoints[6], RatePoints[7] };
+ var rmbSteps = new[] { 0m, RateRmb[0], RateRmb[1], RateRmb[2], RateRmb[3], RateRmb[4], RateRmb[5], RateRmb[6], RateRmb[7] };
+
+ if (points <= pointSteps[0])
+ {
+ return 0m;
+ }
+
+ if (points >= pointSteps[pointSteps.Length - 1])
+ {
+ return Interpolate(points, pointSteps[pointSteps.Length - 2], pointSteps[pointSteps.Length - 1], rmbSteps[rmbSteps.Length - 2], rmbSteps[rmbSteps.Length - 1]);
+ }
+
+ for (var i = 1; i < pointSteps.Length; i++)
+ {
+ if (points <= pointSteps[i])
+ {
+ return Interpolate(points, pointSteps[i - 1], pointSteps[i], rmbSteps[i - 1], rmbSteps[i]);
+ }
+ }
+
+ return 0m;
+ }
+
+ private static decimal Interpolate(decimal x, decimal x1, decimal x2, decimal y1, decimal y2)
+ {
+ if (x2 == x1)
+ {
+ return y1;
+ }
+
+ var ratio = (x - x1) / (x2 - x1);
+ return y1 + (y2 - y1) * ratio;
+ }
+
+ private void UpdateRmbDisplay(decimal rmb)
+ {
+ var baseSize = GetRmbOutputBaseSize();
+ var minSize = _rmbLabel.Font.Size;
+ var normalText = rmb.ToString("F2", CultureInfo.InvariantCulture);
+ var normalResult = FitTextToRmbOutput(normalText, baseSize, minSize);
+ if (normalResult.Fits)
+ {
+ SetRmbOutputText(normalResult.Text, normalResult.FontSize);
+ return;
+ }
+
+ var sciText = rmb.ToString("0.###E+0", CultureInfo.InvariantCulture);
+ var sciResult = FitTextToRmbOutput(sciText, baseSize, minSize);
+ if (sciResult.Fits)
+ {
+ SetRmbOutputText(sciResult.Text, sciResult.FontSize);
+ return;
+ }
+
+ var ellipsisText = "…请放大窗口";
+ var ellipsisResult = FitTextToRmbOutput(ellipsisText, baseSize, minSize);
+ SetRmbOutputText(ellipsisResult.Text, ellipsisResult.FontSize);
+ }
+
+ private float GetRmbOutputBaseSize()
+ {
+ var baseSize = _rmbOutputBaseFontSize > 0 ? _rmbOutputBaseFontSize : _rmbOutput.Font.Size;
+ return Math.Max(baseSize, _rmbLabel.Font.Size);
+ }
+
+ private (string Text, float FontSize, bool Fits) FitTextToRmbOutput(string text, float baseSize, float minSize)
+ {
+ var targetSize = Math.Max(baseSize, minSize);
+ if (TryFitText(text, targetSize))
+ {
+ return (text, targetSize, true);
+ }
+
+ for (var size = targetSize; size >= minSize; size -= 0.5f)
+ {
+ if (TryFitText(text, size))
+ {
+ return (text, size, true);
+ }
+ }
+
+ return (text, minSize, false);
+ }
+
+ private bool TryFitText(string text, float fontSize)
+ {
+ var availableWidth = _rmbOutput.ClientSize.Width;
+ var availableHeight = _rmbOutput.ClientSize.Height;
+ if (availableWidth <= 0 || availableHeight <= 0)
+ {
+ return true;
+ }
+
+ using (var font = new Font(_rmbOutput.Font.FontFamily, fontSize, _rmbOutput.Font.Style))
+ {
+ var size = TextRenderer.MeasureText(text, font, new Size(int.MaxValue, int.MaxValue), TextFormatFlags.NoPadding);
+ return size.Width <= availableWidth && size.Height <= availableHeight;
+ }
+ }
+
+ private void SetRmbOutputText(string text, float fontSize)
+ {
+ if (Math.Abs(_rmbOutput.Font.Size - fontSize) > 0.1f)
+ {
+ _rmbOutput.Font = new Font(_rmbOutput.Font.FontFamily, fontSize, _rmbOutput.Font.Style);
+ }
+ _rmbOutput.Text = text;
+ }
+
+ protected override void OnFormClosed(FormClosedEventArgs e)
+ {
+ if (_fallbackTopMostTimer != null)
+ {
+ _fallbackTopMostTimer.Stop();
+ }
+
+ if (_uiAccessDll != IntPtr.Zero)
+ {
+ FreeLibrary(_uiAccessDll);
+ _uiAccessDll = IntPtr.Zero;
+ }
+
+ base.OnFormClosed(e);
+ }
+
+ private void TryInitializeUiAccess()
+ {
+ if (_uiAccessDll != IntPtr.Zero)
+ {
+ return;
+ }
+
+ var dllPath = EnsureUiAccessExtracted();
+ if (dllPath == null)
+ {
+ return;
+ }
+
+ _uiAccessDll = LoadLibraryW(dllPath);
+ if (_uiAccessDll == IntPtr.Zero)
+ {
+ return;
+ }
+
+ var proc = GetProcAddress(_uiAccessDll, "IsUIAccess");
+ if (proc == IntPtr.Zero)
+ {
+ FreeLibrary(_uiAccessDll);
+ _uiAccessDll = IntPtr.Zero;
+ return;
+ }
+
+ _isUiAccess = Marshal.GetDelegateForFunctionPointer(proc);
+ _uiAccessEnabled = _isUiAccess();
+ }
+
+ private void TryInitializeIcon()
+ {
+ try
+ {
+ using (var stream = GetEmbeddedResourceStream(IconResourceSuffix))
+ {
+ if (stream == null)
+ {
+ return;
+ }
+
+ using (var icon = new Icon(stream))
+ {
+ Icon = (Icon)icon.Clone();
+ }
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ private void ShowAboutDialog()
+ {
+ var dialog = new Form
+ {
+ Text = "关于",
+ StartPosition = FormStartPosition.CenterParent,
+ FormBorderStyle = FormBorderStyle.FixedDialog,
+ MaximizeBox = false,
+ MinimizeBox = false,
+ ShowInTaskbar = false,
+ ClientSize = new Size(520, 420)
+ };
+
+ var textBox = new TextBox
+ {
+ Multiline = true,
+ ReadOnly = true,
+ ScrollBars = ScrollBars.Vertical,
+ Dock = DockStyle.Fill,
+ Font = new Font("Microsoft YaHei UI", 10F),
+ BackColor = SystemColors.Window,
+ BorderStyle = BorderStyle.None
+ };
+
+ var buildTime = File.Exists(Application.ExecutablePath)
+ ? File.GetLastWriteTime(Application.ExecutablePath).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
+ : "未知";
+
+ textBox.Text = string.Join(Environment.NewLine, new[]
+ {
+ "善学教育积分卡汇率计算器",
+ $"版本号:{Application.ProductVersion}",
+ "作者:ZerkyLiu",
+ $"构建时间:{buildTime}",
+ $"运行环境:.NET {Environment.Version}",
+ $"系统信息:{Environment.OSVersion}",
+ $"UIAccess状态:{(_uiAccessEnabled ? "已启用" : "未启用")}",
+ "",
+ "功能亮点:",
+ "• 动态自适应结果展示",
+ "• 网络时间同步",
+ "• 高精度阶梯换算",
+ "• UIAccess 置顶支持",
+ "",
+ "未来计划:",
+ "• 联网自动更新",
+ "• 修复已知问题"
+ });
+
+ dialog.Controls.Add(textBox);
+ dialog.ShowDialog(this);
+ }
+
+ private bool TryRelaunchWithUiAccess()
+ {
+ if (_uiAccessEnabled)
+ {
+ return false;
+ }
+
+ var args = Environment.GetCommandLineArgs();
+ foreach (var arg in args)
+ {
+ if (string.Equals(arg, UiAccessRelaunchArg, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+
+ var dllPath = EnsureUiAccessExtracted();
+ if (string.IsNullOrEmpty(dllPath))
+ {
+ return false;
+ }
+
+ var exePath = Application.ExecutablePath;
+ var forwardedArgs = BuildForwardedArgs(args);
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "rundll32.exe",
+ Arguments = $"\"{dllPath}\",run \"{exePath}\" {forwardedArgs} {UiAccessRelaunchArg}",
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ try
+ {
+ Process.Start(startInfo);
+ BeginInvoke(new Action(Close));
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private string? EnsureUiAccessExtracted()
+ {
+ if (!string.IsNullOrEmpty(_uiAccessDllPath) && File.Exists(_uiAccessDllPath))
+ {
+ return _uiAccessDllPath;
+ }
+
+ var tempDir = Path.Combine(Path.GetTempPath(), "善学教育积分卡汇率计算器");
+ var dllPath = Path.Combine(tempDir, $"uiaccess_{Process.GetCurrentProcess().Id}.dll");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+ if (File.Exists(dllPath))
+ {
+ _uiAccessDllPath = dllPath;
+ return dllPath;
+ }
+
+ using (var stream = GetEmbeddedResourceStream(UiAccessResourceSuffix))
+ {
+ if (stream == null)
+ {
+ return null;
+ }
+
+ using (var file = new FileStream(dllPath, FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite))
+ {
+ stream.CopyTo(file);
+ }
+ }
+
+ _uiAccessDllPath = dllPath;
+ return dllPath;
+ }
+ catch
+ {
+ if (File.Exists(dllPath))
+ {
+ _uiAccessDllPath = dllPath;
+ return dllPath;
+ }
+
+ return null;
+ }
+ }
+
+ private static Stream? GetEmbeddedResourceStream(string suffix)
+ {
+ var assembly = typeof(MainForm).Assembly;
+ var resourceName = assembly.GetManifestResourceNames()
+ .FirstOrDefault(name => name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase));
+ if (resourceName == null)
+ {
+ return null;
+ }
+
+ return assembly.GetManifestResourceStream(resourceName);
+ }
+
+ private static string BuildForwardedArgs(string[] args)
+ {
+ if (args.Length <= 1)
+ {
+ return string.Empty;
+ }
+
+ var builder = new System.Text.StringBuilder();
+ for (var i = 1; i < args.Length; i++)
+ {
+ if (string.Equals(args[i], UiAccessRelaunchArg, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (builder.Length > 0)
+ {
+ builder.Append(' ');
+ }
+
+ builder.Append(QuoteArg(args[i]));
+ }
+
+ return builder.ToString();
+ }
+
+ private static string QuoteArg(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return "\"\"";
+ }
+
+ if (value.IndexOfAny(new[] { ' ', '\t', '"', '\\' }) == -1)
+ {
+ return value;
+ }
+
+ var escaped = value.Replace("\\", "\\\\").Replace("\"", "\\\"");
+ return $"\"{escaped}\"";
+ }
+
+ private void ApplyTopMost(bool enabled)
+ {
+ TopMost = enabled;
+ if (!enabled)
+ {
+ SetFallbackTopMost(false);
+ SetUiAccessTopMost(false);
+ if (_uiAccessEnabled && IsHandleCreated)
+ {
+ SetWindowPos(Handle, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+ }
+ return;
+ }
+
+ if (_forceFallbackTopMost)
+ {
+ SetFallbackTopMost(true);
+ ApplyFallbackTopMostTick();
+ return;
+ }
+
+ if (_uiAccessEnabled && IsHandleCreated)
+ {
+ SetFallbackTopMost(false);
+ SetUiAccessTopMost(true);
+ SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+ ApplyFallbackTopMostTick();
+ return;
+ }
+
+ SetUiAccessTopMost(false);
+ SetFallbackTopMost(true);
+ ApplyFallbackTopMostTick();
+ }
+
+ private void SetUiAccessTopMost(bool enabled)
+ {
+ if (_usingUiAccessTopMost == enabled)
+ {
+ return;
+ }
+
+ _usingUiAccessTopMost = enabled;
+ if (enabled)
+ {
+ _fallbackTopMostTimer.Start();
+ }
+ else if (!_usingFallbackTopMost)
+ {
+ _fallbackTopMostTimer.Stop();
+ }
+ }
+
+ private void SetFallbackTopMost(bool enabled)
+ {
+ if (_usingFallbackTopMost == enabled)
+ {
+ return;
+ }
+
+ _usingFallbackTopMost = enabled;
+ _topMostToggle.Text = enabled ? "超级置顶(备)" : "超级置顶";
+ if (enabled)
+ {
+ _fallbackTopMostTimer.Start();
+ }
+ else if (!_usingUiAccessTopMost)
+ {
+ _fallbackTopMostTimer.Stop();
+ }
+ }
+
+ private void ApplyFallbackTopMostTick()
+ {
+ if ((!_usingFallbackTopMost && !_usingUiAccessTopMost) || !IsHandleCreated)
+ {
+ return;
+ }
+
+ TopMost = true;
+ SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+ }
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ private delegate bool IsUIAccessDelegate();
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern IntPtr LoadLibraryW(string lpFileName);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool FreeLibrary(IntPtr hModule);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
+
+ private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
+ private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
+ private const uint SWP_NOMOVE = 0x0002;
+ private const uint SWP_NOSIZE = 0x0001;
+ private const uint SWP_NOACTIVATE = 0x0010;
+ }
+}
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..4384d41
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Windows.Forms;
+
+namespace 善学教育积分卡汇率计算器
+{
+ internal static class Program
+ {
+ [STAThread]
+ private static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.Run(new MainForm());
+ }
+ }
+}
diff --git a/app.manifest b/app.manifest
new file mode 100644
index 0000000..4ef49c6
--- /dev/null
+++ b/app.manifest
@@ -0,0 +1,10 @@
+
+
+
+
+
+ true
+ PerMonitorV2
+
+
+
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..cdc98d5
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,26 @@
+# 善学教育积分卡汇率计算器
+
+## 功能亮点
+
+- **动态自适应结果展示**
+- **网络时间同步**
+- **高精度阶梯换算**
+- **UIAccess 置顶支持**
+
+## 快速开始
+
+从[发布](http://https://gittea.dev/ZerkyLiu/Shanxue-Education-Points-Coin-Converter/releases "发布")页下载程序本体。
+
+> https://gittea.dev/ZerkyLiu/Shanxue-Education-Points-Coin-Converter/releases
+
+源码是手动上传的,可能(~~一定~~)会落后。
+
+**祝您使用愉快!**
+
+
+## 特别鸣谢
+使用了[RunUIAccess](http://https://github.com/shc0743/RunUIAccess "RunUIAccess")以实现“超级置顶”的主要功能。
+
+
+## 许可证
+AGPL3
\ No newline at end of file