Двигаемые мышкой виджеты. Drag and drop в пределах окна

в 23:19, , рубрики: C, c/c++, c++, GTK+, gtk3, графические интерфейсы

Обычное понимание Drag and Drop(D&D) предполагает, что берётся, например, ссылка на файл из одного виджета и перемещается мышкой в другое окно или виджет. Далее пойдёт речь не о библиотечных функциях D&D, а о собственной реализации перемещения виджета в пределах окна и сопутствующего функционала. Код больше для примера, чем конкретного практического применения, написан в стиле Си с классами. Редактор — CodeBlocks 17.12, который перестал вылетать на Ubuntu x64 по сравнению с 16-ой версией.

image

Существует виджет-контейнер GtkFixed, который может хранить другие виджеты по определённым координатам, классическая же концепция создания приложений на GTK предполагает использование виджетов-контейнеров GtkBox(и других), для корректного растягивания окна и заполнения пространства. Контейнер либо растягивается до размеров окна (и до границ других виджетов), либо уменьшается до размеров дочернего виджета как правило.

Код делится на main.cpp, main.hpp, movable_widgets.hpp. Я не стал выделять отдельно файл реализации. Содержание main.cpp довольно типовое:

#include "main.hpp"
#include "movable_widgets.hpp"

void builder_init(gpointer user_data)
{
    appdata *data=(appdata*) user_data;
    GError *error = NULL;
    GtkBuilder *builder = gtk_builder_new();
    if (!gtk_builder_add_from_file (builder, "window.glade", &error))
    {
        // загрузить файл не удалось
        g_critical ("Не могу загрузить файл: %s", error->message);
        g_error_free (error);
    }
    data->win=GTK_WIDGET(gtk_builder_get_object(builder, "window1"));
    data->notebook=GTK_NOTEBOOK(gtk_builder_get_object(builder, "notebook1"));
    gtk_notebook_remove_page(data->notebook,0); ///лишняя вкладка не нужна
    gtk_builder_connect_signals (builder,data);
    g_clear_object(&builder);
}

void application_activate(GtkApplication *application, gpointer user_data)
{
    appdata *data=(appdata*) user_data;
    builder_init(data);
    gtk_widget_set_size_request(data->win,320,240);
    gtk_application_add_window(data->app,GTK_WINDOW(data->win));
    page_body *page=new page_body(data, G_OBJECT(data->notebook));
    const gchar *text ="<span foreground="blue" size="x-large">Blue text</span>" ;
    GtkWidget *label = gtk_label_new (NULL);
    gtk_label_set_markup (GTK_LABEL (label), text);
    GtkWidget *image=gtk_image_new_from_file("opennet2.gif");
    GtkWidget *image2=gtk_image_new_from_file("n_temp.png");
    page->add_widget(label,label_t,10,10);
    page->add_widget(image,image_t,20,20);
    page->add_widget(image2,image_t,40,40);
    gtk_widget_show_all(data->win);
}

void application_shutdown(const GtkApplication *application, gpointer user_data) {}

int main (int argc, char *argv[])
{
    appdata data;
    gtk_init (&argc, &argv);
    gint res;
    data.app = gtk_application_new("gtk3.org", G_APPLICATION_FLAGS_NONE);
    g_signal_connect(data.app, "activate", G_CALLBACK(application_activate), &data);
    g_signal_connect(data.app, "shutdown", G_CALLBACK(application_shutdown), &data);
    res = g_application_run(G_APPLICATION(data.app), 0, NULL);
    return 0;
}

Часть виджетов создаётся из XML описания (функция builder_init), другая часть программно (page->add_widget). Функция gtk_widget_show_all(data->win); нужна для рекурсивного отображения виджетов и их содержимого. GTK самостоятельно очищает содержимое виджетов при их удалении, в частности другие дочерние виджеты. К моменту выполнения функции обратного вызова application_shutdown главное окно и всё содержимые виджеты уже удалены.

#ifndef MAIN_H
#define MAIN_H
#include <gtk/gtk.h>
#include <stdbool.h>
#include <stdlib.h>
#define restrict __restrict__
class appdata
{
public:
    char *glade_name=(char*)"window.glade";
    GtkApplication *restrict app;
    GtkWidget *restrict win;
    GtkNotebook *restrict notebook;
    GArray *restrict pages;
};
#endif

Поле pages — массив указателей на классы с содержимым страниц, в этом примере не используется, так как 1 вкладка только используется. Использование restrict на любителя. Теоретически даёт некоторый прирост быстродействия. В данном случае какой-то необходимости использования нет.

Сам вставляемый виджет помещается в контейнер типа GtkEventBox. Он собирает события
buttonclick. Также опциональный контейнер типа GtkFrame для отображения виджета в рамочке при нажатии на левую кнопку мыши. Операция смены контейнера является достаточно быстрой. Сама вкладка, куда вставляются виджеты, имеет следующую иерархию вложений: GtkScrolledWindow->GtkViewport->GtkFixed. Изначально виджеты имеют тип GtkWidget, который приводится макросами к типам GtkViewport, GtkFixed. Я бы акцентировал
внимание на макрос вида
InsertedWidgetWithProperty * widget_with_property=&g_array_index(widgets,InsertedWidgetWithProperty,i);
так как здесь наиболее просто сделать ошибку. Параметры x_correction, y_correction — координаты клика мыши относительно вставляемого виджета GtkEvent. Флаг button_not_pressed служит для корректного отображения контейнера frame. По логике подразумевается что если по вставляемому виджету кликнута одна из кнопок мыши, то он должен быть помещён в рамку. То есть события buttonclick и buttonrelease не являются парными в отличие от событий enter-notify-event и leave-notify-event, на которые завязано изменение формы курсора. Если параметры x_correction, button_not_pressed являются служебными, то есть их надо бы поместить в секцию private, флаг click_order служит для отображения текущего виджета поверх остальных.

typedef struct
{
    GtkWidget *restrict widget_ptr;
    GtkWidget *restrict eventbox;
    GtkWidget *restrict frame;  //нужно удалять самостоятельно
    GtkWidget *restrict pmenu;
    widget_type type;
    bool button_not_pressed;
} InsertedWidgetWithProperty;

class page_body
{
public:
    GtkWidget *restrict scrolledwindow;
    GtkWidget *restrict viewport;
    GtkWidget *restrict fixed;
    GArray *restrict widgets=g_array_new(FALSE, TRUE, sizeof(InsertedWidgetWithProperty)); 
    GtkAdjustment *restrict h_adj;
    GtkAdjustment *restrict v_adj;
    int num_of_current_widget=0;
    double x_correction=0;
    double y_correction=0;
    GtkWidget *restrict window; ///локальная копия указателя на главное окно
    int widget_count=0;
    bool click_order=FALSE; //TRUE - перекрывание

    page_body(appdata *data, GObject *container)
    {
        window=data->win;
        h_adj=gtk_adjustment_new(0.0,4.0,900.0,1.0,5.0,10.0);
        v_adj=gtk_adjustment_new(0.0,4.0,900.0,1.0,5.0,10.0);
        scrolledwindow=gtk_scrolled_window_new(h_adj, v_adj);
        viewport=gtk_viewport_new(h_adj, v_adj);
        fixed=gtk_fixed_new();
        gtk_container_add(GTK_CONTAINER(scrolledwindow),GTK_WIDGET(viewport));
        gtk_container_add(GTK_CONTAINER(viewport),GTK_WIDGET(fixed));
        if(GTK_IS_NOTEBOOK(container))
        {
            gtk_notebook_append_page ((GtkNotebook*)container,scrolledwindow,NULL);
        }
        else if(GTK_IS_WIDGET(container))
        {
            gtk_container_add(GTK_CONTAINER(container),scrolledwindow);
        }
        g_signal_connect(fixed,"motion-notify-event",G_CALLBACK(fixed_motion_notify), this);
        g_signal_connect(scrolledwindow,"destroy",G_CALLBACK(scrolled_window_destroy_cb), this);
    }

    ~page_body()
    {
        int i=widgets->len;
        if(widget_count>0)
        {
            for(i; i>=0; i--)
            {
                InsertedWidgetWithProperty *widget_with_property;
                widget_with_property=&g_array_index(widgets,InsertedWidgetWithProperty,i);
            }
        }
        g_array_free(widgets,TRUE);
    }

    void add_widget(GtkWidget *widget, widget_type type, int x, int y)
    {
        ++widget_count;
        InsertedWidgetWithProperty *widget_with_property=(InsertedWidgetWithProperty*)
                g_malloc0(sizeof(InsertedWidgetWithProperty));
        widget_with_property->eventbox=gtk_event_box_new();
        widget_with_property->type=type;
        widget_with_property->widget_ptr=widget;
        gtk_container_add(GTK_CONTAINER(widget_with_property->eventbox),widget);
        gtk_fixed_put(GTK_FIXED(fixed),widget_with_property->eventbox,x,y);
        widget_with_property->pmenu=gtk_menu_new();
        GtkWidget *menu_items = gtk_menu_item_new_with_label ("Удалить");
        gtk_widget_show(menu_items);
        gtk_menu_shell_append (GTK_MENU_SHELL (widget_with_property->pmenu), menu_items);
        g_signal_connect(widget_with_property->eventbox,"button-press-event",G_CALLBACK(eventbox_press_cb),this);
        g_signal_connect(widget_with_property->eventbox,"button-release-event",G_CALLBACK(eventbox_release_cb),this);
        g_signal_connect(menu_items,"activate",G_CALLBACK(menu_delete_activate),this);
        g_signal_connect(widget_with_property->eventbox,"leave-notify-event",G_CALLBACK(eventbox_leave_cb),this);
        g_signal_connect(widget_with_property->eventbox,"enter-notify-event",G_CALLBACK(eventbox_enter_cb),this);
gtk_widget_set_events(widget_with_property->eventbox,GDK_LEAVE_NOTIFY_MASK|GDK_ENTER_NOTIFY_MASK|GDK_STRUCTURE_MASK);
        g_array_append_val(widgets, *widget_with_property);
    }

    inline void change_cursor(char *cursor_name)
    {
        GdkDisplay *display;
        GdkCursor *cursor;
        display = gtk_widget_get_display (window);
        if(cursor_name)
            cursor = gdk_cursor_new_from_name (display, cursor_name);
        else
            cursor = gdk_cursor_new_from_name (display, "default");
        GdkWindow *gdkwindow=gtk_widget_get_window (window);
        gdk_window_set_cursor (gdkwindow, cursor);
    }

    inline void delete_widget(int i)
    {
        InsertedWidgetWithProperty *widget_with_property=
            &g_array_index(this->widgets,InsertedWidgetWithProperty,i);
        GtkWidget *eventbox=widget_with_property->eventbox;
        g_object_ref(eventbox);
        gtk_container_remove(GTK_CONTAINER(this->fixed),eventbox);
        if(widget_with_property->frame!=NULL)
        {
            gtk_widget_destroy(widget_with_property->frame);
        }
        gtk_widget_destroy(widget_with_property->eventbox);
        this->widgets=g_array_remove_index_fast(this->widgets,i);
        --this->widget_count;
    }
};

Формула для вычисления координат виджета так, чтобы он не смещался в какую либо сторону при нажатии кнопки мыши по нему. В нём участвуют координаты клика относительно виджета,
Координаты GtkFixed относительно окна приложения, координаты окна относительно экрана.
Меня несколько смущает поправочный коэффициент +25, но как попроще записать я не знаю. Проверял работу в версиях Ubuntu 15.10, 16.04, 18.04. Сравнение с 0 и -1 проводится чтобы вставляемый виджет не ущёл из прокручиваемой области. Саму прокрутку обеспечивает виджет GtkScrolledWindow.

gboolean
fixed_motion_notify (GtkWidget *widget, GdkEvent  *event, gpointer   user_data)
{
    page_body *page=(page_body*) user_data;
    int x_win, y_win, x_fixed, y_fixed;
    gtk_window_get_position(GTK_WINDOW(page->window),&x_win,&y_win);
    gtk_widget_translate_coordinates(page->window,page->fixed,x_win,y_win,&x_fixed,&y_fixed);
    double correction_y=(-y_fixed+y_win)*2+25;
    double correction_x=(-x_fixed+x_win);
    double x_corr=page->x_correction;
    double y_corr=page->y_correction;
    int position_x=event->motion.x_root-x_corr-x_win-correction_x;
    int position_y=event->motion.y_root-y_corr-y_fixed-correction_y;
    InsertedWidgetWithProperty *widget_with_property=&g_array_index(page->widgets,InsertedWidgetWithProperty,page->num_of_current_widget);
    GtkWidget *fixed=page->fixed;
    GtkWidget *eventbox=widget_with_property->eventbox;
    if(position_x<-1)
        position_x=0;
    if(position_y<-1)
        position_y=0;
    gtk_fixed_move(GTK_FIXED(fixed), eventbox, position_x, position_y);
    return FALSE;
}

Остальные функции обратного вызова. Реализовано удаление отдельных виджетов через контексное меню правой кнопкой мыши. Удаление класса page_body повешено на событие удаления GtkScrolledWindow.

void scrolled_window_destroy_cb (GtkWidget *object, gpointer user_data)
{
    page_body *page=(page_body*) user_data;
    delete page;
}

void menu_delete_activate (GtkMenuItem *menuitem, gpointer user_data)
{
    page_body *page=(page_body*) user_data;
    page->delete_widget(page->num_of_current_widget);
}

gboolean
eventbox_leave_cb (GtkWidget *widget,
                   GdkEvent  *event,
                   gpointer   user_data)
{
    page_body *page=(page_body*) user_data;
    page->change_cursor(NULL);
}

gboolean eventbox_enter_cb (GtkWidget *widget, GdkEvent  *event, gpointer   user_data)
{
    page_body *page=(page_body*) user_data;
    page->change_cursor("pointer");
}

gboolean eventbox_press_cb (GtkWidget *widget, GdkEvent *event, gpointer user_data)
{
    page_body *page=(page_body*) user_data;
    page->x_correction=event->button.x;
    page->y_correction=event->button.y;
    int i=0;
    InsertedWidgetWithProperty *widget_compare;
    for(i; i<=page->widgets->len; i++)
    {
        widget_compare=(InsertedWidgetWithProperty*) page->widgets->data+i;
        if(widget==widget_compare->eventbox)
        {
            page->num_of_current_widget=i;
            break;
        }
    }
    if(widget_compare->button_not_pressed==FALSE)
    {
        GtkWidget *eventbox=widget_compare->eventbox;
        if(page->click_order)
        {
            int x, y;
            gtk_widget_translate_coordinates(page->fixed, eventbox,0,0,&x, &y);
            gtk_container_remove(GTK_CONTAINER(page->fixed),eventbox);
            gtk_fixed_put(GTK_FIXED(page->fixed),eventbox,-x,-y);
        }
        g_object_ref(widget_compare->widget_ptr);
        gtk_container_remove(GTK_CONTAINER(eventbox),widget_compare->widget_ptr);
        if(widget_compare->frame==NULL)
            widget_compare->frame=gtk_frame_new(NULL);
        gtk_container_add(GTK_CONTAINER(widget_compare->frame),widget_compare->widget_ptr);
        gtk_container_add(GTK_CONTAINER(eventbox),widget_compare->frame);
        gtk_widget_show_all(eventbox);
        widget_compare->button_not_pressed=TRUE;
    }

    ///обработка контексного меню
    const gint RIGHT_CLICK = 3;
    if (event->type == GDK_BUTTON_PRESS)
    {
        GdkEventButton *bevent = (GdkEventButton *) event;
        if (bevent->button == RIGHT_CLICK)
        {
            gtk_menu_popup(GTK_MENU(widget_compare->pmenu), NULL, NULL, NULL, NULL,
                           bevent->button, bevent->time);
        }
    }
    return FALSE;
}

gboolean eventbox_release_cb (GtkWidget *eventbox, GdkEvent *event, gpointer user_data)
{
    page_body *page=(page_body*) user_data;
    InsertedWidgetWithProperty *widget_with_property=
        &g_array_index(page->widgets,InsertedWidgetWithProperty,page->num_of_current_widget);
    ///событие отпускания кнопки мыши может не сработать, если нажимать часто
    if(widget_with_property->button_not_pressed==TRUE)
    {
        widget_with_property->frame=(GtkWidget*) g_object_ref(widget_with_property->frame);
        widget_with_property->widget_ptr=(GtkWidget*) g_object_ref(widget_with_property->widget_ptr);
        GtkWidget *frame=widget_with_property->frame;
        GtkWidget *widget=widget_with_property->widget_ptr;
        gtk_container_remove(GTK_CONTAINER(eventbox), frame);
        gtk_container_remove(GTK_CONTAINER(frame), widget);
        gtk_container_add(GTK_CONTAINER(eventbox), widget);
        widget_with_property->button_not_pressed=FALSE;
    }
}

Спасибо за внимание.

github.com/SanyaZ7/movable_widgets_on_GtkFixed-

Автор: SanyaZ7

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js