1

I have the following problem. I created a desktop application to generate invoices for my dad's company. The machine I wrote this application for is an iMac from 2011 running macOS High Sierra (10.13.6). Because I have no Mac to test on and the age of the target system, I choose tkinter to implement the GUI mainly for its cross-platform compatibility (and because i taught that tkinter is kind of old so it has good chances to run on an old osx).

Now that my application is nearly finished and I had a chance to test it under OS X, I found a bug regarding the display of pages. My application has several pages that the user can select from. The pages are implemented as frames with the corresponding contents placed on top. For switching between pages, I wrote a widget called "pageSwitcher". This widget stores the current page object in a class variable (self.currentlyDisplayedPage), and if a page switch is triggered, .grid_remove() the current page, stores the page to switch to (new current page) into the variable, and displays the new current page via a .grid() call.


Issue stated here

Under Linux (Ubuntu 24.04.3 LTS), this works just fine. But under OSX (10.13.6), I get a different behavior. When I first switch to a page, it displays correctly. But if I try to switch to a page that was displayed before in the current runtime, it is just not displayed until I do one of the following:

If I click outside the application (desktop, for example), the page will be displayed correctly. Also, if I move the mouse over elements that have the <Enter> or <Leave> event binds, the page will display as soon as I move the mouse over them. Additionally, interactive elements like entries and buttons are working; as soon as I click into an entry or on a button, the page is displayed, and in the case of a button, the corresponding functionality is executed. As I see it, the new page is set, and all the widgets are there, but they are not shown until i perform one of the operations detailed above.

I did try to fix this by:

Using .update_idletasks() to force the page to be displayed. This made it sometimes better, sometimes worse (the page will display, but it can take minutes at worst). So I decided this is not a solution. I also tried to use a different Python version (from 3.12.12 to 3.14.2), hoping this was a bug in a specific version (which it seems it was not). I tested this under OSX Sequoia (15.7.3) with (sadly) the exact same results so i assume it is not a bug in a specific OSX version.

I do not know why this happens or how to fix it. Here is a minimal example that produces the error using empty (colored) frames as pages:

import tkinter as tk

class PageSwitcher(tk.Frame):
    def __init__(self, parent:tk, desiredHeight:int, desiredWidth:int) -> None:
        self.managedPages:list[tk.Frame] = []
        self.currentlyDisplayedPage:tk.Frame = None

        tk.Frame.__init__(self, parent, height=desiredHeight, width=desiredWidth)
        self.grid_propagate(False)
        self.rowconfigure(index=(0,2), weight=1)
        self.columnconfigure(index=(0,2), weight=1)

    def addPage(self, pageToAdd:tk.Frame) -> None:
        self.managedPages.append(pageToAdd)

    def displayPage(self, pageId:int) -> None:
        if self.currentlyDisplayedPage != None:
            self.currentlyDisplayedPage.grid_remove()
        self.currentlyDisplayedPage = self.managedPages[pageId]
        self.currentlyDisplayedPage.grid(row=1, column=1)

class PageSelector(tk.Frame):
    def __init__(self, parent:tk, desiredHeight:int, desiredWidth:int, pageSwitchFunction) -> None:
        self.pageSwitchFunction = pageSwitchFunction
        self.width = desiredWidth
        self.managedSelectors:list[PageSelector.Selector] = []

        tk.Frame.__init__(self, parent, height=desiredHeight, width=desiredWidth)
        self.grid_propagate(False)    

    def registerPage(self, pageName:str, pageId:int):
        selector = self.Selector(
            parent=self, 
            desiredWidth=self.width-10, #For 5px of padding on each side of selector
            name=pageName, 
            pageId=pageId, 
            selectFunction=self.select
        )
        selector.grid(row=self.grid_size()[1], column=0, padx=5, pady=2)
        self.managedSelectors.append(selector)

    def select(self, pageId) -> None:
        self.pageSwitchFunction(pageId) 
    
    class Selector(tk.Frame):
        def __init__(self, parent:tk, name:str, pageId:int, selectFunction, desiredWidth:int) -> None:
            self.pageId = pageId
            self.selectFunction = selectFunction

            tk.Frame.__init__(self, parent, height=32, width=desiredWidth, border=2, relief="raised")
            self.grid_propagate(False)

            self.name = tk.Label(self, text=name, font="Bold")
            self.name.grid(row=1, column=0, padx=5)

            self.bind("<Button-1>", self.select)
            self.name.bind("<Button-1>", self.select)

        def select(self, event:tk.Event) -> None:
            self.selectFunction(self.pageId)

def main():
    height = 600
    width = 600

    window = tk.Tk()
    window.geometry(f"{width}x{height}")

    pageSwitcher = PageSwitcher(window, height, width//2)
    pageSelector = PageSelector(window, height, width//2, pageSwitcher.displayPage)
    pageSwitcher.grid(row=0, column=1)
    pageSelector.grid(row=0, column=0)

    pageNamesAndColors = ["red", "orange", "yellow", "green", "lime", "lightBlue", "blue", "purple", "pink"]

    for index, color in enumerate(pageNamesAndColors):
        newPage = tk.Frame(pageSwitcher, height=height, width=width//2, bg=color)
        pageSwitcher.addPage(newPage)
        pageSelector.registerPage(pageName=f"{color} Page", pageId=index)

    window.mainloop()

main()

I use Python 3.14.2 with Tcl/Tk 8.6.17. I use the python and tkinter from macports.

I build a standalone ondir application using pyinstaller. But the error arises when running the code directly and when running the app build with pyinstaller.

If somebody knows a solution that does not need me to fix Tcl/Tk myself (I read somewhere that Tcl/Tk is bugged under OSX) or to rewrite my entire app in a different GUI framework that "I know works under OSX", please let me know.

9
  • Have your tried to use focus_force on the widget? Commented Jan 4 at 19:33
  • The code you posted is all to reproduce the issue. Have you verified it ? No threading, no call to update or some sort of loop or anything out of the ordinary ? Commented Jan 4 at 20:14
  • @Thingamabobs I tried '.focus_force()' and it works under Linux but not under MacOS. And yes the code i provided reproduces the issue. I verified it under osx 10.13.6. There are no calls to 'update()' and no threading. Commented Jan 5 at 9:00
  • Maybe you can use Qt? There's PySide6 binding for Python, works pretty good on macOS. Also supported by Nuitka (if you will look for AOT Python compilation). Commented Jan 5 at 9:09
  • 1
    As a workaround you might be able to add a dummy <Enter>/<Leave> events and then move the mouse programmatically to trigger those events hopefully temporarily fixing the issue Commented Jan 6 at 13:58

2 Answers 2

3

Thank you all for your suggestions. In the end, I opted for the solution proposed by @TheLizzard. I want to share what I did so people encountering this bug have a (hacky) solution at least. If it is against guidelines to post this as an answer, please let me know. So what I found out is the following: giving Frames an event bind for <Enter> or <Leave> does not help on its own. The binding has to trigger a configure on an element (only tested it with .configure(bg="color") so maybe other uses of configure do not work).

I created a widget like this:

import tkinter as tk

class Updater(tk.Frame):
    def __init__(self, parent, height, width)
        # ececec is the default frame color under OSX 10.13.6
        tk.Frame.__init__(self, parent, height=height, width=width, bg="#ececec")
        self.grid_propagate(False)
        self.bind("<Enter>", self.triggerDisplay)
        self.bind("<Leave>", self.triggerDisplay)

    def triggerDisplay(self, event):
        self.configure(bg="#ececec")

Then I put a 1x1 wide version of this widget onto every one of my pages in the topmost left corner. The effect is that the page will display instantly if the mouse pointer enters or leaves the topmost left corner (0,0). I then simply added a method to the class that handles page transitions, which moves the mouse pointer to (0,0), being the topmost left corner of the page, like this:

def moveToCorner(self):
    self.update_idletasks()
    self.event_generate("<Motion>", warp=True, x=0, y=0)

Lastly, I call moveToCorner() on every page transition, and my application displays pages like it should.

If the code contains typos, it's because I typed it by hand (because I could not copy it out of my VM).

Sign up to request clarification or add additional context in comments.

6 Comments

You might also want to save the old mouse position and reset it after the event_generate otherwise it might annoy users when their mouse moves on its own.
Good idea. But since there is still need to call configure it makes a strong case for a rendering issue by the operating system. I wonder why.
I agree that there is a rendering issue. I would look into it further but I have no MacOS devices. I also agree with your suggestion to file a bug report with the maintainers.
found it "That was the case in 8.6, but not in Tk 9. In 8,6 drawing operations which did not occur within a call to drawRect (which is called asynchronously by Apple) would need to be repeated during the next call to drawRect." source -- So we will have to wait for tkinter to upate, can take awhile I guess.
I tried to figure out how to save the old mouse position, but my research concluded that this is not possible in tkinter. If someone knows how to save the old mouse position, let me know.
Look at this. It uses winfo_pointerx() and winfo_rootx() to figure out the position of the mouse before the event_generate. After the event_generate that forcefully triggers the <Enter> binding, it moves the mouse to the saved coordinates. Note that event_generate("<Motion>", warp=True, x=..., y=...) doesn't work on some Linux devices but it works on Windows. I don't know if it will work on Mac.
2

The issue experienced here seems to be explained in an unrelated Ticket on tcl-lang.org.

Yes it uses a different strategy but the difference is not whether the
drawing is done directly to the screen.  The difference is *when* the
drawing occurs.  It now uses updateLayer, which means that drawing can
occur any time.  In 8.6 it used drawRect, which (since macOS 10.14) only
allowed drawing within the call to drawRect.

In another paragraph a maintainer wrote that:

That was the case in 8.6, but not in Tk 9. In 8,6 drawing operations which did not occur within a call to drawRect (which is called asynchronously by Apple) would need to be repeated during the next call to drawRect

They fixed it in tcl/tk 9.0 which has also breaking changes in the C-Api.
So tkinter is behind the development of tcl/tk. See the related issue on Github

The conclusion is that you didn't do anything wrong in your code and you have to work around the issue till pythons tkinter is ready to be updated.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.