Как преобразовать размер WPF в физические пиксели?
Как лучше всего преобразовать ширину и высоту WPF (независимую от разрешения) в физические пиксели экрана?
Я показываю содержимое WPF в форме WinForms (через ElementHost) и пытаюсь выработать логику определения размеров. У меня это работает нормально, когда ОС работает по умолчанию 96 точек на дюйм. Но это не будет работать, когда ОС установлена на 120 dpi или другое разрешение, потому что тогда элемент WPF, который сообщает о своей ширине в 96, будет иметь ширину 120 пикселей, что касается WinForms.
Я не смог найти какие-либо настройки "пикселей на дюйм" в System.Windows.SystemParameters. Я уверен, что мог бы использовать эквивалент WinForms (System.Windows.Forms.SystemInformation), но есть ли лучший способ сделать это (читай: способ с использованием API-интерфейсов WPF, а не с помощью API-интерфейсов WinForms и ручного выполнения математических операций)? Каков "лучший способ" конвертировать "пиксели" WPF в реальные пиксели экрана?
РЕДАКТИРОВАТЬ: Я также хочу сделать это, прежде чем элемент управления WPF отображается на экране. Похоже, что Visual.PointToScreen можно сделать, чтобы дать мне правильный ответ, но я не могу его использовать, потому что элемент управления еще не является родителем, и я получаю InvalidOperationException "Этот Visual не подключен к PresentationSource".
4 ответа
Преобразование известного размера в пиксели устройства
Если ваш визуальный элемент уже прикреплен к PresentationSource (например, он является частью окна, видимого на экране), преобразование находится следующим образом:
var source = PresentationSource.FromVisual(element);
Matrix transformToDevice = source.CompositionTarget.TransformToDevice;
Если нет, используйте HwndSource для создания временного hWnd:
Matrix transformToDevice;
using(var source = new HwndSource(new HwndSourceParameters()))
transformToDevice = source.TransformToDevice;
Обратите внимание, что это менее эффективно, чем создание с использованием hWnd IntPtr.Zero, но я считаю его более надежным, потому что hWnd, созданный HwndSource, будет подключен к тому же устройству отображения, что и фактическое вновь созданное Окно. Таким образом, если разные устройства отображения имеют разные DPI, вы обязательно получите правильное значение DPI.
Получив преобразование, вы можете преобразовать любой размер из размера WPF в размер в пикселях:
var pixelSize = (Size)transformToDevice.Transform((Vector)wpfSize);
Преобразование размера пикселя в целые числа
Если вы хотите преобразовать размер пикселя в целые числа, вы можете просто сделать:
int pixelWidth = (int)pixelSize.Width;
int pixelHeight = (int)pixelSize.Height;
но более надежное решение было бы тем, которое использует 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));
Получение желаемого размера UIElement
Чтобы получить желаемый размер элемента UIElement, необходимо убедиться, что он измерен. В некоторых обстоятельствах это уже будет измеряться, либо потому что:
- Вы уже измерили это
- Вы измерили одного из его предков, или
- Он является частью PresentationSource (например, он находится в видимом окне), и вы выполняете ниже DispatcherPriority.Render, чтобы вы знали, что измерение уже произошло автоматически.
Если ваш визуальный элемент еще не был измерен, вам следует вызвать Measure для элемента управления или одного из его предков, в зависимости от случая, передав доступный размер (или new Size(double.PositivieInfinity, double.PositiveInfinity)
если вы хотите, чтобы размер к содержанию:
element.Measure(availableSize);
Как только измерение выполнено, все, что необходимо, это использовать матрицу для преобразования DesiredSize:
var pixelSize = (Size)transformToDevice.Transform((Vector)element.DesiredSize);
Собираем все вместе
Вот простой метод, который показывает, как получить размер элемента в пикселях:
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);
}
Обратите внимание, что в этом коде я вызываю Measure только в случае отсутствия DesiredSize. Это обеспечивает удобный способ сделать все, но имеет несколько недостатков:
- Может случиться так, что родительский элемент передал бы в меньшем доступном размере
- Это неэффективно, если фактический DesiredSize равен нулю (он повторно измеряется)
- Он может маскировать ошибки таким образом, что это приводит к сбою приложения из-за непредвиденного времени (например, к коду, вызываемому или выше DispatchPriority.Render).
По этим причинам я был бы склонен опустить вызов Measure в GetElementPixelSize и просто позволить клиенту сделать это.
Простая пропорция между Screen.WorkingArea и SystemParameters.WorkArea:
private double PointsToPixels (double wpfPoints, LengthDirection direction)
{
if (direction == LengthDirection.Horizontal)
{
return wpfPoints * Screen.PrimaryScreen.WorkingArea.Width / SystemParameters.WorkArea.Width;
}
else
{
return wpfPoints * Screen.PrimaryScreen.WorkingArea.Height / SystemParameters.WorkArea.Height;
}
}
private double PixelsToPoints(int pixels, LengthDirection direction)
{
if (direction == LengthDirection.Horizontal)
{
return pixels * SystemParameters.WorkArea.Width / Screen.PrimaryScreen.WorkingArea.Width;
}
else
{
return pixels * SystemParameters.WorkArea.Height / Screen.PrimaryScreen.WorkingArea.Height;
}
}
public enum LengthDirection
{
Vertical, // |
Horizontal // ——
}
Это хорошо работает с несколькими мониторами.
Я нашел способ сделать это, но мне это не очень нравится:
using (var graphics = Graphics.FromHwnd(IntPtr.Zero))
{
var pixelWidth = (int) (element.DesiredSize.Width * graphics.DpiX / 96.0);
var pixelHeight = (int) (element.DesiredSize.Height * graphics.DpiY / 96.0);
// ...
}
Мне это не нравится, потому что (а) оно требует ссылки на System.Drawing, а не на API-интерфейсы WPF; и (б) я должен сделать математику сам, что означает, что я дублирую детали реализации WPF. В.NET 3.5 мне нужно усечь результат вычисления, чтобы он соответствовал тому, что ElementHost делает с AutoSize=true, но я не знаю, будет ли это все еще точно в будущих версиях.NET.
Кажется, это работает, поэтому я публикую это на тот случай, если это поможет другим. Но если у кого-то есть лучший ответ, пожалуйста, отправляйте.
Просто сделал быстрый поиск в ObjectBrowser и нашел что-то довольно интересное, вы можете проверить это.
System.Windows.Form.AutoScaleMode, у него есть свойство DPI. Вот документы, это может быть то, что вы ищете:
public const System.Windows.Forms.AutoScaleMode Dpi = 2 Член System.Windows.Forms.AutoScaleMode
Сводка: управляет масштабом относительно разрешения экрана. Общие разрешения - 96 и 120 DPI.
Примените это к вашей форме, это должно сработать.
{наслаждаться}