首页 > 解决方案 > 无法读取“GetProcessPreferredUILanguages”函数返回的所有语言名称

问题描述

在我使用SetProcessPreferredUILanguages设置最多 5 种首选语言并确保它有效之后,因为pulNumLanguages在调用完成后与我的自定义语言名称分隔字符串的长度相同。

然后现在我正在尝试通过GetProcessPreferredUILanguages函数获取所有进程首选的 UI 语言。问题是我只能读取返回的字符串缓冲区中的一个(第一个)语言名称,但返回pulNumLanguages指定的 5 种语言......

所以,我会要求正确的方法来读取返回的字符串。

pwszLanguagesBuffer请注意有关参数的文档中所说的内容:

指向双空终止的多字符串缓冲区的指针,函数在该缓冲区中按优先顺序检索有序的、以空分隔的列表,从最可取的开始。

这是我的定义:

<DllImport("Kernel32.dll", SetLastError:=True, ExactSpelling:=True, CharSet:=CharSet.Unicode)>
Public Shared Function GetProcessPreferredUILanguages(ByVal flags As UiLanguageMode,
                                                <Out> ByRef refNumLanguages As UInteger,
                    <MarshalAs(UnmanagedType.LPWStr)> ByVal languagesBuffer As StringBuilder,
                                                      ByRef refLanguagesBufferSize As UInteger
) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function

以及我如何尝试使用它:

Public Shared Function GetProcessPreferredUILanguages() As IReadOnlyCollection(Of CultureInfo)

    Dim buffer As New StringBuilder(0)
    Dim numLangs As UInteger
    Dim bufferRequiredLength As UInteger

    ' I do this because If the StringBuilder capacity exceeds the exact required, then I got a blank (or unreadable) string.
    NativeMethods.GetProcessPreferredUILanguages(UiLanguageMode.Name, numLangs, Nothing, bufferRequiredLength)
    buffer.Capacity = CInt(bufferRequiredLength)

    NativeMethods.GetProcessPreferredUILanguages(UiLanguageMode.Name, numLangs, buffer, bufferRequiredLength)
    Console.WriteLine($"{NameOf(numLangs)}: {numLangs}")
    Console.WriteLine(buffer?.ToString().Replace(ControlChars.NullChar, " "))

    Dim langList As New List(Of CultureInfo)
    For Each langName As String In buffer.ToString().Split({ControlChars.NullChar}, StringSplitOptions.RemoveEmptyEntries)
        langList.Add(New CultureInfo(langName))
    Next
    Return langList

End Function

我认为问题在于我缺少替换字符串中的其他一些空字符。


另外,出于测试目的,我也会分享与SetProcessPreferredUILanguages功能相关的源代码:

<DllImport("Kernel32.dll", SetLastError:=True, ExactSpelling:=True, CharSet:=CharSet.Unicode)>
Public Shared Function SetProcessPreferredUILanguages(ByVal flags As UiLanguageMode,
                    <MarshalAs(UnmanagedType.LPWStr)> ByVal languagesBuffer As String,
                                                <Out> ByRef refNumLanguages As UInteger
) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function

和:

Public Function SetProcessPreferredUILanguages(ParamArray langNames As String()) As Integer

    If (langNames Is Nothing) Then
        Throw New ArgumentNullException(paramName:=NameOf(langNames))
    End If

    Dim langList As New List(Of String)
    For Each langName As String In langNames
        langList.Add(langName & ControlChars.NullChar)
    Next

    Dim numLangs As UInteger = CUInt(langList.Count)

    NativeMethods.SetProcessPreferredUILanguages(UiLanguageMode.Name, String.Concat(langList), numLangs)

    #If DEBUG Then
        If numLangs = langList.Count Then
            Debug.WriteLine("Successfully changed UI languages")
        ElseIf numLangs < 1 Then
            Debug.WriteLine("No language could be set.")
        Else
            Debug.WriteLine("Not all languages were set.")
        End If
    #End If

    langList.Clear()
    Return CInt(numLangs)

End Function

标签: .netvb.netwinapi

解决方案


缓冲区包含一个以 null 结尾的多字符串:返回的字符串在第一个\0字符处被截断。

由于该GetProcessPreferredUILanguages函数需要一个指向包含文化 ID 的缓冲区的指针,因此我们提供一个指针,然后使用指定的缓冲区长度将其封送回来。

这是GetProcessPreferredUILanguages函数的原始定义(dwFlags使用uint枚举提供参数):

public enum MUIFlags : uint
{
    MUI_LANGUAGE_ID = 0x4,      // Use traditional language ID convention
    MUI_LANGUAGE_NAME = 0x8,    // Use ISO language (culture) name convention
}

[SuppressUnmanagedCodeSecurity, SecurityCritical]
internal static class NativeMethods
{
    [DllImport("Kernel32.dll", SetLastError = true,  CharSet = CharSet.Unicode)]
    internal static extern bool GetProcessPreferredUILanguages(MUIFlags dwFlags, 
        ref uint pulNumLanguages, IntPtr pwszLanguagesBuffer, ref uint pcchLanguagesBuffer);

    [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern bool SetProcessPreferredUILanguages(MUIFlags dwFlags, 
        string pwszLanguagesBuffer, ref uint pulNumLanguages);
}

顺便说一句,Win32 函数的返回值声明为BOOL,它将被编组为C#'s boolVB.Net's Boolean<MarshalAs(UnmanagedType.Bool)>不需要。

VB.Net 版本:

Public Enum MUIFlags As UInteger
    MUI_LANGUAGE_ID = &H4     ' Use traditional language ID convention
    MUI_LANGUAGE_NAME = &H8   ' Use ISO language (culture) name convention
End Enum

<SuppressUnmanagedCodeSecurity, SecurityCritical>
Friend Class NativeMethods
    <DllImport("Kernel32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
    Friend Shared Function GetProcessPreferredUILanguages(dwFlags As MUIFlags, ByRef pulNumLanguages As UInteger,
        pwszLanguagesBuffer As IntPtr, ByRef pcchLanguagesBuffer As UInteger) As Boolean
    End Function

    <DllImport("Kernel32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
    Friend Shared Function SetProcessPreferredUILanguages(dwFlags As MUIFlags, 
        pwszLanguagesBuffer As String, ByRef pulNumLanguages As UInteger) As Boolean
    End Function
End Class

接收缓冲区声明为IntPtr buffer,最初设置为IntPtr.Zero
第一次调用该函数返回文化的数量和所需的缓冲区大小。我们只需要使用Marshal.StringToHGlobalUni分配指定的大小:

string langNames = new string('0', (int)bufferRequiredLength);
buffer = Marshal.StringToHGlobalUni(langNames);

要将其封送回来,我们可以指定需要复制到缓冲区中的字节数。如果我们不指定这个值,字符串无论如何都会被截断:

string langNames = Marshal.PtrToStringUni(buffer, (int)bufferRequiredLength);

当然,我们需要在退出时释放用于缓冲区的内存:

Marshal.FreeHGlobal(buffer);

修改方法的 C# 版本:

[SecuritySafeCritical]
public static List<CultureInfo> GetProcessPreferredUILanguages()
{
    uint numLangs = 0;
    uint bufferRequiredLength = 0;
    IntPtr buffer = IntPtr.Zero;
    try {
        bool result = NativeMethods.GetProcessPreferredUILanguages(MUIFlags.MUI_LANGUAGE_NAME, ref numLangs, IntPtr.Zero, ref bufferRequiredLength);
        if (!result) return null;

        string langNames = new string('0', (int)bufferRequiredLength);
        buffer = Marshal.StringToHGlobalUni(langNames);
        result = NativeMethods.GetProcessPreferredUILanguages(MUIFlags.MUI_LANGUAGE_NAME, ref numLangs, buffer, ref bufferRequiredLength);
        string langNames = Marshal.PtrToStringUni(buffer, (int)bufferRequiredLength);
        if (langNames.Length > 0)
        {
            string[] isoNames = langNames.Split(new[] { "\0" }, StringSplitOptions.RemoveEmptyEntries);
            var cultures = new List<CultureInfo>();
            foreach (string lang in isoNames) {
                cultures.Add(CultureInfo.CreateSpecificCulture(lang));
            }
            return cultures;
        }
        return null;
    }
    finally {
        Marshal.FreeHGlobal(buffer);
    }
}

VB.Net 版本:

<SecuritySafeCritical>
Public Shared Function GetProcessPreferredUILanguages() As List(Of CultureInfo)
    Dim numLangs As UInteger = 0
    Dim bufferRequiredLength As UInteger = 0
    Dim buffer As IntPtr = IntPtr.Zero
    Try
        Dim result As Boolean = NativeMethods.GetProcessPreferredUILanguages(MUIFlags.MUI_LANGUAGE_NAME, numLangs, IntPtr.Zero, bufferRequiredLength)
        If Not result Then Return Nothing

        Dim langNames As String = New String("0"c, CInt(bufferRequiredLength))
        buffer = Marshal.StringToHGlobalUni(langNames)
        result = NativeMethods.GetProcessPreferredUILanguages(MUIFlags.MUI_LANGUAGE_NAME, numLangs, buffer, bufferRequiredLength)
        langNames = Marshal.PtrToStringUni(buffer, CType(bufferRequiredLength, Integer))
        If langNames.Length > 0 Then
            Dim isoNames As String() = langNames.Split({vbNullChar}, StringSplitOptions.RemoveEmptyEntries)
            Dim cultures = New List(Of CultureInfo)()
            For Each lang As String In isoNames
                cultures.Add(CultureInfo.CreateSpecificCulture(lang))
            Next
            Return cultures
        End If
        Return Nothing
    Finally
        Marshal.FreeHGlobal(buffer)
    End Try
End Function

更新

添加了C#声明SetProcessPreferredUILanguages和实现代码。
请注意,我已将所有声明更改为charset: UnicodeMarshal.StringToHGlobalUni 它比Marshal.AllocHGlobal.

在 Windows 10 1803 17134.765、Windows 7上测试SP1。两者都按预期工作。使用此处提供的代码。

public static int SetProcessPreferredUILanguages(params string[] langNames)
{
    if ((langNames == null)) {
        throw new ArgumentNullException($"Argument {nameof(langNames)} cannot be null");
    }
    string languages = string.Join("\u0000", langNames);

    uint numLangs = 0;
    bool result = NativeMethods.SetProcessPreferredUILanguages(MUIFlags.MUI_LANGUAGE_NAME, languages, ref numLangs);
    if (!result) return 0;
    return (int)numLangs;
}

VB.Net 版本:

Public Shared Function SetProcessPreferredUILanguages(ParamArray langNames As String()) As Integer
    If (langNames Is Nothing) Then
        Throw New ArgumentNullException($"Argument {NameOf(langNames)} cannot be null")
    End If
    Dim languages As String = String.Join(vbNullChar, langNames)

    Dim numLangs As UInteger = 0
    Dim result As Boolean = NativeMethods.SetProcessPreferredUILanguages(MUIFlags.MUI_LANGUAGE_NAME, languages, numLangs)
    If (Not result) Then Return 0
    Return CType(numLangs, Integer)
End Function

示例调用:

string allLanguages = string.Empty;
string[] languages = new[] { "en-US", "es-ES", "it-IT", "de-DE", "fr-FR" };
if (SetProcessPreferredUILanguages(languages) > 0)
{
    var result = GetProcessPreferredUILanguages();
    allLanguages = string.Join(", ", result.OfType<CultureInfo>()
                         .Select(c => c.Name).ToArray());
}

推荐阅读