7. Глава 19: Выпадающие Списки (ComboBox) и Модели Данных (ListStore)

В этой главе мы погрузимся в работу с выпадающими списками (`GtkComboBox`) и связанными с ними моделями данных (`GtkListStore`) в GTK на языке C. Эти элементы являются незаменимыми для предоставления пользователю выбора из ограниченного набора предопределенных значений, например, списка стран, языков или настроек.

Мы рассмотрим два основных подхода к созданию GtkComboBox: 1. Использование упрощенного GtkComboBoxText для простых списков строк. 2. Использование GtkComboBox в паре с GtkListStore и GtkCellRendererText для более гибкого управления данными и их отображением.

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

  • Как создать базовый выпадающий список с помощью GtkComboBoxText.

  • Как добавлять элементы в GtkComboBoxText и получать выбранное значение.

  • Как использовать GtkListStore для хранения данных в табличной форме.

  • Как привязать GtkListStore к GtkComboBox для динамического отображения данных.

  • Что такое GtkCellRendererText и как он используется для отрисовки текста в ячейках.

  • Как обрабатывать выбор элемента в выпадающем списке.

7.1. Основные Компоненты для Работы со Списками

Прежде чем углубиться в примеры, давайте разберём ключевые компоненты, которые мы будем использовать:

  • GtkComboBox: Это основной виджет выпадающего списка. Он может быть привязан к различным моделям данных (например, GtkListStore или GtkTreeStore) для получения своих элементов.

  • GtkComboBoxText: Это упрощенная версия GtkComboBox, предназначенная специально для работы с простыми списками строк. Она автоматически создаёт и управляет внутренней моделью данных, что значительно упрощает код для базовых случаев.

  • GtkListStore: Это гибкая модель данных в GTK, которая представляет собой список строк, где каждая строка может содержать несколько столбцов данных разных типов. GtkListStore — это реализация интерфейса GtkTreeModel и используется для хранения данных, которые затем отображаются такими виджетами, как GtkComboBox или GtkTreeView.

  • GtkCellRendererText: Это «рендерер ячейки». В GTK архитектура «модель-представление» разделяет данные (модель) от их визуального представления (рендерер). GtkCellRendererText отвечает за отображение текста в ячейке виджета. Он не является виджетом сам по себе, а используется виджетами-представлениями (такими как GtkComboBox или GtkTreeView) для рисования содержимого ячеек.

### Пример 19.1: Простой GtkComboBox с массивом значений (GtkComboBoxText)

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

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

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

/**
 * @brief Обработчик события "changed" для GtkComboBoxText.
 *
 * Эта функция вызывается, когда пользователь выбирает новый элемент
 * из выпадающего списка.
 *
 * @param widget Указатель на GtkComboBox, который испустил сигнал.
 * Здесь мы приводим его к GtkComboBoxText, чтобы использовать
 * специфичную для него функцию получения текста.
 * @param data Пользовательские данные (в данном примере NULL).
 */
void on_combo_changed(GtkComboBox *widget, gpointer data) {
    // Получаем активный (выбранный) текст из GtkComboBoxText.
    // gtk_combo_box_text_get_active_text() возвращает новую строку,
    // которую необходимо освободить с помощью g_free(), когда она больше не нужна.
    gchar *text = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(widget));

    // Проверяем, что текст не NULL (т.е. что-то выбрано).
    if (text != NULL) {
        g_print("Вы выбрали: %s\n", text); // Выводим выбранный текст в консоль.
        g_free(text);                      // Освобождаем выделенную память.
    }
}

/**
 * @brief Главная функция программы.
 *
 * Инициализирует GTK, создает окно, добавляет в него GtkComboBoxText
 * с предопределенными значениями и запускает главный цикл событий GTK.
 *
 * @param argc Количество аргументов командной строки.
 * @param argv Массив строк аргументов командной строки.
 * @return Код завершения программы.
 */
int main(int argc, char *argv[]) {
    // Инициализация GTK.
    gtk_init(&argc, &argv);

    // 1. Создание главного окна.
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "Пример GtkComboBoxText");
    gtk_window_set_default_size(GTK_WINDOW(window), 300, 100);
    gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);

    // Подключение сигнала "destroy" для корректного завершения приложения.
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);

    // 2. Создание GtkComboBoxText.
    // Это упрощенный выпадающий список для работы с текстовыми строками.
    GtkWidget *combo = gtk_combo_box_text_new();

    // 3. Добавление элементов в GtkComboBoxText.
    // gtk_combo_box_text_append_text() добавляет новую строку в конец списка.
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo), "C");
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo), "Python");
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo), "Rust");
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo), "Java");
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo), "Go");

    // 4. Установка активного элемента по умолчанию.
    // gtk_combo_box_set_active() устанавливает элемент с заданным индексом
    // как выбранный (0-индексированный). Здесь выбираем первый элемент ("C").
    gtk_combo_box_set_active(GTK_COMBO_BOX(combo), 0);

    // 5. Подключение обработчика события "changed".
    // Сигнал "changed" испускается, когда выбранный элемент в ComboBox изменяется.
    g_signal_connect(combo, "changed", G_CALLBACK(on_combo_changed), NULL);

    // 6. Размещение GtkComboBox в контейнере.
    // Используем GtkBox для размещения ComboBox в окне.
    // VERTICAL - для вертикальной компоновки, 10 - отступ.
    GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    // Устанавливаем внешний отступ вокруг всего GtkBox.
    gtk_container_set_border_width(GTK_CONTAINER(box), 20);
    // Добавляем GtkComboBox в GtkBox.
    // TRUE, TRUE, 10: виджет будет расширяться, заполнять пространство
    // и иметь дополнительный отступ в 10 пикселей.
    gtk_box_pack_start(GTK_BOX(box), combo, TRUE, TRUE, 10);
    // Добавляем GtkBox в главное окно.
    gtk_container_add(GTK_CONTAINER(window), box);

    // 7. Отображение всех виджетов.
    gtk_widget_show_all(window);

    // 8. Запуск главного цикла событий GTK.
    gtk_main();

    return 0;
}

### Пример 19.2: GtkComboBox с GtkListStore для более сложных данных

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

В более сложных сценариях, когда элементы выпадающего списка имеют не только текстовое представление, но и связанные данные (например, ID, числовые значения и т.д.), или когда список генерируется динамически, рекомендуется использовать GtkComboBox с GtkListStore. GtkListStore выступает как модель данных, а GtkCellRendererText используется для отображения текстовых столбцов из этой модели.

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

// Перечисление для удобства определения столбцов в GtkListStore.
// COLUMN_NAME будет хранить строку (имя), COLUMN_ID будет хранить целое число (ID).
// NUM_COLS - общее количество столбцов.
enum {
    COLUMN_NAME,
    COLUMN_ID,
    NUM_COLS
};

/**
 * @brief Обработчик события "changed" для GtkComboBox с GtkListStore.
 *
 * Эта функция вызывается, когда пользователь выбирает новый элемент
 * из выпадающего списка. Она демонстрирует, как получить данные
 * из модели GtkListStore, используя итератор GtkTreeIter.
 *
 * @param widget Указатель на GtkComboBox, который испустил сигнал.
 * @param data Пользовательские данные (в данном примере NULL).
 */
void on_combo_liststore_changed(GtkComboBox *widget, gpointer data) {
    // Получаем модель данных, связанную с GtkComboBox.
    // Модель является GtkTreeModel, но мы знаем, что это GtkListStore.
    GtkTreeModel *model = gtk_combo_box_get_model(widget);

    // Получаем активный (выбранный) итератор в модели.
    GtkTreeIter iter;
    // gtk_combo_box_get_active_iter() возвращает TRUE, если элемент выбран.
    if (gtk_combo_box_get_active_iter(widget, &iter)) {
        gchar *name; // Переменная для хранения имени.
        gint id;     // Переменная для хранения ID.

        // Получаем данные из модели по итератору и индексу столбца.
        // gtk_tree_model_get() позволяет извлечь значения из строки модели.
        // model: наша модель данных.
        // &iter: итератор, указывающий на текущую строку.
        // COLUMN_NAME: индекс столбца для имени (0).
        // &name: адрес переменной, куда будет записано имя.
        // COLUMN_ID: индекс столбца для ID (1).
        // &id: адрес переменной, куда будет записан ID.
        // -1: признак конца списка аргументов.
        gtk_tree_model_get(model, &iter,
                           COLUMN_NAME, &name,
                           COLUMN_ID, &id,
                           -1); // -1 означает конец списка свойств

        g_print("Вы выбрали: %s (ID: %d)\n", name, id); // Выводим выбранные данные.
        g_free(name); // Освобождаем строку, полученную от gtk_tree_model_get().
    }
}

/**
 * @brief Главная функция программы.
 *
 * Инициализирует GTK, создает окно, GtkListStore, наполняет его данными,
 * создает GtkComboBox, связывает его с GtkListStore и GtkCellRendererText,
 * и запускает главный цикл событий GTK.
 *
 * @param argc Количество аргументов командной строки.
 * @param argv Массив строк аргументов командной строки.
 * @return Код завершения программы.
 */
int main(int argc, char *argv[]) {
    gtk_init(&argc, &argv);

    // 1. Создание главного окна.
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "Пример ComboBox с ListStore");
    gtk_window_set_default_size(GTK_WINDOW(window), 300, 150);
    gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);

    // 2. Создание GtkListStore (модели данных).
    // gtk_list_store_new() создает новую модель данных.
    // Первый аргумент: количество столбцов (NUM_COLS).
    // Остальные аргументы: типы данных для каждого столбца.
    // COLUMN_NAME будет G_TYPE_STRING, COLUMN_ID будет G_TYPE_INT.
    GtkListStore *store = gtk_list_store_new(NUM_COLS, G_TYPE_STRING, G_TYPE_INT);
    GtkTreeIter iter; // Итератор, который будет указывать на текущую строку.

    // 3. Наполнение GtkListStore данными.
    // Для каждой записи:
    //   - gtk_list_store_append(store, &iter): добавляет новую пустую строку в store
    //     и устанавливает iter на эту новую строку.
    //   - gtk_list_store_set(store, &iter, column_idx, value, -1): устанавливает значения
    //     для столбцов в текущей строке (указанной iter).

    gtk_list_store_append(store, &iter); // Добавляем первую строку.
    gtk_list_store_set(store, &iter,
                       COLUMN_NAME, "Опция А",
                       COLUMN_ID, 101,
                       -1); // -1 означает конец списка аргументов.

    gtk_list_store_append(store, &iter); // Добавляем вторую строку.
    gtk_list_store_set(store, &iter,
                       COLUMN_NAME, "Опция Б",
                       COLUMN_ID, 102,
                       -1);

    gtk_list_store_append(store, &iter); // Добавляем третью строку.
    gtk_list_store_set(store, &iter,
                       COLUMN_NAME, "Опция В",
                       COLUMN_ID, 103,
                       -1);

    gtk_list_store_append(store, &iter); // Добавляем четвертую строку.
    gtk_list_store_set(store, &iter,
                       COLUMN_NAME, "Опция Г",
                       COLUMN_ID, 104,
                       -1);

    // 4. Создание GtkComboBox с использованием GtkListStore в качестве модели.
    // GTK_TREE_MODEL(store) - приведение GtkListStore* к GtkTreeModel*,
    // так как GtkComboBox работает с обобщенным интерфейсом GtkTreeModel.
    GtkWidget *combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));

    // Поскольку мы передали GtkListStore, GtkComboBox теперь знает, откуда брать данные.
    // Но ему нужно знать, как эти данные отображать. Для этого используются GtkCellRenderer.

    // 5. Создание GtkCellRendererText для отображения текстового столбца.
    // GtkCellRendererText - это объект, который знает, как отрисовать текст в ячейке.
    GtkCellRenderer *renderer = gtk_cell_renderer_text_new();

    // 6. Добавление GtkCellRenderer в GtkComboBox и связывание его со столбцом модели.
    // gtk_cell_layout_pack_start(): добавляет рендерер в компоновку ячеек ComboBox.
    // GTK_CELL_LAYOUT(combo): GtkComboBox реализует интерфейс GtkCellLayout.
    // renderer: наш GtkCellRendererText.
    // TRUE: рендерер будет расширяться, если есть доступное пространство.
    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE);

    // gtk_cell_layout_set_attributes(): связывает свойство рендерера ("text")
    // с конкретным столбцом в модели данных (COLUMN_NAME).
    // Это говорит ComboBox: "Используй данные из COLUMN_NAME модели для свойства 'text' рендерера".
    gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer, "text", COLUMN_NAME, NULL);

    // 7. Установка активного элемента по умолчанию (например, второй элемент - "Опция Б").
    gtk_combo_box_set_active(GTK_COMBO_BOX(combo), 1); // 0-индексированный

    // 8. Подключение обработчика события "changed".
    g_signal_connect(combo, "changed", G_CALLBACK(on_combo_liststore_changed), NULL);

    // 9. Размещение GtkComboBox в контейнере.
    GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_container_set_border_width(GTK_CONTAINER(box), 20);
    gtk_box_pack_start(GTK_BOX(box), combo, FALSE, FALSE, 0); // Не расширять ComboBox
    gtk_container_add(GTK_CONTAINER(window), box);

    // 10. Отображение всех виджетов.
    gtk_widget_show_all(window);

    // 11. Запуск главного цикла событий GTK.
    gtk_main();

    // Важно: освободить GtkListStore.
    // g_object_unref() уменьшает счетчик ссылок.
    // GtkComboBox увеличивает счетчик ссылок на модель при ее установке.
    // Когда ComboBox уничтожается, он уменьшает счетчик.
    // Здесь мы уменьшаем ссылку, полученную при создании ListStore.
    g_object_unref(store);

    return 0;
}

7.2. Компиляция и Запуск

Сохраните каждый пример кода в соответствующий файл: combo_simple_example.c и combo_liststore_example.c.

### Компиляция:

Для сборки программ используйте следующие команды в терминале, находясь в директории с исходными файлами:

# Для примера с GtkComboBoxText:
gcc combo_simple_example.c -o combo_simple_example `pkg-config --cflags --libs gtk+-3.0`

# Для примера с GtkComboBox и GtkListStore:
gcc combo_liststore_example.c -o combo_liststore_example `pkg-config --cflags --libs gtk+-3.0`

### Запуск:

После успешной компиляции вы можете запустить каждое приложение:

# Для примера с GtkComboBoxText:
./combo_simple_example

# Для примера с GtkComboBox и GtkListStore:
./combo_liststore_example

7.3. Ожидаемый Результат

  • `combo_simple_example.c`:
    • Откроется небольшое окно с заголовком «Пример GtkComboBoxText».

    • В центре окна будет выпадающий список с элементами «C», «Python», «Rust», «Java», «Go».

    • По умолчанию будет выбрано «C».

    • При выборе любого элемента из списка (например, «Python»), в терминал будет выведено сообщение: Вы выбрали: Python.

  • `combo_liststore_example.c`:
    • Откроется небольшое окно с заголовком «Пример ComboBox с ListStore».

    • В центре окна будет выпадающий список с элементами «Опция А», «Опция Б», «Опция В», «Опция Г».

    • По умолчанию будет выбрана «Опция Б».

    • При выборе любого элемента из списка (например, «Опция В»), в терминал будет выведено сообщение, включающее как текст, так и связанный ID: Вы выбрали: Опция В (ID: 103).

7.4. Дополнительные Ресурсы

  • GtkComboBox: Официальная документация GtkComboBox.

  • GtkListStore: Официальная документация GtkListStore.

  • GtkCellRendererText: Официальная документация GtkCellRendererText.

  • GtkTreeModel: Интерфейс GtkTreeModel, который реализует GtkListStore.

  • C_GUI_Handbook GitHub - Репозиторий с примерами кода из этого руководства.