Writing ODF Files with Qt

Материал из Wiki.crossplatform.ru

(Различия между версиями)
Перейти к: навигация, поиск
(Новая: by Thomas Zander <div class="introduction"> The upcoming release of Qt 4.5 marks the appearance of the QTextDocumentWriter class, making it possible to create OpenDocument Format (ODF) f...)
Строка 1: Строка 1:
by Thomas Zander
by Thomas Zander
<div class="introduction">
<div class="introduction">
-
The upcoming release of Qt 4.5 marks the appearance of the QTextDocumentWriter class, making it possible to create OpenDocument Format (ODF) files from any Qt text document. This opens the door to automated document creation and distribution in a standards-compliant format that users can open in a wide variety of word processors.
+
Предстоящий релиз Qt 4.5 отмечается появлением класса QTextDocumentWriter, который позволяет создавать файлы OpenDocument Format (ODF) из любого текстового документа Qt. Это открывает путь для автоматического создания и распространения документа в стандартном совместимом формате, который позволяет пользователю открыть его в различных текстовых редакторах.
*[[#gettingstarted | Getting Started]]
*[[#gettingstarted | Getting Started]]

Версия 05:56, 22 января 2009

by Thomas Zander

Предстоящий релиз Qt 4.5 отмечается появлением класса QTextDocumentWriter, который позволяет создавать файлы OpenDocument Format (ODF) из любого текстового документа Qt. Это открывает путь для автоматического создания и распространения документа в стандартном совместимом формате, который позволяет пользователю открыть его в различных текстовых редакторах.

A prime use case for automated document creation is report generation. An example of this is a store that performs periodic inventory checks and, thus, needs to know if what the database thinks is on the shelves actually reflects real life. Consider some software that takes the database records and creates a readable document for one day's worth of work. That document can then be exported to ODF and sent to the person who does the work.

center

In this article we will write a simple report generator in the form of a phone bill creator. We will create one class, the PhoneBillWriter, to represent the phone bill for one client, and you can modify it or use it as a starting point for your own reporting needs.

Getting Started

The PhoneBillWriter class has the following definition:

    class PhoneBillWriter
    {
    public:
      PhoneBillWriter(const QString &amp;client);
      ~PhoneBillWriter();
      struct PhoneCall {
        QDateTime date;
        int duration; // in seconds
        int cost; // in euro-cents
      };
      void addPhoneCall(const PhoneCall &amp;call);
      void addGraph(QList<int> values,
                    const QString &amp;subtext);
      void write(const QString &amp;fileName);
 
    private:
      QTextDocument * const m_document;
      QTextCursor m_cursor;
    };

This allows us to create one PhoneBillWriter for each bill and fill it with data, using calls to the addPhoneCall() method. After we have added all our data we call the write() method to finish the current bill. This is a classic case of using the builder pattern.

Internally, the class uses a QTextDocument as can be seen in the list of private members. The QTextDocument class is used in a lot of places in Qt and its widgets. QTextEdit uses one and you can access it using the QTextEdit::document() method. The QTextDocument is a text document with the ability to contain structured text as well as markup (bold and italic) and much more. See Qt's Text Edit demo for an overview.

The class QTextCursor is provided to allow the content of a text document to be manipulated. This is very much in the spirit of having a blinking cursor on your word processor and having the ability to move the cursor around and insert text. The QTextCursor class gives us the ability to do all this programatically.

Writing the Bill

The approach used in this report writer is to create a QTextDocument in the constructor and add the header and nice formatting information that will be the same for each phone bill. Here's a simple implementation:

    PhoneBillWriter::PhoneBillWriter(const QString &amp;client)
        : m_document(new QTextDocument()),
        m_cursor(m_document)
    {
      m_cursor.insertText(QObject::tr(
               "Phone bill for %1\n").arg(client));
 
      QTextTableFormat tableFormat;
      tableFormat.setCellPadding(5);
      tableFormat.setHeaderRowCount(1);
      tableFormat.setBorderStyle(
                  QTextFrameFormat::BorderStyle_Solid);
      tableFormat.setWidth(QTextLength(
                  QTextLength::PercentageLength, 100));
      m_cursor.insertTable(1, 3, tableFormat);
      m_cursor.insertText(QObject::tr("Date"));
      m_cursor.movePosition(QTextCursor::NextCell);
      m_cursor.insertText(QObject::tr("Duration (sec)"));
      m_cursor.movePosition(QTextCursor::NextCell);
      m_cursor.insertText(QObject::tr("Cost"));
    }
 
    PhoneBillWriter::~PhoneBillWriter()
    {
      delete m_document;
    }

We start by adding a personal note and follow this with a table and the table header, which we fill them with the header labels. We don't need to specify in advance how many rows there are in the table.

The interesting bits are the m_cursor usage, which includes call to methods like insertTable() and insertText() to actually modify the document.

The QTextCursor also understands the concepts of selecting and removing text. A very powerful method on the cursor is movePosition(), to which you can pass one of the many values from its MoveOperation enum. This makes the class really behave like a cursor since all the usual navigation operations are there. In this case we use a navigation operation that's new in Qt 4.5: the NextCell operation moves the cursor to the start of the next table cell. As a result, the text passed to the following insertText() call will end up at the start of the table cell.

After setting up the header of the document we are ready to add actual user information to it. The caller can add individual phone calls using the addPhoneCall() method, passing in the individual fields that we show in the table.

    void PhoneBillWriter::addPhoneCall(
                  const PhoneBillWriter::PhoneCall &amp;call)
    {
      QTextTable *table = m_cursor.currentTable();
      table->appendRows(1);
      m_cursor.movePosition(QTextCursor::PreviousRow);
      m_cursor.movePosition(QTextCursor::NextCell);
      m_cursor.insertText(call.date.toString());
      m_cursor.movePosition(QTextCursor::NextCell);
      m_cursor.insertText(QString::number(call.duration));
      m_cursor.movePosition(QTextCursor::NextCell);
 
      QChar euro(0x20ac);
      m_cursor.insertText(QString("%1 %2").arg(euro)
              .arg(call.cost / (double) 100, 0, 'f', 2));
    }

To show the phone call later, we add a row to our table and then continue to add the text to the document for each of the table cells. We call the appropriate QString methods to convert the integer data into text so we can fine-tune the way our data is shown. After all, the goal in our example is to make a pretty looking version of the raw data, and properly formatted text is the way to get there.

Having just text, even with tables, makes for boring reading. Adding graphs or other pictures is always a good way to make a document much more readable. This document talks about creating an ODF document, and the OpenDocument Format allows us to embed images into the final file, unlike HTML, for example.

In order to get the document into the final ODF file all we need to do is insert the image into the QTextDocument. It will not be a big surprise to learn that there is a QTextCursor::insertImage() method to do exactly this.

For our example document, we wanted to add a graph to show the amount of calls the client made in the last few months. For this the following code was used:

    QList<int> callsPerMonth;
    callsPerMonth << 6 << 84 << 76 << 0 << 93 << 128 << 76
                  << 31 << 19 << 4 << 12 << 78;
    phoneBill.addGraph(callsPerMonth, "Your past usage:");

To actually create the graph, our solution is to create a QImage, use a QPainter to draw the values onto it, and then insert the image into the document at the right location.

    void PhoneBillWriter::addGraph(QList<int> values, const QString &amp;subtext)
    {
      const int columnSize = 10;
      int width = values.count() * columnSize;
      int max = 0;
      foreach (int x, values)
        max = qMax(max, x);
      QImage image(width, 100, QImage::Format_Mono);
      QPainter painter(&amp;image);
      painter.fillRect(0, 0, image.width(), image.height(),
                       Qt::white); // background
      for (int index = 0; index < values.count(); ++index) {
        // Adjust scale to our 100 pixel tall image:
        int height = values[index] * 100 / max;
        painter.fillRect(index * columnSize,
            image.height() - height, columnSize, height,
            Qt::black);
      }
      painter.end();
 
      QTextCursor cursor(m_document);
      cursor.movePosition(QTextCursor::End);
      cursor.insertText(subtext);
      cursor.insertBlock();
      cursor.insertImage(image);
    }

First, we calculate the required width of our graph based on the amount of values passed in. We choose to always have a height of 100 pixels and scale the values to fit in that range since most graphs are designed to show the relative sizes of the values to each other.

We use a monochrome format for the image, which means only two colors. You can use any image format you want for ODF, though. Two colors just seems enough for this use case.

After creation of the actual image we create a new QTextCursor and move it to the end of the document, where we insert the graph. An important note is that this cursor has a position that may be different from the m_cursor object. The reason for creating a new cursor here is to make sure the user can still call addPhoneCall() after the addGraph() call without affecting the positioning of the textual content.

The last part of our PhoneBillWriter class is to actually create the OpenDocument Format file from all the information we put into the writer object. The hard work is all done by the QTextDocumentWriter class, which is capable of writing to various formats, like plain text, HTML and ODF. The default is ODF, making the implementation trivial for us:

    void PhoneBillWriter::write(const QString &amp;fileName)
    {
      QTextDocumentWriter writer(fileName);
      writer.write(m_document);
    }

center

The above image shows the final document in OpenOffice.org. As it stands, it's intentionally simple for demonstration purposes, but there's a slightly more decorative example included alongside it in the archive available from the Qt Quarterly Web site.

Drawing Conclusions

The example we have presented is fairly simple and is focused on showing the basic ODF writing capabilities of QTextDocumentWriter rather than making the output look as pretty as possible.

Qt has plenty of features that we can use to improve the appearance of the phone bill and usefulness of the writer. For example, we could use the Qt SQL module to extract real information from an existing database and use QPainter to draw more visually appealing graphs and charts.

We could also create a nice frontend application to help the user create reports, or use Qt's ODF capabilities to do something completely unrelated to phone bill, reports or accounting. What you create with this new feature is up to you!

The source code for the example described in this article can be obtained from the Qt Quarterly Web site.