5.登录页及有关功能的设计

HJ Lv2

4.登录页的制作

为了主函数的简洁,我将登录模块直接自定义为lonIn函数,再给出代码前,需要补充两个知识点:

  • 1.由于easyx本身库的限制,我们绘制在界面上的图片或者图形不可以删除,所以如果需要实现画面上某一组件的改变,需要重新绘制整个界面的所有内容。所以需要使用使用while(1)无限循环,检测某些状态并逐帧绘制图像,在符合某些条件时使用break跳出循环。
  • 2.使用while(1)逐帧绘图时,为了避免闪烁,需要用到在循环前使用BeginBatchDraw()函数用于开始批量绘图。执行后,任何绘图操作都将暂时不输出到绘图窗口上,直到执行 FlushBatchDraw 或 EndBatchDraw 才将之前的绘图输出。在循环内使用绘制完图片后使用FlushBatchDraw()函数用于执行未完成的绘制任务。跳出循环后使用EndBatchDraw()函数,结束批量绘制。具体实现原理可移步关于双缓冲消除闪屏的原理 查看,此处不过多赘述。
    下面给出具体代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void logIn() {
    BeginBatchDraw();
    while (1) {
    user = buildListFromFile();
    drawLogin();
    FlushBatchDraw();
    if (checkMessage() == 10) {
    free(user);
    break;
    }
    }
    EndBatchDraw();
    }

这段代码的目的是执行一个登录流程,其中包括从文件构建用户列表、绘制登录界面、检查用户信息并根据检查结果决定是否退出。

1
2
3
while (1) {

}

在这里是绘制登录界面,验证用户信息成功则退出循环,结束绘制。

1
user = buildListFromFile();

这行代码调用 buildListFromFile 函数,该函数的作用是从文件中读取数据,并构建一个链表来存储这些数据,每个节点代表一个用户。函数返回一个指向链表头节点的指针,这个指针被存储在 user 变量中。

1
drawLogin();

这行代码调用 drawLogin 函数,该函数的作用是绘制登录界面。具体实现包括在屏幕上显示登录表单,按钮以及背景。

1
2
3
4
   if (checkMessage() == 10) {
free(user);
break;
}

这行代码调用 checkMessage 函数。检查用户输入的登录信息。如果 checkMessage 返回值为 10,则验证成功,释放链表,跳出循环。

4.1 读取用户数据

在实现登录功能时,我们需要考虑用户信息的存储和读取。由于我们无法知道数据库中存储的用户数量,这时我们就需要用到链表,我们定义函数buildListFromFile(),这个函数就是从外部数据库文件读取用户信息到链表,并返回链表的头结点。

外部数据库文件的设计

文件名为test.txt,格式为username:password,每行一条数据。
例如:

1
2
3
user1:123
user2:456
......

buildListFromFile()函数的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ListNode* buildListFromFile() {//从文件读入用户信息到链表
FILE* file = fopen("test.txt", "r");
if (file == NULL) {
return NULL;
}
ListNode* head = NULL;
ListNode* tail = NULL;
char line[100];
while (fgets(line, sizeof(line), file)) {
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
char* separator = strchr(line, ':');
if (separator) {
*separator = '\0';
strcpy(newNode->username, line);
strcpy(newNode->password, separator + 1);
newNode->next = NULL;

if (head == NULL) {
head = newNode;
tail = newNode;
}
else {
tail->next = newNode;
tail = newNode;
}
}
else {
free(newNode);
}
}
fclose(file);
return head;
}

下面给出函数解析:
1.

1
2
3
4
FILE* file = fopen("test.txt", "r");
if (file == NULL) {
return NULL;
}

这行代码尝试以只读模式("r")打开名为 "test.txt" 的文件,并将文件指针赋值给 file。如果文件不存在或无法打开,函数返回 NULL。
2.

1
2
ListNode* head = NULL;
ListNode* tail = NULL;

这两行代码初始化链表的头指针 head 和尾指针 tail 为 NULL,表示开始时链表为空。
3.

1
2
char line[100];
while (fgets(line, sizeof(line), file)) {

这里定义了一个字符数组 line,并使用 fgets 在文件中逐行读取内容,每次读取的最大长度不超过 line 数组的大小。
4.

1
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));

为每个用户信息创建一个新的链表节点,使用 malloc 分配足够的内存。
5.

1
2
3
4
5
6
   char* separator = strchr(line, ':');
if (separator) {
*separator = '\0';
strcpy(newNode->username, line);
strcpy(newNode->password, separator + 1);
newNode->next = NULL;

这段代码查找每行中的分隔符 ':'。如果找到,将其替换为字符串终止符 '\0',以此将一行分割为用户名和密码两部分,并分别复制到新节点的 username 和 password 字段中。
6.

1
2
3
4
5
6
7
8
9
10
11
          if (head == NULL) {
head = newNode;
tail = newNode;
} else {
tail->next = newNode;
tail = newNode;
}
} else {
free(newNode);
}
}

如果链表为空,新节点既是头节点也是尾节点。否则,将新节点添加到链表末尾,并更新尾节点指针。如果行中没有找到分隔符,释放已分配的新节点内存。
6.

1
2
3
    fclose(file);
return head;
}

关闭文件并返回链表的头指针。
注意:对于每次打开文件后,都应使用fclose()关闭文件,防止发生未知错误,这点以后将不再说明。

4.2 界面UI的绘制

绘制界面UI时,我们需要绘制登录注册按钮,用户名和密码输入框以及背景登,由于此处较为简单,直接给出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void drawLogin() {//绘制登录用背景图案
loadimage(NULL, "image/loadbk.jpg");//背景
putimagePNG(340, 400, &picgroup[4]);//登录按钮
PutL(390, 405, "黑体", 24, BLACK, "登录");
putimagePNG(340, 450, &picgroup[4]);//退出按钮
PutL(390, 455, "黑体", 24, BLACK, "退出");
putimagePNG(340, 500, &picgroup[4]);//注册按钮
PutL(390, 505, "黑体", 24, BLACK, "注册");
setfillcolor(WHITE);//设置填充颜色为白色
setlinecolor(BLACK);//设置边框颜色为黑色
PutL(250, 250, "黑体", 24, BLACK, "用户名");//绘制文字
fillrectangle(340, 250, 540, 280);//绘制用户名输入框
PutL(250, 350, "黑体", 24, BLACK, "密码");
fillrectangle(340, 350, 540, 380);//绘制密码输入框
PutL(345, 255, "黑体", 24, BLACK, username);//绘制输入的用户名信息
PutL(345, 355, "黑体", 24, BLACK, password);
}

4.3 交互信息以及用户验证

这个部分是登录模块中最为重要的部分。都由函数checkMessage()处理。这个函数返回一个整数,若返回10则登陆成功,退出登录界面,否则继续登录。
先给出这个函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int checkMessage() {
ExMessage msg;
if (peekmessage(&msg)) {
if (msg.message == WM_LBUTTONDOWN) {
if (msg.x > 340 && msg.x < 540) {
if (msg.y > 250 && msg.y < 280) {
type = 1;//1是输入用户名
}
if (msg.y > 350 && msg.y < 380) {
type = 2;
}//2是密码
}
else { type = 3; }
if (msg.x > 340 && msg.y > 400 && msg.x < 499 && msg.y < 433) {
if () {//在这里判断是否登陆成功

//登陆成功的提示

return 10;//成功返回10
}
else {
//登陆失败的提示
}
}
if (msg.x >= 340 && msg.x <= 499 && msg.y >= 455 && msg.y <= 488) {
exit(0);
}
if (msg.x >= 340 && msg.x <= 499 && msg.y >= 505 && msg.y <= 543) {
if (text_lengthuser > 0 && text_lengthpass > 0) {
if () {//判断是否注册成功
//如果注册成功
}
else {
//如果注册失败
}
}
else {
//如果未输入
}
}
}
if (msg.message == WM_CHAR) {
if (type == 1) {
handleInput(msg, username, &text_lengthuser);
}
if (type == 2) {
handleInput(msg, password, &text_lengthpass);
}
}
}
return 0;
}

下面我来详细讲解。

信息的输入

由于用户需要输入两个数据:用户名和密码,所以我们考虑建立两个变量来存储这些数据。

1
2
CHAR username[MAX_INPUT_LENGTH + 1] = "";	//用户名输入信息
CHAR password[MAX_INPUT_LENGTH + 1] = ""; //密码输入的信息

并且为了实现不超过最大长度,我们还需要设计两个变量存储当前输入的长度:

1
2
int text_lengthuser = 0;			//当前用户名长度
int text_lengthpass = 0; //当前密码长度

判断输入框

由于easyx库的限制,我们不能使用Windows的gui来实现文字输入,这时就要考虑其他的办法实现文字输入,具体输入方式是:
获取键盘输入内容->将键盘内容追加到字符串数组->若是按退格键移除数组最后一个内容->在每个循环中输出字符串内容
除此之外,我们有两个输入框,一个是用户名,一个是密码,我们需要实现在点击用户名框时输入的内容显示到用户名框,点击密码框时输入的内容显示到密码框。如何实现这一功能呢?
我们可以考虑声明一个全局变量type,若点击了用户名框则将值赋为1,点击了密码框将值赋为2,若点击其他区域将值赋为3.
下面来看具体实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
if (msg.message == WM_LBUTTONDOWN) {若是鼠标消息为点击左键
if (msg.x > 340 && msg.x < 540) {//点击了用户名输入框
if (msg.y > 250 && msg.y < 280) {
type = 1;//1是输入用户名
}
if (msg.y > 350 && msg.y < 380) {//点击了密码输入框
type = 2;
}//2是密码
}
else { type = 3; }//点击了其他区域

......

实时显示输入内容

对于该功能,我们想要实现实时显示用户输入的内容(用户名或者密码)到屏幕(在上面的drawLogin()函数中实时绘制输入的信息),并且当按下退格后删除最后一个字符,而且不能超出最大长度。

1
2
3
4
5
6
7
if (msg.message == WM_CHAR) {//如果是字符消息
if (type == 1) {
handleInput(msg, username, &text_lengthuser);
}
if (type == 2) {
handleInput(msg, password, &text_lengthpass);
}

以上代码很容易理解,如果用户键盘按下字符且点击的位置是用户名(密码)框,则使用自定义函数handleInput在用户名(密码)变量中更新内容。
handleInput的作用是处理在输入用户名和密码的场景中的用户输入。它可以添加字符到输入字符串中,或者处理删除(退格)操作。这个函数依赖于用户按键事件的信息,以及当前输入字符串的状态。
下面是内容处理函数handleInput的设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void handleInput(ExMessage msg, char* input, int* length) {//处理输入用户名密码
char c = msg.ch;
if (c == '\b' && *length > 0)
{
// 将最后一个字符变为'\0'
}
else if (c != '\r' && c != '\b') // 忽略回车键,并且如果不是退格键则添加字符
{
if (*length < MAX_INPUT_LENGTH)
{
// 将最后一个字符变为输入的字符
// 将输入的字符之后的一个字符变为'\0'
}
}
}

它接受三个参数:

  • ExMessage msg:消息变量。
  • char* input:一个字符数组的指针,存储当前已经输入的文本。
  • int* length:一个指向整数的指针,表示当前 input 字符串的长度。

代码首先从 msg 中取出用户按下的字符,并将其存储在局部变量 c 中。

1
2
3
4
if (c == '\b' && *length > 0)
{
// 将最后一个字符变为'\0'
}

如果用户按下了退格键(ASCII码为 \b),并且 input 字符串长度大于0,则减少 length 的值,并在新的字符串末尾加上空字符(即移除了最后一个字符)。

1
2
3
4
5
6
7
8
else if (c != '\r' && c != '\b') 
{
if (*length < MAX_INPUT_LENGTH)
{
// 将最后一个字符变为输入的字符
// 将输入的字符之后的一个字符变为'\0'
}
}

如果按下的键既不是回车(\r)也不是退格(\b),则尝试将字符添加到 input 中。首先检查当前长度是否小于允许的最大长度 MAX_INPUT_LENGTH(这个值在代码中没有给出,但可以假设它是一个定义了的常量),如果是,则将字符 c 添加到当前长度的位置,随后增加 length 的值,并在新的末尾加上空字符。

判断点击了哪个按钮

这里的内容很简单,直接给出代码:

1
2
3
4
5
6
7
8
9
if (msg.x > 340 && msg.y > 400 && msg.x < 499 && msg.y < 433) {//点击了登录按钮

}
if (msg.x >= 340 && msg.x <= 499 && msg.y >= 455 && msg.y <= 488) {//点击了退出按钮

}
if (msg.x >= 340 && msg.x <= 499 && msg.y >= 505 && msg.y <= 543) {//点击了注册按钮

}

登录信息的验证

这个很简单,即比对用户名和密码变量的信息与我们构建好的用户信息链表是否符合。但是需要注意的是,在读取文件到链表时,非最后一行密码后会有换行符,最后一行没有。当比对时需要考虑此问题。
以下是验证函数

1
2
3
4
5
6
7
8
9
10
int checkUser(char username[], char password[]) {//验证登录信息
ListNode* temp =user;
while (temp != NULL) {
if () {//考虑用户名相等并且密码等于读取到的密码或者读取到的密码加上换行符
return 0;
}
temp = temp->next;
}
return 1;
}

该函数,如果验证成功返回1,否则返回0.

注册功能的实现

在这里我们需要实现如下功能:

  • 注册用户:将注册信息写入数据文件
  • 比对重复:若用户名存在则不注册并给出提示
    这个部分也没什么难的,给上代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int registerUser(char username[], char password[]) {//注册用户
    ListNode* temp = user;
    // 检查用户名是否已经存在
    while (temp != NULL) {
    if (strcmp(temp->username, username) == 0) {
    return 0; // 用户名已存在,注册失败
    }
    temp = temp->next;
    }
    // 将新用户的信息写入文件
    FILE* file = fopen("test.txt", "a");
    if (file != NULL) {
    fprintf(file, "%s:%s\n", username, password);
    fclose(file);
    }
    else {
    return 0; // 文件打开失败,注册失败
    }
    return 1;
    }
    如果注册失败返回0,成功返回1.