понедельник, 7 мая 2012 г.

Красочные утилитки для Console Application

Что-то меня последнее время стал не устраивать обычный серый стиль консольного приложения)

И вот я решил это дело немного приукрасить и в результате написал маленький модуль с набором утилитарных функций.

Далее я расскажу о подробностях реализации и приведу листинг самого модуля, а так же минимальный низкоуровневый код самого консольного приложения.

Цветные надписи.

Итак, сначала я задумался о том, чтобы выводить хотя бы сроки разного цвета.
Цвет выводимого текста и фона под ним устанавливается функцией SetConsoleTextAttribute(), прототип который выглядит следующим образом:
C++
  1. BOOL WINAPI SetConsoleTextAttribute(  
  2.   __in  HANDLE hConsoleOutput,  
  3.   __in  WORD wAttributes  
  4. );  
Где hConsoleOutput - это дескриптор экранного буфера консоли, который можно получить следующим образом:
Delphi
  1. outHnd := GetStdHandle(STD_OUTPUT_HANDLE);  
wAttributes - атрибут, который включает цвета символа и фона. Цвет символа(foreground) заключён в разрядах 1-4 младшего байта, а цвет фона(background) в разрядах 5-8. Цвета идут от 0 до 15.
По умолчанию стоит серебряный цвет символа(7) на чёрном фоне(0), записывается это так:
Delphi
  1. SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 7 + 0 * 16);  
А, например, чтобы установить красный шрифт(12) на тёмно синем(1) фоне, надо вызвать функцию  SetConsoleTextAttribute() с такими параметрами:
Delphi
  1. SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 12 + 1 * 16);  
Думаю с этим ясно.

Такими средствами я быстренько сообразил функцию, которая выводила цветную строку, но потом я стал испытывать неудобство, потому что чтобы написать в одну строку несколько цветов, я должен был несколько раз вызывать эту функцию, да ещё и прописывать в неё атрибуты, указывающие на цвет.
Поэтому я начал думать над гибкостью в этом плане.

Разноцветные строки.

В ходе раздумий, я придумал определённые теги, которые можно использовать прямо в строке.
Как это всё выглядит можно увидеть в листинге утилитарного модуля uConsoleUtils о котором я тут рассказываю)
Delphi 2007
  1. unit uConsoleUtils;  
  2.   
  3. interface  
  4.   
  5. uses  
  6.   Windows;  
  7.   
  8. type  
  9.   TConsoleTextColor = (tcBlack, tcNavy, tcGreen, tcTeal, tcMaroon, tcPurple,  
  10.     tcOlive, tcSilver, tcGray, tcBlue, tcLime, tcAqua, tcRed, tcFuchsia,  
  11.     tcYellow, tcWhite);  
  12.   
  13.   TArrOfCTC = array[0..ord(High(TConsoleTextColor))] of string[2];  
  14.   
  15. const  
  16.   // console colors string tags  
  17.   CCST: TArrOfCTC = ('^k''^n''^g''^t''^m''^p''^o''^s''^v',  
  18.     '^b''^l''^a''^r''^f''^y''^w');  
  19.   
  20. var  
  21.   inHnd, outHnd : THandle;  
  22.   
  23. // funcs  
  24.   procedure cuWriteText(aText: string; aFG: TConsoleTextColor = tcSilver; aBG: TConsoleTextColor = tcBlack);  
  25.   function cuGetStrColorCode(const aText: string): integer;  
  26.   function cuWriteColStr(const aText: string; aBG: TConsoleTextColor = tcBlack): boolean;  
  27.   function cuWriteLnColStr(const aText: string; aBG: TConsoleTextColor = tcBlack): boolean;  
  28.   
  29. implementation  
  30.   
  31. procedure cuWriteText(aText: string; aFG: TConsoleTextColor = tcSilver; aBG: TConsoleTextColor = tcBlack);  
  32. var  
  33.   txt : string;  
  34. begin  
  35.   // Конвертируем из Windows-кодировки в DOS  
  36.   SetLength(txt, length(aText)); // Создаем буфер для конвертации  
  37.   AnsiToOem(PAnsiChar(aText), PAnsiChar(txt));  
  38.   // Устанавливаем цвет текста  
  39.   SetConsoleTextAttribute(outHnd, ord(aFG) + ord(aBG) * 16);  
  40.   write(txt);  
  41.   txt := '';  
  42. end;  
  43.   
  44. function cuGetStrColorCode(const aText: string): integer;  
  45. var  
  46.   i : integer;  
  47. begin  
  48.   Result := -1;  
  49.   if length(aText) <> 2 then  
  50.     Exit;  
  51.   for i := 0 to length(CCST) - 1 do  
  52.     if aText = CCST[i] then  
  53.   begin  
  54.     Result := i;  
  55.     Exit;  
  56.   end;  
  57. end;  
  58.   
  59. function cuWriteColStr(const aText: string; aBG: TConsoleTextColor = tcBlack): boolean;  
  60. var  
  61.   i   : integer;  
  62.   sl  : integer;  
  63.   txt, outt : string;  
  64.   fg  : smallint;  
  65. begin  
  66.   Result := False;  
  67.   sl := length(aText);  
  68.   txt := '';  
  69.   i := 1;  
  70.   while i <= sl do  
  71.   begin  
  72.     fg := -1;  
  73.     if i <= (sl - 1then  
  74.       fg := cuGetStrColorCode(copy(aText, i, 2));  
  75.     if fg >= 0 then  
  76.     begin  
  77.       Result := True;  
  78.       if txt <> '' then  
  79.       begin  
  80.         SetLength(outt, length(txt));  
  81.         AnsiToOem(PAnsiChar(txt), PAnsiChar(outt));  
  82.         write(outt);  
  83.         outt := '';  
  84.         txt := '';  
  85.       end;  
  86.       SetConsoleTextAttribute(outHnd, fg + ord(aBG) * 16);  
  87.       inc(i, 2);  
  88.     end  
  89.     else  
  90.     begin  
  91.       txt := txt + aText[i];  
  92.       inc(i);  
  93.     end;  
  94.   end;  
  95.   if txt <> '' then  
  96.   begin  
  97.     SetLength(outt, length(txt));  
  98.     AnsiToOem(PAnsiChar(txt), PAnsiChar(outt));  
  99.     write(outt);  
  100.   end;  
  101.   outt := '';  
  102. end;  
  103.   
  104. function cuWriteLnColStr(const aText: string; aBG: TConsoleTextColor = tcBlack): boolean;  
  105. begin  
  106.   Result := cuWriteColStr(aText, aBG);  
  107.   writeln;  
  108. end;  
  109.   
  110. initialization  
  111.   inHnd := GetStdHandle(STD_INPUT_HANDLE);  
  112.   outHnd := GetStdHandle(STD_OUTPUT_HANDLE);  
  113.   
  114. end.  
Назначения функций в данном модуле:
  • cuWriteText - выводит текст указанного цвета на указанном фоне;
  • cuGetStrColorCode - возвращает код цвета по его тегу;
  • cuWriteColStr - пишет строку с учётом всех цветовых тегов и возвращает True если найден хоть один цветовой тег;
  • cuWriteLnColStr - яснопонятно.
Так же во всех функциях можно заметить поддержку вывода русских текстов.

Насчёт разноцветности это всё, теперь "приукрасим" организацию кода самого консольного приложения.

Цикл обработки ввода.

Для себя я определил следующий минимум требований для приложения:
  1. переименование окна приложения;
  2. перехват нажатий виртуальных клавиш;
  3. возможность вводить текстовые команды;
  4. возможность писать текст в котором будут "на лету" учитываться цветовые теги.
Не буду томить, приведу сразу листинг того, что получилось:
Delphi 2007
  1. program ConAppUtilsDemo;  
  2.   
  3. {$APPTYPE CONSOLE}  
  4.   
  5. uses  
  6.   Windows, SysUtils,  
  7.   uConsoleUtils;  
  8.   
  9. var  
  10.   IR  : INPUT_RECORD; // вспомогательная  
  11.   crd : cardinal// вспомогательная  
  12.   prevCh  : AnsiChar; // предыдущий нажатый символ  
  13.   ach : AnsiChar; // текущий нажатый символ  
  14.   cmd : string// введённая команда  
  15.   
  16. // funcs  
  17.   procedure cmdInfo;  
  18.   begin  
  19.     cuWriteLnColStr('^w--------------------------------------------');  
  20.     cuWriteLnColStr(#9'^sДоступны следующие команды:');  
  21.     cuWriteLnColStr(#9#9'^w info^s - запрос информации по командам;');  
  22.     cuWriteLnColStr(#9#9'^w exit^s - выход из главного цикла.');  
  23.     cuWriteLnColStr('^w--------------------------------------------^s');  
  24.   end;  
  25.   
  26. begin  
  27.   // переименовываем консольное окно  
  28.   SetConsoleTitle(PChar('Con App Utils Demo'));  
  29.   writeln;  
  30.   cuWriteLnColStr(#9#9'^b<<< Console Application Utils Demo. >>>');  
  31.   writeln;  
  32.   cuWriteLnColStr('^lЗапущен главный цикл.^s Для выхода введите команду^w exit^s либо нажмите^w F10^s.');  
  33.   cmdInfo;  
  34.   // запускаем основной цикл приложения  
  35.   prevCh := #0;  
  36.   cmd := '';  
  37.   repeat  
  38.     WaitForSingleObjectEx(inHnd, 0, False); // ждем события  
  39.     ReadConsoleInput(inHnd, IR, 1, crd); // берем запись в переменную IR  
  40.     // обработка нажатия клавиш  
  41.     if (IR.EventType = KEY_EVENT) and IR.Event.KeyEvent.bKeyDown then  
  42.     begin  
  43.       ach := IR.Event.KeyEvent.AsciiChar;  
  44.       if ord(ach) >= 128 then  
  45.         ach := chr(ord(ach) + 64);  
  46.       case ach of  
  47.         #8// back space  
  48.           begin  
  49.             write(#8); write(#0); write(#8);  
  50.             prevCh := #0// обнуляем предыдущий символ  
  51.             cmd := copy(cmd, 1, length(cmd) - 1);  
  52.           end;  
  53.         #9// tab  
  54.           begin  
  55.             write(#9);  
  56.             prevCh := #0// обнуляем предыдущий символ  
  57.           end;  
  58.         #13// enter  
  59.           begin  
  60.             if cmd = 'exit' then  
  61.             begin // выходим из главного цикла  
  62.               writeln;  
  63.               Break;  
  64.             end  
  65.             else  
  66.             if cmd = 'info' then  
  67.             begin // выводим список доступных команд  
  68.               writeln;  
  69.               cmdInfo;  
  70.             end  
  71.             else  
  72.             if cmd <> '' then  
  73.             begin  
  74.               writeln;  
  75.               cuWriteColStr('^rНеизвестная команда:^w ' + cmd + '^s');  
  76.             end;  
  77.             cmd := '';  
  78.             write(#13#10);  
  79.             prevCh := #0;  
  80.           end;  
  81.         //#27: Break; // обработка Esc  
  82.         #32..#255:  
  83.           begin  
  84.             if prevCh >= #32 then  
  85.             begin  
  86.               write(#8);  
  87.               if cuWriteColStr(prevCh + ach) then  
  88.               begin  
  89.                 write(#0);  
  90.                 write(#8);  
  91.                 cmd := copy(cmd, 1, length(cmd) - 1);  
  92.                 ach := #0;  
  93.               end;  
  94.             end  
  95.             else  
  96.               write(ach);  
  97.             prevCh := ach;  
  98.             if ach <> #0 then  
  99.               cmd := cmd + ach; // состыковываем команду  
  100.           end;  
  101.       end;  
  102.     end;  
  103.     // обработка нажатия виртуальных клавиш  
  104.     if IR.Event.KeyEvent.wVirtualKeyCode = VK_F10 then  
  105.     begin  
  106.       writeln;  
  107.       Break;  
  108.     end;  
  109.     sleep(2); // не напрягаем проц  
  110.   until False;  
  111.   cuWriteColStr('^lГлавный цикл остановлен.^s для выхода нажмите^w Enter^s...');  
  112.   readln;  
  113. end.  
 С самого начала можно заметить с помощью какой функции переименовывается окно приложения, а далее функция cuWriteColStr() в действии. Потом пользователя информируют о возможных командах приложения и запускается главный цикл.

В цикле изображена низкоуровневая обработка нажатий клавиш, в том числе и виртуальных. Т.к. обработка низкоуровневая, то многие основные действия необходимо обрабатывать самому, например Enter и BackSpace.
Для распознавания написания цветовых тегов я ввёл дополнительную переменную prevCh т.к. тег состоит из двух символов.
Ну а в самом низу цикла можно наблюдать отлов нажатия виртуальных клавиш)

Смотрим всё это дело в действии:


В дальнейшем, может быть, расширю обзор по фишкам в консольных приложениях, если понадобиться, ну а пока на этом всё.

PS: только один недочёт почему-то проявляется - печать русских символов отображается со сдвигом по ascii таблице кратным 32-м.

Комментариев нет:

Отправить комментарий