07.11.2015

Сквозь говнокод. Новые окошки для старого кода 1

При правильном рефакторинге пользователь не должен заметить разницы между версиями. Может мы избавимся от каких-либо ошибок, может даже добавим фич, но мы должны обеспечить полное сохранение функциональности текущей версии.  Пусть это убогий консольный интерфейс, но иногда пользователи не спешат переходить на Windows интерфейс, считая консольное решение более удобным!


Посмотрим, как работал прежний GUI: из main запускался shell, который содержит как отрисовку, так и обработку клавиш, а так же работу со списком. Оставлю ниже только основные моменты кода, остальное заменив комментариями

void shell()
{
/*
Инициализация списка и переменных
*/
// Хардкод на размер одной страницы - не удалил, для демонстрации "качества" кода
   for (int i = 0; i < 19; i++) 
   {
      ViewEnd = ViewEnd->next;
   }
//Рисование
   DrawWindow(1);
   window(2, 3, 79, 22);
   refrashViewBegin, Act); // Видимо перерисовывает область списка
// Основной цикл работы
   while (exitkey)
   {
//опрос клавиатуры
// Все коды клавиш в цифрах, хорошо, если помнишь, что 13 - Enter, а 27 - Esc, наверное :)
      if (menukey == 27)
         break;

      if (menukey == 80)
      {
/*Движение вниз по списку*/
         refrash(ViewBegin, Act);
      }

      if (menukey == 72)
      {
/*Движение вверх по списку*/
         refrash(ViewBegin, Act);
      }

      if (menukey == 13)
      {
/* Открываем окно а потом вновь перерисовываем*/
         DrawWindow(2);
         ViewMail(Act);
         window(2, 3, 79, 22);
         refrash(ViewBegin, Act);
      }
   }
};

Функция refresh не особо далеко ушла по качеству. Какие-то наборы чисел, позиционирования курсора, поэлементная обработка сишных строк и прочее мракобесие - даже не буду чистить - это надо сохранить для истории:)!

void refrash(mail *Begin, mail *Activ)
{
   clrscr();
   mail *elem;
   elem = new mail;
   elem = Begin;
   int index;
   char *adr, *avt, *tema;
   int i;

   for (i = 1; i < 21; i++)
   {
      textcolor(15);
      textbackground(9);
      if (elem == Activ)
         textcolor(4);
      if (elem != NULL)
      {
         gotoxy(1, i);
         for (index = 0; elem->adresat[index] != '\n' ; index++)
            cprintf("%c", elem->adresat[index]);
         gotoxy(20, i);
         cprintf(" | ");
         for (index = 0; elem->avtor[index] != '\n' ; index++)
            cprintf("%c", elem->avtor[index]);
         gotoxy(40, i);
         cprintf(" | ");
         for (index = 0; elem->tema[index] != '\n' ; index++)
            cprintf("%c", elem->tema[index]);
         puts("\n");
      }

      if (elem == NULL)
         i = 20;
      elem = elem->next;
   }
};

Дальше - веселей! У нас есть еще функция для отображения письма и некая рисовка окна.

void ViewMail(mail *Activ)
{
   int index;
   int x;
   char menukey;
   do {
      if (menukey == 27)
      {
         break;
      }
// И рисуем и по данным бегаем
      if (menukey == 77)
      {
         if (Activ->next != NULL)
            Activ = Activ->next;
      }
      if (menukey == 75)
      {
         if (Activ->prev != NULL)
            Activ = Activ->prev;
      }
/*Всякие позиционирования и поэлементный вывод данных - аналогично refresh*/
      menukey = getch();
   } while (1);
};

И напоследок самое вкусное! Если видишь в коде if() {...много кода..}else{..много похожего кода..}, то значит этот код следует переписать!

void DrawWindow(int p)
{
   int i;
   char ch;
   if (p == 1) // оказывается это режим рисования окна скроллинга
   {
      window(1, 1, 80, 25);
      textcolor(112);
      textbackground(15);
      ch = 205;
/*Всякие ухищрения по отрисовке рамок*/
      cprintf("Menu:Esc - Exit Enter - Open mail");
   }
   if (p == 2) // а это - окно самого письма
   {
      window(9, 9, 71, 23);
      textcolor(0);
      textbackground(15);
      ch = 205;
/*Рисуем рамку*/
      cprintf(" Menu: Esc - Exit Left/Right - Prev/Next Mail         ");
   }
};

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

Приступим к написанию нового GUI, чтобы наконец-таки удалось что-то скомпилить и поиграть в VS. Первым делом сделаем адаптер - обертку над функциями работы с консолью, чтобы через 5 лет не пришлось переписывать весь код из-за того, что какие-то функции или библиотеки исчезли
//Константы для цветов
const WORD BG_WHITE = BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED;
const WORD BG_BLUE  =  BACKGROUND_BLUE;
const WORD BG_GREEN =  BACKGROUND_GREEN;
const WORD TX_BLACK = 0;
const WORD TX_WHITE = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED ;
const WORD TX_RED   = FOREGROUND_RED | FOREGROUND_INTENSITY ;
const WORD TX_BLUE  = FOREGROUND_BLUE | FOREGROUND_INTENSITY ;
/* Рисовалка в консоли*/
class VSConsoleDraw
{
	struct FrameCharSet
	{
		char Horizontal;
		char Vertical;
		char AngleLeftTop;
		char AngleLeftBottom;
		char AngleRightTop;
		char AngleRightBottom;
	};
	HANDLE hOutput;
	COORD Pos;
	COORD NewSBSize;
	CONSOLE_SCREEN_BUFFER_INFO SCRN_INFO;
	WORD BackGround;
	WORD TextColor;
protected:
	FrameCharSet getFrameChar(int FrameType);
public:
	VSConsoleDraw();
        //Рисуем рамку
	void RectDraw(COORD BeginPoint, COORD EndPoint, int FrameType = fr_none);
	void RectDraw(COORD BeginPoint, COORD EndPoint, int FrameType, WORD Background, WORD Textcolor);
	void PrintLine(COORD BeginPoint,string text); // вывести текст от точки
	void Clear();
       //сразу добавим 3 типа рамок
	enum
	{
		fr_none = 0,
		fr_single = 1,
		fr_double = 2
	} _FrameType;
};

// и реализации методов
VSConsoleDraw::VSConsoleDraw()
{
	setlocale(LC_ALL,"RUS");
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	BackGround = BG_WHITE;
	TextColor = TX_BLACK|BackGround;
	NewSBSize.Y = 25;
	NewSBSize.X = 80;
	SetConsoleScreenBufferSize(hOutput,NewSBSize);
}
void VSConsoleDraw::Clear()
{
	GetConsoleScreenBufferInfo(hOutput, &SCRN_INFO);
	Pos.X = 0;
	Pos.Y = 0;
	FillConsoleOutputCharacter(hOutput, ' ', SCRN_INFO.dwSize.X*SCRN_INFO.dwSize.Y, Pos, NULL);
	FillConsoleOutputAttribute(hOutput, BackGround,SCRN_INFO.dwSize.X*SCRN_INFO.dwSize.Y,Pos,NULL);
	SetConsoleTextAttribute(hOutput,TextColor);
	SetConsoleCursorPosition(hOutput,Pos);
};

VSConsoleDraw::FrameCharSet VSConsoleDraw::getFrameChar(int FrameType)
{
	FrameCharSet retval;
	switch(FrameType)
	{
	case fr_single: 
		{
			retval.Horizontal = 196; 
			retval.Vertical = 179;
			retval.AngleLeftBottom = 192;
			retval.AngleLeftTop = 218;
			retval.AngleRightBottom = 217;
			retval.AngleRightTop = 191;
			break;
		}
	case fr_double: 
		{
			retval.Horizontal = 205; 
			retval.Vertical = 186;
			retval.AngleLeftBottom = 200;
			retval.AngleLeftTop = 201;
			retval.AngleRightBottom = 188;
			retval.AngleRightTop = 187;
			break;
		}
	default:
		{ 
			retval.Horizontal = 32; 
			retval.Vertical = 32;
			retval.AngleLeftBottom = 32;
			retval.AngleLeftTop = 32;
			retval.AngleRightBottom = 32;
			retval.AngleRightTop = 32;
			break;
		}
	}
	return retval;
};

void VSConsoleDraw::RectDraw(COORD BeginPoint, COORD EndPoint, int FrameType)
{
	//сменим локаль для получения ASCII графики
	setlocale(LC_ALL,"C");
	
	FrameCharSet FrameChar = getFrameChar(FrameType);
	COORD Cursor1, Cursor2;
	Cursor1.X = Cursor2.X = BeginPoint.X+1;
	Cursor1.Y = BeginPoint.Y;
	Cursor2.Y = EndPoint.Y;

	for(int i = Cursor1.X ; i< EndPoint.X ; i++)
	{
		SetConsoleCursorPosition(hOutput,Cursor1);
		printf("%c",FrameChar.Horizontal);
		Cursor1.X++;
		SetConsoleCursorPosition(hOutput,Cursor2);
		printf("%c",FrameChar.Horizontal);
		Cursor2.X++;
	}
	Cursor1.Y = Cursor2.Y = BeginPoint.Y+1;
	Cursor1.X = BeginPoint.X;
	Cursor2.X = EndPoint.X;

	for(int i = Cursor1.Y; i < EndPoint.Y ; i++)
	{
		SetConsoleCursorPosition(hOutput,Cursor1);
		printf("%c",FrameChar.Vertical);
		Cursor1.Y++;
		SetConsoleCursorPosition(hOutput,Cursor2);
		printf("%c",FrameChar.Vertical);
		Cursor2.Y++;
	}
//Рисуем углы
	SetConsoleCursorPosition(hOutput,BeginPoint);
	printf("%c",FrameChar.AngleLeftTop);	
	Cursor1.X = BeginPoint.X; Cursor1.Y = EndPoint.Y;
	SetConsoleCursorPosition(hOutput,Cursor1);
	printf("%c",FrameChar.AngleLeftBottom);	
	Cursor1.X = EndPoint.X; Cursor1.Y = BeginPoint.Y;
	SetConsoleCursorPosition(hOutput,Cursor1);
	printf("%c",FrameChar.AngleRightTop);	
	SetConsoleCursorPosition(hOutput,EndPoint);
	printf("%c",FrameChar.AngleRightBottom);	
	
	//Вернем курсор туда, где стоял
	SetConsoleCursorPosition(hOutput,Pos);
	setlocale(LC_ALL,"RUS");
}
void VSConsoleDraw::RectDraw(COORD BeginPoint, COORD EndPoint, int FrameType, WORD Background, WORD Textcolor)
{
	SetConsoleTextAttribute(hOutput,Textcolor|Background);
	RectDraw(BeginPoint, EndPoint,FrameType);
	SetConsoleTextAttribute(hOutput,this->TextColor);
};

void VSConsoleDraw::PrintLine(COORD BeginPoint,string text)
{
SetConsoleCursorPosition(hOutput,BeginPoint);
printf("%s",text.c_str());
SetConsoleCursorPosition(hOutput,Pos);
}

Когда готов каркас, можно проверить простоту использования данной рисовалки:

void main()
{
        VSConsoleDraw console;
console.Clear(); COORD Point1, Point2; Point1.X = 0; Point1.Y = 1; Point2.X = 39; Point2.Y = 24; console.RectDraw(Point1,Point2,VSConsoleDraw::fr_double); Point1.X = 41; Point1.Y = 1; Point2.X = 75; Point2.Y = 20; console.RectDraw(Point1,Point2,VSConsoleDraw::fr_single,BG_GREEN,TX_BLACK); Point1.X = 5; Point1.Y = 5; Point2.X = 35; Point2.Y = 15; console.RectDraw(Point1,Point2,VSConsoleDraw::fr_single,BG_BLUE,TX_WHITE); }
Результат работы:

А теперь сделаем базовый класс для создания панелей и скроллингов. VisualElement будет содержать инстанс VSConsoleDraw, что позволит, при необходимости, переопределять различные свойства рисовалки для каждого элемента


//Объявление
class VisualElement
{
	string Caption;
	string Menu;
	COORD Top;
	COORD Bottom;
	int Frame;
	int BackGround;
	int TextColor;
	VSConsoleDraw Console;
public:
	VisualElement(void) {;}
	VisualElement(COORD BeginPoint, COORD EndPoint, int FrameType,string Caption,string Menu);
	~VisualElement(void) {;}
	void virtual View();
	void virtual Run() ;

};
//реализация
	VisualElement::VisualElement(COORD BeginPoint, COORD EndPoint, int FrameType,string Caption,string Menu)
	{
		Console = VSConsoleDraw::VSConsoleDraw();
		Top = BeginPoint;
		Bottom = EndPoint;
		Frame = FrameType;
		this->Caption = Caption;
		this->Menu = Menu;
	};
	void VisualElement::View() 
	{
		Console.RectDraw(Top,Bottom,Frame);
		COORD CaptionPoint; CaptionPoint.X = Top.X+2; CaptionPoint.Y=Top.Y;
		Console.PrintLine(CaptionPoint,Caption);
		COORD MenuPoint; MenuPoint.X = Top.X+2; MenuPoint.Y=Bottom.Y;
		Console.PrintLine(MenuPoint,Menu);
	};
	void VisualElement::Run() {};

Метод Run пока, что оставим пустым, в дальнейшем вернемся к нему. Теперь можно удостовериться, что все работает так, как задумано:

void main()
{
        VSConsoleDraw console;
	console.Clear();
	COORD Point1, Point2;
	Point1.X = 0; Point1.Y = 1;
	Point2.X = 39; Point2.Y = 24;
	VisualElement elem(Point1,Point2,VSConsoleDraw::fr_double,"Заголовок левой области","Меню F1 - Справка Enter - Выбор");
	Point1.X = 40; Point1.Y = 1;
	Point2.X = 79; Point2.Y = 10;
	VisualElement elem2(Point1,Point2,VSConsoleDraw::fr_single,"Заголовок правой верхней области","PageUp/Down - прокрутка");
	Point1.X = 40; Point1.Y = 11;
	Point2.X = 79; Point2.Y = 23;
	VisualElement elem3(Point1,Point2,VSConsoleDraw::fr_single,"Заголовок правой нижней области","F4 - Редактировать F5 - Копировать");
	elem.View();
	elem2.View();
	elem3.View();
}


На этом можно закончить. Далее можно реализовать Классы Скроллинга и Панели, т.е. научиться выводить в полученные области данные.

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

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