Не удается прочитать все имена языков, возвращаемые функцией GetProcessPreferredUILanguages - PullRequest
1 голос
/ 04 июня 2019

После того, как я использовал SetProcessPreferredUILanguages ​​, чтобы установить до 5 предпочитаемых языков, и убедился, что он работает, потому что pulNumLanguages имеет ту же длину, что и моя строка с разделителями из имен языков после завершения вызова.

Затем я пытаюсь получить все предпочитаемые процессом языки пользовательского интерфейса с помощью функции GetProcessPreferredUILanguages ​​. И проблема в том, что я могу прочитать только одно (первое) из названий языков в буфере возвращаемой строки, но 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

1 Ответ

1 голос
/ 05 июня 2019

Буфер содержит завершенную нулем многострочную строку: возвращаемая строка усекается до первого \0 символа.

Поскольку функция GetProcessPreferredUILanguages ожидает указатель на буфер, который будет содержать идентификаторы культур, давайте предоставим один, а затем перенаправим его обратно, используя указанную длину буфера.

Это оригинальное определение функции 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# 'bool, VB.Net' 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: Unicode и Marshal.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());
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...