admin管理员组文章数量:1544588
文章目录
- 前言
- 思路--用计算机“模拟世界”
- c语言
- 数据结构
- 算法
- 总结
- 例子--学生管理系统(控制台)
- 考虑
- 需求
- 分析
- 1、提供菜单
- 2、接收命令
- 3、添加学生信息
- 4、打印学生信息
- 5、查找学生
- 6、删除学生
- 7、修改学生信息
- 8、保存到文件
- 9、程序刚开始时加载学生信息
- 总结
- 给例子换上sqlite数据库
- 1、引入文件
- 2、创建数据库和表格
- 3、插入数据
- 4、打印学生信息
- 5、移除所有链表操作
- 查找(回调函数比打印多了个计数功能):
- 删除
- 修改
- 给例子换界面
- 1、创建窗口
- 2、制作菜单
- 3、处理鼠标单机事件
- 4、添加日志
- 5、替换掉所有控制台有关函数(输入)
- 6、编写功能(插入和显示全部)
- 7、编写功能(查找、删除、修改)
- 给例子添加服务器
- 1、使用json
- 2、服务器客户端连接
- 3、处理请求返回数据
- 4、修改客户端发送请求(插入)
- 5、编写剩余功能
- 6、编写登陆功能
- 百度云链接
前言
回忆当年大一学习c语言的过程,两个礼拜看了一遍学校发的c语言书教材(照着书上的例子写了一遍代码),想写点什么的时候,发现脑子里好像并没有思路和想法,只会写点简单的例子。就像小学刚学完汉字怎么写,还是还不会写作文一样。
书上说c语言什么都能做,但我感觉什么都做不了。
我就在想是不是还有什么没学(学校的书很薄),于是我又拿起了谭浩强、c primer plus。。。
其实现在回想起来,看完学校教材的时候基础应该可以完成很多小程序了,但是少了点编程的思路和经验。
思路–用计算机“模拟世界”
首先我们说一下我们需要具备的计算机常识(我就按照我的理解说一下):
- 内存卡---------计算机的工作场所 用来存储数据 使用变量、指针等可以直接访问
- 硬盘------------计算机的数据仓库 用来 需要用fopen等文件操作
- 显示器、音响、网卡、键盘、鼠标等等我就不一一说了
- ASCII码-------一套对应关系,看着很简单,但是这个映射关系非常实用
- 二进制、八进制和十六进制
我们可以在内存卡或者硬盘里面存储数据,最小的数据1bit也就是存0/1,然后有1byte可以连续存储八个0/1,然后再组合。。。总结一下就是能存储n个连续0/1,有什么用呢?那用处可是相当大,n个连续0/1是二进制数据,我们把它转化为人们常用的十进制数据,可以用来数学计算、充当索引等。重点来了,充当索引能干嘛?举个例子八个0/1组合可以索引ASCII表的任何一项,然后0100 0001就代表了大写字母A,有些人可能会有疑问,这0100 0001怎么就是大写字母A了呢???那是因为我们人为的建立了一套关系(编码),可以理解为数学中的一 一映射,不同的是映射出来的不再是数值,而是我们自定义的信息。
人类为了观测世界,用编码表示世界,用数学计算世界。
c语言
然后我们对着c语言的目录来看思考一下我们学完基础到底掌握了着什么,这关系到我们能做什么。
- 数值类型数据就是映射的实数。字符型数据就是映射的ASCII码(我们可以使用别的编码比如说utf-8,就能映射中文了)。
- 使用printf等函数可以向控制台输出一些数据(ASCII码),这里我们使用的显示器。
- 使用scanf等函数可以获取键盘输入的一些数据,这里我们使用的键盘。
- 一些运算符可以让我们做一些数学方面的数据处理。
- 循环、分支、跳转、函数可以让我们做一些逻辑方面的数据处理。
- 数组、结构体等可以帮我们有规律的组织数据。
- 指针访问内存中的数据,文件操作访问硬盘中的数据。
- c语言标准库(输入输出操作、字符串操作、时间日期操作、内存操作等等):
- 第三方扩展库!!!“我不会造空调难道还不会吹空调嘛,我不会写的代码别人还不会写嘛”。我们初学者完全没必要因为显示一个字符就去吃透printf的代码,不知道printf是怎么写的,用还是会用的吧。其他功能也是类似的情况,不想看控制台想看图形界面,就用图形库,想联网,就用网络库。怎么用?看帮助文档 + 百度例子可解决问题。
大致回忆了一下我们手里掌握的信息,我们可以发现大部分都围绕着数据和数据处理来的。这是我们编程的重中之重!!!显示器、键盘等各种硬件也是通过发送接收数据和数据处理来控制的。而第三方扩展库也是通过函数调用获取改变数据,并且通过这些数据控制硬件的。
当我学了c语言后感觉突然失去了方向,因为在我眼前的分支太多了。问学长老师告诉我要去学数据结构和算法,我也不知道为什么要学就去学了,也没什么学习的重点(本人喜欢入门后用到什么学什么,不然我的兴趣可能会被磨灭殆尽)。
数据结构
我感觉数据结构主要是教你如何把现实世界的信息逻辑转换到计算机中,举个简单的例子:计算机中的存储空间呈现给我们使用的时候只有一维的,那二维数组是怎么来的?我们回忆一下二维数组a[2][1]
可以取第二行第一个的值,而我们计算机地址里面真的有几行几列吗?没有,只是我们人为认定的,也就是逻辑上它是这么存储的。
存储结构:
逻辑结构:
数据结构对应着数据的模拟。
算法
我感觉算法就是一个解决问题的步骤,算法会教你一步一步得到最终想要的结果,对应着数据处理。运算的定义是针对逻辑结构的,指出运算的功能,运算的实现是针对存储结构的,指出运算的具体操作步骤。我们编写的程序则是算法在计算机上的特定实现,在学习的时候最好先从算法解决的问题入手,会更好的了解这个算法。
总结
数据结构为算法服务,简单模拟一件事情就是记录关键信息,处理关键信息。c语言是一个实现的载体。
举个简单的例子:记录教室人流量。
首先我们需要通过某种方法让计算机感知到有人通过教室的门,然后我们在内存中存放一个数据,每当有一个人通过教室(逻辑处理),这个数据就+1(数据处理),最后对这个数据进行其他数据处理,生成人流量信息,显示到屏幕上。
(这里说明一下例子中的某种方法,我们目前用的计算机感知世界的方法不多,只有键盘、鼠标、麦克风等的输入,如果想通过别的方法感知世界,如找到当前的温度,我们就需要使用传感器等其他硬件,传感器通过某种方式把数据(二进制数据)发送给电脑,电脑才会知道。就拿键盘举例子,计算机是怎么知道你输入的信息的?当然是操作系统通过键盘传送来的数据索引键盘码表了。所以这某种方法不是我们初学计算机该操心的事情。)
从这个例子可以看出我们可以模拟一个事物(当然只会记录我们感兴趣的信息),模拟一件事情(目前科学能力所及),或者说我们可以找到计算要素,对计算要素进行表示,然后计算,总结一下最重要的一点:我们可以记录信息并且处理它。
例子–学生管理系统(控制台)
看理论会很枯燥,我甚至感觉有些东西就是写给已经会了的人看的,初学者看着就是受罪。反而找个教程例子实践一下可能就豁然开朗。
现在我们来一个经典的例子,就和hello world一样经典的学生管理系统。
我们需要制作一个通过键盘控制的信息管理系统,通过增删改查学生数据给用户提供信息。
考虑
- 首先我们需要思考一下我们要做什么,一开始写程序很容易犯的问题就是:需求不明确,走一步看一步。当你写了很多代码后突然发现我还有个功能没考虑,这可能会让你改很多很多地方。
- 明确需求后我们可以对着需求思考要保存哪些数据,这些数据有什么关系,是什么结构的,在这些数据上要采用什么算法(难一点的算法目前用不到,就算要用也只是调用第三方库,就像公式推导和使用一样,现在只要会用公式,具体如何推导公式甚至公式长什么样子都不需要太清楚,只要知道公式能带来什么就行了。如果数据结构很难为现有的算法服务就得全部重新考虑)。
- 一开始不会的时候临摹可能是最好的选择(按照我的脑子自己想出来可能不太现实)。所谓临摹也就是百度+CV大法,CV后就要记得这种套路了。
- 目前我们用不到第三方库,但是标准库是用得到的,使用这些库的时候我们主要要了解这些函数的参数、返回值、功能。出bug了可以百度百度是不是有版本问题。
- 这里我们只做32位debug的,因为库是要配置的。
需求
- 提供一个功能菜单给用户选择要执行的功能。(GUI)
- 接收用户输入的目录并且执行对应功能。(输入)
- 添加学生信息(增)
- 显示所有学生信息(GUI)
- 删除学生信息(删)
- 修改学生信息(改)
- 查找学生信息(查)
- 保存学生信息(IO操作)
- 程序刚开始时加载学生信息(IO操作)
- 退出系统
分析
- 单个学生信息采用结构体记录,多个学生之间采用简单的单链表连接(使用数组需要动态扩容,以后数据结构写vector的时候再搞)(无头结点单链表的一些简单增删改查就不赘述了)。
- 输入和GUI就分别用
scanf
和printf
。 - 增:使用
malloc
和指针。 - 删:使用
free
。 - 改:字符串操作。
- 查:字符串操作。
- 学生结构体以二进制形式保存在文件中。
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");
的意思:
*
表示读入某类型的内容,但是这个内容不保存到变量里,所以后面不需要对应的参量。%*[^\n]
表示读入除了回车之外的字符以及读入一个字符后不保存。[]
内是只读入限定读入的字符,如[abcd]
指的是只读入abcd
的字符。- 整行代码的解释是
%*[^\n]
首先读入缓冲区的剩余内容,%*c
读入最后一个没有读入的回车,这样就清空了输入缓冲区。
循环最后的order = -1;
是为了解决我们写的另一个bug:我先运行了功能1,这个时候order
的值就是1,然后输入了错误的指令,order
的值不变,还是会运行功能1(错误的输入真的始料未及)。
这样的话我们就不能通过循环条件退出系统了,所以直接使用exit
函数退出。现在的while
和order
已经没有关系了,改成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
这类的。StringCchPrintf
是sprintf
的一个替代品。
// 读取字符串
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
// NULL代表没找到,FIRST_NODE即第一个节点,因为第一个节点没有先驱,要特殊处理。
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
等代码前肯定是要打开数据库的,如果没打开或者打开失败还让程序运行下去就是程序员的问题了,不应该让程序运行到这个地方。(这里我们可以换成if
判断数据库为空指针,加一个数据库打开失败提醒用户并且直接结束程序)
再看看sqlite3_exec
的callback
参数是不是和我们前面的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语句,再看看攻略上写的一般格式:
可以看出我们主要要写数据库名字、表名字、每个字段名字类型。AUTOINCREMENT
是ID
自动增长的意思,这样我们插入的时候就不用插入ID
了。
运行程序,用可视化数据库软件打开db文件康康:
我们代码里面没写数据库名字可以看到默认是main
,表名字、每个字段名字类型一套搞下来像不像定义一个结构体?
查看的软件是Navicat,希望大家能支持正版。。。
3、插入数据
我们写的list也可以逐渐被数据库取代,以后取数据就直接去数据库取,内存中的可以不用了。
现在SaveAll
和ReadAll
可以删除了,保存学生信息到文件这项菜单可以删掉了,因为我们不用手动做这件事情,交给数据库了。
file.c
和file.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视频攻略链接
一些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);
}
运行结果:
这里稍微说一下这个日志
- 输出等级(低于这个等级的日志才会输出,由
ELOG_OUTPUT_LVL
宏控制)
0. [A]:断言(Assert)(ELOG_LVL_ASSERT)- [E]:错误(Error)(ELOG_LVL_ERROR)
- [W]:警告(Warn)(ELOG_LVL_WARN)
- [I]:信息(Info)(ELOG_LVL_INFO)
- [D]:调试(Debug)(ELOG_LVL_DEBUG)
- [V]:详细(Verbose)(ELOG_LVL_VERBOSE)
- 插件(我们这里选择的是文件)
- 终端:方便用户动态查看,不具有存储功能;
- 文件与Flash:都具有存储功能,用户可以查看历史日志。但是文件方式需要文件系统的支持,而Flash方式更加适合应用在无文件系统的小型嵌入式设备中。
- 自定义输出信息:如
elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
可以设置错误输出格式,而类似于其中时间格式的修改就需修改下面函数的返回值
- 保存文件名
如果有其他需要用到的功能请看文档我这边就不一一列举了。
忽然发现源文件多了起来,我还是喜欢链接库,不然工程文件属实多,给他们分一下类吧:
日志:
用户界面:
数据库:
工具:
学生:
5、替换掉所有控制台有关函数(输入)
打印转码失败改成输入到日志中。
显示菜单、打印无指令、暂停也不需要了。
打印提示信息从printf
改成MessageBox
。
读取信息我们要写个对话框(这里用控件拖拽吧挺想念QT的)
添加一个对话框资源:
找到旁边工具箱拖拽几个控件上来(Static Text显示文字 Edit Control接收输入):
右击控件修改提示内容:
修改ID:
输入学号那个修改为只接收数字:
窗口居中显示:
最后做出对话框:
然后打开系统生成的资源头文件,系统默认名字(红色框出来的)可以删掉了:
接下来开始编写获取用户输入(分数不输入默认为0)
main函数中保存实例句柄到全局变量:
HINSTANCE g_hInstance;
g_hInstance = hInstance;
#define CANCLE -1
编写dialog显示逻辑:
#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创建对话框
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获取控件内容
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);
}
}
}
界面就先做到这里,接下来还有什么好搞的?看着好像没有登陆功能,仔细想起来目前还是单机版,不能联网。
给例子添加服务器
首先我们来思考一下我们联网干嘛?要服务器干什么?
联网是为了在不同的电脑上,不同的地区获取相同的数据,所以我们的服务器就是要给我们提供数据。
那我们获取数据以后要办?有人说这还不简单直接char*
伺候,然后strcmp比较就完事了。这边有个问题:获取到的数据没有结构,就和我们处理文件一样,文件里面想怎么存就怎么存,操作起来很不方便。
1、使用json
我们这边使用json来格式化我们的数据:cjson源码
因为比较简单,我们只需要.c和.h文件,下面有test文件教你如何使用。
首先我们需要解析json字符串变成我们的结构体变量。
为了方便观看我服务器也做成窗口的了,而且界面也不改了。
我们做一个json可视化的窗口:
这三个都选TRUE效果好看点方便代表的style:
- TVS_HASLINES 在父/子结点之间绘制连线
- TVS_LINESATROOT 在根/子结点之间绘制连线
- TVS_HASBUTTONS 在每一个结点前添加一个按钮,用于表示当前结点是否已被展开
编写窗口代码:
#include "gui.h"
#include "resource.h"
extern HINSTANCE g_hInstance;
HWND g_hWndJson;
HWND g_hTreeCtrl;
BOOL CALLBACK ShowJsonProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam);
///
void ShowJsonDialog()
{
if (NULL == g_hWndJson)
{
CreateDialog(g_hInstance, MAKEINTRESOURCE(IDD_DIALOG_SHOW_JSON), NULL, ShowJsonProc);
}
ShowWindow(g_hWndJson, SW_SHOW);
}
void HideStuDialog()
{
TreeView_DeleteAllItems(g_hTreeCtrl);
ShowWindow(g_hWndJson, SW_HIDE);
}
///
HTREEITEM AddTreeNode(HWND hWnd, HTREEITEM hParent, char* pStrText)
{
TVINSERTSTRUCT TvInsertItem;
TvInsertItem.hParent = hParent;
TvInsertItem.hInsertAfter = TVI_LAST;
TvInsertItem.item.mask = TVIF_TEXT;
TvInsertItem.item.pszText = pStrText;
return TreeView_InsertItem(hWnd, &TvInsertItem);
}
BOOL CALLBACK ShowJsonProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
switch (nMsg)
{
case WM_INITDIALOG:
{
g_hWndJson = hWnd;
g_hTreeCtrl = GetDlgItem(hWnd, IDC_TREE_JSON);
break;
}
case WM_COMMAND:
switch (LOWORD(wParam))
{
default:
return FALSE;
}
break;
case WM_CLOSE:
HideStuDialog();
return TRUE;
}
return FALSE;
}
对比一下上面的List Control,使用方法几乎一致。
#include "json.h"
#include "elog.h"
#include "cJSON.h"
#include "gui.h"
#include <strsafe.h>
extern HWND g_hTreeCtrl;
extern HWND g_hWndJson;
#define BUFFER_SIZE 1024
char buffer[BUFFER_SIZE];
char *DoubleToString(double value);
void ShowJsonNode(HTREEITEM item, cJSON *json);
HTREEITEM ShowJsonOne(HTREEITEM item, char *text, char *format1, char *format2, char *string);
void ShowJsonList(HTREEITEM item, cJSON *json, char *start, char *end);
#define ShowJsonNull(item, json) ShowJsonOne(item, "null", "null", "\"%s\":null", json->string)
#define ShowJsonFalse(item, json) ShowJsonOne(item, "false", "false", "\"%s\":false", json->string)
#define ShowJsonTrue(item, json) ShowJsonOne(item, "true", "true", "\"%s\":true", json->string)
#define ShowJsonNumber(item, json) ShowJsonOne(item, DoubleToString(json->valuedouble), "%s", "\"%s\":%s", json->string)
#define ShowJsonString(item, json) ShowJsonOne(item, json->valuestring, "\"%s\"", "\"%s\":\"%s\"", json->string)
#define ShowJsonArray(item, json) ShowJsonList(item, json, "[", "]")
#define ShowJsonObject(item, json) ShowJsonList(item, json, "{", "}")
///
void ShowJson(char *text)
{
cJSON *json = cJSON_Parse(text);
if (!json)
{
log_e("cJSON_Parse Error, error msg:[%s]", cJSON_GetErrorPtr());
}
ShowJsonNode(NULL, json);
cJSON_Delete(json);
}
///
char *DoubleToString(double value)
{
static char buffer[20];
memset(buffer, 0, 20);
StringCchPrintf(buffer, 20, "%f", value);
return buffer;
}
void ShowJsonNode(HTREEITEM item, cJSON *json)
{
switch ((json->type) & 255)
{
case cJSON_NULL: ShowJsonNull(item, json); break;
case cJSON_False: ShowJsonFalse(item, json); break;
case cJSON_True: ShowJsonTrue(item, json); break;
case cJSON_Number: ShowJsonNumber(item, json); break;
case cJSON_String: ShowJsonString(item, json); break;
case cJSON_Array: ShowJsonArray(item, json); break;
case cJSON_Object: ShowJsonObject(item, json); break;
}
}
// 显示一个节点
HTREEITEM ShowJsonOne(HTREEITEM item, char *text, char *format1, char *format2, char *string)
{
memset(buffer, 0, BUFFER_SIZE);
string == NULL ? StringCchPrintf(buffer, BUFFER_SIZE, format1, text) : StringCchPrintf(buffer, BUFFER_SIZE, format2, string, text);
return AddTreeNode(g_hTreeCtrl, item, buffer);
}
// 显示一个链表的节点
void ShowJsonList(HTREEITEM item, cJSON *json, char *start, char *end)
{
HTREEITEM root = ShowJsonOne(item, start, "%s", "\"%s\":%s", json->string);
cJSON *chile = json->child;
while (chile != NULL)
{
ShowJsonNode(root, chile);
chile = chile->next;
}
AddTreeNode(g_hTreeCtrl, item, end);
}
这里可以通过修改format
参数修改字符串显示格式。
这代码我没有测试多大的json数据显示会卡顿,但是应该不会太大因为是一次性加载全部,而且有大量的函数调用。
测试一波:
void MenuFun1(HWND hWnd, CONTROL *control)
{
ShowJsonDialog();
ShowJson("{\"success\": true, \"code\" : 1, \"msg\" : \"success\", \"module\": [ { \"name\": \"张三\", \"number\" : 11111, \"score\" : 80 }, { \"name\": \"李四\", \"number\" : 22222, \"score\" : 70 }] }");
}
网上的运行效果:
运行结果:
我觉得吧换个字体换个背景是不是就和网上的差不多了?emm 以后直接整个换肤的插件吧。
我们还需要将我们的结构体变量变成字符串,毕竟要传输嘛。
定义一个结构体:
#define SUCCESS 1
#define MSG_SUCCESS "success"
typedef struct
{
_Bool success; // 是否调用以及过参数校验
int code; // 业务code
char *msg; // 业务消息
void *module; // 返回值
} Result;
#define SETRESULT(result, bSuccess, iCode, pszMsg, pModule) do { \
result.success = bSuccess; \
result.code = iCode; \
result.msg = pszMsg; \
result.module = pModule; \
} while(0)
这个是我从某个springboot项目上copy的,module
原来是个泛型来着,这里只能用void*
了,success
如果为false
,code
和msg
就是错误信息。success
如果为true
,module
就是具体要得到的值。
char* StuToJson(Result *result, int size)
{
cJSON *module, *stu, *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "success", result->success);
cJSON_AddNumberToObject(root, "code", result->code);
cJSON_AddStringToObject(root, "msg", result->msg);
cJSON_AddItemToObject(root, "module", module = cJSON_CreateArray());
STUNODE *nodes = result->module;
for (int i = 0; i < size; ++i)
{
stu = cJSON_CreateObject();
cJSON_AddStringToObject(stu, "number", nodes[i].number);
cJSON_AddStringToObject(stu, "name", nodes[i].name);
cJSON_AddNumberToObject(stu, "score", nodes[i].score);
cJSON_AddItemToArray(module, stu);
}
char *out = cJSON_Print(root);
cJSON_Delete(root);
return out;
}
构建代码参照的源码例子。
测试一波:
void MenuFun2(HWND hWnd, CONTROL *control)
{
STUNODE stu[2] = { {"张三", "11111", 80}, {"李四", "22222", 90} };
Result result;
SETRESULT(result, TRUE, SUCCESS, MSG_SUCCESS, stu);
char *out = StuToJson(&result, 2);
free(out);
}
2、服务器客户端连接
我们照样还是使用现成的库:socket编程视频攻略链接
我们还是建一个List Control来显示信息:
#include "gui.h"
#include "resource.h"
#include <commctrl.h>
extern HINSTANCE g_hInstance;
HWND g_hWndLink;
HWND g_hListCtrl;
void InsertColumn(HWND hWnd, int iSubItem, int cx, char *text, int cchTextMax, int len);
void HideLinkDialog();
///
void ShowLinkDialog()
{
if (NULL == g_hWndLink)
{
CreateDialog(g_hInstance, MAKEINTRESOURCE(IDD_DIALOG_SHOW_LINK), NULL, ShowLinkProc);
}
ShowWindow(g_hWndLink, SW_SHOW);
}
///
// 插入列函数
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);
}
}
void HideLinkDialog()
{
ListView_DeleteAllItems(g_hListCtrl);
ShowWindow(g_hWndLink, SW_HIDE);
}
BOOL CALLBACK ShowLinkProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
switch (nMsg)
{
case WM_INITDIALOG:
{
g_hWndLink = hWnd;
g_hListCtrl = GetDlgItem(hWnd, IDC_LIST_LINK);
ListView_SetExtendedListViewStyle(g_hListCtrl, LVS_EX_FULLROWSELECT);
InsertColumn(g_hListCtrl, 0, 115, "ip", 50, IPCOL);
InsertColumn(g_hListCtrl, 0, 115, "port", 50, PORTCOL);
break;
}
case WM_COMMAND:
switch (LOWORD(wParam))
{
default:
return FALSE;
}
break;
case WM_CLOSE:
HideLinkDialog();
return TRUE;
}
return FALSE;
}
和前面显示学生操作几乎一模一样。
这里我们使用完成端口网络模型:
#include <Winsock2.h>
#include <mswsock.h>
#include <strsafe.h>
#include "net.h"
#include "elog.h"
#include "gui.h"
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "Mswsock.lib")
extern HWND g_hListCtrl;
#define MAX_COUNT 1024
#define MAX_RECV_COUNT 1024
HANDLE hPort; // 端口句柄
SOCKET g_allSock[MAX_COUNT]; // 所有的socket 第一个为服务器socket 其余的为客户端socket
OVERLAPPED g_allOlp[MAX_COUNT]; // 重叠结构
int g_count; // socket数量
HANDLE *pThread; // 线程数组首地址
int nProcessorsCount; // 线程数量
BOOL g_flag = TRUE; // 线程退出flag
char *ShortToString(u_short value);
DWORD WINAPI ThreadProc(LPVOID lpParameter);
int PostAccept();
int PostRecv(int index, char *buffer);
int PostSend(int index, char *json);
///
void OpenNet()
{
// 打开网络库
WORD wdVersion = MAKEWORD(2, 2);
WSADATA wdScokMsg;
switch (WSAStartup(wdVersion, &wdScokMsg))
{
case WSASYSNOTREADY:
log_e("重启下电脑试试,或者检查网络库");
return;
case WSAVERNOTSUPPORTED:
log_e("请更新网络库");
return;
case WSAEINPROGRESS:
log_e("请重新启动");
return;
case WSAEPROCLIM:
log_e("请尝试关掉不必要的软件,以为当前网络运行提供充足资源");
return;
}
// 校验版本
if (2 != HIBYTE(wdScokMsg.wVersion) || 2 != LOBYTE(wdScokMsg.wVersion))
{
log_e("网络版本不对");
return;
}
// 创建服务器socket
SOCKET socketServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == socketServer)
{
log_e("WSASocket error:[%d]", WSAGetLastError());
return;
}
// 服务器socket放在数组第一个
g_allSock[g_count] = socketServer;
g_allOlp[g_count].hEvent = WSACreateEvent();
g_count++;
// 绑定网络端口和地址
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(PORT);
si.sin_addr.S_un.S_addr = inet_addr(ADDR);
if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr *)&si, sizeof(si)))
{
log_e("bind error:[%d]", WSAGetLastError());
return;
}
// 创建完成端口
hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (0 == hPort)
{
log_e("CreateIoCompletionPort error:[%d]", GetLastError());
return;
}
// 完成端口与socketServer绑定
HANDLE hPort1 = CreateIoCompletionPort((HANDLE)socketServer, hPort, 0, 0);
if (hPort1 != hPort)
{
log_e("CreateIoCompletionPort error:[%d]", GetLastError());
return;
}
// 开始监听
if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
{
log_e("listen error:[%d]", WSAGetLastError());
return;
}
PostAccept();
// 创建线程数量
SYSTEM_INFO systemProcessorsCount;
GetSystemInfo(&systemProcessorsCount);
nProcessorsCount = systemProcessorsCount.dwNumberOfProcessors;
// 创建线程
pThread = (HANDLE*)malloc(sizeof(HANDLE)*nProcessorsCount);
for (int i = 0; i < nProcessorsCount; i++)
{
pThread[i] = CreateThread(NULL, 0, ThreadProc, hPort, 0, NULL);
if (NULL == pThread[i])
{
log_e("CreateThread error:[%d]", GetLastError());
return;
}
}
log_i("打开网络成功");
}
void CloseNet()
{
// 释放线程句柄
for (int i = 0; i < nProcessorsCount; ++i)
{
CloseHandle(pThread[i]);
}
free(pThread);
// 释放socket和event
for (int i = 0; i < g_count; ++i)
{
if (0 == g_allSock[i])
{
continue;
}
closesocket(g_allSock[i]);
WSACloseEvent(g_allOlp[i].hEvent);
}
// 释放完成端口
if (0 != hPort)
{
CloseHandle(hPort);
}
// 清理网络库
WSACleanup();
log_i("关闭网络");
}
void ShowLink()
{
// 显示
struct sockaddr_in sa;
int len = sizeof(sa);
AddData(g_hListCtrl, "本机ip", 0, IPCOL);
AddData(g_hListCtrl, "本机port", 0, PORTCOL);
for (int i = 1; i < g_count; ++i)
{
if (g_allSock[i] != 0)
{
setsockopt(g_allSock[i], SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (char *)&g_allSock[0], sizeof(g_allSock[0]));
getpeername(g_allSock[i], (struct sockaddr*)&sa, &len);
AddData(g_hListCtrl, inet_ntoa(sa.sin_addr), i, IPCOL);
AddData(g_hListCtrl, ShortToString(ntohs(sa.sin_port)), i, PORTCOL);
}
else
{
AddData(g_hListCtrl, "null", i, IPCOL);
AddData(g_hListCtrl, "null", i, PORTCOL);
}
}
}
///
char *ShortToString(u_short value)
{
static char buffer[10];
memset(buffer, 0, 20);
StringCchPrintf(buffer, 20, "%d", value);
return buffer;
}
// 线程回调函数
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
HANDLE port = (HANDLE)lpParameter;
DWORD NumberOfBytes;
ULONG_PTR index;
LPOVERLAPPED lpOverlapped;
char strRecv[MAX_RECV_COUNT];
while (g_flag)
{
if (FALSE == GetQueuedCompletionStatus(port, &NumberOfBytes, &index, &lpOverlapped, INFINITE))
{
if (64 == GetLastError())
{
log_i("用户强制下线");
// 关闭
closesocket(g_allSock[index]);
WSACloseEvent(g_allOlp[index].hEvent);
// 从数组中删掉
g_allSock[index] = 0;
g_allOlp[index].hEvent = NULL;
}
continue;
}
/// 分类处理
// accept
if (0 == index)
{
// 绑定到完成端口
HANDLE hPort1 = CreateIoCompletionPort((HANDLE)g_allSock[g_count], hPort, g_count, 0);
if (hPort1 != hPort)
{
log_e("CreateIoCompletionPort error:[%d]", GetLastError());
closesocket(g_allSock[g_count]);
continue;
}
// 客户端投递recv
PostRecv(g_count, strRecv);
g_count++;
// 继续接收新的链接
PostAccept();
}
else
{
if (0 == NumberOfBytes) // 客户端下线
{
log_i("用户下线");
// 关闭
closesocket(g_allSock[index]);
WSACloseEvent(g_allOlp[index].hEvent);
// 从数组中删掉
g_allSock[index] = 0;
g_allOlp[index].hEvent = NULL;
}
else
{
if (0 != strRecv[0]) // recv
{
// 处理
log_i(strRecv);
PostSend(index, "收到了");
memset(strRecv, 0, sizeof(strRecv));
// 继续接收
PostRecv(index, strRecv);
}
else // send
{
// 发送完毕
}
}
}
}
return 0;
}
// 异步接收链接
int PostAccept()
{
g_allSock[g_count] = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
g_allOlp[g_count].hEvent = WSACreateEvent();
char str[1024] = { 0 };
DWORD dwRecvcount;
BOOL bRes = AcceptEx(g_allSock[0], g_allSock[g_count], str, 0, sizeof(struct sockaddr_in) + 16,
sizeof(struct sockaddr_in) + 16, &dwRecvcount, &g_allOlp[0]);
if (ERROR_IO_PENDING != WSAGetLastError())
{
return 1;
}
return 0;
}
// 异步接收信息
int PostRecv(int index, char *buffer)
{
WSABUF wsabuf;
wsabuf.buf = buffer;
wsabuf.len = MAX_RECV_COUNT;
DWORD dwRecvCount;
DWORD dwFlag = 0;
int nRes = WSARecv(g_allSock[index], &wsabuf, 1, &dwRecvCount, &dwFlag, &g_allOlp[index], NULL);
if (ERROR_IO_PENDING != WSAGetLastError())
{
return 1;
}
return 0;
}
// 异步发送信息
int PostSend(int index, char *json)
{
WSABUF wsabuf;
wsabuf.buf = json;
wsabuf.len = strlen(json);
DWORD dwSendCount;
DWORD dwFlag = 0;
int nRes = WSASend(g_allSock[index], &wsabuf, 1, &dwSendCount, dwFlag, &g_allOlp[index], NULL);
if (ERROR_IO_PENDING != WSAGetLastError())
{
return 1;
}
return 0;
}
这里socket管理很暴力,一共可以存储1024个socket,如果客户断开就置零,没有重复利用,没有边缘检测,只是简单的一个例子就做的简单点了。
有个小问题是如果没有setsockopt
函数,getpeername
会返回错误码:
工作流程(和普通的区别就是把同步的接收链接、发送接收消息全部投递给系统监管,并且使用线程池来同时处理请求,这样就不用傻等可以做别的事情):
测试客户端:
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main(void)
{
WORD wVersionRequird = MAKEWORD(2, 2);
WSADATA wdScokMsg;
int iRes = WSAStartup(wVersionRequird, &wdScokMsg);
switch (iRes)
{
case WSASYSNOTREADY:
printf("重启电脑试试,或者检查网络库");
return -1;
case WSAVERNOTSUPPORTED:
printf("请更新网络库");
return -1;
case WSAEPROCLIM:
printf("请尝试关掉不必要的软件,以为当前网络运行提供充足的资源");
return -1;
case WSAEINPROGRESS:
printf("请重新启动");
return -1;
}
// 校验版本
if (2 != HIBYTE(wdScokMsg.wVersion) || 2 != LOBYTE(wdScokMsg.wVersion))
{
printf("版本不存在");
WSACleanup();
return -1;
}
// 创建服务器socket
SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == socketServer)
{
printf("创建socket失败");
WSACleanup();
return -1;
}
// 连接服务器
SOCKADDR_IN sockAddress;
sockAddress.sin_family = AF_INET;
sockAddress.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddress.sin_port = htons(6666);
if (SOCKET_ERROR == connect(socketServer, (struct sockaddr*)&sockAddress, sizeof(sockAddress)))
{
printf("错误码:%d", WSAGetLastError());
closesocket(socketServer);
WSACleanup();
return -1;
}
// 发送消息给服务器
char szBuffer[1024];
while (1)
{
memset(szBuffer, 0, 1024);
printf("Input Something : ");
scanf_s("%s", szBuffer, 1024);
if ('0' == szBuffer[0])
{
break;
}
// 发送数据, 这个的话,跟服务器端的一样!
if (SOCKET_ERROR == send(socketServer, szBuffer, strlen(szBuffer) + 1, 0))
{
printf("send fail!");
closesocket(socketServer);
WSACleanup();
return -1;
}
memset(szBuffer, 0, 1024);
// 接收数据, 这个的话,跟服务器端的一样!
if (0 == recv(socketServer, szBuffer, sizeof(szBuffer), 0))
{
// 客户端正常关闭 服务端释放Socket
closesocket(socketServer);
}
else
{
// 接收到客户端消息
printf("server data : %s\n", szBuffer);
}
}
closesocket(socketServer);
WSACleanup();
system("pause");
return 0;
}
运行结果(手上就一台电脑,null代表客户端下线了,本机ip就不显示了使用getsockname
函数就行):
3、处理请求返回数据
看看代码突然发现有个问题就是我们的转码函数会被多个线程调用,但是共用一个缓冲区,看到了就修改一下(让使用者传递缓冲区):
void Transcode(char *in, char *out, size_t len, const char* tocode, const char* fromcode);
/* GBK就是GB2312扩展版本 GB18030是兼容前两种编码最全 而ANSI是指GB2312 */
void Gb2312ToUtf8(char *in, char *out, size_t len)
{
Transcode(in, out, len, "UTF-8//IGNORE", "GB18030");
}
void Utf8ToGb2312(char *in, char *out, size_t len)
{
Transcode(in, out, len, "GB18030", "UTF-8//IGNORE");
}
// 转码
void Transcode(char *in, char *out, size_t len, const char* tocode, const char* fromcode)
{
size_t outLen = len;
memset(out, 0, outLen);
iconv_t cd = iconv_open(tocode, fromcode);
if (iconv(cd, &in, &len, &out, &outLen) < 0)
{
log_w("转码失败");
}
iconv_close(cd);
}
在我们的线程处理函数ThreadProc
中添加路由处理:
我们需要在路由中处理客户的请求并且返回处理的结果PostSend
,POST 请求可能会导致新的资源的建立和/或已有资源的修改,GET请求只能只获取信息,这边类型就意思意思,目前没有也没关系。请求路径就按照表+函数名来定。
/// 请求类型
#define GET 1
#define POST 2
/// 请求路径
#define STU_INSERT "STU/Insert"
typedef struct
{
int type; // 请求类型
char path[PATH_SIZE]; // 请求路径
void *parameter; // 请求参数
} Request;
多几个返回类型:
// 参数非法
#define PARAMETER_INVALID 9001
#define MSG_PARAMETER_INVALID "parameter invalid"
// 数据库错误
#define DB_ERROR 9002
#define MSG_DB_ERROR "db error"
Request的json解析:
_Bool GetRequestType(char *text, Request *request)
{
cJSON *json = cJSON_Parse(text);
if (!json)
{
log_e("cJSON_Parse Error, error msg:[%s]", cJSON_GetErrorPtr());
return FALSE;
}
// 请求类型
cJSON *temp = cJSON_GetObjectItem(json, "type");
if (NULL == temp)
{
cJSON_Delete(json);
return FALSE;
}
request->type = temp->valueint;
cJSON_Delete(json);
return TRUE;
}
_Bool GetRequestPath(char *text, Request *request)
{
cJSON *json = cJSON_Parse(text);
if (!json)
{
log_e("cJSON_Parse Error, error msg:[%s]", cJSON_GetErrorPtr());
return FALSE;
}
// 请求路径
cJSON *temp = cJSON_GetObjectItem(json, "path");
if (NULL == temp || NULL == temp->valuestring)
{
cJSON_Delete(json);
return FALSE;
}
StringCchCopy(request->path, PATH_SIZE, temp->valuestring);
cJSON_Delete(json);
return TRUE;
}
_Bool GetRequestStu(char *text, Request *request, int *size)
{
cJSON *json = cJSON_Parse(text);
if (!json)
{
log_e("cJSON_Parse Error, error msg:[%s]", cJSON_GetErrorPtr());
return FALSE;
}
// 参数数组
cJSON *temp = cJSON_GetObjectItem(json, "parameter");
if (NULL == temp)
{
cJSON_Delete(json);
return FALSE;
}
// 计算大小
*size = cJSON_GetArraySize(temp);
// copy值
request->parameter = malloc(sizeof(STUNODE) * (*size));
STUNODE *stu = request->parameter;
cJSON *stuJson, *numberJson, *nameJson, *scoreJson;
for (int i = 0; i < *size; ++i)
{
stuJson = cJSON_GetArrayItem(temp, i);
numberJson = cJSON_GetObjectItem(stuJson, "number");
nameJson = cJSON_GetObjectItem(stuJson, "name");
scoreJson = cJSON_GetObjectItem(stuJson, "score");
if (NULL != numberJson && NULL != nameJson && NULL != scoreJson
&& NULL != numberJson->valuestring && NULL != nameJson->valuestring )
{
StringCchCopy(stu[i].number, NUMBER_SIZE, cJSON_GetObjectItem(stuJson, "number")->valuestring);
StringCchCopy(stu[i].name, NAME_SIZE, cJSON_GetObjectItem(stuJson, "name")->valuestring);
stu[i].score = scoreJson->valueint;
}
}
cJSON_Delete(json);
return TRUE;
}
路由请求类型:
void DealGET(int index, char *json)
{
Request request;
if (!GetRequestPath(json, &request))
{
log_w("请求路径错误:[%s]", json);
return;
}
log_w("get不存在此路径:[%s]", request.path);
}
路由请求路径:
void DealGET(int index, char *json)
{
Request request;
if (!GetRequestPath(json, &request))
{
log_w("请求路径错误:[%s]", json);
return;
}
log_w("get不存在此路径:[%s]", request.path);
}
void DealPOST(int index, char *json)
{
Request request;
if (!GetRequestPath(json, &request))
{
log_w("请求路径错误:[%s]", json);
return;
}
if (0 == strcmp(request.path, STU_INSERT))
{
int size = 0;
if (GetRequestStu(json, &request, &size))
{
Result *result = Insert(request.parameter);
char *out = StuToJson(result, 0);
PostSend(index, out);
free(out);
free(result);
free(request.parameter);
}
return;
}
log_w("post不存在此路径:[%s]", request.path);
}
插入函数也稍微修改一下:
_Bool ExecSql(char *sql, SQLEXECCALLBACK callback, void *data)
{
char *zErrMsg = NULL;
if (sqlite3_exec(db, sql, callback, data, &zErrMsg) != SQLITE_OK)
{
log_e("SQL 错误: [%s]", zErrMsg);
sqlite3_free(zErrMsg);
return FALSE;
}
else
{
return TRUE;
}
}
Result* Insert(STUNODE *node)
{
Result *result = (Result*)malloc(sizeof(Result));
// 参数检验
if (!IsDigitstr(node->number) || IsStringEmpty(node->number)
|| IsStringEmpty(node->name) || !IsScoreInRange(node->score))
{
SETRESULT(*result, FALSE, PARAMETER_INVALID, MSG_PARAMETER_INVALID, NULL);
return result;
}
// 执行逻辑
char insertSql[SQL_SIZE];
char buffer[SQL_SIZE];
StringCchPrintf(insertSql, SQL_SIZE, "INSERT INTO STU (NUMBER, NAME, SCORE) VALUES ('%s', '%s', %d);",
node->number, node->name, node->score);
Gb2312ToUtf8(insertSql, buffer, SQL_SIZE);
if (ExecSql(buffer, EmptyCallBack, NULL))
{
SETRESULT(*result, TRUE, SUCCESS, MSG_SUCCESS, NULL);
}
else
{
SETRESULT(*result, FALSE, DB_ERROR, MSG_DB_ERROR, NULL);
}
return result;
}
再用以前的测试客户端测试一下:
错误的(number应该为字符串类型):
正确的:
4、修改客户端发送请求(插入)
去掉客户端数据库部分内容。
json处理请求和返回,和服务器相反,要把请求对象转化为字符串,把返回字符串解析成对象:
char* StuToJson(Request *request, int size)
{
cJSON *module, *stu, *root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "type", request->type);
cJSON_AddStringToObject(root, "path", request->path);
cJSON_AddItemToObject(root, "parameter", module = cJSON_CreateArray());
STUNODE *nodes = request->parameter;
for (int i = 0; i < size; ++i)
{
stu = cJSON_CreateObject();
cJSON_AddStringToObject(stu, "number", nodes[i].number);
cJSON_AddStringToObject(stu, "name", nodes[i].name);
cJSON_AddNumberToObject(stu, "score", nodes[i].score);
cJSON_AddItemToArray(module, stu);
}
char *out = cJSON_Print(root);
cJSON_Delete(root);
return out;
}
_Bool GetResultMsg(char *text, Result *result)
{
cJSON *json = cJSON_Parse(text);
if (!json)
{
log_e("cJSON_Parse Error, error msg:[%s]", cJSON_GetErrorPtr());
return FALSE;
}
cJSON *success = cJSON_GetObjectItem(json, "success");
cJSON *code = cJSON_GetObjectItem(json, "code");
cJSON *msg = cJSON_GetObjectItem(json, "msg");
if (NULL == success || NULL == code || NULL == msg || NULL == msg->valuestring)
{
cJSON_Delete(json);
return FALSE;
}
result->success = success->type;
result->code = code->valueint;
switch (result->code)
{
case SUCCESS: result->msg = MSG_SUCCESS; break;
case PARAMETER_INVALID: result->msg = MSG_PARAMETER_INVALID; break;
case DB_ERROR: result->msg = MSG_DB_ERROR; break;
default: cJSON_Delete(json); return FALSE;
}
cJSON_Delete(json);
return TRUE;
}
网络处理流程比服务器简单,一次请求包含一个发送和一个接收:
SOCKET socketServer;
///
void OpenNet()
{
// 打开网络库
WORD wdVersion = MAKEWORD(2, 2);
WSADATA wdScokMsg;
switch (WSAStartup(wdVersion, &wdScokMsg))
{
case WSASYSNOTREADY:
log_e("重启下电脑试试,或者检查网络库");
return;
case WSAVERNOTSUPPORTED:
log_e("请更新网络库");
return;
case WSAEINPROGRESS:
log_e("请重新启动");
return;
case WSAEPROCLIM:
log_e("请尝试关掉不必要的软件,以为当前网络运行提供充足资源");
return;
}
// 校验版本
if (2 != HIBYTE(wdScokMsg.wVersion) || 2 != LOBYTE(wdScokMsg.wVersion))
{
log_e("网络版本不对");
return;
}
// 创建服务器socket
socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == socketServer)
{
log_e("创建socket失败");
return;
}
// 连接服务器
SOCKADDR_IN sockAddress;
sockAddress.sin_family = AF_INET;
sockAddress.sin_addr.s_addr = inet_addr(ADDR);
sockAddress.sin_port = htons(PORT);
if (SOCKET_ERROR == connect(socketServer, (struct sockaddr*)&sockAddress, sizeof(sockAddress)))
{
log_e("connect error:[%d]", WSAGetLastError());
return;
}
log_i("已连接服务器");
}
void CloseNet()
{
if (INVALID_SOCKET == socketServer)
{
closesocket(socketServer);
}
WSACleanup();
log_i("与服务器断开连接");
}
Result SendRequest(Request *request)
{
// 发送请求
char *out = StuToJson(request, 1);
if (SOCKET_ERROR == send(socketServer, out, strlen(out) + 1, 0))
{
log_e("send失败,[%s]", WSAGetLastError());
}
free(out);
// 接收返回值
char buffer[1024];
if (0 == recv(socketServer, buffer, sizeof(buffer), 0))
{
log_e("recv失败,[%s]", WSAGetLastError());
}
Result result;
if (!GetResultMsg(buffer, &result))
{
return (Result) {FALSE, PARSE_ERROR, MSG_PARSE_ERROR, NULL};
}
return result;
}
修改insert功能:
#define SETREQUEST(request, iType, pszPath, pParameter) do { \
(request).type = iType; \
StringCchPrintf(request.path, PATH_SIZE, "%s", pszPath); \
(request).parameter = pParameter; \
} while(0)
void MenuFun1(HWND hWnd, CONTROL *control)
{
STUNODE node;
InputStuDialog(hWnd, &node);
if (CANCLE != node.score)
{
Request request;
SETREQUEST(request, POST, STU_INSERT, &node);
Result result = SendRequest(&request);
result.success ? MessageBox(hWnd, "插入成功", "提示", MB_OK) : log_e("插入失败,[%s]", result.msg);
}
}
5、编写剩余功能
在写新的代码前先改一下代码结构(一部分经常出现代码):
因为我们的结构体中前三个是固定的所以没必要每次都写:
// 解析Result确定的部分
cJSON* PraseResultConfirmNode(Result *result, cJSON **module)
{
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "success", result->success);
cJSON_AddNumberToObject(root, "code", result->code);
cJSON_AddStringToObject(root, "msg", result->msg);
if (NULL == module)
{
cJSON_AddItemToObject(root, "module", cJSON_CreateArray());
}
else
{
cJSON_AddItemToObject(root, "module", *module = cJSON_CreateArray());
}
return root;
}
// 对象解析为字符串 并且释放对象
char* ObjectToJson(cJSON *root)
{
char *out = cJSON_Print(root);
cJSON_Delete(root);
return out;
}
char* NullToJson(Result *result)
{
return ObjectToJson(PraseResultConfirmNode(result, NULL));
}
这部分申请检测释放每次都出现:
_Bool UseParse(char *text, JSONCALLBACK fun, Request *request, int *size)
{
cJSON *json = cJSON_Parse(text);
if (!json)
{
log_e("cJSON_Parse Error, error msg:[%s]", cJSON_GetErrorPtr());
return FALSE;
}
_Bool result = fun(json, request, size);
cJSON_Delete(json);
return result;
}
_Bool GetRequestType(cJSON *json, Request *request, int *size)
{
// 请求类型
cJSON *temp = cJSON_GetObjectItem(json, "type");
if (NULL == temp)
{
return FALSE;
}
request->type = temp->valueint;
return TRUE;
}
其余部分类似。
服务器查找:
// 检查stu节点所有的值
_Bool CheckStuAll(STUNODE *node)
{
return !IsDigitstr(node->number) || IsStringEmpty(node->number) || IsStringEmpty(node->name) || !IsScoreInRange(node->score);
}
void Find(STUNODE *node, Result *result)
{
result->module = (STUNODE*)malloc(sizeof(STUNODE));
if (CheckStuNumber(node))
{
SETRESULT(*result, FALSE, PARAMETER_INVALID, MSG_PARAMETER_INVALID, NULL);
return;
}
STUNODE *stu = result->module;
stu->score = -1;
char selectSql[SQL_SIZE];
GetFindSql(node, selectSql);
if (ExecSql(selectSql, FindCallBack, stu))
{
if (-1 != stu->score)
{
SETRESULT(*result, TRUE, SUCCESS, MSG_SUCCESS, stu);
}
else
{
SETRESULT(*result, FALSE, NO_SUCH_STU, MSG_NO_SUCH_STU, NULL);
}
}
else
{
SETRESULT(*result, FALSE, DB_ERROR, MSG_DB_ERROR, NULL);
}
}
路由处理:
if (0 == strcmp(request.path, STU_FIND))
{
int size = 0;
if (UseParse(json, GetRequestStu, &request, &size))
{
Find(request.parameter, &result);
char *out = StuToJson(&result, result.success);
PostSend(index, out);
free(out);
free(result.module);
free(request.parameter);
}
return;
}
删除:
void Delete(STUNODE *node, Result *result)
{
Find(node, result);
if (result->success)
{
char deleteSql[SQL_SIZE];
GetDeleteSql(node, deleteSql);
if (!ExecSql(deleteSql, EmptyCallBack, NULL))
{
SETRESULT(*result, FALSE, DB_ERROR, MSG_DB_ERROR, result->module);
}
}
}
路由处理:
if (0 == strcmp(request.path, STU_DELETE))
{
int size = 0;
if (UseParse(json, GetRequestStu, &request, &size))
{
Delete(request.parameter, &result);
char *out = StuToJson(&result, result.success);
PostSend(index, out);
free(out);
free(result.module);
free(request.parameter);
}
return;
}
得到所有学生:
// 获取学生数量
int GetNumberOfRecords(char *table)
{
char selectSql[SQL_SIZE];
StringCchPrintf(selectSql, SQL_SIZE, "SELECT * FROM %s;", table);
int number = 0;
ExecSql(selectSql, CountCallBack, &number);
return number;
}
void GetAllStu(int *size, Result *result)
{
*size = GetNumberOfRecords(STUTABLENAME);
GETSTU getStu;
getStu.number = 0;
getStu.stu = (STUNODE*)malloc(sizeof(STUNODE) * (*size));
char selectSql[SQL_SIZE];
GetAllStuSql(NULL, selectSql);
if (ExecSql(selectSql, GetAllCallBack, &getStu))
{
SETRESULT(*result, TRUE, SUCCESS, MSG_SUCCESS, getStu.stu);
}
else
{
SETRESULT(*result, FALSE, DB_ERROR, MSG_DB_ERROR, NULL);
}
}
路由处理:
if (0 == strcmp(request.path, STU_GETALLSTU))
{
int size;
GetAllStu(&size, &result);
char *out = StuToJson(&result, size);
PostSend(index, out);
free(out);
free(result.module);
return;
}
修改:
void Modify(STUNODE *node, STUNODE *update, Result *result)
{
Find(node, result);
if (result->success)
{
char updateSql[SQL_SIZE];
GetModifySql(node, update, updateSql);
if (!ExecSql(updateSql, EmptyCallBack, NULL))
{
SETRESULT(*result, FALSE, DB_ERROR, MSG_DB_ERROR, result->module);
}
}
}
路由处理:
if (0 == strcmp(request.path, STU_MODIFY))
{
int size = 0;
if (UseParse(json, GetRequestStu, &request, &size))
{
if (2 != size)
{
free(request.parameter);
PostSend(index, "{\n\t\"success\":\tfalse,\n\t\"code\":\t9001,\n\t\"msg\":\t\"parameter invalid\",\n\t\"module\":\t[]\n}");
return;
}
STUNODE *nodes = request.parameter;
Modify(&nodes[0], &nodes[1], &result);
char *out = StuToJson(&result, result.success);
PostSend(index, out);
free(out);
free(result.module);
free(request.parameter);
}
return;
}
客户端json处理和服务器类似,请求接收稍作修改:
Result SendRequest(Request *request, char *buffer, int size)
{
// 发送请求
char *out = StuToJson(request, size);
if (SOCKET_ERROR == send(socketServer, out, strlen(out) + 1, 0))
{
log_e("send失败,[%s]", WSAGetLastError());
}
free(out);
// 接收返回值
if (0 == recv(socketServer, buffer, 1024, 0))
{
log_e("recv失败,[%s]", WSAGetLastError());
}
Result result;
if (!UseParse(buffer, GetResultMsg, &result, NULL))
{
return (Result) {FALSE, PARSE_ERROR, MSG_PARSE_ERROR, NULL};
}
return result;
}
删除:
void MenuFun2(HWND hWnd, CONTROL *control)
{
STUNODE node;
InputNumberDialog(hWnd, &node);
if (CANCLE != node.score)
{
Request request;
SETREQUEST(request, POST, STU_DELETE, &node);
char buffer[1024];
Result result = SendRequest(&request, buffer, 1);
ShowMessage(result.code);
}
}
查找:
void MenuFun3(HWND hWnd, CONTROL *control)
{
STUNODE node;
InputNumberDialog(hWnd, &node);
if (CANCLE != node.score)
{
Request request;
SETREQUEST(request, GET, STU_FIND, &node);
char buffer[1024];
Result result = SendRequest(&request, buffer, 1);
if (result.success)
{
int size = 0;
if (UseParse(buffer, GetResultStu, &result, &size))
{
ShowOneStu(result.module);
free(result.module);
}
else
{
MessageBox(NULL, "GetResultStu解析失败", "提示", MB_OK);
}
}
else
{
ShowMessage(result.code);
}
}
}
修改:
void MenuFun4(HWND hWnd, CONTROL *control)
{
STUNODE node[2];
InputNumberDialog(hWnd, &node[0]);
if (CANCLE != node[0].score)
{
InputStuDialog(hWnd, &node[1]);
Request request;
SETREQUEST(request, POST, STU_MODIFY, &node[0]);
char buffer[1024];
Result result = SendRequest(&request, buffer, 2);
ShowMessage(result.code);
}
}
显示所有学生:
void MenuFun5(HWND hWnd, CONTROL *control)
{
ShowStuDialog();
Request request;
SETREQUEST(request, GET, STU_GETALLSTU, NULL);
char buffer[1024];
Result result = SendRequest(&request, buffer, 0);
if (result.success)
{
int size = 0;
if (UseParse(buffer, GetResultStu, &result, &size))
{
STUNODE *stu = result.module;
for (int i = 0; i < size; ++i)
{
AddData(g_hListCtrl, stu[i].number, i, NUMBERCOL);
AddData(g_hListCtrl, stu[i].name, i, NAMECOL);
AddData(g_hListCtrl, DoubleToString(stu[i].score), i, SCORECOL);
}
free(result.module);
}
else
{
MessageBox(NULL, "GetResultStu解析失败", "提示", MB_OK);
}
}
else
{
ShowMessage(result.code);
}
}
6、编写登陆功能
终于要写我们的新功能了,我们整个小程序只是写一个例子,所以就只加一个登陆功能,其他什么注册就不写了。
首先我们需要一个新的表,我就直接在navicat里面创建了:
navicat里面创建一条记录(密码就不加密了,加密代码网上一搜就有了):
定义用户结构体:
/// 用户
#define PASSWORD_SIZE 15
typedef struct
{
char name[NAME_SIZE]; // 姓名
char password[PASSWORD_SIZE]; // 密码
} USERNODE;
数据库获取:
// 检查用户节点所有的值
_Bool CheckUserAll(USERNODE *node)
{
return IsStringEmpty(node->name) || IsStringEmpty(node->password);
}
// 得到用户sql
void GetUserSql(USERNODE *node, char *sql)
{
char buffer[SQL_SIZE];
StringCchPrintf(buffer, SQL_SIZE, "SELECT NAME, PASSWORD FROM USER WHERE NAME = '%s' AND PASSWORD = '%s';", node->name, node->password);
Gb2312ToUtf8(buffer, sql, SQL_SIZE);
}
void Login(USERNODE *node, Result *result)
{
if (CheckUserAll(node))
{
SETRESULT(*result, FALSE, PARAMETER_INVALID, MSG_PARAMETER_INVALID, NULL);
return;
}
char selectSql[SQL_SIZE];
GetUserSql(node, selectSql);
int number = 0;
if (ExecSql(selectSql, CountCallBack, &number))
{
if (0 != number)
{
SETRESULT(*result, TRUE, SUCCESS, MSG_SUCCESS, NULL);
}
else
{
SETRESULT(*result, FALSE, SIGN_IN_ERROR, MSG_SIGN_IN_ERROR, NULL);
}
}
else
{
SETRESULT(*result, FALSE, DB_ERROR, MSG_DB_ERROR, NULL);
}
}
json解析:
#define SETUSERNODE(user, pszName, pszPassword) do { \
StringCchCopy((user).name, NAME_SIZE, pszName); \
StringCchCopy((user).password, PASSWORD_SIZE, pszPassword); \
} while(0)
_Bool GetRequestUser(cJSON *json, Request *request, int *size)
{
// 参数数组
cJSON *temp = cJSON_GetObjectItem(json, "parameter");
if (NULL == temp)
{
return FALSE;
}
// 计算大小
*size = cJSON_GetArraySize(temp);
// copy值
request->parameter = malloc(sizeof(USERNODE) * (*size));
USERNODE *user = request->parameter;
cJSON *userJson, *nameJson, *passwordJson;
for (int i = 0; i < *size; ++i)
{
userJson = cJSON_GetArrayItem(temp, i);
nameJson = cJSON_GetObjectItem(userJson, "name");
passwordJson = cJSON_GetObjectItem(userJson, "password");
if (NULL != nameJson && NULL != passwordJson
&& NULL != nameJson->valuestring && NULL != passwordJson->valuestring)
{
SETUSERNODE(user[i], nameJson->valuestring, passwordJson->valuestring);
}
}
return TRUE;
}
路由:
if (0 == strcmp(request.path, USER_Login))
{
int size;
if (UseParse(json, GetRequestUser, &request, &size))
{
Login(request.parameter, &result);
char *out = NullToJson(&result);
PostSend(index, out);
free(out);
free(request.parameter);
}
return;
}
客户端修改界面,加一个显示用户登陆的label:
typedef struct
{
// 界面大小
int width;
int height;
LABEL banner; // 系统标题
LABEL login; // 显示是否登陆
LABEL menus[MENU_SIZE]; // 功能菜单
} CONTROL;
显示:
void InitControl(CONTROL *control)
{
// 日志
InitLog();
// 屏幕宽高
control->width = GetSystemMetrics(SM_CXSCREEN);
control->height = GetSystemMetrics(SM_CYSCREEN);
// 系统标题
SETLABEL(control->banner, 30, 30, 530, 130, TRUE, 40, 100, "学生管理系统");
// 显示未登录
SETLABEL(control->login, 210, 160, 330, 210, TRUE, 8, 25, "未登录");
// 菜单
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]);
}
}
}
// 绘制所有方块
void PrintAllSqare(HDC insidehdc, CONTROL *control)
{
HBRUSH oldBrush;
HBRUSH newBrush = CreateSolidBrush(RGB(128, 255, 255));
oldBrush = (HBRUSH)SelectObject(insidehdc, newBrush);
// 整个界面
Rectangle(insidehdc, 0, 0, control->width, control->height);
// 是否登陆
newBrush = CreateSolidBrush(RGB(192, 192, 192));
oldBrush = (HBRUSH)SelectObject(insidehdc, newBrush);
PrintfSqare(insidehdc, &control->login);
// 菜单
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, control->banner.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);
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);
PrintfMenu(insidehdc, &control->login, RGB(0, 180, 0));
// 菜单
for (int i = 0; i < MENU_SIZE; ++i)
{
PrintfMenu(insidehdc, &control->menus[i], RGB(0, 0, 136 - i * 16));
}
DeleteObject(hfont);
}
登陆对话框:
#include "gui.h"
#include "resource.h"
extern HINSTANCE g_hInstance;
USERNODE *g_Node;
BOOL CALLBACK LoginProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam);
///
void LoginDialog(HWND hWnd, USERNODE *node)
{
g_Node = node;
DialogBox(g_hInstance, MAKEINTRESOURCE(IDD_DIALOG_LOGIN), hWnd, LoginProc);
}
///
_Bool InputName(HWND hWnd)
{
GetDlgItemText(hWnd, IDC_EDIT_USER, g_Node->name, NAME_SIZE);
if (0 == strlen(g_Node->name))
{
MessageBox(hWnd, "用户不能为空", "提示", MB_OK);
return FALSE;
}
return TRUE;
}
_Bool InputPassword(HWND hWnd)
{
GetDlgItemText(hWnd, IDC_EDIT_PASSWORD, g_Node->password, PASSWORD_SIZE);
if (0 == strlen(g_Node->name))
{
MessageBox(hWnd, "密码不能为空", "提示", MB_OK);
return FALSE;
}
return TRUE;
}
BOOL CALLBACK LoginProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
switch (nMsg)
{
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDOK:
if (InputName(hWnd) && InputPassword(hWnd))
{
EndDialog(hWnd, wParam);
return TRUE;
}
break;
case IDCANCEL:
g_Node->name[0] = '\0';
EndDialog(hWnd, wParam);
return TRUE;
default:
return FALSE;
}
break;
case WM_CLOSE:
EndDialog(hWnd, wParam);
return TRUE;
}
return FALSE;
}
json解析:
char* UserToJson(Request *request, int size)
{
cJSON *parameter, *user, *root = PraseResultConfirmNode(request, ¶meter);
USERNODE *nodes = request->parameter;
for (int i = 0; i < size; ++i)
{
user = cJSON_CreateObject();
cJSON_AddStringToObject(user, "name", nodes[i].name);
cJSON_AddStringToObject(user, "password", nodes[i].password);
cJSON_AddItemToArray(parameter, user);
}
return ObjectToJson(root);
}
发送请求也需要修改一下:
Result SendRequest(Request *request, OBJECTTOJSON fun, char *buffer, int size)
{
// 发送请求
char *out = fun(request, size);
if (SOCKET_ERROR == send(socketServer, out, strlen(out) + 1, 0))
{
log_e("send失败,[%s]", WSAGetLastError());
}
free(out);
// 接收返回值
if (0 == recv(socketServer, buffer, 1024, 0))
{
log_e("recv失败,[%s]", WSAGetLastError());
}
Result result;
if (!UseParse(buffer, GetResultMsg, &result, NULL))
{
return (Result) {FALSE, PARSE_ERROR, MSG_PARSE_ERROR, NULL};
}
return result;
}
添加功能:
void MenuFun6(HWND hWnd, CONTROL *control)
{
USERNODE node;
LoginDialog(hWnd, &node);
if ('\0' != node.name[0])
{
Request request;
SETREQUEST(request, GET, USER_Login, &node);
char buffer[1024];
Result result = SendRequest(&request, UserToJson, buffer, 1);
if (result.success)
{
StringCchPrintf(control->login.str, LABEL_STR_SIZE, "%s", node.name);
InvalidateRect(hWnd, &(RECT) { control->login.left, control->login.top, control->login.right, control->login.bottom}, TRUE);
}
ShowMessage(result.code);
}
}
当然这个功能最好是变成软件一打开就让用户登陆,登陆成功后才是主界面。
现在这个例子基本上完成了,我没有写测试,bug应该不少,但是实践了那么多再去看一些理论应该能理解的更好。
百度云链接
代码链接:https://pan.baidu/s/1KkfOmAh-Yn5DHiAir-wo1A
提取码:zpoz
Navicat 百度云链接:https://pan.baidu/s/10pGv0W4KqZASKaAv8nrXGA
提取码:2lcr
win32程序分享:数字雨、俄罗斯方块、坦克大战、纸牌请看我的其他博客,以前在学习找资料的时候找到一个叫风之残月写的程序,不过bug也挺多的
资源管理器:
浏览器:
编译器:
音乐播放器:
记事本:
任务管理器:
单词本:
链接:https://pan.baidu/s/1YHWxp-EZpXO-0Pt_OfJgKA
提取码:mppt
版权声明:本文标题:编程思想之c语言课程设计--管理系统例子 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dianzi/1727135988a1099095.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论