Why is TextMeshPro not Updating textBounds After Setting text? - c#

I have implemented a tooltip system on hover. For this I use a textmeshpro text and an image. This image should be a simple background depending on the length of the text, which is always different.
If I hover over a certain element, the textmeshpro text is replaced. The background image is moved to the element and the size is shall change based on the length of the text:
// Hover over slot in
public void OnPointerEnter(PointerEventData eventData) {
if (currentItem != null) {
Debug.Log("hover over slot item in");
GamePlay.EnableCanvas(GamePlay.GetHoverCanvas());
TextMeshProUGUI txtMeshPro = GamePlay.GetHoverCanvas().GetComponentInChildren<TextMeshProUGUI>();
Debug.Log(currentItem.GetItemDescription(currentItem));
txtMeshPro.text = currentItem.GetItemDescription(currentItem);
var hoverCanvasText = txtMeshPro.GetComponent<RectTransform>();
var hoverCanvasTextBG = GamePlay.GetHoverCanvas().GetComponentInChildren<Image>().GetComponent<RectTransform>();
hoverCanvasText.position = new Vector3(this.GetComponent<RectTransform>().position.x, this.GetComponent<RectTransform>().position.y + 50, 0);
hoverCanvasTextBG.position = new Vector3(this.GetComponent<RectTransform>().position.x, this.GetComponent<RectTransform>().position.y + 50, 0);
hoverCanvasTextBG.sizeDelta = new Vector2(txtMeshPro.textBounds.size.x + 15, txtMeshPro.textBounds.size.y + 15);
// No clue why image is not resized properly on first hover
}
}
The weird issue I have is that the first time I hover over the element, the image size wont change.
Replacing the text and changing the position always works without issues.
Issue description:
First hover over element A: Image background size wont change at all.
Second hover over element B: Image background size changes, but has the correct size of element A
Third hover over element A: Image background size changes, but has the correct size of element B
How is this possible? I dont get whats happening here. The code works without any issues for the position, why the same logic does not work for the size.
Note: I am very new to unity, tinkering arround for a few days now, so I could be a very simple issue, but after researching for a few hours now, I haven't found anything useful.

After setting the text of a TextMeshProUGUI, the textBounds and its size are not updated until the following frame. This is caused by Unity's deferred layout rebuilding. In their UI Layout Documentation, it reads:
The rebuild will not happen immediately, but at the end of the current
frame, just before rendering happens. The reason it is not immediate
is that this would cause layouts to be potentially rebuild many times
during the same frame, which would be bad for performance.
I'm gonna go ahead and guess GamePlay.GetHoverCanvas() is maintaining an object that's never destroyed and, therefore, your code is working in all cases except its very first run.
There are probably a few ways you can go about fixing this but the easiest would be to call Canvas.ForceUpdateCanvases() between setting the text and reading the size of the textbounds. It's not the fastest function but it should do the trick.
Another option would be to, instead of a synchronous function, kick off a coroutine that sets the text one one frame, yields the next frame, then reads the textbounds on its second frame of execution. (This would likely lead to the size popping in and might look undesirable).

Instead of refreshing all your Canvases, you can use TMP_Text.ForceMeshUpdate(); right after you assign your string. This makes sure your text-information are correct within the same frame.
If the parents of your text uses Layout-Groups, there is a fair chance that you need to update those as well. You do that with LayoutRebuilder.ForceRebuildLayoutImmediate( [Layout-Group.RectTransform] ); (read more here)
Please note that the order is important.
Assign your text
Rebuild Layout-Group
Force Mesh Update

To prevent the need to force update the Canvas, you can use TextMeshProUGUI.GetPreferredValues() to ascertain ahead of time what the width and height would be for the text currently assigned to it. It'll return a Vector2. You can then use that Vector2 instead of TextMeshProUGUI.textBounds.
Or if you need the size for a certain text without actually assigning it, use TextMeshProUGUI.GetPreferredValues(string text).
There are other overloads of that method to get the size given constraints in either width or height.

Related

Unity: get height of GameObject with content size fitter attached

I have a GameObject with a Content Size Fitter attached and would like to read out the height of the GameObject. Based on reading a dozen or so forums posts i've tried a lot of things, but can't get it to work. I think that this code should be almost correct, yet, it doesn't give me the right number:
GameObject go; // The game object whose height i want to read out
...
// Force rebuild before reading out height
LayoutRebuilder.ForceRebuildLayoutImmediate(go.GetComponent<RectTransform>());
// Read out height
float h = go.GetComponent<RectTransform>().rect.height;
Unfortunately, this does not give me the correct height, in the sense that it does not correspond to what i see in the inspector. The code returns a height of around 2400, while the inspector says that it is 1257.8 (which corresponds to what i see on the screen) - see screenshot below. Any help would be appreciated!
Screenshot
Try RectTransform.sizeDelta. I've used it to set the height of RectTransforms in a few projects. Presumably you could just use it to get the height too.
I have only been able to do this using Text.preferredWidth and preferredHeight, these seem to update immediately on changing the text. Of course, this only works if you are fitting to a Text.

WPF - how can I programmatically allow a control to find its desired size when it is never drawn on the screen (for a screenshot)?

I am trying to programmatically take a screenshot of a control that I will never actually display on the screen (it is a duplicate of a control is shown in a different area of the application - this area is just writing a report).
I can set the control size to a fixed size using the following code:
Size renderSize = new Size(1000, 1000);
chartControl.Measure(renderSize);
chartControl.Arrange(new Rect(renderSize));
chartControl.UpdateLayout();
This correctly sets the size of the control to 1000x1000, but unfortunately the height of the control can vary - so I either risk losing a part of the picture, or include a region of blank canvas just to make sure.
I've tried various different approaches, but what I really want is something like:
chartControl.Resize("Auto"); // Pseudo code
chartControl.UpdateLayout();
Does anybody know a practical way to do this?
EDIT:
Just to clarify: at present the UserControl "chartControl" is currently never added to any container. I'm happy for that to remain the case, or to create a dummy container (like a Window) if this can "trick" WPF into sizing the UserControl...
EDIT 2:
Thanks to #Lennart for pointing me in the right direction for this:
It seems that DesiredSize is only available (i.e. not (0,0)) after the Measure call has been made, but after the Measure call, the Desired size is the minimum of the control's actual desired size and the size that it has been given in the Measure call. My original effort set the Measure too small, so I never found out the true desired size of the chartControl.
From this, the following ought to work:
Size renderSize = new Size(2000, 2000);
chartControl.Measure(renderSize);
chartControl.Measure(chartControl.DesiredSize);
chartControl.Arrange(new Rect(renderSize));
chartControl.UpdateLayout();
However, the second call to Measure doesn't have any affect - the ActualWidth and height after the first call becomes 2000, but doesn't get updated on the second call to Measure.
The work-around would be to destroy the original chartControl once its DesiredSize has been determined, and then create a new one using the previously determined DesiredSize. It seems a bit clunky, though...
Thanks to #Lennart for pointing me in the right direction for this, and everyone else who gave pointers:
It seems that DesiredSize is only available (i.e. not (0,0)) after the Measure call has been made, but after the Measure call, the Desired size is the minimum of the control's actual desired size and the size that it has been given in the Measure call. My original effort set the Measure too small, so I never found out the true desired size of the chartControl.
This appears to work:
Size overSize = new Size(2000, 2000); // Something oversized.
chartControl.Measure(overSize);
chartControl.Arrange(new Rect(chartControl.DesiredSize));
chartControl.UpdateLayout();
// Take a screenshot of the chart control
BitmapSource bitmapSource = ScreenshotHelper.CreateScreenshot(chartControl, new Int32Rect(0, 0, (int)chartControl.ActualWidth, (int)chartControl.ActualHeight));

Unity 2D Setting the value of a Scrollbar to 0

I just wanted to set the value of the scrollbar of my scrollview object to 0 every time a new text object is created. This way the newest object would always remain at the bottom without needing to scroll down (think of the chat of a video game for example). My problem is that for some reason the program is setting the value to 0 and then creating the text object, so it keeps the 2nd newest object at the bottom of the scrollview.
My code:
//Creates the Text objects
private GameObject GenerateTextObject(string content)
{
//Creates the Text objects
GameObject messagePrefab = Instantiate(MessagePrefab);
Text textComponent = messagePrefab.GetComponent<Text>();
//Sets the content of the text and parent
textComponent.text = content;
messagePrefab.transform.SetParent(ContentPanel.transform, false);
ScrollbarVertical.value = 0;
return messagePrefab;
}
In the code I am setting the value at the end of the function, but still sets the value of the scrollbar to 0 (properly moving the scrollview to the bottom) before the object is created.
https://gyazo.com/897982521f13d7792ec26540490a40c0
In the Gyazo picture you can see how it doesn't scroll all the way down.
I have tried using a coroutine and waitForEndFrame aswell as waitforseconds(1), but none seem to work.
Edit: when loading up Unity and sending new messages to the scrollview, I see the scrollbar go all the way down and then really quickly move up just a bit hiding the new text object.
What you can do is create a new Scroll View and follow these steps:
The result is from calling GenerateTextObject(Time.time.ToString()); every second.
private GameObject GenerateTextObject(string content)
{
//Creates the Text objects
GameObject messagePrefab = Instantiate(MessagePrefab);
Text textComponent = messagePrefab.GetComponent<Text>();
//Sets the content of the text and parent
textComponent.text = content;
messagePrefab.transform.SetParent(ContentPanel.transform, false);
//Force reset to the bottom on each message
//ScrollbarVertical.value = 0;
return messagePrefab;
}
I have not come up with a proper fix for this problem (I honestly do not know the cause of problem), but I have put together a little work-around for this situation.
To do this I simply created a small function:
public void Testing()
{
if (testing == true)
{
ScrollbarVertical.value = 0;
testing = false;
}
}
This would be attached to the built-in functionality of scrollbars "On Value Changed (Single)". In order to allow this to work, I just change the bool "testing" to true after creating a text object on GenerateTextObject(string content).
This fix is really ugly, and probably would cause more problems in the near future, but for now, it is a quick and dirty solution for this problem.
Sorry to necro this, but I had a similar problem and googling couldn't find a good answer. So I'm posting the solution that I found.
I tried to change the scrollbar value to 1 or 0, depending on the direction, so that everytime I open a UI panel, it'll start at the top. But no matter what I did, nothing seemed to work.
So my solution is to change the Y position of the content instead of the scrollbar value.
My scrollbar direction is set at bottom to top. Then I take the sizeDelta of the content, divide it by 2, (since I need the value of 1 for the scrollbar.value). I then multiplied it by -1 (if I dont, it'll start at the bottom). Then I set that value as the Y position of the content.
So far it works like a charm everytime I open my UI panel.
Hope this helps anyone looking for a solution.

How do I add a new image to the window of an already running game in XNA C# using an if statement in the Update method?

I am pretty sure something like this has been asked before, but I haven't been able to find it. Anyway, I am making a simple game animation with two moving objects (halloween themed). The objects bounce off the walls when they hit them, but I also need an image to be displayed when the two objects hit eachother. I have tried multiple ways, but none of them work. They either have no effect or raise an error. Anyway, here is the last thing I tried:
public PumpkinCheckCollisionPumpkin(GameTime gameTime)
{
if (pumpkin1.BoundingBox.Intersects(pumpkin2.BoundingBox))
{
pumpkinCollide = True;
Draw(gameTime);
}
I then tried passing the that bool (which I set to false earlier) to the draw section, but it did not work. The above method is called within the Update method.
I tried having the draw method invoked in the Update (GameTime gameTime) part, but that didn't work either. How do I trigger another image to be displayed in addition to what is already is displayed when my two objects collide? (also, that public method was originally private but made it public so another tactic I tried might work(didn't work)).
The answer is pretty simple, never directly call the Draw function.
In this case, I would set a class level state variable that holds the fact that a collision graphic should be drawn on the next draw call, which can then happen.
You erase every time you draw anyways, so even if the call did work correctly, the next Draw call (which will happen really soon) would erase it. For the same reason, don't unset the flag after the Draw call happens, because it will get erased faster than your players can see it. You need to keep it on the screen for some time.
You could do this by setting a variable that holds the time when the flag was set, then checking it against the current time until enough "visible" time has passed. This would then clear the "show collision" flag, and the graphic would no longer draw.
Just let the framework handle the timing of that call, and let the state you set drive what is drawn.

Silverlight different behaviour when being run from website

I am working on a simple Silverlight application. The point of application is to display data loaded from XML file. Data consists of String "password" and Integer "passwordCount". Each loaded position is displayed as a coloured square and whenever user moves mouse over this square it is resized to "passwordCount/10" pixel width or height and the "password" string is displayed on it. Here is an example:
On Mouseover:
Everything works fine if I run it via VisualStudio Run button. The problem is that when I place the script on website, tiles resize in the wrong direction (they become smaller instead of bigger). The text is not displayed either. I don't have the faintest idea why. Silverlight on website opens in a separate window and it looks like that on mouseover:
(source: screenshu.com)
Here is the function I use to animate tiles:
public void rectangle_MouseEnter(object sender, MouseEventArgs e)
{
sbMouseON = new Storyboard();
DoubleAnimation sizeAnimation = new DoubleAnimation();
sizeAnimation.To = passwordCount/10; //passwordCount is always greater than 1000
sizeAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(300));
Storyboard.SetTarget(sizeAnimation, (Rectangle)sender);
Storyboard.SetTargetProperty(sizeAnimation, new PropertyPath(direction));
sbMouseON.Children.Add(sizeAnimation);
Canvas.SetZIndex(rect, 2); //move rectangle up to make animation visible
DrawTextBlock();
sbMouseON.Begin();
}
Mods: Can't Add Comment so posting as answer
Here I am assuming that the "direction" you are passing in the TargetProperty is "(FrameworkElement.Width)" to increase the width of the rectangle.
If so only Logical Reason a Object can reduce in size is "sizeAnimation.To" property you are setting is setting the value which is smaller than the actual width of the object.
What is this DrawTextBlock() doing ? is there any code in there which can change the behavior of animation ?
The problem solved itself. I've just copied all files onto server and it works great. Looks like it was some Windows problem. Thank you for your interest #Abhinav.

Categories

Resources