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:
- Use a single Geometry to define all your rectangles, or
- 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:
- Internally construct your chart several times larger than you want to display it, for example by wrapping it in a ViewBox.
- Use a VisualBrush or RenderTargetBitmap to force your chart to be rendered separately
- 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.