Today, I present a useful stand-alone WPF control for viewing Revit fill patterns, originally implemented by Victor Checkalin, Виктор Чекалин, shared with us by Alexander Ignatovich, Александр Игнатович, of Investicionnaya Venchurnaya Companiya, as part of further enhancements for the AddMaterial add-in. The rest of his new enhancements will be discussed as soon as possible in a future post.
The AddMaterials add-in reads a list of material properties from an Excel spreadsheet and generates Revit material elements accordingly.
The input data includes columns defining the material surface and cut patterns.
These patterns are displayed graphically like this in the third and fourth columns of Alex' WPF control:
Making use of the WPF fill pattern control in your own add-in is achieved in two simple steps:
- Add a copy of the XAML module FillPatternViewerControlWpf.xaml to your Visual Studio add-in project.
- Add references to the FillPatternViewerControlWpf control in your own XAML form source.
The fill pattern viewer XAML module appears like this in the solution explorer:
Here is the XAML code to define the surface and cut pattern columns in the form shown above:
<GridViewColumn Header="Surface" Width="80"> <GridViewColumn.CellTemplate> <DataTemplate DataType ="{x:Type ViewModel:MaterialViewModel}"> <Border CornerRadius="2" BorderThickness="1" BorderBrush="Gray"> <Controls:FillPatternViewerControlWpf HorizontalAlignment="Stretch" Height="30" Margin="1,3" FillPattern="{Binding SurfacePattern}"/> </Border> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> <GridViewColumn Header="Cut" Width="80"> <GridViewColumn.CellTemplate> <DataTemplate DataType ="{x:Type ViewModel:MaterialViewModel}"> <Border CornerRadius="2" BorderThickness="1" BorderBrush="Gray"> <Controls:FillPatternViewerControlWpf HorizontalAlignment="Stretch" Height="30" Margin="1,3" FillPattern="{Binding CutPattern}"/> </Border> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn>
The fill pattern viewer control is defined by its XAML description plus the C# code defining its associated behaviour.
The XAML description looks like this:
<UserControl x:Class="AddMaterials.View.Controls.FillPatternViewerControlWpf" 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" xmlns:Converters="clr-namespace:AddMaterials.View.Converters" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Grid Name="_grid" ToolTip="{Binding Path=FillPattern.Name, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"> <Grid.Resources> <Converters:BitmapToImageSourceConverter x:Key="BitmapToImageSourceConverter"/> </Grid.Resources> <Image Source="{Binding Path=FillPatternImage, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Converter={StaticResource BitmapToImageSourceConverter}}" Stretch="None"/> </Grid> </UserControl>
The C# code defining its behaviour is where the real action takes place, in the DrawFillPattern method, which retrieves and displays the graphical data defined by the Revit FillGrid instances managed by the FillPattern object:
using System; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Windows; using Autodesk.Revit.DB; using Image = System.Drawing.Image; using Matrix = System.Drawing.Drawing2D.Matrix; using Rectangle = System.Drawing.Rectangle; namespace AddMaterials.View.Controls { /// <summary> /// Interaction logic for FillPatternViewerControlWpf.xaml /// </summary> public partial class FillPatternViewerControlWpf : INotifyPropertyChanged { private const float Scale = 50; private Bitmap fillPatternImg; public static readonly DependencyProperty FillPatternProperty = DependencyProperty .RegisterAttached( "FillPattern", typeof( FillPattern ), typeof( FillPatternViewerControlWpf ), new UIPropertyMetadata( null, OnFillPatternChanged ) ); private static void OnFillPatternChanged( DependencyObject d, DependencyPropertyChangedEventArgs e ) { var fillPatternViewerControl = d as FillPatternViewerControlWpf; if( fillPatternViewerControl == null ) return; fillPatternViewerControl.OnPropertyChanged( "FillPattern" ); fillPatternViewerControl.CreateFillPatternImage(); } public FillPattern FillPattern { get { return (FillPattern) GetValue( FillPatternProperty ); } set { SetValue( FillPatternProperty, value ); } } public FillPattern GetFillPattern( DependencyObject obj ) { return (FillPattern) obj.GetValue( FillPatternProperty ); } public void SetFillPattern( DependencyObject obj, FillPattern value ) { obj.SetValue( FillPatternProperty, value ); } public FillPatternViewerControlWpf() { InitializeComponent(); } public event PropertyChangedEventHandler PropertyChanged; //[NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged( string propertyName ) { var handler = PropertyChanged; if( handler != null ) handler( this, new PropertyChangedEventArgs( propertyName ) ); } public Image FillPatternImage { get { if( fillPatternImg == null ) CreateFillPatternImage(); return fillPatternImg; } } private void CreateFillPatternImage() { if( FillPattern == null ) return; var width = ( ActualWidth == 0 ? Width : ActualWidth ) == 0 ? 100 : ( ActualWidth == 0 ? Width : ActualWidth ); if( double.IsNaN( width ) ) width = 100; var height = ( ActualHeight == 0 ? Height : ActualHeight ) == 0 ? 30 : ( ActualHeight == 0 ? Height : ActualHeight ); if( double.IsNaN( height ) ) height = 30; fillPatternImg = new Bitmap( (int) width, (int) height ); using( var g = Graphics.FromImage( fillPatternImg ) ) { DrawFillPattern( g ); } OnPropertyChanged( "FillPatternImage" ); } private void DrawFillPattern( Graphics g ) { float matrixScale; var fillPattern = FillPattern; if( fillPattern == null ) return; if( fillPattern.Target == FillPatternTarget.Model ) matrixScale = Scale; else matrixScale = Scale * 10; try { var width = ( ActualWidth == 0 ? Width : ActualWidth ) == 0 ? 100 : ( ActualWidth == 0 ? Width : ActualWidth ); if( double.IsNaN( width ) ) width = 100; var height = ( ActualHeight == 0 ? Height : ActualHeight ) == 0 ? 30 : ( ActualHeight == 0 ? Height : ActualHeight ); if( double.IsNaN( height ) ) height = 30; var rect = new Rectangle( 0, 0, (int) width, (int) height ); var centerX = ( rect.Left + rect.Left + rect.Width ) / 2; var centerY = ( rect.Top + rect.Top + rect.Height ) / 2; g.TranslateTransform( centerX, centerY ); var rectF = new Rectangle( -1, -1, 2, 2 ); g.FillRectangle( Brushes.Blue, rectF ); g.ResetTransform(); var fillGrids = fillPattern.GetFillGrids(); Debug.Print( "FilPattern name: {0}", fillPattern.Name ); Debug.Print( "Grids count: {0}", fillGrids.Count ); foreach( var fillGrid in fillGrids ) { var degreeAngle = (float) RadianToGradus( fillGrid.Angle ); Debug.Print( new string( '-', 100 ) ); Debug.Print( "\tOrigin: U: {0} V:{1}", fillGrid.Origin.U, fillGrid.Origin.V ); Debug.Print( "\tOffset: {0}", fillGrid.Offset ); Debug.Print( "\tAngle: {0}", degreeAngle ); Debug.Print( "\tShift: {0}", fillGrid.Shift ); var pen = new Pen( System.Drawing.Color.Black ) { Width = 1f / matrixScale }; float dashLength = 1; var segments = fillGrid.GetSegments(); if( segments.Count > 0 ) { pen.DashPattern = segments .Select( Convert.ToSingle ) .ToArray(); Debug.Print( "\tSegments:" ); foreach( var segment in segments ) { Debug.Print( "\t\t{0}", segment ); } dashLength = pen.DashPattern.Sum(); } g.ResetTransform(); var rotateMatrix = new Matrix(); rotateMatrix.Rotate( degreeAngle ); var matrix = new Matrix( 1, 0, 0, -1, centerX, centerY ); matrix.Scale( matrixScale, matrixScale ); matrix.Translate( (float) fillGrid.Origin.U, (float) fillGrid.Origin.V ); var backMatrix = matrix.Clone(); backMatrix.Multiply( rotateMatrix ); matrix.Multiply( rotateMatrix ); bool first = true; for( int i = 20; i > 0; i-- ) { if( !first ) { matrix.Translate( (float) fillGrid.Shift, (float) fillGrid.Offset ); backMatrix.Translate( (float) fillGrid.Shift, -(float) fillGrid.Offset ); } else { first = false; } var offset = ( -10 ) * dashLength; matrix.Translate( offset, 0 ); backMatrix.Translate( offset, 0 ); g.Transform = matrix; g.DrawLine( pen, new PointF( 0, 0 ), new PointF( 200, 0 ) ); g.Transform = backMatrix; g.DrawLine( pen, new PointF( 0, 0 ), new PointF( 200, 0 ) ); } } } catch( Exception ex ) { Debug.Print( ex.Message ); } } private double RadianToGradus( double radian ) { return radian * 180 / Math.PI; } } }
The source code, Visual Studio solution and add-in manifest of the complete AddMaterials add-in including the fill pattern viewer WPF control is provided in the AddMaterials GitHub repository.
The version discussed above is release 2014.0.0.2.
Many thanks to Victor for the implementation and to Alex for sharing this valuable tool!
Dear Jeremy,
I am developing a hatch generator as a programming exercise. Using a pat-file limits the total segment length of a fill pattern to 100 feet. Using the API you can define patterns of greater length, and thus greater accuracy. However the preview window of the fill pattern dialog doesn't show these patterns, so perhaps segment length greater than 100 feet is not the intended use.
I have 2 questions:
- Could I safely use these patterns?
- Can the WPF Fill Pattern Viewer Control display these patterns?
Have a nice day,
Frank
Posted by: Frank Aarssen | April 08, 2014 at 07:28
Dear Frank,
That sounds brilliant.
Congratulations on creating such a powerful and fun tool as a programming exercise.
I cannot answer either of your question, and quite possibly nobody else in the entire universe can either.
It is up to you, I think.
I am very interested in hearing what you find out, and would love to publish your tool if you are willing to share it.
Thank you!
Cheers, Jeremy.
Posted by: Jeremy Tammik | April 08, 2014 at 11:15
Wow. Quite unexpectedly for me to see my name in the post:)
I remember I spent a lot of time to understand what does fill grids and fill segments mean and how to create the graphics of fill patterns using that numbers:)
But as I remember there is a performance issue with the complex fill pattern, like 'Sand', what have a lot of fill grids. The control is significantly slows when creating graphics for such pattern.
Anyway, the control works and the performance was enough for my task.
I didn't remember why I didn't share with the control when I created it:)
Alex, thanks for sharing and what you dind't forget to mention the author, i.e. me:)
Posted by: Victor Chekalin | April 14, 2014 at 02:22
Dear Victor,
Nice to hear from you!
Oops. I'm sorry and happy we surprised you. I hope it was pleasant in the end :-)
I wonder whether I should add the performance warning to the main post to ensure it gets seen, or just leave it here in the comments for people to discover for themselves.
I think the latter, actually.
Thank you again for your nice implementation and these further details.
Cheers, Jeremy.
Posted by: Jeremy Tammik | April 14, 2014 at 02:34
Hi Jeremy,
I think we may create simple benchmark and see how much time needs to create the simple and complex pattern. :)
I even would say that the performance is slow when you show the list of the complex patterns. When only single complex patten is displayed, decrease in performanse is barely noticeable.
Regards,
Victor.
Posted by: Victor Chekalin | April 14, 2014 at 02:41