Printing a Worksheet

Problem: Fabian Steiner wanted to know how to print an A4 sized worksheet using addresses from a SQLite database.

Solution (quick incomplete one): Use Qt's rich text support and an HTML template to generate the worksheet, then print using QPrinter and QSimpleRichText.

Note: This code was written for use with PyQt and Qt 3. For a Qt 4 application, you could do things quite differently.

The Code

As usual, we need to import a few classes from the qt module:

   1 import sys
   2 from qt import QApplication, QInputDialog, QKeySequence, QMainWindow, \
   3                QPopupMenu, QPaintDeviceMetrics, QPainter, QPrinter, QRect, \
   4                QSimpleRichText, QTextBrowser, SIGNAL

We'll define a simple worksheet template. The real one could be a lot more complicated.

   1 template = """<html>
   2 <head>
   3   <title>Worksheet for %(name)s</title>
   4 </head>
   5 <body>
   6 <center>
   7 <table width="80%%">
   8 <tr>
   9  <th>Name:</th><td>%(name)s</td><th>Date:</th><td>...</td>
  10 </tr><tr>
  11  <th>Role:</th><td>...</td><th>Departure:</th><td>...</td>
  12 </tr><tr>
  13  <th>Address:</th><td colspan="3">...</td>
  14 </table>
  15 </center>
  16 </body>
  17 </html>
  18 """

To encapsulate everything associated with a worksheet, we define a Sheet class:

   1 class Sheet:
   2 
   3     def __init__(self, name):
   4     
   5         self.name = name
   6         self.text = template % {"name": name}

Although we use the template to create the worksheet's HTML when the class is instantiated, we could instead implement a method to generate it on demand.

Our application will have a main window containing a text browser (we could use an editor instead) and menus that the user accesses to create and print worksheets.

   1 class Window(QMainWindow):
   2 
   3     def __init__(self):
   4     
   5         QMainWindow.__init__(self)
   6         
   7         self.sheets = []
   8         self.sheetIds = {}
   9         self.currentSheet = None

The main window contains a list of Sheet instances, keeps a record of the current sheet in use. We also use a dictionary to map menu IDs to Sheet instances; more about this later.

   1         self.textEdit = QTextBrowser(self)
   2         self.setCentralWidget(self.textEdit)

The main window's central widget is a text browser - a read-only text editor.

We create a standard File menu containing the basic options required for this minimal example:

   1         self.fileMenu = QPopupMenu(self)
   2         self.fileMenu.insertItem(self.tr("&New"), self.newSheet,
   3                                  QKeySequence("Ctrl+N"))
   4         self.fileMenu.insertItem(self.tr("&Print"), self.printSheet,
   5                                  QKeySequence("Ctrl+P"))
   6         self.fileMenu.insertSeparator()
   7         self.fileMenu.insertItem(self.tr("E&xit"), self.close,
   8                                  QKeySequence("Ctrl+Q"))

Note that each of the menu items are defined with references to methods such as newSheet(), printSheet() and close(). This means that, when the items are selected by the user, these methods will be called.

The Sheets menu shows a list of the worksheets that have been created, allowing the user to select an existing worksheet in order to view it. To implement this, we need to connect some signals from the menu to slots (just Python methods) in this class:

   1         self.sheetsMenu = QPopupMenu(self)
   2         self.sheetsMenu.setCheckable(True)
   3         self.connect(self.sheetsMenu, SIGNAL("aboutToShow()"),
   4                      self.setupSheetsMenu)
   5         self.connect(self.sheetsMenu, SIGNAL("activated(int)"),
   6                      self.showSheet)

When the sheetsMenu object's aboutToShow() signal is emitted, the setupSheetsMenu() method will be called so that we can populate the menu with the names of the available worksheets. The activated() signal is connected to a method that performs the task of showing the selected worksheet in the text browser.

   1         self.menuBar().insertItem(self.tr("&File"), self.fileMenu)
   2         self.menuBar().insertItem(self.tr("&Worksheets"), self.sheetsMenu)
   3         
   4         self.setCaption(self.tr("Worksheet Creator"))

The remaining initialization tasks are to add the File and Worksheets menus to the main window's menu bar and set the window title (its caption).

Before the Worksheets menu is opened, it emits the aboutToBeShown() signal. Since we connected this signal to the setupSheetsMenu() method, we get the chance to update the menu before it is shown:

   1     def setupSheetsMenu(self):
   2     
   3         self.sheetsMenu.clear()
   4         self.sheetIds = {}
   5         for sheet in self.sheets:
   6         
   7             menuId = self.sheetsMenu.insertItem(sheet.name)
   8             if sheet == self.currentSheet:
   9                 self.sheetsMenu.setItemChecked(menuId, True)
  10             self.sheetIds[menuId] = sheet

All we do is clear the existing menu items, reset the sheetIds dictionary so that we can define a new mapping between menu IDs and Sheet instances, and populate the menu with the names of the worksheets in the instance's sheets list. For each sheet listed in the menu, we relate the ID created for it by the menu to the relevant Sheet instance in the sheetIds dictionary.

When an item in the Worksheets menu is selected by the user, the showSheet() method is called because it is connected to the menu's activated() signal. The signal specifies the menu ID that was selected, and we use the mapping defined by the sheetIds dictionary to retrieve the Sheet instance associated with that ID:

   1     def showSheet(self, menuId):
   2     
   3         self.currentSheet = self.sheetIds[menuId]
   4         self.textEdit.setText(self.currentSheet.text)
   5         self.setCaption(self.tr("%1 - Worksheet Creator").arg(
   6                         self.currentSheet.name))

We update the current sheet to be the one selected and insert its contents into the text browser. We also change the window's caption to reflect this.

When the user wants to create a new worksheet, they select the File|New menu item, and the newSheet() method is called. (In the original problem, the data required for the new sheet would come from the database.)

   1     def newSheet(self):
   2     
   3         name, valid = QInputDialog.getText(self.tr("New Worksheet"),
   4             self.tr("Input the name of the worksheet's recipient."))
   5         
   6         if not valid or unicode(name) == u"":
   7             return
   8         
   9         self.currentSheet = Sheet(name)
  10         self.sheets.append(self.currentSheet)
  11         self.textEdit.setText(self.currentSheet.text)

All we do in this case is to ask for a name, check that it is valid (or at least not empty), and create a new Sheet instance with the information supplied. We also make the new sheet the current sheet, append it to the list of available worksheets, and insert the contents of the sheet into the text browser.

The printSheet() method, called when the user selects the File|Print menu item, needs to take the contents of the text browser and format it for printing.

Taking the original code from the Usenet posting, we create a QPrinter object suitable for writing output to a printer, and call its setup() method to request user input in the form of a print dialog:

   1     def printSheet(self):
   2 
   3         printer = QPrinter(QPrinter.PrinterResolution)
   4         if printer.setup():
   5             printer.setPageSize(printer.A4)

The requirements were for printing onto a sheet of A4 paper, so the original code overrides the user's choice of paper.

We use a QPainter to draw onto the QPrinter paint device. Device metrics are obtained so that the contents of the worksheet will be drawn at the correct scale for the device.

   1             painter = QPainter(printer)
   2             metrics = QPaintDeviceMetrics(painter.device())
   3             marginHeight = 6
   4             marginWidth =  8
   5             body = QRect(marginWidth, marginHeight,
   6                          metrics.width() - 2 * marginWidth,
   7                          metrics.height() - 2 * marginHeight)

A rectangle within the page area is created to represent the area available for drawing. We create an object to handle drawing of the rich text in the text browser, and use the rectangle's width to contrain the horizontal layout of the text on the page:

   1             richText = QSimpleRichText(self.textEdit.text(),
   2                 self.textEdit.font(), self.textEdit.context(),
   3                 self.textEdit.styleSheet(),
   4                 self.textEdit.mimeSourceFactory())
   5             richText.setWidth(painter, body.width())

This ensures that the text is drawn within the margins defined above.

Finally, we draw the margin rectangle (for illustration purposes) and call the rich text object's draw() method with arguments suitable for printing a single page:

   1             painter.drawRect(body)
   2             richText.draw(painter, body.left(), body.top(), body,
   3                           self.textEdit.colorGroup())
   4             painter.end()

For multiple pages of output, we would make a copy of the body rectangle, and displace it vertically for each page, and pass the appropriate values to the draw() function.

To complete the script, we set up a QApplication object, create and show a single Window instance, and run the application's main loop:

   1 if __name__ == "__main__":
   2 
   3     app = QApplication(sys.argv)
   4     window = Window()
   5     window.resize(640, 480)
   6     window.show()
   7     app.setMainWidget(window)
   8     sys.exit(app.exec_loop())

Comments

Add comments or suggestions below this line.

PyQtWiki: Printing_a_Worksheet (last edited 2009-04-14 07:27:22 by localhost)