|
Post by Uncle Buddy on Mar 29, 2023 3:32:27 GMT -8
I'm in the middle of redesigning the opening window and making it possible to have multiple family trees open at once without starting multiple instances of Treebard. I will be making a feature for importing places from one tree to another, since Treebard in its portability feels no need to come pre-loaded with geographical places. It's coming along pretty well. Except for hurdles caused by my custom widgets and the colorization scheme which works great but regularly costs me big chunks of time. I'd actually love to start the colorizer over from scratch. Again.
And to be perfectly honest, not that anyone asked me to be, I would love to roll back to a time when Treebard GPS was resigned to using tkinter menu bars, Windows title bars, and maybe even ttk.Combobox, in spite of the fact that changing the background colors of these widgets to follow the Treebard color scheme is somewhere between impossible and not worth the trouble.
Having begun this project with zero knowledge of coding outside of HTML and CSS, a large part of my learning process while building Treebard was proving I could make it beautiful. With obstinate, bull-headed persistence I devoted massive amounts of time to mocking up widgets which do work and do colorize, but kinda break if you look at them cross-eyed. Which is an exaggeration but it is true that Treebard is not supposed to be about me proving I can change colors, and this has been a big distraction from the real goal which is to prove I can write a genieware app without being a real programmer.
There are no absolutes here, I'm just blathering. My wife's niece mailed us some chocolate covered marshmallow bunny rabbits from the US, and I think I ate too many of them, and the sugar pollution is messing with my brain. I'm in one of those moods where I'd rather start over from scratch and Keep it Simple This Time than to solve one more problem caused by my custom widgets, which are not proper tk/tcl widgets but rather Tkinter frames and labels and such, patched together and running on good intentions with fingers crossed. That is an exaggeration, they actually work pretty good.
My homemade combobox beats the tar out of ttk.Combobox but I don't want to face putting a scrollbar back into it after having semi-accidentally deleted the scrollbar from it. The combobox had one feature too many and this was the one that was tripping it up, so I saved its life by cutting its scrollbar off. It does not fully appreciate the necessity of the loss.
This rant is all about the annoyance of trying to maintain these homemade widgets when the code changes. Often when I make a change to something seeminly unrelated like the import structure, the fact that I once chose to never see a white background on anything ever is causing me unforeseen problems. Not because it has to be that way, but because not only am I an ignoramus, I'm using an ancient toolkit (Tkinter) which is not apparently going to ever be brought up to date or improved. I'm losing patience with the process of proving stuff, I want to bring this project to a reasonable resting place and learn something new, including DearPyGUI, Linux, Web2Py, Rust, & Tauri. The side trips to fix homemade widgets are preventing me from getting features written in Treebard GPS in a timely manner so I can start a new project.
Let's fantasize about what I would like to do in my extreme mood right now. The mood will pass but I might make some repercussionistic decisions before it does. What might I do?
I will not go back to the ttk.Notebook. It is useless with a color-scheme due to its dependence on Windows themes. You can put the tabs on the bottom but you can't make them look good there. I am not having any trouble with my homemade TabBook widget. The gist of the ttk.Notebook problem is that if you want to have color schemes that work on it, you have to choose a certain Windows theme, then hope Windows doesn't cancel that theme, and worse, once you've chosen a theme based on how the ttk.Notebook looks, you have to settle for how the other stuff looks in that theme too. Fortunately, my homemade TabBook works fine.
I might go back to the ttk.Combobox. It's possible to change its background color using non-tkinter methods dictated by tk/tcl, but since I have no understanding of tk/tcl, when this stops working I might as well be whistling dixie down by the riverside, and besides, that ttk.Combobox doesn't have half the functionality of my combobox. However my combobox is bloated and slow. It can't handle many values, but who wants a combobox with many values anyway? If it has enough values to need a scrollbar, a different widget would be better. But to be perfectly honest, I'm in the mood to go back to the ttk.Combobox and try harder to get the background color to change reliably under a variety of circumstances.
I am not having a problem with my homemade Dropdown menu. What annoys me about it is that it brings in code from so many modules that it's one of those devices that force me to put thousands of lines of code in one module in order to avoid circular imports. So conceivably I could backpedal to the tkinter dropdown menu in spite of its whitish color.
The custom scrollbar, unlike my other widgets, is based on the tk/tcl API or the Tkinter API or whatever I'm supposed to call it, anyway the point is that it is very reliable and colorizes well and doesn't cause me any more trouble than any other Tkinter scrollbar.
The worst problem I am facing right now due to homemade widgets is with my title bar/window border widget which inherits from tk.Canvas. The widget works fine, it works great, it looks good, no problem. Creating it was a fantastic learning experience and I'm proud of my work. The problem is that without a Windows title bar, a window is pretty much guaranteed to not have some of the features that Windows users expect. It will show an icon of some sort on the title bar when minimized, in spite of Windows 11's further dumbing-down of this feature, but let's face it. The purpose of Treebard GPS is not to prove Windows wrong about anything, not to prove ttk widgets wrong about anything, and not to show off how beautiful Tkinter widgets can be if I refuse to play by the stupid rules. (With the exception of ttk.Notebook which I have replaced with no problem except getting it to work with tab traversal was not worth the trouble...after that part of it stopped working...)
The purpose of Treebard GPS is to demonstrate GUI functionalities that genealogy software could or should have. A recently delineated goal is to provide a model of an underlying data structure, which I now call UNIGEDS, that all genieware could use, even commercial software companies that are in competition with each other, which could therefore replace GEDCOM for any company willing to use it. Messing around with widget creations, in my condition of novitiate ignorance, is a waste of my time. So from at least one point of view, I should in fact start over, not from scratch, but I should return to the Windows-themed Windows title bar and the tragically whitish Tkinter menu bar and try to concentrate on the real goals of Treebard instead of wasting time and thousands of lines of code trying to make it look like I know something about programming. The whole point of this project is that writing genieware is not so hard that inexperienced programmers couldn't manage to do it themselves; any motivated person with the time to do it could get it done. And I could do it better if I were better at compromising my high-faluting aspirations and sticking to the basics.
When it comes right down to it, if someone uses Treebard as a model for their own app (and that's what Treebard is for), they're not gonna use Tkinter and they're probably not gonna use Python. So what is it that I'm trying to prove, exactly? The GPS version of Treebard is a showcase of functionalities, a working model, not an app for daily use.
I've mentioned before that I'm an Aries with Capricorn rising, which boils down to this: I live to climb halfway up the tallest ladder I can find, especially if everyone's telling me not to, and then jump off it so I can feel the refreshing breeze in my face as I brace myself for the upcoming belly-flop. I accept this about myself and you can just go ahead and accept it about me too, cuz it ain't a-gonna change, Cuz.
|
|
|
Post by Uncle Buddy on Mar 29, 2023 6:30:44 GMT -8
Fortunately I was wrong about something, and I am fairly ecstatic about that.
In the past, I had followed the instructions for making a toplevel menu which is demonstrated below in dlg_1. Notice that the red background option as well as the font settings are ignored. That's how the creators of Tkinter felt about it. They decided the user should not have a choice, and no doubt this white menu is chosen by Windows themes or by Tkinter hard-coding some off-white color. The toplevel menu is not gridded. It appears at the top of the window, between the title bar and row 0.
What I'd failed to do was to look more carefully at the manual, which clearly teaches how to make a menu any place you want. For example in row 0. This menu can be any color you want. This is happy news, and I don't mind having twice written long-winded code to create my own dropdown menus. I barely remember the pain. Anyway, it was my fault for not reading the manual.
I use the manual by John Shipman which is easier to understand than the official docs at tcl/tk's website, but the Shipman manual is for version 8.5 and the more recent version 8.6 is not always the same. However, it is a good manual and most of it is useful for 8.6. The official docs are not for Tkinter itself, but for tcl/tk which is the code behind Tkinter, so they require more thought and research to understand for a mere Python user.
In the demo code below, the teal-colored menu occupies the same position as a toplevel menu would, but you can change the font and color.
import tkinter as tk from tkinter import ttk
def open_dlg_1(): dlg_1 = tk.Toplevel() dlg_1.geometry("400x400+400+200") cls1 = tk.Button(dlg_1, text="CLOSE", command=lambda dlg=dlg_1: close(dlg)) cls1.grid() menubar = tk.Menu(dlg_1, bg="red", font=("arial black", 17)) drop = tk.Menu(menubar, tearoff=0, bg="yellow") menubar.add_cascade(label="HELP", menu=drop) menubar.add_cascade(label="ABOUT", menu=drop) drop.add_command(label="VERSION") dlg_1.config(menu=menubar)
def open_dlg_2(): dlg_2 = tk.Toplevel() dlg_2.geometry("400x400+850+300") cls2 = tk.Button(dlg_2, text="CLOSE", command=lambda dlg=dlg_2: close(dlg)) cls2.grid()
def close(dlg): dlg.destroy()
root = tk.Tk()
menu = tk.Frame(root, bg="teal") menu.grid(column=0, row=0, sticky="ew")
file = tk.Menubutton( menu, text="FILE", bg="teal", fg="bisque", font=("arial black", 10), relief="flat", anchor="w", activebackground="bisque") file_drop = tk.Menu( file, tearoff=0, bg="teal", fg="bisque", font=("arial black", 10), activebackground="black") file['menu'] = file_drop file_drop.add_command(label="OPEN", command=open_dlg_1) file_drop.add_command(label="NEW", command=open_dlg_2)
edit = tk.Menubutton( menu, text="EDIT", bg="teal", fg="bisque", font=("arial black", 10), relief="flat", anchor="w", activebackground="bisque") edit_drop = tk.Menu( edit, tearoff=0, bg="teal", fg="bisque", font=("arial black", 10), bd=0) edit['menu'] = edit_drop
tools = tk.Menubutton( menu, text="TOOLS", bg="teal", fg="bisque", font=("arial black", 10), relief="flat", anchor="w", activebackground="bisque")
file.grid(column=0, row=0, sticky="w") edit.grid(column=1, row=0, sticky="w") tools.grid(column=2, row=0, sticky="w")
cbo = ttk.Combobox(root, values=["Light Mode", "Earth Tones", "Dark Mode"]) cbo.grid()
b1 = tk.Button(root, text="OPEN DIALOG 1", command=open_dlg_1) b1.grid() b2 = tk.Button(root, text="OPEN DIALOG 2", command=open_dlg_2) b2.grid()
cbo_style = ttk.Style() # cbo_style.theme_use('clam') # combobox arrow way too narrow cbo_style.theme_use('alt') # arrow still too narrow but not quite as ugly cbo_style.configure( "TCombobox", fieldbackground= "teal", background= "steelblue", foreground="bisque")
root.mainloop()
|
|
|
Post by Uncle Buddy on Mar 29, 2023 6:51:42 GMT -8
Also in the code above there's a way of choosing a background color for a ttk.Combobox. If I am correct, this didn't always work or didn't work when setting all ttk.Comboboxes to a different color, but it's a start. I know I was doing something more tcl-ish and one day it stopped working.
Also note that in order to make the combobox option not be ignored by tkinter, a certain Windows theme had to be used. That's what I was complaining about in the original post. Because of this, I'd say that the safest thing to do is to not use more than one ttk widget class in your app, because a theme that makes one widget look the way you want will make another widget look like it was designed by a drunk chimpanzee. Try commenting out all the calls to `use_theme`. The color option will then be ignored and there will be other changes, such as the width of the combobox arrow label will change drastically. In fact, the arrow looks better in the default theme whatever it is, but to get the background to change you have to use clam or alt theme, otherwise you're stuck with a white background.
This is my argument against ttk widgets. They are reliant on Windows themes, and Windows themes are reliant on the geniuses at Microsoft whose specialty is taking choices away, apparently because they feel they are omniscient and know what users should want. On top of that, ttk doesn't tell you when it's gonna ignore an option, you just have to try options to see if they work. As for ttk.Style, it is supposed to make things easier, but if you don't know more about it than you might want to, instantiating this class will sometimes create an unwanted window. Why would a configuration class ever create an unwanted widget? I'd guess bad programming, but what do I know? Anyhow, to rely on Windows for good taste in design is sort of like leaving the dishes outside for the rain to wash, which will cause it to not rain. If we're designing something, don't we want to be the ones making the design decisions?
If comboboxes have to always be some color and can't reliably be changed at will, then at least that color should not be white. I don't care who expects to see white or off-white on a computer screen, every drop of it hurts my eyes. Speaking of which, good night.
|
|
|
Post by Uncle Buddy on Mar 29, 2023 16:46:49 GMT -8
Can two themes be invoked for two different ttk widgets? Well that's not how themes work, they affect things globally. See the code below for the answer. If you uncomment the call to `theme_use` that is commented, the ability to use two custom styles for two different comboboxes is destroyed. This ability is important, maybe not for Comboboxes in my case, but for example if I were to use ttk.Entry (which I'm not going to do), I might want some Entries to blend into the background by being the same color, while other Entries would stand out with a different background (but not white). Using Entries with the same color as the background isn't usually a good idea, but I used to do it and it was important to me at the time. (Nowadays I'm instead using Labels when I want something to look like a Label, and overlaying it with an Entry when I want to change what's in the Label. It takes more clicks to grid the overlay--you can't just start typing in the widget because it's really a Label--but by making the Labels take focus, you can still tab through them, same as Entries. The point is to have a table look less forbidding by not having a widget in each cell that Demands to Be Filled In by standing out from the background. I don't want a genealogy application to look like a government form. God forbid.)
Also I wanted to mention that the wide ugly light gray sculptured border around the otherwise-OK configurable dropdown menu on the main window... that ugly gray border isn't going away. The options that should control this, such as `borderwidth`, `highlightthickness` and `relief`, are either ignored or else they raise an error if you try to use them. So far that's the only objection I have to the dropdown menu if used correctly.
import tkinter as tk from tkinter import ttk
def open_dlg_1(): dlg_1 = tk.Toplevel() dlg_1.geometry("400x400+400+200") cls1 = tk.Button(dlg_1, text="CLOSE", command=lambda dlg=dlg_1: close(dlg)) cls1.grid() menubar = tk.Menu(dlg_1, bg="red", font=("arial black", 17)) drop = tk.Menu(menubar, tearoff=0, bg="yellow") menubar.add_cascade(label="HELP", menu=drop) menubar.add_cascade(label="ABOUT", menu=drop) drop.add_command(label="VERSION") dlg_1.config(menu=menubar)
def open_dlg_2(): dlg_2 = tk.Toplevel() dlg_2.geometry("400x400+850+300") cls2 = tk.Button(dlg_2, text="CLOSE", command=lambda dlg=dlg_2: close(dlg)) cls2.grid()
def close(dlg): dlg.destroy()
root = tk.Tk()
menu = tk.Frame(root, bg="teal") menu.grid(column=0, row=0, sticky="ew")
file = tk.Menubutton( menu, text="FILE", bg="teal", fg="bisque", font=("arial black", 10), relief="flat", anchor="w", activebackground="bisque") file_drop = tk.Menu( file, tearoff=0, bg="teal", fg="bisque", font=("arial black", 10), activebackground="black") file['menu'] = file_drop file_drop.add_command(label="OPEN", command=open_dlg_1) file_drop.add_command(label="NEW", command=open_dlg_2)
edit = tk.Menubutton( menu, text="EDIT", bg="teal", fg="bisque", font=("arial black", 10), relief="flat", anchor="w", activebackground="bisque") edit_drop = tk.Menu( edit, tearoff=0, bg="teal", fg="bisque", font=("arial black", 10), bd=0) edit['menu'] = edit_drop
tools = tk.Menubutton( menu, text="TOOLS", bg="teal", fg="bisque", font=("arial black", 10), relief="flat", anchor="w", activebackground="bisque")
file.grid(column=0, row=0, sticky="w") edit.grid(column=1, row=0, sticky="w") tools.grid(column=2, row=0, sticky="w")
# can more than one Windows theme be used in the same app? cbo_style = ttk.Style() cbo_style.theme_use('alt') cbo_style.configure( "teal.TCombobox", fieldbackground= "teal", background= "steelblue", foreground="bisque")
cbo2_style = ttk.Style() # cbo2_style.theme_use('clam') cbo2_style.configure( "red.TCombobox", fieldbackground= "red", background= "green", foreground="yellow")
cbo = ttk.Combobox( root, style="teal.TCombobox", values=["Light Mode", "Earth Tones", "Dark Mode"]) cbo.grid()
cbo2 = ttk.Combobox( root, style="red.TCombobox", values=["Light Mode", "Earth Tones", "Dark Mode"]) cbo2.grid()
b1 = tk.Button(root, text="OPEN DIALOG 1", command=open_dlg_1) b1.grid() b2 = tk.Button(root, text="OPEN DIALOG 2", command=open_dlg_2) b2.grid()
root.mainloop()
|
|
|
Post by Uncle Buddy on Mar 29, 2023 18:15:50 GMT -8
If I use ttk.Combobox, the routine used to colorize everything else will not work for comboboxes. Here's an example of a ttk.Combobox that changes its own color. This code can be dropped into the code above.
def colorize(evt): widg = evt.widget got = widg.get().split()
print("line", looky(seeline()).lineno, "got:", got) entry_bg, arrow_bg, fg = got cbo_style.configure( "TCombobox", fieldbackground=entry_bg, background=arrow_bg, foreground=fg) widg.select_clear()
cbo_style = ttk.Style() cbo_style.configure( "TCombobox", fieldbackground= "red", background= "blue", foreground="green")
cbo = ttk.Combobox( root, style="TCombobox", values=[ ("teal", "steelblue", "bisque"), ("magenta", "yellow", "navy"), ("black", "gray", "steelblue")]) cbo.grid()
cbo.bind("<<ComboboxSelected>>", colorize)
|
|
|
Post by Uncle Buddy on May 30, 2023 2:06:53 GMT -8
I did go back to ttk.Combobox for a short while since my Toykinter combobox was limping around without a scrollbar, and during that minor ttk relapse I did learn how to configure even the dropdown of the ttk.Combobox. This process was a double set of pains in the butt, because it meant I had to 1) configure comboboxes differently than other widgets and 2) configure ttk.Combobox Entry portion one way while configuring ttk.Combobox dropdown portion a different way. By "different way" I mean using different methods or techniques or whatever you want to call them. The details are not important because the resulting muddle re-inspired me to rewrite the Toykinter Combobox completely. Twice.
Here is the latest Combobox code, and it is a whole different animal than what I came up with previously, but has all the same features plus the addition of simplicity, which my prior combobox code lacked, making it unmaintainable.
''' Replaces ttk.Combobox with an easily configurable widget.
Configuration is done tkinter style, instead of pitting ttk.Style and Windows themes against each other to see which one wins, as is the norm when trying to configure ttk widgets. The ttk.Combobox uses a tk.Listbox as its dropdown window, which is like hiring your ex-wife's homeless brother to run your business.
Unlike ttk.Combobox... ...dropdown items are selected with mouse, Return key, or spacebar ...colors including Entry background and dropdown background are easily configured ...clicking either Entry or Arrow opens then closes dropdown on alternate clicks ...FocusOut event can be bound to the dropdown items ...arrow traversal thru dropdown items loops to top or bottom when bottom or top is reached ...dropdown opens with either top or bottom item highlighted depending on whether Up or Down arrow key is used to open the dropdown ...a dropdown item with text longer than the window displays a tooltip that shows the whole text.
This combobox was created by Scott Robertson, the author of Treebard GPS, May 2023 after years of rewritings and revisions. The license is "Unlicense" i.e. public domain, and please give credit to the Treebard GPS project whence this woundrous widget sprang into existence after much labor. '''
# scrollbar_pref = "skinny" scrollbar_pref = "fat"
values1 = ( "Jed", "FredNedJedPhilBillJillTiffSpiffNewtNewtNewt", "Ned", "Phil", "Jill", "Bill", "Biff", "Tiff", "Spiff", "NNNNNNNNNNNNNNNNNNNN00", "Jed", "Fred", "Ned", "Phil", "Jill", "Bill", "Biff", "Tiff", "Spiff")
values2 = COLOR_STRINGS
values3 = ("red", "white", "blue")
class Combobox(tk.Frame): """ Only one combobox dropdown is shown at a time, so only one dropdown exists per family tree. This is `self.combobox_dropdown` in the FamilyTree class, referenced here by its alias `self.dropdown`. """ formats = {} def __init__( self, master, formats, family_tree=None, entry_width=20, arrow_width=2, callback=None, values=None, *args, **kwargs): tk.Frame.__init__(self, master, *args, **kwargs) self.master = master Combobox.formats = self.formats = formats self.family_tree = family_tree self.entry_width = entry_width self.arrow_width = arrow_width self.callback = callback self.values = values
self.dropdown_is_open = False
self.font_size = self.formats["input_font"][1]
self.style = "frame"
self.dropdown = self.family_tree.combobox_dropdown self.selected = None self.result_string = ''
self.entered = None self.lenval = len(self.values) self.owt = None self.scrollbar_clicked = False self.typed = None
self.fixed_x = 0
self.screen_height = self.winfo_screenheight() self.config(bd=0)
# simulate <<ComboboxSelected>>: self.var = tk.StringVar() self.var.trace_add('write', lambda *args, **kwargs: self.combobox_selected())
# simulate ttk.Combobox.current() self.current = 0
self.make_widgets() self.master.bind_all('<ButtonRelease-1>', self.close_dropdown, add='+')
# Expose only unique methods of Entry e.g. not `self.config`; `self` is # a Frame, but the Entry, Toplevel, Canvas, and canvas window Frame # have to be configured together. To size the entry, use # `config_combo_width()`. self.insert = self.entry.insert self.delete = self.entry.delete self.get = self.entry.get
configall(self, self.formats)
def make_widgets(self):
self.entry = tk.Entry(self, textvariable=self.var, width=self.entry_width) self.arrow = tk.Label(self, text='\u25BC', width=self.arrow_width) self.entry.style = "entry" self.arrow.style = "labelhilited"
self.entry.pack(side="left") self.arrow.pack(side="left")
self.make_dropdown() for frm in (self, self.dropdown.content): """ Bind arrow events to frames to make arrow label highlight on click or focus in. I don't remember why it had to be done this way, but changing it to run a local callback breaks the highlighting. """ frm.bind('<FocusIn>', self.highlight_arrow) frm.bind('<FocusOut>', self.unhighlight_arrow)
self.entry.bind('<KeyPress>', self.open_or_close_dropdown) self.entry.bind('<Tab>', self.open_or_close_dropdown)
for widg in (self.entry, self.arrow): widg.bind('<Button-1>', self.open_or_close_dropdown, add='+') self.arrow.bind('<Button-1>', self.focus_entry_on_arrow_click, add='+')
self.current_combo_parts = [self, self.entry, self.arrow, self.dropdown.sbv] for part in self.current_combo_parts: part.bind('<Enter>', self.unbind_combo_parts, add="+") part.bind('<Leave>', self.rebind_combo_parts, add="+")
def make_dropdown(self): for widg in (self.master, self.dropdown): widg.bind('<Escape>', self.hide_dropdown, add='+')
self.dropdown.bind('<FocusIn>', self.focus_dropdown) self.dropdown.bind('<Unmap>', self.unhighlight_all_drop_items) gauge = tk.Button( self.dropdown.content, text='', bd=0, relief="flat", overrelief="flat", font=self.formats["input_font"]) gauge.style = "button" one_height = gauge.winfo_reqheight() gauge.destroy() self.fit_height = one_height * self.lenval
def populate_dropdown(self): def scroll_start(evt): self.dropdown.canvas.scan_mark(evt.x, evt.y) self.fixed_x = evt.x
def scroll_move(evt): """ Use a fixed vertical line (self.fixed_x instead of evt.x) to scroll up and down only. Gain is 10 by default, 1 is max here, i.e. a 1:1 ratio of canvas motion to mouse motion. """ self.dropdown.canvas.scan_dragto(self.fixed_x, evt.y, gain=1)
def on_mousewheel(event): """ The `//` is integer division. """ self.dropdown.canvas.yview_scroll(-1 * (event.delta // 120), "units")
for button in self.dropdown.content.winfo_children(): button.destroy()
char_width = self.entry_width + self.arrow_width px_width = self.winfo_reqwidth() fat_sb = int(self.font_size * 1.66) skinny_sb = int(self.font_size * 0.75)
self.max_dropdown_height = int(self.screen_height * 0.33) truncate = False if self.fit_height >= self.max_dropdown_height: truncate = True if scrollbar_pref == "fat": self.dropdown.sbv["width"] = fat_sb char_width -= 2 deduct = fat_sb elif scrollbar_pref == "skinny": self.dropdown.sbv["width"] = skinny_sb char_width -= 1 deduct = skinny_sb self.dropdown.canvas.config( height=self.max_dropdown_height, width=px_width - deduct, scrollregion=(0, 0, 0, self.fit_height)) else: self.dropdown.canvas.config( width=px_width, height=self.fit_height, scrollregion=(0, 0, 0, self.fit_height))
for text in self.values: bt = tk.Button( self.dropdown.content, text=text[0:char_width], anchor='w', bd=0, relief="flat", overrelief="flat") bt.style = "comboboxdropdownbutton" bt.pack(side="top", fill="x") for evt_stg in ('<Button-1>', '<Return>', '<space>'): bt.bind(evt_stg, self.get_clicked, add='+') bt.bind('<Enter>', self.highlight_button) bt.bind('<Leave>', self.unhighlight_button) bt.bind('<Tab>', self.tab_out_of_dropdown_fwd) bt.bind('<Shift-Tab>', self.tab_out_of_dropdown_back) bt.bind('<KeyPress>', self.traverse_on_arrow) bt.bind('<FocusOut>', self.unhighlight_button) bt.bind('<FocusOut>', self.get_tip_widg, add='+') bt.bind('<FocusIn>', self.get_tip_widg) bt.bind('<Enter>', self.get_tip_widg, add='+') bt.bind('<Leave>', self.get_tip_widg, add='+') if truncate: bt.bind("<MouseWheel>", on_mousewheel)
for item in self.dropdown.content.winfo_children(): item.config(command=self.callback) configall(self.dropdown, self.formats)
def get_tip_widg(self, evt): """ '10' is `FocusOut`, '9' is `FocusIn`, '7' is `Enter`, '8' is `Leave` """ widg = evt.widget evt_type = evt.type idx = self.dropdown.content.winfo_children().index(widg) if self.entry_width < len(self.values[idx]): if evt_type in ('7', '9'): self.show_overwidth_tip(widg, idx) elif evt_type in ('8', '10'): self.hide_overwidth_tip()
def show_overwidth_tip(self, widg, idx): """ Instead of a horizontal scrollbar, if a dropdown item doesn't all show in the space allotted, the full text will appear in a tooltip on highlight. Most of this code is borrowed from Michael Foord. """ text = self.values[idx] if self.owt: return x, y, cx, cy = widg.bbox() x = x + widg.winfo_rootx() + 32 y = y + cy + widg.winfo_rooty() + 32 self.owt = tk.Toplevel(self) self.owt.wm_overrideredirect(1) l = tk.Label( self.owt, bg=NEUTRAL_COLOR, text=text, bd=1, relief='solid') l.pack(ipadx=6, ipady=3) self.owt.wm_geometry(f"+{x}+{y}")
def hide_overwidth_tip(self): tip = self.owt self.owt = None if tip: tip.destroy()
def close_dropdown(self, evt): widg = evt.widget self.dropdown.withdraw() self.dropdown_is_open = False
def focus_entry_on_arrow_click(self, evt): self.focus_set() self.entry.select_range(0, 'end')
def open_or_close_dropdown(self, evt=None): """ The `self.dropdown.winfo_ismapped()` gets the wrong value if the scrollbar was the last thing clicked so `drop_is_open` has to be used also. """ if self.values: self.populate_dropdown() else: print("line", looky(seeline()).lineno, "returning:") return self.hide_overwidth_tip() if evt is None: # dropdown item clicked--no evt because of Button command option if self.callback: self.callback(self.selected) self.dropdown.withdraw() self.dropdown_is_open = False return if len(self.dropdown.content.winfo_children()) == 0: return evt_type = evt.type evt_sym = evt.keysym if evt_sym == 'Tab': self.dropdown.withdraw() self.dropdown_is_open = False return elif evt_sym == 'Escape': self.hide_dropdown() return first = None last = None dropdown_buttons = self.dropdown.content.winfo_children() if len(dropdown_buttons) != 0: first = dropdown_buttons[0] last = dropdown_buttons[-1] if evt_type == '4': if self.dropdown_is_open: self.arrow["bg"] = self.formats["highlight_bg"] self.dropdown.withdraw() self.dropdown_is_open = False return else: self.arrow["bg"] = self.formats["head_bg"] elif evt_type == '2': if evt_sym not in ('Up', 'Down'): return elif first is None or last is None: pass elif evt_sym == 'Down': first.config(bg=Combobox.formats["bg"]) first.focus_set() self.dropdown.canvas.yview_moveto(0.0) elif evt_sym == 'Up': last.config(bg=Combobox.formats["bg"]) last.focus_set() self.dropdown.canvas.yview_moveto(1.0)
self.fly_up_or_drop_down(evt) self.dropdown.deiconify() self.dropdown_is_open = True
def fly_up_or_drop_down(self, evt): """ Set the dropdown position to either drop down or fly up depending on the position of the combobox in relation to the top and bottom edges of the screen. Before it can be decided whether the dropdown must fly up instead of dropping down, its height has to be limited if it has too many values to fit into the screen height or a set portion thereof. """ if self.max_dropdown_height < self.fit_height: self.dropdown.sbv.grid() height = self.max_dropdown_height else: height = self.fit_height self.dropdown.sbv.grid_remove()
combo_height = self.winfo_reqheight() over_in_screen = self.entry.winfo_rootx() down_in_screen = self.entry.winfo_rooty()
down = down_in_screen + combo_height
fly_up, fly_up_clearance = self.get_vertical_pos( combo_height, height, evt) if fly_up is False: drop_y = down else: drop_y = fly_up_clearance self.dropdown.geometry(f"+{over_in_screen}+{drop_y}")
def get_vertical_pos(self, combo_height, height, evt): click_from_screen_top = evt.y_root click_from_combo_top = evt.y fly_up = False combo_to_screen_top = click_from_screen_top - click_from_combo_top combo_to_screen_bottom = ( self.screen_height - (combo_to_screen_top + combo_height)) if combo_to_screen_bottom < height: fly_up = True fly_up_clearance = combo_to_screen_top - height return fly_up, fly_up_clearance
def highlight_button(self, evt): for widg in self.dropdown.content.winfo_children(): widg.config(bg=self.formats["highlight_bg"]) widget = evt.widget widget.config(bg=self.formats["bg"]) self.selected = widget widget.focus_set()
def unhighlight_button(self, evt): x, y = self.winfo_pointerxy() hovered = self.winfo_containing(x,y) if hovered in self.dropdown.content.winfo_children(): evt.widget.config(bg=self.formats["highlight_bg"])
def unbind_combo_parts(self, evt): self.master.unbind_all('<ButtonRelease-1>')
def rebind_combo_parts(self, evt): self.master.bind_all('<ButtonRelease-1>', self.close_dropdown, add='+')
def unhighlight_all_drop_items(self, evt): for child in self.dropdown.content.winfo_children(): child.config(bg=Combobox.formats["highlight_bg"])
def hide_drops_on_title_bar_click(self, evt): x, y = self.winfo_pointerxy() hovered = self.winfo_containing(x,y)
def focus_dropdown(self, evt): for widg in self.dropdown.content.winfo_children(): widg.config(takefocus=1)
def handle_tab_out_of_dropdown(self, go): for widg in self.dropdown.content.winfo_children(): widg.config(takefocus=0)
self.entry.delete(0, 'end') self.entry.insert(0, self.selected.cget('text')) self.dropdown.withdraw() self.dropdown_is_open = False self.entry.focus_set() if go == 'fwd': goto = self.entry.tk_focusNext() elif go == 'back': goto = self.entry.tk_focusPrev() goto.focus_set()
def tab_out_of_dropdown_fwd(self, evt): self.selected = evt.widget self.handle_tab_out_of_dropdown('fwd')
def tab_out_of_dropdown_back(self, evt): self.selected = evt.widget self.handle_tab_out_of_dropdown('back')
def get_clicked(self, evt): self.selected = evt.widget children = self.dropdown.content.winfo_children() self.current = children.index(self.selected) self.entry.delete(0, 'end') self.entry.insert(0, self.selected.cget('text')) self.entry.select_range(0, 'end') self.open_or_close_dropdown() # WHY NOT JUST CLOSE THE DROPDOWN?
def get_typed(self): self.typed = self.var.get()
def highlight_on_traverse(self, evt, next_item=None, prev_item=None): evt_type = evt.type evt_sym = evt.keysym # 2 is key press, 4 is button press
for widg in self.dropdown.content.winfo_children(): widg.config(bg=self.formats["highlight_bg"]) if evt_type == '4': self.selected = evt.widget elif evt_type == '2' and evt_sym == 'Down': self.selected = next_item elif evt_type == '2' and evt_sym == 'Up': self.selected = prev_item
self.selected.config(bg=self.formats["bg"]) self.widg_height = int(self.fit_height / self.lenval) widg_screenpos = self.selected.winfo_rooty() widg_listpos = self.selected.winfo_y() win_top = self.dropdown.winfo_rooty() win_bottom = win_top + self.fit_height list_ratio = widg_listpos / self.fit_height widg_ratio = self.widg_height / self.fit_height up_ratio = list_ratio - widg_ratio
if widg_screenpos > win_bottom - 0.75 * self.widg_height: self.dropdown.canvas.yview_moveto(float(list_ratio)) elif widg_screenpos < win_top: self.dropdown.canvas.yview_moveto(float(up_ratio)) self.selected.focus_set()
def traverse_on_arrow(self, evt): if evt.keysym not in ('Up', 'Down'): return widg = evt.widget sym = evt.keysym self.widg_height = int(self.fit_height / self.lenval) next_item = widg.tk_focusNext() prev_item = widg.tk_focusPrev()
if sym == 'Down': if next_item in self.dropdown.content.winfo_children(): self.highlight_on_traverse(evt, next_item=next_item) else: next_item = self.dropdown.content.winfo_children()[0] next_item.focus_set() next_item.config(bg=self.formats["bg"]) self.dropdown.canvas.yview_moveto(0.0)
elif sym == 'Up': if prev_item in self.dropdown.content.winfo_children(): self.highlight_on_traverse(evt, prev_item=prev_item) else: prev_item = self.dropdown.content.winfo_children()[-1] prev_item.focus_set() prev_item.config(bg=self.formats["bg"]) self.dropdown.canvas.yview_moveto(1.0)
def highlight_arrow(self, evt): self.config(bg=Combobox.formats["head_bg"])
def unhighlight_arrow(self, evt): self.config(bg=Combobox.formats["highlight_bg"])
def hide_dropdown(self, evt=None): self.dropdown.withdraw() self.dropdown_is_open = False
def callback(self): """ A function specified on instantiation. """ print('this will not print if overridden (callback)') # pass
def combobox_selected(self): """ A function specified on instantiation will run when the selection is made. Similar to ttk's <<ComboboxSelected>> but instead of binding to a virtual event. """ # print('this will not print if overridden (combobox_selected)') pass
if __name__ == "__main__":
formats = make_formats_dict(root=True)
def make_family_tree(): family_tree = tk.Toplevel(treebard) print("line", looky(seeline()).lineno, "family_tree:", family_tree) family_tree.title("Family Tree")
family_tree.combobox_dropdown = tk.Toplevel() family_tree.combobox_dropdown.style = "toplevel"
family_tree.combobox_dropdown.canvas = tk.Canvas( family_tree.combobox_dropdown, bd=0, highlightthickness=0)
family_tree.combobox_dropdown.sbv = Scrollbar( family_tree.combobox_dropdown, formats=formats, command=family_tree.combobox_dropdown.canvas.yview)
family_tree.combobox_dropdown.sbv.style = "scrollbar"
family_tree.combobox_dropdown.canvas.config( yscrollcommand=family_tree.combobox_dropdown.sbv.set) family_tree.combobox_dropdown.canvas.config(bg="blue") family_tree.combobox_dropdown.canvas.grid(column=0, row=0, sticky="news") family_tree.combobox_dropdown.sbv.grid(column=1, row=0, sticky="ns")
family_tree.combobox_dropdown.canvas.style = "bgHi"
family_tree.combobox_dropdown.content = tk.Frame( family_tree.combobox_dropdown.canvas) family_tree.combobox_dropdown.content.style = "bgHi"
family_tree.combobox_dropdown.canvas.create_window( 0, 0, anchor='nw', window=family_tree.combobox_dropdown.content)
family_tree.combobox_dropdown.wm_overrideredirect(1) family_tree.combobox_dropdown.withdraw()
family_tree.style = "toplevel" ent1 = tk.Entry(treebard) ent1.grid() ent1.style = "entry"
cbo = Combobox( family_tree, formats, family_tree, values=values3, entry_width=4, arrow_width=1) cbo.grid(padx=20, pady=20, sticky="w") cbo.style = "frame" cbo1 = Combobox( family_tree, formats, family_tree, values=values1) cbo1.grid(padx=20, pady=20, sticky="w") cbo1.style = "frame" cbo2 = Combobox( family_tree, formats, family_tree, values=values2, entry_width=19) cbo2.grid(padx=20, pady=20, sticky="w") cbo2.style = "frame"
configall(family_tree, formats) return family_tree
treebard = tk.Tk() treebard.style = "toplevel" treebard.title("Treebard GPS")
for i in range(1, 3): tree = make_family_tree() tree.title(f"Family Tree #{i}")
configall(treebard, formats) treebard.mainloop()
|
|
|
Post by Uncle Buddy on May 30, 2023 2:09:42 GMT -8
combobox design learning: #1) truncate the characters in the overly long dropdown items to size the dropdown, then detect that size and make the content frame and scrollregion match it; if the buttons don't stretch all the way to the right edge of the dropdown, it's because the entry_width is too large for the items, make it less wide till the buttons stretch all the way to the scrollbar, #2) currently trying a single combobox dropdown window for all the comboboxes in one family_tree ie main toplevel window for a group of related data; this way there is no code needed to close other dropdowns when one opens so also no need to create, populate and maintain a collection of existing comboboxes. There's only one dropdown in existence for each group of related data such as a family tree, that single dropdown is just hidden, never destroyed, it is an attribute of the family tree (the main toplevel window in a group of related data) and not the combobox class.
|
|