GithubHelp home page GithubHelp logo

insolor / async-tkinter-loop Goto Github PK

View Code? Open in Web Editor NEW
57.0 57.0 3.0 2.02 MB

Asynchronous mainloop implementation for tkinter. Makes it possible to use async functions as event handlers and widget commands.

Home Page: https://insolor.github.io/async-tkinter-loop/

License: MIT License

Python 87.39% HTML 12.61%
aiohttp asyncio customtkinter httpx tkinter

async-tkinter-loop's Introduction

Hi there ๐Ÿ‘‹

About me:

  • Current job (since Feb-Mar 2024): Python Backend Developer

    • Python 3.8+
    • PostgreSQL
    • MongoDB
    • GraphQL
  • Previous job (4+ years): Java Backend + Desktop Developer

    • Most used: Java 1.8, 11, 17
    • Java EE, Spring, etc. for server side
    • JavaFX, JFoenix for desktop
    • PostgreSQL databases
  • Hobby Python developer since 2012-2013

  • Current projects:

    • The Dwarf Fortress fan localization project

      • We develop tools for the localization and help enthisiasts with localization to different languages (there are about 20 languages with an active localization progress)
    • async-tkinter-loop module: https://github.com/insolor/async-tkinter-loop

      • This is a small library which helps to use asynchronous code with tkinter GUI library for Python
    • Trying to revive the pymorphy2 project: https://github.com/pymorphy2-fork

      • pymorphy2 is a morphological analyzer and an inflection engine for Russian, Ukrainian, Belarusian, and potentially it can be used for other slavic languages. Right now the original project is abandoned (see discussion), and I'm trying to continue it's development as a fork project.
  • Active stackoverflow.com contributor (mostly on ru.stackoverflow.com):

    Stack Exchange reputation

    Profile for insolor on Stack Exchange, a network of free, community-driven Q&A sites

  • Participated in the localization of such applications as Poedit, Spyder IDE, Cutter and others. See my profile at crowdin.com.

๐Ÿ“ˆ My GitHub Stats

insolor's GitHub Stats
Contributions

async-tkinter-loop's People

Contributors

dependabot[bot] avatar insolor avatar joaomoa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

jungseung joaomoa

async-tkinter-loop's Issues

best practise when I want to have a uvicorn server besides with tkinter gui

how to integrate this wonderful lib with my need

I want to start a fastapi server in the background for files upload task and have tkinter gui do other tasks.

and if I hit the quit menu in the system tray after close gui windows, it should exit both tkinter and fastapi server.

my code kinda of freezing tkinter though

import sys
import threading
from fastapi import FastAPI
from fastapi.responses import FileResponse
from tkinter import *

from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from pystray import MenuItem as item
import pystray
from PIL import Image, ImageTk
import asyncio
import tkinter as tk
from asyncio import CancelledError
from contextlib import suppress
import random
from async_tkinter_loop import async_handler, async_mainloop

app = FastAPI()
# Allow all origins
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # You can replace this with specific origins if needed
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


async def one_url(url):
    """One task."""
    print(f'run one_url: {url}')  # for debug
    sec = random.randint(1, 8)
    await asyncio.sleep(sec)
    return "url: {}\tsec: {}".format(url, sec)


async def do_urls():
    """Creating and starting 10 tasks."""
    tasks = [one_url(url) for url in range(10)]
    completed, pending = await asyncio.wait(tasks)
    results = [task.result() for task in completed]
    print("\n".join(results))


def do_tasks():
    """Button-Event-Handler starting the asyncio part."""
    asyncio.ensure_future(do_urls())


def start(lang, root=None):
    global mainwindow, canvas

    # root.resizable(width=True, height=True)
    root.iconbitmap("assets/icon.ico")
    root.title('tkinter asyncio demo')
    Button(master=root, text="Asyncio Tasks", command=async_handler(do_tasks())).pack()

    root.update_idletasks()



async def quit_window(icon, item):
    global loop, fastapi_thread

    print('Shutdown icon')
    icon.stop()

    print('Shutdown server')
    server.should_exit = True
    server.force_exit = True
    await server.shutdown()
    print('Shutdown root')
    root.quit()

    print('Stop loop')
    # loop.stop()




def show_window(icon, item):
    icon.stop()
    root.after(0, root.deiconify)


def withdraw_window():
    root.withdraw()
    image = Image.open("assets/icon.ico")
    # menu = (item("Quit", lambda icon, item: threading.Thread(target=quit_window, args=(icon, item)).start()),
    #         item("Show", show_window))
    menu = (item("Quit",async_handler(quit_window)),
            item("Show", show_window))

    
    icon = pystray.Icon("name", image, "title", menu)
    # icon.run_detached()
    icon.run()


def start_fastapi_server(loop):
    import uvicorn
    global server
    config = uvicorn.Config(app, loop=loop, host="0.0.0.0", port=8000)
    server = uvicorn.Server(config)
    try:
        loop.run_until_complete(server.serve())
    except KeyboardInterrupt:
        print("Received Ctrl+C. Stopping gracefully...")
        # Cancel all running tasks
        for task in asyncio.Task.all_tasks():
            task.cancel()
        # Optionally: Close any open resources (sockets, files, etc.)
        # Cleanup code here
    # finally:
    #     loop.close()


def start_tkinter_app():
    global root, settings, db, canvas, locale
    root = tk.Tk()

    locale = 'en'
    start(locale, root)

    root.protocol('WM_DELETE_WINDOW', withdraw_window)

    # root.mainloop()
    async_mainloop(root)


if __name__ == "__main__":
    global loop, fastapi_thread
    loop = None

    if sys.platform == "win32" and (3, 8, 0) <= sys.version_info < (3, 9, 0):
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())


    if sys.platform == 'win32':

        asyncio.get_event_loop().close()
        # On Windows, the default event loop is SelectorEventLoop, which does
        # not support subprocesses. ProactorEventLoop should be used instead.
        # Source: https://docs.python.org/3/library/asyncio-subprocess.html
        loop = asyncio.ProactorEventLoop()
        asyncio.set_event_loop(loop)
    else:
        loop = asyncio.get_event_loop()

    # Start FastAPI server in a separate thread
    fastapi_thread = threading.Thread(target=start_fastapi_server, args=(loop,)).start()

    start_tkinter_app()

Correct way to make cancellation

Hello! Thanks you for your helper!

What is the proper way to cancel the process when closing the Tkinter window?

For example,

root = Tk()
...

def on_closing(): 
    root.quit()
    root.destroy()

root.protocol('WM_DELETE_WINDOW', on_closing)

async_mainloop(root)

After I close the window , the window itself closes, but process is still not finished.

Can use in TaskGroup

Is this code OK?
I used main_loop() function in TaskGroup.

Code
from tkinter import *
from tkinter import messagebox
import asyncio
from async_tkinter_loop import async_handler,main_loop

ON_message = 'ON message'
OFF_message = 'OFF message'

async def get_ON(OnOff_que):
while True:
msg = await OnOff_que.get()
print(msg)

async def main():
OnOff_que = asyncio.Queue()

root = Tk()
root.title('My Home Temperature')
root.geometry("400x300")

@async_handler
async def on_info():
    res = messagebox.showinfo(title="message", message="ON Aircon")
    await OnOff_que.put(ON_message)
    print(res)
@async_handler
async def off_info():
    res = messagebox.showinfo(title="message", message="OFF Aircon")
    await OnOff_que.put(OFF_message)
    print(res)
            
Button(root, text='ON', command=on_info).grid()
Button(root, text='OFF', command=off_info).grid()
async with asyncio.TaskGroup() as tg:
    tsk0 = tg.create_task(main_loop(root))
    tsk1 = tg.create_task(get_ON(OnOff_que))

asyncio.run(main())

TODO: better documentation

  • added basic documentation using mkdocs and github pages
  • examples
  • mixins + CustomTkinter support
  • multilingual documentation
  • host on readthedocs instead of github pages?
  • ...

Invalid command name while executing in customtkinter project

I tried to set async-tkinter-loop for my project on customtkinter. I haven't added any async method yet. The project runs in VS Code while debugging, everything seems to work, but when I close the application, the following messages appear in the console:

Something like this:

invalid command name "139925939756800update"
    while executing
"139925939756800update"
    ("after" script)
invalid command name "140180485790400check_dpi_scaling"
    while executing
"140180485790400check_dpi_scaling"
    ("after" script)

or one of them:

invalid command name "139925939756800update"
    while executing
"139925939756800update"
    ("after" script)

What could be the problem? How critical is it?

Here is parts of my project:

main.py
if __name__ == "__main__":
    settings = get_settings()
    app = App(settings=settings)
    app.async_mainloop()
app.py
class App(ct.CTk, AsyncCTk):
    def __init__(self, settings: Settings):
        super().__init__()

        self.__settings = settings
        self.geometry(f"{1100}x{580}")
        self.title("App")

        self.minsize(700, 300)

        self.mainframe = MainFrame(self, settings=settings)
        self.mainframe.pack_configure(fill="both", expand=True)

    def update_all(self):
        updated_frame = MainFrame(self, settings=self.__settings)
        self.mainframe.destroy()
        self.mainframe = updated_frame
        self.mainframe.pack_configure(fill="both", expand=True)

    @property
    def settings(self) -> Settings:
        return self.__settings
mainframe.py
class AppProtocol(Protocol):
    def update():
        pass

class MainFrame(Page):
    
    def __init__(self, master: AppProtocol, settings: Settings, **kwargs):
        super().__init__(master, settings, **kwargs)

        self.master = master

        self.__pages: Dict[str, Page] = {}
        self.__default_page: str = None
        self.__current_page: str = None

        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)

        self.create_sidebar().grid_configure(row=0, column=0, sticky='wns')

        self.init_pages()

    def init_pages(self):
        self.add_page(SettingsPage(self, settings=self.settings))
        self.add_page(HomePage(self, settings=self.settings))
        self.set_default_page('SettingsPage')
        self.show_default_page()

    def create_sidebar(self) -> CTkFrame:
        self.sidebar = Sidebar(self, settings=self.settings, menu_btn_callback=self.on_click_menu_btn, new_macros_callback=self.show_screenshot_action)
        return self.sidebar
    
    def show_screenshot_action(self):
        self.action = ScreenshotAction(self.master)
        self.action.bind('<Escape>', lambda event: self.action.destroy())
        self.action.mainloop()
        
    
    def show_default_page(self):
        print(self.__default_page)
        self.__pages[self.__default_page].grid_configure(row=0, column=1, sticky='wsne')
        self.__current_page = self.__default_page
    
    def show_page(self, page_name: str):
        if self.__current_page == None:
            self.__pages[page_name].grid_configure(row=0, column=1, sticky='wsne')
            self.__current_page = page_name
        elif self.__current_page != page_name:
            self.__pages[self.__current_page].grid_remove()
            self.__pages[page_name].grid_configure(row=0, column=1, sticky='wsne')
            self.__current_page = page_name

    def hide_page(self, page_name: str):
        if self.__current_page == page_name:
            self.__pages[page_name].grid_remove()

    def add_page(self, page: Page) -> Page:
        self.__pages[page.__class__.__name__] = page

    def set_default_page(self, page_name: str):
        self.__default_page = page_name

    def on_click_menu_btn(self, page_name: str):
        self.show_page(page_name)
page.py
class Page(CTkFrame):
    def __init__(self, master: Any, settings: Settings, **kwargs):
        super().__init__(master, **kwargs)
        self.__settings = settings
        self.frame: CTkFrame = None
        self.__master = master

    @property
    def settings(self):
        return self.__settings
    
    def update_all(self):
        self.__master.update_all()

Dialogs (such as messagebox) block async mainloop

In the following code (pure tkinter) counter continues to work while messagebox dialog is shown:

import tkinter as tk
from tkinter import messagebox

root = tk.Tk()


def loop():
    counter.set(counter.get() + 1)
    root.after(1000, loop)


def show_message():
    messagebox.showinfo("Info", "Some info")


counter = tk.IntVar()
tk.Label(root, textvariable=counter).pack()

tk.Button(root, text="Start counter", command=loop).pack()
tk.Button(root, text="Show message", command=show_message).pack()

root.mainloop()

But with the corresponding code with async_tkinter_loop counter is paused when messagebox is shown:

import asyncio
import tkinter as tk
from tkinter import messagebox
from async_tkinter_loop import async_mainloop, async_handler

root = tk.Tk()


@async_handler
async def loop():
    while True:
        counter.set(counter.get() + 1)
        await asyncio.sleep(1.0)


def show_message():
    messagebox.showinfo("Info", "Some info")


counter = tk.IntVar()
tk.Label(root, textvariable=counter).pack()

tk.Button(root, text="Start counter", command=loop).pack()
tk.Button(root, text="Show message", command=show_message).pack()

async_mainloop(root)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.