Это то, что я сделал как экран ввода, похожий на 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 ответ
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()