我有一个在WPF设计的窗口,并且在WinForms所有者的中心使用了该窗口。现在,我要移动所有者表单,此刻WPF窗口也必须移到表单的中央!
但是我有一个问题,只有当窗口位于屏幕中央的窗体的中央时。否则,其行为方式将与Windows坐标不同。我只是将表单的位移值添加到窗口位置。
现在我得出的结论是WinForms在WPF Windows上的像素坐标不同!
如何将WPF窗口位置转换为WinForms基本位置,反之亦然?
所有者表格代码为:
public partial class Form1 : Form
{
private WPF_Window.WPF win;
public Form1()
{
InitializeComponent();
win = new WPF();
win.Show();
CenterToParent(win);
}
private void CenterToParent(System.Windows.Window win)
{
win.Left = this.Left + (this.Width - win.Width) / 2;
win.Top = this.Top + (this.Height - win.Height) / 2;
}
protected override void OnMove(EventArgs e)
{
base.OnMove(e);
CenterToParent(win);
}
}
与您在Windows窗体中执行的方法相同。 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程序集的引用。大不了不确定您的身份,但是对我来说,这是一个需要避免的问题。
让我们更进一步,以Win API的方式来做。 GetDeviceCaps
函数可检索指定设备的各种信息,并在分别传递LOGPIXELSX
和LOGPIXELSY
参数时能够检索水平和垂直DPI。
[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
也无法获得设备上下文,则会抛出相同的异常。
正如正式记录的here一样,注册表中有一个特殊键:HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI.
它存储一个DWORD值,该值正是用户在显示设置对话框中为DPI选择的值(在此处称为字体大小) )。
阅读本书很容易,但我不建议这样做。您会看到官方API和各种设置的存储之间存在差异。该API是一项公共合同,即使内部逻辑已被完全重写,它也保持不变(如果不是整个平台都糟透了,不是吗?)。
但是没有人保证内部存储会保持不变。它可能已经使用了几十年,但是描述其重新布置的重要设计文档可能已经在等待批准。你永远不会知道。
始终坚持使用API(无论它是什么,本机,Windows窗体,WPF等)。即使基础代码从您知道的位置读取了值。
这是一种非常优雅的WPF方法,我发现它记录在this blog post中。它基于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
。我们需要它作为计算的基础(以前的示例为此使用了整个屏幕的设备上下文)。我认为这不是一个大问题,因为在运行WPF应用程序时,您很可能会遇到Visual
(否则,为什么需要平移像素坐标?)。但是,如果您的视觉尚未附加到演示源(即尚未显示),则无法获取演示源(因此,我们将检查NULL并构造一个新的HwndSource
)。
我刚刚发现并测试了这个(在VB中:)>
formVal = Me.LogicalToDeviceUnits(WPFval)
formVal和WPFval可以是整数或大小。