介绍
本项目将指导你使用 C 编程语言创建一个打字练习程序。你将学习如何进行各种打字练习,包括单字符串、多字符串以及基于键盘布局的打字训练。这些程序将有助于提高你的打字速度和准确性。
👀 预览
开始打字联想练习。
请输入由 '?' 指示的隐藏字符。
按空格键开始。
AS?FG
?m,./\
67890-?\
?XCVB
zx?vb
!"?$%
ZXC?B
hjk?;:]
...(省略)...
🎯 任务
在本项目中,你将学习:
- 如何使用 C 创建一个打字练习程序。
- 如何实现单字符串、多字符串以及键盘布局联想的打字练习。
- 如何使用
curses库进行屏幕输入和输出操作。 - 如何打乱问题顺序以增加练习的多样性。
🏆 成果
完成本项目后,你将能够:
- 创建一个 C 程序来模拟打字练习。
- 处理用户输入并验证其正确性。
- 生成随机问题并打乱问题顺序。
- 使用
curses库进行高级屏幕输入和输出操作。
项目环境
该项目需要使用 curses 库。使用以下命令进行安装:
sudo apt-get update
sudo apt-get install libncurses5-dev
代码中的 getputch.h 头文件的目的是提供一个跨平台的屏幕输入输出函数库,确保程序能够在不同的操作系统和编译器环境中正确地执行屏幕输入输出操作,特别是换行符输出的处理。
接下来,我们将介绍基本的打字练习,这主要涉及 C 语言中的指针、字符串数组和循环结构。
我们将演示字符串数组的应用,从单个字符的基本输入开始,到使用多个无序单词进行练习。
输入一个字符
进入 ~/project 目录并创建项目文件 typing1a.c:
cd ~/project
touch typing1a.c
接下来,我们需要编写 C 代码来创建一个允许输入字符串的打字练习软件。程序如下:
#include <time.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include "getputch.h"
int main(void)
{
int i;
char *str = "How do you do?"; /* 要输入的字符串 */
int len = strlen(str); /* 字符串中的字符数 */
init_getputch();
printf("请按所示输入。\n");
printf("%s\n", str); /* 显示要输入的字符串 */
fflush(stdout);
for (i = 0; i < len; i++) {
int ch;
do {
ch = getch(); /* 从键盘读取输入 */
if (isprint(ch)) {
putch(ch); /* 显示按下的键 */
if (ch!= str[i]) /* 如果按下了错误的键 */
putch('\b'); /* 将光标向后移动一格 */
}
} while (ch!= str[i]);
}
term_getputch();
return 0;
}
要求玩家输入 str 指针指向的字符串:How do you do?。通过指针和数组的互换性,字符串中的字符 'H', 'o',..., '?' 可以依次表示为 str[0], str[1],..., str[13]。
此外,变量 len 表示字符串 str 的长度,初始值为 14。strlen() 用于获取字符串的长度。
for 循环将变量 i 的值从 0, 1, 2,... 递增 len 次,以从头到尾依次遍历字符串中的字符。在每次迭代中,字符 str[i] 是 'H', 'o',..., '?',即要输入的字符。这种打字练习不接受错误的字符(在玩家输入正确的字符之前,程序不会移动到下一个字符)。此控制由 do 循环确保,循环内执行以下操作:
- 将输入的字符(
getch函数的返回值)赋给变量ch。 - 如果字符
ch是可打印字符,则使用putch函数显示它(不包括换行和制表符等不可显示的字符)。 - 如果字符
ch不等于要输入的字符str[i],则输出退格字符'\b'将光标向后移动一格。这确保下一个输入的字符显示在同一位置。
完成上述步骤后,计算 do 循环的控制表达式(ch!= str[i])。当输入错误的字符(当 ch 不等于 str[i] 时),do 循环再次开始。此时,程序不会移动到下一个字符,而是再次运行 do...while 循环内的相关部分。输入正确的字符后,for 循环将 i 的值递增,程序移动到下一个字符。输入所有字符后,程序将显示玩家所用的时间。
使用以下命令进行编译和运行:
cd ~/project
gcc -o typing1a typing1a.c -lcurses
./typing1a

你可以多次练习以提高速度。如果你觉得练习 How do you do? 很无聊,也可以选择另一个字符串进行练习。
消除输入的字符
进入 ~/project 目录并创建项目文件 typing1b.c:
cd ~/project
touch typing1b.c
现在让我们看看这个程序。代码如下:
#include <time.h>
#include <stdio.h>
#include <string.h>
#include "getputch.h"
int main(void)
{
int i;
char *str = "How do you do?"; /* 要输入的字符串 */
int len = strlen(str); /* 字符串 str 中的字符数 */
init_getputch();
printf("请按所示输入。\n");
for (i = 0; i < len; i++) {
/* 显示 str[i] 之后的字符并将光标返回到开头 */
printf("%s \r", &str[i]);
fflush(stdout);
while (getch()!= str[i])
;
}
term_getputch();
return 0;
}
这个程序与前一个稍有不同。每次输入正确的字符时,一个字符会消失,后面的字符会向前移动。同样,除非玩家输入正确的键,否则程序不会移动到下一个字符。当玩家正确输入所有字符时,所有字符都会消失,程序结束。
虽然这里执行的操作比前一个程序更“高级”,但程序实际上更短。for 语句的主体只由两个简短的语句组成。
- 在语句
printf("%s \r", &str[i]);中传递给printf函数的&str[i]是指向str[i]的指针。由于变量i的值为0,指针&str[i]将指向字符'H',导致屏幕显示从str[0]开始的字符串How do you do?,如上图所示。然后程序会在这个字符串之后立即输出空白字符和回车符\r,并将光标返回到该行开头的'H'的位置。 - 如果输入的字符(
getch函数的返回值)不等于str[i],即输入的字符不是'H',while语句将持续循环,直到玩家输入正确的字符,此时while语句将结束。 - 然后变量
i的值会通过for语句的影响变为1。如上图所示,语句printf("%s \r", &str[i]);将输出从str[1]开始的字符串ow do you do?,然后输出空白字符和回车符,并将光标返回到开头的'o'的位置。之后,在后续while语句的作用下,等待玩家正确输入'o'。
使用以下命令进行编译和运行:
cd ~/project
gcc -o typing1b typing1b.c -lcurses
./typing1a

输入多个字符串
接下来,让我们扩展之前的程序,允许玩家练习输入多个字符串。
进入 ~/project 目录并创建一个名为 typing2a.c 的项目文件:
cd ~/project
touch typing2a.c
现在让我们看看这个程序。代码如下:
#include <time.h>
#include <stdio.h>
#include <string.h>
#include "getputch.h"
#define QNO 12 /* 问题数量 */
int main(void)
{
char *str[QNO] = {"book", "computer", "default", "comfort",
"monday", "power", "light", "music",
"programming", "dog", "video", "include"};
int i, stage;
init_getputch();
printf("开始打字练习。\n");
printf("按空格键开始。\n");
while (getch()!= ' ') /* 等待直到 */
; /* 玩家按下空格键 */
for (stage = 0; stage < QNO; stage++) {
int len = strlen(str[stage]); /* 字符串 str[stage] 中的字符数 */
for (i = 0; i < len; i++) {
/* 显示 str[stage][i] 之后的字符并将光标返回到开头 */
printf("%s \r", &str[stage][i]);
fflush(stdout);
while (getch()!= str[stage][i])
;
}
}
term_getputch();
return 0;
}
在这个程序中,输入一个字符串后,下一个字符串将显示在同一行供玩家输入。总共有 12 个字符串可供练习。
这个程序主要基于之前的程序,但有一些不同之处:
for语句被嵌套了。
- 由于问题中的单词数量从 1 个变为了 12 个,所以添加了一个外部
for语句。这个for语句对变量stage从 0 开始迭代QNO次。在strlen(str[stage])之后的内部for循环与之前程序中的for循环等效。 - 每次迭代要输入的字符串是
str[stage](相当于之前程序中的str)。要输入的字符数量因字符串而异,所以strlen(str[stage])语句计算用于问题的字符串str[stage]的长度,并将其存储在变量len中。
- 要输入的字符不再是
str[i],而是str[stage][i]。
在内部 for 循环中,要输入的字符是 str[stage][i],这相当于之前程序中的 str[i]。
使用以下命令进行编译和运行:
cd ~/project
gcc -o typing2a typing2a.c -lcurses
./typing2a

打乱问题顺序(方法一)
使用之前的程序练习几次后,问题中出现的下一个字符串会自动在脑海中浮现,从而削弱训练效果。现在让我们打乱问题的顺序。
进入 ~/project 目录并创建项目文件 typing2b.c:
cd ~/project
touch typing2b.c
现在让我们看看这个程序。代码如下:
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "getputch.h"
#define QNO 12 /* 问题数量 */
#define swap(type, x, y) do { type t = x; x = y; y = t; } while (0)
int main(void)
{
char *str[QNO] = {"book", "computer", "default", "comfort",
"monday", "power", "light", "music",
"programming", "dog", "video", "include"};
int i, stage;
int qno[QNO]; /* 问题顺序 */
init_getputch();
srand(time(NULL)); /* 设置随机数种子 */
for (i = 0; i < QNO; i++)
qno[i] = i;
for (i = QNO - 1; i > 0; i--) {
int j = rand() % (i + 1);
if (i!= j)
swap(int, qno[i], qno[j]);
}
printf("开始打字练习。\n");
printf("按空格键开始。\n");
while (getch()!= ' ') /* 等待直到 */
; /* 玩家按下空格键 */
for (stage = 0; stage < QNO; stage++) {
int len = strlen(str[qno[stage]]); /* 字符串 str[qno[stage]] 中的字符数 */
for (i = 0; i < len; i++) {
/* 显示 str[qno[stage]][i] 之后的字符并将光标返回到开头 */
printf("%s \r", &str[qno[stage]][i]);
fflush(stdout);
while (getch()!= str[qno[stage]][i])
;
}
}
term_getputch();
return 0;
}
为了打乱问题的顺序,程序引入了一个新的数组 qno,其元素类型为 int,大小为 QNO(即问题中字符串的数量,也就是 12)。
在开始打字练习之前,前两个 for 循环用于按 0, 1, 2,..., 11 的顺序为数组 qno 的每个元素赋值。
与之前的程序类似,所有出现的 str[stage] 都被替换为 str[qno[stage]],因为在这个程序的每个循环中,要问的问题是基于 str[qno[stage]] 的。
- 当
stage为 0 时;由于qno[0]的值为 2,程序显示的问题将是str[2],即default。 - 当
stage为 1 时;由于qno[1]的值为 1,程序显示的问题将是str[1],即computer。
依此类推。在对这 12 个字符串进行练习后,程序将结束。
使用以下命令进行编译和运行:
cd ~/project
gcc -o typing2b typing2b.c -lcurses
./typing2b

打乱问题顺序(方法二)
以下程序使用不同的方法打乱问题顺序,且不使用数组。与前一个程序相比,该程序所需变量更少,更为简洁。
进入 ~/project 目录并创建项目文件 typing2c.c:
cd ~/project
touch typing2c.c
现在让我们看看这个程序。代码如下:
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "getputch.h"
#define QNO 12 /* 问题数量 */
#define swap(type, x, y) do { type t = x; x = y; y = t; } while (0)
int main(void)
{
char *str[QNO] = {"book", "computer", "default", "comfort",
"monday", "power", "light", "music",
"programming", "dog", "video", "include"};
int i, stage;
init_getputch();
srand(time(NULL)); /* 设置随机种子 */
for (i = QNO - 1; i > 0; i--) { /* 打乱数组 str */
int j = rand() % (i + 1);
if (i!= j)
swap(char *, str[i], str[j]);
}
printf("开始打字练习。\n");
printf("按空格键开始。\n");
while (getch()!= ' ') /* 等待直到 */
; /* 玩家按下空格键 */
for (stage = 0; stage < QNO; stage++) {
int len = strlen(str[stage]); /* 字符串 str[stage] 中的字符数 */
for (i = 0; i < len; i++) {
/* 显示 str[stage][i] 之后的字符并将光标返回到开头 */
printf("%s \r", &str[stage][i]);
fflush(stdout);
while (getch()!= str[stage][i])
;
}
}
term_getputch();
return 0;
}
数组 str 的每个元素都指向字符串 book, computer, default,...。交换 str[0] 和 str[2] 的值后,str[0] 指向 default,str[2] 指向 book。
程序中的第一个 for 循环(上述代码)负责重新排列指针,这些指针是数组 str 中元素的值,从而打乱元素顺序。由于要交换的对象是指针,所以分配给函数宏 swap 的第一个参数是 char *。
在程序主体的第二个 for 循环中,每个问题阶段都返回到 str[stage](在清单 8 - 4 中是 str[qno[stage]],更为复杂)。这是因为数组 str 已经被随机重新排列,按顺序输出 str[0], str[1],..., str[QNO - 1] 就得到了随机的问题顺序。
这种方法的一个缺点是 一旦单词顺序被打乱,就无法恢复。请注意这一点。
使用以下命令进行编译和运行:
cd ~/project
gcc -o typing2c typing2c.c -lcurses
./typing2c

键盘布局关联打字
接下来,我们要开发一款软件,让玩家在回忆键盘上每个键的位置的同时练习打字。与常规打字练习不同,玩家需要在没有任何提示的情况下输入字符。此外,不同的键盘有不同的布局,这里我们将以下图所示的键盘布局作为参考。


在图中所示的键盘布局中,即使在按下 [0] 键时按住 [Shift] 键,也不会输入任何信息。
关于这个键盘的布局,我们可以观察到以下几点。
- 它由 4 层键组成。
- 每层分为左手击打键和右手击打键,左手击打黑色键,右手击打蓝色键。
- 有些键击打时不需要按住
[Shift]键,有些键击打时需要按住[Shift]键。
我们将按层/手/是否按下 [Shift] 键分类的各种集合称为“块”,整个键盘总共由 4×2×2 = 16 个块组成。
例如,第 3 层中按住 [Shift] 键的左手击打键块是 [A]、[S]、[D]、[F]、[G](分别由小指、无名指、中指、食指、食指操作)。
在这个训练软件中,一个块作为一个问题呈现,但软件会用“?”隐藏该块内的一个字符。例如,对于以下问题,玩家需要将隐藏的“?”与大写字母 D 关联起来,然后输入该字母。
A S? F G
代码与运行结果
进入 ~/project 目录并创建一个名为 typing3.c 的项目文件:
cd ~/project
touch typing3.c
根据上述指导方针编写的程序如下所示:
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "getputch.h"
#define NO 30 /* 练习次数 */
#define KTYPE 16 /* 块的数量 */
int main(void)
{
char *kstr[] = {"12345", "67890-^\\", /* 第 1 层 */
"!\"#$%", "&'() =~|", /* 第 1 层 [Shift] */
"qwert", "yuiop@[", /* 第 2 层 */
"QWERT", "YUIOP`{", /* 第 2 层 [Shift] */
"asdfg", "hjkl;:]", /* 第 3 层 */
"ASDFG", "HJKL+*}", /* 第 3 层 [Shift] */
"zxcvb", "nm,./\\", /* 第 4 层 */
"ZXCVB", "NM<> _", /* 第 4 层 [Shift] */
};
int i, stage;
clock_t start, end; /* 开始和结束时间 */
init_getputch();
srand(time(NULL)); /* 设置随机数生成种子 */
printf("开始打字联想练习。\n");
printf("请输入由 '?' 指示的隐藏字符。\n");
printf("按空格键开始。\n");
fflush(stdout);
while (getch()!= ' ')
;
start = clock(); /* 开始时间 */
for (stage = 0; stage < NO; stage++) {
int k, p, key;
char temp[10];
do {
k = rand() % KTYPE;
p = rand() % strlen(kstr[k]);
key = kstr[k][p];
} while (key == ' ');
strcpy(temp, kstr[k]);
temp[p] = '?';
printf("%s", temp);
fflush(stdout);
while (getch()!= key)
;
putchar('\n');
}
end = clock(); /* 结束时间 */
printf("所用时间:%.1f 秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
term_getputch();
return 0;
}
宏 KTYPE 表示块的数量,为 16,数组 kstr 用于存储由每个块从左到右排列的字符组成的字符串。
出于训练目的,问题中不会包含字符 ?,因此为块声明的最后一个字符串是 NM<> _ 而不是 NM<>?_(由于程序不使用空格键生成问题,所以不会产生错误)。
如果你的键盘布局与本示例中所示的不同,请相应地修改 kstr 数组的声明。
第一个 do while 循环负责生成问题。
- 变量
k表示从哪个块生成问题。由于这个值对应于kstr数组的索引,所以被设置为一个大于或等于 0 且小于KTYPE的随机数。由于块的数量
KTYPE是 16,生成的随机数将在 0 到 15 之间。 - 变量
p表示在块内应该隐藏哪个字符以生成问题。由于这个值对应于块内用于生成问题的字符串的索引,所以被设置为一个大于或等于 0 且小于块内字符数量的随机数。假设
k为 0,该块由 5 个字符'12345'组成,所以p被设置为一个从 0 到 4 的随机数。此外,如果k为 3,该块由 8 个字符'&'()=~|'组成,所以p被设置为一个从 0 到 7 的随机数。 - 变量
key表示隐藏的字符。
例如,如果 k 为 0 且 p 为 2,块 '12345' 中的字符 '3' 就是 key。由于程序已经将空格字符 ' ' 分配给了不应用于生成问题的字符,所以如果隐藏字符 key 是空格字符,do - while 循环会用于重新生成问题。
接下来,strcpy 函数将 kstr[k] 复制到 temp,并将 '?' 赋给 temp[p]。这就生成了要显示在屏幕上的字符串 12?45。
如果程序能够显示字符串 temp 并读取从键盘输入的字符 key,则是正确的。与之前的打字练习程序一样,这个程序不接受输入错误的字符。经过 30 轮训练后,程序将结束执行。
使用以下命令进行编译和运行:
cd ~/project
gcc -o typing3 typing3.c -lcurses
./typing3
开始打字联想练习。
请输入由 '?' 指示的隐藏字符。
按空格键开始。
AS?FG
?m,./\
67890-?\
?XCVB
zx?vb
!"?$%
ZXC?B
hjk?;:]
…(省略)…
总结
在这个项目中,你已经学习了如何使用 C 编程语言创建一个打字练习程序。这个打字练习程序为用户提供了一个多功能的交互式平台,以提高他们的打字技能,无论是用于编码还是一般交流。它为提高打字速度和准确性提供了一个有价值的工具。



