首页 > 解决方案 > System.Windows.Forms.Screen 在显示更改后未更新

问题描述

正如标题所说,System.Windows.Forms.Screen在显示更改后没有正确更新其成员。我会具体:

我有两个显示器,我通过使用ChangeDisplaySettingsEx函数以编程方式交换主显示器。当我交换监视器时,SystemEvents.DisplaySettingsChanged会引发事件,并且我还可以在 Windows 控制面板中看到更改生效(我的意思是启用或禁用使监视器成为主监视器的复选框),但是,在我的应用程序中,System.Windows.Forms.Screen.PrimaryScreen对象保持与交换主监视器之前相同。

我从一个注意到相同的人那里来到这个线程,正如在Screen 类源代码中看到的那样,它应该在拦截WM_DISPLAYCHANGE消息时更新/更新屏幕对象,所以我尝试了这些调用:

Const HWND_BROADCAST As Integer = &HFFFF
Const WM_DISPLAYCHANGE As Integer = &H7e

PostMessage(New IntPtr(HWND_BROADCAST), WM_DISPLAYCHANGE, IntPtr.Zero, IntPtr.Zero)
SendMessage(New IntPtr(HWND_BROADCAST), WM_DISPLAYCHANGE, IntPtr.Zero, IntPtr.Zero)

但它似乎不起作用,或者我没有使用正确的 lParam 和 wParam 调用它,因为System.Windows.Forms.Screen.PrimaryScreen在将消息发送到所有窗口后对象保持不变,并且SystemEvents.DisplaySettingsChanged在这些调用之后引发事件。

在 C# 或 VB.NET 中,在显示更改后,我需要在交换它们之后检测 REAL 主监视器。

因为我Screen在交换监视器而不是使用 Windows API 来枚举监视器时使用该类,所以有什么方法可以解决Screen该类,可能是通过反射或调度WM_DISPLAYCHANGESystem.Windows.Forms.Screen.PrimaryScreen ,以在显示更改后获得一个新的/更新的对象? .

更新

我还在MSDN 论坛上找到了另一个关于这个 ...bug?的证据证明。请注意,从 2012 年开始的线程并没有提供解决方案......


根据评论中的要求,我分享了(部分、相关的)代码。

''' <summary>
''' Sets the specified <see cref="Screen"/> as primary.
''' <para></para>
''' This functionality will work only with a setup of two monitors. 
''' This functionality does not support three or more monitors.
''' </summary>
''' 
''' <param name="scr">
''' The new primary <see cref="Screen"/>.
''' </param>
<DebuggerStepThrough>
Public Shared Sub SetPrimaryScreen(scr As Screen)

    If (Screen.AllScreens.Count = 1) Then
        Throw New InvalidOperationException("There is only one available monitor.")
    End If

    If (Screen.AllScreens.Count > 2) Then
        Throw New NotSupportedException("Three or more monitor setups are not supported.")
    End If

    If scr.Equals(Screen.PrimaryScreen) Then
        Exit Sub
    End If

    '' DIRTY FIX:
    ' If scr.IsRealPrimaryScreen() Then
    '     Exit Sub
    ' End If

    Dim scrPrimary As Screen = (Screen.AllScreens().Where(Function(s As Screen) s.Primary)).Single()

    '' DIRTY FIX:
    ' Dim scrPrimary As Screen = (Screen.AllScreens().Where(Function(s As Screen) s.IsRealPrimaryScreen())).Single()

    ' Set current primary device position.
    Dim setPositionPrimary As New DevMode(initializeSize:=True) With {.Fields = DeviceModeFields.Position}
    setPositionPrimary.Mode.DisplayDevMode.Position.X = scr.Bounds.Width
    setPositionPrimary.Mode.DisplayDevMode.Position.Y = 0
    NativeMethods.ChangeDisplaySettingsEx(scrPrimary.DeviceName, setPositionPrimary, IntPtr.Zero, ChangeDisplaySettingsFlags.UpdateRegistry Or ChangeDisplaySettingsFlags.NoReset, IntPtr.Zero)

    ' Set secondary device position, and set it as the new primary device.
    Dim setPositionSecondary As New DevMode(initializeSize:=True) With {.Fields = DeviceModeFields.Position}
    setPositionSecondary.Mode.DisplayDevMode.Position.X = 0
    setPositionSecondary.Mode.DisplayDevMode.Position.Y = 0
    NativeMethods.ChangeDisplaySettingsEx(scr.DeviceName, setPositionSecondary, IntPtr.Zero, ChangeDisplaySettingsFlags.SetPrimary Or ChangeDisplaySettingsFlags.UpdateRegistry Or ChangeDisplaySettingsFlags.Reset, IntPtr.Zero)

    ' Update current primary device.
    Dim currentPrimary As New DevMode(initializeSize:=True)
    NativeMethods.ChangeDisplaySettingsEx(scrPrimary.DeviceName, currentPrimary, IntPtr.Zero, ChangeDisplaySettingsFlags.UpdateRegistry Or ChangeDisplaySettingsFlags.NoReset, IntPtr.Zero)

    ' Set secondary device as current primary device.
    Dim newPrimary As New DevMode(initializeSize:=True)
    NativeMethods.ChangeDisplaySettingsEx(scr.DeviceName, newPrimary, IntPtr.Zero, ChangeDisplaySettingsFlags.SetPrimary Or ChangeDisplaySettingsFlags.UpdateRegistry Or ChangeDisplaySettingsFlags.NoReset, IntPtr.Zero)

End Sub

交换监视器的使用示例(在两个监视器设置中):

Dim secondaryMonitor As Screen = (Screen.AllScreens().Where(Function(s As Screen) Not s.Primary)).Single()
SetPrimaryScreen(secondaryScreen)

不能在同一个正在运行的应用程序中调用两次,因为Screen.PrimaryScreen在第一次更改后不会更新,所以在随后的方法调用中SetPrimaryScreen,它会处理错误的 false/non-updated Screen.PrimaryScreen,我的意思是因为Screen.PrimaryScreen始终保持指向同一个设备的同一个对象即使不再是主要设备。

请注意,如果尝试将 设置为主屏幕,则该SetPrimaryScreen方法具有终止执行的错误处理(因为它已经应该是主屏幕......但它不是,因为对象未正确刷新)。Screen.PrimaryScreenScreen.PrimaryScreen


请注意,我正在寻找一个简单、自然、最佳的解决方案,但我已经设法通过为具有我将使用的名称而不是默认属性的Screen类创建一个扩展方法,以一种肮脏的方式来解决所有这些问题。IsRealPrimaryScreenScreen.Primary

<HideModuleName>
Public Module ScreenExtensions

    ''' <summary>
    ''' Determine whether the source <see cref="Screen"/> is the primary device.
    ''' <para></para>
    ''' You must always call <see cref="ScreenExtensions.IsRealPrimaryScreen"/> function 
    ''' instead of <see cref="Screen.Primary"/>, 
    ''' which does not properly update the class members after a display change:
    ''' <para></para>
    ''' <see href="https://stackoverflow.com/questions/67061729/system-windows-forms-screen-is-not-updating-after-a-display-change"/>
    ''' <para></para>
    ''' <see href="https://stackoverflow.com/questions/7901247/screen-allscreens-bug-and-posting-a-wm-displaychange-to-a-single-winform-applic"/>
    ''' </summary>
    ''' <example> This is a code example.
    ''' <code language="VB.NET">
    ''' Dim primaryMonitor As Screen = (Screen.AllScreens().Where(Function(scr As Screen) scr.IsRealPrimaryScreen)).Single()
    ''' Dim secondaryMonitor As Screen = (Screen.AllScreens().Where(Function(s As Screen) Not s.IsRealPrimaryScreen)).First()
    ''' Console.WriteLine(primaryMonitor.DeviceName)
    ''' Console.WriteLine(secondaryMonitor.DeviceName)
    ''' </code>
    ''' </example>
    ''' <param name="scr">
    ''' The source <see cref="Screen"/>.
    ''' </param>
    ''' <returns>
    ''' <see langword="True"/> if the source <see cref="Screen"/> is the primary device;
    ''' <see langword="False"/> otherwise.
    ''' </returns>
    <DebuggerStepThrough>
    <Extension>
    <EditorBrowsable(EditorBrowsableState.Always)>
    Public Function IsRealPrimaryScreen(scr As Screen) As Boolean

        Dim result As Boolean

        Dim callback As Delegates.EnumMonitorProc =
        Function(hMonitor As System.IntPtr, hdc As System.IntPtr, ByRef rc As NativeRectangle, data As System.IntPtr) As Boolean

            Dim mi As New MonitorInfoEx(initializeSize:=True)
            NativeMethods.GetMonitorInfo(hMonitor, mi)

            If (mi.DeviceName.Equals(scr.DeviceName, StringComparison.OrdinalIgnoreCase)) Then
                result = mi.Flags.HasFlag(MonitorInfoFlags.PrimaryDevice)
                Return False
            End If

            Return True
        End Function

        NativeMethods.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, callback, IntPtr.Zero)

        Return result

    End Function

End Module

我将使用一种GetRealPrimaryScreen方法来获取Screen主设备的实例,而不是使用Screen.PrimaryScreen

''' <summary>
''' Gets a <see cref="Screen"/> object that represents the primary device.
''' <para></para>
''' You must always call <see cref="DeviceUtil.GetRealPrimaryScreen"/> function 
''' instead of <see cref="Screen.PrimaryScreen"/>, 
''' which does not properly update the class members after a display change:
''' <para></para>
''' <see href="https://stackoverflow.com/questions/67061729/system-windows-forms-screen-is-not-updating-after-a-display-change"/>
''' <para></para>
''' <see href="https://stackoverflow.com/questions/7901247/screen-allscreens-bug-and-posting-a-wm-displaychange-to-a-single-winform-applic"/>
''' </summary>
''' <example> This is a code example.
''' <code language="VB.NET">
''' Dim primary As Screen = GetRealPrimaryScreen()
''' Console.WriteLine(primary.DeviceName)
''' </code>
''' </example>
''' <returns>
''' The resulting <see cref="Screen"/> object.
''' </returns>
<DebuggerStepThrough>
Public Shared Function GetRealPrimaryScreen() As Screen

    Dim result As Screen = Nothing

    Dim callback As Delegates.EnumMonitorProc =
    Function(hMonitor As IntPtr, hdc As IntPtr, ByRef rc As NativeRectangle, data As IntPtr) As Boolean

        Dim mi As New MonitorInfoEx(initializeSize:=True)
        NativeMethods.GetMonitorInfo(hMonitor, mi)

        If mi.Flags.HasFlag(MonitorInfoFlags.PrimaryDevice) Then
            result = DeviceUtil.GetScreenFromDeviceName(mi.DeviceName)
            Return False
        End If

        Return True
    End Function

    NativeMethods.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, callback, IntPtr.Zero)

    Return result

End Function

标签: c#.netvb.netwinapiscreen

解决方案


推荐阅读