Let me start by saying I have searched extensively on this and have found partial answers, but nothing that works all the way.
I need to display bitmap images in my WPF application that are not scaled. I want to map 1 pixel of the bitmap to 1 pixel of the display. I do intend to support multiple resolutions by shipping multiple versions of my bitmaps. But I want to know that, when a particular bitmap has been chosen, it will be rendered EXACTLY as it has been designed.
My strategy for overcoming the automatic scaling that happens in WPF is to look at what is being applied automatically (by virtue of the OS DPI setting), and then apply a LayoutTransform that is the inverse, to the outermost container of my window.
This ensures that, no matter what the user's DPI settings are, the app renders the contents of the window a 1:1 ratio of WPF pixels to hardware pixels. So far, so good.
That code looks like this. (Presume this is called with an argument of 1.0).
private void SetScale(double factor)
{
// First note the current window transform factor.
// This is the factor being applied to the entire window due to OS DPI settings.
Matrix m = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice;
double currentWindowTransformFactorX = m.M11;
double currentWindowTransformFactorY = m.M22;
// Now calculate the inverse.
double currentWindowTransformInverseX = (1 / m.M11);
double currentWindowTransformInverseY = (1 / m.M22);
// This factor will put us "back to 1.0" in terms of a device-independent-pixel to physical pixel mapping.
// On top of this, we can apply our caller-specified factor.
double transformFactorX = currentWindowTransformInverseX * factor;
double transformFactorY = currentWindowTransformInverseY * factor;
// Apply the transform to the registered target container
ScaleTransform dpiTransform = new ScaleTransform(transformFactorX, transformFactorY);
if (dpiTransform.CanFreeze)
dpiTransform.Freeze();
this.pnlOutermost.LayoutTransform = dpiTransform;
}
Up to here, everything works great. No matter what I set my Windows DPI to, the contents of that main container are always exactly the same size, and the bitmaps are rendered precisely.
Now comes the fun part. I want to support different screen resolutions by providing resolution-specific artwork, and scaling my entire UI as appropriate.
It turns out that LayoutTransform works really well for this. So if I call the above method with 1.25 or 1.5 or whatever, the entire UI scales and everything looks perfect...except my images, which are back to looking stretched and crappy, even when I change the source to be an image that is exactly the right size for the new, scaled dimensions.
For example, suppose I have an image that is 100x100 in the XAML. My artwork comes in three flavors: 100x100, 125x125, and 150x150. When I scale the container that houses the image, I also change the source of that image to the appropriate one.
Interestingly, if the image object is sitting at a position that, when scaled by the factor, yields integral results, then the scaled image looks fine. That is to say, suppose the image has the following properties:
Canvas.Left = 12
Canvas.Top = 100
When we apply a factor of 1.25, this yields 15 and 125, and the image looks great. But if the image is moved by one pixel, to say:
Canvas.Left = 13
Canvas.Top = 100
Now when we apply a factor of 1.25, we get 15.25 and 125, and the result looks crappy.
Clearly, this looks like some kind of rounding issue or something like that. So I've tried:
UseLayoutRounding="True"
SnapsToDevicePixels="True"
RenderOptions.EdgeMode="Aliased"
RenderOptions.BitmapScalingMode="NearestNeighbor"
I've tried these in the window, in the container being scaled, and in the image object. And nothing works. And the BitmapScalingMode doesn't really make sense anyway, because the image should not be being scaled at all.
Eternal thanks to anyone who can shed some light on this.
I had the exact same problem so it looks like this has not been fixed in the framework as of 2019.
I managed to solve the issue using a three step approach.
Enable layout rounding on my top level UI element
Apply the inverse
LayoutTransform
to myImage
objects (theLayoutTransform
was applied to the parentListBox
).Subclass
Image
and add a custom override forOnRender
.The code from step 3 is based on this blogpost.
By using the
CustomImage
class in my XAML instead ofImage
and binding to aBitmapSource
that will return a properly sized image based on the current scale factor, I managed to achieve great looking images without any unwanted scaling.Note that you might need to call
InvalidateVisual
on your images when they need to be re-rendered.