Skip to content

Commit 3efc8ef

Browse files
authored
[0129] 优化 PDF 阅读器缩放策略 (#3373)
1 parent 141a90b commit 3efc8ef

3 files changed

Lines changed: 154 additions & 9 deletions

File tree

devel/0129.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,91 @@ static constexpr int PRELOAD_MARGIN = 200; // 预加载边距(像素)
177177
- `pageCache_` 的总内存消耗约为 `sum(pixmap.width * pixmap.height * 4)` 字节。对于 100 页、每页 1000x1400 的 PDF,全缓存约 560MB。暂不设 LRU 上限,因 Mogan 以 STEM 文档为主,PDF 为辅助阅读,通常不会同时打开超大 PDF。若后续需要,可引入 Okular 式的距离排序淘汰。
178178
- MuPDF 的 `fz_context` 非线程安全,若未来引入多线程异步渲染,需为每个线程创建独立 context。
179179
- 防抖定时器使用 `Qt::PreciseTimer` 还是 `Qt::CoarseTimer` 对体验影响不大,默认即可。
180+
181+
---
182+
183+
## 8 缩放步进策略优化(第二期)
184+
185+
### 8.1 现状与问题
186+
187+
当前缩放步进为固定 `10%`(`ZOOM_STEP = 0.1`),存在以下不合理之处:
188+
189+
1. **固定步进体验差**:在 100% 时放大到 110%,相对变化约 10%;但在 400% 时放大到 410%,相对变化仅约 2.5%,几乎看不出差别。
190+
2. **缺乏离散的缩放级别列表**:Okular 等成熟阅读器使用预定义缩放级别列表,放大/缩小是跳到列表中的上一级/下一级。
191+
3. **滚轮缩放过于生硬**:每次滚轮事件直接 `+/- 0.1`,没有平滑的连续缩放感。
192+
4. **缩放上限过低**:`MAX_ZOOM = 5.0`(500%),对于需要放大查看细节的 PDF 不够。
193+
194+
### 8.2 Okular 的策略参考
195+
196+
Okular 的实现要点:
197+
198+
1. **固定缩放级别列表**(16 级):
199+
```cpp
200+
static const std::array<float, 16> kZoomValues {
201+
0.12, 0.25, 0.33, 0.50, 0.66, 0.75,
202+
1.00, 1.25, 1.50, 2.00, 4.00, 8.00,
203+
16.00, 25.00, 50.00, 100.00
204+
};
205+
```
206+
2. **放大/缩小按钮**:跳转到 `kZoomValues` 中最接近当前值的上一级/下一级。
207+
3. **Ctrl + 滚轮**:连续缩放,公式为 `newZoom = zoomFactor * (1.0 + delta / 500.0)`,以光标位置为中心。
208+
4. **下拉框**:同步显示 Fit Width / Fit Page / Auto Fit 以及标准级别列表。
209+
210+
### 8.3 改进方案
211+
212+
#### 8.3.1 引入缩放级别列表
213+
214+
定义一组固定的缩放级别(12 级),覆盖 25% ~ 800%:
215+
216+
```
217+
0.25, 0.33, 0.50, 0.75,
218+
1.00, 1.25, 1.50, 2.00,
219+
3.00, 4.00, 6.00, 8.00
220+
```
221+
222+
放大/缩小操作(工具栏按钮或菜单)时,跳转到列表中**最接近当前比例的上一级/下一级**
223+
224+
#### 8.3.2 滚轮缩放改为连续缩放
225+
226+
将当前固定步进 `+/- ZOOM_STEP` 改为连续公式:
227+
228+
```cpp
229+
zoomFactor_ *= (1.0 + delta / 500.0);
230+
```
231+
232+
- 向上滚动(`delta > 0`):比例增大
233+
- 向下滚动(`delta < 0`):比例减小
234+
- 更符合用户直觉,Zoom、浏览器等主流软件均采用类似策略
235+
236+
#### 8.3.3 扩大缩放范围
237+
238+
- `MIN_ZOOM``0.1`(10%)改为 `0.12`(12%)
239+
- `MAX_ZOOM``5.0`(500%)改为 `8.0`(800%)
240+
241+
#### 8.3.4 工具栏增加放大/缩小按钮
242+
243+
在工具栏的 zoomCombo 两侧增加 `+` / `-` 按钮:
244+
- `-` 按钮:调用 `zoomOut()`,跳转到列表中的下一级(更小的比例)
245+
- `+` 按钮:调用 `zoomIn()`,跳转到列表中的上一级(更大的比例)
246+
247+
#### 8.3.5 下拉框同步更新
248+
249+
下拉框选项调整为:
250+
```
251+
Fit Width, Fit Height,
252+
25%, 33%, 50%, 75%,
253+
100%, 125%, 150%, 200%,
254+
300%, 400%, 600%, 800%
255+
```
256+
257+
#### 8.3.6 新增快捷键
258+
259+
增加键盘快捷键支持:
260+
- `Ctrl + +` / `Ctrl + =`:放大(zoom in)
261+
- `Ctrl + -`:缩小(zoom out)
262+
- `Ctrl + 0`:重置为 100%
263+
264+
### 8.4 实现文件
265+
266+
- `src/Plugins/Qt/qt_pdf_reader_widget.hpp`
267+
- `src/Plugins/Qt/qt_pdf_reader_widget.cpp`

src/Plugins/Qt/qt_pdf_reader_widget.cpp

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ PDFReaderWidget::setupToolBar () {
101101

102102
zoomCombo_->addItem ("Fit Width");
103103
zoomCombo_->addItem ("Fit Height");
104+
zoomCombo_->addItem ("25%");
105+
zoomCombo_->addItem ("33%");
104106
zoomCombo_->addItem ("50%");
105107
zoomCombo_->addItem ("75%");
106108
zoomCombo_->addItem ("100%");
@@ -109,11 +111,23 @@ PDFReaderWidget::setupToolBar () {
109111
zoomCombo_->addItem ("200%");
110112
zoomCombo_->addItem ("300%");
111113
zoomCombo_->addItem ("400%");
114+
zoomCombo_->addItem ("600%");
115+
zoomCombo_->addItem ("800%");
112116

113117
connect (zoomCombo_, QOverload<int>::of (&QComboBox::currentIndexChanged),
114118
this, &PDFReaderWidget::onZoomChanged);
115119

120+
zoomOutBtn_= new QPushButton ("-", toolBar_);
121+
zoomOutBtn_->setFixedWidth (30);
122+
connect (zoomOutBtn_, &QPushButton::clicked, this, &PDFReaderWidget::zoomOut);
123+
124+
zoomInBtn_= new QPushButton ("+", toolBar_);
125+
zoomInBtn_->setFixedWidth (30);
126+
connect (zoomInBtn_, &QPushButton::clicked, this, &PDFReaderWidget::zoomIn);
127+
128+
toolBar_->addWidget (zoomOutBtn_);
116129
toolBar_->addWidget (zoomCombo_);
130+
toolBar_->addWidget (zoomInBtn_);
117131
toolBar_->addSeparator ();
118132

119133
prevPageBtn_= new QPushButton ("<", toolBar_);
@@ -216,6 +230,28 @@ PDFReaderWidget::fitHeight () {
216230
setZoomFactor (static_cast<double> (viewportHeight) / baseHeight);
217231
}
218232

233+
void
234+
PDFReaderWidget::zoomIn () {
235+
for (int i= 0; i < ZOOM_LEVEL_COUNT; ++i) {
236+
if (ZOOM_LEVELS[i] > zoomFactor_ * 1.001) {
237+
setZoomFactor (ZOOM_LEVELS[i]);
238+
return;
239+
}
240+
}
241+
setZoomFactor (MAX_ZOOM);
242+
}
243+
244+
void
245+
PDFReaderWidget::zoomOut () {
246+
for (int i= ZOOM_LEVEL_COUNT - 1; i >= 0; --i) {
247+
if (ZOOM_LEVELS[i] < zoomFactor_ * 0.999) {
248+
setZoomFactor (ZOOM_LEVELS[i]);
249+
return;
250+
}
251+
}
252+
setZoomFactor (MIN_ZOOM);
253+
}
254+
219255
int
220256
PDFReaderWidget::currentPage () const {
221257
if (!scrollArea_ || pageCount_ <= 0) return 0;
@@ -715,6 +751,24 @@ PDFReaderWidget::keyPressEvent (QKeyEvent* event) {
715751
return;
716752
}
717753

754+
if (event->modifiers () & Qt::ControlModifier) {
755+
switch (event->key ()) {
756+
case Qt::Key_Plus:
757+
case Qt::Key_Equal:
758+
zoomIn ();
759+
event->accept ();
760+
return;
761+
case Qt::Key_Minus:
762+
zoomOut ();
763+
event->accept ();
764+
return;
765+
case Qt::Key_0:
766+
setZoomFactor (1.0);
767+
event->accept ();
768+
return;
769+
}
770+
}
771+
718772
QWidget::keyPressEvent (event);
719773
}
720774

@@ -726,12 +780,8 @@ PDFReaderWidget::eventFilter (QObject* watched, QEvent* event) {
726780
if (wheelEvent->modifiers () & Qt::ControlModifier) {
727781
int delta= wheelEvent->angleDelta ().y ();
728782
if (delta != 0) {
729-
if (delta > 0) {
730-
zoomFactor_= qMin (zoomFactor_ + ZOOM_STEP, MAX_ZOOM);
731-
}
732-
else {
733-
zoomFactor_= qMax (zoomFactor_ - ZOOM_STEP, MIN_ZOOM);
734-
}
783+
double factor= 1.0 + static_cast<double> (delta) / 500.0;
784+
zoomFactor_ = qBound (MIN_ZOOM, zoomFactor_ * factor, MAX_ZOOM);
735785
updateZoomDisplay ();
736786
if (!pdfData_.isEmpty () && pageCount_ > 0) {
737787
zoomDebounceTimer_->start ();

src/Plugins/Qt/qt_pdf_reader_widget.hpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class PDFReaderWidget : public QWidget {
5757
void setZoomFactor (double factor);
5858
void fitWidth ();
5959
void fitHeight ();
60+
void zoomIn ();
61+
void zoomOut ();
6062

6163
int currentPage () const;
6264
void goToPage (int page);
@@ -92,10 +94,12 @@ private slots:
9294

9395
QToolBar* toolBar_;
9496
QComboBox* zoomCombo_;
97+
QPushButton* zoomOutBtn_;
9598
QPushButton* prevPageBtn_;
9699
QLineEdit* pageEdit_;
97100
QLabel* pageTotalLabel_;
98101
QPushButton* nextPageBtn_;
102+
QPushButton* zoomInBtn_;
99103

100104
QByteArray pdfData_;
101105
int pageCount_;
@@ -119,11 +123,14 @@ private slots:
119123
static constexpr int DEFAULT_DPI = 150;
120124
static constexpr int PAGE_MARGIN = 16;
121125
static constexpr int PRELOAD_MARGIN = 200;
122-
static constexpr double MIN_ZOOM = 0.1;
123-
static constexpr double MAX_ZOOM = 5.0;
124-
static constexpr double ZOOM_STEP = 0.1;
126+
static constexpr double MIN_ZOOM = 0.12;
127+
static constexpr double MAX_ZOOM = 8.0;
125128
static constexpr int ZOOM_DEBOUNCE_MS = 200;
126129
static constexpr int RESIZE_DEBOUNCE_MS= 300;
130+
131+
static constexpr int ZOOM_LEVEL_COUNT= 12;
132+
static constexpr double ZOOM_LEVELS[ZOOM_LEVEL_COUNT]{
133+
0.25, 0.33, 0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 4.00, 6.00, 8.00};
127134
};
128135

129136
/* PdfPageCacheKey qHash defined above */

0 commit comments

Comments
 (0)