Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt
  • Unsolved
Collapse
qtws26_logo

The Qt Event of the Year: Qt World Summit 2026

Save Your Spot >
Brand Logo
  1. Home
  2. Qt Development
  3. QML and Qt Quick
  4. QML HighDPI scaling problem on Windows 11
Qt 6.12 LTS is out! See what's new in the release blog

QML HighDPI scaling problem on Windows 11

Scheduled Pinned Locked Moved Solved QML and Qt Quick
9 Posts 2 Posters 374 Views 2 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • Y Offline
    Y Offline
    Yuri
    wrote last edited by
    #1

    Some lines (like scrollbars) looks broken on HighDPI.
    Windows 11 26H2, Qt 6.12.0 built from sources with MinGW-w64 (g++ version 16.2.0), 4k monitor, scaling 150%.
    AMD Ryzen 5 5600, NVidia GeForce GTX 1660 Super (driver v617.14).
    Minimal Reproducible Example (main.qml):

    import QtQuick
    import QtQuick.Controls
    
    Window {
        width: 343 // odd value
        height: 100
        visible: true
        title: "DPR 1.5 Subpixel Artifact Test"
    
        Rectangle {
            anchors.fill: parent
            color: "#f0f0f0"
    
            ScrollView {
                anchors.fill: parent
                TextArea {
                    text: "Resize the window horizontally pixel by pixel to observe the 1px line / scrollbar shifting and thickening to 2px."
                }
            }
        }
    }
    

    QML_06.png

    Font dialog from examples\quickcontrols\texteditor:
    QML_02.png

    Could you please tell me what might be causing this behavior? How can it be fixed?
    Tried, but it didn't help:

    • set QT_SCALE_FACTOR_ROUNDING_POLICY=Round
    • set QSG_RHI_BACKEND=opengl
    • set QSG_RHI_BACKEND=d3d11
    • set QSG_RHI_BACKEND=software
    • set QML_USE_GLYPH_CACHE_WORKAROUND=1
    • set QT_ENABLE_REGULAR_EXPRESSION_OPTION_WIN=1
    • set QSG_RENDER_LOOP=basic
    • set QT_QPA_PLATFORM=windows:dpiawareness=1
    • set QT_QPA_PLATFORM=windows:fontengine=freetypedirectwrite
    • set QML_XHR_ALLOW_FILE_READ=1
    1 Reply Last reply
    0
    • Axel SpoerlA Offline
      Axel SpoerlA Offline
      Axel Spoerl
      Moderators
      wrote last edited by
      #2

      If you add

      Component.onCompleted:
          console.log(Screen.devicePixelRatio)
      

      What's the output?

      Software Engineer
      The Qt Company, Oslo

      Y 1 Reply Last reply
      0
      • Axel SpoerlA Axel Spoerl

        If you add

        Component.onCompleted:
            console.log(Screen.devicePixelRatio)
        

        What's the output?

        Y Offline
        Y Offline
        Yuri
        wrote last edited by
        #3

        @Axel-Spoerl
        qml: 1.5

        1 Reply Last reply
        0
        • Axel SpoerlA Offline
          Axel SpoerlA Offline
          Axel Spoerl
          Moderators
          wrote last edited by
          #4

          Okay, thanks @Yuri.
          What you see is technically a consequence of alternating rounding decisions. Since you have set a rounding policy, this should not happen.

          Can you do me two more favors please:

          • It's unlikely that the problem sits with MinGW. To exclude it completely, could you try MSVC instead?
          • To see whether it's a regression, could you try an older Qt build, preferably 6.8?

          Software Engineer
          The Qt Company, Oslo

          Y 1 Reply Last reply
          0
          • Axel SpoerlA Axel Spoerl

            Okay, thanks @Yuri.
            What you see is technically a consequence of alternating rounding decisions. Since you have set a rounding policy, this should not happen.

            Can you do me two more favors please:

            • It's unlikely that the problem sits with MinGW. To exclude it completely, could you try MSVC instead?
            • To see whether it's a regression, could you try an older Qt build, preferably 6.8?
            Y Offline
            Y Offline
            Yuri
            wrote last edited by
            #5

            @Axel-Spoerl
            Thanks @Axel-Spoerl.
            The same code on Qt 6.8.3 MSVC 2022 has the same problem. But colors are differs from Qt 6.12.0 version.
            QML_6.8.3_msvc2022.png

            On Qt 6.8.3 MinGW 15.2 it starts with errors:
            qml: 1.5
            Failed to create swapchain. Retrying by requesting a software rasterizer, if applicable for the 3D API implementation.
            Failed to build or resize swapchain
            Failed to present: IDXGISwapChain is unavailable
            Failed to end frame
            368b30b0-95f1-41e4-9dd7-798b74f1f19f-image.png
            If backend is OpenGL (set QSG_RHI_BACKEND=opengl) then window with any size starts without this problem, but the problem returns on window resizing.
            Identical behavior on any Qt and compiler version.
            Qt 6.8.3 (MSVC and MinGW):
            QML_6.8.3_msvc2022_opengl_2.png
            Qt 6.12.0 MinGW:
            QML_opengl_1.png

            1 Reply Last reply
            0
            • Axel SpoerlA Offline
              Axel SpoerlA Offline
              Axel Spoerl
              Moderators
              wrote last edited by
              #6

              Thanks @Yuri, that's very helpful.
              Your tests rule out the compiler, the graphics backend (it even happens with software) and a recent regression.
              What's left is how geometry maps to device pixels at a fractional scale factor.

              At 150%, one logical pixel is 1.5 physical pixels. Anything at an odd logical coordinate lands on half a physical pixel. Qt Quick doesn't snap item geometry to physical pixels, so a 1px line covers sometimes one and sometimes two physical pixels. If it's antialiased, it gets smeared across an extra pixel.

              The scrollbars show this most. With the default style on Windows 11 (FluentWinUI3), the ScrollBar comes from Fusion: a thin, rounded and therefore antialiased handle. It changes while you resize because the window's logical size is a whole number but its physical size isn't always divisible by 1.5. For example, 515 physical pixels is 343.33 logical, but the window reports 343. Items attached to the right or bottom edge, like the scrollbars, end up half a physical pixel off in one direction or the other. Which way depends on the window width, so it flips with every pixel you resize.

              I wrote earlier that the rounding policy should prevent this. I have to correct myself here. It's only true if it actually takes effect. With QT_SCALE_FACTOR_ROUNDING_POLICY=Round, 1.5 becomes 2, and at 2 the artifacts should disappear. Your log shows qml: 1.5, though. Could you set the variable in the same shell you start the app from and check what Screen.devicePixelRatio prints?

              • If it prints 2 and the artifacts are still there, that's a bug and worth a report.
              • If it still prints 1.5, the variable isn't reaching the application.

              You can solve the problem by either bumping the scale factor to 2. That should solve everything, if 2 is acceptable.
              Otherwise you can use a custom scrollbar, that is more forgiving. I experimented a bit:

              import QtQuick
                import QtQuick.Controls
                
                Window {
                    width: 343
                    height: 100
                    visible: true
                    title: "DPR 1.5 Subpixel Artifact Test"
                    
                    component FlatScrollBar: ScrollBar {
                        id: bar
                        contentItem: Rectangle {
                            implicitWidth: 4    // 4 logical = 6 physical pixels at 150%
                            implicitHeight: 4   
                            antialiasing: false 
                            color: bar.pressed ? bar.palette.dark : bar.palette.mid
                            opacity: 0.0
                            
                            states: State {
                                name: "active"
                                when: bar.policy === ScrollBar.AlwaysOn || (bar.active && bar.size < 1.0)
                                PropertyChanges { bar.contentItem.opacity: 0.75 }
                            }   
                            
                            transitions: Transition {
                                from: "active"
                                SequentialAnimation {
                                    PauseAnimation { duration: 450 }
                                    NumberAnimation { target: bar.contentItem; duration: 200; property: "opacity"; to: 0.0 }
                                }   
                            }   
                        }   
                    }   
                    
                    Rectangle {
                        anchors.fill: parent
                        color: "#f0f0f0"
                        
                        ScrollView {
                            id: scrollView
                            anchors.fill: parent
                            
                            ScrollBar.horizontal: FlatScrollBar {
                                parent: scrollView
                                x: scrollView.leftPadding
                                y: scrollView.height - height
                                width: scrollView.availableWidth
                                active: scrollView.ScrollBar.vertical.active
                            }   
                            
                            ScrollBar.vertical: FlatScrollBar {
                                parent: scrollView
                                x: scrollView.mirrored ? 0 : scrollView.width - width
                                y: scrollView.topPadding 
                                height: scrollView.availableHeight
                                active: scrollView.ScrollBar.horizontal.active
                            }   
                            
                            TextArea {
                                text: "Resize the window horizontally pixel by pixel to observe the 1px line / scrollbar shifting and thickening to 2px."
                            }   
                        }   
                    }   
                }   
              

              Software Engineer
              The Qt Company, Oslo

              Y 1 Reply Last reply
              1
              • Axel SpoerlA Axel Spoerl

                Thanks @Yuri, that's very helpful.
                Your tests rule out the compiler, the graphics backend (it even happens with software) and a recent regression.
                What's left is how geometry maps to device pixels at a fractional scale factor.

                At 150%, one logical pixel is 1.5 physical pixels. Anything at an odd logical coordinate lands on half a physical pixel. Qt Quick doesn't snap item geometry to physical pixels, so a 1px line covers sometimes one and sometimes two physical pixels. If it's antialiased, it gets smeared across an extra pixel.

                The scrollbars show this most. With the default style on Windows 11 (FluentWinUI3), the ScrollBar comes from Fusion: a thin, rounded and therefore antialiased handle. It changes while you resize because the window's logical size is a whole number but its physical size isn't always divisible by 1.5. For example, 515 physical pixels is 343.33 logical, but the window reports 343. Items attached to the right or bottom edge, like the scrollbars, end up half a physical pixel off in one direction or the other. Which way depends on the window width, so it flips with every pixel you resize.

                I wrote earlier that the rounding policy should prevent this. I have to correct myself here. It's only true if it actually takes effect. With QT_SCALE_FACTOR_ROUNDING_POLICY=Round, 1.5 becomes 2, and at 2 the artifacts should disappear. Your log shows qml: 1.5, though. Could you set the variable in the same shell you start the app from and check what Screen.devicePixelRatio prints?

                • If it prints 2 and the artifacts are still there, that's a bug and worth a report.
                • If it still prints 1.5, the variable isn't reaching the application.

                You can solve the problem by either bumping the scale factor to 2. That should solve everything, if 2 is acceptable.
                Otherwise you can use a custom scrollbar, that is more forgiving. I experimented a bit:

                import QtQuick
                  import QtQuick.Controls
                  
                  Window {
                      width: 343
                      height: 100
                      visible: true
                      title: "DPR 1.5 Subpixel Artifact Test"
                      
                      component FlatScrollBar: ScrollBar {
                          id: bar
                          contentItem: Rectangle {
                              implicitWidth: 4    // 4 logical = 6 physical pixels at 150%
                              implicitHeight: 4   
                              antialiasing: false 
                              color: bar.pressed ? bar.palette.dark : bar.palette.mid
                              opacity: 0.0
                              
                              states: State {
                                  name: "active"
                                  when: bar.policy === ScrollBar.AlwaysOn || (bar.active && bar.size < 1.0)
                                  PropertyChanges { bar.contentItem.opacity: 0.75 }
                              }   
                              
                              transitions: Transition {
                                  from: "active"
                                  SequentialAnimation {
                                      PauseAnimation { duration: 450 }
                                      NumberAnimation { target: bar.contentItem; duration: 200; property: "opacity"; to: 0.0 }
                                  }   
                              }   
                          }   
                      }   
                      
                      Rectangle {
                          anchors.fill: parent
                          color: "#f0f0f0"
                          
                          ScrollView {
                              id: scrollView
                              anchors.fill: parent
                              
                              ScrollBar.horizontal: FlatScrollBar {
                                  parent: scrollView
                                  x: scrollView.leftPadding
                                  y: scrollView.height - height
                                  width: scrollView.availableWidth
                                  active: scrollView.ScrollBar.vertical.active
                              }   
                              
                              ScrollBar.vertical: FlatScrollBar {
                                  parent: scrollView
                                  x: scrollView.mirrored ? 0 : scrollView.width - width
                                  y: scrollView.topPadding 
                                  height: scrollView.availableHeight
                                  active: scrollView.ScrollBar.horizontal.active
                              }   
                              
                              TextArea {
                                  text: "Resize the window horizontally pixel by pixel to observe the 1px line / scrollbar shifting and thickening to 2px."
                              }   
                          }   
                      }   
                  }   
                
                Y Offline
                Y Offline
                Yuri
                wrote last edited by
                #7

                @Axel-Spoerl
                Your code runs with some warnings:

                qml test.qml
                file:///C:/Qt/6.12.0/mingw64_162/bin/test.qml:12:24: QML QQuickRectangle: The current style does not support customization of this control (property: "contentItem" item: QQuickRectangle(0x21868a6e230, parent=0x0, geometry=0,0 4x4)). Please customize a non-native style (such as Basic, Fusion, Material, etc). For more information, see: https://doc.qt.io/qt-6/qtquickcontrols2-customize.html#customization-reference
                file:///C:/Qt/6.12.0/mingw64_162/bin/test.qml:12:24: QML QQuickRectangle: The current style does not support customization of this control (property: "contentItem" item: QQuickRectangle(0x21868a6d6f0, parent=0x0, geometry=0,0 4x4)). Please customize a non-native style (such as Basic, Fusion, Material, etc). For more information, see: https://doc.qt.io/qt-6/qtquickcontrols2-customize.html#customization-reference
                qml: 2
                

                But with QT_SCALE_FACTOR_ROUNDING_POLICY=Round the problem is gone.
                Qt 6.12.0 MinGW:
                QML_scale.png
                Qt 6.8.3 MSVC:
                QML_6.8.3_msvc_scale.png
                Also on every resizing it prints this message:

                qrc:/qt-project.org/imports/QtQuick/Controls/Windows/ScrollBar.qml:37:49: Unable to assign [undefined] to int
                

                I also see that in Qt 6.12 a black rectangle is drawn on the scrollbar, which is not present in Qt 6.8. But this problem is not related to this topic.

                1 Reply Last reply
                0
                • Axel SpoerlA Offline
                  Axel SpoerlA Offline
                  Axel Spoerl
                  Moderators
                  wrote last edited by
                  #8

                  Thanks @Yuri, that confirms it. At a device pixel ratio of 2, every logical pixel is exactly two physical pixels, so nothing can land on half a pixel and the artifacts disappear. At 1.5 they're a consequence of fractional scaling rather than a bug in Qt Quick. So you can choose between QT_SCALE_FACTOR_ROUNDING_POLICY=Round (crisp, but a 200% UI) and staying at 150% with these unpleasant effects.

                  Sorry about the warnings, those are my fault. I said earlier that your scrollbars come from FluentWinUI3/Fusion, but on Windows the default is the native Windows style, and it can't be customized. That causes both messages, including the Unable to assign [undefined] to int on resize. Basing the custom scrollbar on Fusion's ScrollBar fixes it. Only these two lines change:

                  import QtQuick.Controls.Fusion as Fusion 
                  
                  component FlatScrollBar: Fusion.ScrollBar {
                  

                  Fusion is only used for this one component. All other controls keep the Windows style.

                  About the black rectangle on the scrollbar in 6.12 that isn't there in 6.8: if it shows up at 100% scaling too, or with Round, it looks like a regression. Could you file a bug report at https://bugreports.qt.io with your example and both screenshots (6.8 and 6.12)? Please post the link here so others can follow it.

                  Software Engineer
                  The Qt Company, Oslo

                  Y 1 Reply Last reply
                  1
                  • Axel SpoerlA Axel Spoerl

                    Thanks @Yuri, that confirms it. At a device pixel ratio of 2, every logical pixel is exactly two physical pixels, so nothing can land on half a pixel and the artifacts disappear. At 1.5 they're a consequence of fractional scaling rather than a bug in Qt Quick. So you can choose between QT_SCALE_FACTOR_ROUNDING_POLICY=Round (crisp, but a 200% UI) and staying at 150% with these unpleasant effects.

                    Sorry about the warnings, those are my fault. I said earlier that your scrollbars come from FluentWinUI3/Fusion, but on Windows the default is the native Windows style, and it can't be customized. That causes both messages, including the Unable to assign [undefined] to int on resize. Basing the custom scrollbar on Fusion's ScrollBar fixes it. Only these two lines change:

                    import QtQuick.Controls.Fusion as Fusion 
                    
                    component FlatScrollBar: Fusion.ScrollBar {
                    

                    Fusion is only used for this one component. All other controls keep the Windows style.

                    About the black rectangle on the scrollbar in 6.12 that isn't there in 6.8: if it shows up at 100% scaling too, or with Round, it looks like a regression. Could you file a bug report at https://bugreports.qt.io with your example and both screenshots (6.8 and 6.12)? Please post the link here so others can follow it.

                    Y Offline
                    Y Offline
                    Yuri
                    wrote last edited by
                    #9

                    @Axel-Spoerl
                    Sorry. It looks like the black rectangle isn't a regression, but rather the implementation of dark mode support. In version 6.8, dark mode wasn't applied automatically. If the system color theme is light, both versions look identical.
                    Qt 6.8.3, Dark theme, scale 100%:
                    QML_08.png
                    Qt 6.12.0, Light theme, scale 100%:
                    QML_6.12_light.png
                    Interestingly, even at 100% scale, the scrollbar looks a bit jagged in both versions, even though the scrollbar itself is straight.

                    However, Fusion.ScrollBar looks good. Thank You.
                    QML_fusion.png

                    Should I close the topic as resolved?

                    1 Reply Last reply
                    0
                    • Y Yuri has marked this topic as solved

                    • Login

                    • Login or register to search.
                    • First post
                      Last post
                    0
                    • Categories
                    • Recent
                    • Tags
                    • Popular
                    • Users
                    • Groups
                    • Search
                    • Get Qt
                    • Unsolved