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 ==========================================
- The code for the Redo menu item is exactly the same as the Undo.
- This code is for an MDI form that calls up MDIChildren forms that load UserControls (which contain the actual TextBox & ComboBox Controls.)
- 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!
Anonymous
May 09, 2007
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?Anonymous
May 03, 2010
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