就是可以设置滚动速度,然后小说会按照速度自动往下滚。类似提词器一样。
我倒是找到了一个,但是他的滚动速度是匀速的,而小说文本其实是有的稀疏有的稠密的。
有没有支持根据文本稠密程度来自动调整速度的呢?
就是可以设置滚动速度,然后小说会按照速度自动往下滚。类似提词器一样。
我倒是找到了一个,但是他的滚动速度是匀速的,而小说文本其实是有的稀疏有的稠密的。
有没有支持根据文本稠密程度来自动调整速度的呢?
能自动翻页的有,但是根据文本密度调整速度的没见到有。
文本密度不好量化吧,个人认为可以按段落大小滚屏?
创建一个timer。按照每行实际字符(排除标点)得到系数,乘以每个字符停留时间得到每行停留的时间。然后在timer中每到一个时间节点就下翻一行。
有个手动的方法:用浏览器、记事本等打开 txt 小说后,按一下鼠标滚轮,这时刚刚点击的地方就会出现一个圆圈。
把鼠标指针放到圆圈下方,就会自动滚动。鼠标指针离刚刚那个圆圈越近、滚动速度越慢。
已找到解决方案,但不完美,Balabolaka
让它朗读小说,然后把音量调到0。把字体设置里的三种状态字体调成一样的就行。
问题1是它不是平滑滚动,而是每次把正朗读那句居中。
我不是很喜欢这种,因为阅读速度和它朗读速度并非完全一致。
问题2是速度不能线性调整。
让AI给写了个,感觉还不错:
空格开始滚屏,↓键和↑键调节速度。简洁够用。
import sys
import math
import re
import os
from PyQt6.QtWidgets import QApplication, QMainWindow, QTextBrowser, QFileDialog, QLabel, QPushButton
from PyQt6.QtCore import QTimer, Qt, QEvent, QPoint, QSettings
from PyQt6.QtGui import QTextCursor
class SmartReader(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("智能平滑阅读器 v13.1")
# 初始化 QSettings
self.settings = QSettings("MyStudio", "SmartReader")
# ===== 默认核心参数(新版)=====
# base_speed 现在代表:满行(30汉字)时滚动一行所需的秒数,默认10.0秒
base_speed_val = self.settings.value("base_speed", 10.0)
try:
self.base_speed = float(base_speed_val)
# 旧版迁移:如果读取到以前的小数值(<2.0),自动升级为新默认10.0
if self.base_speed < 2.0:
self.base_speed = 10.0
self.settings.setValue("base_speed", 10.0)
except:
self.base_speed = 10.0
self.current_speed = 0.0
self.is_scrolling = False
self.current_offset = 0.0
self.is_dragging = False
self.last_file_path = self.settings.value("last_file", "")
self.last_position = int(self.settings.value("last_position", 0))
# ===== 初始化 UI =====
self.init_ui()
# ===== 恢复窗口状态 =====
self.restore_window_state()
# ===== 自动加载上次文件 =====
if self.last_file_path and os.path.exists(self.last_file_path):
QTimer.singleShot(100, lambda: self.load_file(self.last_file_path, self.last_position))
def init_ui(self):
self.setStyleSheet("background-color: #FDF6E3;")
# 文本区域
self.text_display = QTextBrowser(self)
self.text_display.setReadOnly(True)
self.text_display.setFrameShape(QTextBrowser.Shape.NoFrame)
self.text_display.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.text_display.setStyleSheet("""
QTextBrowser {
background-color: #FDF6E3; color: #586E75;
font-family: 'Microsoft YaHei'; font-size: 26px;
line-height: 180%; padding-left: 60px; padding-right: 60px; border: none;
}
""")
# 滚动条
self.scroll_bar_bg = QLabel(self)
self.scroll_bar_bg.setCursor(Qt.CursorShape.SizeVerCursor)
self.scroll_bar_bg.setStyleSheet("background-color: rgba(0,0,0,10); border: 1px solid rgba(88,110,117,10);")
self.scroll_bar_bg.setMouseTracking(True)
self.handle = QLabel(self)
self.handle.setStyleSheet("background-color: rgba(88,110,117,180); border-radius: 4px;")
# 按钮
self.open_btn = QPushButton("+", self)
self.open_btn.setFixedSize(40, 40)
self.open_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.open_btn.setStyleSheet("""
QPushButton {
background: rgba(147,161,161,180); color: white; border-radius: 20px; font-size: 24px; border: none;
}
QPushButton:hover { background: #586E75; }
""")
self.open_btn.clicked.connect(self.on_open_clicked)
# ===== 实时速度 HUD 显示 =====
self.hud_label = QLabel(self)
self.hud_label.setStyleSheet("""
color: rgba(88, 110, 117, 160);
font-size: 15px;
font-family: 'Microsoft YaHei';
font-weight: bold;
background: transparent;
""")
self.hud_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
self.hud_label.hide()
# 定时器
self.timer = QTimer()
self.timer.timeout.connect(self.engine)
QApplication.instance().installEventFilter(self)
self.scroll_bar_bg.installEventFilter(self)
self.setMouseTracking(True)
self.text_display.viewport().setMouseTracking(True)
# ================= 记忆核心逻辑 =================
def restore_window_state(self):
geom = self.settings.value("geometry")
if geom:
self.restoreGeometry(geom)
else:
self.setGeometry(100, 100, 1000, 900)
def closeEvent(self, event):
self.settings.setValue("geometry", self.saveGeometry())
self.settings.setValue("base_speed", self.base_speed)
self.settings.setValue("last_file", self.last_file_path)
current_pos = self.text_display.verticalScrollBar().value()
self.settings.setValue("last_position", current_pos)
super().closeEvent(event)
def load_file(self, path, position=0):
content = ""
for encoding in ['utf-8', 'gb18030']:
try:
with open(path, 'r', encoding=encoding) as f:
content = f.read()
break
except Exception:
continue
if not content:
return
self.last_file_path = path
html = "".join([f"<p> {l.strip()}</p>" for l in content.split('\n') if l.strip()])
self.text_display.setHtml(f"<div style='margin-top:60px;'>{html}</div>")
def restore_pos():
sb = self.text_display.verticalScrollBar()
target = min(position, sb.maximum())
sb.setValue(target)
self.current_offset = float(target)
self.update_scrollbar()
if position > 0:
self.show_msg("已恢复阅读进度")
QTimer.singleShot(200, restore_pos)
# ================= 交互逻辑 =================
def on_open_clicked(self):
path, _ = QFileDialog.getOpenFileName(self, "选择小说", "", "Text (*.txt)")
if path:
self.load_file(path, 0)
def resizeEvent(self, event):
self.text_display.setGeometry(0, 0, self.width(), self.height())
self.open_btn.move(self.width() - 80, 40)
self.hud_label.move(30, self.height() - 45)
self.update_scrollbar()
super().resizeEvent(event)
def update_scrollbar(self):
bar_w = 12
self.scroll_bar_bg.setGeometry(self.width() - bar_w - 5, 0, bar_w, self.height())
sb = self.text_display.verticalScrollBar()
max_v = sb.maximum()
if max_v <= 0:
self.handle.hide()
return
self.handle.show()
h = max(40, int(self.height() * (self.height() / (max_v + self.height()))))
y = int((sb.value() / max_v) * (self.height() - h))
self.handle.setGeometry(self.width() - bar_w - 5 + 2, y, bar_w - 4, h)
def eventFilter(self, source, event):
if event.type() == QEvent.Type.KeyPress:
if event.key() == Qt.Key.Key_Space:
self.toggle()
return True
elif event.key() == Qt.Key.Key_Up:
self.adjust_speed(-0.1)
return True
elif event.key() == Qt.Key.Key_Down:
self.adjust_speed(0.1)
return True
if source is self.scroll_bar_bg:
if event.type() == QEvent.Type.MouseButtonPress:
self.is_dragging = True
self.scroll_bar_bg.grabMouse()
self.jump_from_scrollbar_y(event.pos().y())
return True
elif event.type() == QEvent.Type.MouseMove and self.is_dragging:
self.jump_from_scrollbar_y(event.pos().y())
return True
elif event.type() == QEvent.Type.MouseButtonRelease:
self.is_dragging = False
self.scroll_bar_bg.releaseMouse()
return True
return super().eventFilter(source, event)
def toggle(self):
self.is_scrolling = not self.is_scrolling
if self.is_scrolling:
self.current_offset = float(self.text_display.verticalScrollBar().value())
self.timer.start(16)
self.hud_label.show()
self.show_msg("阅读中...")
else:
self.timer.stop()
self.hud_label.hide()
self.show_msg("已暂停")
def adjust_speed(self, d):
self.base_speed = max(0.5, self.base_speed + d) # 防止过快
self.show_msg(f"速度设定: 每满行 {self.base_speed:.1f} 秒")
if not self.is_scrolling:
self.update_hud()
def jump_from_scrollbar_y(self, y):
sb = self.text_display.verticalScrollBar()
if sb.maximum() <= 0: return
ratio = max(0, min(y / self.height(), 1))
target = ratio * sb.maximum()
sb.setValue(int(target))
self.current_offset = target
self.update_scrollbar()
def update_hud(self):
"""更新左下角实时数据面板(新版带单位)"""
text = f"⏱ 满行 {self.base_speed:.1f}s | ⚡ 实时 {self.current_speed:.2f}"
self.hud_label.setText(text)
self.hud_label.adjustSize()
def engine(self):
if not self.is_scrolling or self.is_dragging: return
# ===== 新版简单翻页计算方案(完全按你的要求实现)=====
# 1. 获取屏幕正中间那一行的文本块
cursor = self.text_display.cursorForPosition(QPoint(0, self.height() // 2))
block = cursor.block()
line_text = block.text() if block.isValid() else ""
# 2. 计算这一行有多少个汉字(满行基准 = 30字)
num_chars = len(re.findall(r'[\u4e00-\u9fff]', line_text))
num_chars = max(num_chars, 1) # 防止除零
# 3. 计算“满的程度”系数
fullness = num_chars / 30.0
# 4. 当前行需要滚动的时间(秒)—— 这就是你想要的核心公式
time_per_line = fullness * self.base_speed
# 示例:满行30字 → 10秒;6字 → 2秒(仍是30字/10秒的阅读速度)
# 5. 获取当前文本块的实际像素高度(自动支持换行)
layout = self.text_display.document().documentLayout()
if block.isValid():
block_rect = layout.blockBoundingRect(block)
line_height_px = max(float(block_rect.height()), 1.0)
else:
line_height_px = 48.0 # 备用值(26px字体×1.8行高)
# 6. 计算目标滚动速度(像素/秒)
scroll_speed_px_per_sec = line_height_px / time_per_line
# 7. 每帧(16ms = 0.016秒)应该滚动的像素
target_speed = scroll_speed_px_per_sec * 0.016
# 8. 平滑过渡(保持丝滑手感)
self.current_speed += (target_speed - self.current_speed) * 0.08
# 刷新HUD
self.update_hud()
# 执行滚动
self.current_offset += self.current_speed
sb = self.text_display.verticalScrollBar()
if self.current_offset >= sb.maximum():
self.toggle()
return
sb.setValue(int(self.current_offset))
self.update_scrollbar()
def show_msg(self, txt):
if not hasattr(self, 'msg'):
self.msg = QLabel(self)
self.msg.setStyleSheet("background: rgba(0,0,0,160); color: white; padding:8px 15px; border-radius:5px;")
self.msg.setText(txt)
self.msg.adjustSize()
self.msg.move((self.width()-self.msg.width())//2, self.height()-100)
self.msg.show()
QTimer.singleShot(1500, self.msg.hide)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = SmartReader()
w.show()
sys.exit(app.exec())