c語言管理系統例子

2020-08-13 11:44:53

前言

想當年剛開始學習c語言的時候,當我第一遍看完學校發的c語言書教材,想寫點什麼的時候,發現腦子裏好像並沒有思路和想法,感覺只會寫點簡單的計算。就像小學學了漢字怎麼寫,還是不會寫作文一樣。
書上說c語言什麼都能做,但我感覺什麼都做不了。
我就在想是不是還有什麼沒學,於是我又拿起了譚浩強、c primer plus。。。

其實現在回想起來,看完學校教材的時候基礎應該可以完成很多小程式了,但是少了點程式設計的思路和經驗

思路–用計算機「模擬世界」

首先我們說一下我們需要具備的計算機常識(我就按照我的理解說一下):

  1. 記憶體卡---------計算機的工作場所 用來儲存數據 使用變數、指針等可以直接存取
  2. 硬碟------------計算機的數據倉庫 用來 需要用fopen等檔案操作
  3. 顯示器、音響、網絡卡、鍵盤、滑鼠等等我就不一一說了
  4. ASCII碼-------一套對應關係,看着很簡單,但是這個對映關係非常實用
  5. 二進制、八進制和十六進制

我們可以在記憶體卡或者硬盤裏面儲存數據,最小的數據1bit也就是存0/1,然後有1byte可以連續儲存八個0/1,然後再組合。。。總結一下就是能儲存n個連續0/1,有什麼用呢?那用處可是相當大,n個連續0/1是二進制數據,我們把它轉化爲人們常用的十進制數據,可以用來數學計算、充當索引等。重點來了,充當索引能幹嘛?舉個例子八個0/1組合可以索引ASCII表的任何一項,然後0100 0001就代表了大寫字母A,有些人可能會有疑問,這0100 0001怎麼就是大寫字母A了呢???那是因爲我們人爲的建立了一套關係(編碼),可以理解爲數學中的一 一對映,不同的是對映出來的不再是數值,而是我們自定義的資訊。

c語言

然後我們對着c語言的目錄來看思考一下我們學完基礎到底掌握了着什麼,這關係到我們能做什麼。

  1. 數值型別數據就是對映的實數。字元型數據就是對映的ASCII碼(我們可以使用別的編碼比如說utf-8,就能對映中文了)。
  2. 使用printf等函數可以向控制檯輸出一些數據(ASCII碼),這裏我們使用的顯示器。
  3. 使用scanf等函數可以獲取鍵盤輸入的一些數據,這裏我們使用的鍵盤。
  4. 一些運算子可以讓我們做一些數學方面的數據處理
  5. 回圈、分支、跳轉、函數可以讓我們做一些邏輯方面的數據處理
  6. 陣列、結構體等可以幫我們有規律的組織數據
  7. 指針存取記憶體中的數據,檔案操作存取硬碟中的數據
  8. c語言標準庫(輸入輸出操作、字串操作、時間日期操作、記憶體操作等等):
    在这里插入图片描述
  9. 第三方擴充套件庫!!!「我不會造空調難道還不會吹空調嘛,我不會寫的程式碼別人還不會寫嘛」。我們初學者完全沒必要因爲顯示一個字元就去吃透printf的程式碼,不知道printf是怎麼寫的,用還是會用的吧。其他功能也是類似的情況,不想看控制檯想看圖形介面,就用圖形庫,想聯網,就用網路庫。怎麼用?看幫助文件 + 百度例子可解決問題。

大致回憶了一下我們手裏掌握的資訊,我們可以發現大部分都圍繞着數據數據處理來的。這是我們程式設計的重中之重!!!顯示器、鍵盤等各種硬體也是通過發送接收數據數據處理來控制的。而第三方擴充套件庫也是通過函數呼叫獲取改變數據,並且通過這些數據控制硬體的。

當我學了c語言後感覺突然失去了方向,在我眼前的分支太多了。問學長老師告訴我要去學數據結構和演算法,我也不知道爲什麼要學就去學了,也沒什麼學習的重點(本人喜歡入門後用到什麼學什麼,不然我的興趣可能會被磨滅殆盡)。

數據結構

我感覺數據結構主要是教你如何把現實世界的資訊邏輯轉換到計算機中,舉個簡單的例子:計算機中的儲存空間呈現給我們使用的時候只有一維的,那二維陣列是怎麼來的?我們回憶一下二維陣列a[2][1]可以取第二行第一個的值,而我們計算機地址裏面真的有幾行幾列嗎?沒有,只是我們人爲認定的,也就是邏輯上它是這麼儲存的。
儲存結構:在这里插入图片描述
邏輯結構:在这里插入图片描述
數據結構對應着數據的模擬。

演算法

我感覺演算法就是一個解決問題的步驟,演算法會教你一步一步得到最終想要的結果,對應着數據處理。運算的定義是針對邏輯結構的,指出運算的功能,運算的實現是針對儲存結構的,指出運算的具體操作步驟。我們編寫的程式則是演算法在計算機上的特定實現,在學習的時候最好先從演算法解決的問題入手,會更好的瞭解這個演算法。

總結

數據結構爲演算法服務,簡單模擬一件事情就是記錄關鍵資訊,處理關鍵資訊。c語言是一個實現的載體。

舉個簡單的例子:記錄教室人流量。
首先我們需要通過某種方法讓計算機感知到有人通過教室的門,然後我們在記憶體中存放一個數據,每當有一個人通過教室(邏輯處理),這個數據就+1(數據處理),最後對這個數據進行其他數據處理,生成人流量資訊,顯示到螢幕上。

(這裏說明一下例子中的某種方法,我們目前用的計算機感知世界的方法不多,只有鍵盤、滑鼠、麥克風等的輸入,如果想通過別的方法感知世界,如找到當前的溫度,我們就需要使用感測器等其他硬體,感測器通過某種方式把數據(二進制數據)發送給電腦,電腦纔會知道。就拿鍵盤舉例子,計算機是怎麼知道你輸入的資訊的?當然是操作系統通過鍵盤傳送來的數據索引鍵盤碼錶 碼表了。所以這某種方法不是我們初學計算機該操心的事情。)

從這個例子可以看出我們可以模擬一個事物(當然只會記錄我們感興趣的資訊),模擬一件事情(目前科學能力所及),總結一下最重要的一點:我們可以記錄資訊並且處理它。

例子–學生管理系統(控制檯)

看理論會很枯燥,我甚至感覺有些東西就是寫給已經會了的人看的,初學者看着就是受罪。反而找個教學例子實踐一下可能就豁然開朗。

現在我們來一個經典的例子,就和hello world一樣經典的學生管理系統。
我們需要製作一個通過鍵盤控制的資訊管理系統,通過增刪改查學生數據給使用者提供資訊。

考慮

  1. 首先我們需要思考一下我們要做什麼,一開始寫程式很容易犯的問題就是:需求不明確,走一步看一步。當你寫了很多程式碼後突然發現我還有個功能沒考慮,這可能會讓你改很多很多地方。
  2. 明確需求後我們可以對着需求思考要儲存哪些數據,這些數據有什麼關係,是什麼結構的,在這些數據上要採用什麼演算法(難一點的演算法目前用不到,就算要用也只是呼叫第三方庫,就像公式推導和使用一樣,現在只要會用公式,具體如何推導公式甚至公式長什麼樣子都不需要太清楚,只要知道公式能帶來什麼就行了。如果數據結構很難爲現有的演算法服務就得全部重新考慮)。
  3. 一開始不會的時候臨摹可能是最好的選擇(按照我的腦子自己想出來可能不太現實)。所謂臨摹也就是百度+CV大法,CV後就要記得這種套路了。
  4. 目前我們用不到第三方庫,但是標準庫是用得到的,使用這些庫的時候我們主要要瞭解這些函數的參數、返回值、功能。出bug了可以百度百度是不是有版本問題。
  5. 這裏我們只做32位元debug的,因爲庫是要設定的。

需求

  1. 提供一個功能選單給使用者選擇要執行的功能。(GUI
  2. 接收使用者輸入的目錄並且執行對應功能。(輸入
  3. 新增學生資訊(
  4. 顯示所有學生資訊(GUI
  5. 刪除學生資訊(
  6. 修改學生資訊(
  7. 查詢學生資訊(
  8. 儲存學生資訊(IO操作
  9. 程式剛開始時載入學生資訊(IO操作
  10. 退出系統

分析

  1. 單個學生資訊採用結構體記錄,多個學生之間採用簡單的單鏈表連線(使用陣列需要動態擴容,以後數據結構寫vector的時候再搞)(無頭結點單鏈表的一些簡單增刪改查就不贅述了)。
  2. 輸入和GUI就分別用scanfprintf
  3. 增:使用malloc和指針。
  4. 刪:使用free
  5. 改:字串操作。
  6. 查:字串操作。
  7. 學生結構體以二進制形式儲存在檔案中。

1、提供選單

選單前面的數位代表了操作的指令,隨便怎麼列印,讓別人能看懂就行了。

void ShowOrder(void)
{
	printf("*******************學生資訊管理系統*********************\n");
	printf("*******************本系 本係統操作指令如下*******************\n");
	printf("***             1、 增加一個學生資訊                 ***\n");
	printf("***             2、 顯示所有學生的資訊               ***\n");
	printf("***             3、 刪除指定學生的資訊               ***\n");
	printf("***             4、 修改指定學生的資訊               ***\n");
	printf("***             5、 查詢指定學生的資訊               ***\n");
	printf("***             6、 儲存學生資訊到檔案               ***\n");
	printf("***             0、 退出系統                         ***\n");
	printf("********************************************************\n");
}

執行結果:在这里插入图片描述

printf我們再熟悉不過了,我們主要要讓它在控制檯顯示字元。而它還有返回值(列印的字元數,如果發生錯誤則返回一個負值)。這裏我們並不處理錯誤情況,講道理錯誤我真的還沒遇到過。因爲我都是照規矩辦事的,用法錯誤歸根到底是程式設計師的事情。

2、接收命令

宏定義比單純的數位更加有表現力,方便我們自己檢視程式。當你看到一堆數位的時候感覺很難記得它代表了什麼。

/// 功能
#define	INSERT_STU	1
#define SHOW_STU	2
#define DELETE_STU	3
#define MODIFY_STU	4
#define FIND_STU	5
#define SAVE_STU	6
#define EXIT_SYS	0

通過一個回圈,用scanf接收使用者輸入,然後用switch處理對應指令。

int main(void)
{
	ShowOrder();

	int order = -1;
	while (order)
	{
		printf("請輸入指令:");
		scanf("%d", &order);

		switch (order)
		{
			case INSERT_STU	:		printf("case 1\n");		break;
			case SHOW_STU	:		printf("case 2\n");		break;
			case DELETE_STU	:		printf("case 3\n");		break;
			case MODIFY_STU	:		printf("case 4\n");		break;
			case FIND_STU	:		printf("case 5\n");		break;
			case SAVE_STU	:		printf("case 6\n");		break;
			case EXIT_SYS	:		printf("case 0\n");		break;
			default:	printf("輸入的指令不對!\n");	break;
		}
	}

	return 0;
}

執行結果:在这里插入图片描述
有個小問題就是操作一多就看不見選單了,所以就要稍微修改一下顯示邏輯,然後把顯示選單移到回圈中。system("cls");是用來清屏的,Sleep(500);是延時半秒,讓使用者有時間看清系統提示(照道理這個500也應該寫成宏的,但是這個一眼就能看懂,沒必要)。

void ClsAndShowOrder(void)
{
	Sleep(500);
	system("cls");
	printf("*******************學生資訊管理系統*********************\n");
	printf("*******************本系 本係統操作指令如下*******************\n");
	printf("***             1、 增加一個學生資訊                 ***\n");
	printf("***             2、 顯示所有學生的資訊               ***\n");
	printf("***             3、 刪除指定學生的資訊               ***\n");
	printf("***             4、 修改指定學生的資訊               ***\n");
	printf("***             5、 查詢指定學生的資訊               ***\n");
	printf("***             6、 儲存學生資訊到檔案               ***\n");
	printf("***             0、 退出系統                         ***\n");
	printf("********************************************************\n");
}

這裏還有一個小問題要注意,如果使用者亂輸字母漢字,或者不小心輸錯了別的,程式就會陷入死回圈(以前困擾了我很久,以爲是電腦壞了)。多次使用scanf的時候,如果輸入緩衝區還有數據的話,那麼scanf就不會詢問使用者輸入,而是直接就將輸入緩衝區的內容拿出來用了。如果前面scanf讀取失敗,就會一直留在緩衝區中,最後變成讀取死回圈。解決這種問題總的思想就是通過各種方法將輸入緩衝區的內容讀出來就行

scanf("%*[^\n]%*c");的意思:

  1. *表示讀入某型別的內容,但是這個內容不儲存到變數裡,所以後面不需要對應的參量。
  2. %*[^\n]表示讀入除了回車之外的字元以及讀入一個字元後不儲存。
  3. []內是隻讀入限定讀入的字元,如[abcd]指的是隻讀入abcd的字元。
  4. 整行程式碼的解釋是%*[^\n]首先讀入緩衝區的剩餘內容,%*c讀入最後一個沒有讀入的回車,這樣就清空了輸入緩衝區。

回圈最後的order = -1;是爲了解決我們寫的另一個bug:我先執行了功能1,這個時候order的值就是1,然後輸入了錯誤的指令,order的值不變,還是會執行功能1(錯誤的輸入真的始料未及)。
這樣的話我們就不能通過回圈條件退出系統了,所以直接使用exit函數退出。現在的whileorder已經沒有關係了,改成while(1)也沒有問題。

int main(void)
{
	int order = -1;
	while (order)
	{
		ClsAndShowOrder();

		printf("請輸入指令:");
		scanf("%d", &order);
		scanf("%*[^\n]%*c");

		switch (order)
		{
			case INSERT_STU	:		printf("case 1\n");		break;
			case SHOW_STU	:		printf("case 2\n");		break;
			case DELETE_STU	:		printf("case 3\n");		break;
			case MODIFY_STU	:		printf("case 4\n");		break;
			case FIND_STU	:		printf("case 5\n");		break;
			case SAVE_STU	:		printf("case 6\n");		break;
			case EXIT_SYS	:		exit(0);				break;
			default:	printf("輸入的指令不對!\n");	break;
		}

		order = -1;
	}

	return 0;
}

最後我們再稍微修改一下程式:

void ShowOrder(void)
{
	printf("*******************學生資訊管理系統*********************\n");
	printf("*******************本系 本係統操作指令如下*******************\n");
	printf("***             1、 增加一個學生資訊                 ***\n");
	printf("***             2、 顯示所有學生的資訊               ***\n");
	printf("***             3、 刪除指定學生的資訊               ***\n");
	printf("***             4、 修改指定學生的資訊               ***\n");
	printf("***             5、 查詢指定學生的資訊               ***\n");
	printf("***             6、 儲存學生資訊到檔案               ***\n");
	printf("***             0、 退出系統                         ***\n");
	printf("********************************************************\n");
}

void ClsAndShowOrder(void)
{
	Sleep(500);
	system("cls");
	ShowOrder();
	printf("請輸入指令:");
}

void ReadOrder(int *order)
{
	scanf("%d", order);
	scanf("%*[^\n]%*c");
}
int main(void)
{
	int order = -1;
	while (order)
	{
		ClsAndShowOrder();
		ReadOrder(&order);

		switch (order)
		{
			case INSERT_STU	:		printf("case 1\n");		break;
			case SHOW_STU	:		printf("case 2\n");		break;
			case DELETE_STU	:		printf("case 3\n");		break;
			case MODIFY_STU	:		printf("case 4\n");		break;
			case FIND_STU	:		printf("case 5\n");		break;
			case SAVE_STU	:		printf("case 6\n");		break;
			case EXIT_SYS	:		exit(0);				break;
			default:	printf("輸入的指令不對!\n");	break;
		}

		order = -1;
	}

	return 0;
}

改一個bug的時候很容易就引起另一個bug,我們需要儘量保持一個函數的健壯,使用一些確定不會出錯的函數能減少出bug的可能。最好是讓各個部分沒有關係,就像這個order變數,一開始又是回圈的條件,又是指令的載體,很容易翻車。

3、新增學生資訊

我們需要一個學生資訊的儲存結構體(這邊記錄的資訊就少點了,其中對學號和分數有特殊要求):

/// 學生節點
#define NUMBER_SIZE	15
#define NAME_SIZE	10
typedef struct _STU
{
	char number[NUMBER_SIZE];	// 學號(全爲數位)
	char name[NAME_SIZE];	// 姓名
	int  score;			// 分數(0-100)
	struct _STU* next;
} STUNODE;

建立頭指針:STUNODE* head = NULL;
在寫插入函數前先要編寫幾個讀取的函數。
這邊統一一下寫了個ReadInt函數,因爲後面還有其他函數需要讀取整數:

// 讀取整數
int ReadInt(int *num)
{
	int result = scanf("%d", num);
	scanf("%*[^\n]%*c");
	return result;
}

void ReadOrder(int *order)
{
	printf("請輸入指令:");
	ReadInt(order);
}

這邊通過參數maxSize限定字元讀取數量,防止越界。cmd組合出來是%5s這類的。StringCchPrintfsprintf的一個替代品。

// 讀取字串
int ReadString(char *str, int maxSize)
{
	static char cmd[CMD_SIZE];
	StringCchPrintf(cmd, CMD_SIZE, "%%%ds", maxSize);
	int result = scanf(cmd, str);
	scanf("%*[^\n]%*c");
	return result;
}

size_t strspn (const char *s,const char * accept);從參數s 字串的開頭計算連續的字元,而這些字元都完全是accept 所指字串中的字元。

// 判斷字串是否全爲數位
int IsDigitstr(char *str)
{
	return (strspn(str, "0123456789") == strlen(str));
}

void ReadNumber(char *number, int maxSize)
{
	printf("輸入學號(全爲數位):");
	while (1 != ReadString(number, maxSize) || 1 != IsDigitstr(number))
	{
		printf("學號輸入有誤\n");
		printf("輸入學號(全爲數位):");
	}
}
void ReadName(char *name, int maxSize)
{
	printf("輸入姓名:");
	while (1 != ReadString(name, maxSize))
	{
		printf("姓名輸入有誤\n");
		printf("輸入姓名:");
	}
}
// 判斷分數是否符合要求
int IsScoreInRange(int score)
{
	return score >= 0 && score <= 100;
}

void ReadScore(int *score)
{
	printf("輸入分數(0~100):");
	while (1 != ReadInt(score) || 1 != IsScoreInRange(*score))
	{
		printf("分數輸入有誤\n");
		printf("輸入分數(0~100):");
	}
}

然後可以編寫插入函數了(這裏因爲要修改指針的值,所以參數是指針的指針):

void InsertHead(LISTNODE **head, LISTNODE *node)
{
	node->next = *head;
	*head = node;
}
void InsertStu(STUNODE **head)
{
	STUNODE *node = (STUNODE*)malloc(sizeof(STUNODE));

	ReadNumber(node->number, NUMBER_SIZE);
	ReadName(node->name, NAME_SIZE);
	ReadScore(&node->score);

	InsertHead(head, node);
}

不要忘記了退出前釋放記憶體:

void FreeAndExit(STUNODE *head)
{
	STUNODE *temp;
	while (head != NULL)
	{
		temp = head;
		head = head->next;
		free(temp);
	}
	exit(0);
}

程式碼的複用也是很重要的,長的函數是程式碼滋生bug的溫牀。

4、列印學生資訊

這邊使用函數指針作爲參數編寫遍歷,以後可以比較簡單的擴充套件功能:

/// 函數型別
typedef _Bool(*OneParameterAndReturnBoolFun)(LISTNODE *node);

void forEach(LISTNODE *head, OneParameterAndReturnBoolFun fun)
{
	while (head != NULL && fun(head))
	{
		head = head->next;
	}
}
void ShowStu(STUNODE *head)
{
	forEach(head, PrintfStu);
	Pause();
}
_Bool PrintfStu(STUNODE *head)
{
	printf("學號:%15s   姓名:%10s   分數:%3d\n", head->number, head->name, head->score);
	return TRUE;
}
void Pause()
{
	system("pause");
}

執行結果:在这里插入图片描述

這裏用到了函數指針,因爲以後的第三方庫很多這種用法,這裏先熟悉熟悉。

5、查詢學生

發現上面的forEach好像無法完成比較,這裏重新寫了個遍歷函數。

typedef _Bool(*TwoParameterAndReturnBoolFun)(LISTNODE *node1, LISTNODE *node2);

LISTNODE* find(LISTNODE *head, LISTNODE *node, TwoParameterAndReturnBoolFun fun)
{
	while (head != NULL && !fun(head, node))
	{
		head = head->next;
	}
	return head;
}
// 按照學號查詢
_Bool compareNumber(STUNODE *head, STUNODE *node)
{
	return !strcmp(head->number, node->number);
}

STUNODE* FindStuByNumber(STUNODE *head)
{
	STUNODE node;
	ReadNumber(node.number, NUMBER_SIZE);

	return find(head, &node, compareNumber);
}

void FindStu(STUNODE *head)
{
	STUNODE *temp = FindStuByNumber(head);
	temp ? PrintfStu(temp) : PrintfNoFind();
	Pause();
}

void PrintfNoFind()
{
	printf("未找到該學生\n");
}

有人可能有疑問爲什麼只要輸入個學號字串要用STUNODE node;,爲了以後擴充套件按照其他欄位查詢,比較的也不一定是字串。

6、刪除學生

我們使用的是不帶頭節點的單鏈表,所以要處理的時候要注意NULL
我們刪除的時候需要用到前一項:

#define FIRST_NODE (void*)-1

LISTNODE* findPrevious(LISTNODE *head, LISTNODE *node, TwoParameterAndReturnBoolFun fun)
{
	if (head == NULL)
	{
		return head;
	}

	if (fun(head, node))
	{
		return FIRST_NODE;
	}

	while (head->next != NULL && !fun(head->next, node))
	{
		head = head->next;
	}
	return head;
}

刪除分爲修改頭指針和不修改頭指針:

_Bool FreeFirstNode(LISTNODE **head)
{
	LISTNODE *temp = *head;
	*head = temp->next;
	free(temp);
	return TRUE;
}

_Bool FreeNode(LISTNODE *node)
{
	LISTNODE *temp = node->next;
	node->next = node->next->next;
	free(temp);
	return TRUE;
}

_Bool DeleteNode(LISTNODE **head, LISTNODE *node)
{
	if (node == NULL)
	{
		return FALSE;
	}

	if (node == FIRST_NODE)
	{
		return FreeFirstNode(head);
	}

	return FreeNode(node);
}
void PrintfDeleteSuccess()
{
	printf("刪除學生成功\n");
}

_Bool DeleteStuByNumber(STUNODE **head)
{
	STUNODE node;
	ReadNumber(node.number, NUMBER_SIZE);

	return DeleteNode(head, findPrevious(*head, &node, compareNumber));
}

void DeleteStu(STUNODE **head)
{
	DeleteStuByNumber(head) ? PrintfDeleteSuccess() : PrintfNoFind();
	Pause();
}

寫到這裏感覺有個頭節點真好,能有統一的處理。

7、修改學生資訊

修改節點提煉出一個函數:

void ModifyNode(STUNODE *node)
{
	ReadNumber(node->number, NUMBER_SIZE);
	ReadName(node->name, NAME_SIZE);
	ReadScore(&node->score);
}

void ModifyStu(STUNODE *head)
{
	STUNODE *temp = FindStuByNumber(head);
	temp ? ModifyNode(temp) : PrintfNoFind();
	Pause();
}

當你的基礎功能完善的時候,複用起來就很簡單了。

8、儲存到檔案

全域性變數雖然可以說是萬惡之源,但是用起來還真爽,不可用太多,並且要注意對它的修改。

/// 檔名
#define FILENAME "data.txt"

void PrintfSaveSuccess()
{
	printf("儲存學生資訊成功\n");
}

// 暫時儲存檔案指針
FILE *file;

_Bool SaveNode(STUNODE *node)
{
	fwrite(node, 1, sizeof(STUNODE), file);
	return TRUE;
}

void SaveAll(STUNODE *head)
{
	file = fopen(FILENAME, "wb");
	forEach(head, SaveNode);
	fclose(file);
	PrintfSaveSuccess();
	Pause();
}

9、程式剛開始時載入學生資訊

怎麼儲存的就怎麼讀取,一般不會出現亂碼。

STUNODE* ReadNode()
{
	STUNODE *node = (STUNODE*)malloc(sizeof(STUNODE));
	if (fread(node, sizeof(STUNODE), 1, file) <= 0)
	{
		free(node);
		return NULL;
	}
	else
	{
		return node;
	}
}

void ReadAll(STUNODE **head)
{
	file = fopen(FILENAME, "rb+");

	STUNODE *node;
	while (node = ReadNode())
	{
		InsertHead(head, node);
	}

	fclose(file);
	file = NULL;
}

總結

整體程式碼看下來除了一些新的函數可能沒見過(直接百度康康是做什麼的,返回值是什麼),其他地方幾乎都是邏輯判斷和賦值定義。上面的程式都看懂了,c語言算是入門了,沒看懂的仔細體會一下數據什麼時候修改的,如何修改的(語句看不懂就翻翻書,查查資料)。

很多人這個時候就在想接下來要學點什麼呢?
去問別人可能會和你說,當然是學數據結構和演算法了,或者說先問你有什麼喜歡的方向,然後推薦一些他的學習路線,又或者康康下學期有什麼課先學起來。
我的選擇是先」玩玩「程式碼,玩熟了在看理論(有人可能還是高中的思想,我課還沒上呢題目怎麼會做?那你想想讓你每次打遊戲之前先像職業選手一樣練習一天,你還想打嗎?電競選手應該不會來搬磚吧。。。)。你可能沒有一門課叫計算機導論,但是你不可能沒用過電腦軟體。你平常用的軟體能幹什麼你c語言就能幹什麼。那問題又來了,太高階了不會玩啊,菜的安詳。這是小問題,不能開歪瓜還不準 不準用攻略嗎?當然可以叫上同學一起做軟體玩更有趣(也可以組隊打比賽搞個獎),有大佬(同學、學長、老師等)帶萌新就更好了。
應該還有人不知道要做什麼,你就這麼想,上面那個程式哪裏讓我用起來不爽?哪裏還能變得更好?我還想加什麼功能?也可能有人感覺挺好,因爲我當時做出來的時候也感覺良好,這裏我稍微說說我的看法:控制檯介面讓我看不下去用起來也很難受、檔案操作感覺很彆扭、錯誤資訊儲存成日誌、能不能做成聯網的、搜尋的效率好像很低等等等。
或者看看電腦上你平時用的一些軟體,試着做做,做不出來就百度百度(這可能會浪費你很多時間)。
什麼時候開始研究理論?我的標準是當你覺得再這麼做下去無法提高的時候,舉個下面 下麪要寫的數據庫的例子:我一直呼叫數據庫的介面來實現功能,突然我發現不香了,我要邊學學數據庫系統概論邊想我平時是怎麼用的,甚至可以找個開源的數據庫程式碼康康。

給例子換上sqlite數據庫

爲什麼先玩數據庫呢?因爲檔案操作真的很麻煩啊,體驗過數據庫你就會愛上它。
sqlite3菜鳥教學攻略鏈接

我們先主要掌握四個函數開啓數據庫sqlite3_open、關閉數據庫sqlite3_close、執行數據庫命令sqlite3_exec、獲取錯誤sqlite3_errmsg
在这里插入图片描述
如果不會用教學下面 下麪有連線數據庫、建立表等等的例子。
有人可能看完還是對數據庫沒有一個感性的認識。這麼說吧開啓excel,新建一個表格,輸入表頭,輸入數據,關閉excel。這個時候你已經手動做了一遍簡單的數據庫操作
以前的檔案你是用word儲存的,想怎麼寫就怎麼寫,現在你是用excel儲存的,是一個表格。
還有sql語句,照貓畫虎的寫就行了,雖然寫錯可能會找不出來錯在哪裏,但是會有錯誤碼,百度百度可能就知道了。簡單康康sql語句的一些格式,花不了多長時間,並且我們只用很簡單的用法。

1、引入檔案

在这里插入图片描述

別人寫了幾十萬行的程式碼我們直接白嫖

2、建立數據庫和表格

首先需要一個數據庫指針(這裏還是用全域性變數)對應檔案操作那個檔案指針:sqlite3 *db;
可以先刪除檔案相關操作,在ReadAll中呼叫我們編寫的建立數據庫程式碼,fprintf是向檔案輸出,stderr爲標準輸出(裝置)檔案,對應終端的螢幕,所以還是輸出到了控制檯上:

/// 檔名
#define FILENAME "stu.db"

typedef int(*SQLEXECCALLBACK)(void *, int, char **, char **);

// 開啓數據庫
void OpenSqlite()
{
	if (db == NULL)
	{
		sqlite3_open(FILENAME, &db)
			? fprintf(stderr, "無法開啓數據庫: %s\n", sqlite3_errmsg(db))
			: fprintf(stderr, "開啓數據庫成功\n");
	}
}

// 關閉數據庫
void CloseSqlite()
{
	if (db != NULL)
	{
		sqlite3_close(db);
	}
}

assert就是斷言,assert判斷的內容爲假程式直接掛掉。這裏爲什麼搞個斷言,一個是爲了整點活哈哈哈,更重要的是這個地方我們在呼叫數據庫ExecSql等程式碼前肯定是要開啓數據庫的,如果沒開啓或者開啓失敗就是程式設計師的問題了,不應該讓程式執行到這個地方。(這裏我們並沒有處理開啓失敗的情況,可以加一個開啓失敗提醒使用者並且直接結束程式)
再看看sqlite3_execcallback參數是不是和我們前面的forEach等函數的參數很像。

// 執行命令
void ExecSql(char *sql, SQLEXECCALLBACK callback, void *data)
{
	assert(db);

	char *zErrMsg = NULL;
	if (sqlite3_exec(db, sql, callback, data, &zErrMsg) != SQLITE_OK)
	{
		fprintf(stderr, "SQL 錯誤: %s\n", zErrMsg);
		sqlite3_free(zErrMsg);
	}
}

// 空回撥函數
int EmptyCallBack(void *NotUsed, int argc, char **argv, char **azColName)
{
	return 0;
}

// 建立表格
void CreateForm()
{
	assert(db);

	char *createSql = "CREATE TABLE STU("  \
		"ID INTEGER PRIMARY KEY AUTOINCREMENT  NOT NULL," \
		"NUMBER         CHAR(15)           NOT NULL," \
		"NAME           CHAR(10)           NOT NULL," \
		"SCORE          INT                NOT NULL);";
		
	ExecSql(createSql, EmptyCallBack, NULL);
}

void ReadAll(STUNODE **head)
{
	OpenSqlite();

	CreateForm();

	CloseSqlite();
}

最後看看那個sql語句,再看看攻略上寫的一般格式:
在这里插入图片描述
可以看出我們主要要寫數據庫名字、表名字、每個欄位名字型別。AUTOINCREMENTID自動增長的意思,這樣我們插入的時候就不用插入ID了。
執行程式,用視覺化數據庫軟體開啓db檔案康康:
在这里插入图片描述

我們程式碼裏面沒寫數據庫名字可以看到預設是main,表名字、每個欄位名字型別一套搞下來像不像定義一個結構體?
檢視的軟體是Navicat,希望大家支援正版。

3、插入數據

我們寫的list也可以逐漸被數據庫取代,以後取數據就直接去數據庫取,記憶體中的可以不用了。
現在SaveAllReadAll可以刪除了,儲存學生資訊到檔案這項選單可以刪掉了,因爲我們不用手動做這件事情,交給數據庫了。
file.cfile.h名字不合適了,改成datebase吧。
編寫插入函數:

#define SQL_SIZE 100

void Insert(STUNODE *node)
{
	assert(db);

	char insertSql[SQL_SIZE];
	StringCchPrintf(insertSql, SQL_SIZE, "INSERT INTO STU (NUMBER, NAME, SCORE) VALUES ('%s', '%s', %d);",
		node->number, node->name, node->score);

	ExecSql(insertSql, EmptyCallBack, NULL);
}

修改以前stu的插入函數:

void InsertStu(STUNODE **head)
{
	STUNODE node;

	ModifyNode(&node);

	Insert(&node);
}

執行看看效果:
在这里插入图片描述
在这里插入图片描述
嘔吼,張三變亂碼了,sqlite3對應的是UTF8編碼,而我們使用的是GB2312編碼(我們是中國人),所以需要轉碼一下。
嘿嘿自己解決不了的就用別人的程式。白嫖走起!
這邊我們不是直接把別人的程式碼直接拿來用,而是換一個套路:動態鏈接庫(.dll)的生成與使用

#define LOG_BUFFER_MAX 1024
static char gOutBuf[LOG_BUFFER_MAX] = { 0 };

char* Gb2312ToUtf8(char *str, size_t len)
{
	char *outStr = gOutBuf;
	size_t outLen = sizeof(gOutBuf);
	memset(outStr, 0, outLen);

	iconv_t cd = iconv_open("UTF-8//IGNORE", "GB18030");  /* GBK就是GB2312擴充套件版本 GB18030是相容前兩種編碼最全 而ANSI是指GB2312 */
	if (iconv(cd, &str, &len, &outStr, &outLen) < 0)
	{
		TranscodeError();
	}

	iconv_close(cd);

	return gOutBuf;
}

呼叫:ExecSql(Gb2312ToUtf8(insertSql, SQL_SIZE), EmptyCallBack, NULL);
再插一次康康執行結果:
在这里插入图片描述

4、列印學生資訊

在这里插入图片描述
從數據庫讀取數據又需要轉碼,所以把轉碼的函數改了改:

/* GBK就是GB2312擴充套件版本 GB18030是相容前兩種編碼最全 而ANSI是指GB2312 */
char* Gb2312ToUtf8(char *str, size_t len)
{
	return Transcode(str, len, "UTF-8//IGNORE", "GB18030");
}

char* Utf8ToGb2312(char *str, size_t len)
{
	return Transcode(str, len, "GB18030", "UTF-8//IGNORE");
}

// 轉碼
char* Transcode(char *str, size_t len, const char* tocode, const char* fromcode)
{
	char *outStr = gOutBuf;
	size_t outLen = sizeof(gOutBuf);
	memset(outStr, 0, outLen);

	iconv_t cd = iconv_open(tocode, fromcode);
	if (iconv(cd, &str, &len, &outStr, &outLen) < 0)
	{
		TranscodeError();
	}

	iconv_close(cd);

	return gOutBuf;
}

顯示所有學生資訊:

void ShowAll()
{
	assert(db);

	char *selectSql = "SELECT NUMBER as '學號', NAME as '姓名', SCORE as '分數' FROM STU";

	ExecSql(selectSql, PrintfCallBack, NULL);
}

int PrintfCallBack(void *data, int argc, char **argv, char **azColName)
{
	printf("%s:%15s   %s:%10s   %s:%3s\n", azColName[0], argv[0], 
		azColName[1], Utf8ToGb2312(argv[1], strlen(argv[1])), 
		azColName[2], argv[2]);
	return 0;
}

5、移除所有鏈表操作

後面修改的內容和前面大同小異,就簡單帶過去了。

查詢(回撥函數比列印多了個計數功能):

void PrintfStu(char **argv, char **azColName)
{
	printf("%s:%15s   %s:%10s   %s:%3s\n", azColName[0], argv[0],
		azColName[1], Utf8ToGb2312(argv[1], strlen(argv[1])),
		azColName[2], argv[2]);
}

int FindCallBack(void *data, int argc, char **argv, char **azColName)
{
	++(*(int*)data);
	PrintfStu(argv, azColName);
	return 0;
}

int Find(STUNODE *node)
{
	assert(db);

	char selectSql[SQL_SIZE];
	StringCchPrintf(selectSql, SQL_SIZE, "SELECT NUMBER as '學號', NAME as '姓名', SCORE as '分數' FROM STU WHERE NUMBER = '%s';", node->number);
	
	int number = 0;
	ExecSql(selectSql, FindCallBack, &number);
	return number;
}
void FindStu()
{
	STUNODE node;
	ReadNumber(node.number, NUMBER_SIZE);

	if (0 == Find(&node))
	{
		PrintfNoFind();
	}
}

刪除

void Delete(STUNODE *node)
{
	assert(db);

	char deleteSql[SQL_SIZE];
	StringCchPrintf(deleteSql, SQL_SIZE, "DELETE FROM STU where NUMBER = '%s';", node->number);

	ExecSql(deleteSql, EmptyCallBack, NULL);
}
void DeleteStu()
{
	STUNODE node;
	ReadNumber(node.number, NUMBER_SIZE);

	if (0 == Find(&node))
	{
		PrintfNoFind();
	}
	else
	{
		Delete(&node);
	}
}

修改

void Modify(STUNODE *node, STUNODE *update)
{
	assert(db);

	char updateSql[SQL_SIZE];
	StringCchPrintf(updateSql, SQL_SIZE, "UPDATE STU SET NUMBER = '%s', NAME = '%s', SCORE = %d where NUMBER = '%s'; ", 
		update->number, update->name, update->score, node->number);

	ExecSql(Gb2312ToUtf8(updateSql, SQL_SIZE), EmptyCallBack, NULL);
}
void ModifyStu()
{
	STUNODE node;
	ReadNumber(node.number, NUMBER_SIZE);

	if (0 == Find(&node))
	{
		PrintfNoFind();
	}
	else
	{
		STUNODE update;
		ModifyNode(&update);
		Modify(&node, &update);
	}
}

數據庫我們算換完了,我們可以感受到數據如何儲存、讀取等操作都變成了sql語句,sql語句用起來就像一條條命令。感覺就像你有一個僕人,你只需要告訴他要去做什麼,不需要告訴他怎麼做,他就會很好的去執行。
想要知道具體他是怎麼做的,看一開始匯入的兩個檔案,看不懂就看別人寫的原始碼解析(有機會我也想寫一個)。

給例子換介面

大一剛看到控制檯的時候感覺很神祕,現在我只想換個介面。原來我想搞個簡單的圖形庫(一直用的easyx等等)來着,但是回憶了一下幾乎都是c++,那就來玩個古老而又強大的windows開發吧(我學的很少,都是百度的,所以用法可能很奇怪)。
win32視訊攻略鏈接

1、建立視窗

先把原來的主函數換掉(直接刪了吧),搞個空白視窗出來,建立視窗都是套路,如果看不懂先看上面視訊。

#include <Windows.h>

LRESULT CALLBACK CallBack(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam);

// WINAPI:呼叫約定  主要是參數的入棧順序,這個棧空間的清理者,和__stdcall, APIENTRY, 本質都是一樣的 
int WINAPI WinMain ( HINSTANCE hInstance,          	// 當前視窗範例控制代碼
                     HINSTANCE hPrevInstance,      	// 應用程式的前一個事例的控制代碼
                     LPTSTR    lpCmdLine,          	// 指定傳遞給應用程式的命令列參數
                     int       nCmdShow)          	// 指定視窗的顯示方式。隱藏,最大,最小顯示,
{
	/*
    	初始化視窗類 結構體WNDCLASSEX  一共12個成員不多不少
    */
	WNDCLASSEX we;

    we.cbClsExtra = 0;								// 緊跟在視窗類尾部的一塊額外的空間,不用設爲0
    we.cbSize = sizeof (WNDCLASSEX);				// 結構體大小
    we.cbWndExtra = 0;								// 緊跟在視窗範例尾部的一塊額外的空間,不用設爲0
    we.hbrBackground = (HBRUSH) CTLCOLOR_BTN;		// 視窗類的背景刷,爲背景刷控制代碼,也可以爲系統顏色值
    we.hCursor = LoadCursor(NULL, IDC_HAND);		// 遊標風格,不用設爲NULL
    we.hIcon = LoadIcon(NULL, IDI_ASTERISK);		// 圖示,不用設爲NULL
    we.hIconSm = LoadIcon(NULL, IDI_QUESTION);		// 小圖示,不用設爲NULL
    we.hInstance = hInstance;						// 當前視窗範例控制代碼
    we.lpfnWndProc = CallBack;						// 回撥函數地址
    we.lpszClassName = "student";					// 視窗的標示,給系統看的
    we.lpszMenuName = NULL;							// 選單的名字,不用設爲NULL
    we.style = CS_HREDRAW | CS_VREDRAW;				// 視窗類的樣式,它的值可以是視窗樣式值的任意組合

	/*
    	註冊視窗類 如果視窗結構體有一個有問題都無法建立 
    */
    // 判斷是否能註冊視窗類
    if ( 0 == RegisterClassEx(&we) )
    {
        return 0;
    }

	/*
    	建立視窗
    */
    HWND hwnd = CreateWindowEx(  WS_EX_ACCEPTFILES, 		// 附加屬性 
							"student", 						// 視窗類的名字,給系統看的 
							"學生管理系統", 					// 視窗的名字,給人看的
							WS_OVERLAPPEDWINDOW, 			// 指定建立視窗的風格
							0, 0, 							// 指定視窗的初始水平 垂直位置  相對於桌面  單位px
							GetSystemMetrics(SM_CXSCREEN), 	// 視窗的寬度  單位px
							GetSystemMetrics(SM_CYSCREEN), 	// 視窗的高度  單位px
							NULL, 							// 父視窗控制代碼,沒有設定爲NULL
							NULL, 							// 選單控制代碼,沒有設定爲NULL
							hInstance, 						// 當前範例控制代碼
							NULL );							// 指向一個值的指針,該值傳遞給視窗WM_CREATE訊息

	// 判斷視窗是否建立成功
    if (NULL == hwnd)
    {
        return 0;
    }

	/*
    	顯示視窗
    */
    ShowWindow(hwnd, nCmdShow);

	/*
    	訊息回圈
    */
	// 建立結構體msj
    MSG msg;

    while (GetMessage ( &msg,    	// 指向MSG訊息結構體的指針
                        NULL,     	// 指定視窗的控制代碼 用以處理該視窗的訊息 NULL表示當前範例的所有訊息
                        0,0 ) )     // 設定要處理的訊息的範圍 0,0表示所有訊息                     
    {
        /*
        	翻譯訊息
        */
        TranslateMessage (&msg);// 指向MSG訊息結構體的指針
        /*
        	分發訊息
        */
        DispatchMessage (&msg); // 指向MSG訊息結構體的指針
    }
	return 0;
}

/*
	回撥函數
*/
LRESULT CALLBACK Back ( HWND hWnd,      // 視窗控制代碼
                        UINT nMsg,      // 訊息ID
                        WPARAM wParam,  // 傳遞控制代碼和整數
                        LPARAM lParam   // 傳遞控制代碼和整數
                        )
{
	switch (nMsg)
	{
	case WM_CREATE:	// 視窗訊息處理程式接收的第一個訊息
		break;

	case WM_PAINT:	// 回掉函數處理的第二個訊息 當視窗顯示區域的一部分顯示內容或者全部變爲「無效」,以致於必須「更新畫面」時,將由這個訊息通知程式
	{
		PAINTSTRUCT pt;
		HDC hdc = BeginPaint(hWnd, &pt);	// 對WM_PAINT的處理幾乎總是從一個BeginPaint呼叫開始
		// 繪製圖形 
		EndPaint(hWnd, &pt);	// 以一個EndPaint呼叫結束
		break;
	}

	case WM_DESTROY:	// 關閉視窗訊息
		PostQuitMessage(0);	// 發出WM_QUIT訊息
		break;
	}
	return DefWindowProc(hWnd, nMsg, wParam, lParam);	// 讓系統自動處理一些預設訊息  保證系統邏輯的連貫性
}

編譯一下出了點小問題:
在这里插入图片描述
以前是控制檯程式,禿然就變成桌面應用程式,vs沒反應過來,需要我們改一下專案屬性:
在这里插入图片描述
執行結果:
在这里插入图片描述

千萬不要被這些程式碼嚇住,仔細看看除了變數宣告賦值和函數呼叫就沒別的東西了。

2、製作選單

我們這裏打算純靠判斷滑鼠座標的位置選擇功能,就不用控制元件了,說到底本來只想加個圖形介面來着。

這裏編寫個控制整個系統介面的變數(這裏只有系統標題和功能選單,可以自己擴充):

#define LABEL_STR_SIZE	20
#define MENU_SIZE		10

typedef struct
{
	// 座標
	int left;
	int top;
	int right;
	int bottom;

	// 顯示字串
	char str[LABEL_STR_SIZE];
	_Bool isShow;
	int fontWidth;
	int fontHeight;
} LABEL;

typedef struct
{
	// 介面大小
	int width;
	int height;

	LABEL banner;	// 系統標題
	LABEL menus[MENU_SIZE];	// 功能選單
} CONTROL;

static CONTROL control;

初始化資訊:

const char MENUNAME[MENU_SIZE][LABEL_STR_SIZE] =
{
	"新增學生資訊",
	"刪除學生資訊",
	"查詢學生資訊",
	"修改學生資訊",
	"顯示學生資訊",
	"",
	"",
	"",
	"",
	""
};

#define SETLABEL(label, iLeft, iTop, iRight, iBottom, bIsShow, iFontWidth, iFontHeight, pszStr) do { \
		label.left = iLeft;					\
		label.top = iTop;					\
		label.right = iRight;				\
		label.bottom = iBottom;				\
		label.isShow = bIsShow;				\
		label.fontWidth = iFontWidth;		\
		label.fontHeight = iFontHeight;		\
		StringCchPrintf(label.str, LABEL_STR_SIZE, "%s", pszStr);	\
	} while(0)
	
void InitControl(CONTROL *control)
{
	// 螢幕寬高 
	control->width = GetSystemMetrics(SM_CXSCREEN);
	control->height = GetSystemMetrics(SM_CYSCREEN);

	// 系統標題
	SETLABEL(control->banner, 30, 30, 530, 130, TRUE, 40, 100, "學生管理系統");

	// 選單
	for (int i = 0; i < 5; ++i)
	{
		for (int j = 0; j < 2; ++j)
		{
			SETLABEL(control->menus[i * 2 + j], 70 + j * 200, 250 + 100 * i, 70 + j * 200 + 200, 350 + 100 * i, TRUE, 8, 25, MENUNAME[i * 2 + j]);
		}
	}
}

繪製介面(主要通過Rectangle繪製矩形,TextOut繪製文字):

void PrintfSqare(HDC insidehdc, LABEL *label)
{
	if (label->isShow)
	{
		Rectangle(insidehdc, label->left, label->top, label->right, label->bottom);
	}
}

void PrintfLabel(HDC insidehdc, LABEL *label, COLORREF color)
{
	if (label->isShow)
	{
		SetTextColor(insidehdc, color);
		TextOut(insidehdc, label->left, label->top, label->str, strlen(label->str));
	}
}

void PrintfMenu(HDC insidehdc, LABEL *label, COLORREF color)
{
	if (label->isShow)
	{
		SetTextColor(insidehdc, color);
		int len = strlen(label->str);
		int left = label->left + (label->right - label->left - label->fontWidth * len) / 2;
		int top = label->top + (label->bottom - label->top - label->fontHeight) / 2;
		TextOut(insidehdc, left, top, label->str, len);
	}
}

// 繪製所有方塊 
void PrintAllSqare(HDC insidehdc, CONTROL *control)
{
	// 建立一個容器來裝系統預設畫刷
    HBRUSH oldBrush;
    // 建立一個顏色的畫刷
    HBRUSH newBrush = CreateSolidBrush( RGB(128, 255, 255) );
    // 系結當前DC與畫刷,返回系統預設畫刷
    oldBrush = (HBRUSH) SelectObject(insidehdc, newBrush);

	// 系統標題
	Rectangle(insidehdc, 0, 0, control->width, control->height);

	// 選單
	for (int i = 0; i < MENU_SIZE; ++i)
	{
		newBrush = CreateSolidBrush(RGB(255, 228, 80 + 13 * i));
		oldBrush = (HBRUSH)SelectObject(insidehdc, newBrush);
		PrintfSqare(insidehdc, &control->menus[i]);
	}

	// 使用完新畫刷,把系統預設畫刷選回來,返回建立的畫刷
    newBrush = (HBRUSH) SelectObject(insidehdc, oldBrush);
    // 釋放系統資源 
    DeleteObject(newBrush);
}

// 繪製所有label
void PrintAllLabel(HDC insidehdc, CONTROL *control)
{
	// 設定背景透明 
	SetBkMode(insidehdc, TRANSPARENT);

	// 修改字型
    HFONT hfont = CreateFont (	control->banner.fontHeight,	// 字型的高度 單位px
								control->banner.fontWidth,	// 字型的寬度 單位px
								0,							// 字型的傾斜角
								0,							// 字型的傾斜角
								FW_BOLD,					// 字型的粗細
								0,							// 是否爲斜體
								0,							// 是否有下劃線
								0,							// 是否有刪除線
								DEFAULT_CHARSET,			// 使用的字元集
								OUT_DEFAULT_PRECIS,			// 指定如何選擇字型
								CLIP_DEFAULT_PRECIS,		// 確定剪裁的精度
								DRAFT_QUALITY,				// 如何與選擇的字型符合
								FIXED_PITCH | FF_SWISS,		// 間距標誌和屬性標誌
								TEXT("Fixedays")			// 字型的名字
							  );
	// 選用字型
	SelectObject(insidehdc, hfont);
	// 系統標題
	PrintfLabel(insidehdc, &control->banner, RGB(0, 180, 0));

	// 選單
	hfont = CreateFont(control->menus[0].fontHeight, control->menus[0].fontWidth, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DRAFT_QUALITY, FIXED_PITCH | FF_SWISS, TEXT("Fixedays"));
	SelectObject(insidehdc, hfont);
	for (int i = 0; i < MENU_SIZE; ++i)
	{
		PrintfMenu(insidehdc, &control->menus[i], RGB(0, 0, 136 - i * 16));
	}

	// 釋放系統資源
	DeleteObject(hfont);
}
void OnPaint(HDC hdc, CONTROL *control)
{
	// 建立相容性DC(記憶體DC)
    HDC insidehdc = CreateCompatibleDC(hdc);
    // 建立相容性點陣圖(就像一張複印紙)
	HBITMAP hbitmapback = CreateCompatibleBitmap(hdc, control->width, control->height);
	// 將DC與點陣圖系結在一起
	SelectObject(insidehdc, hbitmapback);

	PrintAllSqare(insidehdc, control);
	PrintAllLabel(insidehdc, control);

	// 將內容傳遞到視窗DC
	BitBlt(hdc, 0, 0, control->width, control->height, insidehdc, 0, 0, SRCCOPY);

	// 釋放系統資源 
	DeleteObject(hbitmapback);
	DeleteDC(insidehdc);
}

回撥函數:

LRESULT CALLBACK CallBack(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
	static CONTROL control;

	switch (nMsg)
	{
	case WM_CREATE:
		InitControl(&control);
		break;

	case WM_PAINT:
	{
		PAINTSTRUCT pt;
		OnPaint(BeginPaint(hWnd, &pt), &control);
		EndPaint(hWnd, &pt);
		break;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	}
	return DefWindowProc(hWnd, nMsg, wParam, lParam);
}

執行結果:
在这里插入图片描述
雖然介面很醜,但是我們終於看到了不一樣的(不同於控制檯)的介面。
有了這個介面我們可以配合滑鼠做很多事情。

這裏的一系列繪圖函數都是套路,先在記憶體中繪製可以減少螢幕閃爍。

3、處理滑鼠單機事件

我們處理滑鼠左擊事件,根據滑鼠的座標和選單的位置執行對應的功能。

typedef void(*MENUFUNCTION)(HWND hWnd, CONTROL *control);

void MenuFun1(HWND hWnd, CONTROL *control);
void MenuFun2(HWND hWnd, CONTROL *control);
void MenuFun3(HWND hWnd, CONTROL *control);
void MenuFun4(HWND hWnd, CONTROL *control);
void MenuFun5(HWND hWnd, CONTROL *control);
void MenuFun6(HWND hWnd, CONTROL *control);
void MenuFun7(HWND hWnd, CONTROL *control);
void MenuFun8(HWND hWnd, CONTROL *control);
void MenuFun9(HWND hWnd, CONTROL *control);
void MenuFun10(HWND hWnd, CONTROL *control);

const MENUFUNCTION MENUFUN[MENU_SIZE] =
{
	MenuFun1, MenuFun2, MenuFun3, MenuFun4, MenuFun5,
	MenuFun6, MenuFun7, MenuFun8, MenuFun9, MenuFun10
};

void MenuFun1(HWND hWnd, CONTROL *control)
{
	MessageBox(hWnd, "fun1", "提示", MB_OK);
}

有些menu還沒有分配功能,函數體直接空着就行了。

// 點是否在方塊內
_Bool IsInLabel(POINT mouse, LABEL *label)
{
	return label->isShow && mouse.x >= label->left && mouse.x <= label->right && mouse.y >= label->top && mouse.y <= label->bottom;
}

void OnLeftMouseDowm(HWND hWnd, CONTROL *control, POINT mouse)
{
	// 選單
	for (int i = 0; i < MENU_SIZE; ++i)
	{
		if (IsInLabel(mouse, &control->menus[i]))
		{
			MENUFUN[i](hWnd, control);
		}
	}
}

回撥函數中呼叫

case WM_LBUTTONDOWN:
	{
		POINT mouse;
		mouse.x = LOWORD(lParam);
		mouse.y = HIWORD(lParam);
		OnLeftMouseDowm(hWnd, &control, mouse);
		break;
	}

執行結果:
在这里插入图片描述

4、新增日誌

爲什麼要新增日誌呢,整點活。。。可以幫助偵錯(雖然現在用不到),而且現在沒有控制檯還有點想念,可以將日誌當成我們以前的控制檯。。。
EasyLogger gitee地址
在这里插入图片描述
找到例子裏面的windows,下面 下麪那個mian.c是使用例子。
把easylogger資料夾裏面的程式拷貝到工程裏面。
還有下面 下麪幾個檔案也要拷貝:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
按照給的例子編寫個測試:

// 初始化日誌
void InitLog()
{
	elog_init();

	elog_set_fmt(ELOG_LVL_ASSERT, ELOG_FMT_ALL);
	elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
	elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
	elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
	elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~ELOG_FMT_FUNC);
	elog_set_fmt(ELOG_LVL_VERBOSE, ELOG_FMT_ALL & ~ELOG_FMT_FUNC);

	elog_start();
}

void MenuFun1(HWND hWnd, CONTROL *control)
{
	log_a("Hello EasyLogger!");
	log_e("Hello EasyLogger!");
	log_w("Hello EasyLogger!");
	log_i("Hello EasyLogger!");
	log_d("Hello EasyLogger!");
	log_v("Hello EasyLogger!");
	MessageBox(hWnd, "fun1", "提示", MB_OK);
}

執行結果:
在这里插入图片描述
在这里插入图片描述
這裏稍微說一下這個日誌

  1. 輸出等級(低於這個等級的日誌纔會輸出,由ELOG_OUTPUT_LVL宏控制)
    在这里插入图片描述
    1. [A]:斷言(Assert)(ELOG_LVL_ASSERT)
    2. [E]:錯誤(Error)(ELOG_LVL_ERROR)
    3. [W]:警告(Warn)(ELOG_LVL_WARN)
    4. [I]:資訊(Info)(ELOG_LVL_INFO)
    5. [D]:偵錯(Debug)(ELOG_LVL_DEBUG)
    6. [V]:詳細(Verbose)(ELOG_LVL_VERBOSE)
  2. 外掛(我們這裏選擇的是檔案)
    1. 終端:方便使用者動態檢視,不具有儲存功能;
    2. 檔案與Flash:都具有儲存功能,使用者可以檢視歷史日誌。但是檔案方式需要檔案系統的支援,而Flash方式更加適合應用在無檔案系統的小型嵌入式裝置中。
  3. 自定義輸出資訊:如elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);可以設定錯誤輸出格式,而類似於其中時間格式的修改就需修改下面 下麪函數的返回值
    在这里插入图片描述
  4. 儲存檔名
    在这里插入图片描述

如果有其他需要用到的功能請看文件我這邊就不一一列舉了。
忽然發現原始檔多了起來,給他們分一下類吧:
在这里插入图片描述
日誌:
在这里插入图片描述
用戶介面:
在这里插入图片描述
數據庫:
在这里插入图片描述
工具:
在这里插入图片描述
學生:
在这里插入图片描述

5、替換掉所有控制檯有關函數(輸入)

列印轉碼失敗改成輸入到日誌中。
顯示選單、列印無指令、暫停也不需要了。
列印提示資訊從printf改成MessageBox
讀取資訊我們要寫個對話方塊(這裏用控制元件拖拽吧想念QT)
新增一個對話方塊資源:
在这里插入图片描述
在这里插入图片描述
找到旁邊工具箱拖拽幾個控制元件上來(Static Text顯示文字 Edit Control接收輸入):
在这里插入图片描述
右擊控制元件修改提示內容:
在这里插入图片描述
修改ID:
在这里插入图片描述
輸入學號那個修改爲只接收數位:
在这里插入图片描述
視窗居中顯示:
在这里插入图片描述
最後做出對話方塊:
在这里插入图片描述
然後開啓系統生成的資源標頭檔案,系統預設名字可以刪掉了:
在这里插入图片描述
接下來開始編寫獲取使用者輸入(分數不輸入預設爲0):

HINSTANCE g_hInstance;

g_hInstance = hInstance;

#define CANCLE -1
#include "gui.h"
#include "resource.h"

extern HINSTANCE g_hInstance;
STUNODE *g_Node;

BOOL CALLBACK InputStuProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam);

///////////////////////////////////////////////////

void InputStuDialog(HWND hWnd, STUNODE *node)
{
	g_Node = node;
	DialogBox(g_hInstance, MAKEINTRESOURCE(IDD_DIALOG_INPUT_STU), hWnd, InputStuProc);
}

///////////////////////////////////////////////////

// 判斷字串是否全爲數位
int IsDigitstr(char *str)
{
	return (strspn(str, "0123456789") == strlen(str));
}

// 判斷分數是否符合要求
_Bool IsScoreInRange(int score)
{
	return score >= 0 && score <= 100;
}

_Bool InputNumber(HWND hWnd)
{
	GetDlgItemText(hWnd, IDC_EDIT_NUMBER, g_Node->number, NUMBER_SIZE);
	if (0 == strlen(g_Node->number))
	{
		MessageBox(hWnd, "學號不能爲空", "提示", MB_OK);
		return FALSE;
	}

	if (TRUE != IsDigitstr(g_Node->number))
	{
		MessageBox(hWnd, "學號必須全爲數位", "提示", MB_OK);
		return FALSE;
	}

	return TRUE;
}

_Bool InputName(HWND hWnd)
{
	GetDlgItemText(hWnd, IDC_EDIT_NAME, g_Node->name, NAME_SIZE);
	if (0 == strlen(g_Node->name))
	{
		MessageBox(hWnd, "學號不能爲空", "提示", MB_OK);
		return FALSE;
	}

	return TRUE;
}

_Bool InputScore(HWND hWnd)
{
	g_Node->score = GetDlgItemInt(hWnd, IDC_EDIT_SCORE, NULL, TRUE);
	if (TRUE != IsScoreInRange(g_Node->score))
	{
		MessageBox(hWnd, "輸入分數要在0~100之間", "提示", MB_OK);
		return FALSE;
	}

	return TRUE;
}

BOOL CALLBACK InputStuProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
	switch (nMsg)
	{
	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case IDOK:
			if (InputNumber(hWnd) && InputName(hWnd) && InputScore(hWnd))
			{
				EndDialog(hWnd, wParam);
				return TRUE;
			}
			break;
		case IDCANCEL:
			g_Node->score = CANCLE;
			EndDialog(hWnd, wParam);
			return TRUE;
		default:
			return FALSE;
		}
		break;
	case WM_CLOSE:
		EndDialog(hWnd, wParam);
		return TRUE;
	}

	return FALSE;
}

測試一波:

void MenuFun1(HWND hWnd, CONTROL *control)
{
	STUNODE node;
	InputStuDialog(hWnd, &node);

	if (CANCLE != node.score)
	{
		char buffer[BUFFER_SIZE];
		StringCchPrintf(buffer, BUFFER_SIZE, "%s %s %d", node.number, node.name, node.score);
		MessageBox(hWnd, buffer, "提示", MB_OK);
	}
}

執行結果:
在这里插入图片描述
在这里插入图片描述

6、編寫功能(插入和顯示全部)

新建一個顯示的視窗,新增控制元件(其他過程就跳過了):
在这里插入图片描述
在这里插入图片描述

void MenuFun5(HWND hWnd, CONTROL *control)
{
	ShowStuDialog();
}
#include "gui.h"
#include "resource.h"
#include <commctrl.h>

extern HINSTANCE g_hInstance;
HWND g_hWnd;
HWND g_hListCtrl;

BOOL CALLBACK ShowStuProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam);
void InsertColumn(HWND hwnd, int iSubItem, int cx, char *text, int cchTextMax, int len);
void AddData(HWND hwnd, char *text, int x, int y);

///////////////////////////////////////////////////

void ShowStuDialog()
{
	if (NULL == g_hWnd)
	{
		CreateDialog(g_hInstance, MAKEINTRESOURCE(IDD_DIALOG_SHOW_STU), NULL, ShowStuProc);
	}
	ShowWindow(g_hWnd, SW_SHOW);
}

void HideStuDialog()
{
	ShowWindow(g_hWnd, SW_HIDE);
}

///////////////////////////////////////////////////

// 插入列函數
void InsertColumn(HWND hwnd, int iSubItem, int cx, char *text, int cchTextMax, int len)
{
	LVCOLUMN ColInfo1 = { 0 };

	ColInfo1.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_FMT;
	ColInfo1.iSubItem = iSubItem;
	ColInfo1.fmt = LVCFMT_CENTER;
	ColInfo1.cx = cx;
	ColInfo1.pszText = (LPSTR)text;
	ColInfo1.cchTextMax = cchTextMax;

	ListView_InsertColumn(hwnd, (WPARAM)len, (LPARAM)&ColInfo1);
};

// 新增數據
void AddData(HWND hwnd, char *text, int x, int y)
{
	LVITEM item;

	item.mask = LVIF_TEXT;
	item.pszText = text;
	item.iItem = x;
	item.iSubItem = y;

	if (y == 0)
	{
		ListView_InsertItem(hwnd, &item);
	}
	else
	{
		ListView_SetItem(hwnd, &item);
	}
}

BOOL CALLBACK ShowStuProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
	switch (nMsg)
	{
	case WM_INITDIALOG:
	{
		g_hWnd = hWnd;
		g_hListCtrl = GetDlgItem(hWnd, IDC_LIST_STU);
		ListView_SetExtendedListViewStyle(g_hListCtrl, LVS_EX_FULLROWSELECT);

		InsertColumn(g_hListCtrl, 0, 115, "學號", 50, 0);
		InsertColumn(g_hListCtrl, 0, 115, "姓名", 50, 1);
		InsertColumn(g_hListCtrl, 0, 115, "成績", 50, 2);

		for (int i = 0; i < 50; ++i)
		{
			AddData(g_hListCtrl, "1111", i, 0);
			AddData(g_hListCtrl, "張三", i, 1);
			AddData(g_hListCtrl, "11", i, 2);
		}
		
		break;
	}
	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		default:
			return FALSE;
		}
		break;
	case WM_CLOSE:
		HideStuDialog();
		return TRUE;
	}

	return FALSE;
}

需要用DestroyWindow(g_hWnd);銷燬視窗。
執行結果:
在这里插入图片描述
開始編寫功能:
在主函數WM_CREATE中開啓數據庫,WM_DESTROY中關閉數據庫。
插入:

void MenuFun1(HWND hWnd, CONTROL *control)
{
	STUNODE node;
	InputStuDialog(hWnd, &node);

	if (CANCLE != node.score)
	{
		Insert(&node);
		MessageBox(hWnd, "插入成功", "提示", MB_OK);
	}
}

顯示全部:

/// 列
#define NUMBERCOL 0
#define NAMECOL 1
#define SCORECOL 2

// 列印回撥函數
int PrintfCallBack(void *data, int argc, char **argv, char **azColName)
{
	AddData(g_hListCtrl, argv[0], *(int*)data, NUMBERCOL);
	AddData(g_hListCtrl, Utf8ToGb2312(argv[1], strlen(argv[1])), *(int*)data, NAMECOL);
	AddData(g_hListCtrl, argv[2], *(int*)data, SCORECOL);
	++(*(int*)data);
	return 0;
}

void ShowAll()
{
	assert(db);

	char *selectSql = "SELECT NUMBER as '學號', NAME as '姓名', SCORE as '分數' FROM STU";

	int number = 0;
	ExecSql(selectSql, PrintfCallBack, &number);
}
void MenuFun5(HWND hWnd, CONTROL *control)
{
	ShowStuDialog();
	ShowAll();
}

執行結果:
在这里插入图片描述
在这里插入图片描述

7、編寫功能(查詢、刪除、修改)

新建輸入學號視窗:
在这里插入图片描述
查詢:

#include "gui.h"
#include "resource.h"

extern HINSTANCE g_hInstance;
STUNODE *g_Node;

BOOL CALLBACK InputNumberProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam);

///////////////////////////////////////////////////

void InputNumberDialog(HWND hWnd, STUNODE *node)
{
	g_Node = node;
	DialogBox(g_hInstance, MAKEINTRESOURCE(IDD_DIALOG_INPUT_NUMBER), hWnd, InputNumberProc);
}

///////////////////////////////////////////////////

_Bool InputNumberTwo(HWND hWnd)
{
	GetDlgItemText(hWnd, IDC_EDIT_NUMBER_TWO, g_Node->number, NUMBER_SIZE);
	if (0 == strlen(g_Node->number))
	{
		MessageBox(hWnd, "學號不能爲空", "提示", MB_OK);
		return FALSE;
	}

	if (TRUE != IsDigitstr(g_Node->number))
	{
		MessageBox(hWnd, "學號必須全爲數位", "提示", MB_OK);
		return FALSE;
	}

	return TRUE;
}

BOOL CALLBACK InputNumberProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
	switch (nMsg)
	{
	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case IDOK:
			if (InputNumberTwo(hWnd))
			{
				EndDialog(hWnd, wParam);
				return TRUE;
			}
			break;
		case IDCANCEL:
			g_Node->score = CANCLE;
			EndDialog(hWnd, wParam);
			return TRUE;
		default:
			return FALSE;
		}
		break;
	case WM_CLOSE:
		EndDialog(hWnd, wParam);
		return TRUE;
	}

	return FALSE;
}
void ShowOneStu(char **argv, char **azColName)
{
	static char buffer[BUFFER_SIZE];
	StringCchPrintf(buffer, BUFFER_SIZE, "%s:%15s  %s:%10s  %s:%3s\n", azColName[0], argv[0],
		azColName[1], Utf8ToGb2312(argv[1], strlen(argv[1])),
		azColName[2], argv[2]);

	MessageBox(NULL, buffer, "提示", MB_OK);
}

int FindCallBack(void *data, int argc, char **argv, char **azColName)
{
	++(*(int*)data);
	ShowOneStu(argv, azColName);
	return 0;
}

void MenuFun3(HWND hWnd, CONTROL *control)
{
	STUNODE node;
	InputNumberDialog(hWnd, &node);

	if (CANCLE != node.score && 0 == Find(&node))
	{
		ShowNoFind(hWnd);
	}
}

刪除:

void MenuFun2(HWND hWnd, CONTROL *control)
{
	STUNODE node;
	InputNumberDialog(hWnd, &node);

	if (CANCLE != node.score)
	{
		0 == Find(&node) ? ShowNoFind(hWnd) : Delete(&node);
	}
}

修改:

void MenuFun4(HWND hWnd, CONTROL *control)
{
	STUNODE node;
	InputNumberDialog(hWnd, &node);

	if (CANCLE != node.score)
	{
		if (0 == Find(&node))
		{
			ShowNoFind(hWnd);
		}
		else
		{
			STUNODE update;
			InputStuDialog(hWnd, &update);
			Modify(&node, &update);
		}
	}
}

介面就先做到這裏,接下來還有什麼好搞的?看着好像沒有登陸功能,仔細想起來目前還是單機版,不能聯網。

給例子新增伺服器

未完待續

百度雲鏈接

程式碼鏈接:https://pan.baidu.com/s/1KkfOmAh-Yn5DHiAir-wo1A
提取碼:zpoz
Navicat 百度雲鏈接:https://pan.baidu.com/s/10pGv0W4KqZASKaAv8nrXGA
提取碼:2lcr