[ocornut/imgui]InputXXX 小部件编辑堆栈变量的奇怪行为

2024-05-09 952 views
3

Dear ImGui 的版本/分支:

版本:1.85 分支:对接

我的问题:

在编辑堆栈变量时,我注意到 imgui 出现了一些奇怪的行为。

就我而言,我有一个堆栈变量,每个帧都设置为特定变量的副本,然后我使用基本的 imgui 小部件编辑该堆栈副本。我希望仅在小部件停用时更改原始值,因此我使用 IsItemDeactivatedAfterEdit 函数来检测这一点并更新原始值。

这并不可靠,如果我将焦点切换到在活动小部件之前声明/提交的小部件,我会收到值已更改的信号,但实际上并没有更改堆栈变量。现在,如果我将焦点更改为在当前活动的小部件之后声明的小部件,则一切都会按预期工作。这似乎不正确...

我根本没有按回车键/输入键,只是尝试通过焦点开关/停用小部件来提交值。

截图/视频

https://user-images.githubusercontent.com/10767490/141373792-e84566cd-9e4b-4445-9a59-5c724b8fb0ad.mp4

(抱歉出现黑框,W11 上的视频拍摄似乎存在一些问题)

独立、最小、完整且可验证的示例:

            ImVec4 v = temp;
            ImGui::Begin( "Example" );

            ImGui::InputFloat( "##f4x", &v.x, 0, 0, "%.3f", 0 );
            if ( ImGui::IsItemDeactivatedAfterEdit() )
            {
                temp = v;
            }

            ImGui::InputFloat( "##f4y", &v.y, 0, 0, "%.3f", 0 );
            if ( ImGui::IsItemDeactivatedAfterEdit() )
            {
                temp = v;
            }

            ImGui::InputFloat( "##f4z", &v.z, 0, 0, "%.3f", 0 );
            if ( ImGui::IsItemDeactivatedAfterEdit() )
            {
                temp = v;
            }

            ImGui::InputFloat( "##f4w", &v.w, 0, 0, "%.3f", 0 );
            if ( ImGui::IsItemDeactivatedAfterEdit() )
            {
                temp = v;
            }
            ImGui::End();

回答

9

使用 Dear ImGui,您负责维护正在编辑的值的后备存储。在您的代码中,您不断地vtemp.

v当您设置时,您需要更加挑剔temp

如果temp从未在外部写入,最简单的解决方案是创建v一个静态变量(或在调用之间保留的其他变量)。

如果temp可以在外部更改,则在将其复制到v.


它似乎起作用的原因是InputText(用于实现InputFloat)在编辑文本时在内部创建字符串缓冲区的私有副本。

(为什么它有时有效而其他无效的原因更复杂。我开始解释它,但它变成了文字墙。简短的版本总是IsItemDeactivatedAfterEdit有一个延迟帧,并且更新里面的值InputText有 0 或1 帧延迟,具体取决于情况。当它最终为 0 时,v在将其应用回 之前,该值将被覆盖temp。)

3

所以我想我必须复制每个编辑过的值。我的用例是属性网格。我只需要在编辑操作完成后更改实际值,否则我的撤消/重做堆栈会爆炸。

据我了解,我基本上需要在 imgui 之上构建一个保留模式包装器来解决这个问题?缓存每个编辑属性的值,让 imgui 小部件修改该值,然后将其反映回实际编辑的类型?

2

是的,这听起来是一个可靠的计划。

另一种选择(如果撤消/重做堆栈问题是您主要关心的问题)是让您的撤消/重做堆栈仅实际提交IsItemDeactivatedAfterEdit或类似的东西。

7

这看起来像是一个错误InputText()。此行为源于以下事实:所有实例InputText()在活动时都使用单个数据结构来存储数据。因此,例如,当我们处于InputText(&y)活动状态,但尚未提交更改并且我们激活时InputText(&x),这个新激活的小部件会丢弃状态InputText(&y)并用它自己的状态覆盖。当渲染的时候,InputText(&y)它看到其他小部件处于活动状态,并且只是继续渲染根据传递给小部件的参数格式化的文本,而不是不再存在的内部状态。

解决这个问题并不简单。如果我们想像现在一样修补代码,我们将不得不引入 1 帧延迟,因为当先前的小部件被激活时,它应该意识到另一个小部件InputText()处于活动状态并且它没有提交更改,然后等待这种情况发生。

但并非一切都丢失了。 go进行了返工InputText(),我们一直在考虑引入 的多个内部状态InputText()。它不仅会立即解决这个特定问题,而且还会给我们带来一些好处,例如在 deactivated 中显示光标/选择/滚动条InputText()

5

你好,

据我了解,我基本上需要在 imgui 之上构建一个保留模式包装器来解决这个问题?

我听说你已经在其他地方说过六次这种变化了,鉴于库的目的是最大限度地减少用户的工作,考虑到修复错误或在库方面实现功能通常可能更具建设性。使用笼统术语“保留模式包装器”也可能会产生误导,因为它可能意味着很多事情。例如,在很多情况下,您只需要存储 1 个元素(例如,只能有一个活动项),这往往非常容易实现。

这里有各种各样的东西。

(1) 我们明确希望输入具有 _NoLiveEdit 标志的等效项(无论最终名称是什么)。它通常可能是一个很好的默认数值(#701)。如果我们针对标量执行通用版本(包括 ),那么实现通用版本(包括 )的摩擦InputText()可能会更低,在我们合并即将进行的InputText()重构之前可能会起作用。

短版本IsItemDeactivatedAfterEdit()总是有一帧延迟

我认为不存在,可以通过以下测试来确认:

static ImVec4 w;
static int activated_frame = -1;
static int activated_field = 0;
static int deactivated_frame = -1;
static int deactivated_field = 0;

ImGui::InputFloat("w.x", &w.x, 0, 0, "%.3f", 0);
if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 0; }
if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 0; }

ImGui::InputFloat("w.y", &w.y, 0, 0, "%.3f", 0);
if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 1; }
if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 1; }

ImGui::InputFloat("w.z", &w.z, 0, 0, "%.3f", 0);
if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 2; }
if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 2; }

ImGui::InputFloat("w.w", &w.w, 0, 0, "%.3f", 0);
if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 3; }
if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 3; }

ImGui::Text("w %.3f %.3f %.3f %.3f", w.x, w.y, w.z, w.w);
ImGui::Text("Activated frame %d, field %d", activated_frame, activated_field);
ImGui::Text("Deactivated frame %d, field %d", deactivated_frame, deactivated_field);

问题确实在于 @PathogenDavid 中描述的 InputText() 中数据的后备副本的行为是相当不明确的。人们一直依赖它来表达一些习语,但它有问题,而你在这里遇到了一个问题。这是对未定义行为的滥用,但在没有 _NoLiveEdit 标志的情况下,人们尝试这样做是非常可以理解的,我自己经常建议利用该行为的代码(事实上,我们设计了一些回归测试)验证该行为的某些方面)。

我收到值更改的信号,但实际上并没有更改堆栈变量。

IsItemDeactivatedAfterEdit()是在右侧框架上报告的,但关联的小部件已经丢失了其支持数据并且不会写入堆栈变量。从定义上来说,这在技术上并不是不正确的,IsItemDeactivatedAfterEdit()因为编辑可能在很多帧/秒之前发生,但结合前面提到的对备份副本的依赖,我们遇到了问题。取决于焦点之前之后的项目的不一致会造成明确的错误。

我认为我们应该从两方面解决这个问题:

  • [ ] 旨在遵守与停用帧匹配的最后写入。从技术上讲,这还允许假设的“RevertOnUnfocus”标志,该标志绝对需要最后一次写入。
  • [ ] 旨在拥有 _NoLiveEdit 标志 (#701)

两者都可以解决问题,但我认为两者最终都是可取的。

另一种选择(如果撤消/重做堆栈问题是您主要关心的问题)是让您的撤消/重做堆栈仅在 IsItemDeactivatedAfterEdit 或类似的事情上实际提交。

在我的上一款游戏中,撤消堆栈仅存储新值。所以而不是存储

  • “事件:将 X 从 0.0f 更改为 42.0f”
  • “事件:将 X 从 42.0f 更改为 100.0f”

它将存储:

  • “X 的初始状态为 0.0f”
  • “事件:将 X 更改为 42.0f”
  • “事件:将 X 更改为 100.0f”

这不太好* ,但我想这就是我暂时回避 _NoLiveEdit 的原因之一。我的错。 (*出于各种原因:它需要状态缓存(无法扩展)或撤消期间的查找(考虑到它发生在交互事件上,因此可以扩展,但它对副作用或其他影响更脆弱)更改同一字段的代码)

5

解决这个问题并不简单。如果我们想像现在一样修补代码,我们将不得不引入 1 帧延迟,因为当先前的小部件被激活时,它应该意识到另一个 InputText() 处于活动状态并且它没有提交更改,然后等待发生这种情况。

我们绝对会在这里避免滞后框架,主要是因为它可能会对其他逻辑产生副作用。

但并非一切都丢失了。 InputText() 正在进行重做,我们一直在考虑引入 InputText() 的多个内部状态。它不仅会立即解决这个特定问题,而且还会给我们带来一些好处,例如在停用的 InputText() 中显示光标/选择/滚动条。

我们甚至可以在更大的合并之前实现这一点(我想我会尝试,如果它太混乱,我会推迟到合并 InputText 更改时)。

6
  • 旨在尊重与停用帧匹配的最后写入。
  • 目标是拥有一个 _NoLiveEdit 标志(两者都可以解决问题,但我认为两者最终都是可取的。

这里脑放屁:_NoLiveEdit 将在没有堆栈/临时变量的情况下方便使用,但将依赖于我们在停用帧上执行最后一次写入。所以第一项是 100% 首先需要的。

6

我听说你已经在其他地方说过六次这种变化了,鉴于库的目的是最大限度地减少用户的工作,考虑到修复错误或在库方面实现功能通常可能更具建设性。使用笼统术语“保留模式包装器”也可能会产生误导,因为它可能意味着很多事情。例如,在很多情况下,您只需要存储 1 个元素(例如,只能有一个活动项),这往往非常容易实现。

我的错,从第一个回复中我就认为问题出在我这边。因此,我开始解决这个问题,为 imgui 提供一个要处理的固定值,并在收到编辑操作完成的信号时将其复制回实际类型。理想情况下,如果我不必做任何事情,那就太棒了,并且删除了我的大量代码(我需要它快速工作,所以我别无选择,只能解决它)。理想情况下,如果这个问题可以在图书馆方面解决,这显然会更好。我认为如果我正确理解 _noliveedit 标志的作用,那么它将解决我 99% 的问题。

3

对于对此缺乏反应,我深表歉意,我本打算修复 1.86,但没有,我希望它很快就会到来。

我基本上已经完成了 99% 的修复,几乎准备好了,但有一些微妙的边缘情况是有问题的(例如,以编程方式更改焦点,这相当于单击较早的小部件,使先前活动的小部件由于即时滚动而被剪裁,使其成为问题)无法“提交”其数据)。 1.86 的剪辑器更改工作应该允许修复这些内容,将特定 ID 或区域标记为不可剪辑,例如额外的帧。

7

我已经用 5a2b1e8 修复了这个问题。

自动回归测试: https://github.com/ocornut/imgui_test_engine/commit/aa143e9bb4dccf2422bc5aad9d124de95dcb49f5

widgets_inputtext_deactivate_apply_0000

需要明确的是,我相信这正在改进对接受用户不保留其数据的某种不符合规范的功能的支持,但我确实理解,在没有“延迟提交”/“无实时编辑”的情况下标记可能需要利用这一点。

当/如果我们添加该选项时,根据 InputText() 重写的状态,我们可能希望或需要放弃对用户不保留其数据的支持,如果发生这种情况,我将在其他地方发布说明。

下面是一个交互式测试平台:

ImGui::Begin("#4714");

ImGui::Button("Dummy"); // Dummy button to test earlier item stealing active id without reusing InputText() internal buffer.

{
    static ImVec4 color0(1.0f, 0.0f, 0.0f, 1.0f);
    static ImVec4 color1(0.0f, 1.0f, 0.0f, 1.0f);
    static int edited_ret_frame = -1;
    static int edited_ret_field = 0;
    static int edited_query_frame = -1;
    static int edited_query_field = 0;
    static int deactivated_frame = -1;
    static int deactivated_field = 0;

    if (ImGui::ColorEdit4("color0", &color0.x)) { edited_ret_frame = ImGui::GetFrameCount(); edited_ret_field = 0; }
    if (ImGui::IsItemEdited()) { edited_query_frame = ImGui::GetFrameCount(); edited_query_field = 0; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 0; }

    if (ImGui::ColorEdit4("color1", &color1.x)) { edited_ret_frame = ImGui::GetFrameCount(); edited_ret_field = 1; }
    if (ImGui::IsItemEdited()) { edited_query_frame = ImGui::GetFrameCount(); edited_query_field = 1; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 1; }

    ImGui::Text("Edited (ret) frame %d, field %d", edited_ret_frame, edited_ret_field);
    ImGui::Text("Edited (query) frame %d, field %d", edited_query_frame, edited_query_field);
    ImGui::Text("Deactivated frame %d, field %d", deactivated_frame, deactivated_field);
}

ImGui::Separator();

{
    static ImVec4 w;
    static int activated_frame = -1;
    static int activated_field = 0;
    static int deactivated_frame = -1;
    static int deactivated_field = 0;

    ImGui::InputFloat("w.x", &w.x, 0, 0, "%.3f", 0);
    if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 0; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 0; }

    ImGui::InputFloat("w.y", &w.y, 0, 0, "%.3f", 0);
    if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 1; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 1; }

    ImGui::InputFloat("w.z", &w.z, 0, 0, "%.3f", 0);
    if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 2; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 2; }

    ImGui::Text("w %.3f %.3f %.3f %.3f", w.x, w.y, w.z, w.w);
    ImGui::Text("Activated frame %d, field %d", activated_frame, activated_field);
    ImGui::Text("Deactivated frame %d, field %d", deactivated_frame, deactivated_field);
}

ImGui::Separator();

{
    static ImVec4 temp;
    static int activated_frame = -1;
    static int activated_field = 0;
    static int edited_ret_frame = -1;
    static int edited_ret_field = 0;
    static int edited_query_frame = -1;
    static int edited_query_field = 0;
    static int deactivated_frame = -1;
    static int deactivated_field = 0;

    ImVec4 v = temp;
    //ImVec4& v = temp;
    if (ImGui::InputFloat("temp##f4x", &v.x, 0, 0, "%.3f", 0)) { edited_ret_frame = ImGui::GetFrameCount(); edited_ret_field = 0; }
    if (ImGui::IsItemEdited()) { edited_query_frame = ImGui::GetFrameCount(); edited_query_field = 0; }
    if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 0; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 0; }
    if (ImGui::IsItemDeactivatedAfterEdit())
    {
        temp = v;
    }

    if (ImGui::InputFloat("##f4y", &v.y, 0, 0, "%.3f", 0)) { edited_ret_frame = ImGui::GetFrameCount(); edited_ret_field = 1; }
    if (ImGui::IsItemEdited()) { edited_query_frame = ImGui::GetFrameCount(); edited_query_field = 1; }
    if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 1; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 1; }
    if (ImGui::IsItemDeactivatedAfterEdit())
    {
        temp = v;
    }

    if (ImGui::InputFloat("##f4z", &v.z, 0, 0, "%.3f", 0)) { edited_ret_frame = ImGui::GetFrameCount(); edited_ret_field = 2; }
    if (ImGui::IsItemEdited()) { edited_query_frame = ImGui::GetFrameCount(); edited_query_field = 2; }
    if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 2; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 2; }
    if (ImGui::IsItemDeactivatedAfterEdit())
    {
        temp = v;
    }

    ImGui::Text("Temp %.3f %.3f %.3f %.3f", temp.x, temp.y, temp.z, temp.w);
    ImGui::Text("Activated frame %d, field %d", activated_frame, activated_field);
    ImGui::Text("Edited (ret) frame %d, field %d", edited_ret_frame, edited_ret_field);
    ImGui::Text("Edited (query) frame %d, field %d", edited_query_frame, edited_query_field);
    ImGui::Text("Deactivated frame %d, field %d", deactivated_frame, deactivated_field);
}

ImGui::Separator();

{
    static char buf1[100];
    static char buf2[100];
    static char buf3[100];
    static int activated_frame = -1;
    static int activated_field = 0;
    static int edited_ret_frame = -1;
    static int edited_ret_field = 0;
    static int edited_query_frame = -1;
    static int edited_query_field = 0;
    static int deactivated_frame = -1;
    static int deactivated_field = 0;

    //ImVec4 v = temp;
    //ImVec4& v = temp;
    if (ImGui::InputText("##str1", buf1, 100)) { edited_ret_frame = ImGui::GetFrameCount(); edited_ret_field = 0; }
    if (ImGui::IsItemEdited()) { edited_query_frame = ImGui::GetFrameCount(); edited_query_field = 0; }
    if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 0; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 0; }

    if (ImGui::InputText("##str2", buf2, 100)) { edited_ret_frame = ImGui::GetFrameCount(); edited_ret_field = 1; }
    if (ImGui::IsItemEdited()) { edited_query_frame = ImGui::GetFrameCount(); edited_query_field = 1; }
    if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 1; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 1; }

    if (ImGui::InputText("##str3", buf3, 100)) { edited_ret_frame = ImGui::GetFrameCount(); edited_ret_field = 2; }
    if (ImGui::IsItemEdited()) { edited_query_frame = ImGui::GetFrameCount(); edited_query_field = 2; }
    if (ImGui::IsItemActivated()) { activated_frame = ImGui::GetFrameCount(); activated_field = 2; }
    if (ImGui::IsItemDeactivatedAfterEdit()) { deactivated_frame = ImGui::GetFrameCount(); deactivated_field = 2; }

    ImGui::Text("bufs: \"%s\" \"%s\" \"%s\"", buf1, buf2, buf3);
    ImGui::Text("Activated frame %d, field %d", activated_frame, activated_field);
    ImGui::Text("Edited (ret) frame %d, field %d", edited_ret_frame, edited_ret_field);
    ImGui::Text("Edited (query) frame %d, field %d", edited_query_frame, edited_query_field);
    ImGui::Text("Deactivated frame %d, field %d", deactivated_frame, deactivated_field);
}

ImGui::Button("Dummy2");

ImGui::End();
3

仅供参考,这在某些情况下导致崩溃,修复了 e8206db (#6292)