Ok, this is a strange one (or I'm doing something stupidly).
I have a WPF combobox which is populated with a string array on form load (app start).
This part works fine. The sticky bit is when I try to alter any of the information within said combobox. The debug says that it is changing but nothing is being shown visually.
// Populate the combobox:
private void ComboBlocks()
{
comboBox1.Items.Clear();
string[,] _tmp = _kits.BlockIDNames;
string[] _tmp1 = new string[_tmp.GetLength(0)];
for (int i = 0; i < _tmp.GetLength(0); i++)
{
_tmp1[i] = _tmp[i, 0] + " - " + _tmp[i, 1];
}
foreach (string s in _tmp1)
{
string[] _tmpS1 = s.Split(new char[] { '-' });
int _tmpS2 = Convert.ToInt32(_tmpS1[0].Trim());
bool _banneditem = _cbi.BannedItemExists(_tmpS2);
if (_banneditem == true)
AddComboItem(s, true);
else
AddComboItem(s);
}
if (comboBox1.Items.Count > 0)
comboBox1.SelectedIndex = 0;
}
// Add item to combobox:
private void AddComboItem(string _text,bool _redtext = false)
{
Grid grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
TextBlock text = new TextBlock();
text.Text = _text;
if (_redtext == true)
text.Foreground = Brushes.Red;
else
text.Foreground = Brushes.Black;
grid.Children.Add(text);
Grid.SetColumn(text, 0);
ComboBoxItem comboBoxItem = new ComboBoxItem();
comboBoxItem.Content = grid;
comboBoxItem.Tag = _text;
comboBox1.Items.Add(comboBoxItem);
}
Also, I am fairly new to C# so if there is anything that I'm doing wrong/inefficiently, please point it out.
Many thanks.
EDIT: Iterating through each one and changing the text value would probably be just as much work as it gets its information from another array. It would have to check for each item in the array and if it exists, colour it red, if it doesn't, colour it black.
I can't see what, specifically, you're doing wrong here. You can probably get that to work if you hack around on it enough.
But it's not the right approach - at least, not as far as writing maintainable, reliable WPF code is concerned. You should be using data binding to populate this control. You will find, after very little experience with doing this, that it makes for much faster, easier development than the code-WPF-as-if-it-were-WinForms approach you're taking.
Here's how:
Create a class for holding your data items, called, e.g. Block. Have it expose string Text and bool IsBanned properties.
Create an ObservableCollection<Block> and populate it with new Block objects created from your data source.
Expose the collection to binding. There are a lot of ways to do this; the example below assumes you've added it to the window's resource dictionary with a key of Blocks. You could also implement a Blocks property in your window, or (what I do pretty much any time I create a window or user control in WPF) create a class that exposes a Blocks property and set the window's DataContext to an instance of that class.
Now put this in your window's XAML:
<ComboBox ItemsSource="{Binding {DynamicResource Blocks}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Text}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Black"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsBanned}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
You don't actually need that Grid in there (nor do you need to set Grid.Column, since it defaults to 0); I just put that in there so that the example would more accurately replicate what's in your code. Also, I'm not setting the Tag property on the ComboBoxItem, but that's because the ComboBox's SelectedItem contains the actual Block instance for that item, which obviates the need for the Tag property.
Since you're using an ObservableCollection, any changes that you make to the collection (i.e. adding/removing/reordering its items) will automatically be reflected in what's on the screen; binding will take care of that for you.
If the items' Text or IsBanned properties will be changing after you populate the collection, and you need the ComboBox to reflect these changes, you'll need to implement INotifyPropertyChanged in the Block class and have it raise PropertyChanged in the setters of those properties.
Related
I have A WPF Datagrid that has a Collection View Source with 3 levels of grouping on it.
I have styled the datagrid to use 3 expanders such that it looks like this:
Level 1 Expander
<content>
Level 2 Expander
<content>
Level 3 Expander
<content>
Level 2 and Level 1 are just title of the groups
I have a second control that allows the user to show and hide level 3 items which works by binding the Level 3 expander to a Boolean "IsVisible" property in the object behind.
<!-- Style for groups under the top level. this is the style for how a sample is displayed -->
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<!-- The parent control that determines whether or not an item needs to be displayed. This holds all of the sub controls displayed for a sample -->
<Expander Margin="2"
Background="{Binding Path=Name,
Converter={StaticResource SampleTypeToColourConverter}}"
IsExpanded="True"
Visibility="{Binding Path=Items[0].IsVisibleInMainScreen,
Converter={StaticResource BoolToVisibilityConverter}}">
This approach works fantasically well.
HOWEVER
If the user deselects all items in a level 3 expander, the Level 2 expander header still displays meaning that valuable real estate is used up showing the header of a group with no visible data.
What I would like is a way to bind the visibility of the level 2 expander to its child controls and say "If all children are visible then show the expander, otherwise collapse it"
Is this possible?
I found a rather simple and clean way, yet not perfect, to achieve your goal. This should do the trick if hou don't have too much groups.
I've just added this trigger to the GroupItem ControlTemplate :
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=IP, Path=ActualHeight}" Value="0">
<Setter Property="Visibility" Value="Hidden"/>
<Setter Property="Height" Value="1"/>
</DataTrigger>
</ControlTemplate.Triggers>
When the ItemsPresenter (IP) ActualSize drops to zero, it Will almost collapse the header.
Why almost ?
When the control gets initialized and before the binding occurs, the ItemPresenter ActualHeight is 0 and when Visibility is set to Collapsed, the ItemPresenter doesn't get rendered at all.
Using Visibility.Hidden allows the ItemsPresenter to go to the render phase and be mesured.
I succedeed to drop Height to .4 px but I suspect this to be device dependant.
Assuming that you are using an MVVM sort of style, you could bind instead to a property of your group object that returns false if all of the children are invisible:
public bool AreChildrenVisible { get { return _children.Any(x=>x.IsVisibleInMainScreen); } }
Alternatively, pass the collection of Items through a Converter class to return Visibility depending on the aggregate status of all the subItems in the group.
This isn't a direct answer as you would have to implement it specifically for your needs but previously I have used a an override of the Grid Control to create dynamic grid allocation of members, if there are no visible members it then hides the parent group box.
public class DynamicLayoutGrid : Grid
{
protected override void OnInitialized(EventArgs e)
{
//Hook up the loaded event (this is used because it fires after the visibility binding has occurred)
this.Loaded += new RoutedEventHandler(DynamicLayoutGrid_Loaded);
base.OnInitialized(e);
}
void DynamicLayoutGrid_Loaded(object sender, RoutedEventArgs e)
{
int numberOfColumns = ColumnDefinitions.Count;
int columnSpan = 0;
int rowNum = 0;
int columnNum = 0;
int visibleCount = 0;
foreach (UIElement child in Children)
{
//We only want to layout visible items in the grid
if (child.Visibility != Visibility.Visible)
{
continue;
}
else
{
visibleCount++;
}
//Get the column span of the element if it is not in column 0 as we might need to take this into account
columnSpan = Grid.GetColumnSpan(child);
//set the Grid row of the element
Grid.SetRow(child, rowNum);
//set the grid column of the element (and shift it along if the previous element on this row had a rowspan greater than 0
Grid.SetColumn(child, columnNum);
//If there isn't any columnspan then just move to the next column normally
if (columnSpan == 0)
{
columnSpan = 1;
}
//Move to the next available column
columnNum += columnSpan;
//Move to the next row and start the columns again
if (columnNum >= numberOfColumns)
{
rowNum++;
columnNum = 0;
}
}
if (visibleCount == 0)
{
if (this.Parent.GetType() == typeof(GroupBox))
{
(this.Parent as GroupBox).Visibility = Visibility.Collapsed;
}
}
}
}
Use IMultiValueConverter implementation to convert items to visibility.
If all items IsVisibleInMainScreen property return true the converter will return visible else hidden.
Use the converter in the same place U used to convert the first item in original example
I've got a ListBox that is displaying a dynamic number of TextBoxes. The user will enter text into these boxes. When the Submit button is clicked, I need to be able to access the text the user has input, should be at ListBox.Items, like so:
//Called on Submit button click
private void SaveAndSubmit(object sender, ExecutedRoutedEventArgs e)
{
var bounds = MyListBox.Items;
}
But MyListBox.Items doesn't change after I initially set the ItemsSource, here:
//Field declaration
//Bounds is containing a group of strings that represent the boundaries
//for a contour plot. The min/max values are stored at the front and back
//of the group. However, there can be any number of dividers in between.
public ObservableCollection<string> Bounds { get; set; }
...
//Initialize Bounds in the constructor
//Called when the selected item for DVList (an unrelated ListBox) is changed
private void DVSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedDV = DVList.SelectedItem as DVWrapper;
if (selectedDV != null)
{
//Setting min/max
Bounds[0] = selectedDV.MinValue;
Bounds[Bounds.Count - 1] = selectedDV.MaxValue;
MyListBox.ItemsSource = Bounds;
}
}
My XAML looks like this:
<Window.Resources>
<Style x:Key="BoundsStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
...
<TextBox/>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="Focusable" Value="False"/>
</Style>
</Window.Resources>
...
<ListBox Name="MyListBox"
ItemContainerStyle="{StaticResource BoundsStyle}"/>
So when SaveAndSubmit is called, bounds ends up being what I had originally set it to in DVSelectionChanged. In other words, the listbox is not updating based on what the user has input into the textboxes contained in listbox. How can I get the updated ListBoxItems? I think my problem is similar to this one, but it's not working for me at the moment.
When I step through in the debugger, I can get individual ListBoxItems. However, their Content is empty. I'm looking into that right now.
You need to bind content of the textbox.
<TextBox/> need to change to <TextBox Content="{Binding}"/>
But follow MVVM else it will be difficult to find these errors.
I have a ItemsControl which displays its items in a ScrollViewer, and does virtualisation. I am trying to scroll that ScrollViewer to an (offscreen, hence virtualised) item it contains. However, since the item is virtualised, it doesn't really exist on the screen and has no position (IIUC).
I have tried BringIntoView on the child element, but it doesn't scroll into view. I have also tried manually doing it with TransformToAncestor, TransformBounds and ScrollToVerticalOffset, but TransformToAncestor never returns (I guess also because of the virtualisation, because it has no position, but I have no proof of that) and code after it never executes.
Is it possible to scroll to an item with a virtualising ItemsControl? If so, how?
I've been looking at getting a ItemsControl with a VirtualizingStackPanel to scroll to an item for a while now, and kept finding the "use a ListBox" answer. I didn't want to, so I found a way to do it. First you need to setup a control template for your ItemsControl that has a ScrollViewer in it (which you probably already have if you're using an items control). My basic template looks like the following (contained in a handy style for the ItemsControl)
<Style x:Key="TheItemsControlStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}" Padding="{TemplateBinding Control.Padding}" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}" SnapsToDevicePixels="True">
<ScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False" HorizontalScrollBarVisibility="Auto">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
So I've basically got a border with a scroll viewer thats going to contain my content.
My ItemsControl is defined with:
<ItemsControl x:Name="myItemsControl" [..snip..] Style="{DynamicResource TheItemsControlStyle}" ScrollViewer.CanContentScroll="True" VirtualizingStackPanel.IsVirtualizing="True">
Ok now for the fun part. I've created a extension method to attach to any ItemsControl to get it to scroll to the given item:
public static void VirtualizedScrollIntoView(this ItemsControl control, object item) {
try {
// this is basically getting a reference to the ScrollViewer defined in the ItemsControl's style (identified above).
// you *could* enumerate over the ItemsControl's children until you hit a scroll viewer, but this is quick and
// dirty!
// First 0 in the GetChild returns the Border from the ControlTemplate, and the second 0 gets the ScrollViewer from
// the Border.
ScrollViewer sv = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild((DependencyObject)control, 0), 0) as ScrollViewer;
// now get the index of the item your passing in
int index = control.Items.IndexOf(item);
if(index != -1) {
// since the scroll viewer is using content scrolling not pixel based scrolling we just tell it to scroll to the index of the item
// and viola! we scroll there!
sv.ScrollToVerticalOffset(index);
}
} catch(Exception ex) {
Debug.WriteLine("What the..." + ex.Message);
}
}
So with the extension method in place you would use it just like ListBox's companion method:
myItemsControl.VirtualizedScrollIntoView(someItemInTheList);
Works great!
Note that you can also call sv.ScrollToEnd() and the other usual scrolling methods to get around your items.
Poking around in the .NET source code leads me to recommend you the use of a ListBox and its ScrollIntoView method. The implementation of this method relies on a few internal methods like VirtualizingPanel.BringIndexIntoView which forces the creation of the item at that index and scrolls to it. The fact that many of those mechanism are internal means that if you try to do this on your own you're gonna have a bad time.
(To make the selection this brings with it invisible you can retemplate the ListBoxItems)
I know this is an old thread, but in case someone else (like me) comes across it, I figured it would be worth an updated answer that I just discovered.
As of .NET Framework 4.5, VirtualizingPanel has a public BringIndexIntoViewPublic method which works like a charm, including with pixel based scrolling. You'll have to either sub-class your ItemsControl, or use the VisualTreeHelper to find its child VirtualizingPanel, but either way it's now very easy to force your ItemsControl to scroll precisely to a particular item/index.
Using #AaronCook example, Created a behavior that works for my VirtualizingItemsControl. Here is the code for that:
public class ItemsControlScrollToSelectedBehavior : Behavior<ItemsControl>
{
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(ItemsControlScrollToSelectedBehavior),
new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnSelectedItemsChanged)));
public object SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsControlScrollToSelectedBehavior target = (ItemsControlScrollToSelectedBehavior)d;
object oldSelectedItems = e.OldValue;
object newSelectedItems = target.SelectedItem;
target.OnSelectedItemsChanged(oldSelectedItems, newSelectedItems);
}
protected virtual void OnSelectedItemsChanged(object oldSelectedItems, object newSelectedItems)
{
try
{
var sv = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(AssociatedObject, 0), 0) as ScrollViewer;
// now get the index of the item your passing in
int index = AssociatedObject.Items.IndexOf(newSelectedItems);
if (index != -1)
{
sv?.ScrollToVerticalOffset(index);
}
}
catch
{
// Ignore
}
}
}
and usage is:
<ItemsControl Style="{StaticResource VirtualizingItemsControl}"
ItemsSource="{Binding BoundItems}">
<i:Interaction.Behaviors>
<behaviors:ItemsControlScrollToSelectedBehavior SelectedItem="{Binding SelectedItem}" />
</i:Interaction.Behaviors>
</ItemsControl>
Helpful for those who like Behaviors and clean XAML, no code-behind.
I know I'm pretty late to the party but hopefully this may help someone else coming along looking for the solution...
int index = myItemsControl.Items.IndexOf(*your item*).FirstOrDefault();
int rowHeight = *height of your rows*;
myScrollView.ScrollToVerticalOffset(index*rowHeight);
//this will bring the given item to the top of the scrollViewer window
... and my XAML is setup like this...
<ScrollViewer x:Name="myScrollView">
<ItemsControl x:Name="myItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<!-- data here -->
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
This is an old thread, but I would like to suggest one way:
/// <summary>
/// Scrolls to the desired item
/// </summary>
/// <param name="control">ItemsControl</param>
/// <param name="item">item</param>
public static void ScrollIntoView(this ItemsControl control, Object item)
{
FrameworkElement framework = control.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
if (framework == null) { return; }
framework.BringIntoView();
}
I'm unable to figure out how to select an item programmatically in a ListView.
I'm attempting to use the listview's ItemContainerGenerator, but it just doesn't seem to work. For example, obj is null after the following operation:
//VariableList is derived from BindingList
m_VariableList = getVariableList();
lstVariable_Selected.ItemsSource = m_VariableList;
var obj =
lstVariable_Selected.ItemContainerGenerator.ContainerFromItem(m_VariableList[0]);
I've tried (based on suggestions seen here and other places) to use the ItemContainerGenerator's StatusChanged event, but to no avail. The event never fires. For example:
m_VariableList = getVariableList();
lstVariable_Selected.ItemContainerGenerator.StatusChanged += new EventHandler(ItemContainerGenerator_StatusChanged);
lstVariable_Selected.ItemsSource = m_VariableList;
...
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
//This code never gets called
var obj = lstVariable_Selected.ItemContainerGenerator.ContainerFromItem(m_VariableList[0]);
}
The crux of this whole thing is that I simply want to pre-select a few of the items in my ListView.
In the interest of not leaving anything out, the ListView uses some templating and Drag/Drop functionality, so I'm including the XAML here. Essentially, this template makes each item a textbox with some text - and when any item is selected, the checkbox is checked. And each item also gets a little glyph underneath it to insert new items (and this all works fine):
<DataTemplate x:Key="ItemDataTemplate_Variable">
<StackPanel>
<CheckBox x:Name="checkbox"
Content="{Binding Path=ListBoxDisplayName}"
IsChecked="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListViewItem}}, Path=IsSelected}" />
<Image ToolTip="Insert Custom Variable" Source="..\..\Resources\Arrow_Right.gif"
HorizontalAlignment="Left"
MouseLeftButtonDown="OnInsertCustomVariable"
Cursor="Hand" Margin="1, 0, 0, 2" Uid="{Binding Path=CmiOrder}" />
</StackPanel>
</DataTemplate>
...
<ListView Name="lstVariable_All" MinWidth="300" Margin="5"
SelectionMode="Multiple"
ItemTemplate="{StaticResource ItemDataTemplate_Variable}"
SelectionChanged="lstVariable_All_SelectionChanged"
wpfui:DragDropHelper.IsDropTarget="True"
wpfui:DragDropHelper.IsDragSource="True"
wpfui:DragDropHelper.DragDropTemplate="{StaticResource ItemDataTemplate_Variable}"
wpfui:DragDropHelper.ItemDropped="OnItemDropped"/>
So what am I missing? How do I programmatically select one or more of the items in the ListView?
Bind the IsSelected property of the ListViewItem to a property on your model. Then, you need only work with your model rather than worrying about the intricacies of the UI, which includes potential hazards around container virtualization.
For example:
<ListView>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsGroovy}"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
Now, just work with your model's IsGroovy property to select/deselect items in the ListView.
Where 'this' is the ListView instance. This will not only change the selection, but also set the focus on the newly selected item.
private void MoveSelection(int level)
{
var newIndex = this.SelectedIndex + level;
if (newIndex >= 0 && newIndex < this.Items.Count)
{
this.SelectedItem = this.Items[newIndex];
this.UpdateLayout();
((ListViewItem)this.ItemContainerGenerator.ContainerFromIndex(newIndex)).Focus();
}
}
Here would be my best guess, which would be a much simpler method for selection. Since I'm not sure what you're selecting on, here's a generic example:
var indices = new List<int>();
for(int i = 0; i < lstVariable_All.Items.Count; i++)
{
// If this item meets our selection criteria
if( lstVariable_All.Items[i].Text.Contains("foo") )
indices.Add(i);
}
// Reset the selection and add the new items.
lstVariable_All.SelectedIndices.Clear();
foreach(int index in indices)
{
lstVariable_All.SelectedIndices.Add(index);
}
What I'm used to seeing is a settable SelectedItem, but I see you can't set or add to this, but hopefully this method works as a replacement.
In case you are not working with Bindings, this could also be a solution, just find the items in the source and add them to the SelectedItems property of your listview:
lstRoomLights.ItemsSource = RoomLights;
var selectedItems = RoomLights.Where(rl => rl.Name.Contains("foo")).ToList();
selectedItems.ForEach(i => lstRoomLights.SelectedItems.Add(i));
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.
There are two main sub-problems one must solve in order to solve this problem:
Determine which item is being hovered over
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.
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;
}
}
}
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
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!
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>
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);
}
}
}
}
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.