WPF makes it easy to include image files into the UI. However, giving the user the ability to import .jpg and similar files, choosing which part of the photo should get displayed and displaying it as thumbnail EFFICIENTLY is a challenge. This article does not show a complete application, but the problems one encounters and how to solve them.
Introduction
This is a continuation of my previous article, How to Make WPF Behave like Windows when Dealing with Images (Solving DPI Problems), which explains how to prevent WPF displaying images in different sizes than Windows. In this article, I describe all other functionality needed so that the user can import and display pictures.
Importing Images from Clipboard
There are different possibilities from where the user might want to import an image:
- He chooses a picture file in Windows Explorer and copies it into the Clipboard
- He copies the path to a picture file into the Clipboard
- He makes a screen grab which gets stored in the Clipboard
Once the image is in the Clipboard, he uses Ctrl + v to paste the image into the WPF application.
To catch Ctrl + v, add a KeyDown
event handler to the Window
where the user can import images:
private void Window_KeyDown(object sender, KeyEventArgs e) {
if (e.KeyboardDevice.Modifiers == ModifierKeys.Control) {
if (e.Key == Key.V) {
pasteFromClipboard();
}
}
}
More challenging is reading the Clipboard. In theory, it should be simple, something like:
if (Clipboard.ContainsText()) {
var filePath = Clipboard.GetText();
} else if (Clipboard.ContainsImage()) {
var image = System.Windows.Clipboard.GetImage();
}
After a lot of trial and error, I found that Clipboard.ContainsText()
doesn't work as expected and I had to change the code to this:
BitmapSource? bitmapSource;
if (Clipboard.ContainsImage()) {
bitmapSource = Clipboard.GetImage();
} else {
var dataObject = Clipboard.GetDataObject();
var formats = dataObject.GetFormats();
if (formats.Contains("FileName")) {
var f = dataObject.GetData("FileName");
var fn = ((string[])Clipboard.GetData("FileName"))[0];
var extension = System.IO.Path.GetExtension(fn)[1..].ToLowerInvariant();
if (extension == "jpg" || extension == "png" || extension == "gif" ||
extension == "bmp" || extension == "tiff" || extension == "icon")
{
bitmapSource = new BitmapImage(new Uri(fn));
}
}
}
BitmapSource
is the base class containing a hidden bitmap which stores for each pixel of the image its value. The derived classes help with creating a BitmapSource
or processing it.
Before the image can get displayed to the user, one has to undo the DIP (Device Independent Pixel) WPF uses.
Windows does not use the DPI (Dots Per Inch) information of the image file. The size of the image displayed just depends on how many pixels the image contains:
WPF on the other hand shows images with the same number of pixels but different DPIs with different sizes:
To avoid confusing the user, one has to undo WPF's DPI handling before displaying the picture to the user. This can be done like this:
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;
For a detailed explanation how this works, please see my article titled How to Make WPF Behave like Windows when Dealing with Images (Solving DPI Problems).
Note
It is important to use BeginInit()
to construct bitmapImage
. There is also a constructor BitmapImage(string URL)
. It is simpler to use, but gives a headache later when the user decides to delete the file. That produces the error message File.Delete("imageFilePath")
cannot access the file, because another process is accessing it. The "other" process is actually our application, because BitmapImage(string URL)
doesn't release the file until bitmapSource
gets garbage collected. There is not even a Dispose()
for BitmapImage
. How crazy is that?
bitmapSource
just stores the pixel values of the image. ImageControl
is a FrameworkElement
and can be used to display the content of bitmapSource
.
Letting the User Chose Which Part of the Image He Would Like to Import
The user might want to import only part of the image and make it bigger or smaller. My WPF application Photo Importer is doing just that:
First, the user selects in a Windows application like Windows Explorer a picture, copies it to the clipboard, then switches into Photo Importer and presses Ctrl + v. The user can zoom in and out with the Zoom Scrollbar on the right. With the mouse, he can move the gray Selection Rectangle. The area inside the rectangle is the part of the image that will be stored in the application.
In the footer are also TextBoxes
where the user can key in X
and Y
offset manually instead of using the mouse. He can also enter Width
and Height
of the gray Selection Rectangle.
By the way, this is a tricky part when programming that app. The user thinks in pixels of the image, while WPF uses DIP for X
, Y
, Width
and Height
. Depending on the resolution (DPI) of the monitor, 1 image pixel might cover several monitor pixels or only part of a monitor pixel.
This line reads the monitor's DPI:
var dpi = VisualTreeHelper.GetDpi(this);
To translate from image pixels (WidthTextBox
) to WPF DIP (SelectionRectangle.Width
):
SelectionRectangle.Width = int.Parse(WidthTextBox.Text)/dpi.DpiScaleX;
To translate from WPF DIP (mouse movement) to image pixels (position Selection Rectangle):
private void SelectionRectangle_MouseMove(object sender, MouseEventArgs e) {
if (e.LeftButton==MouseButtonState.Released) return;
var newMousePosition = e.GetPosition(ImageGrid);
newMousePosition.Offset((selectionPositionStartX - mouseStartPosition.X),
(selectionPositionStartY - mouseStartPosition.Y));
setSelectionPosition
(newMousePosition.X*dpi.DpiScaleX, newMousePosition.Y*dpi.DpiScaleY);
}
The above code might be difficult to understand. Here is an explanation in pseudo code.
First, we calculate how many DIPs the mouse has travelled:
var mouseTravelDistance = newMousePosition - mouseStartPosition;
We then add this difference to the original SelectionRectangle
position:
var newSelectionRectanglePosition = selectionPositionStart + mouseTravelDistance
Finally, we multiply newSelectionRectanglePosition
with dpi.DpiScaleX
to get the selection position in number of image pixels.
In theory, the change of mouse position in DIP could be used directly to calculate the new position of the SelectionRectangle
, which is also in DIP (i.e., X
and Y
). But since we don't want to allow the user to move the SelectionRectangle
outside the image, we have to limit the SelectionRectangle
position (DIP) with the maximal dimension of the image (pixel). Which means
- calculate how many DIPs the mouse has moved
- translate that DIP distance into a number of pixels
- calculate the new
SelectionRectangle
position in pixel (newPosition = oldPosition + MouseMovementInPixel)
- limit the new
SelectionRectangle
position to the max dimension of the image - convert the limited new
SelectionRectangle
position back into DIPs and use that value to position the Selection Rectangle.
Actually, it is even a bit more complicated, for details, see the code attached to this article.
Luckily, zooming in and out can easily be implemented using a LayoutTransform
:
ImageControl.LayoutTransform = new ScaleTransform(zoomFactor, zoomFactor);
A zoomFactor
of 1 does not change anything, 0.5 decreases the displayed image by half, 2 doubles the image in size.
Note
The value scale of the zoom Scrollbar
must be logarithmic/exponential. Let's say if the user moves the Scrollbar by an inch and the image size doubles, i.e., zoomFactor
becomes 2. If the user moves the Scrollbar by another inch, the image size should double again and zoomFactor
becomes 4, not 2. Again, for details, see the attached code.
Storing the New Image
At this point, the user has chosen which part of the image he wants to use. That is all the functionality in Photo Importer. How the image gets saved is very application specific, for example, in a harddisk directory or in a database. Also, how an image gets linked to the other data is application specific.
I have attached the code for the class ImageCache
. It stores the images in RAM and in a Windows directory. Even WPF is astonishingly fast displaying images stored in a SSD drive, I felt it would be better to read the pictures in a cache first and then display it, because in my application, the same image gets displayed several times. Furthermore, I wanted a small thumbnail for every image which I then could use to display in a DataGrid
.
I would have liked to have the ImageCache
not in the UI layer, but in a lower layer which has no reference to WPF. Unfortunately, a BitmapSource
must be used to store an image for WPF in RAM, so I placed my ImageCache
in the UI layer. Of course, I could place ImageCache
also in its own DLL, which would have the advantage that it is easier to write unit tests for it.
I found it best to store all images in two different Dictionary<int, BitmapSource>
, one for normal sized pics and one for thumbnails, which I use quite often when I display my data in a DataGrid
. The int
is the UserId
, which uniquely identifies each user and will never change. The image I store in a SSD directory with the filename being Pic999.jpg, where 999 stands for the actual UserId
.
Note
I expect my application to store not more than 1000 pictures and I don't mind using 1GByte of RAM for that purpose. If your application deals with tons of pictures, you could write a more sophisticated cash which removes unused pics from the cache or not use a cache at all, since reading an image file from a SSD drive is surprisingly fast. Even if you don't use ImageCache
, use its code to guide you when you write reading, storing and deleting images.
Displaying the Image
Displaying an image in the size it is stored is straight forward:
<Border HorizontalAlignment="Center" VerticalAlignment="Center"
Margin="5" BorderBrush="Black" BorderThickness="2">
<Image x:Name="ImageControl" Stretch="None"/>
</Border>
In code behind:
ImageControl.Source = bitmapSource;
Displaying the thumbnail version in a DataGrid
goes like this:
<DataGridTemplateColumn Header="Pic" Width="SizeToHeader">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Source="{Binding Pic}" Margin="-1"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
The DataGrid.DataContext
binds to a list of user records, which has a property with the name Pic
and the type BitmapSource
. The thumbnail image gets created like this:
const double maxWidth = 19;
const double maxHeight = 30;
var scaleFactor = Math.Min(maxWidth / bitmapSource.Width,
maxHeight / bitmapSource.Height);
userRecord.Pic =
new TransformedBitmap(bitmapSource, new ScaleTransform(scaleFactor, scaleFactor));
I know, reading this article is tedious. But I wish someone else would have written it when I started my application. I hope it will be helpful for others.
Recommended Reading
If you are interested in WPF, I strongly recommend looking at some of my other WPF articles on CodeProject, which are more enjoyable to read:
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):
The most fun:
I wrote MasterGrab 6 years ago and since then, I play it nearly every day before I start programming. It takes about 10 minutes to beat 3 robots who try to grab all 200 countries on a random map. The game finishes once one player owns all the countries. The game is fun and fresh every day because the map looks completely different each time. The robots bring some dynamics into the game, they compete against each other as much as against the human player. If you like, you can even write your own robot, the game is open source. I wrote my robot in about two weeks (the whole game took a year), but I am surprised how hard it is to beat the robots. When playing against them, one has to develop a strategy so that the robots attack each other instead of you. I will write a CodeProject article about it sooner or later, but you can already download and play it. There is good help in the application explaining how to play: