Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

How to Make WPF Behave like Windows when Dealing with Images (Solving DPI Problems)

5.00/5 (4 votes)
10 May 2023CPOL10 min read 15.5K  
WPF design might have been too clever when using DIP (device independent pixels)
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:

XAML
<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.

XAML
<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:

XAML
<Image Name="MyImage"/>
C#
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:

C#
//correct the monitor dpi
var dpi = VisualTreeHelper.GetDpi(this);
var correctionX = 96/dpi.PixelsPerInchX;
var correctionY = 96/dpi.PixelsPerInchY;
//correct the image's dpi
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:

  1. 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.
  2. Calculating DIPs which are needed for Image is complicated, it involves the corrections mentioned above.
  3. 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.
  4. 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.
  5. 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:

C#
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.

C#
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) {
  //use the BitmapImage as it is
  bitmapSource = bitmapImage;
} else {
  //create a new BitmapSource and use dpi to set its DPI without changing any pixels
  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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)