Merge pull request #36 from le-cheng/master

feat(page): 添加日志搜索功能并优化输出样式
This commit is contained in:
x-tools 2025-04-10 21:57:54 +08:00 committed by GitHub
commit d7fa2244c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 369 additions and 8 deletions

View File

@ -403,6 +403,13 @@ void Page::initUiOutput()
ui->toolButtonInputPreset->setMenu(ui->tabPreset->menu());
ui->tabWidget->setCurrentIndex(0);
ui->tabTransfers->setCurrentIndex(0);
// 连接搜索相关的信号和槽
connect(ui->pushButtonSearch, &QPushButton::clicked, this, &Page::onSearchButtonClicked);
connect(ui->lineEditSearch, &QLineEdit::textChanged, this, &Page::onSearchTextChanged);
connect(ui->checkBoxRegex, &QCheckBox::checkStateChanged, this, [this](int) { performSearch(); });
connect(ui->checkBoxMatchCase, &QCheckBox::checkStateChanged, this, [this](int) { performSearch(); });
connect(ui->checkBoxWholeWord, &QCheckBox::checkStateChanged, this, [this](int) { performSearch(); });
}
void Page::initUiInput()
@ -744,20 +751,29 @@ void Page::outputText(const QByteArray &bytes, const QString &flag, bool isRx)
} else {
header = QString("%1 %2").arg(rxTx, dateTimeString);
}
#if 0
header = header.trimmed();
header = QString("<font color=silver>[%1]</font>").arg(header);
QString outputText = QString("%1 %2").arg(header, text);
outputText = outputText.replace("\r", "\\r");
outputText = outputText.replace("\n", "\\n");
#else
header = QString("<font color=silver style='font-family: \"Segoe UI\", Arial; font-size: 12px;'>[%1]</font>").arg(header.trimmed());
QString outputText = QString("<pre style='margin:0; font-family: \"JetBrains Mono\", Consolas, Monaco, monospace; font-size: 13px; line-height: 1.1; padding: 2px 0;'>%1 %2</pre>").arg(header, text);
#endif
// 在添加到输出前进行搜索匹配
if (!ui->lineEditSearch->text().isEmpty()) {
performSearch(outputText);
}
if (m_outputSettings->isEnableFilter()) {
QString filter = m_outputSettings->filterText();
if (outputText.contains(filter)) {
ui->textBrowserOutput->append(outputText);
if (!outputText.contains(filter)) {
return;
}
} else {
ui->textBrowserOutput->append(outputText);
}
ui->textBrowserOutput->append(outputText);
}
void Page::saveControllerParameters()
@ -784,6 +800,223 @@ void Page::updateChartUi()
ui->widgetChartsController->setVisible(ui->toolButtonCharts->isChecked());
}
void Page::onSearchButtonClicked()
{
performSearch();
}
void Page::onSearchTextChanged()
{
// 当搜索文本变化时,如果文本不为空,则执行搜索
if (!ui->lineEditSearch->text().isEmpty()) {
performSearch();
} else {
// 如果搜索文本为空,则清空搜索结果
ui->textBrowserSearchResult->clear();
}
}
void Page::performSearch()
{
QString searchText = ui->lineEditSearch->text();
if (searchText.isEmpty()) {
return;
}
// 获取搜索选项
bool useRegex = ui->checkBoxRegex->isChecked();
bool matchCase = ui->checkBoxMatchCase->isChecked();
bool wholeWord = ui->checkBoxWholeWord->isChecked();
// 构建正则表达式
QString pattern = searchText;
if (!useRegex) {
// 如果不使用正则表达式,则转义特殊字符
pattern = QRegularExpression::escape(pattern);
}
// 如果需要全词匹配,则添加单词边界
if (wholeWord) {
pattern = QString("\\b%1\\b").arg(pattern);
}
// 创建正则表达式对象
QRegularExpression::PatternOptions options = QRegularExpression::NoPatternOption;
if (!matchCase) {
options |= QRegularExpression::CaseInsensitiveOption;
}
QRegularExpression regex(pattern, options);
// 获取textBrowserOutput的文本内容
QString content = ui->textBrowserOutput->toPlainText();
// 执行搜索并高亮结果
highlightSearchResults(content, regex);
}
void Page::performSearch(QString &line)
{
QString searchText = ui->lineEditSearch->text();
if (searchText.isEmpty()) {
return;
}
// 获取搜索选项
bool useRegex = ui->checkBoxRegex->isChecked();
bool matchCase = ui->checkBoxMatchCase->isChecked();
bool wholeWord = ui->checkBoxWholeWord->isChecked();
// 构建正则表达式
QString pattern = searchText;
if (!useRegex) {
// 如果不使用正则表达式,则转义特殊字符
pattern = QRegularExpression::escape(pattern);
}
// 如果需要全词匹配,则添加单词边界
if (wholeWord) {
pattern = QString("\\b%1\\b").arg(pattern);
}
// 创建正则表达式对象
QRegularExpression::PatternOptions options = QRegularExpression::NoPatternOption;
if (!matchCase) {
options |= QRegularExpression::CaseInsensitiveOption;
}
QRegularExpression regex(pattern, options);
// 只在当前行中搜索匹配项
highlightSearchResultsForLine(line, regex);
}
void Page::highlightSearchResults(const QString &text, const QRegularExpression &regex)
{
ui->textBrowserSearchResult->clear();
// 查找所有匹配项
QRegularExpressionMatchIterator matchIterator = regex.globalMatch(text);
int matchCount = 0;
// 存储所有匹配结果
QList<QPair<int, QRegularExpressionMatch>> matches;
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
matches.append(qMakePair(match.capturedStart(), match));
matchCount++;
}
// 如果没有匹配项,显示提示信息
if (matchCount == 0) {
ui->textBrowserSearchResult->setHtml(QString("<p style='color:gray'>未找到匹配项</p>"));
return;
}
// 显示匹配数量
ui->textBrowserSearchResult->setHtml(QString("<p style='color:gray'>找到 %1 个匹配项</p>").arg(matchCount));
// 获取文本的行
QStringList lines = text.split('\n');
// 对于每个匹配项,显示上下文
for (const auto &matchPair : matches) {
int matchPos = matchPair.first;
QRegularExpressionMatch match = matchPair.second;
// 计算匹配项所在的行号
int lineNumber = 0;
int posInLine = matchPos;
for (int i = 0; i < lines.size(); i++) {
if (posInLine <= lines[i].length()) {
lineNumber = i;
break;
}
posInLine -= lines[i].length() + 1; // +1 是为了换行符
}
// 获取匹配行及其上下文(前后各一行)
QString contextBefore = lineNumber > 0 ? lines[lineNumber - 1] : QString();
QString matchLine = lines[lineNumber];
QString contextAfter = lineNumber < lines.size() - 1 ? lines[lineNumber + 1] : QString();
// 在匹配行中高亮匹配文本
QString matchedText = match.captured();
int matchStartInLine = posInLine;
int matchEndInLine = matchStartInLine + matchedText.length();
QString highlightedLine = matchLine.left(matchStartInLine) +
QString("<span style='background-color:yellow;'>%1</span>").arg(matchedText) +
matchLine.mid(matchEndInLine);
// 构建HTML显示上下文
QString html = "<div style='margin-bottom:10px;'>";
html += QString("<p style='color:gray;margin:0;'>行 %1:</p>").arg(lineNumber + 1);
if (!contextBefore.isEmpty()) {
html += QString("<pre style='margin:0;color:gray;'>%1</pre>").arg(contextBefore.toHtmlEscaped());
}
html += QString("<pre style='margin:0;'>%1</pre>").arg(highlightedLine);
if (!contextAfter.isEmpty()) {
html += QString("<pre style='margin:0;color:gray;'>%1</pre>").arg(contextAfter.toHtmlEscaped());
}
html += "</div>";
// 添加到搜索结果
ui->textBrowserSearchResult->append(html);
}
}
void Page::highlightSearchResultsForLine(const QString &line, const QRegularExpression &regex)
{
// 查找所有匹配项
QRegularExpressionMatchIterator matchIterator = regex.globalMatch(line);
// 如果没有匹配项,直接返回
if (!matchIterator.hasNext()) {
return;
}
// 如果这是第一个匹配项,初始化搜索结果区域
if (ui->textBrowserSearchResult->document()->isEmpty()) {
ui->textBrowserSearchResult->setHtml(QString("<p style='color:gray'>实时搜索结果:</p>"));
}
// 使用原始文本进行匹配和高亮与highlightSearchResults保持一致
QString originalText = line;
// 对于每个匹配项,高亮并添加到结果中
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
QString matchedText = match.captured();
int matchStart = match.capturedStart();
int matchEnd = matchStart + matchedText.length();
// 在匹配行中高亮匹配文本使用与highlightSearchResults相同的逻辑
QString highlightedLine = originalText.left(matchStart) +
QString("<span style='background-color:yellow;'>%1</span>").arg(matchedText) +
originalText.mid(matchEnd);
// 构建HTML显示
QString html = "<div style='margin-bottom:5px;'>";
html += QString("<pre style='margin:0;'>%1</pre>").arg(highlightedLine);
html += "</div>";
// 添加到搜索结果
ui->textBrowserSearchResult->append(html);
// 更新匹配计数(可选)
static int matchCount = 0;
matchCount++;
// 更新标题显示匹配数量
QTextCursor cursor = ui->textBrowserSearchResult->textCursor();
cursor.setPosition(0);
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
cursor.insertHtml(QString("<p style='color:gray'>实时搜索结果:找到 %1 个匹配项</p>").arg(matchCount));
}
}
QByteArray Page::payload() const
{
InputSettings::Parameters parameters = m_inputSettings->parameters();

View File

@ -10,6 +10,7 @@
#include <QButtonGroup>
#include <QPushButton>
#include <QRegularExpression>
#include <QSettings>
#include <QTabWidget>
#include <QTimer>
@ -83,6 +84,12 @@ private:
void onBytesRead(const QByteArray &bytes, const QString &from);
void onBytesWritten(const QByteArray &bytes, const QString &to);
void onWrapModeChanged();
void onSearchButtonClicked();
void onSearchTextChanged();
void performSearch();
void highlightSearchResults(const QString &text, const QRegularExpression &regex);
void highlightSearchResultsForLine(const QString &line, const QRegularExpression &regex);
void performSearch(QString &line);
void openDevice();
void closeDevice();

View File

@ -397,19 +397,140 @@
</widget>
</item>
<item>
<widget class="QTextBrowser" name="textBrowserOutput">
<property name="minimumSize">
<widget class="QSplitter" name="splitterOutput">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QTextBrowser" name="textBrowserOutput"/>
<property name="minimumSize">
<size>
<width>0</width>
<height>150</height>
</size>
</property>
</property>
<widget class="QWidget" name="widgetSearch" native="true">
<layout class="QVBoxLayout" name="verticalLayoutSearch">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutSearch">
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QLineEdit" name="lineEditSearch">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="placeholderText">
<string>搜索日志内容</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonSearch">
<property name="text">
<string>搜索</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutSearchOptions">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QCheckBox" name="checkBoxRegex">
<property name="text">
<string>正则表达式</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxMatchCase">
<property name="text">
<string>区分大小写</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxWholeWord">
<property name="text">
<string>全字匹配</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacerSearchOptions">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTextBrowser" name="textBrowserSearchResult"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_10">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<bold>true</bold>