LINUX.ORG.RU

Tkinter + потоки

 , , ,


0

1

Известно, что Tkinter криво работает с потоками, поэтому интересует архитектура программы, в которой есть и то, и другое.

Конкретно проблема в следующем. Есть программа, в которой GUI сделан в Tkinter. Захотел, чтобы программа глобально отлавливала сочетание клавиш «Conrol-c-c», написал следующий модуль:

#!/usr/bin/python3

import sys
import signal
from Xlib.display import Display
from Xlib import X
from Xlib.ext import record
from Xlib.protocol import rq
import threading

globs = {'HotkeyCaught':False}

# todo: взять функции, вычисляющие keysym, из pyxhook
keysym_map = {32: "SPACE", 39: "'", 44: ",", 45: "-", 46: ".", 47: "/", 48: "0", 49: "1", 50: "2", 51: "3", 52: "4", 53: "5", 54: "6", 55: "7", 56: "8", 57: "9", 59: ";", 61: "=", 91: "[", 92: "\\", 93: "]", 96: "`", 97 :"a", 98 :"b", 99 :"c", 100: "d", 101: "e", 102: "f", 103: "g", 104: "h", 105: "i", 106: "j", 107: "k", 108: "l", 109: "m", 110: "n", 111: "o", 112: "p", 113: "q", 114: "r", 115: "s", 116: "t", 117: "u", 118: "v", 119: "w", 120: "x", 121: "y", 122: "z", 65293: "ENTER", 65307: "ESC", 65360: "HOME", 65361: "ARROW_LEFT", 65362: "ARROW_UP", 65363: "ARROW_RIGHT", 65505: "L_SHIFT", 65506: "R_SHIFT", 65507: "L_CTRL", 65508: "R_CTRL", 65513: "L_ALT", 65514: "R_ALT", 65515: "SUPER_KEY", 65288: "BACKSPACE", 65364: "ARROW_DOWN", 65365: "PG_UP", 65366: "PG_DOWN", 65367: "END", 65377: "PRTSCRN", 65535: "DELETE", 65383: "PRINT?", 65509: "CAPS_LOCK", 65289: "TAB", 65470: "F1", 65471: "F2", 65472: "F3", 65473: "F4", 65474: "F5", 65475: "F6", 65476: "F7", 65477: "F8", 65478: "F9", 65479: "F10", 65480: "F11", 65481: "F12"}

def catch_control_c(*args):
	pass
	
signal.signal(signal.SIGINT,catch_control_c) # do not quit when Control-c is pressed

def toggle_hotkey(SetBool=True):
	globs['HotkeyCaught'] = SetBool

# Определить нажатие горячих клавиш глобально в системе
class KeyListener(threading.Thread):
	''' Использование: 
		keylistener = KeyListener()
		keylistener.addKeyListener("L_CTRL+L_SHIFT+y", callable)
		Обратить внимание, что необходимо присвоить все возможные комбинации, поскольку порядок нажатия может быть иной, например, "L_CTRL+y+L_SHIFT"
	'''
	def __init__(self):
		threading.Thread.__init__(self)
		self.finished = threading.Event()
		self.contextEventMask = [X.KeyPress,X.MotionNotify]
		# Give these some initial values
		# Hook to our display.
		self.local_dpy = Display()
		self.record_dpy = Display()
		self.pressed = []
		self.listeners = {}
	#--------------------------------------------------------------------------
	def processevents(self, reply):
		if reply.category != record.FromServer:
			return
		if reply.client_swapped:
			print("* received swapped protocol data, cowardly ignored")
			return
		# Добавил str, иначе получаем ошибку
		if not len(str(reply.data)) or ord(str(reply.data[0])) < 2:
			# not an event
			return
		data = reply.data
		while len(data):
			event, data = rq.EventField(None).parse_binary_value(data, self.record_dpy.display, None, None)
			keycode = event.detail
			keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
			if keysym in keysym_map:
				character = keysym_to_character(keysym)
				if event.type == X.KeyPress:
					keylistener.press(character)
				elif event.type == X.KeyRelease:
					keylistener.release(character)
				#self.KeyUp(hookevent)
		#print "processing events...", event.type
	#--------------------------------------------------------------------------
	def run(self):
		# Check if the extension is present
		if not self.record_dpy.has_extension("RECORD"):
			print("RECORD extension not found")
			sys.exit(1)
		r = self.record_dpy.record_get_version(0, 0)
		print("RECORD extension version %d.%d" % (r.major_version, r.minor_version))
		# Create a recording context; we only want key events
		self.ctx = self.record_dpy.record_create_context(
				0,
				[record.AllClients],
				[{
						'core_requests': (0, 0),
						'core_replies': (0, 0),
						'ext_requests': (0, 0, 0, 0),
						'ext_replies': (0, 0, 0, 0),
						'delivered_events': (0, 0),
						'device_events': tuple(self.contextEventMask), #(X.KeyPress, X.ButtonPress),
						'errors': (0, 0),
						'client_started': False,
						'client_died': False,
				}])

		# Enable the context; this only returns after a call to record_disable_context,
		# while calling the callback function in the meantime
		self.record_dpy.record_enable_context(self.ctx, self.processevents)
		# Finally free the context
		self.record_dpy.record_free_context(self.ctx)
		#print('Starting hotkey watch (thread).')
	#--------------------------------------------------------------------------
	def cancel(self):
		self.finished.set()
		self.local_dpy.record_disable_context(self.ctx)
		self.local_dpy.flush()
		#print('Ending hotkey watch (thread).')
	#--------------------------------------------------------------------------
	def press(self, character):
		if len(self.pressed) == 3:
			self.pressed = []
		if character == 'L_CTRL' or character == 'R_CTRL':
			if len(self.pressed) > 0:
				self.pressed = []
			self.pressed.append(character)
		elif character == 'c':
			if len(self.pressed) > 0:
				if self.pressed[0] == 'L_CTRL' or self.pressed[0] == 'R_CTRL':
					self.pressed.append(character)
		action = self.listeners.get(tuple(self.pressed), False)
		#print('Current action:', str(tuple(self.pressed)))
		if action:
			action()
	#--------------------------------------------------------------------------
	def release(self, character):
		"""must be called whenever a key release event has occurred."""
		# Не засчитывает отпущенный Control
		# Кириллическую 'с' распознает как латинскую
		if character != 'c':
			self.pressed = []
	#--------------------------------------------------------------------------
	def addKeyListener(self, hotkeys, callable):
		keys = tuple(hotkeys.split("+"))
		print("Added new keylistener for :",str(keys))
		self.listeners[keys] = callable
	#--------------------------------------------------------------------------
	def result(self):
		if globs['HotkeyCaught']:
			print('Control-c-c detected!')
			globs['HotkeyCaught'] = False
			return True
		else:
			return False
		
keylistener = KeyListener()
keylistener.addKeyListener("L_CTRL+c+c",toggle_hotkey)
		
# Определить название клавиши по ее коду
# Переменная keysym_map должна быть глобальной
def keysym_to_character(sym):
	if sym in keysym_map:
		return keysym_map[sym]
	else:
		return sym
		
def wait_example():
	from time import sleep
	keylistener.start()
	while not keylistener.result():
		sleep(.5)
	keylistener.cancel()

if __name__ == '__main__':
	wait_example()

Модуль рабочий, осталось его только привязать к главной программе. Сейчас это сделано примерно так:

# Инициализируется tkinter, назначаются переменные и пр.
root = tk.Tk()
# Загружается модуль с кейлоггером, приведенный выше
kl_mod.keylistener.start()
# ... Здесь скачиваются и подготавливаются данные
# ... Здесь инициализируется класс, отображающий GUI на основе подготовленных данных
root.mainloop()
У класса есть спец. режим, в котором программа ждет, пока не будет нажато Control-c-c:
def wait_hotkey():
	while True:
		sleep(0.5)
		if kl_mod.keylistener.result():
			print('YES, we got it!')
			break
		else:
			print('No, still nothing...')
Пользователь нажимает кнопку в главной программе, программа входит в режиме ожидания. При нажатии Control-c-c происходит выход из цикла, на основе содержимого буфера обмена подготавливаются новые данные и происходит их отображение.

Итак, проблема в следующем. Как грамотно построить программу? Можно ли избавиться от необходимости нажимать кнопку, чтобы задействовать спец. режим ожидания, в процессе которого основная программа блокируется? Я хочу, чтобы Control-c-c сразу подхватывалось и основная программа продолжала работу без блокировки.

★★★★★

Ответ на: комментарий от pawnhearts

Спасибо, но тут основная проблема не в кейлоггере (который уже полностью рабочий), а в том, как его подключить. Не уверен, что мне подходит gobject, потому что это для gtk приложений (но я попробую, когда будет свободное время).

Можете немного прокомментировать код? Я так понимаю, потоки в нем не используются, а зацикливает обнаружение хоткея gobject. После того, как хоткей обнаружен, происходит выход. Так? Но как тогда повторно его обнаружить? Или же класс будет все время висеть в памяти и не мешать выполнению основной программы?

P.S. В tkinter есть еще такая штука, как root.after, позволяющая выполнять процедуру в цикле. Но у меня она пока не заработала.

denton ★★★★★ ()
Последнее исправление: denton (всего исправлений: 1)
Ответ на: комментарий от denton

Я уже смутно помню, но.

gobject.timeout_add вызывает next_event каждые 100мс(поскольку next_event возвращает True событие повторяется через 100мс).

В next_event оно проверяет если есть события, и если есть вызывает обработчик action - это указатель на функцию с аргументами.

Потоки не нужны, да и выполнению основной программы он не мешает.

Но это, я так понимаю, всё должно работать внутри gobject/gtk main loop.

Пример использования есть в https://pypi.python.org/pypi/qcon

Можно вместо gobject.timeout_add использовать аналог в Tkiner -http://stackoverflow.com/questions/2400262/how-to-create-a-timer-using-tkinter

pawnhearts ★★★★ ()
Последнее исправление: pawnhearts (всего исправлений: 2)

Извини, я питон вообще не знаю. Но вроде tkinter имеет отношение к tk. Тогда не поможет ли тебе это:

http://wiki.tcl.tk/1886

Сам не пробовал. Может, мой ответ и полностью не в тему.

den73 ★★★★★ ()

Если далеко не ушел, переключись на wx. Там с потоками все нормально.

Shadow ★★★★★ ()
Ответ на: комментарий от den73

вроде tkinter имеет отношение к tk

Я их путаю, разницу не знаю :)

Тогда не поможет ли тебе это

В коде, который я привел выше, Control-c(-c) уже нормально ловится и обрабатывается. Проблема в том, что не совсем понятно, как в однопоточном tkinter постоянно проверять определенное условие и менять в зависимости от этого интерфейс. Сейчас смотрю в сторону .after().

denton ★★★★★ ()
Ответ на: комментарий от Shadow

Думаю, такие кардинальные перемены не требуются. В принципе, ничего не мешает использовать tkinter совместно с потоками, только надо уметь их грамотно изолировать друг от друга. При желании можно и tkinter в отдельный поток запихнуть. Проблема тут только в том, чтобы научиться грамотно передавать данные из одного потока в другой.

denton ★★★★★ ()
Ответ на: комментарий от denton

Ну там это базовый функционал - форма в отдельном потоке, через подписку обмен.

Shadow ★★★★★ ()
Ответ на: комментарий от denton

Условия обычно постоянно не проверяются.

В норме твоя ловушка для Control-C должна бросать событие, которое обрабатывается.

При этом кооперативная многозадачность получается за счёт очередей сообщений.

Пишешь функцию, которая делает кусочек дела, в конце она постит сообщение (event generate, включая after) и передаёт ему информацию, достаточную для продолжения. На сообщение навешен следующий обработчик и т.п. Можно делать замыкания, примерно как в JavaScript, поэтому это может быть довольно удобным.

Небольшая сложность состоит в том, что есть две очереди сообщений.

Уже точно не помню, где это написано. Может, где-нибудь в этом районе:

http://wiki.tcl.tk/1526

http://wiki.tcl.tk/808

Была хорошая статья именно про взаимодействие двух очередей сообщений, но так слёту не могу найти. Если тебе нужно именно периодически что-то проверять, то тут должен after помочь.

Проверяешь и на after (таймаут) навешиваешь повтор.

den73 ★★★★★ ()
Ответ на: комментарий от den73

Спасибо за такой подробный ответ. В event loops я пока что не въехал, но то, что я хотел, получилось реализовать вот так:

# Перехватить нажатие Control-c-c
def timed_update():
	if kl_mod.keylistener.result():
		my_action()
	root.after(1000,timed_update)

Только timed_update() надо прописать до любых прорисовок виджетов.

denton ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.