Home | All Classes | Main Classes | Annotated | Grouped Classes | Functions

Presenting the GUI

The chart application

The chart application provides access to options via menus and toolbar buttons arranged around a central widget, a CanvasView, in a conventional document-centric style.

(Extracts from chartform.h.)

    class ChartForm: public QMainWindow
    {
        Q_OBJECT
    public:
        enum { MAX_ELEMENTS = 100 };
        enum { MAX_RECENTFILES = 9 }; // Must not exceed 9
        enum ChartType { PIE, VERTICAL_BAR, HORIZONTAL_BAR };
        enum AddValuesType { NO, YES, AS_PERCENTAGE };

        ChartForm( const QString& filename );
        ~ChartForm();

        int chartType() { return m_chartType; }
        void setChanged( bool changed = TRUE ) { m_changed = changed; }
        void drawElements();

        QPopupMenu *optionsMenu; // Why public? See canvasview.cpp

    protected:
        virtual void closeEvent( QCloseEvent * );

    private slots:
        void fileNew();
        void fileOpen();
        void fileOpenRecent( int index );
        void fileSave();
        void fileSaveAs();
        void fileSaveAsPixmap();
        void filePrint();
        void fileQuit();
        void optionsSetData();
        void updateChartType( QAction *action );
        void optionsSetFont();
        void optionsSetOptions();
        void helpHelp();
        void helpAbout();
        void helpAboutQt();
        void saveOptions();

    private:
        void init();
        void load( const QString& filename );
        bool okToClear();
        void drawPieChart( const double scales[], double total, int count );
        void drawVerticalBarChart( const double scales[], double total, int count );
        void drawHorizontalBarChart( const double scales[], double total, int count );

        QString valueLabel( const QString& label, double value, double total );
        void updateRecentFiles( const QString& filename );
        void updateRecentFilesMenu();
        void setChartType( ChartType chartType );

        QPopupMenu *fileMenu;
        QAction *optionsPieChartAction;
        QAction *optionsHorizontalBarChartAction;
        QAction *optionsVerticalBarChartAction;
        QString m_filename;
        QStringList m_recentFiles;
        QCanvas *m_canvas;
        CanvasView *m_canvasView;
        bool m_changed;
        ElementVector m_elements;
        QPrinter *m_printer;
        ChartType m_chartType;
        AddValuesType m_addValues;
        int m_decimalPlaces;
        QFont m_font;
    };

We create a ChartForm subclass of QMainWindow. Our subclass uses the Q_OBJECT macro to support Qt's signals and slots mechanism.

The public interface is very small; the type of chart being displayed can be retrieved, the chart can be marked 'changed' (so that the user will be prompted to save on exit), and the chart can be asked to draw itself (drawElements()). We've also made the options menu public because we are also going to use this menu as the canvas view's context menu.

The QCanvas class is used for drawing 2D vector graphics. The QCanvasView class is used to present a view of a canvas in an application's GUI. All our drawing operations take place on the canvas; but events (e.g. mouse clicks) take place on the canvas view.

Each action is represented by a private slot, e.g. fileNew(), optionsSetData(), etc. We also have quite a number of private functions and data members; we'll look at all these as we go through the implementation.

For the sake of convenience and compilation speed the chart form's implementation is split over three files, chartform.cpp for the GUI, chartform_canvas.cpp for the canvas handling and chartform_files.cpp for the file handling. We'll review each in turn.

The Chart Form GUI

(Extracts from chartform.cpp.)

    #include "images/file_new.xpm"
    #include "images/file_open.xpm"
    #include "images/options_piechart.xpm"

All the images used by chart have been created as .xpm files which we've placed in the images subdirectory.

The Constructor

    ChartForm::ChartForm( const QString& filename )
        : QMainWindow( 0, 0, WDestructiveClose )
...
        QAction *fileNewAction;
        QAction *fileOpenAction;
        QAction *fileSaveAction;

For each user action we declare a QAction pointer. Some actions are declared in the header file because they need to be referred to outside of the constructor.

Most user actions are suitable as both menu items and as toolbar buttons. Qt allows us to create a single QAction which can be added to both a menu and a toolbar. This approach ensures that menu items and toolbar buttons stay in sync and saves duplicating code.

        fileNewAction = new QAction(
                "New Chart", QPixmap( file_new ),
                "&New", CTRL+Key_N, this, "new" );
        connect( fileNewAction, SIGNAL( activated() ), this, SLOT( fileNew() ) );

When we construct an action we give it a name, an optional icon, a menu text, and an accelerator short-cut key (or 0 if no accelerator is required). We also make it a child of the form (by passing this). When the user clicks a toolbar button or clicks a menu option the activated() signal is emitted. We connect() this signal to the action's slot, in the snippet shown above, to fileNew().

The chart types are all mutually exclusive: you can have a pie chart or a vertical bar chart or a horizontal bar chart. This means that if the user selects the pie chart menu option, the pie chart toolbar button must be automatically selected too, and the other chart menu options and toolbar buttons must be automatically unselected. This behaviour is achieved by creating a QActionGroup and placing the chart type actions in the group.

        QActionGroup *chartGroup = new QActionGroup( this ); // Connected later
        chartGroup->setExclusive( TRUE );

The action group becomes a child of the form (this) and the exlusive behaviour is achieved by the setExclusive() call.

        optionsPieChartAction = new QAction(
                "Pie Chart", QPixmap( options_piechart ),
                "&Pie Chart", CTRL+Key_I, chartGroup, "pie chart" );
        optionsPieChartAction->setToggleAction( TRUE );

Each action in the group is created in the same way as other actions, except that the action's parent is the group rather than the form. Because our chart type actions have an on/off state we call setToggleAction(TRUE) for each of them. Note that we do not connect the actions; instead, later on, we will connect the group to a slot that will cause the canvas to redraw.

Why haven't we connected the group straight away? Later in the constructor we will read the user's options, one of which is the chart type. We will then set the chart type accordingly. But at that point we still won't have created a canvas or have any data, so all we want to do is toggle the canvas type toolbar buttons, but not actually draw the (at this point non-existent) canvas. After we have set the canvas type we will connect the group.

Once we've created all our user actions we can create the toolbars and menu options that will allow the user to invoke them.

        QToolBar* fileTools = new QToolBar( this, "file operations" );
        fileTools->setLabel( "File Operations" );
        fileNewAction->addTo( fileTools );
        fileOpenAction->addTo( fileTools );
        fileSaveAction->addTo( fileTools );
...
        fileMenu = new QPopupMenu( this );
        menuBar()->insertItem( "&File", fileMenu );
        fileNewAction->addTo( fileMenu );
        fileOpenAction->addTo( fileMenu );
        fileSaveAction->addTo( fileMenu );

Toolbar actions and menu options are easily created from QActions.

As a convenience to our users we will restore the last window position and size and list their recently used files. This is achieved by writing out their settings when the application is closed and reading them back when we construct the form.

        QSettings settings;
        settings.insertSearchPath( QSettings::Windows, WINDOWS_REGISTRY );
        int windowWidth = settings.readNumEntry( APP_KEY + "WindowWidth", 460 );
        int windowHeight = settings.readNumEntry( APP_KEY + "WindowHeight", 530 );
        int windowX = settings.readNumEntry( APP_KEY + "WindowX", -1 );
        int windowY = settings.readNumEntry( APP_KEY + "WindowY", -1 );
        setChartType( ChartType(
                settings.readNumEntry( APP_KEY + "ChartType", int(PIE) ) ) );
        m_font = QFont( "Helvetica", 18, QFont::Bold );
        m_font.fromString(
                settings.readEntry( APP_KEY + "Font", m_font.toString() ) );
        for ( int i = 0; i < MAX_RECENTFILES; ++i ) {
            QString filename = settings.readEntry( APP_KEY + "File" +
                                                   QString::number( i + 1 ) );
            if ( !filename.isEmpty() )
                m_recentFiles.push_back( filename );
        }
        if ( m_recentFiles.count() )
            updateRecentFilesMenu();

The QSettings class handles user settings in a platform-independent way. We simply read and write settings, leaving QSettings to handle the platform dependencies. The insertSearchPath() call does nothing except under Windows so does not have to be #ifdefed.

We use readNumEntry() calls to obtain the chart form's last size and position, providing default values if this is the first time it has been run. The chart type is retrieved as an integer and cast to a ChartType enum value. We create a default label font and then read the "Font" setting, using the default we have just created if necessary.

Although QSettings can handle string lists we've chosen to store each recently used file as a separate entry to make it easier to hand edit the settings. We attempt to read each possible file entry ("File1" to "File9"), and add each non-empty entry to the list of recently used files. If there are one or more recently used files we update the File menu by calling updateRecentFilesMenu(); (we'll review this later on).

        connect( chartGroup, SIGNAL( selected(QAction*) ),
                 this, SLOT( updateChartType(QAction*) ) );

Now that we have set the chart type (when we read it in as a user setting) it is safe to connect the chart group to our updateChartType() slot.

        resize( windowWidth, windowHeight );
        if ( windowX != -1 || windowY != -1 )
            move( windowX, windowY );

And now that we know the window size and position we can resize and move the chart form's window accordingly.

        m_canvas = new QCanvas( this );
        m_canvas->resize( width(), height() );
        m_canvasView = new CanvasView( m_canvas, &m_elements, this );
        setCentralWidget( m_canvasView );
        m_canvasView->show();

We create a new QCanvas and set its size to that of the chart form window's client area. We also create a CanvasView (our own subclass of QCanvasView) to display the QCanvas. We make the canvas view the chart form's main widget and show it.

        if ( !filename.isEmpty() )
            load( filename );
        else {
            init();
            m_elements[0].set( 20, red,    14, "Red" );
            m_elements[1].set( 70, cyan,    2, "Cyan",   darkGreen );
            m_elements[2].set( 35, blue,   11, "Blue" );
            m_elements[3].set( 55, yellow,  1, "Yellow", darkBlue );
            m_elements[4].set( 80, magenta, 1, "Magenta" );
            drawElements();
        }

If we have a file to load we load it; otherwise we initialise our elements vector and draw a sample chart.

        statusBar()->message( "Ready", 2000 );

It is vital that we call statusBar() in the constructor, since the call ensures that a status bar is created for this main window.

init()

    void ChartForm::init()
    {
        setCaption( "Chart" );
        m_filename = QString::null;
        m_changed = FALSE;

        m_elements[0]  = Element( Element::INVALID, red );
        m_elements[1]  = Element( Element::INVALID, cyan );
        m_elements[2]  = Element( Element::INVALID, blue );
...

We use an init() function because we want to initialise the canvas and the elements (in the m_elements ElementVector) when the form is constructed, and also whenever the user loads an existing data set or creates a new data set.

We reset the caption and set the current filename to QString::null. We also populate the elements vector with invalid elements. This isn't necessary, but giving each element a different color is more convenient for the user since when they enter values each one will already have a unique color (which they can change of course).

The File Handling Actions

okToClear()

    bool ChartForm::okToClear()
    {
        if ( m_changed ) {
            QString msg;
            if ( m_filename.isEmpty() )
                msg = "Unnamed chart ";
            else
                msg = QString( "Chart '%1'\n" ).arg( m_filename );
            msg += "has been changed.";

            int x = QMessageBox::information( this, "Chart -- Unsaved Changes",
                                              msg, "&Save", "Cancel", "&Abandon",
                                              0, 1 );
            switch( x ) {
                case 0: // Save
                    fileSave();
                    break;
                case 1: // Cancel
                default:
                    return FALSE;
                case 2: // Abandon
                    break;
            }
        }

        return TRUE;
    }

The okToClear() function is used to prompt the user to save their values if they have any unsaved data. It is used by several other functions.

fileNew()

    void ChartForm::fileNew()
    {
        if ( okToClear() ) {
            init();
            drawElements();
        }
    }

When the user invokes the fileNew() action we call okToClear() to give them the opportunity to save any unsaved data. If they either save or abandon or have no unsaved data we re-initialise the elements vector and draw the default chart.

Should we also have invoked optionsSetData() to pop up the dialog through which the user can create and edit values, colors etc? You could try running the application as it is, and then try it having added a call to optionsSetData() and see which you prefer.

fileOpen()

    void ChartForm::fileOpen()
    {
        if ( !okToClear() )
            return;

        QString filename = QFileDialog::getOpenFileName(
                                QString::null, "Charts (*.cht)", this,
                                "file open", "Chart -- File Open" );
        if ( !filename.isEmpty() )
            load( filename );
        else
            statusBar()->message( "File Open abandoned", 2000 );
    }

We check that it is okToClear(). If it is we use the static QFileDialog::getOpenFileName() function to get the name of the file the user wishes to load. If we get a filename we call load().

fileSaveAs()

    void ChartForm::fileSaveAs()
    {
        QString filename = QFileDialog::getSaveFileName(
                                QString::null, "Charts (*.cht)", this,
                                "file save as", "Chart -- File Save As" );
        if ( !filename.isEmpty() ) {
            int answer = 0;
            if ( QFile::exists( filename ) )
                answer = QMessageBox::warning(
                                this, "Chart -- Overwrite File",
                                QString( "Overwrite\n\'%1\'?" ).
                                    arg( filename ),
                                "&Yes", "&No", QString::null, 1, 1 );
            if ( answer == 0 ) {
                m_filename = filename;
                updateRecentFiles( filename );
                fileSave();
                return;
            }
        }
        statusBar()->message( "Saving abandoned", 2000 );
    }

This function calls the static QFileDialog::getSaveFileName() to get the name of the file to save the data in. If the file exists we use a QMessageBox::warning() to notify the user and give them the option of abandoning the save. If the file is to be saved we update the recently opened files list and call fileSave() (covered in File Handling) to perform the save.

Managing a list of Recently Opened Files

        QStringList m_recentFiles;

We hold the list of recently opened files in a string list.

    void ChartForm::updateRecentFilesMenu()
    {
        for ( int i = 0; i < MAX_RECENTFILES; ++i ) {
            if ( fileMenu->findItem( i ) )
                fileMenu->removeItem( i );
            if ( i < int(m_recentFiles.count()) )
                fileMenu->insertItem( QString( "&%1 %2" ).
                                        arg( i + 1 ).arg( m_recentFiles[i] ),
                                      this, SLOT( fileOpenRecent(int) ),
                                      0, i );
        }
    }

This function is called (usually via updateRecentFiles()) whenever the user opens an existing file or saves a new file. For each file in the string list we insert a new menu item. We prefix each filename with an underlined number from 1 to 9 to support keyboard access (e.g. Alt+F, 2 to open the second file in the list). We give the menu item an id which is the same as the index position of the item in the string list, and connect each menu item to the fileOpenRecent() slot. The old file menu items are deleted at the same time by going through each possible recent file menu item id. This works because the other file menu items had ids created by Qt (all of which are < 0); whereas the menu items we're creating all have ids >= 0.

    void ChartForm::updateRecentFiles( const QString& filename )
    {
        if ( m_recentFiles.find( filename ) != m_recentFiles.end() )
            return;

        m_recentFiles.push_back( filename );
        if ( m_recentFiles.count() > MAX_RECENTFILES )
            m_recentFiles.pop_front();

        updateRecentFilesMenu();
    }

This is called when the user opens an existing file or saves a new file. If the file is already in the list it simply returns. Otherwise the file is added to the end of the list and if the list is too large (> 9 files) the first (oldest) is removed. updateRecentFilesMenu() is then called to recreate the list of recently used files in the File menu.

    void ChartForm::fileOpenRecent( int index )
    {
        if ( !okToClear() )
            return;

        load( m_recentFiles[index] );
    }

When the user selects a recently opened file the fileOpenRecent() slot is called with the menu id of the file they have selected. Because we made the file menu ids equal to the files' index positions in the m_recentFiles list we can simply load the file indexed by the menu item id.

Quiting

    void ChartForm::fileQuit()
    {
        if ( okToClear() ) {
            saveOptions();
            qApp->exit( 0 );
        }
    }

When the user quits we give them the opportunity to save any unsaved data (okToClear()) then save their options, e.g. window size and position, chart type, etc., before terminating.

    void ChartForm::saveOptions()
    {
        QSettings settings;
        settings.insertSearchPath( QSettings::Windows, WINDOWS_REGISTRY );
        settings.writeEntry( APP_KEY + "WindowWidth", width() );
        settings.writeEntry( APP_KEY + "WindowHeight", height() );
        settings.writeEntry( APP_KEY + "WindowX", x() );
        settings.writeEntry( APP_KEY + "WindowY", y() );
        settings.writeEntry( APP_KEY + "ChartType", int(m_chartType) );
        settings.writeEntry( APP_KEY + "AddValues", int(m_addValues) );
        settings.writeEntry( APP_KEY + "Decimals", m_decimalPlaces );
        settings.writeEntry( APP_KEY + "Font", m_font.toString() );
        for ( int i = 0; i < int(m_recentFiles.count()); ++i )
            settings.writeEntry( APP_KEY + "File" + QString::number( i + 1 ),
                                 m_recentFiles[i] );
    }

Saving the user's options using QSettings is straight-forward.

Custom Dialogs

We want the user to be able to set some options manually and to create and edit values, value colors, etc.

    void ChartForm::optionsSetOptions()
    {
        OptionsForm *optionsForm = new OptionsForm( this );
        optionsForm->chartTypeComboBox->setCurrentItem( m_chartType );
        optionsForm->setFont( m_font );
        if ( optionsForm->exec() ) {
            setChartType( ChartType(
                    optionsForm->chartTypeComboBox->currentItem()) );
            m_font = optionsForm->font();
            drawElements();
        }
        delete optionsForm;
    }

The form for setting options is provided by our custom OptionsForm covered in Setting Options. The options form is a standard "dumb" dialog: we create an instance, set all its GUI elements to the relevant settings, and if the user clicked "OK" (exec() returns a true value) we read back settings from the GUI elements.

    void ChartForm::optionsSetData()
    {
        SetDataForm *setDataForm = new SetDataForm( &m_elements, m_decimalPlaces, this );
        if ( setDataForm->exec() ) {
            m_changed = TRUE;
            drawElements();
        }
        delete setDataForm;
    }

The form for creating and editing chart data is provided by our custom SetDataForm covered in Taking Data. This form is a "smart" dialog. We pass in the data structure we want to work on, and the dialog handles the presentation of the data structure itself. If the user clicks "OK" the dialog will update the data structure and exec() will return a true value. All we need to do in optionsSetData() if the user changed the data is mark the chart as changed and call drawElements() to redraw the chart with the new and updated data.

« Mainly Easy | Contents | Canvas Control »


Copyright © 2003 TrolltechTrademarks
Qt version 3.2.0b2