pyapp-kit / magicgui

build GUIs from type annotations
https://pyapp-kit.github.io/magicgui/
MIT License
362 stars 49 forks source link

Styling Widgets #581

Closed multimeric closed 11 months ago

multimeric commented 1 year ago

❓ Questions and Help

I'm trying to do some styling of a Label widget. I want it to change the font colour, and to enable word wrap so it doesn't force the containing panel to be larger.

However, there doesn't seem to be an easy way to do that in magicgui. The widget constructor doesn't seem to pass anything to the final QWidget, and I can't work out how to access the CSS that QT uses either. Any pointers here?

[x] I have searched the documentation

tlambert03 commented 1 year ago

You can access the native backend widget on any magicgui widget instance using the native attribute.

We don't do it in the constructor, because not all backends would support the same api. If you want to use backend-specific stuff, your best bet is to modify the .native widget

tlambert03 commented 1 year ago

here's an example:

from typing import TYPE_CHECKING, cast

from magicgui import widgets

if TYPE_CHECKING:
    from qtpy.QtWidgets import QLabel

label = widgets.Label(value="Hello World!")
qlabel = cast("QLabel", label.native)

qlabel.setStyleSheet("font-size: 20pt; font-family: Comic Sans MS;")

# or:
# font = qlabel.font()
# font.setPointSize(20)
# font.setFamily("Comic Sans MS")
# qlabel.setFont(font)

label.show(run=True)
multimeric commented 1 year ago

Thanks, that's super helpful!

multimeric commented 1 year ago

Just as a thought, I wonder if there could be a nice way to inject native args into the constructor.

The issue I'm hitting right now is that if we modify the native widget after construction, we can actually lose data. For example:

from magicgui.widgets import FloatSpinBox

box = FloatSpinBox(value = 0.14982345)
box.native.setDecimals(10)
print(box.value)
box.show(run=True)

In this case we get 0.15000 logged in the console and GUI, meaning that the true value has been lossily rounded up. However, if we could tell the SpinBox to have 10 decimals from the start, this wouldn't happen.

Of course I understand the point about the backends, so I wonder if there could just be a qtpy_args dictionary that gets passed to the native widget if that backend is being used. Then I could do the following:

box = FloatSpinBox(value = 0.14982345, qtpy_args = {"decimals": 10})
tlambert03 commented 1 year ago

thanks for following up, I'll reopen until the discussion is concluded. I have two thoughts here:

first, unlike styling (fonts, etc...) anything that is value-related (like decimals) definitely is in scope for handling directly by magicgui. So if you can find additional examples where the value itself is being modified, definitely feel free to raise them. In this case however, the step value for all RangedWidget subclasses can control the number of available decimals:

from magicgui.widgets import FloatSpinBox
box = FloatSpinBox(value=0.14982345, step=1e-10)
print(box.value)  # prints 0.14982345

the second thought is that even QDoubleSpinBox doesn't have a way to set the decimals (or even the value for that matter) in the constructor itself. So there too, you must instantiate a widget, set the decimals and then reset the value:

from qtpy.QtWidgets import QDoubleSpinBox, QApplication

app = QApplication([])
box = QDoubleSpinBox()  # no way to set decimals or value here
# has to be done here ...
box.setDecimals(10)
box.setValue(0.14982345)

so even if we did have a qtpy_args dict... we would A) need have an API adapter to convert keys to commands and then B) perform something very similar to the above... which can already be done without using the backend API:

box = FloatSpinBox()
box.step = 1e-10
box.value = 0.14982345
print(box.value)
tlambert03 commented 11 months ago

did you have any additional thoughts here @multimeric? I might close this otherwise. It's still possible that someday we'll have a killer app for providing kwargs to the backend... but as described above, i don't think we're quite there yet

multimeric commented 11 months ago

No, if passing args to the native constructor wouldn't help with this issue then I can't see any immediate need to implement it now. Although it probably would come in handy down the track.