找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 128|回复: 2

[已解决] 让按钮按下时切换图片吧-PressImagebutton

[复制链接]
发表于 2024-10-30 16:38:04 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
前言:
一开始使用 Renpy 时就发现 Renpy 原生 Button 组件在按下时并没有对应的样式来切换图片, 觉得不妥, 决定自己实现一个, 源码后是我实现这个组件的思路

[RenPy] 纯文本查看 复制代码

'''

Copyright 2024.10.30 Koji-Huang(1447396418@qq.com)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

'''

"""renpy

python early:
"""


import pygame


class PressImageButton(renpy.display.behavior.ImageButton):
    def __init__(self, 
                idle = None,
                hover = None,
                insensitive = None,
                activate = None,
                press = None, 
                idle_image = None,
                hover_image = None,
                insensitive_image = None,   
                activate_image = None,
                press_image = None, 
                selected_idle = None,
                selected_hover = None,
                selected_insensitive = None,
                selected_activate = None,
                selected_press = None,
                selected_idle_image = None,
                selected_hover_image = None,
                selected_insensitive_image = None,
                selected_activate_image = None,
                selected_press_image = None, 
                auto = None,
                *args, **kwargs):


        # COPY from "renpy/ui.py" -> "def _imagebutton" -> "def __init__"
        def choice(a, b, name, required=False, auto = None):
            if a:
                return a

            if b:
                return b

            if auto is not None:
                rv = renpy.config.imagemap_auto_function(auto, name)
                if rv is not None:
                    return rv

            if required:
                if auto:
                    raise Exception("Imagebutton does not have a %s image. (auto=%r)." % (name, auto))
                else:
                    raise Exception("Imagebutton does not have a %s image." % (name,))

            return None
        

        idle = choice(idle, idle_image, "idle", required=True, auto=auto)
        hover = choice(hover, hover_image, "hover", auto=auto)
        insensitive = choice(insensitive, insensitive_image, "insensitive", auto=auto)
        selected_idle = choice(selected_idle, selected_idle_image, "selected_idle", auto=auto)
        selected_hover = choice(selected_hover, selected_hover_image, "selected_hover", auto=auto)
        selected_insensitive = choice(selected_insensitive, selected_insensitive_image, "selected_insensitive", auto=auto)

        super(PressImageButton, self).__init__(idle, hover, insensitive, selected_idle, selected_hover, selected_insensitive, *args, **kwargs)

        press_image = press_image if press_image is not None else press
        selected_press_image = selected_press_image if press_image is not None else selected_press

        self.background_state = 0  # 0: idle  1: hover  2: select

        self.state_children['press_'] = renpy.easy.displayable(press_image) if press_image is not None else self.state_children['idle_']
        self.state_children['selected_press_'] = renpy.easy.displayable(selected_press_image) if selected_press_image is not None else self.state_children['selected_idle_']



    def event(self, ev, x, y, st):
        is_in_range = int(self.is_hovered(x, y))
        background_state_last = self.background_state

        if self.background_state - is_in_range and self.background_state != 2:
            self.background_state = is_in_range
            renpy.redraw(self, 0)

        elif ev.type == pygame.MOUSEBUTTONDOWN and is_in_range:
            self.background_state = 2
            renpy.redraw(self, 0)

        elif ev.type == pygame.MOUSEBUTTONUP and self.background_state == 2:
            self.background_state = is_in_range
            renpy.redraw(self, 0)

        return super(PressImageButton, self).event(ev, x, y, st)


    def get_child(self):
        # COPY from "renpy/display/behavior.py" -> "class ImageButton" -> "def get_child" to fix

        raw_child = self.style.child or self.state_children[self.style.prefix]

        # it's strange that renpy.is_sensitive(None) will return True...
        if (self.action is not None and renpy.is_sensitive(self.action)):
            if self.background_state == 2:
                raw_child = self.state_children['selected_press_' if self.is_selected() else "press_"] 

        if raw_child is not self.imagebutton_raw_child:
            self.imagebutton_raw_child = raw_child

            if raw_child._duplicatable:
                self.imagebutton_child = raw_child._duplicate(None)
                self.imagebutton_child._unique()
            else:
                self.imagebutton_child = raw_child

            self.imagebutton_child.per_interact()

        return self.imagebutton_child

    @property
    def displaying_size(self):
        """
        Because the child weight's size is not a const. so I have to use such a ugly way to get the size of child weight.
        Actually, I think window_size can also be used, but not every weight is the son class of window, it doesn't work perfectly. So I give up the try. 
        If child widget's size have been changed, it can't update it. If you found the code didn't work as predict, just try another way to get child's size.
        """
        child = self.get_child()
        if 'displaying_size' not in child.__dir__():
            child.displaying_size = child.render(1920, 1080, 0, 0).get_size()
        return child.displaying_size

    def is_hovered(self, x, y):
        return 0 < x < self.displaying_size[0] and 0 < y < self.displaying_size[1]


renpy.register_sl_displayable("PressImageButton", PressImageButton, "", 1
    ).add_property("press"
    ).add_property("hover"
    ).add_property("idle"
    ).add_property("insensitive"
    ).add_property("activate"
    ).add_property("press_image"
    ).add_property("hover_image"
    ).add_property("idle_image"
    ).add_property("insensitive_image"
    ).add_property("activate_image"
    ).add_property("selected_idle"
    ).add_property("selected_hover"
    ).add_property("selected_insensitive"
    ).add_property("selected_activate"
    ).add_property("selected_press"
    ).add_property("selected_idle_image"
    ).add_property("selected_hover_image"
    ).add_property("selected_insensitive_image"
    ).add_property("selected_activate_image"
    ).add_property("selected_press_image"
    ).add_property("style"
    ).add_property("clicked"
    ).add_property("hovered"
    ).add_property_group("button")



使用方法:

把代码复制到一个 xxx_ren.py 文件里后放到工程里就可以使用了

新增的两个参数:
    press & press_image: 鼠标按下时显示的图像
    select_press &  select_press_image: 鼠标按下并处于选中状态时显示的图像

[RenPy] 纯文本查看 复制代码
[backcolor=rgb(31, 31, 31)]
PressImageButton:
        idle "background.png"
        hover "background_hover.png"
        press "background_press.png"  # 按下时的图片
        action Function(print, "")
        xpos 100
        ypos 100



                               
登录/注册后可看大图


解题思路:

最开始, 我翻阅了 `ImageButton`(renpy-sdk\renpy\display\behavior.py) 的源码, 发现 `Imagebutton` 反自觉地没有重写过 `render` 函数, 反而是重写了 `get_child` 函数. 换句话来说, Imagebutton 并不是通过修改 render 函数来改变样式的, 所以我们追溯一下 `ImageButton` 的父类: `Button`

翻阅一下 `Button` 的源码, 你会发现 `Button`(renpy-sdk\renpy\display\behavior.py) 的 `render` 函数里有一个贯穿整个函数的线索:
[RenPy] 纯文本查看 复制代码
rv = super(Button, self).render(width, height, st, at)

阅读 `render` 函数, 不难发现 `rv` 在 `render` 函数里仅仅进行了一些 `Style` 的变化, 实际上 `rv` 的获取仍然是父类的函数提供的, 而且 `rv` 也是 `render` 函数的返回值, 所以可以断定, `rv` 是由父组件决定的

追踪 `Button` 的父类 `Window` (renpy-sdk\renpy\display\layout.py) 的 `render` 函数, 我们可以看到一个熟悉的影子: `child = self.get_child()`, 而这个 `child` 最后被用来生成 `Render` 对象并返回, 换句话来说, `Button` 组件是通过 `get_child` 函数获取子控件后返回子控件的 `Render` 并进行一些处理后返回的.

简单来说就是: `get_child()` [生成一个渲染对象] -> `Window.render()` [初步处理一下渲染对象] -> `Button.render()` [再次处理一下渲染对象] -> 渲染结果  (注意, 实际上的调用流程是自子到父的, 这里反过来写只是为了方便理解)

至此, 解决这个问题的思路就出来了: 我们只需要改写 get_child 函数的返回值就可以改变最终的渲染结果.

我使用了一个变量来储存鼠标的状态, 并在 `event` 函数触发时更新并检查这个状态, 假如状态更新为"按钮正在被按下"或者"按钮不再被按下"时, 使用`renpy.redraw`来更新组件, 而 `redraw` 调用了 `render` 函数, `render` 调用了 `get_child` 函数, 而重写过的 `get_child` 在 "按钮正在被按下" 状态时会改变原本的返回值为我们定义的图像

此外, 这个 `CDD` 还有一个检测子控件范围的函数(自己写出来的), 不一定适合所有的组件, 所以假如你发现我写的这个 `CDD` 按下按钮的触发范围不符合预期的话, 可以自己修改一下 `displaying_size` 的返回值, 这个函数用于返回 `Press` 的判定范围

顺带一提, 由于是 `CDD`, 这个控件的更新不同于正常的 `Button` 组件, 所以不需要 `restart_interaction` 函数进行更新




最后
其实我觉得这个轮子早就应该有人做过了的, 但是我懒得找, 再加上刚刚接触 `CDD`, 就当是练手了.

实际上这个组件里面的实现方式还有更优的解法, 但是我懒得继续研究了 (笑),

此外, 我尽可能的保留了 imagebutton 的特性, 但我不敢保证所有的特性都支持了, 假如有哪些特性不支持, 或者有更好的解题思路, 欢迎提出来

 楼主| 发表于 2024-11-1 17:23:51 | 显示全部楼层
更新:
增加了两个新的参数

press_sound:  按下按钮时触发的音效
press_sound_channel:  音效播放的通道

[RenPy] 纯文本查看 复制代码
'''

Copyright 2024.10.30 Koji-Huang([email]1447396418@qq.com[/email])

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

'''

"""renpy

python early:
"""


import pygame


class PressImageButton(renpy.display.behavior.ImageButton):
    def __init__(self, 
                idle = None,
                hover = None,
                insensitive = None,
                activate = None,
                press = None, 
                idle_image = None,
                hover_image = None,
                insensitive_image = None,   
                activate_image = None,
                press_image = None, 
                selected_idle = None,
                selected_hover = None,
                selected_insensitive = None,
                selected_activate = None,
                selected_press = None,
                selected_idle_image = None,
                selected_hover_image = None,
                selected_insensitive_image = None,
                selected_activate_image = None,
                selected_press_image = None, 
                press_sound = None,
                press_sound_channel = 'audio',
                auto = None,
                *args, **kwargs):


        # COPY from "renpy/ui.py" -> "def _imagebutton" -> "def __init__"
        def choice(a, b, name, required=False, auto = None):
            if a:
                return a

            if b:
                return b

            if auto is not None:
                rv = renpy.config.imagemap_auto_function(auto, name)
                if rv is not None:
                    return rv

            if required:
                if auto:
                    raise Exception("Imagebutton does not have a %s image. (auto=%r)." % (name, auto))
                else:
                    raise Exception("Imagebutton does not have a %s image." % (name,))

            return None
        

        idle = choice(idle, idle_image, "idle", required=True, auto=auto)
        hover = choice(hover, hover_image, "hover", auto=auto)
        insensitive = choice(insensitive, insensitive_image, "insensitive", auto=auto)
        selected_idle = choice(selected_idle, selected_idle_image, "selected_idle", auto=auto)
        selected_hover = choice(selected_hover, selected_hover_image, "selected_hover", auto=auto)
        selected_insensitive = choice(selected_insensitive, selected_insensitive_image, "selected_insensitive", auto=auto)

        super(PressImageButton, self).__init__(idle, hover, insensitive, selected_idle, selected_hover, selected_insensitive, *args, **kwargs)

        press_image = press_image if press_image is not None else press
        selected_press_image = selected_press_image if press_image is not None else selected_press

        self.background_state = 0  # 0: idle  1: hover  2: select

        self.state_children['press_'] = renpy.easy.displayable(press_image) if press_image is not None else self.state_children['idle_']
        self.state_children['selected_press_'] = renpy.easy.displayable(selected_press_image) if selected_press_image is not None else self.state_children['selected_idle_']

        self.press_sound = press_sound
        self.press_sound_channel = press_sound_channel


    def event(self, ev, x, y, st):
        is_in_range = int(self.is_hovered(x, y))
        background_state_last = self.background_state

        if self.background_state - is_in_range and self.background_state != 2:
            self.background_state = is_in_range
            renpy.redraw(self, 0)

        elif ev.type == pygame.MOUSEBUTTONDOWN and is_in_range:
            
            if self.press_sound is not None:
                renpy.music.play(self.press_sound, channel=self.press_sound_channel)

            self.background_state = 2
            renpy.redraw(self, 0)

        elif ev.type == pygame.MOUSEBUTTONUP and self.background_state == 2:
            self.background_state = is_in_range
            renpy.redraw(self, 0)

        return super(PressImageButton, self).event(ev, x, y, st)


    def get_child(self):
        # COPY from "renpy/display/behavior.py" -> "class ImageButton" -> "def get_child" to fix

        raw_child = self.style.child or self.state_children[self.style.prefix]

        # it's strange that renpy.is_sensitive(None) will return True...
        if self.action is not None and renpy.is_sensitive(self.action):
            if self.background_state == 2:
                raw_child = self.state_children['selected_press_' if self.is_selected() else "press_"] 

        if raw_child is not self.imagebutton_raw_child:
            self.imagebutton_raw_child = raw_child

            if raw_child._duplicatable:
                self.imagebutton_child = raw_child._duplicate(None)
                self.imagebutton_child._unique()
            else:
                self.imagebutton_child = raw_child

            self.imagebutton_child.per_interact()

        return self.imagebutton_child

    @property
    def displaying_size(self):
        """
        Because the child weight's size is not a const. so I have to use such a ugly way to get the size of child weight.
        Actually, I think window_size can also be used, but not every weight is the son class of window, it doesn't work perfectly. So I give up the try. 
        If child widget's size have been changed, it can't update it. If you found the code didn't work as predict, just try another way to get child's size.
        """
        child = self.get_child()
        if 'displaying_size' not in child.__dir__():
            child.displaying_size = child.render(1920, 1080, 0, 0).get_size()
        return child.displaying_size

    def is_hovered(self, x, y):
        return 0 < x < self.displaying_size[0] and 0 < y < self.displaying_size[1]


renpy.register_sl_displayable("PressImageButton", PressImageButton, "", 1
    ).add_property("press"
    ).add_property("hover"
    ).add_property("idle"
    ).add_property("insensitive"
    ).add_property("activate"
    ).add_property("press_image"
    ).add_property("hover_image"
    ).add_property("idle_image"
    ).add_property("insensitive_image"
    ).add_property("activate_image"
    ).add_property("selected_idle"
    ).add_property("selected_hover"
    ).add_property("selected_insensitive"
    ).add_property("selected_activate"
    ).add_property("selected_press"
    ).add_property("selected_idle_image"
    ).add_property("selected_hover_image"
    ).add_property("selected_insensitive_image"
    ).add_property("selected_activate_image"
    ).add_property("selected_press_image"
    ).add_property("style"
    ).add_property("clicked"
    ).add_property("hovered"
    ).add_property("press_sound_channel"
    ).add_property("press_sound"
    ).add_property_group("button")


回复 支持 抱歉

使用道具 举报

发表于 2024-11-2 19:53:35 | 显示全部楼层
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|RenPy中文空间 ( 苏ICP备17067825号|苏公网安备 32092302000068号 )

GMT+8, 2024-11-21 15:08 , Processed in 0.119448 second(s), 24 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表