Showing posts with label WPF. Show all posts
Showing posts with label WPF. Show all posts

Saturday, March 2, 2013

WPF UniformGrid variant

I've been using the WPF UniformGrid to layout the UI representation of collections in some of the applications I work on.  The apps generally follow the MVVM pattern and to get the layout I require I usually need to apply a converter to a bound viewmodel property to set the number of rows or columns.

This usually works ok but in a application where the aspect ratio of the area occupied by the UniformGrid can be changed by the user the UniformGrid doesn't doesn't alter it's row or column count to optimise the layout.

I have searched for a WPF layout that is similar to the UniformGrid but that is more flexible in it's automatic calculation of rows and columns without success so have written my own.

The following AutoUniformGrid checks its constrained size and if this is fixed (i.e. neither the width or height is infinite) it will dynamically set the number of rows and columns to maximise the height & width of its cell.

So, for example, if the AutoUniformGrid has 8 children and is sized to an area that is twice as wide as it is high, it will set its column count to 4 and its row count to 2.

In this situation, the UniformGrid would default to a 3 x 3 grid which would result in badly proportioned cells.

The AutoUniformGrid is similar to a WrapPanel except that the children all occupy the same sized area.  In my usage of this control, the children are all wrapped in a ViewBox to ensure they make full use of their allocated cell.


public class AutoUniformGrid : Panel
    {
        // Fields
        private int _columns;
        private int _rows;
        public static readonly DependencyProperty ColumnsProperty = 
            DependencyProperty.Register("Columns"typeof(int), typeof(AutoUniformGrid), 
                        new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsMeasure), 
                        new ValidateValueCallback(AutoUniformGrid.ValidateColumns));
        public static readonly DependencyProperty RowsProperty = DependencyProperty.Register("Rows"typeof(int), typeof(AutoUniformGrid), 
                        new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsMeasure), 
                        new ValidateValueCallback(AutoUniformGrid.ValidateRows));
 
        
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            double width = OptimalCalculator.CalculateMinDimensionValue(arrangeSize, this._rows, this._columns);
            Size requiredSize = new Size(width * this._columns, width * this._rows);
 
            double xOffset = (arrangeSize.Width - requiredSize.Width) / 2;
            double yOffset = (arrangeSize.Height - requiredSize.Height) / 2;
 
            Rect finalRect = new Rect(xOffset, yOffset, width, width);
            double rightBoundary = xOffset + requiredSize.Width - 1.0;
            foreach (UIElement element in base.InternalChildren)
            {
                element.Arrange(finalRect);
                if (element.Visibility != Visibility.Collapsed)
                {
                    finalRect.X += width;
                    if (finalRect.X >= rightBoundary)
                    {
                        finalRect.Y += finalRect.Height;
                        finalRect.X = xOffset;
                    }
                }
            }
            return arrangeSize;
        }
 
        protected override Size MeasureOverride(Size constraint)
        {
            this.UpdateComputedValues(constraint);
 
            Size availableSize = new Size(constraint.Width / ((double)this._columns), constraint.Height / ((double)this._rows));
            double width = 0.0;
            double height = 0.0;
            int childIndex = 0;
            int count = base.InternalChildren.Count;
            while (childIndex < count)
            {
                UIElement element = base.InternalChildren[childIndex];
                element.Measure(availableSize);
                Size desiredSize = element.DesiredSize;
                if (width < desiredSize.Width)
                {
                    width = desiredSize.Width;
                }
                if (height < desiredSize.Height)
                {
                    height = desiredSize.Height;
                }
                childIndex++;
            }
            return new Size(width * this._columns, height * this._rows);
        }
 
 
        int CountVisibleChildren()
        {
            int visibleCount = 0;
            int childIndex = 0;
            int count = base.InternalChildren.Count;
            while (childIndex < count)
            {
                UIElement element = base.InternalChildren[childIndex];
                if (element.Visibility != Visibility.Collapsed)
                {
                    visibleCount++;
                }
                childIndex++;
            }
            return visibleCount;
        }
 
 
        private void UpdateComputedValues(Size constraint)
        {
            this._columns = this.Columns;
            this._rows = this.Rows;
            if ((this._rows == 0) || (this._columns == 0))
            {
                int visibleCount = CountVisibleChildren();
                if (visibleCount == 0)
                {
                    visibleCount = 1;
                }
                if (this._rows == 0)
                {
                    if (this._columns > 0)
                    {
                        this._rows = (visibleCount + (this._columns - 1)) / this._columns;
                    }
                    else
                    {
                        OptimalCalculator optimal = new OptimalCalculator( constraint, visibleCount);
                        this._rows = optimal.OptimalRows;
                        this._columns = optimal.OptimalColumns;
                    }
                } else if (this._columns == 0)
                {
                    this._columns = (visibleCount + (this._rows - 1)) / this._rows;
                }
            }
        }
 
        private static bool ValidateColumns(object o)
        {
            return (((int)o) >= 0);
        }
 
        private static bool ValidateRows(object o)
        {
            return (((int)o) >= 0);
        }
 
        public int Columns
        {
            get
            {
                return (int)base.GetValue(ColumnsProperty);
            }
            set
            {
                base.SetValue(ColumnsProperty, value);
            }
        }
 
        public int Rows
        {
            get
            {
                return (int)base.GetValue(RowsProperty);
            }
            set
            {
                base.SetValue(RowsProperty, value);
            }
        }
 
        class OptimalCalculator
        {
 
            public int OptimalRows { getprivate set; }
            public int OptimalColumns { getprivate set; }
 
            double DimensionAtOptimal { getset; }
            Size AvailableSize { getset; }
            int VisibleChildCount { getset; }
 
            public OptimalCalculator( Size availableSize, int visibleChildCount )
            {
                AvailableSize = availableSize;
                VisibleChildCount = visibleChildCount;
                DimensionAtOptimal = 0.0;
                Calculate( );
            }
 
            private void Calculate()
            {
                OptimalRows = 0;
                OptimalColumns = 0;
 
                double area = AvailableSize.Height * AvailableSize.Width;
 
                if (!double.IsInfinity(area) && !double.IsNaN(area) && area != 0.0 && VisibleChildCount > 1)
                {
                    double aspect = AvailableSize.Width / AvailableSize.Height;
                    int columns = 1, rows = 1;
 
                    if (aspect > 1)
                    {
                        for (; columns <= VisibleChildCount; columns++)
                        {
                            rows = (intMath.Ceiling( VisibleChildCount/(double)columns );                            
                            MaybeUpdateOptimal(rows, columns);
                        }
                    }
                    else
                    {
                        for (; rows <= VisibleChildCount; rows++)
                        {
                            columns = (intMath.Ceiling( VisibleChildCount / (double) rows);                            
                            MaybeUpdateOptimal(rows, columns);
                        }
                    }
                }
 
                if (OptimalRows <= 0)
                {
                    OptimalRows = (int)Math.Round(Math.Sqrt((double)VisibleChildCount));
                    if (OptimalRows * OptimalRows < VisibleChildCount)
                        OptimalRows++;
                    OptimalColumns = OptimalRows;
                }                
            }
 
            private void MaybeUpdateOptimal(int rows, int columns)
            {
                double dimensionValue = CalculateMinDimensionValue(AvailableSize, rows, columns);
                if (dimensionValue > DimensionAtOptimal)
                {
                    DimensionAtOptimal = dimensionValue;
                    OptimalRows = rows;
                    OptimalColumns = columns;
                }
            }   
        
            static internal double CalculateMinDimensionValue(Size availableSize,  int rows, int columns)
            {
                if (rows <= 0 || columns <= 0)
                    return 0.0;
                double width = availableSize.Width / columns;
                double height = availableSize.Height / rows;
                return Math.Min(width, height);
            }
        }
 
    }

Monday, January 17, 2011

WPF StringFormat Problems

If you're struggling to make StringFormat on a WPF binding work consider whether or not the data type you are binding to is matches the StringFormat you are using.

I've spent too long today trying to make StringFormat work on a DataGrid column bound to a DataTable column which I thought was a double. It didn't seem to matter what I used as the StringFormat, the data would always display the same. I'd forgotten to specify the DataColumn DataType and so when I assigned values to the column, they were stored a string and didn't respond to StringFormats that expected double values.

Sunday, October 31, 2010

Animated sine wave addition

My son is studying physics at school and one of topics was "waves". The physics study book he is using is printed and so doesn't include any wave animations. I thought animations would help with understanding but couldn't find any on-line animation where the user could change the wave frequency, phase.

I've wrote a WPF application where the user could control the frequency, phase of two sine wave and see the result of adding these two wave together. I then ported this to silverlight (which required a few small changes to the code) and published the result on www.nzdev.com

Since then, I've added the ability to also change the wave amplitude.

Sunday, October 24, 2010

WPF DocumentViewer

The WPF DocumentViewer control has some annoying features.

1. A find control ("Type text to find") that doesn't work.
2. A print function with limited configuration.

I work around these features by subclassing the DocumentViewer as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows;
using System.Printing;
using System.Windows.Xps;

namespace FixedDocPrinting
{
public class PrintDocumentViewer : DocumentViewer
{
PageOrientation _pageOrientation = PageOrientation.Portrait;
public PageOrientation PageOrientation
{
get { return _pageOrientation; }
set { _pageOrientation = value; }
}

Visibility _findControlVisibility = Visibility.Collapsed;
public Visibility FindControlVisibility
{
get
{
return _findControlVisibility;
}
set
{
_findControlVisibility = value;
UpdateFindControlVisibility();
}
}

private void UpdateFindControlVisibility()
{
object toolbar = this.Template.FindName("PART_FindToolBarHost", this);
ContentControl cc = toolbar as ContentControl;
if (cc != null)
{
HeaderedItemsControl itemsControl = cc.Content as HeaderedItemsControl;
if (itemsControl != null)
itemsControl.Visibility = FindControlVisibility;
}
}

public PrintDocumentViewer()
{
Loaded += new RoutedEventHandler(PrintDocumentViewer_Loaded);
}

void PrintDocumentViewer_Loaded(object sender, RoutedEventArgs e)
{
UpdateFindControlVisibility();
}

protected override void OnPrintCommand()
{
PrintDialog printDialog = new PrintDialog();
printDialog.PrintQueue = LocalPrintServer.GetDefaultPrintQueue();
printDialog.PrintTicket = printDialog.PrintQueue.DefaultPrintTicket;

printDialog.PrintTicket.PageOrientation = PageOrientation;

if (printDialog.ShowDialog() == true)
{
// Code assumes this.Document will either by a FixedDocument or a FixedDocumentSequence
FixedDocument fixedDocument = this.Document as FixedDocument;
FixedDocumentSequence fixedDocumentSequence = this.Document as FixedDocumentSequence;

if (fixedDocument != null)
fixedDocument.PrintTicket = printDialog.PrintTicket;

if (fixedDocumentSequence!= null)
fixedDocumentSequence.PrintTicket = printDialog.PrintTicket;

XpsDocumentWriter writer = PrintQueue.CreateXpsDocumentWriter(printDialog.PrintQueue);

if (fixedDocument != null)
writer.WriteAsync(fixedDocument, printDialog.PrintTicket);

if (fixedDocumentSequence != null)
writer.WriteAsync(fixedDocumentSequence, printDialog.PrintTicket);
}
}
}
}

This hides the find control and gives me the ability to print in Landscape.

To use this code, you will need to add a reference to the ReachFramework dll which contains useful classes in the System.Printing and System.Windows.Xps name spaces.


Friday, October 30, 2009

WPF binding to a fast changing data source

I was using wpf data binding to display data in a real time monitoring application and found the rate at which the data source was changing resulted in a difficult to read control.

Josh Smith has a good article on ways to reduce the frequency of binding updates and the option of calling UpdateTarget on the controls binding expression was the most appropriate solution for my application. The WPF user controls that I was applying this to had a large number of controls and so I needed a way of finding the binding expressions for all the controls.

Philipp Sumi has a function (FindChildren) to find all child control of a specified type which I used to locate and subsequently update all relevant binding expressions for the control type I was using (TextBlock)

public partial class BindingSample : UserControl

    {
        List<BindingExpression> _bindingExpressions = new List<BindingExpression>();

        public BindingSample()
        {
            InitializeComponent();

            foreach (var tb in this.FindChildren<TextBlock>() )
            {
                BindingExpression be = tb.GetBindingExpression(TextBlock.TextProperty);
                if (be != null)
                    _bindingExpressions.Add(be);
            }
        }

        // Force binding update 
        // NB: Not using OneWay binding because updates are too frequent
        //     An alternative would be to use the converter UpdateThresholdConverter
        //     but this would not necessarily result in the last value being displayed when updates stop.
        public void UpdateTarget()
        {
            _bindingExpressions.ForEach(e => e.UpdateTarget());
        }
    }

XAML

            <StackPanel Style="{StaticResource DisplayLabelValue}">
                <Label Style="{StaticResource DisplayTitle}">Speed</Label>
                <TextBlock Style="{StaticResource DisplayValue}" Text="{Binding Path=Speed, Mode=OneTime, StringFormat=F1}"/>
            </StackPanel>

Source code for FindChildren (by Philipp Sumi)

Thursday, June 11, 2009

Formatted TimeSpan WPF Binding

The .Net TimeSpan object is displayed in a fixed format when bound to a WPF control. If you need to display a TimeSpan with a custom format you need to do this via a converter class that derives from IValueConverter.

class TimeSpanConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
TimeSpan timeSpan = (TimeSpan) value;
String result = String.Empty;
if ( timeSpan != null )
{
result = String.Format(
"{0:D2}:{1:D2}:{2:D2}:{3:D2}.{4:D1}",
timeSpan.Days, timeSpan.Hours,
timeSpan.Minutes, timeSpan.Seconds,
(int) Math.Round( timeSpan.Milliseconds/100.0));
}
return result;
}

public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}

You can create an instance of the Converter class in the Windows resources as follows:
Window.Resources
srm:TimeSpanConverter x:Key="TimeSpanConverter" /srm:TimeSpanConverter
Window.Resources

and use this converter in the binding with the following XAML

GridViewColumn Header="Duration" DisplayMemberBinding=
"{Binding Path=Duration, Converter={StaticResource TimeSpanConverter}}"


Update

I now use the following which has support for an option ConvertParameter:

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
String result = String.Empty;
if ( value is TimeSpan )
{
TimeSpan timeSpan = (TimeSpan) value;
if (timeSpan != TimeSpan.Zero)
{
if (parameter == null || !(parameter is string))
result = String.Format("{0:D2}:{1:D2}:{2:D2}:{3:D2}.{4:D1}",
timeSpan.Days, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, (int)Math.Round(timeSpan.Milliseconds / 100.0));
else
{
DateTime time = DateTime.Today;
time = time.Add(timeSpan);
result = time.ToString((string)parameter);
}
}
}
return result;
}
XAML:
Binding="{Binding Path=Duration, ConverterParameter=H:mm:ss.f, Converter={StaticResource TimeSpanConverter}}"