WinForms: Subclassing the TextBox inside a ComboBox


I learned something new today and decided to share it here.


 


Problem:


I have a form with a ComboBox that has a style of DropDown. This style allows the user to type anything into the ComboBox. To aid the user AutoComplete is turned on. But when the user used Ctrl+Z or Undo from the context menu. The value was not returned to its original value. The last autocompleted value was put back into the ComboBox. Generally, this meant that only the last character typed was removed. I wanted to control how Undo worked on the ComboBox.


 


Solution:


I tried subclassing the ComboBox and overriding WndProc in the ComboBox, but the EM_UNDO and EM_CANUNDO messages didn’t go there. So, I guessed that I had to subclass the TextBox inside the ComboBox. In windows, the ComboBox is a composite control made up of a TextBox, button, and a listbox.


 


Obstacle #1: WinForms doesn’t seem to expose the TextBox window in any public manner. For this reason, I resorted to calling a native method GetWindow to get the first child window of the combobox, which is the textbox.


 


Obstacle #2: GetWindow just returns a handle, what do I do with that? WinForms has a class called NativeWindow. It allows you to override the WndProc of any control given just the handle. So, I build a class called ComboTextBox that derived from NativeWindow and overrode WndProc for EM_UNDO and EM_CANUNDO.


 


Obstacle #3: Ctrl+Z worked great, just like I had planned. But the context menu for the ComboBox was not behaving. It was ignoring my ComboTextBox class or so I thought. I loaded Spy++ and took a look at the messages that the context menu was sending. I was surprised to find out that the context menu doesn’t send EM messages at all. It was sending WM_UNDO instead. So, I changed my WndProc to catch that message as well.


 


Here is the code that I ended up with (I think I got the clean up code correct, but I haven’t done much testing, yet):


 


class ComboBoxInternal : ComboBox


{


    private ComboTextBox m_textBox;


    private string m_undoValue;


 


    public ComboBoxInternal()


    {


    }


 


    public void Undo()


    {


        // code to undo to previous value


        this.Text = m_undoValue;


    }


 


    public bool CanUndo()


    {


        // code to check if Undo is allowed


        // for now return true


        return true;


    }


 


    protected override void OnGotFocus(EventArgs e)


    {


        base.OnGotFocus(e);


        m_undoValue = this.Text;


    }


 


    protected override void OnHandleCreated(EventArgs e)


    {


        base.OnHandleCreated(e);


        m_textBox = new ComboTextBox(this);


    }


 


    protected override void OnHandleDestroyed(EventArgs e)


    {


        m_textBox.Dispose();


        m_textBox = null;


        base.OnHandleDestroyed(e);


    }


 


    #region ComboTextBox class


 


    /// <summary>


    /// Internal class to perform subclassing on the textbox


    /// inside the Combo Box.


    /// </summary>


    private class ComboTextBox : NativeWindow, IDisposable


    {


        private IntPtr m_handle;


        private ComboBoxInternal m_owner;


 


        #region Unmanaged Code


 


        [DllImport(“user32”)]


        private static extern IntPtr GetWindow(IntPtr hWnd, int wCmd);


        private const int GW_CHILD = 5;


        private const int EM_CANUNDO = 0x00C6;


        private const int EM_UNDO = 0x00C7;


        private const int WM_UNDO = 0x0304;


 


        #endregion


 


        public ComboTextBox(ComboBoxInternal comboBox)


        {


            m_owner = comboBox;


            m_handle = GetWindow(comboBox.Handle, GW_CHILD);


            base.AssignHandle(m_handle);


        }


 


        protected override void WndProc(ref Message m)


        {


            switch (m.Msg)


            {


                case EM_CANUNDO:


                    m.Result = (IntPtr)(m_owner.CanUndo() ? 1 : 0);


                    break;


 


                case WM_UNDO:


                case EM_UNDO:


                    m_owner.Undo();


                    m.Result = (IntPtr)1;


                    break;


 


                default:


                    base.WndProc(ref m);


                    break;


            }


        }


 


        #region IDisposable Members


 


        public void Dispose()


        {


            base.ReleaseHandle();


            m_handle = IntPtr.Zero;


            m_owner = null;


        }


 


        #endregion


    }


    #endregion


}


 


There is one remaining question that I need to figure out. What message or mechanism is being used to determine if Undo should be enabled on the context menu. Spy doesn’t seem to show any message that I think could be it. And there is not a WM_CANUNDO. For now this remains a mystery. If I wanted to handle more complicated Undo scenarios like multiple levels of undo, this would be more important to me. If you know the answer, please share it 😉


 


 

Comments (5)

  1. miked says:

    There is the EM_CANUNDO message for textboxes. That should do it.

  2. Jason Prickett says:

    I am already responding to EM_CANUNDO (see the code). But Windows is not using the EM messages to enable/disable the menu items on the context menu. It is using some other mechanism. That’s the real question. What other mechinism is there?

  3. Jordan Shapiro says:

    I don’t know if this is the correct way to do it, but I managed to get Undo/Redo working in a ComboBox without subclassing.  (I’m a VB’er.)

    ====================================

    #Region " Undo/Redo PInvoke Support "

       <System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint:="SendMessage", ExactSpelling:=False, CharSet:=System.Runtime.InteropServices.CharSet.Auto, SetLastError:=True)> _

       Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal Msg As Integer, ByVal wParam As IntPtr, ByRef lParam As IntPtr) As IntPtr

       End Function

       ‘Private Const WM_CUT As Integer = &H300

       ‘Private Const WM_COPY As Integer = &H301

       ‘Private Const WM_PASTE As Integer = &H302

       ‘Private Const WM_CLEAR As Integer = &H303

       Private Const WM_UNDO As Integer = &H304

       Private Const EM_CANUNDO = &HC6

       <System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint:="GetWindow", ExactSpelling:=False, CharSet:=System.Runtime.InteropServices.CharSet.Auto, SetLastError:=True)> _

       Public Shared Function GetWindow(ByVal hWnd As IntPtr, ByVal wMsg As Integer) As IntPtr

       End Function

       Private Const GW_CHILD As Integer = 5

    #End Region

       Private Sub UndoToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles UndoToolStripMenuItem.Click

           Try

               If Me.ActiveControl IsNot Nothing Then

                   Select Case True

                       Case TypeOf Me.ActiveControl Is Form

                           Dim frm As Form = Me.ActiveMdiChild

                           Dim uc As UserControl = frm.ActiveControl

                           If uc IsNot Nothing Then

                               If TypeOf uc.ActiveControl Is TextBox Then

                                   Dim txt As TextBox = uc.ActiveControl

                                   SendMessage(txt.Handle, WM_UNDO, IntPtr.Zero, IntPtr.Zero)

                               ElseIf TypeOf uc.ActiveControl Is ComboBox Then

                                   Dim cbo As ComboBox = uc.ActiveControl

                                   Dim ptr As IntPtr = GetWindow(cbo.Handle, GW_CHILD)

                                   SendMessage(ptr, WM_UNDO, IntPtr.Zero, IntPtr.Zero)

                               End If

                           End If

                       Case TypeOf Me.ActiveControl Is TextBox

                           Dim txt As TextBox = Me.ActiveControl

                           SendMessage(txt.Handle, WM_UNDO, IntPtr.Zero, IntPtr.Zero)

                       Case TypeOf Me.ActiveControl Is ComboBox

                           Dim cbo As ComboBox = Me.ActiveControl

                           Dim ptr As IntPtr = GetWindow(cbo.Handle, GW_CHILD)

                           SendMessage(ptr, WM_UNDO, IntPtr.Zero, IntPtr.Zero)

                   End Select

               End If

           Catch ex As Exception

               MsgBox(ex.Message)

           End Try

       End Sub

    ==========================================

    1) The code for the Redo menu item is exactly the same as the Undo.

    2)  This code is for an MDI form that calls up MDIChildren forms that load UserControls (which contain the actual TextBox & ComboBox Controls.)

    3) My MDI form has a TextBox for performing an application search.  The ComboBox code for the MDI for never gets called but may be in the future — that’s why it’s there.

    Hope this helps someone else.  Cheers & happy programming!

  4. Jason Prickett says:

    It appears that your Undo/Redo is triggered by a button. The purpose of the code above is to get Ctrl+Z to work properly. Does Ctrl+Z work properly for your when AutoComplete is turned on?

  5. jcurl@arcor.de says:

    I come across your site with a similar problem. It appears it isn’t possible to capture the WM_CANUNDO message and one would have to implement their own Context Menu on WM_CONTEXTMENU.

    Please have a look at the forum where I posted something similar.

    http://social.msdn.microsoft.com/Forums/en-US/winforms/thread/78b80213-ab21-44e2-879e-c8633f6a6c16/#f4145e8a-6811-47d6-bafd-dd06c3f2cec2