tkflu.theme_transition · theme_transition 主题过渡时间轴
主题过渡:把整棵控件树的换肤放到同一条时间轴上。
为什么需要它
"一键换肤"的朴素实现是这样写的(见 0.4.0 之前的 :mod:tkflu.thememanager)::
for widget in window.winfo_children():
widget.theme(mode=mode) # 每个组件内部各自排一串 after 做自己的过渡
widget._draw()
widget.update() # ← 这一句把事件循环跑了起来
问题有三个,且互相放大:
- 阻塞。
update()会处理全部待办事件(含用户输入、重绘、定时器)。 组件越多,这一趟循环就越长;实测 41 个组件的画廊里FluThemeManager.mode()单次调用要 2.1 秒,这期间窗口完全点不动。 - 不同步。每个组件的过渡各排各的
after,起点是"轮到它自己"的那一刻。 而上面那个update()又会把已经到点、属于前面组件的帧就地执行掉, 于是画面变成"一个一个地变色"。 - 动画被吃掉。真正跑完的帧全在那次
mode()调用里消耗掉了, 用户看到的不是过渡,而是"卡两秒然后瞬间变色"。
这里换一种组织方式:
- 先静默地算目标。在 :func:
~tkflu.designs.animation.suspended_widget_animation保护下依次调用各组件原有的theme(),于是配色被写进attributes, 但组件自己的过渡动画全部让路。这一趟不碰事件循环,也不重绘。 - 再统一插值。对每个组件做一次
attributes快照,与目标值逐叶子比对, 挑出能插值的叶子(#rrggbb颜色、*_opacity数值)。 - 一条时间轴画所有组件。整棵树共用
steps帧、一张after链, 每帧把所有组件的中间色一次性写进去再统一重绘 —— 这才是"同步"。 - 时间轴自己会丢帧。帧索引由真实流逝的时间算出来,而不是"排了几帧"。
某一帧画得太慢时,下一帧直接跳到该到的位置,因此总时长始终是
steps × step_time,不会因为组件多而拖尾。
对组件的要求只有一条:配色状态放在 self.attributes 里(这本就是 tkfluent 的
一贯做法)。基于"设计规范字典"重算配色的组件(FluCheckBox / FluRadioBox /
FluListBox / FluLiteNav)需要在它们的 _apply_design() 里调一次
:func:blend_theme_design,否则 _draw() 里的重算会把中间色覆盖掉。
用法::
from tkflu.theme_transition import run_theme_transition
run_theme_transition(root, "dark") # 整棵树一起过渡
run_theme_transition(root, "dark", steps=0) # 只要结果,不要动画
ThemeTransition
一次"整棵树同步换肤"。
:param root: 子树的根控件(通常是窗口)
:param mode: "light" / "dark"
:param steps: 帧数;None 表示读全局配置
:param step_time: 每帧毫秒;None 表示读全局配置
:param easing: 缓动曲线名或可调用对象;None 表示读全局配置
源代码位于: tkflu\theme_transition.py
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 | |
animated
property
这次换肤到底要不要动画。
finished
property
过渡是否已经收尾(帧跑完 / 被取消 / 直接落定都算)。
requested_steps
property
调用方要求的帧数(可能因为渲染太慢被 :meth:_start 调低)。
cancel(restore=True)
中断这次过渡。
:param restore: True(默认)把控件落回本次的目标配色;
False 保留当前中间色——紧接着要起一次新过渡时用这个,
新过渡会以当前中间色为起点,画面是连续的。
源代码位于: tkflu\theme_transition.py
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 | |
describe()
一句话说明这次过渡实际发生了什么(日志 / 基准脚本用)。
源代码位于: tkflu\theme_transition.py
852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 | |
finish(paint=True)
落到目标配色并收尾(可重复调用)。
.. note::
收尾只重绘一次:先把各组件的确切目标值写回 attributes
(插值出来的最后一帧可能还差一点点浮点),再统一画一遍。
旧写法是先 _paint(1.0) 再来一轮 restore() + _draw(),
等于白画一整棵树——在 tksvg 下就是白等几百毫秒。
源代码位于: tkflu\theme_transition.py
797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 | |
run()
执行一次换肤:先静默落目标值,再(可选地)拉起统一过渡。
.. note::
这里一帧都不画。静默应用阶段所有组件的 _draw() 都被屏蔽了
(见 :func:_suspend_draw),所以屏幕仍然停在换肤前的样子——
那正好就是过渡的第 0 帧。真正第一笔重绘发生在下一拍的 _start 里,
调用方因此能立刻拿回控制权。在默认的 tksvg 引擎上,一次全树重绘
要几百毫秒,同步做掉就等于"点了按钮卡半秒"。
源代码位于: tkflu\theme_transition.py
564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 | |
WidgetThemePlan
一个组件在这次换肤里的"起点 → 终点"。
只保存能插值的叶子(颜色 / 不透明度)。其它字段(线宽、圆角、 字体、回调)在静默应用阶段就已经落成目标值了,过渡期间不该动它们—— 把它们也插值会让边框"长出来"、圆角"变形"。
源代码位于: tkflu\theme_transition.py
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 | |
t
property
最近一帧的进度(0→1),供 :func:blend_theme_design 读取。
__init__(widget, before, after, after_tree)
:param before: 换肤前的叶子表(路径 → 值) :param after: 换肤后的叶子表 :param after_tree: 换肤后的嵌套结构(保留 EasyDict 等原类型), 用来生成每帧要写回去的子树、以及收尾时恢复目标值
.. note::
顶层就是叶子的情况非常常见(FluLabel 只有
attributes.text_color、FluBadge 只有 back_color),
所以 :meth:values_at 必须单独处理"路径长度为 1"的叶子——
否则 path[1:] 是空元组,回写时会 IndexError。
源代码位于: tkflu\theme_transition.py
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
blend_into(target, t)
把 target(本轮目标配色字典)里的可插值叶子换成中间色。
源代码位于: tkflu\theme_transition.py
332 333 334 335 336 337 338 339 340 341 342 343 344 345 | |
restore()
把 attributes 恢复成本轮的目标值(过渡收尾 / 中断时用)。
从 after_tree 现取一份新的副本,而不是用 _templates——
后者在每一帧里都被就地改写成中间色了。
源代码位于: tkflu\theme_transition.py
347 348 349 350 351 352 353 354 355 | |
values_at(t)
这一帧要写进 attributes 的顶层键 →(含中间色的)子树。
源代码位于: tkflu\theme_transition.py
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | |
active_transition()
当前正在跑的过渡;没有则为 None。
源代码位于: tkflu\theme_transition.py
489 490 491 | |
applying_themes()
是否正处在"统一过渡的静默应用"阶段。
组件里那些会递归的 theme()(FluFrameCanvas / FluMenuBar /
FluToplevel)靠它判断"这一趟是不是别人在指挥":是的话就只改自己的配色,
子控件交给驱动方,免得同一棵子树被换两遍肤。
源代码位于: tkflu\theme_transition.py
494 495 496 497 498 499 500 501 | |
blend_theme_design(widget, design)
给"每次 _draw() 都重算配色"的组件用的钩子。
FluCheckBox / FluRadioBox / FluListBox / FluLiteNav 的
_draw() 开头会调 _apply_design():它按当前
(mode, 状态) 重新查一遍设计规范,于是刚写进去的中间色会被覆盖掉。
在 _apply_design() 里套一层这个函数,中间色就能保住。
没有正在进行的过渡(或这个组件不参与过渡)时原样返回。
:param widget: 组件自己
:param design: _apply_design() 刚算出来的目标配色
源代码位于: tkflu\theme_transition.py
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 | |
collect_themed_widgets(root)
广度优先收集 root 子树里所有"持有主题状态"的控件。
- 父控件排在子控件前面(重绘顺序稳定,容器先画底、子控件后画面);
- 按对象身份去重(同一个控件只会被换肤一次);
- 跳过带
_theme_proxy = True的容器画布:FluFrameCanvas的theme()只是转发给它内嵌的FluFrame,主题状态并不属于画布。 整棵树既然会被走一遍,那个FluFrame自己就会被收集到, 再让画布转发一次就等于把整套配色算两遍。
源代码位于: tkflu\theme_transition.py
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 | |
get_transition_budget()
当前的主题过渡总时长上限(毫秒)。
源代码位于: tkflu\theme_transition.py
477 478 479 480 481 482 483 484 485 486 | |
interpolatable(path, before, after)
公开的小判定:path 上的这两个值之间能不能做过渡插值。
::
interpolatable(("back_color",), "#ffffff", "#000000") # True
interpolatable(("border_width",), 1, 3) # False
源代码位于: tkflu\theme_transition.py
222 223 224 225 226 227 228 229 230 | |
run_theme_transition(root, mode, steps=None, step_time=None, easing=None)
把 root 子树整体换到 mode,所有控件共用一条时间轴。
:param root: 子树根控件
:param mode: "light" / "dark"
:param steps: 帧数;None 读全局配置(:func:tkflu.set_animation_steps)
:param step_time: 每帧毫秒;None 读全局配置
:param easing: 缓动曲线名;None 读全局配置(默认 ease_in_out)
:returns: 这次过渡(动画关闭时返回的是已经收尾的实例)
.. note::
同一时刻只保留一个过渡:新的会把旧的就地接管(保留中间色作为新起点),
所以用户连点"切换主题"不会叠加出越跑越慢的帧队列。
处于静默应用阶段的嵌套调用会被忽略——那种调用来自组件自己的 theme()。
源代码位于: tkflu\theme_transition.py
905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 | |
set_transition_budget(milliseconds)
设置主题过渡的总时长上限(毫秒);0 表示不限制。
源代码位于: tkflu\theme_transition.py
472 473 474 | |