ctk-kit 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/PKG-INFO +31 -7
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/README.md +30 -6
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/__init__.py +6 -3
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/scroll_list.py +168 -5
- ctk_kit-0.3.0/ctk_kit/showcase.py +1450 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/text.py +12 -5
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit.egg-info/PKG-INFO +31 -7
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit.egg-info/SOURCES.txt +2 -0
- ctk_kit-0.3.0/ctk_kit.egg-info/entry_points.txt +2 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/pyproject.toml +4 -1
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_package.py +28 -3
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_scroll_list.py +86 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_showcase.py +16 -23
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_text.py +10 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/LICENSE +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/buttons.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/dropdown.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/inputs.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/layout.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/py.typed +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/theme.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit/tokens.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit.egg-info/dependency_links.txt +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit.egg-info/requires.txt +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/ctk_kit.egg-info/top_level.txt +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/setup.cfg +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_buttons.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_dropdown.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_inputs.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_layout.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_theme.py +0 -0
- {ctk_kit-0.2.0 → ctk_kit-0.3.0}/tests/test_tokens.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctk-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: 面向 CustomTkinter 的轻量界面基件:深浅色令牌、常用控件与懒加载列表
|
|
5
5
|
Author-email: 加点盐 <1431705288@qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -63,12 +63,21 @@ primary_button(app, "新建", lambda: None).pack()
|
|
|
63
63
|
app.mainloop()
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
看实际效果:示例随包发布,装好 ctk-kit 后可以直接跑:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
pip install ctk-kit
|
|
70
|
+
ctk-kit-showcase
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
也可以从源码运行(不用安装):
|
|
67
74
|
|
|
68
75
|
```
|
|
69
76
|
git clone https://gitee.com/kuangjianke/ctk-kit
|
|
70
77
|
cd ctk-kit
|
|
71
78
|
python examples/showcase.py
|
|
79
|
+
# 等价写法
|
|
80
|
+
python -m ctk_kit.showcase
|
|
72
81
|
```
|
|
73
82
|
|
|
74
83
|
展示程序有九个页面:总览、基件、按钮、输入、布局、列表、主题、表单、设置,把每个公开构建都用一遍。
|
|
@@ -101,8 +110,10 @@ python examples/showcase.py
|
|
|
101
110
|
|
|
102
111
|
**大文本**(`text`)
|
|
103
112
|
|
|
104
|
-
- `TextArea(master, *, placeholder="", height=200, readonly=False, show_counter=True, on_change=None)`
|
|
105
|
-
—— 多行文本区:失焦时显示占位符、底部字数统计、可切换只读;读写用 `get()/set()/clear()
|
|
113
|
+
- `TextArea(master, *, placeholder="", height=200, readonly=False, show_counter=True, on_change=None, bg_color=None, text_color=None)`
|
|
114
|
+
—— 多行文本区:失焦时显示占位符、底部字数统计、可切换只读;读写用 `get()/set()/clear()`。
|
|
115
|
+
内部底色默认 `COLOR_INPUT`(输入框语义);当日志面板显示时传 `bg_color=COLOR_CARD` 或
|
|
116
|
+
`bg_color="transparent"`,就不会比所在卡片亮一截,`text_color` 也可单独覆盖
|
|
106
117
|
|
|
107
118
|
**下拉**(`dropdown`)
|
|
108
119
|
|
|
@@ -135,6 +146,17 @@ python examples/showcase.py
|
|
|
135
146
|
listing.render(items)
|
|
136
147
|
```
|
|
137
148
|
|
|
149
|
+
数据量很大(几千条)时,传入固定卡高开启虚拟滚动:只把视口附近的卡片放上布局,
|
|
150
|
+
滚出去的卡片留在内存里复用,滚回来直接放回去,不销毁也不重建。
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
listing = LazyList(frame, make_card, key_of=lambda item: item["id"], item_height=96)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
虚拟滚动要求卡片高度固定(例如自己 `pack_propagate(False)` 并设置 `height`),
|
|
157
|
+
并把卡片间距算进 `item_height`。`item_height` 用和控件尺寸一样的逻辑单位,
|
|
158
|
+
高 DPI 缩放下由 `LazyList` 自己换算。
|
|
159
|
+
|
|
138
160
|
## 设计令牌
|
|
139
161
|
|
|
140
162
|
颜色全部是 `(浅色, 深色)` 二元组,交给 `color_of()` 按当前外观取值:
|
|
@@ -159,10 +181,12 @@ ctk_kit/
|
|
|
159
181
|
├─ inputs.py 输入框 / 分段选择 / 开关
|
|
160
182
|
├─ dropdown.py 自绘下拉
|
|
161
183
|
├─ layout.py 页头、卡片、分隔线、空态、滚动容器
|
|
162
|
-
|
|
184
|
+
├─ scroll_list.py 懒加载列表
|
|
185
|
+
├─ text.py 多行文本区
|
|
186
|
+
└─ showcase.py 全组件展示(随包发布,`ctk-kit-showcase`)
|
|
163
187
|
|
|
164
188
|
examples/
|
|
165
|
-
└─ showcase.py
|
|
189
|
+
└─ showcase.py 仓库里的快捷入口(转发到 ctk_kit.showcase:main)
|
|
166
190
|
```
|
|
167
191
|
|
|
168
192
|
## 测试
|
|
@@ -173,7 +197,7 @@ pytest
|
|
|
173
197
|
```
|
|
174
198
|
|
|
175
199
|
测试跑在真实 Tk 窗口里(窗口全透明,不会在屏幕上闪),覆盖令牌、主题、按钮、输入框、
|
|
176
|
-
|
|
200
|
+
下拉、布局、懒加载列表、大文本区,以及 `ctk_kit/showcase.py` 的九个示例页;没有图形环境时相关用例自动 skip。
|
|
177
201
|
|
|
178
202
|
CI 在 GitHub Actions 上跑 Python 3.8 – 3.14 全矩阵(`.github/workflows/ci.yml`),
|
|
179
203
|
无头环境下用 Xvfb 提供虚拟屏幕;`CI` 环境变量存在时如果建不出窗口会直接失败,而不是静默跳过。
|
|
@@ -29,12 +29,21 @@ primary_button(app, "新建", lambda: None).pack()
|
|
|
29
29
|
app.mainloop()
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
看实际效果:示例随包发布,装好 ctk-kit 后可以直接跑:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pip install ctk-kit
|
|
36
|
+
ctk-kit-showcase
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
也可以从源码运行(不用安装):
|
|
33
40
|
|
|
34
41
|
```
|
|
35
42
|
git clone https://gitee.com/kuangjianke/ctk-kit
|
|
36
43
|
cd ctk-kit
|
|
37
44
|
python examples/showcase.py
|
|
45
|
+
# 等价写法
|
|
46
|
+
python -m ctk_kit.showcase
|
|
38
47
|
```
|
|
39
48
|
|
|
40
49
|
展示程序有九个页面:总览、基件、按钮、输入、布局、列表、主题、表单、设置,把每个公开构建都用一遍。
|
|
@@ -67,8 +76,10 @@ python examples/showcase.py
|
|
|
67
76
|
|
|
68
77
|
**大文本**(`text`)
|
|
69
78
|
|
|
70
|
-
- `TextArea(master, *, placeholder="", height=200, readonly=False, show_counter=True, on_change=None)`
|
|
71
|
-
—— 多行文本区:失焦时显示占位符、底部字数统计、可切换只读;读写用 `get()/set()/clear()
|
|
79
|
+
- `TextArea(master, *, placeholder="", height=200, readonly=False, show_counter=True, on_change=None, bg_color=None, text_color=None)`
|
|
80
|
+
—— 多行文本区:失焦时显示占位符、底部字数统计、可切换只读;读写用 `get()/set()/clear()`。
|
|
81
|
+
内部底色默认 `COLOR_INPUT`(输入框语义);当日志面板显示时传 `bg_color=COLOR_CARD` 或
|
|
82
|
+
`bg_color="transparent"`,就不会比所在卡片亮一截,`text_color` 也可单独覆盖
|
|
72
83
|
|
|
73
84
|
**下拉**(`dropdown`)
|
|
74
85
|
|
|
@@ -101,6 +112,17 @@ python examples/showcase.py
|
|
|
101
112
|
listing.render(items)
|
|
102
113
|
```
|
|
103
114
|
|
|
115
|
+
数据量很大(几千条)时,传入固定卡高开启虚拟滚动:只把视口附近的卡片放上布局,
|
|
116
|
+
滚出去的卡片留在内存里复用,滚回来直接放回去,不销毁也不重建。
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
listing = LazyList(frame, make_card, key_of=lambda item: item["id"], item_height=96)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
虚拟滚动要求卡片高度固定(例如自己 `pack_propagate(False)` 并设置 `height`),
|
|
123
|
+
并把卡片间距算进 `item_height`。`item_height` 用和控件尺寸一样的逻辑单位,
|
|
124
|
+
高 DPI 缩放下由 `LazyList` 自己换算。
|
|
125
|
+
|
|
104
126
|
## 设计令牌
|
|
105
127
|
|
|
106
128
|
颜色全部是 `(浅色, 深色)` 二元组,交给 `color_of()` 按当前外观取值:
|
|
@@ -125,10 +147,12 @@ ctk_kit/
|
|
|
125
147
|
├─ inputs.py 输入框 / 分段选择 / 开关
|
|
126
148
|
├─ dropdown.py 自绘下拉
|
|
127
149
|
├─ layout.py 页头、卡片、分隔线、空态、滚动容器
|
|
128
|
-
|
|
150
|
+
├─ scroll_list.py 懒加载列表
|
|
151
|
+
├─ text.py 多行文本区
|
|
152
|
+
└─ showcase.py 全组件展示(随包发布,`ctk-kit-showcase`)
|
|
129
153
|
|
|
130
154
|
examples/
|
|
131
|
-
└─ showcase.py
|
|
155
|
+
└─ showcase.py 仓库里的快捷入口(转发到 ctk_kit.showcase:main)
|
|
132
156
|
```
|
|
133
157
|
|
|
134
158
|
## 测试
|
|
@@ -139,7 +163,7 @@ pytest
|
|
|
139
163
|
```
|
|
140
164
|
|
|
141
165
|
测试跑在真实 Tk 窗口里(窗口全透明,不会在屏幕上闪),覆盖令牌、主题、按钮、输入框、
|
|
142
|
-
|
|
166
|
+
下拉、布局、懒加载列表、大文本区,以及 `ctk_kit/showcase.py` 的九个示例页;没有图形环境时相关用例自动 skip。
|
|
143
167
|
|
|
144
168
|
CI 在 GitHub Actions 上跑 Python 3.8 – 3.14 全矩阵(`.github/workflows/ci.yml`),
|
|
145
169
|
无头环境下用 Xvfb 提供虚拟屏幕;`CI` 环境变量存在时如果建不出窗口会直接失败,而不是静默跳过。
|
|
@@ -21,9 +21,12 @@
|
|
|
21
21
|
primary_button(box, "立即备份", command=lambda: print("go")).pack(padx=16, pady=16)
|
|
22
22
|
app.mainloop()
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
全组件展示(随包发布,装好后直接运行)::
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
ctk-kit-showcase
|
|
27
|
+
python -m ctk_kit.showcase
|
|
28
|
+
|
|
29
|
+
从源码运行时等价写法:``python examples/showcase.py``。
|
|
27
30
|
|
|
28
31
|
仓库: https://gitee.com/kuangjianke/ctk-kit
|
|
29
32
|
镜像: https://github.com/DashSalter/ctk-kit
|
|
@@ -31,7 +34,7 @@
|
|
|
31
34
|
|
|
32
35
|
from __future__ import annotations
|
|
33
36
|
|
|
34
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.3.0"
|
|
35
38
|
|
|
36
39
|
from . import tokens, theme
|
|
37
40
|
from .buttons import (
|
|
@@ -52,6 +52,11 @@ class LazyList(tk.Frame):
|
|
|
52
52
|
load_threshold: 可滚动区滚到多少比例触发追加,默认 0.92。
|
|
53
53
|
gap: 卡片上下的外边距,默认 ``(0, 10)``。
|
|
54
54
|
padx: 卡片左右留白(给滚动条让位),默认 ``(0, 8)``。
|
|
55
|
+
item_height: 固定卡片高度(含 ``gap``)。给定时开启虚拟滚动:只把
|
|
56
|
+
视口附近的卡片放上布局,滚出视口的卡片保留在内存里复用,滚回来
|
|
57
|
+
直接重新放上去,不销毁、不重建。适合几千条数据的列表;要求
|
|
58
|
+
``render_card`` 返回的卡片高度固定(例如自己 ``pack_propagate(False)``
|
|
59
|
+
并设置 ``height``),否则卡片会互相叠压。
|
|
55
60
|
"""
|
|
56
61
|
|
|
57
62
|
def __init__(
|
|
@@ -66,6 +71,7 @@ class LazyList(tk.Frame):
|
|
|
66
71
|
load_threshold: float = 0.92,
|
|
67
72
|
gap: tuple[int, int] = (0, 10),
|
|
68
73
|
padx: tuple[int, int] = (0, 8),
|
|
74
|
+
item_height: Optional[int] = None,
|
|
69
75
|
) -> None:
|
|
70
76
|
super().__init__(master, bg=color_of(T.COLOR_BG), highlightthickness=0)
|
|
71
77
|
self.render_card: RenderCard = render_card
|
|
@@ -76,6 +82,9 @@ class LazyList(tk.Frame):
|
|
|
76
82
|
self.load_threshold: float = load_threshold
|
|
77
83
|
self.gap: tuple[int, int] = gap
|
|
78
84
|
self.padx: tuple[int, int] = padx
|
|
85
|
+
#: 固定卡片高度(含间距)。给定时开启虚拟滚动:只把视口附近的卡片
|
|
86
|
+
#: 放上布局,滚过去的卡片留在内存里复用,不销毁,回滚时直接放回来。
|
|
87
|
+
self.item_height: Optional[int] = item_height
|
|
79
88
|
|
|
80
89
|
self._items: list[Any] = []
|
|
81
90
|
self._shown: int = 0
|
|
@@ -85,6 +94,10 @@ class LazyList(tk.Frame):
|
|
|
85
94
|
self._signature: Optional[tuple] = None
|
|
86
95
|
self._empty_box: Optional[tk.Frame] = None
|
|
87
96
|
self._scrollbar_visible: Optional[bool] = None
|
|
97
|
+
#: 虚拟模式:索引 -> 已建卡片(卡片不销毁,只在视口外 place_forget)。
|
|
98
|
+
self._built: dict[int, tk.Misc] = {}
|
|
99
|
+
self._range: Optional[tuple[int, int]] = None
|
|
100
|
+
self._resize_job: Optional[str] = None
|
|
88
101
|
|
|
89
102
|
self.grid_rowconfigure(0, weight=1)
|
|
90
103
|
self.grid_columnconfigure(0, weight=1)
|
|
@@ -157,6 +170,12 @@ class LazyList(tk.Frame):
|
|
|
157
170
|
self._empty_box = box
|
|
158
171
|
return
|
|
159
172
|
|
|
173
|
+
if self.item_height is not None:
|
|
174
|
+
self._layout_virtual()
|
|
175
|
+
self._suppress = False
|
|
176
|
+
self.after(1, self._refresh_scroll)
|
|
177
|
+
return
|
|
178
|
+
|
|
160
179
|
self._append(self.first_batch)
|
|
161
180
|
self._suppress = False
|
|
162
181
|
# 滚动条位置留到下一帧再算:set() 会同步布局,放在这里会拖慢首帧
|
|
@@ -189,14 +208,45 @@ class LazyList(tk.Frame):
|
|
|
189
208
|
|
|
190
209
|
@property
|
|
191
210
|
def cards(self) -> list[tk.Misc]:
|
|
192
|
-
"""当前已建出的卡片控件(副本,改动它不会影响列表)。
|
|
211
|
+
"""当前已建出的卡片控件(副本,改动它不会影响列表)。
|
|
212
|
+
|
|
213
|
+
虚拟模式下返回所有建过的卡片(含视口外未布局的),按索引排序。
|
|
214
|
+
"""
|
|
215
|
+
if self.item_height is not None:
|
|
216
|
+
return [self._built[index] for index in sorted(self._built)]
|
|
193
217
|
return list(self.inner.winfo_children())
|
|
194
218
|
|
|
195
219
|
def flush(self) -> None:
|
|
196
|
-
"""一次性补齐所有卡片(测试、导出时用)。
|
|
220
|
+
"""一次性补齐所有卡片(测试、导出时用)。
|
|
221
|
+
|
|
222
|
+
虚拟模式下只建卡、不改变视口,所以耗时也只和卡片总数有关。
|
|
223
|
+
"""
|
|
224
|
+
if self.item_height is not None:
|
|
225
|
+
for index in range(len(self._items)):
|
|
226
|
+
self._ensure_card(index)
|
|
227
|
+
self._sync_window(force=True)
|
|
228
|
+
return
|
|
197
229
|
self._cancel_load()
|
|
198
230
|
self._append(len(self._items) - self._shown)
|
|
199
231
|
|
|
232
|
+
def reset(self) -> None:
|
|
233
|
+
"""回到首屏:只保留 ``first_batch`` 张卡并把滚动位置归零。
|
|
234
|
+
|
|
235
|
+
适合页面被隐藏时调用。已展开几百张卡时,控件数量会让重新显示
|
|
236
|
+
或窗口缩放明显变慢;缩回首屏后重排成本就回到常数级。
|
|
237
|
+
|
|
238
|
+
虚拟模式下卡片本来就只布局视口附近,所以这里只把滚动位置归零,
|
|
239
|
+
已建卡片全部保留复用,不会重建。
|
|
240
|
+
"""
|
|
241
|
+
if not self._items:
|
|
242
|
+
return
|
|
243
|
+
if self.item_height is not None:
|
|
244
|
+
self.canvas.yview_moveto(0)
|
|
245
|
+
self._sync_window(force=True)
|
|
246
|
+
return
|
|
247
|
+
self._signature = None
|
|
248
|
+
self.render(self._items, force=True)
|
|
249
|
+
|
|
200
250
|
def scroll_units(self, steps: int) -> None:
|
|
201
251
|
"""按「行」滚动;steps>0 向下,steps<0 向上。
|
|
202
252
|
|
|
@@ -225,6 +275,7 @@ class LazyList(tk.Frame):
|
|
|
225
275
|
def destroy(self) -> None:
|
|
226
276
|
"""停止滚轮绑定后销毁。"""
|
|
227
277
|
self._alive = False
|
|
278
|
+
self._cancel_resize()
|
|
228
279
|
super().destroy()
|
|
229
280
|
|
|
230
281
|
# ------------------------------------------------------------------ #
|
|
@@ -256,8 +307,11 @@ class LazyList(tk.Frame):
|
|
|
256
307
|
|
|
257
308
|
def _sync_scrollbar(self) -> None:
|
|
258
309
|
"""内容装得下时把滑块涂成背景色(布局宽度不变)。"""
|
|
259
|
-
|
|
260
|
-
|
|
310
|
+
if self.item_height is not None:
|
|
311
|
+
content = len(self._items) * self.item_height * self._card_scale()
|
|
312
|
+
else:
|
|
313
|
+
box = self.canvas.bbox("all")
|
|
314
|
+
content = (box[3] - box[1]) if box else 0
|
|
261
315
|
visible = content > self.canvas.winfo_height() + 1
|
|
262
316
|
if visible == self._scrollbar_visible:
|
|
263
317
|
return
|
|
@@ -275,6 +329,8 @@ class LazyList(tk.Frame):
|
|
|
275
329
|
放在定时器里:Tk 要跑完一次事件循环才会更新内层 Frame 的尺寸,
|
|
276
330
|
这时 bbox 才是准的。
|
|
277
331
|
"""
|
|
332
|
+
if self.item_height is not None:
|
|
333
|
+
return
|
|
278
334
|
if self._shown >= len(self._items):
|
|
279
335
|
return
|
|
280
336
|
box = self.canvas.bbox("all")
|
|
@@ -294,6 +350,10 @@ class LazyList(tk.Frame):
|
|
|
294
350
|
if self._suppress:
|
|
295
351
|
return
|
|
296
352
|
self.scrollbar.set(first, last)
|
|
353
|
+
if self.item_height is not None:
|
|
354
|
+
self._sync_scrollbar()
|
|
355
|
+
self._sync_window(float(first), float(last))
|
|
356
|
+
return
|
|
297
357
|
if self._loading or self._shown >= len(self._items):
|
|
298
358
|
return
|
|
299
359
|
# 不信回调参数:scrollregion 刚变时会回放一次 (0, 1)
|
|
@@ -314,12 +374,23 @@ class LazyList(tk.Frame):
|
|
|
314
374
|
self.after_cancel(self._load_job)
|
|
315
375
|
self._load_job = None
|
|
316
376
|
|
|
377
|
+
def _cancel_resize(self) -> None:
|
|
378
|
+
"""取消还没触发的窗口尺寸同步任务。"""
|
|
379
|
+
if self._resize_job is not None:
|
|
380
|
+
self.after_cancel(self._resize_job)
|
|
381
|
+
self._resize_job = None
|
|
382
|
+
|
|
317
383
|
def _clear_cards(self) -> None:
|
|
318
384
|
"""清空内层卡片并复位滚动位置。"""
|
|
319
385
|
self._loading = False
|
|
320
386
|
self._cancel_load()
|
|
387
|
+
self._cancel_resize()
|
|
388
|
+
self._built.clear()
|
|
389
|
+
self._range = None
|
|
321
390
|
for child in self.inner.winfo_children():
|
|
322
391
|
child.destroy()
|
|
392
|
+
if self.item_height is not None:
|
|
393
|
+
self.inner.configure(height=0)
|
|
323
394
|
self._empty_box = None
|
|
324
395
|
self.canvas.yview_moveto(0)
|
|
325
396
|
self._sync_scrollregion()
|
|
@@ -334,9 +405,101 @@ class LazyList(tk.Frame):
|
|
|
334
405
|
|
|
335
406
|
def _sync_scrollregion(self, _event: Optional[tk.Event] = None) -> None:
|
|
336
407
|
"""把 canvas 的可滚动区域更新为内容包围盒。"""
|
|
408
|
+
if self.item_height is not None and self._items:
|
|
409
|
+
# 虚拟模式的内容高度是算出来的,不依赖当前放上布局的卡片
|
|
410
|
+
height = self.item_height * len(self._items) * self._card_scale()
|
|
411
|
+
self.canvas.configure(
|
|
412
|
+
scrollregion=(0, 0, self.canvas.winfo_width(), height)
|
|
413
|
+
)
|
|
414
|
+
return
|
|
337
415
|
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
|
338
416
|
|
|
339
417
|
def _on_canvas_resize(self, event: tk.Event) -> None:
|
|
340
418
|
"""canvas 宽度变化时让内层 Frame 跟着变宽。"""
|
|
341
419
|
self.canvas.itemconfigure(self._window, width=event.width)
|
|
342
|
-
self.
|
|
420
|
+
if self.item_height is None:
|
|
421
|
+
self.after(1, self._sync_scrollbar)
|
|
422
|
+
return
|
|
423
|
+
# 拖动窗口时 Configure 会连发很多次,合并成一次,避免反复排版
|
|
424
|
+
if self._resize_job is not None:
|
|
425
|
+
self.after_cancel(self._resize_job)
|
|
426
|
+
self._resize_job = self.after(60, self._after_resize)
|
|
427
|
+
|
|
428
|
+
def _after_resize(self) -> None:
|
|
429
|
+
"""窗口尺寸稳定后补一次滚动条与视口同步。"""
|
|
430
|
+
self._resize_job = None
|
|
431
|
+
self._sync_scrollbar()
|
|
432
|
+
if self.item_height is not None:
|
|
433
|
+
self._sync_window()
|
|
434
|
+
|
|
435
|
+
# ------------------------------------------------------------------ #
|
|
436
|
+
# 虚拟滚动
|
|
437
|
+
def _card_scale(self) -> float:
|
|
438
|
+
"""卡片 place 时 CTk 会按窗口缩放放大坐标,这里取同一个系数。"""
|
|
439
|
+
try:
|
|
440
|
+
scale = float(ctk.ScalingTracker.get_widget_scaling(self))
|
|
441
|
+
except Exception:
|
|
442
|
+
scale = 1.0
|
|
443
|
+
return scale if scale > 0 else 1.0
|
|
444
|
+
|
|
445
|
+
def _layout_virtual(self) -> None:
|
|
446
|
+
"""按固定卡片高度把内层撑到总高,然后只铺视口附近的卡片。"""
|
|
447
|
+
height = self.item_height or 0
|
|
448
|
+
total = len(self._items) * height * self._card_scale()
|
|
449
|
+
self.inner.configure(height=total)
|
|
450
|
+
self.canvas.itemconfigure(self._window, width=self.canvas.winfo_width())
|
|
451
|
+
self.canvas.configure(
|
|
452
|
+
scrollregion=(0, 0, self.canvas.winfo_width(), total)
|
|
453
|
+
)
|
|
454
|
+
self._sync_window(force=True)
|
|
455
|
+
|
|
456
|
+
def _ensure_card(self, index: int) -> tk.Misc:
|
|
457
|
+
"""按索引取卡:建过的直接复用,没建过的才调 ``render_card``。"""
|
|
458
|
+
card = self._built.get(index)
|
|
459
|
+
if card is None:
|
|
460
|
+
card = self.render_card(self.inner, self._items[index])
|
|
461
|
+
self._built[index] = card
|
|
462
|
+
return card
|
|
463
|
+
|
|
464
|
+
def _sync_window(
|
|
465
|
+
self, first: Optional[float] = None, last: Optional[float] = None, force: bool = False
|
|
466
|
+
) -> None:
|
|
467
|
+
"""把视口附近的卡片放上布局,滚出视口的收起(保留对象不销毁)。
|
|
468
|
+
|
|
469
|
+
``first`` / ``last`` 由 yscrollcommand 回调直接给出;不传时读
|
|
470
|
+
``canvas.yview()``。滚动过程中回调参数比再读一次更接近真实位置。
|
|
471
|
+
"""
|
|
472
|
+
height = self.item_height
|
|
473
|
+
if height is None:
|
|
474
|
+
return
|
|
475
|
+
count = len(self._items)
|
|
476
|
+
if count == 0:
|
|
477
|
+
return
|
|
478
|
+
# 内层是普通 tk.Frame,高度按物理像素算;卡片 place 的坐标是逻辑值,
|
|
479
|
+
# CTk 会再乘一次缩放,所以总高要乘缩放系数,否则滚动位置会错位。
|
|
480
|
+
scale = self._card_scale()
|
|
481
|
+
total = count * height * scale
|
|
482
|
+
step = height * scale
|
|
483
|
+
if int(self.inner.cget("height")) != total:
|
|
484
|
+
# 任何地方把内层改小时都拉回总高,否则滚动条比例会错
|
|
485
|
+
self.inner.configure(height=total)
|
|
486
|
+
self.canvas.configure(
|
|
487
|
+
scrollregion=(0, 0, self.canvas.winfo_width(), total)
|
|
488
|
+
)
|
|
489
|
+
if first is None or last is None:
|
|
490
|
+
current = self.canvas.yview()
|
|
491
|
+
first, last = float(current[0]), float(current[1])
|
|
492
|
+
# 缓冲几张,快速滚动或拖动滚动条时不会露出空白
|
|
493
|
+
start = max(0, int(first * total // step) - 3)
|
|
494
|
+
end = min(count, int(last * total // step) + 4)
|
|
495
|
+
if not force and (start, end) == self._range:
|
|
496
|
+
return
|
|
497
|
+
self._range = (start, end)
|
|
498
|
+
for index in range(start, end):
|
|
499
|
+
card = self._ensure_card(index)
|
|
500
|
+
# CTk 控件禁止在 place() 里传 width/height,卡片高度由
|
|
501
|
+
# render_card 自己固定;这里只负责纵向定位和铺满宽度。
|
|
502
|
+
card.place(x=self.padx[0], y=index * height + self.gap[0], relwidth=1)
|
|
503
|
+
for index, card in list(self._built.items()):
|
|
504
|
+
if index < start or index >= end:
|
|
505
|
+
card.place_forget()
|