Creating Custom Models

In this section, we will create a custom model to hold the ship data, and display the same model in two different table views. An application that makes use of the model is shown in Figure 14.3. The user can scroll the tables independently, and can edit the data in either of them, safe in the knowledge that any changes will be automatically reflected in both views.

We will begin by showing extracts from the application's main form. This will show us some of the model/view API in use. Then we will look at the implementation of the model itself. One important benefit of PyQt's model/view architecture is that the same coding patterns are used again and again, so once we know how to create one table model, we know how to create any table (or list) model.

The model is provided by class ShipTableModel in chap14/ships.py and the application is in chap14/ships-model.pyw. We have improved the appearance of the data in the view by setting background and foreground colors, but these could have been done in the convenience views by calling the appropriate methods on

Ships (model)

H

Table 1

Table 2

Name

Owner

Country

Description

TEU *

Name

Owner A

1

A,P. Meiller

Maersk Line

<i>Ferraby</i>

91,690

1

A.P. M0ller

Maersk Line

2

Adrian Maersk

Maersk Line

Denmark

Captain <i>G. E. Ericson </i>

93,496

2

Adrian Maersk

Maersk Line

3

Albert Maersk

Maersk Line

Denmark

Captain <i>Tallow</i>

93,496

3

Albert Maersk

Maersk Line

4

Anna Maersk

Maersk Line

<i>Lockhart</i>

93,496

4

Anna Maersk

Maersk Line

5

Arnold Masrsk

Maersk Line

Denmark

Captain <b><i>Mor...

93,496

5

Arnold Maersk

Masrsk Line

,<J.

__llll

>

<

Hi

>

1 Add Ship If Remove Ship j

[ 1 1

1

Figure 14.3 A custom table model in two QTableViews

Figure 14.3 A custom table model in two QTableViews the table items. The problems that existed in the previous example, in particular, no comboboxes for owners or countries, no spinbox for TEUs, and showing the HTML description text raw, remain. These can be solved only by using a delegate, something we will do in the next section.

Implementing the View Logic

Superficially, it would appear that there is no difference between what we can achieve using a convenience view with its built-in model, and a pure view with a separate model. In the preceding example, we had three views presenting the same underlying data, and it was our responsibility to keep them in sync. In this example, we will use two views on the same data, and can leave the work of synchronization to PyQt since both views use the same model. Another benefit is that the views only retrieve or store data that is actually seen or edited, and this can give considerable performance benefits when using large datasets.

We will begin with some extracts from the form's initializer. class MainForm(QDialog):

def_init_(self, parent=None):

super(MainForm, self)._init_(parent)

self.model = ships.ShipTableModel(QString("ships.dat"))

tableLabel1 = QLabel("Table &1")

self.tableView1 = QTableView()

tableLabel1.setBuddy(self.tableView1)

self.tableView1.setModel(self.model)

tableLabel2 = QLabel("Table &2")

self.tableView2 = QTableView()

tableLabel2.setBuddy(self.tableView2)

self.tableView2.setModel(self.model)

First we create a new model. Then we create two table views and accompanying labels to ease navigation. Each table view is given the same model to work on. We have omitted the layout code since it is not relevant.

for tableView in (self.tableViewl, self.tableView2): header = tableView.horizontalHeader() self.connect(header, SIGNAL("sectionClicked(int)"), self.sortTable)

self.connect(addShipButton, SIGNAL("clicked()"), self.addShip) self.connect(removeShipButton, SIGNAL("clicked()"), self.removeShip) self.connect(quitButton, SIGNAL("clicked()"), self.accept)

self.setWindowTitle("Ships (model)")

When we use a custom model we must handle sorting ourselves. We connect each table view's horizontal (columns) header to a sortTable() method. The other connections are similar to what we had before. But notice that we have no connection for when a table item is edited: There is no need, since the view will handle editing for us, automatically reflecting changes back into the model, which in turn will keep both views up-to-date.

def accept(self):

if self.model.dirty and \

QMessageBox.question(self, "Ships - Save?", "Save unsaved changes?",

QMessageBox.Yes|QMessageBox.No) == QMessageBox.Yes:

try:

self.model.save() except IOError, e:

QMessageBox.warning(self, "Ships - Error", "Failed to save: %s" % e) QDialog.accept(self)

If the user terminates the application and there are unsaved changes, we give them the chance to save before exiting. The model's dirty attribute and its save() method are our own extensions to the QAbstractTableModel's API so that the model can load and save its data from and to files.

The base class for models is QAbstractItemModel, but row/column-based models normally inherit QAbstractTableModel, one of QAbstractItemModel's subclasses.

def sortTable(self, section):

if section in (ships.OWNER, ships.COUNTRY):

self.model.sortByCountryOwner() else:

self.model.sortByName() self.resizeColumns()

We have provided only two sorts, but there is no reason why more could not be supported. Again, the sortBy*() methods are extensions that we have added to the standard API. When the user sorts we take the opportunity to resize the columns. We do this because editing may have changed the widths that the columns need, and since the sort will change the view anyway, it seems a sensible place to resize without disturbing the user.

def resizeColumns(self):

for tableView in (self.tableView1, self.tableView2):

for column in (ships.NAME, ships.OWNER, ships.COUNTRY, ships.TEU): tableView.resizeColumnToContents(column)

Here we have chosen to resize every column except the description column in both table views.

def addShip(self):

row = self.model.rowCount() self.model.insertRows(row) index = self.model.index(row, 0) tableView = self.tableView1 if self.tableView2.hasFocus():

tableView = self.tableView2 tableView.setFocus() tableView.setCurrentIndex(index) tableView.edit(index)

Adding a new ship is similar to what we did in the preceding section, but a little neater. We insert a new row as the last row in the model. Then we retrieve a model index that refers to the first column of the new row. We then find out which table view has (or last had) the keyboard focus, and we set the focus back to that view. We set the view's index to the model index we have retrieved and initiate editing on it.

The rowCount(), insertRows(), and index() methods are part of the standard QAbstractTableModel's API.

def removeShip(self):

tableView = self.tableView1 if self.tableView2.hasFocus():

tableView = self.tableView2 index = tableView.currentIndex() if not index.isValid():

return row = index.row() name = self.model.data(

self.model.index(row, ships.NAME)).toString() owner = self.model.data(

self.model.index(row, ships.OWNER)).toString()

country = self.model.data(

self.model.index(row, ships.COUNTRY)).toString() if QMessageBox.question(self, "Ships - Remove",

QString("Remove %1 of %2/%3?").arg(name).arg(owner) \

.arg(country), QMessageBox.Yes|QMessageBox.No) == QMessageBox.No: return self.model.removeRows(row) self.resizeColumns()

If the user clicks the Remove button we retrieve the model index for the current table view's current item. We extract the row from this model index and use it with the QAbstractTableModel.data() method to retrieve the ship's name, owner, and country. The data() method takes a model index as a mandatory argument and returns a QVariant. We use QAbstractTableModel.index() to create model indexes for the row/column combinations we want, and use QVariant.toString() to convert the returned values to QStrings.

If the user confirms their deletion, we simply remove the relevant row from the model. The model will automatically notify the views, which in turn will update themselves. We have added a call to resizeColumns() since the maximum column widths may have changed after the deletion.

Implementing the Custom Model

We have now seen some of the QAbstractTableModel's API in use, along with some extensions of our own. The methods in a model subclass can be divided into three categories:

• Methods that are necessary for implementing read-only models

• Methods that are necessary for implementing editable models

• Methods that we need to extend the API for particular circumstances

The essential methods for read-only table models are data(), rowCount(), and columnCount(), although headerData() is almost always implemented too.

Editable models require reimplementations of the same methods as those needed for read-only models, and in addition, flags() and setData(). If the model is to support adding and removing rows as well as editing existing data, insertRows() and removeRows() must also be implemented.

Other methods can be implemented as well, but those listed in the two preceding paragraphs are the only essential ones.

For the ship model we store the ships in a list in memory and in a binary file on disk. To support this functionality we have extended the model API by adding sortByName(), sortByCountryOwner(), load(), and save().

The ShipTableModel is in chap14/ships.py.

class ShipTableModel(QAbstractTableModel):

def _init_(self, filename=QString()):

super(ShipTableModel, self)._init_()

self.filename = filename self.dirty = False self.ships = [] self.owners = set() self.countries = set()

We want to load and save the model's data from and to a binary file, so we keep an instance variable with the filename. The ships themselves are stored in a list which is initially unordered. We also keep two sets, one of owners and the other of countries: These will be used to populate comboboxes when we create a custom delegate in the next section.

def rowCount(self, index=QModelIndex()): return len(self.ships)

def columnCount(self, index=QModelIndex()): return 5

The row and column counts are easy to provide. It is very common for table models to have a fixed column count.

def data(self, index, role=Qt.DisplayRole): if not index.isValid() or \

not (0 <= index.row() < len(self.ships)): return QVariant() ship = self.ships[index.row()] column = index.column() if role == Qt.DisplayRole: if column == NAME:

return QVariant(ship.name) elif column == OWNER:

return QVariant(ship.owner) elif column == COUNTRY:

return QVariant(ship.country) elif column == DESCRIPTION:

return QVariant(ship.description) elif column == TEU:

return QVariant(QString("%L1").arg(ship.teu))

The data() method has one mandatory argument—the model index of the item concerned—and one optional argument—the "role". The role is used to indicate what kind of information is required. The default role, Qt.DisplayRole, means that the data as displayed is wanted.

If the model index is invalid or if the row is out of range we return an invalid QVariant. PyQt's model/view architecture does not raise exceptions or give error messages; it simply uses invalid QVariants. If the index is valid we retrieve the ship at the row corresponding to the index's row. If the role is Qt.DisplayRole we return the data for the requested column as a QVariant. In the case of the TEU, instead of returning an integer, we return the number as a localized string.

elif role == Qt.TextAlignmentRole: if column == TEU:

return QVariant(int(Qt.AlignRight|Qt.AlignVCenter)) return QVariant(int(Qt.AlignLeft|Qt.AlignVCenter)) elif role == Qt.TextColorRole and column == TEU: if ship.teu < 80000:

return QVariant(QColor(Qt.black)) elif ship.teu < 100000:

return QVariant(QColor(Qt.darkBlue)) elif ship.teu < 120000:

return QVariant(QColor(Qt.blue)) else:

return QVariant(QColor(Qt.red)) elif role == Qt.BackgroundColorRole:

if ship.country in (u"Bahamas", u"Cyprus", u"Denmark", u"France", u"Germany", u"Greece"): return QVariant(QColor(250, 230, 250)) elif ship.country in (u"Hong Kong", u"Japan", u"Taiwan"):

return QVariant(QColor(250, 250, 230)) elif ship.country in (u"Marshall Islands",):

return QVariant(QColor(230, 250, 250)) else:

return QVariant(QColor(210, 230, 230)) return QVariant()

If data() is being called with the Qt.TextAlignmentRole, we return a right-alignment for TEUs and a left-alignment for the other columns. QVariants cannot accept alignments, so we must convert them to an integer value.

For the Qt.TextColorRole, we return a color for the TEU column and ignore other columns. This means that the non-TEU columns will have the default text color, usually black. For the Qt.BackgroundColorRole, we provide different colored backgrounds depending on which group of countries the ship belongs to.

We can handle several other roles if we wish, including Qt.DecorationRole (the item's icon), Qt.ToolTipRole, Qt.StatusTipRole, and Qt.WhatsThisRole. And for controlling appearance, in addition to the alignment and color roles we discussed earlier, there is Qt.FontRole and Qt.CheckStateRole.

We return an invalid QVariant for all the cases we choose not to handle. This tells the model/view architecture to use a default value in these cases.

Some developers don't like mixing appearance-related information with the data, as we have done here in our data() implementation. PyQt is neutral on this issue: It gives us the flexibility to mix, but if we prefer data() to be purely concerned with data we can do that too, and leave all appearance-related issues to the delegate.

def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.TextAlignmentRole: if orientation == Qt.Horizontal:

return QVariant(int(Qt.AlignLeft|Qt.AlignVCenter)) return QVariant(int(Qt.AlignRight|Qt.AlignVCenter)) if role != Qt.DisplayRole:

return QVariant() if orientation == Qt.Horizontal: if section == NAME:

return QVariant("Name") elif section == OWNER:

return QVariant("Owner") elif section == COUNTRY:

return QVariant("Country") elif section == DESCRIPTION:

return QVariant("Description") elif section == TEU:

return QVariant("TEU") return QVariant(int(section + 1))

Although not essential, it is a good practice to provide a headerData() implementation. The section is a row offset when the orientation is Qt.Vertical, and a column offset when the orientation is Qt.Horizontal. Here, we provide column headers, and number the rows from 1.

Like data(), this method accepts a role, and we use this to make the row numbers right-aligned and the column headers left-aligned.

The methods we have looked at so far are enough to implement read-only table models. Now we will look at the additional methods that must be implemented to make a model editable.

def flags(self, index): if not index.isValid():

return Qt.ItemIsEnabled return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| Qt.ItemIsEditable)

If we have a valid model index we return a Qt.ItemFlags that combines the existing item flags with the Qt.ItemIsEditable flag. We can use this method to make items read-only by applying the Qt.ItemIsEditable flag only when the model index is for a row and column that we want to be editable.

def setData(self, index, value, role=Qt.EditRole):

if index.isValid() and 0 <= index.row() < len(self.ships): ship = self.ships[index.row()] column = index.column() if column == NAME:

ship.name = value.toString() elif column == OWNER:

ship.owner = value.toString() elif column == COUNTRY:

ship.country = value.toString() elif column == DESCRIPTION:

ship.description = value.toString() elif column == TEU:

ship.teu = value self.dirty = True self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index) return True return False

This method is called when the user completes an edit. In this case, we ignore the role, although it is possible to have separate display and edit data (for example, a spreadsheet's result and the formula behind it). If the index is valid and the row is in range we retrieve the relevant ship and update the column that has been edited. In the case of the TEU, we apply the change only if what the user typed in was converted successfully to an integer.

The dataChanged() signal must be emitted if a change has taken place. The model/view architecture depends on this signal to ensure that all the views are kept up-to-date. We must pass the model index of the changed item twice because the signal can be used to indicate a block of changes, with the first index being the top-left item and the second index the bottom-right item. We must return True if the change was accepted and applied, and False otherwise.

Implementing flags() and setData() (in addition to the methods necessary for a read-only model) is sufficient to make a model editable. But to make it possible for users to add or delete entire rows we need to implement two additional methods.

def insertRows(self, position, rows=1, index=QModelIndex()): self.beginInsertRows(QModelIndex(), position, position + rows - 1) for row in range(rows):

self.ships.insert(position + row,

Ship(" Unknown", " Unknown", " Unknown"))

self.endInsertRows()

self.dirty = True return True

The call to beginInsertRows() is essential when we want to insert one or more rows into a model. The position is the row we want to insert at. The call to beginInsertRows() is taken straight from the PyQt documentation and should not need to be changed for any table model insertRows() implementation. After the insertions, we must call endInsertRows(). The model will automatically notify the views that the changes have been made, and the views will ask for new data if the relevant rows are visible to the user.

def removeRows(self, position, rows=1, index=QModelIndex()): self.beginRemoveRows(QModelIndex(), position, position + rows - 1) self.ships = self.ships[:position] + \

self.ships[position + rows:] self.endRemoveRows() self.dirty = True return True

This method is similar to the preceding one. The call to beginRemoveRows() is taken from the documentation and is standard for table model reimplementa-tions. After the relevant rows have been removed, we must call endRemoveRows(). The model will automatically notify the views about the changes.

We have now implemented the essential methods for an editable table model. Some models are merely interfaces to external data sources such as database tables (covered in the next chapter), or to external files or processes. In this case, we have stored the data inside the model itself and for this reason we must provide some extra methods, in particular load() and save(). We have also provided a couple of sorting methods as a convenience for the user. Sorting is Ordered- expensive for large datasets, and in such cases using an ordered data structure, Dict such as an OrderedDict, or using a list in conjunction with the bisect module's 92 functions may prove beneficial.

def sortByName(self):

self.ships = sorted(self.ships) self.reset()

When sort() is called on a list it uses the items'_lt_() special method for comparisons, falling back to use the_cmp_() special method if_lt_() has not been implemented. We provided Ship._cmp_() which does a locale-aware comparison of ships' names.

Sorting the data makes all model indexes invalid and means that the views are now showing the wrong data. The model must notify the views that they need to update themselves by retrieving fresh data. One way to do this is to emit a dataChanged() signal, but for big changes it is more efficient to call

Table 14.1 Selected QAbstractItemModel Methods #1

Syntax m.beginInsert-Rows(p, f, I)

m.beginRemove-Rows(p, f, I)

m.columnCount(p)

m.data(i, rl) m.endInsertRowsO m.endRemoveRows() m.flags(i)

m.hasChildren(p)

m.insertRow(r, p)

m.parent(i)

Description

Call in reimplementations of insertRows() before inserting data. The arguments are the parent QModelIndex p and the first and last row numbers the new rows will occupy; m is a QAbstractItemModel subclass. Call in reimplementations of removeRows() before removing data. The arguments are the parent QModel-Index p and the first and last row numbers to be removed; m is a QAbstractItemModel subclass. Subclasses must reimplement this; the parent QModel-Index p matters only to tree models Subclasses must use this to create QModelIndexes with row int r, column int c, and parent QModelIndex p Returns the data as a QVariant for QModelIndex i and Qt.ItemDataRole rl; subclasses must reimplement this Call in reimplementations of insertRows() after inserting new data; m is a QAbstractItemModel subclass Call in reimplementations of removeRows() after removing data; m is a QAbstractItemModel subclass Returns the Qt.ItemFlags for QModelIndex i; these govern whether the item is selectable, editable, and so on. Editable model subclasses must reimplement this Returns True if parent QModelIndex p has children; meaningful only for tree models

Returns a QVariant for "section" (row or column) int s, with Qt.Orientation o indicating row or column, and with Qt.ItemDataRole rl. Subclasses normally reimplement this; m is a QAbstractItemModel subclass. Returns the QModelIndex for the given row int r, column int c, and parent QModelIndex p; subclasses must reimplement this and must use createIndex() Inserts one row before row int r. In tree models, the row is inserted as a child of parent QModelIndex p. Inserts int n rows before row int r. In tree models, the rows are inserted as children of parent QModelIndex p. Editable subclasses often reimplement this—reimplementations must call beginInsertRows() and endInsertRows().

Returns the parent QModelIndex of QModelIndex i. Tree model subclasses must reimplement this.

Table 14.2 Selected QAbstractItemModel Methods #2 Syntax Description m.removeRow(r, p) Removesrow int r. The parent QModelIndex p isrel-

evant only to tree models; m is a QAbstractItemModel subclass.

m.removeRows(r, n, p) Removes int n rowsfromrow int r.The parent

QModelIndex p is relevant only to tree models. Editable model subclasses often reimplement this method—reimplementations must call begin-RemoveRows() and endRemoveRows(). m.reset() Notifies all associated views that the model's data has radically changed—this forces views to refetch all their visible data

Subclasses must reimplement this; the parent QModelIndex p matters only to tree models , rl) Sets QModelIndex i's data for Qt.ItemDataRole rl to QVariant v. Editable model subclasses must reimplement this—reimplementations must emit the data-Changed() signal if data was actually changed. Sets the header data for section int s with rl) Qt.Orientation o (i.e., for row or column), for

Qt.ItemDataRole rl to QVariant v

QAbstractTableModel.reset(); this tells all associated views that everything is out-of-date and forces them to update themselves.

def sortByCountryOwner(self): def compare(a, b):

return QString.localeAwareCompare(a.country, b.country) if a.owner != b.owner:

return QString.localeAwareCompare(a.owner, b.owner) return QString.localeAwareCompare(a.name, b.name) self.ships = sorted(self.ships, compare) self.reset()

Here we provide a custom sort method, sorting by country, by owner, and by ship's name. For a large dataset it might be more efficient to use DSU (decorate, sort, undecorate). For example:

def sortByCountryOwner(self): ships = []

for ship in self.ships:

ships.append((ship.country, ship.owner, ship.name, ship)) ships.sort()

m.rowCount(p) m.setData(i, v m.setHeader-Data(s, o, v, self.ships = [ship for country, owner, name, ship in ships] self.reset()

This uses the normal QString.compare(), so it might be better to have used unicode(ship.country), unicode(ship.owner), and unicode(ship.name). Of course, for very large datasets it is probably better to avoid sorting altogether and to use ordered containers instead.

Saving and

Loading Binary Files

Thanks to using QDataStream we don't have to worry about how long the strings are or about encoding issues.

The ships are loaded in correspondingly: Here is an extract from the load() method:

self.ships = [] while not stream.atEnd(): name = QString() owner = QString() country = QString() description = QString()

stream >> name >> owner >> country >> description teu = stream.readInt32()

self.ships.append(Ship(name, owner, country, teu, description)) self.owners.add(unicode(owner)) self.countries.add(unicode(country))

As noted earlier, we keep sets of owners and countries to make them available in comboboxes when we add a custom delegate.

Implementing custom models, particularly list and table models, is quite straightforward. For read-only models we need to implement only three methods, although normally we implement four. For editable models, we normally implement a total of eight methods. Once you have created a couple of models, creating others will become easy, because all list and table models follow the same pattern. Implementing tree models is more challenging; the topic is covered in the last section of Chapter 16.

The save() and load() methods are very similar to ones we have seen before for handling binary data using QDataStream, so we will just show an extract from the heart of each, starting with the save() method.

for ship in self.ships:

stream << ship.name << ship.owner << ship.country \

<< ship.description stream.writeInt32(ship.teu)

Was this article helpful?

+6 -2
YouTube Saturation

YouTube Saturation

Uncover The Instant Traffic System That Will TRIPLE Your Income Overnight While Putting You In Direct Contact With THOUSANDS Of Hungry Buyers. How to generate instant traffic to your website, easily! Siphon high quality, ultra-targeted traffic to your offers. The easiest way to dominate your market with lase targeted video based campaigns. These video ads will suck in customers by the minute, driving your message out to a global audience of buyers.

Get My Free Ebook


Responses

  • FELICITAS
    How to save changes made to QTableView back into Model pyqt?
    8 years ago
  • Elliot
    What is qvariant pyqt tablemodel?
    6 years ago

Post a comment