Потому что под капотом большинства оконных фреймворков крутится вечный цикл вида "получить оконное сообщение - определить событие - вызвать обработчик события". И следующее оконное сообщение не будет обработано, пока обработчик события не завершится. Как следствие, длительный код в обработчике "подвешивает" окно программы.
Быстрое и грязное решение - дёргать метод update_idletasks() (погугли про него). Он позволит выполнить перерисовку компонентов GUI, не выходя из обработчика события.
Чуть менее грязное - разбить выполняемую работу на маленькие блоки, выполнять блок, затем планировать выполнение следующего блока через метод after(). Тогда в промежутке между вызовом after() и фактическим выполнением кода у GUI будет время обработать поступающие сообщения. Но не всегда это возможно, да и о конкуррентном запуске надо думать (что если юзер кликнет кнопку дважды, а не однажды?)
Ещё менее грязное - вынести длительный код в отдельный поток, научить поток оповещать о своём состоянии (например, менять содержимое переменной), а через after() реализовывать только опрос этой переменной и обновление GUI. Но переменную надо будет защищать мьютексом, чтобы не словить конфликт при одновременном обращении двух потоков.