// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Specialized;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms.Layout;
using System.Windows.Forms.Rendering.Animation;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;
namespace System.Windows.Forms;
/// <summary>
/// Implements the basic functionality required by text
/// controls.
/// </summary>
[DefaultEvent(nameof(TextChanged))]
[DefaultBindingProperty(nameof(Text))]
[Designer($"System.Windows.Forms.Design.TextBoxBaseDesigner, {Assemblies.SystemDesign}")]
public abstract partial class TextBoxBase : Control
{
// The boolean properties for this control are contained in the textBoxFlags bit
// vector. We can store up to 32 boolean values in this one vector. Here we
// create the bitmasks for each bit in the vector.
private static readonly int s_autoSize = BitVector32.CreateMask();
private static readonly int s_hideSelection = BitVector32.CreateMask(s_autoSize);
private static readonly int s_multiline = BitVector32.CreateMask(s_hideSelection);
private static readonly int s_modified = BitVector32.CreateMask(s_multiline);
private static readonly int s_readOnly = BitVector32.CreateMask(s_modified);
private static readonly int s_acceptsTab = BitVector32.CreateMask(s_readOnly);
private static readonly int s_wordWrap = BitVector32.CreateMask(s_acceptsTab);
private static readonly int s_creatingHandle = BitVector32.CreateMask(s_wordWrap);
private static readonly int s_codeUpdateText = BitVector32.CreateMask(s_creatingHandle);
private static readonly int s_shortcutsEnabled = BitVector32.CreateMask(s_codeUpdateText);
private static readonly int s_scrollToCaretOnHandleCreated = BitVector32.CreateMask(s_shortcutsEnabled);
private static readonly int s_setSelectionOnHandleCreated = BitVector32.CreateMask(s_scrollToCaretOnHandleCreated);
private static readonly object s_acceptsTabChangedEvent = new();
private static readonly object s_borderStyleChangedEvent = new();
private static readonly object s_hideSelectionChangedEvent = new();
private static readonly object s_modifiedChangedEvent = new();
private static readonly object s_multilineChangedEvent = new();
private static readonly object s_readOnlyChangedEvent = new();
/// <summary>
/// The current border for this edit control.
/// </summary>
private BorderStyle _borderStyle = BorderStyle.Fixed3D;
private AnimatedFocusIndicatorRenderer? _focusIndicatorRenderer;
private const OBJECT_IDENTIFIER HorizontalScrollBarObjectId = (OBJECT_IDENTIFIER)(-6);
private const OBJECT_IDENTIFIER VerticalScrollBarObjectId = (OBJECT_IDENTIFIER)(-5);
private const uint StateSystemInvisible = 0x00008000;
/// <summary>
/// One-shot latch that gates the single NC-calc round trip used to carve the modern Visual
/// Styles padding band. Set by <see cref="InitializeClientArea"/> and reset on handle recreation.
/// </summary>
private bool _triggerNewClientSizeRequest;
/// <summary>
/// Controls the maximum length of text in the edit control.
/// Matches the Windows limit.
/// </summary>
private int _maxLength = 32767;
/// <summary>
/// Used by the autoSizing code to help figure out the desired height of
/// the edit box.
/// </summary>
private int _requestedHeight;
private bool _integralHeightAdjust;
// these indices are used to cache the values of the selection, by doing this
// if the handle isn't created yet, we don't force a creation.
private int _selectionStart;
private int _selectionLength;
/// <summary>
/// Controls firing of click event (Left click).
/// This is used by TextBox, RichTextBox and MaskedTextBox, code was moved down from TextBox/RichTextBox
/// but cannot make it as default behavior to avoid introducing breaking changes.
/// </summary>
private bool _doubleClickFired;
private static int[]? s_shortcutsToDisable;
// We store all boolean properties in here.
private BitVector32 _textBoxFlags;
/// <summary>
/// Creates a new TextBox control. Uses the parent's current font and color set.
/// </summary>
internal TextBoxBase() : base()
{
// this class overrides GetPreferredSizeCore, let Control automatically cache the result
SetExtendedState(ExtendedStates.UserPreferredSizeCache, true);
_textBoxFlags[s_autoSize | s_hideSelection | s_wordWrap | s_shortcutsEnabled] = true;
SetStyle(ControlStyles.FixedHeight, _textBoxFlags[s_autoSize]);
SetStyle(ControlStyles.StandardClick
| ControlStyles.StandardDoubleClick
| ControlStyles.UseTextForAccessibility
| ControlStyles.UserPaint, false);
// cache requestedHeight. Note: Control calls DefaultSize (overridable) in the constructor
// to set the control's cached height that is returned when calling Height, so we just
// need to get the cached height here.
_requestedHeight = Height;
}
/// <summary>
/// Gets or sets
/// a value indicating whether pressing the TAB key
/// in a multiline text box control types
/// a TAB character in the control instead of moving the focus to the next control
/// in the tab order.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[SRDescription(nameof(SR.TextBoxAcceptsTabDescr))]
public bool AcceptsTab
{
get => _textBoxFlags[s_acceptsTab];
set
{
if (_textBoxFlags[s_acceptsTab] != value)
{
_textBoxFlags[s_acceptsTab] = value;
OnAcceptsTabChanged(EventArgs.Empty);
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnAcceptsTabChangedDescr))]
public event EventHandler? AcceptsTabChanged
{
add => Events.AddHandler(s_acceptsTabChangedEvent, value);
remove => Events.RemoveHandler(s_acceptsTabChangedEvent, value);
}
/// <summary>
/// Gets or sets a value indicating whether the following shortcuts should be enabled or not:
/// Ctrl-Z, Ctrl-C, Ctrl-X, Ctrl-V, Ctrl-A, Ctrl-L, Ctrl-R, Ctrl-E, Ctrl-I, Ctrl-Y,
/// Ctrl-BackSpace, Ctrl-Del, Shift-Del, Shift-Ins.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.TextBoxShortcutsEnabledDescr))]
public virtual bool ShortcutsEnabled
{
get => _textBoxFlags[s_shortcutsEnabled];
set
{
s_shortcutsToDisable ??=
[
(int)Shortcut.CtrlZ, (int)Shortcut.CtrlC, (int)Shortcut.CtrlX,
(int)Shortcut.CtrlV, (int)Shortcut.CtrlA, (int)Shortcut.CtrlL, (int)Shortcut.CtrlR,
(int)Shortcut.CtrlE, (int)Shortcut.CtrlY, (int)Keys.Control + (int)Keys.Back,
(int)Shortcut.CtrlDel, (int)Shortcut.ShiftDel, (int)Shortcut.ShiftIns, (int)Shortcut.CtrlJ
];
_textBoxFlags[s_shortcutsEnabled] = value;
}
}
/// <summary>
/// Implements the <see cref="ShortcutsEnabled"/> property.
/// </summary>
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
// First call parent's ProcessCmdKey, since we don't to eat up
// the shortcut key we are not supported in TextBox.
bool returnedValue = base.ProcessCmdKey(ref msg, keyData);
if (!ShortcutsEnabled && s_shortcutsToDisable is not null)
{
foreach (int shortcutValue in s_shortcutsToDisable)
{
if ((int)keyData == shortcutValue ||
(int)keyData == (shortcutValue | (int)Keys.Shift))
{
return true;
}
}
}
// There are a few keys that change the alignment of the text, but that
// are not ignored by the native control when the ReadOnly property is set.
// We need to workaround that.
if (_textBoxFlags[s_readOnly])
{
int k = (int)keyData;
if (k is ((int)Shortcut.CtrlL) // align left
or ((int)Shortcut.CtrlR) // align right
or ((int)Shortcut.CtrlE) // align center
or ((int)Shortcut.CtrlJ)) // align justified
{
return true;
}
}
if (ReadOnly
|| (keyData != (Keys.Control | Keys.Back)
&& keyData != (Keys.Control | Keys.Shift | Keys.Back)))
{
return returnedValue;
}
if (SelectionLength != 0)
{
SetSelectedTextInternal(string.Empty, clearUndo: false);
}
else if (SelectionStart != 0)
{
int boundaryStart = ClientUtils.GetWordBoundaryStart(Text, SelectionStart);
int length = SelectionStart - boundaryStart;
BeginUpdateInternal();
SelectionStart = boundaryStart;
SelectionLength = length;
EndUpdateInternal();
SetSelectedTextInternal(string.Empty, clearUndo: false);
}
return true;
}
/// <summary>
/// Gets or sets a value indicating whether the size
/// of the control automatically adjusts when the font assigned to the control
/// is changed.
///
/// Note: this works differently than other Controls' AutoSize, so we're hiding
/// it to avoid confusion.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[Localizable(true)]
[SRDescription(nameof(SR.TextBoxAutoSizeDescr))]
[RefreshProperties(RefreshProperties.Repaint)]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool AutoSize
{
get => _textBoxFlags[s_autoSize];
set
{
// Note that we intentionally do not call base. TextBoxes size themselves by
// overriding SetBoundsCore (old RTM code). We let CommonProperties.GetAutoSize
// continue to return false to keep our LayoutEngines from messing with TextBoxes.
// This is done for backwards compatibility since the new AutoSize behavior differs.
if (_textBoxFlags[s_autoSize] != value)
{
_textBoxFlags[s_autoSize] = value;
// AutoSize's effects are ignored for a multi-line textbox
//
if (!Multiline)
{
SetStyle(ControlStyles.FixedHeight, value);
AdjustHeight(false);
}
OnAutoSizeChanged(EventArgs.Empty);
}
}
}
/// <summary>
/// Gets or sets the background color of the control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DispId(PInvokeCore.DISPID_BACKCOLOR)]
[SRDescription(nameof(SR.ControlBackColorDescr))]
public override Color BackColor
{
get
{
if (ShouldSerializeBackColor())
{
return base.BackColor;
}
else
{
return ReadOnly
// If we're ReadOnly and in DarkMode, we are using a different background color.
? Application.IsDarkModeEnabled
&& DarkModeRequestState is true
? SystemColors.ControlDarkDark
: SystemColors.Control
: SystemColors.Window;
}
}
set => base.BackColor = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override Image? BackgroundImage
{
get => base.BackgroundImage;
set => base.BackgroundImage = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? AutoSizeChanged
{
add => base.AutoSizeChanged += value;
remove => base.AutoSizeChanged -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? BackgroundImageChanged
{
add => base.BackgroundImageChanged += value;
remove => base.BackgroundImageChanged -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override ImageLayout BackgroundImageLayout
{
get => base.BackgroundImageLayout;
set => base.BackgroundImageLayout = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? BackgroundImageLayoutChanged
{
add => base.BackgroundImageLayoutChanged += value;
remove => base.BackgroundImageLayoutChanged -= value;
}
/// <summary>
/// Gets or sets the border type
/// of the text box control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DefaultValue(BorderStyle.Fixed3D)]
[DispId(PInvokeCore.DISPID_BORDERSTYLE)]
[SRDescription(nameof(SR.TextBoxBorderDescr))]
public BorderStyle BorderStyle
{
get => _borderStyle;
set
{
if (_borderStyle != value)
{
SourceGenerated.EnumValidator.Validate(value);
_borderStyle = value;
_focusIndicatorRenderer?.Synchronize(Focused, invalidate: false);
CommonProperties.xClearPreferredSizeCache(this);
if (EffectiveVisualStylesMode >= VisualStylesMode.Net11)
{
AdjustHeight(false);
}
UpdateStyles();
if (EffectiveVisualStylesMode >= VisualStylesMode.Net11 && IsHandleCreated)
{
RecalculateVisualStylesClientArea();
}
else
{
RecreateHandle();
}
// PreferredSize depends on BorderStyle : thru CreateParams.ExStyle in User32!AdjustRectEx.
// So when the BorderStyle changes let the parent of this control know about it.
using (LayoutTransaction.CreateTransactionIf(AutoSize, ParentInternal, this, PropertyNames.BorderStyle))
{
OnBorderStyleChanged(EventArgs.Empty);
}
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnBorderStyleChangedDescr))]
public event EventHandler? BorderStyleChanged
{
add => Events.AddHandler(s_borderStyleChangedEvent, value);
remove => Events.RemoveHandler(s_borderStyleChangedEvent, value);
}
internal virtual bool CanRaiseTextChangedEvent
=> true;
protected override bool CanEnableIme
=> !(ReadOnly || PasswordProtect) && base.CanEnableIme;
/// <summary>
/// Gets a value indicating whether the user can undo the previous operation in a text box control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxCanUndoDescr))]
public bool CanUndo
=> IsHandleCreated
&& (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_CANUNDO) != 0;
/// <summary>
/// Returns the parameters needed to create the handle. Inheriting classes
/// can override this to provide extra functionality. They should not,
/// however, forget to call base.getCreateParams() first to get the struct
/// filled up with the basic info.
/// </summary>
protected override CreateParams CreateParams
{
get
{
SetStyle(ControlStyles.ApplyThemingImplicitly, true);
CreateParams cp = base.CreateParams;
cp.ClassName = PInvoke.WC_EDIT;
cp.Style |= PInvoke.ES_AUTOHSCROLL | PInvoke.ES_AUTOVSCROLL;
if (!_textBoxFlags[s_hideSelection])
{
cp.Style |= PInvoke.ES_NOHIDESEL;
}
if (_textBoxFlags[s_readOnly])
{
cp.Style |= PInvoke.ES_READONLY;
}
cp.Style &= ~(int)WINDOW_STYLE.WS_BORDER;
cp.ExStyle &= ~(int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE;
switch (_borderStyle)
{
case BorderStyle.Fixed3D:
cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE;
break;
case BorderStyle.FixedSingle:
cp.Style |= (int)WINDOW_STYLE.WS_BORDER;
break;
}
if (_textBoxFlags[s_multiline])
{
cp.Style |= PInvoke.ES_MULTILINE;
if (_textBoxFlags[s_wordWrap])
{
cp.Style &= ~PInvoke.ES_AUTOHSCROLL;
}
}
if (EffectiveVisualStylesMode >= VisualStylesMode.Net11)
{
// Under a modern VisualStylesMode we custom-draw the frame in the non-client area (see OnNcPaint),
// so the natively drawn border must be suppressed to avoid a double frame. Only the effective
// Win32 styles are stripped here; the public BorderStyle property remains authoritative for what
// the custom non-client paint draws.
cp.Style &= ~(int)WINDOW_STYLE.WS_BORDER;
cp.ExStyle &= ~(int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE;
}
return cp;
}
}
/// <summary>
/// This property is overridden and hidden from statement completion
/// on controls that are based on Win32 Native Controls.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
protected override bool DoubleBuffered
{
get => base.DoubleBuffered;
set => base.DoubleBuffered = value;
}
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
public new event EventHandler? Click
{
add => base.Click += value;
remove => base.Click -= value;
}
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
public new event MouseEventHandler? MouseClick
{
add => base.MouseClick += value;
remove => base.MouseClick -= value;
}
protected override Cursor DefaultCursor
{
get
{
return Cursors.IBeam;
}
}
/// <summary>
/// Deriving classes can override this to configure a default size for their control.
/// This is more efficient than setting the size in the control's constructor.
/// </summary>
protected override Size DefaultSize
=> new Size(100, PreferredHeight);
/// <summary>
/// Gets or sets the foreground color of the control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DispId(PInvokeCore.DISPID_FORECOLOR)]
[SRDescription(nameof(SR.ControlForeColorDescr))]
public override Color ForeColor
{
get => ShouldSerializeForeColor()
? base.ForeColor
: SystemColors.WindowText;
set => base.ForeColor = value;
}
/// <summary>
/// Gets or sets a value indicating whether the selected
/// text in the text box control remains highlighted when the control loses focus.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.TextBoxHideSelectionDescr))]
public bool HideSelection
{
get => _textBoxFlags[s_hideSelection];
set
{
if (_textBoxFlags[s_hideSelection] != value)
{
_textBoxFlags[s_hideSelection] = value;
RecreateHandle();
OnHideSelectionChanged(EventArgs.Empty);
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnHideSelectionChangedDescr))]
public event EventHandler? HideSelectionChanged
{
add => Events.AddHandler(s_hideSelectionChangedEvent, value);
remove => Events.RemoveHandler(s_hideSelectionChangedEvent, value);
}
/// <summary>
/// Internal version of ImeMode property. The ImeMode of TextBoxBase controls depend on its IME restricted
/// mode which is determined by the CanEnableIme property which checks whether the control is in Password or
/// ReadOnly mode.
/// </summary>
protected override ImeMode ImeModeBase
{
get => (DesignMode || CanEnableIme)
? base.ImeModeBase
: ImeMode.Disable;
set => base.ImeModeBase = value;
}
/// <summary>
/// Gets or
/// sets the lines of text in an text box control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[MergableProperty(false)]
[Localizable(true)]
[AllowNull]
[SRDescription(nameof(SR.TextBoxLinesDescr))]
[Editor($"System.Windows.Forms.Design.StringArrayEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))]
public string[] Lines
{
get
{
string text = Text;
List<string> list = [];
int lineStart = 0;
while (lineStart < text.Length)
{
int lineEnd = lineStart;
for (; lineEnd < text.Length; lineEnd++)
{
char c = text[lineEnd];
if (c is '\r' or '\n')
{
break;
}
}
string line = text[lineStart..lineEnd];
list.Add(line);
// Treat "\r", "\r\n", and "\n" as new lines
if (lineEnd < text.Length && text[lineEnd] == '\r')
{
lineEnd++;
}
if (lineEnd < text.Length && text[lineEnd] == '\n')
{
lineEnd++;
}
lineStart = lineEnd;
}
// Corner case -- last character in Text is a new line; need to add blank line to list
if (text.Length > 0 && (text[^1] == '\r' || text[^1] == '\n'))
{
list.Add(string.Empty);
}
return [.. list];
}
set =>
// unparse this string list...
Text = value is not null && value.Length > 0
? string.Join(Environment.NewLine, value)
: string.Empty;
}
/// <summary>
/// Gets or sets the maximum number of
/// characters the user can type into the text box control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(32767)]
[Localizable(true)]
[SRDescription(nameof(SR.TextBoxMaxLengthDescr))]
public virtual int MaxLength
{
get => _maxLength;
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
if (_maxLength != value)
{
_maxLength = value;
UpdateMaxLength();
}
}
}
/// <summary>
/// Gets or sets a value that indicates that the text box control has been modified by the user since
/// the control was created or its contents were last set.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxModifiedDescr))]
public bool Modified
{
get
{
if (!IsHandleCreated)
{
return _textBoxFlags[s_modified];
}
else
{
bool curState = (int)PInvokeCore.SendMessage(
this,
PInvokeCore.EM_GETMODIFY) != 0;
if (_textBoxFlags[s_modified] != curState)
{
// Raise ModifiedChanged event.
// See WmReflectCommand for more info.
_textBoxFlags[s_modified] = curState;
OnModifiedChanged(EventArgs.Empty);
}
return curState;
}
}
set
{
if (Modified != value)
{
if (IsHandleCreated)
{
PInvokeCore.SendMessage(
this,
PInvokeCore.EM_SETMODIFY,
(WPARAM)(BOOL)value);
}
// Must maintain this state always in order for the
// test in the Get method to work properly.
_textBoxFlags[s_modified] = value;
OnModifiedChanged(EventArgs.Empty);
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnModifiedChangedDescr))]
public event EventHandler? ModifiedChanged
{
add => Events.AddHandler(s_modifiedChangedEvent, value);
remove => Events.RemoveHandler(s_modifiedChangedEvent, value);
}
/// <summary>
/// Gets or sets a value indicating whether this
/// is a multiline text box control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[Localizable(true)]
[SRDescription(nameof(SR.TextBoxMultilineDescr))]
[RefreshProperties(RefreshProperties.All)]
public virtual bool Multiline
{
get => _textBoxFlags[s_multiline];
set
{
if (_textBoxFlags[s_multiline] == value)
{
return;
}
using (LayoutTransaction.CreateTransactionIf(
condition: AutoSize,
controlToLayout: ParentInternal,
elementCausingLayout: this,
property: PropertyNames.Multiline))
{
_textBoxFlags[s_multiline] = value;
if (value)
{
// Multi-line textboxes do not have fixed height
SetStyle(ControlStyles.FixedHeight, false);
}
else
{
// Single-line textboxes may have fixed height, depending on AutoSize
SetStyle(ControlStyles.FixedHeight, AutoSize);
}
RecreateHandle();
AdjustHeight(false);
OnMultilineChanged(EventArgs.Empty);
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnMultilineChangedDescr))]
public event EventHandler? MultilineChanged
{
add => Events.AddHandler(s_multilineChangedEvent, value);
remove => Events.RemoveHandler(s_multilineChangedEvent, value);
}
/// <summary>
/// Gets or sets the distance, in pixels, between the text displayed in a text box control and the top, bottom and side edges of the control.
/// </summary>
/// <remarks>
/// <para>
/// Note: While shadowing this property may seem redundant at first glance, it is necessary for binary compatibility.
/// Prior to .NET 11, this property was marked with <see cref="BrowsableAttribute">Browsable(false)</see> and
/// <see cref="EditorBrowsableAttribute">EditorBrowsable(EditorBrowsableState.Never)</see>,
/// and was never serialized. Effectively, <see cref="Control.Padding"/> did not work before .NET 11. While this changed in .NET 11,
/// removing the property shadowing would constitute a binary breaking change. Therefore, the shadowing is retained
/// with updated attributes to ensure <c>Padding</c> now functions as expected.
/// </para>
/// <para>
/// As long as the default value for the <c>Padding</c> property is not modified, <see cref="TextBoxBase"/>-derived
/// controls (<see cref="TextBox"/>, <see cref="RichTextBox"/>) will behave as they always have. However, if you
/// opt into modern styling by calling <see cref="Application.EnableVisualStyles"/> and set
/// <see cref="VisualStylesMode"/> to a value other than <see cref="VisualStylesMode.Classic"/> or
/// <see cref="VisualStylesMode.Disabled"/>, the control will automatically apply styling that conforms to the
/// Windows 10+ Fluent Design Language. This provides both stylistic improvements (such as Dark Mode support) and
/// functional enhancements (including increased touch targets for easier mouse and touch interaction), thereby
/// meeting the accessibility requirements of modern high-resolution displays.
/// </para>
/// </remarks>
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public new Padding Padding
{
get => base.Padding;
set => base.Padding = value;
}
private new bool ShouldSerializePadding()
=> Padding != DefaultPadding;
private void ResetPadding()
=> Padding = DefaultPadding;
// Not new API and not a breaking change: TextBoxBase.PaddingChanged is already in
// PublicAPI.Shipped.txt. It is re-declared here with 'new' purely to apply the designer-hiding
// attributes below (Browsable(false) / EditorBrowsable(Never)) on top of Control.PaddingChanged.
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRCategory(nameof(SR.CatLayout))]
[SRDescription(nameof(SR.ControlOnPaddingChangedDescr))]
public new event EventHandler? PaddingChanged
{
add => base.PaddingChanged += value;
remove => base.PaddingChanged -= value;
}
/// <summary>
/// Determines if the control is in password protect mode. This is overridden in TextBox and
/// MaskedTextBox and is false by default so RichTextBox that doesn't support Password doesn't
/// have to care about this.
/// </summary>
private protected virtual bool PasswordProtect => false;
/// <summary>
/// Returns the preferred
/// height for a single-line text box.
/// </summary>
[SRCategory(nameof(SR.CatLayout))]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Advanced)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxPreferredHeightDescr))]
public int PreferredHeight =>
EffectiveVisualStylesMode switch
{
VisualStylesMode.Disabled => PreferredHeightClassic,
VisualStylesMode.Classic => PreferredHeightClassic,
>= VisualStylesMode.Net11 => PreferredHeightCore,
// We should never be here.
_ => throw new InvalidEnumArgumentException(
argumentName: nameof(VisualStylesMode),
invalidValue: (int)VisualStylesMode,
enumClass: typeof(VisualStylesMode))
};
/// <summary>
/// Returns the preferred height for modern Visual Styles, taking the carved padding band
/// (including the live scrollbar allowance and the user <see cref="Padding"/>) into account.
/// </summary>
private protected virtual int PreferredHeightCore
{
get
{
Padding visualStylesPadding = GetVisualStylesPadding(
includeScrollbars: true);
int preferredHeight = FontHeight + visualStylesPadding.Vertical;
if (AutoSize && !Multiline && BorderStyle == BorderStyle.Fixed3D)
{
preferredHeight = ModernControlVisualStyles.GetPreferredFieldHeight(
FontHeight,
visualStylesPadding,
DeviceDpiInternal);
}
return preferredHeight;
}
}
/// <summary>
/// Returns the classic (Everett-compatible) preferred height for a single-line text box.
/// </summary>
/// <remarks>
/// <para>
/// COMPAT: we must return the same busted height we did in Everett, even if it does not take
/// multiline and word wrap into account. For better accuracy and/or wrapping use
/// <see cref="Control.GetPreferredSize(Size)"/> instead.
/// </para>
/// </remarks>
private int PreferredHeightClassic
{
get
{
int height = FontHeight;
if (_borderStyle != BorderStyle.None)
{
height += SystemInformation.GetBorderSizeForDpi(DeviceDpiInternal).Height
* 4 + 3;
}
return height;
}
}
/// <summary>
/// Defines the visible part of the padding band carved for modern Visual Styles chrome.
/// </summary>
/// <param name="includeScrollbars">
/// <see langword="true"/> to add the live scrollbar allowance (see <see cref="GetScrollBarPadding"/>)
/// on top of the border padding; otherwise <see langword="false"/>.
/// </param>
/// <returns>The visible padding dimensions.</returns>
/// <remarks>
/// <para>
/// The visible part of the padding is the area by which we extend the real-estate of the control
/// with its back color. Border/corner reservation, the DPI-scaled internal chrome inset, the
/// user-provided <see cref="Padding"/>, and scrollbar reservation are each added once.
/// </para>
/// </remarks>
private protected Padding GetVisualStylesPadding(bool includeScrollbars)
{
SystemVisualSettings settings = Application.SystemVisualSettings;
Padding padding = ModernControlVisualStyles.GetFieldPadding(
BorderStyle,
Padding,
settings.FocusBorderMetrics,
settings.TextScaleFactor,
DeviceDpiInternal);
if (includeScrollbars)
{
padding += GetScrollBarPadding();
}
return padding;
}
private int ScaleVisualStylesMetric(int logicalValue)
=> ScaleHelper.ScaleToDpi(logicalValue, DeviceDpiInternal);
private Size GetVisualStylesFocusBorderMetrics()
{
SystemVisualSettings settings = Application.SystemVisualSettings;
return GetVisualStylesFocusBorderMetrics(
settings.FocusBorderMetrics,
settings.TextScaleFactor,
DeviceDpiInternal);
}
private int GetVisualStylesFocusBandHeight()
{
SystemVisualSettings settings = Application.SystemVisualSettings;
return GetVisualStylesFocusBandHeight(
settings.FocusBorderMetrics,
settings.TextScaleFactor,
DeviceDpiInternal);
}
internal static Size GetVisualStylesFocusBorderMetrics(
Size focusBorderMetrics,
float textScaleFactor,
int deviceDpi)
=> ModernControlVisualStyles.GetFocusBorderMetrics(
focusBorderMetrics,
textScaleFactor,
deviceDpi);
internal static int GetVisualStylesFocusBandHeight(
Size focusBorderMetrics,
float textScaleFactor,
int deviceDpi)
=> ModernControlVisualStyles.GetFocusBandHeight(
focusBorderMetrics,
textScaleFactor,
deviceDpi);
internal static bool CanRenderVisualStylesRoundedChrome(
Rectangle bounds,
int cornerSize,
int borderThickness)
{
int minimumDimension = cornerSize + borderThickness;
return bounds.Width >= minimumDimension && bounds.Height >= minimumDimension;
}
internal static Color GetVisualStylesFocusColor(bool highContrast)
=> highContrast
? SystemColors.Highlight
: Application.SystemVisualSettings.AccentColor;
/// <summary>
/// Returns the additional padding required to clear the live scrollbars,
/// if any are currently shown.
/// </summary>
private protected virtual Padding GetScrollBarPadding()
{
Padding padding = Padding.Empty;
// Are the scrollbars visible?
WINDOW_STYLE style = (WINDOW_STYLE)PInvokeCore.GetWindowLong(
this,
WINDOW_LONG_PTR_INDEX.GWL_STYLE);
bool hasHScroll = (style & WINDOW_STYLE.WS_HSCROLL) != 0;
bool hasVScroll = (style & WINDOW_STYLE.WS_VSCROLL) != 0;
if (hasHScroll)
{
padding.Bottom += SystemInformation.GetHorizontalScrollBarHeightForDpi(DeviceDpiInternal);
}
if (hasVScroll)
{
WINDOW_EX_STYLE exStyle = (WINDOW_EX_STYLE)PInvokeCore.GetWindowLong(
this,
WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
if ((exStyle & WINDOW_EX_STYLE.WS_EX_LEFTSCROLLBAR) != 0)
{
padding.Left += SystemInformation.GetVerticalScrollBarWidthForDpi(DeviceDpiInternal);
}
else
{
padding.Right += SystemInformation.GetVerticalScrollBarWidthForDpi(DeviceDpiInternal);
}
}
return padding;
}
/// <summary>
/// Computes the preferred size of the control, honoring border style, multiline, and word wrap.
/// </summary>
/// <remarks>
/// <para>
/// This can return a different value than <see cref="PreferredHeight"/>. <see cref="PreferredHeight"/>
/// is a single-line, border-only height kept for backward compatibility, whereas this method measures
/// the actual text against the available width and additionally accounts for multiline and word wrap,
/// and - for modern Visual Styles - the carved adorner band and the user <see cref="Padding"/>.
/// </para>
/// </remarks>
internal override Size GetPreferredSizeCore(Size proposedConstraints)
{
// 3px vertical space is required between the text and the border to keep the last
// line from being clipped.
// This 3 pixel size was added in Everett, and we do this to maintain compat.
// Old Everett behavior was FontHeight + [SystemInformation.BorderSize.Height * 4 + 3],
// however the [ ] was only added if BorderStyle was not None.
Padding padding = default;
// Fit the text to the remaining space.
// Originally fixed for .NET Framework 4.0
TextFormatFlags format = TextFormatFlags.NoPrefix;
if (!Multiline)
{
format |= TextFormatFlags.SingleLine;
}
else if (WordWrap)
{
format |= TextFormatFlags.WordBreak;
}
if (EffectiveVisualStylesMode >= VisualStylesMode.Net11)
{
// For modern Visual Styles we take the carved adorner band (including scrollbars
// and the user Padding) into account when measuring.
padding = GetVisualStylesPadding(includeScrollbars: true);
proposedConstraints -= padding.Size;
}
else
{
// Keep the pre-.NET 11 layout contract for classic or disabled visual styles, including
// the user Padding contribution that historically participated in preferred-size calculations.
Size bordersAndPadding = SizeFromClientSize(Size.Empty) + Padding.Size;
if (BorderStyle != BorderStyle.None)
{
bordersAndPadding += new Size(0, 3);
}
if (BorderStyle == BorderStyle.FixedSingle)
{
// Bump these by 2px to match BorderStyle.Fixed3D - they'll be omitted from the SizeFromClientSize call.
bordersAndPadding.Width += 2;
bordersAndPadding.Height += 2;
}
padding.Left = 0;
padding.Top = 0;
padding.Right = bordersAndPadding.Width;
padding.Bottom = bordersAndPadding.Height;
// Reduce constraints by the classic border and padding size.
proposedConstraints -= bordersAndPadding;
}
Size textSize = TextRenderer.MeasureText(Text, Font, proposedConstraints, format);
// We use this old computation as a lower bound to ensure backwards compatibility.
textSize.Height = Math.Max(textSize.Height, FontHeight);
Size preferredSize = textSize + new Size(padding.Horizontal, padding.Vertical);
return preferredSize;
}
/// <summary>
/// Get the currently selected text start position and length. Use this method internally
/// to avoid calling SelectionStart + SelectionLength each of which does essentially the
/// same (save one message round trip).
/// </summary>
internal unsafe void GetSelectionStartAndLength(out int start, out int length)
{
int end = 0;
if (!IsHandleCreated)
{
// It is possible that the cached values are no longer valid if the Text has been changed
// while the control does not have a handle. We need to return valid values. We also need
// to keep the old cached values in case the Text is changed again making the cached values
// valid again.
AdjustSelectionStartAndEnd(_selectionStart, _selectionLength, out start, out end, -1);
length = end - start;
}
else
{
start = 0;
int startResult = 0;
PInvokeCore.SendMessage(this, PInvokeCore.EM_GETSEL, (WPARAM)(&startResult), ref end);
start = startResult;
// Here, we return the max of either 0 or the # returned by
// the windows call. This eliminates a problem on nt4 where
// a huge negative # is being returned.
start = Math.Max(0, start);
// ditto for end
end = Math.Max(0, end);
length = end - start;
}
#if DEBUG
{
string t = WindowText;
int len;
end = start + length - 1;
len = t is null
? 0
: t.Length;
Debug.Assert(end <= len,
$"SelectionEnd is outside the set of valid caret positions for the current WindowText (end ={end}, WindowText.Length ={len})");
}
#endif
}
/// <summary>
/// Gets or sets a value indicating whether text in the text box is read-only.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[RefreshProperties(RefreshProperties.Repaint)]
[SRDescription(nameof(SR.TextBoxReadOnlyDescr))]
public bool ReadOnly
{
get => _textBoxFlags[s_readOnly];
set
{
if (_textBoxFlags[s_readOnly] == value)
{
return;
}
_textBoxFlags[s_readOnly] = value;
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETREADONLY, (WPARAM)(BOOL)value);
EnsureReadonlyBackgroundColor(value);
}
OnReadOnlyChanged(EventArgs.Empty);
VerifyImeRestrictedModeChanged();
}
}
private void EnsureReadonlyBackgroundColor(bool value)
{
// If we have no specifically defined back color, we set the back color in case we're in dark mode.
if (Application.IsDarkModeEnabled
&& DarkModeRequestState is true
&& !ShouldSerializeBackColor())
{
base.BackColor = value
? SystemColors.ControlLight
: SystemColors.Window;
Invalidate();
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnReadOnlyChangedDescr))]
public event EventHandler? ReadOnlyChanged
{
add => Events.AddHandler(s_readOnlyChangedEvent, value);
remove => Events.RemoveHandler(s_readOnlyChangedEvent, value);
}
/// <summary>
/// The currently selected text in the control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[AllowNull]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxSelectedTextDescr))]
public virtual string SelectedText
{
get
{
GetSelectionStartAndLength(out int selStart, out int selLength);
return Text.Substring(selStart, selLength);
}
set => SetSelectedTextInternal(value, true);
}
/// <summary>
/// Replaces the selected text with the one passed in.
/// </summary>
internal virtual void SetSelectedTextInternal(string? text, bool clearUndo)
{
if (!IsHandleCreated)
{
CreateHandle();
}
text ??= string.Empty;
// The EM_LIMITTEXT message limits only the text the user can enter. It does not affect any text
// already in the edit control when the message is sent, nor does it affect the length of the text
// copied to the edit control by the WM_SETTEXT message.
PInvokeCore.SendMessage(this, PInvokeCore.EM_LIMITTEXT);
if (clearUndo)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_REPLACESEL, 0, text);
// For consistency with Text, we clear the modified flag
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMODIFY);
ClearUndo();
}
else
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_REPLACESEL, (WPARAM)(-1), text);
}
// Re-enable user input.
PInvokeCore.SendMessage(this, PInvokeCore.EM_LIMITTEXT, (WPARAM)_maxLength);
}
/// <summary>
/// Gets or sets the number of characters selected in the text box.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxSelectionLengthDescr))]
public virtual int SelectionLength
{
get
{
GetSelectionStartAndLength(out int _, out int length);
return length;
}
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
GetSelectionStartAndLength(out int start, out int length);
if (value != length)
{
Select(start, value);
}
}
}
/// <summary>
/// Gets or sets the starting point of text selected in the text box.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxSelectionStartDescr))]
public int SelectionStart
{
get
{
GetSelectionStartAndLength(out int start, out _);
return start;
}
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
Select(value, SelectionLength);
}
}
/// <summary>
/// Gets or sets the current text in the text box.
/// </summary>
[Localizable(true)]
[AllowNull]
[Editor($"System.ComponentModel.Design.MultilineStringEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))]
public override string Text
{
get => base.Text;
set
{
if (value != base.Text)
{
base.Text = value;
if (IsHandleCreated)
{
// clear the modified flag
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMODIFY);
}
}
}
}
[Browsable(false)]
public virtual int TextLength
// Note: Currently WinForms does not fully support surrogates. If
// the text contains surrogate characters this property may return incorrect values.
=> IsHandleCreated ? PInvokeCore.GetWindowTextLength(this) : Text.Length;
/// <summary>
/// Gets or sets the underlying native window text (the edit control's caption). This is overridden
/// so that setting the text from code raises a "code update" flag for the duration of the assignment:
/// updating the text on a created handle produces a <c>WM_COMMAND</c> / <c>EN_CHANGE</c> notification,
/// and the flag lets us swallow it so we do not raise a duplicate <see cref="Control.TextChanged"/> event.
/// </summary>
internal override string WindowText
{
get => base.WindowText;
set
{
value ??= string.Empty;
// Since setting the WindowText while the handle is created generates a WM_COMMAND message, we must trap
// that case and prevent the event from getting fired, or we get double "TextChanged" events.
if (!WindowText.Equals(value))
{
_textBoxFlags[s_codeUpdateText] = true;
try
{
base.WindowText = value;
}
finally
{
_textBoxFlags[s_codeUpdateText] = false;
}
}
}
}
/// <summary>
/// In certain circumstances we might have to force text into the window whether or not the text is the same.
/// Make this a method on <see cref="TextBoxBase"/> rather than <see cref="RichTextBox"/> (which is the only
/// control that needs this at this point), since we need to set <see cref="s_codeUpdateText"/>.
/// </summary>
internal void ForceWindowText(string? value)
{
value ??= string.Empty;
_textBoxFlags[s_codeUpdateText] = true;
try
{
if (IsHandleCreated)
{
PInvoke.SetWindowText(this, value);
}
else
{
Text = value.Length == 0 ? null : value;
}
}
finally
{
_textBoxFlags[s_codeUpdateText] = false;
}
}
/// <summary>
/// Gets or sets a value indicating whether a multiline text box control automatically wraps words to the
/// beginning of the next line when necessary.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Localizable(true)]
[DefaultValue(true)]
[SRDescription(nameof(SR.TextBoxWordWrapDescr))]
public bool WordWrap
{
get => _textBoxFlags[s_wordWrap];
set
{
using (LayoutTransaction.CreateTransactionIf(
condition: AutoSize,
controlToLayout: ParentInternal,
elementCausingLayout: this,
property: PropertyNames.WordWrap))
{
if (_textBoxFlags[s_wordWrap] != value)
{
_textBoxFlags[s_wordWrap] = value;
RecreateHandle();
}
}
}
}
/// <summary>
/// Adjusts the height of a single-line edit control to match the height of
/// the control's font.
/// </summary>
private void AdjustHeight(bool returnIfAnchored)
{
// If we're anchored to two opposite sides of the form, don't adjust the size because
// we'll lose our anchored size by resetting to the requested width.
if (returnIfAnchored
&& (Anchor & (AnchorStyles.Top | AnchorStyles.Bottom))
== (AnchorStyles.Top | AnchorStyles.Bottom))
{
return;
}
int saveHeight = _requestedHeight;
try
{
if (_textBoxFlags[s_autoSize] && !_textBoxFlags[s_multiline])
{
Height = PreferredHeight;
}
else
{
int curHeight = Height;
// Changing the font of a multi-line textbox can sometimes cause a painting problem
// The only workaround I can find is to size the textbox big enough for the font, and
// then restore its correct size.
if (_textBoxFlags[s_multiline])
{
Height = Math.Max(saveHeight, PreferredHeight + 2); // 2 = fudge factor
}
_integralHeightAdjust = true;
try
{
Height = saveHeight;
}
finally
{
_integralHeightAdjust = false;
}
}
}
finally
{
_requestedHeight = saveHeight;
}
}
/// <summary>
/// Append text to the current text of text box.
/// </summary>
public void AppendText(string? text)
{
if (string.IsNullOrEmpty(text))
{
return;
}
GetSelectionStartAndLength(out int selStart, out int selLength);
try
{
// This enables you to use SelectionColor to AppendText in color.
int endOfText = GetEndPosition();
SelectInternal(endOfText, endOfText, endOfText);
SelectedText = text;
}
finally
{
// If AppendText is called when the control is docked and the form is minimized,
// all the text will scroll to the top and the control will look empty when the
// form is restored. We work around this by selecting back whatever was originally
// selected when AppendText was called.
if (Width == 0 || Height == 0)
{
Select(selStart, selLength);
}
}
}
/// <summary>
/// Clears all text from the text box control.
/// </summary>
public void Clear()
{
Text = null;
}
/// <summary>
/// Clears information about the most recent operation
/// from the undo buffer of the text box.
/// </summary>
public void ClearUndo()
{
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_EMPTYUNDOBUFFER);
}
}
protected bool ContainsNavigationKeyCode(Keys keyCode) => keyCode switch
{
Keys.Up or Keys.Down or Keys.PageUp or Keys.PageDown
or Keys.Home or Keys.End or Keys.Left or Keys.Right => true,
_ => false,
};
/// <summary>
/// Copies the current selection in the text box to the Clipboard.
/// </summary>
public void Copy()
=> PInvokeCore.SendMessage(this, PInvokeCore.WM_COPY);
protected override AccessibleObject CreateAccessibilityInstance()
=> new TextBoxBaseAccessibleObject(this);
protected override void CreateHandle()
{
// Should the handle be (re)created at this point, we need to reset the latch so the
// modern Visual Styles client area is re-carved against the new window.
_triggerNewClientSizeRequest = false;
// This "creatingHandle" stuff is to avoid property change events
// when we set the Text property.
_textBoxFlags[s_creatingHandle] = true;
try
{
base.CreateHandle();
// send EM_SETSEL message
SetSelectionOnHandle();
}
finally
{
_textBoxFlags[s_creatingHandle] = false;
}
}
/// <summary>
/// Moves the current selection in the text box to the Clipboard.
/// </summary>
public void Cut()
=> PInvokeCore.SendMessage(this, PInvokeCore.WM_CUT);
/// <summary>
/// Returns the text end position (one past the last input character). This property is virtual to allow MaskedTextBox
/// to set the last input char position as opposed to the last char position which may be a mask character.
/// </summary>
internal virtual int GetEndPosition() =>
// +1 because RichTextBox has this funny EOF pseudo-character after all the text.
IsHandleCreated
? TextLength + 1
: TextLength;
/// <summary>
/// Overridden to handle TAB key.
/// </summary>
protected override bool IsInputKey(Keys keyData)
{
if ((keyData & Keys.Alt) == Keys.Alt)
{
return base.IsInputKey(keyData);
}
switch (keyData & Keys.KeyCode)
{
case Keys.Tab:
// Single-line RichEd's want tab characters (see WM_GETDLGCODE),
// so we don't ask it
return Multiline && _textBoxFlags[s_acceptsTab] && ((keyData & Keys.Control) == 0);
case Keys.Escape:
if (Multiline)
{
return false;
}
break;
case Keys.Back:
if (!ReadOnly)
{
return true;
}
break;
case Keys.PageUp:
case Keys.PageDown:
case Keys.Home:
case Keys.End:
return true;
// else fall through to base
}
return base.IsInputKey(keyData);
}
/// <summary>
/// Overridden to update the newly created handle with the settings of the
/// MaxLength and PasswordChar properties.
/// </summary>
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if (!IsHandleCreated)
{
return;
}
// it's likely here that the create params could have changed
// the border size/etc.
CommonProperties.xClearPreferredSizeCache(this);
AdjustHeight(true);
UpdateMaxLength();
if (_textBoxFlags[s_modified])
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMODIFY, (WPARAM)(BOOL)true);
}
EnsureReadonlyBackgroundColor(true);
if (_textBoxFlags[s_scrollToCaretOnHandleCreated])
{
ScrollToCaret();
_textBoxFlags[s_scrollToCaretOnHandleCreated] = false;
}
RecalculateVisualStylesClientArea();
}
protected override void OnHandleDestroyed(EventArgs e)
{
_focusIndicatorRenderer?.Dispose();
_focusIndicatorRenderer = null;
_textBoxFlags[s_modified] = Modified;
_textBoxFlags[s_setSelectionOnHandleCreated] = true;
// Update text selection cached values to be restored when recreating the handle.
GetSelectionStartAndLength(out _selectionStart, out _selectionLength);
base.OnHandleDestroyed(e);
}
/// <inheritdoc/>
protected override void OnVisualStylesModeChanged(EventArgs e)
{
// UpdateStyles may synchronously provoke WM_NCCALCSIZE, so it must observe a cleared latch.
_triggerNewClientSizeRequest = false;
base.OnVisualStylesModeChanged(e);
AdjustHeight(false);
_focusIndicatorRenderer?.Synchronize(Focused, invalidate: false);
RecalculateVisualStylesClientArea();
}
/// <inheritdoc/>
protected override void OnSystemVisualSettingsChanged(SystemVisualSettingsChangedEventArgs e)
{
base.OnSystemVisualSettingsChanged(e);
if ((e.Changed & (SystemVisualSettingsCategories.TextScale | SystemVisualSettingsCategories.FocusMetrics)) == 0
|| EffectiveVisualStylesMode < VisualStylesMode.Net11)
{
return;
}
CommonProperties.xClearPreferredSizeCache(this);
AdjustHeight(false);
RecalculateVisualStylesClientArea();
if (ParentInternal is { } parent)
{
LayoutTransaction.DoLayout(parent, this, PropertyNames.SystemVisualSettings);
}
}
/// <inheritdoc/>
/// <remarks>
/// <para>
/// <see cref="PreferredHeightCore"/> and the modern non-client padding table are shared by
/// <see cref="VisualStylesMode.Net11"/> and <see cref="VisualStylesMode.Latest"/>. Consequently, switching
/// between those modes only repaints. Crossing the classic or disabled boundary changes both the preferred
/// height selection and the non-client padding model, so it requires a metrics update.
/// </para>
/// </remarks>
protected override VisualStylesModeChangeImpact GetVisualStylesModeChangeImpact(
VisualStylesMode oldMode,
VisualStylesMode newMode)
{
bool oldUsesModernMetrics = oldMode >= VisualStylesMode.Net11;
bool newUsesModernMetrics = newMode >= VisualStylesMode.Net11;
return oldUsesModernMetrics != newUsesModernMetrics
? VisualStylesModeChangeImpact.Metrics
: VisualStylesModeChangeImpact.Repaint;
}
/// <summary>
/// Replaces the current selection in the text box with the contents of the Clipboard.
/// </summary>
public void Paste()
=> PInvokeCore.SendMessage(this, PInvokeCore.WM_PASTE);
protected override bool ProcessDialogKey(Keys keyData)
{
Keys keyCode = keyData & Keys.KeyCode;
if (keyCode == Keys.Tab && AcceptsTab && (keyData & Keys.Control) != 0)
{
// When this control accepts Tabs, Ctrl-Tab is treated exactly like Tab.
keyData &= ~Keys.Control;
}
return base.ProcessDialogKey(keyData);
}
/// <summary>
/// TextBox / RichTextBox Onpaint.
/// </summary>
/// <hideinheritance/>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event PaintEventHandler? Paint
{
add => base.Paint += value;
remove => base.Paint -= value;
}
protected virtual void OnAcceptsTabChanged(EventArgs e)
{
if (Events[s_acceptsTabChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected virtual void OnBorderStyleChanged(EventArgs e)
{
if (Events[s_borderStyleChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected override unsafe void OnGotFocus(EventArgs e)
{
if (EffectiveVisualStylesMode >= VisualStylesMode.Net11)
{
if (BorderStyle == BorderStyle.Fixed3D)
{
FocusIndicatorRenderer.SetFocused(
focused: true,
animate: SystemInformation.UIEffectsEnabled && !SystemInformation.HighContrast);
}
else
{
InvalidateVisualStylesFrame();
}
}
base.OnGotFocus(e);
}
protected override unsafe void OnLostFocus(EventArgs e)
{
if (EffectiveVisualStylesMode >= VisualStylesMode.Net11)
{
if (BorderStyle == BorderStyle.Fixed3D)
{
FocusIndicatorRenderer.SetFocused(
focused: false,
animate: SystemInformation.UIEffectsEnabled && !SystemInformation.HighContrast);
}
else
{
InvalidateVisualStylesFrame();
}
}
base.OnLostFocus(e);
}
protected override unsafe void OnSizeChanged(EventArgs e)
{
if (EffectiveVisualStylesMode >= VisualStylesMode.Net11)
{
// Invalidate the non-client area to ensure the border chrome is drawn correctly after a resize.
PInvoke.RedrawWindow(
hWnd: this,
lprcUpdate: null,
hrgnUpdate: HRGN.Null,
flags: REDRAW_WINDOW_FLAGS.RDW_FRAME | REDRAW_WINDOW_FLAGS.RDW_INVALIDATE);
}
base.OnSizeChanged(e);
}
protected override void OnFontChanged(EventArgs e)
{
base.OnFontChanged(e);
CommonProperties.xClearPreferredSizeCache(this);
LayoutTransaction.DoLayoutIf(AutoSize, ParentInternal, this, PropertyNames.Font);
AdjustHeight(false);
}
protected override void OnDpiChangedAfterParent(EventArgs e)
{
base.OnDpiChangedAfterParent(e);
CommonProperties.xClearPreferredSizeCache(this);
LayoutTransaction.DoLayoutIf(AutoSize, ParentInternal, this, PropertyNames.Bounds);
AdjustHeight(false);
if (EffectiveVisualStylesMode >= VisualStylesMode.Net11)
{
RecalculateVisualStylesClientArea();
}
}
protected virtual void OnHideSelectionChanged(EventArgs e)
{
if (Events[s_hideSelectionChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected virtual void OnModifiedChanged(EventArgs e)
{
if (Events[s_modifiedChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
/// <summary>
/// Raises the MouseUp event.
/// </summary>
protected override void OnMouseUp(MouseEventArgs mevent)
{
if (mevent is not null && mevent.Button == MouseButtons.Left)
{
if (!ValidationCancelled && PInvoke.WindowFromPoint(PointToScreen(mevent.Location)) == HWND)
{
if (!_doubleClickFired)
{
OnClick(mevent);
OnMouseClick(mevent);
}
else
{
_doubleClickFired = false;
OnDoubleClick(mevent);
OnMouseDoubleClick(mevent);
}
}
_doubleClickFired = false;
}
// Because the code has been like that since long time, we assume that mevent is not null.
base.OnMouseUp(mevent!);
}
protected virtual void OnMultilineChanged(EventArgs e)
{
if (Events[s_multilineChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected override void OnPaddingChanged(EventArgs e)
{
base.OnPaddingChanged(e);
CommonProperties.xClearPreferredSizeCache(this);
LayoutTransaction.DoLayoutIf(AutoSize, ParentInternal, this, PropertyNames.Padding);
AdjustHeight(false);
// The carved modern Visual Styles padding band includes the user Padding, so a runtime change
// has to re-provoke the non-client calculation for it to be reflected in the client rectangle.
if (EffectiveVisualStylesMode >= VisualStylesMode.Net11)
{
RecalculateVisualStylesClientArea();
}
}
protected virtual void OnReadOnlyChanged(EventArgs e)
{
if (Events[s_readOnlyChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected override void OnTextChanged(EventArgs e)
{
// since AutoSize existed in Everett, (and is the default) we can't
// relayout the parent when the "PreferredSize" of the control changes.
// this means a multiline = true textbox won't naturally grow in height when
// the text changes.
CommonProperties.xClearPreferredSizeCache(this);
base.OnTextChanged(e);
if (PInvoke.UiaClientsAreListening())
{
RaiseAccessibilityTextChangedEvent();
}
}
private protected virtual void RaiseAccessibilityTextChangedEvent()
{
if (IsAccessibilityObjectCreated)
{
AccessibilityObject.RaiseAutomationEvent(UIA_EVENT_ID.UIA_Text_TextChangedEventId);
using var textVariant = PasswordProtect ? (VARIANT)string.Empty : (VARIANT)Text;
AccessibilityObject.RaiseAutomationPropertyChangedEvent(UIA_PROPERTY_ID.UIA_ValueValuePropertyId, textVariant, textVariant);
}
}
/// <summary>
/// Returns the character nearest to the given point.
/// </summary>
public virtual char GetCharFromPosition(Point pt)
{
string t = Text;
int index = GetCharIndexFromPosition(pt);
return (index < 0 || index >= t.Length) ? (char)0 : t[index];
}
/// <summary>
/// Returns the index of the character nearest to the given point.
/// </summary>
public virtual int GetCharIndexFromPosition(Point pt)
{
int index = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_CHARFROMPOS, 0, PARAM.FromPoint(pt));
index = PARAM.LOWORD(index);
if (index < 0)
{
index = 0;
}
else
{
string t = Text;
// EM_CHARFROMPOS will return an invalid number if the last character in the RichEdit
// is a newline.
//
if (index >= t.Length)
{
index = Math.Max(t.Length - 1, 0);
}
}
return index;
}
/// <summary>
/// Returns the number of the line containing a specified character position
/// in a textbox. Note that this returns the physical line number
/// and not the conceptual line number. For example, if the first conceptual
/// line (line number 0) word-wraps and extends to the second line, and if
/// you pass the index of a overflowed character, GetLineFromCharIndex would
/// return 1 and not 0.
/// </summary>
public virtual int GetLineFromCharIndex(int index)
=> (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_LINEFROMCHAR, (WPARAM)index);
/// <summary>
/// Returns the location of the character at the given index.
/// </summary>
public virtual Point GetPositionFromCharIndex(int index)
{
if (index < 0 || index >= Text.Length)
{
return Point.Empty;
}
int i = (int)PInvokeCore.SendMessage(
hWnd: this,
Msg: PInvokeCore.EM_POSFROMCHAR,
wParam: (WPARAM)index);
return new Point(PARAM.SignedLOWORD(i), PARAM.SignedHIWORD(i));
}
/// <summary>
/// Returns the index of the first character of a given line. Returns -1 of lineNumber is invalid.
/// </summary>
public int GetFirstCharIndexFromLine(int lineNumber)
{
ArgumentOutOfRangeException.ThrowIfNegative(lineNumber);
return (int)PInvokeCore.SendMessage(
hWnd: this,
Msg: PInvokeCore.EM_LINEINDEX,
wParam: (WPARAM)lineNumber);
}
/// <summary>
/// Returns the index of the first character of the line where the caret is.
/// </summary>
public int GetFirstCharIndexOfCurrentLine()
=> (int)PInvokeCore.SendMessage(
hWnd: this,
Msg: PInvokeCore.EM_LINEINDEX,
wParam: (WPARAM)(-1));
/// <summary>
/// Ensures that the caret is visible in the TextBox window, by scrolling the
/// TextBox control surface if necessary.
/// </summary>
public void ScrollToCaret()
{
if (!IsHandleCreated)
{
_textBoxFlags[s_scrollToCaretOnHandleCreated] = true;
return;
}
if (string.IsNullOrEmpty(WindowText))
{
// If there is no text, then there is no place to go.
return;
}
ScrollToCaretCore();
}
/// <summary>
/// Performs the control-specific work of scrolling the caret into view. The base implementation
/// asks the native edit control to scroll the caret into view; <see cref="RichTextBox"/> overrides
/// this to additionally show as much of the surrounding text as possible.
/// </summary>
/// <remarks>
/// <para>
/// Invoked by <see cref="ScrollToCaret"/> only once the handle exists and the control has text, so
/// overrides do not need to re-check those conditions.
/// </para>
/// </remarks>
private protected virtual void ScrollToCaretCore()
=> PInvokeCore.SendMessage(this, PInvokeCore.EM_SCROLLCARET);
/// <summary>
/// Sets the SelectionLength to 0.
/// </summary>
public void DeselectAll()
{
SelectionLength = 0;
}
/// <summary>
/// Selects a range of text in the text box.
/// </summary>
public void Select(int start, int length)
{
ArgumentOutOfRangeException.ThrowIfNegative(start);
int textLen = TextLength;
if (start > textLen)
{
// We shouldn't allow positive length if you're starting at the end, but
// should allow negative length.
long longLength = Math.Min(0, (long)length + start - textLen);
length = longLength < int.MinValue
? int.MinValue
: (int)longLength;
start = textLen;
}
SelectInternal(start, length, textLen);
}
/// <summary>
/// Performs the actual select without doing arg checking.
///
/// Send in -1 for the textLen parameter if you don't have the text
/// length cached when calling this method. It will be computed.
/// But if you do have it cached, please pass it in. This will avoid
/// the expensive call to the TextLength property.
/// </summary>
private protected virtual void SelectInternal(int selectionStart, int selectionLength, int textLength)
{
// if our handle is created - send message...
if (!IsHandleCreated)
{
// otherwise, wait until handle is created to send this message.
// Store the indices until then...
_selectionStart = selectionStart;
_selectionLength = selectionLength;
_textBoxFlags[s_setSelectionOnHandleCreated] = true;
}
else
{
AdjustSelectionStartAndEnd(
selectionStart,
selectionLength,
out int start,
out int end,
textLength);
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETSEL, (WPARAM)start, (LPARAM)end);
if (IsAccessibilityObjectCreated)
{
AccessibilityObject.RaiseAutomationEvent(end == 0
? UIA_EVENT_ID.UIA_AutomationFocusChangedEventId
: UIA_EVENT_ID.UIA_Text_TextSelectionChangedEventId);
}
}
}
/// <summary>
/// Selects all text in the text box.
/// </summary>
public void SelectAll()
{
int textLength = TextLength;
SelectInternal(0, textLength, textLength);
}
/// <summary>
/// Overrides Control.setBoundsCore to enforce autoSize.
/// </summary>
protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
if (!_integralHeightAdjust && height != Height)
{
_requestedHeight = height;
}
if (_textBoxFlags[s_autoSize] && !_textBoxFlags[s_multiline])
{
height = PreferredHeight;
}
base.SetBoundsCore(x, y, width, height, specified);
}
private static void Swap(ref int n1, ref int n2)
=> (n1, n2) = (n2, n1);
// Send in -1 if you don't have the text length cached
// when calling this method. It will be computed. If not,
// please pass in the text length as the last parameter.
// This will avoid the expensive call to the TextLength
// property.
internal void AdjustSelectionStartAndEnd(int selStart, int selLength, out int start, out int end, int textLen)
{
start = selStart;
end = 0;
if (start <= -1)
{
start = -1;
}
else
{
int textLength = textLen >= 0
? textLen
: TextLength;
if (start > textLength)
{
start = textLength;
}
checked
{
try
{
end = start + selLength;
}
catch (OverflowException)
{
// Since we overflowed, cap at the max/min value: we'll correct the value below
end = start > 0 ? int.MaxValue : int.MinValue;
}
}
// Make sure end is in range
if (end < 0)
{
end = 0;
}
else if (end > textLength)
{
end = textLength;
}
}
}
// Called by CreateHandle or OnHandleCreated
internal void SetSelectionOnHandle()
{
Debug.Assert(IsHandleCreated, "Don't call this method until the handle is created.");
if (_textBoxFlags[s_setSelectionOnHandleCreated])
{
_textBoxFlags[s_setSelectionOnHandleCreated] = false;
AdjustSelectionStartAndEnd(_selectionStart, _selectionLength, out int start, out int end, -1);
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETSEL, (WPARAM)start, (LPARAM)end);
}
}
/// <summary>
/// Converts byte offset to unicode offsets.
/// When processing WM_GETSEL/WM_SETSEL, EDIT control works with byte offsets instead of character positions
/// as opposed to RICHEDIT which does it always as character positions.
/// This method is used when handling the WM_GETSEL message.
/// </summary>
private static void ToUnicodeOffsets(string str, ref int start, ref int end)
{
Encoding e = Encoding.Default;
byte[] bytes = e.GetBytes(str);
bool swap = start > end;
if (swap)
{
Swap(ref start, ref end);
}
// Make sure start and end are within the string
//
if (start < 0)
{
start = 0;
}
if (start > bytes.Length)
{
start = bytes.Length;
}
if (end > bytes.Length)
{
end = bytes.Length;
}
// IMPORTANT: Avoid off-by-1 errors!
// The end value passed in is the character immediately after the last character selected.
int newStart = start == 0 ? 0 : e.GetCharCount(bytes, 0, start);
end = newStart + e.GetCharCount(bytes, start, end - start);
start = newStart;
if (swap)
{
Swap(ref start, ref end);
}
}
/// <summary>
/// Converts unicode offset to byte offsets.
/// When processing WM_GETSEL/WM_SETSEL, EDIT control works with byte offsets instead of character positions
/// as opposed to RICHEDIT which does it always as character positions.
/// This method is used when handling the WM_SETSEL message.
/// </summary>
internal static void ToDbcsOffsets(string str, ref int start, ref int end)
{
Encoding e = Encoding.Default;
bool swap = start > end;
if (swap)
{
Swap(ref start, ref end);
}
// Make sure start and end are within the string
//
if (start < 0)
{
start = 0;
}
if (start > str.Length)
{
start = str.Length;
}
if (end < start)
{
end = start;
}
if (end > str.Length)
{
end = str.Length;
}
// IMPORTANT: Avoid off-by-1 errors!
// The end value passed in is the character immediately after the last character selected.
int newStart = start == 0 ? 0 : e.GetByteCount(str.AsSpan(0, start));
end = newStart + e.GetByteCount(str.AsSpan(start, end - start));
start = newStart;
if (swap)
{
Swap(ref start, ref end);
}
}
/// <summary>
/// Provides some interesting information for the TextBox control in
/// String form.
/// </summary>
public override string ToString()
{
string s = base.ToString();
string txt = Text;
if (txt.Length > 40)
{
txt = $"{txt.AsSpan(0, 40)}...";
}
return $"{s}, Text: {txt}";
}
/// <summary>
/// Undoes the last edit operation in the text box.
/// </summary>
public void Undo()
=> PInvokeCore.SendMessage(this, PInvokeCore.EM_UNDO);
internal virtual void UpdateMaxLength()
{
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_LIMITTEXT, (WPARAM)_maxLength);
}
}
internal override HBRUSH InitializeDCForWmCtlColor(HDC dc, MessageId msg)
{
InitializeClientArea(dc, (HWND)Handle);
if (msg == PInvokeCore.WM_CTLCOLORSTATIC && !ShouldSerializeBackColor())
{
// Let the Win32 Edit control handle background colors itself.
// This is necessary because a disabled edit control will display a different
// BackColor than when enabled.
return default;
}
else
{
return base.InitializeDCForWmCtlColor(dc, msg);
}
}
/// <summary>
/// Provokes the single non-client calc round trip that carves the modern Visual Styles padding
/// band from the client area.
/// </summary>
/// <remarks>
/// <para>
/// This runs only for <see cref="VisualStylesMode"/> values of <see cref="VisualStylesMode.Net11"/>
/// and above, and only once per handle. The latch (<see cref="_triggerNewClientSizeRequest"/>) is
/// reset on handle recreation in <see cref="CreateHandle"/>.
/// </para>
/// </remarks>
private protected virtual unsafe void InitializeClientArea(HDC hDC, HWND hwnd)
{
if (EffectiveVisualStylesMode < VisualStylesMode.Net11
|| _triggerNewClientSizeRequest)
{
return;
}
RecalculateVisualStylesClientArea();
}
/// <summary>
/// Sets the latch and provokes the <c>WM_NCCALCSIZE</c> round trip that carves the modern Visual
/// Styles padding band from the client area. Safe to call repeatedly; it re-carves against the
/// current <see cref="Padding"/> and scrollbar state.
/// </summary>
private protected void RecalculateVisualStylesClientArea()
{
if (!IsHandleCreated || EffectiveVisualStylesMode < VisualStylesMode.Net11)
{
return;
}
_triggerNewClientSizeRequest = true;
// Call SetWindowPos with the current bounds and the SWP_FRAMECHANGED flag. We do not change the
// window position/size, but this provokes the WM_NCCALCSIZE message we need to carve the client area.
PInvoke.SetWindowPos(
hWnd: this,
hWndInsertAfter: HWND.HWND_TOP,
X: 0,
Y: 0,
cx: 0,
cy: 0,
uFlags: SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED
| SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE
| SET_WINDOW_POS_FLAGS.SWP_NOMOVE
| SET_WINDOW_POS_FLAGS.SWP_NOSIZE
| SET_WINDOW_POS_FLAGS.SWP_NOZORDER);
Invalidate(true);
}
/// <summary>
/// When <see langword="true"/>, the native window reserves its own non-client metrics (border and
/// scrollbars) during <c>WM_NCCALCSIZE</c>, so the modern Visual Styles padding band is carved from
/// the client rectangle the default handler produces rather than from the raw proposed window
/// rectangle. The default is <see langword="false"/>: the managed carve is authoritative and the
/// default handler is not invoked (a plain <c>EDIT</c> control keeps its scrollbars inside the
/// managed <see cref="GetScrollBarPadding"/> allowance).
/// </summary>
private protected virtual bool ReservesNativeNonClientArea => false;
/// <summary>
/// Handles <c>WM_NCCALCSIZE</c> by carving the modern Visual Styles padding band from the client
/// rectangle. The carve is floored so the client rectangle can never invert.
/// </summary>
private unsafe void WmNcCalcSize(ref Message m)
{
// Make sure we actually kicked this off.
if (_triggerNewClientSizeRequest)
{
NCCALCSIZE_PARAMS* ncCalcSizeParams = (NCCALCSIZE_PARAMS*)(void*)m.LParamInternal;
if (ncCalcSizeParams is not null)
{
// Controls whose native window reserves its own non-client metrics (RichEdit reserves its
// border and scrollbars) must run the default handler first, so we carve the modern padding
// band from the already-adjusted client rectangle rather than the raw proposed window
// rectangle. Their managed scrollbar allowance is omitted from this carve because the
// native handler already reserved it.
if (ReservesNativeNonClientArea)
{
base.WndProc(ref m);
}
Padding padding = GetVisualStylesPadding(includeScrollbars: !ReservesNativeNonClientArea);
ref RECT clientRect = ref ncCalcSizeParams->rgrc._0;
// Never-invert clamp: a large Padding plus the live scrollbar allowance can drive the
// carved client rect to zero or inverted. A 0-1px client area is acceptable and intended
// (shipping multiline TextBox already collapses this way); we only prevent underflow past
// zero. This is deliberately NOT a MinimumSize and does not vary by VisualStylesMode.
int newTop = clientRect.top + padding.Top;
int newBottom = clientRect.bottom - padding.Bottom;
int newLeft = clientRect.left + padding.Left;
int newRight = clientRect.right - padding.Right;
clientRect.top = newTop;
clientRect.bottom = Math.Max(newTop, newBottom);
clientRect.left = newLeft;
clientRect.right = Math.Max(newLeft, newRight);
m.ResultInternal = (LRESULT)0;
return;
}
}
base.WndProc(ref m);
}
/// <summary>
/// Handles <c>WM_NCPAINT</c> for modern Visual Styles by painting the custom chrome into the
/// window DC. The <c>wParam</c> clip region is intentionally ignored for our own paint and the
/// custom frame is repainted to avoid the offscreen-restore "dirty corners" artifact.
/// </summary>
/// <remarks>
/// <para>
/// The default (themed) non-client paint for a scrollbar-bearing edit control fills the client
/// rectangle, which erases the control's content on every non-client repaint - for example when
/// the mouse hovers over the frame. To prevent that, the default handler is invoked with an update
/// region that spans the whole window but excludes the live client rectangle, so the border and
/// scrollbars still paint while the client area is left untouched.
/// </para>
/// <para>
/// The custom chrome blit also excludes native scrollbars that are currently visible. This keeps
/// the native scrollbar pixels painted by the default handler from being covered by the frame.
/// </para>
/// </remarks>
private void WmNcPaint(ref Message m)
{
if (EffectiveVisualStylesMode < VisualStylesMode.Net11)
{
base.WndProc(ref m);
return;
}
HWND hwnd = (HWND)m.HWnd;
// A non-client update region (in screen coordinates, as WM_NCPAINT requires) that excludes the
// client rectangle. Handing this to the default handler keeps it from overpainting the client.
using RegionScope nonClientRegion = CreateNonClientClipRegion();
WPARAM originalWParam = m.WParamInternal;
HDC hdc = PInvokeCore.GetWindowDC(hwnd);
// Intentional: Graphics.FromHdc does NOT own the DC, so we release the DC ourselves in the
// finally. Do not "tidy" the DC into a single using - that would be a regression.
using Graphics graphics = Graphics.FromHdc(hdc);
try
{
if (!nonClientRegion.IsNull)
{
m.WParamInternal = (WPARAM)(nuint)(nint)nonClientRegion.Region;
}
base.WndProc(ref m);
// Restore the original wParam so nothing downstream observes our temporary clip region.
m.WParamInternal = originalWParam;
OnNcPaint(graphics, hdc);
}
finally
{
int result = PInvokeCore.ReleaseDC(hwnd, hdc);
Debug.Assert(result != 0);
}
}
private void WmPrint(ref Message m)
{
base.WndProc(ref m);
if (EffectiveVisualStylesMode < VisualStylesMode.Net11
|| ((nint)m.LParamInternal & PInvoke.PRF_NONCLIENT) == 0)
{
return;
}
HDC hdc = (HDC)m.WParamInternal;
if (hdc.IsNull)
{
return;
}
using Graphics graphics = Graphics.FromHdc(hdc);
OnNcPaint(graphics, hdc);
}
/// <summary>
/// Builds a non-client update region, in screen coordinates, that spans the whole window but
/// excludes the live client rectangle. This is handed to the default <c>WM_NCPAINT</c> handler so
/// it cannot overpaint (erase) the client area of scrollbar-bearing edit controls.
/// </summary>
/// <returns>
/// The non-client region scope, or a null scope when the window rectangle cannot be retrieved.
/// </returns>
private RegionScope CreateNonClientClipRegion()
{
if (!PInvokeCore.GetWindowRect(this, out RECT windowRect))
{
return new RegionScope(HRGN.Null);
}
PInvokeCore.GetClientRect(this, out RECT clientRect);
Point clientTopLeft = default;
PInvoke.ClientToScreen(this, ref clientTopLeft);
RegionScope nonClientRegion = new(windowRect.left, windowRect.top, windowRect.right, windowRect.bottom);
using RegionScope clientRegion = new(
clientTopLeft.X,
clientTopLeft.Y,
clientTopLeft.X + clientRect.Width,
clientTopLeft.Y + clientRect.Height);
if (PInvokeCore.CombineRgn(nonClientRegion.Region, nonClientRegion.Region, clientRegion.Region, RGN_COMBINE_MODE.RGN_DIFF)
== GDI_REGION_TYPE.RGN_ERROR)
{
nonClientRegion.Dispose();
return new RegionScope(HRGN.Null);
}
return nonClientRegion;
}
/// <summary>
/// Paints the modern Visual Styles non-client chrome (border, rounded <see cref="BorderStyle.Fixed3D"/>
/// lozenge, focus indicator) into a shared offscreen buffer and blits it to the supplied window DC.
/// </summary>
private protected virtual void OnNcPaint(Graphics graphics, HDC windowHdc)
{
int cornerRadius = ScaleVisualStylesMetric(ModernControlVisualStyles.FieldCornerRadius);
Size focusBorderMetrics = GetVisualStylesFocusBorderMetrics();
int borderThickness = Math.Max(focusBorderMetrics.Width, focusBorderMetrics.Height);
int focusBandHeight = GetVisualStylesFocusBandHeight();
Color adornerColor = ForeColor;
Color clientBackColor = BackColor;
Color parentBackColor = Parent?.BackColor ?? BackColor;
using var clientBackgroundBrush = clientBackColor.GetCachedSolidBrushScope();
using var adornerBrush = adornerColor.GetCachedSolidBrushScope();
using var adornerPen = adornerColor.GetCachedPenScope(borderThickness);
Rectangle bounds = new(
x: 0,
y: 0,
width: Bounds.Width,
height: Bounds.Height);
// Repaint the outermost client pixel with the chrome background so no residual native edge can
// remain between the managed frame and edit surface. The rest of the live client stays protected.
Rectangle nativeClientBounds = Rectangle.Intersect(bounds, GetNativeClientRectangle());
Rectangle protectedClientBounds = GetProtectedClientBounds(nativeClientBounds);
Rectangle[] scrollBarBounds = GetVisibleScrollBarRectangles(bounds);
Rectangle deflatedBounds = bounds;
// Making sure we never color outside the lines.
deflatedBounds.Width -= 1;
deflatedBounds.Height -= 1;
// Keep the target clip excluded from the GDI+ drawing as well as from the explicit blits below.
using Region region = new(bounds);
graphics.Clip = region;
graphics.ExcludeClip(protectedClientBounds);
// WinForms paints NC serially (one HWND at a time on the UI thread), so a single shared buffer
// suffices for any number of controls. The buffer is reused when the size fits; steady-state
// allocation is zero.
BufferedGraphicsContext context = BufferedGraphicsManager.Current;
using BufferedGraphics buffer = context.Allocate(graphics, bounds);
// Intentional: the buffer owns this Graphics - do NOT dispose buffer.Graphics separately.
Graphics offscreenGraphics = buffer.Graphics;
Rectangle bufferBounds = bounds;
// We need anti-aliasing for the rounded chrome.
offscreenGraphics.SmoothingMode = SmoothingMode.AntiAlias;
// AddRoundedRectangle receives the bounding size of each corner arc, so one corner size plus
// the border thickness is the minimum height that avoids overlapping curves.
bool canRenderRoundedChrome = CanRenderVisualStylesRoundedChrome(
deflatedBounds,
cornerRadius,
borderThickness);
if (BorderStyle == BorderStyle.Fixed3D && canRenderRoundedChrome)
{
ParentBackgroundRenderer.Paint(this, offscreenGraphics, bufferBounds, parentBackColor);
}
else
{
using var parentBackgroundBrush = parentBackColor.GetCachedSolidBrushScope();
offscreenGraphics.FillRectangle(parentBackgroundBrush, bounds);
}
switch (BorderStyle)
{
case BorderStyle.None:
// Just fill a rectangle.
offscreenGraphics.FillRectangle(clientBackgroundBrush, deflatedBounds);
break;
case BorderStyle.FixedSingle:
offscreenGraphics.FillRectangle(clientBackgroundBrush, deflatedBounds);
offscreenGraphics.DrawRectangle(adornerPen, deflatedBounds);
break;
case BorderStyle.Fixed3D:
if (canRenderRoundedChrome)
{
using GraphicsPath roundedBodyPath = new();
roundedBodyPath.AddRoundedRectangle(deflatedBounds, new Size(cornerRadius, cornerRadius));
offscreenGraphics.FillPath(clientBackgroundBrush, roundedBodyPath);
offscreenGraphics.DrawPath(adornerPen, roundedBodyPath);
// The rounded chrome is clipped with a non-antialiased region; blend the resulting
// corner artifacts into the parent by tracing the parent color just outside the border.
ParentBackgroundRenderer.PaintRoundedBorderRegionMitigation(
offscreenGraphics,
deflatedBounds,
new Size(cornerRadius, cornerRadius),
borderThickness,
parentBackColor);
}
else
{
// Chrome degradation fallback - flat render in place of the broken lozenge.
offscreenGraphics.FillRectangle(clientBackgroundBrush, deflatedBounds);
offscreenGraphics.DrawRectangle(adornerPen, deflatedBounds);
}
break;
}
if (BorderStyle == BorderStyle.Fixed3D && canRenderRoundedChrome)
{
Color focusColor = GetVisualStylesFocusColor(Application.SystemVisualSettings.HighContrastEnabled);
FocusIndicatorRenderer.DrawRoundedFocusIndicator(
offscreenGraphics,
deflatedBounds,
cornerRadius,
borderThickness,
focusBandHeight,
adornerColor,
focusColor);
}
else if (Focused)
{
Color focusColor = GetVisualStylesFocusColor(Application.SystemVisualSettings.HighContrastEnabled);
using var focusPen = focusColor.GetCachedPenScope(borderThickness);
int focusLineCount = Math.Min(
Math.Max(2, focusBorderMetrics.Height),
Math.Max(1, deflatedBounds.Height));
for (int i = 0; i < focusLineCount; i++)
{
offscreenGraphics.DrawLine(
focusPen,
deflatedBounds.Left,
deflatedBounds.Bottom - i,
deflatedBounds.Right,
deflatedBounds.Bottom - i);
}
}
Rectangle[] nonClientBands = GetNonClientPaintBands(
bufferBounds,
protectedClientBounds,
scrollBarBounds);
IntPtr bufferHdc = offscreenGraphics.GetHdc();
try
{
foreach (Rectangle band in nonClientBands)
{
if (band.Width > 0 && band.Height > 0)
{
PInvokeCore.BitBlt(
hdc: windowHdc,
x: band.X,
y: band.Y,
cx: band.Width,
cy: band.Height,
hdcSrc: (HDC)bufferHdc,
x1: band.X,
y1: band.Y,
rop: ROP_CODE.SRCCOPY);
}
}
}
finally
{
offscreenGraphics.ReleaseHdcInternal(bufferHdc);
}
}
private static Rectangle[] GetNonClientPaintBands(Rectangle bounds, Rectangle clientBounds)
=> GetNonClientPaintBands(bounds, clientBounds, []);
private static Rectangle GetProtectedClientBounds(Rectangle clientBounds)
=> clientBounds.Width > 2 && clientBounds.Height > 2
? Rectangle.Inflate(clientBounds, -1, -1)
: clientBounds;
private static Rectangle[] GetNonClientPaintBands(
Rectangle bounds,
Rectangle clientBounds,
Rectangle[] additionalProtectedBounds)
{
if (bounds.Width <= 0 || bounds.Height <= 0)
{
return [Rectangle.Empty, Rectangle.Empty, Rectangle.Empty, Rectangle.Empty];
}
Rectangle protectedBounds = Rectangle.Intersect(bounds, clientBounds);
Rectangle[] initialBands = protectedBounds.Width <= 0 || protectedBounds.Height <= 0
? [bounds, Rectangle.Empty, Rectangle.Empty, Rectangle.Empty]
: [
Rectangle.FromLTRB(bounds.Left, bounds.Top, bounds.Right, protectedBounds.Top),
Rectangle.FromLTRB(bounds.Left, protectedBounds.Bottom, bounds.Right, bounds.Bottom),
Rectangle.FromLTRB(bounds.Left, protectedBounds.Top, protectedBounds.Left, protectedBounds.Bottom),
Rectangle.FromLTRB(protectedBounds.Right, protectedBounds.Top, bounds.Right, protectedBounds.Bottom)
];
if (additionalProtectedBounds.Length == 0)
{
return initialBands;
}
List<Rectangle> paintBands = [.. initialBands];
foreach (Rectangle additionalProtectedBoundsItem in additionalProtectedBounds)
{
Rectangle clippedProtectedBounds = Rectangle.Intersect(bounds, additionalProtectedBoundsItem);
if (clippedProtectedBounds.Width <= 0 || clippedProtectedBounds.Height <= 0)
{
continue;
}
for (int i = paintBands.Count - 1; i >= 0; i--)
{
Rectangle band = paintBands[i];
Rectangle intersection = Rectangle.Intersect(band, clippedProtectedBounds);
if (intersection.Width <= 0 || intersection.Height <= 0)
{
continue;
}
paintBands.RemoveAt(i);
AddPaintBand(Rectangle.FromLTRB(band.Left, band.Top, band.Right, intersection.Top));
AddPaintBand(Rectangle.FromLTRB(band.Left, intersection.Bottom, band.Right, band.Bottom));
AddPaintBand(Rectangle.FromLTRB(band.Left, intersection.Top, intersection.Left, intersection.Bottom));
AddPaintBand(Rectangle.FromLTRB(intersection.Right, intersection.Top, band.Right, intersection.Bottom));
}
}
return [.. paintBands];
void AddPaintBand(Rectangle band)
{
if (band.Width > 0 && band.Height > 0)
{
paintBands.Add(band);
}
}
}
private unsafe Rectangle[] GetVisibleScrollBarRectangles(Rectangle bounds)
{
if (!IsHandleCreated
|| !PInvokeCore.GetWindowRect(this, out RECT windowRect))
{
return [];
}
List<Rectangle> scrollBarBounds = [];
AddVisibleScrollBar(HorizontalScrollBarObjectId);
AddVisibleScrollBar(VerticalScrollBarObjectId);
return [.. scrollBarBounds];
void AddVisibleScrollBar(OBJECT_IDENTIFIER objectId)
{
SCROLLBARINFO scrollBarInfo = new()
{
cbSize = (uint)sizeof(SCROLLBARINFO)
};
if (!PInvoke.GetScrollBarInfo((HWND)Handle, objectId, ref scrollBarInfo)
|| (scrollBarInfo.rgstate[0] & StateSystemInvisible) != 0)
{
return;
}
Rectangle scrollBarRectangle = Rectangle.FromLTRB(
scrollBarInfo.rcScrollBar.left - windowRect.left,
scrollBarInfo.rcScrollBar.top - windowRect.top,
scrollBarInfo.rcScrollBar.right - windowRect.left,
scrollBarInfo.rcScrollBar.bottom - windowRect.top);
scrollBarRectangle.Intersect(bounds);
if (scrollBarRectangle.Width > 0 && scrollBarRectangle.Height > 0)
{
scrollBarBounds.Add(scrollBarRectangle);
}
}
}
private Rectangle GetNativeClientRectangle()
{
if (!IsHandleCreated
|| !PInvokeCore.GetWindowRect(this, out RECT windowRect))
{
return Rectangle.Empty;
}
PInvokeCore.GetClientRect(this, out RECT clientRect);
Point clientTopLeft = default;
PInvoke.ClientToScreen(this, ref clientTopLeft);
return new Rectangle(
clientTopLeft.X - windowRect.left,
clientTopLeft.Y - windowRect.top,
clientRect.Width,
clientRect.Height);
}
private AnimatedFocusIndicatorRenderer FocusIndicatorRenderer
=> _focusIndicatorRenderer ??= new(this, InvalidateVisualStylesFrame);
private unsafe void InvalidateVisualStylesFrame()
{
if (!IsHandleCreated)
{
return;
}
PInvoke.RedrawWindow(
hWnd: this,
lprcUpdate: null,
hrgnUpdate: HRGN.Null,
flags: REDRAW_WINDOW_FLAGS.RDW_FRAME | REDRAW_WINDOW_FLAGS.RDW_INVALIDATE);
}
private void WmReflectCommand(ref Message m)
{
if (_textBoxFlags[s_codeUpdateText] || _textBoxFlags[s_creatingHandle])
{
return;
}
uint hiword = m.WParamInternal.HIWORD;
if (hiword == PInvoke.EN_CHANGE && CanRaiseTextChangedEvent)
{
OnTextChanged(EventArgs.Empty);
}
else if (hiword == PInvoke.EN_UPDATE)
{
// Force update to the Modified property, which will trigger ModifiedChanged event handlers
_ = Modified;
}
}
private void WmSetFont(ref Message m)
{
base.WndProc(ref m);
if (!_textBoxFlags[s_multiline])
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMARGINS, (WPARAM)(PInvoke.EC_LEFTMARGIN | PInvoke.EC_RIGHTMARGIN));
}
}
private void WmGetDlgCode(ref Message m)
{
base.WndProc(ref m);
m.ResultInternal = AcceptsTab
? (LRESULT)(nint)(m.ResultInternal | (int)PInvoke.DLGC_WANTTAB)
: (LRESULT)(nint)(m.ResultInternal & ~(int)(PInvoke.DLGC_WANTTAB
| PInvoke.DLGC_WANTALLKEYS));
}
/// <summary>
/// Handles the WM_CONTEXTMENU message. Show the ContextMenuStrip if present.
/// </summary>
private void WmTextBoxContextMenu(ref Message m)
{
if (ContextMenuStrip is null)
{
return;
}
Point client;
bool keyboardActivated = false;
// LParam will be -1 when the user invokes the context menu with the keyboard.
if (m.LParamInternal == -1)
{
keyboardActivated = true;
client = new Point(Width / 2, Height / 2);
}
else
{
client = PointToClient(PARAM.ToPoint(m.LParamInternal));
}
// Only show the context menu when clicked in the client area.
if (ClientRectangle.Contains(client))
{
ContextMenuStrip.ShowInternal(this, client, keyboardActivated);
}
}
/// <summary>
/// The control's window procedure. Inheriting classes can override this
/// to add extra functionality, but should not forget to call
/// base.wndProc(m); to ensure the control continues to function properly.
/// </summary>
protected override void WndProc(ref Message m)
{
switch (m.MsgInternal)
{
case PInvokeCore.WM_NCCALCSIZE:
WmNcCalcSize(ref m);
break;
case PInvokeCore.WM_NCPAINT:
WmNcPaint(ref m);
break;
case PInvokeCore.WM_PRINT:
WmPrint(ref m);
break;
case PInvokeCore.WM_LBUTTONDBLCLK:
_doubleClickFired = true;
base.WndProc(ref m);
break;
case MessageId.WM_REFLECT_COMMAND:
WmReflectCommand(ref m);
break;
case PInvokeCore.WM_GETDLGCODE:
WmGetDlgCode(ref m);
break;
case PInvokeCore.WM_SETFONT:
WmSetFont(ref m);
break;
case PInvokeCore.WM_CONTEXTMENU:
if (ShortcutsEnabled)
{
// Calling base will find ContextMenus in this order:
// 1) ContextMenuStrip 2) SystemMenu
base.WndProc(ref m);
}
else
{
// We'll handle this message so we can hide the
// SystemMenu if ContextMenuStrip menus are null
WmTextBoxContextMenu(ref m);
}
break;
case PInvokeCore.WM_DESTROY:
if (TryGetAccessibilityObject(out AccessibleObject? @object) && @object is TextBoxBaseAccessibleObject accessibleObject &&
!RecreatingHandle)
{
accessibleObject.ClearObjects();
}
base.WndProc(ref m);
break;
default:
base.WndProc(ref m);
break;
}
}
}