在项目初期中使用Label来呈现不可编辑的信息,但是最近用户提出希望可以通过鼠标来选择这些文本并进行复制。本来以为Label可以有个什么属性来支持,其实Label控件将Text属性的值在Paint时画了出来,也就是说在界面上其实是以图形的方式呈现的。在网上搜索了些资料,解决方案就是使用TextBox来代替Label把ReadOnly设置成true,把BorderStyle设置成None。这个方案可行,但是还有一个问题是Label具有AutoSize功能,这使得控件的宽度会随着内容的宽度自动计算。但TextBox并没有这个功能,TextBox将AutoSize属性被隐藏了,TextBox的AutoSize只会根据字号的变化来改变高度,并且默认已经是True。

另外在项目的一些地方还需要TextBox和Label进行互换,如在某种情况下是可编辑的,在某种情况下是不可编辑的。因此决定写个控件来支持一下。

控件的功能:

1. 具有TextBox和Label的功能,并可在TextBox和Label模式切换。

2. 无论在TextBox或Label模式都可以AutoSize。

3. 支持自动切换前景和背景颜色在切换TextBox和Label模式时,TextBox使用默认前景和背景色,Label使用默认背景色,前景色使用GrayText。(默认前景色为WindowText, 默认背景色为Window)

4. 支持自动切换边框样式在切换TextBox和Label模式时,TextBox默认边框为Fixed3D,Label默认边框为None。

5. TextBox在边框样式切换时,由于边框宽度的变化,文字显示的位置会变化,因此看上去会有跳动的感觉。因此在进行边框切换时要通过边框宽度的变化对文字显示的位置进行相对调整以消除跳动的感觉。

开发控件过程总结的几个知识点:

1. EventHandlerList

之前使用事件全部是定义一个委托变量来实现,这次尝试使用EventHandlerList。

为什么使用EventHandlerList,这是由于当事件的数量多时,如果为每一个事件都定义一个委托变量,虽然不为委托变量赋值但是依然会为每个变量开辟一个指针的空间,32位机器为4个字节,64位机器为8个字节。假如一个控件有40个事件,当前打开了有此控件的应用程序3个,每个程序的界面上有10个此控件,那内存的占用量为4 * 40 * 3 * 10 = 4800字节(32位) 9600字节(64位)。换成使用EventHandlerList,仅需要为每个事件声明一个全局的Key,这样内存的占用量为4 * 40 * 3 = 480节字(32位) 960字节(64位)。可见EventHandlerList方式管理的事件的内存占用量随着运行程序数量线性增加,而委托变量的方式会随着控件实例而线性增加,二者不是一个数量级。

2. TextBox Border Width

TextBox控件本身找不到Border的宽度,但在SystemInformation中我们可以找到Border3DSize和BorderSize两个静态属性分别代表两种TextBox BorderStyle的宽度。当BorderSytle为None时宽度为0,即Size.Empty。

但这还并不能解决问题,因为TextBox的边框内部还有1个像素的小边用来区分边框与内部输入框,当TextBox的BorderStyle变成None时,这个内部的小边也一同消失,但是在Fixed3D和FixedFlat两种样式切换时内部的小边却不消失。所以我们还要在Border3DSize和BorderSize的基础上加这1个像素来得出真正的边框宽度,从而再进行偏移量计算。

但这个自动调节文字位置的功能仅限在非自动布局的情况下,如果把TextBox放在FlowLayoutPanel中则失去这个功能,在FlowLayoutPanel中布局是自动完成的。

/// <summary>
    /// The LabelBox control can show in label or textbox mode.
    /// </summary>
    public class LabelBox : TextBox
    {
        #region Variables
        /// <summary>
        /// The textbox inner offset
        /// </summary>
        private const int _TEXTBOX_INNER_OFFSET = 1;
        /// <summary>
        /// The fixed 3d border size
        /// </summary>
        private static readonly Size _FIXED3D_BORDERSIZE = new Size(SystemInformation.Border3DSize.Width + _TEXTBOX_INNER_OFFSET, SystemInformation.Border3DSize.Height + _TEXTBOX_INNER_OFFSET);
        /// <summary>
        /// The fixed single border size
        /// </summary>
        private static readonly Size _FIXSINGLEB_ORDERSIZE = new Size(SystemInformation.BorderSize.Width + _TEXTBOX_INNER_OFFSET, SystemInformation.BorderSize.Height + _TEXTBOX_INNER_OFFSET);
        /// <summary>
        /// The LabelModeChanged event key
        /// </summary>
        private static readonly object _EVENT_LABELMODECHANGED = new object();
        /// <summary>
        /// Whether in label mode or not.
        /// </summary>
        private bool _labelMode = false;
        /// <summary>
        /// Whether in the middle of auto sizing or not.
        /// </summary>
        private bool _autoSizing = false;
        /// <summary>
        /// Whether in the middle of changing LabelMode
        /// </summary>
        private bool _labelModeChanging = false;
        #region Variables for label mode
        /// <summary>
        /// The border style in label mode
        /// </summary>
        private BorderStyle _borderStyleInLabelMode = BorderStyle.None;
        /// <summary>
        /// The fore color in label mode
        /// </summary>
        private Color _foreColorInLabelMode = SystemColors.GrayText;
        /// <summary>
        /// The back color in label mode
        /// </summary>
        private Color _backColorInLabelMode = Color.Empty;
        /// <summary>
        /// The read only in label mode, it's a const to true.
        /// </summary>
        private const bool _readOnlyInLabelMode = true;
        /// <summary>
        /// The width in label mode
        /// </summary>
        private int _widthInLabelMode = 100;
        /// <summary>
        /// The autosize in label mode
        /// </summary>
        private bool _autoSizeInLabelMode = true;
        /// <summary>
        /// The location in label mode
        /// </summary>
        private Point _locationInLabelMode = new Point();
        #endregion
        #region Variable for textbox mode
        /// <summary>
        /// The border style in textbox mode
        /// </summary>
        private BorderStyle _borderStyleInTextBoxMode = BorderStyle.Fixed3D;
        /// <summary>
        /// The fore color in textbox mode
        /// </summary>
        private Color _foreColorInTextBoxMode = TextBox.DefaultForeColor;
        /// <summary>
        /// The back color in text box mode
        /// </summary>
        private Color _backColorInTextBoxMode = Color.Empty;
        /// <summary>
        /// The readonly in textbox mode
        /// </summary>
        private bool _readOnlyInTextBoxMode = false;
        /// <summary>
        /// The width in textbox mode
        /// </summary>
        private int _widthInTextBoxMode = 100;
        /// <summary>
        /// The autosize in textbox mode
        /// </summary>
        private bool _autoSizeInTextBoxMode = false;
        /// <summary>
        /// The location in textbox mode
        /// </summary>
        private Point _locationInTextBoxMdoe = new Point();
        #endregion
        #endregion
        #region Event
        /// <summary>
        /// Event raised when the value of the LabelMode property is changed on control.
        /// </summary>
        [Category("LabelBox"), Description("Event raised when the value of the LabelMode property is changed on control.")]
        public event EventHandler LabelModeChanged
        {
            add
            {
                base.Events.AddHandler(_EVENT_LABELMODECHANGED, value);
            }
            remove
            {
                base.Events.RemoveHandler(_EVENT_LABELMODECHANGED, value);
            }
        }
        #endregion
        #region Properties
        /// <summary>
        /// Please don't use this property, use BackColorInLabelMode or BackColorInTextBoxMode instead.
        /// </summary>
        [Browsable(false), Description("Please don't use this property, use BackColorInLabelMode or BackColorInTextBoxMode instead.")]
        public override Color BackColor
        {
            get
            {
                return base.BackColor;
            }
            set
            {
                base.BackColor = value;
            }
        }
        /// <summary>
        /// Please don't use this property, use ForeColorInLabelMode or ForeColorInTextBoxMode instead.
        /// </summary>
        [Browsable(false), Description("Please don't use this property, use BackColorInLabelMode or BackColorInTextBoxMode instead.")]
        public override System.Drawing.Color ForeColor
        {
            get
            {
                return base.ForeColor;
            }
            set
            {
                base.ForeColor = value;
            }
        }
        /// <summary>
        /// Gets or sets whether in label mode
        /// </summary>
        [DefaultValue(false)]
        [Category("LabelBox"), Description("If true the control will be shown in label mode or else be shown in text box mode.")]
        public bool LabelMode
        {
            get
            {
                return this._labelMode;
            }
            set
            {
                try
                {
                    this._labelModeChanging = true;
                    bool inLabelMode = this._labelMode;
                    this._labelMode = value;
                    if (value != inLabelMode) // if changing LabelMode from false to true, TextBox -> Label
                    {
                        this.OnLabelModeChanged(EventArgs.Empty);
                    }
                }
                catch (Exception e)
                {
                    throw e;
                }
                finally
                {
                    this._labelModeChanging = false;
                }
            }
        }
        /// <summary>
        /// Gets whether auto size width or not
        /// </summary>
        private bool AutoSizeWidth
        {
            get
            {
                return ((this.LabelMode && this.AutoSizeInLabelMode) || (!this.LabelMode && this.AutoSizeInTextBoxMode)) && !this.Multiline;
            }
        }
        #region Properties for label mode
        /// <summary>
        /// Gets or sets the border style in label mode.
        /// </summary>
        [Category("LabelBox"), Description("The border style in label mode"), DefaultValue(BorderStyle.None)]
        public BorderStyle BorderStyleInLabelMode
        {
            get
            {
                return this._borderStyleInLabelMode;
            }
            set
            {
                this._borderStyleInLabelMode = value;
                // Update control's BorderStyle together if in label mode
                if (this.LabelMode)
                {
                    this.BorderStyle = value;
                }
            }
        }
        /// <summary>
        /// Gets or sets the fore color in label mode.
        /// </summary>
        [Category("LabelBox"), Description("The fore color in label mode")]
        public Color ForeColorInLabelMode
        {
            get
            {
                return this._foreColorInLabelMode;
            }
            set
            {
                this._foreColorInLabelMode = value;
                // Update control's ForeColor together if in label mode
                if (this.LabelMode)
                {
                    this.ForeColor = value;
                }
            }
        }
        /// <summary>
        /// Gets or sets the back color in label mode.
        /// </summary>
        [Category("LabelBox"), Description("The back color in label mode")]
        public Color BackColorInLabelMode
        {
            get
            {
                return this._backColorInLabelMode;
            }
            set
            {
                this._backColorInLabelMode = value;
                // Update control's BackColor together if in label mode
                if (this.LabelMode)
                {
                    this.BackColor = value;
                }
            }
        }
        /// <summary>
        /// Gets or sets the width on label mode.
        /// </summary>
        [Category("LabelBox"), Description("The width in label mode"), DefaultValue(100)]
        public int WidthInLabelMode
        {
            get
            {
                return this._widthInLabelMode;
            }
            set
            {
                this._widthInLabelMode = value;
                // Update control's Width together if in label mode
                if (this.LabelMode)
                {
                    if (this.AutoSizeWidth)
                    {
                        this.DoAutoSizeWidth();
                    }
                    else
                    {
                        this.Width = value;
                    }
                }
            }
        }
        /// <summary>
        /// Gets or sets the autosize in label mode.
        /// </summary>
        [Category("LabelBox"), Description("If true the control will be auto sized in label mode."), DefaultValue(true)]
        public bool AutoSizeInLabelMode
        {
            get
            {
                return this._autoSizeInLabelMode;
            }
            set
            {
                this._autoSizeInLabelMode = value;
                // Compute width if allow autosize or else use WidthInLabelMode.
                if (this.LabelMode)
                {
                    if (this.AutoSizeWidth)
                    {
                        this.DoAutoSizeWidth();
                    }
                    else
                    {
                        this.Width = this.WidthInLabelMode;
                    }
                }
            }
        }
        #endregion
        #region Properties for textbox mode
        /// <summary>
        /// Gets or sets the border style in textbox mode.
        /// </summary>
        [Category("LabelBox"), Description("The border style in textbox mode"), DefaultValue(BorderStyle.Fixed3D)]
        public BorderStyle BorderStyleInTextBoxMode
        {
            get
            {
                return this._borderStyleInTextBoxMode;
            }
            set
            {
                this._borderStyleInTextBoxMode = value;
                // Update control's BorderStyle together if in textbox mode
                if (!this.LabelMode)
                {
                    this.BorderStyle = value;
                }
            }
        }
        /// <summary>
        /// Gets or sets the fore color in textbox mode.
        /// </summary>
        [Category("LabelBox"), Description("The fore color in textbox mode")]
        public Color ForeColorInTextBoxMode
        {
            get
            {
                return this._foreColorInTextBoxMode;
            }
            set
            {
                this._foreColorInTextBoxMode = value;
                // Update control's ForeColor together if in textbox mode
                if (!this.LabelMode)
                {
                    this.ForeColor = value;
                }
            }
        }
        /// <summary>
        /// Gets or sets the back color in textbox mode
        /// </summary>
        [Category("LabelBox"), Description("The back color in textbox mode")]
        public Color BackColorInTextBoxMode
        {
            get
            {
                return this._backColorInTextBoxMode;
            }
            set
            {
                this._backColorInTextBoxMode = value;
                // Update control's BackColor together if in textbox mode
                if (!this.LabelMode)
                {
                    this.BackColor = value;
                }
            }
        }
        /// <summary>
        /// Gets or sets the width in textbox mode.
        /// </summary>
        [Category("LabelBox"), Description("The width in textbox mode"), DefaultValue(100)]
        public int WidthInTextBoxMode
        {
            get
            {
                return this._widthInTextBoxMode;
            }
            set
            {
                this._widthInTextBoxMode = value;
                // Update control's Width together if in textbox mode
                if (!this.LabelMode)
                {
                    if (this.AutoSizeWidth)
                    {
                        this.DoAutoSizeWidth();
                    }
                    else
                    {
                        this.Width = value;
                    }
                }
            }
        }
        /// <summary>
        /// Gets or sets the autosize in textbox mode.
        /// </summary>
        [Category("LabelBox"), Description("If true the control will be auto sized in textbox mode."), DefaultValue(false)]
        public bool AutoSizeInTextBoxMode
        {
            get
            {
                return this._autoSizeInTextBoxMode;
            }
            set
            {
                this._autoSizeInTextBoxMode = value;
                // Compute width if allow autosize or else use WidthInTextBoxMode.
                if (!this.LabelMode)
                {
                    if (this.AutoSizeWidth)
                    {
                        this.DoAutoSizeWidth();
                    }
                    else
                    {
                        this.Width = this.WidthInTextBoxMode;
                    }
                }
            }
        }
        #endregion
        #endregion
        #region Methods
        /// <summary>
        /// Raises the System.Windows.Forms.TextBoxBase.BorderStyleChanged event.
        /// </summary>
        /// <param name="e">An System.EventArgs that contains the event data</param>
        protected override void OnBorderStyleChanged(EventArgs e)
        {
            // Shouldn't change BorderStyle directly, please change BorderStyleInLabelMode or BorderStyleInTextBoxMode.
            // If BorderStyle changed but BorderStyleInLabelMode or BorderStyleInTextBoxMode not, the changed will be cancelled.
            base.OnBorderStyleChanged(e);
            if (!this._labelModeChanging)
            {
                this.BorderStyle = this.LabelMode ? this.BorderStyleInLabelMode : this.BorderStyleInTextBoxMode;
                this.ComputeLocation();
            }
        }
        /// <summary>
        /// Raises the System.Windows.Forms.Control.ForeColorChanged event.
        /// </summary>
        /// <param name="e">An System.EventArgs that contains the event data</param>
        protected override void OnForeColorChanged(EventArgs e)
        {
            // Shouldn't change ForeColor directly, please change ForeColorInLabelMode or ForeColorInTextBoxMode.
            // If ForeColor changed but ForeColorInLabelMode or ForeColorInTextBoxMode not, the changed will be cancelled.
            base.OnForeColorChanged(e);
            if (!this._labelModeChanging)
            {
                this.ForeColor = this.LabelMode ? this.ForeColorInLabelMode : this.ForeColorInTextBoxMode;
            }
        }
        /// <summary>
        /// Raises the System.Windows.Forms.Control.BackColorChanged event.
        /// </summary>
        /// <param name="e">An System.EventArgs that contains the event data</param>
        protected override void OnBackColorChanged(EventArgs e)
        {
            // Shouldn't change BackColor directly, please change BackColorInLabelMode or BackColorInTextBoxMode.
            // If BackColor changed but BackColorInLabelMode or BackColorInTextBoxMode not, the changed will be cancelled.
            base.OnBackColorChanged(e);
            if (!this._labelModeChanging)
            {
                this.BackColor = this.LabelMode ? this.BackColorInLabelMode : this.BackColorInTextBoxMode;
            }
        }
        /// <summary>
        /// Raises the System.Windows.Forms.TextBoxBase.ReadOnlyChanged event.
        /// </summary>
        /// <param name="e">An System.EventArgs that contains the event data</param>
        protected override void OnReadOnlyChanged(EventArgs e)
        {
            // Don't allow change ReadOnly in label mode
            if (this.LabelMode)
            {
                this.ReadOnly = _readOnlyInLabelMode;
            }
            else
            {
                this._readOnlyInTextBoxMode = this.ReadOnly;
                base.OnReadOnlyChanged(e);
            }
        }
        /// <summary>
        /// Raises the System.Windows.Forms.Control.SizeChanged event.
        /// </summary>
        /// <param name="e">An System.EventArgs that contains the event data</param>
        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
            if (!this._autoSizing)
            {
                if (this.AutoSizeWidth)
                {
                    this.DoAutoSizeWidth();
                }
                else
                {
                    // for update WidthInLabelMode or WidthInTextBoxMode in Desinger
                    if (this.DesignMode && !this._labelModeChanging)
                    {
                        if (this.LabelMode)
                        {
                            this.WidthInLabelMode = this.Width;
                        }
                        else
                        {
                            this.WidthInTextBoxMode = this.Width;
                        }
                    }
                }
            }
        }
        /// <summary>
        /// Raises the System.Windows.Forms.Control.LocationChanged event.
        /// </summary>
        /// <param name="e">An System.EventArgs that contains the event data</param>
        protected override void OnLocationChanged(EventArgs e)
        {
            base.OnLocationChanged(e);
            if (!this._labelModeChanging)
            {
                this.ComputeLocation();
            }
        }
        /// <summary>
        /// Raises the System.Windows.Forms.TextBoxBase.TextChanged event.
        /// </summary>
        /// <param name="e">An System.EventArgs that contains the event data</param>
        protected override void OnTextChanged(EventArgs e)
        {
            base.OnTextChanged(e);
            if (!this._labelModeChanging)
            {
                if (this.AutoSizeWidth)
                {
                    this.DoAutoSizeWidth();
                }
            }
        }
        /// <summary>
        /// Raises the TOG.Prepress.ControlLibrary.ExtendedWindowsControls.LabelBox.LabelModeChanged event.
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnLabelModeChanged(EventArgs e)
        {
            if (this.LabelMode) // if changing LabelMode from false to true, TextBox -> Label
            {
                this.BorderStyle = this.BorderStyleInLabelMode;
                this.ForeColor = this.ForeColorInLabelMode;
                this.BackColor = this.BackColorInLabelMode;
                this.ReadOnly = _readOnlyInLabelMode;
                this.Location = this._locationInLabelMode;
                if (this.AutoSizeWidth)
                {
                    this.DoAutoSizeWidth();
                }
                else
                {
                    this.Width = this.WidthInLabelMode;
                }
            }
            else // if chaning LabelMode from true to false, Label -> TextBox
            {
                this.BorderStyle = this.BorderStyleInTextBoxMode;
                this.ForeColor = this.ForeColorInTextBoxMode;
                this.BackColor = this.BackColorInTextBoxMode;
                this.ReadOnly = this._readOnlyInTextBoxMode;
                this.Location = this._locationInTextBoxMdoe;
                if (this.AutoSizeWidth)
                {
                    this.DoAutoSizeWidth();
                }
                else
                {
                    this.Width = this.WidthInTextBoxMode;
                }
            }
            EventHandler handler = this.Events[_EVENT_LABELMODECHANGED] as EventHandler;
            if (handler != null)
            {
                handler(this, e);
            }
        }
        /// <summary>
        /// Auto size the control width
        /// </summary>
        private void DoAutoSizeWidth()
        {
            this._autoSizing = true;
            int borderWidth = this.GetTextBoxBorderSize(this.BorderStyle).Width * 2;
            Graphics g = this.CreateGraphics();
            Size s = TextRenderer.MeasureText(g, this.Text, this.Font);
            this.Width = (int)s.Width + borderWidth;
            // If textbox mode the WidthInTextBoxMode is the minimum width
            if (!this.LabelMode)
            {
                this.Width = Math.Max(this.Width, this.WidthInTextBoxMode);
            }
            this._autoSizing = false;
        }
        /// <summary>
        /// Get border size by border style
        /// </summary>
        /// <param name="borderStyle">The border style</param>
        /// <returns>The border size</returns>
        private Size GetTextBoxBorderSize(BorderStyle borderStyle)
        {
            switch (borderStyle)
            {
                case System.Windows.Forms.BorderStyle.Fixed3D:
                    return _FIXED3D_BORDERSIZE;
                case System.Windows.Forms.BorderStyle.FixedSingle:
                    return _FIXSINGLEB_ORDERSIZE;
                case System.Windows.Forms.BorderStyle.None:
                default:
                    return Size.Empty;
            }
        }
        /// <summary>
        /// Compute the location
        /// </summary>
        private void ComputeLocation()
        {
            Size textBoxBorderSize = this.GetTextBoxBorderSize(this.BorderStyleInTextBoxMode);
            Size labelBorderSize = this.GetTextBoxBorderSize(this.BorderStyleInLabelMode);
            if (this.LabelMode)
            {
                this._locationInLabelMode.X = this.Location.X;
                this._locationInLabelMode.Y = this.Location.Y;
                int offsetWidth = labelBorderSize.Width - textBoxBorderSize.Width;
                int offsetHeight = labelBorderSize.Height - textBoxBorderSize.Height;
                this._locationInTextBoxMdoe.X = this.Location.X + offsetWidth;
                this._locationInTextBoxMdoe.Y = this.Location.Y + offsetHeight;
            }
            else
            {
                this._locationInTextBoxMdoe.X = this.Location.X;
                this._locationInTextBoxMdoe.Y = this.Location.Y;
                int offsetWidth = textBoxBorderSize.Width - labelBorderSize.Width;
                int offsetHeight = textBoxBorderSize.Height - labelBorderSize.Height;
                this._locationInLabelMode.X = this.Location.X + offsetWidth;
                this._locationInLabelMode.Y = this.Location.Y + offsetHeight;
            }
        }
        #endregion
    }

这个解决方案希望能够给大家帮助,如果有更好的解决方案,欢迎交流。