Qt/QML音视频文件原始十六进制查看器
前言在做音视频工具时很多问题只看 FFmpeg 解析后的字段并不够。比如MP4 的ftyp、moov、mdat到底在文件哪个位置WAV/AVI 的RIFF、fmt、data块大小是否正确某段元数据、魔数或 ASCII 字符串是否真的存在于原始文件里文件损坏时容器结构是否还能被粗略定位。AudioTools 里的RawHexPage.qml就是为这类场景做的原始字节查看页。它不是简单把整个文件读成字符串而是使用一个 C 自绘 QML 控件HexViewerItem按可见区域读取文件支持十六进制/ASCII 双列显示、搜索、复制、跳转和基础容器结构标记。本文基于当前实现拆解这个页面从 QML 到 C 的设计。效果图一、功能概览RawHexPage.qml提供三个核心区域顶部控制面板显示当前文件、文件大小、当前 Offset、选区、容器结构、搜索结果。中间十六进制查看器使用HexViewer自绘控件展示 Offset / Hex / ASCII 三列内容。右侧结构标记栏展示识别出的 RIFF 或 ISO BMFF 结构块点击可定位到对应字节范围。页面功能包括功能说明每行字节数切换支持 8、16、24、32 字节/行Offset 跳转支持输入0000:0020、0x20、20等形式文件头/文件尾定位快速跳到开头或结尾鼠标选区在 Hex 或 ASCII 区拖拽选中字节复制 Hex/ASCII复制选区内容到剪贴板Hex/ASCII 搜索支持十六进制序列或 ASCII 文本搜索搜索结果导航上一条/下一条循环定位容器结构识别当前支持 RIFF 系列和 ISO BMFF如 WAV、AVI、MP4、MOV、M4A二、QML 页面如何组织页面入口是qml/pages/inspect/RawHexPage.qml。它本身不直接读取文件而是把当前导入文件路径绑定给 C 控件HexViewer { id: hexView anchors.fill: parent anchors.rightMargin: 14 filePath: mediaAnalyzer.currentFile bytesPerRow: bytesPerRowBox.model[bytesPerRowBox.currentIndex] }这里HexViewer是 C 注册到 QML 的类型qmlRegisterTypeHexViewerItem(AudioTools,1,0,HexViewer);页面只负责 UI 编排和调用控件暴露出来的Q_INVOKABLE方法。例如跳转 Offsetfunction performJump() { actionMessage hexView.jumpToHexOffset(offsetInput.text) ? 已跳转 : Offset 无效 }搜索也是同样的模式function performSearch() { var mode searchModeBox.currentIndex 0 ? hex : ascii actionMessage hexView.runSearch(searchInput.text, mode) ? 已定位到首个命中 : 未命中或搜索内容无效 }这种设计让 QML 保持很薄它不关心文件读取、搜索算法、容器解析和绘制细节只绑定属性、展示状态并响应按钮点击。三、为什么用 C 自绘控件十六进制查看器如果直接用 QMLRepeater或TextArea拼完整文本会很快遇到性能问题大文件不能一次性读入内存每个字节都变成 QML delegate 会产生大量对象搜索、选区、结构高亮都需要精确到字节Offset / Hex / ASCII 三列需要稳定对齐。因此HexViewerItem继承自QQuickPaintedItem自己控制绘制classHexViewerItem:publicQQuickPaintedItem{Q_OBJECTQ_PROPERTY(QString filePath READ filePath WRITE setFilePath NOTIFY fileChanged)Q_PROPERTY(qint64 fileSize READ fileSize NOTIFY fileChanged)Q_PROPERTY(intbytesPerRow READ bytesPerRow WRITE setBytesPerRow NOTIFY layoutChanged)Q_PROPERTY(qint64 activeOffset READ activeOffset NOTIFY cursorChanged)Q_PROPERTY(qint64 selectionLength READ selectionLength NOTIFY selectionChanged)Q_PROPERTY(qreal scrollRatio READ scrollRatio WRITE setScrollRatio NOTIFY viewChanged)Q_PROPERTY(QString searchSummary READ searchSummary NOTIFY searchChanged)Q_PROPERTY(QString containerName READ containerName NOTIFY structureChanged)Q_PROPERTY(QVariantList structureMarkers READ structureMarkers NOTIFY structureChanged)...};这些属性一方面供 QML 显示状态另一方面让 QML 的滚动条、按钮启用状态和结构侧栏可以直接绑定。四、大文件浏览的关键只读可见区域实现十六进制查看器时最重要的一点是不要把整个文件转成字符串。当前实现只缓存可见行附近的数据voidHexViewerItem::updateVisibleCache(intvisibleRows){if(!m_file.isOpen()||m_fileSize0)return;// 大文件浏览的关键是虚拟化读取只加载可见行附近的数据不把全文件转成字符串。constqint64 readOffsetm_firstVisibleRow*m_bytesPerRow;constqint64 requestedLengthstatic_castqint64(visibleRows4)*m_bytesPerRow;constintreadLengthstatic_castint(std::minqint64(requestedLength,m_fileSize-readOffset));if(readOffsetm_visibleDataOffsetreadLengthm_visibleDataLength)return;if(!m_file.seek(readOffset)){m_visibleData.clear();m_visibleDataOffset-1;m_visibleDataLength0;return;}m_visibleDatam_file.read(readLength);m_visibleDataOffsetreadOffset;m_visibleDataLengthm_visibleData.size();}这里有几个设计点m_firstVisibleRow决定当前屏幕顶部是哪一行每行字节数由m_bytesPerRow决定只读取visibleRows 4行给滚动留一点缓冲如果当前缓存范围没变就不重复读取。这使得查看几百 MB 甚至 GB 级音视频文件时内存占用仍然稳定。五、绘制 Offset / Hex / ASCII 三列控件使用等宽字体保证三列稳定对齐QFontviewerFont(){// 十六进制查看器必须使用等宽字体才能保证 Offset / Hex / ASCII 三列稳定对齐。QFontfont(QStringLiteral(Consolas));font.setStyleHint(QFont::Monospace);font.setFixedPitch(true);font.setPixelSize(14);returnfont;}布局数据集中在LayoutInfo中structLayoutInfo{qreal offsetLeft12.0;qreal offsetWidth96.0;qreal hexLeft120.0;qreal hexCellWidth30.0;qreal asciiLeft620.0;qreal asciiCellWidth10.0;qreal contentTop38.0;intvisibleRows1;};绘制主循环按可见行和每行字节数遍历for(intvisualRow0;visualRowlayout.visibleRows;visualRow){constqint64 rowm_firstVisibleRowvisualRow;constqint64 rowOffsetrow*m_bytesPerRow;if(rowOffsetm_fileSize)break;painter-drawText(QRectF(layout.offsetLeft,y,layout.offsetWidth,layout.rowHeight),Qt::AlignLeft|Qt::AlignVCenter,formatOffset(rowOffset));for(intcolumn0;columnm_bytesPerRow;column){constqint64 offsetrowOffsetcolumn;if(offsetm_fileSize)break;constintdataIndexstatic_castint(offset-m_visibleDataOffset);constunsignedcharvaluestatic_castunsignedchar(m_visibleData.at(dataIndex));constQRectF hexRecthexByteRect(layout,visualRow,column);constQRectF asciiRectasciiByteRect(layout,visualRow,column);painter-drawText(hexRect,Qt::AlignCenter,byteToHex(value));painter-drawText(asciiRect,Qt::AlignCenter,isPrintableAscii(value)?QString(QChar(QLatin1Char(value))):QStringLiteral(.));}}不可打印 ASCII 字节显示为.这是常见十六进制查看器的习惯。六、滚动条如何和文件 Offset 对齐QML 侧用一个ScrollBar绑定 C 控件的滚动比例ScrollBar { id: verticalBar orientation: Qt.Vertical policy: ScrollBar.AlwaysOn size: Math.min(1, Math.max(0.02, hexView.pageRatio)) Binding { target: verticalBar property: position value: hexView.scrollRatio * verticalBar.travelRange when: !verticalBar.pressed } onPositionChanged: { if (pressed) hexView.scrollRatio position / travelRange } }C 侧则把比例映射到首行行号voidHexViewerItem::setScrollRatio(qreal ratio){constqint64 maxRowmaxFirstVisibleRow();if(maxRow0){setFirstVisibleRow(0,true);return;}constqreal safeRatioclampValueqreal(ratio,0.0,1.0);setFirstVisibleRow(static_castqint64(std::round(safeRatio*maxRow)),true);}这样 UI 滚动条不需要知道文件大小、每行字节数和可见行数量只处理 0 到 1 的比例。七、Offset 跳转和选区页面提供 Offset 输入框TextField { id: offsetInput placeholderText: Offset例如 0000:0020 onAccepted: page.performJump() }C 控件暴露Q_INVOKABLEbooljumpToHexOffset(constQStringtext);Q_INVOKABLEvoidscrollToOffset(qint64 offset);Q_INVOKABLE QStringactiveOffsetText()const;Q_INVOKABLE QStringselectionRangeText()const;鼠标点击和拖拽通过坐标反算文件 Offsetqint64HexViewerItem::offsetAtPosition(constQPointFposition,boolclampToVisible)const{constLayoutInfo layoutlayoutInfo();intvisualRowstatic_castint(std::floor((position.y()-layout.contentTop)/layout.rowHeight));...constqint64 offset(m_firstVisibleRowvisualRow)*m_bytesPerRowcolumn;returnoffset;}拖拽时clampToVisible可以让鼠标横向稍微移出列区域时仍然吸附到最近字节列避免选区中断。选区复制通过重新打开文件读取选中字节QByteArrayHexViewerItem::selectedBytes(intmaxBytes,bool*truncated)const{if(!hasSelection()||m_filePath.isEmpty())returnQByteArray();QFilefile(m_filePath);if(!file.open(QIODevice::ReadOnly))returnQByteArray();constqint64 lengthselectionLength();constqint64 readLengthstd::minqint64(length,maxBytes);if(!file.seek(m_selectionStart))returnQByteArray();returnfile.read(readLength);}这里限制最大复制大小为 1MB避免用户误选超大范围后把大量文本塞进剪贴板。八、Hex/ASCII 搜索搜索入口在 QMLToolComboBox { id: searchModeBox model: [Hex, ASCII] } TextField { id: searchInput placeholderText: searchModeBox.currentIndex 0 ? 输入十六进制序列例如 52 49 46 46 : 输入 ASCII 文本例如 RIFF onAccepted: page.performSearch() }C 中先把用户输入转换成搜索字节序列QByteArrayHexViewerItem::parseSearchNeedle(constQStringpattern,constQStringmode)const{constQString trimmedpattern.trimmed();if(trimmed.isEmpty())returnQByteArray();constQString normalizedModemode.trimmed().toLower();if(normalizedModeQStringLiteral(ascii))returntrimmed.toUtf8();QString cleanedtrimmed;cleaned.remove(QRegularExpression(QStringLiteral([^0-9A-Fa-f])));if(cleaned.size()%2!0)returnQByteArray();returnQByteArray::fromHex(cleaned.toLatin1());}搜索采用分块读取避免一次性把全文件放进内存constexprqint64 kSearchChunkSize2*1024*1024;constexprintkMaxSearchMatches4096;QByteArray overlap;qint64 chunkBaseOffset0;while(!file.atEnd()m_searchMatches.size()kMaxSearchMatches){constQByteArray chunkfile.read(kSearchChunkSize);QByteArray windowoverlapchunk;constqint64 windowBaseOffsetchunkBaseOffset-overlap.size();intsearchFrom0;while(m_searchMatches.size()kMaxSearchMatches){constintindexwindow.indexOf(needle,searchFrom);if(index0)break;SearchMatch match;match.offsetwindowBaseOffsetindex;match.lengthneedle.size();m_searchMatches.append(match);searchFromindex1;}overlapneedle.size()1?window.right(needle.size()-1):QByteArray();chunkBaseOffsetchunk.size();}overlap是关键如果搜索目标刚好跨越两个 2MB 分块边界没有 overlap 就会漏匹配。九、容器结构标记Raw Hex 页面不仅显示字节还会做轻量容器结构分析。当前支持RIFF/RF64 系列例如 WAV、AVIISO BMFF 系列例如 MP4、MOV、M4A。入口函数voidHexViewerItem::performStructureAnalysis(){clearStructureAnalysis();QFilefile(m_filePath);if(!file.open(QIODevice::ReadOnly)){emitstructureChanged();return;}boolparsedparseRiffStructure(file);if(!parsed){file.seek(0);parsedparseIsoBmffStructure(file);}if(!parsed)m_containerNameQStringLiteral(未识别结构);rebuildStructureMarkerData();updateActiveStructureIndex();emitstructureChanged();}RIFF 结构解析RIFF chunk 是小端长度constQString chunkNamesafeAsciiLabel(header.left(4));constquint32 chunkPayloadSizereadLe32(header,4);constqint64 totalChunkSizestd::maxqint64(8,8static_castqint64(chunkPayloadSize)(chunkPayloadSize%2));LIST和RIFF可以包含子 chunk所以递归解析if((chunkNameQStringLiteral(LIST)||chunkNameQStringLiteral(RIFF))safeChunkSize12){parseRiffChunks(file,offset12,offsetsafeChunkSize,level1,depth1);}ISO BMFF 结构解析MP4/MOV 的 box 使用大端长度quint64 boxSizereadBe32(header,0);constQString typesafeAsciiLabel(header.mid(4,4));intheaderSize8;if(boxSize1){headerfile.read(8);boxSizereadBe64(header,8);headerSize16;}elseif(boxSize0){boxSizeend-offset;}容器类 box 会继续递归解析if(isIsoContainerBox(type)safeBoxSizeheaderSize){qint64 childStartoffsetheaderSize;if(typeQStringLiteral(meta))childStart4;parseIsoBoxes(file,childStart,offsetsafeBoxSize,level1,depth1);}结构标记最终转成QVariantList给 QML 侧ListView展示QVariantMap map;map.insert(QStringLiteral(name),marker.name);map.insert(QStringLiteral(label),marker.label);map.insert(QStringLiteral(offset),marker.offset);map.insert(QStringLiteral(offsetText),formatOffset(marker.offset));map.insert(QStringLiteral(size),marker.size);map.insert(QStringLiteral(level),marker.level);map.insert(QStringLiteral(color),marker.color);m_structureMarkerData.append(map);右侧结构栏点击后调用hexView.activateStructureMarker(index)C 会跳转并选中该结构范围voidHexViewerItem::activateStructureMarker(intindex){constStructureMarkermarkerm_structureMarkers.at(index);constqint64 endOffsetmarker.offsetstd::maxqint64(0,marker.size-1);setActiveOffset(marker.offset);setSelection(marker.offset,endOffset);ensureOffsetVisible(marker.offset);}十、页面状态如何绑定顶部状态栏直接绑定HexViewer暴露的属性Text { text: 文件大小 page.formatSize(hexView.fileSize) 当前 Offset (hexView.activeOffset 0 ? hexView.activeOffsetText() : -) 选区 (hexView.selectionLength 0 ? hexView.selectionRangeText() : -) } Text { text: 容器结构 (hexView.containerName.length 0 ? hexView.containerName : -) 当前结构 hexView.activeStructureLabel 搜索结果 hexView.searchSummary }按钮启用状态也基于属性绑定ActionButton { text: 复制 Hex enabled: hexView.selectionLength 0 onClicked: { page.actionMessage hexView.copySelectedHexToClipboard() ? 已复制 Hex : 复制失败 } }这就是 QML C 控件比较舒服的地方重逻辑留在 C状态以属性暴露QML 只做组合和反馈。十一、实现取舍当前实现有几个明确取舍取舍原因C 自绘而不是 QML delegate避免大量 QML 对象保证大文件浏览流畅只读可见区域防止大文件一次性读入内存搜索最多记录 4096 个命中避免高频字节模式造成大量结果复制选区最多 1MB防止误操作卡住剪贴板和 UI结构解析只支持 RIFF 和 ISO BMFF先覆盖音视频最常见容器保持逻辑可控结构解析有深度和数量限制防止异常文件导致递归过深或标记过多十二、小结RawHexPage.qml看起来是一个普通“十六进制查看页”但它的关键不在 UI 控件数量而在数据规模控制文件读取按可见区域虚拟化搜索按 2MB 分块扫描并处理跨块匹配绘制由 C 自绘控件完成结构分析只做轻量容器标记QML 只负责面板、按钮、状态和侧栏组合。这套实现适合音视频工具里的“原始数据检查”场景既能快速查看字节也能结合 RIFF/MP4 结构定位问题而不会因为打开大文件就把 UI 和内存拖垮。

相关新闻

010-伟大的解释者

010-伟大的解释者

费曼学习法系列 第010篇 伟大的解释者——费曼教学哲学的核心 导言:比诺贝尔奖更珍贵的称号 1965年,理查德费曼因量子电动力学的研究获得诺贝尔物理学奖。然而,在全世界无数学生和普通人的心中,他最珍贵的称号却不是"诺贝尔奖得主",而是"伟大的解释者&…

2026/7/4 4:23:20阅读更多 →
【安心陪诊 Agent】从 Web Demo 到 HAP 真机:安心陪诊 Agent 的工程落地路线

【安心陪诊 Agent】从 Web Demo 到 HAP 真机:安心陪诊 Agent 的工程落地路线

应用名称:安心陪诊 Agent 统一合集:安心陪诊 Agent|HarmonyOS 高校创新赛 关键词标签:harmonyos / AI Agent / 医疗陪诊从 Web Demo 到 HAP 真机:安心陪诊 Agent 的工程落地路线摘要:规划从当前 Web 原型到…

2026/7/4 4:23:20阅读更多 →
TRAE Work(工作版)vs Code(编程 / 代码版)完整区别

TRAE Work(工作版)vs Code(编程 / 代码版)完整区别

TRAE Work 是同一软件内一键切换的两种模式,共用账号、文件空间,底层大模型一致,仅任务调度、工具集、AI 优化方向完全不同。Work/Code切换入口 一、核心定位与适用人群 1. Work 工作版(通用办公模式) 面向非研发岗位&…

2026/7/4 4:23:20阅读更多 →
Selenium自动化测试性能优化:5个核心方法提升4倍执行速度

Selenium自动化测试性能优化:5个核心方法提升4倍执行速度

1. 项目概述:为什么你的Selenium脚本跑得慢? 如果你用过Selenium做UI自动化测试,大概率经历过这样的场景:满怀期待地运行脚本,结果浏览器启动慢吞吞,页面加载像蜗牛,元素定位要等半天&#xff0…

2026/7/4 6:03:26阅读更多 →
深入理解Vulkan-Zig的调度表与包装器:高级Vulkan API集成指南

深入理解Vulkan-Zig的调度表与包装器:高级Vulkan API集成指南

深入理解Vulkan-Zig的调度表与包装器:高级Vulkan API集成指南 【免费下载链接】vulkan-zig Vulkan binding generator for Zig 项目地址: https://gitcode.com/gh_mirrors/vu/vulkan-zig Vulkan-Zig是一个功能强大的Vulkan绑定生成器,它为Zig开发…

2026/7/4 6:03:26阅读更多 →
VisTR性能深度测评:ResNet50 vs ResNet101,哪个 backbone 更适合你的视频分割任务?

VisTR性能深度测评:ResNet50 vs ResNet101,哪个 backbone 更适合你的视频分割任务?

VisTR性能深度测评:ResNet50 vs ResNet101,哪个 backbone 更适合你的视频分割任务? 【免费下载链接】VisTR [CVPR2021 Oral] End-to-End Video Instance Segmentation with Transformers 项目地址: https://gitcode.com/gh_mirrors/vi/VisT…

2026/7/4 6:03:26阅读更多 →
Open-Source-Prompt-Library:新手必学的PRD创建模板完全教程

Open-Source-Prompt-Library:新手必学的PRD创建模板完全教程

Open-Source-Prompt-Library:新手必学的PRD创建模板完全教程 【免费下载链接】Open-Source-Prompt-Library User-Centered Product Development Prompt Templates 项目地址: https://gitcode.com/gh_mirrors/op/Open-Source-Prompt-Library Open-Source-Prom…

2026/7/4 6:03:26阅读更多 →
Xous加密服务实战:AES、TRNG和密钥管理的安全实现指南

Xous加密服务实战:AES、TRNG和密钥管理的安全实现指南

Xous加密服务实战:AES、TRNG和密钥管理的安全实现指南 【免费下载链接】xous-core The Xous microkernel 项目地址: https://gitcode.com/gh_mirrors/xo/xous-core Xous微内核系统提供了完整的企业级加密服务解决方案,包括AES加密、真随机数生成器…

2026/7/4 6:03:26阅读更多 →
LIII客户端开发指南:从源码编译到自定义功能的完整路线图

LIII客户端开发指南:从源码编译到自定义功能的完整路线图

LIII客户端开发指南:从源码编译到自定义功能的完整路线图 【免费下载链接】LIII multi-platform bittorrent client 项目地址: https://gitcode.com/gh_mirrors/li/LIII LIII是一款跨平台的BitTorrent客户端,本文将为开发者提供从源码编译到自定义…

2026/7/4 5:58:26阅读更多 →
AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

6个月前的2025年12月,Boris Cherny 公开宣布自己卸载了 IDE。一时间,Vibe Coding 成了全行业最热的话题。6个月后,当我们回过头来拉一份真实账本,发现事情远没有"一句话生成一个App"那么浪漫。本文从产品经理和研发两个…

2026/7/3 14:18:39阅读更多 →
审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

引言:审计结束三个月了,审计员的权限还没关某城商行每年按照监管要求开展至少一次数据安全审计。审计期间,内审部门需要抽样检查各类业务数据——交易流水、客户信息、员工操作日志、权限配置记录。这些数据分布在不同系统中,审计…

2026/7/3 14:38:35阅读更多 →
端到端自动驾驶:从GTC‘26看工程可信落地的核心逻辑

端到端自动驾驶:从GTC‘26看工程可信落地的核心逻辑

1. 项目概述:当算法工程师走进GTC26展厅,看到的不是芯片,而是“端到端”的呼吸节奏“端到端”这三个字,在GTC’26现场出现的频率,高得像NVLink带宽测试时的峰值曲线——它不再是一个论文里的技术路径选项,而…

2026/7/4 0:02:48阅读更多 →
缺牙修复科普:常见义齿类型与选择参考

缺牙修复科普:常见义齿类型与选择参考

缺牙修复科普:常见义齿类型与选择参考牙齿缺失是中老年人群中较为常见的口腔问题,不仅会造成咀嚼不便、进食受影响,长期还可能对营养摄入与日常社交带来困扰。义齿是改善缺牙问题的常用方式,目前市面上的义齿种类较多,…

2026/7/4 0:02:48阅读更多 →
STM32F091RC与LTC6904实现高精度方波信号生成

STM32F091RC与LTC6904实现高精度方波信号生成

1. 项目概述:LTC6904与STM32F091RC的精准方波生成方案在嵌入式系统开发中,精确的时钟信号和定时控制往往是项目成败的关键。LTC6904作为一款低功耗、高精度的可编程振荡器芯片,与STM32F091RC这款ARM Cortex-M0内核微控制器的组合,…

2026/7/4 0:02:48阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/4 1:16:56阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/4 2:33:55阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/4 2:33:55阅读更多 →