使用curses管理基于文本的屏幕--(八)


CD管理程序


现在我们已经了解了curses所提供了功能,我们可以继续开发我们的例子程序。在这里所展示是一个使用curses库的C语言版本。他提供了一些高级的特性,包括更为清晰的屏幕信息显示以及用于跟踪列表的滚动窗口。

完整的程序共页长,所以我们将其分为几部分,在每一部分中介绍一些函数。

试验--一个新的CD管理程序

1 首先,我们包含所有的头文件以及一些全局常量。

#include  <unistd.h>
#include  <stdlib.h>
#include  <stdio.h>
#include  <string.h>
#include  <curses.h>
#define MAX_STRING 80    /* Longest allowed response       */
#define MAX_ENTRY 1024   /* Longest allowed database entry */
#define  MESSAGE_LINE 6    /* Misc. messages on this line     */
#define  ERROR_LINE   22   /* Line to use for errors          */
#define  Q_LINE       20   /* Line for questions              */
#define  PROMPT_LINE  18   /* Line for prompting on           */

2 接下来,我们需要一些全局变量。变量current_cd用于存储当前我们所用的CD标题。对其进行初始化,从而其第一个字符为null表明"没有选中CD"。/0并不是严格必须的,但是他可以保证变量已经进行了初始化,这是一个很好的习惯。变量current_cat用于记录当前CD的分类。

static char current_cd[MAX_STRING] = “/0”;
static char current_cat[MAX_STRING];

3 现在需要定义一些文件名。为了简单,在这个版本中文件名都使用固定的文件名,包括临时文件名。当两个用户在相同的目录中运行这个程序时就会出现问题。

const char *title_file = “title.cdb”;
const char *tracks_file = “tracks.cdb”;
const char *temp_file = “cdb.tmp”;

4 最后,我们定义函数原型。

void clear_all_screen(void);
void get_return(void);
int get_confirm(void);
int getchoice(char *greet, char *choices[]);
void draw_menu(char *options[], int highlight,
               int start_row, int start_col);
void insert_title(char *cdtitle);
void get_string(char *string);
void add_record(void);
void count_cds(void);
void find_cd(void);
void list_tracks(void);
void remove_tracks(void);
void remove_cd(void);
void update_cd(void);

5 在我们查看具体的实现之前,我们需要一些菜单结构(实际上为一个菜单选项的数组)。第一个字符是当菜单被选中时所返回的字符;其余的是要显示的字符。当一个CD被选中时会显示扩展菜单。

char *main_menu[] =
{
    “add new CD”,
    “find CD”,
    “count CDs and tracks in the catalog”,
    “quit”,
    0,
};
char *extended_menu[] =
{
    “add new CD”,
    “find CD”,
    “count CDs and tracks in the catalog”,
    “list tracks on current CD”,
    “remove current CD”,
    “update track information”,
    “quit”,
   0,
};

这样就完成了所有的初始化工作。现在我们可以进入程序功能了,但是首先,我们需要总结一下函数之间的关系,所有16个函数,其功能可以分为:

绘制菜单
向数据库中添加CD
获取并显示CD数据

试验--主函数
主函数允许我们由菜单中进行选择,直到我们选择退出。

int main()
{
    int choice;
    initscr();
    do {
         choice = getchoice(“Options:”,
                            current_cd[0] ? extended_menu : main_menu);
         switch (choice) {
         case ‘q’:
             break;
         case ‘a’:
          add_record();
          break;
      case ‘c’:
          count_cds();
          break;
      case ‘f’:
          find_cd();
          break;
      case ‘l’:
          list_tracks();
          break;
      case ‘r’:
          remove_cd();
          break;
      case ‘u’:
          update_cd();
          break;
      }
  } while (choice != ‘q’);
  endwin();
  exit(EXIT_SUCCESS);
}

试验--菜单
1 main函数所调用的getchoice函数是我们将在这一部介绍的主要函数。调用getchoice会传递给其一个greet以及choices,这会指向主菜单或是扩展菜单(依据是否选择了一个CD)。我们可以在前面的main函数中看到这些函数调用。

int getchoice(char *greet, char *choices[])
{
    static int selected_row = 0;
    int max_row = 0;
    int start_screenrow = MESSAGE_LINE, start_screencol = 10;
    char **option;
    int selected;
    int key = 0;
    option = choices;
    while (*option) {
        max_row++;
        option++;
    }
/* protect against menu getting shorter when CD deleted */
    if (selected_row >= max_row)
        selected_row = 0;
    clear_all_screen();
    mvprintw(start_screenrow - 2, start_screencol, greet);
  keypad(stdscr, TRUE);
  cbreak();
  noecho();
  key = 0;
  while (key != ‘q’ && key != KEY_ENTER && key != ‘/n’) {
      if (key == KEY_UP) {
          if (selected_row == 0)
              selected_row = max_row - 1;
          else
              selected_row—;
      }
      if (key == KEY_DOWN) {
          if (selected_row == (max_row - 1))
              selected_row = 0;
          else
              selected_row++;
      }
      selected = *choices[selected_row];
      draw_menu(choices, selected_row, start_screenrow,
                         start_screencol);
      key = getch();
  }
  keypad(stdscr, FALSE);
  nocbreak();
  echo();
  if (key == ‘q’)
      selected = ‘q’;
  return (selected);
}

2 在这里我们可以注意到在getchoice函数中调用了两个局部函数:clear_all_screen与draw_menu。我们首先来看一下draw_menu:

void draw_menu(char *options[], int current_highlight,
               int start_row, int start_col)
{
    int current_row = 0;
    char **option_ptr;
    char *txt_ptr;
    option_ptr = options;
    while (*option_ptr) {
        if (current_row == current_highlight) attron(A_STANDOUT);
        txt_ptr = options[current_row];
        txt_ptr++;
        mvprintw(start_row + current_row, start_col, “%s”, txt_ptr);
        if (current_row == current_highlight) attroff(A_STANDOUT);
        current_row++;
        option_ptr++;
    }
    mvprintw(start_row + current_row + 3, start_col,
             “Move highlight then press Return “);
    refresh();
}

3 clear_all_screen函数用于清除屏幕并重新输出标题。如果选择了一个CD,则会显示其信息。

void clear_all_screen()
{
    clear();
    mvprintw(2, 20, “%s”, “CD Database Application”);
    if (current_cd[0]) {
        mvprintw(ERROR_LINE, 0, “Current CD: %s: %s/n”,
                 current_cat, current_cd);
    }
    refresh();
}

下面我们来看一下添加和更新CD数据库的函数。由main函数中调用的函数有add_record,update_cd以及remove_cd。这些函数都会调用一些我们在下面部分定义的函数。

试验--数据库文件操作

1 首先,我们如何向数据库中添加一个新的CD记录呢?

void add_record()
{
    char catalog_number[MAX_STRING];
    char cd_title[MAX_STRING];
    char cd_type[MAX_STRING];
    char cd_artist[MAX_STRING];
    char cd_entry[MAX_STRING];
    int screenrow = MESSAGE_LINE;
    int screencol = 10;
    clear_all_screen();
    mvprintw(screenrow, screencol, “Enter new CD details”);
    screenrow += 2;
    mvprintw(screenrow, screencol, “Catalog Number: “);
    get_string(catalog_number);
    screenrow++;
    mvprintw(screenrow, screencol, “      CD Title: “);
    get_string(cd_title);
    screenrow++;
    mvprintw(screenrow, screencol, “       CD Type: “);
    get_string(cd_type);
    screenrow++;
    mvprintw(screenrow, screencol, “        Artist: “);
    get_string(cd_artist);
    screenrow++;
  mvprintw(PROMPT_LINE-2, 5, “About to add this new entry:”);
  sprintf(cd_entry, “%s,%s,%s,%s”,
          catalog_number, cd_title, cd_type, cd_artist);
  mvprintw(PROMPT_LINE, 5, “%s”, cd_entry);
  refresh();
  move(PROMPT_LINE, 0);
  if (get_confirm()) {
      insert_title(cd_entry);
      strcpy(current_cd, cd_title);
      strcpy(current_cat, catalog_number);
  }
}

3 get_confirm函数提示并读取用户的确认信息。他会读取用户的输入字符串并且检测第一个字符是否为Y或是y。如果检测到的为其他字符,则不会给出确认。

int get_confirm()
{
    int confirmed = 0;
    char first_char;
    mvprintw(Q_LINE, 5, “Are you sure? “);
    clrtoeol();
    refresh();
    cbreak();
    first_char = getch();
    if (first_char == ‘Y’ || first_char == ‘y’) {
        confirmed = 1;
    }
    nocbreak();
    if (!confirmed) {
        mvprintw(Q_LINE, 1, “    Cancelled”);
        clrtoeol();
        refresh();
        sleep(1);
    }
    return confirmed;
}

4 最后,我们来看一下insert_title函数。这个函数会通过在标题文件的尾部添加一个标题字符串来向CD数据库中添加一个标题。

void insert_title(char *cdtitle)
{
    FILE *fp = fopen(title_file, “a”);
    if (!fp) {
        mvprintw(ERROR_LINE, 0, “cannot open CD titles database”);
    } else {
        fprintf(fp, “%s/n”, cdtitle);
        fclose(fp);
    }
}

5 我们继续来讨论由main所调用的其他的文件操作函数。我们由update_cd函数开始。这个函数使用一个滚动子窗体,并且需要一些所定义的全局内容,因为在后面的list_tracks函数中会需要这些内容。他们是:

#define BOXED_LINES  11
#define BOXED_ROWS   60
#define BOX_LINE_POS 8
#define BOX_ROW_POS  2

update_cd函数允许用户重新输入当前CD的这些音轨信息。在删除以前的音轨记录以后,他会提示输入新的信息。

void update_cd()
{
    FILE *tracks_fp;
    char track_name[MAX_STRING];
    int len;
    int track = 1;
    int screen_line = 1;
    WINDOW *box_window_ptr;
    WINDOW *sub_window_ptr;
    clear_all_screen();
    mvprintw(PROMPT_LINE, 0, “Re-entering tracks for CD. “);
    if (!get_confirm())
        return;
    move(PROMPT_LINE, 0);
    clrtoeol();
    remove_tracks();
    mvprintw(MESSAGE_LINE, 0, “Enter a blank line to finish”);
    tracks_fp = fopen(tracks_file, “a”);

我们会在稍后继续列表函数的讨论;在这里我们会做一个简短的小结来强调我们是如何通过滚动窗体输入信息的。技巧就是设置一个子窗体,在边缘绘制一个盒子,然后在这个子窗体中添加一个新的滚动子窗体。

  box_window_ptr = subwin(stdscr, BOXED_LINES + 2, BOXED_ROWS + 2,
                          BOX_LINE_POS - 1, BOX_ROW_POS - 1);
  if (!box_window_ptr)
      return;
  box(box_window_ptr, ACS_VLINE, ACS_HLINE);
  sub_window_ptr = subwin(stdscr, BOXED_LINES, BOXED_ROWS,
                          BOX_LINE_POS, BOX_ROW_POS);
  if (!sub_window_ptr)
      return;
  scrollok(sub_window_ptr, TRUE);
  werase(sub_window_ptr);
  touchwin(stdscr);
  do {
      mvwprintw(sub_window_ptr, screen_line++, BOX_ROW_POS + 2,
                “Track %d: “, track);
      clrtoeol();
      refresh();
      wgetnstr(sub_window_ptr, track_name, MAX_STRING);
      len = strlen(track_name);
      if (len > 0 && track_name[len - 1] == ‘/n’)
          track_name[len - 1] = ‘/0’;
  if (*track_name)
  fprintf(tracks_fp, “%s,%d,%s/n”, current_cat, track, track_name);
      track++;
      if (screen_line > BOXED_LINES - 1) {
          /* time to start scrolling */
          scroll(sub_window_ptr);
          screen_line—;
      }
  } while (*track_name);
  delwin(sub_window_ptr);
  fclose(tracks_fp);
}

6 main函数所调用的最后一个函数为remove_cd函数。

void remove_cd()
{
    FILE *titles_fp, *temp_fp;
    char entry[MAX_ENTRY];
    int cat_length;
    if (current_cd[0] == ‘/0’)
        return;
    clear_all_screen();
    mvprintw(PROMPT_LINE, 0, “About to remove CD %s: %s. “,
             current_cat, current_cd);
    if (!get_confirm())
        return;
    cat_length = strlen(current_cat);
  /* Copy the titles file to a temporary, ignoring this CD */
  titles_fp = fopen(title_file, “r”);
  temp_fp = fopen(temp_file, “w”);
  while (fgets(entry, MAX_ENTRY, titles_fp)) {
      /* Compare catalog number and copy entry if no match */
      if (strncmp(current_cat, entry, cat_length) != 0)
          fputs(entry, temp_fp);
  }
  fclose(titles_fp);
  fclose(temp_fp);
  /* Delete the titles file, and rename the temporary file */
  unlink(title_file);
  rename(temp_file, title_file);
  /* Now do the same for the tracks file */
  remove_tracks();
  /* Reset current CD to ‘None’ */
  current_cd[0] = ‘/0’;
}

7 我们现在所需要只是列出remove_tracks函数,这个函数会由当前的CD删除音轨信息。他会由update_cd与remove_cd函数所调用。

void remove_tracks()
{
    FILE *tracks_fp, *temp_fp;
    char entry[MAX_ENTRY];
    int cat_length;
    if (current_cd[0] == ‘/0’)
        return;
    cat_length = strlen(current_cat);
    tracks_fp = fopen(tracks_file, “r”);
    if (tracks_fp == (FILE *)NULL) return;
    temp_fp = fopen(temp_file, “w”);
    while (fgets(entry, MAX_ENTRY, tracks_fp)) {
        /* Compare catalog number and copy entry if no match */
        if (strncmp(current_cat, entry, cat_length) != 0)
            fputs(entry, temp_fp);
    }
    fclose(tracks_fp);
    fclose(temp_fp);
    /* Delete the tracks file, and rename the temporary file */
    unlink(tracks_file);
    rename(temp_file, tracks_file);
}

试验--查询CD数据库

1 下面这个函数会搜索数据库,计数标题与音轨。

void count_cds()
{
    FILE *titles_fp, *tracks_fp;
    char entry[MAX_ENTRY];
    int titles = 0;
    int tracks = 0;
    titles_fp = fopen(title_file, “r”);
    if (titles_fp) {
        while (fgets(entry, MAX_ENTRY, titles_fp))
            titles++;
        fclose(titles_fp);
    }
    tracks_fp = fopen(tracks_file, “r”);
    if (tracks_fp) {
        while (fgets(entry, MAX_ENTRY, tracks_fp))
            tracks++;
        fclose(tracks_fp);
    }
    mvprintw(ERROR_LINE, 0,
             “Database contains %d titles, with a total of %d tracks.”,
             titles, tracks);
    get_return();
}

2 我们也许已经不记得我们最喜欢的CD了,不用担心!通过输入一些信息,我们可以通过find_cd来查进行查找。他会提示输入一个子串并且在数据库中进行匹配,并且将全局变量current_cd设置为所查找到的CD标题。

void find_cd()
{
    char match[MAX_STRING], entry[MAX_ENTRY];
    FILE *titles_fp;
    int count = 0;
    char *found, *title, *catalog;
    mvprintw(Q_LINE, 0, “Enter a string to search for in CD titles: “);
    get_string(match);
    titles_fp = fopen(title_file, “r”);
    if (titles_fp) {
        while (fgets(entry, MAX_ENTRY, titles_fp)) {
            /* Skip past catalog number */
            catalog = entry;
            if (found == strstr(catalog, “,”)) {
                *found = ‘/0’;
              title = found + 1;
              /* Zap the next comma in the entry to reduce it to
                 title only */
              if (found == strstr(title, “,”)) {
                  *found = ‘/0’;
                  /* Now see if the match substring is present */
                  if (found == strstr(title, match)) {
                       count++;
                       strcpy(current_cd, title);
                       strcpy(current_cat, catalog);
                  }
              }
          }
      }
      fclose(titles_fp);
  }
  if (count != 1) {
      if (count == 0) {
          mvprintw(ERROR_LINE, 0, “Sorry, no matching CD found. “);
      }
      if (count > 1) {
          mvprintw(ERROR_LINE, 0,
                    “Sorry, match is ambiguous: %d CDs found. “, count);
      }
      current_cd[0] = ‘/0’;
      get_return();
  }
}

尽管catalog所向的数组要比current_cat大得多,并且可能会覆写内存,但是fgets中的检测避免了这种可能。

3 最后我们需要在屏幕上列出所选择的CD音轨信息。我们会在最后一部分中利用update_cd中所用的#define内容。

void list_tracks()
{
    FILE *tracks_fp;
    char entry[MAX_ENTRY];
    int cat_length;
    int lines_op = 0;
    WINDOW *track_pad_ptr;
    int tracks = 0;
    int key;
    int first_line = 0;
    if (current_cd[0] == ‘/0’) {
        mvprintw(ERROR_LINE, 0, “You must select a CD first. “);
        get_return();
        return;
    }
clear_all_screen();
cat_length = strlen(current_cat);
/* First count the number of tracks for the current CD */
tracks_fp = fopen(tracks_file, “r”);
if (!tracks_fp)
     return;
while (fgets(entry, MAX_ENTRY, tracks_fp)) {
     if (strncmp(current_cat, entry, cat_length) == 0)
         tracks++;
}
fclose(tracks_fp);
  /* Make a new pad, ensure that even if there is only a single
    track the PAD is large enough so the later prefresh() is always
    valid. */
track_pad_ptr = newpad(tracks + 1 + BOXED_LINES, BOXED_ROWS + 1);
if (!track_pad_ptr)
     return;
tracks_fp = fopen(tracks_file, “r”);
if (!tracks_fp)
     return;
mvprintw(4, 0, “CD Track Listing/n”);
/* write the track information into the pad */
while (fgets(entry, MAX_ENTRY, tracks_fp)) {
     /* Compare catalog number and output rest of entry */
     if (strncmp(current_cat, entry, cat_length) == 0) {
         mvwprintw(track_pad_ptr, lines_op++, 0, “%s”,
                   entry + cat_length + 1);
     }
}
fclose(tracks_fp);
if (lines_op > BOXED_LINES) {
     mvprintw(MESSAGE_LINE, 0,
              “Cursor keys to scroll, RETURN or q to exit”);
} else {
     mvprintw(MESSAGE_LINE, 0, “RETURN or q to exit”);
}
wrefresh(stdscr);
keypad(stdscr, TRUE);
cbreak();
noecho();
key = 0;
while (key != ‘q’ && key != KEY_ENTER && key != ‘/n’) {
     if (key == KEY_UP) {
         if (first_line > 0)
             first_line—;
     }
     if (key == KEY_DOWN) {
          if (first_line + BOXED_LINES + 1 < tracks)
              first_line++;
      }
      /* now draw the appropriate part of the pad on the screen */
      prefresh(track_pad_ptr, first_line, 0,
               BOX_LINE_POS, BOX_ROW_POS,
               BOX_LINE_POS + BOXED_LINES, BOX_ROW_POS + BOXED_ROWS);
      key = getch();
  }
  delwin(track_pad_ptr);
  keypad(stdscr, FALSE);
  nocbreak();
  echo();
}

4 最后两个函数调用get_return,这会提示并且读取一个回车,而忽略其他字符。

void get_return()
{
    int ch;
    mvprintw(23, 0, “%s”, “ Press return “);
    refresh();
    while ((ch = getchar()) != ‘/n’ && ch != EOF);
}

小结

在这一章,我们探讨了curses库。curses库为基于文本的程序提供了一个很好的方法来控制屏幕与读取键盘。尽管curses库并没有提供像通用终端接口(GTI)和直接的termios访问那样多的控制,但是他很容易使用。如果我们正在编写一个全屏幕,基于文本的程序,我们应考虑使用curses库来为我们管理屏幕与键盘。
原文地址:https://www.cnblogs.com/dyllove98/p/2461978.html