Skip to content

fix: fix icon positioning with device pixel ratio#606

Merged
18202781743 merged 1 commit intolinuxdeepin:masterfrom
wjyrich:fix-juchi
Apr 21, 2026
Merged

fix: fix icon positioning with device pixel ratio#606
18202781743 merged 1 commit intolinuxdeepin:masterfrom
wjyrich:fix-juchi

Conversation

@wjyrich
Copy link
Copy Markdown
Contributor

@wjyrich wjyrich commented Apr 15, 2026

Changed the layout mechanism for DCI icon images to properly handle device pixel ratio and ensure pixel-perfect positioning. Previously used anchors for centering which didn't account for DPR scaling, causing blurry or misaligned icons on high-DPI displays. Now calculates physical pixel positions and maps between coordinate systems to maintain crisp rendering.

The implementation adds a QTimer for deferred layout to avoid geometry change loops, and includes a QQuickItemChangeListener to detect geometry changes. The layout() method now computes exact pixel positions by considering window devicePixelRatio and mapping between scene and local coordinates.

Log: Fixed blurry icon rendering on high-DPI displays

Influence:

  1. Test icon rendering on displays with different DPR values (1.0, 1.5, 2.0, etc.)
  2. Verify icons remain sharp and properly centered when resizing windows
  3. Test icon positioning in various container sizes and aspect ratios
  4. Verify performance with multiple icons during window geometry changes
  5. Test theme switching and palette changes to ensure positioning stability

fix: 修复图标在高DPI显示下的定位问题

更改了DCI图标图像的布局机制,以正确处理设备像素比并确保像素级精确定位。
之前使用锚点进行居中,未考虑DPR缩放,导致在高DPI显示器上图标模糊或错位。
现在通过计算物理像素位置并在坐标系之间映射来保持清晰渲染。

实现添加了QTimer用于延迟布局以避免几何变化循环,并包含
QQuickItemChangeListener来检测几何变化。layout()方法现在通过考虑窗口 devicePixelRatio并在场景和本地坐标之间映射来计算精确的像素位置。

Log: 修复了高DPI显示器上图标渲染模糊的问题

Influence:

  1. 在不同DPR值(1.0、1.5、2.0等)的显示器上测试图标渲染
  2. 验证调整窗口大小时图标保持清晰和正确居中
  3. 测试各种容器大小和宽高比下的图标定位
  4. 验证窗口几何变化期间多个图标的性能表现
  5. 测试主题切换和调色板更改以确保定位稳定性

Summary by Sourcery

Improve DCI icon layout to achieve pixel-perfect centering on high-DPI displays.

Bug Fixes:

  • Correct icon centering to avoid blurriness and misalignment on high-DPI displays by accounting for device pixel ratio.

Enhancements:

  • Replace anchor-based centering with explicit pixel-aware positioning using mapped coordinates between parent, scene, and content items.
  • Defer layout recalculation using a timer and geometry change listener to handle window and item geometry updates without feedback loops.

Tests:

  • Requires verification of icon sharpness and centering across different DPR values, window resize scenarios, container sizes, and theme changes.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 15, 2026

Reviewer's Guide

Refactors DQuickDciIconImage layout from anchor-based centering to a DPR-aware, coordinate-mapping based manual layout triggered via geometry-change-driven deferred timers, to ensure pixel-perfect icon positioning on high-DPI displays without geometry feedback loops.

Sequence diagram for deferred DPR-aware icon layout on geometry changes

sequenceDiagram
    participant ParentItem as QQuickItem_parent
    participant QQPrivate as QQuickItemPrivate
    participant Listener as DQuickDciIconImagePrivate
    participant LT as QTimer_layoutTimer
    participant IconItem as DQuickIconImage
    participant Window as QQuickWindow
    participant ContentItem as QQuickItem_contentItem

    ParentItem->>QQPrivate: geometryChanged
    QQPrivate->>Listener: itemGeometryChanged(item, change, geom)
    activate Listener
    Listener->>LT: setSingleShot(true), setInterval(100)
    Listener->>LT: start()
    deactivate Listener

    LT-->>Listener: timeout()
    activate Listener
    Listener->>Listener: layout()

    alt parentItem or window missing
        Listener-->>Listener: return
    else perform_centering
        Listener->>IconItem: parentItem = parentItem()
        Listener->>ParentItem: width(), height()
        Listener->>IconItem: width(), height()
        Listener->>Listener: compute targetX, targetY (logical center)
        Listener->>Window: window()
        Window->>ContentItem: contentItem()
        Listener->>ParentItem: mapToItem(ContentItem, targetPoint)
        Listener->>Window: devicePixelRatio()
        Listener->>Listener: physicalX, physicalY = round(scenePos * dpr)
        Listener->>ParentItem: mapFromItem(ContentItem, physicalX / dpr, scenePos.y)
        Listener->>ParentItem: mapFromItem(ContentItem, scenePos.x, physicalY / dpr)
        Listener->>IconItem: setX(localPosX.x)
        Listener->>IconItem: setY(localPosY.y)
    end
    deactivate Listener
Loading

Sequence diagram for component completion and initial deferred layout

sequenceDiagram
    participant QmlEngine as QML_Engine
    participant Icon as DQuickDciIconImage
    participant Private as DQuickDciIconImagePrivate

    QmlEngine->>Icon: componentComplete()
    activate Icon
    Icon->>Private: imageItem->componentComplete()
    Icon->>Icon: QQuickItem::componentComplete()
    Icon->>Icon: QTimer::singleShot(200, this, lambda)
    deactivate Icon

    Icon-->>Private: deferred_lambda_invoked_after_200ms
    activate Private
    Private->>Private: layout()
    deactivate Private
Loading

Class diagram for updated DQuickDciIconImage layout handling

classDiagram
    class DQuickDciIconImage {
        +componentComplete() void
    }

    class DQuickDciIconImagePrivate {
        +DQuickDciIconImagePrivate(qq DQuickDciIconImage*)
        +~DQuickDciIconImagePrivate() void
        +layout() void
        +updateImageSourceUrl() void
        +play(mode DQMLGlobalObject_ControlState) void
        +itemGeometryChanged(item QQuickItem*, change QQuickGeometryChange, geom QRectF) void
        -- attributes --
        +palette DDciIconPalette
        +imageItem DQuickIconImage*
        +mode DQMLGlobalObject_ControlState
        +theme DGuiApplicationHelper_ColorType
        +fallbackToQIcon bool
        +layoutTimer QTimer
    }

    class DQuickDciIconImageItemPrivate {
        +DQuickDciIconImageItemPrivate(parent DQuickDciIconImagePrivate*)
        -- attributes --
        +controlLayers QRect
        +iconPathCache QString
    }

    class DQuickIconImage {
    }

    class QQuickItemChangeListener {
        <<interface>> QQuickItemChangeListener
        +itemGeometryChanged(item QQuickItem*, change QQuickGeometryChange, geom QRectF) void
    }

    class QTimer {
        +setSingleShot(singleShot bool) void
        +setInterval(msec int) void
        +start() void
    }

    DQuickDciIconImagePrivate ..> DQuickDciIconImage : q_ptr
    DQuickDciIconImagePrivate ..> DQuickIconImage : imageItem
    DQuickDciIconImagePrivate ..> QTimer : layoutTimer
    DQuickDciIconImageItemPrivate --|> DQuickIconImagePrivate
    DQuickDciIconImagePrivate --|> DObjectPrivate
    DQuickDciIconImagePrivate --|> QQuickItemChangeListener
    DQuickDciIconImage o-- DQuickDciIconImagePrivate
    DQuickDciIconImagePrivate o-- DQuickDciIconImageItemPrivate
    DQuickDciIconImage ..> QTimer : uses_singleShot_for_deferred_layout
Loading

File-Level Changes

Change Details Files
Replace anchor-based centering with manual, devicePixelRatio-aware layout computation for the icon item.
  • Remove reliance on QQuickItemPrivate anchors()->setCenterIn for centering the icon image within its parent.
  • Compute target logical center coordinates based on parent and icon sizes, rounding to integer pixels.
  • Map target position through the window content item to scene coordinates, apply devicePixelRatio, and round to nearest physical pixel.
  • Map the snapped physical pixel position back to the parent’s local coordinates and set the icon item’s x/y explicitly.
src/private/dquickdciiconimage.cpp
Introduce deferred layout and geometry-change listening to avoid feedback loops and keep layout stable across geometry updates.
  • Make DQuickDciIconImagePrivate implement QQuickItemChangeListener and register it for QQuickItemPrivate::Geometry changes in the constructor.
  • On itemGeometryChanged, start a single-shot QTimer to perform layout after a short delay instead of immediately.
  • Initialize and configure layoutTimer (single-shot, 100 ms) and connect its timeout to invoke layout().
  • Add a destructor to unregister the item change listener from QQuickItemPrivate to avoid dangling listeners.
  • Defer the initial layout call after componentComplete using QTimer::singleShot with a 200 ms delay.
src/private/dquickdciiconimage.cpp
src/private/dquickdciiconimage_p_p.h
Extend the private implementation to support the new layout mechanism dependencies.
  • Include QQuickItemChangeListener, QQuickItemPrivate, and QTimer headers in the private header file.
  • Extend DQuickDciIconImagePrivate to inherit from QQuickItemChangeListener and declare itemGeometryChanged override.
  • Add QTimer layoutTimer as a member of DQuickDciIconImagePrivate for deferred layout scheduling.
src/private/dquickdciiconimage_p_p.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In itemGeometryChanged, the QQuickItem*, QQuickGeometryChange, and QRectF parameters are unused; consider omitting their names or using Q_UNUSED to avoid warnings and make this explicit.
  • The layout() method calls mapFromItem twice to compute X and Y separately; you could simplify and reduce work by mapping a single rounded point (physicalX / dpr, physicalY / dpr) and using that for both coordinates.
  • There are now two deferred layout mechanisms (the layoutTimer and the QTimer::singleShot(200, ...) in componentComplete); consider whether both are necessary or if they can be unified to reduce timing complexity and potential race conditions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `itemGeometryChanged`, the `QQuickItem*`, `QQuickGeometryChange`, and `QRectF` parameters are unused; consider omitting their names or using `Q_UNUSED` to avoid warnings and make this explicit.
- The `layout()` method calls `mapFromItem` twice to compute X and Y separately; you could simplify and reduce work by mapping a single rounded point `(physicalX / dpr, physicalY / dpr)` and using that for both coordinates.
- There are now two deferred layout mechanisms (the `layoutTimer` and the `QTimer::singleShot(200, ...)` in `componentComplete`); consider whether both are necessary or if they can be unified to reduce timing complexity and potential race conditions.

## Individual Comments

### Comment 1
<location path="src/private/dquickdciiconimage.cpp" line_range="181" />
<code_context>
     : DObjectPrivate(qq)
     , imageItem(new DQuickIconImage(*new DQuickDciIconImageItemPrivate(this), qq))
 {
+    QQuickItemPrivate::get(qq)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
+    layoutTimer.setSingleShot(true);
+    layoutTimer.setInterval(100);
</code_context>
<issue_to_address>
**suggestion:** Use consistent change flags when adding and removing the item change listener.

The listener is added with `QQuickItemPrivate::Geometry` but removed with `QQuickItemPrivate::AllChanges`. Even if this works (e.g. if the mask is ignored or `AllChanges` is treated as a superset), it’s clearer and safer to use the same flag for both operations, or to add a short comment explaining why `AllChanges` is required on removal.

Suggested implementation:

```cpp
    QQuickItemPrivate::get(qq)->addItemChangeListener(this, QQuickItemPrivate::Geometry);

```

```cpp
    QQuickItemPrivate::get(qq)->removeItemChangeListener(this, QQuickItemPrivate::Geometry);

```

1. If there are multiple constructors or initialization paths for this class, ensure they all use `QQuickItemPrivate::Geometry` when calling `addItemChangeListener`.
2. Likewise, verify that every destructor or teardown path that calls `removeItemChangeListener` uses `QQuickItemPrivate::Geometry` to keep the semantics consistent.
3. If for some reason `AllChanges` was originally required (e.g. a known Qt quirk), consider adding a brief comment near these calls to document that, instead of changing the flag.
</issue_to_address>

### Comment 2
<location path="src/private/dquickdciiconimage.cpp" line_range="213" />
<code_context>
+    if (!parent)
+        return;
+
+    qreal targetX = qRound(parent->width() / 2.0) - qRound(imageItem->width() / 2.0);
+    qreal targetY = qRound(parent->height() / 2.0) - qRound(imageItem->height() / 2.0);
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider mapping and rounding the DPR-adjusted position in a single step instead of per-axis mapping.

Rounding and mapping X and Y independently (with mixed coordinates in two `mapFromItem` calls) assumes the transform is separable and can become incorrect when rotation, shear, or non-uniform scaling is involved. Instead, build a single DPR-adjusted `QPointF` in scene coordinates and call `mapFromItem` once, using both components of that result to keep the transform coherent.

Suggested implementation:

```cpp
    QQuickItem *parent = imageItem->parentItem();
    if (!parent)
        return;

    // Compute a single DPR-adjusted target position to keep the transform coherent
    const qreal targetX = qRound(parent->width() / 2.0 - imageItem->width() / 2.0);
    const qreal targetY = qRound(parent->height() / 2.0 - imageItem->height() / 2.0);
    const QPointF dprAdjustedTarget(targetX, targetY);

    // Map the full point in one go instead of per-axis to avoid mixing coordinate spaces
    const QPointF mappedTarget = parent->mapFromItem(imageItem, dprAdjustedTarget);
    imageItem->setPosition(mappedTarget);


```

I only see a small portion of `layout()`, so if there is existing code below this that:
1. Separately calls `mapFromItem`/`mapToItem` for X and Y, or
2. Performs additional DPR rounding per-axis,

you should remove or adjust that code to rely on the new `mappedTarget` point and `setPosition()` so that the full transform (including any rotation, shear, or non-uniform scaling) is applied consistently in a single mapping step.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/private/dquickdciiconimage.cpp Outdated
Comment thread src/private/dquickdciiconimage.cpp Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the DCI icon image layout logic to achieve pixel-perfect centering on high-DPI displays by replacing anchor-based centering with DPR-aware coordinate mapping and deferred relayout on geometry changes.

Changes:

  • Added a QQuickItemChangeListener to detect geometry changes and trigger relayout.
  • Replaced anchors()->setCenterIn(...) with explicit pixel-aware positioning using scene/content mapping + DPR rounding.
  • Introduced a QTimer to defer layout recalculation and avoid geometry feedback loops.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/private/dquickdciiconimage_p_p.h Adds Qt Quick change-listener + timer members to support deferred, geometry-driven layout.
src/private/dquickdciiconimage.cpp Implements deferred layout triggering and DPR-aware centering logic in layout().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/private/dquickdciiconimage.cpp Outdated
Comment on lines +222 to +225
qreal dpr = parent->window()->devicePixelRatio();
qreal physicalX = std::round(scenePos.x() * dpr);
qreal physicalY = std::round(scenePos.y() * dpr);
QPointF localPos = parent->mapFromItem(contentItem, QPointF(physicalX / dpr, physicalY / dpr));
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::round is used here but this translation unit does not include . Relying on indirect includes is brittle and can break compilation on some platforms/toolchains; please add the appropriate standard header (or switch to Qt rounding helpers).

Copilot uses AI. Check for mistakes.
Comment thread src/private/dquickdciiconimage.cpp Outdated
QQuickItem *contentItem = parent->window()->contentItem();
QPointF scenePos = parent->mapToItem(contentItem, QPointF(targetX, targetY));

qreal dpr = parent->window()->devicePixelRatio();
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses QQuickWindow::devicePixelRatio(), but elsewhere in this codebase icon rendering uses QQuickWindow::effectiveDevicePixelRatio() (e.g., src/private/dquickiconimage.cpp:150-154). Using a different DPR source can reintroduce positioning mismatches under additional scale factors; consider using effectiveDevicePixelRatio() here to stay consistent.

Suggested change
qreal dpr = parent->window()->devicePixelRatio();
qreal dpr = parent->window()->effectiveDevicePixelRatio();

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/private/dquickdciiconimage.cpp Outdated
Comment on lines +213 to +214
qreal targetX = qRound(parent->width() / 2.0) - qRound(imageItem->width() / 2.0);
qreal targetY = qRound(parent->height() / 2.0) - qRound(imageItem->height() / 2.0);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targetX/targetY are rounded in logical coordinates and then the mapped scene position is rounded again in physical pixels. This double-rounding can shift the center by 1px (especially for non-integer DPR like 1.5). To keep “pixel-perfect” behavior, compute the center using full-precision qreal values and only round once in physical pixel space.

Suggested change
qreal targetX = qRound(parent->width() / 2.0) - qRound(imageItem->width() / 2.0);
qreal targetY = qRound(parent->height() / 2.0) - qRound(imageItem->height() / 2.0);
qreal targetX = (parent->width() - imageItem->width()) / 2.0;
qreal targetY = (parent->height() - imageItem->height()) / 2.0;

Copilot uses AI. Check for mistakes.
Comment thread src/private/dquickdciiconimage.cpp Outdated
{
QQuickItemPrivate::get(qq)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
layoutTimer.setSingleShot(true);
layoutTimer.setInterval(100);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

layoutTimer is configured with a hard-coded 100ms delay. This means the initial layout in componentComplete() and subsequent resize-driven relayouts can visibly “lag” by up to 100ms, leaving the icon temporarily mispositioned. Consider using a 0ms single-shot (event-loop coalescing) or triggering an immediate layout() and only deferring subsequent updates.

Suggested change
layoutTimer.setInterval(100);
layoutTimer.setInterval(0);

Copilot uses AI. Check for mistakes.
Comment thread src/private/dquickdciiconimage.cpp Outdated
qreal targetX = qRound(parent->width() / 2.0) - qRound(imageItem->width() / 2.0);
qreal targetY = qRound(parent->height() / 2.0) - qRound(imageItem->height() / 2.0);

if (!parent->window() || !parent->window()->contentItem()) {
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

layout() bails out when window()/contentItem() is not available, and the only triggers are the geometry listener and the one-shot timer started in componentComplete(). If the item gets a window after this initial attempt (without a geometry change), layout may never run. Consider restarting layoutTimer (or calling layout) from QQuickItem::windowChanged as well.

Suggested change
if (!parent->window() || !parent->window()->contentItem()) {
if (!parent->window() || !parent->window()->contentItem()) {
static const char kWindowChangedConnectionPendingProperty[] = "_d_dciIconImageWindowChangedConnectionPending";
if (!parent->property(kWindowChangedConnectionPendingProperty).toBool()) {
parent->setProperty(kWindowChangedConnectionPendingProperty, true);
QObject::connect(parent, &QQuickItem::windowChanged, q_func(),
[this, parent] {
parent->setProperty("_d_dciIconImageWindowChangedConnectionPending", false);
layoutTimer.start();
},
Qt::SingleShotConnection);
}

Copilot uses AI. Check for mistakes.
Comment thread src/private/dquickdciiconimage.cpp Outdated
Comment on lines +202 to +205
void DQuickDciIconImagePrivate::itemGeometryChanged(QQuickItem *, QQuickGeometryChange, const QRectF &)
{
layoutTimer.start();
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relayout is only scheduled on the wrapper item's geometry changes. With anchors removed, changes in imageItem size (e.g., when the source loads, sourceSize changes, or implicit size updates) will no longer automatically re-center the icon. Consider also triggering layoutTimer when imageItem width/height (or implicit size) changes to keep centering correct.

Copilot uses AI. Check for mistakes.
@wjyrich wjyrich force-pushed the fix-juchi branch 2 times, most recently from e3d3f9f to 6e59361 Compare April 16, 2026 02:16
Comment thread src/private/dquickdciiconimage.cpp Outdated
Comment thread src/private/dquickdciiconimage.cpp Outdated
QQuickItem *contentItem = parent->window()->contentItem();
QPointF scenePos = parent->mapToItem(contentItem, QPointF(targetX, targetY));

qreal dpr = parent->window()->devicePixelRatio();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wjyrich wjyrich force-pushed the fix-juchi branch 3 times, most recently from 91c49a2 to 67a1bb5 Compare April 20, 2026 08:33
18202781743
18202781743 previously approved these changes Apr 20, 2026
The layout method for DQuickDciIconImage has been completely rewritten
to fix positioning issues, particularly on high-DPI displays.
Previously, the image was simply centered using anchors, which could
cause misalignment. The new implementation calculates precise pixel-
aligned positions by considering device pixel ratios and scene
transformations. A timer-based scheduling mechanism was added to prevent
excessive layout recalculations. Additionally, smooth rendering is now
disabled by default during initialization to improve performance, and
geometry/parent change handlers ensure proper layout updates.

Log: Fixed DCI icon positioning issues on high-DPI displays

Influence:
1. Test DCI icon display on monitors with different DPI settings
2. Verify icon centering remains correct when parent item size changes
3. Test icon positioning when moving between windows or screens
4. Verify smooth property is properly initialized to false
5. Test layout updates when icon image dimensions change
6. Verify no performance regression from timer-based layout scheduling

fix: 修复 DCI 图标图像定位和布局问题

DQuickDciIconImage 的布局方法已完全重写,以修复定位问题,特别是在高 DPI
显示器上。之前,图像仅使用锚点居中,可能导致错位。新实现通过考虑设备像素
比和场景变换来计算精确的像素对齐位置。添加了基于定时器的调度机制以防止过
多的布局重新计算。此外,初始化期间默认禁用平滑渲染以提高性能,几何/父项
变更处理程序确保正确的布局更新。

Log: 修复高 DPI 显示器上的 DCI 图标定位问题

Influence:
1. 在不同 DPI 设置的显示器上测试 DCI 图标显示
2. 验证父项大小变化时图标居中保持正确
3. 测试在窗口或屏幕间移动时的图标定位
4. 验证 smooth 属性是否正确初始化为 false
5. 测试图标图像尺寸变化时的布局更新
6. 验证基于定时器的布局调度不会导致性能下降
@deepin-ci-robot
Copy link
Copy Markdown
Contributor

deepin pr auto review

这段代码主要修改了 DQuickDciIconImage 的布局逻辑,从使用 QML 的 anchors 居中改为手动计算坐标,并引入了定时器来防抖动(debounce)布局计算。此外,还增加了对 Qt5 和 Qt6 的兼容性处理。

以下是对该代码的审查意见,分为语法逻辑、代码质量、代码性能和代码安全四个方面:

1. 语法逻辑

  • 类型一致性
    layout() 函数中:

    qreal physicalX = qRound(scenePos.x() * dpr);
    qreal physicalY = qRound(scenePos.y() * dpr);

    qRound 返回的是 int 类型,但这里赋值给了 qreal。虽然编译器会进行隐式转换,但为了保持逻辑清晰(特别是后续又除以 dpr),建议显式声明为 int 或者在计算时直接使用 int
    改进建议

    int physicalX = qRound(scenePos.x() * dpr);
    int physicalY = qRound(scenePos.y() * dpr);
  • 空指针检查
    scheduleLayout() 中,layoutTimer 是动态创建的。虽然逻辑上没问题,但 layoutTimer 作为成员变量,建议在构造函数中初始化为 nullptr(代码中已使用 = nullptr,这是好的做法)。

  • Lambda 捕获
    在构造函数中:

    connect(this, &QQuickItem::scaleChanged, this, [this]() { setSmooth(!qFuzzyCompare(scale(), 1.0)); });

    这里捕获了 this。由于 DQuickDciIconImage 继承自 QObject,连接类型默认为 AutoConnection。如果在多线程环境下(虽然 QML 通常是单线程 UI),这种写法是安全的,但值得注意。

2. 代码质量

  • 布局逻辑的清晰度
    layout() 函数现在的逻辑是:计算目标位置 -> 映射到场景坐标 -> 根据设备像素比取整 -> 映射回本地坐标。
    这段代码的意图是子像素对齐(Sub-pixel alignment),防止图标在高分屏上模糊。
    改进建议:建议添加注释解释为什么要进行 mapToScene -> qRound -> mapFromScene 这一复杂的转换过程,以便后续维护者理解这是为了对齐物理像素网格。

  • 定时器管理
    scheduleLayout() 中创建 QTimer 的逻辑略显分散。
    改进建议:可以在 DQuickDciIconImagePrivate 的构造函数中初始化 layoutTimer,并连接信号,这样 scheduleLayout 只需要负责 start(),逻辑更集中。

    // 在构造函数中
    layoutTimer = new QTimer(q);
    layoutTimer->setSingleShot(true);
    layoutTimer->setInterval(50);
    QObject::connect(layoutTimer, &QTimer::timeout, q, [this]() { layout(); });
    
    // scheduleLayout 中
    if (layoutTimer) {
        layoutTimer->start();
    }
  • 重复的宏定义
    geometryChanged (Qt5) 和 geometryChange (Qt6) 的处理逻辑完全一致。
    改进建议:可以考虑定义一个通用的私有方法(例如 handleGeometryChange),在这两个函数中分别调用它,减少重复代码。

3. 代码性能

  • 频繁的坐标映射
    layout() 中调用了 mapToScenemapFromScene。这两个操作在 Qt 内部涉及矩阵运算。虽然对于单个图标来说开销不大,但如果在大量图标同时刷新(例如列表视图快速滚动)时,可能会有性能影响。
    改进建议:目前的实现是为了画质(抗锯齿/模糊)而牺牲了一点点性能,这在 UI 组件中通常是值得的。只需确保不要在 paint 或高频回调中无必要地触发 layout()。目前的 scheduleLayout (50ms 防抖) 很好地缓解了这个问题。

  • 连接信号的开销
    构造函数中连接了 widthChanged, heightChanged, scaleChanged。这意味着每次尺寸微调都会触发定时器。
    改进建议:目前的防抖机制(50ms)已经很好地处理了这个问题,性能方面是可以接受的。

4. 代码安全

  • 对象生命周期
    layoutTimer 的父对象被设置为 q (DQuickDciIconImage)。这是安全的,因为当 q 析构时,layoutTimer 也会被自动删除。

  • 潜在的递归或死循环
    layout() 中修改了 imageItem 的位置。

    imageItem->setPosition(localPos);

    需要确认 imageItem 的位置变化不会反过来触发 DQuickDciIconImagegeometryChanged 信号,否则可能导致 scheduleLayout 被无限触发。
    分析imageItemDQuickDciIconImage 的子元素,修改子元素的位置通常不会改变父元素(this)的几何形状,因此不会触发父元素的 geometryChanged。逻辑是安全的。

  • 线程安全
    Qt GUI 相关操作必须在主线程进行。代码中使用了 QTimer,它默认在创建它的线程(主线程)触发,因此是安全的。

总结与修改建议代码片段

建议对 layout() 函数添加注释并修正变量类型,同时优化定时器的初始化位置。

修改后的 layout() 示例:

void DQuickDciIconImagePrivate::layout()
{
    Q_Q(DQuickDciIconImage);
    if (!q->isComponentComplete())
        return;

    // 1. 计算理想的居中位置
    qreal targetX = (q->width() - imageItem->width()) / 2.0;
    qreal targetY = (q->height() - imageItem->height()) / 2.0;

    if (!q->parentItem() || !q->window()) {
        imageItem->setPosition(QPointF(targetX, targetY));
        return;
    }

    // 2. 映射到场景坐标以处理变换和窗口偏移
    QPointF scenePos = q->mapToScene(QPointF(targetX, targetY));
    
    // 3. 获取设备像素比,用于物理像素对齐
    qreal dpr = q->window()->effectiveDevicePixelRatio();

    // 4. 对齐到物理像素网格,防止模糊
    // 显式使用 int 接收 qRound 结果
    int physicalX = qRound(scenePos.x() * dpr);
    int physicalY = qRound(scenePos.y() * dpr);

    // 5. 映射回本地坐标
    QPointF localPos = q->mapFromScene(QPointF(physicalX / dpr, physicalY / dpr));

    imageItem->setPosition(localPos);
}

修改后的 DQuickDciIconImagePrivate 构造函数片段(建议):

DQuickDciIconImagePrivate::DQuickDciIconImagePrivate(DQuickDciIconImage *qq)
    : DObjectPrivate(qq)
{
    // 初始化定时器
    layoutTimer = new QTimer(qq);
    layoutTimer->setSingleShot(true);
    layoutTimer->setInterval(50);
    QObject::connect(layoutTimer, &QTimer::timeout, qq, [this]() {
        layout();
    });
    
    // ... 其他初始化代码
}

修改后的 scheduleLayout(简化版):

void DQuickDciIconImagePrivate::scheduleLayout()
{
    if (layoutTimer) {
        layoutTimer->start();
    }
}

@deepin-ci-robot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: 18202781743, wjyrich

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@18202781743 18202781743 merged commit 4e7d2e9 into linuxdeepin:master Apr 21, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants