Closed kociak closed 1 year ago
I think the best way is to use converter with the binding, as described here: https://nionui.readthedocs.io/en/latest/index.html#converters
There are a few converters already available which you can use directly or as examples: https://github.com/nion-software/nionutils/blob/master/nion/utils/Converter.py
Unfortunately I don't have an example of state <-> icon converter. But I think it would work as a convert from str
to numpy.array
with dtype=numpy.uint32
. You would not need to implement convert_back
since it would only be a one way conversion.
I'm going to leave this issue open to see if I can find some other examples of how this might be done too. I'll also post some additional examples since I'll be reworking some of the scan and camera control panels soon using declarative UI code.
Additional simple example is here: https://github.com/nion-software/nionui/blob/master/nionui_app/nionui_examples/ui_demo/Converters.py
There are also many binding/converter examples throughout the rest of the project.
All right. I did not know the icon
was bindable. It is therefore trivial once you get the way to load images. thanks for the hint!
Here is my test version (only 3 states and one button)
I still need to test how the on_click
which will certainly trigger a State
change, will react (the state changing the icon ...)
Also, might be stupid to reload each time the icons, and probably I'll need to load them once for all in the Handler
__init__
.
from nion.ui import CanvasItem
import pkgutil
# The new converter, as suggested; no convert back needed
class StateToIconConverter():
""" Convert a MonchActuatorDevice.State to an icon"""
def __init__(self, stateToIconDict: typing.Dict[MonchActuatorDevice.State,str]):
self.__stateToIconDict = stateToIconDict
def convert(self, value:typing.Optional[MonchActuatorDevice.State]):
if value:
if dataname := self.__stateToIconDict[value]:
data = pkgutil.get_data(__name__, dataname)
icon = CanvasItem.load_rgba_data_from_bytes(data, "png")
return icon
return None
# somewhere in the init of the Handler. In my case, I will need one converter per #button because all the buttons won't reflect/react to the MonchActuatorDevice.State s the same way
mydict = {MonchActuatorDevice.State.Observe : "resources/icon2.png",MonchActuatorDevice.State.Observemove : "resources/icon3.png", MonchActuatorDevice.State.Freeze : "resources/icon1.png"}
self.conv_debug = StateToIconConverter(mydict)
## somewhere in the View
# instrument is a MonchActuatorDevice of course
self.icon_debug_pb = ui.create_push_button(icon='@binding(instrument.state,converter=conv_debug)',width=22, height=22,name ="icon_debug_pb")
I'll close this issue now. If you run into another problem, make a comment here or file another issue.
Just for the record, a better version of the multiple state button, in case someone wants to reproduce it:
# the converter convert a `MonchActuatorDevice.state` into an icon, based on a dictionnary. `MonchActuatorDevice.state` is an `Enum` specific to my project, but of course, you can use whatever fits your need and adapt.
class StateToIconConverter():
""" Convert a MonchActuatorDevice.State to an icon"""
def __init__(self, stateToIconDict: typing.Dict[MonchActuatorDevice.State,typing.Optional[DrawingContext.RGBA32Type]]):
self.__stateToIconDict = stateToIconDict
def convert(self, value:typing.Optional[MonchActuatorDevice.State]):
return self.__stateToIconDict[value] if value else None
# Somewhere in the handler, you need to create a dict that maps the `MonchActuatorDevice.state` to an icon. This can be easily adapted to your needs
def create_state_to_icon_dict(self) -> typing.Dict[MonchActuatorDevice.State,typing.Optional[DrawingContext.RGBA32Type]]:
icon_name_list = [
'resources/icon1.png',
'resources/icon2.png',
'resources/icon3.png'
]
state_list = [member for member in MonchActuatorDevice.State if 'Observe' in member.name or 'Freeze' in member.name]
# I know that state_list has only 3 members, but you better adapt this ...
data_list = [pkgutil.get_data(__name__, file) for file in icon_name_list]
icon_list = [CanvasItem.load_rgba_data_from_bytes(data, "png") for data in data_list]
return dict(zip(state_list, icon_list))
mydict = self.create_state_to_icon_dict()
self.conv_debug = StateToIconConverter(mydict)
# and of course, somewhere in the Panel
self.icon_debug_pb = ui.create_push_button(icon='@binding(instrument.state,converter=conv_debug)',width=26, height=26,name ="icon_debug_pb")
One additional note, instead of using the bitmap data directly, you have the option of using the Bitmap
object which can be created like this:
from nion.ui import Bitmap
bitmap = Bitmap.Bitmap(rgba_bitmap_data=rgba_bitmap_data, shape=Geometry.IntSize(24, 24))
The advantage of doing this is that you can specify the intended size separately from the bitmap data. This allows you to create high resolution bitmaps that look good on high DPI monitors. Typically we create bitmap data that is 48x48 pixels but specify its size as 24x24 so that when it draws on a high DPI monitor, it will take advantage of the high resolution, i.e. it will draw 48x48 pixels into a screen space of 24x24, taking advantage of the high DPI.
Anywhere you can pass a numpy array for a bitmap, you should be able to use a Bitmap
object instead. If I missed any case in the API's, please file a bug so I can fix it.
I am now using Bitmap
and it seemed to work until I used some .png
with alpha layer to 0. Then I understood that there was an ambiguity in the dimensions definitions, which are obvious with a transparent layer.
The image is a file with a given dimension (say, for a square, an edge of size Fsize in pixels). Then the bitmap has another dimension, (shape
), say Bitsize. Finally, the button has also another size (Butsize, specified by parameters width
and height
).
If Bitsize << Butsize, then a square is drawn around the button, the image might be too small but everything seems fine. If Bitsize = Butsize, the image appears larger than the button (we can see the frame of the button being smaller than the image), with potential overlap between the images of different buttons. Buttons clickable area is within the frame, as expected.
I don't think Fsize affects anything.
I am wondering if this is an expected behaviour, but I doubt...
Here is an example code:
self.StageSafeIcon=Bitmap.Bitmap(rgba_bitmap_data=CanvasItem.load_rgba_data_from_bytes(pkgutil.get_data(__name__, "resources/StageSafe.png"), "png"),
shape=Geometry.IntSize(42, 42))
self.StageFocusIcon = Bitmap.Bitmap(rgba_bitmap_data=CanvasItem.load_rgba_data_from_bytes(pkgutil.get_data(__name__, "resources/StageFocus.png"), "png"),
shape=Geometry.IntSize(42, 42))
self.StageFocusSShftZ = ui.create_push_button( name="StageFocusSShftZ_pb",icon='StageFocusIcon', on_clicked="StageFocusSShftZ",width = 42, height = 42)
self.StageSafeSShftZ = ui.create_push_button( name="StageSafeSShftZ_pb", icon = 'StageSafeIcon', width = 42, height = 42,
on_clicked="StageSafeSShftZ")
Using swift for driving external hardwares, I am always trying to simplify the UI for the users. Although the question is quite more generic, the best way I found to synchronize the UI and the state of the hardware is the following. I define a
state
property, a proxy to the state machine state of the HW, of aninstrument
, itself a proxy for the hardware. Theinstrument
fire
s the relevantEvent.Event
. ThisEvent.Event
is filtered by the UIhandler
, which takes responsability to update the UI, especially enabling/disabling variousbuttons
andtextfields
. This works suprinsingly well, as soon as the state machine is rigorously defined. The next step for me is to have push buttons withicon
s that could be dynamically changed upon certain states. This would increase readability and ergonomy and optimize space. I guess we ccould bind theicon
property of thebutton
to some property of theinstrument
, but this won't be really easy. I was wondering if there is somewhere (did not find it) a mix ofcheckbox
group_value
andicon
property of the pushbutton. Otherwise speaking, having theicon
of a pushbutton, and also possibily theenable
property of it that would be tight to anint
. Then I could much more easily tight the appearance and reactivity of pushbutton to thestate
of my instrument. Probably another way would be to do it outside of theDeclarative
way, but this is not really robust I guess. Any hint? not a priority, but certainly something out of my expertise. Thanks