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