13-CommunicatingControls

advertisement
Communicating Controls
Consider again a simple program that draws a circle of a certain
radius. We want to offer the following dialog to control the radius:
There is an edit box in which you can type a new radius. There is
a spin control next to the edit box, which you can use to increase
or decrease the number showing in the edit box. There is a slider
control that you can use to adjust the radius. Our aim is to get the
slider to reflect changes made using the edit box, and to get the edit
box to reflect changes made using the slider.
To prepare for following this example, add to the doc class an int
variable m_radius, and in OnDraw put code to draw a red circle in
the center of the window, with radius pDoc->m_radius. Design
the above dialog. (You can set the caption of the dialog in its
property sheet. To see the property sheet, right-click somewhere
away from the controls.) When you drag controls to the dialog,
drag the spin control immediately after you drag the edit box, so it
will get an ID number just one more than the edit box. You’ll see
why later.
Right-click your dialog in the dialog editor, and add a class
RadiusDialog. Add two member variables m_edit, m_slider of
category value and a member variable m_sliderControl of
category control. Don’t specify a minimum value on your value
variables—in class I will do so and show you why you don’t want
that.
Put code into OnRButtonDown to bring up this dialog using
DoModal, as we have done in previous lectures; initialize both
m_edit and m_slider to pDoc->m_radius, and when DoModal
returns IDOK, set pDoc->m_radius = m_edit.
Now the edit box should work correctly, as in a previous lecture.
But the spin control and the slider do not work, let alone
communicate with the edit box. Now today’s work begins.
The function UpdateData. This is a member function of the
CDialog class. It is called with argument TRUE or FALSE. It
causes the data in the controls to be synchronized with the date in
the member variables of category value. Specifically:
UpdateData(FALSE) sets the values in the controls to be equal to
the values in the member variables. UpdateData(TRUE) sets the
values in the member variables to be equal to the values in the
controls at that time. These functions are called automatically at
the beginning and end of DoModal; so DoModal begins with an
UpdateData(FALSE), setting the controls to whatever initial
values the member variables were given before the call to
DoModal, and when the dialog is dismissed, DoModal calls
UpdataData(TRUE) before exiting. Note, it does this whether the
dialog is exited by OK or by Cancel, which is why you must test
the return value. You do not need to call UpdateData yourself for
a simple dialog without communicating controls. But for
communicating controls, it will be essential.
NotificationMessages are sent by controls (such as edit boxes and
sliders) to their parent window (usually a dialog box) when their
state changes. The edit box will send a message when its text
changes. We must handle that message and change the position of
the slider to correspond to the text. For this we will need control
variables for both the slider and the edit box, as well as value
variables for both controls.
So, the plan is: when the edit text changes, update m_edit
accordingly, and then make m_slider = m_edit, and then call
UpdateData(FALSE) to get the slider position to visually reflect
the new value of m_slider.
Right-click the class RadiusDialog (that’s the name of our dialog
class—yours may differ) in Class View. Select its property sheet
and click the lightning-bolt button whose tooltip says Events.
Scroll down to IDC_EDIT1 (your edit box) and expand the node:
You will see a list of the notification messages this kind of control
can send. Note that the text box at the bottom gives a one-line
description of what each message means. Select the
EN_CHANGE message. (EN = Edit Notification). Add the
handler method that is suggested.
Write the code according to our plan:
void CWidth::OnChangeEditwidth()
{
UpdateData(TRUE); // make m_edit and m_slider reflect
values shown
m_slider = m_width;
UpdateData(FALSE); // change the slider position
}
Now, when you type a different number in the edit box, the slider
adjusts.
Adjusting the editbox when the slider changes.
What message will the slider send when its position changes? You
don't see an appropriate message when you select the slider as we
did the edit box. So, we use help to look things up. (To get this
help, just put the cursor on CSlider and press F1, then select
notification messages.)
A slider control notifies its parent window of user actions by sending
the parent WM_HSCROLL or WM_VSCROLL messages, depending
on the orientation of the slider control. To handle these messages,
add handlers for the WM_HSCROLL and WM_VSCROLL messages
to the parent window. The OnHScroll and OnVScroll member
functions will be passed a notification code, the position of the slider,
and a pointer to the CSliderCtrl object. Note that the pointer is of type
CScrollBar * even though it points to a CSliderCtrl object. You may
need to typecast this pointer if you need to manipulate the slider
control.
Aha, the information we need is in the first sentence! We must
map the WM_HSCROLL message. Note that this is a Windows
message, not an “event” or notification code, so you use the button
to the right of the lightning bolt.
void CWidth::OnHScroll(UINT nSBCode, UINT nPos,
CScrollBar* pScrollBar)
{
CSliderCtrl*p = (CSliderCtrl *) pScrollBar;
UpdateData(TRUE);
m_edit = m_slider;
UpdateData(FALSE);
CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}
This code does the job! Now the slider and edit box communicate
perfectly while the dialog runs.
Why doesn't this code cause an infinite loop? You might worry
that if the edit box changes, then that causes the slider to change,
which in turn causes the edit box to change, etc. But luckily,
WM_SCROLL is only sent when the user changes the slider
position, not when the program does so.
Setting the Slider Range
By default the slider range is 0 to 100. Let’s set it to 0 to 200 to
allow bigger circles. This has to be done in OnInitDialog, since
you can’t do it before the CSlider object actually exists and is
attached to an underlying Win32 slider.
Your RadiusDialog class doesn’t have an OnInitDialog method; it
just inherits it from CDialog. But now, we need to override that
method. Go to the property sheet of the RadiusDialog class and
click the button whose tooltip says Overrides. (two to the right of
the lightning bolt). Scroll down to OnInitDialog and click Add.
Make the code look like this (only the middle line has to be added).
BOOL RadiusDialog::OnInitDialog()
{
CDialog::OnInitDialog();
m_sliderControl.SetRange(0,200);
return TRUE;
}
Making the Spin Button Control Work
The spin control is used with an edit box. The edit box is called
the “buddy” of the spin control. Set the Auto Buddy and Set
Buddy Integer properties of the spin control to True:
How does “auto buddy” know what control is the “buddy” of the
spin control? It uses the “tab order”. This is the order in which
the tab key will cause the cursor to visit the controls. To see or
change it, choose Layout | Tab Order from the menu. You will
see blue numbers on the controls in the Dialog Editor. You can
click the controls in the desired new order. The original order is
the order in which you added the controls to the dialog. You must
ensure that the spin control comes next after the edit control. That
is why I asked you to drag the spin control to the dialog right after
the edit control.
Now, we’ll need a member variable of category control m_spinner.
Add that variable, build the program, and try to run it. What
happens? It crashes!! All you did was add a variable, not even
one line of code, to a working program. How can that cause a
crash?
Dialog Initialization
When you have communicating controls in a dialog, you may
experience a crash at dialog initialization. I will explain the reason
and the cure.
Example. You have an edit box and a spin control. Your code
responds to changed text in the edit box by calling one of the spin
control’s methods, for example GetPos.
The problem. When the dialog is initialized, the text in the edit
box changes (it is set from the associated variable). So, the spin
control’s method is called. But does the spin control exist yet? If
not, the pointer to the spin control will be NULL and the attempt
to call its GetPos method will be an illegal memory access, and
will cause a crash. Such a crash can happen even if you don’t call
the spinner’s methods explicitly, as MFC does call them for data
exchange. (That’s what happens in the example in the lecture.)
Since the controls are initialized in the tab order, you will be OK
if the spin control precedes the edit control in the tab order. But if
you want the edit control first in tab order, or if the dialog is more
complex and has loops of communicating controls, that won’t
work.
The cure. Add a member variable m_initialized to your dialog
class, of type int. It will automatically be initialized to 0. Set it to 1
at the end of OnInitDialog. Put all your code that calls the
methods of the controls, e.g. of the slider, inside if(m_initialized ).
Of course you could use TRUE and FALSE (which are #-defined
to 0 and 1). Or you could make the variable a bool and use true
and false.
Now Finish the Example
In OnInitDialog, set the range of the spin control:
BOOL RadiusDialog::OnInitDialog()
{
CDialog::OnInitDialog();
m_initialized = 1;
m_sliderControl.SetRange(0,200);
m_spinner.SetRange(0,200);
return TRUE;
}
Now everything works! Observe that the slider also responds to the
spinner, since when the edit box text changes, a notification message
is sent.
Download