Skip to content

Commit d65861e

Browse files
authored
[222_68] 教程卡片接入通用dpi工具类 (#3146)
1 parent 1b69bc1 commit d65861e

2 files changed

Lines changed: 187 additions & 63 deletions

File tree

devel/222_68.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
+ 首次启动教程第三步、第四步接入了 GIF 演示图
7272
+ 第二步卡片位置从 `left` 调整为 `bottom`,避免默认布局下更容易贴边
7373
+ 第三步、第四步支持“完成操作后才允许下一步”的约束
74+
+ tutorial 卡片与高亮几何全面接入 `DpiUtils`,用于适配 HiDPI 屏幕
7475

7576
### Why
7677

@@ -81,6 +82,7 @@
8182
+ 某些步骤需要更大的卡片去承载 GIF,而有些步骤只需要较小卡片
8283
+ 仅依赖 `placement` 的四向布局还不够,部分步骤需要做像素级微调
8384
+ 第三步和第四步属于操作体验步骤,如果用户没有真正完成粘贴或 OCR,就不应允许直接跳到后续步骤
85+
+ 之前 tutorial 的宽度、媒体尺寸、边距、字体和定位间距都写死为像素值,在高 DPI 屏幕上容易显得过小,且不同平台之间观感不一致
8486

8587
这些问题都不应该通过继续硬编码 UI 分支来解决,更合适的方式是把能力下沉到教程配置模型中。
8688

@@ -100,6 +102,13 @@
100102
+ `TeXmacs/plugins/tutorial/progs/init-tutorial.scm` 中新增 `tutorial-notify-action`
101103
+ `TeXmacs/progs/generic/generic-edit.scm` 中的 `kbd-magic-paste``ocr-paste``image-and-ocr-paste` 在实际执行后会上报 tutorial action
102104
+ `src/Plugins/Qt/QTMImagePopup.cpp` 中图片悬浮菜单的 OCR 按钮也会上报 `ocr-paste`,因此第四步既支持快捷键,也支持悬浮按钮解锁
105+
+ `src/Plugins/Qt/qt_tutorial.cpp` 中 tutorial 卡片、高亮与逻辑区域的固定尺寸改为统一走 `DpiUtils`
106+
卡片宽度、媒体尺寸、边距、间距、按钮圆角与内边距
107+
标题、正文、进度、按钮字体
108+
`highlight-padding``offset-x``offset-y`
109+
高亮圆角、overlay 安全边距、气泡与目标的 spacing
110+
`mainWindowSafeArea``toolbarArea``editorArea` 等逻辑区域的固定边距
111+
+ `TutorialBubble` 的字号从 stylesheet 中移出,改为 `DpiUtils::applyScaledFont()`,避免“盒子缩放了、字体仍固定 px”的双轨状态
103112
+ `TeXmacs/plugins/tutorial/data/first-launch-tutorial.json` 当前已使用这些能力:
104113
第一页 `target-id` 置空,用作无高亮欢迎页
105114
第二页 `placement` 调整为 `bottom`
@@ -132,6 +141,39 @@
132141
+ `media-path` 指向 Qt 资源路径 GIF 的演示步骤
133142
+ `on-enter + require-action` 的操作型步骤
134143

144+
## 2026/04/15 tutorial DPI 适配
145+
146+
### What
147+
148+
当前 tutorial 已从“固定像素尺寸”切换为“统一 DPI 感知尺寸”:
149+
150+
+ 卡片三档尺寸 `small / medium / large` 仍保留
151+
+ 但它们的实际像素值会在运行时经 `DpiUtils` 按当前屏幕缩放
152+
+ 同一套配置在普通屏幕和 HiDPI 屏幕上都能保持更接近的视觉比例
153+
154+
### How
155+
156+
+ `src/Plugins/Qt/qt_tutorial.cpp` 中将 tutorial 相关的基准值统一提取为常量,并通过 `DpiUtils::scaled()` 使用
157+
+ `TutorialBubble::setStep()` 每次刷新时重新应用:
158+
卡片宽度
159+
媒体尺寸
160+
layout margin / spacing
161+
按钮最小宽度、圆角、padding
162+
+ `TutorialBubble` 的标题、正文、进度、按钮字体改为 `DpiUtils::applyScaledFont()`
163+
+ `TutorialOverlay::setHighlightedRect()``bubbleRectForPlacement()``paintEvent()` 中涉及的:
164+
`highlight-padding`
165+
`offset-x / offset-y`
166+
overlay 安全边距
167+
高亮圆角
168+
卡片与高亮区域之间的距离
169+
也统一按 DPI 缩放
170+
+ `FirstLaunchTutorialController::buildRegistry()` 中逻辑区域 provider 的固定边距同样接入 `DpiUtils`
171+
172+
### Note
173+
174+
+ 由于 `offset-x / offset-y` 现在也按 DPI 缩放,某些步骤在高 DPI 屏幕上的体感偏移会比标准 DPI 更大
175+
+ 如果后续需要继续微调第三、第四步位置,应以“96 DPI 基准配置值”为准来调整 JSON
176+
135177
## 2026/04/14 首次启动教程操作型步骤
136178

137179
### What

src/Plugins/Qt/qt_tutorial.cpp

Lines changed: 145 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,73 @@ using nlohmann::json;
4343

4444
namespace {
4545

46+
constexpr int kBubbleMarginPx = 18;
47+
constexpr int kBubbleSpacingPx = 12;
48+
constexpr int kBubbleFooterSpacingPx = 10;
49+
constexpr int kBubbleBorderRadiusPx = 14;
50+
constexpr int kBubbleButtonRadiusPx = 8;
51+
constexpr int kBubbleButtonPadYPx = 8;
52+
constexpr int kBubbleButtonPadXPx = 14;
53+
constexpr int kBubbleButtonMinWidthPx = 72;
54+
constexpr int kBubbleTitleFontPx = 20;
55+
constexpr int kBubbleBodyFontPx = 16;
56+
constexpr int kBubbleProgressFontPx = 13;
57+
constexpr int kBubbleButtonFontPx = 12;
58+
constexpr int kBubbleWidthSmallPx = 300;
59+
constexpr int kBubbleWidthMediumPx = 360;
60+
constexpr int kBubbleWidthLargePx = 440;
61+
constexpr int kBubbleMediaSmallWidthPx = 240;
62+
constexpr int kBubbleMediaSmallHeightPx = 144;
63+
constexpr int kBubbleMediaMediumWidthPx = 300;
64+
constexpr int kBubbleMediaMediumHeightPx= 180;
65+
constexpr int kBubbleMediaLargeWidthPx = 380;
66+
constexpr int kBubbleMediaLargeHeightPx = 228;
67+
constexpr int kOverlayHighlightInsetPx = 8;
68+
constexpr int kOverlayBubbleSpacingPx = 18;
69+
constexpr int kOverlayBubbleMarginPx = 20;
70+
constexpr int kHighlightRadiusPx = 14;
71+
constexpr int kRegistryMainSafeMarginPx = 24;
72+
constexpr int kRegistryGapPx = 8;
73+
constexpr int kRegistryToolbarHeightPx = 72;
74+
constexpr int kRegistryHorizontalPadPx = 32;
75+
76+
QString
77+
tutorialBubbleStyleSheet () {
78+
return QString (R"(
79+
QWidget#tutorialBubble {
80+
background-color: #fffef8;
81+
border: 1px solid rgba(24, 42, 67, 0.18);
82+
border-radius: %1px;
83+
}
84+
QLabel#tutorialTitle {
85+
color: #122033;
86+
font-weight: 700;
87+
}
88+
QLabel#tutorialBodyText {
89+
color: #334155;
90+
line-height: 1.5;
91+
}
92+
QLabel#tutorialProgress {
93+
color: #6b7280;
94+
font-weight: 600;
95+
}
96+
QPushButton {
97+
border-radius: %2px;
98+
padding: %3px %4px;
99+
font-weight: 600;
100+
min-width: %5px;
101+
}
102+
QPushButton:hover {
103+
background-color: #eef4ff;
104+
}
105+
)")
106+
.arg (DpiUtils::scaled (kBubbleBorderRadiusPx))
107+
.arg (DpiUtils::scaled (kBubbleButtonRadiusPx))
108+
.arg (DpiUtils::scaled (kBubbleButtonPadYPx))
109+
.arg (DpiUtils::scaled (kBubbleButtonPadXPx))
110+
.arg (DpiUtils::scaled (kBubbleButtonMinWidthPx));
111+
}
112+
46113
QRect
47114
mapRectToWindow (QWidget* widget, QMainWindow* window) {
48115
if (widget == nullptr || window == nullptr) return QRect ();
@@ -473,15 +540,17 @@ TutorialBubble::TutorialBubble (QWidget* parent)
473540

474541
auto* footerLayout= new QHBoxLayout ();
475542
footerLayout->setContentsMargins (0, 0, 0, 0);
476-
footerLayout->setSpacing (10);
543+
footerLayout->setSpacing (DpiUtils::scaled (kBubbleFooterSpacingPx));
477544
footerLayout->addWidget (m_progressLabel);
478545
footerLayout->addStretch ();
479546
footerLayout->addWidget (m_previousButton);
480547
footerLayout->addWidget (m_nextButton);
481548

482-
auto* mainLayout= new QVBoxLayout (this);
483-
mainLayout->setContentsMargins (18, 18, 18, 18);
484-
mainLayout->setSpacing (12);
549+
auto* mainLayout = new QVBoxLayout (this);
550+
const int bubbleMargin= DpiUtils::scaled (kBubbleMarginPx);
551+
mainLayout->setContentsMargins (bubbleMargin, bubbleMargin, bubbleMargin,
552+
bubbleMargin);
553+
mainLayout->setSpacing (DpiUtils::scaled (kBubbleSpacingPx));
485554
mainLayout->setSizeConstraint (QLayout::SetFixedSize);
486555
mainLayout->addWidget (m_titleLabel);
487556
mainLayout->addWidget (m_topTextLabel);
@@ -490,39 +559,14 @@ TutorialBubble::TutorialBubble (QWidget* parent)
490559
mainLayout->addLayout (footerLayout);
491560

492561
setLayout (mainLayout);
493-
setFixedWidth (360);
494-
setStyleSheet (QStringLiteral (R"(
495-
QWidget#tutorialBubble {
496-
background-color: #fffef8;
497-
border: 1px solid rgba(24, 42, 67, 0.18);
498-
border-radius: 14px;
499-
}
500-
QLabel#tutorialTitle {
501-
color: #122033;
502-
font-size: 20px;
503-
font-weight: 700;
504-
}
505-
QLabel#tutorialBodyText {
506-
color: #334155;
507-
font-size: 16px;
508-
line-height: 1.5;
509-
}
510-
QLabel#tutorialProgress {
511-
color: #6b7280;
512-
font-size: 13px;
513-
font-weight: 600;
514-
}
515-
QPushButton {
516-
border-radius: 8px;
517-
padding: 8px 14px;
518-
font-size: 12px;
519-
font-weight: 600;
520-
min-width: 72px;
521-
}
522-
QPushButton:hover {
523-
background-color: #eef4ff;
524-
}
525-
)"));
562+
setFixedWidth (DpiUtils::scaled (kBubbleWidthMediumPx));
563+
DpiUtils::applyScaledFont (m_titleLabel, kBubbleTitleFontPx);
564+
DpiUtils::applyScaledFont (m_topTextLabel, kBubbleBodyFontPx);
565+
DpiUtils::applyScaledFont (m_bottomTextLabel, kBubbleBodyFontPx);
566+
DpiUtils::applyScaledFont (m_progressLabel, kBubbleProgressFontPx);
567+
DpiUtils::applyScaledFont (m_previousButton, kBubbleButtonFontPx);
568+
DpiUtils::applyScaledFont (m_nextButton, kBubbleButtonFontPx);
569+
setStyleSheet (tutorialBubbleStyleSheet ());
526570

527571
m_previousButton->setStyleSheet (QStringLiteral (
528572
"QPushButton { background: #f3f4f6; color: #111827; border: 1px solid "
@@ -544,20 +588,46 @@ TutorialBubble::TutorialBubble (QWidget* parent)
544588
void
545589
TutorialBubble::setStep (const TutorialStepConfig& step, int index, int total) {
546590
const QString mediaPath= resolveTutorialMediaPath (step.mediaPath);
547-
QSize mediaSize (300, 180);
591+
QSize mediaSize (DpiUtils::scaled (kBubbleMediaMediumWidthPx),
592+
DpiUtils::scaled (kBubbleMediaMediumHeightPx));
593+
auto* mainLayout= qobject_cast<QVBoxLayout*> (layout ());
594+
595+
if (mainLayout != nullptr) {
596+
const int bubbleMargin= DpiUtils::scaled (kBubbleMarginPx);
597+
mainLayout->setContentsMargins (bubbleMargin, bubbleMargin, bubbleMargin,
598+
bubbleMargin);
599+
mainLayout->setSpacing (DpiUtils::scaled (kBubbleSpacingPx));
600+
if (mainLayout->count () > 0) {
601+
if (auto* footerItem= mainLayout->itemAt (mainLayout->count () - 1)) {
602+
if (auto* footerLayout= footerItem->layout ())
603+
footerLayout->setSpacing (DpiUtils::scaled (kBubbleFooterSpacingPx));
604+
}
605+
}
606+
}
607+
608+
DpiUtils::applyScaledFont (m_titleLabel, kBubbleTitleFontPx);
609+
DpiUtils::applyScaledFont (m_topTextLabel, kBubbleBodyFontPx);
610+
DpiUtils::applyScaledFont (m_bottomTextLabel, kBubbleBodyFontPx);
611+
DpiUtils::applyScaledFont (m_progressLabel, kBubbleProgressFontPx);
612+
DpiUtils::applyScaledFont (m_previousButton, kBubbleButtonFontPx);
613+
DpiUtils::applyScaledFont (m_nextButton, kBubbleButtonFontPx);
614+
setStyleSheet (tutorialBubbleStyleSheet ());
548615

549616
switch (step.bubbleSize) {
550617
case TutorialBubbleSize::Small:
551-
setFixedWidth (300);
552-
mediaSize= QSize (240, 144);
618+
setFixedWidth (DpiUtils::scaled (kBubbleWidthSmallPx));
619+
mediaSize= QSize (DpiUtils::scaled (kBubbleMediaSmallWidthPx),
620+
DpiUtils::scaled (kBubbleMediaSmallHeightPx));
553621
break;
554622
case TutorialBubbleSize::Medium:
555-
setFixedWidth (360);
556-
mediaSize= QSize (300, 180);
623+
setFixedWidth (DpiUtils::scaled (kBubbleWidthMediumPx));
624+
mediaSize= QSize (DpiUtils::scaled (kBubbleMediaMediumWidthPx),
625+
DpiUtils::scaled (kBubbleMediaMediumHeightPx));
557626
break;
558627
case TutorialBubbleSize::Large:
559-
setFixedWidth (440);
560-
mediaSize= QSize (380, 228);
628+
setFixedWidth (DpiUtils::scaled (kBubbleWidthLargePx));
629+
mediaSize= QSize (DpiUtils::scaled (kBubbleMediaLargeWidthPx),
630+
DpiUtils::scaled (kBubbleMediaLargeHeightPx));
561631
break;
562632
}
563633

@@ -681,8 +751,13 @@ TutorialOverlay::setStep (const TutorialStepConfig& step, int index,
681751
void
682752
TutorialOverlay::setHighlightedRect (const QRect& rect, int padding) {
683753
const QRect previousHighlightRect= m_highlightRect;
684-
m_highlightRect= rect.adjusted (-padding, -padding, padding, padding)
685-
.intersected (this->rect ().adjusted (8, 8, -8, -8));
754+
const int scaledPadding = DpiUtils::scaled (padding);
755+
const int highlightInset= DpiUtils::scaled (kOverlayHighlightInsetPx);
756+
m_highlightRect= rect.adjusted (-scaledPadding, -scaledPadding, scaledPadding,
757+
scaledPadding)
758+
.intersected (this->rect ().adjusted (
759+
highlightInset, highlightInset, -highlightInset,
760+
-highlightInset));
686761
m_hasHighlight= true;
687762
repositionBubble (m_currentStep.placement);
688763
refreshExposedArea (previousHighlightRect.united (m_highlightRect));
@@ -739,10 +814,12 @@ TutorialOverlay::refreshExposedArea (const QRect& rect) {
739814

740815
QRect
741816
TutorialOverlay::bubbleRectForPlacement (TutorialPlacement placement) const {
742-
const int spacing= 18;
743-
const int margin = 20;
817+
const int spacing= DpiUtils::scaled (kOverlayBubbleSpacingPx);
818+
const int margin = DpiUtils::scaled (kOverlayBubbleMarginPx);
744819
QSize size = m_bubble->sizeHint ();
745820
QRect safe = rect ().adjusted (margin, margin, -margin, -margin);
821+
const int offsetX= DpiUtils::scaled (m_currentStep.offsetX);
822+
const int offsetY= DpiUtils::scaled (m_currentStep.offsetY);
746823

747824
auto clampRect= [&safe, &size] (QRect candidate) {
748825
int x= qBound (safe.left (), candidate.x (), safe.right () - size.width ());
@@ -754,9 +831,7 @@ TutorialOverlay::bubbleRectForPlacement (TutorialPlacement placement) const {
754831
if (!m_hasHighlight) {
755832
QPoint center=
756833
safe.center () - QPoint (size.width () / 2, size.height () / 2);
757-
return clampRect (
758-
QRect (center, size)
759-
.translated (m_currentStep.offsetX, m_currentStep.offsetY));
834+
return clampRect (QRect (center, size).translated (offsetX, offsetY));
760835
}
761836

762837
auto candidateFor= [this, &size, spacing] (TutorialPlacement p) {
@@ -797,14 +872,12 @@ TutorialOverlay::bubbleRectForPlacement (TutorialPlacement placement) const {
797872
}
798873

799874
for (TutorialPlacement p : placements) {
800-
QRect candidate= candidateFor (p).translated (m_currentStep.offsetX,
801-
m_currentStep.offsetY);
875+
QRect candidate= candidateFor (p).translated (offsetX, offsetY);
802876
if (safe.contains (candidate)) return candidate;
803877
}
804878

805879
return clampRect (
806-
candidateFor (placements.front ())
807-
.translated (m_currentStep.offsetX, m_currentStep.offsetY));
880+
candidateFor (placements.front ()).translated (offsetX, offsetY));
808881
}
809882

810883
void
@@ -832,7 +905,8 @@ TutorialOverlay::paintEvent (QPaintEvent* event) {
832905

833906
if (m_hasHighlight) {
834907
QPainterPath hole;
835-
hole.addRoundedRect (m_highlightRect, 14, 14);
908+
const int highlightRadius= DpiUtils::scaled (kHighlightRadiusPx);
909+
hole.addRoundedRect (m_highlightRect, highlightRadius, highlightRadius);
836910
overlayPath= overlayPath.subtracted (hole);
837911
}
838912

@@ -1220,7 +1294,8 @@ FirstLaunchTutorialController::buildRegistry (QMainWindow* mainWindow) const {
12201294

12211295
registry.registerRectProvider (
12221296
"mainWindowSafeArea", [] (QMainWindow* hostWindow) {
1223-
return hostWindow->rect ().adjusted (24, 24, -24, -24);
1297+
const int margin= DpiUtils::scaled (kRegistryMainSafeMarginPx);
1298+
return hostWindow->rect ().adjusted (margin, margin, -margin, -margin);
12241299
});
12251300

12261301
registry.registerRectProvider ("toolbarArea", [] (QMainWindow* hostWindow) {
@@ -1246,11 +1321,16 @@ FirstLaunchTutorialController::buildRegistry (QMainWindow* mainWindow) const {
12461321
editor->size ().isValid ()) {
12471322
QRect windowbarRect= mapRectToWindow (windowbar, hostWindow);
12481323
QRect editorRect = mapRectToWindow (editor, hostWindow);
1249-
const int top = windowbarRect.bottom () + 8;
1250-
const int bottom = qMin (editorRect.top () - 8, top + 72);
1324+
const int gap = DpiUtils::scaled (kRegistryGapPx);
1325+
const int horizontalPad= DpiUtils::scaled (kRegistryHorizontalPadPx);
1326+
const int top = windowbarRect.bottom () + gap;
1327+
const int bottom=
1328+
qMin (editorRect.top () - gap,
1329+
top + DpiUtils::scaled (kRegistryToolbarHeightPx));
12511330
if (bottom > top) {
1252-
return QRect (QPoint (32, top),
1253-
QPoint (hostWindow->rect ().right () - 32, bottom));
1331+
return QRect (
1332+
QPoint (horizontalPad, top),
1333+
QPoint (hostWindow->rect ().right () - horizontalPad, bottom));
12541334
}
12551335
}
12561336

@@ -1277,10 +1357,12 @@ FirstLaunchTutorialController::buildRegistry (QMainWindow* mainWindow) const {
12771357
guestBar->size ().isValid ()) {
12781358
QRect guestRect= mapRectToWindow (guestBar, hostWindow);
12791359
centralRect.setTop (
1280-
qMin (centralRect.bottom (), guestRect.bottom () + 8));
1360+
qMin (centralRect.bottom (),
1361+
guestRect.bottom () + DpiUtils::scaled (kRegistryGapPx)));
12811362
}
12821363

1283-
return centralRect.adjusted (8, 8, -8, -8);
1364+
const int gap= DpiUtils::scaled (kRegistryGapPx);
1365+
return centralRect.adjusted (gap, gap, -gap, -gap);
12841366
});
12851367

12861368
registry.registerRectProvider (

0 commit comments

Comments
 (0)