If using WPF is an option you can generate Geometry using FormattedText and then add the Geometry figure by figure to a GraphicsPath object.
to use WPF in a WinForms application you will need a reference to
- PresentationFramework
- WindowsBase
- PresentationCore
these methods might get you started.
public static readonly int FORMATTED_TEXT_MAX_SIZE = 3579139;
private static void addStringUsingFormattedText(GraphicsPath targetPath, string text, System.Drawing.FontFamily fontFamily, int fontStyle, float fontSize, System.Drawing.RectangleF layoutRect, System.Drawing.StringFormat stringFormat)
{
if (string.IsNullOrEmpty(text))
return;
var typeFaceToUse = createTypeFace((System.Drawing.FontStyle)fontStyle, fontFamily.Name);
var ft = createFormattedText(text, fontSize, layoutRect.Width, layoutRect.Height, typeFaceToUse, stringFormat);
var geometry = ft.BuildGeometry(new System.Windows.Point(layoutRect.X, layoutRect.Y));
targetPath.StartFigure();
foreach (PathFigure pf in geometry.GetFlattenedPathGeometry().Figures)
{
foreach (PolyLineSegment polySegment in pf.Segments)
targetPath.AddPolygon(polySegment.Points.Select(point => new System.Drawing.PointF((float)point.X, (float)point.Y)).ToArray());
}
targetPath.CloseFigure();
}
private static FormattedText createFormattedText(string text, float fontSize, float width, float height, Typeface typeFaceToUse, System.Drawing.StringFormat stringFormat)
{
var ft = new FormattedText(text, System.Globalization.CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight
, typeFaceToUse, fontSize, Brushes.Black);
if (width > 0)
ft.MaxTextWidth = Math.Min(FORMATTED_TEXT_MAX_SIZE, width);
if (height > 0)
ft.MaxTextHeight = Math.Min(FORMATTED_TEXT_MAX_SIZE, height);
ft.Trimming = TextTrimming.None;
switch (stringFormat.Alignment)
{
case System.Drawing.StringAlignment.Center:
ft.TextAlignment = TextAlignment.Center;
break;
case System.Drawing.StringAlignment.Far:
ft.TextAlignment = ft.FlowDirection == FlowDirection.LeftToRight ? TextAlignment.Right : TextAlignment.Left;
break;
case System.Drawing.StringAlignment.Near:
ft.TextAlignment = ft.FlowDirection == FlowDirection.LeftToRight ? TextAlignment.Left : TextAlignment.Right;
break;
}
return ft;
}
We noticed that if we have an alignment set on our StringFormat it behaves slightly differently. You might need to adjust your logic for that.
In our case we were also using Graphics.MeasureString() to anticipate the width of the string. These following methods should work:
private static System.Drawing.SizeF measureStringUsingFormattedText(string text, System.Drawing.Font measureFont, System.Drawing.RectangleF layoutRect, System.Drawing.StringFormat stringFormat)
{
if (string.IsNullOrEmpty(text))
return new System.Drawing.SizeF(0, 0);
var typeFaceToUse = createTypeFace(measureFont.Style, measureFont.Name);
var ft = createFormattedText(text, measureFont.Size, layoutRect.Width, layoutRect.Height, typeFaceToUse, stringFormat);
bool includeTrailingSpaces = System.Drawing.StringFormatFlags.MeasureTrailingSpaces == (stringFormat.FormatFlags & System.Drawing.StringFormatFlags.MeasureTrailingSpaces);
var rectangleToReturn = new System.Drawing.SizeF((float)(includeTrailingSpaces ? ft.WidthIncludingTrailingWhitespace : ft.Width), (float)ft.Height);
return rectangleToReturn;
}
private static System.Drawing.SizeF measureStringUsingFormattedText(string text, System.Drawing.Font measureFont, System.Drawing.SizeF layoutArea, System.Drawing.StringFormat stringFormat, out int linesFilled)
{
if (string.IsNullOrEmpty(text))
{
linesFilled = 0;
return new System.Drawing.SizeF(0, 0);
}
var typeFaceToUse = createTypeFace(measureFont.Style, measureFont.Name);
var ft = createFormattedText(text, measureFont.Size, layoutArea.Width, layoutArea.Height, typeFaceToUse, stringFormat);
bool includeTrailingSpaces = System.Drawing.StringFormatFlags.MeasureTrailingSpaces == (stringFormat.FormatFlags & System.Drawing.StringFormatFlags.MeasureTrailingSpaces);
var rectangleToReturn = new System.Drawing.SizeF((float)(includeTrailingSpaces ? ft.WidthIncludingTrailingWhitespace : ft.Width), (float)ft.Height);
linesFilled = (int)Math.Floor(ft.Height / ft.Baseline);
return rectangleToReturn;
}
For us the complicated part is how to get the TypeFace object. You can not convert a System.Drawing.FontFamily to a System.Windows.Media.TypeFace object. As long as you do not need a PrivateFontCollection this is no problem.
private static Typeface createTypeFace(System.Drawing.FontStyle fontStyle, string familyName)
{
var fwToUse = System.Drawing.FontStyle.Bold == (fontStyle & System.Drawing.FontStyle.Bold) ? FontWeights.Bold : FontWeights.Normal;
var fsToUse = System.Drawing.FontStyle.Italic == (fontStyle & System.Drawing.FontStyle.Italic) ? FontStyles.Italic : FontStyles.Normal;
// Create and return a System.Windows.Media.TypeFace object...
// if you previously used PrivateFontCollection you might need to load the font from a file.
}
I haven't done proper performance tests on this. I got the feeling this was slightly slower then just using GDI+.