上传文件至「/」
This commit is contained in:
+994
@@ -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 <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[]
|
||||
{
|
||||
"善学教育积分卡汇率计算器",
|
||||
$"版本号:{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;
|
||||
}
|
||||
}
|
||||
+16
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="善学教育积分卡汇率计算器"/>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user