Метод 1
Това е същият начин, по който го направихте в Windows Forms. Обектът System.Drawing.Graphics
предоставя удобни свойства за получаване на хоризонтален и вертикален DPI. Нека скицираме помощен метод:
/// <summary>
/// Transforms device independent units (1/96 of an inch)
/// to pixels
/// </summary>
/// <param name="unitX">a device independent unit value X</param>
/// <param name="unitY">a device independent unit value Y</param>
/// <param name="pixelX">returns the X value in pixels</param>
/// <param name="pixelY">returns the Y value in pixels</param>
public void TransformToPixels(double unitX,
double unitY,
out int pixelX,
out int pixelY)
{
using (Graphics g = Graphics.FromHwnd(IntPtr.Zero))
{
pixelX = (int)((g.DpiX / 96) * unitX);
pixelY = (int)((g.DpiY / 96) * unitY);
}
// alternative:
// using (Graphics g = Graphics.FromHdc(IntPtr.Zero)) { }
}
Можете да го използвате за преобразуване както на координатите, така и на стойностите на размера. Той е доста прост и стабилен и изцяло в управляван код (поне що се отнася до вас, потребителя,). Предаването на IntPtr.Zero
като HWND
или HDC
параметър води до Graphics
обект, който обвива контекста на устройството на целия екран.
Има обаче един проблем с този подход. Има зависимост от Windows Forms/GDI+ инфраструктура. Ще трябва да добавите препратка към сборката System.Drawing. Голяма работа? Не съм сигурен за вас, но за мен това е проблем, който трябва да избягвам.
Метод 2
Нека да го направим една стъпка по-дълбоко и да го направим по Win API начина. Функцията GetDeviceCaps
извлича различна информация за посоченото устройство и е в състояние да извлича хоризонтални и вертикални DPI, когато й предадем съответно LOGPIXELSX
и LOGPIXELSY
параметри.
Функцията GetDeviceCaps
е дефинирана в gdi32.dll
и вероятно е това, което System.Drawing.Graphics
използва под капака.
Нека да видим в какво се превърна нашият помощник:
[DllImport("gdi32.dll")]
public static extern int GetDeviceCaps(IntPtr hDc, int nIndex);
[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDc);
public const int LOGPIXELSX = 88;
public const int LOGPIXELSY = 90;
/// <summary>
/// Transforms device independent units (1/96 of an inch)
/// to pixels
/// </summary>
/// <param name="unitX">a device independent unit value X</param>
/// <param name="unitY">a device independent unit value Y</param>
/// <param name="pixelX">returns the X value in pixels</param>
/// <param name="pixelY">returns the Y value in pixels</param>
public void TransformToPixels(double unitX,
double unitY,
out int pixelX,
out int pixelY)
{
IntPtr hDc = GetDC(IntPtr.Zero);
if (hDc != IntPtr.Zero)
{
int dpiX = GetDeviceCaps(hDc, LOGPIXELSX);
int dpiY = GetDeviceCaps(hDc, LOGPIXELSY);
ReleaseDC(IntPtr.Zero, hDc);
pixelX = (int)(((double)dpiX / 96) * unitX);
pixelY = (int)(((double)dpiY / 96) * unitY);
}
else
throw new ArgumentNullException("Failed to get DC.");
}
Така че сменихме зависимостта от управлявания GDI+ със зависимостта от фантастичните Win API извиквания. Това подобрение ли е? Според мен да, стига да работим на Windows Win API е най-малкият общ знаменател. Той е лек. На други платформи вероятно няма да имаме тази дилема на първо място.
И не се заблуждавайте от това ArgumentNullException
. Това решение е толкова здраво, колкото и първото. System.Drawing.Graphics
ще хвърли същото изключение, ако не може да получи и контекст на устройството.
Метод 3
Както е официално документирано тук има специален ключ в системния регистър: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI.
Той съхранява DWORD стойност, която е точно това, което потребителят избира за DPI в диалоговия прозорец за настройки на дисплея (там се нарича размер на шрифта).
Четенето му е безпроблемно, но не бих го препоръчал. Виждате, че има разлика между официален API и хранилище за различни настройки. API е публичен договор, който остава същият, дори ако вътрешната логика е напълно пренаписана (ако не е така, цялата платформа е гадна, нали?).
Но никой не гарантира, че вътрешната памет ще остане същата. Може да е продължило няколко десетилетия, но важен проектен документ, който описва преместването му, може вече да очаква одобрение. Никога не знаеш.
Винаги се придържайте към API (каквото и да е, родно, Windows Forms, WPF и т.н.). Дори ако базовият код чете стойността от местоположението, което познавате.
Метод 4
Това е доста елегантен WPF подход, който намерих документиран в тази публикация в блог. Базира се на функционалността, осигурена от клас System.Windows.Media.CompositionTarget
, която в крайна сметка представлява повърхността на дисплея, върху която е изчертано WPF приложението. Класът предоставя 2 полезни метода:
TransformFromDevice
TransformToDevice
Имената се обясняват сами по себе си и в двата случая получаваме System.Windows.Media.Matrix
обект, който съдържа коефициентите на картографиране между единиците на устройството (пиксели) и независимите единици. M11 ще съдържа коефициент за оста X, а M22 – за оста Y.
Тъй като досега обмисляхме посока единици->пиксели, нека пренапишем нашия помощник с CompositionTarget.TransformToDevice.
Когато извикваме този метод, M11 и M22 ще съдържат стойности, които сме изчислили като:
Така че на машина с DPI, настроена на 120, коефициентите ще бъдат 1,25.
Ето новия помощник:
/// <summary>
/// Transforms device independent units (1/96 of an inch)
/// to pixels
/// </summary>
/// <param name="visual">a visual object</param>
/// <param name="unitX">a device independent unit value X</param>
/// <param name="unitY">a device independent unit value Y</param>
/// <param name="pixelX">returns the X value in pixels</param>
/// <param name="pixelY">returns the Y value in pixels</param>
public void TransformToPixels(Visual visual,
double unitX,
double unitY,
out int pixelX,
out int pixelY)
{
Matrix matrix;
var source = PresentationSource.FromVisual(visual);
if (source != null)
{
matrix = source.CompositionTarget.TransformToDevice;
}
else
{
using (var src = new HwndSource(new HwndSourceParameters()))
{
matrix = src.CompositionTarget.TransformToDevice;
}
}
pixelX = (int)(matrix.M11 * unitX);
pixelY = (int)(matrix.M22 * unitY);
}
Трябваше да добавя още един параметър към метода, Visual
. Нужен ни е като база за изчисления (предишните проби използваха контекста на устройството на целия екран за това). Не мисля, че това е голям проблем, тъй като е по-вероятно да имате Visual
под ръка, когато изпълнявате вашето WPF приложение (в противен случай защо ще трябва да превеждате координатите на пикселите?). Въпреки това, ако вашето визуално изображение не е прикачено към източник на презентация (т.е. все още не е показано), вие не можете да получите източника на презентация (по този начин имаме проверка за NULL и конструираме нов HwndSource
).
Справка
person
Behzad
schedule
08.06.2015