다음을 통해 공유


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

  • Anonymous
    April 24, 2006
    There is the EM_CANUNDO message for textboxes. That should do it.
  • Anonymous
    April 25, 2006
    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?
  • Anonymous
    May 08, 2007
    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!