{"id":1853,"date":"2026-04-29T15:09:07","date_gmt":"2026-04-29T07:09:07","guid":{"rendered":"https:\/\/notes.coremix.net\/?p=1853"},"modified":"2026-04-29T15:09:07","modified_gmt":"2026-04-29T07:09:07","slug":"%e5%8f%91%e4%b8%80%e4%b8%aa%e5%bf%ab%e9%80%9f%e9%98%85%e8%af%bb%e7%9a%84%e5%b0%8f%e5%b7%a5%e5%85%b7","status":"publish","type":"post","link":"https:\/\/notes.coremix.net\/?p=1853","title":{"rendered":"\u53d1\u4e00\u4e2a\u5feb\u901f\u9605\u8bfb\u7684\u5c0f\u5de5\u5177"},"content":{"rendered":"<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\n&quot;&quot;&quot;\nPDF \u81ea\u52a8\u7ffb\u9875\u5de5\u5177 v4\n\u8de8\u5e73\u53f0 (macOS \/ Windows 11)\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u70ed\u952e\u540e\u7aef\u4f18\u5148\u7ea7\uff1apynput\uff08Mac\/Win \u5747\u53ef\u7528\uff0c\u65e0\u9700\u7ba1\u7406\u5458\u6743\u9650\uff09\u2192 keyboard\uff08\u4ec5 Win\uff09\n\u529f\u80fd\uff1a\n  - tkinter \u6d6e\u52a8 GUI\uff0c\u7f6e\u9876\u5f00\u5173\u53ef\u5207\u6362\n  - Ctrl+Shift+\u2191 \u52a0\u901f \/ Ctrl+Shift+\u2193 \u51cf\u901f \/ Ctrl+Shift+P \u6682\u505c\/\u7ee7\u7eed\n    \uff08\u907f\u514d\u4e0e PDF \u9605\u8bfb\u5668 F1\/F2\/F3 \u51b2\u7a81\uff09\n  - GUI \u5185 &#x5B;-] &#x5B;+] \u6309\u94ae + \u6682\u505c\u6309\u94ae\uff0c\u4e0d\u4f9d\u8d56\u70ed\u952e\u4ea6\u53ef\u64cd\u4f5c\n  - PageDown \/ Space \u624b\u52a8\u7ffb\u9875\uff1a\u59cb\u7ec8\u91cd\u7f6e\u8ba1\u65f6\uff08\u4e0d\u4f1a\u7acb\u5373\u518d\u81ea\u52a8\u7ffb\u9875\uff09\n  - STAY_GUARD = 2.0 s\uff1a\u9f20\u6807\u89e6\u53d1\u9632\u53cc\u7ffb\u4fdd\u62a4\u7a97\u53e3\n  - \u5206\u522b\u7edf\u8ba1\u81ea\u52a8\u7ffb\u9875 \/ \u624b\u52a8\u7ffb\u9875\u6b21\u6570\n  - \u72ec\u7acb\u7edf\u8ba1\u9605\u8bfb\u65f6\u95f4\uff08\u4e0d\u542b\u6682\u505c\uff09\u548c\u7d2f\u8ba1\u6682\u505c\u65f6\u95f4\n  - \u5173\u95ed\u65f6\u8ffd\u52a0\u5199\u5165 scroll_history.json\n  - \u9f20\u6807\u5de6\u8fb9\u7f18\u89e6\u53d1\uff08\u53ef\u9009\uff0c\u9002\u5408\u8f68\u8ff9\u7403\uff09\n&quot;&quot;&quot;\n\nimport sys\nimport os\nimport json\nimport tkinter as tk\nimport tkinter.simpledialog as sd\nfrom tkinter import ttk\nimport pyautogui\nimport time\nimport threading\nfrom datetime import datetime, timedelta\n\n# \u2500\u2500 \u5e73\u53f0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nPLATFORM = sys.platform   # &#039;darwin&#039; | &#039;win32&#039; | &#039;linux&#039;\n\nHISTORY_FILE = os.path.join(\n    os.path.dirname(os.path.abspath(__file__)), &quot;scroll_history.json&quot;\n)\n\n# \u5b57\u4f53\uff08Segoe UI \u4ec5 Windows\uff1bmacOS \u7528 Helvetica\uff09\n_FONT_BOLD  = (&quot;Helvetica&quot;, 11, &quot;bold&quot;) if PLATFORM == &quot;darwin&quot; else (&quot;Segoe UI&quot;, 11, &quot;bold&quot;)\n_FONT_SMALL = (&quot;Helvetica&quot;, 8)          if PLATFORM == &quot;darwin&quot; else (&quot;Segoe UI&quot;, 8)\n\n# \u2500\u2500 \u70ed\u952e\u540e\u7aef \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ntry:\n    from pynput import keyboard as _pynput_kb\n    _BACKEND = &quot;pynput&quot;\nexcept ImportError:\n    _pynput_kb = None\n    try:\n        import keyboard as _kb_module\n        _BACKEND = &quot;keyboard&quot;\n    except ImportError:\n        _kb_module = None\n        _BACKEND = &quot;none&quot;\n\n\nclass AutoScroller:\n    SPEED_MIN  = 1.0    # \u6700\u77ed\u7ffb\u9875\u95f4\u9694\uff08\u79d2\uff09\n    SPEED_MAX  = 120.0  # \u6700\u957f\u7ffb\u9875\u95f4\u9694\uff08\u79d2\uff09\n    SPEED_STEP = 1.0    # \u6bcf\u6b21\u8c03\u901f\u6b65\u957f\uff08\u79d2\uff09\n    STAY_GUARD = 2.0    # \u9f20\u6807\u89e6\u53d1\u9632\u53cc\u7ffb\u4fdd\u62a4\u7a97\u53e3\uff08\u79d2\uff09\uff1b\u952e\u76d8\u65e0\u6b64\u9650\u5236\n\n    def __init__(self, interval: float):\n        self.interval      = interval\n        self._timer        = interval\n\n        # \u7ffb\u9875\u8ba1\u6570\uff08\u81ea\u52a8 \/ \u624b\u52a8 \u5206\u5f00\u7edf\u8ba1\uff09\n        self._auto_count   = 0\n        self._manual_count = 0\n\n        # \u65f6\u95f4\u7edf\u8ba1\n        self._session_start    = time.time()\n        self._total_pause_secs = 0.0\n        self._pause_start      = None   # \u975e None \u8868\u793a\u5f53\u524d\u5904\u4e8e\u6682\u505c\u4e2d\n\n        self._running   = True\n        self._lock      = threading.Lock()\n        self._pause_evt = threading.Event()\n        self._pause_evt.set()   # \u521d\u59cb\uff1a\u8fd0\u884c\u4e2d\n\n        self._build_gui()\n        self._register_hotkeys()\n\n        threading.Thread(target=self._scroll_loop, daemon=True).start()\n        threading.Thread(target=self._mouse_loop,  daemon=True).start()\n\n        self._refresh_gui()   # \u542f\u52a8 GUI \u5468\u671f\u5237\u65b0\n\n    # \u2500\u2500 GUI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    def _build_gui(self):\n        self.root = tk.Tk()\n        self.root.title(&quot;PDF \u81ea\u52a8\u7ffb\u9875 v4&quot;)\n        self.root.resizable(False, False)\n\n        # BooleanVar \u5fc5\u987b\u5728 Tk() \u4e4b\u540e\u521b\u5efa\n        self._mouse_enabled = tk.BooleanVar(value=False)\n        self._topmost_var   = tk.BooleanVar(value=True)\n        self._topmost_var.trace_add(&quot;write&quot;, self._toggle_topmost)\n        self.root.attributes(&#039;-topmost&#039;, True)\n\n        frm = ttk.Frame(self.root, padding=12)\n        frm.pack(fill=tk.BOTH, expand=True)\n\n        # \u72b6\u6001\u884c\n        self.sv_status = tk.StringVar(value=&quot;\u25b6 \u8fd0\u884c\u4e2d&quot;)\n        self._lbl_status = ttk.Label(\n            frm, textvariable=self.sv_status,\n            font=_FONT_BOLD, foreground=&quot;#1a7a1a&quot;\n        )\n        self._lbl_status.grid(row=0, column=0, columnspan=3, pady=(0, 8))\n\n        # \u6570\u636e\u884c\uff083\u5217\uff1a\u6807\u7b7e \/ \u503c \/ \u5360\u4f4d\uff09\n        rows = &#x5B;\n            (&quot;\u81ea\u52a8\u7ffb\u9875&quot;, &quot;sv_auto&quot;,      &quot;0 \u9875&quot;),\n            (&quot;\u624b\u52a8\u7ffb\u9875&quot;, &quot;sv_manual&quot;,    &quot;0 \u9875&quot;),\n            (&quot;\u9605\u8bfb\u65f6\u95f4&quot;, &quot;sv_reading&quot;,   &quot;00:00:00&quot;),\n            (&quot;\u6682\u505c\u65f6\u95f4&quot;, &quot;sv_pause_t&quot;,   &quot;00:00:00&quot;),\n            (&quot;\u7ffb\u9875\u95f4\u9694&quot;, &quot;sv_interval&quot;,  f&quot;{self.interval:.0f} \u79d2&quot;),\n            (&quot;\u4e0b\u6b21\u7ffb\u9875&quot;, &quot;sv_countdown&quot;, f&quot;{self.interval:.1f} \u79d2&quot;),\n        ]\n        for i, (lbl, attr, init) in enumerate(rows, start=1):\n            ttk.Label(frm, text=lbl + &quot;\uff1a&quot;, anchor=&quot;e&quot;).grid(\n                row=i, column=0, sticky=&quot;e&quot;, pady=2\n            )\n            sv = tk.StringVar(value=init)\n            setattr(self, attr, sv)\n            ttk.Label(frm, textvariable=sv, anchor=&quot;w&quot;).grid(\n                row=i, column=1, columnspan=2, sticky=&quot;w&quot;, padx=(6, 0), pady=2\n            )\n\n        n = len(rows)\n\n        # \u2500\u2500 \u901f\u5ea6\u63a7\u5236\u6309\u94ae\u884c \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        ttk.Separator(frm, orient=&quot;horizontal&quot;).grid(\n            row=n + 1, column=0, columnspan=3, sticky=&quot;ew&quot;, pady=(8, 4)\n        )\n        ctrl_frm = ttk.Frame(frm)\n        ctrl_frm.grid(row=n + 2, column=0, columnspan=3, pady=(0, 4))\n\n        ttk.Button(ctrl_frm, text=&quot;  \u2212  &quot;, width=4,\n                   command=self._slower).pack(side=tk.LEFT, padx=2)\n        ttk.Label(ctrl_frm, text=&quot;\u901f\u5ea6&quot;).pack(side=tk.LEFT, padx=4)\n        ttk.Button(ctrl_frm, text=&quot;  \uff0b  &quot;, width=4,\n                   command=self._faster).pack(side=tk.LEFT, padx=2)\n\n        self._btn_pause = ttk.Button(\n            ctrl_frm, text=&quot;\u23f8 \u6682\u505c&quot;, width=8, command=self._toggle_pause\n        )\n        self._btn_pause.pack(side=tk.LEFT, padx=(10, 2))\n\n        # \u2500\u2500 \u9009\u9879 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        ttk.Checkbutton(\n            frm, text=&quot;\u59cb\u7ec8\u7f6e\u9876&quot;,\n            variable=self._topmost_var\n        ).grid(row=n + 3, column=0, columnspan=3, sticky=&quot;w&quot;)\n\n        ttk.Checkbutton(\n            frm, text=&quot;\u9f20\u6807\u5de6\u8fb9\u7f18\u89e6\u53d1\u7ffb\u9875&quot;,\n            variable=self._mouse_enabled\n        ).grid(row=n + 4, column=0, columnspan=3, sticky=&quot;w&quot;)\n\n        # \u2500\u2500 \u70ed\u952e\u63d0\u793a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        ttk.Label(\n            frm,\n            text=&quot;Ctrl+Shift+\u2191 \u52a0\u901f \u00b7 Ctrl+Shift+\u2193 \u51cf\u901f \u00b7 Ctrl+Shift+P \u6682\u505c&quot;,\n            foreground=&quot;gray&quot;, font=_FONT_SMALL\n        ).grid(row=n + 5, column=0, columnspan=3, pady=(4, 0))\n\n        backend_tip = {&quot;pynput&quot;: &quot;\u70ed\u952e\uff1apynput&quot;, &quot;keyboard&quot;: &quot;\u70ed\u952e\uff1akeyboard&quot;, &quot;none&quot;: &quot;\u26a0 \u672a\u627e\u5230\u70ed\u952e\u5e93&quot;}\n        ttk.Label(\n            frm, text=backend_tip.get(_BACKEND, &quot;&quot;),\n            foreground=&quot;gray&quot;, font=_FONT_SMALL\n        ).grid(row=n + 6, column=0, columnspan=3)\n\n    # \u2500\u2500 \u70ed\u952e\u6ce8\u518c\uff08\u8de8\u5e73\u53f0\uff09\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    def _register_hotkeys(self):\n        &quot;&quot;&quot;\n        \u70ed\u952e\u65b9\u6848\uff08\u907f\u514d\u4e0e PDF \u9605\u8bfb\u5668\u51b2\u7a81\uff09\n          Ctrl+Shift+\u2191  \u2192 \u52a0\u901f\uff08\u51cf\u5c0f\u95f4\u9694\uff09\n          Ctrl+Shift+\u2193  \u2192 \u51cf\u901f\uff08\u589e\u5927\u95f4\u9694\uff09\n          Ctrl+Shift+P  \u2192 \u6682\u505c\/\u7ee7\u7eed\n          PageDown \/ Space \u2192 \u624b\u52a8\u7ffb\u9875\u8ba1\u65f6\u91cd\u7f6e\uff08pass-through\uff0c\u9605\u8bfb\u5668\u6b63\u5e38\u63a5\u6536\uff09\n        &quot;&quot;&quot;\n        if _BACKEND == &quot;pynput&quot;:\n            _mods = {&#039;ctrl&#039;: False, &#039;shift&#039;: False}\n            K = _pynput_kb.Key\n\n            def on_press(key):\n                try:\n                    if key in (K.ctrl_l, K.ctrl_r, K.ctrl):\n                        _mods&#x5B;&#039;ctrl&#039;] = True\n                    elif key in (K.shift, K.shift_l, K.shift_r):\n                        _mods&#x5B;&#039;shift&#039;] = True\n                    elif _mods&#x5B;&#039;ctrl&#039;] and _mods&#x5B;&#039;shift&#039;]:\n                        if key == K.up:\n                            self._faster()\n                        elif key == K.down:\n                            self._slower()\n                        elif hasattr(key, &#039;char&#039;) and key.char in (&#039;p&#039;, &#039;P&#039;):\n                            self._toggle_pause()\n                    elif key == K.page_down:\n                        self._on_manual()\n                    elif key == K.space:\n                        self._on_manual()\n                except Exception:\n                    pass\n\n            def on_release(key):\n                try:\n                    if key in (K.ctrl_l, K.ctrl_r, K.ctrl):\n                        _mods&#x5B;&#039;ctrl&#039;] = False\n                    elif key in (K.shift, K.shift_l, K.shift_r):\n                        _mods&#x5B;&#039;shift&#039;] = False\n                except Exception:\n                    pass\n\n            self._kb_listener = _pynput_kb.Listener(\n                on_press=on_press, on_release=on_release, daemon=True\n            )\n            self._kb_listener.start()\n\n        elif _BACKEND == &quot;keyboard&quot;:\n            # suppress=True\uff1aCtrl+Shift \u7ec4\u5408\u952e\u88ab\u672c\u7a0b\u5e8f\u6d88\u8d39\uff0c\u4e0d\u4f20\u7ed9 PDF \u9605\u8bfb\u5668\n            # suppress=False\uff1aPageDown\/Space \u4ecd\u900f\u4f20\uff0c\u8ba9\u9605\u8bfb\u5668\u6b63\u5e38\u7ffb\u9875\n            _kb_module.add_hotkey(&#039;ctrl+shift+up&#039;,   self._faster,       suppress=True)\n            _kb_module.add_hotkey(&#039;ctrl+shift+down&#039;,  self._slower,       suppress=True)\n            _kb_module.add_hotkey(&#039;ctrl+shift+p&#039;,     self._toggle_pause, suppress=True)\n            _kb_module.add_hotkey(&#039;pagedown&#039;,         self._on_manual,    suppress=False)\n            _kb_module.add_hotkey(&#039;space&#039;,            self._on_manual,    suppress=False)\n\n    # \u2500\u2500 \u540e\u53f0\u7ffb\u9875\u7ebf\u7a0b \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    def _scroll_loop(self):\n        while self._running:\n            self._pause_evt.wait()   # \u6682\u505c\u65f6\u5728\u6b64\u963b\u585e\uff0c\u4e0d\u6d88\u8017 CPU\n            time.sleep(0.1)\n            do_turn = False\n            with self._lock:\n                self._timer -= 0.1\n                if self._timer &lt;= 0:\n                    self._auto_count += 1\n                    self._timer = self.interval\n                    do_turn = True\n            if do_turn:\n                pyautogui.press(&#039;pagedown&#039;)\n\n    # \u2500\u2500 \u9f20\u6807\u8fb9\u7f18\u89e6\u53d1\u7ebf\u7a0b \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    def _mouse_loop(self):\n        &quot;&quot;&quot;\u9f20\u6807\u79fb\u5230\u5c4f\u5e55\u6700\u5de6\u4fa7\uff08x &lt; 50\uff09\u65f6\u89e6\u53d1\u7ffb\u9875\uff0c\u79bb\u5f00\u540e\u81ea\u52a8\u79fb\u56de\u3002&quot;&quot;&quot;\n        while self._running:\n            try:\n                if self._mouse_enabled.get() and self._pause_evt.is_set():\n                    pos = pyautogui.position()\n                    if pos.x &lt; 50:\n                        do_turn = False\n                        with self._lock:\n                            if self._timer &gt; self.STAY_GUARD:\n                                self._manual_count += 1\n                                self._timer = self.interval\n                                do_turn = True\n                        if do_turn:\n                            pyautogui.press(&#039;pagedown&#039;)\n                            time.sleep(0.1)\n                            pos2 = pyautogui.position()\n                            if pos2.x &lt;= 130:\n                                pyautogui.moveTo(130, pos2.y)\n            except Exception:\n                pass\n            time.sleep(0.1)\n\n    # \u2500\u2500 \u70ed\u952e\u56de\u8c03 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    def _on_manual(self):\n        &quot;&quot;&quot;\n        \u952e\u76d8\u624b\u52a8\u7ffb\u9875\uff08PageDown \/ Space\uff09\uff1a\n        \u59cb\u7ec8\u91cd\u7f6e\u8ba1\u65f6\u5668\u5230\u5b8c\u6574\u95f4\u9694\uff0c\u786e\u4fdd\u624b\u52a8\u7ffb\u540e\u4e0d\u4f1a\u7acb\u5373\u81ea\u52a8\u7ffb\u3002\n        \u952e\u76d8\u672c\u8eab\u5df2\u5c06\u6309\u952e\u4f20\u7ed9 PDF \u9605\u8bfb\u5668\uff0c\u6b64\u5904\u4e0d\u518d\u989d\u5916 press()\u3002\n        &quot;&quot;&quot;\n        with self._lock:\n            self._manual_count += 1\n            self._timer = self.interval\n\n    def _faster(self):\n        with self._lock:\n            self.interval = max(self.SPEED_MIN, self.interval - self.SPEED_STEP)\n            self._timer   = min(self._timer, self.interval)\n\n    def _slower(self):\n        with self._lock:\n            self.interval = min(self.SPEED_MAX, self.interval + self.SPEED_STEP)\n\n    def _toggle_topmost(self, *_):\n        topmost = self._topmost_var.get()\n        self.root.attributes(&#039;-topmost&#039;, topmost)\n\n    def _toggle_pause(self):\n        if self._pause_evt.is_set():\n            self._pause_evt.clear()   # \u2192 \u6682\u505c\n            with self._lock:\n                self._pause_start = time.time()\n        else:\n            self._pause_evt.set()     # \u2192 \u7ee7\u7eed\n            with self._lock:\n                if self._pause_start is not None:\n                    self._total_pause_secs += time.time() - self._pause_start\n                    self._pause_start = None\n\n    # \u2500\u2500 GUI \u5237\u65b0\uff08\u6bcf 100 ms\uff09\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    def _refresh_gui(self):\n        now = time.time()\n\n        with self._lock:\n            auto_c    = self._auto_count\n            manual_c  = self._manual_count\n            interval  = self.interval\n            countdown = max(0.0, self._timer)\n            pause_acc = self._total_pause_secs\n            p_start   = self._pause_start\n\n        # \u5f53\u524d\u6682\u505c\u6bb5\uff08\u5982\u6b63\u5728\u6682\u505c\uff09\n        current_pause = (now - p_start) if p_start is not None else 0.0\n        total_pause   = pause_acc + current_pause\n        reading_time  = max(0.0, (now - self._session_start) - total_pause)\n\n        paused = not self._pause_evt.is_set()\n\n        self.sv_auto.set(f&quot;{auto_c} \u9875&quot;)\n        self.sv_manual.set(f&quot;{manual_c} \u9875&quot;)\n        self.sv_reading.set(str(timedelta(seconds=int(reading_time))))\n        self.sv_pause_t.set(str(timedelta(seconds=int(total_pause))))\n        self.sv_interval.set(f&quot;{interval:.0f} \u79d2&quot;)\n        self.sv_countdown.set(&quot;\u5df2\u6682\u505c&quot; if paused else f&quot;{countdown:.1f} \u79d2&quot;)\n\n        if paused:\n            self.sv_status.set(&quot;\u23f8 \u5df2\u6682\u505c&quot;)\n            self._lbl_status.configure(foreground=&quot;#b05a00&quot;)\n            self._btn_pause.configure(text=&quot;\u25b6 \u7ee7\u7eed&quot;)\n        else:\n            self.sv_status.set(&quot;\u25b6 \u8fd0\u884c\u4e2d&quot;)\n            self._lbl_status.configure(foreground=&quot;#1a7a1a&quot;)\n            self._btn_pause.configure(text=&quot;\u23f8 \u6682\u505c&quot;)\n\n        self.root.after(100, self._refresh_gui)\n\n    # \u2500\u2500 \u5386\u53f2\u8bb0\u5f55 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    def _save_history(self):\n        now = time.time()\n\n        with self._lock:\n            pause_acc  = self._total_pause_secs\n            p_start    = self._pause_start\n            auto_c     = self._auto_count\n            manual_c   = self._manual_count\n            interval   = self.interval\n\n        current_pause = (now - p_start) if p_start is not None else 0.0\n        total_pause   = pause_acc + current_pause\n        reading_time  = max(0.0, (now - self._session_start) - total_pause)\n\n        record = {\n            &quot;date&quot;:            datetime.now().strftime(&quot;%Y-%m-%d&quot;),\n            &quot;start&quot;:           datetime.fromtimestamp(self._session_start).strftime(&quot;%H:%M:%S&quot;),\n            &quot;end&quot;:             datetime.now().strftime(&quot;%H:%M:%S&quot;),\n            &quot;reading_seconds&quot;: int(reading_time),\n            &quot;pause_seconds&quot;:   int(total_pause),\n            &quot;auto_turns&quot;:      auto_c,\n            &quot;manual_turns&quot;:    manual_c,\n            &quot;interval_final&quot;:  round(interval, 1),\n            &quot;platform&quot;:        PLATFORM,\n        }\n\n        records = &#x5B;]\n        if os.path.exists(HISTORY_FILE):\n            try:\n                with open(HISTORY_FILE, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:\n                    records = json.load(f)\n            except Exception:\n                records = &#x5B;]\n\n        records.append(record)\n\n        try:\n            with open(HISTORY_FILE, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:\n                json.dump(records, f, ensure_ascii=False, indent=2)\n        except Exception:\n            pass\n\n    # \u2500\u2500 \u751f\u547d\u5468\u671f \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    def run(self):\n        self.root.protocol(&quot;WM_DELETE_WINDOW&quot;, self._shutdown)\n        self.root.mainloop()\n\n    def _shutdown(self):\n        self._running = False\n        self._pause_evt.set()   # \u89e3\u9664\u963b\u585e\u8ba9\u7ffb\u9875\u7ebf\u7a0b\u9000\u51fa\n        self._save_history()\n\n        if _BACKEND == &quot;pynput&quot;:\n            try:\n                self._kb_listener.stop()\n            except Exception:\n                pass\n        elif _BACKEND == &quot;keyboard&quot;:\n            try:\n                _kb_module.unhook_all()\n            except Exception:\n                pass\n\n        self.root.destroy()\n\n\n# \u2500\u2500 \u5165\u53e3 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef main():\n    tmp = tk.Tk()\n    tmp.withdraw()\n    interval = sd.askfloat(\n        &quot;PDF \u81ea\u52a8\u7ffb\u9875&quot;,\n        &quot;\u8bf7\u8f93\u5165\u7ffb\u9875\u95f4\u9694\uff08\u79d2\uff09\uff1a&quot;,\n        minvalue=1.0, maxvalue=600.0, initialvalue=5.0,\n        parent=tmp,\n    )\n    tmp.destroy()\n    if interval is None:\n        return   # \u7528\u6237\u53d6\u6d88\n    AutoScroller(interval).run()\n\n\nif __name__ == &quot;__main__&quot;:\n    main()\n\n<\/pre><\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1853","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/notes.coremix.net\/index.php?rest_route=\/wp\/v2\/posts\/1853","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/notes.coremix.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/notes.coremix.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/notes.coremix.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/notes.coremix.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1853"}],"version-history":[{"count":1,"href":"https:\/\/notes.coremix.net\/index.php?rest_route=\/wp\/v2\/posts\/1853\/revisions"}],"predecessor-version":[{"id":1854,"href":"https:\/\/notes.coremix.net\/index.php?rest_route=\/wp\/v2\/posts\/1853\/revisions\/1854"}],"wp:attachment":[{"href":"https:\/\/notes.coremix.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1853"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/notes.coremix.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1853"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/notes.coremix.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1853"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}