计算 ScrollViewer 的滚动条偏移量,使对象居中,同时保持比例可变的布局变换

Calculate a ScrollViewer's scrollbar offsets so that an object will be centered, while maintaining a layout transform who's scale is variable

我已经和这个问题争论了好几个小时了,我似乎无法得出一个可以接受的答案。我希望那里的几何技能比我自己强得多的人可以为我解决这个谜语。任何帮助将不胜感激。我的问题的性质和描述在我提供的图片下方。

这里是我构建的示例项目,它没有正确满足要求。

XAML:

<Window x:Class="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"
    mc:Ignorable="d"
    Title="Center and Zoom ScrollViewer Test" Height="600" Width="800" WindowStartupLocation="CenterScreen">
<Grid>
    <DockPanel>
        <GroupBox Header="Parameters" DockPanel.Dock="Top" Margin="10">
            <StackPanel Orientation="Horizontal">
                <GroupBox Header="Manually Set ScrollBar Positions" Margin="10">
                    <StackPanel Orientation="Horizontal">
                        <TextBox Name="EditHorz" Width="60" Margin="10" TextChanged="EditHorz_TextChanged" />
                        <Label Content="x" Margin="0 10 0 10" />
                        <TextBox Name="EditVert" Width="60" Margin="10" TextChanged="EditVert_TextChanged" />
                    </StackPanel>
                </GroupBox>

                <GroupBox Header="Scale" Margin="10">
                    <DockPanel>
                        <Label Content="{Binding ElementName=scaleValue, Path=Value}" DockPanel.Dock="Right" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Width="40" />
                        <Slider Name="scaleValue" Minimum="1" Maximum="4" SmallChange="0.05" LargeChange="0.1" Width="200" VerticalAlignment="Center" />
                    </DockPanel>
                </GroupBox>
            </StackPanel>
        </GroupBox>

        <GroupBox Header="Debug Output" Margin="10">
            <TextBox Name="text" FontFamily="Courier New" FontSize="12" DockPanel.Dock="Left" Width="500" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" Margin="10" />
        </GroupBox>

        <GroupBox Header="Proof" Margin="10">
            <DockPanel>
                <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" HorizontalAlignment="Center">
                    <Button Width="60" HorizontalAlignment="Left" Content="Center" Click="ButtonCenter_Click" Margin="10" />
                    <Button Width="60" HorizontalAlignment="Left" Content="Reset" Click="ButtonReset_Click" Margin="10" />
                </StackPanel>
                <ScrollViewer Name="scroll" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden" Background="Green" Width="100" Height="100" VerticalAlignment="Top" Margin="10">
                    <Canvas Width="200" Height="200" Background="Red">
                        <Canvas.LayoutTransform>
                            <ScaleTransform ScaleX="{Binding ElementName=scaleValue, Path=Value}" ScaleY="{Binding ElementName=scaleValue, Path=Value}" />
                        </Canvas.LayoutTransform>
                        <Rectangle Name="rect" Width="40" Height="40" Canvas.Left="120" Canvas.Top="70" Fill="Blue" />
                    </Canvas>
                </ScrollViewer>
            </DockPanel>
        </GroupBox>
    </DockPanel>
</Grid>

代码隐藏 (VB.net)

Class MainWindow
''' Calculates the horizontal and vertical scrollbar offsets so that
''' the blue rectangle is centered within the scroll viewer.
Private Sub RecalculateCenter()
    ' the scale we are using
    Dim scale As Double = scaleValue.Value

    ' get the rectangles current position within the canvas
    Dim rectLeft As Double = Canvas.GetLeft(rect)
    Dim rectTop As Double = Canvas.GetTop(rect)

    ' set our point of interest "Rect" equal to the the whole coordinates of the rectangle
    Dim poi As Rect = New Rect(rectLeft, rectTop, rect.Width, rect.Height)

    ' get our view offset
    Dim ofsViewWidth As Double = (scroll.ScrollableWidth - (((scroll.ViewportWidth / 2) - (rect.ActualWidth / 2)) * scale)) / scale
    Dim ofsViewHeight As Double = (scroll.ScrollableHeight - (((scroll.ViewportHeight / 2) - (rect.ActualHeight / 2)) * scale)) / scale

    ' calculate our scroll bar offsets
    Dim verticalOffset As Double = (poi.Top - ofsViewHeight) * scale
    Dim horizontalOffset As Double = (poi.Left - ofsViewWidth) * scale

    ' record the output to the debug output window
    Dim sb As New StringBuilder()
    sb.AppendLine($"Scale      : {scale}")
    sb.AppendLine($"POI        : {poi.ToString()}")
    sb.AppendLine($"Rect       : {rectLeft}x{rectTop}")
    sb.AppendLine($"Extent     : {scroll.ExtentWidth}x{scroll.ExtentHeight}")
    sb.AppendLine($"Scrollable : {scroll.ScrollableWidth}x{scroll.ScrollableHeight}")
    sb.AppendLine($"View Offset: {ofsViewWidth}x{ofsViewHeight}")
    sb.AppendLine($"Horizontal : {horizontalOffset}")
    sb.AppendLine($"Vertical   : {verticalOffset}")

    text.Text = sb.ToString()

    ' set the EditHorz and EditVert text box values, this will trigger the scroll
    ' bar offsets to fire via the TextChanged event handlers
    EditHorz.Text = horizontalOffset.ToString()
    EditVert.Text = verticalOffset.ToString()
End Sub

''' Try and parse the horizontal text box to a double, and set the scroll bar position accordingly
Private Sub SetScrollBarHorizontalOffset()
    Dim ofs As Double = 0
    If Double.TryParse(EditHorz.Text, ofs) Then
        scroll.ScrollToHorizontalOffset(ofs)
    End If
End Sub

''' Try and parse the vertical text box to a double, and set the scroll bar position accordingly
Private Sub SetScrollBarVerticalOffset()
    Dim ofs As Double = 0
    ofs = 0
    If Double.TryParse(EditVert.Text, ofs) Then
        scroll.ScrollToVerticalOffset(ofs)
    End If
End Sub

''' Parse and set scrollbars positions for both Horizontal and Vertical
Private Sub SetScrollBarOffsets()
    SetScrollBarHorizontalOffset()
    SetScrollBarVerticalOffset()
End Sub

Private Sub ButtonCenter_Click(sender As Object, e As RoutedEventArgs)
    RecalculateCenter()
End Sub

Private Sub ButtonReset_Click(sender As Object, e As RoutedEventArgs)
    scroll.ScrollToVerticalOffset(0)
    scroll.ScrollToHorizontalOffset(0)
End Sub

Private Sub EditHorz_TextChanged(sender As Object, e As TextChangedEventArgs)
    SetScrollBarOffsets()
End Sub

Private Sub EditVert_TextChanged(sender As Object, e As TextChangedEventArgs)
    SetScrollBarOffsets()
End Sub

结束Class

经过多次试验和错误,并将其逐个分解,我终于能够使这段代码正常工作。我希望其他人会发现这很有用。解决方法如下:

XAML:

<Window x:Class="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"
    mc:Ignorable="d"
    Title="Center and Zoom ScrollViewer Test" Height="600" Width="800" WindowStartupLocation="CenterScreen">
<Grid>
    <DockPanel>
        <GroupBox Header="Parameters" DockPanel.Dock="Top" Margin="10">
            <StackPanel Orientation="Horizontal">
                <GroupBox Header="Manually Set ScrollBar Positions" Margin="10">
                    <StackPanel Orientation="Horizontal">
                        <TextBox Name="EditHorz" Width="60" Margin="10" TextChanged="EditHorz_TextChanged" />
                        <Label Content="x" Margin="0 10 0 10" />
                        <TextBox Name="EditVert" Width="60" Margin="10" TextChanged="EditVert_TextChanged" />
                    </StackPanel>
                </GroupBox>

                <GroupBox Header="Scale" Margin="10">
                    <DockPanel>
                        <Label Content="{Binding ElementName=scaleValue, Path=Value, StringFormat={}{0:F2}}" DockPanel.Dock="Right" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Width="40" />
                        <Slider Name="scaleValue" Minimum="1" Maximum="4" SmallChange="0.05" LargeChange="0.1" Width="200" VerticalAlignment="Center" />
                    </DockPanel>
                </GroupBox>
            </StackPanel>
        </GroupBox>

        <GroupBox Header="Debug Output" Margin="10">
            <TextBox Name="text" FontFamily="Courier New" FontSize="12" DockPanel.Dock="Left" Width="500" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" Margin="10" />
        </GroupBox>

        <GroupBox Header="Proof" Margin="10">
            <DockPanel>
                <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" HorizontalAlignment="Center">
                    <Button Width="60" HorizontalAlignment="Left" Content="Center" Click="ButtonCenter_Click" Margin="10" />
                    <Button Width="60" HorizontalAlignment="Left" Content="Reset" Click="ButtonReset_Click" Margin="10" />
                </StackPanel>
                <ScrollViewer Name="scroll" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden" Background="Green" Width="100" Height="100" VerticalAlignment="Top" Margin="10">
                    <Canvas Width="200" Height="200" Background="Red">
                        <Canvas.LayoutTransform>
                            <ScaleTransform ScaleX="{Binding ElementName=scaleValue, Path=Value}" ScaleY="{Binding ElementName=scaleValue, Path=Value}" />
                        </Canvas.LayoutTransform>
                        <Rectangle Name="rect" Width="40" Height="40" Canvas.Left="120" Canvas.Top="70" Fill="Blue" />
                    </Canvas>
                </ScrollViewer>
            </DockPanel>
        </GroupBox>
    </DockPanel>
</Grid>

代码隐藏 (VB.net):

Class MainWindow
' Calculates the horizontal and vertical scrollbar offsets so that
' the blue rectangle is centered within the scroll viewer.
Private Sub RecalculateCenter()
    ' the scale we are using
    Dim scale As Double = scaleValue.Value

    ' get our rectangles left and top properties
    Dim rectLeft As Double = Canvas.GetLeft(rect) * scale
    Dim rectTop As Double = Canvas.GetTop(rect) * scale
    Dim rectWidth As Double = rect.Width * scale
    Dim rectHeight As Double = rect.Height * scale

    ' set our point of interest "Rect" equal to the the whole coordinates of the rectangle
    Dim poi As Rect = New Rect(rectLeft, rectTop, rectWidth, rectHeight)

    ' get top and left center values
    Dim horizontalCenter As Double = ((scroll.ViewportWidth / 2) - (rectWidth / 2))
    Dim verticalCenter As Double = ((scroll.ViewportHeight / 2) - (rectHeight / 2))

    ' get our center of viewport with relation to the poi
    Dim viewportCenter As New Rect(horizontalCenter, verticalCenter, rectWidth, rectHeight)

    ' calculate our scroll bar offsets
    Dim verticalOffset As Double = (poi.Top) - (viewportCenter.Top)
    Dim horizontalOffset As Double = (poi.Left) - (viewportCenter.Left)

    ' record the output to the debug output window
    Dim sb As New StringBuilder()
    sb.AppendLine($"Scale .............. {scale,0:F2}")
    sb.AppendLine($"rectLeft ........... {rectLeft,0:F0}")
    sb.AppendLine($"rectTop ............ {rectTop,0:F0}")
    sb.AppendLine($"POI ................ {poi.Left,0:F0},{poi.Top,0:F0},{poi.Width,0:F0},{poi.Height,0:F0}")
    sb.AppendLine($"Horz Center ........ {horizontalCenter,0:F0}")
    sb.AppendLine($"Vert Center ........ {verticalCenter,0:F0}")
    sb.AppendLine($"View Center ........ {viewportCenter.Left,0:F0},{viewportCenter.Top,0:F0},{viewportCenter.Width,0:F0},{viewportCenter.Height,0:F0}")
    sb.AppendLine($"Horizontal ......... {horizontalOffset,0:F0}")
    sb.AppendLine($"Vertical ........... {verticalOffset,0:F0}")
    sb.AppendLine($"------------------------------------")
    sb.AppendLine($"ViewPort ........... {scroll.ViewportWidth,0:F0} x {scroll.ViewportHeight,0:F0}")
    sb.AppendLine($"Extent ............. {scroll.ExtentWidth,0:F0} x {scroll.ExtentHeight,0:F0}")
    sb.AppendLine($"Scrollable ......... {scroll.ScrollableWidth,0:F0} x {scroll.ScrollableHeight,0:F0}")

    text.Text = sb.ToString()

    ' set the EditHorz and EditVert text box values, this will trigger the scroll
    ' bar offsets to fire via the TextChanged event handlers
    EditHorz.Text = $"{horizontalOffset,0:F2}"
    EditVert.Text = $"{verticalOffset,0:F2}"
End Sub

' Try and parse the horizontal text box to a double, and set the scroll bar position accordingly
Private Sub SetScrollBarHorizontalOffset()
    Dim ofs As Double = 0
    If Double.TryParse(EditHorz.Text, ofs) Then
        scroll.ScrollToHorizontalOffset(ofs)
    Else
        scroll.ScrollToHome()
    End If
End Sub

' Try and parse the vertical text box to a double, and set the scroll bar position accordingly
Private Sub SetScrollBarVerticalOffset()
    Dim ofs As Double = 0
    ofs = 0
    If Double.TryParse(EditVert.Text, ofs) Then
        scroll.ScrollToVerticalOffset(ofs)
    Else
        scroll.ScrollToHome()
    End If
End Sub

' Parse and set scrollbars positions for both Horizontal and Vertical
Private Sub SetScrollBarOffsets()
    SetScrollBarHorizontalOffset()
    SetScrollBarVerticalOffset()
End Sub

Private Sub ButtonCenter_Click(sender As Object, e As RoutedEventArgs)
    RecalculateCenter()
End Sub

Private Sub ButtonReset_Click(sender As Object, e As RoutedEventArgs)
    EditHorz.Text = String.Empty
    EditVert.Text = String.Empty
End Sub

Private Sub EditHorz_TextChanged(sender As Object, e As TextChangedEventArgs)
    SetScrollBarOffsets()
End Sub

Private Sub EditVert_TextChanged(sender As Object, e As TextChangedEventArgs)
    SetScrollBarOffsets()
End Sub

Private Sub scaleValue_ValueChanged(sender As Object, e As RoutedPropertyChangedEventArgs(Of Double)) Handles scaleValue.ValueChanged
    Dispatcher.BeginInvoke(Sub() RecalculateCenter())
End Sub

结束Class