3

How to justify the values listed in drop-down part of a ttk.Combobox? I have tried justify='center' but that seems to only configure the selected item. Could use a resource link too if there is, I couldn't find it.

try:                        # In order to be able to import tkinter for
    import tkinter as tk    # either in python 2 or in python 3
    import tkinter.ttk as ttk
except ImportError:
    import Tkinter as tk
    import ttk


if __name__ == '__main__':
    root = tk.Tk()
    cbb = ttk.Combobox(root, justify='center', values=(0, 1, 2))
    cbb.pack()
    root.mainloop()
Nae
  • 14,209
  • 7
  • 52
  • 79

4 Answers4

5

I have a one-liner solution. Use the .option_add() method after declaring ttk.Combobox. Example:

cbb = ttk.Combobox(root, justify='center', values=(0, 1, 2)) # original
cbb.option_add('*TCombobox*Listbox.Justify', 'center')       # new line added
Sun Bear
  • 7,594
  • 11
  • 56
  • 102
3

(Edit: Note that this solution works for Tcl/Tk versions 8.6.5 and above. @CommonSense notes that some tkinter installations may not be patched yet, and this solution will not work).

In Tcl ( I don't know python, so one of the python people can edit the question).

A combobox is an amalgamation of an 'entry' widget and a 'listbox' widget. Sometimes to make the configuration changes you want, you need to access the internal widgets directly.

Tcl:

% ttk::combobox .cb -values [list a abc def14 kjsdf]
.cb
% pack .cb
% set pd [ttk::combobox::PopdownWindow .cb]
.cb.popdown
% set lb $pd.f.l
.cb.popdown.f.l
% $lb configure -justify center

Python:

cb = ttk.Combobox(value=['a', 'abc', 'def14', 'kjsdf'])

cb.pack()
pd = cb.tk.call('ttk::combobox::PopdownWindow', cb)

lb = cb.tk.eval('return {}.f.l'.format(pd))

cb.tk.eval('{} configure -justify center'.format(lb))

Some caveats. The internals of ttk::combobox are subject to change. Not likely, not anytime soon, but in the future, the hard-coded .f.l could change.

ttk::combobox::PopdownWindow will force the creation of the listbox when it is called. A better method is to put the centering adjustment into a procedure and call that procedure when the combobox/listbox is mapped.

This will run for all comboboxes, you will need to check the argument in the proc to make sure that this is the combobox you want to adjust.

proc cblbhandler { w } {
   if { $w eq ".cb" } {
     set pd [ttk::combobox::PopdownWindow $w]
     set lb $pd.f.l
     $lb configure -justify center
   }
}

bind ComboboxListbox <Map> +[list ::cblbhandler %W]
Nae
  • 14,209
  • 7
  • 52
  • 79
Brad Lanam
  • 5,192
  • 2
  • 19
  • 29
  • Thanks for this, I will try to provide a python way when I have time. – Nae Jan 09 '18 at 17:47
  • What does `.f.l` mean? – Nae Jan 10 '18 at 02:34
  • 1
    @Nae, `.f.l` means `frame.listbox` where `frame` is the popdown frame and `listbox` is the popdown listbox. As you can see from the code - popdown is actually a `Toplevel` window, with a `frame`, a `listbox` and a `scrollbar` (optionally). – CommonSense Jan 10 '18 at 09:47
  • While this approach is good in general, it's unacceptable for the ordinary python user, because of the `tkinter` version, which is `8.6` and which [has no](https://core.tcl.tk/tips/doc/trunk/tip/441.md) `-justify` option for the `Listbox` widget. For both `tcl` and `python` users such behaviour can be tested by adding the `catch` line like `if {[catch {$popdown.l configure -justify center} err]} {puts "${err} for the [winfo class $popdown.l]"}` to the end of `ttk::combobox::ConfigureListbox` function in the [source file](https://github.com/tcltk/tk/blob/master/library/ttk/combobox.tcl#L331). – CommonSense Jan 10 '18 at 12:02
  • 1
    @CommonSense, you have `$popdown.l`. Is that a typo (should be `$popdown.f.l`)? I tested with the latest version 8.6.8, and it works. The TIP you reference indicates that it was added in version 8.6.5. – Brad Lanam Jan 10 '18 at 15:56
  • That source code is copying configuration options from the combobox. ttk::combobox's `-justify` option is used for the entry widget, not the listbox widget. Use the code as I wrote it above. – Brad Lanam Jan 10 '18 at 16:01
  • 1
    @Brad Lanam, no, it's not the typo in the context of the `ttk::combobox::ConfigureListbox` function. On my side the `$err` is self-explanatory `unknown option: -justify` and the `[winfo class $popdown.l]` is a `Listbox`. All I want to say that the `tkinter` package (which is actually a `tcl`-bridge + `tk`) is a littlebit outdated (`8.6`) and, unfortunately, your solution wouldn't work. – CommonSense Jan 10 '18 at 16:45
  • 1
    @CommonSense, Right. If the Tk version you are using is less than 8.6.5, this solution will not work. The source code you are looking will not pass on a `-justify` option to the internal listbox in any version. The code as I wrote it must be used. – Brad Lanam Jan 10 '18 at 17:18
  • 1
    @Brad Lanam, I was a little self-assured in my claims and should to check my own `patchlevel` (which is `8.6.4`). However, I [`noticed`](https://ideone.com/WntOrL) that `tkinter` package is actually patched! So, excuse my previous words, your solution would be universal! Still, I think that you should mention that fact in your answer. Cheers! – CommonSense Jan 11 '18 at 14:25
  • I was able to translate the first part to python. – Nae Jan 29 '18 at 20:24
3

Here's one pure Python way that gets close to what you want. The items in the dropdown list all get justified to fit within the Combobox's width (or a default value will be used).

Update

Part of the reason the initial version of my answer wasn't quite right was because the code assumed that a fixed-width font was being used. That's not the case on my test platform at least, so I've modified the code to actually measure the width of values in pixels instead of whole characters, and do essentially what it did originally, but in those units of string-length measure.

import tkinter as tk
import tkinter.font as tkFont
from tkinter import ttk

class CenteredCombobox(ttk.Combobox):
    DEFAULT_WIDTH = 20  # Have read that 20 is the default width of an Entry.

    def __init__(self, master=None, **kwargs):
        values = kwargs.get('values')
        if values:
            entry = ttk.Entry(None)  # Throwaway for getting the default font.
            font = tkFont.Font(font=entry['font'])
            space_width = font.measure(' ')

            entry_width = space_width * kwargs.get('width', self.DEFAULT_WIDTH)
            widths = [font.measure(str(value)) for value in values]
            longest = max(entry_width, *widths)

            justified_values = []
            for value, value_width in zip(values, widths):
                space_needed = (longest-value_width) / 2
                spaces_needed = int(space_needed / space_width)
                padding = ' ' * spaces_needed
                justified_values.append(padding + str(value))

            kwargs['values'] = tuple(justified_values)

        super().__init__(master, **kwargs)


root = tk.Tk()
ccb = CenteredCombobox(root, justify='center', width=10, values=('I', 'XLII', 'MMXVIII'))
ccb.pack()

root.mainloop()
martineau
  • 119,623
  • 25
  • 170
  • 301
  • @Nae: The visual width of the `Entry` widget portion of a `ttk.Combobox` seems to include the width of the list dropdown menu (which is a little black downward-pointing arrow on Windows. You would probably need to add a fudge-factor in to account for this. – martineau Jan 10 '18 at 03:17
  • I think 1 revision early would be almost done if it removed spaces with a checkbox selected event, and have `kwargs['justify'] = 'center'`. Also `root.mainloop()` is gone again. – Nae Jan 10 '18 at 20:24
  • @Nae: Not sure what you mean by "checkbox selected event". This is a `Combobox` and doesn't have checkmarks. Also make sure you've tried the most current version which includes one calculation correction I made after the Update was initially put in. – martineau Jan 10 '18 at 21:29
  • I'm sorry, I meant combobox selected event*. That's the thing though, the latest 2 updates while making the selected value centrally justified, messes up the actual popdown central justification. At least on my end. Which is essentially what I began with using `justify='center'`. – Nae Jan 10 '18 at 21:42
  • `justify='center'` only affects the value shown above the dropdown menu. I'm not sure "combobox selected events" will do what you want. What event(s) were you going to bind? I've been playing with a prototype that uses the tracing abilities of a `tk.Stringvar()` control variable to strip leading whitespace from the value of the `Entry` portion whenever it gets changed. Things get a little involved because the tracing has to be temporarily suspended and then reinstated in order to prevent the callback from causing itself to be called again when _it_ changes the value—I can post it if you wish. – martineau Jan 10 '18 at 23:15
  • All I am saying is that for me the latest code you've provided while centering the values after selection, just like `justify='center'` would, does _not_ center the values in the actual drop-down, but 2 revision earlier code _did_ center values in drop-down but not the selected value, which was rather trivial to fix. As in one could've set `justify='center'` and define an event handler to remove additional space for `'<>'` virtual event. – Nae Jan 10 '18 at 23:20
0

After digging through combobox.tcl source code I've come up with the following subclass of ttk.Combobox. JustifiedCombobox justifies the pop-down list's items almost precisely after1 pop-down list's been first created & customized and then displayed. After the pop-down list's been created, setting self.justify value to a valid one will again, customize the justification almost right after the pop-down list's first been displayed. Enjoy:

try:                        # In order to be able to import tkinter for
    import tkinter as tk    # either in python 2 or in python 3
    from tkinter import ttk
except:
    import Tkinter as tk
    import ttk


class JustifiedCombobox(ttk.Combobox):
    """
    Creates a ttk.Combobox widget with its drop-down list items
    justified with self.justify as late as possible.
    """

    def __init__(self, master, *args, **kwargs):
        ttk.Combobox.__init__(self, master, *args, **kwargs)
        self.justify = 'center'


    def _justify_popdown_list_text(self):
        self._initial_bindtags = self.bindtags()
        _bindtags = list(self._initial_bindtags)
        _index_of_class_tag = _bindtags.index(self.winfo_class())
        # This dummy tag needs to be unique per object, and also needs
        # to be not equal to str(object)
        self._dummy_tag = '_' + str(self)
        _bindtags.insert(_index_of_class_tag + 1, self._dummy_tag)
        self.bindtags(tuple(_bindtags))
        _events_that_produce_popdown = tuple([  '<KeyPress-Down>',
                                                '<ButtonPress-1>',
                                                '<Shift-ButtonPress-1>',
                                                '<Double-ButtonPress-1>',
                                                '<Triple-ButtonPress-1>',
                                                ])
        for _event_name in _events_that_produce_popdown:
            self.bind_class(self._dummy_tag, _event_name,
                                                self._initial_event_handle)


    def _initial_event_handle(self, event):
        _instate = str(self['state'])
        if _instate != 'disabled':
            if event.keysym == 'Down':
                self._justify()
            else:
                _ = self.tk.eval('{} identify element {} {}'.format(self,
                                                            event.x, event.y))
                __ = self.tk.eval('string match *textarea {}'.format(_))
                _is_click_in_entry = bool(int(__))
                if (_instate == 'readonly') or (not _is_click_in_entry):
                    self._justify()


    def _justify(self):
        self.tk.eval('{}.popdown.f.l configure -justify {}'.format(self,
                                                                self.justify))
        self.bindtags(self._initial_bindtags)


    def __setattr__(self, name, value):
        self.__dict__[name] = value
        if name == 'justify':
            self._justify_popdown_list_text()


def select_handle():
    global a
    _selected = a['values'][a.current()]
    if _selected in ("left", "center", "right"):
        a.justify = _selected


if __name__ == '__main__':
    root = tk.Tk()
    for s in ('normal', 'readonly', 'disabled'):
        JustifiedCombobox(root, state=s, values=[1, 2, 3]).grid()
    a = JustifiedCombobox(root, values=["Justify me!", "left", "center", "right"])
    a.current(0)
    a.grid()
    a.bind("<<ComboboxSelected>>", lambda event: select_handle())
    root.mainloop()

1 It basically makes use of bindtag event queue. This was mostly possible thanks to being able to creating a custom bindtag.

Nae
  • 14,209
  • 7
  • 52
  • 79