I am currently learning about discord.ui buttons and i want them to be subclassed since I find them to static otherwise.
I have come a long way of figuring it out but I can't quiet get the view to reset correctly (I have commented the code so you will see where the problem lies).
This is the code of the ButtonCreation subclass with Button from discord.ui import (this is the old code (I leave it as reference))
class ButtonCreation(Button):
def __init__(self, label, style, emoji):
super().__init__(label=label, style=style, emoji=emoji)
async def callback(self, interaction):
# define view again... this will make it empty. Some way to pass on the content of original view?
# or maybe subclassing View instead of Button
view = discord.ui.View()
# checking if current pressed buttons style equals "success"/green
if self.style == discord.ButtonStyle.success:
# change the style to gray
self.style = discord.ButtonStyle.gray
view.add_item(self)
# edit the original message with new view
await interaction.response.edit_message(view=view)
else:
await interaction.response.send_message('This Button has the style: ' + str(self.style))
This is the command cog where the button gets created by passing the arguments.
(arguments were changed to how the new version needs them to be)
#client.command()
async def myroles(self, ctx):
# Buttons being created
button1 = ButtonCreation(ctx=ctx, label='TSLinked', style=discord.ButtonStyle.green, custom_id="tslinked", emoji=None, row=0)
button2 = ButtonCreation(ctx=ctx, label='TSLinked', style=discord.ButtonStyle.gray, custom_id="tsadmin", emoji=None, row=1)
button3 = ButtonCreation(ctx=ctx, label='TSLinked', style=discord.ButtonStyle.danger, custom_id="tsserveradmin", emoji=None, row=2)
# defining view plus adding buttons into view
view = View()
view.add_item(button1)
view.add_item(button2)
view.add_item(button3)
# send view with text above
await ctx.channel.send('Test!', view=view)
There is actually everything working except that I can't get the original view over to the callback to get all of the original buttons.
So the current flow is that all 3 buttons are shown with their according colour and when you press button1 (since green == "ButtonStyle.success") the button colour is changed to gray and gets put into the view which afterwards gets send to the channel.
What should happen: After the green button being pressed the buttons should stay the same except the green button which should switch to gray.
Thanks in advance to anyone willing to lend a hand.
Solution
# ButtonCreation subclass with Button from discord.ui import
class ButtonCreation(Button):
# init needs ctx and at best custom_id or row to do all you need
def __init__(self, ctx, label, style, emoji, custom_id, row):
super().__init__(label=label, style=style, custom_id=custom_id, emoji=emoji, row=row)
self.ctx = ctx
async def callback(self, interaction):
# checks if Button style is green
if self.style == discord.ButtonStyle.green:
# changes button to grey
self.style = discord.ButtonStyle.grey
# self.ctx can interact with channel and user just like ctx would in normal commands
await self.ctx.channel.send('test')
# remove your button (self) before adding it again since you can't "replace" it just like that
self.view.remove_item(self)
# add the Button (self) to the view by using self.view.add_item.
self.view.add_item(self)
# add the view to the edit or send_message method with self.view
await interaction.response.edit_message(view=self.view)
If you need a little clarification:
self is the Button thats how you call the arguments.
the Button also has "View" as object so you can actually get all the buttons that are saved in it.
so before adding the button to the view (self.view) you can change the values of the button and add them again.
Things like row are fixed for the single purpose of the buttons actually needing to stay in the same position, otherwise they would be added behind all others.
Really hope this helps others aswell.
Related
i have a folderscreen that displays buttons from a loop, the buttons have title of the files saved, when i click the buttons, it opens a new page that shows the content of the file, but when i go back to the folder screen, 2 sets of the button in the loop are added to the existing buttons.
what i want is everytime i leave the folderscreen, i want the buttons cleared, so that when i go back, it will run the loop code again and shows only the buttons from the loop without repetition.
or may be there is a way i can stop the loop code from running if the boxlayout that house the buttons is not empty. here is my code:
def on_enter(self, *args):
objf = mm.Storenote()
objf.tif(mm.DB.hod)
for i in range(len(mm.Storenote.ht)):
self.b = Button(text= (mm.Storenote.ht[i]), font_size = "25sp")
#self.b.background_normal = ""
self.b.background_color = 0,0,1,1
self.b.ids = {"id":mm.Storenote.nid[i]}
self.b.size_hint=(1, None)
self.b.size = (dp(370), dp(50))
self.b.bind(on_press=self.build_clickk(self.b))
self.ids.lpl.add_widget(self.b)
#self.ids.lpl.add_widget(self.l[i])
def build_clickk(self, b):
def clickk(*args):
ci = b.ids.id
print(ci)
objff = mm.Show()
objff.getter(ci)
self.manager.current = "Readpage"
return clickk
def on_leave(self, *args):
self.ids.lpl.remove_widget(self.b)
the def on_leave function only remove one of the buttons and add 2 new sets of the button each time i go back to the folderscreen
You can use self.ids.lpl.clear_widgets() right before your for-loop. This way you ensure that the layout is empty before adding new widgets.
If you add many widgets, try using def on_pre_enter(). kivy will render the widgets before entering the screen and this may prevent "flickering" when building all widgets.
So I've given myself a little project and I'm trying to make a little tool to connect to the OKEX exchange. Now I'm currently working on the GUI and I've decided to use Tkinter. After lots of research and what-not, I've come up with the following, but now I've become a bit stuck.
I've got 2 classes, one for the main window and one for the login window. However, some of the functions of the main window rely on what happens after I've submitted the login details. Now I know that Toplevel is for creating additional windows in Tkinter, and you normally close these windows with .destroy() , and if I want to pick up on this event in the main window class then I need to use the Toplevel.protocol("WM_DELETE_WINDOW", function_name) call ...but this isn't working for me.
It works as expected if I close using the cross in the top right, but not if I close with my function that calls .destroy() , can anyone explain to me why this isn't working as intended? Perhaps I have missed something ?
I want to change the text in the first frame to "Logged In" after the user (me) enters in their details, but I need to login first and pass through a user object to contain those details.
Anyways, here's the code, please help me out!
The code in question is the myquit function in the LoginBox class , and the goToLogin function in the MainBox class :)
from tkinter import *
from tkinter.ttk import *
class LoginBox:
def __init__(self, master): # Master is the frame that is passed in.
# Create Frame
self.master = master
self.master.title('~ Please Login ~')
def login_function(self):
user = "xxx"
secret_key = "yyy"
print("User - API Key: " + user)
print("User - Secret Key: " + secret_key)
# Perhaps check the login ...
# if it's a success then quit the function
self.myquit()
def myquit(self):
self.master.destroy()
class MainBox:
def __init__(self, master):
# Set the root window
self.master = master
self.master.geometry("500x500")
self.master.title("OkBot v0.1")
self.master.resizable(False, False)
# Initialize the frames
self.uiFrame1 = Frame(self.master) # The Top Layer -- Login Text + Login Button
# uiFrame1 Initialize --Login Text + Login Button
self.ui1_button = Button(self.uiFrame1, text="Login", command=self.goToLogin).grid(row=0, column=3, sticky=E, padx=1)
# Create Topview for popup , pass in User Object to LoginBox
def goToLogin(self):
loginMaster = Toplevel(self.master)
loginMaster.protocol("WM_DELETE_WINDOW", self.checkLogin) # This is if they close via X
loginGUI = LoginBox(loginMaster)
def checkLogin(self):
print("This function was called -- The Protocol for destroyed windows works")
# Initialize the objects and start the program
mainWindow = Tk()
myProgram = MainBox(mainWindow)
mainWindow.mainloop()
It works as expected if I close using the cross in the top right, but not if I close with my function that calls .destroy() , can anyone explain to me why this isn't working as intended? Perhaps I have missed something ?
loginMaster.protocol("WM_DELETE_WINDOW", self.checkLogin) only tells tkinter what to do when the window manager destroys the window. When you call some function which calls destroy(), that doesn't involve the window manager and therefore your callback is not called.
If you want something to happen when the window is destroyed, you should bind to the <Destroy> event.
I would like to repeatedly get the contents of a Text widget, so I can analyse it and gets stats about what's been entered. These stats would need to be updated in real time as the user types, hence why I need the variable currentContent to update every loop. What I'd like to do is something like this.
main = tk.Tk()
# Y'know, all the typical window setup stuff.
currentContent = inputBox.get(0.0,END)
textBlobContent = TextBlob(currentContent)
# Basically here I'd do a bunch of stuff using TextBlob.
main.mainloop()
However, that doesn't work. It gets the content once, as soon as the window loads, and then stops. Surely mainloop runs repeatedly, and it should keep getting the contents of the Text widget?
A simple solution that works most of the time would be to put a binding on <KeyRelease>. That will enable a function to be called whenever the user is typing. This won't trigger the callback whenever data is pasted with the mouse, or inserted via other means (such as a toolbar button).
A more robust solution is to set up a proxy for the widget, so that an event is generated whenever anything is inserted or deleted in the widget. This proxy can look at what is being done with the widget (insert, delete, selection changed, etc) and generate an event. You can then bind to this event to do whatever you want.
Here's an example of a custom text class that generates a <<TextModified>> event whenever data is inserted or deleted:
import tkinter as tk
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
"""A text widget that report on internal widget commands"""
tk.Text.__init__(self, *args, **kwargs)
# create a proxy for the underlying widget
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._proxy)
def _proxy(self, command, *args):
cmd = (self._orig, command) + args
result = self.tk.call(cmd)
if command in ("insert", "delete", "replace"):
self.event_generate("<<TextModified>>")
return result
This proxy does four things:
First, it calls the actual widget command, passing in all of the arguments it received.
Next it generates an event for every insert and every delete
Then it then generates a virtual event
And finally it returns the results of the actual widget command
You can use this widget exactly like any other Text widget, with the added benefit that you can bind to <<TextModified>>.
For example, if you wanted to display the number of characters in the text widget you could do something like this:
import tkinter as tk
# ... import of definition of CustomText goes here ...
root = tk.Tk()
label = tk.Label(root, anchor="w")
text = CustomText(root, width=40, height=4)
label.pack(side="bottom", fill="x")
text.pack(side="top", fill="both", expand=True)
def onModification(event):
chars = len(event.widget.get("1.0", "end-1c"))
label.configure(text="%s chars" % chars)
text.bind("<<TextModified>>", onModification)
root.mainloop()
In my program I just need to notify user to not press a physical button om a system with no keyboad or mouse,
want to popup a Wait message that disapears when the system is again ready
There are two reasons you don't want a message box here.
First, the whole point of a message box is that it's a modal dialog with some standardized buttons, and you don't want those buttons.
Second, the whole point of a modal dialog is that it's modal—it runs its own event loop, and doesn't return until the dialog is dismissed. This means (unless you're using background threads) your app can't do anything while displaying it.
The first problem is easy to solve. tkMessageBox is just a simple wrapper around tkCommonDialog.Dialog. It's worth looking at the source to see just how simple it is to construct a dialog box that does what you want. But tkSimpleDialog.Dialog is even simpler than tkCommonDialog (hence the name). For example:
class WaitDialog(tkSimpleDialog.Dialog):
def __init__(self, parent, title, message):
self.message = message
Dialog.__init__(self, parent, title=title, message=message)
def body(self, master):
Label(self, text=self.message).pack()
def buttonbox(self):
pass
def wait(message):
WaitDialog(root, title='Wait', message=message)
That's all it takes to create a modal dialog with no buttons. Dialog Windows and the source to tkSimpleDialog have more details.
The second problem is even easier to solve: If you don't want a modal dialog, then all you want is a plain old Toplevel. You may want it to be transient, so it stays on top of the master, hides with it, doesn't show up on the taskbar, etc., and you may want to configure all kinds of other things. But basically, it's this simple:
def wait(message):
win = Toplevel(root)
win.transient()
win.title('Wait')
Label(win, text=message).pack()
return win
Now you can call wait() and continue to run:
def wait_a_sec():
win = wait('Just one second...')
root.after(1000, win.destroy)
root = Tk()
button = Button(root, text='do something', command=wait_a_sec)
root.mainloop()
When opening a new tkinter window, I only want the user to be able to click buttons on the new window. They should not be able to click on buttons from other windows that are part of the application. How would I accomplish this?
Here is a snip of my code:
def exportEFS(self):
self.exportGUI = Toplevel()
Button(self.exportGUI, text='Backup', command=self.backup).pack(padx=100,pady=5)
Button(self.exportGUI, text='Restore', command=self.restore).pack(padx=100,pady=5)
def backup(self):
self.backupWindow = Toplevel()
message = "Enter a name for your Backup."
Label(self.backupWindow, text=message).pack()
self.entry = Entry(self.backupWindow,text="enter your choice")
self.entry.pack(side=TOP,padx=10,pady=12)
self.button = Button(self.backupWindow, text="Backup",command=self.backupCallBack)
self.button.pack(side=BOTTOM,padx=10,pady=10)
In this snip, once the backupWindow is opened, the exportGUI remains open, but the user should not be able to click "Backup" or "Restore" while the backupWindow is opened.
Thanks!
You will want to call grab_set on the TopLevel window so that all keyboard and mouse events are sent to that.
def exportEFS(self):
self.exportGUI = Toplevel()
Button(self.exportGUI, text='Backup', command=self.backup).pack(padx=100,pady=5)
Button(self.exportGUI, text='Restore', command=self.restore).pack(padx=100,pady=5)
def backup(self):
self.backupWindow = Toplevel()
self.backupWindow.grab_set()
message = "Enter a name for your Backup."
Label(self.backupWindow, text=message).pack()
self.entry = Entry(self.backupWindow,text="enter your choice")
self.entry.pack(side=TOP,padx=10,pady=12)
self.button = Button(self.backupWindow, text="Backup",command=self.backupCallBack)
self.button.pack(side=BOTTOM,padx=10,pady=10)
What you can do is set the state to disabled. As so:
self.button.config(state="disabled")
And to enable it, you just use:
self.button.config(state="normal")
However, you must assign your buttons to variables first, like this:
self.backup=Button(self.exportGUI, text='Backup', command=self.backup)
self.backup.pack(padx=100,pady=5)
self.restore=Button(self.exportGUI, text='Restore', command=self.restore)
self.restore.pack(padx=100,pady=5)
so you would disable these using:
self.backup.config(state="disabled")
self.restore.config(state="disabled")
and re-enable using:
self.backup.config(state="normal")
self.restore.config(state="normal")
Please note however, that while the button is disabled, nothing can be changed to that button, both through the code, or through the user using it. So that means if you wanted to change the text of that button, you would have to change the state of the button to "normal" before changing it (if it already isn't in that state, which by default, all widgets are in that state when first created).
Cheers :)