Quickstart¶
This page takes you through concepts of Quasargui.
If you haven't yet installed Quasargui, you can install Quasargui by
pip install quasargui
Now, you're ready to run our first example.
The simplest window¶
The simplest possible window only takes 3 lines of code.
from quasargui import *
layout = Rows(children=['This is the', '<i>Hello, world!</i>', 'window.'])
run(layout, title='The simplest', size=(200, 100))
The important parts of the code:
run
runs the GUI - and the program stays at this line until all the windows are closed.run
always takes aComponent
, the main component to display. In this case the component isRows
.children
can be a list ofComponent
's andstr
's and some other types that we'll discuss later.- The
children
strings accept html code, so simple formats such as<i>
and<b>
can be comfortably written directly. For more complex cases, use components such asLink
,Heading
, etc.
Let's move on to a more advanced code that also can be used as a boilerplate for your app.
Starter code¶
To quickly get a nice window layout, you can use the code of the window below.
"""
This is a starter template.
"""
import quasargui
from quasargui import *
loading = Model(True)
def set_loaded():
loading.value = False
layout = QLayout([
QHeader([
QToolbar([QToolbarTitle([
QIcon('ramen_dining', 'lg', classes='q-mx-md'),
'Your Program Title'
])])
]),
QDrawer([
'<b>'
'Your drawer'
'</b>'
'<div class="q-mt-md">'
'for your parameters.'
'</div>'
]),
QDrawer(side=QDrawer.RIGHT, show=False, children=[
'<b>'
'Your right drawer.'
'</b>'
'<div class="q-mt-md">'
'If you delete a drawer,its sandwich menu disappears'
'</div>'
]),
QPage([
'Here comes the contents of your QPage'
]),
v_show(
loading,
QFooter(children=[
'Here is your footer that is only displayed if loading.value == True',
QButton('ok', events={'click': toggle(loading)})
])
)
])
quasargui.run(layout, title='Program title')
The things you need to know about this window:
QLayout
, QDrawer
, etc. are all Component
s that correspond to Vue components. In particular, we use Quasar's professional-looking components - hence the name Quasargui. All the components you see on the Quasar documentation page, are imported into Quasargui, wrapped into Component
's.
The first parameter of a Component
is children
, so you don't have to type constantly children=
.
Note that QButton
's first parameter is not children but label
since typically you want to add a label to a button and not children (which is also still possible of course...).
Quasargui uses Python's typing system, so if you use an IDE you can always check with typing system if you're using Quasargui correctly.
To make changes on the window, we use Model
's, like loading
in this example. A Model
provides "two-way binding" between your code and the GUI. This means that every change to a Model
's value is reflected in the GUI and every user input changes the Model
's value.
Debugging¶
To get confident with any system, it is best to know how to debug it.
With Quasargui you have an extra debugging option: if you enable debug=True
in run()
, you can see a browser inspector if you right-click and choose inspect. If you would lose the title bar of the window (as we observed it in Mac), you can just go to fullscreen and exit fullscreen and vois-lĂĄ, you got your titlebar back. Luckily, we haven't observed the titlebar disappear in production so far.
If you want even more detailed debugging, also set _render_debug=True
in run()
.
Vue vs. Quasargui¶
If you have experience with Vue, this section shows you how concepts in Vue are transferred to Quasargui.
The transfer from Vue is natural since under the hood Quasargui builds up a Vue/Quasar webpage and interacts with it through pywebview's api.
For example
ok_label = Model('ok')
Div(children=[
'text',
QButton(props={'label': ok_label})
])
<div>
text
<q-button :label='data[1]' />
</div>
So, if you look into quasar's documentation, all Vue components have their Component
counterpart in Quasargui, with the same name. As an example, <q-button/>
corresponds to QButton
.
Vue components to Component parameters¶
Among a Component
's constructor's parameters,
children
corresponds to html-style children.props
corresponds toprops
(eg.label="ok"
,:label="data[1]"
),events
corresponds to Vue events (eg.@click
)classes
corresponds toclass
attribute.style
corresponds tostyle
attribute.
Additionally, we have defined some shorthand parameters for frequently used props (such as label
in case of QButton).
Directives are defined as functions. v_show
and other v_*
functions correspond to v-*
directives. Only v-model
gets special treatment, it is always accessible as the model
parameter of a ComponentWithModel
.
Any Model
corresponds to Vue data that is defined on the Vue app. There is also Computed
that works similar to 'computed' in Vue.
Vue slots in Quasargui¶
In components, Vue's slots can be accessed as Slot('slot-name', [...children...])
. Since Slot
is a Component
, it also has props, events, styles and classes.
from quasargui import *
layout = QInput(
classes='q-ma-lg',
label='Your city',
children=[
Slot('prepend', [
QIcon('place')
])
])
run(layout, title='slots example')
Scoped slots¶
Scoped slots can be accessed a little-bit differently. Since scoped slots are meant as a template, in python we express them as functions, Slot('slot-name', lambda props: [... children...])
. In this formula props
is a PropVar
that behaves just like a Model
.
"""
ref. https://quasar.dev/vue-components/tree#customize-content
"""
from quasargui import *
customize = Model([
{
'label': 'Satisfied customers',
'header': 'root',
'children': [
{
'label': 'Good food',
'icon': 'restaurant_menu',
'header': 'generic',
'children': [
{
'label': 'Quality ingredients',
'header': 'generic',
'body': 'story',
'story': 'Lorem ipsum dolor sit amet.'
},
{
'label': 'Good recipe',
'body': 'story',
'story': 'A Congressman works with his equally conniving wife to exact revenge '
'on the people who betrayed him.'
}
]
},
{
'label': 'Good service',
'header': 'generic',
'body': 'toggle',
'caption': 'Why are we as consumers so captivated by stories of great customer service? '
'Perhaps it is because...',
'enabled': False,
'children': [
{'label': 'Prompt attention'},
{'label': 'Professional waiter'}
]
},
{
'label': 'Pleasant surroundings',
'children': [
{'label': 'Happy atmosphere'},
{'label': 'Good table presentation', 'header': 'generic'},
{'label': 'Pleasing decor'}
]
}
]
}
])
layout = QTree(
props={
'nodes': customize,
'node-key': 'label',
'default-expand-all': True
}, children=[
Slot('default-header', lambda prop: [
Div(classes='row items-center', children=[
QIcon(
name=Computed(lambda ic: ic or 'share', prop['node']['icon']),
size='28px',
color='orange',
classes='q-mr-sm'),
Div(classes='text-weight-bold text-primary', children=[
prop['node']['label']
])
])
]),
Slot('default-body', lambda prop: [
v_if(prop['node']['story'], Div([
CustomComponent('span', classes='text-weight-bold', children=['This node has a story']),
': ',
prop['node']['story']
])),
v_else(
CustomComponent('span', classes='text-weight-light text-black', children=[
'This is some default content.'
])
)
])
])
run(layout, title='Scoped slot demo')
Model and Computed¶
In Quasargui Model
and Computed
enable Component
's to change dynamically. They are the most convenient way to handle complex user interaction.
To see the interactions with Model
and Computed
, consider the following example:
the window displays "is even" or "is odd",
depending on the parity of a+b
from quasargui import *
a = Model(0)
b = Model(0)
even = Computed(lambda x, y: (x+y) % 2 == 0, a, b)
odd = Computed(lambda x: not x, even)
layout = Rows([
QInput('a', model=a),
'+',
QInput('b', model=b),
v_if(even, Div(['is even'])),
v_if(odd, Div(['is odd'])),
])
run(layout)
In the example above even
and odd
are computed from the value of a
and b
.
Although a QInput
's model value is usually str
, in this example it is automatically converted to int
, since a
and b
has initial value of type int
.
In general, a Model
accepts any combination of basic python types (including list
and dict
). If you want to use other python classes, you'll need to write your custom logic to convert a Model's value into json-compatible values.
To access the value of a Model
or a Computed
, use the .value
property. It is a read/write property for Model
's and read-only for Computed
. If you want to be more explicit, you can also use .set_value()
.
Watching changes¶
If you want to execute code when a Model
gets a certain value, you just call add_callback
on that Model
.
from quasargui import *
accepted = Model(False)
layout = InputBool("Accept conditions", accepted)
def notify_if_accepted():
if accepted.value:
accepted.api.plugins.notify('Thanks for accepting!')
accepted.add_callback(notify_if_accepted)
run(layout)
You can add a callback anytime of course, even after a component is added to the window.
Structured Models¶
Models can hold int
, str
, bool
values, and also nested list
's and dict
's.
If a model is a dict
, it can be accessed just like a normal variable:
deep_model = Model({'deep': {'data': 'deep value'}})
data_model = deep_model['deep']['data'] # is also a Model
data_model.value = 'new value'
assert deep_model.value['deep']['data'] == 'new value' # True!
list_model = Model(['a', 'b', 'c'])
list_model[1].value == 'b'
list_model[1].value = 'new'
assert list_model.value == ['a', 'new', 'c'] # True!
your_model.value['item'] == 'new value'
since it will not dynamically set the value in the GUI, and you'll have to manually run your_model.update()
.
Events¶
Events can be defined on Components
using the events
property at construction time. Events that can be defined are specific for a Component
. If you want to check the state of the system at the event, access your Model
and Computed
values.
from quasargui import *
layout = QButton('Click me', events={'click': lambda: layout.api.plugins.notify("I've got clicked!")})
run(layout)
Notifications, dialogs¶
Quasargui offers a range of notification and dialog options, wrapping Quasar's plugins. You can access most Quasar plugins via your_component.api.plugins
or your_model.api.plugins
.
your_component.api.plugins.notify()
, with setting keyword arguments besides message). See Quasar documentation of options; on QuasarConfOptions you can use any option as a keyword argument to notify()
.
from quasargui import *
def show_notification(message):
layout.notify(message=message, position='top-right', group=False, timeout=1500)
def show_success():
layout.api.plugins.notify(message='This is a success!', caption='Just now', type='positive', icon='done')
dialog_events = {
'ok': lambda data: show_notification('OK clicked, data={}'.format(json.dumps(data))),
'cancel': lambda: show_notification('Cancel clicked'),
'dismiss': lambda: show_notification('Dialog dismissed'),
}
def show_alert():
layout.api.plugins.dialog(props={'title': 'Alert', 'message': 'Some message'}, events=dialog_events)
def show_confirm():
layout.api.plugins.dialog(props={
'title': 'Confirm',
'message': 'Would you like to turn on the wifi?',
'cancel': True,
'persistent': True
}, events=dialog_events)
def show_prompt():
layout.api.plugins.dialog(props={
'title': 'Prompt',
'message': 'What is your name?',
'prompt': {'model': '', 'type': 'text'},
'cancel': True,
'persistent': True
}, events=dialog_events)
def show_options():
layout.api.plugins.dialog(props={
'title': 'Options',
'message': 'Choose an option',
'options': {
'model': 'opt1',
'type': 'radio',
'items': [
{'label': 'Option 1', 'value': 'opt1', 'color': 'secondary'},
{'label': 'Option 2', 'value': 'opt2'},
{'label': 'Option 3', 'value': 'opt3'}
]
},
'cancel': True,
'persistent': True
}, events=dialog_events)
def show_bottom_sheet(grid: bool):
layout.api.plugins.bottom_sheet(props={
'message': 'Bottom Sheet message',
'grid': grid,
'actions': [
{'label': 'Drive', 'id': 'drive', 'img': 'https://cdn.quasar.dev/img/logo_drive_128px.png'},
{'label': 'Keep', 'id': 'keep', 'img': 'https://cdn.quasar.dev/img/logo_keep_128px.png'},
{'label': 'Google Hangouts', 'id': 'calendar', 'img': 'https://cdn.quasar.dev/img/logo_hangouts_128px.png'},
{},
{'label': 'Share', 'icon': 'share', 'id': 'share'},
{'label': 'Upload', 'icon': 'cloud_upload', 'color': 'primary', 'id': 'upload'},
{},
{'label': 'John', 'avatar': 'https://cdn.quasar.dev/img/boy-avatar.png', 'id': 'john'}
]
}, events=dialog_events)
dark_mode = Model(False)
dark_mode.add_callback(
lambda: layout.api.plugins.dark(dark_mode.value)
)
layout = QLayout([
QHeader([QToolbar([
QToolbarTitle([
QIcon('announcement', 'lg', classes='q-mx-md'),
'Dialogs'
]),
QSpace(),
QButton(
label=Computed(lambda dark: 'light mode' if dark else 'dark mode', dark_mode),
icon=Computed(lambda dark: 'light_mode' if dark else 'dark_mode', dark_mode),
props={'stretch': True},
events={'click': toggle(dark_mode)}
)
])]),
QPage([Rows(classes='q-py-xl', children=[
QButton('show a success notification', events={'click': show_success}),
QButton('show an alert', events={'click': show_alert}),
QButton('show a confirmation', events={'click': show_confirm}),
QButton('show a prompt', events={'click': show_prompt}),
QButton('show options', events={'click': show_options}),
QButton('show a grid menu', events={'click': lambda: show_bottom_sheet(grid=True)}),
QButton('show a list menu', events={'click': lambda: show_bottom_sheet(grid=False)}),
Div(classes='q-mt-xl', children=[
'See even more examples at ',
Link('Quasar dialog documentation',
'https://quasar.dev/quasar-plugins/dialog#predefined')])
])])
])
run(layout, title='Dialogs demonstration', debug=True)
In general, in Quasar's plugins documentation, if you see notify({options})
you can expect to write in Python my_component.api.plugins.notify(**options)
.
Note that not all plugins are wrapped (because many plugins do not make sense in a desktop app) and some plugins work a little bit different. Look up QuasarPlugins
in the Quasargui
code to see all the implemented plugins.
Custom styles, defaults¶
Once you start working with Quasargui, you will probably want to create your own styling. Luckily, Quasar is pretty flexible, offers a range of styling classes (eg. 'q-mt-xl'
means margin top should be extra large) and Q*
components have lots of props (eg. QButton(props={'glossy': True})
).
It can be a bit of a hustle though to set the same options again and again. So, defaults to the rescue!
from quasargui import *
QButton.defaults['props'] = {
'glossy': True,
'color': 'orange',
'rounded': True
}
QInput.defaults['props'] = {
'outlined': True,
'rounded': True
}
layout = Rows(classes='q-mt-lg q-gutter-md', children=[
Columns([QButton('one'), QButton('two'), QButton('oranje!')]),
QInput('me is outlined')
])
run(layout, title='We likes glossy', size=(300, 200))
Window access¶
Quasargui's run(layout)
command automatically produces a window for you. You can access this window through layout.api
- in fact, your_component.api
and your_model.api
points to the same api after the component or the model is attached to a window. If you want to modify the window's properties, access system dialogs or create new windows, you can find these functionality as api
's (quasargui.main.Api
's) functions.
So, in the following sections we'll basically walk through quasargui.main.Api
.
Menu¶
Menu can be added using run(menu=menu_definition)
where menu_definition
can look like
[{
'title': 'Edit',
'children': [
{'title': 'Copy', 'key': 'c', 'action': copy_cb},
{'title': 'Paste', 'key': 'p', 'action': paste_cb}
]
}]
Api
.
Component
's, mimicking a Windows menu.
"""
From this example you can learn how to set a menu,
and how to declare it dynamically (eg. for internationalization).
At the moment, the menu only works for Mac/Cocoa render.
"""
from quasargui import *
from quasargui.tools import static_vars
menu = [
{'title': 'Top Action', 'action': lambda: layout.notify("Top Action"), 'key': 't'},
{'title': 'Custom menu 1', 'children': [
{'title': 'Action 1', 'action': lambda: layout.notify("Hello 1"), 'key': 'b'},
{'title': 'Action 2', 'action': lambda: layout.notify("Hello 2"), 'key': 'd'},
None, # separator
{'title': 'Submenu', 'children': [
{'title': 'Action 3', 'action': lambda: layout.notify("Hello 3")},
{'title': 'Submenu 2', 'children': [
{'title': 'Submenu goes forever:)', 'children': [
{'title': 'Action 5', 'action': lambda: layout.notify("Hello 5")}
]},
{'title': 'Action 4', 'action': lambda: layout.notify("Hello 4")}
]},
]},
]},
]
@static_vars(counter=0)
def switch_menu():
new_menu = [{'title': 'Alternative action', 'action': lambda: layout.notify("Alternative action")}]
switch_menu.counter += 1
if switch_menu.counter % 2 == 1:
layout.api.set_menu(new_menu)
else:
layout.api.set_menu(menu)
@static_vars(spare_menu=None)
def hide_menu_if_not(platform):
layout.api.set_menu({platform: menu, 'default': []})
props = {'clickable': True}
layout = Rows(
classes='q-ma-lg',
children=[
Heading(5, 'Click on a menu'),
QList(props={'bordered': True}, classes='rounded-borders', children=[
QItem([QItemSection(['Replace menu with alternative menu'])], props=props, events={'click': switch_menu}),
QItem([QItemSection(['No menu if not Mac'])], props=props, events={'click': lambda: hide_menu_if_not('mac')}),
QItem([QItemSection(['No menu if not Windows'])], props=props, events={'click': lambda: hide_menu_if_not('windows')})
])
])
run(layout, menu=menu)
If you want to dynamically change the menu, you can do that using Api
's set_menu()
command.
Window operations¶
To access window operations, we use Api
's methods. You can access the api instance pointing to the window by a Component
that is already mounted to a window (your_component.api
) or a Model
that is mounted to a window (your_model.api
). To minimize, fullscreen or close a window, call
.minimize_window()
, .toggle_fullscreen()
or .close_window()
. To change the title, call .set_title('New title')
.
The standard file/folder/save dialogs can be accessed the same way - see the code of this example below.
from quasargui import Model, QLayout, QHeader, QPage, QToolbar, QSpace, QButton, QInput, Rows, TrueFalse, toggle
from quasargui.main import run
title_model = Model('Your title goes here')
title_model.add_callback(
lambda: title_model.api.set_window_title(title_model.value),
immediate=True
)
is_fullscreen = Model(False)
width = Model(400)
width.add_callback(
lambda: width.api.resize_window((width.value, None)) if 300 <= width.value <= 1000 else None,
immediate=True
)
height = Model(500)
height.add_callback(
lambda: height.api.resize_window((None, height.value)) if 300 <= height.value <= 1000 else None,
immediate=True
)
def toggle_fullscreen():
layout.api.toggle_fullscreen()
is_fullscreen.value = not is_fullscreen.value
layout = QLayout([
QHeader([QToolbar([
QSpace(),
QButton(icon='minimize',
props={'stretch': True},
events={'click': lambda: layout.api.minimize_window()}),
QButton(icon=TrueFalse('fullscreen_exit', 'fullscreen', is_fullscreen),
props={'stretch': True},
events={'click': toggle_fullscreen}),
QButton(icon='close',
props={'stretch': True},
events={'click': lambda: layout.api.close_window()}),
])]),
QPage([
Rows([
QInput('Window title', title_model),
QInput('Resize window width', width, type='number'),
QInput('Resize window height', height, type='number'),
QButton(
'Show file dialog',
events={
'click': lambda: layout.notify('You chose: {}'.format(
layout.api.show_open_dialog()))
}),
QButton(
'Show folder dialog',
events={
'click': lambda: layout.notify('You chose: {}'.format(
layout.api.show_folder_dialog()))
}),
QButton(
'Show save dialog',
events={
'click': lambda: layout.notify('You chose: {}'.format(
layout.api.show_save_dialog()))
}),
])
])
])
run(layout, size=(400, 500))
Frameless windows are also possible.
from quasargui import *
props = {
'dense': True
}
QButton.defaults['props'] = {
'dense': True,
'unelevated': True
}
Columns.defaults['classes'] = ''
Rows.defaults['classes'] = ''
layout = QLayout(children=[
QPage([
QBar(props={'dense': True}, classes='q-pr-xs', children=[
QSpace(),
QButton(icon='minimize',
props=props,
events={'click': lambda: layout.api.minimize_window()}),
QButton(icon='close',
props=props,
events={'click': lambda: layout.api.close_window()}),
]),
Rows([
Columns([QButton(icon='language'), QButton(icon='favorite')]),
Columns([QButton(icon='send'), QButton(icon='help')]),
])
])
])
run(layout, frameless=True, size=(64, 100), resizable=False)
Multiple windows¶
You can manage multiple windows. In that case the main rule of thumb is that each Component
, Model
and Computed
can belong to only one window.
So, it is best to create your layout and models via a factory function.
from random import randint
from time import sleep
from quasargui import Model, QLayout, QHeader, QPage, QToolbar, QSpace, QButton, QInput, Rows, TrueFalse
from quasargui.main import create_window, run
def create_new_window():
layout = create_layout('New window')
create_window(layout, position=(randint(1, 100), randint(1, 100)), size=(400, 500))
sleep(0.5)
layout.notify('A brand new window!', type='positive', timeout=200000)
def close_window(layout):
layout.api.close_window()
print('The window is closed but the app still runs')
def toggle_fullscreen(layout, model):
model.value = not model.value
layout.api.toggle_fullscreen()
def create_layout(title):
title_model = Model(title)
title_model.add_callback(
lambda: title_model.api.set_window_title(title_model.value),
immediate=True
)
is_fullscreen = Model(False)
width = Model(400)
width.add_callback(
lambda: width.api.resize_window((width.value, None)) if 300 <= width.value <= 1000 else None,
immediate=True
)
height = Model(300)
height.add_callback(
lambda: height.api.resize_window((None, height.value)) if 300 <= height.value <= 1000 else None,
immediate=True
)
layout = QLayout([
QHeader([QToolbar([
QSpace(),
QButton(icon='minimize',
props={'stretch': True},
events={'click': lambda: layout.api.minimize_window()}),
QButton(icon=TrueFalse('fullscreen_exit', 'fullscreen', is_fullscreen),
props={'stretch': True},
events={'click': lambda: toggle_fullscreen(layout, is_fullscreen)}),
QButton(icon='close',
props={'stretch': True},
events={'click': lambda: close_window(layout)}),
])]),
QPage([
Rows([
QInput('Window title', title_model),
QInput('Resize window width', width, type='number'),
QInput('Resize window height', height, type='number'),
QButton('Create new window', events={'click': create_new_window}),
])
])
])
return layout
main_layout = create_layout('Main window')
run(main_layout, size=(400, 300))
Most important components¶
In this part we'll go through the most important components. If you're interested in seeing all components, go to the components list.
Layout components¶
The window (or run()
function) can take any component as component
, still it is recommended to use QLayout
with QHeader
, QPage
and QFooter
as children, and optionally QDrawer
's. This leads to a standard layout where you can put your most important functions into QHeader
, as QButton
('stretch' prop is recommended). QHeader's background and foreground can be set using 'q-*' classes, using styles={'background': '#abc', 'color': '#fff'}
or adding your own stylesheet, pushing the path of your css file to your_component.style_sources
or calling your_component.api.import_styles()
after your component is mounted.
Note that adding QHeader
and QFooter
to QLayout
is entirely optional.
Rows and Columns¶
If you want the classic vertical or horizontal layout, use Rows
or Columns
. These result in a html "flex" layout which means that Columns will wrap automatically if the window is not wide enough.
Columns
and Rows
component.
from quasargui import *
layout = QLayout([QPage([
Columns(styles={'height': '100vh'}, column_classes='col-grow', children=[
Rows(row_classes='bg-grey-1 row-grow align-center', children=[
Div(['({}, {})'.format(i+1, j+1)]) for j in range(6)
])
for i in range(4)
])
])])
run(layout, 'Columns and rows example', debug=True, size=(400, 300))
Note that columns don't grow naturally, for even spacing you need to set 'col-grow', also with Rows
you need to set 'row-grow' for even alignment.
Form elements¶
You have access to a range of form elements. There are the Quasar form components (starting with Q, in quasargui.quasar_form
module).
from quasargui import *
from quasargui.vue_tags_input import VueTagsInput
def init_person():
return {
'name': '',
'role': '',
'office': 'New York',
'experience': 0,
'height': 100,
'tags': []
}
person = Model(init_person())
accept = Model(False)
def add_employee():
if accept.value:
person.api.plugins.notify(
type='positive',
icon='done',
message='<b>Employee added!</b>\n'
'name: {name}\n'
'role: {role}\n'
'experience: {experience}\n'
'height: {height}\n'
'tags: {tags}'.format(**person.value))
person.value = init_person()
else:
person.api.plugins.notify('Have to accept the terms.', type='negative', icon='report_problem')
label_classes = 'q-mr-md'
Columns.defaults['classes'] = ''
layout = QLayout([QPage([
QForm(classes='easyread q-px-md q-py-lg', children=[
Heading(4, 'Add Employee', classes='q-mb-md q-pt-lg'),
QInput('name', person['name']),
QSelect('role', person['role'], props={'options': ['Manager', 'Worker'], 'use-input': True}),
QSelect('office', person['office'], props={'options': ['London', 'New York'], 'readonly': True, 'disabled': True}),
Columns(classes='items-center', children=[
Div(['experience:'], label_classes),
QKnob(person['experience'], props={'min': 0, 'max': 10, 'step': 1}),
]),
Columns(classes='items-center', children=[
Div(['height:'], label_classes),
QSlider(person['height'], props={'min': 100, 'max': 250, 'step': 1}, styles={'min-width': '200px'})
]),
Columns(classes='items-center', children=[
Div(['tags:'], label_classes),
VueTagsInput(person['tags']),
]),
Div([
QCheckbox('accept terms', accept),
]),
QButton('submit', events={'click': add_employee}, classes='bg-primary text-white')
])
])])
run(layout, 'Register employee', size=(500, 700))
Also, there are form elements by input value type. These elements are in quasargui.quasar_form_improved
module and are named according to the input value type, eg. InputStr
, InputBool
, etc. The idea is to automatically get a combination of controls that is set up correctly. You can get an apropriate appearance for your input, and later you can refine it, choosing from one of its available appearance
. Eg. an InputChoice
can be a radio, a series of pushable buttons, or a select dropdown. Or, if you allow multiple choices, you can get checkbox, toggles, tags input or a multi-choice select. If you don't choose one, the control is determined based on the number of available choices.
from quasargui import *
few_choices = ['blonde', 'red', 'brown', 'blue', 'pink']
many_choices = [
'blonde', 'red', 'brown', 'blue', 'pink',
'black', 'grey', 'white', 'bold', 'long',
'short', 'thin', 'thick', 'medium', 'rasta'
]
my_choice1 = Model('blonde')
my_choice2 = Model('blonde')
my_choice3 = Model('blonde')
my_choice4 = Model('blonde')
multi_choice1 = Model([])
multi_choice2 = Model([])
multi_choice3 = Model([])
gap_classes = 'q-pt-lg'
selected_tab = Model('single')
single_choice_components = [
InputChoice(
'my choice - no choice set',
my_choice1,
classes=gap_classes),
InputChoice(
'my choice', my_choice2,
choices=few_choices,
classes=gap_classes),
InputChoice(
'my choice - many choices',
my_choice3,
choices=many_choices,
classes=gap_classes),
InputChoice(
'my choice - appearance=buttons',
my_choice4,
choices=few_choices,
appearance='buttons',
classes=gap_classes)
]
multiple_choice_components = [
InputChoice(
'multiple choices - no choice set',
multi_choice1,
classes=gap_classes,
multiple=True),
InputChoice(
'my choice - a few choices set',
multi_choice2,
choices=few_choices,
classes=gap_classes,
multiple=True),
InputChoice(
'my choice - many choices set',
multi_choice3,
choices=many_choices,
classes=gap_classes,
multiple=True),
]
layout = QLayout([QPage(classes='easyread', children=[
Heading(4, 'Input choice appearances', classes='q-mb-md'),
QTabs(selected_tab, classes='text-primary', children=[
QTab('single', label='single value'),
QTab('multiple', label='multiple values')],
),
QTabPanels(selected_tab, [
QTabPanel('single', single_choice_components),
QTabPanel('multiple', multiple_choice_components),
]),
])])
run(layout, 'Form - input choice demo', size=(600, 550), _render_debug=True, debug=True)
There's InputTime
, InputDate
and InputDateTime
that are implementations of Quasar's recommendations on data-time input, with all the conversions between Python and the GUI taken care of (remember that date
, time
and datetime
is a special class that is not a basic type.)
QInput
. The user can enter the date or time via keyboard and with help of popups, clicking on the side icons.
from quasargui import *
layout = QLayout([QPage(classes='easyread q-my-lg', children=[
Heading(4, 'Time, Date and DateTime'),
InputDate('My birthday'),
InputTime('Work starts usually at'),
InputDateTime('Experiment started'),
])])
run(layout, size=(700, 700))
Form validation¶
You can validate form components right when the user inputs a value - but for snappier action you need to add raw javascript to handle simple field validations, using JSRaw
.
import quasargui
from quasargui import *
def on_submit():
validation = True
validation = validation and input_name.validate()
validation = validation and input_age.validate()
if not accept.value:
accept.api.plugins.notify(
color='negative',
message='You need to accept the license and terms first',
icon='report_problem'
)
elif validation:
accept.api.plugins.notify(
icon='done',
color='positive',
message='Submitted'
)
def on_reset():
input_name.reset_validation()
input_age.reset_validation()
QInput.defaults['props'].update({
'filled': True,
'lazy-rules': True
})
accept = Model(False)
input_name = QInput('Your name *', props={
'hint': 'Name and surname',
'rules': JSRaw("[ val => val && val.length > 0 || 'Please type something']")
})
input_age = QInput('Your age *', type='number', props={
'rules': JSRaw("""[
val => val !== null && val !== '' || 'Please type your age',
val => val > 0 && val < 100 || 'Please type a real age'
]""")
})
validate_on_submit = QForm(
styles={'max-width': '30em', 'margin': '0 auto'},
events={
'submit.prevent.stop': on_submit,
'reset.prevent.stop': on_reset
}, children=[
Heading(5, 'Form validation on submit'),
Div(classes='q-mb-md', children=[
'Reference: ',
Link('here', "https://quasar.dev/vue-components/input#example--form-validation")
]),
input_name,
input_age,
QToggle('I accept the license and terms', accept),
Columns([
QButton('Submit', type='submit', color='primary', props={
'unelevated': False
}),
QButton('Reset', type='reset', color='primary', props={
'flat': True
})
])
])
layout = QLayout([
QHeader(['Form validation <small>- on submit</small>']),
QPage([validate_on_submit])
])
quasargui.run(layout, 'Form validation demo', size=(600, 550))
You can also stick to validation on submit, in this case you need to set up a Model
for errors.
Creating your own components¶
It is usually a better idea to use a factory function if you want to create a complex component, from existing components. However, you can also create your own assembled component. In that case, set component='div'
and in the __init__()
method you assemble the children
parameter so that it suits your needs, even if it is only a single component. And you can also extend an existing component.
TODO: example - custom component
Integration¶
Matplotlib integration¶
"""
Coin-flip stock market price simulator
This example requires matplotlib and mpld3
"""
import math
import random
import time
# noinspection PyPackageRequirements, PyUnresolvedReferences
import matplotlib.pyplot as plt
from quasargui import *
from quasargui.plot import Plot
loading = Model[bool](True)
calculation_time = Model(0.0)
n = Model(1000)
n_processes = Model(10)
start = Model(10.0)
drift = Model(0.001)
variance = Model(0.02)
interactive = Model(False)
animated = Model(False)
fig, ax = plt.subplots(1, 1)
plot = Plot(renderer=Computed(lambda i: 'mpld3' if bool(i) else 'png', interactive))
def add_scenario(update_plot=True):
global ax, plot, animated
def create_random_process():
coinflips = random.choices([-1, 1], k=int(n.value))
random_process: list = [0]*(len(coinflips)+1)
random_process[0] = float(start.value)
mu = float(drift.value)
var = float(variance.value)
for i, flip in enumerate(coinflips, start=1):
s_0 = random_process[i-1]
s_1 = s_0 * math.e ** (mu - 0.5 * var**2 + var * flip)
random_process[i] = s_1
return random_process
ax.plot(create_random_process())
if update_plot:
plot.update()
def redraw_plot():
global fig, ax, plot, animated
loading.value = True
t1 = time.time()
fig, ax = plt.subplots(1, 1)
plot.set_figure(fig)
for _ in range(int(n_processes.value)):
add_scenario(update_plot=bool(animated.value))
ax.grid(True, alpha=0.3)
ax.set_xlabel('x')
ax.set_ylabel('y')
if not animated.value:
plot.update()
t2 = time.time()
calculation_time.value = round(t2 - t1, 2)
loading.value = False
# This example loads the plot on load.
QToggle.defaults['props'] = {
'color': 'white',
'keep-color': True,
'left-label': True
}
layout = QLayout(events={'load': redraw_plot}, children=[
QHeader([
QToolbar([
QToolbarTitle([
QIcon('insights', 'lg', classes='q-mx-md'),
'Plot demo <small>- Stock simulation by coin flip</small>'
]),
QToggle(label='Interactive', model=interactive),
QToggle(label='Animated', model=animated)
])
]),
QPage([
plot,
v_if(
interactive,
Div(classes='text-center', children=[
"Interactive plot is rendered by ",
Link('mpld3', 'https://mpld3.github.io/')
])
),
v_if(
Not(Or(interactive, loading)),
Div(classes='text-center', children=[
'Non-interactive plot is rendered by matplotlib as png.'
])
)
]),
QDrawer([Rows([
QInput(label='Number of coinflips', model=n),
QInput(label='Number of screnarios', model=n_processes),
QInput(label='Start value (S_0)', model=start),
QInput(label='Drift (Îź)', model=drift),
QInput(label='Variance (Ď)', model=variance),
QButton(label='Redraw', events={'click': redraw_plot}, props={'loading': loading}),
QButton(label='Add scenario', events={'click': add_scenario}, props={'disable': loading}),
])]),
v_if(
calculation_time,
QFooter(['Plotting took ', calculation_time, ' seconds.'])
)
])
run(layout)
Adding an existing Vue component¶
The simplest method to add an existing Vue component is to get the .uml.js and its .css then its as simple as
class VueTagsInput(ComponentWithModel):
component = 'vue-tags-input'
script_sources = ['vue-tags-input.2.1.0.js']
style_sources = ['vue-tags-input.css']
ComponentWithModel
, if it has even label, then LabeledComponent
, otherwise extend Component
. Set component
to the component's html tag. If you look at VueTagsInput
in Quasargui's source, you'll see that we've added a couple more lines. In the constructor of the component, we added some useful conveniences that make the component work more seamlessly by default.
from quasargui import ComponentWithModel, Model, ClassesType, StylesType, PropsType, build_props
class VueTagsInput(ComponentWithModel):
"""
see http://www.vue-tags-input.com/#/examples/hooks
Note that model points to 'tags' property,
'v-model' can be accessed via self.current_tag.
"""
component = 'vue-tags-input'
script_sources = ['vue-tags-input.2.1.0.js']
style_sources = ['vue-tags-input.css']
def __init__(self,
model: Model = None,
classes: ClassesType = None,
styles: StylesType = None,
props: PropsType = None):
props = build_props({}, props, {
'tags': model or Model([])
})
events = {
'tags-changed': lambda new_tags: model.set_value(new_tags)
}
self.dependents = [props['tags']]
self.current_tag = Model('')
super().__init__(
model=self.current_tag,
props=props,
classes=classes,
styles=styles,
events=events)
self.dependents
. (The model you pass to model
parameter gets passed to self.dependents
automatically.) If you don't add a model to self.dependents
, it does not get attached to the GUI and you will see weird behavior. To debug this, set _render_debug=True
in run()
. It is worth do do a quick test rig of your component, something like the following:
from quasargui import *
model = Model([])
model.add_callback(lambda: model.api.plugins.notify(json.dumps(model.value)))
run(VueTagsInput(model), debug=True, _render_debug=True)
VueTagsInput
with your component.
Importing a SPA-style Vue component¶
```python
TODO: ```