I copied unit based textbox class from internet and class definition looks as follow:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SapQmWp.Classes
{
public class UnitTextBox : TextBox
{
public static DependencyProperty UnitTextProperty =
DependencyProperty.Register(
"UnitText",
typeof(string),
typeof(UnitTextBox),
new FrameworkPropertyMetadata(
default(string),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsRender));
public static DependencyProperty UnitPaddingProperty =
DependencyProperty.Register(
"UnitPadding",
typeof(Thickness),
typeof(UnitTextBox),
new FrameworkPropertyMetadata(
new Thickness(5d, 0d, 0d, 0d),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsRender));
public static DependencyProperty TextBoxWidthProperty =
DependencyProperty.Register(
"TextBoxWidth",
typeof(double),
typeof(UnitTextBox),
new FrameworkPropertyMetadata(
double.NaN,
FrameworkPropertyMetadataOptions.AffectsMeasure));
private FormattedText _unitText;
private Rect _unitTextBounds;
public string UnitText
{
get { return (string) GetValue(UnitTextProperty); }
set { SetValue(UnitTextProperty, value); }
}
public Thickness UnitPadding
{
get { return (Thickness) GetValue(UnitPaddingProperty); }
set { SetValue(UnitPaddingProperty, value); }
}
public double TextBoxWidth
{
get { return (double) GetValue(TextBoxWidthProperty); }
set { SetValue(TextBoxWidthProperty, value); }
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ForegroundProperty)
EnsureUnitText(true);
}
protected override Size MeasureOverride(Size constraint)
{
var textBoxWidth = TextBoxWidth;
var unit = EnsureUnitText(true);
var padding = UnitPadding;
if (unit != null)
{
var unitWidth = unit.Width + padding.Left + padding.Right;
var unitHeight = unit.Height + padding.Top + padding.Bottom;
constraint = new Size(
constraint.Width - unitWidth,
Math.Max(constraint.Height, unitHeight));
}
var hasFixedTextBoxWidth = !double.IsNaN(textBoxWidth) &&
!double.IsInfinity(textBoxWidth);
if (hasFixedTextBoxWidth)
constraint = new Size(textBoxWidth, constraint.Height);
var baseSize = base.MeasureOverride(constraint);
var baseWidth = hasFixedTextBoxWidth ? textBoxWidth : baseSize.Width;
if (unit != null)
{
var unitWidth = unit.Width + padding.Left + padding.Right;
var unitHeight = unit.Height + padding.Top + padding.Bottom;
return new Size(
baseWidth + unitWidth,
Math.Max(baseSize.Height, unitHeight));
}
return new Size(baseWidth, baseSize.Height);
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
var textSize = arrangeBounds;
var unit = EnsureUnitText(false);
var padding = UnitPadding;
if (unit != null)
{
var unitWidth = unit.Width + padding.Left + padding.Right;
var unitHeight = unit.Height + padding.Top + padding.Bottom;
textSize.Width -= unitWidth;
_unitTextBounds = new Rect(
textSize.Width + padding.Left,
(arrangeBounds.Height - unitHeight)/2 + padding.Top,
textSize.Width,
textSize.Height);
}
var baseSize = base.ArrangeOverride(textSize);
if (unit != null)
{
var unitWidth = unit.Width + padding.Left + padding.Right;
var unitHeight = unit.Height + padding.Top + padding.Bottom;
return new Size(
baseSize.Width + unitWidth,
Math.Max(baseSize.Height, unitHeight));
}
return baseSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var unitText = EnsureUnitText(false);
if (unitText != null)
drawingContext.DrawText(unitText, _unitTextBounds.Location);
}
private FormattedText EnsureUnitText(bool invalidate = false)
{
if (invalidate)
_unitText = null;
if (_unitText != null)
return _unitText;
var unit = UnitText;
if (!string.IsNullOrEmpty(unit))
{
_unitText = new FormattedText(
unit,
CultureInfo.InvariantCulture,
FlowDirection,
new Typeface(
FontFamily,
FontStyle,
FontWeight,
FontStretch),
FontSize,
Foreground);
}
return _unitText;
}
}
}
And I insert into the window xaml as follow:
...
<ui:UnitTextBox Grid.Row="1" Style="{StaticResource WeightTbStyle}" UnitText="KG" Text="{Binding TargetWeight, UpdateSourceTrigger=PropertyChanged}"/>
...
But the background color is null rather than a color.
My question, how to set background color for UnitText too?
Update
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SapQmWp.Themes">
<Style x:Key="WeightTbStyle" TargetType="TextBox">
<Setter Property="Background" Value="#F8F8F8" />
<Setter Property="BorderBrush" Value="{x:Null}"/>
<Setter Property="Margin" Value="10,40,10,40" />
<Setter Property="Padding" Value="0,0,0,5" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="#404242"/>
<Setter Property="FontSize" Value="24pt"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="BorderThickness" Value="0" />
</Style>
</ResourceDictionary>
Update 2
When I set the background on controls like
<ui:UnitTextBox Grid.Row="1" Background="Tomato" Style="{StaticResource WeightTbStyle}" UnitText="KG" Text="{Binding TargetWeight, UpdateSourceTrigger=PropertyChanged}"/>
as result I've got:
In your
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var unitText = EnsureUnitText(false);
if (unitText != null)
drawingContext.DrawText(unitText, _unitTextBounds.Location);
}
Try something like this to set color and formatting
var formattedText = new FormattedText(unitText,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(new FontFamily("ANY_FONT_FAMILY"), FontStyles.Normal, FontWeights.Bold, FontStretches.Normal),
24, Brushes.Red);
drawingContext.DrawText(formattedText, _unitTextBounds.Location);
}
And for background color just draw a rectangle before you render your text.
drawingContext.DrawRectangle(Brushes.Red, Nothing, YOUR_TEXT_RECT);
Related
I've used the CodeBox project from CodeProject and it works very well except for the fact that I can't disable text wrapping. In a normal TextBox, simply setting the TextWrapping property to NoWrap does the trick, but not with CodeBox (which inherits from TextBox in code-behind). I've tried adding a horizontal scrollbar but that doesn't help. The scrollbar is visible and it changes the size of the drag button to show that it sees that the unwrapped text is wider than the viewing area, but since the text has already been wrapped, dragging it doesn't make any difference.
I've tracked the problem to a line in OnRender:
"formattedText.MaxTextWidth = this.ViewportWidth; // space for scrollbar"
I'm fairly new to WPF and there's much to it that is still mysterious to me, so the solution may be obvious to someone with more experience with it.
I'd appreciate any suggestions. This is the code-behind C#, lengthy, but it has been trimmed down to only enough to show what's going on. The rest (that has been reomved) is just code that does more text-coloring.
public partial class CodeBox : TextBox
{
bool m_bScrollingEventEnabled;
SolidColorBrush m_brRed = new SolidColorBrush (Colors.Red);
SolidColorBrush m_brOrange = new SolidColorBrush (Colors.Orange);
SolidColorBrush m_brBlack = new SolidColorBrush (Colors.Black);
public CodeBox ()
{
this.TextChanged += new TextChangedEventHandler (txtTest_TextChanged);
this.Foreground = new SolidColorBrush (Colors.Transparent);
this.Background = new SolidColorBrush (Colors.Transparent);
this.TextWrapping = System.Windows.TextWrapping.NoWrap;
base.TextWrapping = System.Windows.TextWrapping.NoWrap;
InitializeComponent ();
}
public static DependencyProperty BaseForegroundProperty = DependencyProperty.Register ("BaseForeground", typeof (Brush), typeof (CodeBox),
new FrameworkPropertyMetadata (new SolidColorBrush (Colors.Black), FrameworkPropertyMetadataOptions.AffectsRender));
public Brush BaseForeground
{
get { return (Brush)GetValue (BaseForegroundProperty); }
set { SetValue (BaseForegroundProperty, value); }
}
public static DependencyProperty BaseBackgroundProperty = DependencyProperty.Register ("BaseBackground", typeof (Brush), typeof (CodeBox),
new FrameworkPropertyMetadata (new SolidColorBrush (Colors.Black), FrameworkPropertyMetadataOptions.AffectsRender));
public Brush BaseBackground
{
get { return (Brush)GetValue (BaseBackgroundProperty); }
set { SetValue (BaseBackgroundProperty, value); }
}
void txtTest_TextChanged (object sender, TextChangedEventArgs e)
{
this.InvalidateVisual ();
}
protected override void OnRender (System.Windows.Media.DrawingContext drawingContext)
{
//base.OnRender(drawingContext);
if (this.Text.Length > 0)
{
EnsureScrolling ();
FormattedText formattedText = new FormattedText (
this.Text,
CultureInfo.GetCultureInfo ("en-us"),
FlowDirection.LeftToRight,
new Typeface (this.FontFamily.Source),
this.FontSize,
BaseForeground); //Text that matches the textbox's
double leftMargin = 4.0 + this.BorderThickness.Left;
double topMargin = 2 + this.BorderThickness.Top;
***formattedText.MaxTextWidth = this.ViewportWidth; // space for scrollbar***
formattedText.MaxTextHeight = Math.Max (this.ActualHeight + this.VerticalOffset, 0); //Adjust for scrolling
drawingContext.PushClip (new RectangleGeometry (new Rect (0, 0, this.ActualWidth, this.ActualHeight)));//restrict text to textbox
int iStartVisibleLine = GetFirstVisibleLineIndex ();
int iEndVisibleLine = GetLastVisibleLineIndex ();
for (int iIdx = iStartVisibleLine; iIdx <= iEndVisibleLine - 1; ++iIdx)
{
// Text coloring
int iOffset = GetCharacterIndexFromLineIndex (iIdx);
int iOffsetNext = GetCharacterIndexFromLineIndex (iIdx + 1);
string strLine = Text.Substring (iOffset, iOffsetNext - iOffset);
}
drawingContext.DrawText (formattedText, new Point (leftMargin, topMargin - this.VerticalOffset));
}
}
private void EnsureScrolling ()
{
if (!m_bScrollingEventEnabled)
{
DependencyObject dp = VisualTreeHelper.GetChild (this, 0);
ScrollViewer sv = VisualTreeHelper.GetChild (dp, 0) as ScrollViewer;
sv.ScrollChanged += new ScrollChangedEventHandler (ScrollChanged);
m_bScrollingEventEnabled = true;
}
}
private void ScrollChanged (object sender, ScrollChangedEventArgs e)
{
this.InvalidateVisual ();
}
}
The xaml from the project:
<TextBox x:Class="CodeBoxControl.CodeBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:CodeBoxControl">
<TextBox.Template>
<ControlTemplate TargetType="c:CodeBox" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}" BorderBrush="{TemplateBinding Border.BorderBrush}"
Background="{TemplateBinding Panel.Background}" Name="Bd" SnapsToDevicePixels="True">
<ScrollViewer Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="UIElement.IsEnabled">
<Setter Property="Panel.Background" TargetName="Bd">
<Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.ControlBrushKey}" />
</Setter.Value>
</Setter>
<Setter Property="TextElement.Foreground">
<Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.GrayTextBrushKey}" />
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>False</s:Boolean>
</Trigger.Value>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</TextBox.Template>
Xaml from the parent window that uses the CodeBox code:
<c:CodeBox Name="DisassemblyOutput"
FontFamily="Courier New"
FontSize="20"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
BaseForeground="Black"
Margin="4,4,4,4"
Background="#CEE9C9"
Foreground="Magenta"
TextWrapping="NoWrap"
AutoWordSelection="False"/>
This is a sample of the code that loads the text into the CodeBox window:
private void OnLoadDASM (object sender, RoutedEventArgs e)
{
DisassemblyOutput.FontSize = 12;
DisassemblyOutput.Clear ();
DisassemblyOutput.m_eWindowData = CodeBox.EWindowData.EDasm;
DisassemblyOutput.Background = new SolidColorBrush (Colors.Transparent);//Color.FromRgb (0xCE, 0xE9, 0xC9));
DisassemblyOutput.Foreground = new SolidColorBrush (Colors.Transparent);
DisassemblyOutput.BaseBackground = new SolidColorBrush (Color.FromRgb (0xCE, 0xE9, 0xC9));
DisassemblyOutput.BaseForeground = new SolidColorBrush (Colors.Transparent);
DisassemblyOutput.TextWrapping = TextWrapping.NoWrap;
DisassemblyOutput.Text += "Loop_02_0A0F 0A0F: SIO F3 10 28 5475 Keyboard Set Error Indicator Restore Data Key " + Environment.NewLine;
DisassemblyOutput.Text += " Disable Interrupt " + Environment.NewLine;
DisassemblyOutput.Text += " 0A12: SNS 70 12 FF,1 0x0B0C 5475 Keyboard 2 sense bytes " + Environment.NewLine;
}
This is what I want:
https://i.stack.imgur.com/M4ts0.png
and what's showing up:
https://i.stack.imgur.com/gdBco.png
I've also noticed that when the text wraps and I use the vertical scrollbar, the text in the top part of the pane disappears, and the more I scroll down, the more of it disappears:
1
The fix is to set MaxTextWidth to the width of the line instead of the ViewportWidth property:
iStartVisibleLine = GetFirstVisibleLineIndex ();
iEndVisibleLine = GetLastVisibleLineIndex ();
iOffset = GetCharacterIndexFromLineIndex (0);
iOffsetNext = GetCharacterIndexFromLineIndex (1);
strLine = Text.Substring (iOffset, iOffsetNext - iOffset);
geomFirstLine = formattedText.BuildHighlightGeometry (new Point (leftMargin, topMargin - this.VerticalOffset), iOffset, strLine.Length);
rcBounds = geomFirstLine.GetRenderBounds (null);
formattedText.MaxTextWidth = rcBounds.Width; // Space for scrollbar
I was looking on how to replicate a Google button's shadow on hover effect in my WPF document and came across this: https://stackoverflow.com/a/53031057/12299798, which answers my problem exactly, but the only issue I have is that I normally do all the frontend things in XAML and so I am not quite experienced in the styling in C#. My problem is that I want to 'Bind' my foreground colour to the shadow colour, this is because in the code it only sets it to a greyish colour which would mean I would have to do a whole other class for each colour that I want to set it for. Here is my code now:
MainWindow.xaml:
<Button Content="shadow">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="local:UI.Elevation" Value="10"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="local:UI.Elevation" Value="0"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
UI.cs:
public static class UI
{
public static readonly DependencyProperty ElevationProperty = DependencyProperty.RegisterAttached("Elevation", typeof(double), typeof(UI), new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsRender, null, OnRedElevationChanged));
public static double GetElevation(this UIElement element) => element.GetValue(ElevationProperty) as double? ?? default;
public static void SetElevation(this UIElement element, double elevation) => element.SetValue(ElevationProperty, elevation);
private static object OnRedElevationChanged(DependencyObject d, object value)
{
if (d is UIElement element && value is double elevation)
if (elevation == 0)
element.Effect = null;
else
{
Effect e = CreateElevation(elevation, element.Effect);
if (e != null)
element.Effect = e;
}
return value;
}
private static Effect CreateElevation(double elevation, Effect source)
{
void MixShadows(DropShadowEffect nearest, DropShadowEffect matched, double balance)
{
matched.BlurRadius = matched.BlurRadius * (1 - balance) + nearest.BlurRadius * balance;
matched.ShadowDepth = matched.ShadowDepth * (1 - balance) + nearest.ShadowDepth * balance;
}
DropShadowEffect[] shadows = new DropShadowEffect[]
{
new DropShadowEffect()
{
BlurRadius = 5,
ShadowDepth = 1
},
new DropShadowEffect()
{
BlurRadius = 8,
ShadowDepth = 1.5
},
new DropShadowEffect()
{
BlurRadius = 14,
ShadowDepth = 4.5
},
new DropShadowEffect()
{
BlurRadius = 25,
ShadowDepth = 8
},
new DropShadowEffect()
{
BlurRadius = 35,
ShadowDepth = 13
}
};
elevation = Math.Max(0, elevation / 12 * shadows.Length - 1);
int prevIndex = (int)Math.Floor(elevation), index = (int)elevation, nextIndex = (int)Math.Ceiling(elevation);
double approx = elevation - index;
DropShadowEffect shadow = shadows[index];
if (approx != 0)
MixShadows(approx < 0 ? shadows[prevIndex] : shadows[nextIndex], shadow, Math.Abs(approx));
bool modify = false;
if (source is DropShadowEffect sourceShadow)
{
sourceShadow.BlurRadius = shadow.BlurRadius;
sourceShadow.ShadowDepth = shadow.ShadowDepth;
shadow = sourceShadow;
modify = true;
}
shadow.Direction = 270;
shadow.Color = Colors.Red;
shadow.Opacity = .42;
shadow.RenderingBias = RenderingBias.Performance;
return modify ? null : shadow;
}
}
And to get what I want in XAML I would do something like this:
<Button Content="shadow">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="local:UI.Elevation" Value="10"/>
<Setter Property="local:UI.Elevation.Foreground" Value="Blue"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="local:UI.Elevation" Value="0"/>
<Setter Property="local:UI.Elevation.Foreground" Value="Blue"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
But I don't quite know how to link up the local:UI.Elevation.Foreground to change the shadow colour, I would have to change this line here from UI.cs: shadow.Color = Colors.Red; and bind it to the Elevation.Foreground, but I am having difficulties. Can anyone help?
All you need to do is updated the UI class with a new attached property named something like Color of type Color and give it a default color you like.
Then, in its OnColorChanged handler, call OnElevationChanged so that the shadow gets recreated.
#region Color (Attached Property)
public static readonly DependencyProperty ColorProperty =
DependencyProperty.RegisterAttached(
"Color",
typeof(Color),
typeof(UI),
new PropertyMetadata(Colors.Black, OnColorChanged));
public static Color GetColor(DependencyObject obj)
{
return (Color)obj.GetValue(ColorProperty);
}
public static void SetColor(DependencyObject obj, Color value)
{
obj.SetValue(ColorProperty, value);
}
private static void OnColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
OnElevationChanged(d, GetElevation((UIElement)d));
}
#endregion
In the CreateElevation method, add a new parameter for the color, so you can set it.
private static Effect CreateElevation(double elevation, Effect source, Color color)
{
...
shadow.Color = color;
...
}
Finally, in OnElevationChanged update it so it to call GetColor so it can pass in the new Color property.
private static object OnElevationChanged(DependencyObject d, object value)
{
if (d is UIElement element && value is double elevation)
if (elevation == 0)
element.Effect = null;
else
{
Effect e = CreateElevation(elevation, element.Effect, GetColor(element));
if (e != null)
element.Effect = e;
}
return value;
}
Example XAML
<Button
Width="100"
Height="20"
Content="shadow">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="local:UI.Elevation" Value="10" />
<Setter Property="local:UI.Color" Value="Blue" />
</Trigger>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="local:UI.Elevation" Value="5" />
<Setter Property="local:UI.Color" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
NOTE: There is some refactoring you could do to make all this cleaner, but hopefully this will get your started.
I want to animate button's background color with DinamicResources. To do this i create attached DependencyProperty:
namespace ModPlusStyle.Controls.Helpers
{
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
public class ButtonAssist
{
private static readonly Dictionary<Button, Color> _initBackgroundBrush = new Dictionary<Button, Color>();
private static readonly Dictionary<Button, Color> _initForegroundBrush = new Dictionary<Button, Color>();
public static readonly DependencyProperty AnimateMouseOverProperty = DependencyProperty.RegisterAttached(
"AnimateMouseOver",
typeof(bool),
typeof(ButtonAssist),
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsRender, AnimateMouseOverChangedCallback));
public static void SetAnimateMouseOver(DependencyObject element, bool value)
{
element.SetValue(AnimateMouseOverProperty, value);
}
public static bool GetAnimateMouseOver(DependencyObject element)
{
return (bool)element.GetValue(AnimateMouseOverProperty);
}
private static void AnimateMouseOverChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Button button)
{
if ((bool)e.NewValue)
{
button.MouseEnter += ButtonOnMouseEnter;
button.MouseLeave += ButtonOnMouseLeave;
}
else
{
button.MouseEnter -= ButtonOnMouseEnter;
button.MouseLeave -= ButtonOnMouseLeave;
}
}
}
private static void ButtonOnMouseEnter(object sender, MouseEventArgs e)
{
if (sender is Button button &&
!(button.Parent is WindowCommands) &&
button.Background is SolidColorBrush backgroundSolidColorBrush &&
button.Foreground is SolidColorBrush foregroundSolidColorBrush)
{
var parentWindow = Window.GetWindow(button);
if (parentWindow != null)
{
if (parentWindow.Resources["WhiteBrush"] is SolidColorBrush whiteBrush &&
parentWindow.Resources["BlackBrush"] is SolidColorBrush blackBrush)
{
if (_initBackgroundBrush.ContainsKey(button))
_initBackgroundBrush[button] = backgroundSolidColorBrush.Color;
else
_initBackgroundBrush.Add(button, backgroundSolidColorBrush.Color);
if (_initForegroundBrush.ContainsKey(button))
_initForegroundBrush[button] = foregroundSolidColorBrush.Color;
else
_initForegroundBrush.Add(button, foregroundSolidColorBrush.Color);
button.Background = new SolidColorBrush(backgroundSolidColorBrush.Color);
ColorAnimation backgroundColorAnimation = new ColorAnimation(
backgroundSolidColorBrush.Color,
whiteBrush.Color,
new Duration(TimeSpan.FromMilliseconds(300)));
button.Background.BeginAnimation(SolidColorBrush.ColorProperty, backgroundColorAnimation);
button.Foreground = new SolidColorBrush(foregroundSolidColorBrush.Color);
ColorAnimation foregroundColorAnimation = new ColorAnimation(
foregroundSolidColorBrush.Color,
blackBrush.Color,
new Duration(TimeSpan.FromMilliseconds(300)));
button.Foreground.BeginAnimation(SolidColorBrush.ColorProperty, foregroundColorAnimation);
}
}
}
}
private static void ButtonOnMouseLeave(object sender, MouseEventArgs e)
{
if (sender is Button button &&
!(button.Parent is WindowCommands) &&
_initBackgroundBrush.ContainsKey(button) &&
_initForegroundBrush.ContainsKey(button))
{
var parentWindow = Window.GetWindow(button);
if (parentWindow != null)
{
if (parentWindow.Resources["AccentColorBrush"] is SolidColorBrush accentColorBrush &&
parentWindow.Resources["ForegroundForAccentedBrush"] is SolidColorBrush foregroundForAccentedBrush)
{
button.Background = new SolidColorBrush(((SolidColorBrush)button.Background).Color);
ColorAnimation backgroundColorAnimation = new ColorAnimation(
((SolidColorBrush)button.Background).Color,
_initBackgroundBrush[button],
new Duration(TimeSpan.FromMilliseconds(300)));
backgroundColorAnimation.Completed += (o, args) =>
{
if (_initBackgroundBrush[button] == accentColorBrush.Color)
button.SetResourceReference(Control.BackgroundProperty, "AccentColorBrush");
_initBackgroundBrush.Remove(button);
};
button.Background.BeginAnimation(SolidColorBrush.ColorProperty, backgroundColorAnimation);
button.Foreground = new SolidColorBrush(((SolidColorBrush)button.Foreground).Color);
ColorAnimation foregroundColorAnimation = new ColorAnimation(
((SolidColorBrush)button.Foreground).Color,
_initForegroundBrush[button],
new Duration(TimeSpan.FromMilliseconds(300)));
foregroundColorAnimation.Completed += (o, args) =>
{
if (_initForegroundBrush[button] == foregroundForAccentedBrush.Color)
button.SetResourceReference(Control.ForegroundProperty, "ForegroundForAccentedBrush");
_initForegroundBrush.Remove(button);
};
button.Foreground.BeginAnimation(SolidColorBrush.ColorProperty, foregroundColorAnimation);
}
}
}
}
}
}
I set value in button's style:
<Style x:Key="ModPlusAccentButton" TargetType="{x:Type ButtonBase}">
<Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
<Setter Property="Background" Value="{DynamicResource AccentColorBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentColorBrush}" />
<Setter Property="Foreground" Value="{DynamicResource ForegroundForAccentedBrush}" />
<Setter Property="Padding" Value="12 6 12 6" />
<Setter Property="helpers:ButtonAssist.AnimateMouseOver" Value="True"></Setter>
<Setter Property="SnapsToDevicePixels" Value="True" />
.......
In debug i get the right colors in ButtonOnMouseEnter method. But button's background not changed
However, the animation in the ButtonOnMouseLeave method works correctly!
Why so?
The problem wath in ClipBorder that used in button's template. Change to Border and it start works
I have a Datagrid where some columns are DataGridTemplateColumn like this
<DataGridTemplateColumn
Width="1.5*"
Header="{x:Static lang:Labels.GENERAL_ValorTotal}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<sty:DecimalTextBox
Text="{Binding Valor,StringFormat=\{0:c3\}}"
IsReadOnly="True"
Style="{StaticResource DecimalTextBoxGridStyle}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
My problem is when I select a row, My custom control doesn't change it's foreground color as DataGridTextColumn does.
How can i do to style my custom control or my datagrid to force all columns to change
Edit 1 My custom object.
public class DecimalTextBox : TextBox
{
#region Float Color
public static readonly DependencyProperty FloatColorProperty =
DependencyProperty.Register("FloatColor", typeof(Color), typeof(DecimalTextBox), new FrameworkPropertyMetadata(Colors.Red));
public Color FloatColor
{
get { return (Color)GetValue(FloatColorProperty); }
set { SetValue(FloatColorProperty, value); }
}
#endregion
#region Show Zero value
public bool ShowZeroValue
{
get { return (bool)GetValue(ShowZeroValueProperty); }
set { SetValue(ShowZeroValueProperty, value); }
}
// Using a DependencyProperty as the backing store for ShowZeroValue. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ShowZeroValueProperty =
DependencyProperty.Register("ShowZeroValue", typeof(bool), typeof(DecimalTextBox), new PropertyMetadata(true));
#endregion
protected TextBlock _textBlock;
protected FrameworkElement _textBoxView;
public DecimalTextBox()
{
_textBlock = new TextBlock() { Margin = new Thickness(1, 0, 0, 0) };
Loaded += ExTextBox_Loaded;
}
private void ExTextBox_Loaded(object sender, RoutedEventArgs e)
{
Loaded -= ExTextBox_Loaded;
// hide the original drawing visuals, by setting opacity on their parent
var visual = this.GetChildOfType<DrawingVisual>();
if (visual != null)
{
_textBoxView = (FrameworkElement)visual.Parent;
_textBoxView.Opacity = 0;
// add textblock to do the text drawing for us
var grid = this.GetChildOfType<Grid>();
if (grid.Children.Count >= 2)
grid.Children.Insert(1, _textBlock);
else
grid.Children.Add(_textBlock);
}
}
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnLostKeyboardFocus(e);
_textBoxView.Opacity = 0;
_textBlock.Visibility = Visibility.Visible;
MustShowValue();
}
protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnGotKeyboardFocus(e);
_textBoxView.Opacity = 1;
_textBlock.Visibility = Visibility.Collapsed;
}
private bool MustShowValue()
{
bool show = true;
if (!ShowZeroValue && Text == "0")
{
show = false;
_textBlock.Inlines.Clear();
_textBlock.Inlines.Add(new Run
{
Text = string.Empty,
FontFamily = FontFamily,
FontSize = FontSize,
Foreground = Foreground
});
_textBlock.Inlines.Add(new Run
{
Text = string.Empty,
FontFamily = FontFamily,
TextDecorations = System.Windows.TextDecorations.Underline,
BaselineAlignment = BaselineAlignment.TextTop,
FontSize = FontSize * 5 / 6,
Foreground = new SolidColorBrush(FloatColor)
});
}
return show;
}
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
if (MustShowValue())
{
// making sure text on TextBlock is updated as per TextBox
var dotPos = Text.IndexOf('.');
var textPart1 = dotPos == -1 ? Text : Text.Substring(0, dotPos + 1);
var textPart2 = (dotPos == -1 || dotPos >= (Text.Length - 1)) ? null : Text.Substring(dotPos + 1);
_textBlock.Inlines.Clear();
_textBlock.Inlines.Add(new Run
{
Text = textPart1,
FontFamily = FontFamily,
FontSize = FontSize,
Foreground = Foreground
});
if (textPart2 != null)
_textBlock.Inlines.Add(new Run
{
Text = textPart2,
FontFamily = FontFamily,
TextDecorations = System.Windows.TextDecorations.Underline,
BaselineAlignment = BaselineAlignment.TextTop,
FontSize = FontSize * 5 / 6,
Foreground = new SolidColorBrush(FloatColor)
});
}
}
}
public static class HelperExtensions
{
public static T GetChildOfType<T>(this DependencyObject depObj) where T : DependencyObject
{
if (depObj == null) return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null) return result;
}
return null;
}
}
<Style x:Key="DecimalTextBoxGridStyle" TargetType="{x:Type local:DecimalTextBox}">
<Setter Property="TextAlignment" Value="Right"/>
<Setter Property="FloatColor" Value="Black"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ShowZeroValue" Value="False"/>
<Style.Triggers>
<!--<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}" Value="True">
<Setter Property="Foreground" Value="White"/>
</DataTrigger>-->
</Style.Triggers>
</Style>
You just need to update the inline(s) color (in inner TextBlock) every-time Foreground or FloatColor changes. The hard way would be add binding(s) between TextBox-properties and inline properties. The easy way would be to either add property changed callbacks to dependency property or just by overriding OnPropertyChanged:
For example (illustrates using OnPropertyChanged to keep control updated):
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ForegroundProperty || e.Property == FloatColorProperty)
UpdateTextBlock(); //updates the text-block inlines
}
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
UpdateTextBlock(); // this can be merged to OnPropertyChanged (as text is also dependency property)
}
// new extracted method from OnTextChanged
private void UpdateTextBlock()
{
if (MustShowValue())
{
// making sure text on TextBlock is updated as per TextBox
var dotPos = Text.IndexOf('.');
var textPart1 = dotPos == -1 ? Text : Text.Substring(0, dotPos + 1);
var textPart2 = (dotPos == -1 || dotPos >= (Text.Length - 1)) ? null : Text.Substring(dotPos + 1);
_textBlock.Inlines.Clear();
_textBlock.Inlines.Add(new Run
{
Text = textPart1,
FontFamily = FontFamily,
FontSize = FontSize,
Foreground = Foreground
});
if (textPart2 != null)
_textBlock.Inlines.Add(new Run
{
Text = textPart2,
FontFamily = FontFamily,
TextDecorations = System.Windows.TextDecorations.Underline,
BaselineAlignment = BaselineAlignment.TextTop,
FontSize = FontSize * 5 / 6,
Foreground = new SolidColorBrush(FloatColor)
});
}
}
Same goes for FontFamily and FontSize too. This way whenever any property, that UpdateTextBlock() uses, is updated - whether through style or triggers - the control will know that it needs to update the inline(s) in inner TextBlock.
Update 08/27
Also, update your Style to set Foreground and FloatColor in setters, while using a MultiDataTrigger to account for both row's selected status and grid's focused state.
<Style x:Key="DecimalTextBoxGridStyle" TargetType="{x:Type local:DecimalTextBox}">
<Setter Property="TextAlignment" Value="Right"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FloatColor" Value="Black"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ShowZeroValue" Value="False"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGridRow}}}" Value="True" />
<Condition Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}, Path=IsKeyboardFocusWithin}" Value="True" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FloatColor" Value="White"/>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</Style.Triggers>
</Style>
I have a customized vertical scrollbar which displays markers for selected items in a DataGrid.
The problem I'm facing is, when there are a great number of items (e.g. could be 5000 to 50000) there is a lag while it is rendering the markers.
With the following code it basically renders as per the selected items index, number of items and height of the track. Obviously this is inefficient and am looking for other solutions.
This is my customized vertical scrollbar
<helpers:MarkerPositionConverter x:Key="MarkerPositionConverter"/>
<ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition MaxHeight="18" />
<RowDefinition Height="0.00001*" />
<RowDefinition MaxHeight="18" />
</Grid.RowDefinitions>
<Border Grid.RowSpan="3"
CornerRadius="2"
Background="#F0F0F0" />
<RepeatButton Grid.Row="0"
Style="{StaticResource ScrollBarLineButton}"
Height="18"
Command="ScrollBar.LineUpCommand"
Content="M 0 4 L 8 4 L 4 0 Z" />
<!--START-->
<ItemsControl VerticalAlignment="Stretch" x:Name="ItemsSelected"
ItemsSource="{Binding ElementName=GenericDataGrid, Path=SelectedItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Fill="SlateGray" Width="9" Height="4">
<Rectangle.RenderTransform>
<TranslateTransform>
<TranslateTransform.Y>
<MultiBinding Converter="{StaticResource MarkerPositionConverter}" FallbackValue="-1000">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type DataGrid}}" />
<Binding Path="ActualHeight" ElementName="ItemsSelected"/>
<Binding Path="Items.Count" ElementName="GenericDataGrid"/>
</MultiBinding>
</TranslateTransform.Y>
</TranslateTransform>
</Rectangle.RenderTransform>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas ClipToBounds="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!--END-->
<Track x:Name="PART_Track" Grid.Row="1" IsDirectionReversed="true">
<Track.DecreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollBarPageButton}"
Command="ScrollBar.PageUpCommand" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumb}" Margin="1,0,1,0">
<Thumb.BorderBrush>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="{DynamicResource BorderLightColor}" Offset="0.0" />
<GradientStop Color="{DynamicResource BorderDarkColor}" Offset="1.0" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Thumb.BorderBrush>
<Thumb.Background>
<LinearGradientBrush StartPoint="0,0"
EndPoint="1,0">
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="{DynamicResource ControlLightColor}" Offset="0.0" />
<GradientStop Color="{DynamicResource ControlMediumColor}" Offset="1.0" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Thumb.Background>
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollBarPageButton}" Command="ScrollBar.PageDownCommand" />
</Track.IncreaseRepeatButton>
</Track>
<RepeatButton Grid.Row="3" Style="{StaticResource ScrollBarLineButton}" Height="18" Command="ScrollBar.LineDownCommand" Content="M 0 0 L 4 4 L 8 0 Z" />
</Grid>
</ControlTemplate>
This is my converter that transforms the Y position and scales accordingly if the DataGrid height changes.
public class MarkerPositionConverter: IMultiValueConverter
{
//Performs the index to translate conversion
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
//calculated the transform values based on the following
object o = (object)values[0];
DataGrid dg = (DataGrid)values[1];
double itemIndex = dg.Items.IndexOf(o);
double trackHeight = (double)values[2];
int itemCount = (int)values[3];
double translateDelta = trackHeight / itemCount;
return itemIndex * translateDelta;
}
catch (Exception ex)
{
Console.WriteLine("MarkerPositionConverter error : " + ex.Message);
return false;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
[RE-EDIT] I have tried to create a separate class for a marker canvas, for use with ObservableCollection's. Note that at present, this does not work.
XAML still the same as yesterday:
<helpers:MarkerCollectionCanvas
x:Name="SearchMarkerCanvas"
Grid.Row="1"
Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
MarkerCollection="{Binding Source={x:Static helpers:MyClass.Instance}, Path=SearchMarkers}"/>
Canvas class, ObservableCollection changed to use object instead of double, there is a console.writeline in MarkerCollectionCanvas_CollectionChanged that never gets called:
class MarkerCollectionCanvas : Canvas
{
public DataGrid Grid
{
get { return (DataGrid)GetValue(GridProperty); }
set { SetValue(GridProperty, value); }
}
public static readonly DependencyProperty GridProperty =
DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCollectionCanvas), new PropertyMetadata(null));
public ObservableCollection<object> MarkerCollection
{
get { return (ObservableCollection<object>)GetValue(MarkerCollectionProperty); }
set { SetValue(MarkerCollectionProperty, value); }
}
public static readonly DependencyProperty MarkerCollectionProperty =
DependencyProperty.Register("MarkerCollection", typeof(ObservableCollection<object>), typeof(MarkerCollectionCanvas), new PropertyMetadata(null, OnCollectionChanged));
private static void OnCollectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MarkerCollectionCanvas canvas = d as MarkerCollectionCanvas;
if (e.NewValue != null)
{
(e.NewValue as ObservableCollection<object>).CollectionChanged += canvas.MarkerCollectionCanvas_CollectionChanged;
}
if (e.OldValue != null)
{
(e.NewValue as ObservableCollection<object>).CollectionChanged -= canvas.MarkerCollectionCanvas_CollectionChanged;
}
}
void MarkerCollectionCanvas_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
Console.WriteLine("InvalidateVisual");
InvalidateVisual();
}
public Brush MarkerBrush
{
get { return (Brush)GetValue(MarkerBrushProperty); }
set { SetValue(MarkerBrushProperty, value); }
}
public static readonly DependencyProperty MarkerBrushProperty =
DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCollectionCanvas), new PropertyMetadata(Brushes.DarkOrange));
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || MarkerCollection == null)
return;
//Get all items
object[] items = new object[Grid.Items.Count];
Grid.Items.CopyTo(items, 0);
//Get all selected items
object[] selection = new object[MarkerCollection.Count];
MarkerCollection.CopyTo(selection, 0);
Dictionary<object, int> indexes = new Dictionary<object, int>();
for (int i = 0; i < selection.Length; i++)
{
indexes.Add(selection[i], 0);
}
int itemCounter = 0;
for (int i = 0; i < items.Length; i++)
{
object item = items[i];
if (indexes.ContainsKey(item))
{
indexes[item] = i;
itemCounter++;
}
if (itemCounter >= selection.Length)
break;
}
double translateDelta = ActualHeight / (double)items.Length;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);
foreach (int itemIndex in sortedIndex)
{
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
}
This is my singleton class with SearchMarkers in it:
public class MyClass : INotifyPropertyChanged
{
public static ObservableCollection<object> m_searchMarkers = new ObservableCollection<object>();
public ObservableCollection<object> SearchMarkers
{
get
{
return m_searchMarkers;
}
set
{
m_searchMarkers = value;
NotifyPropertyChanged();
}
}
private static MyClass m_Instance;
public static MyClass Instance
{
get
{
if (m_Instance == null)
{
m_Instance = new MyClass();
}
return m_Instance;
}
}
private MyClass()
{
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And this is a textbox text changed behavior. This is where the ObservableCollection SearchMarkers gets populated.
public class FindTextChangedBehavior : Behavior<TextBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.TextChanged += OnTextChanged;
}
protected override void OnDetaching()
{
AssociatedObject.TextChanged -= OnTextChanged;
base.OnDetaching();
}
private void OnTextChanged(object sender, TextChangedEventArgs args)
{
var textBox = (sender as TextBox);
if (textBox != null)
{
DataGrid dg = DataGridObject as DataGrid;
string searchValue = textBox.Text;
if (dg.Items.Count > 0)
{
var columnBoundProperties = new List<KeyValuePair<int, string>>();
IEnumerable<DataGridColumn> visibleColumns = dg.Columns.Where(c => c.Visibility == System.Windows.Visibility.Visible);
foreach (var col in visibleColumns)
{
if (col is DataGridTextColumn)
{
var binding = (col as DataGridBoundColumn).Binding as Binding;
columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
}
else if (col is DataGridComboBoxColumn)
{
DataGridComboBoxColumn dgcbc = (DataGridComboBoxColumn)col;
var binding = dgcbc.SelectedItemBinding as Binding;
columnBoundProperties.Add(new KeyValuePair<int, string>(col.DisplayIndex, binding.Path.Path));
}
}
Type itemType = dg.Items[0].GetType();
if (columnBoundProperties.Count > 0)
{
ObservableCollection<Object> tempItems = new ObservableCollection<Object>();
var itemsSource = dg.Items as IEnumerable;
Task.Factory.StartNew(() =>
{
ClassPropTextSearch.init(itemType, columnBoundProperties);
if (itemsSource != null)
{
foreach (object o in itemsSource)
{
if (ClassPropTextSearch.Match(o, searchValue))
{
tempItems.Add(o);
}
}
}
})
.ContinueWith(t =>
{
Application.Current.Dispatcher.Invoke(new Action(() => MyClass.Instance.SearchMarkers = tempItems));
});
}
}
}
}
public static readonly DependencyProperty DataGridObjectProperty =
DependencyProperty.RegisterAttached("DataGridObject", typeof(DataGrid), typeof(FindTextChangedBehavior), new UIPropertyMetadata(null));
public object DataGridObject
{
get { return (object)GetValue(DataGridObjectProperty); }
set { SetValue(DataGridObjectProperty, value); }
}
}
Here you go, I tried to attempt it for you.
Created a class MarkerCanvas deriving Canvas with a property to bind with the data grid
Attached SelectionChanged to listen to any change and requested the canvas to redraw itself by InvalidateVisual
overrided the method OnRender to take control of drawing and did the necessary check and calculation
finally rendered the rectangle on the calculated coordinates using the given brush
MarkerCanvas class
class MarkerCanvas : Canvas
{
public DataGrid Grid
{
get { return (DataGrid)GetValue(GridProperty); }
set { SetValue(GridProperty, value); }
}
// Using a DependencyProperty as the backing store for Grid. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GridProperty =
DependencyProperty.Register("Grid", typeof(DataGrid), typeof(MarkerCanvas), new PropertyMetadata(null, OnGridChanged));
private static void OnGridChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MarkerCanvas canvas = d as MarkerCanvas;
if (e.NewValue != null)
{
(e.NewValue as DataGrid).SelectionChanged += canvas.MarkerCanvas_SelectionChanged;
}
if (e.OldValue != null)
{
(e.NewValue as DataGrid).SelectionChanged -= canvas.MarkerCanvas_SelectionChanged;
}
}
void MarkerCanvas_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
InvalidateVisual();
}
public Brush MarkerBrush
{
get { return (Brush)GetValue(MarkerBrushProperty); }
set { SetValue(MarkerBrushProperty, value); }
}
// Using a DependencyProperty as the backing store for MarkerBrush. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MarkerBrushProperty =
DependencyProperty.Register("MarkerBrush", typeof(Brush), typeof(MarkerCanvas), new PropertyMetadata(Brushes.SlateGray));
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid==null || Grid.SelectedItems == null)
return;
object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
for (int i = 0; i < markers.Length; i++)
{
double itemIndex = Grid.Items.IndexOf(markers[i]);
double top = itemIndex * translateDelta;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
}
}
}
I have also adjusted the height of marker so it grows when there are less items, you can choose to fix it to specific value as per your needs
in XAML replace your items control with the new marker canvas with binding to the grid
<helpers:MarkerCanvas Grid.Row="1" Grid="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
helpers: is referring to WpfAppDataGrid.Helpers where I create the class, you can choose your own namespace
also you can bind the MarkerBrush property for your desired effet, which defaulted to SlateGray
rendering is pretty fast now, perhaps could make it more fast by doing some work on indexof method.
Also to skip some of the overlapping rectangles to be rendered you can change the method like this. little buggy as of now
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid==null || Grid.SelectedItems == null)
return;
object[] markers = Grid.SelectedItems.OfType<object>().ToArray();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
for (int i = 0; i < markers.Length; i++)
{
double itemIndex = Grid.Items.IndexOf(markers[i]);
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
Performance optimization
I tried to optimize the performance using a slight different approach, specially for the select all button
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || Grid.SelectedItems == null)
return;
object[] items = new object[Grid.Items.Count];
Grid.Items.CopyTo(items, 0);
object[] selection = new object[Grid.SelectedItems.Count];
Grid.SelectedItems.CopyTo(selection, 0);
Dictionary<object, int> indexes = new Dictionary<object, int>();
for (int i = 0; i < selection.Length; i++)
{
indexes.Add(selection[i], 0);
}
int itemCounter = 0;
for (int i = 0; i < items.Length; i++)
{
object item = items[i];
if (indexes.ContainsKey(item))
{
indexes[item] = i;
itemCounter++;
}
if (itemCounter >= selection.Length)
break;
}
double translateDelta = ActualHeight / (double)items.Length;
double width = ActualWidth;
double height = Math.Max(translateDelta, 4);
Brush dBrush = MarkerBrush;
double previous = 0;
IEnumerable<int> sortedIndex = indexes.Values.OrderBy(v => v);
foreach (int itemIndex in sortedIndex)
{
double top = itemIndex * translateDelta;
if (top < previous)
continue;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
previous = (top + height) - 1;
}
}
Reflection approach
in this I have tried to get the underlying selection list and attempted to retrieve the selected indexes from the same, also added even more optimization when doing select all
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (Grid == null || Grid.SelectedItems == null)
return;
List<int> indexes = new List<int>();
double translateDelta = ActualHeight / (double)Grid.Items.Count;
double height = Math.Max(translateDelta, 4);
int itemInOneRect = (int)Math.Floor(height / translateDelta);
itemInOneRect -= (int)(itemInOneRect * 0.2);
if (Grid.SelectedItems.Count == Grid.Items.Count)
{
for (int i = 0; i < Grid.Items.Count; i += itemInOneRect)
{
indexes.Add(i);
}
}
else
{
FieldInfo fi = Grid.GetType().GetField("_selectedItems", BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
IEnumerable<object> internalSelectionList = fi.GetValue(Grid) as IEnumerable<object>;
PropertyInfo pi = null;
int lastIndex = int.MinValue;
foreach (var item in internalSelectionList)
{
if (pi == null)
{
pi = item.GetType().GetProperty("Index", BindingFlags.Instance | BindingFlags.NonPublic);
}
int newIndex = (int)pi.GetValue(item);
if (newIndex > (lastIndex + itemInOneRect))
{
indexes.Add(newIndex);
lastIndex = newIndex;
}
}
indexes.Sort();
}
double width = ActualWidth;
Brush dBrush = MarkerBrush;
foreach (int itemIndex in indexes)
{
double top = itemIndex * translateDelta;
dc.DrawRectangle(dBrush, null, new Rect(0, top, width, height));
}
}