目录
1. 简介
前阵子写了一个初版的16进制编辑器,做为另一个软件的子模块来使用,最近重新改进了一下,16进制编辑功能基本够用了。效果图如下:
2. 主要功能
- 带行号以及缩略图显示功能
- 可直接修改16进制数据,修改后的内容高亮标记,当通过SaveFile接口保存文件后高亮标记被自动清除。
- 鼠标右键菜单,提供只读与修订两种模式,修订模式可修改数据。
- 移动光标时,自动跳过空白区域,仅在有效的数字内移动。
- 可选每行显示16个或32个16进制数。
- 颜色等通过API可定制。
- 提供打开文件、保存文件接口。
- 提供外部直接传入数组的方式显示数据接口。
3. API接口
-
void setLineNumBgColor(const QColor &color);
设置行号区背景颜色。
-
void setLineNumTextColor(const QColor& color);
设置行号区文本色。
-
void setHexNumPerLine(int number);
每行显示16 / 32个十六进制数字,number = 16 or 32.
-
void setLineNumHighlightEnabled(bool flag);
是否高亮主编辑器当前行。
-
void setLineNumHightlightBgColor(const QColor &color);
高亮当前行时的背景色。
-
void setThumbnail(bool enable);
是否开启右侧缩略图显示。
-
void setHlModifiedByteEnabled(bool flag);
是否开启修改内容时的高亮。
-
void setData(quint8 *data, quint32 len);
传入外部数组显示数据接口。
-
void openFile(const QString &fileName);
打开文件以16进制显示。
-
void saveFile(const QString &fileName);
修改后文件保存。
-
void setThumbnailAreaTextColor(const QColor &color);
缩略图文本颜色设置。
4. 完整代码
4.1 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 HexLineNumberArea;
class ThumbnailArea;
class CHexEditor : public QPlainTextEdit
{
Q_OBJECT
public:
CHexEditor(QWidget *parent = nullptr);
~CHexEditor();
int lineNumberAreaWidth();
int thumbnailAreaWidth();
// 处理行号区域鼠标点击事件
void lineNumberAreaMouseEvent(QMouseEvent *event);
// 行号区鼠标滚动
void lineNumberAreaMouseWheelEvent(QWheelEvent *event);
// 处理行号区重绘事件
void lineNumberAreaPaintEvent(QPaintEvent *event);
// 缩略图鼠标滚动事件
void thumbnailAreaMouseWheelEvent(QWheelEvent *event);
// 缩略图区重绘事件
void thumbnailAreaPaintEvent(QPaintEvent *event);
// 顶部ruler区域重绘事件
void rulerAreaPaintEvent(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(const QColor &color) { m_lineNumHighlightBgColor = color; }
// 行号颜色设置
void setLineNumBgColor(const QColor &color) { m_lineNumBgColor = color; }
void setLineNumTextColor(const QColor& color) { m_lineNumTextColor = color; }
// 每行显示多少个16进制数
void setHexNumPerLine(int number) {
if (number != 16 && number != 32)
return;
m_hexNumPerLine = number;
if (number == 16)
m_hexNumPowerPerLine = 4;
else
m_hexNumPowerPerLine = 5;
m_rulerStr.clear();
for (int i=0; i<m_hexNumPerLine; i++)
m_rulerStr.append(QString::asprintf("%02x ", i));
}
// 是否开启修改字节的高亮
void setHlModifiedByteEnabled(bool flag) { m_hlModifiedByte = flag; }
// 设置Hex文本数据
void setData(quint8 *data, quint32 len);
void clearData();
// 打开文件
void openFile(const QString &fileName);
// 保存文件
void saveFile(const QString &fileName);
// 设置缩略图文本颜色
void setThumbnailAreaTextColor(const QColor &color) { m_thumbnailAreaTextColor = color; }
// 更改字体后,更新字符宽度
void updateAnsiCharSize() {
QFontMetricsF metrics(font());
m_ansiCharWidth = metrics.horizontalAdvance(QChar('F'));
m_ansiCharHeight = qRound(metrics.height());
}
protected:
virtual void paintEvent(QPaintEvent *event) override;
virtual void resizeEvent(QResizeEvent *event) override;
virtual void keyPressEvent(QKeyEvent *event) override;
virtual void hideEvent(QHideEvent *event) override;
private slots:
void onCursorPosChanged();
void on_docContentsChanged();
void updateLineNumberAreaWidth(int newBlockCount);
void highlightCurrentLine();
void updateLineNumberArea(const QRect &rect, int dy);
void on_customContextMenuRequested(const QPoint &pos);
void on_actionFileModeTriggered(bool checked);
private:
char ansiCharToHex(const char value); // 转换ascii码0-9,a-f 到16进制数字
private:
QWidget *m_lineNumberArea = nullptr; // 行号区对象
QWidget *m_thumbnailArea = nullptr; // 缩略图区对象
QWidget *m_rulerArea = nullptr; // 顶部ruler区域
QByteArray m_fileData; // 文件字节数组
qint64 m_dataSize = 0; // 保存当前打开的文件数组长度
int m_hexNumPerLine = 16; // 每行显示多少个16进制数
int m_hexNumPowerPerLine = 4 ; // 2^n,用于移位操作
bool m_hasThumbnail = true; //是否显示右侧缩略图
int m_thumbnaiAreaLeftPadding = 8; // 绘制缩略图时左填充
QColor m_thumbnailAreaTextColor = QColor(0xc0, 0xc0, 0xc0); // 缩略图区文字颜色
bool m_thumbnailSpliter = true; // 是否显示缩略图左侧虚线分隔条
int m_lineAreaPaddingLeft = 5; //行号文字左侧留5像素画标记
int m_lineAreaPaddingRight = 9; //行号文字右侧留9像素画标记
int m_rulerAreaPaddingTop = 30; // 顶部标尺区域保留高度
// 高亮行背景色
QColor m_currLineHighlightColor = QColor(0x30, 0x50, 0x80).lighter();
// 行号区背景色
QColor m_lineNumBgColor = QColor(0x40, 0x40, 0x40); /* QColor(0x60, 0x50, 0x60)*/
// 行号文本颜色
QColor m_lineNumTextColor = QColor(0xea, 0xb3, 0x08); //QColor(Qt::yellow).lighter(160);
// 行号是否高亮
bool m_lineNumHighlightEnabled = false;
// 行号高亮颜色
QColor m_lineNumHighlightBgColor = m_currLineHighlightColor;
// 当前行行号是否加粗显示
bool m_lineNumHighlightBold = false;
// 单个字符宽度
qreal m_ansiCharWidth = 0;
// 单个字符高度
qreal m_ansiCharHeight = 0; // 行高
// 顶部标尺字符串
QString m_rulerStr;
// 修改内容高亮标志
bool m_hlModifiedByte = true;
// 记录修改过的字节pos
QList<int> m_modifiedBytesPos;
};
class HexLineNumberArea : public QWidget
{
public:
HexLineNumberArea(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);
}
void wheelEvent(QWheelEvent *event) override
{
m_hexEditor->lineNumberAreaMouseWheelEvent(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);
}
void wheelEvent(QWheelEvent *event) override
{
m_hexEditor->thumbnailAreaMouseWheelEvent(event);
}
private:
CHexEditor *m_hexEditor;
};
class RulerArea : public QWidget
{
public:
RulerArea(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->rulerAreaPaintEvent(event);
}
private:
CHexEditor *m_hexEditor;
};
#endif // CHEXEDITOR_H
4.2 chexeditor.cpp
#include "chexeditor.h"
#include <QPainter>
#include <QTextBlock>
#include <QByteArray>
#include <QScrollBar>
#include <QMenu>
#include <QActionGroup>
#include <QFile>
CHexEditor::CHexEditor(QWidget *parent) : QPlainTextEdit(parent)
{
QFont defaultFont = QFont("JetBrains Mono", 10);
setFont(defaultFont);
// 创建行号区对象
m_lineNumberArea = new HexLineNumberArea(this);
m_lineNumberArea->setFont(defaultFont);
m_lineNumberArea->setCursor(Qt::ArrowCursor);
// 创建缩略图对象
m_thumbnailArea = new ThumbnailArea(this);
m_thumbnailArea->setFont(defaultFont);
m_thumbnailArea->setCursor(Qt::OpenHandCursor);
// 创建ruler区域
m_rulerArea = new RulerArea(this);
m_rulerArea->setFont(defaultFont);
m_rulerArea->setCursor(Qt::ArrowCursor);
viewport()->setCursor(Qt::PointingHandCursor); // 更改文本区鼠标指针
setCursorWidth(2); // 光标宽度
updateLineNumberAreaWidth(0); // 初始化行号宽度
document()->setDocumentMargin(0); // 文档边距控制
// 顶部ruler区纵坐标提示字符串 01 02 ... 0f
for (int i=0; i<m_hexNumPerLine; i++)
m_rulerStr.append(QString::asprintf("%02x ", i));
setOverwriteMode(true);
// 配置右键上下文菜单
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(on_customContextMenuRequested(QPoint)));
connect(this, &CHexEditor::blockCountChanged, this, &CHexEditor::updateLineNumberAreaWidth);
connect(this, &CHexEditor::updateRequest, this, &CHexEditor::updateLineNumberArea);
connect(this, &CHexEditor::cursorPositionChanged, this, &CHexEditor::onCursorPosChanged);
//connect(document(), &QTextDocument::contentsChange, this, &CHexEditor::on_docContentsChanged);
connect(document(), SIGNAL(contentsChanged()), this, SLOT(on_docContentsChanged()));
}
CHexEditor::~CHexEditor()
{
}
void CHexEditor::onCursorPosChanged()
{
highlightCurrentLine();
QTextCursor cursor = textCursor();
int pos = cursor.position();
// index 2, 5, 8, 11 自动往后跳一个字符,每行最后一个\n正好替代一个空格字符, 16 * 3
if (((pos + 1) % 0x3) == 0x0)
{
cursor.setPosition(pos + 1);
setTextCursor(cursor);
}
}
char CHexEditor::ansiCharToHex(const char value)
{
if (value >= 0x30 && value <= 0x39) // 0 - 9 数字
return value - 0x30;
else
return value - 87; // 小写 a - f (-0x61 + 10)
}
void CHexEditor::on_docContentsChanged()
{
QTextCursor cursor = textCursor();
if (cursor.position() < 0) // setPlainText时会触发本事件,pos = -1
return;
static bool filterFlag = false;
if (!filterFlag)
{
filterFlag = true;
return;
}
else
filterFlag = false;
int pos = cursor.position();
qDebug() << "+++++++++++++++++";
qDebug() << "position:" << pos;
int realPos = ((pos - 1) % 3 == 0) ? pos - 1 : pos - 2;
qDebug() << "real pos:" << realPos;
if (realPos < 0)
{
qDebug() << "filter the content change(real pos < 0)";
return;
}
// 打高亮标签
if (m_hlModifiedByte)
{
int highBitsPos = realPos;
if (realPos % 3)
highBitsPos = realPos - 1; // realPos在低位时左移一个字符,总是记录高位位置
// 不存在则保存当前位置
if (!m_modifiedBytesPos.contains(highBitsPos))
m_modifiedBytesPos << highBitsPos;
qDebug() << m_modifiedBytesPos;
QList<QTextEdit::ExtraSelection> tags;
tags = extraSelections();
// tag 0 储存整行高亮选择,先保存再恢复
QTextEdit::ExtraSelection hlCurrLineTag = tags.at(0);
tags.clear();
tags.append(hlCurrLineTag);
QTextCursor cursor = textCursor();
// 重新添加所有修改过字节的标记
QTextEdit::ExtraSelection selHex;
selHex.format.setBackground(QColor(56, 112, 83)); //(QColor(0x60, 0x60, 0x75))
selHex.format.setForeground(QColor(Qt::yellow).lighter());
for (int i=0; i<m_modifiedBytesPos.count(); i++)
{
// 总是选择整字节
cursor.setPosition(m_modifiedBytesPos.at(i));
cursor.setPosition(m_modifiedBytesPos.at(i) + 2, QTextCursor::KeepAnchor);
selHex.cursor = cursor;
cursor.clearSelection();
tags.append(selHex);
}
setExtraSelections(tags);
qDebug() << "tags count:" << tags.count();
}
// 所在行号
int lineNum = realPos / (m_hexNumPerLine * 3);
int posInLine = realPos % (m_hexNumPerLine * 3);
QString lineText;
lineText = document()->findBlockByLineNumber(lineNum).text();
// 组合两个ansi char为16进制数
qint8 high4Bits = 0, low4Bits = 0;
if (realPos % 3 == 0)
{
high4Bits = ansiCharToHex(lineText.at(posInLine).toLatin1());
low4Bits = ansiCharToHex(lineText.at(posInLine + 1).toLatin1());
}
else
{
high4Bits = ansiCharToHex(lineText.at(posInLine - 1).toLatin1());
low4Bits = ansiCharToHex(lineText.at(posInLine).toLatin1());
}
quint8 newValue = (high4Bits << 4) | low4Bits;
// 每个16进制数占用ansi字符3个, XX_, 一行最后一个XX\n
int offset = lineNum * m_hexNumPerLine + posInLine / 3;
m_fileData[offset] = newValue;
return;
#if 0
计算行号
// 修改一行的最后一个字符,光标pos会移动到下一行,因此-1再做除法
// 这里的 pos 值最小为1, 所以不会出负数
int lineNum = (pos - 1) / (m_hexNumPerLine * 3);
qDebug() << "lineNum:" << lineNum;
QString lineText;
lineText = document()->findBlockByLineNumber(lineNum).text();
int lineHexNum = m_hexNumPerLine; // 本行hex数组数量
if ((lineNum + 1) * m_hexNumPerLine > m_dataSize)
{
// 最后一行且不是满行
lineHexNum = m_dataSize - lineNum * m_hexNumPerLine;
}
qDebug() << "lineHexNum:" << lineHexNum;
lineText.remove(QChar(' ')); // 消去空格
QByteArray baCurrLine = lineText.toLatin1();// 存入数组
QByteArray newHexArr; // 保存转换后的16进制数
newHexArr.resize(lineHexNum); // 调整数组大小, 最后一行可能不为m_hexNumPerLine
// 数据转换two ansi chars -> one Hex
char *pCurrLine = baCurrLine.data();
for (int i=0; i<lineHexNum; i++)
{
int highIndex = i << 1;
int lowIndex = (i << 1) + 1;
if (pCurrLine[highIndex] >= 0x30 && pCurrLine[highIndex] <= 0x39) // 0 - 9 数组
pCurrLine[highIndex] -= 0x30;
else
pCurrLine[highIndex] -= 87; // 小写 a - f (-0x61 + 10)
if (pCurrLine[lowIndex] >= 0x30 && pCurrLine[lowIndex] <= 0x39) // 0 - 9 数组
pCurrLine[lowIndex] -= 0x30;
else
pCurrLine[lowIndex] -= 87; // 小写 a - f (-0x61 + 10)
// 组合baCurrLine[i], [i+1]为单个byte存入目标数组
newHexArr[i] = pCurrLine[highIndex] << 4 | pCurrLine[lowIndex];
}
qDebug() << "line after modified:" << newHexArr.toHex(' ');
memcpy(&m_fileData[lineNum * m_hexNumPerLine], newHexArr, lineHexNum);
#endif
}
void CHexEditor::on_customContextMenuRequested(const QPoint &pos)
{
QMenu *popMenu = new QMenu(this);
QAction *actRoMode = new QAction(tr("只读模式"));
actRoMode->setObjectName(QString::fromLatin1("actionRoMode"));
QAction *actRwMode = new QAction(tr("修订模式"));
actRwMode->setObjectName(QString::fromLatin1("actionRwMode"));
QAction *actCopy = new QAction(tr("复制"));
actCopy->setObjectName(QString::fromLatin1("actionCopy"));
QAction *actSelectAll = new QAction(QIcon(":/images/Selection.png"), tr("全部选择"));
actSelectAll->setObjectName(QString::fromLatin1("actionSelectAll"));
QActionGroup *modeGroup = new QActionGroup(popMenu);
modeGroup->addAction(actRoMode);
modeGroup->addAction(actRwMode);
actRoMode->setCheckable(true);
actRwMode->setCheckable(true);
if (overwriteMode())
actRwMode->setChecked(true);
else
actRoMode->setChecked(true);
// 加入菜单
popMenu->addAction(actRoMode);
popMenu->addAction(actRwMode);
popMenu->addSeparator();
popMenu->addAction(actCopy);
popMenu->addAction(actSelectAll);
connect(actRoMode, SIGNAL(triggered(bool)), this, SLOT(on_actionFileModeTriggered(bool)));
connect(actRwMode, SIGNAL(triggered(bool)), this, SLOT(on_actionFileModeTriggered(bool)));
connect(actCopy, SIGNAL(triggered()), this, SLOT(copy()));
connect(actSelectAll, SIGNAL(triggered()), this, SLOT(selectAll()));
popMenu->exec(cursor().pos());
delete popMenu;
}
void CHexEditor::on_actionFileModeTriggered(bool checked)
{
QAction *action =qobject_cast<QAction *>(sender());
if (action->objectName() == "actionRoMode")
setOverwriteMode(false);
else if (action->objectName() == "actionRwMode")
setOverwriteMode(true);
else
qDebug() << "invalid action:" << action;
}
// 重新计算行号区域宽度,当行数目变化时,blockCountChanged事件触发本函数计算。
void CHexEditor::updateLineNumberAreaWidth(int newBlockCount)
{
// PlainEdit 默认视口Margin为0,0,0,0,表示滚动区边缘到左、顶、右、底的距离
// 左侧设置为行号区域宽度,则左侧行号区等同于从视口滚动区剥离,
// 主体文字将不会显示在左侧被剥离的区域。
int rightMargin = 0;
if (m_hasThumbnail)
{
// 随着向左resize窗口,contentsRect宽度变小,因此rightMargin也相应变小,右侧缩略图可显示内容变少
rightMargin = thumbnailAreaWidth();
//rightMargin = rightMargin ? rightMargin - 1 : 0; // 修订一个像素
}
setViewportMargins(lineNumberAreaWidth(), m_rulerAreaPaddingTop, rightMargin - 1, 0);
// 右侧视口margin改变后会导致textEdit横向滚动条无法出现或点击时消失,设置一下滚动条最大宽度
//if (m_hasThumbnail)
//viewport()->setMaximumWidth(lineRect.width() + rightMargin);
}
int CHexEditor::lineNumberAreaWidth()
{
// 行号固定8个字符宽 + 左右填充宽度
return m_lineAreaPaddingLeft +
m_lineAreaPaddingRight +
(fontMetrics().horizontalAdvance(QLatin1Char('9')) << 3);
}
int CHexEditor::thumbnailAreaWidth()
{
// 每行显示输出 m_hexNumPerLine Hex数字, 每数字占用宽度m_hexNumPerLine * 3字符
int vScrollBarWidth = verticalScrollBar()->isVisible() ? verticalScrollBar()->rect().width() : 0;
QString strMaxLine(m_hexNumPerLine * 3, QChar('F'));
QRect lineRect = fontMetrics().boundingRect(strMaxLine);
int width = 0;
// document margin 左右都有,因此减去 documentMargin() * 2
width = contentsRect().width() - lineRect.width() - lineNumberAreaWidth() - vScrollBarWidth - document()->documentMargin() * 2;
width = width < 0 ? 0 : width;
return width;
}
// updateReqest信号触发本函数
// 文本卷动时,rect包含整个视口区域,
// 当文本垂直卷动时,dy参数携带视口卷动的像素数。
void CHexEditor::updateLineNumberArea(const QRect &rect, int dy)
{
if (dy) // 垂直滚动时
{
m_lineNumberArea->scroll(0, dy); // 行号区同步滚动dy像素
if (m_hasThumbnail)
m_thumbnailArea->scroll(0, dy); // 缩略图区同步滚动dy像素
}
else
{
m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
if (m_hasThumbnail)
m_thumbnailArea->update(0, rect.y(), m_thumbnailArea->width(), rect.height());
}
//if (rect.contains(viewport()->rect()))
updateLineNumberAreaWidth(0);
}
void CHexEditor::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> tags;
tags = extraSelections();
if (!isReadOnly()) {
QTextEdit::ExtraSelection selection;
selection.format.setBackground(m_currLineHighlightColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
if (tags.isEmpty())
tags.append(selection);
else
tags.replace(0, selection); // 高亮标签始终放在第一个元素
}
setExtraSelections(tags);
}
void CHexEditor::paintEvent(QPaintEvent *event)
{
QPlainTextEdit::paintEvent(event);
if (!m_hasThumbnail)
return;
QPainter dc(viewport());
// 开启反锯齿
dc.setRenderHint(QPainter::Antialiasing, true);
#if 0
// 右侧缩略图虚线位置y坐标
int vertSplitLine = viewport()->rect().right();
QPen verticalLinePen = QPen(QColor(Qt::gray));
verticalLinePen.setStyle(Qt::DotLine);
dc.setPen(verticalLinePen);
// 绘制右侧提示线
dc.drawLine(vertSplitLine, contentsRect().top(), vertSplitLine, contentsRect().bottom());
#endif
}
// PlainEdit大小变化时,同步更新行号区域大小
void CHexEditor::resizeEvent(QResizeEvent *e)
{
QPlainTextEdit::resizeEvent(e);
QRect cr = contentsRect();
// 窗口大小改变时,也需要更新一下行号区域,尽量保持主内容显示完整,右侧缩略图随着窗口缩小可以部分显示或不显示
updateLineNumberAreaWidth(0);
// 设定行号区大小
m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top() + m_rulerAreaPaddingTop, lineNumberAreaWidth(), cr.height()));
// 设定缩略图区大小
int hzBarHeight = horizontalScrollBar()->isVisible() ? horizontalScrollBar()->height() : 0;
// 检查是否有横向滚动条,需要对缩略图区域的高度减去横向滚动条高度
m_thumbnailArea->setGeometry(
QRect(cr.left() + lineNumberAreaWidth() + viewport()->width(),
cr.top() + m_rulerAreaPaddingTop, thumbnailAreaWidth(), cr.height() - hzBarHeight));
// 设定顶部ruler区大小
m_rulerArea->setGeometry(cr.left(), cr.top(), cr.width(), m_rulerAreaPaddingTop);
}
void CHexEditor::keyPressEvent(QKeyEvent *event)
{
if ((event->key() >= Qt::Key_Left) && (event->key() <= (Qt::Key_Up + 3)))
{
QTextCursor cursor = textCursor();
int pos = cursor.position();
switch (event->key())
{
case Qt::Key_Up:
case Qt::Key_Down:
return QPlainTextEdit::keyPressEvent(event);
break;
case Qt::Key_Left:
if ((pos) % 3 == 0)
{
if (pos >= 2)
{
cursor.setPosition(pos - 2); // 向左跳过空格+1字符
setTextCursor(cursor);
}
}
else
return QPlainTextEdit::keyPressEvent(event);
break;
case Qt::Key_Right:
if ((pos + 1) % 3 == 0)
{
cursor.setPosition(pos + 1); // 跳过空格
setTextCursor(cursor);
}
else
return QPlainTextEdit::keyPressEvent(event);
break;
default:
break;
}
}
else if (event->modifiers() & Qt::ControlModifier)
{
// 仅允许 Ctrl+Z、Ctrl+C
if (event->key() == Qt::Key_Z || event->key() == Qt::Key_C)
QPlainTextEdit::keyPressEvent(event);
}
else
{
QString inputKey = event->text();
if (inputKey.isEmpty())
return;
//qDebug() << inputKey;
QChar firstKey = inputKey[0].toLower();
if (firstKey.isDigit() || (firstKey >= 'a' && firstKey <= 'f'))
{
if (overwriteMode())
{
QTextCursor cursor = textCursor();
// characterCount()函数至少包括了一个额外的x2029,因此减1
if (cursor.position() >= document()->characterCount() - 1)
return;
QPlainTextEdit::keyPressEvent(event);
}
}
else
event->ignore();
}
}
void CHexEditor::hideEvent(QHideEvent *event)
{
// 隐藏时释放QByteArray内存
#if 0
if (m_dataSize)
{
m_fileData = QByteArray();
m_dataSize = 0;
}
#endif
QPlainTextEdit::hideEvent(event);
}
void CHexEditor::lineNumberAreaMouseEvent(QMouseEvent *event)
{
QWidget::mousePressEvent(event);
}
void CHexEditor::lineNumberAreaMouseWheelEvent(QWheelEvent *event)
{
QPoint numDegrees = event->angleDelta() / 8;
if (!numDegrees.isNull())
{
// scrollContentsBy(int dx, int dy)
QPoint numSteps = numDegrees / 15;
int dy = 0;
if (numSteps.y() > 0)
dy = -3;
else
dy = 3;
// + (-3) 表示向下滚动3行
// + 3表示向上滚动3行
verticalScrollBar()->setValue(verticalScrollBar()->value() + dy);
event->accept();
return;
}
QWidget::wheelEvent(event);
}
void CHexEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
{
QPainter painter(m_lineNumberArea);
painter.setRenderHint(QPainter::Antialiasing, true);
// 绘制行号默认背景
painter.fillRect(event->rect(), m_lineNumBgColor);
painter.setPen(m_lineNumTextColor);
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()) {
// 当前行号背景高亮与否
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 << m_hexNumPowerPerLine, 8, 16, QChar('0')));
}
block = block.next();
top = bottom;
height = qRound(blockBoundingRect(block).height());
bottom = top + height;
++blockNumber;
}
// 绘制行号区竖线
int vertLineX = lineNumberAreaWidth() - 4;
painter.drawLine(vertLineX, 0, vertLineX, contentsRect().height());
}
void CHexEditor::thumbnailAreaMouseWheelEvent(QWheelEvent *event)
{
return lineNumberAreaMouseWheelEvent(event);
}
void CHexEditor::thumbnailAreaPaintEvent(QPaintEvent *event)
{
if (!m_hasThumbnail)
return;
if (m_dataSize <= 0)
return;
char *fileData = m_fileData.data();
QPainter painter(m_thumbnailArea);
painter.setRenderHint(QPainter::Antialiasing, true);
if (m_thumbnailSpliter)
{
// 缩略图左侧竖虚线位置y坐标
QPen splitLinePen = QPen(QColor(Qt::gray));
splitLinePen.setStyle(Qt::DotLine);
painter.setPen(splitLinePen);
// 绘制缩略图区左侧竖虚线提示线
painter.drawLine(1, contentsRect().top(), 1, contentsRect().bottom());
}
// 绘制缩略图区默认背景
//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 = m_hexNumPerLine;
if ((blockNumber + 1) * m_hexNumPerLine > m_dataSize)
loopFlag = m_dataSize % m_hexNumPerLine;
QString strChar;
for (int i=0; i<loopFlag; i++)
{
if ((blockNumber * m_hexNumPerLine + i) >= m_dataSize)
break;
if ((quint8)fileData[blockNumber * m_hexNumPerLine + i] < 32 ||
((quint8)fileData[blockNumber * m_hexNumPerLine + i] >= 127 && (quint8)fileData[blockNumber * m_hexNumPerLine + i] < 160))
strChar = QString(".");
else
strChar = QString::asprintf("%c", fileData[blockNumber * m_hexNumPerLine + i]);
QRectF textRect(m_thumbnaiAreaLeftPadding + m_ansiCharWidth * i, top, m_ansiCharWidth, m_ansiCharHeight);
painter.drawText(textRect, Qt::AlignLeft, strChar);
//painter.drawText(m_thumbnaiAreaLeftPadding + charWidth * i,
// top, charWidth, charHeight, Qt::AlignLeft, strChar);
}
}
block = block.next();
top = bottom;
height = qRound(blockBoundingRect(block).height());
bottom = top + height;
++blockNumber;
}
}
void CHexEditor::rulerAreaPaintEvent(QPaintEvent *event)
{
QPainter painter(m_rulerArea);
painter.setRenderHint(QPainter::Antialiasing, true);
QPen horzLinePen = QPen(QColor(0xea, 0xb3, 0x08)); // QColor(Qt::red));
horzLinePen.setStyle(Qt::SolidLine);
painter.setPen(horzLinePen);
int centerWidth = m_ansiCharWidth * (m_hexNumPerLine * 3) + 2 * document()->documentMargin();
int x1 = lineNumberAreaWidth();
int y1 = m_rulerAreaPaddingTop - 4;
// 绘制默认背景
// int rightAreaCharsWidth = m_ansiCharWidth * m_hexNumPerLine + m_thumbnaiAreaLeftPadding;
//painter.fillRect(contentsRect().x(), 0, x1 + centerWidth + rightAreaCharsWidth, m_rulerAreaPaddingTop, m_lineNumBgColor);
painter.fillRect(contentsRect().x(), 0, contentsRect().width(), m_rulerAreaPaddingTop, m_lineNumBgColor);
// 绘制顶部水平分隔线
//painter.drawLine(x1, y1, x1 + centerWidth + rightAreaCharsWidth , y1); // 线最右侧为缩略图的右边缘
painter.drawLine(x1, y1, contentsRect().right() , y1); // 线最右侧为窗体边缘
// 绘制顶部 01 02 03 .. 0f...
painter.drawText(x1 + document()->documentMargin(), y1 - m_ansiCharHeight - 4, centerWidth, m_ansiCharHeight, Qt::AlignLeft, m_rulerStr);
//painter.drawText(x1, y1 - 8, m_rulerStr);
}
void CHexEditor::setData(quint8 *data, quint32 len)
{
if (!data || !len)
return;
m_fileData.resize(len);
m_dataSize = len;
char *fileData = m_fileData.data();
memcpy(fileData, data, len);
QString strData;
for (int i=0; i<len; i++)
{
if ((i & (m_hexNumPerLine - 1)) == (m_hexNumPerLine - 1))
strData += QString::asprintf("%02x\n", fileData[i] & 0xff);
else
strData += QString::asprintf("%02x ", fileData[i] & 0xff);
}
setPlainText(strData);
moveCursor(QTextCursor::Start);
}
void CHexEditor::clearData()
{
if (m_dataSize)
{
qDebug() <<"CHexEditor::clearData()";
setPlainText(""); // 清除编辑器显示的文本
// 清空旧标记
if (!m_modifiedBytesPos.isEmpty())
{
m_modifiedBytesPos.clear();
QList<QTextEdit::ExtraSelection> tags;
setExtraSelections(tags); // 清空标记
highlightCurrentLine(); // 重新高亮当前行
}
m_fileData = QByteArray(); // 释放内存
m_dataSize = 0;
}
}
void CHexEditor::openFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly))
{
qDebug() << "CHexEditor::openFile(): failed to open file:" << fileName;
return;
}
m_dataSize = file.size();
m_fileData.resize(m_dataSize); // 分配内存
char *fileData = m_fileData.data();
if (file.read(fileData, m_dataSize) != m_dataSize)
{
qDebug() << "CHexEditor::openFile(): length error on reading file:" << fileName;
m_fileData = QByteArray(); // 释放内存
m_dataSize = 0;
file.close();
return;
}
file.close();
QString strData;
for (int i=0; i<m_dataSize; i++)
{
if ((i & (m_hexNumPerLine - 1)) == (m_hexNumPerLine - 1))
strData += QString::asprintf("%02x\n", fileData[i] & 0xff);
else
strData += QString::asprintf("%02x ", fileData[i] & 0xff);
}
setPlainText(strData);
moveCursor(QTextCursor::Start);
// 读取文件时清空旧标记
if (!m_modifiedBytesPos.isEmpty())
{
m_modifiedBytesPos.clear();
QList<QTextEdit::ExtraSelection> tags;
setExtraSelections(tags); // 清空标记
highlightCurrentLine(); // 重新高亮当前行
}
}
void CHexEditor::saveFile(const QString &fileName)
{
QFile file(fileName);
QFile::remove(fileName);
if (!file.open(QIODevice::WriteOnly))
{
qDebug() << "failed to open file for writting:" << fileName;
return;
}
qint64 wrBytes = file.write(m_fileData, m_dataSize);
if (wrBytes != m_dataSize)
qDebug("failed: only %lld of %lld bytes written", wrBytes, m_dataSize);
// 修保存文件时需要清除所有修订标记
if (!m_modifiedBytesPos.isEmpty())
{
m_modifiedBytesPos.clear(); // 删除记录的位置信息
QList<QTextEdit::ExtraSelection> tags;
setExtraSelections(tags); // 清空标记
highlightCurrentLine(); // 重新高亮当前行
}
file.close();
}
4.3 调用示例
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
m_hexEdit = new CHexEditor(this);
m_hexEdit->setWordWrapMode(QTextOption::NoWrap);
m_hexEdit->setLineNumHighlightEnabled(true);
m_hexEdit->updateAnsiCharSize(); // 这句需要执行一下,为了提高绘图效率
m_hexEdit->setThumbnail(true);
m_hexEdit->setFrameStyle(QFrame::NoFrame);
// 设置字体色
setStyleSheet("CHexEditor {color: #e0e0e0; background-color: #303030; }");
setCentralWidget(m_hexEdit);
quint8 testArr[1024] = {0};
for (int i=0; i<sizeof(testArr); i++)
testArr[i] = QRandomGenerator::global()->bounded(0, 255);
m_hexEdit->setData(&testArr[0], sizeof(testArr));
connect(ui->actionOpen, SIGNAL(triggered(bool)), this, SLOT(on_actionOpenTriggered(bool)));
connect(ui->actionSave, SIGNAL(triggered(bool)), this, SLOT(on_actionSaveTriggered(bool)));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_actionOpenTriggered(bool checked)
{
QString fileName = QFileDialog::getOpenFileName(
this, "以16进制方式打开文件", "./", "Plain Text File(*.txt);; All Files(*.*)");
if (!fileName.isEmpty())
m_hexEdit->openFile(fileName);
}
void MainWindow::on_actionSaveTriggered(bool checked)
{
QString fileName = QFileDialog::getSaveFileName(
this, "另存文件为", "./", "Plain Text File(*.txt);; All Files(*.*)");
if (!fileName.isEmpty())
m_hexEdit->saveFile(fileName);
}
转载需注明出处,码字不易,感谢点赞。