目錄
圖像層的底層抽象——繪圖設備抽象
如何抽象一個繪圖設備?
橋接繪圖設備,特化為OLED設備
題外話:設備的屬性,與設計一個相似函數化簡的通用辦法
使用函數指針來操作設備
總結一下
圖像層的底層抽象——繪圖設備抽象
在上一篇博客中,我們完成了對設備層的抽象。現在,我們終于可以賣出雄心壯志的一步了!那就是嘗試去完成一個最為基礎的圖形庫。我們要做的,就是設計一個更加復雜的繪圖設備。
為什么是繪圖設備呢?我們程序員都是懶蛋,想要最大程度的復用代碼,省最大的力氣干最多的事情。所以,我們的圖像框架在未來,還會使用LCD繪制,還會使用其他形形色色的繪制設備來繪制我們的圖像。而不僅限于OLED。所以,讓我們抽象一個可以繪制的設備而不是一個OLED設備,是非常重要的。
一個繪圖設備,是OLED設備的的子集。他可以開啟關閉,完成繪制操作,刷新繪制操作,清空繪制操作。僅此而已。
typedef void* ? CCDeviceRawHandle;
typedef void* ? CCDeviceRawHandleConfig;
?
// 初始化設備,設備需要做一定的初始化后才能繪制圖形
typedef void(*Initer)(CCDeviceHandler* handler, CCDeviceRawHandleConfig config);
?
// 清空設備
typedef void(*ClearDevice)(CCDeviceHandler* handler);
?
// 更新設備
typedef void(*UpdateDevice)(CCDeviceHandler* handler);
?
// 反色設備
typedef void(*ReverseDevice)(CCDeviceHandler* handler);
?
// 繪制點
typedef void(*SetPixel)(CCDeviceHandler* handler, uint16_t x, uint16_t y);
?
// 繪制面
typedef void(*DrawArea)(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t* sources
);
?
// 面操作(清空,反色,更新等等,反正不需要外來提供繪制資源的操作)
typedef void(*AreaOperation)(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height
);
?
// 這個比較新,筆者后面講
typedef enum{CommonProperty_WIDTH,CommonProperty_HEIGHT,CommonProperty_SUPPORT_RGB
}CommonProperty;
?
// 獲取資源的屬性
typedef void(*FetchProperty)(CCDeviceHandler*, void*, CommonProperty p);
?
// 一個繪圖設備可以完成的操作
// 提示,其實可以化簡,一些函數指針(或者說方法)是沒有必要存在的,思考一下如何化簡呢?
typedef struct __DeviceOperations
{Initer ? ? ? ? init_function;ClearDevice ? ? clear_device_function;UpdateDevice ? update_device_function;SetPixel ? ? ? set_pixel_device_function;ReverseDevice ? reverse_device_function;DrawArea ? ? ? draw_area_device_function;AreaOperation ? clearArea_function;AreaOperation ? updateArea_function;AreaOperation ? reverseArea_function;FetchProperty ? property_function;
}CCDeviceOperations;
?
// 一個繪圖設備的最終抽象
typedef struct __DeviceProperty
{/* device type */CCDeviceType ? ? ? ? ? device_type;/* device raw data handle */CCDeviceRawHandle ? ? ? handle;/* device functions */CCDeviceOperations ? ? operations;
}CCDeviceHandler;
設計上筆者是自底向上設計的,筆者現在打算自頂向下帶大伙解讀一下我的代碼。
如何抽象一個繪圖設備?
這個設備是什么?是一個OLED?還是一個LCD?
/* device type */ CCDeviceType ? ? ? ? ? device_type;
這個設備的底層保存資源是什么?當我們動手準備操作的時候,需要拿什么進行操作呢?
? /* device raw data handle */CCDeviceRawHandle ? ? ? handle;
你不需要在使用的時候關心他到底是什么,因為我們從頭至尾都在使用接口進行操作,你只需要知道,一個繪圖設備可以繪制圖像,這就足夠了
? /* device functions */CCDeviceOperations ? ? operations;
這里是我們的命根子,一個繪圖設備可以完成的操作。我們在之后的設計會大量的見到operations這個操作。
筆者的operations借鑒了Linux是如何抽象文件系統的代碼。顯然,一個良好的面對對象C編寫規范的參考代碼就是Linux的源碼
下一步,就是DeviceType有哪些呢?目前,我們開發的是OLED,也就意味著只有OLED是一個合法的DeviceType
typedef enum{OLED_Type }CCDeviceType;
最后,我們需要思考的是,如何定義一個繪圖設備的行為呢?我們知道我們現在操作的就是一個OLED,所以,我們的問題實際上就轉化成為:
當我們給定了一個明確的,是OLED設備的繪圖設備的時候,怎么聯系起來繪圖設備和OLED設備呢?
答案還是回到我們如何抽象設備層的代碼上,那就是根據我們的類型來選擇我們的方法。
/* calling this is not encouraged! */
void __register_paintdevice(CCDeviceHandler* blank_handler, CCDeviceRawHandle raw_handle, CCDeviceRawHandleConfig config, CCDeviceType type);
?
#define register_oled_paintdevice(handler, raw, config) \__register_paintdevice(handler, raw, config, OLED_Type)
所以,我們注冊一個OLED的繪圖設備,只需要調用接口register_oled_paintdevice就好了,提供一個干凈的OLED_HANDLE和初始化OLED_HANDLE所需要的資源,我們的設備也就完成了初始化。
#include "Graphic/device_adapter/CCGraphic_device_oled_adapter.h"
#include "Graphic/CCGraphic_device_adapter.h"
?
void __register_paintdevice(CCDeviceHandler* blank_handler, CCDeviceRawHandle raw_handle, CCDeviceRawHandleConfig config, CCDeviceType type)
{blank_handler->handle = raw_handle;blank_handler->device_type = type;switch(type){case OLED_Type:{blank_handler->operations.init_function = (Initer)init_device_oled;blank_handler->operations.clear_device_function =clear_device_oled;blank_handler->operations.set_pixel_device_function = setpixel_device_oled;blank_handler->operations.update_device_function = update_device_oled;blank_handler->operations.clearArea_function =clear_area_device_oled;blank_handler->operations.reverse_device_function =reverse_device_oled;blank_handler->operations.reverseArea_function = reversearea_device_oled;blank_handler->operations.updateArea_function = update_area_device_oled;blank_handler->operations.draw_area_device_function =draw_area_device_oled;blank_handler->operations.property_function = property_fetcher_device_oled;}break;}blank_handler->operations.init_function(blank_handler, config);
}
這個仍然是最空泛的代碼,我們只是簡單的橋接了一下,聲明我們的設備是OLED,還有真正完成橋接的文件:CCGraphic_device_oled_adapter
文件沒有給出來。所以,讓我們看看實際上是如何真正的完成橋接的。
橋接繪圖設備,特化為OLED設備
什么是橋接?什么是特化?橋接指的是講一個抽象結合過度到另一個抽象上,在這里,我們講繪圖設備引渡到我們的OLED設備而不是其他更加寬泛的設備上去,而OLED設備屬于繪圖設備的一個子集,看起來,我們就像是把虛無縹緲的“繪圖設備”落地了,把一個抽象的概念更加具體了。我們的聊天從“用繪圖設備完成XXX”轉向了“使用一個OLED作為繪圖設備完成XXX”了。這就是特化,將一個概念明晰起來。
#include "Graphic/CCGraphic_device_adapter.h"
#include "OLED/Driver/oled_config.h"
?
/* * 提供用于 OLED 設備的相關操作函數 */
?
/*** @struct CCGraphic_OLED_Config* @brief OLED 設備的配置結構體*/
typedef struct {OLED_Driver_Type ? createType; ? ? // OLED 驅動類型(軟 I2C、硬 I2C 等)void* ? ? ? ? ? ? ? related_configs; // 與驅動相關的具體配置
} CCGraphic_OLED_Config;
?
/*** @brief 初始化 OLED 設備* @param blank 空的設備句柄,初始化后填充* @param onProvideConfigs OLED 配置參數指針,包含驅動類型及配置* * @note 調用此函數時需要傳遞初始化好的配置(軟 I2C 或硬 I2C 配置等)*/
void init_device_oled(CCDeviceHandler* blank, CCGraphic_OLED_Config* onProvideConfigs);
?
/*** @brief 刷新整個 OLED 屏幕內容* @param handler 設備句柄*/
void update_device_oled(CCDeviceHandler* handler);
?
/*** @brief 清空 OLED 屏幕內容* @param handler 設備句柄*/
void clear_device_oled(CCDeviceHandler* handler);
?
/*** @brief 設置指定位置的像素點* @param handler 設備句柄* @param x 橫坐標* @param y 縱坐標*/
void setpixel_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y);
?
/*** @brief 清除指定區域的顯示內容* @param handler 設備句柄* @param x 區域起點的橫坐標* @param y 區域起點的縱坐標* @param width 區域寬度* @param height 區域高度*/
void clear_area_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height);
?
/*** @brief 更新指定區域的顯示內容* @param handler 設備句柄* @param x 區域起點的橫坐標* @param y 區域起點的縱坐標* @param width 區域寬度* @param height 區域高度*/
void update_area_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height);
?
/*** @brief 反轉整個屏幕的顯示顏色* @param handler 設備句柄*/
void reverse_device_oled(CCDeviceHandler* handler);
?
/*** @brief 反轉指定區域的顯示顏色* @param handler 設備句柄* @param x 區域起點的橫坐標* @param y 區域起點的縱坐標* @param width 區域寬度* @param height 區域高度*/
void reversearea_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height);
?
/*** @brief 繪制指定區域的圖像* @param handler 設備句柄* @param x 區域起點的橫坐標* @param y 區域起點的縱坐標* @param width 區域寬度* @param height 區域高度* @param sources 圖像數據源指針*/
void draw_area_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t* sources
);
?
/*** @brief 獲取設備屬性* @param handler 設備句柄* @param getter 屬性獲取指針* @param p 屬性類型*/
void property_fetcher_device_oled(CCDeviceHandler* handler, void* getter, CommonProperty p
);
?
好在代碼實際上并不困難,具體的代碼含義我寫在下面了,可以參考看看
#include "Graphic/device_adapter/CCGraphic_device_oled_adapter.h"
#include "OLED/Driver/oled_base_driver.h"
?
/*** @brief 初始化 OLED 設備* * 根據提供的配置(軟 I2C、硬 I2C、軟 SPI、硬 SPI)初始化 OLED 設備。* * @param blank 空的設備句柄,初始化后填充* @param onProvideConfigs OLED 配置參數指針,包含驅動類型及具體配置*/
void init_device_oled(CCDeviceHandler* blank, CCGraphic_OLED_Config* onProvideConfigs)
{OLED_Handle* handle = (OLED_Handle*)(blank->handle);OLED_Driver_Type type = onProvideConfigs->createType;
?switch(type){case OLED_SOFT_IIC_DRIVER_TYPE:oled_init_softiic_handle(handle,(OLED_SOFT_IIC_Private_Config*) (onProvideConfigs->related_configs));break;
?case OLED_HARD_IIC_DRIVER_TYPE:oled_init_hardiic_handle(handle, (OLED_HARD_IIC_Private_Config*)(onProvideConfigs->related_configs));break;
?case OLED_SOFT_SPI_DRIVER_TYPE:oled_init_softspi_handle(handle,(OLED_SOFT_SPI_Private_Config*)(onProvideConfigs->related_configs));break;
?case OLED_HARD_SPI_DRIVER_TYPE:oled_init_hardspi_handle(handle,(OLED_HARD_SPI_Private_Config*)(onProvideConfigs->related_configs));break;}
}
?
/*** @brief 刷新整個 OLED 屏幕內容* * @param handler 設備句柄*/
void update_device_oled(CCDeviceHandler* handler)
{OLED_Handle* handle = (OLED_Handle*)handler->handle;oled_helper_update(handle);
}
?
/*** @brief 清空 OLED 屏幕內容* * @param handler 設備句柄*/
void clear_device_oled(CCDeviceHandler* handler)
{OLED_Handle* handle = (OLED_Handle*)handler->handle;oled_helper_clear_frame(handle);
}
?
/*** @brief 設置指定位置的像素點* * @param handler 設備句柄* @param x 橫坐標* @param y 縱坐標*/
void setpixel_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y)
{OLED_Handle* handle = (OLED_Handle*)handler->handle;oled_helper_setpixel(handle, x, y);
}
?
/*** @brief 清除指定區域的顯示內容* * @param handler 設備句柄* @param x 區域起點的橫坐標* @param y 區域起點的縱坐標* @param width 區域寬度* @param height 區域高度*/
void clear_area_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{OLED_Handle* handle = (OLED_Handle*)handler->handle;oled_helper_clear_area(handle, x, y, width, height);
}
?
/*** @brief 更新指定區域的顯示內容* * @param handler 設備句柄* @param x 區域起點的橫坐標* @param y 區域起點的縱坐標* @param width 區域寬度* @param height 區域高度*/
void update_area_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{OLED_Handle* handle = (OLED_Handle*)handler->handle;oled_helper_update_area(handle, x, y, width, height);
}
?
/*** @brief 反轉整個屏幕的顯示顏色* * @param handler 設備句柄*/
void reverse_device_oled(CCDeviceHandler* handler)
{OLED_Handle* handle = (OLED_Handle*)handler->handle;oled_helper_reverse(handle);
}
?
/*** @brief 反轉指定區域的顯示顏色* * @param handler 設備句柄* @param x 區域起點的橫坐標* @param y 區域起點的縱坐標* @param width 區域寬度* @param height 區域高度*/
void reversearea_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{OLED_Handle* handle = (OLED_Handle*)handler->handle;oled_helper_reversearea(handle, x, y, width, height);
}
?
/*** @brief 繪制指定區域的圖像* * @param handler 設備句柄* @param x 區域起點的橫坐標* @param y 區域起點的縱坐標* @param width 區域寬度* @param height 區域高度* @param sources 圖像數據源指針*/
void draw_area_device_oled(CCDeviceHandler* handler, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t* sources
){OLED_Handle* handle = (OLED_Handle*)handler->handle;oled_helper_draw_area(handle, x, y, width, height, sources);
}
?
/*** @brief 獲取 OLED 設備屬性* * @param handler 設備句柄* @param getter 屬性獲取指針* @param p 屬性類型(如:高度、寬度、是否支持 RGB 等)*/
void property_fetcher_device_oled(CCDeviceHandler* handler, void* getter, CommonProperty p
)
{OLED_Handle* handle = (OLED_Handle*)handler->handle;switch (p){case CommonProperty_HEIGHT:{ ? int16_t* pHeight = (int16_t*)getter;*pHeight = oled_height(handle);} break;
?case CommonProperty_WIDTH:{int16_t* pWidth = (int16_t*)getter;*pWidth = oled_width(handle);} break;
?case CommonProperty_SUPPORT_RGB:{uint8_t* pSupportRGB = (uint8_t*)getter;*pSupportRGB = oled_support_rgb(handle);} break;
?default:break;}
}
題外話:設備的屬性,與設計一個相似函數化簡的通用辦法
繪圖設備有自己的屬性,比如說告知自己的可繪圖范圍,是否支持RGB色彩繪圖等等,我們的辦法是提供一個對外暴露的可以訪問的devicePropertyEnum
typedef enum{CommonProperty_WIDTH,CommonProperty_HEIGHT,CommonProperty_SUPPORT_RGB }CommonProperty;
設計一個接口,這個接口函數就是FetchProperty
typedef void(*FetchProperty)(CCDeviceHandler*, void*, CommonProperty p);
上層框架代碼提供一個承接返回值的void*和查詢的設備以及查詢類型,我們就返回這個設備的期望屬性
/*** @brief 獲取 OLED 設備屬性* * @param handler 設備句柄* @param getter 屬性獲取指針* @param p 屬性類型(如:高度、寬度、是否支持 RGB 等)*/
void property_fetcher_device_oled(CCDeviceHandler* handler, void* getter, CommonProperty p
)
{OLED_Handle* handle = (OLED_Handle*)handler->handle;switch (p){case CommonProperty_HEIGHT:{ ? int16_t* pHeight = (int16_t*)getter;*pHeight = oled_height(handle);} break;
?case CommonProperty_WIDTH:{int16_t* pWidth = (int16_t*)getter;*pWidth = oled_width(handle);} break;
?case CommonProperty_SUPPORT_RGB:{uint8_t* pSupportRGB = (uint8_t*)getter;*pSupportRGB = oled_support_rgb(handle);} break;
?default:break;}
}
這個就是一種設計返回相似內容的數據的設計思路,將過多相同返回的函數簡化為一個函數,將差異縮小到使用枚舉宏而不是一大坨函數到處拉屎的設計方式
任務提示:筆者這里實際上做的不夠好,你需要知道的是,我在這里是沒有做錯誤處理的。啥意思?你必須讓人家知道你返回的值是不是合法的,人家才知道這個值敢不敢用。
筆者提示您,兩種辦法:
返回值上動手腳:這個是筆者推介的,也是Linux設備代碼中使用的,那就是將屬性獲取的函數簽名返回值修改為uint8_t,或者更進一步的封裝:
typedef enum {FETCH_PROPERTY_FAILED; // 0, YOU CAN USE AS FALSE, BUT NOT RECOMMENDED!FETCH_PROPERTY_SUCCESS; // 1, YOU CAN USE AS TRUE, BUT NOT RECOMMENDED! }FetchPropertyStatus; ? /*** @brief 獲取 OLED 設備屬性* * @param handler 設備句柄* @param getter 屬性獲取指針* @param p 屬性類型(如:高度、寬度、是否支持 RGB 等)* @return */ FetchPropertyStatus property_fetcher_device_oled(CCDeviceHandler* handler, void* getter, CommonProperty p ) {OLED_Handle* handle = (OLED_Handle*)handler->handle;switch (p){case CommonProperty_HEIGHT:{ ? int16_t* pHeight = (int16_t*)getter;*pHeight = oled_height(handle);} break; ?case CommonProperty_WIDTH:{int16_t* pWidth = (int16_t*)getter;*pWidth = oled_width(handle);} break; ?case CommonProperty_SUPPORT_RGB:{uint8_t* pSupportRGB = (uint8_t*)getter;*pSupportRGB = oled_support_rgb(handle);} break; ?default:return FETCH_PROPERTY_FAILED; // not supported property}return FETCH_PROPERTY_SUCCESS; // fetched value can be used for further }
使用上,事情也就變得非常的簡單,筆者后面的一個代碼
? int16_t device_width = 0;device_handle->operations.property_function(device_handle, &device_width, CommonProperty_WIDTH);int16_t device_height = 0;device_handle->operations.property_function(device_handle, &device_height, CommonProperty_HEIGHT);
也就可以更加合理的修改為
? FetchPropertyStatus status;// fetch the width propertyint16_t device_width = 0;status = device_handle->operations.property_function(device_handle, &device_width, CommonProperty_WIDTH);// check if the value validif(!statue){// handling error, or enter HAL_Hard_Fault... anyway!}int16_t device_height = 0;statue = device_handle->operations.property_function(device_handle, &device_height, CommonProperty_HEIGHT);// check if the value validif(!statue){// handling error, or enter HAL_Hard_Fault... anyway!}// now pass the check// use the variable directly...
選取一個非法值。比如說
#define INVALID_PROPERTY_VALUE -1 ... default:{ (int8_t*)value = (int8_t*)getter;value = INVALID_PROPERTY_VALUE;}
但是顯然不好!我們沒辦法區分:是不支持這個屬性呢?還是設備的返回值確實就是-1呢?誰知道呢?所以筆者很不建議在這樣的場景下這樣做!甚至更糟糕的,如果是返回設備的長度,我們使用的是uint16_t接受,那么我們完全沒辦法區分究竟是設備是0xFFFF長,還是是非法值呢?我們一不小心把判斷值的非法和值的含義本身混淆在一起了!
現在,我們就可以完成對一整個設備的抽象了。
使用函數指針來操作設備
筆者之前的代碼已經反反復復出現了使用函數指針而不是調用函數來進行操作,從開銷分析上講,我們多了若干次的解引用操作,但是從封裝上,我們明確的歸屬了函數隸屬于繪圖設備的方法,在極大量的代碼下,這樣起到了一種自說明的效果。
比起來,在業務層次(拿庫做應用的層次,比如說開發一個OLED菜單,做一個恐龍奔跑小游戲,或者是繪制電棍突臉尖叫的動畫),我們只需要強調是這個設備在繪圖
device_handle->operations.updateArea_function(...);
而不是我們讓繪圖的是這個設備
updateArea_device(device_handle, ...);
顯然前者更加的自然。
總結一下
其實,就是完成了對繪圖設備的特化,現在,我們終于可以直接使用Device作為繪圖設備而不是OLED_Handle,下一步,我們就開始真正的手搓設備繪制了。
目錄導覽
總覽
協議層封裝
OLED設備封裝
繪圖設備抽象
基礎圖形庫封裝
基礎組件實現
動態菜單組件實現