Files
Just-DataInspector/Utilities/Src/Utf8Util.cpp
T
DeepSeek V4.1 Flash 97b361dedd fix(ui): 采样统计显示一致化、GX 拖拽在布局边界下可用、信号「移动到…」与拖拽排序
一、采样间隔/频率显示不一致(用户报告 1ms vs 1000us、100us 0ns)
* FormatDurationCompound 改为「先量化到整数皮秒、再整数分解」,
  彻底消除同一数值因浮点噪声落到不同单位/留下零尾数的问题;
  秒级分支与亚秒分支共用 totalPs 方案,次级进位/单位升级集中在一处。
* FormatFrequency 的单位选择改为按「舍入后的显示值」判定(999.9999999Hz
  与 1000Hz 均为 1.000kHz),并做向下校正避免 0.001kHz。

二、选区截取面板
* 提示文本改为用户指定原文「启用双光标模式以进行选区截取」。
* EdgePanelWidth 计入该提示的实际文本宽度;绘制改用 UiTextClipped 不再省略。
* viewHeight 补回漏减的 bottomPad,修掉条目区与按钮之间的空白。

三、未展开的信号条目栏悬停不再变成手型光标。

四、开启「显示布局边界」后 GX 标签可正常拖拽排序
* 抽出 PaintLaneReorderOverlay(吸附槽位 + 被拖轮廓),贴图与整幅重绘共用。
* 整幅重绘在 LANE_REORDER 时按 LaneDragOffset 逐格平移重画(被拖网格最后画),
  修掉调试模式下不跟随鼠标、无吸附提示、只能盲拖的问题。

五、信号右键新增「移动到…」二级菜单(位于「仅显示该信号」之上)
* 条目为「分组 N(组名)」,当前组打勾;无标题行;底部「新建分组」。
* MenuItem::submenuMaxRows + PopupMenu 独立子菜单滚动(滚动条、滚轮、视口命中过滤),
  IsScrollable 计入子菜单可滚状态。
* 数据层 DataSet::MoveSignalToGroup/MoveSignal 让时间轴随信号搬运
  (独占则移动、共享则复制);TabConfig::RemapAfterSignalMove 改写引用而非先删后插。
* Application::MoveSignalCore 统一入口,保证派生计算暂停、引用修正、选中迁移与同步一致。

六、信号列表支持按住拖拽排序(组内重排 / 跨组移动)
* 4px 阈值进入拖拽;条目脱离原位跟随鼠标(阴影 + 强调色描边 + 插入指示线)。
* 其它信号行/分组行按插入位置做避让位移动画(RowRect 统一叠加插值位移,
  宿主 kScrollTimerId 驱动 AdvanceSignalDragAnimation)。
* 数据层 DataSet::MoveSignal 支持任意位置;TabConfig::RemapAfterSignalReorder 线性重映射。
* 拖拽中禁用组名悬浮;双击改名与 InvalidateRows 均正确终止拖拽态。

验证:删 .o 全量重编译零 error / 零 warning;产物 873,984 B(< 1MB 预警阈值)。
本轮未使用鼠标/键盘/屏幕,未运行任何 GUI 探针,仅编译验证 + 源码级核对。
2026-11-03 19:27:43 +08:00

993 lines
27 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Utf8Util.hpp"
#include <windows.h>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace jdi
{
namespace
{
/** @brief 判断字符是否为需要裁掉的空白字符。 */
bool IsSpaceChar(wchar_t c)
{
return c == L' ' || c == L'\t' || c == L'\r' || c == L'\n' || c == L'\v' || c == L'\f' ||
c == static_cast<wchar_t>(0x00A0);
}
/** @brief 判断字符是否为 ASCII 大写字母。 */
bool IsUpperAscii(wchar_t c)
{
return c >= L'A' && c <= L'Z';
}
/** @brief 时间间隔的显示单位。 */
struct DurationUnit
{
double scale = 1.0; /**< 1 个该单位等于多少秒。 */
const wchar_t* name = L"s"; /**< 单位名称。 */
};
/**
* @brief 复合单位时间文本里**次级单位最多保留的小数位数**。
*
* 用户 2026-11-03 明确要求:`0.003696631s` 必须显示为 `3ms 696.631us`
* (次级单位 ms/us/ns 保留三位小数,能看到真实尾数),
* 而不是四舍五入到 `3ms 700us`(旧实现舍入到 3 位**有效数字**,把尾数抹掉了)。
*
* ⚠️ 这是**上限**,不是强制的固定位数:整数次级单位仍写成整数
* (`5.06s` → `5s 60ms`,而不是 `5s 60.000ms`),只有真的带小数时才补零到 3 位。
*/
constexpr int kDurationSubUnitDecimals = 3;
/**
* @brief 频率文本保留的小数位数(用户 2026-11-03 要求)。
*
* `270.516586...Hz` 必须显示为 `270.517 Hz`(真实值保留三位小数),
* 而不是"先把间隔舍入到 3 位有效数字再换算"得到的 `270.27 Hz`。
*/
constexpr int kFrequencyDecimals = 3;
/**
* @brief 格式化次级单位数值:带小数则保留 `maxDecimals` 位,整数则原样输出。
*
* `696.6314` → `696.631`;`60.0` → `60`;`0.0158` → `0.016`。
* 这样既满足"次级单位保留三位小数"(看得到真实尾数),
* 又不会把整数次级单位写成 `60.000` 这种噪音。
*/
inline std::wstring FormatSubUnit(double value, int maxDecimals)
{
if (!std::isfinite(value))
{
return L"—";
}
const double rounded = std::round(value * std::pow(10.0, maxDecimals)) /
std::pow(10.0, maxDecimals);
if (rounded == std::floor(rounded))
{
return FormatDecimal(rounded); // 整数:不加小数点
}
return FormatDecimalFixed(rounded, maxDecimals);
}
}
std::wstring Utf8ToWide(const char* text, int length)
{
if (text == nullptr)
{
return std::wstring();
}
int byteCount = (length < 0) ? static_cast<int>(std::strlen(text)) : length;
if (byteCount <= 0)
{
return std::wstring();
}
int wideCount = MultiByteToWideChar(CP_UTF8, 0, text, byteCount, nullptr, 0);
if (wideCount <= 0)
{
return std::wstring();
}
std::wstring result(static_cast<size_t>(wideCount), L'\0');
MultiByteToWideChar(CP_UTF8, 0, text, byteCount, &result[0], wideCount);
return result;
}
std::wstring Trim(const std::wstring& text)
{
size_t begin = 0;
size_t end = text.size();
while (begin < end && IsSpaceChar(text[begin]))
{
++begin;
}
while (end > begin && IsSpaceChar(text[end - 1]))
{
--end;
}
return text.substr(begin, end - begin);
}
std::wstring StripQuotes(const std::wstring& text)
{
std::wstring trimmed = Trim(text);
if (trimmed.size() >= 2)
{
const wchar_t first = trimmed.front();
const wchar_t last = trimmed.back();
if ((first == L'"' && last == L'"') || (first == L'\'' && last == L'\''))
{
trimmed = trimmed.substr(1, trimmed.size() - 2);
}
}
return trimmed;
}
std::wstring ToLowerAscii(const std::wstring& text)
{
std::wstring result = text;
for (wchar_t& c : result)
{
if (IsUpperAscii(c))
{
c = static_cast<wchar_t>(c - L'A' + L'a');
}
}
return result;
}
bool ContainsInsensitive(const std::wstring& text, const std::wstring& needle)
{
if (needle.empty())
{
return true;
}
if (needle.size() > text.size())
{
return false;
}
const size_t last = text.size() - needle.size();
for (size_t i = 0; i <= last; ++i)
{
size_t j = 0;
while (j < needle.size())
{
wchar_t a = text[i + j];
wchar_t b = needle[j];
if (IsUpperAscii(a))
{
a = static_cast<wchar_t>(a - L'A' + L'a');
}
if (IsUpperAscii(b))
{
b = static_cast<wchar_t>(b - L'A' + L'a');
}
if (a != b)
{
break;
}
++j;
}
if (j == needle.size())
{
return true;
}
}
return false;
}
/** @brief 快速解析路径允许保留的最大有效位数(超过则交回 strtod 以保证正确舍入)。 */
const int kMaxFastDigits = 17;
/** @brief 快速解析路径允许使用的最大十进制指数(超出查表范围,交回 strtod)。 */
const int kMaxFastExponent = 22;
/** @brief 10 的 0-22 次幂精确表(该范围内每位都是 double 可精确表示的十进制数)。 */
const double kPow10Table[23] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
/**
* @brief 标准库解析路径:去掉首尾空白后交给 strtod。
*
* 作为快速路径的保底实现,保证十六进制浮点、inf/nan、超范围指数、
* 带尾部杂字符(如 "12abc")等输入的处理与改造前完全一致。
*
* @param [IN] begin 文本起始指针。
* @param [IN] end 文本结束指针(不包含)。
* @param [OUT] value 解析结果。
* @return 解析出数值返回 true。
*/
bool ParseDoubleSlow(const char* begin, const char* end, double& value)
{
const char* p = begin;
while (p < end && (*p == ' ' || *p == '\t'))
{
++p;
}
const char* q = end;
while (q > p && (q[-1] == ' ' || q[-1] == '\t' || q[-1] == '\r' || q[-1] == '\n'))
{
--q;
}
if (p >= q)
{
return false;
}
char buffer[96];
const size_t length = static_cast<size_t>(q - p);
const char* text = nullptr;
std::string dynamic;
if (length < sizeof(buffer))
{
std::memcpy(buffer, p, length);
buffer[length] = '\0';
text = buffer;
}
else
{
dynamic.assign(p, q);
text = dynamic.c_str();
}
char* stop = nullptr;
const double parsed = std::strtod(text, &stop);
if (stop == text)
{
return false;
}
value = parsed;
return true;
}
bool TryParseDouble(const char* begin, const char* end, double& value)
{
if (begin == nullptr || end == nullptr || begin >= end)
{
return false;
}
// ===== 快速路径:纯十进制数值 =====
// 大文件解析的绝对瓶颈就在这里:ucrt 的 strtod 要处理本地化、十六进制浮点、
// 无穷/NaN 等全部语法,实测每次调用约 335ns;百万行级文件需调用上千万次,
// 单解析就可能耗掉几十秒。下面这条路径只处理最常见的十进制写法(实测约 13ns,
// 快 25 倍),其余写法(含尾部杂字符)原样交回 strtod,
// 因此输入接受范围与原先完全一致。
const char* head = begin;
while (head < end && (*head == ' ' || *head == '\t'))
{
++head;
}
const char* tail = end;
while (tail > head && (tail[-1] == ' ' || tail[-1] == '\t' || tail[-1] == '\r' || tail[-1] == '\n'))
{
--tail;
}
if (head >= tail)
{
return false;
}
const char* cursor = head;
bool negative = false;
if (*cursor == '+' || *cursor == '-')
{
negative = (*cursor == '-');
++cursor;
}
unsigned long long mantissa = 0;
int digits = 0;
int exponent = 0;
// 整数部分:超过有效位数的高位数字只影响量级,累加进指数。
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
{
if (digits < kMaxFastDigits)
{
mantissa = mantissa * 10ull + static_cast<unsigned>(*cursor - '0');
++digits;
}
else
{
++exponent;
}
++cursor;
}
// 小数部分:每保留一位就记一次小数位数;超出有效位数的直接丢弃(已低于 double 精度)。
int fractionDigits = 0;
if (cursor < tail && *cursor == '.')
{
++cursor;
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
{
if (digits < kMaxFastDigits)
{
mantissa = mantissa * 10ull + static_cast<unsigned>(*cursor - '0');
++digits;
++fractionDigits;
}
++cursor;
}
}
if (digits == 0)
{
return ParseDoubleSlow(begin, end, value);
}
exponent -= fractionDigits;
if (cursor < tail && (*cursor == 'e' || *cursor == 'E'))
{
++cursor;
bool expNegative = false;
if (cursor < tail && (*cursor == '+' || *cursor == '-'))
{
expNegative = (*cursor == '-');
++cursor;
}
if (cursor >= tail || *cursor < '0' || *cursor > '9')
{
return ParseDoubleSlow(begin, end, value);
}
int expValue = 0;
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
{
if (expValue < 1000000)
{
expValue = expValue * 10 + (*cursor - '0');
}
++cursor;
}
exponent += expNegative ? -expValue : expValue;
}
// 存在未消费字符(如 "12abc"、"1,5")或指数超出查表范围时交回标准库:
// 既保持原先 strtod 的宽松接受语义,也保证极端量级的正确舍入。
if (cursor != tail || exponent > kMaxFastExponent || exponent < -kMaxFastExponent)
{
return ParseDoubleSlow(begin, end, value);
}
double result = static_cast<double>(mantissa);
if (exponent > 0)
{
result *= kPow10Table[exponent];
}
else if (exponent < 0)
{
result /= kPow10Table[-exponent];
}
// mantissa 超过 2^53(16 位以上有效数字)时这里会比 strtod 多一次舍入,
// 相对误差不超过 1 ULP(实测 ≤2.3e-16),在波形显示与标尺(约 6 位有效数字)上不可见。
value = negative ? -result : result;
return true;
}
std::wstring FormatNumber(double value, int decimals)
{
if (decimals < 0)
{
decimals = 0;
}
if (decimals > 12)
{
decimals = 12;
}
wchar_t buffer[96];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
return std::wstring(buffer);
}
std::wstring FormatDecimal(double value, int significantDigits)
{
if (std::isnan(value))
{
return L"NaN";
}
if (std::isinf(value))
{
return (value > 0.0) ? L"+Inf" : L"-Inf";
}
if (significantDigits < 1)
{
significantDigits = 1;
}
if (significantDigits > 15)
{
significantDigits = 15;
}
const double magnitude = std::fabs(value);
if (magnitude == 0.0)
{
return L"0";
}
// 小数位数 = 让结果保留约 `significantDigits` 位有效数字(整数部分已超出时取 0 位)。
// ⚠️ **绝不用 `%g` / `%e`**:`%g` 在量级小于 1e-4 或大于 1e6 时会切换成科学计数法,
// 对“时间间隔 / 频率”这类读数极不友好(用户 2026-11-03 明确要求小数形式)。
int decimals = 0;
{
const double exponent = std::floor(std::log10(magnitude));
decimals = significantDigits - 1 - static_cast<int>(exponent);
}
if (decimals < 0)
{
decimals = 0;
}
if (decimals > 12)
{
decimals = 12;
}
wchar_t buffer[160];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
std::wstring text(buffer);
// 去掉尾随的 0 与孤立的小数点(`1.50000` → `1.5`、`2.000000` → `2`)。
if (text.find(L'.') != std::wstring::npos)
{
std::size_t last = text.find_last_not_of(L'0');
if (last == std::wstring::npos)
{
return L"0";
}
if (text[last] == L'.')
{
--last;
}
text.erase(last + 1);
}
if (text == L"-0")
{
text = L"0";
}
return text;
}
std::wstring FormatDecimalFixed(double value, int decimals)
{
if (std::isnan(value))
{
return L"NaN";
}
if (std::isinf(value))
{
return (value > 0.0) ? L"+Inf" : L"-Inf";
}
if (decimals < 0)
{
decimals = 0;
}
if (decimals > 12)
{
decimals = 12;
}
// ⚠️ 固定小数位数 + **保留尾随 0**(与 `FormatDecimal` 的"去尾随 0"语义相反):
// 用户 2026-11-03 明确要求「次级单位保留三位小数」,例如 696.631us / 700.000us。
wchar_t buffer[160];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
std::wstring text(buffer);
if (text == L"-0" || (text.size() > 1 && text[0] == L'-' &&
text.find_first_not_of(L"0-.") == std::wstring::npos))
{
text.erase(0, 1); // -0.000 → 0.000
}
return text;
}
std::wstring FormatDurationCompound(double seconds) {
if (!std::isfinite(seconds))
{
return L"—";
}
const double value = std::fabs(seconds);
if (value <= 0.0)
{
return L"0s";
}
// ---- 顶级单位高于 s(h / min):整合为 h/min/s(整数秒)----
// 注意条件必须是 >= 60s:1~60s 属于"顶级单位为 s"的情况,需按下一条规则处理
// (非整数秒要递进到 ms,例如 5.06s → "5s 60ms")。
if (value >= 60.0)
{
const long long totalSeconds = static_cast<long long>(std::llround(value));
long long hours = 0;
long long minutes = 0;
long long secs = totalSeconds;
if (totalSeconds >= 3600)
{
hours = totalSeconds / 3600;
secs = totalSeconds % 3600;
}
if (secs >= 60)
{
minutes = secs / 60;
secs = secs % 60;
}
std::wstring text;
const auto append = [&text](long long amount, const wchar_t* unit)
{
if (amount <= 0)
{
return;
}
if (!text.empty())
{
text += L" ";
}
text += std::to_wstring(amount);
text += unit;
};
append(hours, L"h");
append(minutes, L"min");
append(secs, L"s");
if (!text.empty())
{
return text;
}
}
// ---- 顶级单位为 s:整数秒 + 可选 ms ----
// ⚠️⚠️ 与下面的亚秒分支同一原则:**先量化到整数皮秒,再分解**。
// 直接对 double 做 `floor` + `fraction * 1000` 会被浮点噪声坑到:
// `0.9999999999s` 的余数四舍五入后是 `1000.000ms`(应进位成 `1s`),
// 而 `5.0599999999999996s` 与 `5.06s` 会得到不同的尾数(用户 2026-11-03 报告)。
if (value >= 1.0)
{
const long long totalPs = static_cast<long long>(std::llround(value * 1.0e12));
long long wholeSeconds = totalPs / 1000000000000LL;
const long long restPs = totalPs % 1000000000000LL;
// 次级(ms)以「千分之一 ms」为整数单位,保证舍入一次到位。
long long milliMs = (restPs + 500000LL) / 1000000LL;
if (milliMs >= 1000LL * 1000LL)
{
++wholeSeconds;
milliMs = 0;
}
std::wstring text = FormatDecimal(static_cast<double>(wholeSeconds)) + L"s";
if (milliMs > 0)
{
// ⚠️ 次级单位(ms)**最多保留三位小数**(用户 2026-11-03 要求:`3ms 696.631us`
// 这类显示要能看到真实尾数);整数则不加小数点(`5.06s` → `5s 60ms`)。
text += L" ";
text += FormatSubUnit(static_cast<double>(milliMs) / 1000.0, kDurationSubUnitDecimals);
text += L"ms";
}
return text;
}
// ---- 顶级单位低于 s:ms / us / ns / ps ----
// ⚠️⚠️ **必须以整数(皮秒)为唯一事实来源做分解**,不能对 double 反复 floor/除法。
// 同一个物理间隔(例如 `0.001s`)在两次独立计算里可能分别是
// `0.0010000000000000002` 与 `0.0009999999999999998`;任何"接近 1000 就进位"
// 的浮点启发式都会在边界漏判,于是同一数值出现 `1ms` 与 `1000us` 两种显示,
// 还会留下 `100us 0ns` 这种纯噪声尾数(用户 2026-11-03 报告)。
// 量化到 ps 之后噪声被彻底消除:同一数值必然得到同一字符串。
const long long totalPs = static_cast<long long>(std::llround(value * 1.0e12));
if (totalPs <= 0)
{
// 小于 0.5ps:无法量化,保留高精度小数直接显示。
return FormatDecimal(value / 1.0e-12) + L" ps";
}
struct SubUnit
{
long long ps;
const wchar_t* name;
};
const SubUnit subUnits[] = {
{1000000000LL, L"ms"}, {1000000LL, L"us"}, {1000LL, L"ns"}, {1LL, L"ps"},
};
const int subCount = 4;
// 量化后达到 1s ⇒ 等价于上面的秒级分支(`999.9999999999s` → `1s`)。
if (totalPs >= 1000000000000LL)
{
long long wholeSeconds = totalPs / 1000000000000LL;
const long long restPs = totalPs % 1000000000000LL;
long long milliMs = (restPs + 500000LL) / 1000000LL;
if (milliMs >= 1000LL * 1000LL)
{
++wholeSeconds;
milliMs = 0;
}
std::wstring text = FormatDecimal(static_cast<double>(wholeSeconds)) + L"s";
if (milliMs > 0)
{
text += L" ";
text += FormatSubUnit(static_cast<double>(milliMs) / 1000.0, kDurationSubUnitDecimals);
text += L"ms";
}
return text;
}
// 选主单位:第一个"能被 totalPs 容纳"的单位。
int index = 0;
while (index + 1 < subCount && totalPs < subUnits[index].ps)
{
++index;
}
// 把 totalPs 分解成 [主单位整数] + [次级单位(千分之一为单位的整数)]。
// 抽成 lambda 是因为升级单位后需要用同一个 totalPs 重算,避免累积误差。
long long whole = 0;
long long milliNext = 0;
const auto decompose = [&]()
{
whole = totalPs / subUnits[index].ps;
milliNext = 0;
if (index + 1 >= subCount)
{
return;
}
const long long restPs = totalPs % subUnits[index].ps;
const long long nextPs = subUnits[index + 1].ps;
// 次级值保留 3 位小数 ⇒ 用「千分之一个次级单位」作为整数量纲。
milliNext = (restPs * 1000LL + nextPs / 2) / nextPs;
// 次级舍入后达到 1000 个次级单位 ⇒ 进位到主单位。
if (milliNext >= 1000LL * 1000LL)
{
++whole;
milliNext = 0;
}
};
decompose();
// 主单位达到 1000 且没有次级尾数 ⇒ 升级到更高的主单位(`1000us` → `1ms`)。
while (index > 0 && whole >= 1000LL && milliNext == 0)
{
--index;
decompose();
}
// 主单位已是 ms 且恰好进位到 1000ms ⇒ 直接显示为秒。
if (index == 0 && milliNext == 0 && whole >= 1000LL)
{
return FormatDecimal(static_cast<double>(whole) / 1000.0) + L"s";
}
std::wstring text = FormatDecimal(static_cast<double>(whole));
text += subUnits[index].name;
if (milliNext > 0)
{
// ⚠️ 次级单位**最多保留三位小数**(用户 2026-11-03 要求):
// `0.003696631s` → 顶级 `3ms` + 次级 `696.631us`(而不是 `3ms 700us`)。
text += L" ";
text += FormatSubUnit(static_cast<double>(milliNext) / 1000.0, kDurationSubUnitDecimals);
text += subUnits[index + 1].name;
}
return text;
}
std::wstring FormatDurationSeconds(double seconds)
{
if (!std::isfinite(seconds))
{
return L"0";
}
// ⚠️ 内联编辑的数值**不得出现科学计数法**(用户 2026-11-03 要求)。
return FormatDecimal(std::fabs(seconds), 12);
}
std::wstring FormatFrequency(double hertz)
{
if (!std::isfinite(hertz) || hertz <= 0.0)
{
return L"—";
}
// ⚠️⚠️ **单位选择必须按「舍入后的显示值」判定,否则同一物理量会出现两种显示**:
// `999.9999999 Hz` 走 Hz 分支显示 `1000.000 Hz`,而 `1000.0000001 Hz` 走 kHz 分支
// 显示 `1.000 kHz` —— 用户 2026-11-03 报告的 `1000.00 Hz` vs `1.000 kHz` 就是这个。
// 因此:先取单位,再用**保留 3 位小数后的值**判断是否达到 1000,达到就升级。
const wchar_t* units[] = {L"Hz", L"kHz", L"MHz", L"GHz"};
const double scales[] = {1.0, 1.0e3, 1.0e6, 1.0e9};
const int unitCount = 4;
int unitIndex = 0;
for (int i = unitCount - 1; i >= 0; --i)
{
if (hertz >= scales[i])
{
unitIndex = i;
break;
}
}
// 向上校正:舍入到 3 位小数后若达到 1000 个当前单位,就升到上一级。
const double epsilon = 0.5 * std::pow(10.0, -kFrequencyDecimals);
while (unitIndex + 1 < unitCount &&
(hertz / scales[unitIndex]) + epsilon >= 1000.0)
{
++unitIndex;
}
// 向下校正:若当前单位下小于 1(极低频边界),降一级以免出现 `0.001 kHz` 这类。
while (unitIndex > 0 && (hertz / scales[unitIndex]) < 1.0 - epsilon)
{
--unitIndex;
}
const wchar_t* unit = units[unitIndex];
const double scaled = hertz / scales[unitIndex];
// ⚠️ 频率同样不得出现科学计数法(用户 2026-11-03 要求):
// `FormatValue` 的 `%.6g` 在 1e-5 Hz 这类量级会输出 “1e-06”,改为十进制形式。
// ⚠️ 并且**基于真实值保留三位小数**(用户 2026-11-03 要求:`270.517 Hz` 而不是
// `270.27 Hz`)—— 旧实现把间隔先舍入到 3 位有效数字再换算,把真实尾数抹掉了。
// ⚠️ 但若保留 3 位后整串变成 `0.000`(超低频,例如 1/3662s ≈ 0.27mHz),
// 则退回按「有效数字」显示,避免把有意义的数值显示成 0(用户 2026-11-03)。
const std::wstring fixedText = FormatDecimalFixed(scaled, kFrequencyDecimals);
if (std::fabs(scaled) > 0.0 && std::fabs(scaled) < 0.5 * std::pow(10.0, -kFrequencyDecimals))
{
return FormatDecimal(scaled) + L" " + unit;
}
return fixedText + L" " + unit;
}
std::wstring FormatByteSize(unsigned long long bytes)
{ // 单位自适应:GB / MB / KB(阈值按 1024 进制)。
constexpr double kKilo = 1024.0;
constexpr double kMega = kKilo * 1024.0;
constexpr double kGiga = kMega * 1024.0;
const double value = static_cast<double>(bytes);
const wchar_t* unit = L"KB";
double scaled = value / kKilo;
if (value >= kGiga)
{
unit = L"GB";
scaled = value / kGiga;
}
else if (value >= kMega)
{
unit = L"MB";
scaled = value / kMega;
}
wchar_t buffer[64];
// ⚠️ 宽字符格式化必须用 `%ls`:MinGW 的 swprintf 按 C99 语义处理 `%s`
//(在宽格式串里 `%s` 表示**窄**字符串),写成 `%s` 会把宽字符串按单字节读取,
// 于是 "KB" 只打印出 "K"。
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.2f %ls", scaled, unit);
return std::wstring(buffer);
}
std::wstring FormatValue(double value)
{
if (std::isnan(value))
{
return L"NaN";
}
if (std::isinf(value))
{
return (value > 0.0) ? L"+Inf" : L"-Inf";
}
wchar_t buffer[96];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.6g", value);
return std::wstring(buffer);
}
std::wstring FormatTick(double value, double step)
{
if (value == 0.0)
{
return L"0";
}
if (!std::isfinite(value))
{
return FormatValue(value);
}
int decimals = 0;
if (step > 0.0 && std::isfinite(step))
{
const double magnitude = std::ceil(-std::log10(step) - 1.0e-9);
if (magnitude > 0.0)
{
decimals = static_cast<int>(magnitude);
}
}
if (decimals > 9)
{
decimals = 9;
}
wchar_t buffer[96];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
std::wstring text(buffer);
if (text.size() > 15)
{
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.4e", value);
text.assign(buffer);
}
return text;
}
std::wstring FormatCount(unsigned long long value)
{
wchar_t buffer[32];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%llu", value);
std::wstring digits(buffer);
std::wstring result;
result.reserve(digits.size() + digits.size() / 3);
const size_t length = digits.size();
for (size_t i = 0; i < length; ++i)
{
if (i > 0 && ((length - i) % 3u) == 0u)
{
result.push_back(L',');
}
result.push_back(digits[i]);
}
return result;
}
double NiceStep(double range, int targetTicks)
{
if (!(range > 0.0) || !std::isfinite(range) || targetTicks <= 0)
{
return 1.0;
}
const double raw = range / static_cast<double>(targetTicks);
const double magnitude = std::pow(10.0, std::floor(std::log10(raw)));
const double normalized = raw / magnitude;
double nice = 10.0;
if (normalized <= 1.0)
{
nice = 1.0;
}
else if (normalized <= 2.0)
{
nice = 2.0;
}
else if (normalized <= 5.0)
{
nice = 5.0;
}
return nice * magnitude;
}
void BuildTicks(double minValue, double maxValue, double step, std::vector<double>& ticks)
{
ticks.clear();
if (!std::isfinite(minValue) || !std::isfinite(maxValue) || !(step > 0.0) || !std::isfinite(step))
{
return;
}
if (maxValue < minValue)
{
const double temp = minValue;
minValue = maxValue;
maxValue = temp;
}
// ⚠️ `minValue / step` 在极端量级下可能溢出成 inf(例如 1e300 / 1e-300),
// 而 double → long long 的越界转换是**未定义行为**。先确认商落在 long long 范围内,
// 否则直接放弃画刻度(总比画出一批垃圾刻度安全)。
constexpr double kTickIndexLimit = 9.0e18;
const double firstIndex = std::ceil(minValue / step - 1.0e-9);
const double lastIndex = std::floor(maxValue / step + 1.0e-9);
if (!(std::fabs(firstIndex) < kTickIndexLimit) || !(std::fabs(lastIndex) < kTickIndexLimit))
{
return;
}
const long long first = static_cast<long long>(firstIndex);
const long long last = static_cast<long long>(lastIndex);
if (last < first)
{
return;
}
const long long count = last - first + 1;
if (count > 512)
{
return;
}
ticks.reserve(static_cast<size_t>(count));
for (long long index = first; index <= last; ++index)
{
ticks.push_back(static_cast<double>(index) * step);
}
}
bool CopyTextToClipboard(HWND owner, const std::wstring& text)
{
if (text.empty())
{
return false;
}
if (OpenClipboard(owner) == FALSE)
{
return false;
}
bool success = false;
if (EmptyClipboard() != FALSE)
{
const SIZE_T bytes = (text.size() + 1) * sizeof(wchar_t);
HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, bytes);
if (handle != nullptr)
{
void* target = GlobalLock(handle);
if (target != nullptr)
{
std::memcpy(target, text.c_str(), bytes);
GlobalUnlock(handle);
if (SetClipboardData(CF_UNICODETEXT, handle) != nullptr)
{
success = true;
}
else
{
GlobalFree(handle);
}
}
else
{
GlobalFree(handle);
}
}
}
CloseClipboard();
return success;
}
bool PasteTextFromClipboard(HWND owner, std::wstring& text)
{
text.clear();
if (OpenClipboard(owner) == FALSE)
{
return false;
}
bool success = false;
if (IsClipboardFormatAvailable(CF_UNICODETEXT) != FALSE)
{
HANDLE handle = GetClipboardData(CF_UNICODETEXT);
if (handle != nullptr)
{
const void* source = GlobalLock(handle);
if (source != nullptr)
{
const wchar_t* wide = static_cast<const wchar_t*>(source);
text.assign(wide);
GlobalUnlock(handle);
success = !text.empty();
}
}
}
CloseClipboard();
// 单行输入框:把换行与制表符折叠为空格,避免内容被截断显示。
if (success)
{
for (wchar_t& c : text)
{
if (c == L'\r' || c == L'\n' || c == L'\t')
{
c = L' ';
}
}
}
return success;
}
} // namespace jdi