一个VB 6应用程序如何确定它是否在运行Windows 10操作系统?

7
我希望我的VB 6应用程序能够检测并显示正在运行的Windows版本。
我尝试了这段代码,来自另一个Stack Overflow问题,但它对我无效。它在旧版本的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?

2
您可能还想添加一条注释,说明如今使用VB6是一个不好的选择 :) - Jonathan Potter
1
@Jonathan,是什么让它成为一个不好的想法呢?我个人不使用它,已经有好几年了,但我的VB 6答案却引起了意外的关注,所以它比你想象的更受欢迎。它肯定失去了它的光泽和吸引力,但我认为这并不意味着使用它是一个坏主意。这似乎有点言过其实。 - Cody Gray
1
@CodyGray XP 仍然很受欢迎,但这并不意味着人们应该使用它。 - Jonathan Potter
3个回答

27

为什么旧代码会出现问题?

那个答案中的代码在旧版Windows上运行良好,特别是在Windows 8(版本6.2)之前的版本中完美运行。但是,正如您所注意到的,它在Windows 8.1(版本6.3)和Windows 10(版本10.0)上开始出现问题。尽管代码看起来应该可以工作,但在Windows 8之后的任何版本中都会获得版本6.2。

由于微软决定改变Windows向应用程序报告其版本号的方式,所以出现了这种情况。为了防止旧程序错误地决定不在这些最新版本的Windows上运行,操作系统已将其版本号“峰值化”为6.2。虽然Windows 8.1和10仍具有分别为6.3和10.0的内部版本号,但它们继续向旧应用程序报告其版本号为6.2。本质上,这个想法是“你无法处理真相”,因此它将被隐瞒。在幕后,你的应用程序和系统之间存在兼容性shim,每当你调用这些API函数时,它们会负责伪造版本号。
这些特定的兼容性shim首次引入于Windows 8.1,并影响了几个版本信息检索API。在Windows 10中,兼容性shim开始影响几乎所有检索版本号的方式,包括尝试直接从系统文件读取版本号。
事实上,微软已正式“弃用”这些旧版信息检索API(例如其他答案中使用的GetVersionEx函数)。在新代码中,应该使用版本助手函数来确定Windows的底层版本。但是这些函数存在两个问题:
  1. 有很多这样的函数-用于检测每个版本的Windows,包括“点”版本-并且它们没有从任何系统DLL中导出。相反,它们是内联函数,定义在分发给Windows SDK的C/C++头文件中。这对于C和C++程序员非常有效,但一个谦虚的VB 6程序员该怎么办?您无法从VB 6调用任何这些“助手”函数。
  2. 即使您可以从VB 6调用它们,Windows 10也扩展了兼容性shim的范围(如我上面提到的),因此即使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,则系统知道您编写的应用程序是在该版本发布后编写的。因此,假定您已准备好处理其破坏性更改和新功能,因此不会向您的应用程序应用兼容性 shims,当然包括 原始代码 中使用的 GetVersionEx 函数。
如果您是一位认真的 Windows 开发人员,那么很有可能您已经将清单嵌入到您的 VB 6 应用程序中。您需要一个清单来获取主题控件(通过明确选择 ComCtl32.dll 的 version 6),通过请求仅具有 asInvoker 权限来防止 UAC 虚拟化,甚至可能通过标记自己为高 DPI 感知来防止 DPI 虚拟化。您可以在网上找到很多关于应用程序清单中这些和其他设置如何工作的信息

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

如果您尚未嵌入清单文件,则需要一些工作。VB 6发布于清单的概念出现几年前,因此IDE没有任何内置的处理它们的功能。您需要自己处理它们。请参见这里有关在VB 6中嵌入清单文件的提示。长话短说,它们只是文本文件,因此您可以在记事本中创建一个,并使用mt.exeWindows 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版本的名称。例如,它可能返回"Windows Server 2003 SP2 (v5.2.3790)""Windows 10 (v10.0.14342)"
(已在Windows 10上进行全面测试并工作!)

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

最后的想法

在网上有几种针对这个问题的“解决方案”。我建议避免使用这些方法。

其中一种流行的建议是从注册表中读取版本号。这是一个非常糟糕的想法。注册表既不是为程序设计的公共接口,也没有记录为此目的而编写的文档。这意味着,这样的代码依赖于可能随时更改的实现细节,让你再次面临破坏性的情况——这正是我们试图解决的问题!与调用记录在文档中的API函数相比,查询注册表从来没有优势。

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

我甚至见过其他的黑客技巧,比如从进程的PEB块中读取版本!当然,那是针对Delphi而言,不是VB 6,由于在VB 6中没有内联汇编,我甚至不确定你是否能够想出VB 6的等效方法。但即使在Delphi中,这也是一个非常糟糕的想法,因为它也依赖于实现细节。所以...不要这样做。


2
微软建议阅读系统 DLL 的产品版本,例如 kernel32.dll。当您想要操作系统名称的文本表示时,WMI 是非常有用的。而且它不受 shims 的影响。 - David Heffernan

0

补充Cody的答案:记住,如果从VB 6 IDE运行,它会报告你选择的兼容性,例如,在Windows 11上从IDE运行VB 6,它报告:

Windows XP Home Edition SP2 (V5.1.2600)

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

Windows 10 (v10.0.2200)


0
作为GetVersionEx的上述显式解决方案的附加,将以下代码放置在Cody的code中第6个方块osv.dwVerMajor之后:
    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之后的版本中可能会被修改或不再可用",这是需要注意的事情。

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