如何在.NET c#中使用Win32 GetMonitorInfo()?

8

我需要实现一个功能,保存窗口的最后位置。当应用程序启动时,需要获取并恢复该位置。

现在可能会拆下第二个监视器。如果最后位置在一个不可见的监视器上(换句话说,保存的坐标超出了可见坐标),应该捕获此情况,并将坐标设置为默认值而非最后位置。

为了检索关于监视器的信息,我需要使用Win32。对我来说让它工作起来并不容易。

我创建了一个助手类:

public static class DisplayHelper
    {
        private const int MONITOR_DEFAULTTONEAREST = 2;

        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern int GetSystemMetrics(int nIndex);

        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern UInt32 MonitorFromPoint(Point pt, UInt32 dwFlags);

        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        private static extern bool GetMonitorInfo(UInt32 monitorHandle, ref MonitorInfo mInfo);


        public static void GetMonitorInfoNow(MonitorInfo mi, Point pt)
        {
            UInt32 mh = MonitorFromPoint(pt, 0);
            mi.cbSize = (UInt32)System.Runtime.InteropServices.Marshal.SizeOf(typeof(MonitorInfo));
            mi.dwFlags = 0;
            bool result = GetMonitorInfo(mh, ref mi);

        }
    }

这是我创建MonitorInfo和Rect类的尝试:

[StructLayout(LayoutKind.Sequential)]
    public class MonitorInfo
    {
        public UInt32 cbSize;
        public Rectangle2 rcMonitor;
        public Rectangle2 rcWork;
        public UInt32 dwFlags;

        public MonitorInfo()
        {
            rcMonitor = new Rectangle2();
            rcWork = new Rectangle2();

            cbSize = (UInt32)System.Runtime.InteropServices.Marshal.SizeOf(typeof(MonitorInfo));
            dwFlags = 0;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    public class Rectangle2
    {
        public UInt64 left;
        public UInt64 top;
        public UInt64 right;
        public UInt64 bottom;

        public Rectangle2()
        {
            left = 0;
            top = 0;
            right = 0;
            bottom = 0;
        }
    }

我正在使用以下代码来获取可见的监视器:

//80 means it counts only visible display monitors.
int lcdNr = DisplayHelper.GetSystemMetrics(80);
var point = new System.Drawing.Point((int) workSpaceWindow.Left, (int) workSpaceWindow.Top);
MonitorInfo monitorInfo = new MonitorInfo();
DisplayHelper.GetMonitorInfoNow(monitorInfo, point);

最后一种方法在尝试执行时会抛出异常。
bool result = GetMonitorInfo(mh, ref mi);

有什么建议可以解决这个问题吗?
4个回答

14
与其调用本地API,你应该使用System.Windows.Forms.Screen。它应该拥有你所需的一切,并且更易于使用。 Screen.FromPoint 是你的GetMonitorInfoNow函数带有MONITOR_DEFAULTTONEAREST选项的托管等效项。我刚刚注意到你没有使用该选项,因此你可能需要自己编写代码或使用正确的P/Invoke签名。
如果你只是引用System.DrawingSystem.Windows.Forms,编写自己的函数应该非常简单。这两个都可以工作:
static Screen ScreenFromPoint1(Point p)
{
    System.Drawing.Point pt = new System.Drawing.Point((int)p.X, (int)p.Y);
    return Screen.AllScreens
                    .Where(scr => scr.Bounds.Contains(pt))
                    .FirstOrDefault();
}

static Screen ScreenFromPoint2(Point p)
{
    System.Drawing.Point pt = new System.Drawing.Point((int)p.X, (int)p.Y);
    var scr = Screen.FromPoint(pt);
    return scr.Bounds.Contains(pt) ? scr : null;
}

如果你更喜欢自己调用Win32,那么你需要调用的函数的适当P/Invoke签名(即从反编译.Net DLL中获取的内容)是:

    [DllImport("User32.dll", CharSet=CharSet.Auto)] 
    public static extern bool GetMonitorInfo(HandleRef hmonitor, [In, Out]MONITORINFOEX info);
    [DllImport("User32.dll", ExactSpelling=true)]
    public static extern IntPtr MonitorFromPoint(POINTSTRUCT pt, int flags);

    [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Auto, Pack=4)]
    public class MONITORINFOEX { 
        public int     cbSize = Marshal.SizeOf(typeof(MONITORINFOEX));
        public RECT    rcMonitor = new RECT(); 
        public RECT    rcWork = new RECT(); 
        public int     dwFlags = 0;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst=32)] 
        public char[]  szDevice = new char[32];
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct POINTSTRUCT { 
        public int x;
        public int y;
        public POINTSTRUCT(int x, int y) {
          this.x = x; 
          this.y = y;
        } 
    } 

    [StructLayout(LayoutKind.Sequential)] 
    public struct RECT {
        public int left; 
        public int top; 
        public int right;
        public int bottom; 
    }

谢谢Gabe,我忘了提到我的应用程序是用WPF编写的。理论上,我将无法访问System.Windows.Forms.Screen,除非它也适用于WPF? - Houman
@Kave:只需添加对System.Windows.Forms.dll的引用,就可以从WPF中使用它。 - Gabe
2
@Gabe - 这样做可能更容易,但我不确定我会为了一个类/方法加载一个5MB的程序集,即使有任何优化。更不用说它的依赖关系了;-) - CodeNaked
谢谢Gabe,是的,我现在明白了。但是我不明白这应该如何工作。屏幕测试 = Screen.FromPoint(new Point(2561, 400)); 根据我的最大X值为2560,这实际上不应该给我一个屏幕对象。然而,我得到了一个X坐标为1280,Y坐标为0,宽度为1280,高度为1024的屏幕对象。我没有看到任何迹象表明我的点坐标会超出屏幕范围。有什么想法吗?谢谢 - Houman
1
@Kave:我明白了,你没有传递“MONITOR_DEFAULTTONEAREST”选项。请参考我的编辑以解决这个问题。 - Gabe
它应该拥有你所需的一切,但它没有相对于自身的屏幕位置。 - user1529413

2
我发现一个不同之处是
public static extern bool GetMonitorInfo(IntPtr hMonitor, [In,Out] MONITORINFO lpmi)

public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi)

在我的情况下,ref关键字使函数始终返回false。
但是如果删除此关键字或使用[In,Out],它可以正常工作。

关于ref和[In,Out]的更多信息,请参见这里

"Original Answer"翻译成"最初的回答"

如果你有一个类而不是结构体,那么你需要使用 [Out]。 - Konstantin S.

1

你的Rectangle2应该使用Int32或者只是int,而不是Int64。更多信息可以在这里找到。

此外,它需要是一个结构体,而不是一个类。对于你的MonitorInfo类也是如此(它应该是一个结构体)。我建议尝试上面链接中的版本,或者将它们与你的版本进行比较。


1

由于您使用的是MonitorInfo类而不是结构体,因此必须指定[Out]属性而不使用ref,以便marshaler可以正确更新您的类。

[DllImport("user32", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool GetMonitorInfo(
    IntPtr hmonitor, 
    [In, Out] MonitorInfo info);

您也可以使用结构体和ref,代码如下:

[DllImport("user32", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool GetMonitorInfo(
    IntPtr hmonitor, 
    ref MonitorInfoEx info);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)]
internal struct MonitorInfoEx
{
   public uint cbSize;
   public RECT rcMonitor;
   public RECT rcWork;
   public uint dwFlags;
   [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
   public char[] szDevice;
}

var info = new MonitorInfoEx
{
    cbSize = (uint)Marshal.SizeOf(typeof(MonitorInfoEx)),
};
GetMonitorInfo(hDesktop, ref info);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接