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:
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:
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.