-
-
Notifications
You must be signed in to change notification settings - Fork 34.7k
bpo-42305: Added Auto_Complete DropBox Suggestion For Tkinter #23210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b2a0ab5
dc8f9b0
0f983c4
97e7a45
3f49b53
0a77559
a172352
6baac5d
cd897b9
3397c1f
4adf733
8b41f0b
16e7ba8
c1ce6e0
df93569
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,155 @@ | ||||||
| try: | ||||||
| import tkinter as tk | ||||||
| from tkinter import ttk | ||||||
| except ImportError: | ||||||
| # Python 2 | ||||||
| import Tkinter as tk | ||||||
| import ttk | ||||||
|
|
||||||
| __all__ = ["AutocompleteEntry"] | ||||||
|
|
||||||
| NO_RESULTS_MESSAGE = "No results found for '{}'" | ||||||
|
|
||||||
|
|
||||||
| class AutocompleteEntry(tk.Frame, object): | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
|
||||||
| """A container for `tk.Entry` and `tk.Listbox` widgets. | ||||||
| An instance of AutocompleteEntry is actually a `tk.Frame`, | ||||||
| containing the `tk.Entry` and `tk.Listbox` widgets needed | ||||||
| to display autocompletion entries. Thus, you can initialize | ||||||
| it with the usual arguments to `tk.Frame`. | ||||||
| Constants: | ||||||
| LISTBOX_HEIGHT -- Default height for the `tk.Listbox` widget | ||||||
| LISTBOX_WIDTH -- Default width for the `tk.Listbox` widget | ||||||
| ENTRY_WIDTH -- Default width for the `tk.Entry` widget | ||||||
| Methods: | ||||||
| __init__ -- Set up the `tk.Listbox` and `tk.Entry` widgets | ||||||
| build -- Build a list of autocompletion entries | ||||||
| _update_autocomplete -- Internal method | ||||||
| _select_entry -- Internal method | ||||||
| _cycle_up -- Internal method | ||||||
| _cycle_down -- Internal method | ||||||
|
Comment on lines
+27
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't bother listing internal methods in the docstring (users are almost always just interested in the public API)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just don't mention _update_autocomplete, _select_entry, _cycle_up & _cycle_down here |
||||||
| Other attributes: | ||||||
| text -- StringVar object associated with the `tk.Entry` widget | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO, this should be private |
||||||
| entry -- The `tk.Entry` widget (access this directly if you | ||||||
| need to change styling) | ||||||
| listbox -- The `tk.Listbox` widget (access this directly if | ||||||
| you need to change styling) | ||||||
| """ | ||||||
| LISTBOX_HEIGHT = 5 | ||||||
| LISTBOX_WIDTH = 25 | ||||||
| ENTRY_WIDTH = 25 | ||||||
|
Comment on lines
+38
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better to specify these in the kwargs
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The user should be able to give these as keyword arguments to |
||||||
|
|
||||||
| def __init__(self, master, *args, **kwargs): | ||||||
| """Constructor. | ||||||
| Create the `self.entry` and `self.listbox` widgets. | ||||||
| Note that these widgets are not yet displayed and will only | ||||||
| be visible when you call `self.build`. | ||||||
| Arguments: | ||||||
| master -- The master tkinter widget | ||||||
| Returns: | ||||||
| None | ||||||
| """ | ||||||
| super(AutocompleteEntry, self).__init__(*args, **kwargs) | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
|
||||||
| self.text = tk.StringVar() | ||||||
| self.entry = tk.Entry( | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
|
||||||
| self, | ||||||
| textvariable=self.text, | ||||||
| width=self.ENTRY_WIDTH | ||||||
| ) | ||||||
| self.listbox = tk.Listbox( | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
|
||||||
| self, | ||||||
| height=self.LISTBOX_HEIGHT, | ||||||
| width=self.LISTBOX_WIDTH | ||||||
| ) | ||||||
|
|
||||||
| def build( | ||||||
| self, | ||||||
| entries, | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
|
||||||
| max_entries=5, | ||||||
| case_sensitive=False, | ||||||
| no_results_message=NO_RESULTS_MESSAGE | ||||||
| ): | ||||||
| """Set up the autocompletion settings. | ||||||
| Binds <KeyRelease>, <<ListboxSelect>>, <Down> and <Up> for | ||||||
| smooth cycling between autocompletion entries. | ||||||
| Arguments: | ||||||
| entries -- An iterable containg autocompletion entries (strings) | ||||||
| max_entries -- [int] The maximum number of entries to display | ||||||
| case_sensitive -- [bool] Set to `True` to make autocompletion | ||||||
| case-sensitive | ||||||
| no_results_message -- [str] Message to display when no entries | ||||||
| match the current entry; you can use a | ||||||
| formatting identifier '{}' which will be | ||||||
| replaced with the entry at runtime | ||||||
| Returns: | ||||||
| None | ||||||
| """ | ||||||
| if not case_sensitive: | ||||||
| entries = [entry.lower() for entry in entries] | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we sorted the list of entries and used |
||||||
|
|
||||||
| self._case_sensitive = case_sensitive | ||||||
| self._entries = entries | ||||||
| self._no_results_message = no_results_message | ||||||
| self._listbox_height = max_entries | ||||||
|
Comment on lines
+90
to
+93
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add some checks to ensure these values are what we want (e.g. |
||||||
|
|
||||||
| self.entry.bind("<KeyRelease>", self._update_autocomplete) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not trace
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
_update_autocomplete will need updating because there will now be 3 arguments rather than just 'event' |
||||||
| self.entry.focus() | ||||||
| self.entry.grid(column=0, row=0) | ||||||
|
|
||||||
| self.listbox.bind("<<ListboxSelect>>", self._select_entry) | ||||||
| self.listbox.grid(column=0, row=1) | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
|
||||||
| self.listbox.grid_forget() | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
|
||||||
| # Initially, the listbox widget doesn't show up. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment should be above
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move this line one up |
||||||
|
|
||||||
| def _update_autocomplete(self, event): | ||||||
| """Internal method. | ||||||
| Update `self.listbox` to display new matches. | ||||||
| """ | ||||||
| self.listbox.delete(0, tk.END) | ||||||
| self.listbox["height"] = self._listbox_height | ||||||
|
|
||||||
| text = self.text.get() | ||||||
| if not self._case_sensitive: | ||||||
| text = text.lower() | ||||||
| if not text: | ||||||
| self.listbox.grid_forget() | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we just return at this point? |
||||||
| else: | ||||||
| for entry in self._entries: | ||||||
| if text in entry.strip(): | ||||||
| self.listbox.insert(tk.END, entry) | ||||||
|
|
||||||
| listbox_size = self.listbox.size() | ||||||
| if not listbox_size: | ||||||
| if self._no_results_message is None: | ||||||
| self.listbox.grid_forget() | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
|
||||||
| else: | ||||||
| try: | ||||||
| self.listbox.insert( | ||||||
| tk.END, | ||||||
| self._no_results_message.format(text) | ||||||
| ) | ||||||
| except UnicodeEncodeError: | ||||||
| self.listbox.insert( | ||||||
| tk.END, | ||||||
| self._no_results_message.format( | ||||||
| text.encode("utf-8") | ||||||
| ) | ||||||
| ) | ||||||
|
Comment on lines
+131
to
+137
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the above comment about validating |
||||||
| if listbox_size <= self.listbox["height"]: | ||||||
|
RajvirSingh1313 marked this conversation as resolved.
|
||||||
| # In case there's less entries than the maximum | ||||||
| # amount of entries allowed, resize the listbox. | ||||||
| self.listbox["height"] = listbox_size | ||||||
|
Comment on lines
+138
to
+141
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure about this. Personally, if I were using this widget, I would prefer it to stay the same size (so my window or other widgets didn't keep resizing). If you think this is helpful, I would make it an option. |
||||||
| self.listbox.grid() | ||||||
| else: | ||||||
| if listbox_size <= self.listbox["height"]: | ||||||
| self.listbox["height"] = listbox_size | ||||||
| self.listbox.grid() | ||||||
|
Comment on lines
+144
to
+146
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To reduce code duplication, please make this a new method (I think bringing it out of the |
||||||
|
|
||||||
| def _select_entry(self, event): | ||||||
| """Internal method. | ||||||
| Set the textvariable corresponding to `self.entry` | ||||||
| to the value currently selected. | ||||||
| """ | ||||||
| widget = event.widget | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will only ever be |
||||||
| value = widget.get(int(widget.curselection()[0])) | ||||||
| self.text.set(value) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| Tkinter Autocomplete | ||
| ==================== | ||
|
|
||
| Text autocompletion provides relevant real-time results to users. | ||
| Because tkinter does not provide a widget for adding autocompletion to | ||
| GUIs out of the box, I decided to make one myself. This utility is | ||
| compatible with and has been tested on Python 2.7.1 and Python 3.6.0. | ||
|
|
||
| Structure | ||
| ~~~~~~~~~ | ||
|
|
||
| NOTE: The ``Tkinter`` library for Python 2 and ``tkinter`` library for Python 3 will from now on be referred to as ``tk``. | ||
|
|
||
|
|
||
| The ``AutocompleteEntry`` class (which can be found | ||
| `here <https://github.com/RajvirSingh1313/Tkinter_Autocomplete_DropBox/blob/master/main.py>`__) | ||
| derives from ``tk.Frame`` and is a container used to group a | ||
| ``tk.Entry`` and ``tk.Listbox`` widget. Should you need to modify the | ||
| widgets, they can be accessed as (respectively) ``AutocompleteEntry`` s | ||
| ``entry`` and ``listbox`` attributes. | ||
|
|
||
| The entry widget acts like a normal textbox. When activated, it binds | ||
| ``<KeyRelease>`` to a private method which will update the list of | ||
| suggestions. The listbox widget contains the suggestions themselves. | ||
| When activated, it binds ``<<ListboxSelect>>`` to a private method which | ||
| sets the entry widget to whatever value was selected. | ||
|
|
||
| Since an instance of ``AutocompleteEntry`` is a ``tk.Frame`` instance | ||
| too, you can place it by calling its ``pack`` or ``grid`` methods with | ||
| their respective arguments. | ||
|
|
||
| Quickstart | ||
| ~~~~~~~~~~ | ||
|
|
||
| NOTE: These examples will only run under Python 3. To make them Python 2-compatible, replace ``tkinter`` with ``Tkinter``. | ||
|
|
||
|
|
||
| To add a new autocompletion frame to our interface, first initialize | ||
| one: | ||
|
|
||
| :: | ||
|
|
||
| import tkinter as tk | ||
|
|
||
| from tkinter import auto_complete | ||
|
|
||
| root = tk.Tk() | ||
|
|
||
| frame = tk.Frame(root) | ||
| frame.pack() | ||
|
|
||
| entry = auto_complete.AutocompleteEntry(frame) | ||
| # You can pass additional parameters to further customize the window; | ||
| # all parameters that you can pass to tk.Frame, are valid here too. | ||
|
|
||
| Now you need to configure the instance by passing it an iterable | ||
| containing all autocompletion entries. Do this by calling its ``build`` | ||
| method: | ||
|
|
||
| :: | ||
|
|
||
| ENTRIES = ( | ||
| "Foo", | ||
| "Bar" | ||
| ) | ||
|
|
||
| entry.build(ENTRIES) | ||
|
|
||
| You can pass additional arguments to ``build``: | ||
|
|
||
| * ``max_entries`` (integer): The maximum number of entries to display | ||
| at once. This value directly corresponds to the listbox widget's | ||
| ``height`` attribute. Defaults to ``5``. | ||
|
|
||
| * ``case_sensitive`` (boolean): If ``True``, only autocomplete entries | ||
| that enforce the same capitalization as the current entry will be | ||
| displayed. If ``False``, all autocomplete entries that match with the | ||
| current entry will be displayed. Defaults to ``False``. | ||
|
|
||
| * ``no_results_message`` (string or ``None``): The message to display | ||
| if no suggestions could be found for the current entry. This argument | ||
| may include a formatting identifier (``{}``) which, at runtime, gets | ||
| formatted as the current entry. If ``None`` is specified, the listbox | ||
| will instead be hidden until the next ``<KeyRelease>`` event. | ||
|
|
||
| Let's play around with these arguments: | ||
|
|
||
| :: | ||
|
|
||
| entry.build( | ||
| entries=ENTRIES, | ||
| no_results_message="< No results found for '{}' >" | ||
| # Note that this is formatted at runtime | ||
| ) | ||
|
|
||
| NOTE: You may call the ``build`` method multiple times on an instance of ``AutocompleteEntry``, to dynamically change the available suggestions. | ||
|
|
||
|
|
||
| With that out of the way, you can display ``entry``: | ||
|
|
||
| :: | ||
|
|
||
| entry.pack() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This module will only ever be on 3.10+ so this is redundant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will never be run on Python 2, so a check like this is not needed