1. 程式人生 > >Qt 日誌輸出

Qt 日誌輸出

define eas AS 後來 cout 默認 成了 ech ios

Qt學習(3)日誌輸出

普通的打印輸出

用 QtCreator 開發 Qt 程序時, 經常需要向控制臺打印一些參數。有時候是查看對象的屬性是否被正確設置,有時候是查看程序是否執行了某一段代碼,或者執行了多少次這一段代碼。盡管使用調試模式可以一行一行的查看代碼的執行情況,也可以看到執行代碼後變量的相應值,但是 Qt 的實現采用了 D 指針,它隱藏了代碼的實現,在查看變量的值時不是非常的方便(另外在 Windows 平臺下打開調試模式經常會出現打開 cdb 程序異常緩慢,卡在 為 ABI ‘x86-windows-msvc2015-pe-64bit‘ 啟動調試器 ‘CdbEngine‘, 不清楚具體的原因是什麽)。

技術分享圖片

初看變量面板其實很難看到 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 輸出到控制臺。

這裏的 qDebug 函數實際上是 qlogging.h 中定義的宏,類似的還有 qInfoqFatal 等:

// 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() 的輸出變成

技術分享圖片

輸出會顯示文件名和行號,這是因為當前項目的構建模式選擇的是 Debug,如果選擇的是 Release 模式,那麽在發布正式版程序時是看不到文件名和行號的(當然也包括函數名),文件名或函數名會顯示為 unknown,行號會顯示為 0。

如果覺得這些格式並不好看,可以看看這篇文章,用 qt 做出漂亮的調試輸出。

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());
// }

技術分享圖片

這裏的輸出格式和 myMessageOutput 函數中 fprintf 函數的格式是一致的,然而請註意,在主函數裏,qSetMessagePattern 函數是沒有註釋掉的,然而輸出忽略掉了 qSetMessagePattern 設置的格式。

導出調試信息到日誌中

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

這裏我打算結合 qSetMessagePatternQtMessageHandler 來實現輸出調試信息到文件中。

在設置了 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;
}

輸出如下:

技術分享圖片

要把輸出信息導出到日誌文件中,只需要將數據導入到文件輸出流就行了。

// 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"

代碼

代碼存放在 github

參考

http://wiki.qt.io/D-Pointer/zh
https://blog.csdn.net/liang19890820/article/details/51838379
https://woboq.com/blog/nice-debug-output-with-qt.html
https://www.cnblogs.com/lvchaoshun/p/7806248.html
https://blog.csdn.net/liang19890820/article/details/51839233
http://doc.qt.io/qt-5/qtglobal.html#qSetMessagePattern
http://doc.qt.io/qt-5/qdebug.html
http://doc.qt.io/qt-5/qtglobal.html#qInstallMessageHandler

Qt 日誌輸出