Python WX – Threads management

Pour pouvoir s’immiscer dans la boucle principale d’une application WX, c’est à dire exécuter des fonctions sans action de la part de l’utilisateur, le plus simple est d’utiliser un thread.

Mais attention, il est impossible d’exécuter une fonction de votre code WX depuis un programme externe comme un thread.

La manipulation consiste donc à faire en sorte que le thread n’appelle aucune fonction de votre appli, mais lui envoie un événement, de la même manière qu’un bouton.

Ainsi, dans votre appli WX, vous créerez une fonction qui sera associé à cet événement ( bind ).

Voici un exemple avec un petit clavier virtuel, originalement développé pour le raspberry pi .


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#
###################################################################
#
# Copyright (C) DDRDEV 2015
#
# Author Gonzague Defos du Rau
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
####################################################################

import threading, time, os

def getWinId(TITLE):
	cmd = "xwininfo -root -tree | grep "+TITLE+" | awk -F' ' '{print $1}'"
	print cmd
	winId = os.popen(cmd).read().strip() 
	winId =  winId.strip()	
	return winId
	
# Check dependencies : wx and xlib
from sys import exit, argv

IMPORTERRORS = ""
try: import wx
except ImportError: IMPORTERRORS += "\nCan not import wx, you shall install python-wxgtk"

try: from Xlib import X
except ImportError: IMPORTERRORS += "\nCan not import Xlib.display, you shall install python-xlib"

if IMPORTERRORS != "":
	print IMPORTERRORS
	exit()
 
from Xlib.display import Display
from Xlib.protocol import event

# Check if XEVFILE is given, and build keys datas, see BuildXkeys() below 
if len(argv) > 1: XEVFILE = argv[1]
else : XEVFILE = "xevLog"

def BuildXkeys( xevLog ):
	'''
	 First build the X keycodes dictionary using xev :
	 Type this command line "xev | grep -e state -e XLookupString > xevLog", 
	  and than press all keys you need 
	  ( all keys + Shift all keys + AltGr all keys ) .
	 After this you can run this program.
	 If you miss some keys, re-run the command line using the "append" sign
	  so you can keep your xevLog and press only the missing keys :
		xev | grep -e state -e XLookupString >> xevLog
	'''

	h = open( xevLog, "r")
	c = h.read()
	h.close()
	logLines = [ l.strip() for l in c.split("\n") if l.strip() != "" ]
	nLine = 0

	XKEYS = {}
	XKEYSBYCARAC = {}
	SUPAKEYS = []

	for line in logLines:
	
		if line.startswith("state"):
			datas = line.split(" ")
			if len(datas) > 6:
				state = datas[1].replace(",", "")
				keycode = datas[3]
				keysym = datas[5].replace(",", "")
				keyname = datas[6].replace(")", "").replace(",", "")
				tstAscii = logLines[nLine+1].split('"')
				if len(tstAscii) > 1:
					ascii = tstAscii[1]
				else: 
					ascii = keyname
					if not keyname in SUPAKEYS:
						SUPAKEYS.append(keyname)
						print keyname
				
				if ascii in "æâ€êþÿûîœôô~ø¨£äßë‘’ðüïŀö´`≤«»©↓¬¿×÷¡µ§ù":
					ascii = unicode(ascii, 'utf-8')
					
				if not keyname in XKEYS:
					d = {"state":int(state,16), "keycode":int(keycode), "keysym":keysym, "ascii":ascii, "keyname": keyname  }
					XKEYS[ keyname ] = d

					XKEYSBYCARAC[ascii] = d
					#print keyname, ascii, d, type(ascii)

		nLine += 1

	return [ XKEYS, XKEYSBYCARAC, SUPAKEYS ]

# Than build our keys dictionaries
XKEYS, XKEYSBYCARAC, SUPAKEYS = BuildXkeys( XEVFILE )

# The X keyboard
class Keyboard():
	'''
	This Keyboard() class is use to send the KeyPress event to X windows . 
	'''
	def __init__(self, parent):
	
		self.ready = False
		
		self.parent = parent
		self.display = Display()
		self.rootWindow = self.display.get_input_focus()._data["focus"] 
		self.lastWindow = False
		self.currentWindow = False
		
		self.kbThread = KbThread(self)
		
		self.supaKeysOn = {}
		for k in ["Shift_L", "Shift_R"]:
			self.supaKeysOn[k] = False
			
	def ResetSupaKeys(self):
		for k in self.supaKeysOn:
			self.supaKeysOn[k] = False
		
	def KeyPress(self, key):
	
		if not self.lastWindow or not self.parent.wxWindow:
			print "No Window selected"
			return
			
		#if not self.ready : print "Please wait while initialising"

		keycode	= False
		if key in XKEYSBYCARAC :
			keycode = XKEYSBYCARAC[key]["keycode"]
			state = XKEYSBYCARAC[key]["state"]
		elif key in XKEYS :
			keycode = XKEYS[key]["keycode"]
			state = XKEYS[key]["state"]
		
		if not keycode:
			print "No keycode for "+key+", "+str(type(key))+", you shall re-run xev..."
		else:
			keyevt = event.KeyPress( 	detail=keycode,
										time=X.CurrentTime,
										root=self.display.screen().root,
										window=self.lastWindow,
										child=X.NONE,
										root_x=1,
										root_y=1,
										event_x=1,
										event_y=1,
										state=state,
										same_screen=1
									)

			#print "SENDING \""+key+"\" ( "+str(keycode)+", "+str(state)+" )" 
			#, keycode, state, self.lastWindow.id
			self.lastWindow.send_event(keyevt)

			self.display.sync()
		
	def __del__(self):
		self.kbThread.stop()
	
# The X thread	
class KbThread(threading.Thread):
	''' 
	This is the main thread that detect the last selected windows.
	It first check its parent's windows to not select them : 
	 parent.rootWindow ( console ) and parent.parent.wxWindow ( wx gui ), 
	 where parent is the Keyboard() instance and parent.parent 
	 is the KbFrame() instance.
	'''
	def __init__(self, parent):

		self.parent = parent
		
		# Have a "slow" delay so we can Alt+Tab without getting the desktop 
		#  as the lastDetectedWin. Note that it could happen. 
		self.delay = 1
		
		self.tmpCheck = time.time()
		
		self.lastDetectedWin = False

		self._stopevent = threading.Event()
		threading.Thread.__init__(self, target=self.run, name="MainThread", args=() )
		
		self.winEvtType = wx.NewEventType()
		self.winEvt = MyEvent( etype=self.winEvtType, eid=9, value="Event Window Focus" )

		self.start()
 
	def run(self):
		'''
		Detect the last focused window and send it to WX as an event.
		'''
		while not self._stopevent.isSet():
			
			currentWindow = self.parent.display.get_input_focus()._data["focus"] 
			
			# TODO: Find something else, currently we're setting the WX windows as the 1rst win
			#   that is focus on if it's not the rootWindow, so we shall not focus any windows
			#   between the start of the program and the end of wx initialisation, brrr .
			if not self.parent.parent.wxWindow and self.parent.rootWindow and currentWindow.id != self.parent.rootWindow.id :
				#self.parent.parent.wxWindow = currentWindow
				self.parent.parent.wxWindow = self.parent.display.create_resource_object('window', int(getWinId("MyKb"), 16)+1)
				print "R C W", self.parent.rootWindow.id , currentWindow.id , self.parent.parent.wxWindow.id
				self.parent.ready = True
				
			'''if self.parent.parent.mainXID:
				print "mainXID", self.parent.parent.mainXID
				self.parent.parent.wxWindow.id = self.parent.parent.mainXID'''
				
			'''if not self.parent.parent.wxWindow and self.parent.rootWindow and currentWindow.id != self.parent.rootWindow.id :	
				mainXID = getWinId("MyKb")
				self.parent.parent.wxWindow = self.parent.display.create_resource_object('window', int(mainXID, 16)+1)
				print "***", mainXID, self.parent.parent.wxWindow.id'''
		
			# We don't want the last selected windows to be this program 
			if currentWindow.id != self.parent.rootWindow.id and currentWindow.id != self.parent.parent.wxWindow.id :
				self.parent.lastWindow = currentWindow
				
				# And we change lastDetectedWin only if it's a different window
				if not self.lastDetectedWin or self.lastDetectedWin.id != currentWindow.id :
					self.lastDetectedWin = currentWindow
					
					# Get the name of the window
					wName = self.lastDetectedWin.get_wm_class()
					if wName == None:
						w = self.lastDetectedWin.query_tree().parent
						wName = w.get_wm_class()
					
					# Sending the event to wx so GUI knows witch is the lastDetectedWin
					msg = "lastDetectedWin has changed to "+str(wName)+", "+str(self.lastDetectedWin.id)
					self.winEvt.SetValue( msg )
					wx.PostEvent(self.parent.parent, self.winEvt)
			
				
			time.sleep( self.delay )
	
	def stop(self):
		print "\n[INFO] KbThread : stopping thread, please wait ..."
		self._stopevent.set()
		print "[INFO] KbThread : thread is stopped."
				
		
# A WX event	
class MyEvent(wx.PyCommandEvent):
	""" A WX event we can send from anywhere """
	def __init__(self, etype=wx.NewEventType(), eid=wx.ID_ANY, value=None):
		"""Creates the event object"""
		wx.PyCommandEvent.__init__(self, etype, eid)
		self._value = value
		self._eid = eid
		
	def GetValue(self):
		"""
		@return: the value of this event
		"""
		return self._value
		
	def SetValue(self, value):
		"""
		Set the value of this event
		"""
		self._value = value
		return 
		
	def GetId(self):
		"""
		@return: the id of this event
		"""
		return self._eid
		
# The main WX frame	
class KbFrame(wx.Frame):
	'''
	KbFrame() is the main wx 
	'''
	def __init__(self, parent, id, title):
		
		self.wxWindow = False

		self.kb = Keyboard(self)

		self.specialValues = { "CR": "Return", "TAB" : "Tab", "ESC" : "Escape",  
								"\"":"quotedbl", "BS":"BackSpace", 
								}
								
		self.specialFunc = {"SW": self.SwitchPanel, 
							"SH": self.OnShift,
							"AG": self.OnAltGr,
							 }
		
		self.currentPanel = 0
		self.panelsName = ["Basic", "Upper case", "Alt Gr", "Misc"]
		
		self.lastPanel = 0
		self.isShift = False
		self.isAltGr = False
		self.isCtrl = False

		# Initialisation
		displays = (wx.Display(i) for i in range(wx.Display.GetCount()))
		sizes = [display.GetGeometry().GetSize() for display in displays]
		szx, szy = sizes[0]
		
		width, height = 550, 115
		if width > szx:
			width = szx
			
		if height > ( szy / 2):
			height = ( szy / 2)
		
		x = 0 #szx-width
		y = szy-height
		print szx, szy, width, height, x,y
		
		wx.Frame.__init__(self, parent, id, title, (x,y), wx.Size(width, height) )
		
		
		# Bind event from thread to the WindowEvt function
		EVT_TYPE = wx.PyEventBinder( self.kb.kbThread.winEvtType , 1)
		self.Bind(EVT_TYPE, self.WindowEvt)
		
		self.SetBackgroundColour( wx.Colour(55, 55, 66) )

		# Store keys by event id
		self.keysByEvtId = {}
		
		# Build default panel
		self.SwitchPanel(False)
		self.SetPosition((x, y))
		#print "GetId", self.GetId()
		
		#mainXID = getWinId("MyKb")
		#self.wxWindow = self.kb.display.create_resource_object('window', mainXID)
		#print "***", mainXID, self.wxWindow.id
		

	def OnShift(self, event):
		'''
		Switch to panel 1 or 0 
		'''
		if not self.isShift:
			self.isShift = True
			self.currentPanel = 0
			self.SwitchPanel( True ) # To panel 1
		else:
			self.isShift = False
			self.currentPanel = 3 
			self.SwitchPanel( True ) # To panel 0
	
	def OnAltGr(self, event):
		'''
		Switch to panel 2 or 0 
		'''
		if not self.isAltGr:
			self.isAltGr = True
			self.currentPanel = 1
			self.SwitchPanel( True ) # To panel 2

		else:
			self.isAltGr = False
			self.currentPanel = 3 
			self.SwitchPanel( True ) # To panel 0
			
	def SwitchPanel(self, event):
		'''
		Should have been call SwitchToNextPanel
		'''
		if event:
			self.lastPanel = self.currentPanel
			sx,sy = self.GetClientSize()
			px, py = self.GetPosition()
			
			self.currentPanel += 1
			if self.currentPanel > 3:
				self.currentPanel = 0

			for child in self.gs.GetChildren():
				child.GetWindow().Destroy()

		# Build the layout
		self.buttons = self.BuildLayout( self.currentPanel )	
		
		# On laisse WX placer nos bouton par ligne de 14 
		self.sizer = wx.BoxSizer(wx.VERTICAL)
		
		# La grille principale, sur 5 lignes, 14 colonnes 
		self.gs = wx.GridSizer(5, 14, 0, 0)
		
		self.gs.AddMany( self.buttons )

		self.sizer.Add(self.gs, 1, wx.EXPAND)

		self.SetSizer(self.sizer)
		self.Centre()

		if event:
			self.SetPosition((px, py))
			print (px, py)
		
		self.SetTitle('MyKb - '+self.panelsName[self.currentPanel] )
		
		# Refresh the layout
		self.Layout()

	def BuildLayout(self, layout):
		'''
		Ordering and coloring buttons' label, then binding to function
		'''
		if layout == 0:
			# 1rst line
			self.buttonsLabels = ["SW"]
			for i in range(1,10):
				self.buttonsLabels.append( str(i) )
			self.buttonsLabels += ["0", "°", "+", "BS"]
			
			# 2nd line
			self.buttonsLabels += ["TAB"]
			for c in "azertyuiop^$":
				self.buttonsLabels.append(c)
			self.buttonsLabels.append("CR")
			
			# 3rd line
			self.buttonsLabels.append("SH")
			for c in "qsdfghjklm"+u"ù"+"*":
				self.buttonsLabels.append(c)
			self.buttonsLabels.append("Left")
			
			# 4rth line
			self.buttonsLabels.append("AG")
			for c in "WXCVBN?./"+u"§"+"  ":
				self.buttonsLabels.append(c)
				
		elif layout == 2:
			# 1rst line
			self.buttonsLabels = ["SW"]
			self.buttonsLabels += [ "²", "~", "#", "{", "[", "|", "`", "\\", "^", "@", "]", "}" ]
			self.buttonsLabels += ["BS"]
			
			# 2nd line
			self.buttonsLabels += ["TAB"]
			for c in u"æâ€êþÿûîœô~ø":
				self.buttonsLabels.append(c)
			self.buttonsLabels.append("CR")
			
			# 3rd line
			self.buttonsLabels.append("SH")
			for c in u"äßë‘’ðüïŀö´` ": 
				self.buttonsLabels.append(c)
			
			# 4rth line	
			self.buttonsLabels.append("AG")
			for c in u"≤«»© ↓¬¿×÷¡  ": 
				self.buttonsLabels.append(c)
		
		elif layout == 3:

			kDone = ["Left", "Down", "Right", "Up"]
			
			self.buttonsLabels = ["SW"]

			n = 1
			for sk in SUPAKEYS:
				if not sk in kDone :
					kDone.append(sk)
					self.buttonsLabels += [ sk ]
				
				if n == 12: # end 1rst, start 2nd line at TAB
					self.buttonsLabels += ["BS", "TAB"]
					
				if n == 24:	# end 2nd line, start 3rd at SH
					self.buttonsLabels += ["CR", "SH", "", "Up", ""]
					
				if n == 32:	# end 3rd, start 4rth line at AG
					self.buttonsLabels += ["", " ", " ", "AG", "Left", "Down", "Right"]
					
				n += 1

		
		# Une petite touche de couleurs
		defaultColor = wx.Colour(240, 240, 255)
		numColor = 	wx.Colour(200, 222, 200)
		buttonsColor = {}
		
		for i in range(10):
			buttonsColor[str(i)] = numColor
			
		buttonsColor["SW"] = wx.Colour(253, 202, 200)

		# Identifiant WX	
		buttonId = 10
		
		# Liste des boutons
		buttons = []
		
		# Loop into labels
		for buttonLabel in self.buttonsLabels :

			# If there's a label
			if buttonLabel not in [ "" ]:
				
				#print "Building "+buttonLabel

				# Création du bouton
				button = wx.Button(self, buttonId, " "+buttonLabel+" ", style=wx.BU_EXACTFIT )
				
				# Assignation à la fonction
				if buttonLabel in self.specialFunc :
					self.Bind(wx.EVT_BUTTON, self.specialFunc[ buttonLabel ], id=buttonId)
				else:
					self.Bind(wx.EVT_BUTTON, self.OnPress, id=buttonId)
				
				self.keysByEvtId[buttonId] = buttonLabel
			
			else:
				button = wx.StaticText(self, -1, buttonLabel)
				
			# Assignation des couleurs
			if buttonLabel in buttonsColor : 
				button.SetBackgroundColour( buttonsColor[ buttonLabel ] )
			else:
				button.SetBackgroundColour( defaultColor )
				
			# Ajout du bouton dans la liste
			buttons.append( button )
			
			# Incrémenter l'id du boutton
			buttonId += 1
		
		return buttons

	def OnPress(self, event):
		'''
		Call the Keyboard's KeyPress method
		'''
		key = self.keysByEvtId[ event.GetId() ]
		
		self.SetTitle('MyKb - '+self.panelsName[self.currentPanel]+' - '+key)
		
		#print "\nPress "+key+"  "

		if key in self.specialValues :
			key = self.specialValues[ key ]

		self.kb.KeyPress(key)
		
		if self.isShift:
			self.OnShift(False)

		if self.isAltGr:
			self.OnAltGr(False)

	def WindowEvt(self, event):
		'''
		Receive event from the thread
		'''
		print "\nEVT from thread :", event.GetId(), event.GetValue(), "\n"
		self.SetTitle('MyKb - '+self.panelsName[self.currentPanel]+' - '+str(event.GetValue()))
		

	def __del__(self):
		self.kb.__del__()

# The WX App
class KbGui(wx.App):
	''' The wx class to create the main frame '''
	def OnInit(self):
		frame = KbFrame(None, -1, 'MyKb')
		frame.Show(True)
		self.SetTopWindow(frame)
		return True

	
zkbd = KbGui(0) 
zkbd.MainLoop()