Home > Back-end >  Get correct bound of combobox item when owner drawing
Get correct bound of combobox item when owner drawing

Time:06-29

I used:

myComboBox.DrawMode = DrawMode.OwnerDrawFixed;

Then I implemented the DrawItem event like so:

private void myComboBox_DrawItem(object sender, DrawItemEventArgs e)
{
    if (e.Index < 0)
        return;

    string text = myComboBox.GetItemText(myComboBox.Items[e.Index]);
    e.DrawBackground();
    using var brush = new SolidBrush(e.ForeColor);
    e.Graphics.DrawString(text, e.Font!, brush, e.Bounds);
    if (myComboBox.DroppedDown && (e.State & DrawItemState.Selected) == DrawItemState.Selected)
    {
        var item = (IndexedItem)myComboBox.Items[e.Index]; // IndexedItem is my own class to hold the index and name of a combobox item

        if (!string.IsNullOrWhiteSpace(item.Name))
            myToolTip.Show(item.Name, myComboBox, e.Bounds.Right, e.Bounds.Bottom);
    }
    e.DrawFocusRectangle();
}

The purpose is to show a tooltip for each item when I hover it. The above code works fine but sometimes the dropdown is shown above instead of below due to available space.

The problem now is, that DrawBackground etc use the adjusted positions but e.Bounds still contains the area below the control. So the dropdown is shown above but the tooltip below the combobox. I guess DrawBackground modifies the bounds internally but doesn't write the adjusted value back to e.Bounds. Now the question is, how can I determine the correct bounds of the item in that case?

CodePudding user response:

Ok I managed to find the correct bounds via interops like this:

Rectangle GetItemBounds(int index)
{
    const int SB_VERT = 0x1;
    const int SIF_RANGE = 0x1;
    const int SIF_POS = 0x4;
    const uint GETCOMBOBOXINFO = 0x0164;

    var info = new COMBOBOXINFO();
    info.cbSize = Marshal.SizeOf(info);
    SendMessageW(
        myComboBox.Handle,
        GETCOMBOBOXINFO,
        IntPtr.Zero,
        ref info);
    GetWindowRect(info.hwndList, out RECT rc);
    var dropdownArea = new Rectangle(myComboBox.PointToClient(rc.Location), rc.Size);
    int yDiff = index * myComboBox.ItemHeight;
    var scrollInfo = new SCROLLINFO();
    scrollInfo.cbSize = (uint)Marshal.SizeOf(scrollInfo);
    scrollInfo.fMask = SIF_RANGE | SIF_POS;
    GetScrollInfo(info.hwndList, SB_VERT, ref scrollInfo);
    int scrollY = scrollInfo.nPos * myComboBox.ItemHeight;
    return new Rectangle(dropdownArea.X, dropdownArea.Y - scrollY   yDiff, rc.Width, myComboBox.ItemHeight);
}

And then use it like this:

var realBounds = GetItemBounds(e.Index);

I am not sure if this is the best approach but it even works with scrolling inside the dropdown now.

The interops look like this:

[StructLayout(LayoutKind.Sequential)]
struct COMBOBOXINFO
{
    public Int32 cbSize;
    public RECT rcItem, rcButton;
    public int buttonState;
    public IntPtr hwndCombo, hwndEdit, hwndList;
}

[Serializable, StructLayout(LayoutKind.Sequential)]
struct SCROLLINFO
{
    public uint cbSize;
    public uint fMask;
    public int nMin;
    public int nMax;
    public uint nPage;
    public int nPos;
    public int nTrackPos;
}

[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO lpsi);

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int Left, Top, Right, Bottom;

    public RECT(int left, int top, int right, int bottom)
    {
        Left = left;
        Top = top;
        Right = right;
        Bottom = bottom;
    }

    public RECT(System.Drawing.Rectangle r) : this(r.Left, r.Top, r.Right, r.Bottom) { }

    public int X
    {
        get { return Left; }
        set { Right -= (Left - value); Left = value; }
    }

    public int Y
    {
        get { return Top; }
        set { Bottom -= (Top - value); Top = value; }
    }

    public int Height
    {
        get { return Bottom - Top; }
        set { Bottom = value   Top; }
    }

    public int Width
    {
        get { return Right - Left; }
        set { Right = value   Left; }
    }

    public System.Drawing.Point Location
    {
        get { return new System.Drawing.Point(Left, Top); }
        set { X = value.X; Y = value.Y; }
    }

    public System.Drawing.Size Size
    {
        get { return new System.Drawing.Size(Width, Height); }
        set { Width = value.Width; Height = value.Height; }
    }

    public static implicit operator System.Drawing.Rectangle(RECT r)
    {
        return new System.Drawing.Rectangle(r.Left, r.Top, r.Width, r.Height);
    }

    public static implicit operator RECT(System.Drawing.Rectangle r)
    {
        return new RECT(r);
    }

    public static bool operator ==(RECT r1, RECT r2)
    {
        return r1.Equals(r2);
    }

    public static bool operator !=(RECT r1, RECT r2)
    {
        return !r1.Equals(r2);
    }

    public bool Equals(RECT r)
    {
        return r.Left == Left && r.Top == Top && r.Right == Right && r.Bottom == Bottom;
    }

    public override bool Equals(object? obj)
    {
        if (obj is RECT)
            return Equals((RECT)obj);
        else if (obj is System.Drawing.Rectangle)
            return Equals(new RECT((System.Drawing.Rectangle)obj));
        return false;
    }

    public override int GetHashCode()
    {
        return ((System.Drawing.Rectangle)this).GetHashCode();
    }

    public override string ToString()
    {
        return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom);
    }
}

[DllImport("user32")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT rc);

[DllImport("user32", ExactSpelling = true)]
public static extern IntPtr SendMessageW(
    IntPtr hWnd,
    uint Msg,
    IntPtr wParam = default,
    IntPtr lParam = default);

public unsafe static IntPtr SendMessageW<T>(
    IntPtr hWnd,
    uint Msg,
    IntPtr wParam,
    ref T lParam) where T : unmanaged
{
    fixed (void* l = &lParam)
    {
        return SendMessageW(hWnd, Msg, wParam, (IntPtr)l);
    }
}

I created an extension:

[EditorBrowsable(EditorBrowsableState.Advanced)]
    public static class ComboBoxExtensions
    {
        public static Rectangle GetItemBounds(this ComboBox comboBox, int index)
        {
            const int SB_VERT = 0x1;
            const int SIF_RANGE = 0x1;
            const int SIF_POS = 0x4;
            const uint GETCOMBOBOXINFO = 0x0164;

            var info = new Interop.COMBOBOXINFO();
            info.cbSize = Marshal.SizeOf(info);
            Interop.SendMessageW(
                comboBox.Handle,
                GETCOMBOBOXINFO,
                IntPtr.Zero,
                ref info);
            Interop.GetWindowRect(info.hwndList, out Interop.RECT rc);
            var dropdownArea = new Rectangle(comboBox.PointToClient(rc.Location), rc.Size);
            int yDiff = index * comboBox.ItemHeight;
            var scrollInfo = new Interop.SCROLLINFO();
            scrollInfo.cbSize = (uint)Marshal.SizeOf(scrollInfo);
            scrollInfo.fMask = SIF_RANGE | SIF_POS;
            Interop.GetScrollInfo(info.hwndList, SB_VERT, ref scrollInfo);
            int scrollY = scrollInfo.nPos * comboBox.ItemHeight;
            return new Rectangle(dropdownArea.X, dropdownArea.Y - scrollY   yDiff, rc.Width, comboBox.ItemHeight);
        }
    }

CodePudding user response:

I realize you have already answered your own question. This is just plan B.

Your post mentions that the purpose is to show a tooltip for each item when I hover it so this answer focuses on that aspect. It doesn't make use of the item bounds info but that would be available in the DrawItem handler if you still need it.

This approach is loosely based on Combo box with tool tips graphic

This sample code for MainForm starts a timer when the ComboBox drops and ends it when it closes. It polls the SelectedIndex property on each tick.

public MainForm()
{
    InitializeComponent();            
    // Works with or without owner draw.
    // This line may be commented out
    comboBox.DrawMode = DrawMode.OwnerDrawFixed;
    // Add items
    foreach (var item in Enum.GetValues(typeof(ComboBoxItems)))
        comboBox.Items.Add(item);
    // Start timer on drop down
    comboBox.DropDown  = (sender, e) => _toolTipTimer.Enabled = true;
    // End timer on closed
    comboBox.DropDownClosed  = (sender, e) =>
    {
        _toolTipTimer.Enabled = false; 
        _lastToolTipIndex = -1;
        _toolTip.Hide(comboBox);
    };
    // Poll index while open
    _toolTipTimer.Tick  = (sender, e) =>
    {
        if(comboBox.SelectedIndex != _lastToolTipIndex)
        {
            _lastToolTipIndex = comboBox.SelectedIndex;
            showToolTip();
        }
    };

If there is a new index, the local method showToolTip is invoked to show the correct text.

    void showToolTip()
    {
        if (_lastToolTipIndex != -1)
        {
            // Get the item
            var item = (ComboBoxItems)comboBox.Items[_lastToolTipIndex];
            // Get the tip
            var tt = TipAttribute.FromMember(item);
            // Get the rel pos
            var mousePosition = PointToClient(MousePosition);
            var rel = new Point(
                (mousePosition.X - comboBox.Location.X) - 10,
                (mousePosition.Y - comboBox.Location.Y) - 30);
            // Show the tip
            _toolTip.Show(tt, comboBox, rel);
        }
    }
}
private ToolTip _toolTip = new ToolTip();
private readonly Timer _toolTipTimer = new Timer() { Interval = 100 };
private int _lastToolTipIndex = -1;

The list items are represented by this enum:

enum ComboBoxItems
{
    [Tip("...a day keeps the doctor away")]
    Apple,
    [Tip("...you glad I didn't say 'banana?'")]
    Orange,
    [Tip("...job on the Tool Tips!")]
    Grape,
}

Where TipAttribute is defined as:

internal class TipAttribute : Attribute
{
    public TipAttribute(string tip) => Tip = tip;
    public string Tip { get; }
    public static string FromMember(Enum value) =>
        ((TipAttribute)value
            .GetType()
            .GetMember($"{value}")
            .Single()
            .GetCustomAttribute(typeof(TipAttribute))).Tip;
}
  • Related