Skip to main content

A powerful, .exe Desktop Application Python GUI framework built on top of PySide6.

Project description

WinUp 🚀

A ridiculously Pythonic and powerful framework for building beautiful desktop applications.

WinUp is a modern UI framework for Python that wraps the power of PySide6 (Qt) in a simple, declarative, and developer-friendly API. It's designed to let you build applications faster, write cleaner code, and enjoy the development process.


Why WinUp? (Instead of raw PySide6 or Tkinter)

Desktop development in Python can feel clunky. WinUp was built to fix that.

Feature WinUp Way ✨ Raw PySide6 / Tkinter Way 😟
Layouts ui.Column(children=[...]), ui.Row(children=[...]) QVBoxLayout(), QHBoxLayout(), layout.addWidget(), pack(), grid()
Styling props={"background-color": "blue", "font-size": "16px"} Manual QSS strings, widget.setStyleSheet(...), complex style objects.
State Management state.bind(widget, "prop", "key") Manual callback functions, getters/setters, StringVar(), boilerplate everywhere.
Two-Way Binding state.bind_two_way(input_widget, "key") Non-existent. Requires manual on_change handlers to update state and UI.
Developer Tools Built-in Hot Reloading, code profiler, and window tools out of the box. Non-existent. Restart the entire app for every single UI change.
Code Structure Reusable, self-contained components with @component. Often leads to large, monolithic classes or procedural scripts.

In short, WinUp provides the "killer features" of modern web frameworks (like React or Vue) for the desktop, saving you time and letting you focus on what matters: your application's logic.


Core Features

  • Declarative & Pythonic UI: Build complex layouts with simple Row and Column objects instead of clunky box layouts.
  • Component-Based Architecture: Use the @component decorator to create modular and reusable UI widgets from simple functions.
  • Powerful Styling System: Style your widgets with simple Python dictionaries using props. Create global "CSS-like" classes with style.add_style_dict.
  • Full Application Shell: Build professional applications with a declarative API for MenuBar, ToolBar, StatusBar, and SystemTrayIcon.
  • Asynchronous Task Runner: Run long-running operations in the background without freezing your UI using the simple @tasks.run decorator.
  • Performance by Default: Includes an opt-in @memo decorator to cache component renders and prevent needless re-computation.
  • Advanced Extensibility:
    • Widget Factory: Replace any default widget with your own custom implementation (e.g., C++ based) using ui.register_widget().
    • Multiple Windows: Create and manage multiple independent windows for complex applications like tool palettes or music players.
  • Reactive State Management:
    • One-Way Binding: Automatically update your UI when your data changes with state.bind().
    • Two-Way Binding: Effortlessly sync input widgets with your state using state.bind_two_way().
    • Subscriptions: Trigger any function in response to state changes with state.subscribe().
  • Developer-Friendly Tooling:
    • Hot Reloading: See your UI changes instantly without restarting your app.
    • Profiler: Easily measure the performance of any function with the @profiler.measure() decorator.
    • Window Tools: Center, flash, or manage your application window with ease.
  • Flexible Data Layer: Includes simple, consistent connectors for SQLite, PostgreSQL, MySQL, MongoDB, and Firebase.

Installation

pip install winup watchdog

The watchdog library is required for the Hot Reloading feature.

winup init

This makes an app template ready for use, if LoadUp doesn't work, use PyInstaller instead.


Getting Started: Hello, WinUp!

Creating an application is as simple as defining a main component and running it.

# hello_world.py
import winup
from winup import ui

# The @component decorator is optional for the main component, but good practice.
@winup.component
def App():
    """This is our main application component."""
    return ui.Column(
        props={
            "alignment": "AlignCenter", 
            "spacing": 20
        },
        children=[
            ui.Label("👋 Hello, WinUp!", props={"font-size": "24px"}),
            ui.Button("Click Me!", on_click=lambda: print("Button clicked!"))
        ]
    )

if __name__ == "__main__":
    winup.run(main_component=App, title="My First WinUp App")

Core Concepts

UI & Layouts

WinUp abstracts away Qt's manual layout system. You build UIs by composing Row and Column components.

def App():
    return ui.Column(  # Arranges children vertically
        children=[
            ui.Label("Top"),
            ui.Row(    # Arranges children horizontally
                children=[
                    ui.Button("Left"),
                    ui.Button("Right")
                ],
                props={"spacing": 10}
            ),
            ui.Label("Bottom")
        ],
        props={"spacing": 15, "margin": "20px"}
    )

Styling

You can style any widget by passing a props dictionary. Props can be CSS-like properties, or special keywords like class and id for use with a global stylesheet.

# Define global styles
winup.style.add_style_dict({
    ".btn-primary": {
        "background-color": "#007bff",
        "color": "white",
        "border-radius": "5px",
        "padding": "10px"
    },
    ".btn-primary:hover": {
        "background-color": "#0056b3"
    }
})

# Use the class in a component
def App():
    return ui.Button("Primary Button", props={"class": "btn-primary"})

Extending Widgets

WinUp allows you to replace any default widget with your own custom class. This is perfect for creating highly specialized components or for integrating widgets written in C++.

To do this, simply create a class that inherits from the widget you want to replace (or from a base Qt class) and then register it with the framework before you run your app.

# To subclass a default widget, you must import it directly
from winup.ui.widgets.button import Button as DefaultButton

# 1. Create your custom widget class
class BigRedButton(DefaultButton):
    def __init__(self, text: str, on_click: callable = None):
        # Define some custom props to make it unique
        custom_props = {
            "background-color": "red",
            "color": "white",
            "font-size": "20px",
            "font-weight": "bold",
            "padding": "15px",
        }
        super().__init__(text=text, on_click=on_click, props=custom_props)

# In your main script:
if __name__ == "__main__":
    # 2. Register your custom class to override the default "Button"
    ui.register_widget("Button", BigRedButton)
    
    # 3. Now, every call to ui.Button() will create a BigRedButton instead!
    def App():
        return ui.Button("I am a custom button!")

    winup.run(main_component=App)

State Management

WinUp's global state object is the single source of truth for your application's data.

1. One-Way Binding (bind)

The UI property updates automatically when state.set() is called.

# one_way_demo.py
import winup
from winup import ui

winup.state.set("counter", 0)

def App():
    # The label's 'text' property will be kept in sync with the 'counter' state key.
    label = ui.Label(f"Initial Value: {winup.state.get('counter')}")
    winup.state.bind(label, "text", "counter")

    def increment():
        winup.state.set("counter", winup.state.get("counter") + 1)

    return ui.Column(children=[
        label,
        ui.Button("Increment", on_click=increment)
    ])

2. Two-Way Binding (bind_two_way)

The UI updates the state, and the state updates the UI. This is perfect for forms.

# two_way_demo.py
import winup
from winup import ui

winup.state.set("username", "Guest")

def App():
    # This input is two-way bound to 'username'. Typing in the field
    # immediately updates the state.
    name_input = ui.Input()
    winup.state.bind_two_way(name_input, "username")
    
    # This label is one-way bound and will update as you type.
    greeting = ui.Label()
    winup.state.bind(greeting, "text", "username")

    return ui.Column(children=[ui.Label("Enter your name:"), name_input, greeting])

3. Subscriptions (subscribe)

For more complex reactions to state changes, like formatting data or triggering other logic, use subscribe.

# subscribe_demo.py
import winup
from winup import ui

winup.state.set("username", "Guest")

def App():
    greeting = ui.Label()

    # This function runs every time the 'username' state changes.
    def update_greeting(new_name):
        greeting.set_text(f"Hello, {new_name.upper()}!")
    
    winup.state.subscribe("username", update_greeting)
    
    # We still need a way to change the state.
    name_input = ui.Input()
    winup.state.bind_two_way(name_input, "username")

    return ui.Column(children=[name_input, greeting])

Multiple Windows

You are not limited to a single window. The winup.Window class lets you spawn new, independent windows at any time. This is ideal for things like settings dialogs, tool palettes, or mini-player controls.

The new window will have its own component and run in the same application event loop.

import winup
from winup import ui

def MiniPlayerComponent():
    """A simple component for the new window."""
    return ui.Label("I'm a mini-player window!")

def open_mini_player():
    """Event handler to create and show the new window."""
    player_component = MiniPlayerComponent()
    # This creates and shows the new window instantly
    winup.Window(
        component=player_component, 
        title="Mini Player", 
        width=250, 
        height=100
    )

def App():
    """The main app component."""
    return ui.Button("Open Player", on_click=open_mini_player)

if __name__ == "__main__":
    winup.run(main_component=App)

Application Shell

WinUp provides simple, declarative classes to build the shell of a professional application. You can define menus, toolbars, and status bars and pass them directly to the winup.run() function.

import winup
from winup import ui, shell

# 1. Define handlers for your actions
def on_new(): print("Action: New")
def on_quit(): winup.core.window._winup_app.app.quit()
def on_about(): winup.ui.dialogs.show_message("About", "WinUp Shell Demo")

# 2. Define the shell components
app_menu = shell.MenuBar({
    "&File": { "New": on_new, "---": None, "Quit": on_quit },
    "&Help": { "About": on_about }
})

app_toolbar = shell.ToolBar({ "New": on_new }) # Add icons via the icon_dir argument
app_statusbar = shell.StatusBar()

# 3. Create your main component
def App():
    # The status bar is globally accessible after creation
    shell.StatusBar.show_message("Welcome to WinUp!", 5000)
    return ui.Label("App with a full shell!")

# 4. Pass the shell components to the run function
if __name__ == "__main__":
    winup.run(
        main_component=App,
        title="App Shell Demo",
        menu_bar=app_menu,
        tool_bar=app_toolbar,
        status_bar=app_statusbar
    )

You can also add a shell.SystemTrayIcon for applications that need to run in the background.

Asynchronous Tasks

Never freeze your UI again. The @tasks.run decorator makes it trivial to run any function on a background thread, with callbacks for success or failure.

import time
from winup import shell, tasks

def on_task_complete(result):
    """This function is called on the main UI thread when the task succeeds."""
    print(f"Success! Result: {result}")
    shell.StatusBar.show_message(f"Task finished: {result}", 4000)

def on_task_error(error_details):
    """This function is called on the main UI thread if the task fails."""
    exception, trace = error_details
    print(f"Error in background task: {exception}")
    shell.StatusBar.show_message(f"Error: {exception}", 4000)

@tasks.run(on_finish=on_task_complete, on_error=on_task_error)
def fetch_data_from_server(url: str):
    """
    A simulated long-running task. This will not block the UI.
    The decorator will pass its return value to 'on_finish'.
    """
    print("Starting background task...")
    shell.StatusBar.show_message("Fetching data...")
    time.sleep(2) # Simulate network latency
    if "fail" in url:
        raise ConnectionError("Could not connect to server.")
    return f"Data from {url}"

# You can now call this function from any event handler (e.g., a button click)
# fetch_data_from_server("my-api.com/data")
# fetch_data_from_server("my-api.com/fail")

Developer Tools

Hot Reloading: To enable hot reloading, you manually start a watcher that calls a reload function. This gives you precise control over what gets reloaded.

# hot_reload_example.py
import winup
from winup import ui
from winup.core import hot_reload

# 1. Define your component(s) in a separate file (e.g., components.py)
#
# --- components.py ---
# from winup import ui
# def MyComponent():
#     return ui.Label("Version 1 of my component")
# ---------------------

# 2. In your main app file, create a placeholder and a reload function
app_container = ui.Frame() # A container to hold the component

def reload_ui():
    """This function clears the container and re-imports the component."""
    hot_reload.clear_layout(app_container.layout())
    # The reloader invalidates Python's import cache
    from components import MyComponent 
    app_container.add_child(MyComponent())
    print("UI Reloaded!")

if __name__ == "__main__":
    # 3. Start the hot reloader before running the app
    # It will watch 'components.py' and call 'reload_ui' when it changes.
    reloader = hot_reload.FileChangeReloader('components.py', reload_ui)
    reloader.start()

    # 4. Run the app with the container, and load the initial UI
    reload_ui() # Initial load
    winup.run(main_component=lambda: app_container, title="Hot Reload App")

This setup allows you to see UI changes instantly just by saving your component file.

Performance & Memoization: For UIs that render large amounts of data, you can significantly improve performance by caching component results. The @winup.memo decorator automatically caches the widget created by a component. If the component is called again with the same arguments, the cached widget is returned instantly instead of being re-created.

import winup
from winup import ui

# By adding @winup.memo, this component will only be re-created
# if the 'color' argument changes.
@winup.memo
def ColorBlock(color: str):
    return ui.Frame(props={"background-color": color, "min-height": "20px"})

def App():
    # In this list, ColorBlock('#AABBCC') will only be called once.
    # The framework will then reuse the cached widget for the other two instances.
    return ui.Column(children=[
        ColorBlock(color="#AABBCC"),
        ColorBlock(color="#EEEEEE"),
        ColorBlock(color="#AABBCC"),
        ColorBlock(color="#AABBCC"),
    ])

Profiler: Simply add the @profiler.measure() decorator to any function to measure its execution time. Results are printed to the console when the application closes. The profiler also automatically tracks the performance of the memoization cache, showing you hits, misses, and the overall hit ratio.

from winup.tools import profiler

@profiler.measure
def some_expensive_function():
    # ... code to measure ...
    import time
    time.sleep(1)

Contributing

WinUp is an open-source project. Contributions are welcome!

License

This project is licensed under the Apache 2.0 License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

winup-1.1.1.tar.gz (34.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

winup-1.1.1-py3-none-any.whl (39.4 kB view details)

Uploaded Python 3

File details

Details for the file winup-1.1.1.tar.gz.

File metadata

  • Download URL: winup-1.1.1.tar.gz
  • Upload date:
  • Size: 34.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.5

File hashes

Hashes for winup-1.1.1.tar.gz
Algorithm Hash digest
SHA256 d5da1c04e385e297a225ceab99077597aa2cb11907025ede92227ad1f86f7772
MD5 4b00544be8265dd169feac81796882b1
BLAKE2b-256 9a4f734d76fcf965cfbf072aabe3a504613e0d6c26d537cf9a43e6c2dd55bd53

See more details on using hashes here.

File details

Details for the file winup-1.1.1-py3-none-any.whl.

File metadata

  • Download URL: winup-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 39.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.5

File hashes

Hashes for winup-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 bc3c33754f782b0a78cf0b38703ab86f33742c5a2041487a895276877a9ce228
MD5 8a52df99f4ddc330e74892e1ac557ccd
BLAKE2b-256 f9b3922df00db6f403a93d2fb1386f6331e0a192ef7330dbb8c0384fe6cbb95d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page