Unexpected behavior of the Canvas - python

I have some code for example. That contains two buttons. I have bind the on_press event for btn, and on click I tried to change its Scale. But in fact this code somehow changes size of btn1 instead btn. I do not know why
class TutorialApp(App):
scale = 1
def btn_click(self, elem):
if self.scale < 0.2: return
sc = Scale()
sc.xyz = (0.8,0.8,1)
self.scale *= 0.8
print self.scale
elem.canvas.add(sc) #after и canvas has same behavior, but before changes scales of both buttons/ I don't know why
def build(self):
bl = Layout(text='APP')
bl.orientation = 'vertical'
btn = Button(text='OK', size_hint=(1,0.5))
btn1 = Button(text='OK1', size_hint=(1,0.5))
btn.bind(on_press = self.btn_click)
bl.add_widget(btn) #,pos=(100,100)
bl.add_widget(btn1) #,pos=(100,100)
return bl
I have tried to employ after or before, and by elem.canvas.after behavior is same as without them, by before I get changes size of the both buttons. Why is this happening?

The elem.canvas.add(sc) appends the Scale to the list of instructions for btn. So it doesn't affect the drawing of btn, but does affect the drawing of btn1 (since it is drawn after btn). Using canvas.before inserts the Scale before the btn drawing, so it affects both. Obviously, using canvas.after has the same effect as the original.

Related

PyQt5 left click not working for mouseMoveEvent

I'm trying to learn PyQt5, and I've got this code:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.label = QLabel()
canvas = QPixmap(400, 300)
canvas.fill(Qt.white)
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
def mouseMoveEvent(self, e):
painter = QPainter(self.label.pixmap())
painter.drawPoint(e.x(), e.y())
painter.end()
self.update()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
And I can draw using right click to draw, but when I left click, it drags the window instead of drawing. This even happens when I make the window fullscreen so I can't move it. How can I stop it from dragging the window so it will draw instead?
In some configurations (specifically, on Linux, and depending on the window manager settings), dragging the left mouse button on an empty (non interactive) area of a QMainWindow allows dragging the whole window.
To prevent that, the mouse move event has to be accepted by the child widget that receives it.
While this can be achieved with an event filter, it's usually better to use a subclass, and this is even more important whenever the widget has to deal with mouse events it receives, exactly like in this case.
Another aspect that has to be considered is that just updating the QLabel pixmap is not completely sufficient, because it doesn't automatically force its update. Also, since Qt 5.15, QLabel.pixmap() doesn't return a pointer to the pixmap, but rather its copy. This means that you should always keep a local reference to the pixmap for the whole time required to access it (otherwise your program will crash), and then call setPixmap() again with the updated pixmap after "ending" the painter. This will automatically schedule an update of the label.
The above may be a bit confusing if you're not used to languages that allow pointers as arguments, but, in order to clarify how it works, you can consider the pixmap() property similarly to the text() one:
text = self.label.text()
text += 'some other text'
The above will obviously not change the text of the label, most importantly because, in most languages (including Python) strings are always immutable objects, so text += ... actually replaces the text reference with another string object.
To clarify, consider the following:
text1 = text2 = self.label.text()
text1 += 'some other text'
print(text1 == text2)
Which will return False.
Now consider this instead:
list1 = list2 = []
list1 += ['item']
print(list1 == list2)
Which will return True, because list is a mutable type, and in python changing the content of a mutable type will affect any reference to it[1], since they refer to the same object.
Until Qt < 5.15, the pixmap of QLabel behaved similarly to a list, meaning that any painting on the label.pixmap() would actually change the content of the displayed pixmap (requiring label.update() to actually show the change). After Qt 5.15 this is no longer valid, as the returned pixmap behaves similarly to a returned string: altering its contents won't change the label's pixmap.
So, the proper way to update the pixmap is to:
handle the mouse event in the label instance (either by subclassing or using an event filter), and not in a parent;
get the pixmap, keep its reference until painting has completed, and call setPixmap() afterwards (mandatory since Qt 5.15, but also suggested anyway);
Finally, QLabel has an alignment property that, when using a pixmap, is used to set the alignment of the pixmap to the available space that the layout manager provides. The default is left aligned and vertically centered (Qt.AlignLeft|Qt.AlignVCenter).
QLabel also features the scaledContents property, which always scales the pixmap to the current size of the label (not considering the aspect ratio).
The above means one of the following:
the pixmap will always be displayed at its actual size, and eventually aligned within its available space;
if the scaledContents property is True, the alignment is ignored and the pixmap will be always scaled to the full extent of its available space; whenever that property is True, the resulting pixmap is also cached, so you have to clear its cache every time (at least, with Qt5);
if you need to always keep aspect ratio, using QLabel is probably pointless, and you may prefer a plain QWidget that actively draws the pixmap within a paintEvent() override;
Considering the above, here is a possible implementation of the label (ignoring the ratio):
class PaintLabel(QLabel):
def mouseMoveEvent(self, event):
pixmap = self.pixmap()
if pixmap is None:
return
pmSize = pixmap.size()
if not pmSize.isValid():
return
pos = event.pos()
scaled = self.hasScaledContents()
if scaled:
# scale the mouse position to the actual pixmap size
pos = QPoint(
round(pos.x() * pmSize.width() / self.width()),
round(pos.y() * pmSize.height() / self.height())
)
else:
# translate the mouse position depending on the alignment
alignment = self.alignment()
dx = dy = 0
if alignment & Qt.AlignRight:
dx += pmSize.width() - self.width()
elif alignment & Qt.AlignHCenter:
dx += round((pmSize.width() - self.width()) / 2)
if alignment & Qt.AlignBottom:
dy += pmSize.height() - self.height()
elif alignment & Qt.AlignVCenter:
dy += round((pmSize.height() - self.height()) // 2)
pos += QPoint(dx, dy)
painter = QPainter(pixmap)
painter.drawPoint(pos)
painter.end()
# this will also force a scheduled update
self.setPixmap(pixmap)
if scaled:
# force pixmap cache clearing
self.setScaledContents(False)
self.setScaledContents(True)
def minimumSizeHint(self):
# just for example purposes
return QSize(10, 10)
And here is an example of its usage:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.label = PaintLabel()
canvas = QPixmap(400, 300)
canvas.fill(Qt.white)
self.label.setPixmap(canvas)
self.hCombo = QComboBox()
for i, hPos in enumerate(('Left', 'HCenter', 'Right')):
hAlign = getattr(Qt, 'Align' + hPos)
self.hCombo.addItem(hPos, hAlign)
if self.label.alignment() & hAlign:
self.hCombo.setCurrentIndex(i)
self.vCombo = QComboBox()
for i, vPos in enumerate(('Top', 'VCenter', 'Bottom')):
vAlign = getattr(Qt, 'Align' + vPos)
self.vCombo.addItem(vPos, vAlign)
if self.label.alignment() & vAlign:
self.vCombo.setCurrentIndex(i)
self.scaledChk = QCheckBox('Scaled')
central = QWidget()
mainLayout = QVBoxLayout(central)
panel = QHBoxLayout()
mainLayout.addLayout(panel)
panel.addWidget(self.hCombo)
panel.addWidget(self.vCombo)
panel.addWidget(self.scaledChk)
mainLayout.addWidget(self.label)
self.setCentralWidget(central)
self.hCombo.currentIndexChanged.connect(self.updateLabel)
self.vCombo.currentIndexChanged.connect(self.updateLabel)
self.scaledChk.toggled.connect(self.updateLabel)
def updateLabel(self):
self.label.setAlignment(Qt.AlignmentFlag(
self.hCombo.currentData() | self.vCombo.currentData()
))
self.label.setScaledContents(self.scaledChk.isChecked())
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Note that if you need more advanced control over the pixmap display and painting (including aspect ratio, but also proper zoom capabilities and any possible complex feature), then the common suggestion is to completely ignore QLabel, as said above: either use a basic QWidget, or consider the more complex (but much more powerful) Graphics View Framework. This will also allow proper editing features, as you can add non-destructive editing that will show ("paint") the result without affecting the actual, original object.
[1]: The above is based on the fact that a function or operator can actually mutate the object: the += operator actually calls the __add__ magic method that, in the case of lists, updates the contents of the same list.

What units does the scaling method work in tkinter canvas?

Trying to figure out how the scaling method in the Tkinter Canvas package works.
import tkinter
root = tkinter.Tk()
root.geometry("1200x800")
root.resizable(False,False)
root.title("Examples")
myCanvas = tkinter.Canvas(root, bg="white", height=700, width=1000)
myCanvas.create_rectangle(0,200,300,300, tags="myTag")
myCanvas.scale("myTag", 8200,1200,0.99,0.99)
myCanvas.create_rectangle(0,400,300,300, tags="myTag2")
myCanvas.move("myTag2",200,0)
input_elem = tkinter.Entry(root,width=50)
input_elem.grid(row=1, column=0)
btn = tkinter.Button(root,width=50, text="here", height=5)
btn.grid(row=1, column=2)
myCanvas.grid(row=0, column=0)
root.mainloop()
in the documentation I found this:
.scale(tagOrId, xOffset, yOffset, xScale, yScale) Scale all objects
according to their distance from a point P=(xOffset, yOffset). The
scale factors xScale and yScale are based on a value of 1.0, which
means no scaling. Every point in the objects selected by tagOrId is
moved so that its x distance from P is multiplied by xScale and its y
distance is multiplied by yScale.
This method will not change the size of a text item, but may move it.
Based on this I would have expected the .scale() to work in the same units as the canvas (by default pixels) but it seems like it doesn't. My xOffset value is fairly large and the rectangle moved only very little. So I created a second rectangle to compare and realized it's scaling based off of the width of the canvas, so this :
myCanvas.scale("myTag", (20*0.99)*1000,1200,0.99,0.99)
myCanvas.move("myTag2",(0.99*200),0)
equals to the same xOffset. Why is is the scaling a factor of 10 though? Shouldn't (200*0.99)*1000 in the scaling method equal to 0.99*200 in the move method? Or can someone point me to a more detailed documentation?
I think the best way to understand Canvas move and scale is to see them in action.
Here is a small interactive demo that may help.
It draws a small rectangle (square) at origin.
Control for moving (pick and place) the square is single Button-1 click
Control for scale is via keyboard (Up, Right) = grow (Down and Left) = shrink
Run the code, pick up the square and place it near canvas center.
Make it grow by pressing and holding Up or Right key.
Now make it shrink by pressing and holding Down or Left key.
Try moving mouse pointer inside and outside of square while changing scale.
Note how the square moves away from your mouse pointer when growing
and moves toward your mouse pointer when shrinking.
Exactly what one would expect from objects approaching or receding.
You can change scale and move square at the same time.
This works with objects line, polygon, rectangle and oval.
import tkinter as tk
class moveScale(tk.Tk):
def __init__(self):
super().__init__()
# Remove various paddings so the entire canvas is visible
self.canvas = tk.Canvas(
self, highlightthickness = 0, borderwidth = 0,
cursor = "crosshair")
self.canvas.grid(sticky = tk.NSEW)
self.xx = self.yy = 0 # Store for dynamic mouse pointer position
self.con = False # Flag controls move on|off
self.tag = "myTag"
self.sU = 1.005 # Scale up (growth)
self.sD = 1 / self.sU # Scale down (shrink)
self.canvas.create_rectangle(
0, 0, 40, 40, fill = "", tags = self.tag)
# Mouse and Keyboard bindings
self.canvas.bind("<Motion>", self.mover)
self.canvas.bind("<Button-1>", self.swap)
self.canvas.event_add("<<BIG>>", "<Up>", "<Right>")
self.canvas.event_add("<<GIB>>", "<Left>", "<Down>")
self.canvas.bind("<<BIG>>", self.big)
self.canvas.bind("<<GIB>>", self.small)
# Keyboard input only works when canvas has the focus
self.canvas.focus_force()
# Movement control
def swap(self, ev):
self.con = self.con == False
def mover(self, ev):
x, y = self.canvas.canvasx(ev.x), self.canvas.canvasy(ev.y)
if self.con:
self.canvas.move(self.tag, x - self.xx, y - self.yy)
self.xx, self.yy = x, y
def big(self, ev):
self.canvas.scale(self.tag, self.xx, self.yy, self.sU, self.sU)
def small(self, ev):
self.canvas.scale(self.tag, self.xx, self.yy, self.sD, self.sD)
main = moveScale()
main.mainloop()

Kivy Rectangle source doesn't get updated?

(I already read a lot of other posts about this but they do not seem to help me (or I simply don't understand them) )
I have a function Add() in which another function Grid() gets called that creates a Grid.png file and saves that to my desktop. This Add() function gets called multiple times (through a button) and with that also the Grid() function within. Here is a little code snippet:
Width = 700
Height = 700
def __init__(self,**kwargs):
super(Drw, self).__init__(**kwargs)
self.CellCount = 1
time.sleep(0.5)
self.cellAdd= int(input("\nCells to add: "))
self.bg = ""
with self.canvas:
self.add = Button(text = "add", font_size =40, pos = (700,300))
self.sub = Button(text="sub", font_size=40, pos=(700, 400))
self.add.bind(on_press = self.Add)
self.sub.bind(on_press= self.Sub)
self.add_widget(self.sub)
self.add_widget(self.add)
def Add(self, instance):
self.CellCount += self.cellAdd
Grid(self.CellCount, self.Width, self.Height)
with self.canvas:
self.bg = Rectangle(source= r"C:\Users\Max\Desktop\Grid.png", pos=(0,0), size= (self.Width, self.Height))
self.L = Label(text=str(self.CellCount)+" columns", pos=(500, 300))
What happens is that the first time I press the "Add" button, it does what it should so Add() gets called and in turn Grid() gets called and creates a new image on my desktop. Then the "bg" (background) is created and the image is correctly displayed. This only works 1 time however. After that, when I continue to press Add, nothing happens even though the Grid.png is getting changed on my desktop everytime I press "Add". The image just doesnt get updated somehow. The path always remains the same so I dont understand why it does not change the image to the new one?
I already tried to manually update the source with
self.bg.source = r"C:\Users\Max\Desktop\Grid.png"
but that does not do anything. I am pretty new to Kivy so I apologize if this gets asked alot.
Thank you for reading!
EDIT
I fixed it with this:
def Add(self, instance):
self.CellCount += self.cellAdd
Grid(self.CellCount, self.Width, self.Height)
with self.canvas:
self.canvas.clear()
self.bg =Image(source= r"C:\Users\Max\Desktop\Grid.png", pos=(0,0), size= (self.Width, self.Height))
self.bg.reload()
self.L = Label(text=str(self.CellCount)+" columns", pos=(500, 300))
I still dont know why Cache.remove() doesnt work as it seems logical to me but at least .reload() works good enough. Thank you for your answers!
Probably the image source is being cached by Kivy's image loader, so you need to inform it of the update. Try from kivy.cache import Cache and Cache.remove("kv.texture", your_filename) (or omit the your_filename argument to clear the whole texture cache).

Pack a text widget on the side doesn't work

I'm kind of new to tkinter. I tried to create a Text widget on the left side at 0,0, but it appears in the middle, like a default pack().
Here is my code:
from Tkinter import *
# the ui of the main window
class Ui(object):
# the init of the client object
def __init__(self):
self.root = Tk()
self.mid_height = self.root.winfo_screenheight() / 2
self.mid_width = self.root.winfo_screenwidth() / 2
self.root.title("Journey-opening")
self.root.geometry("600x600+{}+{}".format(self.mid_width - 300, self.mid_height - 300))
self.root.resizable(width=0, height=0)
self.cyan = "#0990CB"
self.root["background"] = self.cyan
self.frame = Frame(self.root)
self.frame.pack()
self.chat_box = Text(self.frame, height=30, width=50)
self.chat_box.pack(side=LEFT)
def open(self):
self.root.mainloop()
wins = Ui()
wins.open()
I also tried with grid method but it did not change anything, and also created another widget because maybe it needs 2 widgets at least.
I guess its something with my frame but I follow a tutorial and everything seems fine.
"Pack a text widget on the side doesn't work"
That is incorrect the line self.chat_box.pack(side=LEFT) does pack the Text widget to side. It's just that it is done inside self.frame which allocates exactly as much space needed for the widgets it encapsulates(in this case that is only the text widget) by default. So in a way, the Text widget is packed, not just to left, but to all sides.
In order to have self.chat_box on the upper left corner, you should let frame to occupy more space than needed, in this case, it can simply occupy all space in the x-axis inside its parent(self.root). In order to do that, replace:
self.frame.pack()
with:
self.frame.pack(fill='x') # which is the same as self.frame.pack(fill=X)

How do I create a Tiling layout / Flow layout in TkInter?

I want to to fill my window with, say, labels and I want them to wrap once the column would be bigger than the current window (or rather parent frame) size.
I've tried using the grid layout, but then I have to calculate the size of the content of each row myself, to know when to put the next element in the next row.
The reason I ask, is because I want to create some sort of tiled file icons.
Or asked differently, is there something like Swing's FlowLayout for TkInter?
What I do when I want something like this is use the text widget for a container. The text widget can have embedded widgets, and they wrap just like text. As long as your widgets are all the same height the effect is pretty nice.
For example (cut and pasted from the question at the author's request):
textwidget = tk.Text(master)
textwidget.pack(side=tk.LEFT, fill=tk.BOTH)
for f in os.listdir('/tmp'):
textwidget.window_create(tk.INSERT, window=tk.Label(textwidget, text=f))
Here is a way to make flow behavior inside a frame.
I wrote a function that will do this. Basically you pass a frame to the function (not root or top level) and the function will look at all the children of the frame, go through them measure their sizes and place them in the frame.
Here is the placement procedure
Place the first widget, and move x over an amount equal to its width.
Measure the next widget.
If placing the next widget would cause it to goes past the frame width, bump its x value to 0 and bump it down a y value equal to the largest widget in the current row (start a new row).
Reset the value of the largest widget since you are starting a new row.
Keep repeating until all widgets are placed.
Bind that procedure to the resizing of the frame event.
I used 3 functions to make this work:
The function that runs the procedure.
The function that binds the resizing of the frame to the function.
The function that unbinds the resizing of the frame.
Here are the functions:
from tkinter import *
def _reorganizeWidgetsWithPlace(frame):
widgetsFrame = frame
widgetDictionary = widgetsFrame.children
widgetKeys = [] # keys in key value pairs of the childwidgets
for key in widgetDictionary:
widgetKeys.append(key)
# initialization/priming loop
width = 0
i = 0
x = 0
y = 0
height = 0
maxheight = 0
# loop/algorithm for sorting
while i < len(widgetDictionary):
height = widgetDictionary[widgetKeys[i]].winfo_height()
if height > maxheight:
maxheight = height
width = width + widgetDictionary[widgetKeys[i]].winfo_width()
# always place first widget at 0,0
if i == 0:
x = 0
y = 0
width = widgetDictionary[widgetKeys[i]].winfo_width()
# if after adding width, this exceeds the frame width, bump
# widget down. Use maximimum height so far to bump down
# set x at 0 and start over with new row, reset maxheight
elif width > widgetsFrame.winfo_width():
y = y + maxheight
x = 0
width = widgetDictionary[widgetKeys[i]].winfo_width()
maxheight = height
# if after adding width, the widget row length does not exceed
# frame with, add the widget at the start of last widget's
# x value
else:
x = width-widgetDictionary[widgetKeys[i]].winfo_width()
# place the widget at the determined x value
widgetDictionary[widgetKeys[i]].place(x=x, y=y)
i += 1
widgetsFrame.update()
def organizeWidgetsWithPlace(frame):
_reorganizeWidgetsWithPlace(frame)
frame.bind("<Configure>", lambda event: _reorganizeWidgetsWithPlace(frame))
_reorganizeWidgetsWithPlace(frame)
def stopOrganizingWidgetsWithPlace(frame):
frame.unbind("<Configure>")
And here is an example of them in use:
def main():
root = Tk()
root.geometry("250x250")
myframe = Frame(root)
# make sure frame expands to fill parent window
myframe.pack(fill="both", expand=1)
buttonOrganize = Button(myframe, text='start organizing',
command=lambda: organizeWidgetsWithPlace(myframe))
buttonOrganize.pack()
buttonStopOrganize = Button(myframe, text='stop organizing',
command=lambda: stopOrganizingWidgetsWithPlace(myframe))
buttonStopOrganize.pack()
##### a bunch of widgets #####
button = Button(myframe, text="---a random Button---")
canvas = Canvas(myframe, width=80, height=20, bg="orange")
checkbutton = Checkbutton(myframe, text="---checkbutton----")
entry = Entry(myframe, text="entry")
label = Label(myframe, text="Label", height=4, width=20)
listbox = Listbox(myframe, height=3, width=20)
message = Message(myframe, text="hello from Message")
radioButton = Radiobutton(myframe, text="radio button")
scale_widget = Scale(myframe, from_=0, to=100, orient=HORIZONTAL)
scrollbar = Scrollbar(myframe)
textbox = Text(myframe, width=3, height=2)
textbox.insert(END, "Text Widget")
spinbox = Spinbox(myframe, from_=0, to=10)
root.mainloop()
main()
Notice:
That you do not need to grid, pack or place them. As long as you specify the frame, that will all be done at once when the function is called. So that is very convenient. And it can be annoying if you grid a widget, then try to pack another, then try to place another and you get that error that you can only use one geometry manager. I believe this will simply overwrite the previous choices and place them. I believe you can just drop this function in and it will take over management. So far that has always worked for me, but I think you should really not try to mix and match geometry managers.
Notice that initially the buttons are packed, but after pressing the button, they are placed.
I have added the "WithPlace" naming to the functions because I have a similar set of functions that do something very similar with the grid manager.

Categories

Resources