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
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;
}