27

I have a listbox that is databound to a Collection of objects. The listbox is configured to display an identifier property of each object. I would like to show a tooltip with information specific to the item within the listbox that is being hovered over rather than one tooltip for the listbox as a whole.

I am working within WinForms and thanks to some helpful blog posts put together a pretty nice solution, which I wanted to share.

I'd be interested in seeing if there's any other elegant solutions to this problem, or how this may be done in WPF.

efotinis
  • 14,565
  • 6
  • 31
  • 36
Michael Lang
  • 2,157
  • 3
  • 22
  • 29

7 Answers7

20

There are two main sub-problems one must solve in order to solve this problem:

  1. Determine which item is being hovered over
  2. Get the MouseHover event to fire when the user has hovered over one item, then moved the cursor within the listbox and hovered over another item.

The first problem is rather simple to solve. By calling a method like the following within your handler for MouseHover, you can determine which item is being hovered over:

private ITypeOfObjectsBoundToListBox DetermineHoveredItem()
{
    Point screenPosition = ListBox.MousePosition;
    Point listBoxClientAreaPosition = listBox.PointToClient(screenPosition);

    int hoveredIndex = listBox.IndexFromPoint(listBoxClientAreaPosition);
    if (hoveredIndex != -1)
    {
        return listBox.Items[hoveredIndex] as ITypeOfObjectsBoundToListBox;
    }
    else
    {
        return null;
    }
}

Then use the returned value to set the tool-tip as needed.

The second problem is that normally the MouseHover event isn't fired again until the cursor has left the client area of the control and then come back.

You can get around this by wrapping the TrackMouseEvent Win32API call.
In the following code, the ResetMouseHover method wraps the API call to get the desired effect: reset the underlying timer that controls when the hover event is fired.

public static class MouseInput
{
    // TME_HOVER
    // The caller wants hover notification. Notification is delivered as a 
    // WM_MOUSEHOVER message.  If the caller requests hover tracking while 
    // hover tracking is already active, the hover timer will be reset.

    private const int TME_HOVER = 0x1;

    private struct TRACKMOUSEEVENT
    {
        // Size of the structure - calculated in the constructor
        public int cbSize;

        // value that we'll set to specify we want to start over Mouse Hover and get
        // notification when the hover has happened
        public int dwFlags;

        // Handle to what's interested in the event
        public IntPtr hwndTrack;

        // How long it takes for a hover to occur
        public int dwHoverTime;

        // Setting things up specifically for a simple reset
        public TRACKMOUSEEVENT(IntPtr hWnd)
        {
            this.cbSize = Marshal.SizeOf(typeof(TRACKMOUSEEVENT));
            this.hwndTrack = hWnd;
            this.dwHoverTime = SystemInformation.MouseHoverTime;
            this.dwFlags = TME_HOVER;
        }
    }

    // Declaration of the Win32API function
    [DllImport("user32")]
    private static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack);

    public static void ResetMouseHover(IntPtr windowTrackingMouseHandle)
    {
        // Set up the parameter collection for the API call so that the appropriate
        // control fires the event
        TRACKMOUSEEVENT parameterBag = new TRACKMOUSEEVENT(windowTrackingMouseHandle);

        // The actual API call
        TrackMouseEvent(ref parameterBag);
    }
}

With the wrapper in place, you can simply call ResetMouseHover(listBox.Handle) at the end of your MouseHover handler and the hover event will fire again even when the cursor stays within the control's bounds.

I'm sure this approach, sticking all the code in the MouseHover handler must result in more MouseHover events firing than are really necessary, but it'll get the job done. Any improvements are more than welcome.

Jimi
  • 29,621
  • 8
  • 43
  • 61
Michael Lang
  • 2,157
  • 3
  • 22
  • 29
  • Thanks for the revision suggestion, @reformed. The reviewers rejected it, but I made adjustments to the language you found confusing. – Michael Lang Aug 06 '14 at 14:09
17

Using the MouseMove event, you can keep track of the index of the item that the mouse is over and store this in a variable that keeps its value between MouseMoves. Every time MouseMove is triggered, it checks to see if the index has changed. If so, it disables the tooltip, changes the tooltip text for this control, then re-activates it.

Below is an example where a single property of a Car class is shown in a ListBox, but then full information is shown when hovering over any one row. To make this example work, all you need is a ListBox called lstCars with a MouseMove event and a ToolTip text component called tt1 on your WinForm.

Definition of the car class:

    class Car
    {
        // Main properties:
        public string Model { get; set; }
        public string Make { get; set; }
        public int InsuranceGroup { get; set; }
        public string OwnerName { get; set; }
        // Read only property combining all the other informaiton:
        public string Info { get { return string.Format("{0} {1}\nOwner: {2}\nInsurance group: {3}", Make, Model, OwnerName, InsuranceGroup); } }
    }

Form load event:

    private void Form1_Load(object sender, System.EventArgs e)
    {
        // Set up a list of cars:
        List<Car> allCars = new List<Car>();
        allCars.Add(new Car { Make = "Toyota", Model = "Yaris", InsuranceGroup = 6, OwnerName = "Joe Bloggs" });
        allCars.Add(new Car { Make = "Mercedes", Model = "AMG", InsuranceGroup = 50, OwnerName = "Mr Rich" });
        allCars.Add(new Car { Make = "Ford", Model = "Escort", InsuranceGroup = 10, OwnerName = "Fred Normal" });

        // Attach the list of cars to the ListBox:
        lstCars.DataSource = allCars;
        lstCars.DisplayMember = "Model";
    }

The tooltip code (including creating the class level variable called hoveredIndex):

        // Class variable to keep track of which row is currently selected:
        int hoveredIndex = -1;

        private void lstCars_MouseMove(object sender, MouseEventArgs e)
        {
            // See which row is currently under the mouse:
            int newHoveredIndex = lstCars.IndexFromPoint(e.Location);

            // If the row has changed since last moving the mouse:
            if (hoveredIndex != newHoveredIndex)
            {
                // Change the variable for the next time we move the mouse:
                hoveredIndex = newHoveredIndex;

                // If over a row showing data (rather than blank space):
                if (hoveredIndex > -1)
                {
                    //Set tooltip text for the row now under the mouse:
                    tt1.Active = false;
                    tt1.SetToolTip(lstCars, ((Car)lstCars.Items[hoveredIndex]).Info);
                    tt1.Active = true;
                }
            }
        }
rory.ap
  • 34,009
  • 10
  • 83
  • 174
Michael
  • 412
  • 3
  • 12
  • What does the ".InsuranceGroup" in your code ? I can't find anything about that on the internet, and my Visual studio don't know this word either. – theoretisch Aug 03 '16 at 16:49
  • 1
    Insurance group is just a property of the car class I wanted to display. In the UK, the insurance group is an indication of whether the cost of insurance will be cheap or expensive. The cheaper the car to insure, the lower the number, the more expensive to insure the bigger the number. I have expanded my explanation above to make this into a full example. – Michael Aug 06 '16 at 11:29
  • it worked perfect for me, but maybe is missing "ToolTip tt1 = new ToolTip();" as class field in the code? I had to adding it. – Falco Jun 19 '17 at 14:09
  • 1
    Good point Falco. I created tt1 by adding a ToolTip from the toolbox when using the designer in Visual Studio. Creating it in your own code will also work well. – Michael Jun 20 '17 at 12:56
7

You can use this simple code that uses the onMouseMove event of ListBox in WinForms:

private void ListBoxOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
{
        var listbox = sender as ListBox;
        if (listbox == null) return;

        // set tool tip for listbox
        var strTip = string.Empty;
        var index = listbox.IndexFromPoint(mouseEventArgs.Location);

        if ((index >= 0) && (index < listbox.Items.Count))
            strTip = listbox.Items[index].ToString();

        if (_toolTip.GetToolTip(listbox) != strTip)
        {
            _toolTip.SetToolTip(listbox, strTip);
        }
}

Of course you will have to init the ToolTip object in the constructor or some init function:

_toolTip = new ToolTip
{
            AutoPopDelay = 5000,
            InitialDelay = 1000,
            ReshowDelay = 500,
            ShowAlways = true
};

Enjoy!

Shalom
  • 71
  • 1
  • 2
  • Perfect solution for me, I've changed your code a bit for reset the pop delay, with your current code once the tooltip is shown it was not getting into cooldown when I switch items. So I change the re set part, before I set tooltiptext with setToolTip method I disposed the old one and re new it – Shino Lex Sep 22 '20 at 14:59
7

I think the best option, since your databinding your listbox to objects, would be to use a datatemplate. So you could do something like this:

<ListBox Width="400" Margin="10" 
         ItemsSource="{Binding Source={StaticResource myTodoList}}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Path=TaskName}" 
                       ToolTipService.ToolTip="{Binding Path=TaskName}"/>
        </DataTemplate>
     </ListBox.ItemTemplate>
</ListBox>

Of course you'd replace the ItemsSource binding with whatever your binding source is, and the binding Path parts with whatever public property of the objects in the list you actually want to display. More details available on msdn

stukselbax
  • 5,855
  • 3
  • 32
  • 54
0

Using onmouseover you can iterate through each item of the list and can show the ToolTip

onmouseover="doTooltipProd(event,'');

function doTooltipProd(e,tipObj)
{

    Tooltip.init();
      if ( typeof Tooltip == "undefined" || !Tooltip.ready ) {
      return;
      }
      mCounter = 1;
   for (m=1;m<=document.getElementById('lobProductId').length;m++) {

    var mCurrent = document.getElementById('lobProductId').options[m];
        if(mCurrent != null && mCurrent != "null") {
            if (mCurrent.selected) {
                mText = mCurrent.text;
                Tooltip.show(e, mText);
                }
        }   
    }   
}
SanyTiger
  • 666
  • 1
  • 8
  • 23
0

Here is a Style that creates a group of RadioButtons by using a ListBox. All is bound for MVVM-ing. MyClass contains two String properties: MyName and MyToolTip. This will display the list of RadioButtons including proper ToolTip-ing. Of interest to this thread is the Setter for ToolTip near bottom making this an all Xaml solution.

Example usage:

ListBox Style="{StaticResource radioListBox}" ItemsSource="{Binding MyClass}" SelectedValue="{Binding SelectedMyClass}"/>

Style:

    <Style x:Key="radioListBox" TargetType="ListBox" BasedOn="{StaticResource {x:Type ListBox}}">
    <Setter Property="BorderThickness" Value="0" />
    <Setter Property="Margin" Value="5" />
    <Setter Property="Background" Value="{x:Null}" />
    <Setter Property="ItemContainerStyle">
        <Setter.Value>
            <Style TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="ListBoxItem">
                            <Grid Background="Transparent">
                                <RadioButton Focusable="False" IsHitTestVisible="False" IsChecked="{TemplateBinding IsSelected}" Content="{Binding MyName}"/>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
                <Setter Property="ToolTip" Value="{Binding MyToolTip}" />
            </Style>
        </Setter.Value>
    </Setter>
</Style>
BSalita
  • 8,420
  • 10
  • 51
  • 68
  • 2
    The question was aimed at WinForms, not WPF. – Thomas Freudenberg Mar 20 '14 at 14:11
  • 4
    When I posted this question, StackOverflow was describing itself as a wiki organized around questions. I actually have in my question "I'd be interested in seeing if there's any other elegant solutions to this problem, or how this may be done in WPF." I can't evaluate the correctness of this WPF solution, but no need to downvote simply because its not WinForms – Michael Lang Apr 01 '14 at 18:40
-1

Using title attribute, we can set tool tip for each list items in a list box.

Loop this for all the items in a list box.

ListItem li = new ListItem("text","key");
li.Attributes.Add("title","tool tip text");

Hope this helps.

Agustin Meriles
  • 4,866
  • 3
  • 29
  • 44
  • This shows a tooltip only when the select Listbox is opened. Ironically, that works fine for me and I have used this solution, so thanks very much. – Steve Hibbert May 06 '15 at 15:28