Win10 平板模式 & WinForms.Net 窗体事件顺序
Win10 平板模式会改变 WinForms.Net 中窗体事件的顺序 - 本文介绍如何纠正事件顺序
引言
WinForms.Net 在创建和显示窗体时,事件会遵循特定的顺序(参见 此链接)。
Windows 10 引入了一种新的 UI 模式,称为平板电脑模式,最终用户可以启用/禁用它。此新模式会强制更改这些事件的顺序。
本文解释如何将事件顺序纠正回原始顺序。
背景
事件顺序的改变会导致本应按特定顺序发生的代码以错误的顺序执行,从而导致崩溃或意外行为。
事件的原始顺序是
Control.HandleCreated
Control.BindingContextChanged
Form.Load
Control.VisibleChanged
Form.Activated
Form.Shown
TabletMode
会改变顺序如下
Control.HandleCreated
- Form.Activated
- Control.VisibleChanged
Control.BindingContextChanged
Form.Load
Form.Shown
Using the Code
纠正事件顺序需要三个关键步骤
- 在应用程序清单中设置 Windows 10 兼容性(需要才能检查操作系统版本并获取正确的版本)。
- 本文跳过此步骤。网络搜索如何添加应用程序清单以完成此步骤。
- 检测 PC 是否处于
TabletMode
。 - 防止
Activated
&VisibleChanged
事件以错误的顺序触发。
步骤 2:检测 TabletMode
在研究这个问题时,有三种常见的检测平板电脑模式的建议
1. UIViewSettings
UIViewSettings.GetForCurrentView().UserInteractionMode =
Windows.UI.ViewManagement.UserInteractionMode.Touch
优点:代码简单易用
缺点:UWP 代码。本文针对 WinForms.Net,引用 UWP DLL 并不容易。
2. GetSystemMetrics
GetSystemMetrics
是一个可以调用的 Windows API。建议使用的特定属性是 SM_CONVERTIBLESLATEMODE
。即
private const int SM_CONVERTIBLESLATEMODE = 0x2003;
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto, EntryPoint = "GetSystemMetrics")]
private static extern int GetSystemMetrics (int nIndex);
优点:简单。声明函数,并调用 GetSystemMetrics(SM_CONVERTIBLESLTATEMODE)
并将返回值与 0
进行比较。如果为 0
(零),则 Windows 处于平板电脑模式。
缺点:根据 MSDN,在台式 PC 上不起作用。TabletMode
可以在任何只有一个活动监视器的 Windows 10 PC 上启用。我在笔记本电脑上进行了测试,它仍然无法准确报告 PC 是否处于平板电脑模式。
因此此模式不可靠。不太可能在平板电脑模式下使用非平板电脑 PC,但由于 Microsoft 允许这样做,因此您应该保护您的程序免受其影响。
3. 注册表
当 TabletMode
开启/关闭时,Windows 会将一个值写入 HKCU\Software\Microsoft\Windows\CurrentVersion\ImmersiveShell\TabletMode。
private bool GetTabletMode()
{
return (bool)Microsoft.Win32.Registry.GetValue
("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\ImmersiveShell", "TabletMode", 0);
}
Private Function GetTabletMode() As Boolean
Return CBool(Microsoft.Win32.Registry.GetValue
("HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\ImmersiveShell", "TabletMode", 0))
End Function
优点:它位于注册表的 HKCU 区域,因此权限不应成为问题。检索注册表值很容易。TabletMode
的检测是可靠的。
缺点:无(?)
3. 防止 Activated & VisibleChanged
为了防止这两个事件以错误的顺序触发,我们需要重写 OnActivated
和 SetVisibleCore
函数,并防止基函数以错误的顺序被调用。
我发现最有效的逻辑是在 SetVisibleCore
中检测我们是否处于 TabletMode
- 然后按正确的顺序调用相应的事件处理程序。一个非常基本的 Form1
示例如下
using System;
using System.Windows.Forms;
namespace TabletModeFormEventOrderExample
{
public partial class Form1 : Form
{
private bool _loadedEventFired = false;
private bool _onActivatedAllowed = false;
public Form1()
{
InitializeComponent();
this.Load += Form1_Load;
}
private void Form1_Load(object sender, EventArgs e)
{
if (_loadedEventFired) return; // Prevent event from firing twice.
//Do whatever needs to be done in Load event.
_loadedEventFired = true;
}
protected override void OnActivated(EventArgs e)
{
if (GetTabletMode() && !_onActivatedAllowed) return;
base.OnActivated(e);
}
protected override void SetVisibleCore(bool value)
{
// We only need special logic if:
// a. Visible is being set to true and the Loaded event hasn't fired yet.
// b. The OS is Windows 10 or later.
// c. TabletMode is turned on.
if (value && !_loadedEventFired)
{
// Version.Major = 6 unless you set Windows 10 support in application manifest.
if ((Environment.OSVersion.Platform == PlatformID.Win32NT) &&
(Environment.OSVersion.Version.Major >= 10))
{
// Get TabletMode from registry.
if (GetTabletMode())
{
// Handle should already be created, but ensure it is.
if (this.Handle == IntPtr.Zero) this.CreateHandle();
// Ensure BindingContext is properly set and
// fire the BindingContextChanged event.
// This example doesn't include setting a BindingContext.
// Manually fire the load event.
base.OnLoad(EventArgs.Empty);
// Now set the visible core.
base.SetVisibleCore(value);
// Now allow OnActivated to fire.
_onActivatedAllowed = true;
// Calling this.Activate doesn't seem to work
// once we've hijacked the chain of events back from Tablet mode.
// Instead call OnActivated to force it to fire.
if (this.ShowWithoutActivation) OnActivated(EventArgs.Empty);
return;
}
}
}
base.SetVisibleCore(value);
}
private bool GetTabletMode()
{
return (bool)Microsoft.Win32.Registry.GetValue
("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\ImmersiveShell",
"TabletMode", 0);
}
}
}
Imports System
Imports System.Windows.Forms
Namespace TabletModeFormEventOrderExample
Public Partial Class Form1
Inherits Form
Private _loadedEventFired As Boolean = False
Private _onActivatedAllowed As Boolean = False
Public Sub New()
InitializeComponent()
Me.Load += AddressOf Form1_Load
End Sub
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs)
'Prevent event from firing twice.
If _loadedEventFired Then Exit Sub
'Do whatever needs to be done in Load event.
_loadedEventFired = True
End Sub
Protected Overrides Sub OnActivated(ByVal e As EventArgs)
If GetTabletMode() AndAlso Not _onActivatedAllowed Then Exit Sub
MyBase.OnActivated(e)
End Sub
Protected Overrides Sub SetVisibleCore(ByVal value As Boolean)
'We only need special logic if:
' a. Visible is being set to true and the Loaded event hasn't fired yet.
' b. The OS is Windows 10 or later.
' c. TabletMode is turned on.
If value AndAlso Not _loadedEventFired Then
'Version.Major = 6 unless you set Windows 10 support in application manifest.
If (Environment.OSVersion.Platform = PlatformID.Win32NT) _
AndAlso (Environment.OSVersion.Version.Major >= 10) Then
'Get TabletMode from registry.
If GetTabletMode() Then
'Handle should already be created, but ensure it is.
If Me.Handle = IntPtr.Zero Then Me.CreateHandle()
'Ensure BindingContext is properly set and
'fire the BindingContextChanged event.
'This example doesn't include setting a BindingContext.
'Manually fire the load event.
MyBase.OnLoad(EventArgs.Empty)
'Now set the visible core.
MyBase.SetVisibleCore(value)
'Now allow OnActivated to fire.
_onActivatedAllowed = True
'Calling Me.Activate doesn't seem to work
'once we have hijacked the chain of events back from Tablet mode.
'Instead call OnActivated to force it to fire.
If Me.ShowWithoutActivation Then OnActivated(EventArgs.Empty)
Exit Sub
End If
End If
End If
MyBase.SetVisibleCore(value)
End Sub
Private Function GetTabletMode() As Boolean
Return CBool(Microsoft.Win32.Registry.GetValue_
("HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\ImmersiveShell", _
"TabletMode", 0))
End Function
End Class
End Namespace
历史
- 2018年7月27日:初稿