5. Глава 17: Гибкая Компоновка с GtkGrid

В предыдущей главе мы освоили линейную компоновку с GtkBox, которая идеально подходит для размещения виджетов по горизонтали или вертикали. Однако для более сложных, табличных или многомерных макетов нам потребуется более мощный инструмент.

GtkGrid позволяет располагать виджеты в виде таблицы с заданным количеством строк и столбцов. Это даёт разработчику полную свободу и точный контроль над позиционированием элементов, а также возможность объединять ячейки, создавая гибкие и адаптивные интерфейсы.

### Что вы узнаете в этой главе:

  • Как создать и инициализировать контейнер GtkGrid.

  • Как добавлять виджеты в конкретные ячейки сетки, используя координаты (строка, столбец).

  • Как объединять ячейки по горизонтали (column-span) и вертикали (row-span).

  • Как управлять отступами между элементами сетки и вокруг неё.

  • Как использовать параметры расширения (expand) для адаптивного дизайна.

5.1. Основные Возможности GtkGrid

GtkGrid предоставляет гораздо больший контроль над расположением элементов по сравнению с GtkBox:

  • Размещение элементов в ячейках (row, column): Каждый виджет помещается в определённую ячейку сетки, заданную координатами строки и столбца, начиная с (0,0) для левого верхнего угла.

  • Объединение ячеек (row-span, column-span): Виджет может занимать не одну ячейку, а растягиваться на несколько строк или столбцов, что идеально подходит для создания сложных макетов (например, заголовок, занимающий всю ширину, или боковая панель, занимающая несколько строк).

  • Установка отступов: Можно задавать отступы как между строками и столбцами, так и вокруг всей сетки.

  • Растягивание/масштабирование элементов: С помощью свойств расширения (expand) и заполнения (fill), а также гомогенности, можно контролировать, как виджеты и сама сетка реагируют на изменение размера окна.

### Пример 17.1: Использование GtkGrid для размещения кнопок в виде таблицы

Название исходного файла: grid_example.c

Этот пример демонстрирует базовое использование GtkGrid для создания простого калькулятороподобного макета с кнопками, включая объединение ячеек.

#include <gtk/gtk.h> // Подключаем основную библиотеку GTK.

/**
 * @brief Callback-функция, которая будет вызвана при нажатии любой кнопки.
 *
 * Эта функция извлекает метку с нажатой кнопки и выводит её в консоль.
 * Это полезно для демонстрации, какая именно кнопка была активирована.
 *
 * @param widget Указатель на GtkWidget, который испустил сигнал (GtkButton).
 * @param data   Универсальный указатель на пользовательские данные (в данном случае NULL).
 */
static void on_button_clicked(GtkWidget *widget, gpointer data) {
    const char *button_label = gtk_button_get_label(GTK_BUTTON(widget));
    g_print("Нажата кнопка: %s\n", button_label);
}

/**
 * @brief Функция активации приложения.
 *
 * Эта функция вызывается, когда приложение запускается и готово к отображению
 * своих окон. Здесь создаются главное окно, GtkGrid и все виджеты.
 *
 * @param app       Указатель на объект GtkApplication.
 * @param user_data Универсальный указатель на пользовательские данные,
 * переданные при подключении сигнала "activate". В данном случае NULL.
 */
static void activate(GtkApplication *app, gpointer user_data) {
    GtkWidget *window;
    GtkWidget *grid;
    GtkWidget *button1, *button2, *button3, *button4; // Объявляем все кнопки

    // 1. Создание главного окна приложения.
    // gtk_application_window_new(app) создает окно, связанное с GtkApplication,
    // что позволяет GTK лучше управлять жизненным циклом приложения.
    window = gtk_application_window_new(app);
    gtk_window_set_title(GTK_WINDOW(window), "GtkGrid Пример");
    gtk_window_set_default_size(GTK_WINDOW(window), 300, 200);
    gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);

    // 2. Создание контейнера GtkGrid.
    // GtkGrid - это контейнер, который позволяет располагать виджеты
    // в виде таблицы (по строкам и столбцам).
    grid = gtk_grid_new();

    // 3. Настройка GtkGrid.
    // Установка внешнего отступа (границы) вокруг всей сетки в 10 пикселей.
    gtk_container_set_border_width(GTK_CONTAINER(grid), 10);
    // Установка отступа в 5 пикселей между строками.
    gtk_grid_set_row_spacing(GTK_GRID(grid), 5);
    // Установка отступа в 5 пикселей между столбцами.
    gtk_grid_set_column_spacing(GTK_GRID(grid), 5);
    // Можно также сделать строки/столбцы гомогенными (равными по размеру)
    // gtk_grid_set_row_homogeneous(GTK_GRID(grid), TRUE);
    // gtk_grid_set_column_homogeneous(GTK_GRID(grid), TRUE);

    // 4. Создание кнопок, которые будут размещены в сетке.
    button1 = gtk_button_new_with_label("Кнопка 1");
    button2 = gtk_button_new_with_label("Кнопка 2");
    button3 = gtk_button_new_with_label("Кнопка 3 (объединена)");
    button4 = gtk_button_new_with_label("Кнопка 4");

    // 5. Подключение обработчика кликов для каждой кнопки.
    g_signal_connect(button1, "clicked", G_CALLBACK(on_button_clicked), NULL);
    g_signal_connect(button2, "clicked", G_CALLBACK(on_button_clicked), NULL);
    g_signal_connect(button3, "clicked", G_CALLBACK(on_button_clicked), NULL);
    g_signal_connect(button4, "clicked", G_CALLBACK(on_button_clicked), NULL);

    // 6. Добавление кнопок в сетку с использованием gtk_grid_attach().
    // Эта функция - ключевая для GtkGrid. Её параметры:
    //   - GTK_GRID(grid): Указатель на GtkGrid.
    //   - buttonN: Виджет, который добавляется.
    //   - left: Индекс столбца, с которого начинается виджет (0-индексированный).
    //   - top: Индекс строки, с которой начинается виджет (0-индексированный).
    //   - width: Количество столбцов, которое занимает виджет (column-span).
    //   - height: Количество строк, которое занимает виджет (row-span).

    // Кнопка 1: Столбец 0, Строка 0, занимает 1 столбец, 1 строку.
    gtk_grid_attach(GTK_GRID(grid), button1, 0, 0, 1, 1);
    // Кнопка 2: Столбец 1, Строка 0, занимает 1 столбец, 1 строку.
    gtk_grid_attach(GTK_GRID(grid), button2, 1, 0, 1, 1);
    // Кнопка 3: Столбец 0, Строка 1, занимает 2 столбца (растягивается), 1 строку.
    gtk_grid_attach(GTK_GRID(grid), button3, 0, 1, 2, 1);
    // Кнопка 4: Столбец 0, Строка 2, занимает 1 столбец, 1 строку.
    gtk_grid_attach(GTK_GRID(grid), button4, 0, 2, 1, 1);

    // 7. Добавление GtkGrid в окно.
    // GtkWindow может содержать только один дочерний виджет, поэтому мы добавляем GtkGrid.
    gtk_container_add(GTK_CONTAINER(window), grid);

    // 8. Отображение всех виджетов (окна, сетки и всех кнопок внутри).
    gtk_widget_show_all(window);
}

/**
 * @brief Главная функция программы, точка входа.
 *
 * Эта функция отвечает за создание объекта GtkApplication,
 * подключение функции активации и запуск основного цикла приложения.
 *
 * @param argc Количество аргументов командной строки.
 * @param argv Массив строк аргументов командной строки.
 * @return Код завершения программы.
 */
int main(int argc, char **argv) {
    GtkApplication *app; // Указатель на объект приложения GTK
    int status;          // Переменная для хранения статуса завершения приложения

    // Создание нового объекта GtkApplication.
    // "org.example.GridApp" - уникальный идентификатор приложения (обычно в стиле DNS).
    // G_APPLICATION_FLAGS_NONE - стандартные флаги приложения.
    app = gtk_application_new("org.example.GridApp", G_APPLICATION_FLAGS_NONE);

    // Подключение сигнала "activate" к нашей функции 'activate'.
    // Сигнал "activate" испускается, когда приложение запускается и готово
    // создать свое первое окно или показать себя пользователю.
    g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);

    // Запуск приложения.
    // g_application_run() запускает главный цикл событий GLib (и GTK).
    // Управление передается GTK, и приложение будет ждать событий.
    // Функция возвращает статус завершения, когда приложение закрывается.
    status = g_application_run(G_APPLICATION(app), argc, argv);

    // Освобождение ресурсов, связанных с объектом GtkApplication.
    g_object_unref(app);

    return status; // Возвращаем статус завершения.
}