Skip to main content

C# Tutorial - Font Scaling [Beginner]


We have looked at fonts in a previous C# tutorial, in Auto Ellipsingin C#. But whereas in that tutorial a string gets cut off if there is not enough room, in this tutorial we are going to take a look at how to scale the font such that the text will fit in whatever you need it in. As a side note, look for a revisit on ellipsing in C# at some point in the future - as one of our readers sent us some interesting info about the performance characteristics of the API calls we used in that tutorial.

The code we will show here today can probably be used with any drawing API with minimal tweaks, because, surprisingly, we don't need to do anything crazy. Essentially, we are going to build a function today that takes in a string and a Size, and returns a font size that will make that string take up that amount of space. Below, you can see some screen shots of the example app at different sizes:

Font Scaling
Screenshot

So how do we do this? Well, here is the method signature:
public static Font AppropriateFont(Graphics g, float minFontSize, 
    float maxFontSize, Size layoutSize, string s, Font f, out SizeF extent)
 
Some of these arguments should be pretty self explanatory. For example, minFontSize and maxFontSize are the minimum and maximum font sizes, respectively, that the function will be allowed to return. The string s is the string that we will be fitting, and the layoutSize is the size that the function will fit the string to. The graphics object, g, is needed because one of the methods we use in the function is attached to the graphics object. The font f that is passed in is needed so we know what font family and style the string will be drawn with (the size set in this font object will actually be completely ignored). Finally, the out param extent is the final size the string ends up being using the font returned by the function.

Onto the body of the method:
public static Font AppropriateFont(Graphics g, float minFontSize, 
    float maxFontSize, Size layoutSize, string s, Font f, out SizeF extent)
{
  if (maxFontSize == minFontSize)
    f = new Font(f.FontFamily, minFontSize, f.Style);

  extent = g.MeasureString(s, f);

  if (maxFontSize <= minFontSize)
    return f;

  float hRatio = layoutSize.Height / extent.Height;
  float wRatio = layoutSize.Width / extent.Width;
  float ratio = (hRatio <  wRatio) ? hRatio : wRatio;

  float newSize = f.Size * ratio;

  if (newSize < minFontSize)
    newSize = minFontSize;
  else if (newSize > maxFontSize)
    newSize = maxFontSize;

  f = new Font(f.FontFamily, newSize, f.Style);
  extent = g.MeasureString(s, f);

  return f;
}
 
Not much code at all, really. First, we check to see if the min and max font size are equal, and if so, we create a new font with that size (because we will return it without doing any work in a moment). Then we measure the string with the current font (because out params always need to be set before a function can return). Next, if the max font size is less than or equal to the min font size, we return the current font. This means that if you passed in a bad min and max, you are going to get back the font you passed in, and if you passed in min and max as the same number, you will get a font back out whose size is that number.

After those checks, we get to the real work. In order to do the calculation in a single pass, we calculate a ratio of the available size over the size of the string. A ratio above 1 means there is space left and we can make the font bigger, and a ratio below 1 means that the font is too big for the space and it need to be made smaller. And guess what? All we need to do is multiply the current font size by that ratio, and we get the correct font size for the space. Cool, eh?

Of course, that font could be above max or below the minimum value provided, so we throw in some checks to constrain the value. Finally, we create the new font, measure the string with that font (so the extent out param has the right value), and return the new font. And that is all there is to it!

Of course, it wouldn't be very nice of me if I didn't give you the code showing how to use this function. So here is the code from the silly example app:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace FontScaling
{
  public partial class Form1 : Form
  {
    string _text = "Resize Me!!";

    public Form1()
    {
      InitializeComponent();
      this.SetStyle(ControlStyles.ResizeRedraw, true);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
      using (Font f = new Font("Tahoma", 15))
      {
        SizeF size;
        using (Font f2 = AppropriateFont(e.Graphics, 5, 50, 
              ClientRectangle.Size, _text, f, out size))
        {
          PointF p = new PointF(
              (ClientRectangle.Width - size.Width) / 2, 
              (ClientRectangle.Height - size.Height) / 2);
          e.Graphics.DrawString(_text, f2, Brushes.Black, p);
        }
      }

      base.OnPaint(e);
    }

    public static Font AppropriateFont(Graphics g, float minFontSize, 
        float maxFontSize, Size layoutSize, string s, Font f, out SizeF extent)
    {
      if (maxFontSize == minFontSize)
        f = new Font(f.FontFamily, minFontSize, f.Style);

      extent = g.MeasureString(s, f);

      if (maxFontSize <= minFontSize)
        return f;

      float hRatio = layoutSize.Height / extent.Height;
      float wRatio = layoutSize.Width / extent.Width;
      float ratio = (hRatio < wRatio) ? hRatio : wRatio;

      float newSize = f.Size * ratio;

      if (newSize < minFontSize)
        newSize = minFontSize;
      else if (newSize > maxFontSize)
        newSize = maxFontSize;

      f = new Font(f.FontFamily, newSize, f.Style);
      extent = g.MeasureString(s, f);

      return f;
    }
  }
}
 
Pretty simple - I find the appropriate font for the string in the OnPaint method, using the current size of the form as the area for the string. I get back the font and draw the string with it, using the size out param (and the size of the form) to center the string on the form.

One neat little tidbit to note here is the ControlStyle I set in the form constructor. By setting the ResizeRedraw style to true, the form will automatically repaint every time it is resized.

And there you go - quick font scaling in C#!

Comments

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >();

C# Snippet - The Many Uses Of The Using Keyword [Beginner]

What is the first thing that pops into your mind when you think of the using keyword for C#? Probably those lines that always appear at the top of C# code files - the lines that import types from other namespaces into your code. But while that is the most common use of the using keyword, it is not the only one. Today we are going to take a look at the different uses of the using keyword and what they are useful for. The Using Directive There are two main categories of use for the using keyword - as a "Using Directive" and as a "Using Statement". The lines at the top of a C# file are directives, but that is not the only place they can go. They can also go inside of a namespace block, but they have to be before any other elements declared in the namespace (i.e., you can't add a using statement after a class declaration). Namespace Importing This is by far the most common use of the keyword - it is rare that you see a C# file that does not h

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as