WPF - Encapsulate custom Canvas in ScrollViewer - c#

I need to have a Custom Panel that acts as a Canvas but really is a ScrollViewer with a Canvas inside. The user should be able to add controls like in a canvas; I need the ScrollViewer wrapper to handle zoom and pan behaviours.
So, I started writing the control:
[ContentProperty("Children")]
public class MyContainer : ScrollViewer, IAddChild
{
private Canvas _innerCanvas;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public UIElementCollection Children
{
get { return _innerCanvas.Children; }
}
public MyContainer()
{
this._innerCanvas = new Canvas();
this.Content = _innerCanvas;
_innerCanvas.Height = this.ActualHeight;
_innerCanvas.Width = this.ActualWidth;
}
void IAddChild.AddChild(object value)
{
_innerCanvas.Children.Add(value as UIElement);
}
void IAddChild.AddText(string text)
{
;
}
}
but it doesn't work. If I try to use it in XAML
<Controls:MyContainer>
<TextBlock Height="92" Canvas.Left="324" TextWrapping="Wrap" Text="TextBlock 1" Canvas.Top="267" Width="284"/>
<TextBlock Height="92" Canvas.Left="462" TextWrapping="Wrap" Text="TextBlock 1" Canvas.Top="267" Width="284"/>
</Controls:MyContainer>
the children aren't visible.
What is wrong?

You set the size of your canvas to ActualWidth and ActualHeight at the Ctor, but they are not yet initialized at that point. ActualWidth and ActualHeight are only meaningful when the control is presented on the screen. Try to subscribe to the Loaded event of the ScrollViewer, and change the Canvas size then:
public MyContainer()
{
this._innerCanvas = new Canvas();
this.Content = _innerCanvas;
this.Loaded += ScrollViewer_Loaded;
}
public void ScrollViewer_Loaded(object sender, EventArgs e)
{
_innerCanvas.Height = this.ActualHeight;
_innerCanvas.Width = this.ActualWidth;
}

Related

How do I get the height and width from a DockPanel?

I am using a DockPanel to host some image enhancement controls. The image to be enhanced is in the 'center' dock position. I would like to have the image fill the entire area of the DockPanel. I have a custom class that implements the WPF Canvas control and takes a windows.Rect as input. This Rect will set the area for the image render function which I have overridden.
I have tried to get the DockPanel.ActualHeight and .ActualWidth. both are NaN and the DockPanel.Width and Height = 0.
public MainWindow()
{
ImageDataModel = new ImageDataModel();
DataContext = ImageDataModel;
InitializeComponent();
WindowState = WindowState.Maximized;
var tmpWindow = Window.GetWindow(BaseDockPanel);
Rect rect = new Rect(0, 0, BaseDockPanel.ActualWidth, BaseDockPanel.ActualHeight);
bitmapCanvas = new BitMapCanvas(rect);
BaseDockPanel.Children.Add(bitmapCanvas);
}
I would like to be able to create a windows.Rect that is the actual dimension of the DockPanel.center height and width. Currently the height and width for the control are 0.
At the point of the construction of MainWindow, the child controls within the Window, in your case BaseDockPanel has not been loaded yet.
That is why you're getting a 0 for it's ActualWidth and ActualHeight.
What you can do is add an EventHandler for the Window's Loaded event as shown below.
public MainWindow()
{
ImageDataModel = new ImageDataModel();
DataContext = ImageDataModel;
InitializeComponent();
WindowState = WindowState.Maximized;
this.Loaded += Window_Loaded;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var tmpWindow = Window.GetWindow(BaseDockPanel);
// You will get your ActualWidth and ActualHeight here.
Rect rect = new Rect(0, 0, BaseDockPanel.ActualWidth, BaseDockPanel.ActualHeight);
bitmapCanvas = new BitMapCanvas(rect);
BaseDockPanel.Children.Add(bitmapCanvas);
}

How to enable both scrolling and zooming using pinch in WPF?

I am struggling with making both touch events and manipulation work properly in a WPF project. I have a ScrollViewer which contains a picture and I would like to scroll both horizontally and vertically using a swipe gestures. Additionally, I would like to zoom in/out in the center of the pinch gesture. The code below achieves what I wish, but it has the following problems:
Sometimes the scrolling is laggy;
The scrolling does not work on the first try, only when attempting the same gesture a second time;
The zoom in/out does not work on the first try, only when attempting the same gesture a second time.
I enabled the IsManipulationEnabled and I implemented the code for zoom in/out functionality. However, I was not able to combine it with the scrolling functionality (by setting the PanningMode in the ScrollViewer only). Therefore, I created a custom control which inherits from Image control and I overwritten the OnTouchDown and OnTouchUp event handlers. Basically, what I am doing in these overwritten handlers is counting the number of touches on the screen and enabling/disabling manipulation. I also tried setting the PanningMode for the ScrollViewer, but it did not do the trick.
Below is the XAML:
<Grid>
<ScrollViewer
x:Name="ScrollViewerParent"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
PanningMode="Both">
<local:CustomImage
x:Name="MainImage"
Source="{Binding Source={x:Static local:Constants.ImagePath}}"
IsManipulationEnabled="True"
ManipulationStarting="MainImage_ManipulationStarting"
ManipulationDelta="MainImage_ManipulationDelta">
</local:CustomImage>
</ScrollViewer>
</Grid>
Here is the code-behind:
public partial class MainWindow : Window
{
private void MainImage_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
e.ManipulationContainer = ScrollViewerParent;
e.Handled = true;
}
private void MainImage_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
var matrix = MainImage.LayoutTransform.Value;
Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);
if (centerOfPinch == null)
{
return;
}
var deltaManipulation = e.DeltaManipulation;
matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
MainImage.LayoutTransform = new MatrixTransform(matrix);
Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);
double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;
double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;
double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;
ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);
e.Handled = true;
}
}
The XAML for the custom control:
<Style TargetType="{x:Type local:CustomImage}" />
Here is where I override the OnTouchDown and OnTouchUp event handlers:
public class CustomImage : Image
{
private volatile int nrOfTouchPoints;
private volatile bool isManipulationReset;
private object mutex = new object();
static CustomImage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomImage), new FrameworkPropertyMetadata(typeof(CustomImage)));
}
protected override void OnTouchDown(TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints++;
if (nrOfTouchPoints >= 2)
{
IsManipulationEnabled = true;
isManipulationReset = false;
}
}
base.OnTouchDown(e);
}
protected override void OnTouchUp(TouchEventArgs e)
{
lock (mutex)
{
if (!isManipulationReset)
{
IsManipulationEnabled = false;
isManipulationReset = true;
nrOfTouchPoints = 0;
}
}
base.OnTouchUp(e);
}
}
What I expect from this code is the following:
When using one finger to swipe horizontally or vertically across the touchscreen, the image should be scrolled accordingly;
When I use a pinch gesture on the touch screen, the image should be zoomed in/out in the center of the pinch.
Fortunately, I managed to find the perfect solution. Therefore, I am going to post the answer in the case that someone is working on a similar problem and needs some help.
What I did:
Got rid of the custom control as it was not necessary;
Create a field which counts the number of the touch points;
Implemented the TouchDown event handler, which increases the number of touch points by 1 (this method is called each time there is a touch down gesture on the device);
Implemented the TouchUp event handler, which decreases the number of touch points by 1 (this method is called each time there is a touch up gesture on the device);
In the Image_ManipulationDelta event handler, I check the number of touch points:
if the number of touch points < 2, then the translation value is added to the current offset of the scrollbars, thus achieving scrolling;
otherwise, the center of the pinch is calculated and a scale gesture is applied.
Here is the full XAML:
<Grid
x:Name="GridParent">
<ScrollViewer
x:Name="ScrollViewerParent"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
PanningMode="Both">
<Image
x:Name="MainImage"
Source="{Binding Source={x:Static local:Constants.ImagePath}}"
IsManipulationEnabled="True"
TouchDown="MainImage_TouchDown"
TouchUp="MainImage_TouchUp"
ManipulationDelta="Image_ManipulationDelta"
ManipulationStarting="Image_ManipulationStarting"/>
</ScrollViewer>
</Grid>
Here is the entire code discussed above:
public partial class MainWindow : Window
{
private volatile int nrOfTouchPoints;
private object mutex = new object();
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void Image_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
e.ManipulationContainer = ScrollViewerParent;
e.Handled = true;
}
private void Image_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
int nrOfPoints = 0;
lock (mutex)
{
nrOfPoints = nrOfTouchPoints;
}
if (nrOfPoints >= 2)
{
DataLogger.LogActionDescription($"Executed {nameof(Image_ManipulationDelta)}");
var matrix = MainImage.LayoutTransform.Value;
Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);
if (centerOfPinch == null)
{
return;
}
var deltaManipulation = e.DeltaManipulation;
matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
MainImage.LayoutTransform = new MatrixTransform(matrix);
Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);
double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;
double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;
double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;
ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);
e.Handled = true;
}
else
{
ScrollViewerParent.ScrollToHorizontalOffset(ScrollViewerParent.HorizontalOffset - e.DeltaManipulation.Translation.X);
ScrollViewerParent.ScrollToVerticalOffset(ScrollViewerParent.VerticalOffset - e.DeltaManipulation.Translation.Y);
}
}
private void MainImage_TouchDown(object sender, TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints++;
}
}
private void MainImage_TouchUp(object sender, TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints--;
}
}
}
}

UWP Layout Cycle inside StackPanel when resizing children

I have a StackPanel with 2 children that I need to resize based on the StackPanels size. I attached an event handler to SizeChanged of the StackPanel and resize the element there. The problem is the event handler is fired again and again until the app crashes. What's the deal?
class BasicStatusTile : StackPanel {
private TextBlock TopText { get; set; }
private TextBlock BottomText { get; set; }
public BasicStatusTile() {
InitializeComponent();
}
private void InitializeComponent() {
TopText = new TextBlock() {
Text = "0",
HorizontalAlignment = Windows.UI.Xaml.HorizontalAlignment.Stretch,
TextAlignment = Windows.UI.Xaml.TextAlignment.Center,
FontSize = 48
};
BottomText = new TextBlock() {
Text = "Speed (km/h)",
HorizontalAlignment = Windows.UI.Xaml.HorizontalAlignment.Stretch,
VerticalAlignment = Windows.UI.Xaml.VerticalAlignment.Bottom,
TextAlignment = Windows.UI.Xaml.TextAlignment.Center
};
base.Children.Insert(0, TopText);
base.Children.Insert(1, BottomText);
base.Background = new SolidColorBrush(Colors.White);
base.SizeChanged += BasicStatusTile_SizeChanged;
}
private void BasicStatusTile_SizeChanged(object sender, Windows.UI.Xaml.SizeChangedEventArgs e) {
double third = base.ActualHeight / 6;
TopText.FontSize = third * 4;
BottomText.FontSize = third;
}
}
This happened because you change FontSize of TextBlock. It fires the sizechanged event of your custom StackPanel.
You should consider what kind of control you need.
What is your purpose of using custom stackpanel with sizechanged event?
Also, FontSize is not the same as a Height of TextBlock

how to make user control draggable only from specific places

im working now on a UserControl which ive made draggable using the code below (which is quite known and used). This UserControl looks and is used in a similar to MessageBox (the gray color and the blue rectangle) , the task is to make this UserControl draggable only from the blue rectangle just as any MessageBox , not as its now draggable from any place inside it!
any suggestions on how to be able to do this? thanks in advance!
below the code used to drag the UserControl
public UserControl1(Data data, Settings settings)
{
InitializeComponent();
MouseLeftButtonDown += new MouseButtonEventHandler(root_MouseLeftButtonDown);
MouseLeftButtonUp += new MouseButtonEventHandler(root_MouseLeftButtonUp);
MouseMove += new MouseEventHandler(root_MouseMove);
}
...
private void root_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var element = sender as FrameworkElement;
anchorPoint = e.GetPosition(null);
element.CaptureMouse();
isInDrag = true;
e.Handled = true;
}
private void root_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (isInDrag)
{
var element = sender as FrameworkElement;
element.ReleaseMouseCapture();
isInDrag = false;
e.Handled = true;
}
}
private void root_MouseMove(object sender, MouseEventArgs e)
{
if (isInDrag)
{
var element = sender as FrameworkElement;
currentPoint = e.GetPosition(null);
UIElement parentElement = (UIElement)this.Parent;
maxHeightParent = parentElement.RenderSize.Height;
maxWidthParent = parentElement.RenderSize.Width;
maxHeight = RenderSize.Height;
maxWidth = RenderSize.Width;
//Window.ActualHeightProperty
//element.ActualHeight
transform.X += (currentPoint.X - anchorPoint.X);
transform.Y += (currentPoint.Y - anchorPoint.Y);
this.RenderTransform = transform;
anchorPoint = currentPoint;
}
}
}
Two ways to solve this, which are actually the same way with different syntax:
Via code - in the constructor, instead of
MouseLeftButtonDown += new MouseButtonEventHandler(root_MouseLeftButtonDown);
MouseLeftButtonUp += new MouseButtonEventHandler(root_MouseLeftButtonUp);
MouseMove += new MouseEventHandler(root_MouseMove);
use
myBlueRect.MouseLeftButtonDown +=
new MouseButtonEventHandler(root_MouseLeftButtonDown);`
myBlueRect.MouseLeftButtonUp +=
new MouseButtonEventHandler(root_MouseLeftButtonUp);
myBlueRect.MouseMove += new MouseEventHandler(root_MouseMove);
Via XAML - in the tag of the relevant element that you want to grag from (lets say it's a Grid):
<Grid x:Name="myBlueRect"
MouseLeftButtonDown="root_MouseLeftButtonDown"
MouseLeftButtonUp="root_MouseLeftButtonUp"
MouseMove="root_MouseMove"
.../>
Two basic options I think about:
Inside your UserControl make the rectangle a Border (or other control), and put the events on this Border and not on the UserControl.
If from some reason you do want the events to be on the UserControl, put a Hidden Border on the place of the rectangle, and in the event do the dragging only if the mouse is over this Border.

WPF UI AddIn not updating its desired size

I have an application that gets one of its UI controls via an INativeHandleContract from a different AppDomain. When the size of the control changes the FrameworkElement in the host doesn't get an updated size. I was wondering if there was any way to fix this.
Here is the sample code (the XAML is just a blank window):
public partial class Window1 : Window
{
private StackPanel m_stackPanel;
private Expander m_expander;
private UIElement m_expanderAddIn;
public Window1()
{
InitializeComponent();
m_stackPanel = new StackPanel { Orientation = Orientation.Vertical };
m_stackPanel.Background = Brushes.Red;
m_expander = new Expander
{
ExpandDirection = ExpandDirection.Right,
Background=Brushes.Blue,
IsExpanded=true,
};
m_expander.Expanded += CheckStuff;
m_expander.Collapsed += CheckStuff;
Rectangle r = new Rectangle {Fill = Brushes.LightGray, Height = 300, Width = 300};
m_expander.Content = r;
m_expanderAddIn = FrameworkElementAdapters.ContractToViewAdapter(FrameworkElementAdapters.ViewToContractAdapter(m_expander));
m_stackPanel.Children.Add(m_expanderAddIn);
Content = m_stackPanel;
}
private void CheckStuff(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Expander: " + m_expander.DesiredSize);
Debug.WriteLine("Add in: " + m_expanderAddIn.DesiredSize);
Debug.WriteLine("Stack Panel: " + m_stackPanel.DesiredSize);
}
}
As you expand and collapse the Expander you would expect the height of the StackPanel to change but it doesn't. Any ideas would be useful, thanks.
That's really weird and I have no idea why it behaves like that. This is probably just a small example reproducing your problem so maybe this doesn't help you much but I got your example working with 3 changes.
Wrap the Expander in a "dummy" StackPanel and use that as the root element for m_expanderAddIn instead. This took care of the Height problem for the Expander.
Change the type of m_expanderAddIn from UIElement to FrameworkElement
Bind the Height of m_expanderAddIn to ActualHeight of m_expander
Code
public partial class Window1 : Window
{
private StackPanel m_stackPanel;
private Expander m_expander;
private FrameworkElement m_expanderAddIn;
public Window1()
{
InitializeComponent();
m_stackPanel = new StackPanel { Orientation = Orientation.Vertical };
m_stackPanel.Background = Brushes.Red;
m_expander = new Expander
{
ExpandDirection = ExpandDirection.Right,
Background=Brushes.Blue,
IsExpanded=true,
};
m_expander.Expanded += CheckStuff;
m_expander.Collapsed += CheckStuff;
Rectangle r = new Rectangle {Fill = Brushes.LightGray, Height = 300, Width = 300};
m_expander.Content = r;
StackPanel stackPanel = new StackPanel();
stackPanel.Children.Add(m_expander);
m_expanderAddIn = FrameworkElementAdapters.ContractToViewAdapter(FrameworkElementAdapters.ViewToContractAdapter(stackPanel));
Binding binding = new Binding("ActualHeight");
binding.Source = m_expander;
m_expanderAddIn.SetBinding(FrameworkElement.HeightProperty, binding);
m_stackPanel.Children.Add(m_expanderAddIn);
Content = m_stackPanel;
}
private void CheckStuff(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Expander: " + m_expander.DesiredSize);
Debug.WriteLine("Add in: " + m_expanderAddIn.DesiredSize);
Debug.WriteLine("Stack Panel: " + m_stackPanel.DesiredSize);
}
}

Categories