天天看點

C++Qt開發——日志輸出

作者:音視訊開發老舅

普通的列印輸出

用 QtCreator 開發 Qt 程式時, 經常需要向控制台列印一些參數。有時候是檢視對象的屬性是否被正确設定,有時候是檢視程式是否執行了某一段代碼,或者執行了多少次這一段代碼。盡管使用調試模式可以一行一行的檢視代碼的執行情況,也可以看到執行代碼後變量的相應值,但是 Qt 的實作采用了 D 指針,它隐藏了代碼的實作,在檢視變量的值時不是非常的友善。

C++Qt開發——日志輸出

初看變量面闆其實很難看到 objectName 是不是已經設定成了 test,因為沒法知道存儲 objectName 屬性的變量名叫什麼,檢視 Qt setObjectName 的源代碼:

// qobject.cpp
void QObject::setObjectName(const QString &name)
{
    Q_D(QObject);
    if (!d->extraData)
        d->extraData = new QObjectPrivate::ExtraData;

    if (d->extraData->objectName != name) {
        d->extraData->objectName = name;
        emit objectNameChanged(d->extraData->objectName, QPrivateSignal());
    }
}
           

看到實際上 objectName 這一屬性實際上是存放在 d 指針的 extraData 對象裡面。

為了友善的看到屬性的值,可以把屬性值輸出到控制台界面:

#include <QCoreApplication>
#include <QDebug>
// #include <iostream>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QObject obj;
    obj.setObjectName( "test" );

//    std::cout << obj.objectName().toLocal8Bit().constData();  // 顯示到标準輸出流(不是Creator內建的控制台)
    qDebug() << obj.objectName();
    return a.exec();
}
           

首先包含 QDebug 頭檔案(.pro 檔案中加入 Qt += core),然後在程式中使用 qDebug() 及流運算符,就可以把 objectName 輸出到控制台。

【領更多QT學習資料,點選下方連結免費領取↓↓,先碼住不迷路~】

點選→Qt開發(文檔教程+技術視訊+項目實戰源碼)

這裡的 qDebug 函數實際上是 qlogging.h 中定義的宏,類似的還有 qInfo、 qFatal 等:

// qlogging.h
#define qDebug QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).debug
#define qInfo QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).info
#define qWarning QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).warning
#define qCritical QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).critical
#define qFatal QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).fatal
           

格式化輸出

個人覺得直接在程式中輸出某些屬性,其實對于調試簡單的邏輯代碼時候非常友善,是以在程式中加入了大量的 qDebug 用來輸出調試資訊,然而當輸出資訊很多的時候,很難從一堆資訊中找到需要的資訊,還沒法定位輸出資訊的位置。之前不知道 Qt 的輸出機制,就在每個調用 qDebug() 的地方加上一段位置資訊,比如:

qDebug() << "[Debug] MyObject::test " << obj.objectName();           

每個輸出都要自己加上一些額外資訊,一般類名或者函數名都不會變化,是以可以當作位置資訊加到要輸出的前面,但是你可能注意到了,代碼所在的行數經常會變化,是以沒辦法把行數添加到輸出裡。

另外,如果想要在列印輸出的時候加上時間戳,那怎麼辦呢?最開始我想可以在調用 qDebug() 的位置加上 QDateTime,後來想想還是算了,太麻煩了。

為了格式化輸出,加上一些額外資訊,可以使用 qSetMessagePattern(const QString &) 函數或者設定 QT_MESSAGE_PATTERN 環境變量來定制自己的輸出格式,它可以修改資訊處理器的預設輸出,以下是官網的兩種方法(可用的占位符請參考官方文檔):

或者可以在 main 函數中加入以下代碼:

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}" );
           

在項目的建構環境中添加環境變量

QT_MESSAGE_PATTERN = [%{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}

在控制台上可以看到 qDebug() << obj.objectName() 的輸出變成

C++Qt開發——日志輸出

輸出會顯示檔案名和行号,這是因為目前項目的構模組化式選擇的是 Debug,如果選擇的是 Release 模式,那麼在釋出正式版程式時是看不到檔案名和行号的(當然也包括函數名),檔案名或函數名會顯示為 unknown,行号會顯示為 0。

QT_MESSAGE_PATTERN 的優先級要比 qSetMessagePattern 的函數調用優先級高:

The pattern can also be changed at runtime by setting the QT_MESSAGE_PATTERN environment variable; if both qSetMessagePattern() is called and QT_MESSAGE_PATTERN is set, the environment variable takes precedence.

可以通過修改 QT_MESSAGE_PATTERN 環境變量在運作時修改輸出格式;如果調用 qSetMessagePattern 的同時又設定了 QT_MESSAGE_PATTERN,那麼這個環境變量将會生效。

另一種格式化的方式

此外還有一種格式化輸出資訊的方式,使用 qInstallMessageHandler 注冊一個自定義的消息處理器替換掉 預設的 QtMessageHandler:

void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
    QByteArray localMsg = msg.toLocal8Bit();
    switch (type) {
    case QtDebugMsg:
        fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
        break;
    case QtInfoMsg:
        fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
        break;
    case QtWarningMsg:
        fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
        break;
    case QtCriticalMsg:
        fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
        break;
    case QtFatalMsg:
        fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
        break;
    }
}

int main(int argc, char *argv[])
{
    qInstallMessageHandler( myMessageOutput );
    QCoreApplication a(argc, argv);

    ...
}
           

通過 qInstallMessageHandler(QtMessageHandler) 函數注冊一個自定義的 MessageHandler,QtMessageHandler 的定義如下:

typedef void (*QtMessageHandler)(QtMsgType, const QMessageLogContext &, const QString &);           

可以看到,QtMessageHandler 并不是一個類,而是一個函數指針,函數原型滿足 (QtMsgType, const QMessageLogContext &, const QString &) 的函數都可以作為 QtMessageHandler 參數傳遞個 qInstallMessageHandler 函數中。

注冊 MessageHandler 後,輸出調試資訊,

// int main() {
    ...

    QObject dobj;
    dobj.setObjectName( "Debug Test" );
    qDebug() << dobj.objectName();
    QObject wobj;
    wobj.setObjectName( "Warning Test" );
    qWarning() << wobj.objectName();
    QObject cobj;
    cobj.setObjectName( "Critical Test" );
    qCritical() << cobj.objectName();
    QObject iobj;
    iobj.setObjectName( "Info Test" );
    qInfo() << iobj.objectName();
//    QObject fobj;
//    fobj.setObjectName( "Fatal Test" );
//    qFatal(fobj.objectName().toLocal8Bit().constData());
// }
           
C++Qt開發——日志輸出

這裡的輸出格式和 myMessageOutput 函數中 fprintf 函數的格式是一緻的,然而請注意,在主函數裡,qSetMessagePattern 函數是沒有注釋掉的,然而輸出忽略掉了 qSetMessagePattern 設定的格式。

【領更多QT學習資料,點選下方連結免費領取↓↓,先碼住不迷路~】

點選→Qt開發(文檔教程+技術視訊+項目實戰源碼)

導出調試資訊到日志中

以上的方法雖然可以格式化輸出,但是調試輸出的資訊隻會在控制台中顯示,如果想要把輸出資訊導出到日志檔案中怎麼辦呢?需要自己寫個類(或者定義個新的宏),把 qDebug() 替換成自己定義的函數嗎?其實是不需要的,我們可以在自定義的消息處理器中将資訊輸出到檔案中。你可能已經注意到了,官方的示例中是把所有資訊用 fprintf 輸出到标準錯誤流 stderr 中的,我們隻需要改變輸出流到檔案中就好了。

這裡我打算結合 qSetMessagePattern 和 QtMessageHandler 來實作輸出調試資訊到檔案中。

在設定了 qSetMessagePattern 後,要想使用設定好的格式,需要調用 qFormatLogMessage():

Custom message handlers can use qFormatLogMessage() to take pattern into account.
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
    QString formatMsg = qFormatLogMessage( type, context, msg );
    if( type < msgLevel ) {
        return;
    }
    std::cout << formatMsg.toLocal8Bit().constData() << std::endl;
}
           

輸出如下:

C++Qt開發——日志輸出

要把輸出資訊導出到日志檔案中,隻需要将資料導入到檔案輸出流就行了。

// Log::setLogFile {
    file.setFileName( name );
    file.open( QIODevice::WriteOnly | QIODevice::Append );
    outstream.setDevice( &file );
    outstream.setCodec( QTextCodec::codecForName("UTF-8") );
// }

// Log::myMessageOutput {
    ...
    outstream << formatMsg.toLocal8Bit().constData() << "\r\n";
    outstream.flush();
// }
           

我在使用測試程式時,發現 log.txt 檔案中一直沒有内容,在 outstream 輸出後加上 flush 操作,就可以看到 log.txt 檔案中的内容了,和之前在控制台中列印出來的是一樣的:

[20180620 15:01:43.325 中國标準時間 D] main.cpp:main:93 -- "Debug Test"
[20180620 15:01:43.328 中國标準時間 W] main.cpp:main:96 -- "Warning Test"
[20180620 15:01:43.330 中國标準時間 C] main.cpp:main:99 -- "Critical Test"
[20180620 15:01:43.331 中國标準時間 I] main.cpp:main:102 -- "Info Test"
           

繼續閱讀