相对于第三方的日志库,在 Qt 中使用 QDebug 打印更便捷,有时候也需要对 QDebug 输出进行重定向,如写入文件等。
在 Qt4 中使用 qInstallMsgHandler 函数设置重定向的函数指针:
typedef void (*QtMsgHandler)(QtMsgType, const char *);
Q_CORE_EXPORT QT_DEPRECATED QtMsgHandler qInstallMsgHandler(QtMsgHandler);
在 Qt5 中应该使用 qInstallMessageHandler 来注册函数指针:
typedef void (*QtMessageHandler)(QtMsgType, const QMessageLogContext &, const QString &);
Q_CORE_EXPORT QtMessageHandler qInstallMessageHandler(QtMessageHandler);
返回的函数指针我们可以保存起来,需要输出到控制台时进行调用。
默认 Release 模式 QMessageLogContext 不含上下文信息,可以用宏定义 QT_MESSAGELOGCONTEXT 开启,pro 文件加上:
DEFINES += QT_MESSAGELOGCONTEXT
一个最简单的示例如下,重定向到文件:
#include
#include
#include
#include
#include
#include
//重定向qdebug输出到文件
void myMessageHandle(QtMsgType , const QMessageLogContext& , const QString& msg)
{
static QMutex mut; //多线程打印时需要加锁
QMutexLocker locker(&mut);
QFile file("log.txt");
if(file.open(QIODevice::WriteOnly|QIODevice::Append))
{
QTextStream stream(&file);
stream<
运行结果:
需求很简单,同时输出到界面中的编辑框、文件、控制台。
代码链接:https://github.com/gongjianbo/SimpleQtLogger
运行效果(图片为旧版截图):
输出到控制台,我是保存了调用 qInstallMessageHandler 时返回的函数指针,然后调用进行默认的输出。
输出到文件,因为函数调用发生在 qDebug() 调用的线程,所以需要加锁。
输出到界面,我使用了信号槽的方式,将文本发送给 connect 的槽。
此外,增加了按日期和文件大小来新建文件的逻辑。
当然,还可以添加一些细节,如日志等级的控制等。
下面是部分源码:
#include
#include "LogManager.h"
#include "mainwindow.h"
int main(int argc, char *argv[])
{
LogManager::getInstance()->initManager();//初始化
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
#pragma once
#include
#include
#include
#include
/**
* @brief 简易的日志管理类,作为单例
* @author 龚建波 - https://github.com/gongjianbo
* @date 2020-08-13
* @details
* 1.初始化时调用 initManager 重定向 QDebug 输出
* 析构时自动调用 freeManager,也可以手动调用 freeManager
* 2.根据时间戳每天重新生成一个文件,超过文件大小也会重新生成
*/
class LogManager : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(LogManager)
LogManager();
public:
~LogManager();
// 获取单例实例
static LogManager *getInstance();
// 获取带 html 样式标签的富文本
Q_INVOKABLE static QString richText(int msgType, const QString &log);
// 初始化,如重定向等
void initManager(const QString &dir = QString());
// 释放
void freeManager();
// 文件最大大小,超过则新建文件,单位字节
qint64 getFileSizeLimit() const;
void setFileSizeLimit(qint64 limit);
private:
// 重定向到此接口
static void outputHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg);
// 获取重定向的打印信息,在静态函数种回调该接口
void outputLog(QtMsgType type, const QMessageLogContext &context, const QString &msg);
// 计算下一次生成文件的时间
qint64 calcNextTime() const;
// 每次写入时判断是否打开,是否需要新建文件
void prepareFile();
signals:
// 可以关联信号接收日志信息,如显示到 ui 中
// 注意,如果槽函数为 lambda 或者其他没有接收者的情况,需要保证槽函数中的变量有效性
// 因为 static 变量的生命周期更长,可能槽函数所在模块已经释放资源,最好 connect 加上接收者
void newLog(int msgType, const QString &log);
private:
// 保留默认 handle,用于输出到控制台
QtMessageHandler defaultOutput = nullptr;
// 输出到文件
QFile logFile;
// 输出路径
QString logDir;
// 多线程操作时需要加锁
mutable QMutex logMutex;
// 下一次生成文件的时间戳,单位毫秒
qint64 fileNextTime{ 0 };
// 文件最大大小,超过则新建文件,单位字节
qint64 fileSizeLimit{ 1024 * 1024 * 32 };
};
#include "LogManager.h"
#include
#include
#include
#include
#include
LogManager::LogManager()
{
}
LogManager::~LogManager()
{
freeManager();
}
LogManager *LogManager::getInstance()
{
// 单例,初次调用时实例化
static LogManager instance;
return &instance;
}
QString LogManager::richText(int msgType, const QString &log)
{
QString log_text;
QTextStream stream(&log_text);
switch (msgType) {
case QtDebugMsg: stream << ""; break;
case QtInfoMsg: stream << ""; break;
case QtWarningMsg: stream << ""; break;
case QtCriticalMsg: stream << ""; break;
case QtFatalMsg: stream << ""; break;
default: stream << ""; break;
}
stream << log << "";
return log_text;
}
void LogManager::initManager(const QString &dir)
{
QMutexLocker locker(&logMutex);
// 保存路径
logDir = dir;
if (logDir.isEmpty())
{
// 用到了 QCoreApplication::applicationDirPath(),需要先实例化一个app
if (qApp) {
logDir = qApp->applicationDirPath() + "/log";
} else {
int argc = 0;
QCoreApplication app(argc,nullptr);
logDir = app.applicationDirPath() + "/log";
}
}
// 计算下次创建文件的时间点
fileNextTime = calcNextTime();
// 重定向qdebug到自定义函数
defaultOutput = qInstallMessageHandler(LogManager::outputHandler);
}
void LogManager::freeManager()
{
QMutexLocker locker(&logMutex);
logFile.close();
if (defaultOutput) {
qInstallMessageHandler(defaultOutput);
defaultOutput = nullptr;
}
}
qint64 LogManager::getFileSizeLimit() const
{
return fileSizeLimit;
}
void LogManager::setFileSizeLimit(qint64 limit)
{
fileSizeLimit = limit;
}
void LogManager::outputHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
// 转发给单例的成员函数
LogManager::getInstance()->outputLog(type, context, msg);
}
void LogManager::outputLog(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
// widget 中的 log,context.category = default
// qml 中的 log,context.category = qml,此时默认的 output 会增加一个 "qml:" 前缀输出
// fprintf(stderr, "print: type = %d, category = %s \n", type, context.category);
// 如果要写文件需要加锁,因为函数调用在 debug 调用线程
QMutexLocker locker(&logMutex);
QString out_text;
QTextStream stream(&out_text);
// 时间
stream << QDateTime::currentDateTime().toString("[yyyy-MM-dd hh:mm:ss]");
// 日志类型
switch (type) {
case QtDebugMsg: stream << "[Debug]"; break;
case QtInfoMsg: stream << "[Info]"; break;
case QtWarningMsg: stream << "[Warning]"; break;
case QtCriticalMsg: stream << "[Critical]"; break;
case QtFatalMsg: stream << "[Fatal]"; break;
default: stream << "[Unknown]"; break;
}
// 线程 id
stream << "[" << QThread::currentThreadId() << "]";
// 输出位置
stream << "[" << context.file << ":" << context.line << "]";
// 日志信息
stream << msg;
// 判断是否需要打开或者新建文件
prepareFile();
if (logFile.isOpen()) {
// 写入文件
stream.setDevice(&logFile);
stream << out_text << Qt::endl;
}
// 发送信号给需要的对象,如 ui 上显示日志
emit newLog(type, out_text);
// 默认的输出,控制台
// 区分日志类型给文本加颜色
// 常见格式为:\e[显示方式;背景颜色;前景文字颜色m << 输出字符串 << \e[0m
// 其中 \e=\033
// -----------------
// 背景色 字体色
// 40: 30: 黑
// 41: 31: 红
// 42: 32: 绿
// 43: 33: 黄
// 44: 34: 蓝
// 45: 35: 紫
// 46: 36: 深绿
// 47: 37: 白
// -----------------
QString cmd_text;
stream.setString(&cmd_text);
switch (type) {
case QtDebugMsg: // debug 绿色
stream << "\033[32m"; break;
case QtInfoMsg: // info 蓝色
stream << "\033[34m"; break;
case QtWarningMsg: // warning 黄色
stream << "\033[33m"; break;
case QtCriticalMsg: // critical 红字
stream << "\033[31m"; break;
case QtFatalMsg: // fatal 黑底红字
// qFatal 表示致命错误,默认处理会报异常的
stream << "\033[0;31;40m"; break;
default: // defualt 默认颜色
stream << "\033[0m"; break;
}
stream << out_text << "\033[0m";
defaultOutput(type, context, cmd_text);
}
qint64 LogManager::calcNextTime() const
{
// 可以参考 spdlog 的 daily_file_sink 优化,这里先用 Qt 接口进行实现
return QDate::currentDate().addDays(1).startOfDay().toMSecsSinceEpoch();
}
void LogManager::prepareFile()
{
// 写入文件
// 先计算好下一次生成文件的时间点,然后和当前进行比较,这里没有考虑调节系统日期的情况
if (fileNextTime <= QDateTime::currentDateTime().toMSecsSinceEpoch()){
logFile.close();
// 计算下次创建文件的时间点
fileNextTime = calcNextTime();
}
// 文件超过了大小
if (logFile.isOpen() && logFile.size() >= fileSizeLimit) {
logFile.close();
}
// 生成文件名,打开文件
if (!logFile.isOpen()) {
// 创建文件前创建目录,QFile 不会自动创建不存在的目录
QDir dir(logDir);
if (!dir.exists()) {
dir.mkpath(logDir);
}
// 文件日期
QString file_day = QDate::currentDate().toString("yyyyMMdd");
QString file_path = QString("%1/log_%2.txt").arg(logDir).arg(file_day);
logFile.setFileName(file_path);
if (logFile.exists() && logFile.size() >= fileSizeLimit) {
QString file_time = QTime::currentTime().toString("hhmmss");
file_path = QString("%1/log_%2_%3.txt").arg(logDir).arg(file_day).arg(file_time);
logFile.setFileName(file_path);
}
// 打开新的文件
// Append 追加模式,避免同一文件被清除
if (!logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
emit newLog(QtWarningMsg, "Open log file error:" + logFile.errorString() + logFile.fileName());
}
}
}