/* * 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.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; 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. /// /// Looks like we get the same values from ScrollViewer.VerticalOffset. I don't know /// if there's a reason to favor one over the other. /// /// 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. Unfortunately, the /// ScrollIntoView call takes 60-100ms on a list with fewer than 1,000 items. And /// sometimes it just silently fails. Prefer ScrollToIndex() to this. /// public static void ScrollToTopItem(this ListView lv, object item) { ScrollViewer sv = lv.GetVisualChild(); sv.ScrollToBottom(); lv.ScrollIntoView(item); } /// /// Scrolls the ListView to the specified vertical index. The ScrollViewer should /// be operating in "logical" units (lines) rather than "physical" units (pixels). /// public static void ScrollToIndex(this ListView lv, int index) { ScrollViewer sv = lv.GetVisualChild(); sv.ScrollToVerticalOffset(index); } /// /// 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; // Need to take horizontal scrolling into account. ScrollViewer sv = lv.GetVisualChild(); double scrollPos = sv.HorizontalOffset; Point p = e.GetPosition(lv); GridView gv = (GridView)lv.View; double startPos = FUDGE - scrollPos; 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 } /// /// RichTextBox extensions. /// public static class RichTextBoxExtensions { /// /// Overloads RichTextBox.AppendText() with a version that takes a color as an argument. /// NOTE: color is "sticky", and will affect the next call to the built-in AppendText() /// method. /// /// /// Adapted from https://stackoverflow.com/a/23402165/294248 /// /// TODO(someday): figure out how to reset the color for future calls. /// public static void AppendText(this RichTextBox box, string text, Color color) { TextRange tr = new TextRange(box.Document.ContentEnd, box.Document.ContentEnd); tr.Text = text; try { tr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(color)); } catch (FormatException ex) { Debug.WriteLine("RTB AppendText extension failed: " + ex); } } } /// /// BitmapSource extensions. /// public static class BitmapSourceExtensions { /// /// Creates a scaled copy of a BitmapSource. Only scales up, using nearest-neighbor. /// public static BitmapSource CreateScaledCopy(this BitmapSource src, int scale) { // Simple approach always does a "blurry" scale. //return new TransformedBitmap(src, new ScaleTransform(scale, scale)); // Adapted from https://weblogs.asp.net/bleroy/resizing-images-from-the-server-using-wpf-wic-instead-of-gdi // (found via https://stackoverflow.com/a/25570225/294248) BitmapScalingMode scalingMode = BitmapScalingMode.NearestNeighbor; int newWidth = (int)src.Width * scale; int newHeight = (int)src.Height * scale; var group = new DrawingGroup(); RenderOptions.SetBitmapScalingMode(group, scalingMode); group.Children.Add(new ImageDrawing(src, new Rect(0, 0, newWidth, newHeight))); var targetVisual = new DrawingVisual(); var targetContext = targetVisual.RenderOpen(); targetContext.DrawDrawing(group); var target = new RenderTargetBitmap( newWidth, newHeight, 96, 96, PixelFormats.Default); targetContext.Close(); target.Render(targetVisual); var targetFrame = BitmapFrame.Create(target); return targetFrame; } } }