立创实战派S3-文件管理器

本文主要在立创实战派S3上基于官方掌机实验的代码实现一个简易文件管理器,管理存储在SD卡中的文件。

1.创建文件管理器的主页面

文件管理器的主页面如下,主要由顶部的标签、按钮以及下方的列表部件组成。标签主要显示SD卡的存储信息,按钮用于返回掌机主页面,列表则主要展示当前文件夹中的文件。

创建UI界面的代码如下,在代码中主要是实现主界面的UI界面,并且为返回按钮设置事件响应函数file_manager_back_btn_cb以及标题栏的事件响应函数title_label_long_pressed_cb,最后创建了一个文件管理的任务task_file_manager。

bash 复制代码
void file_manager_event_handler(lv_event_t * e){
    lvgl_port_lock(0);
    //创建文件管理页面
    static lv_style_t style;
    lv_style_init(&style);
    lv_style_set_radius(&style, 10);  
    lv_style_set_bg_opa( &style, LV_OPA_COVER );
    lv_style_set_bg_color(&style, lv_color_hex(0xffffff));
    lv_style_set_border_width(&style, 0);
    lv_style_set_pad_all(&style, 0);
    lv_style_set_width(&style, 320);  
    lv_style_set_height(&style, 240); 

    file_manager_obj=lv_obj_create(lv_scr_act());
    lv_obj_add_style(file_manager_obj,&style,0);

    //创建标题背景
    file_manager_title=lv_obj_create(file_manager_obj);
    lv_obj_set_size(file_manager_title,320,40);
    lv_obj_set_style_pad_all(file_manager_title,0,0);
    lv_obj_align(file_manager_title,LV_ALIGN_TOP_LEFT,0,0);
    lv_obj_set_style_bg_color(file_manager_title,lv_color_hex(0x008b8b),0);
    //显示标题
    file_manager_title_label=lv_label_create(file_manager_title);
    lv_label_set_text(file_manager_title_label,"TF卡扫描中...");
    lv_obj_set_style_text_color(file_manager_title_label,lv_color_hex(0xffffff),0);
    lv_obj_set_style_text_font(file_manager_title_label,&font_alipuhui20,0);
    lv_obj_align(file_manager_title_label,LV_ALIGN_CENTER,0,0);
    lv_obj_add_event_cb(file_manager_title,title_label_long_pressed_cb,LV_EVENT_LONG_PRESSED,NULL);

    //创建返回按钮
    lv_obj_t* file_manager_back_btn=lv_btn_create(file_manager_title);
    lv_obj_align(file_manager_back_btn,LV_ALIGN_LEFT_MID,0,0);
    lv_obj_set_size(file_manager_back_btn,60,30);
    lv_obj_set_style_border_width(file_manager_back_btn,0,0);
    lv_obj_set_style_pad_all(file_manager_back_btn,0,0);
    lv_obj_set_style_bg_opa(file_manager_back_btn,LV_OPA_TRANSP,LV_PART_MAIN);
    lv_obj_add_event_cb(file_manager_back_btn,file_manager_back_btn_cb,LV_EVENT_CLICKED,NULL);
    lv_obj_t* file_manager_back_btn_label=lv_label_create(file_manager_back_btn);
    lv_label_set_text(file_manager_back_btn_label,LV_SYMBOL_LEFT);
    lv_obj_set_style_text_font(file_manager_back_btn_label,&lv_font_montserrat_20,0);
    lv_obj_set_style_text_color(file_manager_back_btn_label,lv_color_hex(0xffffff),0);
    lv_obj_align(file_manager_back_btn_label,LV_ALIGN_CENTER,-10,0);

    //创建文件列表
    file_manager_file_list=lv_list_create(file_manager_obj);
    lv_obj_set_size(file_manager_file_list,320,200);
    lv_obj_align(file_manager_file_list,LV_ALIGN_TOP_LEFT,0,40);
    lv_obj_set_style_border_width(file_manager_file_list,0,0);
    lv_obj_set_style_text_font(file_manager_file_list,&font_alipuhui20,0);
    lv_obj_set_scrollbar_mode(file_manager_file_list,LV_SCROLLBAR_MODE_OFF);

    lvgl_port_unlock();
    icon_flag=2;//标记已经进入第2个应用

    xTaskCreatePinnedToCore(task_file_manager, "task_file_manager", 3 * 1024, NULL, 5, NULL, 1);

}

2.文件管理任务task_file_manager

文件管理任务中计算SD卡的存储信息并且列出当前文件夹的文件。计算SD卡存储信息主要通过f_getfree()函数获取FATFS卷的空闲簇数量和文件系统信息。

在FATFS中,扇区(sector) 是SD卡的最小物理读写单位**;簇(cluster)**是文件系统的最小分配单位,由多个扇区组成。根据下面的公式计算SD卡的存储信息

|------|------------------------------------------------|
| 指标 | 公式 |
| 总簇数 | fs->n_fatent - 2(FAT 表项总数减 2,前 2 个表项为保留项) |
| 总扇区数 | 总簇数 * fs->csize(fs->csize 是每簇的扇区数) |
| 总容量 | 总扇区数 * fs->ssize(fs->ssize 是扇区大小,通常 512 字节) |
| 空闲容量 | 空闲簇数 * fs->csize * fs->ssize |
| 已用容量 | 总容量 - 空闲容量 |
| 使用率 | (已用容量 / 总容量) * 100 |

在列出当前文件夹的全部文件中,使用了file_path_info类型变量,这是一个结构体类型,定义如下,主要是用来在浏览文件的过程中记录相关路径,方便回退时及时更新列表显示的文件夹文件。

bash 复制代码
struct file_path_info
{
    uint8_t path_index;  // 在第几级目录
    char path_now[512]; // 当前文件路径
    char path_back[512]; // 上级文件路径
};
bash 复制代码
static void task_file_manager(void* arg){
    esp_err_t ret;
    lvgl_port_lock(0);
    sdmmc_card_print_info(stdout,sdmmc_card);//终端显示SD卡信息
    // 计算SD 卡(FATFS 文件系统)的已用空间和使用率
    FATFS *fs;          // FATFS文件系统对象指针
    DWORD free_clusters;// 空闲簇数量
    uint64_t total_sectors, free_sectors;
    uint64_t total_bytes, free_bytes, used_bytes;
    float usage_percent;

    // 1. 调用FATFS函数获取簇信息
    FRESULT fres = f_getfree(MOUNT_POINT, &free_clusters, &fs);
    if (fres == FR_OK) {
        // 2. 计算总扇区数和空闲扇区数
        // 注意:fs->n_fatent 是FAT表项总数,减2才是数据区总簇数
        total_sectors = (uint64_t)(fs->n_fatent - 2) * fs->csize; 
        free_sectors = (uint64_t)free_clusters * fs->csize;
        // 3. 计算总字节数、空闲字节数、已用字节数
        // fs->ssize 是扇区大小(通常512字节)
        total_bytes = total_sectors * fs->ssize;
        free_bytes = free_sectors * fs->ssize;
        used_bytes = total_bytes - free_bytes;

        // 4. 计算使用率(百分比)
        usage_percent = ((float)used_bytes / total_bytes) * 100;

       // 5. 标题栏显示:总容量(GB) + 已用容量(GB) + 使用率(%)
       // 字节转GB:>> 30 (即除以2^30)
       uint64_t total_gb = total_bytes >> 30;
       uint64_t used_gb = used_bytes >> 30;

       // 计算使用率(放大10倍,变成整数)
       int usage_percent_x10 = (int)((usage_percent) * 10); 

       lv_label_set_text_fmt(file_manager_title_label, 
                             "SD:%lluGB 已用:%lluGB (%d.%d%%)", 
                             total_gb, 
                             used_gb, 
                             usage_percent_x10 / 10,  // 整数部分
                             usage_percent_x10 % 10   // 小数部分
        );
    } 
    else {
        // 获取失败时的备用显示
        lv_label_set_text(file_manager_title_label,"获取空间信息失败");
    }
    lvgl_port_unlock();
    //列出SD卡中的文件
    file_path_info.path_index=0;//当前在跟目录
    strcpy(file_path_info.path_now,MOUNT_POINT);//装入当前路径
    list_sdcard_files(file_path_info.path_now);//列出当前目录文件

    vTaskDelete(NULL);
}

3.列出当前目录下的全部文件list_sdcard_files

在list_sdcard_files函数中,通过opendir(path)打开当前的目录,再通过readdir(dir)依次读取目录中的每个文件,然后通过ent->d_type判断是常规文件还是目录文件。对于常规文件,找到文件的后缀名,并且根据后缀名判断文件类型列举在列表中并添加图标。对于目录文件,同样列举在列表中并添加图标。同时,添加了点击和长按事件的响应函数。对于常规文件,如果是JPG图片文件点击的响应函数会显示这个图片,其他文件则会提示不支持查看。对于文件夹,点击则会进入这个文件夹,显示文件夹中的所有文件。对于常规文件和文件夹来说,长按都是删除文件,会提示是否确定删除。

bash 复制代码
esp_err_t list_sdcard_files(char* path){
    esp_err_t ret;
    DIR* dir;
    struct dirent *ent;
    lv_obj_t * btn;
    if ((dir = opendir(path))!= NULL) { // 打开目录
        while ((ent = readdir(dir))!= NULL) { // 读取目录中的文件
            /* 常规文件处理 */
            if (ent->d_type == DT_REG){ // 如果是常规文件
                int file_type_flag = 0;
                const char* extension = strrchr(ent->d_name, '.'); // 从后往前 找到字符'.'
                if (extension != NULL) { // 如果找到了'.'
                    extension++; // 指针地址+1
                    if (strcmp(extension, "mp3") == 0) { // 如果是mp3
                        file_type_flag = 1; // 标记为音乐文件
                    } else if (strcmp(extension, "wav") == 0) { // 如果是wav
                        file_type_flag = 1; // 标记为音乐文件
                    } else if (strcmp(extension, "mp4") == 0) {
                        file_type_flag = 2; // 标记为视频文件
                    } else if (strcmp(extension, "avi") == 0) {
                        file_type_flag = 2; // 标记为视频文件
                    } else if (strcmp(extension, "jpg") == 0) {
                        file_type_flag = 3; // 标记为图片
                    } else if (strcmp(extension, "jpeg") == 0) {
                        file_type_flag = 3; // 标记为图片
                    } else if (strcmp(extension, "png") == 0) {
                        file_type_flag = 3; // 标记为图片
                    } else if (strcmp(extension, "bmp") == 0) {
                        file_type_flag = 3; // 标记为图片
                    } else if (strcmp(extension, "gif") == 0) {
                        file_type_flag = 3; // 标记为图片
                    } else {
                        file_type_flag = 0; // 除了以上文件 其它文件都归类为普通文件
                    }
                }
                lvgl_port_lock(0);
                switch (file_type_flag)
                {
                case 1:
                    btn = lv_list_add_btn(file_manager_file_list, LV_SYMBOL_AUDIO, (const char *)ent->d_name);  // 显示音乐文件图标
                    break;
                case 2:
                    btn = lv_list_add_btn(file_manager_file_list, LV_SYMBOL_VIDEO, (const char *)ent->d_name);  // 显示视频文件图标
                    break;
                case 3:
                    btn = lv_list_add_btn(file_manager_file_list, LV_SYMBOL_IMAGE, (const char *)ent->d_name);  // 显示图片文件图标
                    break;
                default:
                    btn = lv_list_add_btn(file_manager_file_list, LV_SYMBOL_FILE, (const char *)ent->d_name);  // 显示普通文件图标
                    break;
                }
                lv_obj_t *icon = lv_obj_get_child(btn, 0); // 获取图标指针
                lv_obj_set_style_text_font(icon, &lv_font_montserrat_20, 0); // 修改图标的字体    
                lv_obj_add_event_cb(btn, file_list_btn_cb, LV_EVENT_CLICKED, NULL);  // 添加按下回调函数
                lv_obj_add_event_cb(btn, file_list_btn_long_pressed_cb, LV_EVENT_LONG_PRESSED, (void*) 0);  // 添加长按回调函数
                
                lvgl_port_unlock();
            }
            /* 文件夹处理 */
            else if (ent->d_type == DT_DIR) { // 如果是文件夹
                lvgl_port_lock(0);
                btn = lv_list_add_btn(file_manager_file_list, LV_SYMBOL_DIRECTORY, (const char *)ent->d_name); 
                lv_obj_t *icon = lv_obj_get_child(btn, 0); // 获取图标指针
                lv_obj_set_style_text_font(icon, &lv_font_montserrat_20, 0); // 修改图标的字体
                lv_obj_add_event_cb(btn, file_list_btn_cb, LV_EVENT_CLICKED, NULL); // 添加点击回调函数
                lv_obj_add_event_cb(btn, file_list_btn_long_pressed_cb, LV_EVENT_LONG_PRESSED,(void*) 1);  // 添加长按回调函数
                lvgl_port_unlock();
            }
        }
        closedir(dir);
        ret = ESP_OK;
    } else {
        ESP_LOGE(TAG, "Failed to open directory %s.", path);
        ret = ESP_FAIL;
    }
    return ret;
}

4.文件点击

在文件点击事件响应函数中,首先会判断is_file_long_pressed变量,该变量用来区别长按与点击。随后获取当前文件完整的路径,并保存上一级目录,方便回退。利用stat(file_path_info.path_now, &st)获取文件信息,再判断是普通文件还是文件夹。对于文件夹,则清空当前文件列表,重新调用list_sdcard_files函数列出当前目录下的所有文件。对于普通文件,则根据文件后缀名判断文件类型,非JPG图片类型的文件会弹出提示,显示不支持查看当前文件。对于JPG图片文件,则会调用show_jpeg函数显示。

复制代码
static void file_list_btn_cb(lv_event_t * e)
{
    if(is_file_long_pressed==true)
        return ;
    const char *file_name = NULL; // 当前文件名称
    // 获取点击的按钮名称 即文件名称
    file_name = lv_list_get_btn_text(lv_obj_get_parent(e->target), e->target);
    ESP_LOGI(TAG, "file name: %s", file_name);
    // 列出 SD 卡中的文件
    struct stat st; // 获取文件状态信息结构体
    strcpy(file_path_info.path_back, file_path_info.path_now); // 保存上一级目录
    strcat(file_path_info.path_now, "/");//在当前路径后面加一个斜杠 "/"
    strcat(file_path_info.path_now, file_name);//把刚才获取的文件名拼在后面
    if (stat(file_path_info.path_now, &st) == 0){ // 如果成功获取到状态信息
        if (S_ISDIR(st.st_mode)){ // 如果是目录
            lv_obj_clean(file_manager_file_list); // 清除当前文件列表
            esp_err_t ret = list_sdcard_files(file_path_info.path_now);
            if (ret == ESP_OK){ // 如果成功列出了目录
                file_path_info.path_index++; // 表示进入到下一集目录
                ESP_LOGI(TAG, "path_index: %d", file_path_info.path_index);
                ESP_LOGI(TAG, "path_now: %s", file_path_info.path_now);
                ESP_LOGI(TAG, "path_back: %s", file_path_info.path_back);
            }
            return;
        }
        else if(S_ISREG(st.st_mode)){//如果是普通文件
            const char* file_type=strrchr(file_name,'.');// 从后往前 找到字符'.'
            if(file_type!=NULL){
                file_type++;
                if(strcmp(file_type,"JPG")==0){
                    //文件是JPEG文件,将JPEG图片显示
                    show_jpeg(file_path_info.path_now,file_name);

                }
                else{
                    //文件不是JPEG图片文件,弹出消息提示
                    lvgl_port_lock(0);
                    //创建消息框
                    static const char *btns[] = {"" };
                    lv_obj_t *msgbox = lv_msgbox_create(lv_scr_act(), "提示", "暂不支持查看该文件",btns,false);
                    lv_obj_center(msgbox);
                    lv_obj_set_style_bg_color(msgbox,lv_color_hex(0xD3DEF6),0);
                    lv_obj_set_style_text_color(msgbox,lv_color_hex(0xffffff),0);
                    lv_obj_set_style_text_font(msgbox,&font_alipuhui20,0);
                    lv_obj_del_delayed(msgbox,1000);//延时1000ms后删除
                    lvgl_port_unlock();
                }
            }
        }
    }
    // 如果没有成功进入目录
    strcpy(file_path_info.path_now, file_path_info.path_back); // 没有列出新的列表 还原当前路径
    // 还原再向下退一级的目录路径
    char *slash = strrchr(file_path_info.path_back, '/'); // 从后往前查找字符'/'
    if (slash!= NULL) { // 如果查找到
        *slash = '\0'; // 替换为NULL 表示字符串结束
    }
    ESP_LOGI(TAG, "path_index: %d", file_path_info.path_index);
    ESP_LOGI(TAG, "path_now: %s", file_path_info.path_now);
    ESP_LOGI(TAG, "path_back: %s", file_path_info.path_back);
}

show_jpeg函数主要是创建显示图片的页面,并且调用jpeg_img_decode函数对当前的JPG图片解码显示。jpeg_img_decode函数中首先从SD卡中读取图片数据,随后使用ESP-New-JPEG对读取的数据解码成RGB565格式用于展示。

复制代码
static void show_jpeg(char* file_path,char* file_name){
    lvgl_port_lock(0);
    lv_obj_t* img_obj=lv_obj_create(lv_layer_top());
    lv_obj_clear_flag(img_obj, LV_OBJ_FLAG_SCROLLABLE);  // 禁用滚动条
    lv_obj_set_size(img_obj,320,240);
    lv_obj_set_style_pad_all(img_obj,0,0);
    lv_obj_t* img=lv_img_create(img_obj);
    lv_obj_set_size(img,320,240);
    lv_obj_set_style_pad_all(img,0,0);
    lv_obj_t* img_title=lv_obj_create(img_obj);
    lv_obj_set_size(img_title,320,40);
    lv_obj_set_style_bg_color(img_title,lv_color_hex(0x008b8b),0);
    lv_obj_set_style_bg_opa(img_title,LV_OPA_TRANSP,0);
    lv_obj_set_style_pad_all(img_title,0,0);
    //显示图片文件名
    lv_obj_t* img_title_label=lv_label_create(img_title);
    lv_obj_set_style_bg_opa(img_title_label,LV_OPA_TRANSP,0);
    lv_obj_align(img_title_label,LV_ALIGN_CENTER,0,0);
    lv_obj_set_style_text_font(img_title_label,&lv_font_montserrat_20,0);
    lv_obj_set_style_text_color(img_title_label,lv_color_hex(0xffffff),0);
    lv_label_set_text(img_title_label,"photos");

    //关闭按钮
    lv_obj_t* close_btn=lv_btn_create(img_title);
    lv_obj_set_size(close_btn,30,30);
    lv_obj_align(close_btn,LV_ALIGN_LEFT_MID,0,0);
    lv_obj_set_style_border_width(close_btn,0,0);
    lv_obj_set_style_bg_opa(close_btn,LV_OPA_TRANSP,LV_PART_MAIN);
    lv_obj_add_event_cb(close_btn,show_img_close_btn_cb,LV_EVENT_CLICKED,img_obj);
    lv_obj_t* close_btn_label=lv_label_create(close_btn);
    lv_label_set_text(close_btn_label,LV_SYMBOL_CLOSE);
    lv_obj_set_style_text_font(close_btn_label,&lv_font_montserrat_20,0);
    lv_obj_set_style_text_color(close_btn_label,lv_color_hex(0xffffff),0);
    lv_obj_align(close_btn_label,LV_ALIGN_CENTER,0,0);

    //将JPEG照片解码显示
    uint16_t *rgb565_buf = NULL;
    int width = 0, height = 0;
    lv_img_dsc_t *s_img_dsc = malloc(sizeof(lv_img_dsc_t));
    memset(s_img_dsc, 0, sizeof(lv_img_dsc_t));
    if (jpeg_img_decode(file_path, &rgb565_buf, &width, &height) != ESP_OK) {
        ESP_LOGE("读取JPEG图片","读取失败");
        lvgl_port_unlock();
        return;
    }
    //配置LVGL图像描述符
    s_img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR; // RGB565对应此格式
    s_img_dsc->header.w = width;
    s_img_dsc->header.h = height;
    s_img_dsc->data = (const uint8_t *)rgb565_buf;
    s_img_dsc->data_size = width * height * 2;

    lv_img_set_src(img,s_img_dsc);
    lv_obj_set_user_data(img_obj, s_img_dsc); //存入用户数据
    lvgl_port_unlock();
}

esp_err_t jpeg_img_decode(char* file_path,uint16_t **out_rgb565, int *out_width, int *out_height){
    FILE* f=fopen(file_path,"rb");
    if(!f){
        ESP_LOGE("读取JPEG图片","读取失败:%s",file_path);
        return ESP_FAIL;
    }
    //获取文件大小
    size_t jpeg_img_size;
    fseek(f,0,SEEK_END);
    jpeg_img_size=ftell(f);
    fseek(f,0,SEEK_SET);
    //分配内存
    uint8_t *jpeg_buff=NULL;
    jpeg_buff=malloc(jpeg_img_size);
    if(!jpeg_buff){
        ESP_LOGE("读取JPEG图片","分配内存失败");
        fclose(f);
        return ESP_FAIL;
    }
    //读取图片到内存
    size_t read_len=fread(jpeg_buff,1,jpeg_img_size,f);
    fclose(f);
    if(read_len!=jpeg_img_size){
        ESP_LOGE("读取JPEG图片","读取数据不完整");
        free(jpeg_buff);
        return ESP_FAIL;
    }
    else{
        ESP_LOGI("读取JPEG图片","读取数据到内存");
    }
    //对读取到的数据进行JPEG解码
    //初始化解码器配置
    jpeg_dec_config_t config=DEFAULT_JPEG_DEC_CONFIG();
    config.output_type=JPEG_PIXEL_FORMAT_RGB565_BE;
    jpeg_dec_handle_t jpeg_dec=NULL;
    esp_err_t ret=jpeg_dec_open(&config,&jpeg_dec);
    if(ret!=ESP_OK){
        ESP_LOGE("读取JPEG图片","初始化解码器失败");
        free(jpeg_buff);
        return ret;
    }
    //分配IO和头信息结构体
    jpeg_dec_io_t *jpeg_io=calloc(1,sizeof(jpeg_dec_io_t));
    jpeg_dec_header_info_t *out_info=calloc(1,sizeof(jpeg_dec_header_info_t));
    if(!jpeg_io || !out_info){
        jpeg_dec_close(jpeg_dec);
        free(jpeg_buff);
        free(out_info);
        free(jpeg_io);
        ESP_LOGE("读取JPEG图片","分配IO和头信息结构体失败");
        return ESP_FAIL;
    }
    //绑定输入缓冲区(SD卡读取的内存)
    jpeg_io->inbuf=jpeg_buff;
    jpeg_io->inbuf_len=jpeg_img_size;
    //解析JPEG头
    ret=jpeg_dec_parse_header(jpeg_dec,jpeg_io,out_info);
    if(ret!=ESP_OK){
        jpeg_dec_close(jpeg_dec);
        free(jpeg_buff);
        free(out_info);
        free(jpeg_io);
        ESP_LOGE("读取JPEG图片","解析JPEG头失败");
        return ret;
    }
    //分配16字节对齐的输出缓冲区
    int outbuf_len=out_info->width*out_info->height*2;
    uint8_t *rgb565_buf=jpeg_calloc_align(outbuf_len,16);
    if(!rgb565_buf){
        jpeg_dec_close(jpeg_dec);
        free(jpeg_buff);
        free(out_info);
        free(jpeg_io);
        ESP_LOGE("读取JPEG图片","分配输出缓冲区失败");
        return ESP_FAIL;
    }
    jpeg_io->outbuf=rgb565_buf;
    //更新输入指针
    int consumed=jpeg_io->inbuf_len-jpeg_io->inbuf_remain;
    jpeg_io->inbuf=jpeg_buff+consumed;
    jpeg_io->inbuf_len=jpeg_io->inbuf_remain;
    //执行解码
    ret=jpeg_dec_process(jpeg_dec,jpeg_io);
    if(ret!=ESP_OK){
        jpeg_free_align(rgb565_buf);
        jpeg_dec_close(jpeg_dec);
        free(jpeg_buff);
        free(out_info);
        free(jpeg_io);
        ESP_LOGE("读取JPEG图片","解码失败");
        return ret;
    }
    *out_rgb565=(uint16_t*)rgb565_buf;
    *out_width = out_info->width;
    *out_height = out_info->height;

    jpeg_dec_close(jpeg_dec);
    free(jpeg_buff);
    free(out_info);
    free(jpeg_io);
    ESP_LOGI("读取JPEG图片","解码成功");
    return ret;
}

5.文件长按,删除

文件长按主要是删除文件。长按后弹出一个确认页面,是否删除文件。按下确定按钮的事件响应函数内就是执行删除操作。文件长按事件响应函数file_list_btn_long_pressed_cb中使用了一个结构体变量dialog_data传递用户数据给按下确定按钮的事件响应函数long_pressed_btn_cb,结构体定义如下:其中lv_obj_t *dialog用来传递确认页面,方便删除,file_path是要删除的文件的路径,is_file_dir用来判断要删除的是普通文件还是文件夹。对于普通文件,long_pressed_btn_cb函数直接使用unlink(dialog_data->file_path)来删除文件,再清空文件列表,重新展示当前目录的所有文件。对于文件夹,由于rmdir只能删除空文件夹,所以调用了delete_nonempty_dir函数来删除非空文件夹。delete_nonempty_dir函数主要通过递归删除文件夹中的所有文件和子文件夹。

复制代码
typedef struct {
    lv_obj_t *dialog;       // 弹窗对象指针,用于关闭
    char      file_path[512]; // 完整的文件路径,用于删除
    int is_file_dir;//判断要删除的是否是文件夹
} delete_dialog_data_t;

static void file_list_btn_long_pressed_cb(lv_event_t* e){
    int is_file_dir=(int)lv_event_get_user_data(e);//判断是否是文件夹长按,1表示是文件夹长按

    char *file_name = NULL; // 当前文件名称
    // 获取点击的按钮名称 即文件名称
    file_name = lv_list_get_btn_text(lv_obj_get_parent(e->target), e->target);

     //拼接完整的文件路径 (path_now + "/" + file_name)
    char full_path[512];
    int ret = snprintf(full_path, sizeof(full_path), "%s/%s", file_path_info.path_now, file_name);

    // 检查是否发生截断或错误
    if (ret < 0 || ret >= sizeof(full_path)) {
        ESP_LOGE(TAG, "文件路径过长,无法删除");
        return;
    }


    is_file_long_pressed = true; 
    lv_obj_t* long_pressed_obj=lv_obj_create(lv_scr_act());
    lv_obj_set_style_pad_all(long_pressed_obj,0,0);
    lv_obj_clear_flag(long_pressed_obj,LV_SCROLLBAR_MODE_ON);
    lv_obj_set_size(long_pressed_obj,220,200);
    lv_obj_center(long_pressed_obj);

    lv_obj_t* label=lv_label_create(long_pressed_obj);
    lv_obj_set_style_radius(label,10,0);
    lv_obj_set_style_bg_color(label,lv_color_hex(0x24ABF2),0);
    lv_obj_set_style_bg_opa(label,LV_OPA_COVER,0);
    lv_obj_set_style_text_font(label,&font_alipuhui20,0);
    lv_label_set_text(label,"确定要删除该文件吗?");
    lv_obj_align(label,LV_ALIGN_TOP_MID,0,30);

    delete_dialog_data_t *dialog_data = malloc(sizeof(delete_dialog_data_t));
    dialog_data->dialog = long_pressed_obj;
    strncpy(dialog_data->file_path, full_path, sizeof(dialog_data->file_path) - 1);
    dialog_data->file_path[sizeof(dialog_data->file_path) - 1] = '\0'; // 确保结束
    if(is_file_dir==1){
        dialog_data->is_file_dir=1;//文件夹长按
    }
    else{
        dialog_data->is_file_dir=0;//文件长按
    }

    lv_obj_t* cancel_btn=lv_btn_create(long_pressed_obj);
    lv_obj_set_user_data(cancel_btn,dialog_data);
    lv_obj_add_event_cb(cancel_btn,long_pressed_btn_cb,LV_EVENT_CLICKED,(void*)0);
    lv_obj_align(cancel_btn,LV_ALIGN_BOTTOM_MID,-50,-30);
    lv_obj_t* cancel_btn_label=lv_label_create(cancel_btn);
    lv_obj_center(cancel_btn_label);
    lv_obj_set_style_text_font(cancel_btn_label,&font_alipuhui20,0);
    lv_label_set_text(cancel_btn_label,"取消");

    lv_obj_t* confirm_btn=lv_btn_create(long_pressed_obj);
    lv_obj_set_user_data(confirm_btn,dialog_data);
    lv_obj_add_event_cb(confirm_btn,long_pressed_btn_cb,LV_EVENT_CLICKED,(void*)1);
    lv_obj_align(confirm_btn,LV_ALIGN_BOTTOM_MID,50,-30);
    lv_obj_t* confirm_btn_label=lv_label_create(confirm_btn);
    lv_obj_center(confirm_btn_label);
    lv_obj_set_style_text_font(confirm_btn_label,&font_alipuhui20,0);
    lv_label_set_text(confirm_btn_label,"确定");
}

static void long_pressed_btn_cb(lv_event_t* e){
    is_file_long_pressed=false;
    lv_obj_t* btn=lv_event_get_target(e);

    void* is_confirm = lv_event_get_user_data(e);
    
    // 2. 获取我们之前绑定的数据结构体
    delete_dialog_data_t *dialog_data = (delete_dialog_data_t*)lv_obj_get_user_data(btn);
    lv_obj_t* obj=dialog_data->dialog;
    if(is_confirm==1){
        //确认按钮按下
        ESP_LOGI(TAG,"当前要删除的文件的路径为:%s",dialog_data->file_path);
        if(dialog_data->is_file_dir==0){
            //文件长按
            unlink(dialog_data->file_path);//删除文件
            lv_obj_clean(file_manager_file_list); //先清空列表
            list_sdcard_files(file_path_info.path_now);//刷新显示文件夹的内容
        }
        else{
            //文件夹长按,删除文件夹
            delete_nonempty_dir(dialog_data->file_path);
            lv_obj_clean(file_manager_file_list); //先清空列表
            list_sdcard_files(file_path_info.path_now);//刷新显示文件夹的内容
        }
    }
    lv_obj_del(obj);
    free(dialog_data);
}

static void delete_nonempty_dir(const char *path) {
    esp_err_t ret;
    DIR* dir;
    struct dirent *ent;
    char full_path[512];
    ESP_LOGI(TAG, "正在删除的文件夹为: %s", path);

    if ((dir = opendir(path))!= NULL) {//尝试打开目录
        while ((ent=readdir(dir))!=NULL) { // 读取目录中的文件
            // 跳过 "." 和 ".." 目录
            if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
                continue;
            }
            char file_path[512];//文件的完整路径:文件夹路径path+文件名称ent->d_name
            int ret=snprintf(file_path,sizeof(file_path),"%s/%s",path,ent->d_name);
            if (ret < 0 || ret >= sizeof(full_path)) {
                ESP_LOGW(TAG, "路径过长,跳过: %s/%s", path, ent->d_name);
                continue;
            }
            if(ent->d_type==DT_REG){//常规文件
                unlink(file_path);//删除文件
            }
            else if(ent->d_type==DT_DIR){//文件夹文件
               delete_nonempty_dir(file_path);//递归删除
            }
        }
    }
    closedir(dir);
    rmdir(path);
}

6.创建新文件夹

长按文件管理器页面的标题栏,就会弹出确定新建文件夹的页面。使用mkdir在当前文件夹下创建一个名为MY_PHOTO的文件夹。

复制代码
static void create_dir_btn_cb(lv_event_t* e){
    lv_obj_t* btn=lv_event_get_target(e);
    lv_obj_t* obj=lv_obj_get_user_data(btn);
    int num=(int)lv_event_get_user_data(e);

    char *dir_name = NULL; // 文件夹名称
    dir_name = "MY_PHOTO";

     //拼接完整的文件路径 (path_now + "/" + dir_name)
    char full_path[512];
    int ret = snprintf(full_path, sizeof(full_path), "%s/%s", file_path_info.path_now, dir_name);

    // 检查是否发生截断或错误
    if (ret < 0 || ret >= sizeof(full_path)) {
        ESP_LOGE(TAG, "文件路径过长,无法创建文件夹");
        return;
    }

    if(num==0){
        errno = 0;
        int fres = mkdir(full_path,0);
        // 如果创建成功 OR 文件夹已经存在,都去刷新列表
        if(fres == 0){ 
            lvgl_port_lock(0);
            lv_obj_clean(file_manager_file_list);
            list_sdcard_files(file_path_info.path_now);
            lvgl_port_unlock();
        } else {
            // 只有真正的错误(如磁盘满、路径错误)才报错
            ESP_LOGE(TAG,"创建文件夹失败! 路径: %s, FATFS 错误码: %d", full_path, errno);
        }
    }
    lv_obj_del(obj);
}

//标题标签长按事件响应函数:创建文件夹
static void title_label_long_pressed_cb(lv_event_t* e){
    lv_obj_t* create_dir=lv_obj_create(lv_scr_act());
    lv_obj_set_size(create_dir,220,140);
    lv_obj_center(create_dir);
    lv_obj_set_style_pad_all(create_dir,0,0);
    lv_obj_t* title_label=lv_label_create(create_dir);
    lv_obj_align(title_label,LV_ALIGN_TOP_MID,0,10);
    lv_obj_set_style_text_font(title_label,&font_alipuhui20,0);
    lv_label_set_text(title_label,"确定要创建文件夹吗?");
    lv_obj_t* create_dir_confirm_btn=lv_btn_create(create_dir);
    lv_obj_set_style_bg_color(create_dir_confirm_btn,lv_color_hex(0x1592DB),0);
    lv_obj_align(create_dir_confirm_btn,LV_ALIGN_CENTER,50,0);
    lv_obj_set_user_data(create_dir_confirm_btn,create_dir);
    lv_obj_t* confirm_label=lv_label_create(create_dir_confirm_btn);
    lv_obj_set_style_text_font(confirm_label,&font_alipuhui20,0);
    lv_label_set_text(confirm_label,"确定");
    lv_obj_add_event_cb(create_dir_confirm_btn,create_dir_btn_cb,LV_EVENT_CLICKED,(void*)0);

    lv_obj_t* create_dir_cancel_btn=lv_btn_create(create_dir);
    lv_obj_set_style_bg_color(create_dir_cancel_btn,lv_color_hex(0x1592DB),0);
    lv_obj_align(create_dir_cancel_btn,LV_ALIGN_CENTER,-50,0);
    lv_obj_set_user_data(create_dir_cancel_btn,create_dir);
    lv_obj_t* cancel_label=lv_label_create(create_dir_cancel_btn);
    lv_obj_set_style_text_font(cancel_label,&font_alipuhui20,0);
    lv_label_set_text(cancel_label,"取消");
    lv_obj_add_event_cb(create_dir_cancel_btn,create_dir_btn_cb,LV_EVENT_CLICKED,(void*)1);
}
相关推荐
π同学1 天前
ESP-IDF+vscode开发ESP32第六讲——SPI
vscode·esp32·spi
sz4972385993 天前
WSL2+VSCode搭建ESP-IDF 开发环境
ide·vscode·编辑器·esp32·wsl
deepwater_zone7 天前
ESP32芯片对比
esp32
乐鑫科技 Espressif10 天前
使用 MCP 服务器,把乐鑫文档接入 AI 工作流
人工智能·ai·esp32·乐鑫科技
飞睿科技10 天前
ESP32-S31 高性能 AIoT SoC 在智能音频领域的应用实践
音视频·esp32·智能家居·乐鑫科技·ai智能
我叫洋洋12 天前
[ESP32-S3 点亮灯]
单片机·嵌入式硬件·esp32
i-阿松!12 天前
ESP32-PCB已经通了
物联网·flutter·esp32·go语言
星野云联AIoT技术洞察15 天前
ESP32-C3、ESP32-S3、ESP32-C6 应该怎么选:面向定制固件项目的芯片判断
esp32·乐鑫·esp32-s3·matter·esp32-c3·esp32-c6·wi-fi 6
乐鑫科技 Espressif15 天前
乐鑫联合 Bosch Sensortec(博世传感器)推出磁感应交互方案
esp32·交互·乐鑫科技·博世·c磁感应·交互方案