Presentation on theme: "Rapid GUI Programming with Python and Qt"— Presentation transcript:
1 Rapid GUI Programming with Python and Qt DatabasesByRaed S. Rasheed
2 DatabasesPyQt provides a consistent cross-platform API for database access using the QtSql module and PyQt’s model/view architecture. Python also has its own completely different database API, called DB-API, but it isn’t needed with PyQt and is not covered here. The commercial edition of Qt comes with many database drivers, whereas the GPL edition has fewer due to licensing restrictions. The drivers that are available include ones for IBM’s DB2, Borland’s Interbase, MySQL, Oracle, ODBC (for Microsoft SQL Server), PostgreSQL, SQLite, and Sybase. However, like any aspect of PyQt, it is possible to create additional database drivers if one we need is not available.
3 Connecting to the Database To use PyQt’s SQL classes we must import the QtSql module: from PyQt4.QtSql import * A database connection is established by calling the static QSqlDatabase.addDatabase() method, with the name of the driver we want to use. Then we must set various attributes, such as the database’s name, the username, and the password. And ﬁnally, we must call open() to make the connection.
4 Connecting to the Database db = QSqlDatabase.addDatabase("QSQLITE") db.setDatabaseName(filename) if not db.open(): QMessageBox.warning(None, "Phone Log", QString("Database Error: %1").arg(db.lastError().text())) sys.exit(1)
5 Executing SQL Queriesquery = QSqlQuery() query.exec_("""CREATE TABLE outcomes ( id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, name VARCHAR(40) NOT NULL)""")
6 Executing SQL Queriesquery.exec_("""CREATE TABLE calls ( id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, caller VARCHAR(40) NOT NULL, starttime DATETIME NOT NULL, endtime DATETIME NOT NULL, topic VARCHAR(80) NOT NULL, outcomeid INTEGER NOT NULL, FOREIGN KEY (outcomeid) REFERENCES outcomes)""")
8 Executing SQL QueriesNow that we have created the tables, we can populate them with data. for name in ("Resolved", "Unresolved", "Calling back", "Escalate", "Wrong number"): query.exec_("INSERT INTO outcomes (name) VALUES ('%s')" % name)
9 Executing SQL Queriesquery.prepare("INSERT INTO calls (caller, starttime, endtime, " "topic, outcomeid) VALUES (?, ?, ?, ?, ?)") for name, start, end, topic, outcomeid in data: query.addBindValue(QVariant(QString(name))) query.addBindValue(QVariant(start)) # QDateTime query.addBindValue(QVariant(end)) # QDateTime query.addBindValue(QVariant(QString(topic))) query.addBindValue(QVariant(outcomeid)) # int query.exec_()
10 Executing SQL QueriesWe can use QSqlQuery to execute any arbitrary SQL statement. For example: query.exec_("DELETE FROM calls WHERE id = 12")
11 Executing SQL QueriesWe will conclude our coverage of QSqlQuery by looking at how to use it to execute SELECT statements, and how to iterate over the resultant records. DATETIME_FORMAT = "yyyy-MM-dd hh:mm" ID, CALLER, STARTTIME, ENDTIME, TOPIC, OUTCOMEID = range(6) query.exec_("SELECT id, caller, starttime, endtime, topic, " "outcomeid FROM calls ORDER by starttime")
14 Using Database Form Views The simpliﬁed Phone Log application
15 Using Database Form Views With the widgets in place, we create a QSqlTableModel. Since we did not specify a particular database connection, it uses the default one. We tell the model which able it is to work on and call select() to make it populate itself with data. We also choose to apply a sort order to the table. self.model = QSqlTableModel(self) self.model.setTable("calls") self.model.setSort(STARTTIME, Qt.AscendingOrder) self.model.select()
16 Using Database Form Views Now that we have suitable widgets and a model, we must somehow link them together. This is achieved by using a QDataWidgetMapper.. self.mapper = QDataWidgetMapper(self) self.mapper.setSubmitPolicy(QDataWidgetMapper.ManualSubmit) self.mapper.setModel(self.model) self.mapper.addMapping(self.callerEdit, CALLER) self.mapper.addMapping(self.startDateTime, STARTTIME) self.mapper.addMapping(self.endDateTime, ENDTIME) self.mapper.addMapping(topicEdit, TOPIC) self.mapper.toFirst()
17 Using Database Form Views If the user navigates, we must remember the current row, since it is forgotten after calling submit(). Then, after saving the current record, we set the row to be the one appropriate for the navigation the user requested (but kept within bounds), and then use setCurrentIndex() to move to the appropriate record.
18 Using Database Form Views def saveRecord(self, where): row = self.mapper.currentIndex() self.mapper.submit() if where == PhoneLogDlg.FIRST: row = 0 elif where == PhoneLogDlg.PREV: row = 0 if row <= 1 else row - 1 elif where == PhoneLogDlg.NEXT:
19 Using Database Form Views row += 1 if row >= self.model.rowCount(): row = self.model.rowCount() - 1 elif where == PhoneLogDlg.LAST: self.mapper.setCurrentIndex(row)
20 Using Database Form Views row += 1 if row >= self.model.rowCount(): row = self.model.rowCount() - 1 elif where == PhoneLogDlg.LAST: self.mapper.setCurrentIndex(row)
21 Using Database Form Views We have chosen to always add new records at the end. To do this we ﬁnd the row after the last one, save the current record, and then insert a new record at the last row in the model. Then we set the mapper’s current index to the new row, initialize a couple of ﬁelds, and give the caller ﬁeld the focus, ready for the user to start typing.
22 Using Database Form Views def addRecord(self): row = self.model.rowCount() self.mapper.submit() self.model.insertRow(row) self.mapper.setCurrentIndex(row) now = QDateTime.currentDateTime() self.startDateTime.setDateTime(now) self.endDateTime.setDateTime(now) self.callerEdit.setFocus()
23 Using Database Form Views If the user clicks Delete we pick out some information from the current record and use it when we ask the user to conﬁrm the deletion. If they conﬁrm, we retrieve the current row, remove the row from the model, and call submitAll() to force the model to write back the change to the underlying data source (in this case the database). Then we ﬁnish up by navigating to the next record.
24 Using Database Form Views def deleteRecord(self): caller = self.callerEdit.text() starttime = self.startDateTime.dateTime().toString(DATETIME_FORMAT) if QMessageBox.question(self,QString("Delete"),QString("Delete call made by<br>%1 on %2?").arg(caller).arg(starttime),QMessageBox.Yes|QMessageBox.No) == QMessageBox.No: return row = self.mapper.currentIndex() self.model.removeRow(row) self.model.submitAll() if row + 1 >= self.model.rowCount(): row = self.model.rowCount() - 1 self.mapper.setCurrentIndex(row)
25 Using Database Table Views Probably the most natural and convenient way to present database data is to show database tables and views in GUI tables. This allows users to see many records at once, and it is particularly convenient for showing master–detail relationships. The Asset Manager database design
26 Using Database Table Views Probably the most natural and convenient way to present database data is to show database tables and views in GUI tables. This allows users to see many records at once, and it is particularly convenient for showing master–detail relationships.
27 Using Database Table Views The Asset Manager database design
29 Using Database Table Views The view is a standard QTableView, but instead of setting a QSqlRelationalDelegate, we have set a custom delegate. We will detour to look at this in a moment. The selection mode is set so that users can navigate to individual ﬁelds; the selection behavior is that the row that has the focus is highlighted. We don’t want to show the ID ﬁeld since it isn’t meaningful to the user, so we hide it.
31 Using Database Table Views The heart of the createEditor() method is the code that sets up the QLineEdit for entering room numbers. Room numbers are four digits long, made up of a ﬂoor number, in the range 01–27 (but excluding 13), and a room number on the ﬂoor in the range 01–62. For example, 0231 is ﬂoor 2, room 31, but 0364 is invalid. The regular expression is sufﬁcient for specifying valid room numbers, but it cannot set a minimum number of digits, since one, two, or three digits may be a valid preﬁx for a valid four digit room number. We have solved this by using an input mask that requires exactly four digits to be entered. For the other ﬁelds, we pass the work on to the base class.
33 Using Database Table Views The code for creating the log model is almost the same as the code we used for the asset model. We use a QSqlRelationalTableModel because we have a foreign key ﬁeld, and we provide our own column titles. self.logModel = QSqlRelationalTableModel(self) self.logModel.setTable("logs") self.logModel.setRelation(ACTIONID,QSqlRelation("actions", "id", "name")) self.logModel.setSort(DATE, Qt.AscendingOrder) self.logModel.setHeaderData(DATE, Qt.Horizontal,QVariant("Date")) self.logModel.setHeaderData(ACTIONID, Qt.Horizontal,QVariant("Action")) self.logModel.select()