barre d'état multicolore
(niveau : facile)


Table des matières

Introduction
Classe dérivée de CStatusBar
Stockage des couleurs
Affichage du texte
Utilisation


Introduction

Pendant l'écriture d'un éditeur de niveaux en MFC, j'ai eu besoin d'afficher un texte formé de mots de différentes couleurs dans la barre d'état de l'application. Les sites web sur lesquels je vais habituellement chercher ce genre d'information (cf la page "liens" : divers) contenaient moult explications permettant d'utiliser une couleur différente dans chaque panneau (pane) de la barre d'état, mais aucune se rapportant à l'affichage de plusieurs couleurs dans un même panneau (le 1er, dans mon cas). En fait cela n'a rien de compliqué, à condition comme toujours de savoir par où commencer, d'où cet article.

OK, les plus curieux se demandent probablement : "mais à quoi cela peut-il bien servir ?". Je suis sûr qu'on peut trouver plusieurs occasions d'utiliser ce sytème, en tout cas voici la mienne :

cette image est un morceau de capture d'écran de la vue 3D de mon programme. Lorsque l'utilisateur déplace le curseur sur cette vue, un pick (détection des objets pointés par le curseur) est effectué, et c'est son résultat que j'affiche dans la barre d'état. Remarque pour ceux que ça intéresse : mon pick est réalisé géométriquement en lançant un rayon passant par la caméra et le curseur. Les objets correspondant à la position du curseur sont triés du plus proche de la caméra au plus lointain, et leurs noms sont affichés séparés par des tirets : "objet3 - sphere1 - toto" par exemple.

Le pick sert à déterminer quels objets peuvent être sélectionnés ou retirés de la sélection si l'utilisateur applique la commande adéquate (par exemple un clic gauche). Parmi les objets détectés, certains sont déjà sélectionnés (sur mon image c'est le cas de celui en fil de fer, "pf12") et d'autres non, certains peuvent aussi être masqués dans la vue 3D. Comment connaître alors ceux qui font partie de la sélection courante ? En affichant leurs noms dans une couleur différente, comme dans cette barre d'état (qui correspond à l'image précédente) :

Les objets sélectionnés sont en bleu, les crochets indiquent l'objet sur lequel l'utilisateur va agir et la touche TAB permet de passer de l'un à l'autre.


Classe dérivée de CStatusBar

La première chose à faire est de dériver une classe de CStatusBar, par exemple CColoredStatusBar, à l'aide du ClassWizard ( Insert / New Class). CStatusBar n'apparaît pas dans la liste des classes de base, il faut donc dériver de CWnd (generic CWnd), avant de remplacer manuellement les "CWnd" par des "CStatusBar" dans le .h et le .cpp :

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

Afin d'utiliser cette nouvelle classe, il faut évidemment l'inclure dans MainFrm.h, et y modifier la déclaration de la barre d'état :

#include "ColoredStatusBar.h"

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

protected:  // control bar embedded members
  CColoredStatusBar  m_wndStatusBar;


Stockage des couleurs

Il y a plusieurs façons de stocker les différentes couleurs du texte qui sera affiché dans le premier panneau de la barre d'état :

- le faire dans une structure séparée du texte, indiquant quels caractères sont concernés par quelle couleur ;
- insérer des "marqueurs" dans le texte comme on met des "\n" dans un printf, et utiliser la chaîne de caractères déjà existante dans CStatusBar en la modifiant par m_wndStatusBar.SetPaneText(0,Text);
- utiliser le système des marqueurs sur une chaîne de caractères ajoutée à CColoredStatusBar ;
- etc...

J'ai choisi la 3ème méthode, de sorte que si les MFC modifient le texte du premier panneau pour une raison ou pour une autre, ma propre chaîne est conservée et je peux la relire. Un membre est donc ajouté à CColoredStatusBar :

public:
  CString  m_Text;

Les marqueurs peuvent prendre n'importe quelle forme et être très nombreux, pour cet exemple je décide d'utiliser "$INKblue$" et "$INKblack$" pour fixer la couleur du texte à bleu ou noir respectivement ; si le noir est la couleur par défaut, la chaîne "Hello $INKblue$World $INKblack$!" doit apparaître ainsi :
Hello World !

Au lieu de prédéfinir certaines couleurs (ici bleu et noir), on pourrait directement fournir les composantes RGB sous la forme "$INK0;0;255$" par exemple (pour le bleu).


Affichage du texte

Il est temps à présent d'afficher notre texte stocké dans CColoredStatusBar. Pour cela, nous devons surcharger la méthode OnDrawItem de cette classe "à la main", attention car il ne s'agit pas de la fonction correspondant au message WM_DRAWITEM (le prototype est différent).

dans le .h :
protected:
  void DrawColoredString (CDC& dc,const char* pszTxt);

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

dans le .cpp :
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);
  }

Les deux "if" testent s'il s'agit bien du panneau 0 et si le texte n'est pas vide, SetTextColor sélectionne le noir comme couleur par défaut. DrawColoredString se charge de rechercher les marqueurs dans la chaîne de caractères, d'afficher le texte qui les précède, puis le cas échéant de changer de couleur et de traiter la chaîne restante par un appel récursif.

Cela ne suffit pas tout à fait : il faut informer la barre d'état qu'elle doit appeler cette version de DrawItem pour le panneau 0, grâce à l'instruction :
m_wndStatusBar.GetStatusBarCtrl().SetText("", 0, SBT_OWNERDRAW);
Pour pouvoir accéder à m_wndStatusBar cette instruction doit être placée dans une méthode de la classe CMainFrame. Le plus simple est d'y ajouter une fonction telle que :

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


Utilisation

Tout est en place, il ne reste plus qu'à fournir à SetHelpText une chaîne contenant des marqueurs pour vérifier le bon fonctionnement. Dans le programme d'exemple (168 Ko), un double-clic sur la vue de l'application provoque l'exécution de :
((CMainFrame*)AfxGetMainWnd())->SetHelpText("Hello $INKblue$World $INKblack$!");

Notez que si vous parcourez ensuite les commandes des menus, leur description s'affiche correctement dans la barre d'état.


haut de la page