Using an RTF Log File
This article is intended for anyone who is looking for a new debugging tool to add to their arsenal. This article is divded into the following sections:
1. The case for RTF log filesFirst of all, I'd like to let you know that if you don't have the time or inclination to read through all of this, theres a link at the end of this article to the source being discussed. Second, all code accompanying this article will be written as stand-alone functions. I personally use a C++ singleton class to handle logging, but I'm leaving it up to you to decide how you want to implement this technique in your own program. I'm sure I don't have to convince you of the value of using a log file, but why go through all of the trouble to make an RTF log file when you could just use a regular text file, or even an HTML log file? Here are a couple of points to consider:
If you're still not convinced, I ask you to just give it a shot. What have you got to lose besides 5 minutes of coding time? 2. The RTF file formatAfter I made the decision to write an RTF log file, I went ahead and downloaded the RTF file spec documents. When I opened the RTF 1.7 specification file in Word, I nearly gave up right then and there. The file spec was 225 pages long and covered everything from "document variables" to foreign language support. All I wanted to do was make some text blue. Fortunately, a bit of searching turned up a great site with a basic overview of the RTF file format : The RTF Cookbook RTF files are organized into blocks denoted by { braces } and use formatting commands that begin with a back-slash and are delimited by a space, line-break, semi-colon, or the beginning of another command (the delimiter following a command does not show up in the actual text of the document). Braces and backslashes that are part the actual text have a backslash prepended to them and a space or line-break after them (i.e \\ , \{ . and \} ). RTF files have a header, and it's fairly simple to construct. Well, it doesn't have to be simple (foreign language support, etc, etc), but the one I created is. First, you need a line or two that lets a word processor know this is an RTF file. It looks something like this: {\rtf1 \ansi \deff0 \fonttbl{\f0 Courier New;} \fs20} The rtf1, ansi, and deff0 commands are file format tags and nothing to worry about. The fonttbl command defines a table of all the fonts used in the file. For this tutorial, we will only be using one font: Courier New. The fs20 command sets the font size to 10. The syntax of this command is \fs<double the desired font size>. The command is formatted like this to add easy support for font sizes like "10.5" (which would be \fs21). Next in the file comes the colour table. Any font colour you want to use in your files has to be in this table. Here's an example with three colours (we'll be using more the actual implementation of the logging class): {\colortbl; \red255\green255\blue255; \red128\green128\blue128; \red255\green0\blue0; } The above colour table makes the colours black, grey, and red available for use in the rest of the document. Now we get into the actual text of the document, and the formatting tags that go with it. Just like commands, text can be organized into blocks using braces. Any formatting attributes set inside a block will not affect text outside of the block. Here's where RTF files begin to look a little sexier than HTML files. If we surround every outputted line with braces, each line begins with a nice, plain black font. Also, there's a command we'll be using on each line as sort of a safeguard, "\pard", that removes all special formatting. Here's an example of a line of text: {\pard \cf5 \b <CGraphics::Init>\b0 Direct3D initialized successfully\par} If the 6th entry (index of 5) in the colour table is blue, This will look like: <CGraphics::Init> Direct3D initialized successfully Here's a list of some common formatting commands, and what they do:
Note that many of these formatting flags can be turned off by writing them again with an appended zero (e.g. you can turn off italics by writing \i0) We've covered the header of the file and the meat of the file. All that's left is the footer. Here's where things get really complicated. Below is an example of an RTF file footer: } I suggest just copy-and-pasting that instead of trying to be some kind of programming hero and writing your own. 3. Preparing an RTF file for outputHere are the variables the logger will be using: //External variables referenced by logger -- typically private class members static BOOL bLoaded; //File loaded flag static HANDLE hFile; //Handle to the file static string sFilename; //Filename Here is the initialization function for the logger: //Open log file int Init (const string& sFile) { //Make sure log file is not already open if (bLoaded) return FALSE; //Open file hFile = CreateFile (sFile.c_str(), GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); if (hFile == INVALID_HANDLE_VALUE) return FALSE; //Set file loaded flag bLoaded = TRUE; //Set filename sFilename = sFile; //Write RTF header WriteString ("{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Courier New;}}\\fs20\n"); //Write colour table WriteString ("{\\colortbl;\n"); //Black WriteString ("\\red255\\green255\\blue255;\n"); //White WriteString ("\\red128\\green128\\blue128;\n"); //Grey WriteString ("\\red255\\green0\\blue0;\n"); //Red WriteString ("\\red0\\green255\\blue0;\n"); //Green WriteString ("\\red0\\green0\\blue255;\n"); //Blue WriteString ("\\red0\\green255\\blue255;\n"); //Cyan WriteString ("\\red255\\green255\\blue0;\n"); //Yellow WriteString ("\\red255\\green0\\blue255;\n"); //Magenta WriteString ("\\red128\\green0\\blue0;\n"); //Dark Red WriteString ("\\red0\\green128\\blue0;\n"); //Dark Green WriteString ("\\red0\\green0\\blue128;\n"); //Dark Blue WriteString ("\\red255\\green128\\blue128;\n"); //Light Red WriteString ("\\red128\\green255\\blue128;\n"); //Light Green WriteString ("\\red128\\green128\\blue255;\n"); //Light Blue WriteString ("\\red255\\green128\\blue0;\n"); //Orange WriteString ("}\n"); //Write log header LogString ("*** Logging Started"); LogString (""); //Success return TRUE; } Nothing overly complicated here. The function opens a file, drops in an RTF file header and colour table, and writes a string that says "*** Logging Started" into the file. The function LogString() used to write text in the file is shown later on in the article. Here's a function that will close the file properly //Close log file int Close () { //Make sure log file is already opened if (!bLoaded) return FALSE; //Write closing line LogString (""); LogString ("*** Logging Ended"); //Write closing brace (RTF file footer) WriteString ("}"); //Close file CloseHandle (hFile); //Clear file loaded flag bLoaded = FALSE; //Success return TRUE; } If you use the logging file in a DLL, checking the file for the "*** Logging Ended" message can be a good way of assuring yourself the DLL is cleaning itself up properly. 4. Writing formatted outputHere comes the hard part: actually printing messages to the file. We will be using four functions for this. First however, comes an enumeration. It represents the colour table written to the file in Init(): //RTF file's colour table enum COLOURTABLE { BLACK = 0, WHITE = 1, GREY = 2, RED = 3, GREEN = 4, BLUE = 5, CYAN = 6, YELLOW = 7, MAGENTA = 8, DARK_RED = 9, DARK_GREEN = 10, DARK_BLUE = 11, LIGHT_RED = 12, LIGHT_GREEN = 13, LIGHT_BLUE = 14, ORANGE = 15 }; The first of the four functions, WriteString() puts a plaintext string directly into the file. This should be a private function in your logging class. It is listed below: //Write a string directly to the log file void WriteString (const string& sText) { DWORD bytesWritten; WriteFile (hFile, sText.c_str (), (int) sText.length (), &bytesWritten, NULL); } Nothing that really needs to be explained. The second function, ReplaceTag() simply replaces all occurences of a formatting tag in a string with its RTF command equivalent. It allows us to use formatting tags such as [B] and [/B] rather than writing out the RTF commands. This function should be a private class member //Replace all instances of a formatting tag with the RTF equivalent void ReplaceTag (string* sText, const string& sTag, const string& sReplacement) { unsigned int start = 0; while (sText->find(sTag, start) != string::npos) { start = sText->find(sTag, start); sText->replace (start, sTag.length (), sReplacement); start += sTag.length () + 1; } } This is basically just a substring-replacement function. Not particularly complicated. If you do not want to use special formatting tags, like [B], you should still include this function as it is later used to replace slashes and braces with the RTF comamdns that allow them to show up in the document. The third function, DoFormatting(), is where the special formatting tags I've been going on about are actually implemented. It also allows you to easily include slashes and braces in your logging strings. Furthermore, it puts strings to be logged inside a set of braces and clears any previous formatting. This function should be a private class member. //Replace formatting tags in a string with RTF formatting strings void DoFormatting (string* sText) { //Fix special symbols {, }, and backslash ReplaceTag (sText, "\\", "\\\\"); ReplaceTag (sText, "{", "\\{"); ReplaceTag (sText, "}", "\\}"); //Colours ReplaceTag (sText, "[BLACK]", "\\cf0 "); ReplaceTag (sText, "[WHITE]", "\\cf1 "); ReplaceTag (sText, "[GREY]", "\\cf2 "); ReplaceTag (sText, "[RED]", "\\cf3 "); ReplaceTag (sText, "[GREEN]", "\\cf4 "); ReplaceTag (sText, "[BLUE]", "\\cf5 "); ReplaceTag (sText, "[CYAN]", "\\cf6 "); ReplaceTag (sText, "[YELLOW]", "\\cf7 "); ReplaceTag (sText, "[MAGENTA]", "\\cf8 "); ReplaceTag (sText, "[DARK_RED]", "\\cf9 "); ReplaceTag (sText, "[DARK_GREEN]", "\\cf10 "); ReplaceTag (sText, "[DARK_BLUE]", "\\cf11 "); ReplaceTag (sText, "[LIGHT_RED]", "\\cf12 "); ReplaceTag (sText, "[LIGHT_GREEN]", "\\cf13 "); ReplaceTag (sText, "[LIGHT_BLUE]", "\\cf14 "); ReplaceTag (sText, "[ORANGE]", "\\cf15 "); //Text style ReplaceTag (sText, "[PLAIN]", "\\plain "); ReplaceTag (sText, "[B]", "\\b "); ReplaceTag (sText, "[/B]", "\\b0 "); ReplaceTag (sText, "[I]", "\\i "); ReplaceTag (sText, "[/I]", "\\i0 "); ReplaceTag (sText, "[U]", "\\ul "); ReplaceTag (sText, "[/U]", "\\ul0 "); ReplaceTag (sText, "[S]", "\\strike "); ReplaceTag (sText, "[/S]", "\\strike0 "); ReplaceTag (sText, "[SUB]", "\\sub "); ReplaceTag (sText, "[/SUB]", "\\sub0 "); ReplaceTag (sText, "[SUPER]", "\\super "); ReplaceTag (sText, "[/SUPER]", "\\super0 "); ReplaceTag (sText, "[LEFT]", "\\ql "); ReplaceTag (sText, "[RIGHT]", "\\qr "); ReplaceTag (sText, "[CENTER]", "\\qc "); ReplaceTag (sText, "[FULL]", "\\qj "); ReplaceTag (sText, "[TITLE]", "\\fs40 "); ReplaceTag (sText, "[/TITLE]", "\\fs20 "); ReplaceTag (sText, "[H]", "\\fs24 "); ReplaceTag (sText, "[/H]", "\\fs20 "); //Add brackets and line formatting tags sText->insert (0, "{\\pard "); sText->insert (sText->length (), "\\par}\n"); } If it looks a bit too unwieldy for your liking, you can remove a few of the formatting tags. I personally don't think speed is a big issue in logging. File output buffers make sure the log files dont slow down your program too much, and the cost of a few substring replacements is minimal. Anyways, you would probably want to disable logging in release builds (the next section details methods of making all of the logging function calls compile right out of the program). The final function needed to get a fully-functioning RTF log file up and running is LogString(). This is the function you can call from anywhere to log messages to the file. It should be a public member of your logging class. //Format and log a string int LogString (string sText) { //Make sure log file is already opened if (!bLoaded) return FALSE; //Format string DoFormatting (&sText); //Write string to file WriteString (sText); //Success return TRUE; } That does it. The log file should be running properly at this point. You should be able to open a file with Init(), write to it with LogString() and close it with Close(). 5. Logging macrosIn this section, a series of macros will be presented that allow access to the log file. If you use only these macros to interact with the logging functions, it is quite easy to have the preprocessor remove all logging code from a release build of your program. Before we get to the macros, I would like to introduce a function that allows you to easily write messages that look like the following: <Class::FunctionName> Some information<Class::FunctionName> A notification of success <Class::FunctionName> A warning <Class::FunctionName> A fatal error Here's a short snip from one of my log files that shows how this looks in practice: <CMusicManager::Init> Attempting to load songs in directory: Music\<CMusicManager::Init> Loaded song: (286394ms) Coheed and Cambria - Delirium Trigger (album).ogg <CMusicManager::Init> Successfully loaded all songs in given directory <CGraphics::Init> Initializing Direct3D... <CGraphics::Init> Direct3D 8 object created <CFake::Error> Nothing ever goes wrong with my code so I had to make a fake error <CGraphics::CheckCaps> Examining adapter capabilities... <CGraphics::CheckCaps> Adapter being examined: 3D Blaster GeForce2 MX <CGraphics::CheckCaps> Video card supports textures as big as 1024x1024 <CGraphics::CheckCaps> Video card supports non-square textures <CGraphics::CheckCaps> Video card does not support non-power-of-2 textures <CGraphics::CheckCaps> Video card supports alpha channel in textures <CGraphics::CheckCaps> Video card supports modulated texture blending <CGraphics::CheckCaps> Video card supports source alpha blending <CGraphics::CheckCaps> Video card supports destination alpha blending <CGraphics::CheckCaps> Video card has required capabilities As you can see, it is very easy to pick out warnings and errors from the rest of the messages. Here is an enumeration the function depends on: //Log message types enum MESSAGETYPE { MSG_SUCCESS = 1, MSG_INFO = 2, MSG_WARN = 3, MSG_ERROR = 4, }; Here is the function: //Write a specially formatted message to the log file int CLogFile::LogMessage (const string &sSource, const string& sMessage, MESSAGETYPE messageType) { string sString; switch (messageType) { case MSG_SUCCESS: sString = "[BLUE]"; break; case MSG_INFO: sString = "[GREY]"; break; case MSG_WARN: sString = "[ORANGE]"; break; case MSG_ERROR: sString = "[RED]"; break; } sString += "[B]<"; sString += sSource; sString += ">[/B] "; sString += sMessage; //Format the string and write it to the log file return LogString (sString); } All it does is add a couple colour tags and some formatting, and passes it off to the LogString() function. Now onto the macros. Here they are: //Constant for disabling logging const int LOGGING_DISABLED = 0x00330099; //Logging definitions #ifdef ENABLE_LOGGING #define openLog(fileName) Init(fileName) #define closeLog Close() #define writeLog(s) LogString(s) #define msgLog(message, type) LogMessage(__FUNCTION__, message, type) #define infoLog(message) LogMessage(__FUNCTION__, message, CLogFile::MSG_INFO) #define successLog(message) LogMessage(__FUNCTION__, message, CLogFile::MSG_SUCCESS) #define warnLog(message) LogMessage(__FUNCTION__, message, CLogFile::MSG_WARN) #define errorLog(message) LogMessage(__FUNCTION__, message, CLogFile::MSG_ERROR) #else #define openLog(fileName) LOGGING_DISABLED #define closeLog LOGGING_DISABLED #define writeLog(s) #define msgLog(message, type) #define infoLog(message) #define successLog(message) #define warnLog(message) #define errorLog(message) #endif As should be apparent, logging will only compile into your program if ENABLE_LOGGING is defined. Otherwise, attempts to use openLog and closeLog will "return" LOGGING_DISABLED. You can write to the file with the LogString() function via the writeLog macro. You can also write to the file with the LogMessage() function using the remaining 5 macros, unless you use Visual C++ 6.0. If you do, you don't have the luxury of the __FUNCTION__ macro. For Visual C++ 6 users (and for users of other compilers that do not support __FUNCTION__) I recommend rewriting the macros in this fashion:
#define msgLog(source, message, type) LogMessage(source, message, type)
This concludes my article. All of the functions in this article, have been compiled into the source files available here. Please send any bugs, suggestions, or feedback to Eamonn@gdnMail.net Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|