Emulating CDialog in CDialogBar =============================== CDialogBar is not derived from CDialog. One is left in want when one wants to do CDialog programming in CDialogBar. In this lab we will learn how to program CDialog features into CDialogBar. This is a long lab. If you have any problems with the lab, start over from the beginning. You'll find out that you misread an instruction. Task 1 A new CDialogBar base class We start with a new CDialogBar base class. This base class will support DDX and dialog initialization. You don't instantiate this. Instead you derive your own CDialogBar from this base class and this derived class gets used in your CMainFrame. 1. Insert-> New Class... a. give a name, e.g. BaseDialogBar b. set base class to generic CWnd (toward the bottom). Sorry, you can't select CDialogBar as a base. 2. Go to BaseDialogBar.h a. change the base class from CWnd to CDialogBar. b. Add the following line just before your constructor. DECLARE_DYNCREATE(BaseDialogBar) Beware: botching this step may generate unhelpful error message from the compiler. 3. In BaseDialogBar.cpp, add this line before the constructor IMPLEMENT_DYNCREATE(BaseDialogBar, CDialogBar) Beware: botching this step may generate unhelpful error message from the compiler. 4. In BaseDialogBar.cpp, go to the line beginning with BEGIN_MESSAGE_MAP a. Change the CWnd parameter to CDialogBar. If you forget to do this, you'll generate an assertion from Dockcont.cpp 5. Try doing a compile. If you see any errors, correct them. 6. We need to override Create(), but we can't use ClassWizard. Remember that ClassWizard thinks our class is CWnd derived rather than CDialogBar derived. Manually, add the following member functions to your CDialogBar BOOL Create(CWnd * pParentWnd, UINT nIDTemplate, UINT nStyle, UINT nID); BOOL Create(CWnd * pParentWnd, LPCTSTR lpszTemplateName, UINT nStyle, UINT nID); 7. WM_INIT_DIALOG is not received in CDialogBar, so we can't use OnInitDialog(). In lieu of OnInitDialog(), you'll add member function virtual BOOL OnInitDialogBar(); There's a rumor that WM_INIT_DIALOG is received, but the rumor says it is received in debug mode not release mode. 8. Using ClassWizard, add DoDataExchange(). DoDataExchange() can be added via ClassWizard because it's a CWnd function. 9. Just for safety add ASSERT(pDX) to your DoDataExchange() We have now added 4 member functions to our class. Now we have to define them. 10. In both of your Create() functions, call CDialogBar::Create() followed by a call to OnInitDialogBar(). We call OnInitDialogBar() because CDialogBar doesn't receive a WM_INIT_DIALOG. Your code should look something like this. { // Let MFC Create the control by calling CDialogBar::Create() if(!CDialogBar::Create(pParentWnd, lpszTemplateName, /*Instead of lpszTemplateName, use MAKEINTRESOURCE(nIDTemplate) for other Create()*/ nStyle, nID)) { return FALSE; } // Since there is no WM_INITDIALOG message we have to manually // call our own InitDialog function if(!OnInitDialogBar()) return FALSE; return TRUE; } Don't forget to code the second Create() function. Don't forget to use MAKEINTRESOURCE instead of lpszTemplateName. 11. In your OnInitDialogBar(), call UpdateData(FALSE) and return TRUE. Calling UpdateData(FALSE) will cause DoDataExchange() to be called. DoDataExchange() will then sync the data to the controls. Calling UpdateData(FALSE) is also the default behavior of OnInitDialog(). 12. As the last line of your OnInitDialogBar(), return TRUE. I'm not sure why it's true. I'm just following the documentation for OnInitDialog() and the documentation isn't very clear why it's true. 13. Compile your code, and correct any errors. Notice that DoDataExchange() and OnInitDialogBar() don't do much. These are virtual functions that will be overridden when we derive from this class. Once we derive, then we'll define the functions to support dialog controls. Task 2 Deriving your CDialogBar 1. Re-use steps 1 to 5 of Task 1 to create a derived dialog bar inheriting from your base dialog bar. For the purpose of this lab, we'll assume that you named your derived class, DerivedDialogBar. Hint: do not inherit from CDialogbar. You are inheriting from your base dialog bar, not CDialogBar Task 3 Adding your derived CDialogBar to your application 1. Create a dialog template in your resource editor. If you don't know how to do this, we'll review the steps here. a. Insert->Resource b. Whatever you do, do not expand dialog! I repeat, do not expand dialog! Now click on the text, "Dialog", but do not touch the expansion box. If it expanded, you did it wrong. c. Click button New. You've got a dialog. 2. Right click on the diagram to bring up the diagram properties a. Under styles tab panel, set style to be child b. Set border to none c. I hope you left the visible checkbox turned off. It must be turned off. If not, you'll set off a strange assert. Just to make sure, look under More Styles tab panel. Visible should be OFF. 3. Add a member variable of your dialog bar class to CMainFrame, e.g. DerivedDialogBar m_wndDialogBar; 4. In CMainFrame::OnCreate(), you'll add to the bottom some code that looks something like this. m_wndDialogBar.Create(this, IDD_DIALOG1, WS_CHILD|WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC, IDD_DIALOG1); m_wndDialogBar.EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndDialogBar, AFX_IDW_DOCKBAR_LEFT); Don't forget to replace IDD_DIALOG1 with the id for your dialog. 5. Compile your project. Fix any errors. 6. Run your project. You'll notice that controls in your derived dialog bar are disabled. We'll fix that later. Task 4 Add handlers to a CDialogBar derived class. Normally, CDialogBar's handlers are located in CMainFrame. This creates a tight coupling between CDialogBar handlers and an application. This can be a bad thing as the handlers are not very portable. You probably didn't notice this in the previous steps, but somewhere there you made it possible to put handlers in your CDialogBar. You can still add your handlers to CMainFrame if you wish. In this section, we'll study the steps necessary to add handlers to your derived CDialogBar class. 1. Let's say you need a handler for your OK button. You'll need two functions for this, the handler function and an update function. The update function is there to enable your window control, i.e. OK button. To your dialog bar class, add a handler and update functions to the AFX_MSG section like so. //{{AFX_MSG(DerivedDialogBar) afx_msg void OnIDOK(); // handler function afx_msg void OnUpdateButtonOK(CCmdUI* pCmdUI); //}}AFX_MSG This may seem odd, but MFC initially disables all controls in CDialogBar. That's why you need the update function to manually enable the controls. 2. In your OnIDOK(), add some code like maybe AfxMessageBox(_T("On button handler called")); This will popup a message every time our OK button is called. 3. In your OnUpdateButtonOk(), add nothing. Nothing, really? That's right, the mere presence of the update function is enough to enable your control. Later, if you decide to switch back forth between enable and disable, you would add such code to your update function. 4. In your message map, a. add a ON_BN_CLICKED call to add your handler b. add a ON_UPDATE_COMMAND_UI call to add your update function. For example, BEGIN_MESSAGE_MAP(BaseDialogBar, CDialogBar) //{{AFX_MSG_MAP(BaseDialogBar) // NOTE - the ClassWizard will add and remove mapping macros here. ON_BN_CLICKED(IDOK, OnIDOK) ON_UPDATE_COMMAND_UI(IDOK, OnUpdateButtonOK) //}}AFX_MSG_MAP END_MESSAGE_MAP() Alternatively, you can substitute ON_BN_CLICKED WITH ON_COMMAND ON_COMMAND(IDOK, OnIDOK) If you want to be fancy, you can use ON_COMMAND_EX and ON_COMMAND_RANGE. Read up on these in the MSDN when you have an opportunity. 5. Compile and run your project. Your control should be operational. Task 5 Alternative way of adding handlers Sometimes it's preferable to put your handlers in CMainFrame, e.g. handlers need to affect the view. Here's how to add handlers to your CMainFrame. 1. Add your handler function to CMainFrame. See previous task if you don't know how to do this. 2. Add your handler to the message map. See previous task if you don't know how to do this. You don't need the OnUpdateCmdUI. Just by adding your handler to the message map, the button will be enabled. Task 6 Alternative way of enabling CDialogBar controls If you know beforehand that all controls are unconditionally enabled, there's a shortcut you can use to enable controls in your CDialogBar class. You can do away with all the update functions, and replace them with only one member function. void BaseDialogBar::OnUpdateCmdUI(CFrameWnd* pTarget, BOOL bDisableIfNoHndler) { bDisableIfNoHndler = FALSE; CDialogBar::OnUpdateCmdUI(pTarget,bDisableIfNoHndler); } This doesn't need AFX_MSG or AFX_MSG_MAP. Just add this one member function to your CDialogBar-derived class. Task 7 Adding a control variable Let's say that you have a CEdit in your dialog and you need the corresponding control member variable. Normally, you go to ClassWizard's member variable tab panel to add such a variable. Unfortunately, ClassWizard can't add add variables to dialog bars, so we'll have to do this manually. 1. Using ClassWizard, override the DoDataExchange() function. ClassWizard can do this because DoDataExchange() is a CWnd function. 2. In DoDataExchange, add this for safety ASSERT(pDX); 3. In your derived dialog bar header file, you'll have to manually code an AFX_DATA section and add your CEdit variable in this manner. public: // Dialog Data //{{AFX_DATA(DerivedDialogBar) enum { IDD = IDD_DIALOG1 }; CEdit m_edit; //}}AFX_DATA More control variables can be added in the AFX_DATA section as needed. Don't forget to change IDD_DIALOG1 to your dialog id 4. Your m_edit needs to be coupled to the diagram. Let's say your diagram's CEdit resource ID is IDC_EDIT1. In DoDataExchange(), you'll have to manually code an AFX_DATA_MAP section in this manner //{{AFX_DATA_MAP(DerivedDialogBar) DDX_Control(pDX, IDC_EDIT1, m_edit); //}}AFX_DATA_MAP Other control variables are added in AFX_DATA_MAP as needed. At this point, m_edit is now a usable control variable. You can call functions on m_edit and the corresponding effect will propagate to IDC_EDIT on your dialog. 5. Just to test this, try adding this code to your OK button handler m_edit.SetLimitText(2); When the user presses okay, this code will limit the edit field to 2 bytes of inputs. 6. Run your program and verify that the edit field is indeed limited to 2 bytes after you click on the ok button. Task 8 Add a value variable and DDX value initialization Maybe you want to add a value variable instead of a control variable. No problem. Adding a value variable follows similar steps. 1. Let's say that you want to add a member variable for a check box. The data type for a check box value variable is BOOL. Add a variable declaration like so BOOL m_check; 2. Add your m_check to the AFX_DATA section. 3. Next we will add m_check to the AFX_DATA_MAP section, but wait we can't use use DDX_Control(). DDX_Control() is used for adding control variables, not value variables. To add a value variable, you'll have to pick from a mountain of choices, e.g. DDX_Text, DDX_Check, DDX_Slider, DDX_Radio, DDX_Scroll, etc. To pick the right one, you'll have to create a scratch dialog and observe which DDX function ClassWizard chooses when creating a value variable. For a check box, I found out that ClassWizard uses DDX_Check. Use DDX_Check() to map IDC_CHECK1 to m_check. 4. Let's test m_check. Currently, Create() calls virtual function OnInitDialogBar() which in turn calls UpdateData(FALSE). As you remember UpdateData(FALSE) syncs your values to the controls. a. In your constructor, set m_check to FALSE. b. Run your program, you'll see that the check box is unchecked c. In your constructor, set m_check to TRUE. d. Run your program, you'll see that the check box is checked. Task 9 Initialization of your dialog controls To set up an initialization function for a "normal dialog", you would use ClassWizard to add WM_INITDIALOG. Unfortunately, the ClassWizard provides no such service to CDialogBar. To add initialization to your derived CDialogBar, you'll need the following steps. 1. Override OnInitDialogBar() 2. In OnInitDialogBar(), your first line of code should call your base dialog's OnInitDialogBar(), e.g. BaseDialogBar::OnInitDialogBar(); This is necessary because the base OnInitDialogBar() calls Update(FALSE). If you forget to call the aforementioned code, you'll get a cryptic assert from code elsewhere in your OnInitDialogBar(). 3. The last line of your OnInitDialogBar() should return TRUE. 4. Add the code for the initialization of your controls.