本文主要在立创实战派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);
}