Saturday, October 28, 2017

C# Filter duplicates from a collection quickly!

Today I did some testing to determine which of two methods was faster at finding duplicates in a collection: a LINQ query or a Dictionary.


In C#, there are always more than one way to do things. For example: I have a list of items which may or may not contain some duplicate items. In order to select just the duplicates, i wanted to put those in a separate list. I found two pieces of code able to do that.

For the hasty ones: a Dictionary is faster when you have very few duplicates. When you have a lot of duplicates, the dictionary becomes MUCH slower. The query stays fast.

The first is a LINQ : A Query written directly in C# source code.
In this program "source" is a list<String>. Lets assume that it contains: "aaa", "bbb", "ccc", "aaa". The query groups identical strings together, and then counts how many items are in each group. In this case, executing the query will produce three groups: One group with two strings "aaa", and two groups which each have one string, "bbb" and "ccc" respectively. Only the group with more than one string will be selected.
 ( This function was written just to test. To measure a  meaningful amount of time, the function is repeated a number of times. )

public void LINQ()
 {
 for (int i = 0; i < cycles; i++)
  {
  duplicates.Clear();

  var duplicatefilenames = from string item in source
     group item by item into duplicates
     where duplicates.Count() > 1
     select duplicates;

  foreach (var items in duplicatefilenames)
   {
   duplicates.Add(items.First());      
   }
  }
 }


The second method is by using a Dictionary. The idea here is that a Dictionary can only contain unique keys. If you try to enter a duplicate value an exception will be thrown:


public void Dictionary()
 {
 Dictionary<string, int> UniqueList = new Dictionary<string, int>();

 for (int i = 0; i < cycles; i++)
  {
  duplicates.Clear();
  UniqueList.Clear();

  foreach (string item in source)
   {
   try
    {
    UniqueList.Add(item, i);
    }
   catch (ArgumentException) //item already exists in uniquelist
    {
    duplicates.Add(item);
    }
   }
  }
 }


Now, my assumption was that maybe using a dictionary would be faster than using a LINQ query. So i set up a test with a Stopwatch. (Not a manual one, the .Net class. Click for details --->>

Conclusion:

As it turns out, I was wrong. When you have a source containing no duplicates, the dictionary is actually faster than the query. When you have some duplicates, the dictionary is MUCH slower.

For these tests, the list "source" is filled with 1000 unique strings. Between 0  and 100 % of those are inserted into the source a second time.
Below is the output of the program:

Starting speed test.
Repetitions: 10.000, percentage of duplicates: 0

Starting Linq test
Linq test finished in 2438 ms.

Starting Dictionary test. If there are a lot of collisions this might take a while...
Dictionary test finished in 716 ms.

So far, so good. Until we add some duplicates. The first time i ran the program, i shut it down because i thought it wasn't working right. As it turns out, catching exceptions just takes a really, really long time. Note that the output below has just one hundred cycles:

Starting speed test.
Repetitions: 100, percentage of duplicates: 1

Starting Linq test
Linq test finished in 35 ms.

Starting Dictionary test. If there are a lot of collisions this might take a while...
Dictionary test finished in 5210 ms.

WOW. The dictionary takes almost 149 times as long! For more information on how to use LINQ, a good source is these examples. I hope this helps you out, and have a great day coding!

Sunday, March 26, 2017

Make a 3D cylinder the easy way

To make a 3D cylinder in a WPF application, you could use a 3D model. However in this post I describe a quick alternative: With a progress bar, a adorner and an opacity mask you can get the same effect using only two dimensions.

When deciding to make a vertical cylinder I decided to try to do so by subclassing a progress bar. This offers easy access to the maximum, minimum and value properties, enabling the making of cylinders with arbitrary length.
I updated the appearance of the progress bar in three steps:

1- Paint over the solid color of the bar with a gradient brush to simulate a lightsource
2- Hide some of the bottom of the progress bar to make it look round. This is done using the property OpacityMask
3- Paint an ellipse on the top of the cylinder.

The result looks like this:


Applying the effects to the cylinders is done with an adorner. After the control is loaded, the function AddDecorator gets the adornerlayer for the control, and adds an instance of cylinderadorner to it:

public class Cylinder : ProgressBar
{
 public Cylinder()
  {
  this.Loaded += AddDecorator;
  Background = null;
  if (Foreground as SolidColorBrush is null)
   {
   throw new ArgumentException("Exception: Foreground of cylinder  must be a solid color");
   }
  }

 private void AddDecorator(object sender, RoutedEventArgs e)
  {
  AdornerLayer a = AdornerLayer.GetAdornerLayer(this);
  a.Add(new CylinderAdorner(this));
  }
}
 

Cylinderadorner is a class which inherits from Adorner. When the control is painted, the adorner is painted on top of it by OnRender.
Another example of this can be found Here.


public class CylinderAdorner : Adorner
 {
 //store two variables needed more than once
 private Cylinder cylinder;
 private PathGeometry pathgeometry;
 const int sizeFactor = 4; //ratio of width vs height used for ellipses

 public CylinderAdorner(UIElement element) : base(element)
  {
  cylinder = this.AdornedElement as Cylinder;
  }

 protected override void OnRender(DrawingContext drawingContext)
  {
  //get outline of cylinder, used by all three next functions
  pathgeometry = getCylinderOutline();

  //overlay gradientbrush to simulate 3D shape
  overlayGradient(drawingContext);

  //Now hide the bottom / right of cylinder with an opacitymask    
  cylinder.OpacityMask = getOpacityMask();

  //place ellipse on top of cylinder
  placeEllipse(drawingContext);
  }
       ...

OverlayGradient draws a white and black shape over the progress bar. The shape of this is a cylinder with a rounded bottom, as tall as the "value" part of the progress bar.

GetOpacityMask uses the same shape, filled with a black brush, to only show this part of the bar.

Finally placeEllipse draws the top of the cylinder. It also uses pathgeometry to get the dimensions of the ellipse: pathgeometry contains a RECT poperty called Bounds which contains the outer dimensions.

The work is done by the other members of CylinderAdorner:
public class CylinderAdorner : Adorner
{
//store for two variables needed more than once
private Cylinder cylinder;
private PathGeometry pathgeometry;
const int sizeFactor = 4; //ratio of width vs height used for ellipses

public CylinderAdorner(UIElement element) : base(element)
 {
 cylinder = this.AdornedElement as Cylinder;
 }

protected override void OnRender(DrawingContext drawingContext)
 {
 //get outline of cylinder, used by both PaintGradient and getOpacityMask
 pathgeometry = getCylinderOutline();

 //overlay gradientbrush to simulate 3D shape
 overlayGradient(drawingContext);

 //Now hide the bottom / right of cylinder with an opacitymask    
 cylinder.OpacityMask = getOpacityMask();

 //place ellipse on top of cylinder
 placeEllipse(drawingContext);
 }

 private Rect getCylinderOutlineRect(Orientation orientation)
  {
  Rect rect = new Rect();
  if (cylinder.Orientation == Orientation.Vertical)
   {
   //creatre rectangle on the bottom part of progress bar, from the bottom of the cylinder up to value
   rect.Width = cylinder.ActualWidth;
   rect.Y = cylinder.ActualHeight - ((cylinder.Value * cylinder.ActualHeight) / (cylinder.Maximum - cylinder.Minimum));
   rect.Height = cylinder.ActualHeight - rect.Y;
   }
  else
   {
   //create rectangle on the left of the progress bar, with width equivalent to value
   rect.Height = cylinder.ActualHeight;
   rect.Width = (cylinder.Value * cylinder.ActualWidth) / (cylinder.Maximum - cylinder.Minimum);
   }
  return rect;
  }

 /// <summary>
 /// Create a PathFigure with the outline of a cylinder with rounded bottom
 /// Start drawing from top right corner clockwise
 /// </summary>
 /// <returns>Outline of a cylinder</returns>
 private PathGeometry getCylinderOutline()
  {
  List<PathSegment> pathSegments = new List<PathSegment>();

  //get rect around the straight part of the cylinder
  Rect rect = getCylinderOutlineRect(cylinder.Orientation);

  if (cylinder.Orientation == Orientation.Vertical)
   {
   //draw line to bottom right corner, arc to bottom left, line to top left:
   pathSegments.Add(new LineSegment(new Point(rect.Width, rect.Bottom - (rect.Width / sizeFactor)), false));
   pathSegments.Add(new ArcSegment(new Point(0, rect.Bottom - (rect.Width / sizeFactor)), new Size(rect.Width / 2, rect.Width / sizeFactor), 0, false, SweepDirection.Clockwise, false));
   pathSegments.Add(new LineSegment(new Point(0, rect.Top), false));
   }
  else
   {
   //draw line to bottom right, line to bottom left, arc to top left:
   pathSegments.Add(new LineSegment(new Point(rect.Width, rect.Height), false));
   pathSegments.Add(new LineSegment(new Point(rect.Height / sizeFactor, rect.Height), false)); // stop some distance short of left side
   pathSegments.Add(new ArcSegment(new Point(rect.Height / sizeFactor, 0), new Size(rect.Height / sizeFactor, rect.Height / 2), 0, false, SweepDirection.Clockwise, false));
   }

  //drawing starts at top right:
  PathFigure pf = new PathFigure(new Point(rect.Width, rect.Top), pathSegments, true);
  return new PathGeometry(new List<PathFigure> { pf });
  }

 /// <summary>
 /// Draw a lineair gradient brush over the cylinder to create 3D effect
 /// Start and end points are relative, from 0 to 1. 0.5 is half the dimension of the control
 /// </summary>
 /// <param name="drawingContext"></param>
 private void overlayGradient(DrawingContext drawingContext)
  {
  LinearGradientBrush lbg = new LinearGradientBrush();
  if (cylinder.Orientation == Orientation.Vertical)
   {
   lbg.StartPoint = new Point(0, 0.5);
   lbg.EndPoint = new Point(1, 0.5);
   }
  else
   {
   lbg.StartPoint = new Point(0.5, 0);
   lbg.EndPoint = new Point(0.5, 1);
   }
  lbg.GradientStops.Add(new GradientStop(Colors.White, 0));
  lbg.GradientStops.Add(new GradientStop((cylinder.Foreground as SolidColorBrush).Color, 0.3));
  lbg.GradientStops.Add(new GradientStop(Colors.Black, 0.7));
  lbg.Opacity = 0.25;

  drawingContext.DrawGeometry(lbg, null, pathgeometry);
  }

 /// <summary>
 /// Create an opacitymask composed of a rectangle and a circle,
 /// to round to bottom end of the progress bar
 /// </summary>
 /// <param name="radius">Radius of the circle to use</param>
 public Brush getOpacityMask()
  {
  GeometryDrawing maskDrawing = new GeometryDrawing(Brushes.Black, null, pathgeometry);
  DrawingBrush drawingBrush = new DrawingBrush
   {
   Drawing = maskDrawing,
   Stretch = Stretch.None,
   ViewboxUnits = BrushMappingMode.Absolute,
   AlignmentX = AlignmentX.Left,
   AlignmentY = AlignmentY.Top,
   };
  return drawingBrush;
  }

 /// <summary>
 /// Determine size and place of an ellipse,
 /// it goes on top of the cylinder.
 /// </summary>
 /// <param name="origin">Center of the ellipse</param>
 /// <param name="radius">Width and height of the circle</param>
 private void placeEllipse(DrawingContext drawingContext)
  {
  Point origin = new Point();
  Vector radius = new Vector();

  if (cylinder.Orientation == Orientation.Vertical)
   {
   radius.X = pathgeometry.Bounds.Width / 2;
   radius.Y = pathgeometry.Bounds.Width / sizeFactor;

   origin.X = pathgeometry.Bounds.Width / 2;
   origin.Y = pathgeometry.Bounds.Top;
   }
  else
   {
   radius.X = pathgeometry.Bounds.Height / sizeFactor;
   radius.Y = pathgeometry.Bounds.Height / 2;

   origin.Y = pathgeometry.Bounds.Height / 2;
   origin.X = cylinder.Value / (cylinder.Maximum - cylinder.Minimum) * cylinder.ActualWidth;
   }

  //Paint ellipse on control
  drawingContext.DrawEllipse(cylinder.Foreground, new Pen
   (new SolidColorBrush(Colors.Black), 0.5), origin, radius.X, radius.Y);
  }
 }

As it turns out, this code works quite nicely. There is one caveat though: If you give the control a margin, and make the window small enough for the control to be clipped, the adorner is not clipped with it.
And while this is not a problem in practice when showing a full cylinder, I am still making a proper 3D cylinder the next time around, as shown here.

Want to check out the code for yourself?
download 3D Cylinder.zip from Sabercat File Hosting



Tuesday, January 24, 2017

My free batchwatermarker program is finally done!

So after a few MONTHS of tinkering the first program I decided to make, fully develop and complete is finally finished! Below, you will find some screenshots and a download link to Batchwatermarker V 1.0. There's even a manual!

I am of course fully aware that making this program was not a lot different from pissing into the ocean. But hey, I chose to make it, see it through, and i learned a ton doing it. Mostly, that i need to get a lot better at planning what i want to make. Seriously. Just today I was testing the installation package I made. The first one didn't work because it missed a .dll file. After more testing I fixed the last bugs, and I made a few small improvements. Not good imho, after spending so much time messing with it.
However I did not just spend time making it work, I spent a considerable time to make it work RIGHT. As a result, the few things that still needed attention were very easy to find and change. Properly putting classes in separate files instead of spaghetti code is key. There are only a few functions in this program, so I was able to test them thoroughly and make sure that everything works. Even when you don't press the buttons in the proper order....

So here is what is looks like. First page:
Select the files you wish to mark and where you wish to save the marked files to disc.











Second page:
Specify and move your watermark. You can preview the result on the right.







Third page:
Change the font, style, color, opacity and size of the text used to watermark your image. 
When you press the "Mark my images" button, the watermark is applied to each image in the list from step 1 and written to disk with the location and filename specified.








So, would you like to give it a try? Click the link just below, and let me know what you think. Its FREE.

download BatchWaterMarker.msi from Sabercat File Hosting