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)

Traits System: Adding Behavior without Subclassing

While subclassing is great for creating new kinds of widgets, sometimes you just want to add a small, reusable piece of behavior to an existing widget—like making it draggable or giving it a right-click menu. This is where Traits come in.

Traits are modular behaviors that can be dynamically attached to any widget instance. WinUp comes with several built-in traits:

  • draggable: Makes a widget draggable within its parent.
  • context_menu: Adds a custom right-click context menu.
  • tooltip: A simple way to add a hover tooltip.
  • hover_effect: Applies a [hover="true"] style property on mouse-over, which you can target in your stylesheets (e.g., QPushButton[hover="true"]).
  • highlightable: Makes the text of a widget (like ui.Label) selectable by the user.

You can add a trait to any widget using winup.traits.add_trait().

# traits_demo.py
import winup
from winup import ui, traits

def App():
    # Let's create a simple label that we want to make interactive
    my_label = ui.Label(
        "I'm a draggable label with a context menu!",
        props={
            "padding": "15px",
            "background-color": "#f0f0f0",
            "border": "1px solid #ccc",
            "border-radius": "5px"
        }
    )

    # Add the draggable trait
    traits.add_trait(my_label, "draggable")

    # Add a context menu with a dictionary of actions
    traits.add_trait(my_label, "context_menu", items={
        "Say Hello": lambda: print("Hello from the context menu!"),
        "---": None, # This creates a separator
        "Reset Position": lambda: my_label.move(10, 10)
    })

    # The container needs a null layout for dragging to work relative to it
    return ui.Frame(
        children=[my_label],
        props={"layout": "null"}
    )

if __name__ == "__main__":
    winup.run(main_component=App, title="Traits Demo")

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.2.1.tar.gz (40.8 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.2.1-py3-none-any.whl (47.0 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for winup-1.2.1.tar.gz
Algorithm Hash digest
SHA256 2af030d51a0de08e7a1165600d8dbbf6f4f45503b34c67bb49f3941a88172f65
MD5 970aa49afc961b6a818ae852c3e237a5
BLAKE2b-256 91bc53d1bdc000ef8cc9cc7faaef64e86175efe1854fbe2ead52fb56d360658b

See more details on using hashes here.

File details

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

File metadata

  • Download URL: winup-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 47.0 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.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 93bbe860f56517570f4bfe898c69efaeb970cf3712769fd2906bcd9dbff49b37
MD5 10f985d9e78f9efb69c6a361015c6777
BLAKE2b-256 c981ca51039d351fd45611b8ff28472a8153102aba8bfd11ccc3d08ad5b2ceb9

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