Search code examples
.netwpfcanvasscaleviewbox

How to scale controls with maintained aspect ratio and position without viewbox?


I am trying to replicate the display rearranger UI in display settings, in which there are button placed with respect to their physical location and can be rearranged with mouse-drag

Windows 10 display rearrangement

I can get the screen bounds from the screen class

System.Windows.Forms.Screen

and create button with the specified bounds in a canvas, but they are too large (i.e. 1366 x 768) to be shown in a container, so I decided to use WPF's inbuilt ViewBox control

The problem with viewbox is it scales not only the bounds but also the rendered element

enter image description here

I can't control the BorderThickness, FontSize and Margin, they become extremely negligible to notice or too big when the canvas size is changed.

What I need is Scale just the size, but not everything. I want the borderthickness to have a fixed size, not scaled. In the settings one the BorderThickness of the buttons stay the same even after resizing. What ever I try I get this >

enter image description here

Here the Button's size is 1366 x 768, its borderthickness is 1 px (not clearly visible, I have to set it 4 px to make it clear), and have a font size of 240 px.

This Button is hosted within a canvas whose size is programmaticaly determined, the canvas is hosted in a viewbox which is hosted in a border. What should I do to just scale the size?


Solution

  • I have finally achieved it, I got rid of the viewbox and managed to do it through code

    The Code :

        Private Sub LoadScreens()
            Dim ox As Integer = 0
            Dim oy As Integer = 0
            Dim canvaswidth As Integer = 0
            Dim canvasheight As Integer = 0
            Dim rect As New List(Of System.Drawing.Rectangle)
            For i = 0 To Forms.Screen.AllScreens.Count - 1
                Dim screen = Forms.Screen.AllScreens(i)
                Dim r = screen.Bounds
                rect.Add(r)
            Next
            For Each r In rect
                If r.X < ox Then ox = r.X
                If r.Y < oy Then oy = r.Y
            Next
            For Each r In rect
                If r.X + r.Width + Math.Abs(ox) > canvaswidth Then canvaswidth = r.X + r.Width + Math.Abs(ox)
                If r.Y + r.Height + Math.Abs(oy) > canvasheight Then canvasheight = r.Y + r.Height + Math.Abs(oy)
            Next
    
            ScreenCanvas.Children.Clear()
    
            Dim scale = GetScale(TParent.ActualWidth, TParent.ActualHeight, canvaswidth, canvasheight)
    
            ScreenCanvas.Width = canvaswidth * scale
            ScreenCanvas.Height = canvasheight * scale
    
            For i = 0 To rect.Count - 1
                Dim r = rect(i)
                Dim left = (r.X - ox) * scale
                Dim top = (r.Y - oy) * scale
                Dim nr As New Primitives.ToggleButton With {
                    .Width = r.Width * scale,
                    .Height = r.Height * scale,
                    .Content = New TextBlock() With {.Text = i + 1, .VerticalAlignment = VerticalAlignment.Center, .HorizontalAlignment = HorizontalAlignment.Center}
                }
                ScreenCanvas.Children.Add(nr)
                Canvas.SetLeft(nr, left)
                Canvas.SetTop(nr, top)
            Next
        End Sub
    
        Private Function GetScale(w1 As Double, h1 As Double, w2 As Double, h2 As Double) As Double
            Dim scaleHeight = h1 / h2
            Dim scaleWidth = w1 / w2
            Dim scale = Math.Min(scaleHeight, scaleWidth)
            Return scale
        End Function
    

    The ToggleButtons are programmaticaly created, scaled, positioned and are added to the scaled canvas : ScreenCanvas which is hosted in a grid TParent.

    The XAML :

              <Border CornerRadius="10" Margin="40" Padding="40" Background="#22aaaaaa">
                <Grid x:Name="TParent" Height="150" >
                    <Grid.Resources>
                        <Style TargetType="{x:Type ToggleButton}">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                                        <Border Background="White" BorderBrush="Red" BorderThickness="2">
                                            <ContentPresenter TextBlock.Foreground="Black"/>
                                        </Border>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </Grid.Resources>
                    <Canvas x:Name="ScreenCanvas" HorizontalAlignment="Center"  VerticalAlignment="Bottom"/>
                </Grid>
            </Border>
    

    Here's the final result :

    enter image description here

    Now I can control the borderthickness, cornerradius, margin :)

    Edit :

    I have updated the UI and added code to update their location and size when the grid size is changed.

    Code :

         Private Sub TParent_SizeChanged(sender As Object, e As SizeChangedEventArgs) Handles TParent.SizeChanged
            If e.WidthChanged AndAlso _init Then
                Dim canvaswidth = ScreenCanvas.ActualWidth
                Dim canvasheight = ScreenCanvas.ActualHeight
                Dim scale = GetScale(TParent.ActualWidth, TParent.ActualHeight, canvaswidth, canvasheight)
                ScreenCanvas.Width = canvaswidth * scale
                ScreenCanvas.Height = canvasheight * scale
                For Each c As FrameworkElement In ScreenCanvas.Children
                    c.Width = c.ActualWidth * scale
                    c.Height = c.ActualHeight * scale
                    Dim top = Canvas.GetTop(c) * scale
                    Dim left = Canvas.GetLeft(c) * scale
                    Canvas.SetTop(c, top)
                    Canvas.SetLeft(c, left)
                Next
            End If
        End Sub
    

    UI :

    enter image description here