33

I have a Winforms dialog that contains among other controls a TextBox that allows a single line of input. I would like to allow the user to be able to press Ctrl-Backspace to delete an entire word. This is not the default behaviour with the out-of-the-box TextBox; I get a rectangle character, rather than having the word deleted.

I have confirmed the ShortcutsEnabled property is set to True.

I did find that I can use a RichTextBox rather than a TextBox to get the behaviour I want. The problem with this is that the apperance of the RichTextBox (border in particular) is different from that of the TextBox, and I don't need or want the ability to mark up text.

So my question is how to best handle this situation? Is there some property on the TextBox that I am missing? Or is it best to use the RichTextBox, update the appearance so it is consistent, and disable markup of the text?

I am relatively happy to write the code to handle the KeyDown and KeyPress events explicity if there is no better way, but thought it was worth checking first.

Ian Ringrose
  • 51,220
  • 55
  • 213
  • 317
Rhys Jones
  • 3,833
  • 3
  • 21
  • 17
  • In my experience atleast, stay weeell away from RichTextBox... it's a performance nightmare from what I saw the one time I wanted to use it (admittedly, with alot of colours and stuff, but still a nightmare) – Matthew Scharley Jul 14 '09 at 10:53
  • Related: https://github.com/dotnet/winforms/issues/259 – blackboxlogic Oct 22 '20 at 15:46

12 Answers12

31

Old question, but I just stumbled upon an answer that doesn't require any extra code.

Enable autocompletion for the textbox and CTRL-Backspace should work as you want it to.

CTRL-Backspace deleting whole word to the left of the caret seems to be a 'rogue feature' of the autocomplete handler. That's why enabling autocomplete fixes this issue.

Source 1 | Source 2

--

You can enable the auto complete feature with setting the AutoCompleteMode and AutoCompleteSource to anything you like (for instance; Suggest and RecentlyUsedList)

binki
  • 7,754
  • 5
  • 64
  • 110
Damir
  • 421
  • 4
  • 4
  • 1
    Such a simple fix, yet so hard to find. Thank you very much! – Mathlight Nov 17 '15 at 16:58
  • 9
    FYI, unfortunately, I believe this will not work for multi-line textboxes :( – Tom Dec 15 '15 at 23:21
  • 1
    Adding `AutoCompleteSource = "CustomSource"`and `AutoCompleteMode = "Append"` works without warning or errors, but is it otherwise safe to leave `AutoCompleteCustomSource` entirely undefined? – lapingultah Dec 28 '18 at 12:46
  • Also having problem having this to work with multi-line textbox (vs 2017) – Anders Lindén Jul 12 '19 at 23:43
  • Thanks for the tip! Apparently wxPython uses this control for a wx.TextEntry, which is inherited by things like wx.TextCtrl and wx.ComboBox. If you implement the AutoComplete method of wx.TextEntry it also corrects this. I only needed to implement the wx.TextCompleter.Start() method and simply returned False. See also: https://discuss.wxpython.org/t/clicking-in-a-textctrl-from-another-program-doesnt-move-the-caret/35349. – Samuel Mar 30 '22 at 16:48
24

/* Update 2: Please look at https://positivetinker.com/adding-ctrl-a-and-ctrl-backspace-support-to-the-winforms-textbox-control as it fixes all issues with my simple solution */

/* Update 1: Please look also at Damir’s answer below, it’s probably a better solution :) */

I would simulate Ctrl+Backspace by sending Ctrl+Shift+Left and Backspace to the TextBox. The effect is virtually the same, and there is no need to manually process control’s text. You can achieve it using this code:

class TextBoxEx : TextBox
{
    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        if (keyData == (Keys.Control | Keys.Back))
        {
            SendKeys.SendWait("^+{LEFT}{BACKSPACE}");
            return true;
        }
        return base.ProcessCmdKey(ref msg, keyData);
    }
}

You can also modify the app.config file to force the SendKey class to use newer method of sending keys:

<configuration>
  <appSettings>
    <add key="SendKeys" value="SendInput" />
  </appSettings>
</configuration>
LukeSw
  • 661
  • 3
  • 13
  • 1
    see note in http://msdn.microsoft.com/en-us/library/system.windows.forms.sendkeys.aspx for explanation on why you might need to modify app.config – sjlewis Jan 28 '11 at 05:22
  • 12
    Doesn't work as intended when I hold control, and backspace together. It deletes the complete words first time, and then deletes chars one by one. – Prasanth Oct 04 '12 at 08:50
  • When you send "^" through SendKeys you're sending a full KeyUp and KeyDown, after which Ctrl is no longer depressed. If you drop that from the SendKeys string it'll still accomplish the stated function, since the user is conveniently holding Ctrl for you when Shift-Left-Backspace fires. Of course, that'll also cause a near-infinite loop. – MikeTV Jul 22 '17 at 21:06
9

While the ProcessCmdKey override works nice and all, it limits itself to only one iteration of Ctrl+Backspace, mainly because the use of SendWait mimics a keystroke, and if you were to hold down Ctrl while pressing Backspace again, the system only seems to recognize the Backspace key being pressed. If you were to log the keystrokes of the override, you would find a collection of extra keys that you never actually pressed.

An alternative approach is to explicitly manage the appearance of the textbox in the ProcessCmdKey override, and not send more keys to the system. This could easily be applied to Ctrl+Delete as well.

I've included a few of the common "stopping points" for Ctrl+Backspace behavior, and used a switch statement as opposed to a RegEx. They never feel clean enough, and I usually end up missing a character

If you see any problems with my code, by all means please let me know. Best of luck for anyone that still is befuddled by this conundrum!

public class TextBoxEx : TextBox
{
    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        if (keyData == (Keys.Back | Keys.Control))
        {
            for (int i = this.SelectionStart - 1; i > 0; i--)
            {
                switch (Text.Substring(i, 1))
                {    //set up any stopping points you want
                    case " ":
                    case ";":
                    case ",":
                    case "/":
                    case "\\":                        
                        Text = Text.Remove(i, SelectionStart - i);
                        SelectionStart = i;
                        return true;
                    case "\n":
                        Text = Text.Remove(i - 1, SelectionStart - i);
                        SelectionStart = i;
                        return true;
                }
            }
            Clear();        //in case you never hit a stopping point, the whole textbox goes blank
            return true;
        }
        else
        {
            return base.ProcessCmdKey(ref msg, keyData);
        }
    }  
}
DWF
  • 290
  • 2
  • 11
  • 2
    This is the solution I went with, since the accepted answer only works for an individual press and full release of CTRL+BACK. Instead of the switch block, though, I used a conditional block: if (char.IsPunctuation(Text, i) || char.IsSeparator(Text, i) || char.IsWhiteSpace(Text, i)) { ... }. This checks all of the common separator types like space, period, comma, dash, etc. – kreddkrikk Jan 13 '16 at 19:26
  • 1
    That call to `Clear()` will empty the text box, even if you start ctrl+backspacing from the middle instead of from the very end. It would be better to have the `for`-`switch` find where to start, and then put the removal after the loop. – Jed Schaaf Nov 22 '18 at 07:51
  • Today, I found a situation where a textbox with multiline, it I hit the ctrl+back shortcut and I'm on the last word of any line except the first one, the application stop responding. – Samuel Mar 08 '19 at 20:13
6

I'm not sure it is possible without a custom KeyDown or KeyPress event, the following code works though:

private void textBox1_KeyDown(object sender, KeyEventArgs e)
{
    if ((e.KeyCode == Keys.Back) && e.Control)
    {
        e.SuppressKeyPress = true;
        int selStart = textBox1.SelectionStart;
        while (selStart > 0 && textBox1.Text.Substring(selStart - 1, 1) == " ")
        {
            selStart--;
        }
        int prevSpacePos = -1;
        if (selStart != 0)
        {
            prevSpacePos = textBox1.Text.LastIndexOf(' ', selStart - 1);
        }
        textBox1.Select(prevSpacePos + 1, textBox1.SelectionStart - prevSpacePos - 1);
        textBox1.SelectedText = "";
    }
}
Patrick McDonald
  • 64,141
  • 14
  • 108
  • 120
  • See LukeSw's answer (http://stackoverflow.com/questions/1124639/winforms-textbox-using-ctrl-backspace-to-delete-whole-word/1197339#1197339) for a better method than mine – Patrick McDonald Aug 04 '09 at 09:31
  • Just taken a look at this question again; I think you are right, I have updated the accepted answer :) – Rhys Jones Nov 09 '09 at 09:28
4

Regex was made for this. Use it.

    private void TextBox_KeyDown(object sender, KeyEventArgs e)
    {
        TextBox box = (TextBox)sender;
        if (e.KeyData == (Keys.Back | Keys.Control))
        {
            if (!box.ReadOnly && box.SelectionLength == 0)
            {
                RemoveWord(box);
            }
            e.SuppressKeyPress = true;
        }
    }

    private void RemoveWord(TextBox box)
    {
        string text = Regex.Replace(box.Text.Substring(0, box.SelectionStart), @"(^\W)?\w*\W*$", "");
        box.Text = text + box.Text.Substring(box.SelectionStart);
        box.SelectionStart = text.Length;
    }
Avenicci
  • 41
  • 2
3

This works nice:

static Regex RegExWholeWord = new Regex(@"(\r\n|[^A-Za-z0-9_\r\n]+?|\w+?) *$", RegexOptions.Compiled);

In key-down, use

var m = RegExWholeWord.Match(textbox.Text, 0, textbox.SelectionStart);
if (m.Success)
{
    textbox.Text = textbox.Text.Remove(m.Index, m.Length);
    textbox.SelectionStart = m.Index;
}
Thomsen
  • 773
  • 7
  • 14
3

This is what I landed up using, it also handles multi line textboxes

private void HandleCtrlBackspace_KeyDown(object sender, KeyEventArgs e) {
  switch (e.KeyData) {
    case (Keys.Back | Keys.Control):
      e.SuppressKeyPress = true;
      TextBox textbox = (TextBox)sender;
      int i;
      if (textbox.SelectionStart.Equals(0)) {
        return;
      }
      int space = textbox.Text.LastIndexOf(' ', textbox.SelectionStart - 1);
      int line = textbox.Text.LastIndexOf("\r\n", textbox.SelectionStart - 1);
      if (space > line) {
        i = space;
      } else {
        i = line;
      }
      if (i > -1) {
        while (textbox.Text.Substring(i - 1, 1).Equals(' ')) {
          if (i.Equals(0)) {
            break;
          }
          i--;
        }
        textbox.Text = textbox.Text.Substring(0, i) + textbox.Text.Substring(textbox.SelectionStart);
        textbox.SelectionStart = i;
      } else if (i.Equals(-1)) {
        textbox.Text = textbox.Text.Substring(textbox.SelectionStart);
      }
      break;
  }
}
Carl
  • 31
  • 1
2

This is the way yo go :)

private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
    //if ctrl+bcksp
    if (e.KeyChar == 127)
    {
        //if not last word
        if (textBox1.Text.Split (' ').Count() > 1)
        {
            //remoce last word form list and put it back together (gotta love lambda)
            textBox1.Text = textBox1.Text.Split (' ').Take (textBox1.Text.Split (' ').Count() - 1).Aggregate ((a,b) => a + " " + b);
            //set selection at the end
            textBox1.SelectionStart = textBox1.Text.Length;
        }
        else if (textBox1.Text.Split (' ').Count() == 1)
        {
            textBox1.Text = "";
        }
    }
}
albertjan
  • 7,739
  • 6
  • 44
  • 74
  • Thanks for the response; this works, but needs to also needs to set e.Handled = true. Otherwise, it deletes the word and creates the *rectangle* also. – Rhys Jones Jul 14 '09 at 19:41
0

I am answering in VB rather than C# cuz I was looking for this solution in VB but couldn't find one, but these C# responses helped me work it out :-D

Create this sub in a module

Public Sub ctrl_bksp(ByRef t As TextBox)
    Dim ss As Integer = t.SelectionStart
    Dim sl As Integer = t.SelectionLength
    Dim tl As Integer = t.TextLength

    '//Split either side of selection start
    Dim strPre As String = Strings.Left(t.Text, tl - (tl - ss))
    Dim strPost As String = Strings.Right(t.Text, tl - ss - sl)

    '//Get Last Space Location in StrPre
    Dim s As Integer = Strings.InStrRev(RTrim(strPre), " ")

    strPre = Strings.Left(strPre, s)

    t.Text = strPre & strPost
    t.SelectionStart = s
End Sub

Then you can call this sub from within any textbox's KeyPress event:

Private Sub Textbox1_KeyPress(sender As Object, e As System.Windows.Forms.KeyPressEventArgs) Handles Textbox1.KeyPress
    Select Case e.KeyChar
        Case Chr(127)   '//Ctrl+Backspace
            e.Handled = True
            Call ctrl_bksp(Textbox1)
    End Select
End Sub

This will work no matter where the selection is within the string, and whether or not text is selected, and responds magnificently!

torpid prey
  • 242
  • 1
  • 12
0

DWF and giangurgolo, thanks for your information provided. Below a refined version of it. Note that it also considers ComboBox, as that has the very same issue as TextBox. Also note that shortcuts are only active if configuration of TextBox or ComboBox allow so.

TextBoxEx:

public class TextBoxEx : TextBox
{
    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        // Attention:
        // Similar code exists in ComboBoxEx.ProcessCmdKey().
        // Changes here may have to be applied there too.

        if (ShortcutsEnabled)
        {
            if (keyData == (Keys.Control | Keys.Back))
            {
                if (!ReadOnly)
                {
                    if (SelectionStart > 0)
                    {
                        int i = (SelectionStart - 1);

                        // Potentially trim white space:
                        if (char.IsWhiteSpace(Text, i))
                            i = (StringEx.StartIndexOfSameCharacterClass(Text, i) - 1);

                        // Find previous marker:
                        if (i > 0)
                            i = StringEx.StartIndexOfSameCharacterClass(Text, i);
                        else
                            i = 0; // Limit i as it may become -1 on trimming above.

                        // Remove until previous marker or the beginning:
                        Text = Text.Remove(i, SelectionStart - i);
                        SelectionStart = i;
                        return (true);
                    }
                    else
                    {
                        return (true); // Ignore to prevent a white box being placed.
                    }
                }
            }
            else if (keyData == (Keys.Control | Keys.A))
            {
                if (!ReadOnly && Multiline)
                {
                    SelectAll();
                    return (true);
                }
            }
        }

        return (base.ProcessCmdKey(ref msg, keyData));
    }
}

ComboxBoxEx:

public class ComboBoxEx : ComboBox
{
    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        // Attention:
        // Similar code exists in TextBoxEx.ProcessCmdKey().
        // Changes here may have to be applied there too.

        if (keyData == (Keys.Control | Keys.Back))
        {
            if (DropDownStyle != ComboBoxStyle.DropDownList)
            {
                if (SelectionStart > 0)
                {
                    int i = (SelectionStart - 1);

                    // Potentially trim white space:
                    if (char.IsWhiteSpace(Text, i))
                        i = (StringEx.StartIndexOfSameCharacterClass(Text, i) - 1);

                    // Find previous marker:
                    if (i > 0)
                        i = StringEx.StartIndexOfSameCharacterClass(Text, i);
                    else
                        i = 0; // Limit i as it may become -1 on trimming above.

                    // Remove until previous marker or the beginning:
                    Text = Text.Remove(i, SelectionStart - i);
                    SelectionStart = i;
                    return (true);
                }
                else
                {
                    return (true); // Ignore to prevent a white box being placed.
                }
            }
        }

        return (base.ProcessCmdKey(ref msg, keyData));
    }
}

String auxiliary (e.g. static class StringEx):

/// <summary>
/// Returns the start index of the same character class.
/// </summary>
/// <param name="str">The <see cref="string"/> object to process.</param>
/// <param name="startIndex">The search starting position.</param>
/// <returns>
/// The zero-based index position of the start of the same character class in the string.
/// </returns>
public static int StartIndexOfSameCharacterClass(string str, int startIndex)
{
    int i = startIndex;

    if (char.IsWhiteSpace(str, i)) // Includes 'IsSeparator' (Unicode space/line/paragraph
    {                              // separators) as well as 'IsControl' (<CR>, <LF>,...).
        for (/* i */; i >= 0; i--)
        {
            if (!char.IsWhiteSpace(str, i))
                return (i + 1);
        }
    }
    else if (char.IsPunctuation(str, i))
    {
        for (/* i */; i >= 0; i--)
        {
            if (!char.IsPunctuation(str, i))
                return (i + 1);
        }
    }
    else if (char.IsSymbol(str, i))
    {
        for (/* i */; i >= 0; i--)
        {
            if (!char.IsSymbol(str, i))
                return (i + 1);
        }
    }
    else
    {
        for (/* i */; i >= 0; i--)
        {
            if (char.IsWhiteSpace(str, i) || char.IsPunctuation(str, i) || char.IsSymbol(str, i))
                return (i + 1);
        }
    }

    return (0);
}
0

I had problems with these approaches:

  • Replacing .Text has scrolling issues with large texts.
  • Doing SendKeys.SendWait("^+{LEFT}{BACKSPACE}") in textBox.KeyDown event handler was not stable at all for me.
  • Using .Cut() changes the clipboard (but works fine otherwise).

Looking at the .NET reference source what .Cut() does lead me to the following solution: Select the text in the TextBox and then use WM_CLEAR to clear it. Seems to work fine and it's not sending artificial key press events.

class CtrlBackspaceSupport
{
    TextBox textBox;
    public CtrlBackspaceSupport(TextBox textBox)
    {
        this.textBox = textBox;
        textBox.KeyDown += new KeyEventHandler(textBox_KeyDown);
    }

    [DllImport("user32.dll", SetLastError = true)]
    static extern int SendMessage(IntPtr hwnd, int wMsg, int wParam, int lParam);
    const int WM_CLEAR = 0x0303;

    void textBox_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Control && e.KeyCode == Keys.Back)
        {   // Ctrl+Backspace -> remove till word border before cursor
            e.SuppressKeyPress = true;
            if (0 == textBox.SelectionLength && textBox.SelectionStart > 1)
            {   // nothing selected
                var text = textBox.Text;
                int indexOfSpace = text.LastIndexOf(' ', textBox.SelectionStart - 2);
                if (-1 != indexOfSpace)
                {   // found something
                    indexOfSpace++;
                    textBox.Select(indexOfSpace, textBox.SelectionStart - indexOfSpace);
                    SendMessage(new HandleRef(textBox, textBox.Handle).Handle, WM_CLEAR, 0, 0);
                }
            }
        }
    }
}
Martin Schmidt
  • 1,331
  • 10
  • 5
0

I made a few modifications to Avenicci's code to stop at a set of separators; a string array of commas, parentheses and such.

if (e.KeyData == (Keys.Back | Keys.Control))
{
e.SuppressKeyPress = true;
string text="";
foreach (string s in separators)
{
    // (\))?\W*$ not word
    // (\w)?\w*$ word
    Match m = Regex.Match(Text, $@"(\{s})?\W*$");
    if (!m.Value.Equals(""))
    {
        text = Regex.Replace(Text.Substring(0, SelectionStart), $@"(\{s})?\W?$", "");
        break;
    }
}
if (text.Equals(""))
    text = Regex.Replace(Text.Substring(0, SelectionStart), @"(\w)?\w*$", "");
Text = text + Text.Substring(SelectionStart);
SelectionStart = text.Length;
}
Smorkster
  • 322
  • 3
  • 10