May 23rd, 2022

How does Windows decide whether a newly-created window should use LTR or RTL layout?

You can specify in a window’s extended styles whether it follows left-to-right (LTR) or right-to-left (RTL) layout. The right-to-left layout is used in languages that are written from right to left, of which the most widely used today are probably Arabic and Hebrew. You can request right-to-left layout by setting the WS_EX_LAYOUTRTL extended style.

If you don’t specify the WS_EX_LAYOUTRTL extended style, the system may still apply that style automatically, based on the following rules:

Scenario Rule
Child window Parent omits WS_EX_NO­INHERIT­LAYOUT Inherit WS_EX_LAYOUTRTL from parent.
Parent has WS_EX_NO­INHERIT­LAYOUT Remain LTR.
Top-level window Owned window Remain LTR.
Unowned window Follow Get­Process­Default­Layout.

In the case of a top-level unowned window, the WS_EX_LAYOUTRTL extended style is automatically set if the process default layout has the LAYOUT_RTL bit set.

As I noted some time ago, if a process never calls Set­Process­Default­Layout, then the initial process default layout is inferred by inspecting the FileDescription version property of the primary executable: If it begins with two left-to-right marks (LRMs, represented by Unicode code point U+200E), then the process default layout is set to LAYOUT_RTL.

I also noted some time ago that you can ask Get­Locale­Info­Ex for the LOCALE_IREADING­LAYOUT to determine whether any particular language is LTR or RTL.

// Direction values:
// 0 = left to right (e.g., English)
// 1 = right to left (e.g., Arabic)
// 2 = top to bottom, right to left (e.g., classical Chinese)
// 3 = top to bottom, left to right (e.g., Mongolian)

int GetLanguageReadingLayout(PCWSTR languageName)
{
    int direction = 0;
    THROW_IF_WIN32_BOOL_FALSE(
        GetLocaleInfoEx(languageName,
                        LOCALE_IREADINGLAYOUT | LOCALE_RETURN_NUMBER,
                        reinterpret_cast<LPWSTR>(&direction),
                        sizeof(direction) / sizeof(wchar_t)));
    return direction;
}

int GetSystemDefaultLanguageReadingLayout()
{
    return GetLanguageReadingLayout(LOCALE_NAME_SYSTEM_DEFAULT);
}

int GetUserDefaultLanguageReadingLayout()
{
    return GetLanguageReadingLayout(LOCALE_NAME_USER_DEFAULT);
}

You typically are interested in the primary language for the current thread, since that’s the one that most influences which language resources your program will use. You can use Get­Thread­Preferred­UI­Languages to get all the languages that apply to the current thread, and then pass the first one to Get­Language­Reading­Layout. Calling the Get­Thread­Preferred­UI­Languages function is a bit frustrating because the list of applicable languages can change asynchronously (if another thread calls Set­Process­Preferred­UI­Languages It is double annoying because the wil helper function Adapt­Fixed­Size­To­Allocated­Result assumes null-terminated strings and doesn’t support double-null-terminated strings, so we have to write out the loop manually.

namespace wil
{
    template<typename string_type, size_t stackBufferLength = 40>
    HRESULT GetThreadPreferredUILanguages(DWORD flags,
        _Out_ PULONG languageCount, string_type& result)
    {
        wchar_t stackBuffer[stackBufferLength];
        ULONG required = ARRAYSIZE(stackBuffer);
        if (::GetThreadPreferredUILanguages(flags, languageCount,
                                         stackBuffer, &required))
        {
            result = make_unique_string_nothrow<string_type>
                                             (nullptr, required);
            RETURN_IF_NULL_ALLOC(result);
            memcpy(result.get(), stackBuffer,
                   required * sizeof(wchar_t));
            return S_OK;
        }
        DWORD error = ::GetLastError();
        while (error == ERROR_INSUFFICIENT_BUFFER)
        {
            result = make_unique_string_nothrow<string_type>
                                             (nullptr, required);
            RETURN_IF_NULL_ALLOC(result);
            if (::GetThreadPreferredUILanguages(flags,
                         languageCount, result.get(), &required))
            {
                return S_OK;
            }
            error = ::GetLastError();
        }
        RETURN_WIN32(error);
    }

    template <typename string_type = wil::unique_cotaskmem_string,
              size_t stackBufferLength = 40>
    string_type GetThreadPreferredUILanguages(DWORD flags,
        _Out_ PULONG languageCount)
    {
        string_type result;
        THROW_IF_FAILED((wil::GetThreadPreferredUILanguages<
            string_type, stackBufferLength>
            (flags, languageCount, result)));
        return result;
    }
}

We can now plug this into our existing function.

int GetDefaultThreadLanguageReadingLayout()
{
    ULONG count;
    return GetLanguageReadingLayout(
        wil::GetThreadPreferredUILanguages(MUI_LANGUAGE_NAME |
            MUI_MERGE_UI_FALLBACK, &count).get());
}

Sorry it’s such a pain.

Bonus chatter: All of this logic assumes that your program has been translated into RTL languages in the first place. If your program is English-only, don’t display your English strings in RTL. As I noted in that article, you can leave a breadcrumb in the resources to tell you which direction the resources expect strings to read.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments

Discussion are closed.