Skip to content

fix: fix icon positioning with device pixel ratio#606

Open
wjyrich wants to merge 1 commit intolinuxdeepin:masterfrom
wjyrich:fix-juchi
Open

fix: fix icon positioning with device pixel ratio#606
wjyrich wants to merge 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.

@deepin-ci-robot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: 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

@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
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 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.
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.
{
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.
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 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.
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. 测试主题切换和调色板更改以确保定位稳定性
@deepin-ci-robot
Copy link
Copy Markdown
Contributor

deepin pr auto review

这段代码主要对 DQuickDciIconImage 类的布局逻辑进行了重构,从使用 Qt 的 anchors 属性改为手动计算位置,并引入了防抖定时器。以下是对该代码的审查意见,分为语法逻辑、代码质量、代码性能和代码安全四个方面。

1. 语法逻辑

  • 头文件包含风格

    • dquickdciiconimage.cpp 中添加了 #include <qpoint.h>
    • 意见:Qt 头文件通常建议使用大写形式,即 #include <QPoint>,以保持代码风格的一致性。虽然小写在大多数编译器下也能工作,但遵循官方规范更好。
  • 继承与接口实现

    • DQuickDciIconImagePrivate 继承了 QQuickItemChangeListener 并实现了 itemGeometryChanged
    • 意见:逻辑正确。在构造函数中注册监听器,在析构函数中移除,这是使用 QQuickItemChangeListener 的标准且安全的做法,防止悬空指针。
  • 布局逻辑 (layout 函数)

    • 代码逻辑:计算目标位置 -> 转换为场景坐标 -> 考虑设备像素比 (DPR) 进行四舍五入 -> 转换回本地坐标。
    • 意见:逻辑意图清晰,目的是为了在非整数缩放比例(如 1.25x, 1.5x)下,通过将图标对齐到物理像素网格来消除模糊。

2. 代码质量

  • 版权年份更新

    • dquickdciiconimage_p_p.h 中的版权年份从 2022 更新到了 2026。
    • 意见:这是一个好的实践,表示文件的维护周期。
  • 空指针检查

    • layout() 函数中检查了 parentparent->window()parent->window()->contentItem()
    • 意见:非常必要,防止了在对象未完全构建或销毁过程中访问空指针导致的崩溃。
  • 防抖机制

    • 使用 QTimer 设置了 100ms 的单次触发,在 itemGeometryChangedcomponentComplete 中启动。
    • 意见:这是一个很好的优化。在连续的几何变化(如窗口拖拽缩放)中,避免频繁触发复杂的布局计算,能显著提升 UI 响应流畅度。

3. 代码性能

  • 坐标转换开销

    • layout() 函数中进行了多次坐标映射:mapToItemmapFromItem
    • 意见:虽然这些操作比直接赋值坐标要慢,但考虑到这是为了解决高 DPI 模糊问题所做的必要牺牲,且通过定时器进行了防抖,整体性能是可以接受的。
  • 类型转换

    • qrealdouble/float 的转换以及乘除运算。
    • 意见:计算量很小,对性能影响微乎其微。

4. 代码安全

  • 资源管理

    • 潜在风险:在析构函数 ~DQuickDciIconImagePrivate() 中,调用了 q_func()。在 Qt 的私有类机制中,如果 q_func() 指向的对象(即 DQuickDciIconImage)已经被销毁,或者处于销毁过程中,访问它可能是危险的。
    • 分析:Qt 的 Q_DQ_Q 机制通常能保证生命周期的一致性。DQuickDciIconImage (q) 持有 DQuickDciIconImagePrivate (d)。当 q 销毁时,会触发 d 的析构。此时 q 的对象尚未完全析构(因为它是 d 的成员或通过智能指针管理),调用 q_func() 通常是安全的。
    • 改进建议:为了绝对安全,可以在 removeItemChangeListener 之前检查 q_func() 返回的指针是否有效(虽然 Qt 内部通常处理了这种情况,但显式检查更稳健)。或者,确保 removeItemChangeListener 的调用发生在 q 对象完全失效之前。目前的写法在标准 Qt 对象生命周期内通常是安全的。
  • Lambda 捕获

    • QObject::connect(&layoutTimer, &QTimer::timeout, qq, [this](){ layout(); });
    • 意见:Lambda 按值捕获了 this 指针(隐式捕获)。这里 qq 作为上下文对象(receiver),如果 qq 被销毁,连接会自动断开,这是安全的。如果 layout() 访问了 this 的成员,由于 this (d) 的生命周期由 qq 管理,这也是安全的。

综合改进建议

  1. 头文件命名规范
    #include <qpoint.h> 修改为 #include <QPoint>

  2. 布局逻辑的注释
    layout() 函数中的坐标转换逻辑(特别是涉及 DPR 的部分)比较复杂,建议添加注释解释为什么需要这样做(即:为了在非整数 DPR 下实现像素对齐,防止模糊)。

  3. 宏定义魔法数字
    layoutTimer.setInterval(100); 中的 100 毫秒是一个"魔法数字"。如果这个时间在项目中有通用标准,建议定义为一个常量或静态成员变量,例如 static const int LayoutUpdateDelay = 100;,便于统一维护。

  4. qreal 的使用
    layout() 中:

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

    qRound 返回的是 int(或 qint64),这里赋值给 qreal。虽然后续除法会转回浮点,但为了语义更清晰,可以先用 int 接收四舍五入的结果,再参与运算。不过目前的写法也不会报错。

    修改后的代码片段建议:

    // 建议添加注释
    // 对齐到物理像素网格,解决高分屏下的模糊问题
    int physicalX = qRound(scenePos.x() * dpr);
    int physicalY = qRound(scenePos.y() * dpr);
  5. 析构函数安全性增强(可选)
    虽然 Qt 机制保证了安全性,但为了防御性编程,可以在析构时停止定时器,防止在对象销毁的极短时间内定时器触发(虽然 qq 作为 receiver 会断开连接,但显式停止是个好习惯)。

    DQuickDciIconImagePrivate::~DQuickDciIconImagePrivate()
    {
        layoutTimer.stop(); // 显式停止
        if (auto q = q_func()) { // 检查 q 是否仍然存在
            QQuickItemPrivate::get(q)->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
        }
    }

    (注:q_func()DObjectPrivate 中通常返回原始指针,直接检查 if (q_func()) 即可,不需要 auto q = ... 除非你需要多次使用它)

总结
这段代码的改动质量很高,成功引入了防抖和高分屏像素对齐逻辑。主要需要改进的是代码规范(头文件大小写)和可读性(添加注释和消除魔法数字)。

#include <DPlatformTheme>

#include <QUrlQuery>
#include <qpoint.h>
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.

大写,

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.

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