Transforming a known size to device pixels
If your visual element is already attached to a PresentationSource (for example, it is part of a window that is visible on screen), the transform is found this way:
var source = PresentationSource.FromVisual(element);
Matrix transformToDevice = source.CompositionTarget.TransformToDevice;
If not, use HwndSource to create a temporary hWnd:
Matrix transformToDevice;
using(var source = new HwndSource(new HwndSourceParameters()))
transformToDevice = source.CompositionTarget.TransformToDevice;
Note that this is less efficient than constructing using a hWnd of IntPtr.Zero but I consider it more reliable because the hWnd created by HwndSource will be attached to the same display device as an actual newly-created Window would. That way, if different display devices have different DPIs you are sure to get the right DPI value.
Once you have the transform, you can convert any size from a WPF size to a pixel size:
var pixelSize = (Size)transformToDevice.Transform((Vector)wpfSize);
Converting the pixel size to integers
If you want to convert the pixel size to integers, you can simply do:
int pixelWidth = (int)pixelSize.Width;
int pixelHeight = (int)pixelSize.Height;
but a more robust solution would be the one used by ElementHost:
int pixelWidth = (int)Math.Max(int.MinValue, Math.Min(int.MaxValue, pixelSize.Width));
int pixelHeight = (int)Math.Max(int.MinValue, Math.Min(int.MaxValue, pixelSize.Height));
Getting the desired size of a UIElement
To get the desired size of a UIElement you need to make sure it is measured. In some circumstances it will already be measured, either because:
- You measured it already
- You measured one of its ancestors, or
- It is part of a PresentationSource (eg it is in a visible Window) and you are executing below DispatcherPriority.Render so you know measurement has already happened automatically.
If your visual element has not been measured yet, you should call Measure on the control or one of its ancestors as appropriate, passing in the available size (or new Size(double.PositivieInfinity, double.PositiveInfinity)
if you want to size to content:
element.Measure(availableSize);
Once the measuring is done, all that is necessary is to use the matrix to transform the DesiredSize:
var pixelSize = (Size)transformToDevice.Transform((Vector)element.DesiredSize);
Putting it all together
Here is a simple method that shows how to get the pixel size of an element:
public Size GetElementPixelSize(UIElement element)
{
Matrix transformToDevice;
var source = PresentationSource.FromVisual(element);
if(source!=null)
transformToDevice = source.CompositionTarget.TransformToDevice;
else
using(var source = new HwndSource(new HwndSourceParameters()))
transformToDevice = source.CompositionTarget.TransformToDevice;
if(element.DesiredSize == new Size())
element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
return (Size)transformToDevice.Transform((Vector)element.DesiredSize);
}
Note that in this code I call Measure only if no DesiredSize is present. This provides a convenient method to do everything but has several deficiencies:
- It may be that the element's parent would have passed in a smaller availableSize
- It is inefficient if the actual DesiredSize is zero (it is remeasured repeatedly)
- It may mask bugs in a way that causes the application to fail due to unexpected timing (eg. the code being called at or above DispatchPriority.Render)
Because of these reasons, I would be inclined to omit the Measure call in GetElementPixelSize and just let the client do it.
Here is my (quick and dirty) example. It seems to work for me and I hope it works for you. I'm assuming you can take care of main.cpp on your own. I did this on a MacBook Air 11.6" and substituted a picture of a dime for the USA icon included with OS X:
#ifndef WINDOW_H
#define WINDOW_H
#include <QtGui>
class Window : public QWidget
{
Q_OBJECT
public:
QWidget *canvas;
QSlider *slider;
QPixmap pixmap;
private:
qreal zoom;
qreal pixels;
qreal px_width;
qreal px_height;
qreal mm_width;
qreal mm_height;
public:
Window();
void paintEvent(QPaintEvent *);
public slots:
void setZoom(int);
};
Window::Window()
{
QHBoxLayout *layout = new QHBoxLayout;
canvas = new QWidget;
slider = new QSlider;
slider->setMinimum(0);
slider->setMaximum(100);
slider->setValue(50);
layout->addWidget(canvas);
layout->addWidget(slider);
this->setLayout(layout);
if(!pixmap.load(":/resources/USA.gif"))
{
qDebug() << "Fatal error: Unable to load image";
exit(1);
}
QObject::connect(slider, SIGNAL(valueChanged(int)), SLOT(setZoom(int)));
}
void Window::paintEvent(QPaintEvent *event)
{
QPainter paint;
paint.begin(this);
paint.scale(zoom, zoom);
paint.drawPixmap(0,0, pixmap);
paint.end();
}
void Window::setZoom(int new_zoom)
{
zoom = (qreal)(50+new_zoom) /50;
pixels = pixmap.width() * zoom;
QDesktopWidget desk;
px_width = desk.width() / pixels;
px_height = desk.height() / pixels;
mm_width = px_width * 17.9;
mm_height = px_height * 17.9;
qDebug() << "Zoom: " << zoom;
qDebug() << "desk.widthMM:" << desk.widthMM();
qDebug() << "px_width: " << px_width;
qDebug() << "px_height: " << px_height;
qDebug() << "mm_width: " << mm_width;
qDebug() << "mm_height: " << mm_height;
this->repaint();
}
#include "moc_window.cpp"
#endif // WINDOW_H
Best Answer
This isn't actually possible, because for it to work, WPF would have to know the resolution (in terms of DPI) of your monitor. Sounds nice in theory, but in practice windows doesn't know this information. This is why windows itself always assumes 96dpi blindly instead of being smarter about it.
Even if there were some way to manually tell it, or if your particular monitor has a custom driver that does pass the correct information to windows, this isn't going to work on anyone else's computer, so windows doesn't pass this information on to any applications.
The best you can do is draw a scale like google maps does. You know that 1 pixel == 1 mile, so you can draw a 50 pixel line on your map, with a label saying "this line equals 50 miles"