Microsoft Dot Net Master

Microsoft Dot Net Master
Microsoft Dot Net Master

Tuesday, July 31, 2012

Draw lines excactly on physical device pixels

Why do my lines appear so blurry?

When you draw a line in WPF you will experience that they often appear blurry. The reason for this is the antialiasing system that spreads the line over multiple pixels if it doesn't align with physical device pixels.
The following example shows a usercontrol that overrides the OnRender method for custom drawing a rectange to the drawingContext. Even if all points are integer values and my screen has a resolution of 96dpi the lines appear blurry. Why?
 
protected override void OnRender(DrawingContext drawingContext)
{
    Pen pen = new Pen(Brushes.Black, 1);
    Rect rect = new Rect(20,20, 50, 60);
 
    drawingContext.DrawRectangle(null, pen, rect);
}
 
 

Resolution independence

WPF is resoultion independent. This means you specify the size of an user interface element in inches, not in pixels. A logical unit in WPF is 1/96 of an inch. This scale is chosen, because most screens have a resolution of 96dpi. So in most cases 1 logical unit maches to 1 physical pixel. But if the screen resolution changes, this rule is no longer valid.

Align the edges not the center points

The reason why the lines appear blurry, is that our points are center points of the lines not edges. With a pen width of 1 the edges are drawn excactly between two pixels.
A first approach is to round each point to an integer value (snap to a logical pixel) an give it an offset of half the pen width. This ensures, that the edges of the line align with logical pixels. But this assumes, that logical and physical device pixels are the same. This is only true if the screen resolution is 96dpi, no scale transform is applied and our origin lays on a logical pixel.

Using SnapToDevicePixels for controls

All WPF controls provide a property SnapToDevicePixels. If set to true, the control ensures the all edges are drawn excactly on physical device pixels. But unfortunately this feature is only available on control level.

Using GuidelineSets for custom drawing

Our first approach to snap all points to logical pixels is easy but it has a lot of assumptions that must be true to get the expected result. Fortunately the developers of the milcore (MIL stands for media integration layer, that's WPFs rendering engine) give us a way to guide the rendering engine to align a logical coordinate excatly on a physical device pixels. To achieve this, we need to create a GuidelineSet. The GuidelineSet contains a list of logical X and Y coordinates that we want the engine to align them to physical device pixels.
If we look at the implementation of SnapToDevicePixels we see that it does excatly the same.
 
protected override void OnRender(DrawingContext drawingContext)
{
    Pen pen = new Pen(Brushes.Black, 1);
    Rect rect = new Rect(20,20, 50, 60);
 
    double halfPenWidth = pen.Thickness / 2;
 
    // Create a guidelines set
    GuidelineSet guidelines = new GuidelineSet();
    guidelines.GuidelinesX.Add(rect.Left + halfPenWidth);
    guidelines.GuidelinesX.Add(rect.Right + halfPenWidth);
    guidelines.GuidelinesY.Add(rect.Top + halfPenWidth);
    guidelines.GuidelinesY.Add(rect.Bottom + halfPenWidth);
 
    drawingContext.PushGuidelineSet(guidelines);
    drawingContext.DrawRectangle(null, pen, rect);
    drawingContext.Pop();
}
 
 
The example above is the same as at the beginning of the article. But now we create a GuidelinesSet. To the set we add a horizontal or vertical guidelines for each logical coordinate that we want to have aligned with physical pixels. And that is not the center point, but the edge of our lines. Therefore we add half the penwidth to each point.
Before we draw the rectange on the DrawingContext we push the guidelines to the stack. The result are lines that perfecly match to our physical device pixels

Adjust the penwidth to the screen resolution

The last thing we need to consider is that the width of the pen is still defined in logical units. If we want to keep the pen width to one pixel (think a moment if you really want to have this) you can scale the pen width with the ration between your screen resolution and WPF's logical units which is 1/96. The following sample shows you how to do this.
Matrix m = PresentationSource.FromVisual(this)
                .CompositionTarget.TransformToDevice;
double dpiFactor = 1/m.M11;
 
Pen scaledPen = new Pen( Brushes.Black, 1 * dpiFactor );

DrawRoundedRectangle with individual radius for each corner

DrawRoundedRectangle with individual radius for each corner

drawingContext.DrawRoundedRectangle() has an ugly limitation, that you can set the corner radius only for all four corners at the time. This function extends the drawing context by a new overload of the DrawRoundedRectangle() method, that allows it to set the corner radius for each corner individually.
 
/// <summary>
/// Draws a rounded rectangle with four individual corner radius
/// </summary>
public static void DrawRoundedRectangle(this DrawingContext dc, Brush brush,
    Pen pen, Rect rect, CornerRadius cornerRadius)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        bool isStroked = pen != null;
        const bool isSmoothJoin = true;
 
        context.BeginFigure(rect.TopLeft + new Vector(0, cornerRadius.TopLeft), brush != null, true);
        context.ArcTo(new Point(rect.TopLeft.X + cornerRadius.TopLeft, rect.TopLeft.Y), 
            new Size(cornerRadius.TopLeft, cornerRadius.TopLeft),
            90, false, SweepDirection.Clockwise, isStroked, isSmoothJoin);
        context.LineTo(rect.TopRight - new Vector(cornerRadius.TopRight, 0), isStroked, isSmoothJoin);
        context.ArcTo(new Point(rect.TopRight.X, rect.TopRight.Y + cornerRadius.TopRight), 
            new Size(cornerRadius.TopRight, cornerRadius.TopRight),
            90, false, SweepDirection.Clockwise, isStroked, isSmoothJoin);
        context.LineTo(rect.BottomRight - new Vector(0, cornerRadius.BottomRight), isStroked, isSmoothJoin);
        context.ArcTo(new Point(rect.BottomRight.X - cornerRadius.BottomRight, rect.BottomRight.Y), 
            new Size(cornerRadius.BottomRight, cornerRadius.BottomRight),
            90, false, SweepDirection.Clockwise, isStroked, isSmoothJoin);
        context.LineTo(rect.BottomLeft + new Vector(cornerRadius.BottomLeft, 0), isStroked, isSmoothJoin);
        context.ArcTo(new Point(rect.BottomLeft.X, rect.BottomLeft.Y - cornerRadius.BottomLeft), 
            new Size(cornerRadius.BottomLeft, cornerRadius.BottomLeft),
            90, false, SweepDirection.Clockwise, isStroked, isSmoothJoin);
 
        context.Close();
    }
    dc.DrawGeometry(brush, pen, geometry);

Monday, July 23, 2012

How to assign a dynamic resource link programmatically

Normally you assign resources in XAML by using the StaticResource or DynamicResource extension. But how to do it programmatically?
The static behavior is simple. Just find the resource by using the TryFindResource() method and set it to the property of your choice. But if you want to have it dynamically updated, the following code snipped is your solutions:
 
frameworkElement.SetResourceReference(dependencyProperty, resourceKey);

How to load BAML resources

What is BAML?

WPF is based on XAML an XML dialect to describe object graphs. This is very powerful but parsing an XAML file at runtime is quite expensive. So the MarkupCompiler converts the XAML file to a more compact binary version called BAML. The BAML stream then is stored to the resources of the assembly and loaded within the InitializeComponent method.
The BAML API is something unique of WPF and was not public until .NET 4.0. Now it's available through the Baml2006Reader implementation. The following code shows how to load a BAML stream in .NET 3.5 and 4.0

Load an object from a BAML stream

Load a BAML stream in .NET 3.5 (by using reflection):
 
var pc = new ParserContext();
var readerType = presentationFrameworkAssembly
                  .GetType("System.Windows.Markup.XamlReader");
var method = readerType.GetMethod("LoadBaml", 
                  BindingFlags.NonPublic | BindingFlags.Static);
return method.Invoke(null, new object[] {stream, pc, null, false});
 
 
Load a BAML stream in .NET 4.0:
 
var reader = new Baml2006Reader(stream);
var writer = new XamlObjectWriter(reader.SchemaContext);
while(reader.Read())
{
    writer.WriteNode(reader);
}
return writer.Result;

Merged ResourceDictionaries

Problems loading merged resource dictionaries in .NET 4.0

I experienced a problem, when I am trying to merge resource dictionaries at app-level. As soon as I changed the target framework from 3.5 to 4.0 they don't get loaded anymore. It seems as if they have changed something in the behavior of MergedResourceDictionaries in .NET 4.0.

I found a way how to fix this problem. Just add a dummy default style in the resource dictionary where you merge all resources together. See the following example:
This is the official bug description from Microsoft:
On the creation of every object in XAML, if a default style is present (i.e. style w/ a key of Type) that style should be applied. As you can imagine there are several performance optimizations to make that (implied) lookup a light weight as possible. One of them is that we don’t look inside Resource Dictionaries unless they are flagged as “containing default Styles”. There is a bug: if all your default styles are nested in merged dictionaries three levels deep (or deeper) the top dictionary does not get flagged so the search skips it. The work around is to put a default Style to something, anything, <Style TargetType="x:Dummy" /> in the root Dictionary.
Bug entry on Microsoft Connect
 
<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Styles/DefaultStyle.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>
 
 
 
<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="DefaultStyle/Button.xaml"/>
    </ResourceDictionary.MergedDictionaries>
 
    <Style TargetType="{x:Type Rectangle}" />
 
</ResourceDictionary>
 
 

Improve the performance of merged ResourceDictionaries

Each time a control references a ResourceDictionary XAML creates a new instance of it. So if you have a custom control library with 30 controls in it and each control references a common dictionary you create 30 identical resource dictionaries!
 
<ResourceDictionary.MergedDictionaries>
   <SharedResourceDictionary Source="/MyControlLibrary;component/Themes/Brushes.xaml"  />
</ResourceDictionary.MergedDictionaries>
 
 
To get rid of this problem, I created the SharedResourceDictionary. You can use it the same way as a conventional ResourceDictionary. The only suptile difference is, that if it is instanced multiple times, the resources are loaded only once.
 
 
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", 
"WPFTutorial.Utils")]
 
/// <summary>
/// The shared resource dictionary is a specialized resource dictionary
/// that loads it content only once. If a second instance with the same source
/// is created, it only merges the resources from the cache.
/// </summary>
public class SharedResourceDictionary : ResourceDictionary
{
    /// <summary>
    /// Internal cache of loaded dictionaries 
    /// </summary>
    public static Dictionary<Uri, ResourceDictionary> _sharedDictionaries =
        new Dictionary<Uri, ResourceDictionary>();
 
    /// <summary>
    /// Local member of the source uri
    /// </summary>
    private Uri _sourceUri;
 
    /// <summary>
    /// Gets or sets the uniform resource identifier (URI) to load resources from.
    /// </summary>
    public new Uri Source
    {
        get { return _sourceUri; }
        set
        {
            _sourceUri = value;
 
            if (!_sharedDictionaries.ContainsKey(value))
            {
                // If the dictionary is not yet loaded, load it by setting
                // the source of the base class
                base.Source = value;
 
                // add it to the cache
                _sharedDictionaries.Add(value, this);
            }
            else
            {
                // If the dictionary is already loaded, get it from the cache
                MergedDictionaries.Add(_sharedDictionaries[value]);
            }
        }
    }
}

How to read WPF ResourceDictionaries from WinForms

When you add resources dictionaries to an WPF project the build action is automatically set to Page. This means that the compiler generates a BAML stream and adds it to the resources of the assembly.
Since WPF has the built-in functionality to read a BAML stream but its API is not public, we have to write a little helper class that access the internal method by using reflection.
 
public static class BamlReader
{
    public static object Load(Stream stream)
    {
        ParserContext pc = new ParserContext();
        MethodInfo loadBamlMethod = typeof(XamlReader).GetMethod("LoadBaml", 
            BindingFlags.NonPublic | BindingFlags.Static)
        return loadBamlMethod.Invoke(null, new object[] { stream, pc, null, false });
    }
}
 
 
 
StreamResourceInfo sri = System.Windows.Application.GetResourceStream(
    new Uri("/MyAssemblyName;component/MyResourceDict.xaml", UriKind.Relative));
    ResourceDictionary resources = (ResourceDictionary)BamlReader.Load(sri.Stream);

Drag and Drop in WPF

Drag and Drop in WPF

Introduction

Drag&Drop can drasticly improve the productiviy and user experience of a software. But only a few programmers provide drag and drop functionality in their applications, because they think its much more dificult than it really is. This article shows how simple drag and drop can be implemented in WPF.

Drag&Drop in 6 Steps

  1. Detect a drag as a combinatination of MouseMove and MouseLeftButtonDown
  2. Find the data you want to drag and create a DataObject that contains the format, the data and the allowed effects.
  3. Initiate the dragging by calling DoDragDrop()
  4. Set the AllowDrop property to True on the elements you want to allow dropping.
  5. Register a handler to the DragEnter event to detect a dragging over the drop location. Check the format and the data by calling GetDataPresent() on the event args. If the data can be dropped, set the Effect property on the event args to display the appropriate mouse cursor.
  6. When the user releases the mouse button the DragDrop event is called. Get the data by calling the GetData() method on the Data object provided in the event args.
...and that's all the magic.

Drag

To start the drag operation, we have to detect a mouse move while the left mouse button is pressed. To do this we have to hook up handlers on the PreviewMouseMove and PreviewMouseLeftButtonDown events.
To prevent occasionally drags, its a good design to not start the drag operation until the user has moved the mouse cursor by a couple of pixels. WPF provides a constant that contains the amount of pixel that Windows uses.
When the drag is initiated, we need to specify the data we want to drag. In our case its the data of the ListViewItem we dragged. We find the ListViewItem in the OriginalSource of the mouse event args. By calling ItemContainerGenerator.ItemFromContainer we get the data behind the ListViewItem.
Create a DataObject to transport the data to the drop location. The constructor takes two arguments. A string that describes the format and the data we want to drag.

 
<ListView x:Name="DragList" 
          PreviewMouseLeftButtonDown="List_PreviewMouseLeftButtonDown" 
          PreviewMouseMove="List_MouseMove"/>
 
 
 
private void List_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    // Store the mouse position
    startPoint = e.GetPosition(null);
}
 
 
 
private void List_MouseMove(object sender, MouseEventArgs e)
{
    // Get the current mouse position
    Point mousePos = e.GetPosition(null);
    Vector diff = startPoint - mousePos;
 
    if (e.LeftButton == MouseButtonState.Pressed &&
        Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
        Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance )
    {
        // Get the dragged ListViewItem
        ListView listView = sender as ListView;
        ListViewItem listViewItem = 
            FindAnchestor<ListViewItem>((DependencyObject)e.OriginalSource);
 
        // Find the data behind the ListViewItem
        Contact contact = (Contact)listView.ItemContainerGenerator.
            ItemFromContainer(listViewItem);
 
        // Initialize the drag & drop operation
        DataObject dragData = new DataObject("myFormat", contact );
        DragDrop.DoDragDrop(listViewItem, dragData, DragDropEffects.Move);
    } 
}
 
 
 
// Helper to search up the VisualTree
private static T FindAnchestor<T>(DependencyObject current)
    where T : DependencyObject
{
    do
    {
        if( current is T )
        {
            return (T)current;
        }
        current = VisualTreeHelper.GetParent(current);
    }
    while (current != null);
    return null;
}
 
 

Drop

To make an element be a drop location, set the AllowDrop property to true. When the user drags an item over the element, the DragEnter event is called. In this event you can analyze the data and decide if a drop is allowed or not.
When the user releases the mouse button the Drop event is called. The data is available in the DataObject provided in the DragEventArgs.

 
<ListView x:Name="DropList" 
          Drop="DropList_Drop" 
          DragEnter="DropList_DragEnter" 
          AllowDrop="True" />
 
 
 
private void DropList_DragEnter(object sender, DragEventArgs e)
{
    if (!e.Data.GetDataPresent("myFormat") ||
        sender == e.Source)
    {
        e.Effects = DragDropEffects.None;
    }
}
 
 
 
private void DropList_Drop(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent("myFormat"))
    {
        Contact contact = e.Data.GetData("myFormat") as Contact;
        ListView listView = sender as ListView;
        listView.Items.Add(contact);
    }
}