The new Unity UI system has now been out for over a year. So I thought I’d do a blog post about the old UI system, IMGUI.

新的Unity UI系统现已推出一年多了。 所以我想写一篇关于旧UI系统IMGUI的博客文章。

Strange timing, you might think. Why care about the old UI system now that the new one is available? Well, while the new UI system is intended to cover every in-game user interface situation you might want to throw at it, IMGUI is still used, particularly in one very important situation: the Unity Editor itself. If you’re interested in extending the Unity Editor with custom tools and features, it’s very likely that one of the things you’ll need to do is go toe-to-toe with IMGUI.

您可能会认为计时奇怪。 既然新的UI系统可用,为什么还要关心旧的UI系统? 好吧,尽管新的UI系统旨在涵盖您可能想扔给它的每种游戏内用户界面情况,但仍使用IMGUI,尤其是在一种非常重要的情况下:Unity编辑器本身。 如果您对使用自定义工具和功能扩展Unity编辑器感兴趣,则很有可能需要做的一件事情就是与IMGUI保持一致。

立即进行 (Proceeding Immediately)

First question, then: Why is it called ‘IMGUI’? IMGUI is short for Immediate Mode GUI. OK, so, what’s that? Well, there’s two major approaches to GUI systems: ‘immediate’ and ‘retained.’

那么,第一个问题是:为什么将其称为“ IMGUI”? IMGUI是立即模式GUI的缩写。 好,那是什么? 嗯,GUI系统有两种主要方法:“立即”和“保留”。

A retained mode GUI is one in which the GUI system ‘retains’ information about your GUI: you set up your various GUI widgets – labels, buttons, sliders, text fields, etc – and then that information is kept around and used by the system to render the screen, respond to events, and so on. When you want to change the text on a label, or move a button, then you’re manipulating some information which is stored somewhere, and when you’ve made your change then the system carries on working in its new state. As the user changes values and moves sliders, the system simply stores their changes, and it’s up to you to query the values or respond to callbacks. The new Unity UI system is an example of a retained mode GUI; you create your UI.Labels, UI.Buttons and so on as components, set up them up, and then just let them sit there, and the new UI system will take care of the rest.

保留模式GUI是一种GUI系统,其中GUI系统“保留”有关您的GUI的信息:您设置各种GUI小部件(标签,按钮,滑块,文本字段等),然后该信息将保留并由系统使用渲染屏幕,响应事件等。 当您想更改标签上的文本或移动按钮时,您将需要处理一些存储在某处的信息,当您进行更改后,系统将以新状态继续工作。 当用户更改值并移动滑块时,系统仅存储其更改,这取决于您查询值或响应回调。 新的Unity UI系统是保留模式GUI的示例; 您可以将UI.Labels,UI.Buttons等创建为组件,对其进行设置,然后将它们放在那里,新的UI系统将负责其余的工作。

Meanwhile, an immediate mode GUI is one in which the GUI system generally does not retain information about your GUI, but instead, repeatedly asks you to re-specify what your controls are, and where they are, and so on. As you specify each part of the UI in the form of function calls, it is processed immediately – drawn, clicked, etc – and the consequences of any user interaction returned to you straight away, instead of you needing to query for it. This is inefficient for a game UI – and inconvenient for artists to work with, as everything becomes very code-dependent – but it turns out to be very handy for non-realtime situations (like Editor panels) which are heavily code-driven (like Editor panels) and want to change the displayed controls easily in response to current state (like Editor panels!) so it’s a good choice for things like heavy construction equipment. No, wait. I meant, it’s a good choice for Editor panels.

同时, 即时模式GUI是一个在GUI系统一般不会保留你的GUI的信息,而是反复要求您重新指定您的控件是什么,他们在哪里,等等。 当您以函数调用的形式指定UI的每个部分时,会立即对其进行处理(绘制,单击等),并且任何用户交互的后果都会立即返回给您,而无需您进行查询。 这对于游戏UI而言效率低下-由于一切都变得非常依赖于代码,因此对于艺术家来说不方便使用-但事实证明,这在很大程度上受代码驱动的非实时情况(如“编辑器”面板)非常方便编辑器面板),并希望根据当前状态轻松更改显示的控件(例如编辑器面板!),因此对于重型建筑设备等而言,它是一个不错的选择。 不,等等 我的意思是,这是“编辑器”面板的不错选择。

If you want to know more, Casey Muratori has a great video where he discusses some of the upsides and principles of an Immediate Mode GUI. Or you can just keep reading!

如果您想了解更多,Casey Muratori会提供一个精彩的视频 ,他在其中讨论即时模式GUI的一些优点和原理。 或者,您可以继续阅读!

每一个事件 (Every Event-uality)

Whenever IMGUI code is running, there is a current ‘Event’ being handled – this could be something like ‘user has clicked the mouse button,’ or something like ‘the GUI needs to be repainted.’ You can find out what the current event is by checking Event.current.type.

每当运行IMGUI代码时,都会处理当前的“事件” ,这可能是“用户单击鼠标按钮”或“需要重新绘制GUI”之类的内容。 您可以通过检查Event.current.type来了解当前事件是什么。

Imagine what it might look like if you’re doing a set of buttons in a window somewhere and you had to write separate code to respond to ‘user has clicked the mouse button’ and ‘the GUI needs to be repainted.’ At a block level it might look like this:

想象一下,如果您正在某个窗口的某个窗口中执行一组按钮,并且必须编写单独的代码来响应“用户单击了鼠标按钮”和“需要重新绘制GUI”,那会是什么样子。 在块级别,它可能看起来像这样:

Writing these functions for each separate GUI event is kinda tedious; but you’ll notice that there’s a certain structural similarity between the functions. Each step of the way, we are doing something relating to the same control (button 1, button 2, or button 3). Exactly what we’re doing depends on the event, but the structure is the same. What this means is that we can do this instead:

为每个单独的GUI事件编写这些功能有点繁琐。 但是您会注意到这些函数之间存在一定的结构相似性。 在此过程的每个步骤中,我们都在做与同一控件(按钮1,按钮2或按钮3)有关的事情 。 确切地说,我们在做什么取决于事件,但是结构是相同的。 这意味着我们可以改为:

We have a single OnGUI function which calls library functions like GUI.Button, and those library functions do different things depending on which event we’re handling. Simple!

我们只有一个OnGUI函数,该函数调用诸如GUI.Button之类的库函数,并且这些库函数根据我们正在处理的事件来做不同的事情。 简单!

There are 5 event types that are used most of the time:

大多数时间使用5种事件类型 :

EventType.MouseDown Set when the user has just pressed a mouse button.
EventType.MouseUp Set when the user has just released a mouse button.
EventType.KeyDown Set when the user has just pressed a key.
EventType.KeyUp Set when the user has just released a key.
EventType.Repaint Set when IMGUI needs to redraw the screen.
EventType.MouseDown 用户刚刚按下鼠标按钮时设置。
EventType.MouseUp 在用户刚刚释放鼠标按钮时设置。
EventType.KeyDown 当用户按下按键时设置。
EventType.KeyUp 在用户刚刚释放键时设置。
EventType.Repaint 在IMGUI需要重绘屏幕时设置。

That’s not an exhaustive list – check the EventType documentation for more.

那不是一个详尽的列表–有关更多信息,请查看EventType文档 。

How might a standard control, such as GUI.Button, respond to some of these events?

标准控件(如GUI.Button )如何响应其中的某些事件?

EventType.Repaint Draw the button in the provided rectangle.
EventType.MouseDown Check whether the mouse is within the button’s rectangle. If so, flag the button as being down and trigger a repaint so that it gets redrawn as pressed in.
EventType.MouseUp Unflag the button as down and trigger a repaint, then check whether the mouse is still within the button’s rectangle: if so, return true, so that the caller can respond to the button being clicked.
EventType.Repaint 在提供的矩形中绘制按钮。
EventType.MouseDown 检查鼠标是否在按钮的矩形内。 如果是这样,请将按钮标记为按下并触发重新绘制,以便在按下时重新绘制它。
EventType.MouseUp 取消将按钮标记为按下并触发重新绘制,然后检查鼠标是否仍在按钮的矩形内:如果是,则返回true,以便调用者可以响应被单击的按钮。

The reality is more complicated than this – a button also responds to keyboard events, and there is code to ensure that only the button that you initially clicked on can respond to the MouseUp – but this gives you a general idea. As long as you call GUI.Button at the same point in your code for each of these events, with the same position and contents, then the different behaviours will work together to provide all the functionality of a button.

实际情况要比这复杂得多-按钮还可以响应键盘事件,并且有代码可以确保只有您最初单击的按钮才能响应MouseUp但这给了您一个大概的想法。 只要您针对这些事件中的每个事件在代码的同一点调用GUI.Button ,并且它们具有相同的位置和内容,那么不同的行为将协同工作以提供按钮的所有功能。

To help with tying these different behaviours together under different events, IMGUI has the concept of a ‘control ID.’ The idea of a control ID is to give a consistent way to refer to a given control across every event type. Each distinct part of the UI that has non-trivial interactive behaviour will request a control ID; it’s used to keep track of things like which control currently has keyboard focus, or to store a small amount of information associated with a control. The control IDs are simply awarded to controls in the order that they ask for them, so, again, as long as you’re calling the same GUI functions in the same order under different events, they’ll end up being awarded the same control IDs and the different events will sync up.

为了帮助在不同事件下将这些不同行为联系在一起,IMGUI具有“控件ID”的概念。 控件ID的想法是提供一种一致的方式来引用每种事件类型的给定控件。 UI中具有非平凡的交​​互行为的每个不同部分都将请求控件ID。 它用于跟踪当前哪个控件具有键盘焦点的事物,或存储与控件相关的少量信息。 控件ID只是按其要求的顺序授予控件,因此,再一次,只要您在不同事件下以相同顺序调用相同的GUI函数,它们最终将被授予相同的控件ID和其他事件将同步。

定制控制难题 (Custom Control Conundrum)

If you want to create your own custom Editor classes, your own EditorWindow classes, or your own PropertyDrawer classes, the GUI class – as well as the EditorGUI class – provides a library of useful standard controls that you’ll see used throughout Unity.

如果要创建自己的自定义Editor类, EditorWindow类或PropertyDrawer类,则GUI类以及EditorGUI类均提供了有用的标准控件库,您将在整个Unity中使用它们。

(It’s a common mistake for newbie Editor coders to overlook the GUI class – but the controls in that class can be used when extending the Editor just as freely as the controls in EditorGUI. There’s nothing particularly special about GUI vs EditorGUI – they’re just two libraries of controls for you to use – but the difference is that the controls in EditorGUI cannot be used in game builds, because the code for them is part of the Editor, while GUI is a part of the engine itself).

(这是一个常见的错误为新手编辑程序员忽略了GUI类-但在类的控件可以用来一样自由伸展的编辑器作为控件时EditorGUI没有什么特别之处。 GUI VS EditorGUI -他们只是两个控件库供您使用–但不同之处在于, EditorGUI中的控件无法在游戏版本中使用,因为它们的代码是Editor的一部分,而GUI是引擎本身的一部分)。

But what if you want to do something that goes beyond what’s available in the standard library?

但是,如果您想做一些超出标准库可用范围的事情该怎么办?

Let’s explore how we might create a custom user interface control. Try clicking and dragging the coloured boxes in this little demo:

让我们探讨如何创建自定义用户界面控件。 尝试单击并拖动此小演示中的彩色框:

(You’ll need a browser with WebGL support to see the demo, like current versions of Firefox).

(您需要使用支持WebGL的浏览器来观看演示,例如当前版本的Firefox)。

These custom sliders each drive a separate ‘float’ value between 0 and 1. You might want to use such a thing in the Inspector as another way of displaying, say, hull integrity for different parts of a spaceship object, where 1 represents ‘no damage’ and 0 represents ‘totally destroyed’ – having the bars represent the values as colours may make it easier to tell, at a glance, what state the ship is in. The code for building this as a custom IMGUI control that you can use like any other control is pretty easy, so let’s walk through it.

这些自定义滑块每个都驱动一个介于0和1之间的单独的“ float”值。您可能想在Inspector中使用这种方式,作为另一种显示飞船对象不同部分的船体完整性的方式,其中1表示“否”。损坏”,0表示“完全破坏”-用颜色表示颜色的条形图可以使您一眼便知道船处于什么状态。将其构建为可自定义IMGUI控件的代码就像其他任何控件一样,它非常简单,因此让我们逐步了解一下。

The first step is to decide upon our function signature. In order to cover all the different event types, our control is going to need three things:

第一步是确定我们的功能签名。 为了涵盖所有不同的事件类型,我们的控件将需要三件事:

  • a Rect which defines where it should draw itself and where it should respond to mouse clicks.

    一个Rect ,它定义应该在哪里绘制以及应该对鼠标单击做出响应的位置。

  • the current float value that the bar is representing.

    条形图表示的当前float值。

  • a GUIStyle, which contains any necessary information about spacing, fonts, textures, and so on that the control will need. In our case that includes the texture that we’ll use to draw the bar. More on this parameter later.

    GUIStyle ,其中包含GUIStyle需要的有关间距,字体,纹理等的任何必要信息。 在我们的示例中,包括用于绘制条的纹理。 稍后将详细介绍此参数。

It’s also going to need to return the value that the user has set by dragging the bar. That’s only meaningful on certain events like mouse events, and not on things like repaint events; so by default we’ll return the value that the calling code passed in. The idea is that the calling code can just do “value = MyCustomSlider(... value ...)” without caring about the event that is happening, so if we’re not returning some new value set by the user, we need to preserve the value that currently stands.

还需要通过拖动条来返回用户设置的值。 这仅对某些事件(如鼠标事件)有意义,而对诸如重画事件等事件则无意义。 因此默认情况下,我们将返回调用代码传入的值。这个想法是调用代码可以执行“ value = MyCustomSlider(... value ...) ”而无需关心正在发生的事件,因此如果我们不返回用户设置的一些新值,则需要保留当前保留的值。

So the resulting signature looks like this:

因此,生成的签名如下所示:

public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style) public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)

1

public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)

1

public static float MyCustomSlider ( Rect controlRect , float value , GUIStyle style )

Now we begin implementing the function. The first step is to retrieve a control ID. We’ll use this for certain things when responding to the mouse events. However, even if the event being handled isn’t one we actually care about, we must still request an ID anyway, to ensure that it isn’t allocated to some other control for this particular event. Remember that IMGUI just dishes out IDs in the order they’re requested, so if you don’t ask for an ID it’ll end up being given to the next control instead, causing that control to end up with different IDs for different events, which is likely to break it. So, when requesting IDs, it’s all-or-none – either you request an ID for every event type, or you never request it for any of them (which might be OK, if you’re creating a control that is extremely simple or non-interactive).

现在我们开始实现该功能。 第一步是获取控件ID。 在响应鼠标事件时,将在某些事情上使用它。 但是,即使处理的事件不是我们真正关心的事件,我们仍然仍然必须请求一个ID,以确保不会将其分配给该特定事件的其他控件。 请记住,IMGUI只是按照请求的顺序分配ID,因此,如果您不要求提供ID,它将最终被分配给下一个控件,从而导致该控件针对不同的事件以不同的ID结束,这很可能会破坏它。 因此,在请求ID时,它是全有还是无-您要么为每种事件类型请求一个ID,要么从不为任何事件类型请求ID(如果您要创建的控件非常简单,或者非互动)。

{ int controlID = GUIUtility.GetControlID (FocusType.Passive); { int controlID = GUIUtility.GetControlID (FocusType.Passive);

1

2

{
int controlID = GUIUtility.GetControlID (FocusType.Passive);

1

2

{
int controlID = GUIUtility . GetControlID ( FocusType . Passive ) ;

The FocusType.Passive being passed as a parameter there tells IMGUI what role this control plays in keyboard navigation – whether it’s possible for the control to be the current one reacting to keypresses. My custom slider doesn’t respond to key presses at all, so it specifies Passive, but controls that respond to key presses could specify Native or Keyboard. Check the FocusType docs for more info on them.

FocusType.Passive作为参数传递的FocusType.Passive告诉IMGUI该控件在键盘导航中扮演的角色-该控件是否有可能是当前对按键做出React的控件。 我的自定义滑块完全不响应按键,因此它指定了Passive ,但是响应按键的控件可以指定NativeKeyboard 。 查看FocusType文档以获取有关它们的更多信息。

Next, we do what the majority of custom controls will do at some point in their implementation: we branch depending on the event type, using a switch statement. Instead of just using Event.current.type directly, we’ll use Event.current.GetTypeForControl(), passing it our control ID; this filters the event type, to ensure that, for example, keyboard events are not sent to the wrong control in certain situations. It doesn’t filter everything, though, so we will still need to perform some checks of our own as well.

接下来,我们将执行大多数自定义控件在实现过程中的某些操作:我们使用switch语句根据事件类型进行分支。 而不是仅仅使用Event.current.type直接,我们将使用Event.current.GetTypeForControl()通过它我们的控制ID; 这将过滤事件类型,以确保例如在某些情况下不会将键盘事件发送给错误的控件。 但是,它不会过滤所有内容,因此我们仍然需要对自己进行一些检查。

switch (Event.current.GetTypeForControl(controlID)) {​ switch (Event.current.GetTypeForControl(controlID)) {​

1

2

switch (Event.current.GetTypeForControl(controlID))
{

1

2

switch ( Event . current . GetTypeForControl ( controlID ) )
{

Now we can begin implementing the specific behaviours for the different event types. Let’s start with drawing the control:

现在,我们可以开始为不同的事件类型实现特定的行为。 让我们从绘制控件开始:

case EventType.Repaint: { // Work out the width of the bar in pixels by lerping int pixelWidth = (int)Mathf.Lerp (1f, controlRect.width, value); // Build up the rectangle that the bar will cover // by copying the whole control rect, and then setting the width Rect targetRect = new Rect (controlRect){ width = pixelWidth }; // Tint whatever we draw to be red/green depending on value GUI.color = Color.Lerp (Color.red, Color.green, value); // Draw the texture from the GUIStyle, applying the tint GUI.DrawTexture (targetRect, style.normal.background); // Reset the tint back to white, i.e. untinted GUI.color = Color.white; break; } case EventType.Repaint: { // Work out the width of the bar in pixels by lerping int pixelWidth = (int)Mathf.Lerp (1f, controlRect.width, value); // Build up the rectangle that the bar will cover // by copying the whole control rect, and then setting the width Rect targetRect = new Rect (controlRect){ width = pixelWidth }; // Tint whatever we draw to be red/green depending on value GUI.color = Color.Lerp (Color.red, Color.green, value); // Draw the texture from the GUIStyle, applying the tint GUI.DrawTexture (targetRect, style.normal.background); // Reset the tint back to white, i.e. untinted GUI.color = Color.white; break; }

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

case EventType.Repaint:
{
// Work out the width of the bar in pixels by lerping
int pixelWidth = (int)Mathf.Lerp (1f, controlRect.width, value);
// Build up the rectangle that the bar will cover
// by copying the whole control rect, and then setting the width
Rect targetRect = new Rect (controlRect){ width = pixelWidth };
// Tint whatever we draw to be red/green depending on value
GUI.color = Color.Lerp (Color.red, Color.green, value);
// Draw the texture from the GUIStyle, applying the tint
GUI.DrawTexture (targetRect, style.normal.background);
// Reset the tint back to white, i.e. untinted
GUI.color = Color.white;
break;
}

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

case EventType . Repaint :
{
// Work out the width of the bar in pixels by lerping
int pixelWidth = ( int ) Mathf . Lerp ( 1f , controlRect . width , value ) ;
// Build up the rectangle that the bar will cover
// by copying the whole control rect, and then setting the width
Rect targetRect = new Rect ( controlRect ) { width = pixelWidth } ;
// Tint whatever we draw to be red/green depending on value
GUI . color = Color . Lerp ( Color . red , Color . green , value ) ;
// Draw the texture from the GUIStyle, applying the tint
GUI . DrawTexture ( targetRect , style . normal . background ) ;
// Reset the tint back to white, i.e. untinted
GUI . color = Color . white ;
break ;
}

At this point you could finish up the function and you’d have a functioning ‘read-only’ control for visualising float values between 0 and 1. But let’s continue and make the control interactive.

至此,您可以完成该功能,并且将具有一个功能正常的“只读”控件,以可视化0到1之间的浮点值。但是让我们继续并使该控件具有交互性。

To implement a pleasant mouse behaviour for the control, we have a requirement: once you’ve clicked on the control and started to drag it, you shouldn’t need to keep the mouse over the control. It’s much nicer for the user to be able to just focus on where their cursor is horizontally, and not worry about vertical movement. This means that they might move the mouse over other controls while dragging, and we need those controls to ignore the mouse until the user releases the button again.

为了使控件实现令人愉悦的鼠标行为,我们有一个要求:单击控件并开始拖动它后,就无需将鼠标放在控件上。 对于用户来说,只需要关注光标在水平方向上的位置,而不必担心垂直移动,会更好。 这意味着在拖动时它们可能会将鼠标移到其他控件上,并且我们需要那些控件忽略鼠标,直到用户再次释放按钮为止。

The solution to this is to make use of GUIUtility.hotControl. It’s just a simple variable which is intended to hold the control ID of the control which has captured the mouse. IMGUI uses this value in GetTypeForControl(); when it’s not 0, then mouse events get filtered out unless the control ID being passed in is the hotControl.

解决方案是利用GUIUtility.hotControl 。 这只是一个简单的变量,用于保存已捕获鼠标的控件的控件ID。 IMGUI在GetTypeForControl GetTypeForControl();使用此值GetTypeForControl(); 当它不为0时,除非传入的控件ID为hotControl ,否则鼠标事件将被滤除。

So, setting and resetting hotControl is pretty simple:

因此,设置和重置hotControl非常简单:

case EventType.MouseDown: { // If the click is actually on us... if (controlRect.Contains (Event.current.mousePosition) // ...and the click is with the left mouse button (button 0)... && Event.current.button == 0) // ...then capture the mouse by setting the hotControl. GUIUtility.hotControl = controlID; break; } case EventType.MouseUp: { // If we were the hotControl, we aren't any more. if (GUIUtility.hotControl == controlID) GUIUtility.hotControl = 0; break; } case EventType.MouseDown: { // If the click is actually on us... if (controlRect.Contains (Event.current.mousePosition) // ...and the click is with the left mouse button (button 0)... && Event.current.button == 0) // ...then capture the mouse by setting the hotControl. GUIUtility.hotControl = controlID; break; } case EventType.MouseUp: { // If we were the hotControl, we aren't any more. if (GUIUtility.hotControl == controlID) GUIUtility.hotControl = 0; break; }

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

case EventType.MouseDown:
{
// If the click is actually on us...
if (controlRect.Contains (Event.current.mousePosition)
// ...and the click is with the left mouse button (button 0)...
&& Event.current.button == 0)
// ...then capture the mouse by setting the hotControl.
GUIUtility.hotControl = controlID;
break;
}
case EventType.MouseUp:
{
// If we were the hotControl, we aren't any more.
if (GUIUtility.hotControl == controlID)
GUIUtility.hotControl = 0;
break;
}

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

case EventType . MouseDown :
{
// If the click is actually on us...
if ( controlRect . Contains ( Event . current . mousePosition )
// ...and the click is with the left mouse button (button 0)...
& amp ; & amp ; Event . current . button == 0 )
// ...then capture the mouse by setting the hotControl.
GUIUtility . hotControl = controlID ;
break ;
}
case EventType . MouseUp :
{
// If we were the hotControl, we aren't any more.
if ( GUIUtility . hotControl == controlID )
GUIUtility . hotControl = 0 ;
break ;
}

Note that when some other control is the hot control – i.e. GUIUtility.hotControl is something other than 0 and our own control ID – then these cases simply won’t be executed, because GetTypeForControl() will be returning ‘ignore’ instead of mouseUp/mouseDown events.

请注意,当其他控件是热控件时(即GUIUtility.hotControl不是0以及我们自己的控件ID),则将不会执行这些情况,因为GetTypeForControl()将返回“ ignore”而不是mouseUp / mouseDown事件。

Setting the hotControl is fine, but we still haven’t actually done anything to change the value while the mouse is down. The simplest way to do that is actually to close the switch and then say that any mouse event (clicking, dragging, or releasing) that happens while we’re the hotControl (and therefore are in the middle of click+dragging – though not releasing, because we zeroed out the hotControl in that case above) should result in the value changing:

设置hotControl很好,但是在鼠标按下时我们实际上还没有做任何更改值的操作。 最简单的方法是先关闭开关,然后说是在我们成为hotControl时发生的任何鼠标事件(单击,拖动或释放)(因此处于单击+拖动的中间–尽管没有释放) ,因为在上述情况下我们将hotControl归零了),结果应该会导致值更改:

if (Event.current.isMouse && GUIUtility.hotControl == controlID) { // Get mouse X position relative to left edge of the control float relativeX = Event.current.mousePosition.x - controlRect.x; // Divide by control width to get a value between 0 and 1 value = Mathf.Clamp01 (relativeX / controlRect.width); // Report that the data in the GUI has changed GUI.changed = true; // Mark event as 'used' so other controls don't respond to it, and to // trigger an automatic repaint. Event.current.Use (); } if (Event.current.isMouse && GUIUtility.hotControl == controlID) { // Get mouse X position relative to left edge of the control float relativeX = Event.current.mousePosition.x - controlRect.x; // Divide by control width to get a value between 0 and 1 value = Mathf.Clamp01 (relativeX / controlRect.width); // Report that the data in the GUI has changed GUI.changed = true; // Mark event as 'used' so other controls don't respond to it, and to // trigger an automatic repaint. Event.current.Use (); }

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15

if (Event.current.isMouse && GUIUtility.hotControl == controlID) {
// Get mouse X position relative to left edge of the control
float relativeX = Event.current.mousePosition.x - controlRect.x;
// Divide by control width to get a value between 0 and 1
value = Mathf.Clamp01 (relativeX / controlRect.width);
// Report that the data in the GUI has changed
GUI.changed = true;
// Mark event as 'used' so other controls don't respond to it, and to
// trigger an automatic repaint.
Event.current.Use ();
}

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15

if ( Event . current . isMouse & amp ; & amp ; GUIUtility . hotControl == controlID ) {
// Get mouse X position relative to left edge of the control
float relativeX = Event . current . mousePosition . x - controlRect . x ;
// Divide by control width to get a value between 0 and 1
value = Mathf . Clamp01 ( relativeX / controlRect . width ) ;
// Report that the data in the GUI has changed
GUI . changed = true ;
// Mark event as 'used' so other controls don't respond to it, and to
// trigger an automatic repaint.
Event . current . Use ( ) ;
}

Those last two steps – setting GUI.changed and calling Event.current.Use() – are particularly important, not just to making this control behave correctly, but also to make it play nice with other IMGUI controls and features. In particular, setting GUI.changed to true will allow calling code to use the EditorGUI.BeginChangeCheck() and EditorGUI.EndChangeCheck() functions to detect whether the user actually changed your control’s value or not; but you should also avoid ever setting GUI.changed to false, because that might end up hiding the fact that a previous control had its value changed.

最后两个步骤-设置GUI.changed和调用Event.current.Use() -特别重要,不仅是要使此控件正确运行,而且还要使其与其他IMGUI控件和功能一起正常使用。 特别是,将GUI.changed设置为true将允许调用代码使用EditorGUI.BeginChangeCheck()EditorGUI.EndChangeCheck()函数来检测用户是否实际上更改了控件的值。 但是您还应该避免将GUI.changed设置为false,因为这可能最终掩盖了以前的控件的值已更改的事实。

Lastly, we need to return a value from the function. You’ll remember that we said we would return the modified float value – or the original value, if nothing has changed, which most of the time will be the case:

最后,我们需要从函数返回一个值。 您会记得我们说过,我们将返回修改后的float值-或原始值,如果没有任何变化,大多数情况下会是这种情况:

return value; } return value; }

1

2

return value;
}

1

2

return value ;
}

And we’re done. MyCustomSlider is now a simple functioning IMGUI control, ready to be used in custom Editors, PropertyDrawers, editor windows, and so on. There’s still more we can do to beef it up – like support multi-editing – but we’ll discuss that below.

我们完成了。 MyCustomSlider现在是功能简单的IMGUI控件,可以在自定义编辑器,PropertyDrawers,编辑器窗口等中使用。 我们还可以做更多的工作来增强它-例如支持多编辑-但我们将在下面讨论。

力所能及 (More than you can Handle)

There’s one other particularly important non-obvious thing about IMGUI, and that is its relation to the Scene View. You’ll all be familiar with the helper UI elements that are drawn in the scene view when you go to translate, rotate, and scale objects – the orthogonal arrows, rings, and box-capped lines that you can click and drag to manipulate objects. These UI elements are called ‘Handles.’

关于IMGUI还有另外一件特别重要的非显而易见的事情,那就是它与场景视图的关系。 当您平移,旋转和缩放对象时,您都将熟悉场景视图中绘制的助手UI元素-您可以单击并拖动来操作对象的正交箭头,圆环和带框的线。 这些UI元素称为“句柄”。

What’s not obvious is that Handles are powered by IMGUI as well!

不明显的是,句柄也由IMGUI提供支持!

After all, there’s nothing inherent in what we’ve said about IMGUI so far that is specific to 2D or Editors/EditorWindows. The standard controls you find in the GUI and EditorGUI classes are all 2D, certainly, but the basic concepts like EventType and control IDs don’t depend on 2D at all. So while GUI and EditorGUI provide 2D controls aimed at EditorWindows and Editors for components in the Inspector, the Handles class provides 3D controls intended for use in the Scene View. Just as EditorGUI.IntField will draw a control that lets the user edit a single integer, we have functions like:

毕竟,到目前为止,我们对IMGUI所说的内容并没有固有于2D或Editors / EditorWindows。 在GUIEditorGUI类中找到的标准控件当然都是2D的,但是诸如EventType和控件ID之类的基本概念完全不依赖于2D。 因此,尽管GUIEditorGUI为Inspector中的组件提供了针对EditorWindows和Editors的2D控件,但是Handles类却提供了打算在Scene View中使用的3D控件。 正如EditorGUI.IntField将绘制一个允许用户编辑单个整数的控件一样,我们具有以下功能:

Vector3 PositionHandle(Vector3 position, Quaternion rotation); Vector3 PositionHandle(Vector3 position, Quaternion rotation);

1

Vector3 PositionHandle(Vector3 position, Quaternion rotation);

1

Vector3 PositionHandle ( Vector3 position , Quaternion rotation ) ;

that will allow the user to edit a Vector3 value, visually, by providing a set of interactive arrows in the Scene View. And just as before, you can define your own Handle functions to draw custom user interface elements as well; dealing with mouse interaction is a little more complex, as it’s no longer enough to just check whether the mouse is inside a rectangle or not – the HandleUtility class may be of help to you there – but the basic structure and concepts are all the same.

通过在“场景视图”中提供一组交互式箭头,可以使用户直观地编辑Vector3值。 和以前一样,您可以定义自己的Handle函数来绘制自定义用户界面元素。 处理鼠标交互要稍微复杂一点,因为仅检查鼠标是否位于矩形内已经不够了– HandleUtility类在HandleUtility可能对您有所帮助–但是基本结构和概念都相同。

If you provide an OnSceneGUI function in your custom editor class, you can use Handle functions there to draw into the scene view, and they’ll be positioned correctly in world space as you’d expect. Though bear in mind that it is possible to use Handles in 2D contexts like custom editors, or to use GUI functions in the scene view – you just may need to do things like setting up GL matrices or calling Handles.BeginGUI() and Handles.EndGUI() to set up the context before you use them.

如果在自定义编辑器类中提供OnSceneGUI函数,则可以在其中使用Handle函数以绘制到场景视图中,并且它们将按照您的期望正确地放置在世界空间中。 尽管要记住,可以在2D上下文(如自定义编辑器)中使用Handles,也可以在场景视图中使用GUI函数-您可能只需要做一些事情,例如设置GL矩阵或调用Handles.BeginGUI()Handles.EndGUI()在使用上下文之前先进行设置。

吉恩国 (State of the GUInion)

In the case of MyCustomSlider, there were only really two pieces of information we needed to keep track of: the current value of the slider (which was passed in by the user and returned to them) and whether the user was in the process of changing the value (which we effectively used hotControl to keep track of). But what if a control needs to keep hold of more information than that?

MyCustomSlider ,我们实际上只需要跟踪两条信息:滑块的当前值(由用户传递并返回给他们)以及用户是否正在更改过程中值(我们有效地使用hotControl进行跟踪)。 但是,如果控件需要保留更多信息,该怎么办?

IMGUI provides a simple storage system for ‘state objects’ that are associated with a control. You define your own class for storing values, and then ask IMGUI to manage an instance of it, associated with your control’s ID. You’re only allowed one state object per control ID, and you don’t instantiate it yourself – IMGUI does that for you, using the state object’s default constructor. State objects also aren’t serialised when reloading editor code – something that happens every time your code is recompiled – so you should only be using them for short-lived stuff. (Note that this is true even if you mark your state objects as [Serializable] – the serializer simply doesn’t visit this particular corner of the heap).

IMGUI为与控件关联的“状态对象”提供了一个简单的存储系统。 您定义自己的用于存储值的类,然后要求IMGUI管理与控件ID关联的实例。 每个控件ID只允许一个状态对象,并且您自己不能实例化它-IMGUI使用状态对象的默认构造函数为您做到这一点。 重新加载编辑器代码时,状态对象也不会被序列化(每次重新编译代码时都会发生),因此,您只应将它们用于短命的东西。 (请注意,即使您将状态对象标记为[Serializable] ,也是如此-序列化器根本不会访问堆的这个特定角落)。

Here’s an example. Suppose we want a button which returns true whenever it’s pressed down, but also flashes red if you’ve been holding it down for longer than two seconds. We’ll need to keep track of the time at which the button was originally pressed; we’ll do this by storing it in a state object. So, here’s our state object class:

这是一个例子。 假设我们想要一个按钮,只要按下该按钮,该按钮在返回时会返回true ;但是如果您按住该按钮超过两秒钟,它也会闪烁红色。 我们需要跟踪最初按下按钮的时间; 我们将其存储在状态对象中。 因此,这是我们的状态对象类:

public class FlashingButtonInfo { private double mouseDownAt; public void MouseDownNow() { mouseDownAt = EditorApplication.timeSinceStartup; } public bool IsFlashing(int controlID) { if (GUIUtility.hotControl != controlID) return false; double elapsedTime = EditorApplication.timeSinceStartup - mouseDownAt; if (elapsedTime < 2f) return false; return (int)((elapsedTime - 2f) / 0.1f) % 2 == 0; } } public class FlashingButtonInfo { private double mouseDownAt; public void MouseDownNow() { mouseDownAt = EditorApplication.timeSinceStartup; } public bool IsFlashing(int controlID) { if (GUIUtility.hotControl != controlID) return false; double elapsedTime = EditorApplication.timeSinceStartup - mouseDownAt; if (elapsedTime < 2f) return false; return (int)((elapsedTime - 2f) / 0.1f) % 2 == 0; } }

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class FlashingButtonInfo
{
private double mouseDownAt;
public void MouseDownNow()
{
mouseDownAt = EditorApplication.timeSinceStartup;
}
public bool IsFlashing(int controlID)
{
if (GUIUtility.hotControl != controlID)
return false;
double elapsedTime = EditorApplication.timeSinceStartup - mouseDownAt;
if (elapsedTime < 2f)
return false;
return (int)((elapsedTime - 2f) / 0.1f) % 2 == 0;
}
}

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class FlashingButtonInfo
{
private double mouseDownAt ;
public void MouseDownNow ( )
{
mouseDownAt = EditorApplication . timeSinceStartup ;
}
public bool IsFlashing ( int controlID )
{
if ( GUIUtility . hotControl != controlID )
return false ;
double elapsedTime = EditorApplication . timeSinceStartup - mouseDownAt ;
if ( elapsedTime & lt ; 2f )
return false ;
return ( int ) ( ( elapsedTime - 2f ) / 0.1f ) % 2 == 0 ;
}
}

We’ll store the time at which the mouse was pressed in ‘mouseDownAt’ when MouseDownNow() is called, and then use the IsFlashing function to tell us ‘should the button be colored red right now’ – as you can see, it will definitely not be red if it’s not the hotControl or if fewer than 2 seconds have passed since it was clicked, but after that we make it change color every 0.1 seconds.

当调用MouseDownNow()时,我们将在“ mouseDownAt”中存储按下鼠标的时间,然后使用IsFlashing函数告诉我们“按钮现在应该现在变成红色” –如您所见,它将如果不是hotControl或自单击以来不到2秒,则绝对不是红色,但是此后,我们将其每0.1秒更改一次颜色。

Here’s the code for the actual button control itself:

以下是实际按钮控件本身的代码:

public static bool FlashingButton(Rect rc, GUIContent content, GUIStyle style) { int controlID = GUIUtility.GetControlID (FocusType.Native); // Get (or create) the state object var state = (FlashingButtonInfo)GUIUtility.GetStateObject( typeof(FlashingButtonInfo), controlID); switch (Event.current.GetTypeForControl(controlID)) { case EventType.Repaint: { GUI.color = state.IsFlashing (controlID) ? Color.red : Color.white; style.Draw (rc, content, controlID); break; } case EventType.MouseDown: { if (rc.Contains (Event.current.mousePosition) && Event.current.button == 0 && GUIUtility.hotControl == 0) { GUIUtility.hotControl = controlID; state.MouseDownNow(); } break; } case EventType.MouseUp: { if (GUIUtility.hotControl == controlID) GUIUtility.hotControl = 0; break; } } return GUIUtility.hotControl == controlID; } public static bool FlashingButton(Rect rc, GUIContent content, GUIStyle style) { int controlID = GUIUtility.GetControlID (FocusType.Native); // Get (or create) the state object var state = (FlashingButtonInfo)GUIUtility.GetStateObject( typeof(FlashingButtonInfo), controlID); switch (Event.current.GetTypeForControl(controlID)) { case EventType.Repaint: { GUI.color = state.IsFlashing (controlID) ? Color.red : Color.white; style.Draw (rc, content, controlID); break; } case EventType.MouseDown: { if (rc.Contains (Event.current.mousePosition) && Event.current.button == 0 && GUIUtility.hotControl == 0) { GUIUtility.hotControl = controlID; state.MouseDownNow(); } break; } case EventType.MouseUp: { if (GUIUtility.hotControl == controlID) GUIUtility.hotControl = 0; break; } } return GUIUtility.hotControl == controlID; }

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

public static bool FlashingButton(Rect rc, GUIContent content, GUIStyle style)
{
int controlID = GUIUtility.GetControlID (FocusType.Native);
// Get (or create) the state object
var state = (FlashingButtonInfo)GUIUtility.GetStateObject(
typeof(FlashingButtonInfo),
controlID);
switch (Event.current.GetTypeForControl(controlID)) {
case EventType.Repaint:
{
GUI.color = state.IsFlashing (controlID)
? Color.red
: Color.white;
style.Draw (rc, content, controlID);
break;
}
case EventType.MouseDown:
{
if (rc.Contains (Event.current.mousePosition)
&& Event.current.button == 0
&& GUIUtility.hotControl == 0)
{
GUIUtility.hotControl = controlID;
state.MouseDownNow();
}
break;
}
case EventType.MouseUp:
{
if (GUIUtility.hotControl == controlID)
GUIUtility.hotControl = 0;
break;
}
}
return GUIUtility.hotControl == controlID;
}

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

public static bool FlashingButton ( Rect rc , GUIContent content , GUIStyle style )
{
int controlID = GUIUtility . GetControlID ( FocusType . Native ) ;
// Get (or create) the state object
var state = ( FlashingButtonInfo ) GUIUtility . GetStateObject (
typeof ( FlashingButtonInfo ) ,
controlID ) ;
switch ( Event . current . GetTypeForControl ( controlID ) ) {
case EventType . Repaint :
{
GUI . color = state . IsFlashing ( controlID )
? Color . red
: Color . white ;
style . Draw ( rc , content , controlID ) ;
break ;
}
case EventType . MouseDown :
{
if ( rc . Contains ( Event . current . mousePosition )
& amp ; & amp ; Event . current . button == 0
& amp ; & amp ; GUIUtility . hotControl == 0 )
{
GUIUtility . hotControl = controlID ;
state . MouseDownNow ( ) ;
}
break ;
}
case EventType . MouseUp :
{
if ( GUIUtility . hotControl == controlID )
GUIUtility . hotControl = 0 ;
break ;
}
}
return GUIUtility . hotControl == controlID ;
}

Pretty straightforward – you should recognise the code in the mouseDown/mouseUp cases as being very similar to what we did for capturing the mouse in the custom slider, above. The only differences are the call to state.MouseDownNow() when pressing down the mouse, and changing GUI.color in the repaint event.

非常简单–您应该将mouseDown / mouseUp案例中的代码与我们在上面的自定义滑块中捕获鼠标的行为非常相似。 唯一的区别是在按下鼠标并在重画事件中更改GUI.color时调用state.MouseDownNow()

The eagle-eyed amongst you might have noticed that there’s one other key difference about the repaint event – that call to style.Draw(). What’s up with that?

在您当中,老鹰般的眼睛可能已经注意到,重绘事件还有另一个关键区别–调用style.Draw() 。 那是怎么回事?

用样式做GUI (Doing GUI with Style)

When we were building the custom slider control, we used GUI.DrawTexture to draw the bar itself. That worked OK, but our FlashingButton needs to have a caption on it, in addition to the ‘rounded rectangle’ image that is the button itself. We could try and arrange something with GUI.DrawTexture to draw the button image and then GUI.Label on top of that to draw the caption… but we can do better. We can use the same technique that GUI.Label uses to draw itself, and cut out the middleman.

在构建自定义滑块控件时,我们使用GUI.DrawTexture绘制了条形图本身。 没问题,但是我们的FlashingButton除了按钮本身的“圆角矩形”图像外,还需要在其上加上标题。 我们可以尝试和安排的东西GUI.DrawTexture绘制按钮图像,然后GUI.Label在其顶部绘制标题...但我们可以做的更好。 我们可以使用GUI.Label用来绘制自身并切出中间人的相同技术。

A GUIStyle contains information about the visual properties of a GUI element – both basic things like the font or text color it should use, and more subtle layout properties like how much spacing to give it. All of this information is stored in a GUIStyle alongside functions to work out the width and height of some content using the style, and the functions to actually draw the content to the screen.

GUIStyle包含有关GUI元素的视觉属性的信息–包括应使用的基本内容(如字体或文本颜色),以及更细微的布局属性(如为其提供多少间距)。 所有这些信息都与功能一起使用某种样式计算出某些内容的宽度和高度以及将内容实际绘制到屏幕上的功能一起存储在GUIStyle

In fact, GUIStyle doesn’t just take care of one style for a control: it can take care of rendering it in a bunch of situations that a GUI element might find itself in – drawing it differently when it’s being hovered over, when it has keyboard focus, when it’s disabled, and when it’s “active” (for example, when a button is in the middle of being pressed). You can provide the color and background image information for all of these situations, and the GUIStyle will pick the appropriate one at drawing-time based on the control ID.

实际上, GUIStyle不仅照顾控件的一种样式:它还可以处理在GUI元素可能会遇到的许多情况下呈现它的问题–当鼠标悬停在其上时,以不同的方式绘制它键盘焦点,禁用状态和“活动”状态(例如,在按下按钮时)。 您可以提供所有这些情况的颜色和背景图像信息, GUIStyle会在绘制时根据控件ID选择适当的一种。

There’s four main ways to get hold of GUIStyles that you can use to draw your controls:

可以使用GUIStyle的四种主要方式来绘制控件:

  • Construct one in code (new GUIStyle()) and set up the values on it.

    用代码构造一个( new GUIStyle() )并在其上设置值。

  • Use one of the built-in styles from the EditorStyles class. If you want your custom controls to look like the built-in ones – drawing your own toolbars, Inspector-style controls, etc – then this is the place to look.

    使用EditorStyles类中的内置样式EditorStyles 。 如果您希望自定义控件看起来像内置控​​件-绘制自己的工具栏,Inspector样式的控件等-那么这是一个可以查看的地方。

  • If you just want to create a small variation on an existing style – say, a regular button but with right-aligned text – then you can clone the styles in the EditorStyles class (new GUIStyle(existingStyle)) and then just change the properties you want to change.

    如果您只想在现有样式上创建一个小的变体(例如,一个常规按钮但带有右对齐的文本),则可以在EditorStyles类( new GUIStyle(existingStyle) )中克隆样式,然后只需更改属性即可想改变。

  • Retrieve them from a GUISkin.

    GUISkin检索它们。

A GUISkin is essentially a big bundle of GUIStyle objects; importantly, it can be created as an asset in your project and edited freely through the Inspector. If you create one and take a look, you’ll see slots for all the standard control types – boxes, buttons, labels, toggles, and so on – but as a custom control author, direct your attention to the ‘custom styles’ section near the bottom. Here you can set up any number of custom GUIStyle entries, giving each one a unique name, and then later you can retrieve them using GUISkin.GetStyle(“nameOfCustomStyle”). The only missing piece of the puzzle is figuring out how to get hold of your GUISkin object from code in the first place; if you keep your skin in the ‘Editor Default Resources’ folder, you can use EditorGUIUtility.LoadRequired(); alternatively, you could use a method like AssetDatabase.LoadAssetAtPath() to load from elsewhere in the project. (Just don’t put your editor-only assets somewhere that packs them into asset bundles or the Resources folder by mistake!)

GUISkin本质上是GUIStyle对象。 重要的是,它可以作为项目中的资产创建,并可以通过检查器自由编辑。 如果创建一个并进行查看,您会看到所有标准控件类型的插槽-框,按钮,标签,切换键等,但作为自定义控件的作者,请注意“自定义样式”部分在底部附近。 在这里,您可以设置任意数量的自定义GUIStyle条目,为每个条目指定一个唯一的名称,然后可以使用GUISkin.GetStyle(“nameOfCustomStyle”)检索它们。 唯一缺少的难题是首先弄清楚如何从代码中获取GUISkin对象。 如果将皮肤保留在“ Editor Default Resources”文件夹中,则可以使用EditorGUIUtility.LoadRequired() ; 或者,您可以使用类似AssetDatabase.LoadAssetAtPath()的方法从项目中的其他位置加载。 (仅不要将您仅编辑器的资产放到会错误地打包到资产束或“资源”文件夹中的位置!)

Armed with a GUIStyle, you can then draw a GUIContent – a mix of text, icon, and tooltip – using GUIStyle.Draw(), passing it the rectangle you’re drawing into, the GUIContent you want to draw, and the control ID that should be used to figure out whether the content has things like keyboard focus.

与武装GUIStyle ,你可以再画一个GUIContent -文字,图标的组合,并提示-使用GUIStyle.Draw()通过它的矩形你拉进去,在GUIContent要绘制,并且控制ID应该用来确定内容是否具有键盘焦点之类的内容。

布置位置 (Laying Out the Positions)

You’ll have noticed that all of the GUI controls we’ve discussed and written so far include a Rect parameter that determines the control’s position on screen. And, now that we’ve discussed GUIStyle, you might have paused when I said that a GUIStyle includes “layout properties like how much spacing it needs.” You might be thinking: “uh oh. Does this mean we have to do a bunch of work to calculate our Rect values so that the spacing values are respected?”

您会注意到,到目前为止,我们已经讨论和编写的所有GUI控件都包含一个Rect参数,该参数确定控件在屏幕上的位置。 而且,既然我们已经讨论了GUIStyle ,那么当我说GUIStyle包含“布局属性,例如它需要多少间距”时,您可能已经停顿了。 您可能会想:“哦。 这是否意味着我们必须做大量工作来计算Rect值,以便尊重间距值?”

That’s certainly an approach which is available to us; but there’s an easier way. IMGUI includes a ‘layouting’ mechanism which can automatically calculate appropriate Rect values for our controls, taking things like spacing into account. So how does it work?

当然,这是我们可以使用的一种方法。 但是有一种更简单的方法。 IMGUI包含一种“布局”机制,该机制可以自动计算出适合我们控件的Rect值,并考虑到间距等因素。 那么它是怎样工作的?

The trick is an extra EventType value for controls to respond to: EventType.Layout. IMGUI sends the event to your GUI code, and the controls you invoke respond by calling IMGUI layout functions – GUILayoutUtility.GetRect(), GUILayout.BeginHorizonal / Vertical, and GUILayout.EndHorizontal / Vertical, amongst others – which IMGUI records, effectively building up a tree of the controls in your layout and the space they require. Once it’s finished and the tree is fully built, IMGUI then does a recursive pass over the tree, calculating the actual widths and heights of elements and where they are in relation to one another, positioning successive controls next to one another and so on.

诀窍是控件可以响应的额外EventType值: EventType.Layout 。 IMGUI将事件发送到您的GUI代码,并且您调用的控件通过调用IMGUI布局函数– GUILayoutUtility.GetRect()GUILayout.BeginHorizonal / VerticalGUILayout.EndHorizontal / Vertical等(IMGUI进行记录)来有效地建立布局中的控件及其所需空间的树。 一旦完成并完全构建了树,IMGUI就会在树上进行递归遍历,计算元素的实际宽度和高度以及它们之间的相对位置,将连续的控件彼此相邻放置,依此类推。

Then, when it’s time to do an EventType.Repaint event – or indeed any other kind of event – controls call the same IMGUI layout functions. Only this time, instead of recording the calls, IMGUI ‘plays back’ the calls it previously recorded on the Layout event, returning the rectangles it computed; having called GUILayoutUtility.GetRect() during the layout event to register that you need a rectangle, you call it again during the repaint event and it actually returns the rectangle you should use.

然后,当需要执行EventType.Repaint事件或其他任何类型的事件时,控件将调用相同的IMGUI布局函数。 只是这次,IMGUI不再记录呼叫,而是“回放”它先前在Layout事件中记录的呼叫,返回它计算出的矩形。 在布局事件中调用GUILayoutUtility.GetRect()来注册需要矩形的对象,然后在重绘事件中再次调用了它,它实际上返回了您应该使用的矩形。

Like with control IDs, this means you need to be consistent about the layout calls you make between Layout events and other events – otherwise you’ll end up retrieving computed rectangles for the wrong controls. It also means that the values returned by GUILayoutUtility.GetRect() during a Layout event are useless, because IMGUI won’t actually know the rectangle it’s supposed to give you until the event has completed and the layout tree has been processed.

与控件ID一样,这意味着您需要在Layout事件和其他事件之间进行的布局调用保持一致–否则最终将为错误的控件检索计算出的矩形。 这也意味着在Layout事件期间由GUILayoutUtility.GetRect()返回的值是无用的,因为IMGUI在事件完成且布局树已处理之前,实际上并不知道应该给您的矩形。

What does this look like for our custom slider control? We can actually write a Layout-enabled version of our control really easily, as once we’ve got a rectangle back from IMGUI we can just call the code we already wrote:

自定义滑块控件的外观如何? 实际上,我们可以非常轻松地编写控件的启用布局的版本,因为一旦从IMGUI中获得了矩形,我们就可以调用我们已经编写的代码:

public static float MyCustomSlider(float value, GUIStyle style) { Rect position = GUILayoutUtility.GetRect(GUIContent.none, style); return MyCustomSlider(position, value, style); } public static float MyCustomSlider(float value, GUIStyle style) { Rect position = GUILayoutUtility.GetRect(GUIContent.none, style); return MyCustomSlider(position, value, style); }

1

2
3
4
5

public static float MyCustomSlider(float value, GUIStyle style)
{
Rect position = GUILayoutUtility.GetRect(GUIContent.none, style);
return MyCustomSlider(position, value, style);
}

1

2
3
4
5

public static float MyCustomSlider ( float value , GUIStyle style )
{
Rect position = GUILayoutUtility . GetRect ( GUIContent . none , style ) ;
return MyCustomSlider ( position , value , style ) ;
}

The call to GUILayoutUtility.GetRect will do two things: during a Layout event, it will record that we want to use the given style to draw some empty content – empty because there is no specific text or image that we need to make room for – and during other events, it will retrieve an actual rectangle for us to use. This does mean that during a layout event we’re calling MyCustomSlider with a bogus rectangle, but it doesn’t matter – we still need to do it, in order to make sure that the usual calls are made to GetControlID(), and the rectangle isn’t actually used for anything in there during a Layout event.

GUILayoutUtility.GetRect的调用将做两件事:在Layout事件期间,它将记录我们要使用给定的样式绘制一些空内容–空,因为没有特定的文本或图像需要我们留出空间–在其他事件中,它将检索一个实际的矩形供我们使用。 这确实意味着在布局事件期间,我们使用虚假矩形调用MyCustomSlider ,但这没关系–我们仍然需要这样做,以确保通常对GetControlID()进行调用,并且在Layout事件期间,矩形实际上并未用于其中的任何内容。

You might be wondering how IMGUI can actually work out the size of the slider, given ‘empty’ content and just a style. It’s not a lot of information to go on – we’re relying on the style having all the necessary information specified, that IMGUI can use to work out the rectangle to assign. But what if we wanted to let the user control that – or, say, to use a fixed height from the style but let the user control the width. How would we do that?

您可能想知道,在给定“空”内容和样式的情况下,IMGUI如何实际计算出滑块的大小。 信息不多-我们依赖于已指定所有必要信息的样式,IMGUI可以使用该信息来计算要分配的矩形。 但是,如果我们希望让用户控制该样式,或者说使用样式中的固定高度,但让用户控制宽度,该怎么办? 我们将如何做?

The answer is in the GUILayoutOption class. Instances of this class represent directives to the layout system that a particular rectangle should be calculated in a particular way; for example, “should have height 30” or “should expand horizontally to fill the space” or “must be at least 20 pixels wide.” We create them by calling factory functions in the GUILayout class – GUILayout.ExpandWidth(), GUILayout.MinHeight(), and so on – and pass them to GUILayoutUtility.GetRect() as an array. They’re stored into the layout tree and taken into account when the tree is processed at the end of the layout event.

答案在GUILayoutOption类中。 此类的实例表示对布局系统的指示,即应该以特定方式计算特定矩形;例如, 例如,“应具有30的高度”或“应水平扩展以填充空间”或“必须至少20像素宽”。 我们通过在GUILayout类中调用工厂函数( GUILayout.ExpandWidth()GUILayout.MinHeight()GUILayoutUtility.GetRect()它们,并将它们作为数组传递给GUILayoutUtility.GetRect() 。 它们存储在布局树中,并在布局事件结束时处理该树时将它们考虑在内。

To make it easy for the user to provide as few or as many GUILayoutOption instances as they like without having to create and manage their own arrays, we take advantage of the C# ‘params’ keyword, which lets you call a method passing any number of parameters, and have those parameters arrive within the method packed into an array automatically. Here’s our modified slider now:

为了使用户能够轻松地提供GUILayoutOption实例而无需创建和管理自己的数组,我们利用了C#'params'关键字,该关键字使您可以调用传递任意数量的参数,并将那些参数自动填充到数组中。 现在是我们修改后的滑块:

public static float MyCustomSlider(float value, GUIStyle style, params GUILayoutOption[] opts) { Rect position = GUILayoutUtility.GetRect(GUIContent.none, style, opts); return MyCustomSlider(position, value, style); } public static float MyCustomSlider(float value, GUIStyle style, params GUILayoutOption[] opts) { Rect position = GUILayoutUtility.GetRect(GUIContent.none, style, opts); return MyCustomSlider(position, value, style); }

1

2
3
4
5

public static float MyCustomSlider(float value, GUIStyle style, params GUILayoutOption[] opts)
{
Rect position = GUILayoutUtility.GetRect(GUIContent.none, style, opts);
return MyCustomSlider(position, value, style);
}

1

2
3
4
5

public static float MyCustomSlider ( float value , GUIStyle style , params GUILayoutOption [ ] opts )
{
Rect position = GUILayoutUtility . GetRect ( GUIContent . none , style , opts ) ;
return MyCustomSlider ( position , value , style ) ;
}

As you can see, we just take whatever the user’s given us and pass it onwards to GetRect.

如您所见,我们只是接受用户给我们的任何东西,然后将其传递给GetRect

The approach we’ve used here – of wrapping a manually-positioned IMGUI control function in an auto-layouting version – works for pretty much any IMGUI control, including the built-in ones in the GUI class. In fact, the GUILayout class uses exactly this approach to provide auto-layouted versions of the controls in the GUI class (and we offer a corresponding EditorGUILayout class to wrap controls in the EditorGUI class). You might want to follow this twin-class convention when building your own IMGUI controls.

我们在这里使用的方法-将手动定位的IMGUI控件功能包装在自动布局版本中-几乎适用于任何IMGUI控件,包括GUI类中的内置控件。 实际上, GUILayout类正是使用这种方法在GUI类中提供了控件的自动布局版本(并且我们提供了相应的EditorGUILayout类来将控件包装在EditorGUI类中)。 在构建自己的IMGUI控件时,您可能要遵循这种双类约定。

It’s also completely viable to mix auto-layouted and manually positioned controls. You can call GetRect to reserve a chunk of space, and then do you own calculations to divide that rectangle up into sub-rectangles that you then use to draw multiple controls; the layout system doesn’t use control IDs in any way, so there’s no problem with having multiple controls per layout rectangle ( or even multiple layout rectangles per control). This can sometimes be much faster than using the layout system fully.

混合自动布局的控件和手动放置的控件也是完全可行的。 您可以调用GetRect保留一大块空间,然后进行计算以将该矩形划分为多个子矩形,然后使用它们绘制多个控件。 布局系统不会以任何方式使用控件ID,因此每个布局矩形具有多个控件(甚至每个控件甚至具有多个布局矩形)也没有问题。 有时这可能比完全使用布局系统要快得多。

Also, note that if you’re writing PropertyDrawers, you should not use the layout system; instead, you should just use the rectangle passed to your PropertyDrawer.OnGUI() override. The reason for this is that under the hood, the Editor class itself does not actually use the layout system, for performance reasons; it just calculates a simple rectangle itself, moving it down for each successive property. So, if you did use the layout system in your PropertyDrawer, it wouldn’t have any knowledge of any of the properties that had been drawn before yours, and would end up positioning you on top of them. Which is not what you want!

另外,请注意,如果您正在编写PropertyDrawers ,则不应使用布局系统。 相反,您应该只使用传递给PropertyDrawer.OnGUI()覆盖的矩形。 原因是在后台,出于性能方面的考虑, Editor类本身实际上并未使用布局系统。 它只是自己计算一个简单的矩形,然后为每个连续的属性向下移动矩形。 因此,如果您确实在PropertyDrawer使用了布局系统,那么它将不了解在您之前绘制的任何属性,最终将您置于这些属性之上。 这不是您想要的!

Leeloo Dallas多功能物业 (Leeloo Dallas Multi-Property)

So far, everything we’ve discussed would equip you to build your own IMGUI control that would work pretty smoothly. There’s just a couple more things to discuss for when you really want to polish what you’ve built to the same level as the Unity built-in controls.

到目前为止,我们所讨论的所有内容都将使您能够构建自己的IMGUI控件,该控件将非常顺利地运行。 当您真正想将自己构建的内容与Unity内置控件的水平进行抛光时,还有几件事需要讨论。

The first is the use of SerializedProperty. I don’t want to go into the SerializedProperty system in too much detail in this post – we’ll leave that for another time – but just to summarize quickly: A SerializedProperty ‘wraps’ a single variable handled by Unity’s serialization (load and save) system. Every variable on every script you write that shows up in the Inspector – as well as every variable on every engine object that you see in the Inspector – can be accessed via the SerializedProperty API, at least in the Editor.

首先是使用SerializedProperty 。 我不想在本文中过多地介绍SerializedProperty系统-我们将再留一遍-只是为了快速总结一下: SerializedProperty “包装”由Unity的序列化处理的单个变量(加载并保存)系统。 可以通过SerializedProperty API(至少在编辑器中)访问在Inspector中显示的每个编写脚本上的每个变量,以及在Inspector中看到的每个引擎对象上的每个变量。

SerializedProperty is useful because it doesn’t just give you access to the variable’s value, but also information like whether the variable’s value is different to the value on a prefab it came from, or whether a variable with child fields (e.g. a struct) is expanded or collapsed in the Inspector. It also integrates any changes you make to the value into the Undo and scene-dirtying systems. It lets you do this without ever actually creating the managed version of your object, too, which can help performance greatly. So, if we want our IMGUI controls to play nice and easy with a slew of editor functionality – undo, scene dirtying, prefab overrides, etc – we should make sure we support SerializedProperty.

SerializedProperty很有用,因为它不仅使您能够访问变量的值,而且还提供诸如变量的值是否与它来自的预制变量上的值是否不同或带有子字段(例如struct)的变量是否可用等信息。在检查器中展开或折叠。 它还会将您对值所做的任何更改集成到“撤消”和“场景弄脏”系统中。 它使您无需真正创建对象的托管版本即可执行此操作,这可以极大地提高性能。 因此,如果我们想让我们的IMGUI控件通过一系列编辑器功能(撤消,场景脏污,预制覆盖等)轻松,轻松地播放,我们应该确保我们支持SerializedProperty

If you look through the EditorGUI methods that take a SerializedProperty as an argument, you’ll see the signature is slightly different. Instead of the ‘take a float, return a float’ approach of our previous custom slider, SerializedProperty-enabled IMGUI controls just take a SerializedProperty instance as an argument, and don’t return anything. That’s because any changes they need to make to the value, they just apply directly to the SerializedProperty themselves. So our custom slider from before can now look like this:

如果查看带有SerializedProperty作为参数的EditorGUI方法,您会发现签名略有不同。 取而代之的是“采取浮动,返回浮动”我们以前的自定义滑块的方法, SerializedProperty -启用IMGUI控件只取一SerializedProperty实例作为参数,并且不返回任何东西。 那是因为他们需要对值进行任何更改,它们直接直接应用于SerializedProperty 。 因此,我们之前的自定义滑块现在可以如下所示:

public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style) public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style)

1

public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style)

1

public static void MyCustomSlider ( Rect controlRect , SerializedProperty prop , GUIStyle style )

The ‘value’ parameter we used to have is gone, along with the return value, and instead, the ‘prop’ parameter is there to pass in the SerializedProperty. To retrieve the current value of the property in order to draw the slider bar, we just access prop.floatValue, and when the user changes the slider position we just assign to prop.floatValue.

我们曾经拥有的'value'参数和返回值一起消失了,相反,'prop'参数在那里传递了SerializedProperty 。 要检索属性的当前值以绘制滑块,我们只需要访问prop.floatValue ,而当用户更改滑块位置时,我们只是将其分配给prop.floatValue

Having the whole SerializedProperty present in the IMGUI control code has other benefits, though. For example, consider the way that modified properties in prefab instances are shown in bold. Just check the prefabOverride property on the SerializedProperty, and if it’s true, do whatever you need to do to display the control differently. Happily, if making text bold really is all you want to do, then IMGUI will take care of that for you automatically as long as you don’t specify a font in your GUIStyle when you draw. (If you do specify a font in your GUIStyle, then you’re going to need to take care of this yourself – having regular and bold versions of your font and selecting between them based on prefabOverride when you want to draw).

但是,将整个SerializedProperty包含在IMGUI控制代码中还有其他好处。 例如,考虑预制实例中修改后的属性以粗体显示的方式。 只需检查SerializedProperty上的prefabOverride属性,如果为true,请执行所需的操作以不同方式显示控件。 幸运的是,如果只想使文本变为粗体,那么只要您在GUIStyle时未在GUIStyle指定字体, GUIStyle就会自动为您GUIStyle 。 (If you do specify a font in your GUIStyle , then you're going to need to take care of this yourself – having regular and bold versions of your font and selecting between them based on prefabOverride when you want to draw).

The other major feature you need is support for multi-object editing – i.e. handling things gracefully when your control needs to display multiple values simultaneously. Test for this by checking the value of EditorGUI.showMixedValue; if it’s true, your control is being used to depict multiple different values simultaneously, so do whatever you need to do to indicate that.

The other major feature you need is support for multi-object editing – ie handling things gracefully when your control needs to display multiple values simultaneously. Test for this by checking the value of EditorGUI.showMixedValue ; if it's true, your control is being used to depict multiple different values simultaneously, so do whatever you need to do to indicate that.

Both the bold-on-prefabOverride and showMixedValue mechanisms require that context for the property has been set up using EditorGUI.BeginProperty() and EditorGUI.EndProperty(). The recommended pattern is to say that if your control method takes a SerializedProperty as an argument, then it will make the calls to BeginProperty and EndProperty itself, while if it deals with ‘raw’ values – similar to, say, EditorGUI.IntField, which takes and returns ints directly and doesn’t work with properties – then the calling code is responsible for calling BeginProperty and EndProperty. (It makes sense, really, because if your control is dealing with ‘raw’ values then it doesn’t have a SerializedProperty value it can pass to BeginProperty anyway).

Both the bold-on- prefabOverride and showMixedValue mechanisms require that context for the property has been set up using EditorGUI.BeginProperty() and EditorGUI.EndProperty() . The recommended pattern is to say that if your control method takes a SerializedProperty as an argument, then it will make the calls to BeginProperty and EndProperty itself, while if it deals with 'raw' values – similar to, say, EditorGUI.IntField , which takes and returns ints directly and doesn't work with properties – then the calling code is responsible for calling BeginProperty and EndProperty . (It makes sense, really, because if your control is dealing with 'raw' values then it doesn't have a SerializedProperty value it can pass to BeginProperty anyway).

public class MySliderDrawer : PropertyDrawer { public override float GetPropertyHeight (SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight; } private GUISkin _sliderSkin; public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { if (_sliderSkin == null) _sliderSkin = (GUISkin)EditorGUIUtility.LoadRequired ("MyCustomSlider Skin"); MyCustomSlider (position, property, _sliderSkin.GetStyle ("MyCustomSlider"), label); } } // Then, the updated definition of MyCustomSlider: public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style, GUIContent label) { label = EditorGUI.BeginProperty (controlRect, label, prop); controlRect = EditorGUI.PrefixLabel (controlRect, label); // Use our previous definition of MyCustomSlider, which we’ve updated to do something // sensible if EditorGUI.showMixedValue is true EditorGUI.BeginChangeCheck(); float newValue = MyCustomSlider(controlRect, prop.floatValue, style); if(EditorGUI.EndChangeCheck()) prop.floatValue = newValue; EditorGUI.EndProperty (); } public class MySliderDrawer : PropertyDrawer { public override float GetPropertyHeight (SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight; } private GUISkin _sliderSkin; public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { if (_sliderSkin == null) _sliderSkin = (GUISkin)EditorGUIUtility.LoadRequired ("MyCustomSlider Skin"); MyCustomSlider (position, property, _sliderSkin.GetStyle ("MyCustomSlider"), label); } } // Then, the updated definition of MyCustomSlider: public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style, GUIContent label) { label = EditorGUI.BeginProperty (controlRect, label, prop); controlRect = EditorGUI.PrefixLabel (controlRect, label); // Use our previous definition of MyCustomSlider, which we’ve updated to do something // sensible if EditorGUI.showMixedValue is true EditorGUI.BeginChangeCheck(); float newValue = MyCustomSlider(controlRect, prop.floatValue, style); if(EditorGUI.EndChangeCheck()) prop.floatValue = newValue; EditorGUI.EndProperty (); }

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

public class MySliderDrawer : PropertyDrawer
{
public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
private GUISkin _sliderSkin;
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
{
if (_sliderSkin == null)
_sliderSkin = (GUISkin)EditorGUIUtility.LoadRequired ("MyCustomSlider Skin");
MyCustomSlider (position, property, _sliderSkin.GetStyle ("MyCustomSlider"), label);
}
}
// Then, the updated definition of MyCustomSlider:
public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style, GUIContent label)
{
label = EditorGUI.BeginProperty (controlRect, label, prop);
controlRect = EditorGUI.PrefixLabel (controlRect, label);
// Use our previous definition of MyCustomSlider, which we’ve updated to do something
// sensible if EditorGUI.showMixedValue is true
EditorGUI.BeginChangeCheck();
float newValue = MyCustomSlider(controlRect, prop.floatValue, style);
if(EditorGUI.EndChangeCheck())
prop.floatValue = newValue;
EditorGUI.EndProperty ();
}

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

public class MySliderDrawer : PropertyDrawer
{
public override float GetPropertyHeight ( SerializedProperty property , GUIContent label )
{
return EditorGUIUtility . singleLineHeight ;
}
private GUISkin _sliderSkin ;
public override void OnGUI ( Rect position , SerializedProperty property , GUIContent label )
{
if ( _sliderSkin == null )
_sliderSkin = ( GUISkin ) EditorGUIUtility . LoadRequired ( "MyCustomSlider Skin" ) ;
MyCustomSlider ( position , property , _sliderSkin . GetStyle ( "MyCustomSlider" ) , label ) ;
}
}
// Then, the updated definition of MyCustomSlider:
public static void MyCustomSlider ( Rect controlRect , SerializedProperty prop , GUIStyle style , GUIContent label )
{
label = EditorGUI . BeginProperty ( controlRect , label , prop ) ;
controlRect = EditorGUI . PrefixLabel ( controlRect , label ) ;
// Use our previous definition of MyCustomSlider, which we’ve updated to do something
// sensible if EditorGUI.showMixedValue is true
EditorGUI . BeginChangeCheck ( ) ;
float newValue = MyCustomSlider ( controlRect , prop . floatValue , style ) ;
if ( EditorGUI . EndChangeCheck ( ) )
prop . floatValue = newValue ;
EditorGUI . EndProperty ( ) ;
}

目前为止就这样了 (That’s all for now)

I hope this post has shed some light on some of the core parts of IMGUI that you’ll need to understand if you want to really take your editor customisation to the next level. There’s more to cover before you can be an Editor guru – the SerializedObject / SerializedProperty system, the use of CustomEditor versus EditorWindow versus PropertyDrawer, the handling of Undo, etc – but IMGUI plays a large part in unlocking Unity’s immense potential for creating custom tools – both with a view to selling on the Asset Store, and with a view to empowering developers on your own teams.

I hope this post has shed some light on some of the core parts of IMGUI that you'll need to understand if you want to really take your editor customisation to the next level. There's more to cover before you can be an Editor guru – the SerializedObject / SerializedProperty system, the use of CustomEditor versus EditorWindow versus PropertyDrawer , the handling of Undo, etc – but IMGUI plays a large part in unlocking Unity's immense potential for creating custom tools – both with a view to selling on the Asset Store, and with a view to empowering developers on your own teams.

Give me your questions and feedback in the comments!

Give me your questions and feedback in the comments!

翻译自: https://blogs.unity3d.com/2015/12/22/going-deep-with-imgui-and-editor-customization/

深入了解IMGUI和编辑器自定义相关推荐

  1. ueditor 编辑html文件名,UEditor编辑器自定义上传图片或文件路径的修改方法,ueditor修改方法...

    UEditor编辑器自定义上传图片或文件路径的修改方法,ueditor修改方法 使用ueditor编辑器,附件默认在ueditor/php/upload/,  我的附件地址是网站根目录下/data/u ...

  2. quill 富文本编辑器自定义格式化

    quilljs 现在富文本编辑器轮子太多了,Github 上随便搜一下就有一堆,我需要实现的功能很简单,所以就佛系地选了 quilljs,quilljs 是一个轻量级的富文本编辑器. 链接: 官网:q ...

  3. ueditor 工具栏配置_百度ueditor编辑器自定义工具栏

    百度ueditor编辑器自定义工具栏: //引入编辑器配置文件和核心文件 //内容容器 var ue = UE.getEditor('content', { toolbars: [ [ //自定义的工 ...

  4. 火狐 html编辑器安装,如何使用配置编辑器自定义Firefox

    如何使用配置编辑器自定义Firefox 2021-06-02 Firefox 提供了多种设置,您可以通过"选项"菜单访问,但您可以使用 Mozilla 浏览器进行更多设置.Fire ...

  5. R语言使用fix函数通过编辑器自定义修改数据变量的名称、例如、使用fix函数自定义修改dataframe数据列的名称

    R语言使用fix函数通过编辑器自定义修改数据变量的名称.例如.使用fix函数自定义修改dataframe数据列的名称 目录

  6. html自定义实现文本编辑器,自定义开发富文本编辑器(Javascript实现点击插入内容到textarea光标处)...

    富文本编辑器相信很多程序员都用过,但是如何自己制作一个仿富文本的编辑器来解决一些简单的或自定义的需求呢?下面给大家共享一个使用JavaScript实现在textarea光标处插入指定文本内容以及字符串 ...

  7. html编辑器自定义脚本,CKeditor富文本编辑器使用技巧之添加自定义插件的方法

    本文实例讲述了CKeditor富文本编辑器使用技巧之添加自定义插件的方法.分享给大家供大家参考,具体如下: 首先就是在CKeditor的plugins目录下新建一个目录qchoice: qchoice ...

  8. html编辑器不支持自定义样式,百度编辑器自定义按钮样式问题(写在cssRules不起做用)?...

    UE.registerUI('dialog',function(editor,uiName){ //创建dialog var dialog = new UE.ui.Dialog({ //指定弹出层中页 ...

  9. 百度编辑器插入自定义html,百度编辑器自定义模板

    前言: 有些时候我们想要一些固定格式的模板,然后在这个模板的基础上去进行编写可以提升我们的效率,就像微信发布图文消息的后台就有很多模板.除了可以方便我们写之外,还有就是有些格式默认是很难直接写出来的这 ...

最新文章

  1. 设计模式 — 结构型模式 — 装饰模式
  2. python middle()_Python自学笔记(七):函数
  3. 服务器健康监控管理系统,一种远程健康监控系统服务器
  4. 实现在Windows下安装Lighttpd的方法
  5. dockerfile安装yum_Docker镜像-基于DockerFile制作yum版nginx镜像
  6. SQL中读取Excel 以及 bpc语言
  7. JAVAWEB入门之Servlet相关配置
  8. 信息学奥赛一本通 2040:【例5.7】筛选法找质数 (普通筛 线性筛)
  9. 台式计算机技术参数响应表,联想台式电脑配置推荐及参数详情【图文】
  10. [大妈吐糟] 虾米音乐的系列猜想
  11. Tensorflow——Dropout(解决过拟合问题)
  12. 不借助第三方变量实现两个整数变量值的互换
  13. recyclerView 列表类控件卡顿优化
  14. 一键解决局域网共享之批处理
  15. Linux服务器CPU压力测试(本人亲测)
  16. 查看mysql数据库密码_如何查看mysql数据库的登录名和密码
  17. python青少年编程比赛_有哪些编程比赛适合青少年参加和锻炼的?
  18. wallpaper 壁纸提取
  19. PHP连接并使用人大金仓数据库kingbase
  20. AtCoder Beginner Contest 162 D.RGB Triplets

热门文章

  1. ### The error may exist in com/itheima/mapper/UserMapper.xml ### Cause: org.apache.ibatis.builder.Bu
  2. html隐藏滚动条,并仍然可以滚动
  3. AI让城市交通管理更“智慧”
  4. 关于数据中台系统,需要了解哪些技术?
  5. python罗马数字转整数
  6. 我是如何做到花8000元拔智齿的
  7. 云计算学习路线图讲解:想学云计算怎么入门?
  8. 何红辉设计模式之六大原则
  9. python简单版斗兽棋
  10. 【图像处理】一文弄明白图像配准(SIFT)