Issues with alignment with selection in a listbox

2019-07-23 12:15发布

问题:

I'm looking at creating a list of rectangles that specify their location by an x,y coordinate, however I'm seeing an issue with the alignment of the graphics. I'm using a list box with a custom layout panel.

Here's the XAML for the main window:

<Window x:Class="WpfFunkyPanel.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfFunkyPanel"
    xmlns:system="clr-namespace:System;assembly=mscorlib"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <system:Double x:Key="barWidth">30</system:Double>
    <system:Double x:Key="trackHeight">24</system:Double>
    <Style x:Key="PatternGridStyle" TargetType="{x:Type ListBoxItem}">
        <Setter Property="local:GridPanel.Bar" Value="{Binding Bar}"/>
        <Setter Property="local:GridPanel.Track" Value="{Binding Track}"/>
    </Style>
</Window.Resources>
<Grid>
    <ListBox ItemsSource="{Binding Items}" ItemContainerStyle="{StaticResource PatternGridStyle}">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <local:GridPanel VerticalAlignment="Top" BarWidth="{StaticResource barWidth}" TrackHeight="{StaticResource trackHeight}">
                    <local:GridPanel.Background>
                        <DrawingBrush TileMode="Tile" ViewboxUnits="Absolute" ViewportUnits="Absolute">
                            <DrawingBrush.Viewbox>
                                <Rect X="0" Y="0" Width="{StaticResource barWidth}" Height="{StaticResource trackHeight}"/>
                            </DrawingBrush.Viewbox>
                            <DrawingBrush.Viewport>
                                <Rect X="1" Y="0" Width="{StaticResource barWidth}" Height="{StaticResource trackHeight}"/>
                            </DrawingBrush.Viewport>
                            <DrawingBrush.Drawing>
                                <GeometryDrawing Brush="LightGray">
                                    <GeometryDrawing.Pen>
                                        <Pen Brush="Black" Thickness="1"/>
                                    </GeometryDrawing.Pen>
                                    <GeometryDrawing.Geometry>
                                        <PathGeometry>
                                            <PathFigure IsFilled="True">
                                                <LineSegment>
                                                    <LineSegment.Point>
                                                        <Point X="{StaticResource barWidth}" Y="0"/>
                                                    </LineSegment.Point>
                                                </LineSegment>
                                                <LineSegment>
                                                    <LineSegment.Point>
                                                        <Point X="{StaticResource barWidth}" Y="{StaticResource trackHeight}"/>
                                                    </LineSegment.Point>
                                                </LineSegment>
                                                <LineSegment>
                                                    <LineSegment.Point>
                                                        <Point X="0" Y="{StaticResource trackHeight}"/>
                                                    </LineSegment.Point>
                                                </LineSegment>
                                                <LineSegment/>
                                            </PathFigure>
                                        </PathGeometry>
                                    </GeometryDrawing.Geometry>
                                </GeometryDrawing>
                            </DrawingBrush.Drawing>
                        </DrawingBrush>
                    </local:GridPanel.Background>
                </local:GridPanel>
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid Margin="0,1,0,1">
                    <Rectangle Fill="Gray" Width="28" Height="22"></Rectangle>
                    <TextBlock Text="{Binding Text}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>
</Window>

Here's the grid panel:

using System;

namespace WpfFunkyPanel
{
using System.Windows;
using System.Windows.Controls;

public class GridPanel : Panel
{
    public static readonly DependencyProperty BarProperty = DependencyProperty.RegisterAttached(
        "Bar",
        typeof(int),
        typeof(GridPanel));

    public static readonly DependencyProperty TrackProperty = DependencyProperty.RegisterAttached(
        "Track",
        typeof(int),
        typeof(GridPanel));

    public static readonly DependencyProperty BarWidthProperty = DependencyProperty.Register(
        "BarWidth",
        typeof(double),
        typeof(GridPanel),
        new FrameworkPropertyMetadata(
            28.0,
            FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));

    public static readonly DependencyProperty TrackHeightProperty = DependencyProperty.Register(
        "TrackHeight",
        typeof(double),
        typeof(GridPanel),
        new FrameworkPropertyMetadata(
            24.0,
            FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));

    public static int GetBar(DependencyObject obj)
    {
        return (int)obj.GetValue(BarProperty);
    }

    public static void SetBar(DependencyObject obj, int value)
    {
        obj.SetValue(BarProperty, value);
    }

    public static int GetTrack(DependencyObject obj)
    {
        return (int)obj.GetValue(TrackProperty);
    }

    public static void SetTrack(DependencyObject obj, int value)
    {
        obj.SetValue(TrackProperty, value);
    }

    public GridPanel()
    {

    }

    protected override Size MeasureOverride(Size availableSize)
    {
        Size size = new Size();

        foreach (UIElement child in InternalChildren)
        {
            int bar = GetBar(child);
            int track = GetTrack(child);

            double width = (bar + 1) * BarWidth;
            double height = (track + 1) * TrackHeight;

            size.Width = Math.Max(size.Width, width);
            size.Height = Math.Max(size.Height, height);
        }

        return size;
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement child in InternalChildren)
        {
            int bar = GetBar(child);
            int track = GetTrack(child);

            Rect rect = new Rect(bar * BarWidth, track * TrackHeight, BarWidth, TrackHeight);
            child.Arrange(rect);
        }

        return finalSize;
    }

    public double BarWidth
    {
        get
        {
            return (double)this.GetValue(BarWidthProperty);
        }

        set
        {
            SetValue(BarWidthProperty, value);
        }
    }

    public double TrackHeight
    {
        get
        {
            return (double)this.GetValue(TrackHeightProperty);
        }

        set
        {
            SetValue(TrackHeightProperty, value);
        }
    }
}
}

Here's the pattern item:

namespace WpfFunkyPanel
{
public class PatternItem
{
    private readonly int bar;

    private readonly int track;

    private readonly string text;

    public PatternItem(int bar, int track, string text)
    {
        this.bar = bar;
        this.track = track;
        this.text = text;
    }

    public int Bar
    {
        get
        {
            return bar;
        }
    }

    public int Track
    {
        get
        {
            return track;
        }
    }

    public string Text
    {
        get
        {
            return text;
        }
    }
}
}

Here's the collection view model:

namespace WpfFunkyPanel
{
using System.Collections.ObjectModel;

public class PatternContainer
{
    private int bars;

    private int tracks;

    private ObservableCollection<PatternItem> items; 

    public PatternContainer()
    {
        bars = 32;
        tracks = 24;
        items = new ObservableCollection<PatternItem>();
    }

    public void Add(PatternItem item)
    {
        items.Add(item);
    }

    public int Bars
    {
        get
        {
            return bars;
        }
    }

    public int Tracks
    {
        get
        {
            return tracks;
        }
    }

    public ObservableCollection<PatternItem> Items
    {
        get
        {
            return items;
        }
    }
}
}

Finally, the main window code behind:

using System.Windows;

namespace WpfFunkyPanel
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        var container = new PatternContainer();

        for (int bar = 0; bar < 16; bar++)
        {
            container.Add(new PatternItem(bar, 0, "A" + (bar + 1).ToString()));
        }

        container.Add(new PatternItem(3, 2, "C4"));
        container.Add(new PatternItem(5, 5, "G6"));

        DataContext = container;
    }
}
}

Here's what it looks like, with a rectangle selected:

The problem is the selection box in blue is offset to the left. Also, in order to get things to align, I had to set the viewport to 1,0,w,h not 0,0,w,h as I would have expected.

回答1:

Found a solution - create my own controls GridControl (derived from Selector) and GridItemControl (ContentControl) and now it all makes sense.

Here's my GridControlItem:

public class GridControlItem : ContentControl
{
    public static readonly DependencyProperty BarProperty = DependencyProperty.Register(
        "Bar",
        typeof(int),
        typeof(GridControlItem),
        new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public static readonly DependencyProperty TrackProperty = DependencyProperty.Register(
        "Track",
        typeof(int),
        typeof(GridControlItem),
        new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public static readonly DependencyProperty PickedProperty = DependencyProperty.Register(
        "Picked",
        typeof(bool),
        typeof(GridControlItem),
        new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public int Bar
    {
        get
        {
            return (int)this.GetValue(BarProperty);
        }

        set
        {
            SetValue(BarProperty, value);
        }
    }

    public int Track
    {
        get
        {
            return (int)this.GetValue(TrackProperty);
        }

        set
        {
            SetValue(TrackProperty, value);
        }
    }

    public bool Picked
    {
        get
        {
            return (bool)this.GetValue(PickedProperty);
        }

        set
        {
            SetValue(PickedProperty, value);
        }
    }

    static GridControlItem()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(GridControlItem), new FrameworkPropertyMetadata(typeof(GridControlItem)));
    }

    protected override void OnMouseDown(MouseButtonEventArgs e)
    {
        base.OnMouseDown(e);

        if (e.ChangedButton == MouseButton.Left)
        {
            this.Picked = true;
            e.Handled = true;
        }
    }
}

I deliberately used "Picked" instead of "IsSelected" to make sure this works.

Now the GridControl:

public class GridControl : Selector
{
    static GridControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(GridControl), new FrameworkPropertyMetadata(typeof(GridControl)));
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new GridControlItem();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return (item is GridControlItem);
    }
}

and the Generic.xaml:

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PatternControlLibrary"
xmlns:system="clr-namespace:System;assembly=mscorlib">

<system:Double x:Key="barWidth">30</system:Double>
<system:Double x:Key="trackHeight">24</system:Double>

<Style TargetType="{x:Type local:GridControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:GridControl}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
                        <ItemsPresenter />
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <local:GridPanel VerticalAlignment="Top" BarWidth="{StaticResource barWidth}" TrackHeight="{StaticResource trackHeight}">
                    <local:GridPanel.Background>
                        <DrawingBrush TileMode="Tile" ViewboxUnits="Absolute" ViewportUnits="Absolute">
                            <DrawingBrush.Viewbox>
                                <Rect X="0" Y="0" Width="{StaticResource barWidth}" Height="{StaticResource trackHeight}"/>
                            </DrawingBrush.Viewbox>
                            <DrawingBrush.Viewport>
                                <Rect X="0" Y="0" Width="{StaticResource barWidth}" Height="{StaticResource trackHeight}"/>
                            </DrawingBrush.Viewport>
                            <DrawingBrush.Drawing>
                                <GeometryDrawing Brush="LightGray">
                                    <GeometryDrawing.Pen>
                                        <Pen Brush="Black" Thickness="1"/>
                                    </GeometryDrawing.Pen>
                                    <GeometryDrawing.Geometry>
                                        <PathGeometry>
                                            <PathFigure IsFilled="True">
                                                <LineSegment>
                                                    <LineSegment.Point>
                                                        <Point X="{StaticResource barWidth}" Y="0"/>
                                                    </LineSegment.Point>
                                                </LineSegment>
                                                <LineSegment>
                                                    <LineSegment.Point>
                                                        <Point X="{StaticResource barWidth}" Y="{StaticResource trackHeight}"/>
                                                    </LineSegment.Point>
                                                </LineSegment>
                                                <LineSegment>
                                                    <LineSegment.Point>
                                                        <Point X="0" Y="{StaticResource trackHeight}"/>
                                                    </LineSegment.Point>
                                                </LineSegment>
                                                <LineSegment/>
                                            </PathFigure>
                                        </PathGeometry>
                                    </GeometryDrawing.Geometry>
                                </GeometryDrawing>
                            </DrawingBrush.Drawing>
                        </DrawingBrush>
                    </local:GridPanel.Background>
                </local:GridPanel>
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style TargetType="{x:Type local:GridControlItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:GridControlItem}">
                <Border x:Name="Border">
                    <ContentPresenter />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="Picked" Value="True">
                        <Setter TargetName="Border" Property="Background" Value="Red"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="local:GridPanel.Bar" Value="{Binding Bar}"/>
    <Setter Property="local:GridPanel.Track" Value="{Binding Track}"/>
</Style>
</ResourceDictionary>

Here's the MainWindow.xaml:

<Window x:Class="WpfFunkyPanel.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:system="clr-namespace:System;assembly=mscorlib"
    xmlns:cc="clr-namespace:PatternControlLibrary;assembly=PatternControlLibrary"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>

    <DataTemplate x:Key="dataItemTemplate">
        <Grid>
            <Rectangle Fill="LightPink" Width="28" Height="22"/>
            <TextBlock Text="{Binding Text}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        </Grid>
    </DataTemplate>
</Window.Resources>

<Grid>
    <cc:GridControl 
        ItemsSource="{Binding Items}"
        ItemTemplate="{StaticResource dataItemTemplate}"/>
</Grid>
</Window>