Tkinter: can I bind with arguments? - events

I'm trying to make a map program with tkinter but I'm having trouble creating icons which display information when clicked, or when the mouse passes over them.
root = Tk()
root.title('test')
root.geometry("500x500")
def text_map_icon(canvas, origin_x, origin_y, text):
canvas.create_text(origin_x, origin_y - 60, text=text)
def map_icon(canvas, origin_x, origin_y, text):
icon = canvas.create_arc((origin_x - 50), (origin_x - 50), (origin_y + 50), (origin_y + 50), start=70, extent=40,
fill='green', activefill='red', activewidth=2.0)
canvas.tag_bind(icon, '<Enter>', text_map_icon(c, origin_x, origin_y, text))
map_icon(c, 250, 250, 'map icon')
c.pack()
root.mainloop()
To do this I have created a map icon with the create_arc canvas method. In order to display information upon clicking the icon, I have tried to bind it to the text_map_icon function. In order for that text to display the relevant information on the map (which can change depending on the data fed to it), I need that function to have arguments for the text and coordinates.
I understand that normally when binding functions the argument of the function is 'event', and when called no arguments are given. Is there a way to bind a function with arguments?

You can use lambda:
from tkinter import *
root = Tk()
root.title('test')
root.geometry("500x500")
c = Canvas(root)
def text_map_icon(event, canvas, origin_x, origin_y, text):
canvas.create_text(origin_x, origin_y - 60, text=text)
def map_icon(canvas, origin_x, origin_y, text):
icon = canvas.create_arc((origin_x - 50), (origin_x - 50), (origin_y + 50), (origin_y + 50), start=70, extent=40,
fill='green', activefill='red', activewidth=2.0)
canvas.tag_bind(icon, '<Enter>', lambda e: text_map_icon(e, c, origin_x, origin_y, text))
map_icon(c, 250, 250, 'map icon')
c.pack()
root.mainloop()

Related

How to get the size of the wxpython panel

I'm developing a calendar application
The top level window is a frame containing a panel that displays the calendar grid and a panel that contains a "Close" button.
I'm unable to obtain the size of the calendar grid panel.
When I add code to get the panel size, the result is (20,20), which cannot be correct
The screen size is (1920,1080) so I'm expecting something like (1920, 1000)
When I add the wx.lib.inspection module, I see the correct size being displayed. It is (1920, 968)
Can anyone shed some light how to get the correct size of the panel?
This is the code I have so far
import wx
class DrawFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, title='Agenda', style= wx.CAPTION | wx.CLOSE_BOX)
self.drawpanel = DrawPanel(self)
self.buttonpanel = ButtonPanel(self)
self.framesizer = wx.BoxSizer(wx.VERTICAL)
self.framesizer.Add(self.drawpanel,1, flag=wx.EXPAND)
# Add an empty space 10 pixels high above and below the button panel
self.framesizer.Add((0,10),0)
self.framesizer.Add(self.buttonpanel,0, flag=wx.EXPAND)
self.framesizer.Add((0,10),0)
self.SetSizer(self.framesizer)
self.SetInitialSize()
self.Maximize()
self.Show()
def GetPanelSize(self):
return self.drawpanel.GetSize()
def OnClose(self, event):
self.Close()
class DrawPanel(wx.Panel):
# This panel's parent is DrawFrame. DrawFrame is the top level window.
def __init__(self, parent):
wx.Panel.__init__(self, parent=parent)
self.parent = parent
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.x1, self.y1, self.x2, self.y2 = wx.GetClientDisplayRect()
b = self.x1, self.y1, self.x2, self.y2
print b
self.width, self.height = wx.GetDisplaySize()
c = self.width, self.height
print c
def OnPaint(self, event=None):
dc = wx.PaintDC(self)
dc.Clear()
dc.SetPen(wx.Pen(wx.BLACK, 2))
dc.SetBrush(wx.Brush('WHITE'))
"""
DrawRectangle (self, x, y, width, height)
Draw a rectangle with the given corner coordinate and size.
x and y specify the top left corner coordinates and both width and height are positive.
"""
dc.DrawRectangle(self.x1 + 5, self.y1, self.x2 - 10, self.y2 - 60)
dc.DrawLine(40, 100, 600, 100)
class ButtonPanel(wx.Panel):
# This panel's parent is DrawFrame. DrawFrame is the top level window.
def __init__(self, parent):
wx.Panel.__init__(self, parent=parent)
self.parent=parent
self.buttonpanelsizer = wx.BoxSizer(wx.HORIZONTAL)
self.closebutton = wx.Button(self, label = 'Close')
self.Bind(wx.EVT_BUTTON, self.OnClose, self.closebutton)
self.buttonpanelsizer.AddStretchSpacer(prop=1)
self.buttonpanelsizer.Add(self.closebutton, 0, wx.ALIGN_CENTER)
self.SetSizer(self.buttonpanelsizer)
def OnClose(self, event):
self.parent.OnClose(event)
app = wx.App(False)
frame = DrawFrame()
print frame.GetPanelSize()
app.MainLoop()
Much appreciated,
Thanks
You are calling the GetPanelSize too early. Keep in mind that wxPython (and pretty much any GUI framework) is event based. That means that for it to work it must keep processing events, which in case of wxPython means that app.MainLoop() must run. So do not call GetPanelSize before calling app.MainLoop(). Instead, call it when you need it. Do you need it when you paint something? Just use dc.GetSize(). Do you need it elsewhere? Process the wx.EVT_SIZE event and store the current size. Possibly you will have to trigger some action in the EVT_SIZE handler.

Close all windows except for the starting window in tkinter

In this code below, I made a simple route guidance by opening defined windows each time I press a specific button e.g.) Mainmenu -> Classroom Floor -> Classroom No.
I'm trying to make a 'Home' button that will close all open windows except for the first Main Menu window with WELCOME written.
For example, after I press btn7, btn702, then I have 4 windows open. I would like to add a 'Home' button that will close 3 newly open windows and leave the first window alive. How might I make this kind of button?
from os import system
from tkinter import *
from PIL import ImageTk, Image
mainmenu = Tk()
mainmenu.title("CAU 310 GUIDE MAP")
mainmenu.geometry("1280x800+0+0")
canvas = Canvas(mainmenu, width = 1280, height = 800)
canvas.pack(fill='both', expand = True)
canvas.create_text(640, 250, text = 'WELCOME', font=times 45)
btnclassroom = Button(mainmenu, padx=5, pady=5,text="Classroom", font="times 30", command=selectfloor)
btnclassroom.place(x=140, y=570)
def selectfloor():
mainmenu = Tk()
mainmenu.title("DESTINATION")
mainmenu.geometry("1280x800+0+0")
mainmenu.config(bg='white')
canvas = Canvas(mainmenu, width = 1280, height = 800)
canvas.pack(fill='both', expand = True)
canvas.create_text(640, 150, text = 'Select floor of classroom', font='Arial 40')
btn7=Button(mainmenu, padx=4, pady=4, text="7F", font="Arial 42 bold", command=floor7)
btn7.place(x=160-5, y=490)
def floor7():
mainmenu = Tk()
mainmenu.title("FLOOR 7")
mainmenu.geometry("1280x800+0+0")
canvas = Canvas(mainmenu, width = 1280, height = 800)
canvas.pack(fill='both', expand = True)
canvas.create_text(640, 150, text = 'Select classroom No.', font='Arial 40')
btn702=Button(mainmenu, padx=3, pady=3, text="No.702", font="Arial 38 bold", command=room702)
btn702.place(x=100+220*1, y=240)
mainmenu.mainloop()
def room702():
mainmenu = Tk()
mainmenu.title("Elevator 1")
mainmenu.geometry("1280x800+0+0")
lobby = ImageTk.PhotoImage(Image.open("1F_elevator1.jpg"), master=mainmenu)
canvas = Canvas(mainmenu, width = 1280, height = 800)
canvas.pack(fill='both', expand = True)
canvas.create_image(0, 0, image=lobby,anchor = "nw")
mainmenu.mainloop()
mainmenu.mainloop()
**I've been googling around for this kind of matter, and I think I've got a hint of making the 'selectfloor', 'floor7', 'room702' window as a children widget. But I'm still not sure how to make it happen.
You can store the windows in a list, and then iterate over the list to destroy them.
Here's a simplified example. This example uses Toplevel windows rather than instances of Tk. It's not clear why you're using multiple instances of Tk, but in general you should only ever have one. This same technique works with any sort of widgets, however.
import tkinter as tk
windows = []
def delete_all_but_first():
for window in windows[1:]:
window.destroy()
def new_window():
window = tk.Toplevel(root)
windows.append(window)
window.title(f"Window #{len(windows)}")
root = tk.Tk()
windows.append(root)
del_all = tk.Button(root, text="Delete all", command=delete_all_but_first)
new = tk.Button(root, text="New window", command=new_window)
new.pack(side="top", padx=20, pady=20)
del_all.pack(side="top", padx=20, pady=20)
root.mainloop()
There's an optimization you can make if you use Toplevel instead of Tk, and if you always make the windows direct children of the root window. In that case you can just iterate over all children of root, and delete any window that is a top-level.
In this case, you don't need to maintain the windows list.
def delete_toplevels():
for child in root.winfo_children():
if child.winfo_class() == "Toplevel":
child.destroy()
def new_window():
window = tk.Toplevel(root)
window.title(f"Window #{len(windows)}")
...
del_all = tk.Button(root, text="Delete all", command=delete_toplevels)

Pygame: importing fonts causes game to stall [duplicate]

Is there a way I can display text on a pygame window using python?
I need to display a bunch of live information that updates and would rather not make an image for each character I need.
Can I blit text to the screen?
Yes. It is possible to draw text in pygame:
# initialize font; must be called after 'pygame.init()' to avoid 'Font not Initialized' error
myfont = pygame.font.SysFont("monospace", 15)
# render text
label = myfont.render("Some text!", 1, (255,255,0))
screen.blit(label, (100, 100))
You can use your own custom fonts by setting the font path using pygame.font.Font
pygame.font.Font(filename, size): return Font
example:
pygame.font.init()
font_path = "./fonts/newfont.ttf"
font_size = 32
fontObj = pygame.font.Font(font_path, font_size)
Then render the font using fontObj.render and blit to a surface as in veiset's answer above. :)
I have some code in my game that displays live score. It is in a function for quick access.
def texts(score):
font=pygame.font.Font(None,30)
scoretext=font.render("Score:"+str(score), 1,(255,255,255))
screen.blit(scoretext, (500, 457))
and I call it using this in my while loop:
texts(score)
There are 2 possibilities. In either case PyGame has to be initialized by pygame.init.
import pygame
pygame.init()
Use either the pygame.font module and create a pygame.font.SysFont or pygame.font.Font object. render() a pygame.Surface with the text and blit the Surface to the screen:
my_font = pygame.font.SysFont(None, 50)
text_surface = myfont.render("Hello world!", True, (255, 0, 0))
screen.blit(text_surface, (10, 10))
Or use the pygame.freetype module. Create a pygame.freetype.SysFont() or pygame.freetype.Font object. render() a pygame.Surface with the text or directly render_to() the text to the screen:
my_ft_font = pygame.freetype.SysFont('Times New Roman', 50)
my_ft_font.render_to(screen, (10, 10), "Hello world!", (255, 0, 0))
See also Text and font
Minimal pygame.font example: repl.it/#Rabbid76/PyGame-Text
import pygame
pygame.init()
window = pygame.display.set_mode((500, 150))
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 100)
text = font.render('Hello World', True, (255, 0, 0))
background = pygame.Surface(window.get_size())
ts, w, h, c1, c2 = 50, *window.get_size(), (128, 128, 128), (64, 64, 64)
tiles = [((x*ts, y*ts, ts, ts), c1 if (x+y) % 2 == 0 else c2) for x in range((w+ts-1)//ts) for y in range((h+ts-1)//ts)]
for rect, color in tiles:
pygame.draw.rect(background, color, rect)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
window.blit(background, (0, 0))
window.blit(text, text.get_rect(center = window.get_rect().center))
pygame.display.flip()
pygame.quit()
exit()
Minimal pygame.freetype example: repl.it/#Rabbid76/PyGame-FreeTypeText
import pygame
import pygame.freetype
pygame.init()
window = pygame.display.set_mode((500, 150))
clock = pygame.time.Clock()
ft_font = pygame.freetype.SysFont('Times New Roman', 80)
background = pygame.Surface(window.get_size())
ts, w, h, c1, c2 = 50, *window.get_size(), (128, 128, 128), (64, 64, 64)
tiles = [((x*ts, y*ts, ts, ts), c1 if (x+y) % 2 == 0 else c2) for x in range((w+ts-1)//ts) for y in range((h+ts-1)//ts)]
for rect, color in tiles:
pygame.draw.rect(background, color, rect)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
window.blit(background, (0, 0))
text_rect = ft_font.get_rect('Hello World')
text_rect.center = window.get_rect().center
ft_font.render_to(window, text_rect.topleft, 'Hello World', (255, 0, 0))
pygame.display.flip()
pygame.quit()
exit()
I wrote a wrapper, that will cache text surfaces, only re-render when dirty. googlecode/ninmonkey/nin.text/demo/
I wrote a TextBox class. It can use many custom fonts relatively easily and specify colors.
I wanted to have text in several places on the screen, some of which would update such as lives, scores (of all players) high score, time passed and so on.
Firstly, I created a fonts folder in the project and loaded in the fonts I wanted to use. As an example, I had 'arcade.ttf' in my fots folder. When making an instance of the TextBox, I could specify that font using the fontlocation (optional) arg.
e.g.
self.game_over_text = TextBox("GAME OVER", 100, 80, 420, RED, 'fonts/arcade.ttf')
I found making the text and updating it each time "clunky" so my solution was an update_text method.
For example, updating the Player score:
self.score1_text.update_text(f'{self.p1.score}')
It could be refactored to accept a list of str, but it suited my needs for coding a version of "S
# -*- coding: utf-8 -*-
'''
#author: srattigan
#date: 22-Mar-2022
#project: TextBox class example
#description: A generic text box class
to simplify text objects in PyGame
Fonts can be downloaded from
https://www.dafont.com/
and other such sites.
'''
# imports
import pygame
# initialise and globals
WHITE = (255, 255, 255)
pygame.font.init() # you have to call this at the start
class TextBox:
'''
A text box class to simplify creating text in pygame
'''
def __init__(self, text, size, x=50, y=50, color=WHITE, fontlocation=None):
'''
Constuctor
text: str, the text to be displayed
size: int, the font size
x: int, x-position on the screen
y: int, y-position on the screen
color: tuple of int representing color, default is (255,255,255)
fontlocation: str, location of font file. If None, default system font is used.
'''
pygame.font.init()
self.text = text
self.size = size
self.color = color
self.x = x
self.y = y
if fontlocation == None:
self.font = pygame.font.SysFont('Arial', self.size)
else:
self.font = pygame.font.Font(fontlocation, self.size)
def draw(self, screen):
'''
Draws the text box to the screen passed.
screen: a pygame Surface object
'''
text_surface = self.font.render(f'{self.text}', False, self.color)
screen.blit(text_surface, [self.x, self.y])
def update_text(self, new_text):
'''
Modifier- Updates the text variable in the textbox instance
new_text: str, the updated str for the instance.
'''
if not isinstance(new_text, str):
raise TypeError("Invalid type for text object")
self.text = new_text
def set_position(self, x, y):
'''
Modifier- change or set the position of the txt box
x: int, x-position on the screen
y: int, y-position on the screen
'''
self.x = x
self.y = y
def __repr__(self):
rep = f'TextBox instance, \n\ttext: {self.text} \n\tFontFamly:{self.font} \n\tColor: {self.color} \n\tSize: {self.size} \n\tPos: {self.x, self.y}'
return rep
if __name__ == "__main__":
test = TextBox("Hello World", 30, 30, 30)
print(test)
To use this in my Game class
from textbox import TextBox
and in the initialisation part of the game, something like this:
self.time_text = TextBox("Time Left: 100", 20, 20, 40)
self.cred_text = TextBox("created by Sean R.", 15, 600, 870)
self.score1_text = TextBox("0", 100, 40, 650)
self.score2_text = TextBox("0", 100, 660, 650)
self.lives1_text = TextBox("[P1] Lives: 3", 20, 40, 750)
self.lives2_text = TextBox("[P2] Lives: 3", 20, 660, 750)
self.game_over_text = TextBox("GAME OVER", 100, 80, 420, RED)
self.textbox_list = []
self.textbox_list.append(self.time_text)
self.textbox_list.append(self.cred_text)
self.textbox_list.append(self.score1_text)
self.textbox_list.append(self.score2_text)
self.textbox_list.append(self.lives1_text)
self.textbox_list.append(self.lives2_text)
so that when I want to draw all on the screen:
for txt in self.textbox_list:
txt.draw(screen)
In the update section of the game, I only update directly the boxes that have updated text using the update_text method- if there is nothing to be updated, the text stays the same.
I wrote a TextElement class to handle text placement. It's still has room for improvement. One thing to improve is to add fallback fonts using SysFont in case the font asset isn't available.
import os
from typing import Tuple, Union
from pygame.font import Font
from utils.color import Color
class TextElement:
TEXT_SIZE = 50
def __init__(self, surface, size=TEXT_SIZE, color=Color('white'), font_name='Kanit-Medium') -> None:
self.surface = surface
self._font_name = font_name
self._size = size
self.color = color
self.font = self.__initialize_font()
#property
def font_name(self):
return self._font_name
#font_name.setter
def font_name(self, font_name):
self._font_name = font_name
self.font = self.__initialize_font()
#font_name.deleter
def font_name(self):
del self._font_name
#property
def size(self):
return self._size
#size.setter
def size(self, size):
self._size = size
self.font = self.__initialize_font()
#size.deleter
def size(self):
del self._size
def write(self, text: str, coordinates: Union[str, Tuple[int, int]] = 'center'):
rendered_text = self.font.render(text, True, self.color)
if isinstance(coordinates, str):
coordinates = self.__calculate_alignment(rendered_text, coordinates)
self.surface.blit(rendered_text, coordinates)
return self
def __calculate_alignment(self, rendered_text, alignment):
# https://www.pygame.org/docs/ref/surface.html#pygame.Surface.get_rect
# Aligns rendered_text to the surface at the given alignment position
# e.g: rendered_text.get_rect(center=self.surface.get_rect().center)
alignment_coordinates = getattr(self.surface.get_rect(), alignment)
return getattr(rendered_text, 'get_rect')(**{alignment: alignment_coordinates}).topleft
def __initialize_font(self):
return Font(os.path.join(
'assets', 'fonts', f'{self._font_name}.ttf'), self._size)
Here is how you can use it:
TextElement(self.screen, 80).write('Hello World!', 'midtop')
TextElement(self.screen).write('Hello World 2!', (250, 100))
# OR
text = TextElement(self.screen, 80)
text.size = 100
text.write('Bigger text!', (25, 50))
text.write('Bigger text!', 'midbottom')
I hope this can help someone! Cheers!

Python 2.7 Tkinter Change Label text on button event

Im very new to Python(2.7) im learning GUI design(Tkinter) and keep running into different syntax/no call method/global name not defined errors when trying to implement a simple label text change from a Entry object on button click. Can someone show me the correct syntax for the action
from Tkinter import *
class Part3:
def __init__(self, parent):
GUIFrame =Frame(parent,width= 300, height=200)
GUIFrame.pack(expand = False, anchor = CENTER)
entry = Entry(text="enter your choice")
entry.place(x=65, y = 10)
self.test = StringVar()
self.test.set('''Hi, I'm a Label :)''')
self.Label1 = Label(parent, textvariable = self.test)
self.Label1.place(x = 85, y = 100)
self.Button2 = Button(parent, text='edit',command=self.LabelChange)
self.Button2.place(x= 80, y = 60)
self.Button3 = Button(parent, text='exit', command= parent.quit)
self.Button3.place(x= 160, y = 60)
def LabelChange(self):
test = self.entry.get()
self.Label1(test)
root = Tk()
MainFrame =Part3(root)
root.title('Input Test')
root.mainloop()
The Idea being on the 'edit' (button2) click, the text of Label1 is changed to the text of entry.
Try:
self.entry = Entry(text="enter your choice")
...
test = self.entry.get()
self.test.set(test)
I know most tutorials give examples using textvariables, but in most cases you don't need them. You can get and set the values in the widget without using textvariable. Textvariables are mostly useful for doing traces on variables. Variable traces are a somewhat advanced technique that you will rarely need.
from Tkinter import *
class Part3:
def __init__(self, parent):
GUIFrame =Frame(parent,width= 300, height=200)
GUIFrame.pack(expand = False, anchor = CENTER)
self.entry = Entry(text="enter your choice") # this needs to be in self
self.entry.place(x=65, y = 10)
self.test = StringVar()
self.test.set('''Hi, I'm a Label :)''')
self.Label1 = Label(parent, textvariable = self.test)
self.Label1.place(x = 85, y = 100)
self.Button2 = Button(parent, text='edit',command=self.LabelChange)
self.Button2.place(x= 80, y = 60)
self.Button3 = Button(parent, text='exit', command= parent.quit)
self.Button3.place(x= 160, y = 60)
def LabelChange(self):
self.test.set(self.entry.get())
root = Tk()
MainFrame =Part3(root)
root.title('Input Test')
root.mainloop()
root.destroy()
Use can use a .after command. For example:
Lbl = Label(text='Hi')
def change():
Lbl.after(3000, lambda: Lbl.config(text="hola")
# Or you can use the one below to remove delay.
Lbl.config(text='hola')
return change
Btn = Button(command=change ())
Lbl.pack()
Btn.pack()

PyGTK Change Window Dimensions from Two Entry's

It's pretty simple what I'm trying to accomplish here.
Say you put down 320 in the left textbox. That means the width of the window will be 320px. Same applies for height, except for the right textbox.
However when I debug I get this error.
Traceback (most recent call last):
File "./app.py", line 37, in change_size
self.win.set_size_request(width,height)
TypeError: an integer is required
Here's the code.
#!/usr/bin/env python
import gtk
class app:
def __init__(self):
self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.win.set_title("Change Dimensions")
self.win.set_default_size(235, 60)
self.win.connect("destroy", gtk.main_quit)
vbox = gtk.VBox(spacing=4)
hbox = gtk.HBox(spacing=4)
self.entry = gtk.Entry()
self.entry2 = gtk.Entry()
hbox.pack_start(self.entry)
hbox.pack_start(self.entry2)
hbox2 = gtk.HBox(spacing=4)
ok = gtk.Button("OK")
ok.connect("clicked", self.change_size)
hbox2.pack_start(ok)
exit = gtk.Button("Exit")
exit.connect("clicked", gtk.main_quit)
hbox2.pack_start(exit)
vbox.pack_start(hbox)
vbox.pack_start(hbox2)
self.win.add(vbox)
self.win.show_all()
def change_size(self, w):
width = self.entry.get_text()
height = self.entry2.get_text()
self.win.set_size_request(width,height)
app()
gtk.main()
That's because get_text returns a string, but set_size_request requires integers. Simple fix:
width = int(self.entry.get_text())
height = int(self.entry2.get_text())

Resources