Problem with custom scrolling in custom panel - wpf

I'm coding a custom panel representing the hand of cards. It's a panel that will stack the cards horizontally. If there isn't enough space, each card will overlap part of the card left of it. Minimum part should be always visible. I accomplished this and this is the code:
using System;
using System.Windows;
using System.Windows.Controls;
namespace Hand
{
public class Hand : Panel
{
//TODO Should be dependancy property
private const double MIN_PART = 0.5;
protected override Size MeasureOverride(Size availableSize)
{
Size desiredSize = new Size();
foreach (UIElement element in this.Children)
{
element.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
desiredSize.Width += element.DesiredSize.Width;
desiredSize.Height = Math.Max(desiredSize.Height, element.DesiredSize.Height);
}
return desiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
//percentage of the visible part of the child.
double part = 1;
Double desiredWidth = 0;
//TODO Check how to get desired size because without looping
//this.DesiredSize is minimum of available size and size returned from MeasureOverride
foreach (UIElement element in this.Children)
{
desiredWidth += element.DesiredSize.Width;
}
if (desiredWidth > this.DesiredSize.Width)
{
//Every, but the last child should be overlapped
double lastChildWidth = this.Children[this.Children.Count - 1].DesiredSize.Width;
part = (this.DesiredSize.Width - lastChildWidth) / (desiredWidth - lastChildWidth);
part = Math.Max(part, MIN_PART);
}
double x = 0;
foreach (UIElement element in this.Children)
{
Rect rect = new Rect(x, 0, element.DesiredSize.Width, element.DesiredSize.Height);
element.Arrange(rect);
finalSize.Width = x + element.DesiredSize.Width;
x += element.DesiredSize.Width * part;
}
return finalSize;
}
}
}
I would like to add scrollbar when minimum part is reached, so that the user could still be able to view all the cards. I cannot accomplish this. I tried with the ScrollViewer like this:
<Window x:Class="TestScrollPanel.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:h="clr-namespace:Hand;assembly=Hand"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<h:Hand>
<Button Width="100">One</Button>
<Button Width="150">Two</Button>
<Button Width="200">Three</Button>
</h:Hand>
</ScrollViewer>
</Grid>
</Window>
But this doesn't work because once horizontal scrollbar is visible, MeasureOveride and ArrangeOverride of Hand panel is never called and even if it would be called, Hand would get desired size to arrange all children without overlapping.
Could this be made with ScrollViewer at all and if not, another ideas would be appreciated.
Thank you all for ypur help.
Jurica

Firstly, change your panel's logic to just the opposite: let MeasureOverride pack the cards as tightly as possible, and then let ArrangeOverride spread them evenly over whatever width is given.
Secondly, use the MinWidth property. Bind it to ScrollViewer.ActualWidth.
This way, if the cards can be tightly packed into width less than that of the ScrollViewer, then your Hand will be stretched to all available space. And if they can't, then the Hand's width will be just whatever you calculate it to.

Related

WPF: How to make two controls (list views) share whole available space between them?

I have two list views (vertically aligned) which might have various number of items. I'd like them to share space proportionally when needed (this I can achieve with regular grid and *) but when one list view doesn't have many items to show I would like other list view to fill the whole space. And vice versa.
Tried different things but could not achieve this behavior.
For instance with grid I can specify * and * (or other proportions) but it means that half of the space will be empty if one of list views does not have any items (and another has tons of them).
Is there a way to achieve this? Do I need to implement my own Panel for this or there is another (simpler) way to do it?
Thank you!
Zaki
OK, try this code:
class MyPanel : Panel
{
protected override Size MeasureOverride(Size constraint)
{
// first measuring desired size of children
var availableSize = new Size(constraint.Width, double.PositiveInfinity);
foreach (UIElement ui in InternalChildren)
ui.Measure(availableSize);
var totalHeight = InternalChildren.OfType<UIElement>().Sum(x => x.DesiredSize.Height);
// now resizing children within constraint
var factor = (totalHeight == 0 ? 1.0 : constraint.Height / totalHeight);
foreach (UIElement ui in InternalChildren)
ui.Measure(new Size(constraint.Width, ui.DesiredSize.Height * factor));
var maxWidth = InternalChildren.OfType<UIElement>().Max(x => x.DesiredSize.Width);
return new Size(Math.Min(constraint.Width, maxWidth), Math.Min(constraint.Height, totalHeight));
}
protected override Size ArrangeOverride(Size arrangeSize)
{
// aligning children vertically
var totalHeight = InternalChildren.OfType<UIElement>().Sum(ui => ui.DesiredSize.Height);
var y = 0.0;
var rect = new Rect(arrangeSize);
foreach (UIElement ui in InternalChildren)
{
rect.Y += y;
y = ui.DesiredSize.Height;
rect.Height = y;
ui.Arrange(rect);
}
return arrangeSize;
}
}
This panel would arrange children vertically and give children vertical space proportionally to their desired height, but won't allow them take more space than available.
So, if, for example, you have 200px height available, first list view wants 150px, and second wants 100px, they will be scaled down to 120px + 80px == 200px
Just would like to share the final version which does what I wanted. Thank you to torvin for providing the right direction, appreciate quick and valuable response!
=================================================================
Implemented resizing of main window in such a way that:
If there is unused space than any of list views can use it (so, no unused area with scroll bar at the same time)
If there is not enough space then bottom control takes at least 100 pixel and/or top controls takes at least Height – 100 pixels
Top control docks to top and bottom control docks to bottom
=================================================================
/// <summary>The two children effecient panel.</summary>
public class TwoChildrenEffecientPanel : Panel
{
#region Constants and Fields
/// <summary>The bottom child min size.</summary>
private const double BottomChildMinSize = 110;
#endregion
#region Methods
/// <summary>The arrange override.</summary>
/// <param name="arrangeSize">The arrange size.</param>
/// <returns>The <see cref="Size"/>.</returns>
protected override Size ArrangeOverride(Size arrangeSize)
{
Debug.Assert(this.InternalChildren.Count == 2, "This custom panel supports only two children.");
UIElement top = this.InternalChildren[0];
var topRect = new Rect(arrangeSize);
topRect.Height = top.DesiredSize.Height;
top.Arrange(topRect);
UIElement bottom = this.InternalChildren[1];
var bottomRect = new Rect(arrangeSize);
bottomRect.Height = bottom.DesiredSize.Height;
bottomRect.Y = arrangeSize.Height - bottomRect.Height;
bottom.Arrange(bottomRect);
return arrangeSize;
}
/// <summary>The measure override.</summary>
/// <param name="constraint">The constraint.</param>
/// <returns>The <see cref="Size"/>.</returns>
protected override Size MeasureOverride(Size constraint)
{
Debug.Assert(this.InternalChildren.Count == 2, "This custom panel supports only two children.");
// First measure desired size of all children.
var availableSize = new Size(constraint.Width, double.PositiveInfinity);
foreach (UIElement ui in this.InternalChildren)
{
ui.Measure(availableSize);
}
// Put constraints only if space is not enough
double totalHeight = this.InternalChildren.OfType<UIElement>().Sum(x => x.DesiredSize.Height);
if (totalHeight > constraint.Height)
{
UIElement top = this.InternalChildren[0];
UIElement bottom = this.InternalChildren[1];
if (bottom.DesiredSize.Height < BottomChildMinSize)
{
// If the second control needs less than it can get then put contraint only on the first one
top.Measure(new Size(constraint.Width, Math.Max(constraint.Height - bottom.DesiredSize.Height, 0)));
}
else if (top.DesiredSize.Height < constraint.Height - BottomChildMinSize)
{
// If the first control needs less than it can get then put contraint only on the second one
bottom.Measure(new Size(constraint.Width, Math.Max(constraint.Height - top.DesiredSize.Height, 0)));
}
else
{
top.Measure(new Size(constraint.Width, Math.Max(constraint.Height - BottomChildMinSize, 0)));
bottom.Measure(new Size(constraint.Width, BottomChildMinSize));
}
}
double maxWidth = this.InternalChildren.OfType<UIElement>().Max(x => x.DesiredSize.Width);
return new Size(Math.Min(constraint.Width, maxWidth), Math.Min(constraint.Height, totalHeight));
}
#endregion
}

How to get a WPF Viewbox coefficient of scaling applied

In case we use WPF (Silverlight) Viewbox with Stretch="UniformToFill" or Stretch="Uniform" when it preserves content's native aspect ratio, how could we get knowing the current coefficient of scaling which were applied to the content?
Note: we not always know the exact initial dimensions of the content (for example it's a Grid with lots of stuff in it).
See this question: Get the size (after it has been "streched") of an item in a ViewBox
Basically, if you have a Viewbox called viewbox, you can get the ScaleTransform like this
ContainerVisual child = VisualTreeHelper.GetChild(viewbox, 0) as ContainerVisual;
ScaleTransform scale = child.Transform as ScaleTransform;
You could also make an extension method for Viewbox which you can call like this
viewbox.GetScaleFactor();
ViewBoxExtensions
public static class ViewBoxExtensions
{
public static double GetScaleFactor(this Viewbox viewbox)
{
if (viewbox.Child == null ||
(viewbox.Child is FrameworkElement) == false)
{
return double.NaN;
}
FrameworkElement child = viewbox.Child as FrameworkElement;
return viewbox.ActualWidth / child.ActualWidth;
}
}

Is there a WPF "WrapGrid" control available or an easy way to create one?

Essentially I want a wrapPanel, but I would like items to snap to a grid rather than be pressed up to the left, so I can get a nice uniform looking grid, that automatically consumes available space.
WrapPanel handles the resize part.
WPF.Contrib.AutoGrid handles a nice automatic grid.
Anyone got a control that combines them?
My use case is I have a series of somewhat irregularly shaped controls. I would like them to appear in nice columns so the wrap panel should snap to the next "tabstop" when placing a control
When I read your question I assumed you wanted something like this:
public class UniformWrapPanel : WrapPanel
{
protected override Size MeasureOverride(Size constraint)
{
if(Orientation == Orientation.Horizontal)
ItemWidth = Children.Select(element =>
{
element.Measure(constraint);
return element.DesiredWidth;
}).Max();
else
... same for vertical ...
return base.MeasureOverride(constraint);
}
}
but I see someone else has already implemented a "UniformWrapPanel" and from your comments you indicate this is not what you were looking for.
The comment I don't understand is:
I want it to not force items to be a given size, but use their already existing size and therefore determine column widths automatically
Can you please provide an example to illustrate how you want things laid out with varying sizes? A picture might be nice. You also mention "tabstop" but don't give any definition of what that would be.
Here is some code that I whipped up based on some of the other controls that are close. It does a decent job of doing the layout, although it has an issue where grand-child controls do not fill up all their available space.
protected override Size ArrangeOverride(Size finalSize)
{
double rowY = 0;
int col = 0;
double currentRowHeight = 0;
foreach (UIElement child in Children)
{
var initialSize = child.DesiredSize;
int colspan = (int) Math.Ceiling(initialSize.Width/ ColumnSize);
Console.WriteLine(colspan);
double width = colspan * ColumnSize;
if (col > 0 && (col * ColumnSize) + width > constrainedSize.Width)
{
rowY += currentRowHeight;
col = 0;
currentRowHeight = 0;
}
var childRect = new Rect(col * ColumnSize, rowY, width, initialSize.Height);
child.Arrange(childRect);
currentRowHeight = Math.Max(currentRowHeight, initialSize.Height);
col+=colspan;
}
return finalSize;
}
Size constrainedSize;
protected override Size MeasureOverride(Size constraint)
{
constrainedSize = constraint;
return base.MeasureOverride(constraint);
}
Try setting ItemWidth (or ItemHeight) property of the WrapPanel:
<WrapPanel ItemWidth="48">
<TextBlock Text="H" Background="Red"/>
<TextBlock Text="e" Background="Orange"/>
<TextBlock Text="l" Background="Yellow"/>
<TextBlock Text="l" Background="Green"/>
<TextBlock Text="o" Background="Blue"/>
<TextBlock Text="!" Background="Violet"/>
</WrapPanel>

WPF UserControl is not drawn when overriding MeasureOverride and ArrangeOverride

I have a UserControl looking like this:
<UserControl
MaxHeight="32"
MaxWidth="32"
MinHeight="25"
MinWidth="25">
<DockPanel>
<!-- some stuff -->
</DockPanel>
</UserControl>
In addition to the min/max size constraint, I want the control always being painted with Width = Height. So i override MeasureOverride and ArrangeOverride:
protected override Size MeasureOverride(Size availableSize)
{
var resultSize = new Size(0, 0);
((UIElement)Content).Measure(availableSize);
var sideLength = Math.Min(((UIElement)Content).DesiredSize.Width, ((UIElement)Content).DesiredSize.Height);
resultSize.Width = sideLength;
resultSize.Height = sideLength;
return resultSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
((UIElement)Content).Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
return finalSize;
}
I understand that I must call Measure and Arrange on every child of the UserControl. Since the DocPanel is the only child of my UserControl and (in my understanding) is stored in the Content property of the UserControl, I simply call Measure and Arrange on this Content property. However the UserControl is not displayed. What am I doing wrong?
Depending on how you are hosting your UserControl, the value returned from the Measure phase may not be used. If you have it setup in a Grid with star rows/columns or a DockPanel, then the final size may be completely different.
You would need to apply similar logic to the arrange phase, so it will effectively ignore any extra space it's given.
The following code should work, and is a bit cleaner:
protected override Size MeasureOverride(Size availableSize) {
var desiredSize = base.MeasureOverride(availableSize);
var sideLength = Math.Min(desiredSize.Width, desiredSize.Height);
desiredSize.Width = sideLength;
desiredSize.Height = sideLength;
return desiredSize;
}
protected override Size ArrangeOverride(Size finalSize) {
var sideLength = Math.Min(this.DesiredSize.Width, this.DesiredSize.Height);
return base.ArrangeOverride(new Size(sideLength, sideLength));
}

WPF RichTextBox with no width set

I have the following XAML code:
<Window x:Class="RichText_Wrapping.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1">
<Grid>
<RichTextBox Height="100" Margin="2" Name="richTextBox1">
<FlowDocument>
<Paragraph>
This is a RichTextBox - if you don't specify a width, the text appears in a single column
</Paragraph>
</FlowDocument>
</RichTextBox>
</Grid>
... If you create this window in XAML, you can see that when you don't specify a width for the window, it wraps the text in a single column, one letter at a time. Is there something I'm missing? If it's a known deficiency in the control, is there any workaround?
This is a confirmed bug with the WPF RichTextBox. To fix it, Bind the PageWidth of the FlowDocument to the RichTextBox width, i.e.
<RichTextBox Name="rtb">
<FlowDocument Name="rtbFlowDoc" PageWidth="{Binding ElementName=rtb, Path=ActualWidth}" />
</RichTextBox>
EDIT:
Give the FlowDocument a name so that you can access it in the code behind and never new the flow document in codebehind.
Try binding the FlowDocument's width (one way) to the width of the container RichTextBox.
Worked for me...
The approach in this article worked for me:
WPF RichTextBox doesn't provide the functionality to adjust its width
to the text. As far as I know, RichTextBox use a FlowDocumentView in
its visual tree to render the Flowdocument. It will take the available
space to render its content, so it won't adjust its size to the
content. Since this is an internal class, it seems we cannot override
the layout process to let a RichTextBox to adjust its size to the
text.
Therefore, I think your approach is in the right direction.
Unfortunelately, based on my research, there is no straightforward way
to measure the size of the rendered text in a RichTextBox.
There is a workaround we can try. We can loop through the flowdocument
in RichTextBox recursively to retrieve all Run and Paragraph objects.
Then we convert them into FormattedText to get the size.
This article demonstrates how to convert a FlowDocument to
FormattedText. I also write a simple sample using the
FlowDocumentExtensions class in that article.
public Window2()
{
InitializeComponent();
StackPanel layoutRoot = new StackPanel();
RichTextBox myRichTextBox = new RichTextBox() { Width=20};
this.Content = layoutRoot;
layoutRoot.Children.Add(myRichTextBox);
myRichTextBox.Focus();
myRichTextBox.TextChanged += new TextChangedEventHandler((o,e)=>myRichTextBox.Width=myRichTextBox.Document.GetFormattedText().WidthIncludingTrailingWhitespace+20);
}
public static class FlowDocumentExtensions
{
private static IEnumerable<TextElement> GetRunsAndParagraphs(FlowDocument doc)
{
for (TextPointer position = doc.ContentStart;
position != null && position.CompareTo(doc.ContentEnd) <= 0;
position = position.GetNextContextPosition(LogicalDirection.Forward))
{
if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd)
{
Run run = position.Parent as Run;
if (run != null)
{
yield return run;
}
else
{
Paragraph para = position.Parent as Paragraph;
if (para != null)
{
yield return para;
}
}
}
}
}
public static FormattedText GetFormattedText(this FlowDocument doc)
{
if (doc == null)
{
throw new ArgumentNullException("doc");
}
FormattedText output = new FormattedText(
GetText(doc),
CultureInfo.CurrentCulture,
doc.FlowDirection,
new Typeface(doc.FontFamily, doc.FontStyle, doc.FontWeight, doc.FontStretch),
doc.FontSize,
doc.Foreground);
int offset = 0;
foreach (TextElement el in GetRunsAndParagraphs(doc))
{
Run run = el as Run;
if (run != null)
{
int count = run.Text.Length;
output.SetFontFamily(run.FontFamily, offset, count);
output.SetFontStyle(run.FontStyle, offset, count);
output.SetFontWeight(run.FontWeight, offset, count);
output.SetFontSize(run.FontSize, offset, count);
output.SetForegroundBrush(run.Foreground, offset, count);
output.SetFontStretch(run.FontStretch, offset, count);
output.SetTextDecorations(run.TextDecorations, offset, count);
offset += count;
}
else
{
offset += Environment.NewLine.Length;
}
}
return output;
}
private static string GetText(FlowDocument doc)
{
StringBuilder sb = new StringBuilder();
foreach (TextElement el in GetRunsAndParagraphs(doc))
{
Run run = el as Run;
sb.Append(run == null ? Environment.NewLine : run.Text);
}
return sb.ToString();
}
}
I copy pasted your code and its not in a single column, Do you have a width somewhere that is small? Maybe defined on the code behind for instance.
I noticed that I only had this issue when my default ScrollViewer style explicitly set HorizontalScrollBarVisibility=Hidden. Removing this setter (default value is Hidden anyway) fixed the single column issue for me in my RichTextBox.
Just for the record as I think this thread is missing some explanations as per the why: RichTextBox MeasureOverride implementation is like that. I won't call that a bug, maybe just a poor design behavior justified by the fact that just like mentioned above the FlowDocument is not cheap to measure due to its complexity. Bottom line, avoid unlimited Width constraint by binding MinWidth or wrap it in a limiting container.
/// <summary>
/// Measurement override. Implement your size-to-content logic here.
/// </summary>
/// <param name="constraint">
/// Sizing constraint.
/// </param>
protected override Size MeasureOverride(Size constraint)
{
if (constraint.Width == Double.PositiveInfinity)
{
// If we're sized to infinity, we won't behave the same way TextBox does under
// the same conditions. So, we fake it.
constraint.Width = this.MinWidth;
}
return base.MeasureOverride(constraint);
}

Resources