(Print this page)

Rotate labels on your .Net Windows Forms
Published date: Wednesday, November 27, 2019
On: Moer and Éric Moreau's web site

Windows Forms are not dead. They might not be as sexy as many other UI technologies, but they still work, and a lot of companies have a lot of apps running this “legacy” platform.

Legacy does not mean unsupported by Microsoft, it means that not has much (often near 0!) energy is invested in newer releases of Visual Studio. But since the Visual Studio IDE and the languages used to build apps, it makes it still fun!

There are a lot of snippets out there but not much offering both VB and C# and many not as a re-usable component.

The downloadable code

This month’s solution contains both VB and C# projects. The solution was created using Visual Studio 2019, but the code should work as well in older versions of the .Net Framework because there isn’t really nothing very fancy.

Figure 1: The demo application in action

Building the user control

Since the official label found in your toolbox can’t do it, if you want something reusable, you need to build your own user control exposing some properties like Angle and TextAlign and overriding others like Text and Font. You will also need to handle the Paint event to draw the text on the user control’s surface with the angle required.

To a project, you need to add a class and inherits from the UserControl class. In my demo application, my class is named RotatedLabelVB (or RotatedLabelCS in the C# project).

The full listing of that class reads like this:

Option Strict On

Imports System.ComponentModel


Public Class RotatedLabelVB
    Inherits UserControl

#Region " - Members - "

    Private mintAngle As Integer
    Private mdblRadians As Double
    Private mintAlignment As ContentAlignment = ContentAlignment.TopLeft
    Private mintQuadrant As Integer = 1

#End Region ' - Members - 

#Region " - Properties - "

    <Category("Appearance")>
    <Description("Indicates the angle to which the text will be displayed.")>
    Public Property Angle As Integer
        Get
            Return mintAngle
        End Get
        Set(ByVal pValue As Integer)
            mintAngle = ((pValue Mod 360) + 360) Mod 360 'range must be in 0-360 degrees.
            mdblRadians = Math.PI * mintAngle / 180.0
            CalculateQuadrant()
            Refresh()
        End Set
    End Property

    <Category("Appearance")>
    <Browsable(True)>
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)>
    Public Overrides Property Text As String
        Get
            Return MyBase.Text
        End Get
        Set(ByVal pValue As String)
            MyBase.Text = pValue
            Refresh()
        End Set
    End Property

    <Category("Appearance")>
    <Description("Indicates how the text should be aligned.")>
    Public Property TextAlign As ContentAlignment
        Get
            Return mintAlignment
        End Get
        Set(ByVal pValue As ContentAlignment)
            mintAlignment = pValue
            Refresh()
        End Set
    End Property

#End Region ' - Properties - 

#Region " - Events - "

    Protected Overrides Sub OnPaint(ByVal paintEventArgs As PaintEventArgs)
        'Calculate the text size.
        Dim textSize As SizeF = paintEventArgs.Graphics.MeasureString(Text, Font, Parent.Width)

        Dim x As Integer = Math.Abs(CInt(Math.Ceiling(textSize.Height * Math.Sin(mdblRadians))))
        Dim y As Integer = Math.Abs(CInt(Math.Ceiling(textSize.Height * Math.Cos(mdblRadians))))
        Dim rotatedHeight As Point = New Point(x, y)

        x = Math.Abs(CInt(Math.Ceiling(textSize.Width * Math.Cos(mdblRadians))))
        y = Math.Abs(CInt(Math.Ceiling(textSize.Width * Math.Sin(mdblRadians))))
        Dim rotatedWidth As Point = New Point(x, y)

        Dim textBoundingBox As Size = New Size(rotatedWidth.X + rotatedHeight.X, rotatedWidth.Y + rotatedHeight.Y)
        SetControlSize(textBoundingBox)

        Dim rotationOffset As Point = CalculateOffsetForRotation(rotatedHeight, rotatedWidth, textBoundingBox)
        Dim alignmentOffset As Point = CalculateOffsetForAlignment(textBoundingBox)

        'Apply the transformation and rotation to the graphics.
        paintEventArgs.Graphics.TranslateTransform(rotationOffset.X + alignmentOffset.X, rotationOffset.Y + alignmentOffset.Y)
        paintEventArgs.Graphics.RotateTransform(mintAngle)

        paintEventArgs.Graphics.DrawString(Text, Font, New SolidBrush(ForeColor), 0F, 0F)
        MyBase.OnPaint(paintEventArgs)
    End Sub

    Protected Overrides Sub OnResize(ByVal e As EventArgs)
        MyBase.OnResize(e)
        Refresh()
    End Sub

#End Region ' - Events - 

#Region " - Helper Methods - "

    Private Function CalculateOffsetForRotation(ByRef pRotatedHeight As Point, ByRef pRotatedWidth As Point, ByRef pTextBoundingBox As Size) As Point
        Dim offset As Point = New Point(0, 0)

        Select Case mintQuadrant
            Case 1
                offset.X = pRotatedHeight.X
            Case 2
                offset.X = pTextBoundingBox.Width
                offset.Y = pRotatedHeight.Y
            Case 3
                offset.X = pRotatedWidth.X
                offset.Y = pTextBoundingBox.Height
            Case 4
                offset.Y = pRotatedWidth.Y
        End Select

        Return offset
    End Function

    Private Function CalculateOffsetForAlignment(ByRef pTextBoundingBox As Size) As Point
        Dim offset As Point = New Point(0, 0)

        Select Case mintAlignment
            Case ContentAlignment.TopLeft
                'nothing to do
            Case ContentAlignment.TopCenter
                offset.X = CInt((0.5 * Width - 0.5 * pTextBoundingBox.Width))
            Case ContentAlignment.TopRight
                offset.X = (Width - pTextBoundingBox.Width)
            Case ContentAlignment.MiddleLeft
                offset.Y = CInt((0.5 * Height - 0.5 * pTextBoundingBox.Height))
            Case ContentAlignment.MiddleCenter
                offset.X = CInt((0.5 * Width - 0.5 * pTextBoundingBox.Width))
                offset.Y = CInt((0.5 * Height - 0.5 * pTextBoundingBox.Height))
            Case ContentAlignment.MiddleRight
                offset.X = (Width - pTextBoundingBox.Width)
                offset.Y = CInt((0.5 * Height - 0.5 * pTextBoundingBox.Height))
            Case ContentAlignment.BottomLeft
                offset.Y = (Height - pTextBoundingBox.Height)
            Case ContentAlignment.BottomCenter
                offset.X = CInt((0.5 * Width - 0.5 * pTextBoundingBox.Width))
                offset.Y = (Height - pTextBoundingBox.Height)
            Case ContentAlignment.BottomRight
                offset.X = (Width - pTextBoundingBox.Width)
                offset.Y = (Height - pTextBoundingBox.Height)
        End Select

        Return offset
    End Function

    Private Sub SetControlSize(ByVal pTextBoundingBox As Size)
        If DesignMode Then Return
        If Not AutoSize Then Return

        Width = pTextBoundingBox.Width
        Height = pTextBoundingBox.Height
    End Sub

    Private Sub CalculateQuadrant()
        If mintAngle >= 0 AndAlso mintAngle < 90 Then
            mintQuadrant = 1
        ElseIf mintAngle < 180 Then
            mintQuadrant = 2
        ElseIf mintAngle < 270 Then
            mintQuadrant = 3
        ElseIf mintAngle < 360 Then
            mintQuadrant = 4
        Else
            mintQuadrant = 0
        End If
    End Sub

#End Region ' - Helper Methods - 

End Class
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace DemoLabelRotateCS
{
    public class RotatedLabelCS : UserControl
    {
        #region Members

        private int _angle;
        private double _radians;
        private ContentAlignment _alignment = ContentAlignment.TopLeft;
        private int _quadrant = 1;

        #endregion //Members

        #region Properties

        [Category("Appearance")]
        [Description("Indicates the angle to which the text will be displayed.")]
        public int Angle
        {
            get
            {
                return _angle;
            }
            set
            {
                _angle = ((value % 360) + 360) % 360; // range must be in 0-360 degrees.

                _radians = Math.PI * _angle / 180.0;
                CalculateQuadrant();

                Refresh();
            }
        }

        [Category("Appearance")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public override string Text
        {
            get
            {
                return base.Text;
            }
            set
            {
                base.Text = value;
                Refresh();
            }
        }

        [Category("Appearance")]
        [Description("Indicates how the text should be aligned.")]
        public ContentAlignment TextAlign
        {
            get
            {
                return _alignment;
            }
            set
            {
                _alignment = value;
                Refresh();
            }
        }

        #endregion //Properties

        #region Events

        protected override void OnPaint(PaintEventArgs paintEventArgs)
        {
            // Calculate the text size.
            SizeF textSize = paintEventArgs.Graphics.MeasureString(Text, Font, Parent.Width);

            int x = Math.Abs((int)Math.Ceiling(textSize.Height * Math.Sin(_radians)));
            int y = Math.Abs((int)Math.Ceiling(textSize.Height * Math.Cos(_radians)));
            Point rotatedHeight = new Point(x, y);

            x = Math.Abs((int)Math.Ceiling(textSize.Width * Math.Cos(_radians)));
            y = Math.Abs((int)Math.Ceiling(textSize.Width * Math.Sin(_radians)));
            Point rotatedWidth = new Point(x, y);

            Size textBoundingBox = new Size(rotatedWidth.X + rotatedHeight.X, rotatedWidth.Y + rotatedHeight.Y);
            SetControlSize(textBoundingBox);

            Point rotationOffset = CalculateOffsetForRotation(ref rotatedHeight, ref rotatedWidth, ref textBoundingBox);
            Point alignmentOffset = CalculateOffsetForAlignment(ref textBoundingBox);

            // Apply the transformation and rotation to the graphics.
            paintEventArgs.Graphics.TranslateTransform(rotationOffset.X + alignmentOffset.X, rotationOffset.Y + alignmentOffset.Y);
            paintEventArgs.Graphics.RotateTransform(_angle);

            // Draw the text and let the base class do its painting.
            paintEventArgs.Graphics.DrawString(Text, Font, new SolidBrush(ForeColor), 0f, 0f);
            base.OnPaint(paintEventArgs);
        }

        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            Refresh();
        }

        #endregion // Events

        #region Helper Methods

        private Point CalculateOffsetForRotation(ref Point pRotatedHeight, ref Point pRotatedWidth, ref Size pTextBoundingBox)
        {
            Point offset = new Point(0, 0);

            switch (_quadrant)
            {
                case 1:
                    offset.X = pRotatedHeight.X;
                    break;
                case 2:
                    offset.X = pTextBoundingBox.Width;
                    offset.Y = pRotatedHeight.Y;
                    break;
                case 3:
                    offset.X = pRotatedWidth.X;
                    offset.Y = pTextBoundingBox.Height;
                    break;
                case 4:
                    offset.Y = pRotatedWidth.Y;
                    break;
            }

            return offset;
        }

        private Point CalculateOffsetForAlignment(ref Size pTextBoundingBox)
        {
            Point offset = new Point(0, 0);

            switch (_alignment)
            {
                case ContentAlignment.TopLeft:
                    //nothing to do
                    break;
                case ContentAlignment.TopCenter:
                    offset.X = (int)(0.5 * Width - 0.5 * pTextBoundingBox.Width);
                    break;
                case ContentAlignment.TopRight:
                    offset.X = (Width - pTextBoundingBox.Width);
                    break;
                case ContentAlignment.MiddleLeft:
                    offset.Y = (int)(0.5 * Height - 0.5 * pTextBoundingBox.Height);
                    break;
                case ContentAlignment.MiddleCenter:
                    offset.X = (int)(0.5 * Width - 0.5 * pTextBoundingBox.Width);
                    offset.Y = (int)(0.5 * Height - 0.5 * pTextBoundingBox.Height);
                    break;
                case ContentAlignment.MiddleRight:
                    offset.X = (Width - pTextBoundingBox.Width);
                    offset.Y = (int)(0.5 * Height - 0.5 * pTextBoundingBox.Height);
                    break;
                case ContentAlignment.BottomLeft:
                    offset.Y = (Height - pTextBoundingBox.Height);
                    break;
                case ContentAlignment.BottomCenter:
                    offset.X = (int)(0.5 * Width - 0.5 * pTextBoundingBox.Width);
                    offset.Y = (Height - pTextBoundingBox.Height);
                    break;
                case ContentAlignment.BottomRight:
                    offset.X = (Width - pTextBoundingBox.Width);
                    offset.Y = (Height - pTextBoundingBox.Height);
                    break;
            }

            return offset;
        }

        private void SetControlSize(Size pTextBoundingBox)
        {
            if (DesignMode) return;
            if (!AutoSize) return;

            Width = pTextBoundingBox.Width;
            Height = pTextBoundingBox.Height;
        }

        private void CalculateQuadrant()
        {
            _quadrant = (_angle >= 0 && _angle < 90) ? 1 :
                        (_angle >= 90 && _angle < 180) ? 2 :
                        (_angle >= 180 && _angle < 270) ? 3 :
                        (_angle >= 270 && _angle < 360) ? 4 : 0;
        }

        #endregion //Helper Methods

    }

}

The interesting points worth mentioning about this class are:

  • The Angle property: a new property required to display the text
  • The Text property: because it is not implemented by the base UserControl class
  • The TextAlign property: because it is not implemented by the base UserControl class
  • The OnPaint event: this is what does the heavy job calculating the everything to draw the string on the UserControl

Because we are inheriting from the UserClass, the new controls will already behave link many other controls with built-in properties like Font, Anchor, Dock, …

Once you have this class in your project, you need to build your project and if you don’t have any errors, this new control will be added to your toolbox as shown in figure 2.

Figure 2: The new control available from the toolbox

Building a test UI

Now that we have new control, you might want to test it to find out how it works.

I have created a UI on which the important controls are:

  • A Textbox control: to enter the value to rotate and allowing Multiline
  • A TrackBar control: to easily change the angle of the label (with the Maximum property set to 360)
  • 9 RadioButton controls: to mimic the TextAlign property. Each button has the Appearance property set to Button and its Text property set to blank.
  • A RotatedLabel control: to see the actual result

When you change the Text property of the Textbox control, you also need to update the Text property of the RotatedLabel control (done in the TextChanged event).

When you change the Value property of the TrackBar control, you also need to update the Angle property of the RotatedLabel control (done in the ValueChanged event).

Finally, when one of the 9 RadioButtons is clicked, you need to update the TextAlign property of the RotatedLabel (done in the 9 CheckedChanged events).

The full code of the test form reads like this:

Option Strict On

Public Class Form1
    Protected Overrides Sub OnLoad(e As EventArgs)
        MyBase.OnLoad(e)

        txtTextToRotate.Text = "this line 1" + Environment.NewLine + "line 2" + Environment.NewLine + "line 3 - line 3 - line 3"
        optMiddleCenter.Checked = True
        tbAngle.Value = 315
    End Sub

    Private Sub txtTextToRotate_TextChanged(sender As Object, e As EventArgs) Handles txtTextToRotate.TextChanged
        lblRotated.Text = txtTextToRotate.Text
    End Sub

    Private Sub tbAngle_ValueChanged(sender As Object, e As EventArgs) Handles tbAngle.ValueChanged
        lblRotated.Angle = tbAngle.Value
        lblCurrentAngle.Text = $"Current angle: {tbAngle.Value}"
    End Sub

    Private Sub optTopLeft_CheckedChanged(sender As Object, e As EventArgs) Handles optTopLeft.CheckedChanged
        lblRotated.TextAlign = ContentAlignment.TopLeft
    End Sub

    Private Sub optTopCenter_CheckedChanged(sender As Object, e As EventArgs) Handles optTopCenter.CheckedChanged
        lblRotated.TextAlign = ContentAlignment.TopCenter
    End Sub

    Private Sub optTopRight_CheckedChanged(sender As Object, e As EventArgs) Handles optTopRight.CheckedChanged
        lblRotated.TextAlign = ContentAlignment.TopRight
    End Sub

    Private Sub optMiddleLeft_CheckedChanged(sender As Object, e As EventArgs) Handles optMiddleLeft.CheckedChanged
        lblRotated.TextAlign = ContentAlignment.MiddleLeft
    End Sub

    Private Sub optMiddleCenter_CheckedChanged(sender As Object, e As EventArgs) Handles optMiddleCenter.CheckedChanged
        lblRotated.TextAlign = ContentAlignment.MiddleCenter
    End Sub

    Private Sub optMiddleRight_CheckedChanged(sender As Object, e As EventArgs) Handles optMiddleRight.CheckedChanged
        lblRotated.TextAlign = ContentAlignment.MiddleRight
    End Sub

    Private Sub optBottomLeft_CheckedChanged(sender As Object, e As EventArgs) Handles optBottomLeft.CheckedChanged
        lblRotated.TextAlign = ContentAlignment.BottomLeft
    End Sub

    Private Sub optBottomCenter_CheckedChanged(sender As Object, e As EventArgs) Handles optBottomCenter.CheckedChanged
        lblRotated.TextAlign = ContentAlignment.BottomCenter
    End Sub

    Private Sub optBottomRight_CheckedChanged(sender As Object, e As EventArgs) Handles optBottomRight.CheckedChanged
        lblRotated.TextAlign = ContentAlignment.BottomRight
    End Sub

End Class
using System;
using System.Drawing;
using System.Windows.Forms;

namespace DemoLabelRotateCS
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            txtTextToRotate.Text = "this line 1" + Environment.NewLine + "line 2" + Environment.NewLine + "line 3 - line 3 - line 3";
            optMiddleCenter.Checked = true;
            tbAngle.Value = 315;
        }
        
        private void txtTextToRotate_TextChanged(object sender, EventArgs e)
        {
            lblRotated.Text = txtTextToRotate.Text;
        }

        private void tbAngle_ValueChanged(object sender, EventArgs e)
        {
            lblRotated.Angle = tbAngle.Value;
            lblCurrentAngle.Text = $"Current angle: {tbAngle.Value}";

        }

        private void optTopLeft_CheckedChanged(object sender, EventArgs e)
        {
            lblRotated.TextAlign = ContentAlignment.TopLeft;
        }

        private void optTopCenter_CheckedChanged(object sender, EventArgs e)
        {
            lblRotated.TextAlign = ContentAlignment.TopCenter;
        }

        private void optTopRight_CheckedChanged(object sender, EventArgs e)
        {
            lblRotated.TextAlign = ContentAlignment.TopRight;
        }

        private void optMiddleLeft_CheckedChanged(object sender, EventArgs e)
        {
            lblRotated.TextAlign = ContentAlignment.MiddleLeft;
        }

        private void optMiddleCenter_CheckedChanged(object sender, EventArgs e)
        {
            lblRotated.TextAlign = ContentAlignment.MiddleCenter;
        }

        private void optMiddleRight_CheckedChanged(object sender, EventArgs e)
        {
            lblRotated.TextAlign = ContentAlignment.MiddleRight;
        }

        private void optBottomLeft_CheckedChanged(object sender, EventArgs e)
        {
            lblRotated.TextAlign = ContentAlignment.BottomLeft;
        }

        private void optBottomCenter_CheckedChanged(object sender, EventArgs e)
        {
            lblRotated.TextAlign = ContentAlignment.BottomCenter;
        }

        private void optBottomRight_CheckedChanged(object sender, EventArgs e)
        {
            lblRotated.TextAlign = ContentAlignment.BottomRight;
        }

    }
}

Conclusion

Of course, WPF and other more recent UI platform can do it by changing only a few attributes, but we don’t have that luxury in Windows Forms.

This is probably you will want to keep handy for the day where you will want to rotate some text on a Windows Forms.


(Print this page)