In my game after pressing the start button I wanted to hide the button to stop the user from pressing the start button again and making more bubbles appear. I tried to make a variable equal to 0. Then every time the start button was clicked the value would go up by 1. If the value was 2 then the start button would disappear or become inactive. By trying to add this 'hiding button code' its only showing one bubble on my screen when the start button is clicked. Oh and the started button doesn't hide... ._.
from tkinter import *
import random
import tkinter as tk
# Message box for score to tell user if they have won or lost
from tkinter.messagebox import showinfo
class BubbleFrame:
def __init__(self, root, name):
self.name = name
root.title("Math Bubbles")
self.bubbles = {} # this will hold bubbles ids, positions and velocities
self.score = 0
self.buttonclick = 0 # Newly added in attempt to hide button
Button(root, text="Start", width=8, bg="Pink", command=self.make_bubbles).pack() # This button starts the game, making the bubbles move across the screen
Button(root, text="Quit", width=8, bg="Yellow",command=quit).pack()
self.canvas = Canvas(root, width=800, height=650, bg='#afeeee')
self.canvas.create_text(400, 30, fill="darkblue", font="Times 20 italic bold", text="Click the bubbles that are answers in the two times tables.")
#Shows score at beinging of the game
self.current_score = self.canvas.create_text(200, 60, fill="darkblue", font="Times 15 italic bold", text="Your score is: 0")
self.canvas.pack()
def make_bubbles(self):
for each_no in range(1, 21):
self.buttonclick += 1 #Newly added in attempt to hide button
xval = random.randint(5, 765)
yval = random.randint(5, 615)
COLOURS = ('#00ff7f', '#ffff00', '#ee82ee', '#ff69b4', '#fff0f5') # CAPS represents a constant variable
colour = random.choice(COLOURS) # This picks a colour randomly
oval_id = self.canvas.create_oval(xval, yval, xval + 60, yval + 60,fill=colour, outline="#000000", width=5, tags="bubble")
text_id = self.canvas.create_text(xval + 30, yval + 30, text=each_no, tags="bubble")
self.canvas.tag_bind("bubble", "<Button-1>", lambda x: self.click(x))
self.bubbles[oval_id] = (xval, yval, 0, 0, each_no, text_id) # add bubbles to dictionary
if buttonclick == 2: #Newly added in attempt to hide button
showinfo("Oh No", "Sorry %s, but the game is already started" % self.name)
def click(self, event):
if self.canvas.find_withtag(CURRENT):
item_uid = event.widget.find_closest(event.x, event.y)[0]
is_even = False
try: # clicked oval
self.bubbles[item_uid]
except KeyError: # clicked oval
for key, value in self.bubbles.iteritems():
if item_uid == value[5]: # comparing to text_id
if value[4] % 2 == 0:
is_even = True
self.canvas.delete(key) # deleting oval
self.canvas.delete(item_uid) # deleting text
else:
if self.bubbles[item_uid][4] % 2 == 0:
is_even = True
self.canvas.delete(item_uid) # deleting oval
self.canvas.delete(self.bubbles[item_uid][5]) # deleting text
if is_even:
self.score += 1
else:
self.score -= 1
showinfo("Oh no!", "%s! You clicked the wrong bubble, please start again." % self.name)
if self.score == 10:
#Tells user You won! if score is 10
showinfo("Winner", "You won %s!" % self.name)
self.canvas.delete(self.current_score)
#Shows updated score on canvas
self.current_score = self.canvas.create_text(200, 60, fill="darkblue", font="Times 15 italic bold", text="Your score is: %s"%self.score)
def bubble_move(self, root):
for oval_id, (x, y, dx, dy, each_no, text_id) in self.bubbles.items():
# update velocities and positions
dx += random.randint(-1, 1)
dy += random.randint(-1, 1)
# dx and dy should not be too large
dx, dy = max(-5, min(dx, 5)), max(-5, min(dy, 5))
# bounce off walls
if not 0 < x < 770:
dx = -dx
if not 0 < y < 620:
dy = -dy
# apply new velocities
self.canvas.move(oval_id, dx, dy)
self.canvas.move(text_id, dx, dy)
self.bubbles[oval_id] = (x + dx, y + dy, dx, dy, each_no, text_id)
# have mainloop repeat this after 100 ms
root.after(100, self.bubble_move, root)
if __name__ == "__main__":
root = Tk()
root.title("Welcome")
Label(root, text="Welcome to Math bubbles, what is your name?").pack()
name = Entry(root)
name.pack()
def submit(name, root):
root.destroy()
root = Tk()
Label(root, text="Hello %s, press the Start button to begin.\n" % name).pack()
BubbleFrame(root, name).bubble_move(root)
Button(root, text="Ok", command=lambda: submit(name.get(), root)).pack()
root.mainloop()
I faced IndentationError in line 103 and NameError in 39 (it is self.buttonclick not buttonclick). Next time check your code before posting.
Well, you invoke 20 buttonclicks each time you click on Start button and the value checking happens in the middle of the process.
I fixed the make_bubbles method for you abit.
def make_bubbles(self):
self.buttonclick += 1 #Newly added in attempt to hide button
if self.buttonclick == 2: #Newly added in attempt to hide button
self.startbutton.configure(state="disabled")
showinfo("Oh No", "Sorry %s, but the game is already started" % self.name)
return
for each_no in range(1, 21):
...
The logic to enable the button at the end of game is missing or perhaps no need at all. Maybe I would rather clear all bubbles and generate 20 new ones each time I click the Start button. Anyway, I recommend this site for tkinter programming.
There is some more things to fix here too. No more iteritems() in Python 3. Use self.bubbles.items() instead. Good luck!
Related
Basically, this is an interactive heatmap but the twist is that the source is updated by reading values from a file that gets updated regularly.
dont bother about the class "generator", it is just for keeping data and it runs regularly threaded
make sure a file named "Server_dump.txt" exists in the same directory of the script with a single number greater than 0 inside before u execute the bokeh script.
what basically happens is i change a number inside the file named "Server_dump.txt" by using echo 4 > Server_dump.txt on bash,
u can put any number other than 4 and the script automatically checks the file and plots the new point.
if u don't use bash, u could use a text editor , replace the number and save, and all will be the same.
the run function inside the generator class is the one which checks if this file was modified , reads the number, transforms it into x& y coords and increments the number of taps associated with these coords and gives the source x,y,taps values based on that number.
well that function works fine and each time i echo a number , the correct rectangle is plotted but,
now I want to add the functionality of that clicking on a certain rectangle triggers a callback to plot a second graph based on the coords of the clicked rectangle but i can't even get it to trigger even though i have tried other examples with selected.on_change in them and they worked fine.
*if i increase self.taps for a certain rect by writing the number to the file multiple times, color gets updated but if i hover over the rect it shows me the past values and not the latest value only .
my bokeh version is 1.0.4
from functools import partial
from random import random,randint
import threading
import time
from tornado import gen
from os.path import getmtime
from math import pi
import pandas as pd
from random import randint, random
from bokeh.io import show
from bokeh.models import LinearColorMapper, BasicTicker, widgets, PrintfTickFormatter, ColorBar, ColumnDataSource, FactorRange
from bokeh.plotting import figure, curdoc
from bokeh.layouts import row, column, gridplot
source = ColumnDataSource(data=dict(x=[], y=[], taps=[]))
doc = curdoc()
#sloppy data receiving function to change data to a plottable shape
class generator(threading.Thread):
def __init__(self):
super(generator, self).__init__()
self.chart_coords = {'x':[],'y':[],'taps':[]}
self.Pi_coords = {}
self.coord = 0
self.pos = 0
self.col = 0
self.row = 0
self.s = 0
self.t = 0
def chart_dict_gen(self,row, col):
self.col = col
self.row = row+1
self.chart_coords['x'] = [i for i in range(1,cla.row)]
self.chart_coords['y'] = [i for i in range(cla.col, 0, -1)] #reversed list because chart requires that
self.chart_coords['taps']= [0]*(row * col)
self.taps = [[0 for y in range(col)] for x in range(row)]
def Pi_dict_gen(self,row,col):
key = 1
for x in range(1,row):
for y in range(1,col):
self.Pi_coords[key] = (x,y)
key = key + 1
def Pi_to_chart(self,N):
x,y = self.Pi_coords[N][0], self.Pi_coords[N][1]
return x,y
def run(self):
while True:
if(self.t == 0):
self.t=1
continue
time.sleep(0.1)
h = getmtime("Server_dump.txt")
if self.s != h:
self.s = h
with open('Server_dump.txt') as f:
m = next(f)
y,x = self.Pi_to_chart(int(m))
self.taps[x][y] += 1
# but update the document from callback
doc.add_next_tick_callback(partial(update, x=x, y=y, taps=self.taps[x][y]))
cla = generator()
cla.chart_dict_gen(15,15)
cla.Pi_dict_gen(15, 15)
x = cla.chart_coords['x']
y = cla.chart_coords['y']
taps = cla.chart_coords['taps']
#gen.coroutine
def update(x, y, taps):
taps += taps
print(x,y,taps)
source.stream(dict(x=[x], y=[y], taps=[taps]))
colors = ["#CCEBFF","#B2E0FF","#99D6FF","#80CCFF","#66c2FF","#4DB8FF","#33ADFF","#19A3FF", "#0099FF", "#008AE6", "#007ACC","#006BB2", "#005C99", "#004C80", "#003D66", "#002E4C", "#001F33", "#000F1A", "#000000"]
mapper = LinearColorMapper(palette=colors, low= 0, high= 15) #low = min(cla.chart_coords['taps']) high = max(cla.chart_coords['taps'])
TOOLS = "hover,save,pan,box_zoom,reset,wheel_zoom"
p = figure(title="Tou",
x_range=list(map(str,x)),
y_range=list(map(str,reversed(y))),
x_axis_location="above",
plot_width=900, plot_height=400,
tools=TOOLS, toolbar_location='below',
tooltips=[('coords', '#y #x'), ('taps', '#taps%')])
p.grid.grid_line_color = "#ffffff"
p.axis.axis_line_color = "#ef4723"
p.axis.major_tick_line_color = "#af0a36"
p.axis.major_label_text_font_size = "7pt"
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None
p.rect(x="x", y="y",
width=0.9, height=0.9,
source=source,
fill_color={'field': 'taps', 'transform': mapper},
line_color = "#ffffff",
)
color_bar = ColorBar(color_mapper=mapper,
major_label_text_font_size="7pt",
ticker=BasicTicker(desired_num_ticks=len(colors)),
formatter=PrintfTickFormatter(format="%d%%"),
label_standoff=6, border_line_color=None, location=(0, 0))
curdoc().theme = 'dark_minimal'
def ck(attr, old, new):
print('here') #doesn't even print hi in the terminal if i click anywhere
source.selected.on_change('indices', ck)
p.add_layout(color_bar, 'right')
doc.add_root(p)
thread = cla
thread.start()
i wanted even to get a printed hi in the terminal but nothing
You have not actually added any selection tool at all to your plot, so no selection is ever made. You have specified:
TOOLS = "hover,save,pan,box_zoom,reset,wheel_zoom"
Those are the only tools that will be added, and none of them make selections, there for nothing will cause source.selection.indices to ever be updated. If you are looking for selections based on tap, you must add a TapTool, e.g. with
TOOLS = "hover,save,pan,box_zoom,reset,wheel_zoom,tap"
Note that there will not be repeated callbacks if you tap the same rect multiple times. The callback only fires when the selection changes and clicking the same glyph twice in a row results in an identical selection.
I am new to PyqtGraph ( Infact this is my first time)
I have a Qt designer file which I import in my python code. I 6 windows in which I plot a 42x22 (different sizes) as an inmage, these have been promoted the graphicview.
I have a data set which is 6x42x22 and so I use a for loop to plot the 6 images
for n in range(imageStackSize):
self.glayout = pg.GraphicsLayout()
self.vb = self.glayout.addViewBox()
self.vb.setAspectLocked(lock=True, ratio=self.aspect_ratio)
img_temp = image[n, :, :]
and...
``
img = pg.ImageItem(img_temp, lut=self.jet_lut)
if n == 0:
self.ui.Channel1_img.setCentralItem(self.glayout)
self.vb.addItem(img)
elif n == 1:
self.ui.Channel2_img.setCentralItem(self.glayout)
self.vb.addItem(img)
elif n == 2:
self.ui.Channel3_img.setCentralItem(self.glayout)
self.vb.addItem(img)
elif n == 3:
self.ui.Channel4_img.setCentralItem(self.glayout)
self.vb.addItem(img)
elif n == 4:
self.ui.Channel5_img.setCentralItem(self.glayout)
self.vb.addItem(img)
elif n == 5:
self.ui.Channel6_img.setCentralItem(self.glayout)
self.vb.addItem(img)
After this I am trying to click on one of the image (ideally I would like to make it such that I can click any of the six images) to get the (6,x,y) coordinated the first dimension does not matter. In order to achieve this I did
self.ui.Channel1_img.scene().sigMouseClicked.connect(self.onClick)
#self.ui.Channel2_img.scene().sigMouseClicked.connect(self.onClick)
#self.ui.Channel3_img.scene().sigMouseClicked.connect(self.onClick)
#self.ui.Channel4_img.scene().sigMouseClicked.connect(self.onClick)
#self.ui.Channel5_img.scene().sigMouseClicked.connect(self.onClick)
#self.ui.Channel6_img.scene().sigMouseClicked.connect(self.onClick)
#self.ui.PMTVoltage_plt.scene().sigMouseClicked.connect(self.onClick)
def onClick(self, event):
print("clicked")
and then I tried
items = self.ui.Channel1_img.imageItem.mapFromViewToItem(event.pos())
and
items = self.ui.Channel1_img.imageItem.mapFromSceneToView(event.pos())
but the prog just crashes. I read somewhere that the coordinates are in the viewbox, but I cant seem to find the viewbox or vb in the self.ui.Channel1_img
I went through the entire ui variable in debug to look for vb or image Item and could not find it.
infact the only thing I found was,
items = {dict} {<pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox
object at 0x000001CF73888CA8>: [(0, 0)]}
<pyqtgraph.graphicsItems.ViewBox.ViewBox.ViewBox object at
0x000001CF73888CA8> (1990508186792) = {list} <class 'list'>: [(0, 0)]
0 = {tuple} <class 'tuple'>: (0, 0)
0 = {int} 0
1 = {int} 0
__len__ = {int} 2
__len__ = {int} 1
__len__ = {int} 1
what am I missing? Any help is appreciated
Ok I figured it out, here is my solution for some who may have same question
I plot the data using it as imageItem
vb = pg.ViewBox()
self.ui.PMTVoltage_plt.useOpenGL()
self.ui.PMTVoltage_plt.setCentralItem(vb)
self.img = pg.ImageItem(pmtImage)
vb.addItem(self.img)
and then in other function I recover the vb using getViewBox() and then use mapFromViewtoItem()
vb = self.ui.PMTVoltage_plt.getViewBox()
items = vb.mapSceneToView(event.scenePos())
pixels = vb.mapFromViewToItem(self.img, items)
print(items)
Hope this helps
I was wondering how to write code that would detect the mouse clicking on a sprite. For example:
if #Function that checks for mouse clicked on Sprite:
print ("You have opened a chest!")
I assume your game has a main loop, and all your sprites are in a list called sprites.
In your main loop, get all events, and check for the MOUSEBUTTONDOWN or MOUSEBUTTONUP event.
while ... # your main loop
# get all events
ev = pygame.event.get()
# proceed events
for event in ev:
# handle MOUSEBUTTONUP
if event.type == pygame.MOUSEBUTTONUP:
pos = pygame.mouse.get_pos()
# get a list of all sprites that are under the mouse cursor
clicked_sprites = [s for s in sprites if s.rect.collidepoint(pos)]
# do something with the clicked sprites...
So basically you have to check for a click on a sprite yourself every iteration of the mainloop. You'll want to use mouse.get_pos() and rect.collidepoint().
Pygame does not offer event driven programming, as e.g. cocos2d does.
Another way would be to check the position of the mouse cursor and the state of the pressed buttons, but this approach has some issues.
if pygame.mouse.get_pressed()[0] and mysprite.rect.collidepoint(pygame.mouse.get_pos()):
print ("You have opened a chest!")
You'll have to introduce some kind of flag if you handled this case, since otherwise this code will print "You have opened a chest!" every iteration of the main loop.
handled = False
while ... // your loop
if pygame.mouse.get_pressed()[0] and mysprite.rect.collidepoint(pygame.mouse.get_pos()) and not handled:
print ("You have opened a chest!")
handled = pygame.mouse.get_pressed()[0]
Of course you can subclass Sprite and add a method called is_clicked like this:
class MySprite(Sprite):
...
def is_clicked(self):
return pygame.mouse.get_pressed()[0] and self.rect.collidepoint(pygame.mouse.get_pos())
So, it's better to use the first approach IMHO.
The MOUSEBUTTONDOWN event occurs once when you click the mouse button and the MOUSEBUTTONUP event occurs once when the mouse button is released. The pygame.event.Event() object has two attributes that provide information about the mouse event. pos is a tuple that stores the position that was clicked. button stores the button that was clicked. Each mouse button is associated a value. For instance the value of the attributes is 1, 2, 3, 4, 5 for the left mouse button, middle mouse button, right mouse button, mouse wheel up respectively mouse wheel down. When multiple keys are pressed, multiple mouse button events occur. Further explanations can be found in the documentation of the module pygame.event.
Use the rect attribute of the pygame.sprite.Sprite object and the collidepoint method to see if the Sprite was clicked.
Pass the list of events to the update method of the pygame.sprite.Group so that you can process the events in the Sprite class:
class SpriteObject(pygame.sprite.Sprite):
# [...]
def update(self, event_list):
for event in event_list:
if event.type == pygame.MOUSEBUTTONDOWN:
if self.rect.collidepoint(event.pos):
# [...]
my_sprite = SpriteObject()
group = pygame.sprite.Group(my_sprite)
# [...]
run = True
while run:
event_list = pygame.event.get()
for event in event_list:
if event.type == pygame.QUIT:
run = False
group.update(event_list)
# [...]
Minimal example: repl.it/#Rabbid76/PyGame-MouseClick
import pygame
class SpriteObject(pygame.sprite.Sprite):
def __init__(self, x, y, color):
super().__init__()
self.original_image = pygame.Surface((50, 50), pygame.SRCALPHA)
pygame.draw.circle(self.original_image, color, (25, 25), 25)
self.click_image = pygame.Surface((50, 50), pygame.SRCALPHA)
pygame.draw.circle(self.click_image, color, (25, 25), 25)
pygame.draw.circle(self.click_image, (255, 255, 255), (25, 25), 25, 4)
self.image = self.original_image
self.rect = self.image.get_rect(center = (x, y))
self.clicked = False
def update(self, event_list):
for event in event_list:
if event.type == pygame.MOUSEBUTTONDOWN:
if self.rect.collidepoint(event.pos):
self.clicked = not self.clicked
self.image = self.click_image if self.clicked else self.original_image
pygame.init()
window = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()
sprite_object = SpriteObject(*window.get_rect().center, (128, 128, 0))
group = pygame.sprite.Group([
SpriteObject(window.get_width() // 3, window.get_height() // 3, (128, 0, 0)),
SpriteObject(window.get_width() * 2 // 3, window.get_height() // 3, (0, 128, 0)),
SpriteObject(window.get_width() // 3, window.get_height() * 2 // 3, (0, 0, 128)),
SpriteObject(window.get_width() * 2// 3, window.get_height() * 2 // 3, (128, 128, 0)),
])
run = True
while run:
clock.tick(60)
event_list = pygame.event.get()
for event in event_list:
if event.type == pygame.QUIT:
run = False
group.update(event_list)
window.fill(0)
group.draw(window)
pygame.display.flip()
pygame.quit()
exit()
See further Creating multiple sprites with different update()'s from the same sprite class in Pygame
The current position of the mouse can be determined via pygame.mouse.get_pos(). The return value is a tuple that represents the x and y coordinates of the mouse cursor. pygame.mouse.get_pressed() returns a list of Boolean values that represent the state (True or False) of all mouse buttons. The state of a button is True as long as a button is held down. When multiple buttons are pressed, multiple items in the list are True. The 1st, 2nd and 3rd elements in the list represent the left, middle and right mouse buttons.
Detect evaluate the mouse states in the Update method of the pygame.sprite.Sprite object:
class SpriteObject(pygame.sprite.Sprite):
# [...]
def update(self, event_list):
mouse_pos = pygame.mouse.get_pos()
mouse_buttons = pygame.mouse.get_pressed()
if self.rect.collidepoint(mouse_pos) and any(mouse_buttons):
# [...]
my_sprite = SpriteObject()
group = pygame.sprite.Group(my_sprite)
# [...]
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
group.update(event_list)
# [...]
Minimal example: repl.it/#Rabbid76/PyGame-MouseHover
import pygame
class SpriteObject(pygame.sprite.Sprite):
def __init__(self, x, y, color):
super().__init__()
self.original_image = pygame.Surface((50, 50), pygame.SRCALPHA)
pygame.draw.circle(self.original_image, color, (25, 25), 25)
self.hover_image = pygame.Surface((50, 50), pygame.SRCALPHA)
pygame.draw.circle(self.hover_image, color, (25, 25), 25)
pygame.draw.circle(self.hover_image, (255, 255, 255), (25, 25), 25, 4)
self.image = self.original_image
self.rect = self.image.get_rect(center = (x, y))
self.hover = False
def update(self):
mouse_pos = pygame.mouse.get_pos()
mouse_buttons = pygame.mouse.get_pressed()
#self.hover = self.rect.collidepoint(mouse_pos)
self.hover = self.rect.collidepoint(mouse_pos) and any(mouse_buttons)
self.image = self.hover_image if self.hover else self.original_image
pygame.init()
window = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()
sprite_object = SpriteObject(*window.get_rect().center, (128, 128, 0))
group = pygame.sprite.Group([
SpriteObject(window.get_width() // 3, window.get_height() // 3, (128, 0, 0)),
SpriteObject(window.get_width() * 2 // 3, window.get_height() // 3, (0, 128, 0)),
SpriteObject(window.get_width() // 3, window.get_height() * 2 // 3, (0, 0, 128)),
SpriteObject(window.get_width() * 2// 3, window.get_height() * 2 // 3, (128, 128, 0)),
])
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
group.update()
window.fill(0)
group.draw(window)
pygame.display.flip()
pygame.quit()
exit()
The pygame documentation for mouse events is here.
You can either use the pygame.mouse.get_pressed method in collaboration with the pygame.mouse.get_pos (if needed).
Remember to use the mouse click event via a main event loop. The reason why the event loop is better is due to "short clicks". You may not notice these on normal machines, but computers that use tap-clicks on trackpads have excessively small click periods. Using the mouse events will prevent this.
EDIT:
To perform pixel perfect collisions use pygame.sprite.collide_rect() found on their docs for sprites.
I was looking for the same answer to this question and after much head scratching this is the answer I came up with:
# Python 3.4.3 with Pygame
from sys import exit
import pygame
pygame.init()
WIDTH = HEIGHT = 300
window = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Crash!')
# Draw Once
rectangle = pygame.draw.rect(window, (255, 0, 0), (100, 100, 100, 100))
pygame.display.update()
# Main Loop
while True:
# Mouse position and button clicking
pos = pygame.mouse.get_pos()
pressed1 = pygame.mouse.get_pressed()[0]
# Check if rectangle collided with pos and if the left mouse button was pressed
if rectangle.collidepoint(pos) and pressed1:
print("You have opened a chest!")
# Quit pygame
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
I want to know how to arrange for the text on a ttk widget (a label or button, say) to resize automatically.
Changing the size of the text is easy, it is just a matter of changing the font in the style. However, hooking it into changes in the size of the window is a little more tricky. Looking on the web I found some hints, but there was nowhere a complete answer was posted.
So, here below is a complete working example posted as an answer to my own question. I hope someone finds it useful. If anyone has further improvements to suggest, I will be delighted to see them!
The example below shows two techniques, one activated by re-sizing the window (see the resize() method, bound to the <Configure> event), and the other by directly changing the size of the font (see the mutate() method).
Other code necessary to get resizing working is the grid configuration code in the __init__() method.
When running the example, there is some interaction between the two methods, but I think in a 'real' situation one technique would be sufficient, so that issue won't arise.
from tkinter import *
from tkinter.ttk import *
class ButtonApp(Frame):
"""Container for the buttons."""
def __init__(self, master=None):
"""Initialize the frame and its children."""
super().__init__(master)
self.createWidgets()
# configure the frame's resize behaviour
master.columnconfigure(0, weight=1)
master.rowconfigure(0, weight=1)
self.grid(sticky=(N,S,E,W))
# configure resize behaviour for the frame's children
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
# bind to window resize events
self.bind('<Configure>', self.resize)
def createWidgets(self):
"""Make the widgets."""
# this button mutates
self.mutantButton = Button(self, text='Press Me',
style='10.TButton')
self.mutantButton.grid(column=0, row=0, sticky=(N,S,E,W))
self.mutantButton['command'] = self.mutate
# an ordinary quit button for comparison
self.quitButton = Button(self, text='Quit', style='TButton')
self.quitButton.grid(column=0, row=1, sticky=(N,S,E,W))
self.quitButton['command'] = self.quit
def mutate(self):
"""Rotate through the styles by hitting the button."""
style = int(self.mutantButton['style'].split('.')[0])
newStyle = style + 5
if newStyle > 50: newStyle = 10
print('Choosing font '+str(newStyle))
self.mutantButton['style'] = fontStyle[newStyle]
# resize the frame
# get the current geometries
currentGeometry = self._root().geometry()
w, h, x, y = self.parseGeometry(currentGeometry)
reqWidth = self.mutantButton.winfo_reqwidth()
reqHeight = self.mutantButton.winfo_reqheight()
# note assume height of quit button is constant at 20.
w = max([w, reqWidth])
h = 20 + reqHeight
self._root().geometry('%dx%d+%d+%d' % (w, h, x, y))
def parseGeometry(self, geometry):
"""Geometry parser.
Returns the geometry as a (w, h, x, y) tuple."""
# get w
xsplit = geometry.split('x')
w = int(xsplit[0])
rest = xsplit[1]
# get h, x, y
plussplit = rest.split('+')
h = int(plussplit[0])
x = int(plussplit[1])
y = int(plussplit[2])
return w, h, x, y
def resize(self, event):
"""Method bound to the <Configure> event for resizing."""
# get geometry info from the root window.
wm, hm = self._root().winfo_width(), self._root().winfo_height()
# choose a font height to match
# note subtract 30 for the button we are NOT scaling.
# note we assume optimal font height is 1/2 widget height.
fontHeight = (hm - 20) // 2
print('Resizing to font '+str(fontHeight))
# calculate the best font to use (use int rounding)
bestStyle = fontStyle[10] # use min size as the fallback
if fontHeight < 10: pass # the min size
elif fontHeight >= 50: # the max size
bestStyle = fontStyle[50]
else: # everything in between
bestFitFont = (fontHeight // 5) * 5
bestStyle = fontStyle[bestFitFont]
# set the style on the button
self.mutantButton['style'] = bestStyle
root = Tk()
root.title('Alice in Pythonland')
# make a dictionary of sized font styles in the range of interest.
fontStyle = {}
for font in range(10, 51, 5):
styleName = str(font)+'.TButton'
fontName = ' '.join(['helvetica', str(font), 'bold'])
fontStyle[font] = styleName
Style().configure(styleName, font=fontName)
# run the app
app = ButtonApp(master=root)
app.mainloop()
root.destroy()
I am wondering how to best truncate text in a QLabel based on it's maximum width/height.
The incoming text could be any length, but in order to keep a tidy layout I'd like to truncate long strings to fill a maximum amount of space (widget's maximum width/height).
E.g.:
'A very long string where there should only be a short one, but I can't control input to the widget as it's a user given value'
would become:
'A very long string where there should only be a short one, but ...'
based on the required space the current font needs.
How can I achieve this best?
Here is a simple example of what I'm after, though this is based on word count, not available space:
import sys
from PySide.QtGui import *
from PySide.QtCore import *
def truncateText(text):
maxWords = 10
words = text.split(' ')
return ' '.join(words[:maxWords]) + ' ...'
app = QApplication(sys.argv)
mainWindow = QWidget()
layout = QHBoxLayout()
mainWindow.setLayout(layout)
text = 'this is a very long string, '*10
label = QLabel(truncateText(text))
label.setWordWrap(True)
label.setFixedWidth(200)
layout.addWidget(label)
mainWindow.show()
sys.exit(app.exec_())
Even easier - use the QFontMetrics.elidedText method and overload the paintEvent, here's an example:
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QApplication,\
QLabel,\
QFontMetrics,\
QPainter
class MyLabel(QLabel):
def paintEvent( self, event ):
painter = QPainter(self)
metrics = QFontMetrics(self.font())
elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())
painter.drawText(self.rect(), self.alignment(), elided)
if ( __name__ == '__main__' ):
app = None
if ( not QApplication.instance() ):
app = QApplication([])
label = MyLabel()
label.setText('This is a really, long and poorly formatted runon sentence used to illustrate a point')
label.setWindowFlags(Qt.Dialog)
label.show()
if ( app ):
app.exec_()
I found that #Eric Hulser's answer, while great, didn't work when the label was put into another widget.
I came up with this by hacking together Eric's response with the Qt Elided Label Example. It should behave just like a regular label, yet elide horizontally when the text width exceeds the widget width. It has an extra argument for different elide modes. I also wrote some tests for fun :)
If you want to use PyQt5...
Change "PySide2" to "PyQt5"
Change "Signal" to "pyqtSignal"
Enjoy!
Eliding Label
# eliding_label.py
from PySide2 import QtCore, QtWidgets, QtGui
class ElidingLabel(QtWidgets.QLabel):
"""Label with text elision.
QLabel which will elide text too long to fit the widget. Based on:
https://doc-snapshots.qt.io/qtforpython-5.15/overviews/qtwidgets-widgets-elidedlabel-example.html
Parameters
----------
text : str
Label text.
mode : QtCore.Qt.TextElideMode
Specify where ellipsis should appear when displaying texts that
don’t fit.
Default is QtCore.Qt.ElideMiddle.
Possible modes:
QtCore.Qt.ElideLeft
QtCore.Qt.ElideMiddle
QtCore.Qt.ElideRight
parent : QWidget
Parent widget. Default is None.
f : Qt.WindowFlags()
https://doc-snapshots.qt.io/qtforpython-5.15/PySide2/QtCore/Qt.html#PySide2.QtCore.PySide2.QtCore.Qt.WindowType
"""
elision_changed = QtCore.Signal(bool)
def __init__(self, text='', mode=QtCore.Qt.ElideMiddle, **kwargs):
super().__init__(**kwargs)
self._mode = mode
self.is_elided = False
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.setText(text)
def setText(self, text):
self._contents = text
# This line set for testing. Its value is the return value of
# QFontMetrics.elidedText, set in paintEvent. The variable
# must be initialized for testing. The value should always be
# the same as contents when not elided.
self._elided_line = text
self.update()
def text(self):
return self._contents
def paintEvent(self, event):
super().paintEvent(event)
did_elide = False
painter = QtGui.QPainter(self)
font_metrics = painter.fontMetrics()
text_width = font_metrics.horizontalAdvance(self.text())
# layout phase
text_layout = QtGui.QTextLayout(self._contents, painter.font())
text_layout.beginLayout()
while True:
line = text_layout.createLine()
if not line.isValid():
break
line.setLineWidth(self.width())
if text_width >= self.width():
self._elided_line = font_metrics.elidedText(self._contents, self._mode, self.width())
painter.drawText(QtCore.QPoint(0, font_metrics.ascent()), self._elided_line)
did_elide = line.isValid()
break
else:
line.draw(painter, QtCore.QPoint(0, 0))
text_layout.endLayout()
if did_elide != self.is_elided:
self.is_elided = did_elide
self.elision_changed.emit(did_elide)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
long_text = "this is some long text, wouldn't you say?"
elabel = ElidingLabel(long_text)
elabel.show()
app.exec_()
Test Eliding Label
# test_eliding_label.py.py
#
# Run tests with
#
# python3 -m unittest test_eliding_label.py --failfast --quiet
import unittest
import unittest.mock
from PySide2 import QtCore, QtWidgets, QtGui, QtTest
import eliding_label
if not QtWidgets.QApplication.instance():
APP = QtWidgets.QApplication([]) # pragma: no cover
class TestElidingLabelArguments(unittest.TestCase):
def test_optional_text_argument(self):
elabel = eliding_label.ElidingLabel()
self.assertEqual(elabel.text(), "")
def test_text_argument_sets_label_text(self):
elabel = eliding_label.ElidingLabel(text="Test text")
self.assertEqual(elabel.text(), "Test text")
def test_optional_elision_mode_argument(self):
elabel = eliding_label.ElidingLabel()
self.assertEqual(elabel._mode, QtCore.Qt.ElideMiddle)
class TestElidingLabel(unittest.TestCase):
def setUp(self):
self.elabel = eliding_label.ElidingLabel()
def test_elabel_is_a_label(self):
self.assertIsInstance(self.elabel, QtWidgets.QLabel)
def test_has_elision_predicate(self):
self.assertEqual(self.elabel.is_elided, False)
def test_elision_predicate_changes_when_text_width_exceeds_widget_width(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(25)
self.assertEqual(self.elabel.width(), 25)
long_text = "This is line is definely longer than 25 pixels."
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 25)
self.elabel.setText(long_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
self.assertEqual(self.elabel.is_elided, True)
def test_text_is_elided_when_text_width_exceeds_widget_width(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(25)
self.assertEqual(self.elabel.width(), 25)
long_text = "This is line is definely longer than 25 pixels."
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 25)
self.elabel.setText(long_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
# PySide2.QtGui.QFontMetrics.elidedText states, "If the string
# text is wider than width , returns an elided version of the
# string (i.e., a string with '…' in it). Otherwise, returns
# the original string."
self.assertEqual(self.elabel._elided_line, '…')
def test_text_is_not_elided_when_text_width_is_less_than_widget_width(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(500)
self.assertEqual(self.elabel.width(), 500)
short_text = "Less than 500"
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
short_text_width = font_metrics.horizontalAdvance(short_text)
self.assertLess(short_text_width, 500)
self.elabel.setText(short_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
# PySide2.QtGui.QFontMetrics.elidedText states, "If the string
# text is wider than width , returns an elided version of the
# string (i.e., a string with '…' in it). Otherwise, returns
# the original string."
self.assertEqual(self.elabel._elided_line, short_text)
def test_stores_full_text_even_when_elided(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(25)
self.assertEqual(self.elabel.width(), 25)
long_text = "This is line is definely longer than 25 pixels."
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 25)
self.elabel.setText(long_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
# PySide2.QtGui.QFontMetrics.elidedText states, "If the string
# text is wider than width , returns an elided version of the
# string (i.e., a string with '…' in it). Otherwise, returns
# the original string."
self.assertEqual(self.elabel._elided_line, '…')
self.assertEqual(self.elabel.text(), long_text)
def test_has_elision_changed_signal(self):
self.assertIsInstance(self.elabel.elision_changed, QtCore.Signal)
def test_elision_changed_signal_emits_on_change_to_is_elided_predicate(self):
mock = unittest.mock.Mock()
self.elabel.elision_changed.connect(mock.method)
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
# Induce elision
self.elabel.setFixedWidth(150)
self.assertEqual(self.elabel.width(), 150)
long_text = "This line is definitely going to be more than 150 pixels"
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 150)
self.elabel.setText(long_text)
self.assertEqual(self.elabel.is_elided, False) # no elide until painting
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
self.assertEqual(self.elabel.is_elided, True)
mock.method.assert_called_once()
# Remove elision
short_text = "Less than 150"
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
short_text_width = font_metrics.horizontalAdvance(short_text)
self.assertLess(short_text_width, 150)
self.elabel.setText(short_text)
self.assertEqual(self.elabel.is_elided, True) # still elided until painting
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
self.assertEqual(self.elabel.is_elided, False)
self.assertEqual(mock.method.call_count, 2)
You can achieves this through determining the width with QFontMetrics, see this answer.
You would probably want to use or create some algorithm which finds the place to cut quickly, unless doing it in a simple for loop would be sufficient.
simpler solution if you want show QLabel in center in provided area
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.minimumSizeHint = lambda self=label: QSize(0, QLabel.minimumSizeHint(self).height() )