Rhys Jones
Rhys Jones

Reputation: 3993

Winforms Textbox - Using Ctrl-Backspace to Delete Whole Word

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.

Upvotes: 36

Views: 13733

Answers (12)

LukeSw
LukeSw

Reputation: 671

/* 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>

Upvotes: 25

Damir
Damir

Reputation: 461

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)

Upvotes: 35

Smorkster
Smorkster

Reputation: 357

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;
}

Upvotes: 0

Martin Schmidt
Martin Schmidt

Reputation: 1381

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);
                }
            }
        }
    }
}

Upvotes: 0

Avenicci
Avenicci

Reputation: 41

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;
    }

Upvotes: 4

maettu_this
maettu_this

Reputation: 21

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);
}

Upvotes: 0

torpid prey
torpid prey

Reputation: 242

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!

Upvotes: 0

DWF
DWF

Reputation: 360

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);
        }
    }  
}

Upvotes: 9

Thomsen
Thomsen

Reputation: 773

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;
}

Upvotes: 3

Carl
Carl

Reputation: 31

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;
  }
}

Upvotes: 3

Patrick McDonald
Patrick McDonald

Reputation: 65411

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 = "";
    }
}

Upvotes: 6

albertjan
albertjan

Reputation: 7817

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 = "";
        }
    }
}

Upvotes: 3

Related Questions