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.