/* * Copyright 2019 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; namespace CommonWPF { /// /// Generic Visual helper. /// public static class VisualHelper { /// /// Find a child object in a WPF visual tree. /// /// /// Sample usage: /// GridViewHeaderRowPresenter headerRow = listView.GetVisualChild<GridViewHeaderRowPresenter>(); /// /// From https://social.msdn.microsoft.com/Forums/vstudio/en-US/7d0626cb-67e8-4a09-a01e-8e56ee7411b2/gridviewcolumheader-radiobuttons?forum=wpf /// /// /// Start point. /// Object of appropriate type, or null if not found. public static T GetVisualChild(this Visual referenceVisual) where T : Visual { Visual child = null; for (Int32 i = 0; i < VisualTreeHelper.GetChildrenCount(referenceVisual); i++) { child = VisualTreeHelper.GetChild(referenceVisual, i) as Visual; if (child != null && child is T) { break; } else if (child != null) { child = GetVisualChild(child); if (child != null && child is T) { break; } } } return child as T; } } /// /// Helper functions for working with a ListView. /// /// ListViews are generalized to an absurd degree, so simple things like "what column did /// I click on" and "what row is at the top" that were easy in WinForms are not provided /// by WPF. /// public static class ListViewExtensions { /// /// Figures out which item index is at the top of the window. This only works for a /// ListView with a VirtualizingStackPanel. /// /// /// See https://stackoverflow.com/q/2926722/294248 for an alternative approach that /// uses hit-testing, as well as a copy of this approach. /// /// The item index, or -1 if the list is empty. public static int GetTopItemIndex(this ListView lv) { if (lv.Items.Count == 0) { return -1; } VirtualizingStackPanel vsp = lv.GetVisualChild(); if (vsp == null) { Debug.Assert(false, "ListView does not have a VirtualizingStackPanel"); return -1; } return (int)vsp.VerticalOffset; } /// /// Scrolls the ListView so that the specified item is at the top. The standard /// ListView.ScrollIntoView() makes the item visible but doesn't ensure a /// specific placement. /// /// /// Equivalent to setting myListView.TopItem in WinForms. /// public static void ScrollToTopItem(this ListView lv, object item) { ScrollViewer sv = lv.GetVisualChild(); sv.ScrollToBottom(); lv.ScrollIntoView(item); } /// /// Returns the ListViewItem that was clicked on, or null if an LVI wasn't the target /// of a click (e.g. off the bottom of the list). /// public static ListViewItem GetClickedItem(this ListView lv, MouseButtonEventArgs e) { DependencyObject dep = (DependencyObject)e.OriginalSource; // Should start at something like a TextBlock. Walk up the tree until we hit the // ListViewItem. while (dep != null && !(dep is ListViewItem)) { dep = VisualTreeHelper.GetParent(dep); } if (dep == null) { return null; } return (ListViewItem)dep; } /// /// Determines which column was the target of a mouse click. Only works for ListView /// with GridView. /// /// /// There's just no other way to do this with ListView. With DataGrid you can do this /// somewhat reasonably (see below), but ListView just doesn't want to help. /// /// Column index, or -1 if the click was outside the columns (e.g. off the right /// edge). public static int GetClickEventColumn(this ListView lv, MouseButtonEventArgs e) { // There's a bit of padding that seems to offset things. Not sure how to account // for it, so for now just fudge it. const int FUDGE = 4; Point p = e.GetPosition(lv); GridView gv = (GridView)lv.View; double startPos = FUDGE; for (int index = 0; index < gv.Columns.Count; index++) { GridViewColumn col = gv.Columns[index]; if (p.X < startPos + col.ActualWidth) { return index; } startPos += col.ActualWidth; } return -1; } } /// /// Helper functions for working with DataGrids. /// /// /// It's tempting to handle double-click actions by using the selected row. This gets a /// little weird, though, because double-clicking on a header or blank area doesn't /// clear the selection. /// public static class DataGridExtensions { /// /// Determines which row and column was the target of a mouse button action. /// /// /// Based on https://blog.scottlogic.com/2008/12/02/wpf-datagrid-detecting-clicked-cell-and-row.html /// /// True if the click was on a data item. public static bool GetClickRowColItem(this DataGrid dg, MouseButtonEventArgs e, out int rowIndex, out int colIndex, out object item) { rowIndex = colIndex = -1; item = null; DependencyObject dep = (DependencyObject)e.OriginalSource; // The initial dep will likely be a TextBlock. Walk up the tree until we find // an object for the cell. If we don't find one, this might be a click in the // header or off the bottom of the list. while (!(dep is DataGridCell)) { dep = VisualTreeHelper.GetParent(dep); if (dep == null) { return false; } } DataGridCell cell = (DataGridCell)dep; // Now search up for the DataGridRow object. do { dep = VisualTreeHelper.GetParent(dep); if (dep == null) { Debug.Assert(false, "Found cell but not row?"); return false; } } while (!(dep is DataGridRow)); DataGridRow row = (DataGridRow)dep; // Get a row index for the entry. DataGrid rowGrid = (DataGrid)ItemsControl.ItemsControlFromItemContainer(row); rowIndex = rowGrid.ItemContainerGenerator.IndexFromContainer(row); // Column index is, weirdly enough, just sitting in a property. colIndex = cell.Column.DisplayIndex; // Item is part of the row. item = row.Item; return true; } #if false public static DataGridRow GetRow(this DataGrid grid, int index) { DataGridRow row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(index); 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); } return row; } #endif } }