Search code examples
.netwinformsgraphicsdrawstringgraphicspath

How can I draw multi-colored text using graphics class on panel?


I want to draw the following text on panel:

sample text

It is a multi-colored text.

I found this article about drawing multicolored text.

I replaced characters with words but it doesn't work.

output-screenshot

(I use FillPath/DrawPath to draw text)

my code:

private void Form1_Paint(object sender, PaintEventArgs e)
    {
        const string txt = "C# Helper! Draw some text with each letter in a random color.";

        // Make the font.
        using (Font the_font = new Font("Times New Roman", 40,
            FontStyle.Bold | FontStyle.Italic))
        {
            // Make a StringFormat object to use for text layout.
            using (StringFormat string_format = new StringFormat())
            {
                // Center the text.
                string_format.Alignment = StringAlignment.Center;
                string_format.LineAlignment = StringAlignment.Center;
                string_format.FormatFlags = StringFormatFlags.NoClip;

                // Make CharacterRanges to indicate which
                // ranges we want to measure.

                MatchCollection mc = Regex.Matches(txt, @"[^\s]+");
                CharacterRange[] ranges = new CharacterRange[mc.Count];
                int g = 0;
                foreach (Match m in mc)
                {
                    ranges[g] = new CharacterRange(m.Index, m.Length);
                    g++;
                }
                string_format.SetMeasurableCharacterRanges(ranges);

                // Measure the text to see where each character range goes.
                Region[] regions =
                    e.Graphics.MeasureCharacterRanges(
                        txt, the_font, this.ClientRectangle,
                        string_format);

                // Draw the characters one at a time.
                for (int i = 0; i < ranges.Length; i++)
                {
                    // See where this character would be drawn.
                    RectangleF rectf = regions[i].GetBounds(e.Graphics);
                    Rectangle rect = new Rectangle(
                        (int)rectf.X, (int)rectf.Y,
                        (int)rectf.Width, (int)rectf.Height);

                    // Make a brush with a random color.
                    using (Brush the_brush = new SolidBrush(RandomColor()))
                    {
                        // Draw the character.
                        string txts = txt.Substring(ranges[i].First, ranges[i].Length);
                        e.Graphics.DrawString(txts,
                            the_font, the_brush, rectf, string_format);
                    }
                }
            }
        }
    }

What is the problem?


Solution

  • This is (in a way) a classic.
    There is a discrepancy between the quite precise measure performed by MeasureCharacterRanges and the actual string drawing performed by Graphics.DrawString.

    The RectagleF returned by Region.GetBounds() considers the measure of the Text as it is.
    Graphics.DrawString, on the other hand, performs a sort of grid-fitting when calculating a Text disposition inside the given bounds.

    I won't explain it here, it's quite a broad matter, but I've written something about it already:
    Drawing a Long String on to a Bitmap results in Drawing Issues.
    If you're interested, you can find some details on the Graphics object behaviour in this context.

    The sum of it is, the Text is measured correctly, but the adjustments that Graphics.DrawString performs, cause the Text to not fit completely in the measured bounds: the drawn Text slightly overflows.

    You could correct this problem using a couple of StringFormat flags:
    Add [StringFormat].Trimming = StringTrimming.None

    With this setting applied, you can immediately see what the problem is: the last char (or few chars) are wrapped to a new line, messing up the drawing.

    To correct it, add StringFormatFlags.NoWrap to StringFormatFlags.NoClip
    This will, apparently, solve the problem. Apparently because now the whole string is drawn on a single line.

    I propose you another method, using TextRenderer.DrawText to render the strings.
    Note that TextRenderer is actually the class used by the WinForms controls (well, not all of them) to render Text to the screen.

    This is the result using the method that follows:

    MeasureCharacterRanges

    Sample code, using your original code with some modifications:

    private void panel1_Paint(object sender, PaintEventArgs e)
    {
        Control control = sender as Control;
        const string txt = "C# Helper! Draw some text with each word in a random color.";
    
        TextFormatFlags flags = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter |
                                TextFormatFlags.NoPadding | TextFormatFlags.NoClipping;
    
        using (StringFormat format = new StringFormat())
        {
            format.Alignment = StringAlignment.Center;
            format.LineAlignment = StringAlignment.Center;
    
            MatchCollection mc = Regex.Matches(txt, @"[^\s]+");
            CharacterRange[] ranges = mc.Cast<Match>().Select(m => new CharacterRange(m.Index, m.Length)).ToArray();
            format.SetMeasurableCharacterRanges(ranges);
    
            using (Font font = new Font("Times New Roman", 40, FontStyle.Regular, GraphicsUnit.Point))
            {
                Region[] regions = e.Graphics.MeasureCharacterRanges(txt, font, control.ClientRectangle, format);
    
                for (int i = 0; i < ranges.Length; i++)
                {
                    Rectangle WordBounds = Rectangle.Round(regions[i].GetBounds(e.Graphics));
                    string word = txt.Substring(ranges[i].First, ranges[i].Length);
    
                    TextRenderer.DrawText(e.Graphics, word, font, WordBounds, RandomColor(), flags);
                }
            }
        }
    }
    
    
    private Random rand = new Random();
    private Color[] colors =
    {
        Color.Red,
        Color.Green,
        Color.Blue,
        Color.Lime,
        Color.Orange,
        Color.Fuchsia,
    };
    
    private Color RandomColor()
    {
        return colors[rand.Next(0, colors.Length)];
    }