Ввод с клавиатуры (через pynput) и потоки в python

Я пытаюсь создать своего рода текстовую игру на Python 3. Для игры мне нужно будет слушать ввод с клавиатуры, в частности, измеряя, как долго удерживается клавиша при печати на экране. Я пытаюсь начать с создания рабочего минимального примера.

Во-первых, следующий код с использованием pynput успешно измеряет время, в течение которого пользователь удерживает клавишу:

from pynput import keyboard 
import time

print("Press and hold any key to measure duration of keypress. Esc ends program")

# A dictionary of keys pressed down right now and the time each was pressed down at
keys_currently_pressed = {} 

def on_press(key):
    global keys_currently_pressed 
    # Record the key and the time it was pressed only if we don't already have it
    if key not in keys_currently_pressed:
        keys_currently_pressed[key] = time.time()

def on_release(key):
    global keys_currently_pressed
    if key in keys_currently_pressed:
        animate = False
        duration = time.time() - keys_currently_pressed[key]
        print("The key",key," was pressed for",str(duration)[0:5],"seconds")
        del keys_currently_pressed[key]
    if key == keyboard.Key.esc:
        # Stop the listener
        return False

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener: 
    listener.join()

Теперь то, что я хотел бы сделать, это только тогда, когда пользователь нажимает клавишу, печатать текстовую «анимацию» на экране. В следующем примере моя «анимация» просто печатает "*" каждые полсекунды. До сих пор я пытался обрабатывать «анимацию» вторым потоком, но я новичок, когда дело доходит до многопоточности. Следующий код запустит анимацию в нужное время, но не остановит ее.

from pynput import keyboard 
import sys
import time
import threading

print("Press and hold any key to measure duration of keypress. Esc ends program")

# A dictionary of keys pressed down right now and the time each was pressed down at
keys_currently_pressed = {} 

def my_animation():
    # A simple "animation" that prints a new "*" every half second
    limit = 60 # just in case, don't do more than this many iterations
    j = 0
    while j<limit:
        j += 1
        sys.stdout.write("*")
        time.sleep(0.5)

anim = threading.Thread(target=my_animation)

def on_press(key):
    global keys_currently_pressed 
    # Record the key and the time it was pressed only if we don't already have it
    if key not in keys_currently_pressed:
        keys_currently_pressed[key] = time.time()
        anim.start()

def on_release(key):
    global keys_currently_pressed
    if key in keys_currently_pressed:
        animate = False
        duration = time.time() - keys_currently_pressed[key]
        print("The key",key," was pressed for",str(duration)[0:5],"seconds")
        del keys_currently_pressed[key]
    if key == keyboard.Key.esc:
        # Stop the listener
        return False

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener:         listener.join()

Вот подход (после комментария @furas), в котором анимация кодируется после оператора with, однако я не могу заставить это работать на меня:

from pynput import keyboard 
import time

print("Press and hold any key to measure duration of keypress. Esc ends program")

# A dictionary of keys pressed down right now and the time each was pressed down at
keys_currently_pressed = {} 

# animation flag
anim_allowed = False

def on_press(key):
    global keys_currently_pressed 
    global anim_allowed
    # Record the key and the time it was pressed only if we don't already have it
    if key not in keys_currently_pressed:
        keys_currently_pressed[key] = time.time()
        anim_allowed = True

def on_release(key):
    global keys_currently_pressed
    global anim_allowed
    if key in keys_currently_pressed:
        animate = False
        duration = time.time() - keys_currently_pressed[key]
        print("The key",key," was pressed for",str(duration)[0:5],"seconds")
        del keys_currently_pressed[key]
        anim_allowed = False
    if key == keyboard.Key.esc:
        # Stop the listener
        return False

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener:
    while anim_allowed:
        sys.stdout.write("*")
        time.sleep(0.5)
    listener.join()

В конечном счете, я хочу иметь возможность делать это с более сложной анимацией. Например

def mysquare(delay):
    print("@"*10)
    time.sleep(delay)
    for i in range(8):
        print("@" + " "*8 + "@")
        time.sleep(delay)
    print("@"*10)

Как правильно подойти к этому? Большое спасибо!

listener уже работает в отдельном потоке, поэтому нет необходимости использовать другой поток для анимации. Вы можете запустить свой код в текущем потоке между with ... as listener и listener.join() - и это может быть длительный цикл, который проверяет переменные и обновляет экран. Он будет работать аналогично PyGame, который также запускает циклы, которые все время проверяют, есть ли что перемещать, и перерисовывают элементы.
furas 09.12.2020 21:01

@furas спасибо за совет, я пытался сделать то, что вы предложили, теперь показано в последнем редактировании вопроса, но, похоже, я не могу заставить его работать

Ben S. 09.12.2020 21:16
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
2
1 604
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Listener уже использует thread, поэтому нет необходимости запускать анимацию в отдельном потоке. Вы можете запустить его в текущем протекторе в

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener:         

    #... your code ...

    listener.join()

или без with ... as ...

listener = keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True)
listener.start()

#... your code ...

#listener.wait()
listener.join()

Вы можете запускать там даже длинный исполняемый код - т.е. бесконечный цикл while, который проверяет, является ли переменная animateTrue, и записывает новую *.

Мне пришлось добавить sys.stdout.flush() в Linux, чтобы увидеть * на экране.


Моя версия:

Он запускает анимацию все время, когда вы нажимаете любую кнопку, но также есть код с переменной counter, чтобы ограничить анимацию 6 ходами. Если вы нажмете новую клавишу, когда она запускает анимацию, она сбрасывает этот счетчик, и анимация будет длиннее.

Этот цикл должен выполняться все время, чтобы проверить, есть ли новая анимация — вы не можете закончить этот цикл, когда анимация завершена.

from pynput import keyboard 
import sys
import time

# --- functions ---

def on_press(key):
    global keys_currently_pressed 
    global animate
    #global counter

    # Record the key and the time it was pressed only if we don't already have it
    if key not in keys_currently_pressed and key != keyboard.Key.esc:
        keys_currently_pressed[key] = time.time()
        animate = True
        #counter = 0 # reset counter on new key

def on_release(key):
    global keys_currently_pressed
    global animate

    if key in keys_currently_pressed:
        duration = time.time() - keys_currently_pressed[key]
        print("The key", key, "was pressed for", str(duration)[0:5], "seconds")
        del keys_currently_pressed[key]

        if not keys_currently_pressed: 
            animate = False

    if key == keyboard.Key.esc:
        # Stop the listener
        return False

# --- main ---

print("Press and hold any key to measure duration of keypress. Esc ends program")

# A dictionary of keys pressed down right now and the time each was pressed down at
keys_currently_pressed = {} 

animate = False # default value at start (to use in `while` loop)
#limit = 6 # limit animation to 6 moves
#counter = 0 # count animation moves

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener:         

    while listener.is_alive(): # infinite loop which runs all time

        if animate:
            #sys.stdout.write("\b *")  # animation with removing previous `*` 
            sys.stdout.write("*")  # normal animation
            sys.stdout.flush()  # send buffer on screen
            #counter += 1
            #if counter >= limit:
            #    counter = 0
            #    animate = False
            
        time.sleep(0.5)


    listener.join()

Спасибо, это очень полезно, и это работает для меня. Но предположим, что анимация более сложна, чем просто печать одного и того же символа снова и снова — например, предположим, что она рисует какую-то форму с помощью ascii-арта. Я отредактировал вопрос, чтобы иметь пример чего-то подобного. Можно ли этот подход адаптировать для этого?

Ben S. 09.12.2020 22:19

если вам нужна более сложная анимация, вам понадобится более сложный код внутри while loop. И снова эта анимация должна отрисовывать только один кадр в каждом цикле - не запускайте внутренние циклы.

furas 09.12.2020 22:27

если вы хотите рисовать mysquare со сном, то это неправильная идея - у вас должен быть только один цикл - ` while listener.is_alive()` - и он должен контролировать время и рисовать новые элементы.

furas 09.12.2020 22:34

Хорошо, спасибо за совет. Я думаю, что могу отслеживать состояние анимации с помощью своего рода счетчика, который сбрасывается при запуске новой анимации. Я приму это как ответ.

Ben S. 09.12.2020 23:02

Другие вопросы по теме