WPF Transform 기반 캔버스 경계 초과 감지 및 계산 구현

일부 시나리오에서는 캔버스의 경계를 제어해야 하며, 이때 캔버스의 네 방향 경계와 극값을 계산해야 합니다.

먼저 효과를 확인해보세요:

캔버스는 RenderTransform을 통해 변환됩니다. 변환 과정에서 실제 너비와 높이는 변경되지 않으므로 Transform을 통해 변환 상태를 실시간으로 기록해야 합니다.

  1. 이 예제에서는 Canvas에 대해 RenderTransform 변환을 사용하여 확대/축소 및 이동을 지원합니다.
  2. 가장 바깥쪽은 Border로 감싸서 캔버스의 실제 너비와 높이를 제한합니다. Canvas가 변환될 때 실제 너비와 높이는 변경되지 않으므로, 캔버스 이동 오프셋은 Canvas와 Border의 너비/높이 상대 위치를 계산하여 결정됩니다.
  3. 코드 예제:
<Border x:Name="canvasGrid">
    <Canvas x:Name="writeBorad" Width="1920" Height="1080" 
            MouseDown="WriteBorad_OnMouseDown" 
            MouseWheel="UIElement_OnMouseWheel" 
            Background="{StaticResource test}" 
            MouseUp="WriteBorad_OnMouseUp" 
            MouseMove="UIElement_OnMouseMove">
        <Canvas.RenderTransform>
            <TransformGroup>
                <ScaleTransform x:Name="scale"></ScaleTransform>
                <TranslateTransform x:Name="trans"></TranslateTransform>
            </TransformGroup>
        </Canvas.RenderTransform>
        <Button Width="60" Height="60" Canvas.Left="100" Canvas.Top="100"></Button>
    </Canvas>
</Border>

백엔드 계산 로직:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
        this.writeBorad.Width = 3840;
        this.writeBorad.Height = 2160;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        var drawBrush = this.writeBorad.Background as DrawingBrush;
        if (drawBrush != null)
        {
            var size = drawBrush.Viewport.Size;
            var div = writeBorad.Width / size.Width;
            var rem = writeBorad.Width % size.Width;
            if (rem != 0)
            {
                writeBorad.Width = size.Width * (Math.Floor(div) + 1);
            }
            div = writeBorad.Height / size.Height;
            rem = writeBorad.Height % size.Height;
            if (rem != 0)
            {
                writeBorad.Height = size.Height * (Math.Floor(div) + 1);
            }
        }
        trans.X = -writeBorad.Width / 2;
        trans.Y = -writeBorad.Height / 2;
    }

    private void UIElement_OnMouseWheel(object sender, MouseWheelEventArgs e)
    {
        Point point = e.GetPosition(writeBorad);
        var delta = e.Delta * 0.001;
        DowheelZoom(null, point, delta);
    }

    double scaleSize = 1;
    private void DowheelZoom(TransformGroup group, Point point, double delta)
    {
        if ((scale.ScaleX + delta) <= 1)
        {
            scale.ScaleX = 1;
            scale.ScaleY = 1;
        }
        else if (scale.ScaleX + delta > 5)
        {
            scale.ScaleX = 5;
            scale.ScaleY = 5;
        }
        else
        {
            scale.ScaleX += delta;
            scale.ScaleY += delta;
        }
        var targetX = point.X * (scale.ScaleX - 1);
        var targetY = point.Y * (scale.ScaleX - 1);
        trans.X = -targetX;
        trans.Y = -targetY;
        scaleSize = scale.ScaleX;
    }

    double tranX;
    double tranY;
    Point startPoint;
    private bool isMouseDown;

    private void UIElement_OnMouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton != MouseButtonState.Pressed || !isMouseDown) return;
        Point mousePoint = e.GetPosition(canvasGrid);
        var targetX = tranX + mousePoint.X - startPoint.X;
        var targetY = tranY + mousePoint.Y - startPoint.Y;
        trans.X = targetX;
        trans.Y = targetY;

        var scaleWidth = writeBorad.ActualWidth * scaleSize;
        var scaleHeight = writeBorad.ActualHeight * scaleSize;
        var gridWidth = canvasGrid.ActualWidth;
        var gridHeight = canvasGrid.ActualHeight;
        var minX = -(scaleWidth - gridWidth);
        var minY = -(scaleHeight - gridHeight);

        double resetX = 0;
        double resetY = 0;

        if (targetX > 0)
        {
            targetX = 0;
            resetX = targetX;
            isNeedResetX = true;
        }
        if (targetX < minX)
        {
            targetX = minX;
            resetX = targetX;
            isNeedResetX = true;
        }
        if (targetY > 0)
        {
            targetY = 0;
            resetY = targetY;
            isNeedResetY = true;
        }
        if (targetY < minY)
        {
            targetY = minY;
            resetY = targetY;
            isNeedResetY = true;
        }
        ResetPoint = new Point(resetX, resetY);
    }

    private Point ResetPoint;
    private bool isNeedResetX;
    private bool isNeedResetY;

    private void WriteBorad_OnMouseDown(object sender, MouseButtonEventArgs e)
    {
        isMouseDown = true;
        writeBorad.CaptureMouse();
        startPoint = e.GetPosition(canvasGrid);
        tranX = trans.X;
        tranY = trans.Y;
        isNeedResetX = false;
        isNeedResetY = false;
    }

    private void AddElement(Point position)
    {
        Button btn = new Button();
        btn.Height = 60;
        btn.Width = 60;
        Canvas.SetLeft(btn, position.X);
        Canvas.SetTop(btn, position.Y);
        writeBorad.Children.Add(btn);
    }

    private void WriteBorad_OnMouseUp(object sender, MouseButtonEventArgs e)
    {
        AddElement(e.GetPosition(writeBorad));
        isMouseDown = false;
        writeBorad.ReleaseMouseCapture();

        if (isNeedResetX)
        {
            trans.X = ResetPoint.X;
        }
        if (isNeedResetY)
        {
            trans.Y = ResetPoint.Y;
        }
        isNeedResetY = isNeedResetX = false;
        ResetPoint = new Point();
    }
}

요약:

  1. Border를 통해 캔버스의 실제 너비와 높이를 제한합니다. Canvas는 Transform을 사용하여 이동 및 확대/축소를 수행하며, 실시간 너비/높이 계산을 통해 오프셋을 결정하여 경계 초과를 감지합니다.
  2. 이 데모에서는 Canvas의 ScaleTransform과 TranslateTransform을 사용했지만, 이후 MatrixTransform으로 전환하면 더 높은 유연성을 제공하며 다중 터치 Manipulation 작업을 지원할 수 있습니다.
  3. 향후 경계 초과 시 바운스 효과와 애니메이션 처리를 추가할 예정입니다.

태그: WPF canvas RenderTransform ScaleTransform TranslateTransform

7월 8일 00:08에 게시됨