Хабрахабр

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

Далее пойдёт речь не о библиотечных функциях D&D, а о собственной реализации перемещения виджета в пределах окна и сопутствующего функционала. Обычное понимание Drag and Drop(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)
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;
}

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

#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 на любителя. Теоретически даёт некоторый прирост быстродействия. В данном случае какой-то необходимости использования нет.

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

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-

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть