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.
Related
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.
Need help with a xaml Layout.
I have a Wrapanel with two elements inside. The left one, MainGrid, needs to be fixed at 900 pixels. The second one, AuxillaryDataScrollViewer, I would like to be a minimum of 650 and a maximum of 100% if the WrapPanel wraps it. Is this possible?
Here's what I got so far:
<Window x:Class="scratch.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="768" Width="1024">
<Grid>
<ScrollViewer>
<WrapPanel>
<Grid Width="900" Height="800" Background="Bisque"></Grid>
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Hidden" Width="650">
<Grid Width="1500" Height="700" Background="Tomato"></Grid>
</ScrollViewer>
</WrapPanel>
</ScrollViewer>
</Grid>
</Window>
Thanks!
Edit adding more detail:
Client would like to do data entry and see the calculated results realtime in a panel to the right, but some of the computers in his lab are only capable of 1280pixels width so on those machines he would like the results to wrap to below the data entry form.
Since the requirement is so well defined, you could simply add a SizeChanged handler, and set the second element's width manually per the container's width. If the container is less than 900 + 650, then stretch the second element to 100% of the container.
public MainWindow()
{
SizeChanged += MainWindow_SizeChanged;
}
void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
double firstElementWidth = MainGrid.Width; // 900
double secondElementMinWidth = AuxillaryDataScrollViewer.MinWidth; // 650
double secondElementWidth;
// if wrapped, stretch to the container
if (e.NewSize.Width < firstElementWidth + secondElementMinWidth)
secondElementWidth = e.NewSize.Width;
// otherwise (not wrapped), fill the remainder of the first row
else
secondElementWidth = e.NewSize.Width - firstElementWidth;
}
Obviously this is quick-and-dirty. If you need something more robust, then I suggest writing a custom panel that adheres to the conventions you need. The WrapPanel can't stretch elements to the overall width, but there's no reason you couldn't design a panel that does.
I am designing a user control that needs to be square, and to fill as much room as it is given (to give some context, it is a checkboard).
My user control looks like:
<Grid>
<!-- My 8 lines / colums, etc. , sized with "1*" to have equal lines -->
</Grid>
Now I would simply like to say "This grid has to be square no matter what room it has to expand".
Tried Solutions in vain:
I can't use a UniformGrid because I actually have the names of the lines & columns in addition, so I have a leading header row and column with different sizes.
If I use a Viewbox with Uniform it messes all up.
I tried using the classic
<Grid Height="{Binding RelativeSource={RelativeSource Self}, Path=Width}"> ... </Grid>
but it only works if I manually set the Width property. Otherwise, this constraint is ignored.
Conclusion
I'm out of idea, and I would really like to avoid setting Width / Height manually as this control may be used in many various places (ListItem templates, games, etc...).
Solution from suggestion:
A solution is available with some code-behind. I did not find a XAML only solution.
Grid is now:
<Grid SizeChanged="Board_FullControlSizeChanged">...</Grid>
And the event handler is:
private void Board_FullControlSizeChanged(object sender, SizeChangedEventArgs args)
{
double size = Math.min (args.NewSize.Height, args.NewSize.Width);
((Grid)sender).Width = size;
((Grid)sender).Height = size;
}
I initially tried modifying your binding to ActualWidth and it still did not work when the Grid was the top level element and in some cases it ended up expanding the control further than the available size. Hence tried some other ways of getting the required output.
Got 2 ways of maybe addressing this:
Since this is a view related issue (not breaking MVVM, keeping a square formation, if your ok with having a bit of code-behind, you could do something like)
private void OnSizeChanged(object sender, SizeChangedEventArgs e) {
double minNewSizeOfParentUserControl = Math.Min(e.NewSize.Height, e.NewSize.Width);
mainGrid.Width = minNewSizeOfParentUserControl;
mainGrid.Height = minNewSizeOfParentUserControl;
}
and in your xaml you would name your main top level grid "mainGrid" and attach the UserControl size changed event handler to the above function not the Grid itself.
However if you totally hate code-behind for whatever reason, you can be a bit more fancy and create a behavior such as
public class GridSquareSizeBehavior : Behavior<Grid> {
private UserControl _parent;
protected override void OnAttached() {
DependencyObject ucParent = AssociatedObject.Parent;
while (!(ucParent is UserControl)) {
ucParent = LogicalTreeHelper.GetParent(ucParent);
}
_parent = ucParent as UserControl;
_parent.SizeChanged += SizeChangedHandler;
base.OnAttached();
}
protected override void OnDetaching() {
_parent.SizeChanged -= SizeChangedHandler;
base.OnDetaching();
}
private void SizeChangedHandler(object sender, SizeChangedEventArgs e) {
double minNewSizeOfParentUserControl = Math.Min(e.NewSize.Height, e.NewSize.Width);
AssociatedObject.Width = minNewSizeOfParentUserControl;
AssociatedObject.Height = minNewSizeOfParentUserControl;
}
}
For the behavior your xaml would then look like:
<Grid>
<i:Interaction.Behaviors>
<local:GridSquareSizeBehavior />
</i:Interaction.Behaviors>
</Grid>
Did test these two methods with Snoop and the square size was maintained while expanding/shrinking. Do note both methods in the crux use the same logic(just a quick mock-up) and you might be able to squeeze some better performance if you update the logic to only update height when width is changed and vice versa instead of both and canceling a resize all together if not desired
Try putting your grid in a ViewBox: http://msdn.microsoft.com/en-us/library/system.windows.controls.viewbox.aspx
Here's a code sample I came up with:
The usercontrol:
<UserControl x:Class="StackOverflow.CheckBoard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Viewbox>
<Grid Background="Red" Height="200" Width="200">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="testing" Grid.Row="0"/>
<Button Content="testing" Grid.Row="1"/>
<Button Content="testing" Grid.Row="2"/>
</Grid>
</Viewbox>
</UserControl>
And the main window:
<Window x:Class="StackOverflow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StackOverflow"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:AllNoneCheckboxConverter x:Key="converter"/>
</Window.Resources>
<Grid>
<StackPanel>
<local:CheckBoard MaxWidth="80"/>
</StackPanel>
</Grid>
</Window>
What this Viewbox will do is scale the control to the space it's given. Since the grid inside the viewbox is square, the grid will ALWAYS stay square. Try playing around with the MaxWidth property I used in the MainWindow.
You could bind the Height property to ActualWidth instead of Width:
<Grid Height="{Binding ActualWidth, RelativeSource={RelativeSource Self}}">
...
</Grid>
However, a better solution would be to use a Viewbox. The trick to avoid that it "messes all up" is to make its Child square by defining (sensible) equal values for Width and Height:
<Viewbox>
<Grid Width="500" Height="500">
...
</Grid>
</Viewbox>
Register wherever it suits you (usually in constructor or OnAttached()):
SizeChanged += Square;
and handle size with this:
private void Square(object sender, SizeChangedEventArgs e)
{
if (e.HeightChanged) Width = e.NewSize.Height;
else if (e.WidthChanged) Height = e.NewSize.Width;
}
I have solved this by setting the margin of the contained control from inside the parent control's size changed event.
In my case I have a 'sudoku grid' user control called SudokuBoard inside a standard Grid control called MainGrid (which fills the main window) and it only requires the following code;
private void MainGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
double verticalMargin = Math.Max((e.NewSize.Height - e.NewSize.Width)*0.5, 0.0);
double horizontalMargin = Math.Max((e.NewSize.Width - e.NewSize.Height)*0.5, 0.0);
SudokuBoard.Margin = new Thickness(horizontalMargin, verticalMargin, horizontalMargin, verticalMargin);
}
I am trying to implement a Dots and Boxes style game in Silverlight for Windows Phone 7. What is the best way to draw the dot and box grid so that I get notified when someone touches the space between two boxes? What XAML elements should I look at using?
A polygon shaped like the image below, with an overlaid line, would be your best bet.
You will set the polygon fill (shown in blue) to 1% alpha so that it is not visible, but is hit-testable (0% alpha turns off hit testing).
If you create one as a usercontrol, you can simply place them around your grid of dots with 90% rotation on the vertical ones:
The dots can be simple ellipses (turn off isHitTestVisible on these):
You can then simply turn on/off the visibility of the lines in the user controls (which are always present for hit-testing):
I suggest a canvas for the outer control to give you fine position adjustment from code, but a grid will work too if you get the margin offsets right.
Usercontrol XAML (created with Blend):
<UserControl
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"
x:Class="DotsAndBoxes.Connector"
d:DesignWidth="280" d:DesignHeight="80">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Path Data="M27.706778,279.98367 L68.48111,239.30304 L266.99857,239.30304 L306.88052,278.89899 L266.99857,318.49493 L68.481102,318.49493 z"
Fill="#022E2EFB"
Stretch="Fill"
UseLayoutRounding="False"
IsHitTestVisible="True"
MouseLeftButtonDown="Path_MouseLeftButtonDown"/>
<Path Data="M0,40 L40.218182,40 L280,40" Height="5" Stretch="Fill" StrokeThickness="5" UseLayoutRounding="False" VerticalAlignment="Center" Stroke="White" IsHitTestVisible="False"/>
</Grid>
</UserControl>
Expose a "click" event on the User Control, that is called from a LeftMouseButtonDown event on the polygon and catch those click events in the high-level container:
namespace DotsAndBoxes
{
public partial class Connector : UserControl
{
public event EventHandler<System.Windows.Input.MouseButtonEventArgs> Clicked;
public Connector()
{
// Required to initialize variables
InitializeComponent();
}
private void Path_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (Clicked != null)
{
Clicked(this, e);
}
}
}
}
You could generate one of these polygons by hand as the coordinates required are quite simple.
In WinForms, Form had a ClientSize property (inherited from Control), which returns the size of its client area, i.e., the area inside the title bar and window borders.
I'm not seeing anything similar in WPF: there's no ClientSize, ClientWidth, ClientHeight, GetClientSize(), or anything else that I can think to guess the name of.
How do I go about getting the client size of a WPF Window?
One way you could do it is to take the top most child element, cast this.Content to its type, and call .RenderSize on it, which will give you its size.
<Window x:Class="XML_Reader.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="400" Width="600" WindowStyle="SingleBorderWindow">
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
</Grid>
</Window>
((Grid)this.Content).RenderSize.Height
((Grid)this.Content).RenderSize.Width
edit:
as Trent said, ActualWidth and ActualHeight are also viable solutions. Basically easier methods of getting what I put above.
var h = ((Panel)Application.Current.MainWindow.Content).ActualHeight;
var w = ((Panel)Application.Current.MainWindow.Content).ActualWidth;
One way to do it is with the code below. XAML:
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
<Canvas>
</Canvas>
</Window>
C#:
using System.Windows;
using System.IO;
using System.Xml;
using System.Windows.Controls;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
double dWidth = -1;
double dHeight = -1;
FrameworkElement pnlClient = this.Content as FrameworkElement;
if (pnlClient != null)
{
dWidth = pnlClient.ActualWidth;
dHeight = pnlClient.ActualHeight;
}
}
}
}
I used a Grid with VerticalAlignment=Top. As a result the Grid unfortunately didn't fill the parent Window anymore (which is its default behaviour, but the VerticalAligment property spoils it).
I solved it by putting an empty Border around the Grid. This border fills the complete content of the window, it has the same dimensions as the default border that a wpf window has anyways.
To get the Grid to fill the main window, I used the binding:
<Border BorderThickness="0" x:Name=Main>
<Grid VerticalAlignment="Top" Height="{Binding ElementName=Main, Path=ActualHeight}"> ...
</Grid>
</Border>
All the suggested solutions are based on the idea to use the size of Windows.Content to know what is the actual size available within the window, like this:
var h = ((Panel)Application.Current.MainWindow.Content).ActualHeight;
This of course only works if Window.Content is not null. Which is a problem if you want to set Window.Content from your code and you already then need to know exactly how much space is available.
The other problem is that the above code only provides the available space once a first layout cycle has completed (i.e. in the Window_Loaded event). But what do you do if you need to know the available space during the first layout cycle, for example because you draw to the window during Windows.OnRender() ?
The first control in the visual tree of any Window is always a Border, even if Window.Content is null. Interestingly, Border.RenderSize has already a value, even when RenderSize.ActualSize might still be zero. I guess the reason is that the size of the Border does not depend on Window.Content, but only on the size of the window (unless, of course, if Window.SizeToContent is used).
I recommend to place your code into the Window.SizeChanged event. Because each time the Window size changes, your content needs to change too. You cannot use the size provided in the event parameters, which gives you the size of the complete window, but you can get the the available size within the window like this:
var h = ((Border)GetVisualChild(0)).RenderSize.Height;
You can use that line of code also if you override Windows.OnRender().