multicolored status bar
(level : easy)


Contents

Introduction
CStatusBar derived class
Storing colors
Drawing the text
Use


Introduction

While writing a level editor with MFC, I needed to display a text made of words of different colors in the application's status bar. The web sites I'm used to go to when looking for this kind of information (see the page "links" : misc) were full of explanations about how to have a different color in each pane of the status bar, but none about how to draw many colors in the same pane (the first one, in my case). This is not complicated indeed, if you know where to begin, hence this article.

OK, the most curious of you are probably asking themselves : "what for can this be useful ?". I'm sure several opportunities can be found to take advantage of this system, anyway here is mine :

this picture is a piece of screenshot from the 3D view of my program. When the user moves the cursor above this view, a pick (detection of objects pointed by the cursor) is processed, and it is its result that I display in the status bar. Note for those who are interested : my pick is done geometrically by casting a ray through the camera and cursor 3D positions. Objects corresponding to the cursor's position are sorted according to their distance from the camera, and their names are printed separated by dashes : "object3 - sphere1 - toto" for example.

The pick is used to determine which objects can be selected or removed from the selection if the user activates the right command (for example a left click). Among the objects detected, some are already selected (on my picture this is the case for the one in wireframe, "pf12") and others are not, some can be hidden in the 3D view. How to know then which ones are part of the current selection ? By drawing their names with a different color, like in this status bar (corresponding to the previous picture) :

Selected objects are in blue, the brackets show the object the user is going to interact with, and the TAB key allows to switch from one to the next.


CStatusBar derived class

The first thing to do is to derive a class from CStatusBar, for example CColoredStatusBar, with the ClassWizard ( Insert / New Class). CStatusBar is not included in the list of base classes, so you have to derive from CWnd (generic CWnd), and replace the instances of "CWnd" by "CStatusBar" in the .h and .cpp files :

/////////////////////////////////////////////////////////////////////////////
// CColoredStatusBar window

class CColoredStatusBar : public CStatusBar
{
// Construction

BEGIN_MESSAGE_MAP(CColoredStatusBar, CStatusBar)
  //{{AFX_MSG_MAP(CColoredStatusBar)
    // NOTE - the ClassWizard will add and remove mapping macros here.
  //}}AFX_MSG_MAP
END_MESSAGE_MAP()

In order to use this new class, you have of course to include it in MainFrm.h, and modify the status bar's declaration :

#include "ColoredStatusBar.h"

class CMainFrame : public CMDIFrameWnd
{
  DECLARE_DYNAMIC(CMainFrame)
public:
  CMainFrame();

protected:  // control bar embedded members
  CColoredStatusBar  m_wndStatusBar;


Storing colors

There are many ways to store the different colors of the text that will be displayed in the first pane of the status bar :

- do it in a structure separated from the text, indicating which characters are affected by which color ;
- insert "markers" in the text like one puts "\n" in a printf, and use the string already existing in CStatusBar by calling m_wndStatusBar.SetPaneText(0,Text);
- use the markers' system in a string added to CColoredStatusBar ;
- and so on...

I've chosen the 3rd method, so that if the MFC modify the text in the first pane for any reason, my own string is kept and can be read back. A member is added to CColoredStatusBar :

public:
  CString  m_Text;

Markers can take any form and be numerous, for this example I've decided to use "$INKblue$" and "$INKblack$" to set the text color to blue or black respectively ; if black is the default color, the string "Hello $INKblue$World $INKblack$!" should display like this :
Hello World !

In spite of predefining some colors (blue and black here), one could directly pass the RGB components in the form "$INK0;0;255$" for example (for blue).


Drawing the text

It is now time to display our text stored in CColoredStatusBar. For that purpose, we have to override the OnDrawItem method of this class "by hand", beware this is not the function corresponding to the WM_DRAWITEM message (prototypes are different).

in the .h file :
protected:
  void DrawColoredString (CDC& dc,const char* pszTxt);

// Overrides
  // ClassWizard generated virtual function overrides
  //{{AFX_VIRTUAL(CColoredStatusBar)
  void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
  //}}AFX_VIRTUAL

in the .cpp file :
void CColoredStatusBar::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
  {
  // Attach to a CDC object
  CDC dc;
  dc.Attach(lpDrawItemStruct->hDC);

  dc.SetBkMode(TRANSPARENT);

  // Get the pane rectangle and calculate text coordinates
  CRect rect(&lpDrawItemStruct->rcItem);

  if(lpDrawItemStruct->itemID == 0)
    {
    if(!m_Text.IsEmpty())
      {
      dc.SetTextAlign(TA_UPDATECP);
      dc.MoveTo(rect.left+2, rect.top);

      dc.SetTextColor(RGB(0, 0, 0));
      DrawColoredString(dc,m_Text);

      dc.SetTextAlign(TA_NOUPDATECP);
      }
    }

  // Detach from the CDC object, otherwise the hDC will be
  // destroyed when the CDC object goes out of scope
  dc.Detach();
  }

//

void CColoredStatusBar::DrawColoredString(CDC& dc,const char* pszTxt)
  {
  if(!pszTxt) return;

  char* pszInk = strstr(pszTxt,"$INK");
  if(pszInk) *pszInk = 0;

  dc.TextOut(0,0,pszTxt);                                   // substring before $INK
  if(!pszInk) return;

  pszInk += strlen("$INK");
  char* pszEnd = strchr(pszInk,'$');
  if(!pszEnd) return;                                       // error

  *pszEnd = 0;
  if(0 == strcmp(pszInk,"black")) dc.SetTextColor(RGB(  0,  0,  0));
  if(0 == strcmp(pszInk,"blue"))  dc.SetTextColor(RGB(  0,  0,255));

  DrawColoredString(dc,pszEnd+1);
  }

The two "if" check we are really dealing with pane 0 and the text is not empty, SetTextColor selects black as the default color. DrawColoredString searches for markers in the string, draws the text before them, then if necessary changes color and processes the remaining string with a recursive call.

This is not entirely enough : we have to tell the status bar that it must call our DrawItem version for pane 0, thanks to the instruction :
m_wndStatusBar.GetStatusBarCtrl().SetText("", 0, SBT_OWNERDRAW);
To be able to access m_wndStatusBar, this command must be put in a function of the CMainFrame class. The easiest solution is to add a method like this one :

void CMainFrame::SetHelpText(const CString& Text)
{
  m_wndStatusBar.m_Text = Text;
  // Change 1st pane style to make it Owner-drawn
  m_wndStatusBar.GetStatusBarCtrl().SetText("", 0, SBT_OWNERDRAW); 
}


Use

Everything is in place, we now just have to give SetHelpText a string with markers. In the sample program (168 Kb), a double-click in the application's view produces a call to :
((CMainFrame*)AfxGetMainWnd())->SetHelpText("Hello $INKblue$World $INKblack$!");

Notice that if you go through the commands in the menu, their descriptions are still correctly displayed in the status bar.


back to top