995 lines
35 KiB
C#
995 lines
35 KiB
C#
/*
|
||
* 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 <https://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
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<IsUIAccessDelegate>(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[]
|
||
{
|
||
"善学教育积分卡汇率计算器",
|
||
"版本号: 1.0.0",
|
||
"作者: ZerkyLiu",
|
||
$"构建时间:{buildTime}",
|
||
"运行环境:.NET 4.7",
|
||
$"系统信息:{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;
|
||
}
|
||
}
|