Maintaining fixed-thickness lines in WPF with View

2019-02-09 23:32发布

问题:

I have a <Grid> which contains some vertical and horizontal <Line>s. I want the grid to be scalable with the window size, and retain its aspect ratio, so it's contained in a <Viewbox Stretch="Uniform">.

However, I also want the lines to always render with a width of 1 pixel, so I use:

Line line = new Line();
line.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
// other line settings here...

This makes the lines' initial appearance ideal, but as soon as you start resizing the window, the stretching/scaling kicks in, and the lines become a mixture of 1 and 2 pixels thick again.

Is there any way to have the lines always be 1 pixel thick and also allow for resizing of the window/grid?

Update - Using path geometry as per Clemens' suggestion

@Clemens - Thanks for highlighting the rendering differences between lines and paths. As I try to rework my code using your example, I'm getting that sinking feeling that I'm digging more holes for myself and not really grasping the entire concept (entirely my fault, not yours, I'm just new to WPF).

I'll add some screenshots to illustrate the following description:

I'm making a game board (for the game of Go, in case that helps understand the layout at all). I have a 9x9 grid, and I'm planning on placing the game pieces by simply adding an ellipse to a particular grid cell.

To draw the underlying lines on the board, however, I need to draw lines intersecting the middle of the cells across the board (in Go, pieces are placed on the intersections, not the middle of the cells).

It could well be that I'm taking entirely the wrong approach, please feel free to tell me to start again down a different route, rather than hacking around within the current structure.

This is how I've done it so far (I'm adding the paths programatically, due to the way the coordinates are calculated. Not sure if it can all be done in XAML):

XAML:

<Grid MinHeight="400" MinWidth="400" ShowGridLines="False" x:Name="boardGrid">
    <Grid.Resources>
        <ScaleTransform x:Key="transform"
            ScaleX="{Binding ActualWidth, ElementName=boardGrid}"
            ScaleY="{Binding ActualHeight, ElementName=boardGrid}" />
    </Grid.Resources>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <!-- more rows, 9 in total -->
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <!-- more columns, 9 in total -->
    </Grid.ColumnDefinitions>

    <!-- example game pieces -->
    <Ellipse Stroke="Black" Fill="#333333" Grid.Row="3" Grid.Column="2" />
    <Ellipse Stroke="#777777" Fill="#FFFFFF" Grid.Row="4" Grid.Column="4" />
</Grid>

C#:

int cols = 9;
int rows = 9;

// Draw horizontal lines
for (int row = 0; row < rows; row++)
{
    var path = new System.Windows.Shapes.Path();
    path.Stroke = Brushes.Black;
    path.StrokeThickness = 1;
    path.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
    Grid.SetRow(path, row);
    Grid.SetColumnSpan(path, cols);
    Grid.SetZIndex(path, -1);

    double cellWidth = boardGrid.ColumnDefinitions[0].ActualWidth;
    double cellHeight = boardGrid.RowDefinitions[0].ActualHeight;
    double x1 = (cellWidth / 2) / boardGrid.ActualWidth;
    double y1 = (cellHeight / 2) / boardGrid.ActualHeight;
    double x2 = ((cellWidth * cols) - (cellWidth / 2)) / boardGrid.ActualWidth;
    double y2 = (cellHeight / 2) / boardGrid.ActualHeight;

    path.Data = new LineGeometry(new Point(x1, y1),
                                 new Point(x2, y2), 
                           (ScaleTransform)boardGrid.TryFindResource("transform"));
    boardGrid.Children.Add(path);
}

// Similar loop code follows for vertical lines...

This is what I get when using the code above

This is pretty much how I want it to look. It's raised 2 more questions for me:

1) Am I taking the right approach where I'm calculating the x1, x2, y1 and y2 values by diving them by the total board width to create a number between 0 and 1, so that the ScaleTransform can then be applied to them?

2) Now that I'm not using a Viewbox any more, how do I accomplish fixed-ratio scaling? If I enlarge my window, the board stretches out of proportion (see image below). (It doesn't anti-alias the lines any more though, which is great.)

I know this is getting to be a bit of a monolithic post. I'm very grateful for your patience and responses.

回答1:

A Viewbox can only "visually" scale its child element, including the thickness of any rendered stroke. What you need is a scaling that only applies to the geometry of the lines (or other shapes), but leaves the stroke unaffected.

Instead of using Line objects, you could draw your lines by Path objects that use transformed LineGeometries for their Data property. You could create a ScaleTransform that scales from logical coordinates to viewport coordinates by using the Grid's width and height as scaling factors in x and y direction. Each LineGeometry (or any other Geometry) would use logical coordinates in the range 0..1:

<Grid x:Name="grid">
    <Grid.Resources>
        <ScaleTransform x:Key="transform"
                        ScaleX="{Binding ActualWidth, ElementName=grid}"
                        ScaleY="{Binding ActualHeight, ElementName=grid}"/>
    </Grid.Resources>
    <Path Stroke="Black" StrokeThickness="1">
        <Path.Data>
            <LineGeometry StartPoint="0.1,0.1" EndPoint="0.9,0.9"
                          Transform="{StaticResource transform}"/>
        </Path.Data>
    </Path>
</Grid>

In order to get a uniform scaling you may simply bind both the ScaleTransform's ScaleX and ScaleY properties to either the ActualWidth or ActualHeight of the Grid:

<Grid x:Name="grid">
    <Grid.Resources>
        <ScaleTransform x:Key="transform"
                        ScaleX="{Binding ActualWidth, ElementName=grid}"
                        ScaleY="{Binding ActualWidth, ElementName=grid}"/>
    </Grid.Resources>
    ...
</Grid>

You may also calculate the uniform scaling factor from the minimum value of the width and height, with a bit of code behind:

<Grid x:Name="grid" SizeChanged="grid_SizeChanged">
    <Grid.Resources>
        <ScaleTransform x:Key="transform"/>
    </Grid.Resources>
    ...
</Grid>

with a SizeChanged handler like this:

private void grid_SizeChanged(object sender, SizeChangedEventArgs e)
{
    var transform = grid.Resources["transform"] as ScaleTransform;
    var minScale = Math.Min(grid.ActualWidth, grid.ActualHeight);
    transform.ScaleX = minScale;
    transform.ScaleY = minScale;
}