Version 2 of A fullscreen toplevel

Updated 2004-09-21 22:55:49

I put this page together because I have been spending a lot of time trying to get a full screen toplevel working under Windows. I wanted to share the code I came up with in hopes that it will be of use to someone. When I say fullscreen, I am not talking about a window that has been maximized. Instead, I mean a window that has no borders and takes up the entire screen. The main problem was that I could not create a main window that had no borders and still had an icon in the Windows task list (accessed via Alt-Tab). In order to make it work, I ended up creating a overrideredirect main window and a fake second window that would appear in the task bar. This code handles those messy details.

    # This file contains code to implement a Windows style
    # toplevel window that can be set to fullscreen mode.

    set _wintop(debug) 0

    # Manage a given toplevel so that it can be made as
    # wide as the screen with no window borders.

    proc wintop_fullscreen_manage { t title } {
        global _wintop
        set _wintop(top) $t
        set _wintop(title) $title
        wm title $t $_wintop(title)

        wm protocol $t WM_DELETE_WINDOW {wintop_closed}

        bind $t <Destroy> {wintop_destroyed %W}
        bind $t <Configure> {wintop_configure %W %w %h}

        bind $t <Alt-Return> {wintop_toggle_full_screen}
        bind $t <KeyPress-Escape> {wintop_escape_full_screen}

        bind $t <FocusIn> {wintop_focusin %W}
        bind $t <Unmap> {wintop_unmap %W}

        set _wintop(fullscreen) 0
        set _wintop(fullscreen_iconic) 0

        wintop_set_icon $t

        return
    }

    proc wintop_set_icon { top } {
        #global _wintop
        #wm iconbitmap $top top.ico
    }

    proc wintop_configure { W w h } {
        global _wintop
        if {$W != $_wintop(top)} {
            # Ignore events for sub widgets
            return
        }
        #puts "wintop_configure $W $w $h"
        if {![info exists _wintop(first_mapping)]} {
            set _wintop(first_mapping) 1
            set _wintop(last_geom) [wm geom $_wintop(top)]
            set _wintop(top_width) [winfo width $_wintop(top)]
            set _wintop(top_height) [winfo height $_wintop(top)]
        }
        if {($w != $_wintop(top_width)) || ($h != $_wintop(top_height))} {
            set _wintop(top_width) $w
            set _wintop(top_height) $h
            wintop_resized $W $w $h
        }
    }

    proc wintop_resized { W w h } {
        global _wintop
        # Do stuff when window is resized!
        if {$_wintop(debug)} {
            puts "wintop_resized $w $h"
        }
    }

    # Invoked when the top window should be
    # closed down.

    proc wintop_closed { } {
        global _wintop

        # Invoke caller defined callback for close notification
        if {[info exists _wintop(closed_callback)]} {
            namespace eval :: $_wintop(closed_callback)
        }

        destroy $_wintop(top)
    }

    # Register cmd to invoke when wintop is closed

    proc wintop_closed_callback { cmd } {
        global _wintop
        set _wintop(closed_callback) $cmd
    }

    # Invoked when the top window has been
    # iconified via the wm (or wm command).

    proc wintop_iconified { } {
        global _wintop
        if {$_wintop(debug)} {
            puts "wintop_iconified"
        }

        if {$_wintop(fullscreen)} {
            # Switch to icon from full screen mode, we will
            # need to restore into full screen mode later on
            set _wintop(fullscreen_iconic) 1
            wintop_toggle_fake_win 0
            wm overrideredirect $_wintop(top) 0

            if {$_wintop(debug)} {
                puts "set fullscreen_iconic mode flag in wintop_iconified"
            }
            wm iconify $_wintop(top)
        }

        # FIXME: Add callback
    }

    # Invoked when the wintop widget is in the
    # process of being destroyed. Cleanup state,
    # and ignore DestroyNotify events for children.

    proc wintop_destroyed { W } {
        global _wintop
        if {$W == $_wintop(top)} {
            if {$_wintop(debug)} {
                puts "wintop_destroyed $W"
            }
            unset _wintop(first_mapping)
            set _wintop(fullscreen) 0
            set _wintop(last_geom) ""
        }
    }

    # Invoked when the main window gets focus, this happens
    # when the user clicks on the window, navigates to the
    # window via Alt-Tab, restores from a withdrawn state
    # and so on.

    proc wintop_focusin { W } {
        global _wintop

        if {$W != $_wintop(top)} {
            # Ignore events for sub widgets
            return
        }

        if {$_wintop(debug)} {
            puts "wintop_focusin"
        }

        # A focus in event can be delivered when the user clicks
        # on the icon in the taskbar, make sure to ignore it

        if {[wm state $_wintop(top)] == "iconic"} {
            return
        }

        # If the window was iconified while in full screen mode,
        # then return to full screen mode now

        if {$_wintop(fullscreen_iconic)} {
            if {$_wintop(debug)} {
                puts "restoring to full screen mode from iconic (mode is [wm state $_wintop(top)])"
            }

            set _wintop(fullscreen_iconic) 0

            wintop_toggle_fake_win 1

            wm overrideredirect $_wintop(top) 1
        } elseif {$_wintop(fullscreen)} {
            # Clicking on the fullscreen window followed by
            # an Alt-Tab should show the fake window as the
            # current application
            raise $_wintop(fake)
            raise $_wintop(top)
        }
    }

    proc wintop_unmap { W } {
        global _wintop
        if {$W != $_wintop(top)} {
            # Ignore events for sub widgets
            return
        }

        if {$_wintop(debug)} {
            puts "wintop_unmap (current state is [wm state $_wintop(top)])"
        }
        if {!$_wintop(fullscreen) && [wm state $_wintop(top)] == "iconic"} {
            wintop_iconified
        }
    }

    proc wintop_toggle_full_screen {} {
        global _wintop
        if {$_wintop(fullscreen)} {
            # In full screen mode, return to normal border state
            if {$_wintop(last_state) == "normal"} {
                if {$_wintop(debug)} {
                    puts "returning wintop to normal geom of $_wintop(last_geom)"
                }
                wm withdraw $_wintop(top)
                wm overrideredirect $_wintop(top) 0
                wm state $_wintop(top) normal
                wm geom $_wintop(top) $_wintop(last_geom)
                wm deiconify $_wintop(top)
            } elseif {$_wintop(last_state) == "zoomed"} {
                if {$_wintop(debug)} {
                    puts "returning wintop to zoomed state (last_geom is $_wintop(last_geom))"
                }
                wm state $_wintop(top) normal
                # Putting geom after overrideredirect causes 2 redraws
                wm geom $_wintop(top) $_wintop(last_geom)
                wm overrideredirect $_wintop(top) 0
                wm state $_wintop(top) zoomed
            } else {
                error "unknown last state \"$_wintop(last_state)\""
            }
            # Must set fullscreen before calling wintop_toggle_fake_win
            set _wintop(fullscreen) 0
            wintop_toggle_fake_win 0
        } else {
            if {[wm state $_wintop(top)] == "normal"} {
                set _wintop(last_state) "normal"
                set _wintop(last_geom) [wm geom $_wintop(top)]
            } elseif {[wm state $_wintop(top)] == "zoomed"} {
                set _wintop(last_state) "zoomed"
            } else {
                error "can't switch to full screen mode from \"[wm state $_wintop(top)]\""
            }
            wm overrideredirect $_wintop(top) 1
            wm state $_wintop(top) zoomed
            wintop_toggle_fake_win 1

            set _wintop(fullscreen) 1
        }
    }

    # Switch back to regular window mode if currently in full screen mode

    proc wintop_escape_full_screen { } {
        global _wintop
        if {$_wintop(fullscreen)} {
            wintop_toggle_full_screen
        }
    }

    # Enable/Disable a fake window that uses the same title bar
    # and icon but does not appear on the screen. When a user
    # Alt-Tab navigates to this window in full screen mode,
    # the zoomed toplevel will be raised and given the focus.

    proc wintop_toggle_fake_win { onoff } {
        global _wintop
        set _wintop(fake) .wtfake
        if {![winfo exists $_wintop(fake)]} {
            set t [toplevel $_wintop(fake) -width 100 -height 100 -bg red]

            wintop_set_icon $t

            wm geom $t +-10000+-10000
            wm title $t $_wintop(title)

            wm protocol $t WM_DELETE_WINDOW {wintop_fake_closed}
            bind $t <FocusIn> {wintop_fake_focusin}
            bind $t <Unmap> {wintop_fake_unmap}
            bind $t <Configure> {wintop_fake_configure}
        }
        if {$onoff} {
            wm deiconify $_wintop(fake)
            # Always set to zoomed mode, so that maximize is
            # not an available option in the taskbar.
            wm state $_wintop(fake) zoomed
            if {$_wintop(debug)} {
                puts "enabled fake win in zoomed off screen mode"
            }
        } else {
            # wm state $_wintop(fake) normal
            wm withdraw $_wintop(fake)
        }
    }

    proc wintop_fake_focusin { } {
        global _wintop

        if {$_wintop(debug)} {
            puts "wintop_fake_focusin"
        }

        # If the fake window is is normal mode (not zoomed)
        # then the user must have selected "restore" from
        # the taskbar, leave full screen mode in that case

        if {[wm state $_wintop(fake)] == "normal"} {
            if {$_wintop(debug)} {
                puts "not in zoomed state ([wm state $_wintop(fake)]), must restore to non-zoomed"
            }
            wintop_toggle_full_screen
        } else {
            focus -force $_wintop(top)
        }
    }

    proc wintop_fake_closed { } {
        global _wintop
        destroy $_wintop(fake)
        wintop_closed
    }

    proc wintop_fake_unmap { } {
        global _wintop
        if {$_wintop(debug)} {
            puts "wintop_fake_unmap"
        }
        if {$_wintop(fullscreen)} {
            wintop_iconified
        }
    }

    proc wintop_fake_configure { } {
        global _wintop
        if {$_wintop(debug)} {
            puts "wintop_fake_configure"
        }
    }

    # Here is the example code that uses the procs above

    wm withdraw .
    destroy .t
    set t [toplevel .t]
    wintop_fullscreen_manage $t "Example"
    canvas $t.c -borderwidth 0 -highlightthickness 0 -bg blue
    pack $t.c -fill both -expand true

    # Once created, one should be able to switch from regular to
    # full screen mode via Alt-Return.

While the code above worked for the most part, I have decided to give up on this approach for a couple of reasons. Lots of window resizing operations are visible to the end user and it makes the application look unprofessional. Also, when the application is busy, there are some moments where the fake window can be seen on screen. There are also real problems with the Windows virtual desktop where the fake window stays in one desktop while the real window is in another. All in all, I have decided to give up on this approach and focus on core Tk modifications to better support a full screen toplevel under Windows.