0
votes

What I'm Trying to Achieve

For simplicity, I have a grid of cells and I'm trying to add the FocusVisualStyle dashed rectangle to each cell when it is focused. However the style only works when the element is navigated to using the keyboard, per its design. I'm looking to achieve the same effect, but have it work with Keyboard focus and Mouse focus (when the cell is clicked).

What I Have Tried

I looked into using containers like a Border, but that doesn't have dashed lines, attempted to use a dashed rectangle and position it over the element, but that was providing inconsistent results. I also tried to attach a trigger to the IsFocused property of the cell, but that only works for Keyboard Focus.

Current Code

Right now I have events set to the cell (a StackPanel) that allow for my custom navigation through the grid. My current "Visual Style" is to change the background to a color, which works for both Mouse and Keyboard Focus. I'm looking to swap out the background change for a dashed rectangle around the cell, however the XAML I tried doesn't work since FocusVisualStyle only works with keyboard focus.

Here is a simplified version of my XAML and C# of what I have tried

XAML

<!-- The "cell" I'm trying to acheive a dashed border around -->
<StackPanel x:Key="ContactCell" 
            Focusable="True" 
            GotFocus="StackPanel_GotFocus" 
            LostFocus="StackPanel_LostFocus" 
            PreviewMouseDown="Contact_Select" 
            Style="{DynamicResource ContactFocusStyle}">
    <!-- other children in here -->
</StackPanel>

<Style x:Key="ContactFocusStyle" TargetType="StackPanel">
        <Style.Triggers>
            <Trigger Property="IsFocused" Value="True">
                <Setter Property="FocusVisualStyle" 
                        Value="{DynamicResource MyFocusVisualStyle}"/>
            </Trigger>
        </Style.Triggers>
</Style>

<Style x:Key="MyFocusVisualStyle">
        <Setter Property="Control.Template">
            <Setter.Value>
                <ControlTemplate>
                    <Rectangle Stroke="Black" 
                               StrokeDashArray="2 3" 
                               Fill="Transparent" 
                               StrokeDashCap="Round" 
                               RadiusX="3" 
                               RadiusY="3"/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
</Style>

C#

    private void Contact_Select(object send, MouseButtonEventArgs e)
    {
        StackPanel sender = (StackPanel)send;
        sender.Focus();
    }

    private void StackPanel_GotFocus(object sender, RoutedEventArgs e)
    {
        StackPanel s = (StackPanel)sender;
        s.Background = Brushes.Red;
    }

    private void StackPanel_LostFocus(object sender, RoutedEventArgs e)
    {
        StackPanel s = (StackPanel)sender;
        s.Background = Brushes.Transparent;
    }
2

2 Answers

1
votes

I see two issues in the implementation. First of all its hard to capture mouse-events in a StackPanel with transparent background. Secondly, the FocusVisualStyle only works when the last input is from the keyboard.

To achieve similar visual effect - you can use an adorner to implement the focus-style. For example - you can define an adorner like this:

public class FocusAdorner : Adorner
{
    // Be sure to call the base class constructor.
    public FocusAdorner(UIElement adornedElement)
      : base(adornedElement)
    {
        IsHitTestVisible = false;
    }

    // A common way to implement an adorner's rendering behavior is to override the OnRender
    // method, which is called by the layout system as part of a rendering pass.
    protected override void OnRender(DrawingContext drawingContext)
    {
        var drawRect = LayoutInformation.GetLayoutSlot((FrameworkElement)this.AdornedElement);
        drawRect = new Rect(1, 1, drawRect.Width - 2, drawRect.Height - 2);


        // Some arbitrary drawing implements.
        SolidColorBrush renderBrush = new SolidColorBrush(Colors.Transparent);
        Pen renderPen = new Pen(new SolidColorBrush(Colors.Black), 2);
        renderPen.DashStyle = new DashStyle(new double[] { 2, 3 }, 0);

        drawingContext.DrawRoundedRectangle(renderBrush, renderPen, drawRect, 3, 3);
    }
}

Sample Usage - XAML

<Window.Resources>
    <!-- The "cell" I'm trying to acheive a dashed border around -->

    <Style x:Key="ContactFocusStyle" TargetType="StackPanel">
        <Setter Property="Background" Value="White" />
        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
        <Style.Triggers>
            <Trigger Property="IsFocused" Value="True">
                <Setter Property="Background" Value="Red" />
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>
<Grid Margin="15">
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <StackPanel
        Grid.Row="0"
        Grid.Column="0"
        Focusable="True" 
        GotFocus="Got_Focus"
        LostFocus="Lost_Focus"
        PreviewMouseDown="Contact_Select"
        Style="{DynamicResource ContactFocusStyle}">
        <!-- other children in here -->
    </StackPanel>
    <StackPanel
        Grid.Row="0"
        Grid.Column="1"
        Focusable="True" 
        GotFocus="Got_Focus"
        LostFocus="Lost_Focus"
        PreviewMouseDown="Contact_Select"
        Style="{DynamicResource ContactFocusStyle}">
        <!-- other children in here -->
    </StackPanel>
    <StackPanel
        Grid.Row="1"
        Grid.Column="0"
        Focusable="True" 
        GotFocus="Got_Focus"
        LostFocus="Lost_Focus"
        PreviewMouseDown="Contact_Select"
        Style="{DynamicResource ContactFocusStyle}">

        <!-- other children in here -->
    </StackPanel>
    <StackPanel
        Grid.Row="1"
        Grid.Column="1"
        Focusable="True" 
        GotFocus="Got_Focus"
        LostFocus="Lost_Focus"
        PreviewMouseDown="Contact_Select"
        Style="{DynamicResource ContactFocusStyle}">

        <!-- other children in here -->
    </StackPanel>
</Grid>

and the code-behind

private void Contact_Select(object send, MouseButtonEventArgs e)
{
    var sender = (StackPanel)send;
    Keyboard.Focus(sender);
}

private void Got_Focus(object send, RoutedEventArgs e)
{
    var sender = (StackPanel)send;
    AdornerLayer.GetAdornerLayer(sender).Add(new FocusAdorner(sender));
}

private void Lost_Focus(object send, RoutedEventArgs e)
{
    var sender = (StackPanel)send;
    var layer = AdornerLayer.GetAdornerLayer(sender);
    foreach (var adorner in layer.GetAdorners(sender))
        layer.Remove(adorner);
}

** Output **

enter image description here

0
votes

The best way to do this I believe is to create a selectable panel that handles focus for both mouse and keyboard events. Use the panel to wrap whatever element you want to display a focus rectangle around.

The panel...

 public class SelectablePanel : Panel
{
    public static readonly DependencyProperty FocusBrushProperty = DependencyProperty.Register("FocusBrush", typeof(Brush), typeof(SelectablePanel),
        new PropertyMetadata(Brushes.Red));
    public static readonly DependencyProperty FocusThicknessProperty = DependencyProperty.Register("FocusThickness", typeof(double), typeof(SelectablePanel),
        new PropertyMetadata(1.0));
    public Brush FocusBrush
    {
        get => (Brush)GetValue(FocusBrushProperty);
        set => SetValue(FocusBrushProperty, value);
    }
    public double FocusThickness
    {
        get => (double)GetValue(FocusThicknessProperty);
        set => SetValue(FocusThicknessProperty, value);
    }
    static SelectablePanel()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(SelectablePanel), new FrameworkPropertyMetadata(typeof(SelectablePanel)));
        FocusableProperty.OverrideMetadata(typeof(SelectablePanel), new FrameworkPropertyMetadata(true));
        BackgroundProperty.OverrideMetadata(typeof(SelectablePanel), new FrameworkPropertyMetadata(Brushes.White));
    }
    protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        Focus();
        e.Handled = true;
    }
    protected override void OnGotFocus(RoutedEventArgs e)
    {
        InvalidateVisual();
        base.OnGotFocus(e);
    }
    protected override void OnLostFocus(RoutedEventArgs e)
    {
        InvalidateVisual();
        base.OnLostFocus(e);
    }
    protected override Size MeasureOverride(Size availableSize)
    {
        if (Background == Brushes.Transparent) throw new InvalidOperationException("Selectable panel cannot be transparent.");
        if (InternalChildren.Count > 1) throw new InvalidOperationException("SelectablePanel takes only a single child control.");
        Size panelDesiredSize = new Size();
        foreach (UIElement child in InternalChildren)
        {
            child.Measure(availableSize);
            panelDesiredSize = child.DesiredSize;
        }
        return panelDesiredSize;
    }
    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement child in InternalChildren)
        {
            double x = (finalSize.Width - child.DesiredSize.Width)/ 2;
            double y = (finalSize.Height - child.DesiredSize.Height) / 2;
            child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
        }
        return finalSize;
    }
    protected override void OnRender(DrawingContext dc)
    {
        base.OnRender(dc);
        if (IsFocused)
        {
            var rect = LayoutInformation.GetLayoutSlot(this);
            var r = new Rect(1, 1 + Margin.Top, rect.Width - 2 - Margin.Left - Margin.Right, rect.Height - 2 - Margin.Top - Margin.Bottom);
            SolidColorBrush b = new SolidColorBrush(Colors.Transparent);
            Pen p = new Pen(FocusBrush, FocusThickness);
            dc.DrawRectangle(b, p, r);
        }
    }
}

Example usage...

    <StackPanel>
    <Label Margin="10,0,0,0">Text Bex 1:</Label>
    <TextBox Margin="10,0,10,0" HorizontalAlignment="Stretch" Text="Text Box 1" />
    <Label Margin="10,0,0,0">Image:</Label>
    <local:SelectablePanel Margin="10,0,10,0">
        <Image Source="Images/globe.png" Height="100"/>
    </local:SelectablePanel>
    <Label Margin="10,0,0,0">Text Box 2:</Label>
    <TextBox Margin="10,0,10,0" HorizontalAlignment="Stretch" Text="Text Box 2"/>
</StackPanel>

SelectablePanel with focus