Friday, April 22, 2016

A textbox that auto-wraps to fit contents

The Windows Forms Textbox control is a thin wrapper for the underlying Win32 control and there is no Paint event you can hang extra code on in the normal way. If you write something like mycontrol.Paint += myFunction; and mycontrol is a Textbox them myFunction() will never get called.

You can go the whole hog and

SetStyle(ControlStyles.UserPaint, true);

and provide a Paint method but you have to do everything as it is no good calling the base.OnPaint(). Give it a try, and you will end up with an astonishingly unresponsive textbox.

I wanted to create a version of a Textbox that would resize itself vertically to accommodate extended word wrapped text. That did not result in any issues – all it needed was a FitToContents() method that retained the set width and allowed the height to vary to fit the control content.

protected virtual void FitToContents() {     Size size = this.GetPreferredSize(new Size(this.Width, 0));     if (multiLine) { this.Height = size.Height; } }

However I also wanted to be able to switch between a label displaying a given string and a textbox capable of editing that string. Just to make that interesting I wanted a “fade in” and “fade out” animation to switch between the two. While I was at it, I also wanted to support a textbox “Placeholder” facility to keep the UI nice and clean. It was that final element that reminded me that it was possible to slide a limited paint facility into position to be used in place of the controls own paint when the control was relatively inactive. My own paint facility only needed to manage the fades and the placeholder text display – all the rest could be left to the underlying control.

Fading text in and out was just a matter of tweaking the Alpha component of the control ForeColor in steps over a defined time period. It was a good opportunity to use the recently introduced async/await functionality although a timer would have worked just fine. The control border is unreachable to all intents and purposes although if the control was set borderless one could be drawn around it and arrangements made to fade that rectangle in and out.

OK – I know - WPF and all that, but I also have this funny feeling that as soon as I invest any real time in WPF code Microsoft are going to announce a newer superer duperer common base for Windows apps and it will all be to no avail. Don’t take advice from me though – anything could happen.

The full code for this class can be found at the bottom of this post – usual caveats.

In case anyone fancies just using the placeholder functionality I have generously added an attribute (AutoMultiLine) that can be set false to stop the control re-sizing in response to text longer than the control width. Just set the PlaceHolderText attribute at design or run time. You can always ignore or remove the code associated with the fade.

The control based upon Label with a matching fade facility is very similar but simpler and again the code can be found below. This label also supports automatic height resizing to match the Text.

Please note that these controls are using a language feature from .NET v4.5 so you would need to switch to using a timer to make use of the fade functionality with earlier versions.

Combining these two custom controls into a UserControl made sense as they could then be addressed together as a single entity. This did have its challenges – partly because the TextBox BackColor property can’t be set Transparent and that makes the fade through a bit clunky in one direction – perhaps I should have gone for a “wipe” effect instead…

namespace Adit.Classes.Controls {     [ToolboxBitmap(typeof(TextBox))]     class MultiLineTextBox : TextBox     {         #region Private Declarations         private Color foreColour;         private Color placeholderColour = Color.Gray;         private int opacity = 256;         private int fadeSteps = 10;         private int fadeTime = 750;         private string placeholderText = "Type here";         private bool showPlaceholderText = false, multiLine = true;         #endregion         public MultiLineTextBox()         {             this.Multiline = multiLine;             this.WordWrap = multiLine;         }         #region Design attributes         [Description("Fade time in milliseconds"), Category("Behavior")]         public int FadeTime         {             get { return fadeTime; }             set { fadeTime = value; }         }         [Description("Number of steps from transparent 0 to opaque 256"), Category("Behavior")]         public int FadeSteps         {             get { return fadeSteps; }             set { fadeSteps = value; }         }         [Description("Placeholder Text"), Category("Appearance")]         public string PlaceHolderText         {             get { return placeholderText; }             set { placeholderText = value; Invalidate(); }         }         [Description("Placeholder Colour"), Category("Appearance")]         public Color PlaceHolderColour         {             get { return placeholderColour; }             set { placeholderColour = value; }         }         [Description("Automatic switch to multiline"), Category("Appearance")]         public bool AutoMultiLine         {             get { return multiLine; }             set {                 multiLine = value;                 Multiline = value;                 WordWrap = value;             }         }         #endregion         #region Public methods         public async void FadeInOut()         {             bool saveUserPaint = GetStyle(ControlStyles.UserPaint);             foreColour = ForeColor;             int opStep = 256 / fadeSteps;             if (Visible)             {                 opacity = 256;                 opStep *= -1;             } else             {                 Visible = true;                 opacity = 0;             }             SetStyle(ControlStyles.UserPaint, true); // set after any Visibility change             opacity = opacity + opStep;             while(opacity > 0 && opacity < 256)             {                 foreColour = fadeColour(opacity, foreColour);                 placeholderColour = fadeColour(opacity, placeholderColour);                 Invalidate();                 await Task.Delay(fadeTime / fadeSteps);                 opacity = opacity + opStep;             }             Visible = (opacity >= 255);             SetStyle(ControlStyles.UserPaint, saveUserPaint);             if (Visible)             {                 Invalidate();                 Focus();             }         }         #endregion         #region Override control methods         protected override void OnResize(EventArgs e)         {             base.OnResize(e);             this.FitToContents();         }         protected override void OnKeyUp(KeyEventArgs e)         {             base.OnKeyUp(e);             FitToContents();         }         protected override void OnTextChanged(EventArgs e)         {             base.OnTextChanged(e);             placeholderToggle();             FitToContents();         }         protected override void OnCreateControl()         {             base.OnCreateControl();             placeholderToggle();         }         protected override void OnPaint(PaintEventArgs e)         {             using (var drawBrush = new SolidBrush((showPlaceholderText)? placeholderColour: foreColour))             {                 e.Graphics.DrawString((showPlaceholderText) ? placeholderText : Text, Font, drawBrush, this.ClientRectangle);                 // The underlying control is probably using TextRenderer.DrawText (gdi not gdi+)             }         }         protected virtual void FitToContents()         {             Size size = this.GetPreferredSize(new Size(this.Width, 0));             if (multiLine) { this.Height = size.Height; }         }         #endregion         #region Private methods         private Color fadeColour(int opacity, Color argbColour)         {             return Color.FromArgb(opacity, argbColour);         }         private void placeholderToggle()         {             showPlaceholderText = (Text.Length > 0) ? false : true;             SetStyle(ControlStyles.UserPaint, showPlaceholderText);         }         #endregion     } }

and

namespace CheckBuilder.Classes.Controls {     public partial class WrapLabel : Label     {         #region Private Declarations         private Color foreColour;         private int opacity = 256;         private int fadeSteps = 10;         private int fadeTime = 750;         private bool fading = false;         #endregion         public WrapLabel()         {             base.AutoSize = false;         }         #region Public methods         public async void FadeInOut()         {             fading = true;             foreColour = ForeColor;             int opStep = 256 / fadeSteps;             if (Visible)             {                 opacity = 256;                 opStep *= -1;             }             else             {                 opacity = 0;                 Visible = true;             }             opacity = opacity + opStep;             while (opacity > 0 && opacity < 256)             {                 Invalidate();                 await Task.Delay(fadeTime / fadeSteps);                 opacity = opacity + opStep;             }             Visible = (opacity >= 255);             fading = false;         }         #endregion         #region Design attributes         [Description("Fade time in milliseconds"), Category("Behavior")]         public int FadeTime         {             get { return fadeTime; }             set { fadeTime = value; }         }         [Description("Number of steps from transparent 0 to opaque 256"), Category("Behavior")]         public int FadeSteps         {             get { return fadeSteps; }             set { fadeSteps = value; }         }         #endregion         #region Override control events         protected override void OnPaint(PaintEventArgs pe)         {             if(fading)             {                 using (var drawBrush = new SolidBrush(fadeColour(opacity, foreColour)))                 {                     pe.Graphics.DrawString(Text, Font, drawBrush, ClientRectangle);                 }             } else             {                 base.OnPaint(pe);             }         }         protected override void OnResize(EventArgs e)         {             base.OnResize(e);             this.FitToContents();         }         protected override void OnTextChanged(EventArgs e)         {             base.OnTextChanged(e);             this.FitToContents();         }         protected virtual void FitToContents()         {             Size size = this.GetPreferredSize(new Size(this.Width, 0));             this.Height = size.Height;         }         protected override void OnCreateControl()         {             base.OnCreateControl();             this.AutoSize = false;         }         #endregion         #region Stomp on AutoSize         [DefaultValue(false), Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]         public override bool AutoSize         {             get { return base.AutoSize; }             set { base.AutoSize = value; }         }         #endregion         #region Private methods         private Color fadeColour(int opacity, Color argbColour)         {             return Color.FromArgb(opacity, argbColour);         }         #endregion     } }

No comments: