Программа Pygame сделана так, чтобы она выглядела как NohBoard

Это то, что я сделал как экран ввода, похожий на NohBoard, потому что мне нужно сделать видео для видеоигры, но он работает в Linux, где нет хороших программ отображения ввода. Одна вещь, о которой я беспокоюсь, — это то, как я заставляю мигать колесико прокрутки ровно три кадра при записи 60 FPS. Я проверил, сколько кадров длилось, проверив по записи, и, похоже, он работает нормально, но я все еще не уверен на 100%, лучший ли способ добиться того, чтобы он длился ровно три кадра при записи 60 кадров в секунду.

import pygame
import pynput
import threading

def main():
    
    lock_for_dict = threading.Lock()
    lock_for_scroll = threading.Lock()
    
    pygame.init()
    
    win = pygame.display.set_mode((343, 229))
    pygame.display.set_caption("test")
    font = pygame.font.Font(pygame.font.get_default_font(), 15)
    down_font = pygame.font.Font(pygame.font.get_default_font(), 14)
    
    # {key_or_mouse_button: (rect_location_and_size, (black_key_text, white_key_text), key_text_location)}
    # 0 and 1 are left mouse and right mouse
    shown_keys = {
        0: ((250,94,41,41), (font.render("LM", True, (0,0,0)), font.render("LM", True, (255,255,255))), (260,106)),
        1: ((292,94,41,41), (font.render("RM", True, (0,0,0)), font.render("RM", True, (255,255,255))), (300,106)),
        "a": ((72,94,41,41), (font.render("A", True, (0,0,0)), font.render("A", True, (255,255,255))), (87,106)),
        "w": ((114,52,41,41), (font.render("W", True, (0,0,0)), font.render("W", True, (255,255,255))), (127,64)),
        "s": ((114,94,41,41), (font.render("S", True, (0,0,0)), font.render("S", True, (255,255,255))), (129,106)),
        "d": ((156,94,41,41), (font.render("D", True, (0,0,0)), font.render("D", True, (255,255,255))), (171,106)),
        "q": ((72,52,41,41), (font.render("Q", True, (0,0,0)), font.render("Q", True, (255,255,255))), (87,64)),
        "e": ((156,52,41,41), (font.render("E", True, (0,0,0)), font.render("E", True, (255,255,255))), (171,64)),
        "r": ((198,52,41,41), (font.render("R", True, (0,0,0)), font.render("R", True, (255,255,255))), (213,64)),
        "f4": ((198,10,41,41), (font.render("F4", True, (0,0,0)), font.render("F4", True, (255,255,255))), (210,22)),
        "tab": ((10,52,61,41), (font.render("TAB", True, (0,0,0)), font.render("TAB", True, (255,255,255))), (25,64)),
        "alt": ((72,178,61,41), (font.render("ALT", True, (0,0,0)), font.render("ALT", True, (255,255,255))), (87,190)),
        "shift": ((10,136,81,41), (font.render("SHIFT", True, (0,0,0)), font.render("SHIFT", True, (255,255,255))), (28,148)),
        "ctrl": ((10,178,61,41), (font.render("CTRL", True, (0,0,0)), font.render("CTRL", True, (255,255,255))), (20,190)),
        "enter": ((134,178,81,41), (font.render("ENTER", True, (0,0,0)), font.render("ENTER", True, (255,255,255))), (148,190)),
        "down": ((216,178,41,41), (down_font.render("down", True, (0,0,0)), down_font.render("down", True, (255,255,255))), (217,190))
    }
    held_or_released = dict()
    
    for v in shown_keys.values():
        pygame.draw.rect(win, (0,0,0), v[0])
        win.blit(v[1][1], v[2])
    
    scroll_up_text = (font.render("SU", True, (0,0,0)), font.render("SU", True, (255,255,255)))
    scroll_down_text = (font.render("SD", True, (0,0,0)), font.render("SD", True, (255,255,255)))
    scroll_time_remaining = [None, 0, 0] # [None, up, down]
    
    def on_scroll(
        x,
        y,
        dx,
        dy,
        lock_for_scroll_=lock_for_scroll,
        scroll_time_remaining_=scroll_time_remaining
        ): # dy is 1 when scrolling up and -1 when scrolling down
        with lock_for_scroll_:
            scroll_time_remaining_[dy] = 42 # midpoint between 2 frames and 3 frames at 60 FPS
    
    def on_click(
        x,
        y,
        button,
        pressed,
        lock_for_dict_=lock_for_dict,
        held_or_released_=held_or_released
        ):
        with lock_for_dict_:
            held_or_released_[button.name == "right"] = pressed
    
    def on_press(
        key,
        lock_for_dict_=lock_for_dict,
        held_or_released_=held_or_released,
        shown_keys_=shown_keys,
        hasattr_=hasattr,
        str_lower=str.lower
        ): # this gets called repeatedly if the key is held down
        if hasattr_(key, "char") and (k := key.char) is not None:
            k = str_lower(k)
            if k not in held_or_released_ and k in shown_keys_:
                with lock_for_dict_:
                    held_or_released_[k] = True
        elif hasattr_(key, "name") and (k := key.name) is not None:
            if k not in held_or_released_ and k in shown_keys_:
                with lock_for_dict_:
                    held_or_released_[k] = True
        else:
            with lock_for_dict_:
                held_or_released_[("tab", "alt")[key.vk % 2]] = True
    
    def on_release(
        key,
        lock_for_dict_=lock_for_dict,
        held_or_released_=held_or_released,
        shown_keys_=shown_keys,
        hasattr_=hasattr,
        str_lower=str.lower
        ):
        if hasattr_(key, "char") and (k := key.char) is not None:
            k = str_lower(k)
            if k in shown_keys_:
                with lock_for_dict_:
                    held_or_released_[k] = False
        elif hasattr_(key, "name") and (k := key.name) is not None:
            if k in shown_keys_:
                with lock_for_dict_:
                    held_or_released_[k] = False
        else:
            with lock_for_dict_:
                held_or_released_[("tab", "alt")[key.vk % 2]] = False
    
    key_listener = pynput.keyboard.Listener(on_press=on_press, on_release=on_release)
    mouse_listener = pynput.mouse.Listener(on_scroll=on_scroll, on_click=on_click)
    key_listener.daemon = True
    mouse_listener.daemon = True
    key_listener.start()
    mouse_listener.start()
    
    pygame_time_wait = pygame.time.wait
    pygame_display_update = pygame.display.update
    pygame_draw_rect = pygame.draw.rect
    win_blit = win.blit
    held_or_released_items = held_or_released.items
    held_or_released_clear = held_or_released.clear
    pygame_QUIT = pygame.QUIT
    pygame_event_get = pygame.event.get
    any_ = any
    
    while not any_(event.type == pygame_QUIT for event in pygame_event_get()):
        
        waited_time = pygame_time_wait(1)
        
        pygame_display_update()
        
        with lock_for_scroll:
            if scroll_time_remaining[1] >= 0:
                scroll_time_remaining[1] -= waited_time
                pygame_draw_rect(win, ((0,0,0), (255,255,255))[scroll_time_remaining[1] >= 0], (271,52,41,41))
                win_blit(scroll_up_text[scroll_time_remaining[1] < 0], (281, 64))
            if scroll_time_remaining[2] >= 0:
                scroll_time_remaining[2] -= waited_time
                pygame_draw_rect(win, ((0,0,0), (255,255,255))[scroll_time_remaining[2] >= 0], (271,136,41,41))
                win_blit(scroll_down_text[scroll_time_remaining[2] < 0], (281, 148))
        
        with lock_for_dict:
            for k, v in held_or_released_items():
                args = shown_keys[k]
                pygame_draw_rect(win, ((0,0,0), (255,255,255))[v], args[0])
                win_blit(args[1][not v], args[2])
            held_or_released_clear()
    
    pygame.quit()

main()

1 ответ
1

Linux, в котором нет хороших программ отображения ввода

Я очень сомневаюсь в этом, но двигаюсь дальше:

  • У вас есть программа с одной внешней функцией, которая сильно зависит от замыканий. Это не лучший способ представления состояния, и его невозможно проверить. Есть лучшие способы обойти государство. В моей рекомендации я показываю два в зависимости от контекста: либо привязка к некоторым отдельным функциям с помощью partial, или используйте класс.
  • Добавьте несколько подсказок типа PEP484.
  • Не используйте анонимные кортежи; используйте именованные кортежи, классы данных или обычные классы для отображаемых ключей.
  • Удалите избыточность из shown_keys — вы несколько раз переписываете константы для черного и белого, а также настройку сглаживания и т. д.
  • Возможно, не представляю held_or_released как словарь логических значений, но вместо этого как набор. Членство в наборе указывает на удержание.
  • Централизуйте повторяющийся фрагмент кода, чтобы нарисовать символ.
  • Ваш scroll_time_remaining стратегия индексации совершенно отвратительна. Список из двух элементов с переназначенными индексами или, возможно, словарь, будет менее хитрым. Среди других побочных эффектов вашего текущего дизайнерского решения тот факт, что у вас None в смеси означает, что вам нужно Optional где не следует.
  • Почему вы используете псевдоним кучу встроенных модулей (hasattr, lower, any)? Не делай этого. pygame_time_wait = pygame.time.wait одинаково бесполезен.
  • Не надо held_or_released_clear() вообще. Похоже, это способ обойти тот факт, что вы игнорируете pressed параметр для on_click.
  • held_or_released_[("tab", "alt")[key.vk % 2]] неприятный и, вероятно, несколько хрупких. Если вы знаете виртуальные коды для табуляции и alt, просто используйте коды напрямую. В моем примере кода я пропустил это, и единственная комбинация, которая не работает, — это shift + tab.
  • Вместо hasattr, вы можете проверить фактический тип экземпляра, используя isinstance, и отразить это решение в Union параметр.
  • У вас нет анимации, поэтому вам следует вообще отказаться от идеи FPS и event.get, заменив его на event.wait. Для логики тайм-аута прокрутки вы можете использовать таймер вместо цикла опроса. Я не углублялся в это в моем примере кода.
  • Добавить __main__ сторожить.

Предложенный

from functools import partial
from typing import Optional, List, Union, Tuple, Dict, Set, Callable

import pygame
import pynput
import threading

from pygame.font import Font
from pynput.keyboard import Key, KeyCode
from pynput.mouse import Button

BLACK = 0, 0, 0
WHITE = 255, 255, 255
ANTIALIAS = True


KeyType = Union[str, bool]


class ShownKey:
    def __init__(
        self,
        key: KeyType,
        name: str,
        font: Font,
        rect: Tuple[int, int, int, int],
        text_pos: Tuple[int, int],
    ):
        self.key, self.name, self.rect, self.text_pos = key, name, rect, text_pos
        self.black_text: pygame.Surface = font.render(name, ANTIALIAS, BLACK)
        self.white_text: pygame.Surface = font.render(name, ANTIALIAS, WHITE)

    def draw(self, win: pygame.Surface, held: bool) -> None:
        if held:
            background = WHITE
            text = self.black_text
        else:
            background = BLACK
            text = self.white_text
        pygame.draw.rect(win, background, self.rect)
        win.blit(text, self.text_pos)


ShownKeyDict = Dict[KeyType, ShownKey]
HeldSet = Set[KeyType]


def on_scroll(
    x: int, y: int, dx: int, dy: int,
    lock_for_scroll: threading.Lock,
    scroll_time_remaining: List[Optional[int]],
) -> None:
    # dy is 1 when scrolling up and -1 when scrolling down
    with lock_for_scroll:
        scroll_time_remaining[dy] = 42  # midpoint between 2 frames and 3 frames at 60 FPS


def on_click(
    x: int, y: int, button: Button, pressed: bool,
    lock_for_dict: threading.Lock,
    held_or_released: HeldSet,
) -> None:
    with lock_for_dict:
        val = button.name == "right"
        if pressed:
            held_or_released.add(val)
        else:
            held_or_released.discard(val)


def on_press_or_release(
    key: Union[Key, KeyCode],
    lock_for_dict: threading.Lock,
    is_shown: Callable[[KeyType], bool],
    update_held: Callable[[KeyType], None],
) -> None:
    # this gets called repeatedly if the key is held down
    if isinstance(key, KeyCode):
        if key.char is None:
            # Deal with virtual keys here
            return
        k = key.char.lower()
    elif isinstance(key, Key):
        k = key.name
    else:
        raise NotImplementedError()

    if is_shown(k):
        with lock_for_dict:
            update_held(k)


def main():
    lock_for_dict = threading.Lock()
    lock_for_scroll = threading.Lock()

    pygame.init()

    win = pygame.display.set_mode((343, 229))
    pygame.display.set_caption("test")
    font = pygame.font.Font(pygame.font.get_default_font(), 15)
    down_font = pygame.font.Font(pygame.font.get_default_font(), 14)

    # {key_or_mouse_button: (rect_location_and_size, (black_key_text, white_key_text), key_text_location)}
    # 0 and 1 are left mouse and right mouse
    LEFT_MOUSE = False
    RIGHT_MOUSE = True

    shown_keys: ShownKeyDict = {
        key: ShownKey(key, name, font, rect, text_pos)
        for key, rect, name, text_pos in (
            (LEFT_MOUSE, (250, 94, 41, 41), "LM", (260, 106)),
            (RIGHT_MOUSE, (292, 94, 41, 41), "RM", (300, 106)),
            ("a", (72, 94, 41, 41), "A", (87, 106)),
            ("w", (114, 52, 41, 41), "W", (127, 64)),
            ("s", (114, 94, 41, 41), "S", (129, 106)),
            ("d", (156, 94, 41, 41), "D", (171, 106)),
            ("q", (72, 52, 41, 41), "Q", (87, 64)),
            ("e", (156, 52, 41, 41), "E", (171, 64)),
            ("r", (198, 52, 41, 41), "R", (213, 64)),
            ("f4", (198, 10, 41, 41), "F4", (210, 22)),
            ("tab", (10, 52, 61, 41), "TAB", (25, 64)),
            ("alt", (72, 178, 61, 41), "ALT", (87, 190)),
            ("shift", (10, 136, 81, 41), "SHIFT", (28, 148)),
            ("ctrl", (10, 178, 61, 41), "CTRL", (20, 190)),
            ("enter", (134, 178, 81, 41), "ENTER", (148, 190)),
        )
    }
    shown_keys['down'] = ShownKey('down', 'down', down_font, (216, 178, 41, 41), (217, 190))
    all_keys = set(shown_keys.keys())
    held_or_released: HeldSet = set()

    for key in shown_keys.values():
        key.draw(win, held=False)

    scroll_up_text = (font.render("SU", ANTIALIAS, BLACK), font.render("SU", ANTIALIAS, WHITE))
    scroll_down_text = (font.render("SD", ANTIALIAS, BLACK), font.render("SD", ANTIALIAS, WHITE))
    scroll_time_remaining = [None, 0, 0]  # [None, up, down]

    key_listener = pynput.keyboard.Listener(
        on_press=partial(
            on_press_or_release, lock_for_dict=lock_for_dict,
            is_shown=shown_keys.__contains__, update_held=held_or_released.add,
        ),
        on_release=partial(
            on_press_or_release, lock_for_dict=lock_for_dict,
            is_shown=shown_keys.__contains__, update_held=held_or_released.discard,
        ),
    )
    mouse_listener = pynput.mouse.Listener(
        on_scroll=partial(on_scroll, lock_for_scroll=lock_for_scroll, scroll_time_remaining=scroll_time_remaining),
        on_click=partial(on_click, lock_for_dict=lock_for_dict, held_or_released=held_or_released),
    )
    key_listener.daemon = True
    mouse_listener.daemon = True
    key_listener.start()
    mouse_listener.start()

    while not any(event.type == pygame.QUIT for event in pygame.event.get()):

        waited_time = pygame.time.wait(1)

        pygame.display.update()

        with lock_for_scroll:
            if scroll_time_remaining[1] >= 0:
                scroll_time_remaining[1] -= waited_time
                pygame.draw.rect(win, (BLACK, WHITE)[scroll_time_remaining[1] >= 0], (271, 52, 41, 41))
                win.blit(scroll_up_text[scroll_time_remaining[1] < 0], (281, 64))
            if scroll_time_remaining[2] >= 0:
                scroll_time_remaining[2] -= waited_time
                pygame.draw.rect(win, (BLACK, WHITE)[scroll_time_remaining[2] >= 0], (271, 136, 41, 41))
                win.blit(scroll_down_text[scroll_time_remaining[2] < 0], (281, 148))

        with lock_for_dict:
            for key in held_or_released:
                shown_keys[key].draw(win, held=True)
            for key in all_keys - held_or_released:
                shown_keys[key].draw(win, held=False)
            # held_or_released.clear()

    pygame.quit()


if __name__ == '__main__':
    main()

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *