VB6应用程序如何确定它是否在Windows10上运行



我希望我的VB6应用程序检测并显示在.上运行的Windows版本

我已经从另一个堆栈溢出问题中尝试过此代码,但它对我不起作用。它在旧版本的Windows(如Windows XP和Vista)上显示正确的版本号,但它无法检测到Windows 10.出于某种原因,它说Windows;10是Windows;8.

我以为Windows;10将有一个主要版本的"10"和一个次要版本的"0",这张Windows版本号图表证实了这一点。那么,为什么GetVersionEx函数实际上从未返回10.0版本呢?

如何准确区分Windows;8、Windows;8.1和Windows;10

为什么旧代码坏了

另一个答案中的代码适用于旧版本的Windows。具体地说,它可以一直处理到Windows;8(版本6.2)。但正如您所注意到的,Windows上的问题开始出现;8.1(版本6.3)和Windows;10(版本10.0)。代码看起来应该可以工作,但它正在获得版本;6.2对于Windows之后的任何版本;8.

原因是微软决定改变Windows向应用程序报告其版本号的方式。为了防止旧程序错误地决定不在这些最新版本的Windows上运行,该操作系统的版本号已"达到峰值",为6.2。而Windows;8.1和10的内部版本号分别为6.3和10.0,它们继续向较旧的应用程序报告其版本号为6.2。这个想法本质上是"你无法处理真相",所以它将被扣留。在引擎盖下,应用程序和系统之间有兼容性垫片,每当您调用这些API函数时,它们都会伪造版本号。

这些特定的兼容性垫片最初是在Windows中引入的;8.1,并影响了几个版本信息检索API。在Windows中;10,兼容性垫片开始影响几乎所有检索版本号的方式,包括直接从系统文件读取版本号的尝试。

事实上,这些旧版本的信息检索API(就像另一个答案使用的GetVersionEx函数)已经被微软正式"弃用"了。在新代码中,您应该使用VersionHelper函数来确定Windows的底层版本。但是这些功能有两个问题:

  1. 它们有一大堆——其中一个可以检测Windows的每个版本,包括"点"版本——并且它们不会从任何系统DLL导出。相反,它们是在随Windows一起分发的C/C++头文件中定义的内联函数;SDK。这对C和C++程序员来说很好,但一个不起眼的VB 6程序员该怎么办?您不能从VB调用任何这些"助手"函数;6.

  2. 即使可以从VB调用它们;6、Windows;10扩展了兼容性垫片的范围(如上所述),因此即使是IsWindows8Point1OrGreaterIsWindows10OrGreater功能也将由您决定。

兼容性清单

理想的解决方案,以及链接的SDK文档所暗示的解决方案,是在应用程序的EXE中嵌入一个包含兼容性信息的清单。清单文件最初是在Windows中引入的;XP作为一种将元数据与应用程序捆绑在一起的方式,并且可以包含在清单文件中的信息量随着每个新版本的Windows而增加。

清单文件的相关部分是一个名为compatibility的部分。它可能看起来像这样(清单只是一个遵循特定格式的XML文件):

<!-- Declare support for various versions of Windows -->
<ms_compatibility:compatibility xmlns:ms_compatibility="urn:schemas-microsoft-com:compatibility.v1" xmlns="urn:schemas-microsoft-com:compatibility.v1">
<ms_compatibility:application>
<!-- Windows Vista/Server 2008 -->
<ms_compatibility:supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7/Server 2008 R2 -->
<ms_compatibility:supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8/Server 2012 -->
<ms_compatibility:supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1/Server 2012 R2 -->
<ms_compatibility:supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<ms_compatibility:supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</ms_compatibility:application>
</ms_compatibility:compatibility>

它的工作方式是,每个版本的Windows(自Vista以来)都有一个GUID,如果您的清单中包含该GUID作为supportedOS,则系统知道您在该版本发布后编写了应用程序。因此,假设您已准备好应对其中断变化和新功能,因此兼容性垫片不会应用于您的应用程序。当然,包括原始代码使用的GetVersionEx函数。

如果你是一个认真负责的Windows开发人员,你很可能已经在VB中嵌入了一个清单;6应用程序。您需要一个清单来获取主题控件(通过明确选择ComCtl32.dll的版本6),防止UAC虚拟化(通过仅请求asInvoker权限),甚至可能防止DPI虚拟化(将自己标记为高DPI意识)。您可以在网上找到许多关于应用程序清单中这些设置和其他设置如何工作的信息。

如果您已经在应用程序中嵌入了清单文件,那么只需添加Windows;8.1和Windows;现有清单的10个GUID。这将突破操作系统版本的谎言。

如果您还没有嵌入清单文件,那么您还有一些工作要做。VB;6是在清单构思之前几年发布的,因此,IDE没有任何内置的工具来处理它们。你必须自己处理。有关在VB中嵌入清单文件的提示,请参阅此处;6.长短之处在于它们只是文本文件,因此您可以在记事本中创建一个,并使用mt.exe(Windows SDK的一部分)将其嵌入到EXE中。自动化这个过程有多种可能性,也可以在完成构建后手动进行。

另一种解决方案

如果你不想对清单大惊小怪,还有另一个解决方案。它只涉及向VB中添加代码;6项目,不需要任何类型的清单即可工作。

还有另一个鲜为人知的API函数,您可以调用它来检索true操作系统版本。实际上,GetVersionExVerifyVersionInfo函数调用的是内部内核模式函数。但当您直接调用它时,您可以避免通常应用的兼容性垫片,这意味着您可以获得真实的、未经过滤的版本信息。

这个函数被称为RtlGetVersion,正如链接的文档所示,它是一个运行时例程,供驱动程序使用。但多亏了VB的魔力;6动态调用本机API函数的能力,我们可以从应用程序中使用它。以下模块显示了如何使用它:

'==================================================================================
' RealWinVer.bas     by Cody Gray, 2016
' 
' (Freely available for use and modification, provided that credit is given to the
' original author. Including a comment in the code with my name and/or a link to
' this Stack Overflow answer is sufficient.)
'==================================================================================
Option Explicit
''''''''''''''''''''''''''''''''''''''''''''''''''
' Windows SDK Constants, Types, & Functions
''''''''''''''''''''''''''''''''''''''''''''''''''
Private Const cbCSDVersion As Long = 128 * 2
Private Const STATUS_SUCCESS As Long = 0
Private Const VER_PLATFORM_WIN32s As Long        = 0
Private Const VER_PLATFORM_WIN32_WINDOWS As Long = 1
Private Const VER_PLATFORM_WIN32_NT As Long      = 2
Private Const VER_NT_WORKSTATION As Byte       = 1
Private Const VER_NT_DOMAIN_CONTROLLER As Byte = 2
Private Const VER_NT_SERVER As Byte            = 3
Private Const VER_SUITE_PERSONAL As Integer = &H200
Private Type RTL_OSVERSIONINFOEXW
dwOSVersionInfoSize As Long
dwMajorVersion      As Long
dwMinorVersion      As Long
dwBuildNumber       As Long
dwPlatformId        As Long
szCSDVersion        As String * cbCSDVersion
wServicePackMajor   As Integer
wServicePackMinor   As Integer
wSuiteMask          As Integer
wProductType        As Byte
wReserved           As Byte
End Type
Private Declare Function RtlGetVersion Lib "ntdll" _
(lpVersionInformation As RTL_OSVERSIONINFOEXW) As Long

''''''''''''''''''''''''''''''''''''''''''''''''''
' Internal Helper Functions
''''''''''''''''''''''''''''''''''''''''''''''''''
Private Function IsWinServerVersion(ByRef ver As RTL_OSVERSIONINFOEXW) As Boolean
' There are three documented values for "wProductType".
' Two of the values mean that the OS is a server versions,
' while the other value signifies a home/workstation version.
Debug.Assert ver.wProductType = VER_NT_WORKSTATION Or _
ver.wProductType = VER_NT_DOMAIN_CONTROLLER Or _
ver.wProductType = VER_NT_SERVER
IsWinServerVersion = (ver.wProductType <> VER_NT_WORKSTATION)
End Function
Private Function GetWinVerNumber(ByRef ver As RTL_OSVERSIONINFOEXW) As String
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
GetWinVerNumber = ver.dwMajorVersion & "." & _
ver.dwMinorVersion & "." & _
ver.dwBuildNumber
End Function
Private Function GetWinSPVerNumber(ByRef ver As RTL_OSVERSIONINFOEXW) As String
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
If (ver.wServicePackMajor > 0) Then
If (ver.wServicePackMinor > 0) Then
GetWinSPVerNumber = "SP" & CStr(ver.wServicePackMajor) & "." & CStr(ver.wServicePackMinor)
Exit Function
Else
GetWinSPVerNumber = "SP" & CStr(ver.wServicePackMajor)
Exit Function
End If
End If
End Function
Private Function GetWinVerName(ByRef ver As RTL_OSVERSIONINFOEXW) As String
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
Select Case ver.dwMajorVersion
Case 3
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows NT 3.5 Server"
Exit Function
Else
GetWinVerName = "Windows NT 3.5 Workstation"
Exit Function
End If
Case 4
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows NT 4.0 Server"
Exit Function
Else
GetWinVerName = "Windows NT 4.0 Workstation"
Exit Function
End If
Case 5
Select Case ver.dwMinorVersion
Case 0
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows 2000 Server"
Exit Function
Else
GetWinVerName = "Windows 2000 Workstation"
Exit Function
End If
Case 1
If (ver.wSuiteMask And VER_SUITE_PERSONAL) Then
GetWinVerName = "Windows XP Home Edition"
Exit Function
Else
GetWinVerName = "Windows XP Professional"
Exit Function
End If
Case 2
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2003"
Exit Function
Else
GetWinVerName = "Windows XP 64-bit Edition"
Exit Function
End If
Case Else
Debug.Assert False
End Select
Case 6
Select Case ver.dwMinorVersion
Case 0
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2008"
Exit Function
Else
GetWinVerName = "Windows Vista"
Exit Function
End If
Case 1
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2008 R2"
Exit Function
Else
GetWinVerName = "Windows 7"
Exit Function
End If
Case 2
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2012"
Exit Function
Else
GetWinVerName = "Windows 8"
Exit Function
End If
Case 3
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2012 R2"
Exit Function
Else
GetWinVerName = "Windows 8.1"
Exit Function
End If
Case Else
Debug.Assert False
End Select
Case 10
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2016"
Exit Function
Else
GetWinVerName = "Windows 10"
Exit Function
End If
Case Else
Debug.Assert False
End Select
GetWinVerName = "Unrecognized Version"
End Function

''''''''''''''''''''''''''''''''''''''''''''''''''
' Public Functions
''''''''''''''''''''''''''''''''''''''''''''''''''
' Returns a string that contains the name of the underlying version of Windows,
' the major version of the most recently installed service pack, and the actual
' version number (in "Major.Minor.Build" format).
'
' For example: "Windows Server 2003 SP2 (v5.2.3790)" or
'              "Windows 10 (v10.0.14342)"
'
' This function returns the *real* Windows version, and works correctly on all
' operating systems, including Windows 10, regardless of whether or not the
' application includes a manifest. It calls the native NT version-info function
' directly in order to bypass compatibility shims that would otherwise lie to
' you about the real version number.
Public Function GetActualWindowsVersion() As String
Dim ver As RTL_OSVERSIONINFOEXW
ver.dwOSVersionInfoSize = Len(ver)
If (RtlGetVersion(ver) <> STATUS_SUCCESS) Then
GetActualWindowsVersion = "Failed to retrieve Windows version"
End If
' The following version-parsing logic assumes that the operating system
' is some version of Windows NT. This assumption will be true if you
' are running any version of Windows released in the past 15 years,
' including several that were released before that.
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
GetActualWindowsVersion = GetWinVerName(ver) & " " & GetWinSPVerNumber(ver) & _
" (v" & GetWinVerNumber(ver) & ")"
End Function

预期的公共接口是一个名为GetActualWindowsVersion的函数,它返回一个字符串,其中包含实际底层Windows版本的名称。例如,它可能返回"WindowsServer2003SP2(v.2.3790)">"Windows10nbsp;(v10.0.14342)">
(经过充分测试并在Windows 10上工作!)

该模块的公共函数调用了两个内部助手函数,这些函数从本机RTL_OSVERSIONINFOEXW数据结构中解析信息,从而稍微简化了代码。如果您想花时间修改代码以提取代码,则此结构中还有更多可用信息。例如,有一个wSuiteMask成员包含标志,其存在表示某些功能或产品类型。如何使用此信息的示例出现在GetWinVerName帮助程序函数中,其中会检查VER_SUITE_PERSONAL标志以查看它是否为Windows XP Home或Pro。

最后的想法

对于这个问题,网上流传着其他几种"解决方案"。我建议避免这些。

一个流行的建议是尝试从注册表中读取版本号。这是个糟糕的主意。注册表既不是程序的公共接口,也不是程序的文件接口。这意味着这样的代码依赖于随时可能更改的实现细节,使您再次陷入崩溃的境地——这正是我们最初试图解决的问题!与调用有文档的API函数相比,查询注册表从来没有优势。

另一个经常建议的选项是使用WMI来检索操作系统版本信息。这是一个比注册表更好的想法,因为它实际上是一个文档化的公共接口,但它仍然不是一个理想的解决方案。首先,WMI是一个非常严重的依赖项。并非所有系统都会运行WMI,因此您需要确保它已启用,否则您的代码将无法工作。如果这是你唯一需要使用WMI的事情,它将非常慢,因为你必须等待WMI首先启动并运行。此外,从VB以编程方式查询WMI;6是困难的。我们没有那些PowerShell人员那么容易!然而,如果您无论如何都在使用WMI,那么它将是一种获取人类可读的操作系统版本字符串的方便方法。您可以通过查询Win32_OperatingSystem.Name来完成此操作。

我甚至见过其他黑客,比如从进程的PEB块中读取版本!当然,这是针对Delphi的,而不是VB;6,并且由于在VB中没有内联程序集;6,我甚至不确定你是否能想出一个VB;6当量。但即使在Delphi中,这也是一个非常糟糕的想法,因为它也依赖于实现细节。只是…不要。

作为上述GetVersionEx清单解决方案的附件,将以下放在Cody代码中osv.dwVerMajor的case 6块之后:

Case 10 'Note: The following works only with updated manifest
Select Case osv.dwVerMinor
Case 0
GetWindowsVersion = "Windows 10/Server 2016"
Case Else
End Select

MSDN中的一句话:"GetVersionEx可能会被更改或不适用于Windows 8.1之后的版本。"。

添加到Cody的答案中:记住,如果从VB 6 IDE运行,它将报告您选择的兼容性,以使VB 6运行,例如,在Windows 11上从IDE运行,报告:

Windows XP Home Edition SP2(V5.1.2600)

如果我在同一台Windows 11机器上编译并运行可执行文件,它会报告:

Windows 10(v10.0.2200)