gtk2.0实现树状列表搜索并自动展开

⌚Time: 2025-04-15 22:52:00

👨‍💻Author: Jack Ge

gtk2.0实现树状列表搜索,与列表视图搜索差不多,但是更加复杂:

  1. 在搜索时需要搜索所有子节点。
  2. 需要自动展开搜索到的子节点。

所以代码中就多了这两个部分。其中search_tree_recursive就是递归搜索所有节点,expand_parents是展开节点的所有父节点,实现子节点的可见。

代码主体由deepseek ai提供

#include <gtk/gtk.h>
#include <string.h>

typedef struct {
    GtkTreeView *treeview;
    GtkEntry *entry;
    GList *matches;     // 所有匹配项的路径列表
    GList *current;     // 当前显示的匹配项
    GtkLabel *status;   // 显示状态信息
} SearchData;

/* 清除之前的搜索结果 */
void clear_search_results(SearchData *data)
{
    if (data->matches) {
        g_list_foreach(data->matches, (GFunc)gtk_tree_path_free, NULL);
        g_list_free(data->matches);
        data->matches = NULL;
    }
    data->current = NULL;
}

/* 展开路径的所有父节点 */
void expand_parents(GtkTreeView *treeview, GtkTreePath *path)
{
    GtkTreePath *parent_path = gtk_tree_path_copy(path);
    /*不能直接这样用*/
    /*
    while (gtk_tree_path_up(parent_path)) {
        gtk_tree_view_expand_row(treeview, parent_path, FALSE);
    }
    */
    GList *pathList = NULL;

    // 构建路径列表(从最顶层到最接近的父节点)
    while (gtk_tree_path_up(parent_path)) {
        // 每次都需要复制新的路径对象
        pathList = g_list_prepend(pathList, gtk_tree_path_copy(parent_path));
    }

    // 展开所有父节点(从最顶层到最接近的父节点)
    GList *iter = pathList;
    while (iter != NULL) {
        gtk_tree_view_expand_row(treeview, (GtkTreePath *)iter->data, FALSE);
        iter = iter->next;
    }

    // 释放资源
    g_list_foreach(pathList, (GFunc)gtk_tree_path_free, NULL);
    g_list_free(pathList);
    gtk_tree_path_free(parent_path);
}

/* 递归搜索树的所有节点 */
void search_tree_recursive(GtkTreeModel *model, GtkTreeIter *parent, 
                         const gchar *search_text, SearchData *data)
{
    GtkTreeIter iter;
    gboolean valid = gtk_tree_model_iter_children(model, &iter, parent);
    
    while (valid) {
        gchar *text;
        gtk_tree_model_get(model, &iter, 0, &text, -1);
        
        if (text && strstr(text, search_text)) {
            GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
            data->matches = g_list_append(data->matches, path);
        }
        
        // 递归搜索子节点
        if (gtk_tree_model_iter_has_child(model, &iter)) {
            search_tree_recursive(model, &iter, search_text, data);
        }
        
        g_free(text);
        valid = gtk_tree_model_iter_next(model, &iter);
    }
}

/* 搜索所有匹配项 */
void perform_search(SearchData *data)
{
    GtkTreeModel *model = gtk_tree_view_get_model(data->treeview);
    const gchar *search_text = gtk_entry_get_text(data->entry);
    
    clear_search_results(data);
    
    if (search_text == NULL || strlen(search_text) == 0) {
        gtk_label_set_text(data->status, "Please enter search text");
        return;
    }
    
    /* 从根节点开始递归搜索 */
    search_tree_recursive(model, NULL, search_text, data);
    
    /* 更新状态 */
    guint count = g_list_length(data->matches);
    if (count > 0) {
        data->current = data->matches;
        gchar *msg = g_strdup_printf("Total: %d  Current: 1/%d", count, count);
        gtk_label_set_text(data->status, msg);
        g_free(msg);
        
        /* 跳转到第一个匹配项 */
        GtkTreePath *path = (GtkTreePath *)data->current->data;
        
        // 展开所有父节点确保匹配项可见
        expand_parents(data->treeview, path);
        
        GtkTreeSelection *selection = gtk_tree_view_get_selection(data->treeview);
        gtk_tree_selection_select_path(selection, path);
        gtk_tree_view_scroll_to_cell(data->treeview, path, NULL, TRUE, 0.5, 0.0);
    } else {
        gtk_label_set_text(data->status, "No matches found");
    }
}

/* 按钮点击事件处理函数 */
void on_search_clicked(GtkButton *button, gpointer user_data)
{
    SearchData *data = (SearchData *)user_data;
    perform_search(data);
}

void on_next_clicked(GtkButton *button, gpointer user_data)
{
    SearchData *data = (SearchData *)user_data;
    
    if (data->matches == NULL) {
        perform_search(data);
        return;
    }
    
    if (data->current && data->current->next) {
        data->current = data->current->next;
        
        GtkTreePath *path = (GtkTreePath *)data->current->data;
        
        // 展开所有父节点确保匹配项可见
        expand_parents(data->treeview, path);
        
        GtkTreeSelection *selection = gtk_tree_view_get_selection(data->treeview);
        gtk_tree_selection_select_path(selection, path);
        gtk_tree_view_scroll_to_cell(data->treeview, path, NULL, TRUE, 0.5, 0.0);
        
        /* 更新状态 */
        guint pos = g_list_position(data->matches, data->current) + 1;
        guint total = g_list_length(data->matches);
        gchar *msg = g_strdup_printf("Total: %d  Current: %d/%d", total, pos, total);
        gtk_label_set_text(data->status, msg);
        g_free(msg);
    }
}

void on_previous_clicked(GtkButton *button, gpointer user_data)
{
    SearchData *data = (SearchData *)user_data;
    
    if (data->matches == NULL) {
        perform_search(data);
        return;
    }
    
    if (data->current && data->current->prev) {
        data->current = data->current->prev;
        
        GtkTreePath *path = (GtkTreePath *)data->current->data;
        
        // 展开所有父节点确保匹配项可见
        expand_parents(data->treeview, path);
        
        GtkTreeSelection *selection = gtk_tree_view_get_selection(data->treeview);
        gtk_tree_selection_select_path(selection, path);
        gtk_tree_view_scroll_to_cell(data->treeview, path, NULL, TRUE, 0.5, 0.0);
        
        /* 更新状态 */
        guint pos = g_list_position(data->matches, data->current) + 1;
        guint total = g_list_length(data->matches);
        gchar *msg = g_strdup_printf("Total: %d  Current: %d/%d", total, pos, total);
        gtk_label_set_text(data->status, msg);
        g_free(msg);
    }
}

/* 创建示例树状数据 */
void setup_tree_data(GtkTreeStore *store)
{
    GtkTreeIter iter, child_iter, grandchild_iter;
    
    /* 添加第一级节点 - 水果 */
    gtk_tree_store_append(store, &iter, NULL);
    gtk_tree_store_set(store, &iter, 0, "Fruits", 1, 0, -1);
    
    /* 添加子节点 */
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Apple", 1, 10, -1);
    
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Banana", 1, 20, -1);
    
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Orange", 1, 15, -1);
    
    /* 添加第二级节点 - 蔬菜 */
    gtk_tree_store_append(store, &iter, NULL);
    gtk_tree_store_set(store, &iter, 0, "Vegetables", 1, 0, -1);
    
    /* 添加子节点 */
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Carrot", 1, 8, -1);
    
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Potato", 1, 12, -1);
    
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Tomato", 1, 6, -1);
    
    /* 添加第三级节点 - 饮料 */
    gtk_tree_store_append(store, &iter, NULL);
    gtk_tree_store_set(store, &iter, 0, "Drinks", 1, 0, -1);
    
    /* 添加子节点 */
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Water", 1, 30, -1);
    
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Juice", 1, 5, -1);
    
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Milk", 1, 7, -1);
    
    /* 添加更多层级数据 - 电子产品 */
    gtk_tree_store_append(store, &iter, NULL);
    gtk_tree_store_set(store, &iter, 0, "Electronics", 1, 0, -1);
    
    /* 电脑分类 */
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Computers", 1, 0, -1);
    
    gtk_tree_store_append(store, &grandchild_iter, &child_iter);
    gtk_tree_store_set(store, &grandchild_iter, 0, "Laptop", 1, 5, -1);
    
    gtk_tree_store_append(store, &grandchild_iter, &child_iter);
    gtk_tree_store_set(store, &grandchild_iter, 0, "Desktop", 1, 3, -1);
    
    /* 手机分类 */
    gtk_tree_store_append(store, &child_iter, &iter);
    gtk_tree_store_set(store, &child_iter, 0, "Phones", 1, 0, -1);
    
    gtk_tree_store_append(store, &grandchild_iter, &child_iter);
    gtk_tree_store_set(store, &grandchild_iter, 0, "Smartphone", 1, 8, -1);
    
    gtk_tree_store_append(store, &grandchild_iter, &child_iter);
    gtk_tree_store_set(store, &grandchild_iter, 0, "Tablet", 1, 4, -1);
}

int main(int argc, char *argv[])
{
    GtkWidget *window, *scrolled_window, *treeview, *vbox, *hbox, *entry;
    GtkWidget *search_btn, *prev_btn, *next_btn, *status_label;
    GtkTreeStore *store;
    SearchData *data = g_new0(SearchData, 1);
    
    gtk_init(&argc, &argv);
    
    /* 创建主窗口 */
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "TreeView Search");
    gtk_window_set_default_size(GTK_WINDOW(window), 500, 400);
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    
    /* 创建垂直容器 */
    vbox = gtk_vbox_new(FALSE, 5);
    gtk_container_add(GTK_CONTAINER(window), vbox);
    
    /* 创建搜索栏 */
    hbox = gtk_hbox_new(FALSE, 5);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
    
    /* 创建搜索输入框 */
    entry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(hbox), entry, TRUE, TRUE, 0);
    
    /* 创建搜索按钮 */
    search_btn = gtk_button_new_with_label("Search");
    gtk_box_pack_start(GTK_BOX(hbox), search_btn, FALSE, FALSE, 0);
    
    /* 创建导航按钮栏 */
    hbox = gtk_hbox_new(FALSE, 5);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
    
    prev_btn = gtk_button_new_with_label("Previous");
    gtk_box_pack_start(GTK_BOX(hbox), prev_btn, FALSE, FALSE, 0);
    
    next_btn = gtk_button_new_with_label("Next");
    gtk_box_pack_start(GTK_BOX(hbox), next_btn, FALSE, FALSE, 0);
    
    /* 创建状态标签 */
    status_label = gtk_label_new("Ready to search");
    gtk_box_pack_start(GTK_BOX(vbox), status_label, FALSE, FALSE, 0);
    
    /* 创建滚动窗口和树状视图 */
    scrolled_window = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window),
                                  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    gtk_box_pack_start(GTK_BOX(vbox), scrolled_window, TRUE, TRUE, 0);
    
    treeview = gtk_tree_view_new();
    gtk_container_add(GTK_CONTAINER(scrolled_window), treeview);
    
    /* 初始化搜索数据结构 */
    data->treeview = GTK_TREE_VIEW(treeview);
    data->entry = GTK_ENTRY(entry);
    data->status = GTK_LABEL(status_label);
    data->matches = NULL;
    data->current = NULL;
    
    /* 创建TreeStore并添加数据 */
    store = gtk_tree_store_new(2, G_TYPE_STRING, G_TYPE_INT);
    setup_tree_data(store);
    
    /* 将模型设置到TreeView */
    gtk_tree_view_set_model(GTK_TREE_VIEW(treeview), GTK_TREE_MODEL(store));
    g_object_unref(store); // 模型现在由treeview管理
    
    /* 添加列到TreeView */
    GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
    gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(treeview),
                                             -1, "Name", renderer,
                                             "text", 0, NULL);
    
    renderer = gtk_cell_renderer_text_new();
    gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(treeview),
                                             -1, "Quantity", renderer,
                                             "text", 1, NULL);
    
    /* 连接信号 */
    g_signal_connect(search_btn, "clicked", G_CALLBACK(on_search_clicked), data);
    g_signal_connect(entry, "activate", G_CALLBACK(on_search_clicked), data);
    g_signal_connect(next_btn, "clicked", G_CALLBACK(on_next_clicked), data);
    g_signal_connect(prev_btn, "clicked", G_CALLBACK(on_previous_clicked), data);
    
    /* 窗口关闭时清理数据 */
    g_signal_connect(window, "destroy", G_CALLBACK(clear_search_results), data);
    g_signal_connect(window, "destroy", G_CALLBACK(g_free), data);
    
    gtk_widget_show_all(window);
    gtk_main();
    
    return 0;
}

其中expand_parents展开所有父节点一开始deepseek提供的办法就是

GtkTreePath *parent_path = gtk_tree_path_copy(path);

while (gtk_tree_path_up(parent_path)) {
    gtk_tree_view_expand_row(treeview, parent_path, FALSE);
}
gtk_tree_path_free(parent_path);

测试这样在多层子节点时,并不能展开子节点的所有父节点。只能展开最顶层的父节点。

原因是gtk节点的展开顺序是从最上层到最下层子节点展开的。而deepseek提供的这个代码是直接从子节点的直接父节点一层一层向上展开的。无法展开。

所以我就改成了使用链表倒序储存节点路径,之后再遍历链表,这样就能够实现从上往下的展开了。但是我犯错,导致还是不能正常展开,因为我对于gtk的树状列表不够了解。

错误代码:

    GtkTreePath *parent_path = gtk_tree_path_copy(path);

    GList *pathList = NULL;
        while (gtk_tree_path_up(parent_path)) {
        pathList = g_list_prepend(pathList, parent_path);
        }
    //展开
    GList *currentPos = pathList;
    do{
        gtk_tree_view_expand_row(treeview, (GtkTreePath *)currentPos->data, FALSE);
        currentPos = currentPos->next;
    }while(currentPos->next!=NULL);
    
    g_list_free(pathList);
        gtk_tree_path_free(parent_path);

询问deepseek给我纠正

1.路径列表构建问题:

你在循环中使用 gtk_tree_path_up(parent_path) 修改了原始路径

但将所有修改后的同一路径指针多次添加到列表中(实际上添加的是同一个路径的不同状态)

2.展开循环问题:

循环条件 currentPos->next!=NULL 会导致漏掉最后一个节点

使用 do-while 循环不如 while 循环安全(如果 pathList 为 NULL 会崩溃)

3.内存管理问题:

你只释放了 pathList 和 parent_path,但没有释放列表中存储的路径

它给我改成了

GtkTreePath *parent_path = gtk_tree_path_copy(path);
GList *pathList = NULL;

// 构建路径列表(从最顶层到最接近的父节点)
while (gtk_tree_path_up(parent_path)) {
    // 每次都需要复制新的路径对象
    pathList = g_list_prepend(pathList, gtk_tree_path_copy(parent_path));
}

// 展开所有父节点(从最顶层到最接近的父节点)
GList *iter = pathList;
while (iter != NULL) {
    gtk_tree_view_expand_row(treeview, (GtkTreePath *)iter->data, FALSE);
    iter = iter->next;
}

// 释放资源
g_list_foreach(pathList, (GFunc)gtk_tree_path_free, NULL);
g_list_free(pathList);
gtk_tree_path_free(parent_path);

这样就成功运行了

效果

感谢deepseek!没有它我无法快速理解和创建gtk程序!gtk2.0时至今日,已经没有什么资料可以参考,只有《Foundations of GTK+ Development》,还有官方api参考文档。

但是问deepseek ai好像它什么都懂一样!

没有ai我也能作出来这些功能,但是会浪费大量时间在研究这些api用法上面。有了ai我直接作出来了。省的自己直接去写代码。