views:

133

answers:

2

I have had the problem described in the question Tiling rectangles seamlessly in WPF, but am not really happy with the answers given there.

I am painting a bar chart by painting lots of rectangles right next to each other. Depending on the scale of the canvas containing them, there are small gaps visible between some of them as a result from sub-pixel rendering.

I learned from the above question how to make my rectangles fit with the screen pixels, removing that effect.

Unfortunately, my chart may display way more bars than there are pixels. Apart from the tiny gaps (which manifest as a periodic change in color saturation), this works well. If I snap each bar with the screen pixels, most of the bars vanish, though, so I am looking for another solution.

Thanks in advance!

+1  A: 

Target 4.0, use layout rounding (blog post with demo).

Will
@Will: Thanks for the input. I actually tried that already, but it behaves exactly like my previous approach using GridLineSet, i.e. each bar has a width of at least 1 pixel.
Jens
@Jens no idea other than that. Sounds like more of a design issue (more bars than pixels sounds bad to me). You might think about cutting off bars below a certain point, letting users zoom in on the normally sub-pixel bars' location to see them rendered in an easier to handle fashion.
Will
+2  A: 

Cause of the problem

Subpixel shapes use alpha blending within the pixel. Unfortunately there is no alpha blending algorithm that results in the rectangles blending seamlessly when abutted.

For example, if:

  • The background color is white
  • The foreground color is black, and
  • You have two rectangles, each covering half of a single pixel

Each rectangle will be painted as black with 50% opacity. The first converts the white pixel to gray. The second converts it to a darker gray, but not black. If these rectangles continue black in adjacent pixels you see a dark gray pixel among the black.

Two types of solutions

There are two general ways to solve this problem:

  1. Use a single Geometry to define all your rectangles, or
  2. Force the initial rendering to be at a high enough resolution your user won't see the problem.

How to use a single Geometry

If you just have a set of Rectangles, you can create a simple control that paints over the whole set of rectangles with a single PathGeometry containing the combined shape. To illustrate the idea, if you had two rectangles beside each other of different heights, like this:

<Rectangle Canvas.Left="0" Canvas.Top="0" Width="1.5" Height="2" Fill="Red" />
<Rectangle Canvas.Left="1.5" Canvas.Top="0" Width="1.5" Height="4" Fill="Red" />

You could render it with a single PathGeometry like this:

<Path Data="M0,0 L0,2 L1.5,2 L1.5,4 L3,4 L3,0 Z" Fill="Red" />

A practical way to implement this is to:

  • Paint your rectangles with a Transparent brush so they will be clickable but not visable
  • Add a Path control underneath the rectangles in Z order
  • Data binding the Data property of your Path control to your data source with a converter that constructs the geometry.

If you are using the layout system to position your rectangles, you may instead want to use an AdornerLayer by creating an Adorner for each rectangles, then when rendering the adorners compute the combined path for the first one and make the rest invisible.

The above assumes it is easy to generate the PathGeometry from the source data. For more complex scenarios, the Path control can be subclassed to search the visual tree of its parent for specified shapes and use general geometricl algorithms to compute a PathGeometry that represents the union of them with no extra edges.

If your rectangles will have multiple colors, you can use multiple Path controls one per color, or you can construct a Drawing object and show that.

Here is the structure of the code to construct a PathGeometry:

var geo = new PathGeometry();
var figure = new PathFigure();
var segment = new PolyLineSegment();
segment.Points.Add(...);
segment.Points.Add(...);
segment.Points.Add(...);
segment.Points.Add(...);
segment.Points.Add(...);
figure.Segments.Add(segment);
geo.Figures.Add(figure);

How to force the initial rendering to be at high resolution

To force rendering at higher resolution:

  1. Internally construct your chart several times larger than you want to display it, for example by wrapping it in a ViewBox.
  2. Use a VisualBrush or RenderTargetBitmap to force your chart to be rendered separately
  3. Add a Rectangle painted with that VisualBrush to your UI

Note that normally WPF is clever about rendering at the actual resolution required when you use a ViewBrush, but it can be tricked by having the actual chart actually display on the screen at the larger size, but then be clipped by a parent control so you don't actually see the too-big version.

This problem doesn't exist with RenderTargetBitmap, of course, since you specify the resolution you want, but it can be tricky knowing when to re-render the bitmap. If you only re-render on data changes you can use an event, but if you want any visual change to trigger a re-render it is more difficult.

Ray Burns
Great answer, thanks!
Jens