Depending on the scope and purpose of the application you're wanting to build, there's a few different approaches you could take here. Some options are:
- If it's a limited number of main choices with a handful of widgets making up the sub-questions, perhaps
TabbedContent
would be what you're looking for.
- If you want a wee bit more control, you could wire things up on a single screen using
ContentSwitcher
.
- You could also build a "create the DOM as you go" approach by creating your initial question(s) with
compose
and then using a combination of mount
and remove
.
As suggested by Will in his answer, one very likely approach would be to use a Screen
or two. With a little bit of thought you could probably turn it into quite a flexible and comprehensive application for asking questions.
What follows is a very simplistic illustration of some of the approaches you could take. In it you'll find I've only put together a "bike" screen (with some placeholder questions), and only put in a placeholder screen for a car. Hopefully though it will illustrate some of the key ideas.
What's important here is that it uses ModalScreen
and the screen callback facility to query the user and then get the data back to the main entry point.
There are, of course, a lot of details "left for the reader"; do feel free to ask more about this if anything isn't clear in the example.
from typing import Any
from textual import on
from textual.app import App, ComposeResult
from textual.widgets import Button, OptionList, Label, Checkbox, Pretty
from textual.widgets.option_list import Option
from textual.screen import ModalScreen
class SubScreen(ModalScreen):
DEFAULT_CSS = """
SubScreen {
background: $panel;
border: solid $boost;
}
"""
class CarScreen(SubScreen):
def compose(self) -> ComposeResult:
yield Label("Buy a car!")
yield Label("Lots of car-oriented widgets here I guess!")
yield Button("Buy!", id="buy")
yield Button("Cancel", id="cancel")
@on(Button.Pressed, "#buy")
def buy_it(self) -> None:
self.dismiss({
"options": "everything -- really we'd ask"
})
@on(Button.Pressed, "#cancel")
def cancel_purchase(self) -> None:
self.dismiss({})
class BikeScreen(SubScreen):
def compose(self) -> ComposeResult:
# Here we compose up the question screen for a bike.
yield Label("Buy a bike!")
yield Checkbox("Electric", id="electric")
yield Checkbox("Mudguard", id="mudguard")
yield Checkbox("Bell", id="bell")
yield Checkbox("Wheels, I guess?", id="wheels")
yield Button("Buy!", id="buy")
yield Button("Cancel", id="cancel")
@on(Button.Pressed, "#buy")
def buy_it(self) -> None:
# The user has pressed the buy button, so we make a structure that
# has a key/value mapping of the answers for all the questions. Here
# I'm just using the Checkbox; in a full application you'd want to
# take more types of widgets into account.
self.dismiss({
**{"type": "bike"},
**{
question.id: question.value for question in self.query(Checkbox)
}
})
@on(Button.Pressed, "#cancel")
def cancel_purchase(self) -> None:
# Cancel was pressed. So here we'll return no-data.
self.dismiss({})
class VehiclePurchaseApp(App[None]):
# Here you could create a structure of all of the types of vehicle, with
# their names and the screen that asks the questions.
VEHCILES: dict[str, tuple[str, type[ModalScreen]]] = {
"car": ("Car", CarScreen),
"bike": ("Bike", BikeScreen)
}
def compose(self) -> ComposeResult:
# This builds the initial option list from the vehicles listed above.
yield OptionList(
*[Option(name, identifier) for identifier, (name, _) in self.VEHCILES.items()]
)
# The `Pretty` is just somewhere to show the result. See
# selection_made below.
yield Pretty("")
def selection_made(self, selection: dict[str, Any]) -> None:
# This is the method that receives the selection after the user has
# asked to buy the vehicle. For now I'm just dumping the selection
# into a `Pretty` widget to show it.
self.query_one(Pretty).update(selection)
@on(OptionList.OptionSelected)
def next_screen(self, event: OptionList.OptionSelected) -> None:
# If the ID of the option that was selected is known to us...
if event.option_id in self.VEHCILES:
# ...create an instance of the screen associated with it, push
# it and set up the callback.
self.push_screen(self.VEHCILES[event.option_id][1](), callback=self.selection_made)
if __name__ == "__main__":
VehiclePurchaseApp().run()
screen
andmode
, but I still didn't manage to implement something like "Element Clicked -> Event -> Change State -> Runcompose()
with different state". How does such a state transition look like? And how does the state handling look like? I didnt' find something likeif state == "foo": yield Screen1(); else: yield Screen2()
or even something likedef on_selected(self, bla): sendEvent(blubb)
Can you give an example (apart from the Screens-example, which I already read several times). Maybe I'm too biased by PyQt? – Nefen