In this last part, two private methods, not included in the original example, have been added to the
AddressWidget:
_revName() and
_switchTab() and the
addEntry() and
_setUpTabs() methods have been modified to accomodate their use.
_revName() was created to allow the Address Book entry names to be sorted, and displayed, based on last, rather than first, names.
_switchTab() was added to flip the active tab to the tab containing the newly added name.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
PyQt4 conversion of Qt Address Book (Tabbed) Example
The address book example shows how to use proxy models to display
different views onto data from a single model.
This example provides an address book that allows contacts to be
grouped alphabetically into 9 groups:
ABC, DEF, GHI, ... , VW, ..., XYZ.
This is achieved by using multiple views on the same model, each of
which is filtered using an instance of the QSortFilterProxyModel class.
BEHAVIOUR:
=========
Entries are sorted on their last names.
When a new entry is added, the display switches to the tab
containing the entry
NOTES:
=====
AddressWidget
- added private method _revName(name), called by addEntry()
to put the input name into Last Name, First Name order so
sort occurs on last name
- added private method _swtichTab(name), called by addEntry()
to switch to the tab of the newly entered name. Required
changing 'groups' list in _setUpTabs() to a private
attribute self._groups
- modified _removeEntry() to switch to 'Address Book' tab
if their are no entries
last modified: 2012-01-29 jg
ref:
http://developer.qt.nokia.com/doc/qt-4.8/itemviews-addressbook.html
'''
import pickle
from PyQt4.QtGui import (QApplication, QMainWindow, QWidget, QAction, QFileDialog,
QLabel, QPushButton, QVBoxLayout, QSortFilterProxyModel,
QTableView, QAbstractItemView, QTabWidget, QItemSelection,
QDialog, QLineEdit, QTextEdit, QGridLayout, QHBoxLayout,
QMessageBox, QItemSelectionModel)
from PyQt4.QtCore import (pyqtSlot, pyqtSignal, Qt, QAbstractTableModel, QModelIndex,
QRegExp)
class AddDialog(QDialog):
def __init__(self, parent=None): # initialise base class
super(AddDialog, self).__init__(parent)
nameLabel = QLabel(self.tr("Name"))
addrLabel = QLabel(self.tr("Address"))
okBtn = QPushButton(self.tr("OK"))
cancelBtn = QPushButton(self.tr("Cancel"))
self.nameText = QLineEdit()
self.addrText = QTextEdit()
grid = QGridLayout()
grid.setColumnStretch(1, 2)
grid.addWidget(nameLabel, 0, 0)
grid.addWidget(self.nameText, 0, 1)
grid.addWidget(addrLabel, 1, 0, Qt.AlignLeft | Qt.AlignTop)
grid.addWidget(self.addrText, 1, 1, Qt.AlignLeft)
btnLayout = QHBoxLayout()
btnLayout.addWidget(okBtn)
btnLayout.addWidget(cancelBtn)
grid.addLayout(btnLayout, 2, 1, Qt.AlignRight)
mainLayout = QVBoxLayout()
mainLayout.addLayout(grid)
self.setLayout(mainLayout)
# connect signals/slots for event handling
okBtn.clicked.connect(self.accept)
cancelBtn.clicked.connect(self.reject)
self.setWindowTitle(self.tr("Add a Contact"))
class NewAddressTab(QWidget):
def __init__(self, parent=None): # initialise base class
super(NewAddressTab, self).__init__(parent)
descripLabel = QLabel(self.tr(
"There are currently no contacts in your Address Book"
"\nClick Add to add new contacts."))
addBtn = QPushButton(self.tr("Add"))
addBtn.clicked.connect(self._addEntry)
layout = QVBoxLayout()
layout.addWidget(descripLabel)
layout.addWidget(addBtn)
self.setLayout(layout)
# signal ------------------------------------------------------------------
sendDetails = pyqtSignal(str, str, name="sendDetails")
# private methods ---------------------------------------------------------
def _addEntry(self):
aDialog = AddDialog()
if aDialog.exec():
name = aDialog.nameText.text()
addr = aDialog.addrText.toPlainText()
self.sendDetails.emit(name, addr)
class TableModel(QAbstractTableModel):
def __init__(self, parent=None, pairs=[['', '']]): # initialise base class
super(TableModel, self).__init__(parent)
self._listOfPairs = pairs
# overridden methods ------------------------------------------------------
def rowCount(self, parent=QModelIndex()):
return len(self._listOfPairs)
def columnCount(self, parent=QModelIndex()):
return 2
def data(self, index, role=Qt.DisplayRole):
# displays the row contents
if not index.isValid():
return None
if (index.row() >= len(self._listOfPairs) or
index.row() < 0):
return None
if role == Qt.DisplayRole:
pair = self._listOfPairs[index.row()]
if index.column() == 0:
return pair[0]
elif index.column() == 1:
return pair[1]
return None
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return None
if orientation == Qt.Horizontal:
if section == 0: return self.tr("Name")
elif section == 1: return self.tr("Address")
else: return None
return None
def insertRows(self, position, rows, index=QModelIndex()):
self.beginInsertRows(QModelIndex(), position, position + rows - 1)
for r in range(rows):
pair = [" ", " "]
self._listOfPairs.insert(position, pair)
self.endInsertRows()
return True
def removeRows(self, position, rows, index=QModelIndex()):
self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
for r in range(rows):
# use list slicing to remove rows
self._listOfPairs = (self._listOfPairs[:position] +
self._listOfPairs[position + rows:])
self.endRemoveRows()
return True
def setData(self, index, value, role=Qt.EditRole):
# updates the table model and emits a signal
# to notify views that data has changed
if index.isValid() and role == Qt.EditRole:
row = index.row()
pair = self._listOfPairs[row]
if index.column() == 0:
pair[0] = value
elif index.column() == 1:
pair[1] = value
else:
return False
self._listOfPairs[row] = pair
# this signal is defined in the QAbstractTableModel
# parent class
self.dataChanged.emit(index, index)
return True
return False
def flags(self, index):
if not index.isValid():
return Qt.ItemIsEnabled
return Qt.ItemFlags(QAbstractTableModel.flags(self, index) |
Qt.ItemIsEditable)
# public methods ----------------------------------------------------------
def getList(self):
return self._listOfPairs
class AddressWidget(QTabWidget):
def __init__(self, parent=None): # initialise base class
super(AddressWidget, self).__init__(parent)
self._table = TableModel(self)
self._newAddressTab = NewAddressTab(self)
self._newAddressTab.sendDetails.connect(self.addEntry)
self.addTab(self._newAddressTab, "Address Book")
self._setUpTabs()
# signals --------------------------------------------------------------
selectionChanged = pyqtSignal(QItemSelection, name="selectionChanged")
# public slots ---------------------------------------------------------
@pyqtSlot(str, str)
def addEntry(self, name=None, address=None):
if not name:
aDialog = AddDialog()
if aDialog.exec():
name = aDialog.nameText.text()
address = aDialog.addrText.toPlainText()
# recursive call
self.addEntry(name, address)
else:
entries = self._table.getList()
name = self._revName(name) # use last name for sorting
pair = [name, address]
if not pair in entries:
self._table.insertRows(0, 1, QModelIndex())
index = self._table.index(0, 0, QModelIndex())
self._table.setData(index, name, Qt.EditRole)
index = self._table.index(0, 1, QModelIndex())
self._table.setData(index, address, Qt.EditRole)
self.removeTab(self.indexOf(self._newAddressTab))
self._switchTab(name)
else:
QMessageBox.information(
self,
self.tr("Duplicate Name"),
self.tr("The name {0} already exists.".format(name)))
@pyqtSlot()
def editEntry(self):
# only the address can be edited
temp = self.currentWidget()
proxy = temp.model()
selModel = temp.selectionModel()
indexes = selModel.selectedRows()
row = -1
for index in indexes:
row = proxy.mapToSource(index).row()
i = self._table.index(row, 0, QModelIndex())
name = self._table.data(i, Qt.DisplayRole)
i = self._table.index(row, 1, QModelIndex())
address = self._table.data(i, Qt.DisplayRole)
aDialog = AddDialog()
aDialog.setWindowTitle(self.tr("Edit an Entry"))
aDialog.nameText.setReadOnly(True)
aDialog.nameText.setText(name)
aDialog.addrText.setText(address)
if aDialog.exec():
newAddr = aDialog.addrText.toPlainText()
if newAddr != address:
i = self._table.index(row, 1, QModelIndex())
self._table.setData(i, newAddr, Qt.EditRole)
@pyqtSlot()
def removeEntry(self):
temp = self.currentWidget()
proxy = temp.model()
selModel = temp.selectionModel()
indexes = selModel.selectedRows()
for index in indexes:
row = proxy.mapToSource(index).row()
self._table.removeRows(row, 1, QModelIndex())
# at this point, 'rowCount' still includes the deleted entry
if self._table.rowCount(QModelIndex()) <= 1:
self.insertTab(0, self._newAddressTab, "Address Book")
self.setCurrentIndex(0)
# public methods ---------------------------------------------------------
def readFromFile(self, fname):
with open(fname, 'rb') as f:
pairs = pickle.load(f)
for i in range(len(pairs) - 1):
p = pairs[i]
self.addEntry(p[0], p[1])
def writeToFile(self, fname):
with open(fname, 'wb') as f:
pickle.dump(self._table.getList(), f, pickle.HIGHEST_PROTOCOL)
# private methods ---------------------------------------------------------
def _setUpTabs(self):
self._groups = ["ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"]
for i in range(len(self._groups)):
tabName = self._groups[i] # 'str' is a reserved word in Python
proxyModel = QSortFilterProxyModel(self)
proxyModel.setSourceModel(self._table)
proxyModel.setDynamicSortFilter(True)
tableView = QTableView()
tableView.setModel(proxyModel)
tableView.setSortingEnabled(True)
tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
tableView.horizontalHeader().setStretchLastSection(True)
tableView.verticalHeader().hide()
tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)
tableView.setSelectionMode(QAbstractItemView.SingleSelection)
# use a QRegExp to limit names displayed on each tab
# to their appropriate tab group
newStr = "^[{0}].*".format(tabName)
proxyModel.setFilterRegExp(QRegExp(newStr, Qt.CaseInsensitive))
proxyModel.setFilterKeyColumn(0)
proxyModel.sort(0, Qt.AscendingOrder)
tableView.selectionModel().selectionChanged.connect(self.selectionChanged)
self.addTab(tableView, tabName)
def _revName(self, name):
# reverse name so sort will be on last name
n = name.rsplit(' ', 1)
if len(n) > 1: # allow single names
rname = n[1] + ", " + n[0]
else:
rname = name
return rname
def _switchTab(self, name):
# switch to the tab that contains the given name
pos = self.currentIndex()
txt = self.tabText(pos)
initial = name[0].upper()
if initial not in txt:
for tab in self._groups:
if initial in tab:
index = self._groups.index(tab)
self.setCurrentIndex(index)
return
class MainWindow(QMainWindow):
def __init__(self, parent=None): # initialise base class
super(MainWindow, self).__init__(parent)
self._addrWidget = AddressWidget()
self.setCentralWidget(self._addrWidget)
self._createFileMenu() # extracted from createMenus()
self._createToolsMenu() # extracted from createMenus()
self._addrWidget.selectionChanged.connect(self._updateActions)
self.setWindowTitle(self.tr("Address Book - Part 5"))
# private methods ---------------------------------------------------------
def _createFileMenu(self):
# create actions
openAct = QAction(self.tr("&Open..."), self)
saveAct = QAction(self.tr("&Save..."), self)
exitAct = QAction(self.tr("E&xit"), self)
# connect signals/slots for event handling
openAct.triggered.connect(self._openFile)
saveAct.triggered.connect(self._saveFile)
exitAct.triggered.connect(QApplication.instance().exit)
# create menu
fileMenu = self.menuBar().addMenu(self.tr("&File"))
fileMenu.addAction(openAct)
fileMenu.addAction(saveAct)
fileMenu.addSeparator()
fileMenu.addAction(exitAct)
def _createToolsMenu(self):
# create actions
addAct = QAction(self.tr("&Add Entry..."), self)
self._editAct = QAction(self.tr("&EditEntry..."), self)
self._removeAct = QAction(self.tr("&Remove Entry"), self)
# connect signals/slots for event handling
addAct.triggered.connect(self._addrWidget.addEntry)
self._editAct.triggered.connect(self._addrWidget.editEntry)
self._removeAct.triggered.connect(self._addrWidget.removeEntry)
# create menu
toolMenu = self.menuBar().addMenu(self.tr("&Tools"))
toolMenu.addAction(addAct)
toolMenu.addAction(self._editAct)
toolMenu.addSeparator()
toolMenu.addAction(self._removeAct)
# set menu item visibility
self._editAct.setEnabled(False)
self._removeAct.setEnabled(False)
# private slots ---------------------------------------------------------
@pyqtSlot()
def _openFile(self):
fname = QFileDialog.getOpenFileName(self)
if fname:
self._addrWidget.readFromFile(fname)
@pyqtSlot()
def _saveFile(self):
fname = QFileDialog.getSaveFileName(self)
if fname:
self._addrWidget.writeToFile(fname)
@pyqtSlot(QItemSelection)
def _updateActions(self, sel):
indexes = sel.indexes()
if indexes:
self._removeAct.setEnabled(True)
self._editAct.setEnabled(True)
else:
self._removeAct.setEnabled(False)
self._editAct.setEnabled(False)
# main ========================================================================
def main():
import sys
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()