As you may or may not know, Windows has two ways of scaling up UI elements:

  • DPI scaling
  • Text scaling

Take Windows 10 as an example, the following two entries are about DPI scaling:
dpi1.png

dpi2.png

and this one, is the text only scaling
text1.png

Problems

The text only scaling is not well-documented and not widely supported by applications. Your application is probably one of them. But, if your application uses CEF to display anything, you may notice that it does support text scaling which contradicts with your application and mess up your interface.

And what's worse is that there is no switch or command line flag to turn this off as it is hardcoded into the framework itself.

Locate the culprit

The source code responsible for text factor awareness can be inspected at
https://source.chromium.org/chromium/chromium/src/+/main:ui/display/win/uwp_text_scale_factor.cc;l=137?q=TextScaleFactor
which looks something like:

  float GetTextScaleFactor() const override {
    DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

    double result = 1.0;

    // This is just a null check, so if we don't have access to the text
    // scaling / service for any reason we'll just use 1x.
    if (ui_settings_com_object_) {
      HRESULT hr = ui_settings_com_object_->get_TextScaleFactor(&result);
      if (FAILED(hr)) {
        VLOG(2) << "IUISettings2::TextScaleFactor failed: "
                << logging::SystemErrorCodeToString(hr);
        // COM calls overwrite their out-params, typically by zeroing them out.
        // Since we can't rely on this being a valid value on failure, we'll
        // reset it.
        result = 1.0;
      }
    }

    // Windows documents this property to always have a value greater than or
    // equal to 1. Let's make sure that's the case - if we don't, we could get
    // bizarre behavior and divide-by-zeros later on.
    DCHECK_GE(result, 1.0);
    return static_cast<float>(result);
  }

As you can see, it has no switch or whatsoever. Since we don't be given an official way to disable it, we do it hard.

The hard way

By examing the code, we can see that this functionality needs the existence of ui_settings_com_object_ which is first introduced in Windows 8. That's why it is dynamically created.

To create ui_settings_com_object_, there is a dedicate function called CreateUiSettingsComObject which can be found at https://source.chromium.org/chromium/chromium/src/+/main:ui/display/win/uwp_text_scale_factor.cc?q=RuntimeClass_Windows_UI_ViewManagement_UISettings

// Constructs the UWP UI Settings COM object, or fails with as useful of a log
// message as possible given Windows error reporting.
//
// Lots of things could potentially go wrong so we want to be able to bail out
// when creating the UWP UI Settings object, so we've moved the initialization
// to a separate function.
bool CreateUiSettingsComObject(ComPtr<IUISettings2>& ptr) {
  DCHECK(!ptr);

  // Create the COM object.
  auto hstring = base::win::ScopedHString::Create(
      RuntimeClass_Windows_UI_ViewManagement_UISettings);
  if (!hstring.is_valid()) {
    return false;
  }
  ComPtr<IInspectable> inspectable;
  HRESULT hr = base::win::RoActivateInstance(hstring.get(), &inspectable);
  if (FAILED(hr)) {
    VLOG(2) << "RoActivateInstance failed: "
            << logging::SystemErrorCodeToString(hr);
    return false;
  }

  // Verify that it supports the correct interface.
  hr = inspectable.As(&ptr);
  if (FAILED(hr)) {
    VLOG(2) << "As IUISettings2 failed: "
            << logging::SystemErrorCodeToString(hr);
    return false;
  }

  return true;
}

There are multiple checks of what's created. If any of them fails, the whole creation process fails.

The first call to base::win::ScopedHString::Create will in turn call a Windows API WindowsCreateString. That's a great check to cut into the function and see what we can do to it.

Pull out your ollydbg and load the cefclient.exe. bp WindowsCreateString and F9
When you see the following in the stack window, that's the location we're looking for:

047AEC50   12BBC059  return to libcef.12BBC059
047AEC54   17E749EC  UNICODE "Windows.UI.ViewManagement.UISettings"
047AEC58   00000024

Ctrl+F9 twice to return to libcef and Alt+F9 once to return to the caller. It should look something like

13CBF4FE    6A 24           PUSH 0x24
13CBF500    68 EC49E717     PUSH libcef.17E749EC                     ; UNICODE "Windows.UI.ViewManagement.UISettings"
13CBF505    56              PUSH ESI
13CBF506    E8 F5CAEFFE     CALL libcef.12BBC000
13CBF50B    83C4 0C         ADD ESP,0xC                           ;<-----we are here
13CBF50E    8B46 04         MOV EAX,DWORD PTR DS:[ESI+0x4]
13CBF511    85C0            TEST EAX,EAX
13CBF513    0F84 A9000000   JE libcef.13CBF5C2
13CBF519    8D4D E0         LEA ECX,DWORD PTR SS:[EBP-0x20]
13CBF51C    C701 00000000   MOV DWORD PTR DS:[ECX],0x0
13CBF522    51              PUSH ECX
13CBF523    50              PUSH EAX

Now

  if (!hstring.is_valid()) {
    return false;
  }

is the function we're going to hijack, and clearly 13CBF513 0F84 A9000000 JE libcef.13CBF5C2 is that if check.

Simply modify it to JMP libcef.13CBF5C2 would do the trick.

Copy to executable and save to the disk.

Result

Launch the cefclient.exe again and change text scale factor to see the result. Now CEF should not respond to the change any more.