An Example: PersonalRecordDialog

team bbl


An Example: PersonalRecordDialog

As we saw in the previous chapter, dialogs come in two flavors: modal and modeless. We'll illustrate custom dialog creation with a modal dialog because it's the more common kind and has fewer complications. The application will invoke the dialog with ShowModal and then query the dialog for user selections. Until ShowModal returns, all user interactions with the application will be contained within the little world of your custom dialog (and any further modal dialogs that your dialog may invoke).

Many of the steps involved in creating a custom dialog can be accomplished very easily by using a dialog editor, such as wxDesigner or DialogBlocks. The amount of coding left to do depends on the complexity of your dialog. Here, we will assume handcrafting of all the code in order to demonstrate the principles, but it's highly recommended that you use a tool to help you because it will save you many hours of repetitive work.

We'll illustrate the steps involved in creating a custom dialog with a simple example where the user is required to enter his or her name, age, sex, and whether the user wants to vote. This dialog is called PersonalRecordDialog, as shown in Figure 9-1.

Figure 9-1. Personal record dialog under Windows


The Reset button restores all controls to their default values. The OK button dismisses the dialog and returns wxID_OK from ShowModal. The Cancel button returns wxID_CANCEL and does not update the dialog's variables from the values shown in the controls. The Help button invokes a few lines of text describing the dialog (although in a real application, this button should invoke a nicely formatted help file).

A good user interface should not allow the user to enter data that has no meaning in the current context. In this example, the user should not be able to use the Vote control if Age is less than the voting age (18 in the U.S. or U.K.). So, we will ensure that when the age entered is less than 18, the Vote check box is disabled.

Deriving a New Class

Here's the declaration for our PersonalRecordDialog. We provide run-time type information by using DECLARE_CLASS, and we add an event table with DECLARE_EVENT_TABLE.

 /*!  * PersonalRecordDialog class declaration  */ class PersonalRecordDialog: public wxDialog {     DECLARE_CLASS( PersonalRecordDialog )     DECLARE_EVENT_TABLE() public:     // Constructors     PersonalRecordDialog( );     PersonalRecordDialog( wxWindow* parent,       wxWindowID id = wxID_ANY,       const wxString& caption = wxT("Personal Record"),       const wxPoint& pos = wxDefaultPosition,       const wxSize& size = wxDefaultSize,       long style = wxCAPTION|wxRESIZE_BORDER|wxSYSTEM_MENU );     // Initialize our variables     void Init();     // Creation     bool Create( wxWindow* parent,       wxWindowID id = wxID_ANY,       const wxString& caption = wxT("Personal Record"),       const wxPoint& pos = wxDefaultPosition,       const wxSize& size = wxDefaultSize,       long style = wxCAPTION|wxRESIZE_BORDER|wxSYSTEM_MENU );     // Creates the controls and sizers     void CreateControls(); }; 

Note that we follow wxWidgets convention by allowing both one-step and two-step constructionwe provide a default constructor and Create function as well as a more verbose constructor.

Designing Data Storage

We have four pieces of data to store: name (string), age (integer), sex (boolean), and voting preference (boolean). To make it easier to use a wxChoice control with the data, we're going to use an integer to store the boolean value for sex, but the class interface can present it as boolean: TRue for female and false for male. Let's add these data members and accessors to the PersonalRecordDialog class:

 // Data members wxString    m_name; int         m_age; int         m_sex; bool        m_vote; // Name accessors void SetName(const wxString& name) { m_name = name; } wxString GetName() const { return m_name; } // Age accessors void SetAge(int age) { m_age = age; } int GetAge() const { return m_age; } // Sex accessors (male = false, female = true) void SetSex(bool sex) { sex ? m_sex = 1 : m_sex = 0; } bool GetSex() const { return m_sex == 1; } // Does the person vote? void SetVote(bool vote) { m_vote = vote; } bool GetVote() const { return m_vote; } 

Coding the Controls and Layout

Now let's add a CreateControls function to be called from Create. CreateControls adds wxStaticText controls, wxButton controls, a wxSpinCtrl, a wxTextCtrl, a wxChoice, and a wxCheckBox. Refer to Figure 9-1 earlier in the chapter to see the resulting dialog.

We're using sizer-based layout for this dialog, which is why it looks a bit more involved than you might expect for a small number of controls. (We described sizers in Chapter 7, "Window Layout Using Sizers"briefly, they enable you to create dialogs that look good on any platform and that easily adapt to translation and resizing.) You can use a different method if you want, such as loading the dialog from a wxWidgets resource file (XRC file).

The basic principle of sizer-based layout is to put controls into nested boxes (sizers), which can distribute space among the controls or stretch just enough to contain its controls. The sizers aren't windowsthey form a separate hierarchy, and the controls remain children of their parent, regardless of the complexity of the hierarchy of sizers. You might like to refresh your memory by looking at the schematic view of a sizer layout that we showed in Figure 7-2 in Chapter 7.

In CreateControls, we're using a vertical box sizer (boxSizer) nested in another vertical box sizer (topSizer) to give a decent amount of space around the dialog's controls. A horizontal box sizer is used for the wxSpinCtrl, wxChoice, and wxCheckBox, and a second horizontal box sizer (okCancelSizer) is used for the Reset, OK, Cancel, and Help buttons.

[View full width]

/*! * Control creation for PersonalRecordDialog */ void PersonalRecordDialog::CreateControls() { // A top-level sizer wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); this->SetSizer(topSizer); // A second box sizer to give more space around the controls wxBoxSizer* boxSizer = new wxBoxSizer(wxVERTICAL); topSizer->Add(boxSizer, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5); // A friendly message wxStaticText* descr = new wxStaticText( this, wxID_STATIC, wxT("Please enter your name, age and sex, and specify whether you wish to\nvote in a general election."), wxDefaultPosition, wxDefaultSize, 0 ); boxSizer->Add(descr, 0, wxALIGN_LEFT|wxALL, 5); // Spacer boxSizer->Add(5, 5, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5); // Label for the name text control wxStaticText* nameLabel = new wxStaticText ( this, wxID_STATIC, wxT("&Name:"), wxDefaultPosition, wxDefaultSize, 0 ); boxSizer->Add(nameLabel, 0, wxALIGN_LEFT|wxALL, 5); // A text control for the user's name wxTextCtrl* nameCtrl = new wxTextCtrl ( this, ID_NAME, wxT("Emma"), wxDefaultPosition, wxDefaultSize, 0 ); boxSizer->Add(nameCtrl, 0, wxGROW|wxALL, 5); // A horizontal box sizer to contain age, sex and vote wxBoxSizer* ageSexVoteBox = new wxBoxSizer(wxHORIZONTAL); boxSizer->Add(ageSexVoteBox, 0, wxGROW|wxALL, 5); // Label for the age control wxStaticText* ageLabel = new wxStaticText ( this, wxID_STATIC, wxT("&Age:"), wxDefaultPosition, wxDefaultSize, 0 ); ageSexVoteBox->Add(ageLabel, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5); // A spin control for the user's age wxSpinCtrl* ageSpin = new wxSpinCtrl ( this, ID_AGE, wxEmptyString, wxDefaultPosition, wxSize(60, -1), wxSP_ARROW_KEYS, 0, 120, 25 ); ageSexVoteBox->Add(ageSpin, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5); // Label for the sex control wxStaticText* sexLabel = new wxStaticText ( this, wxID_STATIC, wxT("&Sex:"), wxDefaultPosition, wxDefaultSize, 0 ); ageSexVoteBox->Add(sexLabel, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5); // Create the sex choice control wxString sexStrings[] = { wxT("Male"), wxT("Female") }; wxChoice* sexChoice = new wxChoice ( this, ID_SEX, wxDefaultPosition, wxSize(80, -1), WXSIZEOF(sexStrings), sexStrings, 0 ); sexChoice->SetStringSelection(wxT("Female")); ageSexVoteBox->Add(sexChoice, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5); // Add a spacer that stretches to push the Vote control // to the right ageSexVoteBox->Add(5, 5, 1, wxALIGN_CENTER_VERTICAL|wxALL, 5); wxCheckBox* voteCheckBox = new wxCheckBox( this, ID_VOTE, wxT("&Vote"), wxDefaultPosition, wxDefaultSize, 0 ); voteCheckBox ->SetValue(true); ageSexVoteBox->Add(voteCheckBox, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5); // A dividing line before the OK and Cancel buttons wxStaticLine* line = new wxStaticLine ( this, wxID_STATIC, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); boxSizer->Add(line, 0, wxGROW|wxALL, 5); // A horizontal box sizer to contain Reset, OK, Cancel and Help wxBoxSizer* okCancelBox = new wxBoxSizer(wxHORIZONTAL); boxSizer->Add(okCancelBox, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5); // The Reset button wxButton* reset = new wxButton( this, ID_RESET, wxT("&Reset"), wxDefaultPosition, wxDefaultSize, 0 ); okCancelBox->Add(reset, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5); // The OK button wxButton* ok = new wxButton ( this, wxID_OK, wxT("&OK"), wxDefaultPosition, wxDefaultSize, 0 ); okCancelBox->Add(ok, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5); // The Cancel button wxButton* cancel = new wxButton ( this, wxID_CANCEL, wxT("&Cancel"), wxDefaultPosition, wxDefaultSize, 0 ); okCancelBox->Add(cancel, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5); // The Help button wxButton* help = new wxButton( this, wxID_HELP, wxT("&Help"), wxDefaultPosition, wxDefaultSize, 0 ); okCancelBox->Add(help, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5); }

Data Transfer and Validation

Now we have the bare controls of the dialog, but the controls and the dialog's data are not connected. How do we make that connection?

When a dialog is first shown, wxWidgets calls InitDialog, which in turn sends a wxEVT_INIT_DIALOG event. The default handler for this event calls transferDataToWindow on the dialog. To transfer data from the controls back to the variables, you can call transferDataFromWindow when the user confirms his or her input. Again, wxWidgets does this for you by defining a default handler for wxID_OK command events, which calls transferDataFromWindow before calling EndModal to dismiss the dialog.

So, you can override transferDataToWindow and transferDataFromWindow to transfer your data. For our dialog, the code might look like this:

 /*!  * Transfer data to the window  */ bool PersonalRecordDialog::TransferDataToWindow() {     wxTextCtrl* nameCtrl = (wxTextCtrl*) FindWindow(ID_NAME);     wxSpinCtrl* ageCtrl = (wxSpinCtrl*) FindWindow(ID_SAGE);     wxChoice* sexCtrl = (wxChoice*) FindWindow(ID_SEX);     wxCheckBox* voteCtrl = (wxCheckBox*) FindWindow(ID_VOTE);     nameCtrl->SetValue(m_name);     ageCtrl->SetValue(m_age);     sexCtrl->SetSelection(m_sex);     voteCtrl->SetValue(m_vote);     return true; } /*!  * Transfer data from the window  */ bool PersonalRecordDialog::TransferDataFromWindow() {     wxTextCtrl* nameCtrl = (wxTextCtrl*) FindWindow(ID_NAME);     wxSpinCtrl* ageCtrl = (wxSpinCtrl*) FindWindow(ID_SAGE);     wxChoice* sexCtrl = (wxChoice*) FindWindow(ID_SEX);     wxCheckBox* voteCtrl = (wxCheckBox*) FindWindow(ID_VOTE);     m_name = nameCtrl->GetValue();     m_age = ageCtrl->GetValue();     m_sex = sexCtrl->GetSelection();     m_vote = voteCtrl->GetValue();     return true; } 

However, there's an easier way of transferring data. wxWidgets supports validators, which are objects that link data variables and their corresponding controls. Although not always applicable, the use of validators where possible will save you a lot of time and can make it unnecessary to write transferData ToWindow and transferDataFromWindow functions. In our PersonalRecordDialog example, we can use the following code instead of the previous two functions:

 FindWindow(ID_NAME)->SetValidator(       wxTextValidator(wxFILTER_ALPHA, & m_name)); FindWindow(ID_AGE)->SetValidator(       wxGenericValidator(& m_age)); FindWindow(ID_SEX)->SetValidator(       wxGenericValidator(& m_sex); FindWindow(ID_VOTE)->SetValidator(       wxGenericValidator(& m_vote); 

These few lines of code at the end of CreateControls replace the two overridden functions. As a bonus, the user will be prevented from accidentally entering numbers in the Name field.

Validators can perform two jobsas well as data transfer, they can validate the data and show error messages if the data doesn't conform to a particular specification. In this example, no actual validation of the input is done, other than for the name. wxGenericValidator is a relatively simple class, only doing data transfer. However, it works with the basic control classes. The other validator provided as standard, wxTextValidator, has more sophisticated behavior and can even intercept keystrokes to veto invalid characters. In the example, we just use the standard style wxFILTER_ALPHA, but we could also specify which characters should or should not be regarded as valid by using the validator's SetIncludes and SetExcludes functions.

We need to dig a bit deeper into how wxWidgets handles validators in order to understand what's going on here. As we've seen, the default OnOK handler calls TRansferDataToWindow, but before it does so, it calls Validate, vetoing the calls to transferDataToWindow and EndModal if validation fails. This is the default implementation of OnOK:

 void wxDialog::OnOK(wxCommandEvent& event) {     if ( Validate() && TransferDataFromWindow() )     {         if ( IsModal() )             EndModal(wxID_OK); // If modal         else         {             SetReturnCode(wxID_OK);             this->Show(false); // If modeless         }     } } 

The default implementation of Validate iterates through all the children of the dialog (and their descendants, if you specified the extra window style wxWS_EX_VALIDATE_RECURSIVELY), calling Validate for each control's wxValidator object. If any of these calls fails, then validation for the dialog fails, and the dialog is not dismissed. The validator is expected to show a suitable error message from within its Validate function if it fails the validation.

Similarly, transferDataToWindow and TRansferDataFromWindow will be called automatically for the validators of a dialog's controls. A validator must do data transfer, but validation is optional.

A validator is an event handler, and the event processing mechanism will route events to the validator, if present, before passing the events on to the control. This enables validators to intercept user inputfor example, to veto characters that are not permitted in a control. Such vetoing should normally be accompanied by a beep to inform the user that the key was pressed but not accepted.

Because the two provided validator classes may not be sufficient for your needs, especially if you write your own custom controls, you can derive new validator classes from wxValidator. This class should have a copy constructor and a Clone function that returns a copy of the validator object, as well as implementations for data transfer and validation. A validator will typically store a pointer to a C++ variable, and the constructor may take flags to specify modes of use. You can look at the files include/wx/valtext.h and src/common/valtext.cpp in wxWidgets to see how a validator can be implemented; see also "Writing Your Own Controls" in Chapter 12, "Advanced Window Classes."

Handling Events

In this example, wxWidgets' default processing for OK and Cancel are sufficient without any extra coding on our part, as long as we use the standard wxID_OK and wxID_CANCEL identifiers for the controls. However, for non-trivial dialogs, you probably will have to intercept and handle events from controls. In our example, we have a Reset button, which can be clicked at any time to reset the dialog back to its default values. We add an OnResetClick event handler and a suitable entry in our event table. Implementing OnResetClick turns out to be very easy; first we reset the data variables by calling the Init function we added to centralize data member initialization. Then we call transferDataToWindow to display that data.

 BEGIN_EVENT_TABLE( PersonalRecordDialog, wxDialog )     ...     EVT_BUTTON( ID_RESET, PersonalRecordDialog::OnResetClick)     ... END_EVENT_TABLE() void PersonalRecordDialog::OnResetClick( wxCommandEvent& event ) {     Init();     TransferDataToWindow(); } 

Handling UI Updates

One of the challenges faced by the application developer is making sure that the user can't click on controls and menus that are not currently applicable. A sure sign of sloppy programming is the appearance of messages that say, "This option is not currently available." If an option isn't available, then it should not look available, and clicking on the control or menu should do nothing. As time-consuming as it can be, the programmer should update the elements of the interface to reflect the context at every instant.

In our example, we must disable the Vote check box when the user's age is less than 18 because in that case, the decision is not available to the user. Your first thought might be to add an event handler for the Age spin control and enable or disable the Vote check box according to the spin control's value. Although this may be fine for simple user interfaces, imagine what happens when many factors are influencing the availability of controls. Even worse, there are some cases where the approach doesn't work at all because you cannot be notified when the change occurs. An example of this situation is when you need to enable a Paste button or menu item when data becomes available on the clipboard. This event is outside your power to intercept because the data may become available from another program.

To solve these problems, wxWidgets provides an event class called wxUpdateUIEvent that it sends to all windows in idle timethat is, when the event loop has finished processing all other input. You can add EVT_UPDATE_UI event table entries to your dialog, one for each control whose state you need to maintain. Each UI update event handler evaluates the current state of the world and calls functions in the event object (not the control) to enable, disable, check, or uncheck the control. This technique puts the logic for updating each control in one place, calling the event handler even when no real event has been handled in the application. You can breathe a sigh of relief because you don't have to remember to update the user interface after any change that might happen to be relevant!

Here's our UI update handler for the Vote control. Note that we can't use the m_age variable because transfer from the controls to the variables doesn't happen until the user clicks OK.

 BEGIN_EVENT_TABLE( PersonalRecordDialog, wxDialog )     ...     EVT_UPDATE_UI( ID_VOTE, PersonalRecordDialog::OnVoteUpdate )     ... END_EVENT_TABLE() void PersonalRecordDialog::OnVoteUpdate( wxUpdateUIEvent& event ) {     wxSpinCtrl* ageCtrl = (wxSpinCtrl*) FindWindow(ID_AGE);     if (ageCtrl->GetValue() < 18)     {         event.Enable(false);         event.Check(false);     }     else         event.Enable(true); } 

Don't worry unduly about efficiency considerations; plenty of spare cycles are available for processing these handlers. However, if you have a very complex application and run into performance problems, see the wxUpdateUIEvent documentation for the functions SetMode and SetUpdateInterval that can be used to decrease the time wxWidgets spends processing these events.

Adding Help

There are at least three kinds of help you can provide for your dialog:

  • Tooltips

  • Context-sensitive help

  • Online help

You can probably think of further techniques not explicitly supported by wxWidgets. We already have some descriptive text on the dialog described here. For a more complex dialog, you could create a wxHtmlWindow instead of a wxStaticText and load an HTML file containing further details. Alternatively, a small help button could be placed next to each control to show a description when clicked.

The three main types of help supported by wxWidgets are described in the following sections.

Tooltips

Tooltips are little windows that pop up when the pointer is hovering over a control, containing a short description of the control's purpose. You call SetToolTip to set the tooltip text for a control. Because this can get annoying for experienced users, you should provide an application setting to switch this off (that is, SetToolTip will not be called when dialogs are created and displayed).

Context-Sensitive Help

Context-sensitive help provides a short pop-up description similar to a tooltip. The user must first click on a special button and then on a control to get the help or press F1 to get help for the focused control (on Windows). On Windows, you can specify the extra window style wxDIALOG_EX_CONTEXTHELP to create the little question mark button on the dialog title. On other platforms, you can create a wxContextHelpButton on the dialog (usually next to the OK and Cancel buttons). In your application initialization, you should call

 #include "wx/cshelp.h"     wxHelpProvider::Set(new wxSimpleHelpProvider); 

This tells wxWidgets how to provide the strings for context-sensitive help. You call SetHelpText to set the help text for a control. Here's a function to add context-sensitive help and tooltips to our dialog:

 // Sets the help text for the dialog controls void PersonalRecordDialog::SetDialogHelp() {     wxString nameHelp = wxT("Enter your full name.");     wxString ageHelp = wxT("Specify your age.");     wxString sexHelp = wxT("Specify your gender, male or female.");     wxString voteHelp = wxT("Check this if you wish to vote.");     FindWindow(ID_NAME)->SetHelpText(nameHelp);     FindWindow(ID_NAME)->SetToolTip(nameHelp);     FindWindow(ID_AGE)->SetHelpText(ageHelp);     FindWindow(ID_AGE)->SetToolTip(ageHelp);     FindWindow(ID_SEX)->SetHelpText(sexHelp);     FindWindow(ID_SEX)->SetToolTip(sexHelp);     FindWindow(ID_VOTE)->SetHelpText(voteHelp);     FindWindow(ID_VOTE)->SetToolTip(voteHelp); } 

If you want to invoke context-sensitive help yourself, as opposed to letting the dialog or wxContextHelpButton handle it, you can simply put this in an event handler:

 wxContextHelp contextHelp(window); 

This will put wxWidgets in a loop that detects a left-click on a control, after which it will send a wxEVT_HELP event to the control to initiate popping up a help window.

You don't have to limit yourself to the way wxWidgets implements the storage and display of help text, though. You can create your own class derived from wxHelpProvider, overriding GetHelp, SetHelp, AddHelp, RemoveHelp, and ShowHelp.

Online Help

Most applications come with a help file that provides detailed instructions for use. wxWidgets provides the means to control several kinds of help windows through different derivations of the wxHelpControllerBase class. See Chapter 20, "Perfecting Your Application," for more information about providing online help.

For the purposes of this example, we'll just use a wxMessageBox to display some help when the user clicks on the Help button.

 BEGIN_EVENT_TABLE( PersonalRecordDialog, wxDialog )     ...     EVT_BUTTON( wxID_HELP, PersonalRecordDialog::OnHelpClick )     ... END_EVENT_TABLE() void PersonalRecordDialog::OnHelpClick( wxCommandEvent& event ) {     // Normally we would wish to display proper online help.     /*     wxGetApp().GetHelpController().DisplaySection(wxT("Personal record dialog"));      */     // For this example, we're just using a message box.     wxString helpText =       wxT("Please enter your full name, age and gender.\n")       wxT("Also indicate your willingness to vote in general elections.\n\n")       wxT("No non-alphabetical characters are allowed in the name field.\n")       wxT("Try to be honest about your age.");     wxMessageBox(helpText,         wxT("Personal Record Dialog Help"),         wxOK|wxICON_INFORMATION, this); } 

The Complete Class

The complete implementation of the dialog is listed in Appendix J, "Code Listings," and can also be found in examples/chap09 on the CD-ROM.

Invoking the Dialog

Now that we have the dialog completely coded, we can invoke it:

 PersonalRecordDialog dialog(NULL, ID_PERSONAL_RECORD,     wxT("Personal Record")); dialog.SetName(wxEmptyString); dialog.SetAge(30); dialog.SetSex(0); dialog.SetVote(true); if (dialog.ShowModal() == wxID_OK) {     wxString name = dialog.GetName();     int age = dialog.GetAge();     bool sex = dialog.GetSex();     bool vote = dialog.GetVote(); } 

    team bbl



    Cross-Platform GUI Programming with wxWidgets
    Cross-Platform GUI Programming with wxWidgets
    ISBN: 0131473816
    EAN: 2147483647
    Year: 2005
    Pages: 262

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net