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