TFili
TFili

Reputation: 21

Capturing ComboBox item click

I have a WinForms ComboBox that remembers what items have previously been entered into it. I want a way to remove previous entries. I override the DrawItem event of the ComboBox to render the text along with an X icon. The X icon is just a square image that I scale to the height of the item. The code is pretty straight forward.

// Enable the owner draw on the ComboBox.
ServerComboBox.DrawMode = DrawMode.OwnerDrawFixed;
// Handle the DrawItem event to draw the items.
ServerComboBox.DrawItem += delegate(object cmb, DrawItemEventArgs args)
{
    // Draw the default background
    args.DrawBackground();

    String url = (String)ServerComboBox.Items[args.Index];

    // Get the bounds for the first column
    Rectangle r1 = args.Bounds;
    r1.Width -= r1.Height;

    // Draw the text
    using (SolidBrush sb = new SolidBrush(args.ForeColor))
    {
        args.Graphics.DrawString(url, args.Font, sb, r1);
    }

    // Draw the X icon
    Rectangle r2 = new Rectangle(r1.Width+1, r1.Y + 1, r1.Height - 2, r1.Height - 2);
    args.Graphics.DrawImage(Project.Test.Properties.Resources.CloseIcon, r2);
};

Now my issue is how to capture if the X was clicked. My first thought was to capture the MouseDown event for the ComboBox and check if the DroppedDown property was true, but that event only gets fired when you click the unexpanded ComboBox. How can I capture events from the DropDown part of the ComboBox. Once I get that, I don't think it'll be much of an issue figuring out if the X was clicked or now.

Upvotes: 2

Views: 3454

Answers (4)

ehosca
ehosca

Reputation: 955

COMBOBOXINFO will give you that information that you can use to subclass the dropdown list window. See this for an example:

https://github.com/ehosca/MRUComboBox

Upvotes: 0

King King
King King

Reputation: 63387

In fact your problem is just a trivial problem which Win32 can solve:

public class Form1 : Form {
  [DllImport("user32")]
  private static extern int GetComboBoxInfo(IntPtr hwnd, out COMBOBOXINFO comboInfo);
  struct RECT {
    public int left, top, right, bottom;
  }
  struct COMBOBOXINFO {
        public int cbSize;
        public RECT rcItem;
        public RECT rcButton;
        public int stateButton;
        public IntPtr hwndCombo;
        public IntPtr hwndItem;
        public IntPtr hwndList;
  }
  public Form1(){
    InitializeComponent();  
    comboBox1.HandleCreated += (s, e) => {
       COMBOBOXINFO combo = new COMBOBOXINFO();
       combo.cbSize = Marshal.SizeOf(combo);
       GetComboBoxInfo(comboBox1.Handle, out combo);
       hwnd = combo.hwndList;
       init = false;
    };
  }
  bool init;
  IntPtr hwnd;
  NativeCombo nativeCombo = new NativeCombo();
  //This is to store the Rectangle info of your Icons
  //Key:  the Item index
  //Value: the Rectangle of the Icon of the item (not the Rectangle of the item)
  Dictionary<int, Rectangle> dict = new Dictionary<int, Rectangle>();
  public class NativeCombo : NativeWindow {
        //this is custom MouseDown event to hook into later
        public event MouseEventHandler MouseDown;
        protected override void WndProc(ref Message m)
        {                
            if (m.Msg == 0x201)//WM_LBUTTONDOWN = 0x201
            {                    
                int x = m.LParam.ToInt32() & 0x00ff;
                int y = m.LParam.ToInt32() >> 16;
                if (MouseDown != null) MouseDown(null, new MouseEventArgs(MouseButtons.Left, 1, x, y, 0));                                                                  
            }
            base.WndProc(ref m);
        }
  }
  //DropDown event handler of your comboBox1
  private void comboBox1_DropDown(object sender, EventArgs e) {
        if (!init) {
            //Register the MouseDown event handler <--- THIS is WHAT you want.
            nativeCombo.MouseDown += comboListMouseDown;
            nativeCombo.AssignHandle(hwnd);                
            init = true;
        }
  }
  //This is the MouseDown event handler to handle the clicked icon
  private void comboListMouseDown(object sender, MouseEventArgs e){
    foreach (var kv in dict) {
      if (kv.Value.Contains(e.Location)) {
         //Show the item index whose the corresponding icon was held down
         MessageBox.Show(kv.Key.ToString());
         return;
      }
    }
  }
  //DrawItem event handler
  private void comboBox1_DrawItem(object sender, DrawItemEventArgs e) {
     //We have to save the Rectangle info of the item icons
     Rectangle rect = new Rectangle(0, e.Bounds.Top, e.Bounds.Height, e.Bounds.Height);
     dict[e.Index] = rect;
     //Draw the icon
     //e.Graphics.DrawImage(yourImage, rect);   
  }
}

A little about what happens

A ComboBox has an attached Drop down list, this can be accessed via its Handle. We use GetComboBoxInfo win32 api function to retrieve some info of a ComboBox, this info is saved in a structure called COMBOBOXINFO. We can get the Handle of the Drop down list via the member hwndList in this structure.

After having access to the hwndList. We can hook into its message loop using a custom NativeWindow class (NativeCombo in the example). This allows us to interfere the message loop of the drop down list easily. Then we can catch the WM_LBUTTONDOWN message to handle the MouseDown event. Of course, it's not a full MouseDown event, but it's just a demo and in this case it's enough to solve the problem. The WM_LBUTTONDOWN is sent with some info on the clicked point saved in LParam. The clicked point is calculated in the coordinates of the Client area of the drop down list (not in screen coordinates). We should notice that the DrawItem event handler also has e.Bounds argument which is also calculated in the Client area coordinates (not screen coordinates). Hence they are in the same coordinates system. We use the Rectangle.Contains method to know if the clicked point is contained in the Bounds of some icon. We stores all the icon Bounds in a Dictionary. So the corresponding Key (stores the Item index) of the Rectangle which contains the clicked point let us know the corresponding Item index, then we can process further.

Upvotes: 2

Sriram Sakthivel
Sriram Sakthivel

Reputation: 73502

When the user selects or uses mouse wheel SelectionIndexChanged event will fire. If you don't want to react for mouse wheel and if you need to respond only for clicking and selecting then you may consider using SelectionChangeCommited event.

Then you can read SelectedIndex or SelectedItem property to get the selected item.

Edit: Am sorry it seems I complelely misunderstand the question. I think you need to capture mouse down of ListBox inside the Combo and manually check it using Rectange.Contains. Will get back with more details.

Upvotes: 1

Eric
Eric

Reputation: 19873

Once the user has clicked on an item in the combo box, it becomes the selected item. You can retrieve this property from the combo box object.

Upvotes: 0

Related Questions