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
RowandColumnobjects instead of clunky box layouts. - Component-Based Architecture: Use the
@componentdecorator 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 withstyle.add_style_dict. - Full Application Shell: Build professional applications with a declarative API for
MenuBar,ToolBar,StatusBar, andSystemTrayIcon. - Asynchronous Task Runner: Run long-running operations in the background without freezing your UI using the simple
@tasks.rundecorator. - Performance by Default: Includes an opt-in
@memodecorator 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.
- Widget Factory: Replace any default widget with your own custom implementation (e.g., C++ based) using
- 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().
- One-Way Binding: Automatically update your UI when your data changes with
- 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 (likeui.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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2af030d51a0de08e7a1165600d8dbbf6f4f45503b34c67bb49f3941a88172f65
|
|
| MD5 |
970aa49afc961b6a818ae852c3e237a5
|
|
| BLAKE2b-256 |
91bc53d1bdc000ef8cc9cc7faaef64e86175efe1854fbe2ead52fb56d360658b
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
93bbe860f56517570f4bfe898c69efaeb970cf3712769fd2906bcd9dbff49b37
|
|
| MD5 |
10f985d9e78f9efb69c6a361015c6777
|
|
| BLAKE2b-256 |
c981ca51039d351fd45611b8ff28472a8153102aba8bfd11ccc3d08ad5b2ceb9
|