Mould Text in Any Shape

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Sometimes, putting text in a rectangular block may just be a little too straight. I designed the C++ class QEnvelopeText to widen the range of layout options. With it, you can mould a piece of body type in any shape.

It operates in the realm of GDI+, the extension of GDI which was introduced with Windows XP. In go a GraphicsPath with the “envelope” and a text string; out comes another GraphicsPath containing the enveloped text.

Using QEnvelopeText

QEnvelopeText is a “pure” Windows C++ class. It is not dependent on MFC, ATL, or any other framework, and only uses plain Windows APIs. Therefore, it can be used in virtually any C++ Windows project. It needs GDI+, which is included with Windows XP, and can be downloaded for older versions.

The interface can be summarized like this:

class QEnvelopeText
{
public:
   QEnvelopeText(const GraphicsPath& pathEnvelope,
                 REAL flatness = 10.0f * FlatnessDefault);
   ~QEnvelopeText();

   void SetFont(const LOGFONT& logfont, bool bFlipY = true);

   int GetEnvelopeText(GraphicsPath& pathOutput,
                       LPCTSTR str) const;

   enum justification
   {
      justLeft,
      justCenter,
      justRight,
      justFull,
      justHard
   };
   void SetJustification(justification Just);
   justification GetJustification() const;

   bool SetScaleMargins(REAL min, REAL max);
   void GetScaleMargins(REAL& min, REAL& max) const;

   TCHAR m_Hyphen;

protected:
   enum brkType
   {
      brkNone,
      brkNormal,
      brkSpace,
      brkHyphen,
      brkReturn,
      brkHard
   };
   virtual UINT GetBreakType(LPCTSTR str, int index,
                             int len) const;
   ...
};

The constructor takes a reference to the envelope path. The envelope path may have one or more subpaths. Preferably, these are all closed. Open paths are accepted, though—it just isn’t very likely that the output will be very pleasing.

After construction, the font can be set by using the SetFont() method. It takes a reference to a LOGFONT as parameter. Only scaled vector fonts (TrueType, and PostScript in Windows 2000/XP) will work. SetFont() has a second parameter, a boolean called bFlipY. This determines how QEnvelopeText sees the vertical dimension. If bFlipY is false (default), it assumes that the vertical axis is going upward, and if it’s true, the opposite is assumed. If it has an incorrect value, the text characters will appear mirrored and upside down.

After the font is set, it’s just a matter of calling the member function GetEnvelopeText() to output the enveloped text to a second GraphicsPath. You can draw it to the screen, or do any of the many other things you can do with a GraphicsPath.

If the text string happens to be too long to fit inside the envelope, it will simply be truncated. GetEnvelopeText() returns the number of characters it has successfully processed or, in other words, the index of the first character not displayed (which might be the terminating null-character).

GetEnvelopeText() will not empty pathOutput before it adds the enveloped string to it. This means that an enveloped string can be added to any other figure.

Settings

A few settings fine-tune the operation of QEnvelopeText. First, the text justification can be set to left, right, center, full, or hard. The first four options will be known to anyone familiar with text processing. “Hard” justification is like full justification, but a little more extreme. If no space character is available as a break position, a line of text will be broken at a random place, right in the middle of a word. Thus, from a linguistic perspective, the result will often be incorrect. On the other hand, there is virtually no danger that text lines will ever exceed the envelope boundaries.

Generally, full justification will be the way to go, but with more or less rectangular envelopes the other options may come in useful.

Second, the minimum and maximum values for the scaling can be set. These determine the amount by which a strip of text may be horizontally shrunk or stretched to fit it inside the envelope. Defaults are 0.9 (90%) and 1.1 (110%) respectively. Which values to choose is purely a matter of aesthetics and typographic opinion. Some fonts are extremely sensitive to shrinking or stretching, while others may be more tolerant.

Customizing QEnvelopeText

It’s easy to customize QEnvelopeText. The check whether a character is a valid break position is delegated to a separate, virtual member function, that can be overridden by derivates. The signature is:

virtual UINT GetBreakType(LPCTSTR str, int index,
                          int len) const;

It receives three parameters: the text string str, an index in the string, and the length. The function returns a UINT, stating whether the string can be broken at the index position or not.

GetBreakType() may return one of the following six “break types,” defined in the enum brkType:

  • brkNone: index is not acceptable as a position to break the text
  • brkNormal: The text may be broken at index.
  • brkSpace: There is a space character or equivalent at index, and the text can be broken—if QEnvelopeText decides to do so, the character should be skipped.
  • brkHyphen: index is a good position to break the text, and to insert a hyphen before the break.
  • brkReturn: index is the end of a text line—always break text and skip character, don’t apply full justification.
  • brkHard: Break the text at index, no matter what other conditions apply.

The default version of GetBreakType() returns only brkNone, brkNormal, brkSpace, and brkReturn, but an override may return brkHyphen or brkHard as well. In other words, QEnvelopeText is prepared to handle text hyphenation. Of course, it’s far from trivial to implement it. Note that the hyphen character, m_Hyphen, is a public member of the class and, therefore, can be modified.

Inside QEnvelopeText

Like QPathText, another fruit of my experiments with graphic text, QEnvelopeText is derived from QGraphicsText, described in a separate article. This does a lot of the hard work, such as retrieving the font outlines from Windows (and, believe me, it really is hard work). The SetFont() method actually is a member function of QGraphicsText.

QEnvelopeText’s main task is to set up a collection of horizontal strips that will contain the text fragments. It has to find all the intersections of the top line of potential strips with the envelope path, and do the same for the bottom line. To make this manageable, the envelope path is first “flattened,” right after it is loaded in QEnvelopeText’s constructor. Strips will exist where both the top line and the bottom line are at the inside of the envelope path. It was a challenging puzzle to concoct an algorithm that would be correct in even the most pathological cases, but I’m reasonably confident that I succeeded.

QBlockText

In fact, QEnvelopeText is derived from another class, QBlockText. This class performs the layout of a text string in a rectangular block, and can be used on its own. Of course, QEnvelopeText manages the same if it is loaded with a rectangular envelope path, but QBlockText is somewhat smaller and, at least theoretically, more efficient. In a sense, QEnvelopeText can be seen as a generalization of QBlockText.

If you intend to create only rectangular text blocks, QBlockText will suffice. Instead of GetEnvelopeText(), it has the following member function:

int QBlockText::GetBlockText(GraphicsPath& pathOutput,
                             LPCTSTR str,
                             const RectF& rect) const;

It puts the string in a rectangular envelope defined by the GDI+ RectF rect. QBlockText shares all the other member functions, like SetFont(), with QEnvelopeText. In fact, the latter inherits them from the former.

Demo

The demo speaks for itself. There are quite a lot of options to show QEnvelopeText’s abilities (try resizing the window too). The results won’t always be very pleasing to the eye. From a typographical point of view, one strong advice about QEnvelopeText should be given: Use with extreme care!

Note: your system must support GDI+, which currently only XP does natively. However, other Windows versions can be upgraded. Also, VC++ 6.0 comes without the GDI+ headers. You may obtain them by downloading the Windows Platform SDK. The GDI+ headers are included with VC++ 7.0 and later.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read