Files
Just-DataInspector/Utilities/Src/Utf8Util.cpp
T
DeepSeek V4.1 Flash 53b981a2a9 全项目大检查:修复数据层/UI 真 BUG、清理死代码与过期注释、并评估二进制体积
按「性能 / BUG / 错误 / 过期注释 / 命名与实际含义不符」四项对全仓做了一次审查,
四个只读子审查交叉核对后逐条回到源码确认再改。

一、数据层真 BUG
- MatH5:U8/U16/U32/U64 用逐字节 Cached() 拼装,而 Cached 只服务「完全落在同一个
  64KB 块内」的请求 => 跨块时静默读成 0(U32 整段为 0、U64 高 32 位为 0),分块 B-tree
  的 chunk 偏移/子节点地址被破坏且无任何报错。新增 ScalarLE():命中缓存用缓存,
  否则退回一次 ReadAt;U64 顺带从 8 次缓存查找降到 1 次。
- Mf4Loader::IsBlock 未校验链接表是否落在块内 => DataOffset 越界,随后
  block_len - (data - offset) 在 uint64 上下溢,得到一个约 2^64 的"可用字节数",
  下游 vector(usable+1) 直接 bad_alloc。同时把 offset + bytes > size 改成不产生回绕的写法。
- DecodeRawValue 的整数路径:bit_offset%8 != 0 且位宽 64 时需 9 个字节,先拼成 64 位
  再整体右移会把最高字节静默丢弃 => 改为逐字节带位偏移累加。
- cg_cycle_count 加「数据段总字节 / 记录字节」上限(压缩段按 deflate 最大膨胀比估算)。
- CsvLoader 表头分支同样跳过 #/% 注释行(否则整份文件按 1 列解析并被丢弃)。
- MatLoader:double -> int 的越界转换加了范围检查;普通对象递归加深度上限与成环检测。
- Utf8Util::BuildTicks:double -> long long 越界转换前先判商是否落在 ±9e18 内。

二、界面 BUG
- 工具栏按钮永远不显示悬停高亮:Paint 读的 Button::hover 全仓从未赋值,改为按 m_hoverIndex 判断。
- 菜单打开时"取消底层拖拽"对每个消息都执行,把菜单自己的 Target::EDIT_FIELD 也清掉
  => 菜单内联输入框无法拖拽框选。排除 EDIT_FIELD。
- GdiplusShutdown 之前未释放 PlotView 的 GDI+ 位图(Application 是进程单例,成员析构在
  WinMain 之后)=> 新增 PlotView::ReleaseGraphicsResources() 并在 Shutdown 里调用。
- WM_KILLFOCUS 现在会提交标签页/绘图区编辑并复位捕获与拖拽状态。
- WM_LBUTTONUP 不再抢走中键平移的鼠标捕获。
- TextEdit:被过滤器拒绝的字符返回 false;无选区时 Ctrl+X 只复制、不再清空整段文本。
- SignalPanel::InvalidateRows 作废编辑/待确认/改名计时等行下标状态。
- EndFilterEdit 改走 ApplyFilterText(否则输入纯空格后回车,列表仍停在筛选结果)。
- 新增 SignalBadgeVisible 作为唯一判据(原来命中只看"可见",绘制还要求"已分配网格")。
- 折起动画结束后夹取滚动量;双击分组名改名时清掉该组正在播放的折起动画。
- ScrollToSignal 复用 SignalScrollTarget(含悬浮组名避让)。
- RowNameRect 与绘制使用同一最小宽度(原来退化时返回空矩形)。
- 分隔条拖拽上限改用与 Relayout 同一公式;UpdateDragCursor 补 SPLITTER/EDIT_FIELD。
- SignalPanel 里几个守卫改为判断真正被解引用的指针;分组行 ✕ 也返回手型光标。

三、PlotView
- FindStyleOnlyLanes 改用完整的 SceneStyleEntry::operator==(原来只比 4 个字段,
  另一个网格同时改"调整"时带着旧像素被当成最新)。
- 宽基底图层不再烘焙拖拽光晕(LaneLayerHash 不含它),改由 PaintHoverHighlight 每帧叠加。
- PaintLaneLegend 提前返回前清掉本网格的命中矩形(否则能点中看不见的图例条目)。
- 面板主体改为完全不透明(消除平移后 4% 透出的旧波形,零额外代价)。
- PAN / Shift+滚轮改走 SetTimeView,使"自动点线"判据随平移重算。
- 内联输入框命中字体与绘制统一为 monoSmall。
- 派生曲线取值与窗口按 SourceIndexFor 映射(水平镜像时不再读到镜像另一端)。
- EdgePanelWidth 补 style/axis 空指针守卫;二分结果夹到 count。

四、清理
- 删除 Toolbar::ActionLabel、Theme::statusBg、PopupMenu 的 editResult 死机制、
  Toolbar::Layout 的重复 clear()、HitRow 的恒假分支、OnDoubleClick 的重复判定、
  PaintUtil 里孤立的注释块。
- 修正一批过期/错误注释(包络带"不做加宽"、LaneLayerHash 的图例说法、
  均值滤波曲线的窗口说法、样式区行数与「调整」按钮顺序、FIT_ALL 退出缩放模式、
  RemoveSignal 的 ✕ 位置、.mat/.dat 格式文档、MatH5 的"映射视图"、DataModel 的
  "点线/被移除文件"、SignalPanel 的"行高连续缩小"、TextEdit 的 PaintUtil 引用等)。

五、二进制体积评估(按要求只评估,未做压缩修改)
- release 809,472 字节;.text 652KB、.xdata+.pdata 约 75KB、.rdata 42KB、
  .rsrc 20KB、.idata 11.5KB。
- 去掉 -ld3d11 -ldwmapi -ld3dcompiler(仓内无任何调用)实测 0 字节收益
  (静态库只拉取被引用的对象)=> 删链接库不是压缩手段,只是配置整洁。
- 仓内已无 <iostream>/<sstream>/<filesystem>/<regex>/<locale> 等体积炸弹。
- 结论:已无明显的浪费可捡;要显著变小只能改变取舍(动态 CRT / UPX / 砍功能),
  需要用户决策,本轮未动。

验证
- editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、convcheck 18/18。
- check 136/137:唯一失败项经 git stash 回到改动前复现同一数值,属该探针自身的既有
  不稳定项(取决于前置用例留下的缓存/时序),与本轮改动无关。
- 真实数据:4 个 MF4 => 283/8/18/24 个信号;.mat 样例 8 信号、
  带无障碍对照-防夹力曲线数据 8 信号、BIG_MAT.mat 5 信号(私有内存 +741MB)。
- make release / make debug 零警告。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 171-180 条硬约束
与文末本轮小节(含体积评估数据)。
2026-10-20 16:10:29 +08:00

733 lines
17 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"; /**< 单位名称。 */
};
}
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 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 ----
if (value >= 1.0)
{
const double whole = std::floor(value);
const double fraction = value - whole;
std::wstring text = FormatValue(whole) + L"s";
const double remainderMs = fraction * 1000.0;
if (remainderMs >= 1.0e-6)
{
text += L" ";
text += FormatValue(remainderMs);
text += L"ms";
}
return text;
}
// ---- 顶级单位低于 s:ms / us / ns / ps ----
const DurationUnit subUnits[] = {
{1.0e-3, L"ms"}, {1.0e-6, L"us"}, {1.0e-9, L"ns"}, {1.0e-12, L"ps"},
};
const std::size_t subCount = sizeof(subUnits) / sizeof(subUnits[0]);
std::size_t index = subCount - 1;
for (std::size_t i = 0; i < subCount; ++i)
{
if (value >= subUnits[i].scale)
{
index = i;
break;
}
}
// 顶级单位为 ps:不再递进次级单位(保留小数)。
if (index + 1 >= subCount)
{
return FormatValue(value / subUnits[index].scale) + L" " + subUnits[index].name;
}
const double scaled = value / subUnits[index].scale;
double whole = std::floor(scaled);
double fraction = scaled - whole;
// 浮点误差可能导致 fraction 极接近 1(例如 4.9999999),归一化一下。
if (fraction >= 1.0 - 1.0e-9)
{
whole += 1.0;
fraction = 0.0;
}
std::wstring text = FormatValue(whole);
text += subUnits[index].name;
// 仍是该单位的整数 → 不显示下级单位。
const double remainder = fraction * subUnits[index].scale;
const double inNext = remainder / subUnits[index + 1].scale;
if (inNext >= 1.0e-6)
{
text += L" ";
text += FormatValue(inNext);
text += subUnits[index + 1].name;
}
return text;
}
std::wstring FormatDurationSeconds(double seconds)
{
if (!std::isfinite(seconds))
{
return L"0";
}
return FormatValue(std::fabs(seconds));
}
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