Click here to Skip to main content
15,881,588 members
Articles / Programming Languages / C++
Article

MakeMessage - An STL 'replacement' for the FormatMessage API

Rate me:
Please Sign up or sign in to vote.
4.83/5 (17 votes)
4 Mar 2006CPOL8 min read 48.4K   371   24   8
A function that uses STL strings and streams in an effort to mimic and improve on the FormatMessage API.

Introduction

In an app I am working on, I had placed all my UI strings in an ini file. I had done this to make it easy for anyone to translate the app, all they would need is a simple text editor like Notepad. What I needed now was a simple way of swapping out the variable place holders with the program generated text. I had looked at the FormatMessage API but I found it rather limited. It would only take strings or integers as its input. Anything else would have to be converted to strings before it could be passed to FormatMessage. Also, if the person who rewrites the formatting strings mismatches the number or type of parameters, the FormatMessage function could crash with an access violation. Not a very nice situation to try and debug if you can not remember the correct format.

So I came up with this solution. It is an STL based function that returns a std::string and uses stringstreams to input the variables. Because it uses streams for input, it can take as its parameters any of the standard C++ data types as well as any class that you may define that has an std::ostream insertion operator defined for it. This makes this function much more versatile than FormatMessage, in my humble opinion. That being said, this function is not intended to replace FormatMessage but to compliment it.

A note about tstl.h

This code makes use of the file tstl.h which can be found here[^]. The tstl.h file allows this code to use the STL string and stringstream classes, and be compiled in either ANSI or UNICODE builds with no errors.

The MakeMessage function

The MakeMessage function has two helper functions: TS and SetPrecision. Because these functions are tied together, I debated about whether to wrap them up as members in a class or leave them in the global namespace. In the end, I decided to wrap them up as functions inside a class.

class CFormat
{
private:
    unsigned int precision;
    std::map<TCHAR, std::tstring> markermap;
    std::tstring _make(std::tstring FormatString,
                       unsigned int ParameterCount,
                       va_list valist);
public:
    CFormat();
    CFormat(unsigned int Precision);

    unsigned int SetPrecision(unsigned int p);

    std::tstring AddMarker(TCHAR Marker,
                           std::tstring ReplaceWith);
    std::tstring RemoveMarker(TCHAR Marker);

#ifdef _WINDOWS_
    // The file windows.h declares the HMODULE type
    std::tstring MakeMessage(HMODULE hModule,
                             unsigned long FormatStringID,
                             unsigned int ParameterCount = 0,
                             ...);
#endif // _WINDOWS_
    std::tstring MakeMessage(std::tstring FormatString,
                             unsigned int ParameterCount = 0,
                             ...);
                             
    // default implementation of the TS function
    // convert any type to a string using the inserter << operator
    template <class T> const std::tstring TS (const T &t)
    {
        std::tostringstream s;
        s.setf(std::tios::fixed | std::tios::boolalpha);
        s.precision(precision);
        s << t;
        return s.str();
    }
};

The private _make function is where the actual work is done, and is called by the overridden MakeMessage functions. The variable precision is used to set the default number of digits after the decimal point when floating point numbers are entered into the TS function. AddMarker and RemoveMarker are used to manage custom insertion markers.

Parameter Information

CFormat()

CFormat(unsigned int precision)
  • unsigned int precision

    Used to set the number of digits that appear after the decimal point when the default implementation of TS takes a float or double. The default constructor sets this value to six digits.

  • Returns

    There is no return value.

std::tstring MakeMessage(HMODULE hModule,
                         unsigned long FormatStringID, 
                         unsigned int ParameterCount = 0,
                         ...)
                         
std::tstring MakeMessage(std::tstring FormatString,
                         unsigned int ParameterCount = 0,
                         ...)
  • HMODULE hModule

    Module handle of the module that contains the string table resource of the string that is to be used as the format string. Pass NULL to use the handle to the file used to create the calling process.

  • unsigned long FormatStringID

    The identifier of the format string to be loaded from the string table.

  • std::tstring FormatString

    The format string. It can be either a std::tstring or a pointer to a TCHAR string that the std::tstring constructor can convert.

  • unsigned int ParameterCount

    This is an optional parameter that is used to specify the number of optional std::tstring parameters that follow. If this parameter is not specified it defaults to zero. The maximum value for this parameter is nine. If you use a value greater then nine you will get an assertion when running in debug mode, and in release mode the value will default to nine.

  • ...

    A variable number of std::tstring parameters. You can pass any type of variable to the MakeMessage function by first running it throught the TS function. See the example code below.

  • Returns

    MakeMessage returns the newly formatted string.

template<class T> const std::tstring TS(T &t)
  • template <class T> t

    The parameter t can be of any type. TS uses the stream insertion operator << to enter this parameter into a std::tostringstream stream in order to build a std::tstring that can be used by the MakeString function. Native C++ data types can be handled by the supplied default TS function. Users of the code can use explicit template overrides of the TS function, or write their own stream manipulators if the basic TS function does not do what they want. Any class can have it's data formatted by having it's own std::tostream & operator << defined and TS will use that operator. The possibilities are endless, see the examples below for some possibilities.

  • Returns

    A tstring containing the supplied parameter in text format.

unsigned int SetPrecision(unsigned int precision)
  • unsigned int precision

    Used to set the number of digits that appear after the decimal point when the default implementation of TS takes a float or double.

  • Returns

    SetPrecision returns the previous value.

std::tstring AddMarker(TCHAR Marker,
                       std::tstring ReplaceWith)
  • TCHAR Marker

    The character that is used together with a preceding percentage sign to mark the location in the formatting string where the ReplaceWith text is to be inserted. ie. If this parameter is _T('A') then MakeMessage will look for and understand the insertion marker '%A' in the formatting string. You can use any character for this parameter except the percentage sign (%), the lower case letters 'n' and 't', and the digits 1, 2, 3, 4, 5, 6, 7, 8, and 9.

  • std::tstring ReplaceWith

    This is the string that gets inserted into the final formatted string in place of the insertion marker specified by the Marker parameter.

  • Returns

    If successful AddMarker returns the ReplaceWith string. AddMarker returns an empty string on failure

std::tstring RemoveMarker(TCHAR Marker)
  • TCHAR Marker

    A character that was previous set as an insertion marker by the AddMarker function. MakeMessage will no longer recognize this insertion marker after it has been removed.

  • Returns

    If successful RemoveMarker returns the string that was previously to be inserted in place of this marker. RemoveMarker returns an empty string on failure.

The formatting string

The format string is similar to that which is used by the FormatMessage message function in that it uses a percentage sign followed by a number as a parameter insertion marker. '%1' would be an insertion marker that marks the point where the first variable argument parameter is inserted into the format string, '%2' marks the second, and so on. There is however a limit of nine parameter insertion markers: %1, %2, %3, %4, %5, %6, %7, %8, and %9.

There are also three other special markers, one is the newline marker '%n' that used to insert a carriage return / line feed pair into the string. Another is horizontal tab marker '%t' that is used to insert a tab (ASCII 9) character into the string. And the third is the percent sign '%%' that is used to have a single percent sign in the final output. Any single percentage signs in the formatting string will be combined with character that immediately follows it and be interpreted as an insertion marker. If the insertion marker is unknown then it will be dropped from the final string. The printf style formatting that is supported by FormatMessage is not supported by MakeMessage.

For example if the first variable parameter is the number 25, and the format string is "%1 = %1" and the final text will be "25 = 25", but if the format string is "%%1 = %1" the final text will be " %1 = 25".

Examples

I will start with a simple example, the output of an integer number.

std::tstring FormatString = "The number is %1";
int Number = 15;
CFormat cf;
std::tcout << cf.MakeString(FormatString, 1, cf.TS(Number));

The output of this piece of code will be: "The number is 15"

To output a real number is not much different. You just have to set the number of digits you want after the decimal point using the SetPrecision function. If you do not set the precision it will default to six digits after the decimal point.

std::tstring FormatString = "The number is %1";
double Number = 15.536;
CFormat cf;
cf.SetPrecision(2);
std::tcout << cf.MakeString(FormatString, 1, cf.TS(Number));

The output of this piece of code will be: "The number is 15.54"

If the default implementation of TS does not do what you want for a specific data type, you can create an explicit override either in the CFormat class or in a class derived from CFormat.

You could also apply special formatting to the output by creating a custom modifier. A custom modifier would be a class that takes your data type as an input into it's constructor and has an tostream &operator << stream insertion operator defined

// declare a custom modifier class
class Hex
{
    unsigned int _h;
public:
    Hex(unsigned int h) { _h = h; }
    friend std::tostream &operator << (std::tostream &, const Hex &)
};

std::tostream &operator << (std::tostream &s, const Hex &h)
{
    s.unsetf(std::tios::dec);
    s.setf(std::tios::hex | std::tios::uppercase);
    s << _T("0x") << std::setfill(_T('0')) << std::setw(8) << h._h;
    return s;
}

// use the modifier
std::tstring FormatString = "The Number is %1";
unsigned int number = 15;
CFormat cf;
std::tcout << cf.MakeMessage(FormatString, 1, cf.TS( Hex(number) ));

The output of this piece of code will be: "The Number is 0x0000000F"

Be sure to check out the demo applications as they contain all the examples listed here.

Conclusion

The two main advantages of the MakeMessage function are that it uses the STL string and stream classes for formatting the data and that the programmer sets the number and type of parameters. Using the STL classes makes it very simple for any data type to be properly formatted into the final message string as all the type or class needs is an ostream insertion operator defined.

Having the number and type of parameters set by the formatting string is prone to errors. If the formatting string mismatches the data type or calls for more parameters than the programmer passes to the FormatMessage function, FormatMessage will crash with an access violation. MakeMessage is immune to this type of problem.

History

  • Feb 24, 2006

    Released into the wild

  • March 4, 2006

    Because of a query from steff2003 I revised how the MakeMessage function handled the insertion of the percent sign. It now works the same way string literals handle escape characters. A double percent sign is now needed in order to have a single percent sign in the final string.

    As a result of the changes made I was able to add the AddMarker and RemoveMarker functions, and I was able to add the '%t' insertion marker. I was also able to stream line how the _make function works, it now does all it's work in a single pass instead of the multiple passes it took previously.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
President
Canada Canada
Father of two, brother of two, child of two.
Spouse to one, uncle to many, friend to lots.
Farmer, carpenter, mechanic, electrician, but definitely not a plumber.
Likes walks with the wife, board games, card games, travel, and camping in the summer.
High school graduate, college drop-out.
Hobby programmer who knows C++ with MFC and the STL.
Has dabbled with BASIC, Pascal, Fortran, COBOL, C#, SQL, ASM, and HTML.
Realized long ago that programming is fun when there is nobody pressuring you with schedules and timelines.

Comments and Discussions

 
GeneralNice Pin
Neville Franks3-Mar-06 10:31
Neville Franks3-Mar-06 10:31 
GeneralRe: Nice Pin
PJ Arends4-Mar-06 5:23
professionalPJ Arends4-Mar-06 5:23 
GeneralUseful contribution Pin
Mircea Puiu2-Mar-06 20:46
Mircea Puiu2-Mar-06 20:46 
GeneralRe: Useful contribution Pin
PJ Arends4-Mar-06 5:14
professionalPJ Arends4-Mar-06 5:14 
QuestionWhy this? Pin
steff200328-Feb-06 13:29
steff200328-Feb-06 13:29 
I don't understand why you treat the cases '%%1' and '%%n' not just as an ordinary case of '%%' in _make:
<br />
        if (LoopCounter == 0)<br />
        {<br />
            StringToFind << _T("%%n");<br />
        }<br />
        else<br />
        {<br />
            StringToFind << _T("%%") << LoopCounter;<br />
        }<br />


Maybe i just miss something but this would have the same result:
<br />
StringToFind.str(_T("%%"));<br />


and would also handle a formatstring like "%%%1" correctly D'Oh! | :doh:
AnswerRe: Why this? Pin
PJ Arends28-Feb-06 17:48
professionalPJ Arends28-Feb-06 17:48 
GeneralRe: Why this? Pin
steff200328-Feb-06 22:21
steff200328-Feb-06 22:21 
GeneralBoost Pin
Stephen Hewitt26-Feb-06 18:57
Stephen Hewitt26-Feb-06 18:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.