PyQt+QML 实现解析ANSI Color Code的日志显示器源码分享

发布时间:2026/10/25 1:42:54

PyQt+QML 实现解析ANSI Color Code的日志显示器源码分享 PyQtQML 实现解析ANSI Color Code的日志显示器源码分享一、效果展示二、源码分享1、工程结构2、库安装3、main.py4、main.qml三、实现原理1、整体架构2、ANSI 转义序列解析3、HTML 渲染与转义4、换行与滚动处理5、数据流小结一、效果展示二、源码分享1、工程结构2、库安装3、main.pyimportsysimportosfromPyQt6.QtCoreimportQUrlfromPyQt6.QtGuiimportQGuiApplicationfromPyQt6.QtQmlimportQQmlApplicationEnginedefmain():appQGuiApplication(sys.argv)engineQQmlApplicationEngine()qml_pathos.path.join(os.path.dirname(os.path.abspath(__file__)),main.qml)engine.load(QUrl.fromLocalFile(qml_path))ifnotengine.rootObjects():print(错误无法加载 QML 文件,filesys.stderr)sys.exit(-1)sys.exit(app.exec())if__name____main__:main()4、main.qmlimportQtQuickimportQtQuick.WindowimportQtQuick.Controls ApplicationWindow{id:root visible:true width:900height:600minimumWidth:360minimumHeight:240title:qsTr(日志显示器)color:#1e1e1epropertyvar logSource:[\u001B[0m\u001B[32m01-01 10:51:53 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[31m01-01 10:51:54 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[33m01-01 10:51:55 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[34m01-01 10:51:56 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[35m01-01 10:51:57 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[36m01-01 10:51:58 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[91m01-01 10:51:59 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[92m01-01 10:52:00 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[32m01-01 10:51:53 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[31m01-01 10:51:54 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[33m01-01 10:51:55 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[34m01-01 10:51:56 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[35m01-01 10:51:57 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[36m01-01 10:51:58 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[91m01-01 10:51:59 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[92m01-01 10:52:00 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[32m01-01 10:51:53 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[31m01-01 10:51:54 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[33m01-01 10:51:55 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[34m01-01 10:51:56 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[35m01-01 10:51:57 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[36m01-01 10:51:58 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[91m01-01 10:51:59 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[92m01-01 10:52:00 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[32m01-01 10:51:53 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[31m01-01 10:51:54 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[33m01-01 10:51:55 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[34m01-01 10:51:56 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[35m01-01 10:51:57 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[36m01-01 10:51:58 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[91m01-01 10:51:59 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[92m01-01 10:52:00 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[32m01-01 10:51:53 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[31m01-01 10:51:54 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[33m01-01 10:51:55 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[34m01-01 10:51:56 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[35m01-01 10:51:57 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[36m01-01 10:51:58 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[91m01-01 10:51:59 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m,\u001B[0m\u001B[92m01-01 10:52:00 I/mavlink_comm: mavlink active!\r\r\n\u001B[0m]propertyintcolorIndex:0propertyvar colorCodes:[32,31,33,34,35,36,91,92]//----------ANSI 解析直接写在 QML 里不再需要 ansi.js----------function ansiToHtml(text){//颜色表普通/粗体升级/高亮90-97 var NORMAL{30:#2e3436,31:#cc0000,32:#4e9a06,33:#c4a000,34:#3465a4,35:#75507b,36:#06989a,37:#d3d7cf};var BRIGHT{30:#555753,31:#ef2929,32:#8ae234,33:#fce94f,34:#729fcf,35:#ad7fa8,36:#34e2e2,37:#eeeeec};var BRIGHT_HIGH{90:#555753,91:#ef2929,92:#8ae234,93:#fce94f,94:#729fcf,95:#ad7fa8,96:#34e2e2,97:#eeeeec};function resolveColor(code,bold){if(code90code97)returnBRIGHT_HIGH[code];if(code30code37)returnbold ? BRIGHT[code]:NORMAL[code];returnnull;}function escapeHtml(s){returns.replace(//g,amp;).replace(//g,lt;).replace(//g,gt;);}function wrapSegment(t,color,bold,underline){var sescapeHtml(t);if(bold)sbs/b;if(underline)sus/u;if(color)sfont colorcolors/font;returns;}//同时兼容真正的 ESC 字节(0x1B)和字面字符串\x1Bvar re/(?:\x1b|\\x1[bB])\[([\d;]*)m/g;var parts[];var colorCodenull,colornull,boldfalse,underlinefalse;var last0,m;while((mre.exec(text))!null){if(m.indexlast){parts.push(wrapSegment(text.substring(last,m.index),color,bold,underline));}m[1].split(;).filter(function(s){returns.length0;}).map(function(s){returnparseInt(s,10);}).forEach(function(c){if(c0){colorCodenull;colornull;boldfalse;underlinefalse;}elseif(c1){boldtrue;if(colorCode!null)colorresolveColor(colorCode,bold);}elseif(c4){underlinetrue;}elseif((c30c37)||(c90c97)){colorCodec;colorresolveColor(c,bold);}});lastre.lastIndex;}if(lasttext.length){parts.push(wrapSegment(text.substring(last),color,bold,underline));}returnparts.join().replace(/\r\n?/g,br);}header:ToolBar{Row{anchors.fill:parent anchors.margins:6spacing:8Label{anchors.verticalCenter:parent.verticalCenter text:qsTr(log.txt · %1 行).arg(logSource.length)font.bold:true color:#e6e6e6}Item{width:8;height:8}Button{text:qsTr(添加日志)flat:true palette.buttonText:#9cdcfeonClicked:{var codecolorCodes[colorIndex%colorCodes.length]var nownew Date()var ts(0(now.getMonth()1)).slice(-2)-(0now.getDate()).slice(-2) (0now.getHours()).slice(-2):(0now.getMinutes()).slice(-2):(0now.getSeconds()).slice(-2)var newLine[0m[codemts I/test: new log entry\r\r\n[0mlogSourcelogSource.concat([newLine])colorIndexlogView.positionViewAtEnd()}}Button{text:qsTr(滚动到底部)flat:true palette.buttonText:#9cdcfeonClicked:logView.positionViewAtEnd()}}}ScrollView{id:scroll anchors.fill:parent anchors.margins:8contentWidth:availableWidth clip:true ScrollBar.vertical.policy:ScrollBar.AsNeeded ScrollBar.horizontal.policy:ScrollBar.AlwaysOff ListView{id:logView width:scroll.availableWidth implicitHeight:contentHeight model:logSource spacing:2Timer{id:scrollTimer interval:30onTriggered:logView.positionViewAtEnd()}delegate:Text{requiredpropertystring modelData width:logView.width textFormat:Text.RichText text:root.ansiToHtml(modelData.replace(/[\r\n]/g,))color:#d4d4d4font.family:Consolas, Courier New, monospacefont.pointSize:10wrapMode:Text.Wrap}}}}三、实现原理1、整体架构整个日志显示器采用PyQt6 QML的前后端分离架构main.py负责创建 Qt 应用与加载 QML 界面main.qml负责界面渲染与 ANSI 颜色解析。二者通过QQmlApplicationEngine桥接QML 中无需任何 C 或 Python 逻辑即可独立完成日志的展示与着色。2、ANSI 转义序列解析ANSI 颜色码以ESC[开头、以m结尾例如\u001B[32m表示将后续文本设为绿色。ansiToHtml()函数通过正则表达式/(?:\x1b|\\x1[bB])\[([\d;]*)m/g同时匹配真正的 ESC 字节0x1B与字面字符串\x1B从而兼容不同来源的日志文本。解析时按;拆分参数并逐项处理0重置所有样式颜色、粗体、下划线全部清空1开启粗体并基于当前颜色码重新解析出亮色4开启下划线30~37/90~97设置前景色其中90~97为高亮色。颜色解析通过resolveColor()完成普通色30~37在粗体时自动升级为亮色表BRIGHT高亮色90~97直接查BRIGHT_HIGH表最终映射为十六进制颜色值。3、HTML 渲染与转义QML 的Text控件开启textFormat: Text.RichText后即可渲染 HTML。为避免日志中的、、等字符破坏 HTML 结构escapeHtml()会先将其转义为lt;、gt;、amp;再包裹font、b、u标签实现着色与样式。4、换行与滚动处理日志原始文本中的\r\r\n会被ansiToHtml()统一替换为br换行标签ListView的 delegate 中通过modelData.replace(/[\r\n]/g, )先剔除多余换行避免重复空行。点击「添加日志」或「滚动到底部」时调用positionViewAtEnd()并配合 30ms 的Timer确保新日志稳定滚动到可视区域底部。5、数据流小结main.py 加载 main.qmllogSource 日志数组ListView delegate 逐行取文本ansiToHtml 解析 ANSI 码转义 生成 HTML 片段Text.RichText 渲染彩色日志整体流程为日志数组 → 逐行委托 → ANSI 解析 → HTML 转义 → 富文本渲染最终在界面上呈现带颜色的日志输出。
延伸阅读

更多相关文章

2026/10/25 1:42:53

医药企业环保合规运营路径:环评→排污许可→危废合规处置

——从环评到合规处置的全流程合规自查检查组来了电话是周一上午十点打来的。"明天上午到现场,请准备好环评批复、排污许可证、危废管理台账、转运处置,还有应急预案演练记录。"放下电话,EHS总监的手心微微出汗。不是因为心虚——企…

2026/10/25 2:27:58

VDA 6.3-2023第四版红皮书:过程审核新标准与现场落地指南

简介:VDA 6.3-2023红皮中文版(扫描)过程审核第四版是德国汽车工业协会(VDA)发布的过程审核标准文件,面向汽车制造商、供应商及质量管理体系审核人员,适用于产品诞生过程与批量生产阶段的内部审核…

2026/10/25 2:27:58

Fluent高超声速仿真:从算例路线到等离子体鞘套的完整实践与排错

简介:CFD-Fluent算例仿真手册2021-R1是一份以Ansys Fluent为载体的仿真算例手册,重点面向高超声速飞行器气动设计与等离子体鞘套效应分析场景,适合航空航天、兵器、电子通信等领域工程师以及需要进阶CFD仿真的学习者。压缩包内包含1个PDF文件…

2026/10/25 2:27:58

Tiptop与Easyflow集成难点:从SOAP配置到XML签核的完整排查指南

简介:面向ERP实施顾问与Tiptop二次开发人员的集成参考资料,以新单据接入为例,讲解Tiptop与Easyflow(OA签核)之间如何通过XML实现单据签核的双向联动。文档从环境准备、数据表7个签核字段检查、Schema更新、单据性质与抛…

2026/10/25 2:22:58

使用srvany.exe将普通程序转为Windows服务的完整指南

1. 为什么需要srvany.exe创建Windows服务在Windows系统中,许多后台程序需要以服务形式运行才能实现开机自启、崩溃重启等关键功能。但并非所有程序都原生支持服务化运行,这时候srvany.exe就派上了用场。srvany是Windows Server Resource Kit工具包中的一…

2026/10/25 2:22:58

Mac软件卸载全指南:从基础到深度清理

1. Mac软件卸载的常见误区与正确认知作为一位使用Mac超过十年的老用户,我见过太多人用错误的方式卸载软件。最常见的就是直接把应用图标拖到废纸篓——这种方式看似简单,但实际上只删除了主程序文件,留下了大量系统垃圾。macOS的应用程序采用…

2026/10/25 0:02:22

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/25 0:02:23

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/25 0:02:23

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/25 0:02:22

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/25 0:02:23

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/25 0:02:23

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑