目錄題目題目分析思路解析知識點涉及代碼展示優化思考問題一:觀察界面切換效果,可明顯觀察到界面切換時有明顯的刷新效果,有點影響使用效果問題二:圖片的按鍵位置不能相近或者重合,否則有誤觸導致執行了別的功能問題三:當快速來回點擊觸摸屏兩個位置時,會出現點擊位置坐標讀取與實際觸摸坐標不一致的情況 題目 設計 ...
目錄
題目
設計一個程式,該程式在運行之後自動播放一段開機動畫,開機動畫結束後可以調轉到登錄界面,登錄界面有2個按鈕,分別是登錄和退出,點擊登錄之後可以顯示系統主界面。主界面自擬,要求主界面有一個返回按鈕,點擊返回按鈕可以回到登錄界面。要求:不可以使用 goto 語句。
題目分析
該題目的主要訴求可總結為:
- 開機時需要有一段開機動畫,且在開機動畫結束後可以之間到達操作主界面
- 主界面上會有兩個按鈕,即切換界面和退出
- 界面切換不能使用goto語句
思路解析
- 開機動畫可以使用裁剪工具GIFtiqu將動態圖裁剪為一張張jpeg圖片,在將jpeg圖片解碼迴圈顯示,且可以將登錄界面圖片放至迴圈的最後一張。
- 觸屏按鍵切換,該功能涉及到讀取LCD屏的觸摸屏設備信息。需要創建對應格式的結構體變數,並利用read()函數將設備文件中的信息存儲進創建的結構體變數中。
- 由於讀取觸摸屏參數不能只讀一次,所以採取死迴圈作為迴圈,並設置退出鍵坐標為退出迴圈或者退出整個程式。
知識點涉及
- 開機動畫圖片迴圈時,需要使用usleep()控制迴圈間隙。該函數的單位為微秒,且1s(秒) = 1000ms(毫秒),1ms(毫秒) = 1000μs(微秒)。經過計算,使得圖片迴圈能夠滿足人眼視覺殘留條件,最終達到圖片“動”起來的效果。
- 由於開機動畫使用的是JPEG圖片,所以還涉及到JPEG的解碼步驟。
- 觸摸屏的設備信息在linux系統下也是一個文件,所以我們可以通過read()將這些信息讀取到特定結構的結構體變數中,再來對獲取到的參數做相應的操作。
代碼展示
/*******************************************************************
*
* file name: main.c
* author : [email protected]
* date : 2024/05/14
* function : 該案例是掌握LCD屏觸摸原理以及顯示圖片切換過程
* note : None
*
* CopyRight (c) 2023-2024 [email protected] All Right Reseverd
*
* *****************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <linux/input.h>
#include "jpeglib.h"
/********************************************************************
*
* name : read_JPEG_file
* function : 實現完成libjpeg庫的移植,實現在LCD上的任意位置
顯示一張任意大小的jpg圖片,並且對可能越界的情況做錯誤處理。
* argument :
* @filename :需要解碼的jpg圖片
@start_x :圖片顯示初始位置的橫坐標
@start_y :圖片顯示初始位置的縱坐標
@lcd_mp :LCD屏記憶體映射空間的地址
*
* retval : 調用成功返回1,調用失敗返回0
* author : [email protected]
* date : 2024/05/13
* note : 學習JPEG的解碼過程,以及JPEG存儲顏色分量的方式
*
* *****************************************************************/
int read_JPEG_file(char *filename, int start_x, int start_y, int *lcd_mp)
{
/*[1]:創建解碼對象,並且對解碼對象進行初始化,另外需要創建錯誤處理對象,並和解碼對象進行關聯*/
// 創建解碼對象,其是一個結構體變數
struct jpeg_decompress_struct cinfo;
// 創建錯誤處理對象
struct jpeg_error_mgr jerr;
// 將錯誤處理對象與解碼對象相關聯
cinfo.err = jpeg_std_error(&jerr);
// 對解碼對象進行初始化
jpeg_create_decompress(&cinfo);
/*[2]:打開待解碼的jpg圖片,使用fopen的時候需要添加選項”b”,以二進位方式打開文件!*/
FILE *infile; // 接收打開文件的文件指針
unsigned char *buffer; // 輸出行緩衝區
int row_stride; // buffer一行的像素點數量,即圖片的寬度
// 以二進位方式打開圖片,併進行錯誤處理
if ((infile = fopen(filename, "rb")) == NULL)
{
fprintf(stderr, "can't open %s\n", filename);
return 0;
}
// 把打開的文件的文件指針和解碼對象進行綁定
jpeg_stdio_src(&cinfo, infile);
/*[3]:讀取待解碼圖片的文件頭,並把圖像信息和解碼對象進行關聯,通過解碼對象對jpg圖片進行解碼*/
(void)jpeg_read_header(&cinfo, TRUE);
/*[4]:可以選擇設置解碼參數,如果打算以預設參數對jpg圖片進行解碼,則可以省略該步驟!*/
/* 在該習題要求中,並不涉及圖片縮放等問題,所以我們可以省略該步驟
* jpeg_read_header(),
*/
/*[5]:開始對jpg圖片進行解碼,調用函數之後開始解碼,可以得到圖像寬、圖像高等信息!*/
// 我們只需要調用該函數,將圖像信息放入解碼對象中,無需註意其的返回值
(void)jpeg_start_decompress(&cinfo);
/*[6]:開始設計一個迴圈,在迴圈中每次讀取1行的圖像數據,並寫入到LCD中*/
// 計算圖像一行的大小
row_stride = cinfo.output_width * cinfo.output_components;
// 為自定義緩衝區申請堆記憶體,註意申請的記憶體空間大小應為圖像一行的大小
buffer = calloc(1, row_stride);
// 定義一個int類型變數,用於存放顏色分量數據
int data = 0;
/*定義一個迴圈,用於迴圈寫入一行的圖像數據;
使用解碼對象當前掃描行數與圖像的高比較結果作為迴圈條件,當兩者相等,即圖像數據寫入完後退出迴圈*/
while (cinfo.output_scanline < cinfo.output_height)
{
/*調用jpeg_read_scanlines函數,讀取解碼對象中的圖像一行數據,並存放進自定義緩衝區中
且cinfo.output_scanline會隨著調用該函數而增加1,保證while迴圈能夠正常退出*/
(void)jpeg_read_scanlines(&cinfo, &buffer, 1); // 從上到下,從左到右 RGB RGB RGB RGB
// 將緩衝區中存儲的數據逐一寫入LCD的記憶體映射空間中
for (int i = 0; i < cinfo.output_width; ++i) // 012 345
{
/*由於圖片沒有透明度,所以一個像素點大小為3byte,而data為int類型變數,所以需要
藉助"|=" 使得顏色分量順序存儲正確;又因為JEPG存儲顏色分量順序為RGB,所以進行下麵演算法*/
data |= buffer[3 * i] << 16; // R
data |= buffer[3 * i + 1] << 8; // G
data |= buffer[3 * i + 2]; // B
/*把像素點寫入到LCD的指定位置。其中800*start_y + start_x控制的是用戶自定義的圖片顯示初始位置;
800*(cinfo.output_scanline-1)控制的是寫入圖像數據的行數切換;+ i控制的是寫入圖像數據的列數切換*/
lcd_mp[800 * start_y + start_x + 800 * (cinfo.output_scanline - 1) + i] = data;
// 最後需將data內部清零,避免對下一次迴圈的顏色分量寫入造成影響
data = 0;
}
}
/*[7]:在所有的圖像數據都已經解碼完成後,則調用函數完成解碼即可,然後釋放相關資源!(不要遺漏打開的圖像文件)*/
// 解碼完成
(void)jpeg_finish_decompress(&cinfo);
// 釋放解碼對象
jpeg_destroy_decompress(&cinfo);
// 關閉打開的圖像文件
fclose(infile);
return 1;
}
int main(int argc, char const *argv[])
{
// 1.打開LCD open
int lcd_fd = open("/dev/fb0", O_RDWR);
if(lcd_fd == -1)
{
perror("open file of /dev/fb0 is fail");
return -1;
}
// 讀取用戶觸摸坐標
// 1.打開觸摸屏
int ts_fd = open("/dev/input/event0", O_RDWR);
int cnt = 0; // 記錄收到的觸摸函數值
int x, y; // 定義兩個存放橫坐標與縱坐標值的變數
struct input_event ts_event; // 定義觸摸屏輸入信息的結構體變數
// 2.對LCD進行記憶體映射 mmap
int *lcd_mp = (int *)mmap(NULL, 800 * 480 * 4, PROT_READ | PROT_WRITE, MAP_SHARED, lcd_fd, 0);
if(lcd_mp == MAP_FAILED)
{
perror("memory map LCd is fail");
return -1;
}
// 3.顯示開機動畫
char gif_path[128] = {0};
// 開機動畫總共有60張圖片+一張主頁面圖片;使得開機動畫結束後能夠之間進入
for (int i = 0; i < 61; ++i)
{
sprintf(gif_path, "./gif/Frame%d.jpg", i); // 構造jpg圖片的路徑
read_JPEG_file(gif_path, 0, 0, lcd_mp); // 在LCD上顯示
/*FPS = 60HZ
usleep的單位是μs微秒,且1s(秒) = 1000 ms(毫秒) , 1ms(毫秒) = 1000μs(微秒)
1HZ = 1000 / 1000μs*/
usleep(1000 * 16);
}
// 4.死迴圈:在用戶點退出之前無需退出操作界面
while (1)
{
// 5.分析讀取的設備信息 (type + code + value)
// 獲取信息
read(ts_fd, &ts_event, sizeof(ts_event));
// 分析讀取的設備信息 (type + code + value)
if (ts_event.type == EV_ABS) // 說明是觸摸屏
{
if (ts_event.code == ABS_X) // 說明是X軸
{
cnt++;
x = ts_event.value * 800 / 1024;
}
if (ts_event.code == ABS_Y) // 說明是Y軸
{
cnt++;
y = ts_event.value * 480 / 600;
}
if (cnt >= 2)
{
printf("x = %d\t", x); // 輸出X軸坐標
printf("y = %d\n", y); // 輸出Y軸坐標
cnt = 0;
}
}
// 6.實時判斷讀取到的觸摸屏坐標,施行相應的功能
// 登錄界面
if (x >= 336 && x <= 450 && y >= 396 && y <= 465)
{
read_JPEG_file("./pic/login.jpg", 0, 0, lcd_mp);
x = 0;
y = 0;
}
// 返回主界面
else if ((x >= 646 && x <= 773) && (y >= 33 && y <= 120))
{
cnt = 0;
read_JPEG_file("./gif/Frame60.jpg", 0, 0, lcd_mp);
x = 0;
y = 0;
}
// 退出
else if ((x >= 628 && x <= 733) && (y >= 397 && y <= 454))
{
read_JPEG_file("./pic/close.jpg", 0, 0);
break;
}
}
return 0;
}
優化思考
問題一:觀察界面切換效果,可明顯觀察到界面切換時有明顯的刷新效果,有點影響使用效果
分析:
初步分析是因為JPEG解碼後,是通過一行一行像素點寫入LCD映射空間的,故而導致可以看到明顯的從上到下的切換效果。
優化方向:
- 優化JPEG解碼步驟的迴圈:使得圖片可以更快更好的寫入進LCD映射空間內,但是這個方向實現起來較為麻煩。
- 將界面切換過程均換成動態圖:經過觀察開機動畫,發現過程中完全看不到刷新效果,可以達到絲滑切換圖片的效果,所以初步設想是因為圖片切換速度超過人眼觀察速度,所以我認為這個方向可以實現。且考慮到切換界面動畫的框架可以保持一致,這樣省去了大量的準備步驟,還能夠獲得更好的效果,準備在項目內試驗。
問題二:圖片的按鍵位置不能相近或者重合,否則有誤觸導致執行了別的功能
分析:
這是因為當前程式的架構,是將讀取屏幕觸屏參數與條件判斷直接放在一起迴圈導致的。read()函數會像scanf()函數那樣有等待的過程,所以當用戶沒有觸摸時,程式會卡在read()處。
優化方向:
- 對程式進行模塊化編程:這樣可以將各個界面的讀取參數與判斷間隔開,降低各個界面之間的耦合性。即在這個界面函數結束前,只會進行該界面的按鍵位置條件判斷,從而消除誤觸執行其他功能的可能性。
問題三:當快速來回點擊觸摸屏兩個位置時,會出現點擊位置坐標讀取與實際觸摸坐標不一致的情況
分析:
初步分析是因為讀取觸摸坐標的機制導致的,但是目前沒有辦法證實,期待後續的學習。
優化方向:
-
滿足一次條件判斷後,將存儲獲取參數的變數清空:經過實測發現,只要在滿足條件判斷後,將變數內參數情況,再進行讀取賦值操作,便可以絲滑進行界面切換,不會出現讀取坐標與實際觸摸坐標不同的情況。
同樣,原理未知,期待後續學習補充。