0

I am working currently with the ttkbootstrap librairy instead of the ttk librairy in the past. I used to use the ttk DateEntry widget in the "readonly" state which worked perfectly fine. However, for some reasons i need to stop using the ttk librairy and instead use the ttkbootstrap one. So i created a ttkbootstrap DateEntry like said in the documentations: https://ttkbootstrap.readthedocs.io/en/latest/styleguide/dateentry/

import ttkbootstrap as tb
from ttkbootstrap import constants

root = tb.Window()
date_entry = tb.DateEntry(root)
date_entry.pack(padx = 10, pady = 10)
date_entry["state"] = "readonly"

root.mainloop()

The 'readonly' state in effective because i can't write with the keboard in the entry. Nevertheless when i try to pick a new date the date doesn't appear to change so that the entry will only be on today's date forever.

enter image description here

I know that this interaction is specifically linked to the ttkbootstrap librairy because the dateentry widget worked perfectly fine in the ttk librairy.

I tried to search in the source code of the DateEntry class but i found nothing that could explain this behavior. I am still sure that this isn't impossible to create a readonly DateEntry as it is one of the most important thing you need in this librairy.

3 Answers3

1

You shouldn't edit the code of ttkbootstrap. It would get overwritten at the next update.

You can simply create a new class inheriting from DateEntry, defining the state of the entry at init and enabling the field only when you press the button:

import ttkbootstrap as tb

class CustomDateEntry(tb.DateEntry):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.entry["state"] = "readonly"

    def _on_date_ask(self):
        self.entry["state"] = "normal"
        super()._on_date_ask()
        self.entry["state"] = "readonly"

root = tb.Window()
date_entry = CustomDateEntry(root)
date_entry.pack(padx = 10, pady = 10)

root.mainloop()

Edit: calling super()._on_date_ask() instead of writing a new function, as suggested by @acw1668.

Tranbi
  • 11,407
  • 6
  • 16
  • 33
0

The readonly state makes it so that the date can only be read and not changed/edited so it sets it to the current date, pretty self-explanatory I think, my Question would be why not just get rid of the read-only line since the user can still select the date through the calendar pop-up?

Trynabal
  • 21
  • 3
  • I'm am doing a very big project for my company in which i have to enter datas in a sql database and here i can't allow to have bad formated datas, it means that i can't let the user type their own date because he may chose the wrong format. Moreover the readonly state wasn't designed not to modify the entry via the application it was just made so that the user can't type in it i think – d'Elbreil Clément Aug 17 '23 at 08:39
  • 1
    Ah okay i understand why now but have a look at this link https://stackoverflow.com/questions/58581709/entry-widget-and-state-normal this post has a good description of all the states for tkinter entries, but it applies here aswell. readonly does not allow any user modification to the widget. – Trynabal Aug 17 '23 at 08:48
0

Okay i just went into the source code of the ttkbootstrap DateEntry and copy the class and also the Querybox class. To avoid this problem i explained i found a thing, you have to link the dateentry entry with a global textvariable. Then you can switch every entry.insert(...) with the textvariable.set(...). You also have to transform all the insert in the textvariable.get(...) in the copied Querybox class.

Here it fives the DateEntry class:

class DateEntry_test(ttk.Frame):
    """A date entry widget combines the `Combobox` and a `Button`
    with a callback attached to the `get_date` function.

    When pressed, a date chooser popup is displayed. The returned
    value is inserted into the combobox.

    The date chooser popup will use the date in the combobox as the
    date of focus if it is in the format specified by the
    `dateformat` parameter. By default, this format is "%Y-%m-%d".

    The bootstyle api may be used to change the style of the widget.
    The available colors include -> primary, secondary, success,
    info, warning, danger, light, dark.

    The starting weekday on the date chooser popup can be changed
    with the `firstweekday` parameter. By default this value is
    `6`, which represents "Sunday".

    The `Entry` and `Button` widgets are accessible from the
    `DateEntry.Entry` and `DateEntry.Button` properties.

    ![](../../assets/widgets/date-entry.png)
    """

    def __init__(
        self,
        master=None,
        dateformat=r"%x",
        firstweekday=6,
        startdate=None,
        bootstyle="",
        **kwargs,
    ):
        """
        Parameters:

            master (Widget, optional):
                The parent widget.

            dateformat (str, optional):
                The format string used to render the text in the entry
                widget. For more information on acceptable formats, see https://strftime.org/

            firstweekday (int, optional):
                Specifies the first day of the week. 0=Monday, 1=Tuesday,
                etc...

            startdate (datetime, optional):
                The date that is in focus when the widget is displayed. Default is
                current date.

            bootstyle (str, optional):
                A style keyword used to set the focus color of the entry
                and the background color of the date button. Available
                options include -> primary, secondary, success, info,
                warning, danger, dark, light.

            **kwargs (Dict[str, Any], optional):
                Other keyword arguments passed to the frame containing the
                entry and date button.
        """
        self._dateformat = dateformat
        self._firstweekday = firstweekday

        self._startdate = startdate or datetime.today()
        self._bootstyle = bootstyle
        super().__init__(master, **kwargs)

        # add visual components
        entry_kwargs = {"bootstyle": self._bootstyle}
        if "width" in kwargs:
            entry_kwargs["width"] = kwargs.pop("width")
        self.textvariable = tk.StringVar()
        self.entry = ttk.Entry(self, textvariable=self.textvariable, **entry_kwargs)
        
        self.entry.pack(side=tk.LEFT, fill=tk.X, expand=tk.YES)

        self.button = ttk.Button(
            master=self,
            command=self._on_date_ask,
            bootstyle=f"{self._bootstyle}-date",
        )
        self.button.pack(side=tk.LEFT)

        # starting value
        self.textvariable.set(self._startdate.strftime(self._dateformat))

    def __getitem__(self, key: str):
        return self.configure(cnf=key)

    def __setitem__(self, key: str, value):
        self.configure(cnf=None, **{key: value})

    def _configure_set(self, **kwargs):
        """Override configure method to allow for setting custom
        DateEntry parameters"""

        if "state" in kwargs:
            state = kwargs.pop("state")
            if state in ["readonly", "invalid"]:
                self.entry.configure(state=state)
            elif state in ("disabled", "normal"):
                self.entry.configure(state=state)
                self.button.configure(state=state)
            else:
                kwargs[state] = state
        if "dateformat" in kwargs:
            self._dateformat = kwargs.pop("dateformat")
        if "firstweekday" in kwargs:
            self._firstweekday = kwargs.pop("firstweekday")
        if "startdate" in kwargs:
            self._startdate = kwargs.pop("startdate")
        if "bootstyle" in kwargs:
            self._bootstyle = kwargs.pop("bootstyle")
            self.entry.configure(bootstyle=self._bootstyle)
            self.button.configure(bootstyle=[self._bootstyle, "date"])
        if "width" in kwargs:
            width = kwargs.pop("width")
            self.entry.configure(width=width)

        super(ttk.Frame, self).configure(**kwargs)

    def _configure_get(self, cnf):
        """Override the configure get method"""
        if cnf == "state":
            entrystate = self.entry.cget("state")
            buttonstate = self.button.cget("state")
            return {"Entry": entrystate, "Button": buttonstate}
        if cnf == "dateformat":
            return self._dateformat
        if cnf == "firstweekday":
            return self._firstweekday
        if cnf == "startdate":
            return self._startdate
        if cnf == "bootstyle":
            return self._bootstyle
        else:
            return super(ttk.Frame, self).configure(cnf=cnf)

    def configure(self, cnf=None, **kwargs):
        """Configure the options for this widget.

        Parameters:

            cnf (Dict[str, Any], optional):
                A dictionary of configuration options.

            **kwargs:
                Optional keyword arguments.
        """
        if cnf is not None:
            return self._configure_get(cnf)
        else:
            return self._configure_set(**kwargs)

    def _on_date_ask(self):
        """Callback for pushing the date button"""
        _val = self.entry.get() or datetime.today().strftime(self._dateformat)
        try:
            self._startdate = datetime.strptime(_val, self._dateformat)
        except Exception as e:
            print("Date entry text does not match", self._dateformat)
            self._startdate = datetime.today()
            self.entry.delete(first=0, last=tk.END)
            self.textvariable.set(self._startdate.strftime(self._dateformat))

        old_date = datetime.strptime(_val, self._dateformat)

        # get the new date and insert into the entry
        new_date = Querybox_test.get_date(
            parent=self.entry,
            startdate=old_date,
            firstweekday=self._firstweekday,
            bootstyle=self._bootstyle,
        )
        self.entry.delete(first=0, last=tk.END)
        self.textvariable.set(new_date.strftime(self._dateformat))
        self.entry.focus_force()

and the Querybox class:

class Querybox_test:
    """This class contains various static methods that request data
    from the end user."""

    @staticmethod
    def get_color(
        parent=None, title="Color Chooser", initialcolor=None, **kwargs
    ):
        """Show a color picker and return the select color when the
        user pressed OK.

        ![](../../assets/dialogs/querybox-get-color.png)

        Parameters:

            parent (Widget):
                The parent widget.

            title (str):
                Optional text that appears on the titlebar.

            initialcolor (str):
                The initial color to display in the 'Current' color
                frame.

        Returns:

            Tuple[rgb, hsl, hex]:
                The selected color in various colors models.
        """
        from ttkbootstrap.dialogs.colorchooser import ColorChooserDialog

        dialog = ColorChooserDialog(parent, title, initialcolor)
        if "position" in kwargs:
            position = kwargs.pop("position")
        else:
            position = None
        dialog.show(position)
        return dialog.result

    @staticmethod
    def get_date(
        parent=None,
        title=" ",
        firstweekday=6,
        startdate=None,
        bootstyle="primary",
    ):
        """Shows a calendar popup and returns the selection.

        ![](../../assets/dialogs/querybox-get-date.png)

        Parameters:

            parent (Widget):
                The parent widget; the popup will appear to the
                bottom-right of the parent widget. If no parent is
                provided, the widget is centered on the screen.

            title (str):
                The text that appears on the popup titlebar.

            firstweekday (int):
                Specifies the first day of the week. `0` is Monday, `6` is
                Sunday (the default).

            startdate (datetime):
                The date to be in focus when the widget is displayed;

            bootstyle (str):
                The following colors can be used to change the color of the
                title and hover / pressed color -> primary, secondary, info,
                warning, success, danger, light, dark.

        Returns:

            datetime:
                The date selected; the current date if no date is selected.
        """
        chooser = DatePickerDialog(
            parent=parent,
            title=title,
            firstweekday=firstweekday,
            startdate=startdate,
            bootstyle=bootstyle,
        )
        return chooser.date_selected

    @staticmethod
    def get_string(
        prompt="", title=" ", initialvalue=None, parent=None, **kwargs
    ):
        """Request a string type input from the user.

        ![](../../assets/dialogs/querybox-get-string.png)

        Parameters:

            prompt (str):
                A message to display in the message box above the entry
                widget.

            title (str):
                The string displayed as the title of the message box. This
                option is ignored on Mac OS X, where platform guidelines
                forbid the use of a title on this kind of dialog.

            initialvalue (Any):
                The initial value in the entry widget.

            parent (Widget):
                Makes the window the logical parent of the message box. The
                messagebox is displayed on top of its parent window.

            **kwargs (Dict):
                Other optional keyword arguments.

        Returns:

            str:
                The string value of the entry widget.
        """
        initialvalue = initialvalue or ""
        if "position" in kwargs:
            position = kwargs.pop("position")
        else:
            position = None
        dialog = QueryDialog(
            prompt, title, initialvalue, parent=parent, **kwargs
        )
        dialog.show(position)
        return dialog._result

    @staticmethod
    def get_integer(
        prompt="",
        title=" ",
        initialvalue=None,
        minvalue=None,
        maxvalue=None,
        parent=None,
        **kwargs,
    ):
        """Request an integer type input from the user.

        ![](../../assets/dialogs/querybox-get-integer.png)

        Parameters:

            prompt (str):
                A message to display in the message box above the entry
                widget.

            title (str):
                The string displayed as the title of the message box. This
                option is ignored on Mac OS X, where platform guidelines
                forbid the use of a title on this kind of dialog.

            initialvalue (int):
                The initial value in the entry widget.

            minvalue (int):
                The minimum allowed value.

            maxvalue (int):
                The maximum allowed value.

            parent (Widget):
                Makes the window the logical parent of the message box. The
                messagebox is displayed on top of its parent window.

            **kwargs (Dict):
                Other optional keyword arguments.

        Returns:

            int:
                The integer value of the entry widget.
        """
        initialvalue = initialvalue or ""
        if "position" in kwargs:
            position = kwargs.pop("position")
        else:
            position = None
        dialog = QueryDialog(
            prompt,
            title,
            initialvalue,
            minvalue,
            maxvalue,
            datatype=int,
            parent=parent,
            **kwargs,
        )
        dialog.show(position)
        return dialog._result

    @staticmethod
    def get_float(
        prompt="",
        title=" ",
        initialvalue=None,
        minvalue=None,
        maxvalue=None,
        parent=None,
        **kwargs,
    ):
        """Request a float type input from the user.

        ![](../../assets/dialogs/querybox-get-float.png)

        Parameters:

            prompt (str):
                A message to display in the message box above the entry
                widget.

            title (str):
                The string displayed as the title of the message box. This
                option is ignored on Mac OS X, where platform guidelines
                forbid the use of a title on this kind of dialog.

            initialvalue (float):
                The initial value in the entry widget.

            minvalue (float):
                The minimum allowed value.

            maxvalue (float):
                The maximum allowed value.

            parent (Widget):
                Makes the window the logical parent of the message box. The
                messagebox is displayed on top of its parent window.

            **kwargs (Dict):
                Other optional keyword arguments.

        Returns:

            float:
                The float value of the entry widget.
        """
        initialvalue = initialvalue or ""
        if "position" in kwargs:
            position = kwargs.pop("position")
        else:
            position = None
        dialog = QueryDialog(
            prompt,
            title,
            initialvalue,
            minvalue,
            maxvalue,
            datatype=float,
            parent=parent,
            **kwargs,
        )
        dialog.show(position)
        return dialog._result

    @staticmethod
    def get_font(parent=None, **kwargs):
        """Request a customized font

        ![](../../assets/dialogs/querybox-get-font.png)

        Parameters:

            parent (Widget):
                Makes the window the logical parent of the dialog box. The
                dialog is displayed on top of its parent window.

            **kwargs (Dict):
                Other keyword arguments.

        Returns:

            Font:
                A font object.
        """
        if "position" in kwargs:
            position = kwargs.pop("position")
        else:
            position = None
        dialog = FontDialog(parent=parent, **kwargs)
        dialog.show(position)
        return dialog.result