How to draw scrollbars on WPF Canvas - wpf

I am trying to create a canvas with scroll bars.
Can anyone help me give some ideas on how to do this?
I have already tried using grid of 1 row and 1 column but due to certain constraints I want to use canvas.
Thanks in advance!

You could put the canvas inside of a scrollviewer. I tried this quick test and it allowed me to scroll through the contents of the canvas.
<ScrollViewer Height="100" Width="200">
<Canvas Height="400" Width="400">
//Content here
</Canvas>
</ScrollViewer>
edit: Here is an example where the scroll-bars show up only when needed, and it changes dynamically as the canvas size changes.
<Button Content="Change Canvas Size" Click="ChangeCanvasSize_Click"/>
<ScrollViewer Height="100" Width="200" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<Canvas x:Name="TestCanvas">
<TextBlock Text="Test Test"/>
</Canvas>
</ScrollViewer>
Changing canvas size with button click:
private void ChangeCanvasSize_Click(object sender, RoutedEventArgs e)
{
TestCanvas.Width = 600;
TestCanvas.Height = 600;
}
In this example, I start out with no scroll-bars and when I click the button to expand the canvas, scroll-bars appear.

Ok after working with it for sometime I figured out a way. Create a XAML like this
<ScrollViewer>
<Grid x:Name="drawingGrid" SizeChanged="drawingGrid_SizeChanged">
<Canvas Name="drawingCanvas"> /<Canvas>
</Grid>
</ScrollViewer>
On windowLoad function set the canvas height/width equal to grid height/width. Update the canvas ht/wd:
when grid size changes, due to mininmize/maximize.
dragging an element beyond the boundaries of canvas or creating a new element too close the edge of canvas
double dHeight = 220;
if (drawingCanvas.Height < CurrentPosition.Y + dHeight)
{
// increase canvas height
drawingCanvas.Height += (2 * dHeight);
}
Hope this is of some help. Please share if anyone has any better idea or suggestions to improve this.

By combining Mario-sannum's answer and your question then I've made a solution that should work fine in most cases..
<ScrollViewer>
<Grid x:Name="drawingGrid" SizeChanged="drawingGrid_SizeChanged">
<Canvas Name="c">
<TextBlock x:Name="draw_Text" Text="Test Test"/>
</<Canvas>
</Grid>
</ScrollViewer>
void drawingGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
try { c.Height = draw_Text.ActualHeight; } catch { }
try { c.Width = draw_Text.ActualWidth; } catch { }
}
That should resize the Canvas so the scrollviewer can scroll...

Related

WPF - show an image and set different clickable areas on it

the case is this one:
I have an image representing a schema, let's say a cross like the following
I need to include the image in a WPF UserControl and let the user click on each of the branches (red, green or blue...) and, according to the branch selected, do something different.
What would be the best way to solve this?
I tried with canvas but I don't find a way to trace correctly the background image with shapes (also because the real image is not so simple as the sample cross here)
thanks for any suggestion
It depends on the comlexity of shapes but it is not difficult to find the shape where mouse up event is fired by VisualTreeHelper.HitTest method.
Let's say there is a Canvas which has two Rectangles inside.
<Canvas Background="Transparent"
PreviewMouseUp="Canvas_PreviewMouseUp">
<Rectangle x:Name="Rect1" Canvas.Left="20" Canvas.Top="20"
Width="20" Height="20" Fill="Red"/>
<Rectangle x:Name="Rect2" Canvas.Left="80" Canvas.Top="80"
Width="20" Height="20" Fill="Green"/>
</Canvas>
Catching its PreviewMouseUp event, you can tell the Rectangle where that event is fired.
private void Canvas_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
var position = e.GetPosition((IInputElement)sender);
var result = VisualTreeHelper.HitTest((Visual)sender, position);
if (result.VisualHit is Rectangle rect)
{
Debug.WriteLine($"Hit {rect.Name}");
}
}

Alternate between "Stretch.Uniform" and "Stretching.None" for ViewBox with ScrollViewer

I want to achieve a very well known behavior seen in the browser when you have an image to display that is larger then the monitor:
Originally, the image is displayed fitting inside the window area, and the mouse cursor is a magnifying glass with a "+" icon;
If you click, two things happen:
a. The image is displayed with its native pixel size;
b. Scroll bars appear;
I want this effect with a larger-than-screen UniformGrid. For that, I can use ViewBox. I have already got what I want putting the control inside a ViewBox with Stretch.Uniform property, and upon MouseLeftButtonDown event it toggles between Stretch.None and Stretch.Uniform, just like the large image in browser analogy, only without scroll bars.
Now if I add the ScrollViewer (ViewBox -> ScrollViewer -> UniformGrid), the effect doesn't work anymore, because the ScrollViewer always displays the (larger than window) MyUserControl with its native resolution, that is, clipped and with scroll bars activated, while I would like to alternate between this and a "fitting in ViewBox" version.
Here is how I get the resizing, but the ScrollViewer never displays:
<Viewbox x:Name="vbox" Stretch="None">
<ScrollViewer x:Name="scroll" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" >
<UniformGrid x:Name="ugrid" Columns="2" MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior/>
<local:AtlasMasculinoPosterior/>
</UniformGrid>
</ScrollViewer>
</Viewbox>
And if change the order, then the Scroll bars always display and the zoom doesn't toggle upon mouse click (although the event fires):
<ScrollViewer x:Name="scroll" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" >
<Viewbox x:Name="vbox" Stretch="None">
<UniformGrid x:Name="ugrid" Columns="2" MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior/>
<local:AtlasMasculinoPosterior/>
</UniformGrid>
</Viewbox>
</ScrollViewer>
And here the code behind event:
private void UniformGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (vbox.Stretch == Stretch.None)
{
vbox.Stretch = Stretch.Uniform;
}
else
vbox.Stretch = Stretch.None;
}
So what am I doing wrong, or what should I do so that the intended behavior works?
The way I see it, I would like to alternate between having the control in a ViewBox (Stretch.Uniform) and having the control inside a ScrollViewer, but I wonder how to have the same effect with both elements being part of the layout tree (one inside another), or even if I should, move the UniformGrid in and out of containers I would manipulate programmatically in code behind.
Got it to work in sort of a hackish way, by having a Grid with both a ViewBox and a ScrollViewer, and putting the UniformGrid inside one of them in XAML. Then, in code-behind, I programmatically detach the UniformGrid from its present container, and attach it to the other (using a boolean flag to control where it is, but that is debatable):
<Grid x:Name="grid">
<ScrollViewer x:Name="scroll" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"/>
<Viewbox x:Name="viewbox" Stretch="Uniform">
<UniformGrid x:Name="ugrid" Columns="2" MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior/>
<local:AtlasMasculinoPosterior/>
</UniformGrid>
</Viewbox>
</Grid>
and
bool atlasfullscreen = false;
private void UniformGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
UniformGrid ug = sender as UniformGrid;
if (atlasfullscreen)
{
scroll.Content = null;
viewbox.Child = ug;
atlasfullscreen = false;
}
else
{
viewbox.Child = null;
scroll.Content = ug;
atlasfullscreen = true;
}
}
I had a similar use case where I had an item that I needed to alternate between Stretch.None and Stretch.Uniform, and when Stretch.None, I needed the scrollbars to be visible.
What I finally figured out was that when I set Stretch.None, I needed to set the ScrollViewer's Width & Height to the ViewBox's parent ActualWidth / Height, and when Stretch.Uniform, I needed to clear the ScollViewer's width and height.
So using your original XAML, plus the new Grid, here's the new XAML:
<Grid x:Name="grid">
<Viewbox x:Name="vbox"
Stretch="Uniform">
<ScrollViewer x:Name="scroll"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<UniformGrid x:Name="ugrid"
Columns="2"
MouseLeftButtonDown="UniformGrid_MouseLeftButtonDown">
<local:AtlasMasculinoAnterior />
<local:AtlasMasculinoPosterior />
</UniformGrid>
</ScrollViewer>
</Viewbox>
</Grid>
New code behind:
private void UniformGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (vbox.Stretch == Stretch.None)
{
vbox.Stretch = Stretch.Uniform;
scroll.Width = double.NaN;
scroll.Height = double.NaN;
}
else
{
vbox.Stretch = Stretch.None;
scroll.Width = grid.ActualWidth;
scroll.Height = grid.ActualHeight;
}
}
You might need to tweak the above example for how the Viewbox now being in a grid - but for my use case with similar XAML / code I got mine working without having to constantly move the child from the Viewbox to another control and back again.
So in summary: when Viewbox.Stretch = Uniform, set scrollviewer's width / height to double.NaN, and when Viewbox.Stretch = None, set scrollviewer's width / height to Viewbox.Parent.ActualWidth / Height.

How to implement popup in Windows Phone

I'm implementing a templated control, which should work as virtual keyboard button - when you hold it, it displays a popup with additional options to choose.
I've implemented the popup more less in the following way:
<Grid>
<Border>Content</Border>
<Grid x:Name="gPopup" Visibility="Collapsed">
<StackPanel x:Name="spSubItems" Orientation="Horizontal" />
</Grid>
</Grid>
I show the popup by changing visibility to visible and setting negative margins for top and bottom. However, when I do that, and when the popup is actually larger than the control, the control is being resized to match its size - despite fact, that it is not inside:
How can I implement the popup, such that it won't expand the container it's on? And such that the container will still match size of its contents?
Edit: In response to comments and answers
I'm not sure if I'm understood correctly. Here's an image with explanation:
I'd like to keep the original container's size the same after showing the popup. I'm unsure how WrapPanel or DockPanel could help me with that.
The solution is simply to use Popup instead of positioned Grid.
Sample- Create a grid
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<!-- Setting a Rectangle having transparent background which set the
Visibility of popup -->
<Rectangle Name="popupRect" Fill="#80000000" Visibility="Collapsed"/>
<!—Here in the above Code we are just filling the rectangle With the transparent BackGround -->
<!—Creating A Border -->
<Border Name="popupBorder" Background="{StaticResource PhoneChromeBrush}" BorderBrush="Red" BorderThickness="2"
HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed">
<!-- Creating A grid Inside the Border and Rectangle -->
</Grid>
Create event for which popup should appear(for both cancel and appear)-
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
popupRect.Visibility = Visibility.Collapsed;
popupBorder.Visibility = Visibility.Collapsed;
}
private void popupButton_Click(object sender, RoutedEventArgs e)
{
popupRect.Visibility = Visibility.Visible;
popupBorder.Visibility = Visibility.Visible;
}
It will work, I guess.
Like spook says, put your gPopup Grid in a Popup element and show it by opening the popup. This won't affect the main visual tree.
The reason the embedded grid embiggens the border is that the outer grid has to expand to hold pGrid and the border expands to fill the outer grid.

WPF Zoom + Scrollbar?

I'm trying to zoom some contents within scrollviewer.
The zoom behavior I'm looking for is that of a RenderTransform+ScaleTransform. But this does not work with the ScrollViewer.
Using LayoutTransform+ScaleTransform, the scrollviewer does get affected (ContentTemplate1 only), but does not behave like a zoom.
Assuming ContentTemplate1/ContentTemplate2 cannot be changed (ie, 3rd party controls), how can I get zoom to work with a scrollviewer?
<Grid>
<Grid.Resources>
<!-- Content type 1 -->
<DataTemplate x:Key="ContentTemplate1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Background="DodgerBlue" Text="Left"/>
<TextBlock Grid.Column="1" Background="DarkGray" Text="Right"/>
</Grid>
</DataTemplate>
<!-- Content type 2 -->
<DataTemplate x:Key="ContentTemplate2">
<Viewbox>
<TextBlock Background="DodgerBlue" Text="Scale to fit" Width="100" Height="70" Foreground="White" TextAlignment="Center"/>
</Viewbox>
</DataTemplate>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TabControl>
<!-- Content 1 -->
<TabControl.Resources>
<ScaleTransform x:Key="ScaleTransform"
ScaleX="{Binding ElementName=ZoomSlider,Path=Value}"
ScaleY="{Binding ElementName=ZoomSlider,Path=Value}" />
</TabControl.Resources>
<TabItem Header="Content 1">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ContentControl ContentTemplate="{StaticResource ContentTemplate1}" Margin="10" RenderTransformOrigin=".5,.5">
<!-- Affects scrollviewer, but does not behave like a zoom -->
<!--<FrameworkElement.LayoutTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.LayoutTransform>-->
<!-- Expected zoom behavior, but doesn't affect scrollviewer -->
<FrameworkElement.RenderTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.RenderTransform>
</ContentControl>
</ScrollViewer>
</TabItem>
<!-- Content 2 -->
<TabItem Header="Content 2">
<ContentControl ContentTemplate="{StaticResource ContentTemplate2}" Margin="10" RenderTransformOrigin=".5,.5">
<!-- Affects scrollviewer, but does not behave like a zoom -->
<!--<FrameworkElement.LayoutTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.LayoutTransform>-->
<!-- Expected zoom behavior, but doesn't affect scrollviewer -->
<FrameworkElement.RenderTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.RenderTransform>
</ContentControl>
</TabItem>
</TabControl>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<!-- Zoom -->
<Slider x:Name="ZoomSlider"
Width="100"
Maximum="5"
Minimum="0.1"
Orientation="Horizontal"
Value="1" />
<!-- Autofit -->
<CheckBox Content="Autofit?" x:Name="AutoFitCheckBox" />
</StackPanel>
</Grid>
If I understand correctly:
You want to zoom with the ZoomSlider slider?
You want scrollbars to appear if the content is too large to fit within its tab?
If so, it's LayoutTransform you want. That transformation is done before all elements are measured and laid out, and the ScrollViewer will be able to tell whether scrollbars are needed.
On my machine, the "Content 1" tab works as expected if you just switch to LayoutTransform (note that you have to zoom a lot before "Right" disappears off-screen, toggling the scrollbar):
"Content 2" requires a little more work. First of all, there's no ScrollViewer in that tab, so that needs to be added. Secondly, ContentTemplate2 uses a ViewBox, which stretches by default, so zooming won't have an effect until you zoom in really close. To disable the ViewBox' built-in "zooming", you can center the ContentControl container (using HorizontalAlignment/VerticalAlignment), which forces it to take up as little space as possible:
<TabItem Header="Content 2">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ContentControl ContentTemplate="{StaticResource ContentTemplate2}" ...
HorizontalAlignment="Center" VerticalAlignment="Center" >
<FrameworkElement.LayoutTransform>
...
To make the zoomed elements get the exact RenderTransform look, we may as well stick with RenderTransform, and instead tell the ScrollViewer how to behave by implementing our own scrolling logic. This approach is based on this excellent tutorial:
https://web.archive.org/web/20140809230047/http://tech.pro/tutorial/907/wpf-tutorial-implementing-iscrollinfo
We create our own custom "ZoomableContentControl" which implements IScrollInfo and tell the ScrollViewer to get its scrolling logic from there (ScrollViewer.CanContentScroll = True). The magic happens in ArrangeOverride() where we play with ExtentWidth/ExtentHeight and RenderTransformOrigin.
public class ZoomableContentControl : ContentControl, IScrollInfo
{
public ZoomableContentControl()
{
this.RenderTransformOrigin = new Point(0.5, 0.5);
}
private ScaleTransform _scale = null;
private ScaleTransform Scale
{
get
{
if (_scale == null)
{
_scale = this.RenderTransform as ScaleTransform;
//RenderTransforms don't update the layout, so we need to trigger that ourselves:
_scale.Changed += (s, e) => { InvalidateArrange(); };
}
return _scale;
}
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
Statics.MessageIfDebug("Arranging");
var layout = base.ArrangeOverride(arrangeBounds);
var scale = this.Scale;
if (scale != null)
{
//Because RenderTransforms don't update the layout,
//we need to pretend we're bigger than we are to make room for our zoomed content:
_extent = new Size(layout.Width * scale.ScaleX, layout.Height * scale.ScaleY);
_viewport = layout;
//Coerce offsets..
var maxOffset = new Vector(ExtentWidth - ViewportWidth, ExtentHeight - ViewportHeight);
_offset.X = Math.Max(0, Math.Min(_offset.X, maxOffset.X));
_offset.Y = Math.Max(0, Math.Min(_offset.Y, maxOffset.Y));
//..and move the zoomed content within the ScrollViewer:
var renderOffsetX = (maxOffset.X > 0) ? (_offset.X / maxOffset.X) : 0.5;
var renderOffsetY = (maxOffset.Y > 0) ? (_offset.Y / maxOffset.Y) : 0.5;
this.RenderTransformOrigin = new Point(renderOffsetX, renderOffsetY);
if (ScrollOwner != null)
{
ScrollOwner.InvalidateScrollInfo();
}
}
return layout;
}
#region IScrollInfo
//This is the boilerplate IScrollInfo implementation,
//which can be found in *the first half* of this tutorial:
//https://web.archive.org/web/20140809230047/http://tech.pro/tutorial/907/wpf-tutorial-implementing-iscrollinfo
//(down to and including SetHorizontalOffset()/SetVerticalOffset()).
//Note the bug reported by "Martin" in the comments.
...
Usage:
<TabItem Header="Content 1">
<ScrollViewer CanContentScroll="True"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<v:ZoomableContentControl ContentTemplate="{StaticResource ContentTemplate1}" Margin="10" >
<FrameworkElement.RenderTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.RenderTransform>
</v:ZoomableContentControl>
</ScrollViewer>
</TabItem>
<TabItem Header="Content 2">
<ScrollViewer CanContentScroll="True"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<v:ZoomableContentControl ContentTemplate="{StaticResource ContentTemplate2}" Margin="10" >
<FrameworkElement.RenderTransform>
<StaticResource ResourceKey="ScaleTransform" />
</FrameworkElement.RenderTransform>
</v:ZoomableContentControl>
</ScrollViewer>
</TabItem>
My first advice is to check what kind of zooming functionality you can with using a commercial third party zoom control that already have support for ScrollViewer and also has many additional zooming and panning features.
Now to the solution of your problem:
You can make your code work with using LayoutTransform, but you will need to set the size of the ScrollViewer's content to a fixed value.
Currently you have a Grid inside a ScrollViewer. The Grid does not have its size defined, so it takes all the space it can get. So if you now scale the Grid, for example by factor 2, this means that the content of the Grid is scaled by factor 2, but the Grid will still take all the space it can get. If you would specify the width of the Grid to be 500, then scaling it by 2, would make the Grid's width 1000. But if you say: Grid, you can take all space that the parent gives you and then scale the Grid, it will be still the same. This means that scaling the auto-sized content of ScrollViewer will not show scrollbars.
In your sample this is true until the content of the Grid (the first column with width = 150 + width of the "Right" text in the second column) exceed the available size - at that point the DesiredSize of the Grid would be bigger than the size that ScrollViewer can provide and ScrollViewer will show scrollbars.
For example:
1) let's say that when you start your application, scale is set to 1 and ScrollViewer provides 500 points horizontally for the Grid. The Grid shows first column with 150 points width and shows "Right" text without any scale. The second column is set to fill the remaining space - so: 500 - 150 = 350 points is used by second column.
2) Now user sets the scale to 2. The Grid scales the first column to 300 points. This means that the second column can now take only 200 points. The Grid also scales the "Right" text, but the content (300 of first column + width of text) still does not exceed 500 point that are provided by the ScrollViewer.
3) User now sets scale to 3. Now the total width of the content of the grid exceed 500 points and this means that ScrollViewer will show scroll bars.
So having autosized controls, ScrollViewer and scaling does not work well.
But if you would fix the size of the Grid to 500, than you would get much more predictable results when scaling and using ScrollViewer. For example if you would scale by 10%, than the Grid's size would be 550 and would already exceed the size of the ScrollViewer - so ScrollViewer would show scrollbars. This would also give you the expected behaviour when you would increase the size of the window - the size of the Grid would remain the same and at some point the scrollbars would disappear (when the window would be big enough to show the whole content of the scaled Grid).
To conclude: my advice to you is to set the fixed size to the content of ScrollViewer controls. If you have a fixed size window, that you can set the Width and Height based on that size. Otherwise you can set it dynamically when the control is first loaded:
You can change the XAML for the content control into:
<ContentControl Name="ContentControl1"
ContentTemplate="{StaticResource ContentTemplate1}"
Margin="10"
Loaded="ContentControl1_OnLoaded" >
And also add the ContentControl1_OnLoaded handled that would just set the size to the initial size:
private void ContentControl1_OnLoaded(object sender, RoutedEventArgs e)
{
ContentControl1.Width = ContentControl1.ActualWidth;
ContentControl1.Height = ContentControl1.ActualHeight;
}
Zooming and panning may seem like a very simple task to do. But my experience shows (I am the author of ZoomPanel control) that this task can quickly become very complicated.

WPF Canvas Desired size

I have the following control:
<UserControl x:Class="FooBar.AnnotationControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="400" Width="500" >
<ScrollViewer Height="400" Width="500">
<Canvas Height="400" Width="500" Name="ctlCanvas" MouseLeftButtonDown="MouseLeftButtonDownHandler" MouseWheel="Canvas_MouseWheel" >
<Canvas.RenderTransform>
<ScaleTransform x:Name="ZoomTransform" />
</Canvas.RenderTransform>
</Canvas>
</ScrollViewer>
</UserControl>
namespace FooBar
{
public partial class AnnotationControl : UserControl
{
public AnnotationControl()
{
InitializeComponent();
}
private void MouseLeftButtonDownHandler( object sender, MouseButtonEventArgs args)
{
//Do Something
}
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
ctlCanvas.Measure(new Size(ctlCanvas.ActualWidth * ZoomTransform.ScaleX, ctlCanvas.ActualHeight * ZoomTransform.ScaleY));
}
}
}
I'm trying to get the scroll viewer to respond to the scaling of the Canvas. The call to Canvas.Measure doesn't appear to change the Desired size of the Canvas. Any idea what is going on here?
You should NOT call Measure on your own. This method is supposed to be called in the layout step, and not somewhere else. Also a RenderTransform doesn't change your Size. The RenderTransform is applied AFTER the actual Layout is done. So you have a scrollviewer that don't need to scroll its content, because its the same size. What you might want is LayoutTransform.
Canvas is the most primitive element and it simply not designed to work with the ScrollViewer. Use Grid/StackPanel/WarPanel/UniformGrid instead.
Ok, I seem to have found a solution. It looks like I can wrap my canvas with another canvas and when I scale it, I simply set the height and width for the outer canvas = initial height and width times the current X and Y scales of the ScaleTransform.

Resources