Strange behavior while computing string width in pixels for simulation of word wrap
Asked Answered
M

2

1

Trying to get string width in C# to simulate wordwrap and position of text (now written in richTextBox).

Size of richTextBox is 555x454 px and I use monospaced font Courier New 12pt.

I tried TextRenderer.MeasureText() and also Graphics.MeasureString() methods.

TextRenderer was returning bigger values than Graphics so text which normally fits into one line, my code determined should be wrapped to other line.

But with using Graphics, on the other hand, my code determined that particular string is shorter than it is printed in original richTextBox so it was wrapped to next line in wrong place.

During debugging I found out that computed widths differs, which is strange because I use monospaced font so widths should be same for all characters. But I get something like this from Graphics.MeasureString()(example.: ' ' - 5.33333254, 'S' - 15.2239571, '\r' - 5.328125).

How can I ACCURATELY compute string width with C# and so simulate word wrap and determine particular text positions in pixels?

Why is width different in different characters when using monospaced font?

Note: I am working on personal Eye tracking project and I want to determine, where particular pieces of text was placed during experiment so I can tell on which words was user looking. For ex. at time t user was looking on point [256,350]px and I know that at this place there is call of method WriteLine. My target visual stimulus is source code, with indents, tabs, line endings, placed in some editable text area (In the future maybe some simple online source code editor).

Here is my code:

    //before method call
    var font = new Font("Courier New", 12, GraphicsUnit.Point);
    var graphics = this.CreateGraphics();
    var wrapped = sourceCode.WordWrap(font, 555, graphics);

    public static List<string> WordWrap(this string sourceCode, Font font, int width, Graphics g)
        {
            var wrappedText = new List<string>(); // output
            var actualLine = new StringBuilder();
            var actualWidth = 0.0f; // temp var for computing actual string length
            var lines = Regex.Split(sourceCode, @"(?<=\r\n)"); // split input to lines and maintain line ending \r\n where they are
            string[] wordsOfLine;

            foreach (var line in lines)
            {
                wordsOfLine = Regex.Split(line, @"( |\t)").Where(s => !s.Equals("")).ToArray(); // split line by tabs and spaces and maintain delimiters separately

                foreach (string word in wordsOfLine)
                {
                    var wordWidth = g.MeasureString(word, font).Width; // compute width of word

                    if (actualWidth + wordWidth > width) // if actual line width is grather than width of text area
                    {
                        wrappedText.Add(actualLine.ToString()); // add line to list
                        actualLine.Clear(); // clear StringBuilder
                        actualWidth = 0; // zero actual line width
                    }

                    actualLine.Append(word); // add word to actual line
                    actualWidth += wordWidth; // add word width to actual line width
                }

                if (actualLine.Length > 0) // if there is something in actual line add it to list
                {
                    wrappedText.Add(actualLine.ToString());
                }
                actualLine.Clear(); // clear vars
                actualWidth = 0;
            }

            return wrappedText;
        }
Millican answered 21/2, 2017 at 9:9 Comment(0)
M
0

So I finally added some things in my code. I will shorten it.

public static List<string> WordWrap(this string sourceCode, Font font, int width, Graphics g)
    {
        g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
        var format = StringFormat.GenericTypographic;
        format.FormatFlags = StringFormatFlags.MeasureTrailingSpaces;

        var width = g.MeasureString(word, font, 0, format).Width;
    }

With this corrections I get correct width of common characters (with use of monospaced font i get equal widths).

But there is still problem with other whitespaces like \t and \n where I get 0.0078125 and 9.6015625 when measuring width with Courier New, 12pt font. The second value is a width of any character typed with this font so it is not a big problem but it would be better to be 0 or am I wrong? If anybody have a suggestion to solve this problem leave a comment please.

Millican answered 21/2, 2017 at 17:0 Comment(0)
C
0

I believe that it is much easier to accomplish your task by obtaining a character under a given location on a screen. For example, if you're using the RichTextBox control, refer to the RichTextBox.GetCharIndexFromPosition method to get the index of the character nearest to the specified location. Here is some sample code that demonstrates the idea:

private void richTextBox1_MouseMove(object sender, MouseEventArgs e)
{
    var textIndex = richTextBox1.GetCharIndexFromPosition(e.Location);
    if (richTextBox1.Text.Length > 0)
        label1.Text = richTextBox1.Text[textIndex].ToString();
}
Caloric answered 21/2, 2017 at 11:19 Comment(2)
It is quite harder. RichTextBox is just for test purposes. And moreover I keep it separated. There will be one application (probably some online source code editor - but now just simple RichTextBox), and another separated application which will process offline data gathered in first application.Millican
Yes, I understand this is complicated, but as far as I understand your final goal, as you say, "so I can tell on which words was user looking". On a web page an approach with calculating text size and simulating a word wrapping is way more complicated because of different screen resolutions, different browsers, different zoom settings and so on and so on. That is why I think that it is easier to obtain text by using certain point on a screen. For web this approach was discussed here: #2444930Caloric
M
0

So I finally added some things in my code. I will shorten it.

public static List<string> WordWrap(this string sourceCode, Font font, int width, Graphics g)
    {
        g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
        var format = StringFormat.GenericTypographic;
        format.FormatFlags = StringFormatFlags.MeasureTrailingSpaces;

        var width = g.MeasureString(word, font, 0, format).Width;
    }

With this corrections I get correct width of common characters (with use of monospaced font i get equal widths).

But there is still problem with other whitespaces like \t and \n where I get 0.0078125 and 9.6015625 when measuring width with Courier New, 12pt font. The second value is a width of any character typed with this font so it is not a big problem but it would be better to be 0 or am I wrong? If anybody have a suggestion to solve this problem leave a comment please.

Millican answered 21/2, 2017 at 17:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.