3

From a Windows API call (GetUserPreferredUILanguages()), I get a list of strings as one null-delimited PWideChar. I need to convert this to a list of Delphi strings. I began writing code to manually loop through the list, looking for #0 chars.

Is there a smarter way to do this?

Example of the PWideChar returned by GetUserPreferredUILanguages:

('e','n','-','U','S',#0,'f','r','-','F','R',#0,#0,...)

(based on what I read in the documentation, because when I call the function on my computer, it only returns one language, i.e. 'en-US'#0#0)

Here is my code so far:

procedure GetWinLanguages(aList: TStringList);
var lCount, lSize: ULong;
    lChars: array of WideChar;
    lIndex, lLastIndex: integer;
begin
  lSize := 1000;
  SetLength(lChars, lSize);
  GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, @lCount, @lChars[0], @lSize);

  // untested quick solution to convert from lChars to aList
  lIndex := 0;
  lLastIndex := -1;
  while (lIndex<=lSize) do
  begin
    while (lIndex<lSize) and (lChars[lIndex]<>#0) do
      inc(lIndex);
    if (lIndex-lLastIndex)>1 then
    begin
      // here: copy range lLastIndex to lIndex, convert to string and add to aList
      lLastIndex := lIndex;
      inc(lIndex);
    end else
      Break;
  end;
end;

PS. I am on Windows 10 using Delphi Berlin for a FMX project.

Hans
  • 2,220
  • 13
  • 33
  • To the downvoter: please let me know what is wrong with this question. I will be pleased to improve it. – Hans Apr 03 '17 at 21:24

2 Answers2

5

This API returns a double null-terminated string. This program shows how to parse such a thing:

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  Winapi.Windows;

procedure Main;
var
  NumLanguages, LanguagesBufferLen: ULONG;
  LanguagesBuffer: TArray<WideChar>;
  P: PWideChar;
  str: string;
begin
  LanguagesBufferLen := 0;
  Win32Check(GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, @NumLanguages, nil, @LanguagesBufferLen));
  SetLength(LanguagesBuffer, LanguagesBufferLen);
  Win32Check(GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, @NumLanguages, @LanguagesBuffer[0], @LanguagesBufferLen));
  P := @LanguagesBuffer[0];
  while P^<>#0 do begin
    str := P;
    Writeln(str);

    inc(P, Length(str)+1); // step over the string, and its null terminator
  end;
end;

begin
  try
    Main;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  Readln;
end.

It should be obvious how to extract from this code a function to parse a null-terminated string to a string list. That would allow you to re-use the code in other places.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thanks. Yes, I do know what it returns. I called it a null-delimited PWideChar, which is technically what it is. Your loop is much simpler than mine - I like that. – Hans Apr 03 '17 at 11:41
  • Sorry, I misread your code. I though you had an array of PWideChar – David Heffernan Apr 03 '17 at 11:45
  • @Hans it is technically a **double** null terminated string, not a **single** null-terminated string, like you keep describing. A **double** null-terminated string is a list of strings separated by a null character, and then the list is terminated by an *extra* null character. So there are **two** nulls at the very end. – Remy Lebeau Apr 03 '17 at 15:25
  • 1
    @DavidHeffernan: the documentation only states that the function reports `ERROR_INSUFFICIENT_BUFFER`, but it does not say whether the function returns true or false when `nil/0` is supplied as input. The *typical* convention used by functions that implement this kind of double-query semantics is for the function to return FALSE on *any* error, including `ERROR_INSUFFICIENT_BUFFER`. If that is not the case in this function, then Microsoft is not following its own conventions, and is not documenting the behavior adequately. – Remy Lebeau Apr 03 '17 at 17:09
  • That's not how I read the documentation. In any case, the behaviour of the function makes it clear that my interpretation is correct. – David Heffernan Apr 03 '17 at 17:13
  • @RemyLebeau I never called it a null-terminated string, it's a null-delimited string. – Hans Apr 03 '17 at 21:13
  • @DavidHeffernan I realise it was easy to misread my question. I see that Remy made some improvements to the text, and I just added a bit more text also to make it more clear. – Hans Apr 03 '17 at 21:28
2

The API returns a double-null-terminated string, where each substring is separated by a #0 character, and then the list is terminated by an extra #0 character. So you would simply loop until you encounter that last #0 character. For example:

procedure GetWinLanguages(aList: TStringList);
var
  lCount, lSize: ULONG;
  lChars: array of Char;
  lStr: PChar;
begin
  lSize := 0;
  lChars := nil;

  repeat
    // unlike most Win32 APIs, GetUserPreferredUILanguages() returns TRUE when
    // pwszLanguagesBuffer is nil and pcchLanguagesBuffer is set to 0 (unless a
    // real error occurs!). This is not made clear in the documentation! The
    // function only returns FALSE with an ERROR_INSUFFICENT_BUFFER error code
    // when pwszLanguagesBuffer is not nil and pcchLanguagesBuffer is set too low...

    if not GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, @lCount, PChar(lChars), @lSize) then
    begin
      if GetLastError() <> ERROR_INSUFFICIENT_BUFFER then
        RaiseLastOSError;
    end
    else if lChars <> nil then
      Break;
    SetLength(lChars, lSize);
  until False;

  lStr := PChar(lChars);
  while (lStr^ <> #0) do
  begin
    aList.Add(lStr);
    Inc(lStr, StrLen(lStr)+1);
  end;
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • This behavior is not made clear in the MSDN documentation. So be it. I have fixed the code accordingly. – Remy Lebeau Apr 03 '17 at 17:45
  • Why do you loop? – David Heffernan Apr 03 '17 at 17:48
  • I usually loop when doing this kind of query+allocate for dynamic data. Since system data is being queried, and the size and data are retrieved separately, the loop handles the case where the data may change after the size is retrieved and before the actual data is retrieved. Yes, it is a small window of opportunity, but it still exists nontheless. – Remy Lebeau Apr 03 '17 at 17:50