idlackage
idlackage

Reputation: 2863

How to vertically align non-monospaced text

I'm trying to create two columns of left-aligned text with a variable-width font. My current process:

  1. Measure the string that will be going into the first column
  2. Subtract its width from the maximum width of said first column to get the difference
  3. Divide that with the width of a single space to get the number of spaces needed
  4. Right-pad the result with the necessary number of spaces

This feels logically sound to me, but the output looks like this (with a variable-width font):

a                    12345678910
as                  12345678910
asd                12345678910
asdf               12345678910
asdfg             12345678910

The output I'm looking for is this:

a          12345678910
as         12345678910
asd        12345678910
asdf       12345678910
asdfg      12345678910

What am I missing?

const int maxNameWidth = 150;

Font measurementFont;
Graphics graphics;
float spaceWidth;

void Main()
{
    measurementFont = new Font("Arial", 14);
    graphics = Graphics.FromImage(new Bitmap(1, 1));
    spaceWidth = graphics.MeasureString(" ", measurementFont).Width;

    addRow("a", "12345678910");
    addRow("as", "12345678910");
    addRow("asd", "12345678910");
    addRow("asdf", "12345678910");
    addRow("asdfg", "12345678910");

    measurementFont.Dispose();
    graphics.Dispose();
}

void addRow(string name, string value) {
    float width = graphics.MeasureString(name, measurementFont).Width;
    int amountOfSpacesNeeded = Convert.ToInt32((maxNameWidth - width) / spaceWidth);
    Console.WriteLine(name + " ".PadRight(amountOfSpacesNeeded) + content); // The console font is Arial
}

Edit: Fixed dumb error with padding name instead of actually multiplying the amount of spaces. The result is a lot better now, but still a bit off. I do notice that the width of five spaces is not equal to the spaceWidth * 5 though...

Upvotes: 3

Views: 1912

Answers (2)

Yılmaz Durmaz
Yılmaz Durmaz

Reputation: 3034

Author has added his own solution while I was working on this code. Yet, though his code seems to do its work, it adds to the complexity of the solution. My code has a much simpler algorithm.

void addRows(string name, string value)
{
    string teststring = name + "";
    int spacesAdded = 0;
    while (maxNameWidth > graphics.MeasureString(teststring + "x", measurementFont).Width)
    {
        spacesAdded++;
        teststring += " ";
    }
    Console.WriteLine(":" + name + " ".PadRight(spacesAdded) + ":" + value + ":" + spacesAdded);
}

I used the "x" to get a similar result as his and it can be replaced by other characters to test with more strings to see if it works. I have tested the with only 2 extra strings that include very wide "ooooo" and very narrow "....." to get a satisfying result.


PS: It seems I previously thought differently while answering. Since I am also trying to learn new things and to find new approaches, I tried to get a result for this question. Anyways, this time I am on the right path

Upvotes: 1

idlackage
idlackage

Reputation: 2863

Finally got it. However I think that this solution is disgusting trash so I'll keep this question opened until someone has a nicer/less brute force method.

Basically, since the calculations get me 1-2 spaces off due to incalculable kerning (as far as I can google), I just measure the string again and add or subtract spaces from it until it reaches the opposite side of maxNameWidth from its direction; eg. if it's too short then then add a space, measure again and see if that's over maxNameWidth, if yes then stop, and vice versa.

Full code:

const int maxNameWidth = 150;

Font measurementFont;
Graphics graphics;
float spaceWidth, xWidth;

enum Direction { None, FromHigh, FromLow };

void Main()
{
    measurementFont = new Font("Arial", 14);
    graphics = Graphics.FromImage(new Bitmap(1, 1));
    spaceWidth = graphics.MeasureString(" ", measurementFont).Width;
    xWidth = graphics.MeasureString("x", measurementFont).Width;

    addRow("a", "12345678910");
    addRow("as", "12345678910");
    addRow("asd", "12345678910");
    addRow("asdf", "12345678910");
    addRow("asdfg", "12345678910");

    measurementFont.Dispose();
    graphics.Dispose();
}

void addRow(string name, string value) {

    float width = graphics.MeasureString(name, measurementFont).Width;
    int amountOfSpacesNeeded = Convert.ToInt32((maxNameWidth - width) / spaceWidth);

    string firstColumn = name + " ".PadRight(amountOfSpacesNeeded);
    float currWidth;
    Direction dir = Direction.None;
    while (true) {
        // I add an 'x' here because just measuring a bunch of spaces does not work (the width of one space is apparently equal to the width of five according to graphics)
        currWidth = graphics.MeasureString(firstColumn + "x", measurementFont).Width - xWidth;
        if (((dir == Direction.FromLow) || (dir == Direction.None)) && (currWidth < maxNameWidth)) {
            dir = Direction.FromLow;
            firstColumn += " ";
        }
        else if (((dir == Direction.FromHigh) || (dir == Direction.None)) && (currWidth > maxNameWidth)) {
            dir = Direction.FromHigh;
            firstColumn = firstColumn.Remove(firstColumn.Length - 1);
        }
        else {
            break;
        }
    }

    Console.WriteLine(firstColumn + value);

}

Output (paste somewhere with 14pt Arial to see the alignment):

a                     12345678910
as                   12345678910
asd                 12345678910
asdf                12345678910
asdfg              12345678910

Upvotes: 2

Related Questions