I'm trying to make a tooltip pop up when I hover my mouse over a button. I had some issues calling the main thread from a separate thread, since the main thread is the only thread you can edit game objects from. So I decided to use a boolean and an update function to get the desired result. However, I ran into an issue I have never seen before. I am setting my boolean showToolTip to true, which is supposed to trigger the create pop-up function. However, by the time my update function runs, showTooltip is always false. I know when I set it to true, it becomes true, because I used debug statements in the requestShowTooltip function to see this. However, it is always false when the update function runs. I did some tests by changing showToolTip and removeToolTip to public booleans. I can change them in the editor while running the application, and when I manually change them through the editor they work 100% as expected, and the tooltip will appear and hide as I change the correlating booleans. However when they are changed from the script they are retained as false. I have also tried leaving them unitialized and initializing them to false in the start, and I was still having the same problem. I even tried taking "showTooltip = true;" out of the thread and just having the function do it immediately, and I was still having the same issue. showTooltip is always false when the update function is called. I have determined this by adding a debug statement in the update function that reports the value of the showTootip and removeTooltip booleans. Furthermore, no matter what, "Calling create tooltip" never appears in the console. Except for when I made the booleans public and changed them through the editor.
Some further explanation on how I set this up. I made a Tooltip Game Object that is a child of the canvas that contains the buttons I want the tooltip for. I added the Tooltips script to the tooltip object. I created event triggers in the button Game Objects. the Pointer Enter trigger calls the requestShowTooltip() function, and the Pointer Exit trigger calls the requestHideTooltip() function.
The Tooltip Background and Tooltip Text Game Objects are also children of this same canvas.
public class Tooltips : MonoBehaviour
{
private GameObject toolTipBackground;
private GameObject toolTipText;
private GameObject inButtonObject;
private bool showTooltip = false;
private bool removeTooltip = false;
void Start()
{
toolTipBackground = GameObject.Find("Tooltip background");
toolTipText = GameObject.Find("Tooltip Text");
//inButton = GameObject.Find("Aft Button");
toolTipBackground.SetActive(false);
toolTipText.SetActive(false);
//Debug.Log("Tool Tip Start");
}
void Update()
{
// show the tooltip when the appropriate boolean has been set
// to true
if(removeTooltip)
{
hideTooltip();
}
// this if statement is always false, because showTooltip is
// always false when the update function is called??
if(showTooltip)
{
Debug.Log("Calling create tooltip");
createTooltip();
}
}
// A timed request for a tooltip to show. The tooltip should
// currently show one second after the request has been made. The
// text of the tooltip will be equal to the passed object's name
public void requestShowTooltip(GameObject inButtonObject)
{
Thread thread = new Thread(delegate ()
{
System.Threading.Thread.Sleep(1000);
this.inButtonObject = inButtonObject;
// I know this runs, but showTooltip is always returning
// to false when the update function is called??
showTooltip = true;
removeTooltip = false;
Debug.Log("Request function completed");
});
thread.start();
}
public void createTooltip()
{
toolTipText.SetActive(true);
toolTipBackground.SetActive(true);
string labelString = inButtonObject.name;
Button inButton = inButtonObject.GetComponent<Button>();
int width = labelString.Length * 10;
int height = 35;
Vector2 size = new Vector2(width, height);
Vector3 position = new Vector3(inButton.transform.x,
inButton.transform.position.y,
inButton.transform.position.z + 1);
toolTipBackground.GetComponent<RectTransform>().
sizeDelta = size;
toolTipText.GetComponent<RectTransform>().sizeDelta = size;
toolTipBackground.transform.position = position;
toolTipText.transform.position = position;
toolTipText.GetComponent<Text>().text = labelString;
showTooltip = false;
}
public void requestHideTooltip()
{
removeTooltip = true;
showTooltip = false;
}
public void hideTooltip()
{
Vector2 hide = new Vector2(0, 0);
toolTipBackground.GetComponent<RectTransform>().
sizeDelta = hide;
toolTipText.GetComponent<RectTransform>().sizeDelta = hide;
toolTipText.setActive(false);
toolTopBackground.SetActive(false);
removeTooltip = false;
showTooltip = false;
}
}
Since basically everything in Unity needs to be called in the main Thread (with very few exceptions) I would suggest to change your code using a ConcurrentQueue<Action> and TryDequeue to let the main thread work through all the responses from any thread.
// a queue for storing actions that shall be handled by the main thread
// a ConcurrentQueue in specific is thread-save
private ConcurrentQueue<Action> actions = new ConcurrentQueue<Action>();
private void Update()
{
// invoke all actions from the threads in the main thread
while(!actions.IsEmpty)
{
// TryDequeue writes the first entry to currentAction
// and at the same time removes it from the queue
// if it was successfull invoke the action otherwise do nothing
if(actions.TryDequeue(out var currentAction))
{
currentAction?.Invoke();
}
}
if(removeTooltip)
{
hideTooltip();
}
if(showTooltip)
{
Debug.Log("Calling create tooltip");
createTooltip();
}
}
Then in your Thread instead of making the thread directly execute the stuff itself rather pass it back to the main thread using the actions queue and add a new Action to the end using Enqueue
public void requestShowTooltip(GameObject inButtonObject)
{
Thread thread = new Thread(delegate()
{
System.Threading.Thread.Sleep(1000);
// add an Action to the end of the queue
// e.g. as lambda-expression
actions.Enqueue(()=>
{
this.inButtonObject = inButtonObject;
// I know this runs, but showTooltip is always returning
// to false when the update function is called??
showTooltip = true;
removeTooltip = false;
Debug.Log("Request function completed");
});
});
thread.start();
}
You might want to consider btw to make inButtonObject of type Button - first in order to make sure the passed object always has a/is a Button and second you can get rid of the performance intense GetComponent call.
The same way I would rather store
RectTransform toolTipBackgroundRectTransform;
Text toolTipText;
RectTransform toolTipTextrectTransform;
since those are the components you can get already once in Start and then you can re-use always the same references and get rid of all further GetComponent calls.
Related
I'm making a system to balance calls inside of the OnGUI method of a EditorWindow.
I'm doing the following:
public void Update()
{
Repaint();
}
Inside of my OnGUI method I'm calling this Balancer. I have one list with the callbacks (List).
So the idea is simple, some callvaxc
I'm skipping some repaint frames for the callback that has the complete GUI, and calling on each repaint for other callbacks (for example, a marquee label or dispalying gifs).
By some reason, this error happens "Getting control 0's position in a group with only 0 controls when doing repaint"
private int m_repaintCounter;
public void Draw()
{
Event e = Event.current;
try
{
foreach (var action in m_actions)
{
try
{
// Test 1
// MainAction is a class that inherits from Action (class MainAction : Action)
if (action is MainAction)
{
bool isDesignedType = e.rawType == EventType.Repaint || e.rawType == EventType.Layout;
if (isDesignedType)
++m_repaintCounter;
if (!(m_repaintCounter == 20 && isDesignedType))
continue;
else
m_repaintCounter = 0;
}
// Test 2
action.Value();
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
}
catch
{
// Due to recompile the collection will modified, so we need to avoid the exception
}
}
But if I comment the "Test 1" everythings works fine.
On the ctor of the class we need to specify a callback to a GUI method, for example:
public Balancer(Action drawAction)
{
m_actions = new List<Action>();
m_actions.Add(drawAction);
}
So we could do easily (inside the EditorWindow):
private Balancer m_balancer;
public void OnEnable()
{
m_balancer = new Balancer(Draw);
}
public void Draw()
{
// This block will be called every 20 repaints as specified on the if statment
GUILayout.BeginHorizontal("box");
{
GUILayout.Button("I'm the first button");
GUILayout.Button("I'm to the right");
// This marquee will be called on each repaint
m_balancer.AddAction(() => CustomClass.DisplayMarquee("example"));
}
GUILayout.EndHorizontal();
}
// Inside of the Balancer class we have
// We use System.Linq.Expressions to identify actions that were added already
private HashSet<string> m_alreadyAddedActions = new HashSet<string>();
public void AddAction(Expression<Action> callback)
{
if(!m_alreadyAddedActions.Add(callback.ToString()))
return;
m_actions.Add(callback.Compile());
}
I can't figure this out. I couldn't find any information on the internet. Can anyone help me?
Ok, so, OnGui (IMGui) is awful to work with. If you aren't using it for an editor script, use the new 4.6 UI (UGui) instead.
Now then. The problem.
OnGui is called at least twice every frame. One of those is to calculate layouts and the other is to actually draw stuff ("repaint").
If the number of things, size of things, or anything else changes between these two calls then Unity will error with "Getting control 0's position in a group with only 0 controls when doing repaint."
That is: you cannot change UI state in the IMGui system at any point after Layout and before Repaint.
Only, only, only change state (and thereby which objects are being drawn) during Event.current == EventType.Repaint and only, only, only change state for the next frame (alternatively, do the changes during Event.current == EventType.Layout, provided that this same, new state will result in the same code path during Repaint). Do not, under any circumstances, make changes during Repaint that were not present during the previous Layout.
My ultimate goal is to only enable the photo capture button when a face is actually detected in the frame. Adding face detection using AVCaptureMetadataOutput is really simple, but I am having trouble finding a way to tell when the frame is empty. The AVCaptureMetadataOutput only has the one delegate method, DidOutputMetadataObjects and it is only called when the AVCaptureMetadataOutput detects an object, not when there is no object. I need a method that continually fires every second or so, or I need a way to check the detected objects within a timer.
I need something like:
IsFaceDetected () {
isButtonEnabled = metadataOutput.IsDetectingObject();
}
or
new Timer(repeat 1000ms) {
if (metadataOutput.DetectedObject == Object.Face) {
buttonEnabled == true;
} else {
buttonEnabled == false;
}
}
Has anyone discovered a way to make something like this work?
Well I was unable to really find a way to do what I wanted so I just resorted to adding a single use timer to the end to the end of the callback method to disable the button IF the callback method was not called again.
public void DidOutputMetadataObjects(AVCaptureMetadataOutput captureOutput,
AVMetadataObject[] metadataObjects, AVCaptureConnection connection)
{
// dispose of the existing timer if there is one
_timer?.Dispose();
if (metadataObjects.OfType<AVMetadataFaceObject>() != null) {
Button.Enabled = true;
} else {
Button.Enabled = false;
}
// have the timer disable the button if no face is detected again
_timer = new Timer(new TimerCallback((object state) => {
InvokeOnMainThread(() => {
Button.Enabled = true;
});
}), null, 150, Timeout.Infinite);
}
I have two threads which uses the BeginInvoke method to change some Windows Form object's (Panel and Label) visibility attribute to false.The problem is that I'm not sure when the change happens. I can see that the panel is not there (so the BeginInvoke method works) but my if condition to check the visibility status always returns true the first time the form is activated.
bool notVisible = false;
private void LunchMainScreen_Activated(object sender, EventArgs e) {
String CurrentSite = "";
List<DateTime> availableDates = new List<DateTime>();
// Get available dates
Thread availableDatesThread = new Thread(delegate() {
availableDates = LunchUserPreferences.GetUserAvailableDates();
changeObjVisible(notVisible, selectAvailabilityPanel);
changeObjVisible(notVisible, whenLbl);
}
});
availableDatesThread.Start();
// Get user current site
Thread checkSiteThread = new Thread(delegate() {
CurrentSite = LunchUserPreferences.GetUserSite();
changeObjVisible(notVisible, selectSitePanel);
changeObjVisible(notVisible, whereLbl);
}
updateText(CurrentSite, CurrentSiteSetLbl);
});
checkSiteThread.Start();
while (selectSitePanel.Visible == false && selectAvailabilityPanel.Visible == false) {
// it NEVER gets here, even though the panels are NOT visible when the program loads
WhoLunchTable.Visible = false;
WhoLunchTable.SuspendLayout();
listOfAvailableGroups.Clear();
WhoLunchTable.Controls.Clear();
WhoLunchTable.RowStyles.Clear();
PopulateTable();
WhoLunchTable.Visible = true;
WhoLunchTable.ResumeLayout();
break;
}
}
private delegate void changeObjVisibleDelegate(bool visibility, object obj);
private void changeObjVisible(bool visibility, object obj) {
if (this.InvokeRequired) {
this.BeginInvoke(new changeObjVisibleDelegate(changeObjVisible), new object[] { visibility, obj });
return;
}
// downcast to the correct obj
if (obj is Panel) {
Panel panel = (Panel)obj;
panel.Visible = visibility;
}
if (obj is Label) {
Label lbl = (Label)obj;
lbl.Visible = visibility;
}
}
private delegate void updateTextDelegate(string text, Label lbl);
private void updateText(string text, Label lbl) {
if (this.InvokeRequired) {
this.BeginInvoke(new updateTextDelegate(updateText), new object[] { text, lbl });
return;
}
lbl.Text = text;
}
It does work fine when the Form is activated for the second time, for example:
The form loads for the first time and it doesn't go inside the while loop.
I minimize the form/program.
The LunchMainScreen_Activated runs again and it works as it should because it recognises that the panels are not visible.
UPDATE:
I had an idea after reading AlexF answer which solved the problem but it doesn't look like the ideal solution:
I've created a while condition that will only stop when both threads are not alive and an if condition inside it that will get this point in time and execute what I need:
while (availableDatesThread.IsAlive || checkSiteThread.IsAlive) {
// At least one thread is still alive, keeps checking it...
if (!availableDatesThread.IsAlive && !checkSiteThread.IsAlive) {
// Both threads should be dead now and the panels not visible
WhoLunchTable.Visible = false;
WhoLunchTable.SuspendLayout();
listOfAvailableGroups.Clear();
WhoLunchTable.Controls.Clear();
WhoLunchTable.RowStyles.Clear();
PopulateTable();
WhoLunchTable.Visible = true;
WhoLunchTable.ResumeLayout();
break;
}
}
Reading your code the first time the code doesn't enter in the while loop because selectSitePanel.Visible and selectAvailabilityPanel.Visible are true: this is because the availableDatesThread.Start(); and checkSiteThread.Start(); are started but not finished; those two calls are not blocking so the code continues and skips the while.
Meanwhile the two backgrund threads finishes so the second time the "Activated" event is raised the variables values are "correct" (at least for the last cycle).
Without waiting for the threads to finish you are rushing through the code before having a result for the needed value.
In other words, it's better to not use a background thread to update an interface for the use you need.
If you need you may continue to use the code the way you are using it but moving the "while" section in two separate functions: they may be called when the threads have finished their work and refresh the window in this moment and not in the "activate" event.
I have a Winforms Application with a TabStrip Control. During runtime, UserControls are to be loaded into different tabs dynamically.
I want to present a "User Control xyz is loading" message to the user (setting an existing label to visible and changing its text) before the UserControl is loaded and until the loading is completely finished.
My approaches so far:
Trying to load the User Control in a BackgroundWorker thread. This fails, because I have to access Gui-Controls during the load of the UserControl
Trying to show the message in a BackgroundWorker thread. This obviously fails because the BackgroundWorker thread is not the UI thread ;-)
Show the Message, call DoEvents(), load the UserControl. This leads to different behaviour (flickering, ...) everytime I load a UserControl, and I can not control when and how to set it to invisible again.
To sum it up, I have two questions:
How to ensure the message is visible directly, before loading the User control
How to ensure the message is set to invisible again, just in the moment the UserControl is completely loaded (including all DataBindings, grid formattings, etc.)
what we use is similar to this:
create a new form that has whatever you want to show the user,
implement a static method where you can call this form to be created inside itself, to prevent memory leaks
create a new thread within this form so that form is running in a seperated thread and stays responsive; we use an ajax control that shows a progress bar filling up.
within the method you use to start the thread set its properties to topmost true to ensure it stays on top.
for instance do this in your main form:
loadingForm.ShowLoadingScreen("usercontrollname");
//do something
loadingform.CloseLoadingScreen();
in the loading form class;
public LoadingScreen()
{
InitializeComponent();
}
public static void ShowLoadingScreen(string usercontrollname)
{
// do something with the usercontroll name if desired
if (_LoadingScreenThread == null)
{
_LoadingScreenThread = new Thread(new ThreadStart(DoShowLoadingScreen));
_LoadingScreenThread.IsBackground = true;
_LoadingScreenThread.Start();
}
}
public static void CloseLoadingScreen()
{
if (_ls.InvokeRequired)
{
_ls.Invoke(new MethodInvoker(CloseLoadingScreen));
}
else
{
Application.ExitThread();
_ls.Dispose();
_LoadingScreenThread = null;
}
}
private static void DoShowLoadingScreen()
{
_ls = new LoadingScreen();
_ls.FormBorderStyle = FormBorderStyle.None;
_ls.MinimizeBox = false;
_ls.ControlBox = false;
_ls.MaximizeBox = false;
_ls.TopMost = true;
_ls.StartPosition = FormStartPosition.CenterScreen;
Application.Run(_ls);
}
Try again your second approach:
Trying to show the message in a BackgroundWorker thread. This obviously fails because the BackgroundWorker thread is not the UI thread ;-)
But this time, use the following code in your background thread in order to update your label:
label.Invoke((MethodInvoker) delegate {
label.Text = "User Control xyz is loading";
label.Visible = true;
});
// Load your user control
// ...
label.Invoke((MethodInvoker) delegate {
label.Visible = false;
});
Invoke allows you to update your UI in another thread.
Working from #wterbeek's example, I modified the class for my own purposes:
center it over the loading form
modification of its opacity
sizing it to the parent size
show it as a dialog and block all user interaction
I was required to show a throbber
I received a null error on line:
if (_ls.InvokeRequired)
so I added a _shown condition (if the action completes so fast that the _LoadingScreenThread thread is not even run) to check if the form exists or not.
Also, if the _LoadingScreenThread is not started, Application.Exit will close the main thread.
I thought to post it for it may help someone else. Comments in the code will explain more.
public partial class LoadingScreen : Form {
private static Thread _LoadingScreenThread;
private static LoadingScreen _ls;
//condition required to check if the form has been loaded
private static bool _shown = false;
private static Form _parent;
public LoadingScreen() {
InitializeComponent();
}
//added the parent to the initializer
//CHECKS FOR NULL HAVE NOT BEEN IMPLEMENTED
public static void ShowLoadingScreen(string usercontrollname, Form parent) {
// do something with the usercontroll name if desired
_parent = parent;
if (_LoadingScreenThread == null) {
_LoadingScreenThread = new Thread(new ThreadStart(DoShowLoadingScreen));
_LoadingScreenThread.SetApartmentState(ApartmentState.STA);
_LoadingScreenThread.IsBackground = true;
_LoadingScreenThread.Start();
}
}
public static void CloseLoadingScreen() {
//if the operation is too short, the _ls is not correctly initialized and it throws
//a null error
if (_ls!=null && _ls.InvokeRequired) {
_ls.Invoke(new MethodInvoker(CloseLoadingScreen));
} else {
if (_shown)
{
//if the operation is too short and the thread is not started
//this would close the main thread
_shown = false;
Application.ExitThread();
}
if (_LoadingScreenThread != null)
_LoadingScreenThread.Interrupt();
//this check prevents the appearance of the loader
//or its closing/disposing if shown
//have not found the answer
//if (_ls !=null)
//{
_ls.Close();
_ls.Dispose();
//}
_LoadingScreenThread = null;
}
}
private static void DoShowLoadingScreen() {
_ls = new LoadingScreen();
_ls.FormBorderStyle = FormBorderStyle.None;
_ls.MinimizeBox = false;
_ls.ControlBox = false;
_ls.MaximizeBox = false;
_ls.TopMost = true;
//get the parent size
_ls.Size = _parent.Size;
//get the location of the parent in order to show the form over the
//target form
_ls.Location = _parent.Location;
//in order to use the size and the location specified above
//we need to set the start position to "Manual"
_ls.StartPosition =FormStartPosition.Manual;
//set the opacity
_ls.Opacity = 0.5;
_shown = true;
//Replaced Application.Run with ShowDialog to show as dialog
//Application.Run(_ls);
_ls.ShowDialog();
}
}
Thanks in advance for your help. I'm not a programmer or student asking for homework help, just a technician helping out with work projects.
This one should be easy, but I've brain-farted and I need your help. I've written a GUI control program in C#, but I'll write examples in semi-pseudocode for clarity. I have two boolean control variables and List of void doSomething() methods.
bool ready = true;
bool willLoop = true;
runButton.Click(...) { ...
willLoop is set true/false by a toggle button. I want runButton.Click(...) to iterate through the List of doSomethings(), each of which set ready to false and then back to true when they have finished executing. When ready is set back to true, the next Item in the list will execute and set ready to false and back to true when it is finished. If willLoop is true then the program should iterate though the List over and over again, executing each item. If somebody presses the toggle button and sets willLoop to true or false while the program is executing, I need the program to finish iterating through the list and then either stop(break?) if willLoop is false after the last Item is executed, or iterate again if willLoop is true. All the threading/realtime stuff is handled automatically, I just need a (nested?) looping structure that will use the control variables to do what I need.
I'm only interested in the looping/iterating portion of the code so pseudocode will be fine. The real-world application is controlling an external device with a serial port - each item in the list is a method which sends a command to the device and sets ready to false. When the device finishes moving, it sends a string back and that listener sets ready back to true.
Thanks again for your help.
the structure you need is something like
runButton.Click(...) {
ready = true
while (willLoop)
for each item in list
ready = false
process item
wait until (ready == true)
}
I'm not practical with C# but I guess that Click callback is no asynchronous so as soon as you click the button everything will stop and you won't be able toggle it again. Did you already take care of manage such multithreading issues?
Here is my though on the matter :
public partial class MyFormClass : Form
{
private bool ready = true;
private object readyLock = new object();
private bool willLoop = true;
List<Action> myFunctionList = new List<Action>();
private void button1_Click(object sender, EventArgs e)
{
Queue<Action> remainingActions = new Queue<Action>();
do
{
remainingActions = new Queue<Action>(myFunctionList);
while (remainingActions.Count > 0)
{
lock (readyLock)
{
// In case someone was already in the while,
// but took the last item in the queue.
if (remainingActions.Count == 0) break;
ready = false;
Action currentAction = remainingActions.Dequeue();
currentAction();
ready = true;
}
}
} while (willLoop);
}
}
You'll understand that you can substitute "myFunctionList" by whatever mean you want your function list to be stored.