EWin Tutorial - Custom Controls

Custom Controls? Sounds cool... How do I make a custom control for my dialog?

A custom control is a GUI control like a button or a listbox or an editbox, except it isn't any of these -- it is a new control that you make up yourself. Hence, it is "custom". To make a custom control, you derive from TCustomControl. You draw the control in Paint(), and handle mouse messages in OnMouseMsg(), and you use it pretty much just like you would use a TWin. But if you want to put it onto a dialog, you will need to go an extra step.You need to give it a unique class name. Overload GetClassName() to return a unique string. Make it easy to figure out, since you're going to have to type this string into the MSVC dialog editor whenever you want to place one of these custom controls.

Here is a sample custom control. It just draws a blue square.

 
#include <control.h>

class TBlueSquare : public TCustomControl
{
  public:
  TBlueSquare(TWin *PWindow, const TWinAttr &Changes)
    : TCustomControl(PWindow, Changes)
  { }

  virtual void Paint(TPaintDC &DC)
  {
    TRect r;
    r = GetEffectiveClientRect();
    DC.SetBrush(CLR_BLUE);
    DC.Rectangle(r);
  }

  virtual const char *GetClassName () const
  {
    return "TBlueSquare";
  }
};

You'll notice I've used the helpful mnemonic aid of making the class-name string be the same as the C++ class name. I could have returned the literal "Ugly Blue Square Of Doom", but then I would have to type that into a box in the next step, and I'd have to remember what custom control that string mapped to. This way, if I see a custom control on a dialog, I can tell where the code for that control is just by seeing the class name.

At the bottom of the MSVC 6 dialog editor's control toolbar is the picture of a little head. This is how you add a custom control to your dialog. So plop it down onto your dialog where you want it, then bring up its properties. Give it an ID name just like you would any other control on your dialog. And give it any of the different styles you want. And in the "Class" field of the dialog, type the string that you returned in GetClassName(). For instance, type "TBlueSquare". Without the quotation marks, of course.

And you're just about ready to go! The last thing to do is to make a TModalDlg object (or a TModelessDlg, if you prefer) for the dialog you just made, and give it an instance of the custom control.

Say I have a dialog resource named IDD_VERYBLUEDLG. And it has a TBlueSquare with the ID number of IDC_BLUESQUARE (this #define will, of course, be placed into resource.h for you by the editor). The dialog also has, oh, say, an edit box named IDC_EDITBOXY.

 
#include "resource.h"
#include <dlg.h>
#include <edit.h>
#include <bluesquare.h>  //this is where you put the blue square class 
                           //definition, from above

class TVeryBlueDlg : public TModalDlg
{
  public:
  TLineEdit EditBox;
  TBlueSquare BlueSquare;

  TVeryBlueDlg(TWin *PParent)
    : TModalDlg(PParent, IDD_VERYBLUEDLG),
      EditBox(this, TWinAttr().ID(IDC_EDITBOXY)),
      BlueSquare(this, TWinAttr().(IDC_BLUESQUARE))
  {
  }
};

And now you're set! Make one of these and voila, you'll see that the rectangle you chose in the dialog editor is now a pretty shade of blue.

So now you can make a more complicated custom control, and throw it into a dialog. You can even come up with your own style bits, just as if you were a built-in control. There won't be any check-boxes for your styles in the dialog, though -- just a place where you can type in a hex number. So you'll have to remember the values of the styles, which kind of blows. You are
allowed to use the lower 16 bits for styles. Thus you may use styles with values between 1 and 0x8000 inclusive.

Another thing to note: the custom-control GUI doesn't have checkboxes for all of the built-in styles! It has a checkbox for visible and disabled and whatnot, but no checkbox for the "border" style. WS_BORDER has the value of 0x00800000. So if you want this style, you must OR it into the "Style" field yourself. The same goes for any other WS_XXX styles that aren't present on the Custom Control dialog.


So that's how I make my own custom controls. But isn't there an alternate way to do all of this? (Owner-drawn controls)

There is an alternative, but it isn't quite as flexible, and it isn't even that much shorter. The alternative it to make a TStatic or a TBtn with the SS_USERITEM or BS_OWNERDRAW styles, respectively. These styles (and Microsoft alone knows why these styles have different names! Grr) tell the static or button to not draw itself, but rather, to let the control draw it. So the TStatic or TBtn's Paint() function will get called. Thus:

 
#include <btn.h>

class TBlueBox2 : public TBtn
{
  public:
  TBlueBox2( TWin *PParent, const TWinAttr &Changes)
    : TBtn(PParent, Changes)
  {
  }

  virtual void Paint(TPaintDC &DC)
  {
    TRect r;
    r = GetEffectiveClientRect();
    DC.SetBrush(CLR_BLUE);
    DC.Rectangle(r);
  }
};

Now, instead of using a Custom Control on your dialog, you plop down a button, and you make sure to check the "Owner Draw" checkbox in the dialog. And there you go, a button that gets drawn as a blue box. Whoohoo.

So when do you use this method over making your own control? I recommend using a TCustomControl if you're making an entirely new widget. If you're just making a fancy new button or checkbox or radio button, then use this method.

Oh, you may want to have a different appearance when the button is pressed. In fact you will almost certainly want the button to look different when it is pressed in. You can call TControl::bAppearSelected() to tell if the button is supposed to be selected. You can call TControl::bAppearFocused() to tell if the control is supposed to have the focus rectangle around it [the focus rectangle is that little gray rectangle that indicates that the keyboard focus is on that button]. You can call TControl::bAppearDisabled() to see if the button is supposed to look "grayed out".


So that's how I make an owner-drawn button. What about an owner-drawn listbox?

First let me explain, in case you are confused, about what "owner-drawn" means. It means that you want a button or listbox or combobox or whatever that behaves pretty much the same as a usual button or listbox or whatever, but it looks different. Maybe you want a listbox with icons in it, or a combobox with two lines of text in each item instead of one. All you are trying to change, though, is the appearance. You want Windows to still do all the dirty work of figuring out when the user selects an item or pushes the button or whatever. So this discussion is only relevant if you want a custom-drawn control -- if you just want a regular version, you don't need to go to this trouble.

So, how to do an owner-drawn listbox: you create a listbox, and give it the LBS_OWNERDRAWFIXED or the LBS_OWNERDRAWVARIABLE style. Now, you whip up a TListBox with a Paint() function. But there are two differences.

First, you need to tell the control the height of each item. Do this with TListBox::SetItemHeight(). If you use the LBS_OWNERDRAWFIXED style, which is the more common, you can just figure out the height you want in the object's
OnInit() function, and then call SetItemHeight() once.

[MORE] If you need to set the height of each item individually, you can either call SetItemHeight() over and over, or you can use a callback function: OnMeasureItem(). Overload this function and fill out the MEASUREITEMSTRUCT's itemHeight field as appropriate [using the struct's own itemID field to tell which item it wants the height for].

Ok! So you have set the size. But how do you draw it? Well, you overload Paint() again. But you will probably want to draw each item separately. So Paint() will get called lots of times in this special case. It will get called once for each item that needs to be painted. In the Paint() function, you can use IdxOfItemBeingDrawn() to get the index of the item being drawn. You can also use RectOfItemBeingDrawn() to get the rectangle of the item. And you can use the same functions you used for the owner-drawn button, bAppearSelected(), bAppearFocused(), and bAppearDisabled(), to tell exactly how the item should be drawn. Note that you may not care about all these styles -- you may not ever want to draw the focus-rectangle for your particular flavor of listbox. And you might not concern yourself with whether the item is disabled or not, if you know that the listbox will never be disabled.

So that's all there is to it. You whip up a Paint() function that draws a particular item. And now, for your entertainment, here is a nice owner-drawn listbox that draws items EXACTLY AS IF THE STOCK LISTBOX WAS USED. Why in the world would you want to have an owner-drawn listbox that draws things just as if it wasn't owner-drawn? You would never want this. But, it gives you a basis from which to make your own custom listboxes. For testing purposes, you'll want to change it a bit. Maybe set the colors to something else, so you can tell it's working.

 
#include <listbox.h>

class TVeryRealisticOwnerDrawnListBox : public TListBox
{
  public:
  TVeryRealisticOwnerDrawnListBox(TWin *PParent, const TWinAttr &Changes)
    : TListBox(PParent, Changes)
    {
    }

  virtual void OnInit()
    {
      //here we need to find the height of each item in the list
      TScreenDC DC;
      DC.SetFont(Font());
      TEXTMETRIC tm;
      GetTextMetrics(DC, &tm);
      SetItemHeight(0, tm.tmHeight);
      //now we've set the height to be the height of one line of text.
    }

  virtual void Paint(TPaintDC &DC)
    {
      TRect r = RectOfItemBeingDrawn();

      COLORREF BkColor, TextColor;

      if (bAppearSelected)
      {
        BkColor = CLR_HIGHLIGHT;
        TextColor = CLR_HIGHLIGHTTEXT;
      }
      else
      {
        BkColor = CLR_WINDOW;
        TextColor = CLR_WINDOWTEXT;
      }

      DC.SetFont(Font());
      DC.SetBrush(BkColor);
      DC.SetTextColor(TextColor);
      DC.SetBkColor(BkColor);
      DC.SetBkTransparent(true);

      DC.PatBlt(DC.InvalidRect());

      char text[512];
      GetItemText(IdxOfItemBeingDrawn(), text, sizeof(text));

      DC.TextOut(r.left, r.top, text);

    //Comment out the next two lines if you don't need a focus rect
    if (bAppearFocused())
      DC.OutlineRect(DC.InvalidRect());
  }
};

[Note that this is a lot of code to put into the body of a class. In real life I would put just the class definition and function prototypes in a header file, and then I would put the actual code for the functions in a separate CPP file. I just did it this way so you can see how it works more easily.]

Tada! Now, whip up a dialog template that has a listbox in it. Make sure the listbox has the LBS_OWNERDRAWFIXED style, as well as the bizarre but necessary LBS_HASSTRINGS style. Then make a TModalDlg (or a TModelessDlg) that uses that dialog resource, and have it make a TVeryRealisticOwnerDrawnListBox with the ID of the owner-drawn listbox in your resource. I'm not sure if you've figured out how that works or not. I better give another example in case you haven't.

So say you have a dialog named IDD_FANCYLISTDLG. And it has an owner-drawn listbox with the ID of ID_PRETTYLITTLELIST.

 
#include <dlg.h>
#include "myfancylistbox.h" //this is where I put the decl. for the listbox

class TFancyDlg : public TModalDlg
{
  public:
  TVeryRealisticOwnerDrawnListBox ListBox;

  TFancyDlg(TWin *PParent, const TWinAttr &Changes)
    : TModalDlg(PParent, Changes),
      ListBox(this, TWinAttr().ID( ID_PRETTYLITTLELIST ))
  {
  }

  virtual void OnInit()
  {
    //okay, since I bothered to show you how to set up the dialog, it ought
    //to do something. I'll have it add a few items to the listbox.
    ListBox.AddItem("This is item zero!");
    ListBox.AddItem("This is item one!");
  }

};

Now remember, the listbox in the dialog needs to have the LBS_OWNERDRAWFIXED and the LBS_HASSTRINGS styles. To make sure it's working, go and change TVeryRealisticOwnerDrawnListBox::Paint() so that you can tell if it's working.Maybe set the text color to blue or something.

Why does the LBS_HASSTRINGS style need to be used? Well that's an interesting story. See, when Windows notices the LBS_OWNERDRAWFIXED style (or the LBS_OWNERDRAWVARIABLE style), it goes, "Oho! Well, I can optimize this listbox by not storing any text for the strings! I am very clever. I was written in the eighties when string storage was expensive." So when you call ListBox.AddItem(), it would go, "Yeah, gotcha. Another item." And it would add another item, but the text for that item would be blank. It would just throw away your string. When you called ListBox.GetItemText(), it would return "", an empty string.

So to tell Windows not to throw away your string, you add in the LBS_HASSSTRINGS style. Note that you don't need this style if your listbox isn't owner-drawn; it only "optimizes" away your strings when it sees an owner-drawn listbox.

It can, in fact, be a slight optimization sometimes, if you are, say, drawing a listbox full of icons, and don't need any text. But more often than not, you need text.


And what about an owner-drawn combobox?

Damn near the same as an owner-drawn listbox. So similar, in fact, that I won't even give an example. You just use CBS_OWNERDRAWFIXED and CBS_OWNERDRAW and CBS_HASSTRINGS instead of the LBS_XXXX styles, and derive from TComboBox instead of TListBox, of course.


And what about an owner-drawn listview?

(A listview is a control that can be in any of four modes: big icon, small icon, list, and report. It is the control that is on the right-hand-side pane of the Explorer.)

It's damn near the same as an owner-drawn listbox, but not quite as good. You see, in an owner-drawn listbox or combobox, you can call SetItemHeight() to set the height of the items. But a listview doesn't happen to have this function! Dumb, dumb, dumb!

So then you'd use the other way, overloading OnMeasureItem(). But due to stupidity on the part of Windows programmers, OnMeasureItem() will get called too soon in a dialog -- it will get called before the wrapper object is in place to catch it. In other words, your OnMeasureItem() will never get called if you put an owner-drawn listview in a dialog. (It will get called fine if you create an owner-drawn listview on the fly.) So how do you get an owner-drawn listview in a dialog? You... don't. Sorry. Maybe they'll fix this with IE 6.

Well, you *can* get an owner-drawn listview in a dialog, you just have to create it after the fact. Your dialog resource can't have the listview -- instead, you have to create the TListView in the dialog's OnInit().

 

 (go on to the next page in the tutorial...)