WPF item panel that keeps aspect ratio - wpf

I have an ItemsControl which may contain an arbitrary number of items (unknown at design time). Each of these items is basically represented as an infinitely-scalable image with a fixed aspect ratio (ie. the image will draw itself in whatever space is given to it -- it does not dictate its own size except that the aspect ratio must be preserved). The aspect ratio for each item might differ but is usually the same.
I want to:
Draw a border around each image, ideally of uniform thickness regardless of image scaling.
Draw each image as large as possible within the window, while maintaining its order, aspect ratio, and margins external to the border.
At least two of the four sides of an image must always touch the invisible boundaries of the cell it is within. The other two sides should be centred if not touching. (Assuming that some sort of uniform cell layout is used.)
Not overlap or clip any images.
Automatically re-layout as the containing window is resized.
Waste the minimum amount of non-image space.
Cope as well as possible if the aspect ratios of the items differ. (But it's ok if this increases the wasted space of other items, as long as they rescale to fit the result.)
The general consensus that I've found seems to be to wrap each image in a Stretch.Uniform Viewbox, and then put those into a UniformGrid. I've tried that approach but it doesn't appeal to me because:
Given two items, the UniformGrid always wants to create a 2x2 grid layout even when the window shape would make a 2x1 or 1x2 grid more suitable, which results in over-scaling and wasted space.
If I put the Border within the Viewbox then it scales the border thickness. If I put it outside then it distorts the aspect ratio.
Is there a better way to do this? (Note that the "image" is actually my own custom-draw FrameworkElement, so I can put custom measure/arrange code in here or in a custom container panel if it will help.)
<UniformGrid>
<Rectangle Fill="Red" Margin="4" Width="500" Height="281.25" />
<Rectangle Fill="Blue" Margin="4" Width="500" Height="281.25" />
</UniformGrid>
Here's a simple example. Put this into a window, then try resizing the window. The rectangles change size to fit the window (good), but they also change shape/aspect (bad), and they stop resizing once the window is sufficiently large (bad). Also it leaves space for an entire 2x2 grid even when the window itself is sized such that 1x2 or 2x1 would work better.
<UniformGrid>
<Viewbox Stretch="Uniform" Margin="4"><Rectangle Fill="Red" Width="500" Height="281.25" /></Viewbox>
<Viewbox Stretch="Uniform" Margin="4"><Rectangle Fill="Blue" Width="500" Height="281.25" /></Viewbox>
</UniformGrid>
This works marginally better in that the rectangles continue to stretch when the window gets large, and they no longer distort their shape, but there's still the needless second row when the window is wide or second column when the window is narrow. And I would prefer that the elements line up from the top left (like a WrapPanel) rather than centering, but that's a minor detail.
(And now try adding a Border, both inside and outside of the Viewbox, and see what I mean there.)
Actually a WrapPanel almost does what I want, except that it auto-sizes the items too large when the window gets smaller.

Related

Button height in fraction

Is it possible to have button size in fractions?
I want the height to be specified as 23.7, but when I run the app and check the actual height, it is rounded to 24. The height and width are of type double so I expected it to take and honor the fractional values, but for some reason it is rounding up.
Is it possible to have fractional height with some settings/hack?
<Button
Grid.Column="0"
Margin="0,12,0,0"
Width="80"
Height="23.7"
Content="Market"
Command="{Binding OrderSellMarketButtonCommand}"/>
I even tried binding the Height to VM property of type double. That did not help either.
Screenshot of the issue:
The double type is slightly misleading in that case, because the actual size of the control depends on the currently set DPI value. Normally, at 100% the DPI value is 96 and since the default WPF measuring unit is a device independent pixel (which is 1/96 inch) it maps to whole pixels which aren't fractions.
It's a bit strange that you want to align something in the sub-pixel range, can you maybe show us an example or screenshot?
Edit: After seeing you screenshot, it looks like that the text in the middle has a slightly larger fontsize than the buttons. If it's acceptable, you can up the fontsize on the button by one:
<Button
Grid.Column="0"
Margin="0,12,0,0"
Width="80"
Height="23"
Command="{Binding OrderSellMarketButtonCommand}">
<Button.Content>
<TextBlock Text="Market" FontSize="13"/>
</Button.Content>
</Button>
Layout is complex in WPF. i put here a translation of a training slide.
Since WPF is completely independent of resolution, the prerequisite is never to specify sizes or coordinates so that the layout of the elements works regardless of the resolution or size of the screen.
The layout management process is based on the following principle:
That each element (a potential container) intervenes
That every container, for all these children
Demands their "desired" size
Then arranges them according to his "model" of organization
These 2 main methods are available in the Panel and are overloadable when creating custom controls and containers
MeasureOverride
ArrangeOverride
This means you will have no chance to get the values you put in Width and Height into the real ActualWidth and ActualHeight - Or you have a complete fixed layout but that will not fit with "independent of resolution"

WPF control origin point

After some investigation, I still can't find method to change origin of control.
So, I want just to place one square exactly in center of another square, without margins, so it will be completely independent of first square size.
Theoretically, it can be easily done with HorizontalAlignment and VerticalAlignment set to Center, since it automatically sets Margin of control to half of width and height of parent control. But it is not so simple.
Simplest way to describe problem is next picture
As you can see, margin is counted towards upper left corner. Which is what I call origin. The perfect solution is to change it to center of first square, but this is where I need help - how can I do that?
Point of origin applies when using a transform object, and attaching the transform to your control. It won't actually effect the behaviour of the margin or left, top properties. If using a transform to place your object, point of origin is very useful.
The top, left (if using cavas) and margin (if using say grid) help govern the "auto" placement by the parent control, and this in turn governs where point of origin for the control winds up being relative to the parent control. The transform object then offsets RELATIVE to where that point of origin is.
The other useful thing is that transform overrides the auto placement in the parent control, or rather, forces an offset to where the parent wants to put it, which in some cases is useful - i.e., you might have boxes listed in a grid and want them to "shake" left and right when you hover the mouse over them, their alignment stays in order to the grid, but the transform lets you bump them away from their "forced" position.
For example, attach the same transform object to 2 controls, and set their origins separate, then apply an animation to the transform object - both controls will animate off the one animation object (if you wanted to their movement in perfect sync).
Well, it was weird enough. The given behaviour can be seen only when using Image, and Center alignment. Can be solved by either wrapping Image in Grid, which will be using Center alignment, or using Stretch alignment with Image (which is much simplier).
<Grid Width="500" Height="500">
<Image Width="250" Height="250" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
</Grid>
If you want to reproduce problem I've described in question, replace Stretch with Center in code above.
Probably oversimplifying here but I would just use a Grid to wrap the two items you mentioned like this example (One stretched to fit and one centered):
<Grid>
<Border HorizontalAlignment="Stretch" VerticalAlignment="Stretch" BorderBrush="Black" BorderThickness="10" Margin="4"/>
<Button HorizontalAlignment="Center" VerticalAlignment="Center" Padding="10">InnerButton</Button>
</Grid>

Scrollviewer & Canvas

I am trying to load an image within a canvas such that, if the size of image overflows the canvas, the scroll bars should get activated (MS Paint style)
<Window>
<ScrollViewer>
<Canvas ScrollViewer.HorizontalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollBarVisibility="Visible">
<Image Source="SampleImage.jpg" />
</Canvas>
</ScrollViewer>
</Window>
Now as Canvas is stretched to Window's size, scroll-bars won't appear as Canvas is not actually overflowing out of the Window.
Secondly, as the Image is much bigger than the Canvas, it is getting clipped at the bounds of Canvas, so ScrollViewer doesn't think that its content: Canvas is actually overflowing.
It happens a lot of time with StackPanels too, that even though the bound data has tens of rows, but still the scrollbars don't get activated. Sometimes scrollviewers appear as mystery to me.
So, what should be the basic logic kept in mind when using ScrollViewer control.
Thank you.
Edit: Just edited the question title, so that whosoever has problem with canvas can get this question easily in search.
From MSDN:
Canvas is the only panel element that has no inherent layout characteristics. A Canvas has default Height and Width properties of zero, unless it is the child of an element that automatically sizes its child elements. Child elements of a Canvas are never resized, they are just positioned at their designated coordinates. This provides flexibility for situations in which inherent sizing constraints or alignment are not needed or wanted. For cases in which you want child content to be automatically resized and aligned, it is usually best to use a Grid element.
Hovever, you can set Canvas Height and Width explicitely:
<ScrollViewer Height="100" Width="200">
<Canvas Height="400" Width="400">
//Content here
</Canvas>
</ScrollViewer>
Maybe one of these two Links might help you:
Silverlight Canvas on Scrollviewer not triggering
ScrollBars are not visible after changing positions of controls inside a Canvas

Tiling rectangles seamlessly in WPF

I want to seamlessly tile a bunch of different-colored Rectangles in WPF. That is, I want to put a bunch of rectangles edge-to-edge, and not have gaps between them.
If everything is aligned to pixels, this works fine. But I also want to support arbitrary zoom, and ideally, I don't want to use SnapsToDevicePixels (because it would compromise quality when the image is zoomed way out). But that means my Rectangles sometimes render with gaps. For example:
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="Black">
<Canvas SnapsToDevicePixels="False">
<Canvas.RenderTransform>
<ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
</Canvas.RenderTransform>
<Rectangle Canvas.Left="25" Width="100" Height="100" Fill="#CFC"/>
<Rectangle Canvas.Left="125" Width="100" Height="100" Fill="#CCF"/>
</Canvas>
</Page>
If the ScaleTransform's ScaleX is 1, then the Rectangles fit together seamlessly. When it's 0.5, there's a dark gray streak between them. I understand why -- the combined semi-transparent edge pixels don't combine to be 100% opaque. But I would like a way to fix it.
I could always just make the Rectangles overlap, but I won't always know in advance what patterns they'll be in (this is for a game that will eventually support a map editor). Besides, this would cause artifacts around the overlap area when things were zoomed way in (unless I did bevel-cut angles on the underlapping portion, which is an awful lot of work, and still causes problems at corners).
Is there some way I can combine these Rectangles into a single combined "shape" that does render without internal gaps? I've played around with GeometryDrawing, which does exactly that, but then I don't see a way to paint each RectangleGeometry with a different-colored brush.
Are there any other ways to get shapes to tile seamlessly under an arbitrary transform, without resorting to SnapsToDevicePixels?
You might consider using guidelines (see GuidelineSet on MSDN) and overriding the Rectangles' OnRender methods so that their boundaries line up with the pixel boundaries of the device. WPF uses guidelines to determine whether and where to snap drawings.
Internally, it's exactly what SnapsToDevicePixels is using to ensure that objects line up with the device's pixels, but by placing guidelines manually you'll be able to control when the snapping behaviour is applied and when it is not (so when your image is zoomed all of the way out, you can avoid drawing guidelines, or only draw guidelines where your shapes lie next to other shapes, and rely on WPF's anti-aliasing to take care of the rest). You might be able to do it with an attached property so that you can apply it to any element, though if it's only one type of element (e.g. Rectangle) that you need this behaviour on, it's probably not worth the extra effort.
It seems like Microsoft is aware of this problem, too - WPF 4.0 is expected to feature Layout Rounding, which, like the version in Silverlight, rounds off non-integer values at the Render pass when layout rounding has been enabled.
I guess the gaps are not actual gaps but the stroke that is painted. When you scale it down than you just make the stroke smaller to a point where it is not visible anymore. I tried to paint the stroke in the color of the rectangle wich works just fine on any scale.
&ltPage xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="Black"&gt
&ltCanvas SnapsToDevicePixels="False"&gt
&ltCanvas.RenderTransform&gt
&ltScaleTransform ScaleX="0.5" ScaleY="0.5"/&gt
&lt/Canvas.RenderTransform&gt
&ltRectangle Canvas.Left="25" Width="100" Height="100" Fill="#CFC" Stroke="#CFC"/&gt
&ltRectangle Canvas.Left="125" Width="100" Height="100" Fill="#CCF" Stroke="#CCF"/&gt
&lt/Canvas&gt
&lt/Page&gt

WPF Resizable Canvas

I need to implement a Canvas which scales its contents according to its size. I know there is Viewbox, which scales everything inside of it. However I cannot use that, because some elements have a fixed size and cannot be scaled.
Also how can I bind the size of the Canvas to the parent element (for example a resizable window). There is sizeToContent for windows, I want the size fitting exactly the other way round. Also the canvas uses some drawing based on the size of the hosting element, how is redraw triggered and how can I ensure that it only draws if it gets a valid (or min) size?
If you don't specify any width or height to the canvas it automatically uses all the available space. This is because the default VerticalAlignment and HorizontalAlignment are set to Stretch.
What do you mean by canvas that scales it's contents according to it's size without scaling all the contents as some have fixed size?
Update after comments
If your drawing algorithm already scales the content to the canvas' height and width then all you need to do is to resize the canvas to fit the area I believe? In that case just remove the hardcoded height/width values and the canvas will resize to fit the container.
You might need to use ActualHeight/ActualWidth instead of Height/Width in the drawing algorithm after this though. ActualHeight/ActualWidth return the values that the layout container will give your canvas so these represents the values the canvas is drawn with.
I think you can find the answers to all your questions in my London Underground demo.
I'm doing this from memory, but if I recall correctly a Window uses either a Panel or a Canvas as part of it's ControlTemplate (in which lies the ContentPresenter), which means that a Canvas placed directly in a Window will have issues resizing automatically like it might elsewhere. There are a few basic ways to address this.
1 Write a new ControlTemplate for your Window to use. :(2 Place your content directly in the Window rather than in a Canvas inside the Window. :/
3 Do a by-name binding. :)
<MyWindow x:Name="topWindow">
<Canvas x:Name="topCanvas" Width="{Binding ElementName=topWindow, Path=ActualWidth}" Height="{Binding ElementName=topWindow, Path=ActualHeight}">
...Content...
</Canvas>
</MyWindow>
(As it happens, I often bind grids inside Canvases in this fashion, so I can easily animate items moving from one grid position to another.)

Resources