making a new tk OptionMenu class to add desired formatting - python

So I'm working on a project for myself, I'm not a great programmer or anything, but I was wanting to add a bit of formatting to the OptionMenu class. In typical tk you call optionmenu with a list and it then iterates through that list to make the various .add_commands. i.e.
Button >>
Sword
Dagger
Axe
Great Sword
Great Axe
What I was wanting to do though was add in a visual separator (Not tk's default horizontal rule) that would distinct those items by a 'weapon class' ie.
Button >>
1-Hand --
Sword
Dagger
Axe
2-Hand --
Great Sword
Great Axe
Where the separator, obviously, wasn't a selectable option. So I figured I could write a new class 'myOptionMenu2'. I can generate a widget that appears identical to tk's 'OptionMenu' but I'm not really sure how it's best to implement the drawing of the actual menu itself.
If it inherits tk's (MenuButton) class (like optionmenu does), I assume(?) that I could write a new 'add_separator()' function that would step-on tk's default add-separator function, and i could use that to just insert Label widgets
Or i could just inherit a Frame and cut out all the need to mess with tk's menu/menubutton coding. But I am unsure exactly how tk() implements the 'top-level' frames for menu (with no borders, drop shadow, etc.) I looked in the actual tk.py but I'm still not certain i understand how it accomplishes it.
my overall desire is to invoke it something like this:
baseList = [
[1-hand,[sword,dagger,axe, ...]],
[2-hand,[Great Sword, Great Axe, ...]]
[ ..., [...]] #as needed
]
myOptionMenu2(master, a_parent_IntVar(), baseList)
and then have the class draw the 'button', parse the list and handle clicks.
Any help would be appreciated.

You have very little leeway in what you can do on the menus. Your only options are the ones provided by the tk menu widget. You can't, for example, add a label to the menu. However, you can add a command that does nothing, which can serve as a label.
There's also the problem that an optionmenu isn't really a class under the covers. In tk (which Tkinter is built upon), the option menu is just a helper function that contructs a menubutton and a menu. So, there's simply no way to override the add_separator method.
That being said, it's pretty easy to write your own class that creates the menubutton and associated menu. You can then do whatever the standard widgets allows you to do, such as using non-functioning commands as labels.
Here's a simple example:
class myOptionMenu2(tk.Menubutton):
def __init__(self, parent, var, data):
tk.Menubutton.__init__(self, parent, borderwidth=1, relief="raised",
textvariable=var, indicatoron=True)
self.menu = tk.Menu(self, tearoff=False)
self.configure(menu=self.menu)
default = None
for (category, weapons) in data:
self.menu.add_command(label=category, command=None)
if default is None:
default = weapons[0]
var.set(default)
for weapon in weapons:
self.menu.add_radiobutton(indicatoron=True, value=weapon,
label=weapon, variable=var)
You could use it like this:
baseList = [
["1-Hand", ["sword", "dagger", "axe"]],
["2-Hand", ["Great Sword", "Great Axe"]]
]
weaponVar = tk.StringVar()
om = myOptionMenu2(self, weaponVar, baseList)
You also have the option of making cascade menus. So, the dropdown would have entries for "1-Hand" and "2-Hand", and each of those would have a menu for the various options. You could start with the above code, and instead of making commands you can create cascade items.
Finally, you could do what you suggest in the last part of your question and create your own menu-like object out of a toplevel frame (with the overrideredirect flag set to True). You then have complete control over what the dropdown looks like. You could, for example, embed a canvas and draw whatever you want. This solution would require a fair amount of code, though.

Related

python tkinter - want to call a function when scrollbar clicked in ScrolledText widget

[Edits noted:]
I want to hook into the ScrolledText widget so that when a user clicks anywhere in the scrollbar (or even scrolls it with the mouse wheel, hopefully) I can have it call a callback function where I can
[Add: "set a flag, and then return to let the ScrolledText widget do its thing."]
[Delete: " do something first (like turn off automatic scrolling) before the scrolling action takes place."]
Is this possible?
Thanks
Do you want to do something like turning off automatic scrolling, or is that actually what you want to do?
If you want to turn automatic scrolling on or off, just check the position of the text before inserting text. If it's at the end, add the text and autoscroll. If it's not at the end, add the text but don't scroll. This will work even if they scrolled by some other mechanism such as by using the page up / page down keys.
You can check a couple of different ways. I think the way I've always done it (not at my desktop right now to check, and it's been a few years...) is to call dlineinfo on the last character. If the last character is not visible, this command will return None. You can also use the yview command to see if the viewable range extends to the bottom. It returns two numbers that are a fraction between zero and one, for the first and last visible line.
While the user can't turn auto-scrolling on or off by clicking a button, this is arguably better because it will "just happen" when they scroll back to see something.
Not without reaching inside the ScrolledText to get at the Scrollbar and the Text and hook their bindings.
And, while you can do that, at that point, why even use ScrolledText? The whole point is that it's does the scroll bindings automagically without you having to understand them. If you don't want that, just use a Scrollbar and a Text directly. Tkinter Scrollbar Patterns explains how to do this in detail, but really, if you don't want to do anything unusual, it's just connecting a message from each one to a method on the other.
For example:
from Tkinter import *
def yscroll(*args):
print('yscroll: {}'.format(args))
scrollbar.set(*args)
def yview(*args):
print('view: {}'.format(args))
textbox.yview(*args)
root = Tk()
scrollbar = Scrollbar(root)
scrollbar.pack(side=RIGHT, fill=Y)
textbox = Text(root, yscrollcommand=yscroll)
for i in range(1000):
textbox.insert(END, '{}\n'.format(i))
textbox.pack(side=LEFT, fill=BOTH)
scrollbar.config(command=yview)
mainloop()
If you can't muddle out the details from the (sometimes confusing and incomplete) docs, play around with it. Basically, yview is called whenever the scrollbar is moved, and yscroll is called whenever the view is scrolled. The arguments to yscroll are obvious; those to yview less so, but the docs do explain them pretty well.
Note that, when you've set things up normally, dragging the scrollbar or swiping the trackpad or rolling the mousewheel over the scrollbar sends a yview, which makes our code call textbox.yview, which then sends a yscroll, and that does not cause a new yview (otherwise, there would be an infinite loop). So, you see both methods get called. On the other hand, swiping the trackpad or rolling the mousewheel over the text, or using the keyboard to move off the bottom, sends yscroll, which again does not cause a yview, so in this case you only see one of the two methods.
So, for example, if you change yview to not call textbox.yview, you can drag the scrollbar all you want, but the text view won't move. And if you change yscroll to not call scrollbar.set, you can swipe around the text all you want, but the scrollbar won't move.
If you want a horizontal scrollbar as well, everything is the same except with x in place of y. But ScrolledText doesn't do horizontal scrolling, so I assume you don't want it.
If you really do want to dig into ScrolledText, you can look at the source for your version, which is pretty trivial if you understand the example above. In fact, it's basically just an OO wrapper around the example above.
In at least 2.7 and 3.3, the ScrolledText is itself the Text, and its self.vbar is the Scrollbar. It sets yscrollcommand=self.vbar.set in its superclass initialization, and sets self.vbar['command'] = self.yview after vbar is constructed. And that's it.
So, just remove the explicit scrollbar creation, and access it as textbox.vbar, and the same hooking code as above works the same way:
from Tkinter import *
from ScrolledText import *
def yscroll(*args):
print('yscroll: {}'.format(args))
textbox.vbar.set(*args)
def yview(*args):
print('yview: {}'.format(args))
textbox.yview(*args)
root = Tk()
textbox = ScrolledText(root)
for i in range(1000):
textbox.insert(END, '{}\n'.format(i))
textbox.pack(side=LEFT, fill=BOTH)
textbox['yscrollcommand'] = yscroll
textbox.vbar.config(command=yview)
mainloop()
Just be aware that this (the fact that textbox is a normal Text, and textbox.vbar is its attached Scrollbar) isn't documented anywhere, so it could theoretically change one day.

python tkinter: Custom menu while pulling down

I'm learning creating software with Python and Tkinter. Now I need to change menu items for different conditions, but could not find an easy way to do it. Well, let me try to explain my question clearly using an example:
Like shown in the figure, I have a listbox on the left and a listbox on the right. I also have a menu to move the items around, the commands are "move to right", "move to left" and "exchange". The following conditions are considered:
When I only get items selected in left listbox, I want only the command "move to right" enabled, like shown in the figure.
When I only get items selected in right listbox, I want only the command "move to left" enabled.
When I get items selected in both listboxes, I want all commands enabled.
When I get no item selected, I want all commands disabled.
I know I can get the work done by binding events "ListboxSelect" and "Button-1" to some functions, and then use the functions to configure the menu. But it is really a complex work when I have five listboxes in the actual software. So I am wondering whether there is an easy way to do this, like overloading some functions in tkinter.Menu class (I tried overloading post(), grid(), pack() and place(), none of them works).
Any idea is welcomed.
I think what you want to use is the postcommand to modify the menu as appropriate. If you're going to have multiple listboxes, the simplest solution may be to implement your own class. Here's a rough idea:
class EditMenu(Tkinter.Menu):
def __init__(self, parent, listboxes, **kw):
self.commandhook = kw.get('postcommand', None)
kw['postcommand'] = self.postcommand
super(EditMenu, self).__init__(parent, **kw)
self.listboxes = listboxes
self.add_command(label="Move to right", command=self.move_to_right)
self.add_command(label="Move to left", command=self.move_to_left)
self.add_command(label="Exchange", command=self.exchange)
def postcommand(self):
for i in xrange(3):
# do some checks for each entry
# and set state to either Tkinter.DISABLED or Tkinter.NORMAL
self.entryconfig(i, state=state)
if self.commandhook is not None:
self.commandhook()
# Implement your three functions here
If you start to add more items, probably what you'll want to do is create a class for each menu item. In that class you could put in the logic for enable/disable and the callback function implementation. Comment if you'd like to see an example.

cursor gone in PyQT

I have a window containing multiple QRowWidgets, which are custom widgets defined by me. These QRowWidgets contain QLineEdits and other standard widgets. To show or hide certain parts of a QRowWidget, I overdefined the focusInEvent() methodes of all the widgets within it. It works perfectly, when I click on the QRowWidget, the hidden elements appear.
The weird thing is that the blinking cursor line hovewer doesn't appear in the QLineEdits within the custom widgets. I can select them both by a mouse click or with Tab, and a glow effect indicates that the QLineEdit is selected in it, I can select a text in it, or start typing at any location wherever I clicked, but the cursor never appears and it's quite annoying.
My 1st thought was that it is a bug on Mac, but I have the same experience on SuSe Linux.
I'm using python 2.7 and PyQt4.
This is in the __init__() of the QRowWidget:
for i in self.findChildren(QWidget):
i.focusInEvent = self.focusInEvent
And then this is the own focusInEvent():
def focusInEvent(self, event):
if self.pself.focusedLine:
self.pself.focusedLine.setStyleSheet("color: #666;")
self.pself.focusedLine.desc.hide()
self.pself.focusedLine.closebutton.hide()
self.setStyleSheet("color: #000;")
self.desc.show()
self.closebutton.show()
self.pself.focusedLine = self
I suspect you do not make a call to the original focusInEvent() when you override it. Your function should look something like:
def focusInEvent(self,...):
QParent.focusInEvent(self,...)
# the rest of your code
where QParent is the nearest base class for your widgets is.
Either that, or make sure you call focusInEvent() on your QLineEdit widgets as part of your function.
Given the comments, it sounds like you are dynamically reassigning the focusInEvent function on the insantiatations in your custom widget. I would either make a derived class for each of the widgets you use that just overrides focusInEvent as above, or include a line like
type(self).focusInEvent(self,..)
in you function.

Hyperlink in Tkinter Text widget?

I am re designing a portion of my current software project, and want to use hyperlinks instead of Buttons. I really didn't want to use a Text widget, but that is all I could find when I googled the subject. Anyway, I found an example of this, but keep getting this error:
TclError: bitmap "blue" not defined
When I add this line of code (using the IDLE)
hyperlink = tkHyperlinkManager.HyperlinkManager(text)
The code for the module is located here and the code for the script is located here
Anyone have any ideas?
The part that is giving problems says foreground="blue", which is known as a color in Tkinter, isn't it?
If you don't want to use a text widget, you don't need to. An alternative is to use a label and bind mouse clicks to it. Even though it's a label it still responds to events.
For example:
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
for text in ("link1", "link2", "link3"):
link = tk.Label(text=text, foreground="#0000ff")
link.bind("<1>", lambda event, text=text: self.click_link(event, text))
link.pack()
def click_link(self, event, text):
print("You clicked '%s'" % text)
root = tk.Tk()
app = App(root)
root.mainloop()
If you want, you can get fancy and add additional bindings for <Enter> and <Leave> events so you can alter the look when the user hovers. And, of course, you can change the font so that the text is underlined if you so choose.
Tk is a wonderful toolkit that gives you the building blocks to do just about whatever you want. You just need to look at the widgets not as a set of pre-made walls and doors but more like a pile of lumbar, bricks and mortar.
"blue" should indeed be acceptable (since you're on Windows, Tkinter should use its built-in color names table -- it might be a system misconfiguration on X11, but not on Windows); therefore, this is a puzzling problem (maybe a Tkinter misconfig...?). What happen if you use foreground="#00F" instead, for example? This doesn't explain the problem but might let you work around it, at least...

Tkinter Label bound to StringVar is one click behind when updating

The problem I'm running into here is that, when I click on the different file names in the Listbox, the Label changes value one click behind whatever I'm currently clicking on.
What am I missing here?
import Tkinter as tk
class TkTest:
def __init__(self, master):
self.fraMain = tk.Frame(master)
self.fraMain.pack()
# Set up a list box containing all the paths to choose from
self.lstPaths = tk.Listbox(self.fraMain)
paths = [
'/path/file1',
'/path/file2',
'/path/file3',
]
for path in paths:
self.lstPaths.insert(tk.END, path)
self.lstPaths.bind('<Button-1>', self.update_label)
self.lstPaths.pack()
self.currentpath = tk.StringVar()
self.lblCurrentPath = tk.Label(self.fraMain, textvariable=self.currentpath)
self.lblCurrentPath.pack()
def update_label(self, event):
print self.lstPaths.get(tk.ACTIVE),
print self.lstPaths.curselection()
self.currentpath.set(self.lstPaths.get(tk.ACTIVE))
root = tk.Tk()
app = TkTest(root)
root.mainloop()
The problem has to do with the fundamental design of Tk. The short version is, bindings on specific widgets fire before the default class bindings for a widget. It is in the class bindings that the selection of a listbox is changed. This is exactly what you observe -- you are seeing the selection before the current click.
The best solution is to bind to the virtual event <<ListboxSelect>> which is fired after the selection has changed. Other solutions (unique to Tk and what gives it some of its incredible power and flexibility) is to modify the order that the bindings are applied. This involves either moving the widget bindtag after the class bindtag, or adding a new bindtag after the class bindtag and binding it to that.
Since binding to <<ListboxSelect>> is the better solution I won't go into details on how to modify the bindtags, though it's straight-forward and I think fairly well documented.

Categories

Resources