Using QDataBrowser and QDataView

Figure 6-4. The Book Application's Edit Books Dialog

Drilling Down to a Form using QDataBrowser

Setting up a QDataBrowser

We will now create a new form to allow users to edit book records. Click the New toolbar button, click the Dialog template from the New Form dialog and click OK. Change the name of the form to EditBookForm and its caption to 'Edit Books'. Click the Save toolbar button and call the file editbook.ui. Now that we have the form we can add a QDataBrowser to show the book records.

  1. Click the Data Browser toolbar button, then click the form. The Data Browser Wizard will appear.

  2. The Database Connection and Table wizard page is used to set up a connection if one doesn't exist and to choose the table or view for the QDataBrowser. (See the section called Setting Up Qt Designer's Connections "Setting Up Qt Designer's Connections".)

    Click the connection you wish to use, listed in the Connection list box, e.g. "(default)". The available tables and views will appear in the Table list box. Click the book table and then click the Next button.

  3. The Displayed Fields wizard page provides a means of selecting which fields should be displayed in the QDataBrowser and in what order. By default all fields except the primary key (if there is one) are in the right hand Displayed Fields list box. The left and right blue arrow buttons can be used to move fields between the Displayed Fields and the Available Fields list boxes. The blue up and down arrow buttons are used to select the display order of the displayed fields.

    We don't want to see the authorid foreign key field on the form, so move it to the Available Fields list box. Also, move the title field to the top of the Displayed Fields list. Click the Next button.

  4. The Navigation and Editing wizard page allows us to choose which navigation and editing buttons should appear on the form.

    We will accept the defaults and simply click the Next button.

  5. The SQL wizard page is used to set the QDataBrowser's Filter and Sort properties. The Filter is an SQL WHERE clause (without the word 'WHERE'). For example, to only list book that cost less than 50 (of some currency, e.g. dollars), we would enter price < 50. We will leave the filter empty. The Available Fields list box lists all the fields. The Sort By list box lists the fields that the QDataBrowser is to sort by and the direction of their sorting (ASCending or DESCending). The left and right blue arrows are used to move fields between the two list boxes. The up and down blue arrows move fields up and down within the Sort By list box. The ASC or DESC setting is changed with the sort order button.

    Move the title field into the Sort By list box and click Next.

  6. The Layout wizard page is used to specify the initial layout of the form.

    Change the Number of Columns to 1, then click Next. Now click Finish.

  7. The QDataBrowser will now appear on the form. Resize the form to make it shorter. Click the QDataBrowser then click the Break Layout toolbar button. Click the buttons then click the Break Layout toolbar button. Add another button called 'PushButtonClose' with the text '&Close' and place it to the right of the Delete button.

  8. Shift+Click the Insert, Update, Delete and Close buttons, then click the Lay Out Horizontally toolbar button. Click the QDataBrowser, then click the Lay Out in a Grid toolbar button. Finally click the form and click the Lay Out Vertically toolbar button. Now click the QDataBrowser and rename it 'BookDataBrowser'.

  9. Qt Designer will generate the necessary code to make the browser operational (including generating the appropriate cursor, sort and filter code).

    For finer control over the form, we will be creating our own database cursor. Therefore, set the BookDataBrowser's frameworkCode property to FALSE in the Properties window to prevent Qt Designer from generating redundant code for the cursor.

Performing the Drilldown

We now have a working form for editing book records. We need to start the form when the user clicks our 'Edit Books' button, and to navigate to the record they have selected in the BookDataTable. We also need to provide a means of editing the foreign keys, e.g. authorid.

  1. We need to make a new slot to connect the Edit Books' button's clicked() signal to. Click on the Book form to make it Qt Designer's active form. Invoke the Edit Slots dialog and create a new slot called editClicked(). Now click the Connect Signal/Slots toolbar button. Click the Edit Books button and drag to the form; release the mouse on the form. In the Edit Connections dialog connect the clicked() signal to the editClicked() slot. Click OK to leave the dialog.

  2. In the Object Hierarchy window click Source and then click the editClicked function. We need to change it to the following:
    void BookForm::editClicked()
    {
        EditBookForm *dialog = new EditBookForm( this, "Edit Book Form", TRUE );
        QSqlCursor cur( "book" );
        dialog->BookDataBrowser->setCursor( &cur );
        dialog->BookDataBrowser->setFilter( BookDataTable->filter() );
        dialog->BookDataBrowser->setSort(QSqlIndex::fromStringList( 
                BookDataTable->sort(), &cur ) );
        dialog->BookDataBrowser->refresh();
        int i = BookDataTable->currentRow();
        if ( i == -1 ) i = 0; // Always use the first row
        dialog->BookDataBrowser->seek( i );
        dialog->exec();
        delete dialog;
        BookDataTable->refresh();
    }

    We create our dialog as before. We also create a cursor over the book table and set the dialog's QDataBrowser, BookDataBrowser, to use this new cursor. We set the QDataBrowser's filter and sort to those that applied to the main form's book QDataTable. We refresh the QDataBrowser and seek to the same record the user was viewing on the main form. Then we exec the dialog and delete it when the user has finished with it. Finally we update the BookDataTable in the main form to reflect any changes that were made in the dialog.

  3. Because our code refers to a class declared in editbook.h and to a QDataBrowser we need to add two additional include files. Click on the BookForm, then click on the Source tab of the Object Hierarchy window. Right click the 'Includes (In Declaration)' item and click New. Type in "editbook.h". Now add a second include, this time, <qdatabrowser.h>.

Now when we navigate through the author and book records in the BookForm we can click the Edit Books button to launch our Edit Books dialog. Although the dialog supports UPDATE, DELETE and navigation over the book table, we cannot edit the foreign keys nor perform inserts. We will deal with insertion in the same way as we did with the QDataTable, then we will handle the foreign key relationship to author.

Inserting into a QDataBrowser

We will create a slot to receive the Edit Books form's primeInsert() signal so that we can insert a unique primary key.

  1. Click on the Edit Books form, then create a new Slot called primeInsertBook(QSqlRecord*).

    Click Edit|Slots, then click the New Slot button and type the new slot name in the Slot Properties Slot edit box. Click OK.

  2. Connect the BookDataBrowser's primeInsert() signal to the primeInsertBook() slot.

    Click the Connect Signal/Slots toolbar button, then click the BookDataBrowser and drag to the form; release the mouse on the form. Now click the primeInsert() signal and the primeInsertBook slot. Click OK.

  3. In the Object Hierarchy window click Source and then click the primeInsertBook slot. We need to change it to the following:
    void EditBookForm::primeInsertBook( QSqlRecord * buffer )
    {
        QSqlQuery query;  
        query.exec( "UPDATE sequence SET sequence = sequence + 1 WHERE tablename='book';" );  
        query.exec( "SELECT sequence FROM sequence WHERE tablename='book';" );  
        if ( query.next() ) {  
            buffer->setValue( "id", query.value( 0 ) );  
        }     
    }

  4. We will also tidy up the user interface slightly. Click the Update button and set its default property to True. Connect the Close button's clicked() signal to the EditBookForm's accept() slot.

Handling Foreign Keys in a QDataBrowser

Qt's SQL module provides two approaches to dealing with foreign keys. The most powerful and flexible is to subclass widgets and use property maps to relate the widgets to the database. This approach is described in the Qt SQL Module documentation, particularly the StatusPicker example. A simpler approach that can be taken wholly within Qt Designer is presented here.

We will add a new field to the EditBookForm so that authors can be edited along with the title and price. Once we've handled the visual design we'll write the code to make it all work.

  1. First we'll add the new widgets. Click the BookDataBrowser and click the Break Layout toolbar button. Resize the form to make it larger and drag each set of buttons down to make some room below the title and price QLineEdits. Click the Text Label toolbar button and click on the form beneath the Price label. Click the Text Label and change its text to 'Author'. Click the ComboBox toolbar button and click on the form beneath the price QLineEdit. In the Property Window change the ComboBox's name to ComboBoxAuthor and change its sizePolicy hSizeType to Expanding.

  2. Now we'll lay out the dialog. Shift+Click the Author label and the ComboBox then click the Lay Out Horizontally toolbar button. Now click the BookDataBrowser and click the Lay Out in a Grid toolbar button.

We need to write some code so that the ComboBox will be populated with author names and scroll to the current book's author. We also need to ensure that we put the author's id into the book table's authorid field when a book record is inserted or updated. We'll ensure the code is executed at the right time by putting it in slots and connecting signals to our slots.

  1. Create two new slots called beforeUpdateBook(QSqlRecord *buffer) and primeUpdateBook(QSqlRecord *buffer). (Click Edit|Slots, then in the Edit Slots dialog click New Slot and enter the first new slot. Click New Slot again and enter the second slot then click OK.)

  2. When the user navigates through the dialog, each time they move to a new record, a primeUpdate() signal is emitted. We connect to this so that we can update the ComboBox's display. Just before a record is updated or inserted into the database a beforeUpdate() or beforeInsert() signal is emitted. We connect our beforeUpdateBook() slot to both these signals so that we can ensure that the book's authorid field is correctly populated.

    Click the BookDataBrowser and drag the mouse to the form; release the mouse and the Edit Connections dialog will appear. Connect the beforeUpdate() signal to our beforeUpdateBook() slot. Connect the beforeInsert() signal to our beforeUpdateBook() slot. Finally connect the primeUpdate() signal to our primeUpdateBook() slot.

  3. All that remains is to write the underlying code. All the code snippets are taken from qt/tools/designer/eg/book/book7/editbook.ui.

    1. We start with the init() function; this is called after the dialog is constructed and we will use it to populate the ComboBox with author names.
      void EditBookForm::init()
      {
          QSqlQuery query( "SELECT surname FROM author ORDER BY surname;" );    
          while ( query.next() ) 
              ComboBoxAuthor->insertItem( query.value( 0 ).toString()); 
      }
      Here we execute a query to get a list of author names and insert each one into the ComboBox.

    2. We next write the code which will be executed just before a record is updated (or inserted) in the database.
      void EditBookForm::beforeUpdateBook( QSqlRecord * buffer )
      {
          QSqlQuery query( "SELECT id FROM author WHERE surname ='" + 
              ComboBoxAuthor->currentText() + "';" );
          if ( query.next() )
              buffer->setValue( "authorid", query.value( 0 ) );
      }
      We look up the id of the ComboBox's current author and place it in the update (or insert) buffer's authorid field.

    3. As the user navigates through the records we ensure that the ComboBox reflects the current author.
      void EditBookForm::primeUpdateBook( QSqlRecord * buffer )
      {
          // Who is this book's author?
          QSqlQuery query( "SELECT surname FROM author WHERE id='" +  
              buffer->value( "authorid" ).toString() + "';" ); 
          QString author = "";    
          if ( query.next() )
              author = query.value( 0 ).toString();
          // Set the ComboBox to the right author
          for ( int i = 0; i < ComboBoxAuthor->count(); i++ ) {
              if ( ComboBoxAuthor->text( i ) == author ) {
                  ComboBoxAuthor->setCurrentItem( i ) ;
                  break;
              }
          }
      }
      Firstly we look up the book's author and secondly we iterate through the ComboBox's items until we find the author and set the ComboBox's current item to the matching author.

If the author name has changed or been deleted the query will fail and no author id will be inserted into the buffer causing the INSERT to fail. An alternative is to record the author id's as we populate the ComboBox and store them in a QMap which we can then look up as required. This approach requires changes to the init(), beforeUpdateBook() and primeInsertBook() functions and the addition of a new function, mapAuthor(). The relevant code from qt/tools/designer/eg/book/book8/editbook.ui is shown below.

  1. First we need to create a class variable to map author names to author id's. Click in the Source tab of the Object Hierarchy, then right click the Class Variables item and click New. Type in 'QMap<QString,int> authorMap;'.

  2. We now record the author id's in the init() function.
    void EditBookForm::init()
    {
        QSqlQuery query( "SELECT surname, id FROM author ORDER BY surname;" );    
        while ( query.next() ) {
            ComboBoxAuthor->insertItem( query.value( 0 ).toString() ); 
            int id = query.value( 1 ).toInt();
            mapAuthor( query.value( 0 ).toString(), id, TRUE );
        }
    }
    After inserting each author's name into the ComboBox we populate a QMap with the author's name and id.

  3. Instead of looking up the author's id in the database we look it up in the QMap.
    void EditBookForm::beforeUpdateBook( QSqlRecord * buffer )
    {
        int id;
        mapAuthor( ComboBoxAuthor->currentText(), id, FALSE );
        buffer->setValue( "authorid", id );
    }

  4. We use a single function for storing author id's and returning them so that we can use a static data structure.
    void EditBookForm::mapAuthor( const QString & name, int & id, bool populate )
    {
        if ( populate ) 
            authorMap[ name ] = id;
        else
            id = authorMap[ name ];
    }
    If the populate flag is TRUE, we store the author's name and id in the QMap, otherwise we look up the given author name and set id appropriately.

Another approach which is especially useful if the same foreign key lookups are required in different parts of the application is to subclass a cursor and use this for our lookups. This is described in the Qt SQL Module documentation, particulary the section on subclassing QSqlCursor.

The 'book' example demonstrates the basic techniques needed for SQL programming with Qt. Additional information on the Qt SQL classes, especially the QSqlQuery and QSqlCursor classes is provided in the Qt SQL Module documentation.