资讯动态

hexedit 十六进制编辑控件 MFC 集成实战:装载、查找、保存与避坑指南

发布时间:2026/10/18 12:20:48 来源:尧图企业网站定制
简介HexEdit 是一套基于 VC 环境开发的十六进制编辑控件面向需要直接查看和修改二进制文件的开发者广泛适用于软件调试、系统维护、数据恢复、逆向工程等场景可帮助用户摆脱依赖通用编辑器的局限快速定位并修改任意二进制数据。控件封装了文件读写 API、数据渲染、查找替换、插入删除等核心操作同时支持 ASCII 与二进制双视图模式界面简洁直观并提供充足的扩展接口以便按项目需求定制。压缩包共 53 个文件其中以 9 个 h 头文件、8 个 cpp 源文件为主要代码主体搭配 vcxproj/sln 工程文件与 exe 演示程序另有 htm 升级说明及调试过程文件整体大小仅 2.02MB结构组织清晰适合直接导入工程学习或二次开发。演示项目与升级日志完整展示了从控件集成、界面布局到接口调用的全过程可帮助开发者快速理解控件的初始化、打开文件、数据编辑及保存回写等关键步骤缩短同类功能的开发周期。目前已有 97 人学习下载对于需要在 Windows 平台构建十六进制编辑器或类似二进制数据工具的程序员来说是一份小而实用的参考代码。1. 十六进制编辑控件不是玩具先想清楚要它做什么做固件解析、协议调试、PE 文件修补的同行应该都有过这种时刻拿 Hex 工具打开一个 bin 文件找到偏移 0x1A2C 处改掉 4 个字节再烧回设备。一次两次能忍等要批量处理几十个文件、或者把这种能力塞进自研的测试工具时就发现独立 EXE 帮不上忙了。这时候你会想自己的程序里要是能嵌一个 16 进制编辑器就好了。我最近拆了一份在 VC 环境下流传很广的 hexedit 十六进制编辑控件源码它解决的正是这个问题——把二进制浏览、编辑、查找、跳转能力直接变成控件拖到对话框里就能用。适合做上位机、文件分析器、调试辅助工具的人。但接入它不像往界面上放一个 Button 那么简单源码里有不少隐性的坑今天一次性说清楚。2. 把 hexedit 控件接进对话框从导入源码到绑定控件变量2.1 源码导入与编译配置拿到 hexedit 控件的源码包第一件事不是急着建工程而是先看清目录结构。常见做法是把源码直接编进你的项目而不是编译成 DLL 再引用。因为控件本身依赖很多内部消息循环静态编译反而少了很多导出接口上的麻烦。把HexEdit.h、HexEdit.cpp这两个核心文件拷贝到工程目录然后在 VS 工程的“现有项”里添加它们。如果工程用了 MFC 共享库确保项目属性里选择了“在共享 DLL 中使用 MFC”。字符集要记得统一我一般建议直接使用 Unicode 字符集因为 hexedit 内部对文本的处理是基于 TCHAR 的你如果项目是 Unicode控件里那些宽窄字符转换就不用额外改。以下是集成到对话框的基本步骤。// HexEditDlg.h #include HexEdit.h class CHexEditDlg : public CDialogEx { // 在对话框类中声明一个控件对象 CHexEdit m_wndHex; };这里声明了一个成员对象m_wndHex它对应的是CHexEdit类也就是 hexedit 控件的封装类。注意不需要像普通控件在资源编辑器里拖一个控件进去这个控件是动态创建的。接着在对话框的OnInitDialog里创建它并设置初始位置BOOL CHexEditDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 创建一个位置矩形这里是对应对话框中预留的 IDC_STATIC 控件 CRect rc; CWnd* pWnd GetDlgItem(IDC_STATIC_PLACEHOLDER); pWnd-GetWindowRect(rc); ScreenToClient(rc); // 创建 hexedit 控件 m_wndHex.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER, rc, this, 0x1001); // 初始化控件属性 m_wndHex.SetFont(m_font); m_wndHex.SetBytesPerRow(16); return TRUE; }注意Create的最后一个参数是控件 ID这里用了0x1001建议用IDC_HEXEDIT这样的宏定义避免魔法数字。SetFont是设置显示字体建议用等宽字体如 Consolas否则十六进制字节对不齐。SetBytesPerRow是每行显示多少字节16 是常用设置如果你要查看的是离散寄存器值也可以设为 8 或 32。这一阶段最常见的翻车点是忘记在OnInitDialog里调用Create结果界面上一片空白。另外WS_TABSTOP风格一定要加否则 Tab 键焦点进不了控件键盘方向键无法操作。2.2 控件变量绑定与消息映射如果你不想用Create动态创建也可以把 hexedit 控件静态嵌入到对话框模板里。前提是你要么自己实现一个自定义控件注册要么直接在资源编辑器里放置一个自定义控件指定类名为 hexedit 的 WNDCLASS 名称。不过大多数 hexedit 控件源码是不带注册类的它靠的是 MFC 的窗口子类化机制。你可以在对话框模板里放一个普通的自定义控件窗口然后在DoDataExchange里把它绑定到CHexEdit对象void CHexEditDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_HEXEDIT_PLACEHOLDER, m_wndHex); }这个做法要求 hexedit 控件类实现了Create和PreSubclassWindow否则 DDX_Control 无法接管窗口。很多早期版本的 hexedit 源码在PreSubclassWindow里做了一些初始化如果你发现绑定后控件不可编辑多半是PreSubclassWindow没被调用或者窗口类名不匹配。绑定之后还需要给控件添加通知消息比如当用户修改了某个字节时通知主对话框更新状态栏。在消息映射中手动添加一个自定义消息#define WM_HEXEDIT_UPDATE (WM_USER 100) BEGIN_MESSAGE_MAP(CHexEditDlg, CDialogEx) ON_MESSAGE(WM_HEXEDIT_UPDATE, OnHexEditUpdate) END_MESSAGE_MAP()然后在控件代码内部合适位置发送这个消息比如在OnChar处理完成之后调用::SendMessage(GetParent(), WM_HEXEDIT_UPDATE, 0, 0)。如果你遇到控件没有响应键盘输入先检查对话框是否能接收到焦点。我遇到过类似情况最后发现是因为对话框的WS_EX_CONTROLPARENT样式没设置导致 Tab 链失效。3. 数据装载与保存从 CFile 到内存缓冲区的几种做法3.1 文件读入与初始化显示hexedit 控件的核心是内存缓冲区它内部维护了一块字节数组。装载一个文件本质上就是把这个字节数组填充好然后刷新显示。常见做法是先用CFile把整个文件读入到一个CByteArray或std::vectorBYTE里然后调用控件的SetData方法。有些版本的 hexedit 类没有SetData而是直接暴露m_pData指针你需要把缓冲区的指针传进去。下面是一个稳妥的装载流程// 读取整个文件到内存 CFile file; if (!file.Open(strFilePath, CFile::modeRead)) { AfxMessageBox(_T(打开失败)); return; } ULONGLONG ullLen file.GetLength(); if (ullLen 0) { file.Close(); return; } // 限制最大文件大小避免内存耗尽例如 256MB const ULONGLONG MAX_FILE_SIZE 256 * 1024 * 1024; if (ullLen MAX_FILE_SIZE) { AfxMessageBox(_T(文件过大无法装载)); file.Close(); return; } std::vectorBYTE buffer((size_t)ullLen); file.Read(buffer.data(), (UINT)ullLen); file.Close(); // 将数据设置到 hexedit 控件 m_wndHex.SetData(buffer.data(), (int)buffer.size()); m_wndHex.Invalidate();这里有几个参数值得说明。SetData的第二个参数是数据长度必须与缓冲区实际长度一致。如果你传入的缓冲区来自 local 变量在SetData之后不能立刻释放因为很多 hexedit 实现内部只是保存了指针而不是拷贝数据。你会看到界面能显示但一旦源缓冲区释放控件就是一堆随机数。我一般会用成员变量std::vectorBYTE m_fileBuffer来持有这份数据确保生命周期与控件共存。如果你拿到的 hexedit 版本支持SetData内部拷贝那就无所谓。但为了保险请先用小文件测试加载一个 100 字节的文件然后关闭对话框看是否崩溃。如果不崩溃说明内部有拷贝如果崩溃就用成员缓冲区。3.2 修改后写回与大小变化用户编辑完字节后需要把数据写回文件。大部分 hexedit 控件提供GetData或GetModified接口。写回前要判断控件状态是否被修改过。典型的写回代码// 检查是否有未保存的修改 if (m_wndHex.IsModified()) { // 获取当前数据长度可能因插入/删除而变化 int nDataLen m_wndHex.GetDataLength(); std::vectorBYTE newBuffer(nDataLen); m_wndHex.GetData(newBuffer.data(), nDataLen); // 写回文件 CFile file; if (file.Open(strFilePath, CFile::modeWrite | CFile::modeCreate)) { file.Write(newBuffer.data(), nDataLen); file.Close(); m_wndHex.SetModified(FALSE); } }注意GetDataLength是获取控件内部缓冲区当前长度与原始文件长度不一定相同。因为 hexedit 支持插入和删除字节。如果控件在设计上只允许修改现有字节而不允许增减长度那GetDataLength恒等于初始长度。你要根据自己的需求来决定是否开启插入删除功能。有些版本会在GetData时返回一个指向内部缓冲区的只读指针直接memcpy也行。但为了避免越界我还是建议用GetData加目标缓冲区的模式。3.3 参数地址位数、列宽、字体设置等hexedit 控件比较在意的参数有这么几个参数常见值作用BytesPerRow16每行显示的字节数影响横向滚动范围AddressBytes4 / 8地址显示的字节宽度4 位适合小文件8 位适合大文件ColumnSpacing8~12 像素调整十六进制和 ASCII 区域的间隔Font等宽字体保证字符对齐Consolas 或 Courier New 都可以ReadOnlyTRUE/FALSE是否允许编辑设置这些参数的代码通常在OnInitDialog里m_wndHex.SetBytesPerRow(16); m_wndHex.SetAddressBytes(8); m_wndHex.SetColumnSpacing(10); m_wndHex.SetReadOnly(FALSE);这里特别提醒SetAddressBytes(8)只适合显示超过 4GB 地址空间的文件如果你只是看一个几十 KB 的固件用 4 位更清爽。不过两者只影响显示不影响内部逻辑。字体设置也很关键。如果你不设置等宽字体默认的宋体MFC 对话框字体会导致每个字节占据宽度不同界面看起来非常乱。我通常在对话框初始化时创建一个CFontm_font.CreatePointFont(90, _T(Consolas)); // 9pt m_wndHex.SetFont(m_font);注意CFont对象也要是成员变量否则OnInitDialog返回后字体对象销毁控件字体又变回默认。4. 与编辑需求配套查找、跳转与选中高亮4.1 查找二进制序列十六进制编辑器的查找功能不能简单用strstr因为数据里可能有\0。控件内部一般提供Find函数传入字节数组和模式。常见实现是封装一个FindPattern方法int CHexEdit::FindPattern(const BYTE* pPattern, int nPatternLen, int nStartAddr) { for (int i nStartAddr; i m_nDataLen - nPatternLen; i) { if (memcmp(m_pData i, pPattern, nPatternLen) 0) return i; } return -1; }这个函数从指定地址开始逐字节比对。虽然效率不高但对于一般几十 MB 的文件都能接受。如果你要查找几 GB 文件建议用 BM 算法但 hexedit 控件大多没有内置需要自己扩展。调用查找时通常会弹出一个对话框让用户输入十六进制字符串比如输入A1 B2 C3然后解析成字节数组再调用FindPattern。注意解析时别忽略空格也不要幻想用户会规范输入。4.2 跳转到偏移地址跳转功能往往是配合查找使用的。找到某个字节位置后需要让控件滚动到对应行并高亮显示。控件一般提供SetSel或GoTo方法。实现跳转int nOffset m_wndHex.FindPattern(pPattern, nLen, 0); if (nOffset 0) { m_wndHex.SetSel(nOffset, nOffset nLen); m_wndHex.ShowAddr(nOffset); }这里SetSel选中一段范围ShowAddr让指定地址出现在可见区域。不同版本的 hexedit 可能方法名不同但基本逻辑一致先移动光标再滚动视口。有一个细节ShowAddr有些实现是设置当前显示页的起始地址而不是精确滚动到目标行。如果你发现跳转后目标行在视口底部看不全可以用EnsureVisible之类的额外步骤。如果没有这个方法就自己算一下行号调用SetScrollPos加ScrollWindow。这种“假滚动”问题我在好几个版本里都遇到过。4.3 高亮与标记除了查找另一个高频需求是把某些特征字节高亮比如协议头、魔数。hexedit 控件不一定支持多区域高亮需要你查看源码里是否有SetHighlightRgn或SetMark接口。如果没有你可以通过重绘控件来实现。但更省事的方法是把高亮区域的信息存储在一个数组里然后修改控件的OnPaint在绘制完原有内容后再绘制一个半透明矩形覆盖在目标行上。这个做法需要你对 MFC 绘图有一定了解否则容易造成重影。常见做法是利用控件的OnDraw中已有的字节坐标计算函数。你可以在源码里找到类似GetRectByOffset的私有方法然后在OnPaint末尾调用它画一个FillRect。注意要处理滚动偏移否则高亮位置会错。我一般不会在主线程里频繁重绘高亮而是用一个标记结构体struct HEX_HIGHLIGHT { int nStart; int nEnd; COLORREF clr; }; std::vectorHEX_HIGHLIGHT m_vecHighlights;然后通知控件刷新。这样你可以一次高亮多个区域比如把固件里的所有关键配置块都标出来。5. 避坑/常见问题内存、刷新、Unicode、光标闪烁我在把 hexedit 控件集成到自己的工具中时踩过不少坑这里列出 5 个最具代表性的每条都是“现象 → 原因 → 解决”。坑 1关闭对话框时崩溃现象加载文件、编辑几处、关闭对话框程序直接报内存访问越界。原因控件内部的m_pData指针引用了临时缓冲区而这个缓冲区在OnInitDialog里用局部变量创建函数返回后就被释放。控件在析构时企图访问它就崩溃了。解决将缓冲区声明为对话框类的成员变量并在装载时保持其生命周期。或者在SetData之后让控件自己复制一份数据。最好在控件类的SetData实现里加一个memcpy而不是单纯保存指针。坑 2界面显示乱码字节不对齐现象十六进制列看起来歪歪扭扭ASCII 列对不上。原因没有使用等宽字体或者某个版本的控件内部使用DrawText没有固定字符宽度。解决统一设置Consolas或Courier New字体同时确保SetBytesPerRow在设置字体之后调用否则行宽计算用旧字体会出现错位。坑 3控件不接收键盘输入现象点击控件后光标不闪烁按方向键没反应。原因控件没有获得焦点或者对话框没有把焦点转移给控件。有时候是因为控件创建时没有WS_TABSTOP风格。解决在OnInitDialog里调用m_wndHex.SetFocus()。同时检查控件创建参数里是否包含WS_TABSTOP这样 Tab 键才能真正进入控件。坑 4修改状态保存无效重新打开文件后修改丢失现象调用SetModified(FALSE)后再编辑关闭程序时提示未保存或者写回文件后发现没有变化。原因SetModified只是清除了界面上的修改标记但控件内部的状态计数器可能没有正确重置。或者你的写回逻辑用了旧的缓冲区指针没有同步更新。解决写回完成后明确调用m_wndHex.Invalidate()并确保GetData拿到的数据是最新的。这里我自己的习惯是每次写回后主动重建内部缓冲区而不是依赖控件内部的“懒更新”。坑 5Unicode 编译环境下部分功能异常现象项目从多字节字符集切换到 Unicode 后控件显示地址区时出现乱码或者编辑中文注释时崩溃。原因早期 hexedit 源码里大量使用char*和CString混用没有统一用_tcs函数导致宽窄字符转换出错。解决在工程属性中强制使用 Unicode 字符集并检查控件源码中所有与字符串相关的 API把char*改为TCHAR*或者使用CString自动转换。更简单的方法是找最新版的 hexedit 源码或者自己对涉及文本绘制的部分做一次替换。6. 让控件更好用的两个进阶技巧拖拽打开与实时校验6.1 拖拽文件到控件很多用户喜欢直接把文件拖到窗口上打开这比点按钮选择文件快得多。实现拖拽打开需要让对话框接受WM_DROPFILES消息。在ON_WM_DROPFILES消息处理函数里解析文件路径然后调用已有的LoadFile函数void CHexEditDlg::OnDropFiles(HDROP hDropInfo) { // 只处理第一个拖进来的文件 TCHAR szPath[MAX_PATH] {0}; DragQueryFile(hDropInfo, 0, szPath, MAX_PATH); if (PathFileExists(szPath)) { LoadFile(szPath); } DragFinish(hDropInfo); CDialogEx::OnDropFiles(hDropInfo); }记得在OnInitDialog里调用DragAcceptFiles(TRUE)否则窗口不会接收拖拽消息。这里有个细节如果你希望拖进来一个文件后控件自动跳到文件末尾或某个特定位置可以在LoadFile里增加一个可选参数比如nStartOffset这样后续扩展能更灵活。6.2 修改后自动更新 CRC/校验和很多十六进制文件在尾部附带 CRC 或校验和。你用 hexedit 修改了中间某个字节但忘了更新 CRC整个文件就不合法了。所以我做了一个自动校验功能当控件发出修改消息时实时计算整个缓冲区的 CRC32并显示在状态栏。下面是计算 CRC32 的简版实现DWORD CHexEditDlg::CalcCRC32(const BYTE* pData, size_t nLen) { DWORD crc 0xFFFFFFFF; for (size_t i 0; i nLen; i) { crc ^ pData[i]; for (int bit 0; bit 8; bit) { if (crc 1) crc (crc 1) ^ 0xEDB88320; else crc 1; } } return ~crc; }然后在OnHexEditUpdate消息处理里调用它LRESULT CHexEditDlg::OnHexEditUpdate(WPARAM wParam, LPARAM lParam) { int nLen m_wndHex.GetDataLength(); std::vectorBYTE tmp(nLen); m_wndHex.GetData(tmp.data(), nLen); DWORD crc CalcCRC32(tmp.data(), nLen); CString strInfo; strInfo.Format(_T(数据长度: %d, CRC32: %08X), nLen, crc); m_statusBar.SetWindowText(strInfo); return 0; }注意不要在每次按键时都计算整个文件否则文件一大就很卡。我通常的做法是加一个 300 毫秒的定时器修改消息发出后重置定时器定时时间到了才算一次 CRC。这样既能保持实时性又不会影响编辑体验。这个技巧很实用但也容易把校验值算错。有一次我改完了配置区软件依然报校验失败排查了半天才发现自己用的是 CRC16而固件实际要求 CRC32。从那以后我每次集成 hexedit 控件都会先确认目标文件用的校验算法是 CRC8、CRC16 还是 CRC32再写对应的函数。再后来我干脆在控件上做了一个右键菜单直接读取文件尾部两个字节作为期望值和当前计算值对比这样一眼就能看出修改有没有同步。希望这个习惯能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑