Introduction
Sometimes the controls in a dialog box interact with each other. For example in the standard Color selection dialog box, the settings for Hue, Saturation and Luminosity are related to the settings for Red, Green and Blue. One way to code this interaction is to put code into member routines for the class associated with the dialog box. But another way is to put the related controls into a custom control. This handles the interactions between the various controls internally, and communicates the final result of these interactions, but not the interactions themselves, up to the parent dialog box. The custom control is then better encapsulated, and does not require any help from its parent dialog box to make it work properly.
Background
My situation was that, I was writing an MFC application to play Midi files, and one of the things I wanted a user to be able to do was specify the place to start playing either as a bar number, or in minutes and seconds from the start. So at first I put a slider control, an up/down spin control, and an edit control into a dialog box. The user could drag the slider to specify the time from the start, or spin up or down to select the bar number. In both cases the bar number would appear in the edit control. And when the user spun the spinner the slider would move accordingly. As the piece of music was played, the bar number would change and the "time from start" slider would move.
But then I decided to move these controls into a different dialog box, and I realized that I would need to move the code that glued the controls together, from the class for one dialog box to the class for the other dialog box. But if I combined the controls into a single custom control first, then all I would need to do was move a single control from one dialog box to another. The glue code could stay where it was.
Interacting controls in a single custom control
The example given here is a custom control, which contains 2 sliders, 2 up/down spinners, and 2 edit controls giving the current positions of the sliders. Drag one slider and the other slider moves in the opposite direction. Spin one spinner up or down and one slider moves with it, the other slider moves in the opposite direction again.
The custom control communicates the position of one of its sliders to its parent dialog box. In this example the parent has its own slider that moves to show this communication. And the parent can set the position for the custom control, whereupon the custom control sets one of its sliders accordingly, the other slider taking a complimentary position, with the edit controls showing the positions.
In fact, the parent dialog box in this example has 2 independent custom controls of the type described above, each one has a corresponding slider in the parent dialog box. Just try running it and you'll see how the various controls interact.
Creating the custom control
This is described here (thanks Chris!). In fact it's easier than that, because there's no need to implement OnPaint()
or OnEraseBkgnd()
(this custom control consists only of Windows common controls and they already know how to draw themselves).
The custom control in this example has class CRH_CustomWnd
, which is derived from CWnd
. Its member function RegisterWindowClass()
, called from the constructor, registers the window class CRH_CustomWnd
with Windows.
The custom control is added to a dialog box using the Dialog Editor. In the control's properties the class name is CRH_CustomWnd
. In this example there are 2 of the custom controls in the IDD_CRH_TESTDIALOG
dialog box, and their style is 0x50810000 (the 0x00800000 bit gives them a border).
In the class for the dialog box, we declare a CRH_CustomWnd
(or 2 of them in this example) ...
class CRH_TestDialog : public CDialog
{
...
protected:
CRH_CustomWnd v1CRH_CustomWnd;
CRH_CustomWnd v2CRH_CustomWnd;
};
Inside the custom control
In this example the custom control contains 2 sliders, 2 up/down spinners, and 2 edit controls. So we declare the variables for these Windows common controls in the normal way.
class CRH_CustomWnd : public CWnd
{
...
private:
UINT NewPos1;
CSliderCtrl v1CSliderCtrl;
CEdit v1CEdit;
CSpinButtonCtrl v1CSpinButtonCtrl;
UINT NewPos2;
CSliderCtrl v2CSliderCtrl;
CEdit v2CEdit;
CSpinButtonCtrl v2CSpinButtonCtrl;
};
and we create the controls in CRH_CustomWnd::PreSubclassWindow()
.
void CRH_CustomWnd::PreSubclassWindow()
{
v1CSliderCtrl.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP,
CRect(10,10,110,60), this, SLIDER1);
v1CSliderCtrl.SetRange(MIN_POS,MAX_POS);
v1CSliderCtrl.ShowWindow(SW_SHOWNORMAL);
...
CWnd::PreSubclassWindow();
}
We need some handlers for the events that the controls will generate, UDN_DELTAPOS
for the spinners and WM_HSCROLL
for the sliders.
BEGIN_MESSAGE_MAP(CRH_CustomWnd, CWnd)
ON_WM_HSCROLL()
ON_NOTIFY(UDN_DELTAPOS, SPINNER1, OnDeltaposSPINNER1)
ON_NOTIFY(UDN_DELTAPOS, SPINNER2, OnDeltaposSPINNER2)
END_MESSAGE_MAP()
These handlers find out what's happened to their own control, set the other control, and call UpdateControls()
to update all the controls and inform the parent dialog box.
void CRH_CustomWnd::OnDeltaposSPINNER1(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_UPDOWN* pNMUpDown = (NM_UPDOWN*)pNMHDR;
int NewPos = (pNMUpDown->iPos + pNMUpDown->iDelta);
if (NewPos < MIN_POS)
NewPos = MIN_POS;
else
if (NewPos > MAX_POS)
NewPos = MAX_POS;
NewPos1 = NewPos;
NewPos2 = OtherPos(NewPos1);
UpdateControls();
*pResult = 1;
}
void CRH_CustomWnd::UpdateControls()
{
char buffer[100];
sprintf(buffer, "%d", NewPos1);
v1CSliderCtrl.SetPos(NewPos1);
v1CEdit.SetWindowText(buffer);
v1CSpinButtonCtrl.SetPos(NewPos1);
sprintf(buffer, "%d", NewPos2);
v2CSliderCtrl.SetPos(NewPos2);
v2CEdit.SetWindowText(buffer);
v2CSpinButtonCtrl.SetPos(NewPos2);
::PostMessage(GetParent()->m_hWnd, CRH_CUSTOMWND_VALUE_HAS_CHANGED,
NewPos1, GetDlgCtrlID());
}
And finally there's a handler for the parent's instruction to the custom control, to set itself to a particular value.
void CRH_CustomWnd::CRHSetPos(int NewPos)
{
NewPos1 = NewPos;
NewPos2 = OtherPos(NewPos1);
UpdateControls();
}
Shortcomings
I'll be interested to see anyone's comments on any of the following, or anything else.
- There's no keyboard interface to the controls within the custom control.
- The controls within the custom control are laid out by hand (see
CRH_CustomWnd::PreSubclassWindow()
above). It would be much more convenient if the Dialog Editor Window could be used, in the same sort of way as laying out the Windows common controls in a normal dialog box. I tried deriving CRH_CustomWnd
from CDialog
, but I couldn't get it to work.
- Is
CRH_CustomWnd::PreSubclassWindow()
the right place to call Create()
for the controls?
- Is there a better way of handling the
UDN_DELTAPOS
notifications from an up/down spinner than ON_NOTIFY(UDN_DELTAPOS, -, -)
(something perhaps more like ON_WM_HSCROLL()
)?
- In
CRH_CustomWnd::OnHScroll()
I've done a switch()
on the Control ID. Is it possible to specify different handlers in the AFX_MSG_MAP
for the same message from different controls?
- In
CRH_CustomWnd::UpdateControls()
, the message up to the parent dialog box contains the control ID of the custom control (got from GetDlgCtrlID()
). Is there some way that the parent dialog box can tell, from which of its controls, a message has come?
- I originally attempted to get the test dialog box to send a message to its custom controls to get them to set their positions, but the message didn't arrive. Why not? (I've left in the relevant code, commented out.) In the end I've had to provide a public member function in the custom control that the parent dialog box can call. I'm not sure if this is really acceptable.
- Would there be any benefit from using
DDX_Control()
in CRH_CustomWnd::DoDataExchange()
? How would this work?
Finally
If anyone's interested in seeing the original MidiPlay application that prompted this article, you can find it at http://mysite.freeserve.com/staplefordsingers (click on the MidiPlay.exe v1.06 link). There should be a better version of Midiplay there soon, incorporating the sort of custom controls described above.
History
- 30th August 2003 - first submitted.