前言
上一篇我们已经实现了 RK3399 开发板的 JPEG 图片显示和 BMP 转 JPEG 功能,但有个明显的不足:图片路径需要手动写死在代码里,如果要新增或替换图片,就得修改代码重新编译,非常不方便。
这篇文章我们升级功能:通过目录文件操作 API自动扫描指定文件夹下的所有图片,用链表动态存储图片路径(解决数组固定大小的局限性),最终实现 “一键启动、自动加载所有图片、循环播放” 的完整电子相册。
整个过程由浅入深,从 API 讲解到链表实现,再到项目整合,新手也能一步步跟着做,而且项目亮点十足,非常适合写进简历,应对面试官的深入提问~
一、核心知识点铺垫:为什么需要这两个技术?
在整合项目前,先搞懂两个核心技术的作用:
| 技术 | 解决的问题 | 优势 |
|---|---|---|
| 目录文件操作 API | 自动获取指定文件夹下的所有图片文件名 | 不用手动写路径,新增图片直接放文件夹即可 |
| 链表存储 | 动态存储不确定数量的图片路径 | 图片数量可多可少,不浪费内存(数组做不到) |
举个例子:如果用数组存储图片路径,假设数组大小是 10,最多只能存 10 张图;但用链表,不管是 5 张还是 20 张,都能动态扩展存储,灵活性拉满~
二、目录文件操作 API 详解(自动扫图核心)
要实现 “自动找图片”,需要用到 Linux 系统的 3 个核心 API:opendir(打开目录)、readdir(读取文件)、closedir(关闭目录)。
2.1 核心 API 速查表
| API 函数 | 头文件 | 函数原型 | 核心功能 | 参数说明 | 返回值说明 |
|---|---|---|---|---|---|
opendir | <sys/types.h>+<dirent.h> | DIR *opendir(const char *name) | 打开指定目录 | name:目录路径(如/root/photos) | 成功:返回目录指针(DIR*);失败:NULL |
readdir | <dirent.h> | struct dirent *readdir(DIR *dirp) | 读取目录下一个文件的信息 | dirp:opendir返回的目录指针 | 成功:返回文件信息结构体;失败 / 读完:NULL |
closedir | <dirent.h> | int closedir(DIR *dirp) | 关闭目录,释放资源 | dirp:opendir返回的目录指针 | 成功:0;失败:-1 |
2.2 关键结构体:struct dirent(存储文件信息)
readdir返回的struct dirent结构体,包含了文件的核心信息,我们只需要关注两个字段:
c
运行
struct dirent {
unsigned char d_type; // 文件类型(重点!)
char d_name[256]; // 文件名(重点!如1.jpg、2.bmp)
};
重点字段说明:
d_type:文件类型,用来过滤图片文件(避免把文件夹、设备文件当成图片),常用取值:DT_REG:普通文件(我们要的图片文件就是这种);DT_DIR:目录文件(跳过,不处理);DT_LNK:符号链接(跳过)。
d_name:存储文件名(含后缀),比如"test.jpg"、"background.bmp"。
2.3 示例 1:获取指定目录下的所有图片文件名
下面代码实现:打开/root/photos目录,自动过滤出.jpg、.jpeg、.bmp格式的图片,打印文件名。
c
运行
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
// 图片格式过滤函数:判断文件名是否为图片
int is_image(const char *filename) {
// 支持的图片后缀(可扩展,如添加.png)
const char *suffixes[] = {".jpg", ".jpeg", ".bmp", ".JPG", ".BMP"};
int suffix_num = sizeof(suffixes) / sizeof(suffixes[0]);
// 遍历所有后缀,判断文件名是否以其中一个结尾
for (int i = 0; i < suffix_num; i++) {
if (strstr(filename, suffixes[i]) != NULL) {
return 1; // 是图片
}
}
return 0; // 不是图片
}
int main() {
DIR *dirp;
struct dirent *file_info;
const char *photo_dir = "/root/photos"; // 图片文件夹路径
// 1. 打开目录
dirp = opendir(photo_dir);
if (dirp == NULL) {
perror("opendir failed");
return -1;
}
printf("成功打开目录:%s\n", photo_dir);
// 2. 循环读取目录下的所有文件
while ((file_info = readdir(dirp)) != NULL) {
// 跳过目录文件(.和..是当前目录和上级目录,也跳过)
if (file_info->d_type != DT_REG) {
continue;
}
// 3. 过滤出图片文件
if (is_image(file_info->d_name)) {
printf("找到图片:%s\n", file_info->d_name);
}
}
// 4. 关闭目录(必须调用,释放资源)
closedir(dirp);
return 0;
}
2.4 编译与运行效果
编译命令(Ubuntu 交叉编译):
bash
运行
aarch64-linux-gnu-gcc dir_scan.c -o dir_scan
运行步骤:
- 在 RK3399 的
/root/目录下创建photos文件夹,放入几张图片; - 将
dir_scan拷贝到 RK3399,执行chmod +x dir_scan; - 运行:
./dir_scan。
三、链表实现:动态存储图片路径
现在我们已经能自动找到图片了,接下来用有头双向循环链表存储图片路径(这种链表操作方便,首尾衔接,遍历效率高)。
3.1 链表基础:节点结构设计
每个节点存储一张图片的完整路径(目录 + 文件名),链表节点定义:
c
运行
// 链表节点结构体
typedef struct Node {
char img_path[1024]; // 存储图片完整路径(如/root/photos/1.jpg)
struct Node *prev; // 前驱指针(指向前一个节点)
struct Node *next; // 后继指针(指向后一个节点)
} Node;
// 链表头结构体(不存储数据,只用来管理链表)
typedef struct List {
Node *head; // 指向链表的头节点
int count; // 链表中节点的数量(图片总数)
} List;
3.2 链表核心操作函数(4 个必备函数)
3.2.1 1. 创建链表头(初始化链表)
c
运行
// 创建链表头,返回链表指针
List *list_create() {
List *list = (List *)malloc(sizeof(List));
if (list == NULL) {
perror("malloc list failed");
return NULL;
}
// 初始化链表头:head指向自己(双向循环链表的特点)
list->head = (Node *)malloc(sizeof(Node));
list->head->prev = list->head;
list->head->next = list->head;
list->count = 0; // 初始时没有节点
return list;
}
3.2.2 2. 创建节点并插入链表
c
运行
// 向链表尾部插入一个节点(存储图片路径)
int list_insert(List *list, const char *img_path) {
if (list == NULL || img_path == NULL) {
return -1;
}
// 1. 创建新节点
Node *new_node = (Node *)malloc(sizeof(Node));
if (new_node == NULL) {
perror("malloc node failed");
return -1;
}
// 2. 给新节点赋值(存储图片路径)
strncpy(new_node->img_path, img_path, sizeof(new_node->img_path)-1);
new_node->img_path[sizeof(new_node->img_path)-1] = '\0'; // 避免字符串溢出
// 3. 插入链表尾部(双向循环链表插入逻辑)
Node *tail = list->head->prev; // 找到链表尾部节点
new_node->prev = tail; // 新节点的前驱指向尾部
new_node->next = list->head; // 新节点的后继指向链表头
tail->next = new_node; // 尾部节点的后继指向新节点
list->head->prev = new_node; // 链表头的前驱指向新节点
list->count++; // 节点数量+1
return 0;
}
3.2.3 3. 遍历链表(获取所有图片路径)
c
运行
// 遍历链表,打印所有图片路径(测试用)
void list_traverse(List *list) {
if (list == NULL || list->count == 0) {
printf("链表为空\n");
return;
}
Node *p = list->head->next; // 从第一个数据节点开始遍历
int i = 0;
while (p != list->head) { // 循环到链表头结束
printf("第%d张图片路径:%s\n", ++i, p->img_path);
p = p->next; // 移动到下一个节点
}
}
3.2.4 4. 释放链表(避免内存泄漏)
c
运行
// 释放链表所有资源
void list_destroy(List *list) {
if (list == NULL) {
return;
}
Node *p = list->head->next;
Node *temp;
while (p != list->head) {
temp = p;
p = p->next;
free(temp); // 释放每个数据节点
}
free(list->head); // 释放链表头
free(list); // 释放链表
list = NULL;
}
3.3 链表操作示例(整合目录扫描)
下面代码实现:扫描目录→找到图片→插入链表→遍历链表。
c
运行
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
// 链表节点和链表头定义(同上)
typedef struct Node {
char img_path[1024];
struct Node *prev;
struct Node *next;
} Node;
typedef struct List {
Node *head;
int count;
} List;
// 链表操作函数(同上:list_create、list_insert、list_traverse、list_destroy)
// ...(此处省略上面的4个函数,实际代码中需要包含)
// 图片格式过滤函数(同上)
int is_image(const char *filename) {
const char *suffixes[] = {".jpg", ".jpeg", ".bmp", ".JPG", ".BMP"};
int suffix_num = sizeof(suffixes) / sizeof(suffixes[0]);
for (int i = 0; i < suffix_num; i++) {
if (strstr(filename, suffixes[i]) != NULL) {
return 1;
}
}
return 0;
}
// 扫描目录,将图片路径插入链表
int scan_photos_to_list(const char *dir_path, List *list) {
DIR *dirp = opendir(dir_path);
if (dirp == NULL) {
perror("opendir failed");
return -1;
}
struct dirent *file_info;
char full_path[1024]; // 存储图片完整路径
while ((file_info = readdir(dirp)) != NULL) {
// 跳过非普通文件
if (file_info->d_type != DT_REG) {
continue;
}
// 过滤图片文件
if (is_image(file_info->d_name)) {
// 拼接完整路径:目录路径 + 文件名
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, file_info->d_name);
// 插入链表
list_insert(list, full_path);
}
}
closedir(dirp);
return 0;
}
int main() {
const char *photo_dir = "/root/photos";
// 1. 创建链表
List *img_list = list_create();
if (img_list == NULL) {
return -1;
}
// 2. 扫描目录,将图片路径存入链表
if (scan_photos_to_list(photo_dir, img_list) != 0) {
list_destroy(img_list);
return -1;
}
// 3. 遍历链表,打印图片路径
printf("共找到%d张图片:\n", img_list->count);
list_traverse(img_list);
// 4. 释放链表
list_destroy(img_list);
return 0;
}
3.4 运行效果
编译后在 RK3399 上运行,会打印出所有图片的完整路径,链表节点数量等于图片数量,实现了动态存储~
四、终极整合:完整电子相册项目(目录扫描 + 链表 + JPEG 显示)
现在把前面的技术整合起来,实现完整的电子相册:
- 自动扫描
/root/photos目录下的所有图片; - 用链表存储图片完整路径;
- 循环显示所有图片(每张显示 3 秒);
- 支持 ESC 键退出。
4.1 完整代码
c
运行
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <setjmp.h>
#include <jpeglib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <SDL2/SDL.h> // 用于事件处理(ESC退出)
// 链表相关定义和函数
typedef struct Node {
char img_path[1024];
struct Node *prev;
struct Node *next;
} Node;
typedef struct List {
Node *head;
int count;
} List;
List *list_create() {
List *list = (List *)malloc(sizeof(List));
if (list == NULL) {
perror("malloc list failed");
return NULL;
}
list->head = (Node *)malloc(sizeof(Node));
list->head->prev = list->head;
list->head->next = list->head;
list->count = 0;
return list;
}
int list_insert(List *list, const char *img_path) {
if (list == NULL || img_path == NULL) {
return -1;
}
Node *new_node = (Node *)malloc(sizeof(Node));
if (new_node == NULL) {
perror("malloc node failed");
return -1;
}
strncpy(new_node->img_path, img_path, sizeof(new_node->img_path)-1);
new_node->img_path[sizeof(new_node->img_path)-1] = '\0';
Node *tail = list->head->prev;
new_node->prev = tail;
new_node->next = list->head;
tail->next = new_node;
list->head->prev = new_node;
list->count++;
return 0;
}
void list_destroy(List *list) {
if (list == NULL) {
return;
}
Node *p = list->head->next;
Node *temp;
while (p != list->head) {
temp = p;
p = p->next;
free(temp);
}
free(list->head);
free(list);
list = NULL;
}
// 图片格式过滤
int is_image(const char *filename) {
const char *suffixes[] = {".jpg", ".jpeg", ".bmp", ".JPG", ".BMP"};
int suffix_num = sizeof(suffixes) / sizeof(suffixes[0]);
for (int i = 0; i < suffix_num; i++) {
if (strstr(filename, suffixes[i]) != NULL) {
return 1;
}
}
return 0;
}
// 扫描图片到链表
int scan_photos_to_list(const char *dir_path, List *list) {
DIR *dirp = opendir(dir_path);
if (dirp == NULL) {
perror("opendir failed");
return -1;
}
struct dirent *file_info;
char full_path[1024];
while ((file_info = readdir(dirp)) != NULL) {
if (file_info->d_type != DT_REG) {
continue;
}
if (is_image(file_info->d_name)) {
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, file_info->d_name);
list_insert(list, full_path);
}
}
closedir(dirp);
return 0;
}
// LCD初始化(显存映射)
int lcd_init(int *fd_lcd, int **pfile_lcd, struct fb_var_screeninfo *var_info) {
*fd_lcd = open("/dev/fb0", O_RDWR);
if (*fd_lcd < 0) {
perror("open /dev/fb0 failed");
return -1;
}
struct fb_fix_screeninfo fix_info;
if (ioctl(*fd_lcd, FBIOGET_FSCREENINFO, &fix_info) < 0) {
perror("ioctl FBIOGET_FSCREENINFO failed");
return -1;
}
if (ioctl(*fd_lcd, FBIOGET_VSCREENINFO, var_info) < 0) {
perror("ioctl FBIOGET_VSCREENINFO failed");
return -1;
}
*pfile_lcd = (int *)mmap(NULL, fix_info.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, *fd_lcd, 0);
if (*pfile_lcd == MAP_FAILED) {
perror("mmap fb0 failed");
return -1;
}
return 0;
}
// JPEG图片显示函数
int jpeg_show(const char *img_path, int *pfile_lcd, struct fb_var_screeninfo var_info) {
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
FILE *infile = fopen(img_path, "rb");
if (infile == NULL) {
fprintf(stderr, "open %s failed\n", img_path);
return -1;
}
jpeg_stdio_src(&cinfo, infile);
jpeg_read_header(&cinfo, TRUE);
cinfo.scale_num = 1;
cinfo.scale_denom = 1;
cinfo.out_color_space = JCS_RGB;
jpeg_start_decompress(&cinfo);
unsigned char *buffer = (unsigned char *)malloc(cinfo.output_width * cinfo.output_components);
if (buffer == NULL) {
perror("malloc buffer failed");
return -1;
}
// 清空屏幕(黑色背景)
memset(pfile_lcd, 0, var_info.xres * var_info.yres * 4);
int x, color;
while (cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines(&cinfo, &buffer, 1);
for (x = 0; x < cinfo.output_width; x++) {
color = (255 << 24) | (buffer[x*3] << 16) | (buffer[x*3+1] << 8) | buffer[x*3+2];
*(pfile_lcd + x + var_info.xres * (cinfo.output_scanline - 1)) = color;
}
}
free(buffer);
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return 0;
}
int main() {
const char *photo_dir = "/root/photos";
List *img_list = list_create();
if (img_list == NULL) {
return -1;
}
// 扫描图片到链表
if (scan_photos_to_list(photo_dir, img_list) != 0 || img_list->count == 0) {
printf("未找到图片!\n");
list_destroy(img_list);
return -1;
}
printf("共找到%d张图片,开始循环播放...(ESC键退出)\n", img_list->count);
// 初始化LCD
int fd_lcd;
int *pfile_lcd;
struct fb_var_screeninfo var_info;
if (lcd_init(&fd_lcd, &pfile_lcd, &var_info) != 0) {
list_destroy(img_list);
return -1;
}
// 初始化SDL(用于事件处理,监测ESC键)
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
fprintf(stderr, "SDL init failed: %s\n", SDL_GetError());
list_destroy(img_list);
return -1;
}
// 循环播放图片
Node *current_node = img_list->head->next; // 当前显示的节点
SDL_Event event;
int running = 1;
while (running) {
// 显示当前图片
jpeg_show(current_node->img_path, pfile_lcd, var_info);
printf("正在显示:%s\n", current_node->img_path);
// 延时3秒(同时监测ESC键)
int delay = 3000; // 3000毫秒
while (delay > 0 && running) {
SDL_PollEvent(&event);
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) {
running = 0; // ESC键退出
break;
}
usleep(1000); // 1毫秒
delay--;
}
// 切换到下一张图片(链表循环)
current_node = current_node->next;
if (current_node == img_list->head) { // 到尾部了,回到头部
current_node = img_list->head->next;
}
}
// 释放所有资源
SDL_Quit();
munmap(pfile_lcd, var_info.xres * var_info.yres * 4);
close(fd_lcd);
list_destroy(img_list);
printf("程序退出!\n");
return 0;
}
4.2 编译与运行
编译命令(需链接 SDL2 和 libjpeg):
bash
运行
aarch64-linux-gnu-gcc album.c -o album -I/home/你的用户名/work/rk3399/armlib/include \
-L/home/你的用户名/work/rk3399/armlib/lib -ljpeg -lSDL2
运行步骤:
- 在 RK3399 的
/root/photos目录放入图片; - 拷贝
album到开发板,执行chmod +x album; - 运行:
./album; - 效果:LCD 循环显示所有图片,按 ESC 键退出。
928

被折叠的 条评论
为什么被折叠?



