Гайд [СЛОЖНО] Drag'N'Drop в ImGui. Часть 1: Основы

money++
Разработчик
Статус
Оффлайн
Регистрация
14 Июн 2018
Сообщения
638
Реакции[?]
339
Поинты[?]
22K
Увидел я эту (https://yougame.biz/threads/182489/) тему и сразу вспомнил свои муки с Drag'N'Drop'ом...
Поэтому вот гайд для людей, которые готовятся впервые столкнуться с Drag'N'Drop'ом в ImGui, но тем не менее умеют в C++ и знают основы ImGui (если ты НЕ умеешь в С++ или ImGui, у тебя скорее всего ничего не получится ни повторить, ни тем более понять).

Итак, в текущей версии ImGui 1.79, для работы с Drag'N'Drop'ом (я заебался это переписывать, поэтому теперь это будет DND) в ImGui есть следующие методы:
C++:
    IMGUI_API bool                  BeginDragDropSource(ImGuiDragDropFlags flags = 0);                                          // call when the current item is active. If this return true, you can call SetDragDropPayload() + EndDragDropSource()
    IMGUI_API bool                  SetDragDropPayload(const char* type, const void* data, size_t size, ImGuiCond cond = 0);    // type is a user defined string of maximum 32 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui.
    IMGUI_API void                  EndDragDropSource();                                                                        // only call EndDragDropSource() if BeginDragDropSource() returns true!
    IMGUI_API bool                  BeginDragDropTarget();                                                                      // call after submitting an item that may receive a payload. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget()
    IMGUI_API const ImGuiPayload*   AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags = 0);                      // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released.
    IMGUI_API void                  EndDragDropTarget();                                                                        // only call EndDragDropTarget() if BeginDragDropTarget() returns true!
    IMGUI_API const ImGuiPayload*   GetDragDropPayload();                                                                       // peek directly into the current payload from anywhere. may return NULL. use ImGuiPayload::IsDataType() to test for the payload type.
(не)Интересный факт: в версии 1.69 (а может и раньше) DND API в ImGui было тем же, но ocornut держал его в бете, и говорил, что оно может измениться...

Есть следующие флаги (про какие-то я расскажу, какие-то вам придется понимать по строчке комментария на английском):
C++:
enum ImGuiDragDropFlags_
{
    ImGuiDragDropFlags_None                         = 0,
    // BeginDragDropSource() flags
    ImGuiDragDropFlags_SourceNoPreviewTooltip       = 1 << 0,   // By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disable this behavior.
    ImGuiDragDropFlags_SourceNoDisableHover         = 1 << 1,   // By default, when dragging we clear data so that IsItemHovered() will return false, to avoid subsequent user code submitting tooltips. This flag disable this behavior so you can still call IsItemHovered() on the source item.
    ImGuiDragDropFlags_SourceNoHoldToOpenOthers     = 1 << 2,   // Disable the behavior that allows to open tree nodes and collapsing header by holding over them while dragging a source item.
    ImGuiDragDropFlags_SourceAllowNullID            = 1 << 3,   // Allow items such as Text(), Image() that have no unique identifier to be used as drag source, by manufacturing a temporary identifier based on their window-relative position. This is extremely unusual within the dear imgui ecosystem and so we made it explicit.
    ImGuiDragDropFlags_SourceExtern                 = 1 << 4,   // External source (from outside of dear imgui), won't attempt to read current item/window info. Will always return true. Only one Extern source can be active simultaneously.
    ImGuiDragDropFlags_SourceAutoExpirePayload      = 1 << 5,   // Automatically expire the payload if the source cease to be submitted (otherwise payloads are persisting while being dragged)
    // AcceptDragDropPayload() flags
    ImGuiDragDropFlags_AcceptBeforeDelivery         = 1 << 10,  // AcceptDragDropPayload() will returns true even before the mouse button is released. You can then call IsDelivery() to test if the payload needs to be delivered.
    ImGuiDragDropFlags_AcceptNoDrawDefaultRect      = 1 << 11,  // Do not draw the default highlight rectangle when hovering over target.
    ImGuiDragDropFlags_AcceptNoPreviewTooltip       = 1 << 12,  // Request hiding the BeginDragDropSource tooltip from the BeginDragDropTarget site.
    ImGuiDragDropFlags_AcceptPeekOnly               = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect  // For peeking ahead and inspecting the payload before delivery.
};
Ну и наконец для DND ImGui содержит специальную структурку, которая хранит информацию о том, что вы переносите непосредственно своим DND:
C++:
struct ImGuiPayload
{
    // Members
    void*           Data;               // Data (copied and owned by dear imgui)
    int             DataSize;           // Data size

    // [Internal]
    ImGuiID         SourceId;           // Source item id
    ImGuiID         SourceParentId;     // Source parent id (if available)
    int             DataFrameCount;     // Data timestamp
    char            DataType[32+1];     // Data type tag (short user-supplied string, 32 characters max)
    bool            Preview;            // Set when AcceptDragDropPayload() was called and mouse has been hovering the target item (nb: handle overlapping drag targets)
    bool            Delivery;           // Set when AcceptDragDropPayload() was called and mouse button is released over the target item.

    ImGuiPayload()  { Clear(); }
    void Clear()    { SourceId = SourceParentId = 0; Data = NULL; DataSize = 0; memset(DataType, 0, sizeof(DataType)); DataFrameCount = -1; Preview = Delivery = false; }
    bool IsDataType(const char* type) const { return DataFrameCount != -1 && strcmp(type, DataType) == 0; }
    bool IsPreview() const                  { return Preview; }
    bool IsDelivery() const                 { return Delivery; }
};
Итак, нам надо сделать какой-то объект, начав "тянуть" который мы сможем начать перемещать нашу ImGuiPayload...

Лирическое отступление: Что же такое этот ваш ImGuiPayload???
Ответ: Это НЕ то, что вы увидите (или не увидите) на экране при перетягивании. Это структурка, которая хранит тип нашей информации (DataType), значение (Data) и размер информации (DataSize). И еще разные вещи, которые нас особо волновать не должны, так как используются внутри реализации DND в ImGui (оставим работу с ними ocornut'у, он вроде справляется).

Как же нам создать какой-то такой объект? Рассмотрим такой кусок кода:
C++:
static int counter = 0;
if (ImGui::Button("DND source")) {  // Вот эту хрень мы хотим тянуть
    counter++;
}
if (ImGui::BeginDragDropSource()) {
    ImGui::SetDragDropPayload("Counter##Payload", &counter, sizeof(int), ImGuiCond_Once);

    ImGui::Text("Payload data is: %d", *(int*)ImGui::GetDragDropPayload()->Data));

    ImGui::EndDragDropSource();
}
Сначала мы создали кнопку, при нажатии на которую будет увеличиваться какой-то счетчик (по приколу пускай увеличивается, для гайда это ни на что не влияет). Если попробовать потянуть эту кнопку, окажется, что мы начнем "тянуть" наш счетчик:
Пожалуйста, авторизуйтесь для просмотра ссылки.
Возникает справедливый вопрос: "Если Иисус может ходить по воде, может ли он плавать по суше?"... Ну и естественно "А почему мы что-то тянем?!"
Догадливый читатель уже наверное понял, что в этом "виновата" вторая часть кода. И он прав!
Итак, ImGui::BeginDragDropSource() берет последний объект, который мы создали, и наделяет его возможностью быть отдавать информацию, если начать его "тянуть".

Давайте-ка попробуем это все улучшить, и добавить еще один "тянучий" объект:
C++:
ImGui::Text("Answer To Everything is 42");  // Вот эту хрень мы тоже хотим тянуть
if (ImGui::BeginDragDropSource()) {
    int answer_to_everything = 42;
    ImGui::SetDragDropPayload("AnswerToEverything##Payload", &answer_to_everything, sizeof(int), ImGuiCond_Once);

    ImGui::Text("Payload data is: %d", *(int*)ImGui::GetDragDropPayload()->Data));

    ImGui::EndDragDropSource();
}
С довольным лицом мы компилируем нашу программу, зовем всю семью и тут... Программа крашиться... В чем проблема?! Как?! sn0wyQ, ах ты проблема форума, обмануть меня решил, да?
Оказывается, что ImGui::BeginDragDropSource() действительно берет последний объект, который мы создали, но если у него нет ImGuiID (читатель, если он, конечно, прочитал второй абзац, должен был понимать, что от него ДЕЙСТВИТЕЛЬНО потребуется знание основ ImGui), то ImGui::BeginDragDropSource() отказывается работать:
1611057931946.png
Дело в том, что мистер ocornut не смог придумать, как заставить "тянуться" объекты с нулевым ImGuiID, поэтому он вынужден был добавить следующую инструкцию:
"Если вы хотите использовать BeginDragDropSource() с объектом без уникального ImGuiID, такого как, например, Text() или Image(), то вам надо:
A) Прочитать инструкцию далее, B) Использовать флаг ImGuiDragDropFlags_SourceAllowNullID, C) Проглотить вашу кодерскую гордость."
Ладно, следуем инструкции:
C++:
ImGui::Text("Answer To Everything is 42");  // Вот эту хрень мы тоже хотим тянуть
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {  // добавляем флаг
    int answer_to_everything = 42;
    ImGui::SetDragDropPayload("AnswerToEverything##Payload", &answer_to_everything, sizeof(int), ImGuiCond_Once);

    ImGui::Text("Payload data is: %d", *(int*)ImGui::GetDragDropPayload()->Data));

    ImGui::EndDragDropSource();
}
Смотрим на результат:
Пожалуйста, авторизуйтесь для просмотра ссылки.
Такое не стыдно и бойфренду показать! Ой... (ТС не гей, это шутка для поддержания позитивного настроения)

Теперь возникает очередной справедливый вопрос: "Есть ли у глухих людей внутренний голос?". Ну и конечно же вопрос "А зачем нам начинать что-то тянуть, если мы это тянем в никуда?". Исправляюсь.
Как можно было видеть выше, при помощи метода ImGui::GetDragDropPayload() мы можем получить нашу ImGuiPayload (payload кстати переводиться с английского как "полезная нагрузка"). Однако как нам это использовать? Ведь мы же не знаем, в какой именно момент и куда нам сделать вторую часть DND, а именно Drop...
Оказывается есть ImGui::BeginDragDropTarget() который делает предыдущий объект ImGui созданный нами "местом сброса" нашего ImGuiPayload. Мало того, оказывается ImGui::GetDragDropPayload() не подходит, чтобы "принять" перетягиваемые объект, так как этот метод не отслеживает отпустил пользователь объект или нет... Слава богу есть ImGui::AcceptDragDropPayload... Но и с ним все не так просто...

Очередное лирическое отступление к ImGuiPayload:
У этой структуры есть поле char* DataType[33], но за что оно отвечает?
Во время работы программы у нас может быть одновременно несколько объектов, которые мы будем хотеть "тянуть", и несколько мест, куда мы будет хотеть их тянуть. Более того, иногда мы будем хотеть сделать так, чтобы мы могли тянуть из объекта не в каждое из мест, которое может "принять объект". Например если мы будем хотеть тянуть строки с именами только в столбец для имен, а фамилии только в столбец для фамилий.
Но, sn0wyQ, как мы сможем отличить что из этого имя, а что фамилия? Мы же скорее всего будем хранить и то, и то, как std::string?.. Дело в том, что DataType в ImGuiPayload мы можем присвоить любую строку.
Например в примере выше у ImGui::Button("DND source") DataType "отдаваемой" информации был "Counter##Payload", а у ImGui::Text("Answer To Everything is 42") DataType "отдаваемой" информации был "AnswerToEverything##Payload".

Теперь от кучи текста перейдем к куче кода:
C++:
static int last_ate_int_counter;
static bool never_ate_anything_counter = true;
ImGui::Text("I am DND Target for Counter.\nLast thing I ate was ");
ImGui::SameLine();
if (ImGui::BeginDragDropTarget()) {
    auto payload = ImGui::AcceptDragDropPayload("Counter##Payload");
    if (payload != NULL) {  // А вдруг у нас нет ничего, что мы перетягиваем? Или тип того, что перетягиваем не тот? А?
        never_ate_anything_counter = false;
        last_ate_int_counter = *(int*)payload->Data;
    }

    ImGui::EndDragDropTarget();
}

if (never_ate_anything_counter) {
    ImGui::Text("\nnothing :(");  // Ghetto-way чтобы вывести текст, но мы же не этому учимся, так?
}
else {
    ImGui::Text("\n%d", last_ate_int_counter);  // Ghetto-way чтобы вывести текст, но мы же не этому учимся, так?
}

static int last_ate_int_answer_to_everything;
static bool never_ate_anything_answer_to_everything = true;
ImGui::Text("I am DND Target for AnswerToEverything.\nLast thing I ate was ");
ImGui::SameLine();
if (ImGui::BeginDragDropTarget()) {
    auto payload = ImGui::AcceptDragDropPayload("AnswerToEverything##Payload");
    if (payload != NULL) {  // А вдруг у нас нет ничего, что мы перетягиваем? Или тип того, что перетягиваем не тот? А?
        never_ate_anything_answer_to_everything = false;
        last_ate_int_answer_to_everything = *(int*)payload->Data;
    }

    ImGui::EndDragDropTarget();
}

if (never_ate_anything_answer_to_everything) {
    ImGui::Text("\nnothing :(");  // Ghetto-way чтобы вывести текст, но мы же не этому учимся, так?
} else {
    ImGui::Text("\n%d", last_ate_int_answer_to_everything);  // Ghetto-way чтобы вывести текст, но мы же не этому учимся, так?
}
Ну и видео результата:
Пожалуйста, авторизуйтесь для просмотра ссылки.

-Фух..., - облегченно выдохнул sn0wyQ, - вроде готово...
Но у читателя вновь возник очередной справедливый вопрос: "Если я куплю грузовик на eBay, привезут ли его еще в более большом грузовике?". И сразу после этого еще миллион вопросов по теме DND в ImGui. Например:
  • А как сделать так, чтобы объект, который я перетягивал исчез?
  • А как отключить превью того, что я перетягиваю?
  • А как...
В общем много вопросов (которые можно кстати добавить в ответы к теме, и я отвечу на них сразу/в следующей части гайда) которые уже можно решить самому прочитав все комментарии к функциям или же долго долго ломав голову...

А пока спасибо за внимание и удачи в этой нелегкой теме :) Без шуток, сложнее этого мне в ImGui больше ничего не встречалось (кроме красивого дизайна, но тут уже просто я криворукий)
 
Последнее редактирование:
///
Пользователь
Статус
Оффлайн
Регистрация
25 Янв 2018
Сообщения
511
Реакции[?]
114
Поинты[?]
0
Один из самых расписанных гайдов, что я видел. Респект.
Как скоро получится сделать вторую часть?
 
Начинающий
Статус
Оффлайн
Регистрация
20 Май 2020
Сообщения
31
Реакции[?]
14
Поинты[?]
0
на вид гайд хороший но читать лень поэтому стовлю класс
 
money++
Разработчик
Статус
Оффлайн
Регистрация
14 Июн 2018
Сообщения
638
Реакции[?]
339
Поинты[?]
22K
Забаненный
Статус
Оффлайн
Регистрация
15 Янв 2021
Сообщения
46
Реакции[?]
40
Поинты[?]
0
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
шикарный гайд
 
ставь чайник, зажигай плиту
Эксперт
Статус
Оффлайн
Регистрация
22 Май 2020
Сообщения
1,444
Реакции[?]
1,092
Поинты[?]
10K
Воды больше чем во всей Африке
 
money++
Разработчик
Статус
Оффлайн
Регистрация
14 Июн 2018
Сообщения
638
Реакции[?]
339
Поинты[?]
22K
Сверху Снизу