Splitting tkinter canvas and frame apart - python-3.x

Got a tkinter frame on the left being used for labels, checkbuttons, etc. On the right is a canvas displaying a map. I can scroll over the map and it will give the longitude/latitude coordinates of where the mouse pointer is located on the map at the time in question. I can click on the map and it will zoom in on the map. The problem is when I'm on the frame where I want to display underlying map data as I scroll the mouse across the frame the longitude/latitude changes, even though I'm not on the canvas. If I click on the frame, haven't put any checkbuttons on there yet to test it that way, it zooms right in just like it would over on the canvas.
Is there any way to split apart the action 'sensing' of the frame and canvas to keep them separate.
I would post up the code, a bit lengthy, but I got get out of here as I'm already running late.
Edit:
I'm back and thanks to Bryan's reply I think I understand what he was saying to do, just not sure how to do it. In a couple of attempts nothing seemed to work. Granted I'm still not fully sure of the (self,parent) 'addressing' method in the code below.
Also I see high probability coming up in the not to distant future of needing to be able to reference the mouse button to both t he canvas and the frame separately, aka have it do different things depending on where I have clicked on. Fortunately with the delay thanks to having to get out of here earlier and with Bryan's answer I have been able to shorten the code down even more and now have code that is doing exactly what I'm talking about. The delay in posting code worked to my benefit.
import tkinter as tk
from tkinter import *
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.frame = tk.Frame(self,bg='black', width=1366, height=714)
self.frame1 = tk.Frame(self,bg='gray', width=652, height=714)
self.frame.pack()
self.canvas = tk.Canvas(self, background="black", width=714, height=714)
self.canvas.pack_propagate(0)
self.canvas.place(x=652,y=0)
self.frame1.pack_propagate(0)
self.frame1.place(x=0,y=0)
self.longitudecenter = -95.9477127
self.latitudecenter = 36.989772
self.p = 57.935628
global v
s = Canvas(self, width=150, height=20)
s.pack_propagate(0)
s.place(x=0,y=695)
v = Label(s, bg='gray',fg='black',borderwidth=0,anchor='w')
v.pack()
parent.bind("<Motion>", self.on_motion)
self.canvas.focus_set()
self.canvas.configure(xscrollincrement=1, yscrollincrement=1)
def on_motion(self, event):
self.canvas.delete("sx")
self.startx, self.starty = self.canvas.canvasx(event.x),self.canvas.canvasy(event.y)
px = -(round((-self.longitudecenter + (self.p/2))- (self.startx * (self.p/714)),5))
py = round((self.latitudecenter + (self.p/2))-(self.starty * (self.p /714)),5)
v.config(text = "Longitude: " + str(px))
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
This is part of what I've been using. How do I change it so I can bind to to the frame and to the canvas separately. Right now I only need, with the case of the mouse position, to be able to bind to the canvas, but in the future I will need to be able to use mouse clicks, maybe even mouse position separately on the canvas and frame.(who knows given how much this project has changed/advanced since I started it three weeks ago...the sky is the limit).

If you want a binding to only fire for a specific widget, but the binding on that widget rather than on a containing widget.
Change this:
parent.bind("<Motion>", self.on_motion)
To this:
self.canvas.bind("<Motion>", self.on_motion)

Related

Is it possible to make Tkinter scrollbars move independently of each other in different Toplevel windows?

Imagine there are two Tkinter.Toplevel() windows, called Window_1 and Window_2, which can be opened by clicking the same button (lets called Button_0).
Button_0 is pressed and Window_1 pops up. In Window_1, I can scroll up and down using a mouse pad (MAC OS). After that, I left Window_1 open.
Button_0 is pressed again and Window_2 pops up, while Window_1 stays open. In Window_2, I can again scroll up and down.
Now, I go back to Window_1 and try to scroll using mouse pad, contents in Window_1 DO NOT MOVE, but contents in Window_2 DO MOVE.
Then I close Window_2, and try to scroll on Window_1, this time I got error messages asking for a canvas on Window_2.
I did bind function,
def on_vertical(canvas,event):
canvas.yview_scroll(-3 * event.delta, 'units')
to a canvas inside each windows. As far as I know about the error, it seems that this function could not be used twice at the same time (both windows are opened).
I would like the way that when both Windows stay open. While on each window, I can scroll up-down while the another one do not move. Is it possible to code that?
This is the code example (please do noted that the Window name is not corrected label.)
from tkinter import *
######################## FUNCTIONS (DEF) ########################
def on_vertical(canvas,event):
canvas.yview_scroll(-3 * event.delta, 'units')
######################## FUNCTIONS (CLASS) ########################
class Window(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.master = master
self.init_window()
#INITIAL WINDOW
def init_window(self):
self.master.title("Main Window")
self.pack(fill=BOTH, expand=1)
Button(self, text="Button_0",command = self.load_and_print).place(x = 7, y = 95)
# creating a button instance
Button(self, text="EXIT PROGRAM", command=self.client_exit).place(x=500, y=250)
#OPEN A NEW WINDOW CONTAINING STOCK LISTS
def load_and_print(self):
new_window = Toplevel(self)
new_window.title("Window")
canvas = Canvas(new_window, width = 800, height = 500, scrollregion = (0, 0, 0, 2500))
frame = Frame(canvas)
vbar = Scrollbar(new_window, orient = VERTICAL, command = canvas.yview)
vbar.pack(side = RIGHT,fill = Y)
canvas.create_window(0,0, window = frame, anchor = NW)
canvas.config(yscrollcommand = vbar.set)
canvas.pack(side = TOP,expand = True,fill = BOTH)
canvas.bind_all('<MouseWheel>', lambda event, canvas=canvas: on_vertical(canvas,event))
#MAKE PROGRAM EXIT
def client_exit(self):
exit()
######################## MAIN PROGRAMME ########################
#call window
root = Tk()
#size of the window
root.geometry("700x300")
app = Window(root)
root.mainloop()
root.update()
The problem is that you are using bind_all instead of bind for the mousewheel event.
Because you're using bind_all, each time you create a new window it replaces the old binding with a new binding. No matter which window gets the event, your function will always only work for the last window to be created. And, of course, when that window is destroyed then the mouse binding will throw an error since the canvas no longer exists.
Using bind
One solution is simple: use bind instead of bind_all.
canvas.bind_all('<MouseWheel>', lambda event, canvas=canvas: on_vertical(canvas,event))
Using bind_all
If you want the benefits of bind_all -- namely, that the scrolling works even if the mouse is over some other widget, you need to modify on_vertical to figure out which canvas to scroll at the time that it is running instead of having the canvas being passed in.
You can do that with a little bit of introspection. For example, the event object knows which widget received the event. From that you can figure out which window the mouse is in, and from that you can figure out which canvas to scroll.
For example, move the binding up to the __init__ and change it like this:
self.bind_all('<MouseWheel>', on_vertical)
Next, change on_vertical to figure out which canvas to scroll. In the following example I assume each toplevel has exactly one canvas and that you always want to scroll that canvas (ie: you lose the ability to scroll text widgets and listboxes)
If that's not the case, you can add whatever logic you want to figure out which widget to scroll.
def on_vertical(event):
top = event.widget.winfo_toplevel()
for child in top.winfo_children():
if child.winfo_class() == "Canvas":
child.yview_scroll(-3 * event.delta, 'units')
break

PyQt QScrollArea doesn't display widgets

I am somewhat new to GUI programming and very new to PyQt, and I'm trying to build a GUI that displays a list of questions. I have created a QuestionBank class that subclasses QWidget and overrides the .show() method to display the list properly. I have tested this alone and it works correctly. However, the list of questions can be quite long, so I've been trying to make it scrollable. Rather than add a QScrollBar to the widget and then set up the event triggers by hand, I've been trying to my QuestionBank widget in a QScrollArea based on the syntax I've seen in examples online. While the scroll area shows up fine, it does not at all display the question bank but rather just shows a blank outline.
The QuestionBank class looks like this:
class QuestionBank(QWidget):
BUFFER = 10 # space between questions (can be modified)
def __init__(self, parent, questions):
# `parent` should be the QWidget that contains the QuestionBank, or None if
# QuestionBank is top level
# `questions` should be a list of MasterQuestion objects (not widgets)
QWidget.__init__(self, parent)
self.questions = [MasterQuestionWidget(self, q) for q in questions]
self.bottomEdge = 0
def show(self, y=BUFFER):
QWidget.show(self)
for q in self.questions:
# coordinates for each each question
q.move(QuestionBank.BUFFER, y)
q.show()
# update y-coordinate so that questions don't overlap
y += q.frameGeometry().height() + QuestionBank.BUFFER
self.bottomEdge = y + 3 * QuestionBank.BUFFER
# ... other methods down here
My code for showing the scroll bar looks like this:
app = QApplication(sys.argv)
frame = QScrollArea()
qs = QuestionBank(None, QFileManager.importQuestions())
qs.resize(350, 700)
frame.setGeometry(0, 0, 350, 300)
frame.setWidget(qs)
frame.show()
sys.exit(app.exec_())
I have tried many variants of this, including calling resize on frame instead of qs, getting rid of setGeometry, and setting the parent of qs to frame instead of None and I have no idea where I'm going wrong.
If it helps, I'm using PyQt5
Here is the question bank without the scroll area, to see what it is supposed to look like:
Here is the output of the code above with the scroll area:
This variation on the code is the only one that produces any output whatsoever, the rest just have blank windows. I'm convinced its something simple I'm missing, as the frame is obviously resizing correctly and it obviously knows what widget to display but its not showing the whole thing.
Any help is much appreciated, thank you in advance.

Hiding/displaying a canvas

How do you hide a canvas so it only shows when you want it displayed?
self.canvas.config(state='hidden')
just gives the error saying you can only use 'disabled' or 'normal'
In the comments you say you are using pack. In that case, you can make it hidden by using pack_forget.
import tkinter as tk
def show():
canvas.pack()
def hide():
canvas.pack_forget()
root = tk.Tk()
root.geometry("400x400")
show_button = tk.Button(root, text="show", command=show)
hide_button = tk.Button(root, text="hide", command=hide)
canvas = tk.Canvas(root, background="pink")
show_button.pack(side="top")
hide_button.pack(side="top")
canvas.pack(side="top")
root.mainloop()
However, it's usually better to use grid in such a case. pack_forget() doesn't remember where the widget was, so the next time you call pack the widget may end up in a different place. To see an example, move canvas.pack(side="top") up two lines, before show_button.pack(side="top")
grid, on the other hand, has a grid_remove method which will remember all of the settings, so that a subsequent call to grid() with no options will put the widget back in the exact same spot.

How to make a frame (like a data entry form or a status bar) always appear on top of other tkinter frames in Python 3?

So, I have a small Python 3 project that should have a window composed of 3 main areas:
Tool bar (top);
Table/treeview with database data (center) - this table should be able to grow downwards, with scrollbars, but I still do not know how to make them work with treeview;
Status bar (bottom).
I am also trying to add a quick data entry form that should apear on user demand right above the status bar. I think I have been able to create the main structure for this window, using pack(). The data entry form also shows up and goes away when the user clicks the proper buttons. But there are a couple of things that I don't know how to solve. The first one is that when the user resizes the window, making it smaller, the status bar and the data entry form both disappear under the table/treeview. How can I make them be always on top?
Here is my code:
#!/usr/local/bin/python3
# encoding: utf-8
from tkinter import *
from tkinter import ttk
entryform_visible = 0
class mainWindow:
def __init__(self, master):
self.master = master
master.title("AppTittle")
master.minsize(width=700, height=200)
def show_entryform():
global entryform_visible
if entryform_visible == 1:
hide_entryform()
return
else:
entryform_visible = 1
# Form for data entry (bottom of main window)
status_txt.set("Introducing new data...")
self.bottomframe.pack(side=BOTTOM, fill=X)
def hide_entryform():
global entryform_visible
self.bottomframe.pack_forget()
entryform_visible = 0
status_txt.set("Introduction of data was cancelled.")
def add_register():
hide_entryform()
status_txt.set("New register added!")
# Tool bar (buttons)
self.topframe = ttk.Frame(root, padding="3 3 12 12")
self.topframe.pack(side=TOP, fill=X)
btn_add = ttk.Button(self.topframe, text="Button1").pack(side=LEFT)
btn_add = ttk.Button(self.topframe, text="+", width=2, command=show_entryform).pack(side=RIGHT)
btn_add = ttk.Button(self.topframe, text="Button2").pack(side=RIGHT)
# Data table (center area of window)
self.mainframe = ttk.Frame(root)
self.mainframe.pack(side=TOP, fill=X)
tree = ttk.Treeview(self.mainframe, height=25, selectmode='extended')
tree['columns'] = ('id', 'name', 'descr')
tree.pack(side=TOP, fill=BOTH)
tree.column('#0', anchor=W, minwidth=0, stretch=0, width=0)
tree.column('id', anchor=W, minwidth=30, stretch=0, width=40)
tree.column('name', minwidth=30, stretch=1, width=30)
tree.column('descr', minwidth=100, stretch=1, width=200)
tree.heading('id', text="ID")
tree.heading('name', text="Name")
tree.heading('descr', text="Description")
# Data entry form
self.bottomframe = ttk.Frame(root, padding="3 3 12 12")
ttk.Label(self.bottomframe, text="Field 1").pack(side=LEFT)
text_input_obj = ttk.Entry(self.bottomframe, width=13)
text_input_obj.pack(side=LEFT)
text_input_obj.focus_set()
btn_add = ttk.Button(self.bottomframe, text="Add", command=add_register).pack(side=RIGHT)
# We are going to pack() this later (when the user clicks the '+' button)
# Status bar
self.statusframe = ttk.Frame(root, padding="2 2 2 2")
self.statusframe.pack(side=BOTTOM, fill=X)
status_txt = StringVar()
self.statusbar = ttk.Label(self.statusframe, textvariable=status_txt).pack(side=LEFT)
root = Tk()
appwindow = mainWindow(root)
root.mainloop()
The first one is that when the user resizes the window, making it
smaller, the status bar and the data entry form both disappear under
the table/treeview. How can I make them be always on top?
This has to do with how the packer works. It will start chopping off widgets when the space available is less than the space needed. It does this in the reverse order of how widgets were packed.
In this case, because the bottom frame was packed last, when you shrink the window to be too small to fit everything, pack first shrinks any windows that might have expanded to larger than their default size, and then starts chopping off widgets starting with the last widget that was packed. In this case it is self.statusframe that gets chopped since it was the last to be packed.
There are two ways to solve this. One is to simply reverse the order that you pack everything into the root window. The other is to initially make the tree widget very small, and then let it grow to fill the space. Since the natural size of the treeview is small, tkinter will shrink it before it starts removing widgets from view. If you do this second technique, you can use wm_geometry to make the window large when it starts out (otherwise the tree won't have any space to grow into)
My advice is to always group your pack and grid statements. It makes changes like this very easy, and it make visualizing your code much easier. For example, using the first technique I would move the packing of the main areas to the bottom of the function:
# layout the main GUI
self.topframe.pack(side=TOP, fill=X)
self.statusframe.pack(side=BOTTOM, fill=X)
self.mainframe.pack(side=TOP, fill=BOTH, expand=True)
With this, when space gets tight pack will start to chop off the tree. But since the tree is big, there's plenty to chop before it starts to disappear.
When you do this, you'll expose another problem in your layout, and that is that you don't have the treeview expanding to fill extra space in mainframe. You need to pack it like this:
tree.pack(side=TOP, fill=BOTH, expand=True)
This brings up another problem. Namely, your popup bottomframe may pop up hidden. That is because pack isn't designed to work well in this sort of situation. Again, that is because pack will start to chop off widgets in the reverse order that they were added. Since you're adding it last, it's the first widget to get chopped off. If the user resizes the window to be smaller, packing it at the bottom may cause it to never appear.
A better solution in this specific case is to not use pack for this one widget. Instead, since you want it to "pop up", you can use place. place is perfect for popping up widgets on top of other widgets. The only other thing you need to do is make sure that this frame is higher in the stacking order than the statusbar, which you can do with lift:
def show_entryform():
...
self.bottomframe.place(relx=.5, rely=1, anchor="s", relwidth=1.0)
self.bottomframe.lift()
And, of course, use place_forget instead of pack_forget:
def hide_entryform():
...
self.bottomframe.place_forget()
...

Stopping, restarting and changing variables in a thread from the main program (Python 3.5)

I'm very new to threading am and still trying to get my head around how to code most of it. I am trying to make what is effectively a text editor-type input box and so, like every text editor I know, I need a cursor-bar thing to indicate the location at which the text is being typed to. Thus I also want to be able to flicker/blink the cursor, which i thought would also prove good practice for threading.
I have a class cursor that creates a rectangle on the canvas based on the bounding box of my canvas text, but I then need to change it's location as more characters are typed; stop the thread and instantaneously hide the cursor rectangle when the user clicks outside of the input box; and lastly restart the thread/a loop within the thread (once again, sharing a variable) - the idea here being that the cursor blinks 250 times and after then, disappears (though not necessary, I thought it would make a good learning exercise).
So assuming that I have captured the events needed to trigger these, what would be the best way to go about them? I have some code, but I really don't think it will work, and just keeps getting messier. My idea being that the blinking method itself was the thread. Would it be better to make the whole class a thread instead? Please don't feel restricted by the ideas in my code and feel free to improve it. I don't think that the stopping is working correctly because every time I alt+tab out of the window (which i have programmed to disengage from the input box) the Python shell and tkinter GUI stop responding.
from tkinter import *
import threading, time
class Cursor:
def __init__(self, parent, xy):
self.parent = parent
#xy is a tuple of 4 integers based on a text object's .bbox()
coords = [xy[2]] + list(xy[1:])
self.obj = self.parent.create_rectangle(coords)
self.parent.itemconfig(self.obj, state='hidden')
def blink(self):
blinks = 0
while not self.stop blinks <= 250:
self.parent.itemconfig(self.obj, state='normal')
for i in range(8):
time.sleep(0.1)
if self.stop: break
self.parent.itemconfig(self.obj, state='hidden')
time.sleep(0.2)
blinks += 1
self.parent.itemconfig(self.obj, state='hidden')
def startThread(self):
self.stop = False
self.blinking = threading.Thread(target=self.blink, args=[])
self.blinking.start()
def stopThread(self):
self.stop = True
self.blinking.join()
def adjustPos(self, xy):
#I am not overly sure if this will work because of the thread...
coords = [xy[2]] + list(xy[1:])
self.parent.coords(self.obj, coords)
#Below this comment, I have extracted relevant parts of classes to global
#and therefore, it may not be completely syntactically correct nor
#specifically how I initially wrote the code.
def keyPress(e):
text = canvas.itemcget(textObj, text)
if focused:
if '\\x' not in repr(e.char) and len(e.char)>0:
text += e.char
elif e.keysym == 'BackSpace':
text = text[:-1]
canvas.itemconfig(textObj, text=text)
cursor.adjustPos(canvas.bbox(textObj))
def toggle(e):
if cursor.blinking.isAlive(): #<< I'm not sure if that is right?
cursor.stopThread()
else:
cursor.startThread()
if __name__=="__main__":
root = Tk()
canvas = Canvas(root, width=600, height=400, borderwidth=0, hightlightthickness=0)
canvas.pack()
textObj = canvas.create_text(50, 50, text='', anchor=NW)
root.bind('<Key>', keyPress)
cursor = Cursor(canvas, canvas.bbox(textObj))
#Using left-click event to toggle thread start and stop
root.bind('<ButtonPress-1', toggle)
#Using right-click event to somehow restart thread or set blinks=0
#root.bind('<ButtonPress-3', cursor.dosomething_butimnotsurewhat)
root.mainloop()
If there is a better way to do something written above, please also tell me.
Thanks.

Resources