|
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...) |