ЛЕКЦИЯ 8
Тема: а примере проектирования простой консольной формы ввода логина и пароля рассмотрены особенности составных типов данных: структур и перечислений, а так же основные принципы пошагового проектирования интерактивных программ.
"Хорошая структура программы и плохая структукра данных – плохая программа. Хорошая структура данных и плохая структура программного кода – хорошая программа!"
Новые навыки.
Построение экранных форм (в режиме псевдографики).
Определение и использование структур и перечислений для структуризации данных программного продукта.
Пошаговая разработки программного приложения по технологии "сверху вниз".
На предыдущих практических занятиях были определены требования к учебной интерактивной программе определения логина и пароля пользователя, а так же общая архитектура программного проекта, повторяющая структуру программирования системы Arduino: определение главных подпрограмм setup – начальные установки, и loop – основной интерактивный цикл работы программы.
Функционал приложения определяется так.
Программа должна обеспечивать:
- возможность ввода текстового значения "логина" (буквы латинского алфавита, кириллицы и цифры) с отображением вводимого текста на экране и возможностью его редактирования;
- возможность ввода текстового значения "пароля" (буквы латинского алфавита, кириллицы и цифры) с отображением условных символов вместо вводимых символов текста и возможностью его редактирования;
- очистку полей ввода логина и пароля;
- переход "вперед-назад" между полями ввода.
Напомним общую структуру модулей разрабатываемого приложения (проекта):
<!--[if !vml]-->
Здесь модуль codec необходим для поддержки перекодировок ср1251 и ср866 системы Windows.
Начальный код модуля myprograms.c
#include "myprograms.h"
void setup(){
} // setup
void loop(){
} // loop
Общая концепция структуры системы может быть изображена в виде диаграммы деятельности:
<!--[if !vml]-->
Первый шаг разработки. Построение экранной формы.
Подпрограмма, реализующая построение этой формы – setPanel.
Определяем "болванку" данной подпрограммы и включаем ее ф подпрограмму setup.
void setPanel();
void setup(){
setPanel();
} // setup
void setPanel(){
} // setup
Сразу заметим, что строить заданную экранную форму одной подпрограммой в корне неверно!
Почему?
Внимательно рассмотрим эскиз требуемой формы:
<!--[if !vml]-->
Видно, что основной графический элемент формы – прямоугольник: окантока формы, выделение полей ввода, определение поля сброса данных.
Таким образом, подпрограмма построения прямоугольника – необходимый элемент подпрограммы построения формы.
Какие параметры (в данном случае), изменяются при построении конкретных прямоугольников формы? Точка "привязки" (координаты левого верхнего угла прямоугольника), ширина и высота. Напоминаем, что при использовании псевдографики все координаты определяются в "знакоместах" и начальное значение координат – единица!
Теперь можно определить подпрограмму построения прямоугольника:
void ramka( int x, int y, int w, int h, int dubL=0 ){
gotoxy( x, y );
uchar vl, vp, nl, np, gor, vert;
if ( dubL ){
vl=201; vp=187; nl=200; np=188; gor=205; vert=186;
} else {
vl=218; vp=191; nl=192; np=217; gor=196; vert=179;
}
putch( vl );
for ( int i=0; i<( w-1 ); i++ ) putch( gor );
putch( vp );
// niz
gotoxy( x, y+h );
putch( nl );
for ( int i=0; i<( w-1 ); i++ ) putch( gor );
putch( np );
for ( int i=1; i<h; i++ ) {
// levo
gotoxy( x, y+i ); putch( vert );
// pravo
gotoxy( x+w, y+i ); putch( vert );
}
} // ramka
Обратите внимание на еще один параметр – dubL. Он определяет, какой линией будет произведена прорисовка прямоугольника: одинарной или двойной.
Теперь можно, используя для перекодировки текстовых данных подпрограмму cp866cp1251, сформировать подпрограмму построения экранной формы – setPanel:
void setPanel(){
char s[50];
textbackground( BLUE );
textcolor( WHITE );
clrscr();
ramka( 1, 1, PANEL_W, PANEL_H, 1 );
ramka( 10, 3, 19, 5 );
ramka( 10, 9, 19, 5 );
gotoxy( 3, 5 );
puts( cp866cp1251( s, "ЛОГИН" ) );
gotoxy( 3, 11 );
puts( cp866cp1251( s, "ПАРОЛЬ" ) );
ramka( PANEL_W/2-5, 16, 11, 4 );
gotoxy( PANEL_W/2-3, 18 );
puts( cp866cp1251( s, "ОБНОВИТЬ" ) );
} // setPanel
Выделенным шрифтом отмечены выражения, определяющие иные (не "по умолчанию") цвета прорисовки: цвет фона и цвет текста. В данном случае – белый текст по голубому фону.
Использование структур для объединения "связанных" между собой данных. Например – цвет фона и цвет текста.
А что делать, если понадобиться сменить параметры цветовой палитры? Напрашивает решение: добавить два параметра процедуры: цвет фона и цвет текста.
На наш взгляд – это не лучшее решение: эти два параметра "связаны" друг с другом, представляют комбинацию этих величин.
В системе С, как в большинстве других языков программирования, для этих целей служат специальные конструкции: составные типы данных – структуры.
В данном случае определим структуру, позволяющую задать комбинацию параметров: "цвет поля"-"цвет шрифта".
typedef struct Colors_panel {
int textColor, backgroundColor;
} TColors_panel;
Примечание. Структура – это определяемый программистом набор байтов, элементы которого (точнее – их адреса в памяти) могут быть определены с помощью "внутренних" имен этой структуры. Формальное определения структур и приемы работы с ними выучите по предлагаемым учебникам. Здесь мы приводим практику их применения. Отметим, что определение структуры с помощью выражения typedef позволяет более "компактно" в дальнейшем записывать определения, включающие этот тип данных.
Обратите внимание на размер структуры (сумма размеров составляющих ее элементов) и на возможность присваивания значения:
typedef struct panel {
int textColor, backgroundColor;
} Tpanel;
Tpanel p1={10, 20}, p2={40, 50};
int main(){
printf( "size TColors_panel=%d\n", sizeof( Tpanel ) );
printf( "textColor=%d backgroundColor=%d\n",
p1.textColor, p1.backgroundColor );
p1 = p2;
printf( "textColor=%d backgroundColor=%d\n",
p1.textColor, p1.backgroundColor );
return 0;
} // main
<!--[if !vml]-->
Таким образом, мы имеем возможность работать как с отдельными элементами (составляющими) структуры, так со структурой в целом.
Теперь мы можем "предопределить" два вида цветовой гаммы нашего оконного приложения:
typedef struct Colors_panel {
int textColor, backgroundColor;
} TColors_panel;
Начальное присваивание можно реализоват так:
static TColors_panel wbPanel = { WHITE, BLACK };
static TColors_panel wblPanel = { WHITE, BLUE };
или так:
static TColors_panel wbPanel = { .textColor=WHITE, .backgroundColor=BLACK };
static TColors_panel wblPanel = { .textColor=WHITE, .backgroundColor=BLUE };
А новое определение подпрограммы прорисовки экранной формы приолжения буде выглядеть:
void setPanel( TColors_panel cp = wbPanel ){
char s[50];
textbackground( cp.backgroundColor );
textcolor( cp.textColor );
clrscr();
ramka( 1, 1, PANEL_W, PANEL_H, 1 );
ramka( 10, 3, 19, 5 );
ramka( 10, 9, 19, 5 );
gotoxy( 3, 5 );
puts( cp866cp1251( s, "ЛОГИН" ) );
gotoxy( 3, 11 );
puts( cp866cp1251( s, "ПАРОЛЬ" ) );
ramka( PANEL_W/2-5, 16, 11, 4 );
gotoxy( PANEL_W/2-3, 18 );
puts( cp866cp1251( s, "ОБНОВИТЬ" ) );
} // setPanel
Теперь варианты вызова этой полпрограммы:
void setup(){
//setPanel(); // черно-белая панель
setPanel( wblPanel ); // голубая-белая панель
} // setup
Результат обоих вызовов:
<!--[if !vml]--><!--[endif]--> <!--[if !vml]-->
Второй шаг разработки. Построение интерактивного функционала приложения.
Напомним основные элементы функционала:
- ввод и редактирование текстовых данных в поля логина и пароля, причем во втором случае, с заменой изображения вводимых символов на замещающие их условные символы;
- переход по нажатию определенных клавиш между полями ввода;
- возможность очистки полей ввода по требованию пользователя (нажатие клавиши "ВВОД") при нахождении курсора в поле "ОБНОВИТЬ".
Прежде всего отметим, что использование для наших целей (ввод и редактирование текста) стандартных средств С невозможно: ввод продолжается, пока "жмется клава".
Например – ввод логина с помощью gets (fgets) scanf.
void setup(){
setPanel();
//setPanel( wblPanel );
} // setup
void loop(){
uchar s[50];
gotoxy( 12, 5 );
fgets( s, 15, stdin );
gotoxy( 1, PANEL_H+6 );
puts( s );
} // loop
<!--[if !vml]-->
Таким образом, необходимо разработать специальную программу ввода редактирования текста в интерактивном режиме.
Общая структура подпрограммы (интерактивного цикла работы приложения) может быть представлена в виде следующей диаграммы деятельности:
<!--[if !vml]-->
Как мы видели на предыдущих примерах работы с подпрограммами консольного ввода-вывода ряд клавиш при нажатии вырабатываю "двойной" код – функция getch должна вызываться дважды.
Поскольку в нашей программе необходимо будет использовать специальные клавиши управления вводом (перемещения по вводимому тексту, забой символов, переход от заполнения одной формы к другой) всякий раз анализировать непосредственно эти коды неудобно.
В подобных случаях данные (в данном случае – коды клавиш) разбиваются на классы (подклассы), корорым присваиваются числовые индексы и дальнейшая работа производится исходя из индекса соответствующего класса символа и его (символа) кода.
Например: класс "навигации", класс "печатного символа" и т.п.
В С удобно использовать еще один тип данных языка С – перечисления.
Общая форма определения перечислений:
enum [identifier] { enumerator-list };
Примеры.
Вообще перечисления можно представить как задание некоторых поименованных констант.
enum color { black, red, green, yellow, blue, white=7, gray };
enum { OFF, ON } toggle = ON;
enum { OFF, ON, STOP = 0, GO = 1, CLOSED = 0, OPEN = 1 };
Теперь можно определить "классы" кодов клавиш (точнее – условные наименования типов клавиш – команд) и создать функцию, которая будет обрабатывать "нажатие" клавиш, вырабатывая код типа клавиши и, при необходимости , ее значение. ....
Определим типы клавиш (заметте – независимо от количестваа водимых символов на нажатие клавиши!):
M_ESC – закончить работу (27);
M_LEVO – сдвиг влево по тексту вводимой строки (224, 75);
M_PRAVO – сдвиг влево по тексту вводимой строки (224, 77);
M_DELETE – удалить текущий символ (224, 83);
M_BASCSP – удалить предыдущий символ (8);
M_TOP – переход на предыдущее поле (Ctrl-tab – 0, 148);
M_DOWN – переход на следующее поле (tab – 9);
M_CLEAR – нажатие ENTER (13 – для очистки полей ввода );
M_CHAR – печатный символ (ср866: 48-57, 65-122, 128-175, 224-245(239))
enum typeKlavr { M_ESC, M_LEVO, M_PRAVO, M_DELETE, M_BASCSP,
M_TOP, M_DOWN, M_CLEAR, M_CHAR };
Примечание. "Жирным" текстом выделены типы клавиш (команд), необходимых для "полноценного" редактирования вводимых полей. В нашем случае при разработке строкового редактора ввода ограничимся упрощенным редактором, позволяющим изменять (стирать) только последний символ в строке.
Теперь подпрограмма (функция) ввода с клавиатуры должна вырабатывать "класс" клавиши" и, для "допустимых" печатных символов, их код.
Как мы уже делали это ранее, связанные логически данные удобно представить в виде структуры. Например – такой:
typedef enum typeKlav { M_ESC, M_LEVO, M_PRAVO, M_DELETE, M_BASCSP,
M_TOP, M_DOWN, M_CLEAR, M_CHAR } TtypeKlav;
//typedef struct KodKav { enum typeKlav klav; uchar ch; } TkodKlav;
typedef struct KodKav { TtypeKlav klav; uchar ch; } TkodKlav;
//TkodKlav ku = { M_ESC, 27 }; // пример данных
Тут специально представлен вариант (выделено цветом) определения TkodKlav без использования конструкции typedef для определения перечисления типов клавиш.
Теперь определение подпрограммы ввода можно представить так:
TkodKlav getKlav();
А ее реализация:
TkodKlav getKlav(){
TkodKlav rez;
uchar c[2];
int n, k;
while ( !kbhit() ); // ожидание нажатия клавиши
n = 0;
while ( 1 ) {
while ( kbhit() ){
k = getch();
c[ n++ ] = k;
}
if ( n==1 ){
switch( c[0] ){
case 27 : rez.klav = M_ESC; return rez;
case 8 : rez.klav = M_BASCSP; return rez;
case 9 : rez.klav = M_DOWN; return rez;
case 13 : rez.klav = M_CLEAR; return rez;
default :
if ( isMyAlpha( c[0] )) {
rez.klav=M_CHAR; rez.ch=c[0]; return rez;
}
} // switch
} else {
if ( c[0]==224 ){
switch( c[1]){
case 75 : rez.klav = M_LEVO; return rez;
case 77 : rez.klav = M_PRAVO; return rez;
case 83 : rez.klav = M_DELETE; return rez;
}
} else if ( c[0]==0 && c[1]==148 )
rez.klav = M_TOP; return rez;
}
} // while
return rez;
} // TkodKlav
Задание: самостоятельно изобразите диаграмму деятельности этой подпрограммы.
Построим теперь подпрограммы редактирования поля ввода.
Опять главный вопрос: представление параметров ввода, с которыми работает подпрограмма редактирования.
Как говорилось ранее: если у нас есть хотя бы два похожих фрагмента – их необходимо объединить в одну подпрограмму!
Однако такая подпрограмма, почти наверняка, должна будет работать в различных условиях. Иначе – в разном контекстном окружении.
В данном случае это и положение на экране отображаемых при вводе символов и различная реакция на вводимые символы: при вводе логина – отображение (вывод) символов, а при вводе пароля – вывод условных обозначений.
Поэтому перед разработкой подпрограммы определим структуру "контекста".
Например, в данном случае, это может быть такая структура:
typedef struct KodKav { TtypeKlav klav; uchar ch; } TkodKlav;
typedef struct PoleVvoda { uchar posX, posY, s[20], maxLen,
curLen, posInStr, kodZameny; } TPoleVvoda;
У нас два поля ввода: логина и пароля.
Определим их контексты:
TPoleVvoda login={ .posX=12, .posY=5, .s="", .maxLen=15,
.curLen=0, .posInStr=0, .kodZameny=0 },
parol={ .posX=12, .posY=11, .s="", .maxLen=15,
.curLen=0, .posInStr=0, .kodZameny=249 };
Теперь подпрограммы работы с этими контекстами (не только редактирование, но и сброс – начальная установка контекста, установка позиции ввода – курсора) могут быть определены следующим образом:
void ustPos( TPoleVvoda * pole );
void sbrosPola( TPoleVvoda * pole );
void redPola( TPoleVvoda * pole, TkodKlav kod );
Построим их "болванки" и приступим к реализации главной части программы – подпрограммы loop.
Именно на основе постепенного подключения функционала к этой подпрограмме мы и будем отлаживать всю программную систему:
- получение данных клавиатуры и выход из подпрограммы;
- переход между полями ввода:
- ввод и редактирование данных.
void loop(){
uchar s[50], sost = 0;
TkodKlav kod;
gotoxy( 12, 5 ); // ??????
while( 1 ){
kod = getKlav();
if ( kod.klav==M_ESC ) break;
switch ( sost ){
case 0 : // vvod logina
if ( kod.klav==M_DOWN ) {
ustPos( &parol );
sost=1;
} else redPola( &login, kod );
break;
case 1 : // vvod parola
if ( kod.klav==M_DOWN ) {
gotoxy( 12, 18 );
sost=2;
} else if ( kod.klav==M_TOP ){
ustPos( &login );
sost=0;
} else redPola( &parol, kod );
break;
case 2 : // ogidanie sbrosa
if ( kod.klav==M_TOP ) {
ustPos( &parol );
sost=1;
} else if ( kod.klav==M_CLEAR ) {
sbrosPola( &login );
sbrosPola( &parol );
ustPos( &login );
sost=0;
}
break;
} // switch sost
}
gotoxy( 1, PANEL_H+4 );
login.s[ login.curLen ] = 0;
parol.s[ parol.curLen ] = 0;
puts( login.s );
puts( parol.s );
gotoxy( 1, PANEL_H+8 );
} // loop
Реализация подпрограмм работы к контекстом ввода данных:
void sbrosPola( TPoleVvoda * pole ){
// parol={ .posX=12, .posY=11, .s="", .maxLen=15, .posInStr=0, .curLen=0, .kodZameny=249 };
gotoxy( pole->posX, pole->posY );
while ( pole->curLen ){
putch( ' ' );
pole->curLen--;
}
pole->s[0] = 0; //pole->curLen=0;
pole->posInStr = 0;
} // sbrosPola
void ustPos( TPoleVvoda * pole ){
gotoxy( pole->posX+pole->curLen, pole->posY );
pole->posInStr = pole->curLen;
} // ustPos
void redPola( TPoleVvoda * pole, TkodKlav kod ){
switch( kod.klav ){
case M_LEVO :
break;
case M_PRAVO :
break;
case M_DELETE :
break;
case M_BASCSP :
if ( pole->curLen ){
pole->curLen--;
gotoxy( pole->posX+pole->curLen, pole->posY );
putch( ' ' );
gotoxy( pole->posX+pole->curLen, pole->posY );
}
break;
case M_CHAR :
if ( pole->curLen < pole->maxLen ){
pole->s[ pole->curLen++ ] = kod.ch;
if ( pole->kodZameny ) putch( pole->kodZameny );
else putch( kod.ch );
}
}
} // redPola