前提:加载完成后全部采样都已驻留内存,程序此后与源文件再无联系。 据此修正了一批会让人误以为"仍在读写文件"的命名: - SampleFileAt → SampleSignalAt(取的是内存信号在给定时刻的采样值)。 - DataSet::AddPreparedFile → AddDataFile。 - PlotView::InvalidatePolylineCache → InvalidateDerivedCaches (它同时清空折线、边沿统计、中心平均值曲线、静态场景缓存与悬停引用)。 - PlotView::CursorColor(int which) → CursorColor()(两条光标同色,参数从未使用)。 - UiEllipsize(g,…) → UiEllipsize(…):纯测量不绘制,删掉被忽略的绘图上下文参数; 同时删除与 UiMeasureTextWidth 完全重复的 UiTextWidth,调用点统一到 UiMeasureTextWidth。 清理残留参数((void)xxx;): - TabConfig 的可见性/网格族(SetVisible / ToggleVisible / SetFileVisible / SetAllVisible / SoloSignal / SetMode / SyncLanes / FileHasVisible / FileAllVisible)去掉从未使用的 const DataSet& 参数(仅 SyncToDataSet 真正需要数据层,保留)。 - PlotView::OnLButtonUp(int y)、OnMouseWheel(int delta, bool shift);PainterSignalHalo 去掉未用的 ref。 - BuildDataFile 中未使用的局部 rowCount。 删除无人调用的死函数: DataSet::Clear / AddFromTable;SignalColorCount;WideToUtf8 / StartsWith; PlotView::ViewMinTime / ViewMaxTime / EdgePanelRect / EdgePanelCloseRect / EdgeCenterPanelRect / ScaleLaneRange / BeginLaneValueEdit / LaneContentHeight; SignalPanel::NeedsScrollBar / IsFilterEditing / FilterText / ContentHeight; PopupMenu::EditingItemId;TabBar::IsDragging;TextEdit::PlaceCaret。 删除只写不读的死字段:CsvTable::sourceRowCount(与 rowCount 等义)与 skippedRowCount。 以 VERY_BIG_DATA.CSV 回归验证:加载与勾选、筛选框(含"无匹配的筛选信号")、 样式区线宽文案省略、选区放大(横向与纵向)、双光标读数与 Δt、右键菜单(Y 轴数值与 实验性功能子菜单)、合并/拆分网格、标签与信号重命名省略号,行为均与改动前一致; make clean && make all 无任何警告。
563 lines
12 KiB
C++
563 lines
12 KiB
C++
#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';
|
||
}
|
||
}
|
||
|
||
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 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;
|
||
}
|
||
|
||
const long long first = static_cast<long long>(std::ceil(minValue / step - 1.0e-9));
|
||
const long long last = static_cast<long long>(std::floor(maxValue / step + 1.0e-9));
|
||
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
|