1
votes

I'm experimenting with the concept of drawing grid lines over a control and was wondering what adjustments I might need to make to make this actually work. I found some code on another post that enables grid lines to be drawn OnRender over a canvas. Here's what that looks like:

public class MyCanvas : Canvas
{
    public bool IsGridVisible = true;

    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        base.OnRender(dc);

        if (IsGridVisible)
        {
            // Draw GridLines
            Pen pen = new Pen(Brushes.Black, 1);
            pen.DashStyle = DashStyles.Solid;

            for (double x = 0; x < this.ActualWidth; x += 2)
            {
                dc.DrawLine(pen, new Point(x, 0), new Point(x, this.ActualHeight));
            }

            for (double y = 0; y < this.ActualHeight; y += 2)
            {
                dc.DrawLine(pen, new Point(0, y), new Point(this.ActualWidth, y));
            }
        }
    }

    public MyCanvas()
    {
        DefaultStyleKey = typeof(MyCanvas);
    }
}

This part: y += 2 indicates how many other pixels/points to wait before drawing next line, though I am uncertain of it's correctness.

Here's the xaml:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="30"/>
    </Grid.RowDefinitions>
    <ScrollViewer>
        <local:MyCanvas>
            <local:MyCanvas.LayoutTransform>
                <ScaleTransform ScaleX="{Binding ElementName=Slider, Path=Value}" ScaleY="{Binding ElementName=Slider, Path=Value}"/>
            </local:MyCanvas.LayoutTransform>
            <Image Canvas.Top="2" Canvas.Left="2"  Source="C:\Users\Me\Pictures\nyan-wallpaper2.jpg" Width="325" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
        </local:MyCanvas>
    </ScrollViewer>
    <Slider x:Name="Slider" Maximum="500" Grid.Row="1" Value="1"/>
</Grid>

Here are screenshots of what the above results in.

enter image description here

As you can see, the grid lines change in size as you zoom and the lines themselves do not snap around each individual pixel. I highlighted an example pixel in red to show how small the lines should be versus how they actually are.

I read that the thickness of the pen should be divided by the scale value, however, I tested this by replacing Pen pen = new Pen(Brushes.Black, 1); with Pen pen = new Pen(Brushes.Black, 1 / 3); and set the ScaleX and ScaleY of MyCanvas to 3. At that point, no lines showed at all.

Any help at all is immensely valued!

1

1 Answers

2
votes

Got it working like this for anyone curious:

MainWindow.xaml.cs

namespace Test
{
    public class MyCanvas : Canvas
    {
        public bool IsGridVisible = false;

        #region Dependency Properties

        public static DependencyProperty ZoomValueProperty = DependencyProperty.Register("ZoomValue", typeof(double), typeof(MyCanvas), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnZoomValueChanged));
        public double ZoomValue
        {
            get
            {
                return (double)GetValue(ZoomValueProperty);
            }
            set
            {
                SetValue(ZoomValueProperty, value);
            }
        }
        private static void OnZoomValueChanged(DependencyObject Object, DependencyPropertyChangedEventArgs e)
        {
        }

        #endregion

        protected override void OnRender(System.Windows.Media.DrawingContext dc)
        {
            base.OnRender(dc);
            IsGridVisible = ZoomValue > 4.75 ? true : false;
            if (IsGridVisible)
            {
                // Draw GridLines
                Pen pen = new Pen(Brushes.Black, 1 / ZoomValue);
                pen.DashStyle = DashStyles.Solid;

                for (double x = 0; x < this.ActualWidth; x += 1)
                {
                    dc.DrawLine(pen, new Point(x, 0), new Point(x, this.ActualHeight));
                }

                for (double y = 0; y < this.ActualHeight; y += 1)
                {
                    dc.DrawLine(pen, new Point(0, y), new Point(this.ActualWidth, y));
                }
            }
        }

        public MyCanvas()
        {
            DefaultStyleKey = typeof(MyCanvas);
        }
    }

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

        private WriteableBitmap bitmap = new WriteableBitmap(500, 500, 96d, 96d, PixelFormats.Bgr24, null);

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            int size = 1;

            Random rnd = new Random(DateTime.Now.Millisecond);
            bitmap.Lock(); // Lock() and Unlock() could be moved to the DrawRectangle() method. Just do some performance tests.

            for (int y = 0; y < 500; y++)
            {
                for (int x = 0; x < 500; x++)
                {
                    byte colR = (byte)rnd.Next(256);
                    byte colG = (byte)rnd.Next(256);
                    byte colB = (byte)rnd.Next(256);

                    DrawRectangle(bitmap, size * x, size * y, size, size, Color.FromRgb(colR, colG, colB));
                }
            }

            bitmap.Unlock(); // Lock() and Unlock() could be moved to the DrawRectangle() method. Just do some performance tests.
            Image.Source = bitmap; // This should be done only once
        }

        public void DrawRectangle(WriteableBitmap writeableBitmap, int left, int top, int width, int height, Color color)
        {
            // Compute the pixel's color
            int colorData = color.R << 16; // R
            colorData |= color.G << 8; // G
            colorData |= color.B << 0; // B
            int bpp = writeableBitmap.Format.BitsPerPixel / 8;

            unsafe
            {
                for (int y = 0; y < height; y++)
                {
                    // Get a pointer to the back buffer
                    int pBackBuffer = (int)writeableBitmap.BackBuffer;

                    // Find the address of the pixel to draw
                    pBackBuffer += (top + y) * writeableBitmap.BackBufferStride;
                    pBackBuffer += left * bpp;

                    for (int x = 0; x < width; x++)
                    {
                        // Assign the color data to the pixel
                        *((int*)pBackBuffer) = colorData;

                        // Increment the address of the pixel to draw
                        pBackBuffer += bpp;
                    }
                }
            }

            writeableBitmap.AddDirtyRect(new Int32Rect(left, top, width, height));
        }
    }
}

MainWindow.xaml

<Window x:Class="Test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Test"
        mc:Ignorable="d"
        Title="MainWindow" 
        Height="Auto" 
        Width="Auto"
        WindowStartupLocation="CenterScreen"
        WindowState="Maximized">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <ScrollViewer>
            <local:MyCanvas ZoomValue="{Binding ElementName=ScaleTransform, Path=ScaleX}">
                <local:MyCanvas.LayoutTransform>
                    <ScaleTransform x:Name="ScaleTransform" ScaleX="{Binding ElementName=Slider, Path=Value}" ScaleY="{Binding ElementName=Slider, Path=Value}"/>
                </local:MyCanvas.LayoutTransform>
                <Image Canvas.Top="1" Canvas.Left="1" x:Name="Image" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
            </local:MyCanvas>
        </ScrollViewer>
        <StackPanel Grid.Row="1" Orientation="Horizontal">
            <Slider x:Name="Slider" Maximum="100" Minimum="0.5" Value="1" Width="200"/>
            <Button Click="Button_Click" Content="Click Me!"/>
        </StackPanel>
    </Grid>
</Window>

We generate a bitmap with random colored pixels and then render the grid lines only if zoomed up close. Performance-wise, this is actually better than expected. I should note, though, that if you attempt to zoom below 50%, the app crashes. Not sure if it's an issue with the grid lines being drawn at a minute size (IsGridVisible = true where ZoomValue < 0.5) or with the bitmap being generated. Either way, cheers!

Update

Didn't realize the grid lines are still behind the contents of the canvas. Haven't worked out a solution for that yet...

Update 2

Replace:

<local:MyCanvas ZoomValue="{Binding ElementName=ScaleTransform, Path=ScaleX}">
    <local:MyCanvas.LayoutTransform>
        <ScaleTransform x:Name="ScaleTransform" ScaleX="{Binding ElementName=Slider, Path=Value}" ScaleY="{Binding ElementName=Slider, Path=Value}"/>
        </local:MyCanvas.LayoutTransform>
        <Image Canvas.Top="1" Canvas.Left="1" x:Name="Image" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
</local:MyCanvas>

With:

<Grid>
    <Canvas>
        <Canvas.LayoutTransform>
            <ScaleTransform ScaleX="{Binding ElementName=Slider, Path=Value}" ScaleY="{Binding ElementName=Slider, Path=Value}"/>
        </Canvas.LayoutTransform>
        <Image Canvas.Top="5" Canvas.Left="5" x:Name="Image" RenderOptions.BitmapScalingMode="NearestNeighbor"/>
    </Canvas>
    <local:MyGrid ZoomValue="{Binding ElementName=ScaleTransform, Path=ScaleX}">
        <local:MyGrid.LayoutTransform>
            <ScaleTransform x:Name="ScaleTransform" ScaleX="{Binding ElementName=Slider, Path=Value}" ScaleY="{Binding ElementName=Slider, Path=Value}"/>
        </local:MyGrid.LayoutTransform>
    </local:MyGrid>
</Grid>

I believe another boost in performance as we are utilizing a simpler control to display the grid lines, plus, the grid lines can be placed either below or above desired controls.

Update 3

I have decided to post my latest solution, which is significantly more efficient and can all be done in XAML:

<Grid>
    <Grid.Background>
        <DrawingBrush Viewport="0,0,5,5" ViewportUnits="Absolute" TileMode="Tile">
            <DrawingBrush.Drawing>
                <DrawingGroup>
                    <DrawingGroup.Children>
                        <GeometryDrawing Geometry="M-.5,0 L50,0 M0,10 L50,10 M0,20 L50,20 M0,30 L50,30 M0,40 L50,40 M0,0 L0,50 M10,0 L10,50 M20,0 L20,50 M30,0 L30,50 M40,0 L40,50">
                            <GeometryDrawing.Pen>
                                <Pen Thickness="1" Brush="Black" />
                            </GeometryDrawing.Pen>
                        </GeometryDrawing>
                    </DrawingGroup.Children>
                </DrawingGroup>
            </DrawingBrush.Drawing>
        </DrawingBrush>
    </Grid.Background>
</Grid>