主页 | 所有的类 | 主要的类 | 注释的类 | 分组的类 | 函数

实现图形用户界面

The chart application

chart程序提供了通过排列在中央窗口部件周围的菜单和工具条来访问选项,和一个通常的文档在中央的风格的CanvasView。

(由chartform.h展开。)

    class ChartForm: public QMainWindow
    {
        Q_OBJECT
    public:
        enum { MAX_ELEMENTS = 100 };
        enum { MAX_RECENTFILES = 9 }; // 必须不超过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; // 为什么是公有的?请看canvasview.cpp。

    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;
    };

我们创建了一个QMainWindow的子类ChartForm。我们的子类使用了Q_OBJECT宏来支持Qt的信号和槽机制。

公有接口是很少的,被显示的图表类型能够被追溯,图表可以被标记为“changed”(这样用户在退出的时候会被提示保存),并且图表可以要求拖拽自己(drawElements())。我们已经把选项菜单设为公有,因为我们也会把这个菜单作为画布视图的关联菜单。

QCanvas类用来绘制二维矢量图。QCanvasView类用来在一个应用程序的图形用户界面中实现一个画布的视图。我们所有的绘制操作都发生在画布上,但是事件(比如鼠标点击)却发生在画布视图中。

每一个动作都被一个私有槽实现,比如fileNew()optionsSetData()等等。我们也需要相当多的私有函数和数据成员,当我们执行这些实现的时候,我们来看看这些。

为了方便和编译速度的原因,图表视窗的实现被分为三个文件,chartform.cpp实现图形用户界面,chartform_canvas.cpp实现画布处理和chartform_files.cpp实现文件处理。我们会依次评论每一个。

图表视窗图形用户界面

(由chartform.cpp展开。)

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

chart中使用的所有图像是我们已经创建好并放在images子目录中的.xpm文件。

构造函数

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

对于每一个用户动作我们声明了一个QAction指针。一些动作在头文件中已经声明,因为它们需要在构造函数外被参考。

大部分用户动作适用于菜单条目和工具条按钮。Qt允许用户创建一个单一的QAction而被添加到菜单和工具条中。这种方法保证了菜单条目和工具条按钮处于同步状态并且可以节省代码。

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

当我们构造一个动作时,我们给它一个名字、一个可选的图标、一个菜单文本和一个加速快捷键(或者0如果不需要加速键)。我们也可以使它成为视窗的子对象(通过this)。当用户点击一个工具条按钮或者点击一个菜单选项时,activated()信号会被发射。我们把这个信号和这个动作的槽连接起来,就是上面的程序代码中提到的fileNew()。

图表类型是互斥的:我们可以用一个饼图一个竖直条形图一个水平条形图。这也就是说如果用户选择了饼图菜单选项,饼图工具条按钮也必须被自动地选中,并且其它图表菜单选项和工具条按钮必须被自动地取消选择。这种行为是通过创建一个QActionGroup来实现的并且把这些图表类型动作放到这个组中。

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

动作组成为了视窗(this)的子对象并且exlusive行为通过setExclusive()调用实现的。

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

组中的每一个动作都以和其它动作一样的方式创建,除了动作的父对象是组而不是视窗。因为我们的图表类型动作由开/关状态,我们为它们中的每一个调用setToggleAction(TRUE)。注意我们没有连接动作,相反,稍后我们会我们会把这个组连接到一个可以使画布重画的槽。

为什么我们不马上连接这个组呢?稍后在构造函数中我们将会读取用户选项,图表类型之一。我们将会直接设置图表类型。但那时我们还没有创建画布或者有任何数据,所以我们想做的一切就是切换画布类型工具条按钮,而不是真正地画(这时还不存在的)画布。在我们设置好画布类型之后,我们将会连接这个组。

一旦我们已经创建完所有的用户动作,我们就可以创建工具条和菜单选项来允许用户调用它们。

        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 );

工具条动作和菜单选项可以很容易地由QAction生成。

作为一个对我们的用户提供的方便,我们将会重新载入上次窗口的位置和大小并列出最近使用的文件。这是通过在程序退出的时候写出这些设置,在我们构造视窗的时候再把它们都回来实现的。

        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", 0 );
        int windowY = settings.readNumEntry( APP_KEY + "WindowY", 0 );
        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();

QSettings类通过和平台无关的方式来处理用户设置。我们很简单地读写设置,把处理平台依赖性的问题留给QSettings来处理。insertSearchPath()调用没有做任何事,除非在Windows下被#ifdef过。

我们使用readNumEntry()调用来得到图表视窗上次的大小和位置,并且为它的第一次运行提供了默认值。图表类型是以一个整数重新获得并把它扔给CharType枚举值。我们创建默认标签字体,然后读取“Font”设置,如果需要的话我们使用刚才生成的默认字体。

尽管QSettings可以处理字符串列表,但是我们已经选择把最近使用的每一个文件作为单一的条目来存储,这样就可以更容易地处理和编辑这些设置。我们试着去读每一个可能的文件条目(从“File1”到“File9”),并把每一个非空条目添加到最近使用的文件的列表中。如果有一个或多个最近使用的文件,我们通过调用updateRecentFilesMenu()来更新File菜单,(我们将会在稍后再评论这个)。

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

现在我们已经设置图表类型(当我们把它作为一个用户设置读入的时候),把图表组和我们的updateChartType()槽连接起来是安全的。

        resize( windowWidth, windowHeight );
        move( windowX, windowY );

并且现在我们已经知道窗口大小和位置,我们就可以根据这些重新定义大小并移动图表视窗窗口。

        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();

我们创建一个新的QCanvas并且设置它的大小为图表视窗窗口的客户区域。我们也创建一个CanvasView(我们自己的QCanvasView的子类)来显示QCanvas。我们把这个画布视图作为图表视窗的主窗口部件并显示它。

        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();
        }

如果我们有一个文件要载入,我们就载入它,否则我们就初始化我们的元素矢量并画一个示例图表。

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

我们在构造函数中调用statusBar()是非常重要的,因为这个调用保证了我们能够在这个主窗口中创建一个状态条。

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 );
...

我们使用了init()函数是因为我们想在视窗被构造的时候和无论用户载入一个存在的数据组或者创建一个新的数据组的时候初始化画布和元素(在m_elements ElementVector中)。

我们重新设置标题并设置当前文件名称为QString::null。我们也用无效的元素来组装元素矢量。这不是必需的,但是给每一个元素一个不同的颜色对于用户来讲是更方便的,因为当他们输入值的时候每一个都会已经有了一个确定的颜色(当然他们可以修改)。

文件处理动作

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.";
            switch( QMessageBox::information( this, "Chart -- Unsaved Changes",
                                              msg, "&Save", "Cancel", "&Abandon",
                                              0, 1 ) ) {
                case 0:
                    fileSave();
                    break;
                case 1:
                default:
                    return false;
                    break;
                case 2:
                    break;
            }
        }

        return true;
    }

okToClear()函数用来提示用户在有没保存的数据的时候保存它们。它也被其它几个函数使用。

fileNew()

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

当用户调用fileNew()动作时,我们调用okToClear()来给他们一个保存任何为保存的数据的机会。无论他们保存或者放弃或者没有任何为保存的数据,我们都重新初始化元素矢量并绘制默认图表。

我们是不是也应该调用optionsSetData()来弹出一个对话框,让用户通过它来创建和编辑值、颜色等等呢?你可以运行一下现在的应用程序,然后试着把optionsSetData()的调用添加进去后再运行并观察它们来决定你更喜欢哪一个。

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 );
    }

我们检查它是否是okToClear()。如果是的话,我们使用静态的QFileDialog::getOpenFileName()函数来获得用户想要载入的文件的名称。如果我们得到一个文件名,我们就调用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 );
    }

这个函数调用了静态的QFileDialog::getSaveFileName()来得到一个要保存数据的文件的明处那个。如果文件存在,我们使用使用一个QMessageBox::warning()来提醒用户并给他们一个放弃保存的选择。如果文件被保存了我们就更新最近打开的文件列表并调用fileSave()(在文件处理中)来执行存储。

管理最近打开文件的列表

        QStringList m_recentFiles;

我们用一个字符串列表来处理这个最近打开文件的列表。

    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 );
        }
    }

无论用户打开一个存在的文件或者保存一个新文件的时候,这个函数会被调用(通常是通过updateRecentFiles())。对于这个字符串列表中的每一个文件我们都插入一个新的菜单条目。我们在每一个文件名的前面都加上一个从19带下划线的数字,这样就可以支持键盘操作(比如,Alt+F,2就可以打开列表中的第二个文件)。我们给每一个菜单条目一个和它们在字符串列表中的索引位置相同的数值作为id,并且把每一个菜单条目都和fileOpenRecent()槽相连。老的文件菜单条目会在每一个最新的文件菜单条目id来到的同时被删除。它会工作是因为其它文件菜单条目都有一个由Qt生成的id(它们都是<0的),然而我们所创建的菜单条目的id都是>=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();
    }

当用户打开一个存在的文件或者保存一个新文件的时候,它会被调用。如果文件已经存在于列表中,它就会很简单地返回。否则这个文件会被添加到列表的末尾并且如果列表太大(>9个文件)的话,第一个(最老的)就会被移去。然后updateRecentFilesMenu()被调用来在File菜单中重新创建最近使用的文件列表。

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

        load( m_recentFiles[index] );
    }

当用户选择了一个最近打开的文件时,fileOpenRecent()槽会伴随一个用户选择的文件的菜单id而被调用。因为我们使文件菜单的id和文件在m_recentFiles列表中的索引位置相等,我们就可以很简单的通过文件的菜单条目id来载入了。

退出

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

当用户退出时,我们给他们保存任何未保存数据的机会(okToClear()),然后在结束之前保存它们的选项,比如窗口的大小和位置、图表类型等等。

    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] );
    }

直接使用QSettings来保存用户选项。

自定义对话框

我们想让用户可以手工地设置一些选项并且创建和编辑值、值颜色等等。

    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;
    }

设置选项的视窗是由我们自定义的OptionsForm提供的,在设置选项中。这个选项视窗是一个标准的“哑的”对话框:我们创建一个实例,把所有的图形用户界面元素都和所有相关的设置都组装起来,并且如果用户点击了“OK”(exec()返回一个真值)我们就会从图形用户界面元素中读取设置。

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

创建和编辑图表数据的视窗由我们自定义的SetDataForm提供,在获得数据中。这个视窗是一个“聪明的”对话框。我们传入我们想要使用的数据结构,并且对话框可以自己处理数据机构的表达。如果用户点击“OK”,对话框会更新数据结构并且exec()会返回一个真值。如果用户改变了数据时我们在optionsSetData()中所要做的时把图表标记为changed并调用drawElements()来使用新的和更新过的数据来重新绘制图表。

« 主体很容易 | 目录 | 画布控制 »


Copyright © 2002 Trolltech Trademarks 译者:Cavendish
Qt 3.0.5版