1
votes

I have a canvas in a scrollviewer. To be able to use the scrollviewer, I've overridden the Canvas's MeasureOverride method to return the size of all children.

This works fine, except that the canvas can only contain items in positive space for the scrollviewer to work correctly.

I'd like the user to be able to drag elements around without the restriction of them having to be on the positive side of the canvas's origin.

So basicly I want to be able to position the elements anywhere, like at (-200, 10) or (500, -20) , and be able to scroll from the most left element (-200) to the most right(500), and from top to bottom.

To complicate matters the canvas can be scaled using the LayoutTransform, and I'd like to include the view-area in the scrollbars, so not only the min/max bounds of the children are taken into account by the scrollbars, but also the min/max of the current view-area.

Does anybody know how to make this work ?

2

2 Answers

1
votes

Today I worked on this problem only :) Happy to share the code which works:

public void RepositionAllObjects(Canvas canvas)
{
    adjustNodesHorizontally(canvas);
    adjustNodesVertically(canvas);
}

private void adjustNodesVertically(Canvas canvas)
{
    double minLeft = Canvas.GetLeft(canvas.Children[0]);
    foreach (UIElement child in canvas.Children)
    {
        double left = Canvas.GetLeft(child);
        if (left < minLeft)
            minLeft = left;
    }

    if (minLeft < 0)
    {
        minLeft = -minLeft;
        foreach (UIElement child in canvas.Children)
            Canvas.SetLeft(child, Canvas.GetLeft(child) + minLeft);
    }
}

private void adjustNodesHorizontally(Canvas canvas)
{
    double minTop = Canvas.GetTop(canvas.Children[0]);
    foreach (UIElement child in canvas.Children)
    {
        double top = Canvas.GetTop(child);
        if (top < minTop)
            minTop = top;
    }

    if (minTop < 0)
    {
        minTop = -minTop;
        foreach (UIElement child in canvas.Children)
            Canvas.SetTop(child, Canvas.GetTop(child) + minTop);
    }
}

Now call RepositionAllObjects method to realign the objects as and when required.

Of course, you need to have your own canvas derived from Canvas. And this method is required (you can tweak it if you like):

    public static Rect GetDimension(UIElement element)
    {
        Rect box = new Rect();
        box.X = Canvas.GetLeft(element);
        box.Y = Canvas.GetTop(element);
        box.Width = element.DesiredSize.Width;
        box.Height = element.DesiredSize.Height;
        return box;
    }

    protected override Size MeasureOverride(Size constraint)
    {
        Size availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
        double minX = 900000; //some dummy high number
        double minY = 900000; //some dummy high number
        double maxX = 0;
        double maxY = 0;

        foreach (UIElement element in this.Children)
        {
            element.Measure(availableSize);

            Rect box = GetDimension(element);
            if (minX > box.X) minX = box.X;
            if (minY > box.Y) minY = box.Y;
            if (maxX < box.X + box.Width) maxX = box.X + box.Width;
            if (maxY < box.Y + box.Height) maxY = box.Y + box.Height;
        }

        if (minX == 900000) minX = 0;
        if (minY == 900000) minY = 0;

        return new Size { Width = maxX - minX, Height = maxY - minY };
    }

Now, all you need to do is wrap this canvas inside a scrollviewer.

Hope it helps!

0
votes

I think the ScrollViewer assumes that the origin will be at (0, 0) -- after all, there's no way to tell it otherwise; Canvas is the only panel that knows about specified-and-nonzero origins.

A simple option might be to forget about a ScrollViewer, and implement your own panning functionality -- perhaps with dragging, or perhaps with a big "scroll left" button on the left side of the view, a "scroll right" button on the right, etc. -- and implement it in terms of a transform on the content.

But if you need to stick with the classic scrollbar metaphor, I think you could make your own panel (or override ArrangeOverride too, which amounts to the same thing) and offset all the positions -- make them zero-based. So if you have an element at (20, -20) and another at (0, 5), you would need to offset everything downward by 20 to make it fit in a zero-based space; and when you lay out your children, the first would go at (20, 0) and the second at (0, 25).

But now scrolling is weird. If the user is scrolled all the way to the bottom, and drags something off the bottom edge, the view stays put; same with the right. But if they're scrolled all the way to the top, and drag something off the top edge, suddenly everything jumps downward, because the ScrollViewer's VerticalOffset was zero before and is still zero, but zero means something different because of your layout offset.

You might be able to get around the scrolling weirdness by binding the HorizontalOffset and VerticalOffset. If your ViewModel automatically fixed up those properties (and fired PropertyChanged) whenever a control moves "negative", then you might be able to keep the view scrolled to the same logical content, even though you're now telling WPF's layout engine that everything is somewhere else. I.e., your ViewModel would track the logical scroll position (zero, before you drag the element negative), and would report the zero-based scroll position to the ScrollViewer (so after the drag, you would tell the ScrollViewer that its VerticalOffset is now 20).