Ed Spencer created an awesome library for printing grids and trees. Check it out at Ext.ux.Printer.

The thing is that my app contains a lot of grids that either use row bodies or the RowExpander grid plugin for extra data. I tweaked the GridPanelRenderer so that it would print the extra body if visible. Below is the new code.

Code:
/**
 * @class Ext.ux.Printer.GridPanelRenderer
 * @extends Ext.ux.Printer.BaseRenderer
 * @author Ed Spencer
 * Helper class to easily print the contents of a grid. Will open a new window with a table where the first row
 * contains the headings from your column model, and with a row for each item in your grid's store. When formatted
 * with appropriate CSS it should look very similar to a default grid. If renderers are specified in your column
 * model, they will be used in creating the table. Override headerTpl and bodyTpl to change how the markup is generated.
 * @author pscrawford Enhanced to include the row body.
 * @constructor
 * @param {Object} config
 */
Ext.ux.Printer.GridPanelRenderer = Ext.extend(Ext.ux.Printer.BaseRenderer, {
   /**
    * @property bodyTpl
    * @type Ext.XTemplate
    * The XTemplate used to create each row. This is used inside the 'print' function to build another XTemplate, to which the data
    * are then applied (see the escaped dataIndex attribute here - this ends up as "{dataIndex}")
    */
    bodyTpl: new Ext.XTemplate(
        '<tr>',
            '<tpl for=".">',
                '<td>\{{dataIndex}\}</td>',
            '</tpl>',
        '</tr>'
    )  
  
   /**
    * @property headerTpl
    * @type Ext.XTemplate
    * The XTemplate used to create the headings row. By default this just uses <th> elements, override to provide your own
    */
    ,headerTpl: new Ext.XTemplate(
        '<tr>',
            '<tpl for=".">',
                '<th>{header}</th>',
            '</tpl>',
        '</tr>'
    )
 
    /**
     * @property rowBodySelector
     * @type String
     */
    ,rowBodySelector: '.x-grid3-row-body'
    
    /**
    * Generates the body HTML for the grid
    * @param {Ext.grid.GridPanel} grid The grid to print
    */
    ,generateBody: function(grid) {
        var columns = this.getColumns(grid)
            //use the headerTpl and bodyTpl XTemplates to create the main XTemplate below
            ,headings = this.headerTpl.apply(columns)
            ,body     = this.bodyTpl.apply(columns)
            ,table = [
                '<table>',
                  '<thead>{0}</thead>',
                  '<tpl for=".">{1}',
                    '<tpl if="rowBody">',
                      '<tr><td colspan={2}>{rowBody}</td></tr>',
                    '</tpl>',
                  '</tpl>',
                '</table>'
            ].join('')
        
        return String.format(table, headings, body, columns.length);
    } //eof generateBody
  
    /**
    * Prepares data from the grid for use in the XTemplate
    * @param {Ext.grid.GridPanel} grid The grid panel
    * @return {Array} Data suitable for use in the XTemplate
    */
    ,prepareData: function(grid) {
        //We generate an XTemplate here by using 2 intermediary XTemplates - one to create the header,
        //the other to create the body (see the escaped {} below)
        var columns = this.getColumns(grid)
            //build a useable array of store data for the XTemplate
            ,data = []
            ,ds = grid.getStore()
            ,view = grid.getView()
            ,hasRowBody = view.enableRowBody
            ,sel = this.rowBodySelector
            ,rb
            ,row
            ,el;
            
        ds.each(function(item, rowIndex) {
            var convertedData = {};
            
            //apply renderers from column model
            Ext.iterate(item.data, function(key, value) {
                Ext.each(columns, function(column) {
                    if (column.dataIndex == key) {
                        convertedData[key] = column.renderer ? column.renderer.call(column.scope || this, value, {}, item) : value;
                        return false;
                    }
                }, this);
            });
            
            rb = null;
            if (hasRowBody){
                row = view.getRow(rowIndex);
                if (row){
                    el = Ext.fly(row).child(sel);
                    if (el && el.isVisible()){
                        rb = el.dom.innerHTML;
                    }
                }
            }
            convertedData['rowBody'] = rb;
            
            data.push(convertedData);
        });
        
        return data;
    } //eof prepareData
  
    /**
    * Returns the array of columns from a grid
    * @param {Ext.grid.GridPanel} grid The grid to get columns from
    * @return {Array} The array of grid columns
    */
    ,getColumns: function(grid) {
        var columns = [];
            
        Ext.each(grid.getColumnModel().config, function(col) {
            if (!col.hidden && col.dataIndex){ 
                columns.push(col);
            }
        }, this);
        	
        return columns;
    } //eof getColumns
  
});

Ext.ux.Printer.registerRenderer('grid', Ext.ux.Printer.GridPanelRenderer);
Thanks Ed for such an extensible library!