QT自带有一个Example codeeditor可以显示行号。修改了一下,用于显示16进制数据,并且增加了缩略图显示。效果如下:
类名:CHexEditor
接口:
- void setData(quint8 *data, quint32 len) // 设置待显示数据指针以及长度
- void setLineNumBgColor(QColor color) // 行号背景色
- void setLineNumTextColor(QColor color) // 行号文本色
- void setLineNumHighlightEnabled(bool flag) // 行号是否高亮
- void setLineNumHighlightBold(bool flag) // 行号高亮时背景色
- void setLineNumHighlightBold(bool flag) // 行号高亮时文本是否加粗
- void setThumbnailAreaTextColor(QColor color) // 缩略图文本色
- void setThumbnail(bool enable) // 是否启用缩略图
主文件:mainwindow.cpp
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include "chexeditor.h"
#include <QRandomGenerator>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setContentsMargins(2, 2, 2, 2);
CHexEditor *m_hexEdit = new CHexEditor(this);
m_hexEdit->setWordWrapMode(QTextOption::NoWrap);
m_hexEdit->setLineNumHighlightEnabled(true);
m_hexEdit->setFont(QFont("JetBrains Mono", 12));
m_hexEdit->setThumbnail(true);
m_hexEdit->setReadOnly(true);
// 设置字体色
setStyleSheet("CHexEditor {color: #e0e0e0; background-color: #303030; }");
setCentralWidget(m_hexEdit);
quint8 testArr[70] = {0};
for (int i=0; i<sizeof(testArr); i++)
testArr[i] = QRandomGenerator::global()->bounded(0, 255);
m_hexEdit->setData(&testArr[0], sizeof(testArr));
}
CHexEditor.cpp
#include "chexeditor.h"
#include <QPainter>
#include <QTextBlock>
#include <QByteArray>
#include <QScrollBar>
CHexEditor::CHexEditor(QWidget *parent) : QPlainTextEdit(parent)
{
// 创建行号区对象
m_lineNumberArea = new LineNumberArea(this);
m_lineNumberArea->setCursor(Qt::ArrowCursor);
// 创建缩略图对象
m_thumbnailArea = new ThumbnailArea(this);
m_thumbnailArea->setCursor(Qt::OpenHandCursor);
viewport()->setCursor(Qt::PointingHandCursor); // 更改文本区鼠标指针
setCursorWidth(2); // 光标宽度
updateLineNumberAreaWidth(0);
connect(this, &CHexEditor::blockCountChanged, this, &CHexEditor::updateLineNumberAreaWidth);
connect(this, &CHexEditor::updateRequest, this, &CHexEditor::updateLineNumberArea);
connect(this, &CHexEditor::cursorPositionChanged, this, &CHexEditor::highlightCurrentLine);
}
CHexEditor::~CHexEditor()
{
if (m_data)
{
free(m_data);
m_dataSize = 0;
}
}
// 重新计算行号区域宽度,当行数目变化时,blockCountChanged事件触发本函数计算。
void CHexEditor::updateLineNumberAreaWidth(int newBlockCount)
{
// PlainEdit 默认视口Margin为0,0,0,0,表示滚动区边缘到左、顶、右、底的距离
// 左侧设置为行号区域宽度,则左侧行号区等同于从视口滚动区剥离,
// 主体文字将不会显示在左侧被剥离的区域。
// 每行显示输出 16 * 3 Hex数字
QString strMaxLine(48, QChar('F'));
QRect lineRect = fontMetrics().boundingRect(strMaxLine);
int vScrollBarWidth = verticalScrollBar()->isVisible() ? verticalScrollBar()->rect().width() : 0;
int rightMargin = m_hasThumbnail ? (contentsRect().width() - lineRect.width() - lineNumberAreaWidth() - vScrollBarWidth - 2) : 0;
setViewportMargins(lineNumberAreaWidth(), 0, rightMargin, 0);
// 右侧视口margin改变后会导致textEdit横向滚动条无法出现,设置一下滚动条最大宽度
if (m_hasThumbnail)
viewport()->setMaximumWidth(lineRect.width() + rightMargin);
}
int CHexEditor::lineNumberAreaWidth()
{
return m_lineAreaPaddingLeft + m_lineAreaPaddingRight +
fontMetrics().horizontalAdvance(QLatin1Char('9')) * 8;
}
// updateReqest信号触发本函数
// 文本卷动时,rect包含整个视口区域,
// 当文本垂直卷动时,dy参数携带视口卷动的像素数。
void CHexEditor::updateLineNumberArea(const QRect &rect, int dy)
{
if (dy) // scrolled vertically
{
m_lineNumberArea->scroll(0, dy); // 行号区同步滚动dy像素
if (m_hasThumbnail)
m_thumbnailArea->scroll(0, dy);
}
else
{
m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
if (m_hasThumbnail)
m_thumbnailArea->update();//0, rect.y(), 300, rect.height());
}
if (rect.contains(viewport()->rect()))
updateLineNumberAreaWidth(0);
}
void CHexEditor::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly()) {
QTextEdit::ExtraSelection selection;
selection.format.setBackground(m_currLineHighlightColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
}
setExtraSelections(extraSelections);
}
void CHexEditor::paintEvent(QPaintEvent *event)
{
QPlainTextEdit::paintEvent(event);
if (!m_hasThumbnail)
return;
QPainter dc(viewport());
// 开启反锯齿
dc.setRenderHint(QPainter::Antialiasing, true);
// 右侧缩略图虚线位置y坐标
int splitLineY = viewport()->rect().right();
QPen verticalLinePen = QPen(QColor(Qt::gray));
verticalLinePen.setStyle(Qt::DotLine);
dc.setPen(verticalLinePen);
// 绘制左侧行号与正文分隔线
//dc.drawLine(verticalLineX, contentsRect().top(), verticalLineX, contentsRect().bottom());
// 绘制右侧提示线
dc.drawLine(splitLineY, contentsRect().top(), splitLineY, contentsRect().bottom());
}
// PlainEdit大小变化时,同步更新行号区域大小
void CHexEditor::resizeEvent(QResizeEvent *e)
{
QPlainTextEdit::resizeEvent(e);
QString strMaxLine(48, QChar('F'));
QRect lineRect = fontMetrics().boundingRect(strMaxLine);
QRect cr = contentsRect();
// 设定行号区大小
m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
// viewPort 同时设置左右两侧区域时,似乎有bug(仅设置左侧行号区域无问题)
// 因此resize这里增加重新设置viewPort Margin,但重设viewPort Margin,又会引起resize事件
// 会引起循环,加入一些保护条件
static bool lastVScrollBarVisiableFlag = verticalScrollBar()->isVisible();
if (verticalScrollBar()->isVisible() == false)
{
updateLineNumberAreaWidth(0);
lastVScrollBarVisiableFlag = false;
// 设定缩略图区大小
if (m_hasThumbnail)
m_thumbnailArea->setGeometry(
QRect(cr.left() + lineNumberAreaWidth() + viewport()->width(),
cr.top(), (cr.width() - lineNumberAreaWidth() - viewport()->rect().width()),
cr.height()));
}
else
{
if (verticalScrollBar()->isVisible() != lastVScrollBarVisiableFlag)
{
updateLineNumberAreaWidth(0);
lastVScrollBarVisiableFlag = verticalScrollBar()->isVisible();
// 设定缩略图区大小
if (m_hasThumbnail)
m_thumbnailArea->setGeometry(
QRect(cr.left() + lineNumberAreaWidth() + viewport()->width(),
cr.top(),
(cr.width() - lineNumberAreaWidth() - viewport()->rect().width()),
cr.height()));
}
}
}
void CHexEditor::lineNumberAreaMouseEvent(QMouseEvent *event)
{
QWidget::mousePressEvent(event);
}
void CHexEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
{
QPainter painter(m_lineNumberArea);
painter.setRenderHint(QPainter::Antialiasing, true);
// 绘制行号默认背景
painter.fillRect(event->rect(), m_lineNumBgColor);
QTextBlock block = firstVisibleBlock();
int blockNumber = block.blockNumber();
int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
int height = qRound(blockBoundingRect(block).height());
int bottom = top + height;
int currLineNum = textCursor().blockNumber();
while (block.isValid() && top <= event->rect().bottom()) {
if (block.isVisible() && bottom >= event->rect().top()) {
int lineNum = blockNumber + 1;
// 当前行号背景高亮与否
if (m_lineNumHighlightEnabled && blockNumber == currLineNum)
{
QRect highLightAreaRect(0, top, m_lineNumberArea->width(), fontMetrics().height() + 1);
painter.fillRect(highLightAreaRect, m_lineNumHighlightBgColor);
}
// 当前行的行号字体加粗与否
if (m_lineNumHighlightBold && blockNumber == currLineNum)
{
QFont font = painter.font();
font.setBold(true);
painter.setFont(font);
}
painter.setPen(m_lineNumTextColor);
// 右对齐方式绘制行号
painter.drawText(0, top, m_lineNumberArea->width() - m_lineAreaPaddingRight, fontMetrics().height(),
Qt::AlignRight, QString("%1").arg(blockNumber << 4, 8, 16, QChar('0')));
}
block = block.next();
top = bottom;
height = qRound(blockBoundingRect(block).height());
bottom = top + height;
++blockNumber;
}
}
void CHexEditor::thumbnailAreaPaintEvent(QPaintEvent *event)
{
if (!m_hasThumbnail)
return;
if (!m_data)
return;
QPainter painter(m_thumbnailArea);
painter.setRenderHint(QPainter::Antialiasing, true);
// 绘制缩略图区默认背景
painter.fillRect(event->rect(), palette().brush(QPalette::Base).color());
painter.setPen(m_thumbnailAreaTextColor);
QTextBlock block = firstVisibleBlock();
int blockNumber = block.blockNumber();
int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
int height = qRound(blockBoundingRect(block).height());
int bottom = top + height;
while (block.isValid() && top <= event->rect().bottom()) {
if (block.isVisible() && bottom >= event->rect().top()) {
// 右对齐方式绘制char字符
int loopFlag = 16;
if ((blockNumber + 1) * 16 > m_dataSize)
loopFlag = m_dataSize % 16;
QString strChar;
for (int i=0; i<loopFlag; i++)
{
if ((blockNumber * 16 + i) >= m_dataSize)
break;
if (m_data[blockNumber * 16 + i] < 32 || m_data[blockNumber * 16 + i] > 127)
strChar = QString(".");
else
strChar = QString::asprintf("%c", m_data[blockNumber * 16 + i]);
painter.drawText(8 + fontMetrics().horizontalAdvance('9') * i,
top,
fontMetrics().horizontalAdvance('9'),
fontMetrics().height(),
Qt::AlignRight, strChar);
}
}
block = block.next();
top = bottom;
height = qRound(blockBoundingRect(block).height());
bottom = top + height;
++blockNumber;
}
}
void CHexEditor::setData(quint8 *data, quint32 len)
{
if (!data || !len)
return;
if (m_data)
free(m_data);
m_data = (quint8 *)malloc(len);
if (!m_data)
return;
m_dataSize = len;
memcpy(m_data, data, len);
QString strData;
for (int i=0; i<len; i++)
{
if ((i & 0xf) == 0xf)
strData += QString("%1\n").arg(data[i], 2, 16, QChar('0'));
else
strData += QString("%1 ").arg(data[i], 2, 16, QChar('0'));
}
appendPlainText(strData);
moveCursor(QTextCursor::Start);
}
chexeditor.h
#ifndef CHEXEDITOR_H
#define CHEXEDITOR_H
#include <QPlainTextEdit>
#include <QObject>
#include <QWidget>
QT_BEGIN_NAMESPACE
class QPaintEvent;
class QResizeEvent;
class QSize;
class QWidget;
class QMouseEvent;
QT_END_NAMESPACE
class LineNumberArea;
class ThumbnailArea;
class CHexEditor : public QPlainTextEdit
{
Q_OBJECT
public:
CHexEditor(QWidget *parent = nullptr);
~CHexEditor();
int lineNumberAreaWidth();
// 处理行号区域鼠标点击事件
void lineNumberAreaMouseEvent(QMouseEvent *event);
// 处理行号区重绘事件
void lineNumberAreaPaintEvent(QPaintEvent *event);
// 缩略图区重绘事件
void thumbnailAreaPaintEvent(QPaintEvent *event);
// 缩略图设置
void setThumbnail(bool enable) { m_hasThumbnail = enable; }
// 行号高亮设置
void setLineNumHighlightBold(bool flag) { m_lineNumHighlightBold = flag; };
void setLineNumHighlightEnabled(bool flag) { m_lineNumHighlightEnabled = flag; };
void setLineNumHightlightBgColor(QColor color) { m_lineNumHighlightBgColor = color; };
// 行号颜色设置
void setLineNumBgColor(QColor color) { m_lineNumBgColor = color; };
void setLineNumTextColor(QColor color) { m_lineNumTextColor = color; };
// 设置Hex文本数据
void setData(quint8 *data, quint32 len);
//
void setThumbnailAreaTextColor(QColor color) { m_thumbnailAreaTextColor = color; };
protected:
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private slots:
void updateLineNumberAreaWidth(int newBlockCount);
void highlightCurrentLine();
void updateLineNumberArea(const QRect &rect, int dy);
private:
QWidget *m_lineNumberArea = nullptr; // 行号区对象
QWidget *m_thumbnailArea = nullptr; // 缩略图区对象
quint8 *m_data = nullptr;
quint32 m_dataSize = 0;
bool m_hasThumbnail = true; //是否显示右侧缩略图
// 缩略图区文字颜色
QColor m_thumbnailAreaTextColor = QColor(0xc0, 0xc0, 0xc0);
int m_lineAreaPaddingLeft = 5; //行号文字左侧留5像素画标记
int m_lineAreaPaddingRight = 5; //行号文字右侧留5像素画标记
// 高亮行背景色
QColor m_currLineHighlightColor = QColor(0x30, 0x50, 0x80).lighter();
// 行号区背景色
QColor m_lineNumBgColor = QColor(0x40, 0x40, 0x40); //QColor(0x60, 0x50, 0x60);
// 行号文本颜色
QColor m_lineNumTextColor = QColor(Qt::yellow).lighter();
// 行号是否高亮
bool m_lineNumHighlightEnabled = false;
// 行号高亮颜色
QColor m_lineNumHighlightBgColor = m_currLineHighlightColor; //Qt::blue;
// 当前行行号是否加粗显示
bool m_lineNumHighlightBold = false;
};
class LineNumberArea : public QWidget
{
public:
LineNumberArea(CHexEditor *editor) : QWidget(editor), m_hexEditor(editor)
{
}
QSize sizeHint() const override
{
return QSize(m_hexEditor->lineNumberAreaWidth(), 0);
}
protected:
void paintEvent(QPaintEvent *event) override
{
m_hexEditor->lineNumberAreaPaintEvent(event);
}
void mousePressEvent(QMouseEvent *event) override
{
m_hexEditor->lineNumberAreaMouseEvent(event);
}
private:
CHexEditor *m_hexEditor;
};
class ThumbnailArea : public QWidget
{
public:
ThumbnailArea(CHexEditor *editor) : QWidget(editor), m_hexEditor(editor)
{
}
QSize sizeHint() const override
{
return QSize(0, 0); //QSize(m_hexEditor->lineNumberAreaWidth(), 0);
}
protected:
void paintEvent(QPaintEvent *event) override
{
m_hexEditor->thumbnailAreaPaintEvent(event);
}
private:
CHexEditor *m_hexEditor;
};
#endif // CHEXEDITOR_H