WPF tried to be more clever than Windows and introduced DIP, device independent pixels, which should make sure that an image gets displayed in the same size (inches) regardless of the size of the monitor. What sounds like an improvement might be actually a disadvantage. If an image displayed on a laptop screen occupies x inches, it should not have the same inch size on a mobile phone nor on a TV. This article explains how to make WPF behave more like Windows.
Introduction
When Windows displays a picture like a .jpg file in Photos, it does not care about the file's dpi (Dots Per Inch) settings. Files with different dpi settings but the same number of pixels get displayed in the same size. Here are three files, all of them using 500 x 500 pixels, but different dpi settings:
WPF displays the same pictures like this:
If you write an app which takes an image file produced by a Windows program, change some stuff in the image and then write it back as a new image file, the user expects that the WPF app behaves like a traditional Windows app, i.e., shows the image in the same size as Windows does.
A similar difference exists regarding the dpi setting of the monitor. Windows uses for the image the same number of "pixels" regardless of the monitor's dpi settings. Note that the user can change in Windows how many monitor pixels are used to display one "pixel".
WPF, on the other hand, uses the dpi information of the image file. If that file is 96 pixels wide and has a DPI of 96, WPF tries to display it 1 inch long on a 96 dpi monitor and a 192 dpi monitor. In the first case, it uses 96 monitor pixels, in the second 192.
Unfortunately, in many cases, the user does not care about the real size, but wants to see the whole image, regardless if the user looks at a small phone screen, a laptop screen or a gigantic TV screen. What use is it to display the image with 1 inch on a 100 inch TV?
This article explains how to write code so that WPF deals with images like Windows does. The solution is indeed simple, but it took me a month to figure it out.
How to Display an Image in WPF
The simplest WPF code to display an image file is this:
<Image Source="SomePath\SomeImageFile.jpg"/>
WPF graphic system works best when graphics are defined in graphic commands (vector graphic), like draw a line from point A to point B using a certain thickness and colour.
Side Note
It is mostly impossible to draw a black 1 monitor pixel line. The problem is that the Control
displaying the graphic gets positioned by other controls containing it and the image border most often gets placed between 2 monitor pixels. Which has the effect that the line gets painted on 2 pixels and in a gray colour instead of black. There is one more problem involved. Many think that when they set StrokeThickness = 1
they get 1 pixel on the monitor. But depending on the monitor's dpi, 1DIP might get converted into several pixels or only a part of a pixel. There are properties like SnapsToDevicePixels
or GuidelineSet
that should help with that, but I never managed to get a properly black 1 pixel line.
However, image files are pixel oriented, something WPF does not really like. It stores the image's pixel values in a bitmap, a class called BitmapSource
, which stores the pixels in a 2 dimensional array, but that array is hidden. If you want to read and write pixels, you need to use a WriteableBitmap
, which inherits from BitmapSource
.
Image
uses an internal class derived from BitmapSource
for storing the pixels. It then scales the bitmap's pixels to screen pixels.
To allow processing of the bitmap before displaying it, it is better to first read the image file into a BitmapImage
, which inherits from BitmapSource
, and then assign that to Image.Source
.
<Image Stretch="None">
<Image.Source>
<BitmapImage UriSource="SomePath\SomeImageFile.jpg" />
</Image.Source>
</Image>
Usually, I prefer to define in XAML only UI related controls and do everything else in code behind:
<Image Name="MyImage"/>
var bitmapImage= new BitmapImage(new Uri("SomePath\SomeImageFile.jpg"));
MyImage.Source = bitmapSource;
Image is a FrameworkElement
and has as such a Width
and Height
defined in DIP and can be a part of the visual tree. BitmapImage
does not inherit from Visual
and can therefore not be used in the visual tree. Some of BitmapImage
properties, which are all readonly
:
PixelWidth
, PixelHeight
: Size of bitmap in image pixels DpiX
, DpiY
: Dots Per Inch as indicated in the image file Width
, Height
: Size of bitmap in Device Independent Pixels. Width
= 96 / DpiX
* PixelWidth
. They often have different values than Image.ActualWidth
and Image.ActualHeight
. DecodePixelWidth
, DecodePixelHeight
: can be used before reading the image file to indicate how big the resulting bitmap should be. This minimises the bitmap size when you know already that you need only a small version of the picture, like a thumbnail picture. But when you want to process the image and use the result later in Windows, do not give DecodeXxx
a value, which means the bitmap will contain for each pixel the same value as the image file.
WPF Bitmap Class Inheritance
One challenge when trying to understand bitmaps in WPF is that there are actually quite a number of bitmap classes. Here are the most important ones:
Image
is a FrameworkElement
. It is not a bitmap, but holds a bitmap or vector graphics in its Source
property. ImageSource
is the base class for bitmap or vector graphics. It has only few properties, basically the Height
and Width
of the image using DIP. DrawingImage
is for vector graphics and is not part of this article. BitmapSource
is the base class for all other bitmap classes. I think it holds the actual data of the bitmap in a 2 dimensional array, which is invisible to the programmer. It is possible to create a BitmapSource
from an integer array holding the pixel's values and to export the bitmap as integer array.
These three classes are for creating bitmaps:
BitmapImage
: read an image file BitmapFrame
: useful to store gif files, which might have several frames (images) RenderTargetBitmap
: renders a WPF Visual
into a bitmap
These three classes are for processing bitmaps. They take a BitmapSource
as input and provide a changed bitmap as output:
TransformedBitmap
: scales and rotates a BitmapSource
CroppedBitmap
: cuts out only a part of a BitmapSource
WriteableBitmap
: provides methods to change bitmap pixels
First Approach to Make WPF Behave like Windows
At first, I thought the problem is easy to solve. If WPF displays the image twice as big as in Windows, I simply have to resize the Image control. That can be easily done with a layout transformation like this:
var dpi = VisualTreeHelper.GetDpi(this);
var correctionX = 96/dpi.PixelsPerInchX;
var correctionY = 96/dpi.PixelsPerInchY;
var bitmapImage = new BitmapImage(new Uri("MyFile"));
correctionX *= bitmapImage.DpiX/96;
correctionY *= bitmapImage.DpiY/96;
Image.Source = bitmapImage;
Image.LayoutTransform = new ScaleTransform(correctionX, correctionY);
However, this caused me a lot of problems with the rest of the program:
- The user thinks in image pixels, for example, when he wants to create a photo with a width of 100. He doesn't care about DIP nor monitor pixels.
- Calculating DIPs which are needed for
Image
is complicated, it involves the corrections mentioned above. - Mouse movements are also in DIP. If the user can use the mouse to define which part of the image should be written into the new file, the distance defined by the mouse movement has to be translated into image pixels, so that the user knows what will be the dimension of the new image.
- The dimension of the original image sets limits to where the mouse can go. It might not make sense if the user can give the new image a greater width than the original. Meaning each mouse movement must be matched against image pixels which then need to be translated back to DIP, so that the GUI can paint over the
Image
which part will be used. - In my app, the user can zoom into the image, which can ideally be done by using
Image.LayoutTransform
. Using Image.LayoutTransform
for different purposes (zooming, DPI correction) further complicated the code.
Better programmers than I might have been able to solve all these problems, but I gave up and tried to find a simpler way to correct the DPI handling.
Second Failed Approach
My next idea was to correct the bitmap size to compensate for WPF's treatment of DPI. If WPF displayed the image twice as big than Windows, I wanted to scale the image down by 50%. Transforming an existing bitmap can be done by using TransformedBitmap
, another class inheriting from BitmapSource
. It takes any class inheriting from BitmapSource
as input and applies a transformation, in this case, a scaling:
var bitmapImage = new BitmapImage(new Uri("MyFile"));
var dpi = VisualTreeHelper.GetDpi(this);
var bitmapSource = new TransformedBitmap(bitmapImage,
new ScaleTransform(bitmapImage.DpiX/96/dpi.PixelsPerInchX,
bitmapImage.DpiY/96/dpi.PixelsPerInchX));
Image1.Source = bitmapImage;
However, I could still not get my code to work properly and additionally the image quality suffered because of the transformation, which actually changed the pixel values since it might have to enlarge 1 pixel to 1.1234 pixels or reduce one pixel to 0.4321 pixels.
Final Solution
I realised that I should not change the number of pixels, which led me to the idea that I could adjust the dpi of the bitmap. If WPF displayed the image twice as big than Windows, I did not need to scale the image, just pretend to WPF that the Bitmap has the same DPI as the values of the WPF DPI settings. The result would be that each bitmap pixel would be drawn onto 1 monitor pixel, as Windows does. But there is one catch: the dpi settings of the BitmapImage
are readonly ! Basically, the constructor of BitmapImage
reads the DPI of the image file. In order to change the dpi values, one has to export the bitmap content into a 2 dimensional array and then use that array to create a new BitmapSource
. Yes, this takes twice the amount of RAM, but luckily copying big arrays is fast and we have now lots of RAM.
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.UriSource = new Uri(fileName);
bitmapImage.EndInit();
BitmapSource bitmapSource;
var dpi = VisualTreeHelper.GetDpi(this);
if (bitmapImage.DpiX==dpi.PixelsPerInchX && bitmapImage.DpiY==dpi.PixelsPerInchY) {
bitmapSource = bitmapImage;
} else {
PixelFormat pf = PixelFormats.Bgr32;
int rawStride = (bitmapImage.PixelWidth * pf.BitsPerPixel + 7) / 8;
byte[] rawImage = new byte[rawStride * bitmapImage.PixelHeight];
bitmapImage.CopyPixels(rawImage, rawStride, 0);
bitmapSource = BitmapSource.Create(bitmapImage.PixelWidth, bitmapImage.PixelHeight,
dpi.PixelsPerInchX, dpi.PixelsPerInchY, pf, null, rawImage, rawStride);
}
ImageControl.Source = bitmapSource;
As mentioned above, many BitmapImage
properties can no longer get changed once the image file is read. However, it is possible by using BeginInit()
to set some properties and then call EndInit()
, only after which the file gets read. I had to use this approach in my app, because BitmapImage
has another annoying problem. It keeps the image file open, even after reading is completed, meaning it is impossible to show the image to the user and then let him delete it permanently. Using BitmapCacheOption.OnLoad
closes the file after reading.
The above code could be simplified to:
int rawStride = bitmapImage.PixelWidth * 32;
We already know that each pixel needs 32 bits (8 bits each for ARGB). Other PixelFormats
might not align nicely to word boundaries and for that reason, Microsoft recommends calculating rawStride
, in a more complicated way.
With that, I made WPF behave like Windows when it comes to DPI. Note that I did not change the behavior of the complete WPF Window
, but only of the Image
. The rest of my program got much simpler, although I still had to cater for the user thinking in image pixels and WPF in DIP.
Final Remarks
Yes, I am aware that this was rather tedious reading resulting in only a few lines of code. I am using WPF for over 15 years and I was still struggling to understand how bitmaps work in WPF. So I thought sharing the details of my learning process might help other developers facing the same challenge. WPF is amazingly powerful, but unfortunately also complex and kind of unfinished. Many things don't really make sense when reading just Microsoft's documentation. I hope my articles help to close a few documentation gaps.
My original intention was to write an article on how to develop a WPF app which lets the user select part of a photo and store it as a new .jpg file. I will still write that article, but I figured I need to explain first how to solve the DPI problem.
If you are interested in WPF, I strongly recommend to read some of my other WPF articles:
My most useful WPF article:
The WPF article I am the proudest of:
Indispensable testing tool for WPF controls:
WPF information sorely lacking in MS documentation:
I also wrote some non WPF articles.
Achieved the impossible:
Most popular (3 million views, 37'000 downloads):
History
- 11th May, 2023: Initial version