基于QPlainTextEdit的16进制编辑器再改进

目录

1. 简介

2. 主要功能

3. API接口

4. 完整代码

4.1 chexeditor.h

4.2 chexeditor.cpp

4.3 调用示例


1. 简介

前阵子写了一个初版的16进制编辑器,做为另一个软件的子模块来使用,最近重新改进了一下,16进制编辑功能基本够用了。效果图如下:

2. 主要功能

  1. 带行号以及缩略图显示功能
  2. 可直接修改16进制数据,修改后的内容高亮标记,当通过SaveFile接口保存文件后高亮标记被自动清除。
  3. 鼠标右键菜单,提供只读与修订两种模式,修订模式可修改数据。
  4. 移动光标时,自动跳过空白区域,仅在有效的数字内移动。
  5. 可选每行显示16个或32个16进制数。
  6. 颜色等通过API可定制。
  7. 提供打开文件、保存文件接口。
  8. 提供外部直接传入数组的方式显示数据接口。

3. API接口

  1. void setLineNumBgColor(const QColor &color);

    设置行号区背景颜色。

  2. void setLineNumTextColor(const QColor& color);

    设置行号区文本色。

  3. void setHexNumPerLine(int number);

    每行显示16 / 32个十六进制数字,number = 16 or 32.

  4.  void setLineNumHighlightEnabled(bool flag);

    是否高亮主编辑器当前行。

  5. void setLineNumHightlightBgColor(const QColor &color);

    高亮当前行时的背景色。

  6. void setThumbnail(bool enable);

    是否开启右侧缩略图显示。

  7. void setHlModifiedByteEnabled(bool flag);

    是否开启修改内容时的高亮。

  8. void setData(quint8 *data, quint32 len);

    传入外部数组显示数据接口。

  9. void openFile(const QString &fileName);

    打开文件以16进制显示。

  10. void saveFile(const QString &fileName);

    修改后文件保存。

  11. 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);
}

转载需注明出处,码字不易,感谢点赞。

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值