Setting SpinBox display format to two decimal places

Godot Version

4.2.1

Question

I’m currently messing around with a UI element that I’d like to display float values, but always have them display two decimal places, i.e 17.50. The only problem I seem to be left with is for values like 17.50, it displays 17.5, and I don’t see a clear way to override this behavior. SpinBox does have get_line_edit(), but setting the .text property of that doesn’t seem to do anything, so I’m out of ideas for now.

I’d really prefer not to have to implement something directly based on Range just to have it always display to two decimal places. I guess at that point I’d abandon most of the features and just have it be a LineEdit that does a little validation like I do now, but I like the arrows and prefix and other affordances that come for free with it.

Connect to the SpinBox Range.value_changed signal in a deferred way and set its LineEdit.text there.

Example:

extends SpinBox


func _ready() -> void:
	value_changed.connect(func(value:float):
		get_line_edit().text = '%.2f' % value
	, CONNECT_DEFERRED)
1 Like

Oh wow, interesting! First off, thanks for your help. That does have an effect, and does work… right up until I click back in the SpinBox, at which point the decimal point goes away again (and stays away until I input a different value – re-entering the same one doesn’t make it come back).

I tried to apply your solution and add a deferred signal connection to focus_entered with the same function, but that didn’t do the trick unfortunately.

In that case you’ll need to connect to more signals like Control.focus_exited or LineEdit.text_submitted

For example:

extends SpinBox


func _ready() -> void:
	get_line_edit().focus_exited.connect(format_value, CONNECT_DEFERRED)
	get_line_edit().text_submitted.connect(format_value.unbind(1), CONNECT_DEFERRED)
	value_changed.connect(format_value.unbind(1), CONNECT_DEFERRED)


func format_value() -> void:
	get_line_edit().text = '%.2f' % value

There may be more signals you need to connect to. I’m not sure.

1 Like

The more I mess with things the more I see I’m trying to hack at something that doesn’t want to be hacked.

So once again, thank you for your help. I’m probably going to poke at this some more (and/or give up soon), and if I figure out all the things, I’ll post back here. I noticed that manually modifying the LineEdit messes with the prefix I set as well (hence my first comment).

Thank you so much for helping a stranger on the internet try to figure out this random thing. The two paths seem to be to use trial-and-error to mess with SpinBox until I figure it out, or build my own from LineEdit or Range. Either one feels like a lot of work for something relatively minor, so I’ll probably put it off for a while.

I don’t know who I was trying to fool…

Okay @mrcdk thank you, you helped me solve it. What seems to get it to work is three connections, to value_changed, get_line_edit().focus_entered, and get_line_edit().focus_exited. With those three, alongside ensuring I manually reset the prefix of the SpinBox, I seem to get my desired behavior! They do all seem to need to be CONNECT_DEFERRED, and I’m not quite sure why, but I’m happy with the result! If I clean it up perhaps I’ll post a short sample here.

For anyone coming after me, here’s this. Not sure this warrants a whole asset, but hopefully this helps someone else!

class_name FormatSpinBox extends SpinBox
## A format field for numbers that allows for more control than [SpinBox].
##
## This can be used to, for example, always display values with a specific precision.
## [code]%.2f[/code] will display floats with two decimal places.

## A format string with a single specifier to display the box value.
## See [method String.format] for more information on format strings.
@export var format_string: String = "%s"


## Convenience initializer that sets [member format_string] to [param fstring] if set.
func _init(fstring: String = "") -> void:
	if not fstring.is_empty():
		format_string = fstring


# Connect the signal handlers required to make this work seamlessly.
func _ready() -> void:
	value_changed.connect(_on_value_changed, CONNECT_DEFERRED)
	get_line_edit().focus_entered.connect(_on_focus_entered, CONNECT_DEFERRED)
	get_line_edit().focus_exited.connect(_on_focus_exited, CONNECT_DEFERRED)

	# Also, set this up initially
	get_line_edit().text = (prefix + format_string + suffix) % value


# This handler ensures the format is maintained while editing the field.
func _on_value_changed(value: float) -> void:
	get_line_edit().text = (prefix + format_string + suffix) % value


# This handler ensures the format is retained when entering the field.
func _on_focus_entered() -> void:
	get_line_edit().text = format_string % value


# This handler ensures the format is retained when exiting the field.
func _on_focus_exited() -> void:
	get_line_edit().text = (prefix + format_string + suffix) % value
1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.