玩转Qt(2)-自制简易好看的日志系统
本文于
1071
天之前发表,文中内容可能已经过时。
简介
一个完善的软件工程,自然是少不了log系统的。
这次涛哥教大家,用最少的代码做一个轻量又好看的log系统。
涛哥知道有现成的log4cpp、log4cplus之类的,也有使用过。
这次是抱着学习的心态来造这个轮子的,造轮子的过程才能学到
更多知识,才能有进步、有提升,难道不是么?
预览
先看一下成果
原理
html格式的log
为了实现 “代码最少” 和 “好看” 的需求,涛哥把log写进了一个html文件。
这样的log相当于一个静态的网页,只要装有浏览器的操作系统,都可以打开并看到上面图示那样的log。
涛哥给这个html文件设计了一个固定的模板的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
| <?xml version="1.0" encoding="utf-8" standalone="yes"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html>
<head> <title>TaoLogger</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <style type="text/css" id="logCss"> body { background: #18242b; color: #afc6d1; margin-right: 20px; margin-left: 20px; font-size: 14px; font-family: Arial, sans-serif, sans; }
a { text-decoration: none; }
a:link { color: #a0b2bb; }
a:active { color: #f59504; }
a:visited { color: #adc7d4; }
a:hover { color: #e49115; }
h1 { text-align: center; }
h2 { color: #ebe5e5; }
.d, .w, .c, .f, .i { padding: 3px; overflow: auto; }
.d { background-color: #0f1011; color: #a8c1ce; }
.i { background-color: #294453; color: #a8c1ce; }
.w { background-color: #7993a0; color: #1b2329; }
.c { background-color: #ff952b; color: #1d2930; }
.f { background-color: #fc0808; color: #19242b; } </style> </head>
<body> <h1><a href="https://jaredtao.github.io">TaoLogger</a> 日志文件</h1> <script type="text/JavaScript"> function objHide(obj) { obj.style.display="none" } function objShow(obj) { obj.style.display="block" } function selectType() { var sel = document.getElementById("typeSelect"); const hideList = new Set(['d', 'i', 'w', 'c', 'f']); if (sel.value === 'a') { hideList.forEach(element => { var list = document.querySelectorAll('.' + element); list.forEach(objShow); }); } else { var ss = hideList; ss.delete(sel.value); ss.forEach(element => { var list = document.querySelectorAll('.' + element); list.forEach(objHide); }); var showList = document.querySelectorAll('.' + sel.value); showList.forEach(objShow); } } </script> <select id="typeSelect" onchange="selectType()"> <option value='a' selected="selected">All</option> <option value='d'>Debug</option> <option value='i'>Info</option> <option value='w'>Warning</option> <option value='c'>Critical</option> <option value='f'>Fatal</option> </select>
|
(如果你不懂html,也没关系,直接拿过去用就好了)
这个模板只使用了一些很基本的html元素和css样式表,筛选器那里用了一点JavaScript。
很简单的,模板作为html文件的前面部分,接下来每一行log,以追加的方式跟在模板后面就行了。
(html的body结束标记并没有写,浏览器都能正常打开。容错性真的强!)
当然, 每一条log有个格式要求:
1
| <div class="d"> 山有木兮木有枝,心悦君兮君不知。</div>
|
就是增加了一对div标记, div的class属性要设置为d、i、w、c、f这几个字符中的一个,分别是
debug、info、warning、critical、fatal的首字母, 这正是Qt所提供的log分类。
设置div的class属性,就是给筛选器用来做筛选。
文件读取? 不,太慢了。
这就是一段固定的字符串,直接编译进代码里,程序启动的时候直接装载到内存就好了。
那么C++里面,怎么才能装下这段带有转义字符的字符串呢?涛哥的答案是:C++11的 “原始字符串字面量”或者叫 “R字符串”
可以参考这里 cppreference
简单来说,是这样写的:
1
| string logTemplate = R"(xxxxxx)";
|
只要有了 R”( )” 这个写法,括号中间随便写转义字符、换行符都行。当然为了方便让编译器识别哪个
才是真正的’结束括号’,C++11标准提出了括号前后增加分隔符的写法,即:
1
| string logTemplate = R"prefix(xxxxxx)prefix";
|
左括号的前面和右括号的后面, 是同样的一段字符串作为分隔符就行了。
涛哥的代码里是这么用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| namespace Logger { const static QString logTemplate = u8R"logTemplate( <?xml version="1.0" encoding="utf-8" standalone="yes"?> <!DOCTYPE html PUBLIC "- <html>
<head> <title>TaoLogger</title> ... 这里省略一大堆html代码 ...
)logTemplate";
}
|
Qt的log系统
Qt的打印信息,大家普遍使用的是qDebug,不过Qt除了qDebug,还有qInfo, qWarning, qCritical等等。
涛哥翻了Qt5.12的源码,发现这几个打印最终都是通过fprintf(stderr)或者fprintf(stdout)来实现输出的,
不同的地方就在于Log类型。如果要用好这个分类,那我们平时使用打印的时候,就要注意做区分:
- 调试信息用qDebug
- 常规信息用qInfo
- 警告用qWarning
- 比较严重的问题用qCritical
Qt提供了一个函数qSetMessagePattern,用来定制输出信息。
例如:
1
| qSetMessagePattern("[%{time yyyyMMdd h:mm:ss.zzz t} %{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}] %{file}:%{line} - %{message}");
|
一般只要在main.cpp中添加这一行代码,之后的qDebug、qInfo等函数都会按照这个格式来输出,包含了
时间戳、log类型、文件名、行号 等信息。也可以不改任何代码、改环境变量来做到
这里有个问题,就是文件名和行号在debug模式正常,Release模式会变成空的。
要解决这个问题,那么就需要编译器提供的内置宏__FILE__
和 __LINE__
了
涛哥写了这样几个宏,代替qDebug和qInfo等函数。
1 2 3 4
| #define LOG_DEBUG qDebug() << __FILE__ << __FUNCTION__ << __LINE__ #define LOG_INFO qInfo() << __FILE__ << __FUNCTION__ << __LINE__ #define LOG_WARN qWarning() << __FILE__ << __FUNCTION__ << __LINE__ #define LOG_CRIT qCritical() << __FILE__ << __FUNCTION__ << __LINE__
|
用法类似这样:
1
| LOG_DEBUG << u8"山有木兮木有枝,心悦君兮君不知。";
|
Qt还提供了一个函数 qInstallMessageHandler,可以插入一个回调函数,让每一行qDebug/qInfo等
函数的打印信息,都经过这个回调来处理。看一下帮助文档:
其实帮助文档已经提供了一个简易的log功能,涛哥就是在这个功能的基础上,做了一些定制化的修改。
融合
涛哥写了一个函数和一组静态变量,用来设置和记录log存储的路径和容量
头文件中的声明
1 2 3 4 5 6 7 8 9
| #pragma once #include <QDebug>
namespace Logger {
void initLog(const QString& logPath = QStringLiteral("Log"), int logMaxCount = 1024);
}
|
CPP中的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| namespace Logger {
static QString gLogDir;
static int gLogMaxCount;
void initLog(const QString &logPath, int logMaxCount) { qInstallMessageHandler(outputMessage); gLogDir = QCoreApplication::applicationDirPath() + "/" + logPath; gLogMaxCount = logMaxCount; QDir dir(gLogDir); if (!dir.exists()) { dir.mkpath(dir.absolutePath()); } QStringList infoList = dir.entryList(QDir::Files, QDir::Name); while (infoList.size() > gLogMaxCount) { dir.remove(infoList.first()); infoList.removeFirst(); } } static void outputMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg) { } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| static void outputMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg) { static const QString messageTemp= QString("<div class=\"%1\">%2</div>\r\n"); static const char typeList[] = {'d', 'w', 'c', 'f', 'i'}; static QMutex mutex; QDateTime dt = QDateTime::currentDateTime();
QString fileNameDt = dt.toString("yyyy-MM-dd_hh");
QString contentDt = dt.toString("yyyy-MM-dd hh:mm:ss"); QString message = QString("%1 %2").arg(contentDt).arg(msg); QString htmlMessage = messageTemp.arg(typeList[static_cast<int>(type)]).arg(message);
QFile file(QString("%1/%2_log.html").arg(gLogDir).arg(fileNameDt)); mutex.lock(); bool exist = file.exists(); file.open(QIODevice::WriteOnly | QIODevice::Append); QTextStream text_stream(&file); text_stream.setCodec("UTF-8"); if (!exist) { text_stream << logTemplate << "\r\n"; } text_stream << htmlMessage;
file.close(); mutex.unlock(); ::OutputDebugString(message.toStdWString().data()); ::OutputDebugString(L"\r\n"); }
|
github仓库链接
源代码代码去github吧。
TaoLogger