Clickable QLabel - python-3.x

I'm fairly new to Python, but I've tried dozens of variations to get this to work, no luck so far.
Background: I'm writing a simple number slide game, as a practice project, using PyQt5. The gui displays 15 number tiles, and I want the player to be able to click a tile and it will move into the blank spot (none of the game logic is here yet, I'm still prototyping the gui). If it matters, I'm coding in PyCharm 2022.1.2 (community).
I'm not getting any errors when I run the code, but when I click a tile it doesn't move and then the game exits with "Process finished with exit code -1073740791" (no errors)
I know clickable QLabels are not really a thing, you have to do an override. But I'm not sure where my code is failing. I suspect it has something to do with the override (ClicableQLabel), or my move_tile function. Or even how I'm doing QGridLayout. I tried all of the answers in this question but none of them worked for me.
class ClickableQLabel(QLabel):
def __init__(self, when_clicked, parent=None):
QLabel.__init__(self, parent)
self._when_clicked = when_clicked
def mousePressEvent(self, ev):
self._when_clicked(ev)
class NewGame(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Number Slide Game")
self.gameTiles = []
for tile_no in range(0, 15):
tile_no_img_num = tile_no + 1
num_img = QPixmap('images\\numTile{0}.png'.format(str(tile_no_img_num)))
num_label = ClickableQLabel(self.move_tile)
num_label.setPixmap(num_img)
self.gameTiles.append(num_label)
random.shuffle(self.gameTiles)
game_layout = QGridLayout()
game_layout.setHorizontalSpacing(0)
game_layout.setVerticalSpacing(0)
tile_num = 0
for rowNum in range(0, 4):
for colNum in range(0, 4):
if tile_num < 15:
game_layout.addWidget(self.gameTiles[tile_num], rowNum, colNum)
tile_num += 1
self.setLayout(game_layout)
def move_tile(self, tile_to_move):
game_tile_to_move = self.gameTiles[tile_to_move]
game_tile_to_move.anim = QPropertyAnimation(game_tile_to_move, b"pos")
game_tile_to_move.anim.setEndValue(QPoint(220, 221))
game_tile_to_move.anim.setDuration(200)
game_tile_to_move.anim.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
gameWindow = NewGame()
gameWindow.show()
sys.exit(app.exec_())

Related

Separating Tkinters GUI and control of the application

1) What is my goal:
I’m creating an application that should read data every 60s from ModBusServer, append those data to Graphs and then when the app is closed save the data to excel file.
Site note:
The process of reading data from ModBusServer and appending them to graphs should start after a start button is pressed.
And end after stop button is pressed OR when ModBusServer sends a request to stop.
2) What I have so far:
I created the GUI without any major problems as a class “GUI_komora”.
Everything there works just fine.
3) What is the problem:
But now I’m lost on how to approach the “read data every 60 seconds”, and overall how to control the application.
I did some research on threading but still I’m confused how to implement this to my application.
I learned how to make functions run simultaneously in this tutorial.
And also how to call a function every few seconds using this question.
But none of them helped me to learn how to control the overall flow of the application.
If you could redirect me somewhere or tell me about a better approach I would be really glad.
Some of my code:
from tkinter import *
from GUI_komora import GUI
root = Tk()
my_gui = GUI(root) #my GUI class instance
#main loop
root.mainloop()
"""
How do I achieve something like this???
whenToEnd = False
while whenToEnd:
if step == "Inicialzation":
#inicializace the app
if step == "ReadData":
#read data every 60 seconds and append them to graphs
if step == "EndApp"
#save data to excel file and exit app
whenToEnd = True
"""
Here is an example of a loop that takes a decision (every 60 sec in your case) and pushes the outcome of the decision to tkinter GUI: https://github.com/shorisrip/PixelescoPy/blob/master/base.py
Parts:
main thread - starts tkinter window
control thread - reads some data and decides what to show in GUI
GUI class - has a method "add_image" which takes input an image and displays on GUI.(add_data_to_graph maybe in your case). This method is called everytime by the control thread.
Snippets:
def worker(guiObj, thread_dict):
# read some data and make decision here
time.sleep(60)
data_to_show = <outcome of the decision>
while some_logic:
pictureObj = Picture(chosen, timer)
pictureObj.set_control_thread_obj(thread_dict["control_thread"])
guiObj.set_picture_obj(pictureObj)
pictureObj.display(guiObj)
# do post display tasks
guiObj.quit_window()
# Keep GUI on main thread and everything else on thread
guiObj = GuiWindow()
thread_list = []
thread_dict = {}
thread_for_image_control = threading.Thread(target=worker, args=(guiObj,
thread_dict))
thread_dict["control_thread"] = thread_for_image_control
thread_list.append(thread_for_image_control)
thread_for_image_control.start()
guiObj.run_window_on_loop()
# thread_for_image_control.join()
Code for Picture class:
class Picture:
def __init__(self, path, timer):
self.path = path
self.timer = timer
self.control_thread_obj = None
def set_control_thread_obj(self, thread_obj):
self.control_thread_obj = thread_obj
def display(self, guiObj):
image_path = self.path
guiObj.add_image(image_path)
time.sleep(self.timer)
Code for GUI class
class GuiWindow():
def __init__(self):
self.picture_obj = None
self.root = Tk()
self.image_label = None
self.image = None
self.folder_path = None
self.timer = None
self.root.protocol("WM_DELETE_WINDOW", self.exit_button)
def add_image(self, image_path):
resized_img = self.resize(image_path)
image_obj = ImageTk.PhotoImage(resized_img)
image_label = Label(self.root, image=image_obj,
height=resized_img.height,
width=resized_img.width)
self.image = image_obj # DO NOT REMOVE - Garbage collector error
if self.image_label is not None:
self.remove_image()
image_label.grid(row=0, column=0, columnspan=3)
self.image_label = image_label
Here based on my control loop thread I am changing image (in your case graph data) of the tkinter GUI.
Does this help?

Trying to build Risk style game in Tkinter

I have been trying to build my skills in Python and am trying to create a risk style game.
I am not far in to it at the moment as I am trying to get to grips with classes and Tkinter.
My first trial is to create a series of buttons to take the place of the different countries. I then want these buttons to update the amount of armies on the country when they are clicked.
So far I have been able to get the map to generate from the class I have created and the buttons are clickable. When a button is clicked it updates the amount of armies but always for the last button.
How do I get it so that the button I click updates and not the last one?
Have I gone about this in entirely the wrong way?
from tkinter import *
import random
class territory:
def __init__ (self, country, player = "1", current_armies = 0, x=0, y=0):
self.country = country
self.current_armies = current_armies
self.player = player
self.y = y
self.x = x
def get_armies(self):
print(self.country + " has " + str( self.current_armies)+ " armies.")
def add_armies (self, armies):
self.current_armies += armies
def roll_dice (self, dice=1):
rolls = []
for i in range(0, dice):
rolls.append(random.randint(1,6))
rolls.sort()
rolls.reverse()
print (self.country + " has rolled " + str(rolls))
return rolls
def owner(self):
print (self.country + " is owned by " + self.player)
def get_country(self):
print(country)
def button (self):
Button(window, text = territories[0].current_armies, width = 10, command = click1(territories, 0)).grid(row=y,column=x)
window = Tk()
def create_territories():
countries = ["UK", "GER", "SPA", "RUS"]
terr_pos = [[1,0],[2,0],[1,5],[4,1]]
sta_arm = [1,1,1,1]
terr = []
player = "1"
for i in range(len(countries)):
terr.append(territory(countries[i],player, sta_arm [i] , terr_pos[i][0],terr_pos[i][1]))
if player == "1":
player = "2"
else:
player = "1"
return terr
def click1(territory, i):
territory[i].current_armies += 1
build_board(territory)
def build_board(territories):
for i in range(0,4):
Button(window, text = territories[i].country+"\n"+str(territories[i].current_armies), width = 10, command = lambda: click1(territories, i)).grid(row=territories[i].y,column=territories[i].x)
territories = create_territories()
window.title ("Domination")
create_territories()
build_board(territories)
window.mainloop()
In your def button(self):... you are always referencing territories[0]:
Button(window, text=territories[0].current_armies,... command=click1(territories, 0)...
As such, you are always using the first territory as your reference, so you ought to initialize each territory with its index in territories[] so you can pass that into your Button constructor.
On your question of "entirely the wrong way," I'd personally send that question over to CodeReview, since that's more of their domain (we fix broken code, they address smelly code), though there is significant overlap. We do prefer one question per question, however, and "is this whole thing wrong?" is a little broad for StackOverflow.

wxPython Panel Text Not Rendering

The problem I'm having is that when I set the static text inside a panel (see #Case 2) it only renders the first letter. The code below is a super stripped down version of my actual code but it produces an identical result:
import wx
import time
class TestInterface(wx.Frame):
testStatusFlag = 0
def __init__(self, *args, **kw):
super(TestInterface, self).__init__(*args, **kw)
self.pnl = wx.Panel(self)
self.SetSize((450, 225))
self.SetTitle('example')
self.Centre()
self.Show(True)
self.indicatorFullTest = None
self.buttonFullTest = None
self.setTestStatus(status=1)
def runTest(self, ev=None):
self.setTestStatus(status=2)
#the test is a bunch of functions that take a bunch of time to run
#they're all located in separate files but all access a single piece of hardware
#so multithreading is effectively impossible (I don't want to spend days re-writing stuff to accomodate it)
time.sleep(10)
self.setTestStatus(status=3)
return 0
def setTestStatus(self, ev=None, status=None):
#Handle the optional status argument
if (status in [1,2,3]):
self.testStatusFlag = status
#Remove any old stuff since if we're calling this function they have to get removed
if (self.indicatorFullTest != None):
self.indicatorFullTest.Hide()
if (self.buttonFullTest != None):
self.buttonFullTest.Hide()
#Case 1
if (self.testStatusFlag == 1):
self.buttonFullTest = wx.Button( self.pnl, label='Run Test', pos=(125, 100), size=(250, 50))
self.buttonFullTest.Bind(wx.EVT_BUTTON, self.runTest)
#Case 2
elif (self.testStatusFlag == 2):
self.indicatorFullTest = wx.Panel( self.pnl, pos=(125, 100), size=(250, 50))
wx.StaticText(self.indicatorFullTest, wx.ID_ANY, "Full-Board Test now in progress\nAllow up to 6 min to finish...",
style=wx.ALIGN_CENTRE_HORIZONTAL, pos=(18,7))
self.indicatorFullTest.SetBackgroundColour( 'Tan' )
self.Update()
#Case 3
elif (self.testStatusFlag == 3):
self.buttonFullTest = wx.Button( self.pnl, label='Test Complete\nPress to reset GUI',
pos=(125, 100), size=(250, 50) )
self.buttonFullTest.SetBackgroundColour( (130,255,130) )
self.buttonFullTest.Bind(wx.EVT_BUTTON, self.resetGUI)
#Resets the GUI after a test is complete
def resetGUI(self, ev=None):
self.setTestStatus(status=1) #Reset the fullTest button/indicator thing
if __name__ == '__main__':
ex = wx.App()
gui = TestInterface(None)
ex.MainLoop()
Basically, how do I make the UI fully render the text? I imagine it has something to do with not going back to wx's main loop after changing that indicator, but I feel like calling self.Update() should make that unnecessary. It may also have something to do with how I'm switching between using a button and using a panel (which is probably bad but I'm not sure how else to do it). I know that I could solve this by making my test function run in a separate thread but the problem is that the test function calls separate libraries which I literally do not have the time to re-write.
Thanks
This is a bit easier when you use wxPython's sizers because then you can just Show and Hide widgets. You can see a good example in this tutorial. I would recommend learning sizers just to make your UI more dynamic when you resize the frame.
But regardless, the answer to this conundrum is pretty simple. Instead of calling self.Update() in Case #2, you need to call wx.Yield(). I swapped that one line change in and it worked for me on Linux.

Multithreading in Tkinter

I am new to Python and Tkinter but I was wondering if I could do Multithreading in Tkinter.
I have a Restaurant Simulation program and whenever it accepts an order, this is what should happen:
1) Timer that counts down and shows how many seconds are left before the order is done
2) While the Timer is counting, I want to create another instance of the Restaurant Simulation so that it could accept another order.
This is what I have tried:
from Tkinter import *
class food():
def __init__(self):
self.inventory = []
def cookOrder(self,type):
if type is 'AA': #Cook Barf
self.inventory[0]+=1
class Restaurant():
def Callback(self,root):
if tkMessageBox.askokcancel("Quit", "Do you really wish to quit?"):
root.destroy()
def MainMenu(self):
self.Main = Tk()
self.Main.grid()
Title = Label(self.Main,text = "Welcome to Resto!",font=("Bauhaus 93",20))
Title.grid(columnspan=6)
RestoMenu = Button(self.Main,text = "Order Food",command = lambda:self.Restaurant_Menu('AA'),font=("High Tower Text",12))
RestoMenu.grid(row=2,column=0)
self.Main.mainloop()
def Restaurant_Menu(self,type):
self.Main.destroy()
self.setCookTime(type)
def setCookTime(self,type):
self.MainMenu() #This is not working as I planned it to be
self.cookTimer = Tk()
self.timeLabel = Label(text="")
self.timeLabel.pack()
if type is "AA":
self.Tick(10,type)
self.cookTimer.mainloop()
self.cookTimer.wm_protocol ("WM_DELETE_WINDOW", self.Callback(self.cookTimer)) #Getting TCL Error
def Tick(self,time,type):
time -=1
if time ==-1:
food.cookOrder(type)
self.cook.destroy()
else:
self.timeLabel.configure(text="%d" %time)
self.cookTimer.after(1000, lambda: self.Tick(time,type))
Food = food()
Resto = Restaurant()
Resto.MainMenu()
Now, the problem with the code is, it is not creating another instance of the Restaurant while the countdown is going on. Any help?
Plus, how do you ask the user for the confirmation of exiting program when he clicks the exit button while the timer is ticking, using WM_DELETE_WINDOW?

PyQt: How do I handle QPixmaps from a QThread?

This has to be the biggest nuisance I've encountered with PyQT: I've hacked together a thumbnailing thread for my application (I have to thumbnail tons of big images), and it looks like it would work (and it almost does). My main problem is this error message whenever I send a SIGNAL from my thread:
QPixmap: It is not safe to use pixmaps outside the GUI thread
I can't figure out how to get around this. I've tried passing a QIcon through my SIGNAL, but that still generates the same error. If it helps, here's the code blocks which deal with this stuff:
The Thumbnailer class:
class Thumbnailer(QtCore.QThread):
def __init__(self, ListWidget, parent = None):
super(Thumbnailer, self).__init__(parent)
self.stopped = False
self.completed = False
self.widget = ListWidget
def initialize(self, queue):
self.stopped = False
self.completed = False
self.queue = queue
def stop(self):
self.stopped = True
def run(self):
self.process()
self.stop()
def process(self):
for i in range(self.widget.count()):
item = self.widget.item(i)
icon = QtGui.QIcon(str(item.text()))
pixmap = icon.pixmap(72, 72)
icon = QtGui.QIcon(pixmap)
item.setIcon(icon)
The part which calls the thread (it occurs when a set of images is dropped onto the list box):
self.thread.images.append(f)
item = QtGui.QListWidgetItem(f, self.ui.pageList)
item.setStatusTip(f)
self.thread.start()
I'm not sure how to handle this kind of stuff, as I'm just a GUI newbie ;)
Thanks to all.
After many attempts, I finally got it. I can't use a QIcon or QPixmap from within a non-GUI thread, so I had to use a QImage instead, as that transmits fine.
Here's the magic code:
Excerpt from the thumbnailer.py QThread class:
icon = QtGui.QImage(image_file)
self.emit(QtCore.SIGNAL('makeIcon(int, QImage)'), i, icon)
makeIcon() function:
def makeIcon(self, index, image):
item = self.ui.pageList.item(index)
pixmap = QtGui.QPixmap(72, 72)
pixmap.convertFromImage(image) # <-- This is the magic function!
icon = QtGui.QIcon(pixmap)
item.setIcon(icon)
Hope this helps anyone else trying to make an image thumbnailing thread ;)

Resources