小编心语:锵锵锵!小编我又来了!昨天发了一篇比较实用的《Python聊天室》,鉴于反响还不错,SO ,小编也想给大家多分享点有用的干货,让大家边学边用。好了,闲话不多说,今天要给各位看官们介绍的是基于C语言的带提醒功能计时器。
你还在为错过重要的事情而心烦不已吗?
你还在为没抢到某米手机而扼腕叹息吗?
你还在为没领到美食免单而空遗口水吗?
从今天起,这些都不是问题,且看小编我给你一一道来!
咳咳咳~上菜啦~
dialog+ncurse实现命令行带提醒功能计时器
我们将那个结合shell编程与C语言编程使用dialog工具与ncurses库实现一个仿tmux时间风格的带提醒功能的计时器
一、说明
这次项目课要实现的东西功能很简单,但是却用到了好几个东西,包括dialog
,ncurses
库,zenity
, moc
, 用了这么多东西最后也就是个定时器-_-||。不过我想先告诉你的是为什么是它。这原因呢有两个,一个是之前看到有用户反应希望项目课有一些综合性稍强的 内容,这个项目课就是了,结合了shell编程和C语言编程,C语言编程除了包含本身的内容还涉及到了如何使用外部库来实现我们想要的功能;这第二个原因 是,我自己需要一个这样的定时器啊,我要每周二中午12点的时候准时抢小米手机(抢了几次没抢到啊,就是因为时间没掐准,不够快),所以就想要一个定时器 了,我想的是除了要实现基本功能外还要求“看起来,高大上”。
二、功能介绍
既然我是要实现一个命令行的定时器,要想“高大上”,那就必须要用到命令行的图形库了,一开始想到的是ncurses,这就得完全用C语言来开发了,后来又想到可以用shell下的dialog(基于ncurses实现的命令行图形对话框,包含一些常用的构件)来完成一部分功能,比如设定时间的界面可以用dialog
的timebox
构件来实现,如下图
本想是不是可以用dislog实现全部我想要的效果,比如在设定时间之后,进入倒计时界面,类似下面的效果
这实际是tmux内建时钟效果。然后翻翻dialog的文档,发现连字体大小都没法设置,也无法设置构件内文本的显示位置,那就没办法了,不过 dislog底层就是基于ncurses来实现的,我也就用ncurses来实现倒计时显示的这个功能吧。最后实现的效果如下,是不是山寨得跟原版 (tmux, ctrl+b t)一样。
这样就满足了嘛,当然不,我们还要再加点东西,让它像闹钟一样时间到放歌音乐如何,或者再弹个窗,免得我埋头工作忘了"正事"(抢手机),播放音乐我用moc(一个流行的命令行音乐播放器),弹窗用zentiy(基于gtk构件的简单弹出式对话框)
功能介绍完了,下面就让我们来实现吧
三、具体实现
1.使用dialog设定时间
这个功能在shell脚本中实现
我们创建了一个函数
function SetTime() { local c_hour=`date +%H` local c_minute=`date +%M` local c_time=`date +%H:%M:%S` s_time=`dialog --stdout --title "Set Time" --time-format %H\:%M\:%S --timebox\ "Current time is\n$c_time\nPlease set the deadtime\n" 0 0 $c_hour $c_minute 00` }
上面代码中再SetTime
函数内部定义了三个局部变量,用于通过date
命令分别获取系统当前时间的时、分,并完整时间显示为构件的信息, 这里dialog
的第一参数--stdout
指定绘制窗口的设备,--title
指定构件的标题,--time-format %H\:%M\:%S
,用于指定设置完成后dialog
返回的时间格式,--timebox
就指定了当前dialog
构件的类型(因为dialog支持多种构件类型,所以这里必须指定)。最后5个参数0 0 $c_hour $c_minute 00
,分别以空格区分开,表示构件的宽度,高度,小时,分钟,秒,具体请参看dialog
的man
文档。完成后你可以按键盘的Tab
键切换选中项,然后移到你要修改的时间上,使用键盘的上下键更改数值。
我们首先用shell实现的功能就这些,暂时把它放下,我们再来实现用C语言实现的功能。
2.使用ncurses库实现倒计时显示
关于ncurses库的使用,如果你完成过实验楼上面的另一个项目课C语言贪吃蛇,那么你应该比较熟悉它的使用了,因为这个项目课不是专门介绍ncuses的,所以这里也不会用更多其它关于ncurses的内容了,当然除了我们用到的。
实现显示大号字体(数字)如前面的效果图,因为ncurese本身是没有关于字体设置的实现的,所以这需要我们自己来绘制,我们考虑可以为每个数字 建立一个基于父窗体口的子窗口,然后在每个窗口中绘制一个数字,要绘制数字,我们还需要自定义一组字模(只包含10个数字和一个冒号)。这里我们一共需要 8个子窗体,下面这个函数讲创建多个子窗体
void create_clock_subwindow(WINDOW *win) { int top_win_width, top_win_height; int starty, startx; int i; // 获取父窗口的高度好宽度 getmaxyx(win, top_win_height, top_win_width); if (top_win_height < D_HEIGHT || top_win_width < D_WIDTH){ printf("window is too small, please relize the window\n"); return; } // 用于指示创建的子窗体绘制的起始位置 starty = (top_win_height - D_HEIGHT) / 2; startx = (top_win_width - D_WIDTH*8-7) / 2; for(i = 0; i < 8; i++){ digit_win[i] = subwin(win, D_HEIGHT, D_WIDTH, starty, startx+i*(D_WIDTH+1)); } }
八个子窗体都是基于主窗体的相对位置创建的,以便适应你重新更改终端的大小,这其中digit_win
是预先声明的一个全局数组,D_HEIGHT
,D_WIDTH
,为预先定义的两个指示每个数字的宽和高的宏定义
WINDOW *digit_win[8];
相信你更关心的是如何定义一个字模,呵呵,自己在纸上画好一个5*5的格子,然后画上数字,像你小时候看过的电子表上的数字那样画(点阵字体),然后被覆盖的格子的位置填上1
,否则为0
,然后基于这11个格子建立一个三维数组就像下面这样:
char digit[11][5][5] = { { { 1,1,1,1,1 }, /* 0 */ { 1,0,0,0,1 }, { 1,0,0,0,1 }, { 1,0,0,0,1 }, { 1,1,1,1,1 } }, { { 0,0,0,0,1 }, /* 1 */ { 0,0,0,0,1 }, { 0,0,0,0,1 }, { 0,0,0,0,1 }, { 0,0,0,0,1 } }, { { 1,1,1,1,1 }, /* 2 */ { 0,0,0,0,1 }, { 1,1,1,1,1 }, { 1,0,0,0,0 }, { 1,1,1,1,1 } }, { { 1,1,1,1,1 }, /* 3 */ { 0,0,0,0,1 }, { 1,1,1,1,1 }, { 0,0,0,0,1 }, { 1,1,1,1,1 } }, { { 1,0,0,0,1 }, /* 4 */ { 1,0,0,0,1 }, { 1,1,1,1,1 }, { 0,0,0,0,1 }, { 0,0,0,0,1 } }, { { 1,1,1,1,1 }, /* 5 */ { 1,0,0,0,0 }, { 1,1,1,1,1 }, { 0,0,0,0,1 }, { 1,1,1,1,1 } }, { { 1,1,1,1,1 }, /* 6 */ { 1,0,0,0,0 }, { 1,1,1,1,1 }, { 1,0,0,0,1 }, { 1,1,1,1,1 } }, { { 1,1,1,1,1 }, /* 7 */ { 0,0,0,0,1 }, { 0,0,0,0,1 }, { 0,0,0,0,1 }, { 0,0,0,0,1 } }, { { 1,1,1,1,1 }, /* 8 */ { 1,0,0,0,1 }, { 1,1,1,1,1 }, { 1,0,0,0,1 }, { 1,1,1,1,1 } }, { { 1,1,1,1,1 }, /* 9 */ { 1,0,0,0,1 }, { 1,1,1,1,1 }, { 0,0,0,0,1 }, { 1,1,1,1,1 } }, { { 0,0,0,0,0 }, /* : */ { 0,0,1,0,0 }, { 0,0,0,0,0 }, { 0,0,1,0,0 }, { 0,0,0,0,0 } } }; void display_a_digit(WINDOW *win, char digit[5][5], char isnear) { int y, x; int color; color = isnear?COLOR_PAIR(4):COLOR_PAIR(3); werase(win); // erase the window before re-put the number for(y = 0; y < 5; y++){ for(x = 0; x < 5; x++){ if (digit[y][x] == 1){ wattron(win, color); mvwprintw(win, y, x, "a"); wattroff(win, color); } } } }
这样我们边实现了可能最让你觉得困难的一部分代码
再看主程序
int main(int argc, char *argv[]) { char isnear; int screen_width, screen_height; int i; int hour_tens, hour_units, min_tens, min_units, sec_tens, sec_units; time_t current_time; long left_time, end_seconds; char *str; struct tm *c_time; WINDOW *win1; // 为存放提醒信息的字符串分配内存 str = malloc(256*sizeof(char)); memset(str, '\0', sizeof(char)*256); // 判断参数输入参数个数是否正确 if (argc < 2 || argc > 4){ printf("Usage: %s [options] <seconds> [info strings]\n", argv[0]); return 0; } if (strcmp(argv[1], "-c")){ // 将传递进的计时时间字符串(以秒为单位的时间)转换为lang型 left_time = atol(argv[1]); if(argc == 3) strcpy(str, argv[2]); }else if(!strcmp(argv[1], "-c")){ // 将传递进的计时时间字符串(以秒为单位的时间)转换为lan left_time = atol(argv[2]); if (argc == 4) strcpy(str, argv[3]); }else{ printf("Usage: %s [options] <seconds> [info strings]\n", argv[0]); return 0; } // 初始化ncurses initscr(); cbreak(); noecho(); // 自定义颜色 start_color(); init_color(COLOR_BLACK, 46, 52, 54); init_color(COLOR_BLUE, 52, 101, 164); init_color(COLOR_WHITE, 128, 128, 128); init_pair(1, COLOR_BLACK, COLOR_BLACK); // use for main win init_pair(2, COLOR_BLUE, COLOR_BLACK); // use for clock win init_pair(3, COLOR_BLUE, COLOR_BLUE); // use for clock digit init_pair(4, COLOR_RED, COLOR_RED); // use for clock digit init_pair(5, COLOR_WHITE, COLOR_BLACK); // use for clock digit // 创建病初始化住窗口 getmaxyx(stdscr, screen_height, screen_width); win1 = newwin(screen_height, screen_width, 0, 0); // 设置窗口背景颜色 wbkgd(win1, COLOR_PAIR(1)); // 获取系统时间,并计算出计时终止时间 time(¤t_time); end_seconds = current_time + left_time; // 进入主循环,开始计时 while (left_time){ // 休眠一秒钟,减少cpu占用时间 sleep(1); time(¤t_time); left_time = end_seconds - current_time; // 设置接近计时结束的标志,这里设置的为120s,即分钟,用于当计时只有两分钟时讲数字颜色换成红色 isnear = (left_time < 120)?TRUE:FALSE; // 当传入"-c"参数时,使用正常显示时间模式 if (strcmp(argv[1], "-c")){ hour_tens = left_time/3600/10; hour_units = left_time/3600%10; min_tens = left_time/60%60/10; min_units = left_time/60%60%10; sec_tens = left_time%60/10; sec_units = left_time%60%10; }else{ // 使用倒计时显示模式 c_time = localtime(¤t_time); hour_tens = c_time->tm_hour/10; hour_units = c_time->tm_hour%10; min_tens = c_time->tm_min/10; min_units = c_time->tm_min%10; sec_tens = c_time->tm_sec/10; sec_units = c_time->tm_sec%10; } // 更新数值前,清除主窗口显示 werase(win1); // 每次创建新的子窗体,以便适应终端的大小调整 create_info_subwindow(win1); // info window // 设置提醒信息显示的字体属性 wattron(info_win, COLOR_PAIR(5)); mvwprintw(info_win, 0, 0, "%s", str); wattron(info_win, COLOR_PAIR(5)); // 创建计时数字子窗体 create_clock_subwindow(win1); // 分别设置子窗体背景颜色 for(i = 0; i < 8; i++){ wbkgd(digit_win[i], COLOR_PAIR(2)); } // 讲相应时间的数字绘制到对应窗口中 display_a_digit(digit_win[0], digit[hour_tens], isnear); display_a_digit(digit_win[1], digit[hour_units], isnear); display_a_digit(digit_win[2], digit[10], isnear); display_a_digit(digit_win[3], digit[min_tens], isnear); display_a_digit(digit_win[4], digit[min_units], isnear); display_a_digit(digit_win[5], digit[10], isnear); display_a_digit(digit_win[6], digit[sec_tens], isnear); display_a_digit(digit_win[7], digit[sec_units], isnear); // 放置子窗体在主窗体中 touchwin(win1); // 刷新窗体 wrefresh(win1); } free(str); // 销毁所有窗体,回收系统资源 destory_all_win(win1); endwin(); return 0;
主程序最开始完成一些初始化工作,然后在一个主循环完成了主要的倒计时功能。
使用C语言通过ncurses库实现倒计时的功能完成了,我们再回到shell中去
3.在shell脚本中调用倒计时程序
小编我来说一句,这段小小的代码相信难不倒大家,小编也就不在这里多做叙述了,当然,如果有看不懂得地方欢迎登陆实验楼官方网站http://www.shiyanlou.com/courses/?course_type=project&tag=all
里面有更多详细的内容~
小编先行告退,各位看官请继续~
4.计时完成后,播放音乐和弹窗提醒
if [ $? -eq 0 ];then if [ -n "$music" ];then mocp -S $music mocp -p fi zenity --info --width=100 --text=$info if [ $? -eq 0 ];then _exit fi fi
注意这段代码,应紧接着上面那段代码的后面,因为这里用到了$?
,你如果学过shell编程,应该能明白这是为什么,$?
表示上一个程序的返回值。我们这里判断它是否的等于0,表示上面的计时程序是否计时结束,如果结束正常返回0,就表示该开始提醒你了。 播放音乐就用到了那个mocp程序,你如果不熟悉它的话,可以先安装好这个程序,并在命令行单独运行它来熟悉它的使用,遇到任何困难,最好的解决方法就是使用--help
参数和man
文档。
$ sudo apt-get install moc
上面mocp
的-S
的参数表示以服务的形式运行程序,-p
为play,-x
为退出服务,当然也会停止播放当前音乐,我们使用单独的函数完成程序退操作
function _exit() { if [ -n "$music" ];then mocp -x fi clear exit 0 }
上面的zenity
即为一个基于gtk的弹出式对话框程序,后多种构件样式可选,我们这里使用的是--info
构件
5.程序参数处理
如你预见的一样,这个程序多种参数,如果处理不好可能出现不必要的麻烦,所以这里单独说明一下这个程序是如何处理参数传递的。这里采用了一个很通用的方法,使用shift
命令移位传递参数
while [ -n "$1" ];do case "$1" in -c) option="-c" ;; -m) music="$2" ;; -i) info="$2" ;; *) show_erro ;; esac if [ "$1" == "-c" ];then shift else shift 2 fi done
从上面你可以看到,对-c
参数的处理有点不一样,因为其他几个参数都会有一个值对应,而-c
参数没有,所以它的移位操作只移动了一个位置。现在我们可以使用如下的方式传递参数
$ ./countdown -i "hello shiyanlou" -m music.mp3 $ ./countdown -c -i "hello shiyanlou" -m music.mp3 $ ./countdown -i "hello shiyanlou" $ ./countdown
至此主要的一些麻烦的问题我们都解决了,你在熟悉了上面的实现过程之后,可以参考以下完成代码(小编在这里插播一句,下面的是需要在我们实验楼网站的虚拟平台下才能实现的,不过看到这里,大家应该对于这个程序有了大致的认识,自己稍作修改就可完成属于自己的一个带提醒功能的计时器)
$ git clone https://github.com/shiyanlou/countdown.git
C程序使用下面的命令生成
$ gcc disclock.c -o disclock -lncurses
你可能需要安装如下程序
$ sudo apt-get install libncurses5-dev moc dialog zenity
感谢各位看官的支持~小编以后尽量会在每个工作日,来与大家分享小编整理的干货,小编会整理一些自己觉得比较实用的程序分享给大家,当然如果大家有需求也可以私信我,或者也可以登陆实验楼的课程版块http://www.shiyanlou.com/courses/?course_type=all,如果这些还满足不了看官您的需求,可以登录实验楼的分享版块http://forum.shiyanlou.com/forum.php?mod=guide&view=newthread,这里有在实验楼学习的孩纸们分享的干货和学习心得,最后小编祝大家在码农的康庄大道上一帆风顺!