Using the Convenience Item Widgets

The convenience item widgets are view widgets that have built-in models. They use a default delegate for presenting and editing data, but this can be replaced by a custom delegate if we wish.

The screenshot in Figure 14.2 shows the same dataset in three different convenience view widgets. This means that the data is copied into each widget separately, so there is considerable data duplication. Another issue is that if we allow the user to edit the data, we must write code to ensure that all the views stay in sync. These problems would not exist if we used a custom model, as we will see in the next section.

Qlistwidget
Figure 14.2 QListWidget, QTableWidget, and QTreeWidget in action

The dataset we are using is a set of information about container ships. Each ship is represented by a Ship object, defined in the chap14/ships.py module.

class Ship(object):

def_init_(self, name, owner, country, teu=0, description=""):

self.name = QString(name) self.owner = QString(owner) self.country = QString(country) self.teu = teu self.description = QString(description)

return QString.localeAwareCompare(self.name.toLower(), other.name.toLower())

The preceding code is the complete Ship class. The integer teu attribute stands for "twenty-foot equivalent units", that is, how many 20-foot containers the ship can hold. (Nowadays most containers are 40 feet, so each counts as 2 TEUs.) The name, owner, and country attributes are all plain text, but the description attribute holds one line of HTML.

The_cmp_() special method provides a means of comparison for the purpose of sorting. The QString.localeAwareCompare() method does string comparisons in a locale-sensitive way—for example, correctly handling accented characters.

Since we are using convenience views with no custom delegates, we have only limited control over the editing of the data items. For example, we cannot offer drop-down comboboxes for editing owners and countries, or use spinboxes for editing TEUs. Also, the description text is shown raw, rather than being interpreted as HTML. We will, of course, solve all of these problems as the chapter progresses, but for now we will just focus on using the convenience views.

For the list, table, and tree items that are used with the convenience view widgets, it is possible to set their font, text alignment, text color, and background color, and to give them an icon or make them checkable. For the pure view widgets, we can exercise similar control over the appearance of items through the custom model; or exercise complete control over both the appearance and the editing of items by using a custom delegate.

The code for this section's example is in chap14/ships-dict.pyw. The data is held in a Python dictionary that itself is wrapped in the ships.ShipContainer class. We will discuss only the code that is relevant to model/view programming here—the rest of the code uses ideas and idioms that we already saw earlier in the book—for example, in Chapter 8—and is not hard to follow.

class MainForm(QDialog):

def_init_(self, parent=None):

super(MainForm, self)._init_(parent)

listLabel = QLabel("&List") self.listWidget = QListWidget() listLabel.setBuddy(self.listWidget)

tableLabel = QLabel("&Table") self.tableWidget = QTableWidget() tableLabel.setBuddy(self.tableWidget)

treeLabel = QLabel("Tre&e") self.treeWidget = QTreeWidget() treeLabel.setBuddy(self.treeWidget)

For each convenience view we create a label and set up a buddy to make keyboard navigation easier. The layout code is similar to what we have seen before, so we have omitted it and will concern ourselves only with the connections and with creating the data structure.

self.connect(self.tableWidget,

SIGNAL("itemChanged(QTableWidgetItem*)"), self.tableItemChanged) self.connect(addShipButton, SIGNAL("clicked()"), self.addShip) self.connect(removeShipButton, SIGNAL("clicked()"), self.removeShip) self.connect(quitButton, SIGNAL("clicked()"), self.accept)

self.ships = ships.ShipContainer(QString("ships.dat")) self.setWindowTitle("Ships (dict)")

By default, list widgets are not editable, so all users can do is select an item. This is also true of tree widgets. But table widgets are editable by default, with users able to initiate editing by pressing F2 or by double-clicking a cell. We can exercise full control over whether a view widget is editable using QAbstract-ItemView.setEditTriggers(); so, for example, we can make tables read-only or lists editable.

This application allows users to edit ship data in the table, and to add and remove ships. It also keeps all three views up-to-date by repopulating them after the data is loaded, and whenever a change occurs.

def populateList(self, selectedShip=None): selected = None self.listWidget.clear() for ship in self.ships.inOrder():

item = QListWidgetItem(QString("%1 of %2/%3 (%L4)") \

.arg(ship.name).arg(ship.owner).arg(ship.country) \ .arg(ship.teu)) self.listWidget.addItem(item)

if selectedShip is not None and selectedShip == id(ship):

selected = item if selected is not None:

selected.setSelected(True) self.listWidget.setCurrentltem(selected)

This method, like the other populating methods, is used both to populate the widget and to select the item that corresponds to the selectedShip—a Ship's id()—if one is passed in.

QString .arg()

If we reach a list widget item that is showing the selected ship, we keep a reference to the item in selected, and after the list widget has been populated, we make the selected item both current and selected.

def populateTable(self, selectedShip=None): selected = None self.tableWidget.clear() self.tableWidget.setSortingEnabled(False) self.tableWidget.setRowCount(len(self.ships)) headers = ["Name", "Owner", "Country", "Description", "TEU"] self.tableWidget.setColumnCount(len(headers)) self.tableWidget.setHorizontalHeaderLabels(headers)

The populate table method is quite similar to the populate list method. We begin by clearing the table—this clears both the cells and the vertical and horizontal headers (the row numbers and column titles). We then set the number of rows and columns, as well as the column titles.

We want users to be able to click a column to have the table sort by that column's contents. This functionality is built into QTableWidget, but it must be switched off before populating the table.* We will switch sorting back on once the table is populated.

for row, ship in enumerate(self.ships): item = QTableWidgetItem(ship.name) item.setData(Qt.UserRole, QVariant(long(id(ship)))) if selectedShip is not None and selectedShip == id(ship):

selected = item self.tableWidget.setItem(row, ships.NAME, item) self.tableWidget.setItem(row, ships.OWNER, QTableWidgetItem(ship.owner))

We begin by clearing the widget. Then we iterate over every ship in the ships container. The inOrder() method is provided by our custom ShipContainer class. For each ship we create a single list widget item that holds a single string. We use QString.arg() so that we can use %L to show the TEUs with the appropriate digit separators (e.g., commas).

*In Qt 4.0 and 4.1, forgetting to switch off sorting before repopulating a table is harmless, but from Qt 4.2 it must be done.

self.tableWidget.setItem(row, ships.COUNTRY,

QTableWidgetItem(ship.country)) self.tableWidget.setItem(row, ships.DESCRIPTION,

QTableWidgetItem(ship.description)) item = QTableWidgetItem(QString("%L1") \

.arg(ship.teu, 8, 10, QChar(" "))) item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter) self.tableWidget.setItem(row, ships.TEU, item) self.tableWidget.setSortingEnabled(True) self.tableWidget.resizeColumnsToContents() if selected is not None:

selected.setSelected(True) self.tableWidget.setCurrentItem(selected)

For each ship we must create a separate table item for each cell in the row that is used to show its data. The column indexes, NAME, OWNER, and so on, are integers from the ships module.

In the first item of each row we set the text (the ship's name) and, as user data, the ship's ID. Storing the ID gives us a means of going from a table item to the ship that the item's row represents. This works because the ShipContainer is a dictionary whose keys are ship IDs and whose values are ships.

For simple text items we can usually create the item and insert it into the table in a single statement: We have done this for the owner, country, and description attributes. But if we want to format the item or store user data in it, we must create the item separately, then call its methods, and finally put it in the table with setItem(). We used this second approach to store the ships' IDs as user data, and to right-align the TEU values.

The TEU values are integers, and the QString.arg() method used takes four arguments: an integer, a minimum field width, a number base, and a character to pad with, should padding be necessary to reach the minimum field width.

Once the table is populated we switch sorting back on, resize each column to the width of its widest cell, and make the selected item (if any) current and selected.

Populating lists and tables is very similar because they both use a rows-and-columns approach. Populating trees is quite different because we must use a parents-and-children approach. The tree view of the ships data has two columns. The first column is the tree with the root items being countries, the next level items being owners, and the bottom-level items being the ships themselves. The second column shows just the TEUs. We could have added a third column to show the descriptions, but doing so does not make any difference in terms of understanding how the tree widget works.

def populateTree(self, selectedShip=None): selected = None self.treeWidget.clear()

self.treeWidget.setColumnCount(2)

self.treeWidget.setHeaderLabels(["Country/Owner/Name", "TEU"]) self.treeWidget.setItemsExpandable(True) parentFromCountry = {} parentFromCountryOwner = {}

We start off in a similar way to before, clearing the tree and setting up its columns and column titles. We also set the tree's items to be expandable. We will explain the two dictionaries in a moment.

for ship in self.ships.inCountryOwnerOrder():

ancestor = parentFromCountry.get(ship.country) if ancestor is None:

ancestor = QTreeWidgetItem(self.treeWidget,

[ship.country]) parentFromCountry[ship.country] = ancestor countryowner = ship.country + "/" + ship.owner parent = parentFromCountryOwner.get(countryowner) if parent is None:

parent = QTreeWidgetItem(ancestor, [ship.owner]) parentFromCountryOwner[countryowner] = parent item = QTreeWidgetItem(parent, [ship.name,

QString("%L1").arg(ship.teu)]) item.setTextAlignment(1, Qt.AlignRight|Qt.AlignVCenter) if selectedShip is not None and selectedShip == id(ship):

selected = item self.treeWidget.expandItem(parent) self.treeWidget.expandItem(ancestor)

Each ship must have an owner parent in the tree, and each owner must have a country parent in the tree.

For each ship we check to see whether there is an item in the tree for the ship's country. We do this by looking in the parentFromCountry dictionary. If there is not, we create a new country item with the tree widget as its parent, and keep a reference to the item in the dictionary. At this point, we have either retrieved or created the country (ancestor) item.

Then we check to see whether there is an item for the ship's owner in the tree. We look in the parentFromCountryOwner dictionary for this. Again, if there is not, we create a new owner item, with a parent of the country (ancestor) item we just found or created, and keep a reference to the owner item in the dictionary. At this point, we have either retrieved or created the owner (parent) item. Now we create a new item for the ship with the owner as its parent.

We have a parentFromCountryOwner rather than a parentFromOwner dictionary because a particular owner may operate in more than one country.

Tree widget items can have multiple columns, which is why we pass them a list in addition to their parent when we create them. We use the additional columns for ships, just one extra column in fact, to store the ships' TEUs. We right align the TEU number by calling QTreeWidgetItem.setTextAlignment() passing the column number as its first argument.

When adding items to convenience view widgets, we can either create the items with no parent and then add them, for example, using QTableWidget.setItem(), or we can create them with a parent, in which case PyQt will add them for us. We have chosen this second approach for populating the tree.

We have opted to expand every item so that the tree is fully expanded from the start. This is fine for relatively small trees, but not recommended for large ones.

self.treeWidget,resizeColumnToContents(0) self.treeWidget.resizeColumnToContents(l) if selected is not None:

selected.setSelected(True) self.treeWidget.setCurrentItem(selected)

We finish by resizing the two columns and making the selected item (if any) current and selected.

We have left the list and tree views in their default read-only state. This means that the data can be changed only if the user edits items in the table, or if they add or remove ships; so in these cases, we must make sure that we keep the views in sync. In the case of editing, the tableItemChanged() method is called whenever an edit is completed. Users complete an edit by changing focus, for example, clicking outside the item or by pressing Tab, or by pressing Enter; they cancel an edit by pressing Esc.

def tableItemChanged(self, item): ship = self.currentTableShip() if ship is None: return column = self.tableWidget.currentColumn() if column == ships.NAME:

ship.name = item.text().trimmed() elif column == ships.OWNER:

ship.owner = item.text().trimmed() elif column == ships.COUNTRY:

ship.country = item.text().trimmed() elif column == ships.DESCRIPTION:

ship.description = item.text().trimmed() elif column == ships.TEU:

ship.teu = item.text().toInt()[0] self.ships.dirty = True self.populateList()

self.populateTree()

If the user edits an item in the table, we retrieve the corresponding ship and update the appropriate attribute. We use QString.trimmed() to get rid of any leading and trailing whitespace.* We don't have to do anything to the table itself since the edit has already updated it, so we simply repopulate the list and the tree. Repopulating like this is fine for small datasets (up to hundreds of items), but for larger datasets it can be noticably slow. The solution is to update only those items that have been changed and that are visible in the widget. This is done automatically if we use a custom model with a view widget, as we will see in the next section.

def currentTableShip(self):

item = self.tableWidget.item(self.tableWidget.currentRow(), 0) if item is None: return None return self.ships.ship(item.data(Qt.UserRole).toLongLong()[0])

The QTableWidget.item() method returns the table item for the given row and column. We always want the item for the current row and the first column since it is in these items that we store each row's corresponding ship ID.

We then use the ShipContainer.ship() method to retrieve the ship with the given ID. This is fast because the ships are held in a dictionary whose keys are their IDs.

def addShip(self):

ship = ships.Ship(" Unknown", " Unknown", " Unknown")

self.ships.addShip(ship)

self.populateList()

self.populateTree()

self.populateTable(id(ship))

self.tableWidget.setFocus()

self.tableWidget.editItem(self.tableWidget.currentItem())

Adding a new ship is comparatively easy, in part because we don't do any validation. We simply create a new ship with "unknown" values (the leading spaces are to make the values stand out), and add the ship to the ships dictionary. Then we repopulate the list, tree, and table, all of which will retrieve all the ships, including the one we have just created. We pass the new ship's ID to the populate table method to make sure that its first column is the current and selected table item, and give it the keyboard focus. The editItem() call is the programmatic equivalent of the user pressing F2 or double-clicking to initiate editing, and it results in the first field, the ship's name, being editable. The user can edit the remaining fields just by pressing Tab, since the editing state will be preserved until they leave the row or press Enter (or cancel by pressing Esc).

* The QString.simplified() method is also very handy. It removes whitespace from the ends and reduces each internal sequence of one or more whitespace characters to a single space.

def removeShip(self):

ship = self.currentTableShip() if ship is None: return if QMessageBox.question(self, "Ships - Remove",

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

.arg(ship.owner).arg(ship.country), QMessageBox.Yes|QMessageBox.No) == QMessageBox.No: return self.ships.removeShip(ship) self.populateList() self.populateTree() self.populateTable()

Removing ships is even easier than adding them. We retrieve the current ship and then pop up a message box asking the user if they are sure they want to remove the ship. If they click Yes, we remove the ship from the ShipContainer and repopulate the view widgets.

Although using three different views as we have done here is unconventional, the techniques we have used, particularly with the QTableWidget are perfectly general.

The convenience widgets are very useful for small and ad hoc datasets, and can be used without necessarily having a separate dataset—showing, editing, and storing the data themselves. We chose to separate out the data in this example to prepare the ground for using the model/view techniques and in particular, custom models, the subject of the next section.

Was this article helpful?

+2 -4
Tube Traffic Ninja

Tube Traffic Ninja

Discover How You Can Quickly And Easily Dominate Google and YouTube... With Simple Cash Generating Videos. Did you know that YouTube is the second largest search website on the entire Internet? YouTube gets more daily searches than Bing and Yahoo. In fact, there is only one search engine that gets more action.

Get My Free Ebook


Responses

  • massimo
    How to set item widget alignment in qtreewidget?
    8 years ago
  • Ralph
    How to get the text of selected option in combobox in pyqt?
    8 years ago
  • hagos rezene
    How to edit a qlistwidget item in pyqt5?
    3 years ago

Post a comment