Group widgets in parent container tkinter - python

Im drawing lines in the canvas, the problem is that the number of lines is not static, so to draw them i use a loop:
def createBars(self):
size = (200, 50);
for i in range(6):
self.canvas.create_line(size[0], i*size[1], size[0], size[1]+(i*size[1]), fill = self.COLORS[i], width = 10);
it works, i get my lines one below each other as i wish, the problem now is that i need to move all of them at the same time when clicking of them, since to move each of them i need to access to the method moveto, i dont know how to move all of them as group, cant find any related like draw all them inside a box then i can move the box with them inside.
tried appending each of the lines in an array but when printing i get an integer instead of the line object, but if creating it in the constructor each of them can access to the method, but doing it in array way dont know how

First off, a bit of terminology. The lines you draw are not widgets. They are called canvas items, and are very different than widgets.
The solution is to apply one or more tags to each item, and then pass the tag name to the moveto method, or any other method that takes an item id or a tag.
Here's an example that adds the tags "line" and the line color to each element. The arrow keys will move all of the items together. You can easily modify it to move only a particular color by changing the tag passed to the move method.
class Example(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.canvas = tk.Canvas(self)
self.canvas.pack(fill="both", expand=True)
self.COLORS=["red", "orange", "yellow", "green", "blue", "indigo", "violet"]
self.createBars()
self.canvas.bind("<Left>", self.go_left)
self.canvas.bind("<Right>", self.go_right)
self.canvas.focus_set()
def go_left(self, event):
self.canvas.move("line", -5, 0)
def go_right(self, event):
self.canvas.move("line", +5, 0)
def createBars(self):
size = (200, 50);
for i in range(6):
self.canvas.create_line(
size[0], i*size[1], size[0], size[1]+(i*size[1]),
fill = self.COLORS[i], width = 10,
tags=("line", self.COLORS[i]),
);
tried appending each of the lines in an array but when printing i get an integer instead of the line object
This is the defined behavior. create_line and the other methods for creating canvas items return an integer identifier for the created object. The lines and other canvas items aren't themselves python objects.

Related

Moving shapes from one scene/window to another using Python tkinter canvas

Here is a rough example of what I want to do(my code is too long and messy to share here): -
import tkinter as tk
app = tk.Tk()
w, h = 600, 600
canvas = tk.Canvas(app, width = w, height = h)
canvas.pack()
Rec1 = canvas.create_rectangle(0, 0, 100, 100, fill = 'blue', tag = 'move_to_next_window')
Rec2 = canvas.create_rectangle(100, 100, fill='green', tag = 'dont_move_to_next_window')
app.mainloop()
I am sorry if I messed up a couple lines but this program should run by creating 2 rectangles. What I need help with is if I initiate a brand new window which is running off different code, how would I move Rec1 and its position to the other window. If its possible, could I copy all of the object's properties in the second window? Thank you for taking the time to read this (the second window can also use the tkinter canvas).
What I need help with is if I initiate a brand new window which is running off different code, how would I move Rec1 and its position to the other window.
You can't move canvas items from one canvas to another. Your only option is to delete the item in the first canvas and add a new item in the other canvas.
If its possible, could I copy all of the object's properties in the second window?
Yes, you can use the itemconfigure method to get all of the properties of a canvas object, you can use coords to get the coordinates, and you can get the type with type method.
Here's an example function that copies a rectangle from one canvas to another.
def copy_canvas_item(source, item_id, destination):
item_type = source.type(item_id)
coords = source.coords(item_id)
# N.B. 'itemconfigure' returns a dictionary where each element
# has a value that is a list. The currently configured value
# is at index position 4
attrs = {x[0]: x[4] for x in source.itemconfigure(item_id).values()}
if item_type == "rectangle":
item_id = destination.create_rectangle(*coords, attrs)
return item_id

How to fixed image when moving item in QGraphicsViewer

I am developing an image viewer using pyqt.
I want the image to be fixed when the box moved.
However, the image is pushed when the box tries to reach the viewer's side like this.
It was implemented using QGraphicsView, QGraphicsScene, and QGraphicsitem.
This is part of main class
self.scene_r = GraphicsScene()
self.scene_r.addPixmap(pix_resized)
self.resizedView.setScene(self.scene_r)
self.resizedView.centerOn(128,128)
This is QGraphicScene Class
class GraphicsScene(QGraphicsScene):
def __init__(self, parent=None):
QGraphicsScene.__init__(self)
rect_item = recItem(QRectF(0, 0, 100, 100))
rect_item.setFlag(QGraphicsItem.ItemIsMovable, True)
rect_item.setZValue(1)
rect_item.setPen(Qt.green)
self.addItem(rect_item)
I tried to override mouseMoveEvent() of QGraphicsRectItem class, but it failed.
That happens for two reasons:
Since you are making the item movable, you can move it freely, anywhere you want.
When the scene rectangle is smaller than the one visible in the view, the view tries to ensure that the whole scene rectangle stays visible, possibly by scrolling its contents.
Note that, unless explicitly set using setSceneRect() (on the scene or on the view, the results might differ), Qt automatically sets the scene rect implicitly to the bigger QRect that contains all visible graphics items.
There at least two possible solutions to your problem, which one to choose depends on what you need, and you can also decide to use both of them.
Explicitly set the sceneRect
You can set the sceneRect to a specific rectangle, an item, or all existing items. Note that, while in your case it won't change much if you set the rectangle for the scene or the view, in more complex cases (for example, multiple views showing the same scene) the result might differ.
# on the view
self.setSceneRect(self.scene_r.itemsBoundingRect())
# alternativaly, on the scene
self.setSceneRect(self.itemsBoundingRect())
Limit the area in which the item can be moved
In this case you can intercept the itemChange ItemPositionChange (note that the ItemSendsGeometryChanges flag must be set) and return an adjusted value before it is actually applied:
class RecItem(QGraphicsRectItem):
def __init__(self, *args):
super().__init__(*args)
self.setFlags(self.ItemSendsGeometryChanges)
def itemChange(self, change, value):
if change == self.ItemPositionChange:
sceneRect = self.scene().sceneRect()
newGeometry = self.boundingRect().translated(value)
# the item pen must be taken into account
halfPen = self.pen().width() / 2
if value.x() < sceneRect.x():
value.setX(sceneRect.x() + halfPen)
if value.y() < sceneRect.y():
value.setY(sceneRect.y() + halfPen)
if newGeometry.right() + halfPen > sceneRect.right():
value.setX(sceneRect.right() - newGeometry.width())
if newGeometry.bottom() + halfPen > sceneRect.bottom():
value.setY(sceneRect.bottom() - newGeometry.height())
return value
return super().itemChange(change, value)

Tkinter Canvas: Change option without changing others OR set all options again

Creating a fairly basic window and trying to keep everything nice and tightly jammed in the window so that there are no gaps between items in the grid requires setting the canvas's border to -2 (annoying, should be 0, but a different complaint for a different day). However, when updating other attributes (in this case bg), that bd attribute gets reset irreparably. Requesting that property returns the -2 it got set to, but the canvas acts like it isn't (i.e., the canvas appears to have a bd value of 0, despite saying it has -2 when asked).
How can one update only one attribute without breaking the bd attribute?
OR,
How can one set all the attributes for that object simultaneously after it already exists similarly to when it was created so that bd actually takes effect?
A simple example that lets you play with it:
import tkinter as tk
from functools import partial
class MyGUI:
def __init__(self, master):
self.master = master
self.buttonx = tk.Button(master, text='goblue', command=partial(self.gocol, 'lightblue'), bg='lightblue')
self.buttonx.grid(row=1,column=0)
self.buttony = tk.Button(master, text='gogrey', command=partial(self.gocol, 'grey'), bg='grey')
self.buttony.grid(row=2,column=0)
self.canvasx_specs = {
'width' : 400,
'height' : 400,
'bg' : 'grey',
'bd' : -2
}
self.canvasx = tk.Canvas(master, **self.canvasx_specs)
self.canvasx.grid(rowspan=9,row=1,column=1)
def gocol(self, col):
## Method 1
self.canvasx['bg']=col
# self.canvasx.configure(bg=col)
## Method 2
# self.canvasx_specs['bg'] = col
# self.canvasx.configureall(**self.canvasx_specs)
top = tk.Tk()
mywin = MyGUI(top)
top.mainloop()
It doesn't appear to matter if you set bd to -2 again, it still acts like it is 0. It also doesn't appear to matter if you use members access or configure function (see Method 1 above), it has the same effect.
I don't want to delete the whole canvas, it may have drawn objects in it already, and I don't want to redraw everything when the background (or some other option) changes.
Canvas (and other widgets) may have two elements - border and highlight.
If you set bd to 0 or -2 then you can still see highlight which you can remove with 'highlightthickness': 0

tkinter - resizing empty frame

How do you force a frame to get window_height 0?
the general case where my problem occurs:
import Tkinter as Tk
class App(Tk.Frame):
def __init__(self, master):
Tk.Frame(self, master)
self.place_holder = Tk.Frame(master=self)
self.content = Tk.Frame(master=self)
self.place_holder.pack()
self.content.pack(side=Tk.RIGHT)
Tk.Button(master=self,command=self.add_something).pack(side=Tk.TOP)
self.to_destroy = []
def add_something(self):
foo = Tk.button(master=self.place_holder, command=self.destroy_last)
self.too_destroy.append(foo)
def destroy_last(self):
self.to_destroy[-1].destroy()
the problem:
As I add more elements to the place_holder, it rescales nicely.
When I remove elements from the place_holder, it rescales nicely.
EXCEPT when I remove the last element.
Before i added anything, even when i do place_holder.pack(), it will not show. But after removing the last element, the place_holder will keep the size of this last element. Is there a way to hide the place_holder again untill i add content again?
example image
The empty container at the bottom left does not contain any elements, but still has the size of the last element in it, how can i get this to disappear without removing it (i want it again in the same place)?
What is happening is that when you remove the last widget, pack no longer is managing the frame so it isn't responsible for setting the frame size.
The simplest solution is just to temporarily pack a 1x1 pixel frame, which wil cause the placeholder frame to shrink.
There's no way to make a frame of zero pixels, so this method will always result in a one pixel tall/wide area for the placeholder. If you don't want that one pixel, you can install call pack_forget on the placeholder to completely remove it from the display, and then use pack with suitable options to re-add it when you put something in it.
Example:
def destroy_last(self):
self.to_destroy.pop().destroy()
if len(self.to_destroy) == 0:
tmp = Tk.Frame(self.place_holder, width=1, height=1, borderwidth=0)
tmp.pack()
self.place_holder.update()
tmp.destroy()

Fix Text widget size

I have made an application and part of it involves entering a question and answer. I have this code:
import tkinter as tk
root = tk.Tk()
root.geometry("500x250")
#Main question/answer frame
createFrm = tk.Frame(root)
createFrm.pack(expand = True) #To centre the contents in the window
#Create question entry area
cnqFrm = tk.Frame(createFrm)
cnqFrm.pack()
cnqFrm.pack_propagate(False)
#Question entry
cnqLabQ = tk.Label(cnqFrm, text = "Question")
cnqLabQ.grid(column = 0, row = 0)
#Frame for question Text
cnqTxtQFrm = tk.Frame(cnqFrm, height = 100, width = 100)
cnqTxtQFrm.grid(column = 0, row = 1)
cnqTxtQFrm.grid_propagate(False)
#Question Text
cnqTxtQ = tk.Text(cnqTxtQFrm)
cnqTxtQ.pack()
cnqTxtQ.pack_propagate(False)
#Answer entry
cnqLabA = tk.Label(cnqFrm, text = "Answer")
cnqLabA.grid(column = 1, row = 0)
#Frame for answer text
cnqTxtAFrm = tk.Frame(cnqFrm, height = 100, width = 100)
cnqTxtAFrm.grid(column = 1, row = 1)
cnqTxtAFrm.grid_propagate(False)
#Answer Text
cnqTxtA = tk.Text(cnqTxtAFrm)
cnqTxtA.pack()
cnqTxtA.pack_propagate(False)
Despite the fact the Text widget is in a Frame with grid_propagate(False) and a fixed height and width, and the Text widget itself has pack_propagate(False), it still expands to far larger than it should be. Why is this and how can I fix it?
You don't give the text widget an explicit size, so it defaults to 40x80 average-sized characters. The most common way to force it to a specific size that is determined by its parent is to give it a size that is smaller than the containing widget, and then let grid or pack expand it to fit the space given to it. So, start by giving the text widget a width and height of 1 (one).
Next, in this specific case you are calling grid_propagate(False) on the containing frame, but you are using pack to manage the window. You should call pack_propagate if you're using pack. You also need to tell pack to expand the text widget to fill its frame.
Finally, there's no point in calling cnqTxtQ.pack_propagate(False) since that only affects children of the text widget and you've given it no children.
All of that being said, I strongly encourage you to not use grid_propagate(False) and pack_propagate(False). Tkinter is really good at arranging widgets. Instead of trying to force the text widget to a specific pixel size, set the text widget to the desired size in lines and characters, and let tkinter intelligently arrange everything else to line up with them.

Categories