I have a ListBox or DataGrid filled with thousands of entries. I would like to know items that the user has looked at (scrolling, searching or otherwise). How can I tell what is visible to the user in the ListBox?
Bonus: Set a timer so that the item has to be shown for a minimum of N milliseconds (in the event the user is just pulling down the scrollbar).
Update: This is a near duplicate of Get items in view within a listbox - but the solution it gives, using "SelectedItems", is not sufficient. I need to know the items whether they are selected or not!
All you need to do is to get the underlying StackPanel that's inside the ListBox. It has enough information about which elements are showing. (It implements the interface IScrollInfo).
To get the underlying StackPanel (or actually VirtualizingStackPanel) from a given ListBox, we'll have to use VisualTreeHelper to go through the Visual Tree and look for the VirtualizingStackPanel, like so:
private VirtualizingStackPanel GetInnerStackPanel(FrameworkElement element)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
var child = VisualTreeHelper.GetChild(element, i) as FrameworkElement;
if (child == null) continue;
Debug.WriteLine(child.ToString());
if (child is VirtualizingStackPanel) return child as VirtualizingStackPanel;
var panel = GetInnerStackPanel(child);
if (panel != null)
return panel;
}
return null;
}
Now that we have the StackPanel, we're very close. The StackPanel has the properties VerticalOffset and ViewportHeight (both coming from IScrollInfo) that can give us all the information we need.
private void button1_Click(object sender, RoutedEventArgs e)
{
var theStackPanel = GetInnerStackPanel(MyListBox);
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < theStackPanel.Children.Count; i++)
{
if (i >= theStackPanel.VerticalOffset && i <= theStackPanel.VerticalOffset + theStackPanel.ViewportHeight)
{
visibleElements.Add(theStackPanel.Children[i] as FrameworkElement);
}
}
MessageBox.Show(visibleElements.Count.ToString());
MessageBox.Show(theStackPanel.VerticalOffset.ToString());
MessageBox.Show((theStackPanel.VerticalOffset + theStackPanel.ViewportHeight).ToString());
}
Related
I want to access the elements in the DataGrid.I am using following code.But I am unable to get the row of DataGrid.I am getting null value.I just want to know why I am getting null value and how to resolve this issue.
int itemscount = (dgMtHdr.Items.Count);
dgMtHdr.UpdateLayout();
for (int rowidx = 0; rowidx < itemscount; rowidx++)
{
Microsoft.Windows.Controls.DataGridRow dgrow = (Microsoft.Windows.Controls.DataGridRow)this.dgMtHdr.ItemContainerGenerator.ContainerFromIndex(rowidx);
if (dgrow != null)
{
DataRow dr = ((DataRowView)dgrow.Item).Row;
if (dr != null)
{
obj = new WPFDataGrid();
Microsoft.Windows.Controls.DataGridCell cells = obj.GetCell(dgMtHdr, rowidx, 7);
if (cells != null)
{
ContentPresenter panel = cells.Content as ContentPresenter;
if (panel != null)
{
ComboBox cmb = obj.GetVisualChild<ComboBox>(panel);
}
}
}
}
}
DataGrid internally hosts items in DataGridRowsPresenter which derives from VirtualizingStackPanel which means items rendered on UI by default support Virtualization i.e. ItemContainer won't be generated for items which are not rendered on UI yet.
That's why you getting null when you try to fetch rows which are not rendered on UI.
So, in case you are ready to trade off with Virtualization, you can turn off the Virtualization like this -
<DataGrid x:Name="dgMtHdr" VirtualizingStackPanel.IsVirtualizing="False"/>
Now, DataGridRow won't be null for any index value.
OR
You can get the row by manually calling UpdateLayout() and ScrollIntoView() for the index so that container gets generated for you. For details refer to this link here. From the link -
if (row == null)
{
// May be virtualized, bring into view and try again.
grid.UpdateLayout();
grid.ScrollIntoView(grid.Items[index]);
row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(index);
}
EDIT
Since your DataGrid is in second tab which is not rendered yet. That's why its ItemContainerGenerator haven't generated corresponding containers required for items. So, you need to do it once item container is generated by hooking to StausChanged event -
dgMtHdr.ItemContainerGenerator.StatusChanged += new
EventHandler(ItemContainerGenerator_StatusChanged);
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if ((sender as ItemContainerGenerator).Status ==
GeneratorStatus.ContainersGenerated)
{
// ---- Do your work here and you will get rows as you intended ----
// Make sure to unhook event here, otherwise it will be called
// unnecessarily on every status changed and moreover its good
// practise to unhook events if not in use to avoid any memory
// leak issue in future.
dgMtHdr.ItemContainerGenerator.StatusChanged -=
ItemContainerGenerator_StatusChanged;
}
}
I have implemented a virtualizing panel that works pretty well, the panel implements IScrollInfo. I am in the process of getting it to work using a keyboard. The Panel is used in a ListView as the ListView.ItemsPanel. When the user presses down the selected item correctly changes but no scrolling occurs and when you reach the last visible item you can not press down any longer. I am trying to figure out how to have the panel aware of the selected item and scroll appropriately. I have tried searching around but I cant seem to find anything related to what I want to do. A point in the right direction would be greatly appreciated.
Edit: Here is the code for the VirtualizingWrapPanel
So I found a solution that works fairly well, I am not sure if it is the best route to take but it seems to work fine.
In the ArrangeOverride method while looping over the children I do the following.
var listViewItem = child as ListViewItem;
if (listViewItem != null)
{
listViewItem.Selected -= ListViewItemSelected;
listViewItem.Selected += ListViewItemSelected;
}
In MeasureOverride I make a call to CleanUpItems in here we will need to unsubscribe from the selected event.
var listViewItem = children[i] as ListViewItem;
if (listViewItem != null)
{
listViewItem.Selected -= ListViewItemSelected;
}
I also added the following three functions.
private void ListViewItemSelected(object sender, RoutedEventArgs e)
{
var listViewItem = sender as ListViewItem;
if(listViewItem == null) return;
var content = listViewItem.Content as CollectionViewGroup;
if(content != null) return; //item is a group header dont click
var items = ItemContainerGenerator as ItemContainerGenerator;
if(items == null) return;
BringIndexIntoView(items.IndexFromContainer(listViewItem));
listViewItem.Focus();
}
protected override void BringIndexIntoView(int index)
{
var offset = GetOffsetForFirstVisibleIndex(index);
SetVerticalOffset(offset.Height);
}
private Size GetOffsetForFirstVisibleIndex(int index)
{
int childrenPerRow = CalculateChildrenPerRow(_extent);
var actualYOffset = ((index / childrenPerRow) * ChildDimension.Height) - ((ViewportHeight - ChildDimension.Height) / 2);
if (actualYOffset < 0)
{
actualYOffset = 0;
}
Size offset = new Size(_offset.X, actualYOffset);
return offset;
}
The GetOffsetForFirstVisibleIndex function will probably vary depending on your implementation but that should be enough info if anyone else is having trouble coming up with a solution.
I need to add some decoration to the contents of a WPF TextBox control. That works fine basically, I can get the position of specified character indices and layout my other elements accordingly. But it all breaks when the TextBox is scrolled. My layout positions don't match with the displayed text anymore because it has moved elsewhere.
Now I'm pretty surprised that the TextBox class doesn't provide any information about its scrolling state, nor any events when the scrolling has changed. What can I do now?
I used Snoop to find out whether there is some scrolling sub-element that I could ask, but the ScrollContentPresenter also doesn't have any scrolling information available. I'd really like to put my decoration elements right into the scrolled area so that the scrolling can affect them, too, but there can only be a single content control and that's one of the TextBox internals already.
I'm not sure how to capture an event when the textbox has been scrolled (probably use narohi's answer for that), but there is a simple way to see what the current scroll position is:
// Gets or sets the vertical scroll position.
textBox.VerticalOffset
(From http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.textboxbase.verticaloffset(v=vs.100).aspx)
I'm using it to see if the textbox is scrolled to the end, like this:
public static bool IsScrolledToEnd(this TextBox textBox)
{
return textBox.VerticalOffset + textBox.ViewportHeight == textBox.ExtentHeight;
}
You can get the ScrollViewer with this method by passing in your textbox as the argument and the type ScrollView. Then you may subscribe to the ScrollChanged event.
public static T FindDescendant<T>(DependencyObject obj) where T : DependencyObject
{
if (obj == null) return default(T);
int numberChildren = VisualTreeHelper.GetChildrenCount(obj);
if (numberChildren == 0) return default(T);
for (int i = 0; i < numberChildren; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child is T)
{
return (T)(object)child;
}
}
for (int i = 0; i < numberChildren; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
var potentialMatch = FindDescendant<T>(child);
if (potentialMatch != default(T))
{
return potentialMatch;
}
}
return default(T);
}
Example:
public MainWindow()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
ScrollViewer s = FindDescendant<ScrollViewer>(txtYourTextBox);
s.ScrollChanged += new ScrollChangedEventHandler(s_ScrollChanged);
}
void s_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// check event args for information needed
}
I have 4 List Boxes in my WPF App. Each of them, at any given point of time contains equal no. of String ListBoxItems in them. If selected index of any one of them changes the other three also reflect the same behaviour. What i want is that when a user moves scrollbar of one of them the other three should also move simultaneoulsly.
I tried Scrollintoview but it does not work bcoz if i select an item of a listBox and apply scrollintoview for other three Listboxes the selected item in them come on the top.
That's why i think scrollbar movement should be the best option for this. How to do that?
In XAML hook the ScrollChanged event
ScrollViewer.ScrollChanged="ListBox_ScrollChanged"
In CodeBehind find the Scrollviewers inside the ListBoxes and apply the Vertical offset:
private void ListBox_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var sourceScrollViewer = FindVisualChild<ScrollViewer>(sender as DependencyObject) as ScrollViewer;
var targetScrollViewer = FindVisualChild<ScrollViewer>(listBox2) as ScrollViewer;
targetScrollViewer.ScrollToVerticalOffset(sourceScrollViewer.VerticalOffset);
}
// helper method
private childItem FindVisualChild<childItem>(DependencyObject obj)
where childItem : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is childItem)
return (childItem)child;
else
{
childItem childOfChild = FindVisualChild<childItem>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
Well in code it is something like that :
1) get the four scrollviewers of the four ListViews
( by finding them within the child (VisualTreeHelper.getchild)
inside a method like FindDescendant(...))
2) hook their scrolls events (ScrollChanged) to a common sub that
will get the VerticalOffset of the one that triggered the event
and ScrollToVerticalOffset(.) the others.
must be possible in xaml also, but seems more complicated to me.
I'm implementing simple drag-and-drop functionality in my app, and I would like to know if the user has dropped the item above the first item in the list (in the header row) so i can just insert it as the first item.
I'm using VisualTreeHelper.HitTest to get the item at the drop position, but this only works if there actually is an item there.
HitTestResult hitTestResults = VisualTreeHelper.HitTest(myListView, location);
When the mouse is on the headers i get one of many several items in hitTestResults.VisualHit. In just a few tests, I've gotten ListBoxChrome, TextBlock, and Border How can i know if any of these are part of the header row? I can't just test for them specifically since there could be other UI elements returned.
Can i get the coordinates of the header row of the listview to see if my point is inside it? Or is there a way that i can know if my Point is inside that header row?
I don't know how your current implementation look but you can walk up the Visual Tree until you either find a ListViewItem or a GridViewColumnHeader. If you find a GridViewColumnHeader you know that the item was dropped in this specific Header.
Uploaded a small sample project here demonstrating the effect with MessageBox's on drop: http://www.mediafire.com/?v3l8nl4rnewhz5s
It will look something like this
private void ListView_Drop(object sender, DragEventArgs e)
{
ListView parent = sender as ListView;
YourDataClass data = e.Data.GetData(typeof(YourDataClass)) as YourDataClass;
if (data != null)
{
HitTestResult hitTestResult = VisualTreeHelper.HitTest(parent, e.GetPosition(parent));
ListViewItem hitItem = VisualTreeHelpers.GetVisualParent<ListViewItem>(hitTestResult.VisualHit);
GridViewColumnHeader columnHeader = VisualTreeHelpers.GetVisualParent<GridViewColumnHeader>(hitTestResult.VisualHit);
if (hitItem != null) // ListViewItem Drop
{
//..
}
else if (columnHeader != null) // Header Drop
{
//..
}
}
}
public static T GetVisualParent<T>(object childObject) where T : Visual
{
DependencyObject child = childObject as DependencyObject;
while ((child != null) && !(child is T))
{
child = VisualTreeHelper.GetParent(child);
}
return child as T;
}