I've been struggling with IMGUI (Legacy and Editor GUI system) a lot.
Problem is I cannot get my head around this Instant way and all the different events.
I built a simple example to demonstrate one of the many frustration I'm having with this system.
bool toggleValue;
void OnGUI()
{
if (toggleValue = GUILayout.Toggle(toggleValue, "Toggle"))
{
EditorGUILayout.LabelField("This is a label");
}
}
Alright, so, in this very simple code, I have a native toggle that shows or not a label depending on it's state. This works perfectly.
Now, I'm writing a piece myself.
bool toggleValue;
void OnGUI()
{
if (toggleValue = Toggle(toggleValue, "Toggle"))
{
EditorGUILayout.LabelField("This is a label");
}
}
bool Toggle(bool state, string label)
{
GUILayout.Label(label, state ? EditorStyles.boldLabel : GUIStyle.none);
if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition))
return !state;
return state;
}
My goal with this one is to manually reproduce the behaviour of a toggle: My clickable label is bold when active and normal when disable. Similarly to the checkbox graphic of the native toggle. Then, if I catch a click on it's rect, I return the opposite of the state.
In my head, this is suppose to work. However, I'm aware that there's a lot going on with the OnGUI function like the events Repaint and Layout for which the layout needs to be consistent during a frame. Here the error is
ArgumentException: Getting control 1's position in a group with only 1 controls when doing MouseDown
but often also errors like this one
ArgumentException: GUILayout: Mismatched LayoutGroup.MouseDown
I know that these errors are when you change the content between Layout and Repaint. However I cannot find a fix for my simple control. So here is my question.
With the same arguments given to my function Toggle, what do I need to take care of so the toggle works?
I feel like if I have the answer to this, I might be able to understand the key to this system.
Thank you very much
Oh my god I can't believe it. The ONLY thing I was missing was
Event.current.Use();
When the control is clicked.
That's all! Nothing about the layout being changed between Layout and Repaint or anything of the sort.
So if you've got the same problem, just consume the event when it succeeded.
Related
How do I get rid of this ugly line?
Draw a default bindingnavigator on an empty Form and you will see the problem. RenderMode is ManagerRenderMode. I want this render mode so the mouse over colors is correct. However, If I switch to System as rendermode the ugly line disapears, but then mouse over color/effect gets ugly.
I have been looking around for a solution for some time now, but nothing. Maybe someone here have seen this problem before?
It's not a BindingNavigator specific issue, but the ToolStrip which BindingNavigator inherits.
It's caused by the DrawToolStripBorder method when the ToolStripProfessionalRenderer class RoundedEdges property is true (the default).
In order to turn it off, I can suggest the following helper method:
public static class WindowsFormsExtensions
{
public static void DisableRoundedEdges(this ToolStripRenderer renderer)
{
var professionalRenderer = renderer as ToolStripProfessionalRenderer;
if (professionalRenderer != null)
professionalRenderer.RoundedEdges = false;
}
}
Now you can turn it off for the specific control (it's not available at design time, so it has to be at run time inside your form/control constructor or load event):
this.bindingNavigator1.Renderer.DisableRoundedEdges();
or to disable it globally, add the following in your Main method before calling Application.Run:
ToolStripManager.Renderer.DisableRoundedEdges();
I've noticed that when OnElementPropertyChanged is fired on a VisualElement like a BoxView, the properties of the underlying platform view are not updated at that time.
I want to know when the VisualElement's corresponding platform view is finished rendering, something like:
this.someBoxView.ViewHasRendered += (sender, e) => {
// Here I would know underlying UIView (in iOS) has finished rendering
};
Looking through some code inside of Xamarin.Forms, namely VisualElementRenderer.cs, it would seem that I could raise an event after OnPropertyChanged has finished. Something like:
protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName)
SetBackgroundColor(Element.BackgroundColor);
else if (e.PropertyName == Layout.IsClippedToBoundsProperty.PropertyName)
UpdateClipToBounds();
else if (e.PropertyName == PlatformConfiguration.iOSSpecific.VisualElement.BlurEffectProperty.PropertyName)
SetBlur((BlurEffectStyle)Element.GetValue(PlatformConfiguration.iOSSpecific.VisualElement.BlurEffectProperty));
// Raise event
VisualElement visualElement = sender as VisualElement;
visualElement.ViewHasRendered();
}
Naturally there's a few more complexities to adding an event to the VisualElement class, as it would need to be subclassed. But I think you can see what I'm after.
While poking around I've noticed properties on VisualElement like IsInNativeLayout. But that only seems to be implementing in Win/WP8. Also, UpdateNativeWidget on VisualElementRenderer as well, however I can't figure out the proper way to leverage them.
Any ideas?
Much appreciated.
TL;DR : Run away, do not go down this path...
On iOS everything that displays content to the screen happens within a UIView (or subclass) and drawRect: is the method that does the drawing. So when drawRect: is done, the UIView is drawing is done.
Note: Animations could be occurring and you might see hundreds of completed rendering cycles completed. You might need to hook into every animation's completion handler to determine when things really are done "rendering".
Note: The drawing is done off-screen and depending upon the iDevice, the screen refresh Hz could 30FPS, 60FPS or in the case of iPad Pro it is variable (30-60hz)...
Example:
public class CustomRenderer : ButtonRenderer
{
public override void Draw(CGRect rect)
{
base.Draw(rect);
Console.WriteLine("A UIView is finished w/ drawRect: via msg/selector)");
}
}
On Android the common way to draw content is via a View or subclass, you could obtain a surface, draw/bilt via OpenGL to a screen, etc... and that might not be within a View, but for your use-case, think Views..
Views have Draw methods you can override, and you can also hook into ViewTreeObserver and monitor OnPreDraw and OnDraw, etc, etc, etc... Sometimes you have to monitor the View's parent (ViewGroup) to determine when drawing is going to be done or when is completed.
Also all standard Widgets are completely inflated via xml resources and that is optimized so you will never see a Draw/OnDraw method call (Note: You should always(?) get a OnPreDraw listener call if you force it).
Different Views / Widgets behave differently and there no way to review all the challenges you will have determining when a View is really done "rendering"...
Example:
public class CustomButtonRenderer : Xamarin.Forms.Platform.Android.AppCompat.ButtonRenderer,
ViewTreeObserver.IOnDrawListener, ViewTreeObserver.IOnPreDrawListener
{
public bool OnPreDraw() // IOnPreDrawListener
{
System.Console.WriteLine("A View is *about* to be Drawn");
return true;
}
public void OnDraw() // IOnDrawListener
{
System.Console.WriteLine("A View is really *about* to be Drawn");
}
public override void Draw(Android.Graphics.Canvas canvas)
{
base.Draw(canvas);
System.Console.WriteLine("A View was Drawn");
}
protected override void Dispose(bool disposing)
{
Control?.ViewTreeObserver.RemoveOnDrawListener(this);
Control?.ViewTreeObserver.RemoveOnPreDrawListener(this);
base.Dispose(disposing);
}
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
Control?.SetWillNotDraw(false); // force the OnPreDraw to be called :-(
Control?.ViewTreeObserver.AddOnDrawListener(this); // API16+
Control?.ViewTreeObserver.AddOnPreDrawListener(this); // API16+
System.Console.WriteLine($"{Control?.ViewTreeObserver.IsAlive}");
}
}
}
Misc:
Note: Layout Optimizations, Content caching, GPU caching, is hardware acceleration enabled in the Widget/View or not, etc... can prevent the Draw methods from being called...
Note: Animation, effects, etc... can cause these these methods to be call many, many, many times before an area of the screen is completely finished displaying and ready for user interaction.
Personal Note: I've gone down this path once due to a crazy client requirements, and after banging my head on the desk for some time, a review of the actual goal of that area of the UI was done and I re-wrote the requirement and never tried this again ;-)
I'm going to answer my own question it hopes that the solution will help someone who is struggling with this issue in the future.
Follow #SushiHangover's advice and RUN don't WALK away from doing something like this. (Although his recommendation will work and is sound). Attempting to listen/be notified when the platform has finished rendering a view, is a terrible idea. As #SushiHangover mentions there's simply too many things that can go wrong.
So what brought me down this path?
I have a requirement for a pin code UI similar to the one in iOS to unlock your device, and in many other apps. When a user presses a digit on the pad I want to update the corresponding display "box" (boxes above the pad). When the user inputs the last digit I want the last "box" to be filled in, in my case a Background color change, and then execution to continue in which the view would transition to the next screen in the workflow.
A problem arose as I tried to set the BackgroundColor property on the fourth box and then transition the screen. However, since execution doesn't wait for the property to change the screen transitions before the change is rendered. Naturally this makes for a bad user experience.
In an attempt to fix it, I thought "Oh! I simply need to be notified when the view has been rendered". Not smart, as I've mentioned.
After looking at some objective C implementations of similar UIs I realizes that the fix it quite simple. The UI should wait for a brief moment and allow the BackgroundColor property to render.
Solution
private async Task HandleButtonTouched(object parameter)
{
if (this.EnteredEmployeeCode.Count > 4)
return;
string digit = parameter as string;
this.EnteredEmployeeCode.Add(digit);
this.UpdateEmployeeCodeDigits();
if (this.EnteredEmployeeCode.Count == 4) {
// let the view render on the platform
await Task.Delay (1);
this.SignIn ();
}
}
A small millisecond delay is enough to let the view finished rendering without having to go down a giant rabbit hole and attempt to listen for it.
Thanks again to #SushiHangover for his detailed response. You are awesome my friend! :D
Looking at some of the answers in the Unity forums and Q&A site, the answers for how to make an invisible button do not work because taking away the image affiliated with the button makes it not work.
How do you get around this and keep the invisible property while allowing the button to actually work?
This is one of those weird things about Unity...
100% of real-world projects need this, but Unity forgot to do it.
Short version:
You need Touchable.cs in every Unity project:
// file Touchable.cs
// Correctly backfills the missing Touchable concept in Unity.UI's OO chain.
using UnityEngine;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
[CustomEditor(typeof(Touchable))]
public class Touchable_Editor : Editor
{ public override void OnInspectorGUI(){} }
#endif
public class Touchable:Text
{ protected override void Awake() { base.Awake();} }
Use Unity's ordinary 'Create Button' editor function
As you know, the editor function adds two components for you automatically. One is a Text and one is an Image...
Simply delete them both
Drop the above script Touchable.cs on the Button
You are done. That's all there is to it.
It cannot "decay" with Unity upgrades.
You can actually "buttonize" anything in .UI by dropping Touchable on top of it.
Never again "add a transparent Image" to make a button.
Unity forgot to abstract a "touchable" concept in the OO chain.
So, us developers have to make our own Touchable class "from" Unity's classes.
This is a classic "backfilling" problem in OO.
When "backfilling" the only issue is that: it must be perfectly auto-maintaining. There is only one good solution, Touchable.cs, which everyone uses.
So in all real-world Unity projects a button looks like this:
ONE You have Unity's Button.cs
TWO you have to add Touchable.cs
Some teams make an editor function "Create Better Button" which simply makes a game object, with, Button.cs + Touchable.cs.
Important tip...
Say you may have a very complex UI panel. So it resizes or even has an animation.
In fact, you can just drop "Button+Touchable" on to anything like that, and it will work.
Just set the Button+Touchable so as to expand to fill the parent. That's all there is to it.
In this example image, "resume" and "quit" could be anything. (An animation, a complicated panel with many parts, text, sprites, something invisible, a stack - anything.)
In all cases, just drop a Button+Touchable underneath and you have a flawless button.
In fact: this approach is so simple, you'll probably use it for even simple cases.
Say your button is a trivial image. It's much easier to just have an image, and then drop a Button+Touchable on it. (Rather than use the confusing and problematic "Button" function in the editor.)
Understanding the situation...
1) Unity's Button.cs class is fantastic.
2) But the editor function "make a Button" is garbage...
3) It makes an "upside down" button,
4) i.e., it puts a text/image under Button.cs
5) "Button-ness" is something you should be able to add to anything at all. This is precisely how it works with Button+Touchable.
6) So - quite simply -
1. Have anything you want. Text, image, panel, invisible, animation - whatever.
2. Drop Button+Touchable on it - you're done.
That's how everyone does all buttons in Unity!
Historic credit: I believe Unity forum user "signalZak" was the first to think this out many, many years ago!
As a possible improvement to Fattie's answer, changing Touchable's base class to Graphic and overriding protected void UpdateGeometry() seems to work quite nicely white reducing the (admittedly minor) overhead associated with Text.
public class Touchable:Graphic
{
protected override void UpdateGeometry() { }
}
My first solution was to enable and disable the components like below:
void showButton(Button buttonToShow, bool show)
{
Image bImage = buttonToShow.GetComponent<Image>();
Text bText = buttonToShow.GetComponentInChildren<Text>(); //Text is a child of the Button
if (bImage != null)
{
bImage.enabled = show;
}
if (bText != null)
{
bText.enabled = show;
}
}
but that didn't work. If the button's image and text components are both disabled, the button click event will NOT fire. One of them MUST be enabled in able for click events to be sent.
The solution is to set the alpha of both the image and text components to 0 to hide and to 1 to show again. They will be hidden but not disabled and click events will work.
public Button button;
void Start()
{
//Show Button
showButton(button, true);
//Hide Button
//showButton(button, false);
}
void showButton(Button buttonToShow, bool show)
{
Image bImage = buttonToShow.GetComponent<Image>();
Text bText = buttonToShow.GetComponentInChildren<Text>(); //Text is a child of the Button
if (bImage != null)
{
Color tempColor = bImage.color;
if (show)
{
tempColor.a = 1f; //Show
bImage.color = tempColor;
}
else
{
tempColor.a = 0f; //Hide
bImage.color = tempColor;
}
}
if (bText != null)
{
Color tempColor = bText.color;
if (show)
{
tempColor.a = 1f; //Show
bText.color = tempColor;
}
else
{
tempColor.a = 0f; //Hide
bText.color = tempColor;
}
}
}
I fired up Gimp (that free coder graphic tool). Created new image (any size, I chose 10 pix x 10 pix), selected from advanced (in create dialog) that it's backgroud should be transparent. Saved the file. Exported it as png with save backgroud color selected. Dragged it into Unity as sprite. Put that to the button graphic. Disbaled the text-component of the button. No code required ... just don't draw anything while in Gimp (that was the hardest part).
I have a full screen WPF application built for a touch monitor, and I have some Listboxs on the main screen.
When I flick the 'Listbox' it scrolls fine, but when it gets to the end of the list, the entire application gets pulled down from the top of the screen, can I stop this behavior somehow?
Has anyone else seen this?
Yes, that default behaviour of the ListBox (or rather, the ScrollViewer inside the default ListBox template) is weird - when I first came across it, I thought it must be a practical joke. In fact, it's really hard to find any documentation about it - but it is briefly mentioned here:
The ManipulationBoundaryFeedback event enables applications or components to provide visual feedback when an object hits a boundary. For example, the Window class handles the ManipulationBoundaryFeedback event to cause the window to slightly move when its edge is encountered.
So, a way around it is to handle ManipulationBoundaryFeedback on the ListBox, and set Handled to true:
<ListBox ManipulationBoundaryFeedback="OnManipulationBoundaryFeedback">
// ...
</ListBox>
Code-behind:
private void OnManipulationBoundaryFeedback(object sender, ManipulationBoundaryFeedbackEventArgs e)
{
e.Handled = true;
}
Is there some way to (temporarily) prevent user from changing the value of TrackBar by dragging the slider? Only way I found is to set Enabled property to false but it also greys out the trackbar.
Edit:
Since I´m getting more answers about why shouldn't I do it rather than how it's possible to do it, I decided to explain why I would like to do it.
I'm not using it to allow user adjust value of some application property, but to display and control progress of some action. Imagine for example your favourite media player - it probably contains some control (I'd call it trackbar but English is not my native language so it's maybe wrong)
that displays what part of movie it's currently playing, but also allows you to control it - move back or forward in time and watch the different part.
I use the trackbar in exactly this way - I don't know any other component that would be better (Progressbar won't allow me changing the "position"). It's working fine,
the only thing I'd like to do is not to allow user to "use the trackbar unless the movie is paused".
For this exact reason I've used trackbar component many times in Delphi 6, but when it was disabled it didn't grey out and in my opinion it worked fine. That's why I asked
here if it's possible to achieve the same effect in C#.
The best way would be to set the Enabled property to false, as you rightly pointed out it greys out the track bar too.
Greying out the controls is a windows standard for saying "You cannot use this control at the moment", just imagine you were presented with some controls that was not grayed out where disabled, it would be a very hit and miss affair trying them all to see which is enabled and which isn't.
If you really want to prevent changes without using the Enabled property one possible alternative is to use the ValueChanged event of the TrackBar
private void trackBar1_ValueChanged(object sender, EventArgs e)
{
// Code here to restore the original value
}
If you wanted to be ambitious (or overzealous), you could create a Transparent Panel control to overlay the TrackBar thereby preventing the user from interacting with it, i.e.
public class TransparentPanel : System.Windows.Forms.Panel
{
private const int WS_EX_TRANSPARENT = 0x00000020;
protected override CreateParams CreateParams
{
get
{
CreateParams transparentParams = base.CreateParams;
transparentParams.ExStyle |= WS_EX_TRANSPARENT;
return transparentParams;
}
}
protected override void OnPaintBackground(System.Windows.Forms.PaintEventArgs e)
{
// Do Nothing
}
}
Add this control to your form and then position it over the TrackBar, i.e.
transparentPanel.Location = trackBar1.Location;
transparentPanel.Size = trackBar1.Size;
Then you can hide it when you want to allow user interaction.
As Xander pointed out, having a trackbar that looks perfectly normal but can't actually be used is a good way to drive your users crazy!
If you're worried about the trackbar looking "inactive", you might try another way of displaying the data it represents, like a label. That way when you disable the trackbar, you're only indicating that the editing of the value is unavailable.