Qt重定向QDebug,自定义一个简易的日志管理类

0.前言

相对于第三方的日志库,在 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

1.最简单的操作

一个最简单的示例如下,重定向到文件: 

#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<

运行结果:

Qt重定向QDebug,自定义一个简易的日志管理类_第1张图片

2.实现一个简易的日志管理类

需求很简单,同时输出到界面中的编辑框、文件、控制台。

代码链接:https://github.com/gongjianbo/SimpleQtLogger

运行效果(图片为旧版截图):

Qt重定向QDebug,自定义一个简易的日志管理类_第2张图片

Qt重定向QDebug,自定义一个简易的日志管理类_第3张图片

Qt重定向QDebug,自定义一个简易的日志管理类_第4张图片

输出到控制台,我是保存了调用 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());
        }
    }
}

你可能感兴趣的:(Qt,略知一二,QDebug,Qt重定向,Qt日志)