Can't read all the language names returned by 'GetProcessPreferredUILanguages' function

ε祈祈猫儿з 提交于 2019-12-02 14:08:23

问题


After I used SetProcessPreferredUILanguages to set up to 5 preferred languages, and ensuring it worked because pulNumLanguages has the same length as my custom language-names delimited string after the call is done.

Then now I'm trying to obtain all the process preferred UI languages through the GetProcessPreferredUILanguages function. And the problem is I only can read one (the first) of the language names in the returned string buffer, but the pulNumLanguages specifies 5 languages are returned...

So, I will ask for the proper way to read the returned string.

Note what says the documentation about the pwszLanguagesBuffer parameter:

Pointer to a double null-terminated multi-string buffer in which the function retrieves an ordered, null-delimited list in preference order, starting with the most preferable.

This is my definition:

<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

And how I'm trying to use it:

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

I think the problem is that I'm missing to replace some other null character in the string.


As an addition, for testing purposes, I'll share too the source-code related to SetProcessPreferredUILanguages function:

<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

And:

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

回答1:


The buffer contains a null-terminated multi-string: the returned string is truncated at the first \0 char.

Since the GetProcessPreferredUILanguages function expects a pointer to the buffer that will contain the cultures IDs, let's provide one, then marshal it back using the specified buffer length.

This is the original definition of the GetProcessPreferredUILanguages function (where the dwFlags parameter is provided using an uint enum):

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);
}

Btw, the Win32 function's return value is declared as BOOL, it will be marshalled as C#'s bool, VB.Net's Boolean. <MarshalAs(UnmanagedType.Bool)> is not needed.

VB.Net version:

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

The receiving buffer is declared as IntPtr buffer, initially set to IntPtr.Zero.
The first call the the function returns the number of cultures and the required size of the buffer. We just need to allocate the specified size, using Marshal.StringToHGlobalUni:

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

To marshal it back, we can specify the number of bytes that need to be copied into the buffer. If we don't specify this value, the string will be truncated anyway:

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

Of course we need to deallocate the memory used for the buffer on exit:

Marshal.FreeHGlobal(buffer);

The C# version of the modified method:

[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 version:

<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

Update:

Added the C# declaration of SetProcessPreferredUILanguages and the implementation code.
Note that I've change all the declarations to charset: Unicode and Marshal.StringToHGlobalUni, it's safer (and probably more appropriate) than Marshal.AllocHGlobal.

Tested on Windows 10 1803 17134.765, Windows 7 SP1. Both working as expected. Use the code as presented here.

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 version:

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

Sample call:

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());
}


来源:https://stackoverflow.com/questions/56450144/cant-read-all-the-language-names-returned-by-getprocesspreferreduilanguages-f

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!